Merge develop

This commit is contained in:
Roman Chvanikov 2019-04-30 20:17:52 +07:00
commit 0f0cc2703b
243 changed files with 2062 additions and 657 deletions

1
.gitignore vendored
View file

@ -3,7 +3,6 @@
/db /db
/deps /deps
/*.ez /*.ez
/uploads
/test/uploads /test/uploads
/.elixir_ls /.elixir_ls
/test/fixtures/test_tmp.txt /test/fixtures/test_tmp.txt

View file

@ -16,14 +16,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Configuration: `link_name` option - Configuration: `link_name` option
- Configuration: `fetch_initial_posts` option - Configuration: `fetch_initial_posts` option
- Configuration: `notify_email` option - Configuration: `notify_email` option
- Pleroma API: User subscribtions - Configuration: Media proxy `whitelist` option
- Pleroma API: User subscriptions
- Pleroma API: Healthcheck endpoint
- Admin API: Endpoints for listing/revoking invite tokens - Admin API: Endpoints for listing/revoking invite tokens
- Admin API: Endpoints for making users follow/unfollow each other - Admin API: Endpoints for making users follow/unfollow each other
- Mastodon API: [Scheduled statuses](https://docs.joinmastodon.org/api/rest/scheduled-statuses/) - Mastodon API: [Scheduled statuses](https://docs.joinmastodon.org/api/rest/scheduled-statuses/)
- Mastodon API: `/api/v1/notifications/destroy_multiple` (glitch-soc extension) - Mastodon API: `/api/v1/notifications/destroy_multiple` (glitch-soc extension)
- Mastodon API: `/api/v1/pleroma/accounts/:id/favourites` (API extension)
- Mastodon API: [Reports](https://docs.joinmastodon.org/api/rest/reports/) - Mastodon API: [Reports](https://docs.joinmastodon.org/api/rest/reports/)
- ActivityPub C2S: OAuth endpoints - ActivityPub C2S: OAuth endpoints
- Metadata RelMe provider - Metadata RelMe provider
- Emoji packs and emoji pack manager
### Changed ### Changed
- **Breaking:** Configuration: move from Pleroma.Mailer to Pleroma.Emails.Mailer - **Breaking:** Configuration: move from Pleroma.Mailer to Pleroma.Emails.Mailer
@ -37,11 +41,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Configuration: Dedupe enabled by default - Configuration: Dedupe enabled by default
- Configuration: Added `extra_cookie_attrs` for setting non-standard cookie attributes. Defaults to ["SameSite=Lax"] so that remote follows work. - Configuration: Added `extra_cookie_attrs` for setting non-standard cookie attributes. Defaults to ["SameSite=Lax"] so that remote follows work.
- Pleroma API: Support for emoji tags in `/api/pleroma/emoji` resulting in a breaking API change - Pleroma API: Support for emoji tags in `/api/pleroma/emoji` resulting in a breaking API change
- Timelines: Messages involving people you have blocked will be excluded from the timeline in all cases instead of just repeats.
- Mastodon API: Support for `exclude_types`, `limit` and `min_id` in `/api/v1/notifications` - Mastodon API: Support for `exclude_types`, `limit` and `min_id` in `/api/v1/notifications`
- Mastodon API: Add `languages` and `registrations` to `/api/v1/instance` - Mastodon API: Add `languages` and `registrations` to `/api/v1/instance`
- Mastodon API: Provide plaintext versions of cw/content in the Status entity - Mastodon API: Provide plaintext versions of cw/content in the Status entity
- Mastodon API: Add `pleroma.conversation_id` field to the Status entity - Mastodon API: Add `pleroma.conversation_id`, `pleroma.in_reply_to_account_acct` fields to the Status entity
- Mastodon API: Add `pleroma.tags`, `pleroma.relationship{}`, `pleroma.is_moderator`, `pleroma.is_admin`, `pleroma.confirmation_pending` fields to the User entity - Mastodon API: Add `pleroma.tags`, `pleroma.relationship{}`, `pleroma.is_moderator`, `pleroma.is_admin`, `pleroma.confirmation_pending`, `pleroma.hide_followers`, `pleroma.hide_follows`, `pleroma.hide_favorites` fields to the User entity
- Mastodon API: Add `pleroma.show_role`, `pleroma.no_rich_text` fields to the Source subentity
- Mastodon API: Add support for updating `no_rich_text`, `hide_followers`, `hide_follows`, `hide_favorites`, `show_role` in `PATCH /api/v1/update_credentials`
- Mastodon API: Add `pleroma.is_seen` to the Notification entity - Mastodon API: Add `pleroma.is_seen` to the Notification entity
- Mastodon API: Add `pleroma.local` to the Status entity - Mastodon API: Add `pleroma.local` to the Status entity
- Mastodon API: Add `preview` parameter to `POST /api/v1/statuses` - Mastodon API: Add `preview` parameter to `POST /api/v1/statuses`
@ -50,6 +57,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: Remove attachment limit in the Status entity - Mastodon API: Remove attachment limit in the Status entity
- Deps: Updated Cowboy to 2.6 - Deps: Updated Cowboy to 2.6
- Deps: Updated Ecto to 3.0.7 - Deps: Updated Ecto to 3.0.7
- Don't ship finmoji by default, they can be installed as an emoji pack
- Mastodon API: Added support max_id & since_id for bookmark timeline endpoints.
### Fixed ### Fixed
- Followers counter not being updated when a follower is blocked - Followers counter not being updated when a follower is blocked
@ -64,13 +73,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Federation: Cope with missing or explicitly nulled address lists - Federation: Cope with missing or explicitly nulled address lists
- Federation: Explicitly ensure activities addressed to `as:Public` become addressed to the followers collection - Federation: Explicitly ensure activities addressed to `as:Public` become addressed to the followers collection
- Federation: Better cope with actors which do not declare a followers collection and use `as:Public` with these semantics - Federation: Better cope with actors which do not declare a followers collection and use `as:Public` with these semantics
- Federation: Follow requests from remote users who have been blocked will be automatically rejected if appropriate
- MediaProxy: Parse name from content disposition headers even for non-whitelisted types - MediaProxy: Parse name from content disposition headers even for non-whitelisted types
- MediaProxy: S3 link encoding - MediaProxy: S3 link encoding
- Rich Media: Reject any data which cannot be explicitly encoded into JSON - Rich Media: Reject any data which cannot be explicitly encoded into JSON
- Pleroma API: Importing follows from Mastodon 2.8+
- Twitter API: Exposing default scope, `no_rich_text` of the user to anyone
- Twitter API: Returning the `role` object in user entity despite `show_role = false`
- Mastodon API: `/api/v1/favourites` serving only public activities - Mastodon API: `/api/v1/favourites` serving only public activities
- Mastodon API: Reblogs having `in_reply_to_id` - `null` even when they are replies - Mastodon API: Reblogs having `in_reply_to_id` - `null` even when they are replies
- Mastodon API: Streaming API broadcasting wrong activity id - Mastodon API: Streaming API broadcasting wrong activity id
- Mastodon API: 500 errors when requesting a card for a private conversation - Mastodon API: 500 errors when requesting a card for a private conversation
- Mastodon API: Handling of `reblogs` in `/api/v1/accounts/:id/follow`
- Mastodon API: Correct `reblogged`, `favourited`, and `bookmarked` values in the reblog status JSON
- Mastodon API: Exposing default scope of the user to anyone
## [0.9.9999] - 2019-04-05 ## [0.9.9999] - 2019-04-05
### Security ### Security

View file

@ -39,10 +39,3 @@ does not include the right to compile photos from Unsplash to replicate
a similar or competing service. a similar or competing service.
priv/static/images/city.jpg priv/static/images/city.jpg
---
The files present under the priv/static/finmoji directory are copyright
Finland <https://finland.fi/emoji/>, and are distributed under the Creative
Commons Attribution-NonCommercial-NoDerivatives 4.0 International license, you
should have received a copy of the license file as CC-BY-NC-ND-4.0.

View file

@ -100,9 +100,9 @@
shortcode_globs: ["/emoji/custom/**/*.png"], shortcode_globs: ["/emoji/custom/**/*.png"],
groups: [ groups: [
# Put groups that have higher priority than defaults here. Example in `docs/config/custom_emoji.md` # Put groups that have higher priority than defaults here. Example in `docs/config/custom_emoji.md`
Finmoji: "/finmoji/128px/*-128.png", Custom: ["/emoji/*.png", "/emoji/**/*.png"]
Custom: ["/emoji/*.png", "/emoji/custom/*.png"] ],
] default_manifest: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json"
config :pleroma, :uri_schemes, config :pleroma, :uri_schemes,
valid_schemes: [ valid_schemes: [
@ -221,9 +221,9 @@
allowed_post_formats: [ allowed_post_formats: [
"text/plain", "text/plain",
"text/html", "text/html",
"text/markdown" "text/markdown",
"text/bbcode"
], ],
finmoji_enabled: true,
mrf_transparency: true, mrf_transparency: true,
autofollowed_nicknames: [], autofollowed_nicknames: [],
max_pinned_statuses: 1, max_pinned_statuses: 1,
@ -231,7 +231,8 @@
welcome_user_nickname: nil, welcome_user_nickname: nil,
welcome_message: nil, welcome_message: nil,
max_report_comment_size: 1000, max_report_comment_size: 1000,
safe_dm_mentions: false safe_dm_mentions: false,
healthcheck: false
config :pleroma, :markup, config :pleroma, :markup,
# XXX - unfortunately, inline images must be enabled by default right now, because # XXX - unfortunately, inline images must be enabled by default right now, because
@ -326,7 +327,8 @@
follow_redirect: true, follow_redirect: true,
pool: :media pool: :media
] ]
] ],
whitelist: []
config :pleroma, :chat, enabled: true config :pleroma, :chat, enabled: true

View file

@ -20,6 +20,7 @@ Has these additional fields under the `pleroma` object:
- `local`: true if the post was made on the local instance. - `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 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` - `content`: a map consisting of alternate representations of the `content` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`
- `spoiler_text`: a map consisting of alternate representations of the `spoiler_text` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain` - `spoiler_text`: a map consisting of alternate representations of the `spoiler_text` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`
@ -37,9 +38,18 @@ Has these additional fields under the `pleroma` object:
- `tags`: Lists an array of tags for the user - `tags`: Lists an array of tags for the user
- `relationship{}`: Includes fields as documented for Mastodon API https://docs.joinmastodon.org/api/entities/#relationship - `relationship{}`: Includes fields as documented for Mastodon API https://docs.joinmastodon.org/api/entities/#relationship
- `is_moderator`: boolean, true if user is a moderator - `is_moderator`: boolean, nullable, true if user is a moderator
- `is_admin`: boolean, true if user is an admin - `is_admin`: boolean, nullable, true if user is an admin
- `confirmation_pending`: boolean, true if a new user account is waiting on email confirmation to be activated - `confirmation_pending`: boolean, true if a new user account is waiting on email confirmation to be activated
- `hide_followers`: boolean, true when the user has follower hiding enabled
- `hide_follows`: boolean, true when the user has follow hiding enabled
### Source
Has these additional fields under the `pleroma` object:
- `show_role`: boolean, nullable, true when the user wants his role (e.g admin, moderator) to be shown
- `no_rich_text` - boolean, nullable, true when html tags are stripped from all statuses requested from the API
## Account Search ## Account Search
@ -58,3 +68,15 @@ Has these additional fields under the `pleroma` object:
Additional parameters can be added to the JSON body/Form data: Additional parameters can be added to the JSON body/Form data:
- `preview`: boolean, if set to `true` the post won't be actually posted, but the status entitiy would still be rendered back. This could be useful for previewing rich text/custom emoji, for example. - `preview`: boolean, if set to `true` the post won't be actually posted, but the status entitiy would still be rendered back. This could be useful for previewing rich text/custom emoji, for example.
- `content_type`: string, contain the MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint.
## PATCH `/api/v1/update_credentials`
Additional parameters can be added to the JSON body/Form data:
- `no_rich_text` - if true, html tags are stripped from all statuses requested from the API
- `hide_followers` - if true, user's followers will be hidden
- `hide_follows` - if true, user's follows will be hidden
- `hide_favorites` - if true, user's favorites timeline will be hidden
- `show_role` - if true, user's role (e.g admin, moderator) will be exposed to anyone in the API
- `default_scope` - the scope returned under `privacy` key in Source subentity

View file

@ -77,7 +77,7 @@ Request parameters can be passed via [query strings](https://en.wikipedia.org/wi
* `token`: invite token required when the registrations aren't public. * `token`: invite token required when the registrations aren't public.
* Response: JSON. Returns a user object on success, otherwise returns `{"error": "error_msg"}` * Response: JSON. Returns a user object on success, otherwise returns `{"error": "error_msg"}`
* Example response: * Example response:
``` ```json
{ {
"background_image": null, "background_image": null,
"cover_photo": "https://pleroma.soykaf.com/images/banner.png", "cover_photo": "https://pleroma.soykaf.com/images/banner.png",
@ -187,6 +187,62 @@ See [Admin-API](Admin-API.md)
} }
``` ```
## `/api/v1/pleroma/accounts/:id/favourites`
### Returns favorites timeline of any user
* Method `GET`
* Authentication: not required
* Params:
* `id`: the id of the account for whom to return results
* `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
* Response: JSON, returns a list of Mastodon Status entities on success, otherwise returns `{"error": "error_msg"}`
* Example response:
```json
[
{
"account": {
"id": "9hptFmUF3ztxYh3Svg",
"url": "https://pleroma.example.org/users/nick2",
"username": "nick2",
...
},
"application": {"name": "Web", "website": null},
"bookmarked": false,
"card": null,
"content": "This is :moominmamma: note 0",
"created_at": "2019-04-15T15:42:15.000Z",
"emojis": [],
"favourited": false,
"favourites_count": 1,
"id": "9hptFmVJ02khbzYJaS",
"in_reply_to_account_id": null,
"in_reply_to_id": null,
"language": null,
"media_attachments": [],
"mentions": [],
"muted": false,
"pinned": false,
"pleroma": {
"content": {"text/plain": "This is :moominmamma: note 0"},
"conversation_id": 13679,
"local": true,
"spoiler_text": {"text/plain": "2hu"}
},
"reblog": null,
"reblogged": false,
"reblogs_count": 0,
"replies_count": 0,
"sensitive": false,
"spoiler_text": "2hu",
"tags": [{"name": "2hu", "url": "/tag/2hu"}],
"uri": "https://pleroma.example.org/objects/198ed2a1-7912-4482-b559-244a0369e984",
"url": "https://pleroma.example.org/notice/9hptFmVJ02khbzYJaS",
"visibility": "public"
}
]
```
## `/api/pleroma/notification_settings` ## `/api/pleroma/notification_settings`
### Updates user notification settings ### Updates user notification settings
* Method `PUT` * Method `PUT`
@ -197,3 +253,20 @@ See [Admin-API](Admin-API.md)
* `remote`: BOOLEAN field, receives notifications from people on remote instances * `remote`: BOOLEAN field, receives notifications from people on remote instances
* `local`: BOOLEAN field, receives notifications from people on the local instance * `local`: BOOLEAN field, receives notifications from people on the local instance
* Response: JSON. Returns `{"status": "success"}` if the update was successful, otherwise returns `{"error": "error_msg"}` * Response: JSON. Returns `{"status": "success"}` if the update was successful, otherwise returns `{"error": "error_msg"}`
## `/api/pleroma/healthcheck`
### Healthcheck endpoint with additional system data.
* Method `GET`
* Authentication: not required
* Params: none
* Response: JSON, statuses (200 - healthy, 503 unhealthy).
* Example response:
```json
{
"pool_size": 0, # database connection pool
"active": 0, # active processes
"idle": 0, # idle processes
"memory_used": 0.00, # Memory used
"healthy": true # Instance state
}
```

