Merge remote-tracking branch 'remotes/origin/develop' into 1364-no-pushes-from-blocked-domains-users

# Conflicts:
#	CHANGELOG.md
This commit is contained in:
Ivan Tashkinov 2020-04-21 08:20:50 +03:00
commit bb5d0eafa4
127 changed files with 1913 additions and 1345 deletions

View file

@ -12,19 +12,33 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- NodeInfo: `pleroma_emoji_reactions` to the `features` list.
- Configuration: `:restrict_unauthenticated` setting, restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses.
- New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma wont start. For hackney OTP update is not required.
- Mix task to create trusted OAuth App.
- Notifications: Added `follow_request` notification type (configurable, see `[:notifications, :enable_follow_request_notifications]` setting).
- Added `:reject_deletes` group to SimplePolicy
<details>
<summary>API Changes</summary>
- Mastodon API: Support for `include_types` in `/api/v1/notifications`.
- Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint.
- Admin API: endpoints for create/update/delete OAuth Apps.
</details>
### Fixed
- Support pagination in conversations API
- **Breaking**: SimplePolicy `:reject` and `:accept` allow deletions again
- Filtering of push notifications on activities from blocked domains
## [unreleased-patch]
### Fixed
- Logger configuration through AdminFE
- HTTP Basic Authentication permissions issue
- ObjectAgePolicy didn't filter out old messages
### Added
- NodeInfo: ObjectAgePolicy settings to the `federation` list.
<details>
<summary>API Changes</summary>
- Admin API: `GET /api/pleroma/admin/need_reboot`.
</details>
## [2.0.2] - 2020-04-08
### Added

View file

@ -336,7 +336,8 @@ config :pleroma, :mrf_simple,
reject: [],
accept: [],
avatar_removal: [],
banner_removal: []
banner_removal: [],
reject_deletes: []
config :pleroma, :mrf_keyword,
reject: [],
@ -561,6 +562,8 @@ config :pleroma, :email_notifications,
inactivity_threshold: 7
}
config :pleroma, :notifications, enable_follow_request_notifications: false
config :pleroma, :oauth2,
token_expires_in: 600,
issue_new_refresh_token: true,

View file

@ -1317,13 +1317,13 @@ config :pleroma, :config_description, [
%{
key: :reject,
type: {:list, :string},
description: "List of instances to reject any activities from",
description: "List of instances to reject activities from (except deletes)",
suggestions: ["example.com", "*.example.com"]
},
%{
key: :accept,
type: {:list, :string},
description: "List of instances to accept any activities from",
description: "List of instances to only accept activities from (except deletes)",
suggestions: ["example.com", "*.example.com"]
},
%{
@ -1343,6 +1343,12 @@ config :pleroma, :config_description, [
type: {:list, :string},
description: "List of instances to strip banners from",
suggestions: ["example.com", "*.example.com"]
},
%{
key: :reject_deletes,
type: {:list, :string},
description: "List of instances to reject deletions from",
suggestions: ["example.com", "*.example.com"]
}
]
},
@ -2267,6 +2273,20 @@ config :pleroma, :config_description, [
}
]
},
%{
group: :pleroma,
key: :notifications,
type: :group,
description: "Notification settings",
children: [
%{
key: :enable_follow_request_notifications,
type: :boolean,
description:
"Enables notifications on new follow requests (causes issues with older PleromaFE versions)."
}
]
},
%{
group: :pleroma,
key: Pleroma.Emails.UserEmail,

View file

@ -786,6 +786,8 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
### Restarts pleroma application
**Only works when configuration from database is enabled.**
- Params: none
- Response:
- On failure:
@ -795,11 +797,24 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
{}
```
## `GET /api/pleroma/admin/need_reboot`
### Returns the flag whether the pleroma should be restarted
- Params: none
- Response:
- `need_reboot` - boolean
```json
{
"need_reboot": false
}
```
## `GET /api/pleroma/admin/config`
### Get list of merged default settings with saved in database.
*If `need_reboot` flag exists in response, instance must be restarted, so reboot time settings can take effect.*
*If `need_reboot` is `true`, instance must be restarted, so reboot time settings can take effect.*
**Only works when configuration from database is enabled.**
@ -821,13 +836,12 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
"need_reboot": true
}
```
need_reboot - *optional*, if were changed reboot time settings.
## `POST /api/pleroma/admin/config`
### Update config settings
*If `need_reboot` flag exists in response, instance must be restarted, so reboot time settings can take effect.*
*If `need_reboot` is `true`, instance must be restarted, so reboot time settings can take effect.*
**Only works when configuration from database is enabled.**
@ -971,7 +985,6 @@ config :quack,
"need_reboot": true
}
```
need_reboot - *optional*, if were changed reboot time settings.
## ` GET /api/pleroma/admin/config/descriptions`
@ -1075,3 +1088,104 @@ Loads json generated from `config/descriptions.exs`.
}
}
```
## `GET /api/pleroma/admin/oauth_app`
### List OAuth app
- Params:
- *optional* `name`
- *optional* `client_id`
- *optional* `page`
- *optional* `page_size`
- *optional* `trusted`
- Response:
```json
{
"apps": [
{
"id": 1,
"name": "App name",
"client_id": "yHoDSiWYp5mPV6AfsaVOWjdOyt5PhWRiafi6MRd1lSk",
"client_secret": "nLmis486Vqrv2o65eM9mLQx_m_4gH-Q6PcDpGIMl6FY",
"redirect_uri": "https://example.com/oauth-callback",
"website": "https://example.com",
"trusted": true
}
],
"count": 17,
"page_size": 50
}
```
## `POST /api/pleroma/admin/oauth_app`
### Create OAuth App
- Params:
- `name`
- `redirect_uris`
- `scopes`
- *optional* `website`
- *optional* `trusted`
- Response:
```json
{
"id": 1,
"name": "App name",
"client_id": "yHoDSiWYp5mPV6AfsaVOWjdOyt5PhWRiafi6MRd1lSk",
"client_secret": "nLmis486Vqrv2o65eM9mLQx_m_4gH-Q6PcDpGIMl6FY",
"redirect_uri": "https://example.com/oauth-callback",
"website": "https://example.com",
"trusted": true
}
```
- On failure:
```json
{
"redirect_uris": "can't be blank",
"name": "can't be blank"
}
```
## `PATCH /api/pleroma/admin/oauth_app/:id`
### Update OAuth App
- Params:
- *optional* `name`
- *optional* `redirect_uris`
- *optional* `scopes`
- *optional* `website`
- *optional* `trusted`
- Response:
```json
{
"id": 1,
"name": "App name",
"client_id": "yHoDSiWYp5mPV6AfsaVOWjdOyt5PhWRiafi6MRd1lSk",
"client_secret": "nLmis486Vqrv2o65eM9mLQx_m_4gH-Q6PcDpGIMl6FY",
"redirect_uri": "https://example.com/oauth-callback",
"website": "https://example.com",
"trusted": true
}
```
## `DELETE /api/pleroma/admin/oauth_app/:id`
### Delete OAuth App
- Params: None
- Response:
- On success: `204`, empty response
- On failure:
- 400 Bad Request `"Invalid parameters"` when `status` is missing

View file

@ -0,0 +1,16 @@
# Creating trusted OAuth App
{! backend/administration/CLI_tasks/general_cli_task_info.include !}
## Create trusted OAuth App.
Optional params:
* `-s SCOPES` - scopes for app, e.g. `read,write,follow,push`.
```sh tab="OTP"
./bin/pleroma_ctl app create -n APP_NAME -r REDIRECT_URI
```
```sh tab="From Source"
mix pleroma.app create -n APP_NAME -r REDIRECT_URI
```

View file

@ -41,11 +41,15 @@ config :pleroma, :instance,
Once `SimplePolicy` is enabled, you can configure various groups in the `:mrf_simple` config object. These groups are:
* `media_removal`: Servers in this group will have media stripped from incoming messages.
* `media_nsfw`: Servers in this group will have the #nsfw tag and sensitive setting injected into incoming messages which contain media.
* `reject`: Servers in this group will have their messages rejected.
* `federated_timeline_removal`: Servers in this group will have their messages unlisted from the public timelines by flipping the `to` and `cc` fields.
* `accept`: If not empty, only messages from these instances will be accepted (whitelist federation).
* `media_nsfw`: Servers in this group will have the #nsfw tag and sensitive setting injected into incoming messages which contain media.
* `media_removal`: Servers in this group will have media stripped from incoming messages.
* `avatar_removal`: Avatars from these servers will be stripped from incoming messages.
* `banner_removal`: Banner images from these servers will be stripped from incoming messages.
* `report_removal`: Servers in this group will have their reports (flags) rejected.
* `federated_timeline_removal`: Servers in this group will have their messages unlisted from the public timelines by flipping the `to` and `cc` fields.
* `reject_deletes`: Deletion requests will be rejected from these servers.
Servers should be configured as lists.
@ -113,7 +117,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.RewritePolicy do
@impl true
def describe do
{:ok, %{mrf_sample: %{content: "new message content"}}}`
{:ok, %{mrf_sample: %{content: "new message content"}}}
end
end
```

View file

@ -0,0 +1,49 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.App do
@moduledoc File.read!("docs/administration/CLI_tasks/oauth_app.md")
use Mix.Task
import Mix.Pleroma
@shortdoc "Creates trusted OAuth App"
def run(["create" | options]) do
start_pleroma()
{opts, _} =
OptionParser.parse!(options,
strict: [name: :string, redirect_uri: :string, scopes: :string],
aliases: [n: :name, r: :redirect_uri, s: :scopes]
)
scopes =
if opts[:scopes] do
String.split(opts[:scopes], ",")
else
["read", "write", "follow", "push"]
end
params = %{
client_name: opts[:name],
redirect_uris: opts[:redirect_uri],
trusted: true,
scopes: scopes
}
with {:ok, app} <- Pleroma.Web.OAuth.App.create(params) do
shell_info("#{app.client_name} successfully created:")
shell_info("App client_id: " <> app.client_id)
shell_info("App client_secret: " <> app.client_secret)
else
{:error, changeset} ->
shell_error("Creating failed:")
Enum.each(Pleroma.Web.OAuth.App.errors(changeset), fn {key, error} ->
shell_error("#{key}: #{error}")
end)
end
end
end

View file

@ -27,17 +27,13 @@ defmodule Pleroma.Activity do
# https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19
@mastodon_notification_types %{
"Create" => "mention",
"Follow" => "follow",
"Follow" => ["follow", "follow_request"],
"Announce" => "reblog",
"Like" => "favourite",
"Move" => "move",
"EmojiReact" => "pleroma:emoji_reaction"
}
@mastodon_to_ap_notification_types for {k, v} <- @mastodon_notification_types,
into: %{},
do: {v, k}
schema "activities" do
field(:data, :map)
field(:local, :boolean, default: true)
@ -291,15 +287,43 @@ defmodule Pleroma.Activity do
defp purge_web_resp_cache(nil), do: nil
for {ap_type, type} <- @mastodon_notification_types do
def follow_accepted?(
%Activity{data: %{"type" => "Follow", "object" => followed_ap_id}} = activity
) do
with %User{} = follower <- Activity.user_actor(activity),
%User{} = followed <- User.get_cached_by_ap_id(followed_ap_id) do
Pleroma.FollowingRelationship.following?(follower, followed)
else
_ -> false
end
end
def follow_accepted?(_), do: false
@spec mastodon_notification_type(Activity.t()) :: String.t() | nil
for {ap_type, type} <- @mastodon_notification_types, not is_list(type) do
def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}),
do: unquote(type)
end
def mastodon_notification_type(%Activity{data: %{"type" => "Follow"}} = activity) do
if follow_accepted?(activity) do
"follow"
else
"follow_request"
end
end
def mastodon_notification_type(%Activity{}), do: nil
@spec from_mastodon_notification_type(String.t()) :: String.t() | nil
@doc "Converts Mastodon notification type to AR activity type"
def from_mastodon_notification_type(type) do
Map.get(@mastodon_to_ap_notification_types, type)
with {k, _v} <-
Enum.find(@mastodon_notification_types, fn {_k, v} -> type in List.wrap(v) end) do
k
end
end
def all_by_actor_and_id(actor, status_ids \\ [])

View file

@ -122,7 +122,7 @@ defmodule Pleroma.Config.TransferTask do
:ok = update_env(:logger, :backends, merged)
end
defp configure({group, key, _, merged}) do
defp configure({_, key, _, merged}) when key in [:console, :ex_syslogger] do
merged =
if key == :console do
put_in(merged[:format], merged[:format] <> "\n")
@ -136,7 +136,12 @@ defmodule Pleroma.Config.TransferTask do
else: key
Logger.configure_backend(backend, merged)
:ok = update_env(:logger, group, merged)
:ok = update_env(:logger, key, merged)
end
defp configure({_, key, _, merged}) do
Logger.configure([{key, merged}])
:ok = update_env(:logger, key, merged)
end
defp update({group, key, value, merged}) do

View file

@ -38,22 +38,14 @@ defmodule Pleroma.Emoji.Formatter do
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)
Emoji.get_all()
|> Enum.filter(fn {emoji, %Emoji{}} -> String.contains?(text, ":#{emoji}:") end)
|> 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: []
def get_emoji_map(_), do: %{}
end

View file

@ -31,7 +31,7 @@ defmodule Pleroma.Formatter do
def mention_handler("@" <> nickname, buffer, opts, acc) do
case User.get_cached_by_nickname(nickname) do
%User{id: id} = user ->
ap_id = get_ap_id(user)
user_url = user.uri || user.ap_id
nickname_text = get_nickname_text(nickname, opts)
link =
@ -42,7 +42,7 @@ defmodule Pleroma.Formatter do
["@", Phoenix.HTML.Tag.content_tag(:span, nickname_text)],
"data-user": id,
class: "u-url mention",
href: ap_id,
href: user_url,
rel: "ugc"
),
class: "h-card"
@ -146,9 +146,6 @@ defmodule Pleroma.Formatter do
end
end
defp get_ap_id(%User{source_data: %{"url" => url}}) when is_binary(url), do: url
defp get_ap_id(%User{ap_id: ap_id}), do: ap_id
defp get_nickname_text(nickname, %{mentions_format: :full}), do: User.full_nickname(nickname)
defp get_nickname_text(nickname, _), do: User.local_nickname(nickname)
end

View file

@ -283,8 +283,17 @@ defmodule Pleroma.Notification do
end
end
def create_notifications(%Activity{data: %{"type" => "Follow"}} = activity) do
if Pleroma.Config.get([:notifications, :enable_follow_request_notifications]) ||
Activity.follow_accepted?(activity) do
do_create_notifications(activity)
else
{:ok, []}
end
end
def create_notifications(%Activity{data: %{"type" => type}} = activity)
when type in ["Like", "Announce", "Follow", "Move", "EmojiReact"] do
when type in ["Like", "Announce", "Move", "EmojiReact"] do
do_create_notifications(activity)
end

View file

@ -0,0 +1,17 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.AuthExpectedPlug do
import Plug.Conn
def init(options), do: options
def call(conn, _) do
put_private(conn, :auth_expected, true)
end
def auth_expected?(conn) do
conn.private[:auth_expected]
end
end

View file

@ -4,8 +4,11 @@
defmodule Pleroma.Plugs.AuthenticationPlug do
alias Comeonin.Pbkdf2
import Plug.Conn
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
import Plug.Conn
require Logger
def init(options), do: options
@ -37,6 +40,7 @@ defmodule Pleroma.Plugs.AuthenticationPlug do
if Pbkdf2.checkpw(password, password_hash) do
conn
|> assign(:user, auth_user)
|> OAuthScopesPlug.skip_plug()
else
conn
end

View file

@ -4,6 +4,8 @@
defmodule Pleroma.Plugs.LegacyAuthenticationPlug do
import Plug.Conn
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
def init(options) do
@ -27,6 +29,7 @@ defmodule Pleroma.Plugs.LegacyAuthenticationPlug do
conn
|> assign(:auth_user, user)
|> assign(:user, user)
|> OAuthScopesPlug.skip_plug()
else
_ ->
conn

View file

@ -8,12 +8,15 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do
alias Pleroma.Config
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
alias Pleroma.Plugs.PlugHelper
use Pleroma.Web, :plug
@behaviour Plug
def init(%{scopes: _} = options), do: options
def call(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do
def perform(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do
op = options[:op] || :|
token = assigns[:token]

View 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.Plugs.PlugHelper do
@moduledoc "Pleroma Plug helper"
@called_plugs_list_id :called_plugs
def called_plugs_list_id, do: @called_plugs_list_id
@skipped_plugs_list_id :skipped_plugs
def skipped_plugs_list_id, do: @skipped_plugs_list_id
@doc "Returns `true` if specified plug was called."
def plug_called?(conn, plug_module) do
contained_in_private_list?(conn, @called_plugs_list_id, plug_module)
end
@doc "Returns `true` if specified plug was explicitly marked as skipped."
def plug_skipped?(conn, plug_module) do
contained_in_private_list?(conn, @skipped_plugs_list_id, plug_module)
end
@doc "Returns `true` if specified plug was either called or explicitly marked as skipped."
def plug_called_or_skipped?(conn, plug_module) do
plug_called?(conn, plug_module) || plug_skipped?(conn, plug_module)
end
# Appends plug to known list (skipped, called). Intended to be used from within plug code only.
def append_to_private_list(conn, list_id, value) do
list = conn.private[list_id] || []
modified_list = Enum.uniq(list ++ [value])
Plug.Conn.put_private(conn, list_id, modified_list)
end
defp contained_in_private_list?(conn, private_variable, value) do
list = conn.private[private_variable] || []
value in list
end
end

View file

@ -110,20 +110,9 @@ defmodule Pleroma.Plugs.RateLimiter do
end
def disabled?(conn) do
localhost_or_socket =
case Config.get([Pleroma.Web.Endpoint, :http, :ip]) do
{127, 0, 0, 1} -> true
{0, 0, 0, 0, 0, 0, 0, 1} -> true
{:local, _} -> true
_ -> false
end
remote_ip_not_found =
if Map.has_key?(conn.assigns, :remote_ip_found),
do: !conn.assigns.remote_ip_found,
else: false
localhost_or_socket and remote_ip_not_found
if Map.has_key?(conn.assigns, :remote_ip_found),
do: !conn.assigns.remote_ip_found,
else: false
end
@inspect_bucket_not_found {:error, :not_found}

View file

@ -7,8 +7,6 @@ defmodule Pleroma.Plugs.RemoteIp do
This is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.
"""
import Plug.Conn
@behaviour Plug
@headers ~w[
@ -28,12 +26,11 @@ defmodule Pleroma.Plugs.RemoteIp do
def init(_), do: nil
def call(%{remote_ip: original_remote_ip} = conn, _) do
def call(conn, _) do
config = Pleroma.Config.get(__MODULE__, [])
if Keyword.get(config, :enabled, false) do
%{remote_ip: new_remote_ip} = conn = RemoteIp.call(conn, remote_ip_opts(config))
assign(conn, :remote_ip_found, original_remote_ip != new_remote_ip)
RemoteIp.call(conn, remote_ip_opts(config))
else
conn
end

View file

@ -41,6 +41,7 @@ defmodule Pleroma.Plugs.UploadedMedia do
conn ->
conn
end
|> merge_resp_headers([{"content-security-policy", "sandbox"}])
config = Pleroma.Config.get(Pleroma.Upload)

View file

@ -0,0 +1,31 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
# A test controller reachable only in :test env.
# Serves to test OAuth scopes check skipping / enforcement.
defmodule Pleroma.Tests.OAuthTestController do
@moduledoc false
use Pleroma.Web, :controller
alias Pleroma.Plugs.OAuthScopesPlug
plug(:skip_plug, OAuthScopesPlug when action == :skipped_oauth)
plug(OAuthScopesPlug, %{scopes: ["read"]} when action != :missed_oauth)
def skipped_oauth(conn, _params) do
noop(conn)
end
def performed_oauth(conn, _params) do
noop(conn)
end
def missed_oauth(conn, _params) do
noop(conn)
end
defp noop(conn), do: json(conn, %{})
end

View file

@ -15,6 +15,7 @@ defmodule Pleroma.User do
alias Pleroma.Config
alias Pleroma.Conversation.Participation
alias Pleroma.Delivery
alias Pleroma.Emoji
alias Pleroma.FollowingRelationship
alias Pleroma.Formatter
alias Pleroma.HTML
@ -28,6 +29,7 @@ defmodule Pleroma.User do
alias Pleroma.UserRelationship
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
@ -82,6 +84,7 @@ defmodule Pleroma.User do
field(:password, :string, virtual: true)
field(:password_confirmation, :string, virtual: true)
field(:keys, :string)
field(:public_key, :string)
field(:ap_id, :string)
field(:avatar, :map)
field(:local, :boolean, default: true)
@ -94,7 +97,6 @@ defmodule Pleroma.User do
field(:last_digest_emailed_at, :naive_datetime)
field(:banner, :map, default: %{})
field(:background, :map, default: %{})
field(:source_data, :map, default: %{})
field(:note_count, :integer, default: 0)
field(:follower_count, :integer, default: 0)
field(:following_count, :integer, default: 0)
@ -112,7 +114,7 @@ defmodule Pleroma.User do
field(:show_role, :boolean, default: true)
field(:settings, :map, default: nil)
field(:magic_key, :string, default: nil)
field(:uri, :string, default: nil)
field(:uri, Types.Uri, default: nil)
field(:hide_followers_count, :boolean, default: false)
field(:hide_follows_count, :boolean, default: false)
field(:hide_followers, :boolean, default: false)
@ -122,7 +124,7 @@ defmodule Pleroma.User do
field(:pinned_activities, {:array, :string}, default: [])
field(:email_notifications, :map, default: %{"digest" => false})
field(:mascot, :map, default: nil)
field(:emoji, {:array, :map}, default: [])
field(:emoji, :map, default: %{})
field(:pleroma_settings_store, :map, default: %{})
field(:fields, {:array, :map}, default: [])
field(:raw_fields, {:array, :map}, default: [])
@ -132,6 +134,8 @@ defmodule Pleroma.User do
field(:skip_thread_containment, :boolean, default: false)
field(:actor_type, :string, default: "Person")
field(:also_known_as, {:array, :string}, default: [])
field(:inbox, :string)
field(:shared_inbox, :string)
embeds_one(
:notification_settings,
@ -306,6 +310,7 @@ defmodule Pleroma.User do
end
end
# Should probably be renamed or removed
def ap_id(%User{nickname: nickname}), do: "#{Web.base_url()}/users/#{nickname}"
def ap_followers(%User{follower_address: fa}) when is_binary(fa), do: fa
@ -339,62 +344,72 @@ defmodule Pleroma.User do
end
end
def remote_user_creation(params) do
defp fix_follower_address(%{follower_address: _, following_address: _} = params), do: params
defp fix_follower_address(%{nickname: nickname} = params),
do: Map.put(params, :follower_address, ap_followers(%User{nickname: nickname}))
defp fix_follower_address(params), do: params
def remote_user_changeset(struct \\ %User{local: false}, params) do
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
name =
case params[:name] do
name when is_binary(name) and byte_size(name) > 0 -> name
_ -> params[:nickname]
end
params =
params
|> Map.put(:name, name)
|> Map.put_new(:last_refreshed_at, NaiveDateTime.utc_now())
|> truncate_if_exists(:name, name_limit)
|> truncate_if_exists(:bio, bio_limit)
|> truncate_fields_param()
|> fix_follower_address()
changeset =
%User{local: false}
|> cast(
params,
[
:bio,
:name,
:ap_id,
:nickname,
:avatar,
:ap_enabled,
:source_data,
:banner,
:locked,
:magic_key,
:uri,
:hide_followers,
:hide_follows,
:hide_followers_count,
:hide_follows_count,
:follower_count,
:fields,
:following_count,
:discoverable,
:invisible,
:actor_type,
:also_known_as
]
)
|> validate_required([:name, :ap_id])
|> unique_constraint(:nickname)
|> validate_format(:nickname, @email_regex)
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, max: name_limit)
|> validate_fields(true)
case params[:source_data] do
%{"followers" => followers, "following" => following} ->
changeset
|> put_change(:follower_address, followers)
|> put_change(:following_address, following)
_ ->
followers = ap_followers(%User{nickname: get_field(changeset, :nickname)})
put_change(changeset, :follower_address, followers)
end
struct
|> cast(
params,
[
:bio,
:name,
:emoji,
:ap_id,
:inbox,
:shared_inbox,
:nickname,
:public_key,
:avatar,
:ap_enabled,
:banner,
:locked,
:last_refreshed_at,
:magic_key,
:uri,
:follower_address,
:following_address,
:hide_followers,
:hide_follows,
:hide_followers_count,
:hide_follows_count,
:follower_count,
:fields,
:following_count,
:discoverable,
:invisible,
:actor_type,
:also_known_as
]
)
|> validate_required([:name, :ap_id])
|> unique_constraint(:nickname)
|> validate_format(:nickname, @email_regex)
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, max: name_limit)
|> validate_fields(true)
end
def update_changeset(struct, params \\ %{}) do
@ -407,7 +422,11 @@ defmodule Pleroma.User do
[
:bio,
:name,
:emoji,
:avatar,
:public_key,
:inbox,
:shared_inbox,
:locked,
:no_rich_text,
:default_scope,
@ -434,6 +453,7 @@ defmodule Pleroma.User do
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, min: 1, max: name_limit)
|> put_fields()
|> put_emoji()
|> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
|> put_change_if_present(:avatar, &put_upload(&1, :avatar))
|> put_change_if_present(:banner, &put_upload(&1, :banner))
@ -469,6 +489,18 @@ defmodule Pleroma.User do
|> elem(0)
end
defp put_emoji(changeset) do
bio = get_change(changeset, :bio)
name = get_change(changeset, :name)
if bio || name do
emoji = Map.merge(Emoji.Formatter.get_emoji_map(bio), Emoji.Formatter.get_emoji_map(name))
put_change(changeset, :emoji, emoji)
else
changeset
end
end
defp put_change_if_present(changeset, map_field, value_function) do
if value = get_change(changeset, map_field) do
with {:ok, new_value} <- value_function.(value) do
@ -488,49 +520,6 @@ defmodule Pleroma.User do
end
end
def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now())
params = if remote?, do: truncate_fields_param(params), else: params
struct
|> cast(
params,
[
:bio,
:name,
:follower_address,
:following_address,
:avatar,
:last_refreshed_at,
:ap_enabled,
:source_data,
:banner,
:locked,
:magic_key,
:follower_count,
:following_count,
:hide_follows,
:fields,
:hide_followers,
:allow_following_move,
:discoverable,
:hide_followers_count,
:hide_follows_count,
:actor_type,
:also_known_as
]
)
|> unique_constraint(:nickname)
|> validate_format(:nickname, local_nickname_regex())
|> validate_length(:bio, max: bio_limit)
|> validate_length(:name, max: name_limit)
|> validate_fields(remote?)
end
def update_as_admin_changeset(struct, params) do
struct
|> update_changeset(params)
@ -606,7 +595,7 @@ defmodule Pleroma.User do
struct
|> confirmation_changeset(need_confirmation: need_confirmation?)
|> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
|> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation, :emoji])
|> validate_required([:name, :nickname, :password, :password_confirmation])
|> validate_confirmation(:password)
|> unique_constraint(:email)
@ -699,6 +688,8 @@ defmodule Pleroma.User do
def needs_update?(_), do: true
@spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
# "Locked" (self-locked) users demand explicit authorization of follow requests
def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
follow(follower, followed, :follow_pending)
end
@ -1621,8 +1612,7 @@ defmodule Pleroma.User do
|> set_cache()
end
# AP style
def public_key(%{source_data: %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}) do
def public_key(%{public_key: public_key_pem}) when is_binary(public_key_pem) do
key =
public_key_pem
|> :public_key.pem_decode()
@ -1632,7 +1622,7 @@ defmodule Pleroma.User do
{:ok, key}
end
def public_key(_), do: {:error, "not found key"}
def public_key(_), do: {:error, "key not found"}
def get_public_key_for_ap_id(ap_id) do
with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id),
@ -1643,17 +1633,6 @@ defmodule Pleroma.User do
end
end
defp blank?(""), do: nil
defp blank?(n), do: n
def insert_or_update_user(data) do
data
|> Map.put(:name, blank?(data[:name]) || data[:nickname])
|> remote_user_creation()
|> Repo.insert(on_conflict: {:replace_all_except, [:id]}, conflict_target: :nickname)
|> set_cache()
end
def ap_enabled?(%User{local: true}), do: true
def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
def ap_enabled?(_), do: false
@ -1962,12 +1941,6 @@ defmodule Pleroma.User do
|> update_and_set_cache()
end
def update_source_data(user, source_data) do
user
|> cast(%{source_data: source_data}, [:source_data])
|> update_and_set_cache()
end
def roles(%{is_moderator: is_moderator, is_admin: is_admin}) do
%{
admin: is_admin,
@ -1975,21 +1948,6 @@ defmodule Pleroma.User do
}
end
# ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
# For example: [{"name": "Pronoun", "value": "she/her"}, …]
def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do
limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0)
attachment
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
|> Enum.take(limit)
end
def fields(%{fields: nil}), do: []
def fields(%{fields: fields}), do: fields
def validate_fields(changeset, remote? \\ false) do
limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
limit = Pleroma.Config.get([:instance, limit_name], 0)
@ -2177,9 +2135,7 @@ defmodule Pleroma.User do
# - display name
def sanitize_html(%User{} = user, filter) do
fields =
user
|> User.fields()
|> Enum.map(fn %{"name" => name, "value" => value} ->
Enum.map(user.fields, fn %{"name" => name, "value" => value} ->
%{
"name" => name,
"value" => HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)

View file

@ -1427,19 +1427,44 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
emojis =
data
|> Map.get("tag", [])
|> Enum.filter(fn
%{"type" => "Emoji"} -> true
_ -> false
end)
|> Enum.reduce(%{}, fn %{"icon" => %{"url" => url}, "name" => name}, acc ->
Map.put(acc, String.trim(name, ":"), url)
end)
locked = data["manuallyApprovesFollowers"] || false
data = Transmogrifier.maybe_fix_user_object(data)
discoverable = data["discoverable"] || false
invisible = data["invisible"] || false
actor_type = data["type"] || "Person"
public_key =
if is_map(data["publicKey"]) && is_binary(data["publicKey"]["publicKeyPem"]) do
data["publicKey"]["publicKeyPem"]
else
nil
end
shared_inbox =
if is_map(data["endpoints"]) && is_binary(data["endpoints"]["sharedInbox"]) do
data["endpoints"]["sharedInbox"]
else
nil
end
user_data = %{
ap_id: data["id"],
uri: get_actor_url(data["url"]),
ap_enabled: true,
source_data: data,
banner: banner,
fields: fields,
emoji: emojis,
locked: locked,
discoverable: discoverable,
invisible: invisible,
@ -1449,7 +1474,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
following_address: data["following"],
bio: data["summary"],
actor_type: actor_type,
also_known_as: Map.get(data, "alsoKnownAs", [])
also_known_as: Map.get(data, "alsoKnownAs", []),
public_key: public_key,
inbox: data["inbox"],
shared_inbox: shared_inbox
}
# nickname can be nil because of virtual actors
@ -1551,11 +1579,22 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
def make_user_from_ap_id(ap_id) do
if _user = User.get_cached_by_ap_id(ap_id) do
user = User.get_cached_by_ap_id(ap_id)
if user && !User.ap_enabled?(user) do
Transmogrifier.upgrade_user_from_ap_id(ap_id)
else
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id) do
User.insert_or_update_user(data)
if user do
user
|> User.remote_user_changeset(data)
|> User.update_and_set_cache()
else
data
|> User.remote_user_changeset()
|> Repo.insert()
|> User.set_cache()
end
else
e -> {:error, e}
end

View file

@ -11,7 +11,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
@moduledoc "Filter activities depending on their age"
@behaviour Pleroma.Web.ActivityPub.MRF
defp check_date(%{"published" => published} = message) do
defp check_date(%{"object" => %{"published" => published}} = message) do
with %DateTime{} = now <- DateTime.utc_now(),
{:ok, %DateTime{} = then, _} <- DateTime.from_iso8601(published),
max_ttl <- Config.get([:mrf_object_age, :threshold]),
@ -96,5 +96,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.ObjectAgePolicy do
def filter(message), do: {:ok, message}
@impl true
def describe, do: {:ok, %{}}
def describe do
mrf_object_age =
Pleroma.Config.get(:mrf_object_age)
|> Enum.into(%{})
{:ok, %{mrf_object_age: mrf_object_age}}
end
end

View file

@ -148,6 +148,21 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
defp check_banner_removal(_actor_info, object), do: {:ok, object}
@impl true
def filter(%{"type" => "Delete", "actor" => actor} = object) do
%{host: actor_host} = URI.parse(actor)
reject_deletes =
Pleroma.Config.get([:mrf_simple, :reject_deletes])
|> MRF.subdomains_regex()
if MRF.subdomain_match?(reject_deletes, actor_host) do
{:reject, nil}
else
{:ok, object}
end
end
@impl true
def filter(%{"actor" => actor} = object) do
actor_info = URI.parse(actor)

View file

@ -35,6 +35,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator do
field(:like_count, :integer, default: 0)
field(:announcement_count, :integer, default: 0)
field(:inRepyTo, :string)
field(:uri, Types.Uri)
field(:likes, {:array, :string}, default: [])
field(:announcements, {:array, :string}, default: [])

View file

@ -15,15 +15,9 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID do
def cast(%{"id" => object}), do: cast(object)
def cast(_) do
:error
end
def cast(_), do: :error
def dump(data) do
{:ok, data}
end
def dump(data), do: {:ok, data}
def load(data) do
{:ok, data}
end
def load(data), do: {:ok, data}
end

View file

@ -0,0 +1,20 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.Uri do
use Ecto.Type
def type, do: :string
def cast(uri) when is_binary(uri) do
case URI.parse(uri) do
%URI{host: nil} -> :error
%URI{host: ""} -> :error
%URI{scheme: scheme} when scheme in ["https", "http"] -> {:ok, uri}
_ -> :error
end
end
def cast(_), do: :error
def dump(data), do: {:ok, data}
def load(data), do: {:ok, data}
end

View file

@ -141,8 +141,8 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|> Enum.map(& &1.ap_id)
end
defp maybe_use_sharedinbox(%User{source_data: data}),
do: (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
defp maybe_use_sharedinbox(%User{shared_inbox: nil, inbox: inbox}), do: inbox
defp maybe_use_sharedinbox(%User{shared_inbox: shared_inbox}), do: shared_inbox
@doc """
Determine a user inbox to use based on heuristics. These heuristics
@ -157,7 +157,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
"""
def determine_inbox(
%Activity{data: activity_data},
%User{source_data: data} = user
%User{inbox: inbox} = user
) do
to = activity_data["to"] || []
cc = activity_data["cc"] || []
@ -174,7 +174,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
maybe_use_sharedinbox(user)
true ->
data["inbox"]
inbox
end
end
@ -192,14 +192,13 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
inboxes =
recipients
|> Enum.filter(&User.ap_enabled?/1)
|> Enum.map(fn %{source_data: data} -> data["inbox"] end)
|> Enum.map(fn actor -> actor.inbox end)
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
|> Instances.filter_reachable()
Repo.checkout(fn ->
Enum.each(inboxes, fn {inbox, unreachable_since} ->
%User{ap_id: ap_id} =
Enum.find(recipients, fn %{source_data: data} -> data["inbox"] == inbox end)
%User{ap_id: ap_id} = Enum.find(recipients, fn actor -> actor.inbox == inbox end)
# Get all the recipients on the same host and add them to cc. Otherwise, a remote
# instance would only accept a first message for the first recipient and ignore the rest.

View file

@ -17,7 +17,9 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
def handle(%{data: %{"type" => "Like"}} = object, meta) do
liked_object = Object.get_by_ap_id(object.data["object"])
Utils.add_like_to_object(object, liked_object)
Notification.create_notifications(object)
{:ok, object, meta}
end

View file

@ -711,7 +711,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
{:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
actor
|> User.upgrade_changeset(new_user_data, true)
|> User.remote_user_changeset(new_user_data)
|> User.update_and_set_cache()
ActivityPub.update(%{
@ -1160,7 +1160,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def take_emoji_tags(%User{emoji: emoji}) do
emoji
|> Enum.flat_map(&Map.to_list/1)
|> Map.to_list()
|> Enum.map(&build_emoji_tag/1)
end
@ -1254,12 +1254,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def upgrade_user_from_ap_id(ap_id) do
with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
{:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
already_ap <- User.ap_enabled?(user),
{:ok, user} <- upgrade_user(user, data) do
if not already_ap do
TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
end
{:ok, user} <- update_user(user, data) do
TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
{:ok, user}
else
%User{} = user -> {:ok, user}
@ -1267,9 +1263,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
end
defp upgrade_user(user, data) do
defp update_user(user, data) do
user
|> User.upgrade_changeset(data, true)
|> User.remote_user_changeset(data)
|> User.update_and_set_cache()
end

View file

@ -79,10 +79,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
emoji_tags = Transmogrifier.take_emoji_tags(user)
fields =
user
|> User.fields()
|> Enum.map(&Map.put(&1, "type", "PropertyValue"))
fields = Enum.map(user.fields, &Map.put(&1, "type", "PropertyValue"))
%{
"id" => user.ap_id,
@ -103,7 +100,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
},
"endpoints" => endpoints,
"attachment" => fields,
"tag" => (user.source_data["tag"] || []) ++ emoji_tags,
"tag" => emoji_tags,
"discoverable" => user.discoverable
}
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))

View file

@ -27,7 +27,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.Web.AdminAPI.Search
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Endpoint
alias Pleroma.Web.MastodonAPI.AppView
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.Router
require Logger
@ -914,16 +916,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end)
|> List.flatten()
response = %{configs: merged}
response =
if Restarter.Pleroma.need_reboot?() do
Map.put(response, :need_reboot, true)
else
response
end
json(conn, response)
json(conn, %{configs: merged, need_reboot: Restarter.Pleroma.need_reboot?()})
end
end
@ -950,28 +943,22 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
Config.TransferTask.load_and_update_env(deleted, false)
need_reboot? =
Restarter.Pleroma.need_reboot?() ||
Enum.any?(updated, fn config ->
if !Restarter.Pleroma.need_reboot?() do
changed_reboot_settings? =
(updated ++ deleted)
|> Enum.any?(fn config ->
group = ConfigDB.from_string(config.group)
key = ConfigDB.from_string(config.key)
value = ConfigDB.from_binary(config.value)
Config.TransferTask.pleroma_need_restart?(group, key, value)
end)
response = %{configs: updated}
response =
if need_reboot? do
Restarter.Pleroma.need_reboot()
Map.put(response, :need_reboot, need_reboot?)
else
response
end
if changed_reboot_settings?, do: Restarter.Pleroma.need_reboot()
end
conn
|> put_view(ConfigView)
|> render("index.json", response)
|> render("index.json", %{configs: updated, need_reboot: Restarter.Pleroma.need_reboot?()})
end
end
@ -983,6 +970,10 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end
end
def need_reboot(conn, _params) do
json(conn, %{need_reboot: Restarter.Pleroma.need_reboot?()})
end
defp configurable_from_database(conn) do
if Config.get(:configurable_from_database) do
:ok
@ -1028,6 +1019,83 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
conn |> json("")
end
def oauth_app_create(conn, params) do
params =
if params["name"] do
Map.put(params, "client_name", params["name"])
else
params
end
result =
case App.create(params) do
{:ok, app} ->
AppView.render("show.json", %{app: app, admin: true})
{:error, changeset} ->
App.errors(changeset)
end
json(conn, result)
end
def oauth_app_update(conn, params) do
params =
if params["name"] do
Map.put(params, "client_name", params["name"])
else
params
end
with {:ok, app} <- App.update(params) do
json(conn, AppView.render("show.json", %{app: app, admin: true}))
else
{:error, changeset} ->
json(conn, App.errors(changeset))
nil ->
json_response(conn, :bad_request, "")
end
end
def oauth_app_list(conn, params) do
{page, page_size} = page_params(params)
search_params = %{
client_name: params["name"],
client_id: params["client_id"],
page: page,
page_size: page_size
}
search_params =
if Map.has_key?(params, "trusted") do
Map.put(search_params, :trusted, params["trusted"])
else
search_params
end
with {:ok, apps, count} <- App.search(search_params) do
json(
conn,
AppView.render("index.json",
apps: apps,
count: count,
page_size: page_size,
admin: true
)
)
end
end
def oauth_app_delete(conn, params) do
with {:ok, _app} <- App.destroy(params["id"]) do
json_response(conn, :no_content, "")
else
_ -> json_response(conn, :bad_request, "")
end
end
def stats(conn, _) do
count = Stats.get_status_visibility_count()

View file

@ -4,7 +4,7 @@
defmodule Pleroma.Web.ApiSpec.Helpers do
def request_body(description, schema_ref, opts \\ []) do
media_types = ["application/json", "multipart/form-data"]
media_types = ["application/json", "multipart/form-data", "application/x-www-form-urlencoded"]
content =
media_types

View file

@ -332,26 +332,6 @@ defmodule Pleroma.Web.CommonAPI do
defp maybe_create_activity_expiration(result, _), do: result
# Updates the emojis for a user based on their profile
def update(user) do
emoji = emoji_from_profile(user)
source_data = Map.put(user.source_data, "tag", emoji)
user =
case User.update_source_data(user, source_data) do
{:ok, user} -> user
_ -> user
end
ActivityPub.update(%{
local: true,
to: [Pleroma.Constants.as_public(), user.follower_address],
cc: [],
actor: user.ap_id,
object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
})
end
def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do
with %Activity{
actor: ^user_ap_id,

View file

@ -10,7 +10,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Conversation.Participation
alias Pleroma.Emoji
alias Pleroma.Formatter
alias Pleroma.Object
alias Pleroma.Plugs.AuthenticationPlug
@ -18,7 +17,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Endpoint
alias Pleroma.Web.MediaProxy
require Logger
@ -175,7 +173,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
"replies" => %{"type" => "Collection", "totalItems" => 0}
}
{note, Map.merge(emoji, Emoji.Formatter.get_emoji_map(option))}
{note, Map.merge(emoji, Pleroma.Emoji.Formatter.get_emoji_map(option))}
end)
end_time =
@ -431,19 +429,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
end
def emoji_from_profile(%User{bio: bio, name: name}) do
[bio, name]
|> Enum.map(&Emoji.Formatter.get_emoji/1)
|> Enum.concat()
|> Enum.map(fn {shortcode, %Emoji{file: path}} ->
%{
"type" => "Emoji",
"icon" => %{"type" => "Image", "url" => "#{Endpoint.url()}#{path}"},
"name" => ":#{shortcode}:"
}
end)
end
def maybe_notify_to_recipients(
recipients,
%Activity{data: %{"to" => to, "type" => _type}} = _activity

View file

@ -23,7 +23,7 @@ defmodule Pleroma.Web.Feed.FeedView do
def pub_date(%DateTime{} = date), do: Timex.format!(date, "{RFC822}")
def prepare_activity(activity, opts \\ []) do
object = activity_object(activity)
object = Object.normalize(activity)
actor =
if opts[:actor] do
@ -33,7 +33,6 @@ defmodule Pleroma.Web.Feed.FeedView do
%{
activity: activity,
data: Map.get(object, :data),
object: object,
actor: actor
}
end
@ -68,9 +67,7 @@ defmodule Pleroma.Web.Feed.FeedView do
def last_activity(activities), do: List.last(activities)
def activity_object(activity), do: Object.normalize(activity)
def activity_title(%{data: %{"content" => content}}, opts \\ %{}) do
def activity_title(%{"content" => content}, opts \\ %{}) do
content
|> Pleroma.Web.Metadata.Utils.scrub_html()
|> Pleroma.Emoji.Formatter.demojify()
@ -78,7 +75,7 @@ defmodule Pleroma.Web.Feed.FeedView do
|> escape()
end
def activity_content(%{data: %{"content" => content}}) do
def activity_content(%{"content" => content}) do
content
|> String.replace(~r/[\n\r]/, "")
|> escape()

View file

@ -17,7 +17,7 @@ defmodule Pleroma.Web.MastoFEController do
when action == :index
)
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action != :index)
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action not in [:index, :manifest])
@doc "GET /web/*path"
def index(%{assigns: %{user: user, token: token}} = conn, _params)

View file

@ -21,10 +21,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.ListView
alias Pleroma.Web.MastodonAPI.MastodonAPI
alias Pleroma.Web.MastodonAPI.MastodonAPIController
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.TwitterAPI.TwitterAPI
plug(:skip_plug, OAuthScopesPlug when action == :identity_proofs)
plug(
OAuthScopesPlug,
%{fallback: :proceed_unauthenticated, scopes: ["read:accounts"]}
@ -101,6 +104,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|> Map.put("fullname", params["fullname"] || nickname)
|> Map.put("bio", params["bio"] || "")
|> Map.put("confirm", params["password"])
|> Map.put("trusted_app", app.trusted)
with :ok <- validate_email_param(params),
{:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
@ -146,9 +150,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "PATCH /api/v1/accounts/update_credentials"
def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
user = original_user
def update_credentials(%{assigns: %{user: user}} = conn, params) do
user_params =
[
:no_rich_text,
@ -184,8 +186,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
changeset = User.update_changeset(user, user_params)
with {:ok, user} <- User.update_and_set_cache(changeset) do
if original_user != user, do: CommonAPI.update(user)
render(conn, "show.json", user: user, for: user, with_pleroma_settings: true)
else
_e -> render_error(conn, :forbidden, "Invalid request")
@ -380,6 +380,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end
@doc "GET /api/v1/endorsements"
def endorsements(conn, params),
do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
def endorsements(conn, params), do: MastodonAPIController.empty_array(conn, params)
@doc "GET /api/v1/identity_proofs"
def identity_proofs(conn, params), do: MastodonAPIController.empty_array(conn, params)
end

View file

@ -3,21 +3,31 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
@moduledoc """
Contains stubs for unimplemented Mastodon API endpoints.
Note: instead of routing directly to this controller's action,
it's preferable to define an action in relevant (non-generic) controller,
set up OAuth rules for it and call this controller's function from it.
"""
use Pleroma.Web, :controller
require Logger
plug(:skip_plug, Pleroma.Plugs.OAuthScopesPlug when action in [:empty_array, :empty_object])
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
# Stubs for unimplemented mastodon api
#
def empty_array(conn, _) do
Logger.debug("Unimplemented, returning an empty array")
Logger.debug("Unimplemented, returning an empty array (list)")
json(conn, [])
end
def empty_object(conn, _) do
Logger.debug("Unimplemented, returning an empty object")
Logger.debug("Unimplemented, returning an empty object (map)")
json(conn, %{})
end
end

View file

@ -6,25 +6,22 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
@moduledoc "The module represents functions to manage user subscriptions."
use Pleroma.Web, :controller
alias Pleroma.Web.MastodonAPI.PushSubscriptionView, as: View
alias Pleroma.Web.Push
alias Pleroma.Web.Push.Subscription
action_fallback(:errors)
plug(Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["push"]})
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
plug(:restrict_push_enabled)
# Creates PushSubscription
# POST /api/v1/push/subscription
#
def create(%{assigns: %{user: user, token: token}} = conn, params) do
with true <- Push.enabled(),
{:ok, _} <- Subscription.delete_if_exists(user, token),
with {:ok, _} <- Subscription.delete_if_exists(user, token),
{:ok, subscription} <- Subscription.create(user, token, params) do
view = View.render("push_subscription.json", subscription: subscription)
json(conn, view)
render(conn, "show.json", subscription: subscription)
end
end
@ -32,10 +29,8 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
# GET /api/v1/push/subscription
#
def get(%{assigns: %{user: user, token: token}} = conn, _params) do
with true <- Push.enabled(),
{:ok, subscription} <- Subscription.get(user, token) do
view = View.render("push_subscription.json", subscription: subscription)
json(conn, view)
with {:ok, subscription} <- Subscription.get(user, token) do
render(conn, "show.json", subscription: subscription)
end
end
@ -43,10 +38,8 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
# PUT /api/v1/push/subscription
#
def update(%{assigns: %{user: user, token: token}} = conn, params) do
with true <- Push.enabled(),
{:ok, subscription} <- Subscription.update(user, token, params) do
view = View.render("push_subscription.json", subscription: subscription)
json(conn, view)
with {:ok, subscription} <- Subscription.update(user, token, params) do
render(conn, "show.json", subscription: subscription)
end
end
@ -54,11 +47,20 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do
# DELETE /api/v1/push/subscription
#
def delete(%{assigns: %{user: user, token: token}} = conn, _params) do
with true <- Push.enabled(),
{:ok, _response} <- Subscription.delete(user, token),
with {:ok, _response} <- Subscription.delete(user, token),
do: json(conn, %{})
end
defp restrict_push_enabled(conn, _) do
if Push.enabled() do
conn
else
conn
|> render_error(:forbidden, "Web push subscription is disabled on this Pleroma instance")
|> halt()
end
end
# fallback action
#
def errors(conn, {:error, :not_found}) do

View file

@ -5,10 +5,13 @@
defmodule Pleroma.Web.MastodonAPI.SuggestionController do
use Pleroma.Web, :controller
alias Pleroma.Plugs.OAuthScopesPlug
require Logger
plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :index)
@doc "GET /api/v1/suggestions"
def index(conn, _) do
json(conn, [])
end
def index(conn, params),
do: Pleroma.Web.MastodonAPI.MastodonAPIController.empty_array(conn, params)
end

View file

@ -181,13 +181,11 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
bot = user.actor_type in ["Application", "Service"]
emojis =
(user.source_data["tag"] || [])
|> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
|> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
Enum.map(user.emoji, fn {shortcode, url} ->
%{
"shortcode" => String.trim(name, ":"),
"url" => MediaProxy.url(url),
"static_url" => MediaProxy.url(url),
"shortcode" => shortcode,
"url" => url,
"static_url" => url,
"visible_in_picker" => false
}
end)

View file

@ -7,6 +7,21 @@ defmodule Pleroma.Web.MastodonAPI.AppView do
alias Pleroma.Web.OAuth.App
def render("index.json", %{apps: apps, count: count, page_size: page_size, admin: true}) do
%{
apps: render_many(apps, Pleroma.Web.MastodonAPI.AppView, "show.json", %{admin: true}),
count: count,
page_size: page_size
}
end
def render("show.json", %{admin: true, app: %App{} = app} = assigns) do
"show.json"
|> render(Map.delete(assigns, :admin))
|> Map.put(:trusted, app.trusted)
|> Map.put(:id, app.id)
end
def render("show.json", %{app: %App{} = app}) do
%{
id: app.id |> to_string,

View file

@ -117,14 +117,14 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
# Note: :skip_relationships option being applied to _account_ rendering (here)
put_target(response, activity, reading_user, render_opts)
"follow" ->
response
"pleroma:emoji_reaction" ->
response
|> put_status(parent_activity_fn.(), reading_user, render_opts)
|> put_emoji(activity)
type when type in ["follow", "follow_request"] ->
response
_ ->
nil
end

View file

@ -2,11 +2,11 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.PushSubscriptionView do
defmodule Pleroma.Web.MastodonAPI.SubscriptionView do
use Pleroma.Web, :view
alias Pleroma.Web.Push
def render("push_subscription.json", %{subscription: subscription}) do
def render("show.json", %{subscription: subscription}) do
%{
id: to_string(subscription.id),
endpoint: subscription.endpoint,

View file

@ -5,6 +5,7 @@
defmodule Pleroma.Web.OAuth.App do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
alias Pleroma.Repo
@type t :: %__MODULE__{}
@ -16,14 +17,24 @@ defmodule Pleroma.Web.OAuth.App do
field(:website, :string)
field(:client_id, :string)
field(:client_secret, :string)
field(:trusted, :boolean, default: false)
has_many(:oauth_authorizations, Pleroma.Web.OAuth.Authorization, on_delete: :delete_all)
has_many(:oauth_tokens, Pleroma.Web.OAuth.Token, on_delete: :delete_all)
timestamps()
end
@spec changeset(App.t(), map()) :: Ecto.Changeset.t()
def changeset(struct, params) do
cast(struct, params, [:client_name, :redirect_uris, :scopes, :website, :trusted])
end
@spec register_changeset(App.t(), map()) :: Ecto.Changeset.t()
def register_changeset(struct, params \\ %{}) do
changeset =
struct
|> cast(params, [:client_name, :redirect_uris, :scopes, :website])
|> changeset(params)
|> validate_required([:client_name, :redirect_uris, :scopes])
if changeset.valid? do
@ -41,6 +52,21 @@ defmodule Pleroma.Web.OAuth.App do
end
end
@spec create(map()) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
def create(params) do
with changeset <- __MODULE__.register_changeset(%__MODULE__{}, params) do
Repo.insert(changeset)
end
end
@spec update(map()) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
def update(params) do
with %__MODULE__{} = app <- Repo.get(__MODULE__, params["id"]),
changeset <- changeset(app, params) do
Repo.update(changeset)
end
end
@doc """
Gets app by attrs or create new with attrs.
And updates the scopes if need.
@ -65,4 +91,58 @@ defmodule Pleroma.Web.OAuth.App do
|> change(%{scopes: scopes})
|> Repo.update()
end
@spec search(map()) :: {:ok, [App.t()], non_neg_integer()}
def search(params) do
query = from(a in __MODULE__)
query =
if params[:client_name] do
from(a in query, where: a.client_name == ^params[:client_name])
else
query
end
query =
if params[:client_id] do
from(a in query, where: a.client_id == ^params[:client_id])
else
query
end
query =
if Map.has_key?(params, :trusted) do
from(a in query, where: a.trusted == ^params[:trusted])
else
query
end
query =
from(u in query,
limit: ^params[:page_size],
offset: ^((params[:page] - 1) * params[:page_size])
)
count = Repo.aggregate(__MODULE__, :count, :id)
{:ok, Repo.all(query), count}
end
@spec destroy(pos_integer()) :: {:ok, App.t()} | {:error, Ecto.Changeset.t()}
def destroy(id) do
with %__MODULE__{} = app <- Repo.get(__MODULE__, id) do
Repo.delete(app)
end
end
@spec errors(Ecto.Changeset.t()) :: map()
def errors(changeset) do
Enum.reduce(changeset.errors, %{}, fn
{:client_name, {error, _}}, acc ->
Map.put(acc, :name, error)
{key, {error, _}}, acc ->
Map.put(acc, key, error)
end)
end
end

View file

@ -27,6 +27,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do
plug(:fetch_flash)
plug(RateLimiter, [name: :authentication] when action == :create_authorization)
plug(:skip_plug, Pleroma.Plugs.OAuthScopesPlug)
action_fallback(Pleroma.Web.OAuth.FallbackController)
@oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob"

View file

@ -13,7 +13,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
alias Pleroma.Plugs.RateLimiter
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.StatusView
require Pleroma.Constants
@ -58,38 +57,32 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
@doc "PATCH /api/v1/pleroma/accounts/update_avatar"
def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
{:ok, user} =
{:ok, _user} =
user
|> Changeset.change(%{avatar: nil})
|> User.update_and_set_cache()
CommonAPI.update(user)
json(conn, %{url: nil})
end
def update_avatar(%{assigns: %{user: user}} = conn, params) do
{:ok, %{data: data}} = ActivityPub.upload(params, type: :avatar)
{:ok, user} = user |> Changeset.change(%{avatar: data}) |> User.update_and_set_cache()
{:ok, _user} = user |> Changeset.change(%{avatar: data}) |> User.update_and_set_cache()
%{"url" => [%{"href" => href} | _]} = data
CommonAPI.update(user)
json(conn, %{url: href})
end
@doc "PATCH /api/v1/pleroma/accounts/update_banner"
def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
with {:ok, user} <- User.update_banner(user, %{}) do
CommonAPI.update(user)
with {:ok, _user} <- User.update_banner(user, %{}) do
json(conn, %{url: nil})
end
end
def update_banner(%{assigns: %{user: user}} = conn, params) do
with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
{:ok, user} <- User.update_banner(user, object.data) do
CommonAPI.update(user)
{:ok, _user} <- User.update_banner(user, object.data) do
%{"url" => [%{"href" => href} | _]} = object.data
json(conn, %{url: href})

View file

@ -34,7 +34,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
plug(
OAuthScopesPlug,
%{scopes: ["write:conversations"]} when action == :update_conversation
%{scopes: ["write:conversations"]} when action in [:update_conversation, :read_conversations]
)
plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :read_notification)

View file

@ -16,6 +16,8 @@ defmodule Pleroma.Web.Push.Impl do
require Logger
import Ecto.Query
defdelegate mastodon_notification_type(activity), to: Activity
@types ["Create", "Follow", "Announce", "Like", "Move"]
@doc "Performs sending notifications for user subscriptions"
@ -24,32 +26,32 @@ defmodule Pleroma.Web.Push.Impl do
%{
activity: %{data: %{"type" => activity_type}} = activity,
user: %User{id: user_id}
} = notif
} = notification
)
when activity_type in @types do
actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
actor = User.get_cached_by_ap_id(notification.activity.data["actor"])
type = Activity.mastodon_notification_type(notif.activity)
mastodon_type = mastodon_notification_type(notification.activity)
gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key)
avatar_url = User.avatar_url(actor)
object = Object.normalize(activity)
user = User.get_cached_by_id(user_id)
direct_conversation_id = Activity.direct_conversation_id(activity, user)
for subscription <- fetch_subsriptions(user_id),
get_in(subscription.data, ["alerts", type]) do
for subscription <- fetch_subscriptions(user_id),
Subscription.enabled?(subscription, mastodon_type) do
%{
access_token: subscription.token.token,
notification_id: notif.id,
notification_type: type,
notification_id: notification.id,
notification_type: mastodon_type,
icon: avatar_url,
preferred_locale: "en",
pleroma: %{
activity_id: notif.activity.id,
activity_id: notification.activity.id,
direct_conversation_id: direct_conversation_id
}
}
|> Map.merge(build_content(notif, actor, object))
|> Map.merge(build_content(notification, actor, object, mastodon_type))
|> Jason.encode!()
|> push_message(build_sub(subscription), gcm_api_key, subscription)
end
@ -82,7 +84,7 @@ defmodule Pleroma.Web.Push.Impl do
end
@doc "Gets user subscriptions"
def fetch_subsriptions(user_id) do
def fetch_subscriptions(user_id) do
Subscription
|> where(user_id: ^user_id)
|> preload(:token)
@ -99,28 +101,36 @@ defmodule Pleroma.Web.Push.Impl do
}
end
def build_content(notification, actor, object, mastodon_type \\ nil)
def build_content(
%{
activity: %{data: %{"directMessage" => true}},
user: %{notification_settings: %{privacy_option: true}}
},
actor,
_
_object,
_mastodon_type
) do
%{title: "New Direct Message", body: "@#{actor.nickname}"}
end
def build_content(notif, actor, object) do
def build_content(notification, actor, object, mastodon_type) do
mastodon_type = mastodon_type || mastodon_notification_type(notification.activity)
%{
title: format_title(notif),
body: format_body(notif, actor, object)
title: format_title(notification, mastodon_type),
body: format_body(notification, actor, object, mastodon_type)
}
end
def format_body(activity, actor, object, mastodon_type \\ nil)
def format_body(
%{activity: %{data: %{"type" => "Create"}}},
actor,
%{data: %{"content" => content}}
%{data: %{"content" => content}},
_mastodon_type
) do
"@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}"
end
@ -128,33 +138,44 @@ defmodule Pleroma.Web.Push.Impl do
def format_body(
%{activity: %{data: %{"type" => "Announce"}}},
actor,
%{data: %{"content" => content}}
%{data: %{"content" => content}},
_mastodon_type
) do
"@#{actor.nickname} repeated: #{Utils.scrub_html_and_truncate(content, 80)}"
end
def format_body(
%{activity: %{data: %{"type" => type}}},
%{activity: %{data: %{"type" => type}}} = notification,
actor,
_object
_object,
mastodon_type
)
when type in ["Follow", "Like"] do
case type do
"Follow" -> "@#{actor.nickname} has followed you"
"Like" -> "@#{actor.nickname} has favorited your post"
mastodon_type = mastodon_type || mastodon_notification_type(notification.activity)
case mastodon_type do
"follow" -> "@#{actor.nickname} has followed you"
"follow_request" -> "@#{actor.nickname} has requested to follow you"
"favourite" -> "@#{actor.nickname} has favorited your post"
end
end
def format_title(%{activity: %{data: %{"directMessage" => true}}}) do
def format_title(activity, mastodon_type \\ nil)
def format_title(%{activity: %{data: %{"directMessage" => true}}}, _mastodon_type) do
"New Direct Message"
end
def format_title(%{activity: %{data: %{"type" => type}}}) do
case type do
"Create" -> "New Mention"
"Follow" -> "New Follower"
"Announce" -> "New Repeat"
"Like" -> "New Favorite"
def format_title(%{activity: activity}, mastodon_type) do
mastodon_type = mastodon_type || mastodon_notification_type(activity)
case mastodon_type do
"mention" -> "New Mention"
"follow" -> "New Follower"
"follow_request" -> "New Follow Request"
"reblog" -> "New Repeat"
"favourite" -> "New Favorite"
type -> "New #{String.capitalize(type || "event")}"
end
end
end

View file

@ -32,6 +32,14 @@ defmodule Pleroma.Web.Push.Subscription do
%{"alerts" => alerts}
end
def enabled?(subscription, "follow_request") do
enabled?(subscription, "follow")
end
def enabled?(subscription, alert_type) do
get_in(subscription.data, ["alerts", alert_type])
end
def create(
%User{} = user,
%Token{} = token,

View file

@ -16,78 +16,60 @@ defmodule Pleroma.Web.Router do
plug(Pleroma.Plugs.UserEnabledPlug)
end
pipeline :api do
plug(:accepts, ["json"])
plug(:fetch_session)
pipeline :authenticate do
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Plugs.UserFetcherPlug)
plug(Pleroma.Plugs.SessionAuthenticationPlug)
plug(Pleroma.Plugs.LegacyAuthenticationPlug)
plug(Pleroma.Plugs.AuthenticationPlug)
end
pipeline :after_auth do
plug(Pleroma.Plugs.UserEnabledPlug)
plug(Pleroma.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Plugs.EnsureUserKeyPlug)
plug(Pleroma.Plugs.IdempotencyPlug)
end
pipeline :base_api do
plug(:accepts, ["json"])
plug(:fetch_session)
plug(:authenticate)
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
end
pipeline :api do
plug(:base_api)
plug(:after_auth)
plug(Pleroma.Plugs.IdempotencyPlug)
end
pipeline :authenticated_api do
plug(:accepts, ["json"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Plugs.UserFetcherPlug)
plug(Pleroma.Plugs.SessionAuthenticationPlug)
plug(Pleroma.Plugs.LegacyAuthenticationPlug)
plug(Pleroma.Plugs.AuthenticationPlug)
plug(Pleroma.Plugs.UserEnabledPlug)
plug(Pleroma.Plugs.SetUserSessionIdPlug)
plug(:base_api)
plug(Pleroma.Plugs.AuthExpectedPlug)
plug(:after_auth)
plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
plug(Pleroma.Plugs.IdempotencyPlug)
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
end
pipeline :admin_api do
plug(:accepts, ["json"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Plugs.UserFetcherPlug)
plug(Pleroma.Plugs.SessionAuthenticationPlug)
plug(Pleroma.Plugs.LegacyAuthenticationPlug)
plug(Pleroma.Plugs.AuthenticationPlug)
plug(:base_api)
plug(Pleroma.Plugs.AdminSecretAuthenticationPlug)
plug(Pleroma.Plugs.UserEnabledPlug)
plug(Pleroma.Plugs.SetUserSessionIdPlug)
plug(:after_auth)
plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
plug(Pleroma.Plugs.UserIsAdminPlug)
plug(Pleroma.Plugs.IdempotencyPlug)
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
end
pipeline :mastodon_html do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Plugs.UserFetcherPlug)
plug(Pleroma.Plugs.SessionAuthenticationPlug)
plug(Pleroma.Plugs.LegacyAuthenticationPlug)
plug(Pleroma.Plugs.AuthenticationPlug)
plug(Pleroma.Plugs.UserEnabledPlug)
plug(Pleroma.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Plugs.EnsureUserKeyPlug)
plug(:browser)
plug(:authenticate)
plug(:after_auth)
end
pipeline :pleroma_html do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Plugs.UserFetcherPlug)
plug(Pleroma.Plugs.SessionAuthenticationPlug)
plug(Pleroma.Plugs.AuthenticationPlug)
plug(:browser)
plug(:authenticate)
plug(Pleroma.Plugs.EnsureUserKeyPlug)
end
@ -203,12 +185,18 @@ defmodule Pleroma.Web.Router do
get("/config", AdminAPIController, :config_show)
post("/config", AdminAPIController, :config_update)
get("/config/descriptions", AdminAPIController, :config_descriptions)
get("/need_reboot", AdminAPIController, :need_reboot)
get("/restart", AdminAPIController, :restart)
get("/moderation_log", AdminAPIController, :list_log)
post("/reload_emoji", AdminAPIController, :reload_emoji)
get("/stats", AdminAPIController, :stats)
get("/oauth_app", AdminAPIController, :oauth_app_list)
post("/oauth_app", AdminAPIController, :oauth_app_create)
patch("/oauth_app/:id", AdminAPIController, :oauth_app_update)
delete("/oauth_app/:id", AdminAPIController, :oauth_app_delete)
end
scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do
@ -338,7 +326,7 @@ defmodule Pleroma.Web.Router do
get("/accounts/relationships", AccountController, :relationships)
get("/accounts/:id/lists", AccountController, :lists)
get("/accounts/:id/identity_proofs", MastodonAPIController, :empty_array)
get("/accounts/:id/identity_proofs", AccountController, :identity_proofs)
get("/follow_requests", FollowRequestController, :index)
get("/blocks", AccountController, :blocks)
@ -508,7 +496,7 @@ defmodule Pleroma.Web.Router do
end
scope "/api" do
pipe_through(:api)
pipe_through(:base_api)
get("/openapi", OpenApiSpex.Plug.RenderSpec, [])
end
@ -522,10 +510,6 @@ defmodule Pleroma.Web.Router do
post("/qvitter/statuses/notifications/read", TwitterAPI.Controller, :notifications_read)
end
pipeline :ap_service_actor do
plug(:accepts, ["activity+json", "json"])
end
pipeline :ostatus do
plug(:accepts, ["html", "xml", "rss", "atom", "activity+json", "json"])
plug(Pleroma.Plugs.StaticFEPlug)
@ -536,8 +520,7 @@ defmodule Pleroma.Web.Router do
end
scope "/", Pleroma.Web do
pipe_through(:ostatus)
pipe_through(:http_signature)
pipe_through([:ostatus, :http_signature])
get("/objects/:uuid", OStatus.OStatusController, :object)
get("/activities/:uuid", OStatus.OStatusController, :activity)
@ -555,13 +538,6 @@ defmodule Pleroma.Web.Router do
get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe)
end
# Server to Server (S2S) AP interactions
pipeline :activitypub do
plug(:accepts, ["activity+json", "json"])
plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)
end
scope "/", Pleroma.Web.ActivityPub do
# XXX: not really ostatus
pipe_through(:ostatus)
@ -569,19 +545,22 @@ defmodule Pleroma.Web.Router do
get("/users/:nickname/outbox", ActivityPubController, :outbox)
end
pipeline :ap_service_actor do
plug(:accepts, ["activity+json", "json"])
end
# Server to Server (S2S) AP interactions
pipeline :activitypub do
plug(:ap_service_actor)
plug(:http_signature)
end
# Client to Server (C2S) AP interactions
pipeline :activitypub_client do
plug(:accepts, ["activity+json", "json"])
plug(:ap_service_actor)
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Plugs.UserFetcherPlug)
plug(Pleroma.Plugs.SessionAuthenticationPlug)
plug(Pleroma.Plugs.LegacyAuthenticationPlug)
plug(Pleroma.Plugs.AuthenticationPlug)
plug(Pleroma.Plugs.UserEnabledPlug)
plug(Pleroma.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Plugs.EnsureUserKeyPlug)
plug(:authenticate)
plug(:after_auth)
end
scope "/", Pleroma.Web.ActivityPub do
@ -653,12 +632,7 @@ defmodule Pleroma.Web.Router do
get("/web/*path", MastoFEController, :index)
end
pipeline :remote_media do
end
scope "/proxy/", Pleroma.Web.MediaProxy do
pipe_through(:remote_media)
get("/:sig/:url", MediaProxyController, :remote)
get("/:sig/:url/:filename", MediaProxyController, :remote)
end
@ -671,6 +645,17 @@ defmodule Pleroma.Web.Router do
end
end
# Test-only routes needed to test action dispatching and plug chain execution
if Pleroma.Config.get(:env) == :test do
scope "/test/authenticated_api", Pleroma.Tests do
pipe_through(:authenticated_api)
for action <- [:skipped_oauth, :performed_oauth, :missed_oauth] do
get("/#{action}", OAuthTestController, action)
end
end
end
scope "/", Pleroma.Web.MongooseIM do
get("/user_exists", MongooseIMController, :user_exists)
get("/check_password", MongooseIMController, :check_password)

View file

@ -18,15 +18,6 @@ defmodule Pleroma.Web.StaticFE.StaticFEView do
@media_types ["image", "audio", "video"]
def emoji_for_user(%User{} = user) do
user.source_data
|> Map.get("tag", [])
|> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
|> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
{String.trim(name, ":"), url}
end)
end
def fetch_media_type(%{"mediaType" => mediaType}) do
Utils.fetch_media_type(@media_types, mediaType)
end

View file

@ -2,10 +2,10 @@
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<id><%= @data["id"] %></id>
<title><%= activity_title(@object, Keyword.get(@feed_config, :post_title, %{})) %></title>
<content type="html"><%= activity_content(@object) %></content>
<published><%= @data["published"] %></published>
<updated><%= @data["published"] %></updated>
<title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title>
<content type="html"><%= activity_content(@data) %></content>
<published><%= @activity.data["published"] %></published>
<updated><%= @activity.data["published"] %></updated>
<ostatus:conversation ref="<%= activity_context(@activity) %>">
<%= activity_context(@activity) %>
</ostatus:conversation>

View file

@ -2,10 +2,10 @@
<activity:object-type>http://activitystrea.ms/schema/1.0/note</activity:object-type>
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
<guid><%= @data["id"] %></guid>
<title><%= activity_title(@object, Keyword.get(@feed_config, :post_title, %{})) %></title>
<description><%= activity_content(@object) %></description>
<pubDate><%= @data["published"] %></pubDate>
<updated><%= @data["published"] %></updated>
<title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title>
<description><%= activity_content(@data) %></description>
<pubDate><%= @activity.data["published"] %></pubDate>
<updated><%= @activity.data["published"] %></updated>
<ostatus:conversation ref="<%= activity_context(@activity) %>">
<%= activity_context(@activity) %>
</ostatus:conversation>

View file

@ -5,8 +5,8 @@
<%= render @view_module, "_tag_author.atom", assigns %>
<id><%= @data["id"] %></id>
<title><%= activity_title(@object, Keyword.get(@feed_config, :post_title, %{})) %></title>
<content type="html"><%= activity_content(@object) %></content>
<title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title>
<content type="html"><%= activity_content(@data) %></content>
<%= if @activity.local do %>
<link type="application/atom+xml" href='<%= @data["id"] %>' rel="self"/>
@ -15,8 +15,8 @@
<link type="text/html" href='<%= @data["external_url"] %>' rel="alternate"/>
<% end %>
<published><%= @data["published"] %></published>
<updated><%= @data["published"] %></updated>
<published><%= @activity.data["published"] %></published>
<updated><%= @activity.data["published"] %></updated>
<ostatus:conversation ref="<%= activity_context(@activity) %>">
<%= activity_context(@activity) %>

View file

@ -1,15 +1,14 @@
<item>
<title><%= activity_title(@object, Keyword.get(@feed_config, :post_title, %{})) %></title>
<title><%= activity_title(@data, Keyword.get(@feed_config, :post_title, %{})) %></title>
<guid isPermalink="true"><%= activity_context(@activity) %></guid>
<link><%= activity_context(@activity) %></link>
<pubDate><%= pub_date(@data["published"]) %></pubDate>
<pubDate><%= pub_date(@activity.data["published"]) %></pubDate>
<description><%= activity_content(@object) %></description>
<description><%= activity_content(@data) %></description>
<%= for attachment <- @data["attachment"] || [] do %>
<enclosure url="<%= attachment_href(attachment) %>" type="<%= attachment_type(attachment) %>"/>
<% end %>
</item>

