Merge remote-tracking branch 'MAIN/develop' into feature/jobs

This commit is contained in:
Egor Kislitsyn 2019-01-31 15:07:49 +07:00
commit d3677d2b4d
81 changed files with 817 additions and 266 deletions

View File

@ -8,76 +8,81 @@ Pleroma is written in Elixir, high-performance and can run on small devices like
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 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).
Client applications that are committed to supporting Pleroma:
* Mastalab (Android)
* Tusky (Android)
* Twidere (Android)
* Mast (iOS)
* Amaroq (iOS)
Client applications that are known to work well: Client applications that are known to work well:
* Twidere
* Tusky
* Pawoo (Android + iOS) * Pawoo (Android + iOS)
* Subway Tooter
* Amaroq (iOS)
* Tootdon (Android + iOS) * Tootdon (Android + iOS)
* Tootle (iOS) * Tootle (iOS)
* Whalebird (Windows + Mac + Linux) * Whalebird (Windows + Mac + Linux)
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. 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>.
## Installation ## Installation
### Docker ### 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. While we dont 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 ### Dependencies
* Postgresql version 9.6 or newer * 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). * Elixir version 1.7 or newer. If your distribution only has an old version available, check [Elixirs install page](https://elixir-lang.org/install.html) or use a tool like [asdf](https://github.com/asdf-vm/asdf).
* Build-essential tools * Build-essential tools
### Configuration ### Configuration
* Run `mix deps.get` to install elixir dependencies. * 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.
* 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.
* 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) * 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.
* 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 ## 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. * 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.
### Frontends ### Frontends
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. 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.
### As systemd service (with provided .service file) ### As systemd service (with provided .service file)
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. 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`.
Logs can be watched by using `journalctl -fu pleroma.service`.
### As OpenRC service (with provided RC file) ### 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: 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`
``rc-update add pleroma``
### Standalone/run by other means ### Standalone/run by other means
Run `mix phx.server` in repository's root, it will output log into stdout/stderr.
Run `mix phx.server` in repositorys root, it will output log into stdout/stderr.
### Using an upstream proxy for federation ### 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: Add the following to your `dev.secret.exs` or `prod.secret.exs` if you want to proxify all http requests that Pleroma makes to an upstream proxy server:
config :pleroma, :http, ```elixir
config :pleroma, :http,
proxy_url: "127.0.0.1:8123" proxy_url: "127.0.0.1:8123"
```
This is useful for running pleroma inside Tor or i2p. This is useful for running Pleroma inside Tor or I2P.
## Customization and contribution
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.
## Troubleshooting ## Troubleshooting
### No incoming federation ### No incoming federation
Check that you correctly forward the "host" header to backend. It is needed to validate signatures. Check that you correctly forward the `host` header to the backend. It is needed to validate signatures.

View File

@ -15,6 +15,20 @@
seconds_valid: 60, seconds_valid: 60,
method: Pleroma.Captcha.Kocaptcha method: Pleroma.Captcha.Kocaptcha
config :pleroma, :hackney_pools,
federation: [
max_connections: 50,
timeout: 150_000
],
media: [
max_connections: 50,
timeout: 150_000
],
upload: [
max_connections: 25,
timeout: 300_000
]
config :pleroma, Pleroma.Captcha.Kocaptcha, endpoint: "https://captcha.kotobank.ch" config :pleroma, Pleroma.Captcha.Kocaptcha, endpoint: "https://captcha.kotobank.ch"
# Upload configuration # Upload configuration
@ -22,7 +36,14 @@
uploader: Pleroma.Uploaders.Local, uploader: Pleroma.Uploaders.Local,
filters: [], filters: [],
proxy_remote: false, proxy_remote: false,
proxy_opts: [] proxy_opts: [
redirect_on_failure: false,
max_body_length: 25 * 1_048_576,
http: [
follow_redirect: true,
pool: :upload
]
]
config :pleroma, Pleroma.Uploaders.Local, uploads: "uploads" config :pleroma, Pleroma.Uploaders.Local, uploads: "uploads"
@ -154,6 +175,7 @@
Pleroma.HTML.Scrubber.Default Pleroma.HTML.Scrubber.Default
] ]
# Deprecated, will be gone in 1.0
config :pleroma, :fe, config :pleroma, :fe,
theme: "pleroma-dark", theme: "pleroma-dark",
logo: "/static/logo.png", logo: "/static/logo.png",
@ -172,6 +194,24 @@
subject_line_behavior: "email", subject_line_behavior: "email",
always_show_subject_input: true always_show_subject_input: true
config :pleroma, :frontend_configurations,
pleroma_fe: %{
theme: "pleroma-dark",
logo: "/static/logo.png",
background: "/images/city.jpg",
redirectRootNoLogin: "/main/all",
redirectRootLogin: "/main/friends",
showInstanceSpecificPanel: true,
scopeOptionsEnabled: false,
formattingOptionsEnabled: false,
collapseMessageWithSubject: false,
hidePostStats: false,
hideUserStats: false,
scopeCopy: true,
subjectLineBehavior: "email",
alwaysShowSubjectInput: true
}
config :pleroma, :activitypub, config :pleroma, :activitypub,
accept_blocks: true, accept_blocks: true,
unfollow_blocked: true, unfollow_blocked: true,
@ -195,7 +235,16 @@
reject: [], reject: [],
accept: [] accept: []
config :pleroma, :media_proxy, enabled: false config :pleroma, :media_proxy,
enabled: false,
proxy_opts: [
redirect_on_failure: false,
max_body_length: 25 * 1_048_576,
http: [
follow_redirect: true,
pool: :media
]
]
config :pleroma, :chat, enabled: true config :pleroma, :chat, enabled: true

View File

