Merge branch 'develop' into feature/activitypub
11
README.md
|
@ -22,7 +22,7 @@ No release has been made yet, but several servers have been online for months al
|
|||
### Dependencies
|
||||
|
||||
* Postgresql version 9.6 or newer
|
||||
* Elixir version 1.4 or newer (you will also need erlang-dev, erlang-parsetools, erlang-xmerl packages)
|
||||
* Elixir version 1.5 or newer
|
||||
* Build-essential tools
|
||||
|
||||
### Configuration
|
||||
|
@ -50,3 +50,12 @@ Logs can be watched by using `journalctl -fu pleroma.service`
|
|||
|
||||
### Standalone/run by other means
|
||||
Run `mix phx.server` in repository's root, it will output log into stdout/stderr
|
||||
|
||||
### Using an upstream proxy for federation
|
||||
|
||||
Add the following to your `dev.secret.exs` or `prod.secret.exs` if you want to proxify all http requests that pleroma makes to an upstream proxy server:
|
||||
|
||||
config :pleroma, :http,
|
||||
proxy_url: "127.0.0.1:8123"
|
||||
|
||||
This is useful for running pleroma inside Tor or i2p.
|
||||
|
|
|
@ -33,7 +33,7 @@ config :mime, :types, %{
|
|||
|
||||
config :pleroma, :websub, Pleroma.Web.Websub
|
||||
config :pleroma, :ostatus, Pleroma.Web.OStatus
|
||||
config :pleroma, :httpoison, HTTPoison
|
||||
config :pleroma, :httpoison, Pleroma.HTTP
|
||||
|
||||
version = with {version, 0} <- System.cmd("git", ["rev-parse", "HEAD"]) do
|
||||
"Pleroma #{String.trim(version)}"
|
||||
|
@ -41,6 +41,10 @@ version = with {version, 0} <- System.cmd("git", ["rev-parse", "HEAD"]) do
|
|||
_ -> "Pleroma dev"
|
||||
end
|
||||
|
||||
# Configures http settings, upstream proxy etc.
|
||||
config :pleroma, :http,
|
||||
proxy_url: nil
|
||||
|
||||
config :pleroma, :instance,
|
||||
version: version,
|
||||
name: "Pleroma",
|
||||
|
@ -48,6 +52,14 @@ config :pleroma, :instance,
|
|||
limit: 5000,
|
||||
registrations_open: true
|
||||
|
||||
config :pleroma, :media_proxy,
|
||||
enabled: false,
|
||||
redirect_on_failure: true
|
||||
#base_url: "https://cache.pleroma.social"
|
||||
|
||||
config :pleroma, :chat,
|
||||
enabled: true
|
||||
|
||||
# Import environment specific config. This must remain at the bottom
|
||||
# of this file so it overrides the configuration defined above.
|
||||
import_config "#{Mix.env}.exs"
|
||||
|
|
|
@ -1 +1,31 @@
|
|||
firefox, /emoji/Firefox.gif
|
||||
blank, /emoji/blank.png
|
||||
f_00b, /emoji/f_00b.png
|
||||
f_00b11b, /emoji/f_00b11b.png
|
||||
f_00b33b, /emoji/f_00b33b.png
|
||||
f_00h, /emoji/f_00h.png
|
||||
f_00t, /emoji/f_00t.png
|
||||
f_01b, /emoji/f_01b.png
|
||||
f_03b, /emoji/f_03b.png
|
||||
f_10b, /emoji/f_10b.png
|
||||
f_11b, /emoji/f_11b.png
|
||||
f_11b00b, /emoji/f_11b00b.png
|
||||
f_11b22b, /emoji/f_11b22b.png
|
||||
f_11h, /emoji/f_11h.png
|
||||
f_11t, /emoji/f_11t.png
|
||||
f_12b, /emoji/f_12b.png
|
||||
f_21b, /emoji/f_21b.png
|
||||
f_22b, /emoji/f_22b.png
|
||||
f_22b11b, /emoji/f_22b11b.png
|
||||
f_22b33b, /emoji/f_22b33b.png
|
||||
f_22h, /emoji/f_22h.png
|
||||
f_22t, /emoji/f_22t.png
|
||||
f_23b, /emoji/f_23b.png
|
||||
f_30b, /emoji/f_30b.png
|
||||
f_32b, /emoji/f_32b.png
|
||||
f_33b, /emoji/f_33b.png
|
||||
f_33b00b, /emoji/f_33b00b.png
|
||||
f_33b22b, /emoji/f_33b22b.png
|
||||
f_33h, /emoji/f_33h.png
|
||||
f_33t, /emoji/f_33t.png
|
||||
|
||||
|
|
|
@ -14,9 +14,12 @@ use Mix.Config
|
|||
# manifest is generated by the mix phoenix.digest task
|
||||
# which you typically run after static files are built.
|
||||
config :pleroma, Pleroma.Web.Endpoint,
|
||||
on_init: {Pleroma.Web.Endpoint, :load_from_system_env, []},
|
||||
url: [host: "example.com", port: 80],
|
||||
cache_static_manifest: "priv/static/cache_manifest.json"
|
||||
http: [port: 4000],
|
||||
protocol: "http",
|
||||
debug_errors: true,
|
||||
code_reloader: true,
|
||||
check_origin: false,
|
||||
watchers: []
|
||||
|
||||
# Do not print debug messages in production
|
||||
config :logger, level: :info
|
||||
|
|
5
installation/Caddyfile
Normal file
|
@ -0,0 +1,5 @@
|
|||
instance.example.com { # Your instance's domain
|
||||
proxy / localhost:4000 {
|
||||
websocket
|
||||
}
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
proxy_cache_path /tmp/pleroma-media-cache levels=1:2 keys_zone=pleroma_media_cache:10m max_size=10g
|
||||
inactive=720m use_temp_path=off;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name example.tld;
|
||||
|
@ -19,11 +22,17 @@ server {
|
|||
server_name example.tld;
|
||||
|
||||
location / {
|
||||
add_header 'Access-Control-Allow-Origin' '*';
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_pass http://localhost:4000;
|
||||
}
|
||||
include snippets/well-known.conf;
|
||||
|
||||
}
|
||||
location /proxy {
|
||||
proxy_cache pleroma_media_cache;
|
||||
proxy_cache_lock on;
|
||||
proxy_pass http://localhost:4000;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -8,11 +8,20 @@ defmodule Mix.Tasks.GenerateConfig do
|
|||
domain = IO.gets("What is your domain name? (e.g. pleroma.soykaf.com): ") |> String.trim
|
||||
name = IO.gets("What is the name of your instance? (e.g. Pleroma/Soykaf): ") |> String.trim
|
||||
email = IO.gets("What's your admin email address: ") |> String.trim
|
||||
mediaproxy = IO.gets("Do you want to activate the mediaproxy? (y/N): ")
|
||||
|> String.trim()
|
||||
|> String.downcase()
|
||||
|> String.starts_with?("y")
|
||||
proxy_url = if mediaproxy do
|
||||
IO.gets("What is the mediaproxy's URL? (e.g. https://cache.example.com): ") |> String.trim
|
||||
else
|
||||
"https://cache.example.com"
|
||||
end
|
||||
secret = :crypto.strong_rand_bytes(64) |> Base.encode64 |> binary_part(0, 64)
|
||||
dbpass = :crypto.strong_rand_bytes(64) |> Base.encode64 |> binary_part(0, 64)
|
||||
|
||||
resultSql = EEx.eval_file("lib/mix/tasks/sample_psql.eex", [dbpass: dbpass])
|
||||
result = EEx.eval_file("lib/mix/tasks/sample_config.eex", [domain: domain, email: email, name: name, secret: secret, dbpass: dbpass])
|
||||
result = EEx.eval_file("lib/mix/tasks/sample_config.eex", [domain: domain, email: email, name: name, secret: secret, mediaproxy: mediaproxy, proxy_url: proxy_url, dbpass: dbpass])
|
||||
|
||||
IO.puts("\nWriting config to config/generated_config.exs.\n\nCheck it and configure your database, then copy it to either config/dev.secret.exs or config/prod.secret.exs")
|
||||
File.write("config/generated_config.exs", result)
|
||||
|
|
|
@ -10,6 +10,11 @@ config :pleroma, :instance,
|
|||
limit: 5000,
|
||||
registrations_open: true
|
||||
|
||||
config :pleroma, :media_proxy,
|
||||
enabled: <%= mediaproxy %>,
|
||||
redirect_on_failure: true,
|
||||
base_url: "<%= proxy_url %>"
|
||||
|
||||
# Configure your database
|
||||
config :pleroma, Pleroma.Repo,
|
||||
adapter: Ecto.Adapters.Postgres,
|
||||
|
|
|
@ -20,13 +20,18 @@ defmodule Pleroma.Application do
|
|||
limit: 2500
|
||||
]]),
|
||||
worker(Pleroma.Web.Federator, []),
|
||||
worker(Pleroma.Web.ChatChannel.ChatChannelState, []),
|
||||
worker(Pleroma.Stats, []),
|
||||
]
|
||||
++ if Mix.env == :test, do: [], else: [worker(Pleroma.Web.Streamer, [])]
|
||||
++ if !chat_enabled(), do: [], else: [worker(Pleroma.Web.ChatChannel.ChatChannelState, [])]
|
||||
|
||||
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
|
||||
# for other strategies and supported options
|
||||
opts = [strategy: :one_for_one, name: Pleroma.Supervisor]
|
||||
Supervisor.start_link(children, opts)
|
||||
end
|
||||
|
||||
defp chat_enabled do
|
||||
Application.get_env(:pleroma, :chat, []) |> Keyword.get(:enabled)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
defmodule Pleroma.Formatter do
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.MediaProxy
|
||||
|
||||
@link_regex ~r/https?:\/\/[\w\.\/?=\-#%&@~\(\)]+[\w\/]/u
|
||||
def linkify(text) do
|
||||
|
@ -10,7 +11,7 @@ defmodule Pleroma.Formatter do
|
|||
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"], do: [{"#nsfw", "nsfw"}] ++ map, else: map end).()
|
||||
|> (fn map -> if data["sensitive"] in [true, "True", "true", "1"], do: [{"#nsfw", "nsfw"}] ++ map, else: map end).()
|
||||
end
|
||||
|
||||
def parse_mentions(text) do
|
||||
|
@ -103,13 +104,19 @@ defmodule Pleroma.Formatter do
|
|||
{finmoji, "/finmoji/128px/#{finmoji}-128.png"}
|
||||
end)
|
||||
|
||||
@emoji_from_file (with {:ok, file} <- File.read("config/emoji.txt") do
|
||||
file
|
||||
|> String.trim
|
||||
|> String.split("\n")
|
||||
|> Enum.map(fn(line) ->
|
||||
[name, file] = String.split(line, ", ")
|
||||
{name, file}
|
||||
@emoji_from_file (with {:ok, default} <- File.read("config/emoji.txt") do
|
||||
custom =
|
||||
with {:ok, custom} <- File.read("config/custom_emoji.txt") do
|
||||
custom
|
||||
else
|
||||
_e -> ""
|
||||
end
|
||||
(default <> "\n" <> custom)
|
||||
|> String.trim()
|
||||
|> String.split(~r/\n+/)
|
||||
|> Enum.map(fn(line) ->
|
||||
[name, file] = String.split(line, ~r/,\s*/)
|
||||
{name, file}
|
||||
end)
|
||||
else
|
||||
_ -> []
|
||||
|
@ -125,7 +132,7 @@ defmodule Pleroma.Formatter do
|
|||
end
|
||||
|
||||
Enum.reduce(all_emoji, text, fn ({emoji, file}, text) ->
|
||||
String.replace(text, ":#{emoji}:", "<img height='32px' width='32px' alt='#{emoji}' title='#{emoji}' src='#{file}' />")
|
||||
String.replace(text, ":#{emoji}:", "<img height='32px' width='32px' alt='#{emoji}' title='#{emoji}' src='#{MediaProxy.url(file)}' />")
|
||||
end)
|
||||
end
|
||||
|
||||
|
|
14
lib/pleroma/http/http.ex
Normal file
|
@ -0,0 +1,14 @@
|
|||
|
||||
defmodule Pleroma.HTTP do
|
||||
use HTTPoison.Base
|
||||
|
||||
def process_request_options(options) do
|
||||
config = Application.get_env(:pleroma, :http, [])
|
||||
proxy = Keyword.get(config, :proxy_url, nil)
|
||||
case proxy do
|
||||
nil -> options
|
||||
_ -> options ++ [proxy: proxy]
|
||||
end
|
||||
end
|
||||
|
||||
end
|
41
lib/pleroma/stats.ex
Normal file
|
@ -0,0 +1,41 @@
|
|||
defmodule Pleroma.Stats do
|
||||
import Ecto.Query
|
||||
alias Pleroma.{User, Repo, Activity}
|
||||
|
||||
def start_link do
|
||||
agent = Agent.start_link(fn -> {[], %{}} end, name: __MODULE__)
|
||||
spawn(fn -> schedule_update() end)
|
||||
agent
|
||||
end
|
||||
|
||||
def get_stats do
|
||||
Agent.get(__MODULE__, fn {_, stats} -> stats end)
|
||||
end
|
||||
|
||||
def get_peers do
|
||||
Agent.get(__MODULE__, fn {peers, _} -> peers end)
|
||||
end
|
||||
|
||||
def schedule_update do
|
||||
spawn(fn ->
|
||||
Process.sleep(1000 * 60 * 60 * 1) # 1 hour
|
||||
schedule_update()
|
||||
end)
|
||||
update_stats()
|
||||
end
|
||||
|
||||
def update_stats do
|
||||
peers = from(u in Pleroma.User,
|
||||
select: fragment("distinct ?->'host'", u.info),
|
||||
where: u.local != ^true)
|
||||
|> Repo.all()
|
||||
domain_count = Enum.count(peers)
|
||||
status_query = from(u in User.local_user_query,
|
||||
select: fragment("sum((?->>'note_count')::int)", u.info))
|
||||
status_count = Repo.one(status_query) |> IO.inspect
|
||||
user_count = Repo.aggregate(User.local_user_query, :count, :id)
|
||||
Agent.update(__MODULE__, fn _ ->
|
||||
{peers, %{domain_count: domain_count, status_count: status_count, user_count: user_count}}
|
||||
end)
|
||||
end
|
||||
end
|
|
@ -9,7 +9,7 @@ defmodule Pleroma.Upload do
|
|||
File.cp!(file.path, result_file)
|
||||
|
||||
# fix content type on some image uploads
|
||||
content_type = if file.content_type == "application/octet-stream" do
|
||||
content_type = if file.content_type in [nil, "application/octet-stream"] do
|
||||
get_content_type(file.path)
|
||||
else
|
||||
file.content_type
|
||||
|
|
|
@ -29,14 +29,14 @@ defmodule Pleroma.User do
|
|||
def avatar_url(user) do
|
||||
case user.avatar do
|
||||
%{"url" => [%{"href" => href} | _]} -> href
|
||||
_ -> "https://placehold.it/48x48"
|
||||
_ -> "#{Web.base_url()}/images/avi.png"
|
||||
end
|
||||
end
|
||||
|
||||
def banner_url(user) do
|
||||
case user.info["banner"] do
|
||||
%{"url" => [%{"href" => href} | _]} -> href
|
||||
_ -> nil
|
||||
_ -> "#{Web.base_url()}/images/banner.png"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -5,7 +5,9 @@ defmodule Pleroma.Web.UserSocket do
|
|||
|
||||
## Channels
|
||||
# channel "room:*", Pleroma.Web.RoomChannel
|
||||
channel "chat:*", Pleroma.Web.ChatChannel
|
||||
if Application.get_env(:pleroma, :chat) |> Keyword.get(:enabled) do
|
||||
channel "chat:*", Pleroma.Web.ChatChannel
|
||||
end
|
||||
|
||||
## Transports
|
||||
transport :websocket, Phoenix.Transports.WebSocket
|
||||
|
|
|
@ -24,7 +24,6 @@ defmodule Pleroma.Web.ChatChannel do
|
|||
end
|
||||
|
||||
defmodule Pleroma.Web.ChatChannel.ChatChannelState do
|
||||
use Agent
|
||||
@max_messages 20
|
||||
|
||||
def start_link do
|
||||
|
|
|
@ -95,7 +95,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
|
||||
Enum.reduce(mentions, step_one, fn ({match, %User{ap_id: ap_id}, uuid}, text) ->
|
||||
short_match = String.split(match, "@") |> tl() |> hd()
|
||||
String.replace(text, uuid, "<a href='#{ap_id}'>@#{short_match}</a>")
|
||||
String.replace(text, uuid, "<span><a href='#{ap_id}'>@<span>#{short_match}</span></a></span>")
|
||||
end)
|
||||
end
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
defmodule Pleroma.Web.Endpoint do
|
||||
use Phoenix.Endpoint, otp_app: :pleroma
|
||||
|
||||
socket "/socket", Pleroma.Web.UserSocket
|
||||
if Application.get_env(:pleroma, :chat) |> Keyword.get(:enabled) do
|
||||
socket "/socket", Pleroma.Web.UserSocket
|
||||
end
|
||||
socket "/api/v1", Pleroma.Web.MastodonAPI.MastodonSocket
|
||||
|
||||
# Serve at "/" the static files from "priv/static" directory.
|
||||
|
@ -12,7 +14,7 @@ defmodule Pleroma.Web.Endpoint do
|
|||
at: "/media", from: "uploads", gzip: false
|
||||
plug Plug.Static,
|
||||
at: "/", from: :pleroma,
|
||||
only: ~w(index.html static finmoji emoji packs sounds sw.js)
|
||||
only: ~w(index.html static finmoji emoji packs sounds images instance sw.js)
|
||||
|
||||
# Code reloading can be explicitly enabled under the
|
||||
# :code_reloader configuration of your endpoint.
|
||||
|
|
|
@ -41,12 +41,12 @@ defmodule Pleroma.Web.Federator do
|
|||
def handle(:publish, activity) do
|
||||
Logger.debug(fn -> "Running publish for #{activity.data["id"]}" end)
|
||||
with actor when not is_nil(actor) <- User.get_cached_by_ap_id(activity.data["actor"]) do
|
||||
Logger.debug(fn -> "Sending #{activity.data["id"]} out via websub" end)
|
||||
Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity)
|
||||
|
||||
{:ok, actor} = WebFinger.ensure_keys_present(actor)
|
||||
Logger.debug(fn -> "Sending #{activity.data["id"]} out via salmon" end)
|
||||
Pleroma.Web.Salmon.publish(actor, activity)
|
||||
|
||||
Logger.debug(fn -> "Sending #{activity.data["id"]} out via websub" end)
|
||||
Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
||||
use Pleroma.Web, :controller
|
||||
alias Pleroma.{Repo, Activity, User, Notification}
|
||||
alias Pleroma.{Repo, Activity, User, Notification, Stats}
|
||||
alias Pleroma.Web
|
||||
alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView}
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
|
@ -93,7 +93,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
@instance Application.get_env(:pleroma, :instance)
|
||||
|
||||
def masto_instance(conn, _params) do
|
||||
user_count = Repo.aggregate(User.local_user_query, :count, :id)
|
||||
response = %{
|
||||
uri: Web.base_url,
|
||||
title: Keyword.get(@instance, :name),
|
||||
|
@ -103,17 +102,18 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
urls: %{
|
||||
streaming_api: String.replace(Web.base_url, ["http","https"], "wss")
|
||||
},
|
||||
stats: %{
|
||||
status_count: 2,
|
||||
user_count: user_count,
|
||||
domain_count: 3
|
||||
},
|
||||
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.Formatter.get_custom_emoji()
|
||||
|> Enum.map(fn {shortcode, relative_url} ->
|
||||
|
@ -162,7 +162,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
|
|||
def public_timeline(%{assigns: %{user: user}} = conn, params) do
|
||||
params = params
|
||||
|> Map.put("type", ["Create", "Announce"])
|
||||
|> Map.put("local_only", !!params["local"])
|
||||
|> Map.put("local_only", params["local"] in [true, "True", "true", "1"])
|
||||
|> Map.put("blocking_user", user)
|
||||
|
||||
activities = ActivityPub.fetch_public_activities(params)
|
||||
|
|
|
@ -3,20 +3,17 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|
|||
alias Pleroma.User
|
||||
alias Pleroma.Web.MastodonAPI.AccountView
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
|
||||
defp image_url(%{"url" => [ %{ "href" => href } | _ ]}), do: href
|
||||
defp image_url(_), do: nil
|
||||
alias Pleroma.Web.MediaProxy
|
||||
|
||||
def render("accounts.json", %{users: users} = opts) do
|
||||
render_many(users, AccountView, "account.json", opts)
|
||||
end
|
||||
|
||||
def render("account.json", %{user: user}) do
|
||||
image = User.avatar_url(user)
|
||||
image = User.avatar_url(user) |> MediaProxy.url()
|
||||
header = User.banner_url(user) |> MediaProxy.url()
|
||||
user_info = User.user_info(user)
|
||||
|
||||
header = image_url(user.info["banner"]) || "https://placehold.it/700x335"
|
||||
|
||||
%{
|
||||
id: to_string(user.id),
|
||||
username: hd(String.split(user.nickname, "@")),
|
||||
|
|
|
@ -3,6 +3,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
alias Pleroma.Web.MastodonAPI.{AccountView, StatusView}
|
||||
alias Pleroma.{User, Activity}
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
alias Pleroma.Web.MediaProxy
|
||||
|
||||
def render("index.json", opts) do
|
||||
render_many(opts.activities, StatusView, "status.json", opts)
|
||||
|
@ -121,9 +122,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
|
||||
%{
|
||||
id: to_string(attachment["id"] || hash_id),
|
||||
url: href,
|
||||
url: MediaProxy.url(href),
|
||||
remote_url: href,
|
||||
preview_url: href,
|
||||
preview_url: MediaProxy.url(href),
|
||||
text_url: href,
|
||||
type: type
|
||||
}
|
||||
|
|
84
lib/pleroma/web/media_proxy/controller.ex
Normal file
|
@ -0,0 +1,84 @@
|
|||
defmodule Pleroma.Web.MediaProxy.MediaProxyController do
|
||||
use Pleroma.Web, :controller
|
||||
require Logger
|
||||
|
||||
@httpoison Application.get_env(:pleroma, :httpoison)
|
||||
|
||||
@max_body_length 25 * 1048576
|
||||
|
||||
@cache_control %{
|
||||
default: "public, max-age=1209600",
|
||||
error: "public, must-revalidate, max-age=160",
|
||||
}
|
||||
|
||||
def remote(conn, %{"sig" => sig, "url" => url}) do
|
||||
config = Application.get_env(:pleroma, :media_proxy, [])
|
||||
with \
|
||||
true <- Keyword.get(config, :enabled, false),
|
||||
{:ok, url} <- Pleroma.Web.MediaProxy.decode_url(sig, url),
|
||||
{:ok, content_type, body} <- proxy_request(url)
|
||||
do
|
||||
conn
|
||||
|> put_resp_content_type(content_type)
|
||||
|> set_cache_header(:default)
|
||||
|> send_resp(200, body)
|
||||
else
|
||||
false -> send_error(conn, 404)
|
||||
{:error, :invalid_signature} -> send_error(conn, 403)
|
||||
{:error, {:http, _, url}} -> redirect_or_error(conn, url, Keyword.get(config, :redirect_on_failure, true))
|
||||
end
|
||||
end
|
||||
|
||||
defp proxy_request(link) do
|
||||
headers = [{"user-agent", "Pleroma/MediaProxy; #{Pleroma.Web.base_url()} <#{Application.get_env(:pleroma, :instance)[:email]}>"}]
|
||||
options = @httpoison.process_request_options([:insecure, {:follow_redirect, true}])
|
||||
with \
|
||||
{:ok, 200, headers, client} <- :hackney.request(:get, link, headers, "", options),
|
||||
headers = Enum.into(headers, Map.new),
|
||||
{:ok, body} <- proxy_request_body(client),
|
||||
content_type <- proxy_request_content_type(headers, body)
|
||||
do
|
||||
{:ok, content_type, body}
|
||||
else
|
||||
{:ok, status, _, _} ->
|
||||
Logger.warn "MediaProxy: request failed, status #{status}, link: #{link}"
|
||||
{:error, {:http, :bad_status, link}}
|
||||
{:error, error} ->
|
||||
Logger.warn "MediaProxy: request failed, error #{inspect error}, link: #{link}"
|
||||
{:error, {:http, error, link}}
|
||||
end
|
||||
end
|
||||
|
||||
defp set_cache_header(conn, key) do
|
||||
Plug.Conn.put_resp_header(conn, "cache-control", @cache_control[key])
|
||||
end
|
||||
|
||||
defp redirect_or_error(conn, url, true), do: redirect(conn, external: url)
|
||||
defp redirect_or_error(conn, url, _), do: send_error(conn, 502, "Media proxy error: " <> url)
|
||||
|
||||
defp send_error(conn, code, body \\ "") do
|
||||
conn
|
||||
|> set_cache_header(:error)
|
||||
|> send_resp(code, body)
|
||||
end
|
||||
|
||||
defp proxy_request_body(client), do: proxy_request_body(client, <<>>)
|
||||
defp proxy_request_body(client, body) when byte_size(body) < @max_body_length do
|
||||
case :hackney.stream_body(client) do
|
||||
{:ok, data} -> proxy_request_body(client, <<body :: binary, data :: binary>>)
|
||||
:done -> {:ok, body}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
defp proxy_request_body(client, _) do
|
||||
:hackney.close(client)
|
||||
{:error, :body_too_large}
|
||||
end
|
||||
|
||||
# TODO: the body is passed here as well because some hosts do not provide a content-type.
|
||||
# At some point we may want to use magic numbers to discover the content-type and reply a proper one.
|
||||
defp proxy_request_content_type(headers, _body) do
|
||||
headers["Content-Type"] || headers["content-type"] || "image/jpeg"
|
||||
end
|
||||
|
||||
end
|
32
lib/pleroma/web/media_proxy/media_proxy.ex
Normal file
|
@ -0,0 +1,32 @@
|
|||
defmodule Pleroma.Web.MediaProxy do
|
||||
@base64_opts [padding: false]
|
||||
|
||||
def url(nil), do: nil
|
||||
|
||||
def url(url = "/" <> _), do: url
|
||||
|
||||
def url(url) do
|
||||
config = Application.get_env(:pleroma, :media_proxy, [])
|
||||
if !Keyword.get(config, :enabled, false) or String.starts_with?(url, Pleroma.Web.base_url) do
|
||||
url
|
||||
else
|
||||
secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base]
|
||||
base64 = Base.url_encode64(url, @base64_opts)
|
||||
sig = :crypto.hmac(:sha, secret, base64)
|
||||
sig64 = sig |> Base.url_encode64(@base64_opts)
|
||||
Keyword.get(config, :base_url, Pleroma.Web.base_url) <> "/proxy/#{sig64}/#{base64}"
|
||||
end
|
||||
end
|
||||
|
||||
def decode_url(sig, url) do
|
||||
secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base]
|
||||
sig = Base.url_decode64!(sig, @base64_opts)
|
||||
local_sig = :crypto.hmac(:sha, secret, url)
|
||||
if local_sig == sig do
|
||||
{:ok, Base.url_decode64!(url, @base64_opts)}
|
||||
else
|
||||
{:error, :invalid_signature}
|
||||
end
|
||||
end
|
||||
|
||||
end
|
12
lib/pleroma/web/oauth/fallback_controller.ex
Normal file
|
@ -0,0 +1,12 @@
|
|||
defmodule Pleroma.Web.OAuth.FallbackController do
|
||||
use Pleroma.Web, :controller
|
||||
alias Pleroma.Web.OAuth.OAuthController
|
||||
|
||||
# No user/password
|
||||
def call(conn, _) do
|
||||
conn
|
||||
|> put_flash(:error, "Invalid Username/Password")
|
||||
|> OAuthController.authorize(conn.params)
|
||||
end
|
||||
|
||||
end
|
|
@ -5,6 +5,11 @@ defmodule Pleroma.Web.OAuth.OAuthController do
|
|||
alias Pleroma.{Repo, User}
|
||||
alias Comeonin.Pbkdf2
|
||||
|
||||
plug :fetch_session
|
||||
plug :fetch_flash
|
||||
|
||||
action_fallback Pleroma.Web.OAuth.FallbackController
|
||||
|
||||
def authorize(conn, params) do
|
||||
render conn, "show.html", %{
|
||||
response_type: params["response_type"],
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
defmodule Pleroma.Web.OStatus.FeedRepresenter do
|
||||
alias Pleroma.Web.OStatus
|
||||
alias Pleroma.Web.OStatus.{UserRepresenter, ActivityRepresenter}
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.MediaProxy
|
||||
|
||||
def to_simple_form(user, activities, _users) do
|
||||
most_recent_update = (List.first(activities) || user).updated_at
|
||||
|
@ -25,6 +27,7 @@ defmodule Pleroma.Web.OStatus.FeedRepresenter do
|
|||
{:id, h.(OStatus.feed_path(user))},
|
||||
{:title, ['#{user.nickname}\'s timeline']},
|
||||
{:updated, h.(most_recent_update)},
|
||||
{:logo, [to_charlist(User.avatar_url(user) |> MediaProxy.url())]},
|
||||
{:link, [rel: 'hub', href: h.(OStatus.pubsub_path(user))], []},
|
||||
{:link, [rel: 'salmon', href: h.(OStatus.salmon_path(user))], []},
|
||||
{:link, [rel: 'self', href: h.(OStatus.feed_path(user)), type: 'application/atom+xml'], []},
|
||||
|
|
|
@ -22,6 +22,10 @@ defmodule Pleroma.Web.OStatus 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)
|
||||
|
@ -159,8 +163,7 @@ defmodule Pleroma.Web.OStatus do
|
|||
Get the cw that mastodon uses.
|
||||
"""
|
||||
def get_cw(entry) do
|
||||
with scope when not is_nil(scope) <- string_from_xpath("//mastodon:scope", entry),
|
||||
cw when not is_nil(cw) <- string_from_xpath("/*/summary", entry) do
|
||||
with cw when not is_nil(cw) <- string_from_xpath("/*/summary", entry) do
|
||||
cw
|
||||
else _e -> nil
|
||||
end
|
||||
|
|
|
@ -28,6 +28,13 @@ defmodule Pleroma.Web.Router do
|
|||
plug Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1, optional: true}
|
||||
end
|
||||
|
||||
pipeline :pleroma_html do
|
||||
plug :accepts, ["html"]
|
||||
plug :fetch_session
|
||||
plug Pleroma.Plugs.OAuthPlug
|
||||
plug Pleroma.Plugs.AuthenticationPlug, %{fetcher: &Router.user_fetcher/1, optional: true}
|
||||
end
|
||||
|
||||
pipeline :well_known do
|
||||
plug :accepts, ["xml", "xrd+xml"]
|
||||
end
|
||||
|
@ -51,6 +58,18 @@ defmodule Pleroma.Web.Router do
|
|||
get "/emoji", UtilController, :emoji
|
||||
end
|
||||
|
||||
scope "/", Pleroma.Web.TwitterAPI do
|
||||
pipe_through :pleroma_html
|
||||
get "/ostatus_subscribe", UtilController, :remote_follow
|
||||
post "/ostatus_subscribe", UtilController, :do_remote_follow
|
||||
post "/main/ostatus", UtilController, :remote_subscribe
|
||||
end
|
||||
|
||||
scope "/api/pleroma", Pleroma.Web.TwitterAPI do
|
||||
pipe_through :authenticated_api
|
||||
post "/follow_import", UtilController, :follow_import
|
||||
end
|
||||
|
||||
scope "/oauth", Pleroma.Web.OAuth do
|
||||
get "/authorize", OAuthController, :authorize
|
||||
post "/authorize", OAuthController, :create_authorization
|
||||
|
@ -101,6 +120,7 @@ defmodule Pleroma.Web.Router do
|
|||
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
|
||||
|
||||
|
@ -142,6 +162,8 @@ defmodule Pleroma.Web.Router do
|
|||
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
|
||||
|
||||
|
@ -188,8 +210,6 @@ defmodule Pleroma.Web.Router do
|
|||
|
||||
post "/qvitter/update_avatar", TwitterAPI.Controller, :update_avatar
|
||||
|
||||
get "/statuses/followers", TwitterAPI.Controller, :followers
|
||||
get "/statuses/friends", TwitterAPI.Controller, :friends
|
||||
get "/friends/ids", TwitterAPI.Controller, :friends_ids
|
||||
get "/friendships/no_retweets/ids", TwitterAPI.Controller, :empty_array
|
||||
|
||||
|
@ -243,6 +263,14 @@ defmodule Pleroma.Web.Router do
|
|||
delete "/auth/sign_out", MastodonAPIController, :logout
|
||||
end
|
||||
|
||||
pipeline :remote_media do
|
||||
plug :accepts, ["html"]
|
||||
end
|
||||
scope "/proxy/", Pleroma.Web.MediaProxy do
|
||||
pipe_through :remote_media
|
||||
get "/:sig/:url", MediaProxyController, :remote
|
||||
end
|
||||
|
||||
scope "/", Fallback do
|
||||
get "/*path", RedirectController, :redirector
|
||||
end
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
|
||||
<p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
|
||||
<h2>OAuth Authorization</h2>
|
||||
<%= form_for @conn, o_auth_path(@conn, :authorize), [as: "authorization"], fn f -> %>
|
||||
<%= label f, :name, "Name" %>
|
||||
|
|
11
lib/pleroma/web/templates/twitter_api/util/follow.html.eex
Normal file
|
@ -0,0 +1,11 @@
|
|||
<%= if @error == :error do %>
|
||||
<h2>Error fetching user</h2>
|
||||
<% else %>
|
||||
<h2>Remote follow</h2>
|
||||
<img width="128" height="128" src="<%= @avatar %>">
|
||||
<p><%= @name %></p>
|
||||
<%= form_for @conn, util_path(@conn, :do_remote_follow), [as: "user"], fn f -> %>
|
||||
<%= hidden_input f, :id, value: @id %>
|
||||
<%= submit "Authorize" %>
|
||||
<% end %>
|
||||
<% end %>
|
|
@ -0,0 +1,14 @@
|
|||
<%= if @error do %>
|
||||
<h2><%= @error %></h2>
|
||||
<% end %>
|
||||
<h2>Log in to follow</h2>
|
||||
<p><%= @name %></p>
|
||||
<img height="128" width="128" src="<%= @avatar %>">
|
||||
<%= form_for @conn, util_path(@conn, :do_remote_follow), [as: "authorization"], fn f -> %>
|
||||
<%= text_input f, :name, placeholder: "Username" %>
|
||||
<br>
|
||||
<%= password_input f, :password, placeholder: "Password" %>
|
||||
<br>
|
||||
<%= hidden_input f, :id, value: @id %>
|
||||
<%= submit "Authorize" %>
|
||||
<% end %>
|
|
@ -0,0 +1,6 @@
|
|||
<%= if @error do %>
|
||||
<p>Error following account</p>
|
||||
<% else %>
|
||||
<h2>Account followed!</h2>
|
||||
<% end %>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<%= if @error do %>
|
||||
<h2>Error: <%= @error %></h2>
|
||||
<% else %>
|
||||
<h2>Remotely follow <%= @nickname %></h2>
|
||||
<%= form_for @conn, util_path(@conn, :remote_subscribe), [as: "user"], fn f -> %>
|
||||
<%= hidden_input f, :nickname, value: @nickname %>
|
||||
<%= text_input f, :profile, placeholder: "Your account ID, e.g. lain@quitter.se" %>
|
||||
<%= submit "Follow" %>
|
||||
<% end %>
|
||||
<% end %>
|
|
@ -1,8 +1,12 @@
|
|||
defmodule Pleroma.Web.TwitterAPI.UtilController do
|
||||
use Pleroma.Web, :controller
|
||||
require Logger
|
||||
alias Pleroma.Web
|
||||
alias Pleroma.Web.OStatus
|
||||
alias Pleroma.Web.WebFinger
|
||||
alias Comeonin.Pbkdf2
|
||||
alias Pleroma.Formatter
|
||||
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.{Repo, PasswordResetToken, User}
|
||||
|
||||
def show_password_reset(conn, %{"token" => token}) do
|
||||
|
@ -29,6 +33,72 @@ defmodule Pleroma.Web.TwitterAPI.UtilController 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
|
||||
_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
|
||||
e ->
|
||||
Logger.debug("Remote follow failed with error #{inspect e}")
|
||||
conn
|
||||
|> render("followed.html", %{error: inspect(e)})
|
||||
end
|
||||
end
|
||||
|
||||
@instance Application.get_env(:pleroma, :instance)
|
||||
def config(conn, _params) do
|
||||
case get_format(conn) do
|
||||
|
@ -51,7 +121,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
|
|||
site: %{
|
||||
name: Keyword.get(@instance, :name),
|
||||
server: Web.base_url,
|
||||
textlimit: Keyword.get(@instance, :limit),
|
||||
textlimit: to_string(Keyword.get(@instance, :limit)),
|
||||
closed: if(Keyword.get(@instance, :registrations_open), do: "0", else: "1")
|
||||
}
|
||||
})
|
||||
|
@ -73,4 +143,24 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
|
|||
def emoji(conn, _params) do
|
||||
json conn, Enum.into(Formatter.get_custom_emoji(), %{})
|
||||
end
|
||||
|
||||
def follow_import(conn, %{"list" => %Plug.Upload{} = listfile}) do
|
||||
follow_import(conn, %{"list" => File.read!(listfile.path)})
|
||||
end
|
||||
def follow_import(%{assigns: %{user: user}} = conn, %{"list" => list}) do
|
||||
Task.start(fn ->
|
||||
String.split(list)
|
||||
|> Enum.map(fn nick ->
|
||||
with %User{} = follower <- User.get_cached_by_ap_id(user.ap_id),
|
||||
%User{} = followed <- User.get_or_fetch_by_nickname(nick),
|
||||
{:ok, follower} <- User.follow(follower, followed) do
|
||||
ActivityPub.follow(follower, followed)
|
||||
else
|
||||
_e -> Logger.debug "follow_import: following #{nick} failed"
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
json conn, "job started"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,7 +6,7 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter do
|
|||
data = object.data
|
||||
url = List.first(data["url"])
|
||||
%{
|
||||
url: url["href"],
|
||||
url: url["href"] |> Pleroma.Web.MediaProxy.url(),
|
||||
mimetype: url["mediaType"],
|
||||
id: data["uuid"],
|
||||
oembed: false
|
||||
|
|
|
@ -316,10 +316,12 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
|
|||
|
||||
def get_external_profile(for_user, uri) do
|
||||
with {:ok, %User{} = user} <- OStatus.find_or_make_user(uri) do
|
||||
with url <- user.info["topic"],
|
||||
{:ok, %{body: body}} <- @httpoison.get(url, [], follow_redirect: true, timeout: 10000, recv_timeout: 20000) do
|
||||
OStatus.handle_incoming(body)
|
||||
end
|
||||
spawn(fn ->
|
||||
with url <- user.info["topic"],
|
||||
{:ok, %{body: body}} <- @httpoison.get(url, [], follow_redirect: true, timeout: 10000, recv_timeout: 20000) do
|
||||
OStatus.handle_incoming(body)
|
||||
end
|
||||
end)
|
||||
{:ok, UserView.render("show.json", %{user: user, for: for_user})}
|
||||
else _e ->
|
||||
{:error, "Couldn't find user"}
|
||||
|
|
|
@ -263,16 +263,18 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
|
|||
end
|
||||
end
|
||||
|
||||
def followers(%{assigns: %{user: user}} = conn, _params) do
|
||||
with {:ok, followers} <- User.get_followers(user) do
|
||||
def followers(conn, params) do
|
||||
with {:ok, user} <- TwitterAPI.get_user(conn.assigns.user, params),
|
||||
{:ok, followers} <- User.get_followers(user) do
|
||||
render(conn, UserView, "index.json", %{users: followers, for: user})
|
||||
else
|
||||
_e -> bad_request_reply(conn, "Can't get followers")
|
||||
end
|
||||
end
|
||||
|
||||
def friends(%{assigns: %{user: user}} = conn, _params) do
|
||||
with {:ok, friends} <- User.get_friends(user) do
|
||||
def friends(conn, params) do
|
||||
with {:ok, user} <- TwitterAPI.get_user(conn.assigns.user, params),
|
||||
{:ok, friends} <- User.get_friends(user) do
|
||||
render(conn, UserView, "index.json", %{users: friends, for: user})
|
||||
else
|
||||
_e -> bad_request_reply(conn, "Can't get friends")
|
||||
|
|
|
@ -2,6 +2,7 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
|
|||
use Pleroma.Web, :view
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
alias Pleroma.Web.MediaProxy
|
||||
|
||||
def render("show.json", %{user: user = %User{}} = assigns) do
|
||||
render_one(user, Pleroma.Web.TwitterAPI.UserView, "user.json", assigns)
|
||||
|
@ -12,7 +13,7 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
|
|||
end
|
||||
|
||||
def render("user.json", %{user: user = %User{}} = assigns) do
|
||||
image = User.avatar_url(user)
|
||||
image = User.avatar_url(user) |> MediaProxy.url()
|
||||
{following, follows_you, statusnet_blocking} = if assigns[:for] do
|
||||
{
|
||||
User.following?(assigns[:for], user),
|
||||
|
@ -44,8 +45,9 @@ defmodule Pleroma.Web.TwitterAPI.UserView do
|
|||
"screen_name" => user.nickname,
|
||||
"statuses_count" => user_info[:note_count],
|
||||
"statusnet_profile_url" => user.ap_id,
|
||||
"cover_photo" => image_url(user.info["banner"]),
|
||||
"background_image" => image_url(user.info["background"])
|
||||
"cover_photo" => User.banner_url(user) |> MediaProxy.url(),
|
||||
"background_image" => image_url(user.info["background"]) |> MediaProxy.url(),
|
||||
"is_local" => user.local
|
||||
}
|
||||
|
||||
if assigns[:token] do
|
||||
|
|
|
@ -45,7 +45,8 @@ defmodule Pleroma.Web.WebFinger do
|
|||
{: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: "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
|
||||
|
@ -69,11 +70,13 @@ defmodule Pleroma.Web.WebFinger do
|
|||
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)
|
||||
data = %{
|
||||
"magic_key" => magic_key,
|
||||
"topic" => topic,
|
||||
"subject" => subject,
|
||||
"salmon" => salmon
|
||||
"salmon" => salmon,
|
||||
"subscribe_address" => subscribe_address
|
||||
}
|
||||
{:ok, data}
|
||||
end
|
||||
|
|
BIN
priv/static/emoji/blank.png
Normal file
After Width: | Height: | Size: 423 B |
BIN
priv/static/emoji/f_00b.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
priv/static/emoji/f_00b11b.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
priv/static/emoji/f_00b33b.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
priv/static/emoji/f_00h.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
priv/static/emoji/f_00t.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
priv/static/emoji/f_01b.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
priv/static/emoji/f_03b.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
priv/static/emoji/f_10b.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
priv/static/emoji/f_11b.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
priv/static/emoji/f_11b00b.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
priv/static/emoji/f_11b22b.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
priv/static/emoji/f_11h.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
priv/static/emoji/f_11t.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
priv/static/emoji/f_12b.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
priv/static/emoji/f_21b.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
priv/static/emoji/f_22b.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
priv/static/emoji/f_22b11b.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
priv/static/emoji/f_22b33b.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
priv/static/emoji/f_22h.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
priv/static/emoji/f_22t.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
priv/static/emoji/f_23b.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
priv/static/emoji/f_30b.png
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
priv/static/emoji/f_32b.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
priv/static/emoji/f_33b.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
priv/static/emoji/f_33b00b.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
priv/static/emoji/f_33b22b.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
priv/static/emoji/f_33h.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
priv/static/emoji/f_33t.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
priv/static/images/avi.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
priv/static/images/banner.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
|
@ -1 +1 @@
|
|||
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Pleroma</title><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/app.67f64792f89a96e59442c437c7ded0b3.css rel=stylesheet></head><body style="display: none"><div id=app></div><script type=text/javascript src=/static/js/manifest.ee87253244897e08bdce.js></script><script type=text/javascript src=/static/js/vendor.50cd70f77f559bfe1f27.js></script><script type=text/javascript src=/static/js/app.fefccf252cac9e1310ea.js></script></body></html>
|
||||
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Pleroma</title><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/app.b3deb1dd44970d86cc6b368f36fd09d9.css rel=stylesheet></head><body style="display: none"><div id=app></div><script type=text/javascript src=/static/js/manifest.15dfe939c498cca9840c.js></script><script type=text/javascript src=/static/js/vendor.409059e5a814f448f5bc.js></script><script type=text/javascript src=/static/js/app.30c01d7540d43b760f03.js></script></body></html>
|
BIN
priv/static/instance/thumbnail.jpeg
Normal file
After Width: | Height: | Size: 47 KiB |
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "Pleroma FE",
|
||||
"theme": "pleroma-dark",
|
||||
"background": "/static/bg.jpg",
|
||||
"logo": "/static/logo.png",
|
||||
"registrationOpen": true
|
||||
"defaultPath": "/main/all",
|
||||
"chatDisabled": false
|
||||
}
|
||||
|
|
1
priv/static/static/emoji.json
Normal file
6
priv/static/static/js/app.30c01d7540d43b760f03.js
Normal file
1
priv/static/static/js/app.30c01d7540d43b760f03.js.map
Normal file
2
priv/static/static/js/manifest.15dfe939c498cca9840c.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
!function(e){function t(r){if(n[r])return n[r].exports;var a=n[r]={exports:{},id:r,loaded:!1};return e[r].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var r=window.webpackJsonp;window.webpackJsonp=function(o,c){for(var p,l,s=0,i=[];s<o.length;s++)l=o[s],a[l]&&i.push.apply(i,a[l]),a[l]=0;for(p in c)Object.prototype.hasOwnProperty.call(c,p)&&(e[p]=c[p]);for(r&&r(o,c);i.length;)i.shift().call(null,t);if(c[0])return n[0]=0,t(0)};var n={},a={0:0};t.e=function(e,r){if(0===a[e])return r.call(null,t);if(void 0!==a[e])a[e].push(r);else{a[e]=[r];var n=document.getElementsByTagName("head")[0],o=document.createElement("script");o.type="text/javascript",o.charset="utf-8",o.async=!0,o.src=t.p+"static/js/"+e+"."+{1:"409059e5a814f448f5bc",2:"30c01d7540d43b760f03"}[e]+".js",n.appendChild(o)}},t.m=e,t.c=n,t.p="/"}([]);
|
||||
//# sourceMappingURL=manifest.15dfe939c498cca9840c.js.map
|
|
@ -1,2 +0,0 @@
|
|||
!function(e){function t(n){if(r[n])return r[n].exports;var a=r[n]={exports:{},id:n,loaded:!1};return e[n].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var n=window.webpackJsonp;window.webpackJsonp=function(c,o){for(var f,p,s=0,l=[];s<c.length;s++)p=c[s],a[p]&&l.push.apply(l,a[p]),a[p]=0;for(f in o)e[f]=o[f];for(n&&n(c,o);l.length;)l.shift().call(null,t);if(o[0])return r[0]=0,t(0)};var r={},a={0:0};t.e=function(e,n){if(0===a[e])return n.call(null,t);if(void 0!==a[e])a[e].push(n);else{a[e]=[n];var r=document.getElementsByTagName("head")[0],c=document.createElement("script");c.type="text/javascript",c.charset="utf-8",c.async=!0,c.src=t.p+"static/js/"+e+"."+{1:"50cd70f77f559bfe1f27",2:"fefccf252cac9e1310ea"}[e]+".js",r.appendChild(c)}},t.m=e,t.c=r,t.p="/"}([]);
|
||||
//# sourceMappingURL=manifest.ee87253244897e08bdce.js.map
|
32
priv/static/static/js/vendor.409059e5a814f448f5bc.js
Normal file
1
priv/static/static/js/vendor.409059e5a814f448f5bc.js.map
Normal file
|
@ -14,9 +14,9 @@ defmodule Pleroma.NotificationTest do
|
|||
|
||||
{:ok, [notification, other_notification]} = Notification.create_notifications(activity)
|
||||
|
||||
assert notification.user_id == other_user.id
|
||||
notified_ids = Enum.sort([notification.user_id, other_notification.user_id])
|
||||
assert notified_ids == [other_user.id, third_user.id]
|
||||
assert notification.activity_id == activity.id
|
||||
assert other_notification.user_id == third_user.id
|
||||
assert other_notification.activity_id == activity.id
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,10 +19,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
|
|||
statuses_count: 5,
|
||||
note: user.bio,
|
||||
url: user.ap_id,
|
||||
avatar: "https://placehold.it/48x48",
|
||||
avatar_static: "https://placehold.it/48x48",
|
||||
header: "https://placehold.it/700x335",
|
||||
header_static: "https://placehold.it/700x335",
|
||||
avatar: "http://localhost:4001/images/avi.png",
|
||||
avatar_static: "http://localhost:4001/images/avi.png",
|
||||
header: "http://localhost:4001/images/banner.png",
|
||||
header_static: "http://localhost:4001/images/banner.png",
|
||||
source: %{
|
||||
note: "",
|
||||
privacy: "public",
|
||||
|
|
|
@ -35,7 +35,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
|
|||
{:ok, [_activity]} = OStatus.fetch_activity_from_url("https://shitposter.club/notice/2827873")
|
||||
|
||||
conn = conn
|
||||
|> get("/api/v1/timelines/public")
|
||||
|> get("/api/v1/timelines/public", %{"local" => "False"})
|
||||
|
||||
assert length(json_response(conn, 200)) == 2
|
||||
|
||||
|
@ -43,6 +43,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
|
|||
|> 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
|
||||
|
||||
test "posting a status", %{conn: conn} do
|
||||
|
@ -50,9 +55,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
|
|||
|
||||
conn = conn
|
||||
|> assign(:user, user)
|
||||
|> post("/api/v1/statuses", %{"status" => "cofe", "spoiler_text" => "2hu"})
|
||||
|> post("/api/v1/statuses", %{"status" => "cofe", "spoiler_text" => "2hu", "sensitive" => "false"})
|
||||
|
||||
assert %{"content" => "cofe", "id" => id, "spoiler_text" => "2hu"} = json_response(conn, 200)
|
||||
assert %{"content" => "cofe", "id" => id, "spoiler_text" => "2hu", "sensitive" => false} = json_response(conn, 200)
|
||||
assert Repo.get(Activity, id)
|
||||
end
|
||||
|
||||
|
@ -145,7 +150,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
|
|||
|> assign(:user, user)
|
||||
|> get("/api/v1/notifications")
|
||||
|
||||
expected_response = "hi <a href=\"#{user.ap_id}\">@#{user.nickname}</a>"
|
||||
expected_response = "hi <span><a href=\"#{user.ap_id}\">@<span>#{user.nickname}</span></a></span>"
|
||||
assert [%{"status" => %{"content" => response}} | _rest] = json_response(conn, 200)
|
||||
assert response == expected_response
|
||||
end
|
||||
|
@ -161,7 +166,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
|
|||
|> assign(:user, user)
|
||||
|> get("/api/v1/notifications/#{notification.id}")
|
||||
|
||||
expected_response = "hi <a href=\"#{user.ap_id}\">@#{user.nickname}</a>"
|
||||
expected_response = "hi <span><a href=\"#{user.ap_id}\">@<span>#{user.nickname}</span></a></span>"
|
||||
assert %{"status" => %{"content" => response}} = json_response(conn, 200)
|
||||
assert response == expected_response
|
||||
end
|
||||
|
@ -581,11 +586,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
|
|||
|
||||
{: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
|
||||
|
|
|
@ -56,7 +56,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
|
|||
|
||||
test "contains mentions" do
|
||||
incoming = File.read!("test/fixtures/incoming_reply_mastodon.xml")
|
||||
user = insert(:user, %{ap_id: "https://pleroma.soykaf.com/users/lain"})
|
||||
# a user with this ap id might be in the cache.
|
||||
recipient = "https://pleroma.soykaf.com/users/lain"
|
||||
user = User.get_cached_by_ap_id(recipient) || insert(:user, %{ap_id: recipient})
|
||||
|
||||
{:ok, [activity]} = OStatus.handle_incoming(incoming)
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ defmodule Pleroma.Web.OStatus.FeedRepresenterTest do
|
|||
<id>#{OStatus.feed_path(user)}</id>
|
||||
<title>#{user.nickname}'s timeline</title>
|
||||
<updated>#{most_recent_update}</updated>
|
||||
<logo>#{User.avatar_url(user)}</logo>
|
||||
<link rel="hub" href="#{OStatus.pubsub_path(user)}" />
|
||||
<link rel="salmon" href="#{OStatus.salmon_path(user)}" />
|
||||
<link rel="self" href="#{OStatus.feed_path(user)}" type="application/atom+xml" />
|
||||
|
|
|
@ -302,7 +302,8 @@ defmodule Pleroma.Web.OStatusTest do
|
|||
"host" => "social.heldscal.la",
|
||||
"fqn" => user,
|
||||
"bio" => "cofe",
|
||||
"avatar" => %{"type" => "Image", "url" => [%{"href" => "https://social.heldscal.la/avatar/29191-original-20170421154949.jpeg", "mediaType" => "image/jpeg", "type" => "Link"}]}
|
||||
"avatar" => %{"type" => "Image", "url" => [%{"href" => "https://social.heldscal.la/avatar/29191-original-20170421154949.jpeg", "mediaType" => "image/jpeg", "type" => "Link"}]},
|
||||
"subscribe_address" => "https://social.heldscal.la/main/ostatussub?profile={uri}"
|
||||
}
|
||||
assert data == expected
|
||||
end
|
||||
|
@ -325,7 +326,8 @@ defmodule Pleroma.Web.OStatusTest do
|
|||
"host" => "social.heldscal.la",
|
||||
"fqn" => user,
|
||||
"bio" => "cofe",
|
||||
"avatar" => %{"type" => "Image", "url" => [%{"href" => "https://social.heldscal.la/avatar/29191-original-20170421154949.jpeg", "mediaType" => "image/jpeg", "type" => "Link"}]}
|
||||
"avatar" => %{"type" => "Image", "url" => [%{"href" => "https://social.heldscal.la/avatar/29191-original-20170421154949.jpeg", "mediaType" => "image/jpeg", "type" => "Link"}]},
|
||||
"subscribe_address" => "https://social.heldscal.la/main/ostatussub?profile={uri}"
|
||||
}
|
||||
assert data == expected
|
||||
end
|
||||
|
|
|
@ -21,6 +21,7 @@ defmodule Pleroma.Web.OStatus.UserRepresenterTest do
|
|||
<summary>#{user.bio}</summary>
|
||||
<name>#{user.nickname}</name>
|
||||
<link rel="avatar" href="#{User.avatar_url(user)}" />
|
||||
<link rel="header" href="#{User.banner_url(user)}" />
|
||||
"""
|
||||
|
||||
assert clean(res) == clean(expected)
|
||||
|
|
|
@ -518,7 +518,7 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do
|
|||
end
|
||||
|
||||
describe "GET /api/statuses/friends" do
|
||||
test "it returns a user's friends", %{conn: conn} do
|
||||
test "it returns the logged in user's friends", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
followed_one = insert(:user)
|
||||
followed_two = insert(:user)
|
||||
|
@ -533,6 +533,36 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do
|
|||
|
||||
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 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
|
||||
|> 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 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
|
||||
|> 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
|
||||
|
|
|
@ -34,7 +34,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
|
|||
|
||||
{ :ok, activity = %Activity{} } = TwitterAPI.create_status(user, input)
|
||||
|
||||
assert get_in(activity.data, ["object", "content"]) == "Hello again, <a href='shp'>@shp</a>.<script></script><br>This is on another :moominmamma: line. #2hu #epic #phantasmagoric<br><a href=\"http://example.org/image.jpg\" class='attachment'>image.jpg</a>"
|
||||
assert get_in(activity.data, ["object", "content"]) == "Hello again, <span><a href='shp'>@<span>shp</span></a></span>.<script></script><br>This is on another :moominmamma: line. #2hu #epic #phantasmagoric<br><a href=\"http://example.org/image.jpg\" class='attachment'>image.jpg</a>"
|
||||
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
|
||||
|
@ -291,7 +291,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
|
|||
archaeme_remote = insert(:user, %{nickname: "archaeme@archae.me"})
|
||||
|
||||
mentions = Pleroma.Formatter.parse_mentions(text)
|
||||
expected_text = "<a href='#{gsimg.ap_id}'>@gsimg</a> According to <a href='#{archaeme.ap_id}'>@archaeme</a>, that is @daggsy. Also hello <a href='#{archaeme_remote.ap_id}'>@archaeme</a>"
|
||||
expected_text = "<span><a href='#{gsimg.ap_id}'>@<span>gsimg</span></a></span> According to <span><a href='#{archaeme.ap_id}'>@<span>archaeme</span></a></span>, that is @daggsy. Also hello <span><a href='#{archaeme_remote.ap_id}'>@<span>archaeme</span></a></span>"
|
||||
|
||||
assert Utils.add_user_links(text, mentions) == expected_text
|
||||
end
|
||||
|
@ -404,7 +404,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
|
|||
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")
|
||||
# assert Activity.get_create_activity_by_object_ap_id("tag:mastodon.social,2017-04-05:objectId=1641750:objectType=Status")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|