aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/mix/tasks/relay_follow.ex15
-rw-r--r--lib/mix/tasks/relay_unfollow.ex15
-rw-r--r--lib/mix/tasks/sample_config.eex37
-rw-r--r--lib/pleroma/formatter.ex39
-rw-r--r--lib/pleroma/http/http.ex25
-rw-r--r--lib/pleroma/upload.ex86
-rw-r--r--lib/pleroma/uploaders/local.ex47
-rw-r--r--lib/pleroma/uploaders/s3.ex24
-rw-r--r--lib/pleroma/uploaders/swift/keystone.ex48
-rw-r--r--lib/pleroma/uploaders/swift/swift.ex28
-rw-r--r--lib/pleroma/uploaders/swift/uploader.ex10
-rw-r--r--lib/pleroma/uploaders/uploader.ex20
-rw-r--r--lib/pleroma/user.ex66
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub.ex70
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub_controller.ex12
-rw-r--r--lib/pleroma/web/activity_pub/relay.ex44
-rw-r--r--lib/pleroma/web/activity_pub/transmogrifier.ex50
-rw-r--r--lib/pleroma/web/activity_pub/utils.ex47
-rw-r--r--lib/pleroma/web/activity_pub/views/user_view.ex32
-rw-r--r--lib/pleroma/web/common_api/common_api.ex23
-rw-r--r--lib/pleroma/web/common_api/utils.ex17
-rw-r--r--lib/pleroma/web/endpoint.ex2
-rw-r--r--lib/pleroma/web/federator/federator.ex6
-rw-r--r--lib/pleroma/web/mastodon_api/mastodon_api_controller.ex84
-rw-r--r--lib/pleroma/web/mastodon_api/views/account_view.ex14
-rw-r--r--lib/pleroma/web/mastodon_api/views/status_view.ex22
-rw-r--r--lib/pleroma/web/ostatus/activity_representer.ex5
-rw-r--r--lib/pleroma/web/router.ex27
-rw-r--r--lib/pleroma/web/streamer.ex21
-rw-r--r--lib/pleroma/web/templates/mastodon_api/mastodon/index.html.eex2
-rw-r--r--lib/pleroma/web/twitter_api/controllers/util_controller.ex56
-rw-r--r--lib/pleroma/web/twitter_api/representers/activity_representer.ex11
-rw-r--r--lib/pleroma/web/twitter_api/representers/object_representer.ex10
-rw-r--r--lib/pleroma/web/twitter_api/twitter_api_controller.ex16
-rw-r--r--lib/pleroma/web/twitter_api/views/user_view.ex7
35 files changed, 844 insertions, 194 deletions
diff --git a/lib/mix/tasks/relay_follow.ex b/lib/mix/tasks/relay_follow.ex
new file mode 100644
index 000000000..ac6f20924
--- /dev/null
+++ b/lib/mix/tasks/relay_follow.ex
@@ -0,0 +1,15 @@
+defmodule Mix.Tasks.RelayFollow do
+ use Mix.Task
+ require Logger
+ alias Pleroma.Web.ActivityPub.Relay
+
+ @shortdoc "Follows a remote relay"
+ def run([target]) do
+ Mix.Task.run("app.start")
+
+ :ok = Relay.follow(target)
+
+ # put this task to sleep to allow the genserver to push out the messages
+ :timer.sleep(500)
+ end
+end
diff --git a/lib/mix/tasks/relay_unfollow.ex b/lib/mix/tasks/relay_unfollow.ex
new file mode 100644
index 000000000..4621ace83
--- /dev/null
+++ b/lib/mix/tasks/relay_unfollow.ex
@@ -0,0 +1,15 @@
+defmodule Mix.Tasks.RelayUnfollow do
+ use Mix.Task
+ require Logger
+ alias Pleroma.Web.ActivityPub.Relay
+
+ @shortdoc "Follows a remote relay"
+ def run([target]) do
+ Mix.Task.run("app.start")
+
+ :ok = Relay.unfollow(target)
+
+ # put this task to sleep to allow the genserver to push out the messages
+ :timer.sleep(500)
+ end
+end
diff --git a/lib/mix/tasks/sample_config.eex b/lib/mix/tasks/sample_config.eex
index 6db36fa09..3881ead26 100644
--- a/lib/mix/tasks/sample_config.eex
+++ b/lib/mix/tasks/sample_config.eex
@@ -24,3 +24,40 @@ config :pleroma, Pleroma.Repo,
database: "pleroma_dev",
hostname: "localhost",
pool_size: 10
+
+# Configure S3 support if desired.
+# The public S3 endpoint is different depending on region and provider,
+# consult your S3 provider's documentation for details on what to use.
+#
+# config :pleroma, Pleroma.Uploaders.S3,
+# bucket: "some-bucket",
+# public_endpoint: "https://s3.amazonaws.com"
+#
+# Configure S3 credentials:
+# config :ex_aws, :s3,
+# access_key_id: "xxxxxxxxxxxxx",
+# secret_access_key: "yyyyyyyyyyyy",
+# region: "us-east-1",
+# scheme: "https://"
+#
+# For using third-party S3 clones like wasabi, also do:
+# config :ex_aws, :s3,
+# host: "s3.wasabisys.com"
+
+
+# Configure Openstack Swift support if desired.
+#
+# Many openstack deployments are different, so config is left very open with
+# no assumptions made on which provider you're using. This should allow very
+# wide support without needing separate handlers for OVH, Rackspace, etc.
+#
+# config :pleroma, Pleroma.Uploaders.Swift,
+# container: "some-container",
+# username: "api-username-yyyy",
+# password: "api-key-xxxx",
+# tenant_id: "<openstack-project/tenant-id>",
+# auth_url: "https://keystone-endpoint.provider.com",
+# storage_url: "https://swift-endpoint.prodider.com/v1/AUTH_<tenant>/<container>",
+# object_url: "https://cdn-endpoint.provider.com/<container>"
+#
+
diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex
index d199c9243..9be54e863 100644
--- a/lib/pleroma/formatter.ex
+++ b/lib/pleroma/formatter.ex
@@ -16,7 +16,7 @@ defmodule Pleroma.Formatter do
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
+ ~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()
@@ -154,6 +154,7 @@ defmodule Pleroma.Formatter do
MediaProxy.url(file)
}' />"
)
+ |> HtmlSanitizeEx.basic_html()
end)
end
@@ -165,8 +166,29 @@ defmodule Pleroma.Formatter do
@emoji
end
- @link_regex ~r/https?:\/\/[\w\.\/?=\-#\+%&@~'\(\):]+[\w\/]/u
+ @link_regex ~r/[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+/ui
+
+ # IANA got a list https://www.iana.org/assignments/uri-schemes/ but
+ # Stuff like ipfs isn’t in it
+ # There is very niche stuff
+ @uri_schemes [
+ "https://",
+ "http://",
+ "dat://",
+ "dweb://",
+ "gopher://",
+ "ipfs://",
+ "ipns://",
+ "irc:",
+ "ircs:",
+ "magnet:",
+ "mailto:",
+ "mumble:",
+ "ssb://",
+ "xmpp:"
+ ]
+ # TODO: make it use something other than @link_regex
def html_escape(text) do
Regex.split(@link_regex, text, include_captures: true)
|> Enum.map_every(2, fn chunk ->
@@ -176,11 +198,18 @@ defmodule Pleroma.Formatter do
|> Enum.join("")
end
- @doc "changes http:... links to html links"
+ @doc "changes scheme:... urls to html links"
def add_links({subs, text}) do
+ additionnal_schemes =
+ Application.get_env(:pleroma, :uri_schemes, [])
+ |> Keyword.get(:additionnal_schemes, [])
+
links =
- Regex.scan(@link_regex, text)
- |> Enum.map(fn [url] -> {Ecto.UUID.generate(), url} end)
+ text
+ |> String.split([" ", "\t", "<br>"])
+ |> Enum.filter(fn word -> String.starts_with?(word, @uri_schemes ++ additionnal_schemes) end)
+ |> Enum.filter(fn word -> Regex.match?(@link_regex, word) end)
+ |> Enum.map(fn url -> {Ecto.UUID.generate(), url} end)
|> Enum.sort_by(fn {_, url} -> -String.length(url) end)
uuid_text =
diff --git a/lib/pleroma/http/http.ex b/lib/pleroma/http/http.ex
index 84f34eb4a..c19bccf60 100644
--- a/lib/pleroma/http/http.ex
+++ b/lib/pleroma/http/http.ex
@@ -1,5 +1,23 @@
defmodule Pleroma.HTTP do
- use HTTPoison.Base
+ require HTTPoison
+
+ def request(method, url, body \\ "", headers \\ [], options \\ []) do
+ options =
+ process_request_options(options)
+ |> process_sni_options(url)
+
+ HTTPoison.request(method, url, body, headers, options)
+ end
+
+ defp process_sni_options(options, url) do
+ uri = URI.parse(url)
+ host = uri.host |> to_charlist()
+
+ case uri.scheme do
+ "https" -> options ++ [ssl: [server_name_indication: host]]
+ _ -> options
+ end
+ end
def process_request_options(options) do
config = Application.get_env(:pleroma, :http, [])
@@ -10,4 +28,9 @@ defmodule Pleroma.HTTP do
_ -> options ++ [proxy: proxy]
end
end
+
+ def get(url, headers \\ [], options \\ []), do: request(:get, url, "", headers, options)
+
+ def post(url, body, headers \\ [], options \\ []),
+ do: request(:post, url, body, headers, options)
end
diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex
index e0cb545b0..f188a5f32 100644
--- a/lib/pleroma/upload.ex
+++ b/lib/pleroma/upload.ex
@@ -1,24 +1,19 @@
defmodule Pleroma.Upload do
alias Ecto.UUID
- alias Pleroma.Web
+
+ @storage_backend Application.get_env(:pleroma, Pleroma.Upload)
+ |> Keyword.fetch!(:uploader)
def store(%Plug.Upload{} = file, should_dedupe) do
content_type = get_content_type(file.path)
+
uuid = get_uuid(file, should_dedupe)
name = get_name(file, uuid, content_type, should_dedupe)
- upload_folder = get_upload_path(uuid, should_dedupe)
- url_path = get_url(name, uuid, should_dedupe)
-
- File.mkdir_p!(upload_folder)
- result_file = Path.join(upload_folder, name)
- if File.exists?(result_file) do
- File.rm!(file.path)
- else
- File.cp!(file.path, result_file)
- end
+ strip_exif_data(content_type, file.path)
- strip_exif_data(content_type, result_file)
+ {:ok, url_path} =
+ @storage_backend.put_file(name, uuid, file.path, content_type, should_dedupe)
%{
"type" => "Document",
@@ -36,15 +31,13 @@ defmodule Pleroma.Upload do
def store(%{"img" => "data:image/" <> image_data}, should_dedupe) do
parsed = Regex.named_captures(~r/(?<filetype>jpeg|png|gif);base64,(?<data>.*)/, image_data)
data = Base.decode64!(parsed["data"], ignore: :whitespace)
- uuid = UUID.generate()
- uuidpath = Path.join(upload_path(), uuid)
- uuid = UUID.generate()
- File.mkdir_p!(upload_path())
+ tmp_path = tempfile_for_image(data)
- File.write!(uuidpath, data)
+ uuid = UUID.generate()
- content_type = get_content_type(uuidpath)
+ content_type = get_content_type(tmp_path)
+ strip_exif_data(content_type, tmp_path)
name =
create_name(
@@ -53,23 +46,7 @@ defmodule Pleroma.Upload do
content_type
)
- upload_folder = get_upload_path(uuid, should_dedupe)
- url_path = get_url(name, uuid, should_dedupe)
-
- File.mkdir_p!(upload_folder)
- result_file = Path.join(upload_folder, name)
-
- if should_dedupe do
- if !File.exists?(result_file) do
- File.rename(uuidpath, result_file)
- else
- File.rm!(uuidpath)
- end
- else
- File.rename(uuidpath, result_file)
- end
-
- strip_exif_data(content_type, result_file)
+ {:ok, url_path} = @storage_backend.put_file(name, uuid, tmp_path, content_type, should_dedupe)
%{
"type" => "Image",
@@ -84,21 +61,28 @@ defmodule Pleroma.Upload do
}
end
+ @doc """
+ Creates a tempfile using the Plug.Upload Genserver which cleans them up
+ automatically.
+ """
+ def tempfile_for_image(data) do
+ {:ok, tmp_path} = Plug.Upload.random_file("profile_pics")
+ {:ok, tmp_file} = File.open(tmp_path, [:write, :raw, :binary])
+ IO.binwrite(tmp_file, data)
+
+ tmp_path
+ end
+
def strip_exif_data(content_type, file) do
settings = Application.get_env(:pleroma, Pleroma.Upload)
do_strip = Keyword.fetch!(settings, :strip_exif)
- [filetype, ext] = String.split(content_type, "/")
+ [filetype, _ext] = String.split(content_type, "/")
if filetype == "image" and do_strip == true do
Mogrify.open(file) |> Mogrify.custom("strip") |> Mogrify.save(in_place: true)
end
end
- def upload_path do
- settings = Application.get_env(:pleroma, Pleroma.Upload)
- Keyword.fetch!(settings, :uploads)
- end
-
defp create_name(uuid, ext, type) do
case type do
"application/octet-stream" ->
@@ -142,26 +126,6 @@ defmodule Pleroma.Upload do
end
end
- defp get_upload_path(uuid, should_dedupe) do
- if should_dedupe do
- upload_path()
- else
- Path.join(upload_path(), uuid)
- end
- end
-
- defp get_url(name, uuid, should_dedupe) do
- if should_dedupe do
- url_for(:cow_uri.urlencode(name))
- else
- url_for(Path.join(uuid, :cow_uri.urlencode(name)))
- end
- end
-
- defp url_for(file) do
- "#{Web.base_url()}/media/#{file}"
- end
-
def get_content_type(file) do
match =
File.open(file, [:read], fn f ->
diff --git a/lib/pleroma/uploaders/local.ex b/lib/pleroma/uploaders/local.ex
new file mode 100644
index 000000000..d4624661f
--- /dev/null
+++ b/lib/pleroma/uploaders/local.ex
@@ -0,0 +1,47 @@
+defmodule Pleroma.Uploaders.Local do
+ @behaviour Pleroma.Uploaders.Uploader
+
+ alias Pleroma.Web
+
+ def put_file(name, uuid, tmpfile, _content_type, should_dedupe) do
+ upload_folder = get_upload_path(uuid, should_dedupe)
+ url_path = get_url(name, uuid, should_dedupe)
+
+ File.mkdir_p!(upload_folder)
+
+ result_file = Path.join(upload_folder, name)
+
+ if File.exists?(result_file) do
+ File.rm!(tmpfile)
+ else
+ File.cp!(tmpfile, result_file)
+ end
+
+ {:ok, url_path}
+ end
+
+ def upload_path do
+ settings = Application.get_env(:pleroma, Pleroma.Uploaders.Local)
+ Keyword.fetch!(settings, :uploads)
+ end
+
+ defp get_upload_path(uuid, should_dedupe) do
+ if should_dedupe do
+ upload_path()
+ else
+ Path.join(upload_path(), uuid)
+ end
+ end
+
+ defp get_url(name, uuid, should_dedupe) do
+ if should_dedupe do
+ url_for(:cow_uri.urlencode(name))
+ else
+ url_for(Path.join(uuid, :cow_uri.urlencode(name)))
+ end
+ end
+
+ defp url_for(file) do
+ "#{Web.base_url()}/media/#{file}"
+ end
+end
diff --git a/lib/pleroma/uploaders/s3.ex b/lib/pleroma/uploaders/s3.ex
new file mode 100644
index 000000000..ce0ed3e34
--- /dev/null
+++ b/lib/pleroma/uploaders/s3.ex
@@ -0,0 +1,24 @@
+defmodule Pleroma.Uploaders.S3 do
+ @behaviour Pleroma.Uploaders.Uploader
+
+ def put_file(name, uuid, path, content_type, _should_dedupe) do
+ settings = Application.get_env(:pleroma, Pleroma.Uploaders.S3)
+ bucket = Keyword.fetch!(settings, :bucket)
+ public_endpoint = Keyword.fetch!(settings, :public_endpoint)
+
+ {:ok, file_data} = File.read(path)
+
+ File.rm!(path)
+
+ s3_name = "#{uuid}/#{name}"
+
+ {:ok, _} =
+ ExAws.S3.put_object(bucket, s3_name, file_data, [
+ {:acl, :public_read},
+ {:content_type, content_type}
+ ])
+ |> ExAws.request()
+
+ {:ok, "#{public_endpoint}/#{bucket}/#{s3_name}"}
+ end
+end
diff --git a/lib/pleroma/uploaders/swift/keystone.ex b/lib/pleroma/uploaders/swift/keystone.ex
new file mode 100644
index 000000000..a79214319
--- /dev/null
+++ b/lib/pleroma/uploaders/swift/keystone.ex
@@ -0,0 +1,48 @@
+defmodule Pleroma.Uploaders.Swift.Keystone do
+ use HTTPoison.Base
+
+ @settings Application.get_env(:pleroma, Pleroma.Uploaders.Swift)
+
+ def process_url(url) do
+ Enum.join(
+ [Keyword.fetch!(@settings, :auth_url), url],
+ "/"
+ )
+ end
+
+ def process_response_body(body) do
+ body
+ |> Poison.decode!()
+ end
+
+ def get_token() do
+ username = Keyword.fetch!(@settings, :username)
+ password = Keyword.fetch!(@settings, :password)
+ tenant_id = Keyword.fetch!(@settings, :tenant_id)
+
+ case post(
+ "/tokens",
+ make_auth_body(username, password, tenant_id),
+ ["Content-Type": "application/json"],
+ hackney: [:insecure]
+ ) do
+ {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
+ body["access"]["token"]["id"]
+
+ {:ok, %HTTPoison.Response{status_code: _}} ->
+ ""
+ end
+ end
+
+ def make_auth_body(username, password, tenant) do
+ Poison.encode!(%{
+ :auth => %{
+ :passwordCredentials => %{
+ :username => username,
+ :password => password
+ },
+ :tenantId => tenant
+ }
+ })
+ end
+end
diff --git a/lib/pleroma/uploaders/swift/swift.ex b/lib/pleroma/uploaders/swift/swift.ex
new file mode 100644
index 000000000..819dfebda
--- /dev/null
+++ b/lib/pleroma/uploaders/swift/swift.ex
@@ -0,0 +1,28 @@
+defmodule Pleroma.Uploaders.Swift.Client do
+ use HTTPoison.Base
+
+ @settings Application.get_env(:pleroma, Pleroma.Uploaders.Swift)
+
+ def process_url(url) do
+ Enum.join(
+ [Keyword.fetch!(@settings, :storage_url), url],
+ "/"
+ )
+ end
+
+ def upload_file(filename, body, content_type) do
+ object_url = Keyword.fetch!(@settings, :object_url)
+ token = Pleroma.Uploaders.Swift.Keystone.get_token()
+
+ case put("#{filename}", body, "X-Auth-Token": token, "Content-Type": content_type) do
+ {:ok, %HTTPoison.Response{status_code: 201}} ->
+ {:ok, "#{object_url}/#{filename}"}
+
+ {:ok, %HTTPoison.Response{status_code: 401}} ->
+ {:error, "Unauthorized, Bad Token"}
+
+ {:error, _} ->
+ {:error, "Swift Upload Error"}
+ end
+ end
+end
diff --git a/lib/pleroma/uploaders/swift/uploader.ex b/lib/pleroma/uploaders/swift/uploader.ex
new file mode 100644
index 000000000..794f76cb0
--- /dev/null
+++ b/lib/pleroma/uploaders/swift/uploader.ex
@@ -0,0 +1,10 @@
+defmodule Pleroma.Uploaders.Swift do
+ @behaviour Pleroma.Uploaders.Uploader
+
+ def put_file(name, uuid, tmp_path, content_type, _should_dedupe) do
+ {:ok, file_data} = File.read(tmp_path)
+ remote_name = "#{uuid}/#{name}"
+
+ Pleroma.Uploaders.Swift.Client.upload_file(remote_name, file_data, content_type)
+ end
+end
diff --git a/lib/pleroma/uploaders/uploader.ex b/lib/pleroma/uploaders/uploader.ex
new file mode 100644
index 000000000..b58fc6d71
--- /dev/null
+++ b/lib/pleroma/uploaders/uploader.ex
@@ -0,0 +1,20 @@
+defmodule Pleroma.Uploaders.Uploader do
+ @moduledoc """
+ Defines the contract to put an uploaded file to any backend.
+ """
+
+ @doc """
+ Put a file to the backend.
+
+ Returns `{:ok, String.t } | {:error, String.t} containing the path of the
+ uploaded file, or error information if the file failed to be saved to the
+ respective backend.
+ """
+ @callback put_file(
+ name :: String.t(),
+ uuid :: String.t(),
+ file :: File.t(),
+ content_type :: String.t(),
+ should_dedupe :: Boolean.t()
+ ) :: {:ok, String.t()} | {:error, String.t()}
+end
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index fa0ea171d..64c69b209 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -68,7 +68,8 @@ defmodule Pleroma.User do
following_count: length(user.following) - oneself,
note_count: user.info["note_count"] || 0,
follower_count: user.info["follower_count"] || 0,
- locked: user.info["locked"] || false
+ locked: user.info["locked"] || false,
+ default_scope: user.info["default_scope"] || "public"
}
end
@@ -77,7 +78,7 @@ defmodule Pleroma.User do
changes =
%User{}
|> cast(params, [:bio, :name, :ap_id, :nickname, :info, :avatar])
- |> validate_required([:name, :ap_id, :nickname])
+ |> validate_required([:name, :ap_id])
|> unique_constraint(:nickname)
|> validate_format(:nickname, @email_regex)
|> validate_length(:bio, max: 5000)
@@ -457,13 +458,34 @@ defmodule Pleroma.User do
update_and_set_cache(cs)
end
+ def get_notified_from_activity_query(to) do
+ from(
+ u in User,
+ where: u.ap_id in ^to,
+ where: u.local == true
+ )
+ end
+
+ def get_notified_from_activity(%Activity{recipients: to, data: %{"type" => "Announce"} = data}) do
+ object = Object.normalize(data["object"])
+ actor = User.get_cached_by_ap_id(data["actor"])
+
+ # ensure that the actor who published the announced object appears only once
+ to =
+ if actor.nickname != nil do
+ to ++ [object.data["actor"]]
+ else
+ to
+ end
+ |> Enum.uniq()
+
+ query = get_notified_from_activity_query(to)
+
+ Repo.all(query)
+ 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 = get_notified_from_activity_query(to)
Repo.all(query)
end
@@ -500,7 +522,8 @@ defmodule Pleroma.User do
u.nickname,
u.name
)
- }
+ },
+ where: not is_nil(u.nickname)
)
q =
@@ -579,7 +602,11 @@ defmodule Pleroma.User do
end
def local_user_query() do
- from(u in User, where: u.local == true)
+ from(
+ u in User,
+ where: u.local == true,
+ where: not is_nil(u.nickname)
+ )
end
def deactivate(%User{} = user) do
@@ -638,6 +665,25 @@ defmodule Pleroma.User do
end
end
+ def get_or_create_instance_user do
+ relay_uri = "#{Pleroma.Web.Endpoint.url()}/relay"
+
+ if user = get_by_ap_id(relay_uri) do
+ user
+ else
+ changes =
+ %User{}
+ |> cast(%{}, [:ap_id, :nickname, :local])
+ |> put_change(:ap_id, relay_uri)
+ |> put_change(:nickname, nil)
+ |> put_change(:local, true)
+ |> put_change(:follower_address, relay_uri <> "/followers")
+
+ {:ok, user} = Repo.insert(changes)
+ user
+ end
+ end
+
# AP style
def public_key_from_info(%{
"source_data" => %{"publicKey" => %{"publicKeyPem" => public_key_pem}}
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index ec605b694..81c11dd76 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -12,8 +12,33 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
@instance Application.get_env(:pleroma, :instance)
- def get_recipients(data) do
- (data["to"] || []) ++ (data["cc"] || [])
+ # For Announce activities, we filter the recipients based on following status for any actors
+ # that match actual users. See issue #164 for more information about why this is necessary.
+ defp get_recipients(%{"type" => "Announce"} = data) do
+ to = data["to"] || []
+ cc = data["cc"] || []
+ recipients = to ++ cc
+ actor = User.get_cached_by_ap_id(data["actor"])
+
+ recipients
+ |> Enum.filter(fn recipient ->
+ case User.get_cached_by_ap_id(recipient) do
+ nil ->
+ true
+
+ user ->
+ User.following?(user, actor)
+ end
+ end)
+
+ {recipients, to, cc}
+ end
+
+ defp get_recipients(data) do
+ to = data["to"] || []
+ cc = data["cc"] || []
+ recipients = to ++ cc
+ {recipients, to, cc}
end
defp check_actor_is_active(actor) do
@@ -35,12 +60,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
:ok <- check_actor_is_active(map["actor"]),
{:ok, map} <- MRF.filter(map),
:ok <- insert_full_object(map) do
+ {recipients, _, _} = get_recipients(map)
+
{:ok, activity} =
Repo.insert(%Activity{
data: map,
local: local,
actor: map["actor"],
- recipients: get_recipients(map)
+ recipients: recipients
})
Notification.create_notifications(activity)
@@ -381,6 +408,20 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_tag(query, _), do: query
+ defp restrict_to_cc(query, recipients_to, recipients_cc) do
+ from(
+ activity in query,
+ where:
+ fragment(
+ "(?->'to' \\?| ?) or (?->'cc' \\?| ?)",
+ activity.data,
+ ^recipients_to,
+ activity.data,
+ ^recipients_cc
+ )
+ )
+ end
+
defp restrict_recipients(query, [], _user), do: query
defp restrict_recipients(query, recipients, nil) do
@@ -522,6 +563,13 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> Enum.reverse()
end
+ def fetch_activities_bounded(recipients_to, recipients_cc, opts \\ %{}) do
+ fetch_activities_query([], opts)
+ |> restrict_to_cc(recipients_to, recipients_cc)
+ |> Repo.all()
+ |> Enum.reverse()
+ end
+
def upload(file) do
data = Upload.store(file, Application.get_env(:pleroma, :instance)[:dedupe_media])
Repo.insert(%Object{data: data})
@@ -554,18 +602,29 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
"locked" => locked
},
avatar: avatar,
- nickname: "#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}",
name: data["name"],
follower_address: data["followers"],
bio: data["summary"]
}
+ # nickname can be nil because of virtual actors
+ user_data =
+ if data["preferredUsername"] do
+ Map.put(
+ user_data,
+ :nickname,
+ "#{data["preferredUsername"]}@#{URI.parse(data["id"]).host}"
+ )
+ else
+ Map.put(user_data, :nickname, nil)
+ end
+
{:ok, user_data}
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"),
+ @httpoison.get(ap_id, [Accept: "application/activity+json"], follow_redirect: true),
{:ok, data} <- Jason.decode(body) do
user_data_from_user_object(data)
else
@@ -688,6 +747,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
"actor" => data["attributedTo"],
"object" => data
},
+ :ok <- Transmogrifier.contain_origin(id, params),
{:ok, activity} <- Transmogrifier.handle_incoming(params) do
{:ok, Object.normalize(activity.data["object"])}
else
diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
index d337532d0..52b2a467e 100644
--- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
@@ -3,6 +3,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
alias Pleroma.{User, Object}
alias Pleroma.Web.ActivityPub.{ObjectView, UserView}
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.Federator
require Logger
@@ -107,6 +108,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
json(conn, "ok")
end
+ def relay(conn, params) do
+ with %User{} = user <- Relay.get_actor(),
+ {:ok, user} <- Pleroma.Web.WebFinger.ensure_keys_present(user) do
+ conn
+ |> put_resp_header("content-type", "application/activity+json")
+ |> json(UserView.render("user.json", %{user: user}))
+ else
+ nil -> {:error, :not_found}
+ end
+ end
+
def errors(conn, {:error, :not_found}) do
conn
|> put_status(404)
diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex
new file mode 100644
index 000000000..d30853d62
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/relay.ex
@@ -0,0 +1,44 @@
+defmodule Pleroma.Web.ActivityPub.Relay do
+ alias Pleroma.{User, Object, Activity}
+ alias Pleroma.Web.ActivityPub.ActivityPub
+ require Logger
+
+ def get_actor do
+ User.get_or_create_instance_user()
+ end
+
+ def follow(target_instance) do
+ with %User{} = local_user <- get_actor(),
+ %User{} = target_user <- User.get_or_fetch_by_ap_id(target_instance),
+ {:ok, activity} <- ActivityPub.follow(local_user, target_user) do
+ Logger.info("relay: followed instance: #{target_instance}; id=#{activity.data["id"]}")
+ else
+ e -> Logger.error("error: #{inspect(e)}")
+ end
+
+ :ok
+ end
+
+ def unfollow(target_instance) do
+ with %User{} = local_user <- get_actor(),
+ %User{} = target_user <- User.get_or_fetch_by_ap_id(target_instance),
+ {:ok, activity} <- ActivityPub.unfollow(local_user, target_user) do
+ Logger.info("relay: unfollowed instance: #{target_instance}: id=#{activity.data["id"]}")
+ else
+ e -> Logger.error("error: #{inspect(e)}")
+ end
+
+ :ok
+ end
+
+ def publish(%Activity{data: %{"type" => "Create"}} = activity) do
+ with %User{} = user <- get_actor(),
+ %Object{} = object <- Object.normalize(activity.data["object"]["id"]) do
+ ActivityPub.announce(user, object)
+ else
+ e -> Logger.error("error: #{inspect(e)}")
+ end
+ end
+
+ def publish(_), do: nil
+end
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index e5fb6e033..4a3a82195 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -18,16 +18,30 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
def get_actor(%{"actor" => actor}) when is_list(actor) do
- Enum.at(actor, 0)
+ if is_binary(Enum.at(actor, 0)) do
+ Enum.at(actor, 0)
+ else
+ Enum.find(actor, fn %{"type" => type} -> type == "Person" end)
+ |> Map.get("id")
+ end
end
def get_actor(%{"actor" => actor}) when is_map(actor) do
actor["id"]
end
- def get_actor(%{"actor" => actor_list}) do
- Enum.find(actor_list, fn %{"type" => type} -> type == "Person" end)
- |> Map.get("id")
+ @doc """
+ Checks that an imported AP object's actor matches the domain it came from.
+ """
+ def contain_origin(id, %{"actor" => actor} = params) do
+ id_uri = URI.parse(id)
+ actor_uri = URI.parse(get_actor(params))
+
+ if id_uri.host == actor_uri.host do
+ :ok
+ else
+ :error
+ end
end
@doc """
@@ -42,6 +56,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> fix_emoji
|> fix_tag
|> fix_content_map
+ |> fix_likes
|> fix_addressing
end
@@ -67,6 +82,20 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> Map.put("actor", get_actor(%{"actor" => actor}))
end
+ def fix_likes(%{"likes" => likes} = object)
+ when is_bitstring(likes) do
+ # Check for standardisation
+ # This is what Peertube does
+ # curl -H 'Accept: application/activity+json' $likes | jq .totalItems
+ object
+ |> Map.put("likes", [])
+ |> Map.put("like_count", 0)
+ end
+
+ def fix_likes(object) do
+ object
+ end
+
def fix_in_reply_to(%{"inReplyTo" => in_reply_to_id} = object)
when not is_nil(in_reply_to_id) do
case ActivityPub.fetch_object_from_id(in_reply_to_id) do
@@ -94,8 +123,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def fix_in_reply_to(object), do: object
def fix_context(object) do
+ context = object["context"] || object["conversation"] || Utils.generate_context_id()
+
object
- |> Map.put("context", object["conversation"])
+ |> Map.put("context", context)
+ |> Map.put("conversation", context)
end
def fix_attachments(object) do
@@ -159,11 +191,17 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def fix_content_map(object), do: object
+ # disallow objects with bogus IDs
+ def handle_incoming(%{"id" => nil}), do: :error
+ def handle_incoming(%{"id" => ""}), do: :error
+ # length of https:// = 8, should validate better, but good enough for now.
+ def handle_incoming(%{"id" => id}) when not (is_binary(id) and length(id) > 8), do: :error
+
# TODO: validate those with a Ecto scheme
# - tags
# - emoji
def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data)
- when objtype in ["Article", "Note"] do
+ when objtype in ["Article", "Note", "Video"] do
actor = get_actor(data)
data =
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index 8b41a3bec..0664b5a2e 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -128,7 +128,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
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 ["Article", "Note"] do
+ when is_map(object_data) and type in ["Article", "Note", "Video"] do
with {:ok, _} <- Object.create(object_data) do
:ok
end
@@ -204,13 +204,17 @@ defmodule Pleroma.Web.ActivityPub.Utils do
end
def add_like_to_object(%Activity{data: %{"actor" => actor}}, object) do
- with likes <- [actor | object.data["likes"] || []] |> Enum.uniq() do
+ likes = if is_list(object.data["likes"]), do: object.data["likes"], else: []
+
+ with likes <- [actor | likes] |> Enum.uniq() do
update_likes_in_object(likes, object)
end
end
def remove_like_from_object(%Activity{data: %{"actor" => actor}}, object) do
- with likes <- (object.data["likes"] || []) |> List.delete(actor) do
+ likes = if is_list(object.data["likes"]), do: object.data["likes"], else: []
+
+ with likes <- likes |> List.delete(actor) do
update_likes_in_object(likes, object)
end
end
@@ -302,6 +306,24 @@ defmodule Pleroma.Web.ActivityPub.Utils do
@doc """
Make announce activity data for the given actor and object
"""
+ # for relayed messages, we only want to send to subscribers
+ def make_announce_data(
+ %User{ap_id: ap_id, nickname: nil} = user,
+ %Object{data: %{"id" => id}} = object,
+ activity_id
+ ) do
+ data = %{
+ "type" => "Announce",
+ "actor" => ap_id,
+ "object" => id,
+ "to" => [user.follower_address],
+ "cc" => [],
+ "context" => object.data["context"]
+ }
+
+ if activity_id, do: Map.put(data, "id", activity_id), else: data
+ end
+
def make_announce_data(
%User{ap_id: ap_id} = user,
%Object{data: %{"id" => id}} = object,
@@ -356,14 +378,27 @@ defmodule Pleroma.Web.ActivityPub.Utils do
if activity_id, do: Map.put(data, "id", activity_id), else: data
end
- def add_announce_to_object(%Activity{data: %{"actor" => actor}}, object) do
- with announcements <- [actor | object.data["announcements"] || []] |> Enum.uniq() do
+ def add_announce_to_object(
+ %Activity{
+ data: %{"actor" => actor, "cc" => ["https://www.w3.org/ns/activitystreams#Public"]}
+ },
+ object
+ ) do
+ announcements =
+ if is_list(object.data["announcements"]), do: object.data["announcements"], else: []
+
+ with announcements <- [actor | announcements] |> Enum.uniq() do
update_element_in_object("announcement", announcements, object)
end
end
+ def add_announce_to_object(_, object), do: {:ok, object}
+
def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do
- with announcements <- (object.data["announcements"] || []) |> List.delete(actor) do
+ announcements =
+ if is_list(object.data["announcements"]), do: object.data["announcements"], else: []
+
+ with announcements <- announcements |> List.delete(actor) do
update_element_in_object("announcement", announcements, object)
end
end
diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex
index 0b1d5a9fa..16419e1b7 100644
--- a/lib/pleroma/web/activity_pub/views/user_view.ex
+++ b/lib/pleroma/web/activity_pub/views/user_view.ex
@@ -9,6 +9,35 @@ defmodule Pleroma.Web.ActivityPub.UserView do
alias Pleroma.Web.ActivityPub.Utils
import Ecto.Query
+ # the instance itself is not a Person, but instead an Application
+ def render("user.json", %{user: %{nickname: nil} = user}) do
+ {:ok, user} = WebFinger.ensure_keys_present(user)
+ {:ok, _, public_key} = Salmon.keys_from_pem(user.info["keys"])
+ public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
+ public_key = :public_key.pem_encode([public_key])
+
+ %{
+ "@context" => "https://www.w3.org/ns/activitystreams",
+ "id" => user.ap_id,
+ "type" => "Application",
+ "following" => "#{user.ap_id}/following",
+ "followers" => "#{user.ap_id}/followers",
+ "inbox" => "#{user.ap_id}/inbox",
+ "name" => "Pleroma",
+ "summary" => "Virtual actor for Pleroma relay",
+ "url" => user.ap_id,
+ "manuallyApprovesFollowers" => false,
+ "publicKey" => %{
+ "id" => "#{user.ap_id}#main-key",
+ "owner" => user.ap_id,
+ "publicKeyPem" => public_key
+ },
+ "endpoints" => %{
+ "sharedInbox" => "#{Pleroma.Web.Endpoint.url()}/inbox"
+ }
+ }
+ end
+
def render("user.json", %{user: user}) do
{:ok, user} = WebFinger.ensure_keys_present(user)
{:ok, _, public_key} = Salmon.keys_from_pem(user.info["keys"])
@@ -42,7 +71,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"image" => %{
"type" => "Image",
"url" => User.banner_url(user)
- }
+ },
+ "tag" => user.info["source_data"]["tag"] || []
}
|> Map.merge(Utils.make_json_ld_header())
end
diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index 3f18a68e8..125c57d05 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -1,5 +1,5 @@
defmodule Pleroma.Web.CommonAPI do
- alias Pleroma.{Repo, Activity, Object}
+ alias Pleroma.{User, Repo, Activity, Object}
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Formatter
@@ -61,8 +61,13 @@ defmodule Pleroma.Web.CommonAPI do
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"])
+ case get_replied_to_activity(status_id) do
+ nil ->
+ "public"
+
+ inReplyTo ->
+ Pleroma.Web.MastodonAPI.StatusView.get_visibility(inReplyTo.data["object"])
+ end
end
def get_visibility(_), do: "public"
@@ -118,6 +123,18 @@ defmodule Pleroma.Web.CommonAPI do
end
def update(user) do
+ user =
+ with emoji <- emoji_from_profile(user),
+ source_data <- (user.info["source_data"] || %{}) |> Map.put("tag", emoji),
+ new_info <- Map.put(user.info, "source_data", source_data),
+ change <- User.info_changeset(user, %{info: new_info}),
+ {:ok, user} <- User.update_and_set_cache(change) do
+ user
+ else
+ _e ->
+ user
+ end
+
ActivityPub.update(%{
local: true,
to: [user.follower_address],
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index 30089f553..358ca22ac 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -1,6 +1,7 @@
defmodule Pleroma.Web.CommonAPI.Utils do
alias Pleroma.{Repo, Object, Formatter, Activity}
alias Pleroma.Web.ActivityPub.Utils
+ alias Pleroma.Web.Endpoint
alias Pleroma.User
alias Calendar.Strftime
alias Comeonin.Pbkdf2
@@ -64,7 +65,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def make_content_html(status, mentions, attachments, tags, no_attachment_links \\ false) do
status
- |> String.replace("\r", "")
|> format_input(mentions, tags)
|> maybe_add_attachments(attachments, no_attachment_links)
end
@@ -95,7 +95,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
def format_input(text, mentions, tags) do
text
|> Formatter.html_escape()
- |> String.replace("\n", "<br>")
+ |> String.replace(~r/\r?\n/, "<br>")
|> (&{[], &1}).()
|> Formatter.add_links()
|> Formatter.add_user_links(mentions)
@@ -109,7 +109,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|> 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>"
+ url = "<a href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>##{tag}</a>"
String.replace(text, full, url)
end)
end
@@ -196,4 +196,15 @@ defmodule Pleroma.Web.CommonAPI.Utils do
_ -> {:error, "Invalid password."}
end
end
+
+ def emoji_from_profile(%{info: info} = user) do
+ (Formatter.get_emoji(user.bio) ++ Formatter.get_emoji(user.name))
+ |> Enum.map(fn {shortcode, url} ->
+ %{
+ "type" => "Emoji",
+ "icon" => %{"type" => "Image", "url" => "#{Endpoint.url()}#{url}"},
+ "name" => ":#{shortcode}:"
+ }
+ end)
+ end
end
diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex
index cbedca004..1e5ac2721 100644
--- a/lib/pleroma/web/endpoint.ex
+++ b/lib/pleroma/web/endpoint.ex
@@ -11,7 +11,7 @@ defmodule Pleroma.Web.Endpoint do
#
# 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: Pleroma.Upload.upload_path(), gzip: false)
+ plug(Plug.Static, at: "/media", from: Pleroma.Uploaders.Local.upload_path(), gzip: false)
plug(
Plug.Static,
diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex
index ccefb0bdf..078f3ec11 100644
--- a/lib/pleroma/web/federator/federator.ex
+++ b/lib/pleroma/web/federator/federator.ex
@@ -4,6 +4,7 @@ defmodule Pleroma.Web.Federator do
alias Pleroma.Activity
alias Pleroma.Web.{WebFinger, Websub}
alias Pleroma.Web.ActivityPub.ActivityPub
+ alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
require Logger
@@ -69,6 +70,11 @@ defmodule Pleroma.Web.Federator do
Logger.info(fn -> "Sending #{activity.data["id"]} out via Salmon" end)
Pleroma.Web.Salmon.publish(actor, activity)
+
+ if Mix.env() != :test do
+ Logger.info(fn -> "Relaying #{activity.data["id"]} out" end)
+ Pleroma.Web.ActivityPub.Relay.publish(activity)
+ end
end
Logger.info(fn -> "Sending #{activity.data["id"]} out via AP" end)
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
index cd9525252..e0267f1dc 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
@@ -5,8 +5,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView, ListView}
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
- alias Pleroma.Web.{CommonAPI, OStatus}
+ alias Pleroma.Web.CommonAPI
alias Pleroma.Web.OAuth.{Authorization, Token, App}
+ alias Pleroma.Web.MediaProxy
alias Comeonin.Pbkdf2
import Ecto.Query
require Logger
@@ -19,9 +20,12 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
with cs <- App.register_changeset(%App{}, params) |> IO.inspect(),
{:ok, app} <- Repo.insert(cs) |> IO.inspect() do
res = %{
- id: app.id,
+ id: app.id |> to_string,
+ name: app.client_name,
client_id: app.client_id,
- client_secret: app.client_secret
+ client_secret: app.client_secret,
+ redirect_uri: app.redirect_uris,
+ website: app.website
}
json(conn, res)
@@ -650,17 +654,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
json(conn, %{})
end
- def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
- accounts = User.search(query, params["resolve"] == "true")
-
+ def status_search(query) do
fetched =
if Regex.match?(~r/https?:/, query) do
- with {:ok, activities} <- OStatus.fetch_activity_from_url(query) do
- activities
- |> Enum.filter(fn
- %{data: %{"type" => "Create"}} -> true
- _ -> false
- end)
+ with {:ok, object} <- ActivityPub.fetch_object_from_id(query) do
+ [Activity.get_create_activity_by_object_ap_id(object.data["id"])]
else
_e -> []
end
@@ -681,7 +679,13 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
order_by: [desc: :id]
)
- statuses = Repo.all(q) ++ fetched
+ Repo.all(q) ++ fetched
+ end
+
+ def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
+ accounts = User.search(query, params["resolve"] == "true")
+
+ statuses = status_search(query)
tags_path = Web.base_url() <> "/tag/"
@@ -705,35 +709,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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
- |> Enum.filter(fn
- %{data: %{"type" => "Create"}} -> true
- _ -> false
- end)
- else
- _e -> []
- end
- end || []
-
- q =
- from(
- a in Activity,
- where: fragment("?->>'type' = 'Create'", a.data),
- where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
- where:
- fragment(
- "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
- a.data,
- ^query
- ),
- limit: 20,
- order_by: [desc: :id]
- )
-
- statuses = Repo.all(q) ++ fetched
+ statuses = status_search(query)
tags =
String.split(query)
@@ -855,9 +831,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|> Map.put("type", "Create")
|> Map.put("blocking_user", user)
- # adding title is a hack to not make empty lists function like a public timeline
+ # we must filter the following list for the user to avoid leaking statuses the user
+ # does not actually have permission to see (for more info, peruse security issue #270).
+ following_to =
+ following
+ |> Enum.filter(fn x -> x in user.following end)
+
activities =
- ActivityPub.fetch_activities([title | following], params)
+ ActivityPub.fetch_activities_bounded(following_to, following, params)
|> Enum.reverse()
conn
@@ -1121,7 +1102,20 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
data2 =
Enum.slice(data, 0, 40)
|> Enum.map(fn x ->
- Map.put(x, "id", User.get_or_fetch(x["acct"]).id)
+ Map.put(
+ x,
+ "id",
+ case User.get_or_fetch(x["acct"]) do
+ %{id: id} -> id
+ _ -> 0
+ end
+ )
+ end)
+ |> Enum.map(fn x ->
+ Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
+ end)
+ |> Enum.map(fn x ->
+ Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
end)
conn
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index cc5261616..7bc32e688 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -28,7 +28,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
%{
id: to_string(user.id),
- username: hd(String.split(user.nickname, "@")),
+ username: username_from_nickname(user.nickname),
acct: user.nickname,
display_name: user.name || user.nickname,
locked: user_info.locked,
@@ -36,7 +36,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
followers_count: user_info.follower_count,
following_count: user_info.following_count,
statuses_count: user_info.note_count,
- note: user.bio || "",
+ note: HtmlSanitizeEx.basic_html(user.bio) || "",
url: user.ap_id,
avatar: image,
avatar_static: image,
@@ -46,7 +46,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
fields: [],
source: %{
note: "",
- privacy: "public",
+ privacy: user_info.default_scope,
sensitive: "false"
}
}
@@ -56,7 +56,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
%{
id: to_string(user.id),
acct: user.nickname,
- username: hd(String.split(user.nickname, "@")),
+ username: username_from_nickname(user.nickname),
url: user.ap_id
}
end
@@ -76,4 +76,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
def render("relationships.json", %{user: user, targets: targets}) do
render_many(targets, AccountView, "relationship.json", user: user, as: :target)
end
+
+ defp username_from_nickname(string) when is_binary(string) do
+ hd(String.split(string, "@"))
+ end
+
+ defp username_from_nickname(_), do: nil
end
diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex
index 5dbd59dd9..6962aa54f 100644
--- a/lib/pleroma/web/mastodon_api/views/status_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/status_view.ex
@@ -99,8 +99,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
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)
+ attachment_data = object["attachment"] || []
+ attachment_data = attachment_data ++ if object["type"] == "Video", do: [object], else: []
+ attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
created_at = Utils.to_masto_date(object["published"])
@@ -151,7 +152,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end
def render("attachment.json", %{attachment: attachment}) do
- [%{"mediaType" => media_type, "href" => href} | _] = attachment["url"]
+ [attachment_url | _] = attachment["url"]
+ media_type = attachment_url["mediaType"] || attachment_url["mimeType"]
+ href = attachment_url["href"]
type =
cond do
@@ -208,6 +211,19 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end
end
+ def render_content(%{"type" => "Video"} = object) do
+ name = object["name"]
+
+ content =
+ if !!name and name != "" do
+ "<p><a href=\"#{object["id"]}\">#{name}</a></p>#{object["content"]}"
+ else
+ object["content"]
+ end
+
+ HtmlSanitizeEx.basic_html(content)
+ end
+
def render_content(%{"type" => "Article"} = object) do
summary = object["name"]
diff --git a/lib/pleroma/web/ostatus/activity_representer.ex b/lib/pleroma/web/ostatus/activity_representer.ex
index 5d831459b..537bd9f77 100644
--- a/lib/pleroma/web/ostatus/activity_representer.ex
+++ b/lib/pleroma/web/ostatus/activity_representer.ex
@@ -184,7 +184,10 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do
retweeted_xml = to_simple_form(retweeted_activity, retweeted_user, true)
- mentions = activity.recipients |> get_mentions
+ mentions =
+ ([retweeted_user.ap_id] ++ activity.recipients)
+ |> Enum.uniq()
+ |> get_mentions()
[
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']},
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 2dadf974c..927323794 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -5,11 +5,23 @@ defmodule Pleroma.Web.Router do
@instance Application.get_env(:pleroma, :instance)
@federating Keyword.get(@instance, :federating)
+ @allow_relay Keyword.get(@instance, :allow_relay)
@public Keyword.get(@instance, :public)
@registrations_open Keyword.get(@instance, :registrations_open)
- def user_fetcher(username) do
- {:ok, Repo.get_by(User, %{nickname: username})}
+ def user_fetcher(username_or_email) do
+ {
+ :ok,
+ cond do
+ # First, try logging in as if it was a name
+ user = Repo.get_by(User, %{nickname: username_or_email}) ->
+ user
+
+ # If we get nil, we try using it as an email
+ user = Repo.get_by(User, %{email: username_or_email}) ->
+ user
+ end
+ }
end
pipeline :api do
@@ -282,6 +294,10 @@ defmodule Pleroma.Web.Router do
get("/externalprofile/show", TwitterAPI.Controller, :external_profile)
end
+ pipeline :ap_relay do
+ plug(:accepts, ["activity+json"])
+ end
+
pipeline :ostatus do
plug(:accepts, ["xml", "atom", "html", "activity+json"])
end
@@ -318,6 +334,13 @@ defmodule Pleroma.Web.Router do
end
if @federating do
+ if @allow_relay do
+ scope "/relay", Pleroma.Web.ActivityPub do
+ pipe_through(:ap_relay)
+ get("/", ActivityPubController, :relay)
+ end
+ end
+
scope "/", Pleroma.Web.ActivityPub do
pipe_through(:activitypub)
post("/users/:nickname/inbox", ActivityPubController, :inbox)
diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex
index c61bad830..6b6d40346 100644
--- a/lib/pleroma/web/streamer.ex
+++ b/lib/pleroma/web/streamer.ex
@@ -1,7 +1,8 @@
defmodule Pleroma.Web.Streamer do
use GenServer
require Logger
- alias Pleroma.{User, Notification, Activity, Object}
+ alias Pleroma.{User, Notification, Activity, Object, Repo}
+ alias Pleroma.Web.ActivityPub.ActivityPub
def init(args) do
{:ok, args}
@@ -60,8 +61,24 @@ defmodule Pleroma.Web.Streamer do
end
def handle_cast(%{action: :stream, topic: "list", item: item}, topics) do
+ author = User.get_cached_by_ap_id(item.data["actor"])
+
+ # filter the recipient list if the activity is not public, see #270.
+ recipient_lists =
+ case ActivityPub.is_public?(item) do
+ true ->
+ Pleroma.List.get_lists_from_activity(item)
+
+ _ ->
+ Pleroma.List.get_lists_from_activity(item)
+ |> Enum.filter(fn list ->
+ owner = Repo.get(User, list.user_id)
+ author.follower_address in owner.following
+ end)
+ end
+
recipient_topics =
- Pleroma.List.get_lists_from_activity(item)
+ recipient_lists
|> Enum.map(fn %{id: id} -> "list:#{id}" end)
Enum.each(recipient_topics || [], fn list_topic ->
diff --git a/lib/pleroma/web/templates/mastodon_api/mastodon/index.html.eex b/lib/pleroma/web/templates/mastodon_api/mastodon/index.html.eex
index 6a00b9e2c..0862412ea 100644
--- a/lib/pleroma/web/templates/mastodon_api/mastodon/index.html.eex
+++ b/lib/pleroma/web/templates/mastodon_api/mastodon/index.html.eex
@@ -19,7 +19,7 @@
<script id='initial-state' type='application/json'><%= raw @initial_state %></script>
<script src="/packs/application.js"></script>
</head>
-<body class='app-body no-reduce-motion'>
+<body class='app-body no-reduce-motion system-font'>
<div class='app-holder' data-props='{&quot;locale&quot;:&quot;en&quot;}' id='mastodon'>
</div>
</body>
diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
index 24ebdf007..886b70f5f 100644
--- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
@@ -156,29 +156,39 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
|> send_resp(200, response)
_ ->
- json(conn, %{
- site: %{
- name: Keyword.get(@instance, :name),
- description: Keyword.get(@instance, :description),
- server: Web.base_url(),
- textlimit: to_string(Keyword.get(@instance, :limit)),
- closed: if(Keyword.get(@instance, :registrations_open), do: "0", else: "1"),
- private: if(Keyword.get(@instance, :public, true), do: "0", else: "1"),
- pleromafe: %{
- theme: Keyword.get(@instance_fe, :theme),
- background: Keyword.get(@instance_fe, :background),
- logo: Keyword.get(@instance_fe, :logo),
- redirectRootNoLogin: Keyword.get(@instance_fe, :redirect_root_no_login),
- redirectRootLogin: Keyword.get(@instance_fe, :redirect_root_login),
- chatDisabled: !Keyword.get(@instance_chat, :enabled),
- showInstanceSpecificPanel: Keyword.get(@instance_fe, :show_instance_panel),
- showWhoToFollowPanel: Keyword.get(@instance_fe, :show_who_to_follow_panel),
- scopeOptionsEnabled: Keyword.get(@instance_fe, :scope_options_enabled),
- whoToFollowProvider: Keyword.get(@instance_fe, :who_to_follow_provider),
- whoToFollowLink: Keyword.get(@instance_fe, :who_to_follow_link)
- }
- }
- })
+ data = %{
+ name: Keyword.get(@instance, :name),
+ description: Keyword.get(@instance, :description),
+ server: Web.base_url(),
+ textlimit: to_string(Keyword.get(@instance, :limit)),
+ closed: if(Keyword.get(@instance, :registrations_open), do: "0", else: "1"),
+ private: if(Keyword.get(@instance, :public, true), do: "0", else: "1")
+ }
+
+ pleroma_fe = %{
+ theme: Keyword.get(@instance_fe, :theme),
+ background: Keyword.get(@instance_fe, :background),
+ logo: Keyword.get(@instance_fe, :logo),
+ logoMask: Keyword.get(@instance_fe, :logo_mask),
+ logoMargin: Keyword.get(@instance_fe, :logo_margin),
+ redirectRootNoLogin: Keyword.get(@instance_fe, :redirect_root_no_login),
+ redirectRootLogin: Keyword.get(@instance_fe, :redirect_root_login),
+ chatDisabled: !Keyword.get(@instance_chat, :enabled),
+ showInstanceSpecificPanel: Keyword.get(@instance_fe, :show_instance_panel),
+ scopeOptionsEnabled: Keyword.get(@instance_fe, :scope_options_enabled),
+ collapseMessageWithSubject: Keyword.get(@instance_fe, :collapse_message_with_subject)
+ }
+
+ managed_config = Keyword.get(@instance, :managed_config)
+
+ data =
+ if managed_config do
+ data |> Map.put("pleromafe", pleroma_fe)
+ else
+ data
+ end
+
+ json(conn, %{site: data})
end
end
diff --git a/lib/pleroma/web/twitter_api/representers/activity_representer.ex b/lib/pleroma/web/twitter_api/representers/activity_representer.ex
index 26bfb79af..9abea59a7 100644
--- a/lib/pleroma/web/twitter_api/representers/activity_representer.ex
+++ b/lib/pleroma/web/twitter_api/representers/activity_representer.ex
@@ -170,6 +170,15 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do
HtmlSanitizeEx.basic_html(content)
|> Formatter.emojify(object["emoji"])
+ video =
+ if object["type"] == "Video" do
+ vid = [object]
+ else
+ []
+ end
+
+ attachments = (object["attachment"] || []) ++ video
+
%{
"id" => activity.id,
"uri" => activity.data["object"]["id"],
@@ -181,7 +190,7 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do
"created_at" => created_at,
"in_reply_to_status_id" => object["inReplyToStatusId"],
"statusnet_conversation_id" => conversation_id,
- "attachments" => (object["attachment"] || []) |> ObjectRepresenter.enum_to_list(opts),
+ "attachments" => attachments |> ObjectRepresenter.enum_to_list(opts),
"attentions" => attentions,
"fave_num" => like_count,
"repeat_num" => announcement_count,
diff --git a/lib/pleroma/web/twitter_api/representers/object_representer.ex b/lib/pleroma/web/twitter_api/representers/object_representer.ex
index 9af8a1691..6aa794a59 100644
--- a/lib/pleroma/web/twitter_api/representers/object_representer.ex
+++ b/lib/pleroma/web/twitter_api/representers/object_representer.ex
@@ -7,18 +7,20 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter do
%{
url: url["href"] |> Pleroma.Web.MediaProxy.url(),
- mimetype: url["mediaType"],
+ mimetype: url["mediaType"] || url["mimeType"],
id: data["uuid"],
- oembed: false
+ oembed: false,
+ description: data["name"]
}
end
def to_map(%Object{data: %{"url" => url} = data}, _opts) when is_binary(url) do
%{
url: url |> Pleroma.Web.MediaProxy.url(),
- mimetype: data["mediaType"],
+ mimetype: data["mediaType"] || url["mimeType"],
id: data["uuid"],
- oembed: false
+ oembed: false,
+ description: data["name"]
}
end
diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
index 65e67396b..b3a56b27e 100644
--- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
@@ -1,7 +1,9 @@
defmodule Pleroma.Web.TwitterAPI.Controller do
use Pleroma.Web, :controller
+ alias Pleroma.Formatter
alias Pleroma.Web.TwitterAPI.{TwitterAPI, UserView, ActivityView, NotificationView}
alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
alias Pleroma.{Repo, Activity, User, Notification}
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
@@ -411,8 +413,18 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
def update_profile(%{assigns: %{user: user}} = conn, params) do
params =
if bio = params["description"] do
- bio_brs = Regex.replace(~r/\r?\n/, bio, "<br>")
- Map.put(params, "bio", bio_brs)
+ mentions = Formatter.parse_mentions(bio)
+ tags = Formatter.parse_tags(bio)
+
+ emoji =
+ (user.info["source_data"]["tag"] || [])
+ |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
+ |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
+ {String.trim(name, ":"), url}
+ end)
+
+ bio_html = CommonUtils.format_input(bio, mentions, tags)
+ Map.put(params, "bio", bio_html |> Formatter.emojify(emoji))
else
params
end
diff --git a/lib/pleroma/web/twitter_api/views/user_view.ex b/lib/pleroma/web/twitter_api/views/user_view.ex
index 7d0f0e703..32f93153d 100644
--- a/lib/pleroma/web/twitter_api/views/user_view.ex
+++ b/lib/pleroma/web/twitter_api/views/user_view.ex
@@ -36,12 +36,11 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
{String.trim(name, ":"), url}
end)
- bio = HtmlSanitizeEx.strip_tags(user.bio)
-
data = %{
"created_at" => user.inserted_at |> Utils.format_naive_asctime(),
- "description" => bio,
- "description_html" => bio |> Formatter.emojify(emoji),
+ "description" =>
+ HtmlSanitizeEx.strip_tags((user.bio || "") |> String.replace("<br>", "\n")),
+ "description_html" => HtmlSanitizeEx.basic_html(user.bio),
"favourites_count" => 0,
"followers_count" => user_info[:follower_count],
"following" => following,