Merge branch 'develop' into 'feature/relay'

# Conflicts:
#   lib/pleroma/web/activity_pub/utils.ex
This commit is contained in:
kaniini 2018-08-26 21:06:15 +00:00
commit 0f5bff8c66
287 changed files with 1251 additions and 2778 deletions

View file

@ -16,6 +16,8 @@
config :pleroma, :emoji, shortcode_globs: ["/emoji/custom/**/*.png"] config :pleroma, :emoji, shortcode_globs: ["/emoji/custom/**/*.png"]
config :pleroma, :uri_schemes, additionnal_schemes: []
# Configures the endpoint # Configures the endpoint
config :pleroma, Pleroma.Web.Endpoint, config :pleroma, Pleroma.Web.Endpoint,
url: [host: "localhost"], url: [host: "localhost"],
@ -71,11 +73,8 @@
redirect_root_no_login: "/main/all", redirect_root_no_login: "/main/all",
redirect_root_login: "/main/friends", redirect_root_login: "/main/friends",
show_instance_panel: true, show_instance_panel: true,
show_who_to_follow_panel: false, scope_options_enabled: false,
who_to_follow_provider: collapse_message_with_subject: false
"https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-osa-api.cgi?{{host}}+{{user}}",
who_to_follow_link: "https://vinayaka.distsn.org/?{{host}}+{{user}}",
scope_options_enabled: false
config :pleroma, :activitypub, config :pleroma, :activitypub,
accept_blocks: true, accept_blocks: true,
@ -112,6 +111,13 @@
ip: {0, 0, 0, 0}, ip: {0, 0, 0, 0},
port: 9999 port: 9999
config :pleroma, :suggestions,
enabled: false,
third_party_engine:
"http://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-suggestions-api.cgi?{{host}}+{{user}}",
timeout: 300_000,
web: "https://vinayaka.distsn.org/?{{host}}+{{user}}"
# Import environment specific config. This must remain at the bottom # Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above. # of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs" import_config "#{Mix.env()}.exs"

View file

@ -1,8 +1,32 @@
social.domain.tld { social.domain.tld {
tls user@domain.tld log /var/log/caddy/pleroma_access.log
errors /var/log/caddy/pleroma_error.log
log /var/log/caddy/pleroma.log gzip
proxy / localhost:4000 {
websocket
transparent
}
tls user@domain.tld {
# Remove the rest of the lines in here, if you want to support older devices
key_type p256
ciphers ECDHE-ECDSA-WITH-CHACHA20-POLY1305 ECDHE-RSA-WITH-CHACHA20-POLY1305 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-RSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-RSA-AES128-GCM-SHA256
}
header / {
X-XSS-Protection "1; mode=block"
X-Frame-Options "DENY"
X-Content-Type-Options "nosniff"
Referrer-Policy "same-origin"
Strict-Transport-Security "max-age=31536000; includeSubDomains;"
Expect-CT "enforce, max-age=2592000"
}
# If you do not want remote frontends to be able to access your Pleroma backend server, remove these lines.
# If you want to allow all origins access, remove the origin lines.
# To use this directive, you need the http.cors plugin for Caddy.
cors / { cors / {
origin https://halcyon.domain.tld origin https://halcyon.domain.tld
origin https://pinafore.domain.tld origin https://pinafore.domain.tld
@ -10,9 +34,13 @@ social.domain.tld {
allowed_headers Authorization,Content-Type,Idempotency-Key allowed_headers Authorization,Content-Type,Idempotency-Key
exposed_headers Link,X-RateLimit-Reset,X-RateLimit-Limit,X-RateLimit-Remaining,X-Request-Id exposed_headers Link,X-RateLimit-Reset,X-RateLimit-Limit,X-RateLimit-Remaining,X-Request-Id
} }
# Stop removing lines here.
proxy / localhost:4000 { # If you do not want to use the mediaproxy function, remove these lines.
websocket # To use this directive, you need the http.cache plugin for Caddy.
transparent cache {
match_path /proxy
default_max_age 720m
} }
# Stop removing lines here.
} }

21
installation/init.d/pleroma Executable file
View file

@ -0,0 +1,21 @@
#!/sbin/openrc-run
# Requires OpenRC >= 0.35
directory=~pleroma/pleroma
command=/usr/bin/mix
command_args="phx.server"
command_user=pleroma:pleroma
command_background=1
export PORT=4000
export MIX_ENV=prod
# Ask process to terminate within 30 seconds, otherwise kill it
retry="SIGTERM/30 SIGKILL/5"
pidfile="/var/run/pleroma.pid"
depend() {
need nginx postgresql
}

View file

@ -0,0 +1,25 @@
defmodule Mix.Tasks.GenerateInviteToken do
use Mix.Task
@shortdoc "Generate invite token for user"
def run([]) do
Mix.Task.run("app.start")
with {:ok, token} <- Pleroma.UserInviteToken.create_token() do
IO.puts("Generated user invite token")
IO.puts(
"Url: #{
Pleroma.Web.Router.Helpers.redirect_url(
Pleroma.Web.Endpoint,
:registration_page,
token.token
)
}"
)
else
_ ->
IO.puts("Error creating token")
end
end
end

View file

