Merge branch 'develop' into feature/activitypub

This commit is contained in:
Roger Braun 2017-12-09 11:00:56 +01:00
commit 30e9b22f96
2938 changed files with 211486 additions and 339 deletions

24
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,24 @@
image: elixir:1.5
services:
- postgres:9.6.2
variables:
POSTGRES_DB: pleroma_test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
stages:
- test
before_script:
- mix local.hex --force
- mix local.rebar --force
- mix deps.get
- MIX_ENV=test mix ecto.create
- MIX_ENV=test mix ecto.migrate
unit-testing:
stage: test
script:
- MIX_ENV=test mix test

View file

@ -4,76 +4,36 @@
Pleroma is an OStatus-compatible social networking server written in Elixir, compatible with GNU Social and Mastodon. It is high-performance and can run on small devices like a Raspberry Pi. Pleroma is an OStatus-compatible social networking server written in Elixir, compatible with GNU Social and Mastodon. It is high-performance and can run on small devices like a Raspberry Pi.
For clients it supports both the GNU Social API with Qvitter extensions and the Mastodon client API. For clients it supports both the [GNU Social API with Qvitter extensions](https://twitter-api.readthedocs.io/en/latest/index.html) and the [Mastodon client API](https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md).
Mobile clients that are known to work: Mobile clients that are known to work well:
* Twidere * Twidere
* Tusky * Tusky
* Pawoo (Android) * Pawoo (Android + iOS)
* Subway Tooter * Subway Tooter
* Amaroq (iOS)
* Tootdon (Android + iOS)
No release has been made yet, but several servers have been online for months already. If you want to run your own server, feel free to contact us at @lain@pleroma.soykaf.com or in our dev chat at https://matrix.heldscal.la/#/room/#pleromafe:matrix.heldscal.la. No release has been made yet, but several servers have been online for months already. If you want to run your own server, feel free to contact us at @lain@pleroma.soykaf.com or in our dev chat at #pleroma on freenode or via matrix at https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org.
## Installation ## Installation
### Dependencies ### Dependencies
* Postgresql version 9.6 or newer * Postgresql version 9.6 or newer
* Elixir version 1.4 or newer * Elixir version 1.4 or newer (you will also need erlang-dev, erlang-parsetools, erlang-xmerl packages)
* Build-essential tools * Build-essential tools
#### Installing dependencies on Debian system ### Configuration
PostgreSQL 9.6 should be available on Debian stable (Jessie) from "main" area. Install it using apt: `apt install postgresql-9.6`. Make sure that older versions are not installed since Debian allows multiple versions to coexist but still runs only one version.
You must install elixir 1.4+ from elixir-lang.org, because Debian repos only have 1.3.x version. You will need to add apt repo to sources.list(.d) and import GPG key. Follow instructions here: https://elixir-lang.org/install.html#unix-and-unix-like (See "Ubuntu or Debian 7"). This should be valid until Debian updates elixir in their repositories. Package you want is named `elixir`, so install it using `apt install elixir` * Run `mix deps.get` to install elixir dependencies.
Elixir will also require `make` and probably other related software for building dependencies - in case you don't have them, get them via `apt install build-essential` * Run `mix generate_config`. This will ask you a few questions about your instance and generate a configuration file in `config/generated_config.exs`. Check that and copy it to either `config/dev.secret.exs` or `config/prod.secret.exs`. It will also create a `config/setup_db.psql`, which you need to run as PostgreSQL superuser (i.e. `sudo su postgres -c "psql -f config/setup_db.psql"`). It will setup a pleroma db user, database and will setup needed extensions that need to be set up once as superuser.
### Preparation * Run `mix ecto.migrate` to run the database migrations. You will have to do this again after certain updates.
* You probably want application to run as separte user - so create a new one: `adduser pleroma`, you can login as it via `su pleroma` * You can check if your instance is configured correctly by running it with `mix phx.server` and checking the instance info endpoint at `/api/v1/instance`. If it shows your uri, name and email correctly, you are configured correctly. If it shows something like `localhost:4000`, your configuration is probably wrong, unless you are running a local development setup.
* Clone the git repository into new user's dir (clone as the pleroma user to avoid permissions errors)
* Again, as new user, install dependencies with `mix deps.get` if it asks you to install "hex" - agree to that.
### Database setup
* Create a database user and database for pleroma
* Open psql shell as postgres user: (as root) `su postgres -c psql`
* Create a new PostgreSQL user:
```sql
\c pleroma_dev
CREATE user pleroma;
ALTER user pleroma with encrypted password '<your password>';
GRANT ALL ON ALL tables IN SCHEMA public TO pleroma;
GRANT ALL ON ALL sequences IN SCHEMA public TO pleroma;
```
* Create `config/dev.secret.exs` and copy the database settings from `dev.exs` there.
* Change password in `config/dev.secret.exs`, and change user to `"pleroma"` (line like `username: "postgres"`)
* Create and update your database with `mix ecto.create && mix ecto.migrate`.
### Some additional configuration
* You will need to let pleroma instance to know what hostname/url it's running on. _THIS IS THE MOST IMPORTANT STEP. GET THIS WRONG AND YOU'LL HAVE TO RESET YOUR DATABASE_.
Create the file `config/dev.secret.exs`, add these lines at the end of the file:
```elixir
config :pleroma, Pleroma.Web.Endpoint,
url: [host: "example.tld", scheme: "https", port: 443]
```
replacing `example.tld` with your (sub)domain
* You should also setup your site name and admin email address. Look at config.exs for more available options.
```elixir
config :pleroma, :instance,
name: "My great instance",
email: "someone@example.com"
```
* The common and convenient way for adding HTTPS is by using Nginx as a reverse proxy. You can look at example Nginx configuration in `installation/pleroma.nginx`. If you need TLS/SSL certificates for HTTPS, you can look get some for free with letsencrypt: https://letsencrypt.org/ * The common and convenient way for adding HTTPS is by using Nginx as a reverse proxy. You can look at example Nginx configuration in `installation/pleroma.nginx`. If you need TLS/SSL certificates for HTTPS, you can look get some for free with letsencrypt: https://letsencrypt.org/
On Debian you can use `certbot` package and command to manage letsencrypt certificates. On Debian you can use `certbot` package and command to manage letsencrypt certificates.

View file

@ -49,5 +49,5 @@
try do try do
import_config "dev.secret.exs" import_config "dev.secret.exs"
rescue rescue
_-> nil _-> IO.puts("!!! RUNNING IN LOCALHOST DEV MODE! !!!\nFEDERATION WON'T WORK UNTIL YOU CONFIGURE A dev.secret.exs")
end end

View file

@ -18,7 +18,7 @@
username: "postgres", username: "postgres",
password: "postgres", password: "postgres",
database: "pleroma_test", database: "pleroma_test",
hostname: "localhost", hostname: System.get_env("DB_HOST") || "localhost",
pool: Ecto.Adapters.SQL.Sandbox pool: Ecto.Adapters.SQL.Sandbox

View file

@ -19,6 +19,9 @@ server {
server_name example.tld; server_name example.tld;
location / { location / {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://localhost:4000; proxy_pass http://localhost:4000;
} }
include snippets/well-known.conf; include snippets/well-known.conf;

View file

@ -0,0 +1,22 @@
defmodule Mix.Tasks.GenerateConfig do
use Mix.Task
@shortdoc "Generates a new config"
def run(_) do
IO.puts("Answer a few questions to generate a new config\n")
IO.puts("--- THIS WILL OVERWRITE YOUR config/generated_config.exs! ---\n")
domain = IO.gets("What is your domain name? (e.g. pleroma.soykaf.com): ") |> String.trim
name = IO.gets("What is the name of your instance? (e.g. Pleroma/Soykaf): ") |> String.trim
email = IO.gets("What's your admin email address: ") |> String.trim
secret = :crypto.strong_rand_bytes(64) |> Base.encode64 |> binary_part(0, 64)
dbpass = :crypto.strong_rand_bytes(64) |> Base.encode64 |> binary_part(0, 64)
resultSql = EEx.eval_file("lib/mix/tasks/sample_psql.eex", [dbpass: dbpass])
result = EEx.eval_file("lib/mix/tasks/sample_config.eex", [domain: domain, email: email, name: name, secret: secret, dbpass: dbpass])
IO.puts("\nWriting config to config/generated_config.exs.\n\nCheck it and configure your database, then copy it to either config/dev.secret.exs or config/prod.secret.exs")
File.write("config/generated_config.exs", result)
IO.puts("\nWriting setup_db.psql, please run it as postgre superuser, i.e.: sudo su postgres -c 'psql -f config/setup_db.psql'")
File.write("config/setup_db.psql", resultSql)
end
end

View file

@ -0,0 +1,20 @@
use Mix.Config
config :pleroma, Pleroma.Web.Endpoint,
url: [host: "<%= domain %>", scheme: "https", port: 443],
secret_key_base: "<%= secret %>"
config :pleroma, :instance,
name: "<%= name %>",
email: "<%= email %>",
limit: 5000,
registrations_open: true
# Configure your database
config :pleroma, Pleroma.Repo,
adapter: Ecto.Adapters.Postgres,
username: "pleroma",
password: "<%= dbpass %>",
database: "pleroma_dev",
hostname: "localhost",
pool_size: 10

View file

@ -0,0 +1,8 @@
CREATE USER pleroma WITH ENCRYPTED PASSWORD '<%= dbpass %>' CREATEDB;
-- in case someone runs this second time accidentally
ALTER USER pleroma WITH ENCRYPTED PASSWORD '<%= dbpass %>' CREATEDB;
CREATE DATABASE pleroma_dev;
ALTER DATABASE pleroma_dev OWNER TO pleroma;
\c pleroma_dev;
--Extensions made by ecto.migrate that need superuser access
CREATE EXTENSION IF NOT EXISTS citext;

View file

@ -0,0 +1,44 @@
defmodule Pleroma.PasswordResetToken do
use Ecto.Schema
import Ecto.Changeset
alias Pleroma.{User, PasswordResetToken, Repo}
schema "password_reset_tokens" do
belongs_to :user, User
field :token, :string
field :used, :boolean, default: false
timestamps()
end
def create_token(%User{} = user) do
token = :crypto.strong_rand_bytes(32) |> Base.url_encode64
token = %PasswordResetToken{
user_id: user.id,
used: false,
token: token
}
Repo.insert(token)
end
def used_changeset(struct) do
struct
|> cast(%{}, [])
|> put_change(:used, true)
end
def reset_password(token, data) do
with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}),
%User{} = user <- Repo.get(User, token.user_id),
{:ok, _user} <- User.reset_password(user, data),
{:ok, token} <- Repo.update(used_changeset(token)) do
{:ok, token}
else
_e -> {:error, token}
end
end
end

View file

@ -6,7 +6,8 @@ defmodule Pleroma.Activity do
schema "activities" do schema "activities" do
field :data, :map field :data, :map
field :local, :boolean, default: true field :local, :boolean, default: true
has_many :notifications, Notification field :actor, :string
has_many :notifications, Notification, on_delete: :delete_all
timestamps() timestamps()
end end
@ -16,24 +17,29 @@ def get_by_ap_id(ap_id) do
where: fragment("(?)->>'id' = ?", activity.data, ^to_string(ap_id))) where: fragment("(?)->>'id' = ?", activity.data, ^to_string(ap_id)))
end end
# TODO:
# Go through these and fix them everywhere.
# Wrong name, only returns create activities # Wrong name, only returns create activities
def all_by_object_ap_id_q(ap_id) do def all_by_object_ap_id_q(ap_id) do
from activity in Activity, from activity in Activity,
where: fragment("(?)->'object'->>'id' = ?", activity.data, ^to_string(ap_id)) where: fragment("coalesce((?)->'object'->>'id', (?)->>'object') = ?", activity.data, activity.data, ^to_string(ap_id)),
where: fragment("(?)->>'type' = 'Create'", activity.data)
end end
# Wrong name, returns all.
def all_non_create_by_object_ap_id_q(ap_id) do def all_non_create_by_object_ap_id_q(ap_id) do
from activity in Activity, from activity in Activity,
where: fragment("(?)->>'object' = ?", activity.data, ^to_string(ap_id)) where: fragment("coalesce((?)->'object'->>'id', (?)->>'object') = ?", activity.data, activity.data, ^to_string(ap_id))
end end
# Wrong name plz fix thx
def all_by_object_ap_id(ap_id) do def all_by_object_ap_id(ap_id) do
Repo.all(all_by_object_ap_id_q(ap_id)) Repo.all(all_by_object_ap_id_q(ap_id))
end end
def get_create_activity_by_object_ap_id(ap_id) do def get_create_activity_by_object_ap_id(ap_id) do
Repo.one(from activity in Activity, Repo.one(from activity in Activity,
where: fragment("(?)->'object'->>'id' = ?", activity.data, ^to_string(ap_id)) where: fragment("coalesce((?)->'object'->>'id', (?)->>'object') = ?", activity.data, activity.data, ^to_string(ap_id)),
and fragment("(?)->>'type' = 'Create'", activity.data)) where: fragment("(?)->>'type' = 'Create'", activity.data))
end end
end end

View file

@ -19,8 +19,10 @@ def start(_type, _args) do
ttl_interval: 1000, ttl_interval: 1000,
limit: 2500 limit: 2500
]]), ]]),
worker(Pleroma.Web.Federator, []) worker(Pleroma.Web.Federator, []),
worker(Pleroma.Web.ChatChannel.ChatChannelState, []),
] ]
++ if Mix.env == :test, do: [], else: [worker(Pleroma.Web.Streamer, [])]
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
# for other strategies and supported options # for other strategies and supported options

View file