@ -1,13 +1,9 @@
# Authentication # Pleroma API
Requests that require it can be authenticated with [an OAuth token](https://tools.ietf.org/html/rfc6749), the `_pleroma_key` cookie, or [HTTP Basic Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization). Requests that require it can be authenticated with [an OAuth token](https://tools.ietf.org/html/rfc6749), the `_pleroma_key` cookie, or [HTTP Basic Authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization).
# Request parameters
Request parameters can be passed via [query strings](https://en.wikipedia.org/wiki/Query_string) or as [form data](https://www.w3.org/TR/html401/interact/forms.html). Files must be uploaded as `multipart/form-data`. Request parameters can be passed via [query strings](https://en.wikipedia.org/wiki/Query_string) or as [form data](https://www.w3.org/TR/html401/interact/forms.html). Files must be uploaded as `multipart/form-data`.
# Endpoints
## `/api/pleroma/emoji` ## `/api/pleroma/emoji`
### Lists the custom emoji on that server. ### Lists the custom emoji on that server.
* Method: `GET` * Method: `GET`
@ -15,6 +11,7 @@ Request parameters can be passed via [query strings](https://en.wikipedia.org/wi
* Params: none * Params: none
* Response: JSON * Response: JSON
* Example response: `{"kalsarikannit_f":"/finmoji/128px/kalsarikannit_f-128.png","perkele":"/finmoji/128px/perkele-128.png","blobdab":"/emoji/blobdab.png","happiness":"/finmoji/128px/happiness-128.png"}` * Example response: `{"kalsarikannit_f":"/finmoji/128px/kalsarikannit_f-128.png","perkele":"/finmoji/128px/perkele-128.png","blobdab":"/emoji/blobdab.png","happiness":"/finmoji/128px/happiness-128.png"}`
* Note: Same data as Mastodon APIs `/api/v1/custom_emojis` but in a different format
## `/api/pleroma/follow_import` ## `/api/pleroma/follow_import`
### Imports your follows, for example from a Mastodon CSV file. ### Imports your follows, for example from a Mastodon CSV file.

View File

@ -102,7 +102,24 @@ config :pleroma, Pleroma.Mailer,
* `backends`: `:console` is used to send logs to stdout, `{ExSyslogger, :ex_syslogger}` to log to syslog * `backends`: `:console` is used to send logs to stdout, `{ExSyslogger, :ex_syslogger}` to log to syslog
See: [loggers documentation](https://hexdocs.pm/logger/Logger.html) and [ex_sysloggers documentation](https://hexdocs.pm/ex_syslogger/) See: [loggers documentation](https://hexdocs.pm/logger/Logger.html) and [ex_sysloggers documentation](https://hexdocs.pm/ex_syslogger/)
## :frontend_configurations
This can be used to configure a keyword list that keeps the configuration data for any kind of frontend. By default, settings for `pleroma_fe` are configured.
Frontends can access these settings at `/api/pleroma/frontend_configurations`
To add your own configuration for PleromaFE, use it like this:
`config :pleroma, :frontend_configurations, pleroma_fe: %{redirectRootNoLogin: "/main/all", ...}`
These settings need to be complete, they will override the defaults. See `priv/static/static/config.json` for the available keys.
## :fe ## :fe
__THIS IS DEPRECATED__
If you are using this method, please change it to the `frontend_configurations` method. Please set this option to false in your config like this: `config :pleroma, :fe, false`.
This section is used to configure Pleroma-FE, unless ``:managed_config`` in ``:instance`` is set to false. This section is used to configure Pleroma-FE, unless ``:managed_config`` in ``:instance`` is set to false.
* `theme`: Which theme to use, they are defined in ``styles.json`` * `theme`: Which theme to use, they are defined in ``styles.json``
@ -234,3 +251,20 @@ This config contains two queues: `federator_incoming` and `federator_outgoing`.
* Pleroma.Web.Metadata.Providers.OpenGraph * Pleroma.Web.Metadata.Providers.OpenGraph
* Pleroma.Web.Metadata.Providers.TwitterCard * Pleroma.Web.Metadata.Providers.TwitterCard
* `unfurl_nsfw`: If set to `true` nsfw attachments will be shown in previews * `unfurl_nsfw`: If set to `true` nsfw attachments will be shown in previews
## :hackney_pools
Advanced. Tweaks Hackney (http client) connections pools.
There's three pools used:
* `:federation` for the federation jobs.
You may want this pool max_connections to be at least equal to the number of federator jobs + retry queue jobs.
* `:media` for rich media, media proxy
* `:upload` for uploaded media (if using a remote uploader and `proxy_remote: true`)
For each pool, the options are:
* `max_connections` - how much connections a pool can hold
* `timeout` - retention duration for connections

View File

@ -12,7 +12,7 @@ export PORT=4000
export MIX_ENV=prod export MIX_ENV=prod
# Ask process to terminate within 30 seconds, otherwise kill it # Ask process to terminate within 30 seconds, otherwise kill it
retry="SIGTERM/30 SIGKILL/5" retry="SIGTERM/30/SIGKILL/5"
pidfile="/var/run/pleroma.pid" pidfile="/var/run/pleroma.pid"

View File

@ -22,6 +22,8 @@ def user_agent() do
def start(_type, _args) do def start(_type, _args) do
import Cachex.Spec import Cachex.Spec
Pleroma.Config.DeprecationWarnings.warn()
# Define workers and child supervisors to be supervised # Define workers and child supervisors to be supervised
children = children =
[ [
@ -99,7 +101,10 @@ def start(_type, _args) do
], ],
id: :cachex_idem id: :cachex_idem
), ),
worker(Pleroma.FlakeId, []), worker(Pleroma.FlakeId, [])
] ++
hackney_pool_children() ++
[
worker(Pleroma.Web.Federator.RetryQueue, []), worker(Pleroma.Web.Federator.RetryQueue, []),
worker(Pleroma.Stats, []), worker(Pleroma.Stats, []),
worker(Pleroma.Web.Push, []), worker(Pleroma.Web.Push, []),
@ -120,6 +125,20 @@ def start(_type, _args) do
Supervisor.start_link(children, opts) Supervisor.start_link(children, opts)
end end
def enabled_hackney_pools() do
[:media] ++
if Application.get_env(:tesla, :adapter) == Tesla.Adapter.Hackney do
[:federation]
else
[]
end ++
if Pleroma.Config.get([Pleroma.Uploader, :proxy_remote]) do
[:upload]
else
[]
end
end
if Mix.env() == :test do if Mix.env() == :test do
defp streamer_child(), do: [] defp streamer_child(), do: []
defp chat_child(), do: [] defp chat_child(), do: []
@ -136,4 +155,11 @@ defp chat_child() do
end end
end end
end end
defp hackney_pool_children() do
for pool <- enabled_hackney_pools() do
options = Pleroma.Config.get([:hackney_pools, pool])
:hackney_pool.child_spec(pool, options)
end
end
end end

View File

@ -0,0 +1,20 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Config.DeprecationWarnings do
require Logger
def check_frontend_config_mechanism() do
if Pleroma.Config.get(:fe) do
Logger.warn("""
!!!DEPRECATION WARNING!!!
You are using the old configuration mechanism for the frontend. Please check config.md.
""")
end
end
def warn do
check_frontend_config_mechanism()
end
end

View File

@ -33,6 +33,10 @@ def to_string(flake = <<_::integer-size(64), _::integer-size(48), _::integer-siz
def to_string(s), do: s def to_string(s), do: s
def from_string(int) when is_integer(int) do
from_string(Kernel.to_string(int))
end
for i <- [-1, 0] do for i <- [-1, 0] do
def from_string(unquote(i)), do: <<0::integer-size(128)>> def from_string(unquote(i)), do: <<0::integer-size(128)>>
def from_string(unquote(Kernel.to_string(i))), do: <<0::integer-size(128)>> def from_string(unquote(Kernel.to_string(i))), do: <<0::integer-size(128)>>
@ -90,7 +94,7 @@ def start_link do
@impl GenServer @impl GenServer
def init([]) do def init([]) do
{:ok, %FlakeId{node: mac(), time: time()}} {:ok, %FlakeId{node: worker_id(), time: time()}}
end end
@impl GenServer @impl GenServer
@ -161,23 +165,8 @@ defp time do
1_000_000_000 * mega_seconds + seconds * 1000 + :erlang.trunc(micro_seconds / 1000) 1_000_000_000 * mega_seconds + seconds * 1000 + :erlang.trunc(micro_seconds / 1000)
end end
def mac do defp worker_id() do
{:ok, addresses} = :inet.getifaddrs() <<worker::integer-size(48)>> = :crypto.strong_rand_bytes(6)
macids =
Enum.reduce(addresses, [], fn {_iface, attrs}, acc ->
case attrs[:hwaddr] do
[0, 0, 0 | _] -> acc
mac when is_list(mac) -> [mac_to_worker_id(mac) | acc]
_ -> acc
end
end)
List.first(macids)
end
def mac_to_worker_id(mac) do
<<worker::integer-size(48)>> = :binary.list_to_bin(mac)
worker worker
end end
end end

View File

@ -58,6 +58,20 @@ defp generate_scrubber_signature(scrubbers) do
"#{signature}#{to_string(scrubber)}" "#{signature}#{to_string(scrubber)}"
end) end)
end end
def extract_first_external_url(object, content) do
key = "URL|#{object.id}"
Cachex.fetch!(:scrubber_cache, key, fn _key ->
result =
content
|> Floki.filter_out("a.mention")
|> Floki.attribute("a", "href")
|> Enum.at(0)
{:commit, {:ok, result}}
end)
end
end end
defmodule Pleroma.HTML.Scrubber.TwitterText do defmodule Pleroma.HTML.Scrubber.TwitterText do

View File

@ -10,7 +10,8 @@ defmodule Pleroma.HTTP.Connection do
@hackney_options [ @hackney_options [
timeout: 10000, timeout: 10000,
recv_timeout: 20000, recv_timeout: 20000,
follow_redirect: true follow_redirect: true,
pool: :federation
] ]
@adapter Application.get_env(:tesla, :adapter) @adapter Application.get_env(:tesla, :adapter)

View File

@ -35,7 +35,8 @@ def for_user(user, opts \\ %{}) do
n in Notification, n in Notification,
where: n.user_id == ^user.id, where: n.user_id == ^user.id,
order_by: [desc: n.id], order_by: [desc: n.id],
preload: [:activity], join: activity in assoc(n, :activity),
preload: [activity: activity],
limit: 20 limit: 20
) )
@ -66,7 +67,8 @@ def get(%{id: user_id} = _user, id) do
from( from(
n in Notification, n in Notification,
where: n.id == ^id, where: n.id == ^id,
preload: [:activity] join: activity in assoc(n, :activity),
preload: [activity: activity]
) )
notification = Repo.one(query) notification = Repo.one(query)

View File

@ -33,7 +33,12 @@ def call(conn, _) do
# #
@spec fetch_user_and_token(String.t()) :: {:ok, User.t(), Token.t()} | nil @spec fetch_user_and_token(String.t()) :: {:ok, User.t(), Token.t()} | nil
defp fetch_user_and_token(token) do defp fetch_user_and_token(token) do
query = from(q in Token, where: q.token == ^token, preload: [:user]) query =
from(t in Token,
where: t.token == ^token,
join: user in assoc(t, :user),
preload: [user: user]
)
with %Token{user: %{info: %{deactivated: false} = _} = user} = token_record <- Repo.one(query) do with %Token{user: %{info: %{deactivated: false} = _} = user} = token_record <- Repo.one(query) do
{:ok, user, token_record} {:ok, user, token_record}

View File

@ -24,7 +24,8 @@ def put_file(upload) do
extension = String.split(upload.name, ".") |> List.last() extension = String.split(upload.name, ".") |> List.last()
query = "#{cgi}?#{extension}" query = "#{cgi}?#{extension}"
with {:ok, %{status: 200, body: body}} <- @httpoison.post(query, file_data) do with {:ok, %{status: 200, body: body}} <-
@httpoison.post(query, file_data, adapter: [pool: :default]) do
remote_file_name = String.split(body) |> List.first() remote_file_name = String.split(body) |> List.first()
public_url = "#{files}/#{remote_file_name}.#{extension}" public_url = "#{files}/#{remote_file_name}.#{extension}"
{:ok, {:url, public_url}} {:ok, {:url, public_url}}

View File

@ -309,20 +309,21 @@ def maybe_follow(%User{} = follower, %User{info: _info} = followed) do
@doc "A mass follow for local users. Ignores blocks and has no side effects" @doc "A mass follow for local users. Ignores blocks and has no side effects"
@spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()} @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
def follow_all(follower, followeds) do def follow_all(follower, followeds) do
following = followed_addresses = Enum.map(followeds, fn %{follower_address: fa} -> fa end)
(follower.following ++ Enum.map(followeds, fn %{follower_address: fa} -> fa end))
|> Enum.uniq()
{:ok, follower} = q =
follower from(u in User,
|> follow_changeset(%{following: following}) where: u.id == ^follower.id,
|> update_and_set_cache update: [set: [following: fragment("array_cat(?, ?)", u.following, ^followed_addresses)]]
)
{1, [follower]} = Repo.update_all(q, [], returning: true)
Enum.each(followeds, fn followed -> Enum.each(followeds, fn followed ->
update_follower_count(followed) update_follower_count(followed)
end) end)
{:ok, follower} set_cache(follower)
end end
def follow(%User{} = follower, %User{info: info} = followed) do def follow(%User{} = follower, %User{info: info} = followed) do
@ -343,18 +344,17 @@ def follow(%User{} = follower, %User{info: info} = followed) do
Websub.subscribe(follower, followed) Websub.subscribe(follower, followed)
end end
following = q =
[ap_followers | follower.following] from(u in User,
|> Enum.uniq() where: u.id == ^follower.id,
update: [push: [following: ^ap_followers]]
)
follower = {1, [follower]} = Repo.update_all(q, [], returning: true)
follower
|> follow_changeset(%{following: following})
|> update_and_set_cache
{:ok, _} = update_follower_count(followed) {:ok, _} = update_follower_count(followed)
follower set_cache(follower)
end end
end end
@ -362,17 +362,18 @@ def unfollow(%User{} = follower, %User{} = followed) do
ap_followers = followed.follower_address ap_followers = followed.follower_address
if following?(follower, followed) and follower.ap_id != followed.ap_id do if following?(follower, followed) and follower.ap_id != followed.ap_id do
following = q =
follower.following from(u in User,
|> List.delete(ap_followers) where: u.id == ^follower.id,
update: [pull: [following: ^ap_followers]]
)
{:ok, follower} = {1, [follower]} = Repo.update_all(q, [], returning: true)
follower
|> follow_changeset(%{following: following})
|> update_and_set_cache
{:ok, followed} = update_follower_count(followed) {:ok, followed} = update_follower_count(followed)
set_cache(follower)
{:ok, follower, Utils.fetch_latest_follow(follower, followed)} {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
else else
{:error, "Not subscribed!"} {:error, "Not subscribed!"}
@ -423,12 +424,16 @@ def get_by_guessed_nickname(ap_id) do
get_by_nickname(nickname) get_by_nickname(nickname)
end end
def update_and_set_cache(changeset) do def set_cache(user) do
with {:ok, user} <- Repo.update(changeset) do
Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user) Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
Cachex.put(:user_cache, "nickname:#{user.nickname}", user) Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user)) Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
{:ok, user} {:ok, user}
end
def update_and_set_cache(changeset) do
with {:ok, user} <- Repo.update(changeset) do
set_cache(user)
else else
e -> e e -> e
end end