@ -16,7 +16,7 @@ def parse_tags(text, data \\ %{}) do
def parse_mentions(text) do def parse_mentions(text) do
# Modified from https://www.w3.org/TR/html5/forms.html#valid-e-mail-address # Modified from https://www.w3.org/TR/html5/forms.html#valid-e-mail-address
regex = regex =
~r/@[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@?[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*/u ~r/@[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]*@?[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*/u
Regex.scan(regex, text) Regex.scan(regex, text)
|> List.flatten() |> List.flatten()
@ -165,8 +165,29 @@ def get_custom_emoji() do
@emoji @emoji
end end
@link_regex ~r/https?:\/\/[\w\.\/?=\-#\+%&@~'\(\):]+[\w\/]/u @link_regex ~r/[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+/ui
# IANA got a list https://www.iana.org/assignments/uri-schemes/ but
# Stuff like ipfs isnt in it
# There is very niche stuff
@uri_schemes [
"https://",
"http://",
"dat://",
"dweb://",
"gopher://",
"ipfs://",
"ipns://",
"irc:",
"ircs:",
"magnet:",
"mailto:",
"mumble:",
"ssb://",
"xmpp:"
]
# TODO: make it use something other than @link_regex
def html_escape(text) do def html_escape(text) do
Regex.split(@link_regex, text, include_captures: true) Regex.split(@link_regex, text, include_captures: true)
|> Enum.map_every(2, fn chunk -> |> Enum.map_every(2, fn chunk ->
@ -176,11 +197,18 @@ def html_escape(text) do
|> Enum.join("") |> Enum.join("")
end end
@doc "changes http:... links to html links" @doc "changes scheme:... urls to html links"
def add_links({subs, text}) do def add_links({subs, text}) do
additionnal_schemes =
Application.get_env(:pleroma, :uri_schemes, [])
|> Keyword.get(:additionnal_schemes, [])
links = links =
Regex.scan(@link_regex, text) text
|> Enum.map(fn [url] -> {Ecto.UUID.generate(), url} end) |> String.split([" ", "\t", "<br>"])
|> Enum.filter(fn word -> String.starts_with?(word, @uri_schemes ++ additionnal_schemes) end)
|> Enum.filter(fn word -> Regex.match?(@link_regex, word) end)
|> Enum.map(fn url -> {Ecto.UUID.generate(), url} end)
|> Enum.sort_by(fn {_, url} -> -String.length(url) end) |> Enum.sort_by(fn {_, url} -> -String.length(url) end)
uuid_text = uuid_text =
@ -244,8 +272,8 @@ def add_hashtag_links({subs, text}, tags) do
subs = subs =
subs ++ subs ++
Enum.map(tags, fn {_, tag, uuid} -> Enum.map(tags, fn {tag_text, tag, uuid} ->
url = "<a href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>##{tag}</a>" url = "<a href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>#{tag_text}</a>"
{uuid, url} {uuid, url}
end) end)

View file

@ -54,7 +54,7 @@ def info(text) do
String.split(text, "\r") String.split(text, "\r")
|> Enum.map(fn text -> |> Enum.map(fn text ->
"i#{text}\tfake\(NULL)\t0\r\n" "i#{text}\tfake\t(NULL)\t0\r\n"
end) end)
|> Enum.join("") |> Enum.join("")
end end
@ -77,14 +77,14 @@ def render_activities(activities) do
link("Post ##{activity.id} by #{user.nickname}", "/notices/#{activity.id}") <> link("Post ##{activity.id} by #{user.nickname}", "/notices/#{activity.id}") <>
info("#{like_count} likes, #{announcement_count} repeats") <> info("#{like_count} likes, #{announcement_count} repeats") <>
"\r\n" <> "i\tfake\t(NULL)\t0\r\n" <>
info( info(
HtmlSanitizeEx.strip_tags( HtmlSanitizeEx.strip_tags(
String.replace(activity.data["object"]["content"], "<br>", "\r") String.replace(activity.data["object"]["content"], "<br>", "\r")
) )
) )
end) end)
|> Enum.join("\r\n") |> Enum.join("i\tfake\t(NULL)\t0\r\n")
end end
def response("") do def response("") do

View file

@ -1,5 +1,23 @@
defmodule Pleroma.HTTP do defmodule Pleroma.HTTP do
use HTTPoison.Base require HTTPoison
def request(method, url, body \\ "", headers \\ [], options \\ []) do
options =
process_request_options(options)
|> process_sni_options(url)
HTTPoison.request(method, url, body, headers, options)
end
defp process_sni_options(options, url) do
uri = URI.parse(url)
host = uri.host |> to_charlist()
case uri.scheme do
"https" -> options ++ [ssl: [server_name_indication: host]]
_ -> options
end
end
def process_request_options(options) do def process_request_options(options) do
config = Application.get_env(:pleroma, :http, []) config = Application.get_env(:pleroma, :http, [])
@ -10,4 +28,9 @@ def process_request_options(options) do
_ -> options ++ [proxy: proxy] _ -> options ++ [proxy: proxy]
end end
end end
def get(url, headers \\ [], options \\ []), do: request(:get, url, "", headers, options)
def post(url, body, headers \\ [], options \\ []),
do: request(:post, url, body, headers, options)
end end

View file

@ -124,20 +124,20 @@ defp get_name(file, uuid, type, should_dedupe) do
if should_dedupe do if should_dedupe do
create_name(uuid, List.last(String.split(file.filename, ".")), type) create_name(uuid, List.last(String.split(file.filename, ".")), type)
else else
unless String.contains?(file.filename, ".") do parts = String.split(file.filename, ".")
case type do
"image/png" -> file.filename <> ".png" new_filename =
"image/jpeg" -> file.filename <> ".jpg" if length(parts) > 1 do
"image/gif" -> file.filename <> ".gif" Enum.drop(parts, -1) |> Enum.join(".")
"video/webm" -> file.filename <> ".webm" else
"video/mp4" -> file.filename <> ".mp4" Enum.join(parts)
"audio/mpeg" -> file.filename <> ".mp3"
"audio/ogg" -> file.filename <> ".ogg"
"audio/wav" -> file.filename <> ".wav"
_ -> file.filename
end end
else
file.filename case type do
"application/octet-stream" -> file.filename
"audio/mpeg" -> new_filename <> ".mp3"
"image/jpeg" -> new_filename <> ".jpg"
_ -> Enum.join([new_filename, String.split(type, "/") |> List.last()], ".")
end end
end end
end end

View file

@ -398,6 +398,7 @@ def get_follow_requests(%User{} = user) do
Enum.map(reqs, fn req -> req.actor end) Enum.map(reqs, fn req -> req.actor end)
|> Enum.uniq() |> Enum.uniq()
|> Enum.map(fn ap_id -> get_by_ap_id(ap_id) end) |> Enum.map(fn ap_id -> get_by_ap_id(ap_id) end)
|> Enum.filter(fn u -> !following?(u, user) end)
{:ok, users} {:ok, users}
end end

View file

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

View file

@ -576,7 +576,7 @@ def user_data_from_user_object(data) do
def fetch_and_prepare_user_from_ap_id(ap_id) do def fetch_and_prepare_user_from_ap_id(ap_id) do
with {:ok, %{status_code: 200, body: body}} <- with {:ok, %{status_code: 200, body: body}} <-
@httpoison.get(ap_id, Accept: "application/activity+json"), @httpoison.get(ap_id, [Accept: "application/activity+json"], follow_redirect: true),
{:ok, data} <- Jason.decode(body) do {:ok, data} <- Jason.decode(body) do
user_data_from_user_object(data) user_data_from_user_object(data)
else else

View file

@ -18,12 +18,16 @@ def get_actor(%{"actor" => actor}) when is_binary(actor) do
end end
def get_actor(%{"actor" => actor}) when is_list(actor) do def get_actor(%{"actor" => actor}) when is_list(actor) do
Enum.at(actor, 0) if is_binary(Enum.at(actor, 0)) do
Enum.at(actor, 0)
else
Enum.find(actor, fn %{"type" => type} -> type == "Person" end)
|> Map.get("id")
end
end end
def get_actor(%{"actor" => actor_list}) do def get_actor(%{"actor" => actor}) when is_map(actor) do
Enum.find(actor_list, fn %{"type" => type} -> type == "Person" end) actor["id"]
|> Map.get("id")
end end
@doc """ @doc """
@ -38,6 +42,25 @@ def fix_object(object) do
|> fix_emoji |> fix_emoji
|> fix_tag |> fix_tag
|> fix_content_map |> fix_content_map
|> fix_likes
|> fix_addressing
end
def fix_addressing_list(map, field) do
if is_binary(map[field]) do
map
|> Map.put(field, [map[field]])
else
map
end
end
def fix_addressing(map) do
map
|> fix_addressing_list("to")
|> fix_addressing_list("cc")
|> fix_addressing_list("bto")
|> fix_addressing_list("bcc")
end end
def fix_actor(%{"attributedTo" => actor} = object) do def fix_actor(%{"attributedTo" => actor} = object) do
@ -45,6 +68,20 @@ def fix_actor(%{"attributedTo" => actor} = object) do
|> Map.put("actor", get_actor(%{"actor" => actor})) |> Map.put("actor", get_actor(%{"actor" => actor}))
end end
def fix_likes(%{"likes" => likes} = object)
when is_bitstring(likes) do
# Check for standardisation
# This is what Peertube does
# curl -H 'Accept: application/activity+json' $likes | jq .totalItems
object
|> Map.put("likes", [])
|> Map.put("like_count", 0)
end
def fix_likes(object) do
object
end
def fix_in_reply_to(%{"inReplyTo" => in_reply_to_id} = object) def fix_in_reply_to(%{"inReplyTo" => in_reply_to_id} = object)
when not is_nil(in_reply_to_id) do when not is_nil(in_reply_to_id) do
case ActivityPub.fetch_object_from_id(in_reply_to_id) do case ActivityPub.fetch_object_from_id(in_reply_to_id) do
@ -72,8 +109,11 @@ def fix_in_reply_to(%{"inReplyTo" => in_reply_to_id} = object)
def fix_in_reply_to(object), do: object def fix_in_reply_to(object), do: object
def fix_context(object) do def fix_context(object) do
context = object["context"] || object["conversation"] || Utils.generate_context_id()
object object
|> Map.put("context", object["conversation"]) |> Map.put("context", context)
|> Map.put("conversation", context)
end end
def fix_attachments(object) do def fix_attachments(object) do
@ -137,13 +177,22 @@ def fix_content_map(%{"contentMap" => content_map} = object) do
def fix_content_map(object), do: object def fix_content_map(object), do: object
# disallow objects with bogus IDs
def handle_incoming(%{"id" => nil}), do: :error
def handle_incoming(%{"id" => ""}), do: :error
# length of https:// = 8, should validate better, but good enough for now.
def handle_incoming(%{"id" => id}) when not (is_binary(id) and length(id) > 8), do: :error
# TODO: validate those with a Ecto scheme # TODO: validate those with a Ecto scheme
# - tags # - tags
# - emoji # - emoji
def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data) def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data)
when objtype in ["Article", "Note"] do when objtype in ["Article", "Note", "Video"] do
actor = get_actor(data) actor = get_actor(data)
data = Map.put(data, "actor", actor)
data =
Map.put(data, "actor", actor)
|> fix_addressing
with nil <- Activity.get_create_activity_by_object_ap_id(object["id"]), with nil <- Activity.get_create_activity_by_object_ap_id(object["id"]),
%User{} = user <- User.get_or_fetch_by_ap_id(data["actor"]) do %User{} = user <- User.get_or_fetch_by_ap_id(data["actor"]) do

View file

@ -128,7 +128,7 @@ def lazy_put_object_defaults(map, activity \\ %{}) do
Inserts a full object if it is contained in an activity. Inserts a full object if it is contained in an activity.
""" """
def insert_full_object(%{"object" => %{"type" => type} = object_data}) def insert_full_object(%{"object" => %{"type" => type} = object_data})
when is_map(object_data) and type in ["Article", "Note"] do when is_map(object_data) and type in ["Article", "Note", "Video"] do
with {:ok, _} <- Object.create(object_data) do with {:ok, _} <- Object.create(object_data) do
:ok :ok
end end
@ -204,13 +204,17 @@ def update_likes_in_object(likes, object) do
end end
def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do
with likes <- [actor | object.data["likes"] || []] |> Enum.uniq() do likes = if is_list(object.data["likes"]), do: object.data["likes"], else: []
with likes <- [actor | likes] |> Enum.uniq() do
update_likes_in_object(likes, object) update_likes_in_object(likes, object)
end end
end end
def remove_like_from_object(%Activity{data: %{"actor" => actor}}, object) do def remove_like_from_object(%Activity{data: %{"actor" => actor}}, object) do
with likes <- (object.data["likes"] || []) |> List.delete(actor) do likes = if is_list(object.data["likes"]), do: object.data["likes"], else: []
with likes <- likes |> List.delete(actor) do
update_likes_in_object(likes, object) update_likes_in_object(likes, object)
end end
end end
@ -380,7 +384,10 @@ def add_announce_to_object(
}, },
object object
) do ) do
with announcements <- [actor | object.data["announcements"] || []] |> Enum.uniq() do announcements =
if is_list(object.data["announcements"]), do: object.data["announcements"], else: []
with announcements <- [actor | announcements] |> Enum.uniq() do
update_element_in_object("announcement", announcements, object) update_element_in_object("announcement", announcements, object)
end end
end end
@ -388,7 +395,10 @@ def add_announce_to_object(
def add_announce_to_object(_, object), do: {:ok, object} def add_announce_to_object(_, object), do: {:ok, object}
def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do
with announcements <- (object.data["announcements"] || []) |> List.delete(actor) do announcements =
if is_list(object.data["announcements"]), do: object.data["announcements"], else: []
with announcements <- announcements |> List.delete(actor) do
update_element_in_object("announcement", announcements, object) update_element_in_object("announcement", announcements, object)
end end
end end

View file

@ -127,9 +127,6 @@ def render("outbox.json", %{user: user, max_id: max_qid}) do
info = User.user_info(user) info = User.user_info(user)
params = %{ params = %{
"type" => ["Create", "Announce"],
"actor_id" => user.ap_id,
"whole_db" => true,
"limit" => "10" "limit" => "10"
} }
@ -140,10 +137,8 @@ def render("outbox.json", %{user: user, max_id: max_qid}) do
params params
end end
activities = ActivityPub.fetch_public_activities(params) activities = ActivityPub.fetch_user_activities(user, nil, params)
min_id = Enum.at(activities, 0).id min_id = Enum.at(Enum.reverse(activities), 0).id
activities = Enum.reverse(activities)
max_id = Enum.at(activities, 0).id max_id = Enum.at(activities, 0).id
collection = collection =

View file

@ -64,7 +64,6 @@ def to_for_user_and_mentions(_user, mentions, inReplyTo, "direct") do
def make_content_html(status, mentions, attachments, tags, no_attachment_links \\ false) do def make_content_html(status, mentions, attachments, tags, no_attachment_links \\ false) do
status status
|> String.replace("\r", "")
|> format_input(mentions, tags) |> format_input(mentions, tags)
|> maybe_add_attachments(attachments, no_attachment_links) |> maybe_add_attachments(attachments, no_attachment_links)
end end
@ -95,7 +94,7 @@ def add_attachments(text, attachments) do
def format_input(text, mentions, tags) do def format_input(text, mentions, tags) do
text text
|> Formatter.html_escape() |> Formatter.html_escape()
|> String.replace("\n", "<br>") |> String.replace(~r/\r?\n/, "<br>")
|> (&{[], &1}).() |> (&{[], &1}).()
|> Formatter.add_links() |> Formatter.add_links()
|> Formatter.add_user_links(mentions) |> Formatter.add_user_links(mentions)
@ -109,7 +108,7 @@ def add_tag_links(text, tags) do
|> Enum.sort_by(fn {tag, _} -> -String.length(tag) end) |> Enum.sort_by(fn {tag, _} -> -String.length(tag) end)
Enum.reduce(tags, text, fn {full, tag}, text -> Enum.reduce(tags, text, fn {full, tag}, text ->
url = "#<a href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>#{tag}</a>" url = "<a href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>##{tag}</a>"
String.replace(text, full, url) String.replace(text, full, url)
end) end)
end end

View file

@ -5,21 +5,26 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView, ListView} alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView, ListView}
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.{CommonAPI, OStatus} alias Pleroma.Web.CommonAPI
alias Pleroma.Web.OAuth.{Authorization, Token, App} alias Pleroma.Web.OAuth.{Authorization, Token, App}
alias Comeonin.Pbkdf2 alias Comeonin.Pbkdf2
import Ecto.Query import Ecto.Query
require Logger require Logger
@httpoison Application.get_env(:pleroma, :httpoison)
action_fallback(:errors) action_fallback(:errors)
def create_app(conn, params) do def create_app(conn, params) do
with cs <- App.register_changeset(%App{}, params) |> IO.inspect(), with cs <- App.register_changeset(%App{}, params) |> IO.inspect(),
{:ok, app} <- Repo.insert(cs) |> IO.inspect() do {:ok, app} <- Repo.insert(cs) |> IO.inspect() do
res = %{ res = %{
id: app.id, id: app.id |> to_string,
name: app.client_name,
client_id: app.client_id, client_id: app.client_id,
client_secret: app.client_secret client_secret: app.client_secret,
redirect_uri: app.redirect_uris,
website: app.website
} }
json(conn, res) json(conn, res)
@ -653,12 +658,8 @@ def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
fetched = fetched =
if Regex.match?(~r/https?:/, query) do if Regex.match?(~r/https?:/, query) do
with {:ok, activities} <- OStatus.fetch_activity_from_url(query) do with {:ok, object} <- ActivityPub.fetch_object_from_id(query) do
activities [Activity.get_create_activity_by_object_ap_id(object.data["id"])]
|> Enum.filter(fn
%{data: %{"type" => "Create"}} -> true
_ -> false
end)
else else
_e -> [] _e -> []
end end
@ -705,12 +706,8 @@ def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
fetched = fetched =
if Regex.match?(~r/https?:/, query) do if Regex.match?(~r/https?:/, query) do
with {:ok, activities} <- OStatus.fetch_activity_from_url(query) do with {:ok, object} <- ActivityPub.fetch_object_from_id(query) do
activities [Activity.get_create_activity_by_object_ap_id(object.data["id"])]
|> Enum.filter(fn
%{data: %{"type" => "Create"}} -> true
_ -> false
end)
else else
_e -> [] _e -> []
end end
@ -1097,4 +1094,45 @@ def errors(conn, _) do
|> put_status(500) |> put_status(500)
|> json("Something went wrong") |> json("Something went wrong")
end end
@suggestions Application.get_env(:pleroma, :suggestions)
def suggestions(%{assigns: %{user: user}} = conn, _) do
if Keyword.get(@suggestions, :enabled, false) do
api = Keyword.get(@suggestions, :third_party_engine, "")
timeout = Keyword.get(@suggestions, :timeout, 5000)
host =
Application.get_env(:pleroma, Pleroma.Web.Endpoint)
|> Keyword.get(:url)
|> Keyword.get(:host)
user = user.nickname
url = String.replace(api, "{{host}}", host) |> String.replace("{{user}}", user)
with {:ok, %{status_code: 200, body: body}} <-
@httpoison.get(url, [], timeout: timeout, recv_timeout: timeout),
{:ok, data} <- Jason.decode(body) do
data2 =
Enum.slice(data, 0, 40)
|> Enum.map(fn x ->
Map.put(
x,
"id",
case User.get_or_fetch(x["acct"]) do
%{id: id} -> id
_ -> 0
end
)
end)
conn
|> json(data2)
else
e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
end
else
json(conn, [])
end
end
end end

