commit
1bd1f62af5
@ -0,0 +1,231 @@
|
||||
defmodule Pleroma.LoadTesting.Fetcher do
|
||||
use Pleroma.LoadTesting.Helper
|
||||
|
||||
def fetch_user(user) do
|
||||
Benchee.run(%{
|
||||
"By id" => fn -> Repo.get_by(User, id: user.id) end,
|
||||
"By ap_id" => fn -> Repo.get_by(User, ap_id: user.ap_id) end,
|
||||
"By email" => fn -> Repo.get_by(User, email: user.email) end,
|
||||
"By nickname" => fn -> Repo.get_by(User, nickname: user.nickname) end
|
||||
})
|
||||
end
|
||||
|
||||
def query_timelines(user) do
|
||||
home_timeline_params = %{
|
||||
"count" => 20,
|
||||
"with_muted" => true,
|
||||
"type" => ["Create", "Announce"],
|
||||
"blocking_user" => user,
|
||||
"muting_user" => user,
|
||||
"user" => user
|
||||
}
|
||||
|
||||
mastodon_public_timeline_params = %{
|
||||
"count" => 20,
|
||||
"local_only" => true,
|
||||
"only_media" => "false",
|
||||
"type" => ["Create", "Announce"],
|
||||
"with_muted" => "true",
|
||||
"blocking_user" => user,
|
||||
"muting_user" => user
|
||||
}
|
||||
|
||||
mastodon_federated_timeline_params = %{
|
||||
"count" => 20,
|
||||
"only_media" => "false",
|
||||
"type" => ["Create", "Announce"],
|
||||
"with_muted" => "true",
|
||||
"blocking_user" => user,
|
||||
"muting_user" => user
|
||||
}
|
||||
|
||||
following = User.following(user)
|
||||
|
||||
Benchee.run(%{
|
||||
"User home timeline" => fn ->
|
||||
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities(
|
||||
following,
|
||||
home_timeline_params
|
||||
)
|
||||
end,
|
||||
"User mastodon public timeline" => fn ->
|
||||
Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities(
|
||||
mastodon_public_timeline_params
|
||||
)
|
||||
end,
|
||||
"User mastodon federated public timeline" => fn ->
|
||||
Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities(
|
||||
mastodon_federated_timeline_params
|
||||
)
|
||||
end
|
||||
})
|
||||
|
||||
home_activities =
|
||||
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities(
|
||||
following,
|
||||
home_timeline_params
|
||||
)
|
||||
|
||||
public_activities =
|
||||
Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities(mastodon_public_timeline_params)
|
||||
|
||||
public_federated_activities =
|
||||
Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities(
|
||||
mastodon_federated_timeline_params
|
||||
)
|
||||
|
||||
Benchee.run(%{
|
||||
"Rendering home timeline" => fn ->
|
||||
Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
|
||||
activities: home_activities,
|
||||
for: user,
|
||||
as: :activity
|
||||
})
|
||||
end,
|
||||
"Rendering public timeline" => fn ->
|
||||
Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
|
||||
activities: public_activities,
|
||||
for: user,
|
||||
as: :activity
|
||||
})
|
||||
end,
|
||||
"Rendering public federated timeline" => fn ->
|
||||
Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
|
||||
activities: public_federated_activities,
|
||||
for: user,
|
||||
as: :activity
|
||||
})
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
def query_notifications(user) do
|
||||
without_muted_params = %{"count" => "20", "with_muted" => "false"}
|
||||
with_muted_params = %{"count" => "20", "with_muted" => "true"}
|
||||
|
||||
Benchee.run(%{
|
||||
"Notifications without muted" => fn ->
|
||||
Pleroma.Web.MastodonAPI.MastodonAPI.get_notifications(user, without_muted_params)
|
||||
end,
|
||||
"Notifications with muted" => fn ->
|
||||
Pleroma.Web.MastodonAPI.MastodonAPI.get_notifications(user, with_muted_params)
|
||||
end
|
||||
})
|
||||
|
||||
without_muted_notifications =
|
||||
Pleroma.Web.MastodonAPI.MastodonAPI.get_notifications(user, without_muted_params)
|
||||
|
||||
with_muted_notifications =
|
||||
Pleroma.Web.MastodonAPI.MastodonAPI.get_notifications(user, with_muted_params)
|
||||
|
||||
Benchee.run(%{
|
||||
"Render notifications without muted" => fn ->
|
||||
Pleroma.Web.MastodonAPI.NotificationView.render("index.json", %{
|
||||
notifications: without_muted_notifications,
|
||||
for: user
|
||||
})
|
||||
end,
|
||||
"Render notifications with muted" => fn ->
|
||||
Pleroma.Web.MastodonAPI.NotificationView.render("index.json", %{
|
||||
notifications: with_muted_notifications,
|
||||
for: user
|
||||
})
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
def query_dms(user) do
|
||||
params = %{
|
||||
"count" => "20",
|
||||
"with_muted" => "true",
|
||||
"type" => "Create",
|
||||
"blocking_user" => user,
|
||||
"user" => user,
|
||||
visibility: "direct"
|
||||
}
|
||||
|
||||
Benchee.run(%{
|
||||
"Direct messages with muted" => fn ->
|
||||
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_query([user.ap_id], params)
|
||||
|> Pleroma.Pagination.fetch_paginated(params)
|
||||
end,
|
||||
"Direct messages without muted" => fn ->
|
||||
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_query([user.ap_id], params)
|
||||
|> Pleroma.Pagination.fetch_paginated(Map.put(params, "with_muted", false))
|
||||
end
|
||||
})
|
||||
|
||||
dms_with_muted =
|
||||
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_query([user.ap_id], params)
|
||||
|> Pleroma.Pagination.fetch_paginated(params)
|
||||
|
||||
dms_without_muted =
|
||||
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_query([user.ap_id], params)
|
||||
|> Pleroma.Pagination.fetch_paginated(Map.put(params, "with_muted", false))
|
||||
|
||||
Benchee.run(%{
|
||||
"Rendering dms with muted" => fn ->
|
||||
Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
|
||||
activities: dms_with_muted,
|
||||
for: user,
|
||||
as: :activity
|
||||
})
|
||||
end,
|
||||
"Rendering dms without muted" => fn ->
|
||||
Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
|
||||
activities: dms_without_muted,
|
||||
for: user,
|
||||
as: :activity
|
||||
})
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
def query_long_thread(user, activity) do
|
||||
Benchee.run(%{
|
||||
"Fetch main post" => fn ->
|
||||
Pleroma.Activity.get_by_id_with_object(activity.id)
|
||||
end,
|
||||
"Fetch context of main post" => fn ->
|
||||
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_for_context(
|
||||
activity.data["context"],
|
||||
%{
|
||||
"blocking_user" => user,
|
||||
"user" => user,
|
||||
"exclude_id" => activity.id
|
||||
}
|
||||
)
|
||||
end
|
||||
})
|
||||
|
||||
activity = Pleroma.Activity.get_by_id_with_object(activity.id)
|
||||
|
||||
context =
|
||||
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_for_context(
|
||||
activity.data["context"],
|
||||
%{
|
||||
"blocking_user" => user,
|
||||
"user" => user,
|
||||
"exclude_id" => activity.id
|
||||
}
|
||||
)
|
||||
|
||||
Benchee.run(%{
|
||||
"Render status" => fn ->
|
||||
Pleroma.Web.MastodonAPI.StatusView.render("show.json", %{
|
||||
activity: activity,
|
||||
for: user
|
||||
})
|
||||
end,
|
||||
"Render context" => fn ->
|
||||
Pleroma.Web.MastodonAPI.StatusView.render(
|
||||
"index.json",
|
||||
for: user,
|
||||
activities: context,
|
||||
as: :activity
|
||||
)
|
||||
|> Enum.reverse()
|
||||
end
|
||||
})
|
||||
end
|
||||
end
|
@ -0,0 +1,350 @@
|
||||
defmodule Pleroma.LoadTesting.Generator do
|
||||
use Pleroma.LoadTesting.Helper
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
||||
def generate_users(opts) do
|
||||
IO.puts("Starting generating #{opts[:users_max]} users...")
|
||||
{time, _} = :timer.tc(fn -> do_generate_users(opts) end)
|
||||
|
||||
IO.puts("Inserting users take #{to_sec(time)} sec.\n")
|
||||
end
|
||||
|
||||
defp do_generate_users(opts) do
|
||||
max = Keyword.get(opts, :users_max)
|
||||
|
||||
Task.async_stream(
|
||||
1..max,
|
||||
&generate_user_data(&1),
|
||||
max_concurrency: 10,
|
||||
timeout: 30_000
|
||||
)
|
||||
|> Enum.to_list()
|
||||
end
|
||||
|
||||
defp generate_user_data(i) do
|
||||
remote = Enum.random([true, false])
|
||||
|
||||
user = %User{
|
||||
name: "Test テスト User #{i}",
|
||||
email: "user#{i}@example.com",
|
||||
nickname: "nick#{i}",
|
||||
password_hash:
|
||||
"$pbkdf2-sha512$160000$bU.OSFI7H/yqWb5DPEqyjw$uKp/2rmXw12QqnRRTqTtuk2DTwZfF8VR4MYW2xMeIlqPR/UX1nT1CEKVUx2CowFMZ5JON8aDvURrZpJjSgqXrg",
|
||||
bio: "Tester Number #{i}",
|
||||
info: %{},
|
||||
local: remote
|
||||
}
|
||||
|
||||
user_urls =
|
||||
if remote do
|
||||
base_url =
|
||||
Enum.random(["https://domain1.com", "https://domain2.com", "https://domain3.com"])
|
||||
|
||||
ap_id = "#{base_url}/users/#{user.nickname}"
|
||||
|
||||
%{
|
||||
ap_id: ap_id,
|
||||
follower_address: ap_id <> "/followers",
|
||||
following_address: ap_id <> "/following"
|
||||
}
|
||||
else
|
||||
%{
|
||||
ap_id: User.ap_id(user),
|
||||
follower_address: User.ap_followers(user),
|
||||
following_address: User.ap_following(user)
|
||||
}
|
||||
end
|
||||
|
||||
user = Map.merge(user, user_urls)
|
||||
|
||||
Repo.insert!(user)
|
||||
end
|
||||
|
||||
def generate_activities(user, users) do
|
||||
do_generate_activities(user, users)
|
||||
end
|
||||
|
||||
defp do_generate_activities(user, users) do
|
||||
IO.puts("Starting generating 20000 common activities...")
|
||||
|
||||
{time, _} =
|
||||
:timer.tc(fn ->
|
||||
Task.async_stream(
|
||||
1..20_000,
|
||||
fn _ ->
|
||||
do_generate_activity([user | users])
|
||||
end,
|
||||
max_concurrency: 10,
|
||||
timeout: 30_000
|
||||
)
|
||||
|> Stream.run()
|
||||
end)
|
||||
|
||||
IO.puts("Inserting common activities take #{to_sec(time)} sec.\n")
|
||||
|
||||
IO.puts("Starting generating 20000 activities with mentions...")
|
||||
|
||||
{time, _} =
|
||||
:timer.tc(fn ->
|
||||
Task.async_stream(
|
||||
1..20_000,
|
||||
fn _ ->
|
||||
do_generate_activity_with_mention(user, users)
|
||||
end,
|
||||
max_concurrency: 10,
|
||||
timeout: 30_000
|
||||
)
|
||||
|> Stream.run()
|
||||
end)
|
||||
|
||||
IO.puts("Inserting activities with menthions take #{to_sec(time)} sec.\n")
|
||||
|
||||
IO.puts("Starting generating 10000 activities with threads...")
|
||||
|
||||
{time, _} =
|
||||
:timer.tc(fn ->
|
||||
Task.async_stream(
|
||||
1..10_000,
|
||||
fn _ ->
|
||||
do_generate_threads([user | users])
|
||||
end,
|
||||
max_concurrency: 10,
|
||||
timeout: 30_000
|
||||
)
|
||||
|> Stream.run()
|
||||
end)
|
||||
|
||||
IO.puts("Inserting activities with threads take #{to_sec(time)} sec.\n")
|
||||
end
|
||||
|
||||
defp do_generate_activity(users) do
|
||||
post = %{
|
||||
"status" => "Some status without mention with random user"
|
||||
}
|
||||
|
||||
CommonAPI.post(Enum.random(users), post)
|
||||
end
|
||||
|
||||
defp do_generate_activity_with_mention(user, users) do
|
||||
mentions_cnt = Enum.random([2, 3, 4, 5])
|
||||
with_user = Enum.random([true, false])
|
||||
users = Enum.shuffle(users)
|
||||
mentions_users = Enum.take(users, mentions_cnt)
|
||||
mentions_users = if with_user, do: [user | mentions_users], else: mentions_users
|
||||
|
||||
mentions_str =
|
||||
Enum.map(mentions_users, fn user -> "@" <> user.nickname end) |> Enum.join(", ")
|
||||
|
||||
post = %{
|
||||
"status" => mentions_str <> "some status with mentions random users"
|
||||
}
|
||||
|
||||
CommonAPI.post(Enum.random(users), post)
|
||||
end
|
||||
|
||||
defp do_generate_threads(users) do
|
||||
thread_length = Enum.random([2, 3, 4, 5])
|
||||
actor = Enum.random(users)
|
||||
|
||||
post = %{
|
||||
"status" => "Start of the thread"
|
||||
}
|
||||
|
||||
{:ok, activity} = CommonAPI.post(actor, post)
|
||||
|
||||
Enum.each(1..thread_length, fn _ ->
|
||||
user = Enum.random(users)
|
||||
|
||||
post = %{
|
||||
"status" => "@#{actor.nickname} reply to thread",
|
||||
"in_reply_to_status_id" => activity.id
|
||||
}
|
||||
|
||||
CommonAPI.post(user, post)
|
||||
end)
|
||||
end
|
||||
|
||||
def generate_remote_activities(user, users) do
|
||||
do_generate_remote_activities(user, users)
|
||||
end
|
||||
|
||||
defp do_generate_remote_activities(user, users) do
|
||||
IO.puts("Starting generating 10000 remote activities...")
|
||||
|
||||
{time, _} =
|
||||
:timer.tc(fn ->
|
||||
Task.async_stream(
|
||||
1..10_000,
|
||||
fn i ->
|
||||
do_generate_remote_activity(i, user, users)
|
||||
end,
|
||||
max_concurrency: 10,
|
||||
timeout: 30_000
|
||||
)
|
||||
|> Stream.run()
|
||||
end)
|
||||
|
||||
IO.puts("Inserting remote activities take #{to_sec(time)} sec.\n")
|
||||
end
|
||||
|
||||
defp do_generate_remote_activity(i, user, users) do
|
||||
actor = Enum.random(users)
|
||||
%{host: host} = URI.parse(actor.ap_id)
|
||||
date = Date.utc_today()
|
||||
datetime = DateTime.utc_now()
|
||||
|
||||
map = %{
|
||||
"actor" => actor.ap_id,
|
||||
"cc" => [actor.follower_address, user.ap_id],
|
||||
"context" => "tag:mastodon.example.org,#{date}:objectId=#{i}:objectType=Conversation",
|
||||
"id" => actor.ap_id <> "/statuses/#{i}/activity",
|
||||
"object" => %{
|
||||
"actor" => actor.ap_id,
|
||||
"atomUri" => actor.ap_id <> "/statuses/#{i}",
|
||||
"attachment" => [],
|
||||
"attributedTo" => actor.ap_id,
|
||||
"bcc" => [],
|
||||
"bto" => [],
|
||||
"cc" => [actor.follower_address, user.ap_id],
|
||||
"content" =>
|
||||
"<p><span class=\"h-card\"><a href=\"" <>
|
||||
user.ap_id <>
|
||||
"\" class=\"u-url mention\">@<span>" <> user.nickname <> "</span></a></span></p>",
|
||||
"context" => "tag:mastodon.example.org,#{date}:objectId=#{i}:objectType=Conversation",
|
||||
"conversation" =>
|
||||
"tag:mastodon.example.org,#{date}:objectId=#{i}:objectType=Conversation",
|
||||
"emoji" => %{},
|
||||
"id" => actor.ap_id <> "/statuses/#{i}",
|
||||
"inReplyTo" => nil,
|
||||
"inReplyToAtomUri" => nil,
|
||||
"published" => datetime,
|
||||
"sensitive" => true,
|
||||
"summary" => "cw",
|
||||
"tag" => [
|
||||
%{
|
||||
"href" => user.ap_id,
|
||||
"name" => "@#{user.nickname}@#{host}",
|
||||
"type" => "Mention"
|
||||
}
|
||||
],
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"type" => "Note",
|
||||
"url" => "http://#{host}/@#{actor.nickname}/#{i}"
|
||||
},
|
||||
"published" => datetime,
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"type" => "Create"
|
||||
}
|
||||
|
||||
Pleroma.Web.ActivityPub.ActivityPub.insert(map, false)
|
||||
end
|
||||
|
||||
def generate_dms(user, users, opts) do
|
||||
IO.puts("Starting generating #{opts[:dms_max]} DMs")
|
||||
{time, _} = :timer.tc(fn -> do_generate_dms(user, users, opts) end)
|
||||
IO.puts("Inserting dms take #{to_sec(time)} sec.\n")
|
||||
end
|
||||
|
||||
defp do_generate_dms(user, users, opts) do
|
||||
Task.async_stream(
|
||||
1..opts[:dms_max],
|
||||
fn _ ->
|
||||
do_generate_dm(user, users)
|
||||
end,
|
||||
max_concurrency: 10,
|
||||
timeout: 30_000
|
||||
)
|
||||
|> Stream.run()
|
||||
end
|
||||
|
||||
defp do_generate_dm(user, users) do
|
||||
post = %{
|
||||
"status" => "@#{user.nickname} some direct message",
|
||||
"visibility" => "direct"
|
||||
}
|
||||
|
||||
CommonAPI.post(Enum.random(users), post)
|
||||
end
|
||||
|
||||
def generate_long_thread(user, users, opts) do
|
||||
IO.puts("Starting generating long thread with #{opts[:thread_length]} replies")
|
||||
{time, activity} = :timer.tc(fn -> do_generate_long_thread(user, users, opts) end)
|
||||
IO.puts("Inserting long thread replies take #{to_sec(time)} sec.\n")
|
||||
{:ok, activity}
|
||||
end
|
||||
|
||||
defp do_generate_long_thread(user, users, opts) do
|
||||
{:ok, %{id: id} = activity} = CommonAPI.post(user, %{"status" => "Start of long thread"})
|
||||
|
||||
Task.async_stream(
|
||||
1..opts[:thread_length],
|
||||
fn _ -> do_generate_thread(users, id) end,
|
||||
max_concurrency: 10,
|
||||
timeout: 30_000
|
||||
)
|
||||
|> Stream.run()
|
||||
|
||||
activity
|
||||
end
|
||||
|
||||
defp do_generate_thread(users, activity_id) do
|
||||
CommonAPI.post(Enum.random(users), %{
|
||||
"status" => "reply to main post",
|
||||
"in_reply_to_status_id" => activity_id
|
||||
})
|
||||
end
|
||||
|
||||
def generate_non_visible_message(user, users) do
|
||||
IO.puts("Starting generating 1000 non visible posts")
|
||||
|
||||
{time, _} =
|
||||
:timer.tc(fn ->
|
||||
do_generate_non_visible_posts(user, users)
|
||||
end)
|
||||
|
||||
IO.puts("Inserting non visible posts take #{to_sec(time)} sec.\n")
|
||||
end
|
||||
|
||||
defp do_generate_non_visible_posts(user, users) do
|
||||
[not_friend | users] = users
|
||||
|
||||
make_friends(user, users)
|
||||
|
||||
Task.async_stream(1..1000, fn _ -> do_generate_non_visible_post(not_friend, users) end,
|
||||
max_concurrency: 10,
|
||||
timeout: 30_000
|
||||
)
|
||||
|> Stream.run()
|
||||
end
|
||||
|
||||
defp make_friends(_user, []), do: nil
|
||||
|
||||
defp make_friends(user, [friend | users]) do
|
||||
{:ok, _} = User.follow(user, friend)
|
||||
{:ok, _} = User.follow(friend, user)
|
||||
make_friends(user, users)
|
||||
end
|
||||
|
||||
defp do_generate_non_visible_post(not_friend, users) do
|
||||
post = %{
|
||||
"status" => "some non visible post",
|
||||
"visibility" => "private"
|
||||
}
|
||||
|
||||
{:ok, activity} = CommonAPI.post(not_friend, post)
|
||||
|
||||
thread_length = Enum.random([2, 3, 4, 5])
|
||||
|
||||
Enum.each(1..thread_length, fn _ ->
|
||||
user = Enum.random(users)
|
||||
|
||||
post = %{
|
||||
"status" => "@#{not_friend.nickname} reply to non visible post",
|
||||
"in_reply_to_status_id" => activity.id,
|
||||
"visibility" => "private"
|
||||
}
|
||||
|
||||
CommonAPI.post(user, post)
|
||||
end)
|
||||
end
|
||||
end
|
@ -0,0 +1,11 @@
|
||||
defmodule Pleroma.LoadTesting.Helper do
|
||||
defmacro __using__(_) do
|
||||
quote do
|
||||
import Ecto.Query
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
|
||||
defp to_sec(microseconds), do: microseconds / 1_000_000
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,134 @@
|
||||
defmodule Mix.Tasks.Pleroma.LoadTesting do
|
||||
use Mix.Task
|
||||
use Pleroma.LoadTesting.Helper
|
||||
import Mix.Pleroma
|
||||
import Pleroma.LoadTesting.Generator
|
||||
import Pleroma.LoadTesting.Fetcher
|
||||
|
||||
@shortdoc "Factory for generation data"
|
||||
@moduledoc """
|
||||
Generates data like:
|
||||
- local/remote users
|
||||
- local/remote activities with notifications
|
||||
- direct messages
|
||||
- long thread
|
||||
- non visible posts
|
||||
|
||||
## Generate data
|
||||
MIX_ENV=benchmark mix pleroma.load_testing --users 20000 --dms 20000 --thread_length 2000
|
||||
MIX_ENV=benchmark mix pleroma.load_testing -u 20000 -d 20000 -t 2000
|
||||
|
||||
Options:
|
||||
- `--users NUMBER` - number of users to generate. Defaults to: 20000. Alias: `-u`
|
||||
- `--dms NUMBER` - number of direct messages to generate. Defaults to: 20000. Alias `-d`
|
||||
- `--thread_length` - number of messages in thread. Defaults to: 2000. ALias `-t`
|
||||
"""
|
||||
|
||||
@aliases [u: :users, d: :dms, t: :thread_length]
|
||||
@switches [
|
||||
users: :integer,
|
||||
dms: :integer,
|
||||
thread_length: :integer
|
||||
]
|
||||
@users_default 20_000
|
||||
@dms_default 1_000
|
||||
@thread_length_default 2_000
|
||||
|
||||
def run(args) do
|
||||
start_pleroma()
|
||||
Pleroma.Config.put([:instance, :skip_thread_containment], true)
|
||||
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
|
||||
|
||||
users_max = Keyword.get(opts, :users, @users_default)
|
||||
dms_max = Keyword.get(opts, :dms, @dms_default)
|
||||
thread_length = Keyword.get(opts, :thread_length, @thread_length_default)
|
||||
|
||||
clean_tables()
|
||||
|
||||
opts =
|
||||
Keyword.put(opts, :users_max, users_max)
|
||||
|> Keyword.put(:dms_max, dms_max)
|
||||
|> Keyword.put(:thread_length, thread_length)
|
||||
|
||||
generate_users(opts)
|
||||
|
||||
# main user for queries
|
||||
IO.puts("Fetching local main user...")
|
||||
|
||||
{time, user} =
|
||||
:timer.tc(fn ->
|
||||
Repo.one(
|
||||
from(u in User, where: u.local == true, order_by: fragment("RANDOM()"), limit: 1)
|
||||
)
|
||||
end)
|
||||
|
||||
IO.puts("Fetching main user take #{to_sec(time)} sec.\n")
|
||||
|
||||
IO.puts("Fetching local users...")
|
||||
|
||||
{time, users} =
|
||||
:timer.tc(fn ->
|
||||
Repo.all(
|
||||
from(u in User,
|
||||
where: u.id != ^user.id,
|
||||
where: u.local == true,
|
||||
order_by: fragment("RANDOM()"),
|
||||
limit: 10
|
||||
)
|
||||
)
|
||||
end)
|
||||
|
||||
IO.puts("Fetching local users take #{to_sec(time)} sec.\n")
|
||||
|
||||
IO.puts("Fetching remote users...")
|
||||
|
||||
{time, remote_users} =
|
||||
:timer.tc(fn ->
|
||||
Repo.all(
|
||||
from(u in User,
|
||||
where: u.id != ^user.id,
|
||||
where: u.local == false,
|
||||
order_by: fragment("RANDOM()"),
|
||||
limit: 10
|
||||
)
|
||||
)
|
||||
end)
|
||||
|
||||
IO.puts("Fetching remote users take #{to_sec(time)} sec.\n")
|
||||
|
||||
generate_activities(user, users)
|
||||
|
||||
generate_remote_activities(user, remote_users)
|
||||
|
||||
generate_dms(user, users, opts)
|
||||
|
||||
{:ok, activity} = generate_long_thread(user, users, opts)
|
||||
|
||||
generate_non_visible_message(user, users)
|
||||
|
||||
IO.puts("Users in DB: #{Repo.aggregate(from(u in User), :count, :id)}")
|
||||
|
||||
IO.puts("Activities in DB: #{Repo.aggregate(from(a in Pleroma.Activity), :count, :id)}")
|
||||
|
||||
IO.puts("Objects in DB: #{Repo.aggregate(from(o in Pleroma.Object), :count, :id)}")
|
||||
|
||||
IO.puts(
|
||||
"Notifications in DB: #{Repo.aggregate(from(n in Pleroma.Notification), :count, :id)}"
|
||||
)
|
||||
|
||||
fetch_user(user)
|
||||
query_timelines(user)
|
||||
query_notifications(user)
|
||||
query_dms(user)
|
||||
query_long_thread(user, activity)
|
||||
Pleroma.Config.put([:instance, :skip_thread_containment], false)
|
||||
query_timelines(user)
|
||||
end
|
||||
|
||||
defp clean_tables do
|
||||
IO.puts("Deleting old data...\n")
|
||||
Ecto.Adapters.SQL.query!(Repo, "TRUNCATE users CASCADE;")
|
||||
Ecto.Adapters.SQL.query!(Repo, "TRUNCATE activities CASCADE;")
|
||||
Ecto.Adapters.SQL.query!(Repo, "TRUNCATE objects CASCADE;")
|
||||
end
|
||||
end
|
@ -0,0 +1,84 @@
|
||||
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],
|
||||
url: [port: 4001],
|
||||
server: true
|
||||
|
||||
# Disable captha for tests
|
||||
config :pleroma, Pleroma.Captcha,
|
||||
# It should not be enabled for automatic tests
|
||||
enabled: false,
|
||||
# A fake captcha service for tests
|
||||
method: Pleroma.Captcha.Mock
|
||||
|
||||
# Print only warnings and errors during test
|
||||
config :logger, level: :warn
|
||||
|
||||
config :pleroma, :auth, oauth_consumer_strategies: []
|
||||
|
||||
config :pleroma, Pleroma.Upload, filters: [], link_name: false
|
||||
|
||||
config :pleroma, Pleroma.Uploaders.Local, uploads: "test/uploads"
|
||||
|
||||
config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Test, enabled: true
|
||||
|
||||
config :pleroma, :instance,
|
||||
email: "admin@example.com",
|
||||
notify_email: "noreply@example.com",
|
||||
skip_thread_containment: false,
|
||||
federating: false,
|
||||
external_user_synchronization: false
|
||||
|
||||
config :pleroma, :activitypub, sign_object_fetches: false
|
||||
|
||||
# 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_size: 10
|
||||
|
||||
# Reduce hash rounds for testing
|
||||
config :pbkdf2_elixir, rounds: 1
|
||||
|
||||
config :tesla, adapter: Tesla.Mock
|
||||
|
||||
config :pleroma, :rich_media,
|
||||
enabled: false,
|
||||
ignore_hosts: [],
|
||||
ignore_tld: ["local", "localdomain", "lan"]
|
||||
|
||||
config :web_push_encryption, :vapid_details,
|
||||
subject: "mailto:administrator@example.com",
|
||||
public_key:
|
||||
"BLH1qVhJItRGCfxgTtONfsOKDc9VRAraXw-3NsmjMngWSh7NxOizN6bkuRA7iLTMPS82PjwJAr3UoK9EC1IFrz4",
|
||||
private_key: "_-XZ0iebPrRfZ_o0-IatTdszYa8VCH1yLN-JauK7HHA"
|
||||
|
||||
config :web_push_encryption, :http_client, Pleroma.Web.WebPushHttpClientMock
|
||||
|
||||
config :pleroma_job_queue, disabled: true
|
||||
|
||||
config :pleroma, Pleroma.ScheduledActivity,
|
||||
daily_user_limit: 2,
|
||||
total_user_limit: 3,
|
||||
enabled: false
|
||||
|
||||
config :pleroma, :rate_limit,
|
||||
search: [{1000, 30}, {1000, 30}],
|
||||
app_account_creation: {10_000, 5},
|
||||
password_reset: {1000, 30}
|
||||
|
||||
config :pleroma, :http_security, report_uri: "https://endpoint.com"
|
||||
|
||||
config :pleroma, :http, send_user_agent: false
|
||||
|
||||
rum_enabled = System.get_env("RUM_ENABLED") == "true"
|
||||
config :pleroma, :database, rum_enabled: rum_enabled
|
||||
IO.puts("RUM enabled: #{rum_enabled}")
|
||||
|
||||
config :pleroma, Pleroma.ReverseProxy.Client, Pleroma.ReverseProxy.ClientMock
|
@ -0,0 +1,110 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.FollowingRelationship do
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
import Ecto.Query
|
||||
|
||||
alias FlakeId.Ecto.CompatType
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
|
||||
schema "following_relationships" do
|
||||
field(:state, :string, default: "accept")
|
||||
|
||||
belongs_to(:follower, User, type: CompatType)
|
||||
belongs_to(:following, User, type: CompatType)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
def changeset(%__MODULE__{} = following_relationship, attrs) do
|
||||
following_relationship
|
||||
|> cast(attrs, [:state])
|
||||
|> put_assoc(:follower, attrs.follower)
|
||||
|> put_assoc(:following, attrs.following)
|
||||
|> validate_required([:state, :follower, :following])
|
||||
end
|
||||
|
||||
def get(%User{} = follower, %User{} = following) do
|
||||
__MODULE__
|
||||
|> where(follower_id: ^follower.id, following_id: ^following.id)
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
def update(follower, following, "reject"), do: unfollow(follower, following)
|
||||
|
||||
def update(%User{} = follower, %User{} = following, state) do
|
||||
case get(follower, following) do
|
||||
nil ->
|
||||
follow(follower, following, state)
|
||||
|
||||
following_relationship ->
|
||||
following_relationship
|
||||
|> cast(%{state: state}, [:state])
|
||||
|> validate_required([:state])
|
||||
|> Repo.update()
|
||||
end
|
||||
end
|
||||
|
||||
def follow(%User{} = follower, %User{} = following, state \\ "accept") do
|
||||
%__MODULE__{}
|
||||
|> changeset(%{follower: follower, following: following, state: state})
|
||||
|> Repo.insert(on_conflict: :nothing)
|
||||
end
|
||||
|
||||
def unfollow(%User{} = follower, %User{} = following) do
|
||||
case get(follower, following) do
|
||||
nil -> {:ok, nil}
|
||||
%__MODULE__{} = following_relationship -> Repo.delete(following_relationship)
|
||||
end
|
||||
end
|
||||
|
||||
def follower_count(%User{} = user) do
|
||||
%{followers: user, deactivated: false}
|
||||
|> User.Query.build()
|
||||
|> Repo.aggregate(:count, :id)
|
||||
end
|
||||
|
||||
def following_count(%User{id: nil}), do: 0
|
||||
|
||||
def following_count(%User{} = user) do
|
||||
%{friends: user, deactivated: false}
|
||||
|> User.Query.build()
|
||||
|> Repo.aggregate(:count, :id)
|
||||
end
|
||||
|
||||
def get_follow_requests(%User{id: id}) do
|
||||
__MODULE__
|
||||
|> join(:inner, [r], f in assoc(r, :follower))
|
||||
|> where([r], r.state == "pending")
|
||||
|> where([r], r.following_id == ^id)
|
||||
|> select([r, f], f)
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
def following?(%User{id: follower_id}, %User{id: followed_id}) do
|
||||
__MODULE__
|
||||
|> where(follower_id: ^follower_id, following_id: ^followed_id, state: "accept")
|
||||
|> Repo.exists?()
|
||||
end
|
||||
|
||||
def following(%User{} = user) do
|
||||
following =
|
||||
__MODULE__
|
||||
|> join(:inner, [r], u in User, on: r.following_id == u.id)
|
||||
|> where([r], r.follower_id == ^user.id)
|
||||
|> where([r], r.state == "accept")
|
||||
|> select([r, u], u.follower_address)
|
||||
|> Repo.all()
|
||||
|
||||
if not user.local or user.nickname in [nil, "internal.fetch"] do
|
||||
following
|
||||
else
|
||||
[user.follower_address | following]
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,74 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Marker do
|
||||
use Ecto.Schema
|
||||
|
||||
import Ecto.Changeset
|
||||
import Ecto.Query
|
||||
|
||||
alias Ecto.Multi
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
|
||||
@timelines ["notifications"]
|
||||
|
||||
schema "markers" do
|
||||
field(:last_read_id, :string, default: "")
|
||||
field(:timeline, :string, default: "")
|
||||
field(:lock_version, :integer, default: 0)
|
||||
|
||||
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
|
||||
timestamps()
|
||||
end
|
||||
|
||||
def get_markers(user, timelines \\ []) do
|
||||
Repo.all(get_query(user, timelines))
|
||||
end
|
||||
|
||||
def upsert(%User{} = user, attrs) do
|
||||
attrs
|
||||
|> Map.take(@timelines)
|
||||
|> Enum.reduce(Multi.new(), fn {timeline, timeline_attrs}, multi ->
|
||||
marker =
|
||||
user
|
||||
|> get_marker(timeline)
|
||||
|> changeset(timeline_attrs)
|
||||
|
||||
Multi.insert(multi, timeline, marker,
|
||||
returning: true,
|
||||
on_conflict: {:replace, [:last_read_id]},
|
||||
conflict_target: [:user_id, :timeline]
|
||||
)
|
||||
end)
|
||||
|> Repo.transaction()
|
||||
end
|
||||
|
||||
defp get_marker(user, timeline) do
|
||||
case Repo.find_resource(get_query(user, timeline)) do
|
||||
{:ok, marker} -> %__MODULE__{marker | user: user}
|
||||
_ -> %__MODULE__{timeline: timeline, user_id: user.id}
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
defp changeset(marker, attrs) do
|
||||
marker
|
||||
|> cast(attrs, [:last_read_id])
|
||||
|> validate_required([:user_id, :timeline, :last_read_id])
|
||||
|> validate_inclusion(:timeline, @timelines)
|
||||
end
|
||||
|
||||
defp by_timeline(query, timeline) do
|
||||
from(m in query, where: m.timeline in ^List.wrap(timeline))
|
||||
end
|
||||
|
||||
defp by_user_id(query, id), do: from(m in query, where: m.user_id == ^id)
|
||||
|
||||
defp get_query(user, timelines) do
|
||||
__MODULE__
|
||||
|> by_user_id(user.id)
|
||||
|> by_timeline(timelines)
|
||||
end
|
||||
end
|
File diff suppressed because it is too large
Load Diff
@ -1,478 +0,0 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.User.Info do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
|
||||
alias Pleroma.User.Info
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
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)
|
||||
# Should be filled in only for remote users
|
||||
field(:following_count, :integer, default: nil)
|
||||
field(:locked, :boolean, default: false)
|
||||
field(:confirmation_pending, :boolean, default: false)
|
||||
field(:password_reset_pending, :boolean, default: false)
|
||||
field(:confirmation_token, :string, default: nil)
|
||||
field(:default_scope, :string, default: "public")
|
||||
field(:blocks, {:array, :string}, default: [])
|
||||
field(:domain_blocks, {:array, :string}, default: [])
|
||||
field(:mutes, {:array, :string}, default: [])
|
||||
field(:muted_reblogs, {:array, :string}, default: [])
|
||||
field(:muted_notifications, {:array, :string}, default: [])
|
||||
field(:subscribers, {: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(:show_role, :boolean, default: true)
|
||||
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_followers_count, :boolean, default: false)
|
||||
field(:hide_follows_count, :boolean, default: false)
|
||||
field(:hide_followers, :boolean, default: false)
|
||||
field(:hide_follows, :boolean, default: false)
|
||||
field(:hide_favorites, :boolean, default: true)
|
||||
field(:unread_conversation_count, :integer, default: 0)
|
||||
field(:pinned_activities, {:array, :string}, default: [])
|
||||
field(:email_notifications, :map, default: %{"digest" => false})
|
||||
field(:mascot, :map, default: nil)
|
||||
field(:emoji, {:array, :map}, default: [])
|
||||
field(:pleroma_settings_store, :map, default: %{})
|
||||
field(:fields, {:array, :map}, default: nil)
|
||||
field(:raw_fields, {:array, :map}, default: [])
|
||||
field(:discoverable, :boolean, default: false)
|
||||
|
||||
field(:notification_settings, :map,
|
||||
default: %{
|
||||
"followers" => true,
|
||||
"follows" => true,
|
||||
"non_follows" => true,
|
||||
"non_followers" => true
|
||||
}
|
||||
)
|
||||
|
||||
field(:skip_thread_containment, :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 set_password_reset_pending(info, pending) do
|
||||
params = %{password_reset_pending: pending}
|
||||
|
||||
info
|
||||
|> cast(params, [:password_reset_pending])
|
||||
|> validate_required([:password_reset_pending])
|
||||
end
|
||||
|
||||
def update_notification_settings(info, settings) do
|
||||
settings =
|
||||
settings
|
||||
|> Enum.map(fn {k, v} -> {k, v in [true, "true", "True", "1"]} end)
|
||||
|> Map.new()
|
||||
|
||||
notification_settings =
|
||||
info.notification_settings
|
||||
|> Map.merge(settings)
|
||||
|> Map.take(["followers", "follows", "non_follows", "non_followers"])
|
||||
|
||||
params = %{notification_settings: notification_settings}
|
||||
|
||||
info
|
||||
|> cast(params, [:notification_settings])
|
||||
|> validate_required([:notification_settings])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Update email notifications in the given User.Info struct.
|
||||
|
||||
Examples:
|
||||
|
||||
iex> update_email_notifications(%Pleroma.User.Info{email_notifications: %{"digest" => false}}, %{"digest" => true})
|
||||
%Pleroma.User.Info{email_notifications: %{"digest" => true}}
|
||||
|
||||
"""
|
||||
@spec update_email_notifications(t(), map()) :: Ecto.Changeset.t()
|
||||
def update_email_notifications(info, settings) do
|
||||
email_notifications =
|
||||
info.email_notifications
|
||||
|> Map.merge(settings)
|
||||
|> Map.take(["digest"])
|
||||
|
||||
params = %{email_notifications: email_notifications}
|
||||
fields = [:email_notifications]
|
||||
|
||||
info
|
||||
|> cast(params, fields)
|
||||
|> validate_required(fields)
|
||||
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_mutes(info, mutes) do
|
||||
params = %{mutes: mutes}
|
||||
|
||||
info
|
||||
|> cast(params, [:mutes])
|
||||
|> validate_required([:mutes])
|
||||
end
|
||||
|
||||
@spec set_notification_mutes(Changeset.t(), [String.t()], boolean()) :: Changeset.t()
|
||||
def set_notification_mutes(changeset, muted_notifications, notifications?) do
|
||||
if notifications? do
|
||||
put_change(changeset, :muted_notifications, muted_notifications)
|
||||
|> validate_required([:muted_notifications])
|
||||
else
|
||||
changeset
|
||||
end
|
||||
end
|
||||
|
||||
def set_blocks(info, blocks) do
|
||||
params = %{blocks: blocks}
|
||||
|
||||
info
|
||||
|> cast(params, [:blocks])
|
||||
|> validate_required([:blocks])
|
||||
end
|
||||
|
||||
def set_subscribers(info, subscribers) do
|
||||
params = %{subscribers: subscribers}
|
||||
|
||||
info
|
||||
|> cast(params, [:subscribers])
|
||||
|> validate_required([:subscribers])
|
||||
end
|
||||
|
||||
@spec add_to_mutes(Info.t(), String.t(), boolean()) :: Changeset.t()
|
||||
def add_to_mutes(info, muted, notifications?) do
|
||||
info
|
||||
|> set_mutes(Enum.uniq([muted | info.mutes]))
|
||||
|> set_notification_mutes(
|
||||
Enum.uniq([muted | info.muted_notifications]),
|
||||
notifications?
|
||||
)
|
||||
end
|
||||
|
||||
@spec remove_from_mutes(Info.t(), String.t()) :: Changeset.t()
|
||||
def remove_from_mutes(info, muted) do
|
||||
info
|
||||
|> set_mutes(List.delete(info.mutes, muted))
|
||||
|> set_notification_mutes(List.delete(info.muted_notifications, muted), true)
|
||||
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 add_to_subscribers(info, subscribed) do
|
||||
set_subscribers(info, Enum.uniq([subscribed | info.subscribers]))
|
||||
end
|
||||
|
||||
def remove_from_subscribers(info, subscribed) do
|
||||
set_subscribers(info, List.delete(info.subscribers, subscribed))
|
||||
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
|
||||
params =
|
||||
if Map.has_key?(params, :fields) do
|
||||
Map.put(params, :fields, Enum.map(params[:fields], &truncate_field/1))
|
||||
else
|
||||
params
|
||||
end
|
||||
|
||||
info
|
||||
|> cast(params, [
|
||||
:ap_enabled,
|
||||
:source_data,
|
||||
:banner,
|
||||
:locked,
|
||||
:magic_key,
|
||||
:uri,
|
||||
:hub,
|
||||
:topic,
|
||||
:salmon,
|
||||
:hide_followers,
|
||||
:hide_follows,
|
||||
:hide_followers_count,
|
||||
:hide_follows_count,
|
||||
:follower_count,
|
||||
:fields,
|
||||
:following_count,
|
||||
:discoverable
|
||||
])
|
||||
|> validate_fields(true)
|
||||
end
|
||||
|
||||
def user_upgrade(info, params, remote? \\ false) do
|
||||
info
|
||||
|> cast(params, [
|
||||
:ap_enabled,
|
||||
:source_data,
|
||||
:banner,
|
||||
:locked,
|
||||
:magic_key,
|
||||
:follower_count,
|
||||
:following_count,
|
||||
:hide_follows,
|
||||
:fields,
|
||||
:hide_followers,
|
||||
:discoverable,
|
||||
:hide_followers_count,
|
||||
:hide_follows_count
|
||||
])
|
||||
|> validate_fields(remote?)
|
||||
end
|
||||
|
||||
def profile_update(info, params) do
|
||||
info
|
||||
|> cast(params, [
|
||||
:locked,
|
||||
:no_rich_text,
|
||||
:default_scope,
|
||||
:banner,
|
||||
:hide_follows,
|
||||
:hide_followers,
|
||||
:hide_followers_count,
|
||||
:hide_follows_count,
|
||||
:hide_favorites,
|
||||
:background,
|
||||
:show_role,
|
||||
:skip_thread_containment,
|
||||
:fields,
|
||||
:raw_fields,
|
||||
:pleroma_settings_store,
|
||||
:discoverable
|
||||
])
|
||||
|> validate_fields()
|
||||
end
|
||||
|
||||
def validate_fields(changeset, remote? \\ false) do
|
||||
limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
|
||||
limit = Pleroma.Config.get([:instance, limit_name], 0)
|
||||
|
||||
changeset
|
||||
|> validate_length(:fields, max: limit)
|
||||
|> validate_change(:fields, fn :fields, fields ->
|
||||
if Enum.all?(fields, &valid_field?/1) do
|
||||
[]
|
||||
else
|
||||
[fields: "invalid"]
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp valid_field?(%{"name" => name, "value" => value}) do
|
||||
name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
|
||||
value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
|
||||
|
||||
is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
|
||||
String.length(value) <= value_limit
|
||||
end
|
||||
|
||||
defp valid_field?(_), do: false
|
||||
|
||||
defp truncate_field(%{"name" => name, "value" => value}) do
|
||||
{name, _chopped} =
|
||||
String.split_at(name, Pleroma.Config.get([:instance, :account_field_name_length], 255))
|
||||
|
||||
{value, _chopped} =
|
||||
String.split_at(value, Pleroma.Config.get([:instance, :account_field_value_length], 255))
|
||||
|
||||
%{"name" => name, "value" => value}
|
||||
end
|
||||
|
||||
@spec confirmation_changeset(Info.t(), keyword()) :: Changeset.t()
|
||||
def confirmation_changeset(info, opts) do
|
||||
need_confirmation? = Keyword.get(opts, :need_confirmation)
|
||||
|
||||
params =
|
||||
if need_confirmation? do
|
||||
%{
|
||||
confirmation_pending: true,
|
||||
confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64()
|
||||
}
|
||||
else
|
||||
%{
|
||||
confirmation_pending: false,
|
||||
confirmation_token: nil
|
||||
}
|
||||
end
|
||||
|
||||
cast(info, params, [:confirmation_pending, :confirmation_token])
|
||||
end
|
||||
|
||||
def mastodon_settings_update(info, settings) do
|
||||
params = %{settings: settings}
|
||||
|
||||
info
|
||||
|> cast(params, [:settings])
|
||||
|> validate_required([:settings])
|
||||
end
|
||||
|
||||
def mascot_update(info, url) do
|
||||
params = %{mascot: url}
|
||||
|
||||
info
|
||||
|> cast(params, [:mascot])
|
||||
|> validate_required([:mascot])
|
||||
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,
|
||||
:show_role
|
||||
])
|
||||
end
|
||||
|
||||
def add_pinnned_activity(info, %Pleroma.Activity{id: id}) do
|
||||
if id not in info.pinned_activities do
|
||||
max_pinned_statuses = Pleroma.Config.get([:instance, :max_pinned_statuses], 0)
|
||||
params = %{pinned_activities: info.pinned_activities ++ [id]}
|
||||
|
||||
info
|
||||
|> cast(params, [:pinned_activities])
|
||||
|> validate_length(:pinned_activities,
|
||||
max: max_pinned_statuses,
|
||||
message: "You have already pinned the maximum number of statuses"
|
||||
)
|
||||
else
|
||||
change(info)
|
||||
end
|
||||
end
|
||||
|
||||
def remove_pinnned_activity(info, %Pleroma.Activity{id: id}) do
|
||||
params = %{pinned_activities: List.delete(info.pinned_activities, id)}
|
||||
|
||||
cast(info, params, [:pinned_activities])
|
||||
end
|
||||
|
||||
def roles(%Info{is_moderator: is_moderator, is_admin: is_admin}) do
|
||||
%{
|
||||
admin: is_admin,
|
||||
moderator: is_moderator
|
||||
}
|
||||
end
|
||||
|
||||
def add_reblog_mute(info, ap_id) do
|
||||
params = %{muted_reblogs: info.muted_reblogs ++ [ap_id]}
|
||||
|
||||
cast(info, params, [:muted_reblogs])
|
||||
end
|
||||
|
||||
def remove_reblog_mute(info, ap_id) do
|
||||
params = %{muted_reblogs: List.delete(info.muted_reblogs, ap_id)}
|
||||
|
||||
cast(info, params, [:muted_reblogs])
|
||||
end
|
||||
|
||||
# ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``.
|
||||
# For example: [{"name": "Pronoun", "value": "she/her"}, …]
|
||||
def fields(%{fields: nil, source_data: %{"attachment" => attachment}}) do
|
||||
limit = Pleroma.Config.get([:instance, :max_remote_account_fields], 0)
|
||||
|
||||
attachment
|
||||
|> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end)
|
||||
|> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end)
|
||||
|> Enum.take(limit)
|
||||
end
|
||||
|
||||
def fields(%{fields: nil}), do: []
|
||||
|
||||
def fields(%{fields: fields}), do: fields
|
||||
|
||||
def follow_information_update(info, params) do
|
||||
info
|
||||
|> cast(params, [
|
||||
:hide_followers,
|
||||
:hide_follows,
|
||||
:follower_count,
|
||||
:following_count,
|
||||
:hide_followers_count,
|
||||
:hide_follows_count
|
||||
])
|
||||
end
|
||||
end
|
@ -0,0 +1,32 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.MastodonAPI.MarkerController do
|
||||
use Pleroma.Web, :controller
|
||||
alias Pleroma.Plugs.OAuthScopesPlug
|
||||
|
||||
plug(
|
||||
OAuthScopesPlug,
|
||||
%{scopes: ["read:statuses"]}
|
||||
when action == :index
|
||||
)
|
||||
|
||||
plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action == :upsert)
|
||||
plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
|
||||
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
|
||||
|
||||
# GET /api/v1/markers
|
||||
def index(%{assigns: %{user: user}} = conn, params) do
|
||||
markers = Pleroma.Marker.get_markers(user, params["timeline"])
|
||||
render(conn, "markers.json", %{markers: markers})
|
||||
end
|
||||
|
||||
# POST /api/v1/markers
|
||||
def upsert(%{assigns: %{user: user}} = conn, params) do
|
||||
with {:ok, result} <- Pleroma.Marker.upsert(user, params),
|
||||
markers <- Map.values(result) do
|
||||
render(conn, "markers.json", %{markers: markers})
|
||||
end
|
||||
end
|
||||
end
|
@ -0,0 +1,17 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.MastodonAPI.MarkerView do
|
||||
use Pleroma.Web, :view
|
||||
|
||||
def render("markers.json", %{markers: markers}) do
|
||||
Enum.reduce(markers, %{}, fn m, acc ->
|
||||
Map.put_new(acc, m.timeline, %{
|
||||
last_read_id: m.last_read_id,
|
||||
version: m.lock_version,
|
||||
updated_at: NaiveDateTime.to_iso8601(m.updated_at)
|
||||
})
|
||||
end)
|
||||
end
|
||||
end
|
@ -1,313 +0,0 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.OStatus.ActivityRepresenter do
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.OStatus.UserRepresenter
|
||||
|
||||
require Logger
|
||||
require Pleroma.Constants
|
||||
|
||||
defp get_href(id) do
|
||||
with %Object{data: %{"external_url" => external_url}} <- Object.get_cached_by_ap_id(id) do
|
||||
external_url
|
||||
else
|
||||
_e -> id
|
||||
end
|
||||
end
|
||||
|
||||
defp get_in_reply_to(activity) do
|
||||
with %Object{data: %{"inReplyTo" => in_reply_to}} <- Object.normalize(activity) do
|
||||
[
|
||||
{:"thr:in-reply-to",
|
||||
[ref: to_charlist(in_reply_to), href: to_charlist(get_href(in_reply_to))], []}
|
||||
]
|
||||
else
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp get_mentions(to) do
|
||||
Enum.map(to, fn id ->
|
||||
cond do
|
||||
# Special handling for the AP/Ostatus public collections
|
||||
Pleroma.Constants.as_public() == id ->
|
||||
{:link,
|
||||
[
|
||||
rel: "mentioned",
|
||||
"ostatus:object-type": "http://activitystrea.ms/schema/1.0/collection",
|
||||
href: "http://activityschema.org/collection/public"
|
||||
], []}
|
||||
|
||||
# Ostatus doesn't handle follower collections, ignore these.
|
||||
Regex.match?(~r/^#{Pleroma.Web.base_url()}.+followers$/, id) ->
|
||||
[]
|
||||
|
||||
true ->
|
||||
{:link,
|
||||
[
|
||||
rel: "mentioned",
|
||||
"ostatus:object-type": "http://activitystrea.ms/schema/1.0/person",
|
||||
href: id
|
||||
], []}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp get_links(%{local: true}, %{"id" => object_id}) do
|
||||
h = fn str -> [to_charlist(str)] end
|
||||
|
||||
[
|
||||
{:link, [type: ['application/atom+xml'], href: h.(object_id), rel: 'self'], []},
|
||||
{:link, [type: ['text/html'], href: h.(object_id), rel: 'alternate'], []}
|
||||
]
|
||||
end
|
||||
|
||||
defp get_links(%{local: false}, %{"external_url" => external_url}) do
|
||||
h = fn str -> [to_charlist(str)] end
|
||||
|
||||
[
|
||||
{:link, [type: ['text/html'], href: h.(external_url), rel: 'alternate'], []}
|
||||
]
|
||||
end
|
||||
|
||||
defp get_links(_activity, _object_data), do: []
|
||||
|
||||
defp get_emoji_links(emojis) do
|
||||
Enum.map(emojis, fn {emoji, file} ->
|
||||
{:link, [name: to_charlist(emoji), rel: 'emoji', href: to_charlist(file)], []}
|
||||
end)
|
||||
end
|
||||
|
||||
def to_simple_form(activity, user, with_author \\ false)
|
||||
|
||||
def to_simple_form(%{data: %{"type" => "Create"}} = activity, user, with_author) do
|
||||
h = fn str -> [to_charlist(str)] end
|
||||
|
||||
object = Object.normalize(activity)
|
||||
|
||||
updated_at = object.data["published"]
|
||||
inserted_at = object.data["published"]
|
||||
|
||||
attachments =
|
||||
Enum.map(object.data["attachment"] || [], fn attachment ->
|
||||
url = hd(attachment["url"])
|
||||
|
||||
{:link,
|
||||
[rel: 'enclosure', href: to_charlist(url["href"]), type: to_charlist(url["mediaType"])],
|
||||
[]}
|
||||
end)
|
||||
|
||||
in_reply_to = get_in_reply_to(activity)
|
||||
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
|
||||
mentions = activity.recipients |> get_mentions
|
||||
|
||||
categories =
|
||||
(object.data["tag"] || [])
|
||||
|> Enum.map(fn tag ->
|
||||
if is_binary(tag) do
|
||||
{:category, [term: to_charlist(tag)], []}
|
||||
else
|
||||
nil
|
||||
end
|
||||
end)
|
||||
|> Enum.filter(& &1)
|
||||
|
||||
emoji_links = get_emoji_links(object.data["emoji"] || %{})
|
||||
|
||||
summary =
|
||||
if object.data["summary"] do
|
||||
[{:summary, [], h.(object.data["summary"])}]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
[
|
||||
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/note']},
|
||||
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/post']},
|
||||
# For notes, federate the object id.
|
||||
{:id, h.(object.data["id"])},
|
||||
{:title, ['New note by #{user.nickname}']},
|
||||
{:content, [type: 'html'], h.(object.data["content"] |> String.replace(~r/[\n\r]/, ""))},
|
||||
{:published, h.(inserted_at)},
|
||||
{:updated, h.(updated_at)},
|
||||
{:"ostatus:conversation", [ref: h.(activity.data["context"])],
|
||||
h.(activity.data["context"])},
|
||||
{:link, [ref: h.(activity.data["context"]), rel: 'ostatus:conversation'], []}
|
||||
] ++
|
||||
summary ++
|
||||
get_links(activity, object.data) ++
|
||||
categories ++ attachments ++ in_reply_to ++ author ++ mentions ++ emoji_links
|
||||
end
|
||||
|
||||
def to_simple_form(%{data: %{"type" => "Like"}} = activity, user, with_author) do
|
||||
h = fn str -> [to_charlist(str)] end
|
||||
|
||||
updated_at = activity.data["published"]
|
||||
inserted_at = activity.data["published"]
|
||||
|
||||
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
|
||||
mentions = activity.recipients |> get_mentions
|
||||
|
||||
[
|
||||
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/favorite']},
|
||||
{:id, h.(activity.data["id"])},
|
||||
{:title, ['New favorite by #{user.nickname}']},
|
||||
{:content, [type: 'html'], ['#{user.nickname} favorited something']},
|
||||
{:published, h.(inserted_at)},
|
||||
{:updated, h.(updated_at)},
|
||||
{:"activity:object",
|
||||
[
|
||||
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/note']},
|
||||
# For notes, federate the object id.
|
||||
{:id, h.(activity.data["object"])}
|
||||
]},
|
||||
{:"ostatus:conversation", [ref: h.(activity.data["context"])],
|
||||
h.(activity.data["context"])},
|
||||
{:link, [ref: h.(activity.data["context"]), rel: 'ostatus:conversation'], []},
|
||||
{:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []},
|
||||
{:"thr:in-reply-to", [ref: to_charlist(activity.data["object"])], []}
|
||||
] ++ author ++ mentions
|
||||
end
|
||||
|
||||
def to_simple_form(%{data: %{"type" => "Announce"}} = activity, user, with_author) do
|
||||
h = fn str -> [to_charlist(str)] end
|
||||
|
||||
updated_at = activity.data["published"]
|
||||
inserted_at = activity.data["published"]
|
||||
|
||||
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
|
||||
|
||||
retweeted_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
|
||||
retweeted_object = Object.normalize(retweeted_activity)
|
||||
retweeted_user = User.get_cached_by_ap_id(retweeted_activity.data["actor"])
|
||||
|
||||
retweeted_xml = to_simple_form(retweeted_activity, retweeted_user, true)
|
||||
|
||||
mentions =
|
||||
([retweeted_user.ap_id] ++ activity.recipients)
|
||||
|> Enum.uniq()
|
||||
|> get_mentions()
|
||||
|
||||
[
|
||||
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']},
|
||||
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/share']},
|
||||
{:id, h.(activity.data["id"])},
|
||||
{:title, ['#{user.nickname} repeated a notice']},
|
||||
{:content, [type: 'html'], ['RT #{retweeted_object.data["content"]}']},
|
||||
{:published, h.(inserted_at)},
|
||||
{:updated, h.(updated_at)},
|
||||
{:"ostatus:conversation", [ref: h.(activity.data["context"])],
|
||||
h.(activity.data["context"])},
|
||||
{:link, [ref: h.(activity.data["context"]), rel: 'ostatus:conversation'], []},
|
||||
{:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []},
|
||||
{:"activity:object", retweeted_xml}
|
||||
] ++ mentions ++ author
|
||||
end
|
||||
|
||||
def to_simple_form(%{data: %{"type" => "Follow"}} = activity, user, with_author) do
|
||||
h = fn str -> [to_charlist(str)] end
|
||||
|
||||
updated_at = activity.data["published"]
|
||||
inserted_at = activity.data["published"]
|
||||
|
||||
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
|
||||
|
||||
mentions = (activity.recipients || []) |> get_mentions
|
||||
|
||||
[
|
||||
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']},
|
||||
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/follow']},
|
||||
{:id, h.(activity.data["id"])},
|
||||
{:title, ['#{user.nickname} started following #{activity.data["object"]}']},
|
||||
{:content, [type: 'html'],
|
||||
['#{user.nickname} started following #{activity.data["object"]}']},
|
||||
{:published, h.(inserted_at)},
|
||||
{:updated, h.(updated_at)},
|
||||
{:"activity:object",
|
||||
[
|
||||
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/person']},
|
||||
{:id, h.(activity.data["object"])},
|
||||
{:uri, h.(activity.data["object"])}
|
||||
]},
|
||||
{:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []}
|
||||
] ++ mentions ++ author
|
||||
end
|
||||
|
||||
# Only undos of follow for now. Will need to get redone once there are more
|
||||
def to_simple_form(
|
||||
%{data: %{"type" => "Undo", "object" => %{"type" => "Follow"} = follow_activity}} =
|
||||
activity,
|
||||
user,
|
||||
with_author
|
||||
) do
|
||||
h = fn str -> [to_charlist(str)] end
|
||||
|
||||
updated_at = activity.data["published"]
|
||||
inserted_at = activity.data["published"]
|
||||
|
||||
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
|
||||
|
||||
mentions = (activity.recipients || []) |> get_mentions
|
||||
follow_activity = Activity.normalize(follow_activity)
|
||||
|
||||
[
|
||||
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']},
|
||||
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/unfollow']},
|
||||
{:id, h.(activity.data["id"])},
|
||||
{:title, ['#{user.nickname} stopped following #{follow_activity.data["object"]}']},
|
||||
{:content, [type: 'html'],
|
||||
['#{user.nickname} stopped following #{follow_activity.data["object"]}']},
|
||||
{:published, h.(inserted_at)},
|
||||
{:updated, h.(updated_at)},
|
||||
{:"activity:object",
|
||||
[
|
||||
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/person']},
|
||||
{:id, h.(follow_activity.data["object"])},
|
||||
{:uri, h.(follow_activity.data["object"])}
|
||||
]},
|
||||
{:link, [rel: 'self', type: ['application/atom+xml'], href: h.(activity.data["id"])], []}
|
||||
] ++ mentions ++ author
|
||||
end
|
||||
|
||||
def to_simple_form(%{data: %{"type" => "Delete"}} = activity, user, with_author) do
|
||||
h = fn str -> [to_charlist(str)] end
|
||||
|
||||
updated_at = activity.data["published"]
|
||||
inserted_at = activity.data["published"]
|
||||
|
||||
author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: []
|
||||
|
||||
[
|
||||
{:"activity:object-type", ['http://activitystrea.ms/schema/1.0/activity']},
|
||||
{:"activity:verb", ['http://activitystrea.ms/schema/1.0/delete']},
|
||||
{:id, h.(activity.data["object"])},
|
||||
{:title, ['An object was deleted']},
|
||||
{:content, [type: 'html'], ['An object was deleted']},
|
||||
{:published, h.(inserted_at)},
|
||||
{:updated, h.(updated_at)}
|
||||
] ++ author
|
||||
end
|
||||
|
||||
def to_simple_form(_, _, _), do: nil
|
||||
|
||||
def wrap_with_entry(simple_form) do
|
||||
[
|
||||
{
|
||||
:entry,
|
||||
[
|
||||
xmlns: 'http://www.w3.org/2005/Atom',
|
||||
"xmlns:thr": 'http://purl.org/syndication/thread/1.0',
|
||||
"xmlns:activity": 'http://activitystrea.ms/spec/1.0/',
|
||||
"xmlns:poco": 'http://portablecontacts.net/spec/1.0',
|
||||
"xmlns:ostatus": 'http://ostatus.org/schema/1.0'
|
||||
],
|
||||
simple_form
|
||||
}
|
||||
]
|
||||
end
|
||||
end
|
@ -1,66 +0,0 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.OStatus.FeedRepresenter do
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.MediaProxy
|
||||
alias Pleroma.Web.OStatus
|
||||
alias Pleroma.Web.OStatus.ActivityRepresenter
|
||||
alias Pleroma.Web.OStatus.UserRepresenter
|
||||
|
||||
def to_simple_form(user, activities, _users) do
|
||||
most_recent_update =
|
||||
(List.first(activities) || user).updated_at
|
||||
|> NaiveDateTime.to_iso8601()
|
||||
|
||||
h = fn str -> [to_charlist(str)] end
|
||||
|
||||
last_activity = List.last(activities)
|
||||
|
||||
entries =
|
||||
activities
|
||||
|> Enum.map(fn activity ->
|
||||
{:entry, ActivityRepresenter.to_simple_form(activity, user)}
|
||||
end)
|
||||
|> Enum.filter(fn {_, form} -> form end)
|
||||
|
||||
[
|
||||
{
|
||||
:feed,
|
||||
[
|
||||
xmlns: 'http://www.w3.org/2005/Atom',
|
||||
"xmlns:thr": 'http://purl.org/syndication/thread/1.0',
|
||||
"xmlns:activity": 'http://activitystrea.ms/spec/1.0/',
|
||||
"xmlns:poco": 'http://portablecontacts.net/spec/1.0',
|
||||
"xmlns:ostatus": 'http://ostatus.org/schema/1.0'
|
||||
],
|
||||
[
|
||||
{:id, h.(OStatus.feed_path(user))},
|
||||
{:title, ['#{user.nickname}\'s timeline']},
|
||||
{:updated, h.(most_recent_update)},
|
||||
{:logo, [to_charlist(User.avatar_url(user) |> MediaProxy.url())]},
|
||||
{:link, [rel: 'hub', href: h.(OStatus.pubsub_path(user))], []},
|
||||
{:link, [rel: 'salmon', href: h.(OStatus.salmon_path(user))], []},
|
||||
{:link, [rel: 'self', href: h.(OStatus.feed_path(user)), type: 'application/atom+xml'],
|
||||
[]},
|
||||
{:author, UserRepresenter.to_simple_form(user)}
|
||||
] ++
|
||||
if last_activity do
|
||||
[
|
||||
{:link,
|
||||
[
|
||||
rel: 'next',
|
||||
href:
|
||||
to_charlist(OStatus.feed_path(user)) ++
|
||||
'?max_id=' ++ to_charlist(last_activity.id),
|
||||
type: 'application/atom+xml'
|
||||
], []}
|
||||
]
|
||||
else
|
||||
[]
|
||||
end ++ entries
|
||||
}
|
||||
]
|
||||
end
|
||||
end
|
@ -1,18 +0,0 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.OStatus.DeleteHandler do
|
||||
require Logger
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.XML
|
||||
|
||||
def handle_delete(entry, _doc \\ nil) do
|
||||
with id <- XML.string_from_xpath("//id", entry),
|
||||
%Object{} = object <- Object.normalize(id),
|
||||
{:ok, delete} <- ActivityPub.delete(object, local: false) do
|
||||
delete
|
||||
end
|
||||
end
|
||||
end
|
@ -1,26 +0,0 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.OStatus.FollowHandler do
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.OStatus
|
||||
alias Pleroma.Web.XML
|
||||
|
||||
def handle(entry, doc) do
|
||||
with {:ok, actor} <- OStatus.find_make_or_update_actor(doc),
|
||||
id when not is_nil(id) <- XML.string_from_xpath("/entry/id", entry),
|
||||
followed_uri when not is_nil(followed_uri) <-
|
||||
XML.string_from_xpath("/entry/activity:object/id", entry),
|
||||
{:ok, followed} <- OStatus.find_or_make_user(followed_uri),
|
||||
{:locked, false} <- {:locked, followed.info.locked},
|
||||
{:ok, activity} <- ActivityPub.follow(actor, followed, id, false) do
|
||||
User.follow(actor, followed)
|
||||
{:ok, activity}
|
||||
else
|
||||
{:locked, true} ->
|
||||
{:error, "It's not possible to follow locked accounts over OStatus"}
|
||||
end
|
||||
end
|
||||
end
|
@ -1,168 +0,0 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.OStatus.NoteHandler do
|
||||
require Logger
|
||||
require Pleroma.Constants
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.Federator
|
||||
alias Pleroma.Web.OStatus
|
||||
alias Pleroma.Web.XML
|
||||
|
||||
@doc """
|
||||
Get the context for this note. Uses this:
|
||||
1. The context of the parent activity
|
||||
2. The conversation reference in the ostatus xml
|
||||
3. A newly generated context id.
|
||||
"""
|
||||
def get_context(entry, in_reply_to) do
|
||||
context =
|
||||
(XML.string_from_xpath("//ostatus:conversation[1]", entry) ||
|
||||
XML.string_from_xpath("//ostatus:conversation[1]/@ref", entry) || "")
|
||||
|> String.trim()
|
||||
|
||||
with %{data: %{"context" => context}} <- Object.get_cached_by_ap_id(in_reply_to) do
|
||||
context
|
||||
else
|
||||
_e ->
|
||||
if String.length(context) > 0 do
|
||||
context
|
||||
else
|
||||
Utils.generate_context_id()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def get_people_mentions(entry) do
|
||||
:xmerl_xpath.string(
|
||||
'//link[@rel="mentioned" and @ostatus:object-type="http://activitystrea.ms/schema/1.0/person"]',
|
||||
entry
|
||||
)
|
||||
|> Enum.map(fn person -> XML.string_from_xpath("@href", person) end)
|
||||
end
|
||||
|
||||
def get_collection_mentions(entry) do
|
||||
transmogrify = fn
|
||||
"http://activityschema.org/collection/public" ->
|
||||
Pleroma.Constants.as_public()
|
||||
|
||||
group ->
|
||||
group
|
||||
end
|
||||
|
||||
:xmerl_xpath.string(
|
||||
'//link[@rel="mentioned" and @ostatus:object-type="http://activitystrea.ms/schema/1.0/collection"]',
|
||||
entry
|
||||
)
|
||||
|> Enum.map(fn collection -> XML.string_from_xpath("@href", collection) |> transmogrify.() end)
|
||||
end
|
||||
|
||||
def get_mentions(entry) do
|
||||
(get_people_mentions(entry) ++ get_collection_mentions(entry))
|
||||
|> Enum.filter(& &1)
|
||||
end
|
||||
|
||||
def get_emoji(entry) do
|
||||
try do
|
||||
:xmerl_xpath.string('//link[@rel="emoji"]', entry)
|
||||
|> Enum.reduce(%{}, fn emoji, acc ->
|
||||
Map.put(acc, XML.string_from_xpath("@name", emoji), XML.string_from_xpath("@href", emoji))
|
||||
end)
|
||||
rescue
|
||||
_e -> nil
|
||||
end
|
||||
end
|
||||
|
||||
def make_to_list(actor, mentions) do
|
||||
[
|
||||
actor.follower_address
|
||||
] ++ mentions
|
||||
end
|
||||
|
||||
def add_external_url(note, entry) do
|
||||
url = XML.string_from_xpath("//link[@rel='alternate' and @type='text/html']/@href", entry)
|
||||
Map.put(note, "external_url", url)
|
||||
end
|
||||
|
||||
def fetch_replied_to_activity(entry, in_reply_to, options \\ []) do
|
||||
with %Activity{} = activity <- Activity.get_create_by_object_ap_id(in_reply_to) do
|
||||
activity
|
||||
else
|
||||
_e ->
|
||||
with true <- Federator.allowed_incoming_reply_depth?(options[:depth]),
|
||||
in_reply_to_href when not is_nil(in_reply_to_href) <-
|
||||
XML.string_from_xpath("//thr:in-reply-to[1]/@href", entry),
|
||||
{:ok, [activity | _]} <- OStatus.fetch_activity_from_url(in_reply_to_href, options) do
|
||||
activity
|
||||
else
|
||||
_e -> nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Clean this up a bit.
|
||||
def handle_note(entry, doc \\ nil, options \\ []) do
|
||||
with id <- XML.string_from_xpath("//id", entry),
|
||||
activity when is_nil(activity) <- Activity.get_create_by_object_ap_id_with_object(id),
|
||||
[author] <- :xmerl_xpath.string('//author[1]', doc),
|
||||
{:ok, actor} <- OStatus.find_make_or_update_actor(author),
|
||||
content_html <- OStatus.get_content(entry),
|
||||
cw <- OStatus.get_cw(entry),
|
||||
in_reply_to <- XML.string_from_xpath("//thr:in-reply-to[1]/@ref", entry),
|
||||
options <- Keyword.put(options, :depth, (options[:depth] || 0) + 1),
|
||||
in_reply_to_activity <- fetch_replied_to_activity(entry, in_reply_to, options),
|
||||
in_reply_to_object <-
|
||||
(in_reply_to_activity && Object.normalize(in_reply_to_activity)) || nil,
|
||||
in_reply_to <- (in_reply_to_object && in_reply_to_object.data["id"]) || in_reply_to,
|
||||
attachments <- OStatus.get_attachments(entry),
|
||||
context <- get_context(entry, in_reply_to),
|
||||
tags <- OStatus.get_tags(entry),
|
||||
mentions <- get_mentions(entry),
|
||||
to <- make_to_list(actor, mentions),
|
||||
date <- XML.string_from_xpath("//published", entry),
|
||||
unlisted <- XML.string_from_xpath("//mastodon:scope", entry) == "unlisted",
|
||||
cc <- if(unlisted, do: [Pleroma.Constants.as_public()], else: []),
|
||||
note <-
|
||||
CommonAPI.Utils.make_note_data(
|
||||
actor.ap_id,
|
||||
to,
|
||||
context,
|
||||
content_html,
|
||||
attachments,
|
||||
in_reply_to_activity,
|
||||
[],
|
||||
cw
|
||||
),
|
||||
note <- note |> Map.put("id", id) |> Map.put("tag", tags),
|
||||
note <- note |> Map.put("published", date),
|
||||
note <- note |> Map.put("emoji", get_emoji(entry)),
|
||||
note <- add_external_url(note, entry),
|
||||
note <- note |> Map.put("cc", cc),
|
||||
# TODO: Handle this case in make_note_data
|
||||
note <-
|
||||
if(
|
||||
in_reply_to && !in_reply_to_activity,
|
||||
do: note |> Map.put("inReplyTo", in_reply_to),
|
||||
else: note
|
||||
) do
|
||||
ActivityPub.create(%{
|
||||
to: to,
|
||||
actor: actor,
|
||||
context: context,
|
||||
object: note,
|
||||
published: date,
|
||||
local: false,
|
||||
additional: %{"cc" => cc}
|
||||
})
|
||||
else
|
||||
%Activity{} = activity -> {:ok, activity}
|
||||
e -> {:error, e}
|
||||
end
|
||||
end
|
||||
end
|
@ -1,22 +0,0 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.OStatus.UnfollowHandler do
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.OStatus
|
||||
alias Pleroma.Web.XML
|
||||
|
||||
def handle(entry, doc) do
|
||||
with {:ok, actor} <- OStatus.find_make_or_update_actor(doc),
|
||||
id when not is_nil(id) <- XML.string_from_xpath("/entry/id", entry),
|
||||
followed_uri when not is_nil(followed_uri) <-
|
||||
XML.string_from_xpath("/entry/activity:object/id", entry),
|
||||
{:ok, followed} <- OStatus.find_or_make_user(followed_uri),
|
||||
{:ok, activity} <- ActivityPub.unfollow(actor, followed, id, false) do
|
||||
User.unfollow(actor, followed)
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
end
|
@ -1,395 +0,0 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.OStatus do
|
||||
import Pleroma.Web.XML
|
||||
require Logger
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.HTTP
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.OStatus.DeleteHandler
|
||||
alias Pleroma.Web.OStatus.FollowHandler
|
||||
alias Pleroma.Web.OStatus.NoteHandler
|
||||
alias Pleroma.Web.OStatus.UnfollowHandler
|
||||
alias Pleroma.Web.WebFinger
|
||||
alias Pleroma.Web.Websub
|
||||
|
||||
def is_representable?(%Activity{} = activity) do
|
||||
object = Object.normalize(activity)
|
||||
|
||||
cond do
|
||||
is_nil(object) ->
|
||||
false
|
||||
|
||||
Visibility.is_public?(activity) && object.data["type"] == "Note" ->
|
||||
true
|
||||
|
||||
true ->
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def feed_path(user), do: "#{user.ap_id}/feed.atom"
|
||||
|
||||
def pubsub_path(user), do: "#{Web.base_url()}/push/hub/#{user.nickname}"
|
||||
|
||||
def salmon_path(user), do: "#{user.ap_id}/salmon"
|
||||
|
||||
def remote_follow_path, do: "#{Web.base_url()}/ostatus_subscribe?acct={uri}"
|
||||
|
||||
def handle_incoming(xml_string, options \\ []) do
|
||||
with doc when doc != :error <- parse_document(xml_string) do
|
||||
with {:ok, actor_user} <- find_make_or_update_actor(doc),
|
||||
do: Pleroma.Instances.set_reachable(actor_user.ap_id)
|
||||
|
||||
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, options),
|
||||
do: activity
|
||||
|
||||
'http://activitystrea.ms/schema/1.0/comment' ->
|
||||
with {:ok, activity} <- NoteHandler.handle_note(entry, doc, options),
|
||||
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_actor(doc),
|
||||
%Object{} = object <- Object.normalize(retweeted_activity),
|
||||
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_actor(doc),
|
||||
%Object{} = object <- Object.normalize(favorited_activity),
|
||||
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_by_object_ap_id_with_object(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
|
||||
case string_from_xpath("/*/summary", entry) do
|
||||
cw when not is_nil(cw) -> cw
|
||||
_ -> 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
|
||||
case string_from_xpath("//author[1]/ap_enabled", doc) do
|
||||
"true" ->
|
||||
Transmogrifier.upgrade_user_from_ap_id(user.ap_id)
|
||||
|
||||
_ ->
|
||||
maybe_update_ostatus(doc, user)
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_update_ostatus(doc, user) do
|
||||
old_data = Map.take(user, [:bio, :avatar, :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_actor(doc) do
|
||||
uri = string_from_xpath("//author/uri[1]", doc)
|
||||
|
||||
with {:ok, %User{} = user} <- find_or_make_user(uri),
|
||||
{:ap_enabled, false} <- {:ap_enabled, User.ap_enabled?(user)} do
|
||||
maybe_update(doc, user)
|
||||
else
|
||||
{:ap_enabled, true} ->
|
||||
{:error, :invalid_protocol}
|
||||
|
||||
_ ->
|
||||
{:error, :unknown_user}
|
||||
end
|
||||
end
|
||||
|
||||
@spec find_or_make_user(String.t()) :: {:ok, User.t()}
|
||||
def find_or_make_user(uri) do
|
||||
case User.get_by_ap_id(uri) do
|
||||
%User{} = user -> {:ok, user}
|
||||
_ -> make_user(uri)
|
||||
end
|
||||
end
|
||||
|
||||
@spec make_user(String.t(), boolean()) :: {:ok, User.t()} | {:error, any()}
|
||||
def make_user(uri, update \\ false) do
|
||||
with {:ok, info} <- gather_user_info(uri) do
|
||||
with false <- update,
|
||||
%User{} = user <- User.get_cached_by_ap_id(info["uri"]) do
|
||||
{:ok, user}
|
||||
else
|
||||
_e -> User.insert_or_update_user(build_user_data(info))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp build_user_data(info) do
|
||||
%{
|
||||
name: info["name"],
|
||||
nickname: info["nickname"] <> "@" <> info["host"],
|
||||
ap_id: info["uri"],
|
||||
info: info,
|
||||
avatar: info["avatar"],
|
||||
bio: info["bio"]
|
||||
}
|
||||
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
|
||||
|
||||
@spec gather_user_info(String.t()) :: {:ok, map()} | {:error, any()}
|
||||
def gather_user_info(username) do
|
||||
with {:ok, webfinger_data} <- WebFinger.finger(username),
|
||||
{:ok, feed_data} <- Websub.gather_feed_data(webfinger_data["topic"]) do
|
||||
data =
|
||||
webfinger_data
|
||||
|> Map.merge(feed_data)
|
||||
|> Map.put("fqn", username)
|
||||
|
||||
{:ok, data}
|
||||
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, options \\ []) do
|
||||
with true <- String.starts_with?(url, "http"),
|
||||
{:ok, %{body: body, status: code}} when code in 200..299 <-
|
||||
HTTP.get(url, [{:Accept, "application/atom+xml"}]) do
|
||||
Logger.debug("Got document from #{url}, handling...")
|
||||
handle_incoming(body, options)
|
||||
else
|
||||
e ->
|
||||
Logger.debug("Couldn't get #{url}: #{inspect(e)}")
|
||||
e
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_activity_from_html_url(url, options \\ []) do
|
||||
Logger.debug("Trying to fetch #{url}")
|
||||
|
||||
with true <- String.starts_with?(url, "http"),
|
||||
{:ok, %{body: body}} <- HTTP.get(url, []),
|
||||
{:ok, atom_url} <- get_atom_url(body) do
|
||||
fetch_activity_from_atom_url(atom_url, options)
|
||||
else
|
||||
e ->
|
||||
Logger.debug("Couldn't get #{url}: #{inspect(e)}")
|
||||
e
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_activity_from_url(url, options \\ []) do
|
||||
with {:ok, [_ | _] = activities} <- fetch_activity_from_atom_url(url, options) do
|
||||
{:ok, activities}
|
||||
else
|
||||
_e -> fetch_activity_from_html_url(url, options)
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
Logger.debug("Couldn't get #{url}: #{inspect(e)}")
|
||||
{:error, "Couldn't get #{url}: #{inspect(e)}"}
|
||||
end
|
||||
end
|
@ -1,41 +0,0 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.OStatus.UserRepresenter do
|
||||
alias Pleroma.User
|
||||
|
||||
def to_simple_form(user) do
|
||||
ap_id = to_charlist(user.ap_id)
|
||||
nickname = to_charlist(user.nickname)
|
||||
name = to_charlist(user.name)
|
||||
bio = to_charlist(user.bio)
|
||||
avatar_url = to_charlist(User.avatar_url(user))
|
||||
|
||||
banner =
|
||||
if banner_url = User.banner_url(user) do
|
||||
[{:link, [rel: 'header', href: banner_url], []}]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
ap_enabled =
|
||||
if user.local do
|
||||
[{:ap_enabled, ['true']}]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
[
|
||||
{:id, [ap_id]},
|
||||
{:"activity:object", ['http://activitystrea.ms/schema/1.0/person']},
|
||||
{:uri, [ap_id]},
|
||||
{:"poco:preferredUsername", [nickname]},
|
||||
{:"poco:displayName", [name]},
|
||||
{:"poco:note", [bio]},
|
||||
{:summary, [bio]},
|
||||
{:name, [nickname]},
|
||||
{:link, [rel: 'avatar', href: avatar_url], []}
|
||||
] ++ banner ++ ap_enabled
|
||||
end
|
||||
end
|
@ -1,254 +0,0 @@
|
||||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Salmon do
|
||||
@behaviour Pleroma.Web.Federator.Publisher
|
||||
|
||||
use Bitwise
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.HTTP
|
||||
alias Pleroma.Instances
|
||||
alias Pleroma.Keys
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.Federator.Publisher
|
||||
alias Pleroma.Web.OStatus
|
||||
alias Pleroma.Web.OStatus.ActivityRepresenter
|
||||
alias Pleroma.Web.XML
|
||||
|
||||
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
|
||||
|
||||
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(%User{id: user_id}, %{data: %{"to" => to} = data}) do
|
||||
cc = Map.get(data, "cc", [])
|
||||
|
||||
bcc =
|
||||
data
|
||||
|> Map.get("bcc", [])
|
||||
|> Enum.reduce([], fn ap_id, bcc ->
|
||||
case Pleroma.List.get_by_ap_id(ap_id) do
|
||||
%Pleroma.List{user_id: ^user_id} = list ->
|
||||
{:ok, following} = Pleroma.List.get_following(list)
|
||||
bcc ++ Enum.map(following, & &1.ap_id)
|
||||
|
||||
_ ->
|
||||
bcc
|
||||
end
|
||||
end)
|
||||
|
||||
[to, cc, bcc]
|
||||
|> Enum.concat()
|
||||
|> Enum.map(&User.get_cached_by_ap_id/1)
|
||||
|> Enum.filter(fn user -> user && !user.local end)
|
||||
end
|
||||
|
||||
@doc "Pushes an activity to remote account."
|
||||
def publish_one(%{recipient: %{info: %{salmon: salmon}}} = params),
|
||||
do: publish_one(Map.put(params, :recipient, salmon))
|
||||
|
||||
def publish_one(%{recipient: url, feed: feed} = params) when is_binary(url) do
|
||||
with {:ok, %{status: code}} when code in 200..299 <-
|
||||
HTTP.post(
|
||||
url,
|
||||
feed,
|
||||
[{"Content-Type", "application/magic-envelope+xml"}]
|
||||
) do
|
||||
if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since],
|
||||
do: Instances.set_reachable(url)
|
||||
|
||||
Logger.debug(fn -> "Pushed to #{url}, code #{code}" end)
|
||||
{:ok, code}
|
||||
else
|
||||
e ->
|
||||
unless params[:unreachable_since], do: Instances.set_reachable(url)
|
||||
Logger.debug(fn -> "Pushing Salmon to #{url} failed, #{inspect(e)}" end)
|
||||
{:error, "Unreachable instance"}
|
||||
end
|
||||
end
|
||||
|
||||
def publish_one(%{recipient_id: recipient_id} = params) do
|
||||
recipient = User.get_cached_by_id(recipient_id)
|
||||
|
||||
params
|
||||
|> Map.delete(:recipient_id)
|
||||
|> Map.put(:recipient, recipient)
|
||||
|> publish_one()
|
||||
end
|
||||
|
||||
def publish_one(_), do: :noop
|
||||
|
||||
@supported_activities [
|
||||
"Create",
|
||||
"Follow",
|
||||
"Like",
|
||||
"Announce",
|
||||
"Undo",
|
||||
"Delete"
|
||||
]
|
||||
|
||||
def is_representable?(%Activity{data: %{"type" => type}} = activity)
|
||||
when type in @supported_activities,
|
||||
do: Visibility.is_public?(activity)
|
||||
|
||||
def is_representable?(_), do: false
|
||||
|
||||
@doc """
|
||||
Publishes an activity to remote accounts
|
||||
"""
|
||||
@spec publish(User.t(), Pleroma.Activity.t()) :: none
|
||||
def publish(user, activity)
|
||||
|
||||
def publish(%{keys: keys} = user, %{data: %{"type" => type}} = activity)
|
||||
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.keys_from_pem(keys)
|
||||
{:ok, feed} = encode(private, feed)
|
||||
|
||||
remote_users = remote_users(user, activity)
|
||||
|
||||
salmon_urls = Enum.map(remote_users, & &1.info.salmon)
|
||||
reachable_urls_metadata = Instances.filter_reachable(salmon_urls)
|
||||
reachable_urls = Map.keys(reachable_urls_metadata)
|
||||
|
||||
remote_users
|
||||
|> Enum.filter(&(&1.info.salmon in reachable_urls))
|
||||
|> Enum.each(fn remote_user ->
|
||||
Logger.debug(fn -> "Sending Salmon to #{remote_user.ap_id}" end)
|
||||
|
||||
Publisher.enqueue_one(__MODULE__, %{
|
||||
recipient_id: remote_user.id,
|
||||
feed: feed,
|
||||
unreachable_since: reachable_urls_metadata[remote_user.info.salmon]
|
||||
})
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
def publish(%{id: id}, _), do: Logger.debug(fn -> "Keys missing for user #{id}" end)
|
||||
|
||||
def gather_webfinger_links(%User{} = user) do
|
||||
{:ok, _private, public} = Keys.keys_from_pem(user.keys)
|
||||
magic_key = encode_key(public)
|
||||
|
||||
[
|
||||
%{"rel" => "salmon", "href" => OStatus.salmon_path(user)},
|
||||
%{
|
||||
"rel" => "magic-public-key",
|
||||
"href" => "data:application/magic-public-key,#{magic_key}"
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
def gather_nodeinfo_protocol_names, do: []
|
||||
end
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue