Merge branch 'develop' into oembed_provider

This commit is contained in:
raeno 2018-12-18 14:59:32 +01:00
commit a300336459
87 changed files with 1842 additions and 299 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",
@ -65,6 +81,7 @@
config :mime, :types, %{
"application/xml" => ["xml"],
"application/xrd+xml" => ["xrd+xml"],
"application/jrd+json" => ["jrd+json"],
"application/activity+json" => ["activity+json"],
"application/ld+json" => ["activity+json"]
}
@ -93,6 +110,7 @@
public: true,
quarantined_instances: [],
managed_config: true,
static_dir: "instance/static/",
allowed_post_formats: [
"text/plain",
"text/html",

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`.
@ -67,7 +67,8 @@ config :pleroma, Pleroma.Mailer,
* `avatar_upload_limit`: File size limit of users profile avatars
* `background_upload_limit`: File size limit of users profile backgrounds
* `banner_upload_limit`: File size limit of users profile banners
* `registrations_open`: Enable registrations for anyone, invitations can be used when false.
* `registrations_open`: Enable registrations for anyone, invitations can be enabled when false.
* `invites_enabled`: Enable user invitations for admins (depends on `registrations_open: false`).
* `federating`: Enable federation with other instances
* `allow_relay`: Enable Pleromas Relay, which makes it possible to follow a whole instance
* `rewrite_policy`: Message Rewrite Policy, either one or a list. Here are the ones available by default:
@ -162,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

@ -58,12 +58,15 @@ def run(["gen" | rest]) do
proceed? = Enum.empty?(will_overwrite) or Keyword.get(options, :force, false)
unless not proceed? do
domain =
Common.get_option(
options,
:domain,
"What domain will your instance use? (e.g pleroma.soykaf.com)"
)
[domain, port | _] =
String.split(
Common.get_option(
options,
:domain,
"What domain will your instance use? (e.g pleroma.soykaf.com)"
),
":"
) ++ [443]
name =
Common.get_option(
@ -104,6 +107,7 @@ def run(["gen" | rest]) do
EEx.eval_file(
"sample_config.eex" |> Path.expand(__DIR__),
domain: domain,
port: port,
email: email,
name: name,
dbhost: dbhost,

View file

@ -6,7 +6,7 @@
use Mix.Config
config :pleroma, Pleroma.Web.Endpoint,
url: [host: "<%= domain %>", scheme: "https", port: 443],
url: [host: "<%= domain %>", scheme: "https", port: <%= port %>],
secret_key_base: "<%= secret %>"
config :pleroma, :instance,

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

@ -37,4 +37,30 @@ def password_reset_email(user, password_reset_token) when is_binary(password_res
|> subject("Password reset")
|> html_body(html_body)
end
def user_invitation_email(
user,
%Pleroma.UserInviteToken{} = user_invite_token,
to_email,
to_name \\ nil
) do
registration_url =
Router.Helpers.redirect_url(
Endpoint,
:registration_page,
user_invite_token.token
)
html_body = """
<h3>You are invited to #{instance_name()}</h3>
<p>#{user.name} invites you to join #{instance_name()}, an instance of Pleroma federated social networking platform.</p>
<p>Click the following link to register: <a href="#{registration_url}">accept invitation</a>.</p>
"""
new()
|> to(recipient(to_email, to_name))
|> from(sender())
|> subject("Invitation to #{instance_name()}")
|> html_body(html_body)
end
end

View file

@ -5,6 +5,8 @@ defmodule Pleroma.Formatter do
alias Pleroma.Emoji
@tag_regex ~r/\#\w+/u
@markdown_characters_regex ~r/(`|\*|_|{|}|[|]|\(|\)|#|\+|-|\.|!)/
def parse_tags(text, data \\ %{}) do
Regex.scan(@tag_regex, text)
|> Enum.map(fn ["#" <> tag = full_tag] -> {full_tag, String.downcase(tag)} end)
@ -76,6 +78,18 @@ def html_escape(text, "text/plain") do
|> Enum.join("")
end
@doc """
Escapes a special characters in mention names.
"""
@spec mentions_escape(String.t(), list({String.t(), any()})) :: String.t()
def mentions_escape(text, mentions) do
mentions
|> Enum.reduce(text, fn {name, _}, acc ->
escape_name = String.replace(name, @markdown_characters_regex, "\\\\\\1")
String.replace(acc, name, escape_name)
end)
end
@doc "changes scheme:... urls to html links"
def add_links({subs, text}) do
links =

View file

@ -17,15 +17,9 @@ def filter_tags(html, nil) do
end)
end
def filter_tags(html, scrubber) do
html |> Scrubber.scrub(scrubber)
end
def filter_tags(html, scrubber), do: Scrubber.scrub(html, scrubber)
def filter_tags(html), do: filter_tags(html, nil)
def strip_tags(html) do
html |> Scrubber.scrub(Scrubber.StripTags)
end
def strip_tags(html), do: Scrubber.scrub(html, Scrubber.StripTags)
end
defmodule Pleroma.HTML.Scrubber.TwitterText do

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

