Format the code.

This commit is contained in:
lain 2018-03-30 15:01:53 +02:00
parent 480932c8e5
commit 4afbef39f4
111 changed files with 4912 additions and 2769 deletions

View file

@ -6,14 +6,11 @@
use Mix.Config use Mix.Config
# General application configuration # General application configuration
config :pleroma, config :pleroma, ecto_repos: [Pleroma.Repo]
ecto_repos: [Pleroma.Repo]
config :pleroma, Pleroma.Repo, config :pleroma, Pleroma.Repo, types: Pleroma.PostgresTypes
types: Pleroma.PostgresTypes
config :pleroma, Pleroma.Upload, config :pleroma, Pleroma.Upload, uploads: "uploads"
uploads: "uploads"
# Configures the endpoint # Configures the endpoint
config :pleroma, Pleroma.Web.Endpoint, config :pleroma, Pleroma.Web.Endpoint,
@ -21,8 +18,7 @@
protocol: "https", protocol: "https",
secret_key_base: "aK4Abxf29xU9TTDKre9coZPUgevcVCFQJe/5xP/7Lt4BEif6idBIbjupVbOrbKxl", secret_key_base: "aK4Abxf29xU9TTDKre9coZPUgevcVCFQJe/5xP/7Lt4BEif6idBIbjupVbOrbKxl",
render_errors: [view: Pleroma.Web.ErrorView, accepts: ~w(json)], render_errors: [view: Pleroma.Web.ErrorView, accepts: ~w(json)],
pubsub: [name: Pleroma.PubSub, pubsub: [name: Pleroma.PubSub, adapter: Phoenix.PubSub.PG2]
adapter: Phoenix.PubSub.PG2]
# Configures Elixir's Logger # Configures Elixir's Logger
config :logger, :console, config :logger, :console,
@ -38,15 +34,15 @@
config :pleroma, :ostatus, Pleroma.Web.OStatus config :pleroma, :ostatus, Pleroma.Web.OStatus
config :pleroma, :httpoison, Pleroma.HTTP config :pleroma, :httpoison, Pleroma.HTTP
version = with {version, 0} <- System.cmd("git", ["rev-parse", "HEAD"]) do version =
"Pleroma #{Mix.Project.config[:version]} #{String.trim(version)}" with {version, 0} <- System.cmd("git", ["rev-parse", "HEAD"]) do
else "Pleroma #{Mix.Project.config()[:version]} #{String.trim(version)}"
_ -> "Pleroma #{Mix.Project.config[:version]} dev" else
end _ -> "Pleroma #{Mix.Project.config()[:version]} dev"
end
# Configures http settings, upstream proxy etc. # Configures http settings, upstream proxy etc.
config :pleroma, :http, config :pleroma, :http, proxy_url: nil
proxy_url: nil
config :pleroma, :instance, config :pleroma, :instance,
version: version, version: version,
@ -59,16 +55,15 @@
config :pleroma, :media_proxy, config :pleroma, :media_proxy,
enabled: false, enabled: false,
redirect_on_failure: true redirect_on_failure: true
#base_url: "https://cache.pleroma.social"
config :pleroma, :chat, # base_url: "https://cache.pleroma.social"
enabled: true
config :pleroma, :chat, enabled: true
config :ecto, json_library: Jason config :ecto, json_library: Jason
config :phoenix, :format_encoders, config :phoenix, :format_encoders, json: Jason
json: Jason
# 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

@ -7,7 +7,10 @@
# watchers to your application. For example, we use it # watchers to your application. For example, we use it
# with brunch.io to recompile .js and .css sources. # with brunch.io to recompile .js and .css sources.
config :pleroma, Pleroma.Web.Endpoint, config :pleroma, Pleroma.Web.Endpoint,
http: [port: 4000, protocol_options: [max_request_line_length: 8192, max_header_value_length: 8192]], http: [
port: 4000,
protocol_options: [max_request_line_length: 8192, max_header_value_length: 8192]
],
protocol: "http", protocol: "http",
debug_errors: true, debug_errors: true,
code_reloader: true, code_reloader: true,
@ -49,5 +52,8 @@
try do try do
import_config "dev.secret.exs" import_config "dev.secret.exs"
rescue rescue
_-> IO.puts("!!! RUNNING IN LOCALHOST DEV MODE! !!!\nFEDERATION WON'T WORK UNTIL YOU CONFIGURE A dev.secret.exs") _ ->
IO.puts(
"!!! RUNNING IN LOCALHOST DEV MODE! !!!\nFEDERATION WON'T WORK UNTIL YOU CONFIGURE A dev.secret.exs"
)
end end

View file

@ -9,8 +9,7 @@
# Print only warnings and errors during test # Print only warnings and errors during test
config :logger, level: :warn config :logger, level: :warn
config :pleroma, Pleroma.Upload, config :pleroma, Pleroma.Upload, uploads: "test/uploads"
uploads: "test/uploads"
# Configure your database # Configure your database
config :pleroma, Pleroma.Repo, config :pleroma, Pleroma.Repo,
@ -21,7 +20,6 @@
hostname: System.get_env("DB_HOST") || "localhost", hostname: System.get_env("DB_HOST") || "localhost",
pool: Ecto.Adapters.SQL.Sandbox pool: Ecto.Adapters.SQL.Sandbox
# Reduce hash rounds for testing # Reduce hash rounds for testing
config :comeonin, :pbkdf2_rounds, 1 config :comeonin, :pbkdf2_rounds, 1

View file

@ -1 +1,5 @@
Postgrex.Types.define(Pleroma.PostgresTypes, [] ++ Ecto.Adapters.Postgres.extensions(), json: Jason) Postgrex.Types.define(
Pleroma.PostgresTypes,
[] ++ Ecto.Adapters.Postgres.extensions(),
json: Jason
)

View file

@ -8,12 +8,16 @@ defmodule Mix.Tasks.FixApUsers do
def run([]) do def run([]) do
Mix.Task.run("app.start") Mix.Task.run("app.start")
q = from u in User, q =
where: fragment("? @> ?", u.info, ^%{"ap_enabled" => true}), from(
where: u.local == false u in User,
where: fragment("? @> ?", u.info, ^%{"ap_enabled" => true}),
where: u.local == false
)
users = Repo.all(q) users = Repo.all(q)
Enum.each(users, fn(user) -> Enum.each(users, fn user ->
try do try do
IO.puts("Fetching #{user.nickname}") IO.puts("Fetching #{user.nickname}")
Pleroma.Web.ActivityPub.Transmogrifier.upgrade_user_from_ap_id(user.ap_id, false) Pleroma.Web.ActivityPub.Transmogrifier.upgrade_user_from_ap_id(user.ap_id, false)

View file

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

View file

@ -9,11 +9,20 @@ def run([nickname]) do
with %User{local: true} = user <- User.get_by_nickname(nickname), with %User{local: true} = user <- User.get_by_nickname(nickname),
{:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do {:ok, token} <- Pleroma.PasswordResetToken.create_token(user) do
IO.puts "Generated password reset token for #{user.nickname}" IO.puts("Generated password reset token for #{user.nickname}")
IO.puts "Url: #{Pleroma.Web.Router.Helpers.util_url(Pleroma.Web.Endpoint, :show_password_reset, token.token)}"
IO.puts(
"Url: #{
Pleroma.Web.Router.Helpers.util_url(
Pleroma.Web.Endpoint,
:show_password_reset,
token.token
)
}"
)
else else
_ -> _ ->
IO.puts "No local user #{nickname}" IO.puts("No local user #{nickname}")
end end
end end
end end

View file

@ -7,21 +7,24 @@ defmodule Mix.Tasks.SetModerator do
def run([nickname | rest]) do def run([nickname | rest]) do
ensure_started(Repo, []) ensure_started(Repo, [])
moderator = case rest do moderator =
[moderator] -> moderator == "true" case rest do
_ -> true [moderator] -> moderator == "true"
end _ -> true
end
with %User{local: true} = user <- User.get_by_nickname(nickname) do with %User{local: true} = user <- User.get_by_nickname(nickname) do
info = user.info info =
|> Map.put("is_moderator", !!moderator) user.info
|> Map.put("is_moderator", !!moderator)
cng = User.info_changeset(user, %{info: info}) cng = User.info_changeset(user, %{info: info})
user = Repo.update!(cng) user = Repo.update!(cng)
IO.puts "Moderator status of #{nickname}: #{user.info["is_moderator"]}" IO.puts("Moderator status of #{nickname}: #{user.info["is_moderator"]}")
else else
_ -> _ ->
IO.puts "No local user #{nickname}" IO.puts("No local user #{nickname}")
end end
end end
end end

View file

@ -6,15 +6,15 @@ defmodule Pleroma.PasswordResetToken do
alias Pleroma.{User, PasswordResetToken, Repo} alias Pleroma.{User, PasswordResetToken, Repo}
schema "password_reset_tokens" do schema "password_reset_tokens" do
belongs_to :user, User belongs_to(:user, User)
field :token, :string field(:token, :string)
field :used, :boolean, default: false field(:used, :boolean, default: false)
timestamps() timestamps()
end end
def create_token(%User{} = user) do def create_token(%User{} = user) do
token = :crypto.strong_rand_bytes(32) |> Base.url_encode64 token = :crypto.strong_rand_bytes(32) |> Base.url_encode64()
token = %PasswordResetToken{ token = %PasswordResetToken{
user_id: user.id, user_id: user.id,

View file

@ -4,33 +4,53 @@ defmodule Pleroma.Activity do
import Ecto.Query import Ecto.Query
schema "activities" do schema "activities" do
field :data, :map field(:data, :map)
field :local, :boolean, default: true field(:local, :boolean, default: true)
field :actor, :string field(:actor, :string)
field :recipients, {:array, :string} field(:recipients, {:array, :string})
has_many :notifications, Notification, on_delete: :delete_all has_many(:notifications, Notification, on_delete: :delete_all)
timestamps() timestamps()
end end
def get_by_ap_id(ap_id) do def get_by_ap_id(ap_id) do
Repo.one(from activity in Activity, Repo.one(
where: fragment("(?)->>'id' = ?", activity.data, ^to_string(ap_id))) from(
activity in Activity,
where: fragment("(?)->>'id' = ?", activity.data, ^to_string(ap_id))
)
)
end end
# TODO: # TODO:
# Go through these and fix them everywhere. # Go through these and fix them everywhere.
# Wrong name, only returns create activities # Wrong name, only returns create activities
def all_by_object_ap_id_q(ap_id) do def all_by_object_ap_id_q(ap_id) do
from activity in Activity, from(
where: fragment("coalesce((?)->'object'->>'id', (?)->>'object') = ?", activity.data, activity.data, ^to_string(ap_id)), activity in Activity,
where:
fragment(
"coalesce((?)->'object'->>'id', (?)->>'object') = ?",
activity.data,
activity.data,
^to_string(ap_id)
),
where: fragment("(?)->>'type' = 'Create'", activity.data) where: fragment("(?)->>'type' = 'Create'", activity.data)
)
end end
# Wrong name, returns all. # Wrong name, returns all.
def all_non_create_by_object_ap_id_q(ap_id) do def all_non_create_by_object_ap_id_q(ap_id) do
from activity in Activity, from(
where: fragment("coalesce((?)->'object'->>'id', (?)->>'object') = ?", activity.data, activity.data, ^to_string(ap_id)) activity in Activity,
where:
fragment(
"coalesce((?)->'object'->>'id', (?)->>'object') = ?",
activity.data,
activity.data,
^to_string(ap_id)
)
)
end end
# Wrong name plz fix thx # Wrong name plz fix thx
@ -39,13 +59,21 @@ def all_by_object_ap_id(ap_id) do
end end
def create_activity_by_object_id_query(ap_ids) do def create_activity_by_object_id_query(ap_ids) do
from activity in Activity, from(
where: fragment("coalesce((?)->'object'->>'id', (?)->>'object') = ANY(?)", activity.data, activity.data, ^ap_ids), activity in Activity,
where:
fragment(
"coalesce((?)->'object'->>'id', (?)->>'object') = ANY(?)",
activity.data,
activity.data,
^ap_ids
),
where: fragment("(?)->>'type' = 'Create'", activity.data) where: fragment("(?)->>'type' = 'Create'", activity.data)
)
end end
def get_create_activity_by_object_ap_id(ap_id) do def get_create_activity_by_object_ap_id(ap_id) do
create_activity_by_object_id_query([ap_id]) create_activity_by_object_id_query([ap_id])
|> Repo.one |> Repo.one()
end end
end end

View file

@ -7,23 +7,34 @@ def start(_type, _args) do
import Supervisor.Spec import Supervisor.Spec
# Define workers and child supervisors to be supervised # Define workers and child supervisors to be supervised
children = [ children =
# Start the Ecto repository [
supervisor(Pleroma.Repo, []), # Start the Ecto repository
# Start the endpoint when the application starts supervisor(Pleroma.Repo, []),
supervisor(Pleroma.Web.Endpoint, []), # Start the endpoint when the application starts
# Start your own worker by calling: Pleroma.Worker.start_link(arg1, arg2, arg3) supervisor(Pleroma.Web.Endpoint, []),
# worker(Pleroma.Worker, [arg1, arg2, arg3]), # Start your own worker by calling: Pleroma.Worker.start_link(arg1, arg2, arg3)
worker(Cachex, [:user_cache, [ # worker(Pleroma.Worker, [arg1, arg2, arg3]),
default_ttl: 25000, worker(Cachex, [
ttl_interval: 1000, :user_cache,
limit: 2500 [
]]), default_ttl: 25000,
worker(Pleroma.Web.Federator, []), ttl_interval: 1000,
worker(Pleroma.Stats, []), limit: 2500
] ]
++ if Mix.env == :test, do: [], else: [worker(Pleroma.Web.Streamer, [])] ]),
++ if !chat_enabled(), do: [], else: [worker(Pleroma.Web.ChatChannel.ChatChannelState, [])] worker(Pleroma.Web.Federator, []),
worker(Pleroma.Stats, [])
] ++
if Mix.env() == :test,
do: [],
else:
[worker(Pleroma.Web.Streamer, [])] ++
if(
!chat_enabled(),
do: [],
else: [worker(Pleroma.Web.ChatChannel.ChatChannelState, [])]
)
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
# for other strategies and supported options # for other strategies and supported options

View file

@ -5,19 +5,26 @@ defmodule Pleroma.Formatter do
@tag_regex ~r/\#\w+/u @tag_regex ~r/\#\w+/u
def parse_tags(text, data \\ %{}) do def parse_tags(text, data \\ %{}) do
Regex.scan(@tag_regex, text) Regex.scan(@tag_regex, text)
|> Enum.map(fn (["#" <> tag = full_tag]) -> {full_tag, String.downcase(tag)} end) |> Enum.map(fn ["#" <> tag = full_tag] -> {full_tag, String.downcase(tag)} end)
|> (fn map -> if data["sensitive"] in [true, "True", "true", "1"], do: [{"#nsfw", "nsfw"}] ++ map, else: map end).() |> (fn map ->
if data["sensitive"] in [true, "True", "true", "1"],
do: [{"#nsfw", "nsfw"}] ++ map,
else: map
end).()
end end
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 = ~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 =
~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()
|> Enum.uniq |> Enum.uniq()
|> Enum.map(fn ("@" <> match = full_match) -> {full_match, User.get_cached_by_nickname(match)} end) |> Enum.map(fn "@" <> match = full_match ->
|> Enum.filter(fn ({_match, user}) -> user end) {full_match, User.get_cached_by_nickname(match)}
end)
|> Enum.filter(fn {_match, user} -> user end)
end end
@finmoji [ @finmoji [
@ -86,9 +93,9 @@ def parse_mentions(text) do
"woollysocks" "woollysocks"
] ]
@finmoji_with_filenames Enum.map(@finmoji, fn (finmoji) -> @finmoji_with_filenames Enum.map(@finmoji, fn finmoji ->
{finmoji, "/finmoji/128px/#{finmoji}-128.png"} {finmoji, "/finmoji/128px/#{finmoji}-128.png"}
end) end)
@emoji_from_file (with {:ok, default} <- File.read("config/emoji.txt") do @emoji_from_file (with {:ok, default} <- File.read("config/emoji.txt") do
custom = custom =
@ -97,31 +104,40 @@ def parse_mentions(text) do
else else
_e -> "" _e -> ""
end end
(default <> "\n" <> custom) (default <> "\n" <> custom)
|> String.trim() |> String.trim()
|> String.split(~r/\n+/) |> String.split(~r/\n+/)
|> Enum.map(fn(line) -> |> Enum.map(fn line ->
[name, file] = String.split(line, ~r/,\s*/) [name, file] = String.split(line, ~r/,\s*/)
{name, file} {name, file}
end) end)
else else
_ -> [] _ -> []
end) end)
@emoji @finmoji_with_filenames ++ @emoji_from_file @emoji @finmoji_with_filenames ++ @emoji_from_file
def emojify(text, emoji \\ @emoji) def emojify(text, emoji \\ @emoji)
def emojify(text, nil), do: text def emojify(text, nil), do: text
def emojify(text, emoji) do def emojify(text, emoji) do
Enum.reduce(emoji, text, fn ({emoji, file}, text) -> Enum.reduce(emoji, text, fn {emoji, file}, text ->
emoji = HtmlSanitizeEx.strip_tags(emoji) emoji = HtmlSanitizeEx.strip_tags(emoji)
file = HtmlSanitizeEx.strip_tags(file) file = HtmlSanitizeEx.strip_tags(file)
String.replace(text, ":#{emoji}:", "<img height='32px' width='32px' alt='#{emoji}' title='#{emoji}' src='#{MediaProxy.url(file)}' />")
String.replace(
text,
":#{emoji}:",
"<img height='32px' width='32px' alt='#{emoji}' title='#{emoji}' src='#{
MediaProxy.url(file)
}' />"
)
end) end)
end end
def get_emoji(text) do def get_emoji(text) do
Enum.filter(@emoji, fn ({emoji, _}) -> String.contains?(text, ":#{emoji}:") end) Enum.filter(@emoji, fn {emoji, _} -> String.contains?(text, ":#{emoji}:") end)
end end
def get_custom_emoji() do def get_custom_emoji() do
@ -141,59 +157,71 @@ def html_escape(text) do
@doc "changes http:... links to html links" @doc "changes http:... links to html links"
def add_links({subs, text}) do def add_links({subs, text}) do
links = Regex.scan(@link_regex, text) links =
|> Enum.map(fn ([url]) -> {Ecto.UUID.generate, url} end) Regex.scan(@link_regex, text)
|> Enum.map(fn [url] -> {Ecto.UUID.generate(), url} end)
uuid_text = links uuid_text =
|> Enum.reduce(text, fn({uuid, url}, acc) -> String.replace(acc, url, uuid) end) links
|> Enum.reduce(text, fn {uuid, url}, acc -> String.replace(acc, url, uuid) end)
subs = subs ++ Enum.map(links, fn({uuid, url}) -> subs =
{uuid, "<a href='#{url}'>#{url}</a>"} subs ++
end) Enum.map(links, fn {uuid, url} ->
{uuid, "<a href='#{url}'>#{url}</a>"}
end)
{subs, uuid_text} {subs, uuid_text}
end end
@doc "Adds the links to mentioned users" @doc "Adds the links to mentioned users"
def add_user_links({subs, text}, mentions) do def add_user_links({subs, text}, mentions) do
mentions = mentions mentions =
|> Enum.sort_by(fn ({name, _}) -> -String.length(name) end) mentions
|> Enum.map(fn({name, user}) -> {name, user, Ecto.UUID.generate} end) |> Enum.sort_by(fn {name, _} -> -String.length(name) end)
|> Enum.map(fn {name, user} -> {name, user, Ecto.UUID.generate()} end)
uuid_text = mentions uuid_text =
|> Enum.reduce(text, fn ({match, _user, uuid}, text) -> mentions
String.replace(text, match, uuid) |> Enum.reduce(text, fn {match, _user, uuid}, text ->
end) String.replace(text, match, uuid)
end)
subs = subs ++ Enum.map(mentions, fn ({match, %User{ap_id: ap_id}, uuid}) -> subs =
short_match = String.split(match, "@") |> tl() |> hd() subs ++
{uuid, "<span><a href='#{ap_id}'>@<span>#{short_match}</span></a></span>"} Enum.map(mentions, fn {match, %User{ap_id: ap_id}, uuid} ->
end) short_match = String.split(match, "@") |> tl() |> hd()
{uuid, "<span><a href='#{ap_id}'>@<span>#{short_match}</span></a></span>"}
end)
{subs, uuid_text} {subs, uuid_text}
end end
@doc "Adds the hashtag links" @doc "Adds the hashtag links"
def add_hashtag_links({subs, text}, tags) do def add_hashtag_links({subs, text}, tags) do
tags = tags tags =
|> Enum.sort_by(fn ({name, _}) -> -String.length(name) end) tags
|> Enum.map(fn({name, short}) -> {name, short, Ecto.UUID.generate} end) |> Enum.sort_by(fn {name, _} -> -String.length(name) end)
|> Enum.map(fn {name, short} -> {name, short, Ecto.UUID.generate()} end)
uuid_text = tags uuid_text =
|> Enum.reduce(text, fn ({match, _short, uuid}, text) -> tags
String.replace(text, match, uuid) |> Enum.reduce(text, fn {match, _short, uuid}, text ->
end) String.replace(text, match, uuid)
end)
subs = subs ++ Enum.map(tags, fn ({_, tag, uuid}) -> subs =
url = "<a href='#{Pleroma.Web.base_url}/tag/#{tag}' rel='tag'>##{tag}</a>" subs ++
{uuid, url} Enum.map(tags, fn {_, tag, uuid} ->
end) url = "<a href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>##{tag}</a>"
{uuid, url}
end)
{subs, uuid_text} {subs, uuid_text}
end end
def finalize({subs, text}) do def finalize({subs, text}) do
Enum.reduce(subs, text, fn({uuid, replacement}, result_text) -> Enum.reduce(subs, text, fn {uuid, replacement}, result_text ->
String.replace(result_text, uuid, replacement) String.replace(result_text, uuid, replacement)
end) end)
end end

View file

@ -1,14 +1,13 @@
defmodule Pleroma.HTTP do defmodule Pleroma.HTTP do
use HTTPoison.Base use HTTPoison.Base
def process_request_options(options) do def process_request_options(options) do
config = Application.get_env(:pleroma, :http, []) config = Application.get_env(:pleroma, :http, [])
proxy = Keyword.get(config, :proxy_url, nil) proxy = Keyword.get(config, :proxy_url, nil)
case proxy do case proxy do
nil -> options nil -> options
_ -> options ++ [proxy: proxy] _ -> options ++ [proxy: proxy]
end end
end end
end end

View file

@ -4,75 +4,89 @@ defmodule Pleroma.Notification do
import Ecto.Query import Ecto.Query
schema "notifications" do schema "notifications" do
field :seen, :boolean, default: false field(:seen, :boolean, default: false)
belongs_to :user, Pleroma.User belongs_to(:user, Pleroma.User)
belongs_to :activity, Pleroma.Activity belongs_to(:activity, Pleroma.Activity)
timestamps() timestamps()
end end
# TODO: Make generic and unify (see activity_pub.ex) # TODO: Make generic and unify (see activity_pub.ex)
defp restrict_max(query, %{"max_id" => max_id}) do defp restrict_max(query, %{"max_id" => max_id}) do
from activity in query, where: activity.id < ^max_id from(activity in query, where: activity.id < ^max_id)
end end
defp restrict_max(query, _), do: query defp restrict_max(query, _), do: query
defp restrict_since(query, %{"since_id" => since_id}) do defp restrict_since(query, %{"since_id" => since_id}) do
from activity in query, where: activity.id > ^since_id from(activity in query, where: activity.id > ^since_id)
end end
defp restrict_since(query, _), do: query defp restrict_since(query, _), do: query
def for_user(user, opts \\ %{}) do def for_user(user, opts \\ %{}) do
query = from n in Notification, query =
where: n.user_id == ^user.id, from(
order_by: [desc: n.id], n in Notification,
preload: [:activity], where: n.user_id == ^user.id,
limit: 20 order_by: [desc: n.id],
preload: [:activity],
limit: 20
)
query = query query =
|> restrict_since(opts) query
|> restrict_max(opts) |> restrict_since(opts)
|> restrict_max(opts)
Repo.all(query) Repo.all(query)
end end
def get(%{id: user_id} = _user, id) do def get(%{id: user_id} = _user, id) do
query = from n in Notification, query =
where: n.id == ^id, from(
preload: [:activity] n in Notification,
where: n.id == ^id,
preload: [:activity]
)
notification = Repo.one(query) notification = Repo.one(query)
case notification do case notification do
%{user_id: ^user_id} -> %{user_id: ^user_id} ->
{:ok, notification} {:ok, notification}
_ -> _ ->
{:error, "Cannot get notification"} {:error, "Cannot get notification"}
end end
end end
def clear(user) do def clear(user) do
query = from n in Notification, query = from(n in Notification, where: n.user_id == ^user.id)
where: n.user_id == ^user.id
Repo.delete_all(query) Repo.delete_all(query)
end end
def dismiss(%{id: user_id} = _user, id) do def dismiss(%{id: user_id} = _user, id) do
notification = Repo.get(Notification, id) notification = Repo.get(Notification, id)
case notification do case notification do
%{user_id: ^user_id} -> %{user_id: ^user_id} ->
Repo.delete(notification) Repo.delete(notification)
_ -> _ ->
{:error, "Cannot dismiss notification"} {:error, "Cannot dismiss notification"}
end end
end end
def create_notifications(%Activity{id: _, data: %{"to" => _, "type" => type}} = activity) when type in ["Create", "Like", "Announce", "Follow"] do def create_notifications(%Activity{id: _, data: %{"to" => _, "type" => type}} = activity)
when type in ["Create", "Like", "Announce", "Follow"] do
users = User.get_notified_from_activity(activity) users = User.get_notified_from_activity(activity)
notifications = Enum.map(users, fn (user) -> create_notification(activity, user) end) notifications = Enum.map(users, fn user -> create_notification(activity, user) end)
{:ok, notifications} {:ok, notifications}
end end
def create_notifications(_), do: {:ok, []} def create_notifications(_), do: {:ok, []}
# TODO move to sql, too. # TODO move to sql, too.
@ -85,4 +99,3 @@ def create_notification(%Activity{} = activity, %User{} = user) do
end end
end end
end end

View file

@ -4,14 +4,14 @@ defmodule Pleroma.Object do
import Ecto.{Query, Changeset} import Ecto.{Query, Changeset}
schema "objects" do schema "objects" do
field :data, :map field(:data, :map)
timestamps() timestamps()
end end
def create(data) do def create(data) do
Object.change(%Object{}, %{data: data}) Object.change(%Object{}, %{data: data})
|> Repo.insert |> Repo.insert()
end end
def change(struct, params \\ %{}) do def change(struct, params \\ %{}) do
@ -22,24 +22,30 @@ def change(struct, params \\ %{}) do
end end
def get_by_ap_id(nil), do: nil def get_by_ap_id(nil), do: nil
def get_by_ap_id(ap_id) do def get_by_ap_id(ap_id) do
Repo.one(from object in Object, Repo.one(from(object in Object, where: fragment("(?)->>'id' = ?", object.data, ^ap_id)))
where: fragment("(?)->>'id' = ?", object.data, ^ap_id))
end end
def get_cached_by_ap_id(ap_id) do def get_cached_by_ap_id(ap_id) do
if Mix.env == :test do if Mix.env() == :test do
get_by_ap_id(ap_id) get_by_ap_id(ap_id)
else else
key = "object:#{ap_id}" key = "object:#{ap_id}"
Cachex.get!(:user_cache, key, fallback: fn(_) ->
object = get_by_ap_id(ap_id) Cachex.get!(
if object do :user_cache,
{:commit, object} key,
else fallback: fn _ ->
{:ignore, object} object = get_by_ap_id(ap_id)
if object do
{:commit, object}
else
{:ignore, object}
end
end end
end) )
end end
end end

View file

@ -14,8 +14,7 @@ def call(conn, opts) do
{:ok, user} <- opts[:fetcher].(username), {:ok, user} <- opts[:fetcher].(username),
false <- !!user.info["deactivated"], false <- !!user.info["deactivated"],
saved_user_id <- get_session(conn, :user_id), saved_user_id <- get_session(conn, :user_id),
{:ok, verified_user} <- verify(user, password, saved_user_id) {:ok, verified_user} <- verify(user, password, saved_user_id) do
do
conn conn
|> assign(:user, verified_user) |> assign(:user, verified_user)
|> put_session(:user_id, verified_user.id) |> put_session(:user_id, verified_user.id)
@ -30,7 +29,7 @@ defp verify(%{id: id} = user, _password, id) do
end end
defp verify(nil, _password, _user_id) do defp verify(nil, _password, _user_id) do
Pbkdf2.dummy_checkpw Pbkdf2.dummy_checkpw()
:error :error
end end
@ -45,8 +44,7 @@ defp verify(user, password, _user_id) do
defp decode_header(conn) do defp decode_header(conn) do
with ["Basic " <> header] <- get_req_header(conn, "authorization"), with ["Basic " <> header] <- get_req_header(conn, "authorization"),
{:ok, userinfo} <- Base.decode64(header), {:ok, userinfo} <- Base.decode64(header),
[username, password] <- String.split(userinfo, ":", parts: 2) [username, password] <- String.split(userinfo, ":", parts: 2) do
do
{:ok, username, password} {:ok, username, password}
end end
end end

View file

@ -9,11 +9,14 @@ def init(options) do
end end
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
def call(conn, _) do def call(conn, _) do
token = case get_req_header(conn, "authorization") do token =
["Bearer " <> header] -> header case get_req_header(conn, "authorization") do
_ -> get_session(conn, :oauth_token) ["Bearer " <> header] -> header
end _ -> get_session(conn, :oauth_token)
end
with token when not is_nil(token) <- token, with token when not is_nil(token) <- token,
%Token{user_id: user_id} <- Repo.get_by(Token, token: token), %Token{user_id: user_id} <- Repo.get_by(Token, token: token),
%User{} = user <- Repo.get(User, user_id), %User{} = user <- Repo.get(User, user_id),

View file

@ -18,22 +18,31 @@ def get_peers do
def schedule_update do def schedule_update do
spawn(fn -> spawn(fn ->
Process.sleep(1000 * 60 * 60 * 1) # 1 hour # 1 hour
Process.sleep(1000 * 60 * 60 * 1)
schedule_update() schedule_update()
end) end)
update_stats() update_stats()
end end
def update_stats do def update_stats do
peers = from(u in Pleroma.User, peers =
select: fragment("distinct ?->'host'", u.info), from(
where: u.local != ^true) u in Pleroma.User,
|> Repo.all() select: fragment("distinct ?->'host'", u.info),
where: u.local != ^true
)
|> Repo.all()
domain_count = Enum.count(peers) domain_count = Enum.count(peers)
status_query = from(u in User.local_user_query,
select: fragment("sum((?->>'note_count')::int)", u.info)) status_query =
from(u in User.local_user_query(), select: fragment("sum((?->>'note_count')::int)", u.info))
status_count = Repo.one(status_query) status_count = Repo.one(status_query)
user_count = Repo.aggregate(User.local_user_query, :count, :id) user_count = Repo.aggregate(User.local_user_query(), :count, :id)
Agent.update(__MODULE__, fn _ -> Agent.update(__MODULE__, fn _ ->
{peers, %{domain_count: domain_count, status_count: status_count, user_count: user_count}} {peers, %{domain_count: domain_count, status_count: status_count, user_count: user_count}}
end) end)

View file

@ -1,27 +1,31 @@
defmodule Pleroma.Upload do defmodule Pleroma.Upload do
alias Ecto.UUID alias Ecto.UUID
alias Pleroma.Web alias Pleroma.Web
def store(%Plug.Upload{} = file) do def store(%Plug.Upload{} = file) do
uuid = UUID.generate uuid = UUID.generate()
upload_folder = Path.join(upload_path(), uuid) upload_folder = Path.join(upload_path(), uuid)
File.mkdir_p!(upload_folder) File.mkdir_p!(upload_folder)
result_file = Path.join(upload_folder, file.filename) result_file = Path.join(upload_folder, file.filename)
File.cp!(file.path, result_file) File.cp!(file.path, result_file)
# fix content type on some image uploads # fix content type on some image uploads
content_type = if file.content_type in [nil, "application/octet-stream"] do content_type =
get_content_type(file.path) if file.content_type in [nil, "application/octet-stream"] do
else get_content_type(file.path)
file.content_type else
end file.content_type
end
%{ %{
"type" => "Image", "type" => "Image",
"url" => [%{ "url" => [
"type" => "Link", %{
"mediaType" => content_type, "type" => "Link",
"href" => url_for(Path.join(uuid, :cow_uri.urlencode(file.filename))) "mediaType" => content_type,
}], "href" => url_for(Path.join(uuid, :cow_uri.urlencode(file.filename)))
}
],
"name" => file.filename, "name" => file.filename,
"uuid" => uuid "uuid" => uuid
} }
@ -30,7 +34,7 @@ def store(%Plug.Upload{} = file) do
def store(%{"img" => "data:image/" <> image_data}) do def store(%{"img" => "data:image/" <> image_data}) do
parsed = Regex.named_captures(~r/(?<filetype>jpeg|png|gif);base64,(?<data>.*)/, image_data) parsed = Regex.named_captures(~r/(?<filetype>jpeg|png|gif);base64,(?<data>.*)/, image_data)
data = Base.decode64!(parsed["data"]) data = Base.decode64!(parsed["data"])
uuid = UUID.generate uuid = UUID.generate()
upload_folder = Path.join(upload_path(), uuid) upload_folder = Path.join(upload_path(), uuid)
File.mkdir_p!(upload_folder) File.mkdir_p!(upload_folder)
filename = Base.encode16(:crypto.hash(:sha256, data)) <> ".#{parsed["filetype"]}" filename = Base.encode16(:crypto.hash(:sha256, data)) <> ".#{parsed["filetype"]}"
@ -42,11 +46,13 @@ def store(%{"img" => "data:image/" <> image_data}) do
%{ %{
"type" => "Image", "type" => "Image",
"url" => [%{ "url" => [
"type" => "Link", %{
"mediaType" => content_type, "type" => "Link",
"href" => url_for(Path.join(uuid, :cow_uri.urlencode(filename))) "mediaType" => content_type,
}], "href" => url_for(Path.join(uuid, :cow_uri.urlencode(filename)))
}
],
"name" => filename, "name" => filename,
"uuid" => uuid "uuid" => uuid
} }
@ -62,28 +68,37 @@ defp url_for(file) do
end end
def get_content_type(file) do def get_content_type(file) do
match = File.open(file, [:read], fn(f) -> match =
case IO.binread(f, 8) do File.open(file, [:read], fn f ->
<<0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a>> -> case IO.binread(f, 8) do
"image/png" <<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A>> ->
<<0x47, 0x49, 0x46, 0x38, _, 0x61, _, _>> -> "image/png"
"image/gif"
<<0xff, 0xd8, 0xff, _, _, _, _, _>> -> <<0x47, 0x49, 0x46, 0x38, _, 0x61, _, _>> ->
"image/jpeg" "image/gif"
<<0x1a, 0x45, 0xdf, 0xa3, _, _, _, _>> ->
"video/webm" <<0xFF, 0xD8, 0xFF, _, _, _, _, _>> ->
<<0x00, 0x00, 0x00, _, 0x66, 0x74, 0x79, 0x70>> -> "image/jpeg"
"video/mp4"
<<0x49, 0x44, 0x33, _, _, _, _, _>> -> <<0x1A, 0x45, 0xDF, 0xA3, _, _, _, _>> ->
"audio/mpeg" "video/webm"
<<0x4f, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00>> ->
"audio/ogg" <<0x00, 0x00, 0x00, _, 0x66, 0x74, 0x79, 0x70>> ->
<<0x52, 0x49, 0x46, 0x46, _, _, _, _>> -> "video/mp4"
"audio/wav"
_ -> <<0x49, 0x44, 0x33, _, _, _, _, _>> ->
"application/octet-stream" "audio/mpeg"
end
end) <<0x4F, 0x67, 0x67, 0x53, 0x00, 0x02, 0x00, 0x00>> ->
"audio/ogg"
<<0x52, 0x49, 0x46, 0x46, _, _, _, _>> ->
"audio/wav"
_ ->
"application/octet-stream"
end
end)
case match do case match do
{:ok, type} -> type {:ok, type} -> type

View file

@ -8,20 +8,20 @@ defmodule Pleroma.User do
alias Pleroma.Web.ActivityPub.{Utils, ActivityPub} alias Pleroma.Web.ActivityPub.{Utils, ActivityPub}
schema "users" do schema "users" do
field :bio, :string field(:bio, :string)
field :email, :string field(:email, :string)
field :name, :string field(:name, :string)
field :nickname, :string field(:nickname, :string)
field :password_hash, :string field(:password_hash, :string)
field :password, :string, virtual: true field(:password, :string, virtual: true)
field :password_confirmation, :string, virtual: true field(:password_confirmation, :string, virtual: true)
field :following, {:array, :string}, default: [] field(:following, {:array, :string}, default: [])
field :ap_id, :string field(:ap_id, :string)
field :avatar, :map field(:avatar, :map)
field :local, :boolean, default: true field(:local, :boolean, default: true)
field :info, :map, default: %{} field(:info, :map, default: %{})
field :follower_address, :string field(:follower_address, :string)
has_many :notifications, Notification has_many(:notifications, Notification)
timestamps() timestamps()
end end
@ -41,7 +41,7 @@ def banner_url(user) do
end end
def ap_id(%User{nickname: nickname}) do def ap_id(%User{nickname: nickname}) do
"#{Web.base_url}/users/#{nickname}" "#{Web.base_url()}/users/#{nickname}"
end end
def ap_followers(%User{} = user) do def ap_followers(%User{} = user) do
@ -62,6 +62,7 @@ def info_changeset(struct, params \\ %{}) do
def user_info(%User{} = user) do def user_info(%User{} = user) do
oneself = if user.local, do: 1, else: 0 oneself = if user.local, do: 1, else: 0
%{ %{
following_count: length(user.following) - oneself, following_count: length(user.following) - oneself,
note_count: user.info["note_count"] || 0, note_count: user.info["note_count"] || 0,
@ -71,21 +72,25 @@ def user_info(%User{} = user) do
@email_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])?)*$/ @email_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])?)*$/
def remote_user_creation(params) do def remote_user_creation(params) do
changes = %User{} changes =
|> cast(params, [:bio, :name, :ap_id, :nickname, :info, :avatar]) %User{}
|> validate_required([:name, :ap_id, :nickname]) |> cast(params, [:bio, :name, :ap_id, :nickname, :info, :avatar])
|> unique_constraint(:nickname) |> validate_required([:name, :ap_id, :nickname])
|> validate_format(:nickname, @email_regex) |> unique_constraint(:nickname)
|> validate_length(:bio, max: 5000) |> validate_format(:nickname, @email_regex)
|> validate_length(:name, max: 100) |> validate_length(:bio, max: 5000)
|> put_change(:local, false) |> validate_length(:name, max: 100)
|> put_change(:local, false)
if changes.valid? do if changes.valid? do
case changes.changes[:info]["source_data"] do case changes.changes[:info]["source_data"] do
%{"followers" => followers} -> %{"followers" => followers} ->
changes changes
|> put_change(:follower_address, followers) |> put_change(:follower_address, followers)
_ -> _ ->
followers = User.ap_followers(%User{nickname: changes.changes[:nickname]}) followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
changes changes
|> put_change(:follower_address, followers) |> put_change(:follower_address, followers)
end end
@ -113,13 +118,15 @@ def upgrade_changeset(struct, params \\ %{}) do
end end
def password_update_changeset(struct, params) do def password_update_changeset(struct, params) do
changeset = struct changeset =
|> cast(params, [:password, :password_confirmation]) struct
|> validate_required([:password, :password_confirmation]) |> cast(params, [:password, :password_confirmation])
|> validate_confirmation(:password) |> validate_required([:password, :password_confirmation])
|> validate_confirmation(:password)
if changeset.valid? do if changeset.valid? do
hashed = Pbkdf2.hashpwsalt(changeset.changes[:password]) hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
changeset changeset
|> put_change(:password_hash, hashed) |> put_change(:password_hash, hashed)
else else
@ -132,21 +139,23 @@ def reset_password(user, data) do
end end
def register_changeset(struct, params \\ %{}) do def register_changeset(struct, params \\ %{}) do
changeset = struct changeset =
|> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation]) struct
|> validate_required([:email, :name, :nickname, :password, :password_confirmation]) |> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
|> validate_confirmation(:password) |> validate_required([:email, :name, :nickname, :password, :password_confirmation])
|> unique_constraint(:email) |> validate_confirmation(:password)
|> unique_constraint(:nickname) |> unique_constraint(:email)
|> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/) |> unique_constraint(:nickname)
|> validate_format(:email, @email_regex) |> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/)
|> validate_length(:bio, max: 1000) |> validate_format(:email, @email_regex)
|> validate_length(:name, min: 1, max: 100) |> validate_length(:bio, max: 1000)
|> validate_length(:name, min: 1, max: 100)
if changeset.valid? do if changeset.valid? do
hashed = Pbkdf2.hashpwsalt(changeset.changes[:password]) hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]}) ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]}) followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
changeset changeset
|> put_change(:password_hash, hashed) |> put_change(:password_hash, hashed)
|> put_change(:ap_id, ap_id) |> put_change(:ap_id, ap_id)
@ -161,19 +170,20 @@ def follow(%User{} = follower, %User{info: info} = followed) do
ap_followers = followed.follower_address ap_followers = followed.follower_address
if following?(follower, followed) or info["deactivated"] do if following?(follower, followed) or info["deactivated"] do
{:error, {:error, "Could not follow user: #{followed.nickname} is already on your list."}
"Could not follow user: #{followed.nickname} is already on your list."}
else else
if !followed.local && follower.local && !ap_enabled?(followed) do if !followed.local && follower.local && !ap_enabled?(followed) do
Websub.subscribe(follower, followed) Websub.subscribe(follower, followed)
end end
following = [ap_followers | follower.following] following =
|> Enum.uniq [ap_followers | follower.following]
|> Enum.uniq()
follower = follower follower =
|> follow_changeset(%{following: following}) follower
|> update_and_set_cache |> follow_changeset(%{following: following})
|> update_and_set_cache
{:ok, _} = update_follower_count(followed) {:ok, _} = update_follower_count(followed)
@ -183,13 +193,16 @@ def follow(%User{} = follower, %User{info: info} = followed) do
def unfollow(%User{} = follower, %User{} = followed) do def unfollow(%User{} = follower, %User{} = followed) do
ap_followers = followed.follower_address ap_followers = followed.follower_address
if following?(follower, followed) and follower.ap_id != followed.ap_id do
following = follower.following
|> List.delete(ap_followers)
{ :ok, follower } = follower if following?(follower, followed) and follower.ap_id != followed.ap_id do
|> follow_changeset(%{following: following}) following =
|> update_and_set_cache follower.following
|> List.delete(ap_followers)
{:ok, follower} =
follower
|> follow_changeset(%{following: following})
|> update_and_set_cache
{:ok, followed} = update_follower_count(followed) {:ok, followed} = update_follower_count(followed)
@ -225,12 +238,12 @@ def invalidate_cache(user) do
def get_cached_by_ap_id(ap_id) do def get_cached_by_ap_id(ap_id) do
key = "ap_id:#{ap_id}" key = "ap_id:#{ap_id}"
Cachex.get!(:user_cache, key, fallback: fn(_) -> get_by_ap_id(ap_id) end) Cachex.get!(:user_cache, key, fallback: fn _ -> get_by_ap_id(ap_id) end)
end end
def get_cached_by_nickname(nickname) do def get_cached_by_nickname(nickname) do
key = "nickname:#{nickname}" key = "nickname:#{nickname}"
Cachex.get!(:user_cache, key, fallback: fn(_) -> get_or_fetch_by_nickname(nickname) end) Cachex.get!(:user_cache, key, fallback: fn _ -> get_or_fetch_by_nickname(nickname) end)
end end
def get_by_nickname(nickname) do def get_by_nickname(nickname) do
@ -239,7 +252,7 @@ def get_by_nickname(nickname) do
def get_cached_user_info(user) do def get_cached_user_info(user) do
key = "user_info:#{user.id}" key = "user_info:#{user.id}"
Cachex.get!(:user_cache, key, fallback: fn(_) -> user_info(user) end) Cachex.get!(:user_cache, key, fallback: fn _ -> user_info(user) end)
end end
def fetch_by_nickname(nickname) do def fetch_by_nickname(nickname) do
@ -252,29 +265,37 @@ def fetch_by_nickname(nickname) do
end end
def get_or_fetch_by_nickname(nickname) do def get_or_fetch_by_nickname(nickname) do
with %User{} = user <- get_by_nickname(nickname) do with %User{} = user <- get_by_nickname(nickname) do
user user
else _e -> else
with [_nick, _domain] <- String.split(nickname, "@"), _e ->
{:ok, user} <- fetch_by_nickname(nickname) do with [_nick, _domain] <- String.split(nickname, "@"),
user {:ok, user} <- fetch_by_nickname(nickname) do
else _e -> nil user
end else
_e -> nil
end
end end
end end
def get_followers(%User{id: id, follower_address: follower_address}) do def get_followers(%User{id: id, follower_address: follower_address}) do
q = from u in User, q =
where: fragment("? <@ ?", ^[follower_address], u.following), from(
where: u.id != ^id u in User,
where: fragment("? <@ ?", ^[follower_address], u.following),
where: u.id != ^id
)
{:ok, Repo.all(q)} {:ok, Repo.all(q)}
end end
def get_friends(%User{id: id, following: following}) do def get_friends(%User{id: id, following: following}) do
q = from u in User, q =
where: u.follower_address in ^following, from(
where: u.id != ^id u in User,
where: u.follower_address in ^following,
where: u.id != ^id
)
{:ok, Repo.all(q)} {:ok, Repo.all(q)}
end end
@ -289,9 +310,12 @@ def increase_note_count(%User{} = user) do
end end
def update_note_count(%User{} = user) do def update_note_count(%User{} = user) do
note_count_query = from a in Object, note_count_query =
where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data), from(
select: count(a.id) a in Object,
where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
select: count(a.id)
)
note_count = Repo.one(note_count_query) note_count = Repo.one(note_count_query)
@ -303,10 +327,13 @@ def update_note_count(%User{} = user) do
end end
def update_follower_count(%User{} = user) do def update_follower_count(%User{} = user) do
follower_count_query = from u in User, follower_count_query =
where: ^user.follower_address in u.following, from(
where: u.id != ^user.id, u in User,
select: count(u.id) where: ^user.follower_address in u.following,
where: u.id != ^user.id,
select: count(u.id)
)
follower_count = Repo.one(follower_count_query) follower_count = Repo.one(follower_count_query)
@ -318,20 +345,25 @@ def update_follower_count(%User{} = user) do
end end
def get_notified_from_activity(%Activity{recipients: to}) do def get_notified_from_activity(%Activity{recipients: to}) do
query = from u in User, query =
where: u.ap_id in ^to, from(
where: u.local == true u in User,
where: u.ap_id in ^to,
where: u.local == true
)
Repo.all(query) Repo.all(query)
end end
def get_recipients_from_activity(%Activity{recipients: to}) do def get_recipients_from_activity(%Activity{recipients: to}) do
query = from u in User, query =
where: u.ap_id in ^to, from(
or_where: fragment("? && ?", u.following, ^to) u in User,
where: u.ap_id in ^to,
or_where: fragment("? && ?", u.following, ^to)
)
query = from u in query, query = from(u in query, where: u.local == true)
where: u.local == true
Repo.all(query) Repo.all(query)
end end
@ -340,9 +372,20 @@ def search(query, resolve) do
if resolve do if resolve do
User.get_or_fetch_by_nickname(query) User.get_or_fetch_by_nickname(query)
end end
q = from u in User,
where: fragment("(to_tsvector('english', ?) || to_tsvector('english', ?)) @@ plainto_tsquery('english', ?)", u.nickname, u.name, ^query), q =
limit: 20 from(
u in User,
where:
fragment(
"(to_tsvector('english', ?) || to_tsvector('english', ?)) @@ plainto_tsquery('english', ?)",
u.nickname,
u.name,
^query
),
limit: 20
)
Repo.all(q) Repo.all(q)
end end
@ -370,36 +413,40 @@ def blocks?(user, %{ap_id: ap_id}) do
end end
def local_user_query() do def local_user_query() do
from u in User, from(u in User, where: u.local == true)
where: u.local == true
end end
def deactivate (%User{} = user) do def deactivate(%User{} = user) do
new_info = Map.put(user.info, "deactivated", true) new_info = Map.put(user.info, "deactivated", true)
cs = User.info_changeset(user, %{info: new_info}) cs = User.info_changeset(user, %{info: new_info})
update_and_set_cache(cs) update_and_set_cache(cs)
end end
def delete (%User{} = user) do def delete(%User{} = user) do
{:ok, user} = User.deactivate(user) {:ok, user} = User.deactivate(user)
# Remove all relationships # Remove all relationships
{:ok, followers } = User.get_followers(user) {:ok, followers} = User.get_followers(user)
followers followers
|> Enum.each(fn (follower) -> User.unfollow(follower, user) end) |> Enum.each(fn follower -> User.unfollow(follower, user) end)
{:ok, friends} = User.get_friends(user) {:ok, friends} = User.get_friends(user)
friends
|> Enum.each(fn (followed) -> User.unfollow(user, followed) end)
query = from a in Activity, friends
where: a.actor == ^user.ap_id |> Enum.each(fn followed -> User.unfollow(user, followed) end)
query = from(a in Activity, where: a.actor == ^user.ap_id)
Repo.all(query) Repo.all(query)
|> Enum.each(fn (activity) -> |> Enum.each(fn activity ->
case activity.data["type"] do case activity.data["type"] do
"Create" -> ActivityPub.delete(Object.get_by_ap_id(activity.data["object"]["id"])) "Create" ->
_ -> "Doing nothing" # TODO: Do something with likes, follows, repeats. ActivityPub.delete(Object.get_by_ap_id(activity.data["object"]["id"]))
# TODO: Do something with likes, follows, repeats.
_ ->
"Doing nothing"
end end
end) end)
@ -413,7 +460,9 @@ def get_or_fetch_by_ap_id(ap_id) do
ap_try = ActivityPub.make_user_from_ap_id(ap_id) ap_try = ActivityPub.make_user_from_ap_id(ap_id)
case ap_try do case ap_try do
{:ok, user} -> user {:ok, user} ->
user
_ -> _ ->
case OStatus.make_user(ap_id) do case OStatus.make_user(ap_id) do
{:ok, user} -> user {:ok, user} -> user
@ -424,12 +473,15 @@ def get_or_fetch_by_ap_id(ap_id) do
end end
# AP style # AP style
def public_key_from_info(%{"source_data" => %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}) do def public_key_from_info(%{
key = :public_key.pem_decode(public_key_pem) "source_data" => %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
|> hd() }) do
|> :public_key.pem_entry_decode() key =
:public_key.pem_decode(public_key_pem)
|> hd()
|> :public_key.pem_entry_decode()
{:ok, key} {:ok, key}
end end
# OStatus Magic Key # OStatus Magic Key
@ -450,8 +502,10 @@ defp blank?(""), do: nil
defp blank?(n), do: n defp blank?(n), do: n
def insert_or_update_user(data) do def insert_or_update_user(data) do
data = data data =
|> Map.put(:name, blank?(data[:name]) || data[:nickname]) data
|> Map.put(:name, blank?(data[:name]) || data[:nickname])
cs = User.remote_user_creation(data) cs = User.remote_user_creation(data)
Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname) Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
end end

View file

@ -18,7 +18,14 @@ def insert(map, local \\ true) when is_map(map) do
with nil <- Activity.get_by_ap_id(map["id"]), with nil <- Activity.get_by_ap_id(map["id"]),
map <- lazy_put_activity_defaults(map), map <- lazy_put_activity_defaults(map),
:ok <- insert_full_object(map) do :ok <- insert_full_object(map) do
{:ok, activity} = Repo.insert(%Activity{data: map, local: local, actor: map["actor"], recipients: get_recipients(map)}) {:ok, activity} =
Repo.insert(%Activity{
data: map,
local: local,
actor: map["actor"],
recipients: get_recipients(map)
})
Notification.create_notifications(activity) Notification.create_notifications(activity)
stream_out(activity) stream_out(activity)
{:ok, activity} {:ok, activity}
@ -31,8 +38,10 @@ def insert(map, local \\ true) when is_map(map) do
def stream_out(activity) do def stream_out(activity) do
if activity.data["type"] in ["Create", "Announce"] do if activity.data["type"] in ["Create", "Announce"] do
Pleroma.Web.Streamer.stream("user", activity) Pleroma.Web.Streamer.stream("user", activity)
if Enum.member?(activity.data["to"], "https://www.w3.org/ns/activitystreams#Public") do if Enum.member?(activity.data["to"], "https://www.w3.org/ns/activitystreams#Public") do
Pleroma.Web.Streamer.stream("public", activity) Pleroma.Web.Streamer.stream("public", activity)
if activity.local do if activity.local do
Pleroma.Web.Streamer.stream("public:local", activity) Pleroma.Web.Streamer.stream("public:local", activity)
end end
@ -42,10 +51,15 @@ def stream_out(activity) do
def create(%{to: to, actor: actor, context: context, object: object} = params) do def create(%{to: to, actor: actor, context: context, object: object} = params) do
additional = params[:additional] || %{} additional = params[:additional] || %{}
local = !(params[:local] == false) # only accept false as false value # only accept false as false value
local = !(params[:local] == false)
published = params[:published] published = params[:published]
with create_data <- make_create_data(%{to: to, actor: actor, published: published, context: context, object: object}, additional), with create_data <-
make_create_data(
%{to: to, actor: actor, published: published, context: context, object: object},
additional
),
{:ok, activity} <- insert(create_data, local), {:ok, activity} <- insert(create_data, local),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity} {:ok, activity}
@ -53,7 +67,8 @@ def create(%{to: to, actor: actor, context: context, object: object} = params) d
end end
def accept(%{to: to, actor: actor, object: object} = params) do def accept(%{to: to, actor: actor, object: object} = params) do
local = !(params[:local] == false) # only accept false as false value # only accept false as false value
local = !(params[:local] == false)
with data <- %{"to" => to, "type" => "Accept", "actor" => actor, "object" => object}, with data <- %{"to" => to, "type" => "Accept", "actor" => actor, "object" => object},
{:ok, activity} <- insert(data, local), {:ok, activity} <- insert(data, local),
@ -63,9 +78,16 @@ def accept(%{to: to, actor: actor, object: object} = params) do
end end
def update(%{to: to, cc: cc, actor: actor, object: object} = params) do def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
local = !(params[:local] == false) # only accept false as false value # only accept false as false value
local = !(params[:local] == false)
with data <- %{"to" => to, "cc" => cc, "type" => "Update", "actor" => actor, "object" => object}, with data <- %{
"to" => to,
"cc" => cc,
"type" => "Update",
"actor" => actor,
"object" => object
},
{:ok, activity} <- insert(data, local), {:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity} {:ok, activity}
@ -73,7 +95,12 @@ def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
end end
# TODO: This is weird, maybe we shouldn't check here if we can make the activity. # TODO: This is weird, maybe we shouldn't check here if we can make the activity.
def like(%User{ap_id: ap_id} = user, %Object{data: %{"id" => _}} = object, activity_id \\ nil, local \\ true) do def like(
%User{ap_id: ap_id} = user,
%Object{data: %{"id" => _}} = object,
activity_id \\ nil,
local \\ true
) do
with nil <- get_existing_like(ap_id, object), with nil <- get_existing_like(ap_id, object),
like_data <- make_like_data(user, object, activity_id), like_data <- make_like_data(user, object, activity_id),
{:ok, activity} <- insert(like_data, local), {:ok, activity} <- insert(like_data, local),
@ -91,11 +118,17 @@ def unlike(%User{} = actor, %Object{} = object) do
{:ok, _activity} <- Repo.delete(activity), {:ok, _activity} <- Repo.delete(activity),
{:ok, object} <- remove_like_from_object(activity, object) do {:ok, object} <- remove_like_from_object(activity, object) do
{:ok, object} {:ok, object}
else _e -> {:ok, object} else
_e -> {:ok, object}
end end
end end
def announce(%User{ap_id: _} = user, %Object{data: %{"id" => _}} = object, activity_id \\ nil, local \\ true) do def announce(
%User{ap_id: _} = user,
%Object{data: %{"id" => _}} = object,
activity_id \\ nil,
local \\ true
) do
with true <- is_public?(object), with true <- is_public?(object),
announce_data <- make_announce_data(user, object, activity_id), announce_data <- make_announce_data(user, object, activity_id),
{:ok, activity} <- insert(announce_data, local), {:ok, activity} <- insert(announce_data, local),
@ -119,19 +152,22 @@ def unfollow(follower, followed, local \\ true) do
with %Activity{} = follow_activity <- fetch_latest_follow(follower, followed), with %Activity{} = follow_activity <- fetch_latest_follow(follower, followed),
unfollow_data <- make_unfollow_data(follower, followed, follow_activity), unfollow_data <- make_unfollow_data(follower, followed, follow_activity),
{:ok, activity} <- insert(unfollow_data, local), {:ok, activity} <- insert(unfollow_data, local),
:ok, maybe_federate(activity) do :ok,
maybe_federate(activity) do
{:ok, activity} {:ok, activity}
end end
end end
def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ true) do def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ true) do
user = User.get_cached_by_ap_id(actor) user = User.get_cached_by_ap_id(actor)
data = %{ data = %{
"type" => "Delete", "type" => "Delete",
"actor" => actor, "actor" => actor,
"object" => id, "object" => id,
"to" => [user.follower_address, "https://www.w3.org/ns/activitystreams#Public"] "to" => [user.follower_address, "https://www.w3.org/ns/activitystreams#Public"]
} }
with Repo.delete(object), with Repo.delete(object),
Repo.delete_all(Activity.all_non_create_by_object_ap_id_q(id)), Repo.delete_all(Activity.all_non_create_by_object_ap_id_q(id)),
{:ok, activity} <- insert(data, local), {:ok, activity} <- insert(data, local),
@ -142,112 +178,147 @@ def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ tru
def fetch_activities_for_context(context, opts \\ %{}) do def fetch_activities_for_context(context, opts \\ %{}) do
public = ["https://www.w3.org/ns/activitystreams#Public"] public = ["https://www.w3.org/ns/activitystreams#Public"]
recipients = if opts["user"], do: [opts["user"].ap_id | opts["user"].following] ++ public, else: public
query = from activity in Activity recipients =
query = query if opts["user"], do: [opts["user"].ap_id | opts["user"].following] ++ public, else: public
query = from(activity in Activity)
query =
query
|> restrict_blocked(opts) |> restrict_blocked(opts)
|> restrict_recipients(recipients, opts["user"]) |> restrict_recipients(recipients, opts["user"])
query = from activity in query, query =
where: fragment("?->>'type' = ? and ?->>'context' = ?", activity.data, "Create", activity.data, ^context), from(
order_by: [desc: :id] activity in query,
where:
fragment(
"?->>'type' = ? and ?->>'context' = ?",
activity.data,
"Create",
activity.data,
^context
),
order_by: [desc: :id]
)
Repo.all(query) Repo.all(query)
end end
# TODO: Make this work properly with unlisted. # TODO: Make this work properly with unlisted.
def fetch_public_activities(opts \\ %{}) do def fetch_public_activities(opts \\ %{}) do
q = fetch_activities_query(["https://www.w3.org/ns/activitystreams#Public"], opts) q = fetch_activities_query(["https://www.w3.org/ns/activitystreams#Public"], opts)
q q
|> Repo.all |> Repo.all()
|> Enum.reverse |> Enum.reverse()
end end
defp restrict_since(query, %{"since_id" => since_id}) do defp restrict_since(query, %{"since_id" => since_id}) do
from activity in query, where: activity.id > ^since_id from(activity in query, where: activity.id > ^since_id)
end end
defp restrict_since(query, _), do: query defp restrict_since(query, _), do: query
defp restrict_tag(query, %{"tag" => tag}) do defp restrict_tag(query, %{"tag" => tag}) do
from activity in query, from(
activity in query,
where: fragment("? <@ (? #> '{\"object\",\"tag\"}')", ^tag, activity.data) where: fragment("? <@ (? #> '{\"object\",\"tag\"}')", ^tag, activity.data)
)
end end
defp restrict_tag(query, _), do: query defp restrict_tag(query, _), do: query
defp restrict_recipients(query, [], user), do: query defp restrict_recipients(query, [], user), do: query
defp restrict_recipients(query, recipients, nil) do defp restrict_recipients(query, recipients, nil) do
from activity in query, from(activity in query, where: fragment("? && ?", ^recipients, activity.recipients))
where: fragment("? && ?", ^recipients, activity.recipients)
end end
defp restrict_recipients(query, recipients, user) do defp restrict_recipients(query, recipients, user) do
from activity in query, from(
activity in query,
where: fragment("? && ?", ^recipients, activity.recipients), where: fragment("? && ?", ^recipients, activity.recipients),
or_where: activity.actor == ^user.ap_id or_where: activity.actor == ^user.ap_id
)
end end
defp restrict_limit(query, %{"limit" => limit}) do defp restrict_limit(query, %{"limit" => limit}) do
from activity in query, from(activity in query, limit: ^limit)
limit: ^limit
end end
defp restrict_limit(query, _), do: query defp restrict_limit(query, _), do: query
defp restrict_local(query, %{"local_only" => true}) do defp restrict_local(query, %{"local_only" => true}) do
from activity in query, where: activity.local == true from(activity in query, where: activity.local == true)
end end
defp restrict_local(query, _), do: query defp restrict_local(query, _), do: query
defp restrict_max(query, %{"max_id" => max_id}) do defp restrict_max(query, %{"max_id" => max_id}) do
from activity in query, where: activity.id < ^max_id from(activity in query, where: activity.id < ^max_id)
end end
defp restrict_max(query, _), do: query defp restrict_max(query, _), do: query
defp restrict_actor(query, %{"actor_id" => actor_id}) do defp restrict_actor(query, %{"actor_id" => actor_id}) do
from activity in query, from(activity in query, where: activity.actor == ^actor_id)
where: activity.actor == ^actor_id
end end
defp restrict_actor(query, _), do: query defp restrict_actor(query, _), do: query
defp restrict_type(query, %{"type" => type}) when is_binary(type) do defp restrict_type(query, %{"type" => type}) when is_binary(type) do
restrict_type(query, %{"type" => [type]}) restrict_type(query, %{"type" => [type]})
end end
defp restrict_type(query, %{"type" => type}) do defp restrict_type(query, %{"type" => type}) do
from activity in query, from(activity in query, where: fragment("?->>'type' = ANY(?)", activity.data, ^type))
where: fragment("?->>'type' = ANY(?)", activity.data, ^type)
end end
defp restrict_type(query, _), do: query defp restrict_type(query, _), do: query
defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do
from activity in query, from(
activity in query,
where: fragment("? <@ (? #> '{\"object\",\"likes\"}')", ^ap_id, activity.data) where: fragment("? <@ (? #> '{\"object\",\"likes\"}')", ^ap_id, activity.data)
)
end end
defp restrict_favorited_by(query, _), do: query defp restrict_favorited_by(query, _), do: query
defp restrict_media(query, %{"only_media" => val}) when val == "true" or val == "1" do defp restrict_media(query, %{"only_media" => val}) when val == "true" or val == "1" do
from activity in query, from(
activity in query,
where: fragment("not (? #> '{\"object\",\"attachment\"}' = ?)", activity.data, ^[]) where: fragment("not (? #> '{\"object\",\"attachment\"}' = ?)", activity.data, ^[])
)
end end
defp restrict_media(query, _), do: query defp restrict_media(query, _), do: query
# Only search through last 100_000 activities by default # Only search through last 100_000 activities by default
defp restrict_recent(query, %{"whole_db" => true}), do: query defp restrict_recent(query, %{"whole_db" => true}), do: query
defp restrict_recent(query, _) do defp restrict_recent(query, _) do
since = (Repo.aggregate(Activity, :max, :id) || 0) - 100_000 since = (Repo.aggregate(Activity, :max, :id) || 0) - 100_000
from activity in query, from(activity in query, where: activity.id > ^since)
where: activity.id > ^since
end end
defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do
blocks = info["blocks"] || [] blocks = info["blocks"] || []
from activity in query, from(activity in query, where: fragment("not (? = ANY(?))", activity.actor, ^blocks))
where: fragment("not (? = ANY(?))", activity.actor, ^blocks)
end end
defp restrict_blocked(query, _), do: query defp restrict_blocked(query, _), do: query
def fetch_activities_query(recipients, opts \\ %{}) do def fetch_activities_query(recipients, opts \\ %{}) do
base_query = from activity in Activity, base_query =
limit: 20, from(
order_by: [fragment("? desc nulls last", activity.id)] activity in Activity,
limit: 20,
order_by: [fragment("? desc nulls last", activity.id)]
)
base_query base_query
|> restrict_recipients(recipients, opts["user"]) |> restrict_recipients(recipients, opts["user"])
@ -266,8 +337,8 @@ def fetch_activities_query(recipients, opts \\ %{}) do
def fetch_activities(recipients, opts \\ %{}) do def fetch_activities(recipients, opts \\ %{}) do
fetch_activities_query(recipients, opts) fetch_activities_query(recipients, opts)
|> Repo.all |> Repo.all()
|> Enum.reverse |> Enum.reverse()
end end
def upload(file) do def upload(file) do
@ -276,15 +347,19 @@ def upload(file) do
end end
def user_data_from_user_object(data) do def user_data_from_user_object(data) do
avatar = data["icon"]["url"] && %{ avatar =
"type" => "Image", data["icon"]["url"] &&
"url" => [%{"href" => data["icon"]["url"]}] %{
} "type" => "Image",
"url" => [%{"href" => data["icon"]["url"]}]
}
banner = data["image"]["url"] && %{ banner =
"type" => "Image", data["image"]["url"] &&
"url" => [%{"href" => data["image"]["url"]}] %{
} "type" => "Image",
"url" => [%{"href" => data["image"]["url"]}]
}
user_data = %{ user_data = %{
ap_id: data["id"], ap_id: data["id"],
@ -304,8 +379,9 @@ def user_data_from_user_object(data) do
end end
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}} <- @httpoison.get(ap_id, ["Accept": "application/activity+json"]), with {:ok, %{status_code: 200, body: body}} <-
{:ok, data} <- Jason.decode(body) do @httpoison.get(ap_id, Accept: "application/activity+json"),
{:ok, data} <- Jason.decode(body) do
user_data_from_user_object(data) user_data_from_user_object(data)
else else
e -> Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}") e -> Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
@ -333,32 +409,48 @@ def make_user_from_nickname(nickname) do
end end
def publish(actor, activity) do def publish(actor, activity) do
followers = if actor.follower_address in activity.recipients do followers =
{:ok, followers} = User.get_followers(actor) if actor.follower_address in activity.recipients do
followers |> Enum.filter(&(!&1.local)) {:ok, followers} = User.get_followers(actor)
else followers |> Enum.filter(&(!&1.local))
[] else
end []
end
remote_inboxes = (Pleroma.Web.Salmon.remote_users(activity) ++ followers) remote_inboxes =
|> Enum.filter(fn (user) -> User.ap_enabled?(user) end) (Pleroma.Web.Salmon.remote_users(activity) ++ followers)
|> Enum.map(fn (%{info: %{"source_data" => data}}) -> |> Enum.filter(fn user -> User.ap_enabled?(user) end)
(data["endpoints"] && data["endpoints"]["sharedInbox"]) || data["inbox"] |> Enum.map(fn %{info: %{"source_data" => data}} ->
end) (data["endpoints"] && data["endpoints"]["sharedInbox"]) || data["inbox"]
|> Enum.uniq end)
|> Enum.uniq()
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data) {:ok, data} = Transmogrifier.prepare_outgoing(activity.data)
json = Jason.encode!(data) json = Jason.encode!(data)
Enum.each remote_inboxes, fn(inbox) ->
Federator.enqueue(:publish_single_ap, %{inbox: inbox, json: json, actor: actor, id: activity.data["id"]}) Enum.each(remote_inboxes, fn inbox ->
end Federator.enqueue(:publish_single_ap, %{
inbox: inbox,
json: json,
actor: actor,
id: activity.data["id"]
})
end)
end end
def publish_one(%{inbox: inbox, json: json, actor: actor, id: id}) do def publish_one(%{inbox: inbox, json: json, actor: actor, id: id}) do
Logger.info("Federating #{id} to #{inbox}") Logger.info("Federating #{id} to #{inbox}")
host = URI.parse(inbox).host host = URI.parse(inbox).host
signature = Pleroma.Web.HTTPSignatures.sign(actor, %{host: host, "content-length": byte_size(json)})
@httpoison.post(inbox, json, [{"Content-Type", "application/activity+json"}, {"signature", signature}], hackney: [pool: :default]) signature =
Pleroma.Web.HTTPSignatures.sign(actor, %{host: host, "content-length": byte_size(json)})
@httpoison.post(
inbox,
json,
[{"Content-Type", "application/activity+json"}, {"signature", signature}],
hackney: [pool: :default]
)
end end
# TODO: # TODO:
@ -368,17 +460,34 @@ def fetch_object_from_id(id) do
{:ok, object} {:ok, object}
else else
Logger.info("Fetching #{id} via AP") Logger.info("Fetching #{id} via AP")
with true <- String.starts_with?(id, "http"), with true <- String.starts_with?(id, "http"),
{:ok, %{body: body, status_code: code}} when code in 200..299 <- @httpoison.get(id, [Accept: "application/activity+json"], follow_redirect: true, timeout: 10000, recv_timeout: 20000), {:ok, %{body: body, status_code: code}} when code in 200..299 <-
@httpoison.get(
id,
[Accept: "application/activity+json"],
follow_redirect: true,
timeout: 10000,
recv_timeout: 20000
),
{:ok, data} <- Jason.decode(body), {:ok, data} <- Jason.decode(body),
nil <- Object.get_by_ap_id(data["id"]), nil <- Object.get_by_ap_id(data["id"]),
params <- %{"type" => "Create", "to" => data["to"], "cc" => data["cc"], "actor" => data["attributedTo"], "object" => data}, params <- %{
"type" => "Create",
"to" => data["to"],
"cc" => data["cc"],
"actor" => data["attributedTo"],
"object" => data
},
{:ok, activity} <- Transmogrifier.handle_incoming(params) do {:ok, activity} <- Transmogrifier.handle_incoming(params) do
{:ok, Object.get_by_ap_id(activity.data["object"]["id"])} {:ok, Object.get_by_ap_id(activity.data["object"]["id"])}
else else
object = %Object{} -> {:ok, object} object = %Object{} ->
{:ok, object}
e -> e ->
Logger.info("Couldn't get object via AP, trying out OStatus fetching...") Logger.info("Couldn't get object via AP, trying out OStatus fetching...")
case OStatus.fetch_activity_from_url(id) do case OStatus.fetch_activity_from_url(id) do
{:ok, [activity | _]} -> {:ok, Object.get_by_ap_id(activity.data["object"]["id"])} {:ok, [activity | _]} -> {:ok, Object.get_by_ap_id(activity.data["object"]["id"])}
e -> e e -> e
@ -388,15 +497,17 @@ def fetch_object_from_id(id) do
end end
def is_public?(activity) do def is_public?(activity) do
"https://www.w3.org/ns/activitystreams#Public" in (activity.data["to"] ++ (activity.data["cc"] || [])) "https://www.w3.org/ns/activitystreams#Public" in (activity.data["to"] ++
(activity.data["cc"] || []))
end end
def visible_for_user?(activity, nil) do def visible_for_user?(activity, nil) do
is_public?(activity) is_public?(activity)
end end
def visible_for_user?(activity, user) do def visible_for_user?(activity, user) do
x = [user.ap_id | user.following] x = [user.ap_id | user.following]
y = (activity.data["to"] ++ (activity.data["cc"] || [])) y = activity.data["to"] ++ (activity.data["cc"] || [])
visible_for_user?(activity, nil) || Enum.any?(x, &(&1 in y)) visible_for_user?(activity, nil) || Enum.any?(x, &(&1 in y))
end end
end end

View file

@ -7,7 +7,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
require Logger require Logger
action_fallback :errors action_fallback(:errors)
def user(conn, %{"nickname" => nickname}) do def user(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
@ -31,6 +31,7 @@ def following(conn, %{"nickname" => nickname, "page" => page}) do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do {:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do
{page, _} = Integer.parse(page) {page, _} = Integer.parse(page)
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_header("content-type", "application/activity+json")
|> json(UserView.render("following.json", %{user: user, page: page})) |> json(UserView.render("following.json", %{user: user, page: page}))
@ -50,6 +51,7 @@ def followers(conn, %{"nickname" => nickname, "page" => page}) do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do {:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do
{page, _} = Integer.parse(page) {page, _} = Integer.parse(page)
conn conn
|> put_resp_header("content-type", "application/activity+json") |> put_resp_header("content-type", "application/activity+json")
|> json(UserView.render("followers.json", %{user: user, page: page})) |> json(UserView.render("followers.json", %{user: user, page: page}))
@ -74,7 +76,9 @@ def outbox(conn, %{"nickname" => nickname, "max_id" => max_id}) do
end end
end end
def outbox(conn, %{"nickname" => nickname}) do outbox(conn, %{"nickname" => nickname, "max_id" => nil}) end def outbox(conn, %{"nickname" => nickname}) do
outbox(conn, %{"nickname" => nickname, "max_id" => nil})
end
# TODO: Ensure that this inbox is a recipient of the message # TODO: Ensure that this inbox is a recipient of the message
def inbox(%{assigns: %{valid_signature: true}} = conn, params) do def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
@ -84,7 +88,8 @@ def inbox(%{assigns: %{valid_signature: true}} = conn, params) do
def inbox(conn, params) do def inbox(conn, params) do
headers = Enum.into(conn.req_headers, %{}) headers = Enum.into(conn.req_headers, %{})
if !(String.contains?(headers["signature"] || "", params["actor"])) do
if !String.contains?(headers["signature"] || "", params["actor"]) do
Logger.info("Signature not from author, relayed message, fetching from source") Logger.info("Signature not from author, relayed message, fetching from source")
ActivityPub.fetch_object_from_id(params["object"]["id"]) ActivityPub.fetch_object_from_id(params["object"]["id"])
else else

View file

@ -25,21 +25,25 @@ def fix_object(object) do
|> fix_tag |> fix_tag
end end
def fix_in_reply_to(%{"inReplyTo" => in_reply_to_id} = object) when not is_nil(in_reply_to_id) do def fix_in_reply_to(%{"inReplyTo" => in_reply_to_id} = object)
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
{:ok, replied_object} -> {:ok, replied_object} ->
activity = Activity.get_create_activity_by_object_ap_id(replied_object.data["id"]) activity = Activity.get_create_activity_by_object_ap_id(replied_object.data["id"])
object object
|> Map.put("inReplyTo", replied_object.data["id"]) |> Map.put("inReplyTo", replied_object.data["id"])
|> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id) |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
|> Map.put("inReplyToStatusId", activity.id) |> Map.put("inReplyToStatusId", activity.id)
|> Map.put("conversation", replied_object.data["context"] || object["conversation"]) |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
|> Map.put("context", replied_object.data["context"] || object["conversation"]) |> Map.put("context", replied_object.data["context"] || object["conversation"])
e -> e ->
Logger.error("Couldn't fetch #{object["inReplyTo"]} #{inspect(e)}") Logger.error("Couldn't fetch #{object["inReplyTo"]} #{inspect(e)}")
object object
end end
end end
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
@ -48,27 +52,32 @@ def fix_context(object) do
end end
def fix_attachments(object) do def fix_attachments(object) do
attachments = (object["attachment"] || []) attachments =
|> Enum.map(fn (data) -> (object["attachment"] || [])
url = [%{"type" => "Link", "mediaType" => data["mediaType"], "href" => data["url"]}] |> Enum.map(fn data ->
Map.put(data, "url", url) url = [%{"type" => "Link", "mediaType" => data["mediaType"], "href" => data["url"]}]
end) Map.put(data, "url", url)
end)
object object
|> Map.put("attachment", attachments) |> Map.put("attachment", attachments)
end end
def fix_emoji(object) do def fix_emoji(object) do
tags = (object["tag"] || []) tags = object["tag"] || []
emoji = tags |> Enum.filter(fn (data) -> data["type"] == "Emoji" and data["icon"] end) emoji = tags |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
emoji = emoji |> Enum.reduce(%{}, fn (data, mapping) ->
name = data["name"]
if String.starts_with?(name, ":") do
name = name |> String.slice(1..-2)
end
mapping |> Map.put(name, data["icon"]["url"]) emoji =
end) emoji
|> Enum.reduce(%{}, fn data, mapping ->
name = data["name"]
if String.starts_with?(name, ":") do
name = name |> String.slice(1..-2)
end
mapping |> Map.put(name, data["icon"]["url"])
end)
# we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats # we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
emoji = Map.merge(object["emoji"] || %{}, emoji) emoji = Map.merge(object["emoji"] || %{}, emoji)
@ -78,9 +87,10 @@ def fix_emoji(object) do
end end
def fix_tag(object) do def fix_tag(object) do
tags = (object["tag"] || []) tags =
|> Enum.filter(fn (data) -> data["type"] == "Hashtag" and data["name"] end) (object["tag"] || [])
|> Enum.map(fn (data) -> String.slice(data["name"], 1..-1) end) |> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
|> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
combined = (object["tag"] || []) ++ tags combined = (object["tag"] || []) ++ tags
@ -103,13 +113,13 @@ def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = obje
context: object["conversation"], context: object["conversation"],
local: false, local: false,
published: data["published"], published: data["published"],
additional: Map.take(data, [ additional:
"cc", Map.take(data, [
"id" "cc",
]) "id"
])
} }
ActivityPub.create(params) ActivityPub.create(params)
else else
%Activity{} = activity -> {:ok, activity} %Activity{} = activity -> {:ok, activity}
@ -117,11 +127,14 @@ def handle_incoming(%{"type" => "Create", "object" => %{"type" => "Note"} = obje
end end
end end
def handle_incoming(%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data) do def handle_incoming(
%{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data
) do
with %User{local: true} = followed <- User.get_cached_by_ap_id(followed), with %User{local: true} = followed <- User.get_cached_by_ap_id(followed),
%User{} = follower <- User.get_or_fetch_by_ap_id(follower), %User{} = follower <- User.get_or_fetch_by_ap_id(follower),
{:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do
ActivityPub.accept(%{to: [follower.ap_id], actor: followed.ap_id, object: data, local: true}) ActivityPub.accept(%{to: [follower.ap_id], actor: followed.ap_id, object: data, local: true})
User.follow(follower, followed) User.follow(follower, followed)
{:ok, activity} {:ok, activity}
else else
@ -129,7 +142,9 @@ def handle_incoming(%{"type" => "Follow", "object" => followed, "actor" => follo
end end
end end
def handle_incoming(%{"type" => "Like", "object" => object_id, "actor" => actor, "id" => id} = data) do def handle_incoming(
%{"type" => "Like", "object" => object_id, "actor" => actor, "id" => id} = data
) do
with %User{} = actor <- User.get_or_fetch_by_ap_id(actor), with %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id), {:ok, object} <- get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
{:ok, activity, object} <- ActivityPub.like(actor, object, id, false) do {:ok, activity, object} <- ActivityPub.like(actor, object, id, false) do
@ -139,7 +154,9 @@ def handle_incoming(%{"type" => "Like", "object" => object_id, "actor" => actor,
end end
end end
def handle_incoming(%{"type" => "Announce", "object" => object_id, "actor" => actor, "id" => id} = data) do def handle_incoming(
%{"type" => "Announce", "object" => object_id, "actor" => actor, "id" => id} = data
) do
with %User{} = actor <- User.get_or_fetch_by_ap_id(actor), with %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id), {:ok, object} <- get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
{:ok, activity, object} <- ActivityPub.announce(actor, object, id, false) do {:ok, activity, object} <- ActivityPub.announce(actor, object, id, false) do
@ -149,20 +166,31 @@ def handle_incoming(%{"type" => "Announce", "object" => object_id, "actor" => ac
end end
end end
def handle_incoming(%{"type" => "Update", "object" => %{"type" => "Person"} = object, "actor" => actor_id} = data) do def handle_incoming(
%{"type" => "Update", "object" => %{"type" => "Person"} = object, "actor" => actor_id} =
data
) do
with %User{ap_id: ^actor_id} = actor <- User.get_by_ap_id(object["id"]) do with %User{ap_id: ^actor_id} = actor <- User.get_by_ap_id(object["id"]) do
{:ok, new_user_data} = ActivityPub.user_data_from_user_object(object) {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object)
banner = new_user_data[:info]["banner"] banner = new_user_data[:info]["banner"]
update_data = new_user_data
|> Map.take([:name, :bio, :avatar]) update_data =
|> Map.put(:info, Map.merge(actor.info, %{"banner" => banner})) new_user_data
|> Map.take([:name, :bio, :avatar])
|> Map.put(:info, Map.merge(actor.info, %{"banner" => banner}))
actor actor
|> User.upgrade_changeset(update_data) |> User.upgrade_changeset(update_data)
|> User.update_and_set_cache() |> User.update_and_set_cache()
ActivityPub.update(%{local: false, to: data["to"] || [], cc: data["cc"] || [], object: object, actor: actor_id}) ActivityPub.update(%{
local: false,
to: data["to"] || [],
cc: data["cc"] || [],
object: object,
actor: actor_id
})
else else
e -> e ->
Logger.error(e) Logger.error(e)
@ -171,11 +199,15 @@ def handle_incoming(%{"type" => "Update", "object" => %{"type" => "Person"} = ob
end end
# TODO: Make secure. # TODO: Make secure.
def handle_incoming(%{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => id} = data) do def handle_incoming(
object_id = case object_id do %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => id} = data
%{"id" => id} -> id ) do
id -> id object_id =
end case object_id do
%{"id" => id} -> id
id -> id
end
with %User{} = actor <- User.get_or_fetch_by_ap_id(actor), with %User{} = actor <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id), {:ok, object} <- get_obj_helper(object_id) || ActivityPub.fetch_object_from_id(object_id),
{:ok, activity} <- ActivityPub.delete(object, false) do {:ok, activity} <- ActivityPub.delete(object, false) do
@ -203,6 +235,7 @@ def set_reply_to_uri(%{"inReplyTo" => inReplyTo} = object) do
_e -> object _e -> object
end end
end end
def set_reply_to_uri(obj), do: obj def set_reply_to_uri(obj), do: obj
# Prepares the object of an outgoing create activity. # Prepares the object of an outgoing create activity.
@ -222,20 +255,25 @@ def prepare_object(object) do
""" """
internal -> Mastodon internal -> Mastodon
""" """
def prepare_outgoing(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do def prepare_outgoing(%{"type" => "Create", "object" => %{"type" => "Note"} = object} = data) do
object = object object =
|> prepare_object object
data = data |> prepare_object
|> Map.put("object", object)
|> Map.put("@context", "https://www.w3.org/ns/activitystreams") data =
data
|> Map.put("object", object)
|> Map.put("@context", "https://www.w3.org/ns/activitystreams")
{:ok, data} {:ok, data}
end end
def prepare_outgoing(%{"type" => type} = data) do def prepare_outgoing(%{"type" => type} = data) do
data = data data =
|> maybe_fix_object_url data
|> Map.put("@context", "https://www.w3.org/ns/activitystreams") |> maybe_fix_object_url
|> Map.put("@context", "https://www.w3.org/ns/activitystreams")
{:ok, data} {:ok, data}
end end
@ -245,11 +283,13 @@ def maybe_fix_object_url(data) do
case ActivityPub.fetch_object_from_id(data["object"]) do case ActivityPub.fetch_object_from_id(data["object"]) do
{:ok, relative_object} -> {:ok, relative_object} ->
if relative_object.data["external_url"] do if relative_object.data["external_url"] do
data = data data =
|> Map.put("object", relative_object.data["external_url"]) data
|> Map.put("object", relative_object.data["external_url"])
else else
data data
end end
e -> e ->
Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}") Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
data data
@ -260,8 +300,15 @@ def maybe_fix_object_url(data) do
end end
def add_hashtags(object) do def add_hashtags(object) do
tags = (object["tag"] || []) tags =
|> Enum.map fn (tag) -> %{"href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}", "name" => "##{tag}", "type" => "Hashtag"} end (object["tag"] || [])
|> Enum.map(fn tag ->
%{
"href" => Pleroma.Web.Endpoint.url() <> "/tags/#{tag}",
"name" => "##{tag}",
"type" => "Hashtag"
}
end)
object object
|> Map.put("tag", tags) |> Map.put("tag", tags)
@ -269,10 +316,14 @@ def add_hashtags(object) do
def add_mention_tags(object) do def add_mention_tags(object) do
recipients = object["to"] ++ (object["cc"] || []) recipients = object["to"] ++ (object["cc"] || [])
mentions = recipients
|> Enum.map(fn (ap_id) -> User.get_cached_by_ap_id(ap_id) end) mentions =
|> Enum.filter(&(&1)) recipients
|> Enum.map(fn(user) -> %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"} end) |> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
|> Enum.filter(& &1)
|> Enum.map(fn user ->
%{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
end)
tags = object["tag"] || [] tags = object["tag"] || []
@ -284,13 +335,18 @@ def add_mention_tags(object) do
def add_emoji_tags(object) do def add_emoji_tags(object) do
tags = object["tag"] || [] tags = object["tag"] || []
emoji = object["emoji"] || [] emoji = object["emoji"] || []
out = emoji |> Enum.map(fn {name, url} ->
%{"icon" => %{"url" => url, "type" => "Image"}, out =
"name" => ":" <> name <> ":", emoji
"type" => "Emoji", |> Enum.map(fn {name, url} ->
"updated" => "1970-01-01T00:00:00Z", %{
"id" => url} "icon" => %{"url" => url, "type" => "Image"},
end) "name" => ":" <> name <> ":",
"type" => "Emoji",
"updated" => "1970-01-01T00:00:00Z",
"id" => url
}
end)
object object
|> Map.put("tag", tags ++ out) |> Map.put("tag", tags ++ out)
@ -313,11 +369,12 @@ def add_attributed_to(object) do
end end
def prepare_attachments(object) do def prepare_attachments(object) do
attachments = (object["attachment"] || []) attachments =
|> Enum.map(fn (data) -> (object["attachment"] || [])
[%{"mediaType" => media_type, "href" => href} | _] = data["url"] |> Enum.map(fn data ->
%{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"} [%{"mediaType" => media_type, "href" => href} | _] = data["url"]
end) %{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
end)
object object
|> Map.put("attachment", attachments) |> Map.put("attachment", attachments)
@ -325,9 +382,24 @@ def prepare_attachments(object) do
defp user_upgrade_task(user) do defp user_upgrade_task(user) do
old_follower_address = User.ap_followers(user) old_follower_address = User.ap_followers(user)
q = from u in User,
where: ^old_follower_address in u.following, q =
update: [set: [following: fragment("array_replace(?,?,?)", u.following, ^old_follower_address, ^user.follower_address)]] from(
u in User,
where: ^old_follower_address in u.following,
update: [
set: [
following:
fragment(
"array_replace(?,?,?)",
u.following,
^old_follower_address,
^user.follower_address
)
]
]
)
Repo.update_all(q, []) Repo.update_all(q, [])
maybe_retire_websub(user.ap_id) maybe_retire_websub(user.ap_id)
@ -335,22 +407,40 @@ defp user_upgrade_task(user) do
# Only do this for recent activties, don't go through the whole db. # Only do this for recent activties, don't go through the whole db.
# Only look at the last 1000 activities. # Only look at the last 1000 activities.
since = (Repo.aggregate(Activity, :max, :id) || 0) - 1_000 since = (Repo.aggregate(Activity, :max, :id) || 0) - 1_000
q = from a in Activity,
where: ^old_follower_address in a.recipients, q =
where: a.id > ^since, from(
update: [set: [recipients: fragment("array_replace(?,?,?)", a.recipients, ^old_follower_address, ^user.follower_address)]] a in Activity,
where: ^old_follower_address in a.recipients,
where: a.id > ^since,
update: [
set: [
recipients:
fragment(
"array_replace(?,?,?)",
a.recipients,
^old_follower_address,
^user.follower_address
)
]
]
)
Repo.update_all(q, []) Repo.update_all(q, [])
end end
def upgrade_user_from_ap_id(ap_id, async \\ true) do def upgrade_user_from_ap_id(ap_id, async \\ true) do
with %User{local: false} = user <- User.get_by_ap_id(ap_id), with %User{local: false} = user <- User.get_by_ap_id(ap_id),
{:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id) do {:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id) do
data = data data =
|> Map.put(:info, Map.merge(user.info, data[:info])) data
|> Map.put(:info, Map.merge(user.info, data[:info]))
already_ap = User.ap_enabled?(user) already_ap = User.ap_enabled?(user)
{:ok, user} = User.upgrade_changeset(user, data)
|> Repo.update() {:ok, user} =
User.upgrade_changeset(user, data)
|> Repo.update()
if !already_ap do if !already_ap do
# This could potentially take a long time, do it in the background # This could potentially take a long time, do it in the background
@ -371,9 +461,13 @@ def upgrade_user_from_ap_id(ap_id, async \\ true) do
def maybe_retire_websub(ap_id) do def maybe_retire_websub(ap_id) do
# some sanity checks # some sanity checks
if is_binary(ap_id) && (String.length(ap_id) > 8) do if is_binary(ap_id) && String.length(ap_id) > 8 do
q = from ws in Pleroma.Web.Websub.WebsubClientSubscription, q =
where: fragment("? like ?", ws.topic, ^"#{ap_id}%") from(
ws in Pleroma.Web.Websub.WebsubClientSubscription,
where: fragment("? like ?", ws.topic, ^"#{ap_id}%")
)
Repo.delete_all(q) Repo.delete_all(q)
end end
end end

View file

@ -26,7 +26,7 @@ def make_json_ld_header do
end end
def make_date do def make_date do
DateTime.utc_now() |> DateTime.to_iso8601 DateTime.utc_now() |> DateTime.to_iso8601()
end end
def generate_activity_id do def generate_activity_id do
@ -38,25 +38,28 @@ def generate_context_id do
end end
def generate_object_id do def generate_object_id do
Helpers.o_status_url(Endpoint, :object, UUID.generate) Helpers.o_status_url(Endpoint, :object, UUID.generate())
end end
def generate_id(type) do def generate_id(type) do
"#{Web.base_url()}/#{type}/#{UUID.generate}" "#{Web.base_url()}/#{type}/#{UUID.generate()}"
end end
@doc """ @doc """
Enqueues an activity for federation if it's local Enqueues an activity for federation if it's local
""" """
def maybe_federate(%Activity{local: true} = activity) do def maybe_federate(%Activity{local: true} = activity) do
priority = case activity.data["type"] do priority =
"Delete" -> 10 case activity.data["type"] do
"Create" -> 1 "Delete" -> 10
_ -> 5 "Create" -> 1
end _ -> 5
end
Pleroma.Web.Federator.enqueue(:publish, activity, priority) Pleroma.Web.Federator.enqueue(:publish, activity, priority)
:ok :ok
end end
def maybe_federate(_), do: :ok def maybe_federate(_), do: :ok
@doc """ @doc """
@ -64,9 +67,10 @@ def maybe_federate(_), do: :ok
also adds it to an included object also adds it to an included object
""" """
def lazy_put_activity_defaults(map) do def lazy_put_activity_defaults(map) do
map = map map =
|> Map.put_new_lazy("id", &generate_activity_id/0) map
|> Map.put_new_lazy("published", &make_date/0) |> Map.put_new_lazy("id", &generate_activity_id/0)
|> Map.put_new_lazy("published", &make_date/0)
if is_map(map["object"]) do if is_map(map["object"]) do
object = lazy_put_object_defaults(map["object"]) object = lazy_put_object_defaults(map["object"])
@ -88,11 +92,13 @@ def lazy_put_object_defaults(map) do
@doc """ @doc """
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}) when is_map(object_data) and type in ["Note"] do def insert_full_object(%{"object" => %{"type" => type} = object_data})
when is_map(object_data) and type in ["Note"] do
with {:ok, _} <- Object.create(object_data) do with {:ok, _} <- Object.create(object_data) do
:ok :ok
end end
end end
def insert_full_object(_), do: :ok def insert_full_object(_), do: :ok
def update_object_in_activities(%{data: %{"id" => id}} = object) do def update_object_in_activities(%{data: %{"id" => id}} = object) do
@ -101,7 +107,8 @@ def update_object_in_activities(%{data: %{"id" => id}} = object) do
# Alternatively, just don't do this and fetch the current object each time. Most # Alternatively, just don't do this and fetch the current object each time. Most
# could probably be taken from cache. # could probably be taken from cache.
relevant_activities = Activity.all_by_object_ap_id(id) relevant_activities = Activity.all_by_object_ap_id(id)
Enum.map(relevant_activities, fn (activity) ->
Enum.map(relevant_activities, fn activity ->
new_activity_data = activity.data |> Map.put("object", object.data) new_activity_data = activity.data |> Map.put("object", object.data)
changeset = Changeset.change(activity, data: new_activity_data) changeset = Changeset.change(activity, data: new_activity_data)
Repo.update(changeset) Repo.update(changeset)
@ -114,11 +121,20 @@ def update_object_in_activities(%{data: %{"id" => id}} = object) do
Returns an existing like if a user already liked an object Returns an existing like if a user already liked an object
""" """
def get_existing_like(actor, %{data: %{"id" => id}}) do def get_existing_like(actor, %{data: %{"id" => id}}) do
query = from activity in Activity, query =
where: fragment("(?)->>'actor' = ?", activity.data, ^actor), from(
# this is to use the index activity in Activity,
where: fragment("coalesce((?)->'object'->>'id', (?)->>'object') = ?", activity.data, activity.data, ^id), where: fragment("(?)->>'actor' = ?", activity.data, ^actor),
where: fragment("(?)->>'type' = 'Like'", activity.data) # this is to use the index
where:
fragment(
"coalesce((?)->'object'->>'id', (?)->>'object') = ?",
activity.data,
activity.data,
^id
),
where: fragment("(?)->>'type' = 'Like'", activity.data)
)
Repo.one(query) Repo.one(query)
end end
@ -137,10 +153,12 @@ def make_like_data(%User{ap_id: ap_id} = actor, %{data: %{"id" => id}} = object,
end end
def update_element_in_object(property, element, object) do def update_element_in_object(property, element, object) do
with new_data <- object.data |> Map.put("#{property}_count", length(element)) |> Map.put("#{property}s", element), with new_data <-
object.data |> Map.put("#{property}_count", length(element))
|> Map.put("#{property}s", element),
changeset <- Changeset.change(object, data: new_data), changeset <- Changeset.change(object, data: new_data),
{:ok, object} <- Repo.update(changeset), {:ok, object} <- Repo.update(changeset),
_ <- update_object_in_activities(object) do _ <- update_object_in_activities(object) do
{:ok, object} {:ok, object}
end end
end end
@ -150,7 +168,7 @@ 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 with likes <- [actor | object.data["likes"] || []] |> Enum.uniq() do
update_likes_in_object(likes, object) update_likes_in_object(likes, object)
end end
end end
@ -178,13 +196,20 @@ def make_follow_data(%User{ap_id: follower_id}, %User{ap_id: followed_id}, activ
if activity_id, do: Map.put(data, "id", activity_id), else: data if activity_id, do: Map.put(data, "id", activity_id), else: data
end end
def fetch_latest_follow(%User{ap_id: follower_id}, def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
%User{ap_id: followed_id}) do query =
query = from activity in Activity, from(
where: fragment("? @> ?", activity.data, ^%{type: "Follow", actor: follower_id, activity in Activity,
object: followed_id}), where:
order_by: [desc: :id], fragment(
limit: 1 "? @> ?",
activity.data,
^%{type: "Follow", actor: follower_id, object: followed_id}
),
order_by: [desc: :id],
limit: 1
)
Repo.one(query) Repo.one(query)
end end
@ -193,7 +218,11 @@ def fetch_latest_follow(%User{ap_id: follower_id},
@doc """ @doc """
Make announce activity data for the given actor and object Make announce activity data for the given actor and object
""" """
def make_announce_data(%User{ap_id: ap_id} = user, %Object{data: %{"id" => id}} = object, activity_id) do def make_announce_data(
%User{ap_id: ap_id} = user,
%Object{data: %{"id" => id}} = object,
activity_id
) do
data = %{ data = %{
"type" => "Announce", "type" => "Announce",
"actor" => ap_id, "actor" => ap_id,
@ -207,7 +236,7 @@ def make_announce_data(%User{ap_id: ap_id} = user, %Object{data: %{"id" => id}}
end end
def add_announce_to_object(%Activity{data: %{"actor" => actor}}, object) do def add_announce_to_object(%Activity{data: %{"actor" => actor}}, object) do
with announcements <- [actor | (object.data["announcements"] || [])] |> Enum.uniq do with announcements <- [actor | object.data["announcements"] || []] |> Enum.uniq() do
update_element_in_object("announcement", announcements, object) update_element_in_object("announcement", announcements, object)
end end
end end
@ -223,14 +252,14 @@ def make_unfollow_data(follower, followed, follow_activity) do
} }
end end
#### Create-related helpers #### Create-related helpers
def make_create_data(params, additional) do def make_create_data(params, additional) do
published = params.published || make_date() published = params.published || make_date()
%{ %{
"type" => "Create", "type" => "Create",
"to" => params.to |> Enum.uniq, "to" => params.to |> Enum.uniq(),
"actor" => params.actor.ap_id, "actor" => params.actor.ap_id,
"object" => params.object, "object" => params.object,
"published" => published, "published" => published,

View file

@ -12,6 +12,7 @@ def render("user.json", %{user: user}) do
{:ok, _, public_key} = Salmon.keys_from_pem(user.info["keys"]) {:ok, _, public_key} = Salmon.keys_from_pem(user.info["keys"])
public_key = :public_key.pem_entry_encode(:RSAPublicKey, public_key) public_key = :public_key.pem_entry_encode(:RSAPublicKey, public_key)
public_key = :public_key.pem_encode([public_key]) public_key = :public_key.pem_encode([public_key])
%{ %{
"id" => user.ap_id, "id" => user.ap_id,
"type" => "Person", "type" => "Person",
@ -30,7 +31,7 @@ def render("user.json", %{user: user}) do
"publicKeyPem" => public_key "publicKeyPem" => public_key
}, },
"endpoints" => %{ "endpoints" => %{
"sharedInbox" => "#{Pleroma.Web.Endpoint.url}/inbox" "sharedInbox" => "#{Pleroma.Web.Endpoint.url()}/inbox"
}, },
"icon" => %{ "icon" => %{
"type" => "Image", "type" => "Image",
@ -47,7 +48,8 @@ def render("user.json", %{user: user}) do
def collection(collection, iri, page) do def collection(collection, iri, page) do
offset = (page - 1) * 10 offset = (page - 1) * 10
items = Enum.slice(collection, offset, 10) items = Enum.slice(collection, offset, 10)
items = Enum.map(items, fn (user) -> user.ap_id end) items = Enum.map(items, fn user -> user.ap_id end)
map = %{ map = %{
"id" => "#{iri}?page=#{page}", "id" => "#{iri}?page=#{page}",
"type" => "OrderedCollectionPage", "type" => "OrderedCollectionPage",
@ -55,19 +57,22 @@ def collection(collection, iri, page) do
"totalItems" => length(collection), "totalItems" => length(collection),
"orderedItems" => items "orderedItems" => items
} }
if offset < length(collection) do if offset < length(collection) do
Map.put(map, "next", "#{iri}?page=#{page+1}") Map.put(map, "next", "#{iri}?page=#{page + 1}")
end end
end end
def render("following.json", %{user: user, page: page}) do def render("following.json", %{user: user, page: page}) do
{:ok, following} = User.get_friends(user) {:ok, following} = User.get_friends(user)
collection(following, "#{user.ap_id}/following", page) collection(following, "#{user.ap_id}/following", page)
|> Map.merge(Utils.make_json_ld_header()) |> Map.merge(Utils.make_json_ld_header())
end end
def render("following.json", %{user: user}) do def render("following.json", %{user: user}) do
{:ok, following} = User.get_friends(user) {:ok, following} = User.get_friends(user)
%{ %{
"id" => "#{user.ap_id}/following", "id" => "#{user.ap_id}/following",
"type" => "OrderedCollection", "type" => "OrderedCollection",
@ -79,12 +84,14 @@ def render("following.json", %{user: user}) do
def render("followers.json", %{user: user, page: page}) do def render("followers.json", %{user: user, page: page}) do
{:ok, followers} = User.get_followers(user) {:ok, followers} = User.get_followers(user)
collection(followers, "#{user.ap_id}/followers", page) collection(followers, "#{user.ap_id}/followers", page)
|> Map.merge(Utils.make_json_ld_header()) |> Map.merge(Utils.make_json_ld_header())
end end
def render("followers.json", %{user: user}) do def render("followers.json", %{user: user}) do
{:ok, followers} = User.get_followers(user) {:ok, followers} = User.get_followers(user)
%{ %{
"id" => "#{user.ap_id}/followers", "id" => "#{user.ap_id}/followers",
"type" => "OrderedCollection", "type" => "OrderedCollection",
@ -115,19 +122,21 @@ def render("outbox.json", %{user: user, max_id: max_qid}) do
activities = Enum.reverse(activities) activities = Enum.reverse(activities)
max_id = Enum.at(activities, 0).id max_id = Enum.at(activities, 0).id
collection = Enum.map(activities, fn (act) -> collection =
{:ok, data} = Transmogrifier.prepare_outgoing(act.data) Enum.map(activities, fn act ->
data {:ok, data} = Transmogrifier.prepare_outgoing(act.data)
end) data
end)
iri = "#{user.ap_id}/outbox" iri = "#{user.ap_id}/outbox"
page = %{ page = %{
"id" => "#{iri}?max_id=#{max_id}", "id" => "#{iri}?max_id=#{max_id}",
"type" => "OrderedCollectionPage", "type" => "OrderedCollectionPage",
"partOf" => iri, "partOf" => iri,
"totalItems" => info.note_count, "totalItems" => info.note_count,
"orderedItems" => collection, "orderedItems" => collection,
"next" => "#{iri}?max_id=#{min_id-1}", "next" => "#{iri}?max_id=#{min_id - 1}"
} }
if max_qid == nil do if max_qid == nil do

View file

@ -6,11 +6,11 @@ defmodule Pleroma.Web.UserSocket do
## Channels ## Channels
# channel "room:*", Pleroma.Web.RoomChannel # channel "room:*", Pleroma.Web.RoomChannel
if Application.get_env(:pleroma, :chat) |> Keyword.get(:enabled) do if Application.get_env(:pleroma, :chat) |> Keyword.get(:enabled) do
channel "chat:*", Pleroma.Web.ChatChannel channel("chat:*", Pleroma.Web.ChatChannel)
end end
## Transports ## Transports
transport :websocket, Phoenix.Transports.WebSocket transport(:websocket, Phoenix.Transports.WebSocket)
# transport :longpoll, Phoenix.Transports.LongPoll # transport :longpoll, Phoenix.Transports.LongPoll
# Socket params are passed from the client and can # Socket params are passed from the client and can

View file

@ -9,19 +9,21 @@ def join("chat:public", _message, socket) do
end end
def handle_info(:after_join, socket) do def handle_info(:after_join, socket) do
push socket, "messages", %{messages: ChatChannelState.messages()} push(socket, "messages", %{messages: ChatChannelState.messages()})
{:noreply, socket} {:noreply, socket}
end end
def handle_in("new_msg", %{"text" => text}, %{assigns: %{user_name: user_name}} = socket) do def handle_in("new_msg", %{"text" => text}, %{assigns: %{user_name: user_name}} = socket) do
text = String.trim(text) text = String.trim(text)
if String.length(text) > 0 do if String.length(text) > 0 do
author = User.get_cached_by_nickname(user_name) author = User.get_cached_by_nickname(user_name)
author = Pleroma.Web.MastodonAPI.AccountView.render("account.json", user: author) author = Pleroma.Web.MastodonAPI.AccountView.render("account.json", user: author)
message = ChatChannelState.add_message(%{text: text, author: author}) message = ChatChannelState.add_message(%{text: text, author: author})
broadcast! socket, "new_msg", message broadcast!(socket, "new_msg", message)
end end
{:noreply, socket} {:noreply, socket}
end end
end end
@ -43,6 +45,6 @@ def add_message(message) do
end end
def messages() do def messages() do
Agent.get(__MODULE__, fn state -> state[:messages] |> Enum.reverse end) Agent.get(__MODULE__, fn state -> state[:messages] |> Enum.reverse() end)
end end
end end

View file

@ -8,7 +8,7 @@ defmodule Pleroma.Web.CommonAPI do
def delete(activity_id, user) do def delete(activity_id, user) do
with %Activity{data: %{"object" => %{"id" => object_id}}} <- Repo.get(Activity, activity_id), with %Activity{data: %{"object" => %{"id" => object_id}}} <- Repo.get(Activity, activity_id),
%Object{} = object <- Object.get_by_ap_id(object_id), %Object{} = object <- Object.get_by_ap_id(object_id),
true <- user.info["is_moderator"] || (user.ap_id == object.data["actor"]), true <- user.info["is_moderator"] || user.ap_id == object.data["actor"],
{:ok, delete} <- ActivityPub.delete(object) do {:ok, delete} <- ActivityPub.delete(object) do
{:ok, delete} {:ok, delete}
end end
@ -46,17 +46,22 @@ def unfavorite(id_or_ap_id, user) do
end end
end end
def get_visibility(%{"visibility" => visibility}) when visibility in ~w{public unlisted private direct}, do: visibility def get_visibility(%{"visibility" => visibility})
when visibility in ~w{public unlisted private direct},
do: visibility
def get_visibility(%{"in_reply_to_status_id" => status_id}) when not is_nil(status_id) do def get_visibility(%{"in_reply_to_status_id" => status_id}) when not is_nil(status_id) do
inReplyTo = get_replied_to_activity(status_id) inReplyTo = get_replied_to_activity(status_id)
Pleroma.Web.MastodonAPI.StatusView.get_visibility(inReplyTo.data["object"]) Pleroma.Web.MastodonAPI.StatusView.get_visibility(inReplyTo.data["object"])
end end
def get_visibility(_), do: "public" def get_visibility(_), do: "public"
@instance Application.get_env(:pleroma, :instance) @instance Application.get_env(:pleroma, :instance)
@limit Keyword.get(@instance, :limit) @limit Keyword.get(@instance, :limit)
def post(user, %{"status" => status} = data) do def post(user, %{"status" => status} = data) do
visibility = get_visibility(data) visibility = get_visibility(data)
with status <- String.trim(status), with status <- String.trim(status),
length when length in 1..@limit <- String.length(status), length when length in 1..@limit <- String.length(status),
attachments <- attachments_from_ids(data["media_ids"]), attachments <- attachments_from_ids(data["media_ids"]),
@ -64,18 +69,52 @@ def post(user, %{"status" => status} = data) do
inReplyTo <- get_replied_to_activity(data["in_reply_to_status_id"]), inReplyTo <- get_replied_to_activity(data["in_reply_to_status_id"]),
{to, cc} <- to_for_user_and_mentions(user, mentions, inReplyTo, visibility), {to, cc} <- to_for_user_and_mentions(user, mentions, inReplyTo, visibility),
tags <- Formatter.parse_tags(status, data), tags <- Formatter.parse_tags(status, data),
content_html <- make_content_html(status, mentions, attachments, tags, data["no_attachment_links"]), content_html <-
make_content_html(status, mentions, attachments, tags, data["no_attachment_links"]),
context <- make_context(inReplyTo), context <- make_context(inReplyTo),
cw <- data["spoiler_text"], cw <- data["spoiler_text"],
object <- make_note_data(user.ap_id, to, context, content_html, attachments, inReplyTo, tags, cw, cc), object <-
object <- Map.put(object, "emoji", Formatter.get_emoji(status) |> Enum.reduce(%{}, fn({name, file}, acc) -> Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url}#{file}") end)) do make_note_data(
res = ActivityPub.create(%{to: to, actor: user, context: context, object: object, additional: %{"cc" => cc}}) user.ap_id,
to,
context,
content_html,
attachments,
inReplyTo,
tags,
cw,
cc
),
object <-
Map.put(
object,
"emoji",
Formatter.get_emoji(status)
|> Enum.reduce(%{}, fn {name, file}, acc ->
Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url()}#{file}")
end)
) do
res =
ActivityPub.create(%{
to: to,
actor: user,
context: context,
object: object,
additional: %{"cc" => cc}
})
User.increase_note_count(user) User.increase_note_count(user)
res res
end end
end end
def update(user) do def update(user) do
ActivityPub.update(%{local: true, to: [user.follower_address], cc: [], actor: user.ap_id, object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})}) ActivityPub.update(%{
local: true,
to: [user.follower_address],
cc: [],
actor: user.ap_id,
object: Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
})
end end
end end

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
# This is a hack for twidere. # This is a hack for twidere.
def get_by_id_or_ap_id(id) do def get_by_id_or_ap_id(id) do
activity = Repo.get(Activity, id) || Activity.get_create_activity_by_object_ap_id(id) activity = Repo.get(Activity, id) || Activity.get_create_activity_by_object_ap_id(id)
if activity.data["type"] == "Create" do if activity.data["type"] == "Create" do
activity activity
else else
@ -16,10 +17,11 @@ def get_by_id_or_ap_id(id) do
def get_replied_to_activity(id) when not is_nil(id) do def get_replied_to_activity(id) when not is_nil(id) do
Repo.get(Activity, id) Repo.get(Activity, id)
end end
def get_replied_to_activity(_), do: nil def get_replied_to_activity(_), do: nil
def attachments_from_ids(ids) do def attachments_from_ids(ids) do
Enum.map(ids || [], fn (media_id) -> Enum.map(ids || [], fn media_id ->
Repo.get(Object, media_id).data Repo.get(Object, media_id).data
end) end)
end end
@ -27,8 +29,9 @@ def attachments_from_ids(ids) do
def to_for_user_and_mentions(user, mentions, inReplyTo, "public") do def to_for_user_and_mentions(user, mentions, inReplyTo, "public") do
to = ["https://www.w3.org/ns/activitystreams#Public"] to = ["https://www.w3.org/ns/activitystreams#Public"]
mentioned_users = Enum.map(mentions, fn ({_, %{ap_id: ap_id}}) -> ap_id end) mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end)
cc = [user.follower_address | mentioned_users] cc = [user.follower_address | mentioned_users]
if inReplyTo do if inReplyTo do
{to, Enum.uniq([inReplyTo.data["actor"] | cc])} {to, Enum.uniq([inReplyTo.data["actor"] | cc])}
else else
@ -47,7 +50,8 @@ def to_for_user_and_mentions(user, mentions, inReplyTo, "private") do
end end
def to_for_user_and_mentions(user, mentions, inReplyTo, "direct") do def to_for_user_and_mentions(user, mentions, inReplyTo, "direct") do
mentioned_users = Enum.map(mentions, fn ({_, %{ap_id: ap_id}}) -> ap_id end) mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end)
if inReplyTo do if inReplyTo do
{Enum.uniq([inReplyTo.data["actor"] | mentioned_users]), []} {Enum.uniq([inReplyTo.data["actor"] | mentioned_users]), []}
else else
@ -62,55 +66,72 @@ def make_content_html(status, mentions, attachments, tags, no_attachment_links \
end end
def make_context(%Activity{data: %{"context" => context}}), do: context def make_context(%Activity{data: %{"context" => context}}), do: context
def make_context(_), do: Utils.generate_context_id def make_context(_), do: Utils.generate_context_id()
def maybe_add_attachments(text, attachments, _no_links = true), do: text def maybe_add_attachments(text, attachments, _no_links = true), do: text
def maybe_add_attachments(text, attachments, _no_links) do def maybe_add_attachments(text, attachments, _no_links) do
add_attachments(text, attachments) add_attachments(text, attachments)
end end
def add_attachments(text, attachments) do def add_attachments(text, attachments) do
attachment_text = Enum.map(attachments, fn attachment_text =
(%{"url" => [%{"href" => href} | _]}) -> Enum.map(attachments, fn
name = URI.decode(Path.basename(href)) %{"url" => [%{"href" => href} | _]} ->
"<a href=\"#{href}\" class='attachment'>#{shortname(name)}</a>" name = URI.decode(Path.basename(href))
_ -> "" "<a href=\"#{href}\" class='attachment'>#{shortname(name)}</a>"
end)
_ ->
""
end)
Enum.join([text | attachment_text], "<br>") Enum.join([text | attachment_text], "<br>")
end end
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("\n", "<br>")
|> (&({[], &1})).() |> (&{[], &1}).()
|> Formatter.add_links |> Formatter.add_links()
|> Formatter.add_user_links(mentions) |> Formatter.add_user_links(mentions)
|> Formatter.add_hashtag_links(tags) |> Formatter.add_hashtag_links(tags)
|> Formatter.finalize |> Formatter.finalize()
end end
def add_tag_links(text, tags) do def add_tag_links(text, tags) do
tags = tags tags =
|> Enum.sort_by(fn ({tag, _}) -> -String.length(tag) end) tags
|> 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
def make_note_data(actor, to, context, content_html, attachments, inReplyTo, tags, cw \\ nil, cc \\ []) do def make_note_data(
object = %{ actor,
"type" => "Note", to,
"to" => to, context,
"cc" => cc, content_html,
"content" => content_html, attachments,
"summary" => cw, inReplyTo,
"context" => context, tags,
"attachment" => attachments, cw \\ nil,
"actor" => actor, cc \\ []
"tag" => tags |> Enum.map(fn ({_, tag}) -> tag end) ) do
} object = %{
"type" => "Note",
"to" => to,
"cc" => cc,
"content" => content_html,
"summary" => cw,
"context" => context,
"attachment" => attachments,
"actor" => actor,
"tag" => tags |> Enum.map(fn {_, tag} -> tag end)
}
if inReplyTo do if inReplyTo do
object object
@ -130,24 +151,25 @@ def format_asctime(date) do
end end
def date_to_asctime(date) do def date_to_asctime(date) do
with {:ok, date, _offset} <- date |> DateTime.from_iso8601 do with {:ok, date, _offset} <- date |> DateTime.from_iso8601() do
format_asctime(date) format_asctime(date)
else _e -> else
_e ->
"" ""
end end
end end
def to_masto_date(%NaiveDateTime{} = date) do def to_masto_date(%NaiveDateTime{} = date) do
date date
|> NaiveDateTime.to_iso8601 |> NaiveDateTime.to_iso8601()
|> String.replace(~r/(\.\d+)?$/, ".000Z", global: false) |> String.replace(~r/(\.\d+)?$/, ".000Z", global: false)
end end
def to_masto_date(date) do def to_masto_date(date) do
try do try do
date date
|> NaiveDateTime.from_iso8601! |> NaiveDateTime.from_iso8601!()
|> NaiveDateTime.to_iso8601 |> NaiveDateTime.to_iso8601()
|> String.replace(~r/(\.\d+)?$/, ".000Z", global: false) |> String.replace(~r/(\.\d+)?$/, ".000Z", global: false)
rescue rescue
_e -> "" _e -> ""

View file

@ -2,47 +2,55 @@ defmodule Pleroma.Web.Endpoint do
use Phoenix.Endpoint, otp_app: :pleroma use Phoenix.Endpoint, otp_app: :pleroma
if Application.get_env(:pleroma, :chat) |> Keyword.get(:enabled) do if Application.get_env(:pleroma, :chat) |> Keyword.get(:enabled) do
socket "/socket", Pleroma.Web.UserSocket socket("/socket", Pleroma.Web.UserSocket)
end end
socket "/api/v1", Pleroma.Web.MastodonAPI.MastodonSocket
socket("/api/v1", Pleroma.Web.MastodonAPI.MastodonSocket)
# Serve at "/" the static files from "priv/static" directory. # Serve at "/" the static files from "priv/static" directory.
# #
# You should set gzip to true if you are running phoenix.digest # You should set gzip to true if you are running phoenix.digest
# when deploying your static files in production. # when deploying your static files in production.
plug Plug.Static, plug(Plug.Static, at: "/media", from: "uploads", gzip: false)
at: "/media", from: "uploads", gzip: false
plug Plug.Static, plug(
at: "/", from: :pleroma, Plug.Static,
at: "/",
from: :pleroma,
only: ~w(index.html static finmoji emoji packs sounds images instance sw.js) only: ~w(index.html static finmoji emoji packs sounds images instance sw.js)
)
# Code reloading can be explicitly enabled under the # Code reloading can be explicitly enabled under the
# :code_reloader configuration of your endpoint. # :code_reloader configuration of your endpoint.
if code_reloading? do if code_reloading? do
plug Phoenix.CodeReloader plug(Phoenix.CodeReloader)
end end
plug TrailingFormatPlug plug(TrailingFormatPlug)
plug Plug.RequestId plug(Plug.RequestId)
plug Plug.Logger plug(Plug.Logger)
plug Plug.Parsers, plug(
Plug.Parsers,
parsers: [:urlencoded, :multipart, :json], parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"], pass: ["*/*"],
json_decoder: Jason json_decoder: Jason
)
plug Plug.MethodOverride plug(Plug.MethodOverride)
plug Plug.Head plug(Plug.Head)
# The session will be stored in the cookie and signed, # The session will be stored in the cookie and signed,
# this means its contents can be read but not tampered with. # this means its contents can be read but not tampered with.
# Set :encryption_salt if you would also like to encrypt it. # Set :encryption_salt if you would also like to encrypt it.
plug Plug.Session, plug(
Plug.Session,
store: :cookie, store: :cookie,
key: "_pleroma_key", key: "_pleroma_key",
signing_salt: "CqaoopA2" signing_salt: "CqaoopA2"
)
plug Pleroma.Web.Router plug(Pleroma.Web.Router)
@doc """ @doc """
Dynamically loads configuration from the system environment Dynamically loads configuration from the system environment

View file

@ -16,27 +16,36 @@ defmodule Pleroma.Web.Federator do
def start_link do def start_link do
spawn(fn -> spawn(fn ->
Process.sleep(1000 * 60 * 1) # 1 minute # 1 minute
Process.sleep(1000 * 60 * 1)
enqueue(:refresh_subscriptions, nil) enqueue(:refresh_subscriptions, nil)
end) end)
GenServer.start_link(__MODULE__, %{
in: {:sets.new(), []}, GenServer.start_link(
out: {:sets.new(), []} __MODULE__,
}, name: __MODULE__) %{
in: {:sets.new(), []},
out: {:sets.new(), []}
},
name: __MODULE__
)
end end
def handle(:refresh_subscriptions, _) do def handle(:refresh_subscriptions, _) do
Logger.debug("Federator running refresh subscriptions") Logger.debug("Federator running refresh subscriptions")
Websub.refresh_subscriptions() Websub.refresh_subscriptions()
spawn(fn -> spawn(fn ->
Process.sleep(1000 * 60 * 60 * 6) # 6 hours # 6 hours
Process.sleep(1000 * 60 * 60 * 6)
enqueue(:refresh_subscriptions, nil) enqueue(:refresh_subscriptions, nil)
end) end)
end end
def handle(:request_subscription, websub) do def handle(:request_subscription, websub) do
Logger.debug("Refreshing #{websub.topic}") Logger.debug("Refreshing #{websub.topic}")
with {:ok, websub } <- Websub.request_subscription(websub) do
with {:ok, websub} <- Websub.request_subscription(websub) do
Logger.debug("Successfully refreshed #{websub.topic}") Logger.debug("Successfully refreshed #{websub.topic}")
else else
_e -> Logger.debug("Couldn't refresh #{websub.topic}") _e -> Logger.debug("Couldn't refresh #{websub.topic}")
@ -45,8 +54,10 @@ def handle(:request_subscription, websub) do
def handle(:publish, activity) do def handle(:publish, activity) do
Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end) Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end)
with actor when not is_nil(actor) <- User.get_cached_by_ap_id(activity.data["actor"]) do with actor when not is_nil(actor) <- User.get_cached_by_ap_id(activity.data["actor"]) do
{:ok, actor} = WebFinger.ensure_keys_present(actor) {:ok, actor} = WebFinger.ensure_keys_present(actor)
if ActivityPub.is_public?(activity) do if ActivityPub.is_public?(activity) do
Logger.info(fn -> "Sending #{activity.data["id"]} out via WebSub" end) Logger.info(fn -> "Sending #{activity.data["id"]} out via WebSub" end)
Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity) Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity)
@ -61,7 +72,10 @@ def handle(:publish, activity) do
end end
def handle(:verify_websub, websub) do def handle(:verify_websub, websub) do
Logger.debug(fn -> "Running WebSub verification for #{websub.id} (#{websub.topic}, #{websub.callback})" end) Logger.debug(fn ->
"Running WebSub verification for #{websub.id} (#{websub.topic}, #{websub.callback})"
end)
@websub.verify(websub) @websub.verify(websub)
end end
@ -72,16 +86,18 @@ def handle(:incoming_doc, doc) do
def handle(:incoming_ap_doc, params) do def handle(:incoming_ap_doc, params) do
Logger.info("Handling incoming AP activity") Logger.info("Handling incoming AP activity")
with {:ok, _user} <- ap_enabled_actor(params["actor"]), with {:ok, _user} <- ap_enabled_actor(params["actor"]),
nil <- Activity.get_by_ap_id(params["id"]), nil <- Activity.get_by_ap_id(params["id"]),
{:ok, activity} <- Transmogrifier.handle_incoming(params) do {:ok, activity} <- Transmogrifier.handle_incoming(params) do
else else
%Activity{} -> %Activity{} ->
Logger.info("Already had #{params["id"]}") Logger.info("Already had #{params["id"]}")
e -> e ->
# Just drop those for now # Just drop those for now
Logger.info("Unhandled activity") Logger.info("Unhandled activity")
Logger.info(Poison.encode!(params, [pretty: 2])) Logger.info(Poison.encode!(params, pretty: 2))
end end
end end
@ -93,12 +109,21 @@ def handle(:publish_single_websub, %{xml: xml, topic: topic, callback: callback,
signature = @websub.sign(secret || "", xml) signature = @websub.sign(secret || "", xml)
Logger.debug(fn -> "Pushing #{topic} to #{callback}" end) Logger.debug(fn -> "Pushing #{topic} to #{callback}" end)
with {:ok, %{status_code: code}} <- @httpoison.post(callback, xml, [ with {:ok, %{status_code: code}} <-
{"Content-Type", "application/atom+xml"}, @httpoison.post(
{"X-Hub-Signature", "sha1=#{signature}"} callback,
], timeout: 10000, recv_timeout: 20000, hackney: [pool: :default]) do xml,
[
{"Content-Type", "application/atom+xml"},
{"X-Hub-Signature", "sha1=#{signature}"}
],
timeout: 10000,
recv_timeout: 20000,
hackney: [pool: :default]
) do
Logger.debug(fn -> "Pushed to #{callback}, code #{code}" end) Logger.debug(fn -> "Pushed to #{callback}, code #{code}" end)
else e -> else
e ->
Logger.debug(fn -> "Couldn't push to #{callback}, #{inspect(e)}" end) Logger.debug(fn -> "Couldn't push to #{callback}, #{inspect(e)}" end)
end end
end end
@ -110,7 +135,7 @@ def handle(type, _) do
def enqueue(type, payload, priority \\ 1) do def enqueue(type, payload, priority \\ 1) do
if @federating do if @federating do
if Mix.env == :test do if Mix.env() == :test do
handle(type, payload) handle(type, payload)
else else
GenServer.cast(__MODULE__, {:enqueue, type, payload, priority}) GenServer.cast(__MODULE__, {:enqueue, type, payload, priority})
@ -119,7 +144,7 @@ def enqueue(type, payload, priority \\ 1) do
end end
def maybe_start_job(running_jobs, queue) do def maybe_start_job(running_jobs, queue) do
if (:sets.size(running_jobs) < @max_jobs) && queue != [] do if :sets.size(running_jobs) < @max_jobs && queue != [] do
{{type, payload}, queue} = queue_pop(queue) {{type, payload}, queue} = queue_pop(queue)
{:ok, pid} = Task.start(fn -> handle(type, payload) end) {:ok, pid} = Task.start(fn -> handle(type, payload) end)
mref = Process.monitor(pid) mref = Process.monitor(pid)
@ -129,7 +154,8 @@ def maybe_start_job(running_jobs, queue) do
end end
end end
def handle_cast({:enqueue, type, payload, priority}, state) when type in [:incoming_doc, :incoming_ap_doc] do def handle_cast({:enqueue, type, payload, priority}, state)
when type in [:incoming_doc, :incoming_ap_doc] do
%{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state %{in: {i_running_jobs, i_queue}, out: {o_running_jobs, o_queue}} = state
i_queue = enqueue_sorted(i_queue, {type, payload}, 1) i_queue = enqueue_sorted(i_queue, {type, payload}, 1)
{i_running_jobs, i_queue} = maybe_start_job(i_running_jobs, i_queue) {i_running_jobs, i_queue} = maybe_start_job(i_running_jobs, i_queue)
@ -160,7 +186,7 @@ def handle_info({:DOWN, ref, :process, _pid, _reason}, state) do
def enqueue_sorted(queue, element, priority) do def enqueue_sorted(queue, element, priority) do
[%{item: element, priority: priority} | queue] [%{item: element, priority: priority} | queue]
|> Enum.sort_by(fn (%{priority: priority}) -> priority end) |> Enum.sort_by(fn %{priority: priority} -> priority end)
end end
def queue_pop([%{item: element} | queue]) do def queue_pop([%{item: element} | queue]) do
@ -169,6 +195,7 @@ def queue_pop([%{item: element} | queue]) do
def ap_enabled_actor(id) do def ap_enabled_actor(id) do
user = User.get_by_ap_id(id) user = User.get_by_ap_id(id)
if User.ap_enabled?(user) do if User.ap_enabled?(user) do
{:ok, user} {:ok, user}
else else

View file

@ -11,8 +11,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
require Logger require Logger
def create_app(conn, params) do def create_app(conn, params) do
with cs <- App.register_changeset(%App{}, params) |> IO.inspect, with cs <- App.register_changeset(%App{}, params) |> IO.inspect(),
{:ok, app} <- Repo.insert(cs) |> IO.inspect do {:ok, app} <- Repo.insert(cs) |> IO.inspect() do
res = %{ res = %{
id: app.id, id: app.id,
client_id: app.client_id, client_id: app.client_id,
@ -25,51 +25,57 @@ def create_app(conn, params) do
def update_credentials(%{assigns: %{user: user}} = conn, params) do def update_credentials(%{assigns: %{user: user}} = conn, params) do
original_user = user original_user = user
params = if bio = params["note"] do
Map.put(params, "bio", bio)
else
params
end
params = if name = params["display_name"] do params =
Map.put(params, "name", name) if bio = params["note"] do
else Map.put(params, "bio", bio)
params
end
user = if avatar = params["avatar"] do
with %Plug.Upload{} <- avatar,
{:ok, object} <- ActivityPub.upload(avatar),
change = Ecto.Changeset.change(user, %{avatar: object.data}),
{:ok, user} = User.update_and_set_cache(change) do
user
else else
_e -> user params
end end
else
user
end
user = if banner = params["header"] do params =
with %Plug.Upload{} <- banner, if name = params["display_name"] do
{:ok, object} <- ActivityPub.upload(banner), Map.put(params, "name", name)
new_info <- Map.put(user.info, "banner", object.data),
change <- User.info_changeset(user, %{info: new_info}),
{:ok, user} <- User.update_and_set_cache(change) do
user
else else
_e -> user params
end
user =
if avatar = params["avatar"] do
with %Plug.Upload{} <- avatar,
{:ok, object} <- ActivityPub.upload(avatar),
change = Ecto.Changeset.change(user, %{avatar: object.data}),
{:ok, user} = User.update_and_set_cache(change) do
user
else
_e -> user
end
else
user
end
user =
if banner = params["header"] do
with %Plug.Upload{} <- banner,
{:ok, object} <- ActivityPub.upload(banner),
new_info <- Map.put(user.info, "banner", object.data),
change <- User.info_changeset(user, %{info: new_info}),
{:ok, user} <- User.update_and_set_cache(change) do
user
else
_e -> user
end
else
user
end end
else
user
end
with changeset <- User.update_changeset(user, params), with changeset <- User.update_changeset(user, params),
{:ok, user} <- User.update_and_set_cache(changeset) do {:ok, user} <- User.update_and_set_cache(changeset) do
if original_user != user do if original_user != user do
CommonAPI.update(user) CommonAPI.update(user)
end end
json conn, AccountView.render("account.json", %{user: user})
json(conn, AccountView.render("account.json", %{user: user}))
else else
_e -> _e ->
conn conn
@ -88,9 +94,10 @@ def user(conn, %{"id" => id}) do
account = AccountView.render("account.json", %{user: user}) account = AccountView.render("account.json", %{user: user})
json(conn, account) json(conn, account)
else else
_e -> conn _e ->
|> put_status(404) conn
|> json(%{error: "Can't find user"}) |> put_status(404)
|> json(%{error: "Can't find user"})
end end
end end
@ -98,16 +105,16 @@ def user(conn, %{"id" => id}) do
def masto_instance(conn, _params) do def masto_instance(conn, _params) do
response = %{ response = %{
uri: Web.base_url, uri: Web.base_url(),
title: Keyword.get(@instance, :name), title: Keyword.get(@instance, :name),
description: "A Pleroma instance, an alternative fediverse server", description: "A Pleroma instance, an alternative fediverse server",
version: Keyword.get(@instance, :version), version: Keyword.get(@instance, :version),
email: Keyword.get(@instance, :email), email: Keyword.get(@instance, :email),
urls: %{ urls: %{
streaming_api: String.replace(Web.base_url, ["http","https"], "wss") streaming_api: String.replace(Web.base_url(), ["http", "https"], "wss")
}, },
stats: Stats.get_stats, stats: Stats.get_stats(),
thumbnail: Web.base_url <> "/instance/thumbnail.jpeg", thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
max_toot_chars: Keyword.get(@instance, :limit) max_toot_chars: Keyword.get(@instance, :limit)
} }
@ -115,13 +122,14 @@ def masto_instance(conn, _params) do
end end
def peers(conn, _params) do def peers(conn, _params) do
json(conn, Stats.get_peers) json(conn, Stats.get_peers())
end end
defp mastodonized_emoji do defp mastodonized_emoji do
Pleroma.Formatter.get_custom_emoji() Pleroma.Formatter.get_custom_emoji()
|> Enum.map(fn {shortcode, relative_url} -> |> Enum.map(fn {shortcode, relative_url} ->
url = to_string URI.merge(Web.base_url(), relative_url) url = to_string(URI.merge(Web.base_url(), relative_url))
%{ %{
"shortcode" => shortcode, "shortcode" => shortcode,
"static_url" => url, "static_url" => url,
@ -132,26 +140,30 @@ defp mastodonized_emoji do
def custom_emojis(conn, _params) do def custom_emojis(conn, _params) do
mastodon_emoji = mastodonized_emoji() mastodon_emoji = mastodonized_emoji()
json conn, mastodon_emoji json(conn, mastodon_emoji)
end end
defp add_link_headers(conn, method, activities, param \\ false) do defp add_link_headers(conn, method, activities, param \\ false) do
last = List.last(activities) last = List.last(activities)
first = List.first(activities) first = List.first(activities)
if last do if last do
min = last.id min = last.id
max = first.id max = first.id
{next_url, prev_url} = if param do
{ {next_url, prev_url} =
mastodon_api_url(Pleroma.Web.Endpoint, method, param, max_id: min), if param do
mastodon_api_url(Pleroma.Web.Endpoint, method, param, since_id: max) {
} mastodon_api_url(Pleroma.Web.Endpoint, method, param, max_id: min),
else mastodon_api_url(Pleroma.Web.Endpoint, method, param, since_id: max)
{ }
mastodon_api_url(Pleroma.Web.Endpoint, method, max_id: min), else
mastodon_api_url(Pleroma.Web.Endpoint, method, since_id: max) {
} mastodon_api_url(Pleroma.Web.Endpoint, method, max_id: min),
end mastodon_api_url(Pleroma.Web.Endpoint, method, since_id: max)
}
end
conn conn
|> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"") |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
else else
@ -160,13 +172,15 @@ defp add_link_headers(conn, method, activities, param \\ false) do
end end
def home_timeline(%{assigns: %{user: user}} = conn, params) do def home_timeline(%{assigns: %{user: user}} = conn, params) do
params = params params =
|> Map.put("type", ["Create", "Announce"]) params
|> Map.put("blocking_user", user) |> Map.put("type", ["Create", "Announce"])
|> Map.put("user", user) |> Map.put("blocking_user", user)
|> Map.put("user", user)
activities = ActivityPub.fetch_activities([user.ap_id | user.following], params) activities =
|> Enum.reverse ActivityPub.fetch_activities([user.ap_id | user.following], params)
|> Enum.reverse()
conn conn
|> add_link_headers(:home_timeline, activities) |> add_link_headers(:home_timeline, activities)
@ -174,13 +188,15 @@ def home_timeline(%{assigns: %{user: user}} = conn, params) do
end end
def public_timeline(%{assigns: %{user: user}} = conn, params) do def public_timeline(%{assigns: %{user: user}} = conn, params) do
params = params params =
|> Map.put("type", ["Create", "Announce"]) params
|> Map.put("local_only", params["local"] in [true, "True", "true", "1"]) |> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user) |> Map.put("local_only", params["local"] in [true, "True", "true", "1"])
|> Map.put("blocking_user", user)
activities = ActivityPub.fetch_public_activities(params) activities =
|> Enum.reverse ActivityPub.fetch_public_activities(params)
|> Enum.reverse()
conn conn
|> add_link_headers(:public_timeline, activities) |> add_link_headers(:public_timeline, activities)
@ -189,13 +205,15 @@ def public_timeline(%{assigns: %{user: user}} = conn, params) do
def user_statuses(%{assigns: %{user: user}} = conn, params) do def user_statuses(%{assigns: %{user: user}} = conn, params) do
with %User{ap_id: ap_id} <- Repo.get(User, params["id"]) do with %User{ap_id: ap_id} <- Repo.get(User, params["id"]) do
params = params params =
|> Map.put("type", ["Create", "Announce"]) params
|> Map.put("actor_id", ap_id) |> Map.put("type", ["Create", "Announce"])
|> Map.put("whole_db", true) |> Map.put("actor_id", ap_id)
|> Map.put("whole_db", true)
activities = ActivityPub.fetch_public_activities(params) activities =
|> Enum.reverse ActivityPub.fetch_public_activities(params)
|> Enum.reverse()
conn conn
|> add_link_headers(:user_statuses, activities, params["id"]) |> add_link_headers(:user_statuses, activities, params["id"])
@ -206,19 +224,39 @@ def user_statuses(%{assigns: %{user: user}} = conn, params) do
def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Repo.get(Activity, id), with %Activity{} = activity <- Repo.get(Activity, id),
true <- ActivityPub.visible_for_user?(activity, user) do true <- ActivityPub.visible_for_user?(activity, user) do
render conn, StatusView, "status.json", %{activity: activity, for: user} render(conn, StatusView, "status.json", %{activity: activity, for: user})
end end
end end
def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Repo.get(Activity, id), with %Activity{} = activity <- Repo.get(Activity, id),
activities <- ActivityPub.fetch_activities_for_context(activity.data["context"], %{"blocking_user" => user, "user" => user}), activities <-
activities <- activities |> Enum.filter(fn (%{id: aid}) -> to_string(aid) != to_string(id) end), ActivityPub.fetch_activities_for_context(activity.data["context"], %{
activities <- activities |> Enum.filter(fn (%{data: %{"type" => type}}) -> type == "Create" end), "blocking_user" => user,
grouped_activities <- Enum.group_by(activities, fn (%{id: id}) -> id < activity.id end) do "user" => user
}),
activities <-
activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
activities <-
activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
result = %{ result = %{
ancestors: StatusView.render("index.json", for: user, activities: grouped_activities[true] || [], as: :activity) |> Enum.reverse, ancestors:
descendants: StatusView.render("index.json", for: user, activities: grouped_activities[false] || [], as: :activity) |> Enum.reverse, StatusView.render(
"index.json",
for: user,
activities: grouped_activities[true] || [],
as: :activity
)
|> Enum.reverse(),
descendants:
StatusView.render(
"index.json",
for: user,
activities: grouped_activities[false] || [],
as: :activity
)
|> Enum.reverse()
} }
json(conn, result) json(conn, result)
@ -226,12 +264,13 @@ def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
end end
def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
params = params params =
|> Map.put("in_reply_to_status_id", params["in_reply_to_id"]) params
|> Map.put("no_attachment_links", true) |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
|> Map.put("no_attachment_links", true)
{:ok, activity} = CommonAPI.post(user, params) {:ok, activity} = CommonAPI.post(user, params)
render conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity} render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
end end
def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
@ -247,30 +286,32 @@ def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, announce, _activity} = CommonAPI.repeat(ap_id_or_id, user) do with {:ok, announce, _activity} = CommonAPI.repeat(ap_id_or_id, user) do
render conn, StatusView, "status.json", %{activity: announce, for: user, as: :activity} render(conn, StatusView, "status.json", %{activity: announce, for: user, as: :activity})
end end
end end
def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do 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), 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 %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
render conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity} render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
end end
end end
def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do 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), with {:ok, %{data: %{"id" => id}}} = CommonAPI.unfavorite(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
render conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity} render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
end end
end end
def notifications(%{assigns: %{user: user}} = conn, params) do def notifications(%{assigns: %{user: user}} = conn, params) do
notifications = Notification.for_user(user, params) notifications = Notification.for_user(user, params)
result = Enum.map(notifications, fn x ->
render_notification(user, x) result =
end) Enum.map(notifications, fn x ->
|> Enum.filter(&(&1)) render_notification(user, x)
end)
|> Enum.filter(& &1)
conn conn
|> add_link_headers(:notifications, notifications) |> add_link_headers(:notifications, notifications)
@ -306,27 +347,26 @@ def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _para
def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
id = List.wrap(id) id = List.wrap(id)
q = from u in User, q = from(u in User, where: u.id in ^id)
where: u.id in ^id
targets = Repo.all(q) targets = Repo.all(q)
render conn, AccountView, "relationships.json", %{user: user, targets: targets} render(conn, AccountView, "relationships.json", %{user: user, targets: targets})
end end
def upload(%{assigns: %{user: _}} = conn, %{"file" => file}) do def upload(%{assigns: %{user: _}} = conn, %{"file" => file}) do
with {:ok, object} <- ActivityPub.upload(file) do with {:ok, object} <- ActivityPub.upload(file) do
data = object.data data =
|> Map.put("id", object.id) object.data
|> Map.put("id", object.id)
render conn, StatusView, "attachment.json", %{attachment: data} render(conn, StatusView, "attachment.json", %{attachment: data})
end end
end end
def favourited_by(conn, %{"id" => id}) do def favourited_by(conn, %{"id" => id}) do
with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
q = from u in User, q = from(u in User, where: u.ap_id in ^likes)
where: u.ap_id in ^likes
users = Repo.all(q) users = Repo.all(q)
render conn, AccountView, "accounts.json", %{users: users, as: :user} render(conn, AccountView, "accounts.json", %{users: users, as: :user})
else else
_ -> json(conn, []) _ -> json(conn, [])
end end
@ -334,23 +374,24 @@ def favourited_by(conn, %{"id" => id}) do
def reblogged_by(conn, %{"id" => id}) do def reblogged_by(conn, %{"id" => id}) do
with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
q = from u in User, q = from(u in User, where: u.ap_id in ^announces)
where: u.ap_id in ^announces
users = Repo.all(q) users = Repo.all(q)
render conn, AccountView, "accounts.json", %{users: users, as: :user} render(conn, AccountView, "accounts.json", %{users: users, as: :user})
else else
_ -> json(conn, []) _ -> json(conn, [])
end end
end end
def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
params = params params =
|> Map.put("type", "Create") params
|> Map.put("local_only", !!params["local"]) |> Map.put("type", "Create")
|> Map.put("blocking_user", user) |> Map.put("local_only", !!params["local"])
|> Map.put("blocking_user", user)
activities = ActivityPub.fetch_public_activities(params) activities =
|> Enum.reverse ActivityPub.fetch_public_activities(params)
|> Enum.reverse()
conn conn
|> add_link_headers(:hashtag_timeline, activities, params["tag"]) |> add_link_headers(:hashtag_timeline, activities, params["tag"])
@ -361,14 +402,14 @@ def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
def followers(conn, %{"id" => id}) do def followers(conn, %{"id" => id}) do
with %User{} = user <- Repo.get(User, id), with %User{} = user <- Repo.get(User, id),
{:ok, followers} <- User.get_followers(user) do {:ok, followers} <- User.get_followers(user) do
render conn, AccountView, "accounts.json", %{users: followers, as: :user} render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
end end
end end
def following(conn, %{"id" => id}) do def following(conn, %{"id" => id}) do
with %User{} = user <- Repo.get(User, id), with %User{} = user <- Repo.get(User, id),
{:ok, followers} <- User.get_friends(user) do {:ok, followers} <- User.get_friends(user) do
render conn, AccountView, "accounts.json", %{users: followers, as: :user} render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
end end
end end
@ -376,7 +417,7 @@ def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
with %User{} = followed <- Repo.get(User, id), with %User{} = followed <- Repo.get(User, id),
{:ok, follower} <- User.follow(follower, followed), {:ok, follower} <- User.follow(follower, followed),
{:ok, _activity} <- ActivityPub.follow(follower, followed) do {:ok, _activity} <- ActivityPub.follow(follower, followed) do
render conn, AccountView, "relationship.json", %{user: follower, target: followed} render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
else else
{:error, message} -> {:error, message} ->
conn conn
@ -389,7 +430,7 @@ def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
with %User{} = followed <- Repo.get_by(User, nickname: uri), with %User{} = followed <- Repo.get_by(User, nickname: uri),
{:ok, follower} <- User.follow(follower, followed), {:ok, follower} <- User.follow(follower, followed),
{:ok, _activity} <- ActivityPub.follow(follower, followed) do {:ok, _activity} <- ActivityPub.follow(follower, followed) do
render conn, AccountView, "account.json", %{user: followed} render(conn, AccountView, "account.json", %{user: followed})
else else
{:error, message} -> {:error, message} ->
conn conn
@ -401,20 +442,22 @@ def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
# TODO: Clean up and unify # TODO: Clean up and unify
def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
with %User{} = followed <- Repo.get(User, id), with %User{} = followed <- Repo.get(User, id),
{ :ok, follower, follow_activity } <- User.unfollow(follower, followed), {:ok, follower, follow_activity} <- User.unfollow(follower, followed),
{ :ok, _activity } <- ActivityPub.insert(%{ {:ok, _activity} <-
"type" => "Undo", ActivityPub.insert(%{
"actor" => follower.ap_id, "type" => "Undo",
"object" => follow_activity.data["id"] # get latest Follow for these users "actor" => follower.ap_id,
}) do # get latest Follow for these users
render conn, AccountView, "relationship.json", %{user: follower, target: followed} "object" => follow_activity.data["id"]
}) do
render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
end end
end end
def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
with %User{} = blocked <- Repo.get(User, id), with %User{} = blocked <- Repo.get(User, id),
{:ok, blocker} <- User.block(blocker, blocked) do {:ok, blocker} <- User.block(blocker, blocked) do
render conn, AccountView, "relationship.json", %{user: blocker, target: blocked} render(conn, AccountView, "relationship.json", %{user: blocker, target: blocked})
else else
{:error, message} -> {:error, message} ->
conn conn
@ -426,7 +469,7 @@ def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
with %User{} = blocked <- Repo.get(User, id), with %User{} = blocked <- Repo.get(User, id),
{:ok, blocker} <- User.unblock(blocker, blocked) do {:ok, blocker} <- User.unblock(blocker, blocked) do
render conn, AccountView, "relationship.json", %{user: blocker, target: blocked} render(conn, AccountView, "relationship.json", %{user: blocker, target: blocked})
else else
{:error, message} -> {:error, message} ->
conn conn
@ -438,7 +481,7 @@ def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
# TODO: Use proper query # TODO: Use proper query
def blocks(%{assigns: %{user: user}} = conn, _) do def blocks(%{assigns: %{user: user}} = conn, _) do
with blocked_users <- user.info["blocks"] || [], with blocked_users <- user.info["blocks"] || [],
accounts <- Enum.map(blocked_users, fn (ap_id) -> User.get_cached_by_ap_id(ap_id) end) do accounts <- Enum.map(blocked_users, fn ap_id -> User.get_cached_by_ap_id(ap_id) end) do
res = AccountView.render("accounts.json", users: accounts, for: user, as: :user) res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
json(conn, res) json(conn, res)
end end
@ -447,23 +490,34 @@ def blocks(%{assigns: %{user: user}} = conn, _) do
def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
accounts = User.search(query, params["resolve"] == "true") accounts = User.search(query, params["resolve"] == "true")
fetched = if Regex.match?(~r/https?:/, query) do fetched =
with {:ok, activities} <- OStatus.fetch_activity_from_url(query) do if Regex.match?(~r/https?:/, query) do
activities with {:ok, activities} <- OStatus.fetch_activity_from_url(query) do
else activities
_e -> [] else
end _e -> []
end || [] end
end || []
q =
from(
a in Activity,
where: fragment("?->>'type' = 'Create'", a.data),
where:
fragment(
"to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
a.data,
^query
),
limit: 20
)
q = from a in Activity,
where: fragment("?->>'type' = 'Create'", a.data),
where: fragment("to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)", a.data, ^query),
limit: 20
statuses = Repo.all(q) ++ fetched statuses = Repo.all(q) ++ fetched
res = %{ res = %{
"accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user), "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
"statuses" => StatusView.render("index.json", activities: statuses, for: user, as: :activity), "statuses" =>
StatusView.render("index.json", activities: statuses, for: user, as: :activity),
"hashtags" => [] "hashtags" => []
} }
@ -479,94 +533,102 @@ def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) d
end end
def favourites(%{assigns: %{user: user}} = conn, _) do def favourites(%{assigns: %{user: user}} = conn, _) do
params = %{} params =
|> Map.put("type", "Create") %{}
|> Map.put("favorited_by", user.ap_id) |> Map.put("type", "Create")
|> Map.put("blocking_user", user) |> Map.put("favorited_by", user.ap_id)
|> Map.put("blocking_user", user)
activities = ActivityPub.fetch_public_activities(params) activities =
|> Enum.reverse ActivityPub.fetch_public_activities(params)
|> Enum.reverse()
conn conn
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity}) |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
end end
def index(%{assigns: %{user: user}} = conn, _params) do def index(%{assigns: %{user: user}} = conn, _params) do
token = conn token =
|> get_session(:oauth_token) conn
|> get_session(:oauth_token)
if user && token do if user && token do
mastodon_emoji = mastodonized_emoji() mastodon_emoji = mastodonized_emoji()
accounts = Map.put(%{}, user.id, AccountView.render("account.json", %{user: user})) accounts = Map.put(%{}, user.id, AccountView.render("account.json", %{user: user}))
initial_state = %{
meta: %{ initial_state =
streaming_api_base_url: String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"), %{
access_token: token, meta: %{
locale: "en", streaming_api_base_url:
domain: Pleroma.Web.Endpoint.host(), String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
admin: "1", access_token: token,
me: "#{user.id}", locale: "en",
unfollow_modal: false, domain: Pleroma.Web.Endpoint.host(),
boost_modal: false, admin: "1",
delete_modal: true, me: "#{user.id}",
auto_play_gif: false, unfollow_modal: false,
reduce_motion: false boost_modal: false,
}, delete_modal: true,
compose: %{ auto_play_gif: false,
me: "#{user.id}", reduce_motion: false
default_privacy: "public", },
default_sensitive: false compose: %{
}, me: "#{user.id}",
media_attachments: %{ default_privacy: "public",
accept_content_types: [ default_sensitive: false
".jpg", },
".jpeg", media_attachments: %{
".png", accept_content_types: [
".gif", ".jpg",
".webm", ".jpeg",
".mp4", ".png",
".m4v", ".gif",
"image\/jpeg", ".webm",
"image\/png", ".mp4",
"image\/gif", ".m4v",
"video\/webm", "image\/jpeg",
"video\/mp4" "image\/png",
] "image\/gif",
}, "video\/webm",
settings: %{ "video\/mp4"
onboarded: true, ]
home: %{ },
shows: %{ settings: %{
reblog: true, onboarded: true,
reply: true home: %{
shows: %{
reblog: true,
reply: true
}
},
notifications: %{
alerts: %{
follow: true,
favourite: true,
reblog: true,
mention: true
},
shows: %{
follow: true,
favourite: true,
reblog: true,
mention: true
},
sounds: %{
follow: true,
favourite: true,
reblog: true,
mention: true
}
} }
}, },
notifications: %{ push_subscription: nil,
alerts: %{ accounts: accounts,
follow: true, custom_emojis: mastodon_emoji,
favourite: true, char_limit: Keyword.get(@instance, :limit)
reblog: true, }
mention: true |> Jason.encode!()
},
shows: %{
follow: true,
favourite: true,
reblog: true,
mention: true
},
sounds: %{
follow: true,
favourite: true,
reblog: true,
mention: true
}
}
},
push_subscription: nil,
accounts: accounts,
custom_emojis: mastodon_emoji,
char_limit: Keyword.get(@instance, :limit)
} |> Jason.encode!
conn conn
|> put_layout(false) |> put_layout(false)
|> render(MastodonView, "index.html", %{initial_state: initial_state}) |> render(MastodonView, "index.html", %{initial_state: initial_state})
@ -586,12 +648,18 @@ defp get_or_make_app() do
{:ok, app} {:ok, app}
else else
_e -> _e ->
cs = App.register_changeset(%App{}, %{client_name: "Mastodon-Local", redirect_uris: ".", scopes: "read,write,follow"}) cs =
App.register_changeset(%App{}, %{
client_name: "Mastodon-Local",
redirect_uris: ".",
scopes: "read,write,follow"
})
Repo.insert(cs) Repo.insert(cs)
end end
end end
def login_post(conn, %{"authorization" => %{ "name" => name, "password" => password}}) do def login_post(conn, %{"authorization" => %{"name" => name, "password" => password}}) do
with %User{} = user <- User.get_cached_by_nickname(name), with %User{} = user <- User.get_cached_by_nickname(name),
true <- Pbkdf2.checkpw(password, user.password_hash), true <- Pbkdf2.checkpw(password, user.password_hash),
{:ok, app} <- get_or_make_app(), {:ok, app} <- get_or_make_app(),
@ -615,8 +683,9 @@ def logout(conn, _) do
def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
Logger.debug("Unimplemented, returning unmodified relationship") Logger.debug("Unimplemented, returning unmodified relationship")
with %User{} = target <- Repo.get(User, id) do with %User{} = target <- Repo.get(User, id) do
render conn, AccountView, "relationship.json", %{user: user, target: target} render(conn, AccountView, "relationship.json", %{user: user, target: target})
end end
end end
@ -632,20 +701,53 @@ def empty_object(conn, _) do
def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
actor = User.get_cached_by_ap_id(activity.data["actor"]) actor = User.get_cached_by_ap_id(activity.data["actor"])
created_at = NaiveDateTime.to_iso8601(created_at)
|> String.replace(~r/(\.\d+)?$/, ".000Z", global: false) created_at =
NaiveDateTime.to_iso8601(created_at)
|> String.replace(~r/(\.\d+)?$/, ".000Z", global: false)
case activity.data["type"] do case activity.data["type"] do
"Create" -> "Create" ->
%{id: id, type: "mention", created_at: created_at, account: AccountView.render("account.json", %{user: actor}), status: StatusView.render("status.json", %{activity: activity, for: user})} %{
id: id,
type: "mention",
created_at: created_at,
account: AccountView.render("account.json", %{user: actor}),
status: StatusView.render("status.json", %{activity: activity, for: user})
}
"Like" -> "Like" ->
liked_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"]) liked_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
%{id: id, type: "favourite", created_at: created_at, account: AccountView.render("account.json", %{user: actor}), status: StatusView.render("status.json", %{activity: liked_activity, for: user})}
%{
id: id,
type: "favourite",
created_at: created_at,
account: AccountView.render("account.json", %{user: actor}),
status: StatusView.render("status.json", %{activity: liked_activity, for: user})
}
"Announce" -> "Announce" ->
announced_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"]) announced_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
%{id: id, type: "reblog", created_at: created_at, account: AccountView.render("account.json", %{user: actor}), status: StatusView.render("status.json", %{activity: announced_activity, for: user})}
%{
id: id,
type: "reblog",
created_at: created_at,
account: AccountView.render("account.json", %{user: actor}),
status: StatusView.render("status.json", %{activity: announced_activity, for: user})
}
"Follow" -> "Follow" ->
%{id: id, type: "follow", created_at: created_at, account: AccountView.render("account.json", %{user: actor})} %{
_ -> nil id: id,
type: "follow",
created_at: created_at,
account: AccountView.render("account.json", %{user: actor})
}
_ ->
nil
end end
end end
end end

View file

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

View file

@ -10,37 +10,52 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
defp get_replied_to_activities(activities) do defp get_replied_to_activities(activities) do
activities activities
|> Enum.map(fn |> Enum.map(fn
(%{data: %{"type" => "Create", "object" => %{"inReplyTo" => inReplyTo}}}) -> %{data: %{"type" => "Create", "object" => %{"inReplyTo" => inReplyTo}}} ->
(inReplyTo != "") && inReplyTo inReplyTo != "" && inReplyTo
_ -> nil
_ ->
nil
end) end)
|> Enum.filter(&(&1)) |> Enum.filter(& &1)
|> Activity.create_activity_by_object_id_query() |> Activity.create_activity_by_object_id_query()
|> Repo.all |> Repo.all()
|> Enum.reduce(%{}, fn(activity, acc) -> Map.put(acc,activity.data["object"]["id"], activity) end) |> Enum.reduce(%{}, fn activity, acc ->
Map.put(acc, activity.data["object"]["id"], activity)
end)
end end
def render("index.json", opts) do def render("index.json", opts) do
replied_to_activities = get_replied_to_activities(opts.activities) replied_to_activities = get_replied_to_activities(opts.activities)
render_many(opts.activities, StatusView, "status.json", Map.put(opts, :replied_to_activities, replied_to_activities))
render_many(
opts.activities,
StatusView,
"status.json",
Map.put(opts, :replied_to_activities, replied_to_activities)
)
end end
def render("status.json", %{activity: %{data: %{"type" => "Announce", "object" => object}} = activity} = opts) do def render(
"status.json",
%{activity: %{data: %{"type" => "Announce", "object" => object}} = activity} = opts
) do
user = User.get_cached_by_ap_id(activity.data["actor"]) user = User.get_cached_by_ap_id(activity.data["actor"])
created_at = Utils.to_masto_date(activity.data["published"]) created_at = Utils.to_masto_date(activity.data["published"])
reblogged = Activity.get_create_activity_by_object_ap_id(object) reblogged = Activity.get_create_activity_by_object_ap_id(object)
reblogged = render("status.json", Map.put(opts, :activity, reblogged)) reblogged = render("status.json", Map.put(opts, :activity, reblogged))
mentions = activity.recipients mentions =
|> Enum.map(fn (ap_id) -> User.get_cached_by_ap_id(ap_id) end) activity.recipients
|> Enum.filter(&(&1)) |> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
|> Enum.map(fn (user) -> AccountView.render("mention.json", %{user: user}) end) |> Enum.filter(& &1)
|> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
%{ %{
id: to_string(activity.id), id: to_string(activity.id),
uri: object, uri: object,
url: nil, # TODO: This might be wrong, check with mastodon. # TODO: This might be wrong, check with mastodon.
url: nil,
account: AccountView.render("account.json", %{user: user}), account: AccountView.render("account.json", %{user: user}),
in_reply_to_id: nil, in_reply_to_id: nil,
in_reply_to_account_id: nil, in_reply_to_account_id: nil,
@ -89,27 +104,30 @@ def render("status.json", %{activity: %{data: %{"object" => object}} = activity}
tags = object["tag"] || [] tags = object["tag"] || []
sensitive = object["sensitive"] || Enum.member?(tags, "nsfw") sensitive = object["sensitive"] || Enum.member?(tags, "nsfw")
mentions = activity.recipients mentions =
|> Enum.map(fn (ap_id) -> User.get_cached_by_ap_id(ap_id) end) activity.recipients
|> Enum.filter(&(&1)) |> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
|> Enum.map(fn (user) -> AccountView.render("mention.json", %{user: user}) end) |> Enum.filter(& &1)
|> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end)
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 = render_many(object["attachment"] || [], StatusView, "attachment.json", as: :attachment) attachments =
render_many(object["attachment"] || [], StatusView, "attachment.json", as: :attachment)
created_at = Utils.to_masto_date(object["published"]) created_at = Utils.to_masto_date(object["published"])
reply_to = get_reply_to(activity, opts) reply_to = get_reply_to(activity, opts)
reply_to_user = reply_to && User.get_cached_by_ap_id(reply_to.data["actor"]) reply_to_user = reply_to && User.get_cached_by_ap_id(reply_to.data["actor"])
emojis = (activity.data["object"]["emoji"] || []) emojis =
|> Enum.map(fn {name, url} -> (activity.data["object"]["emoji"] || [])
name = HtmlSanitizeEx.strip_tags(name) |> Enum.map(fn {name, url} ->
url = HtmlSanitizeEx.strip_tags(url) name = HtmlSanitizeEx.strip_tags(name)
%{ shortcode: name, url: url, static_url: url } url = HtmlSanitizeEx.strip_tags(url)
end) %{shortcode: name, url: url, static_url: url}
end)
%{ %{
id: to_string(activity.id), id: to_string(activity.id),
@ -131,7 +149,8 @@ def render("status.json", %{activity: %{data: %{"object" => object}} = activity}
visibility: get_visibility(object), visibility: get_visibility(object),
media_attachments: attachments |> Enum.take(4), media_attachments: attachments |> Enum.take(4),
mentions: mentions, mentions: mentions,
tags: [], # fix, # fix,
tags: [],
application: %{ application: %{
name: "Web", name: "Web",
website: nil website: nil
@ -145,10 +164,11 @@ def get_visibility(object) do
public = "https://www.w3.org/ns/activitystreams#Public" public = "https://www.w3.org/ns/activitystreams#Public"
to = object["to"] || [] to = object["to"] || []
cc = object["cc"] || [] cc = object["cc"] || []
cond do cond do
public in to -> "public" public in to -> "public"
public in cc -> "unlisted" public in cc -> "unlisted"
Enum.any?(to, &(String.contains?(&1, "/followers"))) -> "private" Enum.any?(to, &String.contains?(&1, "/followers")) -> "private"
true -> "direct" true -> "direct"
end end
end end
@ -156,14 +176,15 @@ def get_visibility(object) do
def render("attachment.json", %{attachment: attachment}) do def render("attachment.json", %{attachment: attachment}) do
[%{"mediaType" => media_type, "href" => href} | _] = attachment["url"] [%{"mediaType" => media_type, "href" => href} | _] = attachment["url"]
type = cond do type =
String.contains?(media_type, "image") -> "image" cond do
String.contains?(media_type, "video") -> "video" String.contains?(media_type, "image") -> "image"
String.contains?(media_type, "audio") -> "audio" String.contains?(media_type, "video") -> "video"
true -> "unknown" String.contains?(media_type, "audio") -> "audio"
end true -> "unknown"
end
<< hash_id::signed-32, _rest::binary >> = :crypto.hash(:md5, href) <<hash_id::signed-32, _rest::binary>> = :crypto.hash(:md5, href)
%{ %{
id: to_string(attachment["id"] || hash_id), id: to_string(attachment["id"] || hash_id),

View file

@ -4,47 +4,59 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do
@httpoison Application.get_env(:pleroma, :httpoison) @httpoison Application.get_env(:pleroma, :httpoison)
@max_body_length 25 * 1048576 @max_body_length 25 * 1_048_576
@cache_control %{ @cache_control %{
default: "public, max-age=1209600", default: "public, max-age=1209600",
error: "public, must-revalidate, max-age=160", error: "public, must-revalidate, max-age=160"
} }
def remote(conn, %{"sig" => sig, "url" => url}) do def remote(conn, %{"sig" => sig, "url" => url}) do
config = Application.get_env(:pleroma, :media_proxy, []) config = Application.get_env(:pleroma, :media_proxy, [])
with \
true <- Keyword.get(config, :enabled, false), with true <- Keyword.get(config, :enabled, false),
{:ok, url} <- Pleroma.Web.MediaProxy.decode_url(sig, url), {:ok, url} <- Pleroma.Web.MediaProxy.decode_url(sig, url),
{:ok, content_type, body} <- proxy_request(url) {:ok, content_type, body} <- proxy_request(url) do
do
conn conn
|> put_resp_content_type(content_type) |> put_resp_content_type(content_type)
|> set_cache_header(:default) |> set_cache_header(:default)
|> send_resp(200, body) |> send_resp(200, body)
else else
false -> send_error(conn, 404) false ->
{:error, :invalid_signature} -> send_error(conn, 403) send_error(conn, 404)
{:error, {:http, _, url}} -> redirect_or_error(conn, url, Keyword.get(config, :redirect_on_failure, true))
{:error, :invalid_signature} ->
send_error(conn, 403)
{:error, {:http, _, url}} ->
redirect_or_error(conn, url, Keyword.get(config, :redirect_on_failure, true))
end end
end end
defp proxy_request(link) do defp proxy_request(link) do
headers = [{"user-agent", "Pleroma/MediaProxy; #{Pleroma.Web.base_url()} <#{Application.get_env(:pleroma, :instance)[:email]}>"}] headers = [
options = @httpoison.process_request_options([:insecure, {:follow_redirect, true}]) ++ [{:pool, :default}] {"user-agent",
with \ "Pleroma/MediaProxy; #{Pleroma.Web.base_url()} <#{
{:ok, 200, headers, client} <- :hackney.request(:get, link, headers, "", options), Application.get_env(:pleroma, :instance)[:email]
headers = Enum.into(headers, Map.new), }>"}
{:ok, body} <- proxy_request_body(client), ]
content_type <- proxy_request_content_type(headers, body)
do options =
@httpoison.process_request_options([:insecure, {:follow_redirect, true}]) ++
[{:pool, :default}]
with {:ok, 200, headers, client} <- :hackney.request(:get, link, headers, "", options),
headers = Enum.into(headers, Map.new()),
{:ok, body} <- proxy_request_body(client),
content_type <- proxy_request_content_type(headers, body) do
{:ok, content_type, body} {:ok, content_type, body}
else else
{:ok, status, _, _} -> {:ok, status, _, _} ->
Logger.warn "MediaProxy: request failed, status #{status}, link: #{link}" Logger.warn("MediaProxy: request failed, status #{status}, link: #{link}")
{:error, {:http, :bad_status, link}} {:error, {:http, :bad_status, link}}
{:error, error} -> {:error, error} ->
Logger.warn "MediaProxy: request failed, error #{inspect error}, link: #{link}" Logger.warn("MediaProxy: request failed, error #{inspect(error)}, link: #{link}")
{:error, {:http, error, link}} {:error, {:http, error, link}}
end end
end end
@ -63,13 +75,15 @@ defp send_error(conn, code, body \\ "") do
end end
defp proxy_request_body(client), do: proxy_request_body(client, <<>>) defp proxy_request_body(client), do: proxy_request_body(client, <<>>)
defp proxy_request_body(client, body) when byte_size(body) < @max_body_length do defp proxy_request_body(client, body) when byte_size(body) < @max_body_length do
case :hackney.stream_body(client) do case :hackney.stream_body(client) do
{:ok, data} -> proxy_request_body(client, <<body :: binary, data :: binary>>) {:ok, data} -> proxy_request_body(client, <<body::binary, data::binary>>)
:done -> {:ok, body} :done -> {:ok, body}
{:error, reason} -> {:error, reason} {:error, reason} -> {:error, reason}
end end
end end
defp proxy_request_body(client, _) do defp proxy_request_body(client, _) do
:hackney.close(client) :hackney.close(client)
{:error, :body_too_large} {:error, :body_too_large}
@ -80,5 +94,4 @@ defp proxy_request_body(client, _) do
defp proxy_request_content_type(headers, _body) do defp proxy_request_content_type(headers, _body) do
headers["Content-Type"] || headers["content-type"] || "image/jpeg" headers["Content-Type"] || headers["content-type"] || "image/jpeg"
end end
end end

View file

@ -7,14 +7,15 @@ def url(url = "/" <> _), do: url
def url(url) do def url(url) do
config = Application.get_env(:pleroma, :media_proxy, []) config = Application.get_env(:pleroma, :media_proxy, [])
if !Keyword.get(config, :enabled, false) or String.starts_with?(url, Pleroma.Web.base_url) do
if !Keyword.get(config, :enabled, false) or String.starts_with?(url, Pleroma.Web.base_url()) do
url url
else else
secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base] secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base]
base64 = Base.url_encode64(url, @base64_opts) base64 = Base.url_encode64(url, @base64_opts)
sig = :crypto.hmac(:sha, secret, base64) sig = :crypto.hmac(:sha, secret, base64)
sig64 = sig |> Base.url_encode64(@base64_opts) sig64 = sig |> Base.url_encode64(@base64_opts)
Keyword.get(config, :base_url, Pleroma.Web.base_url) <> "/proxy/#{sig64}/#{base64}" Keyword.get(config, :base_url, Pleroma.Web.base_url()) <> "/proxy/#{sig64}/#{base64}"
end end
end end
@ -22,11 +23,11 @@ def decode_url(sig, url) do
secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base] secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base]
sig = Base.url_decode64!(sig, @base64_opts) sig = Base.url_decode64!(sig, @base64_opts)
local_sig = :crypto.hmac(:sha, secret, url) local_sig = :crypto.hmac(:sha, secret, url)
if local_sig == sig do if local_sig == sig do
{:ok, Base.url_decode64!(url, @base64_opts)} {:ok, Base.url_decode64!(url, @base64_opts)}
else else
{:error, :invalid_signature} {:error, :invalid_signature}
end end
end end
end end

View file

@ -3,25 +3,26 @@ defmodule Pleroma.Web.OAuth.App do
import Ecto.{Changeset} import Ecto.{Changeset}
schema "apps" do schema "apps" do
field :client_name, :string field(:client_name, :string)
field :redirect_uris, :string field(:redirect_uris, :string)
field :scopes, :string field(:scopes, :string)
field :website, :string field(:website, :string)
field :client_id, :string field(:client_id, :string)
field :client_secret, :string field(:client_secret, :string)
timestamps() timestamps()
end end
def register_changeset(struct, params \\ %{}) do def register_changeset(struct, params \\ %{}) do
changeset = struct changeset =
|> cast(params, [:client_name, :redirect_uris, :scopes, :website]) struct
|> validate_required([:client_name, :redirect_uris, :scopes]) |> cast(params, [:client_name, :redirect_uris, :scopes, :website])
|> validate_required([:client_name, :redirect_uris, :scopes])
if changeset.valid? do if changeset.valid? do
changeset changeset
|> put_change(:client_id, :crypto.strong_rand_bytes(32) |> Base.url_encode64) |> put_change(:client_id, :crypto.strong_rand_bytes(32) |> Base.url_encode64())
|> put_change(:client_secret, :crypto.strong_rand_bytes(32) |> Base.url_encode64) |> put_change(:client_secret, :crypto.strong_rand_bytes(32) |> Base.url_encode64())
else else
changeset changeset
end end

View file

@ -7,24 +7,24 @@ defmodule Pleroma.Web.OAuth.Authorization do
import Ecto.{Changeset} import Ecto.{Changeset}
schema "oauth_authorizations" do schema "oauth_authorizations" do
field :token, :string field(:token, :string)
field :valid_until, :naive_datetime field(:valid_until, :naive_datetime)
field :used, :boolean, default: false field(:used, :boolean, default: false)
belongs_to :user, Pleroma.User belongs_to(:user, Pleroma.User)
belongs_to :app, Pleroma.App belongs_to(:app, Pleroma.App)
timestamps() timestamps()
end end
def create_authorization(%App{} = app, %User{} = user) do def create_authorization(%App{} = app, %User{} = user) do
token = :crypto.strong_rand_bytes(32) |> Base.url_encode64 token = :crypto.strong_rand_bytes(32) |> Base.url_encode64()
authorization = %Authorization{ authorization = %Authorization{
token: token, token: token,
used: false, used: false,
user_id: user.id, user_id: user.id,
app_id: app.id, app_id: app.id,
valid_until: NaiveDateTime.add(NaiveDateTime.utc_now, 60 * 10) valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10)
} }
Repo.insert(authorization) Repo.insert(authorization)
@ -37,11 +37,12 @@ def use_changeset(%Authorization{} = auth, params) do
end end
def use_token(%Authorization{used: false, valid_until: valid_until} = auth) do def use_token(%Authorization{used: false, valid_until: valid_until} = auth) do
if NaiveDateTime.diff(NaiveDateTime.utc_now, valid_until) < 0 do if NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) < 0 do
Repo.update(use_changeset(auth, %{used: true})) Repo.update(use_changeset(auth, %{used: true}))
else else
{:error, "token expired"} {:error, "token expired"}
end end
end end
def use_token(%Authorization{used: true}), do: {:error, "already used"} def use_token(%Authorization{used: true}), do: {:error, "already used"}
end end

View file

@ -1,12 +1,11 @@
defmodule Pleroma.Web.OAuth.FallbackController do defmodule Pleroma.Web.OAuth.FallbackController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
alias Pleroma.Web.OAuth.OAuthController alias Pleroma.Web.OAuth.OAuthController
# No user/password # No user/password
def call(conn, _) do def call(conn, _) do
conn conn
|> put_flash(:error, "Invalid Username/Password") |> put_flash(:error, "Invalid Username/Password")
|> OAuthController.authorize(conn.params) |> OAuthController.authorize(conn.params)
end end
end
end

View file

@ -5,38 +5,49 @@ defmodule Pleroma.Web.OAuth.OAuthController do
alias Pleroma.{Repo, User} alias Pleroma.{Repo, User}
alias Comeonin.Pbkdf2 alias Comeonin.Pbkdf2
plug :fetch_session plug(:fetch_session)
plug :fetch_flash plug(:fetch_flash)
action_fallback Pleroma.Web.OAuth.FallbackController action_fallback(Pleroma.Web.OAuth.FallbackController)
def authorize(conn, params) do def authorize(conn, params) do
render conn, "show.html", %{ render(conn, "show.html", %{
response_type: params["response_type"], response_type: params["response_type"],
client_id: params["client_id"], client_id: params["client_id"],
scope: params["scope"], scope: params["scope"],
redirect_uri: params["redirect_uri"], redirect_uri: params["redirect_uri"],
state: params["state"] state: params["state"]
} })
end end
def create_authorization(conn, %{"authorization" => %{"name" => name, "password" => password, "client_id" => client_id, "redirect_uri" => redirect_uri} = params}) do def create_authorization(conn, %{
"authorization" =>
%{
"name" => name,
"password" => password,
"client_id" => client_id,
"redirect_uri" => redirect_uri
} = params
}) do
with %User{} = user <- User.get_cached_by_nickname(name), with %User{} = user <- User.get_cached_by_nickname(name),
true <- Pbkdf2.checkpw(password, user.password_hash), true <- Pbkdf2.checkpw(password, user.password_hash),
%App{} = app <- Repo.get_by(App, client_id: client_id), %App{} = app <- Repo.get_by(App, client_id: client_id),
{:ok, auth} <- Authorization.create_authorization(app, user) do {:ok, auth} <- Authorization.create_authorization(app, user) do
if redirect_uri == "urn:ietf:wg:oauth:2.0:oob" do if redirect_uri == "urn:ietf:wg:oauth:2.0:oob" do
render conn, "results.html", %{ render(conn, "results.html", %{
auth: auth auth: auth
} })
else else
connector = if String.contains?(redirect_uri, "?"), do: "&", else: "?" connector = if String.contains?(redirect_uri, "?"), do: "&", else: "?"
url = "#{redirect_uri}#{connector}code=#{auth.token}" url = "#{redirect_uri}#{connector}code=#{auth.token}"
url = if params["state"] do
url <> "&state=#{params["state"]}" url =
else if params["state"] do
url url <> "&state=#{params["state"]}"
end else
url
end
redirect(conn, external: url) redirect(conn, external: url)
end end
end end
@ -45,7 +56,12 @@ def create_authorization(conn, %{"authorization" => %{"name" => name, "password"
# TODO # TODO
# - proper scope handling # - proper scope handling
def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do
with %App{} = app <- Repo.get_by(App, client_id: params["client_id"], client_secret: params["client_secret"]), with %App{} = app <-
Repo.get_by(
App,
client_id: params["client_id"],
client_secret: params["client_secret"]
),
fixed_token = fix_padding(params["code"]), fixed_token = fix_padding(params["code"]),
%Authorization{} = auth <- Repo.get_by(Authorization, token: fixed_token, app_id: app.id), %Authorization{} = auth <- Repo.get_by(Authorization, token: fixed_token, app_id: app.id),
{:ok, token} <- Token.exchange_token(app, auth) do {:ok, token} <- Token.exchange_token(app, auth) do
@ -56,6 +72,7 @@ def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do
expires_in: 60 * 10, expires_in: 60 * 10,
scope: "read write follow" scope: "read write follow"
} }
json(conn, response) json(conn, response)
else else
_error -> json(conn, %{error: "Invalid credentials"}) _error -> json(conn, %{error: "Invalid credentials"})
@ -64,8 +81,16 @@ def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do
# TODO # TODO
# - investigate a way to verify the user wants to grant read/write/follow once scope handling is done # - investigate a way to verify the user wants to grant read/write/follow once scope handling is done
def token_exchange(conn, %{"grant_type" => "password", "name" => name, "password" => password} = params) do def token_exchange(
with %App{} = app <- Repo.get_by(App, client_id: params["client_id"], client_secret: params["client_secret"]), conn,
%{"grant_type" => "password", "name" => name, "password" => password} = params
) do
with %App{} = app <-
Repo.get_by(
App,
client_id: params["client_id"],
client_secret: params["client_secret"]
),
%User{} = user <- User.get_cached_by_nickname(name), %User{} = user <- User.get_cached_by_nickname(name),
true <- Pbkdf2.checkpw(password, user.password_hash), true <- Pbkdf2.checkpw(password, user.password_hash),
{:ok, auth} <- Authorization.create_authorization(app, user), {:ok, auth} <- Authorization.create_authorization(app, user),
@ -77,6 +102,7 @@ def token_exchange(conn, %{"grant_type" => "password", "name" => name, "password
expires_in: 60 * 10, expires_in: 60 * 10,
scope: "read write follow" scope: "read write follow"
} }
json(conn, response) json(conn, response)
else else
_error -> json(conn, %{error: "Invalid credentials"}) _error -> json(conn, %{error: "Invalid credentials"})
@ -86,6 +112,6 @@ def token_exchange(conn, %{"grant_type" => "password", "name" => name, "password
defp fix_padding(token) do defp fix_padding(token) do
token token
|> Base.url_decode64!(padding: false) |> Base.url_decode64!(padding: false)
|> Base.url_encode64 |> Base.url_encode64()
end end
end end

View file

@ -5,11 +5,11 @@ defmodule Pleroma.Web.OAuth.Token do
alias Pleroma.Web.OAuth.{Token, App, Authorization} alias Pleroma.Web.OAuth.{Token, App, Authorization}
schema "oauth_tokens" do schema "oauth_tokens" do
field :token, :string field(:token, :string)
field :refresh_token, :string field(:refresh_token, :string)
field :valid_until, :naive_datetime field(:valid_until, :naive_datetime)
belongs_to :user, Pleroma.User belongs_to(:user, Pleroma.User)
belongs_to :app, Pleroma.App belongs_to(:app, Pleroma.App)
timestamps() timestamps()
end end
@ -22,15 +22,15 @@ def exchange_token(app, auth) do
end end
def create_token(%App{} = app, %User{} = user) do def create_token(%App{} = app, %User{} = user) do
token = :crypto.strong_rand_bytes(32) |> Base.url_encode64 token = :crypto.strong_rand_bytes(32) |> Base.url_encode64()
refresh_token = :crypto.strong_rand_bytes(32) |> Base.url_encode64 refresh_token = :crypto.strong_rand_bytes(32) |> Base.url_encode64()
token = %Token{ token = %Token{
token: token, token: token,
refresh_token: refresh_token, refresh_token: refresh_token,
user_id: user.id, user_id: user.id,
app_id: app.id, app_id: app.id,
valid_until: NaiveDateTime.add(NaiveDateTime.utc_now, 60 * 10) valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10)
} }
Repo.insert(token) Repo.insert(token)

View file

@ -5,7 +5,7 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do
require Logger require Logger
defp get_href(id) do defp get_href(id) do
with %Object{data: %{ "external_url" => external_url } }<- Object.get_cached_by_ap_id(id) do with %Object{data: %{"external_url" => external_url}} <- Object.get_cached_by_ap_id(id) do
external_url external_url
else else
_e -> id _e -> id
@ -13,42 +13,60 @@ defp get_href(id) do
end end
defp get_in_reply_to(%{"object" => %{"inReplyTo" => in_reply_to}}) do defp get_in_reply_to(%{"object" => %{"inReplyTo" => in_reply_to}}) do
[{:"thr:in-reply-to", [ref: to_charlist(in_reply_to), href: to_charlist(get_href(in_reply_to))], []}] [
{:"thr:in-reply-to",
[ref: to_charlist(in_reply_to), href: to_charlist(get_href(in_reply_to))], []}
]
end end
defp get_in_reply_to(_), do: [] defp get_in_reply_to(_), do: []
defp get_mentions(to) do defp get_mentions(to) do
Enum.map(to, fn (id) -> Enum.map(to, fn id ->
cond do cond do
# Special handling for the AP/Ostatus public collections # Special handling for the AP/Ostatus public collections
"https://www.w3.org/ns/activitystreams#Public" == id -> "https://www.w3.org/ns/activitystreams#Public" == id ->
{:link, [rel: "mentioned", "ostatus:object-type": "http://activitystrea.ms/schema/1.0/collection", href: "http://activityschema.org/collection/public"], []} {:link,
[
rel: "mentioned",
"ostatus:object-type": "http://activitystrea.ms/schema/1.0/collection",
href: "http://activityschema.org/collection/public"
], []}
# Ostatus doesn't handle follower collections, ignore these. # Ostatus doesn't handle follower collections, ignore these.
Regex.match?(~r/^#{Pleroma.Web.base_url}.+followers$/, id) -> Regex.match?(~r/^#{Pleroma.Web.base_url()}.+followers$/, id) ->
[] []
true -> true ->
{:link, [rel: "mentioned", "ostatus:object-type": "http://activitystrea.ms/schema/1.0/person", href: id], []} {:link,
[
rel: "mentioned",
"ostatus:object-type": "http://activitystrea.ms/schema/1.0/person",
href: id
], []}
end end
end) end)
end end
defp get_links(%{local: true, data: data}) do defp get_links(%{local: true, data: data}) do
h = fn(str) -> [to_charlist(str)] end h = fn str -> [to_charlist(str)] end
[ [
{:link, [type: ['application/atom+xml'], href: h.(data["object"]["id"]), rel: 'self'], []}, {:link, [type: ['application/atom+xml'], href: h.(data["object"]["id"]), rel: 'self'], []},
{:link, [type: ['text/html'], href: h.(data["object"]["id"]), rel: 'alternate'], []} {:link, [type: ['text/html'], href: h.(data["object"]["id"]), rel: 'alternate'], []}
] ]
end end
defp get_links(%{local: false, defp get_links(%{
data: %{ local: false,
"object" => %{ data: %{
"external_url" => external_url "object" => %{
} "external_url" => external_url
}}) do }
}
}) do
h = fn str -> [to_charlist(str)] end
h = fn(str) -> [to_charlist(str)] end
[ [
{:link, [type: ['text/html'], href: h.(external_url), rel: 'alternate'], []} {:link, [type: ['text/html'], href: h.(external_url), rel: 'alternate'], []}
] ]
@ -57,60 +75,72 @@ defp get_links(%{local: false,
defp get_links(_activity), do: [] defp get_links(_activity), do: []
defp get_emoji_links(emojis) do defp get_emoji_links(emojis) do
Enum.map(emojis, fn({emoji, file}) -> Enum.map(emojis, fn {emoji, file} ->
{:link, [name: to_charlist(emoji), rel: 'emoji', href: to_charlist(file)], []} {:link, [name: to_charlist(emoji), rel: 'emoji', href: to_charlist(file)], []}
end) end)
end end
def to_simple_form(activity, user, with_author \\ false) def to_simple_form(activity, user, with_author \\ false)
def to_simple_form(%{data: %{"object" => %{"type" => "Note"}}} = activity, user, with_author) do def to_simple_form(%{data: %{"object" => %{"type" => "Note"}}} = activity, user, with_author) do
h = fn(str) -> [to_charlist(str)] end h = fn str -> [to_charlist(str)] end
updated_at = activity.data["object"]["published"] updated_at = activity.data["object"]["published"]
inserted_at = activity.data["object"]["published"] inserted_at = activity.data["object"]["published"]
attachments = Enum.map(activity.data["object"]["attachment"] || [], fn(attachment) -> attachments =
url = hd(attachment["url"]) Enum.map(activity.data["object"]["attachment"] || [], fn attachment ->
{:link, [rel: 'enclosure', href: to_charlist(url["href"]), type: to_charlist(url["mediaType"])], []} url = hd(attachment["url"])
end)
{:link,
[rel: 'enclosure', href: to_charlist(url["href"]), type: to_charlist(url["mediaType"])],
[]}
end)
in_reply_to = get_in_reply_to(activity.data) in_reply_to = get_in_reply_to(activity.data)
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: [] author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
mentions = activity.recipients |> get_mentions mentions = activity.recipients |> get_mentions
categories = (activity.data["object"]["tag"] || []) categories =
|> Enum.map(fn (tag) -> (activity.data["object"]["tag"] || [])
if is_binary(tag) do |> Enum.map(fn tag ->
{:category, [term: to_charlist(tag)], []} if is_binary(tag) do
else {:category, [term: to_charlist(tag)], []}
nil else
end nil
end) end
|> Enum.filter(&(&1)) end)
|> Enum.filter(& &1)
emoji_links = get_emoji_links(activity.data["object"]["emoji"] || %{}) emoji_links = get_emoji_links(activity.data["object"]["emoji"] || %{})
summary = if activity.data["object"]["summary"] do summary =
[{:summary, [], h.(activity.data["object"]["summary"])}] if activity.data["object"]["summary"] do
else [{:summary, [], h.(activity.data["object"]["summary"])}]
[] else
end []
end
[ [
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/note']}, {:"activity:object-type", ['http://activitystrea.ms/schema/1.0/note']},
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/post']}, {:"activity:verb", ['http://activitystrea.ms/schema/1.0/post']},
{:id, h.(activity.data["object"]["id"])}, # For notes, federate the object id. # For notes, federate the object id.
{:id, h.(activity.data["object"]["id"])},
{:title, ['New note by #{user.nickname}']}, {:title, ['New note by #{user.nickname}']},
{:content, [type: 'html'], h.(activity.data["object"]["content"] |> String.replace(~r/[\n\r]/, ""))}, {:content, [type: 'html'],
h.(activity.data["object"]["content"] |> String.replace(~r/[\n\r]/, ""))},
{:published, h.(inserted_at)}, {:published, h.(inserted_at)},
{:updated, h.(updated_at)}, {:updated, h.(updated_at)},
{:"ostatus:conversation", [ref: h.(activity.data["context"])], h.(activity.data["context"])}, {:"ostatus:conversation", [ref: h.(activity.data["context"])], h.(activity.data["context"])},
{:link, [ref: h.(activity.data["context"]), rel: 'ostatus:conversation'], []}, {:link, [ref: h.(activity.data["context"]), rel: 'ostatus:conversation'], []}
] ++ summary ++ get_links(activity) ++ categories ++ attachments ++ in_reply_to ++ author ++ mentions ++ emoji_links ] ++
summary ++
get_links(activity) ++
categories ++ attachments ++ in_reply_to ++ author ++ mentions ++ emoji_links
end end
def to_simple_form(%{data: %{"type" => "Like"}} = activity, user, with_author) do def to_simple_form(%{data: %{"type" => "Like"}} = activity, user, with_author) do
h = fn(str) -> [to_charlist(str)] end h = fn str -> [to_charlist(str)] end
updated_at = activity.data["published"] updated_at = activity.data["published"]
inserted_at = activity.data["published"] inserted_at = activity.data["published"]
@ -126,10 +156,12 @@ def to_simple_form(%{data: %{"type" => "Like"}} = activity, user, with_author) d
{:content, [type: 'html'], ['#{user.nickname} favorited something']}, {:content, [type: 'html'], ['#{user.nickname} favorited something']},
{:published, h.(inserted_at)}, {:published, h.(inserted_at)},
{:updated, h.(updated_at)}, {:updated, h.(updated_at)},
{:"activity:object", [ {:"activity:object",
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/note']}, [
{:id, h.(activity.data["object"])}, # For notes, federate the object id. {:"activity:object-type", ['http://activitystrea.ms/schema/1.0/note']},
]}, # For notes, federate the object id.
{:id, h.(activity.data["object"])}
]},
{:"ostatus:conversation", [ref: h.(activity.data["context"])], h.(activity.data["context"])}, {:"ostatus:conversation", [ref: h.(activity.data["context"])], h.(activity.data["context"])},
{:link, [ref: h.(activity.data["context"]), rel: 'ostatus:conversation'], []}, {:link, [ref: h.(activity.data["context"]), rel: 'ostatus:conversation'], []},
{:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []}, {:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []},
@ -138,7 +170,7 @@ def to_simple_form(%{data: %{"type" => "Like"}} = activity, user, with_author) d
end end
def to_simple_form(%{data: %{"type" => "Announce"}} = activity, user, with_author) do def to_simple_form(%{data: %{"type" => "Announce"}} = activity, user, with_author) do
h = fn(str) -> [to_charlist(str)] end h = fn str -> [to_charlist(str)] end
updated_at = activity.data["published"] updated_at = activity.data["published"]
inserted_at = activity.data["published"] inserted_at = activity.data["published"]
@ -152,6 +184,7 @@ def to_simple_form(%{data: %{"type" => "Announce"}} = activity, user, with_autho
retweeted_xml = to_simple_form(retweeted_activity, retweeted_user, true) retweeted_xml = to_simple_form(retweeted_activity, retweeted_user, true)
mentions = activity.recipients |> get_mentions mentions = activity.recipients |> get_mentions
[ [
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']}, {:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']},
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/share']}, {:"activity:verb", ['http://activitystrea.ms/schema/1.0/share']},
@ -168,7 +201,7 @@ def to_simple_form(%{data: %{"type" => "Announce"}} = activity, user, with_autho
end end
def to_simple_form(%{data: %{"type" => "Follow"}} = activity, user, with_author) do def to_simple_form(%{data: %{"type" => "Follow"}} = activity, user, with_author) do
h = fn(str) -> [to_charlist(str)] end h = fn str -> [to_charlist(str)] end
updated_at = activity.data["published"] updated_at = activity.data["published"]
inserted_at = activity.data["published"] inserted_at = activity.data["published"]
@ -176,26 +209,29 @@ def to_simple_form(%{data: %{"type" => "Follow"}} = activity, user, with_author)
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: [] author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
mentions = (activity.recipients || []) |> get_mentions mentions = (activity.recipients || []) |> get_mentions
[ [
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']}, {:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']},
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/follow']}, {:"activity:verb", ['http://activitystrea.ms/schema/1.0/follow']},
{:id, h.(activity.data["id"])}, {:id, h.(activity.data["id"])},
{:title, ['#{user.nickname} started following #{activity.data["object"]}']}, {:title, ['#{user.nickname} started following #{activity.data["object"]}']},
{:content, [type: 'html'], ['#{user.nickname} started following #{activity.data["object"]}']}, {:content, [type: 'html'],
['#{user.nickname} started following #{activity.data["object"]}']},
{:published, h.(inserted_at)}, {:published, h.(inserted_at)},
{:updated, h.(updated_at)}, {:updated, h.(updated_at)},
{:"activity:object", [ {:"activity:object",
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/person']}, [
{:id, h.(activity.data["object"])}, {:"activity:object-type", ['http://activitystrea.ms/schema/1.0/person']},
{:uri, h.(activity.data["object"])}, {:id, h.(activity.data["object"])},
]}, {:uri, h.(activity.data["object"])}
{:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []}, ]},
{:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []}
] ++ mentions ++ author ] ++ mentions ++ author
end end
# Only undos of follow for now. Will need to get redone once there are more # Only undos of follow for now. Will need to get redone once there are more
def to_simple_form(%{data: %{"type" => "Undo"}} = activity, user, with_author) do def to_simple_form(%{data: %{"type" => "Undo"}} = activity, user, with_author) do
h = fn(str) -> [to_charlist(str)] end h = fn str -> [to_charlist(str)] end
updated_at = activity.data["published"] updated_at = activity.data["published"]
inserted_at = activity.data["published"] inserted_at = activity.data["published"]
@ -204,25 +240,28 @@ def to_simple_form(%{data: %{"type" => "Undo"}} = activity, user, with_author) d
follow_activity = Activity.get_by_ap_id(activity.data["object"]) follow_activity = Activity.get_by_ap_id(activity.data["object"])
mentions = (activity.recipients || []) |> get_mentions mentions = (activity.recipients || []) |> get_mentions
[ [
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']}, {:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']},
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/unfollow']}, {:"activity:verb", ['http://activitystrea.ms/schema/1.0/unfollow']},
{:id, h.(activity.data["id"])}, {:id, h.(activity.data["id"])},
{:title, ['#{user.nickname} stopped following #{follow_activity.data["object"]}']}, {:title, ['#{user.nickname} stopped following #{follow_activity.data["object"]}']},
{:content, [type: 'html'], ['#{user.nickname} stopped following #{follow_activity.data["object"]}']}, {:content, [type: 'html'],
['#{user.nickname} stopped following #{follow_activity.data["object"]}']},
{:published, h.(inserted_at)}, {:published, h.(inserted_at)},
{:updated, h.(updated_at)}, {:updated, h.(updated_at)},
{:"activity:object", [ {:"activity:object",
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/person']}, [
{:id, h.(follow_activity.data["object"])}, {:"activity:object-type", ['http://activitystrea.ms/schema/1.0/person']},
{:uri, h.(follow_activity.data["object"])}, {:id, h.(follow_activity.data["object"])},
]}, {:uri, h.(follow_activity.data["object"])}
{:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []}, ]},
{:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []}
] ++ mentions ++ author ] ++ mentions ++ author
end end
def to_simple_form(%{data: %{"type" => "Delete"}} = activity, user, with_author) do def to_simple_form(%{data: %{"type" => "Delete"}} = activity, user, with_author) do
h = fn(str) -> [to_charlist(str)] end h = fn str -> [to_charlist(str)] end
updated_at = activity.data["published"] updated_at = activity.data["published"]
inserted_at = activity.data["published"] inserted_at = activity.data["published"]
@ -237,20 +276,24 @@ def to_simple_form(%{data: %{"type" => "Delete"}} = activity, user, with_author)
{:content, [type: 'html'], ['An object was deleted']}, {:content, [type: 'html'], ['An object was deleted']},
{:published, h.(inserted_at)}, {:published, h.(inserted_at)},
{:updated, h.(updated_at)} {:updated, h.(updated_at)}
] ++ author ] ++ author
end end
def to_simple_form(_, _, _), do: nil def to_simple_form(_, _, _), do: nil
def wrap_with_entry(simple_form) do def wrap_with_entry(simple_form) do
[{ [
:entry, [ {
xmlns: 'http://www.w3.org/2005/Atom', :entry,
"xmlns:thr": 'http://purl.org/syndication/thread/1.0', [
"xmlns:activity": 'http://activitystrea.ms/spec/1.0/', xmlns: 'http://www.w3.org/2005/Atom',
"xmlns:poco": 'http://portablecontacts.net/spec/1.0', "xmlns:thr": 'http://purl.org/syndication/thread/1.0',
"xmlns:ostatus": 'http://ostatus.org/schema/1.0' "xmlns:activity": 'http://activitystrea.ms/spec/1.0/',
], simple_form "xmlns:poco": 'http://portablecontacts.net/spec/1.0',
}] "xmlns:ostatus": 'http://ostatus.org/schema/1.0'
],
simple_form
}
]
end end
end end

View file

@ -5,44 +5,57 @@ defmodule Pleroma.Web.OStatus.FeedRepresenter do
alias Pleroma.Web.MediaProxy alias Pleroma.Web.MediaProxy
def to_simple_form(user, activities, _users) do def to_simple_form(user, activities, _users) do
most_recent_update = (List.first(activities) || user).updated_at most_recent_update =
|> NaiveDateTime.to_iso8601 (List.first(activities) || user).updated_at
|> NaiveDateTime.to_iso8601()
h = fn(str) -> [to_charlist(str)] end h = fn str -> [to_charlist(str)] end
last_activity = List.last(activities) last_activity = List.last(activities)
entries = activities entries =
|> Enum.map(fn(activity) -> activities
{:entry, ActivityRepresenter.to_simple_form(activity, user)} |> Enum.map(fn activity ->
end) {:entry, ActivityRepresenter.to_simple_form(activity, user)}
|> Enum.filter(fn ({_, form}) -> form end) end)
|> Enum.filter(fn {_, form} -> form end)
[{ [
:feed, [ {
xmlns: 'http://www.w3.org/2005/Atom', :feed,
"xmlns:thr": 'http://purl.org/syndication/thread/1.0', [
"xmlns:activity": 'http://activitystrea.ms/spec/1.0/', xmlns: 'http://www.w3.org/2005/Atom',
"xmlns:poco": 'http://portablecontacts.net/spec/1.0', "xmlns:thr": 'http://purl.org/syndication/thread/1.0',
"xmlns:ostatus": 'http://ostatus.org/schema/1.0' "xmlns:activity": 'http://activitystrea.ms/spec/1.0/',
], [ "xmlns:poco": 'http://portablecontacts.net/spec/1.0',
{:id, h.(OStatus.feed_path(user))}, "xmlns:ostatus": 'http://ostatus.org/schema/1.0'
{:title, ['#{user.nickname}\'s timeline']}, ],
{:updated, h.(most_recent_update)}, [
{:logo, [to_charlist(User.avatar_url(user) |> MediaProxy.url())]}, {:id, h.(OStatus.feed_path(user))},
{:link, [rel: 'hub', href: h.(OStatus.pubsub_path(user))], []}, {:title, ['#{user.nickname}\'s timeline']},
{:link, [rel: 'salmon', href: h.(OStatus.salmon_path(user))], []}, {:updated, h.(most_recent_update)},
{:link, [rel: 'self', href: h.(OStatus.feed_path(user)), type: 'application/atom+xml'], []}, {:logo, [to_charlist(User.avatar_url(user) |> MediaProxy.url())]},
{:author, UserRepresenter.to_simple_form(user)}, {:link, [rel: 'hub', href: h.(OStatus.pubsub_path(user))], []},
] ++ {:link, [rel: 'salmon', href: h.(OStatus.salmon_path(user))], []},
if last_activity do {:link, [rel: 'self', href: h.(OStatus.feed_path(user)), type: 'application/atom+xml'],
[{:link, [rel: 'next', []},
href: to_charlist(OStatus.feed_path(user)) ++ '?max_id=' ++ to_charlist(last_activity.id), {:author, UserRepresenter.to_simple_form(user)}
type: 'application/atom+xml'], []}] ] ++
else if last_activity do
[] [
end {:link,
++ entries [
}] rel: 'next',
href:
to_charlist(OStatus.feed_path(user)) ++
'?max_id=' ++ to_charlist(last_activity.id),
type: 'application/atom+xml'
], []}
]
else
[]
end ++ entries
}
]
end end
end end

View file

@ -6,7 +6,8 @@ defmodule Pleroma.Web.OStatus.FollowHandler do
def handle(entry, doc) do def handle(entry, doc) do
with {:ok, actor} <- OStatus.find_make_or_update_user(doc), with {:ok, actor} <- OStatus.find_make_or_update_user(doc),
id when not is_nil(id) <- XML.string_from_xpath("/entry/id", entry), id when not is_nil(id) <- XML.string_from_xpath("/entry/id", entry),
followed_uri when not is_nil(followed_uri) <- XML.string_from_xpath("/entry/activity:object/id", entry), followed_uri when not is_nil(followed_uri) <-
XML.string_from_xpath("/entry/activity:object/id", entry),
{:ok, followed} <- OStatus.find_or_make_user(followed_uri), {:ok, followed} <- OStatus.find_or_make_user(followed_uri),
{:ok, activity} <- ActivityPub.follow(actor, followed, id, false) do {:ok, activity} <- ActivityPub.follow(actor, followed, id, false) do
User.follow(actor, followed) User.follow(actor, followed)

View file

@ -13,49 +13,56 @@ defmodule Pleroma.Web.OStatus.NoteHandler do
3. A newly generated context id. 3. A newly generated context id.
""" """
def get_context(entry, inReplyTo) do def get_context(entry, inReplyTo) do
context = ( context =
XML.string_from_xpath("//ostatus:conversation[1]", entry) (XML.string_from_xpath("//ostatus:conversation[1]", entry) ||
|| XML.string_from_xpath("//ostatus:conversation[1]/@ref", entry) XML.string_from_xpath("//ostatus:conversation[1]/@ref", entry) || "")
|| "") |> String.trim |> String.trim()
with %{data: %{"context" => context}} <- Object.get_cached_by_ap_id(inReplyTo) do with %{data: %{"context" => context}} <- Object.get_cached_by_ap_id(inReplyTo) do
context context
else _e -> else
if String.length(context) > 0 do _e ->
context if String.length(context) > 0 do
else context
Utils.generate_context_id else
end Utils.generate_context_id()
end
end end
end end
def get_people_mentions(entry) do def get_people_mentions(entry) do
:xmerl_xpath.string('//link[@rel="mentioned" and @ostatus:object-type="http://activitystrea.ms/schema/1.0/person"]', entry) :xmerl_xpath.string(
|> Enum.map(fn(person) -> XML.string_from_xpath("@href", person) end) '//link[@rel="mentioned" and @ostatus:object-type="http://activitystrea.ms/schema/1.0/person"]',
entry
)
|> Enum.map(fn person -> XML.string_from_xpath("@href", person) end)
end end
def get_collection_mentions(entry) do def get_collection_mentions(entry) do
transmogrify = fn transmogrify = fn
("http://activityschema.org/collection/public") -> "http://activityschema.org/collection/public" ->
"https://www.w3.org/ns/activitystreams#Public" "https://www.w3.org/ns/activitystreams#Public"
(group) ->
group ->
group group
end end
:xmerl_xpath.string('//link[@rel="mentioned" and @ostatus:object-type="http://activitystrea.ms/schema/1.0/collection"]', entry) :xmerl_xpath.string(
|> Enum.map(fn(collection) -> XML.string_from_xpath("@href", collection) |> transmogrify.() end) '//link[@rel="mentioned" and @ostatus:object-type="http://activitystrea.ms/schema/1.0/collection"]',
entry
)
|> Enum.map(fn collection -> XML.string_from_xpath("@href", collection) |> transmogrify.() end)
end end
def get_mentions(entry) do def get_mentions(entry) do
(get_people_mentions(entry) (get_people_mentions(entry) ++ get_collection_mentions(entry))
++ get_collection_mentions(entry)) |> Enum.filter(& &1)
|> Enum.filter(&(&1))
end end
def get_emoji(entry) do def get_emoji(entry) do
try do try do
:xmerl_xpath.string('//link[@rel="emoji"]', entry) :xmerl_xpath.string('//link[@rel="emoji"]', entry)
|> Enum.reduce(%{}, fn(emoji, acc) -> |> Enum.reduce(%{}, fn emoji, acc ->
Map.put(acc, XML.string_from_xpath("@name", emoji), XML.string_from_xpath("@href", emoji)) Map.put(acc, XML.string_from_xpath("@name", emoji), XML.string_from_xpath("@href", emoji))
end) end)
rescue rescue
@ -79,7 +86,8 @@ def fetch_replied_to_activity(entry, inReplyTo) do
activity activity
else else
_e -> _e ->
with inReplyToHref when not is_nil(inReplyToHref) <- XML.string_from_xpath("//thr:in-reply-to[1]/@href", entry), with inReplyToHref when not is_nil(inReplyToHref) <-
XML.string_from_xpath("//thr:in-reply-to[1]/@href", entry),
{:ok, [activity | _]} <- OStatus.fetch_activity_from_url(inReplyToHref) do {:ok, [activity | _]} <- OStatus.fetch_activity_from_url(inReplyToHref) do
activity activity
else else
@ -107,16 +115,40 @@ def handle_note(entry, doc \\ nil) do
date <- XML.string_from_xpath("//published", entry), date <- XML.string_from_xpath("//published", entry),
unlisted <- XML.string_from_xpath("//mastodon:scope", entry) == "unlisted", unlisted <- XML.string_from_xpath("//mastodon:scope", entry) == "unlisted",
cc <- if(unlisted, do: ["https://www.w3.org/ns/activitystreams#Public"], else: []), cc <- if(unlisted, do: ["https://www.w3.org/ns/activitystreams#Public"], else: []),
note <- CommonAPI.Utils.make_note_data(actor.ap_id, to, context, content_html, attachments, inReplyToActivity, [], cw), note <-
CommonAPI.Utils.make_note_data(
actor.ap_id,
to,
context,
content_html,
attachments,
inReplyToActivity,
[],
cw
),
note <- note |> Map.put("id", id) |> Map.put("tag", tags), note <- note |> Map.put("id", id) |> Map.put("tag", tags),
note <- note |> Map.put("published", date), note <- note |> Map.put("published", date),
note <- note |> Map.put("emoji", get_emoji(entry)), note <- note |> Map.put("emoji", get_emoji(entry)),
note <- add_external_url(note, entry), note <- add_external_url(note, entry),
note <- note |> Map.put("cc", cc), note <- note |> Map.put("cc", cc),
# TODO: Handle this case in make_note_data # TODO: Handle this case in make_note_data
note <- (if inReplyTo && !inReplyToActivity, do: note |> Map.put("inReplyTo", inReplyTo), else: note) note <-
do if(
res = ActivityPub.create(%{to: to, actor: actor, context: context, object: note, published: date, local: false, additional: %{"cc" => cc}}) inReplyTo && !inReplyToActivity,
do: note |> Map.put("inReplyTo", inReplyTo),
else: note
) do
res =
ActivityPub.create(%{
to: to,
actor: actor,
context: context,
object: note,
published: date,
local: false,
additional: %{"cc" => cc}
})
User.increase_note_count(actor) User.increase_note_count(actor)
res res
else else

View file

@ -16,7 +16,7 @@ def feed_path(user) do
end end
def pubsub_path(user) do def pubsub_path(user) do
"#{Web.base_url}/push/hub/#{user.nickname}" "#{Web.base_url()}/push/hub/#{user.nickname}"
end end
def salmon_path(user) do def salmon_path(user) do
@ -24,48 +24,59 @@ def salmon_path(user) do
end end
def remote_follow_path do def remote_follow_path do
"#{Web.base_url}/ostatus_subscribe?acct={uri}" "#{Web.base_url()}/ostatus_subscribe?acct={uri}"
end end
def handle_incoming(xml_string) do def handle_incoming(xml_string) do
with doc when doc != :error <- parse_document(xml_string) do with doc when doc != :error <- parse_document(xml_string) do
entries = :xmerl_xpath.string('//entry', doc) entries = :xmerl_xpath.string('//entry', doc)
activities = Enum.map(entries, fn (entry) -> activities =
{:xmlObj, :string, object_type} = :xmerl_xpath.string('string(/entry/activity:object-type[1])', entry) Enum.map(entries, fn entry ->
{:xmlObj, :string, verb} = :xmerl_xpath.string('string(/entry/activity:verb[1])', entry) {:xmlObj, :string, object_type} =
Logger.debug("Handling #{verb}") :xmerl_xpath.string('string(/entry/activity:object-type[1])', entry)
try do {:xmlObj, :string, verb} = :xmerl_xpath.string('string(/entry/activity:verb[1])', entry)
case verb do Logger.debug("Handling #{verb}")
'http://activitystrea.ms/schema/1.0/delete' ->
with {:ok, activity} <- DeleteHandler.handle_delete(entry, doc), do: activity try do
'http://activitystrea.ms/schema/1.0/follow' -> case verb do
with {:ok, activity} <- FollowHandler.handle(entry, doc), do: activity 'http://activitystrea.ms/schema/1.0/delete' ->
'http://activitystrea.ms/schema/1.0/share' -> with {:ok, activity} <- DeleteHandler.handle_delete(entry, doc), do: activity
with {:ok, activity, retweeted_activity} <- handle_share(entry, doc), do: [activity, retweeted_activity]
'http://activitystrea.ms/schema/1.0/favorite' -> 'http://activitystrea.ms/schema/1.0/follow' ->
with {:ok, activity, favorited_activity} <- handle_favorite(entry, doc), do: [activity, favorited_activity] with {:ok, activity} <- FollowHandler.handle(entry, doc), do: activity
_ ->
case object_type do 'http://activitystrea.ms/schema/1.0/share' ->
'http://activitystrea.ms/schema/1.0/note' -> with {:ok, activity, retweeted_activity} <- handle_share(entry, doc),
with {:ok, activity} <- NoteHandler.handle_note(entry, doc), do: activity do: [activity, retweeted_activity]
'http://activitystrea.ms/schema/1.0/comment' ->
with {:ok, activity} <- NoteHandler.handle_note(entry, doc), do: activity 'http://activitystrea.ms/schema/1.0/favorite' ->
_ -> with {:ok, activity, favorited_activity} <- handle_favorite(entry, doc),
Logger.error("Couldn't parse incoming document") do: [activity, favorited_activity]
nil
end _ ->
case object_type do
'http://activitystrea.ms/schema/1.0/note' ->
with {:ok, activity} <- NoteHandler.handle_note(entry, doc), do: activity
'http://activitystrea.ms/schema/1.0/comment' ->
with {:ok, activity} <- NoteHandler.handle_note(entry, doc), do: activity
_ ->
Logger.error("Couldn't parse incoming document")
nil
end
end
rescue
e ->
Logger.error("Error occured while handling activity")
Logger.error(xml_string)
Logger.error(inspect(e))
nil
end end
rescue end)
e -> |> Enum.filter(& &1)
Logger.error("Error occured while handling activity")
Logger.error(xml_string)
Logger.error(inspect(e))
nil
end
end)
|> Enum.filter(&(&1))
{:ok, activities} {:ok, activities}
else else
@ -113,15 +124,20 @@ def get_or_build_object(entry) do
def get_or_try_fetching(entry) do def get_or_try_fetching(entry) do
Logger.debug("Trying to get entry from db") Logger.debug("Trying to get entry from db")
with id when not is_nil(id) <- string_from_xpath("//activity:object[1]/id", entry), with id when not is_nil(id) <- string_from_xpath("//activity:object[1]/id", entry),
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
{:ok, activity} {:ok, activity}
else _ -> else
_ ->
Logger.debug("Couldn't get, will try to fetch") Logger.debug("Couldn't get, will try to fetch")
with href when not is_nil(href) <- string_from_xpath("//activity:object[1]/link[@type=\"text/html\"]/@href", entry),
with href when not is_nil(href) <-
string_from_xpath("//activity:object[1]/link[@type=\"text/html\"]/@href", entry),
{:ok, [favorited_activity]} <- fetch_activity_from_url(href) do {:ok, [favorited_activity]} <- fetch_activity_from_url(href) do
{:ok, favorited_activity} {:ok, favorited_activity}
else e -> Logger.debug("Couldn't find href: #{inspect(e)}") else
e -> Logger.debug("Couldn't find href: #{inspect(e)}")
end end
end end
end end
@ -137,20 +153,22 @@ def handle_favorite(entry, doc) do
def get_attachments(entry) do def get_attachments(entry) do
:xmerl_xpath.string('/entry/link[@rel="enclosure"]', entry) :xmerl_xpath.string('/entry/link[@rel="enclosure"]', entry)
|> Enum.map(fn (enclosure) -> |> Enum.map(fn enclosure ->
with href when not is_nil(href) <- string_from_xpath("/link/@href", enclosure), with href when not is_nil(href) <- string_from_xpath("/link/@href", enclosure),
type when not is_nil(type) <- string_from_xpath("/link/@type", enclosure) do type when not is_nil(type) <- string_from_xpath("/link/@type", enclosure) do
%{ %{
"type" => "Attachment", "type" => "Attachment",
"url" => [%{ "url" => [
"type" => "Link", %{
"mediaType" => type, "type" => "Link",
"href" => href "mediaType" => type,
}] "href" => href
}
]
} }
end end
end) end)
|> Enum.filter(&(&1)) |> Enum.filter(& &1)
end end
@doc """ @doc """
@ -166,14 +184,15 @@ def get_content(entry) do
def get_cw(entry) do def get_cw(entry) do
with cw when not is_nil(cw) <- string_from_xpath("/*/summary", entry) do with cw when not is_nil(cw) <- string_from_xpath("/*/summary", entry) do
cw cw
else _e -> nil else
_e -> nil
end end
end end
def get_tags(entry) do def get_tags(entry) do
:xmerl_xpath.string('//category', entry) :xmerl_xpath.string('//category', entry)
|> Enum.map(fn (category) -> string_from_xpath("/category/@term", category) end) |> Enum.map(fn category -> string_from_xpath("/category/@term", category) end)
|> Enum.filter(&(&1)) |> Enum.filter(& &1)
|> Enum.map(&String.downcase/1) |> Enum.map(&String.downcase/1)
end end
@ -184,6 +203,7 @@ def maybe_update(doc, user) do
maybe_update_ostatus(doc, user) maybe_update_ostatus(doc, user)
end end
end end
def maybe_update_ostatus(doc, user) do def maybe_update_ostatus(doc, user) do
old_data = %{ old_data = %{
avatar: user.avatar, avatar: user.avatar,
@ -196,26 +216,33 @@ def maybe_update_ostatus(doc, user) do
avatar <- make_avatar_object(doc), avatar <- make_avatar_object(doc),
bio <- string_from_xpath("//author[1]/summary", doc), bio <- string_from_xpath("//author[1]/summary", doc),
name <- string_from_xpath("//author[1]/poco:displayName", doc), name <- string_from_xpath("//author[1]/poco:displayName", doc),
info <- Map.put(user.info, "banner", make_avatar_object(doc, "header") || user.info["banner"]), info <-
new_data <- %{avatar: avatar || old_data.avatar, name: name || old_data.name, bio: bio || old_data.bio, info: info || old_data.info}, Map.put(user.info, "banner", make_avatar_object(doc, "header") || user.info["banner"]),
new_data <- %{
avatar: avatar || old_data.avatar,
name: name || old_data.name,
bio: bio || old_data.bio,
info: info || old_data.info
},
false <- new_data == old_data do false <- new_data == old_data do
change = Ecto.Changeset.change(user, new_data) change = Ecto.Changeset.change(user, new_data)
Repo.update(change) Repo.update(change)
else _ -> else
{:ok, user} _ ->
{:ok, user}
end end
end end
def find_make_or_update_user(doc) do def find_make_or_update_user(doc) do
uri = string_from_xpath("//author/uri[1]", doc) uri = string_from_xpath("//author/uri[1]", doc)
with {:ok, user} <- find_or_make_user(uri) do with {:ok, user} <- find_or_make_user(uri) do
maybe_update(doc, user) maybe_update(doc, user)
end end
end end
def find_or_make_user(uri) do def find_or_make_user(uri) do
query = from user in User, query = from(user in User, where: user.ap_id == ^uri)
where: user.ap_id == ^uri
user = Repo.one(query) user = Repo.one(query)
@ -236,10 +263,12 @@ def make_user(uri, update \\ false) do
avatar: info["avatar"], avatar: info["avatar"],
bio: info["bio"] bio: info["bio"]
} }
with false <- update, with false <- update,
%User{} = user <- User.get_by_ap_id(data.ap_id) do %User{} = user <- User.get_by_ap_id(data.ap_id) do
{:ok, user} {:ok, user}
else _e -> User.insert_or_update_user(data) else
_e -> User.insert_or_update_user(data)
end end
end end
end end
@ -252,12 +281,13 @@ def make_avatar_object(author_doc, rel \\ "avatar") do
if href do if href do
%{ %{
"type" => "Image", "type" => "Image",
"url" => "url" => [
[%{ %{
"type" => "Link", "type" => "Link",
"mediaType" => type, "mediaType" => type,
"href" => href "href" => href
}] }
]
} }
else else
nil nil
@ -268,9 +298,10 @@ def gather_user_info(username) do
with {:ok, webfinger_data} <- WebFinger.finger(username), with {:ok, webfinger_data} <- WebFinger.finger(username),
{:ok, feed_data} <- Websub.gather_feed_data(webfinger_data["topic"]) do {:ok, feed_data} <- Websub.gather_feed_data(webfinger_data["topic"]) do
{:ok, Map.merge(webfinger_data, feed_data) |> Map.put("fqn", username)} {:ok, Map.merge(webfinger_data, feed_data) |> Map.put("fqn", username)}
else e -> else
Logger.debug(fn -> "Couldn't gather info for #{username}" end) e ->
{:error, e} Logger.debug(fn -> "Couldn't gather info for #{username}" end)
{:error, e}
end end
end end
@ -284,12 +315,15 @@ def get_atom_url(body) do
Regex.match?(@mastodon_regex, body) -> Regex.match?(@mastodon_regex, body) ->
[[_, match]] = Regex.scan(@mastodon_regex, body) [[_, match]] = Regex.scan(@mastodon_regex, body)
{:ok, match} {:ok, match}
Regex.match?(@gs_regex, body) -> Regex.match?(@gs_regex, body) ->
[[_, match]] = Regex.scan(@gs_regex, body) [[_, match]] = Regex.scan(@gs_regex, body)
{:ok, match} {:ok, match}
Regex.match?(@gs_classic_regex, body) -> Regex.match?(@gs_classic_regex, body) ->
[[_, match]] = Regex.scan(@gs_classic_regex, body) [[_, match]] = Regex.scan(@gs_classic_regex, body)
{:ok, match} {:ok, match}
true -> true ->
Logger.debug(fn -> "Couldn't find Atom link in #{inspect(body)}" end) Logger.debug(fn -> "Couldn't find Atom link in #{inspect(body)}" end)
{:error, "Couldn't find the Atom link"} {:error, "Couldn't find the Atom link"}
@ -298,7 +332,14 @@ def get_atom_url(body) do
def fetch_activity_from_atom_url(url) do def fetch_activity_from_atom_url(url) do
with true <- String.starts_with?(url, "http"), with true <- String.starts_with?(url, "http"),
{:ok, %{body: body, status_code: code}} when code in 200..299 <- @httpoison.get(url, [Accept: "application/atom+xml"], follow_redirect: true, timeout: 10000, recv_timeout: 20000) do {:ok, %{body: body, status_code: code}} when code in 200..299 <-
@httpoison.get(
url,
[Accept: "application/atom+xml"],
follow_redirect: true,
timeout: 10000,
recv_timeout: 20000
) do
Logger.debug("Got document from #{url}, handling...") Logger.debug("Got document from #{url}, handling...")
handle_incoming(body) handle_incoming(body)
else else
@ -310,10 +351,12 @@ def fetch_activity_from_atom_url(url) do
def fetch_activity_from_html_url(url) do def fetch_activity_from_html_url(url) do
Logger.debug("Trying to fetch #{url}") Logger.debug("Trying to fetch #{url}")
with true <- String.starts_with?(url, "http"), with true <- String.starts_with?(url, "http"),
{:ok, %{body: body}} <- @httpoison.get(url, [], follow_redirect: true, timeout: 10000, recv_timeout: 20000), {:ok, %{body: body}} <-
@httpoison.get(url, [], follow_redirect: true, timeout: 10000, recv_timeout: 20000),
{:ok, atom_url} <- get_atom_url(body) do {:ok, atom_url} <- get_atom_url(body) do
fetch_activity_from_atom_url(atom_url) fetch_activity_from_atom_url(atom_url)
else else
e -> e ->
Logger.debug("Couldn't get #{url}: #{inspect(e)}") Logger.debug("Couldn't get #{url}: #{inspect(e)}")
@ -326,9 +369,10 @@ def fetch_activity_from_url(url) do
with {:ok, activities} when length(activities) > 0 <- fetch_activity_from_atom_url(url) do with {:ok, activities} when length(activities) > 0 <- fetch_activity_from_atom_url(url) do
{:ok, activities} {:ok, activities}
else else
_e -> with {:ok, activities} <- fetch_activity_from_html_url(url) do _e ->
{:ok, activities} with {:ok, activities} <- fetch_activity_from_html_url(url) do
end {:ok, activities}
end
end end
rescue rescue
e -> e ->

View file

@ -16,23 +16,26 @@ def feed_redirect(conn, %{"nickname" => nickname} = params) do
case get_format(conn) do case get_format(conn) do
"html" -> Fallback.RedirectController.redirector(conn, nil) "html" -> Fallback.RedirectController.redirector(conn, nil)
"activity+json" -> ActivityPubController.user(conn, params) "activity+json" -> ActivityPubController.user(conn, params)
_ -> redirect conn, external: OStatus.feed_path(user) _ -> redirect(conn, external: OStatus.feed_path(user))
end end
end end
def feed(conn, %{"nickname" => nickname} = params) do def feed(conn, %{"nickname" => nickname} = params) do
user = User.get_cached_by_nickname(nickname) user = User.get_cached_by_nickname(nickname)
query_params = Map.take(params, ["max_id"]) query_params =
|> Map.merge(%{"whole_db" => true, "actor_id" => user.ap_id}) Map.take(params, ["max_id"])
|> Map.merge(%{"whole_db" => true, "actor_id" => user.ap_id})
activities = ActivityPub.fetch_public_activities(query_params) activities =
|> Enum.reverse ActivityPub.fetch_public_activities(query_params)
|> Enum.reverse()
response = user response =
|> FeedRepresenter.to_simple_form(activities, [user]) user
|> :xmerl.export_simple(:xmerl_xml) |> FeedRepresenter.to_simple_form(activities, [user])
|> to_string |> :xmerl.export_simple(:xmerl_xml)
|> to_string
conn conn
|> put_resp_content_type("application/atom+xml") |> put_resp_content_type("application/atom+xml")
@ -73,7 +76,7 @@ def object(conn, %{"uuid" => uuid} = params) do
else else
with id <- o_status_url(conn, :object, uuid), with id <- o_status_url(conn, :object, uuid),
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id), %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id),
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
case get_format(conn) do case get_format(conn) do
"html" -> redirect(conn, to: "/notice/#{activity.id}") "html" -> redirect(conn, to: "/notice/#{activity.id}")
_ -> represent_activity(conn, activity, user) _ -> represent_activity(conn, activity, user)
@ -96,24 +99,27 @@ def activity(conn, %{"uuid" => uuid}) do
# TODO: Data leak # TODO: Data leak
def notice(conn, %{"id" => id}) do def notice(conn, %{"id" => id}) do
with %Activity{} = activity <- Repo.get(Activity, id), with %Activity{} = activity <- Repo.get(Activity, id),
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
case get_format(conn) do case get_format(conn) do
"html" -> "html" ->
conn conn
|> put_resp_content_type("text/html") |> put_resp_content_type("text/html")
|> send_file(200, "priv/static/index.html") |> send_file(200, "priv/static/index.html")
_ -> represent_activity(conn, activity, user)
_ ->
represent_activity(conn, activity, user)
end end
end end
end end
defp represent_activity(conn, activity, user) do defp represent_activity(conn, activity, user) do
response = activity response =
|> ActivityRepresenter.to_simple_form(user, true) activity
|> ActivityRepresenter.wrap_with_entry |> ActivityRepresenter.to_simple_form(user, true)
|> :xmerl.export_simple(:xmerl_xml) |> ActivityRepresenter.wrap_with_entry()
|> to_string |> :xmerl.export_simple(:xmerl_xml)
|> to_string
conn conn
|> put_resp_content_type("application/atom+xml") |> put_resp_content_type("application/atom+xml")

View file

@ -1,22 +1,26 @@
defmodule Pleroma.Web.OStatus.UserRepresenter do defmodule Pleroma.Web.OStatus.UserRepresenter do
alias Pleroma.User alias Pleroma.User
def to_simple_form(user) do def to_simple_form(user) do
ap_id = to_charlist(user.ap_id) ap_id = to_charlist(user.ap_id)
nickname = to_charlist(user.nickname) nickname = to_charlist(user.nickname)
name = to_charlist(user.name) name = to_charlist(user.name)
bio = to_charlist(user.bio) bio = to_charlist(user.bio)
avatar_url = to_charlist(User.avatar_url(user)) avatar_url = to_charlist(User.avatar_url(user))
banner = if banner_url = User.banner_url(user) do
[{:link, [rel: 'header', href: banner_url], []}]
else
[]
end
ap_enabled = if user.local do banner =
[{:ap_enabled, ['true']}] if banner_url = User.banner_url(user) do
else [{:link, [rel: 'header', href: banner_url], []}]
[] else
end []
end
ap_enabled =
if user.local do
[{:ap_enabled, ['true']}]
else
[]
end
[ [
{:id, [ap_id]}, {:id, [ap_id]},

View file

@ -11,294 +11,305 @@ def user_fetcher(username) do
end end
pipeline :api do pipeline :api do
plug :accepts, ["json"] plug(:accepts, ["json"])
plug :fetch_session plug(:fetch_session)
plug Pleroma.Plugs.OAuthPlug plug(Pleroma.Plugs.OAuthPlug)
plug Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1, optional: true} plug(Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1, optional: true})
end end
pipeline :authenticated_api do pipeline :authenticated_api do
plug :accepts, ["json"] plug(:accepts, ["json"])
plug :fetch_session plug(:fetch_session)
plug Pleroma.Plugs.OAuthPlug plug(Pleroma.Plugs.OAuthPlug)
plug Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1} plug(Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1})
end end
pipeline :mastodon_html do pipeline :mastodon_html do
plug :accepts, ["html"] plug(:accepts, ["html"])
plug :fetch_session plug(:fetch_session)
plug Pleroma.Plugs.OAuthPlug plug(Pleroma.Plugs.OAuthPlug)
plug Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1, optional: true} plug(Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1, optional: true})
end end
pipeline :pleroma_html do pipeline :pleroma_html do
plug :accepts, ["html"] plug(:accepts, ["html"])
plug :fetch_session plug(:fetch_session)
plug Pleroma.Plugs.OAuthPlug plug(Pleroma.Plugs.OAuthPlug)
plug Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1, optional: true} plug(Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1, optional: true})
end end
pipeline :well_known do pipeline :well_known do
plug :accepts, ["xml", "xrd+xml", "json", "jrd+json"] plug(:accepts, ["xml", "xrd+xml", "json", "jrd+json"])
end end
pipeline :config do pipeline :config do
plug :accepts, ["json", "xml"] plug(:accepts, ["json", "xml"])
end end
pipeline :oauth do pipeline :oauth do
plug :accepts, ["html", "json"] plug(:accepts, ["html", "json"])
end end
pipeline :pleroma_api do pipeline :pleroma_api do
plug :accepts, ["html", "json"] plug(:accepts, ["html", "json"])
end end
scope "/api/pleroma", Pleroma.Web.TwitterAPI do scope "/api/pleroma", Pleroma.Web.TwitterAPI do
pipe_through :pleroma_api pipe_through(:pleroma_api)
get "/password_reset/:token", UtilController, :show_password_reset get("/password_reset/:token", UtilController, :show_password_reset)
post "/password_reset", UtilController, :password_reset post("/password_reset", UtilController, :password_reset)
get "/emoji", UtilController, :emoji get("/emoji", UtilController, :emoji)
end end
scope "/", Pleroma.Web.TwitterAPI do scope "/", Pleroma.Web.TwitterAPI do
pipe_through :pleroma_html pipe_through(:pleroma_html)
get "/ostatus_subscribe", UtilController, :remote_follow get("/ostatus_subscribe", UtilController, :remote_follow)
post "/ostatus_subscribe", UtilController, :do_remote_follow post("/ostatus_subscribe", UtilController, :do_remote_follow)
post "/main/ostatus", UtilController, :remote_subscribe post("/main/ostatus", UtilController, :remote_subscribe)
end end
scope "/api/pleroma", Pleroma.Web.TwitterAPI do scope "/api/pleroma", Pleroma.Web.TwitterAPI do
pipe_through :authenticated_api pipe_through(:authenticated_api)
post "/follow_import", UtilController, :follow_import post("/follow_import", UtilController, :follow_import)
end end
scope "/oauth", Pleroma.Web.OAuth do scope "/oauth", Pleroma.Web.OAuth do
get "/authorize", OAuthController, :authorize get("/authorize", OAuthController, :authorize)
post "/authorize", OAuthController, :create_authorization post("/authorize", OAuthController, :create_authorization)
post "/token", OAuthController, :token_exchange post("/token", OAuthController, :token_exchange)
end end
scope "/api/v1", Pleroma.Web.MastodonAPI do scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through :authenticated_api pipe_through(:authenticated_api)
patch "/accounts/update_credentials", MastodonAPIController, :update_credentials patch("/accounts/update_credentials", MastodonAPIController, :update_credentials)
get "/accounts/verify_credentials", MastodonAPIController, :verify_credentials get("/accounts/verify_credentials", MastodonAPIController, :verify_credentials)
get "/accounts/relationships", MastodonAPIController, :relationships get("/accounts/relationships", MastodonAPIController, :relationships)
get "/accounts/search", MastodonAPIController, :account_search get("/accounts/search", MastodonAPIController, :account_search)
post "/accounts/:id/follow", MastodonAPIController, :follow post("/accounts/:id/follow", MastodonAPIController, :follow)
post "/accounts/:id/unfollow", MastodonAPIController, :unfollow post("/accounts/:id/unfollow", MastodonAPIController, :unfollow)
post "/accounts/:id/block", MastodonAPIController, :block post("/accounts/:id/block", MastodonAPIController, :block)
post "/accounts/:id/unblock", MastodonAPIController, :unblock post("/accounts/:id/unblock", MastodonAPIController, :unblock)
post "/accounts/:id/mute", MastodonAPIController, :relationship_noop post("/accounts/:id/mute", MastodonAPIController, :relationship_noop)
post "/accounts/:id/unmute", MastodonAPIController, :relationship_noop post("/accounts/:id/unmute", MastodonAPIController, :relationship_noop)
post "/follows", MastodonAPIController, :follow post("/follows", MastodonAPIController, :follow)
get "/blocks", MastodonAPIController, :blocks get("/blocks", MastodonAPIController, :blocks)
get "/domain_blocks", MastodonAPIController, :empty_array get("/domain_blocks", MastodonAPIController, :empty_array)
get "/follow_requests", MastodonAPIController, :empty_array get("/follow_requests", MastodonAPIController, :empty_array)
get "/mutes", MastodonAPIController, :empty_array get("/mutes", MastodonAPIController, :empty_array)
get "/timelines/home", MastodonAPIController, :home_timeline get("/timelines/home", MastodonAPIController, :home_timeline)
get "/favourites", MastodonAPIController, :favourites get("/favourites", MastodonAPIController, :favourites)
post "/statuses", MastodonAPIController, :post_status post("/statuses", MastodonAPIController, :post_status)
delete "/statuses/:id", MastodonAPIController, :delete_status delete("/statuses/:id", MastodonAPIController, :delete_status)
post "/statuses/:id/reblog", MastodonAPIController, :reblog_status post("/statuses/:id/reblog", MastodonAPIController, :reblog_status)
post "/statuses/:id/favourite", MastodonAPIController, :fav_status post("/statuses/:id/favourite", MastodonAPIController, :fav_status)
post "/statuses/:id/unfavourite", MastodonAPIController, :unfav_status post("/statuses/:id/unfavourite", MastodonAPIController, :unfav_status)
post "/notifications/clear", MastodonAPIController, :clear_notifications post("/notifications/clear", MastodonAPIController, :clear_notifications)
post "/notifications/dismiss", MastodonAPIController, :dismiss_notification post("/notifications/dismiss", MastodonAPIController, :dismiss_notification)
get "/notifications", MastodonAPIController, :notifications get("/notifications", MastodonAPIController, :notifications)
get "/notifications/:id", MastodonAPIController, :get_notification get("/notifications/:id", MastodonAPIController, :get_notification)
post "/media", MastodonAPIController, :upload post("/media", MastodonAPIController, :upload)
end end
scope "/api/v1", Pleroma.Web.MastodonAPI do scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through :api pipe_through(:api)
get "/instance", MastodonAPIController, :masto_instance get("/instance", MastodonAPIController, :masto_instance)
get "/instance/peers", MastodonAPIController, :peers get("/instance/peers", MastodonAPIController, :peers)
post "/apps", MastodonAPIController, :create_app post("/apps", MastodonAPIController, :create_app)
get "/custom_emojis", MastodonAPIController, :custom_emojis get("/custom_emojis", MastodonAPIController, :custom_emojis)
get "/timelines/public", MastodonAPIController, :public_timeline get("/timelines/public", MastodonAPIController, :public_timeline)
get "/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline get("/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline)
get "/statuses/:id", MastodonAPIController, :get_status get("/statuses/:id", MastodonAPIController, :get_status)
get "/statuses/:id/context", MastodonAPIController, :get_context get("/statuses/:id/context", MastodonAPIController, :get_context)
get "/statuses/:id/card", MastodonAPIController, :empty_object get("/statuses/:id/card", MastodonAPIController, :empty_object)
get "/statuses/:id/favourited_by", MastodonAPIController, :favourited_by get("/statuses/:id/favourited_by", MastodonAPIController, :favourited_by)
get "/statuses/:id/reblogged_by", MastodonAPIController, :reblogged_by get("/statuses/:id/reblogged_by", MastodonAPIController, :reblogged_by)
get "/accounts/:id/statuses", MastodonAPIController, :user_statuses get("/accounts/:id/statuses", MastodonAPIController, :user_statuses)
get "/accounts/:id/followers", MastodonAPIController, :followers get("/accounts/:id/followers", MastodonAPIController, :followers)
get "/accounts/:id/following", MastodonAPIController, :following get("/accounts/:id/following", MastodonAPIController, :following)
get "/accounts/:id", MastodonAPIController, :user get("/accounts/:id", MastodonAPIController, :user)
get "/search", MastodonAPIController, :search get("/search", MastodonAPIController, :search)
end end
scope "/api", Pleroma.Web do scope "/api", Pleroma.Web do
pipe_through :config pipe_through(:config)
get "/help/test", TwitterAPI.UtilController, :help_test get("/help/test", TwitterAPI.UtilController, :help_test)
post "/help/test", TwitterAPI.UtilController, :help_test post("/help/test", TwitterAPI.UtilController, :help_test)
get "/statusnet/config", TwitterAPI.UtilController, :config get("/statusnet/config", TwitterAPI.UtilController, :config)
get "/statusnet/version", TwitterAPI.UtilController, :version get("/statusnet/version", TwitterAPI.UtilController, :version)
end end
@instance Application.get_env(:pleroma, :instance) @instance Application.get_env(:pleroma, :instance)
@registrations_open Keyword.get(@instance, :registrations_open) @registrations_open Keyword.get(@instance, :registrations_open)
scope "/api", Pleroma.Web do scope "/api", Pleroma.Web do
pipe_through :api pipe_through(:api)
get "/statuses/public_timeline", TwitterAPI.Controller, :public_timeline get("/statuses/public_timeline", TwitterAPI.Controller, :public_timeline)
get "/statuses/public_and_external_timeline", TwitterAPI.Controller, :public_and_external_timeline
get "/statuses/networkpublic_timeline", TwitterAPI.Controller, :public_and_external_timeline
get "/statuses/user_timeline", TwitterAPI.Controller, :user_timeline
get "/qvitter/statuses/user_timeline", TwitterAPI.Controller, :user_timeline
get "/users/show", TwitterAPI.Controller, :show_user
get "/statuses/followers", TwitterAPI.Controller, :followers get(
get "/statuses/friends", TwitterAPI.Controller, :friends "/statuses/public_and_external_timeline",
get "/statuses/show/:id", TwitterAPI.Controller, :fetch_status TwitterAPI.Controller,
get "/statusnet/conversation/:id", TwitterAPI.Controller, :fetch_conversation :public_and_external_timeline
)
get("/statuses/networkpublic_timeline", TwitterAPI.Controller, :public_and_external_timeline)
get("/statuses/user_timeline", TwitterAPI.Controller, :user_timeline)
get("/qvitter/statuses/user_timeline", TwitterAPI.Controller, :user_timeline)
get("/users/show", TwitterAPI.Controller, :show_user)
get("/statuses/followers", TwitterAPI.Controller, :followers)
get("/statuses/friends", TwitterAPI.Controller, :friends)
get("/statuses/show/:id", TwitterAPI.Controller, :fetch_status)
get("/statusnet/conversation/:id", TwitterAPI.Controller, :fetch_conversation)
if @registrations_open do if @registrations_open do
post "/account/register", TwitterAPI.Controller, :register post("/account/register", TwitterAPI.Controller, :register)
end 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)
end end
scope "/api", Pleroma.Web do scope "/api", Pleroma.Web do
pipe_through :authenticated_api pipe_through(:authenticated_api)
get "/account/verify_credentials", TwitterAPI.Controller, :verify_credentials get("/account/verify_credentials", TwitterAPI.Controller, :verify_credentials)
post "/account/verify_credentials", TwitterAPI.Controller, :verify_credentials post("/account/verify_credentials", TwitterAPI.Controller, :verify_credentials)
post "/account/update_profile", TwitterAPI.Controller, :update_profile post("/account/update_profile", TwitterAPI.Controller, :update_profile)
post "/account/update_profile_banner", TwitterAPI.Controller, :update_banner post("/account/update_profile_banner", TwitterAPI.Controller, :update_banner)
post "/qvitter/update_background_image", TwitterAPI.Controller, :update_background post("/qvitter/update_background_image", TwitterAPI.Controller, :update_background)
post "/account/most_recent_notification", TwitterAPI.Controller, :update_most_recent_notification post(
"/account/most_recent_notification",
TwitterAPI.Controller,
:update_most_recent_notification
)
get "/statuses/home_timeline", TwitterAPI.Controller, :friends_timeline get("/statuses/home_timeline", TwitterAPI.Controller, :friends_timeline)
get "/statuses/friends_timeline", TwitterAPI.Controller, :friends_timeline get("/statuses/friends_timeline", TwitterAPI.Controller, :friends_timeline)
get "/statuses/mentions", TwitterAPI.Controller, :mentions_timeline get("/statuses/mentions", TwitterAPI.Controller, :mentions_timeline)
get "/statuses/mentions_timeline", TwitterAPI.Controller, :mentions_timeline get("/statuses/mentions_timeline", TwitterAPI.Controller, :mentions_timeline)
post "/statuses/update", TwitterAPI.Controller, :status_update post("/statuses/update", TwitterAPI.Controller, :status_update)
post "/statuses/retweet/:id", TwitterAPI.Controller, :retweet post("/statuses/retweet/:id", TwitterAPI.Controller, :retweet)
post "/statuses/destroy/:id", TwitterAPI.Controller, :delete_post post("/statuses/destroy/:id", TwitterAPI.Controller, :delete_post)
post "/friendships/create", TwitterAPI.Controller, :follow post("/friendships/create", TwitterAPI.Controller, :follow)
post "/friendships/destroy", TwitterAPI.Controller, :unfollow post("/friendships/destroy", TwitterAPI.Controller, :unfollow)
post "/blocks/create", TwitterAPI.Controller, :block post("/blocks/create", TwitterAPI.Controller, :block)
post "/blocks/destroy", TwitterAPI.Controller, :unblock post("/blocks/destroy", TwitterAPI.Controller, :unblock)
post "/statusnet/media/upload", TwitterAPI.Controller, :upload post("/statusnet/media/upload", TwitterAPI.Controller, :upload)
post "/media/upload", TwitterAPI.Controller, :upload_json post("/media/upload", TwitterAPI.Controller, :upload_json)
post "/favorites/create/:id", TwitterAPI.Controller, :favorite post("/favorites/create/:id", TwitterAPI.Controller, :favorite)
post "/favorites/create", TwitterAPI.Controller, :favorite post("/favorites/create", TwitterAPI.Controller, :favorite)
post "/favorites/destroy/:id", TwitterAPI.Controller, :unfavorite post("/favorites/destroy/:id", TwitterAPI.Controller, :unfavorite)
post "/qvitter/update_avatar", TwitterAPI.Controller, :update_avatar post("/qvitter/update_avatar", TwitterAPI.Controller, :update_avatar)
get "/friends/ids", TwitterAPI.Controller, :friends_ids get("/friends/ids", TwitterAPI.Controller, :friends_ids)
get "/friendships/no_retweets/ids", TwitterAPI.Controller, :empty_array get("/friendships/no_retweets/ids", TwitterAPI.Controller, :empty_array)
get "/mutes/users/ids", TwitterAPI.Controller, :empty_array get("/mutes/users/ids", TwitterAPI.Controller, :empty_array)
get "/externalprofile/show", TwitterAPI.Controller, :external_profile get("/externalprofile/show", TwitterAPI.Controller, :external_profile)
end end
pipeline :ostatus do pipeline :ostatus do
plug :accepts, ["xml", "atom", "html", "activity+json"] plug(:accepts, ["xml", "atom", "html", "activity+json"])
end end
scope "/", Pleroma.Web do scope "/", Pleroma.Web do
pipe_through :ostatus pipe_through(:ostatus)
get "/objects/:uuid", OStatus.OStatusController, :object get("/objects/:uuid", OStatus.OStatusController, :object)
get "/activities/:uuid", OStatus.OStatusController, :activity get("/activities/:uuid", OStatus.OStatusController, :activity)
get "/notice/:id", OStatus.OStatusController, :notice get("/notice/:id", OStatus.OStatusController, :notice)
get "/users/:nickname/feed", OStatus.OStatusController, :feed get("/users/:nickname/feed", OStatus.OStatusController, :feed)
get "/users/:nickname", OStatus.OStatusController, :feed_redirect get("/users/:nickname", OStatus.OStatusController, :feed_redirect)
if @federating do if @federating do
post "/users/:nickname/salmon", OStatus.OStatusController, :salmon_incoming post("/users/:nickname/salmon", OStatus.OStatusController, :salmon_incoming)
post "/push/hub/:nickname", Websub.WebsubController, :websub_subscription_request post("/push/hub/:nickname", Websub.WebsubController, :websub_subscription_request)
get "/push/subscriptions/:id", Websub.WebsubController, :websub_subscription_confirmation get("/push/subscriptions/:id", Websub.WebsubController, :websub_subscription_confirmation)
post "/push/subscriptions/:id", Websub.WebsubController, :websub_incoming post("/push/subscriptions/:id", Websub.WebsubController, :websub_incoming)
end end
end end
pipeline :activitypub do pipeline :activitypub do
plug :accepts, ["activity+json"] plug(:accepts, ["activity+json"])
plug Pleroma.Web.Plugs.HTTPSignaturePlug plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
end end
scope "/", Pleroma.Web.ActivityPub do scope "/", Pleroma.Web.ActivityPub do
# XXX: not really ostatus # XXX: not really ostatus
pipe_through :ostatus pipe_through(:ostatus)
get "/users/:nickname/followers", ActivityPubController, :followers get("/users/:nickname/followers", ActivityPubController, :followers)
get "/users/:nickname/following", ActivityPubController, :following get("/users/:nickname/following", ActivityPubController, :following)
get "/users/:nickname/outbox", ActivityPubController, :outbox get("/users/:nickname/outbox", ActivityPubController, :outbox)
end end
if @federating do if @federating do
scope "/", Pleroma.Web.ActivityPub do scope "/", Pleroma.Web.ActivityPub do
pipe_through :activitypub pipe_through(:activitypub)
post "/users/:nickname/inbox", ActivityPubController, :inbox post("/users/:nickname/inbox", ActivityPubController, :inbox)
post "/inbox", ActivityPubController, :inbox post("/inbox", ActivityPubController, :inbox)
end end
scope "/.well-known", Pleroma.Web do scope "/.well-known", Pleroma.Web do
pipe_through :well_known pipe_through(:well_known)
get "/host-meta", WebFinger.WebFingerController, :host_meta get("/host-meta", WebFinger.WebFingerController, :host_meta)
get "/webfinger", WebFinger.WebFingerController, :webfinger get("/webfinger", WebFinger.WebFingerController, :webfinger)
end end
end end
scope "/", Pleroma.Web.MastodonAPI do scope "/", Pleroma.Web.MastodonAPI do
pipe_through :mastodon_html pipe_through(:mastodon_html)
get "/web/login", MastodonAPIController, :login get("/web/login", MastodonAPIController, :login)
post "/web/login", MastodonAPIController, :login_post post("/web/login", MastodonAPIController, :login_post)
get "/web/*path", MastodonAPIController, :index get("/web/*path", MastodonAPIController, :index)
delete "/auth/sign_out", MastodonAPIController, :logout delete("/auth/sign_out", MastodonAPIController, :logout)
end end
pipeline :remote_media do pipeline :remote_media do
plug :accepts, ["html"] plug(:accepts, ["html"])
end end
scope "/proxy/", Pleroma.Web.MediaProxy do scope "/proxy/", Pleroma.Web.MediaProxy do
pipe_through :remote_media pipe_through(:remote_media)
get "/:sig/:url", MediaProxyController, :remote get("/:sig/:url", MediaProxyController, :remote)
end end
scope "/", Fallback do scope "/", Fallback do
get "/*path", RedirectController, :redirector get("/*path", RedirectController, :redirector)
end end
end end
defmodule Fallback.RedirectController do defmodule Fallback.RedirectController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
def redirector(conn, _params) do def redirector(conn, _params) do
if Mix.env != :test do if Mix.env() != :test do
conn conn
|> put_resp_content_type("text/html") |> put_resp_content_type("text/html")
|> send_file(200, "priv/static/index.html") |> send_file(200, "priv/static/index.html")

View file

@ -38,9 +38,10 @@ def fetch_magic_key(salmon) do
def decode_and_validate(magickey, salmon) do def decode_and_validate(magickey, salmon) do
[data, type, encoding, alg, sig] = decode(salmon) [data, type, encoding, alg, sig] = decode(salmon)
signed_text = [data, type, encoding, alg] signed_text =
|> Enum.map(&Base.url_encode64/1) [data, type, encoding, alg]
|> Enum.join(".") |> Enum.map(&Base.url_encode64/1)
|> Enum.join(".")
key = decode_key(magickey) key = decode_key(magickey)
@ -54,22 +55,23 @@ def decode_and_validate(magickey, salmon) do
end end
def decode_key("RSA." <> magickey) do def decode_key("RSA." <> magickey) do
make_integer = fn(bin) -> make_integer = fn bin ->
list = :erlang.binary_to_list(bin) list = :erlang.binary_to_list(bin)
Enum.reduce(list, 0, fn (el, acc) -> (acc <<< 8) ||| el end) Enum.reduce(list, 0, fn el, acc -> acc <<< 8 ||| el end)
end end
[modulus, exponent] = magickey [modulus, exponent] =
|> String.split(".") magickey
|> Enum.map(fn (n) -> Base.url_decode64!(n, padding: false) end) |> String.split(".")
|> Enum.map(make_integer) |> Enum.map(fn n -> Base.url_decode64!(n, padding: false) end)
|> Enum.map(make_integer)
{:RSAPublicKey, modulus, exponent} {:RSAPublicKey, modulus, exponent}
end end
def encode_key({:RSAPublicKey, modulus, exponent}) do def encode_key({:RSAPublicKey, modulus, exponent}) do
modulus_enc = :binary.encode_unsigned(modulus) |> Base.url_encode64 modulus_enc = :binary.encode_unsigned(modulus) |> Base.url_encode64()
exponent_enc = :binary.encode_unsigned(exponent) |> Base.url_encode64 exponent_enc = :binary.encode_unsigned(exponent) |> Base.url_encode64()
"RSA.#{modulus_enc}.#{exponent_enc}" "RSA.#{modulus_enc}.#{exponent_enc}"
end end
@ -78,20 +80,25 @@ def encode_key({:RSAPublicKey, modulus, exponent}) do
# We try at compile time to generate natively an RSA key otherwise we fallback on the old way. # We try at compile time to generate natively an RSA key otherwise we fallback on the old way.
try do try do
_ = :public_key.generate_key({:rsa, 2048, 65537}) _ = :public_key.generate_key({:rsa, 2048, 65537})
def generate_rsa_pem do def generate_rsa_pem do
key = :public_key.generate_key({:rsa, 2048, 65537}) key = :public_key.generate_key({:rsa, 2048, 65537})
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key) entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
pem = :public_key.pem_encode([entry]) |> String.trim_trailing pem = :public_key.pem_encode([entry]) |> String.trim_trailing()
{:ok, pem} {:ok, pem}
end end
rescue rescue
_ -> _ ->
def generate_rsa_pem do def generate_rsa_pem do
port = Port.open({:spawn, "openssl genrsa"}, [:binary]) port = Port.open({:spawn, "openssl genrsa"}, [:binary])
{:ok, pem} = receive do
{^port, {:data, pem}} -> {:ok, pem} {:ok, pem} =
end receive do
{^port, {:data, pem}} -> {:ok, pem}
end
Port.close(port) Port.close(port)
if Regex.match?(~r/RSA PRIVATE KEY/, pem) do if Regex.match?(~r/RSA PRIVATE KEY/, pem) do
{:ok, pem} {:ok, pem}
else else
@ -113,17 +120,20 @@ def encode(private_key, doc) do
encoding = "base64url" encoding = "base64url"
alg = "RSA-SHA256" alg = "RSA-SHA256"
signed_text = [doc, type, encoding, alg] signed_text =
|> Enum.map(&Base.url_encode64/1) [doc, type, encoding, alg]
|> Enum.join(".") |> Enum.map(&Base.url_encode64/1)
|> Enum.join(".")
signature = signed_text signature =
|> :public_key.sign(:sha256, private_key) signed_text
|> to_string |> :public_key.sign(:sha256, private_key)
|> Base.url_encode64 |> to_string
|> Base.url_encode64()
doc_base64 = doc doc_base64 =
|> Base.url_encode64 doc
|> Base.url_encode64()
# Don't need proper xml building, these strings are safe to leave unescaped # Don't need proper xml building, these strings are safe to leave unescaped
salmon = """ salmon = """
@ -141,20 +151,29 @@ def encode(private_key, doc) do
def remote_users(%{data: %{"to" => to} = data}) do def remote_users(%{data: %{"to" => to} = data}) do
to = to ++ (data["cc"] || []) to = to ++ (data["cc"] || [])
to to
|> Enum.map(fn(id) -> User.get_cached_by_ap_id(id) end) |> Enum.map(fn id -> User.get_cached_by_ap_id(id) end)
|> Enum.filter(fn(user) -> user && !user.local end) |> Enum.filter(fn user -> user && !user.local end)
end end
defp send_to_user(%{info: %{"salmon" => salmon}}, feed, poster) do defp send_to_user(%{info: %{"salmon" => salmon}}, feed, poster) do
with {:ok, %{status_code: code}} <- poster.(salmon, feed, [{"Content-Type", "application/magic-envelope+xml"}], timeout: 10000, recv_timeout: 20000, hackney: [pool: :default]) do with {:ok, %{status_code: code}} <-
poster.(
salmon,
feed,
[{"Content-Type", "application/magic-envelope+xml"}],
timeout: 10000,
recv_timeout: 20000,
hackney: [pool: :default]
) do
Logger.debug(fn -> "Pushed to #{salmon}, code #{code}" end) Logger.debug(fn -> "Pushed to #{salmon}, code #{code}" end)
else else
e -> Logger.debug(fn -> "Pushing Salmon to #{salmon} failed, #{inspect(e)}" end) e -> Logger.debug(fn -> "Pushing Salmon to #{salmon} failed, #{inspect(e)}" end)
end end
end end
defp send_to_user(_,_,_), do: nil defp send_to_user(_, _, _), do: nil
@supported_activities [ @supported_activities [
"Create", "Create",
@ -165,18 +184,21 @@ defp send_to_user(_,_,_), do: nil
"Delete" "Delete"
] ]
def publish(user, activity, poster \\ &@httpoison.post/4) def publish(user, activity, poster \\ &@httpoison.post/4)
def publish(%{info: %{"keys" => keys}} = user, %{data: %{"type" => type}} = activity, poster) when type in @supported_activities do
feed = ActivityRepresenter.to_simple_form(activity, user, true) def publish(%{info: %{"keys" => keys}} = user, %{data: %{"type" => type}} = activity, poster)
|> ActivityRepresenter.wrap_with_entry when type in @supported_activities do
|> :xmerl.export_simple(:xmerl_xml) feed =
|> to_string ActivityRepresenter.to_simple_form(activity, user, true)
|> ActivityRepresenter.wrap_with_entry()
|> :xmerl.export_simple(:xmerl_xml)
|> to_string
if feed do if feed do
{:ok, private, _} = keys_from_pem(keys) {:ok, private, _} = keys_from_pem(keys)
{:ok, feed} = encode(private, feed) {:ok, feed} = encode(private, feed)
remote_users(activity) remote_users(activity)
|> Enum.each(fn(remote_user) -> |> Enum.each(fn remote_user ->
Task.start(fn -> Task.start(fn ->
Logger.debug(fn -> "Sending Salmon to #{remote_user.ap_id}" end) Logger.debug(fn -> "Sending Salmon to #{remote_user.ap_id}" end)
send_to_user(remote_user, feed, poster) send_to_user(remote_user, feed, poster)

View file

@ -5,9 +5,11 @@ defmodule Pleroma.Web.Streamer do
def start_link do def start_link do
spawn(fn -> spawn(fn ->
Process.sleep(1000 * 30) # 30 seconds # 30 seconds
Process.sleep(1000 * 30)
GenServer.cast(__MODULE__, %{action: :ping}) GenServer.cast(__MODULE__, %{action: :ping})
end) end)
GenServer.start_link(__MODULE__, %{}, name: __MODULE__) GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
end end
@ -25,39 +27,54 @@ def stream(topic, item) do
def handle_cast(%{action: :ping}, topics) do def handle_cast(%{action: :ping}, topics) do
Map.values(topics) Map.values(topics)
|> List.flatten |> List.flatten()
|> Enum.each(fn (socket) -> |> Enum.each(fn socket ->
Logger.debug("Sending keepalive ping") Logger.debug("Sending keepalive ping")
send socket.transport_pid, {:text, ""} send(socket.transport_pid, {:text, ""})
end) end)
spawn(fn -> spawn(fn ->
Process.sleep(1000 * 30) # 30 seconds # 30 seconds
Process.sleep(1000 * 30)
GenServer.cast(__MODULE__, %{action: :ping}) GenServer.cast(__MODULE__, %{action: :ping})
end) end)
{:noreply, topics} {:noreply, topics}
end end
def handle_cast(%{action: :stream, topic: "user", item: %Notification{} = item}, topics) do def handle_cast(%{action: :stream, topic: "user", item: %Notification{} = item}, topics) do
topic = "user:#{item.user_id}" topic = "user:#{item.user_id}"
Enum.each(topics[topic] || [], fn (socket) ->
json = %{
event: "notification",
payload: Pleroma.Web.MastodonAPI.MastodonAPIController.render_notification(socket.assigns["user"], item) |> Jason.encode!
} |> Jason.encode!
send socket.transport_pid, {:text, json} Enum.each(topics[topic] || [], fn socket ->
json =
%{
event: "notification",
payload:
Pleroma.Web.MastodonAPI.MastodonAPIController.render_notification(
socket.assigns["user"],
item
)
|> Jason.encode!()
}
|> Jason.encode!()
send(socket.transport_pid, {:text, json})
end) end)
{:noreply, topics} {:noreply, topics}
end end
def handle_cast(%{action: :stream, topic: "user", item: item}, topics) do def handle_cast(%{action: :stream, topic: "user", item: item}, topics) do
Logger.debug("Trying to push to users") Logger.debug("Trying to push to users")
recipient_topics = User.get_recipients_from_activity(item)
|> Enum.map(fn (%{id: id}) -> "user:#{id}" end)
Enum.each(recipient_topics, fn (topic) -> recipient_topics =
User.get_recipients_from_activity(item)
|> Enum.map(fn %{id: id} -> "user:#{id}" end)
Enum.each(recipient_topics, fn topic ->
push_to_socket(topics, topic, item) push_to_socket(topics, topic, item)
end) end)
{:noreply, topics} {:noreply, topics}
end end
@ -92,13 +109,21 @@ def handle_cast(m, state) do
end end
def push_to_socket(topics, topic, item) do def push_to_socket(topics, topic, item) do
Enum.each(topics[topic] || [], fn (socket) -> Enum.each(topics[topic] || [], fn socket ->
json = %{ json =
event: "update", %{
payload: Pleroma.Web.MastodonAPI.StatusView.render("status.json", activity: item, for: socket.assigns[:user]) |> Jason.encode! event: "update",
} |> Jason.encode! payload:
Pleroma.Web.MastodonAPI.StatusView.render(
"status.json",
activity: item,
for: socket.assigns[:user]
)
|> Jason.encode!()
}
|> Jason.encode!()
send socket.transport_pid, {:text, json} send(socket.transport_pid, {:text, json})
end) end)
end end

View file

@ -11,21 +11,21 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
def show_password_reset(conn, %{"token" => token}) do def show_password_reset(conn, %{"token" => token}) do
with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}), with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}),
%User{} = user <- Repo.get(User, token.user_id) do %User{} = user <- Repo.get(User, token.user_id) do
render conn, "password_reset.html", %{ render(conn, "password_reset.html", %{
token: token, token: token,
user: user user: user
} })
else else
_e -> render conn, "invalid_token.html" _e -> render(conn, "invalid_token.html")
end end
end end
def password_reset(conn, %{"data" => data}) do def password_reset(conn, %{"data" => data}) do
with {:ok, _} <- PasswordResetToken.reset_password(data["token"], data) do with {:ok, _} <- PasswordResetToken.reset_password(data["token"], data) do
render conn, "password_reset_success.html" render(conn, "password_reset_success.html")
else else
_e -> render conn, "password_reset_failed.html" _e -> render(conn, "password_reset_failed.html")
end end
end end
@ -34,14 +34,19 @@ def help_test(conn, _params) do
end end
def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) do def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) do
with %User{} = user <- User.get_cached_by_nickname(nick), with %User{} = user <- User.get_cached_by_nickname(nick), avatar = User.avatar_url(user) do
avatar = User.avatar_url(user) do
conn conn
|> render("subscribe.html", %{nickname: nick, avatar: avatar, error: false}) |> render("subscribe.html", %{nickname: nick, avatar: avatar, error: false})
else else
_e -> render(conn, "subscribe.html", %{nickname: nick, avatar: nil, error: "Could not find user"}) _e ->
render(conn, "subscribe.html", %{
nickname: nick,
avatar: nil,
error: "Could not find user"
})
end end
end end
def remote_subscribe(conn, %{"user" => %{"nickname" => nick, "profile" => profile}}) do def remote_subscribe(conn, %{"user" => %{"nickname" => nick, "profile" => profile}}) do
with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile), with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile),
%User{ap_id: ap_id} <- User.get_cached_by_nickname(nick) do %User{ap_id: ap_id} <- User.get_cached_by_nickname(nick) do
@ -49,7 +54,11 @@ def remote_subscribe(conn, %{"user" => %{"nickname" => nick, "profile" => profil
|> Phoenix.Controller.redirect(external: String.replace(template, "{uri}", ap_id)) |> Phoenix.Controller.redirect(external: String.replace(template, "{uri}", ap_id))
else else
_e -> _e ->
render(conn, "subscribe.html", %{nickname: nick, avatar: nil, error: "Something went wrong."}) render(conn, "subscribe.html", %{
nickname: nick,
avatar: nil,
error: "Something went wrong."
})
end end
end end
@ -64,17 +73,26 @@ def remote_follow(%{assigns: %{user: user}} = conn, %{"acct" => acct}) do
|> render("follow.html", %{error: err, acct: acct, avatar: avatar, name: name, id: id}) |> render("follow.html", %{error: err, acct: acct, avatar: avatar, name: name, id: id})
else else
conn conn
|> render("follow_login.html", %{error: false, acct: acct, avatar: avatar, name: name, id: id}) |> render("follow_login.html", %{
error: false,
acct: acct,
avatar: avatar,
name: name,
id: id
})
end end
end end
def do_remote_follow(conn, %{"authorization" => %{"name" => username, "password" => password, "id" => id}}) do def do_remote_follow(conn, %{
"authorization" => %{"name" => username, "password" => password, "id" => id}
}) do
followee = Repo.get(User, id) followee = Repo.get(User, id)
avatar = User.avatar_url(followee) avatar = User.avatar_url(followee)
name = followee.nickname name = followee.nickname
with %User{} = user <- User.get_cached_by_nickname(username), with %User{} = user <- User.get_cached_by_nickname(username),
true <- Pbkdf2.checkpw(password, user.password_hash), true <- Pbkdf2.checkpw(password, user.password_hash),
%User{} = followed <- Repo.get(User, id), %User{} = followed <- Repo.get(User, id),
{:ok, follower} <- User.follow(user, followee), {:ok, follower} <- User.follow(user, followee),
{:ok, _activity} <- ActivityPub.follow(follower, followee) do {:ok, _activity} <- ActivityPub.follow(follower, followee) do
conn conn
@ -82,9 +100,15 @@ def do_remote_follow(conn, %{"authorization" => %{"name" => username, "password"
else else
_e -> _e ->
conn conn
|> render("follow_login.html", %{error: "Wrong username or password", id: id, name: name, avatar: avatar}) |> render("follow_login.html", %{
error: "Wrong username or password",
id: id,
name: name,
avatar: avatar
})
end end
end end
def do_remote_follow(%{assigns: %{user: user}} = conn, %{"user" => %{"id" => id}}) do def do_remote_follow(%{assigns: %{user: user}} = conn, %{"user" => %{"id" => id}}) do
with %User{} = followee <- Repo.get(User, id), with %User{} = followee <- Repo.get(User, id),
{:ok, follower} <- User.follow(user, followee), {:ok, follower} <- User.follow(user, followee),
@ -93,9 +117,10 @@ def do_remote_follow(%{assigns: %{user: user}} = conn, %{"user" => %{"id" => id}
|> render("followed.html", %{error: false}) |> render("followed.html", %{error: false})
else else
e -> e ->
Logger.debug("Remote follow failed with error #{inspect e}") Logger.debug("Remote follow failed with error #{inspect(e)}")
conn
|> render("followed.html", %{error: inspect(e)}) conn
|> render("followed.html", %{error: inspect(e)})
end end
end end
@ -107,60 +132,67 @@ def config(conn, _params) do
<config> <config>
<site> <site>
<name>#{Keyword.get(@instance, :name)}</name> <name>#{Keyword.get(@instance, :name)}</name>
<site>#{Web.base_url}</site> <site>#{Web.base_url()}</site>
<textlimit>#{Keyword.get(@instance, :limit)}</textlimit> <textlimit>#{Keyword.get(@instance, :limit)}</textlimit>
<closed>#{!Keyword.get(@instance, :registrations_open)}</closed> <closed>#{!Keyword.get(@instance, :registrations_open)}</closed>
</site> </site>
</config> </config>
""" """
conn conn
|> put_resp_content_type("application/xml") |> put_resp_content_type("application/xml")
|> send_resp(200, response) |> send_resp(200, response)
_ -> _ ->
json(conn, %{ json(conn, %{
site: %{ site: %{
name: Keyword.get(@instance, :name), name: Keyword.get(@instance, :name),
server: Web.base_url, server: Web.base_url(),
textlimit: to_string(Keyword.get(@instance, :limit)), textlimit: to_string(Keyword.get(@instance, :limit)),
closed: if(Keyword.get(@instance, :registrations_open), do: "0", else: "1") closed: if(Keyword.get(@instance, :registrations_open), do: "0", else: "1")
} }
}) })
end end
end end
def version(conn, _params) do def version(conn, _params) do
version = Keyword.get(@instance, :version) version = Keyword.get(@instance, :version)
case get_format(conn) do case get_format(conn) do
"xml" -> "xml" ->
response = "<version>#{version}</version>" response = "<version>#{version}</version>"
conn conn
|> put_resp_content_type("application/xml") |> put_resp_content_type("application/xml")
|> send_resp(200, response) |> send_resp(200, response)
_ -> json(conn, version)
_ ->
json(conn, version)
end end
end end
def emoji(conn, _params) do def emoji(conn, _params) do
json conn, Enum.into(Formatter.get_custom_emoji(), %{}) json(conn, Enum.into(Formatter.get_custom_emoji(), %{}))
end end
def follow_import(conn, %{"list" => %Plug.Upload{} = listfile}) do def follow_import(conn, %{"list" => %Plug.Upload{} = listfile}) do
follow_import(conn, %{"list" => File.read!(listfile.path)}) follow_import(conn, %{"list" => File.read!(listfile.path)})
end end
def follow_import(%{assigns: %{user: user}} = conn, %{"list" => list}) do def follow_import(%{assigns: %{user: user}} = conn, %{"list" => list}) do
Task.start(fn -> Task.start(fn ->
String.split(list) String.split(list)
|> Enum.map(fn nick -> |> Enum.map(fn nick ->
with %User{} = follower <- User.get_cached_by_ap_id(user.ap_id), with %User{} = follower <- User.get_cached_by_ap_id(user.ap_id),
%User{} = followed <- User.get_or_fetch_by_nickname(nick), %User{} = followed <- User.get_or_fetch_by_nickname(nick),
{:ok, follower} <- User.follow(follower, followed) do {:ok, follower} <- User.follow(follower, followed) do
ActivityPub.follow(follower, followed) ActivityPub.follow(follower, followed)
else else
_e -> Logger.debug "follow_import: following #{nick} failed" _e -> Logger.debug("follow_import: following #{nick} failed")
end end
end) end)
end) end)
json conn, "job started" json(conn, "job started")
end end
end end

View file

@ -7,18 +7,22 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do
alias Pleroma.Formatter alias Pleroma.Formatter
defp user_by_ap_id(user_list, ap_id) do defp user_by_ap_id(user_list, ap_id) do
Enum.find(user_list, fn (%{ap_id: user_id}) -> ap_id == user_id end) Enum.find(user_list, fn %{ap_id: user_id} -> ap_id == user_id end)
end end
def to_map(%Activity{data: %{"type" => "Announce", "actor" => actor, "published" => created_at}} = activity, def to_map(
%{users: users, announced_activity: announced_activity} = opts) do %Activity{data: %{"type" => "Announce", "actor" => actor, "published" => created_at}} =
activity,
%{users: users, announced_activity: announced_activity} = opts
) do
user = user_by_ap_id(users, actor) user = user_by_ap_id(users, actor)
created_at = created_at |> Utils.date_to_asctime created_at = created_at |> Utils.date_to_asctime()
text = "#{user.nickname} retweeted a status." text = "#{user.nickname} retweeted a status."
announced_user = user_by_ap_id(users, announced_activity.data["actor"]) announced_user = user_by_ap_id(users, announced_activity.data["actor"])
retweeted_status = to_map(announced_activity, Map.merge(%{user: announced_user}, opts)) retweeted_status = to_map(announced_activity, Map.merge(%{user: announced_user}, opts))
%{ %{
"id" => activity.id, "id" => activity.id,
"user" => UserView.render("show.json", %{user: user, for: opts[:for]}), "user" => UserView.render("show.json", %{user: user, for: opts[:for]}),
@ -35,9 +39,11 @@ def to_map(%Activity{data: %{"type" => "Announce", "actor" => actor, "published"
} }
end end
def to_map(%Activity{data: %{"type" => "Like", "published" => created_at}} = activity, def to_map(
%{user: user, liked_activity: liked_activity} = opts) do %Activity{data: %{"type" => "Like", "published" => created_at}} = activity,
created_at = created_at |> Utils.date_to_asctime %{user: user, liked_activity: liked_activity} = opts
) do
created_at = created_at |> Utils.date_to_asctime()
text = "#{user.nickname} favorited a status." text = "#{user.nickname} favorited a status."
@ -56,12 +62,16 @@ def to_map(%Activity{data: %{"type" => "Like", "published" => created_at}} = act
} }
end end
def to_map(%Activity{data: %{"type" => "Follow", "object" => followed_id}} = activity, %{user: user} = opts) do def to_map(
created_at = activity.data["published"] || (DateTime.to_iso8601(activity.inserted_at)) %Activity{data: %{"type" => "Follow", "object" => followed_id}} = activity,
created_at = created_at |> Utils.date_to_asctime %{user: user} = opts
) do
created_at = activity.data["published"] || DateTime.to_iso8601(activity.inserted_at)
created_at = created_at |> Utils.date_to_asctime()
followed = User.get_cached_by_ap_id(followed_id) followed = User.get_cached_by_ap_id(followed_id)
text = "#{user.nickname} started following #{followed.nickname}" text = "#{user.nickname} started following #{followed.nickname}"
%{ %{
"id" => activity.id, "id" => activity.id,
"user" => UserView.render("show.json", %{user: user, for: opts[:for]}), "user" => UserView.render("show.json", %{user: user, for: opts[:for]}),
@ -79,10 +89,16 @@ def to_map(%Activity{data: %{"type" => "Follow", "object" => followed_id}} = act
# TODO: # TODO:
# Make this more proper. Just a placeholder to not break the frontend. # Make this more proper. Just a placeholder to not break the frontend.
def to_map(%Activity{data: %{"type" => "Undo", "published" => created_at, "object" => undid_activity }} = activity, %{user: user} = opts) do def to_map(
created_at = created_at |> Utils.date_to_asctime %Activity{
data: %{"type" => "Undo", "published" => created_at, "object" => undid_activity}
} = activity,
%{user: user} = opts
) do
created_at = created_at |> Utils.date_to_asctime()
text = "#{user.nickname} undid the action at #{undid_activity}" text = "#{user.nickname} undid the action at #{undid_activity}"
%{ %{
"id" => activity.id, "id" => activity.id,
"user" => UserView.render("show.json", %{user: user, for: opts[:for]}), "user" => UserView.render("show.json", %{user: user, for: opts[:for]}),
@ -98,8 +114,12 @@ def to_map(%Activity{data: %{"type" => "Undo", "published" => created_at, "objec
} }
end end
def to_map(%Activity{data: %{"type" => "Delete", "published" => created_at, "object" => _ }} = activity, %{user: user} = opts) do def to_map(
created_at = created_at |> Utils.date_to_asctime %Activity{data: %{"type" => "Delete", "published" => created_at, "object" => _}} =
activity,
%{user: user} = opts
) do
created_at = created_at |> Utils.date_to_asctime()
%{ %{
"id" => activity.id, "id" => activity.id,
@ -107,7 +127,7 @@ def to_map(%Activity{data: %{"type" => "Delete", "published" => created_at, "obj
"user" => UserView.render("show.json", %{user: user, for: opts[:for]}), "user" => UserView.render("show.json", %{user: user, for: opts[:for]}),
"attentions" => [], "attentions" => [],
"statusnet_html" => "deleted notice {{tag", "statusnet_html" => "deleted notice {{tag",
"text" => "deleted notice {{tag" , "text" => "deleted notice {{tag",
"is_local" => activity.local, "is_local" => activity.local,
"is_post_verb" => false, "is_post_verb" => false,
"created_at" => created_at, "created_at" => created_at,
@ -117,8 +137,11 @@ def to_map(%Activity{data: %{"type" => "Delete", "published" => created_at, "obj
} }
end end
def to_map(%Activity{data: %{"object" => %{"content" => content} = object}} = activity, %{user: user} = opts) do def to_map(
created_at = object["published"] |> Utils.date_to_asctime %Activity{data: %{"object" => %{"content" => content} = object}} = activity,
%{user: user} = opts
) do
created_at = object["published"] |> Utils.date_to_asctime()
like_count = object["like_count"] || 0 like_count = object["like_count"] || 0
announcement_count = object["announcement_count"] || 0 announcement_count = object["announcement_count"] || 0
favorited = opts[:for] && opts[:for].ap_id in (object["likes"] || []) favorited = opts[:for] && opts[:for].ap_id in (object["likes"] || [])
@ -126,10 +149,11 @@ def to_map(%Activity{data: %{"object" => %{"content" => content} = object}} = ac
mentions = opts[:mentioned] || [] mentions = opts[:mentioned] || []
attentions = activity.recipients attentions =
|> Enum.map(fn (ap_id) -> Enum.find(mentions, fn(user) -> ap_id == user.ap_id end) end) activity.recipients
|> Enum.filter(&(&1)) |> Enum.map(fn ap_id -> Enum.find(mentions, fn user -> ap_id == user.ap_id end) end)
|> Enum.map(fn (user) -> UserView.render("show.json", %{user: user, for: opts[:for]}) end) |> Enum.filter(& &1)
|> Enum.map(fn user -> UserView.render("show.json", %{user: user, for: opts[:for]}) end)
conversation_id = conversation_id(activity) conversation_id = conversation_id(activity)
@ -139,14 +163,17 @@ def to_map(%Activity{data: %{"object" => %{"content" => content} = object}} = ac
tags = if possibly_sensitive, do: Enum.uniq(["nsfw" | tags]), else: tags tags = if possibly_sensitive, do: Enum.uniq(["nsfw" | tags]), else: tags
summary = activity.data["object"]["summary"] summary = activity.data["object"]["summary"]
content = if !!summary and summary != "" do
"<span>#{activity.data["object"]["summary"]}</span><br />#{content}</span>"
else
content
end
html = HtmlSanitizeEx.basic_html(content) content =
|> Formatter.emojify(object["emoji"]) if !!summary and summary != "" do
"<span>#{activity.data["object"]["summary"]}</span><br />#{content}</span>"
else
content
end
html =
HtmlSanitizeEx.basic_html(content)
|> Formatter.emojify(object["emoji"])
%{ %{
"id" => activity.id, "id" => activity.id,
@ -175,7 +202,8 @@ def to_map(%Activity{data: %{"object" => %{"content" => content} = object}} = ac
def conversation_id(activity) do def conversation_id(activity) do
with context when not is_nil(context) <- activity.data["context"] do with context when not is_nil(context) <- activity.data["context"] do
TwitterAPI.context_to_conversation_id(context) TwitterAPI.context_to_conversation_id(context)
else _e -> nil else
_e -> nil
end end
end end

View file

@ -1,15 +1,18 @@
defmodule Pleroma.Web.TwitterAPI.Representers.BaseRepresenter do defmodule Pleroma.Web.TwitterAPI.Representers.BaseRepresenter do
defmacro __using__(_opts) do defmacro __using__(_opts) do
quote do quote do
def to_json(object) do to_json(object, %{}) end def to_json(object) do
to_json(object, %{})
end
def to_json(object, options) do def to_json(object, options) do
object object
|> to_map(options) |> to_map(options)
|> Jason.encode! |> Jason.encode!()
end end
def enum_to_list(enum, options) do def enum_to_list(enum, options) do
mapping = fn (el) -> to_map(el, options) end mapping = fn el -> to_map(el, options) end
Enum.map(enum, mapping) Enum.map(enum, mapping)
end end
@ -17,11 +20,14 @@ def to_map(object) do
to_map(object, %{}) to_map(object, %{})
end end
def enum_to_json(enum) do enum_to_json(enum, %{}) end def enum_to_json(enum) do
enum_to_json(enum, %{})
end
def enum_to_json(enum, options) do def enum_to_json(enum, options) do
enum enum
|> enum_to_list(options) |> enum_to_list(options)
|> Jason.encode! |> Jason.encode!()
end end
end end
end end

View file

@ -4,6 +4,7 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter do
def to_map(%Object{data: %{"url" => [url | _]}} = object, _opts) do def to_map(%Object{data: %{"url" => [url | _]}} = object, _opts) do
data = object.data data = object.data
%{ %{
url: url["href"] |> Pleroma.Web.MediaProxy.url(), url: url["href"] |> Pleroma.Web.MediaProxy.url(),
mimetype: url["mediaType"], mimetype: url["mediaType"],

View file

@ -13,37 +13,42 @@ def create_status(%User{} = user, %{"status" => _} = data) do
end end
def fetch_friend_statuses(user, opts \\ %{}) do def fetch_friend_statuses(user, opts \\ %{}) do
opts = opts opts =
|> Map.put("blocking_user", user) opts
|> Map.put("user", user) |> Map.put("blocking_user", user)
|> Map.put("type", ["Create", "Announce", "Follow", "Like"]) |> Map.put("user", user)
|> Map.put("type", ["Create", "Announce", "Follow", "Like"])
ActivityPub.fetch_activities([user.ap_id | user.following], opts) ActivityPub.fetch_activities([user.ap_id | user.following], opts)
|> activities_to_statuses(%{for: user}) |> activities_to_statuses(%{for: user})
end end
def fetch_public_statuses(user, opts \\ %{}) do def fetch_public_statuses(user, opts \\ %{}) do
opts = opts opts =
|> Map.put("local_only", true) opts
|> Map.put("blocking_user", user) |> Map.put("local_only", true)
|> Map.put("type", ["Create", "Announce", "Follow"]) |> Map.put("blocking_user", user)
|> Map.put("type", ["Create", "Announce", "Follow"])
ActivityPub.fetch_public_activities(opts) ActivityPub.fetch_public_activities(opts)
|> activities_to_statuses(%{for: user}) |> activities_to_statuses(%{for: user})
end end
def fetch_public_and_external_statuses(user, opts \\ %{}) do def fetch_public_and_external_statuses(user, opts \\ %{}) do
opts = opts opts =
|> Map.put("blocking_user", user) opts
|> Map.put("type", ["Create", "Announce", "Follow"]) |> Map.put("blocking_user", user)
|> Map.put("type", ["Create", "Announce", "Follow"])
ActivityPub.fetch_public_activities(opts) ActivityPub.fetch_public_activities(opts)
|> activities_to_statuses(%{for: user}) |> activities_to_statuses(%{for: user})
end end
def fetch_user_statuses(user, opts \\ %{}) do def fetch_user_statuses(user, opts \\ %{}) do
opts = opts opts =
|> Map.put("type", ["Create"]) opts
|> Map.put("type", ["Create"])
ActivityPub.fetch_public_activities(opts) ActivityPub.fetch_public_activities(opts)
|> activities_to_statuses(%{for: user}) |> activities_to_statuses(%{for: user})
end end
@ -55,12 +60,16 @@ def fetch_mentions(user, opts \\ %{}) do
def fetch_conversation(user, id) do def fetch_conversation(user, id) do
with context when is_binary(context) <- conversation_id_to_context(id), with context when is_binary(context) <- conversation_id_to_context(id),
activities <- ActivityPub.fetch_activities_for_context(context, %{"blocking_user" => user, "user" => user}), activities <-
statuses <- activities |> activities_to_statuses(%{for: user}) ActivityPub.fetch_activities_for_context(context, %{
do "blocking_user" => user,
"user" => user
}),
statuses <- activities |> activities_to_statuses(%{for: user}) do
statuses statuses
else _e -> else
[] _e ->
[]
end end
end end
@ -74,8 +83,7 @@ def fetch_status(user, id) do
def follow(%User{} = follower, params) do def follow(%User{} = follower, params) do
with {:ok, %User{} = followed} <- get_user(params), with {:ok, %User{} = followed} <- get_user(params),
{:ok, follower} <- User.follow(follower, followed), {:ok, follower} <- User.follow(follower, followed),
{:ok, activity} <- ActivityPub.follow(follower, followed) {:ok, activity} <- ActivityPub.follow(follower, followed) do
do
{:ok, follower, followed, activity} {:ok, follower, followed, activity}
else else
err -> err err -> err
@ -83,16 +91,17 @@ def follow(%User{} = follower, params) do
end end
def unfollow(%User{} = follower, params) do def unfollow(%User{} = follower, params) do
with { :ok, %User{} = unfollowed } <- get_user(params), with {:ok, %User{} = unfollowed} <- get_user(params),
{ :ok, follower, follow_activity } <- User.unfollow(follower, unfollowed), {:ok, follower, follow_activity} <- User.unfollow(follower, unfollowed),
{ :ok, _activity } <- ActivityPub.insert(%{ {:ok, _activity} <-
"type" => "Undo", ActivityPub.insert(%{
"actor" => follower.ap_id, "type" => "Undo",
"object" => follow_activity.data["id"], # get latest Follow for these users "actor" => follower.ap_id,
"published" => make_date() # get latest Follow for these users
}) "object" => follow_activity.data["id"],
do "published" => make_date()
{ :ok, follower, unfollowed } }) do
{:ok, follower, unfollowed}
else else
err -> err err -> err
end end
@ -100,8 +109,7 @@ def unfollow(%User{} = follower, params) do
def block(%User{} = blocker, params) do def block(%User{} = blocker, params) do
with {:ok, %User{} = blocked} <- get_user(params), with {:ok, %User{} = blocked} <- get_user(params),
{:ok, blocker} <- User.block(blocker, blocked) {:ok, blocker} <- User.block(blocker, blocked) do
do
{:ok, blocker, blocked} {:ok, blocker, blocked}
else else
err -> err err -> err
@ -110,8 +118,7 @@ def block(%User{} = blocker, params) do
def unblock(%User{} = blocker, params) do def unblock(%User{} = blocker, params) do
with {:ok, %User{} = blocked} <- get_user(params), with {:ok, %User{} = blocked} <- get_user(params),
{:ok, blocker} <- User.unblock(blocker, blocked) {:ok, blocker} <- User.unblock(blocker, blocked) do
do
{:ok, blocker, blocked} {:ok, blocker, blocked}
else else
err -> err err -> err
@ -163,13 +170,15 @@ def upload(%Plug.Upload{} = file, format \\ "xml") do
<atom:link rel="enclosure" href="#{href}" type="#{type}"></atom:link> <atom:link rel="enclosure" href="#{href}" type="#{type}"></atom:link>
</rsp> </rsp>
""" """
"json" -> "json" ->
%{ %{
media_id: object.id, media_id: object.id,
media_id_string: "#{object.id}}", media_id_string: "#{object.id}}",
media_url: href, media_url: href,
size: 0 size: 0
} |> Jason.encode! }
|> Jason.encode!()
end end
end end
@ -189,9 +198,11 @@ def register_user(params) do
{:ok, user} {:ok, user}
else else
{:error, changeset} -> {:error, changeset} ->
errors = Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end) errors =
|> Jason.encode! Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
{:error, %{error: errors}} |> Jason.encode!()
{:error, %{error: errors}}
end end
end end
@ -209,16 +220,20 @@ def get_user(user \\ nil, params) do
case target = get_by_id_or_nickname(user_id) do case target = get_by_id_or_nickname(user_id) do
nil -> nil ->
{:error, "No user with such user_id"} {:error, "No user with such user_id"}
_ -> _ ->
{:ok, target} {:ok, target}
end end
%{"screen_name" => nickname} -> %{"screen_name" => nickname} ->
case target = Repo.get_by(User, nickname: nickname) do case target = Repo.get_by(User, nickname: nickname) do
nil -> nil ->
{:error, "No user with such screen_name"} {:error, "No user with such screen_name"}
_ -> _ ->
{:ok, target} {:ok, target}
end end
_ -> _ ->
if user do if user do
{:ok, user} {:ok, user}
@ -229,6 +244,7 @@ def get_user(user \\ nil, params) do
end end
defp parse_int(string, default) defp parse_int(string, default)
defp parse_int(string, default) when is_binary(string) do defp parse_int(string, default) when is_binary(string) do
with {n, _} <- Integer.parse(string) do with {n, _} <- Integer.parse(string) do
n n
@ -236,6 +252,7 @@ defp parse_int(string, default) when is_binary(string) do
_e -> default _e -> default
end end
end end
defp parse_int(_, default), do: default defp parse_int(_, default), do: default
def search(user, %{"q" => query} = params) do def search(user, %{"q" => query} = params) do
@ -243,19 +260,28 @@ def search(user, %{"q" => query} = params) do
page = parse_int(params["page"], 1) page = parse_int(params["page"], 1)
offset = (page - 1) * limit offset = (page - 1) * limit
q = from a in Activity, q =
where: fragment("?->>'type' = 'Create'", a.data), from(
where: fragment("to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)", a.data, ^query), a in Activity,
limit: ^limit, where: fragment("?->>'type' = 'Create'", a.data),
offset: ^offset, where:
order_by: [desc: :inserted_at] # this one isn't indexed so psql won't take the wrong index. fragment(
"to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
a.data,
^query
),
limit: ^limit,
offset: ^offset,
# this one isn't indexed so psql won't take the wrong index.
order_by: [desc: :inserted_at]
)
activities = Repo.all(q) activities = Repo.all(q)
activities_to_statuses(activities, %{for: user}) activities_to_statuses(activities, %{for: user})
end end
defp activities_to_statuses(activities, opts) do defp activities_to_statuses(activities, opts) do
Enum.map(activities, fn(activity) -> Enum.map(activities, fn activity ->
activity_to_status(activity, opts) activity_to_status(activity, opts)
end) end)
end end
@ -266,7 +292,10 @@ defp activity_to_status(%Activity{data: %{"type" => "Like"}} = activity, opts) d
user = User.get_cached_by_ap_id(actor) user = User.get_cached_by_ap_id(actor)
[liked_activity] = Activity.all_by_object_ap_id(activity.data["object"]) [liked_activity] = Activity.all_by_object_ap_id(activity.data["object"])
ActivityRepresenter.to_map(activity, Map.merge(opts, %{user: user, liked_activity: liked_activity})) ActivityRepresenter.to_map(
activity,
Map.merge(opts, %{user: user, liked_activity: liked_activity})
)
end end
# For announces, fetch the announced activity and the user. # For announces, fetch the announced activity and the user.
@ -276,7 +305,10 @@ defp activity_to_status(%Activity{data: %{"type" => "Announce"}} = activity, opt
[announced_activity] = Activity.all_by_object_ap_id(activity.data["object"]) [announced_activity] = Activity.all_by_object_ap_id(activity.data["object"])
announced_actor = User.get_cached_by_ap_id(announced_activity.data["actor"]) announced_actor = User.get_cached_by_ap_id(announced_activity.data["actor"])
ActivityRepresenter.to_map(activity, Map.merge(opts, %{users: [user, announced_actor], announced_activity: announced_activity})) ActivityRepresenter.to_map(
activity,
Map.merge(opts, %{users: [user, announced_actor], announced_activity: announced_activity})
)
end end
defp activity_to_status(%Activity{data: %{"type" => "Delete"}} = activity, opts) do defp activity_to_status(%Activity{data: %{"type" => "Delete"}} = activity, opts) do
@ -289,32 +321,41 @@ defp activity_to_status(activity, opts) do
actor = get_in(activity.data, ["actor"]) actor = get_in(activity.data, ["actor"])
user = User.get_cached_by_ap_id(actor) user = User.get_cached_by_ap_id(actor)
# mentioned_users = Repo.all(from user in User, where: user.ap_id in ^activity.data["to"]) # mentioned_users = Repo.all(from user in User, where: user.ap_id in ^activity.data["to"])
mentioned_users = Enum.map(activity.recipients || [], fn (ap_id) -> mentioned_users =
if ap_id do Enum.map(activity.recipients || [], fn ap_id ->
User.get_cached_by_ap_id(ap_id) if ap_id do
else User.get_cached_by_ap_id(ap_id)
nil else
end nil
end) end
|> Enum.filter(&(&1)) end)
|> Enum.filter(& &1)
ActivityRepresenter.to_map(activity, Map.merge(opts, %{user: user, mentioned: mentioned_users})) ActivityRepresenter.to_map(
activity,
Map.merge(opts, %{user: user, mentioned: mentioned_users})
)
end end
defp make_date do defp make_date do
DateTime.utc_now() |> DateTime.to_iso8601 DateTime.utc_now() |> DateTime.to_iso8601()
end end
def context_to_conversation_id(context) do def context_to_conversation_id(context) do
with %Object{id: id} <- Object.get_cached_by_ap_id(context) do with %Object{id: id} <- Object.get_cached_by_ap_id(context) do
id id
else _e -> else
_e ->
changeset = Object.context_mapping(context) changeset = Object.context_mapping(context)
case Repo.insert(changeset) do case Repo.insert(changeset) do
{:ok, %{id: id}} -> id {:ok, %{id: id}} ->
id
# This should be solved by an upsert, but it seems ecto # This should be solved by an upsert, but it seems ecto
# has problems accessing the constraint inside the jsonb. # has problems accessing the constraint inside the jsonb.
{:error, _} -> Object.get_cached_by_ap_id(context).id {:error, _} ->
Object.get_cached_by_ap_id(context).id
end end
end end
end end
@ -322,8 +363,9 @@ def context_to_conversation_id(context) do
def conversation_id_to_context(id) do def conversation_id_to_context(id) do
with %Object{data: %{"id" => context}} <- Repo.get(Object, id) do with %Object{data: %{"id" => context}} <- Repo.get(Object, id) do
context context
else _e -> else
{:error, "No such conversation"} _e ->
{:error, "No such conversation"}
end end
end end
@ -331,12 +373,15 @@ def get_external_profile(for_user, uri) do
with %User{} = user <- User.get_or_fetch(uri) do with %User{} = user <- User.get_or_fetch(uri) do
spawn(fn -> spawn(fn ->
with url <- user.info["topic"], with url <- user.info["topic"],
{:ok, %{body: body}} <- @httpoison.get(url, [], follow_redirect: true, timeout: 10000, recv_timeout: 20000) do {:ok, %{body: body}} <-
@httpoison.get(url, [], follow_redirect: true, timeout: 10000, recv_timeout: 20000) do
OStatus.handle_incoming(body) OStatus.handle_incoming(body)
end end
end) end)
{:ok, UserView.render("show.json", %{user: user, for: for_user})} {:ok, UserView.render("show.json", %{user: user, for: for_user})}
else _e -> else
_e ->
{:error, "Couldn't find user"} {:error, "Couldn't find user"}
end end
end end

View file

@ -16,7 +16,8 @@ def verify_credentials(%{assigns: %{user: user}} = conn, _params) do
def status_update(%{assigns: %{user: user}} = conn, %{"status" => _} = status_data) do def status_update(%{assigns: %{user: user}} = conn, %{"status" => _} = status_data) do
with media_ids <- extract_media_ids(status_data), with media_ids <- extract_media_ids(status_data),
{:ok, activity} <- TwitterAPI.create_status(user, Map.put(status_data, "media_ids", media_ids)) do {:ok, activity} <-
TwitterAPI.create_status(user, Map.put(status_data, "media_ids", media_ids)) do
conn conn
|> json(ActivityRepresenter.to_map(activity, %{user: user})) |> json(ActivityRepresenter.to_map(activity, %{user: user}))
else else
@ -35,10 +36,10 @@ defp empty_status_reply(conn) do
defp extract_media_ids(status_data) do defp extract_media_ids(status_data) do
with media_ids when not is_nil(media_ids) <- status_data["media_ids"], with media_ids when not is_nil(media_ids) <- status_data["media_ids"],
split_ids <- String.split(media_ids, ","), split_ids <- String.split(media_ids, ","),
clean_ids <- Enum.reject(split_ids, fn (id) -> String.length(id) == 0 end) clean_ids <- Enum.reject(split_ids, fn id -> String.length(id) == 0 end) do
do clean_ids
clean_ids else
else _e -> [] _e -> []
end end
end end
@ -69,9 +70,9 @@ def friends_timeline(%{assigns: %{user: user}} = conn, params) do
def show_user(conn, params) do def show_user(conn, params) do
with {:ok, shown} <- TwitterAPI.get_user(params) do with {:ok, shown} <- TwitterAPI.get_user(params) do
if user = conn.assigns.user do if user = conn.assigns.user do
render conn, UserView, "show.json", %{user: shown, for: user} render(conn, UserView, "show.json", %{user: shown, for: user})
else else
render conn, UserView, "show.json", %{user: shown} render(conn, UserView, "show.json", %{user: shown})
end end
else else
{:error, msg} -> {:error, msg} ->
@ -83,9 +84,11 @@ def user_timeline(%{assigns: %{user: user}} = conn, params) do
case TwitterAPI.get_user(user, params) do case TwitterAPI.get_user(user, params) do
{:ok, target_user} -> {:ok, target_user} ->
params = Map.merge(params, %{"actor_id" => target_user.ap_id, "whole_db" => true}) params = Map.merge(params, %{"actor_id" => target_user.ap_id, "whole_db" => true})
statuses = TwitterAPI.fetch_user_statuses(user, params) statuses = TwitterAPI.fetch_user_statuses(user, params)
conn conn
|> json_reply(200, statuses |> Jason.encode!) |> json_reply(200, statuses |> Jason.encode!())
{:error, msg} -> {:error, msg} ->
bad_request_reply(conn, msg) bad_request_reply(conn, msg)
end end
@ -103,29 +106,36 @@ def follow(%{assigns: %{user: user}} = conn, params) do
case TwitterAPI.follow(user, params) do case TwitterAPI.follow(user, params) do
{:ok, user, followed, _activity} -> {:ok, user, followed, _activity} ->
render(conn, UserView, "show.json", %{user: followed, for: user}) render(conn, UserView, "show.json", %{user: followed, for: user})
{:error, msg} -> forbidden_json_reply(conn, msg)
{:error, msg} ->
forbidden_json_reply(conn, msg)
end end
end end
def block(%{assigns: %{user: user}} = conn, params) do def block(%{assigns: %{user: user}} = conn, params) do
case TwitterAPI.block(user, params) do case TwitterAPI.block(user, params) do
{:ok, user, blocked} -> {:ok, user, blocked} ->
render conn, UserView, "show.json", %{user: blocked, for: user} render(conn, UserView, "show.json", %{user: blocked, for: user})
{:error, msg} -> forbidden_json_reply(conn, msg)
{:error, msg} ->
forbidden_json_reply(conn, msg)
end end
end end
def unblock(%{assigns: %{user: user}} = conn, params) do def unblock(%{assigns: %{user: user}} = conn, params) do
case TwitterAPI.unblock(user, params) do case TwitterAPI.unblock(user, params) do
{:ok, user, blocked} -> {:ok, user, blocked} ->
render conn, UserView, "show.json", %{user: blocked, for: user} render(conn, UserView, "show.json", %{user: blocked, for: user})
{:error, msg} -> forbidden_json_reply(conn, msg)
{:error, msg} ->
forbidden_json_reply(conn, msg)
end end
end end
def delete_post(%{assigns: %{user: user}} = conn, %{"id" => id}) do def delete_post(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {:ok, delete} <- CommonAPI.delete(id, user) do with {:ok, delete} <- CommonAPI.delete(id, user) do
json = ActivityRepresenter.to_json(delete, %{user: user, for: user}) json = ActivityRepresenter.to_json(delete, %{user: user, for: user})
conn conn
|> json_reply(200, json) |> json_reply(200, json)
end end
@ -135,14 +145,16 @@ def unfollow(%{assigns: %{user: user}} = conn, params) do
case TwitterAPI.unfollow(user, params) do case TwitterAPI.unfollow(user, params) do
{:ok, user, unfollowed} -> {:ok, user, unfollowed} ->
render(conn, UserView, "show.json", %{user: unfollowed, for: user}) render(conn, UserView, "show.json", %{user: unfollowed, for: user})
{:error, msg} -> forbidden_json_reply(conn, msg)
{:error, msg} ->
forbidden_json_reply(conn, msg)
end end
end end
def fetch_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do def fetch_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Repo.get(Activity, id), with %Activity{} = activity <- Repo.get(Activity, id),
true <- ActivityPub.visible_for_user?(activity, user) do true <- ActivityPub.visible_for_user?(activity, user) do
render conn, ActivityView, "activity.json", %{activity: activity, for: user} render(conn, ActivityView, "activity.json", %{activity: activity, for: user})
end end
end end
@ -156,6 +168,7 @@ def fetch_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
def upload(conn, %{"media" => media}) do def upload(conn, %{"media" => media}) do
response = TwitterAPI.upload(media) response = TwitterAPI.upload(media)
conn conn
|> put_resp_content_type("application/atom+xml") |> put_resp_content_type("application/atom+xml")
|> send_resp(200, response) |> send_resp(200, response)
@ -163,12 +176,14 @@ def upload(conn, %{"media" => media}) do
def upload_json(conn, %{"media" => media}) do def upload_json(conn, %{"media" => media}) do
response = TwitterAPI.upload(media, "json") response = TwitterAPI.upload(media, "json")
conn conn
|> json_reply(200, response) |> json_reply(200, response)
end end
def get_by_id_or_ap_id(id) do def get_by_id_or_ap_id(id) do
activity = Repo.get(Activity, id) || Activity.get_create_activity_by_object_ap_id(id) activity = Repo.get(Activity, id) || Activity.get_create_activity_by_object_ap_id(id)
if activity.data["type"] == "Create" do if activity.data["type"] == "Create" do
activity activity
else else
@ -199,8 +214,8 @@ def register(conn, params) do
render(conn, UserView, "show.json", %{user: user}) render(conn, UserView, "show.json", %{user: user})
else else
{:error, errors} -> {:error, errors} ->
conn conn
|> json_reply(400, Jason.encode!(errors)) |> json_reply(400, Jason.encode!(errors))
end end
end end
@ -219,8 +234,9 @@ def update_banner(%{assigns: %{user: user}} = conn, params) do
change <- User.info_changeset(user, %{info: new_info}), change <- User.info_changeset(user, %{info: new_info}),
{:ok, user} <- User.update_and_set_cache(change) do {:ok, user} <- User.update_and_set_cache(change) do
CommonAPI.update(user) CommonAPI.update(user)
%{"url" => [ %{ "href" => href } | _ ]} = object.data %{"url" => [%{"href" => href} | _]} = object.data
response = %{ url: href } |> Jason.encode! response = %{url: href} |> Jason.encode!()
conn conn
|> json_reply(200, response) |> json_reply(200, response)
end end
@ -231,8 +247,9 @@ def update_background(%{assigns: %{user: user}} = conn, params) do
new_info <- Map.put(user.info, "background", object.data), new_info <- Map.put(user.info, "background", object.data),
change <- User.info_changeset(user, %{info: new_info}), change <- User.info_changeset(user, %{info: new_info}),
{:ok, _user} <- User.update_and_set_cache(change) do {:ok, _user} <- User.update_and_set_cache(change) do
%{"url" => [ %{ "href" => href } | _ ]} = object.data %{"url" => [%{"href" => href} | _]} = object.data
response = %{ url: href } |> Jason.encode! response = %{url: href} |> Jason.encode!()
conn conn
|> json_reply(200, response) |> json_reply(200, response)
end end
@ -285,9 +302,10 @@ def friends(conn, params) do
def friends_ids(%{assigns: %{user: user}} = conn, _params) do def friends_ids(%{assigns: %{user: user}} = conn, _params) do
with {:ok, friends} <- User.get_friends(user) do with {:ok, friends} <- User.get_friends(user) do
ids = friends ids =
|> Enum.map(fn x -> x.id end) friends
|> Jason.encode! |> Enum.map(fn x -> x.id end)
|> Jason.encode!()
json(conn, ids) json(conn, ids)
else else
@ -300,11 +318,12 @@ def empty_array(conn, _params) do
end end
def update_profile(%{assigns: %{user: user}} = conn, params) do def update_profile(%{assigns: %{user: user}} = conn, params) do
params = if bio = params["description"] do params =
Map.put(params, "bio", bio) if bio = params["description"] do
else Map.put(params, "bio", bio)
params else
end params
end
with changeset <- User.update_changeset(user, params), with changeset <- User.update_changeset(user, params),
{:ok, user} <- User.update_and_set_cache(changeset) do {:ok, user} <- User.update_and_set_cache(changeset) do
@ -339,6 +358,6 @@ defp forbidden_json_reply(conn, error_message) do
end end
defp error_json(conn, error_message) do defp error_json(conn, error_message) do
%{"error" => error_message, "request" => conn.request_path} |> Jason.encode! %{"error" => error_message, "request" => conn.request_path} |> Jason.encode!()
end end
end end

View file

@ -10,7 +10,7 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do
def render("activity.json", %{activity: %{data: %{"type" => "Announce"}} = activity} = opts) do def render("activity.json", %{activity: %{data: %{"type" => "Announce"}} = activity} = opts) do
user = User.get_by_ap_id(activity.data["actor"]) user = User.get_by_ap_id(activity.data["actor"])
created_at = activity.data["published"] |> Utils.date_to_asctime created_at = activity.data["published"] |> Utils.date_to_asctime()
announced_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"]) announced_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
text = "#{user.nickname} retweeted a status." text = "#{user.nickname} retweeted a status."
@ -37,8 +37,10 @@ def render("activity.json", %{activity: %{data: %{"type" => "Announce"}} = activ
def render("activity.json", %{activity: %{data: %{"type" => "Like"}} = activity} = opts) do def render("activity.json", %{activity: %{data: %{"type" => "Like"}} = activity} = opts) do
user = User.get_cached_by_ap_id(activity.data["actor"]) user = User.get_cached_by_ap_id(activity.data["actor"])
liked_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"]) liked_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
created_at = activity.data["published"]
|> Utils.date_to_asctime created_at =
activity.data["published"]
|> Utils.date_to_asctime()
text = "#{user.nickname} favorited a status." text = "#{user.nickname} favorited a status."
@ -57,20 +59,24 @@ def render("activity.json", %{activity: %{data: %{"type" => "Like"}} = activity}
} }
end end
def render("activity.json", %{activity: %{data: %{"type" => "Create", "object" => object}} = activity} = opts) do def render(
"activity.json",
%{activity: %{data: %{"type" => "Create", "object" => object}} = activity} = opts
) do
actor = get_in(activity.data, ["actor"]) actor = get_in(activity.data, ["actor"])
user = User.get_cached_by_ap_id(actor) user = User.get_cached_by_ap_id(actor)
created_at = object["published"] |> Utils.date_to_asctime created_at = object["published"] |> Utils.date_to_asctime()
like_count = object["like_count"] || 0 like_count = object["like_count"] || 0
announcement_count = object["announcement_count"] || 0 announcement_count = object["announcement_count"] || 0
favorited = opts[:for] && opts[:for].ap_id in (object["likes"] || []) favorited = opts[:for] && opts[:for].ap_id in (object["likes"] || [])
repeated = opts[:for] && opts[:for].ap_id in (object["announcements"] || []) repeated = opts[:for] && opts[:for].ap_id in (object["announcements"] || [])
attentions = activity.recipients attentions =
|> Enum.map(fn (ap_id) -> User.get_cached_by_ap_id(ap_id) end) activity.recipients
|> Enum.filter(&(&1)) |> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
|> Enum.map(fn (user) -> UserView.render("show.json", %{user: user, for: opts[:for]}) end) |> Enum.filter(& &1)
|> Enum.map(fn user -> UserView.render("show.json", %{user: user, for: opts[:for]}) end)
conversation_id = conversation_id(activity) conversation_id = conversation_id(activity)
@ -81,14 +87,17 @@ def render("activity.json", %{activity: %{data: %{"type" => "Create", "object" =
summary = activity.data["object"]["summary"] summary = activity.data["object"]["summary"]
content = object["content"] content = object["content"]
content = if !!summary and summary != "" do
"<span>#{activity.data["object"]["summary"]}</span><br />#{content}</span>"
else
content
end
html = HtmlSanitizeEx.basic_html(content) content =
|> Formatter.emojify(object["emoji"]) if !!summary and summary != "" do
"<span>#{activity.data["object"]["summary"]}</span><br />#{content}</span>"
else
content
end
html =
HtmlSanitizeEx.basic_html(content)
|> Formatter.emojify(object["emoji"])
%{ %{
"id" => activity.id, "id" => activity.id,
@ -117,7 +126,8 @@ def render("activity.json", %{activity: %{data: %{"type" => "Create", "object" =
defp conversation_id(activity) do defp conversation_id(activity) do
with context when not is_nil(context) <- activity.data["context"] do with context when not is_nil(context) <- activity.data["context"] do
TwitterAPI.context_to_conversation_id(context) TwitterAPI.context_to_conversation_id(context)
else _e -> nil else
_e -> nil
end end
end end
end end

View file

@ -14,20 +14,22 @@ def render("index.json", %{users: users, for: user}) do
def render("user.json", %{user: user = %User{}} = assigns) do def render("user.json", %{user: user = %User{}} = assigns) do
image = User.avatar_url(user) |> MediaProxy.url() image = User.avatar_url(user) |> MediaProxy.url()
{following, follows_you, statusnet_blocking} = if assigns[:for] do
{ {following, follows_you, statusnet_blocking} =
User.following?(assigns[:for], user), if assigns[:for] do
User.following?(user, assigns[:for]), {
User.blocks?(assigns[:for], user) User.following?(assigns[:for], user),
} User.following?(user, assigns[:for]),
else User.blocks?(assigns[:for], user)
{false, false, false} }
end else
{false, false, false}
end
user_info = User.get_cached_user_info(user) user_info = User.get_cached_user_info(user)
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),
"favourites_count" => 0, "favourites_count" => 0,
"followers_count" => user_info[:follower_count], "followers_count" => user_info[:follower_count],
@ -59,9 +61,14 @@ def render("user.json", %{user: user = %User{}} = assigns) do
end end
end end
def render("short.json", %{user: %User{ def render("short.json", %{
nickname: nickname, id: id, ap_id: ap_id, name: name user: %User{
}}) do nickname: nickname,
id: id,
ap_id: ap_id,
name: name
}
}) do
%{ %{
"fullname" => name, "fullname" => name,
"id" => id, "id" => id,
@ -71,6 +78,6 @@ def render("short.json", %{user: %User{
} }
end end
defp image_url(%{"url" => [ %{ "href" => href } | _ ]}), do: href defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
defp image_url(_), do: nil defp image_url(_), do: nil
end end

View file

@ -12,6 +12,6 @@ def render("500.json", _assigns) do
# In case no render clause matches or no # In case no render clause matches or no
# template is found, let's render it as 500 # template is found, let's render it as 500
def template_not_found(_template, assigns) do def template_not_found(_template, assigns) do
render "500.json", assigns render("500.json", assigns)
end end
end end

View file

@ -26,8 +26,9 @@ def controller do
def view do def view do
quote do quote do
use Phoenix.View, root: "lib/pleroma/web/templates", use Phoenix.View,
namespace: Pleroma.Web root: "lib/pleroma/web/templates",
namespace: Pleroma.Web
# Import convenience functions from controllers # Import convenience functions from controllers
import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1] import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1]
@ -59,6 +60,6 @@ defmacro __using__(which) when is_atom(which) do
end end
def base_url do def base_url do
Pleroma.Web.Endpoint.url Pleroma.Web.Endpoint.url()
end end
end end

View file

@ -8,43 +8,56 @@ defmodule Pleroma.Web.WebFinger do
require Logger require Logger
def host_meta do def host_meta do
base_url = Web.base_url base_url = Web.base_url()
{ {
:XRD, %{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"}, :XRD,
%{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"},
{ {
:Link, %{rel: "lrdd", type: "application/xrd+xml", template: "#{base_url}/.well-known/webfinger?resource={uri}"} :Link,
%{
rel: "lrdd",
type: "application/xrd+xml",
template: "#{base_url}/.well-known/webfinger?resource={uri}"
}
} }
} }
|> XmlBuilder.to_doc |> XmlBuilder.to_doc()
end end
def webfinger(resource, "JSON") do def webfinger(resource, "JSON") do
host = Pleroma.Web.Endpoint.host host = Pleroma.Web.Endpoint.host()
regex = ~r/(acct:)?(?<username>\w+)@#{host}/ regex = ~r/(acct:)?(?<username>\w+)@#{host}/
with %{"username" => username} <- Regex.named_captures(regex, resource) do with %{"username" => username} <- Regex.named_captures(regex, resource) do
user = User.get_by_nickname(username) user = User.get_by_nickname(username)
{:ok, represent_user(user, "JSON")} {:ok, represent_user(user, "JSON")}
else _e -> else
with user when not is_nil(user) <- User.get_cached_by_ap_id(resource) do _e ->
{:ok, represent_user(user, "JSON")} with user when not is_nil(user) <- User.get_cached_by_ap_id(resource) do
else _e -> {:ok, represent_user(user, "JSON")}
{:error, "Couldn't find user"} else
end _e ->
{:error, "Couldn't find user"}
end
end end
end end
def webfinger(resource, "XML") do def webfinger(resource, "XML") do
host = Pleroma.Web.Endpoint.host host = Pleroma.Web.Endpoint.host()
regex = ~r/(acct:)?(?<username>\w+)@#{host}/ regex = ~r/(acct:)?(?<username>\w+)@#{host}/
with %{"username" => username} <- Regex.named_captures(regex, resource) do with %{"username" => username} <- Regex.named_captures(regex, resource) do
user = User.get_by_nickname(username) user = User.get_by_nickname(username)
{:ok, represent_user(user, "XML")} {:ok, represent_user(user, "XML")}
else _e -> else
with user when not is_nil(user) <- User.get_cached_by_ap_id(resource) do _e ->
{:ok, represent_user(user, "XML")} with user when not is_nil(user) <- User.get_cached_by_ap_id(resource) do
else _e -> {:ok, represent_user(user, "XML")}
{:error, "Couldn't find user"} else
end _e ->
{:error, "Couldn't find user"}
end
end end
end end
@ -52,16 +65,28 @@ def represent_user(user, "JSON") do
{:ok, user} = ensure_keys_present(user) {:ok, user} = ensure_keys_present(user)
{:ok, _private, public} = Salmon.keys_from_pem(user.info["keys"]) {:ok, _private, public} = Salmon.keys_from_pem(user.info["keys"])
magic_key = Salmon.encode_key(public) magic_key = Salmon.encode_key(public)
%{ %{
"subject" => "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host}", "subject" => "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}",
"aliases" => [user.ap_id], "aliases" => [user.ap_id],
"links" => [ "links" => [
%{"rel" => "http://schemas.google.com/g/2010#updates-from", "type" => "application/atom+xml", "href" => OStatus.feed_path(user)}, %{
%{"rel" => "http://webfinger.net/rel/profile-page", "type" => "text/html", "href" => user.ap_id}, "rel" => "http://schemas.google.com/g/2010#updates-from",
"type" => "application/atom+xml",
"href" => OStatus.feed_path(user)
},
%{
"rel" => "http://webfinger.net/rel/profile-page",
"type" => "text/html",
"href" => user.ap_id
},
%{"rel" => "salmon", "href" => OStatus.salmon_path(user)}, %{"rel" => "salmon", "href" => OStatus.salmon_path(user)},
%{"rel" => "magic-public-key", "href" => "data:application/magic-public-key,#{magic_key}"}, %{"rel" => "magic-public-key", "href" => "data:application/magic-public-key,#{magic_key}"},
%{"rel" => "self", "type" => "application/activity+json", "href" => user.ap_id}, %{"rel" => "self", "type" => "application/activity+json", "href" => user.ap_id},
%{"rel" => "http://ostatus.org/schema/1.0/subscribe", "template" => OStatus.remote_follow_path()} %{
"rel" => "http://ostatus.org/schema/1.0/subscribe",
"template" => OStatus.remote_follow_path()
}
] ]
} }
end end
@ -70,30 +95,42 @@ def represent_user(user, "XML") do
{:ok, user} = ensure_keys_present(user) {:ok, user} = ensure_keys_present(user)
{:ok, _private, public} = Salmon.keys_from_pem(user.info["keys"]) {:ok, _private, public} = Salmon.keys_from_pem(user.info["keys"])
magic_key = Salmon.encode_key(public) magic_key = Salmon.encode_key(public)
{ {
:XRD, %{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"}, :XRD,
%{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"},
[ [
{:Subject, "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host}"}, {:Subject, "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}"},
{:Alias, user.ap_id}, {:Alias, user.ap_id},
{:Link, %{rel: "http://schemas.google.com/g/2010#updates-from", type: "application/atom+xml", href: OStatus.feed_path(user)}}, {:Link,
{:Link, %{rel: "http://webfinger.net/rel/profile-page", type: "text/html", href: user.ap_id}}, %{
rel: "http://schemas.google.com/g/2010#updates-from",
type: "application/atom+xml",
href: OStatus.feed_path(user)
}},
{:Link,
%{rel: "http://webfinger.net/rel/profile-page", type: "text/html", href: user.ap_id}},
{:Link, %{rel: "salmon", href: OStatus.salmon_path(user)}}, {:Link, %{rel: "salmon", href: OStatus.salmon_path(user)}},
{:Link, %{rel: "magic-public-key", href: "data:application/magic-public-key,#{magic_key}"}}, {:Link,
%{rel: "magic-public-key", href: "data:application/magic-public-key,#{magic_key}"}},
{:Link, %{rel: "self", type: "application/activity+json", href: user.ap_id}}, {:Link, %{rel: "self", type: "application/activity+json", href: user.ap_id}},
{:Link, %{rel: "http://ostatus.org/schema/1.0/subscribe", template: OStatus.remote_follow_path()}} {:Link,
%{rel: "http://ostatus.org/schema/1.0/subscribe", template: OStatus.remote_follow_path()}}
] ]
} }
|> XmlBuilder.to_doc |> XmlBuilder.to_doc()
end end
# This seems a better fit in Salmon # This seems a better fit in Salmon
def ensure_keys_present(user) do def ensure_keys_present(user) do
info = user.info || %{} info = user.info || %{}
if info["keys"] do if info["keys"] do
{:ok, user} {:ok, user}
else else
{:ok, pem} = Salmon.generate_rsa_pem {:ok, pem} = Salmon.generate_rsa_pem()
info = Map.put(info, "keys", pem) info = Map.put(info, "keys", pem)
Ecto.Changeset.change(user, info: info) Ecto.Changeset.change(user, info: info)
|> User.update_and_set_cache() |> User.update_and_set_cache()
end end
@ -102,11 +139,28 @@ def ensure_keys_present(user) do
defp webfinger_from_xml(doc) do defp webfinger_from_xml(doc) do
magic_key = XML.string_from_xpath(~s{//Link[@rel="magic-public-key"]/@href}, doc) magic_key = XML.string_from_xpath(~s{//Link[@rel="magic-public-key"]/@href}, doc)
"data:application/magic-public-key," <> magic_key = magic_key "data:application/magic-public-key," <> magic_key = magic_key
topic = XML.string_from_xpath(~s{//Link[@rel="http://schemas.google.com/g/2010#updates-from"]/@href}, doc)
topic =
XML.string_from_xpath(
~s{//Link[@rel="http://schemas.google.com/g/2010#updates-from"]/@href},
doc
)
subject = XML.string_from_xpath("//Subject", doc) subject = XML.string_from_xpath("//Subject", doc)
salmon = XML.string_from_xpath(~s{//Link[@rel="salmon"]/@href}, doc) salmon = XML.string_from_xpath(~s{//Link[@rel="salmon"]/@href}, doc)
subscribe_address = XML.string_from_xpath(~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template}, doc)
ap_id = XML.string_from_xpath(~s{//Link[@rel="self" and @type="application/activity+json"]/@href}, doc) subscribe_address =
XML.string_from_xpath(
~s{//Link[@rel="http://ostatus.org/schema/1.0/subscribe"]/@template},
doc
)
ap_id =
XML.string_from_xpath(
~s{//Link[@rel="self" and @type="application/activity+json"]/@href},
doc
)
data = %{ data = %{
"magic_key" => magic_key, "magic_key" => magic_key,
"topic" => topic, "topic" => topic,
@ -115,41 +169,51 @@ defp webfinger_from_xml(doc) do
"subscribe_address" => subscribe_address, "subscribe_address" => subscribe_address,
"ap_id" => ap_id "ap_id" => ap_id
} }
{:ok, data} {:ok, data}
end end
defp webfinger_from_json(doc) do defp webfinger_from_json(doc) do
data = Enum.reduce(doc["links"], %{"subject" => doc["subject"]}, fn (link, data) -> data =
case {link["type"], link["rel"]} do Enum.reduce(doc["links"], %{"subject" => doc["subject"]}, fn link, data ->
{"application/activity+json", "self"} -> case {link["type"], link["rel"]} do
Map.put(data, "ap_id", link["href"]) {"application/activity+json", "self"} ->
{_, "magic-public-key"} -> Map.put(data, "ap_id", link["href"])
"data:application/magic-public-key," <> magic_key = link["href"]
Map.put(data, "magic_key", magic_key) {_, "magic-public-key"} ->
{"application/atom+xml", "http://schemas.google.com/g/2010#updates-from"} -> "data:application/magic-public-key," <> magic_key = link["href"]
Map.put(data, "topic", link["href"]) Map.put(data, "magic_key", magic_key)
{_, "salmon"} ->
Map.put(data, "salmon", link["href"]) {"application/atom+xml", "http://schemas.google.com/g/2010#updates-from"} ->
{_, "http://ostatus.org/schema/1.0/subscribe"} -> Map.put(data, "topic", link["href"])
Map.put(data, "subscribe_address", link["template"])
_ -> {_, "salmon"} ->
Logger.debug("Unhandled type: #{inspect(link["type"])}") Map.put(data, "salmon", link["href"])
data
end {_, "http://ostatus.org/schema/1.0/subscribe"} ->
end) Map.put(data, "subscribe_address", link["template"])
_ ->
Logger.debug("Unhandled type: #{inspect(link["type"])}")
data
end
end)
{:ok, data} {:ok, data}
end end
def get_template_from_xml(body) do def get_template_from_xml(body) do
xpath = "//Link[@rel='lrdd' and @type='application/xrd+xml']/@template" xpath = "//Link[@rel='lrdd' and @type='application/xrd+xml']/@template"
with doc when doc != :error <- XML.parse_document(body), with doc when doc != :error <- XML.parse_document(body),
template when template != nil <- XML.string_from_xpath(xpath, doc) do template when template != nil <- XML.string_from_xpath(xpath, doc) do
{:ok, template} {:ok, template}
end end
end end
def find_lrdd_template(domain) do def find_lrdd_template(domain) do
with {:ok, %{status_code: status_code, body: body}} when status_code in 200..299 <- @httpoison.get("http://#{domain}/.well-known/host-meta", [], follow_redirect: true) do with {:ok, %{status_code: status_code, body: body}} when status_code in 200..299 <-
@httpoison.get("http://#{domain}/.well-known/host-meta", [], follow_redirect: true) do
get_template_from_xml(body) get_template_from_xml(body)
else else
_ -> _ ->
@ -163,28 +227,38 @@ def find_lrdd_template(domain) do
def finger(account) do def finger(account) do
account = String.trim_leading(account, "@") account = String.trim_leading(account, "@")
domain = with [_name, domain] <- String.split(account, "@") do
domain domain =
else _e -> with [_name, domain] <- String.split(account, "@") do
URI.parse(account).host domain
end else
_e ->
URI.parse(account).host
end
case find_lrdd_template(domain) do case find_lrdd_template(domain) do
{:ok, template} -> {:ok, template} ->
address = String.replace(template, "{uri}", URI.encode(account)) address = String.replace(template, "{uri}", URI.encode(account))
_ -> _ ->
address = "http://#{domain}/.well-known/webfinger?resource=acct:#{account}" address = "http://#{domain}/.well-known/webfinger?resource=acct:#{account}"
end end
with response <- @httpoison.get(address, ["Accept": "application/xrd+xml,application/jrd+json"], follow_redirect: true), with response <-
@httpoison.get(
address,
[Accept: "application/xrd+xml,application/jrd+json"],
follow_redirect: true
),
{:ok, %{status_code: status_code, body: body}} when status_code in 200..299 <- response do {:ok, %{status_code: status_code, body: body}} when status_code in 200..299 <- response do
doc = XML.parse_document(body) doc = XML.parse_document(body)
if doc != :error do
webfinger_from_xml(doc) if doc != :error do
else webfinger_from_xml(doc)
{:ok, doc} = Jason.decode(body) else
webfinger_from_json(doc) {:ok, doc} = Jason.decode(body)
end webfinger_from_json(doc)
end
else else
e -> e ->
Logger.debug(fn -> "Couldn't finger #{account}" end) Logger.debug(fn -> "Couldn't finger #{account}" end)

View file

@ -4,7 +4,7 @@ defmodule Pleroma.Web.WebFinger.WebFingerController do
alias Pleroma.Web.WebFinger alias Pleroma.Web.WebFinger
def host_meta(conn, _params) do def host_meta(conn, _params) do
xml = WebFinger.host_meta xml = WebFinger.host_meta()
conn conn
|> put_resp_content_type("application/xrd+xml") |> put_resp_content_type("application/xrd+xml")
@ -21,12 +21,14 @@ def webfinger(conn, %{"resource" => resource}) do
else else
_e -> send_resp(conn, 404, "Couldn't find user") _e -> send_resp(conn, 404, "Couldn't find user")
end end
n when n in ["json", "jrd+json"] -> n when n in ["json", "jrd+json"] ->
with {:ok, response} <- WebFinger.webfinger(resource, "JSON") do with {:ok, response} <- WebFinger.webfinger(resource, "JSON") do
json(conn, response) json(conn, response)
else else
_e -> send_resp(conn, 404, "Couldn't find user") _e -> send_resp(conn, 404, "Couldn't find user")
end end
_ -> _ ->
send_resp(conn, 404, "Unsupported format") send_resp(conn, 404, "Unsupported format")
end end

View file

@ -26,15 +26,16 @@ def verify(subscription, getter \\ &@httpoison.get/3) do
url = hd(String.split(subscription.callback, "?")) url = hd(String.split(subscription.callback, "?"))
query = URI.parse(subscription.callback).query || "" query = URI.parse(subscription.callback).query || ""
params = Map.merge(params, URI.decode_query(query)) params = Map.merge(params, URI.decode_query(query))
with {:ok, response} <- getter.(url, [], [params: params]),
^challenge <- response.body with {:ok, response} <- getter.(url, [], params: params),
do ^challenge <- response.body do
changeset = Changeset.change(subscription, %{state: "active"}) changeset = Changeset.change(subscription, %{state: "active"})
Repo.update(changeset) Repo.update(changeset)
else e -> else
Logger.debug("Couldn't verify subscription") e ->
Logger.debug(inspect(e)) Logger.debug("Couldn't verify subscription")
{:error, subscription} Logger.debug(inspect(e))
{:error, subscription}
end end
end end
@ -46,17 +47,24 @@ def verify(subscription, getter \\ &@httpoison.get/3) do
"Undo", "Undo",
"Delete" "Delete"
] ]
def publish(topic, user, %{data: %{"type" => type}} = activity) when type in @supported_activities do def publish(topic, user, %{data: %{"type" => type}} = activity)
when type in @supported_activities do
# TODO: Only send to still valid subscriptions. # TODO: Only send to still valid subscriptions.
query = from sub in WebsubServerSubscription, query =
where: sub.topic == ^topic and sub.state == "active", from(
where: fragment("? > NOW()", sub.valid_until) sub in WebsubServerSubscription,
where: sub.topic == ^topic and sub.state == "active",
where: fragment("? > NOW()", sub.valid_until)
)
subscriptions = Repo.all(query) subscriptions = Repo.all(query)
Enum.each(subscriptions, fn(sub) ->
response = user Enum.each(subscriptions, fn sub ->
|> FeedRepresenter.to_simple_form([activity], [user]) response =
|> :xmerl.export_simple(:xmerl_xml) user
|> to_string |> FeedRepresenter.to_simple_form([activity], [user])
|> :xmerl.export_simple(:xmerl_xml)
|> to_string
data = %{ data = %{
xml: response, xml: response,
@ -64,22 +72,24 @@ def publish(topic, user, %{data: %{"type" => type}} = activity) when type in @su
callback: sub.callback, callback: sub.callback,
secret: sub.secret secret: sub.secret
} }
Pleroma.Web.Federator.enqueue(:publish_single_websub, data) Pleroma.Web.Federator.enqueue(:publish_single_websub, data)
end) end)
end end
def publish(_,_,_), do: ""
def publish(_, _, _), do: ""
def sign(secret, doc) do def sign(secret, doc) do
:crypto.hmac(:sha, secret, to_string(doc)) |> Base.encode16 |> String.downcase :crypto.hmac(:sha, secret, to_string(doc)) |> Base.encode16() |> String.downcase()
end end
def incoming_subscription_request(user, %{"hub.mode" => "subscribe"} = params) do def incoming_subscription_request(user, %{"hub.mode" => "subscribe"} = params) do
with {:ok, topic} <- valid_topic(params, user), with {:ok, topic} <- valid_topic(params, user),
{:ok, lease_time} <- lease_time(params), {:ok, lease_time} <- lease_time(params),
secret <- params["hub.secret"], secret <- params["hub.secret"],
callback <- params["hub.callback"] callback <- params["hub.callback"] do
do
subscription = get_subscription(topic, callback) subscription = get_subscription(topic, callback)
data = %{ data = %{
state: subscription.state || "requested", state: subscription.state || "requested",
topic: topic, topic: topic,
@ -90,18 +100,20 @@ def incoming_subscription_request(user, %{"hub.mode" => "subscribe"} = params) d
change = Changeset.change(subscription, data) change = Changeset.change(subscription, data)
websub = Repo.insert_or_update!(change) websub = Repo.insert_or_update!(change)
change = Changeset.change(websub, %{valid_until: change =
NaiveDateTime.add(websub.updated_at, lease_time)}) Changeset.change(websub, %{valid_until: NaiveDateTime.add(websub.updated_at, lease_time)})
websub = Repo.update!(change) websub = Repo.update!(change)
Pleroma.Web.Federator.enqueue(:verify_websub, websub) Pleroma.Web.Federator.enqueue(:verify_websub, websub)
{:ok, websub} {:ok, websub}
else {:error, reason} -> else
Logger.debug("Couldn't create subscription") {:error, reason} ->
Logger.debug(inspect(reason)) Logger.debug("Couldn't create subscription")
Logger.debug(inspect(reason))
{:error, reason} {:error, reason}
end end
end end
@ -112,7 +124,8 @@ defp get_subscription(topic, callback) do
# Temp hack for mastodon. # Temp hack for mastodon.
defp lease_time(%{"hub.lease_seconds" => ""}) do defp lease_time(%{"hub.lease_seconds" => ""}) do
{:ok, 60 * 60 * 24 * 3} # three days # three days
{:ok, 60 * 60 * 24 * 3}
end end
defp lease_time(%{"hub.lease_seconds" => lease_seconds}) do defp lease_time(%{"hub.lease_seconds" => lease_seconds}) do
@ -120,7 +133,8 @@ defp lease_time(%{"hub.lease_seconds" => lease_seconds}) do
end end
defp lease_time(_) do defp lease_time(_) do
{:ok, 60 * 60 * 24 * 3} # three days # three days
{:ok, 60 * 60 * 24 * 3}
end end
defp valid_topic(%{"hub.topic" => topic}, user) do defp valid_topic(%{"hub.topic" => topic}, user) do
@ -134,21 +148,26 @@ defp valid_topic(%{"hub.topic" => topic}, user) do
def subscribe(subscriber, subscribed, requester \\ &request_subscription/1) do def subscribe(subscriber, subscribed, requester \\ &request_subscription/1) do
topic = subscribed.info["topic"] topic = subscribed.info["topic"]
# FIXME: Race condition, use transactions # FIXME: Race condition, use transactions
{:ok, subscription} = with subscription when not is_nil(subscription) <- Repo.get_by(WebsubClientSubscription, topic: topic) do {:ok, subscription} =
subscribers = [subscriber.ap_id | subscription.subscribers] |> Enum.uniq with subscription when not is_nil(subscription) <-
change = Ecto.Changeset.change(subscription, %{subscribers: subscribers}) Repo.get_by(WebsubClientSubscription, topic: topic) do
Repo.update(change) subscribers = [subscriber.ap_id | subscription.subscribers] |> Enum.uniq()
else _e -> change = Ecto.Changeset.change(subscription, %{subscribers: subscribers})
subscription = %WebsubClientSubscription{ Repo.update(change)
topic: topic, else
hub: subscribed.info["hub"], _e ->
subscribers: [subscriber.ap_id], subscription = %WebsubClientSubscription{
state: "requested", topic: topic,
secret: :crypto.strong_rand_bytes(8) |> Base.url_encode64, hub: subscribed.info["hub"],
user: subscribed subscribers: [subscriber.ap_id],
} state: "requested",
Repo.insert(subscription) secret: :crypto.strong_rand_bytes(8) |> Base.url_encode64(),
end user: subscribed
}
Repo.insert(subscription)
end
requester.(subscription) requester.(subscription)
end end
@ -159,24 +178,25 @@ def gather_feed_data(topic, getter \\ &@httpoison.get/1) do
doc <- XML.parse_document(body), doc <- XML.parse_document(body),
uri when not is_nil(uri) <- XML.string_from_xpath("/feed/author[1]/uri", doc), uri when not is_nil(uri) <- XML.string_from_xpath("/feed/author[1]/uri", doc),
hub when not is_nil(hub) <- XML.string_from_xpath(~S{/feed/link[@rel="hub"]/@href}, doc) do hub when not is_nil(hub) <- XML.string_from_xpath(~S{/feed/link[@rel="hub"]/@href}, doc) do
name = XML.string_from_xpath("/feed/author[1]/name", doc) name = XML.string_from_xpath("/feed/author[1]/name", doc)
preferredUsername = XML.string_from_xpath("/feed/author[1]/poco:preferredUsername", doc) preferredUsername = XML.string_from_xpath("/feed/author[1]/poco:preferredUsername", doc)
displayName = XML.string_from_xpath("/feed/author[1]/poco:displayName", doc) displayName = XML.string_from_xpath("/feed/author[1]/poco:displayName", doc)
avatar = OStatus.make_avatar_object(doc) avatar = OStatus.make_avatar_object(doc)
bio = XML.string_from_xpath("/feed/author[1]/summary", doc) bio = XML.string_from_xpath("/feed/author[1]/summary", doc)
{:ok, %{ {:ok,
"uri" => uri, %{
"hub" => hub, "uri" => uri,
"nickname" => preferredUsername || name, "hub" => hub,
"name" => displayName || name, "nickname" => preferredUsername || name,
"host" => URI.parse(uri).host, "name" => displayName || name,
"avatar" => avatar, "host" => URI.parse(uri).host,
"bio" => bio "avatar" => avatar,
}} "bio" => bio
else e -> }}
{:error, e} else
e ->
{:error, e}
end end
end end
@ -190,43 +210,45 @@ def request_subscription(websub, poster \\ &@httpoison.post/3, timeout \\ 10_000
# This checks once a second if we are confirmed yet # This checks once a second if we are confirmed yet
websub_checker = fn -> websub_checker = fn ->
helper = fn (helper) -> helper = fn helper ->
:timer.sleep(1000) :timer.sleep(1000)
websub = Repo.get_by(WebsubClientSubscription, id: websub.id, state: "accepted") websub = Repo.get_by(WebsubClientSubscription, id: websub.id, state: "accepted")
if websub, do: websub, else: helper.(helper) if websub, do: websub, else: helper.(helper)
end end
helper.(helper) helper.(helper)
end end
task = Task.async(websub_checker) task = Task.async(websub_checker)
with {:ok, %{status_code: 202}} <- poster.(websub.hub, {:form, data}, ["Content-type": "application/x-www-form-urlencoded"]), with {:ok, %{status_code: 202}} <-
poster.(websub.hub, {:form, data}, "Content-type": "application/x-www-form-urlencoded"),
{:ok, websub} <- Task.yield(task, timeout) do {:ok, websub} <- Task.yield(task, timeout) do
{:ok, websub} {:ok, websub}
else e -> else
Task.shutdown(task) e ->
Task.shutdown(task)
change = Ecto.Changeset.change(websub, %{state: "rejected"}) change = Ecto.Changeset.change(websub, %{state: "rejected"})
{:ok, websub} = Repo.update(change) {:ok, websub} = Repo.update(change)
Logger.debug(fn -> "Couldn't confirm subscription: #{inspect(websub)}" end) Logger.debug(fn -> "Couldn't confirm subscription: #{inspect(websub)}" end)
Logger.debug(fn -> "error: #{inspect(e)}" end) Logger.debug(fn -> "error: #{inspect(e)}" end)
{:error, websub} {:error, websub}
end end
end end
def refresh_subscriptions(delta \\ 60 * 60 * 24) do def refresh_subscriptions(delta \\ 60 * 60 * 24) do
Logger.debug("Refreshing subscriptions") Logger.debug("Refreshing subscriptions")
cut_off = NaiveDateTime.add(NaiveDateTime.utc_now, delta) cut_off = NaiveDateTime.add(NaiveDateTime.utc_now(), delta)
query = from sub in WebsubClientSubscription, query = from(sub in WebsubClientSubscription, where: sub.valid_until < ^cut_off)
where: sub.valid_until < ^cut_off
subs = Repo.all(query) subs = Repo.all(query)
Enum.each(subs, fn (sub) -> Enum.each(subs, fn sub ->
Pleroma.Web.Federator.enqueue(:request_subscription, sub) Pleroma.Web.Federator.enqueue(:request_subscription, sub)
end) end)
end end

View file

@ -3,13 +3,13 @@ defmodule Pleroma.Web.Websub.WebsubClientSubscription do
alias Pleroma.User alias Pleroma.User
schema "websub_client_subscriptions" do schema "websub_client_subscriptions" do
field :topic, :string field(:topic, :string)
field :secret, :string field(:secret, :string)
field :valid_until, :naive_datetime field(:valid_until, :naive_datetime)
field :state, :string field(:state, :string)
field :subscribers, {:array, :string}, default: [] field(:subscribers, {:array, :string}, default: [])
field :hub, :string field(:hub, :string)
belongs_to :user, User belongs_to(:user, User)
timestamps() timestamps()
end end

View file

@ -8,36 +8,49 @@ defmodule Pleroma.Web.Websub.WebsubController do
def websub_subscription_request(conn, %{"nickname" => nickname} = params) do def websub_subscription_request(conn, %{"nickname" => nickname} = params) do
user = User.get_cached_by_nickname(nickname) user = User.get_cached_by_nickname(nickname)
with {:ok, _websub} <- Websub.incoming_subscription_request(user, params) with {:ok, _websub} <- Websub.incoming_subscription_request(user, params) do
do
conn conn
|> send_resp(202, "Accepted") |> send_resp(202, "Accepted")
else {:error, reason} -> else
conn {:error, reason} ->
|> send_resp(500, reason) conn
|> send_resp(500, reason)
end end
end end
# TODO: Extract this into the Websub module # TODO: Extract this into the Websub module
def websub_subscription_confirmation(conn, %{"id" => id, "hub.mode" => "subscribe", "hub.challenge" => challenge, "hub.topic" => topic} = params) do def websub_subscription_confirmation(
conn,
%{
"id" => id,
"hub.mode" => "subscribe",
"hub.challenge" => challenge,
"hub.topic" => topic
} = params
) do
Logger.debug("Got WebSub confirmation") Logger.debug("Got WebSub confirmation")
Logger.debug(inspect(params)) Logger.debug(inspect(params))
lease_seconds = if params["hub.lease_seconds"] do
String.to_integer(params["hub.lease_seconds"])
else
# Guess 3 days
60 * 60 * 24 * 3
end
with %WebsubClientSubscription{} = websub <- Repo.get_by(WebsubClientSubscription, id: id, topic: topic) do lease_seconds =
valid_until = NaiveDateTime.add(NaiveDateTime.utc_now, lease_seconds) if params["hub.lease_seconds"] do
String.to_integer(params["hub.lease_seconds"])
else
# Guess 3 days
60 * 60 * 24 * 3
end
with %WebsubClientSubscription{} = websub <-
Repo.get_by(WebsubClientSubscription, id: id, topic: topic) do
valid_until = NaiveDateTime.add(NaiveDateTime.utc_now(), lease_seconds)
change = Ecto.Changeset.change(websub, %{state: "accepted", valid_until: valid_until}) change = Ecto.Changeset.change(websub, %{state: "accepted", valid_until: valid_until})
{:ok, _websub} = Repo.update(change) {:ok, _websub} = Repo.update(change)
conn conn
|> send_resp(200, challenge) |> send_resp(200, challenge)
else _e -> else
conn _e ->
|> send_resp(500, "Error") conn
|> send_resp(500, "Error")
end end
end end
@ -48,12 +61,15 @@ def websub_incoming(conn, %{"id" => id}) do
{:ok, body, _conn} = read_body(conn), {:ok, body, _conn} = read_body(conn),
^signature <- Websub.sign(websub.secret, body) do ^signature <- Websub.sign(websub.secret, body) do
Federator.enqueue(:incoming_doc, body) Federator.enqueue(:incoming_doc, body)
conn conn
|> send_resp(200, "OK") |> send_resp(200, "OK")
else _e -> else
Logger.debug("Can't handle incoming subscription post") _e ->
conn Logger.debug("Can't handle incoming subscription post")
|> send_resp(500, "Error")
conn
|> send_resp(500, "Error")
end end
end end
end end

View file

@ -2,11 +2,11 @@ defmodule Pleroma.Web.Websub.WebsubServerSubscription do
use Ecto.Schema use Ecto.Schema
schema "websub_server_subscriptions" do schema "websub_server_subscriptions" do
field :topic, :string field(:topic, :string)
field :callback, :string field(:callback, :string)
field :secret, :string field(:secret, :string)
field :valid_until, :naive_datetime field(:valid_until, :naive_datetime)
field :state, :string field(:state, :string)
timestamps() timestamps()
end end

View file

@ -2,21 +2,24 @@ defmodule Pleroma.Web.XML do
require Logger require Logger
def string_from_xpath(_, :error), do: nil def string_from_xpath(_, :error), do: nil
def string_from_xpath(xpath, doc) do def string_from_xpath(xpath, doc) do
{:xmlObj, :string, res} = :xmerl_xpath.string('string(#{xpath})', doc) {:xmlObj, :string, res} = :xmerl_xpath.string('string(#{xpath})', doc)
res = res res =
|> to_string res
|> String.trim |> to_string
|> String.trim()
if res == "", do: nil, else: res if res == "", do: nil, else: res
end end
def parse_document(text) do def parse_document(text) do
try do try do
{doc, _rest} = text {doc, _rest} =
|> :binary.bin_to_list text
|> :xmerl_scan.string |> :binary.bin_to_list()
|> :xmerl_scan.string()
doc doc
catch catch

View file

@ -1,8 +1,10 @@
defmodule Phoenix.Transports.WebSocket.Raw do defmodule Phoenix.Transports.WebSocket.Raw do
import Plug.Conn, only: [ import Plug.Conn,
fetch_query_params: 1, only: [
send_resp: 3 fetch_query_params: 1,
] send_resp: 3
]
alias Phoenix.Socket.Transport alias Phoenix.Socket.Transport
def default_config do def default_config do
@ -16,21 +18,24 @@ def default_config do
def init(%Plug.Conn{method: "GET"} = conn, {endpoint, handler, transport}) do def init(%Plug.Conn{method: "GET"} = conn, {endpoint, handler, transport}) do
{_, opts} = handler.__transport__(transport) {_, opts} = handler.__transport__(transport)
conn = conn conn =
|> fetch_query_params conn
|> Transport.transport_log(opts[:transport_log]) |> fetch_query_params
|> Transport.force_ssl(handler, endpoint, opts) |> Transport.transport_log(opts[:transport_log])
|> Transport.check_origin(handler, endpoint, opts) |> Transport.force_ssl(handler, endpoint, opts)
|> Transport.check_origin(handler, endpoint, opts)
case conn do case conn do
%{halted: false} = conn -> %{halted: false} = conn ->
case Transport.connect(endpoint, handler, transport, __MODULE__, nil, conn.params) do case Transport.connect(endpoint, handler, transport, __MODULE__, nil, conn.params) do
{:ok, socket} -> {:ok, socket} ->
{:ok, conn, {__MODULE__, {socket, opts}}} {:ok, conn, {__MODULE__, {socket, opts}}}
:error -> :error ->
send_resp(conn, :forbidden, "") send_resp(conn, :forbidden, "")
{:error, conn} {:error, conn}
end end
_ -> _ ->
{:error, conn} {:error, conn}
end end
@ -52,16 +57,19 @@ def ws_handle(op, data, state) do
|> case do |> case do
{op, data} -> {op, data} ->
{:reply, {op, data}, state} {:reply, {op, data}, state}
{op, data, state} -> {op, data, state} ->
{:reply, {op, data}, state} {:reply, {op, data}, state}
%{} = state -> %{} = state ->
{:ok, state} {:ok, state}
_ -> _ ->
{:ok, state} {:ok, state}
end end
end end
def ws_info({_,_} = tuple, state) do def ws_info({_, _} = tuple, state) do
{:reply, tuple, state} {:reply, tuple, state}
end end

View file

@ -23,7 +23,7 @@ def to_xml(content) when is_list(content) do
for element <- content do for element <- content do
to_xml(element) to_xml(element)
end end
|> Enum.join |> Enum.join()
end end
def to_xml(%NaiveDateTime{} = time) do def to_xml(%NaiveDateTime{} = time) do
@ -33,10 +33,12 @@ def to_xml(%NaiveDateTime{} = time) do
def to_doc(content), do: ~s(<?xml version="1.0" encoding="UTF-8"?>) <> to_xml(content) def to_doc(content), do: ~s(<?xml version="1.0" encoding="UTF-8"?>) <> to_xml(content)
defp make_open_tag(tag, attributes) do defp make_open_tag(tag, attributes) do
attributes_string = for {attribute, value} <- attributes do attributes_string =
"#{attribute}=\"#{value}\"" for {attribute, value} <- attributes do
end |> Enum.join(" ") "#{attribute}=\"#{value}\""
end
|> Enum.join(" ")
[tag, attributes_string] |> Enum.join(" ") |> String.trim [tag, attributes_string] |> Enum.join(" ") |> String.trim()
end end
end end

65
mix.exs
View file

@ -2,48 +2,51 @@ defmodule Pleroma.Mixfile do
use Mix.Project use Mix.Project
def project do def project do
[app: :pleroma, [
version: "0.9.0", app: :pleroma,
elixir: "~> 1.4", version: "0.9.0",
elixirc_paths: elixirc_paths(Mix.env), elixir: "~> 1.4",
compilers: [:phoenix, :gettext] ++ Mix.compilers, elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env == :prod, compilers: [:phoenix, :gettext] ++ Mix.compilers(),
aliases: aliases(), start_permanent: Mix.env() == :prod,
deps: deps()] aliases: aliases(),
deps: deps()
]
end end
# Configuration for the OTP application. # Configuration for the OTP application.
# #
# Type `mix help compile.app` for more information. # Type `mix help compile.app` for more information.
def application do def application do
[mod: {Pleroma.Application, []}, [mod: {Pleroma.Application, []}, extra_applications: [:logger, :runtime_tools, :comeonin]]
extra_applications: [:logger, :runtime_tools, :comeonin]]
end end
# Specifies which paths to compile per environment. # Specifies which paths to compile per environment.
defp elixirc_paths(:test), do: ["lib", "test/support"] defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"] defp elixirc_paths(_), do: ["lib"]
# Specifies your project dependencies. # Specifies your project dependencies.
# #
# 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_pubsub, "~> 1.0"}, {:phoenix, "~> 1.3.0"},
{:phoenix_ecto, "~> 3.2"}, {:phoenix_pubsub, "~> 1.0"},
{:postgrex, ">= 0.0.0"}, {:phoenix_ecto, "~> 3.2"},
{:gettext, "~> 0.11"}, {:postgrex, ">= 0.0.0"},
{:cowboy, "~> 1.0", override: true}, {:gettext, "~> 0.11"},
{:comeonin, "~> 3.0"}, {:cowboy, "~> 1.0", override: true},
{:trailing_format_plug, "~> 0.0.5" }, {:comeonin, "~> 3.0"},
{:html_sanitize_ex, "~> 1.3.0-rc1"}, {:trailing_format_plug, "~> 0.0.5"},
{:phoenix_html, "~> 2.10"}, {:html_sanitize_ex, "~> 1.3.0-rc1"},
{:calendar, "~> 0.16.1"}, {:phoenix_html, "~> 2.10"},
{:cachex, "~> 2.1"}, {:calendar, "~> 0.16.1"},
{:httpoison, "~> 0.11.2"}, {:cachex, "~> 2.1"},
{:jason, "~> 1.0"}, {:httpoison, "~> 0.11.2"},
{:ex_machina, "~> 2.0", only: :test}, {:jason, "~> 1.0"},
{:credo, "~> 0.7", only: [:dev, :test]}] {:ex_machina, "~> 2.0", only: :test},
{:credo, "~> 0.7", only: [:dev, :test]}
]
end end
# Aliases are shortcuts or tasks specific to the current project. # Aliases are shortcuts or tasks specific to the current project.
@ -53,8 +56,10 @@ defp deps do
# #
# See the documentation for `Mix` for more info on aliases. # See the documentation for `Mix` for more info on aliases.
defp aliases do defp aliases do
["ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], [
"ecto.reset": ["ecto.drop", "ecto.setup"], "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
"test": ["ecto.create --quiet", "ecto.migrate", "test"]] "ecto.reset": ["ecto.drop", "ecto.setup"],
test: ["ecto.create --quiet", "ecto.migrate", "test"]
]
end end
end end

View file

@ -18,7 +18,9 @@ test "returns activities by it's objects AP ids" do
test "returns the activity that created an object" do test "returns the activity that created an object" do
activity = insert(:note_activity) activity = insert(:note_activity)
found_activity = Pleroma.Activity.get_create_activity_by_object_ap_id(activity.data["object"]["id"])
found_activity =
Pleroma.Activity.get_create_activity_by_object_ap_id(activity.data["object"]["id"])
assert activity == found_activity assert activity == found_activity
end end

View file

@ -7,44 +7,56 @@ defmodule Pleroma.FormatterTest do
describe ".add_hashtag_links" do describe ".add_hashtag_links" do
test "turns hashtags into links" do test "turns hashtags into links" do
text = "I love #cofe and #2hu" text = "I love #cofe and #2hu"
expected_text = "I love <a href='http://localhost:4001/tag/cofe' rel='tag'>#cofe</a> and <a href='http://localhost:4001/tag/2hu' rel='tag'>#2hu</a>"
expected_text =
"I love <a href='http://localhost:4001/tag/cofe' rel='tag'>#cofe</a> and <a href='http://localhost:4001/tag/2hu' rel='tag'>#2hu</a>"
tags = Formatter.parse_tags(text) tags = Formatter.parse_tags(text)
assert expected_text == Formatter.add_hashtag_links({[], text}, tags) |> Formatter.finalize
assert expected_text ==
Formatter.add_hashtag_links({[], text}, tags) |> Formatter.finalize()
end end
end end
describe ".add_links" do describe ".add_links" do
test "turning urls into links" do test "turning urls into links" do
text = "Hey, check out https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla." text = "Hey, check out https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla."
expected = "Hey, check out <a href='https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla'>https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla</a>."
assert Formatter.add_links({[], text}) |> Formatter.finalize == expected expected =
"Hey, check out <a href='https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla'>https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla</a>."
assert Formatter.add_links({[], text}) |> Formatter.finalize() == expected
text = "https://mastodon.social/@lambadalambda" text = "https://mastodon.social/@lambadalambda"
expected = "<a href='https://mastodon.social/@lambadalambda'>https://mastodon.social/@lambadalambda</a>"
assert Formatter.add_links({[], text}) |> Formatter.finalize == expected expected =
"<a href='https://mastodon.social/@lambadalambda'>https://mastodon.social/@lambadalambda</a>"
assert Formatter.add_links({[], text}) |> Formatter.finalize() == expected
text = "@lambadalambda" text = "@lambadalambda"
expected = "@lambadalambda" expected = "@lambadalambda"
assert Formatter.add_links({[], text}) |> Formatter.finalize == expected assert Formatter.add_links({[], text}) |> Formatter.finalize() == expected
text = "http://www.cs.vu.nl/~ast/intel/" text = "http://www.cs.vu.nl/~ast/intel/"
expected = "<a href='http://www.cs.vu.nl/~ast/intel/'>http://www.cs.vu.nl/~ast/intel/</a>" expected = "<a href='http://www.cs.vu.nl/~ast/intel/'>http://www.cs.vu.nl/~ast/intel/</a>"
assert Formatter.add_links({[], text}) |> Formatter.finalize == expected assert Formatter.add_links({[], text}) |> Formatter.finalize() == expected
text = "https://forum.zdoom.org/viewtopic.php?f=44&t=57087" text = "https://forum.zdoom.org/viewtopic.php?f=44&t=57087"
expected = "<a href='https://forum.zdoom.org/viewtopic.php?f=44&t=57087'>https://forum.zdoom.org/viewtopic.php?f=44&t=57087</a>"
assert Formatter.add_links({[], text}) |> Formatter.finalize == expected expected =
"<a href='https://forum.zdoom.org/viewtopic.php?f=44&t=57087'>https://forum.zdoom.org/viewtopic.php?f=44&t=57087</a>"
assert Formatter.add_links({[], text}) |> Formatter.finalize() == expected
text = "https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul" text = "https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul"
expected = "<a href='https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul'>https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul</a>"
assert Formatter.add_links({[], text}) |> Formatter.finalize == expected expected =
"<a href='https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul'>https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul</a>"
assert Formatter.add_links({[], text}) |> Formatter.finalize() == expected
end end
end end
@ -60,9 +72,14 @@ test "gives a replacement for user links" do
{subs, text} = Formatter.add_user_links({[], text}, mentions) {subs, text} = Formatter.add_user_links({[], text}, mentions)
assert length(subs) == 3 assert length(subs) == 3
Enum.each(subs, fn({uuid, _}) -> assert String.contains?(text, uuid) end) Enum.each(subs, fn {uuid, _} -> assert String.contains?(text, uuid) end)
expected_text = "<span><a href='#{gsimg.ap_id}'>@<span>gsimg</span></a></span> According to <span><a href='#{archaeme.ap_id}'>@<span>archaeme</span></a></span>, that is @daggsy. Also hello <span><a href='#{archaeme_remote.ap_id}'>@<span>archaeme</span></a></span>" expected_text =
"<span><a href='#{gsimg.ap_id}'>@<span>gsimg</span></a></span> According to <span><a href='#{
archaeme.ap_id
}'>@<span>archaeme</span></a></span>, that is @daggsy. Also hello <span><a href='#{
archaeme_remote.ap_id
}'>@<span>archaeme</span></a></span>"
assert expected_text == Formatter.finalize({subs, text}) assert expected_text == Formatter.finalize({subs, text})
end end
@ -71,6 +88,7 @@ test "gives a replacement for user links" do
describe ".parse_tags" do describe ".parse_tags" do
test "parses tags in the text" do test "parses tags in the text" do
text = "Here's a #Test. Maybe these are #working or not. What about #漢字? And #は。" text = "Here's a #Test. Maybe these are #working or not. What about #漢字? And #は。"
expected = [ expected = [
{"#Test", "test"}, {"#Test", "test"},
{"#working", "working"}, {"#working", "working"},
@ -92,7 +110,7 @@ test "it can parse mentions and return the relevant users" do
expected_result = [ expected_result = [
{"@gsimg", gsimg}, {"@gsimg", gsimg},
{"@archaeme", archaeme}, {"@archaeme", archaeme},
{"@archaeme@archae.me", archaeme_remote}, {"@archaeme@archae.me", archaeme_remote}
] ]
assert Formatter.parse_mentions(text) == expected_result assert Formatter.parse_mentions(text) == expected_result
@ -101,7 +119,8 @@ test "it can parse mentions and return the relevant users" do
test "it adds cool emoji" do test "it adds cool emoji" do
text = "I love :moominmamma:" text = "I love :moominmamma:"
expected_result = "I love <img height='32px' width='32px' alt='moominmamma' title='moominmamma' src='/finmoji/128px/moominmamma-128.png' />" expected_result =
"I love <img height='32px' width='32px' alt='moominmamma' title='moominmamma' src='/finmoji/128px/moominmamma-128.png' />"
assert Formatter.emojify(text) == expected_result assert Formatter.emojify(text) == expected_result
end end

View file

@ -10,7 +10,10 @@ test "notifies someone when they are directly addressed" do
other_user = insert(:user) other_user = insert(:user)
third_user = insert(:user) third_user = insert(:user)
{:ok, activity} = TwitterAPI.create_status(user, %{"status" => "hey @#{other_user.nickname} and @#{third_user.nickname}"}) {:ok, activity} =
TwitterAPI.create_status(user, %{
"status" => "hey @#{other_user.nickname} and @#{third_user.nickname}"
})
{:ok, [notification, other_notification]} = Notification.create_notifications(activity) {:ok, [notification, other_notification]} = Notification.create_notifications(activity)
@ -37,7 +40,9 @@ test "it gets a notification that belongs to the user" do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
{:ok, activity} = TwitterAPI.create_status(user, %{"status" => "hey @#{other_user.nickname}"}) {:ok, activity} =
TwitterAPI.create_status(user, %{"status" => "hey @#{other_user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity) {:ok, [notification]} = Notification.create_notifications(activity)
{:ok, notification} = Notification.get(other_user, notification.id) {:ok, notification} = Notification.get(other_user, notification.id)
@ -48,7 +53,9 @@ test "it returns error if the notification doesn't belong to the user" do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
{:ok, activity} = TwitterAPI.create_status(user, %{"status" => "hey @#{other_user.nickname}"}) {:ok, activity} =
TwitterAPI.create_status(user, %{"status" => "hey @#{other_user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity) {:ok, [notification]} = Notification.create_notifications(activity)
{:error, _notification} = Notification.get(user, notification.id) {:error, _notification} = Notification.get(user, notification.id)
end end
@ -59,7 +66,9 @@ test "it dismisses a notification that belongs to the user" do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
{:ok, activity} = TwitterAPI.create_status(user, %{"status" => "hey @#{other_user.nickname}"}) {:ok, activity} =
TwitterAPI.create_status(user, %{"status" => "hey @#{other_user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity) {:ok, [notification]} = Notification.create_notifications(activity)
{:ok, notification} = Notification.dismiss(other_user, notification.id) {:ok, notification} = Notification.dismiss(other_user, notification.id)
@ -70,7 +79,9 @@ test "it returns error if the notification doesn't belong to the user" do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
{:ok, activity} = TwitterAPI.create_status(user, %{"status" => "hey @#{other_user.nickname}"}) {:ok, activity} =
TwitterAPI.create_status(user, %{"status" => "hey @#{other_user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity) {:ok, [notification]} = Notification.create_notifications(activity)
{:error, _notification} = Notification.dismiss(user, notification.id) {:error, _notification} = Notification.dismiss(user, notification.id)
end end
@ -82,9 +93,18 @@ test "it clears all notifications belonging to the user" do
other_user = insert(:user) other_user = insert(:user)
third_user = insert(:user) third_user = insert(:user)
{:ok, activity} = TwitterAPI.create_status(user, %{"status" => "hey @#{other_user.nickname} and @#{third_user.nickname} !"}) {:ok, activity} =
TwitterAPI.create_status(user, %{
"status" => "hey @#{other_user.nickname} and @#{third_user.nickname} !"
})
{:ok, _notifs} = Notification.create_notifications(activity) {:ok, _notifs} = Notification.create_notifications(activity)
{:ok, activity} = TwitterAPI.create_status(user, %{"status" => "hey again @#{other_user.nickname} and @#{third_user.nickname} !"})
{:ok, activity} =
TwitterAPI.create_status(user, %{
"status" => "hey again @#{other_user.nickname} and @#{third_user.nickname} !"
})
{:ok, _notifs} = Notification.create_notifications(activity) {:ok, _notifs} = Notification.create_notifications(activity)
Notification.clear(other_user) Notification.clear(other_user)

View file

@ -37,22 +37,24 @@ defp basic_auth_enc(username, password) do
describe "without an authorization header" do describe "without an authorization header" do
test "it halts the application" do test "it halts the application" do
conn = build_conn() conn =
|> Plug.Session.call(Plug.Session.init(@session_opts)) build_conn()
|> fetch_session |> Plug.Session.call(Plug.Session.init(@session_opts))
|> AuthenticationPlug.call(%{}) |> fetch_session
|> AuthenticationPlug.call(%{})
assert conn.status == 403 assert conn.status == 403
assert conn.halted == true assert conn.halted == true
end end
test "it assigns a nil user if the 'optional' option is used" do test "it assigns a nil user if the 'optional' option is used" do
conn = build_conn() conn =
|> Plug.Session.call(Plug.Session.init(@session_opts)) build_conn()
|> fetch_session |> Plug.Session.call(Plug.Session.init(@session_opts))
|> AuthenticationPlug.call(%{optional: true}) |> fetch_session
|> AuthenticationPlug.call(%{optional: true})
assert %{ user: nil } == conn.assigns assert %{user: nil} == conn.assigns
end end
end end
@ -73,9 +75,9 @@ test "it assigns a nil user if the 'optional' option is used" do
build_conn() build_conn()
|> Plug.Session.call(Plug.Session.init(@session_opts)) |> Plug.Session.call(Plug.Session.init(@session_opts))
|> fetch_session |> fetch_session
|> AuthenticationPlug.call(%{optional: true, fetcher: &fetch_nil/1 }) |> AuthenticationPlug.call(%{optional: true, fetcher: &fetch_nil/1})
assert %{ user: nil } == conn.assigns assert %{user: nil} == conn.assigns
end end
end end
@ -113,7 +115,7 @@ test "it assigns a nil user if the 'optional' option is used" do
|> put_req_header("authorization", header) |> put_req_header("authorization", header)
|> AuthenticationPlug.call(opts) |> AuthenticationPlug.call(opts)
assert %{ user: nil } == conn.assigns assert %{user: nil} == conn.assigns
end end
end end
@ -126,13 +128,14 @@ test "it assigns the user", %{conn: conn} do
header = basic_auth_enc("dude", "guy") header = basic_auth_enc("dude", "guy")
conn = conn conn =
conn
|> Plug.Session.call(Plug.Session.init(@session_opts)) |> Plug.Session.call(Plug.Session.init(@session_opts))
|> fetch_session |> fetch_session
|> put_req_header("authorization", header) |> put_req_header("authorization", header)
|> AuthenticationPlug.call(opts) |> AuthenticationPlug.call(opts)
assert %{ user: @user } == conn.assigns assert %{user: @user} == conn.assigns
assert get_session(conn, :user_id) == @user.id assert get_session(conn, :user_id) == @user.id
assert conn.halted == false assert conn.halted == false
end end
@ -147,7 +150,8 @@ test "it halts the appication", %{conn: conn} do
header = basic_auth_enc("dude", "guy") header = basic_auth_enc("dude", "guy")
conn = conn conn =
conn
|> Plug.Session.call(Plug.Session.init(@session_opts)) |> Plug.Session.call(Plug.Session.init(@session_opts))
|> fetch_session |> fetch_session
|> put_req_header("authorization", header) |> put_req_header("authorization", header)
@ -167,14 +171,15 @@ test "it assigns the user", %{conn: conn} do
header = basic_auth_enc("dude", "THIS IS WRONG") header = basic_auth_enc("dude", "THIS IS WRONG")
conn = conn conn =
conn
|> Plug.Session.call(Plug.Session.init(@session_opts)) |> Plug.Session.call(Plug.Session.init(@session_opts))
|> fetch_session |> fetch_session
|> put_session(:user_id, @user.id) |> put_session(:user_id, @user.id)
|> put_req_header("authorization", header) |> put_req_header("authorization", header)
|> AuthenticationPlug.call(opts) |> AuthenticationPlug.call(opts)
assert %{ user: @user } == conn.assigns assert %{user: @user} == conn.assigns
assert get_session(conn, :user_id) == @user.id assert get_session(conn, :user_id) == @user.id
assert conn.halted == false assert conn.halted == false
end end
@ -182,8 +187,9 @@ test "it assigns the user", %{conn: conn} do
describe "with an assigned user" do describe "with an assigned user" do
test "it does nothing, returning the incoming conn", %{conn: conn} do test "it does nothing, returning the incoming conn", %{conn: conn} do
conn = conn conn =
|> assign(:user, @user) conn
|> assign(:user, @user)
conn_result = AuthenticationPlug.call(conn, %{}) conn_result = AuthenticationPlug.call(conn, %{})

View file

@ -4,17 +4,19 @@ defmodule Pleroma.Builders.ActivityBuilder do
def build(data \\ %{}, opts \\ %{}) do def build(data \\ %{}, opts \\ %{}) do
user = opts[:user] || Pleroma.Factory.insert(:user) user = opts[:user] || Pleroma.Factory.insert(:user)
activity = %{ activity = %{
"id" => Pleroma.Web.ActivityPub.Utils.generate_object_id, "id" => Pleroma.Web.ActivityPub.Utils.generate_object_id(),
"actor" => user.ap_id, "actor" => user.ap_id,
"to" => ["https://www.w3.org/ns/activitystreams#Public"], "to" => ["https://www.w3.org/ns/activitystreams#Public"],
"type" => "Create", "type" => "Create",
"object" => %{ "object" => %{
"type" => "Note", "type" => "Note",
"content" => "test", "content" => "test",
"to" => ["https://www.w3.org/ns/activitystreams#Public"], "to" => ["https://www.w3.org/ns/activitystreams#Public"]
} }
} }
Map.merge(activity, data) Map.merge(activity, data)
end end
@ -24,7 +26,7 @@ def insert(data \\ %{}, opts \\ %{}) do
end end
def insert_list(times, data \\ %{}, opts \\ %{}) do def insert_list(times, data \\ %{}, opts \\ %{}) do
Enum.map(1..times, fn (n) -> Enum.map(1..times, fn n ->
{:ok, activity} = insert(data, opts) {:ok, activity} = insert(data, opts)
activity activity
end) end)

View file

@ -10,6 +10,7 @@ def build(data \\ %{}) do
bio: "A tester.", bio: "A tester.",
ap_id: "some id" ap_id: "some id"
} }
Map.merge(user, data) Map.merge(user, data)
end end

View file

@ -25,13 +25,13 @@ defmodule Pleroma.Web.ChannelCase do
end end
end end
setup tags do setup tags do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Pleroma.Repo) :ok = Ecto.Adapters.SQL.Sandbox.checkout(Pleroma.Repo)
unless tags[:async] do unless tags[:async] do
Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, {:shared, self()}) Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, {:shared, self()})
end end
:ok :ok
end end
end end

View file

@ -26,14 +26,14 @@ defmodule Pleroma.Web.ConnCase do
end end
end end
setup tags do setup tags do
Cachex.clear(:user_cache) Cachex.clear(:user_cache)
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Pleroma.Repo) :ok = Ecto.Adapters.SQL.Sandbox.checkout(Pleroma.Repo)
unless tags[:async] do unless tags[:async] do
Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, {:shared, self()}) Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, {:shared, self()})
end end
{:ok, conn: Phoenix.ConnTest.build_conn()} {:ok, conn: Phoenix.ConnTest.build_conn()}
end end
end end

View file

@ -9,20 +9,27 @@ def user_factory do
password_hash: Comeonin.Pbkdf2.hashpwsalt("test"), password_hash: Comeonin.Pbkdf2.hashpwsalt("test"),
bio: sequence(:bio, &"Tester Number #{&1}") bio: sequence(:bio, &"Tester Number #{&1}")
} }
%{ user | ap_id: Pleroma.User.ap_id(user), follower_address: Pleroma.User.ap_followers(user), following: [Pleroma.User.ap_id(user)] }
%{
user
| ap_id: Pleroma.User.ap_id(user),
follower_address: Pleroma.User.ap_followers(user),
following: [Pleroma.User.ap_id(user)]
}
end end
def note_factory do def note_factory do
text = sequence(:text, &"This is :moominmamma: note #{&1}") text = sequence(:text, &"This is :moominmamma: note #{&1}")
user = insert(:user) user = insert(:user)
data = %{ data = %{
"type" => "Note", "type" => "Note",
"content" => text, "content" => text,
"id" => Pleroma.Web.ActivityPub.Utils.generate_object_id, "id" => Pleroma.Web.ActivityPub.Utils.generate_object_id(),
"actor" => user.ap_id, "actor" => user.ap_id,
"to" => ["https://www.w3.org/ns/activitystreams#Public"], "to" => ["https://www.w3.org/ns/activitystreams#Public"],
"published" => DateTime.utc_now() |> DateTime.to_iso8601, "published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"likes" => [], "likes" => [],
"like_count" => 0, "like_count" => 0,
"context" => "2hu", "context" => "2hu",
@ -40,13 +47,14 @@ def note_factory do
def note_activity_factory do def note_activity_factory do
note = insert(:note) note = insert(:note)
data = %{ data = %{
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id, "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
"type" => "Create", "type" => "Create",
"actor" => note.data["actor"], "actor" => note.data["actor"],
"to" => note.data["to"], "to" => note.data["to"],
"object" => note.data, "object" => note.data,
"published" => DateTime.utc_now() |> DateTime.to_iso8601, "published" => DateTime.utc_now() |> DateTime.to_iso8601(),
"context" => note.data["context"] "context" => note.data["context"]
} }
@ -62,11 +70,11 @@ def like_activity_factory do
user = insert(:user) user = insert(:user)
data = %{ data = %{
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id, "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
"actor" => user.ap_id, "actor" => user.ap_id,
"type" => "Like", "type" => "Like",
"object" => note_activity.data["object"]["id"], "object" => note_activity.data["object"]["id"],
"published_at" => DateTime.utc_now() |> DateTime.to_iso8601 "published_at" => DateTime.utc_now() |> DateTime.to_iso8601()
} }
%Pleroma.Activity{ %Pleroma.Activity{
@ -79,11 +87,11 @@ def follow_activity_factory do
followed = insert(:user) followed = insert(:user)
data = %{ data = %{
"id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id, "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(),
"actor" => follower.ap_id, "actor" => follower.ap_id,
"type" => "Follow", "type" => "Follow",
"object" => followed.ap_id, "object" => followed.ap_id,
"published_at" => DateTime.utc_now() |> DateTime.to_iso8601 "published_at" => DateTime.utc_now() |> DateTime.to_iso8601()
} }
%Pleroma.Activity{ %Pleroma.Activity{
@ -96,7 +104,7 @@ def websub_subscription_factory do
topic: "http://example.org", topic: "http://example.org",
callback: "http://example/org/callback", callback: "http://example/org/callback",
secret: "here's a secret", secret: "here's a secret",
valid_until: NaiveDateTime.add(NaiveDateTime.utc_now, 100), valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 100),
state: "requested" state: "requested"
} }
end end

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,6 @@
defmodule Pleroma.Web.OStatusMock do defmodule Pleroma.Web.OStatusMock do
import Pleroma.Factory import Pleroma.Factory
def handle_incoming(_doc) do def handle_incoming(_doc) do
insert(:note_activity) insert(:note_activity)
end end

View file

@ -2,4 +2,3 @@
Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual) Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual)
{:ok, _} = Application.ensure_all_started(:ex_machina) {:ok, _} = Application.ensure_all_started(:ex_machina)

View file

@ -4,20 +4,37 @@ defmodule Pleroma.UploadTest do
describe "Storing a file" do describe "Storing a file" do
test "copies the file to the configured folder" do test "copies the file to the configured folder" do
file = %Plug.Upload{content_type: "image/jpg", path: Path.absname("test/fixtures/image.jpg"), filename: "an [image.jpg"} file = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an [image.jpg"
}
data = Upload.store(file) data = Upload.store(file)
assert data["name"] == "an [image.jpg" assert data["name"] == "an [image.jpg"
assert List.first(data["url"])["href"] == "http://localhost:4001/media/#{data["uuid"]}/an%20%5Bimage.jpg"
assert List.first(data["url"])["href"] ==
"http://localhost:4001/media/#{data["uuid"]}/an%20%5Bimage.jpg"
end end
test "fixes an incorrect content type" do test "fixes an incorrect content type" do
file = %Plug.Upload{content_type: "application/octet-stream", path: Path.absname("test/fixtures/image.jpg"), filename: "an [image.jpg"} file = %Plug.Upload{
content_type: "application/octet-stream",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an [image.jpg"
}
data = Upload.store(file) data = Upload.store(file)
assert hd(data["url"])["mediaType"] == "image/jpeg" assert hd(data["url"])["mediaType"] == "image/jpeg"
end end
test "does not modify a valid content type" do test "does not modify a valid content type" do
file = %Plug.Upload{content_type: "image/png", path: Path.absname("test/fixtures/image.jpg"), filename: "an [image.jpg"} file = %Plug.Upload{
content_type: "image/png",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an [image.jpg"
}
data = Upload.store(file) data = Upload.store(file)
assert hd(data["url"])["mediaType"] == "image/png" assert hd(data["url"])["mediaType"] == "image/png"
end end

View file

@ -10,15 +10,15 @@ defmodule Pleroma.UserTest do
import Ecto.Query import Ecto.Query
test "ap_id returns the activity pub id for the user" do test "ap_id returns the activity pub id for the user" do
user = UserBuilder.build user = UserBuilder.build()
expected_ap_id = "#{Pleroma.Web.base_url}/users/#{user.nickname}" expected_ap_id = "#{Pleroma.Web.base_url()}/users/#{user.nickname}"
assert expected_ap_id == User.ap_id(user) assert expected_ap_id == User.ap_id(user)
end end
test "ap_followers returns the followers collection for the user" do test "ap_followers returns the followers collection for the user" do
user = UserBuilder.build user = UserBuilder.build()
expected_followers_collection = "#{User.ap_id(user)}/followers" expected_followers_collection = "#{User.ap_id(user)}/followers"
@ -67,7 +67,7 @@ test "unfollow takes a user and another user" do
followed = insert(:user) followed = insert(:user)
user = insert(:user, %{following: [User.ap_followers(followed)]}) user = insert(:user, %{following: [User.ap_followers(followed)]})
{:ok, user, _activity } = User.unfollow(user, followed) {:ok, user, _activity} = User.unfollow(user, followed)
user = Repo.get(User, user.id) user = Repo.get(User, user.id)
@ -83,7 +83,6 @@ test "unfollow doesn't unfollow yourself" do
assert user.following == [user.ap_id] assert user.following == [user.ap_id]
end end
test "test if a user is following another user" do test "test if a user is following another user" do
followed = insert(:user) followed = insert(:user)
user = insert(:user, %{following: [User.ap_followers(followed)]}) user = insert(:user, %{following: [User.ap_followers(followed)]})
@ -104,12 +103,12 @@ test "test if a user is following another user" do
test "it requires an email, name, nickname and password, bio is optional" do test "it requires an email, name, nickname and password, bio is optional" do
@full_user_data @full_user_data
|> Map.keys |> Map.keys()
|> Enum.each(fn (key) -> |> Enum.each(fn key ->
params = Map.delete(@full_user_data, key) params = Map.delete(@full_user_data, key)
changeset = User.register_changeset(%User{}, params) changeset = User.register_changeset(%User{}, params)
assert (if key == :bio, do: changeset.valid?, else: not changeset.valid?) assert if key == :bio, do: changeset.valid?, else: not changeset.valid?
end) end)
end end
@ -120,7 +119,11 @@ test "it sets the password_hash, ap_id and following fields" do
assert is_binary(changeset.changes[:password_hash]) assert is_binary(changeset.changes[:password_hash])
assert changeset.changes[:ap_id] == User.ap_id(%User{nickname: @full_user_data.nickname}) assert changeset.changes[:ap_id] == User.ap_id(%User{nickname: @full_user_data.nickname})
assert changeset.changes[:following] == [User.ap_followers(%User{nickname: @full_user_data.nickname})]
assert changeset.changes[:following] == [
User.ap_followers(%User{nickname: @full_user_data.nickname})
]
assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers" assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers"
end end
end end
@ -158,12 +161,24 @@ test "returns nil for nonexistant local user" do
test "returns an ap_id for a user" do test "returns an ap_id for a user" do
user = insert(:user) user = insert(:user)
assert User.ap_id(user) == Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :feed_redirect, user.nickname)
assert User.ap_id(user) ==
Pleroma.Web.Router.Helpers.o_status_url(
Pleroma.Web.Endpoint,
:feed_redirect,
user.nickname
)
end end
test "returns an ap_followers link for a user" do test "returns an ap_followers link for a user" do
user = insert(:user) user = insert(:user)
assert User.ap_followers(user) == Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :feed_redirect, user.nickname) <> "/followers"
assert User.ap_followers(user) ==
Pleroma.Web.Router.Helpers.o_status_url(
Pleroma.Web.Endpoint,
:feed_redirect,
user.nickname
) <> "/followers"
end end
describe "remote user creation changeset" do describe "remote user creation changeset" do
@ -184,7 +199,8 @@ test "it confirms validity" do
test "it sets the follower_adress" do test "it sets the follower_adress" do
cs = User.remote_user_creation(@valid_remote) cs = User.remote_user_creation(@valid_remote)
# remote users get a fake local follower address # remote users get a fake local follower address
assert cs.changes.follower_address == User.ap_followers(%User{ nickname: @valid_remote[:nickname] }) assert cs.changes.follower_address ==
User.ap_followers(%User{nickname: @valid_remote[:nickname]})
end end
test "it enforces the fqn format for nicknames" do test "it enforces the fqn format for nicknames" do
@ -196,7 +212,7 @@ test "it enforces the fqn format for nicknames" do
test "it has required fields" do test "it has required fields" do
[:name, :nickname, :ap_id] [:name, :nickname, :ap_id]
|> Enum.each(fn (field) -> |> Enum.each(fn field ->
cs = User.remote_user_creation(Map.delete(@valid_remote, field)) cs = User.remote_user_creation(Map.delete(@valid_remote, field))
refute cs.valid? refute cs.valid?
end) end)
@ -204,7 +220,7 @@ test "it has required fields" do
test "it restricts some sizes" do test "it restricts some sizes" do
[bio: 5000, name: 100] [bio: 5000, name: 100]
|> Enum.each(fn ({field, size}) -> |> Enum.each(fn {field, size} ->
string = String.pad_leading(".", size) string = String.pad_leading(".", size)
cs = User.remote_user_creation(Map.put(@valid_remote, field, string)) cs = User.remote_user_creation(Map.put(@valid_remote, field, string))
assert cs.valid? assert cs.valid?
@ -323,7 +339,11 @@ test "get recipients from activity" do
user_two = insert(:user, local: false) user_two = insert(:user, local: false)
addressed = insert(:user, local: true) addressed = insert(:user, local: true)
addressed_remote = insert(:user, local: false) addressed_remote = insert(:user, local: false)
{:ok, activity} = CommonAPI.post(actor, %{"status" => "hey @#{addressed.nickname} @#{addressed_remote.nickname}"})
{:ok, activity} =
CommonAPI.post(actor, %{
"status" => "hey @#{addressed.nickname} @#{addressed_remote.nickname}"
})
assert [addressed] == User.get_recipients_from_activity(activity) assert [addressed] == User.get_recipients_from_activity(activity)
@ -379,7 +399,7 @@ test "get_public_key_for_ap_id fetches a user that's not in the db" do
test "insert or update a user from given data" do test "insert or update a user from given data" do
user = insert(:user, %{nickname: "nick@name.de"}) user = insert(:user, %{nickname: "nick@name.de"})
data = %{ ap_id: user.ap_id <> "xxx", name: user.name, nickname: user.nickname } data = %{ap_id: user.ap_id <> "xxx", name: user.name, nickname: user.nickname}
assert {:ok, %User{}} = User.insert_or_update_user(data) assert {:ok, %User{}} = User.insert_or_update_user(data)
end end

View file

@ -9,9 +9,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
test "it returns a json representation of the user", %{conn: conn} do test "it returns a json representation of the user", %{conn: conn} do
user = insert(:user) user = insert(:user)
conn = conn conn =
|> put_req_header("accept", "application/activity+json") conn
|> get("/users/#{user.nickname}") |> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}")
user = Repo.get(User, user.id) user = Repo.get(User, user.id)
@ -22,11 +23,12 @@ test "it returns a json representation of the user", %{conn: conn} do
describe "/object/:uuid" do describe "/object/:uuid" do
test "it returns a json representation of the object", %{conn: conn} do test "it returns a json representation of the object", %{conn: conn} do
note = insert(:note) note = insert(:note)
uuid = String.split(note.data["id"], "/") |> List.last uuid = String.split(note.data["id"], "/") |> List.last()
conn = conn conn =
|> put_req_header("accept", "application/activity+json") conn
|> get("/objects/#{uuid}") |> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note}) assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
end end
@ -34,12 +36,13 @@ test "it returns a json representation of the object", %{conn: conn} do
describe "/users/:nickname/inbox" do describe "/users/:nickname/inbox" do
test "it inserts an incoming activity into the database", %{conn: conn} do test "it inserts an incoming activity into the database", %{conn: conn} do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode! data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
conn = conn conn =
|> assign(:valid_signature, true) conn
|> put_req_header("content-type", "application/activity+json") |> assign(:valid_signature, true)
|> post("/inbox", data) |> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
assert "ok" == json_response(conn, 200) assert "ok" == json_response(conn, 200)
:timer.sleep(500) :timer.sleep(500)

View file

@ -22,7 +22,7 @@ test "it returns a user" do
describe "insertion" do describe "insertion" do
test "returns the activity if one with the same id is already in" do test "returns the activity if one with the same id is already in" do
activity = insert(:note_activity) activity = insert(:note_activity)
{:ok, new_activity}= ActivityPub.insert(activity.data) {:ok, new_activity} = ActivityPub.insert(activity.data)
assert activity == new_activity assert activity == new_activity
end end
@ -37,6 +37,7 @@ test "inserts a given map into the activity database, giving it an id if it has
assert is_binary(activity.data["id"]) assert is_binary(activity.data["id"])
given_id = "bla" given_id = "bla"
data = %{ data = %{
"ok" => true, "ok" => true,
"id" => given_id "id" => given_id
@ -63,7 +64,14 @@ test "adds an id to a given object if it lacks one and is a note and inserts it
describe "create activities" do describe "create activities" do
test "removes doubled 'to' recipients" do test "removes doubled 'to' recipients" do
{:ok, activity} = ActivityPub.create(%{to: ["user1", "user1", "user2"], actor: %User{ap_id: "1"}, context: "", object: %{}}) {:ok, activity} =
ActivityPub.create(%{
to: ["user1", "user1", "user2"],
actor: %User{ap_id: "1"},
context: "",
object: %{}
})
assert activity.data["to"] == ["user1", "user2"] assert activity.data["to"] == ["user1", "user2"]
assert activity.actor == "1" assert activity.actor == "1"
assert activity.recipients == ["user1", "user2"] assert activity.recipients == ["user1", "user2"]
@ -124,11 +132,11 @@ test "doesn't return blocked activities" do
describe "public fetch activities" do describe "public fetch activities" do
test "retrieves public activities" do test "retrieves public activities" do
_activities = ActivityPub.fetch_public_activities _activities = ActivityPub.fetch_public_activities()
%{public: public} = ActivityBuilder.public_and_non_public %{public: public} = ActivityBuilder.public_and_non_public()
activities = ActivityPub.fetch_public_activities activities = ActivityPub.fetch_public_activities()
assert length(activities) == 1 assert length(activities) == 1
assert Enum.at(activities, 0) == public assert Enum.at(activities, 0) == public
end end
@ -137,7 +145,7 @@ test "retrieves a maximum of 20 activities" do
activities = ActivityBuilder.insert_list(30) activities = ActivityBuilder.insert_list(30)
last_expected = List.last(activities) last_expected = List.last(activities)
activities = ActivityPub.fetch_public_activities activities = ActivityPub.fetch_public_activities()
last = List.last(activities) last = List.last(activities)
assert length(activities) == 20 assert length(activities) == 20
@ -232,7 +240,12 @@ test "adds an announce activity to the db" do
{:ok, announce_activity, object} = ActivityPub.announce(user, object) {:ok, announce_activity, object} = ActivityPub.announce(user, object)
assert object.data["announcement_count"] == 1 assert object.data["announcement_count"] == 1
assert object.data["announcements"] == [user.ap_id] assert object.data["announcements"] == [user.ap_id]
assert announce_activity.data["to"] == [User.ap_followers(user), note_activity.data["actor"]]
assert announce_activity.data["to"] == [
User.ap_followers(user),
note_activity.data["actor"]
]
assert announce_activity.data["object"] == object.data["id"] assert announce_activity.data["object"] == object.data["id"]
assert announce_activity.data["actor"] == user.ap_id assert announce_activity.data["actor"] == user.ap_id
assert announce_activity.data["context"] == object.data["context"] assert announce_activity.data["context"] == object.data["context"]
@ -241,7 +254,11 @@ test "adds an announce activity to the db" do
describe "uploading files" do describe "uploading files" do
test "copies the file to the configured folder" do test "copies the file to the configured folder" do
file = %Plug.Upload{content_type: "image/jpg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg"} file = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
{:ok, %Object{} = object} = ActivityPub.upload(file) {:ok, %Object{} = object} = ActivityPub.upload(file)
assert object.data["name"] == "an_image.jpg" assert object.data["name"] == "an_image.jpg"
@ -268,11 +285,14 @@ test "fetches the latest Follow activity" do
describe "fetching an object" do describe "fetching an object" do
test "it fetches an object" do test "it fetches an object" do
{:ok, object} = ActivityPub.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") {:ok, object} =
ActivityPub.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
assert activity = Activity.get_create_activity_by_object_ap_id(object.data["id"]) assert activity = Activity.get_create_activity_by_object_ap_id(object.data["id"])
assert activity.data["id"] assert activity.data["id"]
{:ok, object_again} = ActivityPub.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") {:ok, object_again} =
ActivityPub.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367")
assert [attachment] = object.data["attachment"] assert [attachment] = object.data["attachment"]
assert is_list(attachment["url"]) assert is_list(attachment["url"])
@ -285,7 +305,8 @@ test "it works with objects only available via Ostatus" do
assert activity = Activity.get_create_activity_by_object_ap_id(object.data["id"]) assert activity = Activity.get_create_activity_by_object_ap_id(object.data["id"])
assert activity.data["id"] assert activity.data["id"]
{:ok, object_again} = ActivityPub.fetch_object_from_id("https://shitposter.club/notice/2827873") {:ok, object_again} =
ActivityPub.fetch_object_from_id("https://shitposter.club/notice/2827873")
assert object == object_again assert object == object_again
end end
@ -344,7 +365,14 @@ test "it creates an update activity with the new user data" do
user = insert(:user) user = insert(:user)
{:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(user) {:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(user)
user_data = Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user}) user_data = Pleroma.Web.ActivityPub.UserView.render("user.json", %{user: user})
{:ok, update} = ActivityPub.update(%{actor: user_data["id"], to: [user.follower_address], cc: [], object: user_data})
{:ok, update} =
ActivityPub.update(%{
actor: user_data["id"],
to: [user.follower_address],
cc: [],
object: user_data
})
assert update.data["actor"] == user.ap_id assert update.data["actor"] == user.ap_id
assert update.data["to"] == [user.follower_address] assert update.data["to"] == [user.follower_address]

View file

@ -16,9 +16,10 @@ defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
test "it ignores an incoming notice if we already have it" do test "it ignores an incoming notice if we already have it" do
activity = insert(:note_activity) activity = insert(:note_activity)
data = File.read!("test/fixtures/mastodon-post-activity.json") data =
|> Poison.decode! File.read!("test/fixtures/mastodon-post-activity.json")
|> Map.put("object", activity.data["object"]) |> Poison.decode!()
|> Map.put("object", activity.data["object"])
{:ok, returned_activity} = Transmogrifier.handle_incoming(data) {:ok, returned_activity} = Transmogrifier.handle_incoming(data)
@ -26,51 +27,72 @@ test "it ignores an incoming notice if we already have it" do
end end
test "it fetches replied-to activities if we don't have them" do test "it fetches replied-to activities if we don't have them" do
data = File.read!("test/fixtures/mastodon-post-activity.json") data =
|> Poison.decode! File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
object = data["object"] object =
|> Map.put("inReplyTo", "https://shitposter.club/notice/2827873") data["object"]
|> Map.put("inReplyTo", "https://shitposter.club/notice/2827873")
data = data data =
|> Map.put("object", object) data
|> Map.put("object", object)
{:ok, returned_activity} = Transmogrifier.handle_incoming(data) {:ok, returned_activity} = Transmogrifier.handle_incoming(data)
assert activity = Activity.get_create_activity_by_object_ap_id("tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment") assert activity =
assert returned_activity.data["object"]["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873" Activity.get_create_activity_by_object_ap_id(
"tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
)
assert returned_activity.data["object"]["inReplyToAtomUri"] ==
"https://shitposter.club/notice/2827873"
assert returned_activity.data["object"]["inReplyToStatusId"] == activity.id assert returned_activity.data["object"]["inReplyToStatusId"] == activity.id
end end
test "it works for incoming notices" do test "it works for incoming notices" do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode! data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["id"] == "http://mastodon.example.org/users/admin/statuses/99512778738411822/activity"
assert data["context"] == "tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation" assert data["id"] ==
"http://mastodon.example.org/users/admin/statuses/99512778738411822/activity"
assert data["context"] ==
"tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation"
assert data["to"] == ["https://www.w3.org/ns/activitystreams#Public"] assert data["to"] == ["https://www.w3.org/ns/activitystreams#Public"]
assert data["cc"] == [ assert data["cc"] == [
"http://mastodon.example.org/users/admin/followers", "http://mastodon.example.org/users/admin/followers",
"http://localtesting.pleroma.lol/users/lain" "http://localtesting.pleroma.lol/users/lain"
] ]
assert data["actor"] == "http://mastodon.example.org/users/admin" assert data["actor"] == "http://mastodon.example.org/users/admin"
object = data["object"] object = data["object"]
assert object["id"] == "http://mastodon.example.org/users/admin/statuses/99512778738411822" assert object["id"] == "http://mastodon.example.org/users/admin/statuses/99512778738411822"
assert object["to"] == ["https://www.w3.org/ns/activitystreams#Public"] assert object["to"] == ["https://www.w3.org/ns/activitystreams#Public"]
assert object["cc"] == [ assert object["cc"] == [
"http://mastodon.example.org/users/admin/followers", "http://mastodon.example.org/users/admin/followers",
"http://localtesting.pleroma.lol/users/lain" "http://localtesting.pleroma.lol/users/lain"
] ]
assert object["actor"] == "http://mastodon.example.org/users/admin" assert object["actor"] == "http://mastodon.example.org/users/admin"
assert object["attributedTo"] == "http://mastodon.example.org/users/admin" assert object["attributedTo"] == "http://mastodon.example.org/users/admin"
assert object["context"] == "tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation"
assert object["context"] ==
"tag:mastodon.example.org,2018-02-12:objectId=20:objectType=Conversation"
assert object["sensitive"] == true assert object["sensitive"] == true
end end
test "it works for incoming notices with hashtags" do test "it works for incoming notices with hashtags" do
data = File.read!("test/fixtures/mastodon-post-activity-hashtag.json") |> Poison.decode! data = File.read!("test/fixtures/mastodon-post-activity-hashtag.json") |> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert Enum.at(data["object"]["tag"], 2) == "moo" assert Enum.at(data["object"]["tag"], 2) == "moo"
@ -78,8 +100,10 @@ test "it works for incoming notices with hashtags" do
test "it works for incoming follow requests" do test "it works for incoming follow requests" do
user = insert(:user) user = insert(:user)
data = File.read!("test/fixtures/mastodon-follow-activity.json") |> Poison.decode!
|> Map.put("object", user.ap_id) data =
File.read!("test/fixtures/mastodon-follow-activity.json") |> Poison.decode!()
|> Map.put("object", user.ap_id)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
@ -93,8 +117,9 @@ test "it works for incoming likes" do
user = insert(:user) user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hello"}) {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
data = File.read!("test/fixtures/mastodon-like.json") |> Poison.decode! data =
|> Map.put("object", activity.data["object"]["id"]) File.read!("test/fixtures/mastodon-like.json") |> Poison.decode!()
|> Map.put("object", activity.data["object"]["id"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
@ -105,14 +130,18 @@ test "it works for incoming likes" do
end end
test "it works for incoming announces" do test "it works for incoming announces" do
data = File.read!("test/fixtures/mastodon-announce.json") |> Poison.decode! data = File.read!("test/fixtures/mastodon-announce.json") |> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "http://mastodon.example.org/users/admin" assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "Announce" assert data["type"] == "Announce"
assert data["id"] == "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
assert data["object"] == "http://mastodon.example.org/users/admin/statuses/99541947525187367" assert data["id"] ==
"http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
assert data["object"] ==
"http://mastodon.example.org/users/admin/statuses/99541947525187367"
assert Activity.get_create_activity_by_object_ap_id(data["object"]) assert Activity.get_create_activity_by_object_ap_id(data["object"])
end end
@ -121,53 +150,77 @@ test "it works for incoming announces with an existing activity" do
user = insert(:user) user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey"}) {:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
data = File.read!("test/fixtures/mastodon-announce.json") data =
|> Poison.decode! File.read!("test/fixtures/mastodon-announce.json")
|> Map.put("object", activity.data["object"]["id"]) |> Poison.decode!()
|> Map.put("object", activity.data["object"]["id"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "http://mastodon.example.org/users/admin" assert data["actor"] == "http://mastodon.example.org/users/admin"
assert data["type"] == "Announce" assert data["type"] == "Announce"
assert data["id"] == "http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
assert data["id"] ==
"http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
assert data["object"] == activity.data["object"]["id"] assert data["object"] == activity.data["object"]["id"]
assert Activity.get_create_activity_by_object_ap_id(data["object"]).id == activity.id assert Activity.get_create_activity_by_object_ap_id(data["object"]).id == activity.id
end end
test "it works for incoming update activities" do test "it works for incoming update activities" do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode! data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!()
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode! update_data = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!()
object = update_data["object"]
|> Map.put("actor", data["actor"])
|> Map.put("id", data["actor"])
update_data = update_data object =
|> Map.put("actor", data["actor"]) update_data["object"]
|> Map.put("object", object) |> Map.put("actor", data["actor"])
|> Map.put("id", data["actor"])
update_data =
update_data
|> Map.put("actor", data["actor"])
|> Map.put("object", object)
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(update_data) {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(update_data)
user = User.get_cached_by_ap_id(data["actor"]) user = User.get_cached_by_ap_id(data["actor"])
assert user.name == "gargle" assert user.name == "gargle"
assert user.avatar["url"] == [%{"href" => "https://cd.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"}]
assert user.info["banner"]["url"] == [%{"href" => "https://cd.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"}] assert user.avatar["url"] == [
%{
"href" =>
"https://cd.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"
}
]
assert user.info["banner"]["url"] == [
%{
"href" =>
"https://cd.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"
}
]
assert user.bio == "<p>Some bio</p>" assert user.bio == "<p>Some bio</p>"
end end
test "it works for incoming deletes" do test "it works for incoming deletes" do
activity = insert(:note_activity) activity = insert(:note_activity)
data = File.read!("test/fixtures/mastodon-delete.json")
|> Poison.decode!
object = data["object"] data =
|> Map.put("id", activity.data["object"]["id"]) File.read!("test/fixtures/mastodon-delete.json")
|> Poison.decode!()
data = data object =
|> Map.put("object", object) data["object"]
|> Map.put("actor", activity.data["actor"]) |> Map.put("id", activity.data["object"]["id"])
data =
data
|> Map.put("object", object)
|> Map.put("actor", activity.data["actor"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data) {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
@ -180,7 +233,8 @@ test "it turns mentions into tags" do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey, @#{other_user.nickname}, how are ya? #2hu"}) {:ok, activity} =
CommonAPI.post(user, %{"status" => "hey, @#{other_user.nickname}, how are ya? #2hu"})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
object = modified["object"] object = modified["object"]
@ -192,7 +246,7 @@ test "it turns mentions into tags" do
} }
expected_tag = %{ expected_tag = %{
"href" => Pleroma.Web.Endpoint.url <> "/tags/2hu", "href" => Pleroma.Web.Endpoint.url() <> "/tags/2hu",
"type" => "Hashtag", "type" => "Hashtag",
"name" => "#2hu" "name" => "#2hu"
} }
@ -247,7 +301,9 @@ test "it translates ostatus reply_to IDs to external URLs" do
user = insert(:user) user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "HI!", "in_reply_to_status_id" => referred_activity.id}) {:ok, activity} =
CommonAPI.post(user, %{"status" => "HI!", "in_reply_to_status_id" => referred_activity.id})
{:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data)
assert modified["object"]["inReplyTo"] == "http://gs.example.org:4040/index.php/notice/29" assert modified["object"]["inReplyTo"] == "http://gs.example.org:4040/index.php/notice/29"
@ -256,7 +312,14 @@ test "it translates ostatus reply_to IDs to external URLs" do
describe "user upgrade" do describe "user upgrade" do
test "it upgrades a user to activitypub" do test "it upgrades a user to activitypub" do
user = insert(:user, %{nickname: "rye@niu.moe", local: false, ap_id: "https://niu.moe/users/rye", follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"})}) user =
insert(:user, %{
nickname: "rye@niu.moe",
local: false,
ap_id: "https://niu.moe/users/rye",
follower_address: User.ap_followers(%User{nickname: "rye@niu.moe"})
})
user_two = insert(:user, %{following: [user.follower_address]}) user_two = insert(:user, %{following: [user.follower_address]})
{:ok, activity} = CommonAPI.post(user, %{"status" => "test"}) {:ok, activity} = CommonAPI.post(user, %{"status" => "test"})
@ -279,8 +342,25 @@ test "it upgrades a user to activitypub" do
activity = Repo.get(Activity, activity.id) activity = Repo.get(Activity, activity.id)
assert user.follower_address in activity.recipients assert user.follower_address in activity.recipients
assert %{"url" => [%{"href" => "https://cdn.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"}]} = user.avatar
assert %{"url" => [%{"href" => "https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"}]} = user.info["banner"] assert %{
"url" => [
%{
"href" =>
"https://cdn.niu.moe/accounts/avatars/000/033/323/original/fd7f8ae0b3ffedc9.jpeg"
}
]
} = user.avatar
assert %{
"url" => [
%{
"href" =>
"https://cdn.niu.moe/accounts/headers/000/033/323/original/850b3448fa5fd477.png"
}
]
} = user.info["banner"]
refute "..." in activity.recipients refute "..." in activity.recipients
unrelated_activity = Repo.get(Activity, unrelated_activity.id) unrelated_activity = Repo.get(Activity, unrelated_activity.id)

View file

@ -3,7 +3,8 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
use Pleroma.DataCase use Pleroma.DataCase
test "it adds attachment links to a given text and attachment set" do test "it adds attachment links to a given text and attachment set" do
name = "Sakura%20Mana%20%E2%80%93%20Turned%20on%20by%20a%20Senior%20OL%20with%20a%20Temptating%20Tight%20Skirt-s%20Full%20Hipline%20and%20Panty%20Shot-%20Beautiful%20Thick%20Thighs-%20and%20Erotic%20Ass-%20-2015-%20--%20Oppaitime%208-28-2017%206-50-33%20PM.png" name =
"Sakura%20Mana%20%E2%80%93%20Turned%20on%20by%20a%20Senior%20OL%20with%20a%20Temptating%20Tight%20Skirt-s%20Full%20Hipline%20and%20Panty%20Shot-%20Beautiful%20Thick%20Thighs-%20and%20Erotic%20Ass-%20-2015-%20--%20Oppaitime%208-28-2017%206-50-33%20PM.png"
attachment = %{ attachment = %{
"url" => [%{"href" => name}] "url" => [%{"href" => name}]
@ -11,6 +12,7 @@ test "it adds attachment links to a given text and attachment set" do
res = Utils.add_attachments("", [attachment]) res = Utils.add_attachments("", [attachment])
assert res == "<br><a href=\"#{name}\" class='attachment'>Sakura Mana Turned on by a Se…</a>" assert res ==
"<br><a href=\"#{name}\" class='attachment'>Sakura Mana Turned on by a Se…</a>"
end end
end end

View file

@ -5,7 +5,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
alias Pleroma.User alias Pleroma.User
test "Represent a user account" do test "Represent a user account" do
user = insert(:user, %{info: %{"note_count" => 5, "follower_count" => 3}, nickname: "shp@shitposter.club", inserted_at: ~N[2017-08-15 15:47:06.597036]}) user =
insert(:user, %{
info: %{"note_count" => 5, "follower_count" => 3},
nickname: "shp@shitposter.club",
inserted_at: ~N[2017-08-15 15:47:06.597036]
})
expected = %{ expected = %{
id: to_string(user.id), id: to_string(user.id),

View file

@ -14,17 +14,19 @@ test "the home timeline", %{conn: conn} do
{:ok, _activity} = TwitterAPI.create_status(following, %{"status" => "test"}) {:ok, _activity} = TwitterAPI.create_status(following, %{"status" => "test"})
conn = conn conn =
|> assign(:user, user) conn
|> get("/api/v1/timelines/home") |> assign(:user, user)
|> get("/api/v1/timelines/home")
assert length(json_response(conn, 200)) == 0 assert length(json_response(conn, 200)) == 0
{:ok, user} = User.follow(user, following) {:ok, user} = User.follow(user, following)
conn = build_conn() conn =
|> assign(:user, user) build_conn()
|> get("/api/v1/timelines/home") |> assign(:user, user)
|> get("/api/v1/timelines/home")
assert [%{"content" => "test"}] = json_response(conn, 200) assert [%{"content" => "test"}] = json_response(conn, 200)
end end
@ -32,44 +34,57 @@ test "the home timeline", %{conn: conn} do
test "the public timeline", %{conn: conn} do test "the public timeline", %{conn: conn} do
following = insert(:user) following = insert(:user)
capture_log fn -> capture_log(fn ->
{:ok, _activity} = TwitterAPI.create_status(following, %{"status" => "test"}) {:ok, _activity} = TwitterAPI.create_status(following, %{"status" => "test"})
{:ok, [_activity]} = OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873")
conn = conn {:ok, [_activity]} =
|> get("/api/v1/timelines/public", %{"local" => "False"}) OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873")
conn =
conn
|> get("/api/v1/timelines/public", %{"local" => "False"})
assert length(json_response(conn, 200)) == 2 assert length(json_response(conn, 200)) == 2
conn = build_conn() conn =
|> get("/api/v1/timelines/public", %{"local" => "True"}) build_conn()
|> get("/api/v1/timelines/public", %{"local" => "True"})
assert [%{"content" => "test"}] = json_response(conn, 200) assert [%{"content" => "test"}] = json_response(conn, 200)
conn = build_conn() conn =
|> get("/api/v1/timelines/public", %{"local" => "1"}) build_conn()
|> get("/api/v1/timelines/public", %{"local" => "1"})
assert [%{"content" => "test"}] = json_response(conn, 200) assert [%{"content" => "test"}] = json_response(conn, 200)
end end)
end end
test "posting a status", %{conn: conn} do test "posting a status", %{conn: conn} do
user = insert(:user) user = insert(:user)
conn = conn conn =
|> assign(:user, user) conn
|> post("/api/v1/statuses", %{"status" => "cofe", "spoiler_text" => "2hu", "sensitive" => "false"}) |> assign(:user, user)
|> post("/api/v1/statuses", %{
"status" => "cofe",
"spoiler_text" => "2hu",
"sensitive" => "false"
})
assert %{"content" => "cofe", "id" => id, "spoiler_text" => "2hu", "sensitive" => false} =
json_response(conn, 200)
assert %{"content" => "cofe", "id" => id, "spoiler_text" => "2hu", "sensitive" => false} = json_response(conn, 200)
assert Repo.get(Activity, id) assert Repo.get(Activity, id)
end end
test "posting a sensitive status", %{conn: conn} do test "posting a sensitive status", %{conn: conn} do
user = insert(:user) user = insert(:user)
conn = conn conn =
|> assign(:user, user) conn
|> post("/api/v1/statuses", %{"status" => "cofe", "sensitive" => true}) |> assign(:user, user)
|> post("/api/v1/statuses", %{"status" => "cofe", "sensitive" => true})
assert %{"content" => "cofe", "id" => id, "sensitive" => true} = json_response(conn, 200) assert %{"content" => "cofe", "id" => id, "sensitive" => true} = json_response(conn, 200)
assert Repo.get(Activity, id) assert Repo.get(Activity, id)
@ -80,9 +95,10 @@ test "replying to a status", %{conn: conn} do
{:ok, replied_to} = TwitterAPI.create_status(user, %{"status" => "cofe"}) {:ok, replied_to} = TwitterAPI.create_status(user, %{"status" => "cofe"})
conn = conn conn =
|> assign(:user, user) conn
|> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id}) |> assign(:user, user)
|> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => replied_to.id})
assert %{"content" => "xD", "id" => id} = json_response(conn, 200) assert %{"content" => "xD", "id" => id} = json_response(conn, 200)
@ -95,9 +111,10 @@ test "replying to a status", %{conn: conn} do
test "verify_credentials", %{conn: conn} do test "verify_credentials", %{conn: conn} do
user = insert(:user) user = insert(:user)
conn = conn conn =
|> assign(:user, user) conn
|> get("/api/v1/accounts/verify_credentials") |> assign(:user, user)
|> get("/api/v1/accounts/verify_credentials")
assert %{"id" => id} = json_response(conn, 200) assert %{"id" => id} = json_response(conn, 200)
assert id == to_string(user.id) assert id == to_string(user.id)
@ -106,8 +123,9 @@ test "verify_credentials", %{conn: conn} do
test "get a status", %{conn: conn} do test "get a status", %{conn: conn} do
activity = insert(:note_activity) activity = insert(:note_activity)
conn = conn conn =
|> get("/api/v1/statuses/#{activity.id}") conn
|> get("/api/v1/statuses/#{activity.id}")
assert %{"id" => id} = json_response(conn, 200) assert %{"id" => id} = json_response(conn, 200)
assert id == to_string(activity.id) assert id == to_string(activity.id)
@ -118,9 +136,10 @@ test "when you created it", %{conn: conn} do
activity = insert(:note_activity) activity = insert(:note_activity)
author = User.get_by_ap_id(activity.data["actor"]) author = User.get_by_ap_id(activity.data["actor"])
conn = conn conn =
|> assign(:user, author) conn
|> delete("/api/v1/statuses/#{activity.id}") |> assign(:user, author)
|> delete("/api/v1/statuses/#{activity.id}")
assert %{} = json_response(conn, 200) assert %{} = json_response(conn, 200)
@ -131,9 +150,10 @@ test "when you didn't create it", %{conn: conn} do
activity = insert(:note_activity) activity = insert(:note_activity)
user = insert(:user) user = insert(:user)
conn = conn conn =
|> assign(:user, user) conn
|> delete("/api/v1/statuses/#{activity.id}") |> assign(:user, user)
|> delete("/api/v1/statuses/#{activity.id}")
assert %{"error" => _} = json_response(conn, 403) assert %{"error" => _} = json_response(conn, 403)
@ -146,14 +166,19 @@ test "list of notifications", %{conn: conn} do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
{:ok, activity} = TwitterAPI.create_status(other_user, %{"status" => "hi @#{user.nickname}"}) {:ok, activity} =
TwitterAPI.create_status(other_user, %{"status" => "hi @#{user.nickname}"})
{:ok, [_notification]} = Notification.create_notifications(activity) {:ok, [_notification]} = Notification.create_notifications(activity)
conn = conn conn =
|> assign(:user, user) conn
|> get("/api/v1/notifications") |> assign(:user, user)
|> get("/api/v1/notifications")
expected_response =
"hi <span><a href=\"#{user.ap_id}\">@<span>#{user.nickname}</span></a></span>"
expected_response = "hi <span><a href=\"#{user.ap_id}\">@<span>#{user.nickname}</span></a></span>"
assert [%{"status" => %{"content" => response}} | _rest] = json_response(conn, 200) assert [%{"status" => %{"content" => response}} | _rest] = json_response(conn, 200)
assert response == expected_response assert response == expected_response
end end
@ -162,14 +187,19 @@ test "getting a single notification", %{conn: conn} do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
{:ok, activity} = TwitterAPI.create_status(other_user, %{"status" => "hi @#{user.nickname}"}) {:ok, activity} =
TwitterAPI.create_status(other_user, %{"status" => "hi @#{user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity) {:ok, [notification]} = Notification.create_notifications(activity)
conn = conn conn =
|> assign(:user, user) conn
|> get("/api/v1/notifications/#{notification.id}") |> assign(:user, user)
|> get("/api/v1/notifications/#{notification.id}")
expected_response =
"hi <span><a href=\"#{user.ap_id}\">@<span>#{user.nickname}</span></a></span>"
expected_response = "hi <span><a href=\"#{user.ap_id}\">@<span>#{user.nickname}</span></a></span>"
assert %{"status" => %{"content" => response}} = json_response(conn, 200) assert %{"status" => %{"content" => response}} = json_response(conn, 200)
assert response == expected_response assert response == expected_response
end end
@ -178,12 +208,15 @@ test "dismissing a single notification", %{conn: conn} do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
{:ok, activity} = TwitterAPI.create_status(other_user, %{"status" => "hi @#{user.nickname}"}) {:ok, activity} =
TwitterAPI.create_status(other_user, %{"status" => "hi @#{user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity) {:ok, [notification]} = Notification.create_notifications(activity)
conn = conn conn =
|> assign(:user, user) conn
|> post("/api/v1/notifications/dismiss", %{"id" => notification.id}) |> assign(:user, user)
|> post("/api/v1/notifications/dismiss", %{"id" => notification.id})
assert %{} = json_response(conn, 200) assert %{} = json_response(conn, 200)
end end
@ -192,18 +225,22 @@ test "clearing all notifications", %{conn: conn} do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
{:ok, activity} = TwitterAPI.create_status(other_user, %{"status" => "hi @#{user.nickname}"}) {:ok, activity} =
TwitterAPI.create_status(other_user, %{"status" => "hi @#{user.nickname}"})
{:ok, [_notification]} = Notification.create_notifications(activity) {:ok, [_notification]} = Notification.create_notifications(activity)
conn = conn conn =
|> assign(:user, user) conn
|> post("/api/v1/notifications/clear") |> assign(:user, user)
|> post("/api/v1/notifications/clear")
assert %{} = json_response(conn, 200) assert %{} = json_response(conn, 200)
conn = build_conn() conn =
|> assign(:user, user) build_conn()
|> get("/api/v1/notifications") |> assign(:user, user)
|> get("/api/v1/notifications")
assert all = json_response(conn, 200) assert all = json_response(conn, 200)
assert all == [] assert all == []
@ -215,11 +252,14 @@ test "reblogs and returns the reblogged status", %{conn: conn} do
activity = insert(:note_activity) activity = insert(:note_activity)
user = insert(:user) user = insert(:user)
conn = conn conn =
|> assign(:user, user) conn
|> post("/api/v1/statuses/#{activity.id}/reblog") |> assign(:user, user)
|> post("/api/v1/statuses/#{activity.id}/reblog")
assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} =
json_response(conn, 200)
assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} = json_response(conn, 200)
assert to_string(activity.id) == id assert to_string(activity.id) == id
end end
end end
@ -229,11 +269,14 @@ test "favs a status and returns it", %{conn: conn} do
activity = insert(:note_activity) activity = insert(:note_activity)
user = insert(:user) user = insert(:user)
conn = conn conn =
|> assign(:user, user) conn
|> post("/api/v1/statuses/#{activity.id}/favourite") |> assign(:user, user)
|> post("/api/v1/statuses/#{activity.id}/favourite")
assert %{"id" => id, "favourites_count" => 1, "favourited" => true} =
json_response(conn, 200)
assert %{"id" => id, "favourites_count" => 1, "favourited" => true} = json_response(conn, 200)
assert to_string(activity.id) == id assert to_string(activity.id) == id
end end
end end
@ -245,11 +288,14 @@ test "unfavorites a status and returns it", %{conn: conn} do
{:ok, _, _} = CommonAPI.favorite(activity.id, user) {:ok, _, _} = CommonAPI.favorite(activity.id, user)
conn = conn conn =
|> assign(:user, user) conn
|> post("/api/v1/statuses/#{activity.id}/unfavourite") |> assign(:user, user)
|> post("/api/v1/statuses/#{activity.id}/unfavourite")
assert %{"id" => id, "favourites_count" => 0, "favourited" => false} =
json_response(conn, 200)
assert %{"id" => id, "favourites_count" => 0, "favourited" => false} = json_response(conn, 200)
assert to_string(activity.id) == id assert to_string(activity.id) == id
end end
end end
@ -261,8 +307,9 @@ test "gets a users statuses", %{conn: conn} do
user = User.get_by_ap_id(note_two.data["actor"]) user = User.get_by_ap_id(note_two.data["actor"])
conn = conn conn =
|> get("/api/v1/accounts/#{user.id}/statuses") conn
|> get("/api/v1/accounts/#{user.id}/statuses")
assert [%{"id" => id}] = json_response(conn, 200) assert [%{"id" => id}] = json_response(conn, 200)
@ -273,20 +320,29 @@ test "gets an users media", %{conn: conn} do
note = insert(:note_activity) note = insert(:note_activity)
user = User.get_by_ap_id(note.data["actor"]) user = User.get_by_ap_id(note.data["actor"])
file = %Plug.Upload{content_type: "image/jpg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg"} file = %Plug.Upload{
media = TwitterAPI.upload(file, "json") content_type: "image/jpg",
|> Poison.decode! path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
{:ok, image_post} = TwitterAPI.create_status(user, %{"status" => "cofe", "media_ids" => [media["media_id"]]}) media =
TwitterAPI.upload(file, "json")
|> Poison.decode!()
conn = conn {:ok, image_post} =
|> get("/api/v1/accounts/#{user.id}/statuses", %{"only_media" => "true"}) TwitterAPI.create_status(user, %{"status" => "cofe", "media_ids" => [media["media_id"]]})
conn =
conn
|> get("/api/v1/accounts/#{user.id}/statuses", %{"only_media" => "true"})
assert [%{"id" => id}] = json_response(conn, 200) assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(image_post.id) assert id == to_string(image_post.id)
conn = build_conn() conn =
|> get("/api/v1/accounts/#{user.id}/statuses", %{"only_media" => "1"}) build_conn()
|> get("/api/v1/accounts/#{user.id}/statuses", %{"only_media" => "1"})
assert [%{"id" => id}] = json_response(conn, 200) assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(image_post.id) assert id == to_string(image_post.id)
@ -299,9 +355,10 @@ test "returns the relationships for the current user", %{conn: conn} do
other_user = insert(:user) other_user = insert(:user)
{:ok, user} = User.follow(user, other_user) {:ok, user} = User.follow(user, other_user)
conn = conn conn =
|> assign(:user, user) conn
|> get("/api/v1/accounts/relationships", %{"id" => [other_user.id]}) |> assign(:user, user)
|> get("/api/v1/accounts/relationships", %{"id" => [other_user.id]})
assert [relationship] = json_response(conn, 200) assert [relationship] = json_response(conn, 200)
@ -312,26 +369,33 @@ test "returns the relationships for the current user", %{conn: conn} do
test "account fetching", %{conn: conn} do test "account fetching", %{conn: conn} do
user = insert(:user) user = insert(:user)
conn = conn conn =
|> get("/api/v1/accounts/#{user.id}") conn
|> get("/api/v1/accounts/#{user.id}")
assert %{"id" => id} = json_response(conn, 200) assert %{"id" => id} = json_response(conn, 200)
assert id == to_string(user.id) assert id == to_string(user.id)
conn = build_conn() conn =
|> get("/api/v1/accounts/-1") build_conn()
|> get("/api/v1/accounts/-1")
assert %{"error" => "Can't find user"} = json_response(conn, 404) assert %{"error" => "Can't find user"} = json_response(conn, 404)
end end
test "media upload", %{conn: conn} do test "media upload", %{conn: conn} do
file = %Plug.Upload{content_type: "image/jpg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg"} file = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
user = insert(:user) user = insert(:user)
conn = conn conn =
|> assign(:user, user) conn
|> post("/api/v1/media", %{"file" => file}) |> assign(:user, user)
|> post("/api/v1/media", %{"file" => file})
assert media = json_response(conn, 200) assert media = json_response(conn, 200)
@ -341,16 +405,20 @@ test "media upload", %{conn: conn} do
test "hashtag timeline", %{conn: conn} do test "hashtag timeline", %{conn: conn} do
following = insert(:user) following = insert(:user)
capture_log fn -> capture_log(fn ->
{:ok, activity} = TwitterAPI.create_status(following, %{"status" => "test #2hu"}) {:ok, activity} = TwitterAPI.create_status(following, %{"status" => "test #2hu"})
{:ok, [_activity]} = OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873")
conn = conn {:ok, [_activity]} =
|> get("/api/v1/timelines/tag/2hu") OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873")
conn =
conn
|> get("/api/v1/timelines/tag/2hu")
assert [%{"id" => id}] = json_response(conn, 200) assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(activity.id) assert id == to_string(activity.id)
end end)
end end
test "getting followers", %{conn: conn} do test "getting followers", %{conn: conn} do
@ -358,8 +426,9 @@ test "getting followers", %{conn: conn} do
other_user = insert(:user) other_user = insert(:user)
{:ok, user} = User.follow(user, other_user) {:ok, user} = User.follow(user, other_user)
conn = conn conn =
|> get("/api/v1/accounts/#{other_user.id}/followers") conn
|> get("/api/v1/accounts/#{other_user.id}/followers")
assert [%{"id" => id}] = json_response(conn, 200) assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(user.id) assert id == to_string(user.id)
@ -370,8 +439,9 @@ test "getting following", %{conn: conn} do
other_user = insert(:user) other_user = insert(:user)
{:ok, user} = User.follow(user, other_user) {:ok, user} = User.follow(user, other_user)
conn = conn conn =
|> get("/api/v1/accounts/#{user.id}/following") conn
|> get("/api/v1/accounts/#{user.id}/following")
assert [%{"id" => id}] = json_response(conn, 200) assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(other_user.id) assert id == to_string(other_user.id)
@ -381,23 +451,28 @@ test "following / unfollowing a user", %{conn: conn} do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
conn = conn conn =
|> assign(:user, user) conn
|> post("/api/v1/accounts/#{other_user.id}/follow") |> assign(:user, user)
|> post("/api/v1/accounts/#{other_user.id}/follow")
assert %{"id" => _id, "following" => true} = json_response(conn, 200) assert %{"id" => _id, "following" => true} = json_response(conn, 200)
user = Repo.get(User, user.id) user = Repo.get(User, user.id)
conn = build_conn()
|> assign(:user, user) conn =
|> post("/api/v1/accounts/#{other_user.id}/unfollow") build_conn()
|> assign(:user, user)
|> post("/api/v1/accounts/#{other_user.id}/unfollow")
assert %{"id" => _id, "following" => false} = json_response(conn, 200) assert %{"id" => _id, "following" => false} = json_response(conn, 200)
user = Repo.get(User, user.id) user = Repo.get(User, user.id)
conn = build_conn()
|> assign(:user, user) conn =
|> post("/api/v1/follows", %{"uri" => other_user.nickname}) build_conn()
|> assign(:user, user)
|> post("/api/v1/follows", %{"uri" => other_user.nickname})
assert %{"id" => id} = json_response(conn, 200) assert %{"id" => id} = json_response(conn, 200)
assert id == to_string(other_user.id) assert id == to_string(other_user.id)
@ -407,16 +482,19 @@ test "blocking / unblocking a user", %{conn: conn} do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
conn = conn conn =
|> assign(:user, user) conn
|> post("/api/v1/accounts/#{other_user.id}/block") |> assign(:user, user)
|> post("/api/v1/accounts/#{other_user.id}/block")
assert %{"id" => _id, "blocking" => true} = json_response(conn, 200) assert %{"id" => _id, "blocking" => true} = json_response(conn, 200)
user = Repo.get(User, user.id) user = Repo.get(User, user.id)
conn = build_conn()
|> assign(:user, user) conn =
|> post("/api/v1/accounts/#{other_user.id}/unblock") build_conn()
|> assign(:user, user)
|> post("/api/v1/accounts/#{other_user.id}/unblock")
assert %{"id" => _id, "blocking" => false} = json_response(conn, 200) assert %{"id" => _id, "blocking" => false} = json_response(conn, 200)
end end
@ -427,9 +505,10 @@ test "getting a list of blocks", %{conn: conn} do
{:ok, user} = User.block(user, other_user) {:ok, user} = User.block(user, other_user)
conn = conn conn =
|> assign(:user, user) conn
|> get("/api/v1/blocks") |> assign(:user, user)
|> get("/api/v1/blocks")
other_user_id = to_string(other_user.id) other_user_id = to_string(other_user.id)
assert [%{"id" => ^other_user_id}] = json_response(conn, 200) assert [%{"id" => ^other_user_id}] = json_response(conn, 200)
@ -440,10 +519,11 @@ test "unimplemented mute endpoints" do
other_user = insert(:user) other_user = insert(:user)
["mute", "unmute"] ["mute", "unmute"]
|> Enum.each(fn(endpoint) -> |> Enum.each(fn endpoint ->
conn = build_conn() conn =
|> assign(:user, user) build_conn()
|> post("/api/v1/accounts/#{other_user.id}/#{endpoint}") |> assign(:user, user)
|> post("/api/v1/accounts/#{other_user.id}/#{endpoint}")
assert %{"id" => id} = json_response(conn, 200) assert %{"id" => id} = json_response(conn, 200)
assert id == to_string(other_user.id) assert id == to_string(other_user.id)
@ -454,10 +534,11 @@ test "unimplemented mutes, follow_requests, blocks, domain blocks" do
user = insert(:user) user = insert(:user)
["blocks", "domain_blocks", "mutes", "follow_requests"] ["blocks", "domain_blocks", "mutes", "follow_requests"]
|> Enum.each(fn(endpoint) -> |> Enum.each(fn endpoint ->
conn = build_conn() conn =
|> assign(:user, user) build_conn()
|> get("/api/v1/#{endpoint}") |> assign(:user, user)
|> get("/api/v1/#{endpoint}")
assert [] = json_response(conn, 200) assert [] = json_response(conn, 200)
end) end)
@ -468,9 +549,10 @@ test "account search", %{conn: conn} do
_user_two = insert(:user, %{nickname: "shp@shitposter.club"}) _user_two = insert(:user, %{nickname: "shp@shitposter.club"})
user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"}) user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"})
conn = conn conn =
|> assign(:user, user) conn
|> get("/api/v1/accounts/search", %{"q" => "2hu"}) |> assign(:user, user)
|> get("/api/v1/accounts/search", %{"q" => "2hu"})
assert [account] = json_response(conn, 200) assert [account] = json_response(conn, 200)
assert account["id"] == to_string(user_three.id) assert account["id"] == to_string(user_three.id)
@ -484,8 +566,9 @@ test "search", %{conn: conn} do
{:ok, activity} = CommonAPI.post(user, %{"status" => "This is about 2hu"}) {:ok, activity} = CommonAPI.post(user, %{"status" => "This is about 2hu"})
{:ok, _} = CommonAPI.post(user_two, %{"status" => "This isn't"}) {:ok, _} = CommonAPI.post(user_two, %{"status" => "This isn't"})
conn = conn conn =
|> get("/api/v1/search", %{"q" => "2hu"}) conn
|> get("/api/v1/search", %{"q" => "2hu"})
assert results = json_response(conn, 200) assert results = json_response(conn, 200)
@ -499,19 +582,22 @@ test "search", %{conn: conn} do
end end
test "search fetches remote statuses", %{conn: conn} do test "search fetches remote statuses", %{conn: conn} do
capture_log fn -> capture_log(fn ->
conn = conn conn =
|> get("/api/v1/search", %{"q" => "https://shitposter.club/notice/2827873"}) conn
|> get("/api/v1/search", %{"q" => "https://shitposter.club/notice/2827873"})
assert results = json_response(conn, 200) assert results = json_response(conn, 200)
[status] = results["statuses"] [status] = results["statuses"]
assert status["uri"] == "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment" assert status["uri"] == "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
end end)
end end
test "search fetches remote accounts", %{conn: conn} do test "search fetches remote accounts", %{conn: conn} do
conn = conn conn =
|> get("/api/v1/search", %{"q" => "shp@social.heldscal.la", "resolve" => "true"}) conn
|> get("/api/v1/search", %{"q" => "shp@social.heldscal.la", "resolve" => "true"})
assert results = json_response(conn, 200) assert results = json_response(conn, 200)
[account] = results["accounts"] [account] = results["accounts"]
@ -527,9 +613,10 @@ test "returns the favorites of a user", %{conn: conn} do
{:ok, _, _} = CommonAPI.favorite(activity.id, user) {:ok, _, _} = CommonAPI.favorite(activity.id, user)
conn = conn conn =
|> assign(:user, user) conn
|> get("/api/v1/favourites") |> assign(:user, user)
|> get("/api/v1/favourites")
assert [status] = json_response(conn, 200) assert [status] = json_response(conn, 200)
assert status["id"] == to_string(activity.id) assert status["id"] == to_string(activity.id)
@ -539,9 +626,10 @@ test "returns the favorites of a user", %{conn: conn} do
test "updates the user's bio", %{conn: conn} do test "updates the user's bio", %{conn: conn} do
user = insert(:user) user = insert(:user)
conn = conn conn =
|> assign(:user, user) conn
|> patch("/api/v1/accounts/update_credentials", %{"note" => "I drink #cofe"}) |> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{"note" => "I drink #cofe"})
assert user = json_response(conn, 200) assert user = json_response(conn, 200)
assert user["note"] == "I drink #cofe" assert user["note"] == "I drink #cofe"
@ -550,9 +638,10 @@ test "updates the user's bio", %{conn: conn} do
test "updates the user's name", %{conn: conn} do test "updates the user's name", %{conn: conn} do
user = insert(:user) user = insert(:user)
conn = conn conn =
|> assign(:user, user) conn
|> patch("/api/v1/accounts/update_credentials", %{"display_name" => "markorepairs"}) |> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{"display_name" => "markorepairs"})
assert user = json_response(conn, 200) assert user = json_response(conn, 200)
assert user["display_name"] == "markorepairs" assert user["display_name"] == "markorepairs"
@ -561,11 +650,16 @@ test "updates the user's name", %{conn: conn} do
test "updates the user's avatar", %{conn: conn} do test "updates the user's avatar", %{conn: conn} do
user = insert(:user) user = insert(:user)
new_avatar = %Plug.Upload{content_type: "image/jpg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg"} new_avatar = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
conn = conn conn =
|> assign(:user, user) conn
|> patch("/api/v1/accounts/update_credentials", %{"avatar" => new_avatar}) |> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{"avatar" => new_avatar})
assert user = json_response(conn, 200) assert user = json_response(conn, 200)
assert user["avatar"] != "https://placehold.it/48x48" assert user["avatar"] != "https://placehold.it/48x48"
@ -574,11 +668,16 @@ test "updates the user's avatar", %{conn: conn} do
test "updates the user's banner", %{conn: conn} do test "updates the user's banner", %{conn: conn} do
user = insert(:user) user = insert(:user)
new_header = %Plug.Upload{content_type: "image/jpg", path: Path.absname("test/fixtures/image.jpg"), filename: "an_image.jpg"} new_header = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
conn = conn conn =
|> assign(:user, user) conn
|> patch("/api/v1/accounts/update_credentials", %{"header" => new_header}) |> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{"header" => new_header})
assert user = json_response(conn, 200) assert user = json_response(conn, 200)
assert user["header"] != "https://placehold.it/700x335" assert user["header"] != "https://placehold.it/700x335"
@ -594,8 +693,9 @@ test "get instance information", %{conn: conn} do
Pleroma.Stats.update_stats() Pleroma.Stats.update_stats()
conn = conn conn =
|> get("/api/v1/instance") conn
|> get("/api/v1/instance")
assert result = json_response(conn, 200) assert result = json_response(conn, 200)

View file

@ -13,8 +13,9 @@ test "a note activity" do
status = StatusView.render("status.json", %{activity: note}) status = StatusView.render("status.json", %{activity: note})
created_at = (note.data["object"]["published"] || "") created_at =
|> String.replace(~r/\.\d+Z/, ".000Z") (note.data["object"]["published"] || "")
|> String.replace(~r/\.\d+Z/, ".000Z")
expected = %{ expected = %{
id: to_string(note.id), id: to_string(note.id),
@ -57,7 +58,9 @@ test "a note activity" do
test "a reply" do test "a reply" do
note = insert(:note_activity) note = insert(:note_activity)
user = insert(:user) user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "he", "in_reply_to_status_id" => note.id})
{:ok, activity} =
CommonAPI.post(user, %{"status" => "he", "in_reply_to_status_id" => note.id})
status = StatusView.render("status.json", %{activity: activity}) status = StatusView.render("status.json", %{activity: activity})

View file

@ -4,7 +4,15 @@ defmodule Pleroma.Web.OAuth.AuthorizationTest do
import Pleroma.Factory import Pleroma.Factory
test "create an authorization token for a valid app" do test "create an authorization token for a valid app" do
{:ok, app} = Repo.insert(App.register_changeset(%App{}, %{client_name: "client", scopes: "scope", redirect_uris: "url"})) {:ok, app} =
Repo.insert(
App.register_changeset(%App{}, %{
client_name: "client",
scopes: "scope",
redirect_uris: "url"
})
)
user = insert(:user) user = insert(:user)
{:ok, auth} = Authorization.create_authorization(app, user) {:ok, auth} = Authorization.create_authorization(app, user)
@ -16,7 +24,15 @@ test "create an authorization token for a valid app" do
end end
test "use up a token" do test "use up a token" do
{:ok, app} = Repo.insert(App.register_changeset(%App{}, %{client_name: "client", scopes: "scope", redirect_uris: "url"})) {:ok, app} =
Repo.insert(
App.register_changeset(%App{}, %{
client_name: "client",
scopes: "scope",
redirect_uris: "url"
})
)
user = insert(:user) user = insert(:user)
{:ok, auth} = Authorization.create_authorization(app, user) {:ok, auth} = Authorization.create_authorization(app, user)
@ -30,7 +46,7 @@ test "use up a token" do
expired_auth = %Authorization{ expired_auth = %Authorization{
user_id: user.id, user_id: user.id,
app_id: app.id, app_id: app.id,
valid_until: NaiveDateTime.add(NaiveDateTime.utc_now, -10), valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), -10),
token: "mytoken", token: "mytoken",
used: false used: false
} }

View file

@ -6,7 +6,15 @@ defmodule Pleroma.Web.OAuth.TokenTest do
import Pleroma.Factory import Pleroma.Factory
test "exchanges a auth token for an access token" do test "exchanges a auth token for an access token" do
{:ok, app} = Repo.insert(App.register_changeset(%App{}, %{client_name: "client", scopes: "scope", redirect_uris: "url"})) {:ok, app} =
Repo.insert(
App.register_changeset(%App{}, %{
client_name: "client",
scopes: "scope",
redirect_uris: "url"
})
)
user = insert(:user) user = insert(:user)
{:ok, auth} = Authorization.create_authorization(app, user) {:ok, auth} = Authorization.create_authorization(app, user)

View file

@ -16,9 +16,12 @@ test "an external note activity" do
tuple = ActivityRepresenter.to_simple_form(activity, user) tuple = ActivityRepresenter.to_simple_form(activity, user)
res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary()
assert String.contains?(res, ~s{<link type="text/html" href="https://mastodon.social/users/lambadalambda/updates/2314748" rel="alternate"/>}) assert String.contains?(
res,
~s{<link type="text/html" href="https://mastodon.social/users/lambadalambda/updates/2314748" rel="alternate"/>}
)
end end
test "a note activity" do test "a note activity" do
@ -46,7 +49,7 @@ test "a note activity" do
tuple = ActivityRepresenter.to_simple_form(note_activity, user) tuple = ActivityRepresenter.to_simple_form(note_activity, user)
res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary()
assert clean(res) == clean(expected) assert clean(res) == clean(expected)
end end
@ -61,7 +64,10 @@ test "a reply note" do
answer = %{answer | data: data} answer = %{answer | data: data}
note_object = Object.get_by_ap_id(note.data["object"]["id"]) note_object = Object.get_by_ap_id(note.data["object"]["id"])
Repo.update!(Object.change(note_object, %{ data: Map.put(note_object.data, "external_url", "someurl") }))
Repo.update!(
Object.change(note_object, %{data: Map.put(note_object.data, "external_url", "someurl")})
)
user = User.get_cached_by_ap_id(answer.data["actor"]) user = User.get_cached_by_ap_id(answer.data["actor"])
@ -86,7 +92,7 @@ test "a reply note" do
tuple = ActivityRepresenter.to_simple_form(answer, user) tuple = ActivityRepresenter.to_simple_form(answer, user)
res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary()
assert clean(res) == clean(expected) assert clean(res) == clean(expected)
end end
@ -102,9 +108,11 @@ test "an announce activity" do
note_user = User.get_cached_by_ap_id(note.data["actor"]) note_user = User.get_cached_by_ap_id(note.data["actor"])
note = Repo.get(Activity, note.id) note = Repo.get(Activity, note.id)
note_xml = ActivityRepresenter.to_simple_form(note, note_user, true)
|> :xmerl.export_simple_content(:xmerl_xml) note_xml =
|> to_string ActivityRepresenter.to_simple_form(note, note_user, true)
|> :xmerl.export_simple_content(:xmerl_xml)
|> to_string
expected = """ expected = """
<activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type> <activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
@ -120,13 +128,16 @@ test "an announce activity" do
<activity:object> <activity:object>
#{note_xml} #{note_xml}
</activity:object> </activity:object>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="#{note.data["actor"]}"/> <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="#{
note.data["actor"]
}"/>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
""" """
announce_xml = ActivityRepresenter.to_simple_form(announce, user) announce_xml =
|> :xmerl.export_simple_content(:xmerl_xml) ActivityRepresenter.to_simple_form(announce, user)
|> to_string |> :xmerl.export_simple_content(:xmerl_xml)
|> to_string
assert clean(expected) == clean(announce_xml) assert clean(expected) == clean(announce_xml)
end end
@ -139,7 +150,7 @@ test "a like activity" do
tuple = ActivityRepresenter.to_simple_form(like, user) tuple = ActivityRepresenter.to_simple_form(like, user)
refute is_nil(tuple) refute is_nil(tuple)
res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary()
expected = """ expected = """
<activity:verb>http://activitystrea.ms/schema/1.0/favorite</activity:verb> <activity:verb>http://activitystrea.ms/schema/1.0/favorite</activity:verb>
@ -156,7 +167,9 @@ test "a like activity" do
<link ref="#{like.data["context"]}" rel="ostatus:conversation" /> <link ref="#{like.data["context"]}" rel="ostatus:conversation" />
<link rel="self" type="application/atom+xml" href="#{like.data["id"]}"/> <link rel="self" type="application/atom+xml" href="#{like.data["id"]}"/>
<thr:in-reply-to ref="#{note.data["id"]}" /> <thr:in-reply-to ref="#{note.data["id"]}" />
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="#{note.data["actor"]}"/> <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="#{
note.data["actor"]
}"/>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/> <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/collection" href="http://activityschema.org/collection/public"/>
""" """
@ -166,18 +179,20 @@ test "a like activity" do
test "a follow activity" do test "a follow activity" do
follower = insert(:user) follower = insert(:user)
followed = insert(:user) followed = insert(:user)
{:ok, activity} = ActivityPub.insert(%{
"type" => "Follow", {:ok, activity} =
"actor" => follower.ap_id, ActivityPub.insert(%{
"object" => followed.ap_id, "type" => "Follow",
"to" => [followed.ap_id] "actor" => follower.ap_id,
}) "object" => followed.ap_id,
"to" => [followed.ap_id]
})
tuple = ActivityRepresenter.to_simple_form(activity, follower) tuple = ActivityRepresenter.to_simple_form(activity, follower)
refute is_nil(tuple) refute is_nil(tuple)
res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary()
expected = """ expected = """
<activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type> <activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
@ -193,7 +208,9 @@ test "a follow activity" do
<uri>#{activity.data["object"]}</uri> <uri>#{activity.data["object"]}</uri>
</activity:object> </activity:object>
<link rel="self" type="application/atom+xml" href="#{activity.data["id"]}"/> <link rel="self" type="application/atom+xml" href="#{activity.data["id"]}"/>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="#{activity.data["object"]}"/> <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="#{
activity.data["object"]
}"/>
""" """
assert clean(res) == clean(expected) assert clean(res) == clean(expected)
@ -209,7 +226,7 @@ test "an unfollow activity" do
refute is_nil(tuple) refute is_nil(tuple)
res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary()
expected = """ expected = """
<activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type> <activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
@ -225,7 +242,9 @@ test "an unfollow activity" do
<uri>#{followed.ap_id}</uri> <uri>#{followed.ap_id}</uri>
</activity:object> </activity:object>
<link rel="self" type="application/atom+xml" href="#{activity.data["id"]}"/> <link rel="self" type="application/atom+xml" href="#{activity.data["id"]}"/>
<link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="#{followed.ap_id}"/> <link rel="mentioned" ostatus:object-type="http://activitystrea.ms/schema/1.0/person" href="#{
followed.ap_id
}"/>
""" """
assert clean(res) == clean(expected) assert clean(res) == clean(expected)
@ -233,13 +252,22 @@ test "an unfollow activity" do
test "a delete" do test "a delete" do
user = insert(:user) user = insert(:user)
activity = %Activity{data: %{ "id" => "ap_id", "type" => "Delete", "actor" => user.ap_id, "object" => "some_id", "published" => "2017-06-18T12:00:18+00:00" }}
activity = %Activity{
data: %{
"id" => "ap_id",
"type" => "Delete",
"actor" => user.ap_id,
"object" => "some_id",
"published" => "2017-06-18T12:00:18+00:00"
}
}
tuple = ActivityRepresenter.to_simple_form(activity, nil) tuple = ActivityRepresenter.to_simple_form(activity, nil)
refute is_nil(tuple) refute is_nil(tuple)
res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> IO.iodata_to_binary()
expected = """ expected = """
<activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type> <activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>

View file

@ -11,15 +11,19 @@ test "returns a feed of the last 20 items of the user" do
tuple = FeedRepresenter.to_simple_form(user, [note_activity], [user]) tuple = FeedRepresenter.to_simple_form(user, [note_activity], [user])
most_recent_update = note_activity.updated_at most_recent_update =
|> NaiveDateTime.to_iso8601 note_activity.updated_at
|> NaiveDateTime.to_iso8601()
res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> to_string res = :xmerl.export_simple_content(tuple, :xmerl_xml) |> to_string
user_xml = UserRepresenter.to_simple_form(user)
|> :xmerl.export_simple_content(:xmerl_xml)
entry_xml = ActivityRepresenter.to_simple_form(note_activity, user) user_xml =
|> :xmerl.export_simple_content(:xmerl_xml) UserRepresenter.to_simple_form(user)
|> :xmerl.export_simple_content(:xmerl_xml)
entry_xml =
ActivityRepresenter.to_simple_form(note_activity, user)
|> :xmerl.export_simple_content(:xmerl_xml)
expected = """ expected = """
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:ostatus="http://ostatus.org/schema/1.0"> <feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:poco="http://portablecontacts.net/spec/1.0" xmlns:ostatus="http://ostatus.org/schema/1.0">
@ -39,6 +43,7 @@ test "returns a feed of the last 20 items of the user" do
</entry> </entry>
</feed> </feed>
""" """
assert clean(res) == clean(expected) assert clean(res) == clean(expected)
end end

View file

@ -14,8 +14,13 @@ test "it removes the mentioned activity" do
{:ok, like, _object} = Pleroma.Web.ActivityPub.ActivityPub.like(user, object) {:ok, like, _object} = Pleroma.Web.ActivityPub.ActivityPub.like(user, object)
incoming = File.read!("test/fixtures/delete.xml") incoming =
|> String.replace("tag:mastodon.sdf.org,2017-06-10:objectId=310513:objectType=Status", note.data["object"]["id"]) File.read!("test/fixtures/delete.xml")
|> String.replace(
"tag:mastodon.sdf.org,2017-06-10:objectId=310513:objectType=Status",
note.data["object"]["id"]
)
{:ok, [delete]} = OStatus.handle_incoming(incoming) {:ok, [delete]} = OStatus.handle_incoming(incoming)
refute Repo.get(Activity, note.id) refute Repo.get(Activity, note.id)

View file

@ -7,9 +7,11 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do
test "decodes a salmon", %{conn: conn} do test "decodes a salmon", %{conn: conn} do
user = insert(:user) user = insert(:user)
salmon = File.read!("test/fixtures/salmon.xml") salmon = File.read!("test/fixtures/salmon.xml")
conn = conn
|> put_req_header("content-type", "application/atom+xml") conn =
|> post("/users/#{user.nickname}/salmon", salmon) conn
|> put_req_header("content-type", "application/atom+xml")
|> post("/users/#{user.nickname}/salmon", salmon)
assert response(conn, 200) assert response(conn, 200)
end end
@ -17,21 +19,30 @@ test "decodes a salmon", %{conn: conn} do
test "decodes a salmon with a changed magic key", %{conn: conn} do test "decodes a salmon with a changed magic key", %{conn: conn} do
user = insert(:user) user = insert(:user)
salmon = File.read!("test/fixtures/salmon.xml") salmon = File.read!("test/fixtures/salmon.xml")
conn = conn
|> put_req_header("content-type", "application/atom+xml") conn =
|> post("/users/#{user.nickname}/salmon", salmon) conn
|> put_req_header("content-type", "application/atom+xml")
|> post("/users/#{user.nickname}/salmon", salmon)
assert response(conn, 200) assert response(conn, 200)
# Set a wrong magic-key for a user so it has to refetch # Set a wrong magic-key for a user so it has to refetch
salmon_user = User.get_by_ap_id("http://gs.example.org:4040/index.php/user/1") salmon_user = User.get_by_ap_id("http://gs.example.org:4040/index.php/user/1")
info = salmon_user.info # Wrong key
|> Map.put("magic_key", "RSA.pu0s-halox4tu7wmES1FVSx6u-4wc0YrUFXcqWXZG4-27UmbCOpMQftRCldNRfyA-qLbz-eqiwrong1EwUvjsD4cYbAHNGHwTvDOyx5AKthQUP44ykPv7kjKGh3DWKySJvcs9tlUG87hlo7AvnMo9pwRS_Zz2CacQ-MKaXyDepk=.AQAB") # Wrong key info =
salmon_user.info
|> Map.put(
"magic_key",
"RSA.pu0s-halox4tu7wmES1FVSx6u-4wc0YrUFXcqWXZG4-27UmbCOpMQftRCldNRfyA-qLbz-eqiwrong1EwUvjsD4cYbAHNGHwTvDOyx5AKthQUP44ykPv7kjKGh3DWKySJvcs9tlUG87hlo7AvnMo9pwRS_Zz2CacQ-MKaXyDepk=.AQAB"
)
Repo.update(User.info_changeset(salmon_user, %{info: info})) Repo.update(User.info_changeset(salmon_user, %{info: info}))
conn = build_conn() conn =
|> put_req_header("content-type", "application/atom+xml") build_conn()
|> post("/users/#{user.nickname}/salmon", salmon) |> put_req_header("content-type", "application/atom+xml")
|> post("/users/#{user.nickname}/salmon", salmon)
assert response(conn, 200) assert response(conn, 200)
end end
@ -40,8 +51,9 @@ test "gets a feed", %{conn: conn} do
note_activity = insert(:note_activity) note_activity = insert(:note_activity)
user = User.get_cached_by_ap_id(note_activity.data["actor"]) user = User.get_cached_by_ap_id(note_activity.data["actor"])
conn = conn conn =
|> get("/users/#{user.nickname}/feed.atom") conn
|> get("/users/#{user.nickname}/feed.atom")
assert response(conn, 200) =~ note_activity.data["object"]["content"] assert response(conn, 200) =~ note_activity.data["object"]["content"]
end end
@ -49,27 +61,30 @@ test "gets a feed", %{conn: conn} do
test "gets an object", %{conn: conn} do test "gets an object", %{conn: conn} do
note_activity = insert(:note_activity) note_activity = insert(:note_activity)
user = User.get_by_ap_id(note_activity.data["actor"]) user = User.get_by_ap_id(note_activity.data["actor"])
[_, uuid] = hd Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["object"]["id"]) [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["object"]["id"]))
url = "/objects/#{uuid}" url = "/objects/#{uuid}"
conn = conn conn =
|> get(url) conn
|> get(url)
expected = ActivityRepresenter.to_simple_form(note_activity, user, true) expected =
|> ActivityRepresenter.wrap_with_entry ActivityRepresenter.to_simple_form(note_activity, user, true)
|> :xmerl.export_simple(:xmerl_xml) |> ActivityRepresenter.wrap_with_entry()
|> to_string |> :xmerl.export_simple(:xmerl_xml)
|> to_string
assert response(conn, 200) == expected assert response(conn, 200) == expected
end end
test "gets an activity", %{conn: conn} do test "gets an activity", %{conn: conn} do
note_activity = insert(:note_activity) note_activity = insert(:note_activity)
[_, uuid] = hd Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"]) [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"]))
url = "/activities/#{uuid}" url = "/activities/#{uuid}"
conn = conn conn =
|> get(url) conn
|> get(url)
assert response(conn, 200) assert response(conn, 200)
end end
@ -78,8 +93,9 @@ test "gets a notice", %{conn: conn} do
note_activity = insert(:note_activity) note_activity = insert(:note_activity)
url = "/notice/#{note_activity.id}" url = "/notice/#{note_activity.id}"
conn = conn conn =
|> get(url) conn
|> get(url)
assert response(conn, 200) assert response(conn, 200)
end end

View file

@ -20,12 +20,18 @@ test "handle incoming note - GS, Salmon" do
assert user.info["note_count"] == 1 assert user.info["note_count"] == 1
assert activity.data["type"] == "Create" assert activity.data["type"] == "Create"
assert activity.data["object"]["type"] == "Note" assert activity.data["object"]["type"] == "Note"
assert activity.data["object"]["id"] == "tag:gs.example.org:4040,2017-04-23:noticeId=29:objectType=note"
assert activity.data["object"]["id"] ==
"tag:gs.example.org:4040,2017-04-23:noticeId=29:objectType=note"
assert activity.data["published"] == "2017-04-23T14:51:03+00:00" assert activity.data["published"] == "2017-04-23T14:51:03+00:00"
assert activity.data["object"]["published"] == "2017-04-23T14:51:03+00:00" assert activity.data["object"]["published"] == "2017-04-23T14:51:03+00:00"
assert activity.data["context"] == "tag:gs.example.org:4040,2017-04-23:objectType=thread:nonce=f09e22f58abd5c7b"
assert activity.data["context"] ==
"tag:gs.example.org:4040,2017-04-23:objectType=thread:nonce=f09e22f58abd5c7b"
assert "http://pleroma.example.org:4000/users/lain3" in activity.data["to"] assert "http://pleroma.example.org:4000/users/lain3" in activity.data["to"]
assert activity.data["object"]["emoji"] == %{ "marko" => "marko.png", "reimu" => "reimu.png" } assert activity.data["object"]["emoji"] == %{"marko" => "marko.png", "reimu" => "reimu.png"}
assert activity.local == false assert activity.local == false
end end
@ -65,10 +71,12 @@ test "handle incoming notes with tags" do
test "handle incoming notes - Mastodon, salmon, reply" do test "handle incoming notes - Mastodon, salmon, reply" do
# It uses the context of the replied to object # It uses the context of the replied to object
Repo.insert!(%Object{ Repo.insert!(%Object{
data: %{ data: %{
"id" => "https://pleroma.soykaf.com/objects/c237d966-ac75-4fe3-a87a-d89d71a3a7a4", "id" => "https://pleroma.soykaf.com/objects/c237d966-ac75-4fe3-a87a-d89d71a3a7a4",
"context" => "2hu" "context" => "2hu"
}}) }
})
incoming = File.read!("test/fixtures/incoming_reply_mastodon.xml") incoming = File.read!("test/fixtures/incoming_reply_mastodon.xml")
{:ok, [activity]} = OStatus.handle_incoming(incoming) {:ok, [activity]} = OStatus.handle_incoming(incoming)
@ -113,8 +121,13 @@ test "handle incoming notes - GS, subscription, reply" do
assert activity.data["type"] == "Create" assert activity.data["type"] == "Create"
assert activity.data["object"]["type"] == "Note" assert activity.data["object"]["type"] == "Note"
assert activity.data["object"]["actor"] == "https://social.heldscal.la/user/23211" assert activity.data["object"]["actor"] == "https://social.heldscal.la/user/23211"
assert activity.data["object"]["content"] == "@<a href=\"https://gs.archae.me/user/4687\" class=\"h-card u-url p-nickname mention\" title=\"shpbot\">shpbot</a> why not indeed."
assert activity.data["object"]["inReplyTo"] == "tag:gs.archae.me,2017-04-30:noticeId=778260:objectType=note" assert activity.data["object"]["content"] ==
"@<a href=\"https://gs.archae.me/user/4687\" class=\"h-card u-url p-nickname mention\" title=\"shpbot\">shpbot</a> why not indeed."
assert activity.data["object"]["inReplyTo"] ==
"tag:gs.archae.me,2017-04-30:noticeId=778260:objectType=note"
assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"] assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"]
end end
@ -141,9 +154,11 @@ test "handle incoming retweets - GS, subscription - local message" do
incoming = File.read!("test/fixtures/share-gs-local.xml") incoming = File.read!("test/fixtures/share-gs-local.xml")
note_activity = insert(:note_activity) note_activity = insert(:note_activity)
user = User.get_cached_by_ap_id(note_activity.data["actor"]) user = User.get_cached_by_ap_id(note_activity.data["actor"])
incoming = incoming
|> String.replace("LOCAL_ID", note_activity.data["object"]["id"]) incoming =
|> String.replace("LOCAL_USER", user.ap_id) incoming
|> String.replace("LOCAL_ID", note_activity.data["object"]["id"])
|> String.replace("LOCAL_USER", user.ap_id)
{:ok, [[activity, retweeted_activity]]} = OStatus.handle_incoming(incoming) {:ok, [[activity, retweeted_activity]]} = OStatus.handle_incoming(incoming)
@ -168,7 +183,9 @@ test "handle incoming retweets - Mastodon, salmon" do
assert activity.data["type"] == "Announce" assert activity.data["type"] == "Announce"
assert activity.data["actor"] == "https://mastodon.social/users/lambadalambda" assert activity.data["actor"] == "https://mastodon.social/users/lambadalambda"
assert activity.data["object"] == retweeted_activity.data["object"]["id"] assert activity.data["object"] == retweeted_activity.data["object"]["id"]
assert activity.data["id"] == "tag:mastodon.social,2017-05-03:objectId=4934452:objectType=Status"
assert activity.data["id"] ==
"tag:mastodon.social,2017-05-03:objectId=4934452:objectType=Status"
refute activity.local refute activity.local
assert retweeted_activity.data["type"] == "Create" assert retweeted_activity.data["type"] == "Create"
@ -178,35 +195,42 @@ test "handle incoming retweets - Mastodon, salmon" do
end end
test "handle incoming favorites - GS, websub" do test "handle incoming favorites - GS, websub" do
capture_log fn -> capture_log(fn ->
incoming = File.read!("test/fixtures/favorite.xml") incoming = File.read!("test/fixtures/favorite.xml")
{:ok, [[activity, favorited_activity]]} = OStatus.handle_incoming(incoming) {:ok, [[activity, favorited_activity]]} = OStatus.handle_incoming(incoming)
assert activity.data["type"] == "Like" assert activity.data["type"] == "Like"
assert activity.data["actor"] == "https://social.heldscal.la/user/23211" assert activity.data["actor"] == "https://social.heldscal.la/user/23211"
assert activity.data["object"] == favorited_activity.data["object"]["id"] assert activity.data["object"] == favorited_activity.data["object"]["id"]
assert activity.data["id"] == "tag:social.heldscal.la,2017-05-05:fave:23211:comment:2061643:2017-05-05T09:12:50+00:00"
assert activity.data["id"] ==
"tag:social.heldscal.la,2017-05-05:fave:23211:comment:2061643:2017-05-05T09:12:50+00:00"
refute activity.local refute activity.local
assert favorited_activity.data["type"] == "Create" assert favorited_activity.data["type"] == "Create"
assert favorited_activity.data["actor"] == "https://shitposter.club/user/1" assert favorited_activity.data["actor"] == "https://shitposter.club/user/1"
assert favorited_activity.data["object"]["id"] == "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
assert favorited_activity.data["object"]["id"] ==
"tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
refute favorited_activity.local refute favorited_activity.local
end end)
end end
test "handle conversation references" do test "handle conversation references" do
incoming = File.read!("test/fixtures/mastodon_conversation.xml") incoming = File.read!("test/fixtures/mastodon_conversation.xml")
{:ok, [activity]} = OStatus.handle_incoming(incoming) {:ok, [activity]} = OStatus.handle_incoming(incoming)
assert activity.data["context"] == "tag:mastodon.social,2017-08-28:objectId=7876885:objectType=Conversation" assert activity.data["context"] ==
"tag:mastodon.social,2017-08-28:objectId=7876885:objectType=Conversation"
end end
test "handle incoming favorites with locally available object - GS, websub" do test "handle incoming favorites with locally available object - GS, websub" do
note_activity = insert(:note_activity) note_activity = insert(:note_activity)
incoming = File.read!("test/fixtures/favorite_with_local_note.xml") incoming =
|> String.replace("localid", note_activity.data["object"]["id"]) File.read!("test/fixtures/favorite_with_local_note.xml")
|> String.replace("localid", note_activity.data["object"]["id"])
{:ok, [[activity, favorited_activity]]} = OStatus.handle_incoming(incoming) {:ok, [[activity, favorited_activity]]} = OStatus.handle_incoming(incoming)
@ -224,9 +248,15 @@ test "handle incoming replies" do
assert activity.data["type"] == "Create" assert activity.data["type"] == "Create"
assert activity.data["object"]["type"] == "Note" assert activity.data["object"]["type"] == "Note"
assert activity.data["object"]["inReplyTo"] == "http://pleroma.example.org:4000/objects/55bce8fc-b423-46b1-af71-3759ab4670bc"
assert activity.data["object"]["inReplyTo"] ==
"http://pleroma.example.org:4000/objects/55bce8fc-b423-46b1-af71-3759ab4670bc"
assert "http://pleroma.example.org:4000/users/lain5" in activity.data["to"] assert "http://pleroma.example.org:4000/users/lain5" in activity.data["to"]
assert activity.data["object"]["id"] == "tag:gs.example.org:4040,2017-04-25:noticeId=55:objectType=note"
assert activity.data["object"]["id"] ==
"tag:gs.example.org:4040,2017-04-25:noticeId=55:objectType=note"
assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"] assert "https://www.w3.org/ns/activitystreams#Public" in activity.data["to"]
end end
@ -234,7 +264,10 @@ test "handle incoming follows" do
incoming = File.read!("test/fixtures/follow.xml") incoming = File.read!("test/fixtures/follow.xml")
{:ok, [activity]} = OStatus.handle_incoming(incoming) {:ok, [activity]} = OStatus.handle_incoming(incoming)
assert activity.data["type"] == "Follow" assert activity.data["type"] == "Follow"
assert activity.data["id"] == "tag:social.heldscal.la,2017-05-07:subscription:23211:person:44803:2017-05-07T09:54:48+00:00"
assert activity.data["id"] ==
"tag:social.heldscal.la,2017-05-07:subscription:23211:person:44803:2017-05-07T09:54:48+00:00"
assert activity.data["actor"] == "https://social.heldscal.la/user/23211" assert activity.data["actor"] == "https://social.heldscal.la/user/23211"
assert activity.data["object"] == "https://pawoo.net/users/pekorino" assert activity.data["object"] == "https://pawoo.net/users/pekorino"
refute activity.local refute activity.local
@ -304,7 +337,8 @@ test "it returns user info in a hash" do
expected = %{ expected = %{
"hub" => "https://social.heldscal.la/main/push/hub", "hub" => "https://social.heldscal.la/main/push/hub",
"magic_key" => "RSA.wQ3i9UA0qmAxZ0WTIp4a-waZn_17Ez1pEEmqmqoooRsG1_BvpmOvLN0G2tEcWWxl2KOtdQMCiPptmQObeZeuj48mdsDZ4ArQinexY2hCCTcbV8Xpswpkb8K05RcKipdg07pnI7tAgQ0VWSZDImncL6YUGlG5YN8b5TjGOwk2VG8=.AQAB", "magic_key" =>
"RSA.wQ3i9UA0qmAxZ0WTIp4a-waZn_17Ez1pEEmqmqoooRsG1_BvpmOvLN0G2tEcWWxl2KOtdQMCiPptmQObeZeuj48mdsDZ4ArQinexY2hCCTcbV8Xpswpkb8K05RcKipdg07pnI7tAgQ0VWSZDImncL6YUGlG5YN8b5TjGOwk2VG8=.AQAB",
"name" => "shp", "name" => "shp",
"nickname" => "shp", "nickname" => "shp",
"salmon" => "https://social.heldscal.la/main/salmon/user/29191", "salmon" => "https://social.heldscal.la/main/salmon/user/29191",
@ -314,10 +348,20 @@ test "it returns user info in a hash" do
"host" => "social.heldscal.la", "host" => "social.heldscal.la",
"fqn" => user, "fqn" => user,
"bio" => "cofe", "bio" => "cofe",
"avatar" => %{"type" => "Image", "url" => [%{"href" => "https://social.heldscal.la/avatar/29191-original-20170421154949.jpeg", "mediaType" => "image/jpeg", "type" => "Link"}]}, "avatar" => %{
"type" => "Image",
"url" => [
%{
"href" => "https://social.heldscal.la/avatar/29191-original-20170421154949.jpeg",
"mediaType" => "image/jpeg",
"type" => "Link"
}
]
},
"subscribe_address" => "https://social.heldscal.la/main/ostatussub?profile={uri}", "subscribe_address" => "https://social.heldscal.la/main/ostatussub?profile={uri}",
"ap_id" => nil "ap_id" => nil
} }
assert data == expected assert data == expected
end end
@ -329,7 +373,8 @@ test "it works with the uri" do
expected = %{ expected = %{
"hub" => "https://social.heldscal.la/main/push/hub", "hub" => "https://social.heldscal.la/main/push/hub",
"magic_key" => "RSA.wQ3i9UA0qmAxZ0WTIp4a-waZn_17Ez1pEEmqmqoooRsG1_BvpmOvLN0G2tEcWWxl2KOtdQMCiPptmQObeZeuj48mdsDZ4ArQinexY2hCCTcbV8Xpswpkb8K05RcKipdg07pnI7tAgQ0VWSZDImncL6YUGlG5YN8b5TjGOwk2VG8=.AQAB", "magic_key" =>
"RSA.wQ3i9UA0qmAxZ0WTIp4a-waZn_17Ez1pEEmqmqoooRsG1_BvpmOvLN0G2tEcWWxl2KOtdQMCiPptmQObeZeuj48mdsDZ4ArQinexY2hCCTcbV8Xpswpkb8K05RcKipdg07pnI7tAgQ0VWSZDImncL6YUGlG5YN8b5TjGOwk2VG8=.AQAB",
"name" => "shp", "name" => "shp",
"nickname" => "shp", "nickname" => "shp",
"salmon" => "https://social.heldscal.la/main/salmon/user/29191", "salmon" => "https://social.heldscal.la/main/salmon/user/29191",
@ -339,28 +384,40 @@ test "it works with the uri" do
"host" => "social.heldscal.la", "host" => "social.heldscal.la",
"fqn" => user, "fqn" => user,
"bio" => "cofe", "bio" => "cofe",
"avatar" => %{"type" => "Image", "url" => [%{"href" => "https://social.heldscal.la/avatar/29191-original-20170421154949.jpeg", "mediaType" => "image/jpeg", "type" => "Link"}]}, "avatar" => %{
"type" => "Image",
"url" => [
%{
"href" => "https://social.heldscal.la/avatar/29191-original-20170421154949.jpeg",
"mediaType" => "image/jpeg",
"type" => "Link"
}
]
},
"subscribe_address" => "https://social.heldscal.la/main/ostatussub?profile={uri}", "subscribe_address" => "https://social.heldscal.la/main/ostatussub?profile={uri}",
"ap_id" => nil "ap_id" => nil
} }
assert data == expected assert data == expected
end end
end end
describe "fetching a status by it's HTML url" do describe "fetching a status by it's HTML url" do
test "it builds a missing status from an html url" do test "it builds a missing status from an html url" do
capture_log fn -> capture_log(fn ->
url = "https://shitposter.club/notice/2827873" url = "https://shitposter.club/notice/2827873"
{:ok, [activity] } = OStatus.fetch_activity_from_url(url) {:ok, [activity]} = OStatus.fetch_activity_from_url(url)
assert activity.data["actor"] == "https://shitposter.club/user/1" assert activity.data["actor"] == "https://shitposter.club/user/1"
assert activity.data["object"]["id"] == "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
end assert activity.data["object"]["id"] ==
"tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
end)
end end
test "it works for atom notes, too" do test "it works for atom notes, too" do
url = "https://social.sakamoto.gq/objects/0ccc1a2c-66b0-4305-b23a-7f7f2b040056" url = "https://social.sakamoto.gq/objects/0ccc1a2c-66b0-4305-b23a-7f7f2b040056"
{:ok, [activity] } = OStatus.fetch_activity_from_url(url) {:ok, [activity]} = OStatus.fetch_activity_from_url(url)
assert activity.data["actor"] == "https://social.sakamoto.gq/users/eal" assert activity.data["actor"] == "https://social.sakamoto.gq/users/eal"
assert activity.data["object"]["id"] == url assert activity.data["object"]["id"] == url
end end
@ -370,6 +427,9 @@ test "it doesn't add nil in the do field" do
incoming = File.read!("test/fixtures/nil_mention_entry.xml") incoming = File.read!("test/fixtures/nil_mention_entry.xml")
{:ok, [activity]} = OStatus.handle_incoming(incoming) {:ok, [activity]} = OStatus.handle_incoming(incoming)
assert activity.data["to"] == ["http://localhost:4001/users/atarifrosch@social.stopwatchingus-heidelberg.de/followers", "https://www.w3.org/ns/activitystreams#Public"] assert activity.data["to"] == [
"http://localhost:4001/users/atarifrosch@social.stopwatchingus-heidelberg.de/followers",
"https://www.w3.org/ns/activitystreams#Public"
]
end end
end end

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