Merge branch 'develop' into feature/return-link-for-password-reset

This commit is contained in:
Maxim Filippov 2019-09-24 13:00:48 +03:00
commit 5e9759cd7d
114 changed files with 1973 additions and 324 deletions

View File

@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased] ## [Unreleased]
### Added ### Added
- Refreshing poll results for remote polls - Refreshing poll results for remote polls
- Admin API: Add ability to require password reset
### Changed ### Changed
- **Breaking:** Elixir >=1.8 is now required (was >= 1.7) - **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
- Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings) - Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings)
@ -13,6 +15,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Admin API: Return `total` when querying for reports - Admin API: Return `total` when querying for reports
- Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`) - Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`)
- Admin API: Return link alongside with token on password reset - Admin API: Return link alongside with token on password reset
### Fixed
- Mastodon API: Fix private and direct statuses not being filtered out from the public timeline for an authenticated user (`GET /api/v1/timelines/public`)
## [1.1.0] - 2019-??-?? ## [1.1.0] - 2019-??-??
### Security ### Security
@ -40,6 +44,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- AdminAPI: Add "godmode" while fetching user statuses (i.e. admin can see private statuses) - AdminAPI: Add "godmode" while fetching user statuses (i.e. admin can see private statuses)
- Improve digest email template - Improve digest email template
Pagination: (optional) return `total` alongside with `items` when paginating Pagination: (optional) return `total` alongside with `items` when paginating
- Add `rel="ugc"` to all links in statuses, to prevent SEO spam
### Fixed ### Fixed
- Following from Osada - Following from Osada

View File

@ -109,6 +109,7 @@
config :pleroma, Pleroma.Uploaders.S3, config :pleroma, Pleroma.Uploaders.S3,
bucket: nil, bucket: nil,
streaming_enabled: true,
public_endpoint: "https://s3.amazonaws.com" public_endpoint: "https://s3.amazonaws.com"
config :pleroma, Pleroma.Uploaders.MDII, config :pleroma, Pleroma.Uploaders.MDII,
@ -122,7 +123,8 @@
# 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`
Custom: ["/emoji/*.png", "/emoji/**/*.png"] Custom: ["/emoji/*.png", "/emoji/**/*.png"]
], ],
default_manifest: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json" default_manifest: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json",
shared_pack_cache_seconds_per_file: 60
config :pleroma, :uri_schemes, config :pleroma, :uri_schemes,
valid_schemes: [ valid_schemes: [
@ -507,7 +509,7 @@
class: false, class: false,
strip_prefix: false, strip_prefix: false,
new_window: false, new_window: false,
rel: false rel: "ugc"
] ]
config :pleroma, :ldap, config :pleroma, :ldap,

View File

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

View File

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

View File

@ -318,6 +318,14 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
``` ```
## `/api/pleroma/admin/users/:nickname/force_password_reset`
### Force passord reset for a user with a given nickname
- Methods: `PATCH`
- Params: none
- Response: none (code `204`)
## `/api/pleroma/admin/reports` ## `/api/pleroma/admin/reports`
### Get a list of reports ### Get a list of reports
- Method `GET` - Method `GET`
@ -741,3 +749,10 @@ Compile time settings (need instance reboot):
} }
] ]
``` ```
## `POST /api/pleroma/admin/reload_emoji`
### Reload the instance's custom emoji
* Method `POST`
* Authentication: required
* Params: None
* Response: JSON, "ok" and 200 status

View File

