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

This commit is contained in:
Sadposter 2019-03-18 13:13:07 +00:00
commit e751f3ec56
536 changed files with 4691 additions and 1670 deletions

View file

@ -69,8 +69,8 @@
# You can also customize the exit_status of each check. # You can also customize the exit_status of each check.
# If you don't want TODO comments to cause `mix credo` to fail, just # If you don't want TODO comments to cause `mix credo` to fail, just
# set this value to 0 (zero). # set this value to 0 (zero).
{Credo.Check.Design.TagTODO, exit_status: 2}, {Credo.Check.Design.TagTODO, exit_status: 0},
{Credo.Check.Design.TagFIXME}, {Credo.Check.Design.TagFIXME, exit_status: 0},
{Credo.Check.Readability.FunctionNames}, {Credo.Check.Readability.FunctionNames},
{Credo.Check.Readability.LargeNumbers}, {Credo.Check.Readability.LargeNumbers},
@ -81,7 +81,9 @@
{Credo.Check.Readability.ParenthesesOnZeroArityDefs}, {Credo.Check.Readability.ParenthesesOnZeroArityDefs},
{Credo.Check.Readability.ParenthesesInCondition}, {Credo.Check.Readability.ParenthesesInCondition},
{Credo.Check.Readability.PredicateFunctionNames}, {Credo.Check.Readability.PredicateFunctionNames},
{Credo.Check.Readability.PreferImplicitTry}, # lanodan: I think PreferImplicitTry should be consistency, and the behaviour seems
# inconsistent, see: https://github.com/rrrene/credo/issues/224
{Credo.Check.Readability.PreferImplicitTry, false},
{Credo.Check.Readability.RedundantBlankLines}, {Credo.Check.Readability.RedundantBlankLines},
{Credo.Check.Readability.StringSigils}, {Credo.Check.Readability.StringSigils},
{Credo.Check.Readability.TrailingBlankLine}, {Credo.Check.Readability.TrailingBlankLine},
@ -126,10 +128,6 @@
# Deprecated checks (these will be deleted after a grace period) # Deprecated checks (these will be deleted after a grace period)
{Credo.Check.Readability.Specs, false}, {Credo.Check.Readability.Specs, false},
{Credo.Check.Warning.NameRedeclarationByAssignment, false},
{Credo.Check.Warning.NameRedeclarationByCase, false},
{Credo.Check.Warning.NameRedeclarationByDef, false},
{Credo.Check.Warning.NameRedeclarationByFn, false},
# Custom checks can be created using `mix credo.gen.check`. # Custom checks can be created using `mix credo.gen.check`.
# #

View file

@ -1,4 +1,4 @@
image: elixir:1.7.2 image: elixir:1.8.1
services: services:
- name: postgres:9.6.2 - name: postgres:9.6.2
@ -19,6 +19,7 @@ cache:
stages: stages:
- lint - lint
- test - test
- analysis
before_script: before_script:
- mix local.hex --force - mix local.hex --force
@ -37,3 +38,8 @@ unit-testing:
stage: test stage: test
script: script:
- mix test --trace --preload-modules - mix test --trace --preload-modules
analysis:
stage: analysis
script:
- mix credo --strict --only=warnings,todo,fixme,consistency,readability

View file

@ -1,12 +0,0 @@
Unliking:
- Add a proper undo activity, find out how to ignore those in twitter api.
WEBSUB:
- Add unsubscription
OSTATUS:
- Save and output 'updated'

View file

@ -34,7 +34,8 @@
# Upload configuration # Upload configuration
config :pleroma, Pleroma.Upload, config :pleroma, Pleroma.Upload,
uploader: Pleroma.Uploaders.Local, uploader: Pleroma.Uploaders.Local,
filters: [], filters: [Pleroma.Upload.Filter.Dedupe],
link_name: true,
proxy_remote: false, proxy_remote: false,
proxy_opts: [ proxy_opts: [
redirect_on_failure: false, redirect_on_failure: false,
@ -93,10 +94,11 @@
dispatch: [ dispatch: [
{:_, {:_,
[ [
{"/api/v1/streaming", Elixir.Pleroma.Web.MastodonAPI.WebsocketHandler, []}, {"/api/v1/streaming", Pleroma.Web.MastodonAPI.WebsocketHandler, []},
{"/socket/websocket", Phoenix.Endpoint.CowboyWebSocket, {"/websocket", Phoenix.Endpoint.CowboyWebSocket,
{nil, {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, websocket_config}}}, {Phoenix.Transports.WebSocket,
{:_, Plug.Adapters.Cowboy.Handler, {Pleroma.Web.Endpoint, []}} {Pleroma.Web.Endpoint, Pleroma.Web.UserSocket, websocket_config}}},
{:_, Phoenix.Endpoint.Cowboy2Handler, {Pleroma.Web.Endpoint, []}}
]} ]}
] ]
], ],
@ -132,7 +134,14 @@
config :tesla, adapter: Tesla.Adapter.Hackney config :tesla, adapter: Tesla.Adapter.Hackney
# Configures http settings, upstream proxy etc. # Configures http settings, upstream proxy etc.
config :pleroma, :http, proxy_url: nil config :pleroma, :http,
proxy_url: nil,
adapter: [
ssl_options: [
# We don't support TLS v1.3 yet
versions: [:tlsv1, :"tlsv1.1", :"tlsv1.2"]
]
]
config :pleroma, :instance, config :pleroma, :instance,
name: "Pleroma", name: "Pleroma",
@ -214,6 +223,9 @@
scopeCopy: true, scopeCopy: true,
subjectLineBehavior: "email", subjectLineBehavior: "email",
alwaysShowSubjectInput: true alwaysShowSubjectInput: true
},
masto_fe: %{
showInstanceSpecificPanel: true
} }
config :pleroma, :activitypub, config :pleroma, :activitypub,
@ -344,6 +356,31 @@
federator_outgoing: [max_jobs: 50], federator_outgoing: [max_jobs: 50],
mailer: [max_jobs: 10] mailer: [max_jobs: 10]
config :pleroma, :fetch_initial_posts,
enabled: false,
pages: 5
config :auto_linker,
opts: [
scheme: true,
extra: true,
class: false,
strip_prefix: false,
new_window: false,
rel: false
]
config :pleroma, :ldap,
enabled: System.get_env("LDAP_ENABLED") == "true",
host: System.get_env("LDAP_HOST") || "localhost",
port: String.to_integer(System.get_env("LDAP_PORT") || "389"),
ssl: System.get_env("LDAP_SSL") == "true",
sslopts: [],
tls: System.get_env("LDAP_TLS") == "true",
tlsopts: [],
base: System.get_env("LDAP_BASE") || "dc=example,dc=com",
uid: System.get_env("LDAP_UID") || "cn"
# Import environment specific config. This must remain at the bottom # Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above. # of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs" import_config "#{Mix.env()}.exs"

View file

@ -17,6 +17,8 @@
# Print only warnings and errors during test # Print only warnings and errors during test
config :logger, level: :warn config :logger, level: :warn
config :pleroma, Pleroma.Upload, filters: [], link_name: false
config :pleroma, Pleroma.Uploaders.Local, uploads: "test/uploads" config :pleroma, Pleroma.Uploaders.Local, uploads: "test/uploads"
config :pleroma, Pleroma.Mailer, adapter: Swoosh.Adapters.Test config :pleroma, Pleroma.Mailer, adapter: Swoosh.Adapters.Test
@ -44,6 +46,8 @@
"BLH1qVhJItRGCfxgTtONfsOKDc9VRAraXw-3NsmjMngWSh7NxOizN6bkuRA7iLTMPS82PjwJAr3UoK9EC1IFrz4", "BLH1qVhJItRGCfxgTtONfsOKDc9VRAraXw-3NsmjMngWSh7NxOizN6bkuRA7iLTMPS82PjwJAr3UoK9EC1IFrz4",
private_key: "_-XZ0iebPrRfZ_o0-IatTdszYa8VCH1yLN-JauK7HHA" private_key: "_-XZ0iebPrRfZ_o0-IatTdszYa8VCH1yLN-JauK7HHA"
config :web_push_encryption, :http_client, Pleroma.Web.WebPushHttpClientMock
config :pleroma, Pleroma.Jobs, testing: [max_jobs: 2] config :pleroma, Pleroma.Jobs, testing: [max_jobs: 2]
try do try do

View file