View File

@ -64,7 +64,7 @@ defp check_actor_is_active(actor) do
end end
end end
defp check_remote_limit(%{"object" => %{"content" => content}}) do defp check_remote_limit(%{"object" => %{"content" => content}}) when not is_nil(content) do
limit = Pleroma.Config.get([:instance, :remote_limit]) limit = Pleroma.Config.get([:instance, :remote_limit])
String.length(content) <= limit String.length(content) <= limit
end end
@ -88,6 +88,10 @@ def insert(map, local \\ true) when is_map(map) do
recipients: recipients recipients: recipients
}) })
Task.start(fn ->
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
end)
Notification.create_notifications(activity) Notification.create_notifications(activity)
stream_out(activity) stream_out(activity)
{:ok, activity} {:ok, activity}
@ -426,7 +430,34 @@ defp restrict_since(query, %{"since_id" => since_id}) do
defp restrict_since(query, _), do: query defp restrict_since(query, _), do: query
defp restrict_tag(query, %{"tag" => tag}) do defp restrict_tag_reject(query, %{"tag_reject" => tag_reject})
when is_list(tag_reject) and tag_reject != [] do
from(
activity in query,
where: fragment("(not (? #> '{\"object\",\"tag\"}') \\?| ?)", activity.data, ^tag_reject)
)
end
defp restrict_tag_reject(query, _), do: query
defp restrict_tag_all(query, %{"tag_all" => tag_all})
when is_list(tag_all) and tag_all != [] do
from(
activity in query,
where: fragment("(? #> '{\"object\",\"tag\"}') \\?& ?", activity.data, ^tag_all)
)
end
defp restrict_tag_all(query, _), do: query
defp restrict_tag(query, %{"tag" => tag}) when is_list(tag) do
from(
activity in query,
where: fragment("(? #> '{\"object\",\"tag\"}') \\?| ?", activity.data, ^tag)
)
end
defp restrict_tag(query, %{"tag" => tag}) when is_binary(tag) do
from( from(
activity in query, activity in query,
where: fragment("? <@ (? #> '{\"object\",\"tag\"}')", ^tag, activity.data) where: fragment("? <@ (? #> '{\"object\",\"tag\"}')", ^tag, activity.data)
@ -575,6 +606,8 @@ def fetch_activities_query(recipients, opts \\ %{}) do
base_query base_query
|> restrict_recipients(recipients, opts["user"]) |> restrict_recipients(recipients, opts["user"])
|> restrict_tag(opts) |> restrict_tag(opts)
|> restrict_tag_reject(opts)
|> restrict_tag_all(opts)
|> restrict_since(opts) |> restrict_since(opts)
|> restrict_local(opts) |> restrict_local(opts)
|> restrict_limit(opts) |> restrict_limit(opts)

View File

@ -141,11 +141,11 @@ def fix_actor(%{"attributedTo" => actor} = object) do
|> Map.put("actor", get_actor(%{"actor" => actor})) |> Map.put("actor", get_actor(%{"actor" => actor}))
end end
def fix_likes(%{"likes" => likes} = object)
when is_bitstring(likes) do
# Check for standardisation # Check for standardisation
# This is what Peertube does # This is what Peertube does
# curl -H 'Accept: application/activity+json' $likes | jq .totalItems # curl -H 'Accept: application/activity+json' $likes | jq .totalItems
# Prismo returns only an integer (count) as "likes"
def fix_likes(%{"likes" => likes} = object) when not is_map(likes) do
object object
|> Map.put("likes", []) |> Map.put("likes", [])
|> Map.put("like_count", 0) |> Map.put("like_count", 0)
@ -453,9 +453,9 @@ def handle_incoming(
{:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"), {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]), %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
{:ok, activity} <- {:ok, activity} <-
ActivityPub.accept(%{ ActivityPub.reject(%{
to: follow_activity.data["to"], to: follow_activity.data["to"],
type: "Accept", type: "Reject",
actor: followed.ap_id, actor: followed.ap_id,
object: follow_activity.data["id"], object: follow_activity.data["id"],
local: false local: false

View File

@ -316,6 +316,25 @@ def remove_like_from_object(%Activity{data: %{"actor" => actor}}, object) do
@doc """ @doc """
Updates a follow activity's state (for locked accounts). Updates a follow activity's state (for locked accounts).
""" """
def update_follow_state(
%Activity{data: %{"actor" => actor, "object" => object, "state" => "pending"}} = activity,
state
) do
try do
Ecto.Adapters.SQL.query!(
Repo,
"UPDATE activities SET data = jsonb_set(data, '{state}', $1) WHERE data->>'type' = 'Follow' AND data->>'actor' = $2 AND data->>'object' = $3 AND data->>'state' = 'pending'",
[state, actor, object]
)
activity = Repo.get(Activity, activity.id)
{:ok, activity}
rescue
e ->
{:error, e}
end
end
def update_follow_state(%Activity{} = activity, state) do def update_follow_state(%Activity{} = activity, state) do
with new_data <- with new_data <-
activity.data activity.data

View File

@ -499,7 +499,8 @@ def update_media(%{assigns: %{user: user}} = conn, data) do
def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
with {:ok, object} <- with {:ok, object} <-
ActivityPub.upload(file, ActivityPub.upload(
file,
actor: User.ap_id(user), actor: User.ap_id(user),
description: Map.get(data, "description") description: Map.get(data, "description")
) do ) do
@ -540,15 +541,34 @@ def reblogged_by(conn, %{"id" => id}) do
def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
local_only = params["local"] in [true, "True", "true", "1"] local_only = params["local"] in [true, "True", "true", "1"]
params = tags =
[params["tag"], params["any"]]
|> List.flatten()
|> Enum.uniq()
|> Enum.filter(& &1)
|> Enum.map(&String.downcase(&1))
tag_all =
params["all"] ||
[]
|> Enum.map(&String.downcase(&1))
tag_reject =
params["none"] ||
[]
|> Enum.map(&String.downcase(&1))
query_params =
params params
|> Map.put("type", "Create") |> Map.put("type", "Create")
|> Map.put("local_only", local_only) |> Map.put("local_only", local_only)
|> Map.put("blocking_user", user) |> Map.put("blocking_user", user)
|> Map.put("tag", String.downcase(params["tag"])) |> Map.put("tag", tags)
|> Map.put("tag_all", tag_all)
|> Map.put("tag_reject", tag_reject)
activities = activities =
ActivityPub.fetch_public_activities(params) ActivityPub.fetch_public_activities(query_params)
|> Enum.reverse() |> Enum.reverse()
conn conn
@ -1081,7 +1101,9 @@ def login(conn, %{"code" => code}) do
def login(conn, _) do def login(conn, _) do
with {:ok, app} <- get_or_make_app() do with {:ok, app} <- get_or_make_app() do
path = path =
o_auth_path(conn, :authorize, o_auth_path(
conn,
:authorize,
response_type: "code", response_type: "code",
client_id: app.client_id, client_id: app.client_id,
redirect_uri: ".", redirect_uri: ".",
@ -1289,7 +1311,8 @@ def suggestions(%{assigns: %{user: user}} = conn, _) do
[], [],
adapter: [ adapter: [
timeout: timeout, timeout: timeout,
recv_timeout: timeout recv_timeout: timeout,
pool: :default
] ]
), ),
{:ok, data} <- Jason.decode(body) do {:ok, data} <- Jason.decode(body) do
@ -1322,6 +1345,22 @@ def suggestions(%{assigns: %{user: user}} = conn, _) do
end end
end end
def status_card(conn, %{"id" => status_id}) do
with %Activity{} = activity <- Repo.get(Activity, status_id),
true <- ActivityPub.is_public?(activity) do
data =
StatusView.render(
"card.json",
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
)
json(conn, data)
else
_e ->
%{}
end
end
def try_render(conn, target, params) def try_render(conn, target, params)
when is_binary(target) do when is_binary(target) do
res = render(conn, target, params) res = render(conn, target, params)

View File

@ -112,7 +112,9 @@ defp do_render("account.json", %{user: user} = opts) do
# Pleroma extension # Pleroma extension
pleroma: %{ pleroma: %{
confirmation_pending: user_info.confirmation_pending, confirmation_pending: user_info.confirmation_pending,
tags: user.tags tags: user.tags,
is_moderator: user.info.is_moderator,
is_admin: user.info.is_admin
} }
} }
end end

View File

@ -49,12 +49,11 @@ def render("index.json", opts) do
replied_to_activities = get_replied_to_activities(opts.activities) replied_to_activities = get_replied_to_activities(opts.activities)
opts.activities opts.activities
|> render_many( |> safe_render_many(
StatusView, StatusView,
"status.json", "status.json",
Map.put(opts, :replied_to_activities, replied_to_activities) Map.put(opts, :replied_to_activities, replied_to_activities)
) )
|> Enum.filter(fn x -> not is_nil(x) end)
end end
def render( def render(
@ -140,6 +139,8 @@ def render("status.json", %{activity: %{data: %{"object" => object}} = activity}
__MODULE__ __MODULE__
) )
card = render("card.json", Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity))
%{ %{
id: to_string(activity.id), id: to_string(activity.id),
uri: object["id"], uri: object["id"],
@ -148,6 +149,7 @@ def render("status.json", %{activity: %{data: %{"object" => object}} = activity}
in_reply_to_id: reply_to && to_string(reply_to.id), in_reply_to_id: reply_to && to_string(reply_to.id),
in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id), in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
reblog: nil, reblog: nil,
card: card,
content: content, content: content,
created_at: created_at, created_at: created_at,
reblogs_count: announcement_count, reblogs_count: announcement_count,
@ -176,6 +178,29 @@ def render("status.json", _) do
nil nil
end end
def render("card.json", %{rich_media: rich_media, page_url: page_url}) do
page_url = rich_media[:url] || page_url
page_url_data = URI.parse(page_url)
site_name = rich_media[:site_name] || page_url_data.host
%{
type: "link",
provider_name: site_name,
provider_url: page_url_data.scheme <> "://" <> page_url_data.host,
url: page_url,
image: rich_media[:image] |> MediaProxy.url(),
title: rich_media[:title],
description: rich_media[:description],
pleroma: %{
opengraph: rich_media
}
}
end
def render("card.json", _) do
nil
end
def render("attachment.json", %{attachment: attachment}) do def render("attachment.json", %{attachment: attachment}) do
[attachment_url | _] = attachment["url"] [attachment_url | _] = attachment["url"]
media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image" media_type = attachment_url["mediaType"] || attachment_url["mimeType"] || "image"

View File

@ -9,7 +9,8 @@ defmodule Pleroma.Web.OAuth.FallbackController do
# No user/password # No user/password
def call(conn, _) do def call(conn, _) do
conn conn
|> put_status(:unauthorized)
|> put_flash(:error, "Invalid Username/Password") |> put_flash(:error, "Invalid Username/Password")
|> OAuthController.authorize(conn.params) |> OAuthController.authorize(conn.params["authorization"])
end end
end end

View File

@ -166,10 +166,13 @@ def notice(conn, %{"id" => id}) do
end end
else else
{:public?, false} -> {:public?, false} ->
{:error, :not_found} conn
|> put_status(404)
|> Fallback.RedirectController.redirector(nil, 404)
{:activity, nil} -> {:activity, nil} ->
{:error, :not_found} conn
|> Fallback.RedirectController.redirector(nil, 404)
e -> e ->
e e

View File

@ -1,17 +0,0 @@
defmodule Pleroma.Web.RichMedia.RichMediaController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
def parse(conn, %{"url" => url}) do
case Pleroma.Web.RichMedia.Parser.parse(url) do
{:ok, data} ->
conn
|> json_response(200, data)
{:error, msg} ->
conn
|> json_response(404, msg)
end
end
end

View File

@ -0,0 +1,18 @@
# Pleroma: A lightweight social networking server
# Copyright _ 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Helpers do
alias Pleroma.{Activity, Object, HTML}
alias Pleroma.Web.RichMedia.Parser
def fetch_data_for_activity(%Activity{} = activity) do
with %Object{} = object <- Object.normalize(activity.data["object"]),
{:ok, page_url} <- HTML.extract_first_external_url(object, object.data["content"]),
{:ok, rich_media} <- Parser.parse(page_url) do
%{page_url: page_url, rich_media: rich_media}
else
_ -> %{}
end
end
end

View File

@ -1,3 +1,7 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RichMedia.Parser do defmodule Pleroma.Web.RichMedia.Parser do
@parsers [ @parsers [
Pleroma.Web.RichMedia.Parsers.OGP, Pleroma.Web.RichMedia.Parsers.OGP,
@ -5,17 +9,32 @@ defmodule Pleroma.Web.RichMedia.Parser do
Pleroma.Web.RichMedia.Parsers.OEmbed Pleroma.Web.RichMedia.Parsers.OEmbed
] ]
def parse(nil), do: {:error, "No URL provided"}
if Mix.env() == :test do if Mix.env() == :test do
def parse(url), do: parse_url(url) def parse(url), do: parse_url(url)
else else
def parse(url), def parse(url) do
do: Cachex.fetch!(:rich_media_cache, url, fn _ -> parse_url(url) end) try do
Cachex.fetch!(:rich_media_cache, url, fn _ ->
{:commit, parse_url(url)}
end)
rescue
e ->
{:error, "Cachex error: #{inspect(e)}"}
end
end
end end
defp parse_url(url) do defp parse_url(url) do
{:ok, %Tesla.Env{body: html}} = Pleroma.HTTP.get(url) try do
{:ok, %Tesla.Env{body: html}} = Pleroma.HTTP.get(url, [], adapter: [pool: :media])
html |> maybe_parse() |> get_parsed_data() html |> maybe_parse() |> get_parsed_data()
rescue
e ->
{:error, "Parsing error: #{inspect(e)}"}
end
end end
defp maybe_parse(html) do defp maybe_parse(html) do
@ -27,11 +46,11 @@ defp maybe_parse(html) do
end) end)
end end
defp get_parsed_data(data) when data == %{} do defp get_parsed_data(%{title: title} = data) when is_binary(title) and byte_size(title) > 0 do
{:error, "No metadata found"} {:ok, data}
end end
defp get_parsed_data(data) do defp get_parsed_data(data) do
{:ok, data} {:error, "Found metadata was invalid or incomplete: #{inspect(data)}"}
end end
end end

View File

@ -20,8 +20,12 @@ defp get_oembed_url(nodes) do
end end
defp get_oembed_data(url) do defp get_oembed_data(url) do
{:ok, %Tesla.Env{body: json}} = Pleroma.HTTP.get(url) {:ok, %Tesla.Env{body: json}} = Pleroma.HTTP.get(url, [], adapter: [pool: :media])
{:ok, Poison.decode!(json)} {:ok, data} = Jason.decode(json)
data = data |> Map.new(fn {k, v} -> {String.to_atom(k), v} end)
{:ok, data}
end end
end end

View File

@ -239,12 +239,6 @@ defmodule Pleroma.Web.Router do
put("/settings", MastodonAPIController, :put_settings) put("/settings", MastodonAPIController, :put_settings)
end end
scope "/api", Pleroma.Web.RichMedia do
pipe_through(:authenticated_api)
get("/rich_media/parse", RichMediaController, :parse)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do scope "/api/v1", Pleroma.Web.MastodonAPI do
pipe_through(:api) pipe_through(:api)
get("/instance", MastodonAPIController, :masto_instance) get("/instance", MastodonAPIController, :masto_instance)
@ -258,7 +252,7 @@ defmodule Pleroma.Web.Router do
get("/statuses/:id", MastodonAPIController, :get_status) get("/statuses/:id", MastodonAPIController, :get_status)
get("/statuses/:id/context", MastodonAPIController, :get_context) get("/statuses/:id/context", MastodonAPIController, :get_context)
get("/statuses/:id/card", MastodonAPIController, :empty_object) get("/statuses/:id/card", MastodonAPIController, :status_card)
get("/statuses/:id/favourited_by", MastodonAPIController, :favourited_by) get("/statuses/:id/favourited_by", MastodonAPIController, :favourited_by)
get("/statuses/:id/reblogged_by", MastodonAPIController, :reblogged_by) get("/statuses/:id/reblogged_by", MastodonAPIController, :reblogged_by)
@ -284,6 +278,7 @@ defmodule Pleroma.Web.Router do
post("/help/test", TwitterAPI.UtilController, :help_test) post("/help/test", TwitterAPI.UtilController, :help_test)
get("/statusnet/config", TwitterAPI.UtilController, :config) get("/statusnet/config", TwitterAPI.UtilController, :config)
get("/statusnet/version", TwitterAPI.UtilController, :version) get("/statusnet/version", TwitterAPI.UtilController, :version)
get("/pleroma/frontend_configurations", TwitterAPI.UtilController, :frontend_configurations)
end end
scope "/api", Pleroma.Web do scope "/api", Pleroma.Web do
@ -523,10 +518,10 @@ defmodule Fallback.RedirectController do
alias Pleroma.Web.Metadata alias Pleroma.Web.Metadata
alias Pleroma.User alias Pleroma.User
def redirector(conn, _params) do def redirector(conn, _params, code \\ 200) do
conn conn
|> put_resp_content_type("text/html") |> put_resp_content_type("text/html")
|> send_file(200, index_file_path()) |> send_file(code, index_file_path())
end end
def redirector_with_meta(conn, %{"maybe_nickname_or_id" => maybe_nickname_or_id} = params) do def redirector_with_meta(conn, %{"maybe_nickname_or_id" => maybe_nickname_or_id} = params) do

View File

@ -183,7 +183,9 @@ def config(conn, _params) do
invitesEnabled: if(Keyword.get(instance, :invites_enabled, false), do: "1", else: "0") invitesEnabled: if(Keyword.get(instance, :invites_enabled, false), do: "1", else: "0")
} }
pleroma_fe = %{ pleroma_fe =
if instance_fe do
%{
theme: Keyword.get(instance_fe, :theme), theme: Keyword.get(instance_fe, :theme),
background: Keyword.get(instance_fe, :background), background: Keyword.get(instance_fe, :background),
logo: Keyword.get(instance_fe, :logo), logo: Keyword.get(instance_fe, :logo),
@ -195,13 +197,17 @@ def config(conn, _params) do
showInstanceSpecificPanel: Keyword.get(instance_fe, :show_instance_panel), showInstanceSpecificPanel: Keyword.get(instance_fe, :show_instance_panel),
scopeOptionsEnabled: Keyword.get(instance_fe, :scope_options_enabled), scopeOptionsEnabled: Keyword.get(instance_fe, :scope_options_enabled),
formattingOptionsEnabled: Keyword.get(instance_fe, :formatting_options_enabled), formattingOptionsEnabled: Keyword.get(instance_fe, :formatting_options_enabled),
collapseMessageWithSubject: Keyword.get(instance_fe, :collapse_message_with_subject), collapseMessageWithSubject:
Keyword.get(instance_fe, :collapse_message_with_subject),
hidePostStats: Keyword.get(instance_fe, :hide_post_stats), hidePostStats: Keyword.get(instance_fe, :hide_post_stats),
hideUserStats: Keyword.get(instance_fe, :hide_user_stats), hideUserStats: Keyword.get(instance_fe, :hide_user_stats),
scopeCopy: Keyword.get(instance_fe, :scope_copy), scopeCopy: Keyword.get(instance_fe, :scope_copy),
subjectLineBehavior: Keyword.get(instance_fe, :subject_line_behavior), subjectLineBehavior: Keyword.get(instance_fe, :subject_line_behavior),
alwaysShowSubjectInput: Keyword.get(instance_fe, :always_show_subject_input) alwaysShowSubjectInput: Keyword.get(instance_fe, :always_show_subject_input)
} }
else
Pleroma.Config.get([:frontend_configurations, :pleroma_fe])
end
managed_config = Keyword.get(instance, :managed_config) managed_config = Keyword.get(instance, :managed_config)
@ -216,6 +222,14 @@ def config(conn, _params) do
end end
end end
def frontend_configurations(conn, _params) do
config =
Pleroma.Config.get(:frontend_configurations, %{})
|> Enum.into(%{})
json(conn, config)
end
def version(conn, _params) do def version(conn, _params) do
version = Pleroma.Application.named_version() version = Pleroma.Application.named_version()

View File

@ -12,6 +12,7 @@ defmodule Pleroma.Web.TwitterAPI.Representers.ActivityRepresenter do
alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Formatter alias Pleroma.Formatter
alias Pleroma.HTML alias Pleroma.HTML
alias Pleroma.Web.MastodonAPI.StatusView
defp user_by_ap_id(user_list, ap_id) do defp user_by_ap_id(user_list, ap_id) do
Enum.find(user_list, fn %{ap_id: user_id} -> ap_id == user_id end) Enum.find(user_list, fn %{ap_id: user_id} -> ap_id == user_id end)
@ -186,6 +187,12 @@ def to_map(
summary = HTML.strip_tags(object["summary"]) summary = HTML.strip_tags(object["summary"])
card =
StatusView.render(
"card.json",
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
)
%{ %{
"id" => activity.id, "id" => activity.id,
"uri" => activity.data["object"]["id"], "uri" => activity.data["object"]["id"],
@ -214,7 +221,8 @@ def to_map(
"possibly_sensitive" => possibly_sensitive, "possibly_sensitive" => possibly_sensitive,
"visibility" => Pleroma.Web.MastodonAPI.StatusView.get_visibility(object), "visibility" => Pleroma.Web.MastodonAPI.StatusView.get_visibility(object),
"summary" => summary, "summary" => summary,
"summary_html" => summary |> Formatter.emojify(object["emoji"]) "summary_html" => summary |> Formatter.emojify(object["emoji"]),
"card" => card
} }
end end

View File

@ -10,6 +10,7 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do
alias Pleroma.Web.TwitterAPI.ActivityView alias Pleroma.Web.TwitterAPI.ActivityView
alias Pleroma.Web.TwitterAPI.TwitterAPI alias Pleroma.Web.TwitterAPI.TwitterAPI
alias Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter alias Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.HTML alias Pleroma.HTML
alias Pleroma.Object alias Pleroma.Object
@ -114,7 +115,7 @@ def render("index.json", opts) do
|> Map.put(:context_ids, context_ids) |> Map.put(:context_ids, context_ids)
|> Map.put(:users, users) |> Map.put(:users, users)
render_many( safe_render_many(
opts.activities, opts.activities,
ActivityView, ActivityView,
"activity.json", "activity.json",
@ -274,6 +275,12 @@ def render(
summary = HTML.strip_tags(summary) summary = HTML.strip_tags(summary)
card =
StatusView.render(
"card.json",
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
)
%{ %{
"id" => activity.id, "id" => activity.id,
"uri" => activity.data["object"]["id"], "uri" => activity.data["object"]["id"],
@ -300,9 +307,10 @@ def render(
"tags" => tags, "tags" => tags,
"activity_type" => "post", "activity_type" => "post",
"possibly_sensitive" => possibly_sensitive, "possibly_sensitive" => possibly_sensitive,
"visibility" => Pleroma.Web.MastodonAPI.StatusView.get_visibility(object), "visibility" => StatusView.get_visibility(object),
"summary" => summary, "summary" => summary,
"summary_html" => summary |> Formatter.emojify(object["emoji"]) "summary_html" => summary |> Formatter.emojify(object["emoji"]),
"card" => card
} }
end end

View File

@ -38,6 +38,33 @@ def view do
import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1] import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1]
import Pleroma.Web.{ErrorHelpers, Gettext, Router.Helpers} import Pleroma.Web.{ErrorHelpers, Gettext, Router.Helpers}
require Logger
@doc "Same as `render/3` but wrapped in a rescue block"
def safe_render(view, template, assigns \\ %{}) do
Phoenix.View.render(view, template, assigns)
rescue
error ->
Logger.error(
"#{__MODULE__} failed to render #{inspect({view, template})}: #{inspect(error)}"
)
Logger.error(inspect(__STACKTRACE__))
nil
end
@doc """
Same as `render_many/4` but wrapped in rescue block.
"""
def safe_render_many(collection, view, template, assigns \\ %{}) do
Enum.map(collection, fn resource ->
as = Map.get(assigns, :as) || view.__resource__
assigns = Map.put(assigns, as, resource)
safe_render(view, template, assigns)
end)
|> Enum.filter(& &1)
end
end end
end end

View File

@ -0,0 +1,9 @@
defmodule Pleroma.Repo.Migrations.ChangePushSubscriptionsVarchar do
use Ecto.Migration
def change do
alter table(:push_subscriptions) do
modify(:endpoint, :varchar)
end
end
end

View File

@ -0,0 +1,8 @@
defmodule Pleroma.Repo.Migrations.AddActivitiesLikesIndex do
use Ecto.Migration
@disable_ddl_transaction true
def change do
create index(:activities, ["((data #> '{\"object\",\"likes\"}'))"], concurrently: true, name: :activities_likes, using: :gin)
end
end

BIN
priv/static/images/city.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 MiB

View File

@ -1 +1 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Pleroma</title><!--server-generated-meta--><link rel=icon type=image/png href=/favicon.png><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/app.3d3e30a9afb8c41739656f496e8c79e6.css rel=stylesheet></head><body style="display: none"><div id=app></div><script type=text/javascript src=/static/js/manifest.8dc8d7a1dc85bfdf2b14.js></script><script type=text/javascript src=/static/js/vendor.61fd03d8471aaadcf63c.js></script><script type=text/javascript src=/static/js/app.ddbd2a89e264d04e0d6d.js></script></body></html> <!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1"><title>Pleroma</title><!--server-generated-meta--><link rel=icon type=image/png href=/favicon.png><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/app.42c43da15d7ab16ad8e42d21fcfc5a43.css rel=stylesheet></head><body style="display: none"><div id=app></div><script type=text/javascript src=/static/js/manifest.6aa5664a1a2c0832ce7b.js></script><script type=text/javascript src=/static/js/vendor.56a115a1d7339d6811a0.js></script><script type=text/javascript src=/static/js/app.59ebcfb47f86a7a5ba3f.js></script></body></html>

View File

@ -19,5 +19,8 @@
"loginMethod": "password", "loginMethod": "password",
"webPushNotifications": false, "webPushNotifications": false,
"noAttachmentLinks": false, "noAttachmentLinks": false,
"nsfwCensorImage": "" "nsfwCensorImage": "",
"useOneClickNsfw": true,
"playVideosInline": false,
"useContainFit": false
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -221,6 +221,18 @@
"css": "plus", "css": "plus",
"code": 59413, "code": 59413,
"src": "fontawesome" "src": "fontawesome"
},
{
"uid": "41087bc74d4b20b55059c60a33bf4008",
"css": "edit",
"code": 59415,
"src": "fontawesome"
},
{
"uid": "5717236f6134afe2d2a278a5c9b3927a",
"css": "play-circled",
"code": 61764,
"src": "fontawesome"
} }
] ]
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -229,11 +229,11 @@ body {
} }
@font-face { @font-face {
font-family: 'fontello'; font-family: 'fontello';
src: url('./font/fontello.eot?32716429'); src: url('./font/fontello.eot?28736547');
src: url('./font/fontello.eot?32716429#iefix') format('embedded-opentype'), src: url('./font/fontello.eot?28736547#iefix') format('embedded-opentype'),
url('./font/fontello.woff?32716429') format('woff'), url('./font/fontello.woff?28736547') format('woff'),
url('./font/fontello.ttf?32716429') format('truetype'), url('./font/fontello.ttf?28736547') format('truetype'),
url('./font/fontello.svg?32716429#fontello') format('svg'); url('./font/fontello.svg?28736547#fontello') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@ -331,23 +331,27 @@ body {
<div class="the-icons span3" title="Code: 0xe814"><i class="demo-icon icon-attention">&#xe814;</i> <span class="i-name">icon-attention</span><span class="i-code">0xe814</span></div> <div class="the-icons span3" title="Code: 0xe814"><i class="demo-icon icon-attention">&#xe814;</i> <span class="i-name">icon-attention</span><span class="i-code">0xe814</span></div>
<div class="the-icons span3" title="Code: 0xe815"><i class="demo-icon icon-plus">&#xe815;</i> <span class="i-name">icon-plus</span><span class="i-code">0xe815</span></div> <div class="the-icons span3" title="Code: 0xe815"><i class="demo-icon icon-plus">&#xe815;</i> <span class="i-name">icon-plus</span><span class="i-code">0xe815</span></div>
<div class="the-icons span3" title="Code: 0xe816"><i class="demo-icon icon-adjust">&#xe816;</i> <span class="i-name">icon-adjust</span><span class="i-code">0xe816</span></div> <div class="the-icons span3" title="Code: 0xe816"><i class="demo-icon icon-adjust">&#xe816;</i> <span class="i-name">icon-adjust</span><span class="i-code">0xe816</span></div>
<div class="the-icons span3" title="Code: 0xe832"><i class="demo-icon icon-spin3 animate-spin">&#xe832;</i> <span class="i-name">icon-spin3</span><span class="i-code">0xe832</span></div> <div class="the-icons span3" title="Code: 0xe817"><i class="demo-icon icon-edit">&#xe817;</i> <span class="i-name">icon-edit</span><span class="i-code">0xe817</span></div>
</div> </div>
<div class="row"> <div class="row">
<div class="the-icons span3" title="Code: 0xe832"><i class="demo-icon icon-spin3 animate-spin">&#xe832;</i> <span class="i-name">icon-spin3</span><span class="i-code">0xe832</span></div>
<div class="the-icons span3" title="Code: 0xe834"><i class="demo-icon icon-spin4 animate-spin">&#xe834;</i> <span class="i-name">icon-spin4</span><span class="i-code">0xe834</span></div> <div class="the-icons span3" title="Code: 0xe834"><i class="demo-icon icon-spin4 animate-spin">&#xe834;</i> <span class="i-name">icon-spin4</span><span class="i-code">0xe834</span></div>
<div class="the-icons span3" title="Code: 0xf08e"><i class="demo-icon icon-link-ext">&#xf08e;</i> <span class="i-name">icon-link-ext</span><span class="i-code">0xf08e</span></div> <div class="the-icons span3" title="Code: 0xf08e"><i class="demo-icon icon-link-ext">&#xf08e;</i> <span class="i-name">icon-link-ext</span><span class="i-code">0xf08e</span></div>
<div class="the-icons span3" title="Code: 0xf08f"><i class="demo-icon icon-link-ext-alt">&#xf08f;</i> <span class="i-name">icon-link-ext-alt</span><span class="i-code">0xf08f</span></div> <div class="the-icons span3" title="Code: 0xf08f"><i class="demo-icon icon-link-ext-alt">&#xf08f;</i> <span class="i-name">icon-link-ext-alt</span><span class="i-code">0xf08f</span></div>
<div class="the-icons span3" title="Code: 0xf0c9"><i class="demo-icon icon-menu">&#xf0c9;</i> <span class="i-name">icon-menu</span><span class="i-code">0xf0c9</span></div>
</div> </div>
<div class="row"> <div class="row">
<div class="the-icons span3" title="Code: 0xf0c9"><i class="demo-icon icon-menu">&#xf0c9;</i> <span class="i-name">icon-menu</span><span class="i-code">0xf0c9</span></div>
<div class="the-icons span3" title="Code: 0xf0e0"><i class="demo-icon icon-mail-alt">&#xf0e0;</i> <span class="i-name">icon-mail-alt</span><span class="i-code">0xf0e0</span></div> <div class="the-icons span3" title="Code: 0xf0e0"><i class="demo-icon icon-mail-alt">&#xf0e0;</i> <span class="i-name">icon-mail-alt</span><span class="i-code">0xf0e0</span></div>
<div class="the-icons span3" title="Code: 0xf0e5"><i class="demo-icon icon-comment-empty">&#xf0e5;</i> <span class="i-name">icon-comment-empty</span><span class="i-code">0xf0e5</span></div> <div class="the-icons span3" title="Code: 0xf0e5"><i class="demo-icon icon-comment-empty">&#xf0e5;</i> <span class="i-name">icon-comment-empty</span><span class="i-code">0xf0e5</span></div>
<div class="the-icons span3" title="Code: 0xf0fe"><i class="demo-icon icon-plus-squared">&#xf0fe;</i> <span class="i-name">icon-plus-squared</span><span class="i-code">0xf0fe</span></div> <div class="the-icons span3" title="Code: 0xf0fe"><i class="demo-icon icon-plus-squared">&#xf0fe;</i> <span class="i-name">icon-plus-squared</span><span class="i-code">0xf0fe</span></div>
<div class="the-icons span3" title="Code: 0xf112"><i class="demo-icon icon-reply">&#xf112;</i> <span class="i-name">icon-reply</span><span class="i-code">0xf112</span></div>
</div> </div>
<div class="row"> <div class="row">
<div class="the-icons span3" title="Code: 0xf112"><i class="demo-icon icon-reply">&#xf112;</i> <span class="i-name">icon-reply</span><span class="i-code">0xf112</span></div>
<div class="the-icons span3" title="Code: 0xf13e"><i class="demo-icon icon-lock-open-alt">&#xf13e;</i> <span class="i-name">icon-lock-open-alt</span><span class="i-code">0xf13e</span></div> <div class="the-icons span3" title="Code: 0xf13e"><i class="demo-icon icon-lock-open-alt">&#xf13e;</i> <span class="i-name">icon-lock-open-alt</span><span class="i-code">0xf13e</span></div>
<div class="the-icons span3" title="Code: 0xf144"><i class="demo-icon icon-play-circled">&#xf144;</i> <span class="i-name">icon-play-circled</span><span class="i-code">0xf144</span></div>
<div class="the-icons span3" title="Code: 0xf164"><i class="demo-icon icon-thumbs-up-alt">&#xf164;</i> <span class="i-name">icon-thumbs-up-alt</span><span class="i-code">0xf164</span></div> <div class="the-icons span3" title="Code: 0xf164"><i class="demo-icon icon-thumbs-up-alt">&#xf164;</i> <span class="i-name">icon-thumbs-up-alt</span><span class="i-code">0xf164</span></div>
</div>
<div class="row">
<div class="the-icons span3" title="Code: 0xf1e5"><i class="demo-icon icon-binoculars">&#xf1e5;</i> <span class="i-name">icon-binoculars</span><span class="i-code">0xf1e5</span></div> <div class="the-icons span3" title="Code: 0xf1e5"><i class="demo-icon icon-binoculars">&#xf1e5;</i> <span class="i-name">icon-binoculars</span><span class="i-code">0xf1e5</span></div>
<div class="the-icons span3" title="Code: 0xf234"><i class="demo-icon icon-user-plus">&#xf234;</i> <span class="i-name">icon-user-plus</span><span class="i-code">0xf234</span></div> <div class="the-icons span3" title="Code: 0xf234"><i class="demo-icon icon-user-plus">&#xf234;</i> <span class="i-name">icon-user-plus</span><span class="i-code">0xf234</span></div>
</div> </div>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" standalone="no"?> <?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg"> <svg xmlns="http://www.w3.org/2000/svg">
<metadata>Copyright (C) 2018 by original authors @ fontello.com</metadata> <metadata>Copyright (C) 2019 by original authors @ fontello.com</metadata>
<defs> <defs>
<font id="fontello" horiz-adv-x="1000" > <font id="fontello" horiz-adv-x="1000" >
<font-face font-family="fontello" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="857" descent="-143" /> <font-face font-family="fontello" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="857" descent="-143" />
@ -52,6 +52,8 @@
<glyph glyph-name="adjust" unicode="&#xe816;" d="M429 53v608q-83 0-153-41t-110-111-41-152 41-152 110-111 153-41z m428 304q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" /> <glyph glyph-name="adjust" unicode="&#xe816;" d="M429 53v608q-83 0-153-41t-110-111-41-152 41-152 110-111 153-41z m428 304q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" />
<glyph glyph-name="edit" unicode="&#xe817;" d="M496 196l64 65-85 85-64-65v-31h53v-54h32z m245 402q-9 9-18 0l-196-196q-9-9 0-18t18 0l196 196q9 9 0 18z m45-331v-106q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h464q35 0 65-14 9-4 10-13 2-10-5-16l-27-28q-8-8-18-4-13 3-25 3h-464q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v70q0 7 5 12l36 36q8 8 20 4t11-16z m-54 411l161-160-375-375h-161v160z m248-73l-51-52-161 161 51 52q16 15 38 15t38-15l85-85q16-16 16-38t-16-38z" horiz-adv-x="1000" />
<glyph glyph-name="spin3" unicode="&#xe832;" d="M494 857c-266 0-483-210-494-472-1-19 13-20 13-20l84 0c16 0 19 10 19 18 10 199 176 358 378 358 107 0 205-45 273-118l-58-57c-11-12-11-27 5-31l247-50c21-5 46 11 37 44l-58 227c-2 9-16 22-29 13l-65-60c-89 91-214 148-352 148z m409-508c-16 0-19-10-19-18-10-199-176-358-377-358-108 0-205 45-274 118l59 57c10 12 10 27-5 31l-248 50c-21 5-46-11-37-44l58-227c2-9 16-22 30-13l64 60c89-91 214-148 353-148 265 0 482 210 493 473 1 18-13 19-13 19l-84 0z" horiz-adv-x="1000" /> <glyph glyph-name="spin3" unicode="&#xe832;" d="M494 857c-266 0-483-210-494-472-1-19 13-20 13-20l84 0c16 0 19 10 19 18 10 199 176 358 378 358 107 0 205-45 273-118l-58-57c-11-12-11-27 5-31l247-50c21-5 46 11 37 44l-58 227c-2 9-16 22-29 13l-65-60c-89 91-214 148-352 148z m409-508c-16 0-19-10-19-18-10-199-176-358-377-358-108 0-205 45-274 118l59 57c10 12 10 27-5 31l-248 50c-21 5-46-11-37-44l58-227c2-9 16-22 30-13l64 60c89-91 214-148 353-148 265 0 482 210 493 473 1 18-13 19-13 19l-84 0z" horiz-adv-x="1000" />
<glyph glyph-name="spin4" unicode="&#xe834;" d="M498 857c-114 0-228-39-320-116l0 0c173 140 428 130 588-31 134-134 164-332 89-495-10-29-5-50 12-68 21-20 61-23 84 0 3 3 12 15 15 24 71 180 33 393-112 539-99 98-228 147-356 147z m-409-274c-14 0-29-5-39-16-3-3-13-15-15-24-71-180-34-393 112-539 185-185 479-195 676-31l0 0c-173-140-428-130-589 31-134 134-163 333-89 495 11 29 6 50-12 68-11 11-27 17-44 16z" horiz-adv-x="1001" /> <glyph glyph-name="spin4" unicode="&#xe834;" d="M498 857c-114 0-228-39-320-116l0 0c173 140 428 130 588-31 134-134 164-332 89-495-10-29-5-50 12-68 21-20 61-23 84 0 3 3 12 15 15 24 71 180 33 393-112 539-99 98-228 147-356 147z m-409-274c-14 0-29-5-39-16-3-3-13-15-15-24-71-180-34-393 112-539 185-185 479-195 676-31l0 0c-173-140-428-130-589 31-134 134-163 333-89 495 11 29 6 50-12 68-11 11-27 17-44 16z" horiz-adv-x="1001" />
@ -72,6 +74,8 @@
<glyph glyph-name="lock-open-alt" unicode="&#xf13e;" d="M589 428q23 0 38-15t16-38v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h17v179q0 103 74 177t176 73 177-73 73-177q0-14-10-25t-25-11h-36q-14 0-25 11t-11 25q0 59-42 101t-101 42-101-42-41-101v-179h410z" horiz-adv-x="642.9" /> <glyph glyph-name="lock-open-alt" unicode="&#xf13e;" d="M589 428q23 0 38-15t16-38v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h17v179q0 103 74 177t176 73 177-73 73-177q0-14-10-25t-25-11h-36q-14 0-25 11t-11 25q0 59-42 101t-101 42-101-42-41-101v-179h410z" horiz-adv-x="642.9" />
<glyph glyph-name="play-circled" unicode="&#xf144;" d="M429 786q116 0 215-58t156-156 57-215-57-215-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58z m214-460q18 10 18 31t-18 31l-304 178q-17 11-35 1-18-11-18-31v-358q0-20 18-31 9-4 17-4 10 0 18 5z" horiz-adv-x="857.1" />
<glyph glyph-name="thumbs-up-alt" unicode="&#xf164;" d="M143 107q0 15-11 25t-25 11q-15 0-25-11t-11-25q0-15 11-25t25-11q15 0 25 11t11 25z m89 286v-357q0-15-10-25t-26-11h-160q-15 0-25 11t-11 25v357q0 14 11 25t25 10h160q15 0 26-10t10-25z m661 0q0-48-31-83 9-25 9-43 1-42-24-76 9-31 0-66-9-31-31-52 5-62-27-101-36-43-110-44h-72q-37 0-80 9t-68 16-67 22q-69 24-88 25-15 0-25 11t-11 25v357q0 14 10 25t24 11q13 1 42 33t57 67q38 49 56 67 10 10 17 27t10 27 8 34q4 22 7 34t11 29 19 28q10 11 25 11 25 0 46-6t33-15 22-22 14-25 7-28 2-25 1-22q0-21-6-43t-10-33-16-31q-1-4-5-10t-6-13-5-13h155q43 0 75-32t32-75z" horiz-adv-x="928.6" /> <glyph glyph-name="thumbs-up-alt" unicode="&#xf164;" d="M143 107q0 15-11 25t-25 11q-15 0-25-11t-11-25q0-15 11-25t25-11q15 0 25 11t11 25z m89 286v-357q0-15-10-25t-26-11h-160q-15 0-25 11t-11 25v357q0 14 11 25t25 10h160q15 0 26-10t10-25z m661 0q0-48-31-83 9-25 9-43 1-42-24-76 9-31 0-66-9-31-31-52 5-62-27-101-36-43-110-44h-72q-37 0-80 9t-68 16-67 22q-69 24-88 25-15 0-25 11t-11 25v357q0 14 10 25t24 11q13 1 42 33t57 67q38 49 56 67 10 10 17 27t10 27 8 34q4 22 7 34t11 29 19 28q10 11 25 11 25 0 46-6t33-15 22-22 14-25 7-28 2-25 1-22q0-21-6-43t-10-33-16-31q-1-4-5-10t-6-13-5-13h155q43 0 75-32t32-75z" horiz-adv-x="928.6" />
<glyph glyph-name="binoculars" unicode="&#xf1e5;" d="M393 678v-428q0-15-11-25t-25-11v-321q0-15-10-25t-26-11h-285q-15 0-25 11t-11 25v285l139 488q4 12 17 12h237z m178 0v-392h-142v392h142z m429-500v-285q0-15-11-25t-25-11h-285q-15 0-25 11t-11 25v321q-15 0-25 11t-11 25v428h237q13 0 17-12z m-589 661v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z m375 0v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z" horiz-adv-x="1000" /> <glyph glyph-name="binoculars" unicode="&#xf1e5;" d="M393 678v-428q0-15-11-25t-25-11v-321q0-15-10-25t-26-11h-285q-15 0-25 11t-11 25v285l139 488q4 12 17 12h237z m178 0v-392h-142v392h142z m429-500v-285q0-15-11-25t-25-11h-285q-15 0-25 11t-11 25v321q-15 0-25 11t-11 25v428h237q13 0 17-12z m-589 661v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z m375 0v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z" horiz-adv-x="1000" />

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
priv/static/sw-pleroma.js Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -11,6 +11,7 @@ defmodule Pleroma.FlakeIdTest do
test "from_string/1" do test "from_string/1" do
fake_flake = <<0::integer-size(64), 42::integer-size(64)>> fake_flake = <<0::integer-size(64), 42::integer-size(64)>>
assert from_string("42") == fake_flake assert from_string("42") == fake_flake
assert from_string(42) == fake_flake
end end
test "zero or -1 is a null flake" do test "zero or -1 is a null flake" do

View File

@ -653,6 +653,14 @@ def get("https://social.heldscal.la/user/23211", _, _, Accept: "application/acti
{:ok, Tesla.Mock.json(%{"id" => "https://social.heldscal.la/user/23211"}, status: 200)} {:ok, Tesla.Mock.json(%{"id" => "https://social.heldscal.la/user/23211"}, status: 200)}
end end
def get("http://example.com/ogp", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/ogp.html")}}
end
def get("http://example.com/empty", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: "hello"}}
end
def get(url, query, body, headers) do def get(url, query, body, headers) do
{:error, {:error,
"Not implemented the mock response for get #{inspect(url)}, #{query}, #{inspect(body)}, #{ "Not implemented the mock response for get #{inspect(url)}, #{query}, #{inspect(body)}, #{

View File

@ -50,13 +50,19 @@ test "ap_followers returns the followers collection for the user" do
test "follow_all follows mutliple users" do test "follow_all follows mutliple users" do
user = insert(:user) user = insert(:user)
followed_zero = insert(:user)
followed_one = insert(:user) followed_one = insert(:user)
followed_two = insert(:user) followed_two = insert(:user)
not_followed = insert(:user)
{:ok, user} = User.follow(user, followed_zero)
{:ok, user} = User.follow_all(user, [followed_one, followed_two]) {:ok, user} = User.follow_all(user, [followed_one, followed_two])
assert User.following?(user, followed_one) assert User.following?(user, followed_one)
assert User.following?(user, followed_two) assert User.following?(user, followed_two)
assert User.following?(user, followed_zero)
refute User.following?(user, not_followed)
end end
test "follow takes a user and another user" do test "follow takes a user and another user" do

View File

@ -64,6 +64,34 @@ test "it returns a user" do
assert user.info.ap_enabled assert user.info.ap_enabled
assert user.follower_address == "http://mastodon.example.org/users/admin/followers" assert user.follower_address == "http://mastodon.example.org/users/admin/followers"
end end
test "it fetches the appropriate tag-restricted posts" do
user = insert(:user)
{:ok, status_one} = CommonAPI.post(user, %{"status" => ". #test"})
{:ok, status_two} = CommonAPI.post(user, %{"status" => ". #essais"})
{:ok, status_three} = CommonAPI.post(user, %{"status" => ". #test #reject"})
fetch_one = ActivityPub.fetch_activities([], %{"tag" => "test"})
fetch_two = ActivityPub.fetch_activities([], %{"tag" => ["test", "essais"]})
fetch_three =
ActivityPub.fetch_activities([], %{
"tag" => ["test", "essais"],
"tag_reject" => ["reject"]
})
fetch_four =
ActivityPub.fetch_activities([], %{
"tag" => ["test"],
"tag_all" => ["test", "reject"]
})
assert fetch_one == [status_one, status_three]
assert fetch_two == [status_one, status_two, status_three]
assert fetch_three == [status_one, status_two]
assert fetch_four == [status_three]
end
end end
describe "insertion" do describe "insertion" do
@ -85,6 +113,17 @@ test "drops activities beyond a certain limit" do
assert {:error, {:remote_limit_error, _}} = ActivityPub.insert(data) assert {:error, {:remote_limit_error, _}} = ActivityPub.insert(data)
end end
test "doesn't drop activities with content being null" do
data = %{
"ok" => true,
"object" => %{
"content" => nil
}
}
assert {:ok, _} = ActivityPub.insert(data)
end
test "returns the activity if one with the same id is already in" do test "returns the activity if one with the same id is already in" do
activity = insert(:note_activity) activity = insert(:note_activity)
{:ok, new_activity} = ActivityPub.insert(activity.data) {:ok, new_activity} = ActivityPub.insert(activity.data)
@ -584,8 +623,6 @@ test "it filters broken threads" do
"in_reply_to_status_id" => private_activity_2.id "in_reply_to_status_id" => private_activity_2.id
}) })
assert user1.following == [user3.ap_id <> "/followers", user1.ap_id]
activities = ActivityPub.fetch_activities([user1.ap_id | user1.following]) activities = ActivityPub.fetch_activities([user1.ap_id | user1.following])
assert [public_activity, private_activity_1, private_activity_3] == activities assert [public_activity, private_activity_1, private_activity_3] == activities

View File

@ -61,7 +61,9 @@ test "Represent a user account" do
}, },
pleroma: %{ pleroma: %{
confirmation_pending: false, confirmation_pending: false,
tags: [] tags: [],
is_admin: false,
is_moderator: false
} }
} }
@ -102,7 +104,9 @@ test "Represent a Service(bot) account" do
}, },
pleroma: %{ pleroma: %{
confirmation_pending: false, confirmation_pending: false,
tags: [] tags: [],
is_admin: false,
is_moderator: false
} }
} }

