Merge branch 'develop' into feature/activitypub
24
.gitlab-ci.yml
Normal 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
|
64
README.md
|
@ -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.
|
||||
|
||||
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
|
||||
* Tusky
|
||||
* Pawoo (Android)
|
||||
* Pawoo (Android + iOS)
|
||||
* 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
|
||||
|
||||
### Dependencies
|
||||
|
||||
* 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
|
||||
|
||||
#### Installing dependencies on Debian system
|
||||
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.
|
||||
### Configuration
|
||||
|
||||
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`
|
||||
* 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"
|
||||
```
|
||||
* 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.
|
||||
|
||||
* 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.
|
||||
|
|
|
@ -49,5 +49,5 @@
|
|||
try do
|
||||
import_config "dev.secret.exs"
|
||||
rescue
|
||||
_-> nil
|
||||
_-> IO.puts("!!! RUNNING IN LOCALHOST DEV MODE! !!!\nFEDERATION WON'T WORK UNTIL YOU CONFIGURE A dev.secret.exs")
|
||||
end
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
username: "postgres",
|
||||
password: "postgres",
|
||||
database: "pleroma_test",
|
||||
hostname: "localhost",
|
||||
hostname: System.get_env("DB_HOST") || "localhost",
|
||||
pool: Ecto.Adapters.SQL.Sandbox
|
||||
|
||||
|
||||
|
|
|
@ -19,6 +19,9 @@ server {
|
|||
server_name example.tld;
|
||||
|
||||
location / {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_pass http://localhost:4000;
|
||||
}
|
||||
include snippets/well-known.conf;
|
||||
|
|
22
lib/mix/tasks/generate_config.ex
Normal 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
|
20
lib/mix/tasks/sample_config.eex
Normal 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
|
8
lib/mix/tasks/sample_psql.eex
Normal 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;
|
44
lib/pleroma/PasswordResetToken.ex
Normal 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
|
|
@ -6,7 +6,8 @@ defmodule Pleroma.Activity do
|
|||
schema "activities" do
|
||||
field :data, :map
|
||||
field :local, :boolean, default: true
|
||||
has_many :notifications, Notification
|
||||
field :actor, :string
|
||||
has_many :notifications, Notification, on_delete: :delete_all
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
@ -16,24 +17,29 @@ def get_by_ap_id(ap_id) do
|
|||
where: fragment("(?)->>'id' = ?", activity.data, ^to_string(ap_id)))
|
||||
end
|
||||
|
||||
# TODO:
|
||||
# Go through these and fix them everywhere.
|
||||
# Wrong name, only returns create activities
|
||||
def all_by_object_ap_id_q(ap_id) do
|
||||
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
|
||||
|
||||
# Wrong name, returns all.
|
||||
def all_non_create_by_object_ap_id_q(ap_id) do
|
||||
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
|
||||
|
||||
# Wrong name plz fix thx
|
||||
def all_by_object_ap_id(ap_id) do
|
||||
Repo.all(all_by_object_ap_id_q(ap_id))
|
||||
end
|
||||
|
||||
def get_create_activity_by_object_ap_id(ap_id) do
|
||||
Repo.one(from activity in Activity,
|
||||
where: fragment("(?)->'object'->>'id' = ?", activity.data, ^to_string(ap_id))
|
||||
and fragment("(?)->>'type' = 'Create'", activity.data))
|
||||
where: fragment("coalesce((?)->'object'->>'id', (?)->>'object') = ?", activity.data, activity.data, ^to_string(ap_id)),
|
||||
where: fragment("(?)->>'type' = 'Create'", activity.data))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,8 +19,10 @@ def start(_type, _args) do
|
|||
ttl_interval: 1000,
|
||||
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
|
||||
# for other strategies and supported options
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
defmodule Pleroma.Formatter do
|
||||
alias Pleroma.User
|
||||
|
||||
@link_regex ~r/https?:\/\/[\w\.\/?=\-#%&]+[\w]/u
|
||||
@link_regex ~r/https?:\/\/[\w\.\/?=\-#%&@~\(\)]+[\w\/]/u
|
||||
def linkify(text) do
|
||||
Regex.replace(@link_regex, text, "<a href='\\0'>\\0</a>")
|
||||
end
|
||||
|
||||
@tag_regex ~r/\#\w+/u
|
||||
def parse_tags(text) do
|
||||
def parse_tags(text, data \\ %{}) do
|
||||
Regex.scan(@tag_regex, text)
|
||||
|> 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
|
||||
|
||||
def parse_mentions(text) do
|
||||
|
@ -23,6 +24,15 @@ def parse_mentions(text) do
|
|||
|> Enum.filter(fn ({_match, user}) -> user 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 [
|
||||
"a_trusted_friend",
|
||||
"alandislands",
|
||||
|
@ -122,4 +132,8 @@ def emojify(text, additional \\ nil) do
|
|||
def get_emoji(text) do
|
||||
Enum.filter(@emoji, fn ({emoji, _}) -> String.contains?(text, ":#{emoji}:") end)
|
||||
end
|
||||
|
||||
def get_custom_emoji() do
|
||||
@emoji
|
||||
end
|
||||
end
|
||||
|
|
|
@ -36,7 +36,38 @@ def for_user(user, opts \\ %{}) do
|
|||
Repo.all(query)
|
||||
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)
|
||||
|
||||
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.
|
||||
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)
|
||||
Pleroma.Web.Streamer.stream("user", notification)
|
||||
notification
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -15,15 +15,16 @@ def create(data) do
|
|||
end
|
||||
|
||||
def change(struct, params \\ %{}) do
|
||||
changeset = struct
|
||||
struct
|
||||
|> cast(params, [:data])
|
||||
|> validate_required([:data])
|
||||
|> unique_constraint(:ap_id, name: :objects_unique_apid_index)
|
||||
end
|
||||
|
||||
def get_by_ap_id(nil), do: nil
|
||||
def get_by_ap_id(ap_id) do
|
||||
Repo.one(from object in Object,
|
||||
where: fragment("? @> ?", object.data, ^%{id: ap_id}))
|
||||
where: fragment("(?)->>'id' = ?", object.data, ^ap_id))
|
||||
end
|
||||
|
||||
def get_cached_by_ap_id(ap_id) do
|
||||
|
|
|
@ -12,6 +12,7 @@ def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
|
|||
def call(conn, opts) do
|
||||
with {:ok, username, password} <- decode_header(conn),
|
||||
{:ok, user} <- opts[:fetcher].(username),
|
||||
false <- !!user.info["deactivated"],
|
||||
saved_user_id <- get_session(conn, :user_id),
|
||||
{:ok, verified_user} <- verify(user, password, saved_user_id)
|
||||
do
|
||||
|
@ -44,7 +45,7 @@ defp verify(user, password, _user_id) do
|
|||
defp decode_header(conn) do
|
||||
with ["Basic " <> header] <- get_req_header(conn, "authorization"),
|
||||
{:ok, userinfo} <- Base.decode64(header),
|
||||
[username, password] <- String.split(userinfo, ":")
|
||||
[username, password] <- String.split(userinfo, ":", parts: 2)
|
||||
do
|
||||
{:ok, username, password}
|
||||
end
|
||||
|
|
|
@ -9,10 +9,15 @@ def init(options) do
|
|||
end
|
||||
|
||||
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
|
||||
def call(conn, opts) do
|
||||
with ["Bearer " <> header] <- get_req_header(conn, "authorization"),
|
||||
%Token{user_id: user_id} <- Repo.get_by(Token, token: header),
|
||||
%User{} = user <- Repo.get(User, user_id) do
|
||||
def call(conn, _) do
|
||||
token = case get_req_header(conn, "authorization") do
|
||||
["Bearer " <> header] -> header
|
||||
_ -> 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
|
||||
|> assign(:user, user)
|
||||
else
|
||||
|
|
|
@ -8,11 +8,18 @@ def store(%Plug.Upload{} = file) do
|
|||
result_file = Path.join(upload_folder, file.filename)
|
||||
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",
|
||||
"url" => [%{
|
||||
"type" => "Link",
|
||||
"mediaType" => file.content_type,
|
||||
"mediaType" => content_type,
|
||||
"href" => url_for(Path.join(uuid, :cow_uri.urlencode(file.filename)))
|
||||
}],
|
||||
"name" => file.filename,
|
||||
|
@ -53,4 +60,34 @@ defp upload_path do
|
|||
defp url_for(file) do
|
||||
"#{Web.base_url()}/media/#{file}"
|
||||
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
|
||||
|
|
|
@ -5,8 +5,7 @@ defmodule Pleroma.User do
|
|||
alias Pleroma.{Repo, User, Object, Web, Activity, Notification}
|
||||
alias Comeonin.Pbkdf2
|
||||
alias Pleroma.Web.{OStatus, Websub}
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.ActivityPub.{Utils, ActivityPub}
|
||||
|
||||
schema "users" do
|
||||
field :bio, :string
|
||||
|
@ -62,8 +61,9 @@ def info_changeset(struct, params \\ %{}) do
|
|||
end
|
||||
|
||||
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,
|
||||
follower_count: user.info["follower_count"] || 0
|
||||
}
|
||||
|
@ -89,7 +89,7 @@ def remote_user_creation(params) do
|
|||
end
|
||||
|
||||
def update_changeset(struct, params \\ %{}) do
|
||||
changeset = struct
|
||||
struct
|
||||
|> cast(params, [:bio, :name])
|
||||
|> unique_constraint(:nickname)
|
||||
|> 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)
|
||||
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
|
||||
changeset = struct
|
||||
|> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
|
||||
|
@ -123,9 +142,9 @@ def register_changeset(struct, params \\ %{}) do
|
|||
end
|
||||
end
|
||||
|
||||
def follow(%User{} = follower, %User{} = followed) do
|
||||
def follow(%User{} = follower, %User{info: info} = followed) do
|
||||
ap_followers = followed.follower_address
|
||||
if following?(follower, followed) do
|
||||
if following?(follower, followed) or info["deactivated"] do
|
||||
{:error,
|
||||
"Could not follow user: #{followed.nickname} is already on your list."}
|
||||
else
|
||||
|
@ -138,9 +157,9 @@ def follow(%User{} = follower, %User{} = followed) do
|
|||
|
||||
follower = follower
|
||||
|> follow_changeset(%{following: following})
|
||||
|> Repo.update
|
||||
|> update_and_set_cache
|
||||
|
||||
{:ok, followed} = update_follower_count(followed)
|
||||
{:ok, _} = update_follower_count(followed)
|
||||
|
||||
follower
|
||||
end
|
||||
|
@ -148,13 +167,13 @@ def follow(%User{} = follower, %User{} = followed) do
|
|||
|
||||
def unfollow(%User{} = follower, %User{} = followed) do
|
||||
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
|
||||
|> List.delete(ap_followers)
|
||||
|
||||
{ :ok, follower } = follower
|
||||
|> follow_changeset(%{following: following})
|
||||
|> Repo.update
|
||||
|> update_and_set_cache
|
||||
|
||||
{: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)
|
||||
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
|
||||
key = "ap_id:#{ap_id}"
|
||||
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
|
||||
user
|
||||
else _e ->
|
||||
with [nick, domain] <- String.split(nickname, "@"),
|
||||
with [_nick, _domain] <- String.split(nickname, "@"),
|
||||
{:ok, user} <- OStatus.make_user(nickname) do
|
||||
user
|
||||
else _e -> nil
|
||||
|
@ -220,9 +250,18 @@ def get_friends(%User{id: id, following: following}) do
|
|||
{:ok, Repo.all(q)}
|
||||
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
|
||||
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)
|
||||
|
||||
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})
|
||||
|
||||
Repo.update(cs)
|
||||
update_and_set_cache(cs)
|
||||
end
|
||||
|
||||
def update_follower_count(%User{} = user) do
|
||||
follower_count_query = from u in User,
|
||||
where: fragment("? @> ?", u.following, ^user.follower_address),
|
||||
where: u.id != ^user.id,
|
||||
select: count(u.id)
|
||||
|
||||
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})
|
||||
|
||||
Repo.update(cs)
|
||||
update_and_set_cache(cs)
|
||||
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,
|
||||
where: u.ap_id in ^to,
|
||||
where: u.local == true
|
||||
|
||||
Repo.all(query)
|
||||
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
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
||||
alias Pleroma.{Activity, Repo, Object, Upload, User, Web, Notification}
|
||||
alias Ecto.{Changeset, UUID}
|
||||
alias Pleroma.{Activity, Repo, Object, Upload, User, Notification}
|
||||
import Ecto.Query
|
||||
import Pleroma.Web.ActivityPub.Utils
|
||||
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"]),
|
||||
map <- lazy_put_activity_defaults(map),
|
||||
: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)
|
||||
stream_out(activity)
|
||||
{:ok, activity}
|
||||
else
|
||||
%Activity{} = activity -> {:ok, activity}
|
||||
|
@ -18,6 +18,18 @@ def insert(map, local \\ true) when is_map(map) do
|
|||
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
|
||||
with create_data <- make_create_data(%{to: to, actor: actor, published: published, context: context, object: object}, additional),
|
||||
{:ok, activity} <- insert(create_data, local),
|
||||
|
@ -27,7 +39,7 @@ def create(to, actor, context, object, additional \\ %{}, published \\ nil, loca
|
|||
end
|
||||
|
||||
# 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),
|
||||
like_data <- make_like_data(user, object, activity_id),
|
||||
{:ok, activity} <- insert(like_data, local),
|
||||
|
@ -49,7 +61,7 @@ def unlike(%User{} = actor, %Object{} = object) do
|
|||
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),
|
||||
{:ok, activity} <- insert(announce_data, local),
|
||||
{: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),
|
||||
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 <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_activities_for_context(context) do
|
||||
def fetch_activities_for_context(context, opts \\ %{}) do
|
||||
query = from activity in Activity,
|
||||
where: fragment("?->>'type' = ? and ?->>'context' = ?", activity.data, "Create", activity.data, ^context),
|
||||
order_by: [desc: :id]
|
||||
query = restrict_blocked(query, opts)
|
||||
Repo.all(query)
|
||||
end
|
||||
|
||||
|
@ -137,7 +149,7 @@ defp restrict_max(query, _), do: query
|
|||
|
||||
defp restrict_actor(query, %{"actor_id" => actor_id}) do
|
||||
from activity in query,
|
||||
where: fragment("?->>'actor' = ?", activity.data, ^actor_id)
|
||||
where: activity.actor == ^actor_id
|
||||
end
|
||||
defp restrict_actor(query, _), do: query
|
||||
|
||||
|
@ -156,10 +168,32 @@ defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do
|
|||
end
|
||||
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
|
||||
base_query = from activity in Activity,
|
||||
limit: 20,
|
||||
order_by: [desc: :id]
|
||||
order_by: [fragment("? desc nulls last", activity.id)]
|
||||
|
||||
base_query
|
||||
|> restrict_recipients(recipients)
|
||||
|
@ -170,6 +204,9 @@ def fetch_activities(recipients, opts \\ %{}) do
|
|||
|> restrict_actor(opts)
|
||||
|> restrict_type(opts)
|
||||
|> restrict_favorited_by(opts)
|
||||
|> restrict_recent(opts)
|
||||
|> restrict_blocked(opts)
|
||||
|> restrict_media(opts)
|
||||
|> Repo.all
|
||||
|> Enum.reverse
|
||||
end
|
||||
|
|
|
@ -29,7 +29,12 @@ def generate_id(type) do
|
|||
Enqueues an activity for federation if it's local
|
||||
"""
|
||||
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
|
||||
end
|
||||
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.
|
||||
"""
|
||||
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
|
||||
end
|
||||
end
|
||||
|
@ -88,9 +93,13 @@ def update_object_in_activities(%{data: %{"id" => id}} = object) do
|
|||
@doc """
|
||||
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,
|
||||
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)
|
||||
end
|
||||
|
||||
|
@ -197,7 +206,7 @@ def make_unfollow_data(follower, followed, follow_activity) do
|
|||
def make_create_data(params, additional) do
|
||||
published = params.published || make_date()
|
||||
|
||||
activity = %{
|
||||
%{
|
||||
"type" => "Create",
|
||||
"to" => params.to |> Enum.uniq,
|
||||
"actor" => params.actor.ap_id,
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
defmodule Pleroma.Web.UserSocket do
|
||||
use Phoenix.Socket
|
||||
alias Pleroma.User
|
||||
alias Comeonin.Pbkdf2
|
||||
|
||||
## Channels
|
||||
# channel "room:*", Pleroma.Web.RoomChannel
|
||||
channel "chat:*", Pleroma.Web.ChatChannel
|
||||
|
||||
## Transports
|
||||
transport :websocket, Phoenix.Transports.WebSocket
|
||||
|
@ -19,8 +22,13 @@ defmodule Pleroma.Web.UserSocket do
|
|||
#
|
||||
# See `Phoenix.Token` documentation for examples in
|
||||
# performing token verification on connect.
|
||||
def connect(_params, socket) do
|
||||
{:ok, socket}
|
||||
def connect(%{"token" => token}, socket) do
|
||||
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
|
||||
|
||||
# Socket id's are topics that allow you to identify all sockets for a given user:
|
||||
|
|
46
lib/pleroma/web/chat_channel.ex
Normal 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
|
|
@ -16,7 +16,6 @@ def delete(activity_id, user) do
|
|||
|
||||
def repeat(id_or_ap_id, user) do
|
||||
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
|
||||
ActivityPub.announce(user, object)
|
||||
else
|
||||
|
@ -56,12 +55,14 @@ def post(user, %{"status" => status} = data) do
|
|||
mentions <- Formatter.parse_mentions(status),
|
||||
inReplyTo <- get_replied_to_activity(data["in_reply_to_status_id"]),
|
||||
to <- to_for_user_and_mentions(user, mentions, inReplyTo),
|
||||
tags <- Formatter.parse_tags(status),
|
||||
content_html <- make_content_html(status, mentions, attachments, tags),
|
||||
tags <- Formatter.parse_tags(status, data),
|
||||
content_html <- make_content_html(status, mentions, attachments, tags, data["no_attachment_links"]),
|
||||
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)
|
||||
User.update_note_count(user)
|
||||
User.increase_note_count(user)
|
||||
res
|
||||
end
|
||||
end
|
||||
|
|
|
@ -38,15 +38,19 @@ def to_for_user_and_mentions(user, mentions, inReplyTo) do
|
|||
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
|
||||
|> format_input(mentions, tags)
|
||||
|> add_attachments(attachments)
|
||||
|> maybe_add_attachments(attachments, no_attachment_links)
|
||||
end
|
||||
|
||||
def make_context(%Activity{data: %{"context" => context}}), do: context
|
||||
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
|
||||
attachment_text = Enum.map(attachments, fn
|
||||
(%{"url" => [%{"href" => href} | _]}) ->
|
||||
|
@ -54,15 +58,16 @@ def add_attachments(text, attachments) do
|
|||
"<a href=\"#{href}\" class='attachment'>#{shortname(name)}</a>"
|
||||
_ -> ""
|
||||
end)
|
||||
Enum.join([text | attachment_text], "<br>\n")
|
||||
Enum.join([text | attachment_text], "<br>")
|
||||
end
|
||||
|
||||
def format_input(text, mentions, tags) do
|
||||
HtmlSanitizeEx.strip_tags(text)
|
||||
def format_input(text, mentions, _tags) do
|
||||
text
|
||||
|> Formatter.html_escape
|
||||
|> Formatter.linkify
|
||||
|> String.replace("\n", "<br>\n")
|
||||
|> String.replace("\n", "<br>")
|
||||
|> add_user_links(mentions)
|
||||
|> add_tag_links(tags)
|
||||
# |> add_tag_links(tags)
|
||||
end
|
||||
|
||||
def add_tag_links(text, tags) do
|
||||
|
@ -94,11 +99,12 @@ def add_user_links(text, mentions) do
|
|||
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 = %{
|
||||
"type" => "Note",
|
||||
"to" => to,
|
||||
"content" => content_html,
|
||||
"summary" => cw,
|
||||
"context" => context,
|
||||
"attachment" => attachments,
|
||||
"actor" => actor,
|
||||
|
|
|
@ -2,6 +2,7 @@ defmodule Pleroma.Web.Endpoint do
|
|||
use Phoenix.Endpoint, otp_app: :pleroma
|
||||
|
||||
socket "/socket", Pleroma.Web.UserSocket
|
||||
socket "/api/v1", Pleroma.Web.MastodonAPI.MastodonSocket
|
||||
|
||||
# Serve at "/" the static files from "priv/static" directory.
|
||||
#
|
||||
|
@ -11,7 +12,7 @@ defmodule Pleroma.Web.Endpoint do
|
|||
at: "/media", from: "uploads", gzip: false
|
||||
plug Plug.Static,
|
||||
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_reloader configuration of your endpoint.
|
||||
|
|
|
@ -14,7 +14,10 @@ def start_link do
|
|||
Process.sleep(1000 * 60 * 1) # 1 minute
|
||||
enqueue(:refresh_subscriptions, nil)
|
||||
end)
|
||||
GenServer.start_link(__MODULE__, {:sets.new(), :queue.new()}, name: __MODULE__)
|
||||
GenServer.start_link(__MODULE__, %{
|
||||
in: {:sets.new(), []},
|
||||
out: {:sets.new(), []}
|
||||
}, name: __MODULE__)
|
||||
end
|
||||
|
||||
def handle(:refresh_subscriptions, _) do
|
||||
|
@ -71,22 +74,22 @@ def handle(:publish_single_websub, %{xml: xml, topic: topic, callback: callback,
|
|||
end
|
||||
end
|
||||
|
||||
def handle(type, payload) do
|
||||
def handle(type, _) do
|
||||
Logger.debug(fn -> "Unknown task: #{type}" end)
|
||||
{:error, "Don't know what do do with this"}
|
||||
end
|
||||
|
||||
def enqueue(type, payload) do
|
||||
def enqueue(type, payload, priority \\ 1) do
|
||||
if Mix.env == :test do
|
||||
handle(type, payload)
|
||||
else
|
||||
GenServer.cast(__MODULE__, {:enqueue, type, payload})
|
||||
GenServer.cast(__MODULE__, {:enqueue, type, payload, priority})
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_start_job(running_jobs, queue) do
|
||||
if (:sets.size(running_jobs) < @max_jobs) && !:queue.is_empty(queue) do
|
||||
{{:value, {type, payload}}, queue} = :queue.out(queue)
|
||||
if (:sets.size(running_jobs) < @max_jobs) && queue != [] do
|
||||
{{type, payload}, queue} = queue_pop(queue)
|
||||
{:ok, pid} = Task.start(fn -> handle(type, payload) end)
|
||||
mref = Process.monitor(pid)
|
||||
{:sets.add_element(mref, running_jobs), queue}
|
||||
|
@ -95,20 +98,41 @@ def maybe_start_job(running_jobs, queue) do
|
|||
end
|
||||
end
|
||||
|
||||
def handle_cast({:enqueue, type, payload}, {running_jobs, queue}) do
|
||||
queue = :queue.in({type, payload}, queue)
|
||||
{running_jobs, queue} = maybe_start_job(running_jobs, queue)
|
||||
{:noreply, {running_jobs, queue}}
|
||||
def handle_cast({:enqueue, type, payload, priority}, state) when type in [:incoming_doc] do
|
||||
%{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state
|
||||
i_queue = enqueue_sorted(i_queue, {type, payload}, 1)
|
||||
{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
|
||||
|
||||
def handle_info({:DOWN, ref, :process, _pid, _reason}, {running_jobs, queue}) do
|
||||
running_jobs = :sets.del_element(ref, running_jobs)
|
||||
{running_jobs, queue} = maybe_start_job(running_jobs, queue)
|
||||
{:noreply, {running_jobs, queue}}
|
||||
def handle_cast({:enqueue, type, payload, priority}, state) do
|
||||
%{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state
|
||||
o_queue = enqueue_sorted(o_queue, {type, payload}, 1)
|
||||
{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 handle_cast(m, state) do
|
||||
IO.inspect("Unknown: #{inspect(m)}, #{inspect(state)}")
|
||||
{:noreply, state}
|
||||
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
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
||||
use Pleroma.Web, :controller
|
||||
alias Pleroma.{Repo, Activity, User, Notification}
|
||||
alias Pleroma.Web.OAuth.App
|
||||
alias Pleroma.Web
|
||||
alias Pleroma.Web.MastodonAPI.{StatusView, AccountView}
|
||||
alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView}
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.TwitterAPI.TwitterAPI
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.{CommonAPI, OStatus}
|
||||
alias Pleroma.Web.OAuth.{Authorization, Token, App}
|
||||
alias Comeonin.Pbkdf2
|
||||
import Ecto.Query
|
||||
import Logger
|
||||
require Logger
|
||||
|
||||
def create_app(conn, params) do
|
||||
with cs <- App.register_changeset(%App{}, params) |> IO.inspect,
|
||||
|
@ -23,7 +23,58 @@ def create_app(conn, params) do
|
|||
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})
|
||||
json(conn, account)
|
||||
end
|
||||
|
@ -42,6 +93,7 @@ def user(conn, %{"id" => id}) do
|
|||
@instance Application.get_env(:pleroma, :instance)
|
||||
|
||||
def masto_instance(conn, _params) do
|
||||
user_count = Repo.aggregate(User.local_user_query, :count, :id)
|
||||
response = %{
|
||||
uri: Web.base_url,
|
||||
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")
|
||||
},
|
||||
stats: %{
|
||||
user_count: 1,
|
||||
status_count: 2,
|
||||
user_count: user_count,
|
||||
domain_count: 3
|
||||
}
|
||||
},
|
||||
max_toot_chars: Keyword.get(@instance, :limit)
|
||||
}
|
||||
|
||||
json(conn, response)
|
||||
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
|
||||
last = List.last(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
|
||||
params = params
|
||||
|> Map.put("type", ["Create", "Announce"])
|
||||
|> Map.put("blocking_user", user)
|
||||
|
||||
activities = ActivityPub.fetch_activities([user.ap_id | user.following], params)
|
||||
|> Enum.reverse
|
||||
|
@ -92,6 +163,7 @@ def public_timeline(%{assigns: %{user: user}} = conn, params) do
|
|||
params = params
|
||||
|> Map.put("type", ["Create", "Announce"])
|
||||
|> Map.put("local_only", !!params["local"])
|
||||
|> Map.put("blocking_user", user)
|
||||
|
||||
activities = ActivityPub.fetch_public_activities(params)
|
||||
|> Enum.reverse
|
||||
|
@ -107,6 +179,7 @@ def user_statuses(%{assigns: %{user: user}} = conn, params) do
|
|||
params = params
|
||||
|> Map.put("type", ["Create", "Announce"])
|
||||
|> Map.put("actor_id", ap_id)
|
||||
|> Map.put("whole_db", true)
|
||||
|
||||
activities = ActivityPub.fetch_activities([], params)
|
||||
|> 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
|
||||
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 (%{data: %{"type" => type}}) -> type == "Create" end),
|
||||
grouped_activities <- Enum.group_by(activities, fn (%{id: id}) -> id < activity.id end) do
|
||||
result = %{
|
||||
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
|
||||
|
||||
def post_status(%{assigns: %{user: user}} = conn, %{"status" => status} = params) do
|
||||
def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
|
||||
params = params
|
||||
|> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
|
||||
|> Map.put("no_attachment_links", true)
|
||||
|
||||
{:ok, activity} = CommonAPI.post(user, params)
|
||||
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
|
||||
|
||||
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),
|
||||
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
|
||||
render conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity}
|
||||
with {:ok, announce, _activity} = CommonAPI.repeat(ap_id_or_id, user) do
|
||||
render conn, StatusView, "status.json", %{activity: announce, for: user, as: :activity}
|
||||
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
|
||||
notifications = Notification.for_user(user, params)
|
||||
result = Enum.map(notifications, fn (%{id: id, activity: activity, inserted_at: created_at}) ->
|
||||
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})}
|
||||
"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
|
||||
result = Enum.map(notifications, fn x ->
|
||||
render_notification(user, x)
|
||||
end)
|
||||
|> Enum.filter(&(&1))
|
||||
|
||||
|
@ -202,6 +261,33 @@ def notifications(%{assigns: %{user: user}} = conn, params) do
|
|||
|> json(result)
|
||||
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
|
||||
id = List.wrap(id)
|
||||
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}
|
||||
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
|
||||
data = object.data
|
||||
|> Map.put("id", object.id)
|
||||
|
@ -220,7 +306,7 @@ def upload(%{assigns: %{user: user}} = conn, %{"file" => file}) do
|
|||
end
|
||||
|
||||
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,
|
||||
where: u.ap_id in ^likes
|
||||
users = Repo.all(q)
|
||||
|
@ -246,6 +332,7 @@ def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
|
|||
params = params
|
||||
|> Map.put("type", "Create")
|
||||
|> Map.put("local_only", !!params["local"])
|
||||
|> Map.put("blocking_user", user)
|
||||
|
||||
activities = ActivityPub.fetch_public_activities(params)
|
||||
|> Enum.reverse
|
||||
|
@ -272,8 +359,26 @@ def following(conn, %{"id" => id}) do
|
|||
def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
|
||||
with %User{} = followed <- Repo.get(User, id),
|
||||
{: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}
|
||||
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
|
||||
|
||||
|
@ -290,21 +395,55 @@ def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
|
|||
end
|
||||
end
|
||||
|
||||
def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
|
||||
if params["resolve"] == "true" do
|
||||
User.get_or_fetch_by_nickname(query)
|
||||
def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
|
||||
with %User{} = blocked <- Repo.get(User, id),
|
||||
{: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
|
||||
|
||||
q = from u in User,
|
||||
where: fragment("(to_tsvector('english', ?) || to_tsvector('english', ?)) @@ plainto_tsquery('english', ?)", u.nickname, u.name, ^query),
|
||||
limit: 20
|
||||
accounts = Repo.all(q)
|
||||
def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
|
||||
with %User{} = blocked <- Repo.get(User, id),
|
||||
{:ok, blocker} <- User.unblock(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
|
||||
|
||||
# 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,
|
||||
where: fragment("?->>'type' = 'Create'", a.data),
|
||||
where: fragment("to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)", a.data, ^query),
|
||||
limit: 20
|
||||
statuses = Repo.all(q)
|
||||
statuses = Repo.all(q) ++ fetched
|
||||
|
||||
res = %{
|
||||
"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)
|
||||
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
|
||||
|> Map.put("type", "Create")
|
||||
|> Map.put("favorited_by", user.ap_id)
|
||||
|> Map.put("blocking_user", user)
|
||||
|
||||
activities = ActivityPub.fetch_activities([], params)
|
||||
|> Enum.reverse
|
||||
|
@ -327,6 +475,127 @@ def favourites(%{assigns: %{user: user}} = conn, params) do
|
|||
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
|
||||
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
|
||||
Logger.debug("Unimplemented, returning unmodified relationship")
|
||||
with %User{} = target <- Repo.get(User, id) do
|
||||
|
@ -338,4 +607,23 @@ def empty_array(conn, _) do
|
|||
Logger.debug("Unimplemented, returning an empty array")
|
||||
json(conn, [])
|
||||
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
|
||||
|
|
41
lib/pleroma/web/mastodon_api/mastodon_socket.ex
Normal 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
|
|
@ -4,7 +4,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|
|||
alias Pleroma.Web.MastodonAPI.AccountView
|
||||
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
|
||||
|
||||
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"
|
||||
|
||||
%{
|
||||
id: user.id,
|
||||
id: to_string(user.id),
|
||||
username: hd(String.split(user.nickname, "@")),
|
||||
acct: user.nickname,
|
||||
display_name: user.name,
|
||||
|
@ -43,7 +43,7 @@ def render("account.json", %{user: user}) do
|
|||
|
||||
def render("mention.json", %{user: user}) do
|
||||
%{
|
||||
id: user.id,
|
||||
id: to_string(user.id),
|
||||
acct: user.nickname,
|
||||
username: hd(String.split(user.nickname, "@")),
|
||||
url: user.ap_id
|
||||
|
@ -52,10 +52,10 @@ def render("mention.json", %{user: user}) do
|
|||
|
||||
def render("relationship.json", %{user: user, target: target}) do
|
||||
%{
|
||||
id: target.id,
|
||||
id: to_string(target.id),
|
||||
following: User.following?(user, target),
|
||||
followed_by: User.following?(target, user),
|
||||
blocking: false,
|
||||
blocking: User.blocks?(user, target),
|
||||
muting: false,
|
||||
requested: false,
|
||||
domain_blocking: false
|
||||
|
|
5
lib/pleroma/web/mastodon_api/views/mastodon_view.ex
Normal file
|
@ -0,0 +1,5 @@
|
|||
defmodule Pleroma.Web.MastodonAPI.MastodonView do
|
||||
use Pleroma.Web, :view
|
||||
import Phoenix.HTML
|
||||
import Phoenix.HTML.Form
|
||||
end
|
|
@ -21,9 +21,9 @@ def render("status.json", %{activity: %{data: %{"type" => "Announce", "object" =
|
|||
|> Enum.map(fn (user) -> AccountView.render("mention.json", %{user: user}) end)
|
||||
|
||||
%{
|
||||
id: activity.id,
|
||||
id: to_string(activity.id),
|
||||
uri: object,
|
||||
url: nil,
|
||||
url: nil, # TODO: This might be wrong, check with mastodon.
|
||||
account: AccountView.render("account.json", %{user: user}),
|
||||
in_reply_to_id: nil,
|
||||
in_reply_to_account_id: nil,
|
||||
|
@ -45,7 +45,8 @@ def render("status.json", %{activity: %{data: %{"type" => "Announce", "object" =
|
|||
name: "Web",
|
||||
website: nil
|
||||
},
|
||||
language: nil
|
||||
language: nil,
|
||||
emojis: []
|
||||
}
|
||||
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_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"],
|
||||
url: object["external_url"],
|
||||
url: object["external_url"] || object["id"],
|
||||
account: AccountView.render("account.json", %{user: user}),
|
||||
in_reply_to_id: reply_to && reply_to.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,
|
||||
muted: false,
|
||||
sensitive: sensitive,
|
||||
spoiler_text: "",
|
||||
spoiler_text: object["summary"] || "",
|
||||
visibility: "public",
|
||||
media_attachments: attachments,
|
||||
media_attachments: attachments |> Enum.take(4),
|
||||
mentions: mentions,
|
||||
tags: [], # fix,
|
||||
application: %{
|
||||
name: "Web",
|
||||
website: nil
|
||||
},
|
||||
language: nil
|
||||
language: nil,
|
||||
emojis: emojis
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -115,7 +120,7 @@ def render("attachment.json", %{attachment: attachment}) do
|
|||
<< hash_id::signed-32, _rest::binary >> = :crypto.hash(:md5, href)
|
||||
|
||||
%{
|
||||
id: attachment["id"] || hash_id,
|
||||
id: to_string(attachment["id"] || hash_id),
|
||||
url: href,
|
||||
remote_url: href,
|
||||
preview_url: href,
|
||||
|
|
|
@ -25,7 +25,8 @@ def create_authorization(conn, %{"authorization" => %{"name" => name, "password"
|
|||
auth: auth
|
||||
}
|
||||
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 <> "&state=#{params["state"]}"
|
||||
else
|
||||
|
@ -40,7 +41,8 @@ def create_authorization(conn, %{"authorization" => %{"name" => name, "password"
|
|||
# - proper scope handling
|
||||
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"]),
|
||||
%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
|
||||
response = %{
|
||||
token_type: "Bearer",
|
||||
|
@ -50,6 +52,14 @@ def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do
|
|||
scope: "read write follow"
|
||||
}
|
||||
json(conn, response)
|
||||
else
|
||||
_error -> json(conn, %{error: "Invalid credentials"})
|
||||
end
|
||||
end
|
||||
|
||||
defp fix_padding(token) do
|
||||
token
|
||||
|> Base.url_decode64!(padding: false)
|
||||
|> Base.url_encode64
|
||||
end
|
||||
end
|
||||
|
|
|
@ -56,9 +56,9 @@ defp get_links(%{local: false,
|
|||
|
||||
defp get_links(_activity), do: []
|
||||
|
||||
defp get_emoji_links(content) do
|
||||
Enum.map(Formatter.get_emoji(content), fn({emoji, file}) ->
|
||||
{:link, [name: to_charlist(emoji), rel: 'emoji', href: to_charlist("#{Pleroma.Web.Endpoint.static_url}#{file}")], []}
|
||||
defp get_emoji_links(emojis) do
|
||||
Enum.map(emojis, fn({emoji, file}) ->
|
||||
{:link, [name: to_charlist(emoji), rel: 'emoji', href: to_charlist(file)], []}
|
||||
end)
|
||||
end
|
||||
|
||||
|
@ -81,7 +81,13 @@ def to_simple_form(%{data: %{"object" => %{"type" => "Note"}}} = activity, user,
|
|||
categories = (activity.data["object"]["tag"] || [])
|
||||
|> 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']},
|
||||
|
@ -93,7 +99,7 @@ def to_simple_form(%{data: %{"object" => %{"type" => "Note"}}} = activity, user,
|
|||
{:updated, h.(updated_at)},
|
||||
{:"ostatus:conversation", [ref: h.(activity.data["context"])], h.(activity.data["context"])},
|
||||
{: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
|
||||
|
||||
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"]
|
||||
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: []
|
||||
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"]
|
||||
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: []
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
def to_simple_form(_, _, _), do: nil
|
||||
|
||||
def wrap_with_entry(simple_form) do
|
||||
[{
|
||||
:entry, [
|
||||
|
@ -238,6 +246,4 @@ def wrap_with_entry(simple_form) do
|
|||
], simple_form
|
||||
}]
|
||||
end
|
||||
|
||||
def to_simple_form(_, _, _), do: nil
|
||||
end
|
||||
|
|
|
@ -2,7 +2,7 @@ defmodule Pleroma.Web.OStatus.FeedRepresenter do
|
|||
alias Pleroma.Web.OStatus
|
||||
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
|
||||
|> NaiveDateTime.to_iso8601
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
defmodule Pleroma.Web.OStatus.DeleteHandler do
|
||||
require Logger
|
||||
alias Pleroma.Web.{XML, OStatus}
|
||||
alias Pleroma.{Activity, Object, Repo}
|
||||
alias Pleroma.Web.XML
|
||||
alias Pleroma.Object
|
||||
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),
|
||||
object when not is_nil(object) <- Object.get_by_ap_id(id),
|
||||
{:ok, delete} <- ActivityPub.delete(object, false) do
|
||||
|
|
|
@ -94,6 +94,7 @@ def handle_note(entry, doc \\ nil) do
|
|||
[author] <- :xmerl_xpath.string('//author[1]', doc),
|
||||
{:ok, actor} <- OStatus.find_make_or_update_user(author),
|
||||
content_html <- OStatus.get_content(entry),
|
||||
cw <- OStatus.get_cw(entry),
|
||||
inReplyTo <- XML.string_from_xpath("//thr:in-reply-to[1]/@ref", entry),
|
||||
inReplyToActivity <- fetch_replied_to_activity(entry, inReplyTo),
|
||||
inReplyTo <- (inReplyToActivity && inReplyToActivity.data["object"]["id"]) || inReplyTo,
|
||||
|
@ -103,7 +104,7 @@ def handle_note(entry, doc \\ nil) do
|
|||
mentions <- get_mentions(entry),
|
||||
to <- make_to_list(actor, mentions),
|
||||
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("published", date),
|
||||
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)
|
||||
do
|
||||
res = ActivityPub.create(to, actor, context, note, %{}, date, false)
|
||||
User.update_note_count(actor)
|
||||
User.increase_note_count(actor)
|
||||
res
|
||||
else
|
||||
%Activity{} = activity -> {:ok, activity}
|
||||
|
|
|
@ -7,7 +7,6 @@ defmodule Pleroma.Web.OStatus do
|
|||
|
||||
alias Pleroma.{Repo, User, Web, Object, Activity}
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.{WebFinger, Websub}
|
||||
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),
|
||||
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
|
||||
{:ok, activity}
|
||||
else e ->
|
||||
else _ ->
|
||||
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),
|
||||
{:ok, [favorited_activity]} <- fetch_activity_from_url(href) do
|
||||
|
@ -150,22 +149,28 @@ def get_attachments(entry) do
|
|||
end
|
||||
|
||||
@doc """
|
||||
Gets the content from a an entry. Will add the cw text to the body for cw'd
|
||||
Mastodon notes.
|
||||
Gets the content from a an entry.
|
||||
"""
|
||||
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),
|
||||
cw when not is_nil(cw) <- string_from_xpath("/*/summary", entry) do
|
||||
"<span class='mastodon-cw'>#{cw}</span><br>#{base_content}"
|
||||
else _e -> base_content
|
||||
cw
|
||||
else _e -> nil
|
||||
end
|
||||
end
|
||||
|
||||
def get_tags(entry) do
|
||||
: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
|
||||
|
||||
def maybe_update(doc, user) do
|
||||
|
@ -185,7 +190,7 @@ def maybe_update(doc, user) do
|
|||
false <- new_data == old_data do
|
||||
change = Ecto.Changeset.change(user, new_data)
|
||||
Repo.update(change)
|
||||
else e ->
|
||||
else _ ->
|
||||
{:ok, user}
|
||||
end
|
||||
end
|
||||
|
@ -215,7 +220,7 @@ def insert_or_update_user(data) do
|
|||
Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
|
||||
end
|
||||
|
||||
def make_user(uri) do
|
||||
def make_user(uri, update \\ false) do
|
||||
with {:ok, info} <- gather_user_info(uri) do
|
||||
data = %{
|
||||
name: info["name"],
|
||||
|
@ -225,7 +230,8 @@ def make_user(uri) do
|
|||
avatar: info["avatar"],
|
||||
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}
|
||||
else _e -> insert_or_update_user(data)
|
||||
end
|
||||
|
|
|
@ -5,6 +5,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do
|
|||
alias Pleroma.Web.OStatus.{FeedRepresenter, ActivityRepresenter}
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.Web.{OStatus, Federator}
|
||||
alias Pleroma.Web.XML
|
||||
import Ecto.Query
|
||||
|
||||
def feed_redirect(conn, %{"nickname" => nickname}) do
|
||||
|
@ -36,10 +37,26 @@ def feed(conn, %{"nickname" => nickname}) do
|
|||
|> send_resp(200, response)
|
||||
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, magic_key} = Pleroma.Web.Salmon.fetch_magic_key(body)
|
||||
{:ok, doc} = Pleroma.Web.Salmon.decode_and_validate(magic_key, body)
|
||||
{:ok, doc} = decode_or_retry(body)
|
||||
|
||||
Federator.enqueue(:incoming_doc, doc)
|
||||
|
||||
|
@ -69,6 +86,19 @@ def activity(conn, %{"uuid" => uuid}) do
|
|||
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
|
||||
response = activity
|
||||
|> ActivityRepresenter.to_simple_form(user, true)
|
||||
|
|
|
@ -19,6 +19,7 @@ def to_simple_form(user) do
|
|||
{:"poco:preferredUsername", [nickname]},
|
||||
{:"poco:displayName", [name]},
|
||||
{:"poco:note", [bio]},
|
||||
{:summary, [bio]},
|
||||
{:name, [nickname]},
|
||||
{:link, [rel: 'avatar', href: avatar_url], []}
|
||||
] ++ banner
|
||||
|
|
|
@ -21,6 +21,13 @@ def user_fetcher(username) do
|
|||
plug Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1}
|
||||
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
|
||||
plug :accepts, ["xml", "xrd+xml"]
|
||||
end
|
||||
|
@ -33,6 +40,17 @@ def user_fetcher(username) do
|
|||
plug :accepts, ["html", "json"]
|
||||
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
|
||||
get "/authorize", OAuthController, :authorize
|
||||
post "/authorize", OAuthController, :create_authorization
|
||||
|
@ -42,16 +60,21 @@ def user_fetcher(username) do
|
|||
scope "/api/v1", Pleroma.Web.MastodonAPI do
|
||||
pipe_through :authenticated_api
|
||||
|
||||
patch "/accounts/update_credentials", MastodonAPIController, :update_credentials
|
||||
get "/accounts/verify_credentials", MastodonAPIController, :verify_credentials
|
||||
get "/accounts/relationships", MastodonAPIController, :relationships
|
||||
get "/accounts/search", MastodonAPIController, :account_search
|
||||
post "/accounts/:id/follow", MastodonAPIController, :follow
|
||||
post "/accounts/:id/unfollow", MastodonAPIController, :unfollow
|
||||
post "/accounts/:id/block", MastodonAPIController, :relationship_noop
|
||||
post "/accounts/:id/unblock", MastodonAPIController, :relationship_noop
|
||||
post "/accounts/:id/block", MastodonAPIController, :block
|
||||
post "/accounts/:id/unblock", MastodonAPIController, :unblock
|
||||
post "/accounts/:id/mute", 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 "/follow_requests", 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/unfavourite", MastodonAPIController, :unfav_status
|
||||
|
||||
post "/notifications/clear", MastodonAPIController, :clear_notifications
|
||||
post "/notifications/dismiss", MastodonAPIController, :dismiss_notification
|
||||
get "/notifications", MastodonAPIController, :notifications
|
||||
get "/notifications/:id", MastodonAPIController, :get_notification
|
||||
|
||||
post "/media", MastodonAPIController, :upload
|
||||
end
|
||||
|
@ -76,6 +102,7 @@ def user_fetcher(username) do
|
|||
pipe_through :api
|
||||
get "/instance", MastodonAPIController, :masto_instance
|
||||
post "/apps", MastodonAPIController, :create_app
|
||||
get "/custom_emojis", MastodonAPIController, :custom_emojis
|
||||
|
||||
get "/timelines/public", MastodonAPIController, :public_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/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 "/statusnet/conversation/:id", TwitterAPI.Controller, :fetch_conversation
|
||||
|
@ -123,7 +151,6 @@ def user_fetcher(username) do
|
|||
|
||||
get "/search", TwitterAPI.Controller, :search
|
||||
get "/statusnet/tags/timeline/:tag", TwitterAPI.Controller, :public_and_external_timeline
|
||||
get "/externalprofile/show", TwitterAPI.Controller, :external_profile
|
||||
end
|
||||
|
||||
scope "/api", Pleroma.Web do
|
||||
|
@ -149,6 +176,8 @@ def user_fetcher(username) do
|
|||
|
||||
post "/friendships/create", TwitterAPI.Controller, :follow
|
||||
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 "/media/upload", TwitterAPI.Controller, :upload_json
|
||||
|
@ -161,6 +190,12 @@ def user_fetcher(username) do
|
|||
|
||||
get "/statuses/followers", TwitterAPI.Controller, :followers
|
||||
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
|
||||
|
||||
pipeline :ostatus do
|
||||
|
@ -172,6 +207,7 @@ def user_fetcher(username) do
|
|||
|
||||
get "/objects/:uuid", OStatus.OStatusController, :object
|
||||
get "/activities/:uuid", OStatus.OStatusController, :activity
|
||||
get "/notice/:id", OStatus.OStatusController, :notice
|
||||
|
||||
get "/users/:nickname/feed", OStatus.OStatusController, :feed
|
||||
get "/users/:nickname", OStatus.OStatusController, :feed_redirect
|
||||
|
@ -188,6 +224,15 @@ def user_fetcher(username) do
|
|||
get "/webfinger", WebFinger.WebFingerController, :webfinger
|
||||
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
|
||||
get "/*path", RedirectController, :redirector
|
||||
end
|
||||
|
|
|
@ -73,6 +73,18 @@ def encode_key({:RSAPublicKey, modulus, exponent}) do
|
|||
"RSA.#{modulus_enc}.#{exponent_enc}"
|
||||
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
|
||||
port = Port.open({:spawn, "openssl genrsa"}, [:binary])
|
||||
{:ok, pem} = receive do
|
||||
|
@ -85,6 +97,7 @@ def generate_rsa_pem do
|
|||
:error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def keys_from_pem(pem) do
|
||||
[private_key_code] = :public_key.pem_decode(pem)
|
||||
|
|
112
lib/pleroma/web/streamer.ex
Normal 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
|
|
@ -3,9 +3,73 @@
|
|||
<head>
|
||||
<meta charset=utf-8 />
|
||||
<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>
|
||||
<body>
|
||||
<h1>Welcome to Pleroma</h1>
|
||||
<div class="container">
|
||||
<h1>Pleroma</h1>
|
||||
<%= render @view_module, @view_template, assigns %>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -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='{"locale":"en"}' id='mastodon'>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -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 %>
|
|
@ -0,0 +1 @@
|
|||
<h2>Invalid Token</h2>
|
|
@ -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 %>
|
|
@ -0,0 +1 @@
|
|||
<h2>Password reset failed</h2>
|
|
@ -0,0 +1 @@
|
|||
<h2>Password changed!</h2>
|
|
@ -1,6 +1,29 @@
|
|||
defmodule Pleroma.Web.TwitterAPI.UtilController do
|
||||
use Pleroma.Web, :controller
|
||||
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
|
||||
json(conn, "ok")
|
||||
|
@ -46,4 +69,8 @@ def version(conn, _params) do
|
|||
_ -> json(conn, version)
|
||||
end
|
||||
end
|
||||
|
||||
def emoji(conn, _params) do
|
||||
json conn, Enum.into(Formatter.get_custom_emoji(), %{})
|
||||
end
|
||||
end
|
||||
|
|
|
@ -97,7 +97,7 @@ def to_map(%Activity{data: %{"type" => "Undo", "published" => created_at, "objec
|
|||
}
|
||||
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
|
||||
|
||||
%{
|
||||
|
@ -135,6 +135,13 @@ def to_map(%Activity{data: %{"object" => %{"content" => content} = object}} = ac
|
|||
tags = activity.data["object"]["tag"] || []
|
||||
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"])
|
||||
|
||||
%{
|
||||
|
|
|
@ -4,27 +4,29 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
|
|||
alias Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter
|
||||
alias Pleroma.Web.TwitterAPI.UserView
|
||||
alias Pleroma.Web.{OStatus, CommonAPI}
|
||||
alias Pleroma.Formatter
|
||||
import Ecto.Query
|
||||
|
||||
@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)
|
||||
end
|
||||
|
||||
def fetch_friend_statuses(user, opts \\ %{}) do
|
||||
opts = Map.put(opts, "blocking_user", user)
|
||||
ActivityPub.fetch_activities([user.ap_id | user.following], opts)
|
||||
|> activities_to_statuses(%{for: user})
|
||||
end
|
||||
|
||||
def fetch_public_statuses(user, opts \\ %{}) do
|
||||
opts = Map.put(opts, "local_only", true)
|
||||
opts = Map.put(opts, "blocking_user", user)
|
||||
ActivityPub.fetch_public_activities(opts)
|
||||
|> activities_to_statuses(%{for: user})
|
||||
end
|
||||
|
||||
def fetch_public_and_external_statuses(user, opts \\ %{}) do
|
||||
opts = Map.put(opts, "blocking_user", user)
|
||||
ActivityPub.fetch_public_activities(opts)
|
||||
|> activities_to_statuses(%{for: user})
|
||||
end
|
||||
|
@ -41,7 +43,7 @@ def fetch_mentions(user, opts \\ %{}) do
|
|||
|
||||
def fetch_conversation(user, id) do
|
||||
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})
|
||||
do
|
||||
statuses
|
||||
|
@ -83,6 +85,26 @@ def unfollow(%User{} = follower, params) do
|
|||
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
|
||||
with {:ok, _announce, %{data: %{"id" => id}}} = CommonAPI.repeat(ap_id_or_id, user),
|
||||
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id),
|
||||
|
@ -193,7 +215,7 @@ def get_user(user \\ nil, params) do
|
|||
end
|
||||
end
|
||||
|
||||
defp parse_int(string, default \\ nil)
|
||||
defp parse_int(string, default)
|
||||
defp parse_int(string, default) when is_binary(string) do
|
||||
with {n, _} <- Integer.parse(string) do
|
||||
n
|
||||
|
|
|
@ -3,17 +3,18 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
|
|||
alias Pleroma.Web.TwitterAPI.{TwitterAPI, UserView}
|
||||
alias Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.{Repo, Activity, User, Object}
|
||||
alias Pleroma.{Repo, Activity, User}
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Ecto.Changeset
|
||||
|
||||
require Logger
|
||||
|
||||
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
|
||||
|
||||
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),
|
||||
{:ok, activity} <- TwitterAPI.create_status(user, Map.put(status_data, "media_ids", media_ids)) do
|
||||
conn
|
||||
|
@ -65,10 +66,23 @@ def friends_timeline(%{assigns: %{user: user}} = conn, params) do
|
|||
|> json_reply(200, json)
|
||||
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
|
||||
case TwitterAPI.get_user(user, params) do
|
||||
{: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)
|
||||
conn
|
||||
|> json_reply(200, statuses |> Poison.encode!)
|
||||
|
@ -93,6 +107,22 @@ def follow(%{assigns: %{user: user}} = conn, params) do
|
|||
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
|
||||
with {:ok, delete} <- CommonAPI.delete(id, user) do
|
||||
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"]}),
|
||||
new_info <- Map.put(user.info, "banner", object.data),
|
||||
change <- User.info_changeset(user, %{info: new_info}),
|
||||
{:ok, user} <- Repo.update(change) do
|
||||
%{"url" => [ %{ "href" => href } | t ]} = object.data
|
||||
{:ok, _user} <- Repo.update(change) do
|
||||
%{"url" => [ %{ "href" => href } | _ ]} = object.data
|
||||
response = %{ url: href } |> Poison.encode!
|
||||
conn
|
||||
|> json_reply(200, response)
|
||||
|
@ -198,8 +228,8 @@ def update_background(%{assigns: %{user: user}} = conn, params) do
|
|||
with {:ok, object} <- ActivityPub.upload(params),
|
||||
new_info <- Map.put(user.info, "background", object.data),
|
||||
change <- User.info_changeset(user, %{info: new_info}),
|
||||
{:ok, user} <- Repo.update(change) do
|
||||
%{"url" => [ %{ "href" => href } | t ]} = object.data
|
||||
{:ok, _user} <- Repo.update(change) do
|
||||
%{"url" => [ %{ "href" => href } | _ ]} = object.data
|
||||
response = %{ url: href } |> Poison.encode!
|
||||
conn
|
||||
|> 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),
|
||||
updated_info <- Map.put(info, "most_recent_notification", mrn),
|
||||
changeset <- User.info_changeset(user, %{info: updated_info}),
|
||||
{:ok, user} <- Repo.update(changeset) do
|
||||
{:ok, _user} <- Repo.update(changeset) do
|
||||
conn
|
||||
|> json_reply(200, Poison.encode!(mrn))
|
||||
else
|
||||
|
@ -249,6 +279,22 @@ def friends(%{assigns: %{user: user}} = conn, _params) do
|
|||
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
|
||||
params = if bio = params["description"] do
|
||||
Map.put(params, "bio", bio)
|
||||
|
@ -266,7 +312,7 @@ def update_profile(%{assigns: %{user: user}} = conn, params) do
|
|||
end
|
||||
end
|
||||
|
||||
def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
|
||||
def search(%{assigns: %{user: user}} = conn, %{"q" => _query} = params) do
|
||||
conn
|
||||
|> json(TwitterAPI.search(user, params))
|
||||
end
|
||||
|
|
|
@ -11,25 +11,28 @@ def render("index.json", %{users: users, for: user}) do
|
|||
render_many(users, Pleroma.Web.TwitterAPI.UserView, "user.json", for: user)
|
||||
end
|
||||
|
||||
defp image_url(%{"url" => [ %{ "href" => href } | t ]}), do: href
|
||||
defp image_url(_), do: nil
|
||||
|
||||
def render("user.json", %{user: user = %User{}} = assigns) do
|
||||
image = User.avatar_url(user)
|
||||
following = if assigns[:for] do
|
||||
User.following?(assigns[:for], user)
|
||||
{following, follows_you, statusnet_blocking} = if assigns[:for] do
|
||||
{
|
||||
User.following?(assigns[:for], user),
|
||||
User.following?(user, assigns[:for]),
|
||||
User.blocks?(assigns[:for], user)
|
||||
}
|
||||
else
|
||||
false
|
||||
{false, false, false}
|
||||
end
|
||||
|
||||
user_info = User.get_cached_user_info(user)
|
||||
|
||||
%{
|
||||
data = %{
|
||||
"created_at" => user.inserted_at |> Utils.format_naive_asctime,
|
||||
"description" => HtmlSanitizeEx.strip_tags(user.bio),
|
||||
"favourites_count" => 0,
|
||||
"followers_count" => user_info[:follower_count],
|
||||
"following" => following,
|
||||
"follows_you" => follows_you,
|
||||
"statusnet_blocking" => statusnet_blocking,
|
||||
"friends_count" => user_info[:following_count],
|
||||
"id" => user.id,
|
||||
"name" => user.name,
|
||||
|
@ -44,6 +47,12 @@ def render("user.json", %{user: user = %User{}} = assigns) do
|
|||
"cover_photo" => image_url(user.info["banner"]),
|
||||
"background_image" => image_url(user.info["background"])
|
||||
}
|
||||
|
||||
if assigns[:token] do
|
||||
Map.put(data, "token", assigns[:token])
|
||||
else
|
||||
data
|
||||
end
|
||||
end
|
||||
|
||||
def render("short.json", %{user: %User{
|
||||
|
@ -57,4 +66,7 @@ def render("short.json", %{user: %User{
|
|||
"screen_name" => nickname
|
||||
}
|
||||
end
|
||||
|
||||
defp image_url(%{"url" => [ %{ "href" => href } | _ ]}), do: href
|
||||
defp image_url(_), do: nil
|
||||
end
|
||||
|
|
4
lib/pleroma/web/twitter_api/views/util_view.ex
Normal file
|
@ -0,0 +1,4 @@
|
|||
defmodule Pleroma.Web.TwitterAPI.UtilView do
|
||||
use Pleroma.Web, :view
|
||||
import Phoenix.HTML.Form
|
||||
end
|
|
@ -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
|
||||
get_template_from_xml(body)
|
||||
else
|
||||
e ->
|
||||
_ ->
|
||||
with {:ok, %{body: body}} <- @httpoison.get("https://#{domain}/.well-known/host-meta", []) do
|
||||
get_template_from_xml(body)
|
||||
else
|
||||
|
|
|
@ -31,9 +31,9 @@ def verify(subscription, getter \\ &@httpoison.get/3) do
|
|||
do
|
||||
changeset = Changeset.change(subscription, %{state: "active"})
|
||||
Repo.update(changeset)
|
||||
else _e ->
|
||||
changeset = Changeset.change(subscription, %{state: "rejected"})
|
||||
{:ok, subscription} = Repo.update(changeset)
|
||||
else e ->
|
||||
Logger.debug("Couldn't verify subscription")
|
||||
Logger.debug(inspect(e))
|
||||
{:error, subscription}
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
defmodule Pleroma.Web.XML do
|
||||
require Logger
|
||||
|
||||
def string_from_xpath(xpath, :error), do: nil
|
||||
def string_from_xpath(_, :error), do: nil
|
||||
def string_from_xpath(xpath, doc) do
|
||||
{:xmlObj, :string, res} = :xmerl_xpath.string('string(#{xpath})', doc)
|
||||
|
||||
|
@ -20,7 +20,7 @@ def parse_document(text) do
|
|||
|
||||
doc
|
||||
catch
|
||||
:exit, error ->
|
||||
:exit, _error ->
|
||||
Logger.debug("Couldn't parse xml: #{inspect(text)}")
|
||||
:error
|
||||
end
|
||||
|
|
77
lib/transports.ex
Normal 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
|
|
@ -37,6 +37,6 @@ defp make_open_tag(tag, attributes) do
|
|||
"#{attribute}=\"#{value}\""
|
||||
end |> Enum.join(" ")
|
||||
|
||||
[tag, attributes_string] |> Enum.join(" ") |> String.strip
|
||||
[tag, attributes_string] |> Enum.join(" ") |> String.trim
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
26
priv/repo/migrations/20171109114020_fill_actor_field.exs
Normal 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
|
||||
|
|
@ -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
|
|
@ -0,0 +1,7 @@
|
|||
defmodule Pleroma.Repo.Migrations.AddLocalIndexToUser do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create index(:users, [:local])
|
||||
end
|
||||
end
|
1
priv/static/emoji/1f004.svg
Normal 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 |
1
priv/static/emoji/1f0cf.svg
Normal 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 |
1
priv/static/emoji/1f170.svg
Normal 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 |
1
priv/static/emoji/1f171.svg
Normal 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 |
1
priv/static/emoji/1f17e.svg
Normal 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 |
1
priv/static/emoji/1f17f.svg
Normal 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 |
1
priv/static/emoji/1f18e.svg
Normal 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 |
1
priv/static/emoji/1f191.svg
Normal 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 |
1
priv/static/emoji/1f192.svg
Normal 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 |
1
priv/static/emoji/1f193.svg
Normal 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 |
1
priv/static/emoji/1f194.svg
Normal 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 |
1
priv/static/emoji/1f195.svg
Normal 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 |
1
priv/static/emoji/1f196.svg
Normal 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 |
1
priv/static/emoji/1f197.svg
Normal 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 |
1
priv/static/emoji/1f198.svg
Normal 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 |
1
priv/static/emoji/1f199.svg
Normal 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 |
1
priv/static/emoji/1f19a.svg
Normal 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 |
1
priv/static/emoji/1f1e6-1f1e8.svg
Normal file
After Width: | Height: | Size: 32 KiB |
1
priv/static/emoji/1f1e6-1f1e9.svg
Normal file
After Width: | Height: | Size: 9.6 KiB |
1
priv/static/emoji/1f1e6-1f1ea.svg
Normal 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 |
1
priv/static/emoji/1f1e6-1f1eb.svg
Normal file
After Width: | Height: | Size: 7.9 KiB |
1
priv/static/emoji/1f1e6-1f1ec.svg
Normal 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 |
1
priv/static/emoji/1f1e6-1f1ee.svg
Normal file
After Width: | Height: | Size: 7.9 KiB |
1
priv/static/emoji/1f1e6-1f1f1.svg
Normal file
After Width: | Height: | Size: 20 KiB |
1
priv/static/emoji/1f1e6-1f1f2.svg
Normal 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 |
1
priv/static/emoji/1f1e6-1f1f4.svg
Normal 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 |
1
priv/static/emoji/1f1e6-1f1f6.svg
Normal 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 |
1
priv/static/emoji/1f1e6-1f1f7.svg
Normal 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 |
1
priv/static/emoji/1f1e6-1f1f8.svg
Normal file
After Width: | Height: | Size: 8.1 KiB |
1
priv/static/emoji/1f1e6-1f1f9.svg
Normal 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 |
1
priv/static/emoji/1f1e6-1f1fa.svg
Normal file
After Width: | Height: | Size: 5.3 KiB |
1
priv/static/emoji/1f1e6-1f1fc.svg
Normal 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 |