Sfoglia il codice sorgente

[#1234] Merge remote-tracking branch 'remotes/upstream/develop' into 1234-mastodon-2-4-3-oauth-scopes

# Conflicts:
#	CHANGELOG.md
#	lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
#	lib/pleroma/web/router.ex
object-id-column
Ivan Tashkinov 4 anni fa
parent
commit
64095961fe
100 ha cambiato i file con 4589 aggiunte e 3246 eliminazioni
  1. +21
    -0
      CHANGELOG.md
  2. +6
    -2
      config/config.exs
  3. +51
    -1
      config/description.exs
  4. +2
    -1
      config/test.exs
  5. +34
    -6
      docs/api/admin_api.md
  6. +2
    -1
      docs/api/differences_in_mastoapi_responses.md
  7. +106
    -0
      docs/api/pleroma_api.md
  8. +2
    -2
      docs/clients.md
  9. +19
    -1
      docs/config.md
  10. +28
    -5
      docs/installation/alpine_linux_en.md
  11. +67
    -68
      docs/installation/debian_based_jp.md
  12. +6
    -2
      installation/pleroma-mongooseim.cfg
  13. +1
    -0
      installation/pleroma.nginx
  14. +1
    -1
      lib/mix/tasks/pleroma/emoji.ex
  15. +7
    -26
      lib/mix/tasks/pleroma/user.ex
  16. +12
    -5
      lib/pleroma/activity.ex
  17. +1
    -2
      lib/pleroma/activity_expiration.ex
  18. +5
    -2
      lib/pleroma/application.ex
  19. +1
    -1
      lib/pleroma/bbs/handler.ex
  20. +7
    -6
      lib/pleroma/bookmark.ex
  21. +2
    -2
      lib/pleroma/conversation/participation.ex
  22. +1
    -1
      lib/pleroma/conversation/participation_recipient_ship.ex
  23. +1
    -2
      lib/pleroma/delivery.ex
  24. +34
    -196
      lib/pleroma/emoji.ex
  25. +59
    -0
      lib/pleroma/emoji/formatter.ex
  26. +224
    -0
      lib/pleroma/emoji/loader.ex
  27. +1
    -1
      lib/pleroma/filter.ex
  28. +0
    -182
      lib/pleroma/flake_id.ex
  29. +3
    -50
      lib/pleroma/formatter.ex
  30. +4
    -2
      lib/pleroma/html.ex
  31. +6
    -17
      lib/pleroma/list.ex
  32. +193
    -72
      lib/pleroma/moderation_log.ex
  33. +2
    -2
      lib/pleroma/notification.ex
  34. +7
    -0
      lib/pleroma/object.ex
  35. +39
    -36
      lib/pleroma/object/fetcher.ex
  36. +1
    -0
      lib/pleroma/pagination.ex
  37. +1
    -1
      lib/pleroma/password_reset_token.ex
  38. +54
    -0
      lib/pleroma/plugs/remote_ip.ex
  39. +2
    -2
      lib/pleroma/registration.ex
  40. +1
    -1
      lib/pleroma/scheduled_activity.ex
  41. +2
    -2
      lib/pleroma/thread_mute.ex
  42. +16
    -6
      lib/pleroma/uploaders/s3.ex
  43. +206
    -299
      lib/pleroma/user.ex
  44. +24
    -21
      lib/pleroma/user/info.ex
  45. +56
    -11
      lib/pleroma/web/activity_pub/activity_pub.ex
  46. +111
    -16
      lib/pleroma/web/activity_pub/activity_pub_controller.ex
  47. +2
    -2
      lib/pleroma/web/activity_pub/publisher.ex
  48. +156
    -170
      lib/pleroma/web/activity_pub/transmogrifier.ex
  49. +16
    -1
      lib/pleroma/web/activity_pub/utils.ex
  50. +4
    -3
      lib/pleroma/web/activity_pub/views/object_view.ex
  51. +19
    -81
      lib/pleroma/web/activity_pub/views/user_view.ex
  52. +72
    -49
      lib/pleroma/web/admin_api/admin_api_controller.ex
  53. +22
    -0
      lib/pleroma/web/admin_api/report.ex
  54. +4
    -1
      lib/pleroma/web/admin_api/views/moderation_log_view.ex
  55. +7
    -13
      lib/pleroma/web/admin_api/views/report_view.ex
  56. +1
    -1
      lib/pleroma/web/chat_channel.ex
  57. +219
    -0
      lib/pleroma/web/common_api/activity_draft.ex
  58. +120
    -206
      lib/pleroma/web/common_api/common_api.ex
  59. +71
    -70
      lib/pleroma/web/common_api/utils.ex
  60. +8
    -1
      lib/pleroma/web/controller_helper.ex
  61. +1
    -4
      lib/pleroma/web/endpoint.ex
  62. +344
    -0
      lib/pleroma/web/mastodon_api/controllers/account_controller.ex
  63. +38
    -0
      lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex
  64. +37
    -0
      lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex
  65. +84
    -0
      lib/pleroma/web/mastodon_api/controllers/filter_controller.ex
  66. +57
    -0
      lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex
  67. +3
    -1
      lib/pleroma/web/mastodon_api/controllers/list_controller.ex
  68. +91
    -1410
      lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
  69. +67
    -0
      lib/pleroma/web/mastodon_api/controllers/notification_controller.ex
  70. +20
    -0
      lib/pleroma/web/mastodon_api/controllers/report_controller.ex
  71. +59
    -0
      lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex
  72. +4
    -3
      lib/pleroma/web/mastodon_api/controllers/search_controller.ex
  73. +325
    -0
      lib/pleroma/web/mastodon_api/controllers/status_controller.ex
  74. +142
    -0
      lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex
  75. +26
    -6
      lib/pleroma/web/mastodon_api/views/account_view.ex
  76. +7
    -14
      lib/pleroma/web/mastodon_api/views/conversation_view.ex
  77. +4
    -4
      lib/pleroma/web/mastodon_api/views/notification_view.ex
  78. +1
    -1
      lib/pleroma/web/mastodon_api/views/report_view.ex
  79. +7
    -16
      lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex
  80. +44
    -15
      lib/pleroma/web/mastodon_api/views/status_view.ex
  81. +3
    -2
      lib/pleroma/web/metadata/utils.ex
  82. +1
    -0
      lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
  83. +26
    -0
      lib/pleroma/web/oauth/app.ex
  84. +1
    -1
      lib/pleroma/web/oauth/authorization.ex
  85. +5
    -0
      lib/pleroma/web/oauth/oauth_controller.ex
  86. +1
    -1
      lib/pleroma/web/oauth/token.ex
  87. +2
    -1
      lib/pleroma/web/ostatus/ostatus_controller.ex
  88. +168
    -0
      lib/pleroma/web/pleroma_api/controllers/account_controller.ex
  89. +617
    -0
      lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex
  90. +41
    -0
      lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex
  91. +3
    -1
      lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
  92. +58
    -0
      lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex
  93. +1
    -1
      lib/pleroma/web/push/subscription.ex
  94. +132
    -95
      lib/pleroma/web/router.ex
  95. +2
    -4
      lib/pleroma/web/twitter_api/controllers/util_controller.ex
  96. +1
    -1
      lib/pleroma/web/twitter_api/twitter_api.ex
  97. +8
    -10
      lib/pleroma/web/twitter_api/twitter_api_controller.ex
  98. +2
    -2
      lib/pleroma/web/views/streamer_view.ex
  99. +1
    -1
      lib/pleroma/web/websub/websub_client_subscription.ex
  100. +5
    -0
      lib/pleroma/workers/background_worker.ex

+ 21
- 0
CHANGELOG.md Vedi File

@@ -6,11 +6,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Added
- Refreshing poll results for remote polls
- Admin API: Add ability to require password reset
- Mastodon API: Account entities now include `follow_requests_count` (planned Mastodon 3.x addition)
- Pleroma API: `GET /api/v1/pleroma/accounts/:id/scrobbles` to get a list of recently scrobbled items
- Pleroma API: `POST /api/v1/pleroma/scrobble` to scrobble a media item

### Changed
- **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
- **Breaking:** Admin API: Return link alongside with token on password reset
- Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings)
- Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler
- Admin API: Return `total` when querying for reports
- Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`)
- Admin API: Return link alongside with token on password reset

### Fixed
- Mastodon API: Fix private and direct statuses not being filtered out from the public timeline for an authenticated user (`GET /api/v1/timelines/public`)

## [1.1.0] - 2019-??-??
### Security
@@ -38,6 +49,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- AdminAPI: Add "godmode" while fetching user statuses (i.e. admin can see private statuses)
- Improve digest email template
– Pagination: (optional) return `total` alongside with `items` when paginating
- Add `rel="ugc"` to all links in statuses, to prevent SEO spam

### Fixed
- Following from Osada
@@ -100,8 +112,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Pleroma API: Add `/api/v1/pleroma/accounts/confirmation_resend?email=<email>` for resending account confirmation.
- Pleroma API: Email change endpoint.
- Admin API: Added moderation log
- Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache).
- Web response cache (currently, enabled for ActivityPub)
- Mastodon API: Added an endpoint to get multiple statuses by IDs (`GET /api/v1/statuses/?ids[]=1&ids[]=2`)
- ActivityPub: Add ActivityPub actor's `discoverable` parameter.
- Admin API: Added moderation log filters (user/start date/end date/search/pagination)
- OAuth: support for hierarchical permissions / [Mastodon 2.4.3 OAuth permissions](https://docs.joinmastodon.org/api/permissions/)

### Changed
@@ -110,6 +125,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- RichMedia: parsers and their order are configured in `rich_media` config.
- RichMedia: add the rich media ttl based on image expiration time.

## [1.0.7] - 2019-09-26
### Fixed
- Broken federation on Erlang 22 (previous versions of hackney http client were using an option that got deprecated)
### Changed
- ActivityPub: The first page in inboxes/outboxes is no longer embedded.

## [1.0.6] - 2019-08-14
### Fixed
- MRF: fix use of unserializable keyword lists in describe() implementations


+ 6
- 2
config/config.exs Vedi File

@@ -109,6 +109,7 @@ config :pleroma, Pleroma.Uploaders.Local, uploads: "uploads"

config :pleroma, Pleroma.Uploaders.S3,
bucket: nil,
streaming_enabled: true,
public_endpoint: "https://s3.amazonaws.com"

config :pleroma, Pleroma.Uploaders.MDII,
@@ -122,7 +123,8 @@ config :pleroma, :emoji,
# Put groups that have higher priority than defaults here. Example in `docs/config/custom_emoji.md`
Custom: ["/emoji/*.png", "/emoji/**/*.png"]
],
default_manifest: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json"
default_manifest: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json",
shared_pack_cache_seconds_per_file: 60

config :pleroma, :uri_schemes,
valid_schemes: [
@@ -507,7 +509,7 @@ config :auto_linker,
class: false,
strip_prefix: false,
new_window: false,
rel: false
rel: "ugc"
]

config :pleroma, :ldap,
@@ -589,6 +591,8 @@ config :pleroma, :rate_limit, nil

config :pleroma, Pleroma.ActivityExpiration, enabled: true

config :pleroma, Pleroma.Plugs.RemoteIp, enabled: false

config :pleroma, :web_cache_ttl,
activity_pub: nil,
activity_pub_question: 30_000


+ 51
- 1
config/description.exs Vedi File

@@ -110,6 +110,12 @@ config :pleroma, :config_description, [
description:
"If you use S3 compatible service such as Digital Ocean Spaces or CDN, set folder name or \"\" etc." <>
" For example, when using CDN to S3 virtual host format, set \"\". At this time, write CNAME to CDN in public_endpoint."
},
%{
key: :streaming_enabled,
type: :boolean,
description:
"Enable streaming uploads, when enabled the file will be sent to the server in chunks as it's being read. This may be unsupported by some providers, try disabling this if you have upload problems."
}
]
},
@@ -1900,7 +1906,7 @@ config :pleroma, :config_description, [
key: :rel,
type: [:string, false],
description: "override the rel attribute. false to clear",
suggestions: ["noopener noreferrer", false]
suggestions: ["ugc", "noopener noreferrer", false]
},
%{
key: :new_window,
@@ -2256,6 +2262,14 @@ config :pleroma, :config_description, [
"Location of the JSON-manifest. This manifest contains information about the emoji-packs you can download." <>
" Currently only one manifest can be added (no arrays)",
suggestions: ["https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json"]
},
%{
key: :shared_pack_cache_seconds_per_file,
type: :integer,
descpiption:
"When an emoji pack is shared, the archive is created and cached in memory" <>
" for this amount of seconds multiplied by the number of files.",
suggestions: [60]
}
]
},
@@ -2675,6 +2689,42 @@ config :pleroma, :config_description, [
},
%{
group: :pleroma,
key: Pleroma.Plugs.RemoteIp,
type: :group,
description: """
**If your instance is not behind at least one reverse proxy, you should not enable this plug.**

`Pleroma.Plugs.RemoteIp` is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.
""",
children: [
%{
key: :enabled,
type: :boolean,
description: "Enable/disable the plug. Defaults to `false`.",
suggestions: [true, false]
},
%{
key: :headers,
type: {:list, :string},
description:
"A list of strings naming the `req_headers` to use when deriving the `remote_ip`. Order does not matter. Defaults to `~w[forwarded x-forwarded-for x-client-ip x-real-ip]`."
},
%{
key: :proxies,
type: {:list, :string},
description:
"A list of strings in [CIDR](https://en.wikipedia.org/wiki/CIDR) notation specifying the IPs of known proxies. Defaults to `[]`."
},
%{
key: :reserved,
type: {:list, :string},
description:
"Defaults to [localhost](https://en.wikipedia.org/wiki/Localhost) and [private network](https://en.wikipedia.org/wiki/Private_network)."
}
]
},
%{
group: :pleroma,
key: :web_cache_ttl,
type: :group,
description:


+ 2
- 1
config/test.exs Vedi File

@@ -30,7 +30,8 @@ config :pleroma, :instance,
notify_email: "noreply@example.com",
skip_thread_containment: false,
federating: false,
external_user_synchronization: false
external_user_synchronization: false,
static_dir: "test/instance_static/"

config :pleroma, :activitypub, sign_object_fetches: false



+ 34
- 6
docs/api/admin_api.md Vedi File

@@ -308,16 +308,32 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret

- Methods: `GET`
- Params: none
- Response: password reset token (base64 string)
- Response:

```json
{
"token": "base64 reset token",
"link": "https://pleroma.social/api/pleroma/password_reset/url-encoded-base64-token"
}
```


## `/api/pleroma/admin/users/:nickname/force_password_reset`

### Force passord reset for a user with a given nickname

- Methods: `PATCH`
- Params: none
- Response: none (code `204`)

## `/api/pleroma/admin/reports`
### Get a list of reports
- Method `GET`
- Params:
- `state`: optional, the state of reports. Valid values are `open`, `closed` and `resolved`
- `limit`: optional, the number of records to retrieve
- `since_id`: optional, returns results that are more recent than the specified id
- `max_id`: optional, returns results that are older than the specified id
- *optional* `state`: **string** the state of reports. Valid values are `open`, `closed` and `resolved`
- *optional* `limit`: **integer** the number of records to retrieve
- *optional* `page`: **integer** page number
- *optional* `page_size`: **integer** number of log entries per page (default is `50`)
- Response:
- On failure: 403 Forbidden error `{"error": "error_msg"}` when requested by anonymous or non-admin
- On success: JSON, returns a list of reports, where:
@@ -695,6 +711,7 @@ Compile time settings (need instance reboot):
}
]
}
```

- Response:

@@ -715,7 +732,11 @@ Compile time settings (need instance reboot):
- Method `GET`
- Params:
- *optional* `page`: **integer** page number
- *optional* `page_size`: **integer** number of users per page (default is `50`)
- *optional* `page_size`: **integer** number of log entries per page (default is `50`)
- *optional* `start_date`: **datetime (ISO 8601)** filter logs by creation date, start from `start_date`. Accepts datetime in ISO 8601 format (YYYY-MM-DDThh:mm:ss), e.g. `2005-08-09T18:31:42`
- *optional* `end_date`: **datetime (ISO 8601)** filter logs by creation date, end by from `end_date`. Accepts datetime in ISO 8601 format (YYYY-MM-DDThh:mm:ss), e.g. 2005-08-09T18:31:42
- *optional* `user_id`: **integer** filter logs by actor's id
- *optional* `search`: **string** search logs by the log message
- Response:

```json
@@ -733,3 +754,10 @@ Compile time settings (need instance reboot):
}
]
```

## `POST /api/pleroma/admin/reload_emoji`
### Reload the instance's custom emoji
* Method `POST`
* Authentication: required
* Params: None
* Response: JSON, "ok" and 200 status

+ 2
- 1
docs/api/differences_in_mastoapi_responses.md Vedi File

@@ -21,7 +21,8 @@ Adding the parameter `with_muted=true` to the timeline queries will also return
Has these additional fields under the `pleroma` object:

- `local`: true if the post was made on the local instance
- `conversation_id`: the ID of the conversation the status is associated with (if any)
- `conversation_id`: the ID of the AP context the status is associated with (if any)
- `direct_conversation_id`: the ID of the Mastodon direct message conversation the status is associated with (if any)
- `in_reply_to_account_acct`: the `acct` property of User entity for replied user (if any)
- `content`: a map consisting of alternate representations of the `content` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`
- `spoiler_text`: a map consisting of alternate representations of the `spoiler_text` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`


+ 106
- 0
docs/api/pleroma_api.md Vedi File

@@ -365,3 +365,109 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa
* Params:
* `recipients`: A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though.
* Response: JSON, statuses (200 - healthy, 503 unhealthy)

## `GET /api/pleroma/emoji/packs`
### Lists the custom emoji packs on the server
* Method `GET`
* Authentication: not required
* Params: None
* Response: JSON, "ok" and 200 status and the JSON hashmap of "pack name" to "pack contents"

## `PUT /api/pleroma/emoji/packs/:name`
### Creates an empty custom emoji pack
* Method `PUT`
* Authentication: required
* Params: None
* Response: JSON, "ok" and 200 status or 409 if the pack with that name already exists

## `DELETE /api/pleroma/emoji/packs/:name`
### Delete a custom emoji pack
* Method `DELETE`
* Authentication: required
* Params: None
* Response: JSON, "ok" and 200 status or 500 if there was an error deleting the pack