@ -1,37 +1,93 @@
# Admin API # Admin API
Authentication is required and the user must be an admin. Authentication is required and the user must be an admin.
## `/api/pleroma/admin/users`
### List users
- Method `GET`
- Query Params:
- `query`: **string** *optional* search term
- `local_only`: **bool** *optional* whether to return only local users
- `page`: **integer** *optional* page number
- `page_size`: **integer** *optional* number of users per page (default is `50`)
- Response:
```JSON
{
"page_size": integer,
"count": integer,
"users": [
{
"deactivated": bool,
"id": integer,
"nickname": string
},
...
]
}
```
## `/api/pleroma/admin/user` ## `/api/pleroma/admin/user`
### Remove a user ### Remove a user
* Method `DELETE`
* Params: - Method `DELETE`
* `nickname` - Params:
* Response: Users nickname - `nickname`
- Response: Users nickname
### Create a user ### Create a user
* Method: `POST`
* Params: - Method: `POST`
* `nickname` - Params:
* `email` - `nickname`
* `password` - `email`
* Response: Users nickname - `password`
- Response: Users nickname
## `/api/pleroma/admin/users/:nickname/toggle_activation`
### Toggle user activation
- Method: `PATCH`
- Params:
- `nickname`
- Response: Users object
```JSON
{
"deactivated": bool,
"id": integer,
"nickname": string
}
```
## `/api/pleroma/admin/users/tag` ## `/api/pleroma/admin/users/tag`
### Tag a list of users ### Tag a list of users
* Method: `PUT`
* Params: - Method: `PUT`
* `nickname` - Params:
* `tags` - `nickname`
- `tags`
### Untag a list of users ### Untag a list of users
* Method: `DELETE`
* Params: - Method: `DELETE`
* `nickname` - Params:
* `tags` - `nickname`
- `tags`
## `/api/pleroma/admin/permission_group/:nickname` ## `/api/pleroma/admin/permission_group/:nickname`
### Get user user permission groups membership ### Get user user permission groups membership
* Method: `GET`
* Params: none - Method: `GET`
* Response: - Params: none
- Response:
```JSON ```JSON
{ {
"is_moderator": bool, "is_moderator": bool,
@ -40,69 +96,87 @@ Authentication is required and the user must be an admin.
``` ```
## `/api/pleroma/admin/permission_group/:nickname/:permission_group` ## `/api/pleroma/admin/permission_group/:nickname/:permission_group`
Note: Available `:permission_group` is currently moderator and admin. 404 is returned when the permission group doesnt exist. Note: Available `:permission_group` is currently moderator and admin. 404 is returned when the permission group doesnt exist.
### Get user user permission groups membership ### Get user user permission groups membership
* Method: `GET`
* Params: none - Method: `GET`
* Response: - Params: none
- Response:
```JSON ```JSON
{ {
"is_moderator": bool, "is_moderator": bool,
"is_admin": bool "is_admin": bool
} }
``` ```
### Add user in permission group ### Add user in permission group
* Method: `POST`
* Params: none - Method: `POST`
* Response: - Params: none
* On failure: ``{"error": "…"}`` - Response:
* On success: JSON of the ``user.info`` - On failure: `{"error": "…"}`
- On success: JSON of the `user.info`
### Remove user from permission group ### Remove user from permission group
* Method: `DELETE`
* Params: none - Method: `DELETE`
* Response: - Params: none
* On failure: ``{"error": "…"}`` - Response:
* On success: JSON of the ``user.info`` - On failure: `{"error": "…"}`
* Note: An admin cannot revoke their own admin status. - On success: JSON of the `user.info`
- Note: An admin cannot revoke their own admin status.
## `/api/pleroma/admin/activation_status/:nickname` ## `/api/pleroma/admin/activation_status/:nickname`
### Active or deactivate a user ### Active or deactivate a user
* Method: `PUT`
* Params: - Method: `PUT`
* `nickname` - Params:
* `status` BOOLEAN field, false value means deactivation. - `nickname`
- `status` BOOLEAN field, false value means deactivation.
## `/api/pleroma/admin/relay` ## `/api/pleroma/admin/relay`
### Follow a Relay ### Follow a Relay
* Methods: `POST`
* Params: - Methods: `POST`
* `relay_url` - Params:
* Response: - `relay_url`
* On success: URL of the followed relay - Response:
- On success: URL of the followed relay
### Unfollow a Relay ### Unfollow a Relay
* Methods: `DELETE`
* Params: - Methods: `DELETE`
* `relay_url` - Params:
* Response: - `relay_url`
* On success: URL of the unfollowed relay - Response:
- On success: URL of the unfollowed relay
## `/api/pleroma/admin/invite_token` ## `/api/pleroma/admin/invite_token`
### Get a account registeration invite token ### Get a account registeration invite token
* Methods: `GET`
* Params: none - Methods: `GET`
* Response: invite token (base64 string) - Params: none
- Response: invite token (base64 string)
## `/api/pleroma/admin/email_invite` ## `/api/pleroma/admin/email_invite`
### Sends registration invite via email ### Sends registration invite via email
* Methods: `POST`
* Params: - Methods: `POST`
* `email` - Params:
* `name`, optionnal - `email`
- `name`, optionnal
## `/api/pleroma/admin/password_reset` ## `/api/pleroma/admin/password_reset`
### Get a password reset token for a given nickname ### Get a password reset token for a given nickname
* Methods: `GET`
* Params: none - Methods: `GET`
* Response: password reset token (base64 string) - Params: none
- Response: password reset token (base64 string)

View file

@ -4,8 +4,8 @@ Feel free to contact us to be added to this list!
## Desktop ## Desktop
### Roma for Desktop ### Roma for Desktop
- Homepage: <http://www.pleroma.com/desktop-app/> - Homepage: <https://www.pleroma.com/#desktopApp>
- Source Code: ??? - Source Code: <https://github.com/roma-apps/roma-desktop>
- Platforms: Windows, Mac, (Linux?) - Platforms: Windows, Mac, (Linux?)
- Features: Streaming Ready - Features: Streaming Ready
@ -30,6 +30,12 @@ Feel free to contact us to be added to this list!
- Platforms: iOS - Platforms: iOS
- Features: No Streaming - Features: No Streaming
### Fedilab
- Source Code: <https://gitlab.com/tom79/mastalab/>
- Contact: [@tom79@mastodon.social](https://mastodon.social/users/tom79)
- Platforms: Android
- Features: Streaming Ready
### Nekonium ### Nekonium
- Homepage: [F-Droid Repository](https://repo.gdgd.jp.net/), [Google Play](https://play.google.com/store/apps/details?id=com.apps.nekonium), [Amazon](https://www.amazon.co.jp/dp/B076FXPRBC/) - Homepage: [F-Droid Repository](https://repo.gdgd.jp.net/), [Google Play](https://play.google.com/store/apps/details?id=com.apps.nekonium), [Amazon](https://www.amazon.co.jp/dp/B076FXPRBC/)
- Source: <https://git.gdgd.jp.net/lin/nekonium/> - Source: <https://git.gdgd.jp.net/lin/nekonium/>
@ -37,15 +43,9 @@ Feel free to contact us to be added to this list!
- Platforms: Android - Platforms: Android
- Features: Streaming Ready - Features: Streaming Ready
### Mastalab
- Source Code: <https://gitlab.com/tom79/mastalab/>
- Contact: [@tom79@mastodon.social](https://mastodon.social/users/tom79)
- Platforms: Android
- Features: Streaming Ready
### Roma ### Roma
- Homepage: <http://www.pleroma.com/> - Homepage: <https://www.pleroma.com/#mobileApps>
- Source Code: ??? - Source Code: [iOS](https://github.com/roma-apps/roma-ios), [Android](https://github.com/roma-apps/roma-android)
- Platforms: iOS, Android - Platforms: iOS, Android
- Features: No Streaming - Features: No Streaming

18
docs/Custom-Emoji.md Normal file
View file

@ -0,0 +1,18 @@
# Custom emoji
To add custom emoji:
* Add the image file(s) to `priv/static/emoji/custom`
* In case of conflicts: add the desired shortcode with the path to `config/custom_emoji.txt`, comma-separated and one per line
* Force recompilation (``mix clean && mix compile``)
Example:
image files (in `/priv/static/emoji/custom`): `happy.png` and `sad.png`
content of `config/custom_emoji.txt`:
```
happy, /emoji/custom/happy.png
sad, /emoji/custom/sad.png
```
The files should be PNG (APNG is okay with `.png` for `image/png` Content-type) and under 50kb for compatibility with mastodon.

View file

@ -9,3 +9,29 @@ Pleroma uses 128-bit ids as opposed to Mastodon's 64 bits. However just like Mas
## Attachment cap ## 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. 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.
## Timelines
Adding the parameter `with_muted=true` to the timeline queries will also return activities by muted (not by blocked!) users.
## Statuses
Has these additional fields under the `pleroma` object:
- `local`: true if the post was made on the local instance.
## Attachments
Has these additional fields under the `pleroma` object:
- `mime_type`: mime type of the attachment.
## Accounts
- `/api/v1/accounts/:id`: The `id` parameter can also be the `nickname` of the user. This only works in this endpoint, not the deeper nested ones for following etc.
## Notifications
Has these additional fields under the `pleroma` object:
- `is_seen`: true if the notification was read by the user

View file

@ -0,0 +1,119 @@
# Message Rewrite Facility configuration
The Message Rewrite Facility (MRF) is a subsystem that is implemented as a series of hooks that allows the administrator to rewrite or discard messages.
Possible uses include:
* marking incoming messages with media from a given account or instance as sensitive
* rejecting messages from a specific instance
* removing/unlisting messages from the public timelines
* removing media from messages
* sending only public messages to a specific instance
The MRF provides user-configurable policies. The default policy is `NoOpPolicy`, which disables the MRF functionality. Pleroma also includes an easy to use policy called `SimplePolicy` which maps messages matching certain pre-defined criterion to actions built into the policy module.
It is possible to use multiple, active MRF policies at the same time.
## Quarantine Instances
You have the ability to prevent from private / followers-only messages from federating with specific instances. Which means they will only get the public or unlisted messages from your instance.
If, for example, you're using `MIX_ENV=prod` aka using production mode, you would open your configuration file located in `config/prod.secret.exs` and edit or add the option under your `:instance` config object. Then you would specify the instance within quotes.
```
config :pleroma, :instance,
[...]
quarantined_instances: ["instance.example", "other.example"]
```
## Using `SimplePolicy`
`SimplePolicy` is capable of handling most common admin tasks.
To use `SimplePolicy`, you must enable it. Do so by adding the following to your `:instance` config object, so that it looks like this:
```
config :pleroma, :instance,
[...]
rewrite_policy: Pleroma.Web.ActivityPub.MRF.SimplePolicy
```
Once `SimplePolicy` is enabled, you can configure various groups in the `:mrf_simple` config object. These groups are:
* `media_removal`: Servers in this group will have media stripped from incoming messages.
* `media_nsfw`: Servers in this group will have the #nsfw tag and sensitive setting injected into incoming messages which contain media.
* `reject`: Servers in this group will have their messages rejected.
* `federated_timeline_removal`: Servers in this group will have their messages unlisted from the public timelines by flipping the `to` and `cc` fields.
Servers should be configured as lists.
### Example
This example will enable `SimplePolicy`, block media from `illegalporn.biz`, mark media as NSFW from `porn.biz` and `porn.business`, reject messages from `spam.com` and remove messages from `spam.university` from the federated timeline:
```
config :pleroma, :instance,
rewrite_policy: [Pleroma.Web.ActivityPub.MRF.SimplePolicy]
config :pleroma, :mrf_simple,
media_removal: ["illegalporn.biz"],
media_nsfw: ["porn.biz", "porn.business"],
reject: ["spam.com"],
federated_timeline_removal: ["spam.university"]
```
### Use with Care
The effects of MRF policies can be very drastic. It is important to use this functionality carefully. Always try to talk to an admin before writing an MRF policy concerning their instance.
## Writing your own MRF Policy
As discussed above, the MRF system is a modular system that supports pluggable policies. This means that an admin may write a custom MRF policy in Elixir or any other language that runs on the Erlang VM, by specifying the module name in the `rewrite_policy` config setting.
For example, here is a sample policy module which rewrites all messages to "new message content":
```!elixir
# This is a sample MRF policy which rewrites all Notes to have "new message
# content."
defmodule Site.RewritePolicy do
@behavior Pleroma.Web.ActivityPub.MRF
# Catch messages which contain Note objects with actual data to filter.
# Capture the object as `object`, the message content as `content` and the
# message itself as `message`.
@impl true
def filter(%{"type" => Create", "object" => {"type" => "Note", "content" => content} = object} = message)
when is_binary(content) do
# Subject / CW is stored as summary instead of `name` like other AS2 objects
# because of Mastodon doing it that way.
summary = object["summary"]
# Message edits go here.
content = "new message content"
# Assemble the mutated object.
object =
object
|> Map.put("content", content)
|> Map.put("summary", summary)
# Assemble the mutated message.
message = Map.put(message, "object", object)
{:ok, message}
end
# Let all other messages through without modifying them.
@impl true
def filter(message), do: {:ok, message}
end
```
If you save this file as `lib/site/mrf/rewrite_policy.ex`, it will be included when you next rebuild Pleroma. You can enable it in the configuration like so:
```
config :pleroma, :instance,
rewrite_policy: [
Pleroma.Web.ActivityPub.MRF.SimplePolicy,
Site.RewritePolicy
]
```
Please note that the Pleroma developers consider custom MRF policy modules to fall under the purview of the AGPL. As such, you are obligated to release the sources to your custom MRF policy modules upon request.

View file

@ -108,3 +108,11 @@ See [Admin-API](Admin-API.md)
* Response: JSON string. Returns the user flavour or the default one. * Response: JSON string. Returns the user flavour or the default one.
* Example response: "glitch" * Example response: "glitch"
* Note: This is intended to be used only by mastofe * Note: This is intended to be used only by mastofe
## `/api/pleroma/notifications/read`
### Mark a single notification as read
* Method `POST`
* Authentication: required
* Params:
* `id`: notifications's id
* Response: JSON. Returns `{"status": "success"}` if the reading was successful, otherwise returns `{"error": "error_msg"}`

View file

@ -6,6 +6,7 @@ If you run Pleroma with ``MIX_ENV=prod`` the file is ``prod.secret.exs``, otherw
## Pleroma.Upload ## Pleroma.Upload
* `uploader`: Select which `Pleroma.Uploaders` to use * `uploader`: Select which `Pleroma.Uploaders` to use
* `filters`: List of `Pleroma.Upload.Filter` to use. * `filters`: List of `Pleroma.Upload.Filter` to use.
* `link_name`: When enabled Pleroma will add a `name` parameter to the url of the upload, for example `https://instance.tld/media/corndog.png?name=corndog.png`. This is needed to provide the correct filename in Content-Disposition headers when using filters like `Pleroma.Upload.Filter.Dedupe`
* `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host. * `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host.
* `proxy_remote`: If you\'re using a remote uploader, Pleroma will proxy media requests instead of redirecting to it. * `proxy_remote`: If you\'re using a remote uploader, Pleroma will proxy media requests instead of redirecting to it.
* `proxy_opts`: Proxy options, see `Pleroma.ReverseProxy` documentation. * `proxy_opts`: Proxy options, see `Pleroma.ReverseProxy` documentation.
@ -129,7 +130,7 @@ See: [loggers documentation](https://hexdocs.pm/logger/Logger.html) and [ex_s
## :frontend_configurations ## :frontend_configurations
This can be used to configure a keyword list that keeps the configuration data for any kind of frontend. By default, settings for `pleroma_fe` are configured. This can be used to configure a keyword list that keeps the configuration data for any kind of frontend. By default, settings for `pleroma_fe` and `masto_fe` are configured.
Frontends can access these settings at `/api/pleroma/frontend_configurations` Frontends can access these settings at `/api/pleroma/frontend_configurations`
@ -285,6 +286,10 @@ This config contains two queues: `federator_incoming` and `federator_outgoing`.
## :rich_media ## :rich_media
* `enabled`: if enabled the instance will parse metadata from attached links to generate link previews * `enabled`: if enabled the instance will parse metadata from attached links to generate link previews
## :fetch_initial_posts
* `enabled`: if enabled, when a new user is federated with, fetch some of their latest posts
* `pages`: the amount of pages to fetch
## :hackney_pools ## :hackney_pools
Advanced. Tweaks Hackney (http client) connections pools. Advanced. Tweaks Hackney (http client) connections pools.
@ -301,3 +306,51 @@ For each pool, the options are:
* `max_connections` - how much connections a pool can hold * `max_connections` - how much connections a pool can hold
* `timeout` - retention duration for connections * `timeout` - retention duration for connections
## :auto_linker
Configuration for the `auto_linker` library:
* `class: "auto-linker"` - specify the class to be added to the generated link. false to clear
* `rel: "noopener noreferrer"` - override the rel attribute. false to clear
* `new_window: true` - set to false to remove `target='_blank'` attribute
* `scheme: false` - Set to true to link urls with schema `http://google.com`
* `truncate: false` - Set to a number to truncate urls longer then the number. Truncated urls will end in `..`
* `strip_prefix: true` - Strip the scheme prefix
* `extra: false` - link urls with rarely used schemes (magnet, ipfs, irc, etc.)
Example:
```exs
config :auto_linker,
opts: [
scheme: true,
extra: true,
class: false,
strip_prefix: false,
new_window: false,
rel: false
]
```
## :ldap
Use LDAP for user authentication. When a user logs in to the Pleroma
instance, the name and password will be verified by trying to authenticate
(bind) to an LDAP server. If a user exists in the LDAP directory but there
is no account with the same name yet on the Pleroma instance then a new
Pleroma account will be created with the same name as the LDAP user name.
* `enabled`: enables LDAP authentication
* `host`: LDAP server hostname
* `port`: LDAP port, e.g. 389 or 636
* `ssl`: true to use SSL, usually implies the port 636
* `sslopts`: additional SSL options
* `tls`: true to start TLS, usually implies the port 389
* `tlsopts`: additional TLS options
* `base`: LDAP base, e.g. "dc=example,dc=com"
* `uid`: LDAP attribute name to authenticate the user, e.g. when "cn", the filter will be "cn=username,base"
## Pleroma.Web.Auth.Authenticator
* `Pleroma.Web.Auth.PleromaAuthenticator`: default database authenticator
* `Pleroma.Web.Auth.LDAPAuthenticator`: LDAP authentication

20
docs/static_dir.md Normal file
View file

@ -0,0 +1,20 @@
# Static Directory
Static frontend files are shipped in `priv/static/` and tracked by version control in this repository. If you want to overwrite or update these without the possibility of merge conflicts, you can write your custom versions to `instance/static/`.
```
config :pleroma, :instance,
static_dir: "instance/static/",
```
You can overwrite this value in your configuration to use a different static instance directory.
## robots.txt
By default, the `robots.txt` that ships in `priv/static/` is permissive. It allows well-behaved search engines to index all of your instance's URIs.
If you want to generate a restrictive `robots.txt`, you can run the following mix task. The generated `robots.txt` will be written in your instance static directory.
```
mix pleroma.robots_txt disallow_all
```

View file

@ -11,7 +11,9 @@ proxy_cache_path /tmp/pleroma-media-cache levels=1:2 keys_zone=pleroma_media_cac
server { server {
server_name example.tld; server_name example.tld;
listen 80; listen 80;
listen [::]:80;
return 301 https://$server_name$request_uri; return 301 https://$server_name$request_uri;
# Uncomment this if you need to use the 'webroot' method with certbot. Make sure # Uncomment this if you need to use the 'webroot' method with certbot. Make sure
@ -29,7 +31,10 @@ server {
ssl_session_cache shared:ssl_session_cache:10m; ssl_session_cache shared:ssl_session_cache:10m;
server { server {
server_name example.tld;
listen 443 ssl http2; listen 443 ssl http2;
listen [::]:443 ssl http2;
ssl_session_timeout 5m; ssl_session_timeout 5m;
ssl_trusted_certificate /etc/letsencrypt/live/example.tld/fullchain.pem; ssl_trusted_certificate /etc/letsencrypt/live/example.tld/fullchain.pem;
@ -48,8 +53,6 @@ server {
ssl_stapling on; ssl_stapling on;
ssl_stapling_verify on; ssl_stapling_verify on;
server_name example.tld;
gzip_vary on; gzip_vary on;
gzip_proxied any; gzip_proxied any;
gzip_comp_level 6; gzip_comp_level 6;

View file

@ -4,8 +4,8 @@
defmodule Mix.Tasks.Pleroma.Relay do defmodule Mix.Tasks.Pleroma.Relay do
use Mix.Task use Mix.Task
alias Pleroma.Web.ActivityPub.Relay
alias Mix.Tasks.Pleroma.Common alias Mix.Tasks.Pleroma.Common
alias Pleroma.Web.ActivityPub.Relay
@shortdoc "Manages remote relays" @shortdoc "Manages remote relays"
@moduledoc """ @moduledoc """

View file

@ -0,0 +1,32 @@
# Pleroma: A lightweight social networking server
# Copyright © 2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.RobotsTxt do
use Mix.Task
@shortdoc "Generate robots.txt"
@moduledoc """
Generates robots.txt
## Overwrite robots.txt to disallow all
mix pleroma.robots_txt disallow_all
This will write a robots.txt that will hide all paths on your instance
from search engines and other robots that obey robots.txt
"""
def run(["disallow_all"]) do
static_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static/")
if !File.exists?(static_dir) do
File.mkdir_p!(static_dir)
end
robots_txt_path = Path.join(static_dir, "robots.txt")
robots_txt_content = "User-Agent: *\nDisallow: /\n"
File.write!(robots_txt_path, robots_txt_content, [:write])
end
end

View file

@ -4,9 +4,9 @@
defmodule Mix.Tasks.Pleroma.Uploads do defmodule Mix.Tasks.Pleroma.Uploads do
use Mix.Task use Mix.Task
alias Mix.Tasks.Pleroma.Common
alias Pleroma.Upload alias Pleroma.Upload
alias Pleroma.Uploaders.Local alias Pleroma.Uploaders.Local
alias Mix.Tasks.Pleroma.Common
require Logger require Logger
@log_every 50 @log_every 50
@ -20,7 +20,6 @@ defmodule Mix.Tasks.Pleroma.Uploads do
Options: Options:
- `--delete` - delete local uploads after migrating them to the target uploader - `--delete` - delete local uploads after migrating them to the target uploader
A list of available uploaders can be seen in config.exs A list of available uploaders can be seen in config.exs
""" """
def run(["migrate_local", target_uploader | args]) do def run(["migrate_local", target_uploader | args]) do

View file

@ -5,9 +5,9 @@
defmodule Mix.Tasks.Pleroma.User do defmodule Mix.Tasks.Pleroma.User do
use Mix.Task use Mix.Task
import Ecto.Changeset import Ecto.Changeset
alias Mix.Tasks.Pleroma.Common
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Mix.Tasks.Pleroma.Common
@shortdoc "Manages Pleroma users" @shortdoc "Manages Pleroma users"
@moduledoc """ @moduledoc """

View file

@ -7,9 +7,9 @@ defmodule Pleroma.PasswordResetToken do
import Ecto.Changeset import Ecto.Changeset
alias Pleroma.User
alias Pleroma.Repo
alias Pleroma.PasswordResetToken alias Pleroma.PasswordResetToken
alias Pleroma.Repo
alias Pleroma.User
schema "password_reset_tokens" do schema "password_reset_tokens" do
belongs_to(:user, User, type: Pleroma.FlakeId) belongs_to(:user, User, type: Pleroma.FlakeId)

View file

@ -5,9 +5,9 @@
defmodule Pleroma.Activity do defmodule Pleroma.Activity do
use Ecto.Schema use Ecto.Schema
alias Pleroma.Repo
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Repo
import Ecto.Query import Ecto.Query
@ -107,6 +107,18 @@ def get_in_reply_to_activity(%Activity{data: %{"object" => %{"inReplyTo" => ap_i
def get_in_reply_to_activity(_), do: nil def get_in_reply_to_activity(_), do: nil
def delete_by_ap_id(id) when is_binary(id) do
by_object_ap_id(id)
|> Repo.delete_all(returning: true)
|> elem(1)
|> Enum.find(fn
%{data: %{"type" => "Create", "object" => %{"id" => ap_id}}} -> ap_id == id
_ -> nil
end)
end
def delete_by_ap_id(_), do: nil
for {ap_type, type} <- @mastodon_notification_types do for {ap_type, type} <- @mastodon_notification_types do
def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}), def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}),
do: unquote(type) do: unquote(type)

View file

@ -11,10 +11,10 @@ defmodule Pleroma.Application do
@repository Mix.Project.config()[:source_url] @repository Mix.Project.config()[:source_url]
def name, do: @name def name, do: @name
def version, do: @version def version, do: @version
def named_version(), do: @name <> " " <> @version def named_version, do: @name <> " " <> @version
def repository, do: @repository def repository, do: @repository
def user_agent() do def user_agent do
info = "#{Pleroma.Web.base_url()} <#{Pleroma.Config.get([:instance, :email], "")}>" info = "#{Pleroma.Web.base_url()} <#{Pleroma.Config.get([:instance, :email], "")}>"
named_version() <> "; " <> info named_version() <> "; " <> info
end end
@ -48,7 +48,7 @@ def start(_type, _args) do
[ [
:user_cache, :user_cache,
[ [
default_ttl: 25000, default_ttl: 25_000,
ttl_interval: 1000, ttl_interval: 1000,
limit: 2500 limit: 2500
] ]
@ -60,7 +60,7 @@ def start(_type, _args) do
[ [
:object_cache, :object_cache,
[ [
default_ttl: 25000, default_ttl: 25_000,
ttl_interval: 1000, ttl_interval: 1000,
limit: 2500 limit: 2500
] ]
@ -127,7 +127,7 @@ def start(_type, _args) do
Supervisor.start_link(children, opts) Supervisor.start_link(children, opts)
end end
def enabled_hackney_pools() do def enabled_hackney_pools do
[:media] ++ [:media] ++
if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do
[:federation] [:federation]
@ -142,14 +142,14 @@ def enabled_hackney_pools() do
end end
if Mix.env() == :test do if Mix.env() == :test do
defp streamer_child(), do: [] defp streamer_child, do: []
defp chat_child(), do: [] defp chat_child, do: []
else else
defp streamer_child() do defp streamer_child do
[worker(Pleroma.Web.Streamer, [])] [worker(Pleroma.Web.Streamer, [])]
end end
defp chat_child() do defp chat_child do
if Pleroma.Config.get([:chat, :enabled]) do if Pleroma.Config.get([:chat, :enabled]) do
[worker(Pleroma.Web.ChatChannel.ChatChannelState, [])] [worker(Pleroma.Web.ChatChannel.ChatChannelState, [])]
else else
@ -158,7 +158,7 @@ defp chat_child() do
end end
end end
defp hackney_pool_children() do defp hackney_pool_children do
for pool <- enabled_hackney_pools() do for pool <- enabled_hackney_pools() do
options = Pleroma.Config.get([:hackney_pools, pool]) options = Pleroma.Config.get([:hackney_pools, pool])
:hackney_pool.child_spec(pool, options) :hackney_pool.child_spec(pool, options)

View file

@ -10,7 +10,7 @@ defmodule Pleroma.Captcha do
use GenServer use GenServer
@doc false @doc false
def start_link() do def start_link do
GenServer.start_link(__MODULE__, [], name: __MODULE__) GenServer.start_link(__MODULE__, [], name: __MODULE__)
end end
@ -22,7 +22,7 @@ def init(_) do
@doc """ @doc """
Ask the configured captcha service for a new captcha Ask the configured captcha service for a new captcha
""" """
def new() do def new do
GenServer.call(__MODULE__, :new) GenServer.call(__MODULE__, :new)
end end
@ -73,7 +73,7 @@ def handle_call({:validate, token, captcha, answer_data}, _from, state) do
secret = KeyGenerator.generate(secret_key_base, token <> "_encrypt") secret = KeyGenerator.generate(secret_key_base, token <> "_encrypt")
sign_secret = KeyGenerator.generate(secret_key_base, token <> "_sign") sign_secret = KeyGenerator.generate(secret_key_base, token <> "_sign")
# If the time found is less than (current_time - seconds_valid), then the time has already passed. # If the time found is less than (current_time-seconds_valid) then the time has already passed
# Later we check that the time found is more than the presumed invalidatation time, that means # Later we check that the time found is more than the presumed invalidatation time, that means
# that the data is still valid and the captcha can be checked # that the data is still valid and the captcha can be checked
seconds_valid = Pleroma.Config.get!([Pleroma.Captcha, :seconds_valid]) seconds_valid = Pleroma.Config.get!([Pleroma.Captcha, :seconds_valid])

View file

@ -7,7 +7,7 @@ defmodule Pleroma.Captcha.Kocaptcha do
@behaviour Service @behaviour Service
@impl Service @impl Service
def new() do def new do
endpoint = Pleroma.Config.get!([__MODULE__, :endpoint]) endpoint = Pleroma.Config.get!([__MODULE__, :endpoint])
case Tesla.get(endpoint <> "/new") do case Tesla.get(endpoint <> "/new") do

View file

@ -7,13 +7,13 @@ defmodule Pleroma.Clippy do
# No software is complete until they have a Clippy implementation. # No software is complete until they have a Clippy implementation.
# A ballmer peak _may_ be required to change this module. # A ballmer peak _may_ be required to change this module.
def tip() do def tip do
tips() tips()
|> Enum.random() |> Enum.random()
|> puts() |> puts()
end end
def tips() do def tips do
host = Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host]) host = Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host])
[ [
@ -92,8 +92,8 @@ def puts(text_or_lines) do
# surrond one/five line clippy with blank lines around to not fuck up the layout # surrond one/five line clippy with blank lines around to not fuck up the layout
# #
# yes this fix sucks but it's good enough, have you ever seen a release of windows wihtout some butched # yes this fix sucks but it's good enough, have you ever seen a release of windows
# features anyway? # without some butched features anyway?
lines = lines =
if length(lines) == 1 or length(lines) == 5 do if length(lines) == 1 or length(lines) == 5 do
[""] ++ lines ++ [""] [""] ++ lines ++ [""]

View file

@ -5,7 +5,7 @@
defmodule Pleroma.Config.DeprecationWarnings do defmodule Pleroma.Config.DeprecationWarnings do
require Logger require Logger
def check_frontend_config_mechanism() do def check_frontend_config_mechanism do
if Pleroma.Config.get(:fe) do if Pleroma.Config.get(:fe) do
Logger.warn(""" Logger.warn("""
!!!DEPRECATION WARNING!!! !!!DEPRECATION WARNING!!!

View file

@ -17,13 +17,13 @@ defmodule Pleroma.Emoji do
@ets_options [:ordered_set, :protected, :named_table, {:read_concurrency, true}] @ets_options [:ordered_set, :protected, :named_table, {:read_concurrency, true}]
@doc false @doc false
def start_link() do def start_link do
GenServer.start_link(__MODULE__, [], name: __MODULE__) GenServer.start_link(__MODULE__, [], name: __MODULE__)
end end
@doc "Reloads the emojis from disk." @doc "Reloads the emojis from disk."
@spec reload() :: :ok @spec reload() :: :ok
def reload() do def reload do
GenServer.call(__MODULE__, :reload) GenServer.call(__MODULE__, :reload)
end end
@ -38,7 +38,7 @@ def get(name) do
@doc "Returns all the emojos!!" @doc "Returns all the emojos!!"
@spec get_all() :: [{String.t(), String.t()}, ...] @spec get_all() :: [{String.t(), String.t()}, ...]
def get_all() do def get_all do
:ets.tab2list(@ets) :ets.tab2list(@ets)
end end
@ -72,7 +72,7 @@ def code_change(_old_vsn, state, _extra) do
{:ok, state} {:ok, state}
end end
defp load() do defp load do
emojis = emojis =
(load_finmoji(Keyword.get(Application.get_env(:pleroma, :instance), :finmoji_enabled)) ++ (load_finmoji(Keyword.get(Application.get_env(:pleroma, :instance), :finmoji_enabled)) ++
load_from_file("config/emoji.txt") ++ load_from_file("config/emoji.txt") ++

View file

@ -8,8 +8,8 @@ defmodule Pleroma.Filter do
import Ecto.Changeset import Ecto.Changeset
import Ecto.Query import Ecto.Query
alias Pleroma.User
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User
schema "filters" do schema "filters" do
belongs_to(:user, User, type: Pleroma.FlakeId) belongs_to(:user, User, type: Pleroma.FlakeId)

View file

@ -85,7 +85,7 @@ def dump(value) do
{:ok, FlakeId.from_string(value)} {:ok, FlakeId.from_string(value)}
end end
def autogenerate(), do: get() def autogenerate, do: get()
# -- GenServer API # -- GenServer API
def start_link do def start_link do
@ -165,7 +165,7 @@ defp time do
1_000_000_000 * mega_seconds + seconds * 1000 + :erlang.trunc(micro_seconds / 1000) 1_000_000_000 * mega_seconds + seconds * 1000 + :erlang.trunc(micro_seconds / 1000)
end end
defp worker_id() do defp worker_id do
<<worker::integer-size(48)>> = :crypto.strong_rand_bytes(6) <<worker::integer-size(48)>> = :crypto.strong_rand_bytes(6)
worker worker
end end

View file

@ -8,33 +8,52 @@ defmodule Pleroma.Formatter do
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.MediaProxy alias Pleroma.Web.MediaProxy
@tag_regex ~r/((?<=[^&])|\A)(\#)(\w+)/u
@markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/ @markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/
@link_regex ~r{((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+}ui
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
# Modified from https://www.w3.org/TR/html5/forms.html#valid-e-mail-address @auto_linker_config hashtag: true,
@mentions_regex ~r/@[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]*@?[a-zA-Z0-9_-](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*/u hashtag_handler: &Pleroma.Formatter.hashtag_handler/4,
mention: true,
mention_handler: &Pleroma.Formatter.mention_handler/4
def parse_tags(text, data \\ %{}) do def mention_handler("@" <> nickname, buffer, opts, acc) do
Regex.scan(@tag_regex, text) case User.get_cached_by_nickname(nickname) do
|> Enum.map(fn ["#" <> tag = full_tag | _] -> {full_tag, String.downcase(tag)} end) %User{id: id} = user ->
|> (fn map -> ap_id = get_ap_id(user)
if data["sensitive"] in [true, "True", "true", "1"], nickname_text = get_nickname_text(nickname, opts) |> maybe_escape(opts)
do: [{"#nsfw", "nsfw"}] ++ map,
else: map link =
end).() "<span class='h-card'><a data-user='#{id}' class='u-url mention' href='#{ap_id}'>@<span>#{
nickname_text
}</span></a></span>"
{link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, user})}}
_ ->
{buffer, acc}
end
end end
@doc "Parses mentions text and returns list {nickname, user}." def hashtag_handler("#" <> tag = tag_text, _buffer, _opts, acc) do
@spec parse_mentions(binary()) :: list({binary(), User.t()}) tag = String.downcase(tag)
def parse_mentions(text) do url = "#{Pleroma.Web.base_url()}/tag/#{tag}"
Regex.scan(@mentions_regex, text) link = "<a class='hashtag' data-tag='#{tag}' href='#{url}' rel='tag'>#{tag_text}</a>"
|> List.flatten()
|> Enum.uniq() {link, %{acc | tags: MapSet.put(acc.tags, {tag_text, tag})}}
|> Enum.map(fn nickname -> end
with nickname <- String.trim_leading(nickname, "@"),
do: {"@" <> nickname, User.get_cached_by_nickname(nickname)} @doc """
end) Parses a text and replace plain text links with HTML. Returns a tuple with a result text, mentions, and hashtags.
|> Enum.filter(fn {_match, user} -> user end) """
@spec linkify(String.t(), keyword()) ::
{String.t(), [{String.t(), User.t()}], [{String.t(), String.t()}]}
def linkify(text, options \\ []) do
options = options ++ @auto_linker_config
acc = %{mentions: MapSet.new(), tags: MapSet.new()}
{text, %{mentions: mentions, tags: tags}} = AutoLinker.link_map(text, acc, options)
{text, MapSet.to_list(mentions), MapSet.to_list(tags)}
end end
def emojify(text) do def emojify(text) do
@ -48,9 +67,7 @@ def emojify(text, emoji, strip \\ false) do
emoji = HTML.strip_tags(emoji) emoji = HTML.strip_tags(emoji)
file = HTML.strip_tags(file) file = HTML.strip_tags(file)
String.replace( html =
text,
":#{emoji}:",
if not strip do if not strip do
"<img height='32px' width='32px' alt='#{emoji}' title='#{emoji}' src='#{ "<img height='32px' width='32px' alt='#{emoji}' title='#{emoji}' src='#{
MediaProxy.url(file) MediaProxy.url(file)
@ -58,8 +75,8 @@ def emojify(text, emoji, strip \\ false) do
else else
"" ""
end end
)
|> HTML.filter_tags() String.replace(text, ":#{emoji}:", html) |> HTML.filter_tags()
end) end)
end end
@ -75,12 +92,10 @@ def get_emoji(text) when is_binary(text) do
def get_emoji(_), do: [] def get_emoji(_), do: []
@link_regex ~r/[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+/ui def html_escape({text, mentions, hashtags}, type) do
{html_escape(text, type), mentions, hashtags}
end
@uri_schemes Application.get_env(:pleroma, :uri_schemes, [])
@valid_schemes Keyword.get(@uri_schemes, :valid_schemes, [])
# TODO: make it use something other than @link_regex
def html_escape(text, "text/html") do def html_escape(text, "text/html") do
HTML.filter_tags(text) HTML.filter_tags(text)
end end
@ -94,112 +109,6 @@ def html_escape(text, "text/plain") do
|> Enum.join("") |> Enum.join("")
end end
@doc """
Escapes a special characters in mention names.
"""
@spec mentions_escape(String.t(), list({String.t(), any()})) :: String.t()
def mentions_escape(text, mentions) do
mentions
|> Enum.reduce(text, fn {name, _}, acc ->
escape_name = String.replace(name, @markdown_characters_regex, "\\\\\\1")
String.replace(acc, name, escape_name)
end)
end
@doc "changes scheme:... urls to html links"
def add_links({subs, text}) do
links =
text
|> String.split([" ", "\t", "<br>"])
|> Enum.filter(fn word -> String.starts_with?(word, @valid_schemes) end)
|> Enum.filter(fn word -> Regex.match?(@link_regex, word) end)
|> Enum.map(fn url -> {Ecto.UUID.generate(), url} end)
|> Enum.sort_by(fn {_, url} -> -String.length(url) end)
uuid_text =
links
|> Enum.reduce(text, fn {uuid, url}, acc -> String.replace(acc, url, uuid) end)
subs =
subs ++
Enum.map(links, fn {uuid, url} ->
{uuid, "<a href=\"#{url}\">#{url}</a>"}
end)
{subs, uuid_text}
end
@doc "Adds the links to mentioned users"
def add_user_links({subs, text}, mentions, options \\ []) do
mentions =
mentions
|> Enum.sort_by(fn {name, _} -> -String.length(name) end)
|> Enum.map(fn {name, user} -> {name, user, Ecto.UUID.generate()} end)
uuid_text =
mentions
|> Enum.reduce(text, fn {match, _user, uuid}, text ->
String.replace(text, match, uuid)
end)
subs =
subs ++
Enum.map(mentions, fn {match, %User{id: id, ap_id: ap_id, info: info}, uuid} ->
ap_id =
if is_binary(info.source_data["url"]) do
info.source_data["url"]
else
ap_id
end
nickname =
if options[:format] == :full do
User.full_nickname(match)
else
User.local_nickname(match)
end
{uuid,
"<span class='h-card'><a data-user='#{id}' class='u-url mention' href='#{ap_id}'>" <>
"@<span>#{nickname}</span></a></span>"}
end)
{subs, uuid_text}
end
@doc "Adds the hashtag links"
def add_hashtag_links({subs, text}, tags) do
tags =
tags
|> Enum.sort_by(fn {name, _} -> -String.length(name) end)
|> Enum.map(fn {name, short} -> {name, short, Ecto.UUID.generate()} end)
uuid_text =
tags
|> Enum.reduce(text, fn {match, _short, uuid}, text ->
String.replace(text, ~r/((?<=[^&])|(\A))#{match}/, uuid)
end)
subs =
subs ++
Enum.map(tags, fn {tag_text, tag, uuid} ->
url =
"<a class='hashtag' data-tag='#{tag}' href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>#{
tag_text
}</a>"
{uuid, url}
end)
{subs, uuid_text}
end
def finalize({subs, text}) do
Enum.reduce(subs, text, fn {uuid, replacement}, result_text ->
String.replace(result_text, uuid, replacement)
end)
end
def truncate(text, max_length \\ 200, omission \\ "...") do def truncate(text, max_length \\ 200, omission \\ "...") do
# Remove trailing whitespace # Remove trailing whitespace
text = Regex.replace(~r/([^ \t\r\n])([ \t]+$)/u, text, "\\g{1}") text = Regex.replace(~r/([^ \t\r\n])([ \t]+$)/u, text, "\\g{1}")
@ -211,4 +120,16 @@ def truncate(text, max_length \\ 200, omission \\ "...") do
String.slice(text, 0, length_with_omission) <> omission String.slice(text, 0, length_with_omission) <> omission
end end
end end
defp get_ap_id(%User{info: %{source_data: %{"url" => url}}}) when is_binary(url), do: url
defp get_ap_id(%User{ap_id: ap_id}), do: ap_id
defp get_nickname_text(nickname, %{mentions_format: :full}), do: User.full_nickname(nickname)
defp get_nickname_text(nickname, _), do: User.local_nickname(nickname)
defp maybe_escape(str, %{mentions_escape: true}) do
String.replace(str, @markdown_characters_regex, "\\\\\\1")
end
defp maybe_escape(str, _), do: str
end end

View file

@ -6,7 +6,7 @@ defmodule Pleroma.Gopher.Server do
use GenServer use GenServer
require Logger require Logger
def start_link() do def start_link do
config = Pleroma.Config.get(:gopher, []) config = Pleroma.Config.get(:gopher, [])
ip = Keyword.get(config, :ip, {0, 0, 0, 0}) ip = Keyword.get(config, :ip, {0, 0, 0, 0})
port = Keyword.get(config, :port, 1234) port = Keyword.get(config, :port, 1234)
@ -36,12 +36,12 @@ def init([ip, port]) do
end end
defmodule Pleroma.Gopher.Server.ProtocolHandler do defmodule Pleroma.Gopher.Server.ProtocolHandler do
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.HTML alias Pleroma.HTML
alias Pleroma.User
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility
def start_link(ref, socket, transport, opts) do def start_link(ref, socket, transport, opts) do
pid = spawn_link(__MODULE__, :init, [ref, socket, transport, opts]) pid = spawn_link(__MODULE__, :init, [ref, socket, transport, opts])

View file

@ -9,7 +9,7 @@ defp get_scrubbers(scrubber) when is_atom(scrubber), do: [scrubber]
defp get_scrubbers(scrubbers) when is_list(scrubbers), do: scrubbers defp get_scrubbers(scrubbers) when is_list(scrubbers), do: scrubbers
defp get_scrubbers(_), do: [Pleroma.HTML.Scrubber.Default] defp get_scrubbers(_), do: [Pleroma.HTML.Scrubber.Default]
def get_scrubbers() do def get_scrubbers do
Pleroma.Config.get([:markup, :scrub_policy]) Pleroma.Config.get([:markup, :scrub_policy])
|> get_scrubbers |> get_scrubbers
end end
@ -95,6 +95,13 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do
Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes) Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes)
Meta.allow_tag_with_these_attributes("a", ["name", "title", "class"]) Meta.allow_tag_with_these_attributes("a", ["name", "title", "class"])
Meta.allow_tag_with_this_attribute_values("a", "rel", [
"tag",
"nofollow",
"noopener",
"noreferrer"
])
# paragraphs and linebreaks # paragraphs and linebreaks
Meta.allow_tag_with_these_attributes("br", []) Meta.allow_tag_with_these_attributes("br", [])
Meta.allow_tag_with_these_attributes("p", []) Meta.allow_tag_with_these_attributes("p", [])
@ -137,6 +144,13 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes) Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes)
Meta.allow_tag_with_these_attributes("a", ["name", "title", "class"]) Meta.allow_tag_with_these_attributes("a", ["name", "title", "class"])
Meta.allow_tag_with_this_attribute_values("a", "rel", [
"tag",
"nofollow",
"noopener",
"noreferrer"
])
Meta.allow_tag_with_these_attributes("abbr", ["title"]) Meta.allow_tag_with_these_attributes("abbr", ["title"])
Meta.allow_tag_with_these_attributes("b", []) Meta.allow_tag_with_these_attributes("b", [])

View file

@ -8,8 +8,8 @@ defmodule Pleroma.HTTP.Connection do
""" """
@hackney_options [ @hackney_options [
timeout: 10000, connect_timeout: 2_000,
recv_timeout: 20000, recv_timeout: 20_000,
follow_redirect: true, follow_redirect: true,
pool: :federation pool: :federation
] ]
@ -31,6 +31,10 @@ def new(opts \\ []) do
# #
defp hackney_options(opts) do defp hackney_options(opts) do
options = Keyword.get(opts, :adapter, []) options = Keyword.get(opts, :adapter, [])
@hackney_options ++ options adapter_options = Pleroma.Config.get([:http, :adapter], [])
@hackney_options
|> Keyword.merge(adapter_options)
|> Keyword.merge(options)
end end
end end

View file

@ -27,6 +27,7 @@ defmodule Pleroma.HTTP do
""" """
def request(method, url, body \\ "", headers \\ [], options \\ []) do def request(method, url, body \\ "", headers \\ [], options \\ []) do
try do
options = options =
process_request_options(options) process_request_options(options)
|> process_sni_options(url) |> process_sni_options(url)
@ -41,7 +42,14 @@ def request(method, url, body \\ "", headers \\ [], options \\ []) do
|> Builder.add_param(:body, :body, body) |> Builder.add_param(:body, :body, body)
|> Builder.add_param(:query, :query, params) |> Builder.add_param(:query, :query, params)
|> Enum.into([]) |> Enum.into([])
|> (&Tesla.request(Connection.new(), &1)).() |> (&Tesla.request(Connection.new(options), &1)).()
rescue
e ->
{:error, e}
catch
:exit, e ->
{:error, e}
end
end end
defp process_sni_options(options, nil), do: options defp process_sni_options(options, nil), do: options

View file

@ -2,8 +2,8 @@ defmodule Pleroma.Instances.Instance do
@moduledoc "Instance." @moduledoc "Instance."
alias Pleroma.Instances alias Pleroma.Instances
alias Pleroma.Repo
alias Pleroma.Instances.Instance alias Pleroma.Instances.Instance
alias Pleroma.Repo
use Ecto.Schema use Ecto.Schema

View file

@ -5,14 +5,15 @@
defmodule Pleroma.Notification do defmodule Pleroma.Notification do
use Ecto.Schema use Ecto.Schema
alias Pleroma.User
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.Web.CommonAPI.Utils alias Pleroma.User
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
import Ecto.Query import Ecto.Query
import Ecto.Changeset
schema "notifications" do schema "notifications" do
field(:seen, :boolean, default: false) field(:seen, :boolean, default: false)
@ -22,6 +23,11 @@ defmodule Pleroma.Notification do
timestamps() timestamps()
end end
def changeset(%Notification{} = notification, attrs) do
notification
|> cast(attrs, [:seen])
end
# TODO: Make generic and unify (see activity_pub.ex) # TODO: Make generic and unify (see activity_pub.ex)
defp restrict_max(query, %{"max_id" => max_id}) do defp restrict_max(query, %{"max_id" => max_id}) do
from(activity in query, where: activity.id < ^max_id) from(activity in query, where: activity.id < ^max_id)
@ -68,6 +74,14 @@ def set_read_up_to(%{id: user_id} = _user, id) do
Repo.update_all(query, []) Repo.update_all(query, [])
end end
def read_one(%User{} = user, notification_id) do
with {:ok, %Notification{} = notification} <- get(user, notification_id) do
notification
|> changeset(%{seen: true})
|> Repo.update()
end
end
def get(%{id: user_id} = _user, id) do def get(%{id: user_id} = _user, id) do
query = query =
from( from(

View file

@ -5,11 +5,11 @@
defmodule Pleroma.Object do defmodule Pleroma.Object do
use Ecto.Schema use Ecto.Schema
alias Pleroma.Repo
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.ObjectTombstone alias Pleroma.ObjectTombstone
alias Pleroma.Repo
alias Pleroma.User
import Ecto.Query import Ecto.Query
import Ecto.Changeset import Ecto.Changeset
@ -86,9 +86,9 @@ def swap_object_with_tombstone(object) do
def delete(%Object{data: %{"id" => id}} = object) do def delete(%Object{data: %{"id" => id}} = object) do
with {:ok, _obj} = swap_object_with_tombstone(object), with {:ok, _obj} = swap_object_with_tombstone(object),
Repo.delete_all(Activity.by_object_ap_id(id)), deleted_activity = Activity.delete_by_ap_id(id),
{:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do {:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do
{:ok, object} {:ok, object, deleted_activity}
end end
end end

View file

@ -34,13 +34,16 @@ defp headers do
defp csp_string do defp csp_string do
scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme] scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme]
websocket_url = String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws") static_url = Pleroma.Web.Endpoint.static_url()
websocket_url = String.replace(static_url, "http", "ws")
connect_src = "connect-src 'self' #{static_url} #{websocket_url}"
connect_src = connect_src =
if Mix.env() == :dev do if Mix.env() == :dev do
"connect-src 'self' http://localhost:3035/ " <> websocket_url connect_src <> " http://localhost:3035/"
else else
"connect-src 'self' " <> websocket_url connect_src
end end
script_src = script_src =

View file

@ -3,8 +3,8 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
alias Pleroma.Web.HTTPSignatures
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.HTTPSignatures
import Plug.Conn import Plug.Conn
require Logger require Logger

View file

@ -21,7 +21,8 @@ def file_path(path) do
end end
end end
@only ~w(index.html static emoji packs sounds images instance favicon.png sw.js sw-pleroma.js) @only ~w(index.html robots.txt static emoji packs sounds images instance favicon.png sw.js
sw-pleroma.js)
def init(opts) do def init(opts) do
opts opts

View file

@ -6,8 +6,8 @@ defmodule Pleroma.Plugs.OAuthPlug do
import Plug.Conn import Plug.Conn
import Ecto.Query import Ecto.Query
alias Pleroma.User
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token
@realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i") @realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i")
@ -38,6 +38,7 @@ defp fetch_user_and_token(token) do
preload: [user: user] preload: [user: user]
) )
# credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
with %Token{user: %{info: %{deactivated: false} = _} = user} = token_record <- Repo.one(query) do with %Token{user: %{info: %{deactivated: false} = _} = user} = token_record <- Repo.one(query) do
{:ok, user, token_record} {:ok, user, token_record}
end end

View file

@ -24,6 +24,18 @@ def init(_opts) do
end end
def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do
conn =
case fetch_query_params(conn) do
%{query_params: %{"name" => name}} = conn ->
name = String.replace(name, "\"", "\\\"")
conn
|> put_resp_header("content-disposition", "filename=\"#{name}\"")
conn ->
conn
end
config = Pleroma.Config.get([Pleroma.Upload]) config = Pleroma.Config.get([Pleroma.Upload])
with uploader <- Keyword.fetch!(config, :uploader), with uploader <- Keyword.fetch!(config, :uploader),

View file

@ -3,8 +3,8 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.UserFetcherPlug do defmodule Pleroma.Plugs.UserFetcherPlug do
alias Pleroma.User
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User
import Plug.Conn import Plug.Conn

View file

@ -3,10 +3,12 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ReverseProxy do defmodule Pleroma.ReverseProxy do
@keep_req_headers ~w(accept user-agent accept-encoding cache-control if-modified-since if-unmodified-since if-none-match if-range range) @keep_req_headers ~w(accept user-agent accept-encoding cache-control if-modified-since) ++
~w(if-unmodified-since if-none-match if-range range)
@resp_cache_headers ~w(etag date last-modified cache-control) @resp_cache_headers ~w(etag date last-modified cache-control)
@keep_resp_headers @resp_cache_headers ++ @keep_resp_headers @resp_cache_headers ++
~w(content-type content-disposition content-encoding content-range accept-ranges vary) ~w(content-type content-disposition content-encoding content-range) ++
~w(accept-ranges vary)
@default_cache_control_header "public, max-age=1209600" @default_cache_control_header "public, max-age=1209600"
@valid_resp_codes [200, 206, 304] @valid_resp_codes [200, 206, 304]
@max_read_duration :timer.seconds(30) @max_read_duration :timer.seconds(30)
@ -282,8 +284,8 @@ defp build_resp_cache_headers(headers, _opts) do
headers headers
has_cache? -> has_cache? ->
# There's caching header present but no cache-control -- we need to explicitely override it to public # There's caching header present but no cache-control -- we need to explicitely override it
# as Plug defaults to "max-age=0, private, must-revalidate" # to public as Plug defaults to "max-age=0, private, must-revalidate"
List.keystore(headers, "cache-control", 0, {"cache-control", "public"}) List.keystore(headers, "cache-control", 0, {"cache-control", "public"})
true -> true ->
@ -309,7 +311,25 @@ defp build_resp_content_disposition_header(headers, opts) do
end end
if attachment? do if attachment? do
disposition = "attachment; filename=" <> Keyword.get(opts, :attachment_name, "attachment") name =
try do
{{"content-disposition", content_disposition_string}, _} =
List.keytake(headers, "content-disposition", 0)
[name | _] =
Regex.run(
~r/filename="((?:[^"\\]|\\.)*)"/u,
content_disposition_string || "",
capture: :all_but_first
)
name
rescue
MatchError -> Keyword.get(opts, :attachment_name, "attachment")
end
disposition = "attachment; filename=\"#{name}\""
List.keystore(headers, "content-disposition", 0, {"content-disposition", disposition}) List.keystore(headers, "content-disposition", 0, {"content-disposition", disposition})
else else
headers headers

View file

@ -4,8 +4,8 @@
defmodule Pleroma.Stats do defmodule Pleroma.Stats do
import Ecto.Query import Ecto.Query
alias Pleroma.User
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User
def start_link do def start_link do
agent = Agent.start_link(fn -> {[], %{}} end, name: __MODULE__) agent = Agent.start_link(fn -> {[], %{}} end, name: __MODULE__)

View file

@ -4,7 +4,11 @@
defmodule Pleroma.ThreadMute do defmodule Pleroma.ThreadMute do
use Ecto.Schema use Ecto.Schema
alias Pleroma.{Repo, User, ThreadMute}
alias Pleroma.Repo
alias Pleroma.ThreadMute
alias Pleroma.User
require Ecto.Query require Ecto.Query
schema "thread_mutes" do schema "thread_mutes" do

View file

@ -70,7 +70,7 @@ def store(upload, opts \\ []) do
%{ %{
"type" => "Link", "type" => "Link",
"mediaType" => upload.content_type, "mediaType" => upload.content_type,
"href" => url_from_spec(opts.base_url, url_spec) "href" => url_from_spec(upload, opts.base_url, url_spec)
} }
], ],
"name" => Map.get(opts, :description) || upload.name "name" => Map.get(opts, :description) || upload.name
@ -85,6 +85,10 @@ def store(upload, opts \\ []) do
end end
end end
def char_unescaped?(char) do
URI.char_unreserved?(char) or char == ?/
end
defp get_opts(opts) do defp get_opts(opts) do
{size_limit, activity_type} = {size_limit, activity_type} =
case Keyword.get(opts, :type) do case Keyword.get(opts, :type) do
@ -215,16 +219,18 @@ defp tempfile_for_image(data) do
tmp_path tmp_path
end end
defp url_from_spec(base_url, {:file, path}) do defp url_from_spec(%__MODULE__{name: name}, base_url, {:file, path}) do
path = path =
path URI.encode(path, &char_unescaped?/1) <>
|> URI.encode() if Pleroma.Config.get([__MODULE__, :link_name], false) do
|> String.replace("?", "%3F") "?name=#{URI.encode(name, &char_unescaped?/1)}"
|> String.replace(":", "%3A") else
""
end
[base_url, "media", path] [base_url, "media", path]
|> Path.join() |> Path.join()
end end
defp url_from_spec(_base_url, {:url, url}), do: url defp url_from_spec(_upload, _base_url, {:url, url}), do: url
end end

View file

@ -6,7 +6,8 @@ defmodule Pleroma.Uploaders.S3 do
@behaviour Pleroma.Uploaders.Uploader @behaviour Pleroma.Uploaders.Uploader
require Logger require Logger
# The file name is re-encoded with S3's constraints here to comply with previous links with less strict filenames # The file name is re-encoded with S3's constraints here to comply with previous
# links with less strict filenames
def get_file(file) do def get_file(file) do
config = Pleroma.Config.get([__MODULE__]) config = Pleroma.Config.get([__MODULE__])
bucket = Keyword.fetch!(config, :bucket) bucket = Keyword.fetch!(config, :bucket)

View file

@ -17,7 +17,7 @@ def process_response_body(body) do
|> Poison.decode!() |> Poison.decode!()
end end
def get_token() do def get_token do
settings = Pleroma.Config.get(Pleroma.Uploaders.Swift) settings = Pleroma.Config.get(Pleroma.Uploaders.Swift)
username = Keyword.fetch!(settings, :username) username = Keyword.fetch!(settings, :username)
password = Keyword.fetch!(settings, :password) password = Keyword.fetch!(settings, :password)

View file

@ -29,7 +29,6 @@ defmodule Pleroma.Uploaders.Uploader do
* `{:error, String.t}` error information if the file failed to be saved to the backend. * `{:error, String.t}` error information if the file failed to be saved to the backend.
* `:wait_callback` will wait for an http post request at `/api/pleroma/upload_callback/:upload_path` and call the uploader's `http_callback/3` method. * `:wait_callback` will wait for an http post request at `/api/pleroma/upload_callback/:upload_path` and call the uploader's `http_callback/3` method.
""" """
@type file_spec :: {:file | :url, String.t()} @type file_spec :: {:file | :url, String.t()}
@callback put_file(Pleroma.Upload.t()) :: @callback put_file(Pleroma.Upload.t()) ::

View file

@ -8,20 +8,21 @@ defmodule Pleroma.User do
import Ecto.Changeset import Ecto.Changeset
import Ecto.Query import Ecto.Query
alias Comeonin.Pbkdf2
alias Pleroma.Activity
alias Pleroma.Formatter
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Object
alias Pleroma.Web alias Pleroma.Web
alias Pleroma.Activity
alias Pleroma.Notification
alias Comeonin.Pbkdf2
alias Pleroma.Formatter
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
alias Pleroma.Web.OStatus
alias Pleroma.Web.Websub
alias Pleroma.Web.OAuth
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
alias Pleroma.Web.OAuth
alias Pleroma.Web.OStatus
alias Pleroma.Web.RelMe
alias Pleroma.Web.Websub
require Logger require Logger
@ -29,6 +30,7 @@ defmodule Pleroma.User do
@primary_key {:id, Pleroma.FlakeId, autogenerate: true} @primary_key {:id, Pleroma.FlakeId, autogenerate: true}
# credo:disable-for-next-line Credo.Check.Readability.MaxLineLength
@email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ @email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
@strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/ @strict_local_nickname_regex ~r/^[a-zA-Z\d]+$/
@ -284,7 +286,7 @@ def needs_update?(%User{local: true}), do: false
def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
def needs_update?(%User{local: false} = user) do def needs_update?(%User{local: false} = user) do
NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86400 NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86_400
end end
def needs_update?(_), do: true def needs_update?(_), do: true
@ -434,7 +436,8 @@ def get_by_ap_id(ap_id) do
Repo.get_by(User, ap_id: ap_id) Repo.get_by(User, ap_id: ap_id)
end end
# This is mostly an SPC migration fix. This guesses the user nickname (by taking the last part of the ap_id and the domain) and tries to get that user # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
# of the ap_id and the domain and tries to get that user
def get_by_guessed_nickname(ap_id) do def get_by_guessed_nickname(ap_id) do
domain = URI.parse(ap_id).host domain = URI.parse(ap_id).host
name = List.last(String.split(ap_id, "/")) name = List.last(String.split(ap_id, "/"))
@ -531,6 +534,10 @@ def get_or_fetch_by_nickname(nickname) do
_e -> _e ->
with [_nick, _domain] <- String.split(nickname, "@"), with [_nick, _domain] <- String.split(nickname, "@"),
{:ok, user} <- fetch_by_nickname(nickname) do {:ok, user} <- fetch_by_nickname(nickname) do
if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
{:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
end
user user
else else
_e -> nil _e -> nil
@ -538,6 +545,17 @@ def get_or_fetch_by_nickname(nickname) do
end end
end end
@doc "Fetch some posts when the user has just been federated with"
def fetch_initial_posts(user) do
pages = Pleroma.Config.get!([:fetch_initial_posts, :pages])
Enum.each(
# Insert all the posts in reverse order, so they're in the right order on the timeline
Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)),
&Pleroma.Web.Federator.incoming_ap_doc/1
)
end
def get_followers_query(%User{id: id, follower_address: follower_address}, nil) do def get_followers_query(%User{id: id, follower_address: follower_address}, nil) do
from( from(
u in User, u in User,
@ -547,11 +565,8 @@ def get_followers_query(%User{id: id, follower_address: follower_address}, nil)
end end
def get_followers_query(user, page) do def get_followers_query(user, page) do
from( from(u in get_followers_query(user, nil))
u in get_followers_query(user, nil), |> paginate(page, 20)
limit: 20,
offset: ^((page - 1) * 20)
)
end end
def get_followers_query(user), do: get_followers_query(user, nil) def get_followers_query(user), do: get_followers_query(user, nil)
@ -577,11 +592,8 @@ def get_friends_query(%User{id: id, following: following}, nil) do
end end
def get_friends_query(user, page) do def get_friends_query(user, page) do
from( from(u in get_friends_query(user, nil))
u in get_friends_query(user, nil), |> paginate(page, 20)
limit: 20,
offset: ^((page - 1) * 20)
)
end end
def get_friends_query(user), do: get_friends_query(user, nil) def get_friends_query(user), do: get_friends_query(user, nil)
@ -613,71 +625,65 @@ def get_follow_requests_query(%User{} = user) do
), ),
where: where:
fragment( fragment(
"? @> ?", "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
a.data, a.data,
^%{"object" => user.ap_id} a.data,
^user.ap_id
) )
) )
end end
def update_follow_request_count(%User{} = user) do
subquery =
user
|> User.get_follow_requests_query()
|> select([a], %{count: count(a.id)})
User
|> where(id: ^user.id)
|> join(:inner, [u], s in subquery(subquery))
|> update([u, s],
set: [
info:
fragment(
"jsonb_set(?, '{follow_request_count}', ?::varchar::jsonb, true)",
u.info,
s.count
)
]
)
|> Repo.update_all([], returning: true)
|> case do
{1, [user]} -> {:ok, user}
_ -> {:error, user}
end
end
def get_follow_requests(%User{} = user) do def get_follow_requests(%User{} = user) do
q = get_follow_requests_query(user)
reqs = Repo.all(q)
users = users =
Enum.map(reqs, fn req -> req.actor end) user
|> Enum.uniq() |> User.get_follow_requests_query()
|> Enum.map(fn ap_id -> get_by_ap_id(ap_id) end) |> join(:inner, [a], u in User, a.actor == u.ap_id)
|> Enum.filter(fn u -> !is_nil(u) end) |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
|> Enum.filter(fn u -> !following?(u, user) end) |> group_by([a, u], u.id)
|> select([a, u], u)
|> Repo.all()
{:ok, users} {:ok, users}
end end
def increase_note_count(%User{} = user) do def increase_note_count(%User{} = user) do
info_cng = User.Info.add_to_note_count(user.info, 1) User
|> where(id: ^user.id)
cng = |> update([u],
change(user) set: [
|> put_embed(:info, info_cng) info:
fragment(
update_and_set_cache(cng) "jsonb_set(?, '{note_count}', ((?->>'note_count')::int + 1)::varchar::jsonb, true)",
u.info,
u.info
)
]
)
|> Repo.update_all([], returning: true)
|> case do
{1, [user]} -> set_cache(user)
_ -> {:error, user}
end
end end
def decrease_note_count(%User{} = user) do def decrease_note_count(%User{} = user) do
info_cng = User.Info.add_to_note_count(user.info, -1) User
|> where(id: ^user.id)
cng = |> update([u],
change(user) set: [
|> put_embed(:info, info_cng) info:
fragment(
update_and_set_cache(cng) "jsonb_set(?, '{note_count}', (greatest(0, (?->>'note_count')::int - 1))::varchar::jsonb, true)",
u.info,
u.info
)
]
)
|> Repo.update_all([], returning: true)
|> case do
{1, [user]} -> set_cache(user)
_ -> {:error, user}
end
end end
def update_note_count(%User{} = user) do def update_note_count(%User{} = user) do
@ -701,24 +707,29 @@ def update_note_count(%User{} = user) do
def update_follower_count(%User{} = user) do def update_follower_count(%User{} = user) do
follower_count_query = follower_count_query =
from( User
u in User, |> where([u], ^user.follower_address in u.following)
where: ^user.follower_address in u.following, |> where([u], u.id != ^user.id)
where: u.id != ^user.id, |> select([u], %{count: count(u.id)})
select: count(u.id)
User
|> where(id: ^user.id)
|> join(:inner, [u], s in subquery(follower_count_query))
|> update([u, s],
set: [
info:
fragment(
"jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)",
u.info,
s.count
) )
]
follower_count = Repo.one(follower_count_query) )
|> Repo.update_all([], returning: true)
info_cng = |> case do
user.info {1, [user]} -> set_cache(user)
|> User.Info.set_follower_count(follower_count) _ -> {:error, user}
end
cng =
change(user)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
end end
def get_users_from_set_query(ap_ids, false) do def get_users_from_set_query(ap_ids, false) do
@ -755,6 +766,59 @@ def get_recipients_from_activity(%Activity{recipients: to}) do
Repo.all(query) Repo.all(query)
end end
@spec search_for_admin(%{
local: boolean(),
page: number(),
page_size: number()
}) :: {:ok, [Pleroma.User.t()], number()}
def search_for_admin(%{query: nil, local: local, page: page, page_size: page_size}) do
query =
from(u in User, order_by: u.id)
|> maybe_local_user_query(local)
paginated_query =
query
|> paginate(page, page_size)
count =
query
|> Repo.aggregate(:count, :id)
{:ok, Repo.all(paginated_query), count}
end
@spec search_for_admin(%{
query: binary(),
admin: Pleroma.User.t(),
local: boolean(),
page: number(),
page_size: number()
}) :: {:ok, [Pleroma.User.t()], number()}
def search_for_admin(%{
query: term,
admin: admin,
local: local,
page: page,
page_size: page_size
}) do
term = String.trim_leading(term, "@")
local_paginated_query =
User
|> maybe_local_user_query(local)
|> paginate(page, page_size)
search_query = fts_search_subquery(term, local_paginated_query)
count =
term
|> fts_search_subquery()
|> maybe_local_user_query(local)
|> Repo.aggregate(:count, :id)
{:ok, do_search(search_query, admin), count}
end
def search(query, resolve \\ false, for_user \\ nil) do def search(query, resolve \\ false, for_user \\ nil) do
# Strip the beginning @ off if there is a query # Strip the beginning @ off if there is a query
query = String.trim_leading(query, "@") query = String.trim_leading(query, "@")
@ -788,9 +852,9 @@ defp do_search(subquery, for_user, options \\ []) do
boost_search_results(results, for_user) boost_search_results(results, for_user)
end end
defp fts_search_subquery(query) do defp fts_search_subquery(term, query \\ User) do
processed_query = processed_query =
query term
|> String.replace(~r/\W+/, " ") |> String.replace(~r/\W+/, " ")
|> String.trim() |> String.trim()
|> String.split() |> String.split()
@ -798,7 +862,7 @@ defp fts_search_subquery(query) do
|> Enum.join(" | ") |> Enum.join(" | ")
from( from(
u in User, u in query,
select_merge: %{ select_merge: %{
search_rank: search_rank:
fragment( fragment(
@ -828,19 +892,19 @@ defp fts_search_subquery(query) do
) )
end end
defp trigram_search_subquery(query) do defp trigram_search_subquery(term) do
from( from(
u in User, u in User,
select_merge: %{ select_merge: %{
search_rank: search_rank:
fragment( fragment(
"similarity(?, trim(? || ' ' || coalesce(?, '')))", "similarity(?, trim(? || ' ' || coalesce(?, '')))",
^query, ^term,
u.nickname, u.nickname,
u.name u.name
) )
}, },
where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^query) where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
) )
end end
@ -954,6 +1018,7 @@ def unblock(blocker, %{ap_id: ap_id}) do
update_and_set_cache(cng) update_and_set_cache(cng)
end end
def mutes?(nil, _), do: false
def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id) def mutes?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.mutes, ap_id)
def blocks?(user, %{ap_id: ap_id}) do def blocks?(user, %{ap_id: ap_id}) do
@ -997,9 +1062,13 @@ def unblock_domain(user, domain) do
update_and_set_cache(cng) update_and_set_cache(cng)
end end
def local_user_query do def maybe_local_user_query(query, local) do
if local, do: local_user_query(query), else: query
end
def local_user_query(query \\ User) do
from( from(
u in User, u in query,
where: u.local == true, where: u.local == true,
where: not is_nil(u.nickname) where: not is_nil(u.nickname)
) )
@ -1069,12 +1138,7 @@ def html_filter_policy(%User{info: %{no_rich_text: true}}) do
def html_filter_policy(_), do: @default_scrubbers def html_filter_policy(_), do: @default_scrubbers
def get_or_fetch_by_ap_id(ap_id) do def fetch_by_ap_id(ap_id) do
user = get_by_ap_id(ap_id)
if !is_nil(user) and !User.needs_update?(user) do
user
else
ap_try = ActivityPub.make_user_from_ap_id(ap_id) ap_try = ActivityPub.make_user_from_ap_id(ap_id)
case ap_try do case ap_try do
@ -1088,6 +1152,23 @@ def get_or_fetch_by_ap_id(ap_id) do
end end
end end
end end
def get_or_fetch_by_ap_id(ap_id) do
user = get_by_ap_id(ap_id)
if !is_nil(user) and !User.needs_update?(user) do
user
else
user = fetch_by_ap_id(ap_id)
if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
with %User{} = user do
{:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
end
end
user
end
end end
def get_or_create_instance_user do def get_or_create_instance_user do
@ -1187,9 +1268,6 @@ def parse_bio(nil, _user), do: ""
def parse_bio(bio, _user) when bio == "", do: bio def parse_bio(bio, _user) when bio == "", do: bio
def parse_bio(bio, user) do def parse_bio(bio, user) do
mentions = Formatter.parse_mentions(bio)
tags = Formatter.parse_tags(bio)
emoji = emoji =
(user.info.source_data["tag"] || []) (user.info.source_data["tag"] || [])
|> Enum.filter(fn %{"type" => t} -> t == "Emoji" end) |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
@ -1197,8 +1275,15 @@ def parse_bio(bio, user) do
{String.trim(name, ":"), url} {String.trim(name, ":"), url}
end) end)
# TODO: get profile URLs other than user.ap_id
profile_urls = [user.ap_id]
bio bio
|> CommonUtils.format_input(mentions, tags, "text/plain", user_links: [format: :full]) |> CommonUtils.format_input("text/plain",
mentions_format: :full,
rel: &RelMe.maybe_put_rel_me(&1, profile_urls)
)
|> elem(0)
|> Formatter.emojify(emoji) |> Formatter.emojify(emoji)
end end
@ -1257,7 +1342,7 @@ defp normalize_tags(tags) do
|> Enum.map(&String.downcase(&1)) |> Enum.map(&String.downcase(&1))
end end
defp local_nickname_regex() do defp local_nickname_regex do
if Pleroma.Config.get([:instance, :extended_nickname_format]) do if Pleroma.Config.get([:instance, :extended_nickname_format]) do
@extended_local_nickname_regex @extended_local_nickname_regex
else else
@ -1293,4 +1378,15 @@ def all_superusers do
) )
|> Repo.all() |> Repo.all()
end end
defp paginate(query, page, page_size) do
from(u in query,
limit: ^page_size,
offset: ^((page - 1) * page_size)
)
end
def showing_reblogs?(%User{} = user, %User{} = target) do
target.ap_id not in user.info.muted_reblogs
end
end end

View file

@ -6,13 +6,14 @@ defmodule Pleroma.User.Info do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
alias Pleroma.User.Info
embedded_schema do embedded_schema do
field(:banner, :map, default: %{}) field(:banner, :map, default: %{})
field(:background, :map, default: %{}) field(:background, :map, default: %{})
field(:source_data, :map, default: %{}) field(:source_data, :map, default: %{})
field(:note_count, :integer, default: 0) field(:note_count, :integer, default: 0)
field(:follower_count, :integer, default: 0) field(:follower_count, :integer, default: 0)
field(:follow_request_count, :integer, default: 0)
field(:locked, :boolean, default: false) field(:locked, :boolean, default: false)
field(:confirmation_pending, :boolean, default: false) field(:confirmation_pending, :boolean, default: false)
field(:confirmation_token, :string, default: nil) field(:confirmation_token, :string, default: nil)
@ -20,6 +21,7 @@ defmodule Pleroma.User.Info do
field(:blocks, {:array, :string}, default: []) field(:blocks, {:array, :string}, default: [])
field(:domain_blocks, {:array, :string}, default: []) field(:domain_blocks, {:array, :string}, default: [])
field(:mutes, {:array, :string}, default: []) field(:mutes, {:array, :string}, default: [])
field(:muted_reblogs, {:array, :string}, default: [])
field(:deactivated, :boolean, default: false) field(:deactivated, :boolean, default: false)
field(:no_rich_text, :boolean, default: false) field(:no_rich_text, :boolean, default: false)
field(:ap_enabled, :boolean, default: false) field(:ap_enabled, :boolean, default: false)
@ -251,4 +253,23 @@ def remove_pinnned_activity(info, %Pleroma.Activity{id: id}) do
cast(info, params, [:pinned_activities]) cast(info, params, [:pinned_activities])
end end
def roles(%Info{is_moderator: is_moderator, is_admin: is_admin}) do
%{
admin: is_admin,
moderator: is_moderator
}
end
def add_reblog_mute(info, ap_id) do
params = %{muted_reblogs: info.muted_reblogs ++ [ap_id]}
cast(info, params, [:muted_reblogs])
end
def remove_reblog_mute(info, ap_id) do
params = %{muted_reblogs: List.delete(info.muted_reblogs, ap_id)}
cast(info, params, [:muted_reblogs])
end
end end

View file

@ -14,7 +14,7 @@ def post_welcome_message_to_user(user) do
end end
end end
defp welcome_user() do defp welcome_user do
with nickname when is_binary(nickname) <- with nickname when is_binary(nickname) <-
Pleroma.Config.get([:instance, :welcome_user_nickname]), Pleroma.Config.get([:instance, :welcome_user_nickname]),
%User{local: true} = user <- User.get_cached_by_nickname(nickname) do %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
@ -24,7 +24,7 @@ defp welcome_user() do
end end
end end
defp welcome_message() do defp welcome_message do
Pleroma.Config.get([:instance, :welcome_message]) Pleroma.Config.get([:instance, :welcome_message])
end end
end end

View file

@ -7,8 +7,8 @@ defmodule Pleroma.UserInviteToken do
import Ecto.Changeset import Ecto.Changeset
alias Pleroma.UserInviteToken
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.UserInviteToken
schema "user_invite_tokens" do schema "user_invite_tokens" do
field(:token, :string) field(:token, :string)

View file

@ -4,17 +4,17 @@
defmodule Pleroma.Web.ActivityPub.ActivityPub do defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Repo alias Pleroma.Instances
alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.Upload alias Pleroma.Upload
alias Pleroma.User alias Pleroma.User
alias Pleroma.Notification
alias Pleroma.Instances
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.WebFinger alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.Federator alias Pleroma.Web.Federator
alias Pleroma.Web.OStatus alias Pleroma.Web.OStatus
alias Pleroma.Web.WebFinger
import Ecto.Query import Ecto.Query
import Pleroma.Web.ActivityPub.Utils import Pleroma.Web.ActivityPub.Utils
@ -81,6 +81,14 @@ defp check_remote_limit(%{"object" => %{"content" => content}}) when not is_nil(
defp check_remote_limit(_), do: true defp check_remote_limit(_), do: true
def increase_note_count_if_public(actor, object) do
if is_public?(object), do: User.increase_note_count(actor), else: {:ok, actor}
end
def decrease_note_count_if_public(actor, object) do
if is_public?(object), do: User.decrease_note_count(actor), else: {:ok, actor}
end
def insert(map, local \\ true) when is_map(map) do def insert(map, local \\ true) when is_map(map) do
with nil <- Activity.normalize(map), with nil <- Activity.normalize(map),
map <- lazy_put_activity_defaults(map), map <- lazy_put_activity_defaults(map),
@ -162,8 +170,9 @@ def create(%{to: to, actor: actor, context: context, object: object} = params) d
additional additional
), ),
{:ok, activity} <- insert(create_data, local), {:ok, activity} <- insert(create_data, local),
# Changing note count prior to enqueuing federation task in order to avoid race conditions on updating user.info # Changing note count prior to enqueuing federation task in order to avoid
{:ok, _actor} <- User.increase_note_count(actor), # race conditions on updating user.info
{:ok, _actor} <- increase_note_count_if_public(actor, activity),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity} {:ok, activity}
end end
@ -175,8 +184,7 @@ def accept(%{to: to, actor: actor, object: object} = params) do
with data <- %{"to" => to, "type" => "Accept", "actor" => actor.ap_id, "object" => object}, with data <- %{"to" => to, "type" => "Accept", "actor" => actor.ap_id, "object" => object},
{:ok, activity} <- insert(data, local), {:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity), :ok <- maybe_federate(activity) do
_ <- User.update_follow_request_count(actor) do
{:ok, activity} {:ok, activity}
end end
end end
@ -187,8 +195,7 @@ def reject(%{to: to, actor: actor, object: object} = params) do
with data <- %{"to" => to, "type" => "Reject", "actor" => actor.ap_id, "object" => object}, with data <- %{"to" => to, "type" => "Reject", "actor" => actor.ap_id, "object" => object},
{:ok, activity} <- insert(data, local), {:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity), :ok <- maybe_federate(activity) do
_ <- User.update_follow_request_count(actor) do
{:ok, activity} {:ok, activity}
end end
end end
@ -286,8 +293,7 @@ def unannounce(
def follow(follower, followed, activity_id \\ nil, local \\ true) do def follow(follower, followed, activity_id \\ nil, local \\ true) do
with data <- make_follow_data(follower, followed, activity_id), with data <- make_follow_data(follower, followed, activity_id),
{:ok, activity} <- insert(data, local), {:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity), :ok <- maybe_federate(activity) do
_ <- User.update_follow_request_count(followed) do
{:ok, activity} {:ok, activity}
end end
end end
@ -297,26 +303,27 @@ def unfollow(follower, followed, activity_id \\ nil, local \\ true) do
{:ok, follow_activity} <- update_follow_state(follow_activity, "cancelled"), {:ok, follow_activity} <- update_follow_state(follow_activity, "cancelled"),
unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id), unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id),
{:ok, activity} <- insert(unfollow_data, local), {:ok, activity} <- insert(unfollow_data, local),
:ok <- maybe_federate(activity), :ok <- maybe_federate(activity) do
_ <- User.update_follow_request_count(followed) do
{:ok, activity} {:ok, activity}
end end
end end
def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ true) do def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ true) do
user = User.get_cached_by_ap_id(actor) user = User.get_cached_by_ap_id(actor)
to = (object.data["to"] || []) ++ (object.data["cc"] || [])
data = %{ with {:ok, object, activity} <- Object.delete(object),
data <- %{
"type" => "Delete", "type" => "Delete",
"actor" => actor, "actor" => actor,
"object" => id, "object" => id,
"to" => [user.follower_address, "https://www.w3.org/ns/activitystreams#Public"] "to" => to,
} "deleted_activity_id" => activity && activity.id
},
with {:ok, _} <- Object.delete(object),
{:ok, activity} <- insert(data, local), {:ok, activity} <- insert(data, local),
# Changing note count prior to enqueuing federation task in order to avoid race conditions on updating user.info # Changing note count prior to enqueuing federation task in order to avoid
{:ok, _actor} <- User.decrease_note_count(user), # race conditions on updating user.info
{:ok, _actor} <- decrease_note_count_if_public(user, object),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity} {:ok, activity}
end end
@ -363,20 +370,38 @@ def flag(
content: content content: content
} = params } = params
) do ) do
additional = params[:additional] || %{}
# only accept false as false value # only accept false as false value
local = !(params[:local] == false) local = !(params[:local] == false)
forward = !(params[:forward] == false)
%{ additional = params[:additional] || %{}
params = %{
actor: actor, actor: actor,
context: context, context: context,
account: account, account: account,
statuses: statuses, statuses: statuses,
content: content content: content
} }
|> make_flag_data(additional)
|> insert(local) additional =
if forward do
Map.merge(additional, %{"to" => [], "cc" => [account.ap_id]})
else
Map.merge(additional, %{"to" => [], "cc" => []})
end
with flag_data <- make_flag_data(params, additional),
{:ok, activity} <- insert(flag_data, local),
:ok <- maybe_federate(activity) do
Enum.each(User.all_superusers(), fn superuser ->
superuser
|> Pleroma.AdminEmail.report(actor, account, statuses, content)
|> Pleroma.Mailer.deliver_async()
end)
{:ok, activity}
end
end end
def fetch_activities_for_context(context, opts \\ %{}) do def fetch_activities_for_context(context, opts \\ %{}) do
@ -420,6 +445,30 @@ def fetch_public_activities(opts \\ %{}) do
@valid_visibilities ~w[direct unlisted public private] @valid_visibilities ~w[direct unlisted public private]
defp restrict_visibility(query, %{visibility: visibility})
when is_list(visibility) do
if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
query =
from(
a in query,
where:
fragment(
"activity_visibility(?, ?, ?) = ANY (?)",
a.actor,
a.recipients,
a.data,
^visibility
)
)
Ecto.Adapters.SQL.to_sql(:all, Repo, query)
query
else
Logger.error("Could not restrict visibility to #{visibility}")
end
end
defp restrict_visibility(query, %{visibility: visibility}) defp restrict_visibility(query, %{visibility: visibility})
when visibility in @valid_visibilities do when visibility in @valid_visibilities do
query = query =
@ -473,7 +522,7 @@ defp restrict_tag_reject(query, %{"tag_reject" => tag_reject})
when is_list(tag_reject) and tag_reject != [] do when is_list(tag_reject) and tag_reject != [] do
from( from(
activity in query, activity in query,
where: fragment("(not (? #> '{\"object\",\"tag\"}') \\?| ?)", activity.data, ^tag_reject) where: fragment(~s(\(not \(? #> '{"object","tag"}'\) \\?| ?\)), activity.data, ^tag_reject)
) )
end end
@ -483,7 +532,7 @@ defp restrict_tag_all(query, %{"tag_all" => tag_all})
when is_list(tag_all) and tag_all != [] do when is_list(tag_all) and tag_all != [] do
from( from(
activity in query, activity in query,
where: fragment("(? #> '{\"object\",\"tag\"}') \\?& ?", activity.data, ^tag_all) where: fragment(~s(\(? #> '{"object","tag"}'\) \\?& ?), activity.data, ^tag_all)
) )
end end
@ -492,14 +541,14 @@ defp restrict_tag_all(query, _), do: query
defp restrict_tag(query, %{"tag" => tag}) when is_list(tag) do defp restrict_tag(query, %{"tag" => tag}) when is_list(tag) do
from( from(
activity in query, activity in query,
where: fragment("(? #> '{\"object\",\"tag\"}') \\?| ?", activity.data, ^tag) where: fragment(~s(\(? #> '{"object","tag"}'\) \\?| ?), activity.data, ^tag)
) )
end end
defp restrict_tag(query, %{"tag" => tag}) when is_binary(tag) do defp restrict_tag(query, %{"tag" => tag}) when is_binary(tag) do
from( from(
activity in query, activity in query,
where: fragment("? <@ (? #> '{\"object\",\"tag\"}')", ^tag, activity.data) where: fragment(~s(? <@ (? #> '{"object","tag"}'\)), ^tag, activity.data)
) )
end end
@ -572,7 +621,7 @@ defp restrict_type(query, _), do: query
defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do
from( from(
activity in query, activity in query,
where: fragment("? <@ (? #> '{\"object\",\"likes\"}')", ^ap_id, activity.data) where: fragment(~s(? <@ (? #> '{"object","likes"}'\)), ^ap_id, activity.data)
) )
end end
@ -581,7 +630,7 @@ defp restrict_favorited_by(query, _), do: query
defp restrict_media(query, %{"only_media" => val}) when val == "true" or val == "1" do defp restrict_media(query, %{"only_media" => val}) when val == "true" or val == "1" do
from( from(
activity in query, activity in query,
where: fragment("not (? #> '{\"object\",\"attachment\"}' = ?)", activity.data, ^[]) where: fragment(~s(not (? #> '{"object","attachment"}' = ?\)), activity.data, ^[])
) )
end end
@ -602,6 +651,8 @@ defp restrict_reblogs(query, %{"exclude_reblogs" => val}) when val == "true" or
defp restrict_reblogs(query, _), do: query defp restrict_reblogs(query, _), do: query
defp restrict_muted(query, %{"with_muted" => val}) when val in [true, "true", "1"], do: query
defp restrict_muted(query, %{"muting_user" => %User{info: info}}) do defp restrict_muted(query, %{"muting_user" => %User{info: info}}) do
mutes = info.mutes mutes = info.mutes
@ -646,6 +697,18 @@ defp restrict_pinned(query, %{"pinned" => "true", "pinned_activity_ids" => ids})
defp restrict_pinned(query, _), do: query defp restrict_pinned(query, _), do: query
defp restrict_muted_reblogs(query, %{"muting_user" => %User{info: info}}) do
muted_reblogs = info.muted_reblogs || []
from(
activity in query,
where: fragment("not ?->>'type' = 'Announce'", activity.data),
where: fragment("not ? = ANY(?)", activity.actor, ^muted_reblogs)
)
end
defp restrict_muted_reblogs(query, _), do: query
def fetch_activities_query(recipients, opts \\ %{}) do def fetch_activities_query(recipients, opts \\ %{}) do
base_query = base_query =
from( from(
@ -673,6 +736,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|> restrict_replies(opts) |> restrict_replies(opts)
|> restrict_reblogs(opts) |> restrict_reblogs(opts)
|> restrict_pinned(opts) |> restrict_pinned(opts)
|> restrict_muted_reblogs(opts)
end end
def fetch_activities(recipients, opts \\ %{}) do def fetch_activities(recipients, opts \\ %{}) do
@ -826,7 +890,7 @@ def publish_one(%{inbox: inbox, json: json, actor: actor, id: id} = params) do
date = date =
NaiveDateTime.utc_now() NaiveDateTime.utc_now()
|> Timex.format!("{WDshort}, {D} {Mshort} {YYYY} {h24}:{m}:{s} GMT") |> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT")
signature = signature =
Pleroma.Web.HTTPSignatures.sign(actor, %{ Pleroma.Web.HTTPSignatures.sign(actor, %{

View file

@ -6,15 +6,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.User
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Web.ActivityPub.ObjectView alias Pleroma.User
alias Pleroma.Web.ActivityPub.UserView
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.ActivityPub.ObjectView
alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.UserView
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Federator alias Pleroma.Web.Federator
require Logger require Logger

View file

@ -16,7 +16,7 @@ def filter(object) do
end) end)
end end
def get_policies() do def get_policies do
Application.get_env(:pleroma, :instance, []) Application.get_env(:pleroma, :instance, [])
|> Keyword.get(:rewrite_policy, []) |> Keyword.get(:rewrite_policy, [])
|> get_policies() |> get_policies()

View file

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

View file

@ -45,13 +45,14 @@ defp check_ftl_removal(
defp check_replace(%{"object" => %{"content" => content, "summary" => summary}} = message) do defp check_replace(%{"object" => %{"content" => content, "summary" => summary}} = message) do
{content, summary} = {content, summary} =
Enum.reduce(Pleroma.Config.get([:mrf_keyword, :replace]), {content, summary}, fn {pattern, Enum.reduce(
replacement}, Pleroma.Config.get([:mrf_keyword, :replace]),
{content_acc, {content, summary},
summary_acc} -> fn {pattern, replacement}, {content_acc, summary_acc} ->
{String.replace(content_acc, pattern, replacement), {String.replace(content_acc, pattern, replacement),
String.replace(summary_acc, pattern, replacement)} String.replace(summary_acc, pattern, replacement)}
end) end
)
{:ok, {:ok,
message message

View file

@ -3,9 +3,9 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Relay do defmodule Pleroma.Web.ActivityPub.Relay do
alias Pleroma.User
alias Pleroma.Object
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
require Logger require Logger

View file

@ -7,9 +7,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
A module to handle coding from internal to wire ActivityPub and back. A module to handle coding from internal to wire ActivityPub and back.
""" """
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.User
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.ActivityPub.Visibility
@ -355,6 +355,40 @@ defp get_follow_activity(follow_object, followed) do
end end
end end
# Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
# with nil ID.
def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data) 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 <-
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),
# Remove the reported user from the object list.
statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
params = %{
actor: actor,
context: context,
account: account,
statuses: statuses,
content: content,
additional: %{
"cc" => [account.ap_id]
}
}
ActivityPub.flag(params)
end
end
# disallow objects with bogus IDs # disallow objects with bogus IDs
def handle_incoming(%{"id" => nil}), do: :error def handle_incoming(%{"id" => nil}), do: :error
def handle_incoming(%{"id" => ""}), do: :error def handle_incoming(%{"id" => ""}), do: :error
@ -650,10 +684,10 @@ def get_obj_helper(id) do
if object = Object.normalize(id), do: {:ok, object}, else: nil if object = Object.normalize(id), do: {:ok, object}, else: nil
end end
def set_reply_to_uri(%{"inReplyTo" => inReplyTo} = object) when is_binary(inReplyTo) do def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
with false <- String.starts_with?(inReplyTo, "http"), with false <- String.starts_with?(in_reply_to, "http"),
{:ok, %{data: replied_to_object}} <- get_obj_helper(inReplyTo) do {:ok, %{data: replied_to_object}} <- get_obj_helper(in_reply_to) do
Map.put(object, "inReplyTo", replied_to_object["external_url"] || inReplyTo) Map.put(object, "inReplyTo", replied_to_object["external_url"] || in_reply_to)
else else
_e -> object _e -> object
end end
@ -736,6 +770,7 @@ def prepare_outgoing(%{"type" => "Reject"} = data) do
def prepare_outgoing(%{"type" => _type} = data) do def prepare_outgoing(%{"type" => _type} = data) do
data = data =
data data
|> strip_internal_fields
|> maybe_fix_object_url |> maybe_fix_object_url
|> Map.merge(Utils.make_json_ld_header()) |> Map.merge(Utils.make_json_ld_header())
@ -829,10 +864,10 @@ def set_sensitive(object) do
end end
def add_attributed_to(object) do def add_attributed_to(object) do
attributedTo = object["attributedTo"] || object["actor"] attributed_to = object["attributedTo"] || object["actor"]
object object
|> Map.put("attributedTo", attributedTo) |> Map.put("attributedTo", attributed_to)
end end
def add_likes(%{"id" => id, "like_count" => likes} = object) do def add_likes(%{"id" => id, "like_count" => likes} = object) do
@ -870,7 +905,8 @@ defp strip_internal_fields(object) do
"announcements", "announcements",
"announcement_count", "announcement_count",
"emoji", "emoji",
"context_id" "context_id",
"deleted_activity_id"
]) ])
end end

View file

@ -3,16 +3,17 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Utils do defmodule Pleroma.Web.ActivityPub.Utils do
alias Pleroma.Repo
alias Pleroma.Web
alias Pleroma.Object
alias Pleroma.Activity
alias Pleroma.User
alias Pleroma.Notification
alias Pleroma.Web.Router.Helpers
alias Pleroma.Web.Endpoint
alias Ecto.Changeset alias Ecto.Changeset
alias Ecto.UUID alias Ecto.UUID
alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Router.Helpers
import Ecto.Query import Ecto.Query
@ -274,13 +275,31 @@ def get_object_likes(%{data: %{"id" => id}}) do
Repo.all(query) Repo.all(query)
end end
def make_like_data(%User{ap_id: ap_id} = actor, %{data: %{"id" => id}} = object, activity_id) do def make_like_data(
%User{ap_id: ap_id} = actor,
%{data: %{"actor" => object_actor_id, "id" => id}} = object,
activity_id
) do
object_actor = User.get_cached_by_ap_id(object_actor_id)
to =
if Visibility.is_public?(object) do
[actor.follower_address, object.data["actor"]]
else
[object.data["actor"]]
end
cc =
(object.data["to"] ++ (object.data["cc"] || []))
|> List.delete(actor.ap_id)
|> List.delete(object_actor.follower_address)
data = %{ data = %{
"type" => "Like", "type" => "Like",
"actor" => ap_id, "actor" => ap_id,
"object" => id, "object" => id,
"to" => [actor.follower_address, object.data["actor"]], "to" => to,
"cc" => ["https://www.w3.org/ns/activitystreams#Public"], "cc" => cc,
"context" => object.data["context"] "context" => object.data["context"]
} }
@ -602,7 +621,13 @@ def make_create_data(params, additional) do
#### Flag-related helpers #### Flag-related helpers
def make_flag_data(params, additional) do def make_flag_data(params, additional) do
status_ap_ids = Enum.map(params.statuses || [], & &1.data["id"]) status_ap_ids =
Enum.map(params.statuses || [], fn
%Activity{} = act -> act.data["id"]
act when is_map(act) -> act["id"]
act when is_binary(act) -> act
end)
object = [params.account.ap_id] ++ status_ap_ids object = [params.account.ap_id] ++ status_ap_ids
%{ %{
@ -614,4 +639,43 @@ def make_flag_data(params, additional) do
} }
|> Map.merge(additional) |> Map.merge(additional)
end end
@doc """
Fetches the OrderedCollection/OrderedCollectionPage from `from`, limiting the amount of pages fetched after
the first one to `pages_left` pages.
If the amount of pages is higher than the collection has, it returns whatever was there.
"""
def fetch_ordered_collection(from, pages_left, acc \\ []) do
with {:ok, response} <- Tesla.get(from),
{:ok, collection} <- Poison.decode(response.body) do
case collection["type"] do
"OrderedCollection" ->
# If we've encountered the OrderedCollection and not the page,
# just call the same function on the page address
fetch_ordered_collection(collection["first"], pages_left)
"OrderedCollectionPage" ->
if pages_left > 0 do
# There are still more pages
if Map.has_key?(collection, "next") do
# There are still more pages, go deeper saving what we have into the accumulator
fetch_ordered_collection(
collection["next"],
pages_left - 1,
acc ++ collection["orderedItems"]
)
else
# No more pages left, just return whatever we already have
acc ++ collection["orderedItems"]
end
else
# Got the amount of pages needed, add them all to the accumulator
acc ++ collection["orderedItems"]
end
_ ->
{:error, "Not an OrderedCollection or OrderedCollectionPage"}
end
end
end
end end

View file

@ -5,15 +5,15 @@
defmodule Pleroma.Web.ActivityPub.UserView do defmodule Pleroma.Web.ActivityPub.UserView do
use Pleroma.Web, :view use Pleroma.Web, :view
alias Pleroma.Web.WebFinger
alias Pleroma.Web.Salmon
alias Pleroma.User
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Router.Helpers
alias Pleroma.Web.Endpoint alias Pleroma.Web.Endpoint
alias Pleroma.Web.Router.Helpers
alias Pleroma.Web.Salmon
alias Pleroma.Web.WebFinger
import Ecto.Query import Ecto.Query

View file

@ -3,9 +3,12 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.AdminAPIController do defmodule Pleroma.Web.AdminAPI.AdminAPIController do
@users_page_size 50
use Pleroma.Web, :controller use Pleroma.Web, :controller
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.AdminAPI.AccountView
import Pleroma.Web.ControllerHelper, only: [json_response: 3] import Pleroma.Web.ControllerHelper, only: [json_response: 3]
@ -41,6 +44,15 @@ def user_create(
|> json(user.nickname) |> json(user.nickname)
end end
def user_toggle_activation(conn, %{"nickname" => nickname}) do
user = User.get_by_nickname(nickname)
{:ok, updated_user} = User.deactivate(user, !user.info.deactivated)
conn
|> json(AccountView.render("show.json", %{user: updated_user}))
end
def tag_users(conn, %{"nicknames" => nicknames, "tags" => tags}) do def tag_users(conn, %{"nicknames" => nicknames, "tags" => tags}) do
with {:ok, _} <- User.tag(nicknames, tags), with {:ok, _} <- User.tag(nicknames, tags),
do: json_response(conn, :no_content, "") do: json_response(conn, :no_content, "")
@ -51,6 +63,28 @@ def untag_users(conn, %{"nicknames" => nicknames, "tags" => tags}) do
do: json_response(conn, :no_content, "") do: json_response(conn, :no_content, "")
end end
def list_users(%{assigns: %{user: admin}} = conn, params) do
{page, page_size} = page_params(params)
with {:ok, users, count} <-
User.search_for_admin(%{
query: params["query"],
admin: admin,
local: params["local_only"] == "true",
page: page,
page_size: page_size
}),
do:
conn
|> json(
AccountView.render("index.json",
users: users,
count: count,
page_size: page_size
)
)
end
def right_add(conn, %{"permission_group" => permission_group, "nickname" => nickname}) def right_add(conn, %{"permission_group" => permission_group, "nickname" => nickname})
when permission_group in ["moderator", "admin"] do when permission_group in ["moderator", "admin"] do
user = User.get_by_nickname(nickname) user = User.get_by_nickname(nickname)
@ -194,4 +228,26 @@ def errors(conn, _) do
|> put_status(500) |> put_status(500)
|> json("Something went wrong") |> json("Something went wrong")
end end
defp page_params(params) do
{get_page(params["page"]), get_page_size(params["page_size"])}
end
defp get_page(page_string) when is_nil(page_string), do: 1
defp get_page(page_string) do
case Integer.parse(page_string) do
{page, _} -> page
:error -> 1
end
end
defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
defp get_page_size(page_size_string) do
case Integer.parse(page_size_string) do
{page_size, _} -> page_size
:error -> @users_page_size
end
end
end end

View file

@ -0,0 +1,29 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.AccountView do
use Pleroma.Web, :view
alias Pleroma.User.Info
alias Pleroma.Web.AdminAPI.AccountView
def render("index.json", %{users: users, count: count, page_size: page_size}) do
%{
users: render_many(users, AccountView, "show.json", as: :user),
count: count,
page_size: page_size
}
end
def render("show.json", %{user: user}) do
%{
"id" => user.id,
"nickname" => user.nickname,
"deactivated" => user.info.deactivated,
"local" => user.local,
"roles" => Info.roles(user.info),
"tags" => user.tags || []
}
end
end

View file

@ -0,0 +1,25 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Auth.Authenticator do
alias Pleroma.User
def implementation do
Pleroma.Config.get(
Pleroma.Web.Auth.Authenticator,
Pleroma.Web.Auth.PleromaAuthenticator
)
end
@callback get_user(Plug.Conn.t()) :: {:ok, User.t()} | {:error, any()}
def get_user(plug), do: implementation().get_user(plug)
@callback handle_error(Plug.Conn.t(), any()) :: any()
def handle_error(plug, error), do: implementation().handle_error(plug, error)
@callback auth_template() :: String.t() | nil
def auth_template do
implementation().auth_template() || Pleroma.Config.get(:auth_template, "show.html")
end
end

View file

@ -0,0 +1,143 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Auth.LDAPAuthenticator do
alias Pleroma.User
require Logger
@behaviour Pleroma.Web.Auth.Authenticator
@connection_timeout 10_000
@search_timeout 10_000
def get_user(%Plug.Conn{} = conn) do
if Pleroma.Config.get([:ldap, :enabled]) do
{name, password} =
case conn.params do
%{"authorization" => %{"name" => name, "password" => password}} ->
{name, password}
%{"grant_type" => "password", "username" => name, "password" => password} ->
{name, password}
end
case ldap_user(name, password) do
%User{} = user ->
{:ok, user}
{:error, {:ldap_connection_error, _}} ->
# When LDAP is unavailable, try default authenticator
Pleroma.Web.Auth.PleromaAuthenticator.get_user(conn)
error ->
error
end
else
# Fall back to default authenticator
Pleroma.Web.Auth.PleromaAuthenticator.get_user(conn)
end
end
def handle_error(%Plug.Conn{} = _conn, error) do
error
end
def auth_template, do: nil
defp ldap_user(name, password) do
ldap = Pleroma.Config.get(:ldap, [])
host = Keyword.get(ldap, :host, "localhost")
port = Keyword.get(ldap, :port, 389)
ssl = Keyword.get(ldap, :ssl, false)
sslopts = Keyword.get(ldap, :sslopts, [])
options =
[{:port, port}, {:ssl, ssl}, {:timeout, @connection_timeout}] ++
if sslopts != [], do: [{:sslopts, sslopts}], else: []
case :eldap.open([to_charlist(host)], options) do
{:ok, connection} ->
try do
if Keyword.get(ldap, :tls, false) do
:application.ensure_all_started(:ssl)
case :eldap.start_tls(
connection,
Keyword.get(ldap, :tlsopts, []),
@connection_timeout
) do
:ok ->
:ok
error ->
Logger.error("Could not start TLS: #{inspect(error)}")
end
end
bind_user(connection, ldap, name, password)
after
:eldap.close(connection)
end
{:error, error} ->
Logger.error("Could not open LDAP connection: #{inspect(error)}")
{:error, {:ldap_connection_error, error}}
end
end
defp bind_user(connection, ldap, name, password) do
uid = Keyword.get(ldap, :uid, "cn")
base = Keyword.get(ldap, :base)
case :eldap.simple_bind(connection, "#{uid}=#{name},#{base}", password) do
:ok ->
case User.get_by_nickname_or_email(name) do
%User{} = user ->
user
_ ->
register_user(connection, base, uid, name, password)
end
error ->
error
end
end
defp register_user(connection, base, uid, name, password) do
case :eldap.search(connection, [
{:base, to_charlist(base)},
{:filter, :eldap.equalityMatch(to_charlist(uid), to_charlist(name))},
{:scope, :eldap.wholeSubtree()},
{:attributes, ['mail', 'email']},
{:timeout, @search_timeout}
]) do
{:ok, {:eldap_search_result, [{:eldap_entry, _, attributes}], _}} ->
with {_, [mail]} <- List.keyfind(attributes, 'mail', 0) do
params = %{
email: :erlang.list_to_binary(mail),
name: name,
nickname: name,
password: password,
password_confirmation: password
}
changeset = User.register_changeset(%User{}, params)
case User.register(changeset) do
{:ok, user} -> user
error -> error
end
else
_ ->
Logger.error("Could not find LDAP attribute mail: #{inspect(attributes)}")
{:error, :ldap_registration_missing_attributes}
end
error ->
error
end
end
end

View file

@ -0,0 +1,35 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Auth.PleromaAuthenticator do
alias Comeonin.Pbkdf2
alias Pleroma.User
@behaviour Pleroma.Web.Auth.Authenticator
def get_user(%Plug.Conn{} = conn) do
{name, password} =
case conn.params do
%{"authorization" => %{"name" => name, "password" => password}} ->
{name, password}
%{"grant_type" => "password", "username" => name, "password" => password} ->
{name, password}
end
with {_, %User{} = user} <- {:user, User.get_by_nickname_or_email(name)},
{_, true} <- {:checkpw, Pbkdf2.checkpw(password, user.password_hash)} do
{:ok, user}
else
error ->
{:error, error}
end
end
def handle_error(%Plug.Conn{} = _conn, error) do
error
end
def auth_template, do: nil
end

View file

@ -23,7 +23,7 @@ defmodule Pleroma.Web.UserSocket do
# performing token verification on connect. # performing token verification on connect.
def connect(%{"token" => token}, socket) do def connect(%{"token" => token}, socket) do
with true <- Pleroma.Config.get([:chat, :enabled]), with true <- Pleroma.Config.get([:chat, :enabled]),
{:ok, user_id} <- Phoenix.Token.verify(socket, "user socket", token, max_age: 84600), {:ok, user_id} <- Phoenix.Token.verify(socket, "user socket", token, max_age: 84_600),
%User{} = user <- Pleroma.Repo.get(User, user_id) do %User{} = user <- Pleroma.Repo.get(User, user_id) do
{:ok, assign(socket, :user_name, user.nickname)} {:ok, assign(socket, :user_name, user.nickname)}
else else

View file

@ -4,8 +4,8 @@
defmodule Pleroma.Web.ChatChannel do defmodule Pleroma.Web.ChatChannel do
use Phoenix.Channel use Phoenix.Channel
alias Pleroma.Web.ChatChannel.ChatChannelState
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ChatChannel.ChatChannelState
def join("chat:public", _message, socket) do def join("chat:public", _message, socket) do
send(self(), :after_join) send(self(), :after_join)
@ -48,7 +48,7 @@ def add_message(message) do
end) end)
end end
def messages() do def messages do
Agent.get(__MODULE__, fn state -> state[:messages] |> Enum.reverse() end) Agent.get(__MODULE__, fn state -> state[:messages] |> Enum.reverse() end)
end end
end end

View file

@ -3,21 +3,70 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CommonAPI do defmodule Pleroma.Web.CommonAPI do
alias Pleroma.User
alias Pleroma.Repo
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Formatter
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.ThreadMute alias Pleroma.ThreadMute
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Formatter
import Pleroma.Web.CommonAPI.Utils import Pleroma.Web.CommonAPI.Utils
def follow(follower, followed) do
with {:ok, follower} <- User.maybe_direct_follow(follower, followed),
{:ok, activity} <- ActivityPub.follow(follower, followed),
{:ok, follower, followed} <-
User.wait_and_refresh(
Pleroma.Config.get([:activitypub, :follow_handshake_timeout]),
follower,
followed
) do
{:ok, follower, followed, activity}
end
end
def unfollow(follower, unfollowed) do
with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed),
{:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed) do
{:ok, follower}
end
end
def accept_follow_request(follower, followed) do
with {:ok, follower} <- User.maybe_follow(follower, followed),
%Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
{:ok, _activity} <-
ActivityPub.accept(%{
to: [follower.ap_id],
actor: followed,
object: follow_activity.data["id"],
type: "Accept"
}) do
{:ok, follower}
end
end
def reject_follow_request(follower, followed) do
with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
{:ok, _activity} <-
ActivityPub.reject(%{
to: [follower.ap_id],
actor: followed,
object: follow_activity.data["id"],
type: "Reject"
}) do
{:ok, follower}
end
end
def delete(activity_id, user) do def delete(activity_id, user) do
with %Activity{data: %{"object" => %{"id" => object_id}}} <- Repo.get(Activity, activity_id), with %Activity{data: %{"object" => %{"id" => object_id}}} <- Repo.get(Activity, activity_id),
%Object{} = object <- Object.normalize(object_id), %Object{} = object <- Object.normalize(object_id),
true <- user.info.is_moderator || user.ap_id == object.data["actor"], true <- User.superuser?(user) || user.ap_id == object.data["actor"],
{:ok, _} <- unpin(activity_id, user), {:ok, _} <- unpin(activity_id, user),
{:ok, delete} <- ActivityPub.delete(object) do {:ok, delete} <- ActivityPub.delete(object) do
{:ok, delete} {:ok, delete}
@ -75,48 +124,28 @@ def get_visibility(%{"in_reply_to_status_id" => status_id}) when not is_nil(stat
nil -> nil ->
"public" "public"
inReplyTo -> in_reply_to ->
Pleroma.Web.MastodonAPI.StatusView.get_visibility(inReplyTo.data["object"]) Pleroma.Web.MastodonAPI.StatusView.get_visibility(in_reply_to.data["object"])
end end
end end
def get_visibility(_), do: "public" def get_visibility(_), do: "public"
defp get_content_type(content_type) do
if Enum.member?(Pleroma.Config.get([:instance, :allowed_post_formats]), content_type) do
content_type
else
"text/plain"
end
end
def post(user, %{"status" => status} = data) do def post(user, %{"status" => status} = data) do
visibility = get_visibility(data) visibility = get_visibility(data)
limit = Pleroma.Config.get([:instance, :limit]) limit = Pleroma.Config.get([:instance, :limit])
with status <- String.trim(status), with status <- String.trim(status),
attachments <- attachments_from_ids(data), attachments <- attachments_from_ids(data),
mentions <- Formatter.parse_mentions(status), in_reply_to <- get_replied_to_activity(data["in_reply_to_status_id"]),
inReplyTo <- get_replied_to_activity(data["in_reply_to_status_id"]), {content_html, mentions, tags} <-
{to, cc} <- to_for_user_and_mentions(user, mentions, inReplyTo, visibility),
tags <- Formatter.parse_tags(status, data),
content_html <-
make_content_html( make_content_html(
status, status,
mentions,
attachments, attachments,
tags, data
get_content_type(data["content_type"]),
Enum.member?(
[true, "true"],
Map.get(
data,
"no_attachment_links",
Pleroma.Config.get([:instance, :no_attachment_links], false)
)
)
), ),
context <- make_context(inReplyTo), {to, cc} <- to_for_user_and_mentions(user, mentions, in_reply_to, visibility),
context <- make_context(in_reply_to),
cw <- data["spoiler_text"], cw <- data["spoiler_text"],
full_payload <- String.trim(status <> (data["spoiler_text"] || "")), full_payload <- String.trim(status <> (data["spoiler_text"] || "")),
length when length in 1..limit <- String.length(full_payload), length when length in 1..limit <- String.length(full_payload),
@ -127,7 +156,7 @@ def post(user, %{"status" => status} = data) do
context, context,
content_html, content_html,
attachments, attachments,
inReplyTo, in_reply_to,
tags, tags,
cw, cw,
cc cc
@ -247,7 +276,7 @@ def thread_muted?(user, activity) do
def report(user, data) do def report(user, data) do
with {:account_id, %{"account_id" => account_id}} <- {:account_id, data}, with {:account_id, %{"account_id" => account_id}} <- {:account_id, data},
{:account, %User{} = account} <- {:account, User.get_by_id(account_id)}, {:account, %User{} = account} <- {:account, User.get_by_id(account_id)},
{:ok, content_html} <- make_report_content_html(data["comment"]), {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
{:ok, statuses} <- get_report_statuses(account, data), {:ok, statuses} <- get_report_statuses(account, data),
{:ok, activity} <- {:ok, activity} <-
ActivityPub.flag(%{ ActivityPub.flag(%{
@ -255,14 +284,9 @@ def report(user, data) do
actor: user, actor: user,
account: account, account: account,
statuses: statuses, statuses: statuses,
content: content_html content: content_html,
forward: data["forward"] || false
}) do }) do
Enum.each(User.all_superusers(), fn superuser ->
superuser
|> Pleroma.AdminEmail.report(user, account, statuses, content_html)
|> Pleroma.Mailer.deliver_async()
end)
{:ok, activity} {:ok, activity}
else else
{:error, err} -> {:error, err} {:error, err} -> {:error, err}
@ -270,4 +294,24 @@ def report(user, data) do
{:account, nil} -> {:error, "Account not found"} {:account, nil} -> {:error, "Account not found"}
end end
end end
def hide_reblogs(user, muted) do
ap_id = muted.ap_id
if ap_id not in user.info.muted_reblogs do
info_changeset = User.Info.add_reblog_mute(user.info, ap_id)
changeset = Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset)
User.update_and_set_cache(changeset)
end
end
def show_reblogs(user, muted) do
ap_id = muted.ap_id
if ap_id in user.info.muted_reblogs do
info_changeset = User.Info.remove_reblog_mute(user.info, ap_id)
changeset = Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset)
User.update_and_set_cache(changeset)
end
end
end end

View file

@ -6,14 +6,14 @@ defmodule Pleroma.Web.CommonAPI.Utils do
alias Calendar.Strftime alias Calendar.Strftime
alias Comeonin.Pbkdf2 alias Comeonin.Pbkdf2
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Formatter alias Pleroma.Formatter
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Endpoint alias Pleroma.Web.Endpoint
alias Pleroma.Web.MediaProxy alias Pleroma.Web.MediaProxy
alias Pleroma.Web.ActivityPub.Utils
# This is a hack for twidere. # This is a hack for twidere.
def get_by_id_or_ap_id(id) do def get_by_id_or_ap_id(id) do
@ -100,24 +100,45 @@ def to_for_user_and_mentions(_user, mentions, inReplyTo, "direct") do
def make_content_html( def make_content_html(
status, status,
mentions,
attachments, attachments,
tags, data
content_type,
no_attachment_links \\ false
) do ) do
no_attachment_links =
data
|> Map.get("no_attachment_links", Config.get([:instance, :no_attachment_links]))
|> Kernel.in([true, "true"])
content_type = get_content_type(data["content_type"])
status status
|> format_input(mentions, tags, content_type) |> format_input(content_type)
|> maybe_add_attachments(attachments, no_attachment_links) |> maybe_add_attachments(attachments, no_attachment_links)
|> maybe_add_nsfw_tag(data)
end end
defp get_content_type(content_type) do
if Enum.member?(Config.get([:instance, :allowed_post_formats]), content_type) do
content_type
else
"text/plain"
end
end
defp maybe_add_nsfw_tag({text, mentions, tags}, %{"sensitive" => sensitive})
when sensitive in [true, "True", "true", "1"] do
{text, mentions, [{"#nsfw", "nsfw"} | tags]}
end
defp maybe_add_nsfw_tag(data, _), do: data
def make_context(%Activity{data: %{"context" => context}}), do: context def make_context(%Activity{data: %{"context" => context}}), do: context
def make_context(_), do: Utils.generate_context_id() def make_context(_), do: Utils.generate_context_id()
def maybe_add_attachments(text, _attachments, true = _no_links), do: text def maybe_add_attachments(parsed, _attachments, true = _no_links), do: parsed
def maybe_add_attachments(text, attachments, _no_links) do def maybe_add_attachments({text, mentions, tags}, attachments, _no_links) do
add_attachments(text, attachments) text = add_attachments(text, attachments)
{text, mentions, tags}
end end
def add_attachments(text, attachments) do def add_attachments(text, attachments) do
@ -135,56 +156,39 @@ def add_attachments(text, attachments) do
Enum.join([text | attachment_text], "<br>") Enum.join([text | attachment_text], "<br>")
end end
def format_input(text, mentions, tags, format, options \\ []) def format_input(text, format, options \\ [])
@doc """ @doc """
Formatting text to plain text. Formatting text to plain text.
""" """
def format_input(text, mentions, tags, "text/plain", options) do def format_input(text, "text/plain", options) do
text text
|> Formatter.html_escape("text/plain") |> Formatter.html_escape("text/plain")
|> String.replace(~r/\r?\n/, "<br>") |> Formatter.linkify(options)
|> (&{[], &1}).() |> (fn {text, mentions, tags} ->
|> Formatter.add_links() {String.replace(text, ~r/\r?\n/, "<br>"), mentions, tags}
|> Formatter.add_user_links(mentions, options[:user_links] || []) end).()
|> Formatter.add_hashtag_links(tags)
|> Formatter.finalize()
end end
@doc """ @doc """
Formatting text to html. Formatting text to html.
""" """
def format_input(text, mentions, _tags, "text/html", options) do def format_input(text, "text/html", options) do
text text
|> Formatter.html_escape("text/html") |> Formatter.html_escape("text/html")
|> (&{[], &1}).() |> Formatter.linkify(options)
|> Formatter.add_user_links(mentions, options[:user_links] || [])
|> Formatter.finalize()
end end
@doc """ @doc """
Formatting text to markdown. Formatting text to markdown.
""" """
def format_input(text, mentions, tags, "text/markdown", options) do def format_input(text, "text/markdown", options) do
options = Keyword.put(options, :mentions_escape, true)
text text
|> Formatter.mentions_escape(mentions) |> Formatter.linkify(options)
|> Earmark.as_html!() |> (fn {text, mentions, tags} -> {Earmark.as_html!(text), mentions, tags} end).()
|> Formatter.html_escape("text/html") |> Formatter.html_escape("text/html")
|> (&{[], &1}).()
|> Formatter.add_user_links(mentions, options[:user_links] || [])
|> Formatter.add_hashtag_links(tags)
|> Formatter.finalize()
end
def add_tag_links(text, tags) do
tags =
tags
|> Enum.sort_by(fn {tag, _} -> -String.length(tag) end)
Enum.reduce(tags, text, fn {full, tag}, text ->
url = "<a href='#{Web.base_url()}/tag/#{tag}' rel='tag'>##{tag}</a>"
String.replace(text, full, url)
end)
end end
def make_note_data( def make_note_data(
@ -323,13 +327,13 @@ def maybe_extract_mentions(%{"tag" => tag}) do
def maybe_extract_mentions(_), do: [] def maybe_extract_mentions(_), do: []
def make_report_content_html(nil), do: {:ok, nil} def make_report_content_html(nil), do: {:ok, {nil, [], []}}
def make_report_content_html(comment) do def make_report_content_html(comment) do
max_size = Pleroma.Config.get([:instance, :max_report_comment_size], 1000) max_size = Pleroma.Config.get([:instance, :max_report_comment_size], 1000)
if String.length(comment) <= max_size do if String.length(comment) <= max_size do
{:ok, format_input(comment, [], [], "text/plain")} {:ok, format_input(comment, "text/plain")}
else else
{:error, "Comment must be up to #{max_size} characters"} {:error, "Comment must be up to #{max_size} characters"}
end end

View file

@ -6,7 +6,8 @@ defmodule Pleroma.Web.ControllerHelper do
use Pleroma.Web, :controller use Pleroma.Web, :controller
def oauth_scopes(params, default) do def oauth_scopes(params, default) do
# Note: `scopes` is used by Mastodon — supporting it but sticking to OAuth's standard `scope` wherever we control it # Note: `scopes` is used by Mastodon — supporting it but sticking to
# OAuth's standard `scope` wherever we control it
Pleroma.Web.OAuth.parse_scopes(params["scope"] || params["scopes"], default) Pleroma.Web.OAuth.parse_scopes(params["scope"] || params["scopes"], default)
end end

View file

@ -25,7 +25,8 @@ defmodule Pleroma.Web.Endpoint do
at: "/", at: "/",
from: :pleroma, from: :pleroma,
only: only:
~w(index.html static finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc) ~w(index.html robots.txt static finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc)
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
) )
# Code reloading can be explicitly enabled under the # Code reloading can be explicitly enabled under the

View file

@ -4,27 +4,27 @@
defmodule Pleroma.Web.Federator do defmodule Pleroma.Web.Federator do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Jobs
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.WebFinger
alias Pleroma.Web.Websub
alias Pleroma.Web.Salmon
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Federator.RetryQueue alias Pleroma.Web.Federator.RetryQueue
alias Pleroma.Web.OStatus alias Pleroma.Web.OStatus
alias Pleroma.Jobs alias Pleroma.Web.Salmon
alias Pleroma.Web.WebFinger
alias Pleroma.Web.Websub
require Logger require Logger
@websub Application.get_env(:pleroma, :websub) @websub Application.get_env(:pleroma, :websub)
@ostatus Application.get_env(:pleroma, :ostatus) @ostatus Application.get_env(:pleroma, :ostatus)
def init() do def init do
# 1 minute # 1 minute
Process.sleep(1000 * 60 * 1) Process.sleep(1000 * 60)
refresh_subscriptions() refresh_subscriptions()
end end
@ -58,7 +58,7 @@ def request_subscription(sub) do
Jobs.enqueue(:federator_outgoing, __MODULE__, [:request_subscription, sub]) Jobs.enqueue(:federator_outgoing, __MODULE__, [:request_subscription, sub])
end end
def refresh_subscriptions() do def refresh_subscriptions do
Jobs.enqueue(:federator_outgoing, __MODULE__, [:refresh_subscriptions]) Jobs.enqueue(:federator_outgoing, __MODULE__, [:refresh_subscriptions])
end end

View file

@ -13,7 +13,7 @@ def init(args) do
{:ok, %{args | queue_table: queue_table, running_jobs: :sets.new()}} {:ok, %{args | queue_table: queue_table, running_jobs: :sets.new()}}
end end
def start_link() do def start_link do
enabled = enabled =
if Mix.env() == :test, do: true, else: Pleroma.Config.get([__MODULE__, :enabled], false) if Mix.env() == :test, do: true, else: Pleroma.Config.get([__MODULE__, :enabled], false)
@ -39,11 +39,11 @@ def enqueue(data, transport, retries \\ 0) do
GenServer.cast(__MODULE__, {:maybe_enqueue, data, transport, retries + 1}) GenServer.cast(__MODULE__, {:maybe_enqueue, data, transport, retries + 1})
end end
def get_stats() do def get_stats do
GenServer.call(__MODULE__, :get_stats) GenServer.call(__MODULE__, :get_stats)
end end
def reset_stats() do def reset_stats do
GenServer.call(__MODULE__, :reset_stats) GenServer.call(__MODULE__, :reset_stats)
end end
@ -55,7 +55,7 @@ def get_retry_params(retries) do
end end
end end
def get_retry_timer_interval() do def get_retry_timer_interval do
Pleroma.Config.get([:retry_queue, :interval], 1000) Pleroma.Config.get([:retry_queue, :interval], 1000)
end end
@ -231,7 +231,7 @@ defp growth_function(retries) do
end end
end end
defp maybe_kickoff_timer() do defp maybe_kickoff_timer do
GenServer.cast(__MODULE__, :kickoff_timer) GenServer.cast(__MODULE__, :kickoff_timer)
end end
end end

View file

@ -1 +1,63 @@
defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
import Ecto.Query
import Ecto.Changeset
alias Pleroma.Repo
alias Pleroma.User
@default_limit 20
def get_followers(user, params \\ %{}) do
user
|> User.get_followers_query()
|> paginate(params)
|> Repo.all()
end
def get_friends(user, params \\ %{}) do
user
|> User.get_friends_query()
|> paginate(params)
|> Repo.all()
end
def paginate(query, params \\ %{}) do
options = cast_params(params)
query
|> restrict(:max_id, options)
|> restrict(:since_id, options)
|> restrict(:limit, options)
|> order_by([u], fragment("? desc nulls last", u.id))
end
def cast_params(params) do
param_types = %{
max_id: :string,
since_id: :string,
limit: :integer
}
changeset = cast({%{}, param_types}, params, Map.keys(param_types))
changeset.changes
end
defp restrict(query, :max_id, %{max_id: max_id}) do
query
|> where([q], q.id < ^max_id)
end
defp restrict(query, :since_id, %{since_id: since_id}) do
query
|> where([q], q.id > ^since_id)
end
defp restrict(query, :limit, options) do
limit = Map.get(options, :limit, @default_limit)
query
|> limit(^limit)
end
defp restrict(query, _, _), do: query
end

View file

@ -4,6 +4,7 @@
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.Filter alias Pleroma.Filter
@ -13,21 +14,18 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
alias Pleroma.Stats alias Pleroma.Stats
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MediaProxy
alias Pleroma.Web.Push
alias Push.Subscription
alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.FilterView alias Pleroma.Web.MastodonAPI.FilterView
alias Pleroma.Web.MastodonAPI.ListView alias Pleroma.Web.MastodonAPI.ListView
alias Pleroma.Web.MastodonAPI.MastodonAPI
alias Pleroma.Web.MastodonAPI.MastodonView alias Pleroma.Web.MastodonAPI.MastodonView
alias Pleroma.Web.MastodonAPI.PushSubscriptionView alias Pleroma.Web.MastodonAPI.NotificationView
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.MastodonAPI.ReportView alias Pleroma.Web.MastodonAPI.ReportView
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.MediaProxy
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token
@ -134,8 +132,8 @@ def verify_credentials(%{assigns: %{user: user}} = conn, _) do
json(conn, account) json(conn, account)
end end
def user(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
with %User{} = user <- Repo.get(User, id), with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
account = AccountView.render("account.json", %{user: user, for: for_user}) account = AccountView.render("account.json", %{user: user, for: for_user})
json(conn, account) json(conn, account)
@ -193,6 +191,11 @@ def custom_emojis(conn, _params) do
end end
defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
params =
conn.params
|> Map.drop(["since_id", "max_id"])
|> Map.merge(params)
last = List.last(activities) last = List.last(activities)
first = List.first(activities) first = List.first(activities)
@ -292,13 +295,17 @@ def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
end end
def dm_timeline(%{assigns: %{user: user}} = conn, params) do def dm_timeline(%{assigns: %{user: user}} = conn, params) do
query = params =
ActivityPub.fetch_activities_query( params
[user.ap_id], |> Map.put("type", "Create")
Map.merge(params, %{"type" => "Create", visibility: "direct"}) |> Map.put("blocking_user", user)
) |> Map.put("user", user)
|> Map.put(:visibility, "direct")
activities = Repo.all(query) activities =
[user.ap_id]
|> ActivityPub.fetch_activities_query(params)
|> Repo.all()
conn conn
|> add_link_headers(:dm_timeline, activities) |> add_link_headers(:dm_timeline, activities)
@ -497,19 +504,17 @@ def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
def notifications(%{assigns: %{user: user}} = conn, params) do def notifications(%{assigns: %{user: user}} = conn, params) do
notifications = Notification.for_user(user, params) notifications = Notification.for_user(user, params)
result =
notifications
|> Enum.map(fn x -> render_notification(user, x) end)
|> Enum.filter(& &1)
conn conn
|> add_link_headers(:notifications, notifications) |> add_link_headers(:notifications, notifications)
|> json(result) |> put_view(NotificationView)
|> render("index.json", %{notifications: notifications, for: user})
end end
def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
with {:ok, notification} <- Notification.get(user, id) do with {:ok, notification} <- Notification.get(user, id) do
json(conn, render_notification(user, notification)) conn
|> put_view(NotificationView)
|> render("show.json", %{notification: notification, for: user})
else else
{:error, reason} -> {:error, reason} ->
conn conn
@ -646,9 +651,9 @@ def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
|> render("index.json", %{activities: activities, for: user, as: :activity}) |> render("index.json", %{activities: activities, for: user, as: :activity})
end end
def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
with %User{} = user <- Repo.get(User, id), with %User{} = user <- Repo.get(User, id),
{:ok, followers} <- User.get_followers(user) do followers <- MastodonAPI.get_followers(user, params) do
followers = followers =
cond do cond do
for_user && user.id == for_user.id -> followers for_user && user.id == for_user.id -> followers
@ -657,14 +662,15 @@ def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
end end
conn conn
|> add_link_headers(:followers, followers, user)
|> put_view(AccountView) |> put_view(AccountView)
|> render("accounts.json", %{users: followers, as: :user}) |> render("accounts.json", %{users: followers, as: :user})
end end
end end
def following(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
with %User{} = user <- Repo.get(User, id), with %User{} = user <- Repo.get(User, id),
{:ok, followers} <- User.get_friends(user) do followers <- MastodonAPI.get_friends(user, params) do
followers = followers =
cond do cond do
for_user && user.id == for_user.id -> followers for_user && user.id == for_user.id -> followers
@ -673,6 +679,7 @@ def following(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
end end
conn conn
|> add_link_headers(:following, followers, user)
|> put_view(AccountView) |> put_view(AccountView)
|> render("accounts.json", %{users: followers, as: :user}) |> render("accounts.json", %{users: followers, as: :user})
end end
@ -688,16 +695,7 @@ def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
with %User{} = follower <- Repo.get(User, id), with %User{} = follower <- Repo.get(User, id),
{:ok, follower} <- User.maybe_follow(follower, followed), {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
%Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
{:ok, _activity} <-
ActivityPub.accept(%{
to: [follower.ap_id],
actor: followed,
object: follow_activity.data["id"],
type: "Accept"
}) do
conn conn
|> put_view(AccountView) |> put_view(AccountView)
|> render("relationship.json", %{user: followed, target: follower}) |> render("relationship.json", %{user: followed, target: follower})
@ -711,15 +709,7 @@ def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}
def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
with %User{} = follower <- Repo.get(User, id), with %User{} = follower <- Repo.get(User, id),
%Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
{:ok, _activity} <-
ActivityPub.reject(%{
to: [follower.ap_id],
actor: followed,
object: follow_activity.data["id"],
type: "Reject"
}) do
conn conn
|> put_view(AccountView) |> put_view(AccountView)
|> render("relationship.json", %{user: followed, target: follower}) |> render("relationship.json", %{user: followed, target: follower})
@ -733,18 +723,25 @@ def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) d
def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
with %User{} = followed <- Repo.get(User, id), with %User{} = followed <- Repo.get(User, id),
{:ok, follower} <- User.maybe_direct_follow(follower, followed), false <- User.following?(follower, followed),
{:ok, _activity} <- ActivityPub.follow(follower, followed), {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
{:ok, follower, followed} <-
User.wait_and_refresh(
Config.get([:activitypub, :follow_handshake_timeout]),
follower,
followed
) do
conn conn
|> put_view(AccountView) |> put_view(AccountView)
|> render("relationship.json", %{user: follower, target: followed}) |> render("relationship.json", %{user: follower, target: followed})
else else
true ->
followed = User.get_cached_by_id(id)
{:ok, follower} =
case conn.params["reblogs"] do
true -> CommonAPI.show_reblogs(follower, followed)
false -> CommonAPI.hide_reblogs(follower, followed)
end
conn
|> put_view(AccountView)
|> render("relationship.json", %{user: follower, target: followed})
{:error, message} -> {:error, message} ->
conn conn
|> put_resp_content_type("application/json") |> put_resp_content_type("application/json")
@ -754,8 +751,7 @@ def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
with %User{} = followed <- Repo.get_by(User, nickname: uri), with %User{} = followed <- Repo.get_by(User, nickname: uri),
{:ok, follower} <- User.maybe_direct_follow(follower, followed), {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
{:ok, _activity} <- ActivityPub.follow(follower, followed) do
conn conn
|> put_view(AccountView) |> put_view(AccountView)
|> render("account.json", %{user: followed, for: follower}) |> render("account.json", %{user: followed, for: follower})
@ -769,8 +765,7 @@ def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
with %User{} = followed <- Repo.get(User, id), with %User{} = followed <- Repo.get(User, id),
{:ok, _activity} <- ActivityPub.unfollow(follower, followed), {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
{:ok, follower, _} <- User.unfollow(follower, followed) do
conn conn
|> put_view(AccountView) |> put_view(AccountView)
|> render("relationship.json", %{user: follower, target: followed}) |> render("relationship.json", %{user: follower, target: followed})
@ -894,7 +889,7 @@ def status_search(user, query) do
end end
def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
accounts = User.search(query, params["resolve"] == "true", user) accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
statuses = status_search(user, query) statuses = status_search(user, query)
@ -919,7 +914,7 @@ def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
end end
def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
accounts = User.search(query, params["resolve"] == "true", user) accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
statuses = status_search(user, query) statuses = status_search(user, query)
@ -941,7 +936,7 @@ def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
end end
def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
accounts = User.search(query, params["resolve"] == "true", user) accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
res = AccountView.render("accounts.json", users: accounts, for: user, as: :user) res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
@ -1128,7 +1123,8 @@ def index(%{assigns: %{user: user}} = conn, _params) do
compose: %{ compose: %{
me: "#{user.id}", me: "#{user.id}",
default_privacy: user.info.default_scope, default_privacy: user.info.default_scope,
default_sensitive: false default_sensitive: false,
allow_content_types: Config.get([:instance, :allowed_post_formats])
}, },
media_attachments: %{ media_attachments: %{
accept_content_types: [ accept_content_types: [
@ -1273,7 +1269,7 @@ def login(conn, _) do
end end
end end
defp get_or_make_app() do defp get_or_make_app do
find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."} find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
scopes = ["read", "write", "follow", "push"] scopes = ["read", "write", "follow", "push"]
@ -1326,45 +1322,6 @@ def empty_object(conn, _) do
json(conn, %{}) json(conn, %{})
end end
def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
actor = User.get_cached_by_ap_id(activity.data["actor"])
parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
mastodon_type = Activity.mastodon_notification_type(activity)
response = %{
id: to_string(id),
type: mastodon_type,
created_at: CommonAPI.Utils.to_masto_date(created_at),
account: AccountView.render("account.json", %{user: actor, for: user})
}
case mastodon_type do
"mention" ->
response
|> Map.merge(%{
status: StatusView.render("status.json", %{activity: activity, for: user})
})
"favourite" ->
response
|> Map.merge(%{
status: StatusView.render("status.json", %{activity: parent_activity, for: user})
})
"reblog" ->
response
|> Map.merge(%{
status: StatusView.render("status.json", %{activity: parent_activity, for: user})
})
"follow" ->
response
_ ->
nil
end
end
def get_filters(%{assigns: %{user: user}} = conn, _) do def get_filters(%{assigns: %{user: user}} = conn, _) do
filters = Filter.get_filters(user) filters = Filter.get_filters(user)
res = FilterView.render("filters.json", filters: filters) res = FilterView.render("filters.json", filters: filters)
@ -1424,37 +1381,8 @@ def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
json(conn, %{}) json(conn, %{})
end end
def create_push_subscription(%{assigns: %{user: user, token: token}} = conn, params) do # fallback action
true = Push.enabled() #
Subscription.delete_if_exists(user, token)
{:ok, subscription} = Subscription.create(user, token, params)
view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
json(conn, view)
end
def get_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
true = Push.enabled()
subscription = Subscription.get(user, token)
view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
json(conn, view)
end
def update_push_subscription(
%{assigns: %{user: user, token: token}} = conn,
params
) do
true = Push.enabled()
{:ok, subscription} = Subscription.update(user, token, params)
view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
json(conn, view)
end
def delete_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
true = Push.enabled()
{:ok, _response} = Subscription.delete(user, token)
json(conn, %{})
end
def errors(conn, _) do def errors(conn, _) do
conn conn
|> put_status(500) |> put_status(500)
@ -1483,7 +1411,6 @@ def suggestions(%{assigns: %{user: user}} = conn, _) do
url, url,
[], [],
adapter: [ adapter: [
timeout: timeout,
recv_timeout: timeout, recv_timeout: timeout,
pool: :default pool: :default
] ]

View file

@ -0,0 +1,71 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
@moduledoc "The module represents functions to manage user subscriptions."
use Pleroma.Web, :controller
alias Pleroma.Web.Push
alias Pleroma.Web.Push.Subscription
alias Pleroma.Web.MastodonAPI.PushSubscriptionView, as: View
action_fallback(:errors)
# Creates PushSubscription
# POST /api/v1/push/subscription
#
def create(%{assigns: %{user: user, token: token}} = conn, params) do
with true <- Push.enabled(),
{:ok, _} <- Subscription.delete_if_exists(user, token),
{:ok, subscription} <- Subscription.create(user, token, params) do
view = View.render("push_subscription.json", subscription: subscription)
json(conn, view)
end
end
# Gets PushSubscription
# GET /api/v1/push/subscription
#
def get(%{assigns: %{user: user, token: token}} = conn, _params) do
with true <- Push.enabled(),
{:ok, subscription} <- Subscription.get(user, token) do
view = View.render("push_subscription.json", subscription: subscription)
json(conn, view)
end
end
# Updates PushSubscription
# PUT /api/v1/push/subscription
#
def update(%{assigns: %{user: user, token: token}} = conn, params) do
with true <- Push.enabled(),
{:ok, subscription} <- Subscription.update(user, token, params) do
view = View.render("push_subscription.json", subscription: subscription)
json(conn, view)
end
end
# Deletes PushSubscription
# DELETE /api/v1/push/subscription
#
def delete(%{assigns: %{user: user, token: token}} = conn, _params) do
with true <- Push.enabled(),
{:ok, _response} <- Subscription.delete(user, token),
do: json(conn, %{})
end
# fallback action
#
def errors(conn, {:error, :not_found}) do
conn
|> put_status(404)
|> json("Not found")
end
def errors(conn, _) do
conn
|> put_status(500)
|> json("Something went wrong")
end
end

View file

@ -32,7 +32,11 @@ def render("mention.json", %{user: user}) do
} }
end end
def render("relationship.json", %{user: user, target: target}) do def render("relationship.json", %{user: nil, target: _target}) do
%{}
end
def render("relationship.json", %{user: %User{} = user, target: %User{} = target}) do
follow_activity = Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(user, target) follow_activity = Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(user, target)
requested = requested =
@ -51,7 +55,7 @@ def render("relationship.json", %{user: user, target: target}) do
muting_notifications: false, muting_notifications: false,
requested: requested, requested: requested,
domain_blocking: false, domain_blocking: false,
showing_reblogs: false, showing_reblogs: User.showing_reblogs?(user, target),
endorsed: false endorsed: false
} }
end end
@ -85,6 +89,8 @@ defp do_render("account.json", %{user: user} = opts) do
bio = HTML.filter_tags(user.bio, User.html_filter_policy(opts[:for])) bio = HTML.filter_tags(user.bio, User.html_filter_policy(opts[:for]))
relationship = render("relationship.json", %{user: opts[:for], target: user})
%{ %{
id: to_string(user.id), id: to_string(user.id),
username: username_from_nickname(user.nickname), username: username_from_nickname(user.nickname),
@ -115,7 +121,8 @@ defp do_render("account.json", %{user: user} = opts) do
confirmation_pending: user_info.confirmation_pending, confirmation_pending: user_info.confirmation_pending,
tags: user.tags, tags: user.tags,
is_moderator: user.info.is_moderator, is_moderator: user.info.is_moderator,
is_admin: user.info.is_admin is_admin: user.info.is_admin,
relationship: relationship
} }
} }
end end

View file

@ -0,0 +1,64 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.NotificationView do
use Pleroma.Web, :view
alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.NotificationView
alias Pleroma.Web.MastodonAPI.StatusView
def render("index.json", %{notifications: notifications, for: user}) do
render_many(notifications, NotificationView, "show.json", %{for: user})
end
def render("show.json", %{
notification: %Notification{activity: activity} = notification,
for: user
}) do
actor = User.get_cached_by_ap_id(activity.data["actor"])
parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
mastodon_type = Activity.mastodon_notification_type(activity)
response = %{
id: to_string(notification.id),
type: mastodon_type,
created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at),
account: AccountView.render("account.json", %{user: actor, for: user}),
pleroma: %{
is_seen: notification.seen
}
}
case mastodon_type do
"mention" ->
response
|> Map.merge(%{
status: StatusView.render("status.json", %{activity: activity, for: user})
})
"favourite" ->
response
|> Map.merge(%{
status: StatusView.render("status.json", %{activity: parent_activity, for: user})
})
"reblog" ->
response
|> Map.merge(%{
status: StatusView.render("status.json", %{activity: parent_activity, for: user})
})
"follow" ->
response
_ ->
nil
end
end
end

View file

@ -4,6 +4,7 @@
defmodule Pleroma.Web.MastodonAPI.PushSubscriptionView do defmodule Pleroma.Web.MastodonAPI.PushSubscriptionView do
use Pleroma.Web, :view use Pleroma.Web, :view
alias Pleroma.Web.Push
def render("push_subscription.json", %{subscription: subscription}) do def render("push_subscription.json", %{subscription: subscription}) do
%{ %{
@ -14,7 +15,5 @@ def render("push_subscription.json", %{subscription: subscription}) do
} }
end end
defp server_key do defp server_key, do: Keyword.get(Push.vapid_config(), :public_key)
Keyword.get(Application.get_env(:web_push_encryption, :vapid_details), :public_key)
end
end end

View file

@ -102,7 +102,10 @@ def render(
website: nil website: nil
}, },
language: nil, language: nil,
emojis: [] emojis: [],
pleroma: %{
local: activity.local
}
} }
end end
@ -168,7 +171,7 @@ def render("status.json", %{activity: %{data: %{"object" => object}} = activity}
reblogged: present?(repeated), reblogged: present?(repeated),
favourited: present?(favorited), favourited: present?(favorited),
bookmarked: present?(bookmarked), bookmarked: present?(bookmarked),
muted: CommonAPI.thread_muted?(user, activity), muted: CommonAPI.thread_muted?(user, activity) || User.mutes?(opts[:for], user),
pinned: pinned?(activity, user), pinned: pinned?(activity, user),
sensitive: sensitive, sensitive: sensitive,
spoiler_text: object["summary"] || "", spoiler_text: object["summary"] || "",
@ -181,7 +184,10 @@ def render("status.json", %{activity: %{data: %{"object" => object}} = activity}
website: nil website: nil
}, },
language: nil, language: nil,
emojis: build_emojis(activity.data["object"]["emoji"]) emojis: build_emojis(activity.data["object"]["emoji"]),
pleroma: %{
local: activity.local
}
} }
end end
@ -251,7 +257,8 @@ def render("attachment.json", %{attachment: attachment}) do
preview_url: href, preview_url: href,
text_url: href, text_url: href,
type: type, type: type,
description: attachment["name"] description: attachment["name"],
pleroma: %{mime_type: media_type}
} }
end end

View file

@ -5,11 +5,11 @@
defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
require Logger require Logger
alias Pleroma.Web.OAuth.Token
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.OAuth.Token
@behaviour :cowboy_websocket_handler @behaviour :cowboy_websocket
@streams [ @streams [
"public", "public",
@ -26,37 +26,37 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
# Handled by periodic keepalive in Pleroma.Web.Streamer. # Handled by periodic keepalive in Pleroma.Web.Streamer.
@timeout :infinity @timeout :infinity
def init(_type, _req, _opts) do def init(%{qs: qs} = req, state) do
{:upgrade, :protocol, :cowboy_websocket} with params <- :cow_qs.parse_qs(qs),
end
def websocket_init(_type, req, _opts) do
with {qs, req} <- :cowboy_req.qs(req),
params <- :cow_qs.parse_qs(qs),
access_token <- List.keyfind(params, "access_token", 0), access_token <- List.keyfind(params, "access_token", 0),
{_, stream} <- List.keyfind(params, "stream", 0), {_, stream} <- List.keyfind(params, "stream", 0),
{:ok, user} <- allow_request(stream, access_token), {:ok, user} <- allow_request(stream, access_token),
topic when is_binary(topic) <- expand_topic(stream, params) do topic when is_binary(topic) <- expand_topic(stream, params) do
send(self(), :subscribe) {:cowboy_websocket, req, %{user: user, topic: topic}, %{idle_timeout: @timeout}}
{:ok, req, %{user: user, topic: topic}, @timeout}
else else
{:error, code} -> {:error, code} ->
Logger.debug("#{__MODULE__} denied connection: #{inspect(code)} - #{inspect(req)}") Logger.debug("#{__MODULE__} denied connection: #{inspect(code)} - #{inspect(req)}")
{:ok, req} = :cowboy_req.reply(code, req) {:ok, req} = :cowboy_req.reply(code, req)
{:shutdown, req} {:ok, req, state}
error -> error ->
Logger.debug("#{__MODULE__} denied connection: #{inspect(error)} - #{inspect(req)}") Logger.debug("#{__MODULE__} denied connection: #{inspect(error)} - #{inspect(req)}")
{:shutdown, req} {:ok, req} = :cowboy_req.reply(400, req)
{:ok, req, state}
end end
end end
def websocket_init(state) do
send(self(), :subscribe)
{:ok, state}
end
# We never receive messages. # We never receive messages.
def websocket_handle(_frame, req, state) do def websocket_handle(_frame, state) do
{:ok, req, state} {:ok, state}
end end
def websocket_info(:subscribe, req, state) do def websocket_info(:subscribe, state) do
Logger.debug( Logger.debug(
"#{__MODULE__} accepted websocket connection for user #{ "#{__MODULE__} accepted websocket connection for user #{
(state.user || %{id: "anonymous"}).id (state.user || %{id: "anonymous"}).id
@ -64,14 +64,14 @@ def websocket_info(:subscribe, req, state) do
) )
Pleroma.Web.Streamer.add_socket(state.topic, streamer_socket(state)) Pleroma.Web.Streamer.add_socket(state.topic, streamer_socket(state))
{:ok, req, state} {:ok, state}
end end
def websocket_info({:text, message}, req, state) do def websocket_info({:text, message}, state) do
{:reply, {:text, message}, req, state} {:reply, {:text, message}, state}
end end
def websocket_terminate(reason, _req, state) do def terminate(reason, _req, state) do
Logger.debug( Logger.debug(
"#{__MODULE__} terminating websocket connection for user #{ "#{__MODULE__} terminating websocket connection for user #{
(state.user || %{id: "anonymous"}).id (state.user || %{id: "anonymous"}).id

View file

@ -19,7 +19,8 @@ def url(url) do
else else
secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base] secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base]
# Must preserve `%2F` for compatibility with S3 (https://git.pleroma.social/pleroma/pleroma/issues/580) # Must preserve `%2F` for compatibility with S3
# https://git.pleroma.social/pleroma/pleroma/issues/580
replacement = get_replacement(url, ":2F:") replacement = get_replacement(url, ":2F:")
# The URL is url-decoded and encoded again to ensure it is correctly encoded and not twice. # The URL is url-decoded and encoded again to ensure it is correctly encoded and not twice.

View file

@ -88,7 +88,7 @@ defp build_attachments(%{data: %{"attachment" => attachments}}) do
# TODO: Add additional properties to objects when we have the data available. # TODO: Add additional properties to objects when we have the data available.
# Also, Whatsapp only wants JPEG or PNGs. It seems that if we add a second og:image # Also, Whatsapp only wants JPEG or PNGs. It seems that if we add a second og:image
# object when a Video or GIF is attached it will display that in the Whatsapp Rich Preview. # object when a Video or GIF is attached it will display that in Whatsapp Rich Preview.
case media_type do case media_type do
"audio" -> "audio" ->
[ [

View file

@ -66,9 +66,7 @@ def build_tags(%{user: user}) do
end end
end end
defp build_attachments(id, z = %{data: %{"attachment" => attachments}}) do defp build_attachments(id, %{data: %{"attachment" => attachments}}) do
IO.puts(inspect(z))
Enum.reduce(attachments, [], fn attachment, acc -> Enum.reduce(attachments, [], fn attachment, acc ->
rendered_tags = rendered_tags =
Enum.reduce(attachment["url"], [], fn url, acc -> Enum.reduce(attachment["url"], [], fn url, acc ->
@ -99,7 +97,8 @@ defp build_attachments(id, z = %{data: %{"attachment" => attachments}}) do
| acc | acc
] ]
# TODO: Need the true width and height values here or Twitter renders an iFrame with a bad aspect ratio # TODO: Need the true width and height values here or Twitter renders an iFrame with
# a bad aspect ratio
"video" -> "video" ->
[ [
{:meta, [property: "twitter:card", content: "player"], []}, {:meta, [property: "twitter:card", content: "player"], []},

View file

@ -1,10 +1,10 @@
# Pleroma: A lightweight social networking server # Pleroma: A lightweight social networking server
# Copyright \xc2\xa9 2017-2019 Pleroma Authors <https://pleroma.social/> # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Metadata.Utils do defmodule Pleroma.Web.Metadata.Utils do
alias Pleroma.HTML
alias Pleroma.Formatter alias Pleroma.Formatter
alias Pleroma.HTML
alias Pleroma.Web.MediaProxy alias Pleroma.Web.MediaProxy
def scrub_html_and_truncate(%{data: %{"content" => content}} = object) do def scrub_html_and_truncate(%{data: %{"content" => content}} = object) do
@ -17,14 +17,14 @@ def scrub_html_and_truncate(%{data: %{"content" => content}} = object) do
|> Formatter.truncate() |> Formatter.truncate()
end end
def scrub_html_and_truncate(content) when is_binary(content) do def scrub_html_and_truncate(content, max_length \\ 200) when is_binary(content) do
content content
# html content comes from DB already encoded, decode first and scrub after # html content comes from DB already encoded, decode first and scrub after
|> HtmlEntities.decode() |> HtmlEntities.decode()
|> String.replace(~r/<br\s?\/?>/, " ") |> String.replace(~r/<br\s?\/?>/, " ")
|> HTML.strip_tags() |> HTML.strip_tags()
|> Formatter.demojify() |> Formatter.demojify()
|> Formatter.truncate() |> Formatter.truncate(max_length)
end end
def attachment_url(url) do def attachment_url(url) do

View file

@ -1 +0,0 @@

View file

@ -6,7 +6,6 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.Repo
alias Pleroma.Stats alias Pleroma.Stats
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web alias Pleroma.Web
@ -86,8 +85,7 @@ def raw_nodeinfo do
end end
staff_accounts = staff_accounts =
User.moderator_user_query() User.all_superusers()
|> Repo.all()
|> Enum.map(fn u -> u.ap_id end) |> Enum.map(fn u -> u.ap_id end)
mrf_user_allowlist = mrf_user_allowlist =

View file

@ -5,10 +5,10 @@
defmodule Pleroma.Web.OAuth.Authorization do defmodule Pleroma.Web.OAuth.Authorization do
use Ecto.Schema use Ecto.Schema
alias Pleroma.User
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.Web.OAuth.Authorization alias Pleroma.User
alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization
import Ecto.Changeset import Ecto.Changeset
import Ecto.Query import Ecto.Query

View file

@ -5,12 +5,12 @@
defmodule Pleroma.Web.OAuth.OAuthController do defmodule Pleroma.Web.OAuth.OAuthController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.OAuth.App
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Comeonin.Pbkdf2 alias Pleroma.Web.Auth.Authenticator
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Token
import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2] import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
@ -24,27 +24,25 @@ def authorize(conn, params) do
available_scopes = (app && app.scopes) || [] available_scopes = (app && app.scopes) || []
scopes = oauth_scopes(params, nil) || available_scopes scopes = oauth_scopes(params, nil) || available_scopes
render(conn, "show.html", %{ render(conn, Authenticator.auth_template(), %{
response_type: params["response_type"], response_type: params["response_type"],
client_id: params["client_id"], client_id: params["client_id"],
available_scopes: available_scopes, available_scopes: available_scopes,
scopes: scopes, scopes: scopes,
redirect_uri: params["redirect_uri"], redirect_uri: params["redirect_uri"],
state: params["state"] state: params["state"],
params: params
}) })
end end
def create_authorization(conn, %{ def create_authorization(conn, %{
"authorization" => "authorization" =>
%{ %{
"name" => name,
"password" => password,
"client_id" => client_id, "client_id" => client_id,
"redirect_uri" => redirect_uri "redirect_uri" => redirect_uri
} = auth_params } = auth_params
}) do }) do
with %User{} = user <- User.get_by_nickname_or_email(name), with {_, {:ok, %User{} = user}} <- {:get_user, Authenticator.get_user(conn)},
true <- Pbkdf2.checkpw(password, user.password_hash),
%App{} = app <- Repo.get_by(App, client_id: client_id), %App{} = app <- Repo.get_by(App, client_id: client_id),
true <- redirect_uri in String.split(app.redirect_uris), true <- redirect_uri in String.split(app.redirect_uris),
scopes <- oauth_scopes(auth_params, []), scopes <- oauth_scopes(auth_params, []),
@ -53,9 +51,9 @@ def create_authorization(conn, %{
{:missing_scopes, false} <- {:missing_scopes, scopes == []}, {:missing_scopes, false} <- {:missing_scopes, scopes == []},
{:auth_active, true} <- {:auth_active, User.auth_active?(user)}, {:auth_active, true} <- {:auth_active, User.auth_active?(user)},
{:ok, auth} <- Authorization.create_authorization(app, user, scopes) do {:ok, auth} <- Authorization.create_authorization(app, user, scopes) do
# Special case: Local MastodonFE.
redirect_uri = redirect_uri =
if redirect_uri == "." do if redirect_uri == "." do
# Special case: Local MastodonFE
mastodon_api_url(conn, :login) mastodon_api_url(conn, :login)
else else
redirect_uri redirect_uri
@ -97,7 +95,7 @@ def create_authorization(conn, %{
|> authorize(auth_params) |> authorize(auth_params)
error -> error ->
error Authenticator.handle_error(conn, error)
end end
end end
@ -106,6 +104,7 @@ def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do
fixed_token = fix_padding(params["code"]), fixed_token = fix_padding(params["code"]),
%Authorization{} = auth <- %Authorization{} = auth <-
Repo.get_by(Authorization, token: fixed_token, app_id: app.id), Repo.get_by(Authorization, token: fixed_token, app_id: app.id),
%User{} = user <- Repo.get(User, auth.user_id),
{:ok, token} <- Token.exchange_token(app, auth), {:ok, token} <- Token.exchange_token(app, auth),
{:ok, inserted_at} <- DateTime.from_naive(token.inserted_at, "Etc/UTC") do {:ok, inserted_at} <- DateTime.from_naive(token.inserted_at, "Etc/UTC") do
response = %{ response = %{
@ -114,7 +113,8 @@ def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do
refresh_token: token.refresh_token, refresh_token: token.refresh_token,
created_at: DateTime.to_unix(inserted_at), created_at: DateTime.to_unix(inserted_at),
expires_in: 60 * 10, expires_in: 60 * 10,
scope: Enum.join(token.scopes) scope: Enum.join(token.scopes, " "),
me: user.ap_id
} }
json(conn, response) json(conn, response)
@ -127,11 +127,10 @@ def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do
def token_exchange( def token_exchange(
conn, conn,
%{"grant_type" => "password", "username" => name, "password" => password} = params %{"grant_type" => "password"} = params
) do ) do
with %App{} = app <- get_app_from_request(conn, params), with {_, {:ok, %User{} = user}} <- {:get_user, Authenticator.get_user(conn)},
%User{} = user <- User.get_by_nickname_or_email(name), %App{} = app <- get_app_from_request(conn, params),
true <- Pbkdf2.checkpw(password, user.password_hash),
{:auth_active, true} <- {:auth_active, User.auth_active?(user)}, {:auth_active, true} <- {:auth_active, User.auth_active?(user)},
scopes <- oauth_scopes(params, app.scopes), scopes <- oauth_scopes(params, app.scopes),
[] <- scopes -- app.scopes, [] <- scopes -- app.scopes,
@ -143,7 +142,8 @@ def token_exchange(
access_token: token.token, access_token: token.token,
refresh_token: token.refresh_token, refresh_token: token.refresh_token,
expires_in: 60 * 10, expires_in: 60 * 10,
scope: Enum.join(token.scopes, " ") scope: Enum.join(token.scopes, " "),
me: user.ap_id
} }
json(conn, response) json(conn, response)

View file

@ -7,11 +7,11 @@ defmodule Pleroma.Web.OAuth.Token do
import Ecto.Query import Ecto.Query
alias Pleroma.User
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.Web.OAuth.Token alias Pleroma.User
alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Authorization alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Token
schema "oauth_tokens" do schema "oauth_tokens" do
field(:token, :string) field(:token, :string)

View file

@ -4,8 +4,8 @@
defmodule Pleroma.Web.OStatus.ActivityRepresenter do defmodule Pleroma.Web.OStatus.ActivityRepresenter do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.User
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.OStatus.UserRepresenter alias Pleroma.Web.OStatus.UserRepresenter
require Logger require Logger

View file

@ -4,8 +4,8 @@
defmodule Pleroma.Web.OStatus.FeedRepresenter do defmodule Pleroma.Web.OStatus.FeedRepresenter do
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.OStatus
alias Pleroma.Web.MediaProxy alias Pleroma.Web.MediaProxy
alias Pleroma.Web.OStatus
alias Pleroma.Web.OStatus.ActivityRepresenter alias Pleroma.Web.OStatus.ActivityRepresenter
alias Pleroma.Web.OStatus.UserRepresenter alias Pleroma.Web.OStatus.UserRepresenter

View file

@ -4,9 +4,9 @@
defmodule Pleroma.Web.OStatus.DeleteHandler do defmodule Pleroma.Web.OStatus.DeleteHandler do
require Logger require Logger
alias Pleroma.Web.XML
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.XML
def handle_delete(entry, _doc \\ nil) do def handle_delete(entry, _doc \\ nil) do
with id <- XML.string_from_xpath("//id", entry), with id <- XML.string_from_xpath("//id", entry),

View file

@ -3,10 +3,10 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OStatus.FollowHandler do defmodule Pleroma.Web.OStatus.FollowHandler do
alias Pleroma.Web.XML
alias Pleroma.Web.OStatus
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.OStatus
alias Pleroma.Web.XML
def handle(entry, doc) do def handle(entry, doc) do
with {:ok, actor} <- OStatus.find_make_or_update_user(doc), with {:ok, actor} <- OStatus.find_make_or_update_user(doc),

View file

@ -4,13 +4,14 @@
defmodule Pleroma.Web.OStatus.NoteHandler do defmodule Pleroma.Web.OStatus.NoteHandler do
require Logger require Logger
alias Pleroma.Web.OStatus
alias Pleroma.Web.XML
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.OStatus
alias Pleroma.Web.XML
@doc """ @doc """
Get the context for this note. Uses this: Get the context for this note. Uses this:
@ -18,13 +19,13 @@ defmodule Pleroma.Web.OStatus.NoteHandler do
2. The conversation reference in the ostatus xml 2. The conversation reference in the ostatus xml
3. A newly generated context id. 3. A newly generated context id.
""" """
def get_context(entry, inReplyTo) do def get_context(entry, in_reply_to) do
context = context =
(XML.string_from_xpath("//ostatus:conversation[1]", entry) || (XML.string_from_xpath("//ostatus:conversation[1]", entry) ||
XML.string_from_xpath("//ostatus:conversation[1]/@ref", entry) || "") XML.string_from_xpath("//ostatus:conversation[1]/@ref", entry) || "")
|> String.trim() |> String.trim()
with %{data: %{"context" => context}} <- Object.get_cached_by_ap_id(inReplyTo) do with %{data: %{"context" => context}} <- Object.get_cached_by_ap_id(in_reply_to) do
context context
else else
_e -> _e ->
@ -87,14 +88,14 @@ def add_external_url(note, entry) do
Map.put(note, "external_url", url) Map.put(note, "external_url", url)
end end
def fetch_replied_to_activity(entry, inReplyTo) do def fetch_replied_to_activity(entry, in_reply_to) do
with %Activity{} = activity <- Activity.get_create_by_object_ap_id(inReplyTo) do with %Activity{} = activity <- Activity.get_create_by_object_ap_id(in_reply_to) do
activity activity
else else
_e -> _e ->
with inReplyToHref when not is_nil(inReplyToHref) <- with in_reply_to_href when not is_nil(in_reply_to_href) <-
XML.string_from_xpath("//thr:in-reply-to[1]/@href", entry), XML.string_from_xpath("//thr:in-reply-to[1]/@href", entry),
{:ok, [activity | _]} <- OStatus.fetch_activity_from_url(inReplyToHref) do {:ok, [activity | _]} <- OStatus.fetch_activity_from_url(in_reply_to_href) do
activity activity
else else
_e -> nil _e -> nil
@ -110,11 +111,12 @@ def handle_note(entry, doc \\ nil) do
{:ok, actor} <- OStatus.find_make_or_update_user(author), {:ok, actor} <- OStatus.find_make_or_update_user(author),
content_html <- OStatus.get_content(entry), content_html <- OStatus.get_content(entry),
cw <- OStatus.get_cw(entry), cw <- OStatus.get_cw(entry),
inReplyTo <- XML.string_from_xpath("//thr:in-reply-to[1]/@ref", entry), in_reply_to <- XML.string_from_xpath("//thr:in-reply-to[1]/@ref", entry),
inReplyToActivity <- fetch_replied_to_activity(entry, inReplyTo), in_reply_to_activity <- fetch_replied_to_activity(entry, in_reply_to),
inReplyTo <- (inReplyToActivity && inReplyToActivity.data["object"]["id"]) || inReplyTo, in_reply_to <-
(in_reply_to_activity && in_reply_to_activity.data["object"]["id"]) || in_reply_to,
attachments <- OStatus.get_attachments(entry), attachments <- OStatus.get_attachments(entry),
context <- get_context(entry, inReplyTo), context <- get_context(entry, in_reply_to),
tags <- OStatus.get_tags(entry), tags <- OStatus.get_tags(entry),
mentions <- get_mentions(entry), mentions <- get_mentions(entry),
to <- make_to_list(actor, mentions), to <- make_to_list(actor, mentions),
@ -128,7 +130,7 @@ def handle_note(entry, doc \\ nil) do
context, context,
content_html, content_html,
attachments, attachments,
inReplyToActivity, in_reply_to_activity,
[], [],
cw cw
), ),
@ -140,8 +142,8 @@ def handle_note(entry, doc \\ nil) do
# TODO: Handle this case in make_note_data # TODO: Handle this case in make_note_data
note <- note <-
if( if(
inReplyTo && !inReplyToActivity, in_reply_to && !in_reply_to_activity,
do: note |> Map.put("inReplyTo", inReplyTo), do: note |> Map.put("inReplyTo", in_reply_to),
else: note else: note
) do ) do
ActivityPub.create(%{ ActivityPub.create(%{

View file

@ -3,10 +3,10 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OStatus.UnfollowHandler do defmodule Pleroma.Web.OStatus.UnfollowHandler do
alias Pleroma.Web.XML
alias Pleroma.Web.OStatus
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.OStatus
alias Pleroma.Web.XML
def handle(entry, doc) do def handle(entry, doc) do
with {:ok, actor} <- OStatus.find_make_or_update_user(doc), with {:ok, actor} <- OStatus.find_make_or_update_user(doc),

View file

@ -9,19 +9,19 @@ defmodule Pleroma.Web.OStatus do
import Pleroma.Web.XML import Pleroma.Web.XML
require Logger require Logger
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web alias Pleroma.Web
alias Pleroma.Object
alias Pleroma.Activity
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.OStatus.DeleteHandler
alias Pleroma.Web.OStatus.FollowHandler
alias Pleroma.Web.OStatus.NoteHandler
alias Pleroma.Web.OStatus.UnfollowHandler
alias Pleroma.Web.WebFinger alias Pleroma.Web.WebFinger
alias Pleroma.Web.Websub alias Pleroma.Web.Websub
alias Pleroma.Web.OStatus.FollowHandler
alias Pleroma.Web.OStatus.UnfollowHandler
alias Pleroma.Web.OStatus.NoteHandler
alias Pleroma.Web.OStatus.DeleteHandler
def is_representable?(%Activity{data: data}) do def is_representable?(%Activity{data: data}) do
object = Object.normalize(data["object"]) object = Object.normalize(data["object"])

View file

@ -9,13 +9,13 @@ defmodule Pleroma.Web.OStatus.OStatusController do
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.ActivityPub.ActivityPubController alias Pleroma.Web.ActivityPub.ActivityPubController
alias Pleroma.Web.ActivityPub.ObjectView alias Pleroma.Web.ActivityPub.ObjectView
alias Pleroma.Web.OStatus.ActivityRepresenter alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.OStatus.FeedRepresenter
alias Pleroma.Web.Federator alias Pleroma.Web.Federator
alias Pleroma.Web.OStatus alias Pleroma.Web.OStatus
alias Pleroma.Web.OStatus.ActivityRepresenter
alias Pleroma.Web.OStatus.FeedRepresenter
alias Pleroma.Web.XML alias Pleroma.Web.XML
plug(Pleroma.Web.FederatingPlug when action in [:salmon_incoming]) plug(Pleroma.Web.FederatingPlug when action in [:salmon_incoming])

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