@ -365,3 +365,68 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa
* Params: * Params:
* `recipients`: A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though. * `recipients`: A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though.
* Response: JSON, statuses (200 - healthy, 503 unhealthy) * Response: JSON, statuses (200 - healthy, 503 unhealthy)
## `GET /api/pleroma/emoji/packs`
### Lists the custom emoji packs on the server
* Method `GET`
* Authentication: not required
* Params: None
* Response: JSON, "ok" and 200 status and the JSON hashmap of "pack name" to "pack contents"
## `PUT /api/pleroma/emoji/packs/:name`
### Creates an empty custom emoji pack
* Method `PUT`
* Authentication: required
* Params: None
* Response: JSON, "ok" and 200 status or 409 if the pack with that name already exists
## `DELETE /api/pleroma/emoji/packs/:name`
### Delete a custom emoji pack
* Method `DELETE`
* Authentication: required
* Params: None
* Response: JSON, "ok" and 200 status or 500 if there was an error deleting the pack
## `POST /api/pleroma/emoji/packs/:name/update_file`
### Update a file in a custom emoji pack
* Method `POST`
* Authentication: required
* Params:
* if the `action` is `add`, adds an emoji named `shortcode` to the pack `pack_name`,
that means that the emoji file needs to be uploaded with the request
(thus requiring it to be a multipart request) and be named `file`.
There can also be an optional `filename` that will be the new emoji file name
(if it's not there, the name will be taken from the uploaded file).
* if the `action` is `update`, changes emoji shortcode
(from `shortcode` to `new_shortcode` or moves the file (from the current filename to `new_filename`)
* if the `action` is `remove`, removes the emoji named `shortcode` and it's associated file
* Response: JSON, updated "files" section of the pack and 200 status, 409 if the trying to use a shortcode
that is already taken, 400 if there was an error with the shortcode, filename or file (additional info
in the "error" part of the response JSON)
## `POST /api/pleroma/emoji/packs/:name/update_metadata`
### Updates (replaces) pack metadata
* Method `POST`
* Authentication: required
* Params:
* `new_data`: new metadata to replace the old one
* Response: JSON, updated "metadata" section of the pack and 200 status or 400 if there was a
problem with the new metadata (the error is specified in the "error" part of the response JSON)
## `POST /api/pleroma/emoji/packs/download_from`
### Requests the instance to download the pack from another instance
* Method `POST`
* Authentication: required
* Params:
* `instance_address`: the address of the instance to download from
* `pack_name`: the pack to download from that instance
* Response: JSON, "ok" and 200 status if the pack was downloaded, or 500 if there were
errors downloading the pack
## `GET /api/pleroma/emoji/packs/:name/download_shared`
### Requests a local pack from the instance
* Method `GET`
* Authentication: not required
* Params: None
* Response: the archive of the pack with a 200 status code, 403 if the pack is not set as shared,
404 if the pack does not exist

View File

@ -23,6 +23,7 @@ Note: `strip_exif` has been replaced by `Pleroma.Upload.Filter.Mogrify`.
* `truncated_namespace`: If you use S3 compatible service such as Digital Ocean Spaces or CDN, set folder name or "" etc. * `truncated_namespace`: If you use S3 compatible service such as Digital Ocean Spaces or CDN, set folder name or "" etc.
For example, when using CDN to S3 virtual host format, set "". For example, when using CDN to S3 virtual host format, set "".
At this time, write CNAME to CDN in public_endpoint. At this time, write CNAME to CDN in public_endpoint.
* `streaming_enabled`: Enable streaming uploads, when enabled the file will be sent to the server in chunks as it's being read. This may be unsupported by some providers, try disabling this if you have upload problems.
## Pleroma.Upload.Filter.Mogrify ## Pleroma.Upload.Filter.Mogrify
@ -521,7 +522,7 @@ config :auto_linker,
class: false, class: false,
strip_prefix: false, strip_prefix: false,
new_window: false, new_window: false,
rel: false rel: "ugc"
] ]
``` ```
@ -707,6 +708,8 @@ Configure OAuth 2 provider capabilities:
* `pack_extensions`: A list of file extensions for emojis, when no emoji.txt for a pack is present. Example `[".png", ".gif"]` * `pack_extensions`: A list of file extensions for emojis, when no emoji.txt for a pack is present. Example `[".png", ".gif"]`
* `groups`: Emojis are ordered in groups (tags). This is an array of key-value pairs where the key is the groupname and the value the location or array of locations. `*` can be used as a wildcard. Example `[Custom: ["/emoji/*.png", "/emoji/custom/*.png"]]` * `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). * `default_manifest`: Location of the JSON-manifest. This manifest contains information about the emoji-packs you can download. Currently only one manifest can be added (no arrays).
* `shared_pack_cache_seconds_per_file`: When an emoji pack is shared, the archive is created and cached in
memory for this amount of seconds multiplied by the number of files.
## Database options ## Database options

View File

@ -102,10 +102,14 @@ defp cachex_children do
build_cachex("rich_media", default_ttl: :timer.minutes(120), limit: 5000), build_cachex("rich_media", default_ttl: :timer.minutes(120), limit: 5000),
build_cachex("scrubber", limit: 2500), build_cachex("scrubber", limit: 2500),
build_cachex("idempotency", expiration: idempotency_expiration(), limit: 2500), build_cachex("idempotency", expiration: idempotency_expiration(), limit: 2500),
build_cachex("web_resp", limit: 2500) build_cachex("web_resp", limit: 2500),
build_cachex("emoji_packs", expiration: emoji_packs_expiration(), limit: 10)
] ]
end end
defp emoji_packs_expiration,
do: expiration(default: :timer.seconds(5 * 60), interval: :timer.seconds(60))
defp idempotency_expiration, defp idempotency_expiration,
do: expiration(default: :timer.seconds(6 * 60 * 60), interval: :timer.seconds(60)) do: expiration(default: :timer.seconds(6 * 60 * 60), interval: :timer.seconds(60))

View File

@ -122,6 +122,9 @@ defp load do
fn pack -> load_pack(Path.join(emoji_dir_path, pack), emoji_groups) end fn pack -> load_pack(Path.join(emoji_dir_path, pack), emoji_groups) end
) )
# Clear out old emojis
:ets.delete_all_objects(@ets)
true = :ets.insert(@ets, emojis) true = :ets.insert(@ets, emojis)
end end
@ -143,23 +146,38 @@ defp load do
defp load_pack(pack_dir, emoji_groups) do defp load_pack(pack_dir, emoji_groups) do
pack_name = Path.basename(pack_dir) pack_name = Path.basename(pack_dir)
emoji_txt = Path.join(pack_dir, "emoji.txt") pack_file = Path.join(pack_dir, "pack.json")
if File.exists?(emoji_txt) do if File.exists?(pack_file) do
load_from_file(emoji_txt, emoji_groups) contents = Jason.decode!(File.read!(pack_file))
else
extensions = Pleroma.Config.get([:emoji, :pack_extensions])
Logger.info( contents["files"]
"No emoji.txt found for pack \"#{pack_name}\", assuming all #{Enum.join(extensions, ", ")} files are emoji" |> Enum.map(fn {name, rel_file} ->
)
make_shortcode_to_file_map(pack_dir, extensions)
|> Enum.map(fn {shortcode, rel_file} ->
filename = Path.join("/emoji/#{pack_name}", rel_file) filename = Path.join("/emoji/#{pack_name}", rel_file)
{name, filename, pack_name}
{shortcode, filename, [to_string(match_extra(emoji_groups, filename))]}
end) end)
else
# Load from emoji.txt / all files
emoji_txt = Path.join(pack_dir, "emoji.txt")
if File.exists?(emoji_txt) do
load_from_file(emoji_txt, emoji_groups)
else
extensions = Pleroma.Config.get([:emoji, :pack_extensions])
Logger.info(
"No emoji.txt found for pack \"#{pack_name}\", assuming all #{
Enum.join(extensions, ", ")
} files are emoji"
)
make_shortcode_to_file_map(pack_dir, extensions)
|> Enum.map(fn {shortcode, rel_file} ->
filename = Path.join("/emoji/#{pack_name}", rel_file)
{shortcode, filename, [to_string(match_extra(emoji_groups, filename))]}
end)
end
end end
end end

View File

@ -36,9 +36,9 @@ def mention_handler("@" <> nickname, buffer, opts, acc) do
nickname_text = get_nickname_text(nickname, opts) nickname_text = get_nickname_text(nickname, opts)
link = link =
"<span class='h-card'><a data-user='#{id}' class='u-url mention' href='#{ap_id}'>@<span>#{ ~s(<span class="h-card"><a data-user="#{id}" class="u-url mention" href="#{ap_id}" rel="ugc">@<span>#{
nickname_text nickname_text
}</span></a></span>" }</span></a></span>)
{link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, user})}} {link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, user})}}
@ -50,7 +50,7 @@ def mention_handler("@" <> nickname, buffer, opts, acc) do
def hashtag_handler("#" <> tag = tag_text, _buffer, _opts, acc) do def hashtag_handler("#" <> tag = tag_text, _buffer, _opts, acc) do
tag = String.downcase(tag) tag = String.downcase(tag)
url = "#{Pleroma.Web.base_url()}/tag/#{tag}" url = "#{Pleroma.Web.base_url()}/tag/#{tag}"
link = "<a class='hashtag' data-tag='#{tag}' href='#{url}' rel='tag'>#{tag_text}</a>" link = ~s(<a class="hashtag" data-tag="#{tag}" href="#{url}" rel="tag ugc">#{tag_text}</a>)
{link, %{acc | tags: MapSet.put(acc.tags, {tag_text, tag})}} {link, %{acc | tags: MapSet.put(acc.tags, {tag_text, tag})}}
end end

View File

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

View File

@ -31,6 +31,7 @@ defp maybe_reinject_internal_fields(data, %{data: %{} = old_data}) do
defp maybe_reinject_internal_fields(data, _), do: data defp maybe_reinject_internal_fields(data, _), do: data
@spec reinject_object(struct(), map()) :: {:ok, Object.t()} | {:error, any()}
defp reinject_object(struct, data) do defp reinject_object(struct, data) do
Logger.debug("Reinjecting object #{data["id"]}") Logger.debug("Reinjecting object #{data["id"]}")
@ -61,52 +62,54 @@ def refetch_object(%Object{data: %{"id" => id}} = object) do
# TODO: # TODO:
# This will create a Create activity, which we need internally at the moment. # This will create a Create activity, which we need internally at the moment.
def fetch_object_from_id(id, options \\ []) do def fetch_object_from_id(id, options \\ []) do
if object = Object.get_cached_by_ap_id(id) do with {:fetch_object, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)},
{:fetch, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)},
{:normalize, nil} <- {:normalize, Object.normalize(data, false)},
params <- prepare_activity_params(data),
{:containment, :ok} <- {:containment, Containment.contain_origin(id, params)},
{:ok, activity} <- Transmogrifier.handle_incoming(params, options),
{:object, _data, %Object{} = object} <-
{:object, data, Object.normalize(activity, false)} do
{:ok, object} {:ok, object}
else else
Logger.info("Fetching #{id} via AP") {:containment, _} ->
{:error, "Object containment failed."}
with {:fetch, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)}, {:error, {:reject, nil}} ->
{:normalize, nil} <- {:normalize, Object.normalize(data, false)}, {:reject, nil}
params <- %{
"type" => "Create", {:object, data, nil} ->
"to" => data["to"], reinject_object(%Object{}, data)
"cc" => data["cc"],
# Should we seriously keep this attributedTo thing? {:normalize, object = %Object{}} ->
"actor" => data["actor"] || data["attributedTo"],
"object" => data
},
{:containment, :ok} <- {:containment, Containment.contain_origin(id, params)},
{:ok, activity} <- Transmogrifier.handle_incoming(params, options),
{:object, _data, %Object{} = object} <-
{:object, data, Object.normalize(activity, false)} do
{:ok, object} {:ok, object}
else
{:containment, _} ->
{:error, "Object containment failed."}
{:error, {:reject, nil}} -> {:fetch_object, %Object{} = object} ->
{:reject, nil} {:ok, object}
{:object, data, nil} -> _e ->
reinject_object(%Object{}, data) # Only fallback when receiving a fetch/normalization error with ActivityPub
Logger.info("Couldn't get object via AP, trying out OStatus fetching...")
{:normalize, object = %Object{}} -> # FIXME: OStatus Object Containment?
{:ok, object} case OStatus.fetch_activity_from_url(id) do
{:ok, [activity | _]} -> {:ok, Object.normalize(activity, false)}
_e -> e -> e
# Only fallback when receiving a fetch/normalization error with ActivityPub end
Logger.info("Couldn't get object via AP, trying out OStatus fetching...")
# FIXME: OStatus Object Containment?
case OStatus.fetch_activity_from_url(id) do
{:ok, [activity | _]} -> {:ok, Object.normalize(activity, false)}
e -> e
end
end
end end
end end
defp prepare_activity_params(data) do
%{
"type" => "Create",
"to" => data["to"],
"cc" => data["cc"],
# Should we seriously keep this attributedTo thing?
"actor" => data["actor"] || data["attributedTo"],
"object" => data
}
end
def fetch_object_from_id!(id, options \\ []) do def fetch_object_from_id!(id, options \\ []) do
with {:ok, object} <- fetch_object_from_id(id, options) do with {:ok, object} <- fetch_object_from_id(id, options) do
object object

View File

@ -38,16 +38,26 @@ def get_file(file) do
def put_file(%Pleroma.Upload{} = upload) do def put_file(%Pleroma.Upload{} = upload) do
config = Config.get([__MODULE__]) config = Config.get([__MODULE__])
bucket = Keyword.get(config, :bucket) bucket = Keyword.get(config, :bucket)
streaming = Keyword.get(config, :streaming_enabled)
s3_name = strict_encode(upload.path) s3_name = strict_encode(upload.path)
op = op =
upload.tempfile if streaming do
|> ExAws.S3.Upload.stream_file() upload.tempfile
|> ExAws.S3.upload(bucket, s3_name, [ |> ExAws.S3.Upload.stream_file()
{:acl, :public_read}, |> ExAws.S3.upload(bucket, s3_name, [
{:content_type, upload.content_type} {:acl, :public_read},
]) {:content_type, upload.content_type}
])
else
{:ok, file_data} = File.read(upload.tempfile)
ExAws.S3.put_object(bucket, s3_name, file_data, [
{:acl, :public_read},
{:content_type, upload.content_type}
])
end
case ExAws.request(op) do case ExAws.request(op) do
{:ok, _} -> {:ok, _} ->

View File

@ -269,6 +269,7 @@ def password_update_changeset(struct, params) do
|> validate_required([:password, :password_confirmation]) |> validate_required([:password, :password_confirmation])
|> validate_confirmation(:password) |> validate_confirmation(:password)
|> put_password_hash |> put_password_hash
|> put_embed(:info, User.Info.set_password_reset_pending(struct.info, false))
end end
@spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()} @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
@ -285,6 +286,20 @@ def reset_password(%User{id: user_id} = user, data) do
end end
end end
def force_password_reset_async(user) do
BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
end
@spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def force_password_reset(user) do
info_cng = User.Info.set_password_reset_pending(user.info, true)
user
|> change()
|> put_embed(:info, info_cng)
|> update_and_set_cache()
end
def register_changeset(struct, params \\ %{}, opts \\ []) do def register_changeset(struct, params \\ %{}, opts \\ []) do
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000) bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100) name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
@ -1115,6 +1130,8 @@ def delete(%User{} = user) do
BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id}) BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
end end
def perform(:force_password_reset, user), do: force_password_reset(user)
@spec perform(atom(), User.t()) :: {:ok, User.t()} @spec perform(atom(), User.t()) :: {:ok, User.t()}
def perform(:delete, %User{} = user) do def perform(:delete, %User{} = user) do
{:ok, _user} = ActivityPub.delete(user) {:ok, _user} = ActivityPub.delete(user)

View File

@ -20,6 +20,7 @@ defmodule Pleroma.User.Info do
field(:following_count, :integer, default: nil) field(:following_count, :integer, default: nil)
field(:locked, :boolean, default: false) field(:locked, :boolean, default: false)
field(:confirmation_pending, :boolean, default: false) field(:confirmation_pending, :boolean, default: false)
field(:password_reset_pending, :boolean, default: false)
field(:confirmation_token, :string, default: nil) field(:confirmation_token, :string, default: nil)
field(:default_scope, :string, default: "public") field(:default_scope, :string, default: "public")
field(:blocks, {:array, :string}, default: []) field(:blocks, {:array, :string}, default: [])
@ -82,6 +83,14 @@ def set_activation_status(info, deactivated) do
|> validate_required([:deactivated]) |> validate_required([:deactivated])
end end
def set_password_reset_pending(info, pending) do
params = %{password_reset_pending: pending}
info
|> cast(params, [:password_reset_pending])
|> validate_required([:password_reset_pending])
end
def update_notification_settings(info, settings) do def update_notification_settings(info, settings) do
settings = settings =
settings settings

View File

@ -520,9 +520,10 @@ def fetch_latest_activity_id_for_context(context, opts \\ %{}) do
end end
def fetch_public_activities(opts \\ %{}) do def fetch_public_activities(opts \\ %{}) do
q = fetch_activities_query([Pleroma.Constants.as_public()], opts) opts = Map.drop(opts, ["user"])
q [Pleroma.Constants.as_public()]
|> fetch_activities_query(opts)
|> restrict_unlisted() |> restrict_unlisted()
|> Pagination.fetch_paginated(opts) |> Pagination.fetch_paginated(opts)
|> Enum.reverse() |> Enum.reverse()

View File

@ -49,7 +49,8 @@ def user(conn, %{"nickname" => nickname}) do
{:ok, user} <- User.ensure_keys_present(user) do {:ok, user} <- User.ensure_keys_present(user) do
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("user.json", %{user: user})) |> put_view(UserView)
|> render("user.json", %{user: user})
else else
nil -> {:error, :not_found} nil -> {:error, :not_found}
end end
@ -90,7 +91,8 @@ def object_likes(conn, %{"uuid" => uuid, "page" => page}) do
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(ObjectView.render("likes.json", ap_id, likes, page)) |> put_view(ObjectView)
|> render("likes.json", %{ap_id: ap_id, likes: likes, page: page})
else else
{:public?, false} -> {:public?, false} ->
{:error, :not_found} {:error, :not_found}
@ -104,7 +106,8 @@ def object_likes(conn, %{"uuid" => uuid}) do
likes <- Utils.get_object_likes(object) do likes <- Utils.get_object_likes(object) do
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(ObjectView.render("likes.json", ap_id, likes)) |> put_view(ObjectView)
|> render("likes.json", %{ap_id: ap_id, likes: likes})
else else
{:public?, false} -> {:public?, false} ->
{:error, :not_found} {:error, :not_found}
@ -158,7 +161,8 @@ defp set_cache_ttl_for(conn, entity) do
def following(%{assigns: %{relay: true}} = conn, _params) do def following(%{assigns: %{relay: true}} = conn, _params) do
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("following.json", %{user: Relay.get_actor()})) |> put_view(UserView)
|> render("following.json", %{user: Relay.get_actor()})
end end
def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
@ -170,7 +174,8 @@ def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "p
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("following.json", %{user: user, page: page, for: for_user})) |> put_view(UserView)
|> render("following.json", %{user: user, page: page, for: for_user})
else else
{:show_follows, _} -> {:show_follows, _} ->
conn conn
@ -184,7 +189,8 @@ def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) d
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("following.json", %{user: user, for: for_user})) |> put_view(UserView)
|> render("following.json", %{user: user, for: for_user})
end end
end end
@ -192,7 +198,8 @@ def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) d
def followers(%{assigns: %{relay: true}} = conn, _params) do def followers(%{assigns: %{relay: true}} = conn, _params) do
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("followers.json", %{user: Relay.get_actor()})) |> put_view(UserView)
|> render("followers.json", %{user: Relay.get_actor()})
end end
def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do
@ -204,7 +211,8 @@ def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "p
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("followers.json", %{user: user, page: page, for: for_user})) |> put_view(UserView)
|> render("followers.json", %{user: user, page: page, for: for_user})
else else
{:show_followers, _} -> {:show_followers, _} ->
conn conn
@ -218,7 +226,8 @@ def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) d
{user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("followers.json", %{user: user, for: for_user})) |> put_view(UserView)
|> render("followers.json", %{user: user, for: for_user})
end end
end end
@ -227,7 +236,8 @@ def outbox(conn, %{"nickname" => nickname} = params) do
{:ok, user} <- User.ensure_keys_present(user) do {:ok, user} <- User.ensure_keys_present(user) do
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("outbox.json", %{user: user, max_id: params["max_id"]})) |> put_view(UserView)
|> render("outbox.json", %{user: user, max_id: params["max_id"]})
end end
end end
@ -275,7 +285,8 @@ defp represent_service_actor(%User{} = user, conn) do
with {:ok, user} <- User.ensure_keys_present(user) do with {:ok, user} <- User.ensure_keys_present(user) do
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("user.json", %{user: user})) |> put_view(UserView)
|> render("user.json", %{user: user})
else else
nil -> {:error, :not_found} nil -> {:error, :not_found}
end end
@ -296,7 +307,8 @@ def internal_fetch(conn, _params) do
def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
|> json(UserView.render("user.json", %{user: user})) |> put_view(UserView)
|> render("user.json", %{user: user})
end end
def whoami(_conn, _params), do: {:error, :not_found} def whoami(_conn, _params), do: {:error, :not_found}

View File

@ -42,8 +42,7 @@ def fix_object(object, options \\ []) do
end end
def fix_summary(%{"summary" => nil} = object) do def fix_summary(%{"summary" => nil} = object) do
object Map.put(object, "summary", "")
|> Map.put("summary", "")
end end
def fix_summary(%{"summary" => _} = object) do def fix_summary(%{"summary" => _} = object) do
@ -51,10 +50,7 @@ def fix_summary(%{"summary" => _} = object) do
object object
end end
def fix_summary(object) do def fix_summary(object), do: Map.put(object, "summary", "")
object
|> Map.put("summary", "")
end
def fix_addressing_list(map, field) do def fix_addressing_list(map, field) do
cond do cond do
@ -74,13 +70,9 @@ def fix_explicit_addressing(
explicit_mentions, explicit_mentions,
follower_collection follower_collection
) do ) do
explicit_to = explicit_to = Enum.filter(to, fn x -> x in explicit_mentions end)
to
|> Enum.filter(fn x -> x in explicit_mentions end)
explicit_cc = explicit_cc = Enum.filter(to, fn x -> x not in explicit_mentions end)
to
|> Enum.filter(fn x -> x not in explicit_mentions end)
final_cc = final_cc =
(cc ++ explicit_cc) (cc ++ explicit_cc)
@ -98,13 +90,19 @@ def fix_explicit_addressing(object, _explicit_mentions, _followers_collection),
def fix_explicit_addressing(%{"directMessage" => true} = object), do: object def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
def fix_explicit_addressing(object) do def fix_explicit_addressing(object) do
explicit_mentions = explicit_mentions = Utils.determine_explicit_mentions(object)
%User{follower_address: follower_collection} =
object object
|> Utils.determine_explicit_mentions() |> Containment.get_actor()
|> User.get_cached_by_ap_id()
follower_collection = User.get_cached_by_ap_id(Containment.get_actor(object)).follower_address explicit_mentions =
explicit_mentions ++
explicit_mentions = explicit_mentions ++ [Pleroma.Constants.as_public(), follower_collection] [
Pleroma.Constants.as_public(),
follower_collection
]
fix_explicit_addressing(object, explicit_mentions, follower_collection) fix_explicit_addressing(object, explicit_mentions, follower_collection)
end end
@ -148,48 +146,25 @@ def fix_addressing(object) do
end end
def fix_actor(%{"attributedTo" => actor} = object) do def fix_actor(%{"attributedTo" => actor} = object) do
object Map.put(object, "actor", Containment.get_actor(%{"actor" => actor}))
|> Map.put("actor", Containment.get_actor(%{"actor" => actor}))
end end
def fix_in_reply_to(object, options \\ []) def fix_in_reply_to(object, options \\ [])
def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options) def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
when not is_nil(in_reply_to) do when not is_nil(in_reply_to) do
in_reply_to_id = in_reply_to_id = prepare_in_reply_to(in_reply_to)
cond do
is_bitstring(in_reply_to) ->
in_reply_to
is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
in_reply_to["id"]
is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
Enum.at(in_reply_to, 0)
# Maybe I should output an error too?
true ->
""
end
object = Map.put(object, "inReplyToAtomUri", in_reply_to_id) object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)
if Federator.allowed_incoming_reply_depth?(options[:depth]) do if Federator.allowed_incoming_reply_depth?(options[:depth]) do
case get_obj_helper(in_reply_to_id, options) do with {:ok, replied_object} <- get_obj_helper(in_reply_to_id, options),
{:ok, replied_object} -> %Activity{} = _ <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
with %Activity{} = _activity <- object
Activity.get_create_by_object_ap_id(replied_object.data["id"]) do |> Map.put("inReplyTo", replied_object.data["id"])
object |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
|> Map.put("inReplyTo", replied_object.data["id"]) |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
|> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id) |> Map.put("context", replied_object.data["context"] || object["conversation"])
|> Map.put("conversation", replied_object.data["context"] || object["conversation"]) else
|> Map.put("context", replied_object.data["context"] || object["conversation"])
else
e ->
Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
object
end
e -> e ->
Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}") Logger.error("Couldn't fetch #{inspect(in_reply_to_id)}, error: #{inspect(e)}")
object object
@ -201,6 +176,22 @@ def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
def fix_in_reply_to(object, _options), do: object def fix_in_reply_to(object, _options), do: object
defp prepare_in_reply_to(in_reply_to) do
cond do
is_bitstring(in_reply_to) ->
in_reply_to
is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
in_reply_to["id"]
is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
Enum.at(in_reply_to, 0)
true ->
""
end
end
def fix_context(object) do def fix_context(object) do
context = object["context"] || object["conversation"] || Utils.generate_context_id() context = object["context"] || object["conversation"] || Utils.generate_context_id()
@ -211,11 +202,9 @@ def fix_context(object) do
def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
attachments = attachments =
attachment Enum.map(attachment, fn data ->
|> Enum.map(fn data ->
media_type = data["mediaType"] || data["mimeType"] media_type = data["mediaType"] || data["mimeType"]
href = data["url"] || data["href"] href = data["url"] || data["href"]
url = [%{"type" => "Link", "mediaType" => media_type, "href" => href}] url = [%{"type" => "Link", "mediaType" => media_type, "href" => href}]
data data
@ -223,30 +212,25 @@ def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachm
|> Map.put("url", url) |> Map.put("url", url)
end) end)
object Map.put(object, "attachment", attachments)
|> Map.put("attachment", attachments)
end end
def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
Map.put(object, "attachment", [attachment]) object
|> Map.put("attachment", [attachment])
|> fix_attachments() |> fix_attachments()
end end
def fix_attachments(object), do: object def fix_attachments(object), do: object
def fix_url(%{"url" => url} = object) when is_map(url) do def fix_url(%{"url" => url} = object) when is_map(url) do
object Map.put(object, "url", url["href"])
|> Map.put("url", url["href"])
end end
def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do
first_element = Enum.at(url, 0) first_element = Enum.at(url, 0)
link_element = link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end)
url
|> Enum.filter(fn x -> is_map(x) end)
|> Enum.filter(fn x -> x["mimeType"] == "text/html" end)
|> Enum.at(0)
object object
|> Map.put("attachment", [first_element]) |> Map.put("attachment", [first_element])
@ -264,36 +248,32 @@ def fix_url(%{"type" => object_type, "url" => url} = object)
true -> "" true -> ""
end end
object Map.put(object, "url", url_string)
|> Map.put("url", url_string)
end end
def fix_url(object), do: object def fix_url(object), do: object
def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
emoji = tags |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
emoji = emoji =
emoji tags
|> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
|> Enum.reduce(%{}, fn data, mapping -> |> Enum.reduce(%{}, fn data, mapping ->
name = String.trim(data["name"], ":") name = String.trim(data["name"], ":")
mapping |> Map.put(name, data["icon"]["url"]) Map.put(mapping, name, data["icon"]["url"])
end) end)
# we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
emoji = Map.merge(object["emoji"] || %{}, emoji) emoji = Map.merge(object["emoji"] || %{}, emoji)
object Map.put(object, "emoji", emoji)
|> Map.put("emoji", emoji)
end end
def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
name = String.trim(tag["name"], ":") name = String.trim(tag["name"], ":")
emoji = %{name => tag["icon"]["url"]} emoji = %{name => tag["icon"]["url"]}
object Map.put(object, "emoji", emoji)
|> Map.put("emoji", emoji)
end end
def fix_emoji(object), do: object def fix_emoji(object), do: object
@ -304,17 +284,13 @@ def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
|> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end) |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
|> Enum.map(fn data -> String.slice(data["name"], 1..-1) end) |> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
combined = tag ++ tags Map.put(object, "tag", tag ++ tags)
object
|> Map.put("tag", combined)
end end
def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
combined = [tag, String.slice(hashtag, 1..-1)] combined = [tag, String.slice(hashtag, 1..-1)]
object Map.put(object, "tag", combined)
|> Map.put("tag", combined)
end end
def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag]) def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
@ -326,8 +302,7 @@ def fix_content_map(%{"contentMap" => content_map} = object) do
content_groups = Map.to_list(content_map) content_groups = Map.to_list(content_map)
{_, content} = Enum.at(content_groups, 0) {_, content} = Enum.at(content_groups, 0)
object Map.put(object, "content", content)
|> Map.put("content", content)
end end
def fix_content_map(object), do: object def fix_content_map(object), do: object
@ -336,16 +311,11 @@ def fix_type(object, options \\ [])
def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options) def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
when is_binary(reply_id) do when is_binary(reply_id) do
reply = with true <- Federator.allowed_incoming_reply_depth?(options[:depth]),
with true <- Federator.allowed_incoming_reply_depth?(options[:depth]), {:ok, %{data: %{"type" => "Question"} = _} = _} <- get_obj_helper(reply_id, options) do
{:ok, object} <- get_obj_helper(reply_id, options) do
object
end
if reply && reply.data["type"] == "Question" do
Map.put(object, "type", "Answer") Map.put(object, "type", "Answer")
else else
object _ -> object
end end
end end
@ -377,6 +347,17 @@ defp get_follow_activity(follow_object, followed) do
end end
end end
# Reduce the object list to find the reported user.
defp get_reported(objects) do
Enum.reduce_while(objects, nil, fn ap_id, _ ->
with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
{:halt, user}
else
_ -> {:cont, nil}
end
end)
end
def handle_incoming(data, options \\ []) def handle_incoming(data, options \\ [])
# Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
@ -385,31 +366,19 @@ def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} =
with context <- data["context"] || Utils.generate_context_id(), with context <- data["context"] || Utils.generate_context_id(),
content <- data["content"] || "", content <- data["content"] || "",
%User{} = actor <- User.get_cached_by_ap_id(actor), %User{} = actor <- User.get_cached_by_ap_id(actor),
# Reduce the object list to find the reported user. # Reduce the object list to find the reported user.
%User{} = account <- %User{} = account <- get_reported(objects),
Enum.reduce_while(objects, nil, fn ap_id, _ ->
with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
{:halt, user}
else
_ -> {:cont, nil}
end
end),
# Remove the reported user from the object list. # Remove the reported user from the object list.
statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
params = %{ %{
actor: actor, actor: actor,
context: context, context: context,
account: account, account: account,
statuses: statuses, statuses: statuses,
content: content, content: content,
additional: %{ additional: %{"cc" => [account.ap_id]}
"cc" => [account.ap_id]
}
} }
|> ActivityPub.flag()
ActivityPub.flag(params)
end end
end end
@ -756,8 +725,12 @@ def handle_incoming(
def handle_incoming(_, _), do: :error def handle_incoming(_, _), do: :error
@spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
def get_obj_helper(id, options \\ []) do def get_obj_helper(id, options \\ []) do
if object = Object.normalize(id, true, options), do: {:ok, object}, else: nil case Object.normalize(id, true, options) do
%Object{} = object -> {:ok, object}
_ -> nil
end
end end
def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
@ -856,27 +829,24 @@ def prepare_outgoing(%{"type" => _type} = data) do
{:ok, data} {:ok, data}
end end
def maybe_fix_object_url(data) do def maybe_fix_object_url(%{"object" => object} = data) when is_binary(object) do
if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do with false <- String.starts_with?(object, "http"),
case get_obj_helper(data["object"]) do {:fetch, {:ok, relative_object}} <- {:fetch, get_obj_helper(object)},
{:ok, relative_object} -> %{data: %{"external_url" => external_url}} when not is_nil(external_url) <-
if relative_object.data["external_url"] do relative_object do
_data = Map.put(data, "object", external_url)
data
|> Map.put("object", relative_object.data["external_url"])
else
data
end
e ->
Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
data
end
else else
data {:fetch, e} ->
Logger.error("Couldn't fetch #{object} #{inspect(e)}")
data
_ ->
data
end end
end end
def maybe_fix_object_url(data), do: data
def add_hashtags(object) do def add_hashtags(object) do
tags = tags =
(object["tag"] || []) (object["tag"] || [])
@ -894,53 +864,49 @@ def add_hashtags(object) do
tag tag
end) end)
object Map.put(object, "tag", tags)
|> Map.put("tag", tags)
end end
def add_mention_tags(object) do def add_mention_tags(object) do
mentions = mentions =
object object
|> Utils.get_notified_from_object() |> Utils.get_notified_from_object()
|> Enum.map(fn user -> |> Enum.map(&build_mention_tag/1)
%{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
end)
tags = object["tag"] || [] tags = object["tag"] || []
object Map.put(object, "tag", tags ++ mentions)
|> Map.put("tag", tags ++ mentions)
end end
def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do
user_info = add_emoji_tags(user_info) %{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
end
object def take_emoji_tags(%User{info: %{emoji: emoji} = _user_info} = _user) do
|> Map.put(:info, user_info) emoji
|> Enum.flat_map(&Map.to_list/1)
|> Enum.map(&build_emoji_tag/1)
end end
# TODO: we should probably send mtime instead of unix epoch time for updated # TODO: we should probably send mtime instead of unix epoch time for updated
def add_emoji_tags(%{"emoji" => emoji} = object) do def add_emoji_tags(%{"emoji" => emoji} = object) do
tags = object["tag"] || [] tags = object["tag"] || []
out = out = Enum.map(emoji, &build_emoji_tag/1)
emoji
|> Enum.map(fn {name, url} ->
%{
"icon" => %{"url" => url, "type" => "Image"},
"name" => ":" <> name <> ":",
"type" => "Emoji",
"updated" => "1970-01-01T00:00:00Z",
"id" => url
}
end)
object Map.put(object, "tag", tags ++ out)
|> Map.put("tag", tags ++ out)
end end
def add_emoji_tags(object) do def add_emoji_tags(object), do: object
object
defp build_emoji_tag({name, url}) do
%{
"icon" => %{"url" => url, "type" => "Image"},
"name" => ":" <> name <> ":",
"type" => "Emoji",
"updated" => "1970-01-01T00:00:00Z",
"id" => url
}
end end
def set_conversation(object) do def set_conversation(object) do
@ -960,9 +926,7 @@ def set_type(object), do: object
def add_attributed_to(object) do def add_attributed_to(object) do
attributed_to = object["attributedTo"] || object["actor"] attributed_to = object["attributedTo"] || object["actor"]
Map.put(object, "attributedTo", attributed_to)
object
|> Map.put("attributedTo", attributed_to)
end end
def prepare_attachments(object) do def prepare_attachments(object) do
@ -973,8 +937,7 @@ def prepare_attachments(object) do
%{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"} %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
end) end)
object Map.put(object, "attachment", attachments)
|> Map.put("attachment", attachments)
end end
defp strip_internal_fields(object) do defp strip_internal_fields(object) do
@ -983,12 +946,9 @@ defp strip_internal_fields(object) do
end end
defp strip_internal_tags(%{"tag" => tags} = object) do defp strip_internal_tags(%{"tag" => tags} = object) do
tags = tags = Enum.filter(tags, fn x -> is_map(x) end)
tags
|> Enum.filter(fn x -> is_map(x) end)
object Map.put(object, "tag", tags)
|> Map.put("tag", tags)
end end
defp strip_internal_tags(object), do: object defp strip_internal_tags(object), do: object
@ -1073,16 +1033,11 @@ def maybe_retire_websub(ap_id) do
end end
end end
def maybe_fix_user_url(data) do def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
if is_map(data["url"]) do Map.put(data, "url", url["href"])
Map.put(data, "url", data["url"]["href"])
else
data
end
end end
def maybe_fix_user_object(data) do def maybe_fix_user_url(data), do: data
data
|> maybe_fix_user_url def maybe_fix_user_object(data), do: maybe_fix_user_url(data)
end
end end

View File

@ -37,12 +37,12 @@ def render("object.json", %{object: %Activity{} = activity}) do
Map.merge(base, additional) Map.merge(base, additional)
end end
def render("likes.json", ap_id, likes, page) do def render("likes.json", %{ap_id: ap_id, likes: likes, page: page}) do
collection(likes, "#{ap_id}/likes", page) collection(likes, "#{ap_id}/likes", page)
|> Map.merge(Pleroma.Web.ActivityPub.Utils.make_json_ld_header()) |> Map.merge(Pleroma.Web.ActivityPub.Utils.make_json_ld_header())
end end
def render("likes.json", ap_id, likes) do def render("likes.json", %{ap_id: ap_id, likes: likes}) do
%{ %{
"id" => "#{ap_id}/likes", "id" => "#{ap_id}/likes",
"type" => "OrderedCollection", "type" => "OrderedCollection",

View File

@ -75,10 +75,7 @@ def render("user.json", %{user: user}) do
endpoints = render("endpoints.json", %{user: user}) endpoints = render("endpoints.json", %{user: user})
user_tags = emoji_tags = Transmogrifier.take_emoji_tags(user)
user
|> Transmogrifier.add_emoji_tags()
|> Map.get("tag", [])
fields = fields =
user.info user.info
@ -110,7 +107,7 @@ def render("user.json", %{user: user}) do
}, },
"endpoints" => endpoints, "endpoints" => endpoints,
"attachment" => fields, "attachment" => fields,
"tag" => (user.info.source_data["tag"] || []) ++ user_tags "tag" => (user.info.source_data["tag"] || []) ++ emoji_tags
} }
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user)) |> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
|> Map.merge(maybe_make_image(&User.banner_url/2, "image", user)) |> Map.merge(maybe_make_image(&User.banner_url/2, "image", user))

View File

@ -14,6 +14,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.Web.AdminAPI.Config alias Pleroma.Web.AdminAPI.Config
alias Pleroma.Web.AdminAPI.ConfigView alias Pleroma.Web.AdminAPI.ConfigView
alias Pleroma.Web.AdminAPI.ModerationLogView alias Pleroma.Web.AdminAPI.ModerationLogView
alias Pleroma.Web.AdminAPI.Report
alias Pleroma.Web.AdminAPI.ReportView alias Pleroma.Web.AdminAPI.ReportView
alias Pleroma.Web.AdminAPI.Search alias Pleroma.Web.AdminAPI.Search
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
@ -141,7 +142,8 @@ def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
def user_show(conn, %{"nickname" => nickname}) do def user_show(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
conn conn
|> json(AccountView.render("show.json", %{user: user})) |> put_view(AccountView)
|> render("show.json", %{user: user})
else else
_ -> {:error, :not_found} _ -> {:error, :not_found}
end end
@ -160,7 +162,8 @@ def list_user_statuses(conn, %{"nickname" => nickname} = params) do
}) })
conn conn
|> json(StatusView.render("index.json", %{activities: activities, as: :activity})) |> put_view(StatusView)
|> render("index.json", %{activities: activities, as: :activity})
else else
_ -> {:error, :not_found} _ -> {:error, :not_found}
end end
@ -180,7 +183,8 @@ def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => ni
}) })
conn conn
|> json(AccountView.render("show.json", %{user: updated_user})) |> put_view(AccountView)
|> render("show.json", %{user: updated_user})
end end
def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
@ -426,7 +430,8 @@ def invites(conn, _params) do
invites = UserInviteToken.list_invites() invites = UserInviteToken.list_invites()
conn conn
|> json(AccountView.render("invites.json", %{invites: invites})) |> put_view(AccountView)
|> render("invites.json", %{invites: invites})
end end
@doc "Revokes invite by token" @doc "Revokes invite by token"
@ -434,7 +439,8 @@ def revoke_invite(conn, %{"token" => token}) do
with {:ok, invite} <- UserInviteToken.find_by_token(token), with {:ok, invite} <- UserInviteToken.find_by_token(token),
{:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do {:ok, updated_invite} = UserInviteToken.update_invite(invite, %{used: true}) do
conn conn
|> json(AccountView.render("invite.json", %{invite: updated_invite})) |> put_view(AccountView)
|> render("invite.json", %{invite: updated_invite})
else else
nil -> {:error, :not_found} nil -> {:error, :not_found}
end end
@ -452,6 +458,15 @@ def get_password_reset(conn, %{"nickname" => nickname}) do
}) })
end end
@doc "Force password reset for a given user"
def force_password_reset(conn, %{"nickname" => nickname}) do
(%User{local: true} = user) = User.get_cached_by_nickname(nickname)
User.force_password_reset_async(user)
json_response(conn, :no_content, "")
end
def list_reports(conn, params) do def list_reports(conn, params) do
params = params =
params params
@ -470,7 +485,7 @@ def report_show(conn, %{"id" => id}) do
with %Activity{} = report <- Activity.get_by_id(id) do with %Activity{} = report <- Activity.get_by_id(id) do
conn conn
|> put_view(ReportView) |> put_view(ReportView)
|> render("show.json", %{report: report}) |> render("show.json", Report.extract_report_info(report))
else else
_ -> {:error, :not_found} _ -> {:error, :not_found}
end end
@ -486,7 +501,7 @@ def report_update_state(%{assigns: %{user: admin}} = conn, %{"id" => id, "state"
conn conn
|> put_view(ReportView) |> put_view(ReportView)
|> render("show.json", %{report: report}) |> render("show.json", Report.extract_report_info(report))
end end
end end
@ -604,6 +619,12 @@ def config_update(conn, %{"configs" => configs}) do
|> render("index.json", %{configs: updated}) |> render("index.json", %{configs: updated})
end end
def reload_emoji(conn, _params) do
Pleroma.Emoji.reload()
conn |> json("ok")
end
def errors(conn, {:error, :not_found}) do def errors(conn, {:error, :not_found}) do
conn conn
|> put_status(:not_found) |> put_status(:not_found)

View File

@ -0,0 +1,22 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.Report do
alias Pleroma.Activity
alias Pleroma.User
def extract_report_info(
%{data: %{"actor" => actor, "object" => [account_ap_id | status_ap_ids]}} = report
) do
user = User.get_cached_by_ap_id(actor)
account = User.get_cached_by_ap_id(account_ap_id)
statuses =
Enum.map(status_ap_ids, fn ap_id ->
Activity.get_by_ap_id_with_object(ap_id)
end)
%{report: report, user: user, account: account, statuses: statuses}
end
end

View File

@ -4,27 +4,26 @@
defmodule Pleroma.Web.AdminAPI.ReportView do defmodule Pleroma.Web.AdminAPI.ReportView do
use Pleroma.Web, :view use Pleroma.Web, :view
alias Pleroma.Activity
alias Pleroma.HTML alias Pleroma.HTML
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.AdminAPI.Report
alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
def render("index.json", %{reports: reports}) do def render("index.json", %{reports: reports}) do
%{ %{
reports: reports:
render_many(reports[:items], __MODULE__, "show.json", as: :report) |> Enum.reverse(), reports[:items]
|> Enum.map(&Report.extract_report_info(&1))
|> Enum.map(&render(__MODULE__, "show.json", &1))
|> Enum.reverse(),
total: reports[:total] total: reports[:total]
} }
end end
def render("show.json", %{report: report}) do def render("show.json", %{report: report, user: user, account: account, statuses: statuses}) do
user = User.get_cached_by_ap_id(report.data["actor"])
created_at = Utils.to_masto_date(report.data["published"]) created_at = Utils.to_masto_date(report.data["published"])
[account_ap_id | status_ap_ids] = report.data["object"]
account = User.get_cached_by_ap_id(account_ap_id)
content = content =
unless is_nil(report.data["content"]) do unless is_nil(report.data["content"]) do
HTML.filter_tags(report.data["content"]) HTML.filter_tags(report.data["content"])
@ -32,11 +31,6 @@ def render("show.json", %{report: report}) do
nil nil
end end
statuses =
Enum.map(status_ap_ids, fn ap_id ->
Activity.get_by_ap_id_with_object(ap_id)
end)
%{ %{
id: report.id, id: report.id,
account: merge_account_views(account), account: merge_account_views(account),

View File

@ -381,7 +381,6 @@ def public_timeline(%{assigns: %{user: user}} = conn, params) do
|> Map.put("local_only", local_only) |> Map.put("local_only", local_only)
|> Map.put("blocking_user", user) |> Map.put("blocking_user", user)
|> Map.put("muting_user", user) |> Map.put("muting_user", user)
|> Map.put("user", user)
|> ActivityPub.fetch_public_activities() |> ActivityPub.fetch_public_activities()
|> Enum.reverse() |> Enum.reverse()

View File

@ -19,9 +19,10 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
accounts = User.search(query, search_options(params, user)) accounts = User.search(query, search_options(params, user))
res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
json(conn, res) conn
|> put_view(AccountView)
|> render("accounts.json", users: accounts, for: user, as: :user)
end end
def search2(conn, params), do: do_search(:v2, conn, params) def search2(conn, params), do: do_search(:v2, conn, params)

View File

@ -57,6 +57,7 @@ def raw_nodeinfo do
"mastodon_api_streaming", "mastodon_api_streaming",
"polls", "polls",
"pleroma_explicit_addressing", "pleroma_explicit_addressing",
"shareable_emoji_packs",
if Config.get([:media_proxy, :enabled]) do if Config.get([:media_proxy, :enabled]) do
"media_proxy" "media_proxy"
end, end,

View File

@ -202,6 +202,8 @@ def token_exchange(
{:ok, app} <- Token.Utils.fetch_app(conn), {:ok, app} <- Token.Utils.fetch_app(conn),
{:auth_active, true} <- {:auth_active, User.auth_active?(user)}, {:auth_active, true} <- {:auth_active, User.auth_active?(user)},
{:user_active, true} <- {:user_active, !user.info.deactivated}, {:user_active, true} <- {:user_active, !user.info.deactivated},
{:password_reset_pending, false} <-
{:password_reset_pending, user.info.password_reset_pending},
{:ok, scopes} <- validate_scopes(app, params), {:ok, scopes} <- validate_scopes(app, params),
{:ok, auth} <- Authorization.create_authorization(app, user, scopes), {:ok, auth} <- Authorization.create_authorization(app, user, scopes),
{:ok, token} <- Token.exchange_token(app, auth) do {:ok, token} <- Token.exchange_token(app, auth) do
@ -215,6 +217,9 @@ def token_exchange(
{:user_active, false} -> {:user_active, false} ->
render_error(conn, :forbidden, "Your account is currently disabled") render_error(conn, :forbidden, "Your account is currently disabled")
{:password_reset_pending, true} ->
render_error(conn, :forbidden, "Password reset is required")
_error -> _error ->
render_invalid_credentials_error(conn) render_invalid_credentials_error(conn)
end end

View File

@ -216,7 +216,8 @@ defp represent_activity(
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_header("content-type", "application/activity+json")
|> json(ObjectView.render("object.json", %{object: object})) |> put_view(ObjectView)
|> render("object.json", %{object: object})
end end
defp represent_activity(_conn, "activity+json", _, _) do defp represent_activity(_conn, "activity+json", _, _) do

View File

@ -0,0 +1,575 @@
defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
use Pleroma.Web, :controller
require Logger
@emoji_dir_path Path.join(
Pleroma.Config.get!([:instance, :static_dir]),
"emoji"
)
@cache_seconds_per_file Pleroma.Config.get!([:emoji, :shared_pack_cache_seconds_per_file])
@doc """
Lists the packs available on the instance as JSON.
The information is public and does not require authentification. The format is
a map of "pack directory name" to pack.json contents.
"""
def list_packs(conn, _params) do
with {:ok, results} <- File.ls(@emoji_dir_path) do
pack_infos =
results
|> Enum.filter(&has_pack_json?/1)
|> Enum.map(&load_pack/1)
# Check if all the files are in place and can be sent
|> Enum.map(&validate_pack/1)
# Transform into a map of pack-name => pack-data
|> Enum.into(%{})
json(conn, pack_infos)
end
end
defp has_pack_json?(file) do
dir_path = Path.join(@emoji_dir_path, file)
# Filter to only use the pack.json packs
File.dir?(dir_path) and File.exists?(Path.join(dir_path, "pack.json"))
end
defp load_pack(pack_name) do
pack_path = Path.join(@emoji_dir_path, pack_name)
pack_file = Path.join(pack_path, "pack.json")
{pack_name, Jason.decode!(File.read!(pack_file))}
end
defp validate_pack({name, pack}) do
pack_path = Path.join(@emoji_dir_path, name)
if can_download?(pack, pack_path) do
archive_for_sha = make_archive(name, pack, pack_path)
archive_sha = :crypto.hash(:sha256, archive_for_sha) |> Base.encode16()
pack =
pack
|> put_in(["pack", "can-download"], true)
|> put_in(["pack", "download-sha256"], archive_sha)
{name, pack}
else
{name, put_in(pack, ["pack", "can-download"], false)}
end
end
defp can_download?(pack, pack_path) do
# If the pack is set as shared, check if it can be downloaded
# That means that when asked, the pack can be packed and sent to the remote
# Otherwise, they'd have to download it from external-src
pack["pack"]["share-files"] &&
Enum.all?(pack["files"], fn {_, path} ->
File.exists?(Path.join(pack_path, path))
end)
end
defp create_archive_and_cache(name, pack, pack_dir, md5) do
files =
['pack.json'] ++
(pack["files"] |> Enum.map(fn {_, path} -> to_charlist(path) end))
{:ok, {_, zip_result}} = :zip.zip('#{name}.zip', files, [:memory, cwd: to_charlist(pack_dir)])
cache_ms = :timer.seconds(@cache_seconds_per_file * Enum.count(files))
Cachex.put!(
:emoji_packs_cache,
name,
# if pack.json MD5 changes, the cache is not valid anymore
%{pack_json_md5: md5, pack_data: zip_result},
# Add a minute to cache time for every file in the pack
ttl: cache_ms
)
Logger.debug("Created an archive for the '#{name}' emoji pack, \
keeping it in cache for #{div(cache_ms, 1000)}s")
zip_result
end
defp make_archive(name, pack, pack_dir) do
# Having a different pack.json md5 invalidates cache
pack_file_md5 = :crypto.hash(:md5, File.read!(Path.join(pack_dir, "pack.json")))
case Cachex.get!(:emoji_packs_cache, name) do
%{pack_file_md5: ^pack_file_md5, pack_data: zip_result} ->
Logger.debug("Using cache for the '#{name}' shared emoji pack")
zip_result
_ ->
create_archive_and_cache(name, pack, pack_dir, pack_file_md5)
end
end
@doc """
An endpoint for other instances (via admin UI) or users (via browser)
to download packs that the instance shares.
"""
def download_shared(conn, %{"name" => name}) do
pack_dir = Path.join(@emoji_dir_path, name)
pack_file = Path.join(pack_dir, "pack.json")
with {_, true} <- {:exists?, File.exists?(pack_file)},
pack = Jason.decode!(File.read!(pack_file)),
{_, true} <- {:can_download?, can_download?(pack, pack_dir)} do
zip_result = make_archive(name, pack, pack_dir)
send_download(conn, {:binary, zip_result}, filename: "#{name}.zip")
else
{:can_download?, _} ->
conn
|> put_status(:forbidden)
|> json(%{
error: "Pack #{name} cannot be downloaded from this instance, either pack sharing\
was disabled for this pack or some files are missing"
})
{:exists?, _} ->
conn
|> put_status(:not_found)
|> json(%{error: "Pack #{name} does not exist"})
end
end
@doc """
An admin endpoint to request downloading a pack named `pack_name` from the instance
`instance_address`.
If the requested instance's admin chose to share the pack, it will be downloaded
from that instance, otherwise it will be downloaded from the fallback source, if there is one.
"""
def download_from(conn, %{"instance_address" => address, "pack_name" => name} = data) do
shareable_packs_available =
"#{address}/.well-known/nodeinfo"
|> Tesla.get!()
|> Map.get(:body)
|> Jason.decode!()
|> List.last()
|> Map.get("href")
# Get the actual nodeinfo address and fetch it
|> Tesla.get!()
|> Map.get(:body)
|> Jason.decode!()
|> get_in(["metadata", "features"])
|> Enum.member?("shareable_emoji_packs")
if shareable_packs_available do
full_pack =
"#{address}/api/pleroma/emoji/packs/list"
|> Tesla.get!()
|> Map.get(:body)
|> Jason.decode!()
|> Map.get(name)
pack_info_res =
case full_pack["pack"] do
%{"share-files" => true, "can-download" => true, "download-sha256" => sha} ->
{:ok,
%{
sha: sha,
uri: "#{address}/api/pleroma/emoji/packs/download_shared/#{name}"
}}
%{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) ->
{:ok,
%{
sha: sha,
uri: src,
fallback: true
}}
_ ->
{:error,
"The pack was not set as shared and there is no fallback src to download from"}
end
with {:ok, %{sha: sha, uri: uri} = pinfo} <- pack_info_res,
%{body: emoji_archive} <- Tesla.get!(uri),
{_, true} <- {:checksum, Base.decode16!(sha) == :crypto.hash(:sha256, emoji_archive)} do
local_name = data["as"] || name
pack_dir = Path.join(@emoji_dir_path, local_name)
File.mkdir_p!(pack_dir)
files = Enum.map(full_pack["files"], fn {_, path} -> to_charlist(path) end)
# Fallback cannot contain a pack.json file
files = if pinfo[:fallback], do: files, else: ['pack.json'] ++ files
{:ok, _} = :zip.unzip(emoji_archive, cwd: to_charlist(pack_dir), file_list: files)
# Fallback can't contain a pack.json file, since that would cause the fallback-src-sha256
# in it to depend on itself
if pinfo[:fallback] do
pack_file_path = Path.join(pack_dir, "pack.json")
File.write!(pack_file_path, Jason.encode!(full_pack, pretty: true))
end
json(conn, "ok")
else
{:error, e} ->
conn |> put_status(:internal_server_error) |> json(%{error: e})
{:checksum, _} ->
conn
|> put_status(:internal_server_error)
|> json(%{error: "SHA256 for the pack doesn't match the one sent by the server"})
end
else
conn
|> put_status(:internal_server_error)
|> json(%{error: "The requested instance does not support sharing emoji packs"})
end
end
@doc """
Creates an empty pack named `name` which then can be updated via the admin UI.
"""
def create(conn, %{"name" => name}) do
pack_dir = Path.join(@emoji_dir_path, name)
if not File.exists?(pack_dir) do
File.mkdir_p!(pack_dir)
pack_file_p = Path.join(pack_dir, "pack.json")
File.write!(
pack_file_p,
Jason.encode!(%{pack: %{}, files: %{}}, pretty: true)
)
conn |> json("ok")
else
conn
|> put_status(:conflict)
|> json(%{error: "A pack named \"#{name}\" already exists"})
end
end
@doc """
Deletes the pack `name` and all it's files.
"""
def delete(conn, %{"name" => name}) do
pack_dir = Path.join(@emoji_dir_path, name)
case File.rm_rf(pack_dir) do
{:ok, _} ->
conn |> json("ok")
{:error, _} ->
conn
|> put_status(:internal_server_error)
|> json(%{error: "Couldn't delete the pack #{name}"})
end
end
@doc """
An endpoint to update `pack_names`'s metadata.
`new_data` is the new metadata for the pack, that will replace the old metadata.
"""
def update_metadata(conn, %{"pack_name" => name, "new_data" => new_data}) do
pack_file_p = Path.join([@emoji_dir_path, name, "pack.json"])
full_pack = Jason.decode!(File.read!(pack_file_p))
# The new fallback-src is in the new data and it's not the same as it was in the old data
should_update_fb_sha =
not is_nil(new_data["fallback-src"]) and
new_data["fallback-src"] != full_pack["pack"]["fallback-src"]
with {_, true} <- {:should_update?, should_update_fb_sha},
%{body: pack_arch} <- Tesla.get!(new_data["fallback-src"]),
{:ok, flist} <- :zip.unzip(pack_arch, [:memory]),
{_, true} <- {:has_all_files?, has_all_files?(full_pack, flist)} do
fallback_sha = :crypto.hash(:sha256, pack_arch) |> Base.encode16()
new_data = Map.put(new_data, "fallback-src-sha256", fallback_sha)
update_metadata_and_send(conn, full_pack, new_data, pack_file_p)
else
{:should_update?, _} ->
update_metadata_and_send(conn, full_pack, new_data, pack_file_p)
{:has_all_files?, _} ->
conn
|> put_status(:bad_request)
|> json(%{error: "The fallback archive does not have all files specified in pack.json"})
end
end
# Check if all files from the pack.json are in the archive
defp has_all_files?(%{"files" => files}, flist) do
Enum.all?(files, fn {_, from_manifest} ->
Enum.find(flist, fn {from_archive, _} ->
to_string(from_archive) == from_manifest
end)
end)
end
defp update_metadata_and_send(conn, full_pack, new_data, pack_file_p) do
full_pack = Map.put(full_pack, "pack", new_data)
File.write!(pack_file_p, Jason.encode!(full_pack, pretty: true))
# Send new data back with fallback sha filled
json(conn, new_data)
end
defp get_filename(%{"filename" => filename}), do: filename
defp get_filename(%{"file" => file}) do
case file do
%Plug.Upload{filename: filename} -> filename
url when is_binary(url) -> Path.basename(url)
end
end
defp empty?(str), do: String.trim(str) == ""
defp update_file_and_send(conn, updated_full_pack, pack_file_p) do
# Write the emoji pack file
File.write!(pack_file_p, Jason.encode!(updated_full_pack, pretty: true))
# Return the modified file list
json(conn, updated_full_pack["files"])
end
@doc """
Updates a file in a pack.
Updating can mean three things:
- `add` adds an emoji named `shortcode` to the pack `pack_name`,
that means that the emoji file needs to be uploaded with the request
(thus requiring it to be a multipart request) and be named `file`.
There can also be an optional `filename` that will be the new emoji file name
(if it's not there, the name will be taken from the uploaded file).
- `update` changes emoji shortcode (from `shortcode` to `new_shortcode` or moves the file
(from the current filename to `new_filename`)
- `remove` removes the emoji named `shortcode` and it's associated file
"""
# Add
def update_file(
conn,
%{"pack_name" => pack_name, "action" => "add", "shortcode" => shortcode} = params
) do
pack_dir = Path.join(@emoji_dir_path, pack_name)
pack_file_p = Path.join(pack_dir, "pack.json")
full_pack = Jason.decode!(File.read!(pack_file_p))
with {_, false} <- {:has_shortcode, Map.has_key?(full_pack["files"], shortcode)},
filename <- get_filename(params),
false <- empty?(shortcode),
false <- empty?(filename) do
file_path = Path.join(pack_dir, filename)
# If the name contains directories, create them
if String.contains?(file_path, "/") do
File.mkdir_p!(Path.dirname(file_path))
end
case params["file"] do
%Plug.Upload{path: upload_path} ->
# Copy the uploaded file from the temporary directory
File.copy!(upload_path, file_path)
url when is_binary(url) ->
# Download and write the file
file_contents = Tesla.get!(url).body
File.write!(file_path, file_contents)
end
updated_full_pack = put_in(full_pack, ["files", shortcode], filename)
update_file_and_send(conn, updated_full_pack, pack_file_p)
else
{:has_shortcode, _} ->
conn
|> put_status(:conflict)
|> json(%{error: "An emoji with the \"#{shortcode}\" shortcode already exists"})
true ->
conn
|> put_status(:bad_request)
|> json(%{error: "shortcode or filename cannot be empty"})
end
end
# Remove
def update_file(conn, %{
"pack_name" => pack_name,
"action" => "remove",
"shortcode" => shortcode
}) do
pack_dir = Path.join(@emoji_dir_path, pack_name)
pack_file_p = Path.join(pack_dir, "pack.json")
full_pack = Jason.decode!(File.read!(pack_file_p))
if Map.has_key?(full_pack["files"], shortcode) do
{emoji_file_path, updated_full_pack} = pop_in(full_pack, ["files", shortcode])
emoji_file_path = Path.join(pack_dir, emoji_file_path)
# Delete the emoji file
File.rm!(emoji_file_path)
# If the old directory has no more files, remove it
if String.contains?(emoji_file_path, "/") do
dir = Path.dirname(emoji_file_path)
if Enum.empty?(File.ls!(dir)) do
File.rmdir!(dir)
end
end
update_file_and_send(conn, updated_full_pack, pack_file_p)
else
conn
|> put_status(:bad_request)
|> json(%{error: "Emoji \"#{shortcode}\" does not exist"})
end
end
# Update
def update_file(
conn,
%{"pack_name" => pack_name, "action" => "update", "shortcode" => shortcode} = params
) do
pack_dir = Path.join(@emoji_dir_path, pack_name)
pack_file_p = Path.join(pack_dir, "pack.json")
full_pack = Jason.decode!(File.read!(pack_file_p))
with {_, true} <- {:has_shortcode, Map.has_key?(full_pack["files"], shortcode)},
%{"new_shortcode" => new_shortcode, "new_filename" => new_filename} <- params,
false <- empty?(new_shortcode),
false <- empty?(new_filename) do
# First, remove the old shortcode, saving the old path
{old_emoji_file_path, updated_full_pack} = pop_in(full_pack, ["files", shortcode])
old_emoji_file_path = Path.join(pack_dir, old_emoji_file_path)
new_emoji_file_path = Path.join(pack_dir, new_filename)
# If the name contains directories, create them
if String.contains?(new_emoji_file_path, "/") do
File.mkdir_p!(Path.dirname(new_emoji_file_path))
end
# Move/Rename the old filename to a new filename
# These are probably on the same filesystem, so just rename should work
:ok = File.rename(old_emoji_file_path, new_emoji_file_path)
# If the old directory has no more files, remove it
if String.contains?(old_emoji_file_path, "/") do
dir = Path.dirname(old_emoji_file_path)
if Enum.empty?(File.ls!(dir)) do
File.rmdir!(dir)
end
end
# Then, put in the new shortcode with the new path
updated_full_pack = put_in(updated_full_pack, ["files", new_shortcode], new_filename)
update_file_and_send(conn, updated_full_pack, pack_file_p)
else
{:has_shortcode, _} ->
conn
|> put_status(:bad_request)
|> json(%{error: "Emoji \"#{shortcode}\" does not exist"})
true ->
conn
|> put_status(:bad_request)
|> json(%{error: "new_shortcode or new_filename cannot be empty"})
_ ->
conn
|> put_status(:bad_request)
|> json(%{error: "new_shortcode or new_file were not specified"})
end
end
def update_file(conn, %{"action" => action}) do
conn
|> put_status(:bad_request)
|> json(%{error: "Unknown action: #{action}"})
end
@doc """
Imports emoji from the filesystem.
Importing means checking all the directories in the
`$instance_static/emoji/` for directories which do not have
`pack.json`. If one has an emoji.txt file, that file will be used
to create a `pack.json` file with it's contents. If the directory has
neither, all the files with specific configured extenstions will be
assumed to be emojis and stored in the new `pack.json` file.
"""
def import_from_fs(conn, _params) do
with {:ok, results} <- File.ls(@emoji_dir_path) do
imported_pack_names =
results
|> Enum.filter(fn file ->
dir_path = Path.join(@emoji_dir_path, file)
# Find the directories that do NOT have pack.json
File.dir?(dir_path) and not File.exists?(Path.join(dir_path, "pack.json"))
end)
|> Enum.map(&write_pack_json_contents/1)
json(conn, imported_pack_names)
else
{:error, _} ->
conn
|> put_status(:internal_server_error)
|> json(%{error: "Error accessing emoji pack directory"})
end
end
defp write_pack_json_contents(dir) do
dir_path = Path.join(@emoji_dir_path, dir)
emoji_txt_path = Path.join(dir_path, "emoji.txt")
files_for_pack = files_for_pack(emoji_txt_path, dir_path)
pack_json_contents = Jason.encode!(%{pack: %{}, files: files_for_pack})
File.write!(Path.join(dir_path, "pack.json"), pack_json_contents)
dir
end
defp files_for_pack(emoji_txt_path, dir_path) do
if File.exists?(emoji_txt_path) do
# There's an emoji.txt file, it's likely from a pack installed by the pack manager.
# Make a pack.json file from the contents of that emoji.txt fileh
# FIXME: Copy-pasted from Pleroma.Emoji/load_from_file_stream/2
# Create a map of shortcodes to filenames from emoji.txt
File.read!(emoji_txt_path)
|> String.split("\n")
|> Enum.map(&String.trim/1)
|> Enum.map(fn line ->
case String.split(line, ~r/,\s*/) do
# This matches both strings with and without tags
# and we don't care about tags here
[name, file | _] -> {name, file}
_ -> nil
end
end)
|> Enum.filter(fn x -> not is_nil(x) end)
|> Enum.into(%{})
else
# If there's no emoji.txt, assume all files
# that are of certain extensions from the config are emojis and import them all
pack_extensions = Pleroma.Config.get!([:emoji, :pack_extensions])
Pleroma.Emoji.make_shortcode_to_file_map(dir_path, pack_extensions)
end
end
end

View File

@ -186,6 +186,7 @@ defmodule Pleroma.Web.Router do
post("/users/email_invite", AdminAPIController, :email_invite) post("/users/email_invite", AdminAPIController, :email_invite)
get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset) get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
patch("/users/:nickname/force_password_reset", AdminAPIController, :force_password_reset)
get("/users", AdminAPIController, :list_users) get("/users", AdminAPIController, :list_users)
get("/users/:nickname", AdminAPIController, :user_show) get("/users/:nickname", AdminAPIController, :user_show)
@ -205,6 +206,29 @@ defmodule Pleroma.Web.Router do
get("/config/migrate_from_db", AdminAPIController, :migrate_from_db) get("/config/migrate_from_db", AdminAPIController, :migrate_from_db)
get("/moderation_log", AdminAPIController, :list_log) get("/moderation_log", AdminAPIController, :list_log)
post("/reload_emoji", AdminAPIController, :reload_emoji)
end
scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do
scope "/packs" do
# Modifying packs
pipe_through([:admin_api, :oauth_write])
post("/import_from_fs", EmojiAPIController, :import_from_fs)
post("/:pack_name/update_file", EmojiAPIController, :update_file)
post("/:pack_name/update_metadata", EmojiAPIController, :update_metadata)
put("/:name", EmojiAPIController, :create)
delete("/:name", EmojiAPIController, :delete)
post("/download_from", EmojiAPIController, :download_from)
end
scope "/packs" do
# Pack info / downloading
get("/", EmojiAPIController, :list_packs)
get("/:name/download_shared/", EmojiAPIController, :download_shared)
end
end end
scope "/", Pleroma.Web.TwitterAPI do scope "/", Pleroma.Web.TwitterAPI do

View File

@ -26,6 +26,11 @@ def perform(%{"op" => "delete_user", "user_id" => user_id}, _job) do
User.perform(:delete, user) User.perform(:delete, user)
end end
def perform(%{"op" => "force_password_reset", "user_id" => user_id}, _job) do
user = User.get_cached_by_id(user_id)
User.perform(:force_password_reset, user)
end
def perform( def perform(
%{ %{
"op" => "blocks_import", "op" => "blocks_import",

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1 +1 @@
<!DOCTYPE html><html><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge,chrome=1"><meta name=renderer content=webkit><meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"><title>Admin FE</title><link rel="shortcut icon" href=favicon.ico><link href=chunk-elementUI.e5cd8da6.css rel=stylesheet><link href=chunk-libs.4e8c4664.css rel=stylesheet><link href=app.34fc670f.css rel=stylesheet></head><body><script src=/pleroma/admin/static/tinymce4.7.5/tinymce.min.js></script><div id=app></div><script type=text/javascript src=static/js/runtime.f40c8ec4.js></script><script type=text/javascript src=static/js/chunk-elementUI.1911151b.js></script><script type=text/javascript src=static/js/chunk-libs.fb0b7f4a.js></script><script type=text/javascript src=static/js/app.8e186193.js></script></body></html> <!DOCTYPE html><html><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge,chrome=1"><meta name=renderer content=webkit><meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"><title>Admin FE</title><link rel="shortcut icon" href=favicon.ico><link href=chunk-elementUI.f35d8ab1.css rel=stylesheet><link href=chunk-libs.00388c73.css rel=stylesheet><link href=app.40438ff5.css rel=stylesheet></head><body><script src=/pleroma/admin/static/tinymce4.7.5/tinymce.min.js></script><div id=app></div><script type=text/javascript src=static/js/runtime.e85850af.js></script><script type=text/javascript src=static/js/chunk-elementUI.708d6b68.js></script><script type=text/javascript src=static/js/chunk-libs.14514767.js></script><script type=text/javascript src=static/js/app.90c455c5.js></script></body></html>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -19,7 +19,7 @@ test "turns hashtags into links" do
text = "I love #cofe and #2hu" text = "I love #cofe and #2hu"
expected_text = expected_text =
"I love <a class='hashtag' data-tag='cofe' href='http://localhost:4001/tag/cofe' rel='tag'>#cofe</a> and <a class='hashtag' data-tag='2hu' href='http://localhost:4001/tag/2hu' rel='tag'>#2hu</a>" ~s(I love <a class="hashtag" data-tag="cofe" href="http://localhost:4001/tag/cofe" rel="tag ugc">#cofe</a> and <a class="hashtag" data-tag="2hu" href="http://localhost:4001/tag/2hu" rel="tag ugc">#2hu</a>)
assert {^expected_text, [], _tags} = Formatter.linkify(text) assert {^expected_text, [], _tags} = Formatter.linkify(text)
end end
@ -28,7 +28,7 @@ test "does not turn html characters to tags" do
text = "#fact_3: pleroma does what mastodon't" text = "#fact_3: pleroma does what mastodon't"
expected_text = expected_text =
"<a class='hashtag' data-tag='fact_3' href='http://localhost:4001/tag/fact_3' rel='tag'>#fact_3</a>: pleroma does what mastodon't" ~s(<a class="hashtag" data-tag="fact_3" href="http://localhost:4001/tag/fact_3" rel="tag ugc">#fact_3</a>: pleroma does what mastodon't)
assert {^expected_text, [], _tags} = Formatter.linkify(text) assert {^expected_text, [], _tags} = Formatter.linkify(text)
end end
@ -39,21 +39,21 @@ test "turning urls into links" do
text = "Hey, check out https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla ." text = "Hey, check out https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla ."
expected = expected =
"Hey, check out <a href=\"https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla\">https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla</a> ." ~S(Hey, check out <a href="https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla" rel="ugc">https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla</a> .)
assert {^expected, [], []} = Formatter.linkify(text) assert {^expected, [], []} = Formatter.linkify(text)
text = "https://mastodon.social/@lambadalambda" text = "https://mastodon.social/@lambadalambda"
expected = expected =
"<a href=\"https://mastodon.social/@lambadalambda\">https://mastodon.social/@lambadalambda</a>" ~S(<a href="https://mastodon.social/@lambadalambda" rel="ugc">https://mastodon.social/@lambadalambda</a>)
assert {^expected, [], []} = Formatter.linkify(text) assert {^expected, [], []} = Formatter.linkify(text)
text = "https://mastodon.social:4000/@lambadalambda" text = "https://mastodon.social:4000/@lambadalambda"
expected = expected =
"<a href=\"https://mastodon.social:4000/@lambadalambda\">https://mastodon.social:4000/@lambadalambda</a>" ~S(<a href="https://mastodon.social:4000/@lambadalambda" rel="ugc">https://mastodon.social:4000/@lambadalambda</a>)
assert {^expected, [], []} = Formatter.linkify(text) assert {^expected, [], []} = Formatter.linkify(text)
@ -63,55 +63,57 @@ test "turning urls into links" do
assert {^expected, [], []} = Formatter.linkify(text) assert {^expected, [], []} = Formatter.linkify(text)
text = "http://www.cs.vu.nl/~ast/intel/" text = "http://www.cs.vu.nl/~ast/intel/"
expected = "<a href=\"http://www.cs.vu.nl/~ast/intel/\">http://www.cs.vu.nl/~ast/intel/</a>"
expected =
~S(<a href="http://www.cs.vu.nl/~ast/intel/" rel="ugc">http://www.cs.vu.nl/~ast/intel/</a>)
assert {^expected, [], []} = Formatter.linkify(text) assert {^expected, [], []} = Formatter.linkify(text)
text = "https://forum.zdoom.org/viewtopic.php?f=44&t=57087" text = "https://forum.zdoom.org/viewtopic.php?f=44&t=57087"
expected = expected =
"<a href=\"https://forum.zdoom.org/viewtopic.php?f=44&t=57087\">https://forum.zdoom.org/viewtopic.php?f=44&t=57087</a>" "<a href=\"https://forum.zdoom.org/viewtopic.php?f=44&t=57087\" rel=\"ugc\">https://forum.zdoom.org/viewtopic.php?f=44&t=57087</a>"
assert {^expected, [], []} = Formatter.linkify(text) assert {^expected, [], []} = Formatter.linkify(text)
text = "https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul" text = "https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul"
expected = expected =
"<a href=\"https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul\">https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul</a>" "<a href=\"https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul\" rel=\"ugc\">https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul</a>"
assert {^expected, [], []} = Formatter.linkify(text) assert {^expected, [], []} = Formatter.linkify(text)
text = "https://www.google.co.jp/search?q=Nasim+Aghdam" text = "https://www.google.co.jp/search?q=Nasim+Aghdam"
expected = expected =
"<a href=\"https://www.google.co.jp/search?q=Nasim+Aghdam\">https://www.google.co.jp/search?q=Nasim+Aghdam</a>" "<a href=\"https://www.google.co.jp/search?q=Nasim+Aghdam\" rel=\"ugc\">https://www.google.co.jp/search?q=Nasim+Aghdam</a>"
assert {^expected, [], []} = Formatter.linkify(text) assert {^expected, [], []} = Formatter.linkify(text)
text = "https://en.wikipedia.org/wiki/Duff's_device" text = "https://en.wikipedia.org/wiki/Duff's_device"
expected = expected =
"<a href=\"https://en.wikipedia.org/wiki/Duff's_device\">https://en.wikipedia.org/wiki/Duff's_device</a>" "<a href=\"https://en.wikipedia.org/wiki/Duff's_device\" rel=\"ugc\">https://en.wikipedia.org/wiki/Duff's_device</a>"
assert {^expected, [], []} = Formatter.linkify(text) assert {^expected, [], []} = Formatter.linkify(text)
text = "https://pleroma.com https://pleroma.com/sucks" text = "https://pleroma.com https://pleroma.com/sucks"
expected = expected =
"<a href=\"https://pleroma.com\">https://pleroma.com</a> <a href=\"https://pleroma.com/sucks\">https://pleroma.com/sucks</a>" "<a href=\"https://pleroma.com\" rel=\"ugc\">https://pleroma.com</a> <a href=\"https://pleroma.com/sucks\" rel=\"ugc\">https://pleroma.com/sucks</a>"
assert {^expected, [], []} = Formatter.linkify(text) assert {^expected, [], []} = Formatter.linkify(text)
text = "xmpp:contact@hacktivis.me" text = "xmpp:contact@hacktivis.me"
expected = "<a href=\"xmpp:contact@hacktivis.me\">xmpp:contact@hacktivis.me</a>" expected = "<a href=\"xmpp:contact@hacktivis.me\" rel=\"ugc\">xmpp:contact@hacktivis.me</a>"
assert {^expected, [], []} = Formatter.linkify(text) assert {^expected, [], []} = Formatter.linkify(text)
text = text =
"magnet:?xt=urn:btih:7ec9d298e91d6e4394d1379caf073c77ff3e3136&tr=udp%3A%2F%2Fopentor.org%3A2710&tr=udp%3A%2F%2Ftracker.blackunicorn.xyz%3A6969&tr=udp%3A%2F%2Ftracker.ccc.de%3A80&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com" "magnet:?xt=urn:btih:7ec9d298e91d6e4394d1379caf073c77ff3e3136&tr=udp%3A%2F%2Fopentor.org%3A2710&tr=udp%3A%2F%2Ftracker.blackunicorn.xyz%3A6969&tr=udp%3A%2F%2Ftracker.ccc.de%3A80&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com"
expected = "<a href=\"#{text}\">#{text}</a>" expected = "<a href=\"#{text}\" rel=\"ugc\">#{text}</a>"
assert {^expected, [], []} = Formatter.linkify(text) assert {^expected, [], []} = Formatter.linkify(text)
end end
@ -135,13 +137,13 @@ test "gives a replacement for user links, using local nicknames in user links te
assert length(mentions) == 3 assert length(mentions) == 3
expected_text = expected_text =
"<span class='h-card'><a data-user='#{gsimg.id}' class='u-url mention' href='#{ ~s(<span class="h-card"><a data-user="#{gsimg.id}" class="u-url mention" href="#{
gsimg.ap_id gsimg.ap_id
}'>@<span>gsimg</span></a></span> According to <span class='h-card'><a data-user='#{ }" rel="ugc">@<span>gsimg</span></a></span> According to <span class="h-card"><a data-user="#{
archaeme.id archaeme.id
}' class='u-url mention' href='#{"https://archeme/@archa_eme_"}'>@<span>archa_eme_</span></a></span>, that is @daggsy. Also hello <span class='h-card'><a data-user='#{ }" class="u-url mention" href="#{"https://archeme/@archa_eme_"}" rel="ugc">@<span>archa_eme_</span></a></span>, that is @daggsy. Also hello <span class="h-card"><a data-user="#{
archaeme_remote.id archaeme_remote.id
}' class='u-url mention' href='#{archaeme_remote.ap_id}'>@<span>archaeme</span></a></span>" }" class="u-url mention" href="#{archaeme_remote.ap_id}" rel="ugc">@<span>archaeme</span></a></span>)
assert expected_text == text assert expected_text == text
end end
@ -156,7 +158,9 @@ test "gives a replacement for user links when the user is using Osada" do
assert length(mentions) == 1 assert length(mentions) == 1
expected_text = expected_text =
"<span class='h-card'><a data-user='#{mike.id}' class='u-url mention' href='#{mike.ap_id}'>@<span>mike</span></a></span> test" ~s(<span class="h-card"><a data-user="#{mike.id}" class="u-url mention" href="#{
mike.ap_id
}" rel="ugc">@<span>mike</span></a></span> test)
assert expected_text == text assert expected_text == text
end end
@ -170,7 +174,7 @@ test "gives a replacement for single-character local nicknames" do
assert length(mentions) == 1 assert length(mentions) == 1
expected_text = expected_text =
"<span class='h-card'><a data-user='#{o.id}' class='u-url mention' href='#{o.ap_id}'>@<span>o</span></a></span> hi" ~s(<span class="h-card"><a data-user="#{o.id}" class="u-url mention" href="#{o.ap_id}" rel="ugc">@<span>o</span></a></span> hi)
assert expected_text == text assert expected_text == text
end end
@ -192,13 +196,17 @@ test "given the 'safe_mention' option, it will only mention people in the beginn
assert mentions == [{"@#{user.nickname}", user}, {"@#{other_user.nickname}", other_user}] assert mentions == [{"@#{user.nickname}", user}, {"@#{other_user.nickname}", other_user}]
assert expected_text == assert expected_text ==
"<span class='h-card'><a data-user='#{user.id}' class='u-url mention' href='#{ ~s(<span class="h-card"><a data-user="#{user.id}" class="u-url mention" href="#{
user.ap_id user.ap_id
}'>@<span>#{user.nickname}</span></a></span> <span class='h-card'><a data-user='#{ }" rel="ugc">@<span>#{user.nickname}</span></a></span> <span class="h-card"><a data-user="#{
other_user.id other_user.id
}' class='u-url mention' href='#{other_user.ap_id}'>@<span>#{other_user.nickname}</span></a></span> hey dudes i hate <span class='h-card'><a data-user='#{ }" class="u-url mention" href="#{other_user.ap_id}" rel="ugc">@<span>#{
other_user.nickname
}</span></a></span> hey dudes i hate <span class="h-card"><a data-user="#{
third_user.id third_user.id
}' class='u-url mention' href='#{third_user.ap_id}'>@<span>#{third_user.nickname}</span></a></span>" }" class="u-url mention" href="#{third_user.ap_id}" rel="ugc">@<span>#{
third_user.nickname
}</span></a></span>)
end end
test "given the 'safe_mention' option, it will still work without any mention" do test "given the 'safe_mention' option, it will still work without any mention" do

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 B

View File

@ -0,0 +1,13 @@
{
"pack": {
"license": "Test license",
"homepage": "https://pleroma.social",
"description": "Test description",
"share-files": true
},
"files": {
"blank": "blank.png"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 B

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