View File

@ -136,6 +136,20 @@ test "posting a sensitive status", %{conn: conn} do
assert Repo.get(Activity, id) assert Repo.get(Activity, id)
end end
test "posting a status with OGP link preview", %{conn: conn} do
user = insert(:user)
conn =
conn
|> assign(:user, user)
|> post("/api/v1/statuses", %{
"status" => "http://example.com/ogp"
})
assert %{"id" => id, "card" => %{"title" => "The Rock"}} = json_response(conn, 200)
assert Repo.get(Activity, id)
end
test "posting a direct status", %{conn: conn} do test "posting a direct status", %{conn: conn} do
user1 = insert(:user) user1 = insert(:user)
user2 = insert(:user) user2 = insert(:user)
@ -1044,6 +1058,34 @@ test "hashtag timeline", %{conn: conn} do
end) end)
end end
test "multi-hashtag timeline", %{conn: conn} do
user = insert(:user)
{:ok, activity_test} = CommonAPI.post(user, %{"status" => "#test"})
{:ok, activity_test1} = CommonAPI.post(user, %{"status" => "#test #test1"})
{:ok, activity_none} = CommonAPI.post(user, %{"status" => "#test #none"})
any_test =
conn
|> get("/api/v1/timelines/tag/test", %{"any" => ["test1"]})
[status_none, status_test1, status_test] = json_response(any_test, 200)
assert to_string(activity_test.id) == status_test["id"]
assert to_string(activity_test1.id) == status_test1["id"]
assert to_string(activity_none.id) == status_none["id"]
restricted_test =
conn
|> get("/api/v1/timelines/tag/test", %{"all" => ["test1"], "none" => ["none"]})
assert [status_test1] == json_response(restricted_test, 200)
all_test = conn |> get("/api/v1/timelines/tag/test", %{"all" => ["none"]})
assert [status_none] == json_response(all_test, 200)
end
test "getting followers", %{conn: conn} do test "getting followers", %{conn: conn} do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
@ -1623,5 +1665,32 @@ test "max pinned statuses", %{conn: conn, user: user, activity: activity_one} do
|> post("/api/v1/statuses/#{activity_two.id}/pin") |> post("/api/v1/statuses/#{activity_two.id}/pin")
|> json_response(400) |> json_response(400)
end end
test "Status rich-media Card", %{conn: conn, user: user} do
{:ok, activity} = CommonAPI.post(user, %{"status" => "http://example.com/ogp"})
response =
conn
|> get("/api/v1/statuses/#{activity.id}/card")
|> json_response(200)
assert response == %{
"image" => "http://ia.media-imdb.com/images/rock.jpg",
"provider_name" => "www.imdb.com",
"provider_url" => "http://www.imdb.com",
"title" => "The Rock",
"type" => "link",
"url" => "http://www.imdb.com/title/tt0117500/",
"description" => nil,
"pleroma" => %{
"opengraph" => %{
"image" => "http://ia.media-imdb.com/images/rock.jpg",
"title" => "The Rock",
"type" => "video.movie",
"url" => "http://www.imdb.com/title/tt0117500/"
}
}
}
end
end end
end end

View File

@ -84,6 +84,7 @@ test "a note activity" do
account: AccountView.render("account.json", %{user: user}), account: AccountView.render("account.json", %{user: user}),
in_reply_to_id: nil, in_reply_to_id: nil,
in_reply_to_account_id: nil, in_reply_to_account_id: nil,
card: nil,
reblog: nil, reblog: nil,
content: HtmlSanitizeEx.basic_html(note.data["object"]["content"]), content: HtmlSanitizeEx.basic_html(note.data["object"]["content"]),
created_at: created_at, created_at: created_at,

View File

@ -34,6 +34,31 @@ test "redirects with oauth authorization" do
assert Repo.get_by(Authorization, token: code) assert Repo.get_by(Authorization, token: code)
end end
test "correctly handles wrong credentials", %{conn: conn} do
user = insert(:user)
app = insert(:oauth_app)
result =
conn
|> post("/oauth/authorize", %{
"authorization" => %{
"name" => user.nickname,
"password" => "wrong",
"client_id" => app.client_id,
"redirect_uri" => app.redirect_uris,
"state" => "statepassed"
}
})
|> html_response(:unauthorized)
# Keep the details
assert result =~ app.client_id
assert result =~ app.redirect_uris
# Error message
assert result =~ "Invalid"
end
test "issues a token for an all-body request" do test "issues a token for an all-body request" do
user = insert(:user) user = insert(:user)
app = insert(:oauth_app) app = insert(:oauth_app)