View file

@ -87,7 +87,6 @@ config :pleroma, Pleroma.Emails.Mailer,
* `quarantined_instances`: List of ActivityPub instances where private(DMs, followers-only) activities will not be send. * `quarantined_instances`: List of ActivityPub instances where private(DMs, followers-only) activities will not be send.
* `managed_config`: Whenether the config for pleroma-fe is configured in this config or in ``static/config.json`` * `managed_config`: Whenether the config for pleroma-fe is configured in this config or in ``static/config.json``
* `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML) * `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML)
* `finmoji_enabled`: Whenether to enable the finmojis in the custom emojis.
* `mrf_transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo). * `mrf_transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo).
* `scope_copy`: Copy the scope (private/unlisted/public) in replies to posts by default. * `scope_copy`: Copy the scope (private/unlisted/public) in replies to posts by default.
* `subject_line_behavior`: Allows changing the default behaviour of subject lines in replies. Valid values: * `subject_line_behavior`: Allows changing the default behaviour of subject lines in replies. Valid values:
@ -104,6 +103,7 @@ config :pleroma, Pleroma.Emails.Mailer,
* `welcome_user_nickname`: The nickname of the local user that sends the welcome message. * `welcome_user_nickname`: The nickname of the local user that sends the welcome message.
* `max_report_comment_size`: The maximum size of the report comment (Default: `1000`) * `max_report_comment_size`: The maximum size of the report comment (Default: `1000`)
* `safe_dm_mentions`: If set to true, only mentions at the beginning of a post will be used to address people in direct messages. This is to prevent accidental mentioning of people when talking about them (e.g. "@friend hey i really don't like @enemy"). (Default: `false`) * `safe_dm_mentions`: If set to true, only mentions at the beginning of a post will be used to address people in direct messages. This is to prevent accidental mentioning of people when talking about them (e.g. "@friend hey i really don't like @enemy"). (Default: `false`)
* `healthcheck`: if set to true, system data will be shown on ``/api/pleroma/healthcheck``.
## :logger ## :logger
* `backends`: `:console` is used to send logs to stdout, `{ExSyslogger, :ex_syslogger}` to log to syslog, and `Quack.Logger` to log to Slack * `backends`: `:console` is used to send logs to stdout, `{ExSyslogger, :ex_syslogger}` to log to syslog, and `Quack.Logger` to log to Slack
@ -205,6 +205,7 @@ This section is used to configure Pleroma-FE, unless ``:managed_config`` in ``:i
* `enabled`: Enables proxying of remote media to the instances proxy * `enabled`: Enables proxying of remote media to the instances proxy
* `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts. * `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts.
* `proxy_opts`: All options defined in `Pleroma.ReverseProxy` documentation, defaults to `[max_body_length: (25*1_048_576)]`. * `proxy_opts`: All options defined in `Pleroma.ReverseProxy` documentation, defaults to `[max_body_length: (25*1_048_576)]`.
* `whitelist`: List of domains to bypass the mediaproxy
## :gopher ## :gopher
* `enabled`: Enables the gopher interface * `enabled`: Enables the gopher interface
@ -499,3 +500,8 @@ config :ueberauth, Ueberauth,
microsoft: {Ueberauth.Strategy.Microsoft, [callback_params: []]} microsoft: {Ueberauth.Strategy.Microsoft, [callback_params: []]}
] ]
``` ```
## :emoji
* `shortcode_globs`: Location of custom emoji files. `*` can be used as a wildcard. Example `["/emoji/custom/**/*.png"]`
* `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).

View file

@ -1,15 +1,25 @@
# Custom Emoji # Custom Emoji
Before you add your own custom emoji, check if they are available in an existing pack.
See `Mix.Tasks.Pleroma.Emoji` for information about emoji packs.
To add custom emoji: To add custom emoji:
* Add the image file(s) to `priv/static/emoji/custom` * Create the `STATIC-DIR/emoji/` directory if it doesn't exist
* In case of conflicts: add the desired shortcode with the path to `config/custom_emoji.txt`, comma-separated and one per line (`STATIC-DIR` is configurable, `instance/static/` by default)
* Force recompilation (``mix clean && mix compile``) * Create a directory with whatever name you want (custom is a good name to show the purpose of it).
This will create a local emoji pack.
* Put your `.png` emoji files in that directory. In case of conflicts, you can create an `emoji.txt`
file in that directory and specify a custom shortcode using the following format:
`shortcode, file-path, tag1, tag2, etc`. One emoji per line. Note that if you do so,
you'll have to list all other emojis in the pack too.
* Either restart pleroma or connect to the iex session pleroma's running and
run `Pleroma.Emoji.reload/0` in it.
Example: Example:
image files (in `/priv/static/emoji/custom`): `happy.png` and `sad.png` image files (in `instance/static/emoji/custom`): `happy.png` and `sad.png`
content of `config/custom_emoji.txt`: content of `emoji.txt`:
``` ```
happy, /emoji/custom/happy.png, Tag1,Tag2 happy, /emoji/custom/happy.png, Tag1,Tag2
sad, /emoji/custom/sad.png, Tag1 sad, /emoji/custom/sad.png, Tag1
@ -18,6 +28,11 @@ foo, /emoji/custom/foo.png
The files should be PNG (APNG is okay with `.png` for `image/png` Content-type) and under 50kb for compatibility with mastodon. The files should be PNG (APNG is okay with `.png` for `image/png` Content-type) and under 50kb for compatibility with mastodon.
Default file extentions and locations for emojis are set in `config.exs`. To use different locations or file-extentions, add the `shortcode_globs` to your secrets file (`prod.secret.exs` or `dev.secret.exs`) and edit it. Note that not all fediverse-software will show emojis with other file extentions:
```elixir
config :pleroma, :emoji, shortcode_globs: ["/emoji/custom/**/*.png", "/emoji/custom/**/*.gif"]
```
## Emoji tags (groups) ## Emoji tags (groups)
Default tags are set in `config.exs`. To set your own tags, copy the structure to your secrets file (`prod.secret.exs` or `dev.secret.exs`) and edit it. Default tags are set in `config.exs`. To set your own tags, copy the structure to your secrets file (`prod.secret.exs` or `dev.secret.exs`) and edit it.

60
lib/healthcheck.ex Normal file
View file

@ -0,0 +1,60 @@
defmodule Pleroma.Healthcheck do
@moduledoc """
Module collects metrics about app and assign healthy status.
"""
alias Pleroma.Healthcheck
alias Pleroma.Repo
defstruct pool_size: 0,
active: 0,
idle: 0,
memory_used: 0,
healthy: true
@type t :: %__MODULE__{
pool_size: non_neg_integer(),
active: non_neg_integer(),
idle: non_neg_integer(),
memory_used: number(),
healthy: boolean()
}
@spec system_info() :: t()
def system_info do
%Healthcheck{
memory_used: Float.round(:erlang.memory(:total) / 1024 / 1024, 2)
}
|> assign_db_info()
|> check_health()
end
defp assign_db_info(healthcheck) do
database = Application.get_env(:pleroma, Repo)[:database]
query =
"select state, count(pid) from pg_stat_activity where datname = '#{database}' group by state;"
result = Repo.query!(query)
pool_size = Application.get_env(:pleroma, Repo)[:pool_size]
db_info =
Enum.reduce(result.rows, %{active: 0, idle: 0}, fn [state, cnt], states ->
if state == "active" do
Map.put(states, :active, states.active + cnt)
else
Map.put(states, :idle, states.idle + cnt)
end
end)
|> Map.put(:pool_size, pool_size)
Map.merge(healthcheck, db_info)
end
@spec check_health(Healthcheck.t()) :: Healthcheck.t()
def check_health(%{pool_size: pool_size, active: active} = check)
when active >= pool_size do
%{check | healthy: false}
end
def check_health(check), do: check
end

View file

@ -0,0 +1,293 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Emoji do
use Mix.Task
@shortdoc "Manages emoji packs"
@moduledoc """
Manages emoji packs
## ls-packs
mix pleroma.emoji ls-packs [OPTION...]
Lists the emoji packs and metadata specified in the manifest.
### Options
- `-m, --manifest PATH/URL` - path to a custom manifest, it can
either be an URL starting with `http`, in that case the
manifest will be fetched from that address, or a local path
## get-packs
mix pleroma.emoji get-packs [OPTION...] PACKS
Fetches, verifies and installs the specified PACKS from the
manifest into the `STATIC-DIR/emoji/PACK-NAME`
### Options
- `-m, --manifest PATH/URL` - same as ls-packs
## gen-pack
mix pleroma.emoji gen-pack PACK-URL
Creates a new manifest entry and a file list from the specified
remote pack file. Currently, only .zip archives are recognized
as remote pack files and packs are therefore assumed to be zip
archives. This command is intended to run interactively and will
first ask you some basic questions about the pack, then download
the remote file and generate an SHA256 checksum for it, then
generate an emoji file list for you.
The manifest entry will either be written to a newly created
`index.json` file or appended to the existing one, *replacing*
the old pack with the same name if it was in the file previously.
The file list will be written to the file specified previously,
*replacing* that file. You _should_ check that the file list doesn't
contain anything you don't need in the pack, that is, anything that is
not an emoji (the whole pack is downloaded, but only emoji files
are extracted).
"""
@default_manifest Pleroma.Config.get!([:emoji, :default_manifest])
def run(["ls-packs" | args]) do
Application.ensure_all_started(:hackney)
{options, [], []} = parse_global_opts(args)
manifest =
fetch_manifest(if options[:manifest], do: options[:manifest], else: @default_manifest)
Enum.each(manifest, fn {name, info} ->
to_print = [
{"Name", name},
{"Homepage", info["homepage"]},
{"Description", info["description"]},
{"License", info["license"]},
{"Source", info["src"]}
]
for {param, value} <- to_print do
IO.puts(IO.ANSI.format([:bright, param, :normal, ": ", value]))
end
# A newline
IO.puts("")
end)
end
def run(["get-packs" | args]) do
Application.ensure_all_started(:hackney)
{options, pack_names, []} = parse_global_opts(args)
manifest_url = if options[:manifest], do: options[:manifest], else: @default_manifest
manifest = fetch_manifest(manifest_url)
for pack_name <- pack_names do
if Map.has_key?(manifest, pack_name) do
pack = manifest[pack_name]
src_url = pack["src"]
IO.puts(
IO.ANSI.format([
"Downloading ",
:bright,
pack_name,
:normal,
" from ",
:underline,
src_url
])
)
binary_archive = Tesla.get!(src_url).body
archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16()
sha_status_text = ["SHA256 of ", :bright, pack_name, :normal, " source file is ", :bright]
if archive_sha == String.upcase(pack["src_sha256"]) do
IO.puts(IO.ANSI.format(sha_status_text ++ [:green, "OK"]))
else
IO.puts(IO.ANSI.format(sha_status_text ++ [:red, "BAD"]))
raise "Bad SHA256 for #{pack_name}"
end
# The url specified in files should be in the same directory
files_url = Path.join(Path.dirname(manifest_url), pack["files"])
IO.puts(
IO.ANSI.format([
"Fetching the file list for ",
:bright,
pack_name,
:normal,
" from ",
:underline,
files_url
])
)
files = Tesla.get!(files_url).body |> Poison.decode!()
IO.puts(IO.ANSI.format(["Unpacking ", :bright, pack_name]))
pack_path =
Path.join([
Pleroma.Config.get!([:instance, :static_dir]),
"emoji",
pack_name
])
files_to_unzip =
Enum.map(
files,
fn {_, f} -> to_charlist(f) end
)
{:ok, _} =
:zip.unzip(binary_archive,
cwd: pack_path,
file_list: files_to_unzip
)
IO.puts(IO.ANSI.format(["Writing emoji.txt for ", :bright, pack_name]))
emoji_txt_str =
Enum.map(
files,
fn {shortcode, path} ->
emojo_path = Path.join("/emoji/#{pack_name}", path)
"#{shortcode}, #{emojo_path}"
end
)
|> Enum.join("\n")
File.write!(Path.join(pack_path, "emoji.txt"), emoji_txt_str)
else
IO.puts(IO.ANSI.format([:bright, :red, "No pack named \"#{pack_name}\" found"]))
end
end
end
def run(["gen-pack", src]) do
Application.ensure_all_started(:hackney)
proposed_name = Path.basename(src) |> Path.rootname()
name = String.trim(IO.gets("Pack name [#{proposed_name}]: "))
# If there's no name, use the default one
name = if String.length(name) > 0, do: name, else: proposed_name
license = String.trim(IO.gets("License: "))
homepage = String.trim(IO.gets("Homepage: "))
description = String.trim(IO.gets("Description: "))
proposed_files_name = "#{name}.json"
files_name = String.trim(IO.gets("Save file list to [#{proposed_files_name}]: "))
files_name = if String.length(files_name) > 0, do: files_name, else: proposed_files_name
default_exts = [".png", ".gif"]
default_exts_str = Enum.join(default_exts, " ")
exts =
String.trim(
IO.gets("Emoji file extensions (separated with spaces) [#{default_exts_str}]: ")
)
exts =
if String.length(exts) > 0 do
String.split(exts, " ")
|> Enum.filter(fn e -> e |> String.trim() |> String.length() > 0 end)
else
default_exts
end
IO.puts("Downloading the pack and generating SHA256")
binary_archive = Tesla.get!(src).body
archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16()
IO.puts("SHA256 is #{archive_sha}")
pack_json = %{
name => %{
license: license,
homepage: homepage,
description: description,
src: src,
src_sha256: archive_sha,
files: files_name
}
}
tmp_pack_dir = Path.join(System.tmp_dir!(), "emoji-pack-#{name}")
{:ok, _} =
:zip.unzip(
binary_archive,
cwd: tmp_pack_dir
)
emoji_map = Pleroma.Emoji.make_shortcode_to_file_map(tmp_pack_dir, exts)
File.write!(files_name, Poison.encode!(emoji_map, pretty: true))
IO.puts("""
#{files_name} has been created and contains the list of all found emojis in the pack.
Please review the files in the remove those not needed.
""")
if File.exists?("index.json") do
existing_data = File.read!("index.json") |> Poison.decode!()
File.write!(
"index.json",
Poison.encode!(
Map.merge(
existing_data,
pack_json
),
pretty: true
)
)
IO.puts("index.json file has been update with the #{name} pack")
else
File.write!("index.json", Poison.encode!(pack_json, pretty: true))
IO.puts("index.json has been created with the #{name} pack")
end
end
defp fetch_manifest(from) do
Poison.decode!(
if String.starts_with?(from, "http") do
Tesla.get!(from).body
else
File.read!(from)
end
)
end
defp parse_global_opts(args) do
OptionParser.parse(
args,
strict: [
manifest: :string
],
aliases: [
m: :manifest
]
)
end
end

View file

@ -162,7 +162,7 @@ def run(["new", nickname, email | rest]) do
def run(["rm", nickname]) do def run(["rm", nickname]) do
Common.start_pleroma() Common.start_pleroma()
with %User{local: true} = user <- User.get_by_nickname(nickname) do with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
User.delete(user) User.delete(user)
Mix.shell().info("User #{nickname} deleted.") Mix.shell().info("User #{nickname} deleted.")
else else
@ -174,7 +174,7 @@ def run(["rm", nickname]) do
def run(["toggle_activated", nickname]) do def run(["toggle_activated", nickname]) do
Common.start_pleroma() Common.start_pleroma()
with %User{} = user <- User.get_by_nickname(nickname) do with %User{} = user <- User.get_cached_by_nickname(nickname) do
{:ok, user} = User.deactivate(user, !user.info.deactivated) {:ok, user} = User.deactivate(user, !user.info.deactivated)
Mix.shell().info( Mix.shell().info(
@ -189,7 +189,7 @@ def run(["toggle_activated", nickname]) do
def run(["reset_password", nickname]) do def run(["reset_password", nickname]) do
Common.start_pleroma() Common.start_pleroma()
with %User{local: true} = user <- User.get_by_nickname(nickname), with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
{:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do {:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do
Mix.shell().info("Generated password reset token for #{user.nickname}") Mix.shell().info("Generated password reset token for #{user.nickname}")
@ -211,14 +211,14 @@ def run(["reset_password", nickname]) do
def run(["unsubscribe", nickname]) do def run(["unsubscribe", nickname]) do
Common.start_pleroma() Common.start_pleroma()
with %User{} = user <- User.get_by_nickname(nickname) do with %User{} = user <- User.get_cached_by_nickname(nickname) do
Mix.shell().info("Deactivating #{user.nickname}") Mix.shell().info("Deactivating #{user.nickname}")
User.deactivate(user) User.deactivate(user)
{:ok, friends} = User.get_friends(user) {:ok, friends} = User.get_friends(user)
Enum.each(friends, fn friend -> Enum.each(friends, fn friend ->
user = User.get_by_id(user.id) user = User.get_cached_by_id(user.id)
Mix.shell().info("Unsubscribing #{friend.nickname} from #{user.nickname}") Mix.shell().info("Unsubscribing #{friend.nickname} from #{user.nickname}")
User.unfollow(user, friend) User.unfollow(user, friend)
@ -226,7 +226,7 @@ def run(["unsubscribe", nickname]) do
:timer.sleep(500) :timer.sleep(500)
user = User.get_by_id(user.id) user = User.get_cached_by_id(user.id)
if Enum.empty?(user.following) do if Enum.empty?(user.following) do
Mix.shell().info("Successfully unsubscribed all followers from #{user.nickname}") Mix.shell().info("Successfully unsubscribed all followers from #{user.nickname}")
@ -250,7 +250,7 @@ def run(["set", nickname | rest]) do
] ]
) )
with %User{local: true} = user <- User.get_by_nickname(nickname) do with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
user = user =
case Keyword.get(options, :moderator) do case Keyword.get(options, :moderator) do
nil -> user nil -> user
@ -277,7 +277,7 @@ def run(["set", nickname | rest]) do
def run(["tag", nickname | tags]) do def run(["tag", nickname | tags]) do
Common.start_pleroma() Common.start_pleroma()
with %User{} = user <- User.get_by_nickname(nickname) do with %User{} = user <- User.get_cached_by_nickname(nickname) do
user = user |> User.tag(tags) user = user |> User.tag(tags)
Mix.shell().info("Tags of #{user.nickname}: #{inspect(tags)}") Mix.shell().info("Tags of #{user.nickname}: #{inspect(tags)}")
@ -290,7 +290,7 @@ def run(["tag", nickname | tags]) do
def run(["untag", nickname | tags]) do def run(["untag", nickname | tags]) do
Common.start_pleroma() Common.start_pleroma()
with %User{} = user <- User.get_by_nickname(nickname) do with %User{} = user <- User.get_cached_by_nickname(nickname) do
user = user |> User.untag(tags) user = user |> User.untag(tags)
Mix.shell().info("Tags of #{user.nickname}: #{inspect(tags)}") Mix.shell().info("Tags of #{user.nickname}: #{inspect(tags)}")
@ -379,7 +379,7 @@ def run(["revoke_invite", token]) do
def run(["delete_activities", nickname]) do def run(["delete_activities", nickname]) do
Common.start_pleroma() Common.start_pleroma()
with %User{local: true} = user <- User.get_by_nickname(nickname) do with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
User.delete_user_activities(user) User.delete_user_activities(user)
Mix.shell().info("User #{nickname} statuses deleted.") Mix.shell().info("User #{nickname} statuses deleted.")
else else

View file

@ -39,7 +39,7 @@ def used_changeset(struct) do
def reset_password(token, data) do def reset_password(token, data) do
with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}), with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}),
%User{} = user <- User.get_by_id(token.user_id), %User{} = user <- User.get_cached_by_id(token.user_id),
{:ok, _user} <- User.reset_password(user, data), {:ok, _user} <- User.reset_password(user, data),
{:ok, token} <- Repo.update(used_changeset(token)) do {:ok, token} <- Repo.update(used_changeset(token)) do
{:ok, token} {:ok, token}

60
lib/pleroma/bookmark.ex Normal file
View file

@ -0,0 +1,60 @@
defmodule Pleroma.Bookmark do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
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)
timestamps()
end
@spec create(FlakeId.t(), FlakeId.t()) :: {:ok, Bookmark.t()} | {:error, Changeset.t()}
def create(user_id, activity_id) do
attrs = %{
user_id: user_id,
activity_id: activity_id
}
%Bookmark{}
|> cast(attrs, [:user_id, :activity_id])
|> validate_required([:user_id, :activity_id])
|> unique_constraint(:activity_id, name: :bookmarks_user_id_activity_id_index)
|> Repo.insert()
end
@spec for_user_query(FlakeId.t()) :: Ecto.Query.t()
def for_user_query(user_id) do
Bookmark
|> where(user_id: ^user_id)
|> join(:inner, [b], activity in assoc(b, :activity))
|> preload([b, a], activity: a)
end
def get(user_id, activity_id) do
Bookmark
|> where(user_id: ^user_id)
|> where(activity_id: ^activity_id)
|> Repo.one()
end
@spec destroy(FlakeId.t(), FlakeId.t()) :: {:ok, Bookmark.t()} | {:error, Changeset.t()}
def destroy(user_id, activity_id) do
from(b in Bookmark,
where: b.user_id == ^user_id,
where: b.activity_id == ^activity_id
)
|> Repo.one()
|> Repo.delete()
end
end

View file

@ -6,7 +6,7 @@ defmodule Pleroma.Emoji do
@moduledoc """ @moduledoc """
The emojis are loaded from: The emojis are loaded from:
* the built-in Finmojis (if enabled in configuration), * emoji packs in INSTANCE-DIR/emoji
* the files: `config/emoji.txt` and `config/custom_emoji.txt` * 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 * glob paths, nested folder is used as tag name for grouping e.g. priv/static/emoji/custom/nested_folder
@ -14,6 +14,8 @@ defmodule Pleroma.Emoji do
""" """
use GenServer use GenServer
require Logger
@type pattern :: Regex.t() | module() | String.t() @type pattern :: Regex.t() | module() | String.t()
@type patterns :: pattern() | [pattern()] @type patterns :: pattern() | [pattern()]
@type group_patterns :: keyword(patterns()) @type group_patterns :: keyword(patterns())
@ -79,95 +81,94 @@ def code_change(_old_vsn, state, _extra) do
end end
defp load do defp load do
finmoji_enabled = Keyword.get(Application.get_env(:pleroma, :instance), :finmoji_enabled) emoji_dir_path =
Path.join(
Pleroma.Config.get!([:instance, :static_dir]),
"emoji"
)
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, packs} ->
# Print the packs we've found
Logger.info("Found emoji packs: #{Enum.join(packs, ", ")}")
emojis =
Enum.flat_map(
packs,
fn pack -> load_pack(Path.join(emoji_dir_path, pack)) 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 = Application.get_env(:pleroma, :emoji)[:shortcode_globs] || [] shortcode_globs = Application.get_env(:pleroma, :emoji)[:shortcode_globs] || []
emojis = emojis =
(load_finmoji(finmoji_enabled) ++ (load_from_file("config/emoji.txt") ++
load_from_file("config/emoji.txt") ++
load_from_file("config/custom_emoji.txt") ++ load_from_file("config/custom_emoji.txt") ++
load_from_globs(shortcode_globs)) load_from_globs(shortcode_globs))
|> Enum.reject(fn value -> value == nil end) |> Enum.reject(fn value -> value == nil end)
true = :ets.insert(@ets, emojis) true = :ets.insert(@ets, emojis)
:ok :ok
end end
@finmoji [ defp load_pack(pack_dir) do
"a_trusted_friend", pack_name = Path.basename(pack_dir)
"alandislands",
"association",
"auroraborealis",
"baby_in_a_box",
"bear",
"black_gold",
"christmasparty",
"crosscountryskiing",
"cupofcoffee",
"education",
"fashionista_finns",
"finnishlove",
"flag",
"forest",
"four_seasons_of_bbq",
"girlpower",
"handshake",
"happiness",
"headbanger",
"icebreaker",
"iceman",
"joulutorttu",
"kaamos",
"kalsarikannit_f",
"kalsarikannit_m",
"karjalanpiirakka",
"kicksled",
"kokko",
"lavatanssit",
"losthopes_f",
"losthopes_m",
"mattinykanen",
"meanwhileinfinland",
"moominmamma",
"nordicfamily",
"out_of_office",
"peacemaker",
"perkele",
"pesapallo",
"polarbear",
"pusa_hispida_saimensis",
"reindeer",
"sami",
"sauna_f",
"sauna_m",
"sauna_whisk",
"sisu",
"stuck",
"suomimainittu",
"superfood",
"swan",
"the_cap",
"the_conductor",
"the_king",
"the_voice",
"theoriginalsanta",
"tomoffinland",
"torillatavataan",
"unbreakable",
"waiting",
"white_nights",
"woollysocks"
]
defp load_finmoji(true) do emoji_txt = Path.join(pack_dir, "emoji.txt")
Enum.map(@finmoji, fn finmoji ->
file_name = "/finmoji/128px/#{finmoji}-128.png" if File.exists?(emoji_txt) do
group = match_extra(@groups, file_name) load_from_file(emoji_txt)
{finmoji, file_name, to_string(group)} else
end) Logger.info(
"No emoji.txt found for pack \"#{pack_name}\", assuming all .png files are emoji"
)
make_shortcode_to_file_map(pack_dir, [".png"])
|> Enum.map(fn {shortcode, rel_file} ->
filename = Path.join("/emoji/#{pack_name}", rel_file)
{shortcode, filename, [to_string(match_extra(@groups, filename))]}
end)
end
end end
defp load_finmoji(_), do: [] 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) do defp load_from_file(file) do
if File.exists?(file) do if File.exists?(file) do
@ -182,11 +183,11 @@ defp load_from_file_stream(stream) do
|> Stream.map(&String.trim/1) |> Stream.map(&String.trim/1)
|> Stream.map(fn line -> |> Stream.map(fn line ->
case String.split(line, ~r/,\s*/) do case String.split(line, ~r/,\s*/) do
[name, file, tags] ->
{name, file, tags}
[name, file] -> [name, file] ->
{name, file, to_string(match_extra(@groups, file))} {name, file, [to_string(match_extra(@groups, file))]}
[name, file | tags] ->
{name, file, tags}
_ -> _ ->
nil nil
@ -209,7 +210,7 @@ defp load_from_globs(globs) do
tag = match_extra(@groups, Path.join("/", Path.relative_to(path, static_path))) tag = match_extra(@groups, Path.join("/", Path.relative_to(path, static_path)))
shortcode = Path.basename(path, Path.extname(path)) shortcode = Path.basename(path, Path.extname(path))
external_path = Path.join("/", Path.relative_to(path, static_path)) external_path = Path.join("/", Path.relative_to(path, static_path))
{shortcode, external_path, to_string(tag)} {shortcode, external_path, [to_string(tag)]}
end) end)
end end

View file

@ -76,7 +76,7 @@ def render_activities(activities) do
|> Enum.map(fn activity -> |> Enum.map(fn activity ->
user = User.get_cached_by_ap_id(activity.data["actor"]) user = User.get_cached_by_ap_id(activity.data["actor"])
object = Object.normalize(activity.data["object"]) object = Object.normalize(activity)
like_count = object["like_count"] || 0 like_count = object["like_count"] || 0
announcement_count = object["announcement_count"] || 0 announcement_count = object["announcement_count"] || 0

View file

@ -106,7 +106,14 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do
# links # links
Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes) Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes)
Meta.allow_tag_with_these_attributes("a", ["name", "title", "class"])
Meta.allow_tag_with_this_attribute_values("a", "class", [
"hashtag",
"u-url",
"mention",
"u-url mention",
"mention u-url"
])
Meta.allow_tag_with_this_attribute_values("a", "rel", [ Meta.allow_tag_with_this_attribute_values("a", "rel", [
"tag", "tag",
@ -115,12 +122,15 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do
"noreferrer" "noreferrer"
]) ])
Meta.allow_tag_with_these_attributes("a", ["name", "title"])
# paragraphs and linebreaks # paragraphs and linebreaks
Meta.allow_tag_with_these_attributes("br", []) Meta.allow_tag_with_these_attributes("br", [])
Meta.allow_tag_with_these_attributes("p", []) Meta.allow_tag_with_these_attributes("p", [])
# microformats # microformats
Meta.allow_tag_with_these_attributes("span", ["class"]) Meta.allow_tag_with_this_attribute_values("span", "class", ["h-card"])
Meta.allow_tag_with_these_attributes("span", [])
# allow inline images for custom emoji # allow inline images for custom emoji
@allow_inline_images Keyword.get(@markup, :allow_inline_images) @allow_inline_images Keyword.get(@markup, :allow_inline_images)
@ -155,7 +165,14 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.strip_comments() Meta.strip_comments()
Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes) Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes)
Meta.allow_tag_with_these_attributes("a", ["name", "title", "class"])
Meta.allow_tag_with_this_attribute_values("a", "class", [
"hashtag",
"u-url",
"mention",
"u-url mention",
"mention u-url"
])
Meta.allow_tag_with_this_attribute_values("a", "rel", [ Meta.allow_tag_with_this_attribute_values("a", "rel", [
"tag", "tag",
@ -164,6 +181,8 @@ defmodule Pleroma.HTML.Scrubber.Default do
"noreferrer" "noreferrer"
]) ])
Meta.allow_tag_with_these_attributes("a", ["name", "title"])
Meta.allow_tag_with_these_attributes("abbr", ["title"]) Meta.allow_tag_with_these_attributes("abbr", ["title"])
Meta.allow_tag_with_these_attributes("b", []) Meta.allow_tag_with_these_attributes("b", [])
@ -177,11 +196,13 @@ defmodule Pleroma.HTML.Scrubber.Default do
Meta.allow_tag_with_these_attributes("ol", []) Meta.allow_tag_with_these_attributes("ol", [])
Meta.allow_tag_with_these_attributes("p", []) Meta.allow_tag_with_these_attributes("p", [])
Meta.allow_tag_with_these_attributes("pre", []) Meta.allow_tag_with_these_attributes("pre", [])
Meta.allow_tag_with_these_attributes("span", ["class"])
Meta.allow_tag_with_these_attributes("strong", []) Meta.allow_tag_with_these_attributes("strong", [])
Meta.allow_tag_with_these_attributes("u", []) Meta.allow_tag_with_these_attributes("u", [])
Meta.allow_tag_with_these_attributes("ul", []) Meta.allow_tag_with_these_attributes("ul", [])
Meta.allow_tag_with_this_attribute_values("span", "class", ["h-card"])
Meta.allow_tag_with_these_attributes("span", [])
@allow_inline_images Keyword.get(@markup, :allow_inline_images) @allow_inline_images Keyword.get(@markup, :allow_inline_images)
if @allow_inline_images do if @allow_inline_images do

View file

@ -80,7 +80,7 @@ def get_lists_from_activity(%Activity{actor: ap_id}) do
# Get lists to which the account belongs. # Get lists to which the account belongs.
def get_lists_account_belongs(%User{} = owner, account_id) do def get_lists_account_belongs(%User{} = owner, account_id) do
user = User.get_by_id(account_id) user = User.get_cached_by_id(account_id)
query = query =
from( from(

View file

@ -220,7 +220,7 @@ def skip?(
def skip?(:follows, activity, %{info: %{notification_settings: %{"follows" => false}}} = user) do def skip?(:follows, activity, %{info: %{notification_settings: %{"follows" => false}}} = user) do
actor = activity.data["actor"] actor = activity.data["actor"]
followed = User.get_by_ap_id(actor) followed = User.get_cached_by_ap_id(actor)
User.following?(user, followed) User.following?(user, followed)
end end

View file

@ -39,7 +39,7 @@ def fetch_object_from_id(id) do
Logger.info("Couldn't get object via AP, trying out OStatus fetching...") Logger.info("Couldn't get object via AP, trying out OStatus fetching...")
case OStatus.fetch_activity_from_url(id) do case OStatus.fetch_activity_from_url(id) do
{:ok, [activity | _]} -> {:ok, Object.normalize(activity.data["object"], false)} {:ok, [activity | _]} -> {:ok, Object.normalize(activity, false)}
e -> e e -> e
end end
end end

View file

@ -10,6 +10,7 @@ defmodule Pleroma.User do
alias Comeonin.Pbkdf2 alias Comeonin.Pbkdf2
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.Formatter alias Pleroma.Formatter
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
@ -53,9 +54,9 @@ defmodule Pleroma.User do
field(:search_rank, :float, virtual: true) field(:search_rank, :float, virtual: true)
field(:search_type, :integer, virtual: true) field(:search_type, :integer, virtual: true)
field(:tags, {:array, :string}, default: []) field(:tags, {:array, :string}, default: [])
field(:bookmarks, {:array, :string}, default: [])
field(:last_refreshed_at, :naive_datetime_usec) field(:last_refreshed_at, :naive_datetime_usec)
field(:last_digest_emailed_at, :naive_datetime) field(:last_digest_emailed_at, :naive_datetime)
has_many(:bookmarks, Bookmark)
has_many(:notifications, Notification) has_many(:notifications, Notification)
has_many(:registrations, Registration) has_many(:registrations, Registration)
embeds_one(:info, Pleroma.User.Info) embeds_one(:info, Pleroma.User.Info)
@ -270,6 +271,7 @@ defp autofollow_users(user) do
def register(%Ecto.Changeset{} = changeset) do def register(%Ecto.Changeset{} = changeset) do
with {:ok, user} <- Repo.insert(changeset), with {:ok, user} <- Repo.insert(changeset),
{:ok, user} <- autofollow_users(user), {:ok, user} <- autofollow_users(user),
{:ok, user} <- set_cache(user),
{:ok, _} <- Pleroma.User.WelcomeMessage.post_welcome_message_to_user(user), {:ok, _} <- Pleroma.User.WelcomeMessage.post_welcome_message_to_user(user),
{:ok, _} <- try_send_confirmation_email(user) do {:ok, _} <- try_send_confirmation_email(user) do
{:ok, user} {:ok, user}
@ -454,10 +456,13 @@ def get_by_guessed_nickname(ap_id) do
name = List.last(String.split(ap_id, "/")) name = List.last(String.split(ap_id, "/"))
nickname = "#{name}@#{domain}" nickname = "#{name}@#{domain}"
get_by_nickname(nickname) get_cached_by_nickname(nickname)
end end
def set_cache(user) do def set_cache({:ok, user}), do: set_cache(user)
def set_cache({:error, err}), do: {:error, err}
def set_cache(%User{} = user) do
Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user) Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
Cachex.put(:user_cache, "nickname:#{user.nickname}", user) Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user)) Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
@ -545,6 +550,7 @@ def get_or_fetch_by_nickname(nickname) do
with [_nick, _domain] <- String.split(nickname, "@"), with [_nick, _domain] <- String.split(nickname, "@"),
{:ok, user} <- fetch_by_nickname(nickname) do {:ok, user} <- fetch_by_nickname(nickname) do
if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do if Pleroma.Config.get([:fetch_initial_posts, :enabled]) do
# TODO turn into job
{:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user]) {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user])
end end
@ -1003,7 +1009,7 @@ def block(blocker, %User{ap_id: ap_id} = blocked) do
# helper to handle the block given only an actor's AP id # helper to handle the block given only an actor's AP id
def block(blocker, %{ap_id: ap_id}) do def block(blocker, %{ap_id: ap_id}) do
block(blocker, User.get_by_ap_id(ap_id)) block(blocker, get_cached_by_ap_id(ap_id))
end end
def unblock(blocker, %{ap_id: ap_id}) do def unblock(blocker, %{ap_id: ap_id}) do
@ -1033,7 +1039,7 @@ def blocks?(user, %{ap_id: ap_id}) do
end end
def subscribed_to?(user, %{ap_id: ap_id}) do def subscribed_to?(user, %{ap_id: ap_id}) do
with %User{} = target <- User.get_by_ap_id(ap_id) do with %User{} = target <- get_cached_by_ap_id(ap_id) do
Enum.member?(target.info.subscribers, user.ap_id) Enum.member?(target.info.subscribers, user.ap_id)
end end
end end
@ -1208,7 +1214,7 @@ def fetch_by_ap_id(ap_id) do
end end
def get_or_fetch_by_ap_id(ap_id) do def get_or_fetch_by_ap_id(ap_id) do
user = get_by_ap_id(ap_id) user = get_cached_by_ap_id(ap_id)
if !is_nil(user) and !User.needs_update?(user) do if !is_nil(user) and !User.needs_update?(user) do
user user
@ -1231,7 +1237,7 @@ def get_or_fetch_by_ap_id(ap_id) do
def get_or_create_instance_user do def get_or_create_instance_user do
relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay" relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
if user = get_by_ap_id(relay_uri) do if user = get_cached_by_ap_id(relay_uri) do
user user
else else
changes = changes =
@ -1278,13 +1284,11 @@ defp blank?(""), do: nil
defp blank?(n), do: n defp blank?(n), do: n
def insert_or_update_user(data) do def insert_or_update_user(data) do
data = data
data |> Map.put(:name, blank?(data[:name]) || data[:nickname])
|> Map.put(:name, blank?(data[:name]) || data[:nickname]) |> remote_user_creation()
|> Repo.insert(on_conflict: :replace_all, conflict_target: :nickname)
cs = User.remote_user_creation(data) |> set_cache()
Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
end end
def ap_enabled?(%User{local: true}), do: true def ap_enabled?(%User{local: true}), do: true
@ -1300,8 +1304,8 @@ def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname)
# this is because we have synchronous follow APIs and need to simulate them # this is because we have synchronous follow APIs and need to simulate them
# with an async handshake # with an async handshake
def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
with %User{} = a <- User.get_by_id(a.id), with %User{} = a <- User.get_cached_by_id(a.id),
%User{} = b <- User.get_by_id(b.id) do %User{} = b <- User.get_cached_by_id(b.id) do
{:ok, a, b} {:ok, a, b}
else else
_e -> _e ->
@ -1311,8 +1315,8 @@ def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
def wait_and_refresh(timeout, %User{} = a, %User{} = b) do def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
with :ok <- :timer.sleep(timeout), with :ok <- :timer.sleep(timeout),
%User{} = a <- User.get_by_id(a.id), %User{} = a <- User.get_cached_by_id(a.id),
%User{} = b <- User.get_by_id(b.id) do %User{} = b <- User.get_cached_by_id(b.id) do
{:ok, a, b} {:ok, a, b}
else else
_e -> _e ->
@ -1351,7 +1355,7 @@ def tag(user_identifiers, tags) when is_list(user_identifiers) do
end end
def tag(nickname, tags) when is_binary(nickname), def tag(nickname, tags) when is_binary(nickname),
do: tag(User.get_by_nickname(nickname), tags) do: tag(get_by_nickname(nickname), tags)
def tag(%User{} = user, tags), def tag(%User{} = user, tags),
do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags))) do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
@ -1363,7 +1367,7 @@ def untag(user_identifiers, tags) when is_list(user_identifiers) do
end end
def untag(nickname, tags) when is_binary(nickname), def untag(nickname, tags) when is_binary(nickname),
do: untag(User.get_by_nickname(nickname), tags) do: untag(get_by_nickname(nickname), tags)
def untag(%User{} = user, tags), def untag(%User{} = user, tags),
do: update_tags(user, (user.tags || []) -- normalize_tags(tags)) do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
@ -1377,22 +1381,6 @@ defp update_tags(%User{} = user, new_tags) do
updated_user updated_user
end end
def bookmark(%User{} = user, status_id) do
bookmarks = Enum.uniq(user.bookmarks ++ [status_id])
update_bookmarks(user, bookmarks)
end
def unbookmark(%User{} = user, status_id) do
bookmarks = Enum.uniq(user.bookmarks -- [status_id])
update_bookmarks(user, bookmarks)
end
def update_bookmarks(%User{} = user, bookmarks) do
user
|> change(%{bookmarks: bookmarks})
|> update_and_set_cache
end
defp normalize_tags(tags) do defp normalize_tags(tags) do
[tags] [tags]
|> List.flatten() |> List.flatten()

View file

@ -40,6 +40,7 @@ defmodule Pleroma.User.Info do
field(:salmon, :string, default: nil) field(:salmon, :string, default: nil)
field(:hide_followers, :boolean, default: false) field(:hide_followers, :boolean, default: false)
field(:hide_follows, :boolean, default: false) field(:hide_follows, :boolean, default: false)
field(:hide_favorites, :boolean, default: true)
field(:pinned_activities, {:array, :string}, default: []) field(:pinned_activities, {:array, :string}, default: [])
field(:flavour, :string, default: nil) field(:flavour, :string, default: nil)
field(:email_notifications, :map, default: %{"digest" => false}) field(:email_notifications, :map, default: %{"digest" => false})
@ -229,6 +230,7 @@ def profile_update(info, params) do
:banner, :banner,
:hide_follows, :hide_follows,
:hide_followers, :hide_followers,
:hide_favorites,
:background, :background,
:show_role :show_role
]) ])
@ -252,14 +254,6 @@ def confirmation_changeset(info, params) do
cast(info, params, [:confirmation_pending, :confirmation_token]) cast(info, params, [:confirmation_pending, :confirmation_token])
end end
def mastodon_profile_update(info, params) do
info
|> cast(params, [
:locked,
:banner
])
end
def mastodon_settings_update(info, settings) do def mastodon_settings_update(info, settings) do
params = %{settings: settings} params = %{settings: settings}

View file

@ -168,7 +168,7 @@ def stream_out(activity) do
public = "https://www.w3.org/ns/activitystreams#Public" public = "https://www.w3.org/ns/activitystreams#Public"
if activity.data["type"] in ["Create", "Announce", "Delete"] do if activity.data["type"] in ["Create", "Announce", "Delete"] do
object = Object.normalize(activity.data["object"]) object = Object.normalize(activity)
Pleroma.Web.Streamer.stream("user", activity) Pleroma.Web.Streamer.stream("user", activity)
Pleroma.Web.Streamer.stream("list", activity) Pleroma.Web.Streamer.stream("list", activity)
@ -197,7 +197,7 @@ def stream_out(activity) do
if !Enum.member?(activity.data["cc"] || [], public) && if !Enum.member?(activity.data["cc"] || [], public) &&
!Enum.member?( !Enum.member?(
activity.data["to"], activity.data["to"],
User.get_by_ap_id(activity.data["actor"]).follower_address User.get_cached_by_ap_id(activity.data["actor"]).follower_address
), ),
do: Pleroma.Web.Streamer.stream("direct", activity) do: Pleroma.Web.Streamer.stream("direct", activity)
end end
@ -889,7 +889,7 @@ def fetch_and_prepare_user_from_ap_id(ap_id) do
end end
def make_user_from_ap_id(ap_id) do def make_user_from_ap_id(ap_id) do
if _user = User.get_by_ap_id(ap_id) do if _user = User.get_cached_by_ap_id(ap_id) do
Transmogrifier.upgrade_user_from_ap_id(ap_id) Transmogrifier.upgrade_user_from_ap_id(ap_id)
else else
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id) do with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id) do

View file

@ -438,20 +438,46 @@ def handle_incoming(
with %User{local: true} = followed <- User.get_cached_by_ap_id(followed), with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
%User{} = follower <- User.get_or_fetch_by_ap_id(follower), %User{} = follower <- User.get_or_fetch_by_ap_id(follower),
{:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
if not User.locked?(followed) do with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]),
{:user_blocked, false} <-
{:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked},
{:user_locked, false} <- {:user_locked, User.locked?(followed)},
{:follow, {:ok, follower}} <- {:follow, User.follow(follower, followed)} do
ActivityPub.accept(%{ ActivityPub.accept(%{
to: [follower.ap_id], to: [follower.ap_id],
actor: followed, actor: followed,
object: data, object: data,
local: true local: true
}) })
else
{:user_blocked, true} ->
{:ok, _} = Utils.update_follow_state(activity, "reject")
User.follow(follower, followed) ActivityPub.reject(%{
to: [follower.ap_id],
actor: followed,
object: data,
local: true
})
{:follow, {:error, _}} ->
{:ok, _} = Utils.update_follow_state(activity, "reject")
ActivityPub.reject(%{
to: [follower.ap_id],
actor: followed,
object: data,
local: true
})
{:user_locked, true} ->
:noop
end end
{:ok, activity} {:ok, activity}
else else
_e -> :error _e ->
:error
end end
end end
@ -537,7 +563,7 @@ def handle_incoming(
data data
) )
when object_type in ["Person", "Application", "Service", "Organization"] do when object_type in ["Person", "Application", "Service", "Organization"] do
with %User{ap_id: ^actor_id} = actor <- User.get_by_ap_id(object["id"]) do with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do
{:ok, new_user_data} = ActivityPub.user_data_from_user_object(object) {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
banner = new_user_data[:info]["banner"] banner = new_user_data[:info]["banner"]
@ -964,7 +990,7 @@ def perform(:user_upgrade, user) do
end end
def upgrade_user_from_ap_id(ap_id) do def upgrade_user_from_ap_id(ap_id) do
with %User{local: false} = user <- User.get_by_ap_id(ap_id), with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
{:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id), {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
already_ap <- User.ap_enabled?(user), already_ap <- User.ap_enabled?(user),
{:ok, user} <- user |> User.upgrade_changeset(data) |> User.update_and_set_cache() do {:ok, user} <- user |> User.upgrade_changeset(data) |> User.update_and_set_cache() do

View file

@ -19,7 +19,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
action_fallback(:errors) action_fallback(:errors)
def user_delete(conn, %{"nickname" => nickname}) do def user_delete(conn, %{"nickname" => nickname}) do
User.get_by_nickname(nickname) User.get_cached_by_nickname(nickname)
|> User.delete() |> User.delete()
conn conn
@ -27,8 +27,8 @@ def user_delete(conn, %{"nickname" => nickname}) do
end end
def user_follow(conn, %{"follower" => follower_nick, "followed" => followed_nick}) do def user_follow(conn, %{"follower" => follower_nick, "followed" => followed_nick}) do
with %User{} = follower <- User.get_by_nickname(follower_nick), with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
%User{} = followed <- User.get_by_nickname(followed_nick) do %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
User.follow(follower, followed) User.follow(follower, followed)
end end
@ -37,8 +37,8 @@ def user_follow(conn, %{"follower" => follower_nick, "followed" => followed_nick
end end
def user_unfollow(conn, %{"follower" => follower_nick, "followed" => followed_nick}) do def user_unfollow(conn, %{"follower" => follower_nick, "followed" => followed_nick}) do
with %User{} = follower <- User.get_by_nickname(follower_nick), with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
%User{} = followed <- User.get_by_nickname(followed_nick) do %User{} = followed <- User.get_cached_by_nickname(followed_nick) do
User.unfollow(follower, followed) User.unfollow(follower, followed)
end end
@ -67,7 +67,7 @@ def user_create(
end end
def user_show(conn, %{"nickname" => nickname}) do def user_show(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_by_nickname(nickname) do with %User{} = user <- User.get_cached_by_nickname(nickname) do
conn conn
|> json(AccountView.render("show.json", %{user: user})) |> json(AccountView.render("show.json", %{user: user}))
else else
@ -76,7 +76,7 @@ def user_show(conn, %{"nickname" => nickname}) do
end end
def user_toggle_activation(conn, %{"nickname" => nickname}) do def user_toggle_activation(conn, %{"nickname" => nickname}) do
user = User.get_by_nickname(nickname) user = User.get_cached_by_nickname(nickname)
{:ok, updated_user} = User.deactivate(user, !user.info.deactivated) {:ok, updated_user} = User.deactivate(user, !user.info.deactivated)
@ -131,7 +131,7 @@ defp maybe_parse_filters(filters) do
def right_add(conn, %{"permission_group" => permission_group, "nickname" => nickname}) def right_add(conn, %{"permission_group" => permission_group, "nickname" => nickname})
when permission_group in ["moderator", "admin"] do when permission_group in ["moderator", "admin"] do
user = User.get_by_nickname(nickname) user = User.get_cached_by_nickname(nickname)
info = info =
%{} %{}
@ -156,7 +156,7 @@ def right_add(conn, _) do
end end
def right_get(conn, %{"nickname" => nickname}) do def right_get(conn, %{"nickname" => nickname}) do
user = User.get_by_nickname(nickname) user = User.get_cached_by_nickname(nickname)
conn conn
|> json(%{ |> json(%{
@ -178,7 +178,7 @@ def right_delete(
|> put_status(403) |> put_status(403)
|> json(%{error: "You can't revoke your own admin status."}) |> json(%{error: "You can't revoke your own admin status."})
else else
user = User.get_by_nickname(nickname) user = User.get_cached_by_nickname(nickname)
info = info =
%{} %{}
@ -204,7 +204,7 @@ def right_delete(conn, _) do
def set_activation_status(conn, %{"nickname" => nickname, "status" => status}) do def set_activation_status(conn, %{"nickname" => nickname, "status" => status}) do
with {:ok, status} <- Ecto.Type.cast(:boolean, status), with {:ok, status} <- Ecto.Type.cast(:boolean, status),
%User{} = user <- User.get_by_nickname(nickname), %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, _} <- User.deactivate(user, !status), {:ok, _} <- User.deactivate(user, !status),
do: json_response(conn, :no_content, "") do: json_response(conn, :no_content, "")
end end
@ -277,7 +277,7 @@ def revoke_invite(conn, %{"token" => token}) do
@doc "Get a password reset token (base64 string) for given nickname" @doc "Get a password reset token (base64 string) for given nickname"
def get_password_reset(conn, %{"nickname" => nickname}) do def get_password_reset(conn, %{"nickname" => nickname}) do
(%User{local: true} = user) = User.get_by_nickname(nickname) (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
{:ok, token} = Pleroma.PasswordResetToken.create_token(user) {:ok, token} = Pleroma.PasswordResetToken.create_token(user)
conn conn

View file

@ -24,7 +24,7 @@ defmodule Pleroma.Web.UserSocket do
def connect(%{"token" => token}, socket) do def connect(%{"token" => token}, socket) do
with true <- Pleroma.Config.get([:chat, :enabled]), with true <- Pleroma.Config.get([:chat, :enabled]),
{:ok, user_id} <- Phoenix.Token.verify(socket, "user socket", token, max_age: 84_600), {:ok, user_id} <- Phoenix.Token.verify(socket, "user socket", token, max_age: 84_600),
%User{} = user <- Pleroma.User.get_by_id(user_id) do %User{} = user <- Pleroma.User.get_cached_by_id(user_id) do
{:ok, assign(socket, :user_name, user.nickname)} {:ok, assign(socket, :user_name, user.nickname)}
else else
_e -> :error _e -> :error

View file

@ -4,6 +4,7 @@
defmodule Pleroma.Web.CommonAPI do defmodule Pleroma.Web.CommonAPI do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.Formatter alias Pleroma.Formatter
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.ThreadMute alias Pleroma.ThreadMute
@ -282,9 +283,18 @@ def thread_muted?(user, activity) do
end end
end end
def bookmarked?(user, activity) do
with %Bookmark{} <- Bookmark.get(user.id, activity.id) do
true
else
_ ->
false
end
end
def report(user, data) do def report(user, data) do
with {:account_id, %{"account_id" => account_id}} <- {:account_id, data}, with {:account_id, %{"account_id" => account_id}} <- {:account_id, data},
{:account, %User{} = account} <- {:account, User.get_by_id(account_id)}, {:account, %User{} = account} <- {:account, User.get_cached_by_id(account_id)},
{:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]), {:ok, {content_html, _, _}} <- make_report_content_html(data["comment"]),
{:ok, statuses} <- get_report_statuses(account, data), {:ok, statuses} <- get_report_statuses(account, data),
{:ok, activity} <- {:ok, activity} <-

View file

@ -182,6 +182,18 @@ def format_input(text, "text/plain", options) do
end).() end).()
end end
@doc """
Formatting text as BBCode.
"""
def format_input(text, "text/bbcode", options) do
text
|> String.replace(~r/\r/, "")
|> Formatter.html_escape("text/plain")
|> BBCode.to_html()
|> (fn {:ok, html} -> html end).()
|> Formatter.linkify(options)
end
@doc """ @doc """
Formatting text to html. Formatting text to html.
""" """
@ -226,7 +238,7 @@ def make_note_data(
} }
if in_reply_to do if in_reply_to do
in_reply_to_object = Object.normalize(in_reply_to.data["object"]) in_reply_to_object = Object.normalize(in_reply_to)
object object
|> Map.put("inReplyTo", in_reply_to_object.data["id"]) |> Map.put("inReplyTo", in_reply_to_object.data["id"])
@ -284,7 +296,7 @@ defp shortname(name) do
end end
def confirm_current_password(user, password) do def confirm_current_password(user, password) do
with %User{local: true} = db_user <- User.get_by_id(user.id), with %User{local: true} = db_user <- User.get_cached_by_id(user.id),
true <- Pbkdf2.checkpw(password, db_user.password_hash) do true <- Pbkdf2.checkpw(password, db_user.password_hash) do
{:ok, db_user} {:ok, db_user}
else else

View file

@ -186,7 +186,7 @@ def perform(type, _) do
end end
def ap_enabled_actor(id) do def ap_enabled_actor(id) do
user = User.get_by_ap_id(id) user = User.get_cached_by_ap_id(id)
if User.ap_enabled?(user) do if User.ap_enabled?(user) do
{:ok, user} {:ok, user}

View file

@ -7,6 +7,31 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
alias Pleroma.Pagination alias Pleroma.Pagination
alias Pleroma.ScheduledActivity alias Pleroma.ScheduledActivity
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.CommonAPI
def follow(follower, followed, params \\ %{}) do
options = cast_params(params)
reblogs = options[:reblogs]
result =
if not User.following?(follower, followed) do
CommonAPI.follow(follower, followed)
else
{:ok, follower, followed, nil}
end
with {:ok, follower, followed, _} <- result do
reblogs
|> case do
false -> CommonAPI.hide_reblogs(follower, followed)
_ -> CommonAPI.show_reblogs(follower, followed)
end
|> case do
{:ok, follower} -> {:ok, follower}
_ -> {:ok, follower}
end
end
end
def get_followers(user, params \\ %{}) do def get_followers(user, params \\ %{}) do
user user
@ -37,7 +62,8 @@ def get_scheduled_activities(user, params \\ %{}) do
defp cast_params(params) do defp cast_params(params) do
param_types = %{ param_types = %{
exclude_types: {:array, :string} exclude_types: {:array, :string},
reblogs: :boolean
} }
changeset = cast({%{}, param_types}, params, Map.keys(param_types)) changeset = cast({%{}, param_types}, params, Map.keys(param_types))

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
alias Ecto.Changeset alias Ecto.Changeset
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.Filter alias Pleroma.Filter
alias Pleroma.Notification alias Pleroma.Notification
@ -35,7 +36,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
alias Pleroma.Web.OAuth.Authorization alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token
import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2] alias Pleroma.Web.ControllerHelper
import Ecto.Query import Ecto.Query
require Logger require Logger
@ -46,7 +47,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
action_fallback(:errors) action_fallback(:errors)
def create_app(conn, params) do def create_app(conn, params) do
scopes = oauth_scopes(params, ["read"]) scopes = ControllerHelper.oauth_scopes(params, ["read"])
app_attrs = app_attrs =
params params
@ -96,8 +97,13 @@ def update_credentials(%{assigns: %{user: user}} = conn, params) do
end) end)
info_params = info_params =
%{} [:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role]
|> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end) |> Enum.reduce(%{}, fn key, acc ->
add_if_present(acc, params, to_string(key), key, fn value ->
{:ok, ControllerHelper.truthy_param?(value)}
end)
end)
|> add_if_present(params, "default_scope", :default_scope)
|> add_if_present(params, "header", :banner, fn value -> |> add_if_present(params, "header", :banner, fn value ->
with %Plug.Upload{} <- value, with %Plug.Upload{} <- value,
{:ok, object} <- ActivityPub.upload(value, type: :banner) do {:ok, object} <- ActivityPub.upload(value, type: :banner) do
@ -107,7 +113,7 @@ def update_credentials(%{assigns: %{user: user}} = conn, params) do
end end
end) end)
info_cng = User.Info.mastodon_profile_update(user.info, info_params) info_cng = User.Info.profile_update(user.info, info_params)
with changeset <- User.update_changeset(user, user_params), with changeset <- User.update_changeset(user, user_params),
changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng), changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
@ -190,7 +196,7 @@ defp mastodonized_emoji do
"static_url" => url, "static_url" => url,
"visible_in_picker" => true, "visible_in_picker" => true,
"url" => url, "url" => url,
"tags" => String.split(tags, ",") "tags" => tags
} }
end) end)
end end
@ -279,6 +285,8 @@ def home_timeline(%{assigns: %{user: user}} = conn, params) do
|> ActivityPub.contain_timeline(user) |> ActivityPub.contain_timeline(user)
|> Enum.reverse() |> Enum.reverse()
user = Repo.preload(user, bookmarks: :activity)
conn conn
|> add_link_headers(:home_timeline, activities) |> add_link_headers(:home_timeline, activities)
|> put_view(StatusView) |> put_view(StatusView)
@ -297,6 +305,8 @@ def public_timeline(%{assigns: %{user: user}} = conn, params) do
|> ActivityPub.fetch_public_activities() |> ActivityPub.fetch_public_activities()
|> Enum.reverse() |> Enum.reverse()
user = Repo.preload(user, bookmarks: :activity)
conn conn
|> add_link_headers(:public_timeline, activities, false, %{"local" => local_only}) |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
|> put_view(StatusView) |> put_view(StatusView)
@ -304,7 +314,8 @@ def public_timeline(%{assigns: %{user: user}} = conn, params) do
end end
def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
with %User{} = user <- User.get_by_id(params["id"]) do with %User{} = user <- User.get_cached_by_id(params["id"]),
reading_user <- Repo.preload(reading_user, :bookmarks) do
activities = ActivityPub.fetch_user_activities(user, reading_user, params) activities = ActivityPub.fetch_user_activities(user, reading_user, params)
conn conn
@ -331,6 +342,8 @@ def dm_timeline(%{assigns: %{user: user}} = conn, params) do
|> ActivityPub.fetch_activities_query(params) |> ActivityPub.fetch_activities_query(params)
|> Pagination.fetch_paginated(params) |> Pagination.fetch_paginated(params)
user = Repo.preload(user, bookmarks: :activity)
conn conn
|> add_link_headers(:dm_timeline, activities) |> add_link_headers(:dm_timeline, activities)
|> put_view(StatusView) |> put_view(StatusView)
@ -338,8 +351,10 @@ def dm_timeline(%{assigns: %{user: user}} = conn, params) do
end end
def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Activity.get_by_id(id), with %Activity{} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.visible_for_user?(activity, user) do true <- Visibility.visible_for_user?(activity, user) do
user = Repo.preload(user, bookmarks: :activity)
conn conn
|> put_view(StatusView) |> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user}) |> try_render("status.json", %{activity: activity, for: user})
@ -487,7 +502,10 @@ def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
end end
def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
%Activity{} = announce <- Activity.normalize(announce.data) do
user = Repo.preload(user, bookmarks: :activity)
conn conn
|> put_view(StatusView) |> put_view(StatusView)
|> try_render("status.json", %{activity: announce, for: user, as: :activity}) |> try_render("status.json", %{activity: announce, for: user, as: :activity})
@ -496,7 +514,9 @@ def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user), with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
user = Repo.preload(user, bookmarks: :activity)
conn conn
|> put_view(StatusView) |> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity}) |> try_render("status.json", %{activity: activity, for: user, as: :activity})
@ -544,10 +564,11 @@ def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id), with %Activity{} = activity <- Activity.get_by_id_with_object(id),
%Object{} = object <- Object.normalize(activity), %User{} = user <- User.get_cached_by_nickname(user.nickname),
%User{} = user <- User.get_by_nickname(user.nickname),
true <- Visibility.visible_for_user?(activity, user), true <- Visibility.visible_for_user?(activity, user),
{:ok, user} <- User.bookmark(user, object.data["id"]) do {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
user = Repo.preload(user, bookmarks: :activity)
conn conn
|> put_view(StatusView) |> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity}) |> try_render("status.json", %{activity: activity, for: user, as: :activity})
@ -556,10 +577,11 @@ def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(id), with %Activity{} = activity <- Activity.get_by_id_with_object(id),
%Object{} = object <- Object.normalize(activity), %User{} = user <- User.get_cached_by_nickname(user.nickname),
%User{} = user <- User.get_by_nickname(user.nickname),
true <- Visibility.visible_for_user?(activity, user), true <- Visibility.visible_for_user?(activity, user),
{:ok, user} <- User.unbookmark(user, object.data["id"]) do {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
user = Repo.preload(user, bookmarks: :activity)
conn conn
|> put_view(StatusView) |> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity}) |> try_render("status.json", %{activity: activity, for: user, as: :activity})
@ -749,7 +771,7 @@ def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
end end
def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
with %User{} = user <- User.get_by_id(id), with %User{} = user <- User.get_cached_by_id(id),
followers <- MastodonAPI.get_followers(user, params) do followers <- MastodonAPI.get_followers(user, params) do
followers = followers =
cond do cond do
@ -766,7 +788,7 @@ def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
end end
def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
with %User{} = user <- User.get_by_id(id), with %User{} = user <- User.get_cached_by_id(id),
followers <- MastodonAPI.get_friends(user, params) do followers <- MastodonAPI.get_friends(user, params) do
followers = followers =
cond do cond do
@ -791,7 +813,7 @@ def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
end end
def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
with %User{} = follower <- User.get_by_id(id), with %User{} = follower <- User.get_cached_by_id(id),
{:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
conn conn
|> put_view(AccountView) |> put_view(AccountView)
@ -805,7 +827,7 @@ def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}
end end
def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
with %User{} = follower <- User.get_by_id(id), with %User{} = follower <- User.get_cached_by_id(id),
{:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
conn conn
|> put_view(AccountView) |> put_view(AccountView)
@ -821,8 +843,7 @@ def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) d
def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)}, with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
{_, true} <- {:followed, follower.id != followed.id}, {_, true} <- {:followed, follower.id != followed.id},
false <- User.following?(follower, followed), {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
{:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
conn conn
|> put_view(AccountView) |> put_view(AccountView)
|> render("relationship.json", %{user: follower, target: followed}) |> render("relationship.json", %{user: follower, target: followed})
@ -830,19 +851,6 @@ def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
{:followed, _} -> {:followed, _} ->
{:error, :not_found} {:error, :not_found}
true ->
followed = User.get_cached_by_id(id)
{:ok, follower} =
case conn.params["reblogs"] do
true -> CommonAPI.show_reblogs(follower, followed)
false -> CommonAPI.hide_reblogs(follower, followed)
end
conn
|> put_view(AccountView)
|> render("relationship.json", %{user: follower, target: followed})
{:error, message} -> {:error, message} ->
conn conn
|> put_resp_content_type("application/json") |> put_resp_content_type("application/json")
@ -885,7 +893,7 @@ def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
end end
def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
with %User{} = muted <- User.get_by_id(id), with %User{} = muted <- User.get_cached_by_id(id),
{:ok, muter} <- User.mute(muter, muted) do {:ok, muter} <- User.mute(muter, muted) do
conn conn
|> put_view(AccountView) |> put_view(AccountView)
@ -899,7 +907,7 @@ def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
end end
def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
with %User{} = muted <- User.get_by_id(id), with %User{} = muted <- User.get_cached_by_id(id),
{:ok, muter} <- User.unmute(muter, muted) do {:ok, muter} <- User.unmute(muter, muted) do
conn conn
|> put_view(AccountView) |> put_view(AccountView)
@ -920,7 +928,7 @@ def mutes(%{assigns: %{user: user}} = conn, _) do
end end
def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
with %User{} = blocked <- User.get_by_id(id), with %User{} = blocked <- User.get_cached_by_id(id),
{:ok, blocker} <- User.block(blocker, blocked), {:ok, blocker} <- User.block(blocker, blocked),
{:ok, _activity} <- ActivityPub.block(blocker, blocked) do {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
conn conn
@ -935,7 +943,7 @@ def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
end end
def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
with %User{} = blocked <- User.get_by_id(id), with %User{} = blocked <- User.get_cached_by_id(id),
{:ok, blocker} <- User.unblock(blocker, blocked), {:ok, blocker} <- User.unblock(blocker, blocked),
{:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
conn conn
@ -1094,21 +1102,65 @@ def favourites(%{assigns: %{user: user}} = conn, params) do
ActivityPub.fetch_activities([], params) ActivityPub.fetch_activities([], params)
|> Enum.reverse() |> Enum.reverse()
user = Repo.preload(user, bookmarks: :activity)
conn conn
|> add_link_headers(:favourites, activities) |> add_link_headers(:favourites, activities)
|> put_view(StatusView) |> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity}) |> render("index.json", %{activities: activities, for: user, as: :activity})
end end
def bookmarks(%{assigns: %{user: user}} = conn, _) do def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
user = User.get_by_id(user.id) with %User{} = user <- User.get_by_id(id),
false <- user.info.hide_favorites 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
["https://www.w3.org/ns/activitystreams#Public"] ++
[for_user.ap_id | for_user.following]
else
["https://www.w3.org/ns/activitystreams#Public"]
end
activities =
recipients
|> ActivityPub.fetch_activities(params)
|> Enum.reverse()
conn
|> add_link_headers(:favourites, activities)
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: for_user, as: :activity})
else
nil ->
{:error, :not_found}
true ->
conn
|> put_status(403)
|> json(%{error: "Can't get favorites"})
end
end
def bookmarks(%{assigns: %{user: user}} = conn, params) do
user = User.get_cached_by_id(user.id)
user = Repo.preload(user, bookmarks: :activity)
bookmarks =
Bookmark.for_user_query(user.id)
|> Pagination.fetch_paginated(params)
activities = activities =
user.bookmarks bookmarks
|> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end) |> Enum.map(fn b -> b.activity end)
|> Enum.reverse()
conn conn
|> add_link_headers(:bookmarks, bookmarks)
|> put_view(StatusView) |> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity}) |> render("index.json", %{activities: activities, for: user, as: :activity})
end end
@ -1158,7 +1210,7 @@ def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" =>
accounts accounts
|> Enum.each(fn account_id -> |> Enum.each(fn account_id ->
with %Pleroma.List{} = list <- Pleroma.List.get(id, user), with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
%User{} = followed <- User.get_by_id(account_id) do %User{} = followed <- User.get_cached_by_id(account_id) do
Pleroma.List.follow(list, followed) Pleroma.List.follow(list, followed)
end end
end) end)
@ -1170,7 +1222,7 @@ def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_id
accounts accounts
|> Enum.each(fn account_id -> |> Enum.each(fn account_id ->
with %Pleroma.List{} = list <- Pleroma.List.get(id, user), with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
%User{} = followed <- Pleroma.User.get_by_id(account_id) do %User{} = followed <- Pleroma.User.get_cached_by_id(account_id) do
Pleroma.List.unfollow(list, followed) Pleroma.List.unfollow(list, followed)
end end
end) end)
@ -1214,6 +1266,8 @@ def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params)
|> ActivityPub.fetch_activities_bounded(following, params) |> ActivityPub.fetch_activities_bounded(following, params)
|> Enum.reverse() |> Enum.reverse()
user = Repo.preload(user, bookmarks: :activity)
conn conn
|> put_view(StatusView) |> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity}) |> render("index.json", %{activities: activities, for: user, as: :activity})
@ -1463,7 +1517,7 @@ def logout(conn, _) do
def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
Logger.debug("Unimplemented, returning unmodified relationship") Logger.debug("Unimplemented, returning unmodified relationship")
with %User{} = target <- User.get_by_id(id) do with %User{} = target <- User.get_cached_by_id(id) do
conn conn
|> put_view(AccountView) |> put_view(AccountView)
|> render("relationship.json", %{user: user, target: target}) |> render("relationship.json", %{user: user, target: target})

View file

@ -68,7 +68,7 @@ def render("relationships.json", %{user: user, targets: targets}) do
defp do_render("account.json", %{user: user} = opts) do defp do_render("account.json", %{user: user} = opts) do
image = User.avatar_url(user) |> MediaProxy.url() image = User.avatar_url(user) |> MediaProxy.url()
header = User.banner_url(user) |> MediaProxy.url() header = User.banner_url(user) |> MediaProxy.url()
user_info = User.user_info(user) user_info = User.get_cached_user_info(user)
bot = (user.info.source_data["type"] || "Person") in ["Application", "Service"] bot = (user.info.source_data["type"] || "Person") in ["Application", "Service"]
emojis = emojis =
@ -113,21 +113,23 @@ defp do_render("account.json", %{user: user} = opts) do
bot: bot, bot: bot,
source: %{ source: %{
note: "", note: "",
privacy: user_info.default_scope, sensitive: false,
sensitive: false pleroma: %{}
}, },
# Pleroma extension # Pleroma extension
pleroma: pleroma: %{
%{ confirmation_pending: user_info.confirmation_pending,
confirmation_pending: user_info.confirmation_pending, tags: user.tags,
tags: user.tags, hide_followers: user.info.hide_followers,
is_moderator: user.info.is_moderator, hide_follows: user.info.hide_follows,
is_admin: user.info.is_admin, hide_favorites: user.info.hide_favorites,
relationship: relationship relationship: relationship
} }
|> with_notification_settings(user, opts[:for])
} }
|> maybe_put_role(user, opts[:for])
|> maybe_put_settings(user, opts[:for], user_info)
|> maybe_put_notification_settings(user, opts[:for])
end end
defp username_from_nickname(string) when is_binary(string) do defp username_from_nickname(string) when is_binary(string) do
@ -136,9 +138,37 @@ defp username_from_nickname(string) when is_binary(string) do
defp username_from_nickname(_), do: nil defp username_from_nickname(_), do: nil
defp with_notification_settings(data, %User{id: user_id} = user, %User{id: user_id}) do defp maybe_put_settings(
Map.put(data, :notification_settings, user.info.notification_settings) data,
%User{id: user_id} = user,
%User{id: user_id},
user_info
) do
data
|> Kernel.put_in([:source, :privacy], user_info.default_scope)
|> Kernel.put_in([:source, :pleroma, :show_role], user.info.show_role)
|> Kernel.put_in([:source, :pleroma, :no_rich_text], user.info.no_rich_text)
end end
defp with_notification_settings(data, _, _), do: data defp maybe_put_settings(data, _, _, _), do: data
defp maybe_put_role(data, %User{info: %{show_role: true}} = user, _) do
data
|> Kernel.put_in([:pleroma, :is_admin], user.info.is_admin)
|> Kernel.put_in([:pleroma, :is_moderator], user.info.is_moderator)
end
defp maybe_put_role(data, %User{id: user_id} = user, %User{id: user_id}) do
data
|> Kernel.put_in([:pleroma, :is_admin], user.info.is_admin)
|> Kernel.put_in([:pleroma, :is_moderator], user.info.is_moderator)
end
defp maybe_put_role(data, _, _), do: data
defp maybe_put_notification_settings(data, %User{id: user_id} = user, %User{id: user_id}) do
Kernel.put_in(data, [:pleroma, :notification_settings], user.info.notification_settings)
end
defp maybe_put_notification_settings(data, _, _), do: data
end end

View file

@ -31,7 +31,7 @@ defp get_replied_to_activities(activities) do
|> Activity.create_by_object_ap_id() |> Activity.create_by_object_ap_id()
|> Repo.all() |> Repo.all()
|> Enum.reduce(%{}, fn activity, acc -> |> Enum.reduce(%{}, fn activity, acc ->
object = Object.normalize(activity.data["object"]) object = Object.normalize(activity)
Map.put(acc, object.data["id"], activity) Map.put(acc, object.data["id"], activity)
end) end)
end end
@ -83,6 +83,11 @@ def render(
reblogged_activity = Activity.get_create_by_object_ap_id(object) reblogged_activity = Activity.get_create_by_object_ap_id(object)
reblogged = render("status.json", Map.put(opts, :activity, reblogged_activity)) reblogged = render("status.json", Map.put(opts, :activity, reblogged_activity))
activity_object = Object.normalize(activity)
favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || [])
bookmarked = opts[:for] && CommonAPI.bookmarked?(opts[:for], reblogged_activity)
mentions = mentions =
activity.recipients activity.recipients
|> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end) |> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
@ -103,8 +108,8 @@ def render(
replies_count: 0, replies_count: 0,
favourites_count: 0, favourites_count: 0,
reblogged: reblogged?(reblogged_activity, opts[:for]), reblogged: reblogged?(reblogged_activity, opts[:for]),
favourited: false, favourited: present?(favorited),
bookmarked: false, bookmarked: present?(bookmarked),
muted: false, muted: false,
pinned: pinned?(activity, user), pinned: pinned?(activity, user),
sensitive: false, sensitive: false,
@ -144,7 +149,7 @@ def render("status.json", %{activity: %{data: %{"object" => _object}} = activity
favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || []) favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
bookmarked = opts[:for] && object.data["id"] in opts[:for].bookmarks bookmarked = opts[:for] && CommonAPI.bookmarked?(opts[:for], activity)
attachment_data = object.data["attachment"] || [] attachment_data = object.data["attachment"] || []
attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment) attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
@ -234,6 +239,7 @@ def render("status.json", %{activity: %{data: %{"object" => _object}} = activity
pleroma: %{ pleroma: %{
local: activity.local, local: activity.local,
conversation_id: get_context_id(activity), conversation_id: get_context_id(activity),
in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
content: %{"text/plain" => content_plaintext}, content: %{"text/plain" => content_plaintext},
spoiler_text: %{"text/plain" => summary_plaintext} spoiler_text: %{"text/plain" => summary_plaintext}
} }
@ -312,7 +318,7 @@ def render("attachment.json", %{attachment: attachment}) do
end end
def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do
object = Object.normalize(activity.data["object"]) object = Object.normalize(activity)
with nil <- replied_to_activities[object.data["inReplyTo"]] do with nil <- replied_to_activities[object.data["inReplyTo"]] do
# If user didn't participate in the thread # If user didn't participate in the thread

View file

@ -90,7 +90,7 @@ defp allow_request(stream, nil) when stream in @anonymous_streams do
# Authenticated streams. # Authenticated streams.
defp allow_request(stream, {"access_token", access_token}) when stream in @streams do defp allow_request(stream, {"access_token", access_token}) when stream in @streams do
with %Token{user_id: user_id} <- Repo.get_by(Token, token: access_token), with %Token{user_id: user_id} <- Repo.get_by(Token, token: access_token),
user = %User{} <- User.get_by_id(user_id) do user = %User{} <- User.get_cached_by_id(user_id) do
{:ok, user} {:ok, user}
else else
_ -> {:error, 403} _ -> {:error, 403}

View file

@ -13,32 +13,44 @@ def url("/" <> _ = url), do: url
def url(url) do def url(url) do
config = Application.get_env(:pleroma, :media_proxy, []) config = Application.get_env(:pleroma, :media_proxy, [])
domain = URI.parse(url).host
if !Keyword.get(config, :enabled, false) or String.starts_with?(url, Pleroma.Web.base_url()) do cond do
url !Keyword.get(config, :enabled, false) or String.starts_with?(url, Pleroma.Web.base_url()) ->
else
secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base]
# Must preserve `%2F` for compatibility with S3
# https://git.pleroma.social/pleroma/pleroma/issues/580
replacement = get_replacement(url, ":2F:")
# The URL is url-decoded and encoded again to ensure it is correctly encoded and not twice.
base64 =
url url
|> String.replace("%2F", replacement)
|> URI.decode()
|> URI.encode()
|> String.replace(replacement, "%2F")
|> Base.url_encode64(@base64_opts)
sig = :crypto.hmac(:sha, secret, base64) Enum.any?(Pleroma.Config.get([:media_proxy, :whitelist]), fn pattern ->
sig64 = sig |> Base.url_encode64(@base64_opts) String.equivalent?(domain, pattern)
end) ->
url
build_url(sig64, base64, filename(url)) true ->
encode_url(url)
end end
end end
def encode_url(url) do
secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base]
# Must preserve `%2F` for compatibility with S3
# https://git.pleroma.social/pleroma/pleroma/issues/580
replacement = get_replacement(url, ":2F:")
# The URL is url-decoded and encoded again to ensure it is correctly encoded and not twice.
base64 =
url
|> String.replace("%2F", replacement)
|> URI.decode()
|> URI.encode()
|> String.replace(replacement, "%2F")
|> Base.url_encode64(@base64_opts)
sig = :crypto.hmac(:sha, secret, base64)
sig64 = sig |> Base.url_encode64(@base64_opts)
build_url(sig64, base64, filename(url))
end
def decode_url(sig, url) do def decode_url(sig, url) do
secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base] secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base]
sig = Base.url_decode64!(sig, @base64_opts) sig = Base.url_decode64!(sig, @base64_opts)

View file

@ -23,6 +23,12 @@ defmodule Pleroma.Web.OAuth.OAuthController do
action_fallback(Pleroma.Web.OAuth.FallbackController) action_fallback(Pleroma.Web.OAuth.FallbackController)
# Note: this definition is only called from error-handling methods with `conn.params` as 2nd arg
def authorize(conn, %{"authorization" => _} = params) do
{auth_attrs, params} = Map.pop(params, "authorization")
authorize(conn, Map.merge(params, auth_attrs))
end
def authorize(%{assigns: %{token: %Token{} = token}} = conn, params) do def authorize(%{assigns: %{token: %Token{} = token}} = conn, params) do
if ControllerHelper.truthy_param?(params["force_login"]) do if ControllerHelper.truthy_param?(params["force_login"]) do
do_authorize(conn, params) do_authorize(conn, params)
@ -44,21 +50,20 @@ def authorize(%{assigns: %{token: %Token{} = token}} = conn, params) do
def authorize(conn, params), do: do_authorize(conn, params) def authorize(conn, params), do: do_authorize(conn, params)
defp do_authorize(conn, %{"authorization" => auth_attrs}), do: do_authorize(conn, auth_attrs) defp do_authorize(conn, params) do
app = Repo.get_by(App, client_id: params["client_id"])
defp do_authorize(conn, auth_attrs) do
app = Repo.get_by(App, client_id: auth_attrs["client_id"])
available_scopes = (app && app.scopes) || [] available_scopes = (app && app.scopes) || []
scopes = oauth_scopes(auth_attrs, nil) || available_scopes scopes = oauth_scopes(params, nil) || available_scopes
# Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
render(conn, Authenticator.auth_template(), %{ render(conn, Authenticator.auth_template(), %{
response_type: auth_attrs["response_type"], response_type: params["response_type"],
client_id: auth_attrs["client_id"], client_id: params["client_id"],
available_scopes: available_scopes, available_scopes: available_scopes,
scopes: scopes, scopes: scopes,
redirect_uri: auth_attrs["redirect_uri"], redirect_uri: params["redirect_uri"],
state: auth_attrs["state"], state: params["state"],
params: auth_attrs params: params
}) })
end end
@ -138,7 +143,7 @@ def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do
fixed_token = fix_padding(params["code"]), fixed_token = fix_padding(params["code"]),
%Authorization{} = auth <- %Authorization{} = auth <-
Repo.get_by(Authorization, token: fixed_token, app_id: app.id), Repo.get_by(Authorization, token: fixed_token, app_id: app.id),
%User{} = user <- User.get_by_id(auth.user_id), %User{} = user <- User.get_cached_by_id(auth.user_id),
{:ok, token} <- Token.exchange_token(app, auth), {:ok, token} <- Token.exchange_token(app, auth),
{:ok, inserted_at} <- DateTime.from_naive(token.inserted_at, "Etc/UTC") do {:ok, inserted_at} <- DateTime.from_naive(token.inserted_at, "Etc/UTC") do
response = %{ response = %{

View file

@ -27,7 +27,7 @@ defmodule Pleroma.Web.OAuth.Token do
def exchange_token(app, auth) do def exchange_token(app, auth) do
with {:ok, auth} <- Authorization.use_token(auth), with {:ok, auth} <- Authorization.use_token(auth),
true <- auth.app_id == app.id do true <- auth.app_id == app.id do
create_token(app, User.get_by_id(auth.user_id), auth.scopes) create_token(app, User.get_cached_by_id(auth.user_id), auth.scopes)
end end
end end

View file

@ -84,7 +84,7 @@ def to_simple_form(activity, user, with_author \\ false)
def to_simple_form(%{data: %{"type" => "Create"}} = activity, user, with_author) do def to_simple_form(%{data: %{"type" => "Create"}} = activity, user, with_author) do
h = fn str -> [to_charlist(str)] end h = fn str -> [to_charlist(str)] end
object = Object.normalize(activity.data["object"]) object = Object.normalize(activity)
updated_at = object.data["published"] updated_at = object.data["published"]
inserted_at = object.data["published"] inserted_at = object.data["published"]

View file

@ -294,7 +294,7 @@ def make_user(uri, update \\ false) do
} }
with false <- update, with false <- update,
%User{} = user <- User.get_by_ap_id(data.ap_id) do %User{} = user <- User.get_cached_by_ap_id(data.ap_id) do
{:ok, user} {:ok, user}
else else
_e -> User.insert_or_update_user(data) _e -> User.insert_or_update_user(data)

View file

@ -21,8 +21,10 @@ defmodule Pleroma.Web.Push.Impl do
@doc "Performs sending notifications for user subscriptions" @doc "Performs sending notifications for user subscriptions"
@spec perform(Notification.t()) :: list(any) | :error @spec perform(Notification.t()) :: list(any) | :error
def perform( def perform(
%{activity: %{data: %{"type" => activity_type}, id: activity_id}, user_id: user_id} = %{
notif activity: %{data: %{"type" => activity_type}, id: activity_id} = activity,
user_id: user_id
} = notif
) )
when activity_type in @types do when activity_type in @types do
actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
@ -30,13 +32,14 @@ def perform(
type = Activity.mastodon_notification_type(notif.activity) type = Activity.mastodon_notification_type(notif.activity)
gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key) gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key)
avatar_url = User.avatar_url(actor) avatar_url = User.avatar_url(actor)
object = Object.normalize(activity)
for subscription <- fetch_subsriptions(user_id), for subscription <- fetch_subsriptions(user_id),
get_in(subscription.data, ["alerts", type]) do get_in(subscription.data, ["alerts", type]) do
%{ %{
title: format_title(notif), title: format_title(notif),
access_token: subscription.token.token, access_token: subscription.token.token,
body: format_body(notif, actor), body: format_body(notif, actor, object),
notification_id: notif.id, notification_id: notif.id,
notification_type: type, notification_type: type,
icon: avatar_url, icon: avatar_url,
@ -95,25 +98,25 @@ def build_sub(subscription) do
end end
def format_body( def format_body(
%{activity: %{data: %{"type" => "Create", "object" => %{"content" => content}}}}, %{activity: %{data: %{"type" => "Create"}}},
actor actor,
%{data: %{"content" => content}}
) do ) do
"@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}" "@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}"
end end
def format_body( def format_body(
%{activity: %{data: %{"type" => "Announce", "object" => activity_id}}}, %{activity: %{data: %{"type" => "Announce"}}},
actor actor,
%{data: %{"content" => content}}
) do ) do
%Activity{data: %{"object" => %{"id" => object_id}}} = Activity.get_by_ap_id(activity_id)
%Object{data: %{"content" => content}} = Object.get_by_ap_id(object_id)
"@#{actor.nickname} repeated: #{Utils.scrub_html_and_truncate(content, 80)}" "@#{actor.nickname} repeated: #{Utils.scrub_html_and_truncate(content, 80)}"
end end
def format_body( def format_body(
%{activity: %{data: %{"type" => type}}}, %{activity: %{data: %{"type" => type}}},
actor actor,
_object
) )
when type in ["Follow", "Like"] do when type in ["Follow", "Like"] do
case type do case type do

View file

@ -135,6 +135,7 @@ defmodule Pleroma.Web.Router do
post("/password_reset", UtilController, :password_reset) post("/password_reset", UtilController, :password_reset)
get("/emoji", UtilController, :emoji) get("/emoji", UtilController, :emoji)
get("/captcha", UtilController, :captcha) get("/captcha", UtilController, :captcha)
get("/healthcheck", UtilController, :healthcheck)
end end
scope "/api/pleroma", Pleroma.Web do scope "/api/pleroma", Pleroma.Web do
@ -394,6 +395,8 @@ defmodule Pleroma.Web.Router do
get("/accounts/:id", MastodonAPIController, :user) get("/accounts/:id", MastodonAPIController, :user)
get("/search", MastodonAPIController, :search) get("/search", MastodonAPIController, :search)
get("/pleroma/accounts/:id/favourites", MastodonAPIController, :user_favourites)
end end
end end

View file

@ -81,7 +81,7 @@ def handle_cast(%{action: :stream, topic: "list", item: item}, topics) do
_ -> _ ->
Pleroma.List.get_lists_from_activity(item) Pleroma.List.get_lists_from_activity(item)
|> Enum.filter(fn list -> |> Enum.filter(fn list ->
owner = User.get_by_id(list.user_id) owner = User.get_cached_by_id(list.user_id)
Visibility.visible_for_user?(item, owner) Visibility.visible_for_user?(item, owner)
end) end)

View file

@ -22,7 +22,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
def show_password_reset(conn, %{"token" => token}) do def show_password_reset(conn, %{"token" => token}) do
with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}), with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}),
%User{} = user <- User.get_by_id(token.user_id) do %User{} = user <- User.get_cached_by_id(token.user_id) do
render(conn, "password_reset.html", %{ render(conn, "password_reset.html", %{
token: token, token: token,
user: user user: user
@ -113,13 +113,13 @@ defp is_status?(acct) do
def do_remote_follow(conn, %{ def do_remote_follow(conn, %{
"authorization" => %{"name" => username, "password" => password, "id" => id} "authorization" => %{"name" => username, "password" => password, "id" => id}
}) do }) do
followee = User.get_by_id(id) followee = User.get_cached_by_id(id)
avatar = User.avatar_url(followee) avatar = User.avatar_url(followee)
name = followee.nickname name = followee.nickname
with %User{} = user <- User.get_cached_by_nickname(username), with %User{} = user <- User.get_cached_by_nickname(username),
true <- Pbkdf2.checkpw(password, user.password_hash), true <- Pbkdf2.checkpw(password, user.password_hash),
%User{} = _followed <- User.get_by_id(id), %User{} = _followed <- User.get_cached_by_id(id),
{:ok, follower} <- User.follow(user, followee), {:ok, follower} <- User.follow(user, followee),
{:ok, _activity} <- ActivityPub.follow(follower, followee) do {:ok, _activity} <- ActivityPub.follow(follower, followee) do
conn conn
@ -141,7 +141,7 @@ def do_remote_follow(conn, %{
end end
def do_remote_follow(%{assigns: %{user: user}} = conn, %{"user" => %{"id" => id}}) do def do_remote_follow(%{assigns: %{user: user}} = conn, %{"user" => %{"id" => id}}) do
with %User{} = followee <- User.get_by_id(id), with %User{} = followee <- User.get_cached_by_id(id),
{:ok, follower} <- User.follow(user, followee), {:ok, follower} <- User.follow(user, followee),
{:ok, _activity} <- ActivityPub.follow(follower, followee) do {:ok, _activity} <- ActivityPub.follow(follower, followee) do
conn conn
@ -286,7 +286,7 @@ def emoji(conn, _params) do
emoji = emoji =
Emoji.get_all() Emoji.get_all()
|> Enum.map(fn {short_code, path, tags} -> |> Enum.map(fn {short_code, path, tags} ->
{short_code, %{image_url: path, tags: String.split(tags, ",")}} {short_code, %{image_url: path, tags: tags}}
end) end)
|> Enum.into(%{}) |> Enum.into(%{})
@ -363,4 +363,22 @@ def delete_account(%{assigns: %{user: user}} = conn, params) do
def captcha(conn, _params) do def captcha(conn, _params) do
json(conn, Pleroma.Captcha.new()) json(conn, Pleroma.Captcha.new())
end end
def healthcheck(conn, _params) do
info =
if Pleroma.Config.get([:instance, :healthcheck]) do
Pleroma.Healthcheck.system_info()
else
%{}
end
conn =
if info[:healthy] do
conn
else
Plug.Conn.put_status(conn, :service_unavailable)
end
json(conn, info)
end
end end

View file

@ -240,7 +240,7 @@ def get_user(user \\ nil, params) do
end end
%{"screen_name" => nickname} -> %{"screen_name" => nickname} ->
case User.get_by_nickname(nickname) do case User.get_cached_by_nickname(nickname) do
nil -> {:error, "No user with such screen_name"} nil -> {:error, "No user with such screen_name"}
target -> {:ok, target} target -> {:ok, target}
end end

View file

@ -434,7 +434,7 @@ def password_reset(conn, params) do
end end
def confirm_email(conn, %{"user_id" => uid, "token" => token}) do def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
with %User{} = user <- User.get_by_id(uid), with %User{} = user <- User.get_cached_by_id(uid),
true <- user.local, true <- user.local,
true <- user.info.confirmation_pending, true <- user.info.confirmation_pending,
true <- user.info.confirmation_token == token, true <- user.info.confirmation_token == token,
@ -587,7 +587,7 @@ def friend_requests(conn, params) do
def approve_friend_request(conn, %{"user_id" => uid} = _params) do def approve_friend_request(conn, %{"user_id" => uid} = _params) do
with followed <- conn.assigns[:user], with followed <- conn.assigns[:user],
%User{} = follower <- User.get_by_id(uid), %User{} = follower <- User.get_cached_by_id(uid),
{:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
conn conn
|> put_view(UserView) |> put_view(UserView)
@ -599,7 +599,7 @@ def approve_friend_request(conn, %{"user_id" => uid} = _params) do
def deny_friend_request(conn, %{"user_id" => uid} = _params) do def deny_friend_request(conn, %{"user_id" => uid} = _params) do
with followed <- conn.assigns[:user], with followed <- conn.assigns[:user],
%User{} = follower <- User.get_by_id(uid), %User{} = follower <- User.get_cached_by_id(uid),
{:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
conn conn
|> put_view(UserView) |> put_view(UserView)
@ -632,7 +632,7 @@ def raw_empty_array(conn, _params) do
defp build_info_cng(user, params) do defp build_info_cng(user, params) do
info_params = info_params =
["no_rich_text", "locked", "hide_followers", "hide_follows", "show_role"] ["no_rich_text", "locked", "hide_followers", "hide_follows", "hide_favorites", "show_role"]
|> Enum.reduce(%{}, fn key, res -> |> Enum.reduce(%{}, fn key, res ->
if value = params[key] do if value = params[key] do
Map.put(res, key, value == "true") Map.put(res, key, value == "true")

View file

@ -74,58 +74,49 @@ defp do_render("user.json", %{user: user = %User{}} = assigns) do
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end) |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end) |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
data = %{
"created_at" => user.inserted_at |> Utils.format_naive_asctime(),
"description" => HTML.strip_tags((user.bio || "") |> String.replace("<br>", "\n")),
"description_html" => HTML.filter_tags(user.bio, User.html_filter_policy(for_user)),
"favourites_count" => 0,
"followers_count" => user_info[:follower_count],
"following" => following,
"follows_you" => follows_you,
"statusnet_blocking" => statusnet_blocking,
"friends_count" => user_info[:following_count],
"id" => user.id,
"name" => user.name || user.nickname,
"name_html" =>
if(user.name,
do: HTML.strip_tags(user.name) |> Formatter.emojify(emoji),
else: user.nickname
),
"profile_image_url" => image,
"profile_image_url_https" => image,
"profile_image_url_profile_size" => image,
"profile_image_url_original" => image,
"rights" => %{
"delete_others_notice" => !!user.info.is_moderator,
"admin" => !!user.info.is_admin
},
"screen_name" => user.nickname,
"statuses_count" => user_info[:note_count],
"statusnet_profile_url" => user.ap_id,
"cover_photo" => User.banner_url(user) |> MediaProxy.url(),
"background_image" => image_url(user.info.background) |> MediaProxy.url(),
"is_local" => user.local,
"locked" => user.info.locked,
"default_scope" => user.info.default_scope,
"no_rich_text" => user.info.no_rich_text,
"hide_followers" => user.info.hide_followers,
"hide_follows" => user.info.hide_follows,
"fields" => fields,
# Pleroma extension
"pleroma" =>
%{
"confirmation_pending" => user_info.confirmation_pending,
"tags" => user.tags
}
|> maybe_with_activation_status(user, for_user)
}
data = data =
if(user.info.is_admin || user.info.is_moderator, %{
do: maybe_with_role(data, user, for_user), "created_at" => user.inserted_at |> Utils.format_naive_asctime(),
else: data "description" => HTML.strip_tags((user.bio || "") |> String.replace("<br>", "\n")),
) "description_html" => HTML.filter_tags(user.bio, User.html_filter_policy(for_user)),
"favourites_count" => 0,
"followers_count" => user_info[:follower_count],
"following" => following,
"follows_you" => follows_you,
"statusnet_blocking" => statusnet_blocking,
"friends_count" => user_info[:following_count],
"id" => user.id,
"name" => user.name || user.nickname,
"name_html" =>
if(user.name,
do: HTML.strip_tags(user.name) |> Formatter.emojify(emoji),
else: user.nickname
),
"profile_image_url" => image,
"profile_image_url_https" => image,
"profile_image_url_profile_size" => image,
"profile_image_url_original" => image,
"screen_name" => user.nickname,
"statuses_count" => user_info[:note_count],
"statusnet_profile_url" => user.ap_id,
"cover_photo" => User.banner_url(user) |> MediaProxy.url(),
"background_image" => image_url(user.info.background) |> MediaProxy.url(),
"is_local" => user.local,
"locked" => user.info.locked,
"hide_followers" => user.info.hide_followers,
"hide_follows" => user.info.hide_follows,
"fields" => fields,
# Pleroma extension
"pleroma" =>
%{
"confirmation_pending" => user_info.confirmation_pending,
"tags" => user.tags
}
|> maybe_with_activation_status(user, for_user)
}
|> maybe_with_user_settings(user, for_user)
|> maybe_with_role(user, for_user)
if assigns[:token] do if assigns[:token] do
Map.put(data, "token", token_string(assigns[:token])) Map.put(data, "token", token_string(assigns[:token]))
@ -141,15 +132,35 @@ defp maybe_with_activation_status(data, user, %User{info: %{is_admin: true}}) do
defp maybe_with_activation_status(data, _, _), do: data defp maybe_with_activation_status(data, _, _), do: data
defp maybe_with_role(data, %User{id: id} = user, %User{id: id}) do defp maybe_with_role(data, %User{id: id} = user, %User{id: id}) do
Map.merge(data, %{"role" => role(user), "show_role" => user.info.show_role}) Map.merge(data, %{
"role" => role(user),
"show_role" => user.info.show_role,
"rights" => %{
"delete_others_notice" => !!user.info.is_moderator,
"admin" => !!user.info.is_admin
}
})
end end
defp maybe_with_role(data, %User{info: %{show_role: true}} = user, _user) do defp maybe_with_role(data, %User{info: %{show_role: true}} = user, _user) do
Map.merge(data, %{"role" => role(user)}) Map.merge(data, %{
"role" => role(user),
"rights" => %{
"delete_others_notice" => !!user.info.is_moderator,
"admin" => !!user.info.is_admin
}
})
end end
defp maybe_with_role(data, _, _), do: data defp maybe_with_role(data, _, _), do: data
defp maybe_with_user_settings(data, %User{info: info, id: id} = _user, %User{id: id}) do
data
|> Kernel.put_in(["default_scope"], info.default_scope)
|> Kernel.put_in(["no_rich_text"], info.no_rich_text)
end
defp maybe_with_user_settings(data, _, _), do: data
defp role(%User{info: %{:is_admin => true}}), do: "admin" defp role(%User{info: %{:is_admin => true}}), do: "admin"
defp role(%User{info: %{:is_moderator => true}}), do: "moderator" defp role(%User{info: %{:is_moderator => true}}), do: "moderator"
defp role(_), do: "member" defp role(_), do: "member"

View file

@ -37,7 +37,7 @@ def webfinger(resource, fmt) when fmt in ["XML", "JSON"] do
regex = ~r/(acct:)?(?<username>\w+)@#{host}/ regex = ~r/(acct:)?(?<username>\w+)@#{host}/
with %{"username" => username} <- Regex.named_captures(regex, resource), with %{"username" => username} <- Regex.named_captures(regex, resource),
%User{} = user <- User.get_by_nickname(username) do %User{} = user <- User.get_cached_by_nickname(username) do
{:ok, represent_user(user, fmt)} {:ok, represent_user(user, fmt)}
else else
_e -> _e ->

View file

@ -84,6 +84,7 @@ defp deps do
{:ex_aws, "~> 2.0"}, {:ex_aws, "~> 2.0"},
{:ex_aws_s3, "~> 2.0"}, {:ex_aws_s3, "~> 2.0"},
{:earmark, "~> 1.3"}, {:earmark, "~> 1.3"},
{:bbcode, "~> 0.1"},
{:ex_machina, "~> 2.3", only: :test}, {:ex_machina, "~> 2.3", only: :test},
{:credo, "~> 0.9.3", only: [:dev, :test]}, {:credo, "~> 0.9.3", only: [:dev, :test]},
{:mock, "~> 0.3.1", only: :test}, {:mock, "~> 0.3.1", only: :test},

View file

@ -2,6 +2,7 @@
"accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm"}, "accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm"},
"auto_linker": {:git, "https://git.pleroma.social/pleroma/auto_linker.git", "90613b4bae875a3610c275b7056b61ffdd53210d", [ref: "90613b4bae875a3610c275b7056b61ffdd53210d"]}, "auto_linker": {:git, "https://git.pleroma.social/pleroma/auto_linker.git", "90613b4bae875a3610c275b7056b61ffdd53210d", [ref: "90613b4bae875a3610c275b7056b61ffdd53210d"]},
"base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"}, "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"},
"bbcode": {:hex, :bbcode, "0.1.0", "400e618b640b635261611d7fb7f79d104917fc5b084aae371ab6b08477cb035b", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"},
"cachex": {:hex, :cachex, "3.0.3", "4e2d3e05814a5738f5ff3903151d5c25636d72a3527251b753f501ad9c657967", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm"}, "cachex": {:hex, :cachex, "3.0.3", "4e2d3e05814a5738f5ff3903151d5c25636d72a3527251b753f501ad9c657967", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm"},
"calendar": {:hex, :calendar, "0.17.5", "0ff5b09a60b9677683aa2a6fee948558660501c74a289103ea099806bc41a352", [:mix], [{:tzdata, "~> 0.5.20 or ~> 0.1.201603", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, "calendar": {:hex, :calendar, "0.17.5", "0ff5b09a60b9677683aa2a6fee948558660501c74a289103ea099806bc41a352", [:mix], [{:tzdata, "~> 0.5.20 or ~> 0.1.201603", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},

View file

@ -0,0 +1,14 @@
defmodule Pleroma.Repo.Migrations.CreateBookmarks do
use Ecto.Migration
def change do
create table(:bookmarks) do
add(:user_id, references(:users, type: :uuid, on_delete: :delete_all))
add(:activity_id, references(:activities, type: :uuid, on_delete: :delete_all))
timestamps()
end
create(unique_index(:bookmarks, [:user_id, :activity_id]))
end
end

View file

@ -0,0 +1,29 @@
defmodule Pleroma.Repo.Migrations.MigrateOldBookmarks do
use Ecto.Migration
import Ecto.Query
alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.User
alias Pleroma.Repo
def change do
query =
from(u in User,
where: u.local == true,
where: fragment("array_length(bookmarks, 1)") > 0,
select: %{id: u.id, bookmarks: fragment("bookmarks")}
)
Repo.stream(query)
|> Enum.each(fn %{id: user_id, bookmarks: bookmarks} ->
Enum.each(bookmarks, fn ap_id ->
activity = Activity.get_create_by_object_ap_id(ap_id)
unless is_nil(activity), do: {:ok, _} = Bookmark.create(user_id, activity.id)
end)
end)
alter table(:users) do
remove(:bookmarks)
end
end
end

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

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