commit
e7836adf21
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,59 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Emoji.Formatter do
|
||||||
|
alias Pleroma.Emoji
|
||||||
|
alias Pleroma.HTML
|
||||||
|
alias Pleroma.Web.MediaProxy
|
||||||
|
|
||||||
|
def emojify(text) do
|
||||||
|
emojify(text, Emoji.get_all())
|
||||||
|
end
|
||||||
|
|
||||||
|
def emojify(text, nil), do: text
|
||||||
|
|
||||||
|
def emojify(text, emoji, strip \\ false) do
|
||||||
|
Enum.reduce(emoji, text, fn
|
||||||
|
{_, %Emoji{safe_code: emoji, safe_file: file}}, text ->
|
||||||
|
String.replace(text, ":#{emoji}:", prepare_emoji_html(emoji, file, strip))
|
||||||
|
|
||||||
|
{unsafe_emoji, unsafe_file}, text ->
|
||||||
|
emoji = HTML.strip_tags(unsafe_emoji)
|
||||||
|
file = HTML.strip_tags(unsafe_file)
|
||||||
|
String.replace(text, ":#{emoji}:", prepare_emoji_html(emoji, file, strip))
|
||||||
|
end)
|
||||||
|
|> HTML.filter_tags()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp prepare_emoji_html(_emoji, _file, true), do: ""
|
||||||
|
|
||||||
|
defp prepare_emoji_html(emoji, file, _strip) do
|
||||||
|
"<img class='emoji' alt='#{emoji}' title='#{emoji}' src='#{MediaProxy.url(file)}' />"
|
||||||
|
end
|
||||||
|
|
||||||
|
def demojify(text) do
|
||||||
|
emojify(text, Emoji.get_all(), true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def demojify(text, nil), do: text
|
||||||
|
|
||||||
|
@doc "Outputs a list of the emoji-shortcodes in a text"
|
||||||
|
def get_emoji(text) when is_binary(text) do
|
||||||
|
Enum.filter(Emoji.get_all(), fn {emoji, %Emoji{}} ->
|
||||||
|
String.contains?(text, ":#{emoji}:")
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_emoji(_), do: []
|
||||||
|
|
||||||
|
@doc "Outputs a list of the emoji-Maps in a text"
|
||||||
|
def get_emoji_map(text) when is_binary(text) do
|
||||||
|
get_emoji(text)
|
||||||
|
|> Enum.reduce(%{}, fn {name, %Emoji{file: file}}, acc ->
|
||||||
|
Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url()}#{file}")
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_emoji_map(_), do: []
|
||||||
|
end
|
@ -0,0 +1,224 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Emoji.Loader do
|
||||||
|
@moduledoc """
|
||||||
|
The Loader emoji from:
|
||||||
|
|
||||||
|
* emoji packs in INSTANCE-DIR/emoji
|
||||||
|
* the files: `config/emoji.txt` and `config/custom_emoji.txt`
|
||||||
|
* glob paths, nested folder is used as tag name for grouping e.g. priv/static/emoji/custom/nested_folder
|
||||||
|
"""
|
||||||
|
alias Pleroma.Config
|
||||||
|
alias Pleroma.Emoji
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@type pattern :: Regex.t() | module() | String.t()
|
||||||
|
@type patterns :: pattern() | [pattern()]
|
||||||
|
@type group_patterns :: keyword(patterns())
|
||||||
|
@type emoji :: {String.t(), Emoji.t()}
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Loads emojis from files/packs.
|
||||||
|
|
||||||
|
returns list emojis in format:
|
||||||
|
`{"000", "/emoji/freespeechextremist.com/000.png", ["Custom"]}`
|
||||||
|
"""
|
||||||
|
@spec load() :: list(emoji)
|
||||||
|
def load do
|
||||||
|
emoji_dir_path = Path.join(Config.get!([:instance, :static_dir]), "emoji")
|
||||||
|
|
||||||
|
emoji_groups = Config.get([:emoji, :groups])
|
||||||
|
|
||||||
|
emojis =
|
||||||
|
case File.ls(emoji_dir_path) do
|
||||||
|
{:error, :enoent} ->
|
||||||
|
# The custom emoji directory doesn't exist,
|
||||||
|
# don't do anything
|
||||||
|
[]
|
||||||
|
|
||||||
|
{:error, e} ->
|
||||||
|
# There was some other error
|
||||||
|
Logger.error("Could not access the custom emoji directory #{emoji_dir_path}: #{e}")
|
||||||
|
[]
|
||||||
|
|
||||||
|
{:ok, results} ->
|
||||||
|
grouped =
|
||||||
|
Enum.group_by(results, fn file ->
|
||||||
|
File.dir?(Path.join(emoji_dir_path, file))
|
||||||
|
end)
|
||||||
|
|
||||||
|
packs = grouped[true] || []
|
||||||
|
files = grouped[false] || []
|
||||||
|
|
||||||
|
# Print the packs we've found
|
||||||
|
Logger.info("Found emoji packs: #{Enum.join(packs, ", ")}")
|
||||||
|
|
||||||
|
if not Enum.empty?(files) do
|
||||||
|
Logger.warn(
|
||||||
|
"Found files in the emoji folder. These will be ignored, please move them to a subdirectory\nFound files: #{
|
||||||
|
Enum.join(files, ", ")
|
||||||
|
}"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
emojis =
|
||||||
|
Enum.flat_map(packs, fn pack ->
|
||||||
|
load_pack(Path.join(emoji_dir_path, pack), emoji_groups)
|
||||||
|
end)
|
||||||
|
|
||||||
|
Emoji.clear_all()
|
||||||
|
emojis
|
||||||
|
end
|
||||||
|
|
||||||
|
# Compat thing for old custom emoji handling & default emoji,
|
||||||
|
# it should run even if there are no emoji packs
|
||||||
|
shortcode_globs = Config.get([:emoji, :shortcode_globs], [])
|
||||||
|
|
||||||
|
emojis_txt =
|
||||||
|
(load_from_file("config/emoji.txt", emoji_groups) ++
|
||||||
|
load_from_file("config/custom_emoji.txt", emoji_groups) ++
|
||||||
|
load_from_globs(shortcode_globs, emoji_groups))
|
||||||
|
|> Enum.reject(fn value -> value == nil end)
|
||||||
|
|
||||||
|
Enum.map(emojis ++ emojis_txt, &prepare_emoji/1)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp prepare_emoji({code, _, _} = emoji), do: {code, Emoji.build(emoji)}
|
||||||
|
|
||||||
|
defp load_pack(pack_dir, emoji_groups) do
|
||||||
|
pack_name = Path.basename(pack_dir)
|
||||||
|
|
||||||
|
pack_file = Path.join(pack_dir, "pack.json")
|
||||||
|
|
||||||
|
if File.exists?(pack_file) do
|
||||||
|
contents = Jason.decode!(File.read!(pack_file))
|
||||||
|
|
||||||
|
contents["files"]
|
||||||
|
|> Enum.map(fn {name, rel_file} ->
|
||||||
|
filename = Path.join("/emoji/#{pack_name}", rel_file)
|
||||||
|
{name, filename, ["pack:#{pack_name}"]}
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
# Load from emoji.txt / all files
|
||||||
|
emoji_txt = Path.join(pack_dir, "emoji.txt")
|
||||||
|
|
||||||
|
if File.exists?(emoji_txt) do
|
||||||
|
load_from_file(emoji_txt, emoji_groups)
|
||||||
|
else
|
||||||
|
extensions = Pleroma.Config.get([:emoji, :pack_extensions])
|
||||||
|
|
||||||
|
Logger.info(
|
||||||
|
"No emoji.txt found for pack \"#{pack_name}\", assuming all #{
|
||||||
|
Enum.join(extensions, ", ")
|
||||||
|
} files are emoji"
|
||||||
|
)
|
||||||
|
|
||||||
|
make_shortcode_to_file_map(pack_dir, extensions)
|
||||||
|
|> Enum.map(fn {shortcode, rel_file} ->
|
||||||
|
filename = Path.join("/emoji/#{pack_name}", rel_file)
|
||||||
|
|
||||||
|
{shortcode, filename, [to_string(match_extra(emoji_groups, filename))]}
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def make_shortcode_to_file_map(pack_dir, exts) do
|
||||||
|
find_all_emoji(pack_dir, exts)
|
||||||
|
|> Enum.map(&Path.relative_to(&1, pack_dir))
|
||||||
|
|> Enum.map(fn f -> {f |> Path.basename() |> Path.rootname(), f} end)
|
||||||
|
|> Enum.into(%{})
|
||||||
|
end
|
||||||
|
|
||||||
|
def find_all_emoji(dir, exts) do
|
||||||
|
dir
|
||||||
|
|> File.ls!()
|
||||||
|
|> Enum.flat_map(fn f ->
|
||||||
|
filepath = Path.join(dir, f)
|
||||||
|
|
||||||
|
if File.dir?(filepath) do
|
||||||
|
find_all_emoji(filepath, exts)
|
||||||
|
else
|
||||||
|
[filepath]
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|> Enum.filter(fn f -> Path.extname(f) in exts end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp load_from_file(file, emoji_groups) do
|
||||||
|
if File.exists?(file) do
|
||||||
|
load_from_file_stream(File.stream!(file), emoji_groups)
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp load_from_file_stream(stream, emoji_groups) do
|
||||||
|
stream
|
||||||
|
|> Stream.map(&String.trim/1)
|
||||||
|
|> Stream.map(fn line ->
|
||||||
|
case String.split(line, ~r/,\s*/) do
|
||||||
|
[name, file] ->
|
||||||
|
{name, file, [to_string(match_extra(emoji_groups, file))]}
|
||||||
|
|
||||||
|
[name, file | tags] ->
|
||||||
|
{name, file, tags}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|> Enum.to_list()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp load_from_globs(globs, emoji_groups) do
|
||||||
|
static_path = Path.join(:code.priv_dir(:pleroma), "static")
|
||||||
|
|
||||||
|
paths =
|
||||||
|
Enum.map(globs, fn glob ->
|
||||||
|
Path.join(static_path, glob)
|
||||||
|
|> Path.wildcard()
|
||||||
|
end)
|
||||||
|
|> Enum.concat()
|
||||||
|
|
||||||
|
Enum.map(paths, fn path ->
|
||||||
|
tag = match_extra(emoji_groups, Path.join("/", Path.relative_to(path, static_path)))
|
||||||
|
shortcode = Path.basename(path, Path.extname(path))
|
||||||
|
external_path = Path.join("/", Path.relative_to(path, static_path))
|
||||||
|
{shortcode, external_path, [to_string(tag)]}
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Finds a matching group for the given emoji filename
|
||||||
|
"""
|
||||||
|
@spec match_extra(group_patterns(), String.t()) :: atom() | nil
|
||||||
|
def match_extra(group_patterns, filename) do
|
||||||
|
match_group_patterns(group_patterns, fn pattern ->
|
||||||
|
case pattern do
|
||||||
|
%Regex{} = regex -> Regex.match?(regex, filename)
|
||||||
|
string when is_binary(string) -> filename == string
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp match_group_patterns(group_patterns, matcher) do
|
||||||
|
Enum.find_value(group_patterns, fn {group, patterns} ->
|
||||||
|
patterns =
|
||||||
|
patterns
|
||||||
|
|> List.wrap()
|
||||||
|
|> Enum.map(fn pattern ->
|
||||||
|
if String.contains?(pattern, "*") do
|
||||||
|
~r(#{String.replace(pattern, "*", ".*")})
|
||||||
|
else
|
||||||
|
pattern
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
Enum.any?(patterns, matcher) && group
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
@ -1,182 +0,0 @@
|
|||||||
# Pleroma: A lightweight social networking server
|
|
||||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
|
||||||
# SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
|
|
||||||
defmodule Pleroma.FlakeId do
|
|
||||||
@moduledoc """
|
|
||||||
Flake is a decentralized, k-ordered id generation service.
|
|
||||||
|
|
||||||
Adapted from:
|
|
||||||
|
|
||||||
* [flaky](https://github.com/nirvana/flaky), released under the terms of the Truly Free License,
|
|
||||||
* [Flake](https://github.com/boundary/flake), Copyright 2012, Boundary, Apache License, Version 2.0
|
|
||||||
"""
|
|
||||||
|
|
||||||
@type t :: binary
|
|
||||||
|
|
||||||
@behaviour Ecto.Type
|
|
||||||
use GenServer
|
|
||||||
require Logger
|
|
||||||
alias __MODULE__
|
|
||||||
import Kernel, except: [to_string: 1]
|
|
||||||
|
|
||||||
defstruct node: nil, time: 0, sq: 0
|
|
||||||
|
|
||||||
@doc "Converts a binary Flake to a String"
|
|
||||||
def to_string(<<0::integer-size(64), id::integer-size(64)>>) do
|
|
||||||
Kernel.to_string(id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_string(<<_::integer-size(64), _::integer-size(48), _::integer-size(16)>> = flake) do
|
|
||||||
encode_base62(flake)
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_string(s), do: s
|
|
||||||
|
|
||||||
def from_string(int) when is_integer(int) do
|
|
||||||
from_string(Kernel.to_string(int))
|
|
||||||
end
|
|
||||||
|
|
||||||
for i <- [-1, 0] do
|
|
||||||
def from_string(unquote(i)), do: <<0::integer-size(128)>>
|
|
||||||
def from_string(unquote(Kernel.to_string(i))), do: <<0::integer-size(128)>>
|
|
||||||
end
|
|
||||||
|
|
||||||
def from_string(<<_::integer-size(128)>> = flake), do: flake
|
|
||||||
|
|
||||||
def from_string(string) when is_binary(string) and byte_size(string) < 18 do
|
|
||||||
case Integer.parse(string) do
|
|
||||||
{id, ""} -> <<0::integer-size(64), id::integer-size(64)>>
|
|
||||||
_ -> nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def from_string(string) do
|
|
||||||
string |> decode_base62 |> from_integer
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_integer(<<integer::integer-size(128)>>), do: integer
|
|
||||||
|
|
||||||
def from_integer(integer) do
|
|
||||||
<<_time::integer-size(64), _node::integer-size(48), _seq::integer-size(16)>> =
|
|
||||||
<<integer::integer-size(128)>>
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc "Generates a Flake"
|
|
||||||
@spec get :: binary
|
|
||||||
def get, do: to_string(:gen_server.call(:flake, :get))
|
|
||||||
|
|
||||||
# checks that ID is is valid FlakeID
|
|
||||||
#
|
|
||||||
@spec is_flake_id?(String.t()) :: boolean
|
|
||||||
def is_flake_id?(id), do: is_flake_id?(String.to_charlist(id), true)
|
|
||||||
defp is_flake_id?([c | cs], true) when c >= ?0 and c <= ?9, do: is_flake_id?(cs, true)
|
|
||||||
defp is_flake_id?([c | cs], true) when c >= ?A and c <= ?Z, do: is_flake_id?(cs, true)
|
|
||||||
defp is_flake_id?([c | cs], true) when c >= ?a and c <= ?z, do: is_flake_id?(cs, true)
|
|
||||||
defp is_flake_id?([], true), do: true
|
|
||||||
defp is_flake_id?(_, _), do: false
|
|
||||||
|
|
||||||
# -- Ecto.Type API
|
|
||||||
@impl Ecto.Type
|
|
||||||
def type, do: :uuid
|
|
||||||
|
|
||||||
@impl Ecto.Type
|
|
||||||
def cast(value) do
|
|
||||||
{:ok, FlakeId.to_string(value)}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl Ecto.Type
|
|
||||||
def load(value) do
|
|
||||||
{:ok, FlakeId.to_string(value)}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl Ecto.Type
|
|
||||||
def dump(value) do
|
|
||||||
{:ok, FlakeId.from_string(value)}
|
|
||||||
end
|
|
||||||
|
|
||||||
def autogenerate, do: get()
|
|
||||||
|
|
||||||
# -- GenServer API
|
|
||||||
def start_link(_) do
|
|
||||||
:gen_server.start_link({:local, :flake}, __MODULE__, [], [])
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl GenServer
|
|
||||||
def init([]) do
|
|
||||||
{:ok, %FlakeId{node: worker_id(), time: time()}}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl GenServer
|
|
||||||
def handle_call(:get, _from, state) do
|
|
||||||
{flake, new_state} = get(time(), state)
|
|
||||||
{:reply, flake, new_state}
|
|
||||||
end
|
|
||||||
|
|
||||||
# Matches when the calling time is the same as the state time. Incr. sq
|
|
||||||
defp get(time, %FlakeId{time: time, node: node, sq: seq}) do
|
|
||||||
new_state = %FlakeId{time: time, node: node, sq: seq + 1}
|
|
||||||
{gen_flake(new_state), new_state}
|
|
||||||
end
|
|
||||||
|
|
||||||
# Matches when the times are different, reset sq
|
|
||||||
defp get(newtime, %FlakeId{time: time, node: node}) when newtime > time do
|
|
||||||
new_state = %FlakeId{time: newtime, node: node, sq: 0}
|
|
||||||
{gen_flake(new_state), new_state}
|
|
||||||
end
|
|
||||||
|
|
||||||
# Error when clock is running backwards
|
|
||||||
defp get(newtime, %FlakeId{time: time}) when newtime < time do
|
|
||||||
{:error, :clock_running_backwards}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp gen_flake(%FlakeId{time: time, node: node, sq: seq}) do
|
|
||||||
<<time::integer-size(64), node::integer-size(48), seq::integer-size(16)>>
|
|
||||||
end
|
|
||||||
|
|
||||||
defp nthchar_base62(n) when n <= 9, do: ?0 + n
|
|
||||||
defp nthchar_base62(n) when n <= 35, do: ?A + n - 10
|
|
||||||
defp nthchar_base62(n), do: ?a + n - 36
|
|
||||||
|
|
||||||
defp encode_base62(<<integer::integer-size(128)>>) do
|
|
||||||
integer
|
|
||||||
|> encode_base62([])
|
|
||||||
|> List.to_string()
|
|
||||||
end
|
|
||||||
|
|
||||||
defp encode_base62(int, acc) when int < 0, do: encode_base62(-int, acc)
|
|
||||||
defp encode_base62(int, []) when int == 0, do: '0'
|
|
||||||
defp encode_base62(int, acc) when int == 0, do: acc
|
|
||||||
|
|
||||||
defp encode_base62(int, acc) do
|
|
||||||
r = rem(int, 62)
|
|
||||||
id = div(int, 62)
|
|
||||||
acc = [nthchar_base62(r) | acc]
|
|
||||||
encode_base62(id, acc)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp decode_base62(s) do
|
|
||||||
decode_base62(String.to_charlist(s), 0)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp decode_base62([c | cs], acc) when c >= ?0 and c <= ?9,
|
|
||||||
do: decode_base62(cs, 62 * acc + (c - ?0))
|
|
||||||
|
|
||||||
defp decode_base62([c | cs], acc) when c >= ?A and c <= ?Z,
|
|
||||||
do: decode_base62(cs, 62 * acc + (c - ?A + 10))
|
|
||||||
|
|
||||||
defp decode_base62([c | cs], acc) when c >= ?a and c <= ?z,
|
|
||||||
do: decode_base62(cs, 62 * acc + (c - ?a + 36))
|
|
||||||
|
|
||||||
defp decode_base62([], acc), do: acc
|
|
||||||
|
|
||||||
defp time do
|
|
||||||
{mega_seconds, seconds, micro_seconds} = :erlang.timestamp()
|
|
||||||
1_000_000_000 * mega_seconds + seconds * 1000 + :erlang.trunc(micro_seconds / 1000)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp worker_id do
|
|
||||||
<<worker::integer-size(48)>> = :crypto.strong_rand_bytes(6)
|
|
||||||
worker
|
|
||||||
end
|
|
||||||
end
|
|
@ -0,0 +1,22 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.AdminAPI.Report do
|
||||||
|
alias Pleroma.Activity
|
||||||
|
alias Pleroma.User
|
||||||
|
|
||||||
|
def extract_report_info(
|
||||||
|
%{data: %{"actor" => actor, "object" => [account_ap_id | status_ap_ids]}} = report
|
||||||
|
) do
|
||||||
|
user = User.get_cached_by_ap_id(actor)
|
||||||
|
account = User.get_cached_by_ap_id(account_ap_id)
|
||||||
|
|
||||||
|
statuses =
|
||||||
|
Enum.map(status_ap_ids, fn ap_id ->
|
||||||
|
Activity.get_by_ap_id_with_object(ap_id)
|
||||||
|
end)
|
||||||
|
|
||||||
|
%{report: report, user: user, account: account, statuses: statuses}
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,57 @@
|
|||||||
|
# Pleroma: A lightweight social networking server
|
||||||
|
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
defmodule Pleroma.Web.MastodonAPI.NotificationController do
|
||||||
|
use Pleroma.Web, :controller
|
||||||
|
|
||||||
|
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
|
||||||
|
|
||||||
|
alias Pleroma.Notification
|
||||||
|
alias Pleroma.Web.MastodonAPI.MastodonAPI
|
||||||
|
|
||||||
|
# GET /api/v1/notifications
|
||||||
|
def index(%{assigns: %{user: user}} = conn, params) do
|
||||||
|
notifications = MastodonAPI.get_notifications(user, params)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> add_link_headers(notifications)
|
||||||
|
|> render("index.json", notifications: notifications, for: user)
|
||||||
|
end
|
||||||
|
|
||||||
|
# GET /api/v1/notifications/:id
|
||||||
|
def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
|
||||||
|
with {:ok, notification} <- Notification.get(user, id) do
|
||||||
|
render(conn, "show.json", notification: notification, for: user)
|
||||||
|
else
|
||||||
|
{:error, reason} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:forbidden)
|
||||||
|
|> json(%{"error" => reason})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# POST /api/v1/notifications/clear
|
||||||
|
def clear(%{assigns: %{user: user}} = conn, _params) do
|
||||||
|
Notification.clear(user)
|
||||||
|
json(conn, %{})
|
||||||
|
end
|
||||||
|
|
||||||
|
# POST /api/v1/notifications/dismiss
|
||||||
|
def dismiss(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
|
||||||
|
with {:ok, _notif} <- Notification.dismiss(user, id) do
|
||||||
|
json(conn, %{})
|
||||||
|
else
|
||||||
|
{:error, reason} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:forbidden)
|
||||||
|
|> json(%{"error" => reason})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# DELETE /api/v1/notifications/destroy_multiple
|
||||||
|
def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
|
||||||
|
Notification.destroy_multiple(user, ids)
|
||||||
|
json(conn, %{})
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,617 @@
|
|||||||
|
defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
|
||||||
|
use Pleroma.Web, :controller
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
def emoji_dir_path do
|
||||||
|
Path.join(
|
||||||
|
Pleroma.Config.get!([:instance, :static_dir]),
|
||||||
|
"emoji"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Lists packs from the remote instance.
|
||||||
|
|
||||||
|
Since JS cannot ask remote instances for their packs due to CPS, it has to
|
||||||
|
be done by the server
|
||||||
|
"""
|
||||||
|
def list_from(conn, %{"instance_address" => address}) do
|
||||||
|
address = String.trim(address)
|
||||||
|
|
||||||
|
if shareable_packs_available(address) do
|
||||||
|
list_resp =
|
||||||
|
"#{address}/api/pleroma/emoji/packs" |> Tesla.get!() |> Map.get(:body) |> Jason.decode!()
|
||||||
|
|
||||||
|
json(conn, list_resp)
|
||||||
|
else
|
||||||
|
conn
|
||||||
|
|> put_status(:internal_server_error)
|
||||||
|
|> json(%{error: "The requested instance does not support sharing emoji packs"})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Lists the packs available on the instance as JSON.
|
||||||
|
|
||||||
|
The information is public and does not require authentification. The format is
|
||||||
|
a map of "pack directory name" to pack.json contents.
|
||||||
|
"""
|
||||||
|
def list_packs(conn, _params) do
|
||||||
|
# Create the directory first if it does not exist. This is probably the first request made
|
||||||
|
# with the API so it should be sufficient
|
||||||
|
with {:create_dir, :ok} <- {:create_dir, File.mkdir_p(emoji_dir_path())},
|
||||||
|
{:ls, {:ok, results}} <- {:ls, File.ls(emoji_dir_path())} do
|
||||||
|
pack_infos =
|
||||||
|
results
|
||||||
|
|> Enum.filter(&has_pack_json?/1)
|
||||||
|
|> Enum.map(&load_pack/1)
|
||||||
|
# Check if all the files are in place and can be sent
|
||||||
|
|> Enum.map(&validate_pack/1)
|
||||||
|
# Transform into a map of pack-name => pack-data
|
||||||
|
|> Enum.into(%{})
|
||||||
|
|
||||||
|
json(conn, pack_infos)
|
||||||
|
else
|
||||||
|
{:create_dir, {:error, e}} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:internal_server_error)
|
||||||
|
|> json(%{error: "Failed to create the emoji pack directory at #{emoji_dir_path()}: #{e}"})
|
||||||
|
|
||||||
|
{:ls, {:error, e}} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:internal_server_error)
|
||||||
|
|> json(%{
|
||||||
|
error:
|
||||||
|
"Failed to get the contents of the emoji pack directory at #{emoji_dir_path()}: #{e}"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp has_pack_json?(file) do
|
||||||
|
dir_path = Path.join(emoji_dir_path(), file)
|
||||||
|
# Filter to only use the pack.json packs
|
||||||
|
File.dir?(dir_path) and File.exists?(Path.join(dir_path, "pack.json"))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp load_pack(pack_name) do
|
||||||
|
pack_path = Path.join(emoji_dir_path(), pack_name)
|
||||||
|
pack_file = Path.join(pack_path, "pack.json")
|
||||||
|
|
||||||
|
{pack_name, Jason.decode!(File.read!(pack_file))}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp validate_pack({name, pack}) do
|
||||||
|
pack_path = Path.join(emoji_dir_path(), name)
|
||||||
|
|
||||||
|
if can_download?(pack, pack_path) do
|
||||||
|
archive_for_sha = make_archive(name, pack, pack_path)
|
||||||
|
archive_sha = :crypto.hash(:sha256, archive_for_sha) |> Base.encode16()
|
||||||
|
|
||||||
|
pack =
|
||||||
|
pack
|
||||||
|
|> put_in(["pack", "can-download"], true)
|
||||||
|
|> put_in(["pack", "download-sha256"], archive_sha)
|
||||||
|
|
||||||
|
{name, pack}
|
||||||
|
else
|
||||||
|
{name, put_in(pack, ["pack", "can-download"], false)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp can_download?(pack, pack_path) do
|
||||||
|
# If the pack is set as shared, check if it can be downloaded
|
||||||
|
# That means that when asked, the pack can be packed and sent to the remote
|
||||||
|
# Otherwise, they'd have to download it from external-src
|
||||||
|
pack["pack"]["share-files"] &&
|
||||||
|
Enum.all?(pack["files"], fn {_, path} ->
|
||||||
|
File.exists?(Path.join(pack_path, path))
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_archive_and_cache(name, pack, pack_dir, md5) do
|
||||||
|
files =
|
||||||
|
['pack.json'] ++
|
||||||
|
(pack["files"] |> Enum.map(fn {_, path} -> to_charlist(path) end))
|
||||||
|
|
||||||
|
{:ok, {_, zip_result}} = :zip.zip('#{name}.zip', files, [:memory, cwd: to_charlist(pack_dir)])
|
||||||
|
|
||||||
|
cache_seconds_per_file = Pleroma.Config.get!([:emoji, :shared_pack_cache_seconds_per_file])
|
||||||
|
cache_ms = :timer.seconds(cache_seconds_per_file * Enum.count(files))
|
||||||
|
|
||||||
|
Cachex.put!(
|
||||||
|
:emoji_packs_cache,
|
||||||
|
name,
|
||||||
|
# if pack.json MD5 changes, the cache is not valid anymore
|
||||||
|
%{pack_json_md5: md5, pack_data: zip_result},
|
||||||
|
# Add a minute to cache time for every file in the pack
|
||||||
|
ttl: cache_ms
|
||||||
|
)
|
||||||
|
|
||||||
|
Logger.debug("Created an archive for the '#{name}' emoji pack, \
|
||||||
|
keeping it in cache for #{div(cache_ms, 1000)}s")
|
||||||
|
|
||||||
|
zip_result
|
||||||
|
end
|
||||||
|
|
||||||
|
defp make_archive(name, pack, pack_dir) do
|
||||||
|
# Having a different pack.json md5 invalidates cache
|
||||||
|
pack_file_md5 = :crypto.hash(:md5, File.read!(Path.join(pack_dir, "pack.json")))
|
||||||
|
|
||||||
|
case Cachex.get!(:emoji_packs_cache, name) do
|
||||||
|
%{pack_file_md5: ^pack_file_md5, pack_data: zip_result} ->
|
||||||
|
Logger.debug("Using cache for the '#{name}' shared emoji pack")
|
||||||
|
zip_result
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
create_archive_and_cache(name, pack, pack_dir, pack_file_md5)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
An endpoint for other instances (via admin UI) or users (via browser)
|
||||||
|
to download packs that the instance shares.
|
||||||
|
"""
|
||||||
|
def download_shared(conn, %{"name" => name}) do
|
||||||
|
pack_dir = Path.join(emoji_dir_path(), name)
|
||||||
|
pack_file = Path.join(pack_dir, "pack.json")
|
||||||
|
|
||||||
|
with {_, true} <- {:exists?, File.exists?(pack_file)},
|
||||||
|
pack = Jason.decode!(File.read!(pack_file)),
|
||||||
|
{_, true} <- {:can_download?, can_download?(pack, pack_dir)} do
|
||||||
|
zip_result = make_archive(name, pack, pack_dir)
|
||||||
|
send_download(conn, {:binary, zip_result}, filename: "#{name}.zip")
|
||||||
|
else
|
||||||
|
{:can_download?, _} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:forbidden)
|
||||||
|
|> json(%{
|
||||||
|
error: "Pack #{name} cannot be downloaded from this instance, either pack sharing\
|
||||||
|
was disabled for this pack or some files are missing"
|
||||||
|
})
|
||||||
|
|
||||||
|
{:exists?, _} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:not_found)
|
||||||
|
|> json(%{error: "Pack #{name} does not exist"})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp shareable_packs_available(address) do
|
||||||
|
"#{address}/.well-known/nodeinfo"
|
||||||
|
|> Tesla.get!()
|
||||||
|
|> Map.get(:body)
|
||||||
|
|> Jason.decode!()
|
||||||
|
|> Map.get("links")
|
||||||
|
|> List.last()
|
||||||
|
|> Map.get("href")
|
||||||
|
# Get the actual nodeinfo address and fetch it
|
||||||
|
|> Tesla.get!()
|
||||||
|
|> Map.get(:body)
|
||||||
|
|> Jason.decode!()
|
||||||
|
|> get_in(["metadata", "features"])
|
||||||
|
|> Enum.member?("shareable_emoji_packs")
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
An admin endpoint to request downloading a pack named `pack_name` from the instance
|
||||||
|
`instance_address`.
|
||||||
|
|
||||||
|
If the requested instance's admin chose to share the pack, it will be downloaded
|
||||||
|
from that instance, otherwise it will be downloaded from the fallback source, if there is one.
|
||||||
|
"""
|
||||||
|
def download_from(conn, %{"instance_address" => address, "pack_name" => name} = data) do
|
||||||
|
address = String.trim(address)
|
||||||
|
|
||||||
|
if shareable_packs_available(address) do
|
||||||
|
full_pack =
|
||||||
|
"#{address}/api/pleroma/emoji/packs/list"
|
||||||
|
|> Tesla.get!()
|
||||||
|
|> Map.get(:body)
|
||||||
|
|> Jason.decode!()
|
||||||
|
|> Map.get(name)
|
||||||
|
|
||||||
|
pack_info_res =
|
||||||
|
case full_pack["pack"] do
|
||||||
|
%{"share-files" => true, "can-download" => true, "download-sha256" => sha} ->
|
||||||
|
{:ok,
|
||||||
|
%{
|
||||||
|
sha: sha,
|
||||||
|
uri: "#{address}/api/pleroma/emoji/packs/download_shared/#{name}"
|
||||||
|
}}
|
||||||
|
|
||||||
|
%{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) ->
|
||||||
|
{:ok,
|
||||||
|
%{
|
||||||
|
sha: sha,
|
||||||
|
uri: src,
|
||||||
|
fallback: true
|
||||||
|
}}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{:error,
|
||||||
|
"The pack was not set as shared and there is no fallback src to download from"}
|
||||||
|
end
|
||||||
|
|
||||||
|
with {:ok, %{sha: sha, uri: uri} = pinfo} <- pack_info_res,
|
||||||
|
%{body: emoji_archive} <- Tesla.get!(uri),
|
||||||
|
{_, true} <- {:checksum, Base.decode16!(sha) == :crypto.hash(:sha256, emoji_archive)} do
|
||||||
|
local_name = data["as"] || name
|
||||||
|
pack_dir = Path.join(emoji_dir_path(), local_name)
|
||||||
|
File.mkdir_p!(pack_dir)
|
||||||
|
|
||||||
|
files = Enum.map(full_pack["files"], fn {_, path} -> to_charlist(path) end)
|
||||||
|
# Fallback cannot contain a pack.json file
|
||||||
|
files = if pinfo[:fallback], do: files, else: ['pack.json'] ++ files
|
||||||
|
|
||||||
|
{:ok, _} = :zip.unzip(emoji_archive, cwd: to_charlist(pack_dir), file_list: files)
|
||||||
|
|
||||||
|
# Fallback can't contain a pack.json file, since that would cause the fallback-src-sha256
|
||||||
|
# in it to depend on itself
|
||||||
|
if pinfo[:fallback] do
|
||||||
|
pack_file_path = Path.join(pack_dir, "pack.json")
|
||||||
|
|
||||||
|
File.write!(pack_file_path, Jason.encode!(full_pack, pretty: true))
|
||||||
|
end
|
||||||
|
|
||||||
|
json(conn, "ok")
|
||||||
|
else
|
||||||
|
{:error, e} ->
|
||||||
|
conn |> put_status(:internal_server_error) |> json(%{error: e})
|
||||||
|
|
||||||
|
{:checksum, _} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:internal_server_error)
|
||||||
|
|> json(%{error: "SHA256 for the pack doesn't match the one sent by the server"})
|
||||||
|
end
|
||||||
|
else
|
||||||
|
conn
|
||||||
|
|> put_status(:internal_server_error)
|
||||||
|
|> json(%{error: "The requested instance does not support sharing emoji packs"})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Creates an empty pack named `name` which then can be updated via the admin UI.
|
||||||
|
"""
|
||||||
|
def create(conn, %{"name" => name}) do
|
||||||
|
pack_dir = Path.join(emoji_dir_path(), name)
|
||||||
|
|
||||||
|
if not File.exists?(pack_dir) do
|
||||||
|
File.mkdir_p!(pack_dir)
|
||||||
|
|
||||||
|
pack_file_p = Path.join(pack_dir, "pack.json")
|
||||||
|
|
||||||
|
File.write!(
|
||||||
|
pack_file_p,
|
||||||
|
Jason.encode!(%{pack: %{}, files: %{}}, pretty: true)
|
||||||
|
)
|
||||||
|
|
||||||
|
conn |> json("ok")
|
||||||
|
else
|
||||||
|
conn
|
||||||
|
|> put_status(:conflict)
|
||||||
|
|> json(%{error: "A pack named \"#{name}\" already exists"})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Deletes the pack `name` and all it's files.
|
||||||
|
"""
|
||||||
|
def delete(conn, %{"name" => name}) do
|
||||||
|
pack_dir = Path.join(emoji_dir_path(), name)
|
||||||
|
|
||||||
|
case File.rm_rf(pack_dir) do
|
||||||
|
{:ok, _} ->
|
||||||
|
conn |> json("ok")
|
||||||
|
|
||||||
|
{:error, _} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:internal_server_error)
|
||||||
|
|> json(%{error: "Couldn't delete the pack #{name}"})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
An endpoint to update `pack_names`'s metadata.
|
||||||
|
|
||||||
|
`new_data` is the new metadata for the pack, that will replace the old metadata.
|
||||||
|
"""
|
||||||
|
def update_metadata(conn, %{"pack_name" => name, "new_data" => new_data}) do
|
||||||
|
pack_file_p = Path.join([emoji_dir_path(), name, "pack.json"])
|
||||||
|
|
||||||
|
full_pack = Jason.decode!(File.read!(pack_file_p))
|
||||||
|
|
||||||
|
# The new fallback-src is in the new data and it's not the same as it was in the old data
|
||||||
|
should_update_fb_sha =
|
||||||
|
not is_nil(new_data["fallback-src"]) and
|
||||||
|
new_data["fallback-src"] != full_pack["pack"]["fallback-src"]
|
||||||
|
|
||||||
|
with {_, true} <- {:should_update?, should_update_fb_sha},
|
||||||
|
%{body: pack_arch} <- Tesla.get!(new_data["fallback-src"]),
|
||||||
|
{:ok, flist} <- :zip.unzip(pack_arch, [:memory]),
|
||||||
|
{_, true} <- {:has_all_files?, has_all_files?(full_pack, flist)} do
|
||||||
|
fallback_sha = :crypto.hash(:sha256, pack_arch) |> Base.encode16()
|
||||||
|
|
||||||
|
new_data = Map.put(new_data, "fallback-src-sha256", fallback_sha)
|
||||||
|
update_metadata_and_send(conn, full_pack, new_data, pack_file_p)
|
||||||
|
else
|
||||||
|
{:should_update?, _} ->
|
||||||
|
update_metadata_and_send(conn, full_pack, new_data, pack_file_p)
|
||||||
|
|
||||||
|
{:has_all_files?, _} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:bad_request)
|
||||||
|
|> json(%{error: "The fallback archive does not have all files specified in pack.json"})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Check if all files from the pack.json are in the archive
|
||||||
|
defp has_all_files?(%{"files" => files}, flist) do
|
||||||
|
Enum.all?(files, fn {_, from_manifest} ->
|
||||||
|
Enum.find(flist, fn {from_archive, _} ->
|
||||||
|
to_string(from_archive) == from_manifest
|
||||||
|
end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp update_metadata_and_send(conn, full_pack, new_data, pack_file_p) do
|
||||||
|
full_pack = Map.put(full_pack, "pack", new_data)
|
||||||
|
File.write!(pack_file_p, Jason.encode!(full_pack, pretty: true))
|
||||||
|
|
||||||
|
# Send new data back with fallback sha filled
|
||||||
|
json(conn, new_data)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_filename(%{"filename" => filename}), do: filename
|
||||||
|
|
||||||
|
defp get_filename(%{"file" => file}) do
|
||||||
|
case file do
|
||||||
|
%Plug.Upload{filename: filename} -> filename
|
||||||
|
url when is_binary(url) -> Path.basename(url)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp empty?(str), do: String.trim(str) == ""
|
||||||
|
|
||||||
|
defp update_file_and_send(conn, updated_full_pack, pack_file_p) do
|
||||||
|
# Write the emoji pack file
|
||||||
|
File.write!(pack_file_p, Jason.encode!(updated_full_pack, pretty: true))
|
||||||
|
|
||||||
|
# Return the modified file list
|
||||||
|
json(conn, updated_full_pack["files"])
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Updates a file in a pack.
|
||||||
|
|
||||||
|
Updating can mean three things:
|
||||||
|
|
||||||
|
- `add` adds an emoji named `shortcode` to the pack `pack_name`,
|
||||||
|
that means that the emoji file needs to be uploaded with the request
|
||||||
|
(thus requiring it to be a multipart request) and be named `file`.
|
||||||
|
There can also be an optional `filename` that will be the new emoji file name
|
||||||
|
(if it's not there, the name will be taken from the uploaded file).
|
||||||
|
- `update` changes emoji shortcode (from `shortcode` to `new_shortcode` or moves the file
|
||||||
|
(from the current filename to `new_filename`)
|
||||||
|
- `remove` removes the emoji named `shortcode` and it's associated file
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Add
|
||||||
|
def update_file(
|
||||||
|
conn,
|
||||||
|
%{"pack_name" => pack_name, "action" => "add", "shortcode" => shortcode} = params
|
||||||
|
) do
|
||||||
|
pack_dir = Path.join(emoji_dir_path(), pack_name)
|
||||||
|
pack_file_p = Path.join(pack_dir, "pack.json")
|
||||||
|
|
||||||
|
full_pack = Jason.decode!(File.read!(pack_file_p))
|
||||||
|
|
||||||
|
with {_, false} <- {:has_shortcode, Map.has_key?(full_pack["files"], shortcode)},
|
||||||
|
filename <- get_filename(params),
|
||||||
|
false <- empty?(shortcode),
|
||||||
|
false <- empty?(filename) do
|
||||||
|
file_path = Path.join(pack_dir, filename)
|
||||||
|
|
||||||
|
# If the name contains directories, create them
|
||||||
|
if String.contains?(file_path, "/") do
|
||||||
|
File.mkdir_p!(Path.dirname(file_path))
|
||||||
|
end
|
||||||
|
|
||||||
|
case params["file"] do
|
||||||
|
%Plug.Upload{path: upload_path} ->
|
||||||
|
# Copy the uploaded file from the temporary directory
|
||||||
|
File.copy!(upload_path, file_path)
|
||||||
|
|
||||||
|
url when is_binary(url) ->
|
||||||
|
# Download and write the file
|
||||||
|
file_contents = Tesla.get!(url).body
|
||||||
|
File.write!(file_path, file_contents)
|
||||||
|
end
|
||||||
|
|
||||||
|
updated_full_pack = put_in(full_pack, ["files", shortcode], filename)
|
||||||
|
update_file_and_send(conn, updated_full_pack, pack_file_p)
|
||||||
|
else
|
||||||
|
{:has_shortcode, _} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:conflict)
|
||||||
|
|> json(%{error: "An emoji with the \"#{shortcode}\" shortcode already exists"})
|
||||||
|
|
||||||
|
true ->
|
||||||
|
conn
|
||||||
|
|> put_status(:bad_request)
|
||||||
|
|> json(%{error: "shortcode or filename cannot be empty"})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Remove
|
||||||
|
def update_file(conn, %{
|
||||||
|
"pack_name" => pack_name,
|
||||||
|
"action" => "remove",
|
||||||
|
"shortcode" => shortcode
|
||||||
|
}) do
|
||||||
|
pack_dir = Path.join(emoji_dir_path(), pack_name)
|
||||||
|
pack_file_p = Path.join(pack_dir, "pack.json")
|
||||||
|
|
||||||
|
full_pack = Jason.decode!(File.read!(pack_file_p))
|
||||||
|
|
||||||
|
if Map.has_key?(full_pack["files"], shortcode) do
|
||||||
|
{emoji_file_path, updated_full_pack} = pop_in(full_pack, ["files", shortcode])
|
||||||
|
|
||||||
|
emoji_file_path = Path.join(pack_dir, emoji_file_path)
|
||||||
|
|
||||||
|
# Delete the emoji file
|
||||||
|
File.rm!(emoji_file_path)
|
||||||
|
|
||||||
|
# If the old directory has no more files, remove it
|
||||||
|
if String.contains?(emoji_file_path, "/") do
|
||||||
|
dir = Path.dirname(emoji_file_path)
|
||||||
|
|
||||||
|
if Enum.empty?(File.ls!(dir)) do
|
||||||
|
File.rmdir!(dir)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
update_file_and_send(conn, updated_full_pack, pack_file_p)
|
||||||
|
else
|
||||||
|
conn
|
||||||
|
|> put_status(:bad_request)
|
||||||
|
|> json(%{error: "Emoji \"#{shortcode}\" does not exist"})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Update
|
||||||
|
def update_file(
|
||||||
|
conn,
|
||||||
|
%{"pack_name" => pack_name, "action" => "update", "shortcode" => shortcode} = params
|
||||||
|
) do
|
||||||
|
pack_dir = Path.join(emoji_dir_path(), pack_name)
|
||||||
|
pack_file_p = Path.join(pack_dir, "pack.json")
|
||||||
|
|
||||||
|
full_pack = Jason.decode!(File.read!(pack_file_p))
|
||||||
|
|
||||||
|
with {_, true} <- {:has_shortcode, Map.has_key?(full_pack["files"], shortcode)},
|
||||||
|
%{"new_shortcode" => new_shortcode, "new_filename" => new_filename} <- params,
|
||||||
|
false <- empty?(new_shortcode),
|
||||||
|
false <- empty?(new_filename) do
|
||||||
|
# First, remove the old shortcode, saving the old path
|
||||||
|
{old_emoji_file_path, updated_full_pack} = pop_in(full_pack, ["files", shortcode])
|
||||||
|
old_emoji_file_path = Path.join(pack_dir, old_emoji_file_path)
|
||||||
|
new_emoji_file_path = Path.join(pack_dir, new_filename)
|
||||||
|
|
||||||
|
# If the name contains directories, create them
|
||||||
|
if String.contains?(new_emoji_file_path, "/") do
|
||||||
|
File.mkdir_p!(Path.dirname(new_emoji_file_path))
|
||||||
|
end
|
||||||
|
|
||||||
|
# Move/Rename the old filename to a new filename
|
||||||
|
# These are probably on the same filesystem, so just rename should work
|
||||||
|
:ok = File.rename(old_emoji_file_path, new_emoji_file_path)
|
||||||
|
|
||||||
|
# If the old directory has no more files, remove it
|
||||||
|
if String.contains?(old_emoji_file_path, "/") do
|
||||||
|
dir = Path.dirname(old_emoji_file_path)
|
||||||
|
|
||||||
|
if Enum.empty?(File.ls!(dir)) do
|
||||||
|
File.rmdir!(dir)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Then, put in the new shortcode with the new path
|
||||||
|
updated_full_pack = put_in(updated_full_pack, ["files", new_shortcode], new_filename)
|
||||||
|
update_file_and_send(conn, updated_full_pack, pack_file_p)
|
||||||
|
else
|
||||||
|
{:has_shortcode, _} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:bad_request)
|
||||||
|
|> json(%{error: "Emoji \"#{shortcode}\" does not exist"})
|
||||||
|
|
||||||
|
true ->
|
||||||
|
conn
|
||||||
|
|> put_status(:bad_request)
|
||||||
|
|> json(%{error: "new_shortcode or new_filename cannot be empty"})
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
conn
|
||||||
|
|> put_status(:bad_request)
|
||||||
|
|> json(%{error: "new_shortcode or new_file were not specified"})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_file(conn, %{"action" => action}) do
|
||||||
|
conn
|
||||||
|
|> put_status(:bad_request)
|
||||||
|
|> json(%{error: "Unknown action: #{action}"})
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Imports emoji from the filesystem.
|
||||||
|
|
||||||
|
Importing means checking all the directories in the
|
||||||
|
`$instance_static/emoji/` for directories which do not have
|
||||||
|
`pack.json`. If one has an emoji.txt file, that file will be used
|
||||||
|
to create a `pack.json` file with it's contents. If the directory has
|
||||||
|
neither, all the files with specific configured extenstions will be
|
||||||
|
assumed to be emojis and stored in the new `pack.json` file.
|
||||||
|
"""
|
||||||
|
def import_from_fs(conn, _params) do
|
||||||
|
with {:ok, results} <- File.ls(emoji_dir_path()) do
|
||||||
|
imported_pack_names =
|
||||||
|
results
|
||||||
|
|> Enum.filter(fn file ->
|
||||||
|
dir_path = Path.join(emoji_dir_path(), file)
|
||||||
|
# Find the directories that do NOT have pack.json
|
||||||
|
File.dir?(dir_path) and not File.exists?(Path.join(dir_path, "pack.json"))
|
||||||
|
end)
|
||||||
|
|> Enum.map(&write_pack_json_contents/1)
|
||||||
|
|
||||||
|
json(conn, imported_pack_names)
|
||||||
|
else
|
||||||
|
{:error, _} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:internal_server_error)
|
||||||
|
|> json(%{error: "Error accessing emoji pack directory"})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp write_pack_json_contents(dir) do
|
||||||
|
dir_path = Path.join(emoji_dir_path(), dir)
|
||||||
|
emoji_txt_path = Path.join(dir_path, "emoji.txt")
|
||||||
|
|
||||||
|
files_for_pack = files_for_pack(emoji_txt_path, dir_path)
|
||||||
|
pack_json_contents = Jason.encode!(%{pack: %{}, files: files_for_pack})
|
||||||
|
|
||||||
|
File.write!(Path.join(dir_path, "pack.json"), pack_json_contents)
|
||||||
|
|
||||||
|
dir
|
||||||
|
end
|
||||||
|
|
||||||
|
defp files_for_pack(emoji_txt_path, dir_path) do
|
||||||
|
if File.exists?(emoji_txt_path) do
|
||||||
|
# There's an emoji.txt file, it's likely from a pack installed by the pack manager.
|
||||||
|
# Make a pack.json file from the contents of that emoji.txt fileh
|
||||||
|
|
||||||
|
# FIXME: Copy-pasted from Pleroma.Emoji/load_from_file_stream/2
|
||||||
|
|
||||||
|
# Create a map of shortcodes to filenames from emoji.txt
|
||||||
|
File.read!(emoji_txt_path)
|
||||||
|
|> String.split("\n")
|
||||||
|
|> Enum.map(&String.trim/1)
|
||||||
|
|> Enum.map(fn line ->
|
||||||
|
case String.split(line, ~r/,\s*/) do
|
||||||
|
# This matches both strings with and without tags
|
||||||
|
# and we don't care about tags here
|
||||||
|
[name, file | _] -> {name, file}
|
||||||
|
_ -> nil
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|> Enum.filter(fn x -> not is_nil(x) end)
|
||||||
|
|> Enum.into(%{})
|
||||||
|
else
|
||||||
|
# If there's no emoji.txt, assume all files
|
||||||
|
# that are of certain extensions from the config are emojis and import them all
|
||||||
|
pack_extensions = Pleroma.Config.get!([:emoji, :pack_extensions])
|
||||||
|
Pleroma.Emoji.Loader.make_shortcode_to_file_map(dir_path, pack_extensions)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,11 @@
|
|||||||
|
defmodule Pleroma.Repo.Migrations.UpdateOban do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
Oban.Migrations.up(version: 4)
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
Oban.Migrations.down(version: 2)
|
||||||
|
end
|
||||||
|
end
|
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
|||||||
|
@supports (-webkit-mask:none) and (not (cater-color:#fff)){.login-container .el-input input{color:#fff}.login-container .el-input input:first-line{color:#eee}}.login-container .el-input{display:inline-block;height:47px;width:85%}.login-container .el-input input{background:transparent;border:0;-webkit-appearance:none;border-radius:0;padding:12px 5px 12px 15px;color:#eee;height:47px;caret-color:#fff}.login-container .el-input input:-webkit-autofill{-webkit-box-shadow:0 0 0 1000px #283443 inset!important;-webkit-text-fill-color:#fff!important}.login-container .el-form-item{border:1px solid hsla(0,0%,100%,.1);background:rgba(0,0,0,.1);border-radius:5px;color:#454545}.login-container .login-button{width:100%;margin:0 0 10px}.login-container .omit-host-note{color:#596f8c;font-size:.8em;font-style:italic;margin:-20px 0 15px;padding:3px 0 0 15px}.login-container[data-v-d027d802]{min-height:100%;width:100%;background-color:#2d3a4b;overflow:hidden}.login-container .login-form[data-v-d027d802]{position:relative;width:520px;max-width:100%;padding:160px 35px 0;margin:0 auto;overflow:hidden}.login-container .tips[data-v-d027d802]{font-size:14px;color:#fff;margin-bottom:10px}.login-container .tips span[data-v-d027d802]:first-of-type{margin-right:16px}.login-container .svg-container[data-v-d027d802]{padding:6px 5px 6px 15px;color:#889aa4;vertical-align:middle;width:30px;display:inline-block}.login-container .title-container[data-v-d027d802]{position:relative}.login-container .title-container .title[data-v-d027d802]{font-size:26px;color:#eee;margin:0 auto 40px;text-align:center;font-weight:700}.login-container .title-container .set-language[data-v-d027d802]{color:#fff;position:absolute;top:3px;font-size:18px;right:0;cursor:pointer}.login-container .show-pwd[data-v-d027d802]{position:absolute;right:10px;top:7px;font-size:16px;color:#889aa4;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.login-container .thirdparty-button[data-v-d027d802]{position:absolute;right:0;bottom:6px}
|
@ -0,0 +1 @@
|
|||||||
|
.wscn-http404-container[data-v-1d6b2d2a]{-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);position:absolute;top:40%;left:50%}.wscn-http404[data-v-1d6b2d2a]{position:relative;width:1200px;padding:0 50px;overflow:hidden}.wscn-http404 .pic-404[data-v-1d6b2d2a]{position:relative;float:left;width:600px;overflow:hidden}.wscn-http404 .pic-404__parent[data-v-1d6b2d2a]{width:100%}.wscn-http404 .pic-404__child[data-v-1d6b2d2a]{position:absolute}.wscn-http404 .pic-404__child.left[data-v-1d6b2d2a]{width:80px;top:17px;left:220px;opacity:0;-webkit-animation-name:cloudLeft-data-v-1d6b2d2a;animation-name:cloudLeft-data-v-1d6b2d2a;-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-delay:1s;animation-delay:1s}.wscn-http404 .pic-404__child.mid[data-v-1d6b2d2a]{width:46px;top:10px;left:420px;opacity:0;-webkit-animation-name:cloudMid-data-v-1d6b2d2a;animation-name:cloudMid-data-v-1d6b2d2a;-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-delay:1.2s;animation-delay:1.2s}.wscn-http404 .pic-404__child.right[data-v-1d6b2d2a]{width:62px;top:100px;left:500px;opacity:0;-webkit-animation-name:cloudRight-data-v-1d6b2d2a;animation-name:cloudRight-data-v-1d6b2d2a;-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-delay:1s;animation-delay:1s}@-webkit-keyframes cloudLeft-data-v-1d6b2d2a{0%{top:17px;left:220px;opacity:0}20%{top:33px;left:188px;opacity:1}80%{top:81px;left:92px;opacity:1}to{top:97px;left:60px;opacity:0}}@keyframes cloudLeft-data-v-1d6b2d2a{0%{top:17px;left:220px;opacity:0}20%{top:33px;left:188px;opacity:1}80%{top:81px;left:92px;opacity:1}to{top:97px;left:60px;opacity:0}}@-webkit-keyframes cloudMid-data-v-1d6b2d2a{0%{top:10px;left:420px;opacity:0}20%{top:40px;left:360px;opacity:1}70%{top:130px;left:180px;opacity:1}to{top:160px;left:120px;opacity:0}}@keyframes cloudMid-data-v-1d6b2d2a{0%{top:10px;left:420px;opacity:0}20%{top:40px;left:360px;opacity:1}70%{top:130px;left:180px;opacity:1}to{top:160px;left:120px;opacity:0}}@-webkit-keyframes cloudRight-data-v-1d6b2d2a{0%{top:100px;left:500px;opacity:0}20%{top:120px;left:460px;opacity:1}80%{top:180px;left:340px;opacity:1}to{top:200px;left:300px;opacity:0}}@keyframes cloudRight-data-v-1d6b2d2a{0%{top:100px;left:500px;opacity:0}20%{top:120px;left:460px;opacity:1}80%{top:180px;left:340px;opacity:1}to{top:200px;left:300px;opacity:0}}.wscn-http404 .bullshit[data-v-1d6b2d2a]{position:relative;float:left;width:300px;padding:30px 0;overflow:hidden}.wscn-http404 .bullshit__oops[data-v-1d6b2d2a]{font-size:32px;line-height:40px;color:#1482f0;margin-bottom:20px;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}.wscn-http404 .bullshit__headline[data-v-1d6b2d2a],.wscn-http404 .bullshit__oops[data-v-1d6b2d2a]{font-weight:700;opacity:0;-webkit-animation-name:slideUp-data-v-1d6b2d2a;animation-name:slideUp-data-v-1d6b2d2a;-webkit-animation-duration:.5s;animation-duration:.5s}.wscn-http404 .bullshit__headline[data-v-1d6b2d2a]{font-size:20px;line-height:24px;color:#222;margin-bottom:10px;-webkit-animation-delay:.1s;animation-delay:.1s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}.wscn-http404 .bullshit__info[data-v-1d6b2d2a]{font-size:13px;line-height:21px;color:grey;margin-bottom:30px;-webkit-animation-delay:.2s;animation-delay:.2s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}.wscn-http404 .bullshit__info[data-v-1d6b2d2a],.wscn-http404 .bullshit__return-home[data-v-1d6b2d2a]{opacity:0;-webkit-animation-name:slideUp-data-v-1d6b2d2a;animation-name:slideUp-data-v-1d6b2d2a;-webkit-animation-duration:.5s;animation-duration:.5s}.wscn-http404 .bullshit__return-home[data-v-1d6b2d2a]{display:block;float:left;width:165px;height:36px;background:#1482f0;border-radius:100px;text-align:center;color:#fff;font-size:14px;line-height:36px;cursor:pointer;-webkit-animation-delay:.3s;animation-delay:.3s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}@-webkit-keyframes slideUp-data-v-1d6b2d2a{0%{-webkit-transform:translateY(60px);transform:translateY(60px);opacity:0}to{-webkit-transform:translateY(0);transform:translateY(0);opacity:1}}@keyframes slideUp-data-v-1d6b2d2a{0%{-webkit-transform:translateY(60px);transform:translateY(60px);opacity:0}to{-webkit-transform:translateY(0);transform:translateY(0);opacity:1}}
|
@ -0,0 +1 @@
|
|||||||
|
.prop-row{margin-bottom:1em}.emoji-preview-img{max-width:5em}.copy-to-local-button{margin-top:2em;float:right}.new-emoji-col{margin-top:8em}.or,.shared-pack-dl-box{margin:1em}.dl-as-input{margin:1em;max-width:30%}.contents-collapse{margin:1em}.pack-actions{margin-top:1em}.new-emoji-uploader{margin-bottom:3em}.emoji-packs-container{margin:22px 0 0 15px}.local-packs-actions{margin-top:1em;margin-bottom:1em}.remote-instance-input{max-width:10%}.create-pack-button{margin-top:1em}
|
@ -1 +0,0 @@
|
|||||||
.wscn-http404-container[data-v-b8c8aa9a]{-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);position:absolute;top:40%;left:50%}.wscn-http404[data-v-b8c8aa9a]{position:relative;width:1200px;padding:0 50px;overflow:hidden}.wscn-http404 .pic-404[data-v-b8c8aa9a]{position:relative;float:left;width:600px;overflow:hidden}.wscn-http404 .pic-404__parent[data-v-b8c8aa9a]{width:100%}.wscn-http404 .pic-404__child[data-v-b8c8aa9a]{position:absolute}.wscn-http404 .pic-404__child.left[data-v-b8c8aa9a]{width:80px;top:17px;left:220px;opacity:0;-webkit-animation-name:cloudLeft-data-v-b8c8aa9a;animation-name:cloudLeft-data-v-b8c8aa9a;-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-delay:1s;animation-delay:1s}.wscn-http404 .pic-404__child.mid[data-v-b8c8aa9a]{width:46px;top:10px;left:420px;opacity:0;-webkit-animation-name:cloudMid-data-v-b8c8aa9a;animation-name:cloudMid-data-v-b8c8aa9a;-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-delay:1.2s;animation-delay:1.2s}.wscn-http404 .pic-404__child.right[data-v-b8c8aa9a]{width:62px;top:100px;left:500px;opacity:0;-webkit-animation-name:cloudRight-data-v-b8c8aa9a;animation-name:cloudRight-data-v-b8c8aa9a;-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-delay:1s;animation-delay:1s}@-webkit-keyframes cloudLeft-data-v-b8c8aa9a{0%{top:17px;left:220px;opacity:0}20%{top:33px;left:188px;opacity:1}80%{top:81px;left:92px;opacity:1}to{top:97px;left:60px;opacity:0}}@keyframes cloudLeft-data-v-b8c8aa9a{0%{top:17px;left:220px;opacity:0}20%{top:33px;left:188px;opacity:1}80%{top:81px;left:92px;opacity:1}to{top:97px;left:60px;opacity:0}}@-webkit-keyframes cloudMid-data-v-b8c8aa9a{0%{top:10px;left:420px;opacity:0}20%{top:40px;left:360px;opacity:1}70%{top:130px;left:180px;opacity:1}to{top:160px;left:120px;opacity:0}}@keyframes cloudMid-data-v-b8c8aa9a{0%{top:10px;left:420px;opacity:0}20%{top:40px;left:360px;opacity:1}70%{top:130px;left:180px;opacity:1}to{top:160px;left:120px;opacity:0}}@-webkit-keyframes cloudRight-data-v-b8c8aa9a{0%{top:100px;left:500px;opacity:0}20%{top:120px;left:460px;opacity:1}80%{top:180px;left:340px;opacity:1}to{top:200px;left:300px;opacity:0}}@keyframes cloudRight-data-v-b8c8aa9a{0%{top:100px;left:500px;opacity:0}20%{top:120px;left:460px;opacity:1}80%{top:180px;left:340px;opacity:1}to{top:200px;left:300px;opacity:0}}.wscn-http404 .bullshit[data-v-b8c8aa9a]{position:relative;float:left;width:300px;padding:30px 0;overflow:hidden}.wscn-http404 .bullshit__oops[data-v-b8c8aa9a]{font-size:32px;line-height:40px;color:#1482f0;margin-bottom:20px;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}.wscn-http404 .bullshit__headline[data-v-b8c8aa9a],.wscn-http404 .bullshit__oops[data-v-b8c8aa9a]{font-weight:700;opacity:0;-webkit-animation-name:slideUp-data-v-b8c8aa9a;animation-name:slideUp-data-v-b8c8aa9a;-webkit-animation-duration:.5s;animation-duration:.5s}.wscn-http404 .bullshit__headline[data-v-b8c8aa9a]{font-size:20px;line-height:24px;color:#222;margin-bottom:10px;-webkit-animation-delay:.1s;animation-delay:.1s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}.wscn-http404 .bullshit__info[data-v-b8c8aa9a]{font-size:13px;line-height:21px;color:grey;margin-bottom:30px;-webkit-animation-delay:.2s;animation-delay:.2s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}.wscn-http404 .bullshit__info[data-v-b8c8aa9a],.wscn-http404 .bullshit__return-home[data-v-b8c8aa9a]{opacity:0;-webkit-animation-name:slideUp-data-v-b8c8aa9a;animation-name:slideUp-data-v-b8c8aa9a;-webkit-animation-duration:.5s;animation-duration:.5s}.wscn-http404 .bullshit__return-home[data-v-b8c8aa9a]{display:block;float:left;width:110px;height:36px;background:#1482f0;border-radius:100px;text-align:center;color:#fff;font-size:14px;line-height:36px;cursor:pointer;-webkit-animation-delay:.3s;animation-delay:.3s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}@-webkit-keyframes slideUp-data-v-b8c8aa9a{0%{-webkit-transform:translateY(60px);transform:translateY(60px);opacity:0}to{-webkit-transform:translateY(0);transform:translateY(0);opacity:1}}@keyframes slideUp-data-v-b8c8aa9a{0%{-webkit-transform:translateY(60px);transform:translateY(60px);opacity:0}to{-webkit-transform:translateY(0);transform:translateY(0);opacity:1}}
|
|
@ -0,0 +1 @@
|
|||||||
|
.select-field[data-v-71bc6b38]{width:350px}@media (min-device-width:768px) and (max-device-width:1024px),only screen and (max-width:760px){.select-field[data-v-71bc6b38]{width:100%;margin-bottom:5px}}.actions-button[data-v-19afabea]{text-align:left;width:350px;padding:10px}.actions-button-container[data-v-19afabea]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.el-dropdown[data-v-19afabea]{float:right}.el-icon-edit[data-v-19afabea]{margin-right:5px}.tag-container[data-v-19afabea]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.tag-text[data-v-19afabea]{padding-right:20px}.no-hover[data-v-19afabea]:hover{color:#606266;background-color:#fff;cursor:auto}.el-dialog__body{padding:20px}.create-account-form-item{margin-bottom:20px}.create-account-form-item-without-margin{margin-bottom:0}@media (min-device-width:768px) and (max-device-width:1024px),only screen and (max-width:760px){.create-user-dialog{width:85%}.create-account-form-item{margin-bottom:20px}.el-dialog__body{padding:20px}}.actions-button{text-align:left;width:350px;padding:10px}.actions-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:0 15px 10px}.active-tag{color:#409eff;font-weight:700}.active-tag .el-icon-check{color:#409eff;float:right;margin:7px 0 0 15px}.el-dropdown-link:hover{cursor:pointer;color:#409eff}.el-icon-plus{margin-right:5px}.password-reset-token{margin:0 0 14px}.password-reset-token-dialog{width:50%}.reset-password-link{text-decoration:underline}.users-container h1{margin:22px 0 0 15px}.users-container .pagination{margin:25px 0;text-align:center}.users-container .search{width:350px;float:right}.users-container .filter-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:22px 15px 15px}.users-container .user-count{color:grey;font-size:28px}@media (min-device-width:768px) and (max-device-width:1024px),only screen and (max-width:760px){.password-reset-token-dialog{width:85%}.users-container h1{margin:7px 10px 15px}.users-container .actions-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:0 10px 7px}.users-container .create-account{width:100%}.users-container .el-icon-arrow-down{font-size:12px}.users-container .search{width:100%}.users-container .filter-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:82px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:0 10px}.users-container .el-tag{width:30px;display:inline-block;margin-bottom:4px;font-weight:700}.users-container .el-tag.el-tag--danger,.users-container .el-tag.el-tag--success{padding-left:8px}}
|
@ -0,0 +1 @@
|
|||||||
|
.invites-container .actions-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:20px 15px 15px}.invites-container .create-invite-token{text-align:left;width:350px;padding:10px}.invites-container .create-new-token-dialog{width:40%}.invites-container .el-dialog__body{padding:5px 20px 0}.invites-container h1{margin:22px 0 0 15px}.invites-container .icon{margin-right:5px}.invites-container .invite-token-table{width:100%;margin:0 15px}.invites-container .invite-via-email{text-align:left;width:350px;padding:10px}.invites-container .invite-via-email-dialog{width:50%}.invites-container .info{color:#666;font-size:13px;line-height:22px;margin:0 0 10px}@media (min-device-width:768px) and (max-device-width:1024px),only screen and (max-width:760px){.invites-container .actions-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:82px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:15px 10px 7px}.invites-container .create-invite-token{width:100%}.invites-container .create-new-token-dialog{width:85%}.invites-container .el-date-editor{width:150px}.invites-container .el-dialog__body{padding:5px 15px 0}.invites-container h1{margin:7px 10px 15px}.invites-container .invite-token-table{width:100%;margin:0}.invites-container .invite-via-email{width:100%;margin:10px 0 0}.invites-container .invite-via-email-dialog{width:85%}.invites-container .info{margin:0 0 10px 5px}.create-invite-token,.invite-via-email{width:100%}}
|
@ -1 +0,0 @@
|
|||||||
@supports (-webkit-mask:none) and (not (cater-color:#fff)){.login-container .el-input input{color:#fff}.login-container .el-input input:first-line{color:#eee}}.login-container .el-input{display:inline-block;height:47px;width:85%}.login-container .el-input input{background:transparent;border:0;-webkit-appearance:none;border-radius:0;padding:12px 5px 12px 15px;color:#eee;height:47px;caret-color:#fff}.login-container .el-input input:-webkit-autofill{-webkit-box-shadow:0 0 0 1000px #283443 inset!important;-webkit-text-fill-color:#fff!important}.login-container .el-form-item{border:1px solid hsla(0,0%,100%,.1);background:rgba(0,0,0,.1);border-radius:5px;color:#454545}.login-container[data-v-57350b8e]{min-height:100%;width:100%;background-color:#2d3a4b;overflow:hidden}.login-container .login-form[data-v-57350b8e]{position:relative;width:520px;max-width:100%;padding:160px 35px 0;margin:0 auto;overflow:hidden}.login-container .tips[data-v-57350b8e]{font-size:14px;color:#fff;margin-bottom:10px}.login-container .tips span[data-v-57350b8e]:first-of-type{margin-right:16px}.login-container .svg-container[data-v-57350b8e]{padding:6px 5px 6px 15px;color:#889aa4;vertical-align:middle;width:30px;display:inline-block}.login-container .title-container[data-v-57350b8e]{position:relative}.login-container .title-container .title[data-v-57350b8e]{font-size:26px;color:#eee;margin:0 auto 40px;text-align:center;font-weight:700}.login-container .title-container .set-language[data-v-57350b8e]{color:#fff;position:absolute;top:3px;font-size:18px;right:0;cursor:pointer}.login-container .show-pwd[data-v-57350b8e]{position:absolute;right:10px;top:7px;font-size:16px;color:#889aa4;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.login-container .thirdparty-button[data-v-57350b8e]{position:absolute;right:0;bottom:6px}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue