forked from AkkomaGang/akkoma
Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into remake-remodel-dms
This commit is contained in:
commit
814c3e5171
570 changed files with 3004 additions and 1956 deletions
|
@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- **Breaking:** removed `with_move` parameter from notifications timeline.
|
||||
|
||||
### Added
|
||||
- Instance: Add `background_image` to configuration and `/api/v1/instance`
|
||||
- Instance: Extend `/api/v1/instance` with Pleroma-specific information.
|
||||
- NodeInfo: `pleroma:api/v1/notifications:include_types_filter` to the `features` list.
|
||||
- NodeInfo: `pleroma_emoji_reactions` to the `features` list.
|
||||
|
|
|
@ -183,6 +183,7 @@
|
|||
email: "example@example.com",
|
||||
notify_email: "noreply@example.com",
|
||||
description: "A Pleroma instance, an alternative fediverse server",
|
||||
background_image: "/images/city.jpg",
|
||||
limit: 5_000,
|
||||
chat_limit: 5_000,
|
||||
remote_limit: 100_000,
|
||||
|
@ -271,20 +272,33 @@
|
|||
|
||||
config :pleroma, :frontend_configurations,
|
||||
pleroma_fe: %{
|
||||
theme: "pleroma-dark",
|
||||
logo: "/static/logo.png",
|
||||
background: "/images/city.jpg",
|
||||
redirectRootNoLogin: "/main/all",
|
||||
redirectRootLogin: "/main/friends",
|
||||
showInstanceSpecificPanel: true,
|
||||
scopeOptionsEnabled: false,
|
||||
formattingOptionsEnabled: false,
|
||||
alwaysShowSubjectInput: true,
|
||||
background: "/static/aurora_borealis.jpg",
|
||||
collapseMessageWithSubject: false,
|
||||
disableChat: false,
|
||||
greentext: false,
|
||||
hideFilteredStatuses: false,
|
||||
hideMutedPosts: false,
|
||||
hidePostStats: false,
|
||||
hideSitename: false,
|
||||
hideUserStats: false,
|
||||
loginMethod: "password",
|
||||
logo: "/static/logo.png",
|
||||
logoMargin: ".1em",
|
||||
logoMask: true,
|
||||
minimalScopesMode: false,
|
||||
noAttachmentLinks: false,
|
||||
nsfwCensorImage: "",
|
||||
postContentType: "text/plain",
|
||||
redirectRootLogin: "/main/friends",
|
||||
redirectRootNoLogin: "/main/all",
|
||||
scopeCopy: true,
|
||||
sidebarRight: false,
|
||||
showFeaturesPanel: true,
|
||||
showInstanceSpecificPanel: false,
|
||||
subjectLineBehavior: "email",
|
||||
alwaysShowSubjectInput: true
|
||||
theme: "pleroma-dark",
|
||||
webPushNotifications: false
|
||||
},
|
||||
masto_fe: %{
|
||||
showInstanceSpecificPanel: true
|
||||
|
@ -376,6 +390,10 @@
|
|||
|
||||
config :pleroma, :media_proxy,
|
||||
enabled: false,
|
||||
invalidation: [
|
||||
enabled: false,
|
||||
provider: Pleroma.Web.MediaProxy.Invalidation.Script
|
||||
],
|
||||
proxy_opts: [
|
||||
redirect_on_failure: false,
|
||||
max_body_length: 25 * 1_048_576,
|
||||
|
|
|
@ -1117,6 +1117,7 @@
|
|||
redirectRootLogin: "/main/friends",
|
||||
redirectRootNoLogin: "/main/all",
|
||||
scopeCopy: true,
|
||||
sidebarRight: false,
|
||||
showFeaturesPanel: true,
|
||||
showInstanceSpecificPanel: false,
|
||||
subjectLineBehavior: "email",
|
||||
|
@ -1256,6 +1257,12 @@
|
|||
type: :boolean,
|
||||
description: "Copy the scope (private/unlisted/public) in replies to posts by default"
|
||||
},
|
||||
%{
|
||||
key: :sidebarRight,
|
||||
label: "Sidebar on Right",
|
||||
type: :boolean,
|
||||
description: "Change alignment of sidebar and panels to the right."
|
||||
},
|
||||
%{
|
||||
key: :showFeaturesPanel,
|
||||
label: "Show instance features panel",
|
||||
|
|
|
@ -216,6 +216,7 @@ Has theses additional parameters (which are the same as in Pleroma-API):
|
|||
- `avatar_upload_limit`: The same for avatars
|
||||
- `background_upload_limit`: The same for backgrounds
|
||||
- `banner_upload_limit`: The same for banners
|
||||
- `background_image`: A background image that frontends can use
|
||||
- `pleroma.metadata.features`: A list of supported features
|
||||
- `pleroma.metadata.federation`: The federation restrictions of this instance
|
||||
- `vapid_public_key`: The public key needed for push messages
|
||||
|
|
|
@ -265,7 +265,7 @@ See [Admin-API](admin_api.md)
|
|||
* Method `PUT`
|
||||
* Authentication: required
|
||||
* Params:
|
||||
* `image`: Multipart image
|
||||
* `file`: Multipart image
|
||||
* Response: JSON. Returns a mastodon media attachment entity
|
||||
when successful, otherwise returns HTTP 415 `{"error": "error_msg"}`
|
||||
* Example response:
|
||||
|
@ -426,7 +426,7 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa
|
|||
* Authentication: required
|
||||
* Params:
|
||||
* `file`: file needs to be uploaded with the multipart request or link to remote file.
|
||||
* `shortcode`: (*optional*) shortcode for new emoji, must be uniq for all emoji. If not sended, shortcode will be taken from original filename.
|
||||
* `shortcode`: (*optional*) shortcode for new emoji, must be unique for all emoji. If not sended, shortcode will be taken from original filename.
|
||||
* `filename`: (*optional*) new emoji file name. If not specified will be taken from original filename.
|
||||
* Response: JSON, list of files for updated pack (hashmap -> shortcode => filename) with status 200, either error status with error message.
|
||||
|
||||
|
|
|
@ -249,6 +249,40 @@ This section describe PWA manifest instance-specific values. Currently this opti
|
|||
* `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts.
|
||||
* `proxy_opts`: All options defined in `Pleroma.ReverseProxy` documentation, defaults to `[max_body_length: (25*1_048_576)]`.
|
||||
* `whitelist`: List of domains to bypass the mediaproxy
|
||||
* `invalidation`: options for remove media from cache after delete object:
|
||||
* `enabled`: Enables purge cache
|
||||
* `provider`: Which one of the [purge cache strategy](#purge-cache-strategy) to use.
|
||||
|
||||
### Purge cache strategy
|
||||
|
||||
#### Pleroma.Web.MediaProxy.Invalidation.Script
|
||||
|
||||
This strategy allow perform external bash script to purge cache.
|
||||
Urls of attachments pass to script as arguments.
|
||||
|
||||
* `script_path`: path to external script.
|
||||
|
||||
Example:
|
||||
```elixir
|
||||
config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Script,
|
||||
script_path: "./installation/nginx-cache-purge.example"
|
||||
```
|
||||
|
||||
#### Pleroma.Web.MediaProxy.Invalidation.Http
|
||||
|
||||
This strategy allow perform custom http request to purge cache.
|
||||
|
||||
* `method`: http method. default is `purge`
|
||||
* `headers`: http headers. default is empty
|
||||
* `options`: request options. default is empty
|
||||
|
||||
Example:
|
||||
```elixir
|
||||
config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Http,
|
||||
method: :purge,
|
||||
headers: [],
|
||||
options: []
|
||||
```
|
||||
|
||||
## Link previews
|
||||
|
||||
|
@ -619,24 +653,6 @@ config :pleroma, :workers,
|
|||
* `enabled: false` corresponds to `config :pleroma, :workers, retries: [federator_outgoing: 1]`
|
||||
* deprecated options: `max_jobs`, `initial_timeout`
|
||||
|
||||
### Pleroma.Scheduler
|
||||
|
||||
Configuration for [Quantum](https://github.com/quantum-elixir/quantum-core) jobs scheduler.
|
||||
|
||||
See [Quantum readme](https://github.com/quantum-elixir/quantum-core#usage) for the list of supported options.
|
||||
|
||||
Example:
|
||||
|
||||
```elixir
|
||||
config :pleroma, Pleroma.Scheduler,
|
||||
global: true,
|
||||
overlap: true,
|
||||
timezone: :utc,
|
||||
jobs: [{"0 */6 * * * *", {Pleroma.Web.Websub, :refresh_subscriptions, []}}]
|
||||
```
|
||||
|
||||
The above example defines a single job which invokes `Pleroma.Web.Websub.refresh_subscriptions()` every 6 hours ("0 */6 * * * *", [crontab format](https://en.wikipedia.org/wiki/Cron)).
|
||||
|
||||
## :web_push_encryption, :vapid_details
|
||||
|
||||
Web Push Notifications configuration. You can use the mix task `mix web_push.gen.keypair` to generate it.
|
||||
|
|
40
installation/nginx-cache-purge.sh.example
Executable file
40
installation/nginx-cache-purge.sh.example
Executable file
|
@ -0,0 +1,40 @@
|
|||
#!/bin/sh
|
||||
|
||||
# A simple shell script to delete a media from the Nginx cache.
|
||||
|
||||
SCRIPTNAME=${0##*/}
|
||||
|
||||
# NGINX cache directory
|
||||
CACHE_DIRECTORY="/tmp/pleroma-media-cache"
|
||||
|
||||
## Return the files where the items are cached.
|
||||
## $1 - the filename, can be a pattern .
|
||||
## $2 - the cache directory.
|
||||
## $3 - (optional) the number of parallel processes to run for grep.
|
||||
get_cache_files() {
|
||||
local max_parallel=${3-16}
|
||||
find $2 -maxdepth 2 -type d | xargs -P $max_parallel -n 1 grep -E Rl "^KEY:.*$1" | sort -u
|
||||
}
|
||||
|
||||
## Removes an item from the given cache zone.
|
||||
## $1 - the filename, can be a pattern .
|
||||
## $2 - the cache directory.
|
||||
purge_item() {
|
||||
for f in $(get_cache_files $1 $2); do
|
||||
echo "found file: $f"
|
||||
[ -f $f ] || continue
|
||||
echo "Deleting $f from $2."
|
||||
rm $f
|
||||
done
|
||||
} # purge_item
|
||||
|
||||
purge() {
|
||||
for url in "$@"
|
||||
do
|
||||
echo "$SCRIPTNAME delete \`$url\` from cache ($CACHE_DIRECTORY)"
|
||||
purge_item $url $CACHE_DIRECTORY
|
||||
done
|
||||
|
||||
}
|
||||
|
||||
purge $1
|
|
@ -16,162 +16,78 @@ defmodule Pleroma.Emoji.Pack do
|
|||
|
||||
alias Pleroma.Emoji
|
||||
|
||||
@spec emoji_path() :: Path.t()
|
||||
def emoji_path do
|
||||
static = Pleroma.Config.get!([:instance, :static_dir])
|
||||
Path.join(static, "emoji")
|
||||
end
|
||||
|
||||
@spec create(String.t()) :: :ok | {:error, File.posix()} | {:error, :empty_values}
|
||||
def create(name) when byte_size(name) > 0 do
|
||||
dir = Path.join(emoji_path(), name)
|
||||
|
||||
with :ok <- File.mkdir(dir) do
|
||||
%__MODULE__{
|
||||
pack_file: Path.join(dir, "pack.json")
|
||||
}
|
||||
def create(name) do
|
||||
with :ok <- validate_not_empty([name]),
|
||||
dir <- Path.join(emoji_path(), name),
|
||||
:ok <- File.mkdir(dir) do
|
||||
%__MODULE__{pack_file: Path.join(dir, "pack.json")}
|
||||
|> save_pack()
|
||||
end
|
||||
end
|
||||
|
||||
def create(_), do: {:error, :empty_values}
|
||||
|
||||
@spec show(String.t()) :: {:ok, t()} | {:loaded, nil} | {:error, :empty_values}
|
||||
def show(name) when byte_size(name) > 0 do
|
||||
with {_, %__MODULE__{} = pack} <- {:loaded, load_pack(name)},
|
||||
{_, pack} <- validate_pack(pack) do
|
||||
{:ok, pack}
|
||||
@spec show(String.t()) :: {:ok, t()} | {:error, atom()}
|
||||
def show(name) do
|
||||
with :ok <- validate_not_empty([name]),
|
||||
{:ok, pack} <- load_pack(name) do
|
||||
{:ok, validate_pack(pack)}
|
||||
end
|
||||
end
|
||||
|
||||
def show(_), do: {:error, :empty_values}
|
||||
|
||||
@spec delete(String.t()) ::
|
||||
{:ok, [binary()]} | {:error, File.posix(), binary()} | {:error, :empty_values}
|
||||
def delete(name) when byte_size(name) > 0 do
|
||||
emoji_path()
|
||||
|> Path.join(name)
|
||||
|> File.rm_rf()
|
||||
end
|
||||
|
||||
def delete(_), do: {:error, :empty_values}
|
||||
|
||||
@spec add_file(String.t(), String.t(), Path.t(), Plug.Upload.t() | String.t()) ::
|
||||
{:ok, t()} | {:error, File.posix()} | {:error, :empty_values}
|
||||
def add_file(name, shortcode, filename, file)
|
||||
when byte_size(name) > 0 and byte_size(shortcode) > 0 and byte_size(filename) > 0 do
|
||||
with {_, nil} <- {:exists, Emoji.get(shortcode)},
|
||||
{_, %__MODULE__{} = pack} <- {:loaded, load_pack(name)} do
|
||||
file_path = Path.join(pack.path, filename)
|
||||
|
||||
create_subdirs(file_path)
|
||||
|
||||
case 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
|
||||
|
||||
files = Map.put(pack.files, shortcode, filename)
|
||||
|
||||
updated_pack = %{pack | files: files}
|
||||
|
||||
case save_pack(updated_pack) do
|
||||
:ok ->
|
||||
Emoji.reload()
|
||||
{:ok, updated_pack}
|
||||
|
||||
e ->
|
||||
e
|
||||
end
|
||||
def delete(name) do
|
||||
with :ok <- validate_not_empty([name]) do
|
||||
emoji_path()
|
||||
|> Path.join(name)
|
||||
|> File.rm_rf()
|
||||
end
|
||||
end
|
||||
|
||||
def add_file(_, _, _, _), do: {:error, :empty_values}
|
||||
|
||||
defp create_subdirs(file_path) do
|
||||
if String.contains?(file_path, "/") do
|
||||
file_path
|
||||
|> Path.dirname()
|
||||
|> File.mkdir_p!()
|
||||
@spec add_file(String.t(), String.t(), Path.t(), Plug.Upload.t() | String.t()) ::
|
||||
{:ok, t()} | {:error, File.posix() | atom()}
|
||||
def add_file(name, shortcode, filename, file) do
|
||||
with :ok <- validate_not_empty([name, shortcode, filename]),
|
||||
:ok <- validate_emoji_not_exists(shortcode),
|
||||
{:ok, pack} <- load_pack(name),
|
||||
:ok <- save_file(file, pack, filename),
|
||||
{:ok, updated_pack} <- pack |> put_emoji(shortcode, filename) |> save_pack() do
|
||||
Emoji.reload()
|
||||
{:ok, updated_pack}
|
||||
end
|
||||
end
|
||||
|
||||
@spec delete_file(String.t(), String.t()) ::
|
||||
{:ok, t()} | {:error, File.posix()} | {:error, :empty_values}
|
||||
def delete_file(name, shortcode) when byte_size(name) > 0 and byte_size(shortcode) > 0 do
|
||||
with {_, %__MODULE__{} = pack} <- {:loaded, load_pack(name)},
|
||||
{_, {filename, files}} when not is_nil(filename) <-
|
||||
{:exists, Map.pop(pack.files, shortcode)},
|
||||
emoji <- Path.join(pack.path, filename),
|
||||
{_, true} <- {:exists, File.exists?(emoji)} do
|
||||
emoji_dir = Path.dirname(emoji)
|
||||
|
||||
File.rm!(emoji)
|
||||
|
||||
if String.contains?(filename, "/") and File.ls!(emoji_dir) == [] do
|
||||
File.rmdir!(emoji_dir)
|
||||
end
|
||||
|
||||
updated_pack = %{pack | files: files}
|
||||
|
||||
case save_pack(updated_pack) do
|
||||
:ok ->
|
||||
Emoji.reload()
|
||||
{:ok, updated_pack}
|
||||
|
||||
e ->
|
||||
e
|
||||
end
|
||||
{:ok, t()} | {:error, File.posix() | atom()}
|
||||
def delete_file(name, shortcode) do
|
||||
with :ok <- validate_not_empty([name, shortcode]),
|
||||
{:ok, pack} <- load_pack(name),
|
||||
:ok <- remove_file(pack, shortcode),
|
||||
{:ok, updated_pack} <- pack |> delete_emoji(shortcode) |> save_pack() do
|
||||
Emoji.reload()
|
||||
{:ok, updated_pack}
|
||||
end
|
||||
end
|
||||
|
||||
def delete_file(_, _), do: {:error, :empty_values}
|
||||
|
||||
@spec update_file(String.t(), String.t(), String.t(), String.t(), boolean()) ::
|
||||
{:ok, t()} | {:error, File.posix()} | {:error, :empty_values}
|
||||
def update_file(name, shortcode, new_shortcode, new_filename, force)
|
||||
when byte_size(name) > 0 and byte_size(shortcode) > 0 and byte_size(new_shortcode) > 0 and
|
||||
byte_size(new_filename) > 0 do
|
||||
with {_, %__MODULE__{} = pack} <- {:loaded, load_pack(name)},
|
||||
{_, {filename, files}} when not is_nil(filename) <-
|
||||
{:exists, Map.pop(pack.files, shortcode)},
|
||||
{_, true} <- {:not_used, force or is_nil(Emoji.get(new_shortcode))} do
|
||||
old_path = Path.join(pack.path, filename)
|
||||
old_dir = Path.dirname(old_path)
|
||||
new_path = Path.join(pack.path, new_filename)
|
||||
|
||||
create_subdirs(new_path)
|
||||
|
||||
:ok = File.rename(old_path, new_path)
|
||||
|
||||
if String.contains?(filename, "/") and File.ls!(old_dir) == [] do
|
||||
File.rmdir!(old_dir)
|
||||
end
|
||||
|
||||
files = Map.put(files, new_shortcode, new_filename)
|
||||
|
||||
updated_pack = %{pack | files: files}
|
||||
|
||||
case save_pack(updated_pack) do
|
||||
:ok ->
|
||||
Emoji.reload()
|
||||
{:ok, updated_pack}
|
||||
|
||||
e ->
|
||||
e
|
||||
end
|
||||
{:ok, t()} | {:error, File.posix() | atom()}
|
||||
def update_file(name, shortcode, new_shortcode, new_filename, force) do
|
||||
with :ok <- validate_not_empty([name, shortcode, new_shortcode, new_filename]),
|
||||
{:ok, pack} <- load_pack(name),
|
||||
{:ok, filename} <- get_filename(pack, shortcode),
|
||||
:ok <- validate_emoji_not_exists(new_shortcode, force),
|
||||
:ok <- rename_file(pack, filename, new_filename),
|
||||
{:ok, updated_pack} <-
|
||||
pack
|
||||
|> delete_emoji(shortcode)
|
||||
|> put_emoji(new_shortcode, new_filename)
|
||||
|> save_pack() do
|
||||
Emoji.reload()
|
||||
{:ok, updated_pack}
|
||||
end
|
||||
end
|
||||
|
||||
def update_file(_, _, _, _, _), do: {:error, :empty_values}
|
||||
|
||||
@spec import_from_filesystem() :: {:ok, [String.t()]} | {:error, atom()}
|
||||
@spec import_from_filesystem() :: {:ok, [String.t()]} | {:error, File.posix() | atom()}
|
||||
def import_from_filesystem do
|
||||
emoji_path = emoji_path()
|
||||
|
||||
|
@ -184,7 +100,7 @@ def import_from_filesystem do
|
|||
File.dir?(path) and File.exists?(Path.join(path, "pack.json"))
|
||||
end)
|
||||
|> Enum.map(&write_pack_contents/1)
|
||||
|> Enum.filter(& &1)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|
||||
{:ok, names}
|
||||
else
|
||||
|
@ -193,6 +109,117 @@ def import_from_filesystem do
|
|||
end
|
||||
end
|
||||
|
||||
@spec list_remote(String.t()) :: {:ok, map()} | {:error, atom()}
|
||||
def list_remote(url) do
|
||||
uri = url |> String.trim() |> URI.parse()
|
||||
|
||||
with :ok <- validate_shareable_packs_available(uri) do
|
||||
uri
|
||||
|> URI.merge("/api/pleroma/emoji/packs")
|
||||
|> http_get()
|
||||
end
|
||||
end
|
||||
|
||||
@spec list_local() :: {:ok, map()}
|
||||
def list_local do
|
||||
with {:ok, results} <- list_packs_dir() do
|
||||
packs =
|
||||
results
|
||||
|> Enum.map(fn name ->
|
||||
case load_pack(name) do
|
||||
{:ok, pack} -> pack
|
||||
_ -> nil
|
||||
end
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|> Map.new(fn pack -> {pack.name, validate_pack(pack)} end)
|
||||
|
||||
{:ok, packs}
|
||||
end
|
||||
end
|
||||
|
||||
@spec get_archive(String.t()) :: {:ok, binary()} | {:error, atom()}
|
||||
def get_archive(name) do
|
||||
with {:ok, pack} <- load_pack(name),
|
||||
:ok <- validate_downloadable(pack) do
|
||||
{:ok, fetch_archive(pack)}
|
||||
end
|
||||
end
|
||||
|
||||
@spec download(String.t(), String.t(), String.t()) :: :ok | {:error, atom()}
|
||||
def download(name, url, as) do
|
||||
uri = url |> String.trim() |> URI.parse()
|
||||
|
||||
with :ok <- validate_shareable_packs_available(uri),
|
||||
{:ok, remote_pack} <- uri |> URI.merge("/api/pleroma/emoji/packs/#{name}") |> http_get(),
|
||||
{:ok, %{sha: sha, url: url} = pack_info} <- fetch_pack_info(remote_pack, uri, name),
|
||||
{:ok, archive} <- download_archive(url, sha),
|
||||
pack <- copy_as(remote_pack, as || name),
|
||||
{:ok, _} = unzip(archive, pack_info, remote_pack, pack) do
|
||||
# Fallback can't contain a pack.json file, since that would cause the fallback-src-sha256
|
||||
# in it to depend on itself
|
||||
if pack_info[:fallback] do
|
||||
save_pack(pack)
|
||||
else
|
||||
{:ok, pack}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@spec save_metadata(map(), t()) :: {:ok, t()} | {:error, File.posix()}
|
||||
def save_metadata(metadata, %__MODULE__{} = pack) do
|
||||
pack
|
||||
|> Map.put(:pack, metadata)
|
||||
|> save_pack()
|
||||
end
|
||||
|
||||
@spec update_metadata(String.t(), map()) :: {:ok, t()} | {:error, File.posix()}
|
||||
def update_metadata(name, data) do
|
||||
with {:ok, pack} <- load_pack(name) do
|
||||
if fallback_sha_changed?(pack, data) do
|
||||
update_sha_and_save_metadata(pack, data)
|
||||
else
|
||||
save_metadata(data, pack)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@spec load_pack(String.t()) :: {:ok, t()} | {:error, :not_found}
|
||||
def load_pack(name) do
|
||||
pack_file = Path.join([emoji_path(), name, "pack.json"])
|
||||
|
||||
if File.exists?(pack_file) do
|
||||
pack =
|
||||
pack_file
|
||||
|> File.read!()
|
||||
|> from_json()
|
||||
|> Map.put(:pack_file, pack_file)
|
||||
|> Map.put(:path, Path.dirname(pack_file))
|
||||
|> Map.put(:name, name)
|
||||
|
||||
{:ok, pack}
|
||||
else
|
||||
{:error, :not_found}
|
||||
end
|
||||
end
|
||||
|
||||
@spec emoji_path() :: Path.t()
|
||||
defp emoji_path do
|
||||
[:instance, :static_dir]
|
||||
|> Pleroma.Config.get!()
|
||||
|> Path.join("emoji")
|
||||
end
|
||||
|
||||
defp validate_emoji_not_exists(shortcode, force \\ false)
|
||||
defp validate_emoji_not_exists(_shortcode, true), do: :ok
|
||||
|
||||
defp validate_emoji_not_exists(shortcode, _) do
|
||||
case Emoji.get(shortcode) do
|
||||
nil -> :ok
|
||||
_ -> {:error, :already_exists}
|
||||
end
|
||||
end
|
||||
|
||||
defp write_pack_contents(path) do
|
||||
pack = %__MODULE__{
|
||||
files: files_from_path(path),
|
||||
|
@ -201,7 +228,7 @@ defp write_pack_contents(path) do
|
|||
}
|
||||
|
||||
case save_pack(pack) do
|
||||
:ok -> Path.basename(path)
|
||||
{:ok, _pack} -> Path.basename(path)
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
@ -216,7 +243,8 @@ defp files_from_path(path) do
|
|||
# FIXME: Copy-pasted from Pleroma.Emoji/load_from_file_stream/2
|
||||
|
||||
# Create a map of shortcodes to filenames from emoji.txt
|
||||
File.read!(txt_path)
|
||||
txt_path
|
||||
|> File.read!()
|
||||
|> String.split("\n")
|
||||
|> Enum.map(&String.trim/1)
|
||||
|> Enum.map(fn line ->
|
||||
|
@ -226,21 +254,18 @@ defp files_from_path(path) do
|
|||
[name, file | _] ->
|
||||
file_dir_name = Path.dirname(file)
|
||||
|
||||
file =
|
||||
if String.ends_with?(path, file_dir_name) do
|
||||
Path.basename(file)
|
||||
else
|
||||
file
|
||||
end
|
||||
|
||||
{name, file}
|
||||
if String.ends_with?(path, file_dir_name) do
|
||||
{name, Path.basename(file)}
|
||||
else
|
||||
{name, file}
|
||||
end
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end)
|
||||
|> Enum.filter(& &1)
|
||||
|> Enum.into(%{})
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|> Map.new()
|
||||
else
|
||||
# If there's no emoji.txt, assume all files
|
||||
# that are of certain extensions from the config are emojis and import them all
|
||||
|
@ -249,60 +274,20 @@ defp files_from_path(path) do
|
|||
end
|
||||
end
|
||||
|
||||
@spec list_remote(String.t()) :: {:ok, map()}
|
||||
def list_remote(url) do
|
||||
uri =
|
||||
url
|
||||
|> String.trim()
|
||||
|> URI.parse()
|
||||
|
||||
with {_, true} <- {:shareable, shareable_packs_available?(uri)} do
|
||||
packs =
|
||||
uri
|
||||
|> URI.merge("/api/pleroma/emoji/packs")
|
||||
|> to_string()
|
||||
|> Tesla.get!()
|
||||
|> Map.get(:body)
|
||||
|> Jason.decode!()
|
||||
|
||||
{:ok, packs}
|
||||
end
|
||||
end
|
||||
|
||||
@spec list_local() :: {:ok, map()}
|
||||
def list_local do
|
||||
emoji_path = emoji_path()
|
||||
|
||||
# Create the directory first if it does not exist. This is probably the first request made
|
||||
# with the API so it should be sufficient
|
||||
with {:create_dir, :ok} <- {:create_dir, File.mkdir_p(emoji_path)},
|
||||
{:ls, {:ok, results}} <- {:ls, File.ls(emoji_path)} do
|
||||
packs =
|
||||
results
|
||||
|> Enum.map(&load_pack/1)
|
||||
|> Enum.filter(& &1)
|
||||
|> Enum.map(&validate_pack/1)
|
||||
|> Map.new()
|
||||
|
||||
{:ok, packs}
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_pack(pack) do
|
||||
if downloadable?(pack) do
|
||||
archive = fetch_archive(pack)
|
||||
archive_sha = :crypto.hash(:sha256, archive) |> Base.encode16()
|
||||
info =
|
||||
if downloadable?(pack) do
|
||||
archive = fetch_archive(pack)
|
||||
archive_sha = :crypto.hash(:sha256, archive) |> Base.encode16()
|
||||
|
||||
info =
|
||||
pack.pack
|
||||
|> Map.put("can-download", true)
|
||||
|> Map.put("download-sha256", archive_sha)
|
||||
else
|
||||
Map.put(pack.pack, "can-download", false)
|
||||
end
|
||||
|
||||
{pack.name, Map.put(pack, :pack, info)}
|
||||
else
|
||||
info = Map.put(pack.pack, "can-download", false)
|
||||
{pack.name, Map.put(pack, :pack, info)}
|
||||
end
|
||||
Map.put(pack, :pack, info)
|
||||
end
|
||||
|
||||
defp downloadable?(pack) do
|
||||
|
@ -315,26 +300,6 @@ defp downloadable?(pack) do
|
|||
end)
|
||||
end
|
||||
|
||||
@spec get_archive(String.t()) :: {:ok, binary()}
|
||||
def get_archive(name) do
|
||||
with {_, %__MODULE__{} = pack} <- {:exists?, load_pack(name)},
|
||||
{_, true} <- {:can_download?, downloadable?(pack)} do
|
||||
{:ok, fetch_archive(pack)}
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_archive(pack) do
|
||||
hash = :crypto.hash(:md5, File.read!(pack.pack_file))
|
||||
|
||||
case Cachex.get!(:emoji_packs_cache, pack.name) do
|
||||
%{hash: ^hash, pack_data: archive} ->
|
||||
archive
|
||||
|
||||
_ ->
|
||||
create_archive_and_cache(pack, hash)
|
||||
end
|
||||
end
|
||||
|
||||
defp create_archive_and_cache(pack, hash) do
|
||||
files = ['pack.json' | Enum.map(pack.files, fn {_, file} -> to_charlist(file) end)]
|
||||
|
||||
|
@ -356,152 +321,221 @@ defp create_archive_and_cache(pack, hash) do
|
|||
result
|
||||
end
|
||||
|
||||
@spec download(String.t(), String.t(), String.t()) :: :ok
|
||||
def download(name, url, as) do
|
||||
uri =
|
||||
url
|
||||
|> String.trim()
|
||||
|> URI.parse()
|
||||
|
||||
with {_, true} <- {:shareable, shareable_packs_available?(uri)} do
|
||||
remote_pack =
|
||||
uri
|
||||
|> URI.merge("/api/pleroma/emoji/packs/#{name}")
|
||||
|> to_string()
|
||||
|> Tesla.get!()
|
||||
|> Map.get(:body)
|
||||
|> Jason.decode!()
|
||||
|
||||
result =
|
||||
case remote_pack["pack"] do
|
||||
%{"share-files" => true, "can-download" => true, "download-sha256" => sha} ->
|
||||
{:ok,
|
||||
%{
|
||||
sha: sha,
|
||||
url: URI.merge(uri, "/api/pleroma/emoji/packs/#{name}/archive") |> to_string()
|
||||
}}
|
||||
|
||||
%{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) ->
|
||||
{:ok,
|
||||
%{
|
||||
sha: sha,
|
||||
url: 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, url: url} = pinfo} <- result,
|
||||
%{body: archive} <- Tesla.get!(url),
|
||||
{_, true} <- {:checksum, Base.decode16!(sha) == :crypto.hash(:sha256, archive)} do
|
||||
local_name = as || name
|
||||
|
||||
path = Path.join(emoji_path(), local_name)
|
||||
|
||||
pack = %__MODULE__{
|
||||
name: local_name,
|
||||
path: path,
|
||||
files: remote_pack["files"],
|
||||
pack_file: Path.join(path, "pack.json")
|
||||
}
|
||||
|
||||
File.mkdir_p!(pack.path)
|
||||
|
||||
files = Enum.map(remote_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(archive, cwd: to_charlist(pack.path), 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
|
||||
save_pack(pack)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp save_pack(pack), do: File.write(pack.pack_file, Jason.encode!(pack, pretty: true))
|
||||
|
||||
@spec save_metadata(map(), t()) :: {:ok, t()} | {:error, File.posix()}
|
||||
def save_metadata(metadata, %__MODULE__{} = pack) do
|
||||
pack = Map.put(pack, :pack, metadata)
|
||||
|
||||
with :ok <- save_pack(pack) do
|
||||
defp save_pack(pack) do
|
||||
with {:ok, json} <- Jason.encode(pack, pretty: true),
|
||||
:ok <- File.write(pack.pack_file, json) do
|
||||
{:ok, pack}
|
||||
end
|
||||
end
|
||||
|
||||
@spec update_metadata(String.t(), map()) :: {:ok, t()} | {:error, File.posix()}
|
||||
def update_metadata(name, data) do
|
||||
pack = load_pack(name)
|
||||
|
||||
fb_sha_changed? =
|
||||
not is_nil(data["fallback-src"]) and data["fallback-src"] != pack.pack["fallback-src"]
|
||||
|
||||
with {_, true} <- {:update?, fb_sha_changed?},
|
||||
{:ok, %{body: zip}} <- Tesla.get(data["fallback-src"]),
|
||||
{:ok, f_list} <- :zip.unzip(zip, [:memory]),
|
||||
{_, true} <- {:has_all_files?, has_all_files?(pack.files, f_list)} do
|
||||
fallback_sha = :crypto.hash(:sha256, zip) |> Base.encode16()
|
||||
|
||||
data
|
||||
|> Map.put("fallback-src-sha256", fallback_sha)
|
||||
|> save_metadata(pack)
|
||||
else
|
||||
{:update?, _} -> save_metadata(data, pack)
|
||||
e -> e
|
||||
end
|
||||
end
|
||||
|
||||
# Check if all files from the pack.json are in the archive
|
||||
defp has_all_files?(files, f_list) do
|
||||
Enum.all?(files, fn {_, from_manifest} ->
|
||||
List.keyfind(f_list, to_charlist(from_manifest), 0)
|
||||
end)
|
||||
end
|
||||
|
||||
@spec load_pack(String.t()) :: t() | nil
|
||||
def load_pack(name) do
|
||||
pack_file = Path.join([emoji_path(), name, "pack.json"])
|
||||
|
||||
if File.exists?(pack_file) do
|
||||
pack_file
|
||||
|> File.read!()
|
||||
|> from_json()
|
||||
|> Map.put(:pack_file, pack_file)
|
||||
|> Map.put(:path, Path.dirname(pack_file))
|
||||
|> Map.put(:name, name)
|
||||
end
|
||||
end
|
||||
|
||||
defp from_json(json) do
|
||||
map = Jason.decode!(json)
|
||||
|
||||
struct(__MODULE__, %{files: map["files"], pack: map["pack"]})
|
||||
end
|
||||
|
||||
defp shareable_packs_available?(uri) do
|
||||
uri
|
||||
|> URI.merge("/.well-known/nodeinfo")
|
||||
|> to_string()
|
||||
|> Tesla.get!()
|
||||
|> Map.get(:body)
|
||||
|> Jason.decode!()
|
||||
|> Map.get("links")
|
||||
|> List.last()
|
||||
|> Map.get("href")
|
||||
# Get the actual nodeinfo address and fetch it
|
||||
|> Tesla.get!()
|
||||
|> Map.get(:body)
|
||||
|> Jason.decode!()
|
||||
|> get_in(["metadata", "features"])
|
||||
|> Enum.member?("shareable_emoji_packs")
|
||||
defp validate_shareable_packs_available(uri) do
|
||||
with {:ok, %{"links" => links}} <- uri |> URI.merge("/.well-known/nodeinfo") |> http_get(),
|
||||
# Get the actual nodeinfo address and fetch it
|
||||
{:ok, %{"metadata" => %{"features" => features}}} <-
|
||||
links |> List.last() |> Map.get("href") |> http_get() do
|
||||
if Enum.member?(features, "shareable_emoji_packs") do
|
||||
:ok
|
||||
else
|
||||
{:error, :not_shareable}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_not_empty(list) do
|
||||
if Enum.all?(list, fn i -> is_binary(i) and i != "" end) do
|
||||
:ok
|
||||
else
|
||||
{:error, :empty_values}
|
||||
end
|
||||
end
|
||||
|
||||
defp save_file(file, pack, filename) do
|
||||
file_path = Path.join(pack.path, filename)
|
||||
create_subdirs(file_path)
|
||||
|
||||
case file do
|
||||
%Plug.Upload{path: upload_path} ->
|
||||
# Copy the uploaded file from the temporary directory
|
||||
with {:ok, _} <- File.copy(upload_path, file_path), do: :ok
|
||||
|
||||
url when is_binary(url) ->
|
||||
# Download and write the file
|
||||
file_contents = Tesla.get!(url).body
|
||||
File.write(file_path, file_contents)
|
||||
end
|
||||
end
|
||||
|
||||
defp put_emoji(pack, shortcode, filename) do
|
||||
files = Map.put(pack.files, shortcode, filename)
|
||||
%{pack | files: files}
|
||||
end
|
||||
|
||||
defp delete_emoji(pack, shortcode) do
|
||||
files = Map.delete(pack.files, shortcode)
|
||||
%{pack | files: files}
|
||||
end
|
||||
|
||||
defp rename_file(pack, filename, new_filename) do
|
||||
old_path = Path.join(pack.path, filename)
|
||||
new_path = Path.join(pack.path, new_filename)
|
||||
create_subdirs(new_path)
|
||||
|
||||
with :ok <- File.rename(old_path, new_path) do
|
||||
remove_dir_if_empty(old_path, filename)
|
||||
end
|
||||
end
|
||||
|
||||
defp create_subdirs(file_path) do
|
||||
if String.contains?(file_path, "/") do
|
||||
file_path
|
||||
|> Path.dirname()
|
||||
|> File.mkdir_p!()
|
||||
end
|
||||
end
|
||||
|
||||
defp remove_file(pack, shortcode) do
|
||||
with {:ok, filename} <- get_filename(pack, shortcode),
|
||||
emoji <- Path.join(pack.path, filename),
|
||||
:ok <- File.rm(emoji) do
|
||||
remove_dir_if_empty(emoji, filename)
|
||||
end
|
||||
end
|
||||
|
||||
defp remove_dir_if_empty(emoji, filename) do
|
||||
dir = Path.dirname(emoji)
|
||||
|
||||
if String.contains?(filename, "/") and File.ls!(dir) == [] do
|
||||
File.rmdir!(dir)
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp get_filename(pack, shortcode) do
|
||||
with %{^shortcode => filename} when is_binary(filename) <- pack.files,
|
||||
true <- pack.path |> Path.join(filename) |> File.exists?() do
|
||||
{:ok, filename}
|
||||
else
|
||||
_ -> {:error, :doesnt_exist}
|
||||
end
|
||||
end
|
||||
|
||||
defp http_get(%URI{} = url), do: url |> to_string() |> http_get()
|
||||
|
||||
defp http_get(url) do
|
||||
with {:ok, %{body: body}} <- url |> Pleroma.HTTP.get() do
|
||||
Jason.decode(body)
|
||||
end
|
||||
end
|
||||
|
||||
defp list_packs_dir do
|
||||
emoji_path = emoji_path()
|
||||
# Create the directory first if it does not exist. This is probably the first request made
|
||||
# with the API so it should be sufficient
|
||||
with {:create_dir, :ok} <- {:create_dir, File.mkdir_p(emoji_path)},
|
||||
{:ls, {:ok, results}} <- {:ls, File.ls(emoji_path)} do
|
||||
{:ok, results}
|
||||
else
|
||||
{:create_dir, {:error, e}} -> {:error, :create_dir, e}
|
||||
{:ls, {:error, e}} -> {:error, :ls, e}
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_downloadable(pack) do
|
||||
if downloadable?(pack), do: :ok, else: {:error, :cant_download}
|
||||
end
|
||||
|
||||
defp copy_as(remote_pack, local_name) do
|
||||
path = Path.join(emoji_path(), local_name)
|
||||
|
||||
%__MODULE__{
|
||||
name: local_name,
|
||||
path: path,
|
||||
files: remote_pack["files"],
|
||||
pack_file: Path.join(path, "pack.json")
|
||||
}
|
||||
end
|
||||
|
||||
defp unzip(archive, pack_info, remote_pack, local_pack) do
|
||||
with :ok <- File.mkdir_p!(local_pack.path) do
|
||||
files = Enum.map(remote_pack["files"], fn {_, path} -> to_charlist(path) end)
|
||||
# Fallback cannot contain a pack.json file
|
||||
files = if pack_info[:fallback], do: files, else: ['pack.json' | files]
|
||||
|
||||
:zip.unzip(archive, cwd: to_charlist(local_pack.path), file_list: files)
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_pack_info(remote_pack, uri, name) do
|
||||
case remote_pack["pack"] do
|
||||
%{"share-files" => true, "can-download" => true, "download-sha256" => sha} ->
|
||||
{:ok,
|
||||
%{
|
||||
sha: sha,
|
||||
url: URI.merge(uri, "/api/pleroma/emoji/packs/#{name}/archive") |> to_string()
|
||||
}}
|
||||
|
||||
%{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) ->
|
||||
{:ok,
|
||||
%{
|
||||
sha: sha,
|
||||
url: src,
|
||||
fallback: true
|
||||
}}
|
||||
|
||||
_ ->
|
||||
{:error, "The pack was not set as shared and there is no fallback src to download from"}
|
||||
end
|
||||
end
|
||||
|
||||
defp download_archive(url, sha) do
|
||||
with {:ok, %{body: archive}} <- Tesla.get(url) do
|
||||
if Base.decode16!(sha) == :crypto.hash(:sha256, archive) do
|
||||
{:ok, archive}
|
||||
else
|
||||
{:error, :imvalid_checksum}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_archive(pack) do
|
||||
hash = :crypto.hash(:md5, File.read!(pack.pack_file))
|
||||
|
||||
case Cachex.get!(:emoji_packs_cache, pack.name) do
|
||||
%{hash: ^hash, pack_data: archive} -> archive
|
||||
_ -> create_archive_and_cache(pack, hash)
|
||||
end
|
||||
end
|
||||
|
||||
defp fallback_sha_changed?(pack, data) do
|
||||
is_binary(data[:"fallback-src"]) and data[:"fallback-src"] != pack.pack["fallback-src"]
|
||||
end
|
||||
|
||||
defp update_sha_and_save_metadata(pack, data) do
|
||||
with {:ok, %{body: zip}} <- Tesla.get(data[:"fallback-src"]),
|
||||
:ok <- validate_has_all_files(pack, zip) do
|
||||
fallback_sha = :sha256 |> :crypto.hash(zip) |> Base.encode16()
|
||||
|
||||
data
|
||||
|> Map.put("fallback-src-sha256", fallback_sha)
|
||||
|> save_metadata(pack)
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_has_all_files(pack, zip) do
|
||||
with {:ok, f_list} <- :zip.unzip(zip, [:memory]) do
|
||||
# Check if all files from the pack.json are in the archive
|
||||
pack.files
|
||||
|> Enum.all?(fn {_, from_manifest} ->
|
||||
List.keyfind(f_list, to_charlist(from_manifest), 0)
|
||||
end)
|
||||
|> if(do: :ok, else: {:error, :incomplete})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,11 +9,13 @@ defmodule Pleroma.Object do
|
|||
import Ecto.Changeset
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Object.Fetcher
|
||||
alias Pleroma.ObjectTombstone
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Workers.AttachmentsCleanupWorker
|
||||
|
||||
require Logger
|
||||
|
||||
|
@ -188,27 +190,37 @@ def swap_object_with_tombstone(object) do
|
|||
def delete(%Object{data: %{"id" => id}} = object) do
|
||||
with {:ok, _obj} = swap_object_with_tombstone(object),
|
||||
deleted_activity = Activity.delete_all_by_object_ap_id(id),
|
||||
{:ok, true} <- Cachex.del(:object_cache, "object:#{id}"),
|
||||
{:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do
|
||||
with true <- Pleroma.Config.get([:instance, :cleanup_attachments]) do
|
||||
{:ok, _} =
|
||||
Pleroma.Workers.AttachmentsCleanupWorker.enqueue("cleanup_attachments", %{
|
||||
"object" => object
|
||||
})
|
||||
end
|
||||
{:ok, _} <- invalid_object_cache(object) do
|
||||
cleanup_attachments(
|
||||
Config.get([:instance, :cleanup_attachments]),
|
||||
%{"object" => object}
|
||||
)
|
||||
|
||||
{:ok, object, deleted_activity}
|
||||
end
|
||||
end
|
||||
|
||||
def prune(%Object{data: %{"id" => id}} = object) do
|
||||
@spec cleanup_attachments(boolean(), %{required(:object) => map()}) ::
|
||||
{:ok, Oban.Job.t() | nil}
|
||||
def cleanup_attachments(true, %{"object" => _} = params) do
|
||||
AttachmentsCleanupWorker.enqueue("cleanup_attachments", params)
|
||||
end
|
||||
|
||||
def cleanup_attachments(_, _), do: {:ok, nil}
|
||||
|
||||
def prune(%Object{data: %{"id" => _id}} = object) do
|
||||
with {:ok, object} <- Repo.delete(object),
|
||||
{:ok, true} <- Cachex.del(:object_cache, "object:#{id}"),
|
||||
{:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do
|
||||
{:ok, _} <- invalid_object_cache(object) do
|
||||
{:ok, object}
|
||||
end
|
||||
end
|
||||
|
||||
def invalid_object_cache(%Object{data: %{"id" => id}}) do
|
||||
with {:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do
|
||||
Cachex.del(:web_resp_cache, URI.parse(id).path)
|
||||
end
|
||||
end
|
||||
|
||||
def set_cache(%Object{data: %{"id" => ap_id}} = object) do
|
||||
Cachex.put(:object_cache, "object:#{ap_id}", object)
|
||||
{:ok, object}
|
||||
|
|
|
@ -1204,7 +1204,9 @@ def get_users_from_set(ap_ids, local_only \\ true) do
|
|||
def get_recipients_from_activity(%Activity{recipients: to, actor: actor}) do
|
||||
to = [actor | to]
|
||||
|
||||
User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
|
||||
query = User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
|
||||
|
||||
query
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
|
@ -1430,6 +1432,25 @@ def delete(%User{} = user) do
|
|||
BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
|
||||
end
|
||||
|
||||
defp delete_and_invalidate_cache(%User{} = user) do
|
||||
invalidate_cache(user)
|
||||
Repo.delete(user)
|
||||
end
|
||||
|
||||
defp delete_or_deactivate(%User{local: false} = user), do: delete_and_invalidate_cache(user)
|
||||
|
||||
defp delete_or_deactivate(%User{local: true} = user) do
|
||||
status = account_status(user)
|
||||
|
||||
if status == :confirmation_pending do
|
||||
delete_and_invalidate_cache(user)
|
||||
else
|
||||
user
|
||||
|> change(%{deactivated: true, email: nil})
|
||||
|> update_and_set_cache()
|
||||
end
|
||||
end
|
||||
|
||||
def perform(:force_password_reset, user), do: force_password_reset(user)
|
||||
|
||||
@spec perform(atom(), User.t()) :: {:ok, User.t()}
|
||||
|
@ -1451,14 +1472,7 @@ def perform(:delete, %User{} = user) do
|
|||
|
||||
delete_user_activities(user)
|
||||
|
||||
if user.local do
|
||||
user
|
||||
|> change(%{deactivated: true, email: nil})
|
||||
|> update_and_set_cache()
|
||||
else
|
||||
invalidate_cache(user)
|
||||
Repo.delete(user)
|
||||
end
|
||||
delete_or_deactivate(user)
|
||||
end
|
||||
|
||||
def perform(:deactivate_async, user, status), do: deactivate(user, status)
|
||||
|
|
|
@ -167,20 +167,18 @@ defp compose_query({:friends, %User{id: id}}, query) do
|
|||
end
|
||||
|
||||
defp compose_query({:recipients_from_activity, to}, query) do
|
||||
query
|
||||
|> join(:left, [u], r in FollowingRelationship,
|
||||
as: :relationships,
|
||||
on: r.follower_id == u.id
|
||||
following_query =
|
||||
from(u in User,
|
||||
join: f in FollowingRelationship,
|
||||
on: u.id == f.following_id,
|
||||
where: f.state == ^:follow_accept,
|
||||
where: u.follower_address in ^to,
|
||||
select: f.follower_id
|
||||
)
|
||||
|
||||
from(u in query,
|
||||
where: u.ap_id in ^to or u.id in subquery(following_query)
|
||||
)
|
||||
|> join(:left, [relationships: r], f in User,
|
||||
as: :following,
|
||||
on: f.id == r.following_id
|
||||
)
|
||||
|> where(
|
||||
[u, following: f, relationships: r],
|
||||
u.ap_id in ^to or (f.follower_address in ^to and r.state == ^:follow_accept)
|
||||
)
|
||||
|> distinct(true)
|
||||
end
|
||||
|
||||
defp compose_query({:order_by, key}, query) do
|
||||
|
|
|
@ -393,7 +393,7 @@ defp create_request do
|
|||
format: :password
|
||||
},
|
||||
agreement: %Schema{
|
||||
type: :boolean,
|
||||
allOf: [BooleanLike],
|
||||
description:
|
||||
"Whether the user agrees to the local rules, terms, and policies. These should be presented to the user in order to allow them to consent before setting this parameter to TRUE."
|
||||
},
|
||||
|
@ -463,7 +463,7 @@ defp update_creadentials_request do
|
|||
type: :object,
|
||||
properties: %{
|
||||
bot: %Schema{
|
||||
type: :boolean,
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "Whether the account has a bot flag."
|
||||
},
|
||||
|
@ -486,7 +486,7 @@ defp update_creadentials_request do
|
|||
format: :binary
|
||||
},
|
||||
locked: %Schema{
|
||||
type: :boolean,
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "Whether manual approval of follow requests is required."
|
||||
},
|
||||
|
@ -510,37 +510,37 @@ defp update_creadentials_request do
|
|||
|
||||
# Pleroma-specific fields
|
||||
no_rich_text: %Schema{
|
||||
type: :boolean,
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "html tags are stripped from all statuses requested from the API"
|
||||
},
|
||||
hide_followers: %Schema{
|
||||
type: :boolean,
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "user's followers will be hidden"
|
||||
},
|
||||
hide_follows: %Schema{
|
||||
type: :boolean,
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "user's follows will be hidden"
|
||||
},
|
||||
hide_followers_count: %Schema{
|
||||
type: :boolean,
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "user's follower count will be hidden"
|
||||
},
|
||||
hide_follows_count: %Schema{
|
||||
type: :boolean,
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "user's follow count will be hidden"
|
||||
},
|
||||
hide_favorites: %Schema{
|
||||
type: :boolean,
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "user's favorites timeline will be hidden"
|
||||
},
|
||||
show_role: %Schema{
|
||||
type: :boolean,
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "user's role (e.g admin, moderator) will be exposed to anyone in the
|
||||
API"
|
||||
|
@ -552,12 +552,12 @@ defp update_creadentials_request do
|
|||
description: "Opaque user settings to be saved on the backend."
|
||||
},
|
||||
skip_thread_containment: %Schema{
|
||||
type: :boolean,
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "Skip filtering out broken threads"
|
||||
},
|
||||
allow_following_move: %Schema{
|
||||
type: :boolean,
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "Allows automatically follow moved following accounts"
|
||||
},
|
||||
|
@ -568,7 +568,7 @@ defp update_creadentials_request do
|
|||
format: :binary
|
||||
},
|
||||
discoverable: %Schema{
|
||||
type: :boolean,
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description:
|
||||
"Discovery of this account in search results and other services is allowed."
|
||||
|
@ -678,7 +678,7 @@ defp mute_request do
|
|||
type: :object,
|
||||
properties: %{
|
||||
notifications: %Schema{
|
||||
type: :boolean,
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "Mute notifications in addition to statuses? Defaults to true.",
|
||||
default: true
|
||||
|
|
|
@ -6,6 +6,7 @@ defmodule Pleroma.Web.ApiSpec.FilterOperation do
|
|||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Helpers
|
||||
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
|
@ -171,7 +172,7 @@ defp create_request do
|
|||
type: :object,
|
||||
properties: %{
|
||||
irreversible: %Schema{
|
||||
type: :bolean,
|
||||
allOf: [BooleanLike],
|
||||
description:
|
||||
"Should the server irreversibly drop matching entities from home and notifications?",
|
||||
default: false
|
||||
|
@ -199,13 +200,13 @@ defp update_request do
|
|||
"Array of enumerable strings `home`, `notifications`, `public`, `thread`. At least one context must be specified."
|
||||
},
|
||||
irreversible: %Schema{
|
||||
type: :boolean,
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description:
|
||||
"Should the server irreversibly drop matching entities from home and notifications?"
|
||||
},
|
||||
whole_word: %Schema{
|
||||
type: :boolean,
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "Consider word boundaries?",
|
||||
default: true
|
||||
|
|
|
@ -125,11 +125,17 @@ defp instance do
|
|||
},
|
||||
avatar_upload_limit: %Schema{type: :integer, description: "The title of the website"},
|
||||
background_upload_limit: %Schema{type: :integer, description: "The title of the website"},
|
||||
banner_upload_limit: %Schema{type: :integer, description: "The title of the website"}
|
||||
banner_upload_limit: %Schema{type: :integer, description: "The title of the website"},
|
||||
background_image: %Schema{
|
||||
type: :string,
|
||||
format: :uri,
|
||||
description: "The background image for the website"
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"avatar_upload_limit" => 2_000_000,
|
||||
"background_upload_limit" => 4_000_000,
|
||||
"background_image" => "/static/image.png",
|
||||
"banner_upload_limit" => 4_000_000,
|
||||
"description" => "A Pleroma instance, an alternative fediverse server",
|
||||
"email" => "lain@lain.com",
|
||||
|
|
|
@ -0,0 +1,390 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
|
||||
import Pleroma.Web.ApiSpec.Helpers
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def remote_operation do
|
||||
%Operation{
|
||||
tags: ["Emoji Packs"],
|
||||
summary: "Make request to another instance for emoji packs list",
|
||||
security: [%{"oAuth" => ["write"]}],
|
||||
parameters: [url_param()],
|
||||
operationId: "PleromaAPI.EmojiPackController.remote",
|
||||
responses: %{
|
||||
200 => emoji_packs_response(),
|
||||
500 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def index_operation do
|
||||
%Operation{
|
||||
tags: ["Emoji Packs"],
|
||||
summary: "Lists local custom emoji packs",
|
||||
operationId: "PleromaAPI.EmojiPackController.index",
|
||||
responses: %{
|
||||
200 => emoji_packs_response()
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def show_operation do
|
||||
%Operation{
|
||||
tags: ["Emoji Packs"],
|
||||
summary: "Show emoji pack",
|
||||
operationId: "PleromaAPI.EmojiPackController.show",
|
||||
parameters: [name_param()],
|
||||
responses: %{
|
||||
200 => Operation.response("Emoji Pack", "application/json", emoji_pack()),
|
||||
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def archive_operation do
|
||||
%Operation{
|
||||
tags: ["Emoji Packs"],
|
||||
summary: "Requests a local pack archive from the instance",
|
||||
operationId: "PleromaAPI.EmojiPackController.archive",
|
||||
parameters: [name_param()],
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Archive file", "application/octet-stream", %Schema{
|
||||
type: :string,
|
||||
format: :binary
|
||||
}),
|
||||
403 => Operation.response("Forbidden", "application/json", ApiError),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def download_operation do
|
||||
%Operation{
|
||||
tags: ["Emoji Packs"],
|
||||
summary: "Download pack from another instance",
|
||||
operationId: "PleromaAPI.EmojiPackController.download",
|
||||
security: [%{"oAuth" => ["write"]}],
|
||||
requestBody: request_body("Parameters", download_request(), required: true),
|
||||
responses: %{
|
||||
200 => ok_response(),
|
||||
500 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp download_request do
|
||||
%Schema{
|
||||
type: :object,
|
||||
required: [:url, :name],
|
||||
properties: %{
|
||||
url: %Schema{
|
||||
type: :string,
|
||||
format: :uri,
|
||||
description: "URL of the instance to download from"
|
||||
},
|
||||
name: %Schema{type: :string, format: :uri, description: "Pack Name"},
|
||||
as: %Schema{type: :string, format: :uri, description: "Save as"}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def create_operation do
|
||||
%Operation{
|
||||
tags: ["Emoji Packs"],
|
||||
summary: "Create an empty pack",
|
||||
operationId: "PleromaAPI.EmojiPackController.create",
|
||||
security: [%{"oAuth" => ["write"]}],
|
||||
parameters: [name_param()],
|
||||
responses: %{
|
||||
200 => ok_response(),
|
||||
400 => Operation.response("Not Found", "application/json", ApiError),
|
||||
409 => Operation.response("Conflict", "application/json", ApiError),
|
||||
500 => Operation.response("Error", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def delete_operation do
|
||||
%Operation{
|
||||
tags: ["Emoji Packs"],
|
||||
summary: "Delete a custom emoji pack",
|
||||
operationId: "PleromaAPI.EmojiPackController.delete",
|
||||
security: [%{"oAuth" => ["write"]}],
|
||||
parameters: [name_param()],
|
||||
responses: %{
|
||||
200 => ok_response(),
|
||||
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def update_operation do
|
||||
%Operation{
|
||||
tags: ["Emoji Packs"],
|
||||
summary: "Updates (replaces) pack metadata",
|
||||
operationId: "PleromaAPI.EmojiPackController.update",
|
||||
security: [%{"oAuth" => ["write"]}],
|
||||
requestBody: request_body("Parameters", update_request(), required: true),
|
||||
parameters: [name_param()],
|
||||
responses: %{
|
||||
200 => Operation.response("Metadata", "application/json", metadata()),
|
||||
400 => Operation.response("Bad Request", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def add_file_operation do
|
||||
%Operation{
|
||||
tags: ["Emoji Packs"],
|
||||
summary: "Add new file to the pack",
|
||||
operationId: "PleromaAPI.EmojiPackController.add_file",
|
||||
security: [%{"oAuth" => ["write"]}],
|
||||
requestBody: request_body("Parameters", add_file_request(), required: true),
|
||||
parameters: [name_param()],
|
||||
responses: %{
|
||||
200 => Operation.response("Files Object", "application/json", files_object()),
|
||||
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||
409 => Operation.response("Conflict", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp add_file_request do
|
||||
%Schema{
|
||||
type: :object,
|
||||
required: [:file],
|
||||
properties: %{
|
||||
file: %Schema{
|
||||
description:
|
||||
"File needs to be uploaded with the multipart request or link to remote file",
|
||||
anyOf: [
|
||||
%Schema{type: :string, format: :binary},
|
||||
%Schema{type: :string, format: :uri}
|
||||
]
|
||||
},
|
||||
shortcode: %Schema{
|
||||
type: :string,
|
||||
description:
|
||||
"Shortcode for new emoji, must be unique for all emoji. If not sended, shortcode will be taken from original filename."
|
||||
},
|
||||
filename: %Schema{
|
||||
type: :string,
|
||||
description:
|
||||
"New emoji file name. If not specified will be taken from original filename."
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def update_file_operation do
|
||||
%Operation{
|
||||
tags: ["Emoji Packs"],
|
||||
summary: "Add new file to the pack",
|
||||
operationId: "PleromaAPI.EmojiPackController.update_file",
|
||||
security: [%{"oAuth" => ["write"]}],
|
||||
requestBody: request_body("Parameters", update_file_request(), required: true),
|
||||
parameters: [name_param()],
|
||||
responses: %{
|
||||
200 => Operation.response("Files Object", "application/json", files_object()),
|
||||
400 => Operation.response("Bad Request", "application/json", ApiError),
|
||||
409 => Operation.response("Conflict", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp update_file_request do
|
||||
%Schema{
|
||||
type: :object,
|
||||
required: [:shortcode, :new_shortcode, :new_filename],
|
||||
properties: %{
|
||||
shortcode: %Schema{
|
||||
type: :string,
|
||||
description: "Emoji file shortcode"
|
||||
},
|
||||
new_shortcode: %Schema{
|
||||
type: :string,
|
||||
description: "New emoji file shortcode"
|
||||
},
|
||||
new_filename: %Schema{
|
||||
type: :string,
|
||||
description: "New filename for emoji file"
|
||||
},
|
||||
force: %Schema{
|
||||
type: :boolean,
|
||||
description: "With true value to overwrite existing emoji with new shortcode",
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def delete_file_operation do
|
||||
%Operation{
|
||||
tags: ["Emoji Packs"],
|
||||
summary: "Delete emoji file from pack",
|
||||
operationId: "PleromaAPI.EmojiPackController.delete_file",
|
||||
security: [%{"oAuth" => ["write"]}],
|
||||
parameters: [
|
||||
name_param(),
|
||||
Operation.parameter(:shortcode, :query, :string, "File shortcode",
|
||||
example: "cofe",
|
||||
required: true
|
||||
)
|
||||
],
|
||||
responses: %{
|
||||
200 => Operation.response("Files Object", "application/json", files_object()),
|
||||
400 => Operation.response("Bad Request", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def import_from_filesystem_operation do
|
||||
%Operation{
|
||||
tags: ["Emoji Packs"],
|
||||
summary: "Imports packs from filesystem",
|
||||
operationId: "PleromaAPI.EmojiPackController.import",
|
||||
security: [%{"oAuth" => ["write"]}],
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Array of imported pack names", "application/json", %Schema{
|
||||
type: :array,
|
||||
items: %Schema{type: :string}
|
||||
})
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp name_param do
|
||||
Operation.parameter(:name, :path, :string, "Pack Name", example: "cofe", required: true)
|
||||
end
|
||||
|
||||
defp url_param do
|
||||
Operation.parameter(
|
||||
:url,
|
||||
:query,
|
||||
%Schema{type: :string, format: :uri},
|
||||
"URL of the instance",
|
||||
required: true
|
||||
)
|
||||
end
|
||||
|
||||
defp ok_response do
|
||||
Operation.response("Ok", "application/json", %Schema{type: :string, example: "ok"})
|
||||
end
|
||||
|
||||
defp emoji_packs_response do
|
||||
Operation.response(
|
||||
"Object with pack names as keys and pack contents as values",
|
||||
"application/json",
|
||||
%Schema{
|
||||
type: :object,
|
||||
additionalProperties: emoji_pack(),
|
||||
example: %{
|
||||
"emojos" => emoji_pack().example
|
||||
}
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
defp emoji_pack do
|
||||
%Schema{
|
||||
title: "EmojiPack",
|
||||
type: :object,
|
||||
properties: %{
|
||||
files: files_object(),
|
||||
pack: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
license: %Schema{type: :string},
|
||||
homepage: %Schema{type: :string, format: :uri},
|
||||
description: %Schema{type: :string},
|
||||
"can-download": %Schema{type: :boolean},
|
||||
"share-files": %Schema{type: :boolean},
|
||||
"download-sha256": %Schema{type: :string}
|
||||
}
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"files" => %{"emacs" => "emacs.png", "guix" => "guix.png"},
|
||||
"pack" => %{
|
||||
"license" => "Test license",
|
||||
"homepage" => "https://pleroma.social",
|
||||
"description" => "Test description",
|
||||
"can-download" => true,
|
||||
"share-files" => true,
|
||||
"download-sha256" => "57482F30674FD3DE821FF48C81C00DA4D4AF1F300209253684ABA7075E5FC238"
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp files_object do
|
||||
%Schema{
|
||||
type: :object,
|
||||
additionalProperties: %Schema{type: :string},
|
||||
description: "Object with emoji names as keys and filenames as values"
|
||||
}
|
||||
end
|
||||
|
||||
defp update_request do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
metadata: %Schema{
|
||||
type: :object,
|
||||
description: "Metadata to replace the old one",
|
||||
properties: %{
|
||||
license: %Schema{type: :string},
|
||||
homepage: %Schema{type: :string, format: :uri},
|
||||
description: %Schema{type: :string},
|
||||
"fallback-src": %Schema{
|
||||
type: :string,
|
||||
format: :uri,
|
||||
description: "Fallback url to download pack from"
|
||||
},
|
||||
"fallback-src-sha256": %Schema{
|
||||
type: :string,
|
||||
description: "SHA256 encoded for fallback pack archive"
|
||||
},
|
||||
"share-files": %Schema{type: :boolean, description: "Is pack allowed for sharing?"}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp metadata do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
license: %Schema{type: :string},
|
||||
homepage: %Schema{type: :string, format: :uri},
|
||||
description: %Schema{type: :string},
|
||||
"fallback-src": %Schema{
|
||||
type: :string,
|
||||
format: :uri,
|
||||
description: "Fallback url to download pack from"
|
||||
},
|
||||
"fallback-src-sha256": %Schema{
|
||||
type: :string,
|
||||
description: "SHA256 encoded for fallback pack archive"
|
||||
},
|
||||
"share-files": %Schema{type: :boolean, description: "Is pack allowed for sharing?"}
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
|
@ -0,0 +1,79 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.PleromaMascotOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
|
||||
import Pleroma.Web.ApiSpec.Helpers
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def show_operation do
|
||||
%Operation{
|
||||
tags: ["Mascot"],
|
||||
summary: "Gets user mascot image",
|
||||
security: [%{"oAuth" => ["read:accounts"]}],
|
||||
operationId: "PleromaAPI.MascotController.show",
|
||||
responses: %{
|
||||
200 => Operation.response("Mascot", "application/json", mascot())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def update_operation do
|
||||
%Operation{
|
||||
tags: ["Mascot"],
|
||||
summary: "Set/clear user avatar image",
|
||||
description:
|
||||
"Behaves exactly the same as `POST /api/v1/upload`. Can only accept images - any attempt to upload non-image files will be met with `HTTP 415 Unsupported Media Type`.",
|
||||
operationId: "PleromaAPI.MascotController.update",
|
||||
requestBody:
|
||||
request_body(
|
||||
"Parameters",
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
file: %Schema{type: :string, format: :binary}
|
||||
}
|
||||
},
|
||||
required: true
|
||||
),
|
||||
security: [%{"oAuth" => ["write:accounts"]}],
|
||||
responses: %{
|
||||
200 => Operation.response("Mascot", "application/json", mascot()),
|
||||
415 => Operation.response("Unsupported Media Type", "application/json", ApiError)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp mascot do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
url: %Schema{type: :string, format: :uri},
|
||||
type: %Schema{type: :string},
|
||||
pleroma: %Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
mime_type: %Schema{type: :string}
|
||||
}
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"id" => "abcdefg",
|
||||
"url" => "https://pleroma.example.org/media/abcdefg.png",
|
||||
"type" => "image",
|
||||
"pleroma" => %{
|
||||
"mime_type" => "image/png"
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
|
@ -0,0 +1,102 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Reference
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Account
|
||||
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
|
||||
|
||||
import Pleroma.Web.ApiSpec.Helpers
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
def create_operation do
|
||||
%Operation{
|
||||
tags: ["Scrobbles"],
|
||||
summary: "Creates a new Listen activity for an account",
|
||||
security: [%{"oAuth" => ["write"]}],
|
||||
operationId: "PleromaAPI.ScrobbleController.create",
|
||||
requestBody: request_body("Parameters", create_request(), requried: true),
|
||||
responses: %{
|
||||
200 => Operation.response("Scrobble", "application/json", scrobble())
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def index_operation do
|
||||
%Operation{
|
||||
tags: ["Scrobbles"],
|
||||
summary: "Requests a list of current and recent Listen activities for an account",
|
||||
operationId: "PleromaAPI.ScrobbleController.index",
|
||||
parameters: [
|
||||
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"} | pagination_params()
|
||||
],
|
||||
security: [%{"oAuth" => ["read"]}],
|
||||
responses: %{
|
||||
200 =>
|
||||
Operation.response("Array of Scrobble", "application/json", %Schema{
|
||||
type: :array,
|
||||
items: scrobble()
|
||||
})
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp create_request do
|
||||
%Schema{
|
||||
type: :object,
|
||||
required: [:title],
|
||||
properties: %{
|
||||
title: %Schema{type: :string, description: "The title of the media playing"},
|
||||
album: %Schema{type: :string, description: "The album of the media playing"},
|
||||
artist: %Schema{type: :string, description: "The artist of the media playing"},
|
||||
length: %Schema{type: :integer, description: "The length of the media playing"},
|
||||
visibility: %Schema{
|
||||
allOf: [VisibilityScope],
|
||||
default: "public",
|
||||
description: "Scrobble visibility"
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"title" => "Some Title",
|
||||
"artist" => "Some Artist",
|
||||
"album" => "Some Album",
|
||||
"length" => 180_000
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp scrobble do
|
||||
%Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %Schema{type: :string},
|
||||
account: Account,
|
||||
title: %Schema{type: :string, description: "The title of the media playing"},
|
||||
album: %Schema{type: :string, description: "The album of the media playing"},
|
||||
artist: %Schema{type: :string, description: "The artist of the media playing"},
|
||||
length: %Schema{
|
||||
type: :integer,
|
||||
description: "The length of the media playing",
|
||||
nullable: true
|
||||
},
|
||||
created_at: %Schema{type: :string, format: :"date-time"}
|
||||
},
|
||||
example: %{
|
||||
"id" => "1234",
|
||||
"account" => Account.schema().example,
|
||||
"title" => "Some Title",
|
||||
"artist" => "Some Artist",
|
||||
"album" => "Some Album",
|
||||
"length" => 180_000,
|
||||
"created_at" => "2019-09-28T12:40:45.000Z"
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
|
@ -7,6 +7,7 @@ defmodule Pleroma.Web.ApiSpec.ReportOperation do
|
|||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Helpers
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
|
||||
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
|
@ -47,7 +48,7 @@ defp create_request do
|
|||
description: "Reason for the report"
|
||||
},
|
||||
forward: %Schema{
|
||||
type: :boolean,
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
default: false,
|
||||
description:
|
||||
|
|
|
@ -7,6 +7,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.AccountOperation
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
|
||||
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ScheduledStatus
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Status
|
||||
|
@ -394,12 +395,12 @@ defp create_request do
|
|||
"Duration the poll should be open, in seconds. Must be provided with `poll[options]`"
|
||||
},
|
||||
multiple: %Schema{
|
||||
type: :boolean,
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "Allow multiple choices?"
|
||||
},
|
||||
hide_totals: %Schema{
|
||||
type: :boolean,
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "Hide vote counts until the poll ends?"
|
||||
}
|
||||
|
@ -411,7 +412,7 @@ defp create_request do
|
|||
description: "ID of the status being replied to, if status is a reply"
|
||||
},
|
||||
sensitive: %Schema{
|
||||
type: :boolean,
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "Mark status and attached media as sensitive?"
|
||||
},
|
||||
|
@ -435,7 +436,7 @@ defp create_request do
|
|||
},
|
||||
# Pleroma-specific properties:
|
||||
preview: %Schema{
|
||||
type: :boolean,
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description:
|
||||
"If set to `true` the post won't be actually posted, but the status entitiy would still be rendered back. This could be useful for previewing rich text/custom emoji, for example"
|
||||
|
|
|
@ -7,6 +7,7 @@ defmodule Pleroma.Web.ApiSpec.SubscriptionOperation do
|
|||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.Helpers
|
||||
alias Pleroma.Web.ApiSpec.Schemas.ApiError
|
||||
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
|
||||
alias Pleroma.Web.ApiSpec.Schemas.PushSubscription
|
||||
|
||||
def open_api_operation(action) do
|
||||
|
@ -117,27 +118,27 @@ defp create_request do
|
|||
type: :object,
|
||||
properties: %{
|
||||
follow: %Schema{
|
||||
type: :boolean,
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "Receive follow notifications?"
|
||||
},
|
||||
favourite: %Schema{
|
||||
type: :boolean,
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "Receive favourite notifications?"
|
||||
},
|
||||
reblog: %Schema{
|
||||
type: :boolean,
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "Receive reblog notifications?"
|
||||
},
|
||||
mention: %Schema{
|
||||
type: :boolean,
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "Receive mention notifications?"
|
||||
},
|
||||
poll: %Schema{
|
||||
type: :boolean,
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "Receive poll notifications?"
|
||||
}
|
||||
|
@ -181,27 +182,27 @@ defp update_request do
|
|||
type: :object,
|
||||
properties: %{
|
||||
follow: %Schema{
|
||||
type: :boolean,
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "Receive follow notifications?"
|
||||
},
|
||||
favourite: %Schema{
|
||||
type: :boolean,
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "Receive favourite notifications?"
|
||||
},
|
||||
reblog: %Schema{
|
||||
type: :boolean,
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "Receive reblog notifications?"
|
||||
},
|
||||
mention: %Schema{
|
||||
type: :boolean,
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "Receive mention notifications?"
|
||||
},
|
||||
poll: %Schema{
|
||||
type: :boolean,
|
||||
allOf: [BooleanLike],
|
||||
nullable: true,
|
||||
description: "Receive poll notifications?"
|
||||
}
|
||||
|
|
|
@ -387,11 +387,14 @@ def check_expiry_date(expiry_str) do
|
|||
|> check_expiry_date()
|
||||
end
|
||||
|
||||
def listen(user, %{"title" => _} = data) do
|
||||
with visibility <- data["visibility"] || "public",
|
||||
{to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
|
||||
def listen(user, data) do
|
||||
visibility = Map.get(data, :visibility, "public")
|
||||
|
||||
with {to, cc} <- get_to_and_cc(user, [], nil, visibility, nil),
|
||||
listen_data <-
|
||||
Map.take(data, ["album", "artist", "title", "length"])
|
||||
data
|
||||
|> Map.take([:album, :artist, :title, :length])
|
||||
|> Map.new(fn {key, value} -> {to_string(key), value} end)
|
||||
|> Map.put("type", "Audio")
|
||||
|> Map.put("to", to)
|
||||
|> Map.put("cc", cc)
|
||||
|
|
|
@ -177,6 +177,7 @@ def update_credentials(%{assigns: %{user: original_user}, body_params: params} =
|
|||
)
|
||||
|> add_if_present(params, :pleroma_settings_store, :pleroma_settings_store)
|
||||
|> add_if_present(params, :default_scope, :default_scope)
|
||||
|> add_if_present(params["source"], "privacy", :default_scope)
|
||||
|> add_if_present(params, :actor_type, :actor_type)
|
||||
|
||||
changeset = User.update_changeset(user, user_params)
|
||||
|
@ -189,7 +190,8 @@ def update_credentials(%{assigns: %{user: original_user}, body_params: params} =
|
|||
end
|
||||
|
||||
defp add_if_present(map, params, params_field, map_field, value_function \\ &{:ok, &1}) do
|
||||
with true <- Map.has_key?(params, params_field),
|
||||
with true <- is_map(params),
|
||||
true <- Map.has_key?(params, params_field),
|
||||
{:ok, new_value} <- value_function.(Map.get(params, params_field)) do
|
||||
Map.put(map, map_field, new_value)
|
||||
else
|
||||
|
|
|
@ -33,6 +33,7 @@ def render("show.json", _) do
|
|||
avatar_upload_limit: Keyword.get(instance, :avatar_upload_limit),
|
||||
background_upload_limit: Keyword.get(instance, :background_upload_limit),
|
||||
banner_upload_limit: Keyword.get(instance, :banner_upload_limit),
|
||||
background_image: Keyword.get(instance, :background_image),
|
||||
pleroma: %{
|
||||
metadata: %{
|
||||
features: features(),
|
||||
|
|
|
@ -436,27 +436,6 @@ def render("attachment.json", %{attachment: attachment}) do
|
|||
}
|
||||
end
|
||||
|
||||
def render("listen.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do
|
||||
object = Object.normalize(activity)
|
||||
|
||||
user = get_user(activity.data["actor"])
|
||||
created_at = Utils.to_masto_date(activity.data["published"])
|
||||
|
||||
%{
|
||||
id: activity.id,
|
||||
account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
|
||||
created_at: created_at,
|
||||
title: object.data["title"] |> HTML.strip_tags(),
|
||||
artist: object.data["artist"] |> HTML.strip_tags(),
|
||||
album: object.data["album"] |> HTML.strip_tags(),
|
||||
length: object.data["length"]
|
||||
}
|
||||
end
|
||||
|
||||
def render("listens.json", opts) do
|
||||
safe_render_many(opts.activities, StatusView, "listen.json", opts)
|
||||
end
|
||||
|
||||
def render("context.json", %{activity: activity, activities: activities, user: user}) do
|
||||
%{ancestors: ancestors, descendants: descendants} =
|
||||
activities
|
||||
|
|
26
lib/pleroma/web/media_proxy/invalidation.ex
Normal file
26
lib/pleroma/web/media_proxy/invalidation.ex
Normal file
|
@ -0,0 +1,26 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.MediaProxy.Invalidation do
|
||||
@moduledoc false
|
||||
|
||||
@callback purge(list(String.t()), map()) :: {:ok, String.t()} | {:error, String.t()}
|
||||
|
||||
alias Pleroma.Config
|
||||
|
||||
@spec purge(list(String.t())) :: {:ok, String.t()} | {:error, String.t()}
|
||||
def purge(urls) do
|
||||
[:media_proxy, :invalidation, :enabled]
|
||||
|> Config.get()
|
||||
|> do_purge(urls)
|
||||
end
|
||||
|
||||
defp do_purge(true, urls) do
|
||||
provider = Config.get([:media_proxy, :invalidation, :provider])
|
||||
options = Config.get(provider)
|
||||
provider.purge(urls, options)
|
||||
end
|
||||
|
||||
defp do_purge(_, _), do: :ok
|
||||
end
|
40
lib/pleroma/web/media_proxy/invalidations/http.ex
Normal file
40
lib/pleroma/web/media_proxy/invalidations/http.ex
Normal file
|
@ -0,0 +1,40 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.MediaProxy.Invalidation.Http do
|
||||
@moduledoc false
|
||||
@behaviour Pleroma.Web.MediaProxy.Invalidation
|
||||
|
||||
require Logger
|
||||
|
||||
@impl Pleroma.Web.MediaProxy.Invalidation
|
||||
def purge(urls, opts) do
|
||||
method = Map.get(opts, :method, :purge)
|
||||
headers = Map.get(opts, :headers, [])
|
||||
options = Map.get(opts, :options, [])
|
||||
|
||||
Logger.debug("Running cache purge: #{inspect(urls)}")
|
||||
|
||||
Enum.each(urls, fn url ->
|
||||
with {:error, error} <- do_purge(method, url, headers, options) do
|
||||
Logger.error("Error while cache purge: url - #{url}, error: #{inspect(error)}")
|
||||
end
|
||||
end)
|
||||
|
||||
{:ok, "success"}
|
||||
end
|
||||
|
||||
defp do_purge(method, url, headers, options) do
|
||||
case Pleroma.HTTP.request(method, url, "", headers, options) do
|
||||
{:ok, %{status: status} = env} when 400 <= status and status < 500 ->
|
||||
{:error, env}
|
||||
|
||||
{:error, error} = error ->
|
||||
error
|
||||
|
||||
_ ->
|
||||
{:ok, "success"}
|
||||
end
|
||||
end
|
||||
end
|
41
lib/pleroma/web/media_proxy/invalidations/script.ex
Normal file
41
lib/pleroma/web/media_proxy/invalidations/script.ex
Normal file
|
@ -0,0 +1,41 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.MediaProxy.Invalidation.Script do
|
||||
@moduledoc false
|
||||
|
||||
@behaviour Pleroma.Web.MediaProxy.Invalidation
|
||||
|
||||
require Logger
|
||||
|
||||
@impl Pleroma.Web.MediaProxy.Invalidation
|
||||
def purge(urls, %{script_path: script_path} = _options) do
|
||||
args =
|
||||
urls
|
||||
|> List.wrap()
|
||||
|> Enum.uniq()
|
||||
|> Enum.join(" ")
|
||||
|
||||
path = Path.expand(script_path)
|
||||
|
||||
Logger.debug("Running cache purge: #{inspect(urls)}, #{path}")
|
||||
|
||||
case do_purge(path, [args]) do
|
||||
{result, exit_status} when exit_status > 0 ->
|
||||
Logger.error("Error while cache purge: #{inspect(result)}")
|
||||
{:error, inspect(result)}
|
||||
|
||||
_ ->
|
||||
{:ok, "success"}
|
||||
end
|
||||
end
|
||||
|
||||
def purge(_, _), do: {:error, "not found script path"}
|
||||
|
||||
defp do_purge(path, args) do
|
||||
System.cmd(path, args)
|
||||
rescue
|
||||
error -> {inspect(error), 1}
|
||||
end
|
||||
end
|
|
@ -1,8 +1,10 @@
|
|||
defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
|
||||
defmodule Pleroma.Web.PleromaAPI.EmojiPackController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
alias Pleroma.Emoji.Pack
|
||||
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||
|
||||
plug(
|
||||
Pleroma.Plugs.OAuthScopesPlug,
|
||||
%{scopes: ["write"], admin: true}
|
||||
|
@ -19,39 +21,37 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
|
|||
]
|
||||
)
|
||||
|
||||
plug(
|
||||
:skip_plug,
|
||||
[Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug]
|
||||
when action in [:archive, :show, :list]
|
||||
)
|
||||
@skip_plugs [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug]
|
||||
plug(:skip_plug, @skip_plugs when action in [:archive, :show, :list])
|
||||
|
||||
def remote(conn, %{"url" => url}) do
|
||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaEmojiPackOperation
|
||||
|
||||
def remote(conn, %{url: url}) do
|
||||
with {:ok, packs} <- Pack.list_remote(url) do
|
||||
json(conn, packs)
|
||||
else
|
||||
{:shareable, _} ->
|
||||
{:error, :not_shareable} ->
|
||||
conn
|
||||
|> put_status(:internal_server_error)
|
||||
|> json(%{error: "The requested instance does not support sharing emoji packs"})
|
||||
end
|
||||
end
|
||||
|
||||
def list(conn, _params) do
|
||||
def index(conn, _params) do
|
||||
emoji_path =
|
||||
Path.join(
|
||||
Pleroma.Config.get!([:instance, :static_dir]),
|
||||
"emoji"
|
||||
)
|
||||
[:instance, :static_dir]
|
||||
|> Pleroma.Config.get!()
|
||||
|> Path.join("emoji")
|
||||
|
||||
with {:ok, packs} <- Pack.list_local() do
|
||||
json(conn, packs)
|
||||
else
|
||||
{:create_dir, {:error, e}} ->
|
||||
{:error, :create_dir, e} ->
|
||||
conn
|
||||
|> put_status(:internal_server_error)
|
||||
|> json(%{error: "Failed to create the emoji pack directory at #{emoji_path}: #{e}"})
|
||||
|
||||
{:ls, {:error, e}} ->
|
||||
{:error, :ls, e} ->
|
||||
conn
|
||||
|> put_status(:internal_server_error)
|
||||
|> json(%{
|
||||
|
@ -60,13 +60,13 @@ def list(conn, _params) do
|
|||
end
|
||||
end
|
||||
|
||||
def show(conn, %{"name" => name}) do
|
||||
def show(conn, %{name: name}) do
|
||||
name = String.trim(name)
|
||||
|
||||
with {:ok, pack} <- Pack.show(name) do
|
||||
json(conn, pack)
|
||||
else
|
||||
{:loaded, _} ->
|
||||
{:error, :not_found} ->
|
||||
conn
|
||||
|> put_status(:not_found)
|
||||
|> json(%{error: "Pack #{name} does not exist"})
|
||||
|
@ -78,11 +78,11 @@ def show(conn, %{"name" => name}) do
|
|||
end
|
||||
end
|
||||
|
||||
def archive(conn, %{"name" => name}) do
|
||||
def archive(conn, %{name: name}) do
|
||||
with {:ok, archive} <- Pack.get_archive(name) do
|
||||
send_download(conn, {:binary, archive}, filename: "#{name}.zip")
|
||||
else
|
||||
{:can_download?, _} ->
|
||||
{:error, :cant_download} ->
|
||||
conn
|
||||
|> put_status(:forbidden)
|
||||
|> json(%{
|
||||
|
@ -90,23 +90,23 @@ def archive(conn, %{"name" => name}) do
|
|||
"Pack #{name} cannot be downloaded from this instance, either pack sharing was disabled for this pack or some files are missing"
|
||||
})
|
||||
|
||||
{:exists?, _} ->
|
||||
{:error, :not_found} ->
|
||||
conn
|
||||
|> put_status(:not_found)
|
||||
|> json(%{error: "Pack #{name} does not exist"})
|
||||
end
|
||||
end
|
||||
|
||||
def download(conn, %{"url" => url, "name" => name} = params) do
|
||||
with :ok <- Pack.download(name, url, params["as"]) do
|
||||
def download(%{body_params: %{url: url, name: name} = params} = conn, _) do
|
||||
with {:ok, _pack} <- Pack.download(name, url, params[:as]) do
|
||||
json(conn, "ok")
|
||||
else
|
||||
{:shareable, _} ->
|
||||
{:error, :not_shareable} ->
|
||||
conn
|
||||
|> put_status(:internal_server_error)
|
||||
|> json(%{error: "The requested instance does not support sharing emoji packs"})
|
||||
|
||||
{:checksum, _} ->
|
||||
{:error, :imvalid_checksum} ->
|
||||
conn
|
||||
|> put_status(:internal_server_error)
|
||||
|> json(%{error: "SHA256 for the pack doesn't match the one sent by the server"})
|
||||
|
@ -118,10 +118,10 @@ def download(conn, %{"url" => url, "name" => name} = params) do
|
|||
end
|
||||
end
|
||||
|
||||
def create(conn, %{"name" => name}) do
|
||||
def create(conn, %{name: name}) do
|
||||
name = String.trim(name)
|
||||
|
||||
with :ok <- Pack.create(name) do
|
||||
with {:ok, _pack} <- Pack.create(name) do
|
||||
json(conn, "ok")
|
||||
else
|
||||
{:error, :eexist} ->
|
||||
|
@ -143,7 +143,7 @@ def create(conn, %{"name" => name}) do
|
|||
end
|
||||
end
|
||||
|
||||
def delete(conn, %{"name" => name}) do
|
||||
def delete(conn, %{name: name}) do
|
||||
name = String.trim(name)
|
||||
|
||||
with {:ok, deleted} when deleted != [] <- Pack.delete(name) do
|
||||
|
@ -166,11 +166,11 @@ def delete(conn, %{"name" => name}) do
|
|||
end
|
||||
end
|
||||
|
||||
def update(conn, %{"name" => name, "metadata" => metadata}) do
|
||||
def update(%{body_params: %{metadata: metadata}} = conn, %{name: name}) do
|
||||
with {:ok, pack} <- Pack.update_metadata(name, metadata) do
|
||||
json(conn, pack.pack)
|
||||
else
|
||||
{:has_all_files?, _} ->
|
||||
{:error, :incomplete} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "The fallback archive does not have all files specified in pack.json"})
|
||||
|
@ -184,19 +184,19 @@ def update(conn, %{"name" => name, "metadata" => metadata}) do
|
|||
end
|
||||
end
|
||||
|
||||
def add_file(conn, %{"name" => name} = params) do
|
||||
filename = params["filename"] || get_filename(params["file"])
|
||||
shortcode = params["shortcode"] || Path.basename(filename, Path.extname(filename))
|
||||
def add_file(%{body_params: params} = conn, %{name: name}) do
|
||||
filename = params[:filename] || get_filename(params[:file])
|
||||
shortcode = params[:shortcode] || Path.basename(filename, Path.extname(filename))
|
||||
|
||||
with {:ok, pack} <- Pack.add_file(name, shortcode, filename, params["file"]) do
|
||||
with {:ok, pack} <- Pack.add_file(name, shortcode, filename, params[:file]) do
|
||||
json(conn, pack.files)
|
||||
else
|
||||
{:exists, _} ->
|
||||
{:error, :already_exists} ->
|
||||
conn
|
||||
|> put_status(:conflict)
|
||||
|> json(%{error: "An emoji with the \"#{shortcode}\" shortcode already exists"})
|
||||
|
||||
{:loaded, _} ->
|
||||
{:error, :not_found} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "pack \"#{name}\" is not found"})
|
||||
|
@ -215,20 +215,20 @@ def add_file(conn, %{"name" => name} = params) do
|
|||
end
|
||||
end
|
||||
|
||||
def update_file(conn, %{"name" => name, "shortcode" => shortcode} = params) do
|
||||
new_shortcode = params["new_shortcode"]
|
||||
new_filename = params["new_filename"]
|
||||
force = params["force"] == true
|
||||
def update_file(%{body_params: %{shortcode: shortcode} = params} = conn, %{name: name}) do
|
||||
new_shortcode = params[:new_shortcode]
|
||||
new_filename = params[:new_filename]
|
||||
force = params[:force]
|
||||
|
||||
with {:ok, pack} <- Pack.update_file(name, shortcode, new_shortcode, new_filename, force) do
|
||||
json(conn, pack.files)
|
||||
else
|
||||
{:exists, _} ->
|
||||
{:error, :doesnt_exist} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "Emoji \"#{shortcode}\" does not exist"})
|
||||
|
||||
{:not_used, _} ->
|
||||
{:error, :already_exists} ->
|
||||
conn
|
||||
|> put_status(:conflict)
|
||||
|> json(%{
|
||||
|
@ -236,7 +236,7 @@ def update_file(conn, %{"name" => name, "shortcode" => shortcode} = params) do
|
|||
"New shortcode \"#{new_shortcode}\" is already used. If you want to override emoji use 'force' option"
|
||||
})
|
||||
|
||||
{:loaded, _} ->
|
||||
{:error, :not_found} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "pack \"#{name}\" is not found"})
|
||||
|
@ -255,16 +255,16 @@ def update_file(conn, %{"name" => name, "shortcode" => shortcode} = params) do
|
|||
end
|
||||
end
|
||||
|
||||
def delete_file(conn, %{"name" => name, "shortcode" => shortcode}) do
|
||||
def delete_file(conn, %{name: name, shortcode: shortcode}) do
|
||||
with {:ok, pack} <- Pack.delete_file(name, shortcode) do
|
||||
json(conn, pack.files)
|
||||
else
|
||||
{:exists, _} ->
|
||||
{:error, :doesnt_exist} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "Emoji \"#{shortcode}\" does not exist"})
|
||||
|
||||
{:loaded, _} ->
|
||||
{:error, :not_found} ->
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "pack \"#{name}\" is not found"})
|
|
@ -9,16 +9,19 @@ defmodule Pleroma.Web.PleromaAPI.MascotController do
|
|||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||
plug(OAuthScopesPlug, %{scopes: ["read:accounts"]} when action == :show)
|
||||
plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action != :show)
|
||||
|
||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaMascotOperation
|
||||
|
||||
@doc "GET /api/v1/pleroma/mascot"
|
||||
def show(%{assigns: %{user: user}} = conn, _params) do
|
||||
json(conn, User.get_mascot(user))
|
||||
end
|
||||
|
||||
@doc "PUT /api/v1/pleroma/mascot"
|
||||
def update(%{assigns: %{user: user}} = conn, %{"file" => file}) do
|
||||
def update(%{assigns: %{user: user}, body_params: %{file: file}} = conn, _) do
|
||||
with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
|
||||
# Reject if not an image
|
||||
%{type: "image"} = attachment <- render_attachment(object) do
|
||||
|
|
|
@ -5,34 +5,27 @@
|
|||
defmodule Pleroma.Web.PleromaAPI.ScrobbleController do
|
||||
use Pleroma.Web, :controller
|
||||
|
||||
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, fetch_integer_param: 2]
|
||||
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
|
||||
|
||||
alias Pleroma.Plugs.OAuthScopesPlug
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
|
||||
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["read"], fallback: :proceed_unauthenticated} when action == :user_scrobbles
|
||||
%{scopes: ["read"], fallback: :proceed_unauthenticated} when action == :index
|
||||
)
|
||||
|
||||
plug(OAuthScopesPlug, %{scopes: ["write"]} when action != :user_scrobbles)
|
||||
plug(OAuthScopesPlug, %{scopes: ["write"]} when action == :create)
|
||||
|
||||
def new_scrobble(%{assigns: %{user: user}} = conn, %{"title" => _} = params) do
|
||||
params =
|
||||
if !params["length"] do
|
||||
params
|
||||
else
|
||||
params
|
||||
|> Map.put("length", fetch_integer_param(params, "length"))
|
||||
end
|
||||
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaScrobbleOperation
|
||||
|
||||
def create(%{assigns: %{user: user}, body_params: params} = conn, _) do
|
||||
with {:ok, activity} <- CommonAPI.listen(user, params) do
|
||||
conn
|
||||
|> put_view(StatusView)
|
||||
|> render("listen.json", %{activity: activity, for: user})
|
||||
render(conn, "show.json", activity: activity, for: user)
|
||||
else
|
||||
{:error, message} ->
|
||||
conn
|
||||
|
@ -41,16 +34,18 @@ def new_scrobble(%{assigns: %{user: user}} = conn, %{"title" => _} = params) do
|
|||
end
|
||||
end
|
||||
|
||||
def user_scrobbles(%{assigns: %{user: reading_user}} = conn, params) do
|
||||
with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
|
||||
params = Map.put(params, "type", ["Listen"])
|
||||
def index(%{assigns: %{user: reading_user}} = conn, %{id: id} = params) do
|
||||
with %User{} = user <- User.get_cached_by_nickname_or_id(id, for: reading_user) do
|
||||
params =
|
||||
params
|
||||
|> Map.new(fn {key, value} -> {to_string(key), value} end)
|
||||
|> Map.put("type", ["Listen"])
|
||||
|
||||
activities = ActivityPub.fetch_user_abstract_activities(user, reading_user, params)
|
||||
|
||||
conn
|
||||
|> add_link_headers(activities)
|
||||
|> put_view(StatusView)
|
||||
|> render("listens.json", %{
|
||||
|> render("index.json", %{
|
||||
activities: activities,
|
||||
for: reading_user,
|
||||
as: :activity
|
||||
|
|
37
lib/pleroma/web/pleroma_api/views/scrobble_view.ex
Normal file
37
lib/pleroma/web/pleroma_api/views/scrobble_view.ex
Normal file
|
@ -0,0 +1,37 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.PleromaAPI.ScrobbleView do
|
||||
use Pleroma.Web, :view
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.HTML
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
alias Pleroma.Web.MastodonAPI.AccountView
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
|
||||
def render("show.json", %{activity: %Activity{data: %{"type" => "Listen"}} = activity} = opts) do
|
||||
object = Object.normalize(activity)
|
||||
|
||||
user = StatusView.get_user(activity.data["actor"])
|
||||
created_at = Utils.to_masto_date(activity.data["published"])
|
||||
|
||||
%{
|
||||
id: activity.id,
|
||||
account: AccountView.render("show.json", %{user: user, for: opts[:for]}),
|
||||
created_at: created_at,
|
||||
title: object.data["title"] |> HTML.strip_tags(),
|
||||
artist: object.data["artist"] |> HTML.strip_tags(),
|
||||
album: object.data["album"] |> HTML.strip_tags(),
|
||||
length: object.data["length"]
|
||||
}
|
||||
end
|
||||
|
||||
def render("index.json", opts) do
|
||||
safe_render_many(opts.activities, __MODULE__, "show.json", opts)
|
||||
end
|
||||
end
|
|
@ -216,24 +216,25 @@ defmodule Pleroma.Web.Router do
|
|||
scope "/packs" do
|
||||
pipe_through(:admin_api)
|
||||
|
||||
get("/import", EmojiAPIController, :import_from_filesystem)
|
||||
get("/remote", EmojiAPIController, :remote)
|
||||
post("/download", EmojiAPIController, :download)
|
||||
get("/import", EmojiPackController, :import_from_filesystem)
|
||||
get("/remote", EmojiPackController, :remote)
|
||||
post("/download", EmojiPackController, :download)
|
||||
|
||||
post("/:name", EmojiAPIController, :create)
|
||||
patch("/:name", EmojiAPIController, :update)
|
||||
delete("/:name", EmojiAPIController, :delete)
|
||||
post("/:name", EmojiPackController, :create)
|
||||
patch("/:name", EmojiPackController, :update)
|
||||
delete("/:name", EmojiPackController, :delete)
|
||||
|
||||
post("/:name/files", EmojiAPIController, :add_file)
|
||||
patch("/:name/files", EmojiAPIController, :update_file)
|
||||
delete("/:name/files", EmojiAPIController, :delete_file)
|
||||
post("/:name/files", EmojiPackController, :add_file)
|
||||
patch("/:name/files", EmojiPackController, :update_file)
|
||||
delete("/:name/files", EmojiPackController, :delete_file)
|
||||
end
|
||||
|
||||
# Pack info / downloading
|
||||
scope "/packs" do
|
||||
get("/", EmojiAPIController, :list)
|
||||
get("/:name", EmojiAPIController, :show)
|
||||
get("/:name/archive", EmojiAPIController, :archive)
|
||||
pipe_through(:api)
|
||||
get("/", EmojiPackController, :index)
|
||||
get("/:name", EmojiPackController, :show)
|
||||
get("/:name/archive", EmojiPackController, :archive)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -337,7 +338,7 @@ defmodule Pleroma.Web.Router do
|
|||
get("/mascot", MascotController, :show)
|
||||
put("/mascot", MascotController, :update)
|
||||
|
||||
post("/scrobble", ScrobbleController, :new_scrobble)
|
||||
post("/scrobble", ScrobbleController, :create)
|
||||
end
|
||||
|
||||
scope [] do
|
||||
|
@ -357,7 +358,7 @@ defmodule Pleroma.Web.Router do
|
|||
|
||||
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
|
||||
pipe_through(:api)
|
||||
get("/accounts/:id/scrobbles", ScrobbleController, :user_scrobbles)
|
||||
get("/accounts/:id/scrobbles", ScrobbleController, :index)
|
||||
end
|
||||
|
||||
scope "/api/v1", Pleroma.Web.MastodonAPI do
|
||||
|
|
|
@ -27,8 +27,20 @@ def perform(
|
|||
|
||||
uploader = Pleroma.Config.get([Pleroma.Upload, :uploader])
|
||||
|
||||
prefix =
|
||||
case Pleroma.Config.get([Pleroma.Upload, :base_url]) do
|
||||
nil -> "media"
|
||||
_ -> ""
|
||||
end
|
||||
|
||||
base_url =
|
||||
String.trim_trailing(
|
||||
Pleroma.Config.get([Pleroma.Upload, :base_url], Pleroma.Web.base_url()),
|
||||
"/"
|
||||
)
|
||||
|
||||
# find all objects for copies of the attachments, name and actor doesn't matter here
|
||||
delete_ids =
|
||||
object_ids_and_hrefs =
|
||||
from(o in Object,
|
||||
where:
|
||||
fragment(
|
||||
|
@ -67,29 +79,28 @@ def perform(
|
|||
|> Enum.map(fn {href, %{id: id, count: count}} ->
|
||||
# only delete files that have single instance
|
||||
with 1 <- count do
|
||||
prefix =
|
||||
case Pleroma.Config.get([Pleroma.Upload, :base_url]) do
|
||||
nil -> "media"
|
||||
_ -> ""
|
||||
end
|
||||
href
|
||||
|> String.trim_leading("#{base_url}/#{prefix}")
|
||||
|> uploader.delete_file()
|
||||
|
||||
base_url =
|
||||
String.trim_trailing(
|
||||
Pleroma.Config.get([Pleroma.Upload, :base_url], Pleroma.Web.base_url()),
|
||||
"/"
|
||||
)
|
||||
|
||||
file_path = String.trim_leading(href, "#{base_url}/#{prefix}")
|
||||
|
||||
uploader.delete_file(file_path)
|
||||
{id, href}
|
||||
else
|
||||
_ -> {id, nil}
|
||||
end
|
||||
|
||||
id
|
||||
end)
|
||||
|
||||
from(o in Object, where: o.id in ^delete_ids)
|
||||
object_ids = Enum.map(object_ids_and_hrefs, fn {id, _} -> id end)
|
||||
|
||||
from(o in Object, where: o.id in ^object_ids)
|
||||
|> Repo.delete_all()
|
||||
|
||||
object_ids_and_hrefs
|
||||
|> Enum.filter(fn {_, href} -> not is_nil(href) end)
|
||||
|> Enum.map(&elem(&1, 1))
|
||||
|> Pleroma.Web.MediaProxy.Invalidation.purge()
|
||||
|
||||
{:ok, :success}
|
||||
end
|
||||
|
||||
def perform(%{"op" => "cleanup_attachments", "object" => _object}, _job), do: :ok
|
||||
def perform(%{"op" => "cleanup_attachments", "object" => _object}, _job), do: {:ok, :skip}
|
||||
end
|
||||
|
|
2
mix.exs
2
mix.exs
|
@ -155,7 +155,7 @@ defp deps do
|
|||
{:credo, "~> 1.1.0", only: [:dev, :test], runtime: false},
|
||||
{:mock, "~> 0.3.3", only: :test},
|
||||
{:crypt,
|
||||
git: "https://github.com/msantos/crypt", ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"},
|
||||
git: "https://github.com/msantos/crypt", ref: "f63a705f92c26955977ee62a313012e309a4d77a"},
|
||||
{:cors_plug, "~> 1.5"},
|
||||
{:ex_doc, "~> 0.21", only: :dev, runtime: false},
|
||||
{:web_push_encryption, "~> 0.2.1"},
|
||||
|
|
6
mix.lock
6
mix.lock
|
@ -21,13 +21,13 @@
|
|||
"cowlib": {:hex, :cowlib, "2.8.0", "fd0ff1787db84ac415b8211573e9a30a3ebe71b5cbff7f720089972b2319c8a4", [:rebar3], [], "hexpm", "79f954a7021b302186a950a32869dbc185523d99d3e44ce430cd1f3289f41ed4"},
|
||||
"credo": {:hex, :credo, "1.1.5", "caec7a3cadd2e58609d7ee25b3931b129e739e070539ad1a0cd7efeeb47014f4", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "d0bbd3222607ccaaac5c0340f7f525c627ae4d7aee6c8c8c108922620c5b6446"},
|
||||
"crontab": {:hex, :crontab, "1.1.8", "2ce0e74777dfcadb28a1debbea707e58b879e6aa0ffbf9c9bb540887bce43617", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
|
||||
"crypt": {:git, "https://github.com/msantos/crypt", "1f2b58927ab57e72910191a7ebaeff984382a1d3", [ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"]},
|
||||
"crypt": {:git, "https://github.com/msantos/crypt", "f63a705f92c26955977ee62a313012e309a4d77a", [ref: "f63a705f92c26955977ee62a313012e309a4d77a"]},
|
||||
"custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"},
|
||||
"db_connection": {:hex, :db_connection, "2.2.1", "caee17725495f5129cb7faebde001dc4406796f12a62b8949f4ac69315080566", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "2b02ece62d9f983fcd40954e443b7d9e6589664380e5546b2b9b523cd0fb59e1"},
|
||||
"decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"},
|
||||
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"},
|
||||
"earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"},
|
||||
"ecto": {:hex, :ecto, "3.4.0", "a7a83ab8359bf816ce729e5e65981ce25b9fc5adfc89c2ea3980f4fed0bfd7c1", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "5eed18252f5b5bbadec56a24112b531343507dbe046273133176b12190ce19cc"},
|
||||
"ecto": {:hex, :ecto, "3.4.4", "a2c881e80dc756d648197ae0d936216c0308370332c5e77a2325a10293eef845", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4bd3ad62abc3b21fb629f0f7a3dab23a192fca837d257dd08449fba7373561"},
|
||||
"ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.3.4", "aa18af12eb875fbcda2f75e608b3bd534ebf020fc4f6448e4672fcdcbb081244", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4 or ~> 3.3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5eccbdbf92e3c6f213007a82d5dbba4cd9bb659d1a21331f89f408e4c0efd7a8"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"},
|
||||
|
@ -57,7 +57,7 @@
|
|||
"httpoison": {:hex, :httpoison, "1.6.2", "ace7c8d3a361cebccbed19c283c349b3d26991eff73a1eaaa8abae2e3c8089b6", [:mix], [{:hackney, "~> 1.15 and >= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "aa2c74bd271af34239a3948779612f87df2422c2fdcfdbcec28d9c105f0773fe"},
|
||||
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"},
|
||||
"inet_cidr": {:hex, :inet_cidr, "1.0.4", "a05744ab7c221ca8e395c926c3919a821eb512e8f36547c062f62c4ca0cf3d6e", [:mix], [], "hexpm", "64a2d30189704ae41ca7dbdd587f5291db5d1dda1414e0774c29ffc81088c1bc"},
|
||||
"jason": {:hex, :jason, "1.2.0", "10043418c42d2493d0ee212d3fddd25d7ffe484380afad769a0a38795938e448", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "116747dbe057794c3a3e4e143b7c8390b29f634e16c78a7f59ba75bfa6852e7f"},
|
||||
"jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"},
|
||||
"joken": {:hex, :joken, "2.2.0", "2daa1b12be05184aff7b5ace1d43ca1f81345962285fff3f88db74927c954d3a", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "b4f92e30388206f869dd25d1af628a1d99d7586e5cf0672f64d4df84c4d2f5e9"},
|
||||
"jose": {:hex, :jose, "1.10.1", "16d8e460dae7203c6d1efa3f277e25b5af8b659febfc2f2eb4bacf87f128b80a", [:mix, :rebar3], [], "hexpm", "3c7ddc8a9394b92891db7c2771da94bf819834a1a4c92e30857b7d582e2f8257"},
|
||||
"jumper": {:hex, :jumper, "1.0.1", "3c00542ef1a83532b72269fab9f0f0c82bf23a35e27d278bfd9ed0865cecabff", [:mix], [], "hexpm", "318c59078ac220e966d27af3646026db9b5a5e6703cb2aa3e26bcfaba65b7433"},
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -4,103 +4,69 @@
|
|||
http://jedwatson.github.io/classnames
|
||||
*/
|
||||
|
||||
/*!
|
||||
* escape-html
|
||||
* Copyright(c) 2012-2013 TJ Holowaychuk
|
||||
* Copyright(c) 2015 Andreas Lubbe
|
||||
* Copyright(c) 2015 Tiancheng "Timothy" Gu
|
||||
* MIT Licensed
|
||||
*/
|
||||
|
||||
/*!
|
||||
* wavesurfer.js 3.3.1 (2020-01-14)
|
||||
* https://github.com/katspaugh/wavesurfer.js
|
||||
* @license BSD-3-Clause
|
||||
*/
|
||||
|
||||
/*!****************************************!*\
|
||||
!*** ./node_modules/debounce/index.js ***!
|
||||
\****************************************/
|
||||
|
||||
/*! no static exports found */
|
||||
|
||||
/*!***********************************!*\
|
||||
!*** ./src/drawer.canvasentry.js ***!
|
||||
\***********************************/
|
||||
|
||||
/*! ./util/style */
|
||||
|
||||
/*! ./util/get-id */
|
||||
|
||||
/*!***********************!*\
|
||||
!*** ./src/drawer.js ***!
|
||||
\***********************/
|
||||
|
||||
/*! ./util */
|
||||
|
||||
/*!***********************************!*\
|
||||
!*** ./src/drawer.multicanvas.js ***!
|
||||
\***********************************/
|
||||
/*! ./ajax */
|
||||
|
||||
/*! ./drawer */
|
||||
|
||||
/*! ./drawer.canvasentry */
|
||||
|
||||
/*!**************************************!*\
|
||||
!*** ./src/mediaelement-webaudio.js ***!
|
||||
\**************************************/
|
||||
/*! ./drawer.multicanvas */
|
||||
|
||||
/*! ./mediaelement */
|
||||
/*! ./extend */
|
||||
|
||||
/*!*****************************!*\
|
||||
!*** ./src/mediaelement.js ***!
|
||||
\*****************************/
|
||||
/*! ./fetch */
|
||||
|
||||
/*! ./webaudio */
|
||||
|
||||
/*!**************************!*\
|
||||
!*** ./src/peakcache.js ***!
|
||||
\**************************/
|
||||
|
||||
/*!**************************!*\
|
||||
!*** ./src/util/ajax.js ***!
|
||||
\**************************/
|
||||
|
||||
/*! ./observer */
|
||||
|
||||
/*!****************************!*\
|
||||
!*** ./src/util/extend.js ***!
|
||||
\****************************/
|
||||
|
||||
/*!***************************!*\
|
||||
!*** ./src/util/fetch.js ***!
|
||||
\***************************/
|
||||
|
||||
/*!***************************!*\
|
||||
!*** ./src/util/frame.js ***!
|
||||
\***************************/
|
||||
|
||||
/*! ./request-animation-frame */
|
||||
|
||||
/*!****************************!*\
|
||||
!*** ./src/util/get-id.js ***!
|
||||
\****************************/
|
||||
|
||||
/*!***************************!*\
|
||||
!*** ./src/util/index.js ***!
|
||||
\***************************/
|
||||
|
||||
/*! ./ajax */
|
||||
/*! ./frame */
|
||||
|
||||
/*! ./get-id */
|
||||
|
||||
/*! ./max */
|
||||
|
||||
/*! ./mediaelement */
|
||||
|
||||
/*! ./mediaelement-webaudio */
|
||||
|
||||
/*! ./min */
|
||||
|
||||
/*! ./extend */
|
||||
/*! ./observer */
|
||||
|
||||
/*! ./style */
|
||||
|
||||
/*! ./frame */
|
||||
|
||||
/*! debounce */
|
||||
/*! ./peakcache */
|
||||
|
||||
/*! ./prevent-click */
|
||||
|
||||
/*! ./fetch */
|
||||
/*! ./request-animation-frame */
|
||||
|
||||
/*! ./style */
|
||||
|
||||
/*! ./util */
|
||||
|
||||
/*! ./util/get-id */
|
||||
|
||||
/*! ./util/style */
|
||||
|
||||
/*! ./webaudio */
|
||||
|
||||
/*! debounce */
|
||||
|
||||
/*! no static exports found */
|
||||
|
||||
/*!***********************!*\
|
||||
!*** ./src/drawer.js ***!
|
||||
\***********************/
|
||||
|
||||
/*!*************************!*\
|
||||
!*** ./src/util/max.js ***!
|
||||
|
@ -110,17 +76,29 @@
|
|||
!*** ./src/util/min.js ***!
|
||||
\*************************/
|
||||
|
||||
/*!******************************!*\
|
||||
!*** ./src/util/observer.js ***!
|
||||
\******************************/
|
||||
/*!*************************!*\
|
||||
!*** ./src/webaudio.js ***!
|
||||
\*************************/
|
||||
|
||||
/*!***********************************!*\
|
||||
!*** ./src/util/prevent-click.js ***!
|
||||
\***********************************/
|
||||
/*!**************************!*\
|
||||
!*** ./src/peakcache.js ***!
|
||||
\**************************/
|
||||
|
||||
/*!*********************************************!*\
|
||||
!*** ./src/util/request-animation-frame.js ***!
|
||||
\*********************************************/
|
||||
/*!**************************!*\
|
||||
!*** ./src/util/ajax.js ***!
|
||||
\**************************/
|
||||
|
||||
/*!***************************!*\
|
||||
!*** ./src/util/fetch.js ***!
|
||||
\***************************/
|
||||
|
||||
/*!***************************!*\
|
||||
!*** ./src/util/frame.js ***!
|
||||
\***************************/
|
||||
|
||||
/*!***************************!*\
|
||||
!*** ./src/util/index.js ***!
|
||||
\***************************/
|
||||
|
||||
/*!***************************!*\
|
||||
!*** ./src/util/style.js ***!
|
||||
|
@ -130,20 +108,42 @@
|
|||
!*** ./src/wavesurfer.js ***!
|
||||
\***************************/
|
||||
|
||||
/*! ./drawer.multicanvas */
|
||||
/*!****************************!*\
|
||||
!*** ./src/util/extend.js ***!
|
||||
\****************************/
|
||||
|
||||
/*! ./peakcache */
|
||||
/*!****************************!*\
|
||||
!*** ./src/util/get-id.js ***!
|
||||
\****************************/
|
||||
|
||||
/*! ./mediaelement-webaudio */
|
||||
/*!*****************************!*\
|
||||
!*** ./src/mediaelement.js ***!
|
||||
\*****************************/
|
||||
|
||||
/*!*************************!*\
|
||||
!*** ./src/webaudio.js ***!
|
||||
\*************************/
|
||||
/*!******************************!*\
|
||||
!*** ./src/util/observer.js ***!
|
||||
\******************************/
|
||||
|
||||
/*!
|
||||
* escape-html
|
||||
* Copyright(c) 2012-2013 TJ Holowaychuk
|
||||
* Copyright(c) 2015 Andreas Lubbe
|
||||
* Copyright(c) 2015 Tiancheng "Timothy" Gu
|
||||
* MIT Licensed
|
||||
*/
|
||||
/*!***********************************!*\
|
||||
!*** ./src/drawer.canvasentry.js ***!
|
||||
\***********************************/
|
||||
|
||||
/*!***********************************!*\
|
||||
!*** ./src/drawer.multicanvas.js ***!
|
||||
\***********************************/
|
||||
|
||||
/*!***********************************!*\
|
||||
!*** ./src/util/prevent-click.js ***!
|
||||
\***********************************/
|
||||
|
||||
/*!**************************************!*\
|
||||
!*** ./src/mediaelement-webaudio.js ***!
|
||||
\**************************************/
|
||||
|
||||
/*!****************************************!*\
|
||||
!*** ./node_modules/debounce/index.js ***!
|
||||
\****************************************/
|
||||
|
||||
/*!*********************************************!*\
|
||||
!*** ./src/util/request-animation-frame.js ***!
|
||||
\*********************************************/
|
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.
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.
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.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue