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

View file

@ -9,8 +9,7 @@
# Print only warnings and errors during test
config :logger, level: :warn
config :pleroma, Pleroma.Upload,
uploads: "test/uploads"
config :pleroma, Pleroma.Upload, uploads: "test/uploads"
# Configure your database
config :pleroma, Pleroma.Repo,
@ -21,7 +20,6 @@
hostname: System.get_env("DB_HOST") || "localhost",
pool: Ecto.Adapters.SQL.Sandbox
# Reduce hash rounds for testing
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
Mix.Task.run("app.start")
q = from u in User,
where: fragment("? @> ?", u.info, ^%{"ap_enabled" => true}),
where: u.local == false
q =
from(
u in User,
where: fragment("? @> ?", u.info, ^%{"ap_enabled" => true}),
where: u.local == false
)
users = Repo.all(q)
Enum.each(users, fn(user) ->
Enum.each(users, fn user ->
try do
IO.puts("Fetching #{user.nickname}")
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
IO.puts("Answer a few questions to generate a new config\n")
IO.puts("--- THIS WILL OVERWRITE YOUR config/generated_config.exs! ---\n")
domain = IO.gets("What is your domain name? (e.g. pleroma.soykaf.com): ") |> String.trim
name = IO.gets("What is the name of your instance? (e.g. Pleroma/Soykaf): ") |> String.trim
email = IO.gets("What's your admin email address: ") |> String.trim
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)
domain = IO.gets("What is your domain name? (e.g. pleroma.soykaf.com): ") |> String.trim()
name = IO.gets("What is the name of your instance? (e.g. Pleroma/Soykaf): ") |> String.trim()
email = IO.gets("What's your admin email address: ") |> String.trim()
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])
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)
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)
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)
end
end

View file

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

View file

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

View file

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

View file

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

View file

@ -7,23 +7,34 @@ def start(_type, _args) do
import Supervisor.Spec
# Define workers and child supervisors to be supervised
children = [
# Start the Ecto repository
supervisor(Pleroma.Repo, []),
# Start the endpoint when the application starts
supervisor(Pleroma.Web.Endpoint, []),
# Start your own worker by calling: Pleroma.Worker.start_link(arg1, arg2, arg3)
# worker(Pleroma.Worker, [arg1, arg2, arg3]),
worker(Cachex, [:user_cache, [
default_ttl: 25000,
ttl_interval: 1000,
limit: 2500
]]),
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, [])]
children =
[
# Start the Ecto repository
supervisor(Pleroma.Repo, []),
# Start the endpoint when the application starts
supervisor(Pleroma.Web.Endpoint, []),
# Start your own worker by calling: Pleroma.Worker.start_link(arg1, arg2, arg3)
# worker(Pleroma.Worker, [arg1, arg2, arg3]),
worker(Cachex, [
:user_cache,
[
default_ttl: 25000,
ttl_interval: 1000,
limit: 2500
]
]),
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
# for other strategies and supported options

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,20 +8,20 @@ defmodule Pleroma.User do
alias Pleroma.Web.ActivityPub.{Utils, ActivityPub}
schema "users" do
field :bio, :string
field :email, :string
field :name, :string
field :nickname, :string
field :password_hash, :string
field :password, :string, virtual: true
field :password_confirmation, :string, virtual: true
field :following, {:array, :string}, default: []
field :ap_id, :string
field :avatar, :map
field :local, :boolean, default: true
field :info, :map, default: %{}
field :follower_address, :string
has_many :notifications, Notification
field(:bio, :string)
field(:email, :string)
field(:name, :string)
field(:nickname, :string)
field(:password_hash, :string)
field(:password, :string, virtual: true)
field(:password_confirmation, :string, virtual: true)
field(:following, {:array, :string}, default: [])
field(:ap_id, :string)
field(:avatar, :map)
field(:local, :boolean, default: true)
field(:info, :map, default: %{})
field(:follower_address, :string)
has_many(:notifications, Notification)
timestamps()
end
@ -41,7 +41,7 @@ def banner_url(user) do
end
def ap_id(%User{nickname: nickname}) do
"#{Web.base_url}/users/#{nickname}"
"#{Web.base_url()}/users/#{nickname}"
end
def ap_followers(%User{} = user) do
@ -62,6 +62,7 @@ def info_changeset(struct, params \\ %{}) do
def user_info(%User{} = user) do
oneself = if user.local, do: 1, else: 0
%{
following_count: length(user.following) - oneself,
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])?)*$/
def remote_user_creation(params) do
changes = %User{}
|> cast(params, [:bio, :name, :ap_id, :nickname, :info, :avatar])
|> validate_required([:name, :ap_id, :nickname])
|> unique_constraint(:nickname)
|> validate_format(:nickname, @email_regex)
|> validate_length(:bio, max: 5000)
|> validate_length(:name, max: 100)
|> put_change(:local, false)
changes =
%User{}
|> cast(params, [:bio, :name, :ap_id, :nickname, :info, :avatar])
|> validate_required([:name, :ap_id, :nickname])
|> unique_constraint(:nickname)
|> validate_format(:nickname, @email_regex)
|> validate_length(:bio, max: 5000)
|> validate_length(:name, max: 100)
|> put_change(:local, false)
if changes.valid? do
case changes.changes[:info]["source_data"] do
%{"followers" => followers} ->
changes
|> put_change(:follower_address, followers)
_ ->
followers = User.ap_followers(%User{nickname: changes.changes[:nickname]})
changes
|> put_change(:follower_address, followers)
end
@ -113,13 +118,15 @@ def upgrade_changeset(struct, params \\ %{}) do
end
def password_update_changeset(struct, params) do
changeset = struct
|> cast(params, [:password, :password_confirmation])
|> validate_required([:password, :password_confirmation])
|> validate_confirmation(:password)
changeset =
struct
|> cast(params, [:password, :password_confirmation])
|> validate_required([:password, :password_confirmation])
|> validate_confirmation(:password)
if changeset.valid? do
hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
changeset
|> put_change(:password_hash, hashed)
else
@ -132,21 +139,23 @@ def reset_password(user, data) do
end
def register_changeset(struct, params \\ %{}) do
changeset = struct
|> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
|> validate_required([:email, :name, :nickname, :password, :password_confirmation])
|> validate_confirmation(:password)
|> unique_constraint(:email)
|> unique_constraint(:nickname)
|> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/)
|> validate_format(:email, @email_regex)
|> validate_length(:bio, max: 1000)
|> validate_length(:name, min: 1, max: 100)
changeset =
struct
|> cast(params, [:bio, :email, :name, :nickname, :password, :password_confirmation])
|> validate_required([:email, :name, :nickname, :password, :password_confirmation])
|> validate_confirmation(:password)
|> unique_constraint(:email)
|> unique_constraint(:nickname)
|> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/)
|> validate_format(:email, @email_regex)
|> validate_length(:bio, max: 1000)
|> validate_length(:name, min: 1, max: 100)
if changeset.valid? do
hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
ap_id = User.ap_id(%User{nickname: changeset.changes[:nickname]})
followers = User.ap_followers(%User{nickname: changeset.changes[:nickname]})
changeset
|> put_change(:password_hash, hashed)
|> put_change(:ap_id, ap_id)
@ -161,19 +170,20 @@ def follow(%User{} = follower, %User{info: info} = followed) do
ap_followers = followed.follower_address
if following?(follower, followed) or info["deactivated"] do
{:error,
"Could not follow user: #{followed.nickname} is already on your list."}
{:error, "Could not follow user: #{followed.nickname} is already on your list."}
else
if !followed.local && follower.local && !ap_enabled?(followed) do
Websub.subscribe(follower, followed)
end
following = [ap_followers | follower.following]
|> Enum.uniq
following =
[ap_followers | follower.following]
|> Enum.uniq()
follower = follower
|> follow_changeset(%{following: following})
|> update_and_set_cache
follower =
follower
|> follow_changeset(%{following: following})
|> update_and_set_cache
{: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
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
|> follow_changeset(%{following: following})
|> update_and_set_cache
if following?(follower, followed) and follower.ap_id != followed.ap_id do
following =
follower.following
|> List.delete(ap_followers)
{:ok, follower} =
follower
|> follow_changeset(%{following: following})
|> update_and_set_cache
{:ok, followed} = update_follower_count(followed)
@ -225,12 +238,12 @@ def invalidate_cache(user) do
def get_cached_by_ap_id(ap_id) do
key = "ap_id:#{ap_id}"
Cachex.get!(:user_cache, key, fallback: fn(_) -> get_by_ap_id(ap_id) end)
Cachex.get!(:user_cache, key, fallback: fn _ -> get_by_ap_id(ap_id) end)
end
def get_cached_by_nickname(nickname) do
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
def get_by_nickname(nickname) do
@ -239,7 +252,7 @@ def get_by_nickname(nickname) do
def get_cached_user_info(user) do
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
def fetch_by_nickname(nickname) do
@ -252,29 +265,37 @@ def fetch_by_nickname(nickname) do
end
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
else _e ->
with [_nick, _domain] <- String.split(nickname, "@"),
{:ok, user} <- fetch_by_nickname(nickname) do
user
else _e -> nil
end
else
_e ->
with [_nick, _domain] <- String.split(nickname, "@"),
{:ok, user} <- fetch_by_nickname(nickname) do
user
else
_e -> nil
end
end
end
def get_followers(%User{id: id, follower_address: follower_address}) do
q = from u in User,
where: fragment("? <@ ?", ^[follower_address], u.following),
where: u.id != ^id
q =
from(
u in User,
where: fragment("? <@ ?", ^[follower_address], u.following),
where: u.id != ^id
)
{:ok, Repo.all(q)}
end
def get_friends(%User{id: id, following: following}) do
q = from u in User,
where: u.follower_address in ^following,
where: u.id != ^id
q =
from(
u in User,
where: u.follower_address in ^following,
where: u.id != ^id
)
{:ok, Repo.all(q)}
end
@ -289,9 +310,12 @@ def increase_note_count(%User{} = user) do
end
def update_note_count(%User{} = user) do
note_count_query = from a in Object,
where: fragment("?->>'actor' = ? and ?->>'type' = 'Note'", a.data, ^user.ap_id, a.data),
select: count(a.id)
note_count_query =
from(
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)
@ -303,10 +327,13 @@ def update_note_count(%User{} = user) do
end
def update_follower_count(%User{} = user) do
follower_count_query = from u in User,
where: ^user.follower_address in u.following,
where: u.id != ^user.id,
select: count(u.id)
follower_count_query =
from(
u in User,
where: ^user.follower_address in u.following,
where: u.id != ^user.id,
select: count(u.id)
)
follower_count = Repo.one(follower_count_query)
@ -318,20 +345,25 @@ def update_follower_count(%User{} = user) do
end
def get_notified_from_activity(%Activity{recipients: to}) do
query = from u in User,
where: u.ap_id in ^to,
where: u.local == true
query =
from(
u in User,
where: u.ap_id in ^to,
where: u.local == true
)
Repo.all(query)
end
def get_recipients_from_activity(%Activity{recipients: to}) do
query = from u in User,
where: u.ap_id in ^to,
or_where: fragment("? && ?", u.following, ^to)
query =
from(
u in User,
where: u.ap_id in ^to,
or_where: fragment("? && ?", u.following, ^to)
)
query = from u in query,
where: u.local == true
query = from(u in query, where: u.local == true)
Repo.all(query)
end
@ -340,9 +372,20 @@ def search(query, resolve) do
if resolve do
User.get_or_fetch_by_nickname(query)
end
q = from u in User,
where: fragment("(to_tsvector('english', ?) || to_tsvector('english', ?)) @@ plainto_tsquery('english', ?)", u.nickname, u.name, ^query),
limit: 20
q =
from(
u in User,
where:
fragment(
"(to_tsvector('english', ?) || to_tsvector('english', ?)) @@ plainto_tsquery('english', ?)",
u.nickname,
u.name,
^query
),
limit: 20
)
Repo.all(q)
end
@ -370,36 +413,40 @@ def blocks?(user, %{ap_id: ap_id}) do
end
def local_user_query() do
from u in User,
where: u.local == true
from(u in User, where: u.local == true)
end
def deactivate (%User{} = user) do
def deactivate(%User{} = user) do
new_info = Map.put(user.info, "deactivated", true)
cs = User.info_changeset(user, %{info: new_info})
update_and_set_cache(cs)
end
def delete (%User{} = user) do
def delete(%User{} = user) do
{:ok, user} = User.deactivate(user)
# Remove all relationships
{:ok, followers } = User.get_followers(user)
{:ok, followers} = User.get_followers(user)
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)
friends
|> Enum.each(fn (followed) -> User.unfollow(user, followed) end)
query = from a in Activity,
where: a.actor == ^user.ap_id
friends
|> Enum.each(fn followed -> User.unfollow(user, followed) end)
query = from(a in Activity, where: a.actor == ^user.ap_id)
Repo.all(query)
|> Enum.each(fn (activity) ->
|> Enum.each(fn activity ->
case activity.data["type"] do
"Create" -> ActivityPub.delete(Object.get_by_ap_id(activity.data["object"]["id"]))
_ -> "Doing nothing" # TODO: Do something with likes, follows, repeats.
"Create" ->
ActivityPub.delete(Object.get_by_ap_id(activity.data["object"]["id"]))
# TODO: Do something with likes, follows, repeats.
_ ->
"Doing nothing"
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)
case ap_try do
{:ok, user} -> user
{:ok, user} ->
user
_ ->
case OStatus.make_user(ap_id) do
{:ok, user} -> user
@ -424,12 +473,15 @@ def get_or_fetch_by_ap_id(ap_id) do
end
# AP style
def public_key_from_info(%{"source_data" => %{"publicKey" => %{"publicKeyPem" => public_key_pem}}}) do
key = :public_key.pem_decode(public_key_pem)
|> hd()
|> :public_key.pem_entry_decode()
def public_key_from_info(%{
"source_data" => %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
}) do
key =
:public_key.pem_decode(public_key_pem)
|> hd()
|> :public_key.pem_entry_decode()
{:ok, key}
{:ok, key}
end
# OStatus Magic Key
@ -450,8 +502,10 @@ defp blank?(""), do: nil
defp blank?(n), do: n
def insert_or_update_user(data) do
data = data
|> Map.put(:name, blank?(data[:name]) || data[:nickname])
data =
data
|> Map.put(:name, blank?(data[:name]) || data[:nickname])
cs = User.remote_user_creation(data)
Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
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"]),
map <- lazy_put_activity_defaults(map),
: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)
stream_out(activity)
{:ok, activity}
@ -31,8 +38,10 @@ def insert(map, local \\ true) when is_map(map) do
def stream_out(activity) do
if activity.data["type"] in ["Create", "Announce"] do
Pleroma.Web.Streamer.stream("user", activity)
if Enum.member?(activity.data["to"], "https://www.w3.org/ns/activitystreams#Public") do
Pleroma.Web.Streamer.stream("public", activity)
if activity.local do
Pleroma.Web.Streamer.stream("public:local", activity)
end
@ -42,10 +51,15 @@ def stream_out(activity) do
def create(%{to: to, actor: actor, context: context, object: object} = params) do
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]
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 <- maybe_federate(activity) do
{:ok, activity}
@ -53,7 +67,8 @@ def create(%{to: to, actor: actor, context: context, object: object} = params) d
end
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},
{:ok, activity} <- insert(data, local),
@ -63,9 +78,16 @@ def accept(%{to: to, actor: actor, object: object} = params) do
end
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 <- maybe_federate(activity) do
{:ok, activity}
@ -73,7 +95,12 @@ def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
end
# TODO: This is weird, maybe we shouldn't check here if we can make the activity.
def like(%User{ap_id: ap_id} = user, %Object{data: %{"id" => _}} = object, activity_id \\ nil, local \\ true) do
def like(
%User{ap_id: ap_id} = user,
%Object{data: %{"id" => _}} = object,
activity_id \\ nil,
local \\ true
) do
with nil <- get_existing_like(ap_id, object),
like_data <- make_like_data(user, object, activity_id),
{:ok, activity} <- insert(like_data, local),
@ -91,11 +118,17 @@ def unlike(%User{} = actor, %Object{} = object) do
{:ok, _activity} <- Repo.delete(activity),
{:ok, object} <- remove_like_from_object(activity, object) do
{:ok, object}
else _e -> {:ok, object}
else
_e -> {:ok, object}
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),
announce_data <- make_announce_data(user, object, activity_id),
{: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),
unfollow_data <- make_unfollow_data(follower, followed, follow_activity),
{:ok, activity} <- insert(unfollow_data, local),
:ok, maybe_federate(activity) do
:ok,
maybe_federate(activity) do
{:ok, activity}
end
end
def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ true) do
user = User.get_cached_by_ap_id(actor)
data = %{
"type" => "Delete",
"actor" => actor,
"object" => id,
"to" => [user.follower_address, "https://www.w3.org/ns/activitystreams#Public"]
}
with Repo.delete(object),
Repo.delete_all(Activity.all_non_create_by_object_ap_id_q(id)),
{: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
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
query = query
recipients =
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_recipients(recipients, opts["user"])
query = from activity in query,
where: fragment("?->>'type' = ? and ?->>'context' = ?", activity.data, "Create", activity.data, ^context),
order_by: [desc: :id]
query =
from(
activity in query,
where:
fragment(
"?->>'type' = ? and ?->>'context' = ?",
activity.data,
"Create",
activity.data,
^context
),
order_by: [desc: :id]
)
Repo.all(query)
end
# TODO: Make this work properly with unlisted.
def fetch_public_activities(opts \\ %{}) do
q = fetch_activities_query(["https://www.w3.org/ns/activitystreams#Public"], opts)
q
|> Repo.all
|> Enum.reverse
|> Repo.all()
|> Enum.reverse()
end
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
defp restrict_since(query, _), do: query
defp restrict_tag(query, %{"tag" => tag}) do
from activity in query,
from(
activity in query,
where: fragment("? <@ (? #> '{\"object\",\"tag\"}')", ^tag, activity.data)
)
end
defp restrict_tag(query, _), do: query
defp restrict_recipients(query, [], user), do: query
defp restrict_recipients(query, recipients, nil) do
from activity in query,
where: fragment("? && ?", ^recipients, activity.recipients)
from(activity in query, where: fragment("? && ?", ^recipients, activity.recipients))
end
defp restrict_recipients(query, recipients, user) do
from activity in query,
from(
activity in query,
where: fragment("? && ?", ^recipients, activity.recipients),
or_where: activity.actor == ^user.ap_id
)
end
defp restrict_limit(query, %{"limit" => limit}) do
from activity in query,
limit: ^limit
from(activity in query, limit: ^limit)
end
defp restrict_limit(query, _), do: query
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
defp restrict_local(query, _), do: query
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
defp restrict_max(query, _), do: query
defp restrict_actor(query, %{"actor_id" => actor_id}) do
from activity in query,
where: activity.actor == ^actor_id
from(activity in query, where: activity.actor == ^actor_id)
end
defp restrict_actor(query, _), do: query
defp restrict_type(query, %{"type" => type}) when is_binary(type) do
restrict_type(query, %{"type" => [type]})
end
defp restrict_type(query, %{"type" => type}) do
from activity in query,
where: fragment("?->>'type' = ANY(?)", activity.data, ^type)
from(activity in query, where: fragment("?->>'type' = ANY(?)", activity.data, ^type))
end
defp restrict_type(query, _), do: query
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)
)
end
defp restrict_favorited_by(query, _), do: query
defp restrict_media(query, %{"only_media" => val}) when val == "true" or val == "1" do
from activity in query,
from(
activity in query,
where: fragment("not (? #> '{\"object\",\"attachment\"}' = ?)", activity.data, ^[])
)
end
defp restrict_media(query, _), do: query
# Only search through last 100_000 activities by default
defp restrict_recent(query, %{"whole_db" => true}), do: query
defp restrict_recent(query, _) do
since = (Repo.aggregate(Activity, :max, :id) || 0) - 100_000
from activity in query,
where: activity.id > ^since
from(activity in query, where: activity.id > ^since)
end
defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do
blocks = info["blocks"] || []
from activity in query,
where: fragment("not (? = ANY(?))", activity.actor, ^blocks)
from(activity in query, where: fragment("not (? = ANY(?))", activity.actor, ^blocks))
end
defp restrict_blocked(query, _), do: query
def fetch_activities_query(recipients, opts \\ %{}) do
base_query = from activity in Activity,
limit: 20,
order_by: [fragment("? desc nulls last", activity.id)]
base_query =
from(
activity in Activity,
limit: 20,
order_by: [fragment("? desc nulls last", activity.id)]
)
base_query
|> restrict_recipients(recipients, opts["user"])
@ -266,8 +337,8 @@ def fetch_activities_query(recipients, opts \\ %{}) do
def fetch_activities(recipients, opts \\ %{}) do
fetch_activities_query(recipients, opts)
|> Repo.all
|> Enum.reverse
|> Repo.all()
|> Enum.reverse()
end
def upload(file) do
@ -276,15 +347,19 @@ def upload(file) do
end
def user_data_from_user_object(data) do
avatar = data["icon"]["url"] && %{
"type" => "Image",
"url" => [%{"href" => data["icon"]["url"]}]
}
avatar =
data["icon"]["url"] &&
%{
"type" => "Image",
"url" => [%{"href" => data["icon"]["url"]}]
}
banner = data["image"]["url"] && %{
"type" => "Image",
"url" => [%{"href" => data["image"]["url"]}]
}
banner =
data["image"]["url"] &&
%{
"type" => "Image",
"url" => [%{"href" => data["image"]["url"]}]
}
user_data = %{
ap_id: data["id"],
@ -304,8 +379,9 @@ def user_data_from_user_object(data) do
end
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"]),
{:ok, data} <- Jason.decode(body) do
with {:ok, %{status_code: 200, body: body}} <-
@httpoison.get(ap_id, Accept: "application/activity+json"),
{:ok, data} <- Jason.decode(body) do
user_data_from_user_object(data)
else
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
def publish(actor, activity) do
followers = if actor.follower_address in activity.recipients do
{:ok, followers} = User.get_followers(actor)
followers |> Enum.filter(&(!&1.local))
else
[]
end
followers =
if actor.follower_address in activity.recipients do
{:ok, followers} = User.get_followers(actor)
followers |> Enum.filter(&(!&1.local))
else
[]
end
remote_inboxes = (Pleroma.Web.Salmon.remote_users(activity) ++ followers)
|> Enum.filter(fn (user) -> User.ap_enabled?(user) end)
|> Enum.map(fn (%{info: %{"source_data" => data}}) ->
(data["endpoints"] && data["endpoints"]["sharedInbox"]) || data["inbox"]
end)
|> Enum.uniq
remote_inboxes =
(Pleroma.Web.Salmon.remote_users(activity) ++ followers)
|> Enum.filter(fn user -> User.ap_enabled?(user) end)
|> Enum.map(fn %{info: %{"source_data" => data}} ->
(data["endpoints"] && data["endpoints"]["sharedInbox"]) || data["inbox"]
end)
|> Enum.uniq()
{:ok, data} = Transmogrifier.prepare_outgoing(activity.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"]})
end
Enum.each(remote_inboxes, fn inbox ->
Federator.enqueue(:publish_single_ap, %{
inbox: inbox,
json: json,
actor: actor,
id: activity.data["id"]
})
end)
end
def publish_one(%{inbox: inbox, json: json, actor: actor, id: id}) do
Logger.info("Federating #{id} to #{inbox}")
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
# TODO:
@ -368,17 +460,34 @@ def fetch_object_from_id(id) do
{:ok, object}
else
Logger.info("Fetching #{id} via AP")
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),
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, Object.get_by_ap_id(activity.data["object"]["id"])}
else
object = %Object{} -> {:ok, object}
object = %Object{} ->
{:ok, object}
e ->
Logger.info("Couldn't get object via AP, trying out OStatus fetching...")
case OStatus.fetch_activity_from_url(id) do
{:ok, [activity | _]} -> {:ok, Object.get_by_ap_id(activity.data["object"]["id"])}
e -> e
@ -388,15 +497,17 @@ def fetch_object_from_id(id) do
end
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
def visible_for_user?(activity, nil) do
is_public?(activity)
end
def visible_for_user?(activity, user) do
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))
end
end