View File

@ -1,54 +0,0 @@
defmodule Pleroma.Web.RichMedia.RichMediaControllerTest do
use Pleroma.Web.ConnCase
import Pleroma.Factory
setup do
Tesla.Mock.mock(fn
%{
method: :get,
url: "http://example.com/ogp"
} ->
%Tesla.Env{status: 200, body: File.read!("test/fixtures/rich_media/ogp.html")}
%{method: :get, url: "http://example.com/empty"} ->
%Tesla.Env{status: 200, body: "hello"}
end)
:ok
end
describe "GET /api/rich_media/parse" do
setup do
user = insert(:user)
[user: user]
end
test "returns 404 if not metadata found", %{user: user} do
build_conn()
|> with_credentials(user.nickname, "test")
|> get("/api/rich_media/parse", %{"url" => "http://example.com/empty"})
|> json_response(404)
end
test "returns OGP metadata", %{user: user} do
response =
build_conn()
|> with_credentials(user.nickname, "test")
|> get("/api/rich_media/parse", %{"url" => "http://example.com/ogp"})
|> json_response(200)
assert response == %{
"image" => "http://ia.media-imdb.com/images/rock.jpg",
"title" => "The Rock",
"type" => "video.movie",
"url" => "http://www.imdb.com/title/tt0117500/"
}
end
end
defp with_credentials(conn, username, password) do
header_content = "Basic " <> Base.encode64("#{username}:#{password}")
put_req_header(conn, "authorization", header_content)
end
end

