Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • pleroma/pleroma
  • eal/pleroma
  • feld/pleroma
  • tibike/pleroma
  • href/pleroma
  • hyper/pleroma
  • otremblay/pleroma
  • partial/pleroma
  • notjeff/pleroma
  • mimikun/pleroma
  • hakabahitoyo/pleroma
  • calv/pleroma
  • S_H_/pleroma
  • normandy/pleroma
  • andarna/pleroma
  • qwexvf/pleroma
  • mevo/pleroma
  • kaniini/pleroma
  • tyge/pleroma
  • pony/pleroma
  • mikeo/pleroma
  • boner.engineer/pleroma
  • karolat/pleroma
  • Steph/pleroma
  • nalivaj/pleroma
  • f0x/pleroma
  • explosionguy/pleroma
  • nepfag/pleroma
  • ataalik/pleroma
  • Sir_Boops/pleroma
  • gled/pleroma
  • kirishima/pleroma
  • csaurus/pleroma
  • noyuno/pleroma
  • dashie/pleroma
  • animeirl/pleroma
  • lupine/pleroma
  • andrewzah/pleroma
  • ktsukik/pleroma
  • fotfd/pleroma
  • Syldexia/pleroma
  • witti/pleroma
  • ben/pleroma
  • som/pleroma
  • DeeUnderscore/pleroma
  • Toromino/pleroma
  • riking/pleroma
  • dr1ft/pleroma
  • squidboi/pleroma
  • dex/pleroma
  • viv/pleroma
  • jorty/pleroma
  • stolas/pleroma
  • trqx/pleroma
  • shadowfacts/pleroma
  • scarlett/pleroma
  • vaartis/pleroma
  • worr/pleroma
  • uiri/pleroma
  • shibayashi/pleroma
  • thurloat/pleroma
  • darko/pleroma
  • mkhl/pleroma
  • sn0w/pleroma
  • dfeyer/pleroma
  • mayel/pleroma
  • succfemboi/pleroma
  • nasonfish/pleroma
  • Doctorx/pleroma
  • KokaKiwi/pleroma
  • raven/pleroma
  • h3poteto/pleroma
  • jorin/pleroma
  • greizgh/pleroma
  • cod3monk3y/pleroma
  • rinpatch/pleroma
  • maxf/pleroma
  • minibikini/pleroma
  • parallel588/pleroma
  • oceanvald/pleroma
  • i1t/pleroma
  • woodcat/pleroma
  • l-x/pleroma
  • link0ff/pleroma
  • raeno/pleroma
  • qadeer/pleroma
  • nonlinear/pleroma
  • mloftis/pleroma
  • FloatingGhost/pleroma
  • VyrCossont/pleroma
  • vinzv/pleroma
  • cascode/pleroma
  • anand/pleroma
  • frank87/pleroma
  • iodine/pleroma
  • Horsemans/pleroma
  • barrettbreshears/pleroma
  • luna/pleroma
  • eugenijm/pleroma
  • melissasage/pleroma
  • witcheslive/pleroma
  • edijs/pleroma
  • Dave/pleroma
  • Lidar/pleroma
  • 11backslashes/pleroma
  • 0x1C3B00DA/pleroma
  • FongWan/pleroma
  • foggy1/pleroma
  • faried/pleroma
  • alex.s/pleroma
  • njoseph/pleroma
  • ssuprunenko/pleroma
  • chvanikoff/pleroma
  • quad/pleroma
  • xse/pleroma
  • kunimi53chi/pleroma
  • lexpierce/pleroma
  • aptinio/pleroma
  • iramch/pleroma
  • polymerwitch/pleroma
  • linafilippova/pleroma
  • alfie/pleroma
  • saper/pleroma
  • partridge/pleroma
  • ynakao/pleroma
  • deorsum/pleroma
  • sevvie/pleroma
  • kphrx/pleroma
  • chrismccord/pleroma
  • march/pleroma
  • moonman/pleroma
  • sixohsix/pleroma
  • tallship/pleroma
  • tom79/pleroma
  • outofambit/pleroma
  • oncletom/pleroma
  • skeptik101/pleroma
  • t/pleroma
  • Claire/pleroma
  • Mokou/pleroma
  • nik/pleroma
  • stwf/pleroma
  • aries/pleroma
  • ultem/pleroma
  • daughter_of_fury/pleroma
  • foxiepaws/pleroma
  • pea/pleroma
  • Pasty/pleroma
  • yalh76/pleroma
  • mandel59/pleroma
  • DaKeiser/pleroma
  • technomancy/pleroma
  • jayme/pleroma
  • kensanata/pleroma
  • brian/pleroma
  • ewaf/pleroma
  • Alexpono/pleroma
  • tcit/pleroma
  • a1batross/pleroma
  • doof/pleroma
  • sjw/pleroma
  • mirsal/pleroma
  • rustra/pleroma
  • xenofem/pleroma
  • Igeljaeger/pleroma
  • albino/pleroma
  • phaer/pleroma
  • vala/pleroma
  • cevado/pleroma
  • arkSong/pleroma
  • haoran127/pleroma
  • neftalyluis/pleroma
  • fence/pleroma
  • Hikali/pleroma
  • alexgleason/pleroma
  • Duponin/pleroma
  • patcoll/pleroma
  • ali/pleroma
  • ilja/pleroma
  • kleidukos/pleroma
  • primeos/pleroma
  • translate/pleroma
  • absturztaube/pleroma
  • lukas/pleroma
  • fristi/pleroma
  • NEETzsche/pleroma
  • vpzom/pleroma
  • freon/pleroma
  • guysoft/pleroma
  • dkuku/pleroma
  • bird/pleroma
  • shevek/pleroma
  • piaste/pleroma
  • mkfain/pleroma
  • assyrian.py/pleroma
  • hugo/pleroma
  • seanking/pleroma
  • fikran/pleroma
  • sunny-day/pleroma
  • Snow/pleroma
  • mjc1/pleroma
  • Jeder/pleroma
  • swentel/pleroma
  • Ted/pleroma
  • Hugal31/pleroma
  • barrucadu/pleroma
  • jp/pleroma
  • Larry/pleroma
  • me/pleroma
  • jascou/pleroma
  • volanar/pleroma
  • squeegily/pleroma
  • sf/pleroma
  • zonk/pleroma
  • PestToast/pleroma
  • pasture/pleroma
  • tusooa/pleroma
  • io/pleroma
  • chillout-chat/pleroma
  • gustavs_markos/pleroma
220 results
Show changes
Showing
with 1889 additions and 57 deletions
# Pleroma
<img src="https://git.pleroma.social/pleroma/pleroma/uploads/8cec84f5a084d887339f57deeb8a293e/pleroma-banner-vector-nopad-notext.svg" width="300px" />
## About Pleroma
## About
Pleroma is a microblogging server software that can federate (= exchange messages with) other servers that support the same federation standards (OStatus and ActivityPub). What that means is that you can host a server for yourself or your friends and stay in control of your online identity, but still exchange messages with people on larger servers. Pleroma will federate with all servers that implement either OStatus or ActivityPub, like Friendica, GNU Social, Hubzilla, Mastodon, Misskey, Peertube, and Pixelfed.
Pleroma is a microblogging server software that can federate (= exchange messages with) other servers that support ActivityPub. What that means is that you can host a server for yourself or your friends and stay in control of your online identity, but still exchange messages with people on larger servers. Pleroma will federate with all servers that implement ActivityPub, like Friendica, GNU Social, Hubzilla, Mastodon, Misskey, Peertube, and Pixelfed.
Pleroma is written in Elixir, high-performance and can run on small devices like a Raspberry Pi.
Pleroma is written in Elixir and uses PostgresSQL for data storage. It's efficient enough to be ran on low-power devices like Raspberry Pi (though we wouldn't recommend storing the database on the internal SD card ;) but can scale well when ran on more powerful hardware (albeit only single-node for now).
For clients it supports both the [GNU Social API with Qvitter extensions](https://twitter-api.readthedocs.io/en/latest/index.html) and the [Mastodon client API](https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md).
For clients it supports the [Mastodon client API](https://docs.joinmastodon.org/api/guidelines/) with Pleroma extensions (see the API section on <https://docs-develop.pleroma.social>).
- [Client Applications for Pleroma](docs/Clients.md)
No release has been made yet, but several servers have been online for months already. If you want to run your own server, feel free to contact us at @lain@pleroma.soykaf.com or in our dev chat at #pleroma on freenode or via matrix at <https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org>.
- [Client Applications for Pleroma](https://docs-develop.pleroma.social/backend/clients/)
## Installation
### Docker
While we don’t provide docker files, other people have written very good ones. Take a look at <https://github.com/angristan/docker-pleroma> or <https://github.com/sn0w/pleroma-docker>.
### Dependencies
* Postgresql version 9.6 or newer
* Elixir version 1.7 or newer. If your distribution only has an old version available, check [Elixir’s install page](https://elixir-lang.org/install.html) or use a tool like [asdf](https://github.com/asdf-vm/asdf).
* Build-essential tools
### Configuration
* Run `mix deps.get` to install elixir dependencies.
* Run `mix pleroma.instance gen`. This will ask you questions about your instance and generate a configuration file in `config/generated_config.exs`. Check that and copy it to either `config/dev.secret.exs` or `config/prod.secret.exs`. It will also create a `config/setup_db.psql`, which you should run as the PostgreSQL superuser (i.e., `sudo -u postgres psql -f config/setup_db.psql`). It will create the database, user, and password you gave `mix pleroma.gen.instance` earlier, as well as set up the necessary extensions in the database. PostgreSQL superuser privileges are only needed for this step.
* For these next steps, the default will be to run pleroma using the dev configuration file, `config/dev.secret.exs`. To run them using the prod config file, prefix each command at the shell with `MIX_ENV=prod`. For example: `MIX_ENV=prod mix phx.server`. Documentation for the config can be found at [`docs/config.md`](docs/config.md)
* Run `mix ecto.migrate` to run the database migrations. You will have to do this again after certain updates.
* You can check if your instance is configured correctly by running it with `mix phx.server` and checking the instance info endpoint at `/api/v1/instance`. If it shows your uri, name and email correctly, you are configured correctly. If it shows something like `localhost:4000`, your configuration is probably wrong, unless you are running a local development setup.
* The common and convenient way for adding HTTPS is by using Nginx as a reverse proxy. You can look at example Nginx configuration in `installation/pleroma.nginx`. If you need TLS/SSL certificates for HTTPS, you can look get some for free with letsencrypt: <https://letsencrypt.org/>. The simplest way to obtain and install a certificate is to use [Certbot.](https://certbot.eff.org) Depending on your specific setup, certbot may be able to get a certificate and configure your web server automatically.
## Running
* By default, it listens on port 4000 (TCP), so you can access it on <http://localhost:4000/> (if you are on the same machine). In case of an error it will restart automatically.
### OTP releases (Recommended)
If you are running Linux (glibc or musl) on x86/arm, the recommended way to install Pleroma is by using OTP releases. OTP releases are as close as you can get to binary releases with Erlang/Elixir. The release is self-contained, and provides everything needed to boot it. The installation instructions are available [here](https://docs-develop.pleroma.social/backend/installation/otp_en/).
### Frontends
### From Source
If your platform is not supported, or you just want to be able to edit the source code easily, you may install Pleroma from source.
Pleroma comes with two frontends. The first one, Pleroma FE, can be reached by normally visiting the site. The other one, based on the Mastodon project, can be found by visiting the /web path of your site.
- [Alpine Linux](https://docs-develop.pleroma.social/backend/installation/alpine_linux_en/)
- [Arch Linux](https://docs-develop.pleroma.social/backend/installation/arch_linux_en/)
- [CentOS 7](https://docs-develop.pleroma.social/backend/installation/centos7_en/)
- [Debian-based](https://docs-develop.pleroma.social/backend/installation/debian_based_en/)
- [Debian-based (jp)](https://docs-develop.pleroma.social/backend/installation/debian_based_jp/)
- [FreeBSD](https://docs-develop.pleroma.social/backend/installation/freebsd_en/)
- [Gentoo Linux](https://docs-develop.pleroma.social/backend/installation/gentoo_en/)
- [NetBSD](https://docs-develop.pleroma.social/backend/installation/netbsd_en/)
- [OpenBSD](https://docs-develop.pleroma.social/backend/installation/openbsd_en/)
- [OpenBSD (fi)](https://docs-develop.pleroma.social/backend/installation/openbsd_fi/)
### As systemd service (with provided .service file)
### OS/Distro packages
Currently Pleroma is packaged for [YunoHost](https://yunohost.org), [NixOS](https://nixos.org), [Gentoo through GURU](https://gentoo.org/) and [Archlinux through AUR](https://aur.archlinux.org/packages/pleroma). You may find more at <https://repology.org/project/pleroma/versions>.
If you want to package Pleroma for any OS/Distros, we can guide you through the process on our [community channels](#community-channels). If you want to change default options in your Pleroma package, please **discuss it with us first**.
Example .service file can be found in `installation/pleroma.service`. Copy this to `/etc/systemd/system/`. Running `systemctl enable --now pleroma.service` will run Pleroma and enable startup on boot. Logs can be watched by using `journalctl -fu pleroma.service`.
### As OpenRC service (with provided RC file)
Copy `installation/init.d/pleroma` to `/etc/init.d/pleroma`. You can add it to the services ran by default with: `rc-update add pleroma`
### Standalone/run by other means
Run `mix phx.server` in repository’s root, it will output log into stdout/stderr.
### Using an upstream proxy for federation
Add the following to your `dev.secret.exs` or `prod.secret.exs` if you want to proxify all http requests that Pleroma makes to an upstream proxy server:
```elixir
config :pleroma, :http,
proxy_url: "127.0.0.1:8123"
```
### Docker
While we don’t provide docker files, other people have written very good ones. Take a look at <https://github.com/angristan/docker-pleroma> or <https://glitch.sh/sn0w/pleroma-docker>.
This is useful for running Pleroma inside Tor or I2P.
### Raspberry Pi
Community maintained Raspberry Pi image that you can flash and run Pleroma on your Raspberry Pi. Available here <https://github.com/guysoft/PleromaPi>.
## Customization and contribution
### Compilation Troubleshooting
If you ever encounter compilation issues during the updating of Pleroma, you can try these commands and see if they fix things:
The [Pleroma Wiki](https://git.pleroma.social/pleroma/pleroma/wikis/home) offers manuals and guides on how to further customize your instance to your liking and how you can contribute to the project.
- `mix deps.clean --all`
- `mix local.rebar`
- `mix local.hex`
- `rm -r _build`
## Troubleshooting
If you are not developing Pleroma, it is better to use the OTP release, which comes with everything precompiled.
### No incoming federation
## Documentation
- Latest Released revision: <https://docs.pleroma.social>
- Latest Git revision: <https://docs-develop.pleroma.social>
Check that you correctly forward the `host` header to the backend. It is needed to validate signatures.
## Community Channels
* IRC: **#pleroma** and **#pleroma-dev** on libera.chat, webchat is available at <https://irc.pleroma.social>
* Matrix: [#pleroma:libera.chat](https://matrix.to/#/#pleroma:libera.chat) and [#pleroma-dev:libera.chat](https://matrix.to/#/#pleroma-dev:libera.chat)
# Pleroma backend security policy
## Supported versions
Currently, Pleroma offers bugfixes and security patches only for the latest minor release.
| Version | Support
|---------| --------
| 2.2 | Bugfixes and security patches
## Reporting a vulnerability
Please use confidential issues (tick the "This issue is confidential and should only be visible to team members with at least Reporter access." box when submitting) at our [bugtracker](https://git.pleroma.social/pleroma/pleroma/-/issues/new) for reporting vulnerabilities.
## Announcements
New releases are announced at [pleroma.social](https://pleroma.social/announcements/). All security releases are tagged with ["Security"](https://pleroma.social/announcements/tags/security/). You can be notified of them by subscribing to an Atom feed at <https://pleroma.social/announcements/tags/security/feed.xml>.
defmodule Pleroma.LoadTesting.Activities do
@moduledoc """
Module for generating different activities.
"""
import Ecto.Query
import Pleroma.LoadTesting.Helper, only: [to_sec: 1]
alias Ecto.UUID
alias Pleroma.Constants
alias Pleroma.LoadTesting.Users
alias Pleroma.Repo
alias Pleroma.Web.CommonAPI
require Constants
@defaults [
iterations: 170,
friends_used: 20,
non_friends_used: 20
]
@max_concurrency 10
@visibility ~w(public private direct unlisted)
@types [
:simple,
:simple_filtered,
:emoji,
:mentions,
:hell_thread,
:attachment,
:tag,
:like,
:reblog,
:simple_thread
]
@groups [:friends_local, :friends_remote, :non_friends_local, :non_friends_local]
@remote_groups [:friends_remote, :non_friends_remote]
@friends_groups [:friends_local, :friends_remote]
@non_friends_groups [:non_friends_local, :non_friends_remote]
@spec generate(User.t(), keyword()) :: :ok
def generate(user, opts \\ []) do
{:ok, _} =
Agent.start_link(fn -> %{} end,
name: :benchmark_state
)
opts = Keyword.merge(@defaults, opts)
users = Users.prepare_users(user, opts)
{:ok, _} = Agent.start_link(fn -> users[:non_friends_remote] end, name: :non_friends_remote)
task_data =
for visibility <- @visibility,
type <- @types,
group <- [:user | @groups],
do: {visibility, type, group}
IO.puts("Starting generating #{opts[:iterations]} iterations of activities...")
public_long_thread = fn ->
generate_long_thread("public", users, opts)
end
private_long_thread = fn ->
generate_long_thread("private", users, opts)
end
iterations = opts[:iterations]
{time, _} =
:timer.tc(fn ->
Enum.each(
1..iterations,
fn
i when i == iterations - 2 ->
spawn(public_long_thread)
spawn(private_long_thread)
generate_activities(users, Enum.shuffle(task_data), opts)
_ ->
generate_activities(users, Enum.shuffle(task_data), opts)
end
)
end)
IO.puts("Generating iterations of activities took #{to_sec(time)} sec.\n")
:ok
end
def generate_power_intervals(opts \\ []) do
count = Keyword.get(opts, :count, 20)
power = Keyword.get(opts, :power, 2)
IO.puts("Generating #{count} intervals for a power #{power} series...")
counts = Enum.map(1..count, fn n -> :math.pow(n, power) end)
sum = Enum.sum(counts)
densities =
Enum.map(counts, fn c ->
c / sum
end)
densities
|> Enum.reduce(0, fn density, acc ->
if acc == 0 do
[{0, density}]
else
[{_, lower} | _] = acc
[{lower, lower + density} | acc]
end
end)
|> Enum.reverse()
end
def generate_tagged_activities(opts \\ []) do
tag_count = Keyword.get(opts, :tag_count, 20)
users = Keyword.get(opts, :users, Repo.all(Pleroma.User))
activity_count = Keyword.get(opts, :count, 200_000)
intervals = generate_power_intervals(count: tag_count)
IO.puts(
"Generating #{activity_count} activities using #{tag_count} different tags of format `tag_n`, starting at tag_0"
)
Enum.each(1..activity_count, fn _ ->
random = :rand.uniform()
i = Enum.find_index(intervals, fn {lower, upper} -> lower <= random && upper > random end)
CommonAPI.post(Enum.random(users), %{status: "a post with the tag #tag_#{i}"})
end)
end
defp generate_long_thread(visibility, users, _opts) do
group =
if visibility == "public",
do: :friends_local,
else: :user
tasks = get_reply_tasks(visibility, group) |> Stream.cycle() |> Enum.take(50)
{:ok, activity} =
CommonAPI.post(users[:user], %{
status: "Start of #{visibility} long thread",
visibility: visibility
})
Agent.update(:benchmark_state, fn state ->
key =
if visibility == "public",
do: :public_thread,
else: :private_thread
Map.put(state, key, activity)
end)
acc = {activity.id, ["@" <> users[:user].nickname, "reply to long thread"]}
insert_replies_for_long_thread(tasks, visibility, users, acc)
IO.puts("Generating #{visibility} long thread ended\n")
end
defp insert_replies_for_long_thread(tasks, visibility, users, acc) do
Enum.reduce(tasks, acc, fn
:user, {id, data} ->
user = users[:user]
insert_reply(user, List.delete(data, "@" <> user.nickname), id, visibility)
group, {id, data} ->
replier = Enum.random(users[group])
insert_reply(replier, List.delete(data, "@" <> replier.nickname), id, visibility)
end)
end
defp generate_activities(users, task_data, opts) do
Task.async_stream(
task_data,
fn {visibility, type, group} ->
insert_activity(type, visibility, group, users, opts)
end,
max_concurrency: @max_concurrency,
timeout: 30_000
)
|> Stream.run()
end
defp insert_local_activity(visibility, group, users, status) do
{:ok, _} =
group
|> get_actor(users)
|> CommonAPI.post(%{status: status, visibility: visibility})
end
defp insert_remote_activity(visibility, group, users, status) do
actor = get_actor(group, users)
{act_data, obj_data} = prepare_activity_data(actor, visibility, users[:user])
{activity_data, object_data} = other_data(actor, status)
activity_data
|> Map.merge(act_data)
|> Map.put("object", Map.merge(object_data, obj_data))
|> Pleroma.Web.ActivityPub.ActivityPub.insert(false)
end
defp user_mentions(users) do
user_mentions =
Enum.reduce(
@groups,
[],
fn group, acc ->
acc ++ get_random_mentions(users[group], Enum.random(0..2))
end
)
if Enum.random([true, false]),
do: ["@" <> users[:user].nickname | user_mentions],
else: user_mentions
end
defp hell_thread_mentions(users) do
with {:ok, nil} <- Cachex.get(:user_cache, "hell_thread_mentions") do
cached =
@groups
|> Enum.reduce([users[:user]], fn group, acc ->
acc ++ Enum.take(users[group], 5)
end)
|> Enum.map(&"@#{&1.nickname}")
|> Enum.join(", ")
Cachex.put(:user_cache, "hell_thread_mentions", cached)
cached
else
{:ok, cached} -> cached
end
end
defp insert_activity(:simple, visibility, group, users, _opts)
when group in @remote_groups do
insert_remote_activity(visibility, group, users, "Remote status")
end
defp insert_activity(:simple, visibility, group, users, _opts) do
insert_local_activity(visibility, group, users, "Simple status")
end
defp insert_activity(:simple_filtered, visibility, group, users, _opts)
when group in @remote_groups do
insert_remote_activity(visibility, group, users, "Remote status which must be filtered")
end
defp insert_activity(:simple_filtered, visibility, group, users, _opts) do
insert_local_activity(visibility, group, users, "Simple status which must be filtered")
end
defp insert_activity(:emoji, visibility, group, users, _opts)
when group in @remote_groups do
insert_remote_activity(visibility, group, users, "Remote status with emoji :firefox:")
end
defp insert_activity(:emoji, visibility, group, users, _opts) do
insert_local_activity(visibility, group, users, "Simple status with emoji :firefox:")
end
defp insert_activity(:mentions, visibility, group, users, _opts)
when group in @remote_groups do
mentions = user_mentions(users)
status = Enum.join(mentions, ", ") <> " remote status with mentions"
insert_remote_activity(visibility, group, users, status)
end
defp insert_activity(:mentions, visibility, group, users, _opts) do
mentions = user_mentions(users)
status = Enum.join(mentions, ", ") <> " simple status with mentions"
insert_remote_activity(visibility, group, users, status)
end
defp insert_activity(:hell_thread, visibility, group, users, _)
when group in @remote_groups do
mentions = hell_thread_mentions(users)
insert_remote_activity(visibility, group, users, mentions <> " remote hell thread status")
end
defp insert_activity(:hell_thread, visibility, group, users, _opts) do
mentions = hell_thread_mentions(users)
insert_local_activity(visibility, group, users, mentions <> " hell thread status")
end
defp insert_activity(:attachment, visibility, group, users, _opts) do
actor = get_actor(group, users)
obj_data = %{
"actor" => actor.ap_id,
"name" => "4467-11.jpg",
"type" => "Document",
"url" => [
%{
"href" =>
"#{Pleroma.Web.Endpoint.url()}/media/b1b873552422a07bf53af01f3c231c841db4dfc42c35efde681abaf0f2a4eab7.jpg",
"mediaType" => "image/jpeg",
"type" => "Link"
}
]
}
object = Repo.insert!(%Pleroma.Object{data: obj_data})
{:ok, _activity} =
CommonAPI.post(actor, %{
status: "Post with attachment",
visibility: visibility,
media_ids: [object.id]
})
end
defp insert_activity(:tag, visibility, group, users, _opts) do
insert_local_activity(visibility, group, users, "Status with #tag")
end
defp insert_activity(:like, visibility, group, users, opts) do
actor = get_actor(group, users)
with activity_id when not is_nil(activity_id) <- get_random_create_activity_id(),
{:ok, _activity} <- CommonAPI.favorite(actor, activity_id) do
:ok
else
{:error, _} ->
insert_activity(:like, visibility, group, users, opts)
nil ->
Process.sleep(15)
insert_activity(:like, visibility, group, users, opts)
end
end
defp insert_activity(:reblog, visibility, group, users, opts) do
actor = get_actor(group, users)
with activity_id when not is_nil(activity_id) <- get_random_create_activity_id(),
{:ok, _activity} <- CommonAPI.repeat(activity_id, actor) do
:ok
else
{:error, _} ->
insert_activity(:reblog, visibility, group, users, opts)
nil ->
Process.sleep(15)
insert_activity(:reblog, visibility, group, users, opts)
end
end
defp insert_activity(:simple_thread, "direct", group, users, _opts) do
actor = get_actor(group, users)
tasks = get_reply_tasks("direct", group)
list =
case group do
:user ->
group = Enum.random(@friends_groups)
Enum.take(users[group], 3)
_ ->
Enum.take(users[group], 3)
end
data = Enum.map(list, &("@" <> &1.nickname))
{:ok, activity} =
CommonAPI.post(actor, %{
status: Enum.join(data, ", ") <> "simple status",
visibility: "direct"
})
acc = {activity.id, ["@" <> users[:user].nickname | data] ++ ["reply to status"]}
insert_direct_replies(tasks, users[:user], list, acc)
end
defp insert_activity(:simple_thread, visibility, group, users, _opts) do
actor = get_actor(group, users)
tasks = get_reply_tasks(visibility, group)
{:ok, activity} =
CommonAPI.post(users[:user], %{status: "Simple status", visibility: visibility})
acc = {activity.id, ["@" <> actor.nickname, "reply to status"]}
insert_replies(tasks, visibility, users, acc)
end
defp get_actor(:user, %{user: user}), do: user
defp get_actor(group, users), do: Enum.random(users[group])
defp other_data(actor, content) do
%{host: host} = URI.parse(actor.ap_id)
datetime = DateTime.utc_now() |> to_string()
context_id = "https://#{host}/contexts/#{UUID.generate()}"
activity_id = "https://#{host}/activities/#{UUID.generate()}"
object_id = "https://#{host}/objects/#{UUID.generate()}"
activity_data = %{
"actor" => actor.ap_id,
"context" => context_id,
"id" => activity_id,
"published" => datetime,
"type" => "Create",
"directMessage" => false
}
object_data = %{
"actor" => actor.ap_id,
"attachment" => [],
"attributedTo" => actor.ap_id,
"bcc" => [],
"bto" => [],
"content" => content,
"context" => context_id,
"conversation" => context_id,
"emoji" => %{},
"id" => object_id,
"published" => datetime,
"sensitive" => false,
"summary" => "",
"tag" => [],
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
"type" => "Note"
}
{activity_data, object_data}
end
defp prepare_activity_data(actor, "public", _mention) do
obj_data = %{
"cc" => [actor.follower_address],
"to" => [Constants.as_public()]
}
act_data = %{
"cc" => [actor.follower_address],
"to" => [Constants.as_public()]
}
{act_data, obj_data}
end
defp prepare_activity_data(actor, "private", _mention) do
obj_data = %{
"cc" => [],
"to" => [actor.follower_address]
}
act_data = %{
"cc" => [],
"to" => [actor.follower_address]
}
{act_data, obj_data}
end
defp prepare_activity_data(actor, "unlisted", _mention) do
obj_data = %{
"cc" => [Constants.as_public()],
"to" => [actor.follower_address]
}
act_data = %{
"cc" => [Constants.as_public()],
"to" => [actor.follower_address]
}
{act_data, obj_data}
end
defp prepare_activity_data(_actor, "direct", mention) do
%{host: mentioned_host} = URI.parse(mention.ap_id)
obj_data = %{
"cc" => [],
"content" =>
"<span class=\"h-card\"><a class=\"u-url mention\" href=\"#{mention.ap_id}\" rel=\"ugc\">@<span>#{
mention.nickname
}</span></a></span> direct message",
"tag" => [
%{
"href" => mention.ap_id,
"name" => "@#{mention.nickname}@#{mentioned_host}",
"type" => "Mention"
}
],
"to" => [mention.ap_id]
}
act_data = %{
"cc" => [],
"directMessage" => true,
"to" => [mention.ap_id]
}
{act_data, obj_data}
end
defp get_reply_tasks("public", :user) do
[:friends_local, :friends_remote, :non_friends_local, :non_friends_remote, :user]
end
defp get_reply_tasks("public", group) when group in @friends_groups do
[:non_friends_local, :non_friends_remote, :user, :friends_local, :friends_remote]
end
defp get_reply_tasks("public", group) when group in @non_friends_groups do
[:user, :friends_local, :friends_remote, :non_friends_local, :non_friends_remote]
end
defp get_reply_tasks(visibility, :user) when visibility in ["unlisted", "private"] do
[:friends_local, :friends_remote, :user, :friends_local, :friends_remote]
end
defp get_reply_tasks(visibility, group)
when visibility in ["unlisted", "private"] and group in @friends_groups do
[:user, :friends_remote, :friends_local, :user]
end
defp get_reply_tasks(visibility, group)
when visibility in ["unlisted", "private"] and
group in @non_friends_groups,
do: []
defp get_reply_tasks("direct", :user), do: [:friends_local, :user, :friends_remote]
defp get_reply_tasks("direct", group) when group in @friends_groups,
do: [:user, group, :user]
defp get_reply_tasks("direct", group) when group in @non_friends_groups do
[:user, :non_friends_remote, :user, :non_friends_local]
end
defp insert_replies(tasks, visibility, users, acc) do
Enum.reduce(tasks, acc, fn
:user, {id, data} ->
insert_reply(users[:user], data, id, visibility)
group, {id, data} ->
replier = Enum.random(users[group])
insert_reply(replier, data, id, visibility)
end)
end
defp insert_direct_replies(tasks, user, list, acc) do
Enum.reduce(tasks, acc, fn
:user, {id, data} ->
{reply_id, _} = insert_reply(user, List.delete(data, "@" <> user.nickname), id, "direct")
{reply_id, data}
_, {id, data} ->
actor = Enum.random(list)
{reply_id, _} =
insert_reply(actor, List.delete(data, "@" <> actor.nickname), id, "direct")
{reply_id, data}
end)
end
defp insert_reply(actor, data, activity_id, visibility) do
{:ok, reply} =
CommonAPI.post(actor, %{
status: Enum.join(data, ", "),
visibility: visibility,
in_reply_to_status_id: activity_id
})
{reply.id, ["@" <> actor.nickname | data]}
end
defp get_random_mentions(_users, count) when count == 0, do: []
defp get_random_mentions(users, count) do
users
|> Enum.shuffle()
|> Enum.take(count)
|> Enum.map(&"@#{&1.nickname}")
end
defp get_random_create_activity_id do
Repo.one(
from(a in Pleroma.Activity,
where: fragment("(?)->>'type' = ?", a.data, ^"Create"),
order_by: fragment("RANDOM()"),
limit: 1,
select: a.id
)
)
end
end
defmodule Pleroma.LoadTesting.Fetcher do
alias Pleroma.Activity
alias Pleroma.Pagination
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.MastodonAPI.MastodonAPI
alias Pleroma.Web.MastodonAPI.StatusView
@spec run_benchmarks(User.t()) :: any()
def run_benchmarks(user) do
fetch_user(user)
fetch_timelines(user)
render_views(user)
end
defp formatters do
[
Benchee.Formatters.Console
]
end
defp 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
},
formatters: formatters()
)
end
defp create_filter(user) do
Pleroma.Filter.create(%{
user_id: user.id,
phrase: "must be filtered",
hide: true,
context: ["home"]
})
end
defp delete_filter(filter), do: Repo.delete(filter)
defp fetch_timelines(user) do
fetch_home_timeline(user)
fetch_home_timeline_with_filter(user)
fetch_direct_timeline(user)
fetch_public_timeline(user)
fetch_public_timeline_with_filter(user)
fetch_public_timeline(user, :with_blocks)
fetch_public_timeline(user, :local)
fetch_public_timeline(user, :tag)
fetch_notifications(user)
fetch_favourites(user)
fetch_long_thread(user)
fetch_timelines_with_reply_filtering(user)
end
defp render_views(user) do
render_timelines(user)
render_long_thread(user)
end
defp opts_for_home_timeline(user) do
%{
blocking_user: user,
count: "20",
muting_user: user,
type: ["Create", "Announce"],
user: user,
with_muted: true
}
end
defp fetch_home_timeline(user, title_end \\ "") do
opts = opts_for_home_timeline(user)
recipients = [user.ap_id | User.following(user)]
first_page_last =
ActivityPub.fetch_activities(recipients, opts) |> Enum.reverse() |> List.last()
second_page_last =
ActivityPub.fetch_activities(recipients, Map.put(opts, :max_id, first_page_last.id))
|> Enum.reverse()
|> List.last()
third_page_last =
ActivityPub.fetch_activities(recipients, Map.put(opts, :max_id, second_page_last.id))
|> Enum.reverse()
|> List.last()
forth_page_last =
ActivityPub.fetch_activities(recipients, Map.put(opts, :max_id, third_page_last.id))
|> Enum.reverse()
|> List.last()
title = "home timeline " <> title_end
Benchee.run(
%{
title => fn opts -> ActivityPub.fetch_activities(recipients, opts) end
},
inputs: %{
"1 page" => opts,
"2 page" => Map.put(opts, :max_id, first_page_last.id),
"3 page" => Map.put(opts, :max_id, second_page_last.id),
"4 page" => Map.put(opts, :max_id, third_page_last.id),
"5 page" => Map.put(opts, :max_id, forth_page_last.id),
"1 page only media" => Map.put(opts, :only_media, true),
"2 page only media" =>
Map.put(opts, :max_id, first_page_last.id) |> Map.put(:only_media, true),
"3 page only media" =>
Map.put(opts, :max_id, second_page_last.id) |> Map.put(:only_media, true),
"4 page only media" =>
Map.put(opts, :max_id, third_page_last.id) |> Map.put(:only_media, true),
"5 page only media" =>
Map.put(opts, :max_id, forth_page_last.id) |> Map.put(:only_media, true)
},
formatters: formatters()
)
end
defp fetch_home_timeline_with_filter(user) do
{:ok, filter} = create_filter(user)
fetch_home_timeline(user, "with filters")
delete_filter(filter)
end
defp opts_for_direct_timeline(user) do
%{
visibility: "direct",
blocking_user: user,
count: "20",
type: "Create",
user: user,
with_muted: true
}
end
defp fetch_direct_timeline(user) do
recipients = [user.ap_id]
opts = opts_for_direct_timeline(user)
first_page_last =
recipients
|> ActivityPub.fetch_activities_query(opts)
|> Pagination.fetch_paginated(opts)
|> List.last()
opts2 = Map.put(opts, :max_id, first_page_last.id)
second_page_last =
recipients
|> ActivityPub.fetch_activities_query(opts2)
|> Pagination.fetch_paginated(opts2)
|> List.last()
opts3 = Map.put(opts, :max_id, second_page_last.id)
third_page_last =
recipients
|> ActivityPub.fetch_activities_query(opts3)
|> Pagination.fetch_paginated(opts3)
|> List.last()
opts4 = Map.put(opts, :max_id, third_page_last.id)
forth_page_last =
recipients
|> ActivityPub.fetch_activities_query(opts4)
|> Pagination.fetch_paginated(opts4)
|> List.last()
Benchee.run(
%{
"direct timeline" => fn opts ->
ActivityPub.fetch_activities_query(recipients, opts) |> Pagination.fetch_paginated(opts)
end
},
inputs: %{
"1 page" => opts,
"2 page" => opts2,
"3 page" => opts3,
"4 page" => opts4,
"5 page" => Map.put(opts4, :max_id, forth_page_last.id)
},
formatters: formatters()
)
end
defp opts_for_public_timeline(user) do
%{
type: ["Create", "Announce"],
local_only: false,
blocking_user: user,
muting_user: user
}
end
defp opts_for_public_timeline(user, :local) do
%{
type: ["Create", "Announce"],
local_only: true,
blocking_user: user,
muting_user: user
}
end
defp opts_for_public_timeline(user, :tag) do
%{
blocking_user: user,
count: "20",
local_only: nil,
muting_user: user,
tag: ["tag"],
tag_all: [],
tag_reject: [],
type: "Create",
user: user,
with_muted: true
}
end
defp fetch_public_timeline(user) do
opts = opts_for_public_timeline(user)
fetch_public_timeline(opts, "public timeline")
end
defp fetch_public_timeline_with_filter(user) do
{:ok, filter} = create_filter(user)
opts = opts_for_public_timeline(user)
fetch_public_timeline(opts, "public timeline with filters")
delete_filter(filter)
end
defp fetch_public_timeline(user, :local) do
opts = opts_for_public_timeline(user, :local)
fetch_public_timeline(opts, "public timeline only local")
end
defp fetch_public_timeline(user, :tag) do
opts = opts_for_public_timeline(user, :tag)
fetch_public_timeline(opts, "hashtag timeline")
end
defp fetch_public_timeline(user, :only_media) do
opts = opts_for_public_timeline(user) |> Map.put(:only_media, true)
fetch_public_timeline(opts, "public timeline only media")
end
defp fetch_public_timeline(user, :with_blocks) do
opts = opts_for_public_timeline(user)
remote_non_friends = Agent.get(:non_friends_remote, & &1)
Benchee.run(%{
"public timeline without blocks" => fn ->
ActivityPub.fetch_public_activities(opts)
end
})
Enum.each(remote_non_friends, fn non_friend ->
{:ok, _} = User.block(user, non_friend)
end)
user = User.get_by_id(user.id)
opts = Map.put(opts, :blocking_user, user)
Benchee.run(%{
"public timeline with user block" => fn ->
ActivityPub.fetch_public_activities(opts)
end
})
domains =
Enum.reduce(remote_non_friends, [], fn non_friend, domains ->
{:ok, _user} = User.unblock(user, non_friend)
%{host: host} = URI.parse(non_friend.ap_id)
[host | domains]
end)
domains = Enum.uniq(domains)
Enum.each(domains, fn domain ->
{:ok, _} = User.block_domain(user, domain)
end)
user = User.get_by_id(user.id)
opts = Map.put(opts, :blocking_user, user)
Benchee.run(%{
"public timeline with domain block" => fn ->
ActivityPub.fetch_public_activities(opts)
end
})
end
defp fetch_public_timeline(opts, title) when is_binary(title) do
first_page_last = ActivityPub.fetch_public_activities(opts) |> List.last()
second_page_last =
ActivityPub.fetch_public_activities(Map.put(opts, :max_id, first_page_last.id))
|> List.last()
third_page_last =
ActivityPub.fetch_public_activities(Map.put(opts, :max_id, second_page_last.id))
|> List.last()
forth_page_last =
ActivityPub.fetch_public_activities(Map.put(opts, :max_id, third_page_last.id))
|> List.last()
Benchee.run(
%{
title => fn opts ->
ActivityPub.fetch_public_activities(opts)
end
},
inputs: %{
"1 page" => opts,
"2 page" => Map.put(opts, :max_id, first_page_last.id),
"3 page" => Map.put(opts, :max_id, second_page_last.id),
"4 page" => Map.put(opts, :max_id, third_page_last.id),
"5 page" => Map.put(opts, :max_id, forth_page_last.id)
},
formatters: formatters()
)
end
defp opts_for_notifications do
%{count: "20", with_muted: true}
end
defp fetch_notifications(user) do
opts = opts_for_notifications()
first_page_last = MastodonAPI.get_notifications(user, opts) |> List.last()
second_page_last =
MastodonAPI.get_notifications(user, Map.put(opts, :max_id, first_page_last.id))
|> List.last()
third_page_last =
MastodonAPI.get_notifications(user, Map.put(opts, :max_id, second_page_last.id))
|> List.last()
forth_page_last =
MastodonAPI.get_notifications(user, Map.put(opts, :max_id, third_page_last.id))
|> List.last()
Benchee.run(
%{
"Notifications" => fn opts ->
MastodonAPI.get_notifications(user, opts)
end
},
inputs: %{
"1 page" => opts,
"2 page" => Map.put(opts, :max_id, first_page_last.id),
"3 page" => Map.put(opts, :max_id, second_page_last.id),
"4 page" => Map.put(opts, :max_id, third_page_last.id),
"5 page" => Map.put(opts, :max_id, forth_page_last.id)
},
formatters: formatters()
)
end
defp fetch_favourites(user) do
first_page_last = ActivityPub.fetch_favourites(user) |> List.last()
second_page_last =
ActivityPub.fetch_favourites(user, %{:max_id => first_page_last.id}) |> List.last()
third_page_last =
ActivityPub.fetch_favourites(user, %{:max_id => second_page_last.id}) |> List.last()
forth_page_last =
ActivityPub.fetch_favourites(user, %{:max_id => third_page_last.id}) |> List.last()
Benchee.run(
%{
"Favourites" => fn opts ->
ActivityPub.fetch_favourites(user, opts)
end
},
inputs: %{
"1 page" => %{},
"2 page" => %{:max_id => first_page_last.id},
"3 page" => %{:max_id => second_page_last.id},
"4 page" => %{:max_id => third_page_last.id},
"5 page" => %{:max_id => forth_page_last.id}
},
formatters: formatters()
)
end
defp opts_for_long_thread(user) do
%{
blocking_user: user,
user: user
}
end
defp fetch_long_thread(user) do
%{public_thread: public, private_thread: private} =
Agent.get(:benchmark_state, fn state -> state end)
opts = opts_for_long_thread(user)
private_input = {private.data["context"], Map.put(opts, :exclude_id, private.id)}
public_input = {public.data["context"], Map.put(opts, :exclude_id, public.id)}
Benchee.run(
%{
"fetch context" => fn {context, opts} ->
ActivityPub.fetch_activities_for_context(context, opts)
end
},
inputs: %{
"Private long thread" => private_input,
"Public long thread" => public_input
},
formatters: formatters()
)
end
defp render_timelines(user) do
opts = opts_for_home_timeline(user)
recipients = [user.ap_id | User.following(user)]
home_activities = ActivityPub.fetch_activities(recipients, opts) |> Enum.reverse()
recipients = [user.ap_id]
opts = opts_for_direct_timeline(user)
direct_activities =
recipients
|> ActivityPub.fetch_activities_query(opts)
|> Pagination.fetch_paginated(opts)
opts = opts_for_public_timeline(user)
public_activities = ActivityPub.fetch_public_activities(opts)
opts = opts_for_public_timeline(user, :tag)
tag_activities = ActivityPub.fetch_public_activities(opts)
opts = opts_for_notifications()
notifications = MastodonAPI.get_notifications(user, opts)
favourites = ActivityPub.fetch_favourites(user)
Benchee.run(
%{
"Rendering home timeline" => fn ->
StatusView.render("index.json", %{
activities: home_activities,
for: user,
as: :activity
})
end,
"Rendering direct timeline" => fn ->
StatusView.render("index.json", %{
activities: direct_activities,
for: user,
as: :activity
})
end,
"Rendering public timeline" => fn ->
StatusView.render("index.json", %{
activities: public_activities,
for: user,
as: :activity
})
end,
"Rendering tag timeline" => fn ->
StatusView.render("index.json", %{
activities: tag_activities,
for: user,
as: :activity
})
end,
"Rendering notifications" => fn ->
Pleroma.Web.MastodonAPI.NotificationView.render("index.json", %{
notifications: notifications,
for: user
})
end,
"Rendering favourites timeline" => fn ->
StatusView.render("index.json", %{
activities: favourites,
for: user,
as: :activity
})
end
},
formatters: formatters()
)
end
defp render_long_thread(user) do
%{public_thread: public, private_thread: private} =
Agent.get(:benchmark_state, fn state -> state end)
opts = %{for: user}
public_activity = Activity.get_by_id_with_object(public.id)
private_activity = Activity.get_by_id_with_object(private.id)
Benchee.run(
%{
"render" => fn opts ->
StatusView.render("show.json", opts)
end
},
inputs: %{
"Public root" => Map.put(opts, :activity, public_activity),
"Private root" => Map.put(opts, :activity, private_activity)
},
formatters: formatters()
)
fetch_opts = opts_for_long_thread(user)
public_context =
ActivityPub.fetch_activities_for_context(
public.data["context"],
Map.put(fetch_opts, :exclude_id, public.id)
)
private_context =
ActivityPub.fetch_activities_for_context(
private.data["context"],
Map.put(fetch_opts, :exclude_id, private.id)
)
Benchee.run(
%{
"render" => fn opts ->
StatusView.render("context.json", opts)
end
},
inputs: %{
"Public context" => %{user: user, activity: public_activity, activities: public_context},
"Private context" => %{
user: user,
activity: private_activity,
activities: private_context
}
},
formatters: formatters()
)
end
defp fetch_timelines_with_reply_filtering(user) do
public_params = opts_for_public_timeline(user)
Benchee.run(
%{
"Public timeline without reply filtering" => fn ->
ActivityPub.fetch_public_activities(public_params)
end,
"Public timeline with reply filtering - following" => fn ->
public_params
|> Map.put(:reply_visibility, "following")
|> Map.put(:reply_filtering_user, user)
|> ActivityPub.fetch_public_activities()
end,
"Public timeline with reply filtering - self" => fn ->
public_params
|> Map.put(:reply_visibility, "self")
|> Map.put(:reply_filtering_user, user)
|> ActivityPub.fetch_public_activities()
end
},
formatters: formatters()
)
private_params = opts_for_home_timeline(user)
recipients = [user.ap_id | User.following(user)]
Benchee.run(
%{
"Home timeline without reply filtering" => fn ->
ActivityPub.fetch_activities(recipients, private_params)
end,
"Home timeline with reply filtering - following" => fn ->
private_params =
private_params
|> Map.put(:reply_filtering_user, user)
|> Map.put(:reply_visibility, "following")
ActivityPub.fetch_activities(recipients, private_params)
end,
"Home timeline with reply filtering - self" => fn ->
private_params =
private_params
|> Map.put(:reply_filtering_user, user)
|> Map.put(:reply_visibility, "self")
ActivityPub.fetch_activities(recipients, private_params)
end
},
formatters: formatters()
)
end
end
defmodule Pleroma.LoadTesting.Helper do
alias Ecto.Adapters.SQL
alias Pleroma.Repo
def to_sec(microseconds), do: microseconds / 1_000_000
def clean_tables do
IO.puts("Deleting old data...\n")
SQL.query!(Repo, "TRUNCATE users CASCADE;")
SQL.query!(Repo, "TRUNCATE activities CASCADE;")
SQL.query!(Repo, "TRUNCATE objects CASCADE;")
SQL.query!(Repo, "TRUNCATE oban_jobs CASCADE;")
end
end
defmodule Pleroma.LoadTesting.Users do
@moduledoc """
Module for generating users with friends.
"""
import Ecto.Query
import Pleroma.LoadTesting.Helper, only: [to_sec: 1]
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.User.Query
@defaults [
users: 20_000,
friends: 100
]
@max_concurrency 10
@spec generate(keyword()) :: User.t()
def generate(opts \\ []) do
opts = Keyword.merge(@defaults, opts)
generate_users(opts[:users])
main_user =
Repo.one(from(u in User, where: u.local == true, order_by: fragment("RANDOM()"), limit: 1))
make_friends(main_user, opts[:friends])
User.get_by_id(main_user.id)
end
def generate_users(max) do
IO.puts("Starting generating #{max} users...")
{time, users} =
:timer.tc(fn ->
Task.async_stream(
1..max,
&generate_user(&1),
max_concurrency: @max_concurrency,
timeout: 30_000
)
|> Enum.to_list()
end)
IO.puts("Generating users took #{to_sec(time)} sec.\n")
users
end
defp generate_user(i) do
remote = Enum.random([true, false])
%User{
name: "Test テスト User #{i}",
email: "user#{i}@example.com",
nickname: "nick#{i}",
password_hash: Pleroma.Password.Pbkdf2.hash_pwd_salt("test"),
bio: "Tester Number #{i}",
local: !remote
}
|> user_urls()
|> Repo.insert!()
end
defp user_urls(%{local: true} = user) do
urls = %{
ap_id: User.ap_id(user),
follower_address: User.ap_followers(user),
following_address: User.ap_following(user)
}
Map.merge(user, urls)
end
defp user_urls(%{local: false} = user) do
base_domain = Enum.random(["domain1.com", "domain2.com", "domain3.com"])
ap_id = "https://#{base_domain}/users/#{user.nickname}"
urls = %{
ap_id: ap_id,
follower_address: ap_id <> "/followers",
following_address: ap_id <> "/following"
}
Map.merge(user, urls)
end
def make_friends(main_user, max) when is_integer(max) do
IO.puts("Starting making friends for #{max} users...")
{time, _} =
:timer.tc(fn ->
number_of_users =
(max / 2)
|> Kernel.trunc()
main_user
|> get_users(%{limit: number_of_users, local: :local})
|> run_stream(main_user)
main_user
|> get_users(%{limit: number_of_users, local: :external})
|> run_stream(main_user)
end)
IO.puts("Making friends took #{to_sec(time)} sec.\n")
end
def make_friends(%User{} = main_user, %User{} = user) do
{:ok, _, _} = User.follow(main_user, user)
{:ok, _, _} = User.follow(user, main_user)
end
@spec get_users(User.t(), keyword()) :: [User.t()]
def get_users(user, opts) do
criteria = %{limit: opts[:limit]}
criteria =
if opts[:local] do
Map.put(criteria, opts[:local], true)
else
criteria
end
criteria =
if opts[:friends?] do
Map.put(criteria, :friends, user)
else
criteria
end
query =
criteria
|> Query.build()
|> random_without_user(user)
query =
if opts[:friends?] == false do
friends_ids =
%{friends: user}
|> Query.build()
|> Repo.all()
|> Enum.map(& &1.id)
from(u in query, where: u.id not in ^friends_ids)
else
query
end
Repo.all(query)
end
defp random_without_user(query, user) do
from(u in query,
where: u.id != ^user.id,
order_by: fragment("RANDOM()")
)
end
defp run_stream(users, main_user) do
Task.async_stream(users, &make_friends(main_user, &1),
max_concurrency: @max_concurrency,
timeout: 30_000
)
|> Stream.run()
end
@spec prepare_users(User.t(), keyword()) :: map()
def prepare_users(user, opts) do
friends_limit = opts[:friends_used]
non_friends_limit = opts[:non_friends_used]
%{
user: user,
friends_local: fetch_users(user, friends_limit, :local, true),
friends_remote: fetch_users(user, friends_limit, :external, true),
non_friends_local: fetch_users(user, non_friends_limit, :local, false),
non_friends_remote: fetch_users(user, non_friends_limit, :external, false)
}
end
defp fetch_users(user, limit, local, friends?) do
user
|> get_users(limit: limit, local: local, friends?: friends?)
|> Enum.shuffle()
end
end
This diff is collapsed.
This diff is collapsed.
defmodule Mix.Tasks.Pleroma.Benchmarks.Timelines do
use Mix.Task
import Pleroma.LoadTesting.Helper, only: [clean_tables: 0]
alias Pleroma.Web.CommonAPI
alias Plug.Conn
def run(_args) do
Mix.Pleroma.start_pleroma()
# Cleaning tables
clean_tables()
[{:ok, user} | users] = Pleroma.LoadTesting.Users.generate_users(1000)
# Let the user make 100 posts
1..100
|> Enum.each(fn i -> CommonAPI.post(user, %{status: to_string(i)}) end)
# Let 10 random users post
posts =
users
|> Enum.take_random(10)
|> Enum.map(fn {:ok, random_user} ->
{:ok, activity} = CommonAPI.post(random_user, %{status: "."})
activity
end)
# let our user repeat them
posts
|> Enum.each(fn activity ->
CommonAPI.repeat(activity.id, user)
end)
Benchee.run(
%{
"user timeline, no followers" => fn reading_user ->
conn =
Phoenix.ConnTest.build_conn()
|> Conn.assign(:user, reading_user)
|> Conn.assign(:skip_link_headers, true)
Pleroma.Web.MastodonAPI.AccountController.statuses(conn, %{id: user.id})
end
},
inputs: %{"user" => user, "no user" => nil},
time: 60
)
users
|> Enum.each(fn {:ok, follower} -> Pleroma.User.follow(follower, user) end)
Benchee.run(
%{
"user timeline, all following" => fn reading_user ->
conn =
Phoenix.ConnTest.build_conn()
|> Conn.assign(:user, reading_user)
|> Conn.assign(:skip_link_headers, true)
Pleroma.Web.MastodonAPI.AccountController.statuses(conn, %{id: user.id})
end
},
inputs: %{"user" => user, "no user" => nil},
time: 60
)
end
end
This diff is collapsed.
Add new activity actor/type index. Greatly speeds up retrieval of rare types (like "Listen")
Support Mitra-style emoji likes.
Implement language detection with fastText
\ No newline at end of file
Fix release builds
Support translation providers (DeepL, LibreTranslate)
\ No newline at end of file
Truncate the length of Rich Media title and description fields
## Dependencies
Assuming an AMD64 Alpine system, you're going to need the following packages
- `qemu qemu-openrc qemu-arm qemu-aarch64` for binfmt
- `docker-cli-buildx` for building the images
## Setting up
```
docker login git.pleroma.social:5050
doas rc-service qemu-binfmt start
```
FROM elixir:1.14.5-otp-25
# Single RUN statement, otherwise intermediate images are created
# https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#run
RUN apt-get update &&\
apt-get install -y libmagic-dev cmake libimage-exiftool-perl ffmpeg &&\
mix local.hex --force &&\
mix local.rebar --force
This diff is collapsed.
This diff is collapsed.