View file

@ -7,7 +7,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
require Logger
action_fallback :errors
action_fallback(:errors)
def user(conn, %{"nickname" => nickname}) do
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),
{:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do
{page, _} = Integer.parse(page)
conn
|> put_resp_header("content-type", "application/activity+json")
|> 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),
{:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do
{page, _} = Integer.parse(page)
conn
|> put_resp_header("content-type", "application/activity+json")
|> 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
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
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
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")
ActivityPub.fetch_object_from_id(params["object"]["id"])
else

View file

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

View file

@ -26,7 +26,7 @@ def make_json_ld_header do
end
def make_date do
DateTime.utc_now() |> DateTime.to_iso8601
DateTime.utc_now() |> DateTime.to_iso8601()
end
def generate_activity_id do
@ -38,25 +38,28 @@ def generate_context_id do
end
def generate_object_id do
Helpers.o_status_url(Endpoint, :object, UUID.generate)
Helpers.o_status_url(Endpoint, :object, UUID.generate())
end
def generate_id(type) do
"#{Web.base_url()}/#{type}/#{UUID.generate}"
"#{Web.base_url()}/#{type}/#{UUID.generate()}"
end
@doc """
Enqueues an activity for federation if it's local
"""
def maybe_federate(%Activity{local: true} = activity) do
priority = case activity.data["type"] do
"Delete" -> 10
"Create" -> 1
_ -> 5
end
priority =
case activity.data["type"] do
"Delete" -> 10
"Create" -> 1
_ -> 5
end
Pleroma.Web.Federator.enqueue(:publish, activity, priority)
:ok
end
def maybe_federate(_), do: :ok
@doc """
@ -64,9 +67,10 @@ def maybe_federate(_), do: :ok
also adds it to an included object
"""
def lazy_put_activity_defaults(map) do
map = map
|> Map.put_new_lazy("id", &generate_activity_id/0)
|> Map.put_new_lazy("published", &make_date/0)
map =
map
|> Map.put_new_lazy("id", &generate_activity_id/0)
|> Map.put_new_lazy("published", &make_date/0)
if is_map(map["object"]) do
object = lazy_put_object_defaults(map["object"])
@ -88,11 +92,13 @@ def lazy_put_object_defaults(map) do
@doc """
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
:ok
end
end
def insert_full_object(_), do: :ok
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
# could probably be taken from cache.
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)
changeset = Changeset.change(activity, data: new_activity_data)
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
"""
def get_existing_like(actor, %{data: %{"id" => id}}) do
query = from activity in Activity,
where: fragment("(?)->>'actor' = ?", activity.data, ^actor),
# this is to use the index
where: fragment("coalesce((?)->'object'->>'id', (?)->>'object') = ?", activity.data, activity.data, ^id),
where: fragment("(?)->>'type' = 'Like'", activity.data)
query =
from(
activity in Activity,
where: fragment("(?)->>'actor' = ?", activity.data, ^actor),
# this is to use the index
where:
fragment(
"coalesce((?)->'object'->>'id', (?)->>'object') = ?",
activity.data,
activity.data,
^id
),
where: fragment("(?)->>'type' = 'Like'", activity.data)
)
Repo.one(query)
end
@ -137,10 +153,12 @@ def make_like_data(%User{ap_id: ap_id} = actor, %{data: %{"id" => id}} = object,
end
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),
{:ok, object} <- Repo.update(changeset),
_ <- update_object_in_activities(object) do
_ <- update_object_in_activities(object) do
{:ok, object}
end
end
@ -150,7 +168,7 @@ def update_likes_in_object(likes, object) do
end
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)
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
end
def fetch_latest_follow(%User{ap_id: follower_id},
%User{ap_id: followed_id}) do
query = from activity in Activity,
where: fragment("? @> ?", activity.data, ^%{type: "Follow", actor: follower_id,
object: followed_id}),
order_by: [desc: :id],
limit: 1
def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
query =
from(
activity in Activity,
where:
fragment(
"? @> ?",
activity.data,
^%{type: "Follow", actor: follower_id, object: followed_id}
),
order_by: [desc: :id],
limit: 1
)
Repo.one(query)
end
@ -193,7 +218,11 @@ def fetch_latest_follow(%User{ap_id: follower_id},
@doc """
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 = %{
"type" => "Announce",
"actor" => ap_id,
@ -207,7 +236,7 @@ def make_announce_data(%User{ap_id: ap_id} = user, %Object{data: %{"id" => id}}
end
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)
end
end
@ -223,14 +252,14 @@ def make_unfollow_data(follower, followed, follow_activity) do
}
end
#### Create-related helpers
def make_create_data(params, additional) do
published = params.published || make_date()
%{
"type" => "Create",
"to" => params.to |> Enum.uniq,
"to" => params.to |> Enum.uniq(),
"actor" => params.actor.ap_id,
"object" => params.object,
"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"])
public_key = :public_key.pem_entry_encode(:RSAPublicKey, public_key)
public_key = :public_key.pem_encode([public_key])
%{
"id" => user.ap_id,
"type" => "Person",
@ -30,7 +31,7 @@ def render("user.json", %{user: user}) do
"publicKeyPem" => public_key
},
"endpoints" => %{
"sharedInbox" => "#{Pleroma.Web.Endpoint.url}/inbox"
"sharedInbox" => "#{Pleroma.Web.Endpoint.url()}/inbox"
},
"icon" => %{
"type" => "Image",
@ -47,7 +48,8 @@ def render("user.json", %{user: user}) do
def collection(collection, iri, page) do
offset = (page - 1) * 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 = %{
"id" => "#{iri}?page=#{page}",
"type" => "OrderedCollectionPage",
@ -55,19 +57,22 @@ def collection(collection, iri, page) do
"totalItems" => length(collection),
"orderedItems" => items
}
if offset < length(collection) do
Map.put(map, "next", "#{iri}?page=#{page+1}")
Map.put(map, "next", "#{iri}?page=#{page + 1}")
end
end
def render("following.json", %{user: user, page: page}) do
{:ok, following} = User.get_friends(user)
collection(following, "#{user.ap_id}/following", page)
|> Map.merge(Utils.make_json_ld_header())
end
def render("following.json", %{user: user}) do
{:ok, following} = User.get_friends(user)
%{
"id" => "#{user.ap_id}/following",
"type" => "OrderedCollection",
@ -79,12 +84,14 @@ def render("following.json", %{user: user}) do
def render("followers.json", %{user: user, page: page}) do
{:ok, followers} = User.get_followers(user)
collection(followers, "#{user.ap_id}/followers", page)
|> Map.merge(Utils.make_json_ld_header())
end
def render("followers.json", %{user: user}) do
{:ok, followers} = User.get_followers(user)
%{
"id" => "#{user.ap_id}/followers",
"type" => "OrderedCollection",
@ -115,19 +122,21 @@ def render("outbox.json", %{user: user, max_id: max_qid}) do
activities = Enum.reverse(activities)
max_id = Enum.at(activities, 0).id
collection = Enum.map(activities, fn (act) ->
{:ok, data} = Transmogrifier.prepare_outgoing(act.data)
data
end)
collection =
Enum.map(activities, fn act ->
{:ok, data} = Transmogrifier.prepare_outgoing(act.data)
data
end)
iri = "#{user.ap_id}/outbox"
page = %{
"id" => "#{iri}?max_id=#{max_id}",
"type" => "OrderedCollectionPage",
"partOf" => iri,
"totalItems" => info.note_count,
"orderedItems" => collection,
"next" => "#{iri}?max_id=#{min_id-1}",
"next" => "#{iri}?max_id=#{min_id - 1}"
}
if max_qid == nil do

View file

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

View file

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

View file

@ -8,7 +8,7 @@ defmodule Pleroma.Web.CommonAPI do
def delete(activity_id, user) do
with %Activity{data: %{"object" => %{"id" => object_id}}} <- Repo.get(Activity, activity_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}
end
@ -46,17 +46,22 @@ def unfavorite(id_or_ap_id, user) do
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
inReplyTo = get_replied_to_activity(status_id)
Pleroma.Web.MastodonAPI.StatusView.get_visibility(inReplyTo.data["object"])
end
def get_visibility(_), do: "public"
@instance Application.get_env(:pleroma, :instance)
@limit Keyword.get(@instance, :limit)
def post(user, %{"status" => status} = data) do
visibility = get_visibility(data)
with status <- String.trim(status),
length when length in 1..@limit <- String.length(status),
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"]),
{to, cc} <- to_for_user_and_mentions(user, mentions, inReplyTo, visibility),
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),
cw <- data["spoiler_text"],
object <- make_note_data(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}})
object <-
make_note_data(
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)
res
end
end
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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,47 +4,59 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do
@httpoison Application.get_env(:pleroma, :httpoison)
@max_body_length 25 * 1048576
@max_body_length 25 * 1_048_576
@cache_control %{
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
config = Application.get_env(:pleroma, :media_proxy, [])
with \
true <- Keyword.get(config, :enabled, false),
{:ok, url} <- Pleroma.Web.MediaProxy.decode_url(sig, url),
{:ok, content_type, body} <- proxy_request(url)
do
with true <- Keyword.get(config, :enabled, false),
{:ok, url} <- Pleroma.Web.MediaProxy.decode_url(sig, url),
{:ok, content_type, body} <- proxy_request(url) do
conn
|> put_resp_content_type(content_type)
|> set_cache_header(:default)
|> send_resp(200, body)
else
false -> send_error(conn, 404)
{:error, :invalid_signature} -> send_error(conn, 403)
{:error, {:http, _, url}} -> redirect_or_error(conn, url, Keyword.get(config, :redirect_on_failure, true))
false ->
send_error(conn, 404)
{:error, :invalid_signature} ->
send_error(conn, 403)
{:error, {:http, _, url}} ->
redirect_or_error(conn, url, Keyword.get(config, :redirect_on_failure, true))
end
end
defp proxy_request(link) do
headers = [{"user-agent", "Pleroma/MediaProxy; #{Pleroma.Web.base_url()} <#{Application.get_env(:pleroma, :instance)[:email]}>"}]
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
headers = [
{"user-agent",
"Pleroma/MediaProxy; #{Pleroma.Web.base_url()} <#{
Application.get_env(:pleroma, :instance)[:email]
}>"}
]
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}
else
{: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, 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}}
end
end
@ -63,13 +75,15 @@ defp send_error(conn, code, body \\ "") do
end
defp proxy_request_body(client), do: proxy_request_body(client, <<>>)
defp proxy_request_body(client, body) when byte_size(body) < @max_body_length 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}
{:error, reason} -> {:error, reason}
end
end
defp proxy_request_body(client, _) do
:hackney.close(client)
{:error, :body_too_large}
@ -80,5 +94,4 @@ defp proxy_request_body(client, _) do
defp proxy_request_content_type(headers, _body) do
headers["Content-Type"] || headers["content-type"] || "image/jpeg"
end
end

View file

@ -7,14 +7,15 @@ def url(url = "/" <> _), do: url
def url(url) do
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
else
secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base]
base64 = Base.url_encode64(url, @base64_opts)
sig = :crypto.hmac(:sha, secret, base64)
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
@ -22,11 +23,11 @@ def decode_url(sig, url) do
secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base]
sig = Base.url_decode64!(sig, @base64_opts)
local_sig = :crypto.hmac(:sha, secret, url)
if local_sig == sig do
{:ok, Base.url_decode64!(url, @base64_opts)}
else
{:error, :invalid_signature}
end
end
end

View file

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

View file

@ -7,24 +7,24 @@ defmodule Pleroma.Web.OAuth.Authorization do
import Ecto.{Changeset}
schema "oauth_authorizations" do
field :token, :string
field :valid_until, :naive_datetime
field :used, :boolean, default: false
belongs_to :user, Pleroma.User
belongs_to :app, Pleroma.App
field(:token, :string)
field(:valid_until, :naive_datetime)
field(:used, :boolean, default: false)
belongs_to(:user, Pleroma.User)
belongs_to(:app, Pleroma.App)
timestamps()
end
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{
token: token,
used: false,
user_id: user.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)
@ -37,11 +37,12 @@ def use_changeset(%Authorization{} = auth, params) do
end
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}))
else
{:error, "token expired"}
end
end
def use_token(%Authorization{used: true}), do: {:error, "already used"}
end

View file

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

View file

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

View file

@ -5,11 +5,11 @@ defmodule Pleroma.Web.OAuth.Token do
alias Pleroma.Web.OAuth.{Token, App, Authorization}
schema "oauth_tokens" do
field :token, :string
field :refresh_token, :string
field :valid_until, :naive_datetime
belongs_to :user, Pleroma.User
belongs_to :app, Pleroma.App
field(:token, :string)
field(:refresh_token, :string)
field(:valid_until, :naive_datetime)
belongs_to(:user, Pleroma.User)
belongs_to(:app, Pleroma.App)
timestamps()
end
@ -22,15 +22,15 @@ def exchange_token(app, auth) do
end
def create_token(%App{} = app, %User{} = user) do
token = :crypto.strong_rand_bytes(32) |> Base.url_encode64
refresh_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()
token = %Token{
token: token,
refresh_token: refresh_token,
user_id: user.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)

View file

@ -5,7 +5,7 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do
require Logger
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
else
_e -> id
@ -13,42 +13,60 @@ defp get_href(id) do
end
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
defp get_in_reply_to(_), do: []
defp get_mentions(to) do
Enum.map(to, fn (id) ->
Enum.map(to, fn id ->
cond do
# Special handling for the AP/Ostatus public collections
"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.
Regex.match?(~r/^#{Pleroma.Web.base_url}.+followers$/, id) ->
Regex.match?(~r/^#{Pleroma.Web.base_url()}.+followers$/, id) ->
[]
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
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: ['text/html'], href: h.(data["object"]["id"]), rel: 'alternate'], []}
]
end
defp get_links(%{local: false,
data: %{
"object" => %{
"external_url" => external_url
}
}}) do
defp get_links(%{
local: false,
data: %{
"object" => %{
"external_url" => external_url
}
}
}) 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'], []}
]
@ -57,60 +75,72 @@ defp get_links(%{local: false,
defp get_links(_activity), 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)], []}
end)
end
def to_simple_form(activity, user, with_author \\ false)
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"]
inserted_at = activity.data["object"]["published"]
attachments = Enum.map(activity.data["object"]["attachment"] || [], fn(attachment) ->
url = hd(attachment["url"])
{:link, [rel: 'enclosure', href: to_charlist(url["href"]), type: to_charlist(url["mediaType"])], []}
end)
attachments =
Enum.map(activity.data["object"]["attachment"] || [], fn attachment ->
url = hd(attachment["url"])
{:link,
[rel: 'enclosure', href: to_charlist(url["href"]), type: to_charlist(url["mediaType"])],
[]}
end)
in_reply_to = get_in_reply_to(activity.data)
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
mentions = activity.recipients |> get_mentions
categories = (activity.data["object"]["tag"] || [])
|> Enum.map(fn (tag) ->
if is_binary(tag) do
{:category, [term: to_charlist(tag)], []}
else
nil
end
end)
|> Enum.filter(&(&1))
categories =
(activity.data["object"]["tag"] || [])
|> Enum.map(fn tag ->
if is_binary(tag) do
{:category, [term: to_charlist(tag)], []}
else
nil
end
end)
|> Enum.filter(& &1)
emoji_links = get_emoji_links(activity.data["object"]["emoji"] || %{})
summary = if activity.data["object"]["summary"] do
[{:summary, [], h.(activity.data["object"]["summary"])}]
else
[]
end
summary =
if activity.data["object"]["summary"] do
[{:summary, [], h.(activity.data["object"]["summary"])}]
else
[]
end
[
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/note']},
{:"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}']},
{: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)},
{:updated, h.(updated_at)},
{:"ostatus:conversation", [ref: h.(activity.data["context"])], h.(activity.data["context"])},
{:link, [ref: h.(activity.data["context"]), rel: 'ostatus:conversation'], []},
] ++ summary ++ get_links(activity) ++ categories ++ attachments ++ in_reply_to ++ author ++ mentions ++ emoji_links
{:link, [ref: h.(activity.data["context"]), rel: 'ostatus:conversation'], []}
] ++
summary ++
get_links(activity) ++
categories ++ attachments ++ in_reply_to ++ author ++ mentions ++ emoji_links
end
def to_simple_form(%{data: %{"type" => "Like"}} = activity, user, with_author) do
h = fn(str) -> [to_charlist(str)] end
h = fn str -> [to_charlist(str)] end
updated_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']},
{:published, h.(inserted_at)},
{:updated, h.(updated_at)},
{:"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",
[
{:"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"])},
{:link, [ref: h.(activity.data["context"]), rel: 'ostatus:conversation'], []},
{: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
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"]
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)
mentions = activity.recipients |> get_mentions
[
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']},
{:"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
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"]
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: []
mentions = (activity.recipients || []) |> get_mentions
[
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']},
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/follow']},
{:id, h.(activity.data["id"])},
{: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)},
{:updated, h.(updated_at)},
{:"activity:object", [
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/person']},
{:id, h.(activity.data["object"])},
{:uri, h.(activity.data["object"])},
]},
{:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []},
{:"activity:object",
[
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/person']},
{:id, h.(activity.data["object"])},
{:uri, h.(activity.data["object"])}
]},
{:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []}
] ++ mentions ++ author
end
# 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
h = fn(str) -> [to_charlist(str)] end
h = fn str -> [to_charlist(str)] end
updated_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"])
mentions = (activity.recipients || []) |> get_mentions
[
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']},
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/unfollow']},
{:id, h.(activity.data["id"])},
{: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)},
{:updated, h.(updated_at)},
{:"activity:object", [
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/person']},
{:id, h.(follow_activity.data["object"])},
{:uri, h.(follow_activity.data["object"])},
]},
{:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []},
{:"activity:object",
[
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/person']},
{:id, h.(follow_activity.data["object"])},
{:uri, h.(follow_activity.data["object"])}
]},
{:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []}
] ++ mentions ++ author
end
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"]
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']},
{:published, h.(inserted_at)},
{:updated, h.(updated_at)}
] ++ author
] ++ author
end
def to_simple_form(_, _, _), do: nil
def wrap_with_entry(simple_form) do
[{
:entry, [
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'
], simple_form
}]
[
{
:entry,
[
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'
],
simple_form
}
]
end
end

View file

@ -5,44 +5,57 @@ defmodule Pleroma.Web.OStatus.FeedRepresenter do
alias Pleroma.Web.MediaProxy
def to_simple_form(user, activities, _users) do
most_recent_update = (List.first(activities) || user).updated_at
|> NaiveDateTime.to_iso8601
most_recent_update =
(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)
entries = activities
|> Enum.map(fn(activity) ->
{:entry, ActivityRepresenter.to_simple_form(activity, user)}
end)
|> Enum.filter(fn ({_, form}) -> form end)
entries =
activities
|> Enum.map(fn activity ->
{:entry, ActivityRepresenter.to_simple_form(activity, user)}
end)
|> Enum.filter(fn {_, form} -> form end)
[{
: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'
], [
{:id, h.(OStatus.feed_path(user))},
{:title, ['#{user.nickname}\'s timeline']},
{:updated, h.(most_recent_update)},
{:logo, [to_charlist(User.avatar_url(user) |> MediaProxy.url())]},
{:link, [rel: 'hub', href: h.(OStatus.pubsub_path(user))], []},
{:link, [rel: 'salmon', href: h.(OStatus.salmon_path(user))], []},
{:link, [rel: 'self', href: h.(OStatus.feed_path(user)), type: 'application/atom+xml'], []},
{:author, UserRepresenter.to_simple_form(user)},
] ++
if last_activity do
[{:link, [rel: 'next',
href: to_charlist(OStatus.feed_path(user)) ++ '?max_id=' ++ to_charlist(last_activity.id),
type: 'application/atom+xml'], []}]
else
[]
end
++ entries
}]
[
{
: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'
],
[
{:id, h.(OStatus.feed_path(user))},
{:title, ['#{user.nickname}\'s timeline']},
{:updated, h.(most_recent_update)},
{:logo, [to_charlist(User.avatar_url(user) |> MediaProxy.url())]},
{:link, [rel: 'hub', href: h.(OStatus.pubsub_path(user))], []},
{:link, [rel: 'salmon', href: h.(OStatus.salmon_path(user))], []},
{:link, [rel: 'self', href: h.(OStatus.feed_path(user)), type: 'application/atom+xml'],
[]},
{:author, UserRepresenter.to_simple_form(user)}
] ++
if last_activity do
[
{:link,
[
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

View file

@ -6,7 +6,8 @@ defmodule Pleroma.Web.OStatus.FollowHandler do
def handle(entry, doc) do
with {:ok, actor} <- OStatus.find_make_or_update_user(doc),
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, activity} <- ActivityPub.follow(actor, followed, id, false) do
User.follow(actor, followed)

View file

@ -13,49 +13,56 @@ defmodule Pleroma.Web.OStatus.NoteHandler do
3. A newly generated context id.
"""
def get_context(entry, inReplyTo) do
context = (
XML.string_from_xpath("//ostatus:conversation[1]", entry)
|| XML.string_from_xpath("//ostatus:conversation[1]/@ref", entry)
|| "") |> String.trim
context =
(XML.string_from_xpath("//ostatus:conversation[1]", entry) ||
XML.string_from_xpath("//ostatus:conversation[1]/@ref", entry) || "")
|> String.trim()
with %{data: %{"context" => context}} <- Object.get_cached_by_ap_id(inReplyTo) do
context
else _e ->
if String.length(context) > 0 do
context
else
Utils.generate_context_id
end
else
_e ->
if String.length(context) > 0 do
context
else
Utils.generate_context_id()
end
end
end
def get_people_mentions(entry) do
:xmerl_xpath.string('//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)
:xmerl_xpath.string(
'//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
def get_collection_mentions(entry) do
transmogrify = fn
("http://activityschema.org/collection/public") ->
"http://activityschema.org/collection/public" ->
"https://www.w3.org/ns/activitystreams#Public"
(group) ->
group ->
group
end
:xmerl_xpath.string('//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)
:xmerl_xpath.string(
'//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
def get_mentions(entry) do
(get_people_mentions(entry)
++ get_collection_mentions(entry))
|> Enum.filter(&(&1))
(get_people_mentions(entry) ++ get_collection_mentions(entry))
|> Enum.filter(& &1)
end
def get_emoji(entry) do
try do
: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))
end)
rescue
@ -79,7 +86,8 @@ def fetch_replied_to_activity(entry, inReplyTo) do
activity
else
_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
activity
else
@ -107,16 +115,40 @@ def handle_note(entry, doc \\ nil) do
date <- XML.string_from_xpath("//published", entry),
unlisted <- XML.string_from_xpath("//mastodon:scope", entry) == "unlisted",
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("published", date),
note <- note |> Map.put("emoji", get_emoji(entry)),
note <- add_external_url(note, entry),
note <- note |> Map.put("cc", cc),
# TODO: Handle this case in make_note_data
note <- (if 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}})
note <-
if(
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)
res
else

View file

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

View file

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

View file

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

View file

@ -11,294 +11,305 @@ def user_fetcher(username) do
end
pipeline :api do
plug :accepts, ["json"]
plug :fetch_session
plug Pleroma.Plugs.OAuthPlug
plug Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1, optional: true}
plug(:accepts, ["json"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1, optional: true})
end
pipeline :authenticated_api do
plug :accepts, ["json"]
plug :fetch_session
plug Pleroma.Plugs.OAuthPlug
plug Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1}
plug(:accepts, ["json"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1})
end
pipeline :mastodon_html do
plug :accepts, ["html"]
plug :fetch_session
plug Pleroma.Plugs.OAuthPlug
plug Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1, optional: true}
plug(:accepts, ["html"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1, optional: true})
end
pipeline :pleroma_html do
plug :accepts, ["html"]
plug :fetch_session
plug Pleroma.Plugs.OAuthPlug
plug Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1, optional: true}
plug(:accepts, ["html"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1, optional: true})
end
pipeline :well_known do
plug :accepts, ["xml", "xrd+xml", "json", "jrd+json"]
plug(:accepts, ["xml", "xrd+xml", "json", "jrd+json"])
end
pipeline :config do
plug :accepts, ["json", "xml"]
plug(:accepts, ["json", "xml"])
end
pipeline :oauth do
plug :accepts, ["html", "json"]
plug(:accepts, ["html", "json"])
end
pipeline :pleroma_api do
plug :accepts, ["html", "json"]
plug(:accepts, ["html", "json"])
end
scope "/api/pleroma", Pleroma.Web.TwitterAPI do
pipe_through :pleroma_api
get "/password_reset/:token", UtilController, :show_password_reset
post "/password_reset", UtilController, :password_reset
get "/emoji", UtilController, :emoji
pipe_through(:pleroma_api)
get("/password_reset/:token", UtilController, :show_password_reset)
post("/password_reset", UtilController, :password_reset)
get("/emoji", UtilController, :emoji)
end
scope "/", Pleroma.Web.TwitterAPI do
pipe_through :pleroma_html
get "/ostatus_subscribe", UtilController, :remote_follow
post "/ostatus_subscribe", UtilController, :do_remote_follow
post "/main/ostatus", UtilController, :remote_subscribe
pipe_through(:pleroma_html)
get("/ostatus_subscribe", UtilController, :remote_follow)
post("/ostatus_subscribe", UtilController, :do_remote_follow)
post("/main/ostatus", UtilController, :remote_subscribe)
end
scope "/api/pleroma", Pleroma.Web.TwitterAPI do
pipe_through :authenticated_api
post "/follow_import", UtilController, :follow_import
pipe_through(:authenticated_api)
post("/follow_import", UtilController, :follow_import)
end
scope "/oauth", Pleroma.Web.OAuth do
get "/authorize", OAuthController, :authorize
post "/authorize", OAuthController, :create_authorization
post "/token", OAuthController, :token_exchange
get("/authorize", OAuthController, :authorize)
post("/authorize", OAuthController, :create_authorization)
post("/token", OAuthController, :token_exchange)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through :authenticated_api
pipe_through(:authenticated_api)
patch "/accounts/update_credentials", MastodonAPIController, :update_credentials
get "/accounts/verify_credentials", MastodonAPIController, :verify_credentials
get "/accounts/relationships", MastodonAPIController, :relationships
get "/accounts/search", MastodonAPIController, :account_search
post "/accounts/:id/follow", MastodonAPIController, :follow
post "/accounts/:id/unfollow", MastodonAPIController, :unfollow
post "/accounts/:id/block", MastodonAPIController, :block
post "/accounts/:id/unblock", MastodonAPIController, :unblock
post "/accounts/:id/mute", MastodonAPIController, :relationship_noop
post "/accounts/:id/unmute", MastodonAPIController, :relationship_noop
patch("/accounts/update_credentials", MastodonAPIController, :update_credentials)
get("/accounts/verify_credentials", MastodonAPIController, :verify_credentials)
get("/accounts/relationships", MastodonAPIController, :relationships)
get("/accounts/search", MastodonAPIController, :account_search)
post("/accounts/:id/follow", MastodonAPIController, :follow)
post("/accounts/:id/unfollow", MastodonAPIController, :unfollow)
post("/accounts/:id/block", MastodonAPIController, :block)
post("/accounts/:id/unblock", MastodonAPIController, :unblock)
post("/accounts/:id/mute", 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 "/follow_requests", MastodonAPIController, :empty_array
get "/mutes", MastodonAPIController, :empty_array
get("/domain_blocks", MastodonAPIController, :empty_array)
get("/follow_requests", 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
delete "/statuses/:id", MastodonAPIController, :delete_status
post("/statuses", MastodonAPIController, :post_status)
delete("/statuses/:id", MastodonAPIController, :delete_status)
post "/statuses/:id/reblog", MastodonAPIController, :reblog_status
post "/statuses/:id/favourite", MastodonAPIController, :fav_status
post "/statuses/:id/unfavourite", MastodonAPIController, :unfav_status
post("/statuses/:id/reblog", MastodonAPIController, :reblog_status)
post("/statuses/:id/favourite", MastodonAPIController, :fav_status)
post("/statuses/:id/unfavourite", MastodonAPIController, :unfav_status)
post "/notifications/clear", MastodonAPIController, :clear_notifications
post "/notifications/dismiss", MastodonAPIController, :dismiss_notification
get "/notifications", MastodonAPIController, :notifications
get "/notifications/:id", MastodonAPIController, :get_notification
post("/notifications/clear", MastodonAPIController, :clear_notifications)
post("/notifications/dismiss", MastodonAPIController, :dismiss_notification)
get("/notifications", MastodonAPIController, :notifications)
get("/notifications/:id", MastodonAPIController, :get_notification)
post "/media", MastodonAPIController, :upload
post("/media", MastodonAPIController, :upload)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through :api
get "/instance", MastodonAPIController, :masto_instance
get "/instance/peers", MastodonAPIController, :peers
post "/apps", MastodonAPIController, :create_app
get "/custom_emojis", MastodonAPIController, :custom_emojis
pipe_through(:api)
get("/instance", MastodonAPIController, :masto_instance)
get("/instance/peers", MastodonAPIController, :peers)
post("/apps", MastodonAPIController, :create_app)
get("/custom_emojis", MastodonAPIController, :custom_emojis)
get "/timelines/public", MastodonAPIController, :public_timeline
get "/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline
get("/timelines/public", MastodonAPIController, :public_timeline)
get("/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline)
get "/statuses/:id", MastodonAPIController, :get_status
get "/statuses/:id/context", MastodonAPIController, :get_context
get "/statuses/:id/card", MastodonAPIController, :empty_object
get "/statuses/:id/favourited_by", MastodonAPIController, :favourited_by
get "/statuses/:id/reblogged_by", MastodonAPIController, :reblogged_by
get("/statuses/:id", MastodonAPIController, :get_status)
get("/statuses/:id/context", MastodonAPIController, :get_context)
get("/statuses/:id/card", MastodonAPIController, :empty_object)
get("/statuses/:id/favourited_by", MastodonAPIController, :favourited_by)
get("/statuses/:id/reblogged_by", MastodonAPIController, :reblogged_by)
get "/accounts/:id/statuses", MastodonAPIController, :user_statuses
get "/accounts/:id/followers", MastodonAPIController, :followers
get "/accounts/:id/following", MastodonAPIController, :following
get "/accounts/:id", MastodonAPIController, :user
get("/accounts/:id/statuses", MastodonAPIController, :user_statuses)
get("/accounts/:id/followers", MastodonAPIController, :followers)
get("/accounts/:id/following", MastodonAPIController, :following)
get("/accounts/:id", MastodonAPIController, :user)
get "/search", MastodonAPIController, :search
get("/search", MastodonAPIController, :search)
end
scope "/api", Pleroma.Web do
pipe_through :config
pipe_through(:config)
get "/help/test", TwitterAPI.UtilController, :help_test
post "/help/test", TwitterAPI.UtilController, :help_test
get "/statusnet/config", TwitterAPI.UtilController, :config
get "/statusnet/version", TwitterAPI.UtilController, :version
get("/help/test", TwitterAPI.UtilController, :help_test)
post("/help/test", TwitterAPI.UtilController, :help_test)
get("/statusnet/config", TwitterAPI.UtilController, :config)
get("/statusnet/version", TwitterAPI.UtilController, :version)
end
@instance Application.get_env(:pleroma, :instance)
@registrations_open Keyword.get(@instance, :registrations_open)
scope "/api", Pleroma.Web do
pipe_through :api
pipe_through(:api)
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/public_timeline", TwitterAPI.Controller, :public_timeline)
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
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("/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
post "/account/register", TwitterAPI.Controller, :register
post("/account/register", TwitterAPI.Controller, :register)
end
get "/search", TwitterAPI.Controller, :search
get "/statusnet/tags/timeline/:tag", TwitterAPI.Controller, :public_and_external_timeline
get("/search", TwitterAPI.Controller, :search)
get("/statusnet/tags/timeline/:tag", TwitterAPI.Controller, :public_and_external_timeline)
end
scope "/api", Pleroma.Web do
pipe_through :authenticated_api
pipe_through(:authenticated_api)
get "/account/verify_credentials", TwitterAPI.Controller, :verify_credentials
post "/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/update_profile", TwitterAPI.Controller, :update_profile
post "/account/update_profile_banner", TwitterAPI.Controller, :update_banner
post "/qvitter/update_background_image", TwitterAPI.Controller, :update_background
post("/account/update_profile", TwitterAPI.Controller, :update_profile)
post("/account/update_profile_banner", TwitterAPI.Controller, :update_banner)
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/friends_timeline", TwitterAPI.Controller, :friends_timeline
get "/statuses/mentions", TwitterAPI.Controller, :mentions_timeline
get "/statuses/mentions_timeline", TwitterAPI.Controller, :mentions_timeline
get("/statuses/home_timeline", TwitterAPI.Controller, :friends_timeline)
get("/statuses/friends_timeline", TwitterAPI.Controller, :friends_timeline)
get("/statuses/mentions", TwitterAPI.Controller, :mentions_timeline)
get("/statuses/mentions_timeline", TwitterAPI.Controller, :mentions_timeline)
post "/statuses/update", TwitterAPI.Controller, :status_update
post "/statuses/retweet/:id", TwitterAPI.Controller, :retweet
post "/statuses/destroy/:id", TwitterAPI.Controller, :delete_post
post("/statuses/update", TwitterAPI.Controller, :status_update)
post("/statuses/retweet/:id", TwitterAPI.Controller, :retweet)
post("/statuses/destroy/:id", TwitterAPI.Controller, :delete_post)
post "/friendships/create", TwitterAPI.Controller, :follow
post "/friendships/destroy", TwitterAPI.Controller, :unfollow
post "/blocks/create", TwitterAPI.Controller, :block
post "/blocks/destroy", TwitterAPI.Controller, :unblock
post("/friendships/create", TwitterAPI.Controller, :follow)
post("/friendships/destroy", TwitterAPI.Controller, :unfollow)
post("/blocks/create", TwitterAPI.Controller, :block)
post("/blocks/destroy", TwitterAPI.Controller, :unblock)
post "/statusnet/media/upload", TwitterAPI.Controller, :upload
post "/media/upload", TwitterAPI.Controller, :upload_json
post("/statusnet/media/upload", TwitterAPI.Controller, :upload)
post("/media/upload", TwitterAPI.Controller, :upload_json)
post "/favorites/create/:id", TwitterAPI.Controller, :favorite
post "/favorites/create", TwitterAPI.Controller, :favorite
post "/favorites/destroy/:id", TwitterAPI.Controller, :unfavorite
post("/favorites/create/:id", TwitterAPI.Controller, :favorite)
post("/favorites/create", TwitterAPI.Controller, :favorite)
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 "/friendships/no_retweets/ids", TwitterAPI.Controller, :empty_array
get("/friends/ids", TwitterAPI.Controller, :friends_ids)
get("/friendships/no_retweets/ids", TwitterAPI.Controller, :empty_array)
get "/mutes/users/ids", TwitterAPI.Controller, :empty_array
get("/mutes/users/ids", TwitterAPI.Controller, :empty_array)
get "/externalprofile/show", TwitterAPI.Controller, :external_profile
get("/externalprofile/show", TwitterAPI.Controller, :external_profile)
end
pipeline :ostatus do
plug :accepts, ["xml", "atom", "html", "activity+json"]
plug(:accepts, ["xml", "atom", "html", "activity+json"])
end
scope "/", Pleroma.Web do
pipe_through :ostatus
pipe_through(:ostatus)
get "/objects/:uuid", OStatus.OStatusController, :object
get "/activities/:uuid", OStatus.OStatusController, :activity
get "/notice/:id", OStatus.OStatusController, :notice
get "/users/:nickname/feed", OStatus.OStatusController, :feed
get "/users/:nickname", OStatus.OStatusController, :feed_redirect
get("/objects/:uuid", OStatus.OStatusController, :object)
get("/activities/:uuid", OStatus.OStatusController, :activity)
get("/notice/:id", OStatus.OStatusController, :notice)
get("/users/:nickname/feed", OStatus.OStatusController, :feed)
get("/users/:nickname", OStatus.OStatusController, :feed_redirect)
if @federating do
post "/users/:nickname/salmon", OStatus.OStatusController, :salmon_incoming
post "/push/hub/:nickname", Websub.WebsubController, :websub_subscription_request
get "/push/subscriptions/:id", Websub.WebsubController, :websub_subscription_confirmation
post "/push/subscriptions/:id", Websub.WebsubController, :websub_incoming
post("/users/:nickname/salmon", OStatus.OStatusController, :salmon_incoming)
post("/push/hub/:nickname", Websub.WebsubController, :websub_subscription_request)
get("/push/subscriptions/:id", Websub.WebsubController, :websub_subscription_confirmation)
post("/push/subscriptions/:id", Websub.WebsubController, :websub_incoming)
end
end
pipeline :activitypub do
plug :accepts, ["activity+json"]
plug Pleroma.Web.Plugs.HTTPSignaturePlug
plug(:accepts, ["activity+json"])
plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
end
scope "/", Pleroma.Web.ActivityPub do
# XXX: not really ostatus
pipe_through :ostatus
pipe_through(:ostatus)
get "/users/:nickname/followers", ActivityPubController, :followers
get "/users/:nickname/following", ActivityPubController, :following
get "/users/:nickname/outbox", ActivityPubController, :outbox
get("/users/:nickname/followers", ActivityPubController, :followers)
get("/users/:nickname/following", ActivityPubController, :following)
get("/users/:nickname/outbox", ActivityPubController, :outbox)
end
if @federating do
scope "/", Pleroma.Web.ActivityPub do
pipe_through :activitypub
post "/users/:nickname/inbox", ActivityPubController, :inbox
post "/inbox", ActivityPubController, :inbox
pipe_through(:activitypub)
post("/users/:nickname/inbox", ActivityPubController, :inbox)
post("/inbox", ActivityPubController, :inbox)
end
scope "/.well-known", Pleroma.Web do
pipe_through :well_known
pipe_through(:well_known)
get "/host-meta", WebFinger.WebFingerController, :host_meta
get "/webfinger", WebFinger.WebFingerController, :webfinger
get("/host-meta", WebFinger.WebFingerController, :host_meta)
get("/webfinger", WebFinger.WebFingerController, :webfinger)
end
end
scope "/", Pleroma.Web.MastodonAPI do
pipe_through :mastodon_html
pipe_through(:mastodon_html)
get "/web/login", MastodonAPIController, :login
post "/web/login", MastodonAPIController, :login_post
get "/web/*path", MastodonAPIController, :index
delete "/auth/sign_out", MastodonAPIController, :logout
get("/web/login", MastodonAPIController, :login)
post("/web/login", MastodonAPIController, :login_post)
get("/web/*path", MastodonAPIController, :index)
delete("/auth/sign_out", MastodonAPIController, :logout)
end
pipeline :remote_media do
plug :accepts, ["html"]
plug(:accepts, ["html"])
end
scope "/proxy/", Pleroma.Web.MediaProxy do
pipe_through :remote_media
get "/:sig/:url", MediaProxyController, :remote
pipe_through(:remote_media)
get("/:sig/:url", MediaProxyController, :remote)
end
scope "/", Fallback do
get "/*path", RedirectController, :redirector
get("/*path", RedirectController, :redirector)
end
end
defmodule Fallback.RedirectController do
use Pleroma.Web, :controller
def redirector(conn, _params) do
if Mix.env != :test do
if Mix.env() != :test do
conn
|> put_resp_content_type("text/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
[data, type, encoding, alg, sig] = decode(salmon)
signed_text = [data, type, encoding, alg]
|> Enum.map(&Base.url_encode64/1)
|> Enum.join(".")
signed_text =
[data, type, encoding, alg]
|> Enum.map(&Base.url_encode64/1)
|> Enum.join(".")
key = decode_key(magickey)
@ -54,22 +55,23 @@ def decode_and_validate(magickey, salmon) do
end
def decode_key("RSA." <> magickey) do
make_integer = fn(bin) ->
make_integer = fn 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
[modulus, exponent] = magickey
|> String.split(".")
|> Enum.map(fn (n) -> Base.url_decode64!(n, padding: false) end)
|> Enum.map(make_integer)
[modulus, exponent] =
magickey
|> String.split(".")
|> Enum.map(fn n -> Base.url_decode64!(n, padding: false) end)
|> Enum.map(make_integer)
{:RSAPublicKey, modulus, exponent}
end
def encode_key({:RSAPublicKey, modulus, exponent}) do
modulus_enc = :binary.encode_unsigned(modulus) |> Base.url_encode64
exponent_enc = :binary.encode_unsigned(exponent) |> Base.url_encode64
modulus_enc = :binary.encode_unsigned(modulus) |> Base.url_encode64()
exponent_enc = :binary.encode_unsigned(exponent) |> Base.url_encode64()
"RSA.#{modulus_enc}.#{exponent_enc}"
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.
try do
_ = :public_key.generate_key({:rsa, 2048, 65537})
def generate_rsa_pem do
key = :public_key.generate_key({:rsa, 2048, 65537})
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
pem = :public_key.pem_encode([entry]) |> String.trim_trailing
pem = :public_key.pem_encode([entry]) |> String.trim_trailing()
{:ok, pem}
end
rescue
_ ->
def generate_rsa_pem do
port = Port.open({:spawn, "openssl genrsa"}, [:binary])
{:ok, pem} = receive do
{^port, {:data, pem}} -> {:ok, pem}
end
{:ok, pem} =
receive do
{^port, {:data, pem}} -> {:ok, pem}
end
Port.close(port)
if Regex.match?(~r/RSA PRIVATE KEY/, pem) do
{:ok, pem}
else
@ -113,17 +120,20 @@ def encode(private_key, doc) do
encoding = "base64url"
alg = "RSA-SHA256"
signed_text = [doc, type, encoding, alg]
|> Enum.map(&Base.url_encode64/1)
|> Enum.join(".")
signed_text =
[doc, type, encoding, alg]
|> Enum.map(&Base.url_encode64/1)
|> Enum.join(".")
signature = signed_text
|> :public_key.sign(:sha256, private_key)
|> to_string
|> Base.url_encode64
signature =
signed_text
|> :public_key.sign(:sha256, private_key)
|> to_string
|> Base.url_encode64()
doc_base64 = doc
|> Base.url_encode64
doc_base64 =
doc
|> Base.url_encode64()
# Don't need proper xml building, these strings are safe to leave unescaped
salmon = """
@ -141,20 +151,29 @@ def encode(private_key, doc) do
def remote_users(%{data: %{"to" => to} = data}) do
to = to ++ (data["cc"] || [])
to
|> Enum.map(fn(id) -> User.get_cached_by_ap_id(id) end)
|> Enum.filter(fn(user) -> user && !user.local end)
|> Enum.map(fn id -> User.get_cached_by_ap_id(id) end)
|> Enum.filter(fn user -> user && !user.local end)
end
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)
else
e -> Logger.debug(fn -> "Pushing Salmon to #{salmon} failed, #{inspect(e)}" end)
end
end
defp send_to_user(_,_,_), do: nil
defp send_to_user(_, _, _), do: nil
@supported_activities [
"Create",
@ -165,18 +184,21 @@ defp send_to_user(_,_,_), do: nil
"Delete"
]
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)
|> ActivityRepresenter.wrap_with_entry
|> :xmerl.export_simple(:xmerl_xml)
|> to_string
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)
|> ActivityRepresenter.wrap_with_entry()
|> :xmerl.export_simple(:xmerl_xml)
|> to_string
if feed do
{:ok, private, _} = keys_from_pem(keys)
{:ok, feed} = encode(private, feed)
remote_users(activity)
|> Enum.each(fn(remote_user) ->
|> Enum.each(fn remote_user ->
Task.start(fn ->
Logger.debug(fn -> "Sending Salmon to #{remote_user.ap_id}" end)
send_to_user(remote_user, feed, poster)

View file

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

View file

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

View file

@ -7,18 +7,22 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do
alias Pleroma.Formatter
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
def to_map(%Activity{data: %{"type" => "Announce", "actor" => actor, "published" => created_at}} = activity,
%{users: users, announced_activity: announced_activity} = opts) do
def to_map(
%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)
created_at = created_at |> Utils.date_to_asctime
created_at = created_at |> Utils.date_to_asctime()
text = "#{user.nickname} retweeted a status."
announced_user = user_by_ap_id(users, announced_activity.data["actor"])
retweeted_status = to_map(announced_activity, Map.merge(%{user: announced_user}, opts))
%{
"id" => activity.id,
"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
def to_map(%Activity{data: %{"type" => "Like", "published" => created_at}} = activity,
%{user: user, liked_activity: liked_activity} = opts) do
created_at = created_at |> Utils.date_to_asctime
def to_map(
%Activity{data: %{"type" => "Like", "published" => created_at}} = activity,
%{user: user, liked_activity: liked_activity} = opts
) do
created_at = created_at |> Utils.date_to_asctime()
text = "#{user.nickname} favorited a status."
@ -56,12 +62,16 @@ def to_map(%Activity{data: %{"type" => "Like", "published" => created_at}} = act
}
end
def to_map(%Activity{data: %{"type" => "Follow", "object" => followed_id}} = activity, %{user: user} = opts) do
created_at = activity.data["published"] || (DateTime.to_iso8601(activity.inserted_at))
created_at = created_at |> Utils.date_to_asctime
def to_map(
%Activity{data: %{"type" => "Follow", "object" => followed_id}} = activity,
%{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)
text = "#{user.nickname} started following #{followed.nickname}"
%{
"id" => activity.id,
"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:
# 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
created_at = created_at |> Utils.date_to_asctime
def to_map(
%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}"
%{
"id" => activity.id,
"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
def to_map(%Activity{data: %{"type" => "Delete", "published" => created_at, "object" => _ }} = activity, %{user: user} = opts) do
created_at = created_at |> Utils.date_to_asctime
def to_map(
%Activity{data: %{"type" => "Delete", "published" => created_at, "object" => _}} =
activity,
%{user: user} = opts
) do
created_at = created_at |> Utils.date_to_asctime()
%{
"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]}),
"attentions" => [],
"statusnet_html" => "deleted notice {{tag",
"text" => "deleted notice {{tag" ,
"text" => "deleted notice {{tag",
"is_local" => activity.local,
"is_post_verb" => false,
"created_at" => created_at,
@ -117,8 +137,11 @@ def to_map(%Activity{data: %{"type" => "Delete", "published" => created_at, "obj
}
end
def to_map(%Activity{data: %{"object" => %{"content" => content} = object}} = activity, %{user: user} = opts) do
created_at = object["published"] |> Utils.date_to_asctime
def to_map(
%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
announcement_count = object["announcement_count"] || 0
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] || []
attentions = activity.recipients
|> Enum.map(fn (ap_id) -> Enum.find(mentions, fn(user) -> ap_id == user.ap_id end) end)
|> Enum.filter(&(&1))
|> Enum.map(fn (user) -> UserView.render("show.json", %{user: user, for: opts[:for]}) end)
attentions =
activity.recipients
|> Enum.map(fn ap_id -> Enum.find(mentions, fn user -> ap_id == user.ap_id end) end)
|> Enum.filter(& &1)
|> Enum.map(fn user -> UserView.render("show.json", %{user: user, for: opts[:for]}) end)
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
summary = activity.data["object"]["summary"]
content = if !!summary and summary != "" do
"<span>#{activity.data["object"]["summary"]}</span><br />#{content}</span>"
else
content
end
html = HtmlSanitizeEx.basic_html(content)
|> Formatter.emojify(object["emoji"])
content =
if !!summary and summary != "" do
"<span>#{activity.data["object"]["summary"]}</span><br />#{content}</span>"
else
content
end
html =
HtmlSanitizeEx.basic_html(content)
|> Formatter.emojify(object["emoji"])
%{
"id" => activity.id,
@ -175,7 +202,8 @@ def to_map(%Activity{data: %{"object" => %{"content" => content} = object}} = ac
def conversation_id(activity) do
with context when not is_nil(context) <- activity.data["context"] do
TwitterAPI.context_to_conversation_id(context)
else _e -> nil
else
_e -> nil
end
end

View file

@ -1,15 +1,18 @@
defmodule Pleroma.Web.TwitterAPI.Representers.BaseRepresenter do
defmacro __using__(_opts) 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
object
|> to_map(options)
|> Jason.encode!
|> Jason.encode!()
end
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)
end
@ -17,11 +20,14 @@ def to_map(object) do
to_map(object, %{})
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
enum
|> enum_to_list(options)
|> Jason.encode!
|> Jason.encode!()
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
data = object.data
%{
url: url["href"] |> Pleroma.Web.MediaProxy.url(),
mimetype: url["mediaType"],

View file

@ -13,37 +13,42 @@ def create_status(%User{} = user, %{"status" => _} = data) do
end
def fetch_friend_statuses(user, opts \\ %{}) do
opts = opts
|> Map.put("blocking_user", user)
|> Map.put("user", user)
|> Map.put("type", ["Create", "Announce", "Follow", "Like"])
opts =
opts
|> Map.put("blocking_user", user)
|> Map.put("user", user)
|> Map.put("type", ["Create", "Announce", "Follow", "Like"])
ActivityPub.fetch_activities([user.ap_id | user.following], opts)
|> activities_to_statuses(%{for: user})
end
def fetch_public_statuses(user, opts \\ %{}) do
opts = opts
|> Map.put("local_only", true)
|> Map.put("blocking_user", user)
|> Map.put("type", ["Create", "Announce", "Follow"])
opts =
opts
|> Map.put("local_only", true)
|> Map.put("blocking_user", user)
|> Map.put("type", ["Create", "Announce", "Follow"])
ActivityPub.fetch_public_activities(opts)
|> activities_to_statuses(%{for: user})
end
def fetch_public_and_external_statuses(user, opts \\ %{}) do
opts = opts
|> Map.put("blocking_user", user)
|> Map.put("type", ["Create", "Announce", "Follow"])
opts =
opts
|> Map.put("blocking_user", user)
|> Map.put("type", ["Create", "Announce", "Follow"])
ActivityPub.fetch_public_activities(opts)
|> activities_to_statuses(%{for: user})
end
def fetch_user_statuses(user, opts \\ %{}) do
opts = opts
|> Map.put("type", ["Create"])
opts =
opts
|> Map.put("type", ["Create"])
ActivityPub.fetch_public_activities(opts)
|> activities_to_statuses(%{for: user})
end
@ -55,12 +60,16 @@ def fetch_mentions(user, opts \\ %{}) do
def fetch_conversation(user, id) do
with context when is_binary(context) <- conversation_id_to_context(id),
activities <- ActivityPub.fetch_activities_for_context(context, %{"blocking_user" => user, "user" => user}),
statuses <- activities |> activities_to_statuses(%{for: user})
do
activities <-
ActivityPub.fetch_activities_for_context(context, %{
"blocking_user" => user,
"user" => user
}),
statuses <- activities |> activities_to_statuses(%{for: user}) do
statuses
else _e ->
[]
else
_e ->
[]
end
end
@ -74,8 +83,7 @@ def fetch_status(user, id) do
def follow(%User{} = follower, params) do
with {:ok, %User{} = followed} <- get_user(params),
{:ok, follower} <- User.follow(follower, followed),
{:ok, activity} <- ActivityPub.follow(follower, followed)
do
{:ok, activity} <- ActivityPub.follow(follower, followed) do
{:ok, follower, followed, activity}
else
err -> err
@ -83,16 +91,17 @@ def follow(%User{} = follower, params) do
end
def unfollow(%User{} = follower, params) do
with { :ok, %User{} = unfollowed } <- get_user(params),
{ :ok, follower, follow_activity } <- User.unfollow(follower, unfollowed),
{ :ok, _activity } <- ActivityPub.insert(%{
"type" => "Undo",
"actor" => follower.ap_id,
"object" => follow_activity.data["id"], # get latest Follow for these users
"published" => make_date()
})
do
{ :ok, follower, unfollowed }
with {:ok, %User{} = unfollowed} <- get_user(params),
{:ok, follower, follow_activity} <- User.unfollow(follower, unfollowed),
{:ok, _activity} <-
ActivityPub.insert(%{
"type" => "Undo",
"actor" => follower.ap_id,
# get latest Follow for these users
"object" => follow_activity.data["id"],
"published" => make_date()
}) do
{:ok, follower, unfollowed}
else
err -> err
end
@ -100,8 +109,7 @@ def unfollow(%User{} = follower, params) do
def block(%User{} = blocker, params) do
with {:ok, %User{} = blocked} <- get_user(params),
{:ok, blocker} <- User.block(blocker, blocked)
do
{:ok, blocker} <- User.block(blocker, blocked) do
{:ok, blocker, blocked}
else
err -> err
@ -110,8 +118,7 @@ def block(%User{} = blocker, params) do
def unblock(%User{} = blocker, params) do
with {:ok, %User{} = blocked} <- get_user(params),
{:ok, blocker} <- User.unblock(blocker, blocked)
do
{:ok, blocker} <- User.unblock(blocker, blocked) do
{:ok, blocker, blocked}
else
err -> err
@ -163,13 +170,15 @@ def upload(%Plug.Upload{} = file, format \\ "xml") do
<atom:link rel="enclosure" href="#{href}" type="#{type}"></atom:link>
</rsp>
"""
"json" ->
%{
media_id: object.id,
media_id_string: "#{object.id}}",
media_url: href,
size: 0
} |> Jason.encode!
}
|> Jason.encode!()
end
end
@ -189,9 +198,11 @@ def register_user(params) do
{:ok, user}
else
{:error, changeset} ->
errors = Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
|> Jason.encode!
{:error, %{error: errors}}
errors =
Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
|> Jason.encode!()
{:error, %{error: errors}}
end
end
@ -209,16 +220,20 @@ def get_user(user \\ nil, params) do
case target = get_by_id_or_nickname(user_id) do
nil ->
{:error, "No user with such user_id"}
_ ->
{:ok, target}
end
%{"screen_name" => nickname} ->
case target = Repo.get_by(User, nickname: nickname) do
nil ->
{:error, "No user with such screen_name"}
_ ->
{:ok, target}
end
_ ->
if user do
{:ok, user}
@ -229,6 +244,7 @@ def get_user(user \\ nil, params) do
end
defp parse_int(string, default)
defp parse_int(string, default) when is_binary(string) do
with {n, _} <- Integer.parse(string) do
n
@ -236,6 +252,7 @@ defp parse_int(string, default) when is_binary(string) do
_e -> default
end
end
defp parse_int(_, default), do: default
def search(user, %{"q" => query} = params) do
@ -243,19 +260,28 @@ def search(user, %{"q" => query} = params) do
page = parse_int(params["page"], 1)
offset = (page - 1) * limit
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: ^limit,
offset: ^offset,
order_by: [desc: :inserted_at] # this one isn't indexed so psql won't take the wrong index.
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: ^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_to_statuses(activities, %{for: user})
end
defp activities_to_statuses(activities, opts) do
Enum.map(activities, fn(activity) ->
Enum.map(activities, fn activity ->
activity_to_status(activity, opts)
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)
[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
# 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_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
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"])
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 = Enum.map(activity.recipients || [], fn (ap_id) ->
if ap_id do
User.get_cached_by_ap_id(ap_id)
else
nil
end
end)
|> Enum.filter(&(&1))
mentioned_users =
Enum.map(activity.recipients || [], fn ap_id ->
if ap_id do
User.get_cached_by_ap_id(ap_id)
else
nil
end
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
defp make_date do
DateTime.utc_now() |> DateTime.to_iso8601
DateTime.utc_now() |> DateTime.to_iso8601()
end
def context_to_conversation_id(context) do
with %Object{id: id} <- Object.get_cached_by_ap_id(context) do
id
else _e ->
else
_e ->
changeset = Object.context_mapping(context)
case Repo.insert(changeset) do
{:ok, %{id: id}} -> id
{:ok, %{id: id}} ->
id
# This should be solved by an upsert, but it seems ecto
# 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
@ -322,8 +363,9 @@ def context_to_conversation_id(context) do
def conversation_id_to_context(id) do
with %Object{data: %{"id" => context}} <- Repo.get(Object, id) do
context
else _e ->
{:error, "No such conversation"}
else
_e ->
{:error, "No such conversation"}
end
end
@ -331,12 +373,15 @@ def get_external_profile(for_user, uri) do
with %User{} = user <- User.get_or_fetch(uri) do
spawn(fn ->
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)
end
end)
{:ok, UserView.render("show.json", %{user: user, for: for_user})}
else _e ->
else
_e ->
{:error, "Couldn't find user"}
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
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
|> json(ActivityRepresenter.to_map(activity, %{user: user}))
else
@ -35,10 +36,10 @@ defp empty_status_reply(conn) do
defp extract_media_ids(status_data) do
with media_ids when not is_nil(media_ids) <- status_data["media_ids"],
split_ids <- String.split(media_ids, ","),
clean_ids <- Enum.reject(split_ids, fn (id) -> String.length(id) == 0 end)
do
clean_ids
else _e -> []
clean_ids <- Enum.reject(split_ids, fn id -> String.length(id) == 0 end) do
clean_ids
else
_e -> []
end
end
@ -69,9 +70,9 @@ def friends_timeline(%{assigns: %{user: user}} = conn, params) do
def show_user(conn, params) do
with {:ok, shown} <- TwitterAPI.get_user(params) do
if user = conn.assigns.user do
render conn, UserView, "show.json", %{user: shown, for: user}
render(conn, UserView, "show.json", %{user: shown, for: user})
else
render conn, UserView, "show.json", %{user: shown}
render(conn, UserView, "show.json", %{user: shown})
end
else
{:error, msg} ->
@ -83,9 +84,11 @@ def user_timeline(%{assigns: %{user: user}} = conn, params) do
case TwitterAPI.get_user(user, params) do
{:ok, target_user} ->
params = Map.merge(params, %{"actor_id" => target_user.ap_id, "whole_db" => true})
statuses = TwitterAPI.fetch_user_statuses(user, params)
statuses = TwitterAPI.fetch_user_statuses(user, params)
conn
|> json_reply(200, statuses |> Jason.encode!)
|> json_reply(200, statuses |> Jason.encode!())
{:error, msg} ->
bad_request_reply(conn, msg)
end
@ -103,29 +106,36 @@ def follow(%{assigns: %{user: user}} = conn, params) do
case TwitterAPI.follow(user, params) do
{:ok, user, followed, _activity} ->
render(conn, UserView, "show.json", %{user: followed, for: user})
{:error, msg} -> forbidden_json_reply(conn, msg)
{:error, msg} ->
forbidden_json_reply(conn, msg)
end
end
def block(%{assigns: %{user: user}} = conn, params) do
case TwitterAPI.block(user, params) do
{:ok, user, blocked} ->
render conn, UserView, "show.json", %{user: blocked, for: user}
{:error, msg} -> forbidden_json_reply(conn, msg)
render(conn, UserView, "show.json", %{user: blocked, for: user})
{:error, msg} ->
forbidden_json_reply(conn, msg)
end
end
def unblock(%{assigns: %{user: user}} = conn, params) do
case TwitterAPI.unblock(user, params) do
{:ok, user, blocked} ->
render conn, UserView, "show.json", %{user: blocked, for: user}
{:error, msg} -> forbidden_json_reply(conn, msg)
render(conn, UserView, "show.json", %{user: blocked, for: user})
{:error, msg} ->
forbidden_json_reply(conn, msg)
end
end
def delete_post(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {:ok, delete} <- CommonAPI.delete(id, user) do
json = ActivityRepresenter.to_json(delete, %{user: user, for: user})
conn
|> json_reply(200, json)
end
@ -135,14 +145,16 @@ def unfollow(%{assigns: %{user: user}} = conn, params) do
case TwitterAPI.unfollow(user, params) do
{:ok, user, unfollowed} ->
render(conn, UserView, "show.json", %{user: unfollowed, for: user})
{:error, msg} -> forbidden_json_reply(conn, msg)
{:error, msg} ->
forbidden_json_reply(conn, msg)
end
end
def fetch_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Repo.get(Activity, id),
true <- ActivityPub.visible_for_user?(activity, user) do
render conn, ActivityView, "activity.json", %{activity: activity, for: user}
render(conn, ActivityView, "activity.json", %{activity: activity, for: user})
end
end
@ -156,6 +168,7 @@ def fetch_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
def upload(conn, %{"media" => media}) do
response = TwitterAPI.upload(media)
conn
|> put_resp_content_type("application/atom+xml")
|> send_resp(200, response)
@ -163,12 +176,14 @@ def upload(conn, %{"media" => media}) do
def upload_json(conn, %{"media" => media}) do
response = TwitterAPI.upload(media, "json")
conn
|> json_reply(200, response)
end
def get_by_id_or_ap_id(id) do
activity = Repo.get(Activity, id) || Activity.get_create_activity_by_object_ap_id(id)
if activity.data["type"] == "Create" do
activity
else
@ -199,8 +214,8 @@ def register(conn, params) do
render(conn, UserView, "show.json", %{user: user})
else
{:error, errors} ->
conn
|> json_reply(400, Jason.encode!(errors))
conn
|> json_reply(400, Jason.encode!(errors))
end
end
@ -219,8 +234,9 @@ def update_banner(%{assigns: %{user: user}} = conn, params) do
change <- User.info_changeset(user, %{info: new_info}),
{:ok, user} <- User.update_and_set_cache(change) do
CommonAPI.update(user)
%{"url" => [ %{ "href" => href } | _ ]} = object.data
response = %{ url: href } |> Jason.encode!
%{"url" => [%{"href" => href} | _]} = object.data
response = %{url: href} |> Jason.encode!()
conn
|> json_reply(200, response)
end
@ -231,8 +247,9 @@ def update_background(%{assigns: %{user: user}} = conn, params) do
new_info <- Map.put(user.info, "background", object.data),
change <- User.info_changeset(user, %{info: new_info}),
{:ok, _user} <- User.update_and_set_cache(change) do
%{"url" => [ %{ "href" => href } | _ ]} = object.data
response = %{ url: href } |> Jason.encode!
%{"url" => [%{"href" => href} | _]} = object.data
response = %{url: href} |> Jason.encode!()
conn
|> json_reply(200, response)
end
@ -285,9 +302,10 @@ def friends(conn, params) do
def friends_ids(%{assigns: %{user: user}} = conn, _params) do
with {:ok, friends} <- User.get_friends(user) do
ids = friends
|> Enum.map(fn x -> x.id end)
|> Jason.encode!
ids =
friends
|> Enum.map(fn x -> x.id end)
|> Jason.encode!()
json(conn, ids)
else
@ -300,11 +318,12 @@ def empty_array(conn, _params) do
end
def update_profile(%{assigns: %{user: user}} = conn, params) do
params = if bio = params["description"] do
Map.put(params, "bio", bio)
else
params
end
params =
if bio = params["description"] do
Map.put(params, "bio", bio)
else
params
end
with changeset <- User.update_changeset(user, params),
{:ok, user} <- User.update_and_set_cache(changeset) do
@ -339,6 +358,6 @@ defp forbidden_json_reply(conn, error_message) do
end
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

View file

@ -10,7 +10,7 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do
def render("activity.json", %{activity: %{data: %{"type" => "Announce"}} = activity} = opts) do
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"])
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
user = User.get_cached_by_ap_id(activity.data["actor"])
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."
@ -57,20 +59,24 @@ def render("activity.json", %{activity: %{data: %{"type" => "Like"}} = activity}
}
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"])
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
announcement_count = object["announcement_count"] || 0
favorited = opts[:for] && opts[:for].ap_id in (object["likes"] || [])
repeated = opts[:for] && opts[:for].ap_id in (object["announcements"] || [])
attentions = activity.recipients
|> Enum.map(fn (ap_id) -> User.get_cached_by_ap_id(ap_id) end)
|> Enum.filter(&(&1))
|> Enum.map(fn (user) -> UserView.render("show.json", %{user: user, for: opts[:for]}) end)
attentions =
activity.recipients
|> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
|> Enum.filter(& &1)
|> Enum.map(fn user -> UserView.render("show.json", %{user: user, for: opts[:for]}) end)
conversation_id = conversation_id(activity)
@ -81,14 +87,17 @@ def render("activity.json", %{activity: %{data: %{"type" => "Create", "object" =
summary = activity.data["object"]["summary"]
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)
|> Formatter.emojify(object["emoji"])
content =
if !!summary and summary != "" do
"<span>#{activity.data["object"]["summary"]}</span><br />#{content}</span>"
else
content
end
html =
HtmlSanitizeEx.basic_html(content)
|> Formatter.emojify(object["emoji"])
%{
"id" => activity.id,
@ -117,7 +126,8 @@ def render("activity.json", %{activity: %{data: %{"type" => "Create", "object" =
defp conversation_id(activity) do
with context when not is_nil(context) <- activity.data["context"] do
TwitterAPI.context_to_conversation_id(context)
else _e -> nil
else
_e -> nil
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
image = User.avatar_url(user) |> MediaProxy.url()
{following, follows_you, statusnet_blocking} = if assigns[:for] do
{
User.following?(assigns[:for], user),
User.following?(user, assigns[:for]),
User.blocks?(assigns[:for], user)
}
else
{false, false, false}
end
{following, follows_you, statusnet_blocking} =
if assigns[:for] do
{
User.following?(assigns[:for], user),
User.following?(user, assigns[:for]),
User.blocks?(assigns[:for], user)
}
else
{false, false, false}
end
user_info = User.get_cached_user_info(user)
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),
"favourites_count" => 0,
"followers_count" => user_info[:follower_count],
@ -59,9 +61,14 @@ def render("user.json", %{user: user = %User{}} = assigns) do
end
end
def render("short.json", %{user: %User{
nickname: nickname, id: id, ap_id: ap_id, name: name
}}) do
def render("short.json", %{
user: %User{
nickname: nickname,
id: id,
ap_id: ap_id,
name: name
}
}) do
%{
"fullname" => name,
"id" => id,
@ -71,6 +78,6 @@ def render("short.json", %{user: %User{
}
end
defp image_url(%{"url" => [ %{ "href" => href } | _ ]}), do: href
defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
defp image_url(_), do: nil
end

View file

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

View file

@ -26,8 +26,9 @@ def controller do
def view do
quote do
use Phoenix.View, root: "lib/pleroma/web/templates",
namespace: Pleroma.Web
use Phoenix.View,
root: "lib/pleroma/web/templates",
namespace: Pleroma.Web
# Import convenience functions from controllers
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
def base_url do
Pleroma.Web.Endpoint.url
Pleroma.Web.Endpoint.url()
end
end

View file

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

View file

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

View file

@ -26,15 +26,16 @@ def verify(subscription, getter \\ &@httpoison.get/3) do
url = hd(String.split(subscription.callback, "?"))
query = URI.parse(subscription.callback).query || ""
params = Map.merge(params, URI.decode_query(query))
with {:ok, response} <- getter.(url, [], [params: params]),
^challenge <- response.body
do
with {:ok, response} <- getter.(url, [], params: params),
^challenge <- response.body do
changeset = Changeset.change(subscription, %{state: "active"})
Repo.update(changeset)
else e ->
Logger.debug("Couldn't verify subscription")
Logger.debug(inspect(e))
{:error, subscription}
else
e ->
Logger.debug("Couldn't verify subscription")
Logger.debug(inspect(e))
{:error, subscription}
end
end
@ -46,17 +47,24 @@ def verify(subscription, getter \\ &@httpoison.get/3) do
"Undo",
"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.
query = from sub in WebsubServerSubscription,
where: sub.topic == ^topic and sub.state == "active",
where: fragment("? > NOW()", sub.valid_until)
query =
from(
sub in WebsubServerSubscription,
where: sub.topic == ^topic and sub.state == "active",
where: fragment("? > NOW()", sub.valid_until)
)
subscriptions = Repo.all(query)
Enum.each(subscriptions, fn(sub) ->
response = user
|> FeedRepresenter.to_simple_form([activity], [user])
|> :xmerl.export_simple(:xmerl_xml)
|> to_string
Enum.each(subscriptions, fn sub ->
response =
user
|> FeedRepresenter.to_simple_form([activity], [user])
|> :xmerl.export_simple(:xmerl_xml)
|> to_string
data = %{
xml: response,
@ -64,22 +72,24 @@ def publish(topic, user, %{data: %{"type" => type}} = activity) when type in @su
callback: sub.callback,
secret: sub.secret
}
Pleroma.Web.Federator.enqueue(:publish_single_websub, data)
end)
end
def publish(_,_,_), do: ""
def publish(_, _, _), 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
def incoming_subscription_request(user, %{"hub.mode" => "subscribe"} = params) do
with {:ok, topic} <- valid_topic(params, user),
{:ok, lease_time} <- lease_time(params),
secret <- params["hub.secret"],
callback <- params["hub.callback"]
do
callback <- params["hub.callback"] do
subscription = get_subscription(topic, callback)
data = %{
state: subscription.state || "requested",
topic: topic,
@ -90,18 +100,20 @@ def incoming_subscription_request(user, %{"hub.mode" => "subscribe"} = params) d
change = Changeset.change(subscription, data)
websub = Repo.insert_or_update!(change)
change = Changeset.change(websub, %{valid_until:
NaiveDateTime.add(websub.updated_at, lease_time)})
change =
Changeset.change(websub, %{valid_until: NaiveDateTime.add(websub.updated_at, lease_time)})
websub = Repo.update!(change)
Pleroma.Web.Federator.enqueue(:verify_websub, websub)
{:ok, websub}
else {:error, reason} ->
Logger.debug("Couldn't create subscription")
Logger.debug(inspect(reason))
else
{:error, reason} ->
Logger.debug("Couldn't create subscription")
Logger.debug(inspect(reason))
{:error, reason}
{:error, reason}
end
end
@ -112,7 +124,8 @@ defp get_subscription(topic, callback) do
# Temp hack for mastodon.
defp lease_time(%{"hub.lease_seconds" => ""}) do
{:ok, 60 * 60 * 24 * 3} # three days
# three days
{:ok, 60 * 60 * 24 * 3}
end
defp lease_time(%{"hub.lease_seconds" => lease_seconds}) do
@ -120,7 +133,8 @@ defp lease_time(%{"hub.lease_seconds" => lease_seconds}) do
end
defp lease_time(_) do
{:ok, 60 * 60 * 24 * 3} # three days
# three days
{:ok, 60 * 60 * 24 * 3}
end
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
topic = subscribed.info["topic"]
# FIXME: Race condition, use transactions
{:ok, subscription} = with subscription when not is_nil(subscription) <- Repo.get_by(WebsubClientSubscription, topic: topic) do
subscribers = [subscriber.ap_id | subscription.subscribers] |> Enum.uniq
change = Ecto.Changeset.change(subscription, %{subscribers: subscribers})
Repo.update(change)
else _e ->
subscription = %WebsubClientSubscription{
topic: topic,
hub: subscribed.info["hub"],
subscribers: [subscriber.ap_id],
state: "requested",
secret: :crypto.strong_rand_bytes(8) |> Base.url_encode64,
user: subscribed
}
Repo.insert(subscription)
end
{:ok, subscription} =
with subscription when not is_nil(subscription) <-
Repo.get_by(WebsubClientSubscription, topic: topic) do
subscribers = [subscriber.ap_id | subscription.subscribers] |> Enum.uniq()
change = Ecto.Changeset.change(subscription, %{subscribers: subscribers})
Repo.update(change)
else
_e ->
subscription = %WebsubClientSubscription{
topic: topic,
hub: subscribed.info["hub"],
subscribers: [subscriber.ap_id],
state: "requested",
secret: :crypto.strong_rand_bytes(8) |> Base.url_encode64(),
user: subscribed
}
Repo.insert(subscription)
end
requester.(subscription)
end
@ -159,24 +178,25 @@ def gather_feed_data(topic, getter \\ &@httpoison.get/1) do
doc <- XML.parse_document(body),
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
name = XML.string_from_xpath("/feed/author[1]/name", doc)
preferredUsername = XML.string_from_xpath("/feed/author[1]/poco:preferredUsername", doc)
displayName = XML.string_from_xpath("/feed/author[1]/poco:displayName", doc)
avatar = OStatus.make_avatar_object(doc)
bio = XML.string_from_xpath("/feed/author[1]/summary", doc)
{:ok, %{
"uri" => uri,
"hub" => hub,
"nickname" => preferredUsername || name,
"name" => displayName || name,
"host" => URI.parse(uri).host,
"avatar" => avatar,
"bio" => bio
}}
else e ->
{:error, e}
{:ok,
%{
"uri" => uri,
"hub" => hub,
"nickname" => preferredUsername || name,
"name" => displayName || name,
"host" => URI.parse(uri).host,
"avatar" => avatar,
"bio" => bio
}}
else
e ->
{:error, e}
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
websub_checker = fn ->
helper = fn (helper) ->
helper = fn helper ->
:timer.sleep(1000)
websub = Repo.get_by(WebsubClientSubscription, id: websub.id, state: "accepted")
if websub, do: websub, else: helper.(helper)
end
helper.(helper)
end
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}
else e ->
Task.shutdown(task)
else
e ->
Task.shutdown(task)
change = Ecto.Changeset.change(websub, %{state: "rejected"})
{:ok, websub} = Repo.update(change)
change = Ecto.Changeset.change(websub, %{state: "rejected"})
{:ok, websub} = Repo.update(change)
Logger.debug(fn -> "Couldn't confirm subscription: #{inspect(websub)}" end)
Logger.debug(fn -> "error: #{inspect(e)}" end)
Logger.debug(fn -> "Couldn't confirm subscription: #{inspect(websub)}" end)
Logger.debug(fn -> "error: #{inspect(e)}" end)
{:error, websub}
{:error, websub}
end
end
def refresh_subscriptions(delta \\ 60 * 60 * 24) do
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,
where: sub.valid_until < ^cut_off
query = from(sub in WebsubClientSubscription, where: sub.valid_until < ^cut_off)
subs = Repo.all(query)
Enum.each(subs, fn (sub) ->
Enum.each(subs, fn sub ->
Pleroma.Web.Federator.enqueue(:request_subscription, sub)
end)
end

View file

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

View file

@ -8,36 +8,49 @@ defmodule Pleroma.Web.Websub.WebsubController do
def websub_subscription_request(conn, %{"nickname" => nickname} = params) do
user = User.get_cached_by_nickname(nickname)
with {:ok, _websub} <- Websub.incoming_subscription_request(user, params)
do
with {:ok, _websub} <- Websub.incoming_subscription_request(user, params) do
conn
|> send_resp(202, "Accepted")
else {:error, reason} ->
conn
|> send_resp(500, reason)
else
{:error, reason} ->
conn
|> send_resp(500, reason)
end
end
# 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(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
valid_until = NaiveDateTime.add(NaiveDateTime.utc_now, lease_seconds)
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})
{:ok, _websub} = Repo.update(change)
conn
|> send_resp(200, challenge)
else _e ->
conn
|> send_resp(500, "Error")
else
_e ->
conn
|> send_resp(500, "Error")
end
end
@ -48,12 +61,15 @@ def websub_incoming(conn, %{"id" => id}) do
{:ok, body, _conn} = read_body(conn),
^signature <- Websub.sign(websub.secret, body) do
Federator.enqueue(:incoming_doc, body)
conn
|> send_resp(200, "OK")
else _e ->
Logger.debug("Can't handle incoming subscription post")
conn
|> send_resp(500, "Error")
else
_e ->
Logger.debug("Can't handle incoming subscription post")
conn
|> send_resp(500, "Error")
end
end
end

View file

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

View file

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

View file

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

View file

@ -23,7 +23,7 @@ def to_xml(content) when is_list(content) do
for element <- content do
to_xml(element)
end
|> Enum.join
|> Enum.join()
end
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)
defp make_open_tag(tag, attributes) do
attributes_string = for {attribute, value} <- attributes do
"#{attribute}=\"#{value}\""
end |> Enum.join(" ")
attributes_string =
for {attribute, value} <- attributes do
"#{attribute}=\"#{value}\""
end
|> Enum.join(" ")
[tag, attributes_string] |> Enum.join(" ") |> String.trim
[tag, attributes_string] |> Enum.join(" ") |> String.trim()
end
end

65
mix.exs
View file

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

View file

@ -7,44 +7,56 @@ defmodule Pleroma.FormatterTest do
describe ".add_hashtag_links" do
test "turns hashtags into links" do
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)
assert expected_text == Formatter.add_hashtag_links({[], text}, tags) |> Formatter.finalize
assert expected_text ==
Formatter.add_hashtag_links({[], text}, tags) |> Formatter.finalize()
end
end
describe ".add_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."
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"
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"
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/"
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"
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"
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
@ -60,9 +72,14 @@ test "gives a replacement for user links" do
{subs, text} = Formatter.add_user_links({[], text}, mentions)
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})
end
@ -71,6 +88,7 @@ test "gives a replacement for user links" do
describe ".parse_tags" do
test "parses tags in the text" do
text = "Here's a #Test. Maybe these are #working or not. What about #漢字? And #は。"
expected = [
{"#Test", "test"},
{"#working", "working"},
@ -92,7 +110,7 @@ test "it can parse mentions and return the relevant users" do
expected_result = [
{"@gsimg", gsimg},
{"@archaeme", archaeme},
{"@archaeme@archae.me", archaeme_remote},
{"@archaeme@archae.me", archaeme_remote}
]
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
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
end

View file

@ -10,7 +10,10 @@ test "notifies someone when they are directly addressed" do
other_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)
@ -37,7 +40,9 @@ test "it gets a notification that belongs to the user" do
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.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)
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)
{:error, _notification} = Notification.get(user, notification.id)
end
@ -59,7 +66,9 @@ test "it dismisses a notification that belongs to the user" do
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.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)
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)
{:error, _notification} = Notification.dismiss(user, notification.id)
end
@ -82,9 +93,18 @@ test "it clears all notifications belonging to the user" do
other_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, 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)
Notification.clear(other_user)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -4,20 +4,37 @@ defmodule Pleroma.UploadTest do
describe "Storing a file" 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)
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
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)
assert hd(data["url"])["mediaType"] == "image/jpeg"
end
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)
assert hd(data["url"])["mediaType"] == "image/png"
end

View file

@ -10,15 +10,15 @@ defmodule Pleroma.UserTest do
import Ecto.Query
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)
end
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"
@ -67,7 +67,7 @@ test "unfollow takes a user and another user" do
followed = insert(:user)
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)
@ -83,7 +83,6 @@ test "unfollow doesn't unfollow yourself" do
assert user.following == [user.ap_id]
end
test "test if a user is following another user" do
followed = insert(:user)
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
@full_user_data
|> Map.keys
|> Enum.each(fn (key) ->
|> Map.keys()
|> Enum.each(fn key ->
params = Map.delete(@full_user_data, key)
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
@ -120,7 +119,11 @@ test "it sets the password_hash, ap_id and following fields" do
assert is_binary(changeset.changes[:password_hash])
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"
end
end
@ -158,12 +161,24 @@ test "returns nil for nonexistant local user" do
test "returns an ap_id for a user" do
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
test "returns an ap_followers link for a user" do
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
describe "remote user creation changeset" do
@ -184,7 +199,8 @@ test "it confirms validity" do
test "it sets the follower_adress" do
cs = User.remote_user_creation(@valid_remote)
# 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
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
[:name, :nickname, :ap_id]
|> Enum.each(fn (field) ->
|> Enum.each(fn field ->
cs = User.remote_user_creation(Map.delete(@valid_remote, field))
refute cs.valid?
end)
@ -204,7 +220,7 @@ test "it has required fields" do
test "it restricts some sizes" do
[bio: 5000, name: 100]
|> Enum.each(fn ({field, size}) ->
|> Enum.each(fn {field, size} ->
string = String.pad_leading(".", size)
cs = User.remote_user_creation(Map.put(@valid_remote, field, string))
assert cs.valid?
@ -323,7 +339,11 @@ test "get recipients from activity" do
user_two = insert(:user, local: false)
addressed = insert(:user, local: true)
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)
@ -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
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)
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
user = insert(:user)
conn = conn
|> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}")
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}")
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
test "it returns a json representation of the object", %{conn: conn} do
note = insert(:note)
uuid = String.split(note.data["id"], "/") |> List.last
uuid = String.split(note.data["id"], "/") |> List.last()
conn = conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
end
@ -34,12 +36,13 @@ test "it returns a json representation of the object", %{conn: conn} do
describe "/users/:nickname/inbox" 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
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
conn =
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", data)
assert "ok" == json_response(conn, 200)
:timer.sleep(500)

View file

@ -22,7 +22,7 @@ test "it returns a user" do
describe "insertion" do
test "returns the activity if one with the same id is already in" do
activity = insert(:note_activity)
{:ok, new_activity}= ActivityPub.insert(activity.data)
{:ok, new_activity} = ActivityPub.insert(activity.data)
assert activity == new_activity
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"])
given_id = "bla"
data = %{
"ok" => true,
"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
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.actor == "1"
assert activity.recipients == ["user1", "user2"]
@ -124,11 +132,11 @@ test "doesn't return blocked activities" do
describe "public fetch 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 Enum.at(activities, 0) == public
end
@ -137,7 +145,7 @@ test "retrieves a maximum of 20 activities" do
activities = ActivityBuilder.insert_list(30)
last_expected = List.last(activities)
activities = ActivityPub.fetch_public_activities
activities = ActivityPub.fetch_public_activities()
last = List.last(activities)
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)
assert object.data["announcement_count"] == 1
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["actor"] == user.ap_id
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
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)
assert object.data["name"] == "an_image.jpg"
@ -268,11 +285,14 @@ test "fetches the latest Follow activity" do
describe "fetching 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.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 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.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
end
@ -344,7 +365,14 @@ test "it creates an update activity with the new user data" do
user = insert(:user)
{:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(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["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
activity = insert(:note_activity)
data = File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!
|> Map.put("object", activity.data["object"])
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"])
{:ok, returned_activity} = Transmogrifier.handle_incoming(data)
@ -26,51 +27,72 @@ test "it ignores an incoming notice if we already have it" do
end
test "it fetches replied-to activities if we don't have them" do
data = File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
object = data["object"]
|> Map.put("inReplyTo", "https://shitposter.club/notice/2827873")
object =
data["object"]
|> Map.put("inReplyTo", "https://shitposter.club/notice/2827873")
data = data
|> Map.put("object", object)
data =
data
|> Map.put("object", object)
{: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 returned_activity.data["object"]["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
assert activity =
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
end
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)
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["cc"] == [
"http://mastodon.example.org/users/admin/followers",
"http://localtesting.pleroma.lol/users/lain"
]
"http://mastodon.example.org/users/admin/followers",
"http://localtesting.pleroma.lol/users/lain"
]
assert data["actor"] == "http://mastodon.example.org/users/admin"
object = data["object"]
assert object["id"] == "http://mastodon.example.org/users/admin/statuses/99512778738411822"
assert object["to"] == ["https://www.w3.org/ns/activitystreams#Public"]
assert object["cc"] == [
"http://mastodon.example.org/users/admin/followers",
"http://localtesting.pleroma.lol/users/lain"
]
"http://mastodon.example.org/users/admin/followers",
"http://localtesting.pleroma.lol/users/lain"
]
assert object["actor"] == "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
end
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)
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
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)
@ -93,8 +117,9 @@ test "it works for incoming likes" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
data = File.read!("test/fixtures/mastodon-like.json") |> Poison.decode!
|> Map.put("object", activity.data["object"]["id"])
data =
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)
@ -105,14 +130,18 @@ test "it works for incoming likes" do
end
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)
assert data["actor"] == "http://mastodon.example.org/users/admin"
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"])
end
@ -121,53 +150,77 @@ test "it works for incoming announces with an existing activity" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
data = File.read!("test/fixtures/mastodon-announce.json")
|> Poison.decode!
|> Map.put("object", activity.data["object"]["id"])
data =
File.read!("test/fixtures/mastodon-announce.json")
|> Poison.decode!()
|> Map.put("object", activity.data["object"]["id"])
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
assert data["actor"] == "http://mastodon.example.org/users/admin"
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 Activity.get_create_activity_by_object_ap_id(data["object"]).id == activity.id
end
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)
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 = File.read!("test/fixtures/mastodon-update.json") |> Poison.decode!()
update_data = update_data
|> Map.put("actor", data["actor"])
|> Map.put("object", object)
object =
update_data["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)
user = User.get_cached_by_ap_id(data["actor"])
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>"
end
test "it works for incoming deletes" do
activity = insert(:note_activity)
data = File.read!("test/fixtures/mastodon-delete.json")
|> Poison.decode!
object = data["object"]
|> Map.put("id", activity.data["object"]["id"])
data =
File.read!("test/fixtures/mastodon-delete.json")
|> Poison.decode!()
data = data
|> Map.put("object", object)
|> Map.put("actor", activity.data["actor"])
object =
data["object"]
|> 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)
@ -180,7 +233,8 @@ test "it turns mentions into tags" do
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)
object = modified["object"]
@ -192,7 +246,7 @@ test "it turns mentions into tags" do
}
expected_tag = %{
"href" => Pleroma.Web.Endpoint.url <> "/tags/2hu",
"href" => Pleroma.Web.Endpoint.url() <> "/tags/2hu",
"type" => "Hashtag",
"name" => "#2hu"
}
@ -247,7 +301,9 @@ test "it translates ostatus reply_to IDs to external URLs" do
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)
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
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]})
{: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)
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
unrelated_activity = Repo.get(Activity, unrelated_activity.id)

View file

@ -3,7 +3,8 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
use Pleroma.DataCase
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 = %{
"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])
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

View file

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

View file

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

View file

@ -4,7 +4,15 @@ defmodule Pleroma.Web.OAuth.AuthorizationTest do
import Pleroma.Factory
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)
{:ok, auth} = Authorization.create_authorization(app, user)
@ -16,7 +24,15 @@ test "create an authorization token for a valid app" do
end
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)
{:ok, auth} = Authorization.create_authorization(app, user)
@ -30,7 +46,7 @@ test "use up a token" do
expired_auth = %Authorization{
user_id: user.id,
app_id: app.id,
valid_until: NaiveDateTime.add(NaiveDateTime.utc_now, -10),
valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), -10),
token: "mytoken",
used: false
}

View file

@ -6,7 +6,15 @@ defmodule Pleroma.Web.OAuth.TokenTest do
import Pleroma.Factory
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)
{: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)
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
test "a note activity" do
@ -46,7 +49,7 @@ test "a note activity" do
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)
end
@ -61,7 +64,10 @@ test "a reply note" do
answer = %{answer | data: data}
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"])
@ -86,7 +92,7 @@ test "a reply note" do
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)
end
@ -102,9 +108,11 @@ test "an announce activity" do
note_user = User.get_cached_by_ap_id(note.data["actor"])
note = Repo.get(Activity, note.id)
note_xml = ActivityRepresenter.to_simple_form(note, note_user, true)
|> :xmerl.export_simple_content(:xmerl_xml)
|> to_string
note_xml =
ActivityRepresenter.to_simple_form(note, note_user, true)
|> :xmerl.export_simple_content(:xmerl_xml)
|> to_string
expected = """
<activity:object-type>http://activitystrea.ms/schema/1.0/activity</activity:object-type>
@ -120,13 +128,16 @@ test "an announce activity" do
<activity:object>
#{note_xml}
</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"/>
"""
announce_xml = ActivityRepresenter.to_simple_form(announce, user)
|> :xmerl.export_simple_content(:xmerl_xml)
|> to_string
announce_xml =
ActivityRepresenter.to_simple_form(announce, user)
|> :xmerl.export_simple_content(:xmerl_xml)
|> to_string
assert clean(expected) == clean(announce_xml)
end
@ -139,7 +150,7 @@ test "a like activity" do
tuple = ActivityRepresenter.to_simple_form(like, user)
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 = """
<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 rel="self" type="application/atom+xml" href="#{like.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"/>
"""
@ -166,18 +179,20 @@ test "a like activity" do
test "a follow activity" do
follower = insert(:user)
followed = insert(:user)
{:ok, activity} = ActivityPub.insert(%{
"type" => "Follow",
"actor" => follower.ap_id,
"object" => followed.ap_id,
"to" => [followed.ap_id]
})
{:ok, activity} =
ActivityPub.insert(%{
"type" => "Follow",
"actor" => follower.ap_id,
"object" => followed.ap_id,
"to" => [followed.ap_id]
})
tuple = ActivityRepresenter.to_simple_form(activity, follower)
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 = """
<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>
</activity:object>
<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)
@ -209,7 +226,7 @@ test "an unfollow activity" do
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 = """
<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>
</activity:object>
<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)
@ -233,13 +252,22 @@ test "an unfollow activity" do
test "a delete" do
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)
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 = """
<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])
most_recent_update = note_activity.updated_at
|> NaiveDateTime.to_iso8601
most_recent_update =
note_activity.updated_at
|> NaiveDateTime.to_iso8601()
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)
|> :xmerl.export_simple_content(:xmerl_xml)
user_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 = """
<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>
</feed>
"""
assert clean(res) == clean(expected)
end

View file

@ -14,8 +14,13 @@ test "it removes the mentioned activity" do
{:ok, like, _object} = Pleroma.Web.ActivityPub.ActivityPub.like(user, object)
incoming = File.read!("test/fixtures/delete.xml")
|> String.replace("tag:mastodon.sdf.org,2017-06-10:objectId=310513:objectType=Status", note.data["object"]["id"])
incoming =
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)
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
user = insert(:user)
salmon = File.read!("test/fixtures/salmon.xml")
conn = conn
|> put_req_header("content-type", "application/atom+xml")
|> post("/users/#{user.nickname}/salmon", salmon)
conn =
conn
|> put_req_header("content-type", "application/atom+xml")
|> post("/users/#{user.nickname}/salmon", salmon)
assert response(conn, 200)
end
@ -17,21 +19,30 @@ test "decodes a salmon", %{conn: conn} do
test "decodes a salmon with a changed magic key", %{conn: conn} do
user = insert(:user)
salmon = File.read!("test/fixtures/salmon.xml")
conn = conn
|> put_req_header("content-type", "application/atom+xml")
|> post("/users/#{user.nickname}/salmon", salmon)
conn =
conn
|> put_req_header("content-type", "application/atom+xml")
|> post("/users/#{user.nickname}/salmon", salmon)
assert response(conn, 200)
# 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")
info = salmon_user.info
|> Map.put("magic_key", "RSA.pu0s-halox4tu7wmES1FVSx6u-4wc0YrUFXcqWXZG4-27UmbCOpMQftRCldNRfyA-qLbz-eqiwrong1EwUvjsD4cYbAHNGHwTvDOyx5AKthQUP44ykPv7kjKGh3DWKySJvcs9tlUG87hlo7AvnMo9pwRS_Zz2CacQ-MKaXyDepk=.AQAB") # Wrong key
# 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}))
conn = build_conn()
|> put_req_header("content-type", "application/atom+xml")
|> post("/users/#{user.nickname}/salmon", salmon)
conn =
build_conn()
|> put_req_header("content-type", "application/atom+xml")
|> post("/users/#{user.nickname}/salmon", salmon)
assert response(conn, 200)
end
@ -40,8 +51,9 @@ test "gets a feed", %{conn: conn} do
note_activity = insert(:note_activity)
user = User.get_cached_by_ap_id(note_activity.data["actor"])
conn = conn
|> get("/users/#{user.nickname}/feed.atom")
conn =
conn
|> get("/users/#{user.nickname}/feed.atom")
assert response(conn, 200) =~ note_activity.data["object"]["content"]
end
@ -49,27 +61,30 @@ test "gets a feed", %{conn: conn} do
test "gets an object", %{conn: conn} do
note_activity = insert(:note_activity)
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}"
conn = conn
|> get(url)
conn =
conn
|> get(url)
expected = ActivityRepresenter.to_simple_form(note_activity, user, true)
|> ActivityRepresenter.wrap_with_entry
|> :xmerl.export_simple(:xmerl_xml)
|> to_string
expected =
ActivityRepresenter.to_simple_form(note_activity, user, true)
|> ActivityRepresenter.wrap_with_entry()
|> :xmerl.export_simple(:xmerl_xml)
|> to_string
assert response(conn, 200) == expected
end
test "gets an activity", %{conn: conn} do
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}"
conn = conn
|> get(url)
conn =
conn
|> get(url)
assert response(conn, 200)
end
@ -78,8 +93,9 @@ test "gets a notice", %{conn: conn} do
note_activity = insert(:note_activity)
url = "/notice/#{note_activity.id}"
conn = conn
|> get(url)
conn =
conn
|> get(url)
assert response(conn, 200)
end

View file

@ -20,12 +20,18 @@ test "handle incoming note - GS, Salmon" do
assert user.info["note_count"] == 1
assert activity.data["type"] == "Create"
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["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 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
end
@ -65,10 +71,12 @@ test "handle incoming notes with tags" do
test "handle incoming notes - Mastodon, salmon, reply" do
# It uses the context of the replied to object
Repo.insert!(%Object{
data: %{
"id" => "https://pleroma.soykaf.com/objects/c237d966-ac75-4fe3-a87a-d89d71a3a7a4",
"context" => "2hu"
}})
data: %{
"id" => "https://pleroma.soykaf.com/objects/c237d966-ac75-4fe3-a87a-d89d71a3a7a4",
"context" => "2hu"
}
})
incoming = File.read!("test/fixtures/incoming_reply_mastodon.xml")
{: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["object"]["type"] == "Note"
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"]
end
@ -141,9 +154,11 @@ test "handle incoming retweets - GS, subscription - local message" do
incoming = File.read!("test/fixtures/share-gs-local.xml")
note_activity = insert(:note_activity)
user = User.get_cached_by_ap_id(note_activity.data["actor"])
incoming = incoming
|> String.replace("LOCAL_ID", note_activity.data["object"]["id"])
|> String.replace("LOCAL_USER", user.ap_id)
incoming =
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)
@ -168,7 +183,9 @@ test "handle incoming retweets - Mastodon, salmon" do
assert activity.data["type"] == "Announce"
assert activity.data["actor"] == "https://mastodon.social/users/lambadalambda"
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
assert retweeted_activity.data["type"] == "Create"
@ -178,35 +195,42 @@ test "handle incoming retweets - Mastodon, salmon" do
end
test "handle incoming favorites - GS, websub" do
capture_log fn ->
capture_log(fn ->
incoming = File.read!("test/fixtures/favorite.xml")
{:ok, [[activity, favorited_activity]]} = OStatus.handle_incoming(incoming)
assert activity.data["type"] == "Like"
assert activity.data["actor"] == "https://social.heldscal.la/user/23211"
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
assert favorited_activity.data["type"] == "Create"
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
end
end)
end
test "handle conversation references" do
incoming = File.read!("test/fixtures/mastodon_conversation.xml")
{: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
test "handle incoming favorites with locally available object - GS, websub" do
note_activity = insert(:note_activity)
incoming = File.read!("test/fixtures/favorite_with_local_note.xml")
|> String.replace("localid", note_activity.data["object"]["id"])
incoming =
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)
@ -224,9 +248,15 @@ test "handle incoming replies" do
assert activity.data["type"] == "Create"
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 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"]
end
@ -234,7 +264,10 @@ test "handle incoming follows" do
incoming = File.read!("test/fixtures/follow.xml")
{:ok, [activity]} = OStatus.handle_incoming(incoming)
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["object"] == "https://pawoo.net/users/pekorino"
refute activity.local
@ -304,7 +337,8 @@ test "it returns user info in a hash" do
expected = %{
"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",
"nickname" => "shp",
"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",
"fqn" => user,
"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}",
"ap_id" => nil
}
assert data == expected
end
@ -329,7 +373,8 @@ test "it works with the uri" do
expected = %{
"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",
"nickname" => "shp",
"salmon" => "https://social.heldscal.la/main/salmon/user/29191",
@ -339,28 +384,40 @@ test "it works with the uri" do
"host" => "social.heldscal.la",
"fqn" => user,
"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}",
"ap_id" => nil
}
assert data == expected
end
end
describe "fetching a status by it's 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"
{: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["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
test "it works for atom notes, too" do
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["object"]["id"] == url
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")
{: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

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