@ -1,15 +1,16 @@
defmodule Pleroma.Formatter do defmodule Pleroma.Formatter do
alias Pleroma.User alias Pleroma.User
@link_regex ~r/https?:\/\/[\w\.\/?=\-#%&]+[\w]/u @link_regex ~r/https?:\/\/[\w\.\/?=\-#%&@~\(\)]+[\w\/]/u
def linkify(text) do def linkify(text) do
Regex.replace(@link_regex, text, "<a href='\\0'>\\0</a>") Regex.replace(@link_regex, text, "<a href='\\0'>\\0</a>")
end end
@tag_regex ~r/\#\w+/u @tag_regex ~r/\#\w+/u
def parse_tags(text) do def parse_tags(text, data \\ %{}) do
Regex.scan(@tag_regex, text) Regex.scan(@tag_regex, text)
|> Enum.map(fn (["#" <> tag = full_tag]) -> {full_tag, String.downcase(tag)} end) |> Enum.map(fn (["#" <> tag = full_tag]) -> {full_tag, String.downcase(tag)} end)
|> (fn map -> if data["sensitive"], do: [{"#nsfw", "nsfw"}] ++ map, else: map end).()
end end
def parse_mentions(text) do def parse_mentions(text) do
@ -23,6 +24,15 @@ def parse_mentions(text) do
|> Enum.filter(fn ({_match, user}) -> user end) |> Enum.filter(fn ({_match, user}) -> user end)
end end
def html_escape(text) do
Regex.split(@link_regex, text, include_captures: true)
|> Enum.map_every(2, fn chunk ->
{:safe, part} = Phoenix.HTML.html_escape(chunk)
part
end)
|> Enum.join("")
end
@finmoji [ @finmoji [
"a_trusted_friend", "a_trusted_friend",
"alandislands", "alandislands",
@ -122,4 +132,8 @@ def emojify(text, additional \\ nil) do
def get_emoji(text) do def get_emoji(text) do
Enum.filter(@emoji, fn ({emoji, _}) -> String.contains?(text, ":#{emoji}:") end) Enum.filter(@emoji, fn ({emoji, _}) -> String.contains?(text, ":#{emoji}:") end)
end end
def get_custom_emoji() do
@emoji
end
end end

View file

@ -36,7 +36,38 @@ def for_user(user, opts \\ %{}) do
Repo.all(query) Repo.all(query)
end end
def create_notifications(%Activity{id: id, data: %{"to" => to, "type" => type}} = activity) when type in ["Create", "Like", "Announce", "Follow"] do def get(%{id: user_id} = _user, id) do
query = from n in Notification,
where: n.id == ^id,
preload: [:activity]
notification = Repo.one(query)
case notification do
%{user_id: ^user_id} ->
{:ok, notification}
_ ->
{:error, "Cannot get notification"}
end
end
def clear(user) do
query = from n in Notification,
where: n.user_id == ^user.id
Repo.delete_all(query)
end
def dismiss(%{id: user_id} = _user, id) do
notification = Repo.get(Notification, id)
case notification do
%{user_id: ^user_id} ->
Repo.delete(notification)
_ ->
{:error, "Cannot dismiss notification"}
end
end
def create_notifications(%Activity{id: _, data: %{"to" => _, "type" => type}} = activity) when type in ["Create", "Like", "Announce", "Follow"] do
users = User.get_notified_from_activity(activity) users = User.get_notified_from_activity(activity)
notifications = Enum.map(users, fn (user) -> create_notification(activity, user) end) notifications = Enum.map(users, fn (user) -> create_notification(activity, user) end)
@ -46,9 +77,12 @@ def create_notifications(_), do: {:ok, []}
# TODO move to sql, too. # TODO move to sql, too.
def create_notification(%Activity{} = activity, %User{} = user) do def create_notification(%Activity{} = activity, %User{} = user) do
notification = %Notification{user_id: user.id, activity_id: activity.id} unless User.blocks?(user, %{ap_id: activity.data["actor"]}) do
notification = %Notification{user_id: user.id, activity: activity}
{:ok, notification} = Repo.insert(notification) {:ok, notification} = Repo.insert(notification)
Pleroma.Web.Streamer.stream("user", notification)
notification notification
end end
end end
end

View file

@ -15,15 +15,16 @@ def create(data) do
end end
def change(struct, params \\ %{}) do def change(struct, params \\ %{}) do
changeset = struct struct
|> cast(params, [:data]) |> cast(params, [:data])
|> validate_required([:data]) |> validate_required([:data])
|> unique_constraint(:ap_id, name: :objects_unique_apid_index) |> unique_constraint(:ap_id, name: :objects_unique_apid_index)
end end
def get_by_ap_id(nil), do: nil
def get_by_ap_id(ap_id) do def get_by_ap_id(ap_id) do
Repo.one(from object in Object, Repo.one(from object in Object,
where: fragment("? @> ?", object.data, ^%{id: ap_id})) where: fragment("(?)->>'id' = ?", object.data, ^ap_id))
end end
def get_cached_by_ap_id(ap_id) do def get_cached_by_ap_id(ap_id) do

View file

@ -12,6 +12,7 @@ def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
def call(conn, opts) do def call(conn, opts) do
with {:ok, username, password} <- decode_header(conn), with {:ok, username, password} <- decode_header(conn),
{:ok, user} <- opts[:fetcher].(username), {:ok, user} <- opts[:fetcher].(username),
false <- !!user.info["deactivated"],
saved_user_id <- get_session(conn, :user_id), saved_user_id <- get_session(conn, :user_id),
{:ok, verified_user} <- verify(user, password, saved_user_id) {:ok, verified_user} <- verify(user, password, saved_user_id)
do do
@ -44,7 +45,7 @@ defp verify(user, password, _user_id) do
defp decode_header(conn) do defp decode_header(conn) do
with ["Basic " <> header] <- get_req_header(conn, "authorization"), with ["Basic " <> header] <- get_req_header(conn, "authorization"),
{:ok, userinfo} <- Base.decode64(header), {:ok, userinfo} <- Base.decode64(header),
[username, password] <- String.split(userinfo, ":") [username, password] <- String.split(userinfo, ":", parts: 2)
do do
{:ok, username, password} {:ok, username, password}
end end

View file

@ -9,10 +9,15 @@ def init(options) do
end end
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
def call(conn, opts) do def call(conn, _) do
with ["Bearer " <> header] <- get_req_header(conn, "authorization"), token = case get_req_header(conn, "authorization") do
%Token{user_id: user_id} <- Repo.get_by(Token, token: header), ["Bearer " <> header] -> header
%User{} = user <- Repo.get(User, user_id) do _ -> get_session(conn, :oauth_token)
end
with token when not is_nil(token) <- token,
%Token{user_id: user_id} <- Repo.get_by(Token, token: token),
%User{} = user <- Repo.get(User, user_id),
false <- !!user.info["deactivated"] do
conn conn
|> assign(:user, user) |> assign(:user, user)
else else

View file

@ -8,11 +8,18 @@ def store(%Plug.Upload{} = file) do
result_file = Path.join(upload_folder, file.filename) result_file = Path.join(upload_folder, file.filename)
File.cp!(file.path, result_file) File.cp!(file.path, result_file)
# fix content type on some image uploads
content_type = if file.content_type == "application/octet-stream" do
get_content_type(file.path)
else
file.content_type
end
%{ %{
"type" => "Image", "type" => "Image",
"url" => [%{ "url" => [%{
"type" => "Link", "type" => "Link",
"mediaType" => file.content_type, "mediaType" => content_type,
"href" => url_for(Path.join(uuid, :cow_uri.urlencode(file.filename))) "href" => url_for(Path.join(uuid, :cow_uri.urlencode(file.filename)))
}], }],
"name" => file.filename, "name" => file.filename,
@ -53,4 +60,34 @@ defp upload_path do
defp url_for(file) do defp url_for(file) do
"#{Web.base_url()}/media/#{file}" "#{Web.base_url()}/media/#{file}"
end end
def get_content_type(file) do
match = File.open(file, [:read], fn(f) ->
case IO.binread(f, 8) do
<<0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a>> ->
"image/png"
<<0x47, 0x49, 0x46, 0x38, _, 0x61, _, _>> ->
"image/gif"
<<0xff, 0xd8, 0xff, _, _, _, _, _>> ->
"image/jpeg"
<<0x1a, 0x45, 0xdf, 0xa3, _, _, _, _>> ->
"video/webm"
<<0x00, 0x00, 0x00, _, 0x66, 0x74, 0x79, 0x70>> ->
"video/mp4"
<<0x49, 0x44, 0x33, _, _, _, _, _>> ->
"audio/mpeg"
<<0x4f, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00>> ->
"audio/ogg"
<<0x52, 0x49, 0x46, 0x46, _, _, _, _>> ->
"audio/wav"
_ ->
"application/octet-stream"
end
end)
case match do
{:ok, type} -> type
_e -> "application/octet-stream"
end
end
end end

View file

@ -5,8 +5,7 @@ defmodule Pleroma.User do
alias Pleroma.{Repo, User, Object, Web, Activity, Notification} alias Pleroma.{Repo, User, Object, Web, Activity, Notification}
alias Comeonin.Pbkdf2 alias Comeonin.Pbkdf2
alias Pleroma.Web.{OStatus, Websub} alias Pleroma.Web.{OStatus, Websub}
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.{Utils, ActivityPub}
alias Pleroma.Web.ActivityPub.Utils
schema "users" do schema "users" do
field :bio, :string field :bio, :string
@ -62,8 +61,9 @@ def info_changeset(struct, params \\ %{}) do
end end
def user_info(%User{} = user) do def user_info(%User{} = user) do
oneself = if user.local, do: 1, else: 0
%{ %{
following_count: length(user.following), following_count: length(user.following) - oneself,
note_count: user.info["note_count"] || 0, note_count: user.info["note_count"] || 0,
follower_count: user.info["follower_count"] || 0 follower_count: user.info["follower_count"] || 0
} }
@ -89,7 +89,7 @@ def remote_user_creation(params) do
end end
def update_changeset(struct, params \\ %{}) do def update_changeset(struct, params \\ %{}) do
changeset = struct struct
|> cast(params, [:bio, :name]) |> cast(params, [:bio, :name])
|> unique_constraint(:nickname) |> unique_constraint(:nickname)
|> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/) |> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/)
@ -97,6 +97,25 @@ def update_changeset(struct, params \\ %{}) do
|> validate_length(:name, min: 1, max: 100) |> validate_length(:name, min: 1, max: 100)
end end
def password_update_changeset(struct, params) do
changeset = struct
|> cast(params, [:password, :password_confirmation])
|> validate_required([:password, :password_confirmation])
|> validate_confirmation(:password)
if changeset.valid? do
hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
changeset
|> put_change(:password_hash, hashed)
else
changeset
end
end
def reset_password(user, data) do
update_and_set_cache(password_update_changeset(user, data))
end
def register_changeset(struct, params \\ %{}) do def register_changeset(struct, params \\ %{}) do
changeset = struct changeset = struct
|> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation]) |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
@ -123,9 +142,9 @@ def register_changeset(struct, params \\ %{}) do
end end
end end
def follow(%User{} = follower, %User{} = followed) do def follow(%User{} = follower, %User{info: info} = followed) do
ap_followers = followed.follower_address ap_followers = followed.follower_address
if following?(follower, followed) do if following?(follower, followed) or info["deactivated"] do
{:error, {:error,
"Could not follow user: #{followed.nickname} is already on your list."} "Could not follow user: #{followed.nickname} is already on your list."}
else else
@ -138,9 +157,9 @@ def follow(%User{} = follower, %User{} = followed) do
follower = follower follower = follower
|> follow_changeset(%{following: following}) |> follow_changeset(%{following: following})
|> Repo.update |> update_and_set_cache
{:ok, followed} = update_follower_count(followed) {:ok, _} = update_follower_count(followed)
follower follower
end end
@ -148,13 +167,13 @@ def follow(%User{} = follower, %User{} = followed) do
def unfollow(%User{} = follower, %User{} = followed) do def unfollow(%User{} = follower, %User{} = followed) do
ap_followers = followed.follower_address ap_followers = followed.follower_address
if following?(follower, followed) do if following?(follower, followed) and follower.ap_id != followed.ap_id do
following = follower.following following = follower.following
|> List.delete(ap_followers) |> List.delete(ap_followers)
{ :ok, follower } = follower { :ok, follower } = follower
|> follow_changeset(%{following: following}) |> follow_changeset(%{following: following})
|> Repo.update |> update_and_set_cache
{:ok, followed} = update_follower_count(followed) {:ok, followed} = update_follower_count(followed)
@ -172,6 +191,17 @@ def get_by_ap_id(ap_id) do
Repo.get_by(User, ap_id: ap_id) Repo.get_by(User, ap_id: ap_id)
end end
def update_and_set_cache(changeset) do
with {:ok, user} <- Repo.update(changeset) do
Cachex.set(:user_cache, "ap_id:#{user.ap_id}", user)
Cachex.set(:user_cache, "nickname:#{user.nickname}", user)
Cachex.set(:user_cache, "user_info:#{user.id}", user_info(user))
{:ok, user}
else
e -> e
end
end
def get_cached_by_ap_id(ap_id) do def get_cached_by_ap_id(ap_id) do
key = "ap_id:#{ap_id}" key = "ap_id:#{ap_id}"
Cachex.get!(:user_cache, key, fallback: fn(_) -> get_by_ap_id(ap_id) end) Cachex.get!(:user_cache, key, fallback: fn(_) -> get_by_ap_id(ap_id) end)
@ -195,7 +225,7 @@ def get_or_fetch_by_nickname(nickname) do
with %User{} = user <- get_by_nickname(nickname) do with %User{} = user <- get_by_nickname(nickname) do
user user
else _e -> else _e ->
with [nick, domain] <- String.split(nickname, "@"), with [_nick, _domain] <- String.split(nickname, "@"),
{:ok, user} <- OStatus.make_user(nickname) do {:ok, user} <- OStatus.make_user(nickname) do
user user
else _e -> nil else _e -> nil
@ -220,9 +250,18 @@ def get_friends(%User{id: id, following: following}) do
{:ok, Repo.all(q)} {:ok, Repo.all(q)}
end end
def increase_note_count(%User{} = user) do
note_count = (user.info["note_count"] || 0) + 1
new_info = Map.put(user.info, "note_count", note_count)
cs = info_changeset(user, %{info: new_info})
update_and_set_cache(cs)
end
def update_note_count(%User{} = user) do def update_note_count(%User{} = user) do
note_count_query = from a in Object, note_count_query = from a in Object,
where: fragment("? @> ?", a.data, ^%{actor: user.ap_id, type: "Note"}), where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
select: count(a.id) select: count(a.id)
note_count = Repo.one(note_count_query) note_count = Repo.one(note_count_query)
@ -231,12 +270,13 @@ def update_note_count(%User{} = user) do
cs = info_changeset(user, %{info: new_info}) cs = info_changeset(user, %{info: new_info})
Repo.update(cs) update_and_set_cache(cs)
end end
def update_follower_count(%User{} = user) do def update_follower_count(%User{} = user) do
follower_count_query = from u in User, follower_count_query = from u in User,
where: fragment("? @> ?", u.following, ^user.follower_address), where: fragment("? @> ?", u.following, ^user.follower_address),
where: u.id != ^user.id,
select: count(u.id) select: count(u.id)
follower_count = Repo.one(follower_count_query) follower_count = Repo.one(follower_count_query)
@ -245,14 +285,95 @@ def update_follower_count(%User{} = user) do
cs = info_changeset(user, %{info: new_info}) cs = info_changeset(user, %{info: new_info})
Repo.update(cs) update_and_set_cache(cs)
end end
def get_notified_from_activity(%Activity{data: %{"to" => to}} = activity) do def get_notified_from_activity(%Activity{data: %{"to" => to}}) do
query = from u in User, query = from u in User,
where: u.ap_id in ^to, where: u.ap_id in ^to,
where: u.local == true where: u.local == true
Repo.all(query) Repo.all(query)
end end
def get_recipients_from_activity(%Activity{data: %{"to" => to}}) do
query = from u in User,
where: u.ap_id in ^to,
or_where: fragment("? \\\?| ?", u.following, ^to)
query = from u in query,
where: u.local == true
Repo.all(query)
end
def search(query, resolve) do
if resolve do
User.get_or_fetch_by_nickname(query)
end
q = from u in User,
where: fragment("(to_tsvector('english', ?) || to_tsvector('english', ?)) @@ plainto_tsquery('english', ?)", u.nickname, u.name, ^query),
limit: 20
Repo.all(q)
end
def block(user, %{ap_id: ap_id}) do
blocks = user.info["blocks"] || []
new_blocks = Enum.uniq([ap_id | blocks])
new_info = Map.put(user.info, "blocks", new_blocks)
cs = User.info_changeset(user, %{info: new_info})
update_and_set_cache(cs)
end
def unblock(user, %{ap_id: ap_id}) do
blocks = user.info["blocks"] || []
new_blocks = List.delete(blocks, ap_id)
new_info = Map.put(user.info, "blocks", new_blocks)
cs = User.info_changeset(user, %{info: new_info})
update_and_set_cache(cs)
end
def blocks?(user, %{ap_id: ap_id}) do
blocks = user.info["blocks"] || []
Enum.member?(blocks, ap_id)
end
def local_user_query() do
from u in User,
where: u.local == true
end
def deactivate (%User{} = user) do
new_info = Map.put(user.info, "deactivated", true)
cs = User.info_changeset(user, %{info: new_info})
update_and_set_cache(cs)
end
def delete (%User{} = user) do
{:ok, user} = User.deactivate(user)
# Remove all relationships
{:ok, followers } = User.get_followers(user)
followers
|> Enum.each(fn (follower) -> User.unfollow(follower, user) end)
{:ok, friends} = User.get_friends(user)
friends
|> Enum.each(fn (followed) -> User.unfollow(user, followed) end)
query = from a in Activity,
where: a.actor == ^user.ap_id
Repo.all(query)
|> Enum.each(fn (activity) ->
case activity.data["type"] do
"Create" -> ActivityPub.delete(Object.get_by_ap_id(activity.data["object"]["id"]))
_ -> "Doing nothing" # TODO: Do something with likes, follows, repeats.
end
end)
:ok
end
end end

View file

@ -1,6 +1,5 @@
defmodule Pleroma.Web.ActivityPub.ActivityPub do defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.{Activity, Repo, Object, Upload, User, Web, Notification} alias Pleroma.{Activity, Repo, Object, Upload, User, Notification}
alias Ecto.{Changeset, UUID}
import Ecto.Query import Ecto.Query
import Pleroma.Web.ActivityPub.Utils import Pleroma.Web.ActivityPub.Utils
require Logger require Logger
@ -9,8 +8,9 @@ def insert(map, local \\ true) when is_map(map) do
with nil <- Activity.get_by_ap_id(map["id"]), with nil <- Activity.get_by_ap_id(map["id"]),
map <- lazy_put_activity_defaults(map), map <- lazy_put_activity_defaults(map),
:ok <- insert_full_object(map) do :ok <- insert_full_object(map) do
{:ok, activity} = Repo.insert(%Activity{data: map, local: local}) {:ok, activity} = Repo.insert(%Activity{data: map, local: local, actor: map["actor"]})
Notification.create_notifications(activity) Notification.create_notifications(activity)
stream_out(activity)
{:ok, activity} {:ok, activity}
else else
%Activity{} = activity -> {:ok, activity} %Activity{} = activity -> {:ok, activity}
@ -18,6 +18,18 @@ def insert(map, local \\ true) when is_map(map) do
end end
end end
def stream_out(activity) do
if activity.data["type"] in ["Create", "Announce"] do
Pleroma.Web.Streamer.stream("user", activity)
if Enum.member?(activity.data["to"], "https://www.w3.org/ns/activitystreams#Public") do
Pleroma.Web.Streamer.stream("public", activity)
if activity.local do
Pleroma.Web.Streamer.stream("public:local", activity)
end
end
end
end
def create(to, actor, context, object, additional \\ %{}, published \\ nil, local \\ true) do def create(to, actor, context, object, additional \\ %{}, published \\ nil, local \\ true) do
with create_data <- make_create_data(%{to: to, actor: actor, published: published, context: context, object: object}, additional), with create_data <- make_create_data(%{to: to, actor: actor, published: published, context: context, object: object}, additional),
{:ok, activity} <- insert(create_data, local), {:ok, activity} <- insert(create_data, local),
@ -27,7 +39,7 @@ def create(to, actor, context, object, additional \\ %{}, published \\ nil, loca
end end
# TODO: This is weird, maybe we shouldn't check here if we can make the activity. # TODO: This is weird, maybe we shouldn't check here if we can make the activity.
def like(%User{ap_id: ap_id} = user, %Object{data: %{"id" => id}} = object, activity_id \\ nil, local \\ true) do def like(%User{ap_id: ap_id} = user, %Object{data: %{"id" => _}} = object, activity_id \\ nil, local \\ true) do
with nil <- get_existing_like(ap_id, object), with nil <- get_existing_like(ap_id, object),
like_data <- make_like_data(user, object, activity_id), like_data <- make_like_data(user, object, activity_id),
{:ok, activity} <- insert(like_data, local), {:ok, activity} <- insert(like_data, local),
@ -49,7 +61,7 @@ def unlike(%User{} = actor, %Object{} = object) do
end end
end end
def announce(%User{ap_id: ap_id} = user, %Object{data: %{"id" => id}} = object, activity_id \\ nil, local \\ true) do def announce(%User{ap_id: _} = user, %Object{data: %{"id" => _}} = object, activity_id \\ nil, local \\ true) do
with announce_data <- make_announce_data(user, object, activity_id), with announce_data <- make_announce_data(user, object, activity_id),
{:ok, activity} <- insert(announce_data, local), {:ok, activity} <- insert(announce_data, local),
{:ok, object} <- add_announce_to_object(activity, object), {:ok, object} <- add_announce_to_object(activity, object),
@ -87,17 +99,17 @@ def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ tru
} }
with Repo.delete(object), with Repo.delete(object),
Repo.delete_all(Activity.all_non_create_by_object_ap_id_q(id)), Repo.delete_all(Activity.all_non_create_by_object_ap_id_q(id)),
Repo.delete_all(Activity.all_by_object_ap_id_q(id)),
{:ok, activity} <- insert(data, local), {:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity} {:ok, activity}
end end
end end
def fetch_activities_for_context(context) do def fetch_activities_for_context(context, opts \\ %{}) do
query = from activity in Activity, query = from activity in Activity,
where: fragment("?->>'type' = ? and ?->>'context' = ?", activity.data, "Create", activity.data, ^context), where: fragment("?->>'type' = ? and ?->>'context' = ?", activity.data, "Create", activity.data, ^context),
order_by: [desc: :id] order_by: [desc: :id]
query = restrict_blocked(query, opts)
Repo.all(query) Repo.all(query)
end end
@ -137,7 +149,7 @@ defp restrict_max(query, _), do: query
defp restrict_actor(query, %{"actor_id" => actor_id}) do defp restrict_actor(query, %{"actor_id" => actor_id}) do
from activity in query, from activity in query,
where: fragment("?->>'actor' = ?", activity.data, ^actor_id) where: activity.actor == ^actor_id
end end
defp restrict_actor(query, _), do: query defp restrict_actor(query, _), do: query
@ -156,10 +168,32 @@ defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do
end end
defp restrict_favorited_by(query, _), do: query defp restrict_favorited_by(query, _), do: query
defp restrict_media(query, %{"only_media" => val}) when val == "true" or val == "1" do
from activity in query,
where: fragment("not (? #> '{\"object\",\"attachment\"}' = ?)", activity.data, ^[])
end
defp restrict_media(query, _), do: query
# Only search through last 100_000 activities by default
defp restrict_recent(query, %{"whole_db" => true}), do: query
defp restrict_recent(query, _) do
since = (Repo.aggregate(Activity, :max, :id) || 0) - 100_000
from activity in query,
where: activity.id > ^since
end
defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do
blocks = info["blocks"] || []
from activity in query,
where: fragment("not (? = ANY(?))", activity.actor, ^blocks)
end
defp restrict_blocked(query, _), do: query
def fetch_activities(recipients, opts \\ %{}) do def fetch_activities(recipients, opts \\ %{}) do
base_query = from activity in Activity, base_query = from activity in Activity,
limit: 20, limit: 20,
order_by: [desc: :id] order_by: [fragment("? desc nulls last", activity.id)]
base_query base_query
|> restrict_recipients(recipients) |> restrict_recipients(recipients)
@ -170,6 +204,9 @@ def fetch_activities(recipients, opts \\ %{}) do
|> restrict_actor(opts) |> restrict_actor(opts)
|> restrict_type(opts) |> restrict_type(opts)
|> restrict_favorited_by(opts) |> restrict_favorited_by(opts)
|> restrict_recent(opts)
|> restrict_blocked(opts)
|> restrict_media(opts)
|> Repo.all |> Repo.all
|> Enum.reverse |> Enum.reverse
end end

View file

@ -29,7 +29,12 @@ def generate_id(type) do
Enqueues an activity for federation if it's local Enqueues an activity for federation if it's local
""" """
def maybe_federate(%Activity{local: true} = activity) do def maybe_federate(%Activity{local: true} = activity) do
Pleroma.Web.Federator.enqueue(:publish, activity) priority = case activity.data["type"] do
"Delete" -> 10
"Create" -> 1
_ -> 5
end
Pleroma.Web.Federator.enqueue(:publish, activity, priority)
:ok :ok
end end
def maybe_federate(_), do: :ok def maybe_federate(_), do: :ok
@ -64,7 +69,7 @@ def lazy_put_object_defaults(map) do
Inserts a full object if it is contained in an activity. Inserts a full object if it is contained in an activity.
""" """
def insert_full_object(%{"object" => object_data}) when is_map(object_data) do def insert_full_object(%{"object" => object_data}) when is_map(object_data) do
with {:ok, object} <- Object.create(object_data) do with {:ok, _} <- Object.create(object_data) do
:ok :ok
end end
end end
@ -88,9 +93,13 @@ def update_object_in_activities(%{data: %{"id" => id}} = object) do
@doc """ @doc """
Returns an existing like if a user already liked an object Returns an existing like if a user already liked an object
""" """
def get_existing_like(actor, %{data: %{"id" => id}} = object) do def get_existing_like(actor, %{data: %{"id" => id}}) do
query = from activity in Activity, query = from activity in Activity,
where: fragment("? @> ?", activity.data, ^%{actor: actor, object: id, type: "Like"}) where: fragment("(?)->>'actor' = ?", activity.data, ^actor),
# this is to use the index
where: fragment("coalesce((?)->'object'->>'id', (?)->>'object') = ?", activity.data, activity.data, ^id),
where: fragment("(?)->>'type' = 'Like'", activity.data)
Repo.one(query) Repo.one(query)
end end
@ -197,7 +206,7 @@ def make_unfollow_data(follower, followed, follow_activity) do
def make_create_data(params, additional) do def make_create_data(params, additional) do
published = params.published || make_date() published = params.published || make_date()
activity = %{ %{
"type" => "Create", "type" => "Create",
"to" => params.to |> Enum.uniq, "to" => params.to |> Enum.uniq,
"actor" => params.actor.ap_id, "actor" => params.actor.ap_id,

View file

@ -1,8 +1,11 @@
defmodule Pleroma.Web.UserSocket do defmodule Pleroma.Web.UserSocket do
use Phoenix.Socket use Phoenix.Socket
alias Pleroma.User
alias Comeonin.Pbkdf2
## Channels ## Channels
# channel "room:*", Pleroma.Web.RoomChannel # channel "room:*", Pleroma.Web.RoomChannel
channel "chat:*", Pleroma.Web.ChatChannel
## Transports ## Transports
transport :websocket, Phoenix.Transports.WebSocket transport :websocket, Phoenix.Transports.WebSocket
@ -19,8 +22,13 @@ defmodule Pleroma.Web.UserSocket do
# #
# See `Phoenix.Token` documentation for examples in # See `Phoenix.Token` documentation for examples in
# performing token verification on connect. # performing token verification on connect.
def connect(_params, socket) do def connect(%{"token" => token}, socket) do
{:ok, socket} with {:ok, user_id} <- Phoenix.Token.verify(socket, "user socket", token, max_age: 84600),
%User{} = user <- Pleroma.Repo.get(User, user_id) do
{:ok, assign(socket, :user_name, user.nickname)}
else
_e -> :error
end
end end
# Socket id's are topics that allow you to identify all sockets for a given user: # Socket id's are topics that allow you to identify all sockets for a given user:

View file

@ -0,0 +1,46 @@
defmodule Pleroma.Web.ChatChannel do
use Phoenix.Channel
alias Pleroma.Web.ChatChannel.ChatChannelState
alias Pleroma.User
def join("chat:public", _message, socket) do
send(self(), :after_join)
{:ok, socket}
end
def handle_info(:after_join, socket) do
push socket, "messages", %{messages: ChatChannelState.messages()}
{:noreply, socket}
end
def handle_in("new_msg", %{"text" => text}, %{assigns: %{user_name: user_name}} = socket) do
author = User.get_cached_by_nickname(user_name)
author = Pleroma.Web.MastodonAPI.AccountView.render("account.json", user: author)
message = ChatChannelState.add_message(%{text: text, author: author})
broadcast! socket, "new_msg", message
{:noreply, socket}
end
end
defmodule Pleroma.Web.ChatChannel.ChatChannelState do
use Agent
@max_messages 20
def start_link do
Agent.start_link(fn -> %{max_id: 1, messages: []} end, name: __MODULE__)
end
def add_message(message) do
Agent.get_and_update(__MODULE__, fn state ->
id = state[:max_id] + 1
message = Map.put(message, "id", id)
messages = [message | state[:messages]] |> Enum.take(@max_messages)
{message, %{max_id: id, messages: messages}}
end)
end
def messages() do
Agent.get(__MODULE__, fn state -> state[:messages] |> Enum.reverse end)
end
end

View file

@ -16,7 +16,6 @@ def delete(activity_id, user) do
def repeat(id_or_ap_id, user) do def repeat(id_or_ap_id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id), with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id),
false <- activity.data["actor"] == user.ap_id,
object <- Object.get_by_ap_id(activity.data["object"]["id"]) do object <- Object.get_by_ap_id(activity.data["object"]["id"]) do
ActivityPub.announce(user, object) ActivityPub.announce(user, object)
else else
@ -56,12 +55,14 @@ def post(user, %{"status" => status} = data) do
mentions <- Formatter.parse_mentions(status), mentions <- Formatter.parse_mentions(status),
inReplyTo <- get_replied_to_activity(data["in_reply_to_status_id"]), inReplyTo <- get_replied_to_activity(data["in_reply_to_status_id"]),
to <- to_for_user_and_mentions(user, mentions, inReplyTo), to <- to_for_user_and_mentions(user, mentions, inReplyTo),
tags <- Formatter.parse_tags(status), tags <- Formatter.parse_tags(status, data),
content_html <- make_content_html(status, mentions, attachments, tags), content_html <- make_content_html(status, mentions, attachments, tags, data["no_attachment_links"]),
context <- make_context(inReplyTo), context <- make_context(inReplyTo),
object <- make_note_data(user.ap_id, to, context, content_html, attachments, inReplyTo, tags) do cw <- data["spoiler_text"],
object <- make_note_data(user.ap_id, to, context, content_html, attachments, inReplyTo, tags, cw),
object <- Map.put(object, "emoji", Formatter.get_emoji(status) |> Enum.reduce(%{}, fn({name, file}, acc) -> Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url}#{file}") end)) do
res = ActivityPub.create(to, user, context, object) res = ActivityPub.create(to, user, context, object)
User.update_note_count(user) User.increase_note_count(user)
res res
end end
end end

View file

@ -38,15 +38,19 @@ def to_for_user_and_mentions(user, mentions, inReplyTo) do
end end
end end
def make_content_html(status, mentions, attachments, tags) do def make_content_html(status, mentions, attachments, tags, no_attachment_links \\ false) do
status status
|> format_input(mentions, tags) |> format_input(mentions, tags)
|> add_attachments(attachments) |> maybe_add_attachments(attachments, no_attachment_links)
end end
def make_context(%Activity{data: %{"context" => context}}), do: context def make_context(%Activity{data: %{"context" => context}}), do: context
def make_context(_), do: Utils.generate_context_id def make_context(_), do: Utils.generate_context_id
def maybe_add_attachments(text, attachments, _no_links = true), do: text
def maybe_add_attachments(text, attachments, _no_links) do
add_attachments(text, attachments)
end
def add_attachments(text, attachments) do def add_attachments(text, attachments) do
attachment_text = Enum.map(attachments, fn attachment_text = Enum.map(attachments, fn
(%{"url" => [%{"href" => href} | _]}) -> (%{"url" => [%{"href" => href} | _]}) ->
@ -54,15 +58,16 @@ def add_attachments(text, attachments) do
"<a href=\"#{href}\" class='attachment'>#{shortname(name)}</a>" "<a href=\"#{href}\" class='attachment'>#{shortname(name)}</a>"
_ -> "" _ -> ""
end) end)
Enum.join([text | attachment_text], "<br>\n") Enum.join([text | attachment_text], "<br>")
end end
def format_input(text, mentions, tags) do def format_input(text, mentions, _tags) do
HtmlSanitizeEx.strip_tags(text) text
|> Formatter.html_escape
|> Formatter.linkify |> Formatter.linkify
|> String.replace("\n", "<br>\n") |> String.replace("\n", "<br>")
|> add_user_links(mentions) |> add_user_links(mentions)
|> add_tag_links(tags) # |> add_tag_links(tags)
end end
def add_tag_links(text, tags) do def add_tag_links(text, tags) do
@ -94,11 +99,12 @@ def add_user_links(text, mentions) do
end) end)
end end
def make_note_data(actor, to, context, content_html, attachments, inReplyTo, tags) do def make_note_data(actor, to, context, content_html, attachments, inReplyTo, tags, cw \\ nil) do
object = %{ object = %{
"type" => "Note", "type" => "Note",
"to" => to, "to" => to,
"content" => content_html, "content" => content_html,
"summary" => cw,
"context" => context, "context" => context,
"attachment" => attachments, "attachment" => attachments,
"actor" => actor, "actor" => actor,

View file

@ -2,6 +2,7 @@ defmodule Pleroma.Web.Endpoint do
use Phoenix.Endpoint, otp_app: :pleroma use Phoenix.Endpoint, otp_app: :pleroma
socket "/socket", Pleroma.Web.UserSocket socket "/socket", Pleroma.Web.UserSocket
socket "/api/v1", Pleroma.Web.MastodonAPI.MastodonSocket
# Serve at "/" the static files from "priv/static" directory. # Serve at "/" the static files from "priv/static" directory.
# #
@ -11,7 +12,7 @@ defmodule Pleroma.Web.Endpoint do
at: "/media", from: "uploads", gzip: false at: "/media", from: "uploads", gzip: false
plug Plug.Static, plug Plug.Static,
at: "/", from: :pleroma, at: "/", from: :pleroma,
only: ~w(index.html static finmoji emoji) only: ~w(index.html static finmoji emoji packs sounds sw.js)
# Code reloading can be explicitly enabled under the # Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint. # :code_reloader configuration of your endpoint.

View file

@ -14,7 +14,10 @@ def start_link do
Process.sleep(1000 * 60 * 1) # 1 minute Process.sleep(1000 * 60 * 1) # 1 minute
enqueue(:refresh_subscriptions, nil) enqueue(:refresh_subscriptions, nil)
end) end)
GenServer.start_link(__MODULE__, {:sets.new(), :queue.new()}, name: __MODULE__) GenServer.start_link(__MODULE__, %{
in: {:sets.new(), []},
out: {:sets.new(), []}
}, name: __MODULE__)
end end
def handle(:refresh_subscriptions, _) do def handle(:refresh_subscriptions, _) do
@ -71,22 +74,22 @@ def handle(:publish_single_websub, %{xml: xml, topic: topic, callback: callback,
end end
end end
def handle(type, payload) do def handle(type, _) do
Logger.debug(fn -> "Unknown task: #{type}" end) Logger.debug(fn -> "Unknown task: #{type}" end)
{:error, "Don't know what do do with this"} {:error, "Don't know what do do with this"}
end end
def enqueue(type, payload) do def enqueue(type, payload, priority \\ 1) do
if Mix.env == :test do if Mix.env == :test do
handle(type, payload) handle(type, payload)
else else
GenServer.cast(__MODULE__, {:enqueue, type, payload}) GenServer.cast(__MODULE__, {:enqueue, type, payload, priority})
end end
end end
def maybe_start_job(running_jobs, queue) do def maybe_start_job(running_jobs, queue) do
if (:sets.size(running_jobs) < @max_jobs) && !:queue.is_empty(queue) do if (:sets.size(running_jobs) < @max_jobs) && queue != [] do
{{:value, {type, payload}}, queue} = :queue.out(queue) {{type, payload}, queue} = queue_pop(queue)
{:ok, pid} = Task.start(fn -> handle(type, payload) end) {:ok, pid} = Task.start(fn -> handle(type, payload) end)
mref = Process.monitor(pid) mref = Process.monitor(pid)
{:sets.add_element(mref, running_jobs), queue} {:sets.add_element(mref, running_jobs), queue}
@ -95,20 +98,41 @@ def maybe_start_job(running_jobs, queue) do
end end
end end
def handle_cast({:enqueue, type, payload}, {running_jobs, queue}) do def handle_cast({:enqueue, type, payload, priority}, state) when type in [:incoming_doc] do
queue = :queue.in({type, payload}, queue) %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state
{running_jobs, queue} = maybe_start_job(running_jobs, queue) i_queue = enqueue_sorted(i_queue, {type, payload}, 1)
{:noreply, {running_jobs, queue}} {i_running_jobs, i_queue} = maybe_start_job(i_running_jobs, i_queue)
{:noreply, %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}}}
end end
def handle_info({:DOWN, ref, :process, _pid, _reason}, {running_jobs, queue}) do def handle_cast({:enqueue, type, payload, priority}, state) do
running_jobs = :sets.del_element(ref, running_jobs) %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state
{running_jobs, queue} = maybe_start_job(running_jobs, queue) o_queue = enqueue_sorted(o_queue, {type, payload}, 1)
{:noreply, {running_jobs, queue}} {o_running_jobs, o_queue} = maybe_start_job(o_running_jobs, o_queue)
{:noreply, %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}}}
end end
def handle_cast(m, state) do def handle_cast(m, state) do
IO.inspect("Unknown: #{inspect(m)}, #{inspect(state)}") IO.inspect("Unknown: #{inspect(m)}, #{inspect(state)}")
{:noreply, state} {:noreply, state}
end end
def handle_info({:DOWN, ref, :process, _pid, _reason}, state) do
%{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state
i_running_jobs = :sets.del_element(ref, i_running_jobs)
o_running_jobs = :sets.del_element(ref, o_running_jobs)
{i_running_jobs, i_queue} = maybe_start_job(i_running_jobs, i_queue)
{o_running_jobs, o_queue} = maybe_start_job(o_running_jobs, o_queue)
{:noreply, %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}}}
end
def enqueue_sorted(queue, element, priority) do
[%{item: element, priority: priority} | queue]
|> Enum.sort_by(fn (%{priority: priority}) -> priority end)
end
def queue_pop([%{item: element} | queue]) do
{element, queue}
end
end end

View file

@ -1,14 +1,14 @@
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
alias Pleroma.{Repo, Activity, User, Notification} alias Pleroma.{Repo, Activity, User, Notification}
alias Pleroma.Web.OAuth.App
alias Pleroma.Web alias Pleroma.Web
alias Pleroma.Web.MastodonAPI.{StatusView, AccountView} alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView}
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.TwitterAPI.TwitterAPI alias Pleroma.Web.{CommonAPI, OStatus}
alias Pleroma.Web.CommonAPI alias Pleroma.Web.OAuth.{Authorization, Token, App}
alias Comeonin.Pbkdf2
import Ecto.Query import Ecto.Query
import Logger require Logger
def create_app(conn, params) do def create_app(conn, params) do
with cs <- App.register_changeset(%App{}, params) |> IO.inspect, with cs <- App.register_changeset(%App{}, params) |> IO.inspect,
@ -23,7 +23,58 @@ def create_app(conn, params) do
end end
end end
def verify_credentials(%{assigns: %{user: user}} = conn, params) do def update_credentials(%{assigns: %{user: user}} = conn, params) do
params = if bio = params["note"] do
Map.put(params, "bio", bio)
else
params
end
params = if name = params["display_name"] do
Map.put(params, "name", name)
else
params
end
user = if avatar = params["avatar"] do
with %Plug.Upload{} <- avatar,
{:ok, object} <- ActivityPub.upload(avatar),
change = Ecto.Changeset.change(user, %{avatar: object.data}),
{:ok, user} = Repo.update(change) do
user
else
_e -> user
end
else
user
end
user = if banner = params["header"] do
with %Plug.Upload{} <- banner,
{:ok, object} <- ActivityPub.upload(banner),
new_info <- Map.put(user.info, "banner", object.data),
change <- User.info_changeset(user, %{info: new_info}),
{:ok, user} <- Repo.update(change) do
user
else
_e -> user
end
else
user
end
with changeset <- User.update_changeset(user, params),
{:ok, user} <- Repo.update(changeset) do
json conn, AccountView.render("account.json", %{user: user})
else
_e ->
conn
|> put_status(403)
|> json(%{error: "Invalid request"})
end
end
def verify_credentials(%{assigns: %{user: user}} = conn, _) do
account = AccountView.render("account.json", %{user: user}) account = AccountView.render("account.json", %{user: user})
json(conn, account) json(conn, account)
end end
@ -42,6 +93,7 @@ def user(conn, %{"id" => id}) do
@instance Application.get_env(:pleroma, :instance) @instance Application.get_env(:pleroma, :instance)
def masto_instance(conn, _params) do def masto_instance(conn, _params) do
user_count = Repo.aggregate(User.local_user_query, :count, :id)
response = %{ response = %{
uri: Web.base_url, uri: Web.base_url,
title: Keyword.get(@instance, :name), title: Keyword.get(@instance, :name),
@ -52,15 +104,33 @@ def masto_instance(conn, _params) do
streaming_api: String.replace(Web.base_url, ["http","https"], "wss") streaming_api: String.replace(Web.base_url, ["http","https"], "wss")
}, },
stats: %{ stats: %{
user_count: 1,
status_count: 2, status_count: 2,
user_count: user_count,
domain_count: 3 domain_count: 3
} },
max_toot_chars: Keyword.get(@instance, :limit)
} }
json(conn, response) json(conn, response)
end end
defp mastodonized_emoji do
Pleroma.Formatter.get_custom_emoji()
|> Enum.map(fn {shortcode, relative_url} ->
url = to_string URI.merge(Web.base_url(), relative_url)
%{
"shortcode" => shortcode,
"static_url" => url,
"url" => url
}
end)
end
def custom_emojis(conn, _params) do
mastodon_emoji = mastodonized_emoji()
json conn, mastodon_emoji
end
defp add_link_headers(conn, method, activities) do defp add_link_headers(conn, method, activities) do
last = List.last(activities) last = List.last(activities)
first = List.first(activities) first = List.first(activities)
@ -79,6 +149,7 @@ defp add_link_headers(conn, method, activities) do
def home_timeline(%{assigns: %{user: user}} = conn, params) do def home_timeline(%{assigns: %{user: user}} = conn, params) do
params = params params = params
|> Map.put("type", ["Create", "Announce"]) |> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
activities = ActivityPub.fetch_activities([user.ap_id | user.following], params) activities = ActivityPub.fetch_activities([user.ap_id | user.following], params)
|> Enum.reverse |> Enum.reverse
@ -92,6 +163,7 @@ def public_timeline(%{assigns: %{user: user}} = conn, params) do
params = params params = params
|> Map.put("type", ["Create", "Announce"]) |> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", !!params["local"]) |> Map.put("local_only", !!params["local"])
|> Map.put("blocking_user", user)
activities = ActivityPub.fetch_public_activities(params) activities = ActivityPub.fetch_public_activities(params)
|> Enum.reverse |> Enum.reverse
@ -107,6 +179,7 @@ def user_statuses(%{assigns: %{user: user}} = conn, params) do
params = params params = params
|> Map.put("type", ["Create", "Announce"]) |> Map.put("type", ["Create", "Announce"])
|> Map.put("actor_id", ap_id) |> Map.put("actor_id", ap_id)
|> Map.put("whole_db", true)
activities = ActivityPub.fetch_activities([], params) activities = ActivityPub.fetch_activities([], params)
|> Enum.reverse |> Enum.reverse
@ -123,8 +196,9 @@ def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Repo.get(Activity, id), with %Activity{} = activity <- Repo.get(Activity, id),
activities <- ActivityPub.fetch_activities_for_context(activity.data["object"]["context"]), activities <- ActivityPub.fetch_activities_for_context(activity.data["object"]["context"], %{"blocking_user" => user}),
activities <- activities |> Enum.filter(fn (%{id: aid}) -> to_string(aid) != to_string(id) end), activities <- activities |> Enum.filter(fn (%{id: aid}) -> to_string(aid) != to_string(id) end),
activities <- activities |> Enum.filter(fn (%{data: %{"type" => type}}) -> type == "Create" end),
grouped_activities <- Enum.group_by(activities, fn (%{id: id}) -> id < activity.id end) do grouped_activities <- Enum.group_by(activities, fn (%{id: id}) -> id < activity.id end) do
result = %{ result = %{
ancestors: StatusView.render("index.json", for: user, activities: grouped_activities[true] || [], as: :activity) |> Enum.reverse, ancestors: StatusView.render("index.json", for: user, activities: grouped_activities[true] || [], as: :activity) |> Enum.reverse,
@ -135,9 +209,10 @@ def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
end end
end end
def post_status(%{assigns: %{user: user}} = conn, %{"status" => status} = params) do def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
params = params params = params
|> Map.put("in_reply_to_status_id", params["in_reply_to_id"]) |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
|> Map.put("no_attachment_links", true)
{:ok, activity} = CommonAPI.post(user, params) {:ok, activity} = CommonAPI.post(user, params)
render conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity} render conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity}
@ -155,9 +230,8 @@ def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
end end
def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(ap_id_or_id, user), with {:ok, announce, _activity} = CommonAPI.repeat(ap_id_or_id, user) do
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do render conn, StatusView, "status.json", %{activity: announce, for: user, as: :activity}
render conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity}
end end
end end
@ -177,23 +251,8 @@ def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
def notifications(%{assigns: %{user: user}} = conn, params) do def notifications(%{assigns: %{user: user}} = conn, params) do
notifications = Notification.for_user(user, params) notifications = Notification.for_user(user, params)
result = Enum.map(notifications, fn (%{id: id, activity: activity, inserted_at: created_at}) -> result = Enum.map(notifications, fn x ->
actor = User.get_cached_by_ap_id(activity.data["actor"]) render_notification(user, x)
created_at = NaiveDateTime.to_iso8601(created_at)
|> String.replace(~r/(\.\d+)?$/, ".000Z", global: false)
case activity.data["type"] do
"Create" ->
%{id: id, type: "mention", created_at: created_at, account: AccountView.render("account.json", %{user: actor}), status: StatusView.render("status.json", %{activity: activity})}
"Like" ->
liked_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
%{id: id, type: "favourite", created_at: created_at, account: AccountView.render("account.json", %{user: actor}), status: StatusView.render("status.json", %{activity: liked_activity})}
"Announce" ->
announced_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
%{id: id, type: "reblog", created_at: created_at, account: AccountView.render("account.json", %{user: actor}), status: StatusView.render("status.json", %{activity: announced_activity})}
"Follow" ->
%{id: id, type: "follow", created_at: created_at, account: AccountView.render("account.json", %{user: actor})}
_ -> nil
end
end) end)
|> Enum.filter(&(&1)) |> Enum.filter(&(&1))
@ -202,6 +261,33 @@ def notifications(%{assigns: %{user: user}} = conn, params) do
|> json(result) |> json(result)
end end
def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
with {:ok, notification} <- Notification.get(user, id) do
json(conn, render_notification(user, notification))
else
{:error, reason} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Poison.encode!(%{"error" => reason}))
end
end
def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
Notification.clear(user)
json(conn, %{})
end
def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
with {:ok, _notif} <- Notification.dismiss(user, id) do
json(conn, %{})
else
{:error, reason} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Poison.encode!(%{"error" => reason}))
end
end
def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
id = List.wrap(id) id = List.wrap(id)
q = from u in User, q = from u in User,
@ -210,7 +296,7 @@ def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
render conn, AccountView, "relationships.json", %{user: user, targets: targets} render conn, AccountView, "relationships.json", %{user: user, targets: targets}
end end
def upload(%{assigns: %{user: user}} = conn, %{"file" => file}) do def upload(%{assigns: %{user: _}} = conn, %{"file" => file}) do
with {:ok, object} <- ActivityPub.upload(file) do with {:ok, object} <- ActivityPub.upload(file) do
data = object.data data = object.data
|> Map.put("id", object.id) |> Map.put("id", object.id)
@ -220,7 +306,7 @@ def upload(%{assigns: %{user: user}} = conn, %{"file" => file}) do
end end
def favourited_by(conn, %{"id" => id}) do def favourited_by(conn, %{"id" => id}) do
with %Activity{data: %{"object" => %{"likes" => likes} = data}} <- Repo.get(Activity, id) do with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
q = from u in User, q = from u in User,
where: u.ap_id in ^likes where: u.ap_id in ^likes
users = Repo.all(q) users = Repo.all(q)
@ -246,6 +332,7 @@ def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
params = params params = params
|> Map.put("type", "Create") |> Map.put("type", "Create")
|> Map.put("local_only", !!params["local"]) |> Map.put("local_only", !!params["local"])
|> Map.put("blocking_user", user)
activities = ActivityPub.fetch_public_activities(params) activities = ActivityPub.fetch_public_activities(params)
|> Enum.reverse |> Enum.reverse
@ -272,8 +359,26 @@ def following(conn, %{"id" => id}) do
def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
with %User{} = followed <- Repo.get(User, id), with %User{} = followed <- Repo.get(User, id),
{:ok, follower} <- User.follow(follower, followed), {:ok, follower} <- User.follow(follower, followed),
{:ok, activity} <- ActivityPub.follow(follower, followed) do {:ok, _activity} <- ActivityPub.follow(follower, followed) do
render conn, AccountView, "relationship.json", %{user: follower, target: followed} render conn, AccountView, "relationship.json", %{user: follower, target: followed}
else
{:error, message} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Poison.encode!(%{"error" => message}))
end
end
def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
with %User{} = followed <- Repo.get_by(User, nickname: uri),
{:ok, follower} <- User.follow(follower, followed),
{:ok, _activity} <- ActivityPub.follow(follower, followed) do
render conn, AccountView, "account.json", %{user: followed}
else
{:error, message} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Poison.encode!(%{"error" => message}))
end end
end end
@ -290,21 +395,55 @@ def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
end end
end end
def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
if params["resolve"] == "true" do with %User{} = blocked <- Repo.get(User, id),
User.get_or_fetch_by_nickname(query) {:ok, blocker} <- User.block(blocker, blocked) do
render conn, AccountView, "relationship.json", %{user: blocker, target: blocked}
else
{:error, message} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Poison.encode!(%{"error" => message}))
end
end end
q = from u in User, def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
where: fragment("(to_tsvector('english', ?) || to_tsvector('english', ?)) @@ plainto_tsquery('english', ?)", u.nickname, u.name, ^query), with %User{} = blocked <- Repo.get(User, id),
limit: 20 {:ok, blocker} <- User.unblock(blocker, blocked) do
accounts = Repo.all(q) render conn, AccountView, "relationship.json", %{user: blocker, target: blocked}
else
{:error, message} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Poison.encode!(%{"error" => message}))
end
end
# TODO: Use proper query
def blocks(%{assigns: %{user: user}} = conn, _) do
with blocked_users <- user.info["blocks"] || [],
accounts <- Enum.map(blocked_users, fn (ap_id) -> User.get_cached_by_ap_id(ap_id) end) do
res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
json(conn, res)
end
end
def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
accounts = User.search(query, params["resolve"] == "true")
fetched = if Regex.match?(~r/https?:/, query) do
with {:ok, activities} <- OStatus.fetch_activity_from_url(query) do
activities
else
_e -> []
end
end || []
q = from a in Activity, q = from a in Activity,
where: fragment("?->>'type' = 'Create'", a.data), where: fragment("?->>'type' = 'Create'", a.data),
where: fragment("to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)", a.data, ^query), where: fragment("to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)", a.data, ^query),
limit: 20 limit: 20
statuses = Repo.all(q) statuses = Repo.all(q) ++ fetched
res = %{ res = %{
"accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user), "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
@ -315,10 +454,19 @@ def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
json(conn, res) json(conn, res)
end end
def favourites(%{assigns: %{user: user}} = conn, params) do def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
accounts = User.search(query, params["resolve"] == "true")
res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
json(conn, res)
end
def favourites(%{assigns: %{user: user}} = conn, _) do
params = conn params = conn
|> Map.put("type", "Create") |> Map.put("type", "Create")
|> Map.put("favorited_by", user.ap_id) |> Map.put("favorited_by", user.ap_id)
|> Map.put("blocking_user", user)
activities = ActivityPub.fetch_activities([], params) activities = ActivityPub.fetch_activities([], params)
|> Enum.reverse |> Enum.reverse
@ -327,6 +475,127 @@ def favourites(%{assigns: %{user: user}} = conn, params) do
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity}) |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
end end
def index(%{assigns: %{user: user}} = conn, _params) do
token = conn
|> get_session(:oauth_token)
if user && token do
mastodon_emoji = mastodonized_emoji()
accounts = Map.put(%{}, user.id, AccountView.render("account.json", %{user: user}))
initial_state = %{
meta: %{
streaming_api_base_url: String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
access_token: token,
locale: "en",
domain: Pleroma.Web.Endpoint.host(),
admin: "1",
me: "#{user.id}",
unfollow_modal: false,
boost_modal: false,
delete_modal: true,
auto_play_gif: false,
reduce_motion: false
},
compose: %{
me: "#{user.id}",
default_privacy: "public",
default_sensitive: false
},
media_attachments: %{
accept_content_types: [
".jpg",
".jpeg",
".png",
".gif",
".webm",
".mp4",
".m4v",
"image\/jpeg",
"image\/png",
"image\/gif",
"video\/webm",
"video\/mp4"
]
},
settings: %{
onboarded: true,
home: %{
shows: %{
reblog: true,
reply: true
}
},
notifications: %{
alerts: %{
follow: true,
favourite: true,
reblog: true,
mention: true
},
shows: %{
follow: true,
favourite: true,
reblog: true,
mention: true
},
sounds: %{
follow: true,
favourite: true,
reblog: true,
mention: true
}
}
},
push_subscription: nil,
accounts: accounts,
custom_emojis: mastodon_emoji
} |> Poison.encode!
conn
|> put_layout(false)
|> render(MastodonView, "index.html", %{initial_state: initial_state})
else
conn
|> redirect(to: "/web/login")
end
end
def login(conn, _) do
conn
|> render(MastodonView, "login.html", %{error: false})
end
defp get_or_make_app() do
with %App{} = app <- Repo.get_by(App, client_name: "Mastodon-Local") do
{:ok, app}
else
_e ->
cs = App.register_changeset(%App{}, %{client_name: "Mastodon-Local", redirect_uris: ".", scopes: "read,write,follow"})
Repo.insert(cs)
end
end
def login_post(conn, %{"authorization" => %{ "name" => name, "password" => password}}) do
with %User{} = user <- User.get_cached_by_nickname(name),
true <- Pbkdf2.checkpw(password, user.password_hash),
{:ok, app} <- get_or_make_app(),
{:ok, auth} <- Authorization.create_authorization(app, user),
{:ok, token} <- Token.exchange_token(app, auth) do
conn
|> put_session(:oauth_token, token.token)
|> redirect(to: "/web/getting-started")
else
_e ->
conn
|> render(MastodonView, "login.html", %{error: "Wrong username or password"})
end
end
def logout(conn, _) do
conn
|> clear_session
|> redirect(to: "/")
end
def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
Logger.debug("Unimplemented, returning unmodified relationship") Logger.debug("Unimplemented, returning unmodified relationship")
with %User{} = target <- Repo.get(User, id) do with %User{} = target <- Repo.get(User, id) do
@ -338,4 +607,23 @@ def empty_array(conn, _) do
Logger.debug("Unimplemented, returning an empty array") Logger.debug("Unimplemented, returning an empty array")
json(conn, []) json(conn, [])
end end
def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
actor = User.get_cached_by_ap_id(activity.data["actor"])
created_at = NaiveDateTime.to_iso8601(created_at)
|> String.replace(~r/(\.\d+)?$/, ".000Z", global: false)
case activity.data["type"] do
"Create" ->
%{id: id, type: "mention", created_at: created_at, account: AccountView.render("account.json", %{user: actor}), status: StatusView.render("status.json", %{activity: activity, for: user})}
"Like" ->
liked_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
%{id: id, type: "favourite", created_at: created_at, account: AccountView.render("account.json", %{user: actor}), status: StatusView.render("status.json", %{activity: liked_activity, for: user})}
"Announce" ->
announced_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
%{id: id, type: "reblog", created_at: created_at, account: AccountView.render("account.json", %{user: actor}), status: StatusView.render("status.json", %{activity: announced_activity, for: user})}
"Follow" ->
%{id: id, type: "follow", created_at: created_at, account: AccountView.render("account.json", %{user: actor})}
_ -> nil
end
end
end end

View file

@ -0,0 +1,41 @@
defmodule Pleroma.Web.MastodonAPI.MastodonSocket do
use Phoenix.Socket
alias Pleroma.Web.OAuth.Token
alias Pleroma.{User, Repo}
transport :streaming, Phoenix.Transports.WebSocket.Raw,
timeout: :infinity # We never receive data.
def connect(params, socket) do
with token when not is_nil(token) <- params["access_token"],
%Token{user_id: user_id} <- Repo.get_by(Token, token: token),
%User{} = user <- Repo.get(User, user_id),
stream when stream in ["public", "public:local", "user"] <- params["stream"] do
socket = socket
|> assign(:topic, params["stream"])
|> assign(:user, user)
Pleroma.Web.Streamer.add_socket(params["stream"], socket)
{:ok, socket}
else
_e -> :error
end
end
def id(_), do: nil
def handle(:text, message, _state) do
IO.inspect message
#| :ok
#| state
#| {:text, message}
#| {:text, message, state}
#| {:close, "Goodbye!"}
{:text, message}
end
def handle(:closed, _, %{socket: socket}) do
topic = socket.assigns[:topic]
Pleroma.Web.Streamer.remove_socket(topic, socket)
end
end

View file