@ -851,7 +851,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 ->
@ -862,7 +862,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

@ -147,6 +147,19 @@ def relay_unfollow(conn, %{"relay_url" => target}) do
end
end
@doc "Sends registration invite via email"
def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do
with true <-
Pleroma.Config.get([:instance, :invites_enabled]) &&
!Pleroma.Config.get([:instance, :registrations_open]),
{:ok, invite_token} <- Pleroma.UserInviteToken.create_token(),
email <-
Pleroma.UserEmail.user_invitation_email(user, invite_token, email, params["name"]),
{:ok, _} <- Pleroma.Mailer.deliver(email) do
json_response(conn, :no_content, "")
end
end
@doc "Get a account registeration invite token (base64 string)"
def get_invite_token(conn, _params) do
{:ok, token} = Pleroma.UserInviteToken.create_token()

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

@ -112,6 +112,9 @@ def add_attachments(text, attachments) do
Enum.join([text | attachment_text], "<br>")
end
@doc """
Formatting text to plain text.
"""
def format_input(text, mentions, tags, "text/plain") do
text
|> Formatter.html_escape("text/plain")
@ -123,6 +126,9 @@ def format_input(text, mentions, tags, "text/plain") do
|> Formatter.finalize()
end
@doc """
Formatting text to html.
"""
def format_input(text, mentions, _tags, "text/html") do
text
|> Formatter.html_escape("text/html")
@ -132,8 +138,12 @@ def format_input(text, mentions, _tags, "text/html") do
|> Formatter.finalize()
end
@doc """
Formatting text to markdown.
"""
def format_input(text, mentions, tags, "text/markdown") do
text
|> Formatter.mentions_escape(mentions)
|> Earmark.as_html!()
|> Formatter.html_escape("text/html")
|> String.replace(~r/\r?\n/, "")

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