View File

@ -65,28 +65,27 @@ test "parses OEmbed" do
assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/oembed") == assert Pleroma.Web.RichMedia.Parser.parse("http://example.com/oembed") ==
{:ok, {:ok,
%{ %{
"author_name" => "bees", author_name: "bees",
"author_url" => "https://www.flickr.com/photos/bees/", author_url: "https://www.flickr.com/photos/bees/",
"cache_age" => 3600, cache_age: 3600,
"flickr_type" => "photo", flickr_type: "photo",
"height" => "768", height: "768",
"html" => html:
"<a data-flickr-embed=\"true\" href=\"https://www.flickr.com/photos/bees/2362225867/\" title=\"Bacon Lollys by bees, on Flickr\"><img src=\"https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_b.jpg\" width=\"1024\" height=\"768\" alt=\"Bacon Lollys\"></a><script async src=\"https://embedr.flickr.com/assets/client-code.js\" charset=\"utf-8\"></script>", "<a data-flickr-embed=\"true\" href=\"https://www.flickr.com/photos/bees/2362225867/\" title=\"Bacon Lollys by bees, on Flickr\"><img src=\"https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_b.jpg\" width=\"1024\" height=\"768\" alt=\"Bacon Lollys\"></a><script async src=\"https://embedr.flickr.com/assets/client-code.js\" charset=\"utf-8\"></script>",
"license" => "All Rights Reserved", license: "All Rights Reserved",
"license_id" => 0, license_id: 0,
"provider_name" => "Flickr", provider_name: "Flickr",
"provider_url" => "https://www.flickr.com/", provider_url: "https://www.flickr.com/",
"thumbnail_height" => 150, thumbnail_height: 150,
"thumbnail_url" => thumbnail_url: "https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_q.jpg",
"https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_q.jpg", thumbnail_width: 150,
"thumbnail_width" => 150, title: "Bacon Lollys",
"title" => "Bacon Lollys", type: "photo",
"type" => "photo", url: "https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_b.jpg",
"url" => "https://farm4.staticflickr.com/3040/2362225867_4a87ab8baf_b.jpg", version: "1.0",
"version" => "1.0", web_page: "https://www.flickr.com/photos/bees/2362225867/",
"web_page" => "https://www.flickr.com/photos/bees/2362225867/", web_page_short_url: "https://flic.kr/p/4AK2sc",
"web_page_short_url" => "https://flic.kr/p/4AK2sc", width: "1024"
"width" => "1024"
}} }}
end end
end end