## `POST /api/pleroma/emoji/packs/:name/update_file`
### Update a file in a custom emoji pack
* Method `POST`
* Authentication: required
* Params:
* if the `action` is `add`, adds an emoji named `shortcode` to the pack `pack_name`,
that means that the emoji file needs to be uploaded with the request
(thus requiring it to be a multipart request) and be named `file`.
There can also be an optional `filename` that will be the new emoji file name
(if it's not there, the name will be taken from the uploaded file).
* if the `action` is `update`, changes emoji shortcode
(from `shortcode` to `new_shortcode` or moves the file (from the current filename to `new_filename`)
* if the `action` is `remove`, removes the emoji named `shortcode` and it's associated file
* Response: JSON, updated "files" section of the pack and 200 status, 409 if the trying to use a shortcode
that is already taken, 400 if there was an error with the shortcode, filename or file (additional info
in the "error" part of the response JSON)

## `POST /api/pleroma/emoji/packs/:name/update_metadata`
### Updates (replaces) pack metadata
* Method `POST`
* Authentication: required
* Params:
* `new_data`: new metadata to replace the old one
* Response: JSON, updated "metadata" section of the pack and 200 status or 400 if there was a
problem with the new metadata (the error is specified in the "error" part of the response JSON)

## `POST /api/pleroma/emoji/packs/download_from`
### Requests the instance to download the pack from another instance
* Method `POST`
* Authentication: required
* Params:
* `instance_address`: the address of the instance to download from
* `pack_name`: the pack to download from that instance
* Response: JSON, "ok" and 200 status if the pack was downloaded, or 500 if there were
errors downloading the pack

## `POST /api/pleroma/emoji/packs/list_from`
### Requests the instance to list the packs from another instance
* Method `POST`
* Authentication: required
* Params:
* `instance_address`: the address of the instance to download from
* Response: JSON with the pack list, same as if the request was made to that instance's
list endpoint directly + 200 status

## `GET /api/pleroma/emoji/packs/:name/download_shared`
### Requests a local pack from the instance
* Method `GET`
* Authentication: not required
* Params: None
* Response: the archive of the pack with a 200 status code, 403 if the pack is not set as shared,
404 if the pack does not exist

## `GET /api/v1/pleroma/accounts/:id/scrobbles`
### Requests a list of current and recent Listen activities for an account
* Method `GET`
* Authentication: not required
* Params: None
* Response: An array of media metadata entities.
* Example response:
```json
[
{
"account": {...},
"id": "1234",
"title": "Some Title",
"artist": "Some Artist",
"album": "Some Album",
"length": 180000,
"created_at": "2019-09-28T12:40:45.000Z"
}
]
```

## `POST /api/v1/pleroma/scrobble`
### Creates a new Listen activity for an account
* Method `POST`
* Authentication: required
* Params:
* `title`: the title of the media playing
* `album`: the album of the media playing [optional]
* `artist`: the artist of the media playing [optional]
* `length`: the length of the media playing [optional]
* Response: the newly created media metadata entity representing the Listen activity

+ 2
- 2
docs/clients.md Vedi File

@@ -39,7 +39,7 @@ Feel free to contact us to be added to this list!

### 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/)
- Source: <https://git.gdgd.jp.net/lin/nekonium/>
- Source: <https://gogs.gdgd.jp.net/lin/nekonium>
- Contact: [@lin@pleroma.gdgd.jp.net](https://pleroma.gdgd.jp.net/users/lin)
- Platforms: Android
- Features: Streaming Ready
@@ -67,7 +67,7 @@ Feel free to contact us to be added to this list!
## Alternative Web Interfaces
### Brutaldon
- Homepage: <https://jfm.carcosa.net/projects/software/brutaldon/>
- Source Code: <https://github.com/jfmcbrayer/brutaldon>
- Source Code: <https://git.carcosa.net/jmcbray/brutaldon>
- Contact: [@gcupc@glitch.social](https://glitch.social/users/gcupc)
- Features: No Streaming



+ 19
- 1
docs/config.md Vedi File

@@ -23,6 +23,7 @@ Note: `strip_exif` has been replaced by `Pleroma.Upload.Filter.Mogrify`.
* `truncated_namespace`: If you use S3 compatible service such as Digital Ocean Spaces or CDN, set folder name or "" etc.
For example, when using CDN to S3 virtual host format, set "".
At this time, write CNAME to CDN in public_endpoint.
* `streaming_enabled`: Enable streaming uploads, when enabled the file will be sent to the server in chunks as it's being read. This may be unsupported by some providers, try disabling this if you have upload problems.

## Pleroma.Upload.Filter.Mogrify

@@ -521,7 +522,7 @@ config :auto_linker,
class: false,
strip_prefix: false,
new_window: false,
rel: false
rel: "ugc"
]
```

@@ -707,6 +708,8 @@ Configure OAuth 2 provider capabilities:
* `pack_extensions`: A list of file extensions for emojis, when no emoji.txt for a pack is present. Example `[".png", ".gif"]`
* `groups`: Emojis are ordered in groups (tags). This is an array of key-value pairs where the key is the groupname and the value the location or array of locations. `*` can be used as a wildcard. Example `[Custom: ["/emoji/*.png", "/emoji/custom/*.png"]]`
* `default_manifest`: Location of the JSON-manifest. This manifest contains information about the emoji-packs you can download. Currently only one manifest can be added (no arrays).
* `shared_pack_cache_seconds_per_file`: When an emoji pack is shared, the archive is created and cached in
memory for this amount of seconds multiplied by the number of files.

## Database options

@@ -727,6 +730,8 @@ This will probably take a long time.

This is an advanced feature and disabled by default.

If your instance is behind a reverse proxy you must enable and configure [`Pleroma.Plugs.RemoteIp`](#pleroma-plugs-remoteip).

A keyword list of rate limiters where a key is a limiter name and value is the limiter configuration. The basic configuration is a tuple where:

* The first element: `scale` (Integer). The time scale in milliseconds.
@@ -753,3 +758,16 @@ Available caches:

* `:activity_pub` - activity pub routes (except question activities). Defaults to `nil` (no expiration).
* `:activity_pub_question` - activity pub routes (question activities). Defaults to `30_000` (30 seconds).

## Pleroma.Plugs.RemoteIp

**If your instance is not behind at least one reverse proxy, you should not enable this plug.**

`Pleroma.Plugs.RemoteIp` is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.

Available options:

* `enabled` - Enable/disable the plug. Defaults to `false`.
* `headers` - A list of strings naming the `req_headers` to use when deriving the `remote_ip`. Order does not matter. Defaults to `~w[forwarded x-forwarded-for x-client-ip x-real-ip]`.
* `proxies` - A list of strings in [CIDR](https://en.wikipedia.org/wiki/CIDR) notation specifying the IPs of known proxies. Defaults to `[]`.
* `reserved` - Defaults to [localhost](https://en.wikipedia.org/wiki/Localhost) and [private network](https://en.wikipedia.org/wiki/Private_network).

+ 28
- 5
docs/installation/alpine_linux_en.md Vedi File

@@ -1,7 +1,9 @@
# Installing on Alpine Linux
## Installation

This guide is a step-by-step installation guide for Alpine Linux. It also assumes that you have administrative rights, either as root or a user with [sudo permissions](https://www.linode.com/docs/tools-reference/custom-kernels-distros/install-alpine-linux-on-your-linode/#configuration). If you want to run this guide with root, ignore the `sudo` at the beginning of the lines, unless it calls a user like `sudo -Hu pleroma`; in this case, use `su -l <username> -s $SHELL -c 'command'` instead.
This guide is a step-by-step installation guide for Alpine Linux. The instructions were verified against Alpine v3.10 standard image. You might miss additional dependencies if you use `netboot` instead.

It assumes that you have administrative rights, either as root or a user with [sudo permissions](https://www.linode.com/docs/tools-reference/custom-kernels-distros/install-alpine-linux-on-your-linode/#configuration). If you want to run this guide with root, ignore the `sudo` at the beginning of the lines, unless it calls a user like `sudo -Hu pleroma`; in this case, use `su -l <username> -s $SHELL -c 'command'` instead.

### Required packages

@@ -20,12 +22,13 @@ This guide is a step-by-step installation guide for Alpine Linux. It also assume

### Prepare the system

* First make sure to have the community repository enabled:
* The community repository must be enabled in `/etc/apk/repositories`. Depending on which version and mirror you use this looks like `http://alpine.42.fr/v3.10/community`. If you autogenerated the mirror during installation:

```shell
echo "https://nl.alpinelinux.org/alpine/latest-stable/community" | sudo tee -a /etc/apk/repository
awk 'NR==2' /etc/apk/repositories | sed 's/main/community/' | tee -a /etc/apk/repositories
```


* Then update the system, if not already done:

```shell
@@ -77,7 +80,8 @@ sudo rc-update add postgresql
* Add a new system user for the Pleroma service:

```shell
sudo adduser -S -s /bin/false -h /opt/pleroma -H pleroma
sudo addgroup pleroma
sudo adduser -S -s /bin/false -h /opt/pleroma -H -G pleroma pleroma
```

**Note**: To execute a single command as the Pleroma system user, use `sudo -Hu pleroma command`. You can also switch to a shell by using `sudo -Hu pleroma $SHELL`. If you don’t have and want `sudo` on your system, you can use `su` as root user (UID 0) for a single command by using `su -l pleroma -s $SHELL -c 'command'` and `su -l pleroma -s $SHELL` for starting a shell.
@@ -164,7 +168,26 @@ If that doesn’t work, make sure, that nginx is not already running. If it stil
sudo cp /opt/pleroma/installation/pleroma.nginx /etc/nginx/conf.d/pleroma.conf
```

* Before starting nginx edit the configuration and change it to your needs (e.g. change servername, change cert paths)
* Before starting nginx edit the configuration and change it to your needs. You must change change `server_name` and the paths to the certificates. You can use `nano` (install with `apk add nano` if missing).

```
server {
server_name your.domain;
listen 80;
...
}

server {
server_name your.domain;
listen 443 ssl http2;
...
ssl_trusted_certificate /etc/letsencrypt/live/your.domain/chain.pem;
ssl_certificate /etc/letsencrypt/live/your.domain/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your.domain/privkey.pem;
...
}
```

* Enable and start nginx:

```shell


+ 67
- 68
docs/installation/debian_based_jp.md Vedi File

@@ -5,180 +5,179 @@

## インストール

このガイドはDebian Stretchを仮定しています。Ubuntu 16.04でも可能です
このガイドはDebian Stretchを利用することを想定しています。Ubuntu 16.04や18.04でもおそらく動作します。また、ユーザはrootもしくはsudoにより管理者権限を持っていることを前提とします。もし、以下の操作をrootユーザで行う場合は、 `sudo` を無視してください。ただし、`sudo -Hu pleroma` のようにユーザを指定している場合には `su <username> -s $SHELL -c 'command'` を代わりに使ってください

### 必要なソフトウェア

- PostgreSQL 9.6+ (postgresql-contrib-9.6 または他のバージョンの PSQL をインストールしてください)
- Elixir 1.5 以上 ([Debianのリポジトリからインストールしないこと!!! ここからインストールすること!](https://elixir-lang.org/install.html#unix-and-unix-like))。または [asdf](https://github.com/asdf-vm/asdf) を pleroma ユーザーでインストール。
- erlang-dev
- PostgreSQL 9.6以上 (Ubuntu16.04では9.5しか提供されていないので,[](https://www.postgresql.org/download/linux/ubuntu/)こちらから新しいバージョンを入手してください)
- postgresql-contrib 9.6以上 (同上)
- Elixir 1.5 以上 ([Debianのリポジトリからインストールしないこと!!! ここからインストールすること!](https://elixir-lang.org/install.html#unix-and-unix-like)。または [asdf](https://github.com/asdf-vm/asdf) をpleromaユーザーでインストールしてください)
- erlang-dev
- erlang-tools
- erlang-parsetools
- erlang-eldap (LDAP認証を有効化するときのみ必要)
- erlang-ssh
- erlang-xmerl (Jessieではバックポートからインストールすること!)
- erlang-xmerl
- git
- build-essential
- openssh
- openssl
- nginx prefered (Apacheも動くかもしれませんが、誰もテストしていません!)
- certbot (または何らかのACME Let's encryptクライアント)

#### このガイドで利用している追加パッケージ

- nginx (おすすめです。他のリバースプロキシを使う場合は、参考となる設定をこのリポジトリから探してください)
- certbot (または何らかのLet's Encrypt向けACMEクライアント)

### システムを準備する

* まずシステムをアップデートしてください。
```
apt update && apt dist-upgrade
sudo apt update
sudo apt full-upgrade
```

* 複数のツールとpostgresqlをインストールします。あとで必要になるので
* 上記に挙げたパッケージをインストールしておきます
```
apt install git build-essential openssl ssh sudo postgresql-9.6 postgresql-contrib-9.6
sudo apt install git build-essential postgresql postgresql-contrib
```
(postgresqlのバージョンは、あなたのディストロにあわせて変えてください。または、バージョン番号がいらないかもしれません。)

### ElixirとErlangをインストールします

* Erlangのリポジトリをダウンロードおよびインストールします。
```
wget -P /tmp/ https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb && sudo dpkg -i /tmp/erlang-solutions_1.0_all.deb
wget -P /tmp/ https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb
sudo dpkg -i /tmp/erlang-solutions_1.0_all.deb
```

* ElixirとErlangをインストールします、
```
apt update && apt install elixir erlang-dev erlang-parsetools erlang-xmerl erlang-tools erlang-ssh
sudo apt update
sudo apt install elixir erlang-dev erlang-parsetools erlang-xmerl erlang-tools erlang-ssh
```

### Pleroma BE (バックエンド) をインストールします

* 新しいユーザーを作ります。
```
adduser pleroma
```
(Give it any password you want, make it STRONG)
* Pleroma用に新しいユーザーを作ります。

* 新しいユーザーをsudoグループに入れます。
```
usermod -aG sudo pleroma
sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma
```

* 新しいユーザーに変身し、ホームディレクトリに移動します。
```
su pleroma
cd ~
```
**注意**: Pleromaユーザとして単発のコマンドを実行したい場合はは、`sudo -Hu pleroma command` を使ってください。シェルを使いたい場合は `sudo -Hu pleroma $SHELL`です。もし `sudo` を使わない場合は、rootユーザで `su -l pleroma -s $SHELL -c 'command'` とすることでコマンドを、`su -l pleroma -s $SHELL` とすることでシェルを開始できます。

* Gitリポジトリをクローンします。
```
git clone -b master https://git.pleroma.social/pleroma/pleroma
sudo mkdir -p /opt/pleroma
sudo chown -R pleroma:pleroma /opt/pleroma
sudo -Hu pleroma git clone -b master https://git.pleroma.social/pleroma/pleroma /opt/pleroma
```

* 新しいディレクトリに移動します。
```
cd pleroma/
cd /opt/pleroma
```

* Pleromaが依存するパッケージをインストールします。Hexをインストールしてもよいか聞かれたら、yesを入力してください。
```
mix deps.get
sudo -Hu pleroma mix deps.get
```

* コンフィギュレーションを生成します。
```
mix pleroma.instance gen
sudo -Hu pleroma mix pleroma.instance gen
```
* rebar3をインストールしてもよいか聞かれたら、yesを入力してください。
* この処理には時間がかかります。私もよく分かりませんが、何らかのコンパイルが行われているようです。
* あなたのインスタンスについて、いくつかの質問があります。その回答は `config/generated_config.exs` というコンフィギュレーションファイルに保存されます。
* このときにpleromaの一部がコンパイルされるため、この処理には時間がかかります。
* あなたのインスタンスについて、いくつかの質問されます。この質問により `config/generated_config.exs` という設定ファイルが生成されます。

**注意**: メディアプロクシを有効にすると回答して、なおかつ、キャッシュのURLは空欄のままにしている場合は、`generated_config.exs` を編集して、`base_url` で始まる行をコメントアウトまたは削除してください。そして、上にある行の `true` の後にあるコンマを消してください。

* コンフィギュレーションを確認して、もし問題なければ、ファイル名を変更してください。
```
mv config/{generated_config.exs,prod.secret.exs}
```

* これまでのコマンドで、すでに `config/setup_db.psql` というファイルが作られています。このファイルをもとに、データベースを作成します。
* 先程のコマンドで、すでに `config/setup_db.psql` というファイルが作られています。このファイルをもとに、データベースを作成します。
```
sudo su postgres -c 'psql -f config/setup_db.psql'
sudo -Hu pleroma mix pleroma.instance gen
```

* そして、データベースのグレーションを実行します。
* そして、データベースのマイグレーションを実行します。
```
MIX_ENV=prod mix ecto.migrate
sudo -Hu pleroma MIX_ENV=prod mix ecto.migrate
```

* Pleromaを起動できるようになりました。
* これでPleromaを起動できるようになりました。
```
MIX_ENV=prod mix phx.server
sudo -Hu pleroma MIX_ENV=prod mix phx.server
```

### インストールを終わらせる
### インストールの最終段階

あなたの新しいインスタンスを世界に向けて公開するには、nginxまたは何らかのウェブサーバー (プロクシ) を使用する必要があります。また、Pleroma のためにシステムサービスファイルを作成する必要があります。
あなたの新しいインスタンスを世界に向けて公開するには、nginx等のWebサーバやプロキシサーバをPleromaの前段に使用する必要があります。また、Pleroma のためにシステムサービスファイルを作成する必要があります。

#### Nginx

* まだインストールしていないなら、nginxをインストールします。
```
apt install nginx
sudo apt install nginx
```

* SSLをセットアップします。他の方法でもよいですが、ここではcertbotを説明します。
certbotを使うならば、まずそれをインストールします。
```
apt install certbot
sudo apt install certbot
```
そしてセットアップします。
```
mkdir -p /var/lib/letsencrypt/.well-known
% certbot certonly --email your@emailaddress --webroot -w /var/lib/letsencrypt/ -d yourdomain
sudo mkdir -p /var/lib/letsencrypt/
sudo certbot certonly --email <your@emailaddress> -d <yourdomain> --standalone
```
もしうまくいかないときは、先にnginxを設定してください。ssl "on" を "off" に変えてから再試行してください。
もしうまくいかないときは、nginxが正しく動いていない可能性があります。先にnginxを設定してください。ssl "on" を "off" に変えてから再試行してください。

---

* nginxコンフィギュレーションの例をnginxフォルダーにコピーします。
* nginxの設定ファイルサンプルをnginxフォルダーにコピーします。
```
cp /home/pleroma/pleroma/installation/pleroma.nginx /etc/nginx/sites-enabled/pleroma.nginx
sudo cp /opt/pleroma/installation/pleroma.nginx /etc/nginx/sites-available/pleroma.nginx
sudo ln -s /etc/nginx/sites-available/pleroma.nginx /etc/nginx/sites-enabled/pleroma.nginx
```

* nginxを起動する前に、コンフィギュレーションを編集してください。例えば、サーバー名、証明書のパスなどを変更する必要があります。
* nginxを起動する前に、設定ファイルを編集してください。例えば、サーバー名、証明書のパスなどを変更する必要があります。
* nginxを再起動します。
```
systemctl reload nginx.service
sudo systemctl enable --now nginx.service
```

#### Systemd サービス
もし証明書を更新する必要が出てきた場合には、nginxの関連するlocationブロックのコメントアウトを外し、以下のコマンドを動かします。

* サービスファイルの例をコピーします。
```
cp /home/pleroma/pleroma/installation/pleroma.service /usr/lib/systemd/system/pleroma.service
sudo certbot certonly --email <your@emailaddress> -d <yourdomain> --webroot -w /var/lib/letsencrypt/
```

* サービスファイルを変更します。すべてのパスが正しいことを確認してください。また、`[Service]` セクションに以下の行があることを確認してください。
```
Environment="MIX_ENV=prod"
```
#### 他のWebサーバやプロキシ
これに関してはサンプルが `/opt/pleroma/installation/` にあるので、探してみてください。
#### Systemd サービス

* `pleroma.service` を enable および start してください
* サービスファイルのサンプルをコピーします
```
systemctl enable --now pleroma.service
sudo cp /opt/pleroma/installation/pleroma.service /etc/systemd/system/pleroma.service
```

#### モデレーターを作る

新たにユーザーを作ったら、モデレーター権限を与えたいかもしれません。以下のタスクで可能です。
* サービスファイルを変更します。すべてのパスが正しいことを確認してください
* サービスを有効化し `pleroma.service` を開始してください
```
mix set_moderator username [true|false]
sudo systemctl enable --now pleroma.service
```

モデレーターはすべてのポストを消すことができます。将来的には他のことも可能になるかもしれません。
#### 初期ユーザの作成

#### メディアプロクシを有効にする
新たにインスタンスを作成したら、以下のコマンドにより管理者権限を持った初期ユーザを作成できます。

`generate_config` でメディアプロクシを有効にしているなら、すでにメディアプロクシが動作しています。あとから設定を変更したいなら、[How to activate mediaproxy](How-to-activate-mediaproxy) を見てください。
```
sudo -Hu pleroma MIX_ENV=prod mix pleroma.user new <username> <your@emailaddress> --admin
```

#### コンフィギュレーションとカスタマイズ
#### その他の設定とカスタマイズ

* [Backup your instance](backup.html)
* [Configuration tips](general-tips-for-customizing-pleroma-fe.html)


+ 6
- 2
installation/pleroma-mongooseim.cfg Vedi File

@@ -215,7 +215,9 @@
]}
]},

{ 5222, ejabberd_c2s, [
%% If you want dual stack, you have to clone this entire config stanza
%% and change the bind to "::"
{ {5222, "0.0.0.0"}, ejabberd_c2s, [

%%
%% If TLS is compiled in and you installed a SSL
@@ -246,7 +248,9 @@
%% {max_stanza_size, 65536}
%% ]},

{ 5269, ejabberd_s2s_in, [
%% If you want dual stack, you have to clone this entire config stanza
%% and change the bind to "::"
{ {5269, "0.0.0.0"}, ejabberd_s2s_in, [
{shaper, s2s_shaper},
{max_stanza_size, 131072},
{protocol_options, ["no_sslv3"]}


+ 1
- 0
installation/pleroma.nginx Vedi File

@@ -70,6 +70,7 @@ server {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

# this is explicitly IPv4 since Pleroma.Web.Endpoint binds on IPv4 only
# and `localhost.` resolves to [::0] on some systems: see issue #930


+ 1
- 1
lib/mix/tasks/pleroma/emoji.ex Vedi File

@@ -235,7 +235,7 @@ defmodule Mix.Tasks.Pleroma.Emoji do
cwd: tmp_pack_dir
)

emoji_map = Pleroma.Emoji.make_shortcode_to_file_map(tmp_pack_dir, exts)
emoji_map = Pleroma.Emoji.Loader.make_shortcode_to_file_map(tmp_pack_dir, exts)

File.write!(files_name, Jason.encode!(emoji_map, pretty: true))



+ 7
- 26
lib/mix/tasks/pleroma/user.ex Vedi File

@@ -4,7 +4,6 @@

defmodule Mix.Tasks.Pleroma.User do
use Mix.Task
import Ecto.Changeset
import Mix.Pleroma
alias Pleroma.User
alias Pleroma.UserInviteToken
@@ -228,9 +227,9 @@ defmodule Mix.Tasks.Pleroma.User do
shell_info("Deactivating #{user.nickname}")
User.deactivate(user)

{:ok, friends} = User.get_friends(user)
Enum.each(friends, fn friend ->
user
|> User.get_friends()
|> Enum.each(fn friend ->
user = User.get_cached_by_id(user.id)

shell_info("Unsubscribing #{friend.nickname} from #{user.nickname}")
@@ -405,7 +404,7 @@ defmodule Mix.Tasks.Pleroma.User do
start_pleroma()

with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
{:ok, _} = User.delete_user_activities(user)
User.delete_user_activities(user)
shell_info("User #{nickname} statuses deleted.")
else
_ ->
@@ -443,39 +442,21 @@ defmodule Mix.Tasks.Pleroma.User do
end

defp set_moderator(user, value) do
info_cng = User.Info.admin_api_update(user.info, %{is_moderator: value})

user_cng =
Ecto.Changeset.change(user)
|> put_embed(:info, info_cng)

{:ok, user} = User.update_and_set_cache(user_cng)
{:ok, user} = User.update_info(user, &User.Info.admin_api_update(&1, %{is_moderator: value}))

shell_info("Moderator status of #{user.nickname}: #{user.info.is_moderator}")
user
end

defp set_admin(user, value) do
info_cng = User.Info.admin_api_update(user.info, %{is_admin: value})

user_cng =
Ecto.Changeset.change(user)
|> put_embed(:info, info_cng)

{:ok, user} = User.update_and_set_cache(user_cng)
{:ok, user} = User.update_info(user, &User.Info.admin_api_update(&1, %{is_admin: value}))

shell_info("Admin status of #{user.nickname}: #{user.info.is_admin}")
user
end

defp set_locked(user, value) do
info_cng = User.Info.user_upgrade(user.info, %{locked: value})

user_cng =
Ecto.Changeset.change(user)
|> put_embed(:info, info_cng)

{:ok, user} = User.update_and_set_cache(user_cng)
{:ok, user} = User.update_info(user, &User.Info.user_upgrade(&1, %{locked: value}))

shell_info("Locked status of #{user.nickname}: #{user.info.locked}")
user


+ 12
- 5
lib/pleroma/activity.ex Vedi File

@@ -21,7 +21,7 @@ defmodule Pleroma.Activity do
@type t :: %__MODULE__{}
@type actor :: String.t()

@primary_key {:id, Pleroma.FlakeId, autogenerate: true}
@primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}

# https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19
@mastodon_notification_types %{
@@ -137,11 +137,18 @@ defmodule Pleroma.Activity do
|> Repo.one()
end

@spec get_by_id(String.t()) :: Activity.t() | nil
def get_by_id(id) do
Activity
|> where([a], a.id == ^id)
|> restrict_deactivated_users()
|> Repo.one()
case FlakeId.flake_id?(id) do
true ->
Activity
|> where([a], a.id == ^id)
|> restrict_deactivated_users()
|> Repo.one()

_ ->
nil
end
end

def get_by_id_with_object(id) do


+ 1
- 2
lib/pleroma/activity_expiration.ex Vedi File

@@ -7,7 +7,6 @@ defmodule Pleroma.ActivityExpiration do

alias Pleroma.Activity
alias Pleroma.ActivityExpiration
alias Pleroma.FlakeId
alias Pleroma.Repo

import Ecto.Changeset
@@ -17,7 +16,7 @@ defmodule Pleroma.ActivityExpiration do
@min_activity_lifetime :timer.hours(1)

schema "activity_expirations" do
belongs_to(:activity, Activity, type: FlakeId)
belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
field(:scheduled_at, :naive_datetime)
end



+ 5
- 2
lib/pleroma/application.ex Vedi File

@@ -35,7 +35,6 @@ defmodule Pleroma.Application do
Pleroma.Config.TransferTask,
Pleroma.Emoji,
Pleroma.Captcha,
Pleroma.FlakeId,
Pleroma.Daemons.ScheduledActivityDaemon,
Pleroma.Daemons.ActivityExpirationDaemon
] ++
@@ -102,10 +101,14 @@ defmodule Pleroma.Application do
build_cachex("rich_media", default_ttl: :timer.minutes(120), limit: 5000),
build_cachex("scrubber", limit: 2500),
build_cachex("idempotency", expiration: idempotency_expiration(), limit: 2500),
build_cachex("web_resp", limit: 2500)
build_cachex("web_resp", limit: 2500),
build_cachex("emoji_packs", expiration: emoji_packs_expiration(), limit: 10)
]
end

defp emoji_packs_expiration,
do: expiration(default: :timer.seconds(5 * 60), interval: :timer.seconds(60))

defp idempotency_expiration,
do: expiration(default: :timer.seconds(6 * 60 * 60), interval: :timer.seconds(60))



+ 1
- 1
lib/pleroma/bbs/handler.ex Vedi File

@@ -42,7 +42,7 @@ defmodule Pleroma.BBS.Handler do
end

def puts_activity(activity) do
status = Pleroma.Web.MastodonAPI.StatusView.render("status.json", %{activity: activity})
status = Pleroma.Web.MastodonAPI.StatusView.render("show.json", %{activity: activity})
IO.puts("-- #{status.id} by #{status.account.display_name} (#{status.account.acct})")
IO.puts(HtmlSanitizeEx.strip_tags(status.content))
IO.puts("")


+ 7
- 6
lib/pleroma/bookmark.ex Vedi File

@@ -10,20 +10,20 @@ defmodule Pleroma.Bookmark do

alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.FlakeId
alias Pleroma.Repo
alias Pleroma.User

@type t :: %__MODULE__{}

schema "bookmarks" do
belongs_to(:user, User, type: FlakeId)
belongs_to(:activity, Activity, type: FlakeId)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)

timestamps()
end

@spec create(FlakeId.t(), FlakeId.t()) :: {:ok, Bookmark.t()} | {:error, Changeset.t()}
@spec create(FlakeId.Ecto.CompatType.t(), FlakeId.Ecto.CompatType.t()) ::
{:ok, Bookmark.t()} | {:error, Changeset.t()}
def create(user_id, activity_id) do
attrs = %{
user_id: user_id,
@@ -37,7 +37,7 @@ defmodule Pleroma.Bookmark do
|> Repo.insert()
end

@spec for_user_query(FlakeId.t()) :: Ecto.Query.t()
@spec for_user_query(FlakeId.Ecto.CompatType.t()) :: Ecto.Query.t()
def for_user_query(user_id) do
Bookmark
|> where(user_id: ^user_id)
@@ -52,7 +52,8 @@ defmodule Pleroma.Bookmark do
|> Repo.one()
end

@spec destroy(FlakeId.t(), FlakeId.t()) :: {:ok, Bookmark.t()} | {:error, Changeset.t()}
@spec destroy(FlakeId.Ecto.CompatType.t(), FlakeId.Ecto.CompatType.t()) ::
{:ok, Bookmark.t()} | {:error, Changeset.t()}
def destroy(user_id, activity_id) do
from(b in Bookmark,
where: b.user_id == ^user_id,


+ 2
- 2
lib/pleroma/conversation/participation.ex Vedi File

@@ -13,10 +13,10 @@ defmodule Pleroma.Conversation.Participation do
import Ecto.Query

schema "conversation_participations" do
belongs_to(:user, User, type: Pleroma.FlakeId)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:conversation, Conversation)
field(:read, :boolean, default: false)
field(:last_activity_id, Pleroma.FlakeId, virtual: true)
field(:last_activity_id, FlakeId.Ecto.CompatType, virtual: true)

has_many(:recipient_ships, RecipientShip)
has_many(:recipients, through: [:recipient_ships, :user])


+ 1
- 1
lib/pleroma/conversation/participation_recipient_ship.ex Vedi File

@@ -12,7 +12,7 @@ defmodule Pleroma.Conversation.Participation.RecipientShip do
import Ecto.Changeset

schema "conversation_participation_recipient_ships" do
belongs_to(:user, User, type: Pleroma.FlakeId)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:participation, Participation)
end



+ 1
- 2
lib/pleroma/delivery.ex Vedi File

@@ -6,7 +6,6 @@ defmodule Pleroma.Delivery do
use Ecto.Schema

alias Pleroma.Delivery
alias Pleroma.FlakeId
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
@@ -16,7 +15,7 @@ defmodule Pleroma.Delivery do
import Ecto.Query

schema "deliveries" do
belongs_to(:user, User, type: FlakeId)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:object, Object)
end



+ 34
- 196
lib/pleroma/emoji.ex Vedi File

@@ -4,24 +4,37 @@

defmodule Pleroma.Emoji do
@moduledoc """
The emojis are loaded from:

* emoji packs in INSTANCE-DIR/emoji
* the files: `config/emoji.txt` and `config/custom_emoji.txt`
* glob paths, nested folder is used as tag name for grouping e.g. priv/static/emoji/custom/nested_folder

This GenServer stores in an ETS table the list of the loaded emojis, and also allows to reload the list at runtime.
This GenServer stores in an ETS table the list of the loaded emojis,
and also allows to reload the list at runtime.
"""
use GenServer

require Logger
alias Pleroma.Emoji.Loader

@type pattern :: Regex.t() | module() | String.t()
@type patterns :: pattern() | [pattern()]
@type group_patterns :: keyword(patterns())
require Logger

@ets __MODULE__.Ets
@ets_options [:ordered_set, :protected, :named_table, {:read_concurrency, true}]
@ets_options [
:ordered_set,
:protected,
:named_table,
{:read_concurrency, true}
]

defstruct [:code, :file, :tags, :safe_code, :safe_file]

@doc "Build emoji struct"
def build({code, file, tags}) do
%__MODULE__{
code: code,
file: file,
tags: tags,
safe_code: Pleroma.HTML.strip_tags(code),
safe_file: Pleroma.HTML.strip_tags(file)
}
end

def build({code, file}), do: build({code, file, []})

@doc false
def start_link(_) do
@@ -44,11 +57,14 @@ defmodule Pleroma.Emoji do
end

@doc "Returns all the emojos!!"
@spec get_all() :: [{String.t(), String.t()}, ...]
@spec get_all() :: list({String.t(), String.t(), String.t()})
def get_all do
:ets.tab2list(@ets)
end

@doc "Clear out old emojis"
def clear_all, do: :ets.delete_all_objects(@ets)

@doc false
def init(_) do
@ets = :ets.new(@ets, @ets_options)
@@ -58,13 +74,13 @@ defmodule Pleroma.Emoji do

@doc false
def handle_cast(:reload, state) do
load()
update_emojis(Loader.load())
{:noreply, state}
end

@doc false
def handle_call(:reload, _from, state) do
load()
update_emojis(Loader.load())
{:reply, :ok, state}
end

@@ -75,189 +91,11 @@ defmodule Pleroma.Emoji do

@doc false
def code_change(_old_vsn, state, _extra) do
load()
update_emojis(Loader.load())
{:ok, state}
end

defp load do
emoji_dir_path =
Path.join(
Pleroma.Config.get!([:instance, :static_dir]),
"emoji"
)

emoji_groups = Pleroma.Config.get([:emoji, :groups])

case File.ls(emoji_dir_path) do
{:error, :enoent} ->
# The custom emoji directory doesn't exist,
# don't do anything
nil

{:error, e} ->
# There was some other error
Logger.error("Could not access the custom emoji directory #{emoji_dir_path}: #{e}")

{:ok, results} ->
grouped =
Enum.group_by(results, fn file -> File.dir?(Path.join(emoji_dir_path, file)) end)

packs = grouped[true] || []
files = grouped[false] || []

# Print the packs we've found
Logger.info("Found emoji packs: #{Enum.join(packs, ", ")}")

if not Enum.empty?(files) do
Logger.warn(
"Found files in the emoji folder. These will be ignored, please move them to a subdirectory\nFound files: #{
Enum.join(files, ", ")
}"
)
end

emojis =
Enum.flat_map(
packs,
fn pack -> load_pack(Path.join(emoji_dir_path, pack), emoji_groups) end
)

true = :ets.insert(@ets, emojis)
end

# Compat thing for old custom emoji handling & default emoji,
# it should run even if there are no emoji packs
shortcode_globs = Pleroma.Config.get([:emoji, :shortcode_globs], [])

emojis =
(load_from_file("config/emoji.txt", emoji_groups) ++
load_from_file("config/custom_emoji.txt", emoji_groups) ++
load_from_globs(shortcode_globs, emoji_groups))
|> Enum.reject(fn value -> value == nil end)

true = :ets.insert(@ets, emojis)

:ok
end

defp load_pack(pack_dir, emoji_groups) do
pack_name = Path.basename(pack_dir)

emoji_txt = Path.join(pack_dir, "emoji.txt")

if File.exists?(emoji_txt) do
load_from_file(emoji_txt, emoji_groups)
else
extensions = Pleroma.Config.get([:emoji, :pack_extensions])

Logger.info(
"No emoji.txt found for pack \"#{pack_name}\", assuming all #{Enum.join(extensions, ", ")} files are emoji"
)

make_shortcode_to_file_map(pack_dir, extensions)
|> Enum.map(fn {shortcode, rel_file} ->
filename = Path.join("/emoji/#{pack_name}", rel_file)

{shortcode, filename, [to_string(match_extra(emoji_groups, filename))]}
end)
end
end

def make_shortcode_to_file_map(pack_dir, exts) do
find_all_emoji(pack_dir, exts)
|> Enum.map(&Path.relative_to(&1, pack_dir))
|> Enum.map(fn f -> {f |> Path.basename() |> Path.rootname(), f} end)
|> Enum.into(%{})
end

def find_all_emoji(dir, exts) do
Enum.reduce(
File.ls!(dir),
[],
fn f, acc ->
filepath = Path.join(dir, f)

if File.dir?(filepath) do
acc ++ find_all_emoji(filepath, exts)
else
acc ++ [filepath]
end
end
)
|> Enum.filter(fn f -> Path.extname(f) in exts end)
end

defp load_from_file(file, emoji_groups) do
if File.exists?(file) do
load_from_file_stream(File.stream!(file), emoji_groups)
else
[]
end
end

defp load_from_file_stream(stream, emoji_groups) do
stream
|> Stream.map(&String.trim/1)
|> Stream.map(fn line ->
case String.split(line, ~r/,\s*/) do
[name, file] ->
{name, file, [to_string(match_extra(emoji_groups, file))]}

[name, file | tags] ->
{name, file, tags}

_ ->
nil
end
end)
|> Enum.to_list()
end

defp load_from_globs(globs, emoji_groups) do
static_path = Path.join(:code.priv_dir(:pleroma), "static")

paths =
Enum.map(globs, fn glob ->
Path.join(static_path, glob)
|> Path.wildcard()
end)
|> Enum.concat()

Enum.map(paths, fn path ->
tag = match_extra(emoji_groups, Path.join("/", Path.relative_to(path, static_path)))
shortcode = Path.basename(path, Path.extname(path))
external_path = Path.join("/", Path.relative_to(path, static_path))
{shortcode, external_path, [to_string(tag)]}
end)
end

@doc """
Finds a matching group for the given emoji filename
"""
@spec match_extra(group_patterns(), String.t()) :: atom() | nil
def match_extra(group_patterns, filename) do
match_group_patterns(group_patterns, fn pattern ->
case pattern do
%Regex{} = regex -> Regex.match?(regex, filename)
string when is_binary(string) -> filename == string
end
end)
end

defp match_group_patterns(group_patterns, matcher) do
Enum.find_value(group_patterns, fn {group, patterns} ->
patterns =
patterns
|> List.wrap()
|> Enum.map(fn pattern ->
if String.contains?(pattern, "*") do
~r(#{String.replace(pattern, "*", ".*")})
else
pattern
end
end)

Enum.any?(patterns, matcher) && group
end)
defp update_emojis(emojis) do
:ets.insert(@ets, emojis)
end
end

+ 59
- 0
lib/pleroma/emoji/formatter.ex Vedi File

@@ -0,0 +1,59 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Emoji.Formatter do
alias Pleroma.Emoji
alias Pleroma.HTML
alias Pleroma.Web.MediaProxy

def emojify(text) do
emojify(text, Emoji.get_all())
end

def emojify(text, nil), do: text

def emojify(text, emoji, strip \\ false) do
Enum.reduce(emoji, text, fn
{_, %Emoji{safe_code: emoji, safe_file: file}}, text ->
String.replace(text, ":#{emoji}:", prepare_emoji_html(emoji, file, strip))

{unsafe_emoji, unsafe_file}, text ->
emoji = HTML.strip_tags(unsafe_emoji)
file = HTML.strip_tags(unsafe_file)
String.replace(text, ":#{emoji}:", prepare_emoji_html(emoji, file, strip))
end)
|> HTML.filter_tags()
end

defp prepare_emoji_html(_emoji, _file, true), do: ""

defp prepare_emoji_html(emoji, file, _strip) do
"<img class='emoji' alt='#{emoji}' title='#{emoji}' src='#{MediaProxy.url(file)}' />"
end

def demojify(text) do
emojify(text, Emoji.get_all(), true)
end

def demojify(text, nil), do: text

@doc "Outputs a list of the emoji-shortcodes in a text"
def get_emoji(text) when is_binary(text) do
Enum.filter(Emoji.get_all(), fn {emoji, %Emoji{}} ->
String.contains?(text, ":#{emoji}:")
end)
end

def get_emoji(_), do: []

@doc "Outputs a list of the emoji-Maps in a text"
def get_emoji_map(text) when is_binary(text) do
get_emoji(text)
|> Enum.reduce(%{}, fn {name, %Emoji{file: file}}, acc ->
Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url()}#{file}")
end)
end

def get_emoji_map(_), do: []
end

+ 224
- 0
lib/pleroma/emoji/loader.ex Vedi File

@@ -0,0 +1,224 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Emoji.Loader do
@moduledoc """
The Loader emoji from:

* emoji packs in INSTANCE-DIR/emoji
* the files: `config/emoji.txt` and `config/custom_emoji.txt`
* glob paths, nested folder is used as tag name for grouping e.g. priv/static/emoji/custom/nested_folder
"""
alias Pleroma.Config
alias Pleroma.Emoji

require Logger

@type pattern :: Regex.t() | module() | String.t()
@type patterns :: pattern() | [pattern()]
@type group_patterns :: keyword(patterns())
@type emoji :: {String.t(), Emoji.t()}

@doc """
Loads emojis from files/packs.

returns list emojis in format:
`{"000", "/emoji/freespeechextremist.com/000.png", ["Custom"]}`
"""
@spec load() :: list(emoji)
def load do
emoji_dir_path = Path.join(Config.get!([:instance, :static_dir]), "emoji")

emoji_groups = Config.get([:emoji, :groups])

emojis =
case File.ls(emoji_dir_path) do
{:error, :enoent} ->
# The custom emoji directory doesn't exist,
# don't do anything
[]

{:error, e} ->
# There was some other error
Logger.error("Could not access the custom emoji directory #{emoji_dir_path}: #{e}")
[]

{:ok, results} ->
grouped =
Enum.group_by(results, fn file ->
File.dir?(Path.join(emoji_dir_path, file))
end)

packs = grouped[true] || []
files = grouped[false] || []

# Print the packs we've found
Logger.info("Found emoji packs: #{Enum.join(packs, ", ")}")

if not Enum.empty?(files) do
Logger.warn(
"Found files in the emoji folder. These will be ignored, please move them to a subdirectory\nFound files: #{
Enum.join(files, ", ")
}"
)
end

emojis =
Enum.flat_map(packs, fn pack ->
load_pack(Path.join(emoji_dir_path, pack), emoji_groups)
end)

Emoji.clear_all()
emojis
end

# Compat thing for old custom emoji handling & default emoji,
# it should run even if there are no emoji packs
shortcode_globs = Config.get([:emoji, :shortcode_globs], [])

emojis_txt =
(load_from_file("config/emoji.txt", emoji_groups) ++
load_from_file("config/custom_emoji.txt", emoji_groups) ++
load_from_globs(shortcode_globs, emoji_groups))
|> Enum.reject(fn value -> value == nil end)

Enum.map(emojis ++ emojis_txt, &prepare_emoji/1)
end

defp prepare_emoji({code, _, _} = emoji), do: {code, Emoji.build(emoji)}

defp load_pack(pack_dir, emoji_groups) do
pack_name = Path.basename(pack_dir)

pack_file = Path.join(pack_dir, "pack.json")

if File.exists?(pack_file) do
contents = Jason.decode!(File.read!(pack_file))

contents["files"]
|> Enum.map(fn {name, rel_file} ->
filename = Path.join("/emoji/#{pack_name}", rel_file)
{name, filename, ["pack:#{pack_name}"]}
end)
else
# Load from emoji.txt / all files
emoji_txt = Path.join(pack_dir, "emoji.txt")

if File.exists?(emoji_txt) do
load_from_file(emoji_txt, emoji_groups)
else
extensions = Pleroma.Config.get([:emoji, :pack_extensions])

Logger.info(
"No emoji.txt found for pack \"#{pack_name}\", assuming all #{
Enum.join(extensions, ", ")
} files are emoji"
)

make_shortcode_to_file_map(pack_dir, extensions)
|> Enum.map(fn {shortcode, rel_file} ->
filename = Path.join("/emoji/#{pack_name}", rel_file)

{shortcode, filename, [to_string(match_extra(emoji_groups, filename))]}
end)
end
end
end

def make_shortcode_to_file_map(pack_dir, exts) do
find_all_emoji(pack_dir, exts)
|> Enum.map(&Path.relative_to(&1, pack_dir))
|> Enum.map(fn f -> {f |> Path.basename() |> Path.rootname(), f} end)
|> Enum.into(%{})
end

def find_all_emoji(dir, exts) do
dir
|> File.ls!()
|> Enum.flat_map(fn f ->
filepath = Path.join(dir, f)

if File.dir?(filepath) do
find_all_emoji(filepath, exts)
else
[filepath]
end
end)
|> Enum.filter(fn f -> Path.extname(f) in exts end)
end

defp load_from_file(file, emoji_groups) do
if File.exists?(file) do
load_from_file_stream(File.stream!(file), emoji_groups)
else
[]
end
end

defp load_from_file_stream(stream, emoji_groups) do
stream
|> Stream.map(&String.trim/1)
|> Stream.map(fn line ->
case String.split(line, ~r/,\s*/) do
[name, file] ->
{name, file, [to_string(match_extra(emoji_groups, file))]}

[name, file | tags] ->
{name, file, tags}

_ ->
nil
end
end)
|> Enum.to_list()
end

defp load_from_globs(globs, emoji_groups) do
static_path = Path.join(:code.priv_dir(:pleroma), "static")

paths =
Enum.map(globs, fn glob ->
Path.join(static_path, glob)
|> Path.wildcard()
end)
|> Enum.concat()

Enum.map(paths, fn path ->
tag = match_extra(emoji_groups, Path.join("/", Path.relative_to(path, static_path)))
shortcode = Path.basename(path, Path.extname(path))
external_path = Path.join("/", Path.relative_to(path, static_path))
{shortcode, external_path, [to_string(tag)]}
end)
end

@doc """
Finds a matching group for the given emoji filename
"""
@spec match_extra(group_patterns(), String.t()) :: atom() | nil
def match_extra(group_patterns, filename) do
match_group_patterns(group_patterns, fn pattern ->
case pattern do
%Regex{} = regex -> Regex.match?(regex, filename)
string when is_binary(string) -> filename == string
end
end)
end

defp match_group_patterns(group_patterns, matcher) do
Enum.find_value(group_patterns, fn {group, patterns} ->
patterns =
patterns
|> List.wrap()
|> Enum.map(fn pattern ->
if String.contains?(pattern, "*") do
~r(#{String.replace(pattern, "*", ".*")})
else
pattern
end
end)

Enum.any?(patterns, matcher) && group
end)
end
end

+ 1
- 1
lib/pleroma/filter.ex Vedi File

@@ -12,7 +12,7 @@ defmodule Pleroma.Filter do
alias Pleroma.User

schema "filters" do
belongs_to(:user, User, type: Pleroma.FlakeId)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
field(:filter_id, :integer)
field(:hide, :boolean, default: false)
field(:whole_word, :boolean, default: true)


+ 0
- 182
lib/pleroma/flake_id.ex Vedi File

@@ -1,182 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.FlakeId do
@moduledoc """
Flake is a decentralized, k-ordered id generation service.

Adapted from:

* [flaky](https://github.com/nirvana/flaky), released under the terms of the Truly Free License,
* [Flake](https://github.com/boundary/flake), Copyright 2012, Boundary, Apache License, Version 2.0
"""

@type t :: binary

use Ecto.Type
use GenServer
require Logger
alias __MODULE__
import Kernel, except: [to_string: 1]

defstruct node: nil, time: 0, sq: 0

@doc "Converts a binary Flake to a String"
def to_string(<<0::integer-size(64), id::integer-size(64)>>) do
Kernel.to_string(id)
end

def to_string(<<_::integer-size(64), _::integer-size(48), _::integer-size(16)>> = flake) do
encode_base62(flake)
end

def to_string(s), do: s

def from_string(int) when is_integer(int) do
from_string(Kernel.to_string(int))
end

for i <- [-1, 0] do
def from_string(unquote(i)), do: <<0::integer-size(128)>>
def from_string(unquote(Kernel.to_string(i))), do: <<0::integer-size(128)>>
end

def from_string(<<_::integer-size(128)>> = flake), do: flake

def from_string(string) when is_binary(string) and byte_size(string) < 18 do
case Integer.parse(string) do
{id, ""} -> <<0::integer-size(64), id::integer-size(64)>>
_ -> nil
end
end

def from_string(string) do
string |> decode_base62 |> from_integer
end

def to_integer(<<integer::integer-size(128)>>), do: integer

def from_integer(integer) do
<<_time::integer-size(64), _node::integer-size(48), _seq::integer-size(16)>> =
<<integer::integer-size(128)>>
end

@doc "Generates a Flake"
@spec get :: binary
def get, do: to_string(:gen_server.call(:flake, :get))

# checks that ID is is valid FlakeID
#
@spec is_flake_id?(String.t()) :: boolean
def is_flake_id?(id), do: is_flake_id?(String.to_charlist(id), true)
defp is_flake_id?([c | cs], true) when c >= ?0 and c <= ?9, do: is_flake_id?(cs, true)
defp is_flake_id?([c | cs], true) when c >= ?A and c <= ?Z, do: is_flake_id?(cs, true)
defp is_flake_id?([c | cs], true) when c >= ?a and c <= ?z, do: is_flake_id?(cs, true)
defp is_flake_id?([], true), do: true
defp is_flake_id?(_, _), do: false

# -- Ecto.Type API
@impl Ecto.Type
def type, do: :uuid

@impl Ecto.Type
def cast(value) do
{:ok, FlakeId.to_string(value)}
end

@impl Ecto.Type
def load(value) do
{:ok, FlakeId.to_string(value)}
end

@impl Ecto.Type
def dump(value) do
{:ok, FlakeId.from_string(value)}
end

def autogenerate, do: get()

# -- GenServer API
def start_link(_) do
:gen_server.start_link({:local, :flake}, __MODULE__, [], [])
end

@impl GenServer
def init([]) do
{:ok, %FlakeId{node: worker_id(), time: time()}}
end

@impl GenServer
def handle_call(:get, _from, state) do
{flake, new_state} = get(time(), state)
{:reply, flake, new_state}
end

# Matches when the calling time is the same as the state time. Incr. sq
defp get(time, %FlakeId{time: time, node: node, sq: seq}) do
new_state = %FlakeId{time: time, node: node, sq: seq + 1}
{gen_flake(new_state), new_state}
end

# Matches when the times are different, reset sq
defp get(newtime, %FlakeId{time: time, node: node}) when newtime > time do
new_state = %FlakeId{time: newtime, node: node, sq: 0}
{gen_flake(new_state), new_state}
end

# Error when clock is running backwards
defp get(newtime, %FlakeId{time: time}) when newtime < time do
{:error, :clock_running_backwards}
end

defp gen_flake(%FlakeId{time: time, node: node, sq: seq}) do
<<time::integer-size(64), node::integer-size(48), seq::integer-size(16)>>
end

defp nthchar_base62(n) when n <= 9, do: ?0 + n
defp nthchar_base62(n) when n <= 35, do: ?A + n - 10
defp nthchar_base62(n), do: ?a + n - 36

defp encode_base62(<<integer::integer-size(128)>>) do
integer
|> encode_base62([])
|> List.to_string()
end

defp encode_base62(int, acc) when int < 0, do: encode_base62(-int, acc)
defp encode_base62(int, []) when int == 0, do: '0'
defp encode_base62(int, acc) when int == 0, do: acc

defp encode_base62(int, acc) do
r = rem(int, 62)
id = div(int, 62)
acc = [nthchar_base62(r) | acc]
encode_base62(id, acc)
end

defp decode_base62(s) do
decode_base62(String.to_charlist(s), 0)
end

defp decode_base62([c | cs], acc) when c >= ?0 and c <= ?9,
do: decode_base62(cs, 62 * acc + (c - ?0))

defp decode_base62([c | cs], acc) when c >= ?A and c <= ?Z,
do: decode_base62(cs, 62 * acc + (c - ?A + 10))

defp decode_base62([c | cs], acc) when c >= ?a and c <= ?z,
do: decode_base62(cs, 62 * acc + (c - ?a + 36))

defp decode_base62([], acc), do: acc

defp time do
{mega_seconds, seconds, micro_seconds} = :erlang.timestamp()
1_000_000_000 * mega_seconds + seconds * 1000 + :erlang.trunc(micro_seconds / 1000)
end

defp worker_id do
<<worker::integer-size(48)>> = :crypto.strong_rand_bytes(6)
worker
end
end

+ 3
- 50
lib/pleroma/formatter.ex Vedi File

@@ -3,10 +3,8 @@
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Formatter do
alias Pleroma.Emoji
alias Pleroma.HTML
alias Pleroma.User
alias Pleroma.Web.MediaProxy

@safe_mention_regex ~r/^(\s*(?<mentions>(@.+?\s+){1,})+)(?<rest>.*)/s
@link_regex ~r"((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+"ui
@@ -36,9 +34,9 @@ defmodule Pleroma.Formatter do
nickname_text = get_nickname_text(nickname, opts)

link =
"<span class='h-card'><a data-user='#{id}' class='u-url mention' href='#{ap_id}'>@<span>#{
~s(<span class="h-card"><a data-user="#{id}" class="u-url mention" href="#{ap_id}" rel="ugc">@<span>#{
nickname_text
}</span></a></span>"
}</span></a></span>)

{link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, user})}}

@@ -50,7 +48,7 @@ defmodule Pleroma.Formatter do
def hashtag_handler("#" <> tag = tag_text, _buffer, _opts, acc) do
tag = String.downcase(tag)
url = "#{Pleroma.Web.base_url()}/tag/#{tag}"
link = "<a class='hashtag' data-tag='#{tag}' href='#{url}' rel='tag'>#{tag_text}</a>"
link = ~s(<a class="hashtag" data-tag="#{tag}" href="#{url}" rel="tag ugc">#{tag_text}</a>)

{link, %{acc | tags: MapSet.put(acc.tags, {tag_text, tag})}}
end
@@ -100,51 +98,6 @@ defmodule Pleroma.Formatter do
end
end

def emojify(text) do
emojify(text, Emoji.get_all())
end

def emojify(text, nil), do: text

def emojify(text, emoji, strip \\ false) do
Enum.reduce(emoji, text, fn emoji_data, text ->
emoji = HTML.strip_tags(elem(emoji_data, 0))
file = HTML.strip_tags(elem(emoji_data, 1))

html =
if not strip do
"<img class='emoji' alt='#{emoji}' title='#{emoji}' src='#{MediaProxy.url(file)}' />"
else
""
end

String.replace(text, ":#{emoji}:", html) |> HTML.filter_tags()
end)
end

def demojify(text) do
emojify(text, Emoji.get_all(), true)
end

def demojify(text, nil), do: text

@doc "Outputs a list of the emoji-shortcodes in a text"
def get_emoji(text) when is_binary(text) do
Enum.filter(Emoji.get_all(), fn {emoji, _, _} -> String.contains?(text, ":#{emoji}:") end)
end

def get_emoji(_), do: []

@doc "Outputs a list of the emoji-Maps in a text"
def get_emoji_map(text) when is_binary(text) do
get_emoji(text)
|> Enum.reduce(%{}, fn {name, file, _group}, acc ->
Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url()}#{file}")
end)
end

def get_emoji_map(_), do: []

def html_escape({text, mentions, hashtags}, type) do
{html_escape(text, type), mentions, hashtags}
end


+ 4
- 2
lib/pleroma/html.ex Vedi File

@@ -184,7 +184,8 @@ defmodule Pleroma.HTML.Scrubber.Default do
"tag",
"nofollow",
"noopener",
"noreferrer"
"noreferrer",
"ugc"
])

Meta.allow_tag_with_these_attributes("a", ["name", "title"])
@@ -304,7 +305,8 @@ defmodule Pleroma.HTML.Scrubber.LinksOnly do
"nofollow",
"noopener",
"noreferrer",
"me"
"me",
"ugc"
])

Meta.allow_tag_with_these_attributes("a", ["name", "title"])


+ 6
- 17
lib/pleroma/list.ex Vedi File

@@ -13,7 +13,7 @@ defmodule Pleroma.List do
alias Pleroma.User

schema "lists" do
belongs_to(:user, User, type: Pleroma.FlakeId)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
field(:title, :string)
field(:following, {:array, :string}, default: [])
field(:ap_id, :string)
@@ -84,22 +84,11 @@ defmodule Pleroma.List do
end

# Get lists to which the account belongs.
def get_lists_account_belongs(%User{} = owner, account_id) do
user = User.get_cached_by_id(account_id)

query =
from(
l in Pleroma.List,
where:
l.user_id == ^owner.id and
fragment(
"? = ANY(?)",
^user.follower_address,
l.following
)
)

Repo.all(query)
def get_lists_account_belongs(%User{} = owner, user) do
Pleroma.List
|> where([l], l.user_id == ^owner.id)
|> where([l], fragment("? = ANY(?)", ^user.follower_address, l.following))
|> Repo.all()
end

def rename(%Pleroma.List{} = list, title) do


+ 193
- 72
lib/pleroma/moderation_log.ex Vedi File

@@ -14,61 +14,143 @@ defmodule Pleroma.ModerationLog do
timestamps()
end

def get_all(page, page_size) do
from(q in __MODULE__,
order_by: [desc: q.inserted_at],
def get_all(params) do
base_query =
get_all_query()
|> maybe_filter_by_date(params)
|> maybe_filter_by_user(params)
|> maybe_filter_by_search(params)

query_with_pagination = base_query |> paginate_query(params)

%{
items: Repo.all(query_with_pagination),
count: Repo.aggregate(base_query, :count, :id)
}
end

defp maybe_filter_by_date(query, %{start_date: nil, end_date: nil}), do: query

defp maybe_filter_by_date(query, %{start_date: start_date, end_date: nil}) do
from(q in query,
where: q.inserted_at >= ^parse_datetime(start_date)
)
end

defp maybe_filter_by_date(query, %{start_date: nil, end_date: end_date}) do
from(q in query,
where: q.inserted_at <= ^parse_datetime(end_date)
)
end

defp maybe_filter_by_date(query, %{start_date: start_date, end_date: end_date}) do
from(q in query,
where: q.inserted_at >= ^parse_datetime(start_date),
where: q.inserted_at <= ^parse_datetime(end_date)
)
end

defp maybe_filter_by_user(query, %{user_id: nil}), do: query

defp maybe_filter_by_user(query, %{user_id: user_id}) do
from(q in query,
where: fragment("(?)->'actor'->>'id' = ?", q.data, ^user_id)
)
end

defp maybe_filter_by_search(query, %{search: search}) when is_nil(search) or search == "",
do: query

defp maybe_filter_by_search(query, %{search: search}) do
from(q in query,
where: fragment("(?)->>'message' ILIKE ?", q.data, ^"%#{search}%")
)
end

defp paginate_query(query, %{page: page, page_size: page_size}) do
from(q in query,
limit: ^page_size,
offset: ^((page - 1) * page_size)
)
|> Repo.all()
end

defp get_all_query do
from(q in __MODULE__,
order_by: [desc: q.inserted_at]
)
end

defp parse_datetime(datetime) do
{:ok, parsed_datetime, _} = DateTime.from_iso8601(datetime)

parsed_datetime
end

@spec insert_log(%{actor: User, subject: User, action: String.t(), permission: String.t()}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
subject: %User{} = subject,
action: action,
permission: permission
}) do
Repo.insert(%ModerationLog{
%ModerationLog{
data: %{
actor: user_to_map(actor),
subject: user_to_map(subject),
action: action,
permission: permission
"actor" => user_to_map(actor),
"subject" => user_to_map(subject),
"action" => action,
"permission" => permission,
"message" => ""
}
})
}
|> insert_log_entry_with_message()
end

@spec insert_log(%{actor: User, subject: User, action: String.t()}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
action: "report_update",
subject: %Activity{data: %{"type" => "Flag"}} = subject
}) do
Repo.insert(%ModerationLog{
%ModerationLog{
data: %{
actor: user_to_map(actor),
action: "report_update",
subject: report_to_map(subject)
"actor" => user_to_map(actor),
"action" => "report_update",
"subject" => report_to_map(subject),
"message" => ""
}
})
}
|> insert_log_entry_with_message()
end

@spec insert_log(%{actor: User, subject: Activity, action: String.t(), text: String.t()}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
action: "report_response",
subject: %Activity{} = subject,
text: text
}) do
Repo.insert(%ModerationLog{
%ModerationLog{
data: %{
actor: user_to_map(actor),
action: "report_response",
subject: report_to_map(subject),
text: text
"actor" => user_to_map(actor),
"action" => "report_response",
"subject" => report_to_map(subject),
"text" => text,
"message" => ""
}
})
}
|> insert_log_entry_with_message()
end

@spec insert_log(%{
actor: User,
subject: Activity,
action: String.t(),
sensitive: String.t(),
visibility: String.t()
}) :: {:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
action: "status_update",
@@ -76,41 +158,49 @@ defmodule Pleroma.ModerationLog do
sensitive: sensitive,
visibility: visibility
}) do
Repo.insert(%ModerationLog{
%ModerationLog{
data: %{
actor: user_to_map(actor),
action: "status_update",
subject: status_to_map(subject),
sensitive: sensitive,
visibility: visibility
"actor" => user_to_map(actor),
"action" => "status_update",
"subject" => status_to_map(subject),
"sensitive" => sensitive,
"visibility" => visibility,
"message" => ""
}
})
}
|> insert_log_entry_with_message()
end

@spec insert_log(%{actor: User, action: String.t(), subject_id: String.t()}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
action: "status_delete",
subject_id: subject_id
}) do
Repo.insert(%ModerationLog{
%ModerationLog{
data: %{
actor: user_to_map(actor),
action: "status_delete",
subject_id: subject_id
"actor" => user_to_map(actor),
"action" => "status_delete",
"subject_id" => subject_id,
"message" => ""
}
})
}
|> insert_log_entry_with_message()
end

@spec insert_log(%{actor: User, subject: User, action: String.t()}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{actor: %User{} = actor, subject: subject, action: action}) do
Repo.insert(%ModerationLog{
%ModerationLog{
data: %{
actor: user_to_map(actor),
action: action,
subject: user_to_map(subject)
"actor" => user_to_map(actor),
"action" => action,
"subject" => user_to_map(subject),
"message" => ""
}
})
}
|> insert_log_entry_with_message()
end

@spec insert_log(%{actor: User, subjects: [User], action: String.t()}) ::
@@ -118,97 +208,128 @@ defmodule Pleroma.ModerationLog do
def insert_log(%{actor: %User{} = actor, subjects: subjects, action: action}) do
subjects = Enum.map(subjects, &user_to_map/1)

Repo.insert(%ModerationLog{
%ModerationLog{
data: %{
actor: user_to_map(actor),
action: action,
subjects: subjects
"actor" => user_to_map(actor),
"action" => action,
"subjects" => subjects,
"message" => ""
}
})
}
|> insert_log_entry_with_message()
end

@spec insert_log(%{actor: User, action: String.t(), followed: User, follower: User}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
followed: %User{} = followed,
follower: %User{} = follower,
action: "follow"
}) do
Repo.insert(%ModerationLog{
%ModerationLog{
data: %{
actor: user_to_map(actor),
action: "follow",
followed: user_to_map(followed),
follower: user_to_map(follower)
"actor" => user_to_map(actor),
"action" => "follow",
"followed" => user_to_map(followed),
"follower" => user_to_map(follower),
"message" => ""
}
})
}
|> insert_log_entry_with_message()
end

@spec insert_log(%{actor: User, action: String.t(), followed: User, follower: User}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
followed: %User{} = followed,
follower: %User{} = follower,
action: "unfollow"
}) do
Repo.insert(%ModerationLog{
%ModerationLog{
data: %{
actor: user_to_map(actor),
action: "unfollow",
followed: user_to_map(followed),
follower: user_to_map(follower)
"actor" => user_to_map(actor),
"action" => "unfollow",
"followed" => user_to_map(followed),
"follower" => user_to_map(follower),
"message" => ""
}
})
}
|> insert_log_entry_with_message()
end

@spec insert_log(%{
actor: User,
action: String.t(),
nicknames: [String.t()],
tags: [String.t()]
}) :: {:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
nicknames: nicknames,
tags: tags,
action: action
}) do
Repo.insert(%ModerationLog{
%ModerationLog{
data: %{
actor: user_to_map(actor),
nicknames: nicknames,
tags: tags,
action: action
"actor" => user_to_map(actor),
"nicknames" => nicknames,
"tags" => tags,
"action" => action,
"message" => ""
}
})
}
|> insert_log_entry_with_message()
end

@spec insert_log(%{actor: User, action: String.t(), target: String.t()}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
action: action,
target: target
})
when action in ["relay_follow", "relay_unfollow"] do
Repo.insert(%ModerationLog{
%ModerationLog{
data: %{
actor: user_to_map(actor),
action: action,
target: target
"actor" => user_to_map(actor),
"action" => action,
"target" => target,
"message" => ""
}
})
}
|> insert_log_entry_with_message()
end

@spec insert_log_entry_with_message(ModerationLog) :: {:ok, ModerationLog} | {:error, any}

defp insert_log_entry_with_message(entry) do
entry.data["message"]
|> put_in(get_log_entry_message(entry))
|> Repo.insert()
end

defp user_to_map(%User{} = user) do
user
|> Map.from_struct()
|> Map.take([:id, :nickname])
|> Map.put(:type, "user")
|> Map.new(fn {k, v} -> {Atom.to_string(k), v} end)
|> Map.put("type", "user")
end

defp report_to_map(%Activity{} = report) do
%{
type: "report",
id: report.id,
state: report.data["state"]
"type" => "report",
"id" => report.id,
"state" => report.data["state"]
}
end

defp status_to_map(%Activity{} = status) do
%{
type: "status",
id: status.id
"type" => "status",
"id" => status.id
}
end



+ 2
- 2
lib/pleroma/notification.ex Vedi File

@@ -22,8 +22,8 @@ defmodule Pleroma.Notification do

schema "notifications" do
field(:seen, :boolean, default: false)
belongs_to(:user, User, type: Pleroma.FlakeId)
belongs_to(:activity, Activity, type: Pleroma.FlakeId)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)

timestamps()
end


+ 7
- 0
lib/pleroma/object.ex Vedi File

@@ -248,4 +248,11 @@ defmodule Pleroma.Object do
_ -> :noop
end
end

@doc "Updates data field of an object"
def update_data(%Object{data: data} = object, attrs \\ %{}) do
object
|> Object.change(%{data: Map.merge(data || %{}, attrs)})
|> Repo.update()
end
end

+ 39
- 36
lib/pleroma/object/fetcher.ex Vedi File

@@ -31,6 +31,7 @@ defmodule Pleroma.Object.Fetcher do

defp maybe_reinject_internal_fields(data, _), do: data

@spec reinject_object(struct(), map()) :: {:ok, Object.t()} | {:error, any()}
defp reinject_object(struct, data) do
Logger.debug("Reinjecting object #{data["id"]}")

@@ -61,52 +62,54 @@ defmodule Pleroma.Object.Fetcher do
# TODO:
# This will create a Create activity, which we need internally at the moment.
def fetch_object_from_id(id, options \\ []) do
if object = Object.get_cached_by_ap_id(id) do
with {:fetch_object, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)},
{:fetch, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)},
{:normalize, nil} <- {:normalize, Object.normalize(data, false)},
params <- prepare_activity_params(data),
{:containment, :ok} <- {:containment, Containment.contain_origin(id, params)},
{:ok, activity} <- Transmogrifier.handle_incoming(params, options),
{:object, _data, %Object{} = object} <-
{:object, data, Object.normalize(activity, false)} do
{:ok, object}
else
Logger.info("Fetching #{id} via AP")

with {:fetch, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)},
{:normalize, nil} <- {:normalize, Object.normalize(data, false)},
params <- %{
"type" => "Create",
"to" => data["to"],
"cc" => data["cc"],
# Should we seriously keep this attributedTo thing?
"actor" => data["actor"] || data["attributedTo"],
"object" => data
},
{:containment, :ok} <- {:containment, Containment.contain_origin(id, params)},
{:ok, activity} <- Transmogrifier.handle_incoming(params, options),
{:object, _data, %Object{} = object} <-
{:object, data, Object.normalize(activity, false)} do
{:ok, object}
else
{:containment, _} ->
{:error, "Object containment failed."}
{:containment, _} ->
{:error, "Object containment failed."}

{:error, {:reject, nil}} ->
{:reject, nil}
{:error, {:reject, nil}} ->
{:reject, nil}

{:object, data, nil} ->
reinject_object(%Object{}, data)
{:object, data, nil} ->
reinject_object(%Object{}, data)

{:normalize, object = %Object{}} ->
{:ok, object}
{:normalize, object = %Object{}} ->
{:ok, object}

_e ->
# Only fallback when receiving a fetch/normalization error with ActivityPub
Logger.info("Couldn't get object via AP, trying out OStatus fetching...")
{:fetch_object, %Object{} = object} ->
{:ok, object}

# FIXME: OStatus Object Containment?
case OStatus.fetch_activity_from_url(id) do
{:ok, [activity | _]} -> {:ok, Object.normalize(activity, false)}
e -> e
end
end
_e ->
# Only fallback when receiving a fetch/normalization error with ActivityPub
Logger.info("Couldn't get object via AP, trying out OStatus fetching...")

# FIXME: OStatus Object Containment?
case OStatus.fetch_activity_from_url(id) do
{:ok, [activity | _]} -> {:ok, Object.normalize(activity, false)}
e -> e
end
end
end

defp prepare_activity_params(data) do
%{
"type" => "Create",
"to" => data["to"],
"cc" => data["cc"],
# Should we seriously keep this attributedTo thing?
"actor" => data["actor"] || data["attributedTo"],
"object" => data
}
end

def fetch_object_from_id!(id, options \\ []) do
with {:ok, object} <- fetch_object_from_id(id, options) do
object


+ 1
- 0
lib/pleroma/pagination.ex Vedi File

@@ -64,6 +64,7 @@ defmodule Pleroma.Pagination do

def paginate(query, options, :offset) do
query
|> restrict(:order, options)
|> restrict(:offset, options)
|> restrict(:limit, options)
end


+ 1
- 1
lib/pleroma/password_reset_token.ex Vedi File

@@ -12,7 +12,7 @@ defmodule Pleroma.PasswordResetToken do
alias Pleroma.User

schema "password_reset_tokens" do
belongs_to(:user, User, type: Pleroma.FlakeId)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
field(:token, :string)
field(:used, :boolean, default: false)



+ 54
- 0
lib/pleroma/plugs/remote_ip.ex Vedi File

@@ -0,0 +1,54 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Plugs.RemoteIp do
@moduledoc """
This is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.
"""

@behaviour Plug

@headers ~w[
forwarded
x-forwarded-for
x-client-ip
x-real-ip
]

# https://en.wikipedia.org/wiki/Localhost
# https://en.wikipedia.org/wiki/Private_network
@reserved ~w[
127.0.0.0/8
::1/128
fc00::/7
10.0.0.0/8
172.16.0.0/12
192.168.0.0/16
]

def init(_), do: nil

def call(conn, _) do
config = Pleroma.Config.get(__MODULE__, [])

if Keyword.get(config, :enabled, false) do
RemoteIp.call(conn, remote_ip_opts(config))
else
conn
end
end

defp remote_ip_opts(config) do
headers = config |> Keyword.get(:headers, @headers) |> MapSet.new()
reserved = Keyword.get(config, :reserved, @reserved)

proxies =
config
|> Keyword.get(:proxies, [])
|> Enum.concat(reserved)
|> Enum.map(&InetCidr.parse/1)

{headers, proxies}
end
end

+ 2
- 2
lib/pleroma/registration.ex Vedi File

@@ -11,10 +11,10 @@ defmodule Pleroma.Registration do
alias Pleroma.Repo
alias Pleroma.User

@primary_key {:id, Pleroma.FlakeId, autogenerate: true}
@primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}

schema "registrations" do
belongs_to(:user, User, type: Pleroma.FlakeId)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
field(:provider, :string)
field(:uid, :string)
field(:info, :map, default: %{})


+ 1
- 1
lib/pleroma/scheduled_activity.ex Vedi File

@@ -17,7 +17,7 @@ defmodule Pleroma.ScheduledActivity do
@min_offset :timer.minutes(5)

schema "scheduled_activities" do
belongs_to(:user, User, type: Pleroma.FlakeId)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
field(:scheduled_at, :naive_datetime)
field(:params, :map)



+ 2
- 2
lib/pleroma/thread_mute.ex Vedi File

@@ -12,7 +12,7 @@ defmodule Pleroma.ThreadMute do
require Ecto.Query

schema "thread_mutes" do
belongs_to(:user, User, type: Pleroma.FlakeId)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
field(:context, :string)
end

@@ -24,7 +24,7 @@ defmodule Pleroma.ThreadMute do
end

def query(user_id, context) do
user_id = Pleroma.FlakeId.from_string(user_id)
{:ok, user_id} = FlakeId.Ecto.CompatType.dump(user_id)

ThreadMute
|> Ecto.Query.where(user_id: ^user_id)


+ 16
- 6
lib/pleroma/uploaders/s3.ex Vedi File

@@ -38,16 +38,26 @@ defmodule Pleroma.Uploaders.S3 do
def put_file(%Pleroma.Upload{} = upload) do
config = Config.get([__MODULE__])
bucket = Keyword.get(config, :bucket)
streaming = Keyword.get(config, :streaming_enabled)

s3_name = strict_encode(upload.path)

op =
upload.tempfile
|> ExAws.S3.Upload.stream_file()
|> ExAws.S3.upload(bucket, s3_name, [
{:acl, :public_read},
{:content_type, upload.content_type}
])
if streaming do
upload.tempfile
|> ExAws.S3.Upload.stream_file()
|> ExAws.S3.upload(bucket, s3_name, [
{:acl, :public_read},
{:content_type, upload.content_type}
])
else
{:ok, file_data} = File.read(upload.tempfile)

ExAws.S3.put_object(bucket, s3_name, file_data, [
{:acl, :public_read},
{:content_type, upload.content_type}
])
end

case ExAws.request(op) do
{:ok, _} ->


+ 206
- 299
lib/pleroma/user.ex Vedi File

@@ -34,7 +34,7 @@ defmodule Pleroma.User do

@type t :: %__MODULE__{}

@primary_key {:id, Pleroma.FlakeId, autogenerate: true}
@primary_key {:id, FlakeId.Ecto.CompatType, 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])?)*$/
@@ -106,9 +106,7 @@ defmodule Pleroma.User do
def profile_url(%User{ap_id: ap_id}), do: ap_id
def profile_url(_), do: nil

def ap_id(%User{nickname: nickname}) do
"#{Web.base_url()}/users/#{nickname}"
end
def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"

def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
@@ -119,12 +117,9 @@ defmodule Pleroma.User do

def user_info(%User{} = user, args \\ %{}) do
following_count =
if args[:following_count],
do: args[:following_count],
else: user.info.following_count || following_count(user)
Map.get(args, :following_count, user.info.following_count || following_count(user))

follower_count =
if args[:follower_count], do: args[:follower_count], else: user.info.follower_count
follower_count = Map.get(args, :follower_count, user.info.follower_count)

%{
note_count: user.info.note_count,
@@ -137,12 +132,11 @@ defmodule Pleroma.User do
end

def follow_state(%User{} = user, %User{} = target) do
follow_activity = Utils.fetch_latest_follow(user, target)

if follow_activity,
do: follow_activity.data["state"],
case Utils.fetch_latest_follow(user, target) do
%{data: %{"state" => state}} -> state
# Ideally this would be nil, but then Cachex does not commit the value
else: false
_ -> false
end
end

def get_cached_follow_state(user, target) do
@@ -152,11 +146,7 @@ defmodule Pleroma.User do

@spec set_follow_state_cache(String.t(), String.t(), String.t()) :: {:ok | :error, boolean()}
def set_follow_state_cache(user_ap_id, target_ap_id, state) do
Cachex.put(
:user_cache,
"follow_state:#{user_ap_id}|#{target_ap_id}",
state
)
Cachex.put(:user_cache, "follow_state:#{user_ap_id}|#{target_ap_id}", state)
end

def set_info_cache(user, args) do
@@ -197,34 +187,25 @@ defmodule Pleroma.User do
|> truncate_if_exists(:name, name_limit)
|> truncate_if_exists(:bio, bio_limit)

info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])

changes =
%User{}
changeset =
%User{local: false}
|> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
|> validate_required([:name, :ap_id])
|> unique_constraint(:nickname)
|> validate_format(:nickname, @email_regex)
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, max: name_limit)
|> put_change(:local, false)
|> put_embed(:info, info_cng)

if changes.valid? do
case info_cng.changes[:source_data] do
%{"followers" => followers, "following" => following} ->
changes
|> put_change(:follower_address, followers)
|> put_change(:following_address, following)
|> change_info(&User.Info.remote_user_creation(&1, params[:info]))

_ ->
followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
case params[:info][:source_data] do
%{"followers" => followers, "following" => following} ->
changeset
|> put_change(:follower_address, followers)
|> put_change(:following_address, following)

changes
|> put_change(:follower_address, followers)
end
else
changes
_ ->
followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
put_change(changeset, :follower_address, followers)
end
end

@@ -245,7 +226,6 @@ defmodule Pleroma.User do
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)

params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
info_cng = User.Info.user_upgrade(struct.info, params[:info], remote?)

struct
|> cast(params, [
@@ -260,7 +240,7 @@ defmodule Pleroma.User do
|> validate_format(:nickname, local_nickname_regex())
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, max: name_limit)
|> put_embed(:info, info_cng)
|> change_info(&User.Info.user_upgrade(&1, params[:info], remote?))
end

def password_update_changeset(struct, params) do
@@ -269,6 +249,7 @@ defmodule Pleroma.User do
|> validate_required([:password, :password_confirmation])
|> validate_confirmation(:password)
|> put_password_hash
|> put_embed(:info, User.Info.set_password_reset_pending(struct.info, false))
end

@spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
@@ -285,6 +266,20 @@ defmodule Pleroma.User do
end
end

def force_password_reset_async(user) do
BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
end

@spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def force_password_reset(user) do
info_cng = User.Info.set_password_reset_pending(user.info, true)

user
|> change()
|> put_embed(:info, info_cng)
|> update_and_set_cache()
end

def register_changeset(struct, params \\ %{}, opts \\ []) do
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
@@ -296,43 +291,39 @@ defmodule Pleroma.User do
opts[:need_confirmation]
end

info_change =
User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?)
struct
|> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
|> validate_required([:name, :nickname, :password, :password_confirmation])
|> validate_confirmation(:password)
|> unique_constraint(:email)
|> unique_constraint(:nickname)
|> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
|> validate_format(:nickname, local_nickname_regex())
|> validate_format(:email, @email_regex)
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, min: 1, max: name_limit)
|> change_info(&User.Info.confirmation_changeset(&1, need_confirmation: need_confirmation?))
|> maybe_validate_required_email(opts[:external])
|> put_password_hash
|> put_ap_id()
|> unique_constraint(:ap_id)
|> put_following_and_follower_address()
end

changeset =
struct
|> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
|> validate_required([:name, :nickname, :password, :password_confirmation])
|> validate_confirmation(:password)
|> unique_constraint(:email)
|> unique_constraint(:nickname)
|> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames]))
|> validate_format(:nickname, local_nickname_regex())
|> validate_format(:email, @email_regex)
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, min: 1, max: name_limit)
|> put_change(:info, info_change)
def maybe_validate_required_email(changeset, true), do: changeset
def maybe_validate_required_email(changeset, _), do: validate_required(changeset, [:email])

changeset =
if opts[:external] do
changeset
else
validate_required(changeset, [:email])
end
defp put_ap_id(changeset) do
ap_id = ap_id(%User{nickname: get_field(changeset, :nickname)})
put_change(changeset, :ap_id, ap_id)
end

if changeset.valid? do
ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
defp put_following_and_follower_address(changeset) do
followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})

changeset
|> put_password_hash
|> put_change(:ap_id, ap_id)
|> unique_constraint(:ap_id)
|> put_change(:following, [followers])
|> put_change(:follower_address, followers)
else
changeset
end
changeset
|> put_change(:following, [followers])
|> put_change(:follower_address, followers)
end

defp autofollow_users(user) do
@@ -347,9 +338,8 @@ defmodule Pleroma.User do

@doc "Inserts provided changeset, performs post-registration actions (confirmation email sending etc.)"
def register(%Ecto.Changeset{} = changeset) do
with {:ok, user} <- Repo.insert(changeset),
{:ok, user} <- post_register_action(user) do
{:ok, user}
with {:ok, user} <- Repo.insert(changeset) do
post_register_action(user)
end
end

@@ -395,7 +385,7 @@ defmodule Pleroma.User do
end

def maybe_direct_follow(%User{} = follower, %User{} = followed) do
if not User.ap_enabled?(followed) do
if not ap_enabled?(followed) do
follow(follower, followed)
else
{:ok, follower}
@@ -428,9 +418,7 @@ defmodule Pleroma.User do

{1, [follower]} = Repo.update_all(q, [])

Enum.each(followeds, fn followed ->
update_follower_count(followed)
end)
Enum.each(followeds, &update_follower_count/1)

set_cache(follower)
end
@@ -517,6 +505,11 @@ defmodule Pleroma.User do
|> Repo.all()
end

def get_all_by_ids(ids) do
from(u in __MODULE__, where: u.id in ^ids)
|> Repo.all()
end

# This is mostly an SPC migration fix. This guesses the user nickname by taking the last part
# of the ap_id and the domain and tries to get that user
def get_by_guessed_nickname(ap_id) do
@@ -540,8 +533,6 @@ defmodule Pleroma.User do
def update_and_set_cache(changeset) do
with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
set_cache(user)
else
e -> e
end
end

@@ -578,9 +569,7 @@ defmodule Pleroma.User do
key = "nickname:#{nickname}"

Cachex.fetch!(:user_cache, key, fn ->
user_result = get_or_fetch_by_nickname(nickname)

case user_result do
case get_or_fetch_by_nickname(nickname) do
{:ok, user} -> {:commit, user}
{:error, _error} -> {:ignore, nil}
end
@@ -591,7 +580,7 @@ defmodule Pleroma.User do
restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])

cond do
is_integer(nickname_or_id) or Pleroma.FlakeId.is_flake_id?(nickname_or_id) ->
is_integer(nickname_or_id) or FlakeId.flake_id?(nickname_or_id) ->
get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)

restrict_to_local == false ->
@@ -620,13 +609,11 @@ defmodule Pleroma.User do

def get_cached_user_info(user) do
key = "user_info:#{user.id}"
Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
Cachex.fetch!(:user_cache, key, fn -> user_info(user) end)
end

def fetch_by_nickname(nickname) do
ap_try = ActivityPub.make_user_from_nickname(nickname)

case ap_try do
case ActivityPub.make_user_from_nickname(nickname) do
{:ok, user} -> {:ok, user}
_ -> OStatus.make_user(nickname)
end
@@ -661,7 +648,8 @@ defmodule Pleroma.User do
end

def get_followers_query(user, page) do
from(u in get_followers_query(user, nil))
user
|> get_followers_query(nil)
|> User.Query.paginate(page, 20)
end

@@ -670,25 +658,24 @@ defmodule Pleroma.User do

@spec get_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
def get_followers(user, page \\ nil) do
q = get_followers_query(user, page)
{:ok, Repo.all(q)}
user
|> get_followers_query(page)
|> Repo.all()
end

@spec get_external_followers(User.t(), pos_integer()) :: {:ok, list(User.t())}
def get_external_followers(user, page \\ nil) do
q =
user
|> get_followers_query(page)
|> User.Query.build(%{external: true})

{:ok, Repo.all(q)}
user
|> get_followers_query(page)
|> User.Query.build(%{external: true})
|> Repo.all()
end

def get_followers_ids(user, page \\ nil) do
q = get_followers_query(user, page)

Repo.all(from(u in q, select: u.id))
user
|> get_followers_query(page)
|> select([u], u.id)
|> Repo.all()
end

@spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t()
@@ -697,7 +684,8 @@ defmodule Pleroma.User do
end

def get_friends_query(user, page) do
from(u in get_friends_query(user, nil))
user
|> get_friends_query(nil)
|> User.Query.paginate(page, 20)
end

@@ -705,28 +693,27 @@ defmodule Pleroma.User do
def get_friends_query(user), do: get_friends_query(user, nil)

def get_friends(user, page \\ nil) do
q = get_friends_query(user, page)
{:ok, Repo.all(q)}
user
|> get_friends_query(page)
|> Repo.all()
end

def get_friends_ids(user, page \\ nil) do
q = get_friends_query(user, page)

Repo.all(from(u in q, select: u.id))
user
|> get_friends_query(page)
|> select([u], u.id)
|> Repo.all()
end

@spec get_follow_requests(User.t()) :: {:ok, [User.t()]}
def get_follow_requests(%User{} = user) do
users =
Activity.follow_requests_for_actor(user)
|> join(:inner, [a], u in User, on: a.actor == u.ap_id)
|> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
|> group_by([a, u], u.id)
|> select([a, u], u)
|> Repo.all()

{:ok, users}
user
|> Activity.follow_requests_for_actor()
|> join(:inner, [a], u in User, on: a.actor == u.ap_id)
|> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address]))
|> group_by([a, u], u.id)
|> select([a, u], u)
|> Repo.all()
end

def increase_note_count(%User{} = user) do
@@ -772,20 +759,27 @@ defmodule Pleroma.User do
end

def update_note_count(%User{} = user) do
note_count_query =
note_count =
from(
a in Object,
where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
select: count(a.id)
)
|> Repo.one()

note_count = Repo.one(note_count_query)
update_info(user, &User.Info.set_note_count(&1, note_count))
end

info_cng = User.Info.set_note_count(user.info, note_count)
def update_mascot(user, url) do
info_changeset =
User.Info.mascot_update(
user.info,
url
)

user
|> change()
|> put_embed(:info, info_cng)
|> put_embed(:info, info_changeset)
|> update_and_set_cache()
end

@@ -803,17 +797,7 @@ defmodule Pleroma.User do

def fetch_follow_information(user) do
with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do
info_cng = User.Info.follow_information_update(user.info, info)

changeset =
user
|> change()
|> put_embed(:info, info_cng)

update_and_set_cache(changeset)
else
{:error, _} = e -> e
e -> {:error, e}
update_info(user, &User.Info.follow_information_update(&1, info))
end
end

@@ -887,62 +871,28 @@ defmodule Pleroma.User do

@spec mute(User.t(), User.t(), boolean()) :: {:ok, User.t()} | {:error, String.t()}
def mute(muter, %User{ap_id: ap_id}, notifications? \\ true) do
info = muter.info

info_cng =
User.Info.add_to_mutes(info, ap_id)
|> User.Info.add_to_muted_notifications(info, ap_id, notifications?)

cng =
change(muter)
|> put_embed(:info, info_cng)

update_and_set_cache(cng)
update_info(muter, &User.Info.add_to_mutes(&1, ap_id, notifications?))
end

def unmute(muter, %{ap_id: ap_id}) do
info = muter.info

info_cng =
User.Info.remove_from_mutes(info, ap_id)
|> User.Info.remove_from_muted_notifications(info, ap_id)

cng =
change(muter)
|> put_embed(:info, info_cng)

update_and_set_cache(cng)
update_info(muter, &User.Info.remove_from_mutes(&1, ap_id))
end

def subscribe(subscriber, %{ap_id: ap_id}) do
deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])

with %User{} = subscribed <- get_cached_by_ap_id(ap_id) do
blocked = blocks?(subscribed, subscriber) and deny_follow_blocked
deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])

if blocked do
if blocks?(subscribed, subscriber) and deny_follow_blocked do
{:error, "Could not subscribe: #{subscribed.nickname} is blocking you"}
else
info_cng =
subscribed.info
|> User.Info.add_to_subscribers(subscriber.ap_id)

change(subscribed)
|> put_embed(:info, info_cng)
|> update_and_set_cache()
update_info(subscribed, &User.Info.add_to_subscribers(&1, subscriber.ap_id))
end
end
end

def unsubscribe(unsubscriber, %{ap_id: ap_id}) do
with %User{} = user <- get_cached_by_ap_id(ap_id) do
info_cng =
user.info
|> User.Info.remove_from_subscribers(unsubscriber.ap_id)

change(user)
|> put_embed(:info, info_cng)
|> update_and_set_cache()
update_info(user, &User.Info.remove_from_subscribers(&1, unsubscriber.ap_id))
end
end

@@ -971,21 +921,11 @@ defmodule Pleroma.User do
blocker
end

if following?(blocked, blocker) do
unfollow(blocked, blocker)
end
if following?(blocked, blocker), do: unfollow(blocked, blocker)

{:ok, blocker} = update_follower_count(blocker)

info_cng =
blocker.info
|> User.Info.add_to_block(ap_id)

cng =
change(blocker)
|> put_embed(:info, info_cng)

update_and_set_cache(cng)
update_info(blocker, &User.Info.add_to_block(&1, ap_id))
end

# helper to handle the block given only an actor's AP id
@@ -994,15 +934,7 @@ defmodule Pleroma.User do
end

def unblock(blocker, %{ap_id: ap_id}) do
info_cng =
blocker.info
|> User.Info.remove_from_block(ap_id)

cng =
change(blocker)
|> put_embed(:info, info_cng)

update_and_set_cache(cng)
update_info(blocker, &User.Info.remove_from_block(&1, ap_id))
end

def mutes?(nil, _), do: false
@@ -1059,27 +991,11 @@ defmodule Pleroma.User do
end

def block_domain(user, domain) do
info_cng =
user.info
|> User.Info.add_to_domain_block(domain)

cng =
change(user)
|> put_embed(:info, info_cng)

update_and_set_cache(cng)
update_info(user, &User.Info.add_to_domain_block(&1, domain))
end

def unblock_domain(user, domain) do
info_cng =
user.info
|> User.Info.remove_from_domain_block(domain)

cng =
change(user)
|> put_embed(:info, info_cng)

update_and_set_cache(cng)
update_info(user, &User.Info.remove_from_domain_block(&1, domain))
end

def deactivate_async(user, status \\ true) do
@@ -1087,51 +1003,41 @@ defmodule Pleroma.User do
end

def deactivate(%User{} = user, status \\ true) do
info_cng = User.Info.set_activation_status(user.info, status)

with {:ok, friends} <- User.get_friends(user),
{:ok, followers} <- User.get_followers(user),
{:ok, user} <-
user
|> change()
|> put_embed(:info, info_cng)
|> update_and_set_cache() do
Enum.each(followers, &invalidate_cache(&1))
Enum.each(friends, &update_follower_count(&1))
with {:ok, user} <- update_info(user, &User.Info.set_activation_status(&1, status)) do
Enum.each(get_followers(user), &invalidate_cache/1)
Enum.each(get_friends(user), &update_follower_count/1)

{:ok, user}
end
end

def update_notification_settings(%User{} = user, settings \\ %{}) do
info_changeset = User.Info.update_notification_settings(user.info, settings)

change(user)
|> put_embed(:info, info_changeset)
|> update_and_set_cache()
update_info(user, &User.Info.update_notification_settings(&1, settings))
end

def delete(%User{} = user) do
BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
end

def perform(:force_password_reset, user), do: force_password_reset(user)

@spec perform(atom(), User.t()) :: {:ok, User.t()}
def perform(:delete, %User{} = user) do
{:ok, _user} = ActivityPub.delete(user)

# Remove all relationships
{:ok, followers} = User.get_followers(user)
Enum.each(followers, fn follower ->
user
|> get_followers()
|> Enum.each(fn follower ->
ActivityPub.unfollow(follower, user)
User.unfollow(follower, user)
unfollow(follower, user)
end)

{:ok, friends} = User.get_friends(user)
Enum.each(friends, fn followed ->
user
|> get_friends()
|> Enum.each(fn followed ->
ActivityPub.unfollow(user, followed)
User.unfollow(user, followed)
unfollow(user, followed)
end)

delete_user_activities(user)
@@ -1143,13 +1049,11 @@ defmodule Pleroma.User do
def perform(:fetch_initial_posts, %User{} = 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
)

{:ok, user}
# Insert all the posts in reverse order, so they're in the right order on the timeline
user.info.source_data["outbox"]
|> Utils.fetch_ordered_collection(pages)
|> Enum.reverse()
|> Enum.each(&Pleroma.Web.Federator.incoming_ap_doc/1)
end

def perform(:deactivate_async, user, status), do: deactivate(user, status)
@@ -1235,16 +1139,12 @@ defmodule Pleroma.User do
})
end

def delete_user_activities(%User{ap_id: ap_id} = user) do
def delete_user_activities(%User{ap_id: ap_id}) do
ap_id
|> Activity.Queries.by_actor()
|> RepoStreamer.chunk_stream(50)
|> Stream.each(fn activities ->
Enum.each(activities, &delete_activity(&1))
end)
|> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
|> Stream.run()

{:ok, user}
end

defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
@@ -1254,17 +1154,19 @@ defmodule Pleroma.User do
end

defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
user = get_cached_by_ap_id(activity.actor)
object = Object.normalize(activity)

ActivityPub.unlike(user, object)
activity.actor
|> get_cached_by_ap_id()
|> ActivityPub.unlike(object)
end

defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
user = get_cached_by_ap_id(activity.actor)
object = Object.normalize(activity)

ActivityPub.unannounce(user, object)
activity.actor
|> get_cached_by_ap_id()
|> ActivityPub.unannounce(object)
end

defp delete_activity(_activity), do: "Doing nothing"
@@ -1276,9 +1178,7 @@ defmodule Pleroma.User do
def html_filter_policy(_), do: Pleroma.Config.get([:markup, :scrub_policy])

def fetch_by_ap_id(ap_id) do
ap_try = ActivityPub.make_user_from_ap_id(ap_id)

case ap_try do
case ActivityPub.make_user_from_ap_id(ap_id) do
{:ok, user} ->
{:ok, user}

@@ -1293,7 +1193,7 @@ defmodule Pleroma.User do
def get_or_fetch_by_ap_id(ap_id) do
user = get_cached_by_ap_id(ap_id)

if !is_nil(user) and !User.needs_update?(user) do
if !is_nil(user) and !needs_update?(user) do
{:ok, user}
else
# Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled)
@@ -1313,19 +1213,20 @@ defmodule Pleroma.User do

@doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing."
def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
if user = get_cached_by_ap_id(uri) do
with %User{} = user <- get_cached_by_ap_id(uri) do
user
else
changes =
%User{info: %User.Info{}}
|> cast(%{}, [:ap_id, :nickname, :local])
|> put_change(:ap_id, uri)
|> put_change(:nickname, nickname)
|> put_change(:local, true)
|> put_change(:follower_address, uri <> "/followers")

{:ok, user} = Repo.insert(changes)
user
_ ->
{:ok, user} =
%User{info: %User.Info{}}
|> cast(%{}, [:ap_id, :nickname, :local])
|> put_change(:ap_id, uri)
|> put_change(:nickname, nickname)
|> put_change(:local, true)
|> put_change(:follower_address, uri <> "/followers")
|> Repo.insert()

user
end
end

@@ -1382,23 +1283,21 @@ defmodule Pleroma.User do
# this is because we have synchronous follow APIs and need to simulate them
# with an async handshake
def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
with %User{} = a <- User.get_cached_by_id(a.id),
%User{} = b <- User.get_cached_by_id(b.id) do
with %User{} = a <- get_cached_by_id(a.id),
%User{} = b <- get_cached_by_id(b.id) do
{:ok, a, b}
else
_e ->
:error
nil -> :error
end
end

def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
with :ok <- :timer.sleep(timeout),
%User{} = a <- User.get_cached_by_id(a.id),
%User{} = b <- User.get_cached_by_id(b.id) do
%User{} = a <- get_cached_by_id(a.id),
%User{} = b <- get_cached_by_id(b.id) do
{:ok, a, b}
else
_e ->
:error
nil -> :error
end
end

@@ -1460,7 +1359,7 @@ defmodule Pleroma.User do
defp normalize_tags(tags) do
[tags]
|> List.flatten()
|> Enum.map(&String.downcase(&1))
|> Enum.map(&String.downcase/1)
end

defp local_nickname_regex do
@@ -1553,11 +1452,7 @@ defmodule Pleroma.User do
@spec switch_email_notifications(t(), String.t(), boolean()) ::
{:ok, t()} | {:error, Ecto.Changeset.t()}
def switch_email_notifications(user, type, status) do
info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status})

change(user)
|> put_embed(:info, info)
|> update_and_set_cache()
update_info(user, &User.Info.update_email_notifications(&1, %{type => status}))
end

@doc """
@@ -1579,13 +1474,8 @@ defmodule Pleroma.User do
def toggle_confirmation(%User{} = user) do
need_confirmation? = !user.info.confirmation_pending

info_changeset =
User.Info.confirmation_changeset(user.info, need_confirmation: need_confirmation?)

user
|> change()
|> put_embed(:info, info_changeset)
|> update_and_set_cache()
|> update_info(&User.Info.confirmation_changeset(&1, need_confirmation: need_confirmation?))
end

def get_mascot(%{info: %{mascot: %{} = mascot}}) when not is_nil(mascot) do
@@ -1608,16 +1498,11 @@ defmodule Pleroma.User do
}
end

def ensure_keys_present(%User{info: info} = user) do
if info.keys do
{:ok, user}
else
{:ok, pem} = Keys.generate_rsa_pem()
def ensure_keys_present(%{info: %{keys: keys}} = user) when not is_nil(keys), do: {:ok, user}

user
|> Ecto.Changeset.change()
|> Ecto.Changeset.put_embed(:info, User.Info.set_keys(info, pem))
|> update_and_set_cache()
def ensure_keys_present(%User{} = user) do
with {:ok, pem} <- Keys.generate_rsa_pem() do
update_info(user, &User.Info.set_keys(&1, pem))
end
end

@@ -1663,4 +1548,26 @@ defmodule Pleroma.User do
|> validate_format(:email, @email_regex)
|> update_and_set_cache()
end

@doc """
Changes `user.info` and returns the user changeset.

`fun` is called with the `user.info`.
"""
def change_info(user, fun) do
changeset = change(user)
info = get_field(changeset, :info) || %User.Info{}
put_embed(changeset, :info, fun.(info))
end

@doc """
Updates `user.info` and sets cache.

`fun` is called with the `user.info`.
"""
def update_info(user, fun) do
user
|> change_info(fun)
|> update_and_set_cache()
end
end

+ 24
- 21
lib/pleroma/user/info.ex Vedi File

@@ -20,6 +20,7 @@ defmodule Pleroma.User.Info do
field(:following_count, :integer, default: nil)
field(:locked, :boolean, default: false)
field(:confirmation_pending, :boolean, default: false)
field(:password_reset_pending, :boolean, default: false)
field(:confirmation_token, :string, default: nil)
field(:default_scope, :string, default: "public")
field(:blocks, {:array, :string}, default: [])
@@ -53,6 +54,7 @@ defmodule Pleroma.User.Info do
field(:pleroma_settings_store, :map, default: %{})
field(:fields, {:array, :map}, default: nil)
field(:raw_fields, {:array, :map}, default: [])
field(:discoverable, :boolean, default: false)

field(:notification_settings, :map,
default: %{
@@ -82,6 +84,14 @@ defmodule Pleroma.User.Info do
|> validate_required([:deactivated])
end

def set_password_reset_pending(info, pending) do
params = %{password_reset_pending: pending}

info
|> cast(params, [:password_reset_pending])
|> validate_required([:password_reset_pending])
end

def update_notification_settings(info, settings) do
settings =
settings
@@ -178,16 +188,11 @@ defmodule Pleroma.User.Info do
|> validate_required([:subscribers])
end

@spec add_to_mutes(Info.t(), String.t()) :: Changeset.t()
def add_to_mutes(info, muted) do
set_mutes(info, Enum.uniq([muted | info.mutes]))
end

@spec add_to_muted_notifications(Changeset.t(), Info.t(), String.t(), boolean()) ::
Changeset.t()
def add_to_muted_notifications(changeset, info, muted, notifications?) do
set_notification_mutes(
changeset,
@spec add_to_mutes(Info.t(), String.t(), boolean()) :: Changeset.t()
def add_to_mutes(info, muted, notifications?) do
info
|> set_mutes(Enum.uniq([muted | info.mutes]))
|> set_notification_mutes(
Enum.uniq([muted | info.muted_notifications]),
notifications?
)
@@ -195,12 +200,9 @@ defmodule Pleroma.User.Info do

@spec remove_from_mutes(Info.t(), String.t()) :: Changeset.t()
def remove_from_mutes(info, muted) do
set_mutes(info, List.delete(info.mutes, muted))
end

@spec remove_from_muted_notifications(Changeset.t(), Info.t(), String.t()) :: Changeset.t()
def remove_from_muted_notifications(changeset, info, muted) do
set_notification_mutes(changeset, List.delete(info.muted_notifications, muted), true)
info
|> set_mutes(List.delete(info.mutes, muted))
|> set_notification_mutes(List.delete(info.muted_notifications, muted), true)
end

def add_to_block(info, blocked) do
@@ -268,7 +270,8 @@ defmodule Pleroma.User.Info do
:hide_follows_count,
:follower_count,
:fields,
:following_count
:following_count,
:discoverable
])
|> validate_fields(true)
end
@@ -286,6 +289,7 @@ defmodule Pleroma.User.Info do
:hide_follows,
:fields,
:hide_followers,
:discoverable,
:hide_followers_count,
:hide_follows_count
])
@@ -309,7 +313,8 @@ defmodule Pleroma.User.Info do
:skip_thread_containment,
:fields,
:raw_fields,
:pleroma_settings_store
:pleroma_settings_store,
:discoverable
])
|> validate_fields()
end
@@ -333,9 +338,7 @@ defmodule Pleroma.User.Info do
name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)

is_binary(name) &&
is_binary(value) &&
String.length(name) <= name_limit &&
is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
String.length(value) <= value_limit
end



+ 56
- 11
lib/pleroma/web/activity_pub/activity_pub.ex Vedi File

@@ -248,6 +248,26 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end

def listen(%{to: to, actor: actor, context: context, object: object} = params) do
additional = params[:additional] || %{}
# only accept false as false value
local = !(params[:local] == false)
published = params[:published]

with listen_data <-
make_listen_data(
%{to: to, actor: actor, published: published, context: context, object: object},
additional
),
{:ok, activity} <- insert(listen_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
{:error, message} ->
{:error, message}
end
end

def accept(%{to: to, actor: actor, object: object} = params) do
# only accept false as false value
local = !(params[:local] == false)
@@ -510,7 +530,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end

@spec fetch_latest_activity_id_for_context(String.t(), keyword() | map()) ::
Pleroma.FlakeId.t() | nil
FlakeId.Ecto.CompatType.t() | nil
def fetch_latest_activity_id_for_context(context, opts \\ %{}) do
context
|> fetch_activities_for_context_query(Map.merge(%{"skip_preload" => true}, opts))
@@ -519,12 +539,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> Repo.one()
end

def fetch_public_activities(opts \\ %{}) do
q = fetch_activities_query([Pleroma.Constants.as_public()], opts)
def fetch_public_activities(opts \\ %{}, pagination \\ :keyset) do
opts = Map.drop(opts, ["user"])

q
[Pleroma.Constants.as_public()]
|> fetch_activities_query(opts)
|> restrict_unlisted()
|> Pagination.fetch_paginated(opts)
|> Pagination.fetch_paginated(opts, pagination)
|> Enum.reverse()
end

@@ -587,6 +608,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do

defp restrict_thread_visibility(query, _, _), do: query

def fetch_user_abstract_activities(user, reading_user, params \\ %{}) do
params =
params
|> Map.put("user", reading_user)
|> Map.put("actor_id", user.ap_id)
|> Map.put("whole_db", true)

recipients =
user_activities_recipients(%{
"godmode" => params["godmode"],
"reading_user" => reading_user
})

fetch_activities(recipients, params)
|> Enum.reverse()
end

def fetch_user_activities(user, reading_user, params \\ %{}) do
params =
params
@@ -833,7 +871,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do

defp restrict_muted_reblogs(query, _), do: query

defp exclude_poll_votes(query, %{"include_poll_votes" => "true"}), do: query
defp exclude_poll_votes(query, %{"include_poll_votes" => true}), do: query

defp exclude_poll_votes(query, _) do
if has_named_binding?(query, :object) do
@@ -917,11 +955,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> exclude_poll_votes(opts)
end

def fetch_activities(recipients, opts \\ %{}) do
def fetch_activities(recipients, opts \\ %{}, pagination \\ :keyset) do
list_memberships = Pleroma.List.memberships(opts["user"])

fetch_activities_query(recipients ++ list_memberships, opts)
|> Pagination.fetch_paginated(opts)
|> Pagination.fetch_paginated(opts, pagination)
|> Enum.reverse()
|> maybe_update_cc(list_memberships, opts["user"])
end
@@ -952,10 +990,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
)
end

def fetch_activities_bounded(recipients, recipients_with_public, opts \\ %{}) do
def fetch_activities_bounded(
recipients,
recipients_with_public,
opts \\ %{},
pagination \\ :keyset
) do
fetch_activities_query([], opts)
|> fetch_activities_bounded_query(recipients, recipients_with_public)
|> Pagination.fetch_paginated(opts)
|> Pagination.fetch_paginated(opts, pagination)
|> Enum.reverse()
end

@@ -995,6 +1038,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do

locked = data["manuallyApprovesFollowers"] || false
data = Transmogrifier.maybe_fix_user_object(data)
discoverable = data["discoverable"] || false

user_data = %{
ap_id: data["id"],
@@ -1003,7 +1047,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
source_data: data,
banner: banner,
fields: fields,
locked: locked
locked: locked,
discoverable: discoverable
},
avatar: avatar,
name: data["name"],


+ 111
- 16
lib/pleroma/web/activity_pub/activity_pub_controller.ex Vedi File

@@ -54,7 +54,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
{:ok, user} <- User.ensure_keys_present(user) do
conn
|> put_resp_content_type("application/activity+json")
|> json(UserView.render("user.json", %{user: user}))
|> put_view(UserView)
|> render("user.json", %{user: user})
else
nil -> {:error, :not_found}
end
@@ -95,7 +96,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do

conn
|> put_resp_content_type("application/activity+json")
|> json(ObjectView.render("likes.json", ap_id, likes, page))
|> put_view(ObjectView)
|> render("likes.json", %{ap_id: ap_id, likes: likes, page: page})
else
{:public?, false} ->
{:error, :not_found}
@@ -109,7 +111,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
likes <- Utils.get_object_likes(object) do
conn
|> put_resp_content_type("application/activity+json")
|> json(ObjectView.render("likes.json", ap_id, likes))
|> put_view(ObjectView)
|> render("likes.json", %{ap_id: ap_id, likes: likes})
else
{:public?, false} ->
{:error, :not_found}
@@ -163,7 +166,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
def following(%{assigns: %{relay: true}} = conn, _params) do
conn
|> put_resp_content_type("application/activity+json")
|> json(UserView.render("following.json", %{user: Relay.get_actor()}))
|> put_view(UserView)
|> render("following.json", %{user: Relay.get_actor()})
end

def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
@@ -175,7 +179,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do

conn
|> put_resp_content_type("application/activity+json")
|> json(UserView.render("following.json", %{user: user, page: page, for: for_user}))
|> put_view(UserView)
|> render("following.json", %{user: user, page: page, for: for_user})
else
{:show_follows, _} ->
conn
@@ -189,7 +194,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
conn
|> put_resp_content_type("application/activity+json")
|> json(UserView.render("following.json", %{user: user, for: for_user}))
|> put_view(UserView)
|> render("following.json", %{user: user, for: for_user})
end
end

@@ -197,7 +203,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
def followers(%{assigns: %{relay: true}} = conn, _params) do
conn
|> put_resp_content_type("application/activity+json")
|> json(UserView.render("followers.json", %{user: Relay.get_actor()}))
|> put_view(UserView)
|> render("followers.json", %{user: Relay.get_actor()})
end

def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
@@ -209,7 +216,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do

conn
|> put_resp_content_type("application/activity+json")
|> json(UserView.render("followers.json", %{user: user, page: page, for: for_user}))
|> put_view(UserView)
|> render("followers.json", %{user: user, page: page, for: for_user})
else
{:show_followers, _} ->
conn
@@ -223,16 +231,48 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
conn
|> put_resp_content_type("application/activity+json")
|> json(UserView.render("followers.json", %{user: user, for: for_user}))
|> put_view(UserView)
|> render("followers.json", %{user: user, for: for_user})
end
end

def outbox(conn, %{"nickname" => nickname, "page" => page?} = params)
when page? in [true, "true"] do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- User.ensure_keys_present(user) do
activities =
if params["max_id"] do
ActivityPub.fetch_user_activities(user, nil, %{
"max_id" => params["max_id"],
# This is a hack because postgres generates inefficient queries when filtering by
# 'Answer', poll votes will be hidden by the visibility filter in this case anyway
"include_poll_votes" => true,
"limit" => 10
})
else
ActivityPub.fetch_user_activities(user, nil, %{
"limit" => 10,
"include_poll_votes" => true
})
end

conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("activity_collection_page.json", %{
activities: activities,
iri: "#{user.ap_id}/outbox"
})
end
end

def outbox(conn, %{"nickname" => nickname} = params) do
def outbox(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- User.ensure_keys_present(user) do
conn
|> put_resp_content_type("application/activity+json")
|> json(UserView.render("outbox.json", %{user: user, max_id: params["max_id"]}))
|> put_view(UserView)
|> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
end
end

@@ -280,7 +320,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
with {:ok, user} <- User.ensure_keys_present(user) do
conn
|> put_resp_content_type("application/activity+json")
|> json(UserView.render("user.json", %{user: user}))
|> put_view(UserView)
|> render("user.json", %{user: user})
else
nil -> {:error, :not_found}
end
@@ -298,22 +339,49 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|> represent_service_actor(conn)
end

@doc "Returns the authenticated user's ActivityPub User object or a 404 Not Found if non-authenticated"
def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
conn
|> put_resp_content_type("application/activity+json")
|> json(UserView.render("user.json", %{user: user}))
|> put_view(UserView)
|> render("user.json", %{user: user})
end

def whoami(_conn, _params), do: {:error, :not_found}

def read_inbox(
%{assigns: %{user: %{nickname: nickname} = user}} = conn,
%{"nickname" => nickname} = params
) do
%{"nickname" => nickname, "page" => page?} = params
)
when page? in [true, "true"] do
activities =
if params["max_id"] do
ActivityPub.fetch_activities([user.ap_id | user.following], %{
"max_id" => params["max_id"],
"limit" => 10
})
else
ActivityPub.fetch_activities([user.ap_id | user.following], %{"limit" => 10})
end

conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("inbox.json", user: user, max_id: params["max_id"])
|> render("activity_collection_page.json", %{
activities: activities,
iri: "#{user.ap_id}/inbox"
})
end

def read_inbox(%{assigns: %{user: %{nickname: nickname} = user}} = conn, %{
"nickname" => nickname
}) do
with {:ok, user} <- User.ensure_keys_present(user) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
end
end

def read_inbox(%{assigns: %{user: nil}} = conn, %{"nickname" => nickname}) do
@@ -447,4 +515,31 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do

{new_user, for_user}
end

# TODO: Add support for "object" field
@doc """
Endpoint based on <https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload>

Parameters:
- (required) `file`: data of the media
- (optionnal) `description`: description of the media, intended for accessibility

Response:
- HTTP Code: 201 Created
- HTTP Body: ActivityPub object to be inserted into another's `attachment` field
"""
def upload_media(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
with {:ok, object} <-
ActivityPub.upload(
file,
actor: User.ap_id(user),
description: Map.get(data, "description")
) do
Logger.debug(inspect(object))

conn
|> put_status(:created)
|> json(object.data)
end
end
end

+ 2
- 2
lib/pleroma/web/activity_pub/publisher.ex Vedi File

@@ -111,11 +111,11 @@ defmodule Pleroma.Web.ActivityPub.Publisher do

@spec recipients(User.t(), Activity.t()) :: list(User.t()) | []
defp recipients(actor, activity) do
{:ok, followers} =
followers =
if actor.follower_address in activity.recipients do
User.get_external_followers(actor)
else
{:ok, []}
[]
end

fetchers =


+ 156
- 170
lib/pleroma/web/activity_pub/transmogrifier.ex Vedi File

@@ -42,8 +42,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end

def fix_summary(%{"summary" => nil} = object) do
object
|> Map.put("summary", "")
Map.put(object, "summary", "")
end

def fix_summary(%{"summary" => _} = object) do
@@ -51,10 +50,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
object
end

def fix_summary(object) do
object
|> Map.put("summary", "")
end
def fix_summary(object), do: Map.put(object, "summary", "")

def fix_addressing_list(map, field) do
cond do
@@ -74,13 +70,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
explicit_mentions,
follower_collection
) do
explicit_to =
to
|> Enum.filter(fn x -> x in explicit_mentions end)
explicit_to = Enum.filter(to, fn x -> x in explicit_mentions end)

explicit_cc =
to
|> Enum.filter(fn x -> x not in explicit_mentions end)
explicit_cc = Enum.filter(to, fn x -> x not in explicit_mentions end)

final_cc =
(cc ++ explicit_cc)
@@ -98,13 +90,19 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def fix_explicit_addressing(%{"directMessage" => true} = object), do: object

def fix_explicit_addressing(object) do
explicit_mentions =
object
|> Utils.determine_explicit_mentions()
explicit_mentions = Utils.determine_explicit_mentions(object)

follower_collection = User.get_cached_by_ap_id(Containment.get_actor(object)).follower_address
%User{follower_address: follower_collection} =
object
|> Containment.get_actor()
|> User.get_cached_by_ap_id()

explicit_mentions = explicit_mentions ++ [Pleroma.Constants.as_public(), follower_collection]
explicit_mentions =
explicit_mentions ++
[
Pleroma.Constants.as_public(),
follower_collection
]

fix_explicit_addressing(object, explicit_mentions, follower_collection)
end
@@ -148,48 +146,25 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end

def fix_actor(%{"attributedTo" => actor} = object) do
object
|> Map.put("actor", Containment.get_actor(%{"actor" => actor}))
Map.put(object, "actor", Containment.get_actor(%{"actor" => actor}))
end

def fix_in_reply_to(object, options \\ [])

def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
when not is_nil(in_reply_to) do
in_reply_to_id =
cond do
is_bitstring(in_reply_to) ->
in_reply_to

is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
in_reply_to["id"]

is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
Enum.at(in_reply_to, 0)

# Maybe I should output an error too?
true ->
""
end

in_reply_to_id = prepare_in_reply_to(in_reply_to)
object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)

if Federator.allowed_incoming_reply_depth?(options[:depth]) do
case get_obj_helper(in_reply_to_id, options) do
{:ok, replied_object} ->
with %Activity{} = _activity <-
Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
object
|> Map.put("inReplyTo", replied_object.data["id"])
|> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
|> Map.put("conversation", replied_object.data["context"] || object["conversation"])
|> Map.put("context", replied_object.data["context"] || object["conversation"])
else
e ->
Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
object
end

with {:ok, replied_object} <- get_obj_helper(in_reply_to_id, options),
%Activity{} = _ <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
object
|> Map.put("inReplyTo", replied_object.data["id"])
|> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
|> Map.put("conversation", replied_object.data["context"] || object["conversation"])
|> Map.put("context", replied_object.data["context"] || object["conversation"])
else
e ->
Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
object
@@ -201,6 +176,22 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do

def fix_in_reply_to(object, _options), do: object

defp prepare_in_reply_to(in_reply_to) do
cond do
is_bitstring(in_reply_to) ->
in_reply_to

is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
in_reply_to["id"]

is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
Enum.at(in_reply_to, 0)

true ->
""
end
end

def fix_context(object) do
context = object["context"] || object["conversation"] || Utils.generate_context_id()

@@ -211,11 +202,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do

def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
attachments =
attachment
|> Enum.map(fn data ->
Enum.map(attachment, fn data ->
media_type = data["mediaType"] || data["mimeType"]
href = data["url"] || data["href"]

url = [%{"type" => "Link", "mediaType" => media_type, "href" => href}]

data
@@ -223,30 +212,25 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> Map.put("url", url)
end)

object
|> Map.put("attachment", attachments)
Map.put(object, "attachment", attachments)
end

def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
Map.put(object, "attachment", [attachment])
object
|> Map.put("attachment", [attachment])
|> fix_attachments()
end

def fix_attachments(object), do: object

def fix_url(%{"url" => url} = object) when is_map(url) do
object
|> Map.put("url", url["href"])
Map.put(object, "url", url["href"])
end

def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do
first_element = Enum.at(url, 0)

link_element =
url
|> Enum.filter(fn x -> is_map(x) end)
|> Enum.filter(fn x -> x["mimeType"] == "text/html" end)
|> Enum.at(0)
link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end)

object
|> Map.put("attachment", [first_element])
@@ -264,36 +248,32 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
true -> ""
end

object
|> Map.put("url", url_string)
Map.put(object, "url", url_string)
end

def fix_url(object), do: object

def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
emoji = tags |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)

emoji =
emoji
tags
|> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
|> Enum.reduce(%{}, fn data, mapping ->
name = String.trim(data["name"], ":")

mapping |> Map.put(name, data["icon"]["url"])
Map.put(mapping, name, data["icon"]["url"])
end)

# we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
emoji = Map.merge(object["emoji"] || %{}, emoji)

object
|> Map.put("emoji", emoji)
Map.put(object, "emoji", emoji)
end

def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
name = String.trim(tag["name"], ":")
emoji = %{name => tag["icon"]["url"]}

object
|> Map.put("emoji", emoji)
Map.put(object, "emoji", emoji)
end

def fix_emoji(object), do: object
@@ -304,17 +284,13 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
|> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)

combined = tag ++ tags

object
|> Map.put("tag", combined)
Map.put(object, "tag", tag ++ tags)
end

def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
combined = [tag, String.slice(hashtag, 1..-1)]

object
|> Map.put("tag", combined)
Map.put(object, "tag", combined)
end

def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
@@ -326,8 +302,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
content_groups = Map.to_list(content_map)
{_, content} = Enum.at(content_groups, 0)

object
|> Map.put("content", content)
Map.put(object, "content", content)
end

def fix_content_map(object), do: object
@@ -336,16 +311,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do

def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
when is_binary(reply_id) do
reply =
with true <- Federator.allowed_incoming_reply_depth?(options[:depth]),
{:ok, object} <- get_obj_helper(reply_id, options) do
object
end

if reply && reply.data["type"] == "Question" do
with true <- Federator.allowed_incoming_reply_depth?(options[:depth]),
{:ok, %{data: %{"type" => "Question"} = _} = _} <- get_obj_helper(reply_id, options) do
Map.put(object, "type", "Answer")
else
object
_ -> object
end
end

@@ -377,6 +347,17 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
end

# Reduce the object list to find the reported user.
defp get_reported(objects) do
Enum.reduce_while(objects, nil, fn ap_id, _ ->
with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
{:halt, user}
else
_ -> {:cont, nil}
end
end)
end

def handle_incoming(data, options \\ [])

# Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
@@ -385,31 +366,19 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier 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),

%User{} = account <- get_reported(objects),
# Remove the reported user from the object list.
statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
params = %{
%{
actor: actor,
context: context,
account: account,
statuses: statuses,
content: content,
additional: %{
"cc" => [account.ap_id]
}
additional: %{"cc" => [account.ap_id]}
}

ActivityPub.flag(params)
|> ActivityPub.flag()
end
end

@@ -462,6 +431,36 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end

def handle_incoming(
%{"type" => "Listen", "object" => %{"type" => "Audio"} = object} = data,
options
) do
actor = Containment.get_actor(data)

data =
Map.put(data, "actor", actor)
|> fix_addressing

with {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do
options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
object = fix_object(object, options)

params = %{
to: data["to"],
object: object,
actor: user,
context: nil,
local: false,
published: data["published"],
additional: Map.take(data, ["cc", "id"])
}

ActivityPub.listen(params)
else
_e -> :error
end
end

def handle_incoming(
%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data,
_options
) do
@@ -756,8 +755,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do

def handle_incoming(_, _), do: :error

@spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
def get_obj_helper(id, options \\ []) do
if object = Object.normalize(id, true, options), do: {:ok, object}, else: nil
case Object.normalize(id, true, options) do
%Object{} = object -> {:ok, object}
_ -> nil
end
end

def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
@@ -792,7 +795,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
# internal -> Mastodon
# """

def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do
def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data)
when activity_type in ["Create", "Listen"] do
object =
object_id
|> Object.normalize()
@@ -856,27 +860,24 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
{:ok, data}
end

def maybe_fix_object_url(data) do
if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
case get_obj_helper(data["object"]) do
{:ok, relative_object} ->
if relative_object.data["external_url"] do
_data =
data
|> Map.put("object", relative_object.data["external_url"])
else
data
end

e ->
Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
data
end
def maybe_fix_object_url(%{"object" => object} = data) when is_binary(object) do
with false <- String.starts_with?(object, "http"),
{:fetch, {:ok, relative_object}} <- {:fetch, get_obj_helper(object)},
%{data: %{"external_url" => external_url}} when not is_nil(external_url) <-
relative_object do
Map.put(data, "object", external_url)
else
data
{:fetch, e} ->
Logger.error("Couldn't fetch #{object} #{inspect(e)}")
data

_ ->
data
end
end

def maybe_fix_object_url(data), do: data

def add_hashtags(object) do
tags =
(object["tag"] || [])
@@ -894,53 +895,49 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
tag
end)

object
|> Map.put("tag", tags)
Map.put(object, "tag", tags)
end

def add_mention_tags(object) do
mentions =
object
|> Utils.get_notified_from_object()
|> Enum.map(fn user ->
%{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
end)
|> Enum.map(&build_mention_tag/1)

tags = object["tag"] || []

object
|> Map.put("tag", tags ++ mentions)
Map.put(object, "tag", tags ++ mentions)
end

def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do
user_info = add_emoji_tags(user_info)
defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do
%{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
end

object
|> Map.put(:info, user_info)
def take_emoji_tags(%User{info: %{emoji: emoji} = _user_info} = _user) do
emoji
|> Enum.flat_map(&Map.to_list/1)
|> Enum.map(&build_emoji_tag/1)
end

# TODO: we should probably send mtime instead of unix epoch time for updated
def add_emoji_tags(%{"emoji" => emoji} = object) do
tags = object["tag"] || []

out =
emoji
|> Enum.map(fn {name, url} ->
%{
"icon" => %{"url" => url, "type" => "Image"},
"name" => ":" <> name <> ":",
"type" => "Emoji",
"updated" => "1970-01-01T00:00:00Z",
"id" => url
}
end)
out = Enum.map(emoji, &build_emoji_tag/1)

object
|> Map.put("tag", tags ++ out)
Map.put(object, "tag", tags ++ out)
end

def add_emoji_tags(object) do
object
def add_emoji_tags(object), do: object

defp build_emoji_tag({name, url}) do
%{
"icon" => %{"url" => url, "type" => "Image"},
"name" => ":" <> name <> ":",
"type" => "Emoji",
"updated" => "1970-01-01T00:00:00Z",
"id" => url
}
end

def set_conversation(object) do
@@ -960,9 +957,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do

def add_attributed_to(object) do
attributed_to = object["attributedTo"] || object["actor"]

object
|> Map.put("attributedTo", attributed_to)
Map.put(object, "attributedTo", attributed_to)
end

def prepare_attachments(object) do
@@ -973,8 +968,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
%{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
end)

object
|> Map.put("attachment", attachments)
Map.put(object, "attachment", attachments)
end

defp strip_internal_fields(object) do
@@ -983,12 +977,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end

defp strip_internal_tags(%{"tag" => tags} = object) do
tags =
tags
|> Enum.filter(fn x -> is_map(x) end)
tags = Enum.filter(tags, fn x -> is_map(x) end)

object
|> Map.put("tag", tags)
Map.put(object, "tag", tags)
end

defp strip_internal_tags(object), do: object
@@ -1073,16 +1064,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
end

def maybe_fix_user_url(data) do
if is_map(data["url"]) do
Map.put(data, "url", data["url"]["href"])
else
data
end
def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
Map.put(data, "url", url["href"])
end

def maybe_fix_user_object(data) do
data
|> maybe_fix_user_url
end
def maybe_fix_user_url(data), do: data

def maybe_fix_user_object(data), do: maybe_fix_user_url(data)
end

+ 16
- 1
lib/pleroma/web/activity_pub/utils.ex Vedi File

@@ -20,7 +20,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
require Logger
require Pleroma.Constants

@supported_object_types ["Article", "Note", "Video", "Page", "Question", "Answer"]
@supported_object_types ["Article", "Note", "Video", "Page", "Question", "Answer", "Audio"]
@supported_report_states ~w(open closed resolved)
@valid_visibilities ~w(public unlisted private direct)

@@ -581,6 +581,21 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|> Map.merge(additional)
end

#### Listen-related helpers
def make_listen_data(params, additional) do
published = params.published || make_date()

%{
"type" => "Listen",
"to" => params.to |> Enum.uniq(),
"actor" => params.actor.ap_id,
"object" => params.object,
"published" => published,
"context" => params.context
}
|> Map.merge(additional)
end

#### Flag-related helpers
@spec make_flag_data(map(), map()) :: map()
def make_flag_data(%{actor: actor, context: context, content: content} = params, additional) do


+ 4
- 3
lib/pleroma/web/activity_pub/views/object_view.ex Vedi File

@@ -15,7 +15,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
Map.merge(base, additional)
end

def render("object.json", %{object: %Activity{data: %{"type" => "Create"}} = activity}) do
def render("object.json", %{object: %Activity{data: %{"type" => activity_type}} = activity})
when activity_type in ["Create", "Listen"] do
base = Pleroma.Web.ActivityPub.Utils.make_json_ld_header()
object = Object.normalize(activity)

@@ -37,12 +38,12 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
Map.merge(base, additional)
end

def render("likes.json", ap_id, likes, page) do
def render("likes.json", %{ap_id: ap_id, likes: likes, page: page}) do
collection(likes, "#{ap_id}/likes", page)
|> Map.merge(Pleroma.Web.ActivityPub.Utils.make_json_ld_header())
end

def render("likes.json", ap_id, likes) do
def render("likes.json", %{ap_id: ap_id, likes: likes}) do
%{
"id" => "#{ap_id}/likes",
"type" => "OrderedCollection",


+ 19
- 81
lib/pleroma/web/activity_pub/views/user_view.ex Vedi File

@@ -8,7 +8,6 @@ defmodule Pleroma.Web.ActivityPub.UserView do
alias Pleroma.Keys
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Endpoint
@@ -25,7 +24,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"oauthAuthorizationEndpoint" => Helpers.o_auth_url(Endpoint, :authorize),
"oauthRegistrationEndpoint" => Helpers.mastodon_api_url(Endpoint, :create_app),
"oauthTokenEndpoint" => Helpers.o_auth_url(Endpoint, :token_exchange),
"sharedInbox" => Helpers.activity_pub_url(Endpoint, :inbox)
"sharedInbox" => Helpers.activity_pub_url(Endpoint, :inbox),
"uploadMedia" => Helpers.activity_pub_url(Endpoint, :upload_media)
}
end

@@ -75,10 +75,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do

endpoints = render("endpoints.json", %{user: user})

user_tags =
user
|> Transmogrifier.add_emoji_tags()
|> Map.get("tag", [])
emoji_tags = Transmogrifier.take_emoji_tags(user)

fields =
user.info
@@ -110,7 +107,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do
},
"endpoints" => endpoints,
"attachment" => fields,
"tag" => (user.info.source_data["tag"] || []) ++ user_tags
"tag" => (user.info.source_data["tag"] || []) ++ emoji_tags,
"discoverable" => user.info.discoverable
}
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
|> Map.merge(maybe_make_image(&User.banner_url/2, "image", user))
@@ -213,25 +211,22 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|> Map.merge(Utils.make_json_ld_header())
end

def render("outbox.json", %{user: user, max_id: max_qid}) do
params = %{
"limit" => "10"
def render("activity_collection.json", %{iri: iri}) do
%{
"id" => iri,
"type" => "OrderedCollection",
"first" => "#{iri}?page=true"
}
|> Map.merge(Utils.make_json_ld_header())
end

params =
if max_qid != nil do
Map.put(params, "max_id", max_qid)
else
params
end

activities = ActivityPub.fetch_user_activities(user, nil, params)

def render("activity_collection_page.json", %{activities: activities, iri: iri}) do
# this is sorted chronologically, so first activity is the newest (max)
{max_id, min_id, collection} =
if length(activities) > 0 do
{
Enum.at(Enum.reverse(activities), 0).id,
Enum.at(activities, 0).id,
Enum.at(Enum.reverse(activities), 0).id,
Enum.map(activities, fn act ->
{:ok, data} = Transmogrifier.prepare_outgoing(act.data)
data
@@ -245,71 +240,14 @@ defmodule Pleroma.Web.ActivityPub.UserView do
}
end

iri = "#{user.ap_id}/outbox"

page = %{
"id" => "#{iri}?max_id=#{max_id}",
"type" => "OrderedCollectionPage",
"partOf" => iri,
"orderedItems" => collection,
"next" => "#{iri}?max_id=#{min_id}"
}

if max_qid == nil do
%{
"id" => iri,
"type" => "OrderedCollection",
"first" => page
}
|> Map.merge(Utils.make_json_ld_header())
else
page |> Map.merge(Utils.make_json_ld_header())
end
end

def render("inbox.json", %{user: user, max_id: max_qid}) do
params = %{
"limit" => "10"
}

params =
if max_qid != nil do
Map.put(params, "max_id", max_qid)
else
params
end

activities = ActivityPub.fetch_activities([user.ap_id | user.following], params)

min_id = Enum.at(Enum.reverse(activities), 0).id
max_id = Enum.at(activities, 0).id

collection =
Enum.map(activities, fn act ->
{:ok, data} = Transmogrifier.prepare_outgoing(act.data)
data
end)

iri = "#{user.ap_id}/inbox"

page = %{
"id" => "#{iri}?max_id=#{max_id}",
%{
"id" => "#{iri}?max_id=#{max_id}&page=true",
"type" => "OrderedCollectionPage",
"partOf" => iri,
"orderedItems" => collection,
"next" => "#{iri}?max_id=#{min_id}"
"next" => "#{iri}?max_id=#{min_id}&page=true"
}

if max_qid == nil do
%{
"id" => iri,
"type" => "OrderedCollection",
"first" => page
}
|> Map.merge(Utils.make_json_ld_header())
else
page |> Map.merge(Utils.make_json_ld_header())
end
|> Map.merge(Utils.make_json_ld_header())
end

def collection(collection, iri, page, show_items \\ true, total \\ nil) do


+ 72
- 49
lib/pleroma/web/admin_api/admin_api_controller.ex Vedi File

@@ -15,10 +15,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.Web.AdminAPI.Config
alias Pleroma.Web.AdminAPI.ConfigView
alias Pleroma.Web.AdminAPI.ModerationLogView
alias Pleroma.Web.AdminAPI.Report
alias Pleroma.Web.AdminAPI.ReportView
alias Pleroma.Web.AdminAPI.Search
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Endpoint
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.Router

import Pleroma.Web.ControllerHelper, only: [json_response: 3]

@@ -201,7 +204,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
def user_show(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
conn
|> json(AccountView.render("show.json", %{user: user}))
|> put_view(AccountView)
|> render("show.json", %{user: user})
else
_ -> {:error, :not_found}
end
@@ -220,7 +224,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
})

conn
|> json(StatusView.render("index.json", %{activities: activities, as: :activity}))
|> put_view(StatusView)
|> render("index.json", %{activities: activities, as: :activity})
else
_ -> {:error, :not_found}
end
@@ -240,7 +245,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
})

conn
|> json(AccountView.render("show.json", %{user: updated_user}))
|> put_view(AccountView)
|> render("show.json", %{user: updated_user})
end

def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
@@ -312,18 +318,12 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
"nickname" => nickname
})
when permission_group in ["moderator", "admin"] do
user = User.get_cached_by_nickname(nickname)

info =
%{}
|> Map.put("is_" <> permission_group, true)
info = Map.put(%{}, "is_" <> permission_group, true)

info_cng = User.Info.admin_api_update(user.info, info)

cng =
user
|> Ecto.Changeset.change()
|> Ecto.Changeset.put_embed(:info, info_cng)
{:ok, user} =
nickname
|> User.get_cached_by_nickname()
|> User.update_info(&User.Info.admin_api_update(&1, info))

ModerationLog.insert_log(%{
action: "grant",
@@ -332,8 +332,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
permission: permission_group
})

{:ok, _user} = User.update_and_set_cache(cng)

json(conn, info)
end

@@ -351,40 +349,33 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
})
end

def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
render_error(conn, :forbidden, "You can't revoke your own admin status.")
end

def right_delete(
%{assigns: %{user: %User{:nickname => admin_nickname} = admin}} = conn,
%{assigns: %{user: admin}} = conn,
%{
"permission_group" => permission_group,
"nickname" => nickname
}
)
when permission_group in ["moderator", "admin"] do
if admin_nickname == nickname do
render_error(conn, :forbidden, "You can't revoke your own admin status.")
else
user = User.get_cached_by_nickname(nickname)

info =
%{}
|> Map.put("is_" <> permission_group, false)
info = Map.put(%{}, "is_" <> permission_group, false)

info_cng = User.Info.admin_api_update(user.info, info)
{:ok, user} =
nickname
|> User.get_cached_by_nickname()
|> User.update_info(&User.Info.admin_api_update(&1, info))

cng =
Ecto.Changeset.change(user)
|> Ecto.Changeset.put_embed(:info, info_cng)

{:ok, _user} = User.update_and_set_cache(cng)

ModerationLog.insert_log(%{
action: "revoke",
actor: admin,
subject: user,
permission: permission_group
})
ModerationLog.insert_log(%{
action: "revoke",
actor: admin,
subject: user,
permission: permission_group
})

json(conn, info)
end
json(conn, info)
end

def right_delete(conn, _) do
@@ -486,7 +477,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
invites = UserInviteToken.list_invites()

conn
|> json(AccountView.render("invites.json", %{invites: invites}))
|> put_view(AccountView)
|> render("invites.json", %{invites: invites})
end

@doc "Revokes invite by token"
@@ -494,7 +486,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
with {:ok, invite} <- UserInviteToken.find_by_token(token),
{:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
conn
|> json(AccountView.render("invite.json", %{invite: updated_invite}))
|> put_view(AccountView)
|> render("invite.json", %{invite: updated_invite})
else
nil -> {:error, :not_found}
end
@@ -506,17 +499,33 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
{:ok, token} = Pleroma.PasswordResetToken.create_token(user)

conn
|> json(token.token)
|> json(%{
token: token.token,
link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
})
end

@doc "Force password reset for a given user"
def force_password_reset(conn, %{"nickname" => nickname}) do
(%User{local: true} = user) = User.get_cached_by_nickname(nickname)

User.force_password_reset_async(user)

json_response(conn, :no_content, "")
end

def list_reports(conn, params) do
{page, page_size} = page_params(params)

params =
params
|> Map.put("type", "Flag")
|> Map.put("skip_preload", true)
|> Map.put("total", true)
|> Map.put("limit", page_size)
|> Map.put("offset", (page - 1) * page_size)

reports = ActivityPub.fetch_activities([], params)
reports = ActivityPub.fetch_activities([], params, :offset)

conn
|> put_view(ReportView)
@@ -527,7 +536,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
with %Activity{} = report <- Activity.get_by_id(id) do
conn
|> put_view(ReportView)
|> render("show.json", %{report: report})
|> render("show.json", Report.extract_report_info(report))
else
_ -> {:error, :not_found}
end
@@ -543,7 +552,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do

conn
|> put_view(ReportView)
|> render("show.json", %{report: report})
|> render("show.json", Report.extract_report_info(report))
end
end

@@ -566,7 +575,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do

conn
|> put_view(StatusView)
|> render("status.json", %{activity: activity})
|> render("show.json", %{activity: activity})
else
true ->
{:param_cast, nil}
@@ -590,7 +599,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do

conn
|> put_view(StatusView)
|> render("status.json", %{activity: activity})
|> render("show.json", %{activity: activity})
end
end

@@ -609,7 +618,15 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
def list_log(conn, params) do
{page, page_size} = page_params(params)

log = ModerationLog.get_all(page, page_size)
log =
ModerationLog.get_all(%{
page: page,
page_size: page_size,
start_date: params["start_date"],
end_date: params["end_date"],
user_id: params["user_id"],
search: params["search"]
})

conn
|> put_view(ModerationLogView)
@@ -661,6 +678,12 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|> render("index.json", %{configs: updated})
end

def reload_emoji(conn, _params) do
Pleroma.Emoji.reload()

conn |> json("ok")
end

def errors(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)


+ 22
- 0
lib/pleroma/web/admin_api/report.ex Vedi File

@@ -0,0 +1,22 @@
# 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.Report do
alias Pleroma.Activity
alias Pleroma.User

def extract_report_info(
%{data: %{"actor" => actor, "object" => [account_ap_id | status_ap_ids]}} = report
) do
user = User.get_cached_by_ap_id(actor)
account = User.get_cached_by_ap_id(account_ap_id)

statuses =
Enum.map(status_ap_ids, fn ap_id ->
Activity.get_by_ap_id_with_object(ap_id)
end)

%{report: report, user: user, account: account, statuses: statuses}
end
end

+ 4
- 1
lib/pleroma/web/admin_api/views/moderation_log_view.ex Vedi File

@@ -8,7 +8,10 @@ defmodule Pleroma.Web.AdminAPI.ModerationLogView do
alias Pleroma.ModerationLog

def render("index.json", %{log: log}) do
render_many(log, __MODULE__, "show.json", as: :log_entry)
%{
items: render_many(log.items, __MODULE__, "show.json", as: :log_entry),
total: log.count
}
end

def render("show.json", %{log_entry: log_entry}) do


+ 7
- 13
lib/pleroma/web/admin_api/views/report_view.ex Vedi File

@@ -4,27 +4,26 @@

defmodule Pleroma.Web.AdminAPI.ReportView do
use Pleroma.Web, :view
alias Pleroma.Activity
alias Pleroma.HTML
alias Pleroma.User
alias Pleroma.Web.AdminAPI.Report
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.StatusView

def render("index.json", %{reports: reports}) do
%{
reports:
render_many(reports[:items], __MODULE__, "show.json", as: :report) |> Enum.reverse(),
reports[:items]
|> Enum.map(&Report.extract_report_info(&1))
|> Enum.map(&render(__MODULE__, "show.json", &1))
|> Enum.reverse(),
total: reports[:total]
}
end

def render("show.json", %{report: report}) do
user = User.get_cached_by_ap_id(report.data["actor"])
def render("show.json", %{report: report, user: user, account: account, statuses: statuses}) do
created_at = Utils.to_masto_date(report.data["published"])

[account_ap_id | status_ap_ids] = report.data["object"]
account = User.get_cached_by_ap_id(account_ap_id)

content =
unless is_nil(report.data["content"]) do
HTML.filter_tags(report.data["content"])
@@ -32,11 +31,6 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
nil
end

statuses =
Enum.map(status_ap_ids, fn ap_id ->
Activity.get_by_ap_id_with_object(ap_id)
end)

%{
id: report.id,
account: merge_account_views(account),
@@ -49,7 +43,7 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
end

defp merge_account_views(%User{} = user) do
Pleroma.Web.MastodonAPI.AccountView.render("account.json", %{user: user})
Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user})
|> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user}))
end



+ 1
- 1
lib/pleroma/web/chat_channel.ex Vedi File

@@ -22,7 +22,7 @@ defmodule Pleroma.Web.ChatChannel do

if String.length(text) > 0 do
author = User.get_cached_by_nickname(user_name)
author = Pleroma.Web.MastodonAPI.AccountView.render("account.json", user: author)
author = Pleroma.Web.MastodonAPI.AccountView.render("show.json", user: author)
message = ChatChannelState.add_message(%{text: text, author: author})

broadcast!(socket, "new_msg", message)


+ 219
- 0
lib/pleroma/web/common_api/activity_draft.ex Vedi File

@@ -0,0 +1,219 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.CommonAPI.ActivityDraft do
alias Pleroma.Activity
alias Pleroma.Conversation.Participation
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils

import Pleroma.Web.Gettext

defstruct valid?: true,
errors: [],
user: nil,
params: %{},
status: nil,
summary: nil,
full_payload: nil,
attachments: [],
in_reply_to: nil,
in_reply_to_conversation: nil,
visibility: nil,
expires_at: nil,
poll: nil,
emoji: %{},
content_html: nil,
mentions: [],
tags: [],
to: [],
cc: [],
context: nil,
sensitive: false,
object: nil,
preview?: false,
changes: %{}

def create(user, params) do
%__MODULE__{user: user}
|> put_params(params)
|> status()
|> summary()
|> with_valid(&attachments/1)
|> full_payload()
|> expires_at()
|> poll()
|> with_valid(&in_reply_to/1)
|> with_valid(&in_reply_to_conversation/1)
|> with_valid(&visibility/1)
|> content()
|> with_valid(&to_and_cc/1)
|> with_valid(&context/1)
|> sensitive()
|> with_valid(&object/1)
|> preview?()
|> with_valid(&changes/1)
|> validate()
end

defp put_params(draft, params) do
params = Map.put_new(params, "in_reply_to_status_id", params["in_reply_to_id"])
%__MODULE__{draft | params: params}
end

defp status(%{params: %{"status" => status}} = draft) do
%__MODULE__{draft | status: String.trim(status)}
end

defp summary(%{params: params} = draft) do
%__MODULE__{draft | summary: Map.get(params, "spoiler_text", "")}
end

defp full_payload(%{status: status, summary: summary} = draft) do
full_payload = String.trim(status <> summary)

case Utils.validate_character_limit(full_payload, draft.attachments) do
:ok -> %__MODULE__{draft | full_payload: full_payload}
{:error, message} -> add_error(draft, message)
end
end

defp attachments(%{params: params} = draft) do
attachments = Utils.attachments_from_ids(params)
%__MODULE__{draft | attachments: attachments}
end

defp in_reply_to(draft) do
case Map.get(draft.params, "in_reply_to_status_id") do
"" -> draft
nil -> draft
id -> %__MODULE__{draft | in_reply_to: Activity.get_by_id(id)}
end
end

defp in_reply_to_conversation(draft) do
in_reply_to_conversation = Participation.get(draft.params["in_reply_to_conversation_id"])
%__MODULE__{draft | in_reply_to_conversation: in_reply_to_conversation}
end

defp visibility(%{params: params} = draft) do
case CommonAPI.get_visibility(params, draft.in_reply_to, draft.in_reply_to_conversation) do
{visibility, "direct"} when visibility != "direct" ->
add_error(draft, dgettext("errors", "The message visibility must be direct"))

{visibility, _} ->
%__MODULE__{draft | visibility: visibility}
end
end

defp expires_at(draft) do
case CommonAPI.check_expiry_date(draft.params["expires_in"]) do
{:ok, expires_at} -> %__MODULE__{draft | expires_at: expires_at}
{:error, message} -> add_error(draft, message)
end
end

defp poll(draft) do
case Utils.make_poll_data(draft.params) do
{:ok, {poll, poll_emoji}} ->
%__MODULE__{draft | poll: poll, emoji: Map.merge(draft.emoji, poll_emoji)}

{:error, message} ->
add_error(draft, message)
end
end

defp content(draft) do
{content_html, mentions, tags} =
Utils.make_content_html(
draft.status,
draft.attachments,
draft.params,
draft.visibility
)

%__MODULE__{draft | content_html: content_html, mentions: mentions, tags: tags}
end

defp to_and_cc(draft) do
addressed_users =
draft.mentions
|> Enum.map(fn {_, mentioned_user} -> mentioned_user.ap_id end)
|> Utils.get_addressed_users(draft.params["to"])

{to, cc} =
Utils.get_to_and_cc(
draft.user,
addressed_users,
draft.in_reply_to,
draft.visibility,
draft.in_reply_to_conversation
)

%__MODULE__{draft | to: to, cc: cc}
end

defp context(draft) do
context = Utils.make_context(draft.in_reply_to, draft.in_reply_to_conversation)
%__MODULE__{draft | context: context}
end

defp sensitive(draft) do
sensitive = draft.params["sensitive"] || Enum.member?(draft.tags, {"#nsfw", "nsfw"})
%__MODULE__{draft | sensitive: sensitive}
end

defp object(draft) do
emoji = Map.merge(Pleroma.Emoji.Formatter.get_emoji_map(draft.full_payload), draft.emoji)

object =
Utils.make_note_data(
draft.user.ap_id,
draft.to,
draft.context,
draft.content_html,
draft.attachments,
draft.in_reply_to,
draft.tags,
draft.summary,
draft.cc,
draft.sensitive,
draft.poll
)
|> Map.put("emoji", emoji)

%__MODULE__{draft | object: object}
end

defp preview?(draft) do
preview? = Pleroma.Web.ControllerHelper.truthy_param?(draft.params["preview"]) || false
%__MODULE__{draft | preview?: preview?}
end

defp changes(draft) do
direct? = draft.visibility == "direct"

changes =
%{
to: draft.to,
actor: draft.user,
context: draft.context,
object: draft.object,
additional: %{"cc" => draft.cc, "directMessage" => direct?}
}
|> Utils.maybe_add_list_data(draft.user, draft.visibility)

%__MODULE__{draft | changes: changes}
end

defp with_valid(%{valid?: true} = draft, func), do: func.(draft)
defp with_valid(draft, _func), do: draft

defp add_error(draft, message) do
%__MODULE__{draft | valid?: false, errors: [message | draft.errors]}
end

defp validate(%{valid?: true} = draft), do: {:ok, draft}
defp validate(%{errors: [message | _]}), do: {:error, message}
end

+ 120
- 206
lib/pleroma/web/common_api/common_api.ex Vedi File

@@ -6,7 +6,6 @@ defmodule Pleroma.Web.CommonAPI do
alias Pleroma.Activity
alias Pleroma.ActivityExpiration
alias Pleroma.Conversation.Participation
alias Pleroma.Formatter
alias Pleroma.Object
alias Pleroma.ThreadMute
alias Pleroma.User
@@ -18,14 +17,11 @@ defmodule Pleroma.Web.CommonAPI do
import Pleroma.Web.CommonAPI.Utils

def follow(follower, followed) do
timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])

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} <- User.wait_and_refresh(timeout, follower, followed) do
{:ok, follower, followed, activity}
end
end
@@ -76,8 +72,7 @@ defmodule Pleroma.Web.CommonAPI do
{:ok, delete} <- ActivityPub.delete(object) do
{:ok, delete}
else
_ ->
{:error, dgettext("errors", "Could not delete")}
_ -> {:error, dgettext("errors", "Could not delete")}
end
end

@@ -87,18 +82,16 @@ defmodule Pleroma.Web.CommonAPI do
nil <- Utils.get_existing_announce(user.ap_id, object) do
ActivityPub.announce(user, object)
else
_ ->
{:error, dgettext("errors", "Could not repeat")}
_ -> {:error, dgettext("errors", "Could not repeat")}
end
end

def unrepeat(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
object <- Object.normalize(activity) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id) do
object = Object.normalize(activity)
ActivityPub.unannounce(user, object)
else
_ ->
{:error, dgettext("errors", "Could not unrepeat")}
_ -> {:error, dgettext("errors", "Could not unrepeat")}
end
end

@@ -108,30 +101,23 @@ defmodule Pleroma.Web.CommonAPI do
nil <- Utils.get_existing_like(user.ap_id, object) do
ActivityPub.like(user, object)
else
_ ->
{:error, dgettext("errors", "Could not favorite")}
_ -> {:error, dgettext("errors", "Could not favorite")}
end
end

def unfavorite(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
object <- Object.normalize(activity) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id) do
object = Object.normalize(activity)
ActivityPub.unlike(user, object)
else
_ ->
{:error, dgettext("errors", "Could not unfavorite")}
_ -> {:error, dgettext("errors", "Could not unfavorite")}
end
end

def vote(user, object, choices) do
with "Question" <- object.data["type"],
{:author, false} <- {:author, object.data["actor"] == user.ap_id},
{:existing_votes, []} <- {:existing_votes, Utils.get_existing_votes(user.ap_id, object)},
{options, max_count} <- get_options_and_max_count(object),
option_count <- Enum.count(options),
{:choice_check, {choices, true}} <-
{:choice_check, normalize_and_validate_choice_indices(choices, option_count)},
{:count_check, true} <- {:count_check, Enum.count(choices) <= max_count} do
def vote(user, %{data: %{"type" => "Question"}} = object, choices) do
with :ok <- validate_not_author(object, user),
:ok <- validate_existing_votes(user, object),
{:ok, options, choices} <- normalize_and_validate_choices(choices, object) do
answer_activities =
Enum.map(choices, fn index ->
answer_data = make_answer_data(user, object, Enum.at(options, index)["name"])
@@ -150,33 +136,41 @@ defmodule Pleroma.Web.CommonAPI do

object = Object.get_cached_by_ap_id(object.data["id"])
{:ok, answer_activities, object}
else
{:author, _} -> {:error, dgettext("errors", "Poll's author can't vote")}
{:existing_votes, _} -> {:error, dgettext("errors", "Already voted")}
{:choice_check, {_, false}} -> {:error, dgettext("errors", "Invalid indices")}
{:count_check, false} -> {:error, dgettext("errors", "Too many choices")}
end
end

defp get_options_and_max_count(object) do
if Map.has_key?(object.data, "anyOf") do
{object.data["anyOf"], Enum.count(object.data["anyOf"])}
defp validate_not_author(%{data: %{"actor" => ap_id}}, %{ap_id: ap_id}),
do: {:error, dgettext("errors", "Poll's author can't vote")}

defp validate_not_author(_, _), do: :ok

defp validate_existing_votes(%{ap_id: ap_id}, object) do
if Utils.get_existing_votes(ap_id, object) == [] do
:ok
else
{object.data["oneOf"], 1}
{:error, dgettext("errors", "Already voted")}
end
end

defp normalize_and_validate_choice_indices(choices, count) do
Enum.map_reduce(choices, true, fn index, valid ->
index = if is_binary(index), do: String.to_integer(index), else: index
{index, if(valid, do: index < count, else: valid)}
end)
end
defp get_options_and_max_count(%{data: %{"anyOf" => any_of}}), do: {any_of, Enum.count(any_of)}
defp get_options_and_max_count(%{data: %{"oneOf" => one_of}}), do: {one_of, 1}

defp normalize_and_validate_choices(choices, object) do
choices = Enum.map(choices, fn i -> if is_binary(i), do: String.to_integer(i), else: i end)
{options, max_count} = get_options_and_max_count(object)
count = Enum.count(options)

def get_visibility(_, _, %Participation{}) do
{"direct", "direct"}
with {_, true} <- {:valid_choice, Enum.all?(choices, &(&1 < count))},
{_, true} <- {:count_check, Enum.count(choices) <= max_count} do
{:ok, options, choices}
else
{:valid_choice, _} -> {:error, dgettext("errors", "Invalid indices")}
{:count_check, _} -> {:error, dgettext("errors", "Too many choices")}
end
end

def get_visibility(_, _, %Participation{}), do: {"direct", "direct"}

def get_visibility(%{"visibility" => visibility}, in_reply_to, _)
when visibility in ~w{public unlisted private direct},
do: {visibility, get_replied_to_visibility(in_reply_to)}
@@ -197,13 +191,13 @@ defmodule Pleroma.Web.CommonAPI do

def get_replied_to_visibility(activity) do
with %Object{} = object <- Object.normalize(activity) do
Pleroma.Web.ActivityPub.Visibility.get_visibility(object)
Visibility.get_visibility(object)
end
end

defp check_expiry_date({:ok, nil} = res), do: res
def check_expiry_date({:ok, nil} = res), do: res

defp check_expiry_date({:ok, in_seconds}) do
def check_expiry_date({:ok, in_seconds}) do
expiry = NaiveDateTime.utc_now() |> NaiveDateTime.add(in_seconds)

if ActivityExpiration.expires_late_enough?(expiry) do
@@ -213,105 +207,57 @@ defmodule Pleroma.Web.CommonAPI do
end
end

defp check_expiry_date(expiry_str) do
def check_expiry_date(expiry_str) do
Ecto.Type.cast(:integer, expiry_str)
|> check_expiry_date()
end

def post(user, %{"status" => status} = data) do
limit = Pleroma.Config.get([:instance, :limit])

with status <- String.trim(status),
attachments <- attachments_from_ids(data),
in_reply_to <- get_replied_to_activity(data["in_reply_to_status_id"]),
in_reply_to_conversation <- Participation.get(data["in_reply_to_conversation_id"]),
{visibility, in_reply_to_visibility} <-
get_visibility(data, in_reply_to, in_reply_to_conversation),
{_, false} <-
{:private_to_public, in_reply_to_visibility == "direct" && visibility != "direct"},
{content_html, mentions, tags} <-
make_content_html(
status,
attachments,
data,
visibility
),
mentioned_users <- for({_, mentioned_user} <- mentions, do: mentioned_user.ap_id),
addressed_users <- get_addressed_users(mentioned_users, data["to"]),
{poll, poll_emoji} <- make_poll_data(data),
{to, cc} <-
get_to_and_cc(user, addressed_users, in_reply_to, visibility, in_reply_to_conversation),
context <- make_context(in_reply_to, in_reply_to_conversation),
cw <- data["spoiler_text"] || "",
sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}),
{:ok, expires_at} <- check_expiry_date(data["expires_in"]),
full_payload <- String.trim(status <> cw),
:ok <- validate_character_limit(full_payload, attachments, limit),
object <-
make_note_data(
user.ap_id,
to,
context,
content_html,
attachments,
in_reply_to,
tags,
cw,
cc,
sensitive,
poll
),
object <-
Map.put(
object,
"emoji",
Map.merge(Formatter.get_emoji_map(full_payload), poll_emoji)
) do
preview? = Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false
direct? = visibility == "direct"

result =
%{
to: to,
actor: user,
context: context,
object: object,
additional: %{"cc" => cc, "directMessage" => direct?}
}
|> maybe_add_list_data(user, visibility)
|> ActivityPub.create(preview?)

if expires_at do
with {:ok, activity} <- result do
{:ok, _} = ActivityExpiration.create(activity, expires_at)
end
end

result
else
{:private_to_public, true} ->
{:error, dgettext("errors", "The message visibility must be direct")}
def listen(user, %{"title" => _} = data) do
with visibility <- data["visibility"] || "public",
{to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
listen_data <-
Map.take(data, ["album", "artist", "title", "length"])
|> Map.put("type", "Audio")
|> Map.put("to", to)
|> Map.put("cc", cc)
|> Map.put("actor", user.ap_id),
{:ok, activity} <-
ActivityPub.listen(%{
actor: user,
to: to,
object: listen_data,
context: Utils.generate_context_id(),
additional: %{"cc" => cc}
}) do
{:ok, activity}
end
end

{:error, _} = e ->
e
def post(user, %{"status" => _} = data) do
with {:ok, draft} <- Pleroma.Web.CommonAPI.ActivityDraft.create(user, data) do
draft.changes
|> ActivityPub.create(draft.preview?)
|> maybe_create_activity_expiration(draft.expires_at)
end
end

e ->
{:error, e}
defp maybe_create_activity_expiration({:ok, activity}, %NaiveDateTime{} = expires_at) do
with {:ok, _} <- ActivityExpiration.create(activity, expires_at) do
{:ok, activity}
end
end

defp maybe_create_activity_expiration(result, _), do: result

# Updates the emojis for a user based on their profile
def update(user) do
emoji = emoji_from_profile(user)
source_data = user.info |> Map.get(:source_data, %{}) |> Map.put("tag", emoji)

user =
with emoji <- emoji_from_profile(user),
source_data <- (user.info.source_data || %{}) |> Map.put("tag", emoji),
info_cng <- User.Info.set_source_data(user.info, source_data),
change <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
{:ok, user} <- User.update_and_set_cache(change) do
user
else
_e ->
user
case User.update_info(user, &User.Info.set_source_data(&1, source_data)) do
{:ok, user} -> user
_ -> user
end

ActivityPub.update(%{
@@ -326,44 +272,25 @@ defmodule Pleroma.Web.CommonAPI do
def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
with %Activity{
actor: ^user_ap_id,
data: %{
"type" => "Create"
},
object: %Object{
data: %{
"type" => "Note"
}
}
data: %{"type" => "Create"},
object: %Object{data: %{"type" => "Note"}}
} = activity <- get_by_id_or_ap_id(id_or_ap_id),
true <- Visibility.is_public?(activity),
%{valid?: true} = info_changeset <- User.Info.add_pinnned_activity(user.info, activity),
changeset <-
Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset),
{:ok, _user} <- User.update_and_set_cache(changeset) do
{:ok, _user} <- User.update_info(user, &User.Info.add_pinnned_activity(&1, activity)) do
{:ok, activity}
else
%{errors: [pinned_activities: {err, _}]} ->
{:error, err}

_ ->
{:error, dgettext("errors", "Could not pin")}
{:error, %{changes: %{info: %{errors: [pinned_activities: {err, _}]}}}} -> {:error, err}
_ -> {:error, dgettext("errors", "Could not pin")}
end
end

def unpin(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
%{valid?: true} = info_changeset <-
User.Info.remove_pinnned_activity(user.info, activity),
changeset <-
Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset),
{:ok, _user} <- User.update_and_set_cache(changeset) do
{:ok, _user} <- User.update_info(user, &User.Info.remove_pinnned_activity(&1, activity)) do
{:ok, activity}
else
%{errors: [pinned_activities: {err, _}]} ->
{:error, err}

_ ->
{:error, dgettext("errors", "Could not unpin")}
%{errors: [pinned_activities: {err, _}]} -> {:error, err}
_ -> {:error, dgettext("errors", "Could not unpin")}
end
end

@@ -383,51 +310,46 @@ defmodule Pleroma.Web.CommonAPI do
def thread_muted?(%{id: nil} = _user, _activity), do: false

def thread_muted?(user, activity) do
with [] <- ThreadMute.check_muted(user.id, activity.data["context"]) do
false
else
_ -> true
end
ThreadMute.check_muted(user.id, activity.data["context"]) != []
end

def report(user, data) do
with {:account_id, %{"account_id" => account_id}} <- {:account_id, data},
{:account, %User{} = account} <- {:account, User.get_cached_by_id(account_id)},
def report(user, %{"account_id" => account_id} = data) do
with {:ok, account} <- get_reported_account(account_id),
{:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
{:ok, statuses} <- get_report_statuses(account, data),
{:ok, activity} <-
ActivityPub.flag(%{
context: Utils.generate_context_id(),
actor: user,
account: account,
statuses: statuses,
content: content_html,
forward: data["forward"] || false
}) do
{:ok, activity}
else
{:error, err} -> {:error, err}
{:account_id, %{}} -> {:error, dgettext("errors", "Valid `account_id` required")}
{:account, nil} -> {:error, dgettext("errors", "Account not found")}
{:ok, statuses} <- get_report_statuses(account, data) do
ActivityPub.flag(%{
context: Utils.generate_context_id(),
actor: user,
account: account,
statuses: statuses,
content: content_html,
forward: data["forward"] || false
})
end
end

def report(_user, _params), do: {:error, dgettext("errors", "Valid `account_id` required")}

defp get_reported_account(account_id) do
case User.get_cached_by_id(account_id) do
%User{} = account -> {:ok, account}
_ -> {:error, dgettext("errors", "Account not found")}
end
end

def update_report_state(activity_id, state) do
with %Activity{} = activity <- Activity.get_by_id(activity_id),
{:ok, activity} <- Utils.update_report_state(activity, state) do
{:ok, activity}
with %Activity{} = activity <- Activity.get_by_id(activity_id) do
Utils.update_report_state(activity, state)
else
nil -> {:error, :not_found}
{:error, reason} -> {:error, reason}
_ -> {:error, dgettext("errors", "Could not update state")}
end
end

def update_activity_scope(activity_id, opts \\ %{}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
{:ok, activity} <- toggle_sensitive(activity, opts),
{:ok, activity} <- set_visibility(activity, opts) do
{:ok, activity}
{:ok, activity} <- toggle_sensitive(activity, opts) do
set_visibility(activity, opts)
else
nil -> {:error, :not_found}
{:error, reason} -> {:error, reason}
@@ -458,23 +380,15 @@ defmodule Pleroma.Web.CommonAPI do

defp set_visibility(activity, _), do: {:ok, activity}

def hide_reblogs(user, muted) do
ap_id = muted.ap_id

def hide_reblogs(user, %{ap_id: ap_id} = _muted) do
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)
User.update_info(user, &User.Info.add_reblog_mute(&1, ap_id))
end
end

def show_reblogs(user, muted) do
ap_id = muted.ap_id

def show_reblogs(user, %{ap_id: ap_id} = _muted) do
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)
User.update_info(user, &User.Info.remove_reblog_mute(&1, ap_id))
end
end
end

+ 71
- 70
lib/pleroma/web/common_api/utils.ex Vedi File

@@ -4,11 +4,13 @@

defmodule Pleroma.Web.CommonAPI.Utils do
import Pleroma.Web.Gettext
import Pleroma.Web.ControllerHelper, only: [truthy_param?: 1]

alias Calendar.Strftime
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Conversation.Participation
alias Pleroma.Emoji
alias Pleroma.Formatter
alias Pleroma.Object
alias Pleroma.Plugs.AuthenticationPlug
@@ -25,7 +27,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
# This is a hack for twidere.
def get_by_id_or_ap_id(id) do
activity =
with true <- Pleroma.FlakeId.is_flake_id?(id),
with true <- FlakeId.flake_id?(id),
%Activity{} = activity <- Activity.get_by_id_with_object(id) do
activity
else
@@ -40,14 +42,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
end

def get_replied_to_activity(""), do: nil

def get_replied_to_activity(id) when not is_nil(id) do
Activity.get_by_id(id)
end

def get_replied_to_activity(_), do: nil

def attachments_from_ids(%{"media_ids" => ids, "descriptions" => desc} = _) do
attachments_from_ids_descs(ids, desc)
end
@@ -158,70 +152,74 @@ defmodule Pleroma.Web.CommonAPI.Utils do

def maybe_add_list_data(activity_params, _, _), do: activity_params

def make_poll_data(%{"poll" => %{"expires_in" => expires_in}} = data)
when is_binary(expires_in) do
# In some cases mastofe sends out strings instead of integers
data
|> put_in(["poll", "expires_in"], String.to_integer(expires_in))
|> make_poll_data()
end

def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_in}} = data)
when is_list(options) do
%{max_expiration: max_expiration, min_expiration: min_expiration} =
limits = Pleroma.Config.get([:instance, :poll_limits])

# XXX: There is probably a cleaner way of doing this
try do
# In some cases mastofe sends out strings instead of integers
expires_in = if is_binary(expires_in), do: String.to_integer(expires_in), else: expires_in

if Enum.count(options) > limits.max_options do
raise ArgumentError, message: "Poll can't contain more than #{limits.max_options} options"
end
limits = Pleroma.Config.get([:instance, :poll_limits])

{poll, emoji} =
with :ok <- validate_poll_expiration(expires_in, limits),
:ok <- validate_poll_options_amount(options, limits),
:ok <- validate_poll_options_length(options, limits) do
{option_notes, emoji} =
Enum.map_reduce(options, %{}, fn option, emoji ->
if String.length(option) > limits.max_option_chars do
raise ArgumentError,
message:
"Poll options cannot be longer than #{limits.max_option_chars} characters each"
end

{%{
"name" => option,
"type" => "Note",
"replies" => %{"type" => "Collection", "totalItems" => 0}
}, Map.merge(emoji, Formatter.get_emoji_map(option))}
end)

case expires_in do
expires_in when expires_in > max_expiration ->
raise ArgumentError, message: "Expiration date is too far in the future"

expires_in when expires_in < min_expiration ->
raise ArgumentError, message: "Expiration date is too soon"
note = %{
"name" => option,
"type" => "Note",
"replies" => %{"type" => "Collection", "totalItems" => 0}
}

_ ->
:noop
end
{note, Map.merge(emoji, Emoji.Formatter.get_emoji_map(option))}
end)

end_time =
NaiveDateTime.utc_now()
|> NaiveDateTime.add(expires_in)
|> NaiveDateTime.to_iso8601()

poll =
if Pleroma.Web.ControllerHelper.truthy_param?(data["poll"]["multiple"]) do
%{"type" => "Question", "anyOf" => poll, "closed" => end_time}
else
%{"type" => "Question", "oneOf" => poll, "closed" => end_time}
end
key = if truthy_param?(data["poll"]["multiple"]), do: "anyOf", else: "oneOf"
poll = %{"type" => "Question", key => option_notes, "closed" => end_time}

{poll, emoji}
rescue
e in ArgumentError -> e.message
{:ok, {poll, emoji}}
end
end

def make_poll_data(%{"poll" => poll}) when is_map(poll) do
"Invalid poll"
{:error, "Invalid poll"}
end

def make_poll_data(_data) do
{%{}, %{}}
{:ok, {%{}, %{}}}
end

defp validate_poll_options_amount(options, %{max_options: max_options}) do
if Enum.count(options) > max_options do
{:error, "Poll can't contain more than #{max_options} options"}
else
:ok
end
end

defp validate_poll_options_length(options, %{max_option_chars: max_option_chars}) do
if Enum.any?(options, &(String.length(&1) > max_option_chars)) do
{:error, "Poll options cannot be longer than #{max_option_chars} characters each"}
else
:ok
end
end

defp validate_poll_expiration(expires_in, %{min_expiration: min, max_expiration: max}) do
cond do
expires_in > max -> {:error, "Expiration date is too far in the future"}
expires_in < min -> {:error, "Expiration date is too soon"}
true -> :ok
end
end

def make_content_html(
@@ -233,7 +231,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
no_attachment_links =
data
|> Map.get("no_attachment_links", Config.get([:instance, :no_attachment_links]))
|> Kernel.in([true, "true"])
|> truthy_param?()

content_type = get_content_type(data["content_type"])

@@ -346,25 +344,25 @@ defmodule Pleroma.Web.CommonAPI.Utils do
attachments,
in_reply_to,
tags,
cw \\ nil,
summary \\ nil,
cc \\ [],
sensitive \\ false,
merge \\ %{}
extra_params \\ %{}
) do
%{
"type" => "Note",
"to" => to,
"cc" => cc,
"content" => content_html,
"summary" => cw,
"sensitive" => !Enum.member?(["false", "False", "0", false], sensitive),
"summary" => summary,
"sensitive" => truthy_param?(sensitive),
"context" => context,
"attachment" => attachments,
"actor" => actor,
"tag" => Keyword.values(tags) |> Enum.uniq()
}
|> add_in_reply_to(in_reply_to)
|> Map.merge(merge)
|> Map.merge(extra_params)
end

defp add_in_reply_to(object, nil), do: object
@@ -433,12 +431,14 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
end

def emoji_from_profile(%{info: _info} = user) do
(Formatter.get_emoji(user.bio) ++ Formatter.get_emoji(user.name))
|> Enum.map(fn {shortcode, url, _} ->
def emoji_from_profile(%User{bio: bio, name: name}) do
[bio, name]
|> Enum.map(&Emoji.Formatter.get_emoji/1)
|> Enum.concat()
|> Enum.map(fn {shortcode, %Emoji{file: path}} ->
%{
"type" => "Emoji",
"icon" => %{"type" => "Image", "url" => "#{Endpoint.url()}#{url}"},
"icon" => %{"type" => "Image", "url" => "#{Endpoint.url()}#{path}"},
"name" => ":#{shortcode}:"
}
end)
@@ -570,15 +570,16 @@ defmodule Pleroma.Web.CommonAPI.Utils do
}
end

def validate_character_limit(full_payload, attachments, limit) do
def validate_character_limit("" = _full_payload, [] = _attachments) do
{:error, dgettext("errors", "Cannot post an empty status without attachments")}
end

def validate_character_limit(full_payload, _attachments) do
limit = Pleroma.Config.get([:instance, :limit])
length = String.length(full_payload)

if length < limit do
if length > 0 or Enum.count(attachments) > 0 do
:ok
else
{:error, dgettext("errors", "Cannot post an empty status without attachments")}
end
:ok
else
{:error, dgettext("errors", "The status is over the character limit")}
end


+ 8
- 1
lib/pleroma/web/controller_helper.ex Vedi File

@@ -6,7 +6,7 @@ defmodule Pleroma.Web.ControllerHelper do
use Pleroma.Web, :controller

# As in MastoAPI, per https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html
@falsy_param_values [false, 0, "0", "f", "F", "false", "FALSE", "off", "OFF"]
@falsy_param_values [false, 0, "0", "f", "F", "false", "False", "FALSE", "off", "OFF"]
def truthy_param?(blank_value) when blank_value in [nil, ""], do: nil
def truthy_param?(value), do: value not in @falsy_param_values

@@ -68,4 +68,11 @@ defmodule Pleroma.Web.ControllerHelper do
conn
end
end

def assign_account_by_id(%{params: %{"id" => id}} = conn, _) do
case Pleroma.User.get_cached_by_id(id) do
%Pleroma.User{} = account -> assign(conn, :account, account)
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
end
end
end

+ 1
- 4
lib/pleroma/web/endpoint.ex Vedi File

@@ -97,10 +97,7 @@ defmodule Pleroma.Web.Endpoint do
extra: extra
)

# Note: the plug and its configuration is compile-time this can't be upstreamed yet
if proxies = Pleroma.Config.get([__MODULE__, :reverse_proxies]) do
plug(RemoteIp, proxies: proxies)
end
plug(Pleroma.Plugs.RemoteIp)

defmodule Instrumenter do
use Prometheus.PhoenixInstrumenter


+ 344
- 0
lib/pleroma/web/mastodon_api/controllers/account_controller.ex Vedi File

@@ -0,0 +1,344 @@
# 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.AccountController do
use Pleroma.Web, :controller

import Pleroma.Web.ControllerHelper,
only: [add_link_headers: 2, truthy_param?: 1, assign_account_by_id: 2, json_response: 3]

alias Pleroma.Emoji
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.ListView
alias Pleroma.Web.MastodonAPI.MastodonAPI
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.TwitterAPI.TwitterAPI

plug(
OAuthScopesPlug,
%{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
when action == :show
)

plug(
OAuthScopesPlug,
%{scopes: ["read:accounts"]}
when action in [:endorsements, :verify_credentials, :followers, :following]
)

plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action == :update_credentials)

plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :lists)

plug(
OAuthScopesPlug,
%{scopes: ["follow", "write:blocks"]} when action in [:block, :unblock]
)

plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)

plug(
OAuthScopesPlug,
%{scopes: ["follow", "write:follows"]} when action in [:follow, :unfollow]
)

plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])

plug(
Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
when action != :create
)

@relations [:follow, :unfollow]
@needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a

plug(RateLimiter, {:relations_id_action, params: ["id", "uri"]} when action in @relations)
plug(RateLimiter, :relations_actions when action in @relations)
plug(RateLimiter, :app_account_creation when action == :create)
plug(:assign_account_by_id when action in @needs_account)

action_fallback(Pleroma.Web.MastodonAPI.FallbackController)

@doc "POST /api/v1/accounts"
def create(
%{assigns: %{app: app}} = conn,
%{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
) do
params =
params
|> Map.take([
"email",
"captcha_solution",
"captcha_token",
"captcha_answer_data",
"token",
"password"
])
|> Map.put("nickname", nickname)
|> Map.put("fullname", params["fullname"] || nickname)
|> Map.put("bio", params["bio"] || "")
|> Map.put("confirm", params["password"])

with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
{:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
json(conn, %{
token_type: "Bearer",
access_token: token.token,
scope: app.scopes,
created_at: Token.Utils.format_created_at(token)
})
else
{:error, errors} -> json_response(conn, :bad_request, errors)
end
end

def create(%{assigns: %{app: _app}} = conn, _) do
render_error(conn, :bad_request, "Missing parameters")
end

def create(conn, _) do
render_error(conn, :forbidden, "Invalid credentials")
end

@doc "GET /api/v1/accounts/verify_credentials"
def verify_credentials(%{assigns: %{user: user}} = conn, _) do
chat_token = Phoenix.Token.sign(conn, "user socket", user.id)

render(conn, "show.json",
user: user,
for: user,
with_pleroma_settings: true,
with_chat_token: chat_token
)
end

@doc "PATCH /api/v1/accounts/update_credentials"
def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
user = original_user

user_params =
%{}
|> add_if_present(params, "display_name", :name)
|> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
|> add_if_present(params, "avatar", :avatar, fn value ->
with %Plug.Upload{} <- value,
{:ok, object} <- ActivityPub.upload(value, type: :avatar) do
{:ok, object.data}
end
end)

emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")

user_info_emojis =
user.info
|> Map.get(:emoji, [])
|> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
|> Enum.dedup()

info_params =
[
:no_rich_text,
:locked,
:hide_followers_count,
:hide_follows_count,
:hide_followers,
:hide_follows,
:hide_favorites,
:show_role,
:skip_thread_containment,
:discoverable
]
|> Enum.reduce(%{}, fn key, acc ->
add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
end)
|> add_if_present(params, "default_scope", :default_scope)
|> add_if_present(params, "fields", :fields, fn fields ->
fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)

{:ok, fields}
end)
|> add_if_present(params, "fields", :raw_fields)
|> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
{:ok, Map.merge(user.info.pleroma_settings_store, value)}
end)
|> add_if_present(params, "header", :banner, fn value ->
with %Plug.Upload{} <- value,
{:ok, object} <- ActivityPub.upload(value, type: :banner) do
{:ok, object.data}
end
end)
|> add_if_present(params, "pleroma_background_image", :background, fn value ->
with %Plug.Upload{} <- value,
{:ok, object} <- ActivityPub.upload(value, type: :background) do
{:ok, object.data}
end
end)
|> Map.put(:emoji, user_info_emojis)

changeset =
user
|> User.update_changeset(user_params)
|> User.change_info(&User.Info.profile_update(&1, info_params))

with {:ok, user} <- User.update_and_set_cache(changeset) do
if original_user != user, do: CommonAPI.update(user)

render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
else
_e -> render_error(conn, :forbidden, "Invalid request")
end
end

defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
with true <- Map.has_key?(params, params_field),
{:ok, new_value} <- value_function.(params[params_field]) do
Map.put(map, map_field, new_value)
else
_ -> map
end
end

@doc "GET /api/v1/accounts/relationships"
def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
targets = User.get_all_by_ids(List.wrap(id))

render(conn, "relationships.json", user: user, targets: targets)
end

# Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])

@doc "GET /api/v1/accounts/:id"
def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
render(conn, "show.json", user: user, for: for_user)
else
_e -> render_error(conn, :not_found, "Can't find user")
end
end

@doc "GET /api/v1/accounts/:id/statuses"
def statuses(%{assigns: %{user: reading_user}} = conn, params) do
with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
params = Map.put(params, "tag", params["tagged"])
activities = ActivityPub.fetch_user_activities(user, reading_user, params)

conn
|> add_link_headers(activities)
|> put_view(StatusView)
|> render("index.json", activities: activities, for: reading_user, as: :activity)
end
end

@doc "GET /api/v1/accounts/:id/followers"
def followers(%{assigns: %{user: for_user, account: user}} = conn, params) do
followers =
cond do
for_user && user.id == for_user.id -> MastodonAPI.get_followers(user, params)
user.info.hide_followers -> []
true -> MastodonAPI.get_followers(user, params)
end

conn
|> add_link_headers(followers)
|> render("index.json", for: for_user, users: followers, as: :user)
end

@doc "GET /api/v1/accounts/:id/following"
def following(%{assigns: %{user: for_user, account: user}} = conn, params) do
followers =
cond do
for_user && user.id == for_user.id -> MastodonAPI.get_friends(user, params)
user.info.hide_follows -> []
true -> MastodonAPI.get_friends(user, params)
end

conn
|> add_link_headers(followers)
|> render("index.json", for: for_user, users: followers, as: :user)
end

@doc "GET /api/v1/accounts/:id/lists"
def lists(%{assigns: %{user: user, account: account}} = conn, _params) do
lists = Pleroma.List.get_lists_account_belongs(user, account)

conn
|> put_view(ListView)
|> render("index.json", lists: lists)
end

@doc "POST /api/v1/accounts/:id/follow"
def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
{:error, :not_found}
end

def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
with {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
render(conn, "relationship.json", user: follower, target: followed)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end

@doc "POST /api/v1/accounts/:id/unfollow"
def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
{:error, :not_found}
end

def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
with {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
render(conn, "relationship.json", user: follower, target: followed)
end
end

@doc "POST /api/v1/accounts/:id/mute"
def mute(%{assigns: %{user: muter, account: muted}} = conn, params) do
notifications? = params |> Map.get("notifications", true) |> truthy_param?()

with {:ok, muter} <- User.mute(muter, muted, notifications?) do
render(conn, "relationship.json", user: muter, target: muted)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end

@doc "POST /api/v1/accounts/:id/unmute"
def unmute(%{assigns: %{user: muter, account: muted}} = conn, _params) do
with {:ok, muter} <- User.unmute(muter, muted) do
render(conn, "relationship.json", user: muter, target: muted)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end

@doc "POST /api/v1/accounts/:id/block"
def block(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
with {:ok, blocker} <- User.block(blocker, blocked),
{:ok, _activity} <- ActivityPub.block(blocker, blocked) do
render(conn, "relationship.json", user: blocker, target: blocked)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end

@doc "POST /api/v1/accounts/:id/unblock"
def unblock(%{assigns: %{user: blocker, account: blocked}} = conn, _params) do
with {:ok, blocker} <- User.unblock(blocker, blocked),
{:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
render(conn, "relationship.json", user: blocker, target: blocked)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end

@doc "GET /api/v1/endorsements"
def endorsements(conn, params),
do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
end

+ 38
- 0
lib/pleroma/web/mastodon_api/controllers/conversation_controller.ex Vedi File

@@ -0,0 +1,38 @@
# 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.ConversationController do
use Pleroma.Web, :controller

import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]

alias Pleroma.Conversation.Participation
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Repo

action_fallback(Pleroma.Web.MastodonAPI.FallbackController)

plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action == :index)
plug(OAuthScopesPlug, %{scopes: ["write:conversations"]} when action == :read)

plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)

@doc "GET /api/v1/conversations"
def index(%{assigns: %{user: user}} = conn, params) do
participations = Participation.for_user_with_last_activity_id(user, params)

conn
|> add_link_headers(participations)
|> render("participations.json", participations: participations, for: user)
end

@doc "POST /api/v1/conversations/:id/read"
def read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
with %Participation{} = participation <-
Repo.get_by(Participation, id: participation_id, user_id: user.id),
{:ok, participation} <- Participation.mark_as_read(participation) do
render(conn, "participation.json", participation: participation, for: user)
end
end
end

+ 37
- 0
lib/pleroma/web/mastodon_api/controllers/domain_block_controller.ex Vedi File

@@ -0,0 +1,37 @@
# 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.DomainBlockController do
use Pleroma.Web, :controller

alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User

plug(
OAuthScopesPlug,
%{scopes: ["follow", "read:blocks"]} when action == :index
)

plug(
OAuthScopesPlug,
%{scopes: ["follow", "write:blocks"]} when action != :index
)

@doc "GET /api/v1/domain_blocks"
def index(%{assigns: %{user: %{info: info}}} = conn, _) do
json(conn, Map.get(info, :domain_blocks, []))
end

@doc "POST /api/v1/domain_blocks"
def create(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
User.block_domain(blocker, domain)
json(conn, %{})
end

@doc "DELETE /api/v1/domain_blocks"
def delete(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
User.unblock_domain(blocker, domain)
json(conn, %{})
end
end

+ 84
- 0
lib/pleroma/web/mastodon_api/controllers/filter_controller.ex Vedi File

@@ -0,0 +1,84 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.MastodonAPI.FilterController do
use Pleroma.Web, :controller

alias Pleroma.Filter
alias Pleroma.Plugs.OAuthScopesPlug

@oauth_read_actions [:show, :index]

plug(OAuthScopesPlug, %{scopes: ["read:filters"]} when action in @oauth_read_actions)

plug(
OAuthScopesPlug,
%{scopes: ["write:filters"]} when action not in @oauth_read_actions
)

plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)

@doc "GET /api/v1/filters"
def index(%{assigns: %{user: user}} = conn, _) do
filters = Filter.get_filters(user)

render(conn, "filters.json", filters: filters)
end

@doc "POST /api/v1/filters"
def create(
%{assigns: %{user: user}} = conn,
%{"phrase" => phrase, "context" => context} = params
) do
query = %Filter{
user_id: user.id,
phrase: phrase,
context: context,
hide: Map.get(params, "irreversible", false),
whole_word: Map.get(params, "boolean", true)
# expires_at
}

{:ok, response} = Filter.create(query)

render(conn, "filter.json", filter: response)
end

@doc "GET /api/v1/filters/:id"
def show(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
filter = Filter.get(filter_id, user)

render(conn, "filter.json", filter: filter)
end

@doc "PUT /api/v1/filters/:id"
def update(
%{assigns: %{user: user}} = conn,
%{"phrase" => phrase, "context" => context, "id" => filter_id} = params
) do
query = %Filter{
user_id: user.id,
filter_id: filter_id,
phrase: phrase,
context: context,
hide: Map.get(params, "irreversible", nil),
whole_word: Map.get(params, "boolean", true)
# expires_at
}

{:ok, response} = Filter.update(query)
render(conn, "filter.json", filter: response)
end

@doc "DELETE /api/v1/filters/:id"
def delete(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
query = %Filter{
user_id: user.id,
filter_id: filter_id
}

{:ok, _} = Filter.delete(query)
json(conn, %{})
end
end

+ 57
- 0
lib/pleroma/web/mastodon_api/controllers/follow_request_controller.ex Vedi File

@@ -0,0 +1,57 @@
# 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.FollowRequestController do
use Pleroma.Web, :controller

alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
alias Pleroma.Web.CommonAPI

plug(:put_view, Pleroma.Web.MastodonAPI.AccountView)
plug(:assign_follower when action != :index)

action_fallback(:errors)

plug(OAuthScopesPlug, %{scopes: ["follow", "read:follows"]} when action == :index)

plug(
OAuthScopesPlug,
%{scopes: ["follow", "write:follows"]} when action != :index
)

@doc "GET /api/v1/follow_requests"
def index(%{assigns: %{user: followed}} = conn, _params) do
follow_requests = User.get_follow_requests(followed)

render(conn, "index.json", for: followed, users: follow_requests, as: :user)
end

@doc "POST /api/v1/follow_requests/:id/authorize"
def authorize(%{assigns: %{user: followed, follower: follower}} = conn, _params) do
with {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
render(conn, "relationship.json", user: followed, target: follower)
end
end

@doc "POST /api/v1/follow_requests/:id/reject"
def reject(%{assigns: %{user: followed, follower: follower}} = conn, _params) do
with {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
render(conn, "relationship.json", user: followed, target: follower)
end
end

defp assign_follower(%{params: %{"id" => id}} = conn, _) do
case User.get_cached_by_id(id) do
%User{} = follower -> assign(conn, :follower, follower)
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
end
end

defp errors(conn, {:error, message}) do
conn
|> put_status(:forbidden)
|> json(%{error: message})
end
end

+ 3
- 1
lib/pleroma/web/mastodon_api/controllers/list_controller.ex Vedi File

@@ -19,6 +19,8 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
when action in [:create, :update, :delete, :add_to_list, :remove_from_list]
)

plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)

action_fallback(Pleroma.Web.MastodonAPI.FallbackController)

# GET /api/v1/lists
@@ -58,7 +60,7 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
with {:ok, users} <- Pleroma.List.get_following(list) do
conn
|> put_view(AccountView)
|> render("accounts.json", for: user, users: users, as: :user)
|> render("index.json", for: user, users: users, as: :user)
end
end



+ 91
- 1410
lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
File diff soppresso perché troppo grande
Vedi File


+ 67
- 0
lib/pleroma/web/mastodon_api/controllers/notification_controller.ex Vedi File

@@ -0,0 +1,67 @@
# 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.NotificationController do
use Pleroma.Web, :controller

import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]

alias Pleroma.Notification
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Web.MastodonAPI.MastodonAPI

@oauth_read_actions [:show, :index]

plug(
OAuthScopesPlug,
%{scopes: ["read:notifications"]} when action in @oauth_read_actions
)

plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action not in @oauth_read_actions)

# GET /api/v1/notifications
def index(%{assigns: %{user: user}} = conn, params) do
notifications = MastodonAPI.get_notifications(user, params)

conn
|> add_link_headers(notifications)
|> render("index.json", notifications: notifications, for: user)
end

# GET /api/v1/notifications/:id
def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {:ok, notification} <- Notification.get(user, id) do
render(conn, "show.json", notification: notification, for: user)
else
{:error, reason} ->
conn
|> put_status(:forbidden)
|> json(%{"error" => reason})
end
end

# POST /api/v1/notifications/clear
def clear(%{assigns: %{user: user}} = conn, _params) do
Notification.clear(user)
json(conn, %{})
end

# POST /api/v1/notifications/dismiss
def dismiss(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
with {:ok, _notif} <- Notification.dismiss(user, id) do
json(conn, %{})
else
{:error, reason} ->
conn
|> put_status(:forbidden)
|> json(%{"error" => reason})
end
end

# DELETE /api/v1/notifications/destroy_multiple
def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
Notification.destroy_multiple(user, ids)
json(conn, %{})
end
end

+ 20
- 0
lib/pleroma/web/mastodon_api/controllers/report_controller.ex Vedi File

@@ -0,0 +1,20 @@
# 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.ReportController do
alias Pleroma.Plugs.OAuthScopesPlug

use Pleroma.Web, :controller

action_fallback(Pleroma.Web.MastodonAPI.FallbackController)

plug(OAuthScopesPlug, %{scopes: ["write:reports"]} when action == :create)

@doc "POST /api/v1/reports"
def create(%{assigns: %{user: user}} = conn, params) do
with {:ok, activity} <- Pleroma.Web.CommonAPI.report(user, params) do
render(conn, "show.json", activity: activity)
end
end
end

+ 59
- 0
lib/pleroma/web/mastodon_api/controllers/scheduled_activity_controller.ex Vedi File

@@ -0,0 +1,59 @@
# 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.ScheduledActivityController do
use Pleroma.Web, :controller

import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]

alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.ScheduledActivity
alias Pleroma.Web.MastodonAPI.MastodonAPI

plug(:assign_scheduled_activity when action != :index)

@oauth_read_actions [:show, :index]

plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in @oauth_read_actions)
plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action not in @oauth_read_actions)

plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)

action_fallback(Pleroma.Web.MastodonAPI.FallbackController)

@doc "GET /api/v1/scheduled_statuses"
def index(%{assigns: %{user: user}} = conn, params) do
with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
conn
|> add_link_headers(scheduled_activities)
|> render("index.json", scheduled_activities: scheduled_activities)
end
end

@doc "GET /api/v1/scheduled_statuses/:id"
def show(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, _params) do
render(conn, "show.json", scheduled_activity: scheduled_activity)
end

@doc "PUT /api/v1/scheduled_statuses/:id"
def update(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, params) do
with {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
render(conn, "show.json", scheduled_activity: scheduled_activity)
end
end

@doc "DELETE /api/v1/scheduled_statuses/:id"
def delete(%{assigns: %{scheduled_activity: scheduled_activity}} = conn, _params) do
with {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
render(conn, "show.json", scheduled_activity: scheduled_activity)
end
end

defp assign_scheduled_activity(%{assigns: %{user: user}, params: %{"id" => id}} = conn, _) do
case ScheduledActivity.get(user, id) do
%ScheduledActivity{} = activity -> assign(conn, :scheduled_activity, activity)
nil -> Pleroma.Web.MastodonAPI.FallbackController.call(conn, {:error, :not_found}) |> halt()
end
end
end

+ 4
- 3
lib/pleroma/web/mastodon_api/controllers/search_controller.ex Vedi File

@@ -24,9 +24,10 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do

def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
accounts = User.search(query, search_options(params, user))
res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)

json(conn, res)
conn
|> put_view(AccountView)
|> render("index.json", users: accounts, for: user, as: :user)
end

def search2(conn, params), do: do_search(:v2, conn, params)
@@ -76,7 +77,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do

defp resource_search(_, "accounts", query, options) do
accounts = with_fallback(fn -> User.search(query, options) end)
AccountView.render("accounts.json", users: accounts, for: options[:for_user], as: :user)
AccountView.render("index.json", users: accounts, for: options[:for_user], as: :user)
end

defp resource_search(_, "statuses", query, options) do


+ 325
- 0
lib/pleroma/web/mastodon_api/controllers/status_controller.ex Vedi File

@@ -0,0 +1,325 @@
# 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.StatusController do
use Pleroma.Web, :controller

import Pleroma.Web.MastodonAPI.MastodonAPIController, only: [try_render: 3]

require Ecto.Query

alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.Object
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter
alias Pleroma.Repo
alias Pleroma.ScheduledActivity
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.ScheduledActivityView

@unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}

plug(
OAuthScopesPlug,
%{@unauthenticated_access | scopes: ["read:statuses"]}
when action in [
:index,
:show,
:card,
:context
]
)

plug(
OAuthScopesPlug,
%{scopes: ["write:statuses"]}
when action in [
:create,
:delete,
:reblog,
:unreblog
]
)

plug(
OAuthScopesPlug,
%{scopes: ["write:favourites"]} when action in [:favourite, :unfavourite]
)

plug(
OAuthScopesPlug,
%{scopes: ["write:mutes"]} when action in [:mute_conversation, :unmute_conversation]
)

plug(
OAuthScopesPlug,
%{@unauthenticated_access | scopes: ["read:accounts"]}
when action in [:favourited_by, :reblogged_by]
)

plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action in [:pin, :unpin])

# Note: scope not present in Mastodon: write:bookmarks
plug(
OAuthScopesPlug,
%{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
)

plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)

@rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a

plug(
RateLimiter,
{:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
when action in ~w(reblog unreblog)a
)

plug(
RateLimiter,
{:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
when action in ~w(favourite unfavourite)a
)

plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)

action_fallback(Pleroma.Web.MastodonAPI.FallbackController)

@doc """
GET `/api/v1/statuses?ids[]=1&ids[]=2`

`ids` query param is required
"""
def index(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
limit = 100

activities =
ids
|> Enum.take(limit)
|> Activity.all_by_ids_with_object()
|> Enum.filter(&Visibility.visible_for_user?(&1, user))

render(conn, "index.json", activities: activities, for: user, as: :activity)
end

@doc """
POST /api/v1/statuses

Creates a scheduled status when `scheduled_at` param is present and it's far enough
"""
def create(
%{assigns: %{user: user}} = conn,
%{"status" => _, "scheduled_at" => scheduled_at} = params
) do
params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"])

if ScheduledActivity.far_enough?(scheduled_at) do
with {:ok, scheduled_activity} <-
ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
conn
|> put_view(ScheduledActivityView)
|> render("show.json", scheduled_activity: scheduled_activity)
end
else
create(conn, Map.drop(params, ["scheduled_at"]))
end
end

@doc """
POST /api/v1/statuses

Creates a regular status
"""
def create(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"])

with {:ok, activity} <- CommonAPI.post(user, params) do
try_render(conn, "show.json",
activity: activity,
for: user,
as: :activity,
with_direct_conversation_id: true
)
else
{:error, message} ->
conn
|> put_status(:unprocessable_entity)
|> json(%{error: message})
end
end

def create(%{assigns: %{user: _user}} = conn, %{"media_ids" => _} = params) do
create(conn, Map.put(params, "status", ""))
end

@doc "GET /api/v1/statuses/:id"
def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.visible_for_user?(activity, user) do
try_render(conn, "show.json", activity: activity, for: user)
end
end

@doc "DELETE /api/v1/statuses/:id"
def delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
json(conn, %{})
else
_e -> render_error(conn, :forbidden, "Can't delete this post")
end
end

@doc "POST /api/v1/statuses/:id/reblog"
def reblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
%Activity{} = announce <- Activity.normalize(announce.data) do
try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
end
end

@doc "POST /api/v1/statuses/:id/unreblog"
def unreblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
end
end

@doc "POST /api/v1/statuses/:id/favourite"
def favourite(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end

@doc "POST /api/v1/statuses/:id/unfavourite"
def unfavourite(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end

@doc "POST /api/v1/statuses/:id/pin"
def pin(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end

@doc "POST /api/v1/statuses/:id/unpin"
def unpin(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end

@doc "POST /api/v1/statuses/:id/bookmark"
def bookmark(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
%User{} = user <- User.get_cached_by_nickname(user.nickname),
true <- Visibility.visible_for_user?(activity, user),
{:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end

@doc "POST /api/v1/statuses/:id/unbookmark"
def unbookmark(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
%User{} = user <- User.get_cached_by_nickname(user.nickname),
true <- Visibility.visible_for_user?(activity, user),
{:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end

@doc "POST /api/v1/statuses/:id/mute"
def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Activity.get_by_id(id),
{:ok, activity} <- CommonAPI.add_mute(user, activity) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end

@doc "POST /api/v1/statuses/:id/unmute"
def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Activity.get_by_id(id),
{:ok, activity} <- CommonAPI.remove_mute(user, activity) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end

@doc "GET /api/v1/statuses/:id/card"
@deprecated "https://github.com/tootsuite/mastodon/pull/11213"
def card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
with %Activity{} = activity <- Activity.get_by_id(status_id),
true <- Visibility.visible_for_user?(activity, user) do
data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
render(conn, "card.json", data)
else
_ -> render_error(conn, :not_found, "Record not found")
end
end

@doc "GET /api/v1/statuses/:id/favourited_by"
def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
{:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
%Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
users =
User
|> Ecto.Query.where([u], u.ap_id in ^likes)
|> Repo.all()
|> Enum.filter(&(not User.blocks?(user, &1)))

conn
|> put_view(AccountView)
|> render("index.json", for: user, users: users, as: :user)
else
{:visible, false} -> {:error, :not_found}
_ -> json(conn, [])
end
end

@doc "GET /api/v1/statuses/:id/reblogged_by"
def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id),
{:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
%Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
users =
User
|> Ecto.Query.where([u], u.ap_id in ^announces)
|> Repo.all()
|> Enum.filter(&(not User.blocks?(user, &1)))

conn
|> put_view(AccountView)
|> render("index.json", for: user, users: users, as: :user)
else
{:visible, false} -> {:error, :not_found}
_ -> json(conn, [])
end
end

@doc "GET /api/v1/statuses/:id/context"
def context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Activity.get_by_id(id) do
activities =
ActivityPub.fetch_activities_for_context(activity.data["context"], %{
"blocking_user" => user,
"user" => user,
"exclude_id" => activity.id
})

render(conn, "context.json", activity: activity, activities: activities, user: user)
end
end
end

+ 142
- 0
lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex Vedi File

@@ -0,0 +1,142 @@
# 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.TimelineController do
use Pleroma.Web, :controller

import Pleroma.Web.ControllerHelper,
only: [add_link_headers: 2, add_link_headers: 3, truthy_param?: 1]

alias Pleroma.Pagination
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Web.ActivityPub.ActivityPub

plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:home, :direct])
plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action == :list)

plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)

plug(:put_view, Pleroma.Web.MastodonAPI.StatusView)

# GET /api/v1/timelines/home
def home(%{assigns: %{user: user}} = conn, params) do
params =
params
|> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)

recipients = [user.ap_id | user.following]

activities =
recipients
|> ActivityPub.fetch_activities(params)
|> Enum.reverse()

conn
|> add_link_headers(activities)
|> render("index.json", activities: activities, for: user, as: :activity)
end

# GET /api/v1/timelines/direct
def direct(%{assigns: %{user: user}} = conn, params) do
params =
params
|> Map.put("type", "Create")
|> Map.put("blocking_user", user)
|> Map.put("user", user)
|> Map.put(:visibility, "direct")

activities =
[user.ap_id]
|> ActivityPub.fetch_activities_query(params)
|> Pagination.fetch_paginated(params)

conn
|> add_link_headers(activities)
|> render("index.json", activities: activities, for: user, as: :activity)
end

# GET /api/v1/timelines/public
def public(%{assigns: %{user: user}} = conn, params) do
local_only = truthy_param?(params["local"])

activities =
params
|> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", local_only)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> ActivityPub.fetch_public_activities()
|> Enum.reverse()

conn
|> add_link_headers(activities, %{"local" => local_only})
|> render("index.json", activities: activities, for: user, as: :activity)
end

# GET /api/v1/timelines/tag/:tag
def hashtag(%{assigns: %{user: user}} = conn, params) do
local_only = truthy_param?(params["local"])

tags =
[params["tag"], params["any"]]
|> List.flatten()
|> Enum.uniq()
|> Enum.filter(& &1)
|> Enum.map(&String.downcase(&1))

tag_all =
params
|> Map.get("all", [])
|> Enum.map(&String.downcase(&1))

tag_reject =
params
|> Map.get("none", [])
|> Enum.map(&String.downcase(&1))

activities =
params
|> Map.put("type", "Create")
|> Map.put("local_only", local_only)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
|> Map.put("tag", tags)
|> Map.put("tag_all", tag_all)
|> Map.put("tag_reject", tag_reject)
|> ActivityPub.fetch_public_activities()
|> Enum.reverse()

conn
|> add_link_headers(activities, %{"local" => local_only})
|> render("index.json", activities: activities, for: user, as: :activity)
end

# GET /api/v1/timelines/list/:list_id
def list(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
params =
params
|> Map.put("type", "Create")
|> Map.put("blocking_user", user)
|> Map.put("user", user)
|> Map.put("muting_user", user)

# we must filter the following list for the user to avoid leaking statuses the user
# does not actually have permission to see (for more info, peruse security issue #270).
activities =
following
|> Enum.filter(fn x -> x in user.following end)
|> ActivityPub.fetch_activities_bounded(following, params)
|> Enum.reverse()

render(conn, "index.json", activities: activities, for: user, as: :activity)
else
_e -> render_error(conn, :forbidden, "Error.")
end
end
end

+ 26
- 6
lib/pleroma/web/mastodon_api/views/account_view.ex Vedi File

@@ -11,15 +11,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MediaProxy

def render("accounts.json", %{users: users} = opts) do
def render("index.json", %{users: users} = opts) do
users
|> render_many(AccountView, "account.json", opts)
|> render_many(AccountView, "show.json", opts)
|> Enum.filter(&Enum.any?/1)
end

def render("account.json", %{user: user} = opts) do
def render("show.json", %{user: user} = opts) do
if User.visible_for?(user, opts[:for]),
do: do_render("account.json", opts),
do: do_render("show.json", opts),
else: %{}
end

@@ -66,7 +66,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
render_many(targets, AccountView, "relationship.json", user: user, as: :target)
end

defp do_render("account.json", %{user: user} = opts) do
defp do_render("show.json", %{user: user} = opts) do
display_name = HTML.strip_tags(user.name || user.nickname)

image = User.avatar_url(user) |> MediaProxy.url()
@@ -116,6 +116,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
bio = HTML.filter_tags(user.bio, User.html_filter_policy(opts[:for]))
relationship = render("relationship.json", %{user: opts[:for], target: user})

discoverable = user.info.discoverable

%{
id: to_string(user.id),
username: username_from_nickname(user.nickname),
@@ -139,7 +141,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
note: HTML.strip_tags((user.bio || "") |> String.replace("<br>", "\n")),
sensitive: false,
fields: raw_fields,
pleroma: %{}
pleroma: %{
discoverable: discoverable
}
},

# Pleroma extension
@@ -162,6 +166,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|> maybe_put_settings_store(user, opts[:for], opts)
|> maybe_put_chat_token(user, opts[:for], opts)
|> maybe_put_activation_status(user, opts[:for])
|> maybe_put_follow_requests_count(user, opts[:for])
end

defp username_from_nickname(string) when is_binary(string) do
@@ -170,6 +175,21 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do

defp username_from_nickname(_), do: nil

defp maybe_put_follow_requests_count(
data,
%User{id: user_id} = user,
%User{id: user_id}
) do
count =
User.get_follow_requests(user)
|> length()

data
|> Kernel.put_in([:follow_requests_count], count)
end

defp maybe_put_follow_requests_count(data, _, _), do: data

defp maybe_put_settings(
data,
%User{id: user_id} = user,


+ 7
- 14
lib/pleroma/web/mastodon_api/views/conversation_view.ex Vedi File

@@ -11,6 +11,10 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.StatusView

def render("participations.json", %{participations: participations, for: user}) do
render_many(participations, __MODULE__, "participation.json", as: :participation, for: user)
end

def render("participation.json", %{participation: participation, for: user}) do
participation = Repo.preload(participation, conversation: [], recipients: [])

@@ -23,25 +27,14 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do
end

activity = Activity.get_by_id_with_object(last_activity_id)

last_status = StatusView.render("status.json", %{activity: activity, for: user})

# Conversations return all users except the current user.
users =
participation.recipients
|> Enum.reject(&(&1.id == user.id))

accounts =
AccountView.render("accounts.json", %{
users: users,
as: :user
})
users = Enum.reject(participation.recipients, &(&1.id == user.id))

%{
id: participation.id |> to_string(),
accounts: accounts,
accounts: render(AccountView, "index.json", users: users, as: :user),
unread: !participation.read,
last_status: last_status
last_status: render(StatusView, "show.json", activity: activity, for: user)
}
end
end

+ 4
- 4
lib/pleroma/web/mastodon_api/views/notification_view.ex Vedi File

@@ -29,7 +29,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
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}),
account: AccountView.render("show.json", %{user: actor, for: user}),
pleroma: %{
is_seen: notification.seen
}
@@ -39,19 +39,19 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
"mention" ->
response
|> Map.merge(%{
status: StatusView.render("status.json", %{activity: activity, for: user})
status: StatusView.render("show.json", %{activity: activity, for: user})
})

"favourite" ->
response
|> Map.merge(%{
status: StatusView.render("status.json", %{activity: parent_activity, for: user})
status: StatusView.render("show.json", %{activity: parent_activity, for: user})
})

"reblog" ->
response
|> Map.merge(%{
status: StatusView.render("status.json", %{activity: parent_activity, for: user})
status: StatusView.render("show.json", %{activity: parent_activity, for: user})
})

"follow" ->


+ 1
- 1
lib/pleroma/web/mastodon_api/views/report_view.ex Vedi File

@@ -5,7 +5,7 @@
defmodule Pleroma.Web.MastodonAPI.ReportView do
use Pleroma.Web, :view

def render("report.json", %{activity: activity}) do
def render("show.json", %{activity: activity}) do
%{
id: to_string(activity.id),
action_taken: false


+ 7
- 16
lib/pleroma/web/mastodon_api/views/scheduled_activity_view.ex Vedi File

@@ -7,11 +7,10 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do

alias Pleroma.ScheduledActivity
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.ScheduledActivityView
alias Pleroma.Web.MastodonAPI.StatusView

def render("index.json", %{scheduled_activities: scheduled_activities}) do
render_many(scheduled_activities, ScheduledActivityView, "show.json")
render_many(scheduled_activities, __MODULE__, "show.json")
end

def render("show.json", %{scheduled_activity: %ScheduledActivity{} = scheduled_activity}) do
@@ -24,12 +23,8 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do
end

defp with_media_attachments(data, %{params: %{"media_attachments" => media_attachments}}) do
try do
attachments = render_many(media_attachments, StatusView, "attachment.json", as: :attachment)
Map.put(data, :media_attachments, attachments)
rescue
_ -> data
end
attachments = render_many(media_attachments, StatusView, "attachment.json", as: :attachment)
Map.put(data, :media_attachments, attachments)
end

defp with_media_attachments(data, _), do: data
@@ -45,13 +40,9 @@ defmodule Pleroma.Web.MastodonAPI.ScheduledActivityView do
in_reply_to_id: params["in_reply_to_id"]
}

data =
if media_ids = params["media_ids"] do
Map.put(data, :media_ids, media_ids)
else
data
end

data
case params["media_ids"] do
nil -> data
media_ids -> Map.put(data, :media_ids, media_ids)
end
end
end

+ 44
- 15
lib/pleroma/web/mastodon_api/views/status_view.ex Vedi File

@@ -73,17 +73,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do

def render("index.json", opts) do
replied_to_activities = get_replied_to_activities(opts.activities)
opts = Map.put(opts, :replied_to_activities, replied_to_activities)

opts.activities
|> safe_render_many(
StatusView,
"status.json",
Map.put(opts, :replied_to_activities, replied_to_activities)
)
safe_render_many(opts.activities, StatusView, "show.json", opts)
end

def render(
"status.json",
"show.json",
%{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts
) do
user = get_user(activity.data["actor"])
@@ -96,7 +92,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|> Activity.with_set_thread_muted_field(opts[:for])
|> Repo.one()

reblogged = render("status.json", Map.put(opts, :activity, reblogged_activity))
reblogged = render("show.json", Map.put(opts, :activity, reblogged_activity))

favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || [])

@@ -112,7 +108,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
id: to_string(activity.id),
uri: activity_object.data["id"],
url: activity_object.data["id"],
account: AccountView.render("account.json", %{user: user, for: opts[:for]}),
account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
in_reply_to_id: nil,
in_reply_to_account_id: nil,
reblog: reblogged,
@@ -144,7 +140,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
}
end

def render("status.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
object = Object.normalize(activity)

user = get_user(activity.data["actor"])
@@ -262,7 +258,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
id: to_string(activity.id),
uri: object.data["id"],
url: url,
account: AccountView.render("account.json", %{user: user, for: opts[:for]}),
account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
in_reply_to_id: reply_to && to_string(reply_to.id),
in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
reblog: nil,
@@ -303,7 +299,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
}
end

def render("status.json", _) do
def render("show.json", _) do
nil
end

@@ -343,9 +339,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
}
end

def render("card.json", _) do
nil
end
def render("card.json", _), do: nil

def render("attachment.json", %{attachment: attachment}) do
[attachment_url | _] = attachment["url"]
@@ -374,6 +368,27 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
}
end

def render("listen.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do
object = Object.normalize(activity)

user = get_user(activity.data["actor"])
created_at = Utils.to_masto_date(activity.data["published"])

%{
id: activity.id,
account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
created_at: created_at,
title: object.data["title"] |> HTML.strip_tags(),
artist: object.data["artist"] |> HTML.strip_tags(),
album: object.data["album"] |> HTML.strip_tags(),
length: object.data["length"]
}
end

def render("listens.json", opts) do
safe_render_many(opts.activities, StatusView, "listen.json", opts)
end

def render("poll.json", %{object: object} = opts) do
{multiple, options} =
case object.data do
@@ -443,6 +458,20 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end
end

def render("context.json", %{activity: activity, activities: activities, user: user}) do
%{ancestors: ancestors, descendants: descendants} =
activities
|> Enum.reverse()
|> Enum.group_by(fn %{id: id} -> if id < activity.id, do: :ancestors, else: :descendants end)
|> Map.put_new(:ancestors, [])
|> Map.put_new(:descendants, [])

%{
ancestors: render("index.json", for: user, activities: ancestors, as: :activity),
descendants: render("index.json", for: user, activities: descendants, as: :activity)
}
end

def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
object = Object.normalize(activity)



+ 3
- 2
lib/pleroma/web/metadata/utils.ex Vedi File

@@ -3,6 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.Metadata.Utils do
alias Pleroma.Emoji
alias Pleroma.Formatter
alias Pleroma.HTML
alias Pleroma.Web.MediaProxy
@@ -13,7 +14,7 @@ defmodule Pleroma.Web.Metadata.Utils do
|> HtmlEntities.decode()
|> String.replace(~r/<br\s?\/?>/, " ")
|> HTML.get_cached_stripped_html_for_activity(object, "metadata")
|> Formatter.demojify()
|> Emoji.Formatter.demojify()
|> Formatter.truncate()
end

@@ -23,7 +24,7 @@ defmodule Pleroma.Web.Metadata.Utils do
|> HtmlEntities.decode()
|> String.replace(~r/<br\s?\/?>/, " ")
|> HTML.strip_tags()
|> Formatter.demojify()
|> Emoji.Formatter.demojify()
|> Formatter.truncate(max_length)
end



+ 1
- 0
lib/pleroma/web/nodeinfo/nodeinfo_controller.ex Vedi File

@@ -57,6 +57,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
"mastodon_api_streaming",
"polls",
"pleroma_explicit_addressing",
"shareable_emoji_packs",
if Config.get([:media_proxy, :enabled]) do
"media_proxy"
end,


+ 26
- 0
lib/pleroma/web/oauth/app.ex Vedi File

@@ -5,6 +5,7 @@
defmodule Pleroma.Web.OAuth.App do
use Ecto.Schema
import Ecto.Changeset
alias Pleroma.Repo

@type t :: %__MODULE__{}

@@ -39,4 +40,29 @@ defmodule Pleroma.Web.OAuth.App do
changeset
end
end

@doc """
Gets app by attrs or create new with attrs.
And updates the scopes if need.
"""
@spec get_or_make(map(), list(String.t())) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
def get_or_make(attrs, scopes) do
with %__MODULE__{} = app <- Repo.get_by(__MODULE__, attrs) do
update_scopes(app, scopes)
else
_e ->
%__MODULE__{}
|> register_changeset(Map.put(attrs, :scopes, scopes))
|> Repo.insert()
end
end

defp update_scopes(%__MODULE__{} = app, []), do: {:ok, app}
defp update_scopes(%__MODULE__{scopes: scopes} = app, scopes), do: {:ok, app}

defp update_scopes(%__MODULE__{} = app, scopes) do
app
|> change(%{scopes: scopes})
|> Repo.update()
end
end

+ 1
- 1
lib/pleroma/web/oauth/authorization.ex Vedi File

@@ -20,7 +20,7 @@ defmodule Pleroma.Web.OAuth.Authorization do
field(:scopes, {:array, :string}, default: [])
field(:valid_until, :naive_datetime_usec)
field(:used, :boolean, default: false)
belongs_to(:user, User, type: Pleroma.FlakeId)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:app, App)

timestamps()


+ 5
- 0
lib/pleroma/web/oauth/oauth_controller.ex Vedi File

@@ -202,6 +202,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do
{:ok, app} <- Token.Utils.fetch_app(conn),
{:auth_active, true} <- {:auth_active, User.auth_active?(user)},
{:user_active, true} <- {:user_active, !user.info.deactivated},
{:password_reset_pending, false} <-
{:password_reset_pending, user.info.password_reset_pending},
{:ok, scopes} <- validate_scopes(app, params),
{:ok, auth} <- Authorization.create_authorization(app, user, scopes),
{:ok, token} <- Token.exchange_token(app, auth) do
@@ -215,6 +217,9 @@ defmodule Pleroma.Web.OAuth.OAuthController do
{:user_active, false} ->
render_error(conn, :forbidden, "Your account is currently disabled")

{:password_reset_pending, true} ->
render_error(conn, :forbidden, "Password reset is required")

_error ->
render_invalid_credentials_error(conn)
end


+ 1
- 1
lib/pleroma/web/oauth/token.ex Vedi File

@@ -21,7 +21,7 @@ defmodule Pleroma.Web.OAuth.Token do
field(:refresh_token, :string)
field(:scopes, {:array, :string}, default: [])
field(:valid_until, :naive_datetime_usec)
belongs_to(:user, User, type: Pleroma.FlakeId)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:app, App)

timestamps()


+ 2
- 1
lib/pleroma/web/ostatus/ostatus_controller.ex Vedi File

@@ -216,7 +216,8 @@ defmodule Pleroma.Web.OStatus.OStatusController do

conn
|> put_resp_header("content-type", "application/activity+json")
|> json(ObjectView.render("object.json", %{object: object}))
|> put_view(ObjectView)
|> render("object.json", %{object: object})
end

defp represent_activity(_conn, "activity+json", _, _) do


+ 168
- 0
lib/pleroma/web/pleroma_api/controllers/account_controller.ex Vedi File

@@ -0,0 +1,168 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.PleromaAPI.AccountController do
use Pleroma.Web, :controller

import Pleroma.Web.ControllerHelper,
only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2]

alias Ecto.Changeset
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.StatusView

require Pleroma.Constants

plug(
OAuthScopesPlug,
%{scopes: ["follow", "write:follows"]} when action in [:subscribe, :unsubscribe]
)

plug(
OAuthScopesPlug,
%{scopes: ["write:accounts"]}
# Note: the following actions are not permission-secured in Mastodon:
when action in [
:update_avatar,
:update_banner,
:update_background
]
)

plug(OAuthScopesPlug, %{scopes: ["read:favourites"]} when action == :favourites)

# An extra safety measure for possible actions not guarded by OAuth permissions specification
plug(
Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
when action != :confirmation_resend
)

plug(RateLimiter, :account_confirmation_resend when action == :confirmation_resend)
plug(:assign_account_by_id when action in [:favourites, :subscribe, :unsubscribe])
plug(:put_view, Pleroma.Web.MastodonAPI.AccountView)

@doc "POST /api/v1/pleroma/accounts/confirmation_resend"
def confirmation_resend(conn, params) do
nickname_or_email = params["email"] || params["nickname"]

with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
{:ok, _} <- User.try_send_confirmation_email(user) do
json_response(conn, :no_content, "")
end
end

@doc "PATCH /api/v1/pleroma/accounts/update_avatar"
def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
{:ok, user} =
user
|> Changeset.change(%{avatar: nil})
|> User.update_and_set_cache()

CommonAPI.update(user)

json(conn, %{url: nil})
end

def update_avatar(%{assigns: %{user: user}} = conn, params) do
{:ok, %{data: data}} = ActivityPub.upload(params, type: :avatar)
{:ok, user} = user |> Changeset.change(%{avatar: data}) |> User.update_and_set_cache()
%{"url" => [%{"href" => href} | _]} = data

CommonAPI.update(user)

json(conn, %{url: href})
end

@doc "PATCH /api/v1/pleroma/accounts/update_banner"
def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
new_info = %{"banner" => %{}}

with {:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
CommonAPI.update(user)
json(conn, %{url: nil})
end
end

def update_banner(%{assigns: %{user: user}} = conn, params) do
with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
new_info <- %{"banner" => object.data},
{:ok, user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
CommonAPI.update(user)
%{"url" => [%{"href" => href} | _]} = object.data

json(conn, %{url: href})
end
end

@doc "PATCH /api/v1/pleroma/accounts/update_background"
def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
new_info = %{"background" => %{}}

with {:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
json(conn, %{url: nil})
end
end

def update_background(%{assigns: %{user: user}} = conn, params) do
with {:ok, object} <- ActivityPub.upload(params, type: :background),
new_info <- %{"background" => object.data},
{:ok, _user} <- User.update_info(user, &User.Info.profile_update(&1, new_info)) do
%{"url" => [%{"href" => href} | _]} = object.data

json(conn, %{url: href})
end
end

@doc "GET /api/v1/pleroma/accounts/:id/favourites"
def favourites(%{assigns: %{account: %{info: %{hide_favorites: true}}}} = conn, _params) do
render_error(conn, :forbidden, "Can't get favorites")
end

def favourites(%{assigns: %{user: for_user, account: user}} = conn, params) do
params =
params
|> Map.put("type", "Create")
|> Map.put("favorited_by", user.ap_id)
|> Map.put("blocking_user", for_user)

recipients =
if for_user do
[Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
else
[Pleroma.Constants.as_public()]
end

activities =
recipients
|> ActivityPub.fetch_activities(params)
|> Enum.reverse()

conn
|> add_link_headers(activities)
|> put_view(StatusView)
|> render("index.json", activities: activities, for: for_user, as: :activity)
end

@doc "POST /api/v1/pleroma/accounts/:id/subscribe"
def subscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _params) do
with {:ok, subscription_target} <- User.subscribe(user, subscription_target) do
render(conn, "relationship.json", user: user, target: subscription_target)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end

@doc "POST /api/v1/pleroma/accounts/:id/unsubscribe"
def unsubscribe(%{assigns: %{user: user, account: subscription_target}} = conn, _params) do
with {:ok, subscription_target} <- User.unsubscribe(user, subscription_target) do
render(conn, "relationship.json", user: user, target: subscription_target)
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end
end

+ 617
- 0
lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex Vedi File

@@ -0,0 +1,617 @@
defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
use Pleroma.Web, :controller

require Logger

def emoji_dir_path do
Path.join(
Pleroma.Config.get!([:instance, :static_dir]),
"emoji"
)
end

@doc """
Lists packs from the remote instance.

Since JS cannot ask remote instances for their packs due to CPS, it has to
be done by the server
"""
def list_from(conn, %{"instance_address" => address}) do
address = String.trim(address)

if shareable_packs_available(address) do
list_resp =
"#{address}/api/pleroma/emoji/packs" |> Tesla.get!() |> Map.get(:body) |> Jason.decode!()

json(conn, list_resp)
else
conn
|> put_status(:internal_server_error)
|> json(%{error: "The requested instance does not support sharing emoji packs"})
end
end

@doc """
Lists the packs available on the instance as JSON.

The information is public and does not require authentification. The format is
a map of "pack directory name" to pack.json contents.
"""
def list_packs(conn, _params) do
# Create the directory first if it does not exist. This is probably the first request made
# with the API so it should be sufficient
with {:create_dir, :ok} <- {:create_dir, File.mkdir_p(emoji_dir_path())},
{:ls, {:ok, results}} <- {:ls, File.ls(emoji_dir_path())} do
pack_infos =
results
|> Enum.filter(&has_pack_json?/1)
|> Enum.map(&load_pack/1)
# Check if all the files are in place and can be sent
|> Enum.map(&validate_pack/1)
# Transform into a map of pack-name => pack-data
|> Enum.into(%{})

json(conn, pack_infos)
else
{:create_dir, {:error, e}} ->
conn
|> put_status(:internal_server_error)
|> json(%{error: "Failed to create the emoji pack directory at #{emoji_dir_path()}: #{e}"})

{:ls, {:error, e}} ->
conn
|> put_status(:internal_server_error)
|> json(%{
error:
"Failed to get the contents of the emoji pack directory at #{emoji_dir_path()}: #{e}"
})
end
end

defp has_pack_json?(file) do
dir_path = Path.join(emoji_dir_path(), file)
# Filter to only use the pack.json packs
File.dir?(dir_path) and File.exists?(Path.join(dir_path, "pack.json"))
end

defp load_pack(pack_name) do
pack_path = Path.join(emoji_dir_path(), pack_name)
pack_file = Path.join(pack_path, "pack.json")

{pack_name, Jason.decode!(File.read!(pack_file))}
end

defp validate_pack({name, pack}) do
pack_path = Path.join(emoji_dir_path(), name)

if can_download?(pack, pack_path) do
archive_for_sha = make_archive(name, pack, pack_path)
archive_sha = :crypto.hash(:sha256, archive_for_sha) |> Base.encode16()

pack =
pack
|> put_in(["pack", "can-download"], true)
|> put_in(["pack", "download-sha256"], archive_sha)

{name, pack}
else
{name, put_in(pack, ["pack", "can-download"], false)}
end
end

defp can_download?(pack, pack_path) do
# If the pack is set as shared, check if it can be downloaded
# That means that when asked, the pack can be packed and sent to the remote
# Otherwise, they'd have to download it from external-src
pack["pack"]["share-files"] &&
Enum.all?(pack["files"], fn {_, path} ->
File.exists?(Path.join(pack_path, path))
end)
end

defp create_archive_and_cache(name, pack, pack_dir, md5) do
files =
['pack.json'] ++
(pack["files"] |> Enum.map(fn {_, path} -> to_charlist(path) end))

{:ok, {_, zip_result}} = :zip.zip('#{name}.zip', files, [:memory, cwd: to_charlist(pack_dir)])

cache_seconds_per_file = Pleroma.Config.get!([:emoji, :shared_pack_cache_seconds_per_file])
cache_ms = :timer.seconds(cache_seconds_per_file * Enum.count(files))

Cachex.put!(
:emoji_packs_cache,
name,
# if pack.json MD5 changes, the cache is not valid anymore
%{pack_json_md5: md5, pack_data: zip_result},
# Add a minute to cache time for every file in the pack
ttl: cache_ms
)

Logger.debug("Created an archive for the '#{name}' emoji pack, \
keeping it in cache for #{div(cache_ms, 1000)}s")

zip_result
end

defp make_archive(name, pack, pack_dir) do
# Having a different pack.json md5 invalidates cache
pack_file_md5 = :crypto.hash(:md5, File.read!(Path.join(pack_dir, "pack.json")))

case Cachex.get!(:emoji_packs_cache, name) do
%{pack_file_md5: ^pack_file_md5, pack_data: zip_result} ->
Logger.debug("Using cache for the '#{name}' shared emoji pack")
zip_result

_ ->
create_archive_and_cache(name, pack, pack_dir, pack_file_md5)
end
end

@doc """
An endpoint for other instances (via admin UI) or users (via browser)
to download packs that the instance shares.
"""
def download_shared(conn, %{"name" => name}) do
pack_dir = Path.join(emoji_dir_path(), name)
pack_file = Path.join(pack_dir, "pack.json")

with {_, true} <- {:exists?, File.exists?(pack_file)},
pack = Jason.decode!(File.read!(pack_file)),
{_, true} <- {:can_download?, can_download?(pack, pack_dir)} do
zip_result = make_archive(name, pack, pack_dir)
send_download(conn, {:binary, zip_result}, filename: "#{name}.zip")
else
{:can_download?, _} ->
conn
|> put_status(:forbidden)
|> json(%{
error: "Pack #{name} cannot be downloaded from this instance, either pack sharing\
was disabled for this pack or some files are missing"
})

{:exists?, _} ->
conn
|> put_status(:not_found)
|> json(%{error: "Pack #{name} does not exist"})
end
end

defp shareable_packs_available(address) do
"#{address}/.well-known/nodeinfo"
|> Tesla.get!()
|> Map.get(:body)
|> Jason.decode!()
|> Map.get("links")
|> List.last()
|> Map.get("href")
# Get the actual nodeinfo address and fetch it
|> Tesla.get!()
|> Map.get(:body)
|> Jason.decode!()
|> get_in(["metadata", "features"])
|> Enum.member?("shareable_emoji_packs")
end

@doc """
An admin endpoint to request downloading a pack named `pack_name` from the instance
`instance_address`.

If the requested instance's admin chose to share the pack, it will be downloaded
from that instance, otherwise it will be downloaded from the fallback source, if there is one.
"""
def download_from(conn, %{"instance_address" => address, "pack_name" => name} = data) do
address = String.trim(address)

if shareable_packs_available(address) do
full_pack =
"#{address}/api/pleroma/emoji/packs/list"
|> Tesla.get!()
|> Map.get(:body)
|> Jason.decode!()
|> Map.get(name)

pack_info_res =
case full_pack["pack"] do
%{"share-files" => true, "can-download" => true, "download-sha256" => sha} ->
{:ok,
%{
sha: sha,
uri: "#{address}/api/pleroma/emoji/packs/download_shared/#{name}"
}}

%{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) ->
{:ok,
%{
sha: sha,
uri: src,
fallback: true
}}

_ ->
{:error,
"The pack was not set as shared and there is no fallback src to download from"}
end

with {:ok, %{sha: sha, uri: uri} = pinfo} <- pack_info_res,
%{body: emoji_archive} <- Tesla.get!(uri),
{_, true} <- {:checksum, Base.decode16!(sha) == :crypto.hash(:sha256, emoji_archive)} do
local_name = data["as"] || name
pack_dir = Path.join(emoji_dir_path(), local_name)
File.mkdir_p!(pack_dir)

files = Enum.map(full_pack["files"], fn {_, path} -> to_charlist(path) end)
# Fallback cannot contain a pack.json file
files = if pinfo[:fallback], do: files, else: ['pack.json'] ++ files

{:ok, _} = :zip.unzip(emoji_archive, cwd: to_charlist(pack_dir), file_list: files)

# Fallback can't contain a pack.json file, since that would cause the fallback-src-sha256
# in it to depend on itself
if pinfo[:fallback] do
pack_file_path = Path.join(pack_dir, "pack.json")

File.write!(pack_file_path, Jason.encode!(full_pack, pretty: true))
end

json(conn, "ok")
else
{:error, e} ->
conn |> put_status(:internal_server_error) |> json(%{error: e})

{:checksum, _} ->
conn
|> put_status(:internal_server_error)
|> json(%{error: "SHA256 for the pack doesn't match the one sent by the server"})
end
else
conn
|> put_status(:internal_server_error)
|> json(%{error: "The requested instance does not support sharing emoji packs"})
end
end

@doc """
Creates an empty pack named `name` which then can be updated via the admin UI.
"""
def create(conn, %{"name" => name}) do
pack_dir = Path.join(emoji_dir_path(), name)

if not File.exists?(pack_dir) do
File.mkdir_p!(pack_dir)

pack_file_p = Path.join(pack_dir, "pack.json")

File.write!(
pack_file_p,
Jason.encode!(%{pack: %{}, files: %{}}, pretty: true)
)

conn |> json("ok")
else
conn
|> put_status(:conflict)
|> json(%{error: "A pack named \"#{name}\" already exists"})
end
end

@doc """
Deletes the pack `name` and all it's files.
"""
def delete(conn, %{"name" => name}) do
pack_dir = Path.join(emoji_dir_path(), name)

case File.rm_rf(pack_dir) do
{:ok, _} ->
conn |> json("ok")

{:error, _} ->
conn
|> put_status(:internal_server_error)
|> json(%{error: "Couldn't delete the pack #{name}"})
end
end

@doc """
An endpoint to update `pack_names`'s metadata.

`new_data` is the new metadata for the pack, that will replace the old metadata.
"""
def update_metadata(conn, %{"pack_name" => name, "new_data" => new_data}) do
pack_file_p = Path.join([emoji_dir_path(), name, "pack.json"])

full_pack = Jason.decode!(File.read!(pack_file_p))

# The new fallback-src is in the new data and it's not the same as it was in the old data
should_update_fb_sha =
not is_nil(new_data["fallback-src"]) and
new_data["fallback-src"] != full_pack["pack"]["fallback-src"]

with {_, true} <- {:should_update?, should_update_fb_sha},
%{body: pack_arch} <- Tesla.get!(new_data["fallback-src"]),
{:ok, flist} <- :zip.unzip(pack_arch, [:memory]),
{_, true} <- {:has_all_files?, has_all_files?(full_pack, flist)} do
fallback_sha = :crypto.hash(:sha256, pack_arch) |> Base.encode16()

new_data = Map.put(new_data, "fallback-src-sha256", fallback_sha)
update_metadata_and_send(conn, full_pack, new_data, pack_file_p)
else
{:should_update?, _} ->
update_metadata_and_send(conn, full_pack, new_data, pack_file_p)

{:has_all_files?, _} ->
conn
|> put_status(:bad_request)
|> json(%{error: "The fallback archive does not have all files specified in pack.json"})
end
end

# Check if all files from the pack.json are in the archive
defp has_all_files?(%{"files" => files}, flist) do
Enum.all?(files, fn {_, from_manifest} ->
Enum.find(flist, fn {from_archive, _} ->
to_string(from_archive) == from_manifest
end)
end)
end

defp update_metadata_and_send(conn, full_pack, new_data, pack_file_p) do
full_pack = Map.put(full_pack, "pack", new_data)
File.write!(pack_file_p, Jason.encode!(full_pack, pretty: true))

# Send new data back with fallback sha filled
json(conn, new_data)
end

defp get_filename(%{"filename" => filename}), do: filename

defp get_filename(%{"file" => file}) do
case file do
%Plug.Upload{filename: filename} -> filename
url when is_binary(url) -> Path.basename(url)
end
end

defp empty?(str), do: String.trim(str) == ""

defp update_file_and_send(conn, updated_full_pack, pack_file_p) do
# Write the emoji pack file
File.write!(pack_file_p, Jason.encode!(updated_full_pack, pretty: true))

# Return the modified file list
json(conn, updated_full_pack["files"])
end

@doc """
Updates a file in a pack.

Updating can mean three things:

- `add` adds an emoji named `shortcode` to the pack `pack_name`,
that means that the emoji file needs to be uploaded with the request
(thus requiring it to be a multipart request) and be named `file`.
There can also be an optional `filename` that will be the new emoji file name
(if it's not there, the name will be taken from the uploaded file).
- `update` changes emoji shortcode (from `shortcode` to `new_shortcode` or moves the file
(from the current filename to `new_filename`)
- `remove` removes the emoji named `shortcode` and it's associated file
"""

# Add
def update_file(
conn,
%{"pack_name" => pack_name, "action" => "add", "shortcode" => shortcode} = params
) do
pack_dir = Path.join(emoji_dir_path(), pack_name)
pack_file_p = Path.join(pack_dir, "pack.json")

full_pack = Jason.decode!(File.read!(pack_file_p))

with {_, false} <- {:has_shortcode, Map.has_key?(full_pack["files"], shortcode)},
filename <- get_filename(params),
false <- empty?(shortcode),
false <- empty?(filename) do
file_path = Path.join(pack_dir, filename)

# If the name contains directories, create them
if String.contains?(file_path, "/") do
File.mkdir_p!(Path.dirname(file_path))
end

case params["file"] do
%Plug.Upload{path: upload_path} ->
# Copy the uploaded file from the temporary directory
File.copy!(upload_path, file_path)

url when is_binary(url) ->
# Download and write the file
file_contents = Tesla.get!(url).body
File.write!(file_path, file_contents)
end

updated_full_pack = put_in(full_pack, ["files", shortcode], filename)
update_file_and_send(conn, updated_full_pack, pack_file_p)
else
{:has_shortcode, _} ->
conn
|> put_status(:conflict)
|> json(%{error: "An emoji with the \"#{shortcode}\" shortcode already exists"})

true ->
conn
|> put_status(:bad_request)
|> json(%{error: "shortcode or filename cannot be empty"})
end
end

# Remove
def update_file(conn, %{
"pack_name" => pack_name,
"action" => "remove",
"shortcode" => shortcode
}) do
pack_dir = Path.join(emoji_dir_path(), pack_name)
pack_file_p = Path.join(pack_dir, "pack.json")

full_pack = Jason.decode!(File.read!(pack_file_p))

if Map.has_key?(full_pack["files"], shortcode) do
{emoji_file_path, updated_full_pack} = pop_in(full_pack, ["files", shortcode])

emoji_file_path = Path.join(pack_dir, emoji_file_path)

# Delete the emoji file
File.rm!(emoji_file_path)

# If the old directory has no more files, remove it
if String.contains?(emoji_file_path, "/") do
dir = Path.dirname(emoji_file_path)

if Enum.empty?(File.ls!(dir)) do
File.rmdir!(dir)
end
end

update_file_and_send(conn, updated_full_pack, pack_file_p)
else
conn
|> put_status(:bad_request)
|> json(%{error: "Emoji \"#{shortcode}\" does not exist"})
end
end

# Update
def update_file(
conn,
%{"pack_name" => pack_name, "action" => "update", "shortcode" => shortcode} = params
) do
pack_dir = Path.join(emoji_dir_path(), pack_name)
pack_file_p = Path.join(pack_dir, "pack.json")

full_pack = Jason.decode!(File.read!(pack_file_p))

with {_, true} <- {:has_shortcode, Map.has_key?(full_pack["files"], shortcode)},
%{"new_shortcode" => new_shortcode, "new_filename" => new_filename} <- params,
false <- empty?(new_shortcode),
false <- empty?(new_filename) do
# First, remove the old shortcode, saving the old path
{old_emoji_file_path, updated_full_pack} = pop_in(full_pack, ["files", shortcode])
old_emoji_file_path = Path.join(pack_dir, old_emoji_file_path)
new_emoji_file_path = Path.join(pack_dir, new_filename)

# If the name contains directories, create them
if String.contains?(new_emoji_file_path, "/") do
File.mkdir_p!(Path.dirname(new_emoji_file_path))
end

# Move/Rename the old filename to a new filename
# These are probably on the same filesystem, so just rename should work
:ok = File.rename(old_emoji_file_path, new_emoji_file_path)

# If the old directory has no more files, remove it
if String.contains?(old_emoji_file_path, "/") do
dir = Path.dirname(old_emoji_file_path)

if Enum.empty?(File.ls!(dir)) do
File.rmdir!(dir)
end
end

# Then, put in the new shortcode with the new path
updated_full_pack = put_in(updated_full_pack, ["files", new_shortcode], new_filename)
update_file_and_send(conn, updated_full_pack, pack_file_p)
else
{:has_shortcode, _} ->
conn
|> put_status(:bad_request)
|> json(%{error: "Emoji \"#{shortcode}\" does not exist"})

true ->
conn
|> put_status(:bad_request)
|> json(%{error: "new_shortcode or new_filename cannot be empty"})

_ ->
conn
|> put_status(:bad_request)
|> json(%{error: "new_shortcode or new_file were not specified"})
end
end

def update_file(conn, %{"action" => action}) do
conn
|> put_status(:bad_request)
|> json(%{error: "Unknown action: #{action}"})
end

@doc """
Imports emoji from the filesystem.

Importing means checking all the directories in the
`$instance_static/emoji/` for directories which do not have
`pack.json`. If one has an emoji.txt file, that file will be used
to create a `pack.json` file with it's contents. If the directory has
neither, all the files with specific configured extenstions will be
assumed to be emojis and stored in the new `pack.json` file.
"""
def import_from_fs(conn, _params) do
with {:ok, results} <- File.ls(emoji_dir_path()) do
imported_pack_names =
results
|> Enum.filter(fn file ->
dir_path = Path.join(emoji_dir_path(), file)
# Find the directories that do NOT have pack.json
File.dir?(dir_path) and not File.exists?(Path.join(dir_path, "pack.json"))
end)
|> Enum.map(&write_pack_json_contents/1)

json(conn, imported_pack_names)
else
{:error, _} ->
conn
|> put_status(:internal_server_error)
|> json(%{error: "Error accessing emoji pack directory"})
end
end

defp write_pack_json_contents(dir) do
dir_path = Path.join(emoji_dir_path(), dir)
emoji_txt_path = Path.join(dir_path, "emoji.txt")

files_for_pack = files_for_pack(emoji_txt_path, dir_path)
pack_json_contents = Jason.encode!(%{pack: %{}, files: files_for_pack})

File.write!(Path.join(dir_path, "pack.json"), pack_json_contents)

dir
end

defp files_for_pack(emoji_txt_path, dir_path) do
if File.exists?(emoji_txt_path) do
# There's an emoji.txt file, it's likely from a pack installed by the pack manager.
# Make a pack.json file from the contents of that emoji.txt fileh

# FIXME: Copy-pasted from Pleroma.Emoji/load_from_file_stream/2

# Create a map of shortcodes to filenames from emoji.txt
File.read!(emoji_txt_path)
|> String.split("\n")
|> Enum.map(&String.trim/1)
|> Enum.map(fn line ->
case String.split(line, ~r/,\s*/) do
# This matches both strings with and without tags
# and we don't care about tags here
[name, file | _] -> {name, file}
_ -> nil
end
end)
|> Enum.filter(fn x -> not is_nil(x) end)
|> Enum.into(%{})
else
# If there's no emoji.txt, assume all files
# that are of certain extensions from the config are emojis and import them all
pack_extensions = Pleroma.Config.get!([:emoji, :pack_extensions])
Pleroma.Emoji.Loader.make_shortcode_to_file_map(dir_path, pack_extensions)
end
end
end

+ 41
- 0
lib/pleroma/web/pleroma_api/controllers/mascot_controller.ex Vedi File

@@ -0,0 +1,41 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.PleromaAPI.MascotController do
use Pleroma.Web, :controller

alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub

plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action == :show)
plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action != :show)

plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)

@doc "GET /api/v1/pleroma/mascot"
def show(%{assigns: %{user: user}} = conn, _params) do
json(conn, User.get_mascot(user))
end

@doc "PUT /api/v1/pleroma/mascot"
def update(%{assigns: %{user: user}} = conn, %{"file" => file}) do
with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
# Reject if not an image
%{type: "image"} = attachment <- render_attachment(object) do
# Sure!
# Save to the user's info
{:ok, _user} = User.update_info(user, &User.Info.mascot_update(&1, attachment))

json(conn, attachment)
else
%{type: _} -> render_error(conn, :unsupported_media_type, "mascots can only be images")
end
end

defp render_attachment(object) do
attachment_data = Map.put(object.data, "id", object.id)
Pleroma.Web.MastodonAPI.StatusView.render("attachment.json", %{attachment: attachment_data})
end
end

lib/pleroma/web/pleroma_api/pleroma_api_controller.ex → lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex Vedi File

@@ -22,11 +22,13 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do

plug(
OAuthScopesPlug,
%{scopes: ["write:conversations"]} when action in [:conversations, :conversation_read]
%{scopes: ["write:conversations"]} when action == :update_conversation
)

plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :read_notification)

plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)

def conversation(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
with %Participation{} = participation <- Participation.get(participation_id),
true <- user.id == participation.user_id do

+ 58
- 0
lib/pleroma/web/pleroma_api/controllers/scrobble_controller.ex Vedi File

@@ -0,0 +1,58 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.PleromaAPI.ScrobbleController do
use Pleroma.Web, :controller

import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, fetch_integer_param: 2]

alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.StatusView

plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :user_scrobbles)
plug(OAuthScopesPlug, %{scopes: ["write"]} when action != :user_scrobbles)

plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)

def new_scrobble(%{assigns: %{user: user}} = conn, %{"title" => _} = params) do
params =
if !params["length"] do
params
else
params
|> Map.put("length", fetch_integer_param(params, "length"))
end

with {:ok, activity} <- CommonAPI.listen(user, params) do
conn
|> put_view(StatusView)
|> render("listen.json", %{activity: activity, for: user})
else
{:error, message} ->
conn
|> put_status(:bad_request)
|> json(%{"error" => message})
end
end

def user_scrobbles(%{assigns: %{user: reading_user}} = conn, params) do
with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
params = Map.put(params, "type", ["Listen"])

activities = ActivityPub.fetch_user_abstract_activities(user, reading_user, params)

conn
|> add_link_headers(activities)
|> put_view(StatusView)
|> render("listens.json", %{
activities: activities,
for: reading_user,
as: :activity
})
end
end
end

+ 1
- 1
lib/pleroma/web/push/subscription.ex Vedi File

@@ -15,7 +15,7 @@ defmodule Pleroma.Web.Push.Subscription do
@type t :: %__MODULE__{}

schema "push_subscriptions" do
belongs_to(:user, User, type: Pleroma.FlakeId)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:token, Token)
field(:endpoint, :string)
field(:key_p256dh, :string)


+ 132
- 95
lib/pleroma/web/router.ex Vedi File

@@ -161,6 +161,7 @@ defmodule Pleroma.Web.Router do
post("/users/email_invite", AdminAPIController, :email_invite)

get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
patch("/users/:nickname/force_password_reset", AdminAPIController, :force_password_reset)

get("/users", AdminAPIController, :list_users)
get("/users/:nickname", AdminAPIController, :user_show)
@@ -180,6 +181,30 @@ defmodule Pleroma.Web.Router do
get("/config/migrate_from_db", AdminAPIController, :migrate_from_db)

get("/moderation_log", AdminAPIController, :list_log)

post("/reload_emoji", AdminAPIController, :reload_emoji)
end

scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do
scope "/packs" do
# Modifying packs
pipe_through(:admin_api)

post("/import_from_fs", EmojiAPIController, :import_from_fs)

post("/:pack_name/update_file", EmojiAPIController, :update_file)
post("/:pack_name/update_metadata", EmojiAPIController, :update_metadata)
put("/:name", EmojiAPIController, :create)
delete("/:name", EmojiAPIController, :delete)
post("/download_from", EmojiAPIController, :download_from)
post("/list_from", EmojiAPIController, :list_from)
end

scope "/packs" do
# Pack info / downloading
get("/", EmojiAPIController, :list_packs)
get("/:name/download_shared/", EmojiAPIController, :download_shared)
end
end

scope "/", Pleroma.Web.TwitterAPI do
@@ -225,85 +250,112 @@ defmodule Pleroma.Web.Router do
end

scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:authenticated_api)
scope [] do
pipe_through(:authenticated_api)

get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
get("/conversations/:id", PleromaAPIController, :conversation)
end

scope [] do
pipe_through(:authenticated_api)

patch("/conversations/:id", PleromaAPIController, :update_conversation)
post("/notifications/read", PleromaAPIController, :read_notification)

patch("/accounts/update_avatar", AccountController, :update_avatar)
patch("/accounts/update_banner", AccountController, :update_banner)
patch("/accounts/update_background", AccountController, :update_background)

get("/mascot", MascotController, :show)
put("/mascot", MascotController, :update)

post("/scrobble", ScrobbleController, :new_scrobble)
end

scope [] do
pipe_through(:api)
get("/accounts/:id/favourites", AccountController, :favourites)
end

scope [] do
pipe_through(:authenticated_api)

get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
get("/conversations/:id", PleromaAPIController, :conversation)
post("/accounts/:id/subscribe", AccountController, :subscribe)
post("/accounts/:id/unsubscribe", AccountController, :unsubscribe)
end

patch("/conversations/:id", PleromaAPIController, :update_conversation)
post("/notifications/read", PleromaAPIController, :read_notification)
post("/accounts/confirmation_resend", AccountController, :confirmation_resend)
end

scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:api)

get("/accounts/:id/scrobbles", ScrobbleController, :user_scrobbles)
end

scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:authenticated_api)

get("/accounts/verify_credentials", MastodonAPIController, :verify_credentials)
get("/accounts/verify_credentials", AccountController, :verify_credentials)

get("/accounts/relationships", MastodonAPIController, :relationships)
get("/accounts/relationships", AccountController, :relationships)

get("/accounts/:id/lists", MastodonAPIController, :account_lists)
get("/accounts/:id/lists", AccountController, :lists)
get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array)

get("/follow_requests", MastodonAPIController, :follow_requests)
get("/follow_requests", FollowRequestController, :index)
get("/blocks", MastodonAPIController, :blocks)
get("/mutes", MastodonAPIController, :mutes)

get("/timelines/home", MastodonAPIController, :home_timeline)
get("/timelines/direct", MastodonAPIController, :dm_timeline)
get("/timelines/home", TimelineController, :home)
get("/timelines/direct", TimelineController, :direct)

get("/favourites", MastodonAPIController, :favourites)
# Note: not present in Mastodon: bookmarks
get("/bookmarks", MastodonAPIController, :bookmarks)

post("/notifications/clear", MastodonAPIController, :clear_notifications)
post("/notifications/dismiss", MastodonAPIController, :dismiss_notification)
get("/notifications", MastodonAPIController, :notifications)
get("/notifications/:id", MastodonAPIController, :get_notification)
get("/notifications", NotificationController, :index)
get("/notifications/:id", NotificationController, :show)
post("/notifications/clear", NotificationController, :clear)
post("/notifications/dismiss", NotificationController, :dismiss)
delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple)

delete(
"/notifications/destroy_multiple",
MastodonAPIController,
:destroy_multiple_notifications
)

get("/scheduled_statuses", MastodonAPIController, :scheduled_statuses)
get("/scheduled_statuses/:id", MastodonAPIController, :show_scheduled_status)
get("/scheduled_statuses", ScheduledActivityController, :index)
get("/scheduled_statuses/:id", ScheduledActivityController, :show)

get("/lists", ListController, :index)
get("/lists/:id", ListController, :show)
get("/lists/:id/accounts", ListController, :list_accounts)

get("/domain_blocks", MastodonAPIController, :domain_blocks)
get("/domain_blocks", DomainBlockController, :index)

get("/filters", MastodonAPIController, :get_filters)
get("/filters", FilterController, :index)

get("/suggestions", MastodonAPIController, :suggestions)

get("/conversations", MastodonAPIController, :conversations)
post("/conversations/:id/read", MastodonAPIController, :conversation_read)
get("/conversations", ConversationController, :index)
post("/conversations/:id/read", ConversationController, :read)

get("/endorsements", MastodonAPIController, :endorsements)
get("/endorsements", AccountController, :endorsements)

patch("/accounts/update_credentials", MastodonAPIController, :update_credentials)
patch("/accounts/update_credentials", AccountController, :update_credentials)

post("/statuses", MastodonAPIController, :post_status)
delete("/statuses/:id", MastodonAPIController, :delete_status)
post("/statuses", StatusController, :create)
delete("/statuses/:id", StatusController, :delete)

post("/statuses/:id/reblog", MastodonAPIController, :reblog_status)
post("/statuses/:id/unreblog", MastodonAPIController, :unreblog_status)
post("/statuses/:id/favourite", MastodonAPIController, :fav_status)
post("/statuses/:id/unfavourite", MastodonAPIController, :unfav_status)
post("/statuses/:id/pin", MastodonAPIController, :pin_status)
post("/statuses/:id/unpin", MastodonAPIController, :unpin_status)
# Note: not present in Mastodon: bookmark
post("/statuses/:id/bookmark", MastodonAPIController, :bookmark_status)
# Note: not present in Mastodon: unbookmark
post("/statuses/:id/unbookmark", MastodonAPIController, :unbookmark_status)
post("/statuses/:id/mute", MastodonAPIController, :mute_conversation)
post("/statuses/:id/unmute", MastodonAPIController, :unmute_conversation)
post("/statuses/:id/reblog", StatusController, :reblog)
post("/statuses/:id/unreblog", StatusController, :unreblog)
post("/statuses/:id/favourite", StatusController, :favourite)
post("/statuses/:id/unfavourite", StatusController, :unfavourite)
post("/statuses/:id/pin", StatusController, :pin)
post("/statuses/:id/unpin", StatusController, :unpin)
post("/statuses/:id/bookmark", StatusController, :bookmark)
post("/statuses/:id/unbookmark", StatusController, :unbookmark)
post("/statuses/:id/mute", StatusController, :mute_conversation)
post("/statuses/:id/unmute", StatusController, :unmute_conversation)

put("/scheduled_statuses/:id", MastodonAPIController, :update_scheduled_status)
delete("/scheduled_statuses/:id", MastodonAPIController, :delete_scheduled_status)
put("/scheduled_statuses/:id", ScheduledActivityController, :update)
delete("/scheduled_statuses/:id", ScheduledActivityController, :delete)

post("/polls/:id/votes", MastodonAPIController, :poll_vote)

@@ -317,37 +369,28 @@ defmodule Pleroma.Web.Router do
post("/lists/:id/accounts", ListController, :add_to_list)
delete("/lists/:id/accounts", ListController, :remove_from_list)

post("/filters", MastodonAPIController, :create_filter)
get("/filters/:id", MastodonAPIController, :get_filter)
put("/filters/:id", MastodonAPIController, :update_filter)
delete("/filters/:id", MastodonAPIController, :delete_filter)

patch("/pleroma/accounts/update_avatar", MastodonAPIController, :update_avatar)
patch("/pleroma/accounts/update_banner", MastodonAPIController, :update_banner)
patch("/pleroma/accounts/update_background", MastodonAPIController, :update_background)

get("/pleroma/mascot", MastodonAPIController, :get_mascot)
put("/pleroma/mascot", MastodonAPIController, :set_mascot)
post("/filters", FilterController, :create)
get("/filters/:id", FilterController, :show)
put("/filters/:id", FilterController, :update)
delete("/filters/:id", FilterController, :delete)

post("/reports", MastodonAPIController, :create_report)
post("/reports", ReportController, :create)

post("/follows", MastodonAPIController, :follow)
post("/accounts/:id/follow", MastodonAPIController, :follow)
# To do: POST /api/v1/follows is not present in Mastodon - consider removing
post("/follows", MastodonAPIController, :follows)

post("/accounts/:id/unfollow", MastodonAPIController, :unfollow)
post("/accounts/:id/block", MastodonAPIController, :block)
post("/accounts/:id/unblock", MastodonAPIController, :unblock)
post("/accounts/:id/mute", MastodonAPIController, :mute)
post("/accounts/:id/unmute", MastodonAPIController, :unmute)
post("/accounts/:id/follow", AccountController, :follow)
post("/accounts/:id/unfollow", AccountController, :unfollow)
post("/accounts/:id/block", AccountController, :block)
post("/accounts/:id/unblock", AccountController, :unblock)
post("/accounts/:id/mute", AccountController, :mute)
post("/accounts/:id/unmute", AccountController, :unmute)

post("/follow_requests/:id/authorize", MastodonAPIController, :authorize_follow_request)
post("/follow_requests/:id/reject", MastodonAPIController, :reject_follow_request)
post("/follow_requests/:id/authorize", FollowRequestController, :authorize)
post("/follow_requests/:id/reject", FollowRequestController, :reject)

post("/domain_blocks", MastodonAPIController, :block_domain)
delete("/domain_blocks", MastodonAPIController, :unblock_domain)

post("/pleroma/accounts/:id/subscribe", MastodonAPIController, :subscribe)
post("/pleroma/accounts/:id/unsubscribe", MastodonAPIController, :unsubscribe)
post("/domain_blocks", DomainBlockController, :create)
delete("/domain_blocks", DomainBlockController, :delete)

post("/push/subscription", SubscriptionController, :create)
get("/push/subscription", SubscriptionController, :get)
@@ -364,7 +407,7 @@ defmodule Pleroma.Web.Router do
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:api)

post("/accounts", MastodonAPIController, :account_register)
post("/accounts", AccountController, :create)

get("/instance", MastodonAPIController, :masto_instance)
get("/instance/peers", MastodonAPIController, :peers)
@@ -372,39 +415,31 @@ defmodule Pleroma.Web.Router do
get("/apps/verify_credentials", MastodonAPIController, :verify_app_credentials)
get("/custom_emojis", MastodonAPIController, :custom_emojis)

get("/statuses/:id/card", MastodonAPIController, :status_card)
get("/statuses/:id/card", StatusController, :card)

get("/statuses/:id/favourited_by", MastodonAPIController, :favourited_by)
get("/statuses/:id/reblogged_by", MastodonAPIController, :reblogged_by)
get("/statuses/:id/favourited_by", StatusController, :favourited_by)
get("/statuses/:id/reblogged_by", StatusController, :reblogged_by)

get("/trends", MastodonAPIController, :empty_array)

get("/accounts/search", SearchController, :account_search)

post(
"/pleroma/accounts/confirmation_resend",
MastodonAPIController,
:account_confirmation_resend
)

get("/timelines/public", MastodonAPIController, :public_timeline)
get("/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline)
get("/timelines/list/:list_id", MastodonAPIController, :list_timeline)
get("/timelines/public", TimelineController, :public)
get("/timelines/tag/:tag", TimelineController, :hashtag)
get("/timelines/list/:list_id", TimelineController, :list)

get("/statuses", MastodonAPIController, :get_statuses)
get("/statuses/:id", MastodonAPIController, :get_status)
get("/statuses/:id/context", MastodonAPIController, :get_context)
get("/statuses", StatusController, :index)
get("/statuses/:id", StatusController, :show)
get("/statuses/:id/context", StatusController, :context)

get("/polls/:id", MastodonAPIController, :get_poll)

get("/accounts/:id/statuses", MastodonAPIController, :user_statuses)
get("/accounts/:id/followers", MastodonAPIController, :followers)
get("/accounts/:id/following", MastodonAPIController, :following)
get("/accounts/:id", MastodonAPIController, :user)
get("/accounts/:id/statuses", AccountController, :statuses)
get("/accounts/:id/followers", AccountController, :followers)
get("/accounts/:id/following", AccountController, :following)
get("/accounts/:id", AccountController, :show)

get("/search", SearchController, :search)

get("/pleroma/accounts/:id/favourites", MastodonAPIController, :user_favourites)
end

scope "/api/v2", Pleroma.Web.MastodonAPI do
@@ -506,14 +541,16 @@ defmodule Pleroma.Web.Router do

get("/api/ap/whoami", ActivityPubController, :whoami)
get("/users/:nickname/inbox", ActivityPubController, :read_inbox)

post("/users/:nickname/outbox", ActivityPubController, :update_outbox)
post("/api/ap/upload_media", ActivityPubController, :upload_media)

get("/users/:nickname/followers", ActivityPubController, :followers)
get("/users/:nickname/following", ActivityPubController, :following)
end

scope "/", Pleroma.Web.ActivityPub do
pipe_through(:activitypub)

post("/inbox", ActivityPubController, :inbox)
post("/users/:nickname/inbox", ActivityPubController, :inbox)
end


+ 2
- 4
lib/pleroma/web/twitter_api/controllers/util_controller.ex Vedi File

@@ -260,11 +260,9 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do

def emoji(conn, _params) do
emoji =
Emoji.get_all()
|> Enum.map(fn {short_code, path, tags} ->
{short_code, %{image_url: path, tags: tags}}
Enum.reduce(Emoji.get_all(), %{}, fn {code, %Emoji{file: file, tags: tags}}, acc ->
Map.put(acc, code, %{image_url: file, tags: tags})
end)
|> Enum.into(%{})

json(conn, emoji)
end


+ 1
- 1
lib/pleroma/web/twitter_api/twitter_api.ex Vedi File

@@ -29,7 +29,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
captcha_enabled = Pleroma.Config.get([Pleroma.Captcha, :enabled])
# true if captcha is disabled or enabled and valid, false otherwise
captcha_ok =
if !captcha_enabled do
if not captcha_enabled do
:ok
else
Pleroma.Captcha.validate(


+ 8
- 10
lib/pleroma/web/twitter_api/twitter_api_controller.ex Vedi File

@@ -5,7 +5,6 @@
defmodule Pleroma.Web.TwitterAPI.Controller do
use Pleroma.Web, :controller

alias Ecto.Changeset
alias Pleroma.Notification
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
@@ -16,18 +15,17 @@ defmodule Pleroma.Web.TwitterAPI.Controller do

plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :notifications_read)

plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)

action_fallback(:errors)

def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
with %User{} = user <- User.get_cached_by_id(uid),
true <- user.local,
true <- user.info.confirmation_pending,
true <- user.info.confirmation_token == token,
info_change <- User.Info.confirmation_changeset(user.info, need_confirmation: false),
changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_change),
{:ok, _} <- User.update_and_set_cache(changeset) do
conn
|> redirect(to: "/")
new_info = [need_confirmation: false]

with %User{info: info} = user <- User.get_cached_by_id(uid),
true <- user.local and info.confirmation_pending and info.confirmation_token == token,
{:ok, _} <- User.update_info(user, &User.Info.confirmation_changeset(&1, new_info)) do
redirect(conn, to: "/")
end
end



+ 2
- 2
lib/pleroma/web/views/streamer_view.ex Vedi File

@@ -16,7 +16,7 @@ defmodule Pleroma.Web.StreamerView do
event: "update",
payload:
Pleroma.Web.MastodonAPI.StatusView.render(
"status.json",
"show.json",
activity: activity,
for: user
)
@@ -43,7 +43,7 @@ defmodule Pleroma.Web.StreamerView do
event: "update",
payload:
Pleroma.Web.MastodonAPI.StatusView.render(
"status.json",
"show.json",
activity: activity
)
|> Jason.encode!()


+ 1
- 1
lib/pleroma/web/websub/websub_client_subscription.ex Vedi File

@@ -13,7 +13,7 @@ defmodule Pleroma.Web.Websub.WebsubClientSubscription do
field(:state, :string)
field(:subscribers, {:array, :string}, default: [])
field(:hub, :string)
belongs_to(:user, User, type: Pleroma.FlakeId)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)

timestamps()
end


+ 5
- 0
lib/pleroma/workers/background_worker.ex Vedi File

@@ -26,6 +26,11 @@ defmodule Pleroma.Workers.BackgroundWorker do
User.perform(:delete, user)
end

def perform(%{"op" => "force_password_reset", "user_id" => user_id}, _job) do
user = User.get_cached_by_id(user_id)
User.perform(:force_password_reset, user)
end

def perform(
%{
"op" => "blocks_import",


Dato che sono stati cambiati molti file in questo diff, alcuni di essi non verranno mostrati

Loading…
Annulla
Salva