@ -4,7 +4,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.CommonAPI.Utils
defp image_url(%{"url" => [ %{ "href" => href } | t ]}), do: href defp image_url(%{"url" => [ %{ "href" => href } | _ ]}), do: href
defp image_url(_), do: nil defp image_url(_), do: nil
def render("accounts.json", %{users: users} = opts) do def render("accounts.json", %{users: users} = opts) do
@ -18,7 +18,7 @@ def render("account.json", %{user: user}) do
header = image_url(user.info["banner"]) || "https://placehold.it/700x335" header = image_url(user.info["banner"]) || "https://placehold.it/700x335"
%{ %{
id: user.id, id: to_string(user.id),
username: hd(String.split(user.nickname, "@")), username: hd(String.split(user.nickname, "@")),
acct: user.nickname, acct: user.nickname,
display_name: user.name, display_name: user.name,
@ -43,7 +43,7 @@ def render("account.json", %{user: user}) do
def render("mention.json", %{user: user}) do def render("mention.json", %{user: user}) do
%{ %{
id: user.id, id: to_string(user.id),
acct: user.nickname, acct: user.nickname,
username: hd(String.split(user.nickname, "@")), username: hd(String.split(user.nickname, "@")),
url: user.ap_id url: user.ap_id
@ -52,10 +52,10 @@ def render("mention.json", %{user: user}) do
def render("relationship.json", %{user: user, target: target}) do def render("relationship.json", %{user: user, target: target}) do
%{ %{
id: target.id, id: to_string(target.id),
following: User.following?(user, target), following: User.following?(user, target),
followed_by: User.following?(target, user), followed_by: User.following?(target, user),
blocking: false, blocking: User.blocks?(user, target),
muting: false, muting: false,
requested: false, requested: false,
domain_blocking: false domain_blocking: false

View file

@ -0,0 +1,5 @@
defmodule Pleroma.Web.MastodonAPI.MastodonView do
use Pleroma.Web, :view
import Phoenix.HTML
import Phoenix.HTML.Form
end

View file

@ -21,9 +21,9 @@ def render("status.json", %{activity: %{data: %{"type" => "Announce", "object" =
|> Enum.map(fn (user) -> AccountView.render("mention.json", %{user: user}) end) |> Enum.map(fn (user) -> AccountView.render("mention.json", %{user: user}) end)
%{ %{
id: activity.id, id: to_string(activity.id),
uri: object, uri: object,
url: nil, url: nil, # TODO: This might be wrong, check with mastodon.
account: AccountView.render("account.json", %{user: user}), account: AccountView.render("account.json", %{user: user}),
in_reply_to_id: nil, in_reply_to_id: nil,
in_reply_to_account_id: nil, in_reply_to_account_id: nil,
@ -45,7 +45,8 @@ def render("status.json", %{activity: %{data: %{"type" => "Announce", "object" =
name: "Web", name: "Web",
website: nil website: nil
}, },
language: nil language: nil,
emojis: []
} }
end end
@ -74,10 +75,13 @@ def render("status.json", %{activity: %{data: %{"object" => object}} = activity}
reply_to = Activity.get_create_activity_by_object_ap_id(object["inReplyTo"]) reply_to = Activity.get_create_activity_by_object_ap_id(object["inReplyTo"])
reply_to_user = reply_to && User.get_cached_by_ap_id(reply_to.data["actor"]) reply_to_user = reply_to && User.get_cached_by_ap_id(reply_to.data["actor"])
emojis = (activity.data["object"]["emoji"] || [])
|> Enum.map(fn {name, url} -> %{ shortcode: name, url: url, static_url: url } end)
%{ %{
id: activity.id, id: to_string(activity.id),
uri: object["id"], uri: object["id"],
url: object["external_url"], url: object["external_url"] || object["id"],
account: AccountView.render("account.json", %{user: user}), account: AccountView.render("account.json", %{user: user}),
in_reply_to_id: reply_to && reply_to.id, in_reply_to_id: reply_to && reply_to.id,
in_reply_to_account_id: reply_to_user && reply_to_user.id, in_reply_to_account_id: reply_to_user && reply_to_user.id,
@ -90,16 +94,17 @@ def render("status.json", %{activity: %{data: %{"object" => object}} = activity}
favourited: !!favorited, favourited: !!favorited,
muted: false, muted: false,
sensitive: sensitive, sensitive: sensitive,
spoiler_text: "", spoiler_text: object["summary"] || "",
visibility: "public", visibility: "public",
media_attachments: attachments, media_attachments: attachments |> Enum.take(4),
mentions: mentions, mentions: mentions,
tags: [], # fix, tags: [], # fix,
application: %{ application: %{
name: "Web", name: "Web",
website: nil website: nil
}, },
language: nil language: nil,
emojis: emojis
} }
end end
@ -115,7 +120,7 @@ def render("attachment.json", %{attachment: attachment}) do
<< hash_id::signed-32, _rest::binary >> = :crypto.hash(:md5, href) << hash_id::signed-32, _rest::binary >> = :crypto.hash(:md5, href)
%{ %{
id: attachment["id"] || hash_id, id: to_string(attachment["id"] || hash_id),
url: href, url: href,
remote_url: href, remote_url: href,
preview_url: href, preview_url: href,

View file

@ -25,7 +25,8 @@ def create_authorization(conn, %{"authorization" => %{"name" => name, "password"
auth: auth auth: auth
} }
else else
url = "#{redirect_uri}?code=#{auth.token}" connector = if String.contains?(redirect_uri, "?"), do: "&", else: "?"
url = "#{redirect_uri}#{connector}code=#{auth.token}"
url = if params["state"] do url = if params["state"] do
url <> "&state=#{params["state"]}" url <> "&state=#{params["state"]}"
else else
@ -40,7 +41,8 @@ def create_authorization(conn, %{"authorization" => %{"name" => name, "password"
# - proper scope handling # - proper scope handling
def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do
with %App{} = app <- Repo.get_by(App, client_id: params["client_id"], client_secret: params["client_secret"]), with %App{} = app <- Repo.get_by(App, client_id: params["client_id"], client_secret: params["client_secret"]),
%Authorization{} = auth <- Repo.get_by(Authorization, token: params["code"], app_id: app.id), fixed_token = fix_padding(params["code"]),
%Authorization{} = auth <- Repo.get_by(Authorization, token: fixed_token, app_id: app.id),
{:ok, token} <- Token.exchange_token(app, auth) do {:ok, token} <- Token.exchange_token(app, auth) do
response = %{ response = %{
token_type: "Bearer", token_type: "Bearer",
@ -50,6 +52,14 @@ def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do
scope: "read write follow" scope: "read write follow"
} }
json(conn, response) json(conn, response)
else
_error -> json(conn, %{error: "Invalid credentials"})
end end
end end
defp fix_padding(token) do
token
|> Base.url_decode64!(padding: false)
|> Base.url_encode64
end
end end

View file

@ -56,9 +56,9 @@ defp get_links(%{local: false,
defp get_links(_activity), do: [] defp get_links(_activity), do: []
defp get_emoji_links(content) do defp get_emoji_links(emojis) do
Enum.map(Formatter.get_emoji(content), fn({emoji, file}) -> Enum.map(emojis, fn({emoji, file}) ->
{:link, [name: to_charlist(emoji), rel: 'emoji', href: to_charlist("#{Pleroma.Web.Endpoint.static_url}#{file}")], []} {:link, [name: to_charlist(emoji), rel: 'emoji', href: to_charlist(file)], []}
end) end)
end end
@ -81,7 +81,13 @@ def to_simple_form(%{data: %{"object" => %{"type" => "Note"}}} = activity, user,
categories = (activity.data["object"]["tag"] || []) categories = (activity.data["object"]["tag"] || [])
|> Enum.map(fn (tag) -> {:category, [term: to_charlist(tag)], []} end) |> Enum.map(fn (tag) -> {:category, [term: to_charlist(tag)], []} end)
emoji_links = get_emoji_links(activity.data["object"]["content"] || "") emoji_links = get_emoji_links(activity.data["object"]["emoji"] || %{})
summary = if activity.data["object"]["summary"] do
[{:summary, [], h.(activity.data["object"]["summary"])}]
else
[]
end
[ [
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/note']}, {:"activity:object-type", ['http://activitystrea.ms/schema/1.0/note']},
@ -93,7 +99,7 @@ def to_simple_form(%{data: %{"object" => %{"type" => "Note"}}} = activity, user,
{:updated, h.(updated_at)}, {:updated, h.(updated_at)},
{:"ostatus:conversation", [ref: h.(activity.data["context"])], h.(activity.data["context"])}, {:"ostatus:conversation", [ref: h.(activity.data["context"])], h.(activity.data["context"])},
{:link, [ref: h.(activity.data["context"]), rel: 'ostatus:conversation'], []}, {:link, [ref: h.(activity.data["context"]), rel: 'ostatus:conversation'], []},
] ++ get_links(activity) ++ categories ++ attachments ++ in_reply_to ++ author ++ mentions ++ emoji_links ] ++ summary ++ get_links(activity) ++ categories ++ attachments ++ in_reply_to ++ author ++ mentions ++ emoji_links
end end
def to_simple_form(%{data: %{"type" => "Like"}} = activity, user, with_author) do def to_simple_form(%{data: %{"type" => "Like"}} = activity, user, with_author) do
@ -102,7 +108,7 @@ def to_simple_form(%{data: %{"type" => "Like"}} = activity, user, with_author) d
updated_at = activity.data["published"] updated_at = activity.data["published"]
inserted_at = activity.data["published"] inserted_at = activity.data["published"]
in_reply_to = get_in_reply_to(activity.data) _in_reply_to = get_in_reply_to(activity.data)
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: [] author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
mentions = activity.data["to"] |> get_mentions mentions = activity.data["to"] |> get_mentions
@ -130,7 +136,7 @@ def to_simple_form(%{data: %{"type" => "Announce"}} = activity, user, with_autho
updated_at = activity.data["published"] updated_at = activity.data["published"]
inserted_at = activity.data["published"] inserted_at = activity.data["published"]
in_reply_to = get_in_reply_to(activity.data) _in_reply_to = get_in_reply_to(activity.data)
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: [] author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
retweeted_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"]) retweeted_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
@ -227,6 +233,8 @@ def to_simple_form(%{data: %{"type" => "Delete"}} = activity, user, with_author)
] ++ author ] ++ author
end end
def to_simple_form(_, _, _), do: nil
def wrap_with_entry(simple_form) do def wrap_with_entry(simple_form) do
[{ [{
:entry, [ :entry, [
@ -238,6 +246,4 @@ def wrap_with_entry(simple_form) do
], simple_form ], simple_form
}] }]
end end
def to_simple_form(_, _, _), do: nil
end end

View file

@ -2,7 +2,7 @@ defmodule Pleroma.Web.OStatus.FeedRepresenter do
alias Pleroma.Web.OStatus alias Pleroma.Web.OStatus
alias Pleroma.Web.OStatus.{UserRepresenter, ActivityRepresenter} alias Pleroma.Web.OStatus.{UserRepresenter, ActivityRepresenter}
def to_simple_form(user, activities, users) do def to_simple_form(user, activities, _users) do
most_recent_update = (List.first(activities) || user).updated_at most_recent_update = (List.first(activities) || user).updated_at
|> NaiveDateTime.to_iso8601 |> NaiveDateTime.to_iso8601

View file

@ -1,10 +1,10 @@
defmodule Pleroma.Web.OStatus.DeleteHandler do defmodule Pleroma.Web.OStatus.DeleteHandler do
require Logger require Logger
alias Pleroma.Web.{XML, OStatus} alias Pleroma.Web.XML
alias Pleroma.{Activity, Object, Repo} alias Pleroma.Object
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
def handle_delete(entry, doc \\ nil) do def handle_delete(entry, _doc \\ nil) do
with id <- XML.string_from_xpath("//id", entry), with id <- XML.string_from_xpath("//id", entry),
object when not is_nil(object) <- Object.get_by_ap_id(id), object when not is_nil(object) <- Object.get_by_ap_id(id),
{:ok, delete} <- ActivityPub.delete(object, false) do {:ok, delete} <- ActivityPub.delete(object, false) do

View file

@ -94,6 +94,7 @@ def handle_note(entry, doc \\ nil) do
[author] <- :xmerl_xpath.string('//author[1]', doc), [author] <- :xmerl_xpath.string('//author[1]', doc),
{:ok, actor} <- OStatus.find_make_or_update_user(author), {:ok, actor} <- OStatus.find_make_or_update_user(author),
content_html <- OStatus.get_content(entry), content_html <- OStatus.get_content(entry),
cw <- OStatus.get_cw(entry),
inReplyTo <- XML.string_from_xpath("//thr:in-reply-to[1]/@ref", entry), inReplyTo <- XML.string_from_xpath("//thr:in-reply-to[1]/@ref", entry),
inReplyToActivity <- fetch_replied_to_activity(entry, inReplyTo), inReplyToActivity <- fetch_replied_to_activity(entry, inReplyTo),
inReplyTo <- (inReplyToActivity && inReplyToActivity.data["object"]["id"]) || inReplyTo, inReplyTo <- (inReplyToActivity && inReplyToActivity.data["object"]["id"]) || inReplyTo,
@ -103,7 +104,7 @@ def handle_note(entry, doc \\ nil) do
mentions <- get_mentions(entry), mentions <- get_mentions(entry),
to <- make_to_list(actor, mentions), to <- make_to_list(actor, mentions),
date <- XML.string_from_xpath("//published", entry), date <- XML.string_from_xpath("//published", entry),
note <- CommonAPI.Utils.make_note_data(actor.ap_id, to, context, content_html, attachments, inReplyToActivity, []), note <- CommonAPI.Utils.make_note_data(actor.ap_id, to, context, content_html, attachments, inReplyToActivity, [], cw),
note <- note |> Map.put("id", id) |> Map.put("tag", tags), note <- note |> Map.put("id", id) |> Map.put("tag", tags),
note <- note |> Map.put("published", date), note <- note |> Map.put("published", date),
note <- note |> Map.put("emoji", get_emoji(entry)), note <- note |> Map.put("emoji", get_emoji(entry)),
@ -112,7 +113,7 @@ def handle_note(entry, doc \\ nil) do
note <- (if inReplyTo && !inReplyToActivity, do: note |> Map.put("inReplyTo", inReplyTo), else: note) note <- (if inReplyTo && !inReplyToActivity, do: note |> Map.put("inReplyTo", inReplyTo), else: note)
do do
res = ActivityPub.create(to, actor, context, note, %{}, date, false) res = ActivityPub.create(to, actor, context, note, %{}, date, false)
User.update_note_count(actor) User.increase_note_count(actor)
res res
else else
%Activity{} = activity -> {:ok, activity} %Activity{} = activity -> {:ok, activity}

View file

@ -7,7 +7,6 @@ defmodule Pleroma.Web.OStatus do
alias Pleroma.{Repo, User, Web, Object, Activity} alias Pleroma.{Repo, User, Web, Object, Activity}
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.{WebFinger, Websub} alias Pleroma.Web.{WebFinger, Websub}
alias Pleroma.Web.OStatus.{FollowHandler, NoteHandler, DeleteHandler} alias Pleroma.Web.OStatus.{FollowHandler, NoteHandler, DeleteHandler}
@ -112,7 +111,7 @@ def get_or_try_fetching(entry) do
with id when not is_nil(id) <- string_from_xpath("//activity:object[1]/id", entry), with id when not is_nil(id) <- string_from_xpath("//activity:object[1]/id", entry),
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
{:ok, activity} {:ok, activity}
else e -> else _ ->
Logger.debug("Couldn't get, will try to fetch") Logger.debug("Couldn't get, will try to fetch")
with href when not is_nil(href) <- string_from_xpath("//activity:object[1]/link[@type=\"text/html\"]/@href", entry), with href when not is_nil(href) <- string_from_xpath("//activity:object[1]/link[@type=\"text/html\"]/@href", entry),
{:ok, [favorited_activity]} <- fetch_activity_from_url(href) do {:ok, [favorited_activity]} <- fetch_activity_from_url(href) do
@ -150,22 +149,28 @@ def get_attachments(entry) do
end end
@doc """ @doc """
Gets the content from a an entry. Will add the cw text to the body for cw'd Gets the content from a an entry.
Mastodon notes.
""" """
def get_content(entry) do def get_content(entry) do
base_content = string_from_xpath("//content", entry) string_from_xpath("//content", entry)
end
@doc """
Get the cw that mastodon uses.
"""
def get_cw(entry) do
with scope when not is_nil(scope) <- string_from_xpath("//mastodon:scope", entry), with scope when not is_nil(scope) <- string_from_xpath("//mastodon:scope", entry),
cw when not is_nil(cw) <- string_from_xpath("/*/summary", entry) do cw when not is_nil(cw) <- string_from_xpath("/*/summary", entry) do
"<span class='mastodon-cw'>#{cw}</span><br>#{base_content}" cw
else _e -> base_content else _e -> nil
end end
end end
def get_tags(entry) do def get_tags(entry) do
:xmerl_xpath.string('//category', entry) :xmerl_xpath.string('//category', entry)
|> Enum.map(fn (category) -> string_from_xpath("/category/@term", category) |> String.downcase end) |> Enum.map(fn (category) -> string_from_xpath("/category/@term", category) end)
|> Enum.filter(&(&1))
|> Enum.map(&String.downcase/1)
end end
def maybe_update(doc, user) do def maybe_update(doc, user) do
@ -185,7 +190,7 @@ def maybe_update(doc, user) do
false <- new_data == old_data do false <- new_data == old_data do
change = Ecto.Changeset.change(user, new_data) change = Ecto.Changeset.change(user, new_data)
Repo.update(change) Repo.update(change)
else e -> else _ ->
{:ok, user} {:ok, user}
end end
end end
@ -215,7 +220,7 @@ def insert_or_update_user(data) do
Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname) Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
end end
def make_user(uri) do def make_user(uri, update \\ false) do
with {:ok, info} <- gather_user_info(uri) do with {:ok, info} <- gather_user_info(uri) do
data = %{ data = %{
name: info["name"], name: info["name"],
@ -225,7 +230,8 @@ def make_user(uri) do
avatar: info["avatar"], avatar: info["avatar"],
bio: info["bio"] bio: info["bio"]
} }
with %User{} = user <- User.get_by_ap_id(data.ap_id) do with false <- update,
%User{} = user <- User.get_by_ap_id(data.ap_id) do
{:ok, user} {:ok, user}
else _e -> insert_or_update_user(data) else _e -> insert_or_update_user(data)
end end

View file

@ -5,6 +5,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do
alias Pleroma.Web.OStatus.{FeedRepresenter, ActivityRepresenter} alias Pleroma.Web.OStatus.{FeedRepresenter, ActivityRepresenter}
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.Web.{OStatus, Federator} alias Pleroma.Web.{OStatus, Federator}
alias Pleroma.Web.XML
import Ecto.Query import Ecto.Query
def feed_redirect(conn, %{"nickname" => nickname}) do def feed_redirect(conn, %{"nickname" => nickname}) do
@ -36,10 +37,26 @@ def feed(conn, %{"nickname" => nickname}) do
|> send_resp(200, response) |> send_resp(200, response)
end end
def salmon_incoming(conn, params) do defp decode_or_retry(body) do
with {:ok, magic_key} <- Pleroma.Web.Salmon.fetch_magic_key(body),
{:ok, doc} <- Pleroma.Web.Salmon.decode_and_validate(magic_key, body) do
{:ok, doc}
else
_e ->
with [decoded | _] <- Pleroma.Web.Salmon.decode(body),
doc <- XML.parse_document(decoded),
uri when not is_nil(uri) <- XML.string_from_xpath("/entry/author[1]/uri", doc),
{:ok, _} <- Pleroma.Web.OStatus.make_user(uri, true),
{:ok, magic_key} <- Pleroma.Web.Salmon.fetch_magic_key(body),
{:ok, doc} <- Pleroma.Web.Salmon.decode_and_validate(magic_key, body) do
{:ok, doc}
end
end
end
def salmon_incoming(conn, _) do
{:ok, body, _conn} = read_body(conn) {:ok, body, _conn} = read_body(conn)
{:ok, magic_key} = Pleroma.Web.Salmon.fetch_magic_key(body) {:ok, doc} = decode_or_retry(body)
{:ok, doc} = Pleroma.Web.Salmon.decode_and_validate(magic_key, body)
Federator.enqueue(:incoming_doc, doc) Federator.enqueue(:incoming_doc, doc)
@ -69,6 +86,19 @@ def activity(conn, %{"uuid" => uuid}) do
end end
end end
def notice(conn, %{"id" => id}) do
with %Activity{} = activity <- Repo.get(Activity, id),
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
case get_format(conn) do
"html" ->
conn
|> put_resp_content_type("text/html")
|> send_file(200, "priv/static/index.html")
_ -> represent_activity(conn, activity, user)
end
end
end
defp represent_activity(conn, activity, user) do defp represent_activity(conn, activity, user) do
response = activity response = activity
|> ActivityRepresenter.to_simple_form(user, true) |> ActivityRepresenter.to_simple_form(user, true)

View file

@ -19,6 +19,7 @@ def to_simple_form(user) do
{:"poco:preferredUsername", [nickname]}, {:"poco:preferredUsername", [nickname]},
{:"poco:displayName", [name]}, {:"poco:displayName", [name]},
{:"poco:note", [bio]}, {:"poco:note", [bio]},
{:summary, [bio]},
{:name, [nickname]}, {:name, [nickname]},
{:link, [rel: 'avatar', href: avatar_url], []} {:link, [rel: 'avatar', href: avatar_url], []}
] ++ banner ] ++ banner

View file

@ -21,6 +21,13 @@ def user_fetcher(username) do
plug Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1} plug Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1}
end end
pipeline :mastodon_html do
plug :accepts, ["html"]
plug :fetch_session
plug Pleroma.Plugs.OAuthPlug
plug Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1, optional: true}
end
pipeline :well_known do pipeline :well_known do
plug :accepts, ["xml", "xrd+xml"] plug :accepts, ["xml", "xrd+xml"]
end end
@ -33,6 +40,17 @@ def user_fetcher(username) do
plug :accepts, ["html", "json"] plug :accepts, ["html", "json"]
end end
pipeline :pleroma_api do
plug :accepts, ["html", "json"]
end
scope "/api/pleroma", Pleroma.Web.TwitterAPI do
pipe_through :pleroma_api
get "/password_reset/:token", UtilController, :show_password_reset
post "/password_reset", UtilController, :password_reset
get "/emoji", UtilController, :emoji
end
scope "/oauth", Pleroma.Web.OAuth do scope "/oauth", Pleroma.Web.OAuth do
get "/authorize", OAuthController, :authorize get "/authorize", OAuthController, :authorize
post "/authorize", OAuthController, :create_authorization post "/authorize", OAuthController, :create_authorization
@ -42,16 +60,21 @@ def user_fetcher(username) do
scope "/api/v1", Pleroma.Web.MastodonAPI do scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through :authenticated_api pipe_through :authenticated_api
patch "/accounts/update_credentials", MastodonAPIController, :update_credentials
get "/accounts/verify_credentials", MastodonAPIController, :verify_credentials get "/accounts/verify_credentials", MastodonAPIController, :verify_credentials
get "/accounts/relationships", MastodonAPIController, :relationships get "/accounts/relationships", MastodonAPIController, :relationships
get "/accounts/search", MastodonAPIController, :account_search
post "/accounts/:id/follow", MastodonAPIController, :follow post "/accounts/:id/follow", MastodonAPIController, :follow
post "/accounts/:id/unfollow", MastodonAPIController, :unfollow post "/accounts/:id/unfollow", MastodonAPIController, :unfollow
post "/accounts/:id/block", MastodonAPIController, :relationship_noop post "/accounts/:id/block", MastodonAPIController, :block
post "/accounts/:id/unblock", MastodonAPIController, :relationship_noop post "/accounts/:id/unblock", MastodonAPIController, :unblock
post "/accounts/:id/mute", MastodonAPIController, :relationship_noop post "/accounts/:id/mute", MastodonAPIController, :relationship_noop
post "/accounts/:id/unmute", MastodonAPIController, :relationship_noop post "/accounts/:id/unmute", MastodonAPIController, :relationship_noop
get "/blocks", MastodonAPIController, :empty_array post "/follows", MastodonAPIController, :follow
get "/blocks", MastodonAPIController, :blocks
get "/domain_blocks", MastodonAPIController, :empty_array get "/domain_blocks", MastodonAPIController, :empty_array
get "/follow_requests", MastodonAPIController, :empty_array get "/follow_requests", MastodonAPIController, :empty_array
get "/mutes", MastodonAPIController, :empty_array get "/mutes", MastodonAPIController, :empty_array
@ -67,7 +90,10 @@ def user_fetcher(username) do
post "/statuses/:id/favourite", MastodonAPIController, :fav_status post "/statuses/:id/favourite", MastodonAPIController, :fav_status
post "/statuses/:id/unfavourite", MastodonAPIController, :unfav_status post "/statuses/:id/unfavourite", MastodonAPIController, :unfav_status
post "/notifications/clear", MastodonAPIController, :clear_notifications
post "/notifications/dismiss", MastodonAPIController, :dismiss_notification
get "/notifications", MastodonAPIController, :notifications get "/notifications", MastodonAPIController, :notifications
get "/notifications/:id", MastodonAPIController, :get_notification
post "/media", MastodonAPIController, :upload post "/media", MastodonAPIController, :upload
end end
@ -76,6 +102,7 @@ def user_fetcher(username) do
pipe_through :api pipe_through :api
get "/instance", MastodonAPIController, :masto_instance get "/instance", MastodonAPIController, :masto_instance
post "/apps", MastodonAPIController, :create_app post "/apps", MastodonAPIController, :create_app
get "/custom_emojis", MastodonAPIController, :custom_emojis
get "/timelines/public", MastodonAPIController, :public_timeline get "/timelines/public", MastodonAPIController, :public_timeline
get "/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline get "/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline
@ -113,6 +140,7 @@ def user_fetcher(username) do
get "/statuses/networkpublic_timeline", TwitterAPI.Controller, :public_and_external_timeline get "/statuses/networkpublic_timeline", TwitterAPI.Controller, :public_and_external_timeline
get "/statuses/user_timeline", TwitterAPI.Controller, :user_timeline get "/statuses/user_timeline", TwitterAPI.Controller, :user_timeline
get "/qvitter/statuses/user_timeline", TwitterAPI.Controller, :user_timeline get "/qvitter/statuses/user_timeline", TwitterAPI.Controller, :user_timeline
get "/users/show", TwitterAPI.Controller, :show_user
get "/statuses/show/:id", TwitterAPI.Controller, :fetch_status get "/statuses/show/:id", TwitterAPI.Controller, :fetch_status
get "/statusnet/conversation/:id", TwitterAPI.Controller, :fetch_conversation get "/statusnet/conversation/:id", TwitterAPI.Controller, :fetch_conversation
@ -123,7 +151,6 @@ def user_fetcher(username) do
get "/search", TwitterAPI.Controller, :search get "/search", TwitterAPI.Controller, :search
get "/statusnet/tags/timeline/:tag", TwitterAPI.Controller, :public_and_external_timeline get "/statusnet/tags/timeline/:tag", TwitterAPI.Controller, :public_and_external_timeline
get "/externalprofile/show", TwitterAPI.Controller, :external_profile
end end
scope "/api", Pleroma.Web do scope "/api", Pleroma.Web do
@ -149,6 +176,8 @@ def user_fetcher(username) do
post "/friendships/create", TwitterAPI.Controller, :follow post "/friendships/create", TwitterAPI.Controller, :follow
post "/friendships/destroy", TwitterAPI.Controller, :unfollow post "/friendships/destroy", TwitterAPI.Controller, :unfollow
post "/blocks/create", TwitterAPI.Controller, :block
post "/blocks/destroy", TwitterAPI.Controller, :unblock
post "/statusnet/media/upload", TwitterAPI.Controller, :upload post "/statusnet/media/upload", TwitterAPI.Controller, :upload
post "/media/upload", TwitterAPI.Controller, :upload_json post "/media/upload", TwitterAPI.Controller, :upload_json
@ -161,6 +190,12 @@ def user_fetcher(username) do
get "/statuses/followers", TwitterAPI.Controller, :followers get "/statuses/followers", TwitterAPI.Controller, :followers
get "/statuses/friends", TwitterAPI.Controller, :friends get "/statuses/friends", TwitterAPI.Controller, :friends
get "/friends/ids", TwitterAPI.Controller, :friends_ids
get "/friendships/no_retweets/ids", TwitterAPI.Controller, :empty_array
get "/mutes/users/ids", TwitterAPI.Controller, :empty_array
get "/externalprofile/show", TwitterAPI.Controller, :external_profile
end end
pipeline :ostatus do pipeline :ostatus do
@ -172,6 +207,7 @@ def user_fetcher(username) do
get "/objects/:uuid", OStatus.OStatusController, :object get "/objects/:uuid", OStatus.OStatusController, :object
get "/activities/:uuid", OStatus.OStatusController, :activity get "/activities/:uuid", OStatus.OStatusController, :activity
get "/notice/:id", OStatus.OStatusController, :notice
get "/users/:nickname/feed", OStatus.OStatusController, :feed get "/users/:nickname/feed", OStatus.OStatusController, :feed
get "/users/:nickname", OStatus.OStatusController, :feed_redirect get "/users/:nickname", OStatus.OStatusController, :feed_redirect
@ -188,6 +224,15 @@ def user_fetcher(username) do
get "/webfinger", WebFinger.WebFingerController, :webfinger get "/webfinger", WebFinger.WebFingerController, :webfinger
end end
scope "/", Pleroma.Web.MastodonAPI do
pipe_through :mastodon_html
get "/web/login", MastodonAPIController, :login
post "/web/login", MastodonAPIController, :login_post
get "/web/*path", MastodonAPIController, :index
delete "/auth/sign_out", MastodonAPIController, :logout
end
scope "/", Fallback do scope "/", Fallback do
get "/*path", RedirectController, :redirector get "/*path", RedirectController, :redirector
end end

View file

@ -73,6 +73,18 @@ def encode_key({:RSAPublicKey, modulus, exponent}) do
"RSA.#{modulus_enc}.#{exponent_enc}" "RSA.#{modulus_enc}.#{exponent_enc}"
end end
# Native generation of RSA keys is only available since OTP 20+ and in default build conditions
# We try at compile time to generate natively an RSA key otherwise we fallback on the old way.
try do
_ = :public_key.generate_key({:rsa, 2048, 65537})
def generate_rsa_pem do
key = :public_key.generate_key({:rsa, 2048, 65537})
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
pem = :public_key.pem_encode([entry]) |> String.trim_trailing
{:ok, pem}
end
rescue
_ ->
def generate_rsa_pem do def generate_rsa_pem do
port = Port.open({:spawn, "openssl genrsa"}, [:binary]) port = Port.open({:spawn, "openssl genrsa"}, [:binary])
{:ok, pem} = receive do {:ok, pem} = receive do
@ -85,6 +97,7 @@ def generate_rsa_pem do
:error :error
end end
end end
end
def keys_from_pem(pem) do def keys_from_pem(pem) do
[private_key_code] = :public_key.pem_decode(pem) [private_key_code] = :public_key.pem_decode(pem)

112
lib/pleroma/web/streamer.ex Normal file
View file

@ -0,0 +1,112 @@
defmodule Pleroma.Web.Streamer do
use GenServer
require Logger
alias Pleroma.{User, Notification}
def start_link do
spawn(fn ->
Process.sleep(1000 * 30) # 30 seconds
GenServer.cast(__MODULE__, %{action: :ping})
end)
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
end
def add_socket(topic, socket) do
GenServer.cast(__MODULE__, %{action: :add, socket: socket, topic: topic})
end
def remove_socket(topic, socket) do
GenServer.cast(__MODULE__, %{action: :remove, socket: socket, topic: topic})
end
def stream(topic, item) do
GenServer.cast(__MODULE__, %{action: :stream, topic: topic, item: item})
end
def handle_cast(%{action: :ping}, topics) do
Map.values(topics)
|> List.flatten
|> Enum.each(fn (socket) ->
Logger.debug("Sending keepalive ping")
send socket.transport_pid, {:text, ""}
end)
spawn(fn ->
Process.sleep(1000 * 30) # 30 seconds
GenServer.cast(__MODULE__, %{action: :ping})
end)
{:noreply, topics}
end
def handle_cast(%{action: :stream, topic: "user", item: %Notification{} = item}, topics) do
topic = "user:#{item.user_id}"
Enum.each(topics[topic] || [], fn (socket) ->
json = %{
event: "notification",
payload: Pleroma.Web.MastodonAPI.MastodonAPIController.render_notification(socket.assigns["user"], item) |> Poison.encode!
} |> Poison.encode!
send socket.transport_pid, {:text, json}
end)
{:noreply, topics}
end
def handle_cast(%{action: :stream, topic: "user", item: item}, topics) do
Logger.debug("Trying to push to users")
recipient_topics = User.get_recipients_from_activity(item)
|> Enum.map(fn (%{id: id}) -> "user:#{id}" end)
Enum.each(recipient_topics, fn (topic) ->
push_to_socket(topics, topic, item)
end)
{:noreply, topics}
end
def handle_cast(%{action: :stream, topic: topic, item: item}, topics) do
Logger.debug("Trying to push to #{topic}")
Logger.debug("Pushing item to #{topic}")
push_to_socket(topics, topic, item)
{:noreply, topics}
end
def handle_cast(%{action: :add, topic: topic, socket: socket}, sockets) do
topic = internal_topic(topic, socket)
sockets_for_topic = sockets[topic] || []
sockets_for_topic = Enum.uniq([socket | sockets_for_topic])
sockets = Map.put(sockets, topic, sockets_for_topic)
Logger.debug("Got new conn for #{topic}")
IO.inspect(sockets)
{:noreply, sockets}
end
def handle_cast(%{action: :remove, topic: topic, socket: socket}, sockets) do
topic = internal_topic(topic, socket)
sockets_for_topic = sockets[topic] || []
sockets_for_topic = List.delete(sockets_for_topic, socket)
sockets = Map.put(sockets, topic, sockets_for_topic)
Logger.debug("Removed conn for #{topic}")
IO.inspect(sockets)
{:noreply, sockets}
end
def handle_cast(m, state) do
IO.inspect("Unknown: #{inspect(m)}, #{inspect(state)}")
{:noreply, state}
end
def push_to_socket(topics, topic, item) do
Enum.each(topics[topic] || [], fn (socket) ->
json = %{
event: "update",
payload: Pleroma.Web.MastodonAPI.StatusView.render("status.json", activity: item, for: socket.assigns[:user]) |> Poison.encode!
} |> Poison.encode!
send socket.transport_pid, {:text, json}
end)
end
defp internal_topic("user", socket) do
"user:#{socket.assigns[:user].id}"
end
defp internal_topic(topic, _), do: topic
end

View file

@ -3,9 +3,73 @@
<head> <head>
<meta charset=utf-8 /> <meta charset=utf-8 />
<title>Pleroma</title> <title>Pleroma</title>
<style>
body {
background-color: #282c37;
font-family: sans-serif;
color:white;
text-align: center;
}
.container {
margin: 50px auto;
max-width: 320px;
padding: 0;
padding: 40px 40px 40px 40px;
background-color: #313543;
border-radius: 4px;
}
h1 {
margin: 0;
}
h2 {
color: #9baec8;
font-weight: normal;
font-size: 20px;
margin-bottom: 40px;
}
form {
width: 100%;
}
input {
box-sizing: border-box;
width: 100%;
padding: 10px;
margin-top: 20px;
background-color: rgba(0,0,0,.1);
color: white;
border: 0;
border-bottom: 2px solid #9baec8;
font-size: 14px;
}
input:focus {
border-bottom: 2px solid #4b8ed8;
}
button {
box-sizing: border-box;
width: 100%;
color: white;
background-color: #419bdd;
border-radius: 4px;
border: none;
padding: 10px;
margin-top: 30px;
text-transform: uppercase;
font-weight: 500;
font-size: 16px;
}
</style>
</head> </head>
<body> <body>
<h1>Welcome to Pleroma</h1> <div class="container">
<h1>Pleroma</h1>
<%= render @view_module, @view_template, assigns %> <%= render @view_module, @view_template, assigns %>
</div>
</body> </body>
</html> </html>

View file

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<meta content='width=device-width, initial-scale=1' name='viewport'>
<link rel="stylesheet" media="all" href="/packs/common.css" />
<link rel="stylesheet" media="all" href="/packs/default.css" />
<link rel="stylesheet" media="all" href="/packs/pl-dark-masto-fe.css" />
<script src="/packs/common.js"></script>
<script src="/packs/locale_en.js"></script>
<script id='initial-state' type='application/json'><%= raw @initial_state %></script>
<script src="/packs/application.js"></script>
</head>
<body class='app-body'>
<div class='app-holder' data-props='{&quot;locale&quot;:&quot;en&quot;}' id='mastodon'>
</div>
</body>
</html>

View file

@ -0,0 +1,11 @@
<h2>Login in to Mastodon Frontend</h2>
<%= if @error do %>
<h2><%= @error %></h2>
<% end %>
<%= form_for @conn, mastodon_api_path(@conn, :login), [as: "authorization"], fn f -> %>
<%= text_input f, :name, placeholder: "Username" %>
<br>
<%= password_input f, :password, placeholder: "Password" %>
<br>
<%= submit "Log in" %>
<% end %>

View file

@ -0,0 +1 @@
<h2>Invalid Token</h2>

View file

@ -0,0 +1,12 @@
<h2>Password Reset for <%= @user.nickname %></h2>
<%= form_for @conn, util_path(@conn, :password_reset), [as: "data"], fn f -> %>
<%= label f, :password, "Password" %>
<%= password_input f, :password %>
<br>
<%= label f, :password_confirmation, "Confirmation" %>
<%= password_input f, :password_confirmation %>
<br>
<%= hidden_input f, :token, value: @token.token %>
<%= submit "Reset" %>
<% end %>

View file

@ -0,0 +1 @@
<h2>Password reset failed</h2>

View file

@ -0,0 +1 @@
<h2>Password changed!</h2>

View file

@ -1,6 +1,29 @@
defmodule Pleroma.Web.TwitterAPI.UtilController do defmodule Pleroma.Web.TwitterAPI.UtilController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
alias Pleroma.Web alias Pleroma.Web
alias Pleroma.Formatter
alias Pleroma.{Repo, PasswordResetToken, User}
def show_password_reset(conn, %{"token" => token}) do
with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}),
%User{} = user <- Repo.get(User, token.user_id) do
render conn, "password_reset.html", %{
token: token,
user: user
}
else
_e -> render conn, "invalid_token.html"
end
end
def password_reset(conn, %{"data" => data}) do
with {:ok, _} <- PasswordResetToken.reset_password(data["token"], data) do
render conn, "password_reset_success.html"
else
_e -> render conn, "password_reset_failed.html"
end
end
def help_test(conn, _params) do def help_test(conn, _params) do
json(conn, "ok") json(conn, "ok")
@ -46,4 +69,8 @@ def version(conn, _params) do
_ -> json(conn, version) _ -> json(conn, version)
end end
end end
def emoji(conn, _params) do
json conn, Enum.into(Formatter.get_custom_emoji(), %{})
end
end end

View file

@ -97,7 +97,7 @@ def to_map(%Activity{data: %{"type" => "Undo", "published" => created_at, "objec
} }
end end
def to_map(%Activity{data: %{"type" => "Delete", "published" => created_at, "object" => deleted_object }} = activity, %{user: user} = opts) do def to_map(%Activity{data: %{"type" => "Delete", "published" => created_at, "object" => _ }} = activity, %{user: user} = opts) do
created_at = created_at |> Utils.date_to_asctime created_at = created_at |> Utils.date_to_asctime
%{ %{
@ -135,6 +135,13 @@ def to_map(%Activity{data: %{"object" => %{"content" => content} = object}} = ac
tags = activity.data["object"]["tag"] || [] tags = activity.data["object"]["tag"] || []
possibly_sensitive = Enum.member?(tags, "nsfw") possibly_sensitive = Enum.member?(tags, "nsfw")
summary = activity.data["object"]["summary"]
content = if !!summary and summary != "" do
"<span>#{activity.data["object"]["summary"]}</span><br />#{content}</span>"
else
content
end
html = HtmlSanitizeEx.basic_html(content) |> Formatter.emojify(object["emoji"]) html = HtmlSanitizeEx.basic_html(content) |> Formatter.emojify(object["emoji"])
%{ %{

View file

@ -4,27 +4,29 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
alias Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter alias Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter
alias Pleroma.Web.TwitterAPI.UserView alias Pleroma.Web.TwitterAPI.UserView
alias Pleroma.Web.{OStatus, CommonAPI} alias Pleroma.Web.{OStatus, CommonAPI}
alias Pleroma.Formatter
import Ecto.Query import Ecto.Query
@httpoison Application.get_env(:pleroma, :httpoison) @httpoison Application.get_env(:pleroma, :httpoison)
def create_status(%User{} = user, %{"status" => status} = data) do def create_status(%User{} = user, %{"status" => _} = data) do
CommonAPI.post(user, data) CommonAPI.post(user, data)
end end
def fetch_friend_statuses(user, opts \\ %{}) do def fetch_friend_statuses(user, opts \\ %{}) do
opts = Map.put(opts, "blocking_user", user)
ActivityPub.fetch_activities([user.ap_id | user.following], opts) ActivityPub.fetch_activities([user.ap_id | user.following], opts)
|> activities_to_statuses(%{for: user}) |> activities_to_statuses(%{for: user})
end end
def fetch_public_statuses(user, opts \\ %{}) do def fetch_public_statuses(user, opts \\ %{}) do
opts = Map.put(opts, "local_only", true) opts = Map.put(opts, "local_only", true)
opts = Map.put(opts, "blocking_user", user)
ActivityPub.fetch_public_activities(opts) ActivityPub.fetch_public_activities(opts)
|> activities_to_statuses(%{for: user}) |> activities_to_statuses(%{for: user})
end end
def fetch_public_and_external_statuses(user, opts \\ %{}) do def fetch_public_and_external_statuses(user, opts \\ %{}) do
opts = Map.put(opts, "blocking_user", user)
ActivityPub.fetch_public_activities(opts) ActivityPub.fetch_public_activities(opts)
|> activities_to_statuses(%{for: user}) |> activities_to_statuses(%{for: user})
end end
@ -41,7 +43,7 @@ def fetch_mentions(user, opts \\ %{}) do
def fetch_conversation(user, id) do def fetch_conversation(user, id) do
with context when is_binary(context) <- conversation_id_to_context(id), with context when is_binary(context) <- conversation_id_to_context(id),
activities <- ActivityPub.fetch_activities_for_context(context), activities <- ActivityPub.fetch_activities_for_context(context, %{"blocking_user" => user}),
statuses <- activities |> activities_to_statuses(%{for: user}) statuses <- activities |> activities_to_statuses(%{for: user})
do do
statuses statuses
@ -83,6 +85,26 @@ def unfollow(%User{} = follower, params) do
end end
end end
def block(%User{} = blocker, params) do
with {:ok, %User{} = blocked} <- get_user(params),
{:ok, blocker} <- User.block(blocker, blocked)
do
{:ok, blocker, blocked}
else
err -> err
end
end
def unblock(%User{} = blocker, params) do
with {:ok, %User{} = blocked} <- get_user(params),
{:ok, blocker} <- User.unblock(blocker, blocked)
do
{:ok, blocker, blocked}
else
err -> err
end
end
def repeat(%User{} = user, ap_id_or_id) do def repeat(%User{} = user, ap_id_or_id) do
with {:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(ap_id_or_id, user), with {:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id), %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id),
@ -193,7 +215,7 @@ def get_user(user \\ nil, params) do
end end
end end
defp parse_int(string, default \\ nil) defp parse_int(string, default)
defp parse_int(string, default) when is_binary(string) do defp parse_int(string, default) when is_binary(string) do
with {n, _} <- Integer.parse(string) do with {n, _} <- Integer.parse(string) do
n n

View file

@ -3,17 +3,18 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
alias Pleroma.Web.TwitterAPI.{TwitterAPI, UserView} alias Pleroma.Web.TwitterAPI.{TwitterAPI, UserView}
alias Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter alias Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.{Repo, Activity, User, Object} alias Pleroma.{Repo, Activity, User}
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Ecto.Changeset alias Ecto.Changeset
require Logger require Logger
def verify_credentials(%{assigns: %{user: user}} = conn, _params) do def verify_credentials(%{assigns: %{user: user}} = conn, _params) do
render(conn, UserView, "show.json", %{user: user}) token = Phoenix.Token.sign(conn, "user socket", user.id)
render(conn, UserView, "show.json", %{user: user, token: token})
end end
def status_update(%{assigns: %{user: user}} = conn, %{"status" => status_text} = status_data) do def status_update(%{assigns: %{user: user}} = conn, %{"status" => _} = status_data) do
with media_ids <- extract_media_ids(status_data), with media_ids <- extract_media_ids(status_data),
{:ok, activity} <- TwitterAPI.create_status(user, Map.put(status_data, "media_ids", media_ids)) do {:ok, activity} <- TwitterAPI.create_status(user, Map.put(status_data, "media_ids", media_ids)) do
conn conn
@ -65,10 +66,23 @@ def friends_timeline(%{assigns: %{user: user}} = conn, params) do
|> json_reply(200, json) |> json_reply(200, json)
end end
def show_user(conn, params) do
with {:ok, shown} <- TwitterAPI.get_user(params) do
if user = conn.assigns.user do
render conn, UserView, "show.json", %{user: shown, for: user}
else
render conn, UserView, "show.json", %{user: shown}
end
else
{:error, msg} ->
bad_request_reply(conn, msg)
end
end
def user_timeline(%{assigns: %{user: user}} = conn, params) do def user_timeline(%{assigns: %{user: user}} = conn, params) do
case TwitterAPI.get_user(user, params) do case TwitterAPI.get_user(user, params) do
{:ok, target_user} -> {:ok, target_user} ->
params = Map.merge(params, %{"actor_id" => target_user.ap_id}) params = Map.merge(params, %{"actor_id" => target_user.ap_id, "whole_db" => true})
statuses = TwitterAPI.fetch_user_statuses(user, params) statuses = TwitterAPI.fetch_user_statuses(user, params)
conn conn
|> json_reply(200, statuses |> Poison.encode!) |> json_reply(200, statuses |> Poison.encode!)
@ -93,6 +107,22 @@ def follow(%{assigns: %{user: user}} = conn, params) do
end end
end end
def block(%{assigns: %{user: user}} = conn, params) do
case TwitterAPI.block(user, params) do
{:ok, user, blocked} ->
render conn, UserView, "show.json", %{user: blocked, for: user}
{:error, msg} -> forbidden_json_reply(conn, msg)
end
end
def unblock(%{assigns: %{user: user}} = conn, params) do
case TwitterAPI.unblock(user, params) do
{:ok, user, blocked} ->
render conn, UserView, "show.json", %{user: blocked, for: user}
{:error, msg} -> forbidden_json_reply(conn, msg)
end
end
def delete_post(%{assigns: %{user: user}} = conn, %{"id" => id}) do def delete_post(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {:ok, delete} <- CommonAPI.delete(id, user) do with {:ok, delete} <- CommonAPI.delete(id, user) do
json = ActivityRepresenter.to_json(delete, %{user: user, for: user}) json = ActivityRepresenter.to_json(delete, %{user: user, for: user})
@ -186,8 +216,8 @@ def update_banner(%{assigns: %{user: user}} = conn, params) do
with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}), with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}),
new_info <- Map.put(user.info, "banner", object.data), new_info <- Map.put(user.info, "banner", object.data),
change <- User.info_changeset(user, %{info: new_info}), change <- User.info_changeset(user, %{info: new_info}),
{:ok, user} <- Repo.update(change) do {:ok, _user} <- Repo.update(change) do
%{"url" => [ %{ "href" => href } | t ]} = object.data %{"url" => [ %{ "href" => href } | _ ]} = object.data
response = %{ url: href } |> Poison.encode! response = %{ url: href } |> Poison.encode!
conn conn
|> json_reply(200, response) |> json_reply(200, response)
@ -198,8 +228,8 @@ def update_background(%{assigns: %{user: user}} = conn, params) do
with {:ok, object} <- ActivityPub.upload(params), with {:ok, object} <- ActivityPub.upload(params),
new_info <- Map.put(user.info, "background", object.data), new_info <- Map.put(user.info, "background", object.data),
change <- User.info_changeset(user, %{info: new_info}), change <- User.info_changeset(user, %{info: new_info}),
{:ok, user} <- Repo.update(change) do {:ok, _user} <- Repo.update(change) do
%{"url" => [ %{ "href" => href } | t ]} = object.data %{"url" => [ %{ "href" => href } | _ ]} = object.data
response = %{ url: href } |> Poison.encode! response = %{ url: href } |> Poison.encode!
conn conn
|> json_reply(200, response) |> json_reply(200, response)
@ -225,7 +255,7 @@ def update_most_recent_notification(%{assigns: %{user: user}} = conn, %{"id" =>
mrn <- max(id, user.info["most_recent_notification"] || 0), mrn <- max(id, user.info["most_recent_notification"] || 0),
updated_info <- Map.put(info, "most_recent_notification", mrn), updated_info <- Map.put(info, "most_recent_notification", mrn),
changeset <- User.info_changeset(user, %{info: updated_info}), changeset <- User.info_changeset(user, %{info: updated_info}),
{:ok, user} <- Repo.update(changeset) do {:ok, _user} <- Repo.update(changeset) do
conn conn
|> json_reply(200, Poison.encode!(mrn)) |> json_reply(200, Poison.encode!(mrn))
else else
@ -249,6 +279,22 @@ def friends(%{assigns: %{user: user}} = conn, _params) do
end end
end end
def friends_ids(%{assigns: %{user: user}} = conn, _params) do
with {:ok, friends} <- User.get_friends(user) do
ids = friends
|> Enum.map(fn x -> x.id end)
|> Poison.encode!
json(conn, ids)
else
_e -> bad_request_reply(conn, "Can't get friends")
end
end
def empty_array(conn, _params) do
json(conn, Poison.encode!([]))
end
def update_profile(%{assigns: %{user: user}} = conn, params) do def update_profile(%{assigns: %{user: user}} = conn, params) do
params = if bio = params["description"] do params = if bio = params["description"] do
Map.put(params, "bio", bio) Map.put(params, "bio", bio)
@ -266,7 +312,7 @@ def update_profile(%{assigns: %{user: user}} = conn, params) do
end end
end end
def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do def search(%{assigns: %{user: user}} = conn, %{"q" => _query} = params) do
conn conn
|> json(TwitterAPI.search(user, params)) |> json(TwitterAPI.search(user, params))
end end

View file

@ -11,25 +11,28 @@ def render("index.json", %{users: users, for: user}) do
render_many(users, Pleroma.Web.TwitterAPI.UserView, "user.json", for: user) render_many(users, Pleroma.Web.TwitterAPI.UserView, "user.json", for: user)
end end
defp image_url(%{"url" => [ %{ "href" => href } | t ]}), do: href
defp image_url(_), do: nil
def render("user.json", %{user: user = %User{}} = assigns) do def render("user.json", %{user: user = %User{}} = assigns) do
image = User.avatar_url(user) image = User.avatar_url(user)
following = if assigns[:for] do {following, follows_you, statusnet_blocking} = if assigns[:for] do
User.following?(assigns[:for], user) {
User.following?(assigns[:for], user),
User.following?(user, assigns[:for]),
User.blocks?(assigns[:for], user)
}
else else
false {false, false, false}
end end
user_info = User.get_cached_user_info(user) user_info = User.get_cached_user_info(user)
%{ data = %{
"created_at" => user.inserted_at |> Utils.format_naive_asctime, "created_at" => user.inserted_at |> Utils.format_naive_asctime,
"description" => HtmlSanitizeEx.strip_tags(user.bio), "description" => HtmlSanitizeEx.strip_tags(user.bio),
"favourites_count" => 0, "favourites_count" => 0,
"followers_count" => user_info[:follower_count], "followers_count" => user_info[:follower_count],
"following" => following, "following" => following,
"follows_you" => follows_you,
"statusnet_blocking" => statusnet_blocking,
"friends_count" => user_info[:following_count], "friends_count" => user_info[:following_count],
"id" => user.id, "id" => user.id,
"name" => user.name, "name" => user.name,
@ -44,6 +47,12 @@ def render("user.json", %{user: user = %User{}} = assigns) do
"cover_photo" => image_url(user.info["banner"]), "cover_photo" => image_url(user.info["banner"]),
"background_image" => image_url(user.info["background"]) "background_image" => image_url(user.info["background"])
} }
if assigns[:token] do
Map.put(data, "token", assigns[:token])
else
data
end
end end
def render("short.json", %{user: %User{ def render("short.json", %{user: %User{
@ -57,4 +66,7 @@ def render("short.json", %{user: %User{
"screen_name" => nickname "screen_name" => nickname
} }
end end
defp image_url(%{"url" => [ %{ "href" => href } | _ ]}), do: href
defp image_url(_), do: nil
end end

View file

@ -0,0 +1,4 @@
defmodule Pleroma.Web.TwitterAPI.UtilView do
use Pleroma.Web, :view
import Phoenix.HTML.Form
end

View file

@ -89,7 +89,7 @@ def find_lrdd_template(domain) do
with {:ok, %{status_code: status_code, body: body}} when status_code in 200..299 <- @httpoison.get("http://#{domain}/.well-known/host-meta", [], follow_redirect: true) do with {:ok, %{status_code: status_code, body: body}} when status_code in 200..299 <- @httpoison.get("http://#{domain}/.well-known/host-meta", [], follow_redirect: true) do
get_template_from_xml(body) get_template_from_xml(body)
else else
e -> _ ->
with {:ok, %{body: body}} <- @httpoison.get("https://#{domain}/.well-known/host-meta", []) do with {:ok, %{body: body}} <- @httpoison.get("https://#{domain}/.well-known/host-meta", []) do
get_template_from_xml(body) get_template_from_xml(body)
else else

View file

@ -31,9 +31,9 @@ def verify(subscription, getter \\ &@httpoison.get/3) do
do do
changeset = Changeset.change(subscription, %{state: "active"}) changeset = Changeset.change(subscription, %{state: "active"})
Repo.update(changeset) Repo.update(changeset)
else _e -> else e ->
changeset = Changeset.change(subscription, %{state: "rejected"}) Logger.debug("Couldn't verify subscription")
{:ok, subscription} = Repo.update(changeset) Logger.debug(inspect(e))
{:error, subscription} {:error, subscription}
end end
end end

View file

@ -1,7 +1,7 @@
defmodule Pleroma.Web.XML do defmodule Pleroma.Web.XML do
require Logger require Logger
def string_from_xpath(xpath, :error), do: nil def string_from_xpath(_, :error), do: nil
def string_from_xpath(xpath, doc) do def string_from_xpath(xpath, doc) do
{:xmlObj, :string, res} = :xmerl_xpath.string('string(#{xpath})', doc) {:xmlObj, :string, res} = :xmerl_xpath.string('string(#{xpath})', doc)
@ -20,7 +20,7 @@ def parse_document(text) do
doc doc
catch catch
:exit, error -> :exit, _error ->
Logger.debug("Couldn't parse xml: #{inspect(text)}") Logger.debug("Couldn't parse xml: #{inspect(text)}")
:error :error
end end

77
lib/transports.ex Normal file
View file

@ -0,0 +1,77 @@
defmodule Phoenix.Transports.WebSocket.Raw do
import Plug.Conn, only: [
fetch_query_params: 1,
send_resp: 3
]
alias Phoenix.Socket.Transport
def default_config do
[
timeout: 60_000,
transport_log: false,
cowboy: Phoenix.Endpoint.CowboyWebSocket
]
end
def init(%Plug.Conn{method: "GET"} = conn, {endpoint, handler, transport}) do
{_, opts} = handler.__transport__(transport)
conn = conn
|> fetch_query_params
|> Transport.transport_log(opts[:transport_log])
|> Transport.force_ssl(handler, endpoint, opts)
|> Transport.check_origin(handler, endpoint, opts)
case conn do
%{halted: false} = conn ->
case Transport.connect(endpoint, handler, transport, __MODULE__, nil, conn.params) do
{:ok, socket} ->
{:ok, conn, {__MODULE__, {socket, opts}}}
:error ->
send_resp(conn, :forbidden, "")
{:error, conn}
end
_ ->
{:error, conn}
end
end
def init(conn, _) do
send_resp(conn, :bad_request, "")
{:error, conn}
end
def ws_init({socket, config}) do
Process.flag(:trap_exit, true)
{:ok, %{socket: socket}, config[:timeout]}
end
def ws_handle(op, data, state) do
state.socket.handler
|> apply(:handle, [op, data, state])
|> case do
{op, data} ->
{:reply, {op, data}, state}
{op, data, state} ->
{:reply, {op, data}, state}
%{} = state ->
{:ok, state}
_ ->
{:ok, state}
end
end
def ws_info({_,_} = tuple, state) do
{:reply, tuple, state}
end
def ws_info(_tuple, state), do: {:ok, state}
def ws_close(state) do
ws_handle(:closed, :normal, state)
end
def ws_terminate(reason, state) do
ws_handle(:closed, reason, state)
end
end

View file

@ -37,6 +37,6 @@ defp make_open_tag(tag, attributes) do
"#{attribute}=\"#{value}\"" "#{attribute}=\"#{value}\""
end |> Enum.join(" ") end |> Enum.join(" ")
[tag, attributes_string] |> Enum.join(" ") |> String.strip [tag, attributes_string] |> Enum.join(" ") |> String.trim
end end
end end

View file

@ -0,0 +1,13 @@
defmodule Pleroma.Repo.Migrations.CreatePasswordResetTokens do
use Ecto.Migration
def change do
create table(:password_reset_tokens) do
add :token, :string
add :user_id, references(:users)
add :used, :boolean, default: false
timestamps()
end
end
end

View file

@ -0,0 +1,10 @@
defmodule Pleroma.Repo.Migrations.AddSecondObjectIndexToActivty do
use Ecto.Migration
@disable_ddl_transaction true
def change do
drop_if_exists index(:activities, ["(data->'object'->>'id')", "(data->>'type')"], name: :activities_create_objects_index)
create index(:activities, ["(coalesce(data->'object'->>'id', data->>'object'))"], name: :activities_create_objects_index, concurrently: true)
end
end

View file

@ -0,0 +1,7 @@
defmodule Pleroma.Repo.Migrations.DropObjectIndex do
use Ecto.Migration
def change do
drop_if_exists index(:objects, [:data], using: :gin)
end
end

View file

@ -0,0 +1,9 @@
defmodule Pleroma.Repo.Migrations.AddObjectActorIndex do
use Ecto.Migration
@disable_ddl_transaction true
def change do
create index(:objects, ["(data->>'actor')", "(data->>'type')"], concurrently: true, name: :objects_actor_type)
end
end

View file

@ -0,0 +1,20 @@
defmodule Pleroma.Repo.Migrations.AddActorToActivity do
use Ecto.Migration
@disable_ddl_transaction true
def up do
alter table(:activities) do
add :actor, :string
end
create index(:activities, [:actor, "id DESC NULLS LAST"], concurrently: true)
end
def down do
drop index(:activities, [:actor, "id DESC NULLS LAST"])
alter table(:activities) do
remove :actor
end
end
end

View file

@ -0,0 +1,26 @@
defmodule Pleroma.Repo.Migrations.FillActorField do
use Ecto.Migration
alias Pleroma.{Repo, Activity}
def up do
max = Repo.aggregate(Activity, :max, :id)
if max do
IO.puts("#{max} activities")
chunks = 0..(round(max / 10_000))
Enum.each(chunks, fn (i) ->
min = i * 10_000
max = min + 10_000
execute("""
update activities set actor = data->>'actor' where id > #{min} and id <= #{max};
""")
|> IO.inspect
end)
end
end
def down do
end
end

View file

@ -0,0 +1,8 @@
defmodule Pleroma.Repo.Migrations.AddSortIndexToActivities do
use Ecto.Migration
@disable_ddl_transaction true
def change do
create index(:activities, ["id desc nulls last"], concurrently: true)
end
end

View file

@ -0,0 +1,7 @@
defmodule Pleroma.Repo.Migrations.AddLocalIndexToUser do
use Ecto.Migration
def change do
create index(:users, [:local])
end
end

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" style="enable-background:new 0 0 45 45;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath16" clipPathUnits="userSpaceOnUse"><path id="path18" d="M 0,36 36,36 36,0 0,0 0,36 Z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,45)" id="g10"><g id="g12"><g clip-path="url(#clipPath16)" id="g14"><g transform="translate(32,4)" id="g20"><path id="path22" style="fill:#e6e7e8;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-2.209 -1.791,-4 -4,-4 l -20,0 c -2.209,0 -4,1.791 -4,4 l 0,28 c 0,2.209 1.791,4 4,4 l 20,0 c 2.209,0 4,-1.791 4,-4 L 0,0 Z"/></g><g transform="translate(19.0029,23.5781)" id="g24"><path id="path26" style="fill:#be1931;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 0,-6.163 c 0.728,0.087 1.435,0.179 2.119,0.265 1.326,0.178 2.286,0.286 2.879,0.332 0.048,0.11 0.103,0.232 0.168,0.363 0.199,0.641 0.463,1.645 0.798,3.015 0.264,1.084 0.393,1.769 0.393,2.057 0,0.353 -0.338,0.529 -1.024,0.529 C 4.292,0.398 2.637,0.277 0.36,0.035 0.232,0.035 0.107,0.022 0,0 m -7.259,-6.69 c 1.812,0.085 3.545,0.195 5.203,0.327 l 0,6.132 c -1.702,-0.199 -3.347,-0.464 -4.937,-0.796 -0.31,-0.111 -0.596,-0.187 -0.862,-0.232 0,-0.264 0.022,-0.529 0.067,-0.793 0.265,-2.012 0.441,-3.56 0.529,-4.638 m -3.346,7.221 c 0.53,0 0.995,-0.078 1.391,-0.233 0.244,-0.066 0.453,-0.143 0.63,-0.231 0.796,0.044 1.612,0.122 2.453,0.231 1.104,0.134 2.463,0.298 4.075,0.498 l 0,2.319 c 0,1.613 -0.188,2.948 -0.563,4.01 -0.155,0.332 -0.233,0.551 -0.233,0.662 0,0.331 0.133,0.497 0.398,0.497 0.707,0 1.36,-0.089 1.955,-0.266 C 0.187,7.82 0.528,7.565 0.528,7.257 0.528,7.146 0.486,6.915 0.395,6.561 0.13,5.876 0,4.606 0,2.751 L 0,1.028 c 1.299,0.132 2.723,0.276 4.271,0.43 0.706,0.045 1.38,0.166 2.022,0.366 0.463,0.088 0.739,0.132 0.83,0.132 0.264,0 0.837,-0.222 1.72,-0.662 C 9.771,0.741 10.236,0.309 10.236,0 c 0,-0.242 -0.12,-0.486 -0.362,-0.729 C 9.055,-1.61 8.466,-2.538 8.115,-3.512 L 7.317,-5.203 C 7.187,-5.532 7.041,-5.787 6.892,-5.963 6.934,-5.984 6.978,-6.009 7.02,-6.03 c 0.507,-0.311 0.76,-0.586 0.76,-0.828 0,-0.199 -0.182,-0.31 -0.559,-0.332 -1.7,0 -3.237,-0.088 -4.607,-0.266 L 0,-7.652 -0.034,-12.095 c 0,-2.009 -0.069,-3.62 -0.198,-4.838 -0.134,-1.368 -0.334,-2.34 -0.598,-2.914 -0.243,-0.486 -0.443,-0.731 -0.596,-0.731 -0.09,0 -0.232,0.266 -0.431,0.798 -0.133,0.617 -0.199,1.687 -0.199,3.214 l 0,8.749 -1.789,-0.167 c -1.194,-0.112 -2.111,-0.168 -2.75,-0.168 -0.266,0 -0.488,0.024 -0.664,0.069 -0.044,-0.2 -0.11,-0.376 -0.197,-0.533 -0.133,-0.287 -0.277,-0.43 -0.433,-0.43 -0.197,0 -0.385,0.154 -0.562,0.465 -0.287,0.438 -0.442,0.892 -0.464,1.358 l -0.397,2.584 c -0.155,1.349 -0.332,2.286 -0.531,2.818 -0.111,0.551 -0.409,1.081 -0.894,1.59 -0.177,0.11 -0.266,0.187 -0.266,0.231 0,0.354 0.133,0.531 0.398,0.531"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" style="enable-background:new 0 0 45 45;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath16" clipPathUnits="userSpaceOnUse"><path id="path18" d="M 0,36 36,36 36,0 0,0 0,36 Z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,45)" id="g10"><g id="g12"><g clip-path="url(#clipPath16)" id="g14"><g transform="translate(32,4)" id="g20"><path id="path22" style="fill:#e6e7e8;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-2.209 -1.791,-4 -4,-4 l -20,0 c -2.209,0 -4,1.791 -4,4 l 0,28 c 0,2.209 1.791,4 4,4 l 20,0 c 2.209,0 4,-1.791 4,-4 L 0,0 Z"/></g><g transform="translate(11,29)" id="g24"><path id="path26" style="fill:#dd2e44;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 C -2.519,0 -4.583,-1.87 -4.929,-4.294 -4.198,-3.503 -3.161,-3 -2,-3 0.209,-3 2,-4.791 2,-7 2,-9 3.497,-9.198 2.706,-9.929 5.13,-9.583 5,-7.519 5,-5 5,-2.239 2.761,0 0,0"/></g><g transform="translate(23,22)" id="g28"><path id="path30" style="fill:#55acee;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 C 0,2.209 1.791,4 4,4 5.161,4 6.198,3.497 6.929,2.706 6.583,5.13 4.52,7 2,7 -0.762,7 -3,4.761 -3,2 -3,-0.519 -3.131,-2.583 -0.707,-2.929 -1.497,-2.198 0,-2 0,0"/></g><g transform="translate(14,24)" id="g32"><path id="path34" style="fill:#ffac33;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 C 0,4.971 4,9 4,9 4,9 8,4.971 8,0 8,-4.971 6.209,-9 4,-9 1.791,-9 0,-4.971 0,0"/></g><g transform="translate(11.7065,14.9287)" id="g36"><path id="path38" style="fill:#553788;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0.791,-0.73 1.294,-1.768 1.294,-2.929 0,-2.209 -1.791,-4 -4,-4 -1.162,0 -2.199,0.503 -2.929,1.293 0.345,-2.424 2.409,-4.293 4.929,-4.293 2.761,0 5,2.239 5,5 0,2.52 -1.87,4.583 -4.294,4.929"/></g><g transform="translate(27,8)" id="g40"><path id="path42" style="fill:#553788;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c -2.209,0 -4,1.791 -4,4 0,1.161 0.503,2.198 1.293,2.929 C -5.131,6.583 -7,4.52 -7,2 c 0,-2.762 2.238,-5 5,-5 2.52,0 4.583,1.869 4.929,4.293 C 2.198,0.503 1.161,0 0,0"/></g><g transform="translate(14,12)" id="g44"><path id="path46" style="fill:#9266cc;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 C 0,-4.971 3,-9 4,-9 5,-9 8,-4.971 8,0 8,4.971 6.209,9 4,9 1.791,9 0,4.971 0,0"/></g><g transform="translate(13,19)" id="g48"><path id="path50" style="fill:#edbb9f;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 C 0,3.866 3,4 5,4 7,4 10,3.866 10,0 10,-3.865 7.762,-7 5,-7 2.239,-7 0,-3.865 0,0"/></g><g transform="translate(17.0005,19)" id="g52"><path id="path54" style="fill:#662113;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-0.552 -0.448,-1 -1,-1 -0.552,0 -1,0.448 -1,1 0,0.552 0.448,1 1,1 0.552,0 1,-0.448 1,-1"/></g><g transform="translate(21,19)" id="g56"><path id="path58" style="fill:#662113;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-0.552 -0.447,-1 -1,-1 -0.552,0 -1,0.448 -1,1 0,0.552 0.448,1 1,1 0.553,0 1,-0.448 1,-1"/></g><g transform="translate(18,14)" id="g60"><path id="path62" style="fill:#662113;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 C 1.105,0 2,0.896 2,2 L -2,2 C -2,0.896 -1.104,0 0,0"/></g><g transform="translate(7,25)" id="g64"><path id="path66" style="fill:#a0041e;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-0.552 -0.448,-1 -1,-1 -0.552,0 -1,0.448 -1,1 0,0.552 0.448,1 1,1 0.552,0 1,-0.448 1,-1"/></g><g transform="translate(29,25)" id="g68"><path id="path70" style="fill:#226699;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 C 0,-0.552 0.447,-1 1,-1 1.553,-1 2,-0.552 2,0 2,0.552 1.553,1 1,1 0.447,1 0,0.552 0,0"/></g><g transform="translate(17,33)" id="g72"><path id="path74" style="fill:#dd2e44;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 C 0,0.552 0.448,1 1,1 1.552,1 2,0.552 2,0 2,-0.552 1.552,-1 1,-1 0.448,-1 0,-0.552 0,0"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" style="enable-background:new 0 0 45 45;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath16" clipPathUnits="userSpaceOnUse"><path id="path18" d="M 0,36 36,36 36,0 0,0 0,36 Z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,45)" id="g10"><g id="g12"><g clip-path="url(#clipPath16)" id="g14"><g transform="translate(36,4)" id="g20"><path id="path22" style="fill:#dd2e44;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-2.209 -1.791,-4 -4,-4 l -28,0 c -2.209,0 -4,1.791 -4,4 l 0,28 c 0,2.209 1.791,4 4,4 l 28,0 c 2.209,0 4,-1.791 4,-4 L 0,0 Z"/></g><g transform="translate(15.0874,15.6191)" id="g24"><path id="path26" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 5.891,0 3.008,8.992 2.946,8.992 0,0 Z m -0.341,11.256 c 0.527,1.426 1.736,2.573 3.318,2.573 1.643,0 2.791,-1.085 3.317,-2.573 l 6.078,-16.867 c 0.185,-0.496 0.248,-0.931 0.248,-1.148 0,-1.209 -0.993,-2.046 -2.139,-2.046 -1.303,0 -1.954,0.682 -2.264,1.612 l -0.93,2.915 -8.62,0 -0.93,-2.884 c -0.31,-0.961 -0.962,-1.643 -2.233,-1.643 -1.24,0 -2.294,0.93 -2.294,2.17 0,0.496 0.155,0.868 0.217,1.024 l 6.232,16.867 z"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" style="enable-background:new 0 0 45 45;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath16" clipPathUnits="userSpaceOnUse"><path id="path18" d="M 0,36 36,36 36,0 0,0 0,36 Z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,45)" id="g10"><g id="g12"><g clip-path="url(#clipPath16)" id="g14"><g transform="translate(36,4)" id="g20"><path id="path22" style="fill:#dd2e44;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-2.209 -1.791,-4 -4,-4 l -28,0 c -2.209,0 -4,1.791 -4,4 l 0,28 c 0,2.209 1.791,4 4,4 l 28,0 c 2.209,0 4,-1.791 4,-4 L 0,0 Z"/></g><g transform="translate(15.1489,11.0928)" id="g24"><path id="path26" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 3.659,0 c 1.675,0 2.915,0.961 2.915,2.697 0,1.458 -1.117,2.45 -3.287,2.45 L 0,5.147 0,0 Z m 0,9.24 2.419,0 c 1.519,0 2.511,0.899 2.511,2.449 0,1.457 -1.147,2.202 -2.511,2.202 L 0,13.891 0,9.24 Z m -4.65,6.418 c 0,1.488 1.023,2.325 2.449,2.325 l 5.953,0 c 3.224,0 5.83,-2.17 5.83,-5.457 0,-2.17 -0.901,-3.628 -2.885,-4.557 l 0,-0.063 c 2.637,-0.372 4.713,-2.573 4.713,-5.27 0,-4.372 -2.914,-6.729 -7.194,-6.729 l -6.386,0 c -1.427,0 -2.48,0.9 -2.48,2.357 l 0,17.394 z"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" style="enable-background:new 0 0 45 45;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath16" clipPathUnits="userSpaceOnUse"><path id="path18" d="M 0,36 36,36 36,0 0,0 0,36 Z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,45)" id="g10"><g id="g12"><g clip-path="url(#clipPath16)" id="g14"><g transform="translate(36,4)" id="g20"><path id="path22" style="fill:#dd2e44;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-2.209 -1.791,-4 -4,-4 l -28,0 c -2.209,0 -4,1.791 -4,4 l 0,28 c 0,2.209 1.791,4 4,4 l 28,0 c 2.209,0 4,-1.791 4,-4 L 0,0 Z"/></g><g transform="translate(24.0156,17.999)" id="g24"><path id="path26" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,3.872 -2.016,7.36 -6.016,7.36 -4,0 -6.015,-3.488 -6.015,-7.36 0,-3.903 1.951,-7.359 6.015,-7.359 C -1.951,-7.359 0,-3.903 0,0 m -17.023,0 c 0,6.656 4.48,11.776 11.007,11.776 6.432,0 11.008,-5.28 11.008,-11.776 0,-6.623 -4.449,-11.774 -11.008,-11.774 -6.495,0 -11.007,5.151 -11.007,11.774"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" style="enable-background:new 0 0 45 45;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath16" clipPathUnits="userSpaceOnUse"><path id="path18" d="M 0,36 36,36 36,0 0,0 0,36 Z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,45)" id="g10"><g id="g12"><g clip-path="url(#clipPath16)" id="g14"><g transform="translate(36,4)" id="g20"><path id="path22" style="fill:#226699;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-2.209 -1.791,-4 -4,-4 l -28,0 c -2.209,0 -4,1.791 -4,4 l 0,28 c 0,2.209 1.791,4 4,4 l 28,0 c 2.209,0 4,-1.791 4,-4 L 0,0 Z"/></g><g transform="translate(16,18)" id="g24"><path id="path26" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 3.063,0 c 2.017,0 3.296,1.465 3.296,3.385 0,1.92 -1.279,3.391 -3.296,3.391 L 0,6.776 0,0 Z M -5,8.504 C -5,10.008 -4.104,11 -2.504,11 l 5.664,0 c 4.703,0 8.192,-2.944 8.192,-7.52 0,-4.67 -3.618,-7.48 -8,-7.48 L 0,-4 0,-9.479 c 0,-1.599 -1.024,-2.496 -2.4,-2.496 -1.376,0 -2.6,0.897 -2.6,2.496 l 0,17.983 z"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" style="enable-background:new 0 0 45 45;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath16" clipPathUnits="userSpaceOnUse"><path id="path18" d="M 0,36 36,36 36,0 0,0 0,36 Z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,45)" id="g10"><g id="g12"><g clip-path="url(#clipPath16)" id="g14"><g transform="translate(36,4)" id="g20"><path id="path22" style="fill:#dd2e44;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-2.209 -1.791,-4 -4,-4 l -28,0 c -2.209,0 -4,1.791 -4,4 l 0,28 c 0,2.209 1.791,4 4,4 l 28,0 c 2.209,0 4,-1.791 4,-4 L 0,0 Z"/></g><g transform="translate(7.458,15.7646)" id="g24"><path id="path26" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 5.32,0 2.716,8.119 2.66,8.119 0,0 Z m -0.308,10.163 c 0.476,1.288 1.568,2.324 2.996,2.324 1.484,0 2.52,-0.979 2.997,-2.324 l 5.488,-15.231 c 0.168,-0.449 0.223,-0.84 0.223,-1.036 0,-1.092 -0.896,-1.848 -1.931,-1.848 -1.177,0 -1.765,0.616 -2.044,1.456 l -0.879,2.731 -7.72,0 -0.866,-2.703 c -0.28,-0.868 -0.868,-1.484 -2.016,-1.484 -1.12,0 -2.072,0.84 -2.072,1.96 0,0.448 0.14,0.784 0.196,0.924 l 5.628,15.231 z"/></g><g transform="translate(24.2002,12)" id="g28"><path id="path30" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 3.304,0 c 1.512,0 2.632,0.729 2.632,2.296 0,1.315 -1.008,2.112 -2.968,2.112 L 0,4.408 0,0 Z m 0,8 2.184,0 c 1.372,0 2.268,0.815 2.268,2.216 0,1.315 -1.036,2.088 -2.268,2.088 L 0,12.304 0,8 Z m -4.2,5.9 c 0,1.344 0.924,2.1 2.212,2.1 l 5.376,0 C 6.3,16 8.652,14.04 8.652,11.072 8.652,9.112 7.84,7.796 6.048,6.956 l 0,-0.056 C 8.428,6.564 10.304,4.477 10.304,2.041 10.304,-1.907 7.672,-4 3.808,-4 l -5.768,0 c -1.288,0 -2.24,0.876 -2.24,2.192 l 0,15.708 z"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" style="enable-background:new 0 0 45 45;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath16" clipPathUnits="userSpaceOnUse"><path id="path18" d="M 0,36 36,36 36,0 0,0 0,36 Z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,45)" id="g10"><g id="g12"><g clip-path="url(#clipPath16)" id="g14"><g transform="translate(36,4)" id="g20"><path id="path22" style="fill:#dd2e44;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-2.209 -1.791,-4 -4,-4 l -28,0 c -2.209,0 -4,1.791 -4,4 l 0,28 c 0,2.209 1.791,4 4,4 l 28,0 c 2.209,0 4,-1.791 4,-4 L 0,0 Z"/></g><g transform="translate(12.8101,29.4482)" id="g24"><path id="path26" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 2.263,0 6.667,-0.744 6.667,-3.473 0,-1.116 -0.776,-2.077 -1.923,-2.077 -1.271,0 -2.14,1.085 -4.744,1.085 -3.845,0 -5.829,-3.256 -5.829,-7.038 0,-3.689 2.015,-6.852 5.829,-6.852 2.604,0 3.658,1.301 4.93,1.301 1.395,0 2.046,-1.394 2.046,-2.107 0,-2.977 -4.682,-3.659 -6.976,-3.659 -6.294,0 -10.666,4.992 -10.666,11.41 C -10.666,-4.961 -6.325,0 0,0"/></g><g transform="translate(21.332,26.8438)" id="g28"><path id="path30" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 C 0,1.55 0.992,2.418 2.326,2.418 3.66,2.418 4.652,1.55 4.652,0 l 0,-15.564 5.518,0 c 1.582,0 2.264,-1.179 2.232,-2.233 -0.06,-1.023 -0.867,-2.047 -2.232,-2.047 l -7.75,0 C 0.9,-19.844 0,-18.852 0,-17.301 L 0,0 Z"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" style="enable-background:new 0 0 45 45;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath16" clipPathUnits="userSpaceOnUse"><path id="path18" d="M 0,36 36,36 36,0 0,0 0,36 Z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,45)" id="g10"><g id="g12"><g clip-path="url(#clipPath16)" id="g14"><g transform="translate(36,4)" id="g20"><path id="path22" style="fill:#3b88c3;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-2.209 -1.791,-4 -4,-4 l -28,0 c -2.209,0 -4,1.791 -4,4 l 0,28 c 0,2.209 1.791,4 4,4 l 28,0 c 2.209,0 4,-1.791 4,-4 L 0,0 Z"/></g><g transform="translate(5.9697,23.1416)" id="g24"><path id="path26" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 1.022,0 3.012,-0.336 3.012,-1.569 0,-0.504 -0.35,-0.939 -0.869,-0.939 -0.574,0 -0.966,0.49 -2.143,0.49 -1.737,0 -2.633,-1.47 -2.633,-3.179 0,-1.667 0.91,-3.096 2.633,-3.096 1.177,0 1.653,0.589 2.227,0.589 0.63,0 0.925,-0.631 0.925,-0.953 0,-1.345 -2.115,-1.653 -3.152,-1.653 -2.843,0 -4.818,2.255 -4.818,5.155 C -4.818,-2.241 -2.857,0 0,0"/></g><g transform="translate(16.3169,17.9863)" id="g28"><path id="path30" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,1.695 -0.882,3.222 -2.633,3.222 -1.751,0 -2.634,-1.527 -2.634,-3.222 0,-1.709 0.855,-3.222 2.634,-3.222 C -0.854,-3.222 0,-1.709 0,0 m -7.452,0 c 0,2.914 1.961,5.155 4.819,5.155 2.815,0 4.819,-2.311 4.819,-5.155 0,-2.899 -1.948,-5.154 -4.819,-5.154 -2.844,0 -4.819,2.255 -4.819,5.154"/></g><g transform="translate(26.4258,17.9863)" id="g32"><path id="path34" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,1.695 -0.883,3.222 -2.635,3.222 -1.75,0 -2.633,-1.527 -2.633,-3.222 0,-1.709 0.855,-3.222 2.633,-3.222 C -0.855,-3.222 0,-1.709 0,0 m -7.453,0 c 0,2.914 1.961,5.155 4.818,5.155 2.817,0 4.819,-2.311 4.819,-5.155 0,-2.899 -1.946,-5.154 -4.819,-5.154 -2.843,0 -4.818,2.255 -4.818,5.154"/></g><g transform="translate(29.1934,21.9648)" id="g36"><path id="path38" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 C 0,0.7 0.447,1.093 1.051,1.093 1.652,1.093 2.102,0.7 2.102,0 l 0,-7.032 2.492,0 c 0.715,0 1.023,-0.532 1.01,-1.008 -0.03,-0.463 -0.393,-0.925 -1.01,-0.925 l -3.502,0 C 0.406,-8.965 0,-8.517 0,-7.816 L 0,0 Z"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" style="enable-background:new 0 0 45 45;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath16" clipPathUnits="userSpaceOnUse"><path id="path18" d="M 0,36 36,36 36,0 0,0 0,36 Z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,45)" id="g10"><g id="g12"><g clip-path="url(#clipPath16)" id="g14"><g transform="translate(36,4)" id="g20"><path id="path22" style="fill:#3b88c3;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-2.209 -1.791,-4 -4,-4 l -28,0 c -2.209,0 -4,1.791 -4,4 l 0,28 c 0,2.209 1.791,4 4,4 l 28,0 c 2.209,0 4,-1.791 4,-4 L 0,0 Z"/></g><g transform="translate(1.8364,23.4155)" id="g24"><path id="path26" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,0.738 0.504,1.404 1.405,1.404 l 4.609,0 C 6.806,1.404 7.22,0.792 7.22,0.162 7.22,-0.45 6.824,-1.08 6.014,-1.08 l -3.313,0 0,-2.629 2.791,0 c 0.864,0 1.296,-0.613 1.296,-1.224 0,-0.631 -0.432,-1.261 -1.296,-1.261 l -2.791,0 0,-3.925 c 0,-0.9 -0.576,-1.405 -1.35,-1.405 -0.775,0 -1.351,0.505 -1.351,1.405 L 0,0 Z"/></g><g transform="translate(12.5293,19.1484)" id="g28"><path id="path30" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 1.855,0 c 0.99,0 1.674,0.594 1.674,1.603 0,1.025 -0.684,1.584 -1.674,1.584 L 0,3.187 0,0 Z m -2.701,4.267 c 0,0.864 0.486,1.404 1.387,1.404 l 3.169,0 c 2.772,0 4.483,-1.242 4.483,-4.068 0,-1.982 -1.495,-3.116 -3.331,-3.404 l 3.061,-3.277 c 0.252,-0.27 0.36,-0.54 0.36,-0.792 0,-0.702 -0.558,-1.387 -1.351,-1.387 -0.324,0 -0.756,0.126 -1.044,0.469 l -3.997,4.843 -0.036,0 0,-3.907 c 0,-0.9 -0.576,-1.405 -1.351,-1.405 -0.774,0 -1.35,0.505 -1.35,1.405 l 0,10.119 z"/></g><g transform="translate(19.4766,23.2534)" id="g32"><path id="path34" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,0.9 0.449,1.566 1.404,1.566 l 4.465,0 c 0.865,0 1.279,-0.612 1.279,-1.242 0,-0.612 -0.433,-1.242 -1.279,-1.242 l -3.168,0 0,-2.629 2.952,0 c 0.882,0 1.314,-0.613 1.314,-1.243 0,-0.612 -0.45,-1.242 -1.314,-1.242 l -2.952,0 0,-2.737 3.33,0 c 0.865,0 1.279,-0.611 1.279,-1.242 0,-0.613 -0.433,-1.242 -1.279,-1.242 l -4.644,0 C 0.594,-11.253 0,-10.713 0,-9.903 L 0,0 Z"/></g><g transform="translate(27.4512,23.2534)" id="g36"><path id="path38" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,0.9 0.449,1.566 1.404,1.566 l 4.465,0 c 0.863,0 1.277,-0.612 1.277,-1.242 0,-0.612 -0.431,-1.242 -1.277,-1.242 l -3.17,0 0,-2.629 2.953,0 c 0.883,0 1.315,-0.613 1.315,-1.243 0,-0.612 -0.449,-1.242 -1.315,-1.242 l -2.953,0 0,-2.737 3.332,0 c 0.864,0 1.278,-0.611 1.278,-1.242 0,-0.613 -0.432,-1.242 -1.278,-1.242 l -4.646,0 C 0.594,-11.253 0,-10.713 0,-9.903 L 0,0 Z"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" style="enable-background:new 0 0 45 45;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath16" clipPathUnits="userSpaceOnUse"><path id="path18" d="M 0,36 36,36 36,0 0,0 0,36 Z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,45)" id="g10"><g id="g12"><g clip-path="url(#clipPath16)" id="g14"><g transform="translate(36,4)" id="g20"><path id="path22" style="fill:#9266cc;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-2.209 -1.791,-4 -4,-4 l -28,0 c -2.209,0 -4,1.791 -4,4 l 0,28 c 0,2.209 1.791,4 4,4 l 28,0 c 2.209,0 4,-1.791 4,-4 L 0,0 Z"/></g><g transform="translate(5.7173,26.8438)" id="g24"><path id="path26" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 C 0,1.55 0.992,2.418 2.325,2.418 3.658,2.418 4.65,1.55 4.65,0 l 0,-17.611 c 0,-1.551 -0.992,-2.418 -2.325,-2.418 -1.333,0 -2.325,0.867 -2.325,2.418 L 0,0 Z"/></g><g transform="translate(17.8071,11.2793)" id="g28"><path id="path30" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 3.132,0 c 4,0 5.828,2.945 5.828,6.666 0,3.969 -1.859,6.852 -6.138,6.852 L 0,13.518 0,0 Z m -4.65,15.409 c 0,1.427 0.992,2.388 2.387,2.388 l 5.147,0 c 6.946,0 10.914,-4.465 10.914,-11.348 0,-6.511 -4.216,-10.728 -10.604,-10.728 l -5.395,0 c -1.024,0 -2.449,0.558 -2.449,2.325 l 0,17.363 z"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" style="enable-background:new 0 0 45 45;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath16" clipPathUnits="userSpaceOnUse"><path id="path18" d="M 0,36 36,36 36,0 0,0 0,36 Z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,45)" id="g10"><g id="g12"><g clip-path="url(#clipPath16)" id="g14"><g transform="translate(36,4)" id="g20"><path id="path22" style="fill:#3b88c3;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-2.209 -1.791,-4 -4,-4 l -28,0 c -2.209,0 -4,1.791 -4,4 l 0,28 c 0,2.209 1.791,4 4,4 l 28,0 c 2.209,0 4,-1.791 4,-4 L 0,0 Z"/></g><g transform="translate(1.5273,22.8789)" id="g24"><path id="path26" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,0.85 0.544,1.326 1.275,1.326 0.323,0 0.85,-0.255 1.071,-0.561 l 5.388,-7.191 0.034,0 0,6.426 c 0,0.85 0.544,1.326 1.275,1.326 0.731,0 1.275,-0.476 1.275,-1.326 l 0,-9.655 c 0,-0.85 -0.544,-1.325 -1.275,-1.325 -0.323,0 -0.833,0.254 -1.071,0.56 l -5.389,7.106 -0.033,0 0,-6.341 c 0,-0.85 -0.544,-1.325 -1.275,-1.325 C 0.544,-10.98 0,-10.505 0,-9.655 L 0,0 Z"/></g><g transform="translate(12.5942,22.624)" id="g28"><path id="path30" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,0.85 0.425,1.479 1.326,1.479 l 4.215,0 c 0.816,0 1.207,-0.578 1.207,-1.173 0,-0.578 -0.408,-1.173 -1.207,-1.173 l -2.992,0 0,-2.482 2.788,0 c 0.833,0 1.241,-0.578 1.241,-1.172 0,-0.579 -0.425,-1.173 -1.241,-1.173 l -2.788,0 0,-2.584 3.145,0 c 0.816,0 1.207,-0.578 1.207,-1.173 0,-0.578 -0.407,-1.173 -1.207,-1.173 l -4.385,0 C 0.561,-10.624 0,-10.114 0,-9.35 L 0,0 Z"/></g><g transform="translate(19.9043,22.5049)" id="g32"><path id="path34" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c -0.051,0.221 -0.068,0.34 -0.068,0.578 0,0.544 0.459,1.122 1.207,1.122 0.816,0 1.207,-0.476 1.359,-1.224 l 1.445,-7.224 0.035,0 2.209,7.445 C 6.375,1.309 6.885,1.7 7.514,1.7 8.143,1.7 8.652,1.309 8.84,0.697 l 2.209,-7.445 0.033,0 1.445,7.224 C 12.68,1.224 13.072,1.7 13.887,1.7 14.635,1.7 15.094,1.122 15.094,0.578 15.094,0.34 15.078,0.221 15.025,0 l -2.158,-9.281 c -0.17,-0.714 -0.73,-1.325 -1.681,-1.325 -0.834,0 -1.481,0.543 -1.684,1.24 l -1.973,6.561 -0.033,0 -1.972,-6.561 c -0.204,-0.697 -0.85,-1.24 -1.682,-1.24 -0.952,0 -1.514,0.611 -1.684,1.325 L 0,0 Z"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" style="enable-background:new 0 0 45 45;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath16" clipPathUnits="userSpaceOnUse"><path id="path18" d="M 0,36 36,36 36,0 0,0 0,36 Z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,45)" id="g10"><g id="g12"><g clip-path="url(#clipPath16)" id="g14"><g transform="translate(36,4)" id="g20"><path id="path22" style="fill:#3b88c3;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-2.209 -1.791,-4 -4,-4 l -28,0 c -2.209,0 -4,1.791 -4,4 l 0,28 c 0,2.209 1.791,4 4,4 l 28,0 c 2.209,0 4,-1.791 4,-4 L 0,0 Z"/></g><g transform="translate(2.5083,25.3613)" id="g24"><path id="path26" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,1.2 0.768,1.872 1.8,1.872 0.456,0 1.2,-0.36 1.513,-0.792 l 7.608,-10.153 0.048,0 0,9.073 c 0,1.2 0.768,1.872 1.8,1.872 1.032,0 1.8,-0.672 1.8,-1.872 l 0,-13.633 c 0,-1.2 -0.768,-1.872 -1.8,-1.872 -0.456,0 -1.176,0.36 -1.512,0.792 l -7.609,10.033 -0.047,0 0,-8.953 c 0,-1.2 -0.768,-1.872 -1.801,-1.872 -1.032,0 -1.8,0.672 -1.8,1.872 L 0,0 Z"/></g><g transform="translate(31.9102,20.1289)" id="g28"><path id="path30" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 1.512,0 2.111,-0.768 2.111,-2.305 0,-4.632 -3.023,-8.112 -7.824,-8.112 -4.873,0 -8.257,3.864 -8.257,8.833 0,4.992 3.361,8.833 8.257,8.833 3.623,0 6.6,-1.705 6.6,-3.385 0,-1.032 -0.647,-1.68 -1.489,-1.68 -1.63,0 -1.966,1.752 -5.111,1.752 -3,0 -4.513,-2.616 -4.513,-5.52 0,-2.929 1.464,-5.521 4.513,-5.521 1.897,0 4.08,1.056 4.08,3.792 l -2.447,0 c -0.984,0 -1.682,0.697 -1.682,1.681 0,1.008 0.77,1.632 1.682,1.632 L 0,0 Z"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" style="enable-background:new 0 0 45 45;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath16" clipPathUnits="userSpaceOnUse"><path id="path18" d="M 0,36 36,36 36,0 0,0 0,36 Z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,45)" id="g10"><g id="g12"><g clip-path="url(#clipPath16)" id="g14"><g transform="translate(36,4)" id="g20"><path id="path22" style="fill:#3b88c3;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-2.209 -1.791,-4 -4,-4 l -28,0 c -2.209,0 -4,1.791 -4,4 l 0,28 c 0,2.209 1.791,4 4,4 l 28,0 c 2.209,0 4,-1.791 4,-4 L 0,0 Z"/></g><g transform="translate(14.7017,18.5449)" id="g24"><path id="path26" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,2.904 -1.512,5.52 -4.513,5.52 -3,0 -4.512,-2.616 -4.512,-5.52 0,-2.929 1.464,-5.521 4.512,-5.521 C -1.464,-5.521 0,-2.929 0,0 m -12.769,0 c 0,4.992 3.36,8.833 8.256,8.833 4.825,0 8.257,-3.961 8.257,-8.833 0,-4.969 -3.336,-8.833 -8.257,-8.833 -4.872,0 -8.256,3.864 -8.256,8.833"/></g><g transform="translate(19.9316,25.4575)" id="g28"><path id="path30" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,0.984 0.721,1.776 1.801,1.776 1.031,0 1.8,-0.672 1.8,-1.776 l 0,-5.185 5.905,6.289 c 0.264,0.288 0.719,0.672 1.39,0.672 0.913,0 1.778,-0.696 1.778,-1.728 0,-0.624 -0.385,-1.128 -1.176,-1.92 l -4.537,-4.465 5.545,-5.785 c 0.576,-0.576 1.008,-1.103 1.008,-1.824 0,-1.128 -0.889,-1.655 -1.873,-1.655 -0.696,0 -1.151,0.407 -1.825,1.128 l -6.215,6.721 0,-6.122 c 0,-0.935 -0.72,-1.727 -1.8,-1.727 -1.032,0 -1.801,0.672 -1.801,1.727 L 0,0 Z"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" style="enable-background:new 0 0 45 45;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath16" clipPathUnits="userSpaceOnUse"><path id="path18" d="M 0,36 36,36 36,0 0,0 0,36 Z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,45)" id="g10"><g id="g12"><g clip-path="url(#clipPath16)" id="g14"><g transform="translate(36,4)" id="g20"><path id="path22" style="fill:#dd2e44;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-2.209 -1.791,-4 -4,-4 l -28,0 c -2.209,0 -4,1.791 -4,4 l 0,28 c 0,2.209 1.791,4 4,4 l 28,0 c 2.209,0 4,-1.791 4,-4 L 0,0 Z"/></g><g transform="translate(9.7622,23.4824)" id="g24"><path id="path26" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-0.78 -0.52,-1.48 -1.34,-1.48 -0.82,0 -1.46,0.6 -2.66,0.6 -0.861,0 -1.641,-0.46 -1.641,-1.3 0,-2.06 6.681,-0.74 6.681,-5.901 0,-2.861 -2.36,-4.642 -5.121,-4.642 -1.54,0 -4.861,0.361 -4.861,2.241 0,0.78 0.521,1.42 1.34,1.42 0.941,0 2.061,-0.78 3.361,-0.78 1.321,0 2.041,0.74 2.041,1.721 0,2.36 -6.682,0.939 -6.682,5.58 C -8.882,0.26 -6.581,2 -3.92,2 -2.8,2 0,1.581 0,0"/></g><g transform="translate(21.7627,18.1211)" id="g28"><path id="path30" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,2.421 -1.261,4.602 -3.761,4.602 -2.5,0 -3.76,-2.181 -3.76,-4.602 0,-2.44 1.22,-4.601 3.76,-4.601 C -1.22,-4.601 0,-2.44 0,0 m -10.643,0 c 0,4.161 2.801,7.362 6.882,7.362 4.021,0 6.881,-3.3 6.881,-7.362 0,-4.141 -2.78,-7.361 -6.881,-7.361 -4.061,0 -6.882,3.22 -6.882,7.361"/></g><g transform="translate(34.1426,23.4824)" id="g32"><path id="path34" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-0.78 -0.521,-1.48 -1.342,-1.48 -0.82,0 -1.459,0.6 -2.66,0.6 -0.859,0 -1.641,-0.46 -1.641,-1.3 0,-2.06 6.682,-0.74 6.682,-5.901 0,-2.861 -2.359,-4.642 -5.121,-4.642 -1.539,0 -4.861,0.361 -4.861,2.241 0,0.78 0.521,1.42 1.341,1.42 0.94,0 2.061,-0.78 3.36,-0.78 1.32,0 2.041,0.74 2.041,1.721 0,2.36 -6.682,0.939 -6.682,5.58 C -8.883,0.26 -6.582,2 -3.922,2 -2.801,2 0,1.581 0,0"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" style="enable-background:new 0 0 45 45;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath16" clipPathUnits="userSpaceOnUse"><path id="path18" d="M 0,36 36,36 36,0 0,0 0,36 Z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,45)" id="g10"><g id="g12"><g clip-path="url(#clipPath16)" id="g14"><g transform="translate(36,4)" id="g20"><path id="path22" style="fill:#3b88c3;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-2.209 -1.791,-4 -4,-4 l -28,0 c -2.209,0 -4,1.791 -4,4 l 0,28 c 0,2.209 1.791,4 4,4 l 28,0 c 2.209,0 4,-1.791 4,-4 L 0,0 Z"/></g><g transform="translate(2.2812,24.4375)" id="g24"><path id="path26" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 C 0,1.05 0.672,1.638 1.575,1.638 2.478,1.638 3.149,1.05 3.149,0 l 0,-7.328 c 0,-1.932 1.239,-3.464 3.234,-3.464 1.91,0 3.212,1.616 3.212,3.464 l 0,7.328 c 0,1.05 0.672,1.638 1.575,1.638 0.903,0 1.575,-0.588 1.575,-1.638 l 0,-7.496 c 0,-3.527 -2.898,-6.193 -6.362,-6.193 C 2.876,-13.689 0,-11.064 0,-7.496 L 0,0 Z"/></g><g transform="translate(19.9414,18.7266)" id="g28"><path id="path30" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 2.142,0 c 1.323,0 2.163,0.966 2.163,2.225 0,1.26 -0.84,2.226 -2.163,2.226 L 0,4.451 0,0 Z m -3.149,5.585 c 0,0.987 0.587,1.638 1.637,1.638 l 3.717,0 c 3.086,0 5.375,-2.016 5.375,-5.018 0,-3.066 -2.373,-4.977 -5.25,-4.977 l -2.33,0 0,-3.443 c 0,-1.05 -0.672,-1.638 -1.575,-1.638 -0.903,0 -1.574,0.588 -1.574,1.638 l 0,11.8 z"/></g><g transform="translate(29.5176,24.7734)" id="g32"><path id="path34" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 C 0,0.882 0.65,1.428 1.512,1.428 2.352,1.428 3.023,0.861 3.023,0 l 0,-8.084 c 0,-0.86 -0.671,-1.428 -1.511,-1.428 C 0.65,-9.512 0,-8.965 0,-8.084 L 0,0 Z m -0.127,-12.388 c 0,0.904 0.736,1.638 1.639,1.638 0.902,0 1.636,-0.734 1.636,-1.638 0,-0.903 -0.734,-1.637 -1.636,-1.637 -0.903,0 -1.639,0.734 -1.639,1.637"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" style="enable-background:new 0 0 45 45;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath16" clipPathUnits="userSpaceOnUse"><path id="path18" d="M 0,36 36,36 36,0 0,0 0,36 Z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,45)" id="g10"><g id="g12"><g clip-path="url(#clipPath16)" id="g14"><g transform="translate(36,4)" id="g20"><path id="path22" style="fill:#f4900c;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-4 -4,-4 -4,-4 l -28,0 c -4,0 -4,4 -4,4 l 0,28 c 0,4 4,4 4,4 l 28,0 c 0,0 4,0 4,-4 L 0,0 Z"/></g><g transform="translate(1.5005,25.5898)" id="g24"><path id="path26" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c -0.15,0.39 -0.21,0.69 -0.21,1.11 0,1.26 1.109,2.16 2.311,2.16 C 3.12,3.27 3.75,2.61 4.14,1.8 L 8.79,-10.679 13.439,1.8 c 0.391,0.81 1.021,1.47 2.041,1.47 1.2,0 2.309,-0.9 2.309,-2.16 C 17.789,0.69 17.73,0.39 17.58,0 l -6.57,-16.71 c -0.39,-0.959 -0.9,-1.738 -2.22,-1.738 -1.32,0 -1.83,0.779 -2.221,1.738 L 0,0 Z"/></g><g transform="translate(33.1504,26.04)" id="g28"><path id="path30" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-1.17 -0.781,-2.22 -2.01,-2.22 -1.23,0 -2.191,0.9 -3.99,0.9 -1.291,0 -2.461,-0.69 -2.461,-1.95 0,-3.089 10.02,-1.11 10.02,-8.849 0,-4.291 -3.539,-6.961 -7.68,-6.961 -2.309,0 -7.289,0.541 -7.289,3.361 0,1.17 0.779,2.129 2.01,2.129 1.41,0 3.089,-1.17 5.041,-1.17 1.978,0 3.058,1.11 3.058,2.58 0,3.541 -10.019,1.41 -10.019,8.369 C -13.32,0.391 -9.871,3 -5.881,3 -4.201,3 0,2.37 0,0"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 32 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.6 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" style="enable-background:new 0 0 45 45;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath16" clipPathUnits="userSpaceOnUse"><path id="path18" d="M 0,36 36,36 36,0 0,0 0,36 Z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,45)" id="g10"><g id="g12"><g clip-path="url(#clipPath16)" id="g14"><g transform="translate(32,31)" id="g20"><path id="path22" style="fill:#068241;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 -23,0 0,-9 27,0 0,5 C 4,-1.791 2.209,0 0,0"/></g><path id="path24" style="fill:#eeeeee;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 9,14 27,0 0,8 -27,0 0,-8 z"/><g transform="translate(9,5)" id="g26"><path id="path28" style="fill:#141414;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 23,0 c 2.209,0 4,1.791 4,4 L 27,9 0,9 0,0 Z"/></g><g transform="translate(4,31)" id="g30"><path id="path32" style="fill:#ec2028;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c -2.209,0 -4,-1.791 -4,-4 l 0,-18 c 0,-2.209 1.791,-4 4,-4 l 5,0 0,26 -5,0 z"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.9 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" style="enable-background:new 0 0 45 45;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath20" clipPathUnits="userSpaceOnUse"><path id="path22" d="M 0,36 36,36 36,0 0,0 0,36 Z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,45)" id="g10"><g transform="translate(11.0769,15)" id="g12"><path id="path14" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 6.923,-10 13.846,0 0,0 Z"/></g><g id="g16"><g clip-path="url(#clipPath20)" id="g18"><g transform="translate(10.2787,21)" id="g24"><path id="path26" style="fill:#141414;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 -0.105,0.022 3.883,0.849 0.491,3.266 4.468,2.377 2.187,6.017 5.547,3.547 4.726,7.855 6.958,4.179 7.721,8.5 8.485,4.179 10.716,7.855 9.895,3.547 l 3.36,2.47 -2.28,-3.64 3.977,0.889 L 11.559,0.849 15.547,0.022 15.443,0 18.798,0 24.754,8.603 C 24.021,9.457 22.935,10 21.721,10 l -28,0 C -7.493,10 -8.578,9.457 -9.312,8.603 L -3.356,0 0,0 Z"/></g><g transform="translate(25.8261,21.0217)" id="g28"><path id="path30" style="fill:#fcd116;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 -3.988,0.827 3.392,2.418 -3.976,-0.89 2.28,3.64 -3.36,-2.47 0.821,4.308 -2.232,-3.675 -0.763,4.32 -0.763,-4.32 -2.232,3.675 0.821,-4.308 -3.36,2.47 2.28,-3.64 -3.976,0.89 3.392,-2.418 -3.988,-0.827 0.105,-0.022 15.442,0 L 0,0 Z"/></g><g transform="translate(10.2787,21)" id="g32"><path id="path34" style="fill:#0072c6;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 -3.356,0 0.798,-6 14.644,-6 18.798,0 15.443,0 0,0 Z"/></g><g transform="translate(29.0769,21)" id="g36"><path id="path38" style="fill:#ce1126;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 -4.154,-6 -6.923,-10 14,0 c 2.209,0 4,1.791 4,4 l 0,18 c 0,0.995 -0.366,1.903 -0.967,2.603 L 0,0 Z"/></g><g transform="translate(6.9231,21)" id="g40"><path id="path42" style="fill:#ce1126;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 -5.956,8.603 C -6.557,7.903 -6.923,6.995 -6.923,6 l 0,-18 c 0,-2.209 1.791,-4 4,-4 l 14,0 L 4.154,-6 0,0 Z"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" style="enable-background:new 0 0 45 45;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath16" clipPathUnits="userSpaceOnUse"><path id="path18" d="M 0,36 36,36 36,0 0,0 0,36 Z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,45)" id="g10"><g id="g12"><g clip-path="url(#clipPath16)" id="g14"><g transform="translate(32,31)" id="g20"><path id="path22" style="fill:#d90012;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 -28,0 c -2.209,0 -4,-1.791 -4,-4 l 0,-4 36,0 0,4 C 4,-1.791 2.209,0 0,0"/></g><g transform="translate(4,5)" id="g24"><path id="path26" style="fill:#f2a800;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 28,0 c 2.209,0 4,1.791 4,4 L 32,8 -4,8 -4,4 C -4,1.791 -2.209,0 0,0"/></g><path id="path28" style="fill:#0033a0;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,13 36,13 36,23 0,23 0,13 Z"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" style="enable-background:new 0 0 45 45;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath16" clipPathUnits="userSpaceOnUse"><path id="path18" d="M 0,36 36,36 36,0 0,0 0,36 Z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,45)" id="g10"><g id="g12"><g clip-path="url(#clipPath16)" id="g14"><g transform="translate(0,18)" id="g20"><path id="path22" style="fill:#141414;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 0,-9 c 0,-2.209 1.791,-4 4,-4 l 28,0 c 2.209,0 4,1.791 4,4 L 36,0 0,0 Z"/></g><g transform="translate(36,18)" id="g24"><path id="path26" style="fill:#ce1b26;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 0,9 c 0,2.209 -1.791,4 -4,4 l -28,0 c -2.209,0 -4,-1.791 -4,-4 l 0,-9 36,0 z"/></g><g transform="translate(17.4517,22.3545)" id="g28"><path id="path30" style="fill:#f9d616;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 0.602,-1.222 1.951,-1.418 0.975,-2.368 1.206,-3.711 0,-3.077 l -1.206,-0.634 0.23,1.343 -0.975,0.95 1.348,0.196 L 0,0 Z"/></g><g transform="translate(15.1563,18.8125)" id="g32"><path id="path34" style="fill:#f9d616;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0.344,-0.562 3.172,-3.516 5.922,-5.234 0.359,-0.235 1.344,-0.985 1.719,-1.25 -0.157,-0.203 -0.375,-0.5 -0.61,-0.75 -0.39,0.312 -3.968,2.515 -5.14,3.109 C 0.719,-3.531 -0.344,-2.5 -0.344,-1.516 -0.344,-0.531 0,0 0,0"/></g><g transform="translate(22.9844,12.2031)" id="g36"><path id="path38" style="fill:#f9d616;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 C -0.172,-0.25 -0.437,-0.594 -0.594,-0.781 -0.281,-0.828 0.516,-1.219 0.922,-2.063 1.328,-2.906 2.063,-2.469 2,-2.016 1.937,-1.562 1.047,-0.578 0,0"/></g><g transform="translate(23.4375,11.5078)" id="g40"><path id="path42" style="fill:#292f33;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-0.1 -0.08,-0.18 -0.18,-0.18 -0.099,0 -0.179,0.08 -0.179,0.18 0,0.1 0.08,0.18 0.179,0.18 C -0.08,0.18 0,0.1 0,0"/></g><g transform="translate(24.4375,10.5078)" id="g44"><path id="path46" style="fill:#292f33;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-0.1 -0.08,-0.18 -0.18,-0.18 -0.099,0 -0.179,0.08 -0.179,0.18 0,0.1 0.08,0.18 0.179,0.18 C -0.08,0.18 0,0.1 0,0"/></g><g transform="translate(17.8906,15.2812)" id="g48"><path id="path50" style="fill:none;stroke:#292f33;stroke-width:0.30000001;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1" d="M 0,0 4.656,-3.172"/></g><g transform="translate(24.8262,17.3521)" id="g52"><path id="path54" style="fill:#f9d616;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 0.008,1.295 -1.077,1.288 c -0.041,0.417 -0.126,0.82 -0.244,1.209 L -0.35,2.886 -0.787,4.105 -1.833,3.69 c -0.178,0.322 -0.384,0.625 -0.614,0.909 l 0.699,0.798 -0.994,0.832 -0.625,-0.71 c -0.31,0.25 -0.641,0.472 -0.994,0.66 L -3.907,7.146 -5.12,7.603 -5.562,6.668 C -5.919,6.771 -6.288,6.844 -6.667,6.882 L -6.805,5.535 c 2.495,-0.257 4.448,-2.341 4.448,-4.903 0,-1.508 -0.688,-2.842 -1.751,-3.751 l 0.552,-0.382 1.365,-1.015 0.532,0.578 -0.833,0.618 c 0.252,0.303 0.476,0.627 0.668,0.974 l 1.006,-0.408 0.5,1.195 -1.001,0.406 c 0.112,0.369 0.196,0.751 0.238,1.146 L 0,0 Z"/></g><g transform="translate(19.8965,12.2324)" id="g56"><path id="path58" style="fill:#f9d616;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 -0.412,0.799 -0.51,0.243 C -1.386,0.898 -1.87,0.799 -2.381,0.799 c -1.198,0 -2.282,0.442 -3.139,1.15 L -6.352,0.971 c 0.308,-0.255 0.645,-0.473 0.999,-0.665 l -0.446,-0.959 1.195,-0.503 0.45,0.971 c 0.345,-0.103 0.701,-0.175 1.069,-0.218 l -0.008,-1.01 1.296,0.014 0.007,0.96 c 0.404,0.039 0.797,0.115 1.175,0.226 l 0.344,-0.994 0.687,0.203 -0.431,0.999 C -0.01,-0.003 -0.005,-0.002 0,0"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" style="enable-background:new 0 0 45 45;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath16" clipPathUnits="userSpaceOnUse"><path id="path18" d="M 0,36 36,36 36,0 0,0 0,36 Z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,45)" id="g10"><g id="g12"><g clip-path="url(#clipPath16)" id="g14"><g transform="translate(36,9)" id="g20"><path id="path22" style="fill:#265fb5;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-2.209 -1.791,-4 -4,-4 l -28,0 c -2.209,0 -4,1.791 -4,4 l 0,18 c 0,2.209 1.791,4 4,4 l 28,0 c 2.209,0 4,-1.791 4,-4 L 0,0 Z"/></g><g transform="translate(27.5322,17.0674)" id="g24"><path id="path26" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 C -0.051,0.525 0.801,1.828 0.117,1.734 -0.72,1.62 -1.374,2.196 -1.657,2.183 -0.751,3.464 -1.424,3.79 -1.626,4.62 -1.982,6.078 -0.845,6.792 -2.876,7.105 -4.197,7.308 -5.108,7.17 -5.506,8.679 -6.089,8.27 -6.781,8.168 -7.437,8.455 c -0.42,0.183 -0.551,0.532 -0.947,0.701 -0.299,0.127 -0.925,0.126 -1.26,0.179 -0.923,0.146 -1.399,-0.264 -2.227,-0.127 0.079,-0.121 0.091,-0.275 0.146,-0.403 -0.51,-0.018 -0.821,-0.36 -0.876,-0.837 -0.747,0.075 -0.937,-0.898 -0.853,-1.512 -0.026,-0.007 -0.052,-0.016 -0.078,-0.023 l 0.031,-0.032 c -0.157,-1.625 -0.818,-2.438 -2.483,-2.693 -1.096,-0.168 -2.07,0.561 -3.017,1.147 -0.207,0.128 -0.571,0.408 -0.766,0.625 -0.28,0.31 -0.478,0.747 -0.75,0.968 -0.125,0.102 -0.39,0.188 -0.354,-0.02 -0.172,-1.078 0.616,-2.421 1.522,-2.94 -1.242,-0.573 0.315,-0.916 0.538,-1.111 0.004,-0.004 0.539,-0.74 0.543,-0.767 0.085,-0.526 -0.277,-0.466 -0.315,-0.887 -0.04,-0.436 -0.039,-0.787 0.107,-1.222 -0.011,-0.01 -0.021,-0.021 -0.031,-0.031 0.006,-0.35 -0.26,-0.225 -0.603,-0.147 0.047,-1.062 1.058,-1.154 1.228,-1.362 0.545,-0.669 0.357,-1.642 0.992,-2.265 1.564,-1.532 3.347,-0.628 5.117,-0.884 0.994,-0.145 1.846,-0.979 2.747,-0.038 1.059,-1.16 -0.815,-2.535 -0.357,-2.926 0.131,-0.113 0.269,-0.159 0.41,-0.167 -0.026,-0.072 -0.067,-0.136 -0.086,-0.211 1.273,-0.12 2.613,-0.424 3.802,0.202 -0.002,-0.191 0.126,-0.423 0.133,-0.525 0.292,0.349 0.52,0.33 0.892,0.515 0.465,0.233 1.286,0.511 1.594,0.976 0.368,0.553 -0.21,1.319 0.949,1.082 0.089,0.4 0.127,0.358 0.339,0.624 -0.319,0.8 0.629,1.34 0.914,1.912 0.057,0.116 0.062,0.652 0.137,0.854 0.145,0.385 0.556,0.599 0.67,1.081 C 0.581,-0.922 0.074,-0.769 0,0"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" style="enable-background:new 0 0 45 45;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath16" clipPathUnits="userSpaceOnUse"><path id="path18" d="M 0,36 36,36 36,0 0,0 0,36 Z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,45)" id="g10"><g id="g12"><g clip-path="url(#clipPath16)" id="g14"><g transform="translate(36,9)" id="g20"><path id="path22" style="fill:#75aadb;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-2.209 -1.791,-4 -4,-4 l -28,0 c -2.209,0 -4,1.791 -4,4 l 0,18 c 0,2.209 1.791,4 4,4 l 28,0 c 2.209,0 4,-1.791 4,-4 L 0,0 Z"/></g><path id="path24" style="fill:#eeeeee;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 36,13 0,13 0,23 36,23 36,13 Z"/><g transform="translate(17.5771,15.874)" id="g26"><path id="path28" style="fill:#fcbf49;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 -1.236,-1.879 0.455,2.203 -1.862,-1.263 1.264,1.861 -2.203,-0.455 1.878,1.236 -2.209,0.423 2.21,0.423 L -3.582,3.785 -1.379,3.33 -2.643,5.191 -0.781,3.928 -1.236,6.131 0,4.252 0.423,6.461 0.846,4.253 2.082,6.131 1.627,3.928 3.488,5.191 2.225,3.33 4.428,3.785 2.549,2.549 4.758,2.126 2.549,1.703 4.428,0.467 2.225,0.922 3.487,-0.938 1.627,0.324 2.082,-1.879 0.846,0 0.423,-2.209 0,0 Z"/></g><g transform="translate(22.6191,20)" id="g30"><path id="path32" style="fill:#843511;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 0.005,0 0,0 Z m -1.084,2 0.005,0 -0.005,0 z M -2.705,3 -2.7,3 -2.705,3 Z m -1.914,0 -0.488,-2.548 -1.425,2.167 0.524,-2.54 -2.147,1.457 1.457,-2.147 -2.54,0.524 2.167,-1.425 -2.548,-0.488 2.548,-0.488 -2.167,-1.426 2.54,0.525 -1.457,-2.146 2.147,1.457 -0.524,-2.541 1.425,2.167 0.488,-2.548 0.488,2.548 1.426,-2.167 -0.525,2.541 2.146,-1.457 -1.457,2.146 L 0,-3.914 -2.167,-2.488 0.381,-2 -2.167,-1.512 0,-0.087 -2.541,-0.611 -1.084,1.536 -3.23,0.079 -2.705,2.619 -4.131,0.452 -4.619,3 Z m 0,-1.33 0.242,-1.265 0.116,-0.605 0.339,0.515 0.707,1.076 -0.26,-1.262 -0.125,-0.604 0.51,0.347 1.066,0.723 -0.724,-1.066 -0.346,-0.509 0.604,0.124 1.261,0.26 -1.076,-0.707 -0.514,-0.339 0.605,-0.116 1.266,-0.242 -1.266,-0.242 -0.604,-0.116 0.513,-0.339 1.076,-0.707 -1.261,0.26 -0.604,0.125 0.346,-0.51 0.724,-1.066 -1.066,0.724 -0.51,0.346 0.125,-0.604 0.26,-1.262 -0.707,1.077 -0.339,0.513 -0.116,-0.604 -0.242,-1.266 -0.242,1.266 -0.116,0.605 -0.339,-0.514 -0.707,-1.077 0.26,1.262 0.124,0.604 -0.509,-0.346 -1.066,-0.724 0.723,1.066 0.346,0.51 -0.603,-0.125 -1.262,-0.26 1.076,0.707 0.515,0.339 -0.605,0.116 -1.265,0.242 1.265,0.242 0.605,0.116 -0.515,0.339 -1.076,0.707 1.262,-0.26 0.603,-0.124 -0.346,0.509 -0.723,1.066 1.066,-0.723 0.509,-0.346 -0.124,0.603 -0.26,1.262 0.707,-1.076 0.339,-0.515 0.116,0.605 0.242,1.265 z"/></g><g transform="translate(16,18)" id="g34"><path id="path36" style="fill:#fcbf49;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 C 0,1.105 0.896,2 2,2 3.105,2 4,1.105 4,0 4,-1.104 3.105,-2 2,-2 0.896,-2 0,-1.104 0,0"/></g><g transform="translate(16,18)" id="g38"><path id="path40" style="fill:none;stroke:#843511;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1" d="M 0,0 C 0,1.105 0.896,2 2,2 3.105,2 4,1.105 4,0 4,-1.104 3.105,-2 2,-2 0.896,-2 0,-1.104 0,0 Z"/></g><g transform="translate(17.8013,18.2261)" id="g42"><path id="path44" style="fill:#c16540;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-0.155 -0.261,-0.28 -0.583,-0.28 -0.323,0 -0.584,0.125 -0.584,0.28 0,0.155 0.261,0.28 0.584,0.28 C -0.261,0.28 0,0.155 0,0"/></g><g transform="translate(19.3545,18.25)" id="g46"><path id="path48" style="fill:#c16540;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,0.161 -0.266,0.292 -0.594,0.292 -0.328,0 -0.593,-0.131 -0.593,-0.292 0,-0.161 0.265,-0.292 0.593,-0.292 C -0.266,-0.292 0,-0.161 0,0"/></g><g transform="translate(17.4629,17.126)" id="g50"><path id="path52" style="fill:#ed8662;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 C 0,0.126 0.246,0.229 0.548,0.229 0.851,0.229 1.097,0.126 1.097,0 1.097,-0.126 0.851,-0.229 0.548,-0.229 0.246,-0.229 0,-0.126 0,0"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.1 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" style="enable-background:new 0 0 45 45;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath18" clipPathUnits="userSpaceOnUse"><path id="path20" d="M 0,36 36,36 36,0 0,0 0,36 Z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,45)" id="g10"><path id="path12" style="fill:#eeeeee;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,13 36,0 0,10.001 -36,0 L 0,13 Z"/><g id="g14"><g clip-path="url(#clipPath18)" id="g16"><g transform="translate(32,31)" id="g22"><path id="path24" style="fill:#ed2939;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 -28,0 c -2.209,0 -4,-1.791 -4,-4 l 0,-4 36,0 0,4 C 4,-1.791 2.209,0 0,0"/></g><g transform="translate(4,5)" id="g26"><path id="path28" style="fill:#ed2939;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 28,0 c 2.209,0 4,1.791 4,4 L 32,8 -4,8 -4,4 C -4,1.791 -2.209,0 0,0"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.3 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45 45" style="enable-background:new 0 0 45 45;" xml:space="preserve" version="1.1" id="svg2"><metadata id="metadata8"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/></cc:Work></rdf:RDF></metadata><defs id="defs6"><clipPath id="clipPath16" clipPathUnits="userSpaceOnUse"><path id="path18" d="M 0,36 36,36 36,0 0,0 0,36 Z"/></clipPath></defs><g transform="matrix(1.25,0,0,-1.25,0,45)" id="g10"><g id="g12"><g clip-path="url(#clipPath16)" id="g14"><g transform="translate(6.2761,24.7239)" id="g20"><path id="path22" style="fill:#4189dd;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 -0.943,-3.057 -1.886,0 -4.943,0.943 -1.886,1.886 -0.943,4.943 0,1.886 3.057,0.943 0,0 Z m 25.724,6.276 -28,0 c -2.209,0 -4,-1.791 -4,-4 l 0,-13.055 36,0 0,13.055 c 0,2.209 -1.791,4 -4,4"/></g><g transform="translate(4,5)" id="g24"><path id="path26" style="fill:#4189dd;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 28,0 c 1.872,0 3.431,1.291 3.867,3.028 l -35.734,0 C -3.431,1.291 -1.872,0 0,0"/></g><path id="path28" style="fill:#4189dd;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,10 36,0 0,1.972 -36,0 L 0,10 Z"/><g transform="translate(6.0596,24.9404)" id="g30"><path id="path32" style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 -0.726,-2.355 -1.453,0 -3.808,0.726 -1.453,1.453 -0.726,3.808 0,1.453 2.355,0.726 0,0 Z m -0.726,4.726 -0.943,-3.057 -3.057,-0.943 3.057,-0.942 0.943,-3.058 0.942,3.058 3.058,0.942 -3.058,0.943 -0.942,3.057 z"/></g><g transform="translate(5.3333,28.7482)" id="g34"><path id="path36" style="fill:#d21034;fill-opacity:1;fill-rule:nonzero;stroke:none" d="M 0,0 -0.726,-2.355 -3.081,-3.081 -0.726,-3.808 0,-6.163 0.726,-3.808 3.082,-3.081 0.726,-2.355 0,0 Z"/></g><path id="path38" style="fill:#f9d616;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,11.972 36,0 0,1.972 -36,0 0,-1.972 z"/><g transform="translate(0,9)" id="g40"><path id="path42" style="fill:#f9d616;fill-opacity:1;fill-rule:nonzero;stroke:none" d="m 0,0 c 0,-0.337 0.054,-0.659 0.133,-0.972 l 35.734,0 C 35.946,-0.659 36,-0.337 36,0 L 36,1 0,1 0,0 Z"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

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