Merge branch 'develop' into fix/formatter-ignore-html-chars

This commit is contained in:
rinpatch 2018-12-18 21:39:36 +03:00
commit ef318fb8a9
79 changed files with 1616 additions and 337 deletions

4
.gitignore vendored
View file

@ -8,7 +8,9 @@
/.elixir_ls
/test/fixtures/test_tmp.txt
/test/fixtures/image_tmp.jpg
/test/tmp/
/doc
/instance
# Prevent committing custom emojis
/priv/static/emoji/custom/*
@ -31,4 +33,4 @@ erl_crash.dump
.env
# Editor config
/.vscode
/.vscode/

View file

@ -13,7 +13,6 @@ cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- deps
- _build
stages:
- lint
- test

View file

@ -10,6 +10,13 @@
config :pleroma, Pleroma.Repo, types: Pleroma.PostgresTypes
config :pleroma, Pleroma.Captcha,
enabled: false,
seconds_retained: 180,
method: Pleroma.Captcha.Kocaptcha
config :pleroma, Pleroma.Captcha.Kocaptcha, endpoint: "https://captcha.kotobank.ch"
# Upload configuration
config :pleroma, Pleroma.Upload,
uploader: Pleroma.Uploaders.Local,
@ -50,6 +57,15 @@
# Configures the endpoint
config :pleroma, Pleroma.Web.Endpoint,
url: [host: "localhost"],
http: [
dispatch: [
{:_,
[
{"/api/v1/streaming", Elixir.Pleroma.Web.MastodonAPI.WebsocketHandler, []},
{:_, Plug.Adapters.Cowboy.Handler, {Pleroma.Web.Endpoint, []}}
]}
]
],
protocol: "https",
secret_key_base: "aK4Abxf29xU9TTDKre9coZPUgevcVCFQJe/5xP/7Lt4BEif6idBIbjupVbOrbKxl",
signing_salt: "CqaoopA2",
@ -94,6 +110,7 @@
public: true,
quarantined_instances: [],
managed_config: true,
static_dir: "instance/static/",
allowed_post_formats: [
"text/plain",
"text/html",
@ -120,8 +137,8 @@
logo_mask: true,
logo_margin: "0.1em",
background: "/static/aurora_borealis.jpg",
redirect_root_no_login: "/main/all",
redirect_root_login: "/main/friends",
redirect_root_no_login: "/~/main/all",
redirect_root_login: "/~/main/friends",
show_instance_panel: true,
scope_options_enabled: false,
formatting_options_enabled: false,

View file

@ -7,7 +7,7 @@ If you run Pleroma with ``MIX_ENV=prod`` the file is ``prod.secret.exs``, otherw
* `uploader`: Select which `Pleroma.Uploaders` to use
* `filters`: List of `Pleroma.Upload.Filter` to use.
* `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host.
* `proxy_remote`: If you're using a remote uploader, Pleroma will proxy media requests instead of redirecting to it.
* `proxy_remote`: If you\'re using a remote uploader, Pleroma will proxy media requests instead of redirecting to it.
* `proxy_opts`: Proxy options, see `Pleroma.ReverseProxy` documentation.
Note: `strip_exif` has been replaced by `Pleroma.Upload.Filter.Mogrify`.
@ -163,3 +163,15 @@ Web Push Notifications configuration. You can use the mix task `mix web_push.gen
* ``subject``: a mailto link for the administrative contact. Its best if this email is not a personal email address, but rather a group email so that if a person leaves an organization, is unavailable for an extended period, or otherwise cant respond, someone else on the list can.
* ``public_key``: VAPID public key
* ``private_key``: VAPID private key
## Pleroma.Captcha
* `enabled`: Whether the captcha should be shown on registration
* `method`: The method/service to use for captcha
* `seconds_retained`: The time in seconds for which the captcha is valid (stored in the cache)
### Pleroma.Captcha.Kocaptcha
Kocaptcha is a very simple captcha service with a single API endpoint,
the source code is here: https://github.com/koto-bank/kocaptcha. The default endpoint
`https://captcha.kotobank.ch` is hosted by the developer.
* `endpoint`: the kocaptcha endpoint to use

View file

@ -12,6 +12,7 @@
protocol_options: [max_request_line_length: 8192, max_header_value_length: 8192]
],
protocol: "http",
secure_cookie_flag: false,
debug_errors: true,
code_reloader: true,
check_origin: false,

View file

@ -4,7 +4,14 @@
# you can enable the server option below.
config :pleroma, Pleroma.Web.Endpoint,
http: [port: 4001],
server: false
url: [port: 4001],
server: true
# Disable captha for tests
config :pleroma, Pleroma.Captcha,
enabled: true,
# A fake captcha service for tests
method: Pleroma.Captcha.Mock
# Print only warnings and errors during test
config :logger, level: :warn

View file

@ -0,0 +1,57 @@
#!/bin/sh
# PROVIDE: pleroma
# REQUIRE: DAEMON pgsql
if [ -f /etc/rc.subr ]; then
. /etc/rc.subr
fi
name="pleroma"
rcvar=${name}
command="/usr/pkg/bin/elixir"
command_args="--detached -S /usr/pkg/bin/mix phx.server"
start_precmd="ulimit -n unlimited"
pidfile="/dev/null"
pleroma_chdir="${pleroma_home}/pleroma"
pleroma_env="HOME=${pleroma_home} MIX_ENV=prod"
check_pidfile()
{
pid=$(pgrep -U "${pleroma_user}" /bin/beam.smp$)
echo -n "${pid}"
}
if [ -f /etc/rc.subr -a -d /etc/rc.d -a -f /etc/rc.d/DAEMON ]; then
# newer NetBSD
load_rc_config ${name}
run_rc_command "$1"
else
# ancient NetBSD, Solaris and illumos, Linux, etc...
cmd=${1:-start}
case ${cmd} in
start)
echo "Starting ${name}."
${start_cmd}
;;
stop)
echo "Stopping ${name}."
check_pidfile
! [ -n ${pid} ] && kill ${pid}
;;
restart)
( $0 stop )
sleep 5
$0 start
;;
*)
echo 1>&2 "Usage: $0 [start|stop|restart]"
exit 1
;;
esac
exit 0
fi

View file

@ -0,0 +1,36 @@
#
# Default httpd.conf file for Pleroma on OpenBSD
# Simple installation instructions
# 1. Place file in /etc
# 2. Replace <IPv4 address> with your public IP address
# 3. If using IPv6, uncomment IPv6 lines and replace <IPv6 address> with your public IPv6 address
# 4. Check file using 'doas httpd -n'
# 5. Enable and start httpd:
# # doas rcctl enable httpd
# # doas rcctl start httpd
#
ext_inet="<IPv4 address>"
#ext_inet6="<IPv6 address>"
server "default" {
listen on $ext_inet port 80 # Comment to disable listening on IPv4
# listen on $ext_inet6 port 80 # Comment to disable listening on IPv6
listen on 127.0.0.1 port 80 # Do NOT comment this line
log syslog
directory no index
location "/.well-known/acme-challenge/*" {
root "/acme"
request strip 2
}
location "/robots.txt" { root "/htdocs/local/" }
location "/*" { block return 302 "https://$HTTP_HOST$REQUEST_URI" }
}
types {
include "/usr/share/misc/mime.types"
}

View file

@ -0,0 +1,34 @@
#!/bin/ksh
#
# Default init file for Pleroma on OpenBSD
#
# Simple installation instructions:
# 1. Install Pleroma per wiki instructions
# 2. Place this pleromad file in /etc/rc.d
# 3. Enable and start Pleroma
# # doas rcctl enable pleromad
# # doas rcctl start pleromad
#
daemon="/usr/local/bin/elixir"
daemon_flags="--detached -S /usr/local/bin/mix phx.server"
daemon_user="_pleroma"
. /etc/rc.d/rc.subr
rc_reload=NO
pexp="phx.server"
rc_check() {
pgrep -q -U _pleroma -f "phx.server"
}
rc_start() {
${rcexec} "cd pleroma; ${daemon} ${daemon_flags}"
}
rc_stop() {
pkill -q -U _pleroma -f "phx.server"
}
rc_cmd $1

View file

@ -0,0 +1,44 @@
#
# Default relayd.conf file for Pleroma on OpenBSD
# Simple installation instructions:
# 1. Place in /etc
# 2. Replace <ipaddr> with your public IPv4 address
# 3. If using IPv6i, uncomment IPv6 lines and replace <ip6addr> with your public IPv6 address
# 4. Check file using 'doas relayd -n'
# 5. Reload/start relayd
# # doas rcctl enable relayd
# # doas rcctl start relayd
#
ext_inet="<ipaddr>"
#ext_inet6="<ip6addr>"
table <pleroma_server> { 127.0.0.1 }
table <httpd_server> { 127.0.0.1 }
http protocol plerup { # Protocol for upstream pleroma server
#tcp { nodelay, sack, socket buffer 65536, backlog 128 } # Uncomment and adjust as you see fit
tls ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA0-POLY1305"
tls ecdhe secp384r1
# Forward some paths to the local server (as pleroma won't respond to them as you might want)
pass request quick path "/robots.txt" forward to <httpd_server>
# Append a bunch of headers
match request header append "X-Forwarded-For" value "$REMOTE_ADDR" # This two header and the next one are not strictl required by pleroma but adding them won't hurt
match request header append "X-Forwarded-By" value "$SERVER_ADDR:$SERVER_PORT"
match request header append "Connection" value "upgrade"
}
relay wwwtls {
listen on $ext_inet port https tls # Comment to disable listening on IPv4
# listen on $ext_inet6 port https tls # Comment to disable listening on IPv6
protocol plerup
forward to <pleroma_server> port 4000 check http "/" code 200
forward to <httpd_server> port 80 check http "/robots.txt" code 200
}

View file

@ -1,6 +1,6 @@
CREATE USER pleroma WITH ENCRYPTED PASSWORD '<%= dbpass %>';
CREATE DATABASE pleroma_dev OWNER pleroma;
\c pleroma_dev;
CREATE USER <%= dbuser %> WITH ENCRYPTED PASSWORD '<%= dbpass %>';
CREATE DATABASE <%= dbname %> OWNER <%= dbuser %>;
\c <%= dbname %>;
--Extensions made by ecto.migrate that need superuser access
CREATE EXTENSION IF NOT EXISTS citext;
CREATE EXTENSION IF NOT EXISTS pg_trgm;

View file

@ -94,16 +94,14 @@ def run(["new", nickname, email | rest]) do
unless not proceed? do
Common.start_pleroma()
params =
%{
nickname: nickname,
email: email,
password: password,
password_confirmation: password,
name: name,
bio: bio
}
|> IO.inspect()
params = %{
nickname: nickname,
email: email,
password: password,
password_confirmation: password,
name: name,
bio: bio
}
user = User.register_changeset(%User{}, params)
Repo.insert!(user)

View file

@ -24,6 +24,7 @@ def start(_type, _args) do
# Start the Ecto repository
supervisor(Pleroma.Repo, []),
worker(Pleroma.Emoji, []),
worker(Pleroma.Captcha, []),
worker(
Cachex,
[

View file

@ -0,0 +1,66 @@
defmodule Pleroma.Captcha do
use GenServer
@ets_options [:ordered_set, :private, :named_table, {:read_concurrency, true}]
@doc false
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
@doc false
def init(_) do
# Create a ETS table to store captchas
ets_name = Module.concat(method(), Ets)
^ets_name = :ets.new(Module.concat(method(), Ets), @ets_options)
# Clean up old captchas every few minutes
seconds_retained = Pleroma.Config.get!([__MODULE__, :seconds_retained])
Process.send_after(self(), :cleanup, 1000 * seconds_retained)
{:ok, nil}
end
@doc """
Ask the configured captcha service for a new captcha
"""
def new() do
GenServer.call(__MODULE__, :new)
end
@doc """
Ask the configured captcha service to validate the captcha
"""
def validate(token, captcha) do
GenServer.call(__MODULE__, {:validate, token, captcha})
end
@doc false
def handle_call(:new, _from, state) do
enabled = Pleroma.Config.get([__MODULE__, :enabled])
if !enabled do
{:reply, %{type: :none}, state}
else
{:reply, method().new(), state}
end
end
@doc false
def handle_call({:validate, token, captcha}, _from, state) do
{:reply, method().validate(token, captcha), state}
end
@doc false
def handle_info(:cleanup, state) do
:ok = method().cleanup()
seconds_retained = Pleroma.Config.get!([__MODULE__, :seconds_retained])
# Schedule the next clenup
Process.send_after(self(), :cleanup, 1000 * seconds_retained)
{:noreply, state}
end
defp method, do: Pleroma.Config.get!([__MODULE__, :method])
end

View file

@ -0,0 +1,28 @@
defmodule Pleroma.Captcha.Service do
@doc """
Request new captcha from a captcha service.
Returns:
Service-specific data for using the newly created captcha
"""
@callback new() :: map
@doc """
Validated the provided captcha solution.
Arguments:
* `token` the captcha is associated with
* `captcha` solution of the captcha to validate
Returns:
`true` if captcha is valid, `false` if not
"""
@callback validate(token :: String.t(), captcha :: String.t()) :: boolean
@doc """
This function is called periodically to clean up old captchas
"""
@callback cleanup() :: :ok
end

View file

@ -0,0 +1,67 @@
defmodule Pleroma.Captcha.Kocaptcha do
alias Calendar.DateTime
alias Pleroma.Captcha.Service
@behaviour Service
@ets __MODULE__.Ets
@impl Service
def new() do
endpoint = Pleroma.Config.get!([__MODULE__, :endpoint])
case Tesla.get(endpoint <> "/new") do
{:error, _} ->
%{error: "Kocaptcha service unavailable"}
{:ok, res} ->
json_resp = Poison.decode!(res.body)
token = json_resp["token"]
true =
:ets.insert(
@ets,
{token, json_resp["md5"], DateTime.now_utc() |> DateTime.Format.unix()}
)
%{type: :kocaptcha, token: token, url: endpoint <> json_resp["url"]}
end
end
@impl Service
def validate(token, captcha) do
with false <- is_nil(captcha),
[{^token, saved_md5, _}] <- :ets.lookup(@ets, token),
true <- :crypto.hash(:md5, captcha) |> Base.encode16() == String.upcase(saved_md5) do
# Clear the saved value
:ets.delete(@ets, token)
true
else
_ -> false
end
end
@impl Service
def cleanup() do
seconds_retained = Pleroma.Config.get!([Pleroma.Captcha, :seconds_retained])
# If the time in ETS is less than current_time - seconds_retained, then the time has
# already passed
delete_after =
DateTime.subtract!(DateTime.now_utc(), seconds_retained) |> DateTime.Format.unix()
:ets.select_delete(
@ets,
[
{
{:_, :_, :"$1"},
[{:<, :"$1", {:const, delete_after}}],
[true]
}
]
)
:ok
end
end

View file

@ -11,7 +11,8 @@ def call(conn, _opts) do
else
conn
|> put_status(404)
|> Phoenix.Controller.render(Pleroma.Web.ErrorView, "404.json")
|> Phoenix.Controller.put_view(Pleroma.Web.ErrorView)
|> Phoenix.Controller.render("404.json")
|> halt()
end
end

View file

@ -0,0 +1,54 @@
defmodule Pleroma.Plugs.InstanceStatic do
@moduledoc """
This is a shim to call `Plug.Static` but with runtime `from` configuration.
Mountpoints are defined directly in the module to avoid calling the configuration for every request including non-static ones.
"""
@behaviour Plug
def file_path(path) do
instance_path =
Path.join(Pleroma.Config.get([:instance, :static_dir], "instance/static/"), path)
if File.exists?(instance_path) do
instance_path
else
Path.join(Application.app_dir(:pleroma, "priv/static/"), path)
end
end
@only ~w(index.html static emoji packs sounds images instance favicon.png)
def init(opts) do
opts
|> Keyword.put(:from, "__unconfigured_instance_static_plug")
|> Keyword.put(:at, "/__unconfigured_instance_static_plug")
|> Plug.Static.init()
end
for only <- @only do
at = Plug.Router.Utils.split("/")
def call(conn = %{request_path: "/" <> unquote(only) <> _}, opts) do
call_static(
conn,
opts,
unquote(at),
Pleroma.Config.get([:instance, :static_dir], "instance/static")
)
end
end
def call(conn, _) do
conn
end
defp call_static(conn, opts, at, from) do
opts =
opts
|> Map.put(:from, from)
|> Map.put(:at, at)
Plug.Static.call(conn, opts)
end
end

View file

@ -838,7 +838,7 @@ def tag(nickname, tags) when is_binary(nickname),
do: tag(User.get_by_nickname(nickname), tags)
def tag(%User{} = user, tags),
do: update_tags(user, Enum.uniq(user.tags ++ normalize_tags(tags)))
do: update_tags(user, Enum.uniq((user.tags || []) ++ normalize_tags(tags)))
def untag(user_identifiers, tags) when is_list(user_identifiers) do
Repo.transaction(fn ->
@ -849,7 +849,8 @@ def untag(user_identifiers, tags) when is_list(user_identifiers) do
def untag(nickname, tags) when is_binary(nickname),
do: untag(User.get_by_nickname(nickname), tags)
def untag(%User{} = user, tags), do: update_tags(user, user.tags -- normalize_tags(tags))
def untag(%User{} = user, tags),
do: update_tags(user, (user.tags || []) -- normalize_tags(tags))
defp update_tags(%User{} = user, new_tags) do
{:ok, updated_user} =

View file

@ -149,9 +149,12 @@ def mastodon_profile_update(info, params) do
])
end
def mastodon_settings_update(info, params) do
def mastodon_settings_update(info, settings) do
params = %{settings: settings}
info
|> cast(params, [:settings])
|> validate_required([:settings])
end
def set_source_data(info, source_data) do

View file

@ -6,10 +6,6 @@ defmodule Pleroma.Web.UserSocket do
# channel "room:*", Pleroma.Web.RoomChannel
channel("chat:*", Pleroma.Web.ChatChannel)
## Transports
transport(:websocket, Phoenix.Transports.WebSocket)
# transport :longpoll, Phoenix.Transports.LongPoll
# Socket params are passed from the client and can
# be used to verify and authenticate a user. After
# verification, you can put default assigns into

View file

@ -3,8 +3,6 @@ defmodule Pleroma.Web.Endpoint do
socket("/socket", Pleroma.Web.UserSocket)
socket("/api/v1", Pleroma.Web.MastodonAPI.MastodonSocket)
# Serve at "/" the static files from "priv/static" directory.
#
# You should set gzip to true if you are running phoenix.digest
@ -14,6 +12,10 @@ defmodule Pleroma.Web.Endpoint do
plug(Pleroma.Plugs.UploadedMedia)
# InstanceStatic needs to be before Plug.Static to be able to override shipped-static files
# If you're adding new paths to `only:` you'll need to configure them in InstanceStatic as well
plug(Pleroma.Plugs.InstanceStatic, at: "/")
plug(
Plug.Static,
at: "/",

View file

@ -226,7 +226,8 @@ def home_timeline(%{assigns: %{user: user}} = conn, params) do
conn
|> add_link_headers(:home_timeline, activities)
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
end
def public_timeline(%{assigns: %{user: user}} = conn, params) do
@ -244,7 +245,8 @@ def public_timeline(%{assigns: %{user: user}} = conn, params) do
conn
|> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
end
def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
@ -259,7 +261,8 @@ def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
conn
|> add_link_headers(:user_statuses, activities, params["id"])
|> render(StatusView, "index.json", %{
|> put_view(StatusView)
|> render("index.json", %{
activities: activities,
for: reading_user,
as: :activity
@ -278,13 +281,16 @@ def dm_timeline(%{assigns: %{user: user}} = conn, params) do
conn
|> add_link_headers(:dm_timeline, activities)
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
end
def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Repo.get(Activity, id),
true <- ActivityPub.visible_for_user?(activity, user) do
try_render(conn, StatusView, "status.json", %{activity: activity, for: user})
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user})
end
end
@ -347,7 +353,9 @@ def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
{:ok, activity} =
Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
try_render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
end
def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
@ -363,28 +371,36 @@ def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
try_render(conn, StatusView, "status.json", %{activity: announce, for: user, as: :activity})
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: announce, for: user, as: :activity})
end
end
def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
try_render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
end
end
def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
try_render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
end
end
def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
try_render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
end
end
@ -433,7 +449,10 @@ def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
id = List.wrap(id)
q = from(u in User, where: u.id in ^id)
targets = Repo.all(q)
render(conn, AccountView, "relationships.json", %{user: user, targets: targets})
conn
|> put_view(AccountView)
|> render("relationships.json", %{user: user, targets: targets})
end
# Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
@ -452,7 +471,10 @@ def update_media(%{assigns: %{user: user}} = conn, data) do
|> Repo.update()
attachment_data = Map.put(new_data, "id", object.id)
render(conn, StatusView, "attachment.json", %{attachment: attachment_data})
conn
|> put_view(StatusView)
|> render("attachment.json", %{attachment: attachment_data})
end
end
@ -463,7 +485,10 @@ def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
description: Map.get(data, "description")
) do
attachment_data = Map.put(object.data, "id", object.id)
render(conn, StatusView, "attachment.json", %{attachment: attachment_data})
conn
|> put_view(StatusView)
|> render("attachment.json", %{attachment: attachment_data})
end
end
@ -471,7 +496,10 @@ def favourited_by(conn, %{"id" => 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)
render(conn, AccountView, "accounts.json", %{users: users, as: :user})
conn
|> put_view(AccountView)
|> render(AccountView, "accounts.json", %{users: users, as: :user})
else
_ -> json(conn, [])
end
@ -481,7 +509,10 @@ def reblogged_by(conn, %{"id" => id}) do
with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
q = from(u in User, where: u.ap_id in ^announces)
users = Repo.all(q)
render(conn, AccountView, "accounts.json", %{users: users, as: :user})
conn
|> put_view(AccountView)
|> render("accounts.json", %{users: users, as: :user})
else
_ -> json(conn, [])
end
@ -503,7 +534,8 @@ def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
conn
|> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
end
def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
@ -516,7 +548,9 @@ def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
true -> followers
end
render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
conn
|> put_view(AccountView)
|> render("accounts.json", %{users: followers, as: :user})
end
end
@ -530,13 +564,17 @@ def following(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
true -> followers
end
render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
conn
|> put_view(AccountView)
|> render("accounts.json", %{users: followers, as: :user})
end
end
def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
with {:ok, follow_requests} <- User.get_follow_requests(followed) do
render(conn, AccountView, "accounts.json", %{users: follow_requests, as: :user})
conn
|> put_view(AccountView)
|> render("accounts.json", %{users: follow_requests, as: :user})
end
end
@ -552,7 +590,9 @@ def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}
object: follow_activity.data["id"],
type: "Accept"
}) do
render(conn, AccountView, "relationship.json", %{user: followed, target: follower})
conn
|> put_view(AccountView)
|> render("relationship.json", %{user: followed, target: follower})
else
{:error, message} ->
conn
@ -572,7 +612,9 @@ def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) d
object: follow_activity.data["id"],
type: "Reject"
}) do
render(conn, AccountView, "relationship.json", %{user: followed, target: follower})
conn
|> put_view(AccountView)
|> render("relationship.json", %{user: followed, target: follower})
else
{:error, message} ->
conn
@ -591,7 +633,9 @@ def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
follower,
followed
) do
render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
conn
|> put_view(AccountView)
|> render("relationship.json", %{user: follower, target: followed})
else
{:error, message} ->
conn
@ -604,7 +648,9 @@ def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
with %User{} = followed <- Repo.get_by(User, nickname: uri),
{:ok, follower} <- User.maybe_direct_follow(follower, followed),
{:ok, _activity} <- ActivityPub.follow(follower, followed) do
render(conn, AccountView, "account.json", %{user: followed, for: follower})
conn
|> put_view(AccountView)
|> render("account.json", %{user: followed, for: follower})
else
{:error, message} ->
conn
@ -617,7 +663,9 @@ def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
with %User{} = followed <- Repo.get(User, id),
{:ok, _activity} <- ActivityPub.unfollow(follower, followed),
{:ok, follower, _} <- User.unfollow(follower, followed) do
render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
conn
|> put_view(AccountView)
|> render("relationship.json", %{user: follower, target: followed})
end
end
@ -625,7 +673,9 @@ def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
with %User{} = blocked <- Repo.get(User, id),
{:ok, blocker} <- User.block(blocker, blocked),
{:ok, _activity} <- ActivityPub.block(blocker, blocked) do
render(conn, AccountView, "relationship.json", %{user: blocker, target: blocked})
conn
|> put_view(AccountView)
|> render("relationship.json", %{user: blocker, target: blocked})
else
{:error, message} ->
conn
@ -638,7 +688,9 @@ def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
with %User{} = blocked <- Repo.get(User, id),
{:ok, blocker} <- User.unblock(blocker, blocked),
{:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
render(conn, AccountView, "relationship.json", %{user: blocker, target: blocked})
conn
|> put_view(AccountView)
|> render("relationship.json", %{user: blocker, target: blocked})
else
{:error, message} ->
conn
@ -763,7 +815,8 @@ def favourites(%{assigns: %{user: user}} = conn, _) do
|> Enum.reverse()
conn
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
end
def get_lists(%{assigns: %{user: user}} = conn, opts) do
@ -831,7 +884,9 @@ def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_id
def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
{:ok, users} = Pleroma.List.get_following(list) do
render(conn, AccountView, "accounts.json", %{users: users, as: :user})
conn
|> put_view(AccountView)
|> render("accounts.json", %{users: users, as: :user})
end
end
@ -864,7 +919,8 @@ def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params)
|> Enum.reverse()
conn
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
else
_e ->
conn
@ -929,7 +985,7 @@ def index(%{assigns: %{user: user}} = conn, _params) do
]
},
settings:
Map.get(user.info, :settings) ||
user.info.settings ||
%{
onboarded: true,
home: %{
@ -968,7 +1024,8 @@ def index(%{assigns: %{user: user}} = conn, _params) do
conn
|> put_layout(false)
|> render(MastodonView, "index.html", %{initial_state: initial_state})
|> put_view(MastodonView)
|> render("index.html", %{initial_state: initial_state})
else
conn
|> redirect(to: "/web/login")
@ -978,13 +1035,15 @@ def index(%{assigns: %{user: user}} = conn, _params) do
def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
info_cng = User.Info.mastodon_settings_update(user.info, settings)
with changeset <- User.update_changeset(user),
with changeset <- Ecto.Changeset.change(user),
changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
{:ok, _user} <- User.update_and_set_cache(changeset) do
json(conn, %{})
else
e ->
json(conn, %{error: inspect(e)})
conn
|> put_resp_content_type("application/json")
|> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
end
end
@ -1039,7 +1098,9 @@ def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
Logger.debug("Unimplemented, returning unmodified relationship")
with %User{} = target <- Repo.get(User, id) do
render(conn, AccountView, "relationship.json", %{user: user, target: target})
conn
|> put_view(AccountView)
|> render("relationship.json", %{user: user, target: target})
end
end
@ -1240,9 +1301,9 @@ def suggestions(%{assigns: %{user: user}} = conn, _) do
end
end
def try_render(conn, renderer, target, params)
def try_render(conn, target, params)
when is_binary(target) do
res = render(conn, renderer, target, params)
res = render(conn, target, params)
if res == nil do
conn
@ -1253,7 +1314,7 @@ def try_render(conn, renderer, target, params)
end
end
def try_render(conn, _, _, _) do
def try_render(conn, _, _) do
conn
|> put_status(501)
|> json(%{error: "Can't display this activity"})

View file

@ -1,81 +0,0 @@
defmodule Pleroma.Web.MastodonAPI.MastodonSocket do
use Phoenix.Socket
alias Pleroma.Web.OAuth.Token
alias Pleroma.{User, Repo}
transport(
:websocket,
Phoenix.Transports.WebSocket.Raw,
# We never receive data.
timeout: :infinity
)
@spec connect(params :: map(), Phoenix.Socket.t()) :: {:ok, Phoenix.Socket.t()} | :error
def connect(%{"access_token" => token} = params, socket) do
with %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",
"public:media",
"public:local:media",
"user",
"direct",
"list",
"hashtag"
] <- params["stream"] do
topic =
case stream do
"hashtag" -> "hashtag:#{params["tag"]}"
"list" -> "list:#{params["list"]}"
_ -> stream
end
socket =
socket
|> assign(:topic, topic)
|> assign(:user, user)
Pleroma.Web.Streamer.add_socket(topic, socket)
{:ok, socket}
else
_e -> :error
end
end
def connect(%{"stream" => stream} = params, socket)
when stream in ["public", "public:local", "hashtag"] do
topic =
case stream do
"hashtag" -> "hashtag:#{params["tag"]}"
_ -> stream
end
socket =
socket
|> assign(:topic, topic)
Pleroma.Web.Streamer.add_socket(topic, socket)
{:ok, socket}
end
def connect(_params, _socket), do: :error
def id(_), do: nil
def handle(:text, message, _state) do
# | :ok
# | state
# | {:text, message}
# | {:text, message, state}
# | {:close, "Goodbye!"}
{:text, message}
end
def handle(:closed, _, %{socket: socket}) do
topic = socket.assigns[:topic]
Pleroma.Web.Streamer.remove_socket(topic, socket)
end
end

View file

@ -0,0 +1,120 @@
defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
require Logger
alias Pleroma.Web.OAuth.Token
alias Pleroma.{User, Repo}
@behaviour :cowboy_websocket_handler
@streams [
"public",
"public:local",
"public:media",
"public:local:media",
"user",
"direct",
"list",
"hashtag"
]
@anonymous_streams ["public", "public:local", "hashtag"]
# Handled by periodic keepalive in Pleroma.Web.Streamer.
@timeout :infinity
def init(_type, _req, _opts) do
{:upgrade, :protocol, :cowboy_websocket}
end
def websocket_init(_type, req, _opts) do
with {qs, req} <- :cowboy_req.qs(req),
params <- :cow_qs.parse_qs(qs),
access_token <- List.keyfind(params, "access_token", 0),
{_, stream} <- List.keyfind(params, "stream", 0),
{:ok, user} <- allow_request(stream, access_token),
topic when is_binary(topic) <- expand_topic(stream, params) do
send(self(), :subscribe)
{:ok, req, %{user: user, topic: topic}, @timeout}
else
{:error, code} ->
Logger.debug("#{__MODULE__} denied connection: #{inspect(code)} - #{inspect(req)}")
{:ok, req} = :cowboy_req.reply(code, req)
{:shutdown, req}
error ->
Logger.debug("#{__MODULE__} denied connection: #{inspect(error)} - #{inspect(req)}")
{:shutdown, req}
end
end
# We never receive messages.
def websocket_handle(_frame, req, state) do
{:ok, req, state}
end
def websocket_info(:subscribe, req, state) do
Logger.debug(
"#{__MODULE__} accepted websocket connection for user #{
(state.user || %{id: "anonymous"}).id
}, topic #{state.topic}"
)
Pleroma.Web.Streamer.add_socket(state.topic, streamer_socket(state))
{:ok, req, state}
end
def websocket_info({:text, message}, req, state) do
{:reply, {:text, message}, req, state}
end
def websocket_terminate(reason, _req, state) do
Logger.debug(
"#{__MODULE__} terminating websocket connection for user #{
(state.user || %{id: "anonymous"}).id
}, topic #{state.topic || "?"}: #{inspect(reason)}"
)
Pleroma.Web.Streamer.remove_socket(state.topic, streamer_socket(state))
:ok
end
# Public streams without authentication.
defp allow_request(stream, nil) when stream in @anonymous_streams do
{:ok, nil}
end
# Authenticated streams.
defp allow_request(stream, {"access_token", access_token}) when stream in @streams do
with %Token{user_id: user_id} <- Repo.get_by(Token, token: access_token),
user = %User{} <- Repo.get(User, user_id) do
{:ok, user}
else
_ -> {:error, 403}
end
end
# Not authenticated.
defp allow_request(stream, _) when stream in @streams, do: {:error, 403}
# No matching stream.
defp allow_request(_, _), do: {:error, 404}
defp expand_topic("hashtag", params) do
case List.keyfind(params, "tag", 0) do
{_, tag} -> "hashtag:#{tag}"
_ -> nil
end
end
defp expand_topic("list", params) do
case List.keyfind(params, "list", 0) do
{_, list} -> "list:#{list}"
_ -> nil
end
end
defp expand_topic(topic, _), do: topic
defp streamer_socket(state) do
%{transport_pid: self(), assigns: state}
end
end

View file

@ -136,7 +136,7 @@ def notice(conn, %{"id" => id}) do
"html" ->
conn
|> put_resp_content_type("text/html")
|> send_file(200, Application.app_dir(:pleroma, "priv/static/index.html"))
|> send_file(200, Pleroma.Plugs.InstanceStatic.file_path("index.html"))
_ ->
represent_activity(conn, format, activity, user)

View file

@ -99,6 +99,7 @@ defmodule Pleroma.Web.Router do
get("/password_reset/:token", UtilController, :show_password_reset)
post("/password_reset", UtilController, :password_reset)
get("/emoji", UtilController, :emoji)
get("/captcha", UtilController, :captcha)
end
scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do
@ -458,7 +459,7 @@ defmodule Fallback.RedirectController do
def redirector(conn, _params) do
conn
|> put_resp_content_type("text/html")
|> send_file(200, Application.app_dir(:pleroma, "priv/static/index.html"))
|> send_file(200, Pleroma.Plugs.InstanceStatic.file_path("index.html"))
end
def registration_page(conn, params) do

View file

@ -4,17 +4,9 @@ defmodule Pleroma.Web.Streamer do
alias Pleroma.{User, Notification, Activity, Object, Repo}
alias Pleroma.Web.ActivityPub.ActivityPub
def init(args) do
{:ok, args}
end
@keepalive_interval :timer.seconds(30)
def start_link do
spawn(fn ->
# 30 seconds
Process.sleep(1000 * 30)
GenServer.cast(__MODULE__, %{action: :ping})
end)
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
end
@ -30,6 +22,16 @@ def stream(topic, item) do
GenServer.cast(__MODULE__, %{action: :stream, topic: topic, item: item})
end
def init(args) do
spawn(fn ->
# 30 seconds
Process.sleep(@keepalive_interval)
GenServer.cast(__MODULE__, %{action: :ping})
end)
{:ok, args}
end
def handle_cast(%{action: :ping}, topics) do
Map.values(topics)
|> List.flatten()
@ -40,7 +42,7 @@ def handle_cast(%{action: :ping}, topics) do
spawn(fn ->
# 30 seconds
Process.sleep(1000 * 30)
Process.sleep(@keepalive_interval)
GenServer.cast(__MODULE__, %{action: :ping})
end)

View file

@ -284,4 +284,8 @@ def delete_account(%{assigns: %{user: user}} = conn, params) do
json(conn, %{error: msg})
end
end
def captcha(conn, _params) do
json(conn, Pleroma.Captcha.new())
end
end

View file

@ -132,38 +132,55 @@ def register_user(params) do
bio: User.parse_bio(params["bio"]),
email: params["email"],
password: params["password"],
password_confirmation: params["confirm"]
password_confirmation: params["confirm"],
captcha_solution: params["captcha_solution"],
captcha_token: params["captcha_token"]
}
registrations_open = Pleroma.Config.get([:instance, :registrations_open])
# no need to query DB if registration is open
token =
unless registrations_open || is_nil(tokenString) do
Repo.get_by(UserInviteToken, %{token: tokenString})
captcha_enabled = Pleroma.Config.get([Pleroma.Captcha, :enabled])
# true if captcha is disabled or enabled and valid, false otherwise
captcha_ok =
if !captcha_enabled do
true
else
Pleroma.Captcha.validate(params[:captcha_token], params[:captcha_solution])
end
cond do
registrations_open || (!is_nil(token) && !token.used) ->
changeset = User.register_changeset(%User{info: %{}}, params)
# Captcha invalid
if not captcha_ok do
# I have no idea how this error handling works
{:error, %{error: Jason.encode!(%{captcha: ["Invalid CAPTCHA"]})}}
else
registrations_open = Pleroma.Config.get([:instance, :registrations_open])
with {:ok, user} <- Repo.insert(changeset) do
!registrations_open && UserInviteToken.mark_as_used(token.token)
{:ok, user}
else
{:error, changeset} ->
errors =
Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
|> Jason.encode!()
{:error, %{error: errors}}
# no need to query DB if registration is open
token =
unless registrations_open || is_nil(tokenString) do
Repo.get_by(UserInviteToken, %{token: tokenString})
end
!registrations_open && is_nil(token) ->
{:error, "Invalid token"}
cond do
registrations_open || (!is_nil(token) && !token.used) ->
changeset = User.register_changeset(%User{info: %{}}, params)
!registrations_open && token.used ->
{:error, "Expired token"}
with {:ok, user} <- Repo.insert(changeset) do
!registrations_open && UserInviteToken.mark_as_used(token.token)
{:ok, user}
else
{:error, changeset} ->
errors =
Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
|> Jason.encode!()
{:error, %{error: errors}}
end
!registrations_open && is_nil(token) ->
{:error, "Invalid token"}
!registrations_open && token.used ->
{:error, "Expired token"}
end
end
end

View file

@ -17,7 +17,10 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
def verify_credentials(%{assigns: %{user: user}} = conn, _params) do
token = Phoenix.Token.sign(conn, "user socket", user.id)
render(conn, UserView, "show.json", %{user: user, token: token})
conn
|> put_view(UserView)
|> render("show.json", %{user: user, token: token})
end
def status_update(%{assigns: %{user: user}} = conn, %{"status" => _} = status_data) do
@ -58,7 +61,8 @@ def public_and_external_timeline(%{assigns: %{user: user}} = conn, params) do
activities = ActivityPub.fetch_public_activities(params)
conn
|> render(ActivityView, "index.json", %{activities: activities, for: user})
|> put_view(ActivityView)
|> render("index.json", %{activities: activities, for: user})
end
def public_timeline(%{assigns: %{user: user}} = conn, params) do
@ -71,7 +75,8 @@ def public_timeline(%{assigns: %{user: user}} = conn, params) do
activities = ActivityPub.fetch_public_activities(params)
conn
|> render(ActivityView, "index.json", %{activities: activities, for: user})
|> put_view(ActivityView)
|> render("index.json", %{activities: activities, for: user})
end
def friends_timeline(%{assigns: %{user: user}} = conn, params) do
@ -86,16 +91,22 @@ def friends_timeline(%{assigns: %{user: user}} = conn, params) do
|> ActivityPub.contain_timeline(user)
conn
|> render(ActivityView, "index.json", %{activities: activities, for: user})
|> put_view(ActivityView)
|> render("index.json", %{activities: activities, for: user})
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
params =
if user = conn.assigns.user do
%{user: shown, for: user}
else
%{user: shown}
end
conn
|> put_view(UserView)
|> render("show.json", params)
else
{:error, msg} ->
bad_request_reply(conn, msg)
@ -108,7 +119,8 @@ def user_timeline(%{assigns: %{user: user}} = conn, params) do
activities = ActivityPub.fetch_user_activities(target_user, user, params)
conn
|> render(ActivityView, "index.json", %{activities: activities, for: user})
|> put_view(ActivityView)
|> render("index.json", %{activities: activities, for: user})
{:error, msg} ->
bad_request_reply(conn, msg)
@ -124,7 +136,8 @@ def mentions_timeline(%{assigns: %{user: user}} = conn, params) do
activities = ActivityPub.fetch_activities([user.ap_id], params)
conn
|> render(ActivityView, "index.json", %{activities: activities, for: user})
|> put_view(ActivityView)
|> render("index.json", %{activities: activities, for: user})
end
def dm_timeline(%{assigns: %{user: user}} = conn, params) do
@ -137,14 +150,16 @@ def dm_timeline(%{assigns: %{user: user}} = conn, params) do
activities = Repo.all(query)
conn
|> render(ActivityView, "index.json", %{activities: activities, for: user})
|> put_view(ActivityView)
|> render("index.json", %{activities: activities, for: user})
end
def notifications(%{assigns: %{user: user}} = conn, params) do
notifications = Notification.for_user(user, params)
conn
|> render(NotificationView, "notification.json", %{notifications: notifications, for: user})
|> put_view(NotificationView)
|> render("notification.json", %{notifications: notifications, for: user})
end
def notifications_read(%{assigns: %{user: user}} = conn, %{"latest_id" => latest_id} = params) do
@ -153,7 +168,8 @@ def notifications_read(%{assigns: %{user: user}} = conn, %{"latest_id" => latest
notifications = Notification.for_user(user, params)
conn
|> render(NotificationView, "notification.json", %{notifications: notifications, for: user})
|> put_view(NotificationView)
|> render("notification.json", %{notifications: notifications, for: user})
end
def notifications_read(%{assigns: %{user: _user}} = conn, _) do
@ -163,7 +179,9 @@ def notifications_read(%{assigns: %{user: _user}} = conn, _) do
def follow(%{assigns: %{user: user}} = conn, params) do
case TwitterAPI.follow(user, params) do
{:ok, user, followed, _activity} ->
render(conn, UserView, "show.json", %{user: followed, for: user})
conn
|> put_view(UserView)
|> render("show.json", %{user: followed, for: user})
{:error, msg} ->
forbidden_json_reply(conn, msg)
@ -173,7 +191,9 @@ def follow(%{assigns: %{user: user}} = conn, params) do
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})
conn
|> put_view(UserView)
|> render("show.json", %{user: blocked, for: user})
{:error, msg} ->
forbidden_json_reply(conn, msg)
@ -183,7 +203,9 @@ def block(%{assigns: %{user: user}} = conn, params) do
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})
conn
|> put_view(UserView)
|> render("show.json", %{user: blocked, for: user})
{:error, msg} ->
forbidden_json_reply(conn, msg)
@ -192,14 +214,18 @@ def unblock(%{assigns: %{user: user}} = conn, params) do
def delete_post(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {:ok, activity} <- TwitterAPI.delete(user, id) do
render(conn, ActivityView, "activity.json", %{activity: activity, for: user})
conn
|> put_view(ActivityView)
|> render("activity.json", %{activity: activity, for: user})
end
end
def unfollow(%{assigns: %{user: user}} = conn, params) do
case TwitterAPI.unfollow(user, params) do
{:ok, user, unfollowed} ->
render(conn, UserView, "show.json", %{user: unfollowed, for: user})
conn
|> put_view(UserView)
|> render("show.json", %{user: unfollowed, for: user})
{:error, msg} ->
forbidden_json_reply(conn, msg)
@ -209,7 +235,9 @@ def unfollow(%{assigns: %{user: user}} = conn, params) do
def fetch_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Repo.get(Activity, id),
true <- ActivityPub.visible_for_user?(activity, user) do
render(conn, ActivityView, "activity.json", %{activity: activity, for: user})
conn
|> put_view(ActivityView)
|> render("activity.json", %{activity: activity, for: user})
end
end
@ -223,7 +251,8 @@ def fetch_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
"user" => user
}) do
conn
|> render(ActivityView, "index.json", %{activities: activities, for: user})
|> put_view(ActivityView)
|> render("index.json", %{activities: activities, for: user})
end
end
@ -290,34 +319,44 @@ def get_by_id_or_ap_id(id) do
def favorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)},
{:ok, activity} <- TwitterAPI.fav(user, id) do
render(conn, ActivityView, "activity.json", %{activity: activity, for: user})
conn
|> put_view(ActivityView)
|> render("activity.json", %{activity: activity, for: user})
end
end
def unfavorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)},
{:ok, activity} <- TwitterAPI.unfav(user, id) do
render(conn, ActivityView, "activity.json", %{activity: activity, for: user})
conn
|> put_view(ActivityView)
|> render("activity.json", %{activity: activity, for: user})
end
end
def retweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)},
{:ok, activity} <- TwitterAPI.repeat(user, id) do
render(conn, ActivityView, "activity.json", %{activity: activity, for: user})
conn
|> put_view(ActivityView)
|> render("activity.json", %{activity: activity, for: user})
end
end
def unretweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)},
{:ok, activity} <- TwitterAPI.unrepeat(user, id) do
render(conn, ActivityView, "activity.json", %{activity: activity, for: user})
conn
|> put_view(ActivityView)
|> render("activity.json", %{activity: activity, for: user})
end
end
def register(conn, params) do
with {:ok, user} <- TwitterAPI.register_user(params) do
render(conn, UserView, "show.json", %{user: user})
conn
|> put_view(UserView)
|> render("show.json", %{user: user})
else
{:error, errors} ->
conn
@ -339,7 +378,9 @@ def update_avatar(%{assigns: %{user: user}} = conn, params) do
{:ok, user} = User.update_and_set_cache(change)
CommonAPI.update(user)
render(conn, UserView, "show.json", %{user: user, for: user})
conn
|> put_view(UserView)
|> render("show.json", %{user: user, for: user})
end
def update_banner(%{assigns: %{user: user}} = conn, params) do
@ -394,7 +435,9 @@ def followers(%{assigns: %{user: for_user}} = conn, params) do
true -> followers
end
render(conn, UserView, "index.json", %{users: followers, for: conn.assigns[:user]})
conn
|> put_view(UserView)
|> render("index.json", %{users: followers, for: conn.assigns[:user]})
else
_e -> bad_request_reply(conn, "Can't get followers")
end
@ -410,7 +453,9 @@ def friends(%{assigns: %{user: for_user}} = conn, params) do
true -> friends
end
render(conn, UserView, "index.json", %{users: friends, for: conn.assigns[:user]})
conn
|> put_view(UserView)
|> render("index.json", %{users: friends, for: conn.assigns[:user]})
else
_e -> bad_request_reply(conn, "Can't get friends")
end
@ -419,7 +464,9 @@ def friends(%{assigns: %{user: for_user}} = conn, params) do
def friend_requests(conn, params) do
with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
{:ok, friend_requests} <- User.get_follow_requests(user) do
render(conn, UserView, "index.json", %{users: friend_requests, for: conn.assigns[:user]})
conn
|> put_view(UserView)
|> render("index.json", %{users: friend_requests, for: conn.assigns[:user]})
else
_e -> bad_request_reply(conn, "Can't get friend requests")
end
@ -439,7 +486,9 @@ def approve_friend_request(conn, %{"user_id" => uid} = _params) do
object: follow_activity.data["id"],
type: "Accept"
}) do
render(conn, UserView, "show.json", %{user: follower, for: followed})
conn
|> put_view(UserView)
|> render("show.json", %{user: follower, for: followed})
else
e -> bad_request_reply(conn, "Can't approve user: #{inspect(e)}")
end
@ -458,7 +507,9 @@ def deny_friend_request(conn, %{"user_id" => uid} = _params) do
object: follow_activity.data["id"],
type: "Reject"
}) do
render(conn, UserView, "show.json", %{user: follower, for: followed})
conn
|> put_view(UserView)
|> render("show.json", %{user: follower, for: followed})
else
e -> bad_request_reply(conn, "Can't deny user: #{inspect(e)}")
end
@ -522,7 +573,10 @@ def update_profile(%{assigns: %{user: user}} = conn, params) do
changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
{:ok, user} <- User.update_and_set_cache(changeset) do
CommonAPI.update(user)
render(conn, UserView, "user.json", %{user: user, for: user})
conn
|> put_view(UserView)
|> render("user.json", %{user: user, for: user})
else
error ->
Logger.debug("Can't update user: #{inspect(error)}")
@ -534,14 +588,16 @@ def search(%{assigns: %{user: user}} = conn, %{"q" => _query} = params) do
activities = TwitterAPI.search(user, params)
conn
|> render(ActivityView, "index.json", %{activities: activities, for: user})
|> put_view(ActivityView)
|> render("index.json", %{activities: activities, for: user})
end
def search_user(%{assigns: %{user: user}} = conn, %{"query" => query}) do
users = User.search(query, true)
conn
|> render(UserView, "index.json", %{users: users, for: user})
|> put_view(UserView)
|> render("index.json", %{users: users, for: user})
end
defp bad_request_reply(conn, error_message) do

View file

@ -244,6 +244,11 @@ def render(
|> HTML.filter_tags(User.html_filter_policy(opts[:for]))
|> Formatter.emojify(object["emoji"])
text =
content
|> String.replace(~r/<br\s?\/?>/, "\n")
|> HTML.strip_tags()
reply_parent = Activity.get_in_reply_to_activity(activity)
reply_user = reply_parent && User.get_cached_by_ap_id(reply_parent.actor)
@ -253,7 +258,7 @@ def render(
"uri" => activity.data["object"]["id"],
"user" => UserView.render("show.json", %{user: user, for: opts[:for]}),
"statusnet_html" => html,
"text" => HTML.strip_tags(content),
"text" => text,
"is_local" => activity.local,
"is_post_verb" => true,
"created_at" => created_at,

View file

@ -86,7 +86,7 @@ def render("user.json", %{user: user = %User{}} = assigns) do
}
if assigns[:token] do
Map.put(data, "token", assigns[:token])
Map.put(data, "token", token_string(assigns[:token]))
else
data
end
@ -111,4 +111,7 @@ def render("short.json", %{
defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
defp image_url(_), do: nil
defp token_string(%Pleroma.Web.OAuth.Token{token: token_str}), do: token_str
defp token_string(token), do: token
end

View file

@ -35,4 +35,8 @@ def webfinger(conn, %{"resource" => resource}) do
send_resp(conn, 404, "Unsupported format")
end
end
def webfinger(conn, _params) do
send_resp(conn, 400, "Bad Request")
end
end

10
mix.exs
View file

@ -43,12 +43,13 @@ defp elixirc_paths(_), do: ["lib"]
# Type `mix help deps` for examples and options.
defp deps do
[
{:phoenix, "~> 1.3.3"},
{:phoenix_pubsub, "~> 1.0.2"},
# Until Phoenix 1.4.1 is released
{:phoenix, github: "phoenixframework/phoenix", branch: "v1.4"},
{:plug_cowboy, "~> 1.0"},
{:phoenix_pubsub, "~> 1.1"},
{:phoenix_ecto, "~> 3.3"},
{:postgrex, ">= 0.13.5"},
{:gettext, "~> 0.15"},
{:cowboy, "~> 1.1.2", override: true},
{:comeonin, "~> 4.1.1"},
{:pbkdf2_elixir, "~> 0.12.3"},
{:trailing_format_plug, "~> 0.0.7"},
@ -72,7 +73,8 @@ defp deps do
{:ex_doc, "> 0.18.3 and < 0.20.0", only: :dev, runtime: false},
{:web_push_encryption, "~> 0.2.1"},
{:swoosh, "~> 0.20"},
{:gen_smtp, "~> 0.13"}
{:gen_smtp, "~> 0.13"},
{:websocket_client, git: "https://github.com/jeremyong/websocket_client.git", only: :test}
]
end

View file

@ -32,7 +32,7 @@
"makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
"meck": {:hex, :meck, "0.8.9", "64c5c0bd8bcca3a180b44196265c8ed7594e16bcc845d0698ec6b4e577f48188", [:rebar3], [], "hexpm"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},
"mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm"},
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"},
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"},
"mochiweb": {:hex, :mochiweb, "2.15.0", "e1daac474df07651e5d17cc1e642c4069c7850dc4508d3db7263a0651330aacc", [:rebar3], [], "hexpm"},
"mock": {:hex, :mock, "0.3.1", "994f00150f79a0ea50dc9d86134cd9ebd0d177ad60bd04d1e46336cdfdb98ff9", [:mix], [{:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"},
@ -40,11 +40,13 @@
"nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm"},
"parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm"},
"pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.3", "6706a148809a29c306062862c803406e88f048277f6e85b68faf73291e820b84", [:mix], [], "hexpm"},
"phoenix": {:hex, :phoenix, "1.3.4", "aaa1b55e5523083a877bcbe9886d9ee180bf2c8754905323493c2ac325903dc5", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix": {:git, "https://github.com/phoenixframework/phoenix.git", "ea22dc50b574178a300ecd19253443960407df93", [branch: "v1.4"]},
"phoenix_ecto": {:hex, :phoenix_ecto, "3.3.0", "702f6e164512853d29f9d20763493f2b3bcfcb44f118af2bc37bb95d0801b480", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_html": {:hex, :phoenix_html, "2.11.2", "86ebd768258ba60a27f5578bec83095bdb93485d646fc4111db8844c316602d6", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.2", "bfa7fd52788b5eaa09cb51ff9fcad1d9edfeb68251add458523f839392f034c1", [:mix], [], "hexpm"},
"plug": {:hex, :plug, "1.6.2", "e06a7bd2bb6de5145da0dd950070110dce88045351224bd98e84edfdaaf5ffee", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.1", "6668d787e602981f24f17a5fbb69cc98f8ab085114ebfac6cc36e10a90c8e93c", [:mix], [], "hexpm"},
"plug": {:hex, :plug, "1.7.1", "8516d565fb84a6a8b2ca722e74e2cd25ca0fc9d64f364ec9dbec09d33eb78ccd", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}], "hexpm"},
"plug_cowboy": {:hex, :plug_cowboy, "1.0.0", "2e2a7d3409746d335f451218b8bb0858301c3de6d668c3052716c909936eb57a", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"},
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
"poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"},
"postgrex": {:hex, :postgrex, "0.13.5", "3d931aba29363e1443da167a4b12f06dcd171103c424de15e5f3fc2ba3e6d9c5", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"},
@ -57,4 +59,5 @@
"unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"},
"unsafe": {:hex, :unsafe, "1.0.0", "7c21742cd05380c7875546b023481d3a26f52df8e5dfedcb9f958f322baae305", [:mix], [], "hexpm"},
"web_push_encryption": {:hex, :web_push_encryption, "0.2.1", "d42cecf73420d9dc0053ba3299cc8c8d6ff2be2487d67ca2a57265868e4d9a98", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:poison, "~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
"websocket_client": {:git, "https://github.com/jeremyong/websocket_client.git", "9a6f65d05ebf2725d62fb19262b21f1805a59fbf", []},
}

View file

@ -1 +1 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Pleroma</title><link rel=icon type=image/png href=/favicon.png><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/app.ea00efb3229c8591fcc5249f0241b986.css rel=stylesheet></head><body style="display: none"><div id=app></div><script type=text/javascript src=/static/js/manifest.e076977b8e6c6844fb00.js></script><script type=text/javascript src=/static/js/vendor.cc4190750f6ed4d697bd.js></script><script type=text/javascript src=/static/js/app.67f548ecb9e9fd6b25b0.js></script></body></html>
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Pleroma</title><!--server-generated-meta--><link rel=icon type=image/png href=/favicon.png><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/app.285fa56c62b811bbd37880f7e2656b13.css rel=stylesheet></head><body style="display: none"><div id=app></div><script type=text/javascript src=/static/js/manifest.60e190da7cc4cc4711dc.js></script><script type=text/javascript src=/static/js/vendor.48d4753220bd83360796.js></script><script type=text/javascript src=/static/js/app.6cb7378f44092df9536a.js></script></body></html>

View file

@ -4,8 +4,8 @@
"logo": "/static/logo.png",
"logoMask": true,
"logoMargin": ".1em",
"redirectRootNoLogin": "/main/all",
"redirectRootLogin": "/main/friends",
"redirectRootNoLogin": "/~/main/all",
"redirectRootLogin": "/~/main/friends",
"chatDisabled": false,
"showInstanceSpecificPanel": false,
"scopeOptionsEnabled": false,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,4 +1,5 @@
{
"name": "",
"css_prefix_text": "icon-",
"css_use_suffix": false,
"hinting": true,
@ -50,7 +51,7 @@
{
"uid": "09feb4465d9bd1364f4e301c9ddbaa92",
"css": "retweet",
"code": 59398,
"code": 59396,
"src": "fontawesome"
},
{
@ -185,10 +186,16 @@
"code": 59411,
"src": "iconic"
},
{
"uid": "9dd9e835aebe1060ba7190ad2b2ed951",
"css": "search",
"code": 59398,
"src": "fontawesome"
},
{
"uid": "ca90da02d2c6a3183f2458e4dc416285",
"css": "adjust",
"code": 59396,
"code": 59414,
"src": "fontawesome"
},
{

View file

@ -3,9 +3,9 @@ .icon-cancel:before { content: '\e800'; } /* '' */
.icon-upload:before { content: '\e801'; } /* '' */
.icon-star:before { content: '\e802'; } /* '' */
.icon-star-empty:before { content: '\e803'; } /* '' */
.icon-adjust:before { content: '\e804'; } /* '' */
.icon-retweet:before { content: '\e804'; } /* '' */
.icon-eye-off:before { content: '\e805'; } /* '' */
.icon-retweet:before { content: '\e806'; } /* '' */
.icon-search:before { content: '\e806'; } /* '' */
.icon-cog:before { content: '\e807'; } /* '' */
.icon-logout:before { content: '\e808'; } /* '' */
.icon-down-open:before { content: '\e809'; } /* '' */
@ -21,6 +21,7 @@ .icon-globe:before { content: '\e812'; } /* '' */
.icon-brush:before { content: '\e813'; } /* '' */
.icon-attention:before { content: '\e814'; } /* '' */
.icon-plus:before { content: '\e815'; } /* '' */
.icon-adjust:before { content: '\e816'; } /* '' */
.icon-spin3:before { content: '\e832'; } /* '' */
.icon-spin4:before { content: '\e834'; } /* '' */
.icon-link-ext:before { content: '\f08e'; } /* '' */