View file

@ -14,6 +14,18 @@ def render("account.json", %{user: user}) do
header = User.banner_url(user) |> MediaProxy.url() header = User.banner_url(user) |> MediaProxy.url()
user_info = User.user_info(user) user_info = User.user_info(user)
emojis =
(user.info["source_data"]["tag"] || [])
|> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
|> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
%{
"shortcode" => String.trim(name, ":"),
"url" => MediaProxy.url(url),
"static_url" => MediaProxy.url(url),
"visible_in_picker" => false
}
end)
%{ %{
id: to_string(user.id), id: to_string(user.id),
username: hd(String.split(user.nickname, "@")), username: hd(String.split(user.nickname, "@")),
@ -24,13 +36,13 @@ def render("account.json", %{user: user}) do
followers_count: user_info.follower_count, followers_count: user_info.follower_count,
following_count: user_info.following_count, following_count: user_info.following_count,
statuses_count: user_info.note_count, statuses_count: user_info.note_count,
note: user.bio || "", note: HtmlSanitizeEx.basic_html(user.bio) || "",
url: user.ap_id, url: user.ap_id,
avatar: image, avatar: image,
avatar_static: image, avatar_static: image,
header: header, header: header,
header_static: header, header_static: header,
emojis: [], emojis: emojis,
fields: [], fields: [],
source: %{ source: %{
note: "", note: "",

View file

@ -99,8 +99,9 @@ def render("status.json", %{activity: %{data: %{"object" => object}} = activity}
repeated = opts[:for] && opts[:for].ap_id in (object["announcements"] || []) repeated = opts[:for] && opts[:for].ap_id in (object["announcements"] || [])
favorited = opts[:for] && opts[:for].ap_id in (object["likes"] || []) favorited = opts[:for] && opts[:for].ap_id in (object["likes"] || [])
attachments = attachment_data = object["attachment"] || []
render_many(object["attachment"] || [], StatusView, "attachment.json", as: :attachment) attachment_data = attachment_data ++ if object["type"] == "Video", do: [object], else: []
attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
created_at = Utils.to_masto_date(object["published"]) created_at = Utils.to_masto_date(object["published"])
@ -151,7 +152,9 @@ def render("status.json", %{activity: %{data: %{"object" => object}} = activity}
end end
def render("attachment.json", %{attachment: attachment}) do def render("attachment.json", %{attachment: attachment}) do
[%{"mediaType" => media_type, "href" => href} | _] = attachment["url"] [attachment_url | _] = attachment["url"]
media_type = attachment_url["mediaType"] || attachment_url["mimeType"]
href = attachment_url["href"]
type = type =
cond do cond do
@ -208,6 +211,19 @@ def get_visibility(object) do
end end
end end
def render_content(%{"type" => "Video"} = object) do
name = object["name"]
content =
if !!name and name != "" do
"<p><a href=\"#{object["id"]}\">#{name}</a></p>#{object["content"]}"
else
object["content"]
end
HtmlSanitizeEx.basic_html(content)
end
def render_content(%{"type" => "Article"} = object) do def render_content(%{"type" => "Article"} = object) do
summary = object["name"] summary = object["name"]

View file

@ -21,6 +21,7 @@ def schemas(conn, _params) do
def nodeinfo(conn, %{"version" => "2.0"}) do def nodeinfo(conn, %{"version" => "2.0"}) do
instance = Application.get_env(:pleroma, :instance) instance = Application.get_env(:pleroma, :instance)
media_proxy = Application.get_env(:pleroma, :media_proxy) media_proxy = Application.get_env(:pleroma, :media_proxy)
suggestions = Application.get_env(:pleroma, :suggestions)
stats = Stats.get_stats() stats = Stats.get_stats()
response = %{ response = %{
@ -45,7 +46,13 @@ def nodeinfo(conn, %{"version" => "2.0"}) do
nodeName: Keyword.get(instance, :name), nodeName: Keyword.get(instance, :name),
nodeDescription: Keyword.get(instance, :description), nodeDescription: Keyword.get(instance, :description),
mediaProxy: Keyword.get(media_proxy, :enabled), mediaProxy: Keyword.get(media_proxy, :enabled),
private: !Keyword.get(instance, :public, true) private: !Keyword.get(instance, :public, true),
suggestions: %{
enabled: Keyword.get(suggestions, :enabled, false),
thirdPartyEngine: Keyword.get(suggestions, :third_party_engine, ""),
timeout: Keyword.get(suggestions, :timeout, 5000),
web: Keyword.get(suggestions, :web, "")
}
} }
} }

View file

@ -142,6 +142,8 @@ def user_fetcher(username) do
get("/domain_blocks", MastodonAPIController, :domain_blocks) get("/domain_blocks", MastodonAPIController, :domain_blocks)
post("/domain_blocks", MastodonAPIController, :block_domain) post("/domain_blocks", MastodonAPIController, :block_domain)
delete("/domain_blocks", MastodonAPIController, :unblock_domain) delete("/domain_blocks", MastodonAPIController, :unblock_domain)
get("/suggestions", MastodonAPIController, :suggestions)
end end
scope "/api/web", Pleroma.Web.MastodonAPI do scope "/api/web", Pleroma.Web.MastodonAPI do
@ -203,9 +205,7 @@ def user_fetcher(username) do
get("/statuses/show/:id", TwitterAPI.Controller, :fetch_status) get("/statuses/show/:id", TwitterAPI.Controller, :fetch_status)
get("/statusnet/conversation/:id", TwitterAPI.Controller, :fetch_conversation) get("/statusnet/conversation/:id", TwitterAPI.Controller, :fetch_conversation)
if @registrations_open do post("/account/register", TwitterAPI.Controller, :register)
post("/account/register", TwitterAPI.Controller, :register)
end
get("/search", TwitterAPI.Controller, :search) get("/search", TwitterAPI.Controller, :search)
get("/statusnet/tags/timeline/:tag", TwitterAPI.Controller, :public_and_external_timeline) get("/statusnet/tags/timeline/:tag", TwitterAPI.Controller, :public_and_external_timeline)
@ -368,6 +368,7 @@ def user_fetcher(username) do
end end
scope "/", Fallback do scope "/", Fallback do
get("/registration/:token", RedirectController, :registration_page)
get("/*path", RedirectController, :redirector) get("/*path", RedirectController, :redirector)
end end
end end
@ -382,4 +383,8 @@ def redirector(conn, _params) do
|> send_file(200, "priv/static/index.html") |> send_file(200, "priv/static/index.html")
end end
end end
def registration_page(conn, params) do
redirector(conn, params)
end
end end

View file

@ -19,7 +19,7 @@
<script id='initial-state' type='application/json'><%= raw @initial_state %></script> <script id='initial-state' type='application/json'><%= raw @initial_state %></script>
<script src="/packs/application.js"></script> <script src="/packs/application.js"></script>
</head> </head>
<body class='app-body no-reduce-motion'> <body class='app-body no-reduce-motion system-font'>
<div class='app-holder' data-props='{&quot;locale&quot;:&quot;en&quot;}' id='mastodon'> <div class='app-holder' data-props='{&quot;locale&quot;:&quot;en&quot;}' id='mastodon'>
</div> </div>
</body> </body>

View file

@ -99,6 +99,10 @@ def do_remote_follow(conn, %{
conn conn
|> render("followed.html", %{error: false}) |> render("followed.html", %{error: false})
else else
# Was already following user
{:error, "Could not follow user:" <> _rest} ->
render(conn, "followed.html", %{error: false})
_e -> _e ->
conn conn
|> render("follow_login.html", %{ |> render("follow_login.html", %{
@ -117,6 +121,11 @@ def do_remote_follow(%{assigns: %{user: user}} = conn, %{"user" => %{"id" => id}
conn conn
|> render("followed.html", %{error: false}) |> render("followed.html", %{error: false})
else else
# Was already following user
{:error, "Could not follow user:" <> _rest} ->
conn
|> render("followed.html", %{error: false})
e -> e ->
Logger.debug("Remote follow failed with error #{inspect(e)}") Logger.debug("Remote follow failed with error #{inspect(e)}")
@ -163,10 +172,9 @@ def config(conn, _params) do
redirectRootLogin: Keyword.get(@instance_fe, :redirect_root_login), redirectRootLogin: Keyword.get(@instance_fe, :redirect_root_login),
chatDisabled: !Keyword.get(@instance_chat, :enabled), chatDisabled: !Keyword.get(@instance_chat, :enabled),
showInstanceSpecificPanel: Keyword.get(@instance_fe, :show_instance_panel), showInstanceSpecificPanel: Keyword.get(@instance_fe, :show_instance_panel),
showWhoToFollowPanel: Keyword.get(@instance_fe, :show_who_to_follow_panel),
scopeOptionsEnabled: Keyword.get(@instance_fe, :scope_options_enabled), scopeOptionsEnabled: Keyword.get(@instance_fe, :scope_options_enabled),
whoToFollowProvider: Keyword.get(@instance_fe, :who_to_follow_provider), collapseMessageWithSubject:
whoToFollowLink: Keyword.get(@instance_fe, :who_to_follow_link) Keyword.get(@instance_fe, :collapse_message_with_subject)
} }
} }
}) })

View file

@ -170,6 +170,15 @@ def to_map(
HtmlSanitizeEx.basic_html(content) HtmlSanitizeEx.basic_html(content)
|> Formatter.emojify(object["emoji"]) |> Formatter.emojify(object["emoji"])
video =
if object["type"] == "Video" do
vid = [object]
else
[]
end
attachments = (object["attachment"] || []) ++ video
%{ %{
"id" => activity.id, "id" => activity.id,
"uri" => activity.data["object"]["id"], "uri" => activity.data["object"]["id"],
@ -181,7 +190,7 @@ def to_map(
"created_at" => created_at, "created_at" => created_at,
"in_reply_to_status_id" => object["inReplyToStatusId"], "in_reply_to_status_id" => object["inReplyToStatusId"],
"statusnet_conversation_id" => conversation_id, "statusnet_conversation_id" => conversation_id,
"attachments" => (object["attachment"] || []) |> ObjectRepresenter.enum_to_list(opts), "attachments" => attachments |> ObjectRepresenter.enum_to_list(opts),
"attentions" => attentions, "attentions" => attentions,
"fave_num" => like_count, "fave_num" => like_count,
"repeat_num" => announcement_count, "repeat_num" => announcement_count,

View file

@ -7,18 +7,20 @@ def to_map(%Object{data: %{"url" => [url | _]}} = object, _opts) do
%{ %{
url: url["href"] |> Pleroma.Web.MediaProxy.url(), url: url["href"] |> Pleroma.Web.MediaProxy.url(),
mimetype: url["mediaType"], mimetype: url["mediaType"] || url["mimeType"],
id: data["uuid"], id: data["uuid"],
oembed: false oembed: false,
description: data["name"]
} }
end end
def to_map(%Object{data: %{"url" => url} = data}, _opts) when is_binary(url) do def to_map(%Object{data: %{"url" => url} = data}, _opts) when is_binary(url) do
%{ %{
url: url |> Pleroma.Web.MediaProxy.url(), url: url |> Pleroma.Web.MediaProxy.url(),
mimetype: data["mediaType"], mimetype: data["mediaType"] || url["mimeType"],
id: data["uuid"], id: data["uuid"],
oembed: false oembed: false,
description: data["name"]
} }
end end

View file

@ -1,11 +1,13 @@
defmodule Pleroma.Web.TwitterAPI.TwitterAPI do defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
alias Pleroma.{User, Activity, Repo, Object} alias Pleroma.{UserInviteToken, User, Activity, Repo, Object}
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.TwitterAPI.UserView alias Pleroma.Web.TwitterAPI.UserView
alias Pleroma.Web.{OStatus, CommonAPI} alias Pleroma.Web.{OStatus, CommonAPI}
import Ecto.Query import Ecto.Query
@instance Application.get_env(:pleroma, :instance)
@httpoison Application.get_env(:pleroma, :httpoison) @httpoison Application.get_env(:pleroma, :httpoison)
@registrations_open Keyword.get(@instance, :registrations_open)
def create_status(%User{} = user, %{"status" => _} = data) do def create_status(%User{} = user, %{"status" => _} = data) do
CommonAPI.post(user, data) CommonAPI.post(user, data)
@ -120,6 +122,8 @@ def upload(%Plug.Upload{} = file, format \\ "xml") do
end end
def register_user(params) do def register_user(params) do
tokenString = params["token"]
params = %{ params = %{
nickname: params["nickname"], nickname: params["nickname"],
name: params["fullname"], name: params["fullname"],
@ -129,17 +133,33 @@ def register_user(params) do
password_confirmation: params["confirm"] password_confirmation: params["confirm"]
} }
changeset = User.register_changeset(%User{}, params) # no need to query DB if registration is open
token =
unless @registrations_open || is_nil(tokenString) do
Repo.get_by(UserInviteToken, %{token: tokenString})
end
with {:ok, user} <- Repo.insert(changeset) do cond do
{:ok, user} @registrations_open || (!is_nil(token) && !token.used) ->
else changeset = User.register_changeset(%User{}, params)
{:error, changeset} ->
errors =
Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
|> Jason.encode!()
{:error, %{error: errors}} 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 end

View file

@ -1,7 +1,9 @@
defmodule Pleroma.Web.TwitterAPI.Controller do defmodule Pleroma.Web.TwitterAPI.Controller do
use Pleroma.Web, :controller use Pleroma.Web, :controller
alias Pleroma.Formatter
alias Pleroma.Web.TwitterAPI.{TwitterAPI, UserView, ActivityView, NotificationView} alias Pleroma.Web.TwitterAPI.{TwitterAPI, UserView, ActivityView, NotificationView}
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
alias Pleroma.{Repo, Activity, User, Notification} alias Pleroma.{Repo, Activity, User, Notification}
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
@ -411,8 +413,18 @@ def raw_empty_array(conn, _params) do
def update_profile(%{assigns: %{user: user}} = conn, params) do def update_profile(%{assigns: %{user: user}} = conn, params) do
params = params =
if bio = params["description"] do if bio = params["description"] do
bio_brs = Regex.replace(~r/\r?\n/, bio, "<br>") mentions = Formatter.parse_mentions(bio)
Map.put(params, "bio", bio_brs) tags = Formatter.parse_tags(bio)
emoji =
(user.info["source_data"]["tag"] || [])
|> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
|> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
{String.trim(name, ":"), url}
end)
bio_html = CommonUtils.format_input(bio, mentions, tags)
Map.put(params, "bio", bio_html |> Formatter.emojify(emoji))
else else
params params
end end

View file

@ -1,6 +1,7 @@
defmodule Pleroma.Web.TwitterAPI.UserView do defmodule Pleroma.Web.TwitterAPI.UserView do
use Pleroma.Web, :view use Pleroma.Web, :view
alias Pleroma.User alias Pleroma.User
alias Pleroma.Formatter
alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MediaProxy alias Pleroma.Web.MediaProxy
@ -28,9 +29,18 @@ def render("user.json", %{user: user = %User{}} = assigns) do
user_info = User.get_cached_user_info(user) user_info = User.get_cached_user_info(user)
emoji =
(user.info["source_data"]["tag"] || [])
|> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
|> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
{String.trim(name, ":"), url}
end)
data = %{ data = %{
"created_at" => user.inserted_at |> Utils.format_naive_asctime(), "created_at" => user.inserted_at |> Utils.format_naive_asctime(),
"description" => HtmlSanitizeEx.strip_tags(user.bio), "description" =>
HtmlSanitizeEx.strip_tags((user.bio || "") |> String.replace("<br>", "\n")),
"description_html" => HtmlSanitizeEx.basic_html(user.bio),
"favourites_count" => 0, "favourites_count" => 0,
"followers_count" => user_info[:follower_count], "followers_count" => user_info[:follower_count],
"following" => following, "following" => following,
@ -39,6 +49,7 @@ def render("user.json", %{user: user = %User{}} = assigns) do
"friends_count" => user_info[:following_count], "friends_count" => user_info[:following_count],
"id" => user.id, "id" => user.id,
"name" => user.name, "name" => user.name,
"name_html" => HtmlSanitizeEx.strip_tags(user.name) |> Formatter.emojify(emoji),
"profile_image_url" => image, "profile_image_url" => image,
"profile_image_url_https" => image, "profile_image_url_https" => image,
"profile_image_url_profile_size" => image, "profile_image_url_profile_size" => image,

34
mix.exs
View file

@ -30,25 +30,25 @@ defp elixirc_paths(_), do: ["lib"]
# Type `mix help deps` for examples and options. # Type `mix help deps` for examples and options.
defp deps do defp deps do
[ [
{:phoenix, "~> 1.3.0"}, {:phoenix, "~> 1.3.3"},
{:phoenix_pubsub, "~> 1.0"}, {:phoenix_pubsub, "~> 1.0.2"},
{:phoenix_ecto, "~> 3.2"}, {:phoenix_ecto, "~> 3.3"},
{:postgrex, ">= 0.0.0"}, {:postgrex, ">= 0.13.5"},
{:gettext, "~> 0.11"}, {:gettext, "~> 0.15"},
{:cowboy, "~> 1.0", override: true}, {:cowboy, "~> 1.1.2", override: true},
{:comeonin, "~> 4.0"}, {:comeonin, "~> 4.1.1"},
{:pbkdf2_elixir, "~> 0.12"}, {:pbkdf2_elixir, "~> 0.12.3"},
{:trailing_format_plug, "~> 0.0.5"}, {:trailing_format_plug, "~> 0.0.7"},
{:html_sanitize_ex, "~> 1.3.0-rc1"}, {:html_sanitize_ex, "~> 1.3.0"},
{:phoenix_html, "~> 2.10"}, {:phoenix_html, "~> 2.10"},
{:calendar, "~> 0.16.1"}, {:calendar, "~> 0.17.4"},
{:cachex, "~> 3.0"}, {:cachex, "~> 3.0.2"},
{:httpoison, "~> 1.1.0"}, {:httpoison, "~> 1.2.0"},
{:jason, "~> 1.0"}, {:jason, "~> 1.0"},
{:ex_machina, "~> 2.0", only: :test}, {:mogrify, "~> 0.6.1"},
{:credo, "~> 0.7", only: [:dev, :test]}, {:ex_machina, "~> 2.2", only: :test},
{:mock, "~> 0.3.0", only: :test}, {:credo, "~> 0.9.3", only: [:dev, :test]},
{:mogrify, "~> 0.6.1"} {:mock, "~> 0.3.1", only: :test}
] ]
end end

View file

@ -1,45 +1,45 @@
%{ %{
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"},
"cachex": {:hex, :cachex, "3.0.2", "1351caa4e26e29f7d7ec1d29b53d6013f0447630bbf382b4fb5d5bad0209f203", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm"}, "cachex": {:hex, :cachex, "3.0.2", "1351caa4e26e29f7d7ec1d29b53d6013f0447630bbf382b4fb5d5bad0209f203", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm"},
"calendar": {:hex, :calendar, "0.16.1", "782327ad8bae7c797b887840dc4ddb933f05ce6e333e5b04964d7a5d5f79bde3", [:mix], [{:tzdata, "~> 0.5.8 or ~> 0.1.201603", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, "calendar": {:hex, :calendar, "0.17.4", "22c5e8d98a4db9494396e5727108dffb820ee0d18fed4b0aa8ab76e4f5bc32f1", [:mix], [{:tzdata, "~> 0.5.8 or ~> 0.1.201603", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
"certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [:rebar3], [{:parse_trans, "3.2.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, "certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [:rebar3], [{:parse_trans, "3.2.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
"comeonin": {:hex, :comeonin, "4.1.1", "c7304fc29b45b897b34142a91122bc72757bc0c295e9e824999d5179ffc08416", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm"}, "comeonin": {:hex, :comeonin, "4.1.1", "c7304fc29b45b897b34142a91122bc72757bc0c295e9e824999d5179ffc08416", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm"},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"},
"cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
"cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"}, "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"},
"credo": {:hex, :credo, "0.9.2", "841d316612f568beb22ba310d816353dddf31c2d94aa488ae5a27bb53760d0bf", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "credo": {:hex, :credo, "0.9.3", "76fa3e9e497ab282e0cf64b98a624aa11da702854c52c82db1bf24e54ab7c97a", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
"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"}, "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"}, "decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [: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"}, "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"}, "eternal": {:hex, :eternal, "1.2.0", "e2a6b6ce3b8c248f7dc31451aefca57e3bdf0e48d73ae5043229380a67614c41", [:mix], [], "hexpm"},
"ex_machina": {:hex, :ex_machina, "2.2.0", "fec496331e04fc2db2a1a24fe317c12c0c4a50d2beb8ebb3531ed1f0d84be0ed", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, "ex_machina": {:hex, :ex_machina, "2.2.0", "fec496331e04fc2db2a1a24fe317c12c0c4a50d2beb8ebb3531ed1f0d84be0ed", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
"gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm"}, "gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm"},
"hackney": {:hex, :hackney, "1.12.1", "8bf2d0e11e722e533903fe126e14d6e7e94d9b7983ced595b75f532e04b7fdc7", [:rebar3], [{:certifi, "2.3.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, "hackney": {:hex, :hackney, "1.13.0", "24edc8cd2b28e1c652593833862435c80661834f6c9344e84b6a2255e7aeef03", [:rebar3], [{:certifi, "2.3.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.2", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
"html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"}, "html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"},
"httpoison": {:hex, :httpoison, "1.1.1", "96ed7ab79f78a31081bb523eefec205fd2900a02cda6dbc2300e7a1226219566", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "httpoison": {:hex, :httpoison, "1.2.0", "2702ed3da5fd7a8130fc34b11965c8cfa21ade2f232c00b42d96d4967c39a3a3", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"idna": {:hex, :idna, "5.1.1", "cbc3b2fa1645113267cc59c760bafa64b2ea0334635ef06dbac8801e42f7279c", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, "idna": {:hex, :idna, "5.1.2", "e21cb58a09f0228a9e0b95eaa1217f1bcfc31a1aaa6e1fdf2f53a33f7dbd9494", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
"jason": {:hex, :jason, "1.0.0", "0f7cfa9bdb23fed721ec05419bcee2b2c21a77e926bce0deda029b5adc716fe2", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, "jason": {:hex, :jason, "1.0.0", "0f7cfa9bdb23fed721ec05419bcee2b2c21a77e926bce0deda029b5adc716fe2", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
"meck": {:hex, :meck, "0.8.9", "64c5c0bd8bcca3a180b44196265c8ed7594e16bcc845d0698ec6b4e577f48188", [:rebar3], [], "hexpm"}, "meck": {:hex, :meck, "0.8.9", "64c5c0bd8bcca3a180b44196265c8ed7594e16bcc845d0698ec6b4e577f48188", [:rebar3], [], "hexpm"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},
"mime": {:hex, :mime, "1.2.0", "78adaa84832b3680de06f88f0997e3ead3b451a440d183d688085be2d709b534", [:mix], [], "hexpm"}, "mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm"},
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"},
"mochiweb": {:hex, :mochiweb, "2.15.0", "e1daac474df07651e5d17cc1e642c4069c7850dc4508d3db7263a0651330aacc", [: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"}, "mock": {:hex, :mock, "0.3.1", "994f00150f79a0ea50dc9d86134cd9ebd0d177ad60bd04d1e46336cdfdb98ff9", [:mix], [{:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"},
"mogrify": {:hex, :mogrify, "0.6.1", "de1b527514f2d95a7bbe9642eb556061afb337e220cf97adbf3a4e6438ed70af", [:mix], [], "hexpm"}, "mogrify": {:hex, :mogrify, "0.6.1", "de1b527514f2d95a7bbe9642eb556061afb337e220cf97adbf3a4e6438ed70af", [:mix], [], "hexpm"},
"parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm"}, "parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm"},
"pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.3", "6706a148809a29c306062862c803406e88f048277f6e85b68faf73291e820b84", [:mix], [], "hexpm"}, "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.3", "6706a148809a29c306062862c803406e88f048277f6e85b68faf73291e820b84", [:mix], [], "hexpm"},
"phoenix": {:hex, :phoenix, "1.3.2", "2a00d751f51670ea6bc3f2ba4e6eb27ecb8a2c71e7978d9cd3e5de5ccf7378bd", [: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": {: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_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_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_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"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.2", "bfa7fd52788b5eaa09cb51ff9fcad1d9edfeb68251add458523f839392f034c1", [:mix], [], "hexpm"},
"plug": {:hex, :plug, "1.5.1", "1ff35bdecfb616f1a2b1c935ab5e4c47303f866cb929d2a76f0541e553a58165", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.3", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "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"},
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
"poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "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"}, "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"},
"ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"}, "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"},
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"tzdata": {:hex, :tzdata, "0.5.16", "13424d3afc76c68ff607f2df966c0ab4f3258859bbe3c979c9ed1606135e7352", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "tzdata": {:hex, :tzdata, "0.5.17", "50793e3d85af49736701da1a040c415c97dc1caf6464112fd9bd18f425d3053b", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"},
"unsafe": {:hex, :unsafe, "1.0.0", "7c21742cd05380c7875546b023481d3a26f52df8e5dfedcb9f958f322baae305", [:mix], [], "hexpm"}, "unsafe": {:hex, :unsafe, "1.0.0", "7c21742cd05380c7875546b023481d3a26f52df8e5dfedcb9f958f322baae305", [:mix], [], "hexpm"},
} }

View file

@ -0,0 +1,12 @@
defmodule Pleroma.Repo.Migrations.CreateUserInviteTokens do
use Ecto.Migration
def change do
create table(:user_invite_tokens) do
add :token, :string
add :used, :boolean, default: false
timestamps()
end
end
end

View file

@ -1 +1 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Pleroma</title><link rel=icon type=image/png href=/favicon.png><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/app.5d0189b6f119febde070b703869bbd06.css rel=stylesheet></head><body style="display: none"><div id=app></div><script type=text/javascript src=/static/js/manifest.f2341edd686e54ee9b4a.js></script><script type=text/javascript src=/static/js/vendor.a93310d51acbd9480094.js></script><script type=text/javascript src=/static/js/app.de965bb2a0a8bffbeafa.js></script></body></html> <!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Pleroma</title><link rel=icon type=image/png href=/favicon.png><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/app.b76dcba564bc8d13e27c546e95fbb72e.css rel=stylesheet></head><body style="display: none"><div id=app></div><script type=text/javascript src=/static/js/manifest.004e63f0a7e5719fbbe8.js></script><script type=text/javascript src=/static/js/vendor.48cf760f1485c83eb636.js></script><script type=text/javascript src=/static/js/app.d3eba781c5f30aabbb47.js></script></body></html>

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,2 +1,2 @@
webpackJsonp([69],{658:function(e,c,t){"use strict";function o(e){var c=e.detail,t=c[0],o=document.querySelector('[data-id="'+t.id+'"]');o&&o.parentNode.removeChild(o)}Object.defineProperty(c,"__esModule",{value:!0});var n=t(144);t.n(n);[].forEach.call(document.querySelectorAll(".trash-button"),function(e){e.addEventListener("ajax:success",o)});Object(n.delegate)(document,"#batch_checkbox_all","change",function(e){var c=e.target;[].forEach.call(document.querySelectorAll('.batch-checkbox input[type="checkbox"]'),function(e){e.checked=c.checked})}),Object(n.delegate)(document,'.batch-checkbox input[type="checkbox"]',"change",function(){var e=document.querySelector("#batch_checkbox_all");e&&(e.checked=[].every.call(document.querySelectorAll('.batch-checkbox input[type="checkbox"]'),function(e){return e.checked}))}),Object(n.delegate)(document,".media-spoiler-show-button","click",function(){[].forEach.call(document.querySelectorAll("button.media-spoiler"),function(e){e.click()})}),Object(n.delegate)(document,".media-spoiler-hide-button","click",function(){[].forEach.call(document.querySelectorAll(".spoiler-button.spoiler-button--visible button"),function(e){e.click()})})}},[658]); webpackJsonp([85],{661:function(e,c,t){"use strict";function o(e){var c=e.detail,t=c[0],o=document.querySelector('[data-id="'+t.id+'"]');o&&o.parentNode.removeChild(o)}Object.defineProperty(c,"__esModule",{value:!0});var n=t(152);t.n(n);[].forEach.call(document.querySelectorAll(".trash-button"),function(e){e.addEventListener("ajax:success",o)});var l='.batch-checkbox input[type="checkbox"]';Object(n.delegate)(document,"#batch_checkbox_all","change",function(e){var c=e.target;[].forEach.call(document.querySelectorAll(l),function(e){e.checked=c.checked})}),Object(n.delegate)(document,l,"change",function(){var e=document.querySelector("#batch_checkbox_all");e&&(e.checked=[].every.call(document.querySelectorAll(l),function(e){return e.checked}),e.indeterminate=!e.checked&&[].some.call(document.querySelectorAll(l),function(e){return e.checked}))}),Object(n.delegate)(document,".media-spoiler-show-button","click",function(){[].forEach.call(document.querySelectorAll("button.media-spoiler"),function(e){e.click()})}),Object(n.delegate)(document,".media-spoiler-hide-button","click",function(){[].forEach.call(document.querySelectorAll(".spoiler-button.spoiler-button--visible button"),function(e){e.click()})})}},[661]);
//# sourceMappingURL=admin.js.map //# sourceMappingURL=admin.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,5 @@
CACHE MANIFEST CACHE MANIFEST
#ver:2018-4-9 21:57:37 #ver:2018-8-12 18:01:32
#plugin:4.8.4 #plugin:4.8.4
CACHE: CACHE:
@ -13,33 +13,46 @@ CACHE:
/packs/features/home_timeline.js /packs/features/home_timeline.js
/packs/features/public_timeline.js /packs/features/public_timeline.js
/packs/features/community_timeline.js /packs/features/community_timeline.js
/packs/features/favourited_statuses.js /packs/features/direct_timeline.js
/packs/features/list_timeline.js /packs/features/pinned_statuses.js
/packs/features/domain_blocks.js
/packs/features/following.js /packs/features/following.js
/packs/features/followers.js /packs/features/followers.js
/packs/features/favourited_statuses.js
/packs/features/list_timeline.js
/packs/features/account_gallery.js
/packs/features/hashtag_timeline.js /packs/features/hashtag_timeline.js
/packs/features/status.js /packs/features/status.js
/packs/features/account_gallery.js /packs/features/lists.js
/packs/features/blocks.js /packs/modals/report_modal.js
/packs/features/getting_started.js
/packs/features/follow_requests.js /packs/features/follow_requests.js
/packs/features/mutes.js
/packs/features/blocks.js
/packs/features/reblogs.js /packs/features/reblogs.js
/packs/features/favourites.js /packs/features/favourites.js
/packs/features/getting_started.js
/packs/features/keyboard_shortcuts.js /packs/features/keyboard_shortcuts.js
/packs/modals/mute_modal.js
/packs/features/generic_not_found.js /packs/features/generic_not_found.js
/packs/features/list_editor.js /packs/features/list_editor.js
/packs/modals/embed_modal.js
/packs/status/media_gallery.js /packs/status/media_gallery.js
/packs/containers/media_container.js
/packs/share.js /packs/share.js
/packs/application.js /packs/application.js
/packs/about.js /packs/about.js
/packs/public.js
/packs/mailer.js /packs/mailer.js
/packs/mastodon-light.js
/packs/contrast.js
/packs/default.js /packs/default.js
/packs/public.js
/packs/admin.js /packs/admin.js
/packs/common.js /packs/common.js
/packs/common.css /packs/common.css
/packs/mailer.css /packs/mailer.css
/packs/default.css /packs/default.css
/packs/contrast.css
/packs/mastodon-light.css
/packs/manifest.json /packs/manifest.json
NETWORK: NETWORK:

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"version":3,"sources":[],"names":[],"mappings":"","file":"contrast.css","sourceRoot":""}

View file

@ -0,0 +1,2 @@
webpackJsonp([82],{803:function(n,c){}},[803]);
//# sourceMappingURL=contrast.js.map

View file

@ -0,0 +1 @@
{"version":3,"sources":["webpack:///contrast.js"],"names":["webpackJsonp","803","module","exports"],"mappings":"AAAAA,cAAc,KAERC,IACA,SAAUC,EAAQC,OAMrB","file":"contrast.js","sourcesContent":["webpackJsonp([82],{\n\n/***/ 803:\n/***/ (function(module, exports) {\n\n// removed by extract-text-webpack-plugin\n\n/***/ })\n\n},[803]);\n\n\n// WEBPACK FOOTER //\n// contrast.js"],"sourceRoot":""}

File diff suppressed because one or more lines are too long

View file

@ -1,2 +1,2 @@
webpackJsonp([68],{799:function(n,c){}},[799]); webpackJsonp([83],{802:function(n,c){}},[802]);
//# sourceMappingURL=default.js.map //# sourceMappingURL=default.js.map

View file

@ -1 +1 @@
{"version":3,"sources":["webpack:///default.js"],"names":["webpackJsonp","799","module","exports"],"mappings":"AAAAA,cAAc,KAERC,IACA,SAAUC,EAAQC,OAMrB","file":"default.js","sourcesContent":["webpackJsonp([68],{\n\n/***/ 799:\n/***/ (function(module, exports) {\n\n// removed by extract-text-webpack-plugin\n\n/***/ })\n\n},[799]);\n\n\n// WEBPACK FOOTER //\n// default.js"],"sourceRoot":""} {"version":3,"sources":["webpack:///default.js"],"names":["webpackJsonp","802","module","exports"],"mappings":"AAAAA,cAAc,KAERC,IACA,SAAUC,EAAQC,OAMrB","file":"default.js","sourcesContent":["webpackJsonp([83],{\n\n/***/ 802:\n/***/ (function(module, exports) {\n\n// removed by extract-text-webpack-plugin\n\n/***/ })\n\n},[802]);\n\n\n// WEBPACK FOOTER //\n// default.js"],"sourceRoot":""}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.3 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,2 +1,2 @@
webpackJsonp([23],{150:function(n,e,i){"use strict";i.d(e,"a",function(){return v});var t=i(2),r=i.n(t),o=i(1),a=i.n(o),c=i(3),l=i.n(c),u=i(4),s=i.n(u),d=i(0),f=i.n(d),h=i(10),p=i.n(h),v=function(n){function e(){var i,t,r;a()(this,e);for(var o=arguments.length,c=Array(o),u=0;u<o;u++)c[u]=arguments[u];return i=t=l()(this,n.call.apply(n,[this].concat(c))),t.handleClick=function(){t.props.onClick()},r=i,l()(t,r)}return s()(e,n),e.prototype.render=function(){var n=this.props,e=n.icon,i=n.type,t=n.active,o=n.columnHeaderId,a="";return e&&(a=r()("i",{className:"fa fa-fw fa-"+e+" column-header__icon"})),r()("h1",{className:p()("column-header",{active:t}),id:o||null},void 0,r()("button",{onClick:this.handleClick},void 0,a,i))},e}(f.a.PureComponent)},284:function(n,e,i){"use strict";i.d(e,"a",function(){return g});var t=i(2),r=i.n(t),o=i(1),a=i.n(o),c=i(3),l=i.n(c),u=i(4),s=i.n(u),d=i(34),f=i.n(d),h=i(0),p=i.n(h),v=i(150),b=i(91),m=i(35),g=function(n){function e(){var i,t,r;a()(this,e);for(var o=arguments.length,c=Array(o),u=0;u<o;u++)c[u]=arguments[u];return i=t=l()(this,n.call.apply(n,[this].concat(c))),t.handleHeaderClick=function(){var n=t.node.querySelector(".scrollable");n&&(t._interruptScrollAnimation=Object(b.b)(n))},t.handleScroll=f()(function(){void 0!==t._interruptScrollAnimation&&t._interruptScrollAnimation()},200),t.setRef=function(n){t.node=n},r=i,l()(t,r)}return s()(e,n),e.prototype.scrollTop=function(){var n=this.node.querySelector(".scrollable");n&&(this._interruptScrollAnimation=Object(b.b)(n))},e.prototype.render=function(){var n=this.props,e=n.heading,i=n.icon,t=n.children,o=n.active,a=n.hideHeadingOnMobile,c=e&&(!a||a&&!Object(m.b)(window.innerWidth)),l=c&&e.replace(/ /g,"-"),u=c&&r()(v.a,{icon:i,active:o,type:e,onClick:this.handleHeaderClick,columnHeaderId:l});return p.a.createElement("div",{ref:this.setRef,role:"region","aria-labelledby":l,className:"column",onScroll:this.handleScroll},u,t)},e}(p.a.PureComponent)},820:function(n,e,i){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var t=i(2),r=i.n(t),o=i(0),a=(i.n(o),i(284)),c=i(843),l=function(){return r()(a.a,{},void 0,r()(c.a,{}))};e.default=l},843:function(n,e,i){"use strict";var t=i(2),r=i.n(t),o=i(0),a=(i.n(o),i(6)),c=function(){return r()("div",{className:"regeneration-indicator missing-indicator"},void 0,r()("div",{},void 0,r()("div",{className:"regeneration-indicator__label"},void 0,r()(a.b,{id:"missing_indicator.label",tagName:"strong",defaultMessage:"Not found"}),r()(a.b,{id:"missing_indicator.sublabel",defaultMessage:"This resource could not be found"}))))};e.a=c}}); webpackJsonp([30],{155:function(n,e,i){"use strict";i.d(e,"a",function(){return v});var t=i(2),r=i.n(t),o=i(1),a=i.n(o),c=i(3),l=i.n(c),u=i(4),s=i.n(u),d=i(0),f=i.n(d),h=i(10),p=i.n(h),v=function(n){function e(){var i,t,r;a()(this,e);for(var o=arguments.length,c=Array(o),u=0;u<o;u++)c[u]=arguments[u];return i=t=l()(this,n.call.apply(n,[this].concat(c))),t.handleClick=function(){t.props.onClick()},r=i,l()(t,r)}return s()(e,n),e.prototype.render=function(){var n=this.props,e=n.icon,i=n.type,t=n.active,o=n.columnHeaderId,a="";return e&&(a=r()("i",{className:"fa fa-fw fa-"+e+" column-header__icon"})),r()("h1",{className:p()("column-header",{active:t}),id:o||null},void 0,r()("button",{onClick:this.handleClick},void 0,a,i))},e}(f.a.PureComponent)},274:function(n,e,i){"use strict";i.d(e,"a",function(){return g});var t=i(2),r=i.n(t),o=i(1),a=i.n(o),c=i(3),l=i.n(c),u=i(4),s=i.n(u),d=i(32),f=i.n(d),h=i(0),p=i.n(h),v=i(155),m=i(91),b=i(43),g=function(n){function e(){var i,t,r;a()(this,e);for(var o=arguments.length,c=Array(o),u=0;u<o;u++)c[u]=arguments[u];return i=t=l()(this,n.call.apply(n,[this].concat(c))),t.handleHeaderClick=function(){var n=t.node.querySelector(".scrollable");n&&(t._interruptScrollAnimation=Object(m.b)(n))},t.handleScroll=f()(function(){void 0!==t._interruptScrollAnimation&&t._interruptScrollAnimation()},200),t.setRef=function(n){t.node=n},r=i,l()(t,r)}return s()(e,n),e.prototype.scrollTop=function(){var n=this.node.querySelector(".scrollable");n&&(this._interruptScrollAnimation=Object(m.b)(n))},e.prototype.render=function(){var n=this.props,e=n.heading,i=n.icon,t=n.children,o=n.active,a=n.hideHeadingOnMobile,c=e&&(!a||a&&!Object(b.b)(window.innerWidth)),l=c&&e.replace(/ /g,"-"),u=c&&r()(v.a,{icon:i,active:o,type:e,onClick:this.handleHeaderClick,columnHeaderId:l});return p.a.createElement("div",{ref:this.setRef,role:"region","aria-labelledby":l,className:"column",onScroll:this.handleScroll},u,t)},e}(p.a.PureComponent)},829:function(n,e,i){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var t=i(2),r=i.n(t),o=i(0),a=(i.n(o),i(274)),c=i(866),l=function(){return r()(a.a,{},void 0,r()(c.a,{}))};e.default=l},866:function(n,e,i){"use strict";var t=i(2),r=i.n(t),o=i(0),a=(i.n(o),i(7)),c=function(){return r()("div",{className:"regeneration-indicator missing-indicator"},void 0,r()("div",{},void 0,r()("div",{className:"regeneration-indicator__figure"}),r()("div",{className:"regeneration-indicator__label"},void 0,r()(a.b,{id:"missing_indicator.label",tagName:"strong",defaultMessage:"Not found"}),r()(a.b,{id:"missing_indicator.sublabel",defaultMessage:"This resource could not be found"}))))};e.a=c}});
//# sourceMappingURL=generic_not_found.js.map //# sourceMappingURL=generic_not_found.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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