View file

@ -4,7 +4,7 @@
<img src="<%= User.avatar_url(@user) |> MediaProxy.url %>" width="48" height="48" alt="">
</div>
<span class="display-name">
<bdi><%= raw (@user.name |> Formatter.emojify(emoji_for_user(@user))) %></bdi>
<bdi><%= raw Formatter.emojify(@user.name, @user.emoji) %></bdi>
<span class="nickname"><%= @user.nickname %></span>
</span>
</a>

View file

@ -7,7 +7,7 @@
<input type="hidden" name="profile" value="">
<button type="submit" class="collapse">Remote follow</button>
</form>
<%= raw Formatter.emojify(@user.name, emoji_for_user(@user)) %> |
<%= raw Formatter.emojify(@user.name, @user.emoji) %> |
<%= link "@#{@user.nickname}@#{Endpoint.host()}", to: (@user.uri || @user.ap_id) %>
</h3>
<p><%= raw @user.bio %></p>

View file

@ -13,6 +13,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
def register_user(params, opts \\ []) do
token = params["token"]
trusted_app? = params["trusted_app"]
params = %{
nickname: params["nickname"],
@ -29,7 +30,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
captcha_enabled = Pleroma.Config.get([Pleroma.Captcha, :enabled])
# true if captcha is disabled or enabled and valid, false otherwise
captcha_ok =
if not captcha_enabled do
if trusted_app? || not captcha_enabled do
:ok
else
Pleroma.Captcha.validate(

View file

@ -15,6 +15,8 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :notifications_read)
plug(:skip_plug, OAuthScopesPlug when action in [:oauth_tokens, :revoke_token])
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
action_fallback(:errors)

View file

@ -29,11 +29,45 @@ defmodule Pleroma.Web do
import Pleroma.Web.Router.Helpers
import Pleroma.Web.TranslationHelpers
alias Pleroma.Plugs.PlugHelper
plug(:set_put_layout)
defp set_put_layout(conn, _) do
put_layout(conn, Pleroma.Config.get(:app_layout, "app.html"))
end
# Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain
defp skip_plug(conn, plug_module) do
try do
plug_module.skip_plug(conn)
rescue
UndefinedFunctionError ->
raise "#{plug_module} is not skippable. Append `use Pleroma.Web, :plug` to its code."
end
end
# Executed just before actual controller action, invokes before-action hooks (callbacks)
defp action(conn, params) do
with %Plug.Conn{halted: false} <- maybe_halt_on_missing_oauth_scopes_check(conn) do
super(conn, params)
end
end
# Halts if authenticated API action neither performs nor explicitly skips OAuth scopes check
defp maybe_halt_on_missing_oauth_scopes_check(conn) do
if Pleroma.Plugs.AuthExpectedPlug.auth_expected?(conn) &&
not PlugHelper.plug_called_or_skipped?(conn, Pleroma.Plugs.OAuthScopesPlug) do
conn
|> render_error(
:forbidden,
"Security violation: OAuth scopes check was neither handled nor explicitly skipped."
)
|> halt()
else
conn
end
end
end
end
@ -96,6 +130,35 @@ defmodule Pleroma.Web do
end
end
def plug do
quote do
alias Pleroma.Plugs.PlugHelper
@doc """
Marks a plug intentionally skipped and blocks its execution if it's present in plugs chain.
"""
def skip_plug(conn) do
PlugHelper.append_to_private_list(
conn,
PlugHelper.skipped_plugs_list_id(),
__MODULE__
)
end
@impl Plug
@doc "If marked as skipped, returns `conn`, and calls `perform/2` otherwise."
def call(%Plug.Conn{} = conn, options) do
if PlugHelper.plug_skipped?(conn, __MODULE__) do
conn
else
conn
|> PlugHelper.append_to_private_list(PlugHelper.called_plugs_list_id(), __MODULE__)
|> perform(options)
end
end
end
end
@doc """
When used, dispatch to the appropriate controller/view/etc.
"""

33
mix.exs
View file

@ -183,7 +183,7 @@ defmodule Pleroma.Mixfile do
{:flake_id, "~> 0.1.0"},
{:remote_ip,
git: "https://git.pleroma.social/pleroma/remote_ip.git",
ref: "825dc00aaba5a1b7c4202a532b696b595dd3bcb3"},
ref: "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8"},
{:captcha,
git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git",
ref: "e0f16822d578866e186a0974d65ad58cddc1e2ab"},
@ -221,19 +221,26 @@ defmodule Pleroma.Mixfile do
identifier_filter = ~r/[^0-9a-z\-]+/i
# Pre-release version, denoted from patch version with a hyphen
{tag, tag_err} =
System.cmd("git", ["describe", "--tags", "--abbrev=0"], stderr_to_stdout: true)
{describe, describe_err} = System.cmd("git", ["describe", "--tags", "--abbrev=8"])
{commit_hash, commit_hash_err} = System.cmd("git", ["rev-parse", "--short", "HEAD"])
git_pre_release =
with {tag, 0} <-
System.cmd("git", ["describe", "--tags", "--abbrev=0"], stderr_to_stdout: true),
{describe, 0} <- System.cmd("git", ["describe", "--tags", "--abbrev=8"]) do
describe
|> String.trim()
|> String.replace(String.trim(tag), "")
|> String.trim_leading("-")
|> String.trim()
else
_ ->
{commit_hash, 0} = System.cmd("git", ["rev-parse", "--short", "HEAD"])
cond do
tag_err == 0 and describe_err == 0 ->
describe
|> String.trim()
|> String.replace(String.trim(tag), "")
|> String.trim_leading("-")
|> String.trim()
commit_hash_err == 0 ->
"0-g" <> String.trim(commit_hash)
true ->
""
end
# Branch name as pre-release version component, denoted with a dot
@ -251,6 +258,8 @@ defmodule Pleroma.Mixfile do
|> String.replace(identifier_filter, "-")
branch_name
else
_ -> "stable"
end
build_name =

View file

@ -97,7 +97,7 @@
"quack": {:hex, :quack, "0.1.1", "cca7b4da1a233757fdb44b3334fce80c94785b3ad5a602053b7a002b5a8967bf", [:mix], [{:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: false]}, {:tesla, "~> 1.2.0", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "d736bfa7444112eb840027bb887832a0e403a4a3437f48028c3b29a2dbbd2543"},
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
"recon": {:hex, :recon, "2.5.0", "2f7fcbec2c35034bade2f9717f77059dc54eb4e929a3049ca7ba6775c0bd66cd", [:mix, :rebar3], [], "hexpm", "72f3840fedd94f06315c523f6cecf5b4827233bed7ae3fe135b2a0ebeab5e196"},
"remote_ip": {:git, "https://git.pleroma.social/pleroma/remote_ip.git", "825dc00aaba5a1b7c4202a532b696b595dd3bcb3", [ref: "825dc00aaba5a1b7c4202a532b696b595dd3bcb3"]},
"remote_ip": {:git, "https://git.pleroma.social/pleroma/remote_ip.git", "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8", [ref: "b647d0deecaa3acb140854fe4bda5b7e1dc6d1c8"]},
"sleeplocks": {:hex, :sleeplocks, "1.1.1", "3d462a0639a6ef36cc75d6038b7393ae537ab394641beb59830a1b8271faeed3", [:rebar3], [], "hexpm", "84ee37aeff4d0d92b290fff986d6a95ac5eedf9b383fadfd1d88e9b84a1c02e1"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"},
"sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm", "2e1ec458f892ffa81f9f8386e3f35a1af6db7a7a37748a64478f13163a1f3573"},

View file

@ -0,0 +1,9 @@
defmodule Pleroma.Repo.Migrations.AddTrustedToApps do
use Ecto.Migration
def change do
alter table(:apps) do
add(:trusted, :boolean, default: false)
end
end
end

View file

@ -0,0 +1,17 @@
defmodule Pleroma.Repo.Migrations.UsersAddPublicKey do
use Ecto.Migration
def up do
alter table(:users) do
add_if_not_exists(:public_key, :text)
end
execute("UPDATE users SET public_key = source_data->'publicKey'->>'publicKeyPem'")
end
def down do
alter table(:users) do
remove_if_exists(:public_key, :text)
end
end
end

View file

@ -0,0 +1,20 @@
defmodule Pleroma.Repo.Migrations.UsersAddInboxes do
use Ecto.Migration
def up do
alter table(:users) do
add_if_not_exists(:inbox, :text)
add_if_not_exists(:shared_inbox, :text)
end
execute("UPDATE users SET inbox = source_data->>'inbox'")
execute("UPDATE users SET shared_inbox = source_data->'endpoints'->>'sharedInbox'")
end
def down do
alter table(:users) do
remove_if_exists(:inbox, :text)
remove_if_exists(:shared_inbox, :text)
end
end
end

View file

@ -0,0 +1,38 @@
defmodule Pleroma.Repo.Migrations.UsersPopulateEmoji do
use Ecto.Migration
import Ecto.Query
alias Pleroma.User
alias Pleroma.Repo
def up do
execute("ALTER TABLE users ALTER COLUMN emoji SET DEFAULT '{}'::jsonb")
execute("UPDATE users SET emoji = DEFAULT WHERE emoji = '[]'::jsonb")
from(u in User)
|> select([u], struct(u, [:id, :ap_id, :source_data]))
|> Repo.stream()
|> Enum.each(fn user ->
emoji =
user.source_data
|> Map.get("tag", [])
|> Enum.filter(fn
%{"type" => "Emoji"} -> true
_ -> false
end)
|> Enum.reduce(%{}, fn %{"icon" => %{"url" => url}, "name" => name}, acc ->
Map.put(acc, String.trim(name, ":"), url)
end)
user
|> Ecto.Changeset.cast(%{emoji: emoji}, [:emoji])
|> Repo.update()
end)
end
def down do
execute("ALTER TABLE users ALTER COLUMN emoji SET DEFAULT '[]'::jsonb")
execute("UPDATE users SET emoji = DEFAULT WHERE emoji = '{}'::jsonb")
end
end

View file

@ -0,0 +1,15 @@
defmodule Pleroma.Repo.Migrations.UsersRemoveSourceData do
use Ecto.Migration
def up do
alter table(:users) do
remove_if_exists(:source_data, :map)
end
end
def down do
alter table(:users) do
add_if_not_exists(:source_data, :map, default: %{})
end
end
end

View file

@ -1,126 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>Copyright (C) 2019 by original authors @ fontello.com</metadata>
<defs>
<font id="fontello" horiz-adv-x="1000" >
<font-face font-family="fontello" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="857" descent="-143" />
<missing-glyph horiz-adv-x="1000" />
<glyph glyph-name="cancel" unicode="&#xe800;" d="M724 119q0-22-15-38l-76-76q-16-15-38-15t-38 15l-164 165-164-165q-16-15-38-15t-38 15l-76 76q-16 16-16 38t16 38l164 164-164 164q-16 16-16 38t16 38l76 76q16 16 38 16t38-16l164-164 164 164q16 16 38 16t38-16l76-76q15-15 15-38t-15-38l-164-164 164-164q15-15 15-38z" horiz-adv-x="785.7" />
<glyph glyph-name="upload" unicode="&#xe801;" d="M714 36q0 14-10 25t-25 10-25-10-11-25 11-25 25-11 25 11 10 25z m143 0q0 14-10 25t-26 10-25-10-10-25 10-25 25-11 26 11 10 25z m72 125v-179q0-22-16-38t-38-16h-821q-23 0-38 16t-16 38v179q0 22 16 38t38 15h238q12-31 39-51t62-20h143q34 0 61 20t40 51h238q22 0 38-15t16-38z m-182 361q-9-22-33-22h-143v-250q0-15-10-25t-25-11h-143q-15 0-25 11t-11 25v250h-143q-23 0-33 22-9 22 8 39l250 250q10 10 25 10t25-10l250-250q18-17 8-39z" horiz-adv-x="928.6" />
<glyph glyph-name="star" unicode="&#xe802;" d="M929 496q0-12-15-27l-202-197 48-279q0-4 0-12 0-11-6-19t-17-9q-10 0-22 7l-251 132-250-132q-12-7-23-7-11 0-17 9t-6 19q0 4 1 12l48 279-203 197q-14 15-14 27 0 21 31 26l280 40 126 254q11 23 27 23t28-23l125-254 280-40q32-5 32-26z" horiz-adv-x="928.6" />
<glyph glyph-name="star-empty" unicode="&#xe803;" d="M635 297l170 166-235 34-106 213-105-213-236-34 171-166-41-235 211 111 211-111z m294 199q0-12-15-27l-202-197 48-279q0-4 0-12 0-28-23-28-10 0-22 7l-251 132-250-132q-12-7-23-7-11 0-17 9t-6 19q0 4 1 12l48 279-203 197q-14 15-14 27 0 21 31 26l280 40 126 254q11 23 27 23t28-23l125-254 280-40q32-5 32-26z" horiz-adv-x="928.6" />
<glyph glyph-name="retweet" unicode="&#xe804;" d="M714 18q0-7-5-13t-13-5h-535q-5 0-8 1t-5 4-3 4-2 7 0 6v335h-107q-15 0-25 11t-11 25q0 13 8 23l179 214q11 12 27 12t28-12l178-214q9-10 9-23 0-15-11-25t-25-11h-107v-214h321q9 0 14-6l89-108q4-5 4-11z m357 232q0-13-8-23l-178-214q-12-13-28-13t-27 13l-179 214q-8 10-8 23 0 14 11 25t25 11h107v214h-322q-9 0-14 7l-89 107q-4 5-4 11 0 7 5 12t13 6h536q4 0 7-1t5-4 3-5 2-6 1-7v-334h107q14 0 25-11t10-25z" horiz-adv-x="1071.4" />
<glyph glyph-name="eye-off" unicode="&#xe805;" d="M310 112l43 79q-48 35-76 88t-27 114q0 67 34 125-128-65-213-197 94-144 239-209z m217 424q0 11-8 19t-19 7q-70 0-120-50t-50-119q0-11 8-19t19-8 19 8 8 19q0 48 34 82t82 34q11 0 19 8t8 19z m202 106q0-4 0-5-59-105-176-316t-176-316l-28-50q-5-9-15-9-7 0-75 39-9 6-9 16 0 7 25 49-80 36-147 96t-117 137q-11 17-11 38t11 39q86 131 212 207t277 76q50 0 100-10l31 54q5 9 15 9 3 0 10-3t18-9 18-10 18-10 10-7q9-5 9-15z m21-249q0-78-44-142t-117-91l157 280q4-25 4-47z m250-72q0-19-11-38-22-36-61-81-84-96-194-149t-234-53l41 74q119 10 219 76t169 171q-65 100-158 164l35 63q53-36 102-85t81-103q11-19 11-39z" horiz-adv-x="1000" />
<glyph glyph-name="search" unicode="&#xe806;" d="M643 393q0 103-73 176t-177 74-177-74-73-176 73-177 177-73 177 73 73 177z m286-465q0-29-22-50t-50-21q-30 0-50 21l-191 191q-100-69-223-69-80 0-153 31t-125 84-84 125-31 153 31 152 84 126 125 84 153 31 153-31 125-84 84-126 31-152q0-123-69-223l191-191q21-21 21-51z" horiz-adv-x="928.6" />
<glyph glyph-name="cog" unicode="&#xe807;" d="M571 357q0 59-41 101t-101 42-101-42-42-101 42-101 101-42 101 42 41 101z m286 61v-124q0-7-4-13t-11-7l-104-16q-10-30-21-51 19-27 59-77 6-6 6-13t-5-13q-15-21-55-61t-53-39q-7 0-14 5l-77 60q-25-13-51-21-9-76-16-104-4-16-20-16h-124q-8 0-14 5t-6 12l-16 103q-27 9-50 21l-79-60q-6-5-14-5-8 0-14 6-70 64-92 94-4 5-4 13 0 6 5 12 8 12 28 37t30 40q-15 28-23 55l-102 15q-7 1-11 7t-5 13v124q0 7 5 13t10 7l104 16q8 25 22 51-23 32-60 77-6 7-6 14 0 5 5 12 15 20 55 60t53 40q7 0 15-5l77-60q24 13 50 21 9 76 17 104 3 16 20 16h124q7 0 13-5t7-12l15-103q28-9 51-20l79 59q5 5 13 5 7 0 14-5 72-67 92-95 4-5 4-12 0-7-4-13-9-12-29-37t-30-40q15-28 23-54l102-16q7-1 12-7t4-13z" horiz-adv-x="857.1" />
<glyph glyph-name="logout" unicode="&#xe808;" d="M357 53q0-2 1-11t0-14-2-14-5-10-12-4h-178q-67 0-114 47t-47 114v392q0 67 47 114t114 47h178q8 0 13-5t5-13q0-2 1-11t0-15-2-13-5-11-12-3h-178q-37 0-63-26t-27-64v-392q0-37 27-63t63-27h174t6 0 7-2 4-3 4-5 1-8z m518 304q0-14-11-25l-303-304q-11-10-25-10t-25 10-11 25v161h-250q-14 0-25 11t-11 25v214q0 15 11 25t25 11h250v161q0 14 11 25t25 10 25-10l303-304q11-10 11-25z" horiz-adv-x="928.6" />
<glyph glyph-name="down-open" unicode="&#xe809;" d="M939 406l-414-413q-10-11-25-11t-25 11l-414 413q-11 11-11 26t11 25l93 92q10 11 25 11t25-11l296-296 296 296q11 11 25 11t26-11l92-92q11-11 11-25t-11-26z" horiz-adv-x="1000" />
<glyph glyph-name="attach" unicode="&#xe80a;" d="M244-133q-102 0-170 72-72 70-74 166t84 190l496 496q80 80 174 54 44-12 79-47t47-79q26-96-54-176l-474-474q-40-40-88-46-48-4-80 28-30 24-27 74t47 92l332 334q24 26 50 0t0-50l-332-332q-44-44-20-70 12-8 24-6 24 4 46 26l474 474q50 50 34 108-16 60-76 76-54 14-108-36l-494-494q-66-76-64-143t52-117q50-48 117-50t141 62l496 494q24 24 50 0 26-22 0-48l-496-496q-82-82-186-82z" horiz-adv-x="939" />
<glyph glyph-name="picture" unicode="&#xe80b;" d="M357 536q0-45-31-76t-76-32-76 32-31 76 31 76 76 31 76-31 31-76z m572-215v-250h-786v107l178 179 90-89 285 285z m53 393h-893q-7 0-12-5t-6-13v-678q0-7 6-13t12-5h893q7 0 13 5t5 13v678q0 8-5 13t-13 5z m89-18v-678q0-37-26-63t-63-27h-893q-36 0-63 27t-26 63v678q0 37 26 63t63 27h893q37 0 63-27t26-63z" horiz-adv-x="1071.4" />
<glyph glyph-name="video" unicode="&#xe80c;" d="M214-36v72q0 14-10 25t-25 10h-72q-14 0-25-10t-11-25v-72q0-14 11-25t25-11h72q14 0 25 11t10 25z m0 214v72q0 14-10 25t-25 11h-72q-14 0-25-11t-11-25v-72q0-14 11-25t25-10h72q14 0 25 10t10 25z m0 215v71q0 15-10 25t-25 11h-72q-14 0-25-11t-11-25v-71q0-15 11-25t25-11h72q14 0 25 11t10 25z m572-429v286q0 14-11 25t-25 11h-429q-14 0-25-11t-10-25v-286q0-14 10-25t25-11h429q15 0 25 11t11 25z m-572 643v71q0 15-10 26t-25 10h-72q-14 0-25-10t-11-26v-71q0-14 11-25t25-11h72q14 0 25 11t10 25z m786-643v72q0 14-11 25t-25 10h-71q-15 0-25-10t-11-25v-72q0-14 11-25t25-11h71q15 0 25 11t11 25z m-214 429v285q0 15-11 26t-25 10h-429q-14 0-25-10t-10-26v-285q0-15 10-25t25-11h429q15 0 25 11t11 25z m214-215v72q0 14-11 25t-25 11h-71q-15 0-25-11t-11-25v-72q0-14 11-25t25-10h71q15 0 25 10t11 25z m0 215v71q0 15-11 25t-25 11h-71q-15 0-25-11t-11-25v-71q0-15 11-25t25-11h71q15 0 25 11t11 25z m0 214v71q0 15-11 26t-25 10h-71q-15 0-25-10t-11-26v-71q0-14 11-25t25-11h71q15 0 25 11t11 25z m71 89v-750q0-37-26-63t-63-26h-893q-36 0-63 26t-26 63v750q0 37 26 63t63 27h893q37 0 63-27t26-63z" horiz-adv-x="1071.4" />
<glyph glyph-name="right-open" unicode="&#xe80d;" d="M618 368l-414-415q-11-10-25-10t-25 10l-93 93q-11 11-11 25t11 25l296 297-296 296q-11 11-11 25t11 25l93 93q10 11 25 11t25-11l414-414q10-11 10-25t-10-25z" horiz-adv-x="714.3" />
<glyph glyph-name="left-open" unicode="&#xe80e;" d="M654 689l-297-296 297-297q10-10 10-25t-10-25l-93-93q-11-10-25-10t-25 10l-414 415q-11 10-11 25t11 25l414 414q10 11 25 11t25-11l93-93q10-10 10-25t-10-25z" horiz-adv-x="714.3" />
<glyph glyph-name="up-open" unicode="&#xe80f;" d="M939 114l-92-92q-11-10-26-10t-25 10l-296 297-296-297q-11-10-25-10t-25 10l-93 92q-11 11-11 26t11 25l414 414q11 10 25 10t25-10l414-414q11-11 11-25t-11-26z" horiz-adv-x="1000" />
<glyph glyph-name="bell-ringing-o" unicode="&#xe810;" d="M498 857c-30 0-54-24-54-53 0-8 2-15 5-22-147-22-236-138-236-245 0-268-95-393-177-462 0-39 32-71 71-71h249c0-79 63-143 142-143s142 64 142 143h249c39 0 71 32 71 71-82 69-178 194-178 462 0 107-88 223-235 245 2 7 4 14 4 22 0 29-24 53-53 53z m-309-45c-81-74-118-170-118-275l71 0c0 89 28 162 95 223l-48 52z m617 0l-48-52c67-61 96-134 95-223l71 0c1 105-37 201-118 275z m-397-799c5 0 9-4 9-9 0-44 36-80 80-80 5 0 9-4 9-9s-4-9-9-9c-54 0-98 44-98 98 0 5 4 9 9 9z" horiz-adv-x="1000" />
<glyph glyph-name="lock" unicode="&#xe811;" d="M179 428h285v108q0 59-42 101t-101 41-101-41-41-101v-108z m464-53v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h17v108q0 102 74 176t176 74 177-74 73-176v-108h18q23 0 38-15t16-38z" horiz-adv-x="642.9" />
<glyph glyph-name="globe" unicode="&#xe812;" d="M429 786q116 0 215-58t156-156 57-215-57-215-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58z m153-291q-2-1-6-5t-7-6q1 0 2 3t3 6 2 4q3 4 12 8 8 4 29 7 19 5 29-6-1 1 5 7t8 7q2 1 8 3t9 4l1 12q-7-1-10 4t-3 12q0-2-4-5 0 4-2 5t-7-1-5-1q-5 2-8 5t-5 9-2 8q-1 3-5 6t-5 6q-1 1-2 3t-1 4-3 3-3 1-4-3-4-5-2-3q-2 1-4 1t-2-1-3-1-3-2q-1-2-4-2t-5-1q8 3-1 6-5 2-9 2 6 2 5 6t-5 8h3q-1 2-5 5t-10 5-7 3q-5 3-19 5t-18 1q-3-4-3-6t2-8 2-7q1-3-3-7t-3-7q0-4 7-9t6-12q-2-4-9-9t-9-6q-3-5-1-11t6-9q1-1 1-2t-2-3-3-2-4-2l-1-1q-7-3-12 3t-7 15q-4 14-9 17-13 4-16-1-3 7-23 15-14 5-33 2 4 0 0 8-4 9-10 7 1 3 2 10t0 7q2 8 7 13 1 1 4 5t5 7 1 4q19-3 28 6 2 3 6 9t6 10q5 3 8 3t8-3 8-3q8-1 8 6t-4 11q7 0 2 10-2 4-5 5-6 2-15-3-4-2 2-4-1 0-6-6t-9-10-9 3q0 0-3 7t-5 8q-5 0-9-9 1 5-6 9t-14 4q11 7-4 15-4 3-12 3t-11-2q-2-4-3-7t3-4 6-3 6-2 5-2q8-6 5-8-1 0-5-2t-6-2-4-2q-1-3 0-8t-1-8q-3 3-5 10t-4 9q4-5-14-3l-5 0q-3 0-9-1t-12-1-7 5q-3 4 0 11 0 2 2 1-2 2-6 5t-6 5q-25-8-52-23 3 0 6 1 3 1 8 4t5 3q19 7 24 4l3 2q7-9 11-14-4 3-17 1-11-3-12-7 4-6 2-10-2 2-6 6t-8 6-8 3q-9 0-13-1-81-45-131-124 4-4 7-4 2-1 3-5t1-6 6 1q5-4 2-10 1 0 25-15 10-10 11-12 2-6-5-10-1 1-5 5t-5 2q-2-3 0-10t6-7q-4 0-5-9t-2-20 0-13l1-1q-2-6 3-19t12-11q-7-1 11-24 3-4 4-5 2-1 7-4t9-6 5-5q2-3 6-13t8-13q-2-3 5-11t6-13q-1 0-2-1t-1 0q2-4 9-8t8-7q1-2 1-6t2-6 4-1q2 11-13 35-8 13-9 16-2 2-4 8t-2 8q1 0 3 0t5-2 4-3 1-1q-1-4 1-10t7-10 10-11 6-7q4-4 8-11t0-8q5 0 11-5t10-11q3-5 4-15t3-13q1-4 5-8t7-5l9-5t7-3q3-2 10-6t12-7q6-2 9-2t8 1 8 2q8 1 16-8t12-12q20-10 30-6-1 0 1-4t4-9 5-8 3-5q3-3 10-8t10-8q4 2 4 5-1-5 4-11t10-6q8 2 8 18-17-8-27 10 0 0-2 3t-2 5-1 4 0 5 2 1q5 0 6 2t-1 7-2 8q-1 4-6 11t-7 8q-3-5-9-4t-9 5q0-1-1-3t-1-4q-7 0-8 0 1 2 1 10t2 13q1 2 3 6t5 9 2 7-3 5-9 1q-11 0-15-11-1-2-2-6t-2-6-5-4q-4-2-14-1t-13 3q-8 4-13 16t-5 20q0 6 1 15t2 14-3 14q2 1 5 5t5 6q2 1 3 1t3 0 2 1 1 3q0 1-2 2-1 1-2 1 4-1 16 1t15-1q9-6 12 1 0 1-1 6t0 7q3-15 16-5 2-1 9-3t9-2q2-1 4-3t3-3 3 0 5 4q5-8 7-13 6-23 10-25 4-2 6-1t3 5 0 8-1 7l-1 5v10l0 4q-8 2-10 7t0 10 9 10q0 1 4 2t9 4 7 4q12 11 8 20 4 0 6 5 0 0-2 2t-5 2-2 2q5 2 1 8 3 2 4 7t4 5q5-6 12-1 5 5 1 9 2 4 11 6t10 5q4-1 5 1t0 7 2 7q2 2 9 5t7 2l9 7q2 2 0 2 10-1 18 6 5 6-4 11 2 4-1 5t-9 4q2 0 7 0t5 1q9 5-3 9-10 2-24-7z m-91-490q115 21 195 106-1 2-7 2t-7 2q-10 4-13 5 1 4-1 7t-5 5-7 5-6 4q-1 1-4 3t-4 3-4 2-5 2-5-1l-2-1q-2 0-3-1t-3-2-2-1 0-2q-12 10-20 13-3 0-6 3t-6 4-6 0-6-3q-3-3-4-9t-1-7q-4 3 0 10t1 10q-1 3-6 2t-6-2-7-5-5-3-4-3-5-5q-2-2-4-6t-2-6q-1 2-7 3t-5 3q1-5 2-19t3-22q4-17-7-26-15-14-16-23-2-12 7-14 0-4-5-12t-4-12q0-3 2-9z" horiz-adv-x="857.1" />
<glyph glyph-name="brush" unicode="&#xe813;" d="M464 209q0-124-87-212t-210-87q-81 0-149 40 68 39 109 108t40 151q0 61 44 105t105 44 105-44 43-105z m415 562q32-32 32-79t-33-79l-318-318q-20 55-61 97t-97 62l318 318q32 32 79 32t80-33z" horiz-adv-x="928" />
<glyph glyph-name="attention" unicode="&#xe814;" d="M571 90v106q0 8-5 13t-12 5h-108q-7 0-12-5t-5-13v-106q0-8 5-13t12-6h108q7 0 12 6t5 13z m-1 208l10 257q0 6-5 10-7 6-14 6h-122q-6 0-14-6-5-4-5-12l9-255q0-5 6-9t13-3h103q8 0 14 3t5 9z m-7 522l428-786q20-35-1-70-9-17-26-26t-35-10h-858q-18 0-35 10t-26 26q-21 35-1 70l429 786q9 17 26 27t36 10 36-10 27-27z" horiz-adv-x="1000" />
<glyph glyph-name="plus" unicode="&#xe815;" d="M786 446v-107q0-22-16-38t-38-15h-232v-233q0-22-16-37t-38-16h-107q-22 0-38 16t-15 37v233h-232q-23 0-38 15t-16 38v107q0 23 16 38t38 16h232v232q0 22 15 38t38 16h107q23 0 38-16t16-38v-232h232q23 0 38-16t16-38z" horiz-adv-x="785.7" />
<glyph glyph-name="adjust" unicode="&#xe816;" d="M429 53v608q-83 0-153-41t-110-111-41-152 41-152 110-111 153-41z m428 304q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" />
<glyph glyph-name="edit" unicode="&#xe817;" d="M496 196l64 65-85 85-64-65v-31h53v-54h32z m245 402q-9 9-18 0l-196-196q-9-9 0-18t18 0l196 196q9 9 0 18z m45-331v-106q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h464q35 0 65-14 9-4 10-13 2-10-5-16l-27-28q-8-8-18-4-13 3-25 3h-464q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v70q0 7 5 12l36 36q8 8 20 4t11-16z m-54 411l161-160-375-375h-161v160z m248-73l-51-52-161 161 51 52q16 15 38 15t38-15l85-85q16-16 16-38t-16-38z" horiz-adv-x="1000" />
<glyph glyph-name="pencil" unicode="&#xe818;" d="M203 0l50 51-131 131-51-51v-60h72v-71h60z m291 518q0 12-12 12-5 0-9-4l-303-302q-4-4-4-10 0-12 13-12 5 0 9 4l303 302q3 4 3 10z m-30 107l232-232-464-465h-232v233z m381-54q0-29-20-50l-93-93-232 233 93 92q20 21 50 21 29 0 51-21l131-131q20-22 20-51z" horiz-adv-x="857.1" />
<glyph glyph-name="pin" unicode="&#xe819;" d="M268 375v250q0 8-5 13t-13 5-13-5-5-13v-250q0-8 5-13t13-5 13 5 5 13z m375-197q0-14-11-25t-25-10h-239l-29-270q-1-7-6-11t-11-5h-1q-15 0-17 15l-43 271h-225q-15 0-25 10t-11 25q0 69 44 124t99 55v286q-29 0-50 21t-22 50 22 50 50 22h357q29 0 50-22t21-50-21-50-50-21v-286q55 0 99-55t44-124z" horiz-adv-x="642.9" />
<glyph glyph-name="wrench" unicode="&#xe81a;" d="M214 36q0 14-10 25t-25 10-25-10-11-25 11-25 25-11 25 11 10 25z m360 234l-381-381q-21-20-50-20-29 0-51 20l-59 61q-21 20-21 50 0 29 21 51l380 380q22-55 64-97t97-64z m354 243q0-22-13-59-27-75-92-122t-144-46q-104 0-177 73t-73 177 73 176 177 74q32 0 67-10t60-26q9-6 9-15t-9-16l-163-94v-125l108-60q2 2 44 27t75 45 40 20q8 0 13-5t5-14z" horiz-adv-x="928.6" />
<glyph glyph-name="verified" unicode="&#xe81b;" d="M926 453l-19 13c-21 14-30 41-23 65l6 22c10 34-13 69-48 75l-23 4c-25 4-45 23-49 48l-4 23c-6 35-41 57-75 47l-22-7c-24-7-51 2-65 22l-14 20c-21 29-62 33-88 9l-17-16c-19-17-46-21-69-8l-20 11c-31 17-70 3-84-30l-9-22c-9-24-33-39-58-37l-23 1c-36 2-65-28-62-63l2-23c2-25-13-49-36-59l-21-9c-33-14-46-53-29-84l12-20c13-22 10-50-7-69l-15-17c-24-27-19-68 11-88l19-13c21-14 30-41 23-65l-9-23c-10-34 13-69 48-75l23-4c25-4 45-23 49-48l4-23c6-35 41-57 75-47l22 7c24 7 51-2 65-22l14-19c21-29 62-33 88-9l17 16c19 17 46 21 69 8l20-11c31-17 70-3 84 30l9 22c9 24 33 39 58 37l23-1c36-2 65 28 62 63l-1 23c-2 25 13 49 36 59l21 9c33 14 46 53 29 84l-12 20c-13 22-10 50 7 69l15 17c25 26 20 68-9 88z m-399-189l-82-81-81 82-78 79 82 81 78-79 187 186 81-82-187-186z" horiz-adv-x="1000" />
<glyph glyph-name="chart-bar" unicode="&#xe81c;" d="M357 357v-286h-143v286h143z m214 286v-572h-142v572h142z m572-643v-72h-1143v858h71v-786h1072z m-357 500v-429h-143v429h143z m214 214v-643h-143v643h143z" horiz-adv-x="1142.9" />
<glyph glyph-name="users" unicode="&#xe81d;" d="M331 357q-90-3-148-71h-75q-45 0-77 22t-31 66q0 197 69 197 4 0 25-11t54-24 66-12q38 0 75 13-3-21-3-37 0-78 45-143z m598-355q0-67-41-106t-108-39h-488q-68 0-108 39t-41 106q0 29 2 57t8 61 14 61 24 54 35 45 48 30 62 11q6 0 24-12t41-26 59-27 76-12 75 12 60 27 41 26 24 12q34 0 62-11t47-30 35-45 24-54 15-61 8-61 2-57z m-572 712q0-59-42-101t-101-42-101 42-42 101 42 101 101 42 101-42 42-101z m393-214q0-89-63-152t-151-62-152 62-63 152 63 151 152 63 151-63 63-151z m321-126q0-43-31-66t-77-22h-75q-57 68-147 71 45 65 45 143 0 16-3 37 37-13 74-13 33 0 67 12t54 24 24 11q69 0 69-197z m-71 340q0-59-42-101t-101-42-101 42-42 101 42 101 101 42 101-42 42-101z" horiz-adv-x="1071.4" />
<glyph glyph-name="address-book" unicode="&#xe81e;" d="M0-143l0 1000 1000 0 0-178-109 0 0-95 109 0 0-178-109 0 0-98 109 0 0-177-109 0 0-96 109 0 0-178-1000 0z m193 285l504 0 0 155-187 111q37 19 58 54t22 77q0 58-42 101t-103 43-102-43-42-101q0-43 22-77t58-54l-188-111 0-155z" horiz-adv-x="1000" />
<glyph glyph-name="cog-alt" unicode="&#xe81f;" d="M500 357q0 59-42 101t-101 42-101-42-42-101 42-101 101-42 101 42 42 101z m429-286q0 29-22 51t-50 21-50-21-21-51q0-29 21-50t50-21 51 21 21 50z m0 572q0 29-22 50t-50 21-50-21-21-50q0-30 21-51t50-21 51 21 21 51z m-215-235v-103q0-6-4-11t-8-6l-87-14q-6-19-18-42 19-27 50-64 4-6 4-11 0-7-4-11-12-17-46-50t-43-33q-7 0-12 4l-64 50q-21-11-43-17-6-60-13-87-4-13-17-13h-104q-6 0-11 4t-5 10l-13 85q-19 6-42 18l-66-50q-4-4-11-4-6 0-12 4-80 75-80 90 0 5 4 10 5 8 23 30t26 34q-13 24-20 46l-85 13q-5 1-9 5t-4 11v104q0 5 4 10t9 6l86 14q7 19 18 42-19 27-50 64-4 6-4 11 0 7 4 12 12 16 46 49t44 33q6 0 12-4l64-50q19 10 43 18 6 60 13 86 3 13 16 13h104q6 0 11-4t6-10l13-85q19-6 42-17l65 49q5 4 12 4 6 0 11-4 81-75 81-90 0-4-4-10-7-9-24-30t-25-34q13-27 19-46l85-12q6-2 9-6t4-11z m357-298v-78q0-9-83-17-6-15-16-29 28-63 28-77 0-2-2-4-68-40-69-40-5 0-26 27t-29 37q-11-1-17-1t-17 1q-7-11-29-37t-25-27q-1 0-69 40-3 2-3 4 0 14 29 77-10 14-17 29-83 8-83 17v78q0 9 83 18 7 16 17 29-29 63-29 77 0 2 3 4 2 1 19 11t33 19 17 9q4 0 25-26t29-38q12 1 17 1t17-1q28 40 51 63l4 1q2 0 69-39 2-2 2-4 0-14-28-77 9-13 16-29 83-9 83-18z m0 572v-78q0-9-83-18-6-15-16-29 28-63 28-77 0-2-2-4-68-39-69-39-5 0-26 26t-29 38q-11-1-17-1t-17 1q-7-12-29-38t-25-26q-1 0-69 39-3 2-3 4 0 14 29 77-10 14-17 29-83 9-83 18v78q0 9 83 17 7 16 17 29-29 63-29 77 0 2 3 4 2 1 19 11t33 19 17 9q4 0 25-26t29-37q12 1 17 1t17-1q28 39 51 62l4 1q2 0 69-39 2-2 2-4 0-14-28-77 9-13 16-29 83-8 83-17z" horiz-adv-x="1071.4" />
<glyph glyph-name="zoom-in" unicode="&#xe820;" d="M571 411v-36q0-7-5-13t-12-5h-125v-125q0-7-6-13t-12-5h-36q-7 0-13 5t-5 13v125h-125q-7 0-12 5t-6 13v36q0 7 6 12t12 5h125v125q0 8 5 13t13 5h36q7 0 12-5t6-13v-125h125q7 0 12-5t5-12z m72-18q0 103-73 176t-177 74-177-74-73-176 73-177 177-73 177 73 73 177z m286-465q0-29-21-50t-51-21q-30 0-50 21l-191 191q-100-69-223-69-80 0-153 31t-125 84-84 125-31 153 31 152 84 126 125 84 153 31 153-31 125-84 84-126 31-152q0-123-69-223l191-191q21-21 21-51z" horiz-adv-x="928.6" />
<glyph glyph-name="home-2" unicode="&#xe821;" d="M521 826q322-279 500-429 20-16 20-40 0-21-15-37t-36-15l-105 0 0-364q0-21-15-37t-36-16l-156 0q-22 0-37 16t-16 37l0 208-209 0 0-208q0-21-15-37t-36-16l-156 0q-21 0-37 16t-16 37l0 364-103 0q-22 0-37 15t-16 37 19 40z" horiz-adv-x="1041" />
<glyph glyph-name="spin3" unicode="&#xe832;" d="M494 857c-266 0-483-210-494-472-1-19 13-20 13-20l84 0c16 0 19 10 19 18 10 199 176 358 378 358 107 0 205-45 273-118l-58-57c-11-12-11-27 5-31l247-50c21-5 46 11 37 44l-58 227c-2 9-16 22-29 13l-65-60c-89 91-214 148-352 148z m409-508c-16 0-19-10-19-18-10-199-176-358-377-358-108 0-205 45-274 118l59 57c10 12 10 27-5 31l-248 50c-21 5-46-11-37-44l58-227c2-9 16-22 30-13l64 60c89-91 214-148 353-148 265 0 482 210 493 473 1 18-13 19-13 19l-84 0z" horiz-adv-x="1000" />
<glyph glyph-name="spin4" unicode="&#xe834;" d="M498 857c-114 0-228-39-320-116l0 0c173 140 428 130 588-31 134-134 164-332 89-495-10-29-5-50 12-68 21-20 61-23 84 0 3 3 12 15 15 24 71 180 33 393-112 539-99 98-228 147-356 147z m-409-274c-14 0-29-5-39-16-3-3-13-15-15-24-71-180-34-393 112-539 185-185 479-195 676-31l0 0c-173-140-428-130-589 31-134 134-163 333-89 495 11 29 6 50-12 68-11 11-27 17-44 16z" horiz-adv-x="1001" />
<glyph glyph-name="link-ext" unicode="&#xf08e;" d="M786 339v-178q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h393q7 0 12-5t5-13v-36q0-8-5-13t-12-5h-393q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v178q0 8 5 13t13 5h36q8 0 13-5t5-13z m214 482v-285q0-15-11-25t-25-11-25 11l-98 98-364-364q-5-6-13-6t-12 6l-64 64q-6 5-6 12t6 13l364 364-98 98q-11 11-11 25t11 25 25 11h285q15 0 25-11t11-25z" horiz-adv-x="1000" />
<glyph glyph-name="link-ext-alt" unicode="&#xf08f;" d="M714 339v268q0 15-10 25t-25 11h-268q-24 0-33-22-10-23 8-39l80-80-298-298q-11-11-11-26t11-25l57-57q11-10 25-10t25 10l298 298 81-80q10-11 25-11 6 0 14 3 21 10 21 33z m143 286v-536q0-66-47-113t-114-48h-535q-67 0-114 48t-47 113v536q0 66 47 113t114 48h535q67 0 114-48t47-113z" horiz-adv-x="857.1" />
<glyph glyph-name="menu" unicode="&#xf0c9;" d="M857 107v-71q0-15-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 25t25 11h785q15 0 26-11t10-25z m0 286v-72q0-14-10-25t-26-10h-785q-15 0-25 10t-11 25v72q0 14 11 25t25 10h785q15 0 26-10t10-25z m0 285v-71q0-14-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 26t25 10h785q15 0 26-10t10-26z" horiz-adv-x="857.1" />
<glyph glyph-name="mail-alt" unicode="&#xf0e0;" d="M1000 461v-443q0-37-26-63t-63-27h-822q-36 0-63 27t-26 63v443q25-27 56-49 202-137 278-192 32-24 51-37t53-27 61-13h2q28 0 61 13t53 27 51 37q95 68 278 192 32 22 56 49z m0 164q0-44-27-84t-68-69q-210-146-262-181-5-4-23-17t-30-22-29-18-32-15-28-5h-2q-12 0-27 5t-32 15-30 18-30 22-23 17q-51 35-147 101t-114 80q-35 23-65 64t-31 77q0 43 23 72t66 29h822q36 0 63-26t26-63z" horiz-adv-x="1000" />
<glyph glyph-name="gauge" unicode="&#xf0e4;" d="M214 214q0 30-21 51t-50 21-51-21-21-51 21-50 51-21 50 21 21 50z m107 250q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m239-268l57 213q3 14-5 27t-21 16-27-3-17-22l-56-213q-33-3-60-25t-35-55q-11-43 11-81t66-50 81 11 50 66q9 33-4 65t-40 51z m369 18q0 30-21 51t-51 21-50-21-21-51 21-50 50-21 51 21 21 50z m-358 357q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m250-107q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m179-250q0-145-79-269-10-17-30-17h-782q-20 0-30 17-79 123-79 269 0 102 40 194t106 160 160 107 194 39 194-39 160-107 106-160 40-194z" horiz-adv-x="1000" />
<glyph glyph-name="comment-empty" unicode="&#xf0e5;" d="M500 643q-114 0-213-39t-157-105-59-142q0-62 40-119t113-98l48-28-15-53q-13-51-39-97 85 36 154 96l24 21 32-3q38-5 72-5 114 0 213 39t157 105 59 142-59 142-157 105-213 39z m500-286q0-97-67-179t-182-130-251-48q-39 0-81 4-110-97-257-135-27-8-63-12h-3q-8 0-15 6t-9 15v1q-2 2 0 6t1 6 2 5l4 5t4 5 4 5q4 5 17 19t20 22 17 22 18 28 15 33 15 42q-88 50-138 123t-51 157q0 97 67 179t182 130 251 48 251-48 182-130 67-179z" horiz-adv-x="1000" />
<glyph glyph-name="bell-alt" unicode="&#xf0f3;" d="M509-89q0 8-9 8-33 0-57 24t-23 57q0 9-9 9t-9-9q0-41 29-70t69-28q9 0 9 9z m455 160q0-29-21-50t-50-21h-250q0-59-42-101t-101-42-101 42-42 101h-250q-29 0-50 21t-21 50q28 24 51 49t47 67 42 89 27 115 11 145q0 84 66 157t171 89q-5 10-5 21 0 23 16 38t38 16 38-16 16-38q0-11-5-21 106-16 171-89t66-157q0-78 11-145t28-115 41-89 48-67 50-49z" horiz-adv-x="1000" />
<glyph glyph-name="plus-squared" unicode="&#xf0fe;" d="M714 321v72q0 14-10 25t-25 10h-179v179q0 15-11 25t-25 11h-71q-15 0-25-11t-11-25v-179h-178q-15 0-25-10t-11-25v-72q0-14 11-25t25-10h178v-179q0-14 11-25t25-11h71q15 0 25 11t11 25v179h179q14 0 25 10t10 25z m143 304v-536q0-66-47-113t-114-48h-535q-67 0-114 48t-47 113v536q0 66 47 113t114 48h535q67 0 114-48t47-113z" horiz-adv-x="857.1" />
<glyph glyph-name="laptop" unicode="&#xf109;" d="M232 143q-37 0-63 26t-26 63v393q0 37 26 63t63 26h607q37 0 63-26t27-63v-393q0-37-27-63t-63-26h-607z m-18 482v-393q0-7 6-13t12-5h607q8 0 13 5t5 13v393q0 7-5 12t-13 6h-607q-7 0-12-6t-6-12z m768-518h89v-54q0-22-26-37t-63-16h-893q-36 0-63 16t-26 37v54h982z m-402-54q9 0 9 9t-9 9h-89q-9 0-9-9t9-9h89z" horiz-adv-x="1071.4" />
<glyph glyph-name="quote-right" unicode="&#xf10e;" d="M429 678v-392q0-58-23-111t-61-91-91-61-111-23h-36q-14 0-25 11t-11 25v71q0 15 11 25t25 11h36q59 0 101 42t42 101v17q0 23-16 38t-38 16h-125q-44 0-76 31t-31 76v214q0 45 31 76t76 32h214q45 0 76-32t32-76z m500 0v-392q0-58-23-111t-61-91-91-61-111-23h-36q-14 0-25 11t-11 25v71q0 15 11 25t25 11h36q59 0 101 42t42 101v17q0 23-16 38t-38 16h-125q-44 0-76 31t-31 76v214q0 45 31 76t76 32h214q45 0 76-32t32-76z" horiz-adv-x="928.6" />
<glyph glyph-name="reply" unicode="&#xf112;" d="M1000 232q0-93-71-252-1-4-6-13t-7-17-7-12q-7-10-16-10-8 0-13 6t-5 14q0 5 1 15t2 13q3 38 3 69 0 56-10 101t-27 77-45 56-59 39-74 24-86 12-98 3h-125v-143q0-14-10-25t-26-11-25 11l-285 286q-11 10-11 25t11 25l285 286q11 10 25 10t26-10 10-25v-143h125q398 0 488-225 30-75 30-186z" horiz-adv-x="1000" />
<glyph glyph-name="smile" unicode="&#xf118;" d="M633 257q-21-67-77-109t-127-41-128 41-77 109q-4 14 3 27t21 18q14 4 27-2t17-22q14-44 52-72t85-28 84 28 52 72q4 15 18 22t27 2 21-18 2-27z m-276 243q0-30-21-51t-50-21-51 21-21 51 21 50 51 21 50-21 21-50z m286 0q0-30-21-51t-51-21-50 21-21 51 21 50 50 21 51-21 21-50z m143-143q0 73-29 139t-76 114-114 76-138 28-139-28-114-76-76-114-29-139 29-139 76-113 114-77 139-28 138 28 114 77 76 113 29 139z m71 0q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" />
<glyph glyph-name="lock-open-alt" unicode="&#xf13e;" d="M589 428q23 0 38-15t16-38v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h17v179q0 103 74 177t176 73 177-73 73-177q0-14-10-25t-25-11h-36q-14 0-25 11t-11 25q0 59-42 101t-101 42-101-42-41-101v-179h410z" horiz-adv-x="642.9" />
<glyph glyph-name="ellipsis" unicode="&#xf141;" d="M214 446v-107q0-22-15-38t-38-15h-107q-23 0-38 15t-16 38v107q0 23 16 38t38 16h107q22 0 38-16t15-38z m286 0v-107q0-22-16-38t-38-15h-107q-22 0-38 15t-15 38v107q0 23 15 38t38 16h107q23 0 38-16t16-38z m286 0v-107q0-22-16-38t-38-15h-107q-22 0-38 15t-16 38v107q0 23 16 38t38 16h107q23 0 38-16t16-38z" horiz-adv-x="785.7" />
<glyph glyph-name="play-circled" unicode="&#xf144;" d="M429 786q116 0 215-58t156-156 57-215-57-215-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58z m214-460q18 10 18 31t-18 31l-304 178q-17 11-35 1-18-11-18-31v-358q0-20 18-31 9-4 17-4 10 0 18 5z" horiz-adv-x="857.1" />
<glyph glyph-name="thumbs-up-alt" unicode="&#xf164;" d="M143 107q0 15-11 25t-25 11q-15 0-25-11t-11-25q0-15 11-25t25-11q15 0 25 11t11 25z m89 286v-357q0-15-10-25t-26-11h-160q-15 0-25 11t-11 25v357q0 14 11 25t25 10h160q15 0 26-10t10-25z m661 0q0-48-31-83 9-25 9-43 1-42-24-76 9-31 0-66-9-31-31-52 5-62-27-101-36-43-110-44h-72q-37 0-80 9t-68 16-67 22q-69 24-88 25-15 0-25 11t-11 25v357q0 14 10 25t24 11q13 1 42 33t57 67q38 49 56 67 10 10 17 27t10 27 8 34q4 22 7 34t11 29 19 28q10 11 25 11 25 0 46-6t33-15 22-22 14-25 7-28 2-25 1-22q0-21-6-43t-10-33-16-31q-1-4-5-10t-6-13-5-13h155q43 0 75-32t32-75z" horiz-adv-x="928.6" />
<glyph glyph-name="apple" unicode="&#xf179;" d="M777 179q-21-70-68-139-72-110-144-110-27 0-78 18-48 18-84 18-34 0-79-19-45-19-74-19-85 0-168 145-82 146-82 281 0 127 63 208 63 81 159 81 40 0 98-17 58-17 77-17 25 0 80 19 57 19 97 19 66 0 119-36 29-20 58-56-44-37-64-66-36-52-36-115 0-69 38-125t88-70z m-209 655q0-34-17-76-16-42-52-77-30-30-60-40-20-7-58-10 2 83 44 143 41 60 139 83 1-2 2-6t1-6q0-2 0-6t1-5z" horiz-adv-x="785.7" />
<glyph glyph-name="android" unicode="&#xf17b;" d="M275 588q9 0 16 6t6 15-6 16-16 6-15-6-6-16 6-15 15-6z m236 0q9 0 15 6t6 15-6 16-15 6-16-6-6-16 6-15 16-6z m-453-103q23 0 40-17t16-40v-240q0-24-16-41t-40-17-41 17-17 41v240q0 23 17 40t41 17z m591-11v-371q0-26-18-44t-43-18h-42v-127q0-24-16-40t-41-17-41 17-17 40v127h-77v-127q0-24-16-40t-41-17q-24 0-40 17t-17 40l-1 127h-41q-26 0-43 18t-18 44v371h512z m-129 226q59-30 95-85t36-121h-516q0 66 35 121t96 85l-39 73q-4 8 2 12 8 3 12-4l40-74q53 24 112 24t112-24l40 74q4 7 11 4 7-4 3-12z m266-272v-240q0-24-17-41t-41-17q-23 0-40 17t-17 41v240q0 24 17 40t40 17q24 0 41-17t17-40z" horiz-adv-x="785.7" />
<glyph glyph-name="paper-plane-empty" unicode="&#xf1d9;" d="M984 851q19-13 15-36l-142-857q-3-16-18-25-8-5-18-5-6 0-13 3l-294 120-166-182q-10-12-27-12-7 0-12 2-11 4-17 13t-6 21v252l-264 108q-20 8-22 30-2 22 18 33l928 536q20 12 38-1z m-190-837l123 739-800-462 187-76 482 356-267-444z" horiz-adv-x="1000" />
<glyph glyph-name="binoculars" unicode="&#xf1e5;" d="M393 678v-428q0-15-11-25t-25-11v-321q0-15-10-25t-26-11h-285q-15 0-25 11t-11 25v285l139 488q4 12 17 12h237z m178 0v-392h-142v392h142z m429-500v-285q0-15-11-25t-25-11h-285q-15 0-25 11t-11 25v321q-15 0-25 11t-11 25v428h237q13 0 17-12z m-589 661v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z m375 0v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z" horiz-adv-x="1000" />
<glyph glyph-name="user-plus" unicode="&#xf234;" d="M393 357q-89 0-152 63t-62 151 62 152 152 63 151-63 63-152-63-151-151-63z m536-71h196q7 0 13-6t5-12v-107q0-8-5-13t-13-5h-196v-197q0-7-6-12t-12-6h-107q-8 0-13 6t-5 12v197h-197q-7 0-12 5t-6 13v107q0 7 6 12t12 6h197v196q0 7 5 13t13 5h107q7 0 12-5t6-13v-196z m-411-125q0-29 21-51t50-21h143v-133q-38-28-95-28h-488q-67 0-108 39t-41 106q0 30 2 58t8 61 15 60 24 55 34 45 48 30 62 11q11 0 22-10 44-34 86-51t92-17 92 17 86 51q11 10 22 10 73 0 121-54h-125q-29 0-50-21t-21-50v-107z" horiz-adv-x="1142.9" />
<glyph glyph-name="hashtag" unicode="&#xf292;" d="M553 286l36 142h-142l-36-142h142z m429 281l-32-125q-4-14-17-14h-182l-36-142h173q9 0 14-7 6-8 4-16l-32-125q-2-13-17-13h-182l-45-183q-4-14-18-14h-125q-9 0-14 7-5 7-4 16l44 174h-142l-45-183q-4-14-17-14h-126q-8 0-14 7-5 7-3 16l43 174h-173q-9 0-14 7-5 6-4 15l32 125q4 14 17 14h182l36 142h-173q-9 0-14 7-6 8-4 16l32 125q2 13 17 13h182l46 183q3 14 17 14h125q9 0 14-7 5-7 4-16l-44-174h142l45 183q4 14 18 14h125q8 0 14-7 5-7 3-16l-43-174h173q9 0 14-7 5-6 4-15z" horiz-adv-x="1000" />
</font>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 28 KiB

View file

@ -1,126 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>Copyright (C) 2019 by original authors @ fontello.com</metadata>
<defs>
<font id="fontello" horiz-adv-x="1000" >
<font-face font-family="fontello" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="857" descent="-143" />
<missing-glyph horiz-adv-x="1000" />
<glyph glyph-name="cancel" unicode="&#xe800;" d="M724 119q0-22-15-38l-76-76q-16-15-38-15t-38 15l-164 165-164-165q-16-15-38-15t-38 15l-76 76q-16 16-16 38t16 38l164 164-164 164q-16 16-16 38t16 38l76 76q16 16 38 16t38-16l164-164 164 164q16 16 38 16t38-16l76-76q15-15 15-38t-15-38l-164-164 164-164q15-15 15-38z" horiz-adv-x="785.7" />
<glyph glyph-name="upload" unicode="&#xe801;" d="M714 36q0 14-10 25t-25 10-25-10-11-25 11-25 25-11 25 11 10 25z m143 0q0 14-10 25t-26 10-25-10-10-25 10-25 25-11 26 11 10 25z m72 125v-179q0-22-16-38t-38-16h-821q-23 0-38 16t-16 38v179q0 22 16 38t38 15h238q12-31 39-51t62-20h143q34 0 61 20t40 51h238q22 0 38-15t16-38z m-182 361q-9-22-33-22h-143v-250q0-15-10-25t-25-11h-143q-15 0-25 11t-11 25v250h-143q-23 0-33 22-9 22 8 39l250 250q10 10 25 10t25-10l250-250q18-17 8-39z" horiz-adv-x="928.6" />
<glyph glyph-name="star" unicode="&#xe802;" d="M929 496q0-12-15-27l-202-197 48-279q0-4 0-12 0-11-6-19t-17-9q-10 0-22 7l-251 132-250-132q-12-7-23-7-11 0-17 9t-6 19q0 4 1 12l48 279-203 197q-14 15-14 27 0 21 31 26l280 40 126 254q11 23 27 23t28-23l125-254 280-40q32-5 32-26z" horiz-adv-x="928.6" />
<glyph glyph-name="star-empty" unicode="&#xe803;" d="M635 297l170 166-235 34-106 213-105-213-236-34 171-166-41-235 211 111 211-111z m294 199q0-12-15-27l-202-197 48-279q0-4 0-12 0-28-23-28-10 0-22 7l-251 132-250-132q-12-7-23-7-11 0-17 9t-6 19q0 4 1 12l48 279-203 197q-14 15-14 27 0 21 31 26l280 40 126 254q11 23 27 23t28-23l125-254 280-40q32-5 32-26z" horiz-adv-x="928.6" />
<glyph glyph-name="retweet" unicode="&#xe804;" d="M714 18q0-7-5-13t-13-5h-535q-5 0-8 1t-5 4-3 4-2 7 0 6v335h-107q-15 0-25 11t-11 25q0 13 8 23l179 214q11 12 27 12t28-12l178-214q9-10 9-23 0-15-11-25t-25-11h-107v-214h321q9 0 14-6l89-108q4-5 4-11z m357 232q0-13-8-23l-178-214q-12-13-28-13t-27 13l-179 214q-8 10-8 23 0 14 11 25t25 11h107v214h-322q-9 0-14 7l-89 107q-4 5-4 11 0 7 5 12t13 6h536q4 0 7-1t5-4 3-5 2-6 1-7v-334h107q14 0 25-11t10-25z" horiz-adv-x="1071.4" />
<glyph glyph-name="eye-off" unicode="&#xe805;" d="M310 112l43 79q-48 35-76 88t-27 114q0 67 34 125-128-65-213-197 94-144 239-209z m217 424q0 11-8 19t-19 7q-70 0-120-50t-50-119q0-11 8-19t19-8 19 8 8 19q0 48 34 82t82 34q11 0 19 8t8 19z m202 106q0-4 0-5-59-105-176-316t-176-316l-28-50q-5-9-15-9-7 0-75 39-9 6-9 16 0 7 25 49-80 36-147 96t-117 137q-11 17-11 38t11 39q86 131 212 207t277 76q50 0 100-10l31 54q5 9 15 9 3 0 10-3t18-9 18-10 18-10 10-7q9-5 9-15z m21-249q0-78-44-142t-117-91l157 280q4-25 4-47z m250-72q0-19-11-38-22-36-61-81-84-96-194-149t-234-53l41 74q119 10 219 76t169 171q-65 100-158 164l35 63q53-36 102-85t81-103q11-19 11-39z" horiz-adv-x="1000" />
<glyph glyph-name="search" unicode="&#xe806;" d="M643 393q0 103-73 176t-177 74-177-74-73-176 73-177 177-73 177 73 73 177z m286-465q0-29-22-50t-50-21q-30 0-50 21l-191 191q-100-69-223-69-80 0-153 31t-125 84-84 125-31 153 31 152 84 126 125 84 153 31 153-31 125-84 84-126 31-152q0-123-69-223l191-191q21-21 21-51z" horiz-adv-x="928.6" />
<glyph glyph-name="cog" unicode="&#xe807;" d="M571 357q0 59-41 101t-101 42-101-42-42-101 42-101 101-42 101 42 41 101z m286 61v-124q0-7-4-13t-11-7l-104-16q-10-30-21-51 19-27 59-77 6-6 6-13t-5-13q-15-21-55-61t-53-39q-7 0-14 5l-77 60q-25-13-51-21-9-76-16-104-4-16-20-16h-124q-8 0-14 5t-6 12l-16 103q-27 9-50 21l-79-60q-6-5-14-5-8 0-14 6-70 64-92 94-4 5-4 13 0 6 5 12 8 12 28 37t30 40q-15 28-23 55l-102 15q-7 1-11 7t-5 13v124q0 7 5 13t10 7l104 16q8 25 22 51-23 32-60 77-6 7-6 14 0 5 5 12 15 20 55 60t53 40q7 0 15-5l77-60q24 13 50 21 9 76 17 104 3 16 20 16h124q7 0 13-5t7-12l15-103q28-9 51-20l79 59q5 5 13 5 7 0 14-5 72-67 92-95 4-5 4-12 0-7-4-13-9-12-29-37t-30-40q15-28 23-54l102-16q7-1 12-7t4-13z" horiz-adv-x="857.1" />
<glyph glyph-name="logout" unicode="&#xe808;" d="M357 53q0-2 1-11t0-14-2-14-5-10-12-4h-178q-67 0-114 47t-47 114v392q0 67 47 114t114 47h178q8 0 13-5t5-13q0-2 1-11t0-15-2-13-5-11-12-3h-178q-37 0-63-26t-27-64v-392q0-37 27-63t63-27h174t6 0 7-2 4-3 4-5 1-8z m518 304q0-14-11-25l-303-304q-11-10-25-10t-25 10-11 25v161h-250q-14 0-25 11t-11 25v214q0 15 11 25t25 11h250v161q0 14 11 25t25 10 25-10l303-304q11-10 11-25z" horiz-adv-x="928.6" />
<glyph glyph-name="down-open" unicode="&#xe809;" d="M939 406l-414-413q-10-11-25-11t-25 11l-414 413q-11 11-11 26t11 25l93 92q10 11 25 11t25-11l296-296 296 296q11 11 25 11t26-11l92-92q11-11 11-25t-11-26z" horiz-adv-x="1000" />
<glyph glyph-name="attach" unicode="&#xe80a;" d="M244-133q-102 0-170 72-72 70-74 166t84 190l496 496q80 80 174 54 44-12 79-47t47-79q26-96-54-176l-474-474q-40-40-88-46-48-4-80 28-30 24-27 74t47 92l332 334q24 26 50 0t0-50l-332-332q-44-44-20-70 12-8 24-6 24 4 46 26l474 474q50 50 34 108-16 60-76 76-54 14-108-36l-494-494q-66-76-64-143t52-117q50-48 117-50t141 62l496 494q24 24 50 0 26-22 0-48l-496-496q-82-82-186-82z" horiz-adv-x="939" />
<glyph glyph-name="picture" unicode="&#xe80b;" d="M357 536q0-45-31-76t-76-32-76 32-31 76 31 76 76 31 76-31 31-76z m572-215v-250h-786v107l178 179 90-89 285 285z m53 393h-893q-7 0-12-5t-6-13v-678q0-7 6-13t12-5h893q7 0 13 5t5 13v678q0 8-5 13t-13 5z m89-18v-678q0-37-26-63t-63-27h-893q-36 0-63 27t-26 63v678q0 37 26 63t63 27h893q37 0 63-27t26-63z" horiz-adv-x="1071.4" />
<glyph glyph-name="video" unicode="&#xe80c;" d="M214-36v72q0 14-10 25t-25 10h-72q-14 0-25-10t-11-25v-72q0-14 11-25t25-11h72q14 0 25 11t10 25z m0 214v72q0 14-10 25t-25 11h-72q-14 0-25-11t-11-25v-72q0-14 11-25t25-10h72q14 0 25 10t10 25z m0 215v71q0 15-10 25t-25 11h-72q-14 0-25-11t-11-25v-71q0-15 11-25t25-11h72q14 0 25 11t10 25z m572-429v286q0 14-11 25t-25 11h-429q-14 0-25-11t-10-25v-286q0-14 10-25t25-11h429q15 0 25 11t11 25z m-572 643v71q0 15-10 26t-25 10h-72q-14 0-25-10t-11-26v-71q0-14 11-25t25-11h72q14 0 25 11t10 25z m786-643v72q0 14-11 25t-25 10h-71q-15 0-25-10t-11-25v-72q0-14 11-25t25-11h71q15 0 25 11t11 25z m-214 429v285q0 15-11 26t-25 10h-429q-14 0-25-10t-10-26v-285q0-15 10-25t25-11h429q15 0 25 11t11 25z m214-215v72q0 14-11 25t-25 11h-71q-15 0-25-11t-11-25v-72q0-14 11-25t25-10h71q15 0 25 10t11 25z m0 215v71q0 15-11 25t-25 11h-71q-15 0-25-11t-11-25v-71q0-15 11-25t25-11h71q15 0 25 11t11 25z m0 214v71q0 15-11 26t-25 10h-71q-15 0-25-10t-11-26v-71q0-14 11-25t25-11h71q15 0 25 11t11 25z m71 89v-750q0-37-26-63t-63-26h-893q-36 0-63 26t-26 63v750q0 37 26 63t63 27h893q37 0 63-27t26-63z" horiz-adv-x="1071.4" />
<glyph glyph-name="right-open" unicode="&#xe80d;" d="M618 368l-414-415q-11-10-25-10t-25 10l-93 93q-11 11-11 25t11 25l296 297-296 296q-11 11-11 25t11 25l93 93q10 11 25 11t25-11l414-414q10-11 10-25t-10-25z" horiz-adv-x="714.3" />
<glyph glyph-name="left-open" unicode="&#xe80e;" d="M654 689l-297-296 297-297q10-10 10-25t-10-25l-93-93q-11-10-25-10t-25 10l-414 415q-11 10-11 25t11 25l414 414q10 11 25 11t25-11l93-93q10-10 10-25t-10-25z" horiz-adv-x="714.3" />
<glyph glyph-name="up-open" unicode="&#xe80f;" d="M939 114l-92-92q-11-10-26-10t-25 10l-296 297-296-297q-11-10-25-10t-25 10l-93 92q-11 11-11 26t11 25l414 414q11 10 25 10t25-10l414-414q11-11 11-25t-11-26z" horiz-adv-x="1000" />
<glyph glyph-name="bell-ringing-o" unicode="&#xe810;" d="M498 857c-30 0-54-24-54-53 0-8 2-15 5-22-147-22-236-138-236-245 0-268-95-393-177-462 0-39 32-71 71-71h249c0-79 63-143 142-143s142 64 142 143h249c39 0 71 32 71 71-82 69-178 194-178 462 0 107-88 223-235 245 2 7 4 14 4 22 0 29-24 53-53 53z m-309-45c-81-74-118-170-118-275l71 0c0 89 28 162 95 223l-48 52z m617 0l-48-52c67-61 96-134 95-223l71 0c1 105-37 201-118 275z m-397-799c5 0 9-4 9-9 0-44 36-80 80-80 5 0 9-4 9-9s-4-9-9-9c-54 0-98 44-98 98 0 5 4 9 9 9z" horiz-adv-x="1000" />
<glyph glyph-name="lock" unicode="&#xe811;" d="M179 428h285v108q0 59-42 101t-101 41-101-41-41-101v-108z m464-53v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h17v108q0 102 74 176t176 74 177-74 73-176v-108h18q23 0 38-15t16-38z" horiz-adv-x="642.9" />
<glyph glyph-name="globe" unicode="&#xe812;" d="M429 786q116 0 215-58t156-156 57-215-57-215-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58z m153-291q-2-1-6-5t-7-6q1 0 2 3t3 6 2 4q3 4 12 8 8 4 29 7 19 5 29-6-1 1 5 7t8 7q2 1 8 3t9 4l1 12q-7-1-10 4t-3 12q0-2-4-5 0 4-2 5t-7-1-5-1q-5 2-8 5t-5 9-2 8q-1 3-5 6t-5 6q-1 1-2 3t-1 4-3 3-3 1-4-3-4-5-2-3q-2 1-4 1t-2-1-3-1-3-2q-1-2-4-2t-5-1q8 3-1 6-5 2-9 2 6 2 5 6t-5 8h3q-1 2-5 5t-10 5-7 3q-5 3-19 5t-18 1q-3-4-3-6t2-8 2-7q1-3-3-7t-3-7q0-4 7-9t6-12q-2-4-9-9t-9-6q-3-5-1-11t6-9q1-1 1-2t-2-3-3-2-4-2l-1-1q-7-3-12 3t-7 15q-4 14-9 17-13 4-16-1-3 7-23 15-14 5-33 2 4 0 0 8-4 9-10 7 1 3 2 10t0 7q2 8 7 13 1 1 4 5t5 7 1 4q19-3 28 6 2 3 6 9t6 10q5 3 8 3t8-3 8-3q8-1 8 6t-4 11q7 0 2 10-2 4-5 5-6 2-15-3-4-2 2-4-1 0-6-6t-9-10-9 3q0 0-3 7t-5 8q-5 0-9-9 1 5-6 9t-14 4q11 7-4 15-4 3-12 3t-11-2q-2-4-3-7t3-4 6-3 6-2 5-2q8-6 5-8-1 0-5-2t-6-2-4-2q-1-3 0-8t-1-8q-3 3-5 10t-4 9q4-5-14-3l-5 0q-3 0-9-1t-12-1-7 5q-3 4 0 11 0 2 2 1-2 2-6 5t-6 5q-25-8-52-23 3 0 6 1 3 1 8 4t5 3q19 7 24 4l3 2q7-9 11-14-4 3-17 1-11-3-12-7 4-6 2-10-2 2-6 6t-8 6-8 3q-9 0-13-1-81-45-131-124 4-4 7-4 2-1 3-5t1-6 6 1q5-4 2-10 1 0 25-15 10-10 11-12 2-6-5-10-1 1-5 5t-5 2q-2-3 0-10t6-7q-4 0-5-9t-2-20 0-13l1-1q-2-6 3-19t12-11q-7-1 11-24 3-4 4-5 2-1 7-4t9-6 5-5q2-3 6-13t8-13q-2-3 5-11t6-13q-1 0-2-1t-1 0q2-4 9-8t8-7q1-2 1-6t2-6 4-1q2 11-13 35-8 13-9 16-2 2-4 8t-2 8q1 0 3 0t5-2 4-3 1-1q-1-4 1-10t7-10 10-11 6-7q4-4 8-11t0-8q5 0 11-5t10-11q3-5 4-15t3-13q1-4 5-8t7-5l9-5t7-3q3-2 10-6t12-7q6-2 9-2t8 1 8 2q8 1 16-8t12-12q20-10 30-6-1 0 1-4t4-9 5-8 3-5q3-3 10-8t10-8q4 2 4 5-1-5 4-11t10-6q8 2 8 18-17-8-27 10 0 0-2 3t-2 5-1 4 0 5 2 1q5 0 6 2t-1 7-2 8q-1 4-6 11t-7 8q-3-5-9-4t-9 5q0-1-1-3t-1-4q-7 0-8 0 1 2 1 10t2 13q1 2 3 6t5 9 2 7-3 5-9 1q-11 0-15-11-1-2-2-6t-2-6-5-4q-4-2-14-1t-13 3q-8 4-13 16t-5 20q0 6 1 15t2 14-3 14q2 1 5 5t5 6q2 1 3 1t3 0 2 1 1 3q0 1-2 2-1 1-2 1 4-1 16 1t15-1q9-6 12 1 0 1-1 6t0 7q3-15 16-5 2-1 9-3t9-2q2-1 4-3t3-3 3 0 5 4q5-8 7-13 6-23 10-25 4-2 6-1t3 5 0 8-1 7l-1 5v10l0 4q-8 2-10 7t0 10 9 10q0 1 4 2t9 4 7 4q12 11 8 20 4 0 6 5 0 0-2 2t-5 2-2 2q5 2 1 8 3 2 4 7t4 5q5-6 12-1 5 5 1 9 2 4 11 6t10 5q4-1 5 1t0 7 2 7q2 2 9 5t7 2l9 7q2 2 0 2 10-1 18 6 5 6-4 11 2 4-1 5t-9 4q2 0 7 0t5 1q9 5-3 9-10 2-24-7z m-91-490q115 21 195 106-1 2-7 2t-7 2q-10 4-13 5 1 4-1 7t-5 5-7 5-6 4q-1 1-4 3t-4 3-4 2-5 2-5-1l-2-1q-2 0-3-1t-3-2-2-1 0-2q-12 10-20 13-3 0-6 3t-6 4-6 0-6-3q-3-3-4-9t-1-7q-4 3 0 10t1 10q-1 3-6 2t-6-2-7-5-5-3-4-3-5-5q-2-2-4-6t-2-6q-1 2-7 3t-5 3q1-5 2-19t3-22q4-17-7-26-15-14-16-23-2-12 7-14 0-4-5-12t-4-12q0-3 2-9z" horiz-adv-x="857.1" />
<glyph glyph-name="brush" unicode="&#xe813;" d="M464 209q0-124-87-212t-210-87q-81 0-149 40 68 39 109 108t40 151q0 61 44 105t105 44 105-44 43-105z m415 562q32-32 32-79t-33-79l-318-318q-20 55-61 97t-97 62l318 318q32 32 79 32t80-33z" horiz-adv-x="928" />
<glyph glyph-name="attention" unicode="&#xe814;" d="M571 90v106q0 8-5 13t-12 5h-108q-7 0-12-5t-5-13v-106q0-8 5-13t12-6h108q7 0 12 6t5 13z m-1 208l10 257q0 6-5 10-7 6-14 6h-122q-6 0-14-6-5-4-5-12l9-255q0-5 6-9t13-3h103q8 0 14 3t5 9z m-7 522l428-786q20-35-1-70-9-17-26-26t-35-10h-858q-18 0-35 10t-26 26q-21 35-1 70l429 786q9 17 26 27t36 10 36-10 27-27z" horiz-adv-x="1000" />
<glyph glyph-name="plus" unicode="&#xe815;" d="M786 446v-107q0-22-16-38t-38-15h-232v-233q0-22-16-37t-38-16h-107q-22 0-38 16t-15 37v233h-232q-23 0-38 15t-16 38v107q0 23 16 38t38 16h232v232q0 22 15 38t38 16h107q23 0 38-16t16-38v-232h232q23 0 38-16t16-38z" horiz-adv-x="785.7" />
<glyph glyph-name="adjust" unicode="&#xe816;" d="M429 53v608q-83 0-153-41t-110-111-41-152 41-152 110-111 153-41z m428 304q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" />
<glyph glyph-name="edit" unicode="&#xe817;" d="M496 196l64 65-85 85-64-65v-31h53v-54h32z m245 402q-9 9-18 0l-196-196q-9-9 0-18t18 0l196 196q9 9 0 18z m45-331v-106q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h464q35 0 65-14 9-4 10-13 2-10-5-16l-27-28q-8-8-18-4-13 3-25 3h-464q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v70q0 7 5 12l36 36q8 8 20 4t11-16z m-54 411l161-160-375-375h-161v160z m248-73l-51-52-161 161 51 52q16 15 38 15t38-15l85-85q16-16 16-38t-16-38z" horiz-adv-x="1000" />
<glyph glyph-name="pencil" unicode="&#xe818;" d="M203 0l50 51-131 131-51-51v-60h72v-71h60z m291 518q0 12-12 12-5 0-9-4l-303-302q-4-4-4-10 0-12 13-12 5 0 9 4l303 302q3 4 3 10z m-30 107l232-232-464-465h-232v233z m381-54q0-29-20-50l-93-93-232 233 93 92q20 21 50 21 29 0 51-21l131-131q20-22 20-51z" horiz-adv-x="857.1" />
<glyph glyph-name="pin" unicode="&#xe819;" d="M268 375v250q0 8-5 13t-13 5-13-5-5-13v-250q0-8 5-13t13-5 13 5 5 13z m375-197q0-14-11-25t-25-10h-239l-29-270q-1-7-6-11t-11-5h-1q-15 0-17 15l-43 271h-225q-15 0-25 10t-11 25q0 69 44 124t99 55v286q-29 0-50 21t-22 50 22 50 50 22h357q29 0 50-22t21-50-21-50-50-21v-286q55 0 99-55t44-124z" horiz-adv-x="642.9" />
<glyph glyph-name="wrench" unicode="&#xe81a;" d="M214 36q0 14-10 25t-25 10-25-10-11-25 11-25 25-11 25 11 10 25z m360 234l-381-381q-21-20-50-20-29 0-51 20l-59 61q-21 20-21 50 0 29 21 51l380 380q22-55 64-97t97-64z m354 243q0-22-13-59-27-75-92-122t-144-46q-104 0-177 73t-73 177 73 176 177 74q32 0 67-10t60-26q9-6 9-15t-9-16l-163-94v-125l108-60q2 2 44 27t75 45 40 20q8 0 13-5t5-14z" horiz-adv-x="928.6" />
<glyph glyph-name="verified" unicode="&#xe81b;" d="M926 453l-19 13c-21 14-30 41-23 65l6 22c10 34-13 69-48 75l-23 4c-25 4-45 23-49 48l-4 23c-6 35-41 57-75 47l-22-7c-24-7-51 2-65 22l-14 20c-21 29-62 33-88 9l-17-16c-19-17-46-21-69-8l-20 11c-31 17-70 3-84-30l-9-22c-9-24-33-39-58-37l-23 1c-36 2-65-28-62-63l2-23c2-25-13-49-36-59l-21-9c-33-14-46-53-29-84l12-20c13-22 10-50-7-69l-15-17c-24-27-19-68 11-88l19-13c21-14 30-41 23-65l-9-23c-10-34 13-69 48-75l23-4c25-4 45-23 49-48l4-23c6-35 41-57 75-47l22 7c24 7 51-2 65-22l14-19c21-29 62-33 88-9l17 16c19 17 46 21 69 8l20-11c31-17 70-3 84 30l9 22c9 24 33 39 58 37l23-1c36-2 65 28 62 63l-1 23c-2 25 13 49 36 59l21 9c33 14 46 53 29 84l-12 20c-13 22-10 50 7 69l15 17c25 26 20 68-9 88z m-399-189l-82-81-81 82-78 79 82 81 78-79 187 186 81-82-187-186z" horiz-adv-x="1000" />
<glyph glyph-name="chart-bar" unicode="&#xe81c;" d="M357 357v-286h-143v286h143z m214 286v-572h-142v572h142z m572-643v-72h-1143v858h71v-786h1072z m-357 500v-429h-143v429h143z m214 214v-643h-143v643h143z" horiz-adv-x="1142.9" />
<glyph glyph-name="users" unicode="&#xe81d;" d="M331 357q-90-3-148-71h-75q-45 0-77 22t-31 66q0 197 69 197 4 0 25-11t54-24 66-12q38 0 75 13-3-21-3-37 0-78 45-143z m598-355q0-67-41-106t-108-39h-488q-68 0-108 39t-41 106q0 29 2 57t8 61 14 61 24 54 35 45 48 30 62 11q6 0 24-12t41-26 59-27 76-12 75 12 60 27 41 26 24 12q34 0 62-11t47-30 35-45 24-54 15-61 8-61 2-57z m-572 712q0-59-42-101t-101-42-101 42-42 101 42 101 101 42 101-42 42-101z m393-214q0-89-63-152t-151-62-152 62-63 152 63 151 152 63 151-63 63-151z m321-126q0-43-31-66t-77-22h-75q-57 68-147 71 45 65 45 143 0 16-3 37 37-13 74-13 33 0 67 12t54 24 24 11q69 0 69-197z m-71 340q0-59-42-101t-101-42-101 42-42 101 42 101 101 42 101-42 42-101z" horiz-adv-x="1071.4" />
<glyph glyph-name="address-book" unicode="&#xe81e;" d="M0-143l0 1000 1000 0 0-178-109 0 0-95 109 0 0-178-109 0 0-98 109 0 0-177-109 0 0-96 109 0 0-178-1000 0z m193 285l504 0 0 155-187 111q37 19 58 54t22 77q0 58-42 101t-103 43-102-43-42-101q0-43 22-77t58-54l-188-111 0-155z" horiz-adv-x="1000" />
<glyph glyph-name="cog-alt" unicode="&#xe81f;" d="M500 357q0 59-42 101t-101 42-101-42-42-101 42-101 101-42 101 42 42 101z m429-286q0 29-22 51t-50 21-50-21-21-51q0-29 21-50t50-21 51 21 21 50z m0 572q0 29-22 50t-50 21-50-21-21-50q0-30 21-51t50-21 51 21 21 51z m-215-235v-103q0-6-4-11t-8-6l-87-14q-6-19-18-42 19-27 50-64 4-6 4-11 0-7-4-11-12-17-46-50t-43-33q-7 0-12 4l-64 50q-21-11-43-17-6-60-13-87-4-13-17-13h-104q-6 0-11 4t-5 10l-13 85q-19 6-42 18l-66-50q-4-4-11-4-6 0-12 4-80 75-80 90 0 5 4 10 5 8 23 30t26 34q-13 24-20 46l-85 13q-5 1-9 5t-4 11v104q0 5 4 10t9 6l86 14q7 19 18 42-19 27-50 64-4 6-4 11 0 7 4 12 12 16 46 49t44 33q6 0 12-4l64-50q19 10 43 18 6 60 13 86 3 13 16 13h104q6 0 11-4t6-10l13-85q19-6 42-17l65 49q5 4 12 4 6 0 11-4 81-75 81-90 0-4-4-10-7-9-24-30t-25-34q13-27 19-46l85-12q6-2 9-6t4-11z m357-298v-78q0-9-83-17-6-15-16-29 28-63 28-77 0-2-2-4-68-40-69-40-5 0-26 27t-29 37q-11-1-17-1t-17 1q-7-11-29-37t-25-27q-1 0-69 40-3 2-3 4 0 14 29 77-10 14-17 29-83 8-83 17v78q0 9 83 18 7 16 17 29-29 63-29 77 0 2 3 4 2 1 19 11t33 19 17 9q4 0 25-26t29-38q12 1 17 1t17-1q28 40 51 63l4 1q2 0 69-39 2-2 2-4 0-14-28-77 9-13 16-29 83-9 83-18z m0 572v-78q0-9-83-18-6-15-16-29 28-63 28-77 0-2-2-4-68-39-69-39-5 0-26 26t-29 38q-11-1-17-1t-17 1q-7-12-29-38t-25-26q-1 0-69 39-3 2-3 4 0 14 29 77-10 14-17 29-83 9-83 18v78q0 9 83 17 7 16 17 29-29 63-29 77 0 2 3 4 2 1 19 11t33 19 17 9q4 0 25-26t29-37q12 1 17 1t17-1q28 39 51 62l4 1q2 0 69-39 2-2 2-4 0-14-28-77 9-13 16-29 83-8 83-17z" horiz-adv-x="1071.4" />
<glyph glyph-name="zoom-in" unicode="&#xe820;" d="M571 411v-36q0-7-5-13t-12-5h-125v-125q0-7-6-13t-12-5h-36q-7 0-13 5t-5 13v125h-125q-7 0-12 5t-6 13v36q0 7 6 12t12 5h125v125q0 8 5 13t13 5h36q7 0 12-5t6-13v-125h125q7 0 12-5t5-12z m72-18q0 103-73 176t-177 74-177-74-73-176 73-177 177-73 177 73 73 177z m286-465q0-29-21-50t-51-21q-30 0-50 21l-191 191q-100-69-223-69-80 0-153 31t-125 84-84 125-31 153 31 152 84 126 125 84 153 31 153-31 125-84 84-126 31-152q0-123-69-223l191-191q21-21 21-51z" horiz-adv-x="928.6" />
<glyph glyph-name="home-2" unicode="&#xe821;" d="M521 826q322-279 500-429 20-16 20-40 0-21-15-37t-36-15l-105 0 0-364q0-21-15-37t-36-16l-156 0q-22 0-37 16t-16 37l0 208-209 0 0-208q0-21-15-37t-36-16l-156 0q-21 0-37 16t-16 37l0 364-103 0q-22 0-37 15t-16 37 19 40z" horiz-adv-x="1041" />
<glyph glyph-name="spin3" unicode="&#xe832;" d="M494 857c-266 0-483-210-494-472-1-19 13-20 13-20l84 0c16 0 19 10 19 18 10 199 176 358 378 358 107 0 205-45 273-118l-58-57c-11-12-11-27 5-31l247-50c21-5 46 11 37 44l-58 227c-2 9-16 22-29 13l-65-60c-89 91-214 148-352 148z m409-508c-16 0-19-10-19-18-10-199-176-358-377-358-108 0-205 45-274 118l59 57c10 12 10 27-5 31l-248 50c-21 5-46-11-37-44l58-227c2-9 16-22 30-13l64 60c89-91 214-148 353-148 265 0 482 210 493 473 1 18-13 19-13 19l-84 0z" horiz-adv-x="1000" />
<glyph glyph-name="spin4" unicode="&#xe834;" d="M498 857c-114 0-228-39-320-116l0 0c173 140 428 130 588-31 134-134 164-332 89-495-10-29-5-50 12-68 21-20 61-23 84 0 3 3 12 15 15 24 71 180 33 393-112 539-99 98-228 147-356 147z m-409-274c-14 0-29-5-39-16-3-3-13-15-15-24-71-180-34-393 112-539 185-185 479-195 676-31l0 0c-173-140-428-130-589 31-134 134-163 333-89 495 11 29 6 50-12 68-11 11-27 17-44 16z" horiz-adv-x="1001" />
<glyph glyph-name="link-ext" unicode="&#xf08e;" d="M786 339v-178q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h393q7 0 12-5t5-13v-36q0-8-5-13t-12-5h-393q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v178q0 8 5 13t13 5h36q8 0 13-5t5-13z m214 482v-285q0-15-11-25t-25-11-25 11l-98 98-364-364q-5-6-13-6t-12 6l-64 64q-6 5-6 12t6 13l364 364-98 98q-11 11-11 25t11 25 25 11h285q15 0 25-11t11-25z" horiz-adv-x="1000" />
<glyph glyph-name="link-ext-alt" unicode="&#xf08f;" d="M714 339v268q0 15-10 25t-25 11h-268q-24 0-33-22-10-23 8-39l80-80-298-298q-11-11-11-26t11-25l57-57q11-10 25-10t25 10l298 298 81-80q10-11 25-11 6 0 14 3 21 10 21 33z m143 286v-536q0-66-47-113t-114-48h-535q-67 0-114 48t-47 113v536q0 66 47 113t114 48h535q67 0 114-48t47-113z" horiz-adv-x="857.1" />
<glyph glyph-name="menu" unicode="&#xf0c9;" d="M857 107v-71q0-15-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 25t25 11h785q15 0 26-11t10-25z m0 286v-72q0-14-10-25t-26-10h-785q-15 0-25 10t-11 25v72q0 14 11 25t25 10h785q15 0 26-10t10-25z m0 285v-71q0-14-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 26t25 10h785q15 0 26-10t10-26z" horiz-adv-x="857.1" />
<glyph glyph-name="mail-alt" unicode="&#xf0e0;" d="M1000 461v-443q0-37-26-63t-63-27h-822q-36 0-63 27t-26 63v443q25-27 56-49 202-137 278-192 32-24 51-37t53-27 61-13h2q28 0 61 13t53 27 51 37q95 68 278 192 32 22 56 49z m0 164q0-44-27-84t-68-69q-210-146-262-181-5-4-23-17t-30-22-29-18-32-15-28-5h-2q-12 0-27 5t-32 15-30 18-30 22-23 17q-51 35-147 101t-114 80q-35 23-65 64t-31 77q0 43 23 72t66 29h822q36 0 63-26t26-63z" horiz-adv-x="1000" />
<glyph glyph-name="gauge" unicode="&#xf0e4;" d="M214 214q0 30-21 51t-50 21-51-21-21-51 21-50 51-21 50 21 21 50z m107 250q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m239-268l57 213q3 14-5 27t-21 16-27-3-17-22l-56-213q-33-3-60-25t-35-55q-11-43 11-81t66-50 81 11 50 66q9 33-4 65t-40 51z m369 18q0 30-21 51t-51 21-50-21-21-51 21-50 50-21 51 21 21 50z m-358 357q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m250-107q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m179-250q0-145-79-269-10-17-30-17h-782q-20 0-30 17-79 123-79 269 0 102 40 194t106 160 160 107 194 39 194-39 160-107 106-160 40-194z" horiz-adv-x="1000" />
<glyph glyph-name="comment-empty" unicode="&#xf0e5;" d="M500 643q-114 0-213-39t-157-105-59-142q0-62 40-119t113-98l48-28-15-53q-13-51-39-97 85 36 154 96l24 21 32-3q38-5 72-5 114 0 213 39t157 105 59 142-59 142-157 105-213 39z m500-286q0-97-67-179t-182-130-251-48q-39 0-81 4-110-97-257-135-27-8-63-12h-3q-8 0-15 6t-9 15v1q-2 2 0 6t1 6 2 5l4 5t4 5 4 5q4 5 17 19t20 22 17 22 18 28 15 33 15 42q-88 50-138 123t-51 157q0 97 67 179t182 130 251 48 251-48 182-130 67-179z" horiz-adv-x="1000" />
<glyph glyph-name="bell-alt" unicode="&#xf0f3;" d="M509-89q0 8-9 8-33 0-57 24t-23 57q0 9-9 9t-9-9q0-41 29-70t69-28q9 0 9 9z m455 160q0-29-21-50t-50-21h-250q0-59-42-101t-101-42-101 42-42 101h-250q-29 0-50 21t-21 50q28 24 51 49t47 67 42 89 27 115 11 145q0 84 66 157t171 89q-5 10-5 21 0 23 16 38t38 16 38-16 16-38q0-11-5-21 106-16 171-89t66-157q0-78 11-145t28-115 41-89 48-67 50-49z" horiz-adv-x="1000" />
<glyph glyph-name="plus-squared" unicode="&#xf0fe;" d="M714 321v72q0 14-10 25t-25 10h-179v179q0 15-11 25t-25 11h-71q-15 0-25-11t-11-25v-179h-178q-15 0-25-10t-11-25v-72q0-14 11-25t25-10h178v-179q0-14 11-25t25-11h71q15 0 25 11t11 25v179h179q14 0 25 10t10 25z m143 304v-536q0-66-47-113t-114-48h-535q-67 0-114 48t-47 113v536q0 66 47 113t114 48h535q67 0 114-48t47-113z" horiz-adv-x="857.1" />
<glyph glyph-name="laptop" unicode="&#xf109;" d="M232 143q-37 0-63 26t-26 63v393q0 37 26 63t63 26h607q37 0 63-26t27-63v-393q0-37-27-63t-63-26h-607z m-18 482v-393q0-7 6-13t12-5h607q8 0 13 5t5 13v393q0 7-5 12t-13 6h-607q-7 0-12-6t-6-12z m768-518h89v-54q0-22-26-37t-63-16h-893q-36 0-63 16t-26 37v54h982z m-402-54q9 0 9 9t-9 9h-89q-9 0-9-9t9-9h89z" horiz-adv-x="1071.4" />
<glyph glyph-name="quote-right" unicode="&#xf10e;" d="M429 678v-392q0-58-23-111t-61-91-91-61-111-23h-36q-14 0-25 11t-11 25v71q0 15 11 25t25 11h36q59 0 101 42t42 101v17q0 23-16 38t-38 16h-125q-44 0-76 31t-31 76v214q0 45 31 76t76 32h214q45 0 76-32t32-76z m500 0v-392q0-58-23-111t-61-91-91-61-111-23h-36q-14 0-25 11t-11 25v71q0 15 11 25t25 11h36q59 0 101 42t42 101v17q0 23-16 38t-38 16h-125q-44 0-76 31t-31 76v214q0 45 31 76t76 32h214q45 0 76-32t32-76z" horiz-adv-x="928.6" />
<glyph glyph-name="reply" unicode="&#xf112;" d="M1000 232q0-93-71-252-1-4-6-13t-7-17-7-12q-7-10-16-10-8 0-13 6t-5 14q0 5 1 15t2 13q3 38 3 69 0 56-10 101t-27 77-45 56-59 39-74 24-86 12-98 3h-125v-143q0-14-10-25t-26-11-25 11l-285 286q-11 10-11 25t11 25l285 286q11 10 25 10t26-10 10-25v-143h125q398 0 488-225 30-75 30-186z" horiz-adv-x="1000" />
<glyph glyph-name="smile" unicode="&#xf118;" d="M633 257q-21-67-77-109t-127-41-128 41-77 109q-4 14 3 27t21 18q14 4 27-2t17-22q14-44 52-72t85-28 84 28 52 72q4 15 18 22t27 2 21-18 2-27z m-276 243q0-30-21-51t-50-21-51 21-21 51 21 50 51 21 50-21 21-50z m286 0q0-30-21-51t-51-21-50 21-21 51 21 50 50 21 51-21 21-50z m143-143q0 73-29 139t-76 114-114 76-138 28-139-28-114-76-76-114-29-139 29-139 76-113 114-77 139-28 138 28 114 77 76 113 29 139z m71 0q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" />
<glyph glyph-name="lock-open-alt" unicode="&#xf13e;" d="M589 428q23 0 38-15t16-38v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h17v179q0 103 74 177t176 73 177-73 73-177q0-14-10-25t-25-11h-36q-14 0-25 11t-11 25q0 59-42 101t-101 42-101-42-41-101v-179h410z" horiz-adv-x="642.9" />
<glyph glyph-name="ellipsis" unicode="&#xf141;" d="M214 446v-107q0-22-15-38t-38-15h-107q-23 0-38 15t-16 38v107q0 23 16 38t38 16h107q22 0 38-16t15-38z m286 0v-107q0-22-16-38t-38-15h-107q-22 0-38 15t-15 38v107q0 23 15 38t38 16h107q23 0 38-16t16-38z m286 0v-107q0-22-16-38t-38-15h-107q-22 0-38 15t-16 38v107q0 23 16 38t38 16h107q23 0 38-16t16-38z" horiz-adv-x="785.7" />
<glyph glyph-name="play-circled" unicode="&#xf144;" d="M429 786q116 0 215-58t156-156 57-215-57-215-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58z m214-460q18 10 18 31t-18 31l-304 178q-17 11-35 1-18-11-18-31v-358q0-20 18-31 9-4 17-4 10 0 18 5z" horiz-adv-x="857.1" />
<glyph glyph-name="thumbs-up-alt" unicode="&#xf164;" d="M143 107q0 15-11 25t-25 11q-15 0-25-11t-11-25q0-15 11-25t25-11q15 0 25 11t11 25z m89 286v-357q0-15-10-25t-26-11h-160q-15 0-25 11t-11 25v357q0 14 11 25t25 10h160q15 0 26-10t10-25z m661 0q0-48-31-83 9-25 9-43 1-42-24-76 9-31 0-66-9-31-31-52 5-62-27-101-36-43-110-44h-72q-37 0-80 9t-68 16-67 22q-69 24-88 25-15 0-25 11t-11 25v357q0 14 10 25t24 11q13 1 42 33t57 67q38 49 56 67 10 10 17 27t10 27 8 34q4 22 7 34t11 29 19 28q10 11 25 11 25 0 46-6t33-15 22-22 14-25 7-28 2-25 1-22q0-21-6-43t-10-33-16-31q-1-4-5-10t-6-13-5-13h155q43 0 75-32t32-75z" horiz-adv-x="928.6" />
<glyph glyph-name="apple" unicode="&#xf179;" d="M777 179q-21-70-68-139-72-110-144-110-27 0-78 18-48 18-84 18-34 0-79-19-45-19-74-19-85 0-168 145-82 146-82 281 0 127 63 208 63 81 159 81 40 0 98-17 58-17 77-17 25 0 80 19 57 19 97 19 66 0 119-36 29-20 58-56-44-37-64-66-36-52-36-115 0-69 38-125t88-70z m-209 655q0-34-17-76-16-42-52-77-30-30-60-40-20-7-58-10 2 83 44 143 41 60 139 83 1-2 2-6t1-6q0-2 0-6t1-5z" horiz-adv-x="785.7" />
<glyph glyph-name="android" unicode="&#xf17b;" d="M275 588q9 0 16 6t6 15-6 16-16 6-15-6-6-16 6-15 15-6z m236 0q9 0 15 6t6 15-6 16-15 6-16-6-6-16 6-15 16-6z m-453-103q23 0 40-17t16-40v-240q0-24-16-41t-40-17-41 17-17 41v240q0 23 17 40t41 17z m591-11v-371q0-26-18-44t-43-18h-42v-127q0-24-16-40t-41-17-41 17-17 40v127h-77v-127q0-24-16-40t-41-17q-24 0-40 17t-17 40l-1 127h-41q-26 0-43 18t-18 44v371h512z m-129 226q59-30 95-85t36-121h-516q0 66 35 121t96 85l-39 73q-4 8 2 12 8 3 12-4l40-74q53 24 112 24t112-24l40 74q4 7 11 4 7-4 3-12z m266-272v-240q0-24-17-41t-41-17q-23 0-40 17t-17 41v240q0 24 17 40t40 17q24 0 41-17t17-40z" horiz-adv-x="785.7" />
<glyph glyph-name="paper-plane-empty" unicode="&#xf1d9;" d="M984 851q19-13 15-36l-142-857q-3-16-18-25-8-5-18-5-6 0-13 3l-294 120-166-182q-10-12-27-12-7 0-12 2-11 4-17 13t-6 21v252l-264 108q-20 8-22 30-2 22 18 33l928 536q20 12 38-1z m-190-837l123 739-800-462 187-76 482 356-267-444z" horiz-adv-x="1000" />
<glyph glyph-name="binoculars" unicode="&#xf1e5;" d="M393 678v-428q0-15-11-25t-25-11v-321q0-15-10-25t-26-11h-285q-15 0-25 11t-11 25v285l139 488q4 12 17 12h237z m178 0v-392h-142v392h142z m429-500v-285q0-15-11-25t-25-11h-285q-15 0-25 11t-11 25v321q-15 0-25 11t-11 25v428h237q13 0 17-12z m-589 661v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z m375 0v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z" horiz-adv-x="1000" />
<glyph glyph-name="user-plus" unicode="&#xf234;" d="M393 357q-89 0-152 63t-62 151 62 152 152 63 151-63 63-152-63-151-151-63z m536-71h196q7 0 13-6t5-12v-107q0-8-5-13t-13-5h-196v-197q0-7-6-12t-12-6h-107q-8 0-13 6t-5 12v197h-197q-7 0-12 5t-6 13v107q0 7 6 12t12 6h197v196q0 7 5 13t13 5h107q7 0 12-5t6-13v-196z m-411-125q0-29 21-51t50-21h143v-133q-38-28-95-28h-488q-67 0-108 39t-41 106q0 30 2 58t8 61 15 60 24 55 34 45 48 30 62 11q11 0 22-10 44-34 86-51t92-17 92 17 86 51q11 10 22 10 73 0 121-54h-125q-29 0-50-21t-21-50v-107z" horiz-adv-x="1142.9" />
<glyph glyph-name="hashtag" unicode="&#xf292;" d="M553 286l36 142h-142l-36-142h142z m429 281l-32-125q-4-14-17-14h-182l-36-142h173q9 0 14-7 6-8 4-16l-32-125q-2-13-17-13h-182l-45-183q-4-14-18-14h-125q-9 0-14 7-5 7-4 16l44 174h-142l-45-183q-4-14-17-14h-126q-8 0-14 7-5 7-3 16l43 174h-173q-9 0-14 7-5 6-4 15l32 125q4 14 17 14h182l36 142h-173q-9 0-14 7-6 8-4 16l32 125q2 13 17 13h182l46 183q3 14 17 14h125q9 0 14-7 5-7 4-16l-44-174h142l45 183q4 14 18 14h125q8 0 14-7 5-7 3-16l-43-174h173q9 0 14-7 5-6 4-15z" horiz-adv-x="1000" />
</font>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 28 KiB

View file

@ -1,146 +0,0 @@
@font-face {
font-family: "Icons";
src: url("./font/fontello.1575660578688.eot");
src: url("./font/fontello.1575660578688.eot") format("embedded-opentype"),
url("./font/fontello.1575660578688.woff2") format("woff2"),
url("./font/fontello.1575660578688.woff") format("woff"),
url("./font/fontello.1575660578688.ttf") format("truetype"),
url("./font/fontello.1575660578688.svg") format("svg");
font-weight: normal;
font-style: normal;
}
[class^="icon-"]::before,
[class*=" icon-"]::before {
font-family: "Icons";
font-style: normal;
font-weight: normal;
speak: none;
display: inline-block;
text-decoration: inherit;
width: 1em;
margin-right: .2em;
text-align: center;
font-variant: normal;
text-transform: none;
line-height: 1em;
margin-left: .2em;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-cancel::before { content: "\e800"; }
.icon-upload::before { content: "\e801"; }
.icon-spin3::before { content: "\e832"; }
.icon-reply::before { content: "\f112"; }
.icon-star::before { content: "\e802"; }
.icon-star-empty::before { content: "\e803"; }
.icon-retweet::before { content: "\e804"; }
.icon-eye-off::before { content: "\e805"; }
.icon-binoculars::before { content: "\f1e5"; }
.icon-cog::before { content: "\e807"; }
.icon-user-plus::before { content: "\f234"; }
.icon-menu::before { content: "\f0c9"; }
.icon-logout::before { content: "\e808"; }
.icon-down-open::before { content: "\e809"; }
.icon-attach::before { content: "\e80a"; }
.icon-link-ext::before { content: "\f08e"; }
.icon-link-ext-alt::before { content: "\f08f"; }
.icon-picture::before { content: "\e80b"; }
.icon-video::before { content: "\e80c"; }
.icon-right-open::before { content: "\e80d"; }
.icon-left-open::before { content: "\e80e"; }
.icon-up-open::before { content: "\e80f"; }
.icon-comment-empty::before { content: "\f0e5"; }
.icon-mail-alt::before { content: "\f0e0"; }
.icon-lock::before { content: "\e811"; }
.icon-lock-open-alt::before { content: "\f13e"; }
.icon-globe::before { content: "\e812"; }
.icon-brush::before { content: "\e813"; }
.icon-search::before { content: "\e806"; }
.icon-adjust::before { content: "\e816"; }
.icon-thumbs-up-alt::before { content: "\f164"; }
.icon-attention::before { content: "\e814"; }
.icon-plus-squared::before { content: "\f0fe"; }
.icon-plus::before { content: "\e815"; }
.icon-edit::before { content: "\e817"; }
.icon-play-circled::before { content: "\f144"; }
.icon-pencil::before { content: "\e818"; }
.icon-spin4::before { content: "\e834"; }
.icon-verified::before { content: "\e81b"; }
.icon-smile::before { content: "\f118"; }
.icon-bell-alt::before { content: "\f0f3"; }
.icon-wrench::before { content: "\e81a"; }
.icon-pin::before { content: "\e819"; }
.icon-ellipsis::before { content: "\f141"; }
.icon-bell-ringing-o::before { content: "\e810"; }
.icon-users::before { content: "\e81d"; }
.icon-address-book::before { content: "\e81e"; }
.icon-cog-alt::before { content: "\e81f"; }
.icon-apple::before { content: "\f179"; }
.icon-android::before { content: "\f17b"; }
.icon-home-2::before { content: "\e821"; }
.icon-hashtag::before { content: "\f292"; }
.icon-quote-right::before { content: "\f10e"; }
.icon-laptop::before { content: "\f109"; }
.icon-chart-bar::before { content: "\e81c"; }
.icon-zoom-in::before { content: "\e820"; }
.icon-gauge::before { content: "\f0e4"; }
.icon-paper-plane-empty::before { content: "\f1d9"; }

View file

@ -1,146 +0,0 @@
@font-face {
font-family: "Icons";
src: url("./font/fontello.1575662648966.eot");
src: url("./font/fontello.1575662648966.eot") format("embedded-opentype"),
url("./font/fontello.1575662648966.woff2") format("woff2"),
url("./font/fontello.1575662648966.woff") format("woff"),
url("./font/fontello.1575662648966.ttf") format("truetype"),
url("./font/fontello.1575662648966.svg") format("svg");
font-weight: normal;
font-style: normal;
}
[class^="icon-"]::before,
[class*=" icon-"]::before {
font-family: "Icons";
font-style: normal;
font-weight: normal;
speak: none;
display: inline-block;
text-decoration: inherit;
width: 1em;
margin-right: .2em;
text-align: center;
font-variant: normal;
text-transform: none;
line-height: 1em;
margin-left: .2em;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-cancel::before { content: "\e800"; }
.icon-upload::before { content: "\e801"; }
.icon-spin3::before { content: "\e832"; }
.icon-reply::before { content: "\f112"; }
.icon-star::before { content: "\e802"; }
.icon-star-empty::before { content: "\e803"; }
.icon-retweet::before { content: "\e804"; }
.icon-eye-off::before { content: "\e805"; }
.icon-binoculars::before { content: "\f1e5"; }
.icon-cog::before { content: "\e807"; }
.icon-user-plus::before { content: "\f234"; }
.icon-menu::before { content: "\f0c9"; }
.icon-logout::before { content: "\e808"; }
.icon-down-open::before { content: "\e809"; }
.icon-attach::before { content: "\e80a"; }
.icon-link-ext::before { content: "\f08e"; }
.icon-link-ext-alt::before { content: "\f08f"; }
.icon-picture::before { content: "\e80b"; }
.icon-video::before { content: "\e80c"; }
.icon-right-open::before { content: "\e80d"; }
.icon-left-open::before { content: "\e80e"; }
.icon-up-open::before { content: "\e80f"; }
.icon-comment-empty::before { content: "\f0e5"; }
.icon-mail-alt::before { content: "\f0e0"; }
.icon-lock::before { content: "\e811"; }
.icon-lock-open-alt::before { content: "\f13e"; }
.icon-globe::before { content: "\e812"; }
.icon-brush::before { content: "\e813"; }
.icon-search::before { content: "\e806"; }
.icon-adjust::before { content: "\e816"; }
.icon-thumbs-up-alt::before { content: "\f164"; }
.icon-attention::before { content: "\e814"; }
.icon-plus-squared::before { content: "\f0fe"; }
.icon-plus::before { content: "\e815"; }
.icon-edit::before { content: "\e817"; }
.icon-play-circled::before { content: "\f144"; }
.icon-pencil::before { content: "\e818"; }
.icon-spin4::before { content: "\e834"; }
.icon-verified::before { content: "\e81b"; }
.icon-smile::before { content: "\f118"; }
.icon-bell-alt::before { content: "\f0f3"; }
.icon-wrench::before { content: "\e81a"; }
.icon-pin::before { content: "\e819"; }
.icon-ellipsis::before { content: "\f141"; }
.icon-bell-ringing-o::before { content: "\e810"; }
.icon-users::before { content: "\e81d"; }
.icon-address-book::before { content: "\e81e"; }
.icon-cog-alt::before { content: "\e81f"; }
.icon-apple::before { content: "\f179"; }
.icon-android::before { content: "\f17b"; }
.icon-home-2::before { content: "\e821"; }
.icon-hashtag::before { content: "\f292"; }
.icon-quote-right::before { content: "\f10e"; }
.icon-laptop::before { content: "\f109"; }
.icon-chart-bar::before { content: "\e81c"; }
.icon-zoom-in::before { content: "\e820"; }
.icon-gauge::before { content: "\f0e4"; }
.icon-paper-plane-empty::before { content: "\f1d9"; }

View file

@ -1 +1 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1,user-scalable=no"><title>Pleroma</title><!--server-generated-meta--><link rel=icon type=image/png href=/favicon.png><link href=/static/css/vendors~app.b2603a50868c68a1c192.css rel=stylesheet><link href=/static/css/app.1055039ce3f2fe4dd110.css rel=stylesheet><link href=/static/fontello.1583594169021.css rel=stylesheet></head><body class=hidden><noscript>To use Pleroma, please enable JavaScript.</noscript><div id=app></div><script type=text/javascript src=/static/js/vendors~app.c5bbd3734647f0cc7eef.js></script><script type=text/javascript src=/static/js/app.5c94bdec79a7d0f3cfcb.js></script></body></html>
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1,user-scalable=no"><title>Pleroma</title><!--server-generated-meta--><link rel=icon type=image/png href=/favicon.png><link href=/static/css/vendors~app.b2603a50868c68a1c192.css rel=stylesheet><link href=/static/css/app.1055039ce3f2fe4dd110.css rel=stylesheet><link href=/static/fontello.1587147224637.css rel=stylesheet></head><body class=hidden><noscript>To use Pleroma, please enable JavaScript.</noscript><div id=app></div><script type=text/javascript src=/static/js/vendors~app.c5bbd3734647f0cc7eef.js></script><script type=text/javascript src=/static/js/app.def6476e8bc9b214218b.js></script></body></html>

View file

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

View file

@ -1,11 +1,11 @@
@font-face {
font-family: "Icons";
src: url("./font/fontello.1583594169021.eot");
src: url("./font/fontello.1583594169021.eot") format("embedded-opentype"),
url("./font/fontello.1583594169021.woff2") format("woff2"),
url("./font/fontello.1583594169021.woff") format("woff"),
url("./font/fontello.1583594169021.ttf") format("truetype"),
url("./font/fontello.1583594169021.svg") format("svg");
src: url("./font/fontello.1587147224637.eot");
src: url("./font/fontello.1587147224637.eot") format("embedded-opentype"),
url("./font/fontello.1587147224637.woff2") format("woff2"),
url("./font/fontello.1587147224637.woff") format("woff"),
url("./font/fontello.1587147224637.ttf") format("truetype"),
url("./font/fontello.1587147224637.svg") format("svg");
font-weight: normal;
font-style: normal;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,4 +1,4 @@
var serviceWorkerOption = {"assets":["/static/fontello.1583594169021.css","/static/font/fontello.1583594169021.eot","/static/font/fontello.1583594169021.svg","/static/font/fontello.1583594169021.ttf","/static/font/fontello.1583594169021.woff","/static/font/fontello.1583594169021.woff2","/static/img/nsfw.74818f9.png","/static/css/app.1055039ce3f2fe4dd110.css","/static/js/app.5c94bdec79a7d0f3cfcb.js","/static/css/vendors~app.b2603a50868c68a1c192.css","/static/js/vendors~app.c5bbd3734647f0cc7eef.js","/static/js/2.f158cbd2b8770e467dfe.js"]};
var serviceWorkerOption = {"assets":["/static/fontello.1587147224637.css","/static/font/fontello.1587147224637.eot","/static/font/fontello.1587147224637.svg","/static/font/fontello.1587147224637.ttf","/static/font/fontello.1587147224637.woff","/static/font/fontello.1587147224637.woff2","/static/img/nsfw.74818f9.png","/static/css/app.1055039ce3f2fe4dd110.css","/static/js/app.def6476e8bc9b214218b.js","/static/css/vendors~app.b2603a50868c68a1c192.css","/static/js/vendors~app.c5bbd3734647f0cc7eef.js","/static/js/2.f158cbd2b8770e467dfe.js"]};
!function(e){var n={};function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)t.d(r,o,function(n){return e[n]}.bind(null,o));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="/",t(t.s=1)}([function(e,n){
/*!

View file

@ -16,6 +16,7 @@ defmodule Pleroma.Config.TransferTaskTest do
refute Application.get_env(:pleroma, :test_key)
refute Application.get_env(:idna, :test_key)
refute Application.get_env(:quack, :test_key)
initial = Application.get_env(:logger, :level)
ConfigDB.create(%{
group: ":pleroma",
@ -35,16 +36,20 @@ defmodule Pleroma.Config.TransferTaskTest do
value: [:test_value1, :test_value2]
})
ConfigDB.create(%{group: ":logger", key: ":level", value: :debug})
TransferTask.start_link([])
assert Application.get_env(:pleroma, :test_key) == [live: 2, com: 3]
assert Application.get_env(:idna, :test_key) == [live: 15, com: 35]
assert Application.get_env(:quack, :test_key) == [:test_value1, :test_value2]
assert Application.get_env(:logger, :level) == :debug
on_exit(fn ->
Application.delete_env(:pleroma, :test_key)
Application.delete_env(:idna, :test_key)
Application.delete_env(:quack, :test_key)
Application.put_env(:logger, :level, initial)
end)
end
@ -78,8 +83,8 @@ defmodule Pleroma.Config.TransferTaskTest do
end
test "transfer config values with full subkey update" do
emoji = Application.get_env(:pleroma, :emoji)
assets = Application.get_env(:pleroma, :assets)
clear_config(:emoji)
clear_config(:assets)
ConfigDB.create(%{
group: ":pleroma",
@ -99,11 +104,6 @@ defmodule Pleroma.Config.TransferTaskTest do
assert emoji_env[:groups] == [a: 1, b: 2]
assets_env = Application.get_env(:pleroma, :assets)
assert assets_env[:mascots] == [a: 1, b: 2]
on_exit(fn ->
Application.put_env(:pleroma, :emoji, emoji)
Application.put_env(:pleroma, :assets, assets)
end)
end
describe "pleroma restart" do
@ -112,8 +112,7 @@ defmodule Pleroma.Config.TransferTaskTest do
end
test "don't restart if no reboot time settings were changed" do
emoji = Application.get_env(:pleroma, :emoji)
on_exit(fn -> Application.put_env(:pleroma, :emoji, emoji) end)
clear_config(:emoji)
ConfigDB.create(%{
group: ":pleroma",
@ -128,8 +127,7 @@ defmodule Pleroma.Config.TransferTaskTest do
end
test "on reboot time key" do
chat = Application.get_env(:pleroma, :chat)
on_exit(fn -> Application.put_env(:pleroma, :chat, chat) end)
clear_config(:chat)
ConfigDB.create(%{
group: ":pleroma",
@ -141,8 +139,7 @@ defmodule Pleroma.Config.TransferTaskTest do
end
test "on reboot time subkey" do
captcha = Application.get_env(:pleroma, Pleroma.Captcha)
on_exit(fn -> Application.put_env(:pleroma, Pleroma.Captcha, captcha) end)
clear_config(Pleroma.Captcha)
ConfigDB.create(%{
group: ":pleroma",
@ -154,13 +151,8 @@ defmodule Pleroma.Config.TransferTaskTest do
end
test "don't restart pleroma on reboot time key and subkey if there is false flag" do
chat = Application.get_env(:pleroma, :chat)
captcha = Application.get_env(:pleroma, Pleroma.Captcha)
on_exit(fn ->
Application.put_env(:pleroma, :chat, chat)
Application.put_env(:pleroma, Pleroma.Captcha, captcha)
end)
clear_config(:chat)
clear_config(Pleroma.Captcha)
ConfigDB.create(%{
group: ":pleroma",

View file

@ -3,7 +3,6 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emoji.FormatterTest do
alias Pleroma.Emoji
alias Pleroma.Emoji.Formatter
use Pleroma.DataCase
@ -32,30 +31,19 @@ defmodule Pleroma.Emoji.FormatterTest do
end
end
describe "get_emoji" do
describe "get_emoji_map" do
test "it returns the emoji used in the text" do
text = "I love :firefox:"
assert Formatter.get_emoji(text) == [
{"firefox",
%Emoji{
code: "firefox",
file: "/emoji/Firefox.gif",
tags: ["Gif", "Fun"],
safe_code: "firefox",
safe_file: "/emoji/Firefox.gif"
}}
]
assert Formatter.get_emoji_map("I love :firefox:") == %{
"firefox" => "http://localhost:4001/emoji/Firefox.gif"
}
end
test "it returns a nice empty result when no emojis are present" do
text = "I love moominamma"
assert Formatter.get_emoji(text) == []
assert Formatter.get_emoji_map("I love moominamma") == %{}
end
test "it doesn't die when text is absent" do
text = nil
assert Formatter.get_emoji(text) == []
assert Formatter.get_emoji_map(nil) == %{}
end
end
end

View file

@ -140,7 +140,7 @@ defmodule Pleroma.FormatterTest do
archaeme =
insert(:user,
nickname: "archa_eme_",
source_data: %{"url" => "https://archeme/@archa_eme_"}
uri: "https://archeme/@archa_eme_"
)
archaeme_remote = insert(:user, %{nickname: "archaeme@archae.me"})

View file

@ -8,11 +8,13 @@ defmodule Pleroma.NotificationTest do
import Pleroma.Factory
import Mock
alias Pleroma.FollowingRelationship
alias Pleroma.Notification
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.NotificationView
alias Pleroma.Web.Push
alias Pleroma.Web.Streamer
@ -272,16 +274,6 @@ defmodule Pleroma.NotificationTest do
refute Notification.create_notification(activity, author)
end
test "it doesn't create a notification for follow-unfollow-follow chains" do
user = insert(:user)
followed_user = insert(:user)
{:ok, _, _, activity} = CommonAPI.follow(user, followed_user)
Notification.create_notification(activity, followed_user)
CommonAPI.unfollow(user, followed_user)
{:ok, _, _, activity_dupe} = CommonAPI.follow(user, followed_user)
refute Notification.create_notification(activity_dupe, followed_user)
end
test "it doesn't create duplicate notifications for follow+subscribed users" do
user = insert(:user)
subscriber = insert(:user)
@ -304,6 +296,74 @@ defmodule Pleroma.NotificationTest do
end
end
describe "follow / follow_request notifications" do
test "it creates `follow` notification for approved Follow activity" do
user = insert(:user)
followed_user = insert(:user, locked: false)
{:ok, _, _, _activity} = CommonAPI.follow(user, followed_user)
assert FollowingRelationship.following?(user, followed_user)
assert [notification] = Notification.for_user(followed_user)
assert %{type: "follow"} =
NotificationView.render("show.json", %{
notification: notification,
for: followed_user
})
end
test "if `follow_request` notifications are enabled, " <>
"it creates `follow_request` notification for pending Follow activity" do
clear_config([:notifications, :enable_follow_request_notifications], true)
user = insert(:user)
followed_user = insert(:user, locked: true)
{:ok, _, _, _activity} = CommonAPI.follow(user, followed_user)
refute FollowingRelationship.following?(user, followed_user)
assert [notification] = Notification.for_user(followed_user)
render_opts = %{notification: notification, for: followed_user}
assert %{type: "follow_request"} = NotificationView.render("show.json", render_opts)
# After request is accepted, the same notification is rendered with type "follow":
assert {:ok, _} = CommonAPI.accept_follow_request(user, followed_user)
notification_id = notification.id
assert [%{id: ^notification_id}] = Notification.for_user(followed_user)
assert %{type: "follow"} = NotificationView.render("show.json", render_opts)
end
test "if `follow_request` notifications are disabled, " <>
"it does NOT create `follow*` notification for pending Follow activity" do
clear_config([:notifications, :enable_follow_request_notifications], false)
user = insert(:user)
followed_user = insert(:user, locked: true)
{:ok, _, _, _activity} = CommonAPI.follow(user, followed_user)
refute FollowingRelationship.following?(user, followed_user)
assert [] = Notification.for_user(followed_user)
# After request is accepted, no new notifications are generated:
assert {:ok, _} = CommonAPI.accept_follow_request(user, followed_user)
assert [] = Notification.for_user(followed_user)
end
test "it doesn't create a notification for follow-unfollow-follow chains" do
user = insert(:user)
followed_user = insert(:user, locked: false)
{:ok, _, _, _activity} = CommonAPI.follow(user, followed_user)
assert FollowingRelationship.following?(user, followed_user)
assert [notification] = Notification.for_user(followed_user)
CommonAPI.unfollow(user, followed_user)
{:ok, _, _, _activity_dupe} = CommonAPI.follow(user, followed_user)
notification_id = notification.id
assert [%{id: ^notification_id}] = Notification.for_user(followed_user)
end
end
describe "get notification" do
test "it gets a notification that belongs to the user" do
user = insert(:user)

View file

@ -6,6 +6,8 @@ defmodule Pleroma.Plugs.AuthenticationPlugTest do
use Pleroma.Web.ConnCase, async: true
alias Pleroma.Plugs.AuthenticationPlug
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.PlugHelper
alias Pleroma.User
import ExUnit.CaptureLog
@ -36,13 +38,16 @@ defmodule Pleroma.Plugs.AuthenticationPlugTest do
assert ret_conn == conn
end
test "with a correct password in the credentials, it assigns the auth_user", %{conn: conn} do
test "with a correct password in the credentials, " <>
"it assigns the auth_user and marks OAuthScopesPlug as skipped",
%{conn: conn} do
conn =
conn
|> assign(:auth_credentials, %{password: "guy"})
|> AuthenticationPlug.call(%{})
assert conn.assigns.user == conn.assigns.auth_user
assert PlugHelper.plug_skipped?(conn, OAuthScopesPlug)
end
test "with a wrong password in the credentials, it does nothing", %{conn: conn} do

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