File diff suppressed because one or more lines are too long

View file

@ -3,9 +3,9 @@ .icon-cancel { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTM
.icon-upload { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe801;&nbsp;'); }
.icon-star { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe802;&nbsp;'); }
.icon-star-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe803;&nbsp;'); }
.icon-adjust { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe804;&nbsp;'); }
.icon-retweet { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe804;&nbsp;'); }
.icon-eye-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe805;&nbsp;'); }
.icon-retweet { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe806;&nbsp;'); }
.icon-search { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe806;&nbsp;'); }
.icon-cog { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe807;&nbsp;'); }
.icon-logout { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe808;&nbsp;'); }
.icon-down-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe809;&nbsp;'); }
@ -21,6 +21,7 @@ .icon-globe { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML
.icon-brush { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe813;&nbsp;'); }
.icon-attention { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe814;&nbsp;'); }
.icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe815;&nbsp;'); }
.icon-adjust { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe816;&nbsp;'); }
.icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe832;&nbsp;'); }
.icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe834;&nbsp;'); }
.icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08e;&nbsp;'); }

View file

@ -14,9 +14,9 @@ .icon-cancel { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTM
.icon-upload { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe801;&nbsp;'); }
.icon-star { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe802;&nbsp;'); }
.icon-star-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe803;&nbsp;'); }
.icon-adjust { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe804;&nbsp;'); }
.icon-retweet { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe804;&nbsp;'); }
.icon-eye-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe805;&nbsp;'); }
.icon-retweet { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe806;&nbsp;'); }
.icon-search { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe806;&nbsp;'); }
.icon-cog { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe807;&nbsp;'); }
.icon-logout { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe808;&nbsp;'); }
.icon-down-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe809;&nbsp;'); }
@ -32,6 +32,7 @@ .icon-globe { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML
.icon-brush { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe813;&nbsp;'); }
.icon-attention { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe814;&nbsp;'); }
.icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe815;&nbsp;'); }
.icon-adjust { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe816;&nbsp;'); }
.icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe832;&nbsp;'); }
.icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe834;&nbsp;'); }
.icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08e;&nbsp;'); }

View file

@ -1,11 +1,11 @@
@font-face {
font-family: 'fontello';
src: url('../font/fontello.eot?3996201');
src: url('../font/fontello.eot?3996201#iefix') format('embedded-opentype'),
url('../font/fontello.woff2?3996201') format('woff2'),
url('../font/fontello.woff?3996201') format('woff'),
url('../font/fontello.ttf?3996201') format('truetype'),
url('../font/fontello.svg?3996201#fontello') format('svg');
src: url('../font/fontello.eot?97335193');
src: url('../font/fontello.eot?97335193#iefix') format('embedded-opentype'),
url('../font/fontello.woff2?97335193') format('woff2'),
url('../font/fontello.woff?97335193') format('woff'),
url('../font/fontello.ttf?97335193') format('truetype'),
url('../font/fontello.svg?97335193#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
@ -15,7 +15,7 @@ @font-face {
@media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face {
font-family: 'fontello';
src: url('../font/fontello.svg?3996201#fontello') format('svg');
src: url('../font/fontello.svg?97335193#fontello') format('svg');
}
}
*/
@ -59,9 +59,9 @@ .icon-cancel:before { content: '\e800'; } /* '' */
.icon-upload:before { content: '\e801'; } /* '' */
.icon-star:before { content: '\e802'; } /* '' */
.icon-star-empty:before { content: '\e803'; } /* '' */
.icon-adjust:before { content: '\e804'; } /* '' */
.icon-retweet:before { content: '\e804'; } /* '' */
.icon-eye-off:before { content: '\e805'; } /* '' */
.icon-retweet:before { content: '\e806'; } /* '' */
.icon-search:before { content: '\e806'; } /* '' */
.icon-cog:before { content: '\e807'; } /* '' */
.icon-logout:before { content: '\e808'; } /* '' */
.icon-down-open:before { content: '\e809'; } /* '' */
@ -77,6 +77,7 @@ .icon-globe:before { content: '\e812'; } /* '' */
.icon-brush:before { content: '\e813'; } /* '' */
.icon-attention:before { content: '\e814'; } /* '' */
.icon-plus:before { content: '\e815'; } /* '' */
.icon-adjust:before { content: '\e816'; } /* '' */
.icon-spin3:before { content: '\e832'; } /* '' */
.icon-spin4:before { content: '\e834'; } /* '' */
.icon-link-ext:before { content: '\f08e'; } /* '' */

View file

@ -229,11 +229,11 @@ body {
}
@font-face {
font-family: 'fontello';
src: url('./font/fontello.eot?15755415');
src: url('./font/fontello.eot?15755415#iefix') format('embedded-opentype'),
url('./font/fontello.woff?15755415') format('woff'),
url('./font/fontello.ttf?15755415') format('truetype'),
url('./font/fontello.svg?15755415#fontello') format('svg');
src: url('./font/fontello.eot?32716429');
src: url('./font/fontello.eot?32716429#iefix') format('embedded-opentype'),
url('./font/fontello.woff?32716429') format('woff'),
url('./font/fontello.ttf?32716429') format('truetype'),
url('./font/fontello.svg?32716429#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
@ -304,9 +304,9 @@ body {
<div class="the-icons span3" title="Code: 0xe803"><i class="demo-icon icon-star-empty">&#xe803;</i> <span class="i-name">icon-star-empty</span><span class="i-code">0xe803</span></div>
</div>
<div class="row">
<div class="the-icons span3" title="Code: 0xe804"><i class="demo-icon icon-adjust">&#xe804;</i> <span class="i-name">icon-adjust</span><span class="i-code">0xe804</span></div>
<div class="the-icons span3" title="Code: 0xe804"><i class="demo-icon icon-retweet">&#xe804;</i> <span class="i-name">icon-retweet</span><span class="i-code">0xe804</span></div>
<div class="the-icons span3" title="Code: 0xe805"><i class="demo-icon icon-eye-off">&#xe805;</i> <span class="i-name">icon-eye-off</span><span class="i-code">0xe805</span></div>
<div class="the-icons span3" title="Code: 0xe806"><i class="demo-icon icon-retweet">&#xe806;</i> <span class="i-name">icon-retweet</span><span class="i-code">0xe806</span></div>
<div class="the-icons span3" title="Code: 0xe806"><i class="demo-icon icon-search">&#xe806;</i> <span class="i-name">icon-search</span><span class="i-code">0xe806</span></div>
<div class="the-icons span3" title="Code: 0xe807"><i class="demo-icon icon-cog">&#xe807;</i> <span class="i-name">icon-cog</span><span class="i-code">0xe807</span></div>
</div>
<div class="row">
@ -330,22 +330,23 @@ body {
<div class="row">
<div class="the-icons span3" title="Code: 0xe814"><i class="demo-icon icon-attention">&#xe814;</i> <span class="i-name">icon-attention</span><span class="i-code">0xe814</span></div>
<div class="the-icons span3" title="Code: 0xe815"><i class="demo-icon icon-plus">&#xe815;</i> <span class="i-name">icon-plus</span><span class="i-code">0xe815</span></div>
<div class="the-icons span3" title="Code: 0xe816"><i class="demo-icon icon-adjust">&#xe816;</i> <span class="i-name">icon-adjust</span><span class="i-code">0xe816</span></div>
<div class="the-icons span3" title="Code: 0xe832"><i class="demo-icon icon-spin3 animate-spin">&#xe832;</i> <span class="i-name">icon-spin3</span><span class="i-code">0xe832</span></div>
<div class="the-icons span3" title="Code: 0xe834"><i class="demo-icon icon-spin4 animate-spin">&#xe834;</i> <span class="i-name">icon-spin4</span><span class="i-code">0xe834</span></div>
</div>
<div class="row">
<div class="the-icons span3" title="Code: 0xe834"><i class="demo-icon icon-spin4 animate-spin">&#xe834;</i> <span class="i-name">icon-spin4</span><span class="i-code">0xe834</span></div>
<div class="the-icons span3" title="Code: 0xf08e"><i class="demo-icon icon-link-ext">&#xf08e;</i> <span class="i-name">icon-link-ext</span><span class="i-code">0xf08e</span></div>
<div class="the-icons span3" title="Code: 0xf08f"><i class="demo-icon icon-link-ext-alt">&#xf08f;</i> <span class="i-name">icon-link-ext-alt</span><span class="i-code">0xf08f</span></div>
<div class="the-icons span3" title="Code: 0xf0c9"><i class="demo-icon icon-menu">&#xf0c9;</i> <span class="i-name">icon-menu</span><span class="i-code">0xf0c9</span></div>
<div class="the-icons span3" title="Code: 0xf0e0"><i class="demo-icon icon-mail-alt">&#xf0e0;</i> <span class="i-name">icon-mail-alt</span><span class="i-code">0xf0e0</span></div>
</div>
<div class="row">
<div class="the-icons span3" title="Code: 0xf0e0"><i class="demo-icon icon-mail-alt">&#xf0e0;</i> <span class="i-name">icon-mail-alt</span><span class="i-code">0xf0e0</span></div>
<div class="the-icons span3" title="Code: 0xf0e5"><i class="demo-icon icon-comment-empty">&#xf0e5;</i> <span class="i-name">icon-comment-empty</span><span class="i-code">0xf0e5</span></div>
<div class="the-icons span3" title="Code: 0xf0fe"><i class="demo-icon icon-plus-squared">&#xf0fe;</i> <span class="i-name">icon-plus-squared</span><span class="i-code">0xf0fe</span></div>
<div class="the-icons span3" title="Code: 0xf112"><i class="demo-icon icon-reply">&#xf112;</i> <span class="i-name">icon-reply</span><span class="i-code">0xf112</span></div>
<div class="the-icons span3" title="Code: 0xf13e"><i class="demo-icon icon-lock-open-alt">&#xf13e;</i> <span class="i-name">icon-lock-open-alt</span><span class="i-code">0xf13e</span></div>
</div>
<div class="row">
<div class="the-icons span3" title="Code: 0xf13e"><i class="demo-icon icon-lock-open-alt">&#xf13e;</i> <span class="i-name">icon-lock-open-alt</span><span class="i-code">0xf13e</span></div>
<div class="the-icons span3" title="Code: 0xf164"><i class="demo-icon icon-thumbs-up-alt">&#xf164;</i> <span class="i-name">icon-thumbs-up-alt</span><span class="i-code">0xf164</span></div>
<div class="the-icons span3" title="Code: 0xf1e5"><i class="demo-icon icon-binoculars">&#xf1e5;</i> <span class="i-name">icon-binoculars</span><span class="i-code">0xf1e5</span></div>
<div class="the-icons span3" title="Code: 0xf234"><i class="demo-icon icon-user-plus">&#xf234;</i> <span class="i-name">icon-user-plus</span><span class="i-code">0xf234</span></div>

View file

@ -14,11 +14,11 @@
<glyph glyph-name="star-empty" unicode="&#xe803;" d="M635 297l170 166-235 34-106 213-105-213-236-34 171-166-41-235 211 111 211-111z m294 199q0-12-15-27l-202-197 48-279q0-4 0-12 0-28-23-28-10 0-22 7l-251 132-250-132q-12-7-23-7-11 0-17 9t-6 19q0 4 1 12l48 279-203 197q-14 15-14 27 0 21 31 26l280 40 126 254q11 23 27 23t28-23l125-254 280-40q32-5 32-26z" horiz-adv-x="928.6" />
<glyph glyph-name="adjust" unicode="&#xe804;" d="M429 53v608q-83 0-153-41t-110-111-41-152 41-152 110-111 153-41z m428 304q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" />
<glyph glyph-name="retweet" unicode="&#xe804;" d="M714 18q0-7-5-13t-13-5h-535q-5 0-8 1t-5 4-3 4-2 7 0 6v335h-107q-15 0-25 11t-11 25q0 13 8 23l179 214q11 12 27 12t28-12l178-214q9-10 9-23 0-15-11-25t-25-11h-107v-214h321q9 0 14-6l89-108q4-5 4-11z m357 232q0-13-8-23l-178-214q-12-13-28-13t-27 13l-179 214q-8 10-8 23 0 14 11 25t25 11h107v214h-322q-9 0-14 7l-89 107q-4 5-4 11 0 7 5 12t13 6h536q4 0 7-1t5-4 3-5 2-6 1-7v-334h107q14 0 25-11t10-25z" horiz-adv-x="1071.4" />
<glyph glyph-name="eye-off" unicode="&#xe805;" d="M310 112l43 79q-48 35-76 88t-27 114q0 67 34 125-128-65-213-197 94-144 239-209z m217 424q0 11-8 19t-19 7q-70 0-120-50t-50-119q0-11 8-19t19-8 19 8 8 19q0 48 34 82t82 34q11 0 19 8t8 19z m202 106q0-4 0-5-59-105-176-316t-176-316l-28-50q-5-9-15-9-7 0-75 39-9 6-9 16 0 7 25 49-80 36-147 96t-117 137q-11 17-11 38t11 39q86 131 212 207t277 76q50 0 100-10l31 54q5 9 15 9 3 0 10-3t18-9 18-10 18-10 10-7q9-5 9-15z m21-249q0-78-44-142t-117-91l157 280q4-25 4-47z m250-72q0-19-11-38-22-36-61-81-84-96-194-149t-234-53l41 74q119 10 219 76t169 171q-65 100-158 164l35 63q53-36 102-85t81-103q11-19 11-39z" horiz-adv-x="1000" />
<glyph glyph-name="retweet" unicode="&#xe806;" d="M714 18q0-7-5-13t-13-5h-535q-5 0-8 1t-5 4-3 4-2 7 0 6v335h-107q-15 0-25 11t-11 25q0 13 8 23l179 214q11 12 27 12t28-12l178-214q9-10 9-23 0-15-11-25t-25-11h-107v-214h321q9 0 14-6l89-108q4-5 4-11z m357 232q0-13-8-23l-178-214q-12-13-28-13t-27 13l-179 214q-8 10-8 23 0 14 11 25t25 11h107v214h-322q-9 0-14 7l-89 107q-4 5-4 11 0 7 5 12t13 6h536q4 0 7-1t5-4 3-5 2-6 1-7v-334h107q14 0 25-11t10-25z" horiz-adv-x="1071.4" />
<glyph glyph-name="search" unicode="&#xe806;" d="M643 393q0 103-73 176t-177 74-177-74-73-176 73-177 177-73 177 73 73 177z m286-465q0-29-22-50t-50-21q-30 0-50 21l-191 191q-100-69-223-69-80 0-153 31t-125 84-84 125-31 153 31 152 84 126 125 84 153 31 153-31 125-84 84-126 31-152q0-123-69-223l191-191q21-21 21-51z" horiz-adv-x="928.6" />
<glyph glyph-name="cog" unicode="&#xe807;" d="M571 357q0 59-41 101t-101 42-101-42-42-101 42-101 101-42 101 42 41 101z m286 61v-124q0-7-4-13t-11-7l-104-16q-10-30-21-51 19-27 59-77 6-6 6-13t-5-13q-15-21-55-61t-53-39q-7 0-14 5l-77 60q-25-13-51-21-9-76-16-104-4-16-20-16h-124q-8 0-14 5t-6 12l-16 103q-27 9-50 21l-79-60q-6-5-14-5-8 0-14 6-70 64-92 94-4 5-4 13 0 6 5 12 8 12 28 37t30 40q-15 28-23 55l-102 15q-7 1-11 7t-5 13v124q0 7 5 13t10 7l104 16q8 25 22 51-23 32-60 77-6 7-6 14 0 5 5 12 15 20 55 60t53 40q7 0 15-5l77-60q24 13 50 21 9 76 17 104 3 16 20 16h124q7 0 13-5t7-12l15-103q28-9 51-20l79 59q5 5 13 5 7 0 14-5 72-67 92-95 4-5 4-12 0-7-4-13-9-12-29-37t-30-40q15-28 23-54l102-16q7-1 12-7t4-13z" horiz-adv-x="857.1" />
@ -50,6 +50,8 @@
<glyph glyph-name="plus" unicode="&#xe815;" d="M786 446v-107q0-22-16-38t-38-15h-232v-233q0-22-16-37t-38-16h-107q-22 0-38 16t-15 37v233h-232q-23 0-38 15t-16 38v107q0 23 16 38t38 16h232v232q0 22 15 38t38 16h107q23 0 38-16t16-38v-232h232q23 0 38-16t16-38z" horiz-adv-x="785.7" />
<glyph glyph-name="adjust" unicode="&#xe816;" d="M429 53v608q-83 0-153-41t-110-111-41-152 41-152 110-111 153-41z m428 304q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" />
<glyph glyph-name="spin3" unicode="&#xe832;" d="M494 857c-266 0-483-210-494-472-1-19 13-20 13-20l84 0c16 0 19 10 19 18 10 199 176 358 378 358 107 0 205-45 273-118l-58-57c-11-12-11-27 5-31l247-50c21-5 46 11 37 44l-58 227c-2 9-16 22-29 13l-65-60c-89 91-214 148-352 148z m409-508c-16 0-19-10-19-18-10-199-176-358-377-358-108 0-205 45-274 118l59 57c10 12 10 27-5 31l-248 50c-21 5-46-11-37-44l58-227c2-9 16-22 30-13l64 60c89-91 214-148 353-148 265 0 482 210 493 473 1 18-13 19-13 19l-84 0z" horiz-adv-x="1000" />
<glyph glyph-name="spin4" unicode="&#xe834;" d="M498 857c-114 0-228-39-320-116l0 0c173 140 428 130 588-31 134-134 164-332 89-495-10-29-5-50 12-68 21-20 61-23 84 0 3 3 12 15 15 24 71 180 33 393-112 539-99 98-228 147-356 147z m-409-274c-14 0-29-5-39-16-3-3-13-15-15-24-71-180-34-393 112-539 185-185 479-195 676-31l0 0c-173-140-428-130-589 31-134 134-163 333-89 495 11 29 6 50-12 68-11 11-27 17-44 16z" horiz-adv-x="1001" />

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,2 @@
!function(e){function t(r){if(n[r])return n[r].exports;var a=n[r]={exports:{},id:r,loaded:!1};return e[r].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var r=window.webpackJsonp;window.webpackJsonp=function(o,p){for(var c,l,s=0,i=[];s<o.length;s++)l=o[s],a[l]&&i.push.apply(i,a[l]),a[l]=0;for(c in p)Object.prototype.hasOwnProperty.call(p,c)&&(e[c]=p[c]);for(r&&r(o,p);i.length;)i.shift().call(null,t);if(p[0])return n[0]=0,t(0)};var n={},a={0:0};t.e=function(e,r){if(0===a[e])return r.call(null,t);if(void 0!==a[e])a[e].push(r);else{a[e]=[r];var n=document.getElementsByTagName("head")[0],o=document.createElement("script");o.type="text/javascript",o.charset="utf-8",o.async=!0,o.src=t.p+"static/js/"+e+"."+{1:"48d4753220bd83360796",2:"6cb7378f44092df9536a"}[e]+".js",n.appendChild(o)}},t.m=e,t.c=n,t.p="/"}([]);
//# sourceMappingURL=manifest.60e190da7cc4cc4711dc.js.map

View file

@ -1,2 +0,0 @@
!function(e){function t(r){if(n[r])return n[r].exports;var a=n[r]={exports:{},id:r,loaded:!1};return e[r].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var r=window.webpackJsonp;window.webpackJsonp=function(c,o){for(var p,l,s=0,i=[];s<c.length;s++)l=c[s],a[l]&&i.push.apply(i,a[l]),a[l]=0;for(p in o)Object.prototype.hasOwnProperty.call(o,p)&&(e[p]=o[p]);for(r&&r(c,o);i.length;)i.shift().call(null,t);if(o[0])return n[0]=0,t(0)};var n={},a={0:0};t.e=function(e,r){if(0===a[e])return r.call(null,t);if(void 0!==a[e])a[e].push(r);else{a[e]=[r];var n=document.getElementsByTagName("head")[0],c=document.createElement("script");c.type="text/javascript",c.charset="utf-8",c.async=!0,c.src=t.p+"static/js/"+e+"."+{1:"cc4190750f6ed4d697bd",2:"67f548ecb9e9fd6b25b0"}[e]+".js",n.appendChild(c)}},t.m=e,t.c=n,t.p="/"}([]);
//# sourceMappingURL=manifest.e076977b8e6c6844fb00.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,4 +1,4 @@
var serviceWorkerOption = {"assets":["/static/img/nsfw.50fd83c.png","/static/js/manifest.e076977b8e6c6844fb00.js","/static/js/vendor.cc4190750f6ed4d697bd.js","/static/js/app.67f548ecb9e9fd6b25b0.js","/static/css/app.ea00efb3229c8591fcc5249f0241b986.css"]};
var serviceWorkerOption = {"assets":["/static/img/nsfw.50fd83c.png","/static/js/manifest.60e190da7cc4cc4711dc.js","/static/js/vendor.48d4753220bd83360796.js","/static/js/app.6cb7378f44092df9536a.js","/static/css/app.285fa56c62b811bbd37880f7e2656b13.css"]};
!function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return e[r].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var n={};return t.m=e,t.c=n,t.p="/",t(0)}([function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}function o(){return u.default.getItem("vuex-lz").then(function(e){return e.config.webPushNotifications})}function i(){return clients.matchAll({includeUncontrolled:!0}).then(function(e){return e.filter(function(e){var t=e.type;return"window"===t})})}var a=n(1),u=r(a);self.addEventListener("push",function(e){e.data&&e.waitUntil(o().then(function(t){return t&&i().then(function(t){var n=e.data.json();if(0===t.length)return self.registration.showNotification(n.title,n)})}))}),self.addEventListener("notificationclick",function(e){e.notification.close(),e.waitUntil(i().then(function(e){for(var t=0;t<e.length;t++){var n=e[t];if("/"===n.url&&"focus"in n)return n.focus()}if(clients.openWindow)return clients.openWindow("/")}))})},function(e,t){/*!
localForage -- Offline Storage, Improved

40
test/captcha_test.exs Normal file
View file

@ -0,0 +1,40 @@
defmodule Pleroma.CaptchaTest do
use ExUnit.Case
import Tesla.Mock
alias Pleroma.Captcha.Kocaptcha
@ets_options [:ordered_set, :private, :named_table, {:read_concurrency, true}]
describe "Kocaptcha" do
setup do
ets_name = Kocaptcha.Ets
^ets_name = :ets.new(ets_name, @ets_options)
mock(fn
%{method: :get, url: "https://captcha.kotobank.ch/new"} ->
json(%{
md5: "63615261b77f5354fb8c4e4986477555",
token: "afa1815e14e29355e6c8f6b143a39fa2",
url: "/captchas/afa1815e14e29355e6c8f6b143a39fa2.png"
})
end)
:ok
end
test "new and validate" do
assert Kocaptcha.new() == %{
type: :kocaptcha,
token: "afa1815e14e29355e6c8f6b143a39fa2",
url: "https://captcha.kotobank.ch/captchas/afa1815e14e29355e6c8f6b143a39fa2.png"
}
assert Kocaptcha.validate(
"afa1815e14e29355e6c8f6b143a39fa2",
"7oEy8c"
)
end
end
end

View file

@ -0,0 +1,100 @@
defmodule Pleroma.Integration.MastodonWebsocketTest do
use Pleroma.DataCase
import Pleroma.Factory
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.OAuth
alias Pleroma.Integration.WebsocketClient
alias Pleroma.Web.Streamer
@path Pleroma.Web.Endpoint.url()
|> URI.parse()
|> Map.put(:scheme, "ws")
|> Map.put(:path, "/api/v1/streaming")
|> URI.to_string()
setup do
GenServer.start(Streamer, %{}, name: Streamer)
on_exit(fn ->
if pid = Process.whereis(Streamer) do
Process.exit(pid, :kill)
end
end)
end
def start_socket(qs \\ nil, headers \\ []) do
path =
case qs do
nil -> @path
qs -> @path <> qs
end
WebsocketClient.start_link(self(), path, headers)
end
test "refuses invalid requests" do
assert {:error, {400, _}} = start_socket()
assert {:error, {404, _}} = start_socket("?stream=ncjdk")
end
test "requires authentication and a valid token for protected streams" do
assert {:error, {403, _}} = start_socket("?stream=user&access_token=aaaaaaaaaaaa")
assert {:error, {403, _}} = start_socket("?stream=user")
end
test "allows public streams without authentication" do
assert {:ok, _} = start_socket("?stream=public")
assert {:ok, _} = start_socket("?stream=public:local")
assert {:ok, _} = start_socket("?stream=hashtag&tag=lain")
end
test "receives well formatted events" do
user = insert(:user)
{:ok, _} = start_socket("?stream=public")
{:ok, activity} = CommonAPI.post(user, %{"status" => "nice echo chamber"})
assert_receive {:text, raw_json}, 1_000
assert {:ok, json} = Jason.decode(raw_json)
assert "update" == json["event"]
assert json["payload"]
assert {:ok, json} = Jason.decode(json["payload"])
# Note: we remove the "statuses_count" from this result as it changes in the meantime
view_json =
Pleroma.Web.MastodonAPI.StatusView.render("status.json", activity: activity, for: nil)
|> Jason.encode!()
|> Jason.decode!()
|> put_in(["account", "statuses_count"], 0)
assert json == view_json
end
describe "with a valid user token" do
setup do
{:ok, app} =
Pleroma.Repo.insert(
OAuth.App.register_changeset(%OAuth.App{}, %{
client_name: "client",
scopes: "scope",
redirect_uris: "url"
})
)
user = insert(:user)
{:ok, auth} = OAuth.Authorization.create_authorization(app, user)
{:ok, token} = OAuth.Token.exchange_token(app, auth)
%{user: user, token: token}
end
test "accepts valid tokens", state do
assert {:ok, _} = start_socket("?stream=user&access_token=#{state.token.token}")
end
end
end

View file

@ -0,0 +1,43 @@
defmodule Pleroma.Web.RuntimeStaticPlugTest do
use Pleroma.Web.ConnCase
@dir "test/tmp/instance_static"
setup do
static_dir = Pleroma.Config.get([:instance, :static_dir])
Pleroma.Config.put([:instance, :static_dir], @dir)
File.mkdir_p!(@dir)
on_exit(fn ->
Pleroma.Config.put([:instance, :static_dir], static_dir)
File.rm_rf(@dir)
end)
end
test "overrides index" do
bundled_index = get(build_conn(), "/")
assert html_response(bundled_index, 200) == File.read!("priv/static/index.html")
File.write!(@dir <> "/index.html", "hello world")
index = get(build_conn(), "/")
assert html_response(index, 200) == "hello world"
end
test "overrides any file in static/static" do
bundled_index = get(build_conn(), "/static/terms-of-service.html")
assert html_response(bundled_index, 200) ==
File.read!("priv/static/static/terms-of-service.html")
File.mkdir!(@dir <> "/static")
File.write!(@dir <> "/static/terms-of-service.html", "plz be kind")
index = get(build_conn(), "/static/terms-of-service.html")
assert html_response(index, 200) == "plz be kind"
File.write!(@dir <> "/static/kaniini.html", "<h1>rabbit hugs as a service</h1>")
index = get(build_conn(), "/static/kaniini.html")
assert html_response(index, 200) == "<h1>rabbit hugs as a service</h1>"
end
end

View file

@ -1,6 +1,8 @@
defmodule Pleroma.Plugs.SetUserSessionIdPlugTest do
use Pleroma.Web.ConnCase, async: true
Code.ensure_compiled(Pleroma.User)
alias Pleroma.Plugs.SetUserSessionIdPlug
alias Pleroma.User

View file

@ -0,0 +1,13 @@
defmodule Pleroma.Captcha.Mock do
alias Pleroma.Captcha.Service
@behaviour Service
@impl Service
def new(), do: %{type: :mock}
@impl Service
def validate(_token, _captcha), do: true
@impl Service
def cleanup(), do: :ok
end

View file

@ -0,0 +1,58 @@
defmodule Pleroma.Integration.WebsocketClient do
# https://github.com/phoenixframework/phoenix/blob/master/test/support/websocket_client.exs
@doc """
Starts the WebSocket server for given ws URL. Received Socket.Message's
are forwarded to the sender pid
"""
def start_link(sender, url, headers \\ []) do
:crypto.start()
:ssl.start()
:websocket_client.start_link(
String.to_charlist(url),
__MODULE__,
[sender],
extra_headers: headers
)
end
@doc """
Closes the socket
"""
def close(socket) do
send(socket, :close)
end
@doc """
Sends a low-level text message to the client.
"""
def send_text(server_pid, msg) do
send(server_pid, {:text, msg})
end
@doc false
def init([sender], _conn_state) do
{:ok, %{sender: sender}}
end
@doc false
def websocket_handle(frame, _conn_state, state) do
send(state.sender, frame)
{:ok, state}
end
@doc false
def websocket_info({:text, msg}, _conn_state, state) do
{:reply, {:text, msg}, state}
end
def websocket_info(:close, _conn_state, _state) do
{:close, <<>>, "done"}
end
@doc false
def websocket_terminate(_reason, _conn_state, _state) do
:ok
end
end

65
test/tasks/relay_test.exs Normal file
View file

@ -0,0 +1,65 @@
defmodule Mix.Tasks.Pleroma.RelayTest do
alias Pleroma.Activity
alias Pleroma.Web.ActivityPub.{ActivityPub, Relay, Utils}
alias Pleroma.User
use Pleroma.DataCase
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
Mix.shell(Mix.Shell.Process)
on_exit(fn ->
Mix.shell(Mix.Shell.IO)
end)
:ok
end
describe "running follow" do
test "relay is followed" do
target_instance = "http://mastodon.example.org/users/admin"
Mix.Tasks.Pleroma.Relay.run(["follow", target_instance])
local_user = Relay.get_actor()
assert local_user.ap_id =~ "/relay"
target_user = User.get_by_ap_id(target_instance)
refute target_user.local
activity = Utils.fetch_latest_follow(local_user, target_user)
assert activity.data["type"] == "Follow"
assert activity.data["actor"] == local_user.ap_id
assert activity.data["object"] == target_user.ap_id
end
end
describe "running unfollow" do
test "relay is unfollowed" do
target_instance = "http://mastodon.example.org/users/admin"
Mix.Tasks.Pleroma.Relay.run(["follow", target_instance])
%User{ap_id: follower_id} = local_user = Relay.get_actor()
target_user = User.get_by_ap_id(target_instance)
follow_activity = Utils.fetch_latest_follow(local_user, target_user)
Mix.Tasks.Pleroma.Relay.run(["unfollow", target_instance])
cancelled_activity = Activity.get_by_ap_id(follow_activity.data["id"])
assert cancelled_activity.data["state"] == "cancelled"
[undo_activity] =
ActivityPub.fetch_activities([], %{
"type" => "Undo",
"actor_id" => follower_id,
"limit" => 1
})
assert undo_activity.data["type"] == "Undo"
assert undo_activity.data["actor"] == local_user.ap_id
assert undo_activity.data["object"] == cancelled_activity.data
end
end
end

View file

@ -0,0 +1,52 @@
defmodule Mix.Tasks.Pleroma.UploadsTest do
alias Pleroma.Upload
use Pleroma.DataCase
import Mock
setup_all do
Mix.shell(Mix.Shell.Process)
on_exit(fn ->
Mix.shell(Mix.Shell.IO)
end)
:ok
end
describe "running migrate_local" do
test "uploads migrated" do
with_mock Upload,
store: fn %Upload{name: _file, path: _path}, _opts -> {:ok, %{}} end do
Mix.Tasks.Pleroma.Uploads.run(["migrate_local", "S3"])
assert_received {:mix_shell, :info, [message]}
assert message =~ "Migrating files from local"
assert_received {:mix_shell, :info, [message]}
assert %{"total_count" => total_count} =
Regex.named_captures(~r"^Found (?<total_count>\d+) uploads$", message)
assert_received {:mix_shell, :info, [message]}
# @logevery in Mix.Tasks.Pleroma.Uploads
count =
min(50, String.to_integer(total_count))
|> to_string()
assert %{"count" => ^count, "total_count" => ^total_count} =
Regex.named_captures(
~r"^Uploaded (?<count>\d+)/(?<total_count>\d+) files$",
message
)
end
end
test "nonexistent uploader" do
assert_raise RuntimeError, ~r/The uploader .* is not an existing/, fn ->
Mix.Tasks.Pleroma.Uploads.run(["migrate_local", "nonexistent"])
end
end
end
end

247
test/tasks/user_test.exs Normal file
View file

@ -0,0 +1,247 @@
defmodule Mix.Tasks.Pleroma.UserTest do
alias Pleroma.User
use Pleroma.DataCase
import Pleroma.Factory
import ExUnit.CaptureIO
setup_all do
Mix.shell(Mix.Shell.Process)
on_exit(fn ->
Mix.shell(Mix.Shell.IO)
end)
:ok
end
describe "running new" do
test "user is created" do
# just get random data
unsaved = build(:user)
# prepare to answer yes
send(self(), {:mix_shell_input, :yes?, true})
Mix.Tasks.Pleroma.User.run([
"new",
unsaved.nickname,
unsaved.email,
"--name",
unsaved.name,
"--bio",
unsaved.bio,
"--password",
"test",
"--moderator",
"--admin"
])
assert_received {:mix_shell, :info, [message]}
assert message =~ "user will be created"
assert_received {:mix_shell, :yes?, [message]}
assert message =~ "Continue"
assert_received {:mix_shell, :info, [message]}
assert message =~ "created"
user = User.get_by_nickname(unsaved.nickname)
assert user.name == unsaved.name
assert user.email == unsaved.email
assert user.bio == unsaved.bio
assert user.info.is_moderator
assert user.info.is_admin
end
test "user is not created" do
unsaved = build(:user)
# prepare to answer no
send(self(), {:mix_shell_input, :yes?, false})
Mix.Tasks.Pleroma.User.run(["new", unsaved.nickname, unsaved.email])
assert_received {:mix_shell, :info, [message]}
assert message =~ "user will be created"
assert_received {:mix_shell, :yes?, [message]}
assert message =~ "Continue"
assert_received {:mix_shell, :info, [message]}
assert message =~ "will not be created"
refute User.get_by_nickname(unsaved.nickname)
end
end
describe "running rm" do
test "user is deleted" do
user = insert(:user)
Mix.Tasks.Pleroma.User.run(["rm", user.nickname])
assert_received {:mix_shell, :info, [message]}
assert message =~ " deleted"
user = User.get_by_nickname(user.nickname)
assert user.info.deactivated
end
test "no user to delete" do
Mix.Tasks.Pleroma.User.run(["rm", "nonexistent"])
assert_received {:mix_shell, :error, [message]}
assert message =~ "No local user"
end
end
describe "running toggle_activated" do
test "user is deactivated" do
user = insert(:user)
Mix.Tasks.Pleroma.User.run(["toggle_activated", user.nickname])
assert_received {:mix_shell, :info, [message]}
assert message =~ " deactivated"
user = User.get_by_nickname(user.nickname)
assert user.info.deactivated
end
test "user is activated" do
user = insert(:user, info: %{deactivated: true})
Mix.Tasks.Pleroma.User.run(["toggle_activated", user.nickname])
assert_received {:mix_shell, :info, [message]}
assert message =~ " activated"
user = User.get_by_nickname(user.nickname)
refute user.info.deactivated
end
test "no user to toggle" do
Mix.Tasks.Pleroma.User.run(["toggle_activated", "nonexistent"])
assert_received {:mix_shell, :error, [message]}
assert message =~ "No user"
end
end
describe "running unsubscribe" do
test "user is unsubscribed" do
followed = insert(:user)
user = insert(:user, %{following: [User.ap_followers(followed)]})
Mix.Tasks.Pleroma.User.run(["unsubscribe", user.nickname])
assert_received {:mix_shell, :info, [message]}
assert message =~ "Deactivating"
assert_received {:mix_shell, :info, [message]}
assert message =~ "Unsubscribing"
# Note that the task has delay :timer.sleep(500)
assert_received {:mix_shell, :info, [message]}
assert message =~ "Successfully unsubscribed"
user = User.get_by_nickname(user.nickname)
assert length(user.following) == 0
assert user.info.deactivated
end
test "no user to unsubscribe" do
Mix.Tasks.Pleroma.User.run(["unsubscribe", "nonexistent"])
assert_received {:mix_shell, :error, [message]}
assert message =~ "No user"
end
end
describe "running set" do
test "All statuses set" do
user = insert(:user)
Mix.Tasks.Pleroma.User.run(["set", user.nickname, "--moderator", "--admin", "--locked"])
assert_received {:mix_shell, :info, [message]}
assert message =~ ~r/Moderator status .* true/
assert_received {:mix_shell, :info, [message]}
assert message =~ ~r/Locked status .* true/
assert_received {:mix_shell, :info, [message]}
assert message =~ ~r/Admin status .* true/
user = User.get_by_nickname(user.nickname)
assert user.info.is_moderator
assert user.info.locked
assert user.info.is_admin
end
test "All statuses unset" do
user = insert(:user, info: %{is_moderator: true, locked: true, is_admin: true})
Mix.Tasks.Pleroma.User.run([
"set",
user.nickname,
"--no-moderator",
"--no-admin",
"--no-locked"
])
assert_received {:mix_shell, :info, [message]}
assert message =~ ~r/Moderator status .* false/
assert_received {:mix_shell, :info, [message]}
assert message =~ ~r/Locked status .* false/
assert_received {:mix_shell, :info, [message]}
assert message =~ ~r/Admin status .* false/
user = User.get_by_nickname(user.nickname)
refute user.info.is_moderator
refute user.info.locked
refute user.info.is_admin
end
test "no user to set status" do
Mix.Tasks.Pleroma.User.run(["set", "nonexistent", "--moderator"])
assert_received {:mix_shell, :error, [message]}
assert message =~ "No local user"
end
end
describe "running reset_password" do
test "password reset token is generated" do
user = insert(:user)
assert capture_io(fn ->
Mix.Tasks.Pleroma.User.run(["reset_password", user.nickname])
end) =~ "URL:"
assert_received {:mix_shell, :info, [message]}
assert message =~ "Generated"
end
test "no user to reset password" do
Mix.Tasks.Pleroma.User.run(["reset_password", "nonexistent"])
assert_received {:mix_shell, :error, [message]}
assert message =~ "No local user"
end
end
describe "running invite" do
test "invite token is generated" do
assert capture_io(fn ->
Mix.Tasks.Pleroma.User.run(["invite"])
end) =~ "http"
assert_received {:mix_shell, :info, [message]}
assert message =~ "Generated"
end
end
end

View file

@ -11,6 +11,23 @@ defmodule Pleroma.UserTest do
:ok
end
describe "when tags are nil" do
test "tagging a user" do
user = insert(:user, %{tags: nil})
user = User.tag(user, ["cool", "dude"])
assert "cool" in user.tags
assert "dude" in user.tags
end
test "untagging a user" do
user = insert(:user, %{tags: nil})
user = User.untag(user, ["cool", "dude"])
assert user.tags == []
end
end
test "ap_id returns the activity pub id for the user" do
user = UserBuilder.build()

View file

@ -1415,4 +1415,18 @@ test "get instance information", %{conn: conn} do
assert result["stats"]["user_count"] == 2
assert result["stats"]["status_count"] == 1
end
test "put settings", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> put("/api/web/settings", %{"data" => %{"programming" => "socks"}})
assert result = json_response(conn, 200)
user = User.get_cached_by_ap_id(user.ap_id)
assert user.info.settings == %{"programming" => "socks"}
end
end

View file

@ -1,31 +0,0 @@
defmodule Pleroma.Web.MastodonApi.MastodonSocketTest do
use Pleroma.DataCase
alias Pleroma.Web.{Streamer, CommonAPI}
import Pleroma.Factory
test "public is working when non-authenticated" do
user = insert(:user)
task =
Task.async(fn ->
assert_receive {:text, _}, 4_000
end)
fake_socket = %{
transport_pid: task.pid,
assigns: %{}
}
topics = %{
"public" => [fake_socket]
}
{:ok, activity} = CommonAPI.post(user, %{"status" => "Test"})
Streamer.push_to_socket(topics, "public", activity)
Task.await(task)
end
end

View file

@ -14,6 +14,22 @@ defmodule Pleroma.Web.TwitterAPI.ActivityViewTest do
import Pleroma.Factory
import Mock
test "a create activity with a html status" do
text = """
#Bike log - Commute Tuesday\nhttps://pla.bike/posts/20181211/\n#cycling #CHScycling #commute\nMVIMG_20181211_054020.jpg
"""
{:ok, activity} = CommonAPI.post(insert(:user), %{"status" => text})
result = ActivityView.render("activity.json", activity: activity)
assert result["statusnet_html"] ==
"<a data-tag=\"bike\" href=\"http://localhost:4001/tag/bike\">#Bike</a> log - Commute Tuesday<br /><a href=\"https://pla.bike/posts/20181211/\">https://pla.bike/posts/20181211/</a><br /><a data-tag=\"cycling\" href=\"http://localhost:4001/tag/cycling\">#cycling</a> <a data-tag=\"chscycling\" href=\"http://localhost:4001/tag/chscycling\">#CHScycling</a> <a data-tag=\"commute\" href=\"http://localhost:4001/tag/commute\">#commute</a><br />MVIMG_20181211_054020.jpg"
assert result["text"] ==
"#Bike log - Commute Tuesday\nhttps://pla.bike/posts/20181211/\n#cycling #CHScycling #commute\nMVIMG_20181211_054020.jpg"
end
test "a create activity with a note" do
user = insert(:user)
other_user = insert(:user, %{nickname: "shp"})

View file

@ -1,11 +1,7 @@
defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.User
alias Pleroma.Web.WebFinger.WebFingerController
import Pleroma.Factory
import ExUnit.CaptureLog
import Tesla.Mock
setup do
@ -29,9 +25,18 @@ test "Webfinger XML" do
response =
build_conn()
|> put_req_header("accept", "application/jrd+json")
|> put_req_header("accept", "application/xrd+xml")
|> get("/.well-known/webfinger?resource=acct:#{user.nickname}@localhost")
assert response(response, 200)
end
test "Sends a 400 when resource param is missing" do
response =
build_conn()
|> put_req_header("accept", "application/xrd+xml,application/jrd+json")
|> get("/.well-known/webfinger")
assert response(response, 400)
end
end