@ -140,7 +140,7 @@ def render("status.json", %{activity: %{data: %{"object" => object}} = activity}
visibility: get_visibility(object),
media_attachments: attachments |> Enum.take(4),
mentions: mentions,
tags: tags,
tags: build_tags(tags),
application: %{
name: "Web",
website: nil
@ -234,6 +234,27 @@ def render_content(%{"type" => object_type} = object)
def render_content(object), do: object["content"] || ""
@doc """
Builds a dictionary tags.
## Examples
iex> Pleroma.Web.MastodonAPI.StatusView.build_tags(["fediverse", "nextcloud"])
[{"name": "fediverse", "url": "/tag/fediverse"},
{"name": "nextcloud", "url": "/tag/nextcloud"}]
"""
@spec build_tags(list(any())) :: list(map())
def build_tags(object_tags) when is_list(object_tags) do
object_tags = for tag when is_binary(tag) <- object_tags, do: tag
Enum.reduce(object_tags, [], fn tag, tags ->
tags ++ [%{name: tag, url: "/tag/#{tag}"}]
end)
end
def build_tags(_), do: []
@doc """
Builds list emojis.

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

@ -132,6 +132,7 @@ def nodeinfo(conn, %{"version" => "2.0"}) do
banner: Keyword.get(instance, :banner_upload_limit),
background: Keyword.get(instance, :background_upload_limit)
},
invitesEnabled: Keyword.get(instance, :invites_enabled, false),
features: features
}
}

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
@ -117,6 +118,8 @@ defmodule Pleroma.Web.Router do
delete("/relay", AdminAPIController, :relay_unfollow)
get("/invite_token", AdminAPIController, :get_invite_token)
post("/email_invite", AdminAPIController, :email_invite)
get("/password_reset", AdminAPIController, :get_password_reset)
end
@ -481,7 +484,7 @@ def redirector_with_meta(conn, params) do
end
def index_file_path do
Application.app_dir(:pleroma, "priv/static/index.html")
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

@ -173,7 +173,8 @@ def config(conn, _params) do
uploadlimit: uploadlimit,
closed: if(Keyword.get(instance, :registrations_open), do: "0", else: "1"),
private: if(Keyword.get(instance, :public, true), do: "0", else: "1"),
vapidPublicKey: vapid_public_key
vapidPublicKey: vapid_public_key,
invitesEnabled: if(Keyword.get(instance, :invites_enabled, false), do: "1", else: "0")
}
pleroma_fe = %{
@ -283,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

@ -14,6 +14,7 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do
alias Pleroma.HTML
import Ecto.Query
require Logger
defp query_context_ids([]), do: []
@ -239,9 +240,15 @@ def render(
{summary, content} = render_content(object)
html =
HTML.filter_tags(content, User.html_filter_policy(opts[:for]))
content
|> 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)
@ -251,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,
@ -276,6 +283,11 @@ def render(
}
end
def render("activity.json", %{activity: unhandled_activity}) do
Logger.warn("#{__MODULE__} unhandled activity: #{inspect(unhandled_activity)}")
nil
end
def render_content(%{"type" => "Note"} = object) do
summary = object["summary"]

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

19
mix.exs
View file

@ -8,12 +8,7 @@ def project do
elixir: "~> 1.4",
elixirc_paths: elixirc_paths(Mix.env()),
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
elixirc_options:
if Mix.env() == :test do
[]
else
[warnings_as_errors: true]
end,
elixirc_options: [warnings_as_errors: true],
start_permanent: Mix.env() == :prod,
aliases: aliases(),
deps: deps(),
@ -48,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"},
@ -68,7 +64,7 @@ defp deps do
{:mogrify, "~> 0.6.1"},
{:ex_aws, "~> 2.0"},
{:ex_aws_s3, "~> 2.0"},
{:earmark, "~> 1.2"},
{:earmark, "~> 1.3"},
{:ex_machina, "~> 2.2", only: :test},
{:credo, "~> 0.9.3", only: [:dev, :test]},
{:mock, "~> 0.3.1", only: :test},
@ -78,7 +74,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

@ -13,7 +13,7 @@
"crypt": {:git, "https://github.com/msantos/crypt", "1f2b58927ab57e72910191a7ebaeff984382a1d3", [ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"]},
"db_connection": {:hex, :db_connection, "1.1.3", "89b30ca1ef0a3b469b1c779579590688561d586694a3ce8792985d4d7e575a61", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
"decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [:mix], [], "hexpm"},
"earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm"},
"earmark": {:hex, :earmark, "1.3.0", "17f0c38eaafb4800f746b457313af4b2442a8c2405b49c645768680f900be603", [:mix], [], "hexpm"},
"ecto": {:hex, :ecto, "2.2.10", "e7366dc82f48f8dd78fcbf3ab50985ceeb11cb3dc93435147c6e13f2cda0992e", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
"eternal": {:hex, :eternal, "1.2.0", "e2a6b6ce3b8c248f7dc31451aefca57e3bdf0e48d73ae5043229380a67614c41", [:mix], [], "hexpm"},
"ex_aws": {:hex, :ex_aws, "2.1.0", "b92651527d6c09c479f9013caa9c7331f19cba38a650590d82ebf2c6c16a1d8a", [:mix], [{:configparser_ex, "~> 2.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "1.6.3 or 1.6.5 or 1.7.1 or 1.8.6 or ~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8", [hex: :jsx, repo: "hexpm", optional: true]}, {:poison, ">= 1.2.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:xml_builder, "~> 0.1.0", [hex: :xml_builder, repo: "hexpm", optional: true]}], "hexpm"},
@ -33,7 +33,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"},
@ -41,11 +41,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"},
@ -58,4 +60,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", []},
}

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"
},
{

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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

@ -257,4 +257,23 @@ test "it doesn't die when text is absent" do
text = nil
assert Formatter.get_emoji(text) == []
end
describe "/mentions_escape" do
test "it returns text with escaped mention names" do
text = """
@a_breakin_glass@cybre.space
(also, little voice inside my head thinking "maybe this will encourage people
pronouncing it properly instead of saying _raKEWdo_ ")
"""
escape_text = """
@a\\_breakin\\_glass@cybre\\.space
(also, little voice inside my head thinking \"maybe this will encourage people
pronouncing it properly instead of saying _raKEWdo_ \")
"""
mentions = [{"@a_breakin_glass@cybre.space", %{}}]
assert Formatter.mentions_escape(text, mentions) == escape_text
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

@ -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

@ -154,6 +154,105 @@ test "/:right DELETE, can remove from a permission group" do
end
end
describe "POST /api/pleroma/admin/email_invite, with valid config" do
setup do
registrations_open = Pleroma.Config.get([:instance, :registrations_open])
invites_enabled = Pleroma.Config.get([:instance, :invites_enabled])
Pleroma.Config.put([:instance, :registrations_open], false)
Pleroma.Config.put([:instance, :invites_enabled], true)
on_exit(fn ->
Pleroma.Config.put([:instance, :registrations_open], registrations_open)
Pleroma.Config.put([:instance, :invites_enabled], invites_enabled)
:ok
end)
[user: insert(:user, info: %{is_admin: true})]
end
test "sends invitation and returns 204", %{conn: conn, user: user} do
recipient_email = "foo@bar.com"
recipient_name = "J. D."
conn =
conn
|> assign(:user, user)
|> post("/api/pleroma/admin/email_invite?email=#{recipient_email}&name=#{recipient_name}")
assert json_response(conn, :no_content)
token_record = List.last(Pleroma.Repo.all(Pleroma.UserInviteToken))
assert token_record
refute token_record.used
Swoosh.TestAssertions.assert_email_sent(
Pleroma.UserEmail.user_invitation_email(
user,
token_record,
recipient_email,
recipient_name
)
)
end
test "it returns 403 if requested by a non-admin", %{conn: conn} do
non_admin_user = insert(:user)
conn =
conn
|> assign(:user, non_admin_user)
|> post("/api/pleroma/admin/email_invite?email=foo@bar.com&name=JD")
assert json_response(conn, :forbidden)
end
end
describe "POST /api/pleroma/admin/email_invite, with invalid config" do
setup do
[user: insert(:user, info: %{is_admin: true})]
end
test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn, user: user} do
registrations_open = Pleroma.Config.get([:instance, :registrations_open])
invites_enabled = Pleroma.Config.get([:instance, :invites_enabled])
Pleroma.Config.put([:instance, :registrations_open], false)
Pleroma.Config.put([:instance, :invites_enabled], false)
on_exit(fn ->
Pleroma.Config.put([:instance, :registrations_open], registrations_open)
Pleroma.Config.put([:instance, :invites_enabled], invites_enabled)
:ok
end)
conn =
conn
|> assign(:user, user)
|> post("/api/pleroma/admin/email_invite?email=foo@bar.com&name=JD")
assert json_response(conn, :internal_server_error)
end
test "it returns 500 if `registrations_open` is enabled", %{conn: conn, user: user} do
registrations_open = Pleroma.Config.get([:instance, :registrations_open])
invites_enabled = Pleroma.Config.get([:instance, :invites_enabled])
Pleroma.Config.put([:instance, :registrations_open], true)
Pleroma.Config.put([:instance, :invites_enabled], true)
on_exit(fn ->
Pleroma.Config.put([:instance, :registrations_open], registrations_open)
Pleroma.Config.put([:instance, :invites_enabled], invites_enabled)
:ok
end)
conn =
conn
|> assign(:user, user)
|> post("/api/pleroma/admin/email_invite?email=foo@bar.com&name=JD")
assert json_response(conn, :internal_server_error)
end
end
test "/api/pleroma/admin/invite_token" do
admin = insert(:user, info: %{is_admin: true})

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

@ -62,7 +62,12 @@ test "a note activity" do
visibility: "public",
media_attachments: [],
mentions: [],
tags: note.data["object"]["tag"],
tags: [
%{
name: "#{note.data["object"]["tag"]}",
url: "/tag/#{note.data["object"]["tag"]}"
}
],
application: %{
name: "Web",
website: nil
@ -151,4 +156,25 @@ test "a reblog" do
assert represented[:reblog][:id] == to_string(activity.id)
assert represented[:emojis] == []
end
describe "build_tags/1" do
test "it returns a a dictionary tags" do
object_tags = [
"fediverse",
"mastodon",
"nextcloud",
%{
"href" => "https://kawen.space/users/lain",
"name" => "@lain@kawen.space",
"type" => "Mention"
}
]
assert StatusView.build_tags(object_tags) == [
%{name: "fediverse", url: "/tag/fediverse"},
%{name: "mastodon", url: "/tag/mastodon"},
%{name: "nextcloud", url: "/tag/nextcloud"}
]
end
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

@ -0,0 +1,42 @@
defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do
use Pleroma.Web.ConnCase
import Pleroma.Factory
import Tesla.Mock
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
test "Webfinger JRD" do
user = insert(:user)
response =
build_conn()
|> put_req_header("accept", "application/jrd+json")
|> get("/.well-known/webfinger?resource=acct:#{user.nickname}@localhost")
assert json_response(response, 200)["subject"] == "acct:#{user.nickname}@localhost"
end
test "Webfinger XML" do
user = insert(:user)
response =
build_conn()
|> 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