Page MenuHomePhorge

No OneTemporary

Size
542 KB
Referenced Files
None
Subscribers
None
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/changelog.d/bookmark-folders.ignore b/changelog.d/bookmark-folders.ignore
new file mode 100644
index 000000000..8705ac00b
--- /dev/null
+++ b/changelog.d/bookmark-folders.ignore
@@ -0,0 +1 @@
+Various bookmark folders-related improvements
diff --git a/changelog.d/gopher-genserver-crash-on-boot.fix b/changelog.d/gopher-genserver-crash-on-boot.fix
new file mode 100644
index 000000000..3b51662be
--- /dev/null
+++ b/changelog.d/gopher-genserver-crash-on-boot.fix
@@ -0,0 +1 @@
+Gopher: Fix Ranch listener not being stopped properly on Pleroma restart when database configuration is enabled
diff --git a/changelog.d/plug-test-typo.skip b/changelog.d/plug-test-typo.skip
new file mode 100644
index 000000000..e69de29bb
diff --git a/changelog.d/twitter-api.skip b/changelog.d/twitter-api.skip
new file mode 100644
index 000000000..e69de29bb
diff --git a/changelog.d/update-comment.ignore b/changelog.d/update-comment.ignore
new file mode 100644
index 000000000..733e813b3
--- /dev/null
+++ b/changelog.d/update-comment.ignore
@@ -0,0 +1 @@
+Update comment for prepare_object, rename prepare_outgoing
diff --git a/docs/development/API/differences_in_mastoapi_responses.md b/docs/development/API/differences_in_mastoapi_responses.md
index 358d05bf4..194c98e3f 100644
--- a/docs/development/API/differences_in_mastoapi_responses.md
+++ b/docs/development/API/differences_in_mastoapi_responses.md
@@ -1,526 +1,526 @@
# Differences in Mastodon API responses from vanilla Mastodon
A Pleroma instance can be identified by "<Mastodon version> (compatible; Pleroma <version>)" present in `version` field in response from `/api/v1/instance` and `/api/v2/instance`
## Flake IDs
Pleroma uses 128-bit ids as opposed to Mastodon's 64 bits. However, just like Mastodon's ids, they are lexically sortable strings
## Timelines
Adding the parameter `with_muted=true` to the timeline queries will also return activities by muted (not by blocked!) users.
Adding the parameter `exclude_visibilities` to the timeline queries will exclude the statuses with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`), e.g., `exclude_visibilities[]=direct&exclude_visibilities[]=private`.
Adding the parameter `reply_visibility` to the public and home timelines queries will filter replies. Possible values: without parameter (default) shows all replies, `following` - replies directed to you or users you follow, `self` - replies directed to you.
Adding the parameter `instance=lain.com` to the public timeline will show only statuses originating from `lain.com` (or any remote instance).
Home, public, hashtag & list timelines accept these parameters:
- `only_media`: show only statuses with media attached
- `local`: show only local statuses
- `remote`: show only remote statuses
## Statuses
- `visibility`: has additional possible values `list` and `local` (for local-only statuses)
Has these additional fields under the `pleroma` object:
- `local`: true if the post was made on the local instance
- `conversation_id`: the ID of the AP context the status is associated with (if any)
- `direct_conversation_id`: the ID of the Mastodon direct message conversation the status is associated with (if any)
- `in_reply_to_account_acct`: the `acct` property of User entity for replied user (if any)
- `content`: a map consisting of alternate representations of the `content` property with the key being its 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 its 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
- `thread_muted`: true if the thread the post belongs to is muted
- `emoji_reactions`: A list with emoji / reaction maps. The format is `{name: "☕", count: 1, me: true}`. Contains no information about the reacting users, for that use the `/statuses/:id/reactions` endpoint.
- `parent_visible`: If the parent of this post is visible to the user or not.
- `pinned_at`: a datetime (iso8601) when status was pinned, `null` otherwise.
- `bookmark_folder`: the ID of the folder bookmark is stored within (if any).
- `list_id`: the ID of the list the post is addressed to (if any, only returned to author).
Has these additional fields under the `poll.pleroma` object:
- `non_anonymous`: true if the source post specifies the poll results are not anonymous. Currently only implemented by Smithereen.
The `GET /api/v1/statuses/:id/source` endpoint additionally has the following attributes:
- `content_type`: The content type of the status source.
## Scheduled statuses
Has these additional fields in `params`:
- `expires_in`: the number of seconds the posted activity should expire in.
## Media Attachments
Has these additional fields under the `pleroma` object:
- `mime_type`: mime type of the attachment.
### Attachment cap
Some apps operate under the assumption that no more than 4 attachments can be returned or uploaded. Pleroma however does not enforce any limits on attachment count neither when returning the status object nor when posting.
### Limitations
Pleroma does not process remote images and therefore cannot include fields such as `meta` and `blurhash`. It does not support focal points or aspect ratios. The frontend is expected to handle it.
## Bookmarks
The `GET /api/v1/bookmarks` endpoint accepts optional parameter `folder_id` for bookmark folder ID.
-The `POST /api/v1/statuses/:id/bookmark` endpoint accepts optional parameter `folder_id` for bookmark folder ID.
+The `POST /api/v1/statuses/:id/bookmark` endpoint accepts optional parameter `folder_id` for bookmark folder ID. Bookmarking an already bookmarked post will update the folder association, or remove it if `folder_id` is omitted or `null`.
## Accounts
The `id` parameter can also be the `nickname` of the user. This only works in these endpoints, not the deeper nested ones for following etc.
- `/api/v1/accounts/:id`
- `/api/v1/accounts/:id/statuses`
`/api/v1/accounts/:id/statuses` endpoint accepts these parameters:
- `pinned`: include only pinned statuses
- `tagged`: with tag
- `only_media`: include only statuses with media attached
- `with_muted`: include statuses/reactions from muted accounts
- `exclude_reblogs`: exclude reblogs
- `only_reblogs`: include only reblogs
- `exclude_replies`: exclude replies
- `exclude_visibilities`: exclude visibilities
Endpoints which accept `with_relationships` parameter:
- `/api/v1/accounts/:id`
- `/api/v1/accounts/:id/followers`
- `/api/v1/accounts/:id/following`
- `/api/v1/mutes`
- `/api/v1/blocks`
- `/api/v1/search`
- `/api/v2/search`
Has these additional fields under the `pleroma` object:
- `ap_id`: nullable URL string, ActivityPub id of the user
- `background_image`: nullable URL string, background image of the user
- `tags`: Lists an array of tags for the user
- `relationship` (object): Includes fields as documented for Mastodon API https://docs.joinmastodon.org/entities/relationship/
- `is_moderator`: boolean, nullable, true if user is a moderator
- `is_admin`: boolean, nullable, true if user is an admin
- `confirmation_pending`: boolean, true if a new user account is waiting on email confirmation to be activated
- `hide_favorites`: boolean, true when the user has hiding favorites enabled
- `hide_followers`: boolean, true when the user has follower hiding enabled
- `hide_follows`: boolean, true when the user has follow hiding enabled
- `hide_followers_count`: boolean, true when the user has follower stat hiding enabled
- `hide_follows_count`: boolean, true when the user has follow stat hiding enabled
- `settings_store`: A generic map of settings for frontends. Opaque to the backend. Only returned in `/api/v1/accounts/verify_credentials` and `/api/v1/accounts/update_credentials`
- `chat_token`: The token needed for Pleroma shoutbox. Only returned in `/api/v1/accounts/verify_credentials`
- `deactivated`: boolean, true when the user is deactivated
- `allow_following_move`: boolean, true when the user allows automatically follow moved following accounts
- `unread_conversation_count`: The count of unread conversations. Only returned to the account owner.
- `unread_notifications_count`: The count of unread notifications. Only returned to the account owner.
- `notification_settings`: object, can be absent. See `/api/v1/pleroma/notification_settings` for the parameters/keys returned.
- `accepts_chat_messages`: boolean, but can be null if we don't have that information about a user
- `favicon`: nullable URL string, Favicon image of the user's instance
### Source
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
- `discoverable`: boolean, true when the user allows external services (search bots) etc. to index / list the account (regardless of this setting, user will still appear in regular search results)
- `actor_type`: string, the type of this account.
## 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.
## GET `/api/v1/conversations`
Accepts additional parameters:
- `recipients`: Only return conversations with the given recipients (a list of user ids). Usage example: `GET /api/v1/conversations?recipients[]=1&recipients[]=2`
## Account Search
Behavior has changed:
- `/api/v1/accounts/search`: Does not require authentication
## Search (global)
Unlisted posts are available in search results, they are considered to be public posts that shouldn't be shown in local/federated timeline.
## Notifications
Has these additional fields under the `pleroma` object:
- `is_seen`: true if the notification was read by the user
### Move Notification
The `type` value is `move`. Has an additional field:
- `target`: new account
### EmojiReact Notification
The `type` value is `pleroma:emoji_reaction`. Has these fields:
- `emoji`: The used emoji
- `account`: The account of the user who reacted
- `status`: The status that was reacted on
### ChatMention Notification (not default)
This notification has to be requested explicitly.
The `type` value is `pleroma:chat_mention`
- `account`: The account who sent the message
- `chat_message`: The chat message
### Report Notification (not default)
This notification has to be requested explicitly.
The `type` value is `pleroma:report`
- `account`: The account who reported
- `report`: The report
## GET `/api/v1/notifications`
Accepts additional parameters:
- `exclude_visibilities`: will exclude the notifications for activities with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`). Usage example: `GET /api/v1/notifications?exclude_visibilities[]=direct&exclude_visibilities[]=private`.
- `include_types`: will include the notifications for activities with the given types. The parameter accepts an array of types (`mention`, `follow`, `reblog`, `favourite`, `move`, `pleroma:emoji_reaction`, `pleroma:chat_mention`, `pleroma:report`). Usage example: `GET /api/v1/notifications?include_types[]=mention&include_types[]=reblog`.
## DELETE `/api/v1/notifications/destroy_multiple`
An endpoint to delete multiple statuses by IDs.
Required parameters:
- `ids`: array of activity ids
Usage example: `DELETE /api/v1/notifications/destroy_multiple/?ids[]=1&ids[]=2`.
Returns on success: 200 OK `{}`
## POST `/api/v1/statuses`
Additional parameters can be added to the JSON body/Form data:
- `preview`: boolean, if set to `true` the post won't be actually posted, but the status entity would still be rendered back. This could be useful for previewing rich text/custom emoji, for example.
- `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 post visibility are not affected by this and will still apply.
- `visibility`: string, besides standard MastoAPI values (`direct`, `private`, `unlisted`, `local` 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`.
## GET `/api/v1/statuses`
An endpoint to get multiple statuses by IDs.
Required parameters:
- `ids`: array of activity ids
Usage example: `GET /api/v1/statuses/?ids[]=1&ids[]=2`.
Returns: array of Status.
The maximum number of statuses is limited to 100 per request.
## PATCH `/api/v1/accounts/update_credentials`
Additional parameters can be added to the JSON body/Form data:
- `no_rich_text` - if true, html tags are stripped from all statuses requested from the API
- `hide_followers` - if true, user's followers will be hidden
- `hide_follows` - if true, user's follows will be hidden
- `hide_followers_count` - if true, user's follower count will be hidden
- `hide_follows_count` - if true, user's follow count will be hidden
- `hide_favorites` - if true, user's favorites timeline will be hidden
- `show_role` - if true, user's role (e.g admin, moderator) will be exposed to anyone in the API
- `default_scope` - the scope returned under `privacy` key in Source subentity
- `pleroma_settings_store` - Opaque user settings to be saved on the backend.
- `skip_thread_containment` - if true, skip filtering out broken threads
- `allow_following_move` - if true, allows automatically follow moved following accounts
- `also_known_as` - array of ActivityPub IDs, needed for following move
- `pleroma_background_image` - sets the background image of the user. Can be set to "" (an empty string) to reset.
- `discoverable` - if true, external services (search bots) etc. are allowed to index / list the account (regardless of this setting, user will still appear in regular search results).
- `actor_type` - the type of this account.
- `accepts_chat_messages` - if false, this account will reject all chat messages.
- `language` - user's preferred language for receiving emails (digest, confirmation, etc.)
- `avatar_description` - image description for user avatar
- `header_description` - image description for user banner
All images (avatar, banner and background) can be reset to the default by sending an empty string ("") instead of a file.
### Pleroma Settings Store
Pleroma has mechanism that allows frontends to save blobs of json for each user on the backend. This can be used to save frontend-specific settings for a user that the backend does not need to know about.
The parameter should have a form of `{frontend_name: {...}}`, with `frontend_name` identifying your type of client, e.g. `pleroma_fe`. It will overwrite everything under this property, but will not overwrite other frontend's settings.
This information is returned in the `/api/v1/accounts/verify_credentials` endpoint.
## Authentication
*Pleroma supports refreshing tokens.*
### POST `/oauth/token`
You can obtain access tokens for a user in a few additional ways.
#### Refreshing a token
To obtain a new access token from a refresh token, pass `grant_type=refresh_token` with the following extra parameters:
- `refresh_token`: The refresh token.
#### Getting a token with a password
To obtain a token from a user's password, pass `grant_type=password` with the following extra parameters:
- `username`: Username to authenticate.
- `password`: The user's password.
#### Response body
Additional fields are returned in the response:
- `id`: The primary key of this token in Pleroma's database.
- `me` (user tokens only): The ActivityPub ID of the user who owns the token.
## Account Registration
`POST /api/v1/accounts`
Has these additional parameters (which are the same as in Pleroma-API):
- `fullname`: optional
- `bio`: optional
- `captcha_solution`: optional, contains provider-specific captcha solution,
- `captcha_token`: optional, contains provider-specific captcha token
- `captcha_answer_data`: optional, contains provider-specific captcha data
- `token`: invite token required when the registrations aren't public.
- `language`: optional, user's preferred language for receiving emails (digest, confirmation, etc.), default to the language set in the `userLanguage` cookies or `Accept-Language` header.
## Instance
`GET /api/v1/instance` has additional fields
- `max_toot_chars`: The maximum characters per post
- `max_media_attachments`: Maximum number of post media attachments
- `chat_limit`: The maximum characters per chat message
- `description_limit`: The maximum characters per image description
- `poll_limits`: The limits of polls
- `shout_limit`: The maximum characters per Shoutbox message
- `upload_limit`: The maximum upload file size
- `avatar_upload_limit`: The same for avatars
- `background_upload_limit`: The same for backgrounds
- `banner_upload_limit`: The same for banners
- `background_image`: A background image that frontends can use
- `pleroma.metadata.account_activation_required`: Whether users are required to confirm their emails before signing in
- `pleroma.metadata.birthday_required`: Whether users are required to provide their birth day when signing in
- `pleroma.metadata.birthday_min_age`: The minimum user age (in days)
- `pleroma.metadata.features`: A list of supported features
- `pleroma.metadata.federation`: The federation restrictions of this instance
- `pleroma.metadata.fields_limits`: A list of values detailing the length and count limitation for various instance-configurable fields.
- `pleroma.metadata.post_formats`: A list of the allowed post format types
- `pleroma.stats.mau`: Monthly active user count
- `pleroma.vapid_public_key`: The public key needed for push messages
In, `GET /api/v2/instance` Pleroma-specific fields are all moved into `pleroma` object. `max_toot_chars`, `poll_limits` and `upload_limit` are replaced with their MastoAPI counterparts.
## Push Subscription
`POST /api/v1/push/subscription`
`PUT /api/v1/push/subscription`
Permits these additional alert types:
- pleroma:chat_mention
- pleroma:emoji_reaction
## Markers
Has these additional fields under the `pleroma` object:
- `unread_count`: contains number unread notifications
## Streaming
### Chats
There is an additional `user:pleroma_chat` stream. Incoming chat messages will make the current chat be sent to this `user` stream. The `event` of an incoming chat message is `pleroma:chat_update`. The payload is the updated chat with the incoming chat message in the `last_message` field.
### Remote timelines
For viewing remote server timelines, there are `public:remote` and `public:remote:media` streams. Each of these accept a parameter like `?instance=lain.com`.
### Follow relationships updates
Pleroma streams follow relationships updates as `pleroma:follow_relationships_update` events to the `user` stream.
The message payload consist of:
- `state`: a relationship state, one of `follow_pending`, `follow_accept` or `follow_reject`.
- `follower` and `following` maps with following fields:
- `id`: user ID
- `follower_count`: follower count
- `following_count`: following count
### Authenticating via `sec-websocket-protocol` header
Pleroma allows to authenticate via the `sec-websocket-protocol` header, for example, if your access token is `your-access-token`, you can authenticate using the following:
```
sec-websocket-protocol: your-access-token
```
### Authenticating after connection via `pleroma:authenticate` event
Pleroma allows to authenticate after connection is established, via the `pleroma:authenticate` event. For example, if your access token is `your-access-token`, you can send the following after the connection is established:
```
{"type": "pleroma:authenticate", "token": "your-access-token"}
```
### Response to client-sent events
Pleroma will respond to client-sent events that it recognizes. Supported event types are:
- `subscribe`
- `unsubscribe`
- `pleroma:authenticate`
The reply will be in the following format:
```
{
"event": "pleroma:respond",
"payload": "{\"type\": \"<type of the client-sent event>\", \"result\": \"<result of the action>\", \"error\": \"<error code>\"}"
}
```
Result of the action can be either `success`, `ignored` or `error`. If it is `error`, the `error` property will contain the error code. Otherwise, the `error` property will not be present. Below are some examples:
```
{
"event": "pleroma:respond",
"payload": "{\"type\": \"pleroma:authenticate\", \"result\": \"success\"}"
}
{
"event": "pleroma:respond",
"payload": "{\"type\": \"subscribe\", \"result\": \"ignored\"}"
}
{
"event": "pleroma:respond",
"payload": "{\"type\": \"unsubscribe\", \"result\": \"error\", \"error\": \"bad_topic\"}"
}
```
If the sent event is not of a type that Pleroma supports, it will not reply.
### The `stream` attribute of a server-sent event
Technically, this is in Mastodon, but its documentation does nothing to specify its format.
This attribute appears on every event type except `pleroma:respond` and `delete`. It helps clients determine where they should display the new statuses.
The value of the attribute is an array containing one or two elements. The first element is the type of the stream. The second is the identifier related to that specific stream, if applicable.
For the following stream types, there is a second element in the array:
- `list`: The second element is the id of the list, as a string.
- `hashtag`: The second element is the name of the hashtag.
- `public:remote:media` and `public:remote`: The second element is the domain of the corresponding instance.
For all other stream types, there is no second element.
Some examples of valid `stream` values:
- `["list", "1"]`: List of id 1.
- `["hashtag", "mew"]`: The hashtag #mew.
- `["user:notifications"]`: Notifications for the current user.
- `["user"]`: Home timeline.
- `["public:remote", "mew.moe"]`: Public posts from the instance mew.moe .
### The unified streaming endpoint
If you do not specify a stream to connect to when requesting `/api/v1/streaming`, you will enter a connection that subscribes to no streams. After the connection is established, you can authenticate and then subscribe to different streams.
### List of supported streams
Below is a list of supported streams by Pleroma. To make a single-stream WebSocket connection, append the string specified in "Query style" to the streaming endpoint url.
To subscribe to a stream after the connection is established, merge the JSON object specified in "Subscribe style" with `{"type": "subscribe"}`. To unsubscribe, merge it with `{"type": "unsubscribe"}`.
For example, to receive updates on the list 1, you can connect to `/api/v1/streaming/?stream=list&list=1`, or send
```
{"type": "subscribe", "stream": "list", "list": "1"}
```
upon establishing the websocket connection.
To unsubscribe to list 1, send
```
{"type": "unsubscribe", "stream": "list", "list": "1"}
```
Note that if you specify a stream that requires a logged-in user in the query string (for example, `user` or `list`), you have to specify the access token when you are trying to establish the connection, i.e. in the query string or via the `sec-websocket-protocol` header.
- `list`
- Query style: `?stream=list&list=<id>`
- Subscribe style: `{"stream": "list", "list": "<id>"}`
- `public`, `public:local`, `public:media`, `public:local:media`, `user`, `user:pleroma_chat`, `user:notifications`, `direct`
- Query style: `?stream=<stream name>`
- Subscribe style: `{"stream": "<stream name>"}`
- `hashtag`
- Query style: `?stream=hashtag&tag=<name>`
- Subscribe style: `{"stream": "hashtag", "tag": "<name>"}`
- `public:remote`, `public:remote:media`
- Query style: `?stream=<stream name>&instance=<instance domain>`
- Subscribe style: `{"stream": "<stream name>", "instance": "<instance domain>"}`
## User muting and thread muting
Both user muting and thread muting can be done for only a certain time by adding an `expires_in` parameter to the API calls and giving the expiration time in seconds.
## Not implemented
Pleroma is generally compatible with the Mastodon 2.7.2 API, but some newer features and non-essential features are omitted. These features usually return an HTTP 200 status code, but with an empty response. While they may be added in the future, they are considered low priority.
### Suggestions
*Added in Mastodon 2.4.3*
- `GET /api/v1/suggestions`: Returns an empty array, `[]`
### Trends
*Added in Mastodon 3.0.0*
- `GET /api/v1/trends`: Returns an empty array, `[]`
### Featured tags
*Added in Mastodon 3.0.0*
- `GET /api/v1/featured_tags`: Returns HTTP 404
diff --git a/lib/pleroma/bookmark.ex b/lib/pleroma/bookmark.ex
index 1a2a63b82..1c78d495f 100644
--- a/lib/pleroma/bookmark.ex
+++ b/lib/pleroma/bookmark.ex
@@ -1,86 +1,79 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Bookmark do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.BookmarkFolder
alias Pleroma.Repo
alias Pleroma.User
@type t :: %__MODULE__{}
schema "bookmarks" do
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
- belongs_to(:folder, BookmarkFolder, type: FlakeId.Ecto.CompatType)
+ belongs_to(:folder, BookmarkFolder, type: FlakeId.Ecto.Type)
timestamps()
end
@spec create(Ecto.UUID.t(), Ecto.UUID.t()) ::
{:ok, Bookmark.t()} | {:error, Ecto.Changeset.t()}
def create(user_id, activity_id, folder_id \\ nil) do
attrs = %{
user_id: user_id,
activity_id: activity_id,
folder_id: folder_id
}
%Bookmark{}
|> cast(attrs, [:user_id, :activity_id, :folder_id])
|> validate_required([:user_id, :activity_id])
|> unique_constraint(:activity_id, name: :bookmarks_user_id_activity_id_index)
|> Repo.insert(
- on_conflict: [set: [folder_id: folder_id]],
+ on_conflict: [set: [folder_id: folder_id, updated_at: NaiveDateTime.utc_now()]],
conflict_target: [:user_id, :activity_id]
)
end
@spec for_user_query(Ecto.UUID.t()) :: Ecto.Query.t()
def for_user_query(user_id, folder_id \\ nil) do
Bookmark
|> where(user_id: ^user_id)
|> maybe_filter_by_folder(folder_id)
|> join(:inner, [b], activity in assoc(b, :activity))
|> preload([b, a], activity: a)
end
defp maybe_filter_by_folder(query, nil), do: query
defp maybe_filter_by_folder(query, folder_id) do
query
|> where(folder_id: ^folder_id)
end
def get(user_id, activity_id) do
Bookmark
|> where(user_id: ^user_id)
|> where(activity_id: ^activity_id)
|> Repo.one()
end
@spec destroy(Ecto.UUID.t(), Ecto.UUID.t()) ::
{:ok, Bookmark.t()} | {:error, Ecto.Changeset.t()}
def destroy(user_id, activity_id) do
from(b in Bookmark,
where: b.user_id == ^user_id,
where: b.activity_id == ^activity_id
)
|> Repo.one()
|> Repo.delete()
end
-
- def set_folder(bookmark, folder_id) do
- bookmark
- |> cast(%{folder_id: folder_id}, [:folder_id])
- |> validate_required([:folder_id])
- |> Repo.update()
- end
end
diff --git a/lib/pleroma/bookmark_folder.ex b/lib/pleroma/bookmark_folder.ex
index 14d37e197..65856bb29 100644
--- a/lib/pleroma/bookmark_folder.ex
+++ b/lib/pleroma/bookmark_folder.ex
@@ -1,115 +1,115 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.BookmarkFolder do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
alias Pleroma.BookmarkFolder
alias Pleroma.Emoji
alias Pleroma.Repo
alias Pleroma.User
@type t :: %__MODULE__{}
- @primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
+ @primary_key {:id, FlakeId.Ecto.Type, autogenerate: true}
schema "bookmark_folders" do
field(:name, :string)
field(:emoji, :string)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
timestamps()
end
def get_by_id(id), do: Repo.get_by(BookmarkFolder, id: id)
def create(user_id, name, emoji \\ nil) do
%BookmarkFolder{}
|> cast(
%{
user_id: user_id,
name: name,
emoji: emoji
},
[:user_id, :name, :emoji]
)
|> validate_required([:user_id, :name])
|> fix_emoji()
|> validate_emoji()
|> unique_constraint([:user_id, :name])
|> Repo.insert()
end
def update(folder_id, name, emoji \\ nil) do
get_by_id(folder_id)
|> cast(
%{
name: name,
emoji: emoji
},
[:name, :emoji]
)
|> fix_emoji()
|> validate_emoji()
|> unique_constraint([:user_id, :name])
|> Repo.update()
end
defp fix_emoji(changeset) do
with {:emoji_field, emoji} when is_binary(emoji) <-
{:emoji_field, get_field(changeset, :emoji)},
{:fixed_emoji, emoji} <-
{:fixed_emoji,
emoji
|> Pleroma.Emoji.fully_qualify_emoji()
|> Pleroma.Emoji.maybe_quote()} do
put_change(changeset, :emoji, emoji)
else
{:emoji_field, _} -> changeset
end
end
defp validate_emoji(changeset) do
validate_change(changeset, :emoji, fn
:emoji, nil ->
[]
:emoji, emoji ->
if Emoji.unicode?(emoji) or valid_local_custom_emoji?(emoji) do
[]
else
[emoji: "Invalid emoji"]
end
end)
end
defp valid_local_custom_emoji?(emoji) do
with %{file: _path} <- Emoji.get(emoji) do
true
else
_ -> false
end
end
def delete(folder_id) do
BookmarkFolder
|> Repo.get_by(id: folder_id)
|> Repo.delete()
end
def for_user(user_id) do
BookmarkFolder
|> where(user_id: ^user_id)
|> Repo.all()
end
def belongs_to_user?(folder_id, user_id) do
BookmarkFolder
|> where(id: ^folder_id, user_id: ^user_id)
|> Repo.exists?()
end
end
diff --git a/lib/pleroma/gopher/server.ex b/lib/pleroma/gopher/server.ex
index add3ba925..7b6985efe 100644
--- a/lib/pleroma/gopher/server.ex
+++ b/lib/pleroma/gopher/server.ex
@@ -1,166 +1,173 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Gopher.Server do
use GenServer
require Logger
def start_link(_) do
config = Pleroma.Config.get(:gopher, [])
ip = Keyword.get(config, :ip, {0, 0, 0, 0})
port = Keyword.get(config, :port, 1234)
if Keyword.get(config, :enabled, false) do
GenServer.start_link(__MODULE__, [ip, port], [])
else
Logger.info("Gopher server disabled")
:ignore
end
end
def init([ip, port]) do
Logger.info("Starting gopher server on #{port}")
+ Process.flag(:trap_exit, true)
+
+ listener = :gopher
{:ok, _pid} =
:ranch.start_listener(
- :gopher,
+ listener,
:ranch_tcp,
%{
num_acceptors: 100,
max_connections: 100,
socket_opts: [ip: ip, port: port]
},
__MODULE__.ProtocolHandler,
[]
)
- {:ok, %{ip: ip, port: port}}
+ {:ok, %{ip: ip, port: port, listener: listener}}
+ end
+
+ def terminate(_reason, state) do
+ :ranch.stop_listener(state.listener)
end
end
defmodule Pleroma.Gopher.Server.ProtocolHandler do
alias Pleroma.Activity
alias Pleroma.HTML
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility
def start_link(ref, transport, opts) do
pid = spawn_link(__MODULE__, :init, [ref, transport, opts])
{:ok, pid}
end
def init(ref, transport, opts \\ []) do
{:ok, socket} = :ranch.handshake(ref, opts)
loop(socket, transport)
end
def info(text) do
text = String.replace(text, ~r/[\t\n]/, "")
String.split(text, "\r")
|> Enum.map(fn text ->
"i#{text}\tfake\t(NULL)\t0\r\n"
end)
|> Enum.join("")
end
def link(name, selector, type \\ 1) do
address = Pleroma.Web.Endpoint.host()
port = Pleroma.Config.get([:gopher, :port], 1234)
dstport = Pleroma.Config.get([:gopher, :dstport], port)
"#{type}#{name}\t#{selector}\t#{address}\t#{dstport}\r\n"
end
def render_activities(activities) do
activities
|> Enum.reverse()
|> Enum.map(fn activity ->
user = User.get_cached_by_ap_id(activity.data["actor"])
object = Object.normalize(activity, fetch: false)
like_count = object.data["like_count"] || 0
announcement_count = object.data["announcement_count"] || 0
link("Post ##{activity.id} by #{user.nickname}", "/notices/#{activity.id}") <>
info("#{like_count} likes, #{announcement_count} repeats") <>
"i\tfake\t(NULL)\t0\r\n" <>
info(HTML.strip_tags(String.replace(object.data["content"], "<br>", "\r")))
end)
|> Enum.join("i\tfake\t(NULL)\t0\r\n")
end
def response("") do
info("Welcome to #{Pleroma.Config.get([:instance, :name], "Pleroma")}!") <>
link("Public Timeline", "/main/public") <>
link("Federated Timeline", "/main/all") <> ".\r\n"
end
def response("/main/public") do
posts =
%{type: ["Create"], local_only: true}
|> ActivityPub.fetch_public_activities()
|> render_activities()
info("Welcome to the Public Timeline!") <> posts <> ".\r\n"
end
def response("/main/all") do
posts =
%{type: ["Create"]}
|> ActivityPub.fetch_public_activities()
|> render_activities()
info("Welcome to the Federated Timeline!") <> posts <> ".\r\n"
end
def response("/notices/" <> id) do
with %Activity{} = activity <- Activity.get_by_id(id),
true <- Visibility.public?(activity) do
activities =
ActivityPub.fetch_activities_for_context(activity.data["context"])
|> render_activities
user = User.get_cached_by_ap_id(activity.data["actor"])
info("Post #{activity.id} by #{user.nickname}") <>
link("More posts by #{user.nickname}", "/users/#{user.nickname}") <> activities <> ".\r\n"
else
_e ->
info("Not public") <> ".\r\n"
end
end
def response("/users/" <> nickname) do
with %User{} = user <- User.get_cached_by_nickname(nickname) do
params = %{
type: ["Create"],
actor_id: user.ap_id
}
activities =
params
|> ActivityPub.fetch_public_activities()
|> render_activities()
info("Posts by #{user.nickname}") <> activities <> ".\r\n"
else
_e ->
info("No such user") <> ".\r\n"
end
end
def loop(socket, transport) do
case transport.recv(socket, 0, 5000) do
{:ok, data} ->
data = String.trim_trailing(data, "\r\n")
transport.send(socket, response(data))
:ok = transport.close(socket)
_ ->
:ok = transport.close(socket)
end
end
end
diff --git a/lib/pleroma/user/backup.ex b/lib/pleroma/user/backup.ex
index 3f67cdf0c..35535528b 100644
--- a/lib/pleroma/user/backup.ex
+++ b/lib/pleroma/user/backup.ex
@@ -1,361 +1,361 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User.Backup do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
import Pleroma.Web.Gettext
require Logger
require Pleroma.Constants
alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.Config
alias Pleroma.Repo
alias Pleroma.SafeZip
alias Pleroma.Uploaders.Uploader
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.UserView
alias Pleroma.Workers.BackupWorker
@type t :: %__MODULE__{}
schema "backups" do
field(:content_type, :string)
field(:file_name, :string)
field(:file_size, :integer, default: 0)
field(:processed, :boolean, default: false)
field(:tempdir, :string)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
timestamps()
end
@doc """
Schedules a job to backup a user if the number of backup requests has not exceeded the limit.
Admins can directly call new/1 and schedule_backup/1 to bypass the limit.
"""
@spec user(User.t()) :: {:ok, t()} | {:error, any()}
def user(user) do
days = Config.get([__MODULE__, :limit_days])
with true <- permitted?(user),
%__MODULE__{} = backup <- new(user),
{:ok, inserted_backup} <- Repo.insert(backup),
{:ok, %Oban.Job{}} <- schedule_backup(inserted_backup) do
{:ok, inserted_backup}
else
false ->
{:error,
dngettext(
"errors",
"Last export was less than a day ago",
"Last export was less than %{days} days ago",
days,
days: days
)}
e ->
{:error, e}
end
end
@doc "Generates a %Backup{} for a user with a random file name"
@spec new(User.t()) :: t()
def new(user) do
rand_str = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false)
datetime = Calendar.NaiveDateTime.Format.iso8601_basic(NaiveDateTime.utc_now())
name = "archive-#{user.nickname}-#{datetime}-#{rand_str}.zip"
%__MODULE__{
content_type: "application/zip",
file_name: name,
tempdir: tempdir(),
user: user
}
end
@doc "Schedules the execution of the provided backup"
@spec schedule_backup(t()) :: {:ok, Oban.Job.t()} | {:error, any()}
def schedule_backup(backup) do
with false <- is_nil(backup.id) do
%{"op" => "process", "backup_id" => backup.id}
|> BackupWorker.new()
|> Oban.insert()
else
true ->
{:error, "Backup is missing id. Please insert it into the Repo first."}
end
end
@doc "Deletes the backup archive file and removes the database record"
@spec delete_archive(t()) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()}
def delete_archive(backup) do
uploader = Config.get([Pleroma.Upload, :uploader])
with :ok <- uploader.delete_file(Path.join("backups", backup.file_name)) do
Repo.delete(backup)
end
end
@doc "Schedules a job to delete the backup archive"
@spec schedule_delete(t()) :: {:ok, Oban.Job.t()} | {:error, any()}
def schedule_delete(backup) do
days = Config.get([__MODULE__, :purge_after_days])
time = 60 * 60 * 24 * days
scheduled_at = Calendar.NaiveDateTime.add!(backup.inserted_at, time)
%{"op" => "delete", "backup_id" => backup.id}
|> BackupWorker.new(scheduled_at: scheduled_at)
|> Oban.insert()
end
defp permitted?(user) do
with {_, %__MODULE__{inserted_at: inserted_at}} <- {:last, get_last(user)} do
days = Config.get([__MODULE__, :limit_days])
diff = Timex.diff(NaiveDateTime.utc_now(), inserted_at, :days)
diff > days
else
{:last, nil} -> true
end
end
@doc "Returns last backup for the provided user"
@spec get_last(User.t()) :: t()
def get_last(%User{id: user_id}) do
__MODULE__
|> where(user_id: ^user_id)
|> order_by(desc: :id)
|> limit(1)
|> Repo.one()
end
@doc "Lists all existing backups for a user"
@spec list(User.t()) :: [Ecto.Schema.t() | term()]
def list(%User{id: user_id}) do
__MODULE__
|> where(user_id: ^user_id)
|> order_by(desc: :id)
|> Repo.all()
end
@doc "Schedules deletion of all but the the most recent backup"
@spec remove_outdated(User.t()) :: :ok
def remove_outdated(user) do
with %__MODULE__{} = latest_backup <- get_last(user) do
__MODULE__
|> where(user_id: ^user.id)
|> where([b], b.id != ^latest_backup.id)
|> Repo.all()
|> Enum.each(&schedule_delete/1)
else
_ -> :ok
end
end
def get_by_id(id), do: Repo.get(__MODULE__, id)
@doc "Generates changeset for %Pleroma.User.Backup{}"
@spec changeset(%__MODULE__{}, map()) :: %Ecto.Changeset{}
def changeset(backup \\ %__MODULE__{}, attrs) do
backup
|> cast(attrs, [:content_type, :file_name, :file_size, :processed, :tempdir])
end
@doc "Updates the backup record"
@spec update_record(%__MODULE__{}, map()) :: {:ok, %__MODULE__{}} | {:error, %Ecto.Changeset{}}
def update_record(%__MODULE__{} = backup, attrs) do
backup
|> changeset(attrs)
|> Repo.update()
end
@files [
"actor.json",
"outbox.json",
"likes.json",
"bookmarks.json",
"followers.json",
"following.json"
]
@spec run(t()) :: {:ok, t()} | {:error, :failed}
def run(%__MODULE__{} = backup) do
backup = Repo.preload(backup, :user)
tempfile = Path.join([backup.tempdir, backup.file_name])
with {_, :ok} <- {:mkdir, Pleroma.Backports.mkdir_p(backup.tempdir)},
{_, :ok} <- {:actor, actor(backup.tempdir, backup.user)},
{_, :ok} <- {:statuses, statuses(backup.tempdir, backup.user)},
{_, :ok} <- {:likes, likes(backup.tempdir, backup.user)},
{_, :ok} <- {:bookmarks, bookmarks(backup.tempdir, backup.user)},
{_, :ok} <- {:followers, followers(backup.tempdir, backup.user)},
{_, :ok} <- {:following, following(backup.tempdir, backup.user)},
{_, {:ok, _zip_path}} <-
{:zip, SafeZip.zip(tempfile, @files, backup.tempdir)},
{_, {:ok, %File.Stat{size: zip_size}}} <- {:filestat, File.stat(tempfile)},
{:ok, updated_backup} <- update_record(backup, %{file_size: zip_size}) do
{:ok, updated_backup}
else
_ ->
File.rm_rf(backup.tempdir)
{:error, :failed}
end
end
defp tempdir do
rand = :crypto.strong_rand_bytes(8) |> Base.url_encode64(padding: false)
subdir = "backup-#{rand}"
case Config.get([__MODULE__, :tempdir]) do
nil ->
Path.join([System.tmp_dir!(), subdir])
path ->
Path.join([path, subdir])
end
end
@doc "Uploads the completed backup and marks it as processed"
@spec upload(t()) :: {:ok, t()}
def upload(%__MODULE__{tempdir: tempdir} = backup) when is_binary(tempdir) do
uploader = Config.get([Pleroma.Upload, :uploader])
upload = %Pleroma.Upload{
name: backup.file_name,
tempfile: Path.join([tempdir, backup.file_name]),
content_type: backup.content_type,
path: Path.join("backups", backup.file_name)
}
with {:ok, _} <- Uploader.put_file(uploader, upload),
{:ok, uploaded_backup} <- update_record(backup, %{processed: true}),
{:ok, _} <- File.rm_rf(tempdir) do
{:ok, uploaded_backup}
end
end
defp actor(dir, user) do
with {:ok, json} <-
UserView.render("user.json", %{user: user})
|> Map.merge(%{
"bookmarks" => "bookmarks.json",
"likes" => "likes.json",
"outbox" => "outbox.json",
"followers" => "followers.json",
"following" => "following.json"
})
|> Jason.encode() do
File.write(Path.join(dir, "actor.json"), json)
end
end
defp write_header(file, name) do
IO.write(
file,
"""
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "#{name}.json",
"type": "OrderedCollection",
"orderedItems": [
"""
)
end
defp write(query, dir, name, fun) do
path = Path.join(dir, "#{name}.json")
chunk_size = Config.get([__MODULE__, :process_chunk_size])
with {:ok, file} <- File.open(path, [:write, :utf8]),
:ok <- write_header(file, name) do
total =
query
|> Pleroma.Repo.chunk_stream(chunk_size, _returns_as = :one, timeout: :infinity)
|> Enum.reduce(0, fn i, acc ->
with {:ok, data} <-
(try do
fun.(i)
rescue
e -> {:error, e}
end),
{:ok, str} <- Jason.encode(data),
:ok <- IO.write(file, str <> ",\n") do
acc + 1
else
{:error, e} ->
Logger.warning(
"Error processing backup item: #{inspect(e)}\n The item is: #{inspect(i)}"
)
acc
end
end)
with :ok <- :file.pwrite(file, {:eof, -2}, "\n],\n \"totalItems\": #{total}}") do
File.close(file)
end
end
end
defp bookmarks(dir, %{id: user_id} = _user) do
Bookmark
|> where(user_id: ^user_id)
|> join(:inner, [b], activity in assoc(b, :activity))
|> select([b, a], %{id: b.id, object: fragment("(?)->>'object'", a.data)})
|> write(dir, "bookmarks", fn a -> {:ok, a.object} end)
end
defp likes(dir, user) do
user.ap_id
|> Activity.Queries.by_actor()
|> Activity.Queries.by_type("Like")
|> select([like], %{id: like.id, object: fragment("(?)->>'object'", like.data)})
|> write(dir, "likes", fn a -> {:ok, a.object} end)
end
defp statuses(dir, user) do
opts =
%{}
|> Map.put(:type, ["Create", "Announce"])
|> Map.put(:actor_id, user.ap_id)
[
[Pleroma.Constants.as_public(), user.ap_id],
User.following(user),
Pleroma.List.memberships(user)
]
|> Enum.concat()
|> ActivityPub.fetch_activities_query(opts)
|> write(
dir,
"outbox",
fn a ->
- with {:ok, activity} <- Transmogrifier.prepare_outgoing(a.data) do
+ with {:ok, activity} <- Transmogrifier.prepare_activity(a.data) do
{:ok, Map.delete(activity, "@context")}
end
end
)
end
defp followers(dir, user) do
User.get_followers_query(user)
|> write(dir, "followers", fn a -> {:ok, a.ap_id} end)
end
defp following(dir, user) do
User.get_friends_query(user)
|> write(dir, "following", fn a -> {:ok, a.ap_id} end)
end
end
diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex
index b6c814aed..e3c4e01a4 100644
--- a/lib/pleroma/web/activity_pub/publisher.ex
+++ b/lib/pleroma/web/activity_pub/publisher.ex
@@ -1,421 +1,421 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Publisher do
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Delivery
alias Pleroma.HTTP
alias Pleroma.Instances
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Publisher.Prepared
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Workers.PublisherWorker
require Pleroma.Constants
import Pleroma.Web.ActivityPub.Visibility
require Logger
@moduledoc """
ActivityPub outgoing federation module.
"""
@signature_impl Application.compile_env(
:pleroma,
[__MODULE__, :signature_impl],
Pleroma.Signature
)
@transmogrifier_impl Application.compile_env(
:pleroma,
[__MODULE__, :transmogrifier_impl],
Pleroma.Web.ActivityPub.Transmogrifier
)
@doc """
Enqueue publishing a single activity.
"""
@spec enqueue_one(map(), Keyword.t()) :: {:ok, %Oban.Job{}}
def enqueue_one(%{} = params, worker_args \\ []) do
PublisherWorker.new(
%{"op" => "publish_one", "params" => params},
worker_args
)
|> Oban.insert()
end
@doc """
Gathers a set of remote users given an IR envelope.
"""
def remote_users(%User{id: user_id}, %{data: %{"to" => to} = data}) do
cc = Map.get(data, "cc", [])
bcc =
data
|> Map.get("bcc", [])
|> Enum.reduce([], fn ap_id, bcc ->
case Pleroma.List.get_by_ap_id(ap_id) do
%Pleroma.List{user_id: ^user_id} = list ->
{:ok, following} = Pleroma.List.get_following(list)
bcc ++ Enum.map(following, & &1.ap_id)
_ ->
bcc
end
end)
[to, cc, bcc]
|> Enum.concat()
|> Enum.map(&User.get_cached_by_ap_id/1)
|> Enum.filter(fn user -> user && !user.local end)
end
@doc """
Determine if an activity can be represented by running it through Transmogrifier.
"""
def representable?(%Activity{} = activity) do
- with {:ok, _data} <- @transmogrifier_impl.prepare_outgoing(activity.data) do
+ with {:ok, _data} <- @transmogrifier_impl.prepare_activity(activity.data) do
true
else
_e ->
false
end
end
@doc """
Prepare an activity for publishing from an Oban job
* `inbox`: the inbox to publish to
* `activity_id`: the internal activity id
* `cc`: the cc recipients relevant to this inbox (optional)
"""
@spec prepare_one(map()) :: Prepared.t()
def prepare_one(%{inbox: inbox, activity_id: activity_id} = params) do
activity = Activity.get_by_id_with_user_actor(activity_id)
actor = activity.user_actor
ap_id = activity.data["id"]
Logger.debug("Federating #{ap_id} to #{inbox}")
uri = %{path: path} = URI.parse(inbox)
- {:ok, data} = @transmogrifier_impl.prepare_outgoing(activity.data)
+ {:ok, data} = @transmogrifier_impl.prepare_activity(activity.data)
{actor, data} =
with {_, false} <- {:actor_changed?, data["actor"] != activity.data["actor"]} do
{actor, data}
else
{:actor_changed?, true} ->
- # If prepare_outgoing changes the actor, re-get it from the db
+ # If prepare_activity changes the actor, re-get it from the db
new_actor = User.get_cached_by_ap_id(data["actor"])
{new_actor, data}
end
param_cc = Map.get(params, :cc, [])
original_cc = Map.get(data, "cc", [])
public_address = Pleroma.Constants.as_public()
# Ensure unlisted posts don't lose the public address in the cc
# if the param_cc was set
cc =
if public_address in original_cc and public_address not in param_cc do
[public_address | param_cc]
else
param_cc
end
json =
data
|> Map.put("cc", cc)
|> Jason.encode!()
digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
date = @signature_impl.signed_date()
signature =
@signature_impl.sign(actor, %{
"(request-target)": "post #{path}",
host: signature_host(uri),
"content-length": byte_size(json),
digest: digest,
date: date
})
%Prepared{
activity_id: activity_id,
json: json,
date: date,
signature: signature,
digest: digest,
inbox: inbox,
unreachable_since: params[:unreachable_since]
}
end
@doc """
Publish a single message to a peer. Takes a struct with the following
parameters set:
* `activity_id`: the activity id
* `json`: the json payload
* `date`: the signed date from Pleroma.Signature.signed_date()
* `signature`: the signature from Pleroma.Signature.sign/2
* `digest`: base64 encoded the hash of the json payload prefixed with "SHA-256="
* `inbox`: the inbox URI of this delivery
* `unreachable_since`: timestamp the instance was marked unreachable
"""
def publish_one(%Prepared{} = p) do
with {:ok, %{status: code}} = result when code in 200..299 <-
HTTP.post(
p.inbox,
p.json,
[
{"Content-Type", "application/activity+json"},
{"Date", p.date},
{"signature", p.signature},
{"digest", p.digest}
]
) do
result
else
{_post_result, %{status: code} = response} = e ->
Logger.metadata(activity: p.activity_id, inbox: p.inbox, status: code)
Logger.error("Publisher failed to inbox #{p.inbox} with status #{code}")
case response do
%{status: 400} -> {:cancel, :bad_request}
%{status: 403} -> {:cancel, :forbidden}
%{status: 404} -> {:cancel, :not_found}
%{status: 410} -> {:cancel, :not_found}
_ -> {:error, e}
end
{:error, {:already_started, _}} ->
Logger.debug("Publisher snoozing worker job due worker :already_started race condition")
connection_pool_snooze()
{:error, :pool_full} ->
Logger.debug("Publisher snoozing worker job due to full connection pool")
connection_pool_snooze()
e ->
Logger.metadata(activity: p.activity_id, inbox: p.inbox)
Logger.error("Publisher failed to inbox #{p.inbox} #{inspect(e)}")
{:error, e}
end
end
defp connection_pool_snooze, do: {:snooze, 3}
defp signature_host(%URI{port: port, scheme: scheme, host: host}) do
if port == URI.default_port(scheme) do
host
else
"#{host}:#{port}"
end
end
def should_federate?(nil, _), do: false
def should_federate?(_, true), do: true
def should_federate?(inbox, _) do
%{host: host} = URI.parse(inbox)
quarantined_instances =
Config.get([:instance, :quarantined_instances], [])
|> Pleroma.Web.ActivityPub.MRF.instance_list_from_tuples()
|> Pleroma.Web.ActivityPub.MRF.subdomains_regex()
!Pleroma.Web.ActivityPub.MRF.subdomain_match?(quarantined_instances, host)
end
@spec recipients(User.t(), Activity.t()) :: [[User.t()]]
defp recipients(actor, activity) do
followers =
if actor.follower_address in activity.recipients do
User.get_external_followers(actor)
else
[]
end
fetchers =
with %Activity{data: %{"type" => "Delete"}} <- activity,
%Object{id: object_id} <- Object.normalize(activity, fetch: false),
fetchers <- User.get_delivered_users_by_object_id(object_id),
_ <- Delivery.delete_all_by_object_id(object_id) do
fetchers
else
_ ->
[]
end
mentioned = remote_users(actor, activity)
non_mentioned = (followers ++ fetchers) -- mentioned
[mentioned, non_mentioned]
end
defp get_cc_ap_ids(ap_id, recipients) do
host = Map.get(URI.parse(ap_id), :host)
recipients
|> Enum.filter(fn %User{ap_id: ap_id} -> Map.get(URI.parse(ap_id), :host) == host end)
|> Enum.map(& &1.ap_id)
end
defp maybe_use_sharedinbox(%User{shared_inbox: nil, inbox: inbox}), do: inbox
defp maybe_use_sharedinbox(%User{shared_inbox: shared_inbox}), do: shared_inbox
@doc """
Determine a user inbox to use based on heuristics. These heuristics
are based on an approximation of the ``sharedInbox`` rules in the
[ActivityPub specification][ap-sharedinbox].
Please do not edit this function (or its children) without reading
the spec, as editing the code is likely to introduce some breakage
without some familiarity.
[ap-sharedinbox]: https://www.w3.org/TR/activitypub/#shared-inbox-delivery
"""
def determine_inbox(
%Activity{data: activity_data},
%User{inbox: inbox} = user
) do
to = activity_data["to"] || []
cc = activity_data["cc"] || []
type = activity_data["type"]
cond do
type == "Delete" ->
maybe_use_sharedinbox(user)
Pleroma.Constants.as_public() in to || Pleroma.Constants.as_public() in cc ->
maybe_use_sharedinbox(user)
length(to) + length(cc) > 1 ->
maybe_use_sharedinbox(user)
true ->
inbox
end
end
@doc """
Publishes an activity with BCC to all relevant peers.
"""
def publish(%User{} = actor, %{data: %{"bcc" => bcc}} = activity)
when is_list(bcc) and bcc != [] do
public = public?(activity)
[priority_recipients, recipients] = recipients(actor, activity)
[priority_inboxes, other_inboxes] =
[priority_recipients, recipients]
|> Enum.map(fn recipients ->
recipients
|> Enum.map(fn %User{} = user ->
determine_inbox(activity, user)
end)
|> Enum.uniq()
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
|> Instances.filter_reachable()
end)
Repo.checkout(fn ->
Enum.each([priority_inboxes, other_inboxes], fn inboxes ->
Enum.each(inboxes, fn inbox ->
{%User{ap_id: ap_id}, priority} =
get_user_with_priority(inbox, priority_recipients, recipients)
# Get all the recipients on the same host and add them to cc. Otherwise, a remote
# instance would only accept a first message for the first recipient and ignore the rest.
cc = get_cc_ap_ids(ap_id, recipients)
__MODULE__.enqueue_one(
%{
inbox: inbox,
cc: cc,
activity_id: activity.id
},
priority: priority
)
end)
end)
end)
end
# Publishes an activity to all relevant peers.
def publish(%User{} = actor, %Activity{} = activity) do
public = public?(activity)
if public && Config.get([:instance, :allow_relay]) do
Logger.debug(fn -> "Relaying #{activity.data["id"]} out" end)
Relay.publish(activity)
end
[priority_inboxes, inboxes] =
recipients(actor, activity)
|> Enum.map(fn recipients ->
recipients
|> Enum.map(fn %User{} = user ->
determine_inbox(activity, user)
end)
|> Enum.uniq()
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
end)
inboxes = inboxes -- priority_inboxes
[{priority_inboxes, 0}, {inboxes, 1}]
|> Enum.each(fn {inboxes, priority} ->
inboxes
|> Instances.filter_reachable()
|> Enum.each(fn inbox ->
__MODULE__.enqueue_one(
%{
inbox: inbox,
activity_id: activity.id
},
priority: priority
)
end)
end)
:ok
end
def gather_webfinger_links(%User{} = user) do
[
%{"rel" => "self", "type" => "application/activity+json", "href" => user.ap_id},
%{
"rel" => "self",
"type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
"href" => user.ap_id
},
%{
"rel" => "http://ostatus.org/schema/1.0/subscribe",
"template" => "#{Pleroma.Web.Endpoint.url()}/ostatus_subscribe?acct={uri}"
}
]
end
def gather_nodeinfo_protocol_names, do: ["activitypub"]
defp get_user_with_priority(inbox, priority_recipients, recipients) do
[{priority_recipients, 0}, {recipients, 1}]
|> Enum.find_value(fn {recipients, priority} ->
with %User{} = user <- Enum.find(recipients, fn actor -> actor.inbox == inbox end) do
{user, priority}
else
_ -> nil
end
end)
end
end
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index ba6f4030d..6af5ee89d 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -1,1081 +1,1081 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Transmogrifier do
@moduledoc """
A module to handle coding from internal to wire ActivityPub and back.
"""
@behaviour Pleroma.Web.ActivityPub.Transmogrifier.API
alias Pleroma.Activity
alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.Emoji
alias Pleroma.Maps
alias Pleroma.Object
alias Pleroma.Object.Containment
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.ObjectValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Federator
import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
require Pleroma.Constants
@doc """
Modifies an incoming AP object (mastodon format) to our internal format.
"""
def fix_object(object, options \\ []) do
object
|> strip_internal_fields()
|> fix_actor()
|> fix_url()
|> fix_attachments()
|> fix_context()
|> fix_in_reply_to(options)
|> fix_emoji()
|> fix_tag()
|> fix_content_map()
|> fix_addressing()
|> fix_summary()
|> fix_history(&fix_object/1)
end
defp maybe_fix_object(%{"attributedTo" => _} = object), do: fix_object(object)
defp maybe_fix_object(object), do: object
defp fix_history(%{"formerRepresentations" => %{"orderedItems" => list}} = obj, fix_fun)
when is_list(list) do
update_in(obj["formerRepresentations"]["orderedItems"], fn h -> Enum.map(h, fix_fun) end)
end
defp fix_history(obj, _), do: obj
defp fix_recursive(obj, fun) do
# unlike Erlang, Elixir does not support recursive inline functions
# which would allow us to avoid reconstructing this on every recursion
rec_fun = fn
obj when is_map(obj) -> fix_recursive(obj, fun)
# there may be simple AP IDs in history (or object field)
obj -> obj
end
obj
|> fun.()
|> fix_history(rec_fun)
|> then(fn
%{"object" => object} = doc when is_map(object) ->
update_in(doc["object"], rec_fun)
apdoc ->
apdoc
end)
end
def fix_summary(%{"summary" => nil} = object) do
Map.put(object, "summary", "")
end
def fix_summary(%{"summary" => _} = object) do
# summary is present, nothing to do
object
end
def fix_summary(object), do: Map.put(object, "summary", "")
def fix_addressing_list(map, field) do
addrs = map[field]
cond do
is_list(addrs) ->
Map.put(map, field, Enum.filter(addrs, &is_binary/1))
is_binary(addrs) ->
Map.put(map, field, [addrs])
true ->
Map.put(map, field, [])
end
end
@doc """
Bovine compatibility
https://codeberg.org/bovine/bovine/issues/53
"""
def fix_addressing_public(map, field) do
addrs = Map.get(map, field, []) |> List.wrap()
Map.put(
map,
field,
Enum.map(addrs, fn
"Public" -> Pleroma.Constants.as_public()
"as:Public" -> Pleroma.Constants.as_public()
x -> x
end)
)
end
# if directMessage flag is set to true, leave the addressing alone
def fix_explicit_addressing(%{"directMessage" => true} = object, _follower_collection),
do: object
def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, follower_collection) do
explicit_mentions =
Utils.determine_explicit_mentions(object) ++
[Pleroma.Constants.as_public(), follower_collection]
explicit_to = Enum.filter(to, fn x -> x in explicit_mentions end)
explicit_cc = Enum.filter(to, fn x -> x not in explicit_mentions end)
final_cc =
(cc ++ explicit_cc)
|> Enum.filter(& &1)
|> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end)
|> Enum.uniq()
object
|> Map.put("to", explicit_to)
|> Map.put("cc", final_cc)
end
# if as:Public is addressed, then make sure the followers collection is also addressed
# so that the activities will be delivered to local users.
def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collection) do
recipients = to ++ cc
if followers_collection not in recipients do
cond do
Pleroma.Constants.as_public() in cc ->
to = to ++ [followers_collection]
Map.put(object, "to", to)
Pleroma.Constants.as_public() in to ->
cc = cc ++ [followers_collection]
Map.put(object, "cc", cc)
true ->
object
end
else
object
end
end
def fix_addressing(object) do
{:ok, %User{follower_address: follower_collection}} =
object
|> Containment.get_actor()
|> User.get_or_fetch_by_ap_id()
object
|> fix_addressing_list("to")
|> fix_addressing_list("cc")
|> fix_addressing_list("bto")
|> fix_addressing_list("bcc")
|> fix_addressing_public("to")
|> fix_addressing_public("cc")
|> fix_addressing_public("bto")
|> fix_addressing_public("bcc")
|> fix_explicit_addressing(follower_collection)
|> fix_implicit_addressing(follower_collection)
end
def fix_actor(%{"attributedTo" => actor} = object) do
actor = Containment.get_actor(%{"actor" => actor})
# TODO: Remove actor field for Objects
object
|> Map.put("actor", actor)
|> Map.put("attributedTo", actor)
end
def fix_in_reply_to(object, options \\ [])
def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
when not is_nil(in_reply_to) do
in_reply_to_id = prepare_in_reply_to(in_reply_to)
depth = (options[:depth] || 0) + 1
if Federator.allowed_thread_distance?(depth) do
with {:ok, replied_object} <- get_obj_helper(in_reply_to_id, options),
%Activity{} <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
object
|> Map.put("inReplyTo", replied_object.data["id"])
|> Map.put("context", replied_object.data["context"] || object["conversation"])
|> Map.drop(["conversation", "inReplyToAtomUri"])
else
_ ->
object
end
else
object
end
end
def fix_in_reply_to(object, _options), do: object
def fix_quote_url_and_maybe_fetch(object, options \\ []) do
quote_url =
case CommonFixes.fix_quote_url(object) do
%{"quoteUrl" => quote_url} -> quote_url
_ -> nil
end
with {:quoting?, true} <- {:quoting?, not is_nil(quote_url)},
{:ok, quoted_object} <- get_obj_helper(quote_url, options),
%Activity{} <- Activity.get_create_by_object_ap_id(quoted_object.data["id"]) do
Map.put(object, "quoteUrl", quoted_object.data["id"])
else
{:quoting?, _} ->
object
_ ->
object
end
end
defp prepare_in_reply_to(in_reply_to) do
cond do
is_bitstring(in_reply_to) ->
in_reply_to
is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
in_reply_to["id"]
is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
Enum.at(in_reply_to, 0)
true ->
""
end
end
def fix_context(object) do
context = object["context"] || object["conversation"] || Utils.generate_context_id()
object
|> Map.put("context", context)
|> Map.drop(["conversation"])
end
def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
attachments =
Enum.map(attachment, fn data ->
url =
cond do
is_list(data["url"]) -> List.first(data["url"])
is_map(data["url"]) -> data["url"]
true -> nil
end
media_type =
cond do
is_map(url) && url =~ Pleroma.Constants.mime_regex() ->
url["mediaType"]
is_bitstring(data["mediaType"]) && data["mediaType"] =~ Pleroma.Constants.mime_regex() ->
data["mediaType"]
is_bitstring(data["mimeType"]) && data["mimeType"] =~ Pleroma.Constants.mime_regex() ->
data["mimeType"]
true ->
nil
end
href =
cond do
is_map(url) && is_binary(url["href"]) -> url["href"]
is_binary(data["url"]) -> data["url"]
is_binary(data["href"]) -> data["href"]
true -> nil
end
if href do
attachment_url =
%{
"href" => href,
"type" => Map.get(url || %{}, "type", "Link")
}
|> Maps.put_if_present("mediaType", media_type)
|> Maps.put_if_present("width", (url || %{})["width"] || data["width"])
|> Maps.put_if_present("height", (url || %{})["height"] || data["height"])
%{
"url" => [attachment_url],
"type" => data["type"] || "Document"
}
|> Maps.put_if_present("mediaType", media_type)
|> Maps.put_if_present("name", data["name"])
|> Maps.put_if_present("blurhash", data["blurhash"])
else
nil
end
end)
|> Enum.filter(& &1)
Map.put(object, "attachment", attachments)
end
def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
object
|> Map.put("attachment", [attachment])
|> fix_attachments()
end
def fix_attachments(object), do: object
def fix_url(%{"url" => url} = object) when is_map(url) do
Map.put(object, "url", url["href"])
end
def fix_url(%{"url" => url} = object) when is_list(url) do
first_element = Enum.at(url, 0)
url_string =
cond do
is_bitstring(first_element) -> first_element
is_map(first_element) -> first_element["href"] || ""
true -> ""
end
Map.put(object, "url", url_string)
end
def fix_url(object), do: object
def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
emoji =
tags
|> Enum.filter(fn data -> is_map(data) and data["type"] == "Emoji" and data["icon"] end)
|> Enum.reduce(%{}, fn data, mapping ->
name = String.trim(data["name"], ":")
Map.put(mapping, name, data["icon"]["url"])
end)
Map.put(object, "emoji", emoji)
end
def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
name = String.trim(tag["name"], ":")
emoji = %{name => tag["icon"]["url"]}
Map.put(object, "emoji", emoji)
end
def fix_emoji(object), do: object
def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
tags =
tag
|> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
|> Enum.map(fn
%{"name" => "#" <> hashtag} -> String.downcase(hashtag)
%{"name" => hashtag} -> String.downcase(hashtag)
end)
Map.put(object, "tag", tag ++ tags)
end
def fix_tag(%{"tag" => %{} = tag} = object) do
object
|> Map.put("tag", [tag])
|> fix_tag
end
def fix_tag(object), do: object
# prefer content over contentMap
def fix_content_map(%{"content" => content} = object) when not_empty_string(content), do: object
# content map usually only has one language so this will do for now.
def fix_content_map(%{"contentMap" => content_map} = object) do
content_groups = Map.to_list(content_map)
{_, content} = Enum.at(content_groups, 0)
Map.put(object, "content", content)
end
def fix_content_map(object), do: object
defp fix_type(%{"type" => "Note", "inReplyTo" => reply_id, "name" => _} = object, options)
when is_binary(reply_id) do
options = Keyword.put(options, :fetch, true)
with %Object{data: %{"type" => "Question"}} <- Object.normalize(reply_id, options) do
Map.put(object, "type", "Answer")
else
_ -> object
end
end
defp fix_type(object, _options), do: object
# Reduce the object list to find the reported user.
defp get_reported(objects) do
Enum.reduce_while(objects, nil, fn ap_id, _ ->
with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
{:halt, user}
else
_ -> {:cont, nil}
end
end)
end
def handle_incoming(data, options \\ []) do
data
|> fix_recursive(&strip_internal_fields/1)
|> handle_incoming_normalized(options)
end
# Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
# with nil ID.
defp handle_incoming_normalized(
%{"type" => "Flag", "object" => objects, "actor" => actor} = data,
_options
) do
with context <- data["context"] || Utils.generate_context_id(),
content <- data["content"] || "",
%User{} = actor <- User.get_cached_by_ap_id(actor),
# Reduce the object list to find the reported user.
%User{} = account <- get_reported(objects),
# Remove the reported user from the object list.
statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
%{
actor: actor,
context: context,
account: account,
statuses: statuses,
content: content,
additional: %{"cc" => [account.ap_id]}
}
|> ActivityPub.flag()
end
end
# disallow objects with bogus IDs
defp handle_incoming_normalized(%{"id" => nil}, _options), do: :error
defp handle_incoming_normalized(%{"id" => ""}, _options), do: :error
# length of https:// = 8, should validate better, but good enough for now.
defp handle_incoming_normalized(%{"id" => id}, _options)
when is_binary(id) and byte_size(id) < 8,
do: :error
defp handle_incoming_normalized(
%{"type" => "Listen", "object" => %{"type" => "Audio"} = object} = data,
options
) do
actor = Containment.get_actor(data)
data =
Map.put(data, "actor", actor)
|> fix_addressing
with {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
reply_depth = (options[:depth] || 0) + 1
options = Keyword.put(options, :depth, reply_depth)
object = fix_object(object, options)
params = %{
to: data["to"],
object: object,
actor: user,
context: nil,
local: false,
published: data["published"],
additional: Map.take(data, ["cc", "id"])
}
ActivityPub.listen(params)
else
_e -> :error
end
end
@misskey_reactions %{
"like" => "👍",
"love" => "❤️",
"laugh" => "😆",
"hmm" => "🤔",
"surprise" => "😮",
"congrats" => "🎉",
"angry" => "💢",
"confused" => "😥",
"rip" => "😇",
"pudding" => "🍮",
"star" => "⭐"
}
# Rewrite misskey likes into EmojiReacts
defp handle_incoming_normalized(
%{
"type" => "Like",
"content" => content
} = data,
options
)
when is_binary(content) do
data
|> Map.put("type", "EmojiReact")
|> handle_incoming_normalized(options)
end
defp handle_incoming_normalized(
%{
"type" => "Like",
"_misskey_reaction" => reaction
} = data,
options
) do
data
|> Map.put("content", @misskey_reactions[reaction] || reaction)
|> handle_incoming_normalized(options)
end
defp handle_incoming_normalized(
%{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data,
options
)
when objtype in ~w{Question Answer ChatMessage Audio Video Event Article Note Page Image} do
fetch_options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
object =
data["object"]
|> strip_internal_fields()
|> fix_type(fetch_options)
|> fix_in_reply_to(fetch_options)
|> fix_quote_url_and_maybe_fetch(fetch_options)
data = Map.put(data, "object", object)
options = Keyword.put(options, :local, false)
with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
nil <- Activity.get_create_by_object_ap_id(obj_id),
{:ok, activity, _} <- Pipeline.common_pipeline(data, options) do
{:ok, activity}
else
%Activity{} = activity -> {:ok, activity}
e -> e
end
end
defp handle_incoming_normalized(%{"type" => type} = data, _options)
when type in ~w{Like EmojiReact Announce Add Remove} do
with :ok <- ObjectValidator.fetch_actor_and_object(data),
{:ok, activity, _meta} <-
Pipeline.common_pipeline(data, local: false) do
{:ok, activity}
else
e -> {:error, e}
end
end
defp handle_incoming_normalized(
%{"type" => type} = data,
_options
)
when type in ~w{Update Block Follow Accept Reject} do
fixed_obj = maybe_fix_object(data["object"])
data = if fixed_obj != nil, do: %{data | "object" => fixed_obj}, else: data
with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
{:ok, activity, _} <-
Pipeline.common_pipeline(data, local: false) do
{:ok, activity}
end
end
defp handle_incoming_normalized(
%{"type" => "Delete"} = data,
_options
) do
with {:ok, activity, _} <-
Pipeline.common_pipeline(data, local: false) do
{:ok, activity}
else
{:error, {:validate, _}} = e ->
# Check if we have a create activity for this
with {:ok, object_id} <- ObjectValidators.ObjectID.cast(data["object"]),
%Activity{data: %{"actor" => actor}} <-
Activity.create_by_object_ap_id(object_id) |> Repo.one(),
# We have one, insert a tombstone and retry
{:ok, tombstone_data, _} <- Builder.tombstone(actor, object_id),
{:ok, _tombstone} <- Object.create(tombstone_data) do
handle_incoming(data)
else
_ -> e
end
e ->
{:error, e}
end
end
defp handle_incoming_normalized(
%{
"type" => "Undo",
"object" => %{"type" => "Follow", "object" => followed},
"actor" => follower,
"id" => id
} = _data,
_options
) do
with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
{:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower),
{:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do
User.unfollow(follower, followed)
{:ok, activity}
else
_e -> :error
end
end
defp handle_incoming_normalized(
%{
"type" => "Undo",
"object" => %{"type" => type}
} = data,
_options
)
when type in ["Like", "EmojiReact", "Announce", "Block"] do
with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
{:ok, activity}
end
end
# For Undos that don't have the complete object attached, try to find it in our database.
defp handle_incoming_normalized(
%{
"type" => "Undo",
"object" => object
} = activity,
options
)
when is_binary(object) do
with %Activity{data: data} <- Activity.get_by_ap_id(object) do
activity
|> Map.put("object", data)
|> handle_incoming_normalized(options)
else
_e -> :error
end
end
defp handle_incoming_normalized(
%{
"type" => "Move",
"actor" => origin_actor,
"object" => origin_actor,
"target" => target_actor
},
_options
) do
with %User{} = origin_user <- User.get_cached_by_ap_id(origin_actor),
{:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_actor),
true <- origin_actor in target_user.also_known_as do
ActivityPub.move(origin_user, target_user, false)
else
_e -> :error
end
end
# Rewrite dislikes into the thumbs down emoji
defp handle_incoming_normalized(%{"type" => "Dislike"} = data, options) do
data
|> Map.put("type", "EmojiReact")
|> Map.put("content", "👎")
|> handle_incoming_normalized(options)
end
defp handle_incoming_normalized(
%{"type" => "Undo", "object" => %{"type" => "Dislike"}} = data,
options
) do
data
|> put_in(["object", "type"], "EmojiReact")
|> put_in(["object", "content"], "👎")
|> handle_incoming_normalized(options)
end
defp handle_incoming_normalized(_, _), do: :error
@spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
def get_obj_helper(id, options \\ []) do
options = Keyword.put(options, :fetch, true)
case Object.normalize(id, options) do
%Object{} = object -> {:ok, object}
_ -> nil
end
end
@spec get_embedded_obj_helper(String.t() | Object.t(), User.t()) :: {:ok, Object.t()} | nil
def get_embedded_obj_helper(%{"attributedTo" => attributed_to, "id" => object_id} = data, %User{
ap_id: ap_id
})
when attributed_to == ap_id do
with {:ok, activity} <-
handle_incoming(%{
"type" => "Create",
"to" => data["to"],
"cc" => data["cc"],
"actor" => attributed_to,
"object" => data
}) do
{:ok, Object.normalize(activity, fetch: false)}
else
_ -> get_obj_helper(object_id)
end
end
def get_embedded_obj_helper(object_id, _) do
get_obj_helper(object_id)
end
def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
with false <- String.starts_with?(in_reply_to, "http"),
{:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
else
_e -> object
end
end
def set_reply_to_uri(obj), do: obj
@doc """
Fedibird compatibility
https://github.com/fedibird/mastodon/commit/dbd7ae6cf58a92ec67c512296b4daaea0d01e6ac
"""
def set_quote_url(%{"quoteUrl" => quote_url} = object) when is_binary(quote_url) do
Map.put(object, "quoteUri", quote_url)
end
def set_quote_url(obj), do: obj
@doc """
Inline first page of the `replies` collection,
containing any replies in chronological order.
"""
def set_replies(%{"type" => type} = obj_data)
when type in Pleroma.Constants.status_object_types() do
with obj_ap_id when is_binary(obj_ap_id) <- obj_data["id"],
limit when limit > 0 <-
Pleroma.Config.get([:activitypub, :note_replies_output_limit], 0),
collection <-
Pleroma.Web.ActivityPub.ObjectView.render("object_replies.json", %{
render_params: %{object_ap_id: obj_data["id"], limit: limit, skip_ap_ctx: true}
}) do
Map.put(obj_data, "replies", collection)
else
0 -> Map.put(obj_data, "replies", obj_data["id"] <> "/replies")
_ -> obj_data
end
end
def set_replies(obj_data), do: obj_data
- # Prepares the object of an outgoing create activity.
+ # Prepares and sanitizes the object for federation.
def prepare_object(object) do
object
|> add_hashtags
|> add_mention_tags
|> add_emoji_tags
|> add_attributed_to
|> prepare_attachments
|> set_conversation
|> set_reply_to_uri
|> set_quote_url
|> set_replies
|> CommonFixes.maybe_add_content_map()
|> strip_internal_fields
|> strip_internal_tags
|> set_type
|> maybe_process_history
end
defp maybe_process_history(%{"formerRepresentations" => %{"orderedItems" => history}} = object) do
processed_history =
Enum.map(
history,
fn
item when is_map(item) -> prepare_object(item)
item -> item
end
)
put_in(object, ["formerRepresentations", "orderedItems"], processed_history)
end
defp maybe_process_history(object) do
object
end
# @doc
# """
# internal -> Mastodon
# """
- def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
+ def prepare_activity(%{"type" => activity_type, "object" => object_id} = data)
when activity_type in ["Create", "Listen"] do
object =
object_id
|> Object.normalize(fetch: false)
|> Map.get(:data)
data =
data
|> Map.put("object", prepare_object(object))
|> Map.merge(Utils.make_json_ld_header(object))
|> Map.delete("bcc")
{:ok, data}
end
- def prepare_outgoing(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data)
+ def prepare_activity(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data)
when objtype in Pleroma.Constants.updatable_object_types() do
data =
data
|> Map.put("object", prepare_object(object))
|> Map.merge(Utils.make_json_ld_header(object))
|> Map.delete("bcc")
{:ok, data}
end
- def prepare_outgoing(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data)
+ def prepare_activity(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data)
when objtype in Pleroma.Constants.actor_types() do
object =
object
|> maybe_fix_user_object()
|> strip_internal_fields()
data =
data
|> Map.put("object", object)
|> strip_internal_fields()
|> Map.merge(Utils.make_json_ld_header(object))
|> Map.delete("bcc")
{:ok, data}
end
- def prepare_outgoing(%{"type" => "Update", "object" => %{}} = data) do
+ def prepare_activity(%{"type" => "Update", "object" => %{}} = data) do
raise "Requested to serve an Update for non-updateable object type: #{inspect(data)}"
end
- def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
+ def prepare_activity(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
object =
object_id
|> Object.normalize(fetch: false)
data =
if Visibility.private?(object) && object.data["actor"] == ap_id do
data |> Map.put("object", object |> Map.get(:data) |> prepare_object)
else
data |> maybe_fix_object_url
end
data =
data
|> strip_internal_fields
|> Map.merge(Utils.make_json_ld_header())
|> Map.delete("bcc")
{:ok, data}
end
# Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
# because of course it does.
- def prepare_outgoing(%{"type" => "Accept"} = data) do
+ def prepare_activity(%{"type" => "Accept"} = data) do
with follow_activity <- Activity.normalize(data["object"]) do
object = %{
"actor" => follow_activity.actor,
"object" => follow_activity.data["object"],
"id" => follow_activity.data["id"],
"type" => "Follow"
}
data =
data
|> Map.put("object", object)
|> Map.merge(Utils.make_json_ld_header())
{:ok, data}
end
end
- def prepare_outgoing(%{"type" => "Reject"} = data) do
+ def prepare_activity(%{"type" => "Reject"} = data) do
with follow_activity <- Activity.normalize(data["object"]) do
object = %{
"actor" => follow_activity.actor,
"object" => follow_activity.data["object"],
"id" => follow_activity.data["id"],
"type" => "Follow"
}
data =
data
|> Map.put("object", object)
|> Map.merge(Utils.make_json_ld_header())
{:ok, data}
end
end
- def prepare_outgoing(%{"type" => "Flag"} = data) do
+ def prepare_activity(%{"type" => "Flag"} = data) do
with {:ok, stripped_activity} <- Utils.strip_report_status_data(data),
stripped_activity <- Utils.maybe_anonymize_reporter(stripped_activity),
stripped_activity <- Map.merge(stripped_activity, Utils.make_json_ld_header()) do
{:ok, stripped_activity}
end
end
- def prepare_outgoing(%{"type" => _type} = data) do
+ def prepare_activity(%{"type" => _type} = data) do
data =
data
|> strip_internal_fields
|> maybe_fix_object_url
|> Map.merge(Utils.make_json_ld_header(data))
{:ok, data}
end
def maybe_fix_object_url(%{"object" => object} = data) when is_binary(object) do
with false <- String.starts_with?(object, "http"),
{:fetch, {:ok, relative_object}} <- {:fetch, get_obj_helper(object)},
%{data: %{"external_url" => external_url}} when not is_nil(external_url) <-
relative_object do
Map.put(data, "object", external_url)
else
{:fetch, _} ->
data
_ ->
data
end
end
def maybe_fix_object_url(data), do: data
def add_hashtags(object) do
tags =
(object["tag"] || [])
|> Enum.map(fn
# Expand internal representation tags into AS2 tags.
tag when is_binary(tag) ->
%{
"href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
"name" => "##{tag}",
"type" => "Hashtag"
}
# Do not process tags which are already AS2 tag objects.
tag when is_map(tag) ->
tag
end)
Map.put(object, "tag", tags)
end
# TODO These should be added on our side on insertion, it doesn't make much
# sense to regenerate these all the time
def add_mention_tags(object) do
to = object["to"] || []
cc = object["cc"] || []
mentioned = User.get_users_from_set(to ++ cc, local_only: false)
mentions = Enum.map(mentioned, &build_mention_tag/1)
tags = object["tag"] || []
Map.put(object, "tag", tags ++ mentions)
end
defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do
%{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
end
def take_emoji_tags(%User{emoji: emoji}) do
emoji
|> Map.to_list()
|> Enum.map(&Emoji.build_emoji_tag/1)
end
# TODO: we should probably send mtime instead of unix epoch time for updated
def add_emoji_tags(%{"emoji" => emoji} = object) do
tags = object["tag"] || []
out = Enum.map(emoji, &Emoji.build_emoji_tag/1)
Map.put(object, "tag", tags ++ out)
end
def add_emoji_tags(object), do: object
def set_conversation(object) do
Map.put(object, "conversation", object["context"])
end
def set_type(%{"type" => "Answer"} = object) do
Map.put(object, "type", "Note")
end
def set_type(object), do: object
def add_attributed_to(object) do
attributed_to = object["attributedTo"] || object["actor"]
Map.put(object, "attributedTo", attributed_to)
end
# TODO: Revisit this
def prepare_attachments(%{"type" => "ChatMessage"} = object), do: object
def prepare_attachments(object) do
attachments =
object
|> Map.get("attachment", [])
|> Enum.map(fn data ->
[%{"mediaType" => media_type, "href" => href} = url | _] = data["url"]
%{
"url" => href,
"mediaType" => media_type,
"name" => data["name"],
"type" => "Document"
}
|> Maps.put_if_present("width", url["width"])
|> Maps.put_if_present("height", url["height"])
|> Maps.put_if_present("blurhash", data["blurhash"])
end)
Map.put(object, "attachment", attachments)
end
def strip_internal_fields(object) do
Map.drop(object, Pleroma.Constants.object_internal_fields())
end
defp strip_internal_tags(%{"tag" => tags} = object) do
tags = Enum.filter(tags, fn x -> is_map(x) end)
Map.put(object, "tag", tags)
end
defp strip_internal_tags(object), do: object
def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
Map.put(data, "url", url["href"])
end
def maybe_fix_user_url(data), do: data
def maybe_fix_user_object(data), do: maybe_fix_user_url(data)
end
diff --git a/lib/pleroma/web/activity_pub/transmogrifier/api.ex b/lib/pleroma/web/activity_pub/transmogrifier/api.ex
index b9f65d17c..f2e416575 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier/api.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier/api.ex
@@ -1,11 +1,11 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Transmogrifier.API do
@moduledoc """
Behaviour for the subset of Transmogrifier used by Publisher.
"""
- @callback prepare_outgoing(map()) :: {:ok, map()} | {:error, term()}
+ @callback prepare_activity(map()) :: {:ok, map()} | {:error, term()}
end
diff --git a/lib/pleroma/web/activity_pub/views/object_view.ex b/lib/pleroma/web/activity_pub/views/object_view.ex
index a672ccc8b..9334b797a 100644
--- a/lib/pleroma/web/activity_pub/views/object_view.ex
+++ b/lib/pleroma/web/activity_pub/views/object_view.ex
@@ -1,111 +1,111 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectView do
use Pleroma.Web, :view
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.CollectionViewHelper
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ControllerHelper
def render("object.json", %{object: %Object{} = object}) do
base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header(object.data)
additional = Transmogrifier.prepare_object(object.data)
Map.merge(base, additional)
end
def render("object.json", %{object: %Activity{} = activity}) do
- {:ok, ap_data} = Transmogrifier.prepare_outgoing(activity.data)
+ {:ok, ap_data} = Transmogrifier.prepare_activity(activity.data)
ap_data
end
def render("object_replies.json", %{
conn: conn,
render_params: %{object_ap_id: object_ap_id, page: "true"} = params
}) do
params = Map.put_new(params, :limit, 40)
items = ActivityPub.fetch_objects_for_replies_collection(object_ap_id, params)
display_items = map_reply_collection_items(items)
pagination = ControllerHelper.get_pagination_fields(conn, items, %{}, :asc)
CollectionViewHelper.collection_page_keyset(display_items, pagination, params[:limit])
end
def render(
"object_replies.json",
%{
render_params: %{object_ap_id: object_ap_id} = params
} = opts
) do
params =
params
|> Map.drop([:max_id, :min_id, :since_id, :object_ap_id])
|> Map.put_new(:limit, 40)
|> Map.put(:total, true)
%{total: total, items: items} =
ActivityPub.fetch_objects_for_replies_collection(object_ap_id, params)
display_items = map_reply_collection_items(items)
first_pagination = reply_collection_first_pagination(items, opts)
col_ap =
%{
"id" => object_ap_id <> "/replies",
"type" => "OrderedCollection",
"totalItems" => total
}
col_ap =
if total > 0 do
first_page =
CollectionViewHelper.collection_page_keyset(
display_items,
first_pagination,
params[:limit],
true
)
Map.put(col_ap, "first", first_page)
else
col_ap
end
if params[:skip_ap_ctx] do
col_ap
else
Map.merge(col_ap, Pleroma.Web.ActivityPub.Utils.make_json_ld_header())
end
end
defp map_reply_collection_items(items), do: Enum.map(items, fn %{ap_id: ap_id} -> ap_id end)
defp reply_collection_first_pagination(items, %{conn: %Plug.Conn{} = conn}) do
pagination = ControllerHelper.get_pagination_fields(conn, items, %{"page" => true}, :asc)
Map.put(pagination, "id", Phoenix.Controller.current_url(conn, %{"page" => true}))
end
defp reply_collection_first_pagination(items, %{render_params: %{object_ap_id: object_ap_id}}) do
%{
"id" => object_ap_id <> "/replies?page=true",
"partOf" => object_ap_id <> "/replies"
}
|> then(fn m ->
case items do
[] ->
m
i ->
next_id = object_ap_id <> "/replies?page=true&min_id=#{List.last(i)[:id]}"
Map.put(m, "next", next_id)
end
end)
end
end
diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex
index 4362db324..c7fb2a1d7 100644
--- a/lib/pleroma/web/activity_pub/views/user_view.ex
+++ b/lib/pleroma/web/activity_pub/views/user_view.ex
@@ -1,338 +1,338 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.UserView do
use Pleroma.Web, :view
alias Pleroma.Keys
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.CollectionViewHelper
alias Pleroma.Web.ActivityPub.ObjectView
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Router.Helpers
import Ecto.Query
def render("endpoints.json", %{user: %User{nickname: nil, local: true} = _user}) do
%{"sharedInbox" => Helpers.activity_pub_url(Endpoint, :inbox)}
end
def render("endpoints.json", %{user: %User{local: true} = _user}) do
%{
"oauthAuthorizationEndpoint" => Helpers.o_auth_url(Endpoint, :authorize),
"oauthRegistrationEndpoint" => Helpers.app_url(Endpoint, :create),
"oauthTokenEndpoint" => Helpers.o_auth_url(Endpoint, :token_exchange),
"sharedInbox" => Helpers.activity_pub_url(Endpoint, :inbox),
"uploadMedia" => Helpers.activity_pub_url(Endpoint, :upload_media)
}
end
def render("endpoints.json", _), do: %{}
def render("service.json", %{user: user}) do
{:ok, _, public_key} = Keys.keys_from_pem(user.keys)
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key])
endpoints = render("endpoints.json", %{user: user})
%{
"id" => user.ap_id,
"type" => "Application",
"following" => "#{user.ap_id}/following",
"followers" => "#{user.ap_id}/followers",
"inbox" => "#{user.ap_id}/inbox",
"outbox" => "#{user.ap_id}/outbox",
"name" => "Pleroma",
"summary" =>
"An internal service actor for this Pleroma instance. No user-serviceable parts inside.",
"url" => user.ap_id,
"manuallyApprovesFollowers" => false,
"publicKey" => %{
"id" => "#{user.ap_id}#main-key",
"owner" => user.ap_id,
"publicKeyPem" => public_key
},
"endpoints" => endpoints,
"invisible" => User.invisible?(user)
}
|> Map.merge(Utils.make_json_ld_header())
end
# the instance itself is not a Person, but instead an Application
def render("user.json", %{user: %User{nickname: nil} = user}),
do: render("service.json", %{user: user})
def render("user.json", %{user: %User{nickname: "internal." <> _} = user}) do
render("service.json", %{user: user})
|> Map.merge(%{
"preferredUsername" => user.nickname,
"webfinger" => "acct:#{User.full_nickname(user)}"
})
end
def render("user.json", %{user: user}) do
{:ok, _, public_key} = Keys.keys_from_pem(user.keys)
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key])
user = User.sanitize_html(user)
endpoints = render("endpoints.json", %{user: user})
emoji_tags = Transmogrifier.take_emoji_tags(user)
fields = Enum.map(user.fields, &Map.put(&1, "type", "PropertyValue"))
capabilities =
if is_boolean(user.accepts_chat_messages) do
%{
"acceptsChatMessages" => user.accepts_chat_messages
}
else
%{}
end
birthday =
if user.show_birthday && user.birthday,
do: Date.to_iso8601(user.birthday),
else: nil
%{
"id" => user.ap_id,
"type" => user.actor_type,
"following" => "#{user.ap_id}/following",
"followers" => "#{user.ap_id}/followers",
"inbox" => "#{user.ap_id}/inbox",
"outbox" => "#{user.ap_id}/outbox",
"featured" => "#{user.ap_id}/collections/featured",
"preferredUsername" => user.nickname,
"name" => user.name,
"summary" => user.bio,
"url" => user.ap_id,
"manuallyApprovesFollowers" => user.is_locked,
"publicKey" => %{
"id" => "#{user.ap_id}#main-key",
"owner" => user.ap_id,
"publicKeyPem" => public_key
},
"endpoints" => endpoints,
"attachment" => fields,
"tag" => emoji_tags,
# Note: key name is indeed "discoverable" (not an error)
"discoverable" => user.is_discoverable,
"capabilities" => capabilities,
"alsoKnownAs" => user.also_known_as,
"vcard:bday" => birthday,
"webfinger" => "acct:#{User.full_nickname(user)}",
"published" => Pleroma.Web.CommonAPI.Utils.to_masto_date(user.inserted_at)
}
|> Map.merge(
maybe_make_image(
&User.avatar_url/2,
User.image_description(user.avatar, nil),
"icon",
user
)
)
|> Map.merge(
maybe_make_image(
&User.banner_url/2,
User.image_description(user.banner, nil),
"image",
user
)
)
|> Map.merge(Utils.make_json_ld_header())
end
def render("following.json", %{user: user, page: page} = opts) do
showing_items = (opts[:for] && opts[:for] == user) || !user.hide_follows
showing_count = showing_items || !user.hide_follows_count
query = User.get_friends_query(user)
query = from(user in query, select: [:ap_id])
following = Repo.all(query)
total =
if showing_count do
length(following)
else
0
end
CollectionViewHelper.collection_page_offset(
following,
"#{user.ap_id}/following",
page,
showing_items,
total
)
|> Map.merge(Utils.make_json_ld_header())
end
def render("following.json", %{user: user} = opts) do
showing_items = (opts[:for] && opts[:for] == user) || !user.hide_follows
showing_count = showing_items || !user.hide_follows_count
query = User.get_friends_query(user)
query = from(user in query, select: [:ap_id])
following = Repo.all(query)
total =
if showing_count do
length(following)
else
0
end
%{
"id" => "#{user.ap_id}/following",
"type" => "OrderedCollection",
"totalItems" => total,
"first" =>
if showing_items do
CollectionViewHelper.collection_page_offset(
following,
"#{user.ap_id}/following",
1,
!user.hide_follows
)
else
"#{user.ap_id}/following?page=1"
end
}
|> Map.merge(Utils.make_json_ld_header())
end
def render("followers.json", %{user: user, page: page} = opts) do
showing_items = (opts[:for] && opts[:for] == user) || !user.hide_followers
showing_count = showing_items || !user.hide_followers_count
query = User.get_followers_query(user)
query = from(user in query, select: [:ap_id])
followers = Repo.all(query)
total =
if showing_count do
length(followers)
else
0
end
CollectionViewHelper.collection_page_offset(
followers,
"#{user.ap_id}/followers",
page,
showing_items,
total
)
|> Map.merge(Utils.make_json_ld_header())
end
def render("followers.json", %{user: user} = opts) do
showing_items = (opts[:for] && opts[:for] == user) || !user.hide_followers
showing_count = showing_items || !user.hide_followers_count
query = User.get_followers_query(user)
query = from(user in query, select: [:ap_id])
followers = Repo.all(query)
total =
if showing_count do
length(followers)
else
0
end
%{
"id" => "#{user.ap_id}/followers",
"type" => "OrderedCollection",
"first" =>
if showing_items do
CollectionViewHelper.collection_page_offset(
followers,
"#{user.ap_id}/followers",
1,
showing_items
)
else
"#{user.ap_id}/followers?page=1"
end
}
|> maybe_put_total_items(showing_count, total)
|> Map.merge(Utils.make_json_ld_header())
end
def render("activity_collection.json", %{iri: iri}) do
%{
"id" => iri,
"type" => "OrderedCollection",
"first" => "#{iri}?page=true"
}
|> Map.merge(Utils.make_json_ld_header())
end
def render("activity_collection_page.json", %{
activities: activities,
pagination: pagination
}) do
collection =
Enum.map(activities, fn activity ->
- {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
+ {:ok, data} = Transmogrifier.prepare_activity(activity.data)
data
end)
CollectionViewHelper.collection_page_keyset(collection, pagination)
end
def render("featured.json", %{
user: %{featured_address: featured_address, pinned_objects: pinned_objects}
}) do
objects =
pinned_objects
|> Enum.sort_by(fn {_, pinned_at} -> pinned_at end, &>=/2)
|> Enum.map(fn {id, _} ->
ObjectView.render("object.json", %{object: Object.get_cached_by_ap_id(id)})
end)
%{
"id" => featured_address,
"type" => "OrderedCollection",
"orderedItems" => objects,
"totalItems" => length(objects)
}
|> Map.merge(Utils.make_json_ld_header())
end
defp maybe_put_total_items(map, false, _total), do: map
defp maybe_put_total_items(map, true, total) do
Map.put(map, "totalItems", total)
end
defp maybe_make_image(func, description, key, user) do
if image = func.(user, no_default: true) do
%{
key =>
%{
"type" => "Image",
"url" => image
}
|> maybe_put_description(description)
}
else
%{}
end
end
defp maybe_put_description(map, description) when is_binary(description) do
Map.put(map, "name", description)
end
defp maybe_put_description(map, _description), do: map
end
diff --git a/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_util_operation.ex
similarity index 84%
rename from lib/pleroma/web/api_spec/operations/twitter_util_operation.ex
rename to lib/pleroma/web/api_spec/operations/pleroma_util_operation.ex
index 724d873c0..4bb4f112c 100644
--- a/lib/pleroma/web/api_spec/operations/twitter_util_operation.ex
+++ b/lib/pleroma/web/api_spec/operations/pleroma_util_operation.ex
@@ -1,442 +1,396 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
+defmodule Pleroma.Web.ApiSpec.PleromaUtilOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
import Pleroma.Web.ApiSpec.Helpers
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def emoji_operation do
%Operation{
tags: ["Custom emojis"],
summary: "List all custom emojis",
- operationId: "UtilController.emoji",
+ operationId: "PleromaAPI.UtilController.emoji",
parameters: [],
responses: %{
200 =>
Operation.response("List", "application/json", %Schema{
type: :object,
additionalProperties: %Schema{
type: :object,
properties: %{
image_url: %Schema{type: :string},
tags: %Schema{type: :array, items: %Schema{type: :string}}
},
extensions: %{"x-additionalPropertiesName": "Emoji name"}
},
example: %{
"firefox" => %{
"image_url" => "/emoji/firefox.png",
"tag" => ["Fun"]
}
}
})
}
}
end
def frontend_configurations_operation do
%Operation{
tags: ["Others"],
summary: "Dump frontend configurations",
- operationId: "UtilController.frontend_configurations",
+ operationId: "PleromaAPI.UtilController.frontend_configurations",
parameters: [],
responses: %{
200 =>
Operation.response("List", "application/json", %Schema{
type: :object,
additionalProperties: %Schema{
type: :object,
description:
"Opaque object representing the instance-wide configuration for the frontend",
extensions: %{"x-additionalPropertiesName": "Frontend name"}
}
})
}
}
end
def change_password_operation do
%Operation{
tags: ["Account credentials"],
summary: "Change account password",
security: [%{"oAuth" => ["write:accounts"]}],
- operationId: "UtilController.change_password",
+ operationId: "PleromaAPI.UtilController.change_password",
requestBody: request_body("Parameters", change_password_request(), required: true),
responses: %{
200 =>
Operation.response("Success", "application/json", %Schema{
type: :object,
properties: %{status: %Schema{type: :string, example: "success"}}
}),
400 => Operation.response("Error", "application/json", ApiError),
403 => Operation.response("Error", "application/json", ApiError)
}
}
end
defp change_password_request do
%Schema{
title: "ChangePasswordRequest",
description: "POST body for changing the account's password",
type: :object,
required: [:password, :new_password, :new_password_confirmation],
properties: %{
password: %Schema{type: :string, description: "Current password"},
new_password: %Schema{type: :string, description: "New password"},
new_password_confirmation: %Schema{
type: :string,
description: "New password, confirmation"
}
}
}
end
def change_email_operation do
%Operation{
tags: ["Account credentials"],
summary: "Change account email",
security: [%{"oAuth" => ["write:accounts"]}],
- operationId: "UtilController.change_email",
+ operationId: "PleromaAPI.UtilController.change_email",
requestBody: request_body("Parameters", change_email_request(), required: true),
responses: %{
200 =>
Operation.response("Success", "application/json", %Schema{
type: :object,
properties: %{status: %Schema{type: :string, example: "success"}}
}),
400 => Operation.response("Error", "application/json", ApiError),
403 => Operation.response("Error", "application/json", ApiError)
}
}
end
defp change_email_request do
%Schema{
title: "ChangeEmailRequest",
description: "POST body for changing the account's email",
type: :object,
required: [:email, :password],
properties: %{
email: %Schema{
type: :string,
description: "New email. Set to blank to remove the user's email."
},
password: %Schema{type: :string, description: "Current password"}
}
}
end
def update_notification_settings_operation do
%Operation{
tags: ["Settings"],
summary: "Update Notification Settings",
security: [%{"oAuth" => ["write:accounts"]}],
- operationId: "UtilController.update_notification_settings",
+ operationId: "PleromaAPI.UtilController.update_notification_settings",
parameters: [
Operation.parameter(
:block_from_strangers,
:query,
BooleanLike.schema(),
"blocks notifications from accounts you do not follow"
),
Operation.parameter(
:hide_notification_contents,
:query,
BooleanLike.schema(),
"removes the contents of a message from the push notification"
)
],
requestBody: nil,
responses: %{
200 =>
Operation.response("Success", "application/json", %Schema{
type: :object,
properties: %{status: %Schema{type: :string, example: "success"}}
}),
400 => Operation.response("Error", "application/json", ApiError)
}
}
end
def disable_account_operation do
%Operation{
tags: ["Account credentials"],
summary: "Disable Account",
security: [%{"oAuth" => ["write:accounts"]}],
- operationId: "UtilController.disable_account",
+ operationId: "PleromaAPI.UtilController.disable_account",
parameters: [
Operation.parameter(:password, :query, :string, "Password")
],
responses: %{
200 =>
Operation.response("Success", "application/json", %Schema{
type: :object,
properties: %{status: %Schema{type: :string, example: "success"}}
}),
403 => Operation.response("Error", "application/json", ApiError)
}
}
end
def delete_account_operation do
%Operation{
tags: ["Account credentials"],
summary: "Delete Account",
security: [%{"oAuth" => ["write:accounts"]}],
- operationId: "UtilController.delete_account",
+ operationId: "PleromaAPI.UtilController.delete_account",
parameters: [
Operation.parameter(:password, :query, :string, "Password")
],
requestBody: request_body("Parameters", delete_account_request(), required: false),
responses: %{
200 =>
Operation.response("Success", "application/json", %Schema{
type: :object,
properties: %{status: %Schema{type: :string, example: "success"}}
}),
403 => Operation.response("Error", "application/json", ApiError)
}
}
end
def captcha_operation do
%Operation{
summary: "Get a captcha",
- operationId: "UtilController.captcha",
+ operationId: "PleromaAPI.UtilController.captcha",
tags: ["Others"],
parameters: [],
responses: %{
200 => Operation.response("Success", "application/json", %Schema{type: :object})
}
}
end
def move_account_operation do
%Operation{
tags: ["Account credentials"],
summary: "Move account",
security: [%{"oAuth" => ["write:accounts"]}],
- operationId: "UtilController.move_account",
+ operationId: "PleromaAPI.UtilController.move_account",
requestBody: request_body("Parameters", move_account_request(), required: true),
responses: %{
200 =>
Operation.response("Success", "application/json", %Schema{
type: :object,
properties: %{status: %Schema{type: :string, example: "success"}}
}),
400 => Operation.response("Error", "application/json", ApiError),
403 => Operation.response("Error", "application/json", ApiError),
404 => Operation.response("Error", "application/json", ApiError)
}
}
end
defp move_account_request do
%Schema{
title: "MoveAccountRequest",
description: "POST body for moving the account",
type: :object,
required: [:password, :target_account],
properties: %{
password: %Schema{type: :string, description: "Current password"},
target_account: %Schema{
type: :string,
description: "The nickname of the target account to move to"
}
}
}
end
def list_aliases_operation do
%Operation{
tags: ["Account credentials"],
summary: "List account aliases",
security: [%{"oAuth" => ["read:accounts"]}],
- operationId: "UtilController.list_aliases",
+ operationId: "PleromaAPI.UtilController.list_aliases",
responses: %{
200 =>
Operation.response("Success", "application/json", %Schema{
type: :object,
properties: %{
aliases: %Schema{
type: :array,
items: %Schema{type: :string},
example: ["foo@example.org"]
}
}
}),
400 => Operation.response("Error", "application/json", ApiError),
403 => Operation.response("Error", "application/json", ApiError)
}
}
end
def add_alias_operation do
%Operation{
tags: ["Account credentials"],
summary: "Add an alias to this account",
security: [%{"oAuth" => ["write:accounts"]}],
- operationId: "UtilController.add_alias",
+ operationId: "PleromaAPI.UtilController.add_alias",
requestBody: request_body("Parameters", add_alias_request(), required: true),
responses: %{
200 =>
Operation.response("Success", "application/json", %Schema{
type: :object,
properties: %{
status: %Schema{
type: :string,
example: "success"
}
}
}),
400 => Operation.response("Error", "application/json", ApiError),
403 => Operation.response("Error", "application/json", ApiError),
404 => Operation.response("Error", "application/json", ApiError)
}
}
end
defp add_alias_request do
%Schema{
title: "AddAliasRequest",
description: "PUT body for adding aliases",
type: :object,
required: [:alias],
properties: %{
alias: %Schema{
type: :string,
description: "The nickname of the account to add to aliases"
}
}
}
end
def delete_alias_operation do
%Operation{
tags: ["Account credentials"],
summary: "Delete an alias from this account",
security: [%{"oAuth" => ["write:accounts"]}],
- operationId: "UtilController.delete_alias",
+ operationId: "PleromaAPI.UtilController.delete_alias",
requestBody: request_body("Parameters", delete_alias_request(), required: true),
responses: %{
200 =>
Operation.response("Success", "application/json", %Schema{
type: :object,
properties: %{
status: %Schema{
type: :string,
example: "success"
}
}
}),
400 => Operation.response("Error", "application/json", ApiError),
403 => Operation.response("Error", "application/json", ApiError),
404 => Operation.response("Error", "application/json", ApiError)
}
}
end
defp delete_alias_request do
%Schema{
title: "DeleteAliasRequest",
description: "PUT body for deleting aliases",
type: :object,
required: [:alias],
properties: %{
alias: %Schema{
type: :string,
description: "The nickname of the account to delete from aliases"
}
}
}
end
def healthcheck_operation do
%Operation{
tags: ["Others"],
summary: "Quick status check on the instance",
security: [%{"oAuth" => ["write:accounts"]}],
- operationId: "UtilController.healthcheck",
+ operationId: "PleromaAPI.UtilController.healthcheck",
parameters: [],
responses: %{
200 => Operation.response("Healthy", "application/json", %Schema{type: :object}),
503 =>
Operation.response("Disabled or Unhealthy", "application/json", %Schema{type: :object})
}
}
end
- def remote_subscribe_operation do
- %Operation{
- tags: ["Remote interaction"],
- summary: "Remote Subscribe",
- operationId: "UtilController.remote_subscribe",
- parameters: [],
- responses: %{200 => Operation.response("Web Page", "test/html", %Schema{type: :string})}
- }
- end
-
- def remote_interaction_operation do
- %Operation{
- tags: ["Remote interaction"],
- summary: "Remote interaction",
- operationId: "UtilController.remote_interaction",
- requestBody: request_body("Parameters", remote_interaction_request(), required: true),
- responses: %{
- 200 =>
- Operation.response("Remote interaction URL", "application/json", %Schema{type: :object})
- }
- }
- end
-
- defp remote_interaction_request do
- %Schema{
- title: "RemoteInteractionRequest",
- description: "POST body for remote interaction",
- type: :object,
- required: [:ap_id, :profile],
- properties: %{
- ap_id: %Schema{type: :string, description: "Profile or status ActivityPub ID"},
- profile: %Schema{type: :string, description: "Remote profile webfinger"}
- }
- }
- end
-
- def show_subscribe_form_operation do
- %Operation{
- tags: ["Remote interaction"],
- summary: "Show remote subscribe form",
- operationId: "UtilController.show_subscribe_form",
- parameters: [],
- responses: %{200 => Operation.response("Web Page", "test/html", %Schema{type: :string})}
- }
- end
-
defp delete_account_request do
%Schema{
title: "AccountDeleteRequest",
description: "POST body for deleting one's own account",
type: :object,
properties: %{
password: %Schema{
type: :string,
description: "The user's own password for confirmation.",
format: :password
}
},
example: %{
"password" => "prettyp0ony1313"
}
}
end
end
diff --git a/lib/pleroma/web/api_spec/operations/remote_interaction_operation.ex b/lib/pleroma/web/api_spec/operations/remote_interaction_operation.ex
new file mode 100644
index 000000000..54edbcf32
--- /dev/null
+++ b/lib/pleroma/web/api_spec/operations/remote_interaction_operation.ex
@@ -0,0 +1,99 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ApiSpec.RemoteInteractionOperation do
+ alias OpenApiSpex.Operation
+ alias OpenApiSpex.Schema
+
+ import Pleroma.Web.ApiSpec.Helpers
+
+ def open_api_operation(action) do
+ operation = String.to_existing_atom("#{action}_operation")
+ apply(__MODULE__, operation, [])
+ end
+
+ def remote_interaction_operation do
+ %Operation{
+ tags: ["Remote interaction"],
+ summary: "Remote interaction",
+ operationId: "RemoteInteractionController.remote_interaction",
+ requestBody: request_body("Parameters", remote_interaction_request(), required: true),
+ responses: %{
+ 200 =>
+ Operation.response("Remote interaction URL", "application/json", %Schema{type: :object})
+ }
+ }
+ end
+
+ defp remote_interaction_request do
+ %Schema{
+ title: "RemoteInteractionRequest",
+ description: "POST body for remote interaction",
+ type: :object,
+ required: [:ap_id, :profile],
+ properties: %{
+ ap_id: %Schema{type: :string, description: "Profile or status ActivityPub ID"},
+ profile: %Schema{type: :string, description: "Remote profile webfinger"}
+ }
+ }
+ end
+
+ def follow_operation do
+ %Operation{
+ tags: ["Remote interaction"],
+ summary: "Display follow form",
+ operationId: "RemoteInteractionController.follow",
+ parameters: [],
+ responses: %{
+ 200 => Operation.response("Web Page", "text/html", %Schema{type: :string}),
+ 302 => Operation.response("Redirect to the status page", nil, nil)
+ }
+ }
+ end
+
+ def do_follow_operation do
+ %Operation{
+ tags: ["Remote interaction"],
+ summary: "Perform follow activity",
+ operationId: "RemoteInteractionController.do_follow",
+ parameters: [],
+ responses: %{
+ 200 => Operation.response("Web page", "text/html", %Schema{type: :string}),
+ 302 => Operation.response("Redirect to the account page", nil, nil)
+ }
+ }
+ end
+
+ def authorize_interaction_operation do
+ %Operation{
+ tags: ["Remote interaction"],
+ summary: "Authorize remote interaction",
+ operationId: "RemoteInteractionController.authorize_interaction",
+ parameters: [],
+ responses: %{
+ 302 => Operation.response("Redirect to remote_interaction path", nil, nil)
+ }
+ }
+ end
+
+ def show_subscribe_form_operation do
+ %Operation{
+ tags: ["Remote interaction"],
+ summary: "Show remote subscribe form",
+ operationId: "RemoteInteractionController.show_subscribe_form",
+ parameters: [],
+ responses: %{200 => Operation.response("Web Page", "text/html", %Schema{type: :string})}
+ }
+ end
+
+ def remote_subscribe_operation do
+ %Operation{
+ tags: ["Remote interaction"],
+ summary: "Remote Subscribe",
+ operationId: "RemoteInteractionController.remote_subscribe",
+ parameters: [],
+ responses: %{200 => Operation.response("Web Page", "text/html", %Schema{type: :string})}
+ }
+ end
+end
diff --git a/lib/pleroma/web/api_spec/schemas/bookmark_folder.ex b/lib/pleroma/web/api_spec/schemas/bookmark_folder.ex
index e8b4f43b7..f5ce9e8a1 100644
--- a/lib/pleroma/web/api_spec/schemas/bookmark_folder.ex
+++ b/lib/pleroma/web/api_spec/schemas/bookmark_folder.ex
@@ -1,26 +1,32 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.BookmarkFolder do
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
require OpenApiSpex
OpenApiSpex.schema(%{
title: "BookmarkFolder",
description: "Response schema for a bookmark folder",
type: :object,
properties: %{
id: FlakeID,
name: %Schema{type: :string, description: "Folder name"},
- emoji: %Schema{type: :string, description: "Folder emoji", nullable: true}
+ emoji: %Schema{type: :string, description: "Folder emoji", nullable: true},
+ emoji_url: %Schema{
+ type: :string,
+ description: "URL of the folder emoji if it's a custom emoji, null otherwise",
+ nullable: true
+ }
},
example: %{
"id" => "9toJCu5YZW7O7gfvH6",
"name" => "Read later",
- "emoji" => nil
+ "emoji" => nil,
+ "emoji_url" => nil
}
})
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
index 6dc731ed4..6d5851029 100644
--- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex
@@ -1,698 +1,698 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AccountController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper,
only: [
add_link_headers: 2,
assign_account_by_id: 2,
embed_relationships?: 1,
json_response: 3
]
alias Pleroma.Maps
alias Pleroma.User
alias Pleroma.UserNote
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.ListView
alias Pleroma.Web.MastodonAPI.MastodonAPI
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.OAuth.OAuthController
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.Plugs.RateLimiter
- alias Pleroma.Web.TwitterAPI.TwitterAPI
+ alias Pleroma.Web.Registration
alias Pleroma.Web.Utils.Params
plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(:skip_auth when action in [:create])
plug(:skip_public_check when action in [:show, :statuses])
plug(
OAuthScopesPlug,
%{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
when action in [:show, :followers, :following, :lookup, :endorsements]
)
plug(
OAuthScopesPlug,
%{fallback: :proceed_unauthenticated, scopes: ["read:statuses"]}
when action == :statuses
)
plug(
OAuthScopesPlug,
%{scopes: ["read:accounts"]}
when action in [:verify_credentials, :endorsements, :own_endorsements]
)
plug(
OAuthScopesPlug,
%{scopes: ["write:accounts"]}
when action in [:update_credentials, :note, :endorse, :unendorse]
)
plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)
plug(
OAuthScopesPlug,
%{scopes: ["follow", "read:blocks"]} when action == :blocks
)
plug(
OAuthScopesPlug,
%{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
)
plug(
OAuthScopesPlug,
%{scopes: ["read:follows"]} when action in [:relationships, :familiar_followers]
)
plug(
OAuthScopesPlug,
%{scopes: ["follow", "write:follows"]}
when action in [:follow_by_uri, :follow, :unfollow, :remove_from_followers]
)
plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
@relationship_actions [:follow, :unfollow, :remove_from_followers]
@needs_account ~W(
followers following lists follow unfollow mute unmute block unblock
note endorse unendorse endorsements remove_from_followers
)a
plug(
RateLimiter,
[name: :relation_id_action, params: ["id", "uri"]] when action in @relationship_actions
)
plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
plug(RateLimiter, [name: :app_account_creation] when action == :create)
plug(:assign_account_by_id when action in @needs_account)
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AccountOperation
@doc "POST /api/v1/accounts"
def create(
%{assigns: %{app: app}, private: %{open_api_spex: %{body_params: params}}} = conn,
_params
) do
with :ok <- validate_email_param(params),
- :ok <- TwitterAPI.validate_captcha(app, params),
- {:ok, user} <- TwitterAPI.register_user(params),
+ :ok <- Registration.validate_captcha(app, params),
+ {:ok, user} <- Registration.register_user(params),
{_, {:ok, token}} <-
{:login, OAuthController.login(user, app, app.scopes)} do
OAuthController.after_token_exchange(conn, %{user: user, token: token})
else
{:login, {:account_status, :confirmation_pending}} ->
json_response(conn, :ok, %{
message: "You have been registered. Please check your email for further instructions.",
identifier: "missing_confirmed_email"
})
{:login, {:account_status, :approval_pending}} ->
json_response(conn, :ok, %{
message:
"You have been registered. You'll be able to log in once your account is approved.",
identifier: "awaiting_approval"
})
{:login, _} ->
json_response(conn, :ok, %{
message:
"You have been registered. Some post-registration steps may be pending. " <>
"Please log in manually.",
identifier: "manual_login_required"
})
{:error, error} ->
json_response(conn, :bad_request, %{error: error})
end
end
def create(%{assigns: %{app: _app}} = conn, _) do
render_error(conn, :bad_request, "Missing parameters")
end
def create(conn, _) do
render_error(conn, :forbidden, "Invalid credentials")
end
defp validate_email_param(%{email: email}) when not is_nil(email), do: :ok
defp validate_email_param(_) do
case Pleroma.Config.get([:instance, :account_activation_required]) do
true -> {:error, dgettext("errors", "Missing parameter: %{name}", name: "email")}
_ -> :ok
end
end
@doc "GET /api/v1/accounts/verify_credentials"
def verify_credentials(%{assigns: %{user: user}} = conn, _) do
chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
render(conn, "show.json",
user: user,
for: user,
with_pleroma_settings: true,
with_chat_token: chat_token
)
end
@doc "PATCH /api/v1/accounts/update_credentials"
def update_credentials(
%{assigns: %{user: user}, private: %{open_api_spex: %{body_params: params}}} = conn,
_params
) do
params =
params
|> Enum.filter(fn {_, value} -> not is_nil(value) end)
|> Enum.into(%{})
# We use an empty string as a special value to reset
# avatars, banners, backgrounds
user_image_value = fn
"" -> {:ok, nil}
value -> {:ok, value}
end
user_params =
[
:no_rich_text,
:hide_followers_count,
:hide_follows_count,
:hide_followers,
:hide_follows,
:hide_favorites,
:show_role,
:skip_thread_containment,
:allow_following_move,
:also_known_as,
:accepts_chat_messages,
:show_birthday
]
|> Enum.reduce(%{}, fn key, acc ->
Maps.put_if_present(acc, key, params[key], &{:ok, Params.truthy_param?(&1)})
end)
|> Maps.put_if_present(:name, params[:display_name])
|> Maps.put_if_present(:bio, params[:note])
|> Maps.put_if_present(:raw_bio, params[:note])
|> Maps.put_if_present(:avatar, params[:avatar], user_image_value)
|> Maps.put_if_present(:banner, params[:header], user_image_value)
|> Maps.put_if_present(:background, params[:pleroma_background_image], user_image_value)
|> Maps.put_if_present(
:raw_fields,
params[:fields_attributes],
&{:ok, normalize_fields_attributes(&1)}
)
|> Maps.put_if_present(:pleroma_settings_store, params[:pleroma_settings_store])
|> Maps.put_if_present(:default_scope, params[:default_scope])
|> Maps.put_if_present(:default_scope, params["source"]["privacy"])
|> Maps.put_if_present(:actor_type, params[:bot], fn bot ->
if bot, do: {:ok, "Service"}, else: {:ok, "Person"}
end)
|> Maps.put_if_present(:actor_type, params[:actor_type])
|> Maps.put_if_present(:also_known_as, params[:also_known_as])
# Note: param name is indeed :locked (not an error)
|> Maps.put_if_present(:is_locked, params[:locked])
# Note: param name is indeed :discoverable (not an error)
|> Maps.put_if_present(:is_discoverable, params[:discoverable])
|> Maps.put_if_present(:birthday, params[:birthday])
|> Maps.put_if_present(:language, Pleroma.Web.Gettext.normalize_locale(params[:language]))
|> Maps.put_if_present(:avatar_description, params[:avatar_description])
|> Maps.put_if_present(:header_description, params[:header_description])
# What happens here:
#
# We want to update the user through the pipeline, but the ActivityPub
# update information is not quite enough for this, because this also
# contains local settings that don't federate and don't even appear
# in the Update activity.
#
# So we first build the normal local changeset, then apply it to the
# user data, but don't persist it. With this, we generate the object
# data for our update activity. We feed this and the changeset as meta
# information into the pipeline, where they will be properly updated and
# federated.
with changeset <- User.update_changeset(user, user_params),
{:ok, unpersisted_user} <- Ecto.Changeset.apply_action(changeset, :update),
updated_object <-
Pleroma.Web.ActivityPub.UserView.render("user.json", user: unpersisted_user)
|> Map.delete("@context"),
{:ok, update_data, []} <- Builder.update(user, updated_object),
{:ok, _update, _} <-
Pipeline.common_pipeline(update_data,
local: true,
user_update_changeset: changeset
) do
render(conn, "show.json",
user: unpersisted_user,
for: unpersisted_user,
with_pleroma_settings: true
)
else
{:error, %Ecto.Changeset{errors: [avatar: {"file is too large", _}]}} ->
render_error(conn, :request_entity_too_large, "File is too large")
{:error, %Ecto.Changeset{errors: [banner: {"file is too large", _}]}} ->
render_error(conn, :request_entity_too_large, "File is too large")
{:error, %Ecto.Changeset{errors: [background: {"file is too large", _}]}} ->
render_error(conn, :request_entity_too_large, "File is too large")
{:error, %Ecto.Changeset{errors: [{:bio, {_, _}} | _]}} ->
render_error(conn, :request_entity_too_large, "Bio is too long")
{:error, %Ecto.Changeset{errors: [{:name, {_, _}} | _]}} ->
render_error(conn, :request_entity_too_large, "Name is too long")
{:error, %Ecto.Changeset{errors: [{:avatar_description, {_, _}} | _]}} ->
render_error(conn, :request_entity_too_large, "Avatar description is too long")
{:error, %Ecto.Changeset{errors: [{:header_description, {_, _}} | _]}} ->
render_error(conn, :request_entity_too_large, "Banner description is too long")
{:error, %Ecto.Changeset{errors: [{:fields, {"invalid", _}} | _]}} ->
render_error(conn, :request_entity_too_large, "One or more field entries are too long")
{:error, %Ecto.Changeset{errors: [{:fields, {_, _}} | _]}} ->
render_error(conn, :request_entity_too_large, "Too many field entries")
_e ->
render_error(conn, :forbidden, "Invalid request")
end
end
defp normalize_fields_attributes(fields) do
if(Enum.all?(fields, &is_tuple/1), do: Enum.map(fields, fn {_, v} -> v end), else: fields)
|> Enum.map(fn
%{} = field -> %{"name" => field.name, "value" => field.value}
field -> field
end)
end
@doc "GET /api/v1/accounts/relationships"
def relationships(
%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
_
) do
targets = User.get_all_by_ids(List.wrap(id))
render(conn, "relationships.json", user: user, targets: targets)
end
# Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
@doc "GET /api/v1/accounts/:id"
def show(
%{
assigns: %{user: for_user},
private: %{open_api_spex: %{params: %{id: nickname_or_id} = params}}
} = conn,
_params
) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
:visible <- User.visible_for(user, for_user) do
render(conn, "show.json",
user: user,
for: for_user,
embed_relationships: embed_relationships?(params)
)
else
error -> user_visibility_error(conn, error)
end
end
@doc "GET /api/v1/accounts/:id/statuses"
def statuses(
%{assigns: %{user: reading_user}, private: %{open_api_spex: %{params: params}}} = conn,
_params
) do
with %User{} = user <- User.get_cached_by_nickname_or_id(params.id, for: reading_user),
:visible <- User.visible_for(user, reading_user) do
params =
params
|> Map.delete(:tagged)
|> Map.put(:tag, params[:tagged])
activities = ActivityPub.fetch_user_activities(user, reading_user, params)
conn
|> add_link_headers(activities)
|> put_view(StatusView)
|> render("index.json",
activities: activities,
for: reading_user,
as: :activity,
with_muted: Map.get(params, :with_muted, false)
)
else
error -> user_visibility_error(conn, error)
end
end
defp user_visibility_error(conn, error) do
case error do
:restrict_unauthenticated ->
render_error(conn, :unauthorized, "This API requires an authenticated user")
_ ->
render_error(conn, :not_found, "Can't find user")
end
end
@doc "GET /api/v1/accounts/:id/followers"
def followers(
%{assigns: %{user: for_user, account: user}, private: %{open_api_spex: %{params: params}}} =
conn,
_params
) do
params =
params
|> Enum.map(fn {key, value} -> {to_string(key), value} end)
|> Enum.into(%{})
followers =
cond do
for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
user.hide_followers -> []
true -> MastodonAPI.get_followers(user, params)
end
conn
|> add_link_headers(followers)
# https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
|> render("index.json",
for: for_user,
users: followers,
as: :user,
embed_relationships: embed_relationships?(params)
)
end
@doc "GET /api/v1/accounts/:id/following"
def following(
%{assigns: %{user: for_user, account: user}, private: %{open_api_spex: %{params: params}}} =
conn,
_params
) do
params =
params
|> Enum.map(fn {key, value} -> {to_string(key), value} end)
|> Enum.into(%{})
followers =
cond do
for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
user.hide_follows -> []
true -> MastodonAPI.get_friends(user, params)
end
conn
|> add_link_headers(followers)
# https://git.pleroma.social/pleroma/pleroma-fe/-/issues/838#note_59223
|> render("index.json",
for: for_user,
users: followers,
as: :user,
embed_relationships: embed_relationships?(params)
)
end
@doc "GET /api/v1/accounts/:id/lists"
def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
lists = Pleroma.List.get_lists_account_belongs(user, account)
conn
|> put_view(ListView)
|> render("index.json", lists: lists)
end
@doc "POST /api/v1/accounts/:id/follow"
def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
{:error, "Can not follow yourself"}
end
def follow(
%{
assigns: %{user: follower, account: followed},
private: %{open_api_spex: %{body_params: params}}
} = conn,
_
) do
with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do
render(conn, "relationship.json", user: follower, target: followed)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end
@doc "POST /api/v1/accounts/:id/unfollow"
def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
{:error, "Can not unfollow yourself"}
end
def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
with {:ok, follower} <- CommonAPI.unfollow(followed, follower) do
render(conn, "relationship.json", user: follower, target: followed)
end
end
@doc "POST /api/v1/accounts/:id/mute"
def mute(
%{
assigns: %{user: muter, account: muted},
private: %{open_api_spex: %{body_params: params}}
} = conn,
_params
) do
params =
params
|> Map.put_new(:duration, Map.get(params, :expires_in, 0))
with {:ok, _user_relationships} <- User.mute(muter, muted, params) do
render(conn, "relationship.json", user: muter, target: muted)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end
@doc "POST /api/v1/accounts/:id/unmute"
def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
with {:ok, _user_relationships} <- User.unmute(muter, muted) do
render(conn, "relationship.json", user: muter, target: muted)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end
@doc "POST /api/v1/accounts/:id/block"
def block(
%{
assigns: %{user: blocker, account: blocked},
private: %{open_api_spex: %{body_params: params}}
} = conn,
_params
) do
with {:ok, _activity} <- CommonAPI.block(blocked, blocker, params) do
render(conn, "relationship.json", user: blocker, target: blocked)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end
@doc "POST /api/v1/accounts/:id/unblock"
def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
with {:ok, _activity} <- CommonAPI.unblock(blocked, blocker) do
render(conn, "relationship.json", user: blocker, target: blocked)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end
@doc "POST /api/v1/accounts/:id/note"
def note(
%{
assigns: %{user: noter, account: target},
private: %{open_api_spex: %{body_params: %{comment: comment}}}
} = conn,
_params
) do
with {:ok, _user_note} <- UserNote.create(noter, target, comment) do
render(conn, "relationship.json", user: noter, target: target)
end
end
@doc "POST /api/v1/accounts/:id/pin"
def endorse(%{assigns: %{user: endorser, account: endorsed}} = conn, _params) do
with {:ok, _user_relationships} <- User.endorse(endorser, endorsed) do
render(conn, "relationship.json", user: endorser, target: endorsed)
else
{:error, message} -> json_response(conn, :bad_request, %{error: message})
end
end
@doc "POST /api/v1/accounts/:id/unpin"
def unendorse(%{assigns: %{user: endorser, account: endorsed}} = conn, _params) do
with {:ok, _user_relationships} <- User.unendorse(endorser, endorsed) do
render(conn, "relationship.json", user: endorser, target: endorsed)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end
@doc "GET /api/v1/accounts/:id/endorsements"
def endorsements(%{assigns: %{user: for_user, account: user}} = conn, params) do
users =
user
|> User.endorsed_users_relation(_restrict_deactivated = true)
|> Pleroma.Repo.all()
conn
|> render("index.json",
for: for_user,
users: users,
as: :user,
embed_relationships: embed_relationships?(params)
)
end
@doc "POST /api/v1/accounts/:id/remove_from_followers"
def remove_from_followers(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
{:error, "Can not unfollow yourself"}
end
def remove_from_followers(%{assigns: %{user: followed, account: follower}} = conn, _params) do
with {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
render(conn, "relationship.json", user: followed, target: follower)
else
nil ->
render_error(conn, :not_found, "Record not found")
end
end
@doc "POST /api/v1/follows"
def follow_by_uri(%{private: %{open_api_spex: %{body_params: %{uri: uri}}}} = conn, _) do
case User.get_cached_by_nickname(uri) do
%User{} = user ->
conn
|> assign(:account, user)
|> follow(%{})
nil ->
{:error, :not_found}
end
end
@doc "GET /api/v1/mutes"
def mutes(%{assigns: %{user: user}} = conn, params) do
users =
user
|> User.muted_users_relation(_restrict_deactivated = true)
|> Pleroma.Pagination.fetch_paginated(params)
conn
|> add_link_headers(users)
|> render("index.json",
users: users,
for: user,
as: :user,
embed_relationships: embed_relationships?(params),
mutes: true
)
end
@doc "GET /api/v1/blocks"
def blocks(%{assigns: %{user: user}} = conn, params) do
users =
user
|> User.blocked_users_relation(_restrict_deactivated = true)
|> Pleroma.Pagination.fetch_paginated(params)
conn
|> add_link_headers(users)
|> render("index.json",
users: users,
for: user,
as: :user,
embed_relationships: embed_relationships?(params),
blocks: true
)
end
@doc "GET /api/v1/accounts/lookup"
def lookup(
%{assigns: %{user: for_user}, private: %{open_api_spex: %{params: %{acct: nickname}}}} =
conn,
_params
) do
with %User{} = user <- User.get_by_nickname(nickname),
:visible <- User.visible_for(user, for_user) do
render(conn, "show.json",
user: user,
skip_visibility_check: true
)
else
error -> user_visibility_error(conn, error)
end
end
@doc "GET /api/v1/endorsements"
def own_endorsements(%{assigns: %{user: user}} = conn, params) do
users =
user
|> User.endorsed_users_relation(_restrict_deactivated = true)
|> Pleroma.Repo.all()
conn
|> render("index.json",
users: users,
for: user,
as: :user,
embed_relationships: embed_relationships?(params)
)
end
@doc "GET /api/v1/accounts/familiar_followers"
def familiar_followers(
%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
_id
) do
users =
User.get_all_by_ids(List.wrap(id))
|> Enum.map(&%{id: &1.id, accounts: get_familiar_followers(&1, user)})
conn
|> render("familiar_followers.json",
for: user,
users: users,
as: :user
)
end
defp get_familiar_followers(%{id: id} = user, %{id: id}) do
User.get_familiar_followers(user, user)
end
defp get_familiar_followers(%{hide_followers: true}, _current_user) do
[]
end
defp get_familiar_followers(user, current_user) do
User.get_familiar_followers(user, current_user)
end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
index fbb54a171..653b5fc29 100644
--- a/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/auth_controller.ex
@@ -1,24 +1,24 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AuthController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
- alias Pleroma.Web.TwitterAPI.TwitterAPI
+ alias Pleroma.Web.Registration
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
plug(Pleroma.Web.Plugs.RateLimiter, [name: :password_reset] when action == :password_reset)
@doc "POST /auth/password"
def password_reset(conn, params) do
nickname_or_email = params["email"] || params["nickname"]
- TwitterAPI.password_reset(nickname_or_email)
+ Registration.password_reset(nickname_or_email)
json_response(conn, :no_content, "")
end
end
diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
index db2b61b3c..26e38aac8 100644
--- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex
@@ -1,731 +1,729 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.StatusController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper,
only: [try_render: 3, add_link_headers: 2]
require Ecto.Query
require Pleroma.Constants
alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.BookmarkFolder
alias Pleroma.Language.Translation
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.ScheduledActivity
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.ScheduledActivityView
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.Plugs.RateLimiter
plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(:skip_public_check when action in [:index, :show])
@unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
plug(
OAuthScopesPlug,
%{@unauthenticated_access | scopes: ["read:statuses"]}
when action in [
:index,
:show,
:context,
:show_history,
:show_source,
:quotes
]
)
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action == :translate)
plug(
OAuthScopesPlug,
%{scopes: ["write:statuses"]}
when action in [
:create,
:delete,
:reblog,
:unreblog,
:update
]
)
plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites)
plug(
OAuthScopesPlug,
%{scopes: ["write:favourites"]} when action in [:favourite, :unfavourite]
)
plug(
OAuthScopesPlug,
%{scopes: ["write:mutes"]} when action in [:mute_conversation, :unmute_conversation]
)
plug(
OAuthScopesPlug,
%{@unauthenticated_access | scopes: ["read:accounts"]}
when action in [:favourited_by, :reblogged_by]
)
plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action in [:pin, :unpin])
- # Note: scope not present in Mastodon: read:bookmarks
plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :bookmarks)
- # Note: scope not present in Mastodon: write:bookmarks
plug(
OAuthScopesPlug,
%{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
)
@rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete translate)a
plug(
RateLimiter,
[name: :status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: [:id]]
when action in ~w(reblog unreblog)a
)
plug(
RateLimiter,
[name: :status_id_action, bucket_name: "status_id_action:fav_unfav", params: [:id]]
when action in ~w(favourite unfavourite)a
)
plug(RateLimiter, [name: :statuses_actions] when action in @rate_limited_status_actions)
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.StatusOperation
@doc """
GET `/api/v1/statuses?ids[]=1&ids[]=2`
`ids` query param is required
"""
def index(
%{assigns: %{user: user}, private: %{open_api_spex: %{params: params}}} =
conn,
_
) do
ids = Map.get(params, :id, Map.get(params, :ids))
limit = 100
activities =
ids
|> Enum.take(limit)
|> Activity.all_by_ids_with_object()
|> Enum.filter(&Visibility.visible_for_user?(&1, user))
render(conn, "index.json",
activities: activities,
for: user,
as: :activity,
with_muted: Map.get(params, :with_muted, false)
)
end
@doc """
POST /api/v1/statuses
"""
# Creates a scheduled status when `scheduled_at` param is present and it's far enough
def create(
%{
assigns: %{user: user},
private: %{
open_api_spex: %{body_params: %{status: _, scheduled_at: scheduled_at} = params}
}
} = conn,
_
)
when not is_nil(scheduled_at) do
params =
Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
|> put_application(conn)
attrs = %{
params: Map.new(params, fn {key, value} -> {to_string(key), value} end),
scheduled_at: scheduled_at
}
with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},
{:ok, scheduled_activity} <- ScheduledActivity.create(user, attrs) do
conn
|> put_view(ScheduledActivityView)
|> render("show.json", scheduled_activity: scheduled_activity)
else
{:far_enough, _} ->
params = Map.drop(params, [:scheduled_at])
put_in(
conn,
[Access.key(:private), Access.key(:open_api_spex), Access.key(:body_params)],
params
)
|> do_create
error ->
error
end
end
# Creates a regular status
def create(
%{
private: %{open_api_spex: %{body_params: %{status: _}}}
} = conn,
_
) do
do_create(conn)
end
def create(
%{
assigns: %{user: _user},
private: %{open_api_spex: %{body_params: %{media_ids: _} = params}}
} = conn,
_
) do
params = Map.put(params, :status, "")
put_in(
conn,
[Access.key(:private), Access.key(:open_api_spex), Access.key(:body_params)],
params
)
|> do_create
end
defp do_create(
%{assigns: %{user: user}, private: %{open_api_spex: %{body_params: params}}} = conn
) do
params =
Map.put(params, :in_reply_to_status_id, params[:in_reply_to_id])
|> put_application(conn)
with {:ok, activity} <- CommonAPI.post(user, params) do
try_render(conn, "show.json",
activity: activity,
for: user,
as: :activity,
with_direct_conversation_id: true
)
else
{:error, {:reject, message}} ->
conn
|> put_status(:unprocessable_entity)
|> json(%{error: message})
{:error, message} ->
conn
|> put_status(:unprocessable_entity)
|> json(%{error: message})
end
end
@doc "GET /api/v1/statuses/:id/history"
def show_history(
%{assigns: assigns, private: %{open_api_spex: %{params: %{id: id} = params}}} = conn,
_
) do
with user = assigns[:user],
%Activity{} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.visible_for_user?(activity, user) do
try_render(conn, "history.json",
activity: activity,
for: user,
with_direct_conversation_id: true,
with_muted: Map.get(params, :with_muted, false)
)
else
_ -> {:error, :not_found}
end
end
@doc "GET /api/v1/statuses/:id/source"
def show_source(%{assigns: assigns, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
with user = assigns[:user],
%Activity{} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.visible_for_user?(activity, user) do
try_render(conn, "source.json",
activity: activity,
for: user
)
else
_ -> {:error, :not_found}
end
end
@doc "PUT /api/v1/statuses/:id"
def update(
%{
assigns: %{user: user},
private: %{open_api_spex: %{body_params: body_params, params: %{id: id} = params}}
} = conn,
_
) do
with {_, %Activity{}} = {_, activity} <- {:activity, Activity.get_by_id_with_object(id)},
{_, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
{_, true} <- {:is_create, activity.data["type"] == "Create"},
actor <- Activity.user_actor(activity),
{_, true} <- {:own_status, actor.id == user.id},
changes <- body_params |> put_application(conn),
{_, {:ok, _update_activity}} <- {:pipeline, CommonAPI.update(activity, user, changes)},
{_, %Activity{}} = {_, activity} <- {:refetched, Activity.get_by_id_with_object(id)} do
try_render(conn, "show.json",
activity: activity,
for: user,
with_direct_conversation_id: true,
with_muted: Map.get(params, :with_muted, false)
)
else
{:own_status, _} -> {:error, :forbidden}
{:pipeline, _} -> {:error, :internal_server_error}
_ -> {:error, :not_found}
end
end
@doc "GET /api/v1/statuses/:id"
def show(
%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id} = params}}} =
conn,
_
) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.visible_for_user?(activity, user) do
try_render(conn, "show.json",
activity: activity,
for: user,
with_direct_conversation_id: true,
with_muted: Map.get(params, :with_muted, false)
)
else
_ -> {:error, :not_found}
end
end
@doc "DELETE /api/v1/statuses/:id"
def delete(%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn, _) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
# CommonAPI already checks whether user is allowed to delete
{:ok, %Activity{}} <- CommonAPI.delete(id, user) do
try_render(conn, "show.json",
activity: activity,
for: user,
with_direct_conversation_id: true,
with_source: true
)
else
_e -> {:error, :not_found}
end
end
@doc "POST /api/v1/statuses/:id/reblog"
def reblog(
%{
assigns: %{user: user},
private: %{open_api_spex: %{body_params: params, params: %{id: ap_id_or_id}}}
} = conn,
_
) do
with {:ok, announce} <- CommonAPI.repeat(ap_id_or_id, user, params),
# CommonAPI already checks whether user is allowed to reblog
%Activity{} = announce <- Activity.normalize(announce.data) do
try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
end
end
@doc "POST /api/v1/statuses/:id/unreblog"
def unreblog(
%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: activity_id}}}} =
conn,
_
) do
with {:ok, _unannounce} <- CommonAPI.unrepeat(activity_id, user),
%Activity{} = activity <- Activity.get_by_id(activity_id) do
try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
end
end
@doc "POST /api/v1/statuses/:id/favourite"
def favourite(
%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: activity_id}}}} =
conn,
_
) do
with {:ok, _fav} <- CommonAPI.favorite(activity_id, user),
# CommonAPI already checks whether user is allowed to reblog
%Activity{} = activity <- Activity.get_by_id(activity_id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end
@doc "POST /api/v1/statuses/:id/unfavourite"
def unfavourite(
%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: activity_id}}}} =
conn,
_
) do
with {:ok, _unfav} <- CommonAPI.unfavorite(activity_id, user),
%Activity{} = activity <- Activity.get_by_id(activity_id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end
@doc "POST /api/v1/statuses/:id/pin"
def pin(
%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: ap_id_or_id}}}} =
conn,
_
) do
with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
else
# Order matters, if status is not owned by user and is not visible to user
# return 404 just like other endpoints
{:error, :pinned_statuses_limit_reached} ->
{:error, "You have already pinned the maximum number of statuses"}
{:error, :ownership_error} ->
{:error, :unprocessable_entity, "Someone else's status cannot be pinned"}
{:error, :visibility_error} ->
{:error, :not_found, "Record not found"}
{:error, :non_public_error} ->
{:error, :unprocessable_entity, "Non-public status cannot be pinned"}
error ->
error
end
end
@doc "POST /api/v1/statuses/:id/unpin"
def unpin(
%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: ap_id_or_id}}}} =
conn,
_
) do
# CommonAPI already checks whether user can unpin
with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
else
# Order matters, if status is not owned by user and is not visible to user
# return 404 just like other endpoints
{:error, :visibility_error} ->
{:error, :not_found, "Record not found"}
{:error, :ownership_error} ->
{:error, :unprocessable_entity, "Someone else's status cannot be unpinned"}
error ->
error
end
end
@doc "POST /api/v1/statuses/:id/bookmark"
def bookmark(
%{
assigns: %{user: user},
private: %{open_api_spex: %{body_params: body_params, params: %{id: id}}}
} = conn,
_
) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
%User{} = user <- User.get_cached_by_nickname(user.nickname),
true <- Visibility.visible_for_user?(activity, user),
folder_id <- Map.get(body_params, :folder_id, nil),
folder_id <-
if(folder_id && BookmarkFolder.belongs_to_user?(folder_id, user.id),
do: folder_id,
else: nil
),
{:ok, _bookmark} <- Bookmark.create(user.id, activity.id, folder_id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
else
false ->
{:error, :not_found, "Record not found"}
error ->
error
end
end
@doc "POST /api/v1/statuses/:id/unbookmark"
def unbookmark(
%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
_
) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
%User{} = user <- User.get_cached_by_nickname(user.nickname),
true <- Visibility.visible_for_user?(activity, user),
{:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
else
false ->
{:error, :not_found, "Record not found"}
error ->
error
end
end
@doc "POST /api/v1/statuses/:id/mute"
def mute_conversation(
%{
assigns: %{user: user},
private: %{open_api_spex: %{body_params: params, params: %{id: id}}}
} = conn,
_
) do
with %Activity{} = activity <- Activity.get_by_id(id),
# CommonAPI already checks whether user is allowed to mute
{:ok, activity} <- CommonAPI.add_mute(activity, user, params) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
else
{:error, :visibility_error} ->
{:error, :not_found, "Record not found"}
error ->
error
end
end
@doc "POST /api/v1/statuses/:id/unmute"
def unmute_conversation(
%{
assigns: %{user: user},
private: %{open_api_spex: %{params: %{id: id}}}
} = conn,
_
) do
with %Activity{} = activity <- Activity.get_by_id(id),
# CommonAPI already checks whether user is allowed to unmute
{:ok, activity} <- CommonAPI.remove_mute(activity, user) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
else
{:error, :visibility_error} ->
{:error, :not_found, "Record not found"}
error ->
error
end
end
@doc "GET /api/v1/statuses/:id/favourited_by"
def favourited_by(
%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
_
) do
with true <- Pleroma.Config.get([:instance, :show_reactions]),
%Activity{} = activity <- Activity.get_by_id_with_object(id),
{:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
%Object{data: %{"likes" => likes}} <- Object.normalize(activity, fetch: false) do
users =
User
|> Ecto.Query.where([u], u.ap_id in ^likes)
|> Ecto.Query.order_by([u], fragment("array_position(?, ?)", ^likes, u.ap_id))
|> Repo.all()
|> Enum.filter(&(not User.blocks?(user, &1)))
conn
|> put_view(AccountView)
|> render("index.json", for: user, users: users, as: :user)
else
{:visible, false} -> {:error, :not_found}
_ -> json(conn, [])
end
end
@doc "GET /api/v1/statuses/:id/reblogged_by"
def reblogged_by(
%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
_
) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
{:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
%Object{data: %{"announcements" => announces, "id" => ap_id}} <-
Object.normalize(activity, fetch: false) do
announces =
"Announce"
|> Activity.Queries.by_type()
|> Ecto.Query.where([a], a.actor in ^announces)
# this is to use the index
|> Activity.Queries.by_object_id(ap_id)
|> Repo.all()
|> Enum.filter(&Visibility.visible_for_user?(&1, user))
|> Enum.map(& &1.actor)
|> Enum.uniq()
users =
User
|> Ecto.Query.where([u], u.ap_id in ^announces)
|> Ecto.Query.order_by([u], fragment("array_position(?, ?)", ^announces, u.ap_id))
|> Repo.all()
|> Enum.filter(&(not User.blocks?(user, &1)))
conn
|> put_view(AccountView)
|> render("index.json", for: user, users: users, as: :user)
else
{:visible, false} -> {:error, :not_found}
_ -> json(conn, [])
end
end
@doc "GET /api/v1/statuses/:id/context"
def context(
%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
_
) do
with %Activity{} = activity <- Activity.get_by_id(id) do
activities =
ActivityPub.fetch_activities_for_context(activity.data["context"], %{
blocking_user: user,
user: user,
exclude_id: activity.id
})
render(conn, "context.json", activity: activity, activities: activities, user: user)
end
end
@doc "POST /api/v1/statuses/:id/translate"
def translate(
%{
assigns: %{user: user},
private: %{open_api_spex: %{body_params: params, params: %{id: status_id}}}
} = conn,
_
) do
with %Activity{object: object} <- Activity.get_by_id_with_object(status_id),
{:visibility, visibility} when visibility in ["public", "unlisted"] <-
{:visibility, Visibility.get_visibility(object)},
{:language, language} when is_binary(language) <-
{:language, Map.get(params, :lang) || user.language},
{:ok, result} <-
Translation.translate(
object.data["content"],
object.data["language"],
language
) do
render(conn, "translation.json", result)
else
{:language, nil} ->
render_error(conn, :bad_request, "Language not specified")
{:visibility, _} ->
render_error(conn, :not_found, "Record not found")
{:error, :not_found} ->
render_error(conn, :not_found, "Translation service not configured")
{:error, error} when error in [:unexpected_response, :quota_exceeded, :too_many_requests] ->
render_error(conn, :service_unavailable, "Translation service not available")
_ ->
render_error(conn, :internal_server_error, "Translation failed")
end
end
@doc "GET /api/v1/favourites"
def favourites(
%{assigns: %{user: %User{} = user}, private: %{open_api_spex: %{params: params}}} = conn,
_
) do
activities = ActivityPub.fetch_favourites(user, params)
conn
|> add_link_headers(activities)
|> render("index.json",
activities: activities,
for: user,
as: :activity
)
end
@doc "GET /api/v1/bookmarks"
def bookmarks(%{assigns: %{user: user}, private: %{open_api_spex: %{params: params}}} = conn, _) do
user = User.get_cached_by_id(user.id)
folder_id = Map.get(params, :folder_id)
bookmarks =
user.id
|> Bookmark.for_user_query(folder_id)
|> Pleroma.Pagination.fetch_paginated(params)
activities =
bookmarks
|> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
conn
|> add_link_headers(bookmarks)
|> render("index.json",
activities: activities,
for: user,
as: :activity
)
end
@doc "GET /api/v1/statuses/:id/quotes"
def quotes(
%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id} = params}}} =
conn,
_
) do
with %Activity{object: object} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.visible_for_user?(activity, user) do
params =
params
|> Map.put(:type, "Create")
|> Map.put(:blocking_user, user)
|> Map.put(:quote_url, object.data["id"])
recipients =
if user do
[Pleroma.Constants.as_public()] ++ [user.ap_id | User.following(user)]
else
[Pleroma.Constants.as_public()]
end
activities =
recipients
|> ActivityPub.fetch_activities(params)
|> Enum.reverse()
conn
|> add_link_headers(activities)
|> render("index.json",
activities: activities,
for: user,
as: :activity
)
else
nil -> {:error, :not_found}
false -> {:error, :not_found}
end
end
defp put_application(params, %{assigns: %{token: %Token{user: %User{} = user} = token}} = _conn) do
if user.disclose_client do
%{client_name: client_name, website: website} = Repo.preload(token, :app).app
Map.put(params, :generator, %{type: "Application", name: client_name, url: website})
else
Map.put(params, :generator, nil)
end
end
defp put_application(params, _), do: Map.put(params, :generator, nil)
end
diff --git a/lib/pleroma/web/twitter_api/controllers/password_controller.ex b/lib/pleroma/web/o_auth/password_controller.ex
similarity index 90%
copy from lib/pleroma/web/twitter_api/controllers/password_controller.ex
copy to lib/pleroma/web/o_auth/password_controller.ex
index e5482de9d..b209e7564 100644
--- a/lib/pleroma/web/twitter_api/controllers/password_controller.ex
+++ b/lib/pleroma/web/o_auth/password_controller.ex
@@ -1,52 +1,52 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Web.TwitterAPI.PasswordController do
+defmodule Pleroma.Web.OAuth.PasswordController do
@moduledoc """
The module contains functions for password reset.
"""
use Pleroma.Web, :controller
require Logger
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
alias Pleroma.PasswordResetToken
alias Pleroma.Repo
alias Pleroma.User
- alias Pleroma.Web.TwitterAPI.TwitterAPI
+ alias Pleroma.Web.Registration
plug(Pleroma.Web.Plugs.RateLimiter, [name: :request] when action == :request)
@doc "POST /auth/password"
def request(conn, params) do
nickname_or_email = params["email"] || params["nickname"]
- TwitterAPI.password_reset(nickname_or_email)
+ Registration.password_reset(nickname_or_email)
json_response(conn, :no_content, "")
end
def reset(conn, %{"token" => token}) do
with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}),
false <- PasswordResetToken.expired?(token),
%User{} = user <- User.get_cached_by_id(token.user_id) do
render(conn, "reset.html", %{
token: token,
user: user
})
else
_e -> render(conn, "invalid_token.html")
end
end
def do_reset(conn, %{"data" => data}) do
with {:ok, _} <- PasswordResetToken.reset_password(data["token"], data) do
render(conn, "reset_success.html")
else
_e -> render(conn, "reset_failed.html")
end
end
end
diff --git a/lib/pleroma/web/twitter_api/views/password_view.ex b/lib/pleroma/web/o_auth/password_view.ex
similarity index 83%
rename from lib/pleroma/web/twitter_api/views/password_view.ex
rename to lib/pleroma/web/o_auth/password_view.ex
index 55790941f..0b85c76e8 100644
--- a/lib/pleroma/web/twitter_api/views/password_view.ex
+++ b/lib/pleroma/web/o_auth/password_view.ex
@@ -1,9 +1,9 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Web.TwitterAPI.PasswordView do
+defmodule Pleroma.Web.OAuth.PasswordView do
use Pleroma.Web, :view
import Phoenix.HTML.Form
alias Pleroma.Web.Gettext
end
diff --git a/lib/pleroma/web/twitter_api/controller.ex b/lib/pleroma/web/o_auth/token_controller.ex
similarity index 94%
copy from lib/pleroma/web/twitter_api/controller.ex
copy to lib/pleroma/web/o_auth/token_controller.ex
index 6db3d6067..37d08eea3 100644
--- a/lib/pleroma/web/twitter_api/controller.ex
+++ b/lib/pleroma/web/o_auth/token_controller.ex
@@ -1,59 +1,59 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Web.TwitterAPI.Controller do
+defmodule Pleroma.Web.OAuth.TokenController do
use Pleroma.Web, :controller
alias Pleroma.User
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.Plugs.OAuthScopesPlug
- alias Pleroma.Web.TwitterAPI.TokenView
+ alias Pleroma.Web.OAuth.TokenView
require Logger
plug(:skip_auth when action == :confirm_email)
plug(:skip_plug, OAuthScopesPlug when action in [:oauth_tokens, :revoke_token])
action_fallback(:errors)
def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
with %User{} = user <- User.get_cached_by_id(uid),
true <- user.local and !user.is_confirmed and user.confirmation_token == token,
{:ok, _} <- User.confirm(user) do
redirect(conn, to: "/")
end
end
def oauth_tokens(%{assigns: %{user: user}} = conn, _params) do
with oauth_tokens <- Token.get_user_tokens(user) do
conn
|> put_view(TokenView)
|> render("index.json", %{tokens: oauth_tokens})
end
end
def revoke_token(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
Token.delete_user_token(user, id)
json_reply(conn, 201, "")
end
defp errors(conn, {:param_cast, _}) do
conn
|> put_status(400)
|> json("Invalid parameters")
end
defp errors(conn, _) do
conn
|> put_status(500)
|> json("Something went wrong")
end
defp json_reply(conn, status, json) do
conn
|> put_resp_content_type("application/json")
|> send_resp(status, json)
end
end
diff --git a/lib/pleroma/web/twitter_api/views/token_view.ex b/lib/pleroma/web/o_auth/token_view.ex
similarity index 81%
rename from lib/pleroma/web/twitter_api/views/token_view.ex
rename to lib/pleroma/web/o_auth/token_view.ex
index 36776ce3b..f9894399a 100644
--- a/lib/pleroma/web/twitter_api/views/token_view.ex
+++ b/lib/pleroma/web/o_auth/token_view.ex
@@ -1,22 +1,22 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Web.TwitterAPI.TokenView do
+defmodule Pleroma.Web.OAuth.TokenView do
use Pleroma.Web, :view
def render("index.json", %{tokens: tokens}) do
tokens
- |> render_many(Pleroma.Web.TwitterAPI.TokenView, "show.json")
+ |> render_many(Pleroma.Web.OAuth.TokenView, "show.json")
|> Enum.filter(&Enum.any?/1)
end
def render("show.json", %{token: token_entry}) do
%{
id: token_entry.id,
valid_until: token_entry.valid_until,
app_name: token_entry.app.client_name,
scopes: token_entry.scopes
}
end
end
diff --git a/lib/pleroma/web/pleroma_api/controllers/bookmark_folder_controller.ex b/lib/pleroma/web/pleroma_api/controllers/bookmark_folder_controller.ex
index 6d6e2e940..55f323a16 100644
--- a/lib/pleroma/web/pleroma_api/controllers/bookmark_folder_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/bookmark_folder_controller.ex
@@ -1,68 +1,66 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.BookmarkFolderController do
use Pleroma.Web, :controller
alias Pleroma.BookmarkFolder
alias Pleroma.Web.Plugs.OAuthScopesPlug
plug(Pleroma.Web.ApiSpec.CastAndValidate)
- # Note: scope not present in Mastodon: read:bookmarks
plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :index)
- # Note: scope not present in Mastodon: write:bookmarks
plug(
OAuthScopesPlug,
%{scopes: ["write:bookmarks"]} when action in [:create, :update, :delete]
)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaBookmarkFolderOperation
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
def index(%{assigns: %{user: user}} = conn, _params) do
with folders <- BookmarkFolder.for_user(user.id) do
conn
|> render("index.json", %{folders: folders, as: :folder})
end
end
def create(
%{assigns: %{user: user}, private: %{open_api_spex: %{body_params: params}}} = conn,
_
) do
with {:ok, folder} <- BookmarkFolder.create(user.id, params[:name], params[:emoji]) do
render(conn, "show.json", folder: folder)
end
end
def update(
%{
assigns: %{user: user},
private: %{open_api_spex: %{body_params: params, params: %{id: id}}}
} = conn,
_
) do
with true <- BookmarkFolder.belongs_to_user?(id, user.id),
{:ok, folder} <- BookmarkFolder.update(id, params[:name], params[:emoji]) do
render(conn, "show.json", folder: folder)
else
false -> {:error, :forbidden}
end
end
def delete(
%{assigns: %{user: user}, private: %{open_api_spex: %{params: %{id: id}}}} = conn,
_
) do
with true <- BookmarkFolder.belongs_to_user?(id, user.id),
{:ok, folder} <- BookmarkFolder.delete(id) do
render(conn, "show.json", folder: folder)
else
false -> {:error, :forbidden}
end
end
end
diff --git a/lib/pleroma/web/twitter_api/controllers/password_controller.ex b/lib/pleroma/web/pleroma_api/controllers/password_controller.ex
similarity index 90%
rename from lib/pleroma/web/twitter_api/controllers/password_controller.ex
rename to lib/pleroma/web/pleroma_api/controllers/password_controller.ex
index e5482de9d..444477245 100644
--- a/lib/pleroma/web/twitter_api/controllers/password_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/password_controller.ex
@@ -1,52 +1,52 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Web.TwitterAPI.PasswordController do
+defmodule Pleroma.Web.PleromaAPI.PasswordController do
@moduledoc """
The module contains functions for password reset.
"""
use Pleroma.Web, :controller
require Logger
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
alias Pleroma.PasswordResetToken
alias Pleroma.Repo
alias Pleroma.User
- alias Pleroma.Web.TwitterAPI.TwitterAPI
+ alias Pleroma.Web.Registration
plug(Pleroma.Web.Plugs.RateLimiter, [name: :request] when action == :request)
@doc "POST /auth/password"
def request(conn, params) do
nickname_or_email = params["email"] || params["nickname"]
- TwitterAPI.password_reset(nickname_or_email)
+ Registration.password_reset(nickname_or_email)
json_response(conn, :no_content, "")
end
def reset(conn, %{"token" => token}) do
with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}),
false <- PasswordResetToken.expired?(token),
%User{} = user <- User.get_cached_by_id(token.user_id) do
render(conn, "reset.html", %{
token: token,
user: user
})
else
_e -> render(conn, "invalid_token.html")
end
end
def do_reset(conn, %{"data" => data}) do
with {:ok, _} <- PasswordResetToken.reset_password(data["token"], data) do
render(conn, "reset_success.html")
else
_e -> render(conn, "reset_failed.html")
end
end
end
diff --git a/lib/pleroma/web/twitter_api/controller.ex b/lib/pleroma/web/pleroma_api/controllers/token_controller.ex
similarity index 94%
rename from lib/pleroma/web/twitter_api/controller.ex
rename to lib/pleroma/web/pleroma_api/controllers/token_controller.ex
index 6db3d6067..581dd569a 100644
--- a/lib/pleroma/web/twitter_api/controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/token_controller.ex
@@ -1,59 +1,59 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Web.TwitterAPI.Controller do
+defmodule Pleroma.Web.PleromaAPI.TokenController do
use Pleroma.Web, :controller
alias Pleroma.User
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.Plugs.OAuthScopesPlug
- alias Pleroma.Web.TwitterAPI.TokenView
+ alias Pleroma.Web.PleromaAPI.TokenView
require Logger
plug(:skip_auth when action == :confirm_email)
plug(:skip_plug, OAuthScopesPlug when action in [:oauth_tokens, :revoke_token])
action_fallback(:errors)
def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
with %User{} = user <- User.get_cached_by_id(uid),
true <- user.local and !user.is_confirmed and user.confirmation_token == token,
{:ok, _} <- User.confirm(user) do
redirect(conn, to: "/")
end
end
def oauth_tokens(%{assigns: %{user: user}} = conn, _params) do
with oauth_tokens <- Token.get_user_tokens(user) do
conn
|> put_view(TokenView)
|> render("index.json", %{tokens: oauth_tokens})
end
end
def revoke_token(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
Token.delete_user_token(user, id)
json_reply(conn, 201, "")
end
defp errors(conn, {:param_cast, _}) do
conn
|> put_status(400)
|> json("Invalid parameters")
end
defp errors(conn, _) do
conn
|> put_status(500)
|> json("Something went wrong")
end
defp json_reply(conn, status, json) do
conn
|> put_resp_content_type("application/json")
|> send_resp(status, json)
end
end
diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/pleroma_api/controllers/util_controller.ex
similarity index 65%
rename from lib/pleroma/web/twitter_api/controllers/util_controller.ex
rename to lib/pleroma/web/pleroma_api/controllers/util_controller.ex
index 1c072f98a..882e9476e 100644
--- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/util_controller.ex
@@ -1,407 +1,277 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Web.TwitterAPI.UtilController do
+defmodule Pleroma.Web.PleromaAPI.UtilController do
use Pleroma.Web, :controller
require Logger
- alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Emoji
alias Pleroma.Healthcheck
alias Pleroma.User
alias Pleroma.Utils.URIEncoding
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.Auth.WrapperAuthenticator, as: Authenticator
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Plugs.OAuthScopesPlug
- alias Pleroma.Web.WebFinger
- plug(
- Pleroma.Web.ApiSpec.CastAndValidate,
- [replace_params: false]
- when action != :remote_subscribe and action != :show_subscribe_form
- )
-
- plug(
- Pleroma.Web.Plugs.FederatingPlug
- when action == :remote_subscribe
- when action == :show_subscribe_form
- )
+ plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(
OAuthScopesPlug,
%{scopes: ["write:accounts"]}
when action in [
:change_email,
:change_password,
:delete_account,
:update_notification_settings,
:disable_account,
:move_account,
:add_alias,
:delete_alias
]
)
plug(
OAuthScopesPlug,
%{scopes: ["read:accounts"]}
when action in [
:list_aliases
]
)
- defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.TwitterUtilOperation
-
- def show_subscribe_form(conn, %{"nickname" => nick}) do
- with %User{} = user <- User.get_cached_by_nickname(nick),
- avatar = User.avatar_url(user) do
- conn
- |> render("subscribe.html", %{nickname: nick, avatar: avatar, error: false})
- else
- _e ->
- render(conn, "subscribe.html", %{
- nickname: nick,
- avatar: nil,
- error:
- Pleroma.Web.Gettext.dpgettext(
- "static_pages",
- "remote follow error message - user not found",
- "Could not find user"
- )
- })
- end
- end
-
- def show_subscribe_form(conn, %{"status_id" => id}) do
- with %Activity{} = activity <- Activity.get_by_id(id),
- {:ok, ap_id} <- get_ap_id(activity),
- %User{} = user <- User.get_cached_by_ap_id(activity.actor),
- avatar = User.avatar_url(user) do
- conn
- |> render("status_interact.html", %{
- status_link: ap_id,
- status_id: id,
- nickname: user.nickname,
- avatar: avatar,
- error: false
- })
- else
- _e ->
- render(conn, "status_interact.html", %{
- status_id: id,
- avatar: nil,
- error:
- Pleroma.Web.Gettext.dpgettext(
- "static_pages",
- "status interact error message - status not found",
- "Could not find status"
- )
- })
- end
- end
-
- def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) do
- show_subscribe_form(conn, %{"nickname" => nick})
- end
-
- def remote_subscribe(conn, %{"status_id" => id, "profile" => _}) do
- show_subscribe_form(conn, %{"status_id" => id})
- end
-
- def remote_subscribe(conn, %{"user" => %{"nickname" => nick, "profile" => profile}}) do
- with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile),
- %User{ap_id: ap_id} <- User.get_cached_by_nickname(nick) do
- conn
- |> Phoenix.Controller.redirect(external: String.replace(template, "{uri}", ap_id))
- else
- _e ->
- render(conn, "subscribe.html", %{
- nickname: nick,
- avatar: nil,
- error:
- Pleroma.Web.Gettext.dpgettext(
- "static_pages",
- "remote follow error message - unknown error",
- "Something went wrong."
- )
- })
- end
- end
-
- def remote_subscribe(conn, %{"status" => %{"status_id" => id, "profile" => profile}}) do
- with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile),
- %Activity{} = activity <- Activity.get_by_id(id),
- {:ok, ap_id} <- get_ap_id(activity) do
- conn
- |> Phoenix.Controller.redirect(external: String.replace(template, "{uri}", ap_id))
- else
- _e ->
- render(conn, "status_interact.html", %{
- status_id: id,
- avatar: nil,
- error:
- Pleroma.Web.Gettext.dpgettext(
- "static_pages",
- "status interact error message - unknown error",
- "Something went wrong."
- )
- })
- end
- end
-
- def remote_interaction(
- %{private: %{open_api_spex: %{body_params: %{ap_id: ap_id, profile: profile}}}} = conn,
- _params
- ) do
- with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile) do
- conn
- |> json(%{url: String.replace(template, "{uri}", ap_id)})
- else
- _e -> json(conn, %{error: "Couldn't find user"})
- end
- end
-
- defp get_ap_id(activity) do
- object = Pleroma.Object.normalize(activity, fetch: false)
-
- case object do
- %{data: %{"id" => ap_id}} -> {:ok, ap_id}
- _ -> {:no_ap_id, nil}
- end
- end
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaUtilOperation
def frontend_configurations(conn, _params) do
render(conn, "frontend_configurations.json")
end
def emoji(conn, _params) do
emoji =
Enum.reduce(Emoji.get_all(), %{}, fn {code, %Emoji{file: file, tags: tags}}, acc ->
file = encode_emoji_url(file)
Map.put(acc, code, %{image_url: file, tags: tags})
end)
json(conn, emoji)
end
defp encode_emoji_url(nil), do: nil
defp encode_emoji_url("http" <> _ = url), do: URIEncoding.encode_url(url)
defp encode_emoji_url("/" <> _ = path),
do: URIEncoding.encode_url(path, bypass_parse: true, bypass_decode: true)
defp encode_emoji_url(path) when is_binary(path),
do: URIEncoding.encode_url(path, bypass_parse: true, bypass_decode: true)
def update_notification_settings(%{assigns: %{user: user}} = conn, params) do
with {:ok, _} <- User.update_notification_settings(user, params) do
json(conn, %{status: "success"})
end
end
def change_password(
%{assigns: %{user: user}, private: %{open_api_spex: %{body_params: body_params}}} = conn,
_
) do
with {:ok, %User{}} <-
Authenticator.change_password(
user,
body_params.password,
body_params.new_password,
body_params.new_password_confirmation
) do
json(conn, %{status: "success"})
else
{:error, %Ecto.Changeset{} = changeset} ->
{_, {error, _}} = Enum.at(changeset.errors, 0)
json(conn, %{error: "New password #{error}."})
{:error, :password_confirmation} ->
json(conn, %{error: "New password does not match confirmation."})
{:error, msg} ->
json(conn, %{error: msg})
end
end
def change_email(
%{assigns: %{user: user}, private: %{open_api_spex: %{body_params: body_params}}} = conn,
_
) do
case CommonAPI.Utils.confirm_current_password(user, body_params.password) do
{:ok, user} ->
with {:ok, _user} <- User.change_email(user, body_params.email) do
json(conn, %{status: "success"})
else
{:error, changeset} ->
{_, {error, _}} = Enum.at(changeset.errors, 0)
json(conn, %{error: "Email #{error}."})
_ ->
json(conn, %{error: "Unable to change email."})
end
{:error, msg} ->
json(conn, %{error: msg})
end
end
def delete_account(
%{
assigns: %{user: user},
private: %{open_api_spex: %{body_params: body_params, params: params}}
} = conn,
_
) do
# This endpoint can accept a query param or JSON body for backwards-compatibility.
# Submitting a JSON body is recommended, so passwords don't end up in server logs.
password = body_params[:password] || params[:password] || ""
case CommonAPI.Utils.confirm_current_password(user, password) do
{:ok, user} ->
User.delete(user)
json(conn, %{status: "success"})
{:error, msg} ->
json(conn, %{error: msg})
end
end
def disable_account(
%{assigns: %{user: user}, private: %{open_api_spex: %{params: params}}} = conn,
_
) do
case CommonAPI.Utils.confirm_current_password(user, params[:password]) do
{:ok, user} ->
User.set_activation_async(user, false)
json(conn, %{status: "success"})
{:error, msg} ->
json(conn, %{error: msg})
end
end
def move_account(
%{assigns: %{user: user}, private: %{open_api_spex: %{body_params: body_params}}} = conn,
_
) do
case CommonAPI.Utils.confirm_current_password(user, body_params.password) do
{:ok, user} ->
with {:ok, target_user} <- find_or_fetch_user_by_nickname(body_params.target_account),
{:ok, _user} <- ActivityPub.move(user, target_user) do
json(conn, %{status: "success"})
else
{:not_found, _} ->
conn
|> put_status(404)
|> json(%{error: "Target account not found."})
{:error, error} ->
json(conn, %{error: error})
end
{:error, msg} ->
json(conn, %{error: msg})
end
end
def add_alias(
%{assigns: %{user: user}, private: %{open_api_spex: %{body_params: body_params}}} = conn,
_
) do
with {:ok, alias_user} <- find_user_by_nickname(body_params.alias),
{:ok, _user} <- user |> User.add_alias(alias_user) do
json(conn, %{status: "success"})
else
{:not_found, _} ->
conn
|> put_status(404)
|> json(%{error: "Target account does not exist."})
{:error, error} ->
json(conn, %{error: error})
end
end
def delete_alias(
%{assigns: %{user: user}, private: %{open_api_spex: %{body_params: body_params}}} = conn,
_
) do
with {:ok, alias_user} <- find_user_by_nickname(body_params.alias),
{:ok, _user} <- user |> User.delete_alias(alias_user) do
json(conn, %{status: "success"})
else
{:error, :no_such_alias} ->
conn
|> put_status(404)
|> json(%{error: "Account has no such alias."})
{:error, error} ->
json(conn, %{error: error})
end
end
def list_aliases(%{assigns: %{user: user}} = conn, _) do
alias_nicks =
user
|> User.alias_users()
|> Enum.map(&User.full_nickname/1)
json(conn, %{aliases: alias_nicks})
end
defp find_user_by_nickname(nickname) do
user = User.get_cached_by_nickname(nickname)
if user == nil do
{:error, :not_found}
else
{:ok, user}
end
end
defp find_or_fetch_user_by_nickname(nickname) do
user = User.get_by_nickname(nickname)
if user != nil and user.local do
{:ok, user}
else
with {:ok, user} <- User.fetch_by_nickname(nickname) do
{:ok, user}
else
_ ->
{:not_found, nil}
end
end
end
def captcha(conn, _params) do
json(conn, Pleroma.Captcha.new())
end
def healthcheck(conn, _params) do
with {:cfg, true} <- {:cfg, Config.get([:instance, :healthcheck])},
%{healthy: true} = info <- Healthcheck.system_info() do
json(conn, info)
else
%{healthy: false} = info ->
service_unavailable(conn, info)
{:cfg, false} ->
service_unavailable(conn, %{"error" => "Healthcheck disabled"})
_ ->
service_unavailable(conn, %{})
end
end
defp service_unavailable(conn, info) do
conn
|> put_status(:service_unavailable)
|> json(info)
end
end
diff --git a/lib/pleroma/web/pleroma_api/views/bookmark_folder_view.ex b/lib/pleroma/web/pleroma_api/views/bookmark_folder_view.ex
index 29028781f..ddffa25d3 100644
--- a/lib/pleroma/web/pleroma_api/views/bookmark_folder_view.ex
+++ b/lib/pleroma/web/pleroma_api/views/bookmark_folder_view.ex
@@ -1,41 +1,38 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.BookmarkFolderView do
use Pleroma.Web, :view
alias Pleroma.BookmarkFolder
alias Pleroma.Emoji
def render("show.json", %{folder: %BookmarkFolder{} = folder}) do
+ {emoji, emoji_url} = get_emoji(folder.emoji)
+
%{
id: folder.id |> to_string(),
name: folder.name,
- emoji: folder.emoji,
- emoji_url: get_emoji_url(folder.emoji)
+ emoji: emoji,
+ emoji_url: emoji_url
}
end
def render("index.json", %{folders: folders} = opts) do
render_many(folders, __MODULE__, "show.json", Map.delete(opts, :folders))
end
- defp get_emoji_url(nil) do
- nil
- end
+ defp get_emoji(nil), do: {nil, nil}
- defp get_emoji_url(emoji) do
+ defp get_emoji(emoji) do
if Emoji.unicode?(emoji) do
- nil
+ {emoji, nil}
else
- emoji = Emoji.get(emoji)
-
- if emoji != nil do
- Emoji.local_url(emoji.file)
- else
- nil
+ case Emoji.get(emoji) do
+ nil -> {nil, nil}
+ emoji_data -> {emoji, Emoji.local_url(emoji_data.file)}
end
end
end
end
diff --git a/lib/pleroma/web/pleroma_api/views/util_view.ex b/lib/pleroma/web/pleroma_api/views/util_view.ex
new file mode 100644
index 000000000..b5e07c006
--- /dev/null
+++ b/lib/pleroma/web/pleroma_api/views/util_view.ex
@@ -0,0 +1,13 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.PleromaAPI.UtilView do
+ use Pleroma.Web, :view
+ alias Pleroma.Config
+
+ def render("frontend_configurations.json", _) do
+ Config.get(:frontend_configurations, %{})
+ |> Enum.into(%{})
+ end
+end
diff --git a/lib/pleroma/web/preload/providers/instance.ex b/lib/pleroma/web/preload/providers/instance.ex
index 6183f7b70..d5417af30 100644
--- a/lib/pleroma/web/preload/providers/instance.ex
+++ b/lib/pleroma/web/preload/providers/instance.ex
@@ -1,59 +1,59 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Preload.Providers.Instance do
alias Pleroma.Web.MastodonAPI.InstanceView
alias Pleroma.Web.Nodeinfo.Nodeinfo
alias Pleroma.Web.Plugs.InstanceStatic
alias Pleroma.Web.Preload.Providers.Provider
- alias Pleroma.Web.TwitterAPI.UtilView
+ alias Pleroma.Web.PleromaAPI.UtilView
@behaviour Provider
@instance_url "/api/v1/instance"
@panel_url "/instance/panel.html"
@nodeinfo_url "/nodeinfo/2.0.json"
@fe_config_url "/api/pleroma/frontend_configurations"
@impl Provider
def generate_terms(_params) do
%{}
|> build_info_tag()
|> build_panel_tag()
|> build_nodeinfo_tag()
|> build_fe_config_tag()
end
defp build_info_tag(acc) do
info_data = InstanceView.render("show.json", %{})
Map.put(acc, @instance_url, info_data)
end
defp build_panel_tag(acc) do
instance_path = InstanceStatic.file_path(@panel_url |> to_string())
if File.exists?(instance_path) do
panel_data = File.read!(instance_path)
Map.put(acc, @panel_url, panel_data)
else
acc
end
end
defp build_nodeinfo_tag(acc) do
case Nodeinfo.get_nodeinfo("2.0") do
{:error, _} ->
acc
nodeinfo_data ->
Map.put(acc, @nodeinfo_url, nodeinfo_data)
end
end
defp build_fe_config_tag(acc) do
fe_data = UtilView.render("frontend_configurations.json", %{})
Map.put(acc, @fe_config_url, fe_data)
end
end
diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/registration.ex
similarity index 98%
rename from lib/pleroma/web/twitter_api/twitter_api.ex
rename to lib/pleroma/web/registration.ex
index ef2eb75f4..df71300ce 100644
--- a/lib/pleroma/web/twitter_api/twitter_api.ex
+++ b/lib/pleroma/web/registration.ex
@@ -1,136 +1,136 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
+defmodule Pleroma.Web.Registration do
import Pleroma.Web.Gettext
alias Pleroma.Emails.Mailer
alias Pleroma.Emails.UserEmail
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.UserInviteToken
def register_user(params, opts \\ []) do
fallback_language = Gettext.get_locale()
params =
params
|> Map.take([:email, :token, :password])
|> Map.put(:bio, params |> Map.get(:bio, "") |> User.parse_bio())
|> Map.put(:nickname, params[:username])
|> Map.put(:name, Map.get(params, :fullname, params[:username]))
|> Map.put(:password_confirmation, params[:password])
|> Map.put(:registration_reason, params[:reason])
|> Map.put(:birthday, params[:birthday])
|> Map.put(
:language,
Pleroma.Web.Gettext.normalize_locale(params[:language]) || fallback_language
)
if Pleroma.Config.get([:instance, :registrations_open]) do
create_user(params, opts)
else
create_user_with_invite(params, opts)
end
end
defp create_user_with_invite(params, opts) do
with %{token: token} when is_binary(token) <- params,
%UserInviteToken{} = invite <- Repo.get_by(UserInviteToken, %{token: token}),
true <- UserInviteToken.valid_invite?(invite) do
UserInviteToken.update_usage!(invite)
create_user(params, opts)
else
nil -> {:error, "Invalid token"}
_ -> {:error, "Expired token"}
end
end
defp create_user(params, opts) do
changeset = User.register_changeset(%User{}, params, opts)
case User.register(changeset) do
{:ok, user} ->
{:ok, user}
{:error, changeset} ->
errors =
changeset
|> Ecto.Changeset.traverse_errors(fn {msg, _opts} -> msg end)
|> Jason.encode!()
{:error, errors}
end
end
def password_reset(nickname_or_email) do
with true <- is_binary(nickname_or_email),
%User{local: true, email: email, is_active: true} = user when is_binary(email) <-
User.get_by_nickname_or_email(nickname_or_email),
{:ok, token_record} <- Pleroma.PasswordResetToken.create_token(user) do
user
|> UserEmail.password_reset_email(token_record.token)
|> Mailer.deliver_async()
{:ok, :enqueued}
else
_ ->
{:ok, :noop}
end
end
def validate_captcha(app, params) do
if app.trusted || not Pleroma.Captcha.enabled?() do
:ok
else
do_validate_captcha(params)
end
end
defp do_validate_captcha(params) do
with :ok <- validate_captcha_presence(params),
:ok <-
Pleroma.Captcha.validate(
params[:captcha_token],
params[:captcha_solution],
params[:captcha_answer_data]
) do
:ok
else
{:error, :captcha_error} ->
captcha_error(dgettext("errors", "CAPTCHA Error"))
{:error, :invalid} ->
captcha_error(dgettext("errors", "Invalid CAPTCHA"))
{:error, :kocaptcha_service_unavailable} ->
captcha_error(dgettext("errors", "Kocaptcha service unavailable"))
{:error, :expired} ->
captcha_error(dgettext("errors", "CAPTCHA expired"))
{:error, :already_used} ->
captcha_error(dgettext("errors", "CAPTCHA already used"))
{:error, :invalid_answer_data} ->
captcha_error(dgettext("errors", "Invalid answer data"))
{:error, error} ->
captcha_error(error)
end
end
defp validate_captcha_presence(params) do
[:captcha_solution, :captcha_token, :captcha_answer_data]
|> Enum.find_value(:ok, fn key ->
unless is_binary(params[key]) do
error = dgettext("errors", "Invalid CAPTCHA (Missing parameter: %{name})", name: key)
{:error, error}
end
end)
end
# For some reason FE expects error message to be a serialized JSON
defp captcha_error(error), do: {:error, Jason.encode!(%{captcha: [error]})}
end
diff --git a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex b/lib/pleroma/web/remote_interaction/remote_interaction_controller.ex
similarity index 57%
rename from lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex
rename to lib/pleroma/web/remote_interaction/remote_interaction_controller.ex
index 38ebc8c5d..90f15cdb6 100644
--- a/lib/pleroma/web/twitter_api/controllers/remote_follow_controller.ex
+++ b/lib/pleroma/web/remote_interaction/remote_interaction_controller.ex
@@ -1,165 +1,292 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
+defmodule Pleroma.Web.RemoteInteraction.RemoteInteractionController do
use Pleroma.Web, :controller
require Logger
alias Pleroma.Activity
alias Pleroma.MFA
alias Pleroma.Object.Fetcher
alias Pleroma.User
alias Pleroma.Web.Auth.TOTPAuthenticator
alias Pleroma.Web.Auth.WrapperAuthenticator
alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.WebFinger
@status_types ["Article", "Event", "Note", "Video", "Page", "Question"]
+ plug(
+ Pleroma.Web.ApiSpec.CastAndValidate,
+ [replace_params: false]
+ when action == :remote_interaction
+ )
+
plug(Pleroma.Web.Plugs.FederatingPlug)
# Note: follower can submit the form (with password auth) not being signed in (having no token)
plug(
Pleroma.Web.Plugs.OAuthScopesPlug,
%{fallback: :proceed_unauthenticated, scopes: ["follow", "write:follows"]}
- when action in [:do_follow]
+ when action == :do_follow
)
+ defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.RemoteInteractionOperation
+
# GET /ostatus_subscribe
#
def follow(%{assigns: %{user: user}} = conn, %{"acct" => acct}) do
case status?(acct) do
true -> follow_status(conn, user, acct)
_ -> follow_account(conn, user, acct)
end
end
defp follow_status(conn, _user, acct) do
with {:ok, object} <- Fetcher.fetch_object_from_id(acct),
%Activity{id: activity_id} <- Activity.get_create_by_object_ap_id(object.data["id"]) do
redirect(conn, to: Routes.o_status_path(conn, :notice, activity_id))
else
error ->
handle_follow_error(conn, error)
end
end
defp follow_account(conn, user, acct) do
with {:ok, followee} <- User.get_or_fetch(acct) do
render(conn, follow_template(user), %{error: false, followee: followee, acct: acct})
else
{:error, _reason} ->
render(conn, follow_template(user), %{error: :error})
end
end
defp follow_template(%User{} = _user), do: "follow.html"
defp follow_template(_), do: "follow_login.html"
defp status?(acct) do
case Fetcher.fetch_and_contain_remote_object_from_id(acct) do
{:ok, %{"type" => type}} when type in @status_types ->
true
_ ->
false
end
end
# POST /ostatus_subscribe
#
# adds a remote account in followers if user already is signed in.
#
def do_follow(%{assigns: %{user: %User{} = user}} = conn, %{"user" => %{"id" => id}}) do
with {:fetch_user, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)},
{:ok, _, _, _} <- CommonAPI.follow(followee, user) do
redirect(conn, to: "/users/#{followee.id}")
else
error ->
handle_follow_error(conn, error)
end
end
# POST /ostatus_subscribe
#
# step 1.
# checks login\password and displays step 2 form of MFA if need.
#
def do_follow(conn, %{"authorization" => %{"name" => _, "password" => _, "id" => id}}) do
with {_, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)},
{_, {:ok, user}, _} <- {:auth, WrapperAuthenticator.get_user(conn), followee},
{_, _, _, false} <- {:mfa_required, followee, user, MFA.require?(user)},
{:ok, _, _, _} <- CommonAPI.follow(followee, user) do
redirect(conn, to: "/users/#{followee.id}")
else
error ->
handle_follow_error(conn, error)
end
end
# POST /ostatus_subscribe
#
# step 2
# checks TOTP code. otherwise displays form with errors
#
def do_follow(conn, %{"mfa" => %{"code" => code, "token" => token, "id" => id}}) do
with {_, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)},
{_, _, {:ok, %{user: user}}} <- {:mfa_token, followee, MFA.Token.validate(token)},
{_, _, _, {:ok, _}} <-
{:verify_mfa_code, followee, token, TOTPAuthenticator.verify(code, user)},
{:ok, _, _, _} <- CommonAPI.follow(followee, user) do
redirect(conn, to: "/users/#{followee.id}")
else
error ->
handle_follow_error(conn, error)
end
end
def do_follow(%{assigns: %{user: nil}} = conn, _) do
Logger.debug("Insufficient permissions: follow | write:follows.")
render(conn, "followed.html", %{error: "Insufficient permissions: follow | write:follows."})
end
# GET /authorize_interaction
#
def authorize_interaction(conn, %{"uri" => uri}) do
conn
- |> redirect(to: Routes.remote_follow_path(conn, :follow, %{acct: uri}))
+ |> redirect(to: Routes.remote_interaction_path(conn, :follow, %{acct: uri}))
end
defp handle_follow_error(conn, {:mfa_token, followee, _} = _) do
render(conn, "follow_login.html", %{error: "Wrong username or password", followee: followee})
end
defp handle_follow_error(conn, {:verify_mfa_code, followee, token, _} = _) do
render(conn, "follow_mfa.html", %{
error: "Wrong authentication code",
followee: followee,
mfa_token: token
})
end
defp handle_follow_error(conn, {:mfa_required, followee, user, _} = _) do
{:ok, %{token: token}} = MFA.Token.create(user)
render(conn, "follow_mfa.html", %{followee: followee, mfa_token: token, error: false})
end
defp handle_follow_error(conn, {:auth, _, followee} = _) do
render(conn, "follow_login.html", %{error: "Wrong username or password", followee: followee})
end
defp handle_follow_error(conn, {:fetch_user, error} = _) do
Logger.debug("Remote follow failed with error #{inspect(error)}")
render(conn, "followed.html", %{error: "Could not find user"})
end
defp handle_follow_error(conn, {:error, "Could not follow user:" <> _} = _) do
render(conn, "followed.html", %{error: "Error following account"})
end
defp handle_follow_error(conn, error) do
Logger.debug("Remote follow failed with error #{inspect(error)}")
render(conn, "followed.html", %{error: "Something went wrong."})
end
+
+ def show_subscribe_form(conn, %{"nickname" => nick}) do
+ with %User{} = user <- User.get_cached_by_nickname(nick),
+ avatar = User.avatar_url(user) do
+ conn
+ |> render("subscribe.html", %{nickname: nick, avatar: avatar, error: false})
+ else
+ _e ->
+ render(conn, "subscribe.html", %{
+ nickname: nick,
+ avatar: nil,
+ error:
+ Pleroma.Web.Gettext.dpgettext(
+ "static_pages",
+ "remote follow error message - user not found",
+ "Could not find user"
+ )
+ })
+ end
+ end
+
+ def show_subscribe_form(conn, %{"status_id" => id}) do
+ with %Activity{} = activity <- Activity.get_by_id(id),
+ {:ok, ap_id} <- get_ap_id(activity),
+ %User{} = user <- User.get_cached_by_ap_id(activity.actor),
+ avatar = User.avatar_url(user) do
+ conn
+ |> render("status_interact.html", %{
+ status_link: ap_id,
+ status_id: id,
+ nickname: user.nickname,
+ avatar: avatar,
+ error: false
+ })
+ else
+ _e ->
+ render(conn, "status_interact.html", %{
+ status_id: id,
+ avatar: nil,
+ error:
+ Pleroma.Web.Gettext.dpgettext(
+ "static_pages",
+ "status interact error message - status not found",
+ "Could not find status"
+ )
+ })
+ end
+ end
+
+ def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) do
+ show_subscribe_form(conn, %{"nickname" => nick})
+ end
+
+ def remote_subscribe(conn, %{"status_id" => id, "profile" => _}) do
+ show_subscribe_form(conn, %{"status_id" => id})
+ end
+
+ def remote_subscribe(conn, %{"user" => %{"nickname" => nick, "profile" => profile}}) do
+ with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile),
+ %User{ap_id: ap_id} <- User.get_cached_by_nickname(nick) do
+ conn
+ |> Phoenix.Controller.redirect(external: String.replace(template, "{uri}", ap_id))
+ else
+ _e ->
+ render(conn, "subscribe.html", %{
+ nickname: nick,
+ avatar: nil,
+ error:
+ Pleroma.Web.Gettext.dpgettext(
+ "static_pages",
+ "remote follow error message - unknown error",
+ "Something went wrong."
+ )
+ })
+ end
+ end
+
+ def remote_subscribe(conn, %{"status" => %{"status_id" => id, "profile" => profile}}) do
+ with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile),
+ %Activity{} = activity <- Activity.get_by_id(id),
+ {:ok, ap_id} <- get_ap_id(activity) do
+ conn
+ |> Phoenix.Controller.redirect(external: String.replace(template, "{uri}", ap_id))
+ else
+ _e ->
+ render(conn, "status_interact.html", %{
+ status_id: id,
+ avatar: nil,
+ error:
+ Pleroma.Web.Gettext.dpgettext(
+ "static_pages",
+ "status interact error message - unknown error",
+ "Something went wrong."
+ )
+ })
+ end
+ end
+
+ def remote_interaction(
+ %{private: %{open_api_spex: %{body_params: %{ap_id: ap_id, profile: profile}}}} = conn,
+ _params
+ ) do
+ with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile) do
+ conn
+ |> json(%{url: String.replace(template, "{uri}", ap_id)})
+ else
+ _e -> json(conn, %{error: "Couldn't find user"})
+ end
+ end
+
+ defp get_ap_id(activity) do
+ object = Pleroma.Object.normalize(activity, fetch: false)
+
+ case object do
+ %{data: %{"id" => ap_id}} -> {:ok, ap_id}
+ _ -> {:no_ap_id, nil}
+ end
+ end
end
diff --git a/lib/pleroma/web/twitter_api/views/remote_follow_view.ex b/lib/pleroma/web/remote_interaction/remote_interaction_view.ex
similarity index 75%
rename from lib/pleroma/web/twitter_api/views/remote_follow_view.ex
rename to lib/pleroma/web/remote_interaction/remote_interaction_view.ex
index 8902261b0..6e7f40749 100644
--- a/lib/pleroma/web/twitter_api/views/remote_follow_view.ex
+++ b/lib/pleroma/web/remote_interaction/remote_interaction_view.ex
@@ -1,15 +1,17 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Web.TwitterAPI.RemoteFollowView do
+defmodule Pleroma.Web.RemoteInteraction.RemoteInteractionView do
use Pleroma.Web, :view
+ import Phoenix.HTML
import Phoenix.HTML.Form
+ import Phoenix.HTML.Link
alias Pleroma.Web.Gettext
def avatar_url(user) do
user
|> Pleroma.User.avatar_url()
|> Pleroma.Web.MediaProxy.url()
end
end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 20ac1c67b..eea0a0912 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -1,1112 +1,1117 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Router do
use Pleroma.Web, :router
import Phoenix.LiveDashboard.Router
import Oban.Web.Router
pipeline :accepts_html do
plug(:accepts, ["html"])
end
pipeline :accepts_html_xml do
plug(:accepts, ["html", "xml", "rss", "atom"])
end
pipeline :accepts_html_json do
plug(:accepts, ["html", "activity+json", "json"])
end
pipeline :accepts_html_xml_json do
plug(:accepts, ["html", "xml", "rss", "atom", "activity+json", "json"])
end
pipeline :accepts_xml_rss_atom do
plug(:accepts, ["xml", "rss", "atom"])
end
pipeline :browser do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(Pleroma.Web.Plugs.LoggerMetadataUser)
end
pipeline :oauth do
plug(:fetch_session)
plug(Pleroma.Web.Plugs.OAuthPlug)
plug(Pleroma.Web.Plugs.UserEnabledPlug)
plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug)
end
# Note: expects _user_ authentication (user-unbound app-bound tokens don't qualify)
pipeline :expect_user_authentication do
plug(Pleroma.Web.Plugs.ExpectAuthenticatedCheckPlug)
end
# Note: expects public instance or _user_ authentication (user-unbound tokens don't qualify)
pipeline :expect_public_instance_or_user_authentication do
plug(Pleroma.Web.Plugs.ExpectPublicOrAuthenticatedCheckPlug)
end
pipeline :authenticate do
plug(Pleroma.Web.Plugs.OAuthPlug)
plug(Pleroma.Web.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Web.Plugs.UserFetcherPlug)
plug(Pleroma.Web.Plugs.AuthenticationPlug)
end
pipeline :after_auth do
plug(Pleroma.Web.Plugs.UserEnabledPlug)
plug(Pleroma.Web.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug)
plug(Pleroma.Web.Plugs.UserTrackingPlug)
end
pipeline :base_api do
plug(:accepts, ["json"])
plug(:fetch_session)
plug(:authenticate)
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
plug(Pleroma.Web.Plugs.LoggerMetadataUser)
end
pipeline :no_auth_or_privacy_expectations_api do
plug(:base_api)
plug(:after_auth)
plug(Pleroma.Web.Plugs.IdempotencyPlug)
plug(Pleroma.Web.Plugs.LoggerMetadataUser)
end
# Pipeline for app-related endpoints (no user auth checks — app-bound tokens must be supported)
pipeline :app_api do
plug(:no_auth_or_privacy_expectations_api)
end
pipeline :api do
plug(:expect_public_instance_or_user_authentication)
plug(:no_auth_or_privacy_expectations_api)
plug(Pleroma.Web.Plugs.LoggerMetadataUser)
end
pipeline :authenticated_api do
plug(:expect_user_authentication)
plug(:no_auth_or_privacy_expectations_api)
plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug)
plug(Pleroma.Web.Plugs.LoggerMetadataUser)
end
pipeline :admin_api do
plug(:expect_user_authentication)
plug(:base_api)
plug(Pleroma.Web.Plugs.AdminSecretAuthenticationPlug)
plug(:after_auth)
plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug)
plug(Pleroma.Web.Plugs.UserIsStaffPlug)
plug(Pleroma.Web.Plugs.IdempotencyPlug)
plug(Pleroma.Web.Plugs.LoggerMetadataUser)
end
pipeline :require_admin do
plug(Pleroma.Web.Plugs.UserIsAdminPlug)
end
pipeline :require_privileged_role_users_delete do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :users_delete)
end
pipeline :require_privileged_role_users_manage_credentials do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :users_manage_credentials)
end
pipeline :require_privileged_role_messages_read do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :messages_read)
end
pipeline :require_privileged_role_users_manage_tags do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :users_manage_tags)
end
pipeline :require_privileged_role_users_manage_activation_state do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :users_manage_activation_state)
end
pipeline :require_privileged_role_users_manage_invites do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :users_manage_invites)
end
pipeline :require_privileged_role_reports_manage_reports do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :reports_manage_reports)
end
pipeline :require_privileged_role_users_read do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :users_read)
end
pipeline :require_privileged_role_messages_delete do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :messages_delete)
end
pipeline :require_privileged_role_emoji_manage_emoji do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :emoji_manage_emoji)
end
pipeline :require_privileged_role_instances_delete do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :instances_delete)
end
pipeline :require_privileged_role_moderation_log_read do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :moderation_log_read)
end
pipeline :require_privileged_role_statistics_read do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :statistics_read)
end
pipeline :require_privileged_role_announcements_manage_announcements do
plug(:admin_api)
plug(Pleroma.Web.Plugs.EnsurePrivilegedPlug, :announcements_manage_announcements)
end
pipeline :pleroma_html do
plug(:browser)
plug(:authenticate)
plug(Pleroma.Web.Plugs.EnsureUserTokenAssignsPlug)
plug(Pleroma.Web.Plugs.LoggerMetadataUser)
end
pipeline :well_known do
plug(:accepts, ["activity+json", "json", "jrd", "jrd+json", "xml", "xrd+xml", "html"])
end
pipeline :config do
plug(:accepts, ["json", "xml"])
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
end
pipeline :pleroma_api do
plug(:accepts, ["html", "json"])
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
plug(Pleroma.Web.Plugs.LoggerMetadataUser)
end
pipeline :mailbox_preview do
plug(:accepts, ["html"])
plug(:put_secure_browser_headers, %{
"content-security-policy" =>
"default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' 'unsafe-eval'"
})
end
pipeline :http_signature do
plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)
end
pipeline :inbox_guard do
plug(Pleroma.Web.Plugs.InboxGuardPlug)
end
pipeline :static_fe do
plug(Pleroma.Web.Plugs.StaticFEPlug)
end
- scope "/api/v1/pleroma", Pleroma.Web.TwitterAPI do
+ scope "/api/v1/pleroma", Pleroma.Web.OAuth do
pipe_through(:pleroma_api)
get("/password_reset/:token", PasswordController, :reset, as: :reset_password)
post("/password_reset", PasswordController, :do_reset, as: :reset_password)
+ end
+
+ scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
+ pipe_through(:pleroma_api)
+
get("/emoji", UtilController, :emoji)
get("/captcha", UtilController, :captcha)
get("/healthcheck", UtilController, :healthcheck)
- post("/remote_interaction", UtilController, :remote_interaction)
+ get("/federation_status", InstancesController, :show)
end
- scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
+ scope "/api/v1/pleroma", Pleroma.Web.RemoteInteraction do
pipe_through(:pleroma_api)
- get("/federation_status", InstancesController, :show)
+ post("/remote_interaction", RemoteInteractionController, :remote_interaction)
end
scope "/api/v1/pleroma", Pleroma.Web do
pipe_through(:pleroma_api)
post("/uploader_callback/:upload_path", UploaderController, :callback)
end
# AdminAPI: only admins can perform these actions
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through([:admin_api, :require_admin])
get("/users/:nickname/permission_group", AdminAPIController, :right_get)
get("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_get)
post("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_add)
delete(
"/users/:nickname/permission_group/:permission_group",
AdminAPIController,
:right_delete
)
post("/users/permission_group/:permission_group", AdminAPIController, :right_add_multiple)
delete(
"/users/permission_group/:permission_group",
AdminAPIController,
:right_delete_multiple
)
post("/users/follow", UserController, :follow)
post("/users/unfollow", UserController, :unfollow)
post("/users", UserController, :create)
patch("/users/suggest", UserController, :suggest)
patch("/users/unsuggest", UserController, :unsuggest)
get("/relay", RelayController, :index)
post("/relay", RelayController, :follow)
delete("/relay", RelayController, :unfollow)
get("/instance_document/:name", InstanceDocumentController, :show)
patch("/instance_document/:name", InstanceDocumentController, :update)
delete("/instance_document/:name", InstanceDocumentController, :delete)
get("/config", ConfigController, :show)
post("/config", ConfigController, :update)
get("/config/descriptions", ConfigController, :descriptions)
get("/need_reboot", AdminAPIController, :need_reboot)
get("/restart", AdminAPIController, :restart)
get("/oauth_app", OAuthAppController, :index)
post("/oauth_app", OAuthAppController, :create)
patch("/oauth_app/:id", OAuthAppController, :update)
delete("/oauth_app/:id", OAuthAppController, :delete)
get("/media_proxy_caches", MediaProxyCacheController, :index)
post("/media_proxy_caches/delete", MediaProxyCacheController, :delete)
post("/media_proxy_caches/purge", MediaProxyCacheController, :purge)
get("/frontends", FrontendController, :index)
post("/frontends/install", FrontendController, :install)
post("/backups", AdminAPIController, :create_backup)
get("/rules", RuleController, :index)
post("/rules", RuleController, :create)
patch("/rules/:id", RuleController, :update)
delete("/rules/:id", RuleController, :delete)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_announcements_manage_announcements)
get("/announcements", AnnouncementController, :index)
post("/announcements", AnnouncementController, :create)
get("/announcements/:id", AnnouncementController, :show)
patch("/announcements/:id", AnnouncementController, :change)
delete("/announcements/:id", AnnouncementController, :delete)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_users_delete)
delete("/users", UserController, :delete)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_users_manage_credentials)
get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
get("/users/:nickname/credentials", AdminAPIController, :show_user_credentials)
patch("/users/:nickname/credentials", AdminAPIController, :update_user_credentials)
put("/users/disable_mfa", AdminAPIController, :disable_mfa)
patch("/users/force_password_reset", AdminAPIController, :force_password_reset)
patch("/users/confirm_email", AdminAPIController, :confirm_email)
patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_messages_read)
get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses)
get("/users/:nickname/chats", AdminAPIController, :list_user_chats)
get("/statuses", StatusController, :index)
get("/chats/:id", ChatController, :show)
get("/chats/:id/messages", ChatController, :messages)
get("/instances/:instance/statuses", InstanceController, :list_statuses)
get("/statuses/:id", StatusController, :show)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_users_manage_tags)
put("/users/tag", AdminAPIController, :tag_users)
delete("/users/tag", AdminAPIController, :untag_users)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_users_manage_activation_state)
patch("/users/:nickname/toggle_activation", UserController, :toggle_activation)
patch("/users/activate", UserController, :activate)
patch("/users/deactivate", UserController, :deactivate)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_users_manage_invites)
patch("/users/approve", UserController, :approve)
post("/users/invite_token", InviteController, :create)
get("/users/invites", InviteController, :index)
post("/users/revoke_invite", InviteController, :revoke)
post("/users/email_invite", InviteController, :email)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_reports_manage_reports)
get("/reports", ReportController, :index)
get("/reports/:id", ReportController, :show)
patch("/reports", ReportController, :update)
post("/reports/assign_account", ReportController, :assign_account)
post("/reports/:id/notes", ReportController, :notes_create)
delete("/reports/:report_id/notes/:id", ReportController, :notes_delete)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_users_read)
get("/users", UserController, :index)
get("/users/:nickname", UserController, :show)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_messages_delete)
put("/statuses/:id", StatusController, :update)
delete("/statuses/:id", StatusController, :delete)
delete("/chats/:id/messages/:message_id", ChatController, :delete_message)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_emoji_manage_emoji)
post("/reload_emoji", AdminAPIController, :reload_emoji)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_instances_delete)
delete("/instances/:instance", InstanceController, :delete)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_moderation_log_read)
get("/moderation_log", AdminAPIController, :list_log)
end
# AdminAPI: admins and mods (staff) can perform these actions (if privileged by role)
scope "/api/v1/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:require_privileged_role_statistics_read)
get("/stats", AdminAPIController, :stats)
end
scope "/api/v1/pleroma/emoji", Pleroma.Web.PleromaAPI do
scope "/pack" do
pipe_through(:require_privileged_role_emoji_manage_emoji)
post("/", EmojiPackController, :create)
patch("/", EmojiPackController, :update)
delete("/", EmojiPackController, :delete)
end
scope "/pack" do
pipe_through(:api)
get("/", EmojiPackController, :show)
end
# Modifying packs
scope "/packs" do
pipe_through(:require_privileged_role_emoji_manage_emoji)
get("/import", EmojiPackController, :import_from_filesystem)
get("/remote", EmojiPackController, :remote)
post("/download", EmojiPackController, :download)
post("/download_zip", EmojiPackController, :download_zip)
post("/files", EmojiFileController, :create)
patch("/files", EmojiFileController, :update)
delete("/files", EmojiFileController, :delete)
end
# Pack info / downloading
scope "/packs" do
pipe_through(:api)
get("/", EmojiPackController, :index)
get("/archive", EmojiPackController, :archive)
end
end
- scope "/", Pleroma.Web.TwitterAPI do
+ scope "/", Pleroma.Web.RemoteInteraction do
pipe_through(:pleroma_html)
- post("/main/ostatus", UtilController, :remote_subscribe)
- get("/main/ostatus", UtilController, :show_subscribe_form)
- get("/ostatus_subscribe", RemoteFollowController, :follow)
- post("/ostatus_subscribe", RemoteFollowController, :do_follow)
+ post("/main/ostatus", RemoteInteractionController, :remote_subscribe)
+ get("/main/ostatus", RemoteInteractionController, :show_subscribe_form)
+ get("/ostatus_subscribe", RemoteInteractionController, :follow)
+ post("/ostatus_subscribe", RemoteInteractionController, :do_follow)
- get("/authorize_interaction", RemoteFollowController, :authorize_interaction)
+ get("/authorize_interaction", RemoteInteractionController, :authorize_interaction)
end
- scope "/api/pleroma", Pleroma.Web.TwitterAPI do
+ scope "/api/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:authenticated_api)
post("/change_email", UtilController, :change_email)
post("/change_password", UtilController, :change_password)
post("/delete_account", UtilController, :delete_account)
put("/notification_settings", UtilController, :update_notification_settings)
post("/disable_account", UtilController, :disable_account)
post("/move_account", UtilController, :move_account)
put("/aliases", UtilController, :add_alias)
get("/aliases", UtilController, :list_aliases)
delete("/aliases", UtilController, :delete_alias)
end
scope "/api/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:authenticated_api)
post("/mutes_import", UserImportController, :mutes)
post("/blocks_import", UserImportController, :blocks)
post("/follow_import", UserImportController, :follow)
get("/accounts/mfa", TwoFactorAuthenticationController, :settings)
get("/accounts/mfa/backup_codes", TwoFactorAuthenticationController, :backup_codes)
get("/accounts/mfa/setup/:method", TwoFactorAuthenticationController, :setup)
post("/accounts/mfa/confirm/:method", TwoFactorAuthenticationController, :confirm)
delete("/accounts/mfa/:method", TwoFactorAuthenticationController, :disable)
end
scope "/oauth", Pleroma.Web.OAuth do
# Note: use /api/v1/accounts/verify_credentials for userinfo of signed-in user
get("/registration_details", OAuthController, :registration_details)
post("/mfa/verify", MFAController, :verify, as: :mfa_verify)
get("/mfa", MFAController, :show)
scope [] do
pipe_through(:oauth)
get("/authorize", OAuthController, :authorize)
post("/authorize", OAuthController, :create_authorization)
end
scope [] do
pipe_through(:fetch_session)
post("/token", OAuthController, :token_exchange)
post("/revoke", OAuthController, :token_revoke)
post("/mfa/challenge", MFAController, :challenge)
end
scope [] do
pipe_through(:browser)
get("/prepare_request", OAuthController, :prepare_request)
get("/:provider", OAuthController, :request)
get("/:provider/callback", OAuthController, :callback)
post("/register", OAuthController, :register)
end
end
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:api)
get("/apps", AppController, :index)
get("/statuses/:id/reactions/:emoji", EmojiReactionController, :index)
get("/statuses/:id/reactions", EmojiReactionController, :index)
get(
"/preferred_frontend/available",
FrontendSettingsController,
:available_frontends
)
put(
"/preferred_frontend",
FrontendSettingsController,
:update_preferred_frontend
)
end
scope "/api/v0/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:authenticated_api)
get("/reports", ReportController, :index)
get("/reports/:id", ReportController, :show)
end
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
scope [] do
pipe_through(:authenticated_api)
post("/chats/by-account-id/:id", ChatController, :create)
get("/chats", ChatController, :index)
get("/chats/:id", ChatController, :show)
get("/chats/:id/messages", ChatController, :messages)
post("/chats/:id/messages", ChatController, :post_chat_message)
delete("/chats/:id/messages/:message_id", ChatController, :delete_message)
post("/chats/:id/read", ChatController, :mark_as_read)
post("/chats/:id/messages/:message_id/read", ChatController, :mark_message_as_read)
post("/chats/:id/pin", ChatController, :pin)
post("/chats/:id/unpin", ChatController, :unpin)
get("/conversations/:id/statuses", ConversationController, :statuses)
get("/conversations/:id", ConversationController, :show)
post("/conversations/read", ConversationController, :mark_as_read)
patch("/conversations/:id", ConversationController, :update)
put("/statuses/:id/reactions/:emoji", EmojiReactionController, :create)
delete("/statuses/:id/reactions/:emoji", EmojiReactionController, :delete)
post("/notifications/read", NotificationController, :mark_as_read)
get("/mascot", MascotController, :show)
put("/mascot", MascotController, :update)
post("/scrobble", ScrobbleController, :create)
get("/backups", BackupController, :index)
post("/backups", BackupController, :create)
get("/bookmark_folders", BookmarkFolderController, :index)
post("/bookmark_folders", BookmarkFolderController, :create)
patch("/bookmark_folders/:id", BookmarkFolderController, :update)
delete("/bookmark_folders/:id", BookmarkFolderController, :delete)
get("/outgoing_follow_requests", FollowRequestController, :outgoing)
end
scope [] do
pipe_through(:api)
get("/accounts/:id/favourites", AccountController, :favourites)
get("/statuses/:id/quotes", StatusController, :quotes)
end
scope [] do
pipe_through(:authenticated_api)
post("/accounts/:id/subscribe", AccountController, :subscribe)
post("/accounts/:id/unsubscribe", AccountController, :unsubscribe)
get("/birthdays", AccountController, :birthdays)
end
scope [] do
pipe_through(:authenticated_api)
get("/settings/:app", SettingsController, :show)
patch("/settings/:app", SettingsController, :update)
end
post("/accounts/confirmation_resend", AccountController, :confirmation_resend)
end
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:api)
get("/accounts/:id/scrobbles", ScrobbleController, :index)
end
scope "/api/v1/pleroma", Pleroma.Web.MastodonAPI do
pipe_through(:api)
get("/accounts/:id/endorsements", AccountController, :endorsements)
end
scope "/api/v2/pleroma", Pleroma.Web.PleromaAPI do
scope [] do
pipe_through(:authenticated_api)
get("/chats", ChatController, :index2)
end
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:authenticated_api)
get("/accounts/verify_credentials", AccountController, :verify_credentials)
patch("/accounts/update_credentials", AccountController, :update_credentials)
get("/accounts/relationships", AccountController, :relationships)
get("/accounts/familiar_followers", AccountController, :familiar_followers)
get("/accounts/:id/lists", AccountController, :lists)
get("/endorsements", AccountController, :own_endorsements)
get("/blocks", AccountController, :blocks)
get("/mutes", AccountController, :mutes)
post("/follows", AccountController, :follow_by_uri)
post("/accounts/:id/follow", AccountController, :follow)
post("/accounts/:id/unfollow", AccountController, :unfollow)
post("/accounts/:id/block", AccountController, :block)
post("/accounts/:id/unblock", AccountController, :unblock)
post("/accounts/:id/mute", AccountController, :mute)
post("/accounts/:id/unmute", AccountController, :unmute)
post("/accounts/:id/note", AccountController, :note)
post("/accounts/:id/pin", AccountController, :endorse)
post("/accounts/:id/unpin", AccountController, :unendorse)
post("/accounts/:id/endorse", AccountController, :endorse)
post("/accounts/:id/unendorse", AccountController, :unendorse)
post("/accounts/:id/remove_from_followers", AccountController, :remove_from_followers)
get("/conversations", ConversationController, :index)
post("/conversations/:id/read", ConversationController, :mark_as_read)
delete("/conversations/:id", ConversationController, :delete)
get("/domain_blocks", DomainBlockController, :index)
post("/domain_blocks", DomainBlockController, :create)
delete("/domain_blocks", DomainBlockController, :delete)
get("/filters", FilterController, :index)
post("/filters", FilterController, :create)
get("/filters/:id", FilterController, :show)
put("/filters/:id", FilterController, :update)
delete("/filters/:id", FilterController, :delete)
get("/follow_requests", FollowRequestController, :index)
post("/follow_requests/:id/authorize", FollowRequestController, :authorize)
post("/follow_requests/:id/reject", FollowRequestController, :reject)
get("/lists", ListController, :index)
get("/lists/:id", ListController, :show)
get("/lists/:id/accounts", ListController, :list_accounts)
delete("/lists/:id", ListController, :delete)
post("/lists", ListController, :create)
put("/lists/:id", ListController, :update)
post("/lists/:id/accounts", ListController, :add_to_list)
delete("/lists/:id/accounts", ListController, :remove_from_list)
get("/markers", MarkerController, :index)
post("/markers", MarkerController, :upsert)
post("/media", MediaController, :create)
get("/media/:id", MediaController, :show)
put("/media/:id", MediaController, :update)
get("/notifications", NotificationController, :index)
get("/notifications/:id", NotificationController, :show)
post("/notifications/:id/dismiss", NotificationController, :dismiss)
post("/notifications/clear", NotificationController, :clear)
delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple)
# Deprecated: was removed in Mastodon v3, use `/notifications/:id/dismiss` instead
post("/notifications/dismiss", NotificationController, :dismiss_via_body)
post("/polls/:id/votes", PollController, :vote)
post("/reports", ReportController, :create)
get("/scheduled_statuses", ScheduledActivityController, :index)
get("/scheduled_statuses/:id", ScheduledActivityController, :show)
put("/scheduled_statuses/:id", ScheduledActivityController, :update)
delete("/scheduled_statuses/:id", ScheduledActivityController, :delete)
# Unlike `GET /api/v1/accounts/:id/favourites`, demands authentication
get("/favourites", StatusController, :favourites)
get("/bookmarks", StatusController, :bookmarks)
post("/statuses", StatusController, :create)
put("/statuses/:id", StatusController, :update)
delete("/statuses/:id", StatusController, :delete)
post("/statuses/:id/reblog", StatusController, :reblog)
post("/statuses/:id/unreblog", StatusController, :unreblog)
post("/statuses/:id/favourite", StatusController, :favourite)
post("/statuses/:id/unfavourite", StatusController, :unfavourite)
post("/statuses/:id/pin", StatusController, :pin)
post("/statuses/:id/unpin", StatusController, :unpin)
post("/statuses/:id/bookmark", StatusController, :bookmark)
post("/statuses/:id/unbookmark", StatusController, :unbookmark)
post("/statuses/:id/mute", StatusController, :mute_conversation)
post("/statuses/:id/unmute", StatusController, :unmute_conversation)
post("/statuses/:id/translate", StatusController, :translate)
get("/statuses/:id/quotes", StatusController, :quotes)
post("/push/subscription", SubscriptionController, :create)
get("/push/subscription", SubscriptionController, :show)
put("/push/subscription", SubscriptionController, :update)
delete("/push/subscription", SubscriptionController, :delete)
get("/suggestions", SuggestionController, :index)
delete("/suggestions/:account_id", SuggestionController, :dismiss)
get("/timelines/home", TimelineController, :home)
get("/timelines/direct", TimelineController, :direct)
get("/timelines/list/:list_id", TimelineController, :list)
get("/announcements", AnnouncementController, :index)
post("/announcements/:id/dismiss", AnnouncementController, :mark_read)
get("/tags/:id", TagController, :show)
post("/tags/:id/follow", TagController, :follow)
post("/tags/:id/unfollow", TagController, :unfollow)
get("/followed_tags", TagController, :show_followed)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:app_api)
post("/apps", AppController, :create)
get("/apps/verify_credentials", AppController, :verify_credentials)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:api)
get("/accounts/search", SearchController, :account_search)
get("/search", SearchController, :search)
get("/accounts/lookup", AccountController, :lookup)
get("/accounts/:id/statuses", AccountController, :statuses)
get("/accounts/:id/followers", AccountController, :followers)
get("/accounts/:id/following", AccountController, :following)
get("/accounts/:id/endorsements", AccountController, :endorsements)
get("/accounts/:id", AccountController, :show)
post("/accounts", AccountController, :create)
get("/instance", InstanceController, :show)
get("/instance/peers", InstanceController, :peers)
get("/instance/rules", InstanceController, :rules)
get("/instance/domain_blocks", InstanceController, :domain_blocks)
get("/instance/translation_languages", InstanceController, :translation_languages)
get("/statuses", StatusController, :index)
get("/statuses/:id", StatusController, :show)
get("/statuses/:id/context", StatusController, :context)
get("/statuses/:id/favourited_by", StatusController, :favourited_by)
get("/statuses/:id/reblogged_by", StatusController, :reblogged_by)
get("/statuses/:id/history", StatusController, :show_history)
get("/statuses/:id/source", StatusController, :show_source)
get("/custom_emojis", CustomEmojiController, :index)
get("/trends", MastodonAPIController, :empty_array)
get("/timelines/public", TimelineController, :public)
get("/timelines/tag/:tag", TimelineController, :hashtag)
get("/polls/:id", PollController, :show)
get("/directory", DirectoryController, :index)
end
scope "/api/v2", Pleroma.Web.MastodonAPI do
pipe_through(:api)
get("/search", SearchController, :search2)
post("/media", MediaController, :create2)
get("/suggestions", SuggestionController, :index2)
get("/instance", InstanceController, :show2)
end
scope "/api", Pleroma.Web do
pipe_through(:config)
- get("/pleroma/frontend_configurations", TwitterAPI.UtilController, :frontend_configurations)
+ get("/pleroma/frontend_configurations", PleromaAPI.UtilController, :frontend_configurations)
end
scope "/api", Pleroma.Web do
pipe_through(:api)
get(
"/account/confirm_email/:user_id/:token",
- TwitterAPI.Controller,
+ OAuth.TokenController,
:confirm_email,
as: :confirm_email
)
end
scope "/api" do
pipe_through(:base_api)
get("/openapi", OpenApiSpex.Plug.RenderSpec, [])
end
- scope "/api", Pleroma.Web, as: :authenticated_twitter_api do
+ scope "/api", Pleroma.Web, as: :authenticated_pleroma_api do
pipe_through(:authenticated_api)
- get("/oauth_tokens", TwitterAPI.Controller, :oauth_tokens)
- delete("/oauth_tokens/:id", TwitterAPI.Controller, :revoke_token)
+ get("/oauth_tokens", OAuth.TokenController, :oauth_tokens)
+ delete("/oauth_tokens/:id", OAuth.TokenController, :revoke_token)
end
scope "/", Pleroma.Web do
# Note: html format is supported only if static FE is enabled
# Note: http signature is only considered for json requests (no auth for non-json requests)
pipe_through([:accepts_html_json, :http_signature, :static_fe])
get("/objects/:uuid", OStatus.OStatusController, :object)
get("/activities/:uuid", OStatus.OStatusController, :activity)
get("/notice/:id", OStatus.OStatusController, :notice)
# Mastodon compatibility routes
get("/users/:nickname/statuses/:id", OStatus.OStatusController, :object)
get("/users/:nickname/statuses/:id/activity", OStatus.OStatusController, :activity)
end
scope "/", Pleroma.Web do
# Note: html format is supported only if static FE is enabled
# Note: http signature is only considered for json requests (no auth for non-json requests)
pipe_through([:accepts_html_xml_json, :http_signature, :static_fe])
# Note: returns user _profile_ for json requests, redirects to user _feed_ for non-json ones
get("/users/:nickname", Feed.UserController, :feed_redirect, as: :user_feed)
end
scope "/", Pleroma.Web do
pipe_through([:accepts_html_xml])
get("/users/:nickname/feed", Feed.UserController, :feed, as: :user_feed)
end
scope "/", Pleroma.Web do
pipe_through(:accepts_html)
get("/notice/:id/embed_player", OStatus.OStatusController, :notice_player)
end
scope "/", Pleroma.Web do
pipe_through(:accepts_xml_rss_atom)
get("/tags/:tag", Feed.TagController, :feed, as: :tag_feed)
end
scope "/", Pleroma.Web do
pipe_through(:browser)
get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe)
get("/frontend_switcher", FrontendSwitcher.FrontendSwitcherController, :switch)
post("/frontend_switcher", FrontendSwitcher.FrontendSwitcherController, :do_switch)
end
pipeline :ap_service_actor do
plug(:accepts, ["activity+json", "json"])
end
# Server to Server (S2S) AP interactions
pipeline :activitypub do
plug(:ap_service_actor)
plug(:http_signature)
end
# Client to Server (C2S) AP interactions
pipeline :activitypub_client do
plug(:ap_service_actor)
plug(Pleroma.Web.Plugs.APClientApiEnabledPlug)
plug(:fetch_session)
plug(:authenticate)
plug(:after_auth)
end
# AP interactions used by both S2S and C2S
pipeline :activitypub_server_or_client do
plug(:ap_service_actor)
plug(:fetch_session)
plug(:authenticate)
plug(Pleroma.Web.Plugs.APClientApiEnabledPlug, allow_server: true)
plug(:after_auth)
plug(:http_signature)
end
scope "/", Pleroma.Web.ActivityPub do
pipe_through([:activitypub_client])
get("/api/ap/whoami", ActivityPubController, :whoami)
get("/users/:nickname/inbox", ActivityPubController, :read_inbox)
post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
post("/api/ap/upload_media", ActivityPubController, :upload_media)
end
scope "/", Pleroma.Web.ActivityPub do
pipe_through([:activitypub_server_or_client])
get("/users/:nickname/outbox", ActivityPubController, :outbox)
get("/users/:nickname/followers", ActivityPubController, :followers)
get("/users/:nickname/following", ActivityPubController, :following)
get("/users/:nickname/collections/featured", ActivityPubController, :pinned)
get("/objects/:uuid/replies", ActivityPubController, :object_replies)
end
scope "/", Pleroma.Web.ActivityPub do
pipe_through([:activitypub, :inbox_guard])
post("/inbox", ActivityPubController, :inbox)
post("/users/:nickname/inbox", ActivityPubController, :inbox)
end
scope "/relay", Pleroma.Web.ActivityPub do
pipe_through(:ap_service_actor)
get("/", ActivityPubController, :relay)
scope [] do
pipe_through(:http_signature)
post("/inbox", ActivityPubController, :inbox)
end
get("/following", ActivityPubController, :relay_following)
get("/followers", ActivityPubController, :relay_followers)
end
scope "/internal/fetch", Pleroma.Web.ActivityPub do
pipe_through(:ap_service_actor)
get("/", ActivityPubController, :internal_fetch)
post("/inbox", ActivityPubController, :inbox)
end
scope "/.well-known", Pleroma.Web do
pipe_through(:well_known)
get("/host-meta", WebFinger.WebFingerController, :host_meta)
get("/webfinger", WebFinger.WebFingerController, :webfinger)
get("/nodeinfo", Nodeinfo.NodeinfoController, :schemas)
end
scope "/nodeinfo", Pleroma.Web do
get("/:version", Nodeinfo.NodeinfoController, :nodeinfo)
end
scope "/", Pleroma.Web do
pipe_through(:api)
get("/manifest.json", ManifestController, :show)
end
scope "/", Pleroma.Web do
pipe_through(:pleroma_html)
- post("/auth/password", TwitterAPI.PasswordController, :request)
+ post("/auth/password", OAuth.PasswordController, :request)
end
scope "/proxy/", Pleroma.Web do
get("/preview/:sig/:url", MediaProxy.MediaProxyController, :preview)
get("/preview/:sig/:url/:filename", MediaProxy.MediaProxyController, :preview)
get("/:sig/:url", MediaProxy.MediaProxyController, :remote)
get("/:sig/:url/:filename", MediaProxy.MediaProxyController, :remote)
end
if Pleroma.Config.get(:env) == :dev do
scope "/dev" do
pipe_through([:mailbox_preview])
forward("/mailbox", Plug.Swoosh.MailboxPreview, base_path: "/dev/mailbox")
end
end
scope "/" do
pipe_through([:pleroma_html, :authenticate, :require_admin])
live_dashboard("/pleroma/live_dashboard", additional_pages: [oban: Oban.LiveDashboard])
oban_dashboard("/pleroma/oban")
end
# Test-only routes needed to test action dispatching and plug chain execution
if Pleroma.Config.get(:env) == :test do
@test_actions [
:do_oauth_check,
:fallback_oauth_check,
:skip_oauth_check,
:fallback_oauth_skip_publicity_check,
:skip_oauth_skip_publicity_check,
:missing_oauth_check_definition
]
scope "/test/api", Pleroma.Tests do
pipe_through(:api)
for action <- @test_actions do
get("/#{action}", AuthTestController, action)
end
end
scope "/test/authenticated_api", Pleroma.Tests do
pipe_through(:authenticated_api)
for action <- @test_actions do
get("/#{action}", AuthTestController, action)
end
end
end
scope "/", Pleroma.Web.MongooseIM do
get("/user_exists", MongooseIMController, :user_exists)
get("/check_password", MongooseIMController, :check_password)
end
scope "/", Pleroma.Web.Fallback do
get("/registration/:token", RedirectController, :registration_page)
get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta)
match(:*, "/api/pleroma/*path", LegacyPleromaApiRerouterPlug, [])
get("/api/*path", RedirectController, :api_not_implemented)
get("/phoenix/live_dashboard", RedirectController, :live_dashboard)
get("/*path", RedirectController, :redirector_with_preload)
options("/*path", RedirectController, :empty)
end
# /pleroma/{phoenix,oban}/* need to get filtered out from api routes for frontend configuration
# to not drop admin overrides for /pleroma/admin.
@non_api_routes ["/pleroma/live_dashboard", "/pleroma/oban"]
def get_api_routes do
Phoenix.Router.routes(__MODULE__)
|> Enum.reject(fn r -> r.plug == Pleroma.Web.Fallback.RedirectController end)
|> Enum.reject(fn r -> String.starts_with?(r.path, @non_api_routes) end)
|> Enum.map(fn r ->
r.path
|> String.split("/", trim: true)
|> List.first()
end)
|> Enum.uniq()
end
end
diff --git a/lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex b/lib/pleroma/web/templates/o_auth/password/invalid_token.html.eex
similarity index 100%
rename from lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex
rename to lib/pleroma/web/templates/o_auth/password/invalid_token.html.eex
diff --git a/lib/pleroma/web/templates/twitter_api/password/reset.html.eex b/lib/pleroma/web/templates/o_auth/password/reset.html.eex
similarity index 100%
rename from lib/pleroma/web/templates/twitter_api/password/reset.html.eex
rename to lib/pleroma/web/templates/o_auth/password/reset.html.eex
diff --git a/lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex b/lib/pleroma/web/templates/o_auth/password/reset_failed.html.eex
similarity index 100%
rename from lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex
rename to lib/pleroma/web/templates/o_auth/password/reset_failed.html.eex
diff --git a/lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex b/lib/pleroma/web/templates/o_auth/password/reset_success.html.eex
similarity index 100%
rename from lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex
rename to lib/pleroma/web/templates/o_auth/password/reset_success.html.eex
diff --git a/lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex b/lib/pleroma/web/templates/remote_interaction/remote_interaction/follow.html.eex
similarity index 83%
rename from lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex
rename to lib/pleroma/web/templates/remote_interaction/remote_interaction/follow.html.eex
index e2d251fac..00cc7d383 100644
--- a/lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex
+++ b/lib/pleroma/web/templates/remote_interaction/remote_interaction/follow.html.eex
@@ -1,11 +1,11 @@
<%= if @error == :error do %>
<h2><%= Gettext.dpgettext("static_pages", "remote follow error", "Error fetching user") %></h2>
<% else %>
<h2><%= Gettext.dpgettext("static_pages", "remote follow header", "Remote follow") %></h2>
<img height="128" width="128" src="<%= avatar_url(@followee) %>">
<p><%= @followee.nickname %></p>
- <%= form_for @conn, Routes.remote_follow_path(@conn, :do_follow), [as: "user"], fn f -> %>
+ <%= form_for @conn, Routes.remote_interaction_path(@conn, :do_follow), [as: "user"], fn f -> %>
<%= hidden_input f, :id, value: @followee.id %>
<%= submit Gettext.dpgettext("static_pages", "remote follow authorization button", "Authorize") %>
<% end %>
<% end %>
diff --git a/lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex b/lib/pleroma/web/templates/remote_interaction/remote_interaction/follow_login.html.eex
similarity index 88%
rename from lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex
rename to lib/pleroma/web/templates/remote_interaction/remote_interaction/follow_login.html.eex
index 26340a906..e5fe720e8 100644
--- a/lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex
+++ b/lib/pleroma/web/templates/remote_interaction/remote_interaction/follow_login.html.eex
@@ -1,14 +1,14 @@
<%= if @error do %>
<h2><%= @error %></h2>
<% end %>
<h2><%= Gettext.dpgettext("static_pages", "remote follow header, need login", "Log in to follow") %></h2>
<p><%= @followee.nickname %></p>
<img height="128" width="128" src="<%= avatar_url(@followee) %>">
-<%= form_for @conn, Routes.remote_follow_path(@conn, :do_follow), [as: "authorization"], fn f -> %>
+<%= form_for @conn, Routes.remote_interaction_path(@conn, :do_follow), [as: "authorization"], fn f -> %>
<%= text_input f, :name, placeholder: Gettext.dpgettext("static_pages", "placeholder text for username entry", "Username"), required: true, autocomplete: "username" %>
<br>
<%= password_input f, :password, placeholder: Gettext.dpgettext("static_pages", "placeholder text for password entry", "Password"), required: true, autocomplete: "password" %>
<br>
<%= hidden_input f, :id, value: @followee.id %>
<%= submit Gettext.dpgettext("static_pages", "remote follow authorization button for login", "Authorize") %>
<% end %>
diff --git a/lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex b/lib/pleroma/web/templates/remote_interaction/remote_interaction/follow_mfa.html.eex
similarity index 86%
rename from lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex
rename to lib/pleroma/web/templates/remote_interaction/remote_interaction/follow_mfa.html.eex
index 638212c1e..e5f26f104 100644
--- a/lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex
+++ b/lib/pleroma/web/templates/remote_interaction/remote_interaction/follow_mfa.html.eex
@@ -1,13 +1,13 @@
<%= if @error do %>
<h2><%= @error %></h2>
<% end %>
<h2><%= Gettext.dpgettext("static_pages", "remote follow mfa header", "Two-factor authentication") %></h2>
<p><%= @followee.nickname %></p>
<img height="128" width="128" src="<%= avatar_url(@followee) %>">
-<%= form_for @conn, Routes.remote_follow_path(@conn, :do_follow), [as: "mfa"], fn f -> %>
+<%= form_for @conn, Routes.remote_interaction_path(@conn, :do_follow), [as: "mfa"], fn f -> %>
<%= text_input f, :code, placeholder: Gettext.dpgettext("static_pages", "placeholder text for auth code entry", "Authentication code"), required: true %>
<br>
<%= hidden_input f, :id, value: @followee.id %>
<%= hidden_input f, :token, value: @mfa_token %>
<%= submit Gettext.dpgettext("static_pages", "remote follow authorization button for mfa", "Authorize") %>
<% end %>
diff --git a/lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex b/lib/pleroma/web/templates/remote_interaction/remote_interaction/followed.html.eex
similarity index 100%
rename from lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex
rename to lib/pleroma/web/templates/remote_interaction/remote_interaction/followed.html.eex
diff --git a/lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex b/lib/pleroma/web/templates/remote_interaction/remote_interaction/status_interact.html.eex
similarity index 88%
rename from lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex
rename to lib/pleroma/web/templates/remote_interaction/remote_interaction/status_interact.html.eex
index d77174967..b3d590186 100644
--- a/lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex
+++ b/lib/pleroma/web/templates/remote_interaction/remote_interaction/status_interact.html.eex
@@ -1,10 +1,10 @@
<%= if @error do %>
<h2><%= Gettext.dpgettext("static_pages", "status interact error", "Error: %{error}", error: @error) %></h2>
<% else %>
<h2><%= raw Gettext.dpgettext("static_pages", "status interact header", "Interacting with %{nickname}'s %{status_link}", nickname: safe_to_string(html_escape(@nickname)), status_link: safe_to_string(link(Gettext.dpgettext("static_pages", "status interact header - status link text", "status"), to: @status_link))) %></h2>
- <%= form_for @conn, Routes.util_path(@conn, :remote_subscribe), [as: "status"], fn f -> %>
+ <%= form_for @conn, Routes.remote_interaction_path(@conn, :remote_subscribe), [as: "status"], fn f -> %>
<%= hidden_input f, :status_id, value: @status_id %>
<%= text_input f, :profile, placeholder: Gettext.dpgettext("static_pages", "placeholder text for account id", "Your account ID, e.g. lain@quitter.se") %>
<%= submit Gettext.dpgettext("static_pages", "status interact authorization button", "Interact") %>
<% end %>
<% end %>
diff --git a/lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex b/lib/pleroma/web/templates/remote_interaction/remote_interaction/subscribe.html.eex
similarity index 85%
rename from lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex
rename to lib/pleroma/web/templates/remote_interaction/remote_interaction/subscribe.html.eex
index 848660f26..7e1dd723c 100644
--- a/lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex
+++ b/lib/pleroma/web/templates/remote_interaction/remote_interaction/subscribe.html.eex
@@ -1,10 +1,10 @@
<%= if @error do %>
<h2><%= Gettext.dpgettext("static_pages", "remote follow error", "Error: %{error}", error: @error) %></h2>
<% else %>
<h2><%= Gettext.dpgettext("static_pages", "remote follow header", "Remotely follow %{nickname}", nickname: @nickname) %></h2>
- <%= form_for @conn, Routes.util_path(@conn, :remote_subscribe), [as: "user"], fn f -> %>
+ <%= form_for @conn, Routes.remote_interaction_path(@conn, :remote_subscribe), [as: "user"], fn f -> %>
<%= hidden_input f, :nickname, value: @nickname %>
<%= text_input f, :profile, placeholder: Gettext.dpgettext("static_pages", "placeholder text for account id", "Your account ID, e.g. lain@quitter.se") %>
<%= submit Gettext.dpgettext("static_pages", "remote follow authorization button for following with a remote account", "Follow") %>
<% end %>
<% end %>
diff --git a/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex b/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex
index a14ca305e..85bfd7b3a 100644
--- a/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex
+++ b/lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex
@@ -1,31 +1,31 @@
<header>
<h1><%= link instance_name(), to: "/" %></h1>
<h3>
- <form class="pull-right collapse" method="POST" action="<%= Helpers.util_path(@conn, :remote_subscribe) %>">
+ <form class="pull-right collapse" method="POST" action="<%= Helpers.remote_interaction_path(@conn, :remote_subscribe) %>">
<input type="hidden" name="nickname" value="<%= @user.nickname %>">
<input type="hidden" name="profile" value="">
<button type="submit" class="collapse"><%= Gettext.dpgettext("static_pages", "static fe profile page remote follow button", "Remote follow") %></button>
</form>
<%= raw Formatter.emojify(@user.name, @user.emoji) %> |
<%= link "@#{@user.nickname}@#{Endpoint.host()}", to: (@user.uri || @user.ap_id) %>
</h3>
<p><%= raw @user.bio %></p>
</header>
<main>
<div class="activity-stream">
<%= for activity <- @timeline do %>
<%= render("_notice.html", Map.put(activity, :selected, false)) %>
<% end %>
<p id="pagination">
<%= if @prev_page_id do %>
<%= link "«", to: "?min_id=" <> @prev_page_id %>
<% end %>
<%= if @prev_page_id && @next_page_id, do: " | " %>
<%= if @next_page_id do %>
<%= link "»", to: "?max_id=" <> @next_page_id %>
<% end %>
</p>
</div>
</main>
diff --git a/lib/pleroma/web/twitter_api/views/util_view.ex b/lib/pleroma/web/twitter_api/views/util_view.ex
deleted file mode 100644
index 31b7c0c0c..000000000
--- a/lib/pleroma/web/twitter_api/views/util_view.ex
+++ /dev/null
@@ -1,31 +0,0 @@
-# Pleroma: A lightweight social networking server
-# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
-# SPDX-License-Identifier: AGPL-3.0-only
-
-defmodule Pleroma.Web.TwitterAPI.UtilView do
- use Pleroma.Web, :view
- import Phoenix.HTML
- import Phoenix.HTML.Form
- import Phoenix.HTML.Link
- alias Pleroma.Config
- alias Pleroma.Web.Endpoint
- alias Pleroma.Web.Gettext
-
- def status_net_config(instance) do
- """
- <config>
- <site>
- <name>#{Keyword.get(instance, :name)}</name>
- <site>#{Endpoint.url()}</site>
- <textlimit>#{Keyword.get(instance, :limit)}</textlimit>
- <closed>#{!Keyword.get(instance, :registrations_open)}</closed>
- </site>
- </config>
- """
- end
-
- def render("frontend_configurations.json", _) do
- Config.get(:frontend_configurations, %{})
- |> Enum.into(%{})
- end
-end
diff --git a/test/pleroma/web/activity_pub/mrf/hashtag_policy_test.exs b/test/pleroma/web/activity_pub/mrf/hashtag_policy_test.exs
index 32991c966..e231f88a8 100644
--- a/test/pleroma/web/activity_pub/mrf/hashtag_policy_test.exs
+++ b/test/pleroma/web/activity_pub/mrf/hashtag_policy_test.exs
@@ -1,101 +1,101 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.HashtagPolicyTest do
use Oban.Testing, repo: Pleroma.Repo
use Pleroma.DataCase
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
test "it sets the sensitive property with relevant hashtags" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "#nsfw hey"})
- {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
+ {:ok, modified} = Transmogrifier.prepare_activity(activity.data)
assert modified["object"]["sensitive"]
end
test "it is history-aware" do
activity = %{
"type" => "Create",
"object" => %{
"content" => "hey",
"tag" => []
}
}
activity_data =
activity
|> put_in(
["object", "formerRepresentations"],
%{
"type" => "OrderedCollection",
"orderedItems" => [
Map.put(
activity["object"],
"tag",
[%{"type" => "Hashtag", "name" => "#nsfw"}]
)
]
}
)
{:ok, modified} =
Pleroma.Web.ActivityPub.MRF.filter_one(
Pleroma.Web.ActivityPub.MRF.HashtagPolicy,
activity_data
)
refute modified["object"]["sensitive"]
assert Enum.at(modified["object"]["formerRepresentations"]["orderedItems"], 0)["sensitive"]
end
test "it works with Update" do
activity = %{
"type" => "Update",
"object" => %{
"content" => "hey",
"tag" => []
}
}
activity_data =
activity
|> put_in(
["object", "formerRepresentations"],
%{
"type" => "OrderedCollection",
"orderedItems" => [
Map.put(
activity["object"],
"tag",
[%{"type" => "Hashtag", "name" => "#nsfw"}]
)
]
}
)
{:ok, modified} =
Pleroma.Web.ActivityPub.MRF.filter_one(
Pleroma.Web.ActivityPub.MRF.HashtagPolicy,
activity_data
)
refute modified["object"]["sensitive"]
assert Enum.at(modified["object"]["formerRepresentations"]["orderedItems"], 0)["sensitive"]
end
test "it doesn't sets the sensitive property with irrelevant hashtags" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "#cofe hey"})
- {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
+ {:ok, modified} = Transmogrifier.prepare_activity(activity.data)
refute modified["object"]["sensitive"]
end
end
diff --git a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
index 3c7ff0eeb..c32811c5b 100644
--- a/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
+++ b/test/pleroma/web/activity_pub/object_validators/article_note_page_validator_test.exs
@@ -1,309 +1,309 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidatorTest do
use Pleroma.DataCase, async: true
alias Pleroma.Language.LanguageDetectorMock
alias Pleroma.StaticStubbedConfigMock
alias Pleroma.Web.ActivityPub.ObjectValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator
alias Pleroma.Web.ActivityPub.Utils
import Mox
import Pleroma.Factory
# Setup for all tests
setup do
# Stub the StaticStubbedConfigMock to return our mock for the provider
StaticStubbedConfigMock
|> stub(:get, fn
[Pleroma.Language.LanguageDetector, :provider] -> LanguageDetectorMock
_other -> nil
end)
# Stub the LanguageDetectorMock with default implementations
LanguageDetectorMock
|> stub(:missing_dependencies, fn -> [] end)
|> stub(:configured?, fn -> true end)
|> stub(:detect, fn _text -> nil end)
:ok
end
describe "Notes" do
setup do
user = insert(:user)
note = %{
"id" => Utils.generate_activity_id(),
"type" => "Note",
"actor" => user.ap_id,
"to" => [user.follower_address],
"cc" => [],
"content" => "Hellow this is content.",
"context" => "xxx",
"summary" => "a post"
}
%{user: user, note: note}
end
test "a basic note validates", %{note: note} do
%{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
end
test "a note from factory validates" do
note = insert(:note)
%{valid?: true} = ArticleNotePageValidator.cast_and_validate(note.data)
end
end
describe "Note with history" do
setup do
user = insert(:user)
{:ok, activity} = Pleroma.Web.CommonAPI.post(user, %{status: "mew mew :dinosaur:"})
{:ok, edit} = Pleroma.Web.CommonAPI.update(activity, user, %{status: "edited :blank:"})
{:ok, %{"object" => external_rep}} =
- Pleroma.Web.ActivityPub.Transmogrifier.prepare_outgoing(edit.data)
+ Pleroma.Web.ActivityPub.Transmogrifier.prepare_activity(edit.data)
%{external_rep: external_rep}
end
test "edited note", %{external_rep: external_rep} do
assert %{"formerRepresentations" => %{"orderedItems" => [%{"tag" => [_]}]}} = external_rep
{:ok, validate_res, []} = ObjectValidator.validate(external_rep, [])
assert %{"formerRepresentations" => %{"orderedItems" => [%{"emoji" => %{"dinosaur" => _}}]}} =
validate_res
end
test "edited note, badly-formed formerRepresentations", %{external_rep: external_rep} do
external_rep = Map.put(external_rep, "formerRepresentations", %{})
assert {:error, _} = ObjectValidator.validate(external_rep, [])
end
test "edited note, badly-formed history item", %{external_rep: external_rep} do
history_item =
Enum.at(external_rep["formerRepresentations"]["orderedItems"], 0)
|> Map.put("type", "Foo")
external_rep =
put_in(
external_rep,
["formerRepresentations", "orderedItems"],
[history_item]
)
assert {:error, _} = ObjectValidator.validate(external_rep, [])
end
end
test "a Note from Roadhouse validates" do
insert(:user, ap_id: "https://macgirvin.com/channel/mike")
%{"object" => note} =
"test/fixtures/roadhouse-create-activity.json"
|> File.read!()
|> Jason.decode!()
%{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
end
test "a Note from Convergence AP Bridge validates" do
insert(:user, ap_id: "https://cc.mkdir.uk/ap/acct/hiira")
note =
"test/fixtures/ccworld-ap-bridge_note.json"
|> File.read!()
|> Jason.decode!()
%{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
end
test "a note with an attachment should work", _ do
insert(:user, %{ap_id: "https://owncast.localhost.localdomain/federation/user/streamer"})
note =
"test/fixtures/owncast-note-with-attachment.json"
|> File.read!()
|> Jason.decode!()
%{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
end
test "a Note without replies/first/items validates" do
insert(:user, ap_id: "https://mastodon.social/users/emelie")
note =
"test/fixtures/tesla_mock/status.emelie.json"
|> File.read!()
|> Jason.decode!()
|> pop_in(["replies", "first", "items"])
|> elem(1)
%{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
end
test "a Note with validated likes collection validates" do
insert(:user, ap_id: "https://pol.social/users/mkljczk")
%{"object" => note} =
"test/fixtures/mastodon-update-with-likes.json"
|> File.read!()
|> Jason.decode!()
%{valid?: true} = ArticleNotePageValidator.cast_and_validate(note)
end
test "Fedibird quote post" do
insert(:user, ap_id: "https://fedibird.com/users/noellabo")
data = File.read!("test/fixtures/quote_post/fedibird_quote_post.json") |> Jason.decode!()
cng = ArticleNotePageValidator.cast_and_validate(data)
assert cng.valid?
assert cng.changes.quoteUrl == "https://misskey.io/notes/8vsn2izjwh"
end
test "Fedibird quote post with quoteUri field" do
insert(:user, ap_id: "https://fedibird.com/users/noellabo")
data = File.read!("test/fixtures/quote_post/fedibird_quote_uri.json") |> Jason.decode!()
cng = ArticleNotePageValidator.cast_and_validate(data)
assert cng.valid?
assert cng.changes.quoteUrl == "https://fedibird.com/users/yamako/statuses/107699333438289729"
end
test "Misskey quote post" do
insert(:user, ap_id: "https://misskey.io/users/7rkrarq81i")
data = File.read!("test/fixtures/quote_post/misskey_quote_post.json") |> Jason.decode!()
cng = ArticleNotePageValidator.cast_and_validate(data)
assert cng.valid?
assert cng.changes.quoteUrl == "https://misskey.io/notes/8vs6wxufd0"
end
test "Parse tag as quote" do
# https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md
insert(:user, ap_id: "https://server.example/users/1")
data = File.read!("test/fixtures/quote_post/fep-e232-tag-example.json") |> Jason.decode!()
cng = ArticleNotePageValidator.cast_and_validate(data)
assert cng.valid?
assert cng.changes.quoteUrl == "https://server.example/objects/123"
assert Enum.at(cng.changes.tag, 0).changes == %{
type: "Link",
mediaType: "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
href: "https://server.example/objects/123",
name: "RE: https://server.example/objects/123"
}
end
describe "Note language" do
test "it detects language from JSON-LD context" do
user = insert(:user)
note_activity = %{
"@context" => ["https://www.w3.org/ns/activitystreams", %{"@language" => "pl"}],
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"type" => "Create",
"object" => %{
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"id" => Utils.generate_object_id(),
"type" => "Note",
"content" => "Szczęść Boże",
"attributedTo" => user.ap_id
},
"actor" => user.ap_id
}
{:ok, _create_activity, meta} = ObjectValidator.validate(note_activity, [])
assert meta[:object_data]["language"] == "pl"
end
test "it detects language from contentMap" do
user = insert(:user)
note = %{
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"id" => Utils.generate_object_id(),
"type" => "Note",
"content" => "Szczęść Boże",
"contentMap" => %{
"de" => "Gott segne",
"pl" => "Szczęść Boże"
},
"attributedTo" => user.ap_id
}
{:ok, object} = ArticleNotePageValidator.cast_and_apply(note)
assert object.language == "pl"
end
test "it doesn't call LanguageDetector when language is specified" do
# Set up expectation that detect should not be called
LanguageDetectorMock
|> expect(:detect, 0, fn _ -> flunk("LanguageDetector.detect should not be called") end)
|> stub(:missing_dependencies, fn -> [] end)
|> stub(:configured?, fn -> true end)
# Stub the StaticStubbedConfigMock to return our mock for the provider
StaticStubbedConfigMock
|> stub(:get, fn
[Pleroma.Language.LanguageDetector, :provider] -> LanguageDetectorMock
_other -> nil
end)
user = insert(:user)
note = %{
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"id" => Utils.generate_object_id(),
"type" => "Note",
"content" => "a post in English",
"contentMap" => %{
"en" => "a post in English"
},
"attributedTo" => user.ap_id
}
ArticleNotePageValidator.cast_and_apply(note)
end
test "it adds contentMap if language is specified" do
user = insert(:user)
note = %{
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"id" => Utils.generate_object_id(),
"type" => "Note",
"content" => "тест",
"language" => "uk",
"attributedTo" => user.ap_id
}
{:ok, object} = ArticleNotePageValidator.cast_and_apply(note)
assert object.contentMap == %{
"uk" => "тест"
}
end
end
end
diff --git a/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs b/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs
index c88339d14..347c5b578 100644
--- a/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs
+++ b/test/pleroma/web/activity_pub/object_validators/update_handling_test.exs
@@ -1,168 +1,168 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateHandlingTest do
use Pleroma.DataCase, async: true
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.ObjectValidator
import Pleroma.Factory
describe "updates" do
setup do
user = insert(:user)
object = %{
"id" => user.ap_id,
"name" => "A new name",
"summary" => "A new bio"
}
{:ok, valid_update, []} = Builder.update(user, object)
%{user: user, valid_update: valid_update}
end
test "validates a basic object", %{valid_update: valid_update} do
assert {:ok, _update, []} = ObjectValidator.validate(valid_update, [])
end
test "returns an error if the object can't be updated by the actor", %{
valid_update: valid_update
} do
other_user = insert(:user, local: false)
update =
valid_update
|> Map.put("actor", other_user.ap_id)
assert {:error, _cng} = ObjectValidator.validate(update, [])
end
test "validates as long as the object is same-origin with the actor", %{
valid_update: valid_update
} do
other_user = insert(:user)
update =
valid_update
|> Map.put("actor", other_user.ap_id)
assert {:ok, _update, []} = ObjectValidator.validate(update, [])
end
test "validates if the object is not of an Actor type" do
note = insert(:note)
updated_note = note.data |> Map.put("content", "edited content")
other_user = insert(:user)
{:ok, update, _} = Builder.update(other_user, updated_note)
assert {:ok, _update, _} = ObjectValidator.validate(update, [])
end
end
describe "update note" do
test "converts object into Pleroma's format" do
mastodon_tags = [
%{
"icon" => %{
"mediaType" => "image/png",
"type" => "Image",
"url" => "https://somewhere.org/emoji/url/1.png"
},
"id" => "https://somewhere.org/emoji/1",
"name" => ":some_emoji:",
"type" => "Emoji",
"updated" => "2021-04-07T11:00:00Z"
}
]
user = insert(:user)
note = insert(:note, user: user)
updated_note =
note.data
|> Map.put("content", "edited content")
|> Map.put("tag", mastodon_tags)
{:ok, update, _} = Builder.update(user, updated_note)
assert {:ok, _update, meta} = ObjectValidator.validate(update, [])
assert %{"emoji" => %{"some_emoji" => "https://somewhere.org/emoji/url/1.png"}} =
meta[:object_data]
end
test "returns no object_data in meta for a local Update" do
user = insert(:user)
note = insert(:note, user: user)
updated_note =
note.data
|> Map.put("content", "edited content")
{:ok, update, _} = Builder.update(user, updated_note)
assert {:ok, _update, meta} = ObjectValidator.validate(update, local: true)
assert is_nil(meta[:object_data])
end
test "returns object_data in meta for a remote Update" do
user = insert(:user)
note = insert(:note, user: user)
updated_note =
note.data
|> Map.put("content", "edited content")
{:ok, update, _} = Builder.update(user, updated_note)
assert {:ok, _update, meta} = ObjectValidator.validate(update, local: false)
assert meta[:object_data]
assert {:ok, _update, meta} = ObjectValidator.validate(update, [])
assert meta[:object_data]
end
end
describe "update with history" do
setup do
user = insert(:user)
{:ok, activity} = Pleroma.Web.CommonAPI.post(user, %{status: "mew mew :dinosaur:"})
{:ok, edit} = Pleroma.Web.CommonAPI.update(activity, user, %{status: "edited :blank:"})
- {:ok, external_rep} = Pleroma.Web.ActivityPub.Transmogrifier.prepare_outgoing(edit.data)
+ {:ok, external_rep} = Pleroma.Web.ActivityPub.Transmogrifier.prepare_activity(edit.data)
%{external_rep: external_rep}
end
test "edited note", %{external_rep: external_rep} do
{:ok, _validate_res, meta} = ObjectValidator.validate(external_rep, [])
assert %{"formerRepresentations" => %{"orderedItems" => [%{"emoji" => %{"dinosaur" => _}}]}} =
meta[:object_data]
end
test "edited note, badly-formed formerRepresentations", %{external_rep: external_rep} do
external_rep = put_in(external_rep, ["object", "formerRepresentations"], %{})
assert {:error, _} = ObjectValidator.validate(external_rep, [])
end
test "edited note, badly-formed history item", %{external_rep: external_rep} do
history_item =
Enum.at(external_rep["object"]["formerRepresentations"]["orderedItems"], 0)
|> Map.put("type", "Foo")
external_rep =
put_in(
external_rep,
["object", "formerRepresentations", "orderedItems"],
[history_item]
)
assert {:error, _} = ObjectValidator.validate(external_rep, [])
end
end
end
diff --git a/test/pleroma/web/activity_pub/publisher_test.exs b/test/pleroma/web/activity_pub/publisher_test.exs
index c0908655a..447ffda1b 100644
--- a/test/pleroma/web/activity_pub/publisher_test.exs
+++ b/test/pleroma/web/activity_pub/publisher_test.exs
@@ -1,605 +1,605 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.PublisherTest do
use Oban.Testing, repo: Pleroma.Repo
use Pleroma.Web.ConnCase
import Pleroma.Factory
import Tesla.Mock
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Tests.ObanHelpers
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Publisher
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
@as_public "https://www.w3.org/ns/activitystreams#Public"
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
setup_all do: clear_config([:instance, :federating], true)
describe "should_federate?/1" do
test "it returns false when the inbox is nil" do
refute Publisher.should_federate?(nil, false)
refute Publisher.should_federate?(nil, true)
end
test "it returns true when public is true" do
assert Publisher.should_federate?(false, true)
end
end
describe "gather_webfinger_links/1" do
test "it returns links" do
user = insert(:user)
expected_links = [
%{"href" => user.ap_id, "rel" => "self", "type" => "application/activity+json"},
%{
"href" => user.ap_id,
"rel" => "self",
"type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
},
%{
"rel" => "http://ostatus.org/schema/1.0/subscribe",
"template" => "#{Pleroma.Web.Endpoint.url()}/ostatus_subscribe?acct={uri}"
}
]
assert expected_links == Publisher.gather_webfinger_links(user)
end
end
describe "determine_inbox/2" do
test "it returns sharedInbox for messages involving as:Public in to" do
user = insert(:user, %{shared_inbox: "http://example.com/inbox"})
activity = %Activity{
data: %{"to" => [@as_public], "cc" => [user.follower_address]}
}
assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox"
end
test "it returns sharedInbox for messages involving as:Public in cc" do
user = insert(:user, %{shared_inbox: "http://example.com/inbox"})
activity = %Activity{
data: %{"cc" => [@as_public], "to" => [user.follower_address]}
}
assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox"
end
test "it returns sharedInbox for messages involving multiple recipients in to" do
user = insert(:user, %{shared_inbox: "http://example.com/inbox"})
user_two = insert(:user)
user_three = insert(:user)
activity = %Activity{
data: %{"cc" => [], "to" => [user.ap_id, user_two.ap_id, user_three.ap_id]}
}
assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox"
end
test "it returns sharedInbox for messages involving multiple recipients in cc" do
user = insert(:user, %{shared_inbox: "http://example.com/inbox"})
user_two = insert(:user)
user_three = insert(:user)
activity = %Activity{
data: %{"to" => [], "cc" => [user.ap_id, user_two.ap_id, user_three.ap_id]}
}
assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox"
end
test "it returns sharedInbox for messages involving multiple recipients in total" do
user =
insert(:user, %{
shared_inbox: "http://example.com/inbox",
inbox: "http://example.com/personal-inbox"
})
user_two = insert(:user)
activity = %Activity{
data: %{"to" => [user_two.ap_id], "cc" => [user.ap_id]}
}
assert Publisher.determine_inbox(activity, user) == "http://example.com/inbox"
end
test "it returns inbox for messages involving single recipients in total" do
user =
insert(:user, %{
shared_inbox: "http://example.com/inbox",
inbox: "http://example.com/personal-inbox"
})
activity = %Activity{
data: %{"to" => [user.ap_id], "cc" => []}
}
assert Publisher.determine_inbox(activity, user) == "http://example.com/personal-inbox"
end
end
describe "publish_one/1" do
test "publish to url with with different ports" do
inbox80 = "http://42.site/users/nick1/inbox"
inbox42 = "http://42.site:42/users/nick1/inbox"
activity = insert(:note_activity)
mock(fn
%{method: :post, url: "http://42.site:42/users/nick1/inbox"} ->
{:ok, %Tesla.Env{status: 200, body: "port 42"}}
%{method: :post, url: "http://42.site/users/nick1/inbox"} ->
{:ok, %Tesla.Env{status: 200, body: "port 80"}}
end)
_actor = insert(:user)
assert {:ok, %{body: "port 42"}} =
Publisher.prepare_one(%{
inbox: inbox42,
activity_id: activity.id,
unreachable_since: true
})
|> Publisher.publish_one()
assert {:ok, %{body: "port 80"}} =
Publisher.prepare_one(%{
inbox: inbox80,
activity_id: activity.id,
unreachable_since: true
})
|> Publisher.publish_one()
end
end
describe "publish/2" do
test "doesn't publish a non-public activity to quarantined instances." do
Config.put([:instance, :quarantined_instances], [{"domain.com", "some reason"}])
follower =
insert(:user, %{
local: false,
inbox: "https://domain.com/users/nick1/inbox"
})
actor = insert(:user, follower_address: follower.ap_id)
{:ok, follower, actor} = Pleroma.User.follow(follower, actor)
actor = refresh_record(actor)
note_activity =
insert(:followers_only_note_activity,
user: actor,
recipients: [follower.ap_id]
)
res = Publisher.publish(actor, note_activity)
assert res == :ok
refute_enqueued(
worker: "Pleroma.Workers.PublisherWorker",
args: %{
"params" => %{
inbox: "https://domain.com/users/nick1/inbox",
activity_id: note_activity.id
}
}
)
end
test "Publishes a non-public activity to non-quarantined instances." do
Config.put([:instance, :quarantined_instances], [{"somedomain.com", "some reason"}])
follower =
insert(:user, %{
local: false,
inbox: "https://domain.com/users/nick1/inbox"
})
actor = insert(:user, follower_address: follower.ap_id)
{:ok, follower, actor} = Pleroma.User.follow(follower, actor)
actor = refresh_record(actor)
note_activity =
insert(:followers_only_note_activity,
user: actor,
recipients: [follower.ap_id]
)
res = Publisher.publish(actor, note_activity)
assert res == :ok
assert_enqueued(
worker: "Pleroma.Workers.PublisherWorker",
args: %{
"params" => %{
inbox: "https://domain.com/users/nick1/inbox",
activity_id: note_activity.id
}
},
priority: 1
)
end
test "Publishes to directly addressed actors with higher priority." do
note_activity = insert(:direct_note_activity)
actor = Pleroma.User.get_by_ap_id(note_activity.data["actor"])
res = Publisher.publish(actor, note_activity)
assert res == :ok
assert_enqueued(
worker: "Pleroma.Workers.PublisherWorker",
args: %{
"op" => "publish_one",
"params" => %{"activity_id" => note_activity.id}
},
priority: 0
)
end
- test "Publishes with the new actor if prepare_outgoing changes the actor." do
+ test "Publishes with the new actor if prepare_activity changes the actor." do
mock(fn
%{method: :post, url: "https://domain.com/users/nick1/inbox", body: body} ->
{:ok, %Tesla.Env{status: 200, body: body}}
end)
other_user =
insert(:user, %{
local: false,
inbox: "https://domain.com/users/nick1/inbox"
})
actor = insert(:user)
replaced_actor = insert(:user)
note_activity =
insert(:note_activity,
user: actor,
data_attrs: %{"to" => [other_user.ap_id]}
)
Pleroma.Web.ActivityPub.TransmogrifierMock
- |> Mox.expect(:prepare_outgoing, fn data ->
+ |> Mox.expect(:prepare_activity, fn data ->
{:ok, Map.put(data, "actor", replaced_actor.ap_id)}
end)
prepared =
Publisher.prepare_one(%{
inbox: "https://domain.com/users/nick1/inbox",
activity_id: note_activity.id,
cc: ["https://domain.com/users/nick2/inbox"]
})
{:ok, decoded} = Jason.decode(prepared.json)
assert decoded["actor"] == replaced_actor.ap_id
{:ok, published} = Publisher.publish_one(prepared)
sent_activity = Jason.decode!(published.body)
assert sent_activity["actor"] == replaced_actor.ap_id
end
test "publishes an activity with BCC to all relevant peers." do
follower =
insert(:user, %{
local: false,
inbox: "https://domain.com/users/nick1/inbox"
})
actor = insert(:user, follower_address: follower.ap_id)
user = insert(:user)
{:ok, follower, actor} = Pleroma.User.follow(follower, actor)
note_activity =
insert(:note_activity,
recipients: [follower.ap_id],
data_attrs: %{"bcc" => [user.ap_id]}
)
res = Publisher.publish(actor, note_activity)
assert res == :ok
assert_enqueued(
worker: "Pleroma.Workers.PublisherWorker",
args: %{
"params" => %{
inbox: "https://domain.com/users/nick1/inbox",
activity_id: note_activity.id
}
}
)
end
test "activity with BCC is published to a list member." do
actor = insert(:user)
{:ok, list} = Pleroma.List.create(%{title: "list"}, actor)
list_member = insert(:user, %{local: false})
Pleroma.List.follow(list, list_member)
note_activity =
insert(:note_activity,
# recipients: [follower.ap_id],
data_attrs: %{"bcc" => [list.ap_id]}
)
res = Publisher.publish(actor, note_activity)
assert res == :ok
assert_enqueued(
worker: "Pleroma.Workers.PublisherWorker",
args: %{
"params" => %{
inbox: list_member.inbox,
activity_id: note_activity.id
}
}
)
end
test "publishes a delete activity to peers who signed fetch requests to the create acitvity/object." do
fetcher =
insert(:user,
local: false,
inbox: "https://domain.com/users/nick1/inbox"
)
another_fetcher =
insert(:user,
local: false,
inbox: "https://domain2.com/users/nick1/inbox"
)
actor = insert(:user)
note_activity = insert(:note_activity, user: actor)
object = Object.normalize(note_activity, fetch: false)
activity_path = String.trim_leading(note_activity.data["id"], Pleroma.Web.Endpoint.url())
object_path = String.trim_leading(object.data["id"], Pleroma.Web.Endpoint.url())
build_conn()
|> put_req_header("accept", "application/activity+json")
|> assign(:user, fetcher)
|> get(object_path)
|> json_response(200)
build_conn()
|> put_req_header("accept", "application/activity+json")
|> assign(:user, another_fetcher)
|> get(activity_path)
|> json_response(200)
{:ok, delete} = CommonAPI.delete(note_activity.id, actor)
res = Publisher.publish(actor, delete)
assert res == :ok
assert_enqueued(
worker: "Pleroma.Workers.PublisherWorker",
args: %{
"params" => %{
inbox: "https://domain.com/users/nick1/inbox",
activity_id: delete.id
}
},
priority: 1
)
assert_enqueued(
worker: "Pleroma.Workers.PublisherWorker",
args: %{
"params" => %{
inbox: "https://domain2.com/users/nick1/inbox",
activity_id: delete.id
}
},
priority: 1
)
end
end
test "cc in prepared json for a follow request is an empty list" do
user = insert(:user)
remote_user = insert(:user, local: false)
{:ok, _, _, activity} = CommonAPI.follow(remote_user, user)
assert_enqueued(
worker: "Pleroma.Workers.PublisherWorker",
args: %{
"activity_id" => activity.id,
"op" => "publish"
}
)
ObanHelpers.perform_all()
expected_params =
%{
"activity_id" => activity.id,
"inbox" => remote_user.inbox,
"unreachable_since" => nil
}
assert_enqueued(
worker: "Pleroma.Workers.PublisherWorker",
args: %{
"op" => "publish_one",
"params" => expected_params
}
)
# params need to be atom keys for Publisher.prepare_one.
# this is done in the Oban job.
expected_params = Map.new(expected_params, fn {k, v} -> {String.to_atom(k), v} end)
%{json: json} = Publisher.prepare_one(expected_params)
{:ok, decoded} = Jason.decode(json)
assert decoded["cc"] == []
end
test "unlisted activities retain public address in cc" do
user = insert(:user)
# simulate unlistd activity by only having
# public address in cc
activity =
insert(:note_activity,
user: user,
data_attrs: %{
"cc" => [@as_public],
"to" => [user.follower_address]
}
)
assert @as_public in activity.data["cc"]
prepared =
Publisher.prepare_one(%{
inbox: "https://remote.instance/users/someone/inbox",
activity_id: activity.id
})
{:ok, decoded} = Jason.decode(prepared.json)
assert @as_public in decoded["cc"]
# maybe we also have another inbox in cc
# during Publishing
activity =
insert(:note_activity,
user: user,
data_attrs: %{
"cc" => [@as_public],
"to" => [user.follower_address]
}
)
prepared =
Publisher.prepare_one(%{
inbox: "https://remote.instance/users/someone/inbox",
activity_id: activity.id,
cc: ["https://remote.instance/users/someone_else/inbox"]
})
{:ok, decoded} = Jason.decode(prepared.json)
assert decoded["cc"] == [@as_public, "https://remote.instance/users/someone_else/inbox"]
end
test "public address in cc parameter is preserved" do
user = insert(:user)
cc_with_public = [@as_public, "https://example.org/users/other"]
activity =
insert(:note_activity,
user: user,
data_attrs: %{
"cc" => cc_with_public,
"to" => [user.follower_address]
}
)
assert @as_public in activity.data["cc"]
prepared =
Publisher.prepare_one(%{
inbox: "https://remote.instance/users/someone/inbox",
activity_id: activity.id,
cc: cc_with_public
})
{:ok, decoded} = Jason.decode(prepared.json)
assert cc_with_public == decoded["cc"]
end
test "cc parameter is preserved" do
user = insert(:user)
activity =
insert(:note_activity,
user: user,
data_attrs: %{
"cc" => ["https://example.com/specific/user"],
"to" => [user.follower_address]
}
)
prepared =
Publisher.prepare_one(%{
inbox: "https://remote.instance/users/someone/inbox",
activity_id: activity.id,
cc: ["https://example.com/specific/user"]
})
{:ok, decoded} = Jason.decode(prepared.json)
assert decoded["cc"] == ["https://example.com/specific/user"]
end
describe "prepare_one/1 with reporter anonymization" do
test "signs with the anonymized actor keys when Transmogrifier changes actor" do
Pleroma.SignatureMock
|> Mox.stub(:signed_date, fn -> Pleroma.Signature.signed_date() end)
|> Mox.expect(:sign, fn %Pleroma.User{} = user, _headers ->
send(self(), {:signed_as, user.ap_id})
"TESTSIG"
end)
placeholder = insert(:user)
reporter = insert(:user)
target_account = insert(:user)
clear_config([:activitypub, :anonymize_reporter], true)
clear_config([:activitypub, :anonymize_reporter_local_nickname], placeholder.nickname)
{:ok, reported} = CommonAPI.post(target_account, %{status: "content"})
context = Utils.generate_context_id()
{:ok, activity} =
ActivityPub.flag(%{
actor: reporter,
context: context,
account: target_account,
statuses: [reported],
content: "reason"
})
_prepared =
Publisher.prepare_one(%{
inbox: "http://remote.example/users/alice/inbox",
activity_id: activity.id
})
assert_received {:signed_as, ap_id}
assert ap_id == placeholder.ap_id
end
end
end
diff --git a/test/pleroma/web/activity_pub/transmogrifier/answer_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/answer_handling_test.exs
index 39a1598a8..913d143a5 100644
--- a/test/pleroma/web/activity_pub/transmogrifier/answer_handling_test.exs
+++ b/test/pleroma/web/activity_pub/transmogrifier/answer_handling_test.exs
@@ -1,79 +1,79 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Transmogrifier.AnswerHandlingTest do
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.CommonAPI
import Pleroma.Factory
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
test "incoming, rewrites Note to Answer and increments vote counters" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "suya...",
poll: %{options: ["suya", "suya.", "suya.."], expires_in: 10}
})
object = Object.normalize(activity, fetch: false)
assert object.data["repliesCount"] == nil
data =
File.read!("test/fixtures/mastodon-vote.json")
|> Jason.decode!()
|> Kernel.put_in(["to"], user.ap_id)
|> Kernel.put_in(["object", "inReplyTo"], object.data["id"])
|> Kernel.put_in(["object", "to"], user.ap_id)
{:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
answer_object = Object.normalize(activity, fetch: false)
assert answer_object.data["type"] == "Answer"
assert answer_object.data["inReplyTo"] == object.data["id"]
new_object = Object.get_by_ap_id(object.data["id"])
assert new_object.data["repliesCount"] == nil
assert Enum.any?(
new_object.data["oneOf"],
fn
%{"name" => "suya..", "replies" => %{"totalItems" => 1}} -> true
_ -> false
end
)
end
test "outgoing, rewrites Answer to Note" do
user = insert(:user)
{:ok, poll_activity} =
CommonAPI.post(user, %{
status: "suya...",
poll: %{options: ["suya", "suya.", "suya.."], expires_in: 10}
})
poll_object = Object.normalize(poll_activity, fetch: false)
# TODO: Replace with CommonAPI vote creation when implemented
data =
File.read!("test/fixtures/mastodon-vote.json")
|> Jason.decode!()
|> Kernel.put_in(["to"], user.ap_id)
|> Kernel.put_in(["object", "inReplyTo"], poll_object.data["id"])
|> Kernel.put_in(["object", "to"], user.ap_id)
{:ok, %Activity{local: false} = activity} = Transmogrifier.handle_incoming(data)
- {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
+ {:ok, data} = Transmogrifier.prepare_activity(activity.data)
assert data["object"]["type"] == "Note"
end
end
diff --git a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs
index 403c98a2d..31b9a699d 100644
--- a/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs
+++ b/test/pleroma/web/activity_pub/transmogrifier/note_handling_test.exs
@@ -1,796 +1,796 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Transmogrifier.NoteHandlingTest do
use Oban.Testing, repo: Pleroma.Repo
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
import Mock
import Pleroma.Factory
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
setup do: clear_config([:instance, :max_remote_account_fields])
describe "handle_incoming" do
test "it works for incoming notices with tag not being an array (kroeg)" do
data = File.read!("test/fixtures/kroeg-array-less-emoji.json") |> Jason.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
object = Object.normalize(data["object"], fetch: false)
assert object.data["emoji"] == %{
"icon_e_smile" => "https://puckipedia.com/forum/images/smilies/icon_e_smile.png"
}
data = File.read!("test/fixtures/kroeg-array-less-hashtag.json") |> Jason.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
object = Object.normalize(data["object"], fetch: false)
assert "test" in Object.tags(object)
assert Object.hashtags(object) == ["test"]
end
test "it ignores an incoming notice if we already have it" do
activity = insert(:note_activity)
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Jason.decode!()
|> Map.put("object", Object.normalize(activity, fetch: false).data)
{:ok, returned_activity} = Transmogrifier.handle_incoming(data)
assert activity == returned_activity
end
test "it fetches reply-to activities if we don't have them" do
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Jason.decode!()
object =
data["object"]
|> Map.put("inReplyTo", "https://mstdn.io/users/mayuutann/statuses/99568293732299394")
data = Map.put(data, "object", object)
{:ok, returned_activity} = Transmogrifier.handle_incoming(data)
returned_object = Object.normalize(returned_activity, fetch: false)
assert %Activity{} =
Activity.get_create_by_object_ap_id(
"https://mstdn.io/users/mayuutann/statuses/99568293732299394"
)
assert returned_object.data["inReplyTo"] ==
"https://mstdn.io/users/mayuutann/statuses/99568293732299394"
end
test "it does not fetch reply-to activities beyond max replies depth limit" do
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Jason.decode!()
object =
data["object"]
|> Map.put("inReplyTo", "https://shitposter.club/notice/2827873")
data = Map.put(data, "object", object)
with_mock Pleroma.Web.Federator,
allowed_thread_distance?: fn _ -> false end do
{:ok, returned_activity} = Transmogrifier.handle_incoming(data)
returned_object = Object.normalize(returned_activity, fetch: false)
refute Activity.get_create_by_object_ap_id(
"tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
)
assert returned_object.data["inReplyTo"] == "https://shitposter.club/notice/2827873"
end
end
test "it does not crash if the object in inReplyTo can't be fetched" do
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Jason.decode!()
object =
data["object"]
|> Map.put("inReplyTo", "https://404.site/whatever")
data =
data
|> Map.put("object", object)
assert {:ok, _returned_activity} = Transmogrifier.handle_incoming(data)
end
test "it does not work for deactivated users" do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
insert(:user, ap_id: data["actor"], is_active: false)
assert {:error, _} = Transmogrifier.handle_incoming(data)
end
test "it works for incoming notices" do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["id"] ==
"http://mastodon.example.org/users/admin/statuses/99512778738411822/activity"
assert data["context"] ==
"tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation"
assert data["to"] == ["https://www.w3.org/ns/activitystreams#Public"]
assert data["cc"] == [
"http://localtesting.pleroma.lol/users/lain",
"http://mastodon.example.org/users/admin/followers"
]
assert data["actor"] == "http://mastodon.example.org/users/admin"
object_data = Object.normalize(data["object"], fetch: false).data
assert object_data["id"] ==
"http://mastodon.example.org/users/admin/statuses/99512778738411822"
assert object_data["to"] == ["https://www.w3.org/ns/activitystreams#Public"]
assert object_data["cc"] == [
"http://localtesting.pleroma.lol/users/lain",
"http://mastodon.example.org/users/admin/followers"
]
assert object_data["actor"] == "http://mastodon.example.org/users/admin"
assert object_data["attributedTo"] == "http://mastodon.example.org/users/admin"
assert object_data["context"] ==
"tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation"
assert object_data["sensitive"] == true
user = User.get_cached_by_ap_id(object_data["actor"])
assert user.note_count == 1
end
test "it works for incoming notices without the sensitive property but an nsfw hashtag" do
data = File.read!("test/fixtures/mastodon-post-activity-nsfw.json") |> Jason.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
object_data = Object.normalize(data["object"], fetch: false).data
assert object_data["sensitive"] == true
end
test "it works for incoming notices with hashtags" do
data = File.read!("test/fixtures/mastodon-post-activity-hashtag.json") |> Jason.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
object = Object.normalize(data["object"], fetch: false)
assert match?(
%{
"href" => "http://localtesting.pleroma.lol/users/lain",
"name" => "@lain@localtesting.pleroma.lol",
"type" => "Mention"
},
Enum.at(object.data["tag"], 0)
)
assert match?(
%{
"href" => "http://mastodon.example.org/tags/moo",
"name" => "#moo",
"type" => "Hashtag"
},
Enum.at(object.data["tag"], 1)
)
assert "moo" == Enum.at(object.data["tag"], 2)
end
test "it works for incoming notices with contentMap" do
data = File.read!("test/fixtures/mastodon-post-activity-contentmap.json") |> Jason.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
object = Object.normalize(data["object"], fetch: false)
assert object.data["content"] ==
"<p><span class=\"h-card\"><a href=\"http://localtesting.pleroma.lol/users/lain\" class=\"u-url mention\">@<span>lain</span></a></span></p>"
end
test "it only uses contentMap if content is not present" do
user = insert(:user)
message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"type" => "Create",
"object" => %{
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"id" => Utils.generate_object_id(),
"type" => "Note",
"content" => "Hi",
"contentMap" => %{
"de" => "Hallo",
"uk" => "Привіт"
},
"inReplyTo" => nil,
"attributedTo" => user.ap_id
},
"actor" => user.ap_id
}
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(message)
object = Object.normalize(data["object"], fetch: false)
assert object.data["content"] == "Hi"
end
test "it works for incoming notices with a nil contentMap (firefish)" do
data =
File.read!("test/fixtures/mastodon-post-activity-contentmap.json")
|> Jason.decode!()
|> Map.put("contentMap", nil)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
object = Object.normalize(data["object"], fetch: false)
assert object.data["content"] ==
"<p><span class=\"h-card\"><a href=\"http://localtesting.pleroma.lol/users/lain\" class=\"u-url mention\">@<span>lain</span></a></span></p>"
end
test "it works for incoming notices with to/cc not being an array (kroeg)" do
data = File.read!("test/fixtures/kroeg-post-activity.json") |> Jason.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
object = Object.normalize(data["object"], fetch: false)
assert object.data["content"] ==
"<p>henlo from my Psion netBook</p><p>message sent from my Psion netBook</p>"
end
test "it ensures that as:Public activities make it to their followers collection" do
user = insert(:user)
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Jason.decode!()
|> Map.put("actor", user.ap_id)
|> Map.put("to", ["https://www.w3.org/ns/activitystreams#Public"])
|> Map.put("cc", [])
object =
data["object"]
|> Map.put("attributedTo", user.ap_id)
|> Map.put("to", ["https://www.w3.org/ns/activitystreams#Public"])
|> Map.put("cc", [])
|> Map.put("id", user.ap_id <> "/activities/12345678")
data = Map.put(data, "object", object)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["cc"] == [User.ap_followers(user)]
end
test "it ensures that address fields become lists" do
user = insert(:user)
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Jason.decode!()
|> Map.put("actor", user.ap_id)
|> Map.put("cc", nil)
object =
data["object"]
|> Map.put("attributedTo", user.ap_id)
|> Map.put("cc", nil)
|> Map.put("id", user.ap_id <> "/activities/12345678")
data = Map.put(data, "object", object)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
refute is_nil(data["cc"])
end
test "it strips internal likes" do
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Jason.decode!()
likes = %{
"first" =>
"http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes?page=1",
"id" => "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes",
"totalItems" => 3,
"type" => "OrderedCollection"
}
object = Map.put(data["object"], "likes", likes)
data = Map.put(data, "object", object)
{:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(data)
object = Object.normalize(activity)
assert object.data["likes"] == []
end
test "it strips internal reactions" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "#cofe"})
{:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "📢")
%{object: object} = Activity.get_by_id_with_object(activity.id)
assert Map.has_key?(object.data, "reactions")
assert Map.has_key?(object.data, "reaction_count")
object_data = Transmogrifier.strip_internal_fields(object.data)
refute Map.has_key?(object_data, "reactions")
refute Map.has_key?(object_data, "reaction_count")
end
test "it correctly processes messages with non-array to field" do
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
|> Map.put("to", "https://www.w3.org/ns/activitystreams#Public")
|> put_in(["object", "to"], "https://www.w3.org/ns/activitystreams#Public")
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
assert [
"http://localtesting.pleroma.lol/users/lain",
"http://mastodon.example.org/users/admin/followers"
] == activity.data["cc"]
assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["to"]
end
test "it correctly processes messages with non-array cc field" do
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
|> Map.put("cc", "http://mastodon.example.org/users/admin/followers")
|> put_in(["object", "cc"], "http://mastodon.example.org/users/admin/followers")
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
assert ["http://mastodon.example.org/users/admin/followers"] == activity.data["cc"]
assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["to"]
end
test "it correctly processes messages with weirdness in address fields" do
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
|> Map.put("cc", ["http://mastodon.example.org/users/admin/followers", ["¿"]])
|> put_in(["object", "cc"], ["http://mastodon.example.org/users/admin/followers", ["¿"]])
assert {:ok, activity} = Transmogrifier.handle_incoming(data)
assert ["http://mastodon.example.org/users/admin/followers"] == activity.data["cc"]
assert ["https://www.w3.org/ns/activitystreams#Public"] == activity.data["to"]
end
end
describe "`handle_incoming/2`, Mastodon format `replies` handling" do
setup do: clear_config([:activitypub, :note_replies_output_limit], 5)
setup do: clear_config([:instance, :federation_incoming_replies_max_depth])
setup do
data =
"test/fixtures/mastodon-post-activity.json"
|> File.read!()
|> Jason.decode!()
items = get_in(data, ["object", "replies", "first", "items"])
assert length(items) > 0
%{data: data, items: items}
end
test "schedules background fetching of `replies` items if max thread depth limit allows", %{
data: data,
items: items
} do
clear_config([:instance, :federation_incoming_replies_max_depth], 10)
{:ok, activity} = Transmogrifier.handle_incoming(data)
object = Object.normalize(activity.data["object"])
assert object.data["replies"] == items
for id <- items do
job_args = %{"op" => "fetch_remote", "id" => id, "depth" => 1}
assert_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker, args: job_args)
end
end
test "does NOT schedule background fetching of `replies` beyond max thread depth limit allows",
%{data: data} do
clear_config([:instance, :federation_incoming_replies_max_depth], 0)
{:ok, _activity} = Transmogrifier.handle_incoming(data)
assert all_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker) == []
end
end
describe "`handle_incoming/2`, Pleroma format `replies` handling" do
setup do: clear_config([:activitypub, :note_replies_output_limit], 5)
setup do: clear_config([:instance, :federation_incoming_replies_max_depth])
setup do
replies = %{
"type" => "Collection",
"items" => [Utils.generate_object_id(), Utils.generate_object_id()]
}
activity =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
|> Kernel.put_in(["object", "replies"], replies)
%{activity: activity}
end
test "schedules background fetching of `replies` items if max thread depth limit allows", %{
activity: activity
} do
clear_config([:instance, :federation_incoming_replies_max_depth], 1)
assert {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(activity)
object = Object.normalize(data["object"])
for id <- object.data["replies"] do
job_args = %{"op" => "fetch_remote", "id" => id, "depth" => 1}
assert_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker, args: job_args)
end
end
test "does NOT schedule background fetching of `replies` beyond max thread depth limit allows",
%{activity: activity} do
clear_config([:instance, :federation_incoming_replies_max_depth], 0)
{:ok, _activity} = Transmogrifier.handle_incoming(activity)
assert all_enqueued(worker: Pleroma.Workers.RemoteFetcherWorker) == []
end
end
describe "reserialization" do
test "successfully reserializes a message with inReplyTo == nil" do
user = insert(:user)
message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"type" => "Create",
"object" => %{
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"id" => Utils.generate_object_id(),
"type" => "Note",
"content" => "Hi",
"inReplyTo" => nil,
"attributedTo" => user.ap_id
},
"actor" => user.ap_id
}
{:ok, activity} = Transmogrifier.handle_incoming(message)
- {:ok, _} = Transmogrifier.prepare_outgoing(activity.data)
+ {:ok, _} = Transmogrifier.prepare_activity(activity.data)
end
test "successfully reserializes a message with AS2 objects in IR" do
user = insert(:user)
message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"type" => "Create",
"object" => %{
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"id" => Utils.generate_object_id(),
"type" => "Note",
"content" => "Hi",
"inReplyTo" => nil,
"attributedTo" => user.ap_id,
"tag" => [
%{"name" => "#2hu", "href" => "http://example.com/2hu", "type" => "Hashtag"},
%{"name" => "Bob", "href" => "http://example.com/bob", "type" => "Mention"}
]
},
"actor" => user.ap_id
}
{:ok, activity} = Transmogrifier.handle_incoming(message)
- {:ok, _} = Transmogrifier.prepare_outgoing(activity.data)
+ {:ok, _} = Transmogrifier.prepare_activity(activity.data)
end
end
describe "fix_in_reply_to/2" do
setup do: clear_config([:instance, :federation_incoming_replies_max_depth])
setup do
data = Jason.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
[data: data]
end
test "returns not modified object when has no inReplyTo field", %{data: data} do
assert Transmogrifier.fix_in_reply_to(data) == data
end
test "returns object with inReplyTo when denied incoming reply", %{data: data} do
clear_config([:instance, :federation_incoming_replies_max_depth], 0)
object_with_reply =
Map.put(data["object"], "inReplyTo", "https://shitposter.club/notice/2827873")
modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
assert modified_object["inReplyTo"] == "https://shitposter.club/notice/2827873"
object_with_reply =
Map.put(data["object"], "inReplyTo", %{"id" => "https://shitposter.club/notice/2827873"})
modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
assert modified_object["inReplyTo"] == %{"id" => "https://shitposter.club/notice/2827873"}
object_with_reply =
Map.put(data["object"], "inReplyTo", ["https://shitposter.club/notice/2827873"])
modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
assert modified_object["inReplyTo"] == ["https://shitposter.club/notice/2827873"]
object_with_reply = Map.put(data["object"], "inReplyTo", [])
modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
assert modified_object["inReplyTo"] == []
end
test "returns modified object when allowed incoming reply", %{data: data} do
object_with_reply =
Map.put(
data["object"],
"inReplyTo",
"https://mstdn.io/users/mayuutann/statuses/99568293732299394"
)
clear_config([:instance, :federation_incoming_replies_max_depth], 5)
modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
assert modified_object["inReplyTo"] ==
"https://mstdn.io/users/mayuutann/statuses/99568293732299394"
assert modified_object["context"] ==
"tag:shitposter.club,2018-02-22:objectType=thread:nonce=e5a7c72d60a9c0e4"
end
end
describe "fix_attachments/1" do
test "returns not modified object" do
data = Jason.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
assert Transmogrifier.fix_attachments(data) == data
end
test "returns modified object when attachment is map" do
assert Transmogrifier.fix_attachments(%{
"attachment" => %{
"mediaType" => "video/mp4",
"url" => "https://peertube.moe/stat-480.mp4"
}
}) == %{
"attachment" => [
%{
"mediaType" => "video/mp4",
"type" => "Document",
"url" => [
%{
"href" => "https://peertube.moe/stat-480.mp4",
"mediaType" => "video/mp4",
"type" => "Link"
}
]
}
]
}
end
test "returns modified object when attachment is list" do
assert Transmogrifier.fix_attachments(%{
"attachment" => [
%{"mediaType" => "video/mp4", "url" => "https://pe.er/stat-480.mp4"},
%{"mimeType" => "video/mp4", "href" => "https://pe.er/stat-480.mp4"}
]
}) == %{
"attachment" => [
%{
"mediaType" => "video/mp4",
"type" => "Document",
"url" => [
%{
"href" => "https://pe.er/stat-480.mp4",
"mediaType" => "video/mp4",
"type" => "Link"
}
]
},
%{
"mediaType" => "video/mp4",
"type" => "Document",
"url" => [
%{
"href" => "https://pe.er/stat-480.mp4",
"mediaType" => "video/mp4",
"type" => "Link"
}
]
}
]
}
end
end
describe "fix_emoji/1" do
test "returns not modified object when object not contains tags" do
data = Jason.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
assert Transmogrifier.fix_emoji(data) == data
end
test "returns object with emoji when object contains list tags" do
assert Transmogrifier.fix_emoji(%{
"tag" => [
%{"type" => "Emoji", "name" => ":bib:", "icon" => %{"url" => "/test"}},
%{"type" => "Hashtag"}
]
}) == %{
"emoji" => %{"bib" => "/test"},
"tag" => [
%{"icon" => %{"url" => "/test"}, "name" => ":bib:", "type" => "Emoji"},
%{"type" => "Hashtag"}
]
}
end
test "returns object with emoji when object contains map tag" do
assert Transmogrifier.fix_emoji(%{
"tag" => %{"type" => "Emoji", "name" => ":bib:", "icon" => %{"url" => "/test"}}
}) == %{
"emoji" => %{"bib" => "/test"},
"tag" => %{"icon" => %{"url" => "/test"}, "name" => ":bib:", "type" => "Emoji"}
}
end
end
describe "set_replies/1" do
setup do: clear_config([:activitypub, :note_replies_output_limit], 2)
test "still provides reply collection id even if activity doesn't have replies yet" do
data = Jason.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
object = data["object"] |> Map.delete("replies")
modified = Transmogrifier.set_replies(object)
refute object["replies"]
assert modified["replies"]
assert match?(%{"id" => "http" <> _, "totalItems" => 0}, modified["replies"])
# first page should be omitted if there are no entries anyway
refute modified["replies"]["first"]
end
test "sets `replies` collection with a limited number of replies, preferring oldest" do
[user, another_user] = insert_list(2, :user)
{:ok, %{id: id1} = activity} = CommonAPI.post(user, %{status: "1"})
{:ok, %{id: id2} = self_reply1} =
CommonAPI.post(user, %{status: "self-reply 1", in_reply_to_status_id: id1})
{:ok, self_reply2} =
CommonAPI.post(user, %{status: "self-reply 2", in_reply_to_status_id: id1})
# Assuming to _not_ be present in `replies` due to :note_replies_output_limit is set to 2
{:ok, _} = CommonAPI.post(user, %{status: "self-reply 3", in_reply_to_status_id: id1})
{:ok, _} =
CommonAPI.post(user, %{
status: "self-reply to self-reply",
in_reply_to_status_id: id2
})
{:ok, _} =
CommonAPI.post(another_user, %{
status: "another user's reply",
in_reply_to_status_id: id1
})
object = Object.normalize(activity, fetch: false)
replies_uris = Enum.map([self_reply1, self_reply2], fn a -> a.object.data["id"] end)
assert %{"type" => "OrderedCollection", "first" => %{"orderedItems" => ^replies_uris}} =
Transmogrifier.set_replies(object.data)["replies"]
end
end
test "take_emoji_tags/1" do
user = insert(:user, %{emoji: %{"firefox" => "https://example.org/firefox.png"}})
assert Transmogrifier.take_emoji_tags(user) == [
%{
"icon" => %{"type" => "Image", "url" => "https://example.org/firefox.png"},
"id" => "https://example.org/firefox.png",
"name" => ":firefox:",
"type" => "Emoji",
"updated" => "1970-01-01T00:00:00Z"
}
]
end
test "the standalone note uses its own ID when context is missing" do
insert(:user, ap_id: "https://mk.absturztau.be/users/8ozbzjs3o8")
activity =
"test/fixtures/tesla_mock/mk.absturztau.be-93e7nm8wqg-activity.json"
|> File.read!()
|> Jason.decode!()
{:ok, %Activity{} = modified} = Transmogrifier.handle_incoming(activity)
object = Object.normalize(modified, fetch: false)
assert object.data["context"] == object.data["id"]
assert modified.data["context"] == object.data["id"]
end
test "the reply note uses its parent's ID when context is missing and reply is unreachable" do
insert(:user, ap_id: "https://mk.absturztau.be/users/8ozbzjs3o8")
activity =
"test/fixtures/tesla_mock/mk.absturztau.be-93e7nm8wqg-activity.json"
|> File.read!()
|> Jason.decode!()
object =
activity["object"]
|> Map.put("inReplyTo", "https://404.site/object/went-to-buy-milk")
activity =
activity
|> Map.put("object", object)
{:ok, %Activity{} = modified} = Transmogrifier.handle_incoming(activity)
object = Object.normalize(modified, fetch: false)
assert object.data["context"] == object.data["inReplyTo"]
assert modified.data["context"] == object.data["inReplyTo"]
end
end
diff --git a/test/pleroma/web/activity_pub/transmogrifier_test.exs b/test/pleroma/web/activity_pub/transmogrifier_test.exs
index 1e9be54ea..c1e01557d 100644
--- a/test/pleroma/web/activity_pub/transmogrifier_test.exs
+++ b/test/pleroma/web/activity_pub/transmogrifier_test.exs
@@ -1,1119 +1,1119 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
use Oban.Testing, repo: Pleroma.Repo
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.UserView
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.CommonAPI
import Mock
import Pleroma.Factory
import ExUnit.CaptureLog
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
setup do: clear_config([:instance, :max_remote_account_fields])
describe "handle_incoming" do
test "it works for incoming unfollows with an existing follow" do
user = insert(:user)
follow_data =
File.read!("test/fixtures/mastodon-follow-activity.json")
|> Jason.decode!()
|> Map.put("object", user.ap_id)
{:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(follow_data)
data =
File.read!("test/fixtures/mastodon-unfollow-activity.json")
|> Jason.decode!()
|> Map.put("object", follow_data)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["type"] == "Undo"
assert data["object"]["type"] == "Follow"
assert data["object"]["object"] == user.ap_id
assert data["actor"] == "http://mastodon.example.org/users/admin"
refute User.following?(User.get_cached_by_ap_id(data["actor"]), user)
end
test "it accepts Flag activities" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "test post"})
object = Object.normalize(activity, fetch: false)
note_obj = %{
"type" => "Note",
"id" => activity.object.data["id"],
"content" => "test post",
"published" => object.data["published"],
"actor" => AccountView.render("show.json", %{user: user, skip_visibility_check: true})
}
message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"cc" => [user.ap_id],
"object" => [user.ap_id, activity.data["id"]],
"type" => "Flag",
"content" => "blocked AND reported!!!",
"actor" => other_user.ap_id
}
assert {:ok, activity} = Transmogrifier.handle_incoming(message)
assert activity.data["object"] == [user.ap_id, note_obj]
assert activity.data["content"] == "blocked AND reported!!!"
assert activity.data["actor"] == other_user.ap_id
assert activity.data["cc"] == [user.ap_id]
end
test "it accepts Move activities" do
old_user = insert(:user)
new_user = insert(:user)
message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Move",
"actor" => old_user.ap_id,
"object" => old_user.ap_id,
"target" => new_user.ap_id
}
assert :error = Transmogrifier.handle_incoming(message)
{:ok, _new_user} = User.update_and_set_cache(new_user, %{also_known_as: [old_user.ap_id]})
assert {:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(message)
assert activity.actor == old_user.ap_id
assert activity.data["actor"] == old_user.ap_id
assert activity.data["object"] == old_user.ap_id
assert activity.data["target"] == new_user.ap_id
assert activity.data["type"] == "Move"
end
test "it fixes both the Create and object contexts in a reply" do
insert(:user, ap_id: "https://mk.absturztau.be/users/8ozbzjs3o8")
insert(:user, ap_id: "https://p.helene.moe/users/helene")
create_activity =
"test/fixtures/create-pleroma-reply-to-misskey-thread.json"
|> File.read!()
|> Jason.decode!()
assert {:ok, %Activity{} = activity} = Transmogrifier.handle_incoming(create_activity)
object = Object.normalize(activity, fetch: false)
assert activity.data["context"] == object.data["context"]
end
test "it fixes the public scope addressing" do
insert(:user,
ap_id: "https://mymath.rocks/endpoints/SYn3cl_N4HAPfPHgo2x37XunLEmhV9LnxCggcYwyec0"
)
object =
"test/fixtures/bovine-bogus-public-note.json"
|> File.read!()
|> Jason.decode!()
message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Create",
"actor" => "https://mymath.rocks/endpoints/SYn3cl_N4HAPfPHgo2x37XunLEmhV9LnxCggcYwyec0",
"object" => object
}
assert {:ok, activity} = Transmogrifier.handle_incoming(message)
object = Object.normalize(activity, fetch: false)
assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"]
assert "https://www.w3.org/ns/activitystreams#Public" in object.data["to"]
end
test "it keeps link tags" do
insert(:user, ap_id: "https://example.org/users/alice")
message = File.read!("test/fixtures/fep-e232.json") |> Jason.decode!()
assert capture_log(fn ->
assert {:ok, activity} = Transmogrifier.handle_incoming(message)
object = Object.normalize(activity)
assert [%{"type" => "Mention"}, %{"type" => "Link"}] = object.data["tag"]
end) =~ "Object rejected while fetching"
end
test "it accepts quote posts" do
insert(:user, ap_id: "https://misskey.io/users/7rkrarq81i")
object = File.read!("test/fixtures/quote_post/misskey_quote_post.json") |> Jason.decode!()
message = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"type" => "Create",
"actor" => "https://misskey.io/users/7rkrarq81i",
"object" => object
}
assert {:ok, activity} = Transmogrifier.handle_incoming(message)
# Object was created in the database
object = Object.normalize(activity)
assert object.data["quoteUrl"] == "https://misskey.io/notes/8vs6wxufd0"
# It fetched the quoted post
assert Object.normalize("https://misskey.io/notes/8vs6wxufd0")
end
test "doesn't allow remote edits to fake local likes" do
# as a spot check for no internal fields getting injected
now = DateTime.utc_now()
pub_date = DateTime.to_iso8601(Timex.subtract(now, Timex.Duration.from_minutes(3)))
edit_date = DateTime.to_iso8601(now)
local_user = insert(:user)
create_data = %{
"type" => "Create",
"id" => "http://mastodon.example.org/users/admin/statuses/2619539638/activity",
"actor" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => "http://mastodon.example.org/users/admin/statuses/2619539638",
"attributedTo" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"published" => pub_date,
"content" => "miaow",
"likes" => [local_user.ap_id]
}
}
update_data =
create_data
|> Map.put("type", "Update")
|> Map.put("id", create_data["object"]["id"] <> "/update/1")
|> put_in(["object", "content"], "miaow :3")
|> put_in(["object", "updated"], edit_date)
|> put_in(["object", "formerRepresentations"], %{
"type" => "OrderedCollection",
"totalItems" => 1,
"orderedItems" => [create_data["object"]]
})
{:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(create_data)
%Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"])
assert object.data["content"] == "miaow"
assert object.data["likes"] == []
assert object.data["like_count"] == 0
{:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(update_data)
%Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"]["id"])
assert object.data["content"] == "miaow :3"
assert object.data["likes"] == []
assert object.data["like_count"] == 0
end
test "strips internal fields from history items in edited notes" do
now = DateTime.utc_now()
pub_date = DateTime.to_iso8601(Timex.subtract(now, Timex.Duration.from_minutes(3)))
edit_date = DateTime.to_iso8601(now)
local_user = insert(:user)
create_data = %{
"type" => "Create",
"id" => "http://mastodon.example.org/users/admin/statuses/2619539638/activity",
"actor" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => "http://mastodon.example.org/users/admin/statuses/2619539638",
"attributedTo" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"published" => pub_date,
"content" => "miaow",
"likes" => [],
"like_count" => 0
}
}
update_data =
create_data
|> Map.put("type", "Update")
|> Map.put("id", create_data["object"]["id"] <> "/update/1")
|> put_in(["object", "content"], "miaow :3")
|> put_in(["object", "updated"], edit_date)
|> put_in(["object", "formerRepresentations"], %{
"type" => "OrderedCollection",
"totalItems" => 1,
"orderedItems" => [
Map.merge(create_data["object"], %{
"likes" => [local_user.ap_id],
"like_count" => 1,
"pleroma" => %{"internal_field" => "should_be_stripped"}
})
]
})
{:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(create_data)
%Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"])
assert object.data["content"] == "miaow"
assert object.data["likes"] == []
assert object.data["like_count"] == 0
{:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(update_data)
%Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"]["id"])
assert object.data["content"] == "miaow :3"
assert object.data["likes"] == []
assert object.data["like_count"] == 0
# Check that internal fields are stripped from history items
history_item = List.first(object.data["formerRepresentations"]["orderedItems"])
assert history_item["likes"] == []
assert history_item["like_count"] == 0
refute Map.has_key?(history_item, "pleroma")
end
test "doesn't trip over remote likes in notes" do
now = DateTime.utc_now()
pub_date = DateTime.to_iso8601(Timex.subtract(now, Timex.Duration.from_minutes(3)))
edit_date = DateTime.to_iso8601(now)
create_data = %{
"type" => "Create",
"id" => "http://mastodon.example.org/users/admin/statuses/3409297097/activity",
"actor" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Note",
"id" => "http://mastodon.example.org/users/admin/statuses/3409297097",
"attributedTo" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"published" => pub_date,
"content" => "miaow",
"likes" => %{
"id" => "http://mastodon.example.org/users/admin/statuses/3409297097/likes",
"totalItems" => 0,
"type" => "Collection"
}
}
}
update_data =
create_data
|> Map.put("type", "Update")
|> Map.put("id", create_data["object"]["id"] <> "/update/1")
|> put_in(["object", "content"], "miaow :3")
|> put_in(["object", "updated"], edit_date)
|> put_in(["object", "likes", "totalItems"], 666)
|> put_in(["object", "formerRepresentations"], %{
"type" => "OrderedCollection",
"totalItems" => 1,
"orderedItems" => [create_data["object"]]
})
{:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(create_data)
%Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"])
assert object.data["content"] == "miaow"
assert object.data["likes"] == []
assert object.data["like_count"] == 0
{:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(update_data)
%Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"]["id"])
assert object.data["content"] == "miaow :3"
assert object.data["likes"] == []
# in the future this should retain remote likes, but for now:
assert object.data["like_count"] == 0
end
test "doesn't trip over remote likes in polls" do
now = DateTime.utc_now()
pub_date = DateTime.to_iso8601(Timex.subtract(now, Timex.Duration.from_minutes(3)))
edit_date = DateTime.to_iso8601(now)
create_data = %{
"type" => "Create",
"id" => "http://mastodon.example.org/users/admin/statuses/2471790073/activity",
"actor" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"object" => %{
"type" => "Question",
"id" => "http://mastodon.example.org/users/admin/statuses/2471790073",
"attributedTo" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"cc" => [],
"published" => pub_date,
"content" => "vote!",
"anyOf" => [
%{
"type" => "Note",
"name" => "a",
"replies" => %{
"type" => "Collection",
"totalItems" => 3
}
},
%{
"type" => "Note",
"name" => "b",
"replies" => %{
"type" => "Collection",
"totalItems" => 1
}
}
],
"likes" => %{
"id" => "http://mastodon.example.org/users/admin/statuses/2471790073/likes",
"totalItems" => 0,
"type" => "Collection"
}
}
}
update_data =
create_data
|> Map.put("type", "Update")
|> Map.put("id", create_data["object"]["id"] <> "/update/1")
|> put_in(["object", "content"], "vote now!")
|> put_in(["object", "updated"], edit_date)
|> put_in(["object", "likes", "totalItems"], 666)
|> put_in(["object", "formerRepresentations"], %{
"type" => "OrderedCollection",
"totalItems" => 1,
"orderedItems" => [create_data["object"]]
})
{:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(create_data)
%Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"])
assert object.data["content"] == "vote!"
assert object.data["likes"] == []
assert object.data["like_count"] == 0
{:ok, %Pleroma.Activity{} = activity} = Transmogrifier.handle_incoming(update_data)
%Pleroma.Object{} = object = Object.get_by_ap_id(activity.data["object"]["id"])
assert object.data["content"] == "vote now!"
assert object.data["likes"] == []
# in the future this should retain remote likes, but for now:
assert object.data["like_count"] == 0
end
end
describe "prepare outgoing" do
test "it inlines private announced objects" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "hey", visibility: "private"})
{:ok, announce_activity} = CommonAPI.repeat(activity.id, user)
- {:ok, modified} = Transmogrifier.prepare_outgoing(announce_activity.data)
+ {:ok, modified} = Transmogrifier.prepare_activity(announce_activity.data)
assert modified["object"]["content"] == "hey"
assert modified["object"]["actor"] == modified["object"]["attributedTo"]
end
test "it turns mentions into tags" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{status: "hey, @#{other_user.nickname}, how are ya? #2hu"})
with_mock Pleroma.Notification,
get_notified_from_activity: fn _, _ -> [] end do
- {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
+ {:ok, modified} = Transmogrifier.prepare_activity(activity.data)
object = modified["object"]
expected_mention = %{
"href" => other_user.ap_id,
"name" => "@#{other_user.nickname}",
"type" => "Mention"
}
expected_tag = %{
"href" => Pleroma.Web.Endpoint.url() <> "/tags/2hu",
"type" => "Hashtag",
"name" => "#2hu"
}
refute called(Pleroma.Notification.get_notified_from_activity(:_, :_))
assert Enum.member?(object["tag"], expected_tag)
assert Enum.member?(object["tag"], expected_mention)
end
end
test "it adds the json-ld context and the conversation property" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "hey"})
- {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
+ {:ok, modified} = Transmogrifier.prepare_activity(activity.data)
assert modified["@context"] == Utils.make_json_ld_header()["@context"]
assert modified["object"]["conversation"] == modified["context"]
end
test "it sets the 'attributedTo' property to the actor of the object if it doesn't have one" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "hey"})
- {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
+ {:ok, modified} = Transmogrifier.prepare_activity(activity.data)
assert modified["object"]["actor"] == modified["object"]["attributedTo"]
end
test "it strips internal hashtag data" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "#2hu"})
expected_tag = %{
"href" => Pleroma.Web.Endpoint.url() <> "/tags/2hu",
"type" => "Hashtag",
"name" => "#2hu"
}
- {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
+ {:ok, modified} = Transmogrifier.prepare_activity(activity.data)
assert modified["object"]["tag"] == [expected_tag]
end
test "it strips internal fields" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{
status: "#2hu :firefox:",
generator: %{type: "Application", name: "TestClient", url: "https://pleroma.social"}
})
# Ensure injected application data made it into the activity
# as we don't have a Token to derive it from, otherwise it will
# be nil and the test will pass
assert %{
type: "Application",
name: "TestClient",
url: "https://pleroma.social"
} == activity.object.data["generator"]
- {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
+ {:ok, modified} = Transmogrifier.prepare_activity(activity.data)
assert length(modified["object"]["tag"]) == 2
assert is_nil(modified["object"]["emoji"])
assert is_nil(modified["object"]["like_count"])
assert is_nil(modified["object"]["announcements"])
assert is_nil(modified["object"]["announcement_count"])
assert is_nil(modified["object"]["generator"])
assert is_nil(modified["object"]["rules"])
assert is_nil(modified["object"]["language"])
assert is_nil(modified["object"]["voters"])
end
test "it strips internal fields of article" do
activity = insert(:article_activity)
- {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
+ {:ok, modified} = Transmogrifier.prepare_activity(activity.data)
assert length(modified["object"]["tag"]) == 2
assert is_nil(modified["object"]["emoji"])
assert is_nil(modified["object"]["like_count"])
assert is_nil(modified["object"]["announcements"])
assert is_nil(modified["object"]["announcement_count"])
assert is_nil(modified["object"]["likes"])
end
test "the directMessage flag is present" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "2hu :moominmamma:"})
- {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
+ {:ok, modified} = Transmogrifier.prepare_activity(activity.data)
assert modified["directMessage"] == false
{:ok, activity} = CommonAPI.post(user, %{status: "@#{other_user.nickname} :moominmamma:"})
- {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
+ {:ok, modified} = Transmogrifier.prepare_activity(activity.data)
assert modified["directMessage"] == false
{:ok, activity} =
CommonAPI.post(user, %{
status: "@#{other_user.nickname} :moominmamma:",
visibility: "direct"
})
- {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
+ {:ok, modified} = Transmogrifier.prepare_activity(activity.data)
assert modified["directMessage"] == true
end
test "it strips BCC field" do
user = insert(:user)
{:ok, list} = Pleroma.List.create(%{title: "foo"}, user)
{:ok, activity} = CommonAPI.post(user, %{status: "foobar", visibility: "list:#{list.id}"})
- {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
+ {:ok, modified} = Transmogrifier.prepare_activity(activity.data)
assert is_nil(modified["bcc"])
end
test "it can handle Listen activities" do
listen_activity = insert(:listen)
# This has an inlined object as in ObjectView
- {:ok, modified} = Transmogrifier.prepare_outgoing(listen_activity.data)
+ {:ok, modified} = Transmogrifier.prepare_activity(listen_activity.data)
assert modified["type"] == "Listen"
user = insert(:user)
{:ok, activity} = CommonAPI.listen(user, %{"title" => "lain radio episode 1"})
user_ap_id = user.ap_id
activity_ap_id = activity.data["id"]
activity_to = activity.data["to"]
activity_cc = activity.data["cc"]
object_ap_id = activity.data["object"]
object_type = activity.object.data["type"]
# This does not have an inlined object
- {:ok, modified2} = Transmogrifier.prepare_outgoing(activity.data)
+ {:ok, modified2} = Transmogrifier.prepare_activity(activity.data)
assert match?(
%{
"@context" => [_ | _],
"type" => "Listen",
"actor" => ^user_ap_id,
"to" => ^activity_to,
"cc" => ^activity_cc,
"context" => "http://localhost" <> _,
"id" => ^activity_ap_id,
"object" => %{
"actor" => ^user_ap_id,
"attributedTo" => ^user_ap_id,
"id" => ^object_ap_id,
"type" => ^object_type,
"to" => ^activity_to,
"cc" => ^activity_cc
}
},
modified2
)
end
test "custom emoji urls are URI encoded" do
# :dinosaur: filename has a space -> dino walking.gif
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "everybody do the dinosaur :dinosaur:"})
- {:ok, prepared} = Transmogrifier.prepare_outgoing(activity.data)
+ {:ok, prepared} = Transmogrifier.prepare_activity(activity.data)
assert length(prepared["object"]["tag"]) == 1
url = prepared["object"]["tag"] |> List.first() |> Map.get("icon") |> Map.get("url")
assert url == "http://localhost:4001/emoji/dino%20walking.gif"
end
test "Updates of Notes are handled" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "everybody do the dinosaur :dinosaur:"})
{:ok, update} = CommonAPI.update(activity, user, %{status: "mew mew :blank:"})
- {:ok, prepared} = Transmogrifier.prepare_outgoing(update.data)
+ {:ok, prepared} = Transmogrifier.prepare_activity(update.data)
assert %{
"content" => "mew mew :blank:",
"tag" => [%{"name" => ":blank:", "type" => "Emoji"}],
"formerRepresentations" => %{
"orderedItems" => [
%{
"content" => "everybody do the dinosaur :dinosaur:",
"tag" => [%{"name" => ":dinosaur:", "type" => "Emoji"}]
}
]
}
} = prepared["object"]
end
test "Updates of Actors are handled" do
user = insert(:user, local: true)
changeset = User.update_changeset(user, %{name: "new name"})
{:ok, unpersisted_user} = Ecto.Changeset.apply_action(changeset, :update)
updated_object =
UserView.render("user.json", user: unpersisted_user)
|> Map.delete("@context")
{:ok, update_data, []} = Builder.update(user, updated_object)
{:ok, activity, _} =
Pipeline.common_pipeline(update_data,
local: true,
user_update_changeset: changeset
)
- assert {:ok, prepared} = Transmogrifier.prepare_outgoing(activity.data)
+ assert {:ok, prepared} = Transmogrifier.prepare_activity(activity.data)
assert prepared["type"] == "Update"
assert prepared["@context"]
assert prepared["object"]["type"] == user.actor_type
end
test "Correctly handles Undo activities" do
blocked = insert(:user)
blocker = insert(:user, local: true)
blocked_ap_id = blocked.ap_id
blocker_ap_id = blocker.ap_id
{:ok, %Activity{} = block_activity} = CommonAPI.block(blocked, blocker)
{:ok, %Activity{} = undo_activity} = CommonAPI.unblock(blocked, blocker)
- {:ok, data} = Transmogrifier.prepare_outgoing(undo_activity.data)
+ {:ok, data} = Transmogrifier.prepare_activity(undo_activity.data)
block_ap_id = block_activity.data["id"]
assert is_binary(block_ap_id)
assert match?(
%{
"@context" => [_ | _],
"type" => "Undo",
"id" => "http://localhost" <> _,
"actor" => ^blocker_ap_id,
"object" => ^block_ap_id,
"to" => [^blocked_ap_id],
"cc" => [],
"bto" => [],
"bcc" => []
},
data
)
end
test "Correctly handles EmojiReact activities" do
user = insert(:user, local: true)
note_activity = insert(:note_activity)
user_ap_id = user.ap_id
user_followers = user.follower_address
note_author = note_activity.data["actor"]
note_ap_id = note_activity.data["object"]
assert is_binary(note_author)
assert is_binary(note_ap_id)
{:ok, react_activity} = CommonAPI.react_with_emoji(note_activity.id, user, "🐈")
- {:ok, data} = Transmogrifier.prepare_outgoing(react_activity.data)
+ {:ok, data} = Transmogrifier.prepare_activity(react_activity.data)
assert match?(
%{
"@context" => [_ | _],
"type" => "EmojiReact",
"actor" => ^user_ap_id,
"to" => [^user_followers, ^note_author],
"cc" => ["https://www.w3.org/ns/activitystreams#Public"],
"bto" => [],
"bcc" => [],
"content" => "🐈",
"context" => "2hu",
"id" => "http://localhost" <> _,
"object" => ^note_ap_id,
"tag" => []
},
data
)
end
test "EmojiReact custom emoji urls are URI encoded" do
user = insert(:user, local: true)
note_activity = insert(:note_activity)
{:ok, react_activity} = CommonAPI.react_with_emoji(note_activity.id, user, ":dinosaur:")
- {:ok, data} = Transmogrifier.prepare_outgoing(react_activity.data)
+ {:ok, data} = Transmogrifier.prepare_activity(react_activity.data)
assert length(data["tag"]) == 1
tag = List.first(data["tag"])
url = tag["icon"]["url"]
assert url == "http://localhost:4001/emoji/dino%20walking.gif"
assert tag["id"] == "http://localhost:4001/emoji/dino%20walking.gif"
end
test "it prepares a quote post" do
user = insert(:user)
{:ok, quoted_post} = CommonAPI.post(user, %{status: "hey"})
{:ok, quote_post} = CommonAPI.post(user, %{status: "hey", quoted_status_id: quoted_post.id})
- {:ok, modified} = Transmogrifier.prepare_outgoing(quote_post.data)
+ {:ok, modified} = Transmogrifier.prepare_activity(quote_post.data)
%{data: %{"id" => quote_id}} = Object.normalize(quoted_post)
assert modified["object"]["quoteUrl"] == quote_id
assert modified["object"]["quoteUri"] == quote_id
end
test "it adds language of the object to its json-ld context" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "Cześć", language: "pl"})
- {:ok, modified} = Transmogrifier.prepare_outgoing(activity.object.data)
+ {:ok, modified} = Transmogrifier.prepare_activity(activity.object.data)
assert [_, _, %{"@language" => "pl"}] = modified["@context"]
end
test "it adds language of the object to Create activity json-ld context" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "Cześć", language: "pl"})
- {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
+ {:ok, modified} = Transmogrifier.prepare_activity(activity.data)
assert [_, _, %{"@language" => "pl"}] = modified["@context"]
end
test "it strips report data" do
reporter = insert(:user)
target_account = insert(:user)
content = "foobar"
{:ok, reported_activity} = CommonAPI.post(target_account, %{status: content})
context = Utils.generate_context_id()
object_ap_id = reported_activity.object.data["id"]
assert {:ok, activity} =
Pleroma.Web.ActivityPub.ActivityPub.flag(%{
actor: reporter,
context: context,
account: target_account,
statuses: [reported_activity],
content: content
})
- {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
+ {:ok, data} = Transmogrifier.prepare_activity(activity.data)
expected_data =
activity.data
|> put_in(["object"], [target_account.ap_id, object_ap_id])
|> Map.put("actor", reporter.ap_id)
|> Map.merge(Utils.make_json_ld_header())
assert data == expected_data
end
test "it strips report data and anonymize" do
placeholder = insert(:user)
reporter = insert(:user)
target_account = insert(:user)
content = "foobar"
{:ok, reported_activity} = CommonAPI.post(target_account, %{status: content})
context = Utils.generate_context_id()
object_ap_id = reported_activity.object.data["id"]
assert {:ok, activity} =
Pleroma.Web.ActivityPub.ActivityPub.flag(%{
actor: reporter,
context: context,
account: target_account,
statuses: [reported_activity],
content: content
})
clear_config([:activitypub, :anonymize_reporter], true)
clear_config([:activitypub, :anonymize_reporter_local_nickname], placeholder.nickname)
- {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
+ {:ok, data} = Transmogrifier.prepare_activity(activity.data)
expected_data =
activity.data
|> put_in(["object"], [target_account.ap_id, object_ap_id])
|> Map.put("actor", placeholder.ap_id)
|> Map.merge(Utils.make_json_ld_header())
assert data == expected_data
end
end
describe "actor rewriting" do
test "it fixes the actor URL property to be a proper URI" do
data = %{
"url" => %{"href" => "http://example.com"}
}
rewritten = Transmogrifier.maybe_fix_user_object(data)
assert rewritten["url"] == "http://example.com"
end
end
describe "actor origin containment" do
test "it rejects activities which reference objects with bogus origins" do
data = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "http://mastodon.example.org/users/admin/activities/1234",
"actor" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"object" => "https://info.pleroma.site/activity.json",
"type" => "Announce"
}
assert capture_log(fn ->
{:error, _} = Transmogrifier.handle_incoming(data)
end) =~ "Object rejected while fetching"
end
test "it rejects activities which reference objects that have an incorrect attribution (variant 1)" do
data = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "http://mastodon.example.org/users/admin/activities/1234",
"actor" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"object" => "https://info.pleroma.site/activity2.json",
"type" => "Announce"
}
assert capture_log(fn ->
{:error, _} = Transmogrifier.handle_incoming(data)
end) =~ "Object rejected while fetching"
end
test "it rejects activities which reference objects that have an incorrect attribution (variant 2)" do
data = %{
"@context" => "https://www.w3.org/ns/activitystreams",
"id" => "http://mastodon.example.org/users/admin/activities/1234",
"actor" => "http://mastodon.example.org/users/admin",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"object" => "https://info.pleroma.site/activity3.json",
"type" => "Announce"
}
assert capture_log(fn ->
{:error, _} = Transmogrifier.handle_incoming(data)
end) =~ "Object rejected while fetching"
end
end
describe "fix_explicit_addressing" do
setup do
user = insert(:user)
[user: user]
end
test "moves non-explicitly mentioned actors to cc", %{user: user} do
explicitly_mentioned_actors = [
"https://pleroma.gold/users/user1",
"https://pleroma.gold/user2"
]
object = %{
"actor" => user.ap_id,
"to" => explicitly_mentioned_actors ++ ["https://social.beepboop.ga/users/dirb"],
"cc" => [],
"tag" =>
Enum.map(explicitly_mentioned_actors, fn href ->
%{"type" => "Mention", "href" => href}
end)
}
fixed_object = Transmogrifier.fix_explicit_addressing(object, user.follower_address)
assert Enum.all?(explicitly_mentioned_actors, &(&1 in fixed_object["to"]))
refute "https://social.beepboop.ga/users/dirb" in fixed_object["to"]
assert "https://social.beepboop.ga/users/dirb" in fixed_object["cc"]
end
test "does not move actor's follower collection to cc", %{user: user} do
object = %{
"actor" => user.ap_id,
"to" => [user.follower_address],
"cc" => []
}
fixed_object = Transmogrifier.fix_explicit_addressing(object, user.follower_address)
assert user.follower_address in fixed_object["to"]
refute user.follower_address in fixed_object["cc"]
end
test "removes recipient's follower collection from cc", %{user: user} do
recipient = insert(:user)
object = %{
"actor" => user.ap_id,
"to" => [recipient.ap_id, "https://www.w3.org/ns/activitystreams#Public"],
"cc" => [user.follower_address, recipient.follower_address]
}
fixed_object = Transmogrifier.fix_explicit_addressing(object, user.follower_address)
assert user.follower_address in fixed_object["cc"]
refute recipient.follower_address in fixed_object["cc"]
refute recipient.follower_address in fixed_object["to"]
end
end
describe "fix_summary/1" do
test "returns fixed object" do
assert Transmogrifier.fix_summary(%{"summary" => nil}) == %{"summary" => ""}
assert Transmogrifier.fix_summary(%{"summary" => "ok"}) == %{"summary" => "ok"}
assert Transmogrifier.fix_summary(%{}) == %{"summary" => ""}
end
end
describe "fix_url/1" do
test "fixes data for object when url is map" do
object = %{
"url" => %{
"type" => "Link",
"mimeType" => "video/mp4",
"href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
}
}
assert Transmogrifier.fix_url(object) == %{
"url" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
}
end
test "returns non-modified object" do
assert Transmogrifier.fix_url(%{"type" => "Text"}) == %{"type" => "Text"}
end
end
describe "get_obj_helper/2" do
test "returns nil when cannot normalize object" do
assert capture_log(fn ->
refute Transmogrifier.get_obj_helper("test-obj-id")
end) =~ "Unsupported URI scheme"
end
test "returns {:ok, %Object{}} for success case" do
assert {:ok, %Object{}} =
Transmogrifier.get_obj_helper(
"https://mstdn.io/users/mayuutann/statuses/99568293732299394"
)
end
end
describe "fix_attachments/1" do
test "puts dimensions into attachment url field" do
object = %{
"attachment" => [
%{
"type" => "Document",
"name" => "Hello world",
"url" => "https://media.example.tld/1.jpg",
"width" => 880,
"height" => 960,
"mediaType" => "image/jpeg",
"blurhash" => "eTKL26+HDjcEIBVl;ds+K6t301W.t7nit7y1E,R:v}ai4nXSt7V@of"
}
]
}
expected = %{
"attachment" => [
%{
"type" => "Document",
"name" => "Hello world",
"url" => [
%{
"type" => "Link",
"mediaType" => "image/jpeg",
"href" => "https://media.example.tld/1.jpg",
"width" => 880,
"height" => 960
}
],
"mediaType" => "image/jpeg",
"blurhash" => "eTKL26+HDjcEIBVl;ds+K6t301W.t7nit7y1E,R:v}ai4nXSt7V@of"
}
]
}
assert Transmogrifier.fix_attachments(object) == expected
end
end
describe "prepare_object/1" do
test "it processes history" do
original = %{
"formerRepresentations" => %{
"orderedItems" => [
%{
"generator" => %{},
"emoji" => %{"blobcat" => "http://localhost:4001/emoji/blobcat.png"}
}
]
}
}
processed = Transmogrifier.prepare_object(original)
history_item = Enum.at(processed["formerRepresentations"]["orderedItems"], 0)
refute Map.has_key?(history_item, "generator")
assert [%{"name" => ":blobcat:"}] = history_item["tag"]
end
test "it works when there is no or bad history" do
original = %{
"formerRepresentations" => %{
"items" => [
%{
"generator" => %{},
"emoji" => %{"blobcat" => "http://localhost:4001/emoji/blobcat.png"}
}
]
}
}
processed = Transmogrifier.prepare_object(original)
assert processed["formerRepresentations"] == original["formerRepresentations"]
end
test "it uses contentMap to specify post language" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "Cześć", language: "pl"})
object = Transmogrifier.prepare_object(activity.object.data)
assert %{"contentMap" => %{"pl" => "Cześć"}} = object
end
end
end
diff --git a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs
index fb84569bc..67f292506 100644
--- a/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs
+++ b/test/pleroma/web/mastodon_api/controllers/account_controller_test.exs
@@ -1,2430 +1,2430 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.UserRelationship
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.InternalFetchActor
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.Plugs.SetLocalePlug
import Pleroma.Factory
setup do
Mox.stub_with(Pleroma.UnstubbedConfigMock, Pleroma.Test.StaticConfig)
:ok
end
describe "account fetching" do
test "works by id" do
%User{id: user_id} = insert(:user)
assert %{"id" => ^user_id} =
build_conn()
|> get("/api/v1/accounts/#{user_id}")
|> json_response_and_validate_schema(200)
assert %{"error" => "Can't find user"} =
build_conn()
|> get("/api/v1/accounts/-1")
|> json_response_and_validate_schema(404)
end
test "relationship field" do
%{conn: conn, user: user} = oauth_access(["read"])
other_user = insert(:user)
response =
conn
|> get("/api/v1/accounts/#{other_user.id}")
|> json_response_and_validate_schema(200)
assert response["id"] == other_user.id
assert response["pleroma"]["relationship"] == %{}
assert %{"pleroma" => %{"relationship" => %{"following" => false, "followed_by" => false}}} =
conn
|> get("/api/v1/accounts/#{other_user.id}?with_relationships=true")
|> json_response_and_validate_schema(200)
{:ok, _, %{id: other_id}} = User.follow(user, other_user)
assert %{
"id" => ^other_id,
"pleroma" => %{"relationship" => %{"following" => true, "followed_by" => false}}
} =
conn
|> get("/api/v1/accounts/#{other_id}?with_relationships=true")
|> json_response_and_validate_schema(200)
{:ok, _, _} = User.follow(other_user, user)
assert %{
"id" => ^other_id,
"pleroma" => %{"relationship" => %{"following" => true, "followed_by" => true}}
} =
conn
|> get("/api/v1/accounts/#{other_id}?with_relationships=true")
|> json_response_and_validate_schema(200)
end
test "works by nickname" do
user = insert(:user)
assert %{"id" => _user_id} =
build_conn()
|> get("/api/v1/accounts/#{user.nickname}")
|> json_response_and_validate_schema(200)
end
test "works by nickname for remote users" do
clear_config([:instance, :limit_to_local_content], false)
user = insert(:user, nickname: "user@example.com", local: false)
assert %{"id" => _user_id} =
build_conn()
|> get("/api/v1/accounts/#{user.nickname}")
|> json_response_and_validate_schema(200)
end
test "respects limit_to_local_content == :all for remote user nicknames" do
clear_config([:instance, :limit_to_local_content], :all)
user = insert(:user, nickname: "user@example.com", local: false)
assert build_conn()
|> get("/api/v1/accounts/#{user.nickname}")
|> json_response_and_validate_schema(404)
end
test "respects limit_to_local_content == :unauthenticated for remote user nicknames" do
clear_config([:instance, :limit_to_local_content], :unauthenticated)
user = insert(:user, nickname: "user@example.com", local: false)
reading_user = insert(:user)
conn =
build_conn()
|> get("/api/v1/accounts/#{user.nickname}")
assert json_response_and_validate_schema(conn, 404)
conn =
build_conn()
|> assign(:user, reading_user)
|> assign(:token, insert(:oauth_token, user: reading_user, scopes: ["read:accounts"]))
|> get("/api/v1/accounts/#{user.nickname}")
assert %{"id" => id} = json_response_and_validate_schema(conn, 200)
assert id == user.id
end
test "accounts fetches correct account for nicknames beginning with numbers", %{conn: conn} do
# Need to set an old-style integer ID to reproduce the problem
# (these are no longer assigned to new accounts but were preserved
# for existing accounts during the migration to flakeIDs)
user_one = insert(:user, %{id: 1212})
user_two = insert(:user, %{nickname: "#{user_one.id}garbage"})
acc_one =
conn
|> get("/api/v1/accounts/#{user_one.id}")
|> json_response_and_validate_schema(:ok)
acc_two =
conn
|> get("/api/v1/accounts/#{user_two.nickname}")
|> json_response_and_validate_schema(:ok)
acc_three =
conn
|> get("/api/v1/accounts/#{user_two.id}")
|> json_response_and_validate_schema(:ok)
refute acc_one == acc_two
assert acc_two == acc_three
end
test "returns 404 when user is invisible", %{conn: conn} do
user = insert(:user, %{invisible: true})
assert %{"error" => "Can't find user"} =
conn
|> get("/api/v1/accounts/#{user.nickname}")
|> json_response_and_validate_schema(404)
end
test "returns 404 for internal.fetch actor", %{conn: conn} do
%User{nickname: "internal.fetch"} = InternalFetchActor.get_actor()
assert %{"error" => "Can't find user"} =
conn
|> get("/api/v1/accounts/internal.fetch")
|> json_response_and_validate_schema(404)
end
test "returns 404 for deactivated user", %{conn: conn} do
user = insert(:user, is_active: false)
assert %{"error" => "Can't find user"} =
conn
|> get("/api/v1/accounts/#{user.id}")
|> json_response_and_validate_schema(:not_found)
end
end
defp local_and_remote_users do
local = insert(:user)
remote = insert(:user, local: false)
{:ok, local: local, remote: remote}
end
describe "user fetching with restrict unauthenticated profiles for local and remote" do
setup do: local_and_remote_users()
setup do: clear_config([:restrict_unauthenticated, :profiles, :local], true)
setup do: clear_config([:restrict_unauthenticated, :profiles, :remote], true)
test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
assert %{"error" => "This API requires an authenticated user"} ==
conn
|> get("/api/v1/accounts/#{local.id}")
|> json_response_and_validate_schema(:unauthorized)
assert %{"error" => "This API requires an authenticated user"} ==
conn
|> get("/api/v1/accounts/#{remote.id}")
|> json_response_and_validate_schema(:unauthorized)
end
test "if user is authenticated", %{local: local, remote: remote} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/accounts/#{local.id}")
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
res_conn = get(conn, "/api/v1/accounts/#{remote.id}")
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
end
end
describe "user fetching with restrict unauthenticated profiles for local" do
setup do: local_and_remote_users()
setup do: clear_config([:restrict_unauthenticated, :profiles, :local], true)
test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
res_conn = get(conn, "/api/v1/accounts/#{local.id}")
assert json_response_and_validate_schema(res_conn, :unauthorized) == %{
"error" => "This API requires an authenticated user"
}
res_conn = get(conn, "/api/v1/accounts/#{remote.id}")
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
end
test "if user is authenticated", %{local: local, remote: remote} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/accounts/#{local.id}")
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
res_conn = get(conn, "/api/v1/accounts/#{remote.id}")
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
end
end
describe "user fetching with restrict unauthenticated profiles for remote" do
setup do: local_and_remote_users()
setup do: clear_config([:restrict_unauthenticated, :profiles, :remote], true)
test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
res_conn = get(conn, "/api/v1/accounts/#{local.id}")
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
res_conn = get(conn, "/api/v1/accounts/#{remote.id}")
assert json_response_and_validate_schema(res_conn, :unauthorized) == %{
"error" => "This API requires an authenticated user"
}
end
test "if user is authenticated", %{local: local, remote: remote} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/accounts/#{local.id}")
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
res_conn = get(conn, "/api/v1/accounts/#{remote.id}")
assert %{"id" => _} = json_response_and_validate_schema(res_conn, 200)
end
end
describe "user timelines" do
setup do: oauth_access(["read:statuses"])
test "works with announces that are just addressed to public", %{conn: conn} do
user = insert(:user, ap_id: "https://honktest/u/test", local: false)
other_user = insert(:user)
{:ok, post} = CommonAPI.post(other_user, %{status: "bonkeronk"})
{:ok, announce, _} =
%{
"@context" => "https://www.w3.org/ns/activitystreams",
"actor" => "https://honktest/u/test",
"id" => "https://honktest/u/test/bonk/1793M7B9MQ48847vdx",
"object" => post.data["object"],
"published" => "2019-06-25T19:33:58Z",
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"type" => "Announce"
}
|> ActivityPub.persist(local: false)
assert resp =
conn
|> get("/api/v1/accounts/#{user.id}/statuses")
|> json_response_and_validate_schema(200)
assert [%{"id" => id}] = resp
assert id == announce.id
end
test "deactivated user", %{conn: conn} do
user = insert(:user, is_active: false)
assert %{"error" => "Can't find user"} ==
conn
|> get("/api/v1/accounts/#{user.id}/statuses")
|> json_response_and_validate_schema(:not_found)
end
test "returns 404 when user is invisible", %{conn: conn} do
user = insert(:user, %{invisible: true})
assert %{"error" => "Can't find user"} =
conn
|> get("/api/v1/accounts/#{user.id}")
|> json_response_and_validate_schema(404)
end
test "respects blocks", %{user: user_one, conn: conn} do
user_two = insert(:user)
user_three = insert(:user)
User.block(user_one, user_two)
{:ok, activity} = CommonAPI.post(user_two, %{status: "User one sux0rz"})
{:ok, repeat} = CommonAPI.repeat(activity.id, user_three)
assert resp =
conn
|> get("/api/v1/accounts/#{user_two.id}/statuses")
|> json_response_and_validate_schema(200)
assert [%{"id" => id}] = resp
assert id == activity.id
# Even a blocked user will deliver the full user timeline, there would be
# no point in looking at a blocked users timeline otherwise
assert resp =
conn
|> get("/api/v1/accounts/#{user_two.id}/statuses")
|> json_response_and_validate_schema(200)
assert [%{"id" => id}] = resp
assert id == activity.id
# Third user's timeline includes the repeat when viewed by unauthenticated user
resp =
build_conn()
|> get("/api/v1/accounts/#{user_three.id}/statuses")
|> json_response_and_validate_schema(200)
assert [%{"id" => id}] = resp
assert id == repeat.id
# When viewing a third user's timeline, the blocked users' statuses will NOT be shown
resp = get(conn, "/api/v1/accounts/#{user_three.id}/statuses")
assert [] == json_response_and_validate_schema(resp, 200)
end
test "gets users statuses", %{conn: conn} do
user_one = insert(:user)
user_two = insert(:user)
user_three = insert(:user)
{:ok, _user_three, _user_one} = User.follow(user_three, user_one)
{:ok, activity} = CommonAPI.post(user_one, %{status: "HI!!!"})
{:ok, direct_activity} =
CommonAPI.post(user_one, %{
status: "Hi, @#{user_two.nickname}.",
visibility: "direct"
})
{:ok, private_activity} =
CommonAPI.post(user_one, %{status: "private", visibility: "private"})
# TODO!!!
resp =
conn
|> get("/api/v1/accounts/#{user_one.id}/statuses")
|> json_response_and_validate_schema(200)
assert [%{"id" => id}] = resp
assert id == to_string(activity.id)
resp =
conn
|> assign(:user, user_two)
|> assign(:token, insert(:oauth_token, user: user_two, scopes: ["read:statuses"]))
|> get("/api/v1/accounts/#{user_one.id}/statuses")
|> json_response_and_validate_schema(200)
assert [%{"id" => id_one}, %{"id" => id_two}] = resp
assert id_one == to_string(direct_activity.id)
assert id_two == to_string(activity.id)
resp =
conn
|> assign(:user, user_three)
|> assign(:token, insert(:oauth_token, user: user_three, scopes: ["read:statuses"]))
|> get("/api/v1/accounts/#{user_one.id}/statuses")
|> json_response_and_validate_schema(200)
assert [%{"id" => id_one}, %{"id" => id_two}] = resp
assert id_one == to_string(private_activity.id)
assert id_two == to_string(activity.id)
end
test "gets local-only statuses for authenticated users", %{user: _user, conn: conn} do
user_one = insert(:user)
{:ok, activity} = CommonAPI.post(user_one, %{status: "HI!!!", visibility: "local"})
resp =
conn
|> get("/api/v1/accounts/#{user_one.id}/statuses")
|> json_response_and_validate_schema(200)
assert [%{"id" => id}] = resp
assert id == to_string(activity.id)
end
test "gets an users media, excludes reblogs", %{conn: conn} do
note = insert(:note_activity)
user = User.get_cached_by_ap_id(note.data["actor"])
other_user = insert(:user)
file = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
{:ok, %{id: media_id}} = ActivityPub.upload(file, actor: user.ap_id)
{:ok, %{id: image_post_id}} = CommonAPI.post(user, %{status: "cofe", media_ids: [media_id]})
{:ok, %{id: media_id}} = ActivityPub.upload(file, actor: other_user.ap_id)
{:ok, %{id: other_image_post_id}} =
CommonAPI.post(other_user, %{status: "cofe2", media_ids: [media_id]})
{:ok, _announce} = CommonAPI.repeat(other_image_post_id, user)
conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?only_media=true")
assert [%{"id" => ^image_post_id}] = json_response_and_validate_schema(conn, 200)
conn = get(build_conn(), "/api/v1/accounts/#{user.id}/statuses?only_media=1")
assert [%{"id" => ^image_post_id}] = json_response_and_validate_schema(conn, 200)
end
test "gets a user's statuses without reblogs", %{user: user, conn: conn} do
{:ok, %{id: post_id}} = CommonAPI.post(user, %{status: "HI!!!"})
{:ok, _} = CommonAPI.repeat(post_id, user)
conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?exclude_reblogs=true")
assert [%{"id" => ^post_id}] = json_response_and_validate_schema(conn, 200)
conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?exclude_reblogs=1")
assert [%{"id" => ^post_id}] = json_response_and_validate_schema(conn, 200)
end
test "gets only a user's reblogs", %{user: user, conn: conn} do
{:ok, %{id: post_id}} = CommonAPI.post(user, %{status: "HI!!!"})
{:ok, %{id: reblog_id}} = CommonAPI.repeat(post_id, user)
conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?only_reblogs=true")
assert [%{"id" => ^reblog_id}] = json_response_and_validate_schema(conn, 200)
conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?only_reblogs=1")
assert [%{"id" => ^reblog_id}] = json_response_and_validate_schema(conn, 200)
end
test "filters user's statuses by a hashtag", %{user: user, conn: conn} do
{:ok, %{id: post_id}} = CommonAPI.post(user, %{status: "#hashtag"})
{:ok, _post} = CommonAPI.post(user, %{status: "hashtag"})
conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?tagged=hashtag")
assert [%{"id" => ^post_id}] = json_response_and_validate_schema(conn, 200)
end
test "the user views their own timelines and excludes direct messages", %{
user: user,
conn: conn
} do
{:ok, %{id: public_activity_id}} =
CommonAPI.post(user, %{status: ".", visibility: "public"})
{:ok, _direct_activity} = CommonAPI.post(user, %{status: ".", visibility: "direct"})
conn = get(conn, "/api/v1/accounts/#{user.id}/statuses?exclude_visibilities[]=direct")
assert [%{"id" => ^public_activity_id}] = json_response_and_validate_schema(conn, 200)
end
test "muted reactions", %{user: user, conn: conn} do
user2 = insert(:user)
User.mute(user, user2)
{:ok, activity} = CommonAPI.post(user, %{status: "."})
{:ok, _} = CommonAPI.react_with_emoji(activity.id, user2, "🎅")
result =
conn
|> get("/api/v1/accounts/#{user.id}/statuses")
|> json_response_and_validate_schema(200)
assert [
%{
"pleroma" => %{
"emoji_reactions" => []
}
}
] = result
result =
conn
|> get("/api/v1/accounts/#{user.id}/statuses?with_muted=true")
|> json_response_and_validate_schema(200)
assert [
%{
"pleroma" => %{
"emoji_reactions" => [%{"count" => 1, "me" => false, "name" => "🎅"}]
}
}
] = result
end
test "paginates a user's statuses", %{user: user, conn: conn} do
{:ok, post_1} = CommonAPI.post(user, %{status: "first post"})
{:ok, post_2} = CommonAPI.post(user, %{status: "second post"})
response_1 = get(conn, "/api/v1/accounts/#{user.id}/statuses?limit=1")
assert [res] = json_response_and_validate_schema(response_1, 200)
assert res["id"] == post_2.id
response_2 = get(conn, "/api/v1/accounts/#{user.id}/statuses?limit=1&max_id=#{res["id"]}")
assert [res] = json_response_and_validate_schema(response_2, 200)
assert res["id"] == post_1.id
refute response_1 == response_2
end
end
defp local_and_remote_activities(%{local: local, remote: remote}) do
insert(:note_activity, user: local)
insert(:note_activity, user: remote, local: false)
:ok
end
describe "statuses with restrict unauthenticated profiles for local and remote" do
setup do: local_and_remote_users()
setup :local_and_remote_activities
setup do: clear_config([:restrict_unauthenticated, :profiles, :local], true)
setup do: clear_config([:restrict_unauthenticated, :profiles, :remote], true)
test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
assert %{"error" => "This API requires an authenticated user"} ==
conn
|> get("/api/v1/accounts/#{local.id}/statuses")
|> json_response_and_validate_schema(:unauthorized)
assert %{"error" => "This API requires an authenticated user"} ==
conn
|> get("/api/v1/accounts/#{remote.id}/statuses")
|> json_response_and_validate_schema(:unauthorized)
end
test "if user is authenticated", %{local: local, remote: remote} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses")
assert length(json_response_and_validate_schema(res_conn, 200)) == 1
res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses")
assert length(json_response_and_validate_schema(res_conn, 200)) == 1
end
end
describe "statuses with restrict unauthenticated profiles for local" do
setup do: local_and_remote_users()
setup :local_and_remote_activities
setup do: clear_config([:restrict_unauthenticated, :profiles, :local], true)
test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
assert %{"error" => "This API requires an authenticated user"} ==
conn
|> get("/api/v1/accounts/#{local.id}/statuses")
|> json_response_and_validate_schema(:unauthorized)
res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses")
assert length(json_response_and_validate_schema(res_conn, 200)) == 1
end
test "if user is authenticated", %{local: local, remote: remote} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses")
assert length(json_response_and_validate_schema(res_conn, 200)) == 1
res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses")
assert length(json_response_and_validate_schema(res_conn, 200)) == 1
end
end
describe "statuses with restrict unauthenticated profiles for remote" do
setup do: local_and_remote_users()
setup :local_and_remote_activities
setup do: clear_config([:restrict_unauthenticated, :profiles, :remote], true)
test "if user is unauthenticated", %{conn: conn, local: local, remote: remote} do
res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses")
assert length(json_response_and_validate_schema(res_conn, 200)) == 1
assert %{"error" => "This API requires an authenticated user"} ==
conn
|> get("/api/v1/accounts/#{remote.id}/statuses")
|> json_response_and_validate_schema(:unauthorized)
end
test "if user is authenticated", %{local: local, remote: remote} do
%{conn: conn} = oauth_access(["read"])
res_conn = get(conn, "/api/v1/accounts/#{local.id}/statuses")
assert length(json_response_and_validate_schema(res_conn, 200)) == 1
res_conn = get(conn, "/api/v1/accounts/#{remote.id}/statuses")
assert length(json_response_and_validate_schema(res_conn, 200)) == 1
end
end
describe "followers" do
setup do: oauth_access(["read:accounts"])
test "getting followers", %{user: user, conn: conn} do
other_user = insert(:user)
{:ok, %{id: user_id}, other_user} = User.follow(user, other_user)
conn = get(conn, "/api/v1/accounts/#{other_user.id}/followers")
assert [%{"id" => ^user_id}] = json_response_and_validate_schema(conn, 200)
end
test "following with relationship", %{conn: conn, user: user} do
other_user = insert(:user)
{:ok, %{id: id}, _} = User.follow(other_user, user)
assert [
%{
"id" => ^id,
"pleroma" => %{
"relationship" => %{
"id" => ^id,
"following" => false,
"followed_by" => true
}
}
}
] =
conn
|> get("/api/v1/accounts/#{user.id}/followers?with_relationships=true")
|> json_response_and_validate_schema(200)
{:ok, _, _} = User.follow(user, other_user)
assert [
%{
"id" => ^id,
"pleroma" => %{
"relationship" => %{
"id" => ^id,
"following" => true,
"followed_by" => true
}
}
}
] =
conn
|> get("/api/v1/accounts/#{user.id}/followers?with_relationships=true")
|> json_response_and_validate_schema(200)
end
test "getting followers, hide_followers", %{user: user, conn: conn} do
other_user = insert(:user, hide_followers: true)
{:ok, _user, _other_user} = User.follow(user, other_user)
conn = get(conn, "/api/v1/accounts/#{other_user.id}/followers")
assert [] == json_response_and_validate_schema(conn, 200)
end
test "getting followers, hide_followers, same user requesting" do
user = insert(:user)
other_user = insert(:user, hide_followers: true)
{:ok, _user, _other_user} = User.follow(user, other_user)
conn =
build_conn()
|> assign(:user, other_user)
|> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
|> get("/api/v1/accounts/#{other_user.id}/followers")
refute [] == json_response_and_validate_schema(conn, 200)
end
test "getting followers, pagination", %{user: user, conn: conn} do
{:ok, %User{id: follower1_id}, _user} = :user |> insert() |> User.follow(user)
{:ok, %User{id: follower2_id}, _user} = :user |> insert() |> User.follow(user)
{:ok, %User{id: follower3_id}, _user} = :user |> insert() |> User.follow(user)
assert [%{"id" => ^follower3_id}, %{"id" => ^follower2_id}] =
conn
|> get("/api/v1/accounts/#{user.id}/followers?since_id=#{follower1_id}")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^follower2_id}, %{"id" => ^follower1_id}] =
conn
|> get("/api/v1/accounts/#{user.id}/followers?max_id=#{follower3_id}")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^follower2_id}, %{"id" => ^follower1_id}] =
conn
|> get(
"/api/v1/accounts/#{user.id}/followers?id=#{user.id}&limit=20&max_id=#{follower3_id}"
)
|> json_response_and_validate_schema(200)
res_conn = get(conn, "/api/v1/accounts/#{user.id}/followers?limit=1&max_id=#{follower3_id}")
assert [%{"id" => ^follower2_id}] = json_response_and_validate_schema(res_conn, 200)
assert [link_header] = get_resp_header(res_conn, "link")
assert link_header =~ ~r/min_id=#{follower2_id}/
assert link_header =~ ~r/max_id=#{follower2_id}/
end
end
describe "following" do
setup do: oauth_access(["read:accounts"])
test "getting following", %{user: user, conn: conn} do
other_user = insert(:user)
{:ok, user, other_user} = User.follow(user, other_user)
conn = get(conn, "/api/v1/accounts/#{user.id}/following")
assert [%{"id" => id}] = json_response_and_validate_schema(conn, 200)
assert id == to_string(other_user.id)
end
test "following with relationship", %{conn: conn, user: user} do
other_user = insert(:user)
{:ok, user, other_user} = User.follow(user, other_user)
conn = get(conn, "/api/v1/accounts/#{user.id}/following?with_relationships=true")
id = other_user.id
assert [
%{
"id" => ^id,
"pleroma" => %{
"relationship" => %{"id" => ^id, "following" => true, "followed_by" => false}
}
}
] = json_response_and_validate_schema(conn, 200)
end
test "getting following, hide_follows, other user requesting" do
user = insert(:user, hide_follows: true)
other_user = insert(:user)
{:ok, user, other_user} = User.follow(user, other_user)
conn =
build_conn()
|> assign(:user, other_user)
|> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:accounts"]))
|> get("/api/v1/accounts/#{user.id}/following")
assert [] == json_response_and_validate_schema(conn, 200)
end
test "getting following, hide_follows, same user requesting" do
user = insert(:user, hide_follows: true)
other_user = insert(:user)
{:ok, user, _other_user} = User.follow(user, other_user)
conn =
build_conn()
|> assign(:user, user)
|> assign(:token, insert(:oauth_token, user: user, scopes: ["read:accounts"]))
|> get("/api/v1/accounts/#{user.id}/following")
refute [] == json_response_and_validate_schema(conn, 200)
end
test "getting following, pagination", %{user: user, conn: conn} do
following1 = insert(:user)
following2 = insert(:user)
following3 = insert(:user)
{:ok, _, _} = User.follow(user, following1)
{:ok, _, _} = User.follow(user, following2)
{:ok, _, _} = User.follow(user, following3)
res_conn = get(conn, "/api/v1/accounts/#{user.id}/following?since_id=#{following1.id}")
assert [%{"id" => id3}, %{"id" => id2}] = json_response_and_validate_schema(res_conn, 200)
assert id3 == following3.id
assert id2 == following2.id
res_conn = get(conn, "/api/v1/accounts/#{user.id}/following?max_id=#{following3.id}")
assert [%{"id" => id2}, %{"id" => id1}] = json_response_and_validate_schema(res_conn, 200)
assert id2 == following2.id
assert id1 == following1.id
res_conn =
get(
conn,
"/api/v1/accounts/#{user.id}/following?id=#{user.id}&limit=20&max_id=#{following3.id}"
)
assert [%{"id" => id2}, %{"id" => id1}] = json_response_and_validate_schema(res_conn, 200)
assert id2 == following2.id
assert id1 == following1.id
res_conn =
get(conn, "/api/v1/accounts/#{user.id}/following?limit=1&max_id=#{following3.id}")
assert [%{"id" => id2}] = json_response_and_validate_schema(res_conn, 200)
assert id2 == following2.id
assert [link_header] = get_resp_header(res_conn, "link")
assert link_header =~ ~r/min_id=#{following2.id}/
assert link_header =~ ~r/max_id=#{following2.id}/
end
end
describe "follow/unfollow" do
setup do: oauth_access(["follow"])
test "following / unfollowing a user", %{conn: conn} do
%{id: other_user_id, nickname: other_user_nickname} = insert(:user)
assert %{"id" => _id, "following" => true} =
conn
|> post("/api/v1/accounts/#{other_user_id}/follow")
|> json_response_and_validate_schema(200)
assert %{"id" => _id, "following" => false} =
conn
|> post("/api/v1/accounts/#{other_user_id}/unfollow")
|> json_response_and_validate_schema(200)
assert %{"id" => ^other_user_id} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/follows", %{"uri" => other_user_nickname})
|> json_response_and_validate_schema(200)
end
test "cancelling follow request", %{conn: conn} do
%{id: other_user_id} = insert(:user, %{is_locked: true})
assert %{"id" => ^other_user_id, "following" => false, "requested" => true} =
conn
|> post("/api/v1/accounts/#{other_user_id}/follow")
|> json_response_and_validate_schema(:ok)
assert %{"id" => ^other_user_id, "following" => false, "requested" => false} =
conn
|> post("/api/v1/accounts/#{other_user_id}/unfollow")
|> json_response_and_validate_schema(:ok)
end
test "following without reblogs" do
%{conn: conn} = oauth_access(["follow", "read:statuses"])
followed = insert(:user)
other_user = insert(:user)
ret_conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/accounts/#{followed.id}/follow", %{reblogs: false})
assert %{"showing_reblogs" => false} = json_response_and_validate_schema(ret_conn, 200)
{:ok, activity} = CommonAPI.post(other_user, %{status: "hey"})
{:ok, %{id: reblog_id}} = CommonAPI.repeat(activity.id, followed)
assert [] ==
conn
|> get("/api/v1/timelines/home")
|> json_response_and_validate_schema(200)
assert %{"showing_reblogs" => true} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/accounts/#{followed.id}/follow", %{reblogs: true})
|> json_response_and_validate_schema(200)
assert %{"showing_reblogs" => true} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/accounts/#{followed.id}/follow", %{reblogs: "1"})
|> json_response_and_validate_schema(200)
assert [%{"id" => ^reblog_id}] =
conn
|> get("/api/v1/timelines/home")
|> json_response_and_validate_schema(200)
end
test "following with reblogs" do
%{conn: conn} = oauth_access(["follow", "read:statuses"])
followed = insert(:user)
other_user = insert(:user)
ret_conn = post(conn, "/api/v1/accounts/#{followed.id}/follow")
assert %{"showing_reblogs" => true} = json_response_and_validate_schema(ret_conn, 200)
{:ok, activity} = CommonAPI.post(other_user, %{status: "hey"})
{:ok, %{id: reblog_id}} = CommonAPI.repeat(activity.id, followed)
assert [%{"id" => ^reblog_id}] =
conn
|> get("/api/v1/timelines/home")
|> json_response_and_validate_schema(200)
assert %{"showing_reblogs" => false} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/accounts/#{followed.id}/follow", %{reblogs: false})
|> json_response_and_validate_schema(200)
assert %{"showing_reblogs" => false} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/accounts/#{followed.id}/follow", %{reblogs: "0"})
|> json_response_and_validate_schema(200)
assert [] ==
conn
|> get("/api/v1/timelines/home")
|> json_response_and_validate_schema(200)
end
test "following with subscription and unsubscribing" do
%{conn: conn} = oauth_access(["follow"])
followed = insert(:user)
assert %{"subscribing" => true} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/accounts/#{followed.id}/follow", %{notify: true})
|> json_response_and_validate_schema(200)
assert %{"subscribing" => true} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/accounts/#{followed.id}/follow", %{notify: "1"})
|> json_response_and_validate_schema(200)
assert %{"subscribing" => false} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/accounts/#{followed.id}/follow", %{notify: false})
|> json_response_and_validate_schema(200)
end
test "following / unfollowing errors", %{user: user, conn: conn} do
# self follow
conn_res = post(conn, "/api/v1/accounts/#{user.id}/follow")
assert %{"error" => "Can not follow yourself"} =
json_response_and_validate_schema(conn_res, 400)
# self unfollow
user = User.get_cached_by_id(user.id)
conn_res = post(conn, "/api/v1/accounts/#{user.id}/unfollow")
assert %{"error" => "Can not unfollow yourself"} =
json_response_and_validate_schema(conn_res, 400)
# self follow via uri
user = User.get_cached_by_id(user.id)
assert %{"error" => "Can not follow yourself"} =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/v1/follows", %{"uri" => user.nickname})
|> json_response_and_validate_schema(400)
# follow non existing user
conn_res = post(conn, "/api/v1/accounts/doesntexist/follow")
assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn_res, 404)
# follow non existing user via uri
conn_res =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/v1/follows", %{"uri" => "doesntexist"})
assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn_res, 404)
# unfollow non existing user
conn_res = post(conn, "/api/v1/accounts/doesntexist/unfollow")
assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn_res, 404)
end
end
describe "mute/unmute" do
setup do: oauth_access(["write:mutes"])
test "with notifications", %{conn: conn} do
other_user = insert(:user)
assert %{"id" => _id, "muting" => true, "muting_notifications" => true} =
conn
|> post("/api/v1/accounts/#{other_user.id}/mute")
|> json_response_and_validate_schema(200)
conn = post(conn, "/api/v1/accounts/#{other_user.id}/unmute")
assert %{"id" => _id, "muting" => false, "muting_notifications" => false} =
json_response_and_validate_schema(conn, 200)
end
test "without notifications", %{conn: conn} do
other_user = insert(:user)
ret_conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/v1/accounts/#{other_user.id}/mute", %{"notifications" => "false"})
assert %{"id" => _id, "muting" => true, "muting_notifications" => false} =
json_response_and_validate_schema(ret_conn, 200)
conn = post(conn, "/api/v1/accounts/#{other_user.id}/unmute")
assert %{"id" => _id, "muting" => false, "muting_notifications" => false} =
json_response_and_validate_schema(conn, 200)
end
test "expiring", %{conn: conn, user: user} do
other_user = insert(:user)
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/v1/accounts/#{other_user.id}/mute", %{"duration" => "86400"})
assert %{"id" => _id, "muting" => true} = json_response_and_validate_schema(conn, 200)
mute_expires_at = UserRelationship.get_mute_expire_date(user, other_user)
assert DateTime.diff(
mute_expires_at,
DateTime.utc_now() |> DateTime.add(24 * 60 * 60)
) in -3..3
end
test "falls back to expires_in", %{conn: conn, user: user} do
other_user = insert(:user)
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/v1/accounts/#{other_user.id}/mute", %{"expires_in" => "86400"})
|> json_response_and_validate_schema(200)
mute_expires_at = UserRelationship.get_mute_expire_date(user, other_user)
assert DateTime.diff(
mute_expires_at,
DateTime.utc_now() |> DateTime.add(24 * 60 * 60)
) in -3..3
end
end
describe "pinned statuses" do
setup do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{status: "HI!!!"})
%{conn: conn} = oauth_access(["read:statuses"], user: user)
[conn: conn, user: user, activity: activity]
end
test "returns pinned statuses", %{conn: conn, user: user, activity: %{id: activity_id}} do
{:ok, _} = CommonAPI.pin(activity_id, user)
assert [%{"id" => ^activity_id, "pinned" => true}] =
conn
|> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
|> json_response_and_validate_schema(200)
end
end
test "view pinned private statuses" do
user = insert(:user)
reader = insert(:user)
# Create a private status and pin it
{:ok, %{id: activity_id} = activity} =
CommonAPI.post(user, %{status: "psst", visibility: "private"})
%{data: %{"id" => object_ap_id}} = Object.normalize(activity)
{:ok, _} = User.add_pinned_object_id(user, object_ap_id)
%{conn: conn} = oauth_access(["read:statuses"], user: reader)
# A non-follower can't see the pinned status
assert [] ==
conn
|> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
|> json_response_and_validate_schema(200)
# Follow the user, then the pinned status can be seen
CommonAPI.follow(user, reader)
ObanHelpers.perform_all()
assert [%{"id" => ^activity_id, "pinned" => true}] =
conn
|> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
|> json_response_and_validate_schema(200)
end
test "blocking / unblocking a user" do
%{conn: conn} = oauth_access(["follow"])
other_user = insert(:user)
ret_conn = post(conn, "/api/v1/accounts/#{other_user.id}/block")
assert %{"id" => _id, "blocking" => true} = json_response_and_validate_schema(ret_conn, 200)
conn = post(conn, "/api/v1/accounts/#{other_user.id}/unblock")
assert %{"id" => _id, "blocking" => false} = json_response_and_validate_schema(conn, 200)
end
describe "create account by app" do
setup do
valid_params = %{
username: "lain",
email: "lain@example.org",
password: "PlzDontHackLain",
agreement: true
}
[valid_params: valid_params]
end
test "registers and logs in without :account_activation_required / :account_approval_required",
%{conn: conn} do
clear_config([:instance, :account_activation_required], false)
clear_config([:instance, :account_approval_required], false)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/apps", %{
client_name: "client_name",
redirect_uris: "urn:ietf:wg:oauth:2.0:oob",
scopes: "read, write, follow"
})
assert %{
"client_id" => client_id,
"client_secret" => client_secret,
"id" => _,
"name" => "client_name",
"redirect_uri" => "urn:ietf:wg:oauth:2.0:oob",
"vapid_key" => _,
"website" => nil
} = json_response_and_validate_schema(conn, 200)
conn =
post(conn, "/oauth/token", %{
grant_type: "client_credentials",
client_id: client_id,
client_secret: client_secret
})
assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} =
json_response(conn, 200)
assert token
token_from_db = Repo.get_by(Token, token: token)
assert token_from_db
assert refresh
assert scope == "read write follow"
clear_config([User, :email_blacklist], ["example.org"])
params = %{
username: "lain",
email: "lain@example.org",
password: "PlzDontHackLain",
bio: "Test Bio",
agreement: true
}
conn =
build_conn()
|> put_req_header("content-type", "multipart/form-data")
|> put_req_header("authorization", "Bearer " <> token)
|> post("/api/v1/accounts", params)
assert %{"error" => "{\"email\":[\"Invalid email\"]}"} =
json_response_and_validate_schema(conn, 400)
clear_config([User, :email_blacklist], [])
conn =
build_conn()
|> put_req_header("content-type", "multipart/form-data")
|> put_req_header("authorization", "Bearer " <> token)
|> post("/api/v1/accounts", params)
%{
"access_token" => token,
"created_at" => _created_at,
"scope" => ^scope,
"token_type" => "Bearer"
} = json_response_and_validate_schema(conn, 200)
token_from_db = Repo.get_by(Token, token: token)
assert token_from_db
user = Repo.preload(token_from_db, :user).user
assert user
assert user.is_confirmed
assert user.is_approved
end
test "registers but does not log in with :account_activation_required", %{conn: conn} do
clear_config([:instance, :account_activation_required], true)
clear_config([:instance, :account_approval_required], false)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/apps", %{
client_name: "client_name",
redirect_uris: "urn:ietf:wg:oauth:2.0:oob",
scopes: "read, write, follow"
})
assert %{
"client_id" => client_id,
"client_secret" => client_secret,
"id" => _,
"name" => "client_name",
"redirect_uri" => "urn:ietf:wg:oauth:2.0:oob",
"vapid_key" => _,
"website" => nil
} = json_response_and_validate_schema(conn, 200)
conn =
post(conn, "/oauth/token", %{
grant_type: "client_credentials",
client_id: client_id,
client_secret: client_secret
})
assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} =
json_response(conn, 200)
assert token
token_from_db = Repo.get_by(Token, token: token)
assert token_from_db
assert refresh
assert scope == "read write follow"
conn =
build_conn()
|> put_req_header("content-type", "multipart/form-data")
|> put_req_header("authorization", "Bearer " <> token)
|> post("/api/v1/accounts", %{
username: "lain",
email: "lain@example.org",
password: "PlzDontHackLain",
bio: "Test Bio",
agreement: true
})
response = json_response_and_validate_schema(conn, 200)
assert %{"identifier" => "missing_confirmed_email"} = response
refute response["access_token"]
refute response["token_type"]
user = Repo.get_by(User, email: "lain@example.org")
refute user.is_confirmed
end
test "registers but does not log in with :account_approval_required", %{conn: conn} do
clear_config([:instance, :account_approval_required], true)
clear_config([:instance, :account_activation_required], false)
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/apps", %{
client_name: "client_name",
redirect_uris: "urn:ietf:wg:oauth:2.0:oob",
scopes: "read, write, follow"
})
assert %{
"client_id" => client_id,
"client_secret" => client_secret,
"id" => _,
"name" => "client_name",
"redirect_uri" => "urn:ietf:wg:oauth:2.0:oob",
"vapid_key" => _,
"website" => nil
} = json_response_and_validate_schema(conn, 200)
conn =
post(conn, "/oauth/token", %{
grant_type: "client_credentials",
client_id: client_id,
client_secret: client_secret
})
assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} =
json_response(conn, 200)
assert token
token_from_db = Repo.get_by(Token, token: token)
assert token_from_db
assert refresh
assert scope == "read write follow"
conn =
build_conn()
|> put_req_header("content-type", "multipart/form-data")
|> put_req_header("authorization", "Bearer " <> token)
|> post("/api/v1/accounts", %{
username: "lain",
email: "lain@example.org",
password: "PlzDontHackLain",
bio: "Test Bio",
agreement: true,
reason: "I'm a cool dude, bro"
})
response = json_response_and_validate_schema(conn, 200)
assert %{"identifier" => "awaiting_approval"} = response
refute response["access_token"]
refute response["token_type"]
user = Repo.get_by(User, email: "lain@example.org")
refute user.is_approved
assert user.registration_reason == "I'm a cool dude, bro"
end
test "returns error when user already registered", %{conn: conn, valid_params: valid_params} do
_user = insert(:user, email: "lain@example.org")
app_token = insert(:oauth_token, user: nil)
res =
conn
|> put_req_header("authorization", "Bearer " <> app_token.token)
|> put_req_header("content-type", "application/json")
|> post("/api/v1/accounts", valid_params)
assert json_response_and_validate_schema(res, 400) == %{
"error" => "{\"email\":[\"has already been taken\"]}"
}
end
test "returns bad_request if missing required params", %{
conn: conn,
valid_params: valid_params
} do
app_token = insert(:oauth_token, user: nil)
conn =
conn
|> put_req_header("authorization", "Bearer " <> app_token.token)
|> put_req_header("content-type", "application/json")
res = post(conn, "/api/v1/accounts", valid_params)
assert json_response_and_validate_schema(res, 200)
[{127, 0, 0, 1}, {127, 0, 0, 2}, {127, 0, 0, 3}, {127, 0, 0, 4}]
|> Stream.zip(Map.delete(valid_params, :email))
|> Enum.each(fn {ip, {attr, _}} ->
res =
conn
|> Map.put(:remote_ip, ip)
|> post("/api/v1/accounts", Map.delete(valid_params, attr))
|> json_response_and_validate_schema(400)
assert res == %{
"error" => "Missing field: #{attr}.",
"errors" => [
%{
"message" => "Missing field: #{attr}",
"source" => %{"pointer" => "/#{attr}"},
"title" => "Invalid value"
}
]
}
end)
end
test "returns bad_request if missing email params when :account_activation_required is enabled",
%{conn: conn, valid_params: valid_params} do
clear_config([:instance, :account_activation_required], true)
app_token = insert(:oauth_token, user: nil)
conn =
conn
|> put_req_header("authorization", "Bearer " <> app_token.token)
|> put_req_header("content-type", "application/json")
res =
conn
|> Map.put(:remote_ip, {127, 0, 0, 5})
|> post("/api/v1/accounts", Map.delete(valid_params, :email))
assert json_response_and_validate_schema(res, 400) ==
%{"error" => "Missing parameter: email"}
res =
conn
|> Map.put(:remote_ip, {127, 0, 0, 6})
|> post("/api/v1/accounts", Map.put(valid_params, :email, ""))
assert json_response_and_validate_schema(res, 400) == %{
"error" => "{\"email\":[\"can't be blank\"]}"
}
end
test "allow registration without an email", %{conn: conn, valid_params: valid_params} do
app_token = insert(:oauth_token, user: nil)
conn = put_req_header(conn, "authorization", "Bearer " <> app_token.token)
res =
conn
|> put_req_header("content-type", "application/json")
|> Map.put(:remote_ip, {127, 0, 0, 7})
|> post("/api/v1/accounts", Map.delete(valid_params, :email))
assert json_response_and_validate_schema(res, 200)
end
test "allow registration with an empty email", %{conn: conn, valid_params: valid_params} do
app_token = insert(:oauth_token, user: nil)
conn = put_req_header(conn, "authorization", "Bearer " <> app_token.token)
res =
conn
|> put_req_header("content-type", "application/json")
|> Map.put(:remote_ip, {127, 0, 0, 8})
|> post("/api/v1/accounts", Map.put(valid_params, :email, ""))
assert json_response_and_validate_schema(res, 200)
end
test "returns forbidden if token is invalid", %{conn: conn, valid_params: valid_params} do
res =
conn
|> put_req_header("authorization", "Bearer " <> "invalid-token")
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/v1/accounts", valid_params)
assert json_response_and_validate_schema(res, 403) == %{"error" => "Invalid credentials"}
end
test "registration from trusted app" do
clear_config([Pleroma.Captcha, :enabled], true)
app = insert(:oauth_app, trusted: true, scopes: ["read", "write", "follow", "push"])
conn =
build_conn()
|> post("/oauth/token", %{
"grant_type" => "client_credentials",
"client_id" => app.client_id,
"client_secret" => app.client_secret
})
assert %{"access_token" => token, "token_type" => "Bearer"} = json_response(conn, 200)
response =
build_conn()
|> Plug.Conn.put_req_header("authorization", "Bearer " <> token)
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/v1/accounts", %{
nickname: "nickname",
agreement: true,
email: "email@example.com",
fullname: "Lain",
username: "Lain",
password: "some_password",
confirm: "some_password"
})
|> json_response_and_validate_schema(200)
assert %{
"access_token" => access_token,
"created_at" => _,
"scope" => "read write follow push",
"token_type" => "Bearer"
} = response
response =
build_conn()
|> Plug.Conn.put_req_header("authorization", "Bearer " <> access_token)
|> get("/api/v1/accounts/verify_credentials")
|> json_response_and_validate_schema(200)
assert %{
"acct" => "Lain",
"bot" => false,
"display_name" => "Lain",
"follow_requests_count" => 0,
"followers_count" => 0,
"following_count" => 0,
"locked" => false,
"note" => "",
"source" => %{
"fields" => [],
"note" => "",
"pleroma" => %{
"actor_type" => "Person",
"discoverable" => false,
"no_rich_text" => false,
"show_role" => true
},
"privacy" => "public",
"sensitive" => false
},
"statuses_count" => 0,
"username" => "Lain"
} = response
end
end
describe "create account by app / rate limit" do
setup do: clear_config([:rate_limit, :app_account_creation], {10_000, 2})
test "respects rate limit setting", %{conn: conn} do
app_token = insert(:oauth_token, user: nil)
conn =
conn
|> put_req_header("authorization", "Bearer " <> app_token.token)
|> Map.put(:remote_ip, {15, 15, 15, 15})
|> put_req_header("content-type", "multipart/form-data")
for i <- 1..2 do
conn =
conn
|> post("/api/v1/accounts", %{
username: "#{i}lain",
email: "#{i}lain@example.org",
password: "PlzDontHackLain",
agreement: true
})
%{
"access_token" => token,
"created_at" => _created_at,
"scope" => _scope,
"token_type" => "Bearer"
} = json_response_and_validate_schema(conn, 200)
token_from_db = Repo.get_by(Token, token: token)
assert token_from_db
token_from_db = Repo.preload(token_from_db, :user)
assert token_from_db.user
end
conn =
post(conn, "/api/v1/accounts", %{
username: "6lain",
email: "6lain@example.org",
password: "PlzDontHackLain",
agreement: true
})
assert json_response_and_validate_schema(conn, :too_many_requests) == %{
"error" => "Throttled"
}
end
end
describe "create account with enabled captcha" do
setup %{conn: conn} do
app_token = insert(:oauth_token, user: nil)
conn =
conn
|> put_req_header("authorization", "Bearer " <> app_token.token)
|> put_req_header("content-type", "multipart/form-data")
[conn: conn]
end
setup do: clear_config([Pleroma.Captcha, :enabled], true)
test "creates an account and returns 200 if captcha is valid", %{conn: conn} do
%{token: token, answer_data: answer_data} = Pleroma.Captcha.new()
params = %{
username: "lain",
email: "lain@example.org",
password: "PlzDontHackLain",
agreement: true,
captcha_solution: Pleroma.Captcha.Mock.solution(),
captcha_token: token,
captcha_answer_data: answer_data
}
assert %{
"access_token" => access_token,
"created_at" => _,
"scope" => "read",
"token_type" => "Bearer"
} =
conn
|> post("/api/v1/accounts", params)
|> json_response_and_validate_schema(:ok)
assert Token |> Repo.get_by(token: access_token) |> Repo.preload(:user) |> Map.get(:user)
end
test "returns 400 if any captcha field is not provided", %{conn: conn} do
captcha_fields = [:captcha_solution, :captcha_token, :captcha_answer_data]
valid_params = %{
username: "lain",
email: "lain@example.org",
password: "PlzDontHackLain",
agreement: true,
captcha_solution: "xx",
captcha_token: "xx",
captcha_answer_data: "xx"
}
for field <- captcha_fields do
expected = %{
"error" => "{\"captcha\":[\"Invalid CAPTCHA (Missing parameter: #{field})\"]}"
}
assert expected ==
conn
|> post("/api/v1/accounts", Map.delete(valid_params, field))
|> json_response_and_validate_schema(:bad_request)
end
end
test "returns an error if captcha is invalid", %{conn: conn} do
params = %{
username: "lain",
email: "lain@example.org",
password: "PlzDontHackLain",
agreement: true,
captcha_solution: "cofe",
captcha_token: "cofe",
captcha_answer_data: "cofe"
}
assert %{"error" => "{\"captcha\":[\"Invalid answer data\"]}"} ==
conn
|> post("/api/v1/accounts", params)
|> json_response_and_validate_schema(:bad_request)
end
end
describe "create account with required birth date" do
setup %{conn: conn} do
clear_config([:instance, :birthday_required], true)
clear_config([:instance, :birthday_min_age], 18 * 365)
app_token = insert(:oauth_token, user: nil)
conn =
conn
|> put_req_header("authorization", "Bearer " <> app_token.token)
|> put_req_header("content-type", "multipart/form-data")
[conn: conn]
end
test "creates an account if provided valid birth date", %{conn: conn} do
birthday =
Date.utc_today()
|> Date.add(-19 * 365)
|> Date.to_string()
params = %{
username: "mkljczk",
email: "mkljczk@example.org",
password: "dupa.8",
agreement: true,
birthday: birthday
}
res =
conn
|> post("/api/v1/accounts", params)
assert json_response_and_validate_schema(res, 200)
end
test "returns an error if missing birth date", %{conn: conn} do
params = %{
username: "mkljczk",
email: "mkljczk@example.org",
password: "dupa.8",
agreement: true
}
res =
conn
|> post("/api/v1/accounts", params)
assert json_response_and_validate_schema(res, 400) == %{
"error" => "{\"birthday\":[\"can't be blank\"]}"
}
end
end
describe "create account with language" do
setup %{conn: conn} do
app_token = insert(:oauth_token, user: nil)
conn =
conn
|> put_req_header("authorization", "Bearer " <> app_token.token)
|> put_req_header("content-type", "multipart/form-data")
|> put_req_cookie(SetLocalePlug.frontend_language_cookie_name(), "zh-Hans")
|> SetLocalePlug.call([])
[conn: conn]
end
test "creates an account with language parameter", %{conn: conn} do
params = %{
username: "foo",
email: "foo@example.org",
password: "dupa.8",
agreement: true,
language: "ru"
}
res =
conn
|> post("/api/v1/accounts", params)
assert json_response_and_validate_schema(res, 200)
assert %{language: "ru"} = Pleroma.User.get_by_nickname("foo")
end
test "language parameter should be normalized", %{conn: conn} do
params = %{
username: "foo",
email: "foo@example.org",
password: "dupa.8",
agreement: true,
language: "ru-RU"
}
res =
conn
|> post("/api/v1/accounts", params)
assert json_response_and_validate_schema(res, 200)
assert %{language: "ru_RU"} = Pleroma.User.get_by_nickname("foo")
end
test "creating an account without language parameter should fallback to cookie/header language",
%{conn: conn} do
params = %{
username: "foo2",
email: "foo2@example.org",
password: "dupa.8",
agreement: true
}
res =
conn
|> post("/api/v1/accounts", params)
assert json_response_and_validate_schema(res, 200)
assert %{language: "zh_Hans"} = Pleroma.User.get_by_nickname("foo2")
end
end
describe "GET /api/v1/accounts/:id/lists - account_lists" do
test "returns lists to which the account belongs" do
%{user: user, conn: conn} = oauth_access(["read:lists"])
other_user = insert(:user)
assert {:ok, %Pleroma.List{id: _list_id} = list} = Pleroma.List.create(%{title: "Test List"}, user)
{:ok, %{following: _following}} = Pleroma.List.follow(list, other_user)
assert [%{"id" => _list_id, "title" => "Test List"}] =
conn
|> get("/api/v1/accounts/#{other_user.id}/lists")
|> json_response_and_validate_schema(200)
end
end
describe "verify_credentials" do
test "verify_credentials" do
%{user: user, conn: conn} = oauth_access(["read:accounts"])
[notification | _] =
insert_list(7, :notification, user: user, activity: insert(:note_activity))
Pleroma.Notification.set_read_up_to(user, notification.id)
conn = get(conn, "/api/v1/accounts/verify_credentials")
response = json_response_and_validate_schema(conn, 200)
assert %{"id" => id, "source" => %{"privacy" => "public"}} = response
assert response["pleroma"]["chat_token"]
assert response["pleroma"]["unread_notifications_count"] == 6
assert id == to_string(user.id)
end
test "verify_credentials default scope unlisted" do
user = insert(:user, default_scope: "unlisted")
%{conn: conn} = oauth_access(["read:accounts"], user: user)
conn = get(conn, "/api/v1/accounts/verify_credentials")
assert %{"id" => id, "source" => %{"privacy" => "unlisted"}} =
json_response_and_validate_schema(conn, 200)
assert id == to_string(user.id)
end
test "locked accounts" do
user = insert(:user, default_scope: "private")
%{conn: conn} = oauth_access(["read:accounts"], user: user)
conn = get(conn, "/api/v1/accounts/verify_credentials")
assert %{"id" => id, "source" => %{"privacy" => "private"}} =
json_response_and_validate_schema(conn, 200)
assert id == to_string(user.id)
end
end
describe "user relationships" do
setup do: oauth_access(["read:follows"])
test "returns the relationships for the current user", %{user: user, conn: conn} do
%{id: other_user_id} = other_user = insert(:user)
{:ok, _user, _other_user} = User.follow(user, other_user)
assert [%{"id" => ^other_user_id}] =
conn
|> get("/api/v1/accounts/relationships?id=#{other_user.id}")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^other_user_id}] =
conn
|> get("/api/v1/accounts/relationships?id[]=#{other_user.id}")
|> json_response_and_validate_schema(200)
end
test "returns an empty list on a bad request", %{conn: conn} do
conn = get(conn, "/api/v1/accounts/relationships", %{})
assert [] = json_response_and_validate_schema(conn, 200)
end
end
test "getting a list of mutes" do
%{user: user, conn: conn} = oauth_access(["read:mutes"])
%{id: id1} = other_user1 = insert(:user)
%{id: id2} = other_user2 = insert(:user)
%{id: id3} = other_user3 = insert(:user)
{:ok, _user_relationships} = User.mute(user, other_user1)
{:ok, _user_relationships} = User.mute(user, other_user2)
{:ok, _user_relationships} = User.mute(user, other_user3, %{duration: 24 * 60 * 60})
date =
DateTime.utc_now()
|> DateTime.add(24 * 60 * 60)
|> DateTime.truncate(:second)
|> DateTime.to_iso8601()
result =
conn
|> get("/api/v1/mutes")
|> json_response_and_validate_schema(200)
assert [id3, id2, id1] == Enum.map(result, & &1["id"])
result =
conn
|> get("/api/v1/mutes?limit=1")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^id3}] = result
result =
conn
|> get("/api/v1/mutes?since_id=#{id1}")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^id3}, %{"id" => ^id2}] = result
result =
conn
|> get("/api/v1/mutes?since_id=#{id1}&max_id=#{id3}")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^id2}] = result
result =
conn
|> get("/api/v1/mutes?since_id=#{id1}&limit=1")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^id3}] = result
result =
conn
|> get("/api/v1/mutes")
|> json_response_and_validate_schema(200)
assert [
%{"id" => ^id3, "mute_expires_at" => ^date},
%{"id" => ^id2, "mute_expires_at" => nil},
%{"id" => ^id1, "mute_expires_at" => nil}
] = result
end
test "list of mutes with with_relationships parameter" do
%{user: user, conn: conn} = oauth_access(["read:mutes"])
%{id: id1} = other_user1 = insert(:user)
%{id: id2} = other_user2 = insert(:user)
%{id: id3} = other_user3 = insert(:user)
{:ok, _, _} = User.follow(other_user1, user)
{:ok, _, _} = User.follow(other_user2, user)
{:ok, _, _} = User.follow(other_user3, user)
{:ok, _} = User.mute(user, other_user1)
{:ok, _} = User.mute(user, other_user2)
{:ok, _} = User.mute(user, other_user3, %{duration: 24 * 60 * 60})
date =
DateTime.utc_now()
|> DateTime.add(24 * 60 * 60)
|> DateTime.truncate(:second)
|> DateTime.to_iso8601()
assert [
%{
"id" => ^id3,
"pleroma" => %{
"relationship" => %{
"muting" => true,
"mute_expires_at" => ^date,
"followed_by" => true
}
}
},
%{
"id" => ^id2,
"pleroma" => %{
"relationship" => %{
"muting" => true,
"mute_expires_at" => nil,
"followed_by" => true
}
}
},
%{
"id" => ^id1,
"pleroma" => %{
"relationship" => %{
"muting" => true,
"mute_expires_at" => nil,
"followed_by" => true
}
}
}
] =
conn
|> get("/api/v1/mutes?with_relationships=true")
|> json_response_and_validate_schema(200)
end
test "getting a list of blocks" do
%{user: user, conn: conn} = oauth_access(["read:blocks"])
%{id: id1} = other_user1 = insert(:user)
%{id: id2} = other_user2 = insert(:user)
%{id: id3} = other_user3 = insert(:user)
{:ok, _user_relationship} = User.block(user, other_user1)
{:ok, _user_relationship} = User.block(user, other_user3)
{:ok, _user_relationship} = User.block(user, other_user2, %{duration: 24 * 60 * 60})
date =
DateTime.utc_now()
|> DateTime.add(24 * 60 * 60)
|> DateTime.truncate(:second)
|> DateTime.to_iso8601()
result =
conn
|> assign(:user, user)
|> get("/api/v1/blocks")
|> json_response_and_validate_schema(200)
assert [id3, id2, id1] == Enum.map(result, & &1["id"])
result =
conn
|> assign(:user, user)
|> get("/api/v1/blocks?limit=1")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^id3}] = result
result =
conn
|> assign(:user, user)
|> get("/api/v1/blocks?since_id=#{id1}")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^id3}, %{"id" => ^id2}] = result
result =
conn
|> assign(:user, user)
|> get("/api/v1/blocks?since_id=#{id1}&max_id=#{id3}")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^id2}] = result
result =
conn
|> assign(:user, user)
|> get("/api/v1/blocks?since_id=#{id1}&limit=1")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^id3}] = result
conn_res =
conn
|> assign(:user, user)
|> get("/api/v1/blocks?limit=2")
next_url =
~r{<.+?(?<link>/api[^>]+)>; rel=\"next\"}
|> Regex.named_captures(get_resp_header(conn_res, "link") |> Enum.at(0))
|> Map.get("link")
result =
conn_res
|> json_response_and_validate_schema(200)
assert [%{"id" => ^id3}, %{"id" => ^id2}] = result
result =
conn
|> assign(:user, user)
|> get(next_url)
|> json_response_and_validate_schema(200)
assert [%{"id" => ^id1}] = result
result =
conn
|> assign(:user, user)
- |> get("api/v1/blocks")
+ |> get("/api/v1/blocks")
|> json_response_and_validate_schema(200)
assert [
%{"id" => ^id3, "block_expires_at" => nil},
%{"id" => ^id2, "block_expires_at" => ^date},
%{"id" => ^id1, "block_expires_at" => nil}
] = result
end
test "list of blocks with with_relationships parameter" do
%{user: user, conn: conn} = oauth_access(["read:blocks"])
%{id: id1} = other_user1 = insert(:user)
%{id: id2} = other_user2 = insert(:user)
%{id: id3} = other_user3 = insert(:user)
{:ok, _, _} = User.follow(other_user1, user)
{:ok, _, _} = User.follow(other_user2, user)
{:ok, _, _} = User.follow(other_user3, user)
{:ok, _} = User.block(user, other_user1)
{:ok, _} = User.block(user, other_user2)
{:ok, _} = User.block(user, other_user3, %{duration: 24 * 60 * 60})
date =
DateTime.utc_now()
|> DateTime.add(24 * 60 * 60)
|> DateTime.truncate(:second)
|> DateTime.to_iso8601()
assert [
%{
"id" => ^id3,
"pleroma" => %{
"relationship" => %{
"blocking" => true,
"block_expires_at" => ^date,
"followed_by" => false
}
}
},
%{
"id" => ^id2,
"pleroma" => %{
"relationship" => %{
"blocking" => true,
"block_expires_at" => nil,
"followed_by" => false
}
}
},
%{
"id" => ^id1,
"pleroma" => %{
"relationship" => %{
"blocking" => true,
"block_expires_at" => nil,
"followed_by" => false
}
}
}
] =
conn
|> get("/api/v1/blocks?with_relationships=true")
|> json_response_and_validate_schema(200)
end
test "account lookup", %{conn: conn} do
%{nickname: acct} = insert(:user, %{nickname: "nickname"})
%{nickname: acct_two} = insert(:user, %{nickname: "nickname@notlocaldoma.in"})
result =
conn
|> get("/api/v1/accounts/lookup?acct=#{acct}")
|> json_response_and_validate_schema(200)
assert %{"acct" => ^acct} = result
result =
conn
|> get("/api/v1/accounts/lookup?acct=#{acct_two}")
|> json_response_and_validate_schema(200)
assert %{"acct" => ^acct_two} = result
_result =
conn
|> get("/api/v1/accounts/lookup?acct=unexisting_nickname")
|> json_response_and_validate_schema(404)
end
test "account lookup with restrict unauthenticated profiles for local" do
clear_config([:restrict_unauthenticated, :profiles, :local], true)
user = insert(:user, local: true)
reading_user = insert(:user)
conn =
build_conn()
|> get("/api/v1/accounts/lookup?acct=#{user.nickname}")
assert json_response_and_validate_schema(conn, 401)
conn =
build_conn()
|> assign(:user, reading_user)
|> assign(:token, insert(:oauth_token, user: reading_user, scopes: ["read:accounts"]))
|> get("/api/v1/accounts/lookup?acct=#{user.nickname}")
assert %{"id" => id} = json_response_and_validate_schema(conn, 200)
assert id == user.id
end
test "account lookup with restrict unauthenticated profiles for remote" do
clear_config([:restrict_unauthenticated, :profiles, :remote], true)
user = insert(:user, nickname: "user@example.com", local: false)
reading_user = insert(:user)
conn =
build_conn()
|> get("/api/v1/accounts/lookup?acct=#{user.nickname}")
assert json_response_and_validate_schema(conn, 401)
conn =
build_conn()
|> assign(:user, reading_user)
|> assign(:token, insert(:oauth_token, user: reading_user, scopes: ["read:accounts"]))
|> get("/api/v1/accounts/lookup?acct=#{user.nickname}")
assert %{"id" => id} = json_response_and_validate_schema(conn, 200)
assert id == user.id
end
test "create a note on a user" do
%{conn: conn} = oauth_access(["write:accounts", "read:follows"])
other_user = insert(:user)
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/accounts/#{other_user.id}/note", %{
"comment" => "Example note"
})
assert [%{"note" => "Example note"}] =
conn
|> put_req_header("content-type", "application/json")
|> get("/api/v1/accounts/relationships?id=#{other_user.id}")
|> json_response_and_validate_schema(200)
end
describe "account endorsements" do
setup do: oauth_access(["read:accounts", "write:accounts", "write:follows"])
setup do: clear_config([:instance, :max_endorsed_users], 1)
test "pin account", %{user: user, conn: conn} do
%{id: id1} = other_user1 = insert(:user)
CommonAPI.follow(other_user1, user)
assert %{"id" => ^id1, "endorsed" => true} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/accounts/#{id1}/endorse")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^id1}] =
conn
|> put_req_header("content-type", "application/json")
|> get("/api/v1/endorsements")
|> json_response_and_validate_schema(200)
end
test "unpin account", %{user: user, conn: conn} do
%{id: id1} = other_user1 = insert(:user)
CommonAPI.follow(other_user1, user)
User.endorse(user, other_user1)
assert %{"id" => ^id1, "endorsed" => false} =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/accounts/#{id1}/unendorse")
|> json_response_and_validate_schema(200)
assert [] =
conn
|> put_req_header("content-type", "application/json")
|> get("/api/v1/endorsements")
|> json_response_and_validate_schema(200)
end
test "max pinned accounts", %{user: user, conn: conn} do
%{id: id1} = other_user1 = insert(:user)
%{id: id2} = other_user2 = insert(:user)
CommonAPI.follow(other_user1, user)
CommonAPI.follow(other_user2, user)
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/accounts/#{id1}/endorse")
|> json_response_and_validate_schema(200)
assert %{"error" => "You have already pinned the maximum number of users"} =
conn
|> assign(:user, user)
|> post("/api/v1/accounts/#{id2}/endorse")
|> json_response_and_validate_schema(400)
end
test "returns a list of pinned accounts", %{conn: conn} do
clear_config([:instance, :max_endorsed_users], 3)
%{id: id1} = user1 = insert(:user)
%{id: id2} = user2 = insert(:user)
%{id: id3} = user3 = insert(:user)
CommonAPI.follow(user2, user1)
CommonAPI.follow(user3, user1)
User.endorse(user1, user2)
User.endorse(user1, user3)
[%{"id" => ^id2}, %{"id" => ^id3}] =
conn
|> get("/api/v1/accounts/#{id1}/endorsements")
|> json_response_and_validate_schema(200)
end
test "returns 404 error when specified user is not exist", %{conn: conn} do
conn = get(conn, "/api/v1/accounts/test/endorsements")
assert json_response_and_validate_schema(conn, 404) == %{"error" => "Record not found"}
end
end
describe "familiar followers" do
setup do: oauth_access(["read:follows"])
test "fetch user familiar followers", %{user: user, conn: conn} do
%{id: id1} = other_user1 = insert(:user)
%{id: id2} = other_user2 = insert(:user)
_ = insert(:user)
User.follow(user, other_user1)
User.follow(other_user1, other_user2)
assert [%{"accounts" => [%{"id" => ^id1}], "id" => ^id2}] =
conn
|> put_req_header("content-type", "application/json")
|> get("/api/v1/accounts/familiar_followers?id[]=#{id2}")
|> json_response_and_validate_schema(200)
end
test "returns empty array if followers are hidden", %{user: user, conn: conn} do
other_user1 = insert(:user, hide_follows: true)
%{id: id2} = other_user2 = insert(:user)
_ = insert(:user)
User.follow(user, other_user1)
User.follow(other_user1, other_user2)
assert [%{"accounts" => [], "id" => ^id2}] =
conn
|> put_req_header("content-type", "application/json")
|> get("/api/v1/accounts/familiar_followers?id[]=#{id2}")
|> json_response_and_validate_schema(200)
end
test "it respects hide_followers", %{user: user, conn: conn} do
other_user1 = insert(:user)
%{id: id2} = other_user2 = insert(:user, hide_followers: true)
_ = insert(:user)
User.follow(user, other_user1)
User.follow(other_user1, other_user2)
assert [%{"accounts" => [], "id" => ^id2}] =
conn
|> put_req_header("content-type", "application/json")
|> get("/api/v1/accounts/familiar_followers?id[]=#{id2}")
|> json_response_and_validate_schema(200)
end
end
describe "remove from followers" do
setup do: oauth_access(["follow"])
test "removing user from followers", %{conn: conn, user: user} do
%{id: other_user_id} = other_user = insert(:user)
CommonAPI.follow(user, other_user)
assert %{"id" => ^other_user_id, "followed_by" => false} =
conn
|> post("/api/v1/accounts/#{other_user_id}/remove_from_followers")
|> json_response_and_validate_schema(200)
refute User.following?(other_user, user)
end
test "removing remote user from followers", %{conn: conn, user: user} do
%{id: other_user_id} = other_user = insert(:user, local: false)
CommonAPI.follow(user, other_user)
assert User.following?(other_user, user)
assert %{"id" => ^other_user_id, "followed_by" => false} =
conn
|> post("/api/v1/accounts/#{other_user_id}/remove_from_followers")
|> json_response_and_validate_schema(200)
refute User.following?(other_user, user)
end
test "removing user from followers errors", %{user: user, conn: conn} do
# self remove
conn_res = post(conn, "/api/v1/accounts/#{user.id}/remove_from_followers")
assert %{"error" => "Can not unfollow yourself"} =
json_response_and_validate_schema(conn_res, 400)
# remove non existing user
conn_res = post(conn, "/api/v1/accounts/doesntexist/remove_from_followers")
assert %{"error" => "Record not found"} = json_response_and_validate_schema(conn_res, 404)
end
end
end
diff --git a/test/pleroma/web/twitter_api/password_controller_test.exs b/test/pleroma/web/o_auth/password_controller_test.exs
similarity index 99%
rename from test/pleroma/web/twitter_api/password_controller_test.exs
rename to test/pleroma/web/o_auth/password_controller_test.exs
index 26cca1345..bb61b24fd 100644
--- a/test/pleroma/web/twitter_api/password_controller_test.exs
+++ b/test/pleroma/web/o_auth/password_controller_test.exs
@@ -1,213 +1,213 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Web.TwitterAPI.PasswordControllerTest do
+defmodule Pleroma.Web.OAuth.PasswordControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.Config
alias Pleroma.PasswordResetToken
alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.OAuth.Token
import Pleroma.Factory
import Swoosh.TestAssertions
describe "GET /api/pleroma/password_reset/token" do
test "it returns error when token invalid", %{conn: conn} do
response =
conn
|> get("/api/pleroma/password_reset/token")
|> html_response(:ok)
assert response =~ "<h2>Invalid Token</h2>"
end
test "it shows password reset form", %{conn: conn} do
user = insert(:user)
{:ok, token} = PasswordResetToken.create_token(user)
response =
conn
|> get("/api/pleroma/password_reset/#{token.token}")
|> html_response(:ok)
assert response =~ "<h2>Password Reset for #{user.nickname}</h2>"
end
test "it returns an error when the token has expired", %{conn: conn} do
clear_config([:instance, :password_reset_token_validity], 0)
user = insert(:user)
{:ok, token} = PasswordResetToken.create_token(user)
{:ok, token} = time_travel(token, -2)
response =
conn
|> get("/api/pleroma/password_reset/#{token.token}")
|> html_response(:ok)
assert response =~ "<h2>Invalid Token</h2>"
end
end
describe "POST /api/pleroma/password_reset" do
test "it fails for an expired token", %{conn: conn} do
clear_config([:instance, :password_reset_token_validity], 0)
user = insert(:user)
{:ok, token} = PasswordResetToken.create_token(user)
{:ok, token} = time_travel(token, -2)
{:ok, _access_token} = Token.create(insert(:oauth_app), user, %{})
params = %{
"password" => "test",
password_confirmation: "test",
token: token.token
}
response =
conn
|> assign(:user, user)
|> post("/api/pleroma/password_reset", %{data: params})
|> html_response(:ok)
refute response =~ "<h2>Password changed!</h2>"
end
test "it returns HTTP 200", %{conn: conn} do
user = insert(:user)
{:ok, token} = PasswordResetToken.create_token(user)
{:ok, _access_token} = Token.create(insert(:oauth_app), user, %{})
params = %{
"password" => "test",
password_confirmation: "test",
token: token.token
}
response =
conn
|> assign(:user, user)
|> post("/api/pleroma/password_reset", %{data: params})
|> html_response(:ok)
assert response =~ "<h2>Password changed!</h2>"
user = refresh_record(user)
assert Pleroma.Password.Pbkdf2.verify_pass("test", user.password_hash)
assert Enum.empty?(Token.get_user_tokens(user))
end
test "it sets password_reset_pending to false", %{conn: conn} do
user = insert(:user, password_reset_pending: true)
{:ok, token} = PasswordResetToken.create_token(user)
{:ok, _access_token} = Token.create(insert(:oauth_app), user, %{})
params = %{
"password" => "test",
password_confirmation: "test",
token: token.token
}
conn
|> assign(:user, user)
|> post("/api/pleroma/password_reset", %{data: params})
|> html_response(:ok)
assert User.get_by_id(user.id).password_reset_pending == false
end
end
describe "POST /auth/password, with valid parameters" do
setup %{conn: conn} do
user = insert(:user)
conn = post(conn, "/auth/password?email=#{user.email}")
%{conn: conn, user: user}
end
test "it returns 204", %{conn: conn} do
assert empty_json_response(conn)
end
test "it creates a PasswordResetToken record for user", %{user: user} do
token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id)
assert token_record
end
test "it sends an email to user", %{user: user} do
ObanHelpers.perform_all()
token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id)
email = Pleroma.Emails.UserEmail.password_reset_email(user, token_record.token)
notify_email = Config.get([:instance, :notify_email])
instance_name = Config.get([:instance, :name])
assert_email_sent(
from: {instance_name, notify_email},
to: {user.name, user.email},
html_body: email.html_body
)
end
end
describe "POST /auth/password, with nickname" do
test "it returns 204", %{conn: conn} do
user = insert(:user)
assert conn
|> post("/auth/password?nickname=#{user.nickname}")
|> empty_json_response()
ObanHelpers.perform_all()
token_record = Repo.get_by(Pleroma.PasswordResetToken, user_id: user.id)
email = Pleroma.Emails.UserEmail.password_reset_email(user, token_record.token)
notify_email = Config.get([:instance, :notify_email])
instance_name = Config.get([:instance, :name])
assert_email_sent(
from: {instance_name, notify_email},
to: {user.name, user.email},
html_body: email.html_body
)
end
test "it doesn't fail when a user has no email", %{conn: conn} do
user = insert(:user, %{email: nil})
assert conn
|> post("/auth/password?nickname=#{user.nickname}")
|> empty_json_response()
end
end
describe "POST /auth/password, with invalid parameters" do
setup do
user = insert(:user)
{:ok, user: user}
end
test "it returns 204 when user is not found", %{conn: conn, user: user} do
conn = post(conn, "/auth/password?email=nonexisting_#{user.email}")
assert empty_json_response(conn)
end
test "it returns 204 when user is not local", %{conn: conn, user: user} do
{:ok, user} = Repo.update(Ecto.Changeset.change(user, local: false))
conn = post(conn, "/auth/password?email=#{user.email}")
assert empty_json_response(conn)
end
test "it returns 204 when user is deactivated", %{conn: conn, user: user} do
{:ok, user} = Repo.update(Ecto.Changeset.change(user, is_active: false, local: true))
conn = post(conn, "/auth/password?email=#{user.email}")
assert empty_json_response(conn)
end
end
end
diff --git a/test/pleroma/web/twitter_api/controller_test.exs b/test/pleroma/web/o_auth/token_controller_test.exs
similarity index 97%
rename from test/pleroma/web/twitter_api/controller_test.exs
rename to test/pleroma/web/o_auth/token_controller_test.exs
index 494be9ec7..5c64cb394 100644
--- a/test/pleroma/web/twitter_api/controller_test.exs
+++ b/test/pleroma/web/o_auth/token_controller_test.exs
@@ -1,87 +1,87 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Web.TwitterAPI.ControllerTest do
+defmodule Pleroma.Web.OAuth.TokenControllerTest do
use Pleroma.Web.ConnCase, async: true
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.OAuth.Token
import Pleroma.Factory
describe "GET /api/account/confirm_email/:id/:token" do
setup do
{:ok, user} =
insert(:user)
|> User.confirmation_changeset(set_confirmation: false)
|> Repo.update()
refute user.is_confirmed
[user: user]
end
test "it redirects to root url", %{conn: conn, user: user} do
conn = get(conn, "/api/account/confirm_email/#{user.id}/#{user.confirmation_token}")
assert 302 == conn.status
end
test "it confirms the user account", %{conn: conn, user: user} do
get(conn, "/api/account/confirm_email/#{user.id}/#{user.confirmation_token}")
user = User.get_cached_by_id(user.id)
assert user.is_confirmed
refute user.confirmation_token
end
test "it returns 500 if user cannot be found by id", %{conn: conn, user: user} do
conn = get(conn, "/api/account/confirm_email/0/#{user.confirmation_token}")
assert 500 == conn.status
end
test "it returns 500 if token is invalid", %{conn: conn, user: user} do
conn = get(conn, "/api/account/confirm_email/#{user.id}/wrong_token")
assert 500 == conn.status
end
end
describe "GET /api/oauth_tokens" do
setup do
token = insert(:oauth_token) |> Repo.preload(:user)
%{token: token}
end
test "renders list", %{token: token} do
response =
build_conn()
|> assign(:user, token.user)
|> get("/api/oauth_tokens")
keys =
json_response(response, 200)
|> hd()
|> Map.keys()
assert Enum.sort(keys) == Enum.sort(["id", "app_name", "valid_until", "scopes"])
end
test "revoke token", %{token: token} do
response =
build_conn()
|> assign(:user, token.user)
|> delete("/api/oauth_tokens/#{token.id}")
tokens = Token.get_user_tokens(token.user)
assert tokens == []
assert response.status == 201
end
end
end
diff --git a/test/pleroma/web/twitter_api/util_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/util_controller_test.exs
similarity index 85%
rename from test/pleroma/web/twitter_api/util_controller_test.exs
rename to test/pleroma/web/pleroma_api/controllers/util_controller_test.exs
index d06ae71aa..6e817df56 100644
--- a/test/pleroma/web/twitter_api/util_controller_test.exs
+++ b/test/pleroma/web/pleroma_api/controllers/util_controller_test.exs
@@ -1,982 +1,835 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
+defmodule Pleroma.Web.PleromaAPI.UtilControllerTest do
use Pleroma.Web.ConnCase
use Oban.Testing, repo: Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
import Pleroma.Factory
import Mock
setup do
Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
setup do: clear_config([:instance])
setup do: clear_config([:frontend_configurations, :pleroma_fe])
describe "PUT /api/pleroma/notification_settings" do
setup do: oauth_access(["write:accounts"])
test "it updates notification settings", %{user: user, conn: conn} do
conn
|> put(
"/api/pleroma/notification_settings?#{URI.encode_query(%{block_from_strangers: true})}"
)
|> json_response_and_validate_schema(:ok)
user = refresh_record(user)
assert %Pleroma.User.NotificationSetting{
block_from_strangers: true,
hide_notification_contents: false
} == user.notification_settings
end
test "it updates notification settings to enable hiding contents", %{user: user, conn: conn} do
conn
|> put(
"/api/pleroma/notification_settings?#{URI.encode_query(%{hide_notification_contents: 1})}"
)
|> json_response_and_validate_schema(:ok)
user = refresh_record(user)
assert %Pleroma.User.NotificationSetting{
block_from_strangers: false,
hide_notification_contents: true
} == user.notification_settings
end
end
describe "GET /api/pleroma/frontend_configurations" do
test "returns everything in :pleroma, :frontend_configurations", %{conn: conn} do
config = [
frontend_a: %{
x: 1,
y: 2
},
frontend_b: %{
z: 3
}
]
clear_config(:frontend_configurations, config)
response =
conn
|> get("/api/pleroma/frontend_configurations")
|> json_response_and_validate_schema(:ok)
assert response == Jason.encode!(config |> Enum.into(%{})) |> Jason.decode!()
end
end
describe "/api/pleroma/emoji" do
test "returns json with custom emoji with tags", %{conn: conn} do
emoji =
conn
|> get("/api/pleroma/emoji")
|> json_response_and_validate_schema(200)
assert Enum.all?(emoji, fn
{_key,
%{
"image_url" => url,
"tags" => tags
}} ->
is_binary(url) and is_list(tags)
end)
end
end
describe "GET /api/pleroma/healthcheck" do
setup do: clear_config([:instance, :healthcheck])
test "returns 503 when healthcheck disabled", %{conn: conn} do
clear_config([:instance, :healthcheck], false)
response =
conn
|> get("/api/pleroma/healthcheck")
|> json_response_and_validate_schema(503)
assert response == %{"error" => "Healthcheck disabled"}
end
test "returns 200 when healthcheck enabled and all ok", %{conn: conn} do
clear_config([:instance, :healthcheck], true)
with_mock Pleroma.Healthcheck,
system_info: fn -> %Pleroma.Healthcheck{healthy: true} end do
response =
conn
|> get("/api/pleroma/healthcheck")
|> json_response_and_validate_schema(200)
assert %{
"active" => _,
"healthy" => true,
"idle" => _,
"memory_used" => _,
"pool_size" => _
} = response
end
end
test "returns 503 when healthcheck enabled and health is false", %{conn: conn} do
clear_config([:instance, :healthcheck], true)
with_mock Pleroma.Healthcheck,
system_info: fn -> %Pleroma.Healthcheck{healthy: false} end do
response =
conn
|> get("/api/pleroma/healthcheck")
|> json_response_and_validate_schema(503)
assert %{
"active" => _,
"healthy" => false,
"idle" => _,
"memory_used" => _,
"pool_size" => _
} = response
end
end
end
describe "POST /api/pleroma/disable_account" do
setup do: oauth_access(["write:accounts"])
test "with valid permissions and password, it disables the account", %{conn: conn, user: user} do
response =
conn
|> post("/api/pleroma/disable_account?password=test")
|> json_response_and_validate_schema(:ok)
assert response == %{"status" => "success"}
ObanHelpers.perform_all()
user = User.get_cached_by_id(user.id)
refute user.is_active
end
test "with valid permissions and invalid password, it returns an error", %{conn: conn} do
user = insert(:user)
response =
conn
|> post("/api/pleroma/disable_account?password=test1")
|> json_response_and_validate_schema(:ok)
assert response == %{"error" => "Invalid password."}
user = User.get_cached_by_id(user.id)
assert user.is_active
end
end
- describe "POST /main/ostatus - remote_subscribe/2" do
- setup do: clear_config([:instance, :federating], true)
-
- test "renders subscribe form", %{conn: conn} do
- user = insert(:user)
-
- response =
- conn
- |> post("/main/ostatus", %{"nickname" => user.nickname, "profile" => ""})
- |> response(:ok)
-
- refute response =~ "Could not find user"
- assert response =~ "Remotely follow #{user.nickname}"
- end
-
- test "renders subscribe form with error when user not found", %{conn: conn} do
- response =
- conn
- |> post("/main/ostatus", %{"nickname" => "nickname", "profile" => ""})
- |> response(:ok)
-
- assert response =~ "Could not find user"
- refute response =~ "Remotely follow"
- end
-
- test "it redirect to webfinger url", %{conn: conn} do
- user = insert(:user)
- user2 = insert(:user, ap_id: "shp@social.heldscal.la")
-
- conn =
- conn
- |> post("/main/ostatus", %{
- "user" => %{"nickname" => user.nickname, "profile" => user2.ap_id}
- })
-
- assert redirected_to(conn) ==
- "https://social.heldscal.la/main/ostatussub?profile=#{user.ap_id}"
- end
-
- test "it renders form with error when user not found", %{conn: conn} do
- user2 = insert(:user, ap_id: "shp@social.heldscal.la")
-
- response =
- conn
- |> post("/main/ostatus", %{"user" => %{"nickname" => "jimm", "profile" => user2.ap_id}})
- |> response(:ok)
-
- assert response =~ "Something went wrong."
- end
- end
-
- describe "POST /main/ostatus - remote_subscribe/2 - with statuses" do
- setup do: clear_config([:instance, :federating], true)
-
- test "renders subscribe form", %{conn: conn} do
- user = insert(:user)
- status = insert(:note_activity, %{user: user})
- status_id = status.id
-
- assert is_binary(status_id)
-
- response =
- conn
- |> post("/main/ostatus", %{"status_id" => status_id, "profile" => ""})
- |> response(:ok)
-
- refute response =~ "Could not find status"
- assert response =~ "Interacting with"
- end
-
- test "renders subscribe form with error when status not found", %{conn: conn} do
- response =
- conn
- |> post("/main/ostatus", %{"status_id" => "somerandomid", "profile" => ""})
- |> response(:ok)
-
- assert response =~ "Could not find status"
- refute response =~ "Interacting with"
- end
-
- test "it redirect to webfinger url", %{conn: conn} do
- user = insert(:user)
- status = insert(:note_activity, %{user: user})
- status_id = status.id
- status_ap_id = status.data["object"]
-
- assert is_binary(status_id)
- assert is_binary(status_ap_id)
-
- user2 = insert(:user, ap_id: "shp@social.heldscal.la")
-
- conn =
- conn
- |> post("/main/ostatus", %{
- "status" => %{"status_id" => status_id, "profile" => user2.ap_id}
- })
-
- assert redirected_to(conn) ==
- "https://social.heldscal.la/main/ostatussub?profile=#{status_ap_id}"
- end
-
- test "it renders form with error when status not found", %{conn: conn} do
- user2 = insert(:user, ap_id: "shp@social.heldscal.la")
-
- response =
- conn
- |> post("/main/ostatus", %{
- "status" => %{"status_id" => "somerandomid", "profile" => user2.ap_id}
- })
- |> response(:ok)
-
- assert response =~ "Something went wrong."
- end
- end
-
- describe "GET /main/ostatus - show_subscribe_form/2" do
- setup do: clear_config([:instance, :federating], true)
-
- test "it works with users", %{conn: conn} do
- user = insert(:user)
-
- response =
- conn
- |> get("/main/ostatus", %{"nickname" => user.nickname})
- |> response(:ok)
-
- refute response =~ "Could not find user"
- assert response =~ "Remotely follow #{user.nickname}"
- end
-
- test "it works with statuses", %{conn: conn} do
- user = insert(:user)
- status = insert(:note_activity, %{user: user})
- status_id = status.id
-
- assert is_binary(status_id)
-
- response =
- conn
- |> get("/main/ostatus", %{"status_id" => status_id})
- |> response(:ok)
-
- refute response =~ "Could not find status"
- assert response =~ "Interacting with"
- end
- end
-
test "it returns new captcha", %{conn: conn} do
with_mock Pleroma.Captcha,
new: fn -> "test_captcha" end do
resp =
conn
|> get("/api/pleroma/captcha")
|> response(200)
assert resp == "\"test_captcha\""
assert called(Pleroma.Captcha.new())
end
end
describe "POST /api/pleroma/change_email" do
setup do: oauth_access(["write:accounts"])
test "without permissions", %{conn: conn} do
conn =
conn
|> assign(:token, nil)
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/change_email", %{password: "hi", email: "test@test.com"})
assert json_response_and_validate_schema(conn, 403) == %{
"error" => "Insufficient permissions: write:accounts."
}
end
test "with proper permissions and invalid password", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/change_email", %{password: "hi", email: "test@test.com"})
assert json_response_and_validate_schema(conn, 200) == %{"error" => "Invalid password."}
end
test "with proper permissions, valid password and invalid email", %{
conn: conn
} do
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/change_email", %{password: "test", email: "foobar"})
assert json_response_and_validate_schema(conn, 200) == %{
"error" => "Email has invalid format."
}
end
test "with proper permissions, valid password and no email", %{
conn: conn
} do
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/change_email", %{password: "test"})
assert %{"error" => "Missing field: email."} = json_response_and_validate_schema(conn, 400)
end
test "with proper permissions, valid password and blank email, when instance requires user email",
%{
conn: conn
} do
orig_account_activation_required =
Pleroma.Config.get([:instance, :account_activation_required])
Pleroma.Config.put([:instance, :account_activation_required], true)
on_exit(fn ->
Pleroma.Config.put(
[:instance, :account_activation_required],
orig_account_activation_required
)
end)
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/change_email", %{password: "test", email: ""})
assert json_response_and_validate_schema(conn, 200) == %{"error" => "Email can't be blank."}
end
test "with proper permissions, valid password and blank email, when instance does not require user email",
%{
conn: conn
} do
orig_account_activation_required =
Pleroma.Config.get([:instance, :account_activation_required])
Pleroma.Config.put([:instance, :account_activation_required], false)
on_exit(fn ->
Pleroma.Config.put(
[:instance, :account_activation_required],
orig_account_activation_required
)
end)
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/change_email", %{password: "test", email: ""})
assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"}
end
test "with proper permissions, valid password and non unique email", %{
conn: conn
} do
user = insert(:user)
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/change_email", %{password: "test", email: user.email})
assert json_response_and_validate_schema(conn, 200) == %{
"error" => "Email has already been taken."
}
end
test "with proper permissions, valid password and valid email", %{
conn: conn
} do
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/change_email", %{password: "test", email: "cofe@foobar.com"})
assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"}
end
end
describe "POST /api/pleroma/change_password" do
setup do: oauth_access(["write:accounts"])
test "without permissions", %{conn: conn} do
conn =
conn
|> assign(:token, nil)
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/change_password", %{
"password" => "hi",
"new_password" => "newpass",
"new_password_confirmation" => "newpass"
})
assert json_response_and_validate_schema(conn, 403) == %{
"error" => "Insufficient permissions: write:accounts."
}
end
test "with proper permissions and invalid password", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/change_password", %{
"password" => "hi",
"new_password" => "newpass",
"new_password_confirmation" => "newpass"
})
assert json_response_and_validate_schema(conn, 200) == %{"error" => "Invalid password."}
end
test "with proper permissions, valid password and new password and confirmation not matching",
%{
conn: conn
} do
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/change_password", %{
"password" => "test",
"new_password" => "newpass",
"new_password_confirmation" => "notnewpass"
})
assert json_response_and_validate_schema(conn, 200) == %{
"error" => "New password does not match confirmation."
}
end
test "with proper permissions, valid password and invalid new password", %{
conn: conn
} do
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/change_password", %{
password: "test",
new_password: "",
new_password_confirmation: ""
})
assert json_response_and_validate_schema(conn, 200) == %{
"error" => "New password can't be blank."
}
end
test "with proper permissions, valid password and matching new password and confirmation", %{
conn: conn,
user: user
} do
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post(
"/api/pleroma/change_password",
%{
password: "test",
new_password: "newpass",
new_password_confirmation: "newpass"
}
)
assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"}
fetched_user = User.get_cached_by_id(user.id)
assert Pleroma.Password.Pbkdf2.verify_pass("newpass", fetched_user.password_hash) == true
end
end
describe "POST /api/pleroma/delete_account" do
setup do: oauth_access(["write:accounts"])
test "without permissions", %{conn: conn} do
conn =
conn
|> assign(:token, nil)
|> post("/api/pleroma/delete_account")
assert json_response_and_validate_schema(conn, 403) ==
%{"error" => "Insufficient permissions: write:accounts."}
end
test "with proper permissions and wrong or missing password", %{conn: conn} do
for params <- [%{"password" => "hi"}, %{}] do
ret_conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/pleroma/delete_account", params)
assert json_response_and_validate_schema(ret_conn, 200) == %{
"error" => "Invalid password."
}
end
end
test "with proper permissions and valid password (URL query)", %{conn: conn, user: user} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/pleroma/delete_account?password=test")
ObanHelpers.perform_all()
assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"}
user = User.get_by_id(user.id)
refute user.is_active
assert user.name == nil
assert user.bio == ""
assert user.password_hash == nil
end
test "with proper permissions and valid password (JSON body)", %{conn: conn, user: user} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/pleroma/delete_account", %{password: "test"})
ObanHelpers.perform_all()
assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"}
user = User.get_by_id(user.id)
refute user.is_active
assert user.name == nil
assert user.bio == ""
assert user.password_hash == nil
end
end
describe "POST /api/pleroma/move_account" do
setup do: oauth_access(["write:accounts"])
test "without permissions", %{conn: conn} do
target_user = insert(:user)
target_nick = target_user |> User.full_nickname()
conn =
conn
|> assign(:token, nil)
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/move_account", %{
"password" => "hi",
"target_account" => target_nick
})
assert json_response_and_validate_schema(conn, 403) == %{
"error" => "Insufficient permissions: write:accounts."
}
end
test "with proper permissions and invalid password", %{conn: conn} do
target_user = insert(:user)
target_nick = target_user |> User.full_nickname()
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/move_account", %{
"password" => "hi",
"target_account" => target_nick
})
assert json_response_and_validate_schema(conn, 200) == %{"error" => "Invalid password."}
end
test "with proper permissions, valid password and target account does not alias it",
%{
conn: conn
} do
target_user = insert(:user)
target_nick = target_user |> User.full_nickname()
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/move_account", %{
"password" => "test",
"target_account" => target_nick
})
assert json_response_and_validate_schema(conn, 200) == %{
"error" => "Target account must have the origin in `alsoKnownAs`"
}
end
test "with proper permissions, valid password and target account does not exist",
%{
conn: conn
} do
target_nick = "not_found@mastodon.social"
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/move_account", %{
"password" => "test",
"target_account" => target_nick
})
assert json_response_and_validate_schema(conn, 404) == %{
"error" => "Target account not found."
}
end
test "with proper permissions, valid password, remote target account aliases it and local cache does not exist",
%{} do
user = insert(:user, ap_id: "https://lm.kazv.moe/users/testuser")
%{user: _user, conn: conn} = oauth_access(["write:accounts"], user: user)
target_nick = "mewmew@lm.kazv.moe"
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/move_account", %{
"password" => "test",
"target_account" => target_nick
})
assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"}
end
test "with proper permissions, valid password, remote target account aliases it and local cache does not aliases it",
%{} do
user = insert(:user, ap_id: "https://lm.kazv.moe/users/testuser")
%{user: _user, conn: conn} = oauth_access(["write:accounts"], user: user)
target_user =
insert(
:user,
ap_id: "https://lm.kazv.moe/users/mewmew",
nickname: "mewmew@lm.kazv.moe",
local: false
)
target_nick = target_user |> User.full_nickname()
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/move_account", %{
"password" => "test",
"target_account" => target_nick
})
assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"}
end
test "with proper permissions, valid password, remote target account does not aliases it and local cache aliases it",
%{
user: user,
conn: conn
} do
target_user =
insert(
:user,
ap_id: "https://lm.kazv.moe/users/mewmew",
nickname: "mewmew@lm.kazv.moe",
local: false,
also_known_as: [user.ap_id]
)
target_nick = target_user |> User.full_nickname()
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post("/api/pleroma/move_account", %{
"password" => "test",
"target_account" => target_nick
})
assert json_response_and_validate_schema(conn, 200) == %{
"error" => "Target account must have the origin in `alsoKnownAs`"
}
end
test "with proper permissions, valid password and target account aliases it", %{
conn: conn,
user: user
} do
target_user = insert(:user, also_known_as: [user.ap_id])
target_nick = target_user |> User.full_nickname()
follower = insert(:user)
User.follow(follower, user)
assert User.following?(follower, user)
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post(
"/api/pleroma/move_account",
%{
password: "test",
target_account: target_nick
}
)
assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"}
params = %{
"op" => "move_following",
"origin_id" => user.id,
"target_id" => target_user.id
}
assert_enqueued(worker: Pleroma.Workers.BackgroundWorker, args: params)
Pleroma.Workers.BackgroundWorker.perform(%Oban.Job{args: params})
refute User.following?(follower, user)
assert User.following?(follower, target_user)
end
test "prefix nickname by @ should work", %{
conn: conn,
user: user
} do
target_user = insert(:user, also_known_as: [user.ap_id])
target_nick = target_user |> User.full_nickname()
follower = insert(:user)
User.follow(follower, user)
assert User.following?(follower, user)
conn =
conn
|> put_req_header("content-type", "multipart/form-data")
|> post(
"/api/pleroma/move_account",
%{
password: "test",
target_account: "@" <> target_nick
}
)
assert json_response_and_validate_schema(conn, 200) == %{"status" => "success"}
params = %{
"op" => "move_following",
"origin_id" => user.id,
"target_id" => target_user.id
}
assert_enqueued(worker: Pleroma.Workers.BackgroundWorker, args: params)
Pleroma.Workers.BackgroundWorker.perform(%Oban.Job{args: params})
refute User.following?(follower, user)
assert User.following?(follower, target_user)
end
end
describe "GET /api/pleroma/aliases" do
setup do: oauth_access(["read:accounts"])
test "without permissions", %{conn: conn} do
conn =
conn
|> assign(:token, nil)
|> get("/api/pleroma/aliases")
assert json_response_and_validate_schema(conn, 403) == %{
"error" => "Insufficient permissions: read:accounts."
}
end
test "with permissions", %{
conn: conn
} do
assert %{"aliases" => []} =
conn
|> get("/api/pleroma/aliases")
|> json_response_and_validate_schema(200)
end
test "with permissions and aliases", %{} do
user = insert(:user)
user2 = insert(:user)
assert {:ok, user} = user |> User.add_alias(user2)
%{user: _user, conn: conn} = oauth_access(["read:accounts"], user: user)
assert %{"aliases" => aliases} =
conn
|> get("/api/pleroma/aliases")
|> json_response_and_validate_schema(200)
assert aliases == [user2 |> User.full_nickname()]
end
end
describe "PUT /api/pleroma/aliases" do
setup do: oauth_access(["write:accounts"])
test "without permissions", %{conn: conn} do
conn =
conn
|> assign(:token, nil)
|> put_req_header("content-type", "application/json")
|> put("/api/pleroma/aliases", %{alias: "none"})
assert json_response_and_validate_schema(conn, 403) == %{
"error" => "Insufficient permissions: write:accounts."
}
end
test "with permissions, no alias param", %{
conn: conn
} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> put("/api/pleroma/aliases", %{})
assert %{"error" => "Missing field: alias."} = json_response_and_validate_schema(conn, 400)
end
test "with permissions, with alias param", %{
conn: conn
} do
user2 = insert(:user)
conn =
conn
|> put_req_header("content-type", "application/json")
|> put("/api/pleroma/aliases", %{alias: user2 |> User.full_nickname()})
assert json_response_and_validate_schema(conn, 200) == %{
"status" => "success"
}
end
end
describe "DELETE /api/pleroma/aliases" do
setup do
alias_user = insert(:user)
non_alias_user = insert(:user)
user = insert(:user, also_known_as: [alias_user.ap_id])
oauth_access(["write:accounts"], user: user)
|> Map.put(:alias_user, alias_user)
|> Map.put(:non_alias_user, non_alias_user)
end
test "without permissions", %{conn: conn} do
conn =
conn
|> assign(:token, nil)
|> put_req_header("content-type", "application/json")
|> delete("/api/pleroma/aliases", %{alias: "none"})
assert json_response_and_validate_schema(conn, 403) == %{
"error" => "Insufficient permissions: write:accounts."
}
end
test "with permissions, no alias param", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> delete("/api/pleroma/aliases", %{})
assert %{"error" => "Missing field: alias."} = json_response_and_validate_schema(conn, 400)
end
test "with permissions, account does not have such alias", %{
conn: conn,
non_alias_user: non_alias_user
} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> delete("/api/pleroma/aliases", %{alias: non_alias_user |> User.full_nickname()})
assert %{"error" => "Account has no such alias."} =
json_response_and_validate_schema(conn, 404)
end
test "with permissions, account does have such alias", %{
conn: conn,
alias_user: alias_user
} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> delete("/api/pleroma/aliases", %{alias: alias_user |> User.full_nickname()})
assert %{"status" => "success"} = json_response_and_validate_schema(conn, 200)
end
end
end
diff --git a/test/pleroma/web/twitter_api/twitter_api_test.exs b/test/pleroma/web/registration_test.exs
similarity index 90%
rename from test/pleroma/web/twitter_api/twitter_api_test.exs
rename to test/pleroma/web/registration_test.exs
index b3cd80146..896e1a600 100644
--- a/test/pleroma/web/twitter_api/twitter_api_test.exs
+++ b/test/pleroma/web/registration_test.exs
@@ -1,424 +1,424 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
+defmodule Pleroma.Web.RegistrationTest do
use Pleroma.DataCase
import Pleroma.Factory
alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.UserInviteToken
- alias Pleroma.Web.TwitterAPI.TwitterAPI
+ alias Pleroma.Web.Registration
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
test "it registers a new user and returns the user." do
data = %{
:username => "lain",
:email => "lain@wired.jp",
:fullname => "lain iwakura",
:password => "bear",
:confirm => "bear"
}
- {:ok, user} = TwitterAPI.register_user(data)
+ {:ok, user} = Registration.register_user(data)
assert user == User.get_cached_by_nickname("lain")
end
test "it registers a new user with empty string in bio and returns the user" do
data = %{
:username => "lain",
:email => "lain@wired.jp",
:fullname => "lain iwakura",
:bio => "",
:password => "bear",
:confirm => "bear"
}
- {:ok, user} = TwitterAPI.register_user(data)
+ {:ok, user} = Registration.register_user(data)
assert user == User.get_cached_by_nickname("lain")
end
test "it sends confirmation email if :account_activation_required is specified in instance config" do
clear_config([:instance, :account_activation_required], true)
data = %{
:username => "lain",
:email => "lain@wired.jp",
:fullname => "lain iwakura",
:bio => "",
:password => "bear",
:confirm => "bear"
}
- {:ok, user} = TwitterAPI.register_user(data)
+ {:ok, user} = Registration.register_user(data)
ObanHelpers.perform_all()
refute user.is_confirmed
email = Pleroma.Emails.UserEmail.account_confirmation_email(user)
notify_email = Pleroma.Config.get([:instance, :notify_email])
instance_name = Pleroma.Config.get([:instance, :name])
Swoosh.TestAssertions.assert_email_sent(
from: {instance_name, notify_email},
to: {user.name, user.email},
html_body: email.html_body
)
end
test "it sends an admin email if :account_approval_required is specified in instance config" do
clear_config([:instance, :account_approval_required], true)
admin = insert(:user, is_admin: true)
data = %{
:username => "lain",
:email => "lain@wired.jp",
:fullname => "lain iwakura",
:bio => "",
:password => "bear",
:confirm => "bear",
:reason => "I love anime"
}
- {:ok, user} = TwitterAPI.register_user(data)
+ {:ok, user} = Registration.register_user(data)
ObanHelpers.perform_all()
refute user.is_approved
user_email = Pleroma.Emails.UserEmail.approval_pending_email(user)
admin_email = Pleroma.Emails.AdminEmail.new_unapproved_registration(admin, user)
notify_email = Pleroma.Config.get([:instance, :notify_email])
instance_name = Pleroma.Config.get([:instance, :name])
# User approval email
Swoosh.TestAssertions.assert_email_sent(
from: {instance_name, notify_email},
to: {user.name, user.email},
html_body: user_email.html_body
)
# Admin email
Swoosh.TestAssertions.assert_email_sent(
from: {instance_name, notify_email},
to: {admin.name, admin.email},
html_body: admin_email.html_body
)
end
test "it registers a new user and parses mentions in the bio" do
data1 = %{
:username => "john",
:email => "john@gmail.com",
:fullname => "John Doe",
:bio => "test",
:password => "bear",
:confirm => "bear"
}
- {:ok, user1} = TwitterAPI.register_user(data1)
+ {:ok, user1} = Registration.register_user(data1)
data2 = %{
:username => "lain",
:email => "lain@wired.jp",
:fullname => "lain iwakura",
:bio => "@john test",
:password => "bear",
:confirm => "bear"
}
- {:ok, user2} = TwitterAPI.register_user(data2)
+ {:ok, user2} = Registration.register_user(data2)
expected_text =
~s(<span class="h-card"><a class="u-url mention" data-user="#{user1.id}" href="#{user1.ap_id}" rel="ugc">@<span>john</span></a></span> test)
assert user2.bio == expected_text
end
describe "register with one time token" do
setup do: clear_config([:instance, :registrations_open], false)
test "returns user on success" do
{:ok, invite} = UserInviteToken.create_invite()
data = %{
:username => "vinny",
:email => "pasta@pizza.vs",
:fullname => "Vinny Vinesauce",
:bio => "streamer",
:password => "hiptofbees",
:confirm => "hiptofbees",
:token => invite.token
}
- {:ok, user} = TwitterAPI.register_user(data)
+ {:ok, user} = Registration.register_user(data)
assert user == User.get_cached_by_nickname("vinny")
invite = Repo.get_by(UserInviteToken, token: invite.token)
assert invite.used == true
end
test "returns error on invalid token" do
data = %{
:username => "GrimReaper",
:email => "death@reapers.afterlife",
:fullname => "Reaper Grim",
:bio => "Your time has come",
:password => "scythe",
:confirm => "scythe",
:token => "DudeLetMeInImAFairy"
}
- {:error, msg} = TwitterAPI.register_user(data)
+ {:error, msg} = Registration.register_user(data)
assert msg == "Invalid token"
refute User.get_cached_by_nickname("GrimReaper")
end
test "returns error on expired token" do
{:ok, invite} = UserInviteToken.create_invite()
UserInviteToken.update_invite!(invite, used: true)
data = %{
:username => "GrimReaper",
:email => "death@reapers.afterlife",
:fullname => "Reaper Grim",
:bio => "Your time has come",
:password => "scythe",
:confirm => "scythe",
:token => invite.token
}
- {:error, msg} = TwitterAPI.register_user(data)
+ {:error, msg} = Registration.register_user(data)
assert msg == "Expired token"
refute User.get_cached_by_nickname("GrimReaper")
end
end
describe "registers with date limited token" do
setup do: clear_config([:instance, :registrations_open], false)
setup do
data = %{
:username => "vinny",
:email => "pasta@pizza.vs",
:fullname => "Vinny Vinesauce",
:bio => "streamer",
:password => "hiptofbees",
:confirm => "hiptofbees"
}
check_fn = fn invite ->
data = Map.put(data, :token, invite.token)
- {:ok, user} = TwitterAPI.register_user(data)
+ {:ok, user} = Registration.register_user(data)
assert user == User.get_cached_by_nickname("vinny")
end
{:ok, data: data, check_fn: check_fn}
end
test "returns user on success", %{check_fn: check_fn} do
{:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today()})
check_fn.(invite)
invite = Repo.get_by(UserInviteToken, token: invite.token)
refute invite.used
end
test "returns user on token which expired tomorrow", %{check_fn: check_fn} do
{:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), 1)})
check_fn.(invite)
invite = Repo.get_by(UserInviteToken, token: invite.token)
refute invite.used
end
test "returns an error on overdue date", %{data: data} do
{:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1)})
data = Map.put(data, "token", invite.token)
- {:error, msg} = TwitterAPI.register_user(data)
+ {:error, msg} = Registration.register_user(data)
assert msg == "Expired token"
refute User.get_cached_by_nickname("vinny")
invite = Repo.get_by(UserInviteToken, token: invite.token)
refute invite.used
end
end
describe "registers with reusable token" do
setup do: clear_config([:instance, :registrations_open], false)
test "returns user on success, after him registration fails" do
{:ok, invite} = UserInviteToken.create_invite(%{max_use: 100})
UserInviteToken.update_invite!(invite, uses: 99)
data = %{
:username => "vinny",
:email => "pasta@pizza.vs",
:fullname => "Vinny Vinesauce",
:bio => "streamer",
:password => "hiptofbees",
:confirm => "hiptofbees",
:token => invite.token
}
- {:ok, user} = TwitterAPI.register_user(data)
+ {:ok, user} = Registration.register_user(data)
assert user == User.get_cached_by_nickname("vinny")
invite = Repo.get_by(UserInviteToken, token: invite.token)
assert invite.used == true
data = %{
:username => "GrimReaper",
:email => "death@reapers.afterlife",
:fullname => "Reaper Grim",
:bio => "Your time has come",
:password => "scythe",
:confirm => "scythe",
:token => invite.token
}
- {:error, msg} = TwitterAPI.register_user(data)
+ {:error, msg} = Registration.register_user(data)
assert msg == "Expired token"
refute User.get_cached_by_nickname("GrimReaper")
end
end
describe "registers with reusable date limited token" do
setup do: clear_config([:instance, :registrations_open], false)
test "returns user on success" do
{:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 100})
data = %{
:username => "vinny",
:email => "pasta@pizza.vs",
:fullname => "Vinny Vinesauce",
:bio => "streamer",
:password => "hiptofbees",
:confirm => "hiptofbees",
:token => invite.token
}
- {:ok, user} = TwitterAPI.register_user(data)
+ {:ok, user} = Registration.register_user(data)
assert user == User.get_cached_by_nickname("vinny")
invite = Repo.get_by(UserInviteToken, token: invite.token)
refute invite.used
end
test "error after max uses" do
{:ok, invite} = UserInviteToken.create_invite(%{expires_at: Date.utc_today(), max_use: 100})
UserInviteToken.update_invite!(invite, uses: 99)
data = %{
:username => "vinny",
:email => "pasta@pizza.vs",
:fullname => "Vinny Vinesauce",
:bio => "streamer",
:password => "hiptofbees",
:confirm => "hiptofbees",
:token => invite.token
}
- {:ok, user} = TwitterAPI.register_user(data)
+ {:ok, user} = Registration.register_user(data)
assert user == User.get_cached_by_nickname("vinny")
invite = Repo.get_by(UserInviteToken, token: invite.token)
assert invite.used == true
data = %{
:username => "GrimReaper",
:email => "death@reapers.afterlife",
:fullname => "Reaper Grim",
:bio => "Your time has come",
:password => "scythe",
:confirm => "scythe",
:token => invite.token
}
- {:error, msg} = TwitterAPI.register_user(data)
+ {:error, msg} = Registration.register_user(data)
assert msg == "Expired token"
refute User.get_cached_by_nickname("GrimReaper")
end
test "returns error on overdue date" do
{:ok, invite} =
UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1), max_use: 100})
data = %{
:username => "GrimReaper",
:email => "death@reapers.afterlife",
:fullname => "Reaper Grim",
:bio => "Your time has come",
:password => "scythe",
:confirm => "scythe",
:token => invite.token
}
- {:error, msg} = TwitterAPI.register_user(data)
+ {:error, msg} = Registration.register_user(data)
assert msg == "Expired token"
refute User.get_cached_by_nickname("GrimReaper")
end
test "returns error on with overdue date and after max" do
{:ok, invite} =
UserInviteToken.create_invite(%{expires_at: Date.add(Date.utc_today(), -1), max_use: 100})
UserInviteToken.update_invite!(invite, uses: 100)
data = %{
:username => "GrimReaper",
:email => "death@reapers.afterlife",
:fullname => "Reaper Grim",
:bio => "Your time has come",
:password => "scythe",
:confirm => "scythe",
:token => invite.token
}
- {:error, msg} = TwitterAPI.register_user(data)
+ {:error, msg} = Registration.register_user(data)
assert msg == "Expired token"
refute User.get_cached_by_nickname("GrimReaper")
end
end
test "it returns the error on registration problems" do
data = %{
:username => "lain",
:email => "lain@wired.jp",
:fullname => "lain iwakura",
:bio => "close the world."
}
- {:error, error} = TwitterAPI.register_user(data)
+ {:error, error} = Registration.register_user(data)
assert is_binary(error)
refute User.get_cached_by_nickname("lain")
end
end
diff --git a/test/pleroma/web/twitter_api/remote_follow_controller_test.exs b/test/pleroma/web/remote_interaction/remote_interaction_controller_test.exs
similarity index 68%
rename from test/pleroma/web/twitter_api/remote_follow_controller_test.exs
rename to test/pleroma/web/remote_interaction/remote_interaction_controller_test.exs
index f762b1356..9df86c0a2 100644
--- a/test/pleroma/web/twitter_api/remote_follow_controller_test.exs
+++ b/test/pleroma/web/remote_interaction/remote_interaction_controller_test.exs
@@ -1,497 +1,649 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
-defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do
+defmodule Pleroma.Web.RemoteInteraction.RemoteInteractionControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.MFA
alias Pleroma.MFA.TOTP
alias Pleroma.UnstubbedConfigMock, as: ConfigMock
alias Pleroma.User
alias Pleroma.Web.CommonAPI
import Ecto.Query
import ExUnit.CaptureLog
import Mox
import Pleroma.Factory
+ setup do
+ Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
+ :ok
+ end
+
setup_all do: clear_config([:instance, :federating], true)
setup do: clear_config([:user, :deny_follow_blocked])
describe "GET /ostatus_subscribe - remote_follow/2" do
test "adds status to pleroma instance if the `acct` is a status", %{conn: conn} do
Tesla.Mock.mock(fn
%{method: :get, url: "https://mastodon.social/users/emelie/statuses/101849165031453009"} ->
%Tesla.Env{
status: 200,
headers: [{"content-type", "application/activity+json"}],
body: File.read!("test/fixtures/tesla_mock/status.emelie.json")
}
%{method: :get, url: "https://mastodon.social/users/emelie/collections/featured"} ->
%Tesla.Env{
status: 200,
headers: [{"content-type", "application/activity+json"}],
body:
File.read!("test/fixtures/users_mock/masto_featured.json")
|> String.replace("{{domain}}", "mastodon.social")
|> String.replace("{{nickname}}", "emelie")
}
%{method: :get, url: "https://mastodon.social/users/emelie"} ->
%Tesla.Env{
status: 200,
headers: [{"content-type", "application/activity+json"}],
body: File.read!("test/fixtures/tesla_mock/emelie.json")
}
end)
assert conn
|> get(
- remote_follow_path(conn, :follow, %{
+ remote_interaction_path(conn, :follow, %{
acct: "https://mastodon.social/users/emelie/statuses/101849165031453009"
})
)
|> redirected_to() =~ "/notice/"
end
test "show follow account page if the `acct` is a account link", %{conn: conn} do
Tesla.Mock.mock(fn
%{method: :get, url: "https://mastodon.social/users/emelie"} ->
%Tesla.Env{
status: 200,
headers: [{"content-type", "application/activity+json"}],
body: File.read!("test/fixtures/tesla_mock/emelie.json")
}
%{method: :get, url: "https://mastodon.social/users/emelie/collections/featured"} ->
%Tesla.Env{
status: 200,
headers: [{"content-type", "application/activity+json"}],
body:
File.read!("test/fixtures/users_mock/masto_featured.json")
|> String.replace("{{domain}}", "mastodon.social")
|> String.replace("{{nickname}}", "emelie")
}
end)
response =
conn
- |> get(remote_follow_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"}))
+ |> get(remote_interaction_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"}))
|> html_response(200)
assert response =~ "Log in to follow"
end
test "show follow page if the `acct` is a account link", %{conn: conn} do
Tesla.Mock.mock(fn
%{method: :get, url: "https://mastodon.social/users/emelie"} ->
%Tesla.Env{
status: 200,
headers: [{"content-type", "application/activity+json"}],
body: File.read!("test/fixtures/tesla_mock/emelie.json")
}
%{method: :get, url: "https://mastodon.social/users/emelie/collections/featured"} ->
%Tesla.Env{
status: 200,
headers: [{"content-type", "application/activity+json"}],
body:
File.read!("test/fixtures/users_mock/masto_featured.json")
|> String.replace("{{domain}}", "mastodon.social")
|> String.replace("{{nickname}}", "emelie")
}
end)
user = insert(:user)
response =
conn
|> assign(:user, user)
- |> get(remote_follow_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"}))
+ |> get(remote_interaction_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"}))
|> html_response(200)
assert response =~ "Remote follow"
end
test "show follow page with error when user can not be fetched by `acct` link", %{conn: conn} do
Tesla.Mock.mock(fn
%{method: :get, url: "https://mastodon.social/users/not_found"} ->
%Tesla.Env{
status: 404
}
end)
user = insert(:user)
assert capture_log(fn ->
response =
conn
|> assign(:user, user)
|> get(
- remote_follow_path(conn, :follow, %{
+ remote_interaction_path(conn, :follow, %{
acct: "https://mastodon.social/users/not_found"
})
)
|> html_response(200)
assert response =~ "Error fetching user"
end) =~ ":not_found"
end
end
describe "POST /ostatus_subscribe - do_follow/2 with assigned user " do
test "required `follow | write:follows` scope", %{conn: conn} do
user = insert(:user)
user2 = insert(:user)
read_token = insert(:oauth_token, user: user, scopes: ["read"])
assert capture_log(fn ->
response =
conn
|> assign(:user, user)
|> assign(:token, read_token)
- |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}})
+ |> post(remote_interaction_path(conn, :do_follow), %{"user" => %{"id" => user2.id}})
|> response(200)
assert response =~ "Error following account"
end) =~ "Insufficient permissions: follow | write:follows."
end
test "follows user", %{conn: conn} do
user = insert(:user)
user2 = insert(:user)
conn =
conn
|> assign(:user, user)
|> assign(:token, insert(:oauth_token, user: user, scopes: ["write:follows"]))
- |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}})
+ |> post(remote_interaction_path(conn, :do_follow), %{"user" => %{"id" => user2.id}})
assert redirected_to(conn) == "/users/#{user2.id}"
end
test "returns error when user is deactivated", %{conn: conn} do
user = insert(:user, is_active: false)
user2 = insert(:user)
response =
conn
|> assign(:user, user)
- |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}})
+ |> post(remote_interaction_path(conn, :do_follow), %{"user" => %{"id" => user2.id}})
|> response(200)
assert response =~ "Error following account"
end
test "returns error when user is blocked", %{conn: conn} do
clear_config([:user, :deny_follow_blocked], true)
user = insert(:user)
user2 = insert(:user)
{:ok, _user_block} = Pleroma.User.block(user2, user)
response =
conn
|> assign(:user, user)
- |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}})
+ |> post(remote_interaction_path(conn, :do_follow), %{"user" => %{"id" => user2.id}})
|> response(200)
assert response =~ "Error following account"
end
test "returns error when followee not found", %{conn: conn} do
user = insert(:user)
response =
conn
|> assign(:user, user)
- |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => "jimm"}})
+ |> post(remote_interaction_path(conn, :do_follow), %{"user" => %{"id" => "jimm"}})
|> response(200)
assert response =~ "Error following account"
end
test "returns success result when user already in followers", %{conn: conn} do
user = insert(:user)
user2 = insert(:user)
{:ok, _, _, _} = CommonAPI.follow(user2, user)
conn =
conn
|> assign(:user, refresh_record(user))
|> assign(:token, insert(:oauth_token, user: user, scopes: ["write:follows"]))
- |> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}})
+ |> post(remote_interaction_path(conn, :do_follow), %{"user" => %{"id" => user2.id}})
assert redirected_to(conn) == "/users/#{user2.id}"
end
end
describe "POST /ostatus_subscribe - follow/2 with enabled Two-Factor Auth " do
test "render the MFA login form", %{conn: conn} do
otp_secret = TOTP.generate_secret()
user =
insert(:user,
multi_factor_authentication_settings: %MFA.Settings{
enabled: true,
totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
}
)
user2 = insert(:user)
response =
conn
- |> post(remote_follow_path(conn, :do_follow), %{
+ |> post(remote_interaction_path(conn, :do_follow), %{
"authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id}
})
|> response(200)
mfa_token = Pleroma.Repo.one(from(q in Pleroma.MFA.Token, where: q.user_id == ^user.id))
assert response =~ "Two-factor authentication"
assert response =~ "Authentication code"
assert response =~ mfa_token.token
refute user2.follower_address in User.following(user)
end
test "returns error when password is incorrect", %{conn: conn} do
otp_secret = TOTP.generate_secret()
user =
insert(:user,
multi_factor_authentication_settings: %MFA.Settings{
enabled: true,
totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
}
)
user2 = insert(:user)
response =
conn
- |> post(remote_follow_path(conn, :do_follow), %{
+ |> post(remote_interaction_path(conn, :do_follow), %{
"authorization" => %{"name" => user.nickname, "password" => "test1", "id" => user2.id}
})
|> response(200)
assert response =~ "Wrong username or password"
refute user2.follower_address in User.following(user)
end
test "follows", %{conn: conn} do
otp_secret = TOTP.generate_secret()
user =
insert(:user,
multi_factor_authentication_settings: %MFA.Settings{
enabled: true,
totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
}
)
{:ok, %{token: token}} = MFA.Token.create(user)
user2 = insert(:user)
otp_token = TOTP.generate_token(otp_secret)
conn =
conn
|> post(
- remote_follow_path(conn, :do_follow),
+ remote_interaction_path(conn, :do_follow),
%{
"mfa" => %{"code" => otp_token, "token" => token, "id" => user2.id}
}
)
assert redirected_to(conn) == "/users/#{user2.id}"
assert user2.follower_address in User.following(user)
end
test "returns error when auth code is incorrect", %{conn: conn} do
otp_secret = TOTP.generate_secret()
user =
insert(:user,
multi_factor_authentication_settings: %MFA.Settings{
enabled: true,
totp: %MFA.Settings.TOTP{secret: otp_secret, confirmed: true}
}
)
{:ok, %{token: token}} = MFA.Token.create(user)
user2 = insert(:user)
otp_token = TOTP.generate_token(TOTP.generate_secret())
response =
conn
|> post(
- remote_follow_path(conn, :do_follow),
+ remote_interaction_path(conn, :do_follow),
%{
"mfa" => %{"code" => otp_token, "token" => token, "id" => user2.id}
}
)
|> response(200)
assert response =~ "Wrong authentication code"
refute user2.follower_address in User.following(user)
end
end
describe "POST /ostatus_subscribe - follow/2 without assigned user " do
test "follows", %{conn: conn} do
user = insert(:user)
user2 = insert(:user)
conn =
conn
- |> post(remote_follow_path(conn, :do_follow), %{
+ |> post(remote_interaction_path(conn, :do_follow), %{
"authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id}
})
assert redirected_to(conn) == "/users/#{user2.id}"
assert user2.follower_address in User.following(user)
end
test "returns error when followee not found", %{conn: conn} do
user = insert(:user)
response =
conn
- |> post(remote_follow_path(conn, :do_follow), %{
+ |> post(remote_interaction_path(conn, :do_follow), %{
"authorization" => %{"name" => user.nickname, "password" => "test", "id" => "jimm"}
})
|> response(200)
assert response =~ "Error following account"
end
test "returns error when login invalid", %{conn: conn} do
user = insert(:user)
response =
conn
- |> post(remote_follow_path(conn, :do_follow), %{
+ |> post(remote_interaction_path(conn, :do_follow), %{
"authorization" => %{"name" => "jimm", "password" => "test", "id" => user.id}
})
|> response(200)
assert response =~ "Wrong username or password"
end
test "returns error when password invalid", %{conn: conn} do
user = insert(:user)
user2 = insert(:user)
response =
conn
- |> post(remote_follow_path(conn, :do_follow), %{
+ |> post(remote_interaction_path(conn, :do_follow), %{
"authorization" => %{"name" => user.nickname, "password" => "42", "id" => user2.id}
})
|> response(200)
assert response =~ "Wrong username or password"
end
test "returns error when user is blocked", %{conn: conn} do
clear_config([:user, :deny_follow_blocked], true)
user = insert(:user)
user2 = insert(:user)
{:ok, _user_block} = Pleroma.User.block(user2, user)
response =
conn
- |> post(remote_follow_path(conn, :do_follow), %{
+ |> post(remote_interaction_path(conn, :do_follow), %{
"authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id}
})
|> response(200)
assert response =~ "Error following account"
end
end
describe "avatar url" do
test "without media proxy" do
clear_config([:media_proxy, :enabled], false)
user =
insert(:user, %{
local: false,
avatar: %{"url" => [%{"href" => "https://remote.org/avatar.png"}]}
})
- avatar_url = Pleroma.Web.TwitterAPI.RemoteFollowView.avatar_url(user)
+ avatar_url = Pleroma.Web.RemoteInteraction.RemoteInteractionView.avatar_url(user)
assert avatar_url == "https://remote.org/avatar.png"
end
test "with media proxy" do
clear_config([:media_proxy, :enabled], true)
ConfigMock
|> stub_with(Pleroma.Test.StaticConfig)
user =
insert(:user, %{
local: false,
avatar: %{"url" => [%{"href" => "https://remote.org/avatar.png"}]}
})
- avatar_url = Pleroma.Web.TwitterAPI.RemoteFollowView.avatar_url(user)
+ avatar_url = Pleroma.Web.RemoteInteraction.RemoteInteractionView.avatar_url(user)
url = Pleroma.Web.Endpoint.url()
assert String.starts_with?(avatar_url, url)
end
test "local avatar is not proxied" do
clear_config([:media_proxy, :enabled], true)
user =
insert(:user, %{
local: true,
avatar: %{"url" => [%{"href" => "#{Pleroma.Web.Endpoint.url()}/localuser/avatar.png"}]}
})
- avatar_url = Pleroma.Web.TwitterAPI.RemoteFollowView.avatar_url(user)
+ avatar_url = Pleroma.Web.RemoteInteraction.RemoteInteractionView.avatar_url(user)
assert avatar_url == "#{Pleroma.Web.Endpoint.url()}/localuser/avatar.png"
end
end
describe "GET /authorize_interaction - authorize_interaction/2" do
test "redirects to /ostatus_subscribe", %{conn: conn} do
Tesla.Mock.mock(fn
%{method: :get, url: "https://mastodon.social/users/emelie"} ->
%Tesla.Env{
status: 200,
headers: [{"content-type", "application/activity+json"}],
body: File.read!("test/fixtures/tesla_mock/emelie.json")
}
%{method: :get, url: "https://mastodon.social/users/emelie/collections/featured"} ->
%Tesla.Env{
status: 200,
headers: [{"content-type", "application/activity+json"}],
body:
File.read!("test/fixtures/users_mock/masto_featured.json")
|> String.replace("{{domain}}", "mastodon.social")
|> String.replace("{{nickname}}", "emelie")
}
end)
conn =
conn
|> get(
- remote_follow_path(conn, :authorize_interaction, %{
+ remote_interaction_path(conn, :authorize_interaction, %{
uri: "https://mastodon.social/users/emelie"
})
)
assert redirected_to(conn) ==
- remote_follow_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"})
+ remote_interaction_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"})
+ end
+ end
+
+ describe "POST /main/ostatus - remote_subscribe/2" do
+ setup do: clear_config([:instance, :federating], true)
+
+ test "renders subscribe form", %{conn: conn} do
+ user = insert(:user)
+
+ response =
+ conn
+ |> post("/main/ostatus", %{"nickname" => user.nickname, "profile" => ""})
+ |> response(:ok)
+
+ refute response =~ "Could not find user"
+ assert response =~ "Remotely follow #{user.nickname}"
+ end
+
+ test "renders subscribe form with error when user not found", %{conn: conn} do
+ response =
+ conn
+ |> post("/main/ostatus", %{"nickname" => "nickname", "profile" => ""})
+ |> response(:ok)
+
+ assert response =~ "Could not find user"
+ refute response =~ "Remotely follow"
+ end
+
+ test "it redirect to webfinger url", %{conn: conn} do
+ user = insert(:user)
+ user2 = insert(:user, ap_id: "shp@social.heldscal.la")
+
+ conn =
+ conn
+ |> post("/main/ostatus", %{
+ "user" => %{"nickname" => user.nickname, "profile" => user2.ap_id}
+ })
+
+ assert redirected_to(conn) ==
+ "https://social.heldscal.la/main/ostatussub?profile=#{user.ap_id}"
+ end
+
+ test "it renders form with error when user not found", %{conn: conn} do
+ user2 = insert(:user, ap_id: "shp@social.heldscal.la")
+
+ response =
+ conn
+ |> post("/main/ostatus", %{"user" => %{"nickname" => "jimm", "profile" => user2.ap_id}})
+ |> response(:ok)
+
+ assert response =~ "Something went wrong."
+ end
+ end
+
+ describe "POST /main/ostatus - remote_subscribe/2 - with statuses" do
+ setup do: clear_config([:instance, :federating], true)
+
+ test "renders subscribe form", %{conn: conn} do
+ user = insert(:user)
+ status = insert(:note_activity, %{user: user})
+ status_id = status.id
+
+ assert is_binary(status_id)
+
+ response =
+ conn
+ |> post("/main/ostatus", %{"status_id" => status_id, "profile" => ""})
+ |> response(:ok)
+
+ refute response =~ "Could not find status"
+ assert response =~ "Interacting with"
+ end
+
+ test "renders subscribe form with error when status not found", %{conn: conn} do
+ response =
+ conn
+ |> post("/main/ostatus", %{"status_id" => "somerandomid", "profile" => ""})
+ |> response(:ok)
+
+ assert response =~ "Could not find status"
+ refute response =~ "Interacting with"
+ end
+
+ test "it redirect to webfinger url", %{conn: conn} do
+ user = insert(:user)
+ status = insert(:note_activity, %{user: user})
+ status_id = status.id
+ status_ap_id = status.data["object"]
+
+ assert is_binary(status_id)
+ assert is_binary(status_ap_id)
+
+ user2 = insert(:user, ap_id: "shp@social.heldscal.la")
+
+ conn =
+ conn
+ |> post("/main/ostatus", %{
+ "status" => %{"status_id" => status_id, "profile" => user2.ap_id}
+ })
+
+ assert redirected_to(conn) ==
+ "https://social.heldscal.la/main/ostatussub?profile=#{status_ap_id}"
+ end
+
+ test "it renders form with error when status not found", %{conn: conn} do
+ user2 = insert(:user, ap_id: "shp@social.heldscal.la")
+
+ response =
+ conn
+ |> post("/main/ostatus", %{
+ "status" => %{"status_id" => "somerandomid", "profile" => user2.ap_id}
+ })
+ |> response(:ok)
+
+ assert response =~ "Something went wrong."
+ end
+ end
+
+ describe "GET /main/ostatus - show_subscribe_form/2" do
+ setup do: clear_config([:instance, :federating], true)
+
+ test "it works with users", %{conn: conn} do
+ user = insert(:user)
+
+ response =
+ conn
+ |> get("/main/ostatus", %{"nickname" => user.nickname})
+ |> response(:ok)
+
+ refute response =~ "Could not find user"
+ assert response =~ "Remotely follow #{user.nickname}"
+ end
+
+ test "it works with statuses", %{conn: conn} do
+ user = insert(:user)
+ status = insert(:note_activity, %{user: user})
+ status_id = status.id
+
+ assert is_binary(status_id)
+
+ response =
+ conn
+ |> get("/main/ostatus", %{"status_id" => status_id})
+ |> response(:ok)
+
+ refute response =~ "Could not find status"
+ assert response =~ "Interacting with"
end
end
end

File Metadata

Mime Type
text/x-diff
Expires
Sun, Oct 11, 1:39 PM (1 d, 16 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1785829
Default Alt Text
(542 KB)

Event Timeline