View File

@ -164,6 +164,7 @@ test "an activity" do
"possibly_sensitive" => true, "possibly_sensitive" => true,
"uri" => activity.data["object"]["id"], "uri" => activity.data["object"]["id"],
"visibility" => "direct", "visibility" => "direct",
"card" => nil,
"summary" => "2hu :2hu:", "summary" => "2hu :2hu:",
"summary_html" => "summary_html" =>
"2hu <img height=\"32px\" width=\"32px\" alt=\"2hu\" title=\"2hu\" src=\"corndog.png\" />" "2hu <img height=\"32px\" width=\"32px\" alt=\"2hu\" title=\"2hu\" src=\"corndog.png\" />"

View File

@ -32,4 +32,71 @@ test "it returns HTTP 200", %{conn: conn} do
assert response == "job started" assert response == "job started"
end end
end end
describe "GET /api/statusnet/config.json" do
test "it returns the managed config", %{conn: conn} do
Pleroma.Config.put([:instance, :managed_config], false)
response =
conn
|> get("/api/statusnet/config.json")
|> json_response(:ok)
refute response["site"]["pleromafe"]
Pleroma.Config.put([:instance, :managed_config], true)
response =
conn
|> get("/api/statusnet/config.json")
|> json_response(:ok)
assert response["site"]["pleromafe"]
end
test "if :pleroma, :fe is false, it returns the new style config settings", %{conn: conn} do
Pleroma.Config.put([:instance, :managed_config], true)
Pleroma.Config.put([:fe, :theme], "rei-ayanami-towel")
Pleroma.Config.put([:frontend_configurations, :pleroma_fe], %{theme: "asuka-hospital"})
response =
conn
|> get("/api/statusnet/config.json")
|> json_response(:ok)
assert response["site"]["pleromafe"]["theme"] == "rei-ayanami-towel"
Pleroma.Config.put([:fe], false)
response =
conn
|> get("/api/statusnet/config.json")
|> json_response(:ok)
assert response["site"]["pleromafe"]["theme"] == "asuka-hospital"
end
end
describe "GET /api/pleroma/frontend_configurations" do
test "returns everything in :pleroma, :frontend_configurations", %{conn: conn} do
config = [
frontend_a: %{
x: 1,
y: 2
},
frontend_b: %{
z: 3
}
]
Pleroma.Config.put(:frontend_configurations, config)
response =
conn
|> get("/api/pleroma/frontend_configurations")
|> json_response(:ok)
assert response == Jason.encode!(config |> Enum.into(%{})) |> Jason.decode!()
end
end
end end

View File

@ -148,7 +148,8 @@ test "a create activity with a note" do
"text" => "Hey @shp!", "text" => "Hey @shp!",
"uri" => activity.data["object"]["id"], "uri" => activity.data["object"]["id"],
"user" => UserView.render("show.json", %{user: user}), "user" => UserView.render("show.json", %{user: user}),
"visibility" => "direct" "visibility" => "direct",
"card" => nil
} }
assert result == expected assert result == expected