Merge branch 'develop' into feature/moderation-log-filters

This commit is contained in:
Maxim Filippov 2019-09-10 01:23:20 +03:00
commit d1abf7a358
71 changed files with 816 additions and 5247 deletions

View file

@ -8,6 +8,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- OStatus: eliminate the possibility of a protocol downgrade attack.
- OStatus: prevent following locked accounts, bypassing the approval process.
### Removed
- **Breaking:** GNU Social API with Qvitter extensions support
- **Breaking:** ActivityPub: The `accept_blocks` configuration setting.
- Emoji: Remove longfox emojis.
- Remove `Reply-To` header from report emails for admins.
### Changed
- **Breaking:** Configuration: A setting to explicitly disable the mailer was added, defaulting to true, if you are using a mailer add `config :pleroma, Pleroma.Emails.Mailer, enabled: true` to your config
- **Breaking:** Configuration: `/media/` is now removed when `base_url` is configured, append `/media/` to your `base_url` config to keep the old behaviour if desired
@ -32,7 +38,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- `federation_incoming_replies_max_depth` option being ignored in certain cases
- Federation/MediaProxy not working with instances that have wrong certificate order
- Mastodon API: Handling of search timeouts (`/api/v1/search` and `/api/v2/search`)
- Mastodon API: Misskey's endless polls being unable to render
- Mastodon API: Embedded relationships not being properly rendered in the Account entity of Status entity
- Mastodon API: Notifications endpoint crashing if one notification failed to render
- Mastodon API: follower/following counters not being nullified, when `hide_follows`/`hide_followers` is set
- Mastodon API: `muted` in the Status entity, using author's account to determine if the tread was muted
- Mastodon API: Add `account_id`, `type`, `offset`, and `limit` to search API (`/api/v1/search` and `/api/v2/search`)
@ -52,6 +60,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Reverse Proxy limiting `max_body_length` was incorrectly defined and only checked `Content-Length` headers which may not be sufficient in some circumstances
- MRF: fix use of unserializable keyword lists in describe() implementations
- ActivityPub: Deactivated user deletion
- ActivityPub: Fix `/users/:nickname/inbox` crashing without an authenticated user
- MRF: fix ability to follow a relay when AntiFollowbotPolicy was enabled
### Added
@ -100,6 +109,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mix Tasks: `mix pleroma.database fix_likes_collections`
- Federation: Remove `likes` from objects.
- Admin API: Added moderation log
- Web response cache (currently, enabled for ActivityPub)
- Admin API: Added moderation log filters (user/start date/end date/search/pagination)
### Changed
@ -108,10 +118,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- RichMedia: parsers and their order are configured in `rich_media` config.
- RichMedia: add the rich media ttl based on image expiration time.
### Removed
- Emoji: Remove longfox emojis.
- Remove `Reply-To` header from report emails for admins.
- ActivityPub: The `accept_blocks` configuration setting.
## [1.0.1] - 2019-07-14
### Security

View file

@ -1,4 +1,4 @@
FROM rinpatch/elixir:1.9.0-rc.0-alpine as build
FROM elixir:1.9-alpine as build
COPY . .
@ -12,7 +12,7 @@ RUN apk add git gcc g++ musl-dev make &&\
mkdir release &&\
mix release --path release
FROM alpine:latest
FROM alpine:3.9
ARG HOME=/opt/pleroma
ARG DATA=/var/lib/pleroma

View file

@ -8,7 +8,7 @@ Pleroma is a microblogging server software that can federate (= exchange message
Pleroma is written in Elixir, high-performance and can run on small devices like a Raspberry Pi.
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://docs.joinmastodon.org/api/guidelines/).
For clients it supports the [Mastodon client API](https://docs.joinmastodon.org/api/guidelines/) with Pleroma extensions (see "Pleroma's APIs and Mastodon API extensions" section on <https://docs-develop.pleroma.social>).
- [Client Applications for Pleroma](https://docs-develop.pleroma.social/clients.html)

View file

@ -560,6 +560,10 @@
config :pleroma, Pleroma.ActivityExpiration, enabled: true
config :pleroma, :web_cache_ttl,
activity_pub: nil,
activity_pub_question: 30_000
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"

View file

@ -10,7 +10,7 @@
notify_email: System.get_env("NOTIFY_EMAIL"),
limit: 5000,
registrations_open: false,
dynamic_configuration: true
healthcheck: true
config :pleroma, Pleroma.Repo,
adapter: Ecto.Adapters.Postgres,

View file

@ -690,3 +690,12 @@ Supported rate limiters:
* `:relation_id_action` for actions on relation with a specific user (follow, unfollow)
* `:statuses_actions` for create / delete / fav / unfav / reblog / unreblog actions on any statuses
* `:status_id_action` for fav / unfav or reblog / unreblog actions on the same status by the same user
## :web_cache_ttl
The expiration time for the web responses cache. Values should be in milliseconds or `nil` to disable expiration.
Available caches:
* `:activity_pub` - activity pub routes (except question activities). Defaults to `nil` (no expiration).
* `:activity_pub_question` - activity pub routes (question activities). Defaults to `30_000` (30 seconds).

View file

@ -308,10 +308,19 @@ def delete_by_ap_id(id) when is_binary(id) do
%{data: %{"type" => "Create", "object" => %{"id" => ap_id}}} -> ap_id == id
_ -> nil
end)
|> purge_web_resp_cache()
end
def delete_by_ap_id(_), do: nil
defp purge_web_resp_cache(%Activity{} = activity) do
%{path: path} = URI.parse(activity.data["id"])
Cachex.del(:web_resp_cache, path)
activity
end
defp purge_web_resp_cache(nil), do: nil
for {ap_type, type} <- @mastodon_notification_types do
def mastodon_notification_type(%Activity{data: %{"type" => unquote(ap_type)}}),
do: unquote(type)
@ -362,12 +371,12 @@ def query_by_actor(actor) do
end
def restrict_deactivated_users(query) do
deactivated_users =
from(u in User.Query.build(deactivated: true), select: u.ap_id)
|> Repo.all()
from(activity in query,
where:
fragment(
"? not in (SELECT ap_id FROM users WHERE info->'deactivated' @> 'true')",
activity.actor
)
where: activity.actor not in ^deactivated_users
)
end

View file

@ -116,7 +116,8 @@ defp cachex_children do
build_cachex("object", default_ttl: 25_000, ttl_interval: 1000, limit: 2500),
build_cachex("rich_media", default_ttl: :timer.minutes(120), limit: 5000),
build_cachex("scrubber", limit: 2500),
build_cachex("idempotency", expiration: idempotency_expiration(), limit: 2500)
build_cachex("idempotency", expiration: idempotency_expiration(), limit: 2500),
build_cachex("web_resp", limit: 2500)
]
end

View file

@ -130,14 +130,16 @@ def swap_object_with_tombstone(object) do
def delete(%Object{data: %{"id" => id}} = object) do
with {:ok, _obj} = swap_object_with_tombstone(object),
deleted_activity = Activity.delete_by_ap_id(id),
{:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do
{:ok, true} <- Cachex.del(:object_cache, "object:#{id}"),
{:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do
{:ok, object, deleted_activity}
end
end
def prune(%Object{data: %{"id" => id}} = object) do
with {:ok, object} <- Repo.delete(object),
{:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do
{:ok, true} <- Cachex.del(:object_cache, "object:#{id}"),
{:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do
{:ok, object}
end
end

122
lib/pleroma/plugs/cache.ex Normal file
View file

@ -0,0 +1,122 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.Cache do
@moduledoc """
Caches successful GET responses.
To enable the cache add the plug to a router pipeline or controller:
plug(Pleroma.Plugs.Cache)
## Configuration
To configure the plug you need to pass settings as the second argument to the `plug/2` macro:
plug(Pleroma.Plugs.Cache, [ttl: nil, query_params: true])
Available options:
- `ttl`: An expiration time (time-to-live). This value should be in milliseconds or `nil` to disable expiration. Defaults to `nil`.
- `query_params`: Take URL query string into account (`true`), ignore it (`false`) or limit to specific params only (list). Defaults to `true`.
Additionally, you can overwrite the TTL inside a controller action by assigning `cache_ttl` to the connection struct:
def index(conn, _params) do
ttl = 60_000 # one minute
conn
|> assign(:cache_ttl, ttl)
|> render("index.html")
end
"""
import Phoenix.Controller, only: [current_path: 1, json: 2]
import Plug.Conn
@behaviour Plug
@defaults %{ttl: nil, query_params: true}
@impl true
def init([]), do: @defaults
def init(opts) do
opts = Map.new(opts)
Map.merge(@defaults, opts)
end
@impl true
def call(%{method: "GET"} = conn, opts) do
key = cache_key(conn, opts)
case Cachex.get(:web_resp_cache, key) do
{:ok, nil} ->
cache_resp(conn, opts)
{:ok, record} ->
send_cached(conn, record)
{atom, message} when atom in [:ignore, :error] ->
render_error(conn, message)
end
end
def call(conn, _), do: conn
# full path including query params
defp cache_key(conn, %{query_params: true}), do: current_path(conn)
# request path without query params
defp cache_key(conn, %{query_params: false}), do: conn.request_path
# request path with specific query params
defp cache_key(conn, %{query_params: query_params}) when is_list(query_params) do
query_string =
conn.params
|> Map.take(query_params)
|> URI.encode_query()
conn.request_path <> "?" <> query_string
end
defp cache_resp(conn, opts) do
register_before_send(conn, fn
%{status: 200, resp_body: body} = conn ->
ttl = Map.get(conn.assigns, :cache_ttl, opts.ttl)
key = cache_key(conn, opts)
content_type = content_type(conn)
record = {content_type, body}
Cachex.put(:web_resp_cache, key, record, ttl: ttl)
put_resp_header(conn, "x-cache", "MISS from Pleroma")
conn ->
conn
end)
end
defp content_type(conn) do
conn
|> Plug.Conn.get_resp_header("content-type")
|> hd()
end
defp send_cached(conn, {content_type, body}) do
conn
|> put_resp_content_type(content_type, nil)
|> put_resp_header("x-cache", "HIT from Pleroma")
|> send_resp(:ok, body)
|> halt()
end
defp render_error(conn, message) do
conn
|> put_status(:internal_server_error)
|> json(%{error: message})
|> halt()
end
end

View file

@ -0,0 +1,41 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.TrailingFormatPlug do
@moduledoc "Calls TrailingFormatPlug for specific paths. Ideally we would just do this in the router, but TrailingFormatPlug needs to be called before Plug.Parsers."
@behaviour Plug
@paths [
"/api/statusnet",
"/api/statuses",
"/api/qvitter",
"/api/search",
"/api/account",
"/api/friends",
"/api/mutes",
"/api/media",
"/api/favorites",
"/api/blocks",
"/api/friendships",
"/api/users",
"/users",
"/nodeinfo",
"/api/help",
"/api/externalprofile",
"/notice",
"/api/pleroma/emoji"
]
def init(opts) do
TrailingFormatPlug.init(opts)
end
for path <- @paths do
def call(%{request_path: unquote(path) <> _} = conn, opts) do
TrailingFormatPlug.call(conn, opts)
end
end
def call(conn, _opts), do: conn
end

View file

@ -569,8 +569,22 @@ def get_cached_by_nickname(nickname) do
end)
end
def get_cached_by_nickname_or_id(nickname_or_id) do
get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
def get_cached_by_nickname_or_id(nickname_or_id, opts \\ []) do
restrict_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
cond do
is_integer(nickname_or_id) or Pleroma.FlakeId.is_flake_id?(nickname_or_id) ->
get_cached_by_id(nickname_or_id) || get_cached_by_nickname(nickname_or_id)
restrict_to_local == false ->
get_cached_by_nickname(nickname_or_id)
restrict_to_local == :unauthenticated and match?(%User{}, opts[:for]) ->
get_cached_by_nickname(nickname_or_id)
true ->
nil
end
end
def get_by_nickname(nickname) do

View file

@ -796,7 +796,7 @@ defp restrict_muted(query, %{"muting_user" => %User{info: info}} = opts) do
)
unless opts["skip_preload"] do
from([thread_mute: tm] in query, where: is_nil(tm))
from([thread_mute: tm] in query, where: is_nil(tm.user_id))
else
query
end

View file

@ -23,6 +23,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
action_fallback(:errors)
plug(Pleroma.Plugs.Cache, [query_params: false] when action in [:activity, :object])
plug(Pleroma.Web.FederatingPlug when action in [:inbox, :relay])
plug(:set_requester_reachable when action in [:inbox])
plug(:relay_active? when action in [:relay])
@ -53,8 +54,10 @@ def object(conn, %{"uuid" => uuid}) do
%Object{} = object <- Object.get_cached_by_ap_id(ap_id),
{_, true} <- {:public?, Visibility.is_public?(object)} do
conn
|> set_cache_ttl_for(object)
|> put_resp_content_type("application/activity+json")
|> json(ObjectView.render("object.json", %{object: object}))
|> put_view(ObjectView)
|> render("object.json", object: object)
else
{:public?, false} ->
{:error, :not_found}
@ -96,14 +99,36 @@ def activity(conn, %{"uuid" => uuid}) do
%Activity{} = activity <- Activity.normalize(ap_id),
{_, true} <- {:public?, Visibility.is_public?(activity)} do
conn
|> set_cache_ttl_for(activity)
|> put_resp_content_type("application/activity+json")
|> json(ObjectView.render("object.json", %{object: activity}))
|> put_view(ObjectView)
|> render("object.json", object: activity)
else
{:public?, false} ->
{:error, :not_found}
{:public?, false} -> {:error, :not_found}
nil -> {:error, :not_found}
end
end
defp set_cache_ttl_for(conn, %Activity{object: object}) do
set_cache_ttl_for(conn, object)
end
defp set_cache_ttl_for(conn, entity) do
ttl =
case entity do
%Object{data: %{"type" => "Question"}} ->
Pleroma.Config.get([:web_cache_ttl, :activity_pub_question])
%Object{} ->
Pleroma.Config.get([:web_cache_ttl, :activity_pub])
_ ->
nil
end
assign(conn, :cache_ttl, ttl)
end
# GET /relay/following
def following(%{assigns: %{relay: true}} = conn, _params) do
conn
@ -251,22 +276,36 @@ def whoami(%{assigns: %{user: %User{} = user}} = conn, _params) do
def whoami(_conn, _params), do: {:error, :not_found}
def read_inbox(%{assigns: %{user: user}} = conn, %{"nickname" => nickname} = params) do
if nickname == user.nickname do
conn
|> put_resp_content_type("application/activity+json")
|> json(UserView.render("inbox.json", %{user: user, max_id: params["max_id"]}))
else
err =
dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
nickname: nickname,
as_nickname: user.nickname
)
def read_inbox(
%{assigns: %{user: %{nickname: nickname} = user}} = conn,
%{"nickname" => nickname} = params
) do
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
|> render("inbox.json", user: user, max_id: params["max_id"])
end
conn
|> put_status(:forbidden)
|> json(err)
end
def read_inbox(%{assigns: %{user: nil}} = conn, %{"nickname" => nickname}) do
err = dgettext("errors", "can't read inbox of %{nickname}", nickname: nickname)
conn
|> put_status(:forbidden)
|> json(err)
end
def read_inbox(%{assigns: %{user: %{nickname: as_nickname}}} = conn, %{
"nickname" => nickname
}) do
err =
dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}",
nickname: nickname,
as_nickname: as_nickname
)
conn
|> put_status(:forbidden)
|> json(err)
end
def handle_user_activity(user, %{"type" => "Create"} = params) do

View file

@ -57,7 +57,7 @@ defmodule Pleroma.Web.Endpoint do
plug(Phoenix.CodeReloader)
end
plug(TrailingFormatPlug)
plug(Pleroma.Plugs.TrailingFormatPlug)
plug(Plug.RequestId)
plug(Plug.Logger)

View file

@ -290,7 +290,7 @@ def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) d
end
def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
account = AccountView.render("account.json", %{user: user, for: for_user})
json(conn, account)
@ -390,7 +390,7 @@ def public_timeline(%{assigns: %{user: user}} = conn, params) do
end
def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"]) do
with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
params =
params
|> Map.put("tag", params["tagged"])

View file

@ -14,7 +14,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
alias Pleroma.Web.MastodonAPI.StatusView
def render("index.json", %{notifications: notifications, for: user}) do
render_many(notifications, NotificationView, "show.json", %{for: user})
safe_render_many(notifications, NotificationView, "show.json", %{for: user})
end
def render("show.json", %{

View file

@ -385,16 +385,27 @@ def render("poll.json", %{object: object} = opts) do
end
if options do
end_time =
(object.data["closed"] || object.data["endTime"])
|> NaiveDateTime.from_iso8601!()
{end_time, expired} =
case object.data["closed"] || object.data["endTime"] do
end_time when is_binary(end_time) ->
end_time =
(object.data["closed"] || object.data["endTime"])
|> NaiveDateTime.from_iso8601!()
expired =
end_time
|> NaiveDateTime.compare(NaiveDateTime.utc_now())
|> case do
:lt -> true
_ -> false
expired =
end_time
|> NaiveDateTime.compare(NaiveDateTime.utc_now())
|> case do
:lt -> true
_ -> false
end
end_time = Utils.to_masto_date(end_time)
{end_time, expired}
_ ->
{nil, false}
end
voted =
@ -421,7 +432,7 @@ def render("poll.json", %{object: object} = opts) do
# Mastodon uses separate ids for polls, but an object can't have
# more than one poll embedded so object id is fine
id: to_string(object.id),
expires_at: Utils.to_masto_date(end_time),
expires_at: end_time,
expired: expired,
multiple: multiple,
votes_count: votes_count,

View file

@ -477,53 +477,12 @@ defmodule Pleroma.Web.Router do
scope "/api", Pleroma.Web do
pipe_through(:api)
post("/account/register", TwitterAPI.Controller, :register)
post("/account/password_reset", TwitterAPI.Controller, :password_reset)
post("/account/resend_confirmation_email", TwitterAPI.Controller, :resend_confirmation_email)
get(
"/account/confirm_email/:user_id/:token",
TwitterAPI.Controller,
:confirm_email,
as: :confirm_email
)
scope [] do
pipe_through(:oauth_read_or_public)
get("/statuses/user_timeline", TwitterAPI.Controller, :user_timeline)
get("/qvitter/statuses/user_timeline", TwitterAPI.Controller, :user_timeline)
get("/users/show", TwitterAPI.Controller, :show_user)
get("/statuses/followers", TwitterAPI.Controller, :followers)
get("/statuses/friends", TwitterAPI.Controller, :friends)
get("/statuses/blocks", TwitterAPI.Controller, :blocks)
get("/statuses/show/:id", TwitterAPI.Controller, :fetch_status)
get("/statusnet/conversation/:id", TwitterAPI.Controller, :fetch_conversation)
get("/search", TwitterAPI.Controller, :search)
get("/statusnet/tags/timeline/:tag", TwitterAPI.Controller, :public_and_external_timeline)
end
end
scope "/api", Pleroma.Web do
pipe_through([:api, :oauth_read_or_public])
get("/statuses/public_timeline", TwitterAPI.Controller, :public_timeline)
get(
"/statuses/public_and_external_timeline",
TwitterAPI.Controller,
:public_and_external_timeline
)
get("/statuses/networkpublic_timeline", TwitterAPI.Controller, :public_and_external_timeline)
end
scope "/api", Pleroma.Web, as: :twitter_api_search do
pipe_through([:api, :oauth_read_or_public])
get("/pleroma/search_user", TwitterAPI.Controller, :search_user)
end
scope "/api", Pleroma.Web, as: :authenticated_twitter_api do
@ -535,67 +494,8 @@ defmodule Pleroma.Web.Router do
scope [] do
pipe_through(:oauth_read)
get("/account/verify_credentials", TwitterAPI.Controller, :verify_credentials)
post("/account/verify_credentials", TwitterAPI.Controller, :verify_credentials)
get("/statuses/home_timeline", TwitterAPI.Controller, :friends_timeline)
get("/statuses/friends_timeline", TwitterAPI.Controller, :friends_timeline)
get("/statuses/mentions", TwitterAPI.Controller, :mentions_timeline)
get("/statuses/mentions_timeline", TwitterAPI.Controller, :mentions_timeline)
get("/statuses/dm_timeline", TwitterAPI.Controller, :dm_timeline)
get("/qvitter/statuses/notifications", TwitterAPI.Controller, :notifications)
get("/pleroma/friend_requests", TwitterAPI.Controller, :friend_requests)
get("/friends/ids", TwitterAPI.Controller, :friends_ids)
get("/friendships/no_retweets/ids", TwitterAPI.Controller, :empty_array)
get("/mutes/users/ids", TwitterAPI.Controller, :empty_array)
get("/qvitter/mutes", TwitterAPI.Controller, :raw_empty_array)
get("/externalprofile/show", TwitterAPI.Controller, :external_profile)
post("/qvitter/statuses/notifications/read", TwitterAPI.Controller, :notifications_read)
end
scope [] do
pipe_through(:oauth_write)
post("/account/update_profile", TwitterAPI.Controller, :update_profile)
post("/account/update_profile_banner", TwitterAPI.Controller, :update_banner)
post("/qvitter/update_background_image", TwitterAPI.Controller, :update_background)
post("/statuses/update", TwitterAPI.Controller, :status_update)
post("/statuses/retweet/:id", TwitterAPI.Controller, :retweet)
post("/statuses/unretweet/:id", TwitterAPI.Controller, :unretweet)
post("/statuses/destroy/:id", TwitterAPI.Controller, :delete_post)
post("/statuses/pin/:id", TwitterAPI.Controller, :pin)
post("/statuses/unpin/:id", TwitterAPI.Controller, :unpin)
post("/statusnet/media/upload", TwitterAPI.Controller, :upload)
post("/media/upload", TwitterAPI.Controller, :upload_json)
post("/media/metadata/create", TwitterAPI.Controller, :update_media)
post("/favorites/create/:id", TwitterAPI.Controller, :favorite)
post("/favorites/create", TwitterAPI.Controller, :favorite)
post("/favorites/destroy/:id", TwitterAPI.Controller, :unfavorite)
post("/qvitter/update_avatar", TwitterAPI.Controller, :update_avatar)
end
scope [] do
pipe_through(:oauth_follow)
post("/pleroma/friendships/approve", TwitterAPI.Controller, :approve_friend_request)
post("/pleroma/friendships/deny", TwitterAPI.Controller, :deny_friend_request)
post("/friendships/create", TwitterAPI.Controller, :follow)
post("/friendships/destroy", TwitterAPI.Controller, :unfollow)
post("/blocks/create", TwitterAPI.Controller, :block)
post("/blocks/destroy", TwitterAPI.Controller, :unblock)
end
end
pipeline :ap_service_actor do

View file

@ -1,38 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.Representers.BaseRepresenter do
defmacro __using__(_opts) do
quote do
def to_json(object) do
to_json(object, %{})
end
def to_json(object, options) do
object
|> to_map(options)
|> Jason.encode!()
end
def enum_to_list(enum, options) do
mapping = fn el -> to_map(el, options) end
Enum.map(enum, mapping)
end
def to_map(object) do
to_map(object, %{})
end
def enum_to_json(enum) do
enum_to_json(enum, %{})
end
def enum_to_json(enum, options) do
enum
|> enum_to_list(options)
|> Jason.encode!()
end
end
end
end

View file

@ -1,39 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter do
use Pleroma.Web.TwitterAPI.Representers.BaseRepresenter
alias Pleroma.Object
def to_map(%Object{data: %{"url" => [url | _]}} = object, _opts) do
data = object.data
%{
url: url["href"] |> Pleroma.Web.MediaProxy.url(),
mimetype: url["mediaType"] || url["mimeType"],
id: data["uuid"],
oembed: false,
description: data["name"]
}
end
def to_map(%Object{data: %{"url" => url} = data}, _opts) when is_binary(url) do
%{
url: url |> Pleroma.Web.MediaProxy.url(),
mimetype: data["mediaType"] || data["mimeType"],
id: data["uuid"],
oembed: false,
description: data["name"]
}
end
def to_map(%Object{}, _opts) do
%{}
end
# If we only get the naked data, wrap in an object
def to_map(%{} = data, opts) do
to_map(%Object{data: data}, opts)
end
end

View file

@ -3,133 +3,14 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
alias Pleroma.Activity
alias Pleroma.Emails.Mailer
alias Pleroma.Emails.UserEmail
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.TwitterAPI.UserView
import Ecto.Query
require Pleroma.Constants
def create_status(%User{} = user, %{"status" => _} = data) do
CommonAPI.post(user, data)
end
def delete(%User{} = user, id) do
with %Activity{data: %{"type" => _type}} <- Activity.get_by_id(id),
{:ok, activity} <- CommonAPI.delete(id, user) do
{:ok, activity}
end
end
def follow(%User{} = follower, params) do
with {:ok, %User{} = followed} <- get_user(params) do
CommonAPI.follow(follower, followed)
end
end
def unfollow(%User{} = follower, params) do
with {:ok, %User{} = unfollowed} <- get_user(params),
{:ok, follower} <- CommonAPI.unfollow(follower, unfollowed) do
{:ok, follower, unfollowed}
end
end
def block(%User{} = blocker, params) do
with {:ok, %User{} = blocked} <- get_user(params),
{:ok, blocker} <- User.block(blocker, blocked),
{:ok, _activity} <- ActivityPub.block(blocker, blocked) do
{:ok, blocker, blocked}
else
err -> err
end
end
def unblock(%User{} = blocker, params) do
with {:ok, %User{} = blocked} <- get_user(params),
{:ok, blocker} <- User.unblock(blocker, blocked),
{:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
{:ok, blocker, blocked}
else
err -> err
end
end
def repeat(%User{} = user, ap_id_or_id) do
with {:ok, _announce, %{data: %{"id" => id}}} <- CommonAPI.repeat(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
{:ok, activity}
end
end
def unrepeat(%User{} = user, ap_id_or_id) do
with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
{:ok, activity}
end
end
def pin(%User{} = user, ap_id_or_id) do
CommonAPI.pin(ap_id_or_id, user)
end
def unpin(%User{} = user, ap_id_or_id) do
CommonAPI.unpin(ap_id_or_id, user)
end
def fav(%User{} = user, ap_id_or_id) do
with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
{:ok, activity}
end
end
def unfav(%User{} = user, ap_id_or_id) do
with {:ok, _unfav, _fav, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
{:ok, activity}
end
end
def upload(%Plug.Upload{} = file, %User{} = user, format \\ "xml") do
{:ok, object} = ActivityPub.upload(file, actor: User.ap_id(user))
url = List.first(object.data["url"])
href = url["href"]
type = url["mediaType"]
case format do
"xml" ->
# Fake this as good as possible...
"""
<?xml version="1.0" encoding="UTF-8"?>
<rsp stat="ok" xmlns:atom="http://www.w3.org/2005/Atom">
<mediaid>#{object.id}</mediaid>
<media_id>#{object.id}</media_id>
<media_id_string>#{object.id}</media_id_string>
<media_url>#{href}</media_url>
<mediaurl>#{href}</mediaurl>
<atom:link rel="enclosure" href="#{href}" type="#{type}"></atom:link>
</rsp>
"""
"json" ->
%{
media_id: object.id,
media_id_string: "#{object.id}}",
media_url: href,
size: 0
}
|> Jason.encode!()
end
end
def register_user(params, opts \\ []) do
token = params["token"]
@ -236,80 +117,4 @@ def password_reset(nickname_or_email) do
{:error, "unknown user"}
end
end
def get_user(user \\ nil, params) do
case params do
%{"user_id" => user_id} ->
case User.get_cached_by_nickname_or_id(user_id) do
nil ->
{:error, "No user with such user_id"}
%User{info: %{deactivated: true}} ->
{:error, "User has been disabled"}
user ->
{:ok, user}
end
%{"screen_name" => nickname} ->
case User.get_cached_by_nickname(nickname) do
nil -> {:error, "No user with such screen_name"}
target -> {:ok, target}
end
_ ->
if user do
{:ok, user}
else
{:error, "You need to specify screen_name or user_id"}
end
end
end
defp parse_int(string, default)
defp parse_int(string, default) when is_binary(string) do
with {n, _} <- Integer.parse(string) do
n
else
_e -> default
end
end
defp parse_int(_, default), do: default
# TODO: unify the search query with MastoAPI one and do only pagination here
def search(_user, %{"q" => query} = params) do
limit = parse_int(params["rpp"], 20)
page = parse_int(params["page"], 1)
offset = (page - 1) * limit
q =
from(
[a, o] in Activity.with_preloaded_object(Activity),
where: fragment("?->>'type' = 'Create'", a.data),
where: ^Pleroma.Constants.as_public() in a.recipients,
where:
fragment(
"to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
o.data,
^query
),
limit: ^limit,
offset: ^offset,
# this one isn't indexed so psql won't take the wrong index.
order_by: [desc: :inserted_at]
)
_activities = Repo.all(q)
end
def get_external_profile(for_user, uri) do
with {:ok, %User{} = user} <- User.get_or_fetch(uri) do
{:ok, UserView.render("show.json", %{user: user, for: for_user})}
else
_e ->
{:error, "Couldn't find user"}
end
end
end

View file

@ -5,448 +5,16 @@
defmodule Pleroma.Web.TwitterAPI.Controller do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
alias Ecto.Changeset
alias Pleroma.Activity
alias Pleroma.Formatter
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.TwitterAPI.ActivityView
alias Pleroma.Web.TwitterAPI.NotificationView
alias Pleroma.Web.TwitterAPI.TokenView
alias Pleroma.Web.TwitterAPI.TwitterAPI
alias Pleroma.Web.TwitterAPI.UserView
require Logger
plug(Pleroma.Plugs.RateLimiter, :password_reset when action == :password_reset)
plug(:only_if_public_instance when action in [:public_timeline, :public_and_external_timeline])
action_fallback(:errors)
def verify_credentials(%{assigns: %{user: user}} = conn, _params) do
token = Phoenix.Token.sign(conn, "user socket", user.id)
conn
|> put_view(UserView)
|> render("show.json", %{user: user, token: token, for: user})
end
def status_update(%{assigns: %{user: user}} = conn, %{"status" => _} = status_data) do
with media_ids <- extract_media_ids(status_data),
{:ok, activity} <-
TwitterAPI.create_status(user, Map.put(status_data, "media_ids", media_ids)) do
conn
|> json(ActivityView.render("activity.json", activity: activity, for: user))
else
_ -> empty_status_reply(conn)
end
end
def status_update(conn, _status_data) do
empty_status_reply(conn)
end
defp empty_status_reply(conn) do
bad_request_reply(conn, "Client must provide a 'status' parameter with a value.")
end
defp extract_media_ids(status_data) do
with media_ids when not is_nil(media_ids) <- status_data["media_ids"],
split_ids <- String.split(media_ids, ","),
clean_ids <- Enum.reject(split_ids, fn id -> String.length(id) == 0 end) do
clean_ids
else
_e -> []
end
end
def public_and_external_timeline(%{assigns: %{user: user}} = conn, params) do
params =
params
|> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
activities = ActivityPub.fetch_public_activities(params)
conn
|> put_view(ActivityView)
|> render("index.json", %{activities: activities, for: user})
end
def public_timeline(%{assigns: %{user: user}} = conn, params) do
params =
params
|> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", true)
|> Map.put("blocking_user", user)
activities = ActivityPub.fetch_public_activities(params)
conn
|> put_view(ActivityView)
|> render("index.json", %{activities: activities, for: user})
end
def friends_timeline(%{assigns: %{user: user}} = conn, params) do
params =
params
|> Map.put("type", ["Create", "Announce", "Follow", "Like"])
|> Map.put("blocking_user", user)
|> Map.put("user", user)
activities = ActivityPub.fetch_activities([user.ap_id | user.following], params)
conn
|> put_view(ActivityView)
|> render("index.json", %{activities: activities, for: user})
end
def show_user(conn, params) do
for_user = conn.assigns.user
with {:ok, shown} <- TwitterAPI.get_user(params),
true <-
User.auth_active?(shown) ||
(for_user && (for_user.id == shown.id || User.superuser?(for_user))) do
params =
if for_user do
%{user: shown, for: for_user}
else
%{user: shown}
end
conn
|> put_view(UserView)
|> render("show.json", params)
else
{:error, msg} ->
bad_request_reply(conn, msg)
false ->
conn
|> put_status(404)
|> json(%{error: "Unconfirmed user"})
end
end
def user_timeline(%{assigns: %{user: user}} = conn, params) do
case TwitterAPI.get_user(user, params) do
{:ok, target_user} ->
# Twitter and ActivityPub use a different name and sense for this parameter.
{include_rts, params} = Map.pop(params, "include_rts")
params =
case include_rts do
x when x == "false" or x == "0" -> Map.put(params, "exclude_reblogs", "true")
_ -> params
end
activities = ActivityPub.fetch_user_activities(target_user, user, params)
conn
|> put_view(ActivityView)
|> render("index.json", %{activities: activities, for: user})
{:error, msg} ->
bad_request_reply(conn, msg)
end
end
def mentions_timeline(%{assigns: %{user: user}} = conn, params) do
params =
params
|> Map.put("type", ["Create", "Announce", "Follow", "Like"])
|> Map.put("blocking_user", user)
|> Map.put(:visibility, ~w[unlisted public private])
activities = ActivityPub.fetch_activities([user.ap_id], params)
conn
|> put_view(ActivityView)
|> render("index.json", %{activities: activities, for: user})
end
def dm_timeline(%{assigns: %{user: user}} = conn, params) do
params =
params
|> Map.put("type", "Create")
|> Map.put("blocking_user", user)
|> Map.put("user", user)
|> Map.put(:visibility, "direct")
|> Map.put(:order, :desc)
activities =
ActivityPub.fetch_activities_query([user.ap_id], params)
|> Repo.all()
conn
|> put_view(ActivityView)
|> render("index.json", %{activities: activities, for: user})
end
def notifications(%{assigns: %{user: user}} = conn, params) do
params =
if Map.has_key?(params, "with_muted") do
Map.put(params, :with_muted, params["with_muted"] in [true, "True", "true", "1"])
else
params
end
notifications = Notification.for_user(user, params)
conn
|> put_view(NotificationView)
|> render("notification.json", %{notifications: notifications, for: user})
end
def notifications_read(%{assigns: %{user: user}} = conn, %{"latest_id" => latest_id} = params) do
Notification.set_read_up_to(user, latest_id)
notifications = Notification.for_user(user, params)
conn
|> put_view(NotificationView)
|> render("notification.json", %{notifications: notifications, for: user})
end
def notifications_read(%{assigns: %{user: _user}} = conn, _) do
bad_request_reply(conn, "You need to specify latest_id")
end
def follow(%{assigns: %{user: user}} = conn, params) do
case TwitterAPI.follow(user, params) do
{:ok, user, followed, _activity} ->
conn
|> put_view(UserView)
|> render("show.json", %{user: followed, for: user})
{:error, msg} ->
forbidden_json_reply(conn, msg)
end
end
def block(%{assigns: %{user: user}} = conn, params) do
case TwitterAPI.block(user, params) do
{:ok, user, blocked} ->
conn
|> put_view(UserView)
|> render("show.json", %{user: blocked, for: user})
{:error, msg} ->
forbidden_json_reply(conn, msg)
end
end
def unblock(%{assigns: %{user: user}} = conn, params) do
case TwitterAPI.unblock(user, params) do
{:ok, user, blocked} ->
conn
|> put_view(UserView)
|> render("show.json", %{user: blocked, for: user})
{:error, msg} ->
forbidden_json_reply(conn, msg)
end
end
def delete_post(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {:ok, activity} <- TwitterAPI.delete(user, id) do
conn
|> put_view(ActivityView)
|> render("activity.json", %{activity: activity, for: user})
end
end
def unfollow(%{assigns: %{user: user}} = conn, params) do
case TwitterAPI.unfollow(user, params) do
{:ok, user, unfollowed} ->
conn
|> put_view(UserView)
|> render("show.json", %{user: unfollowed, for: user})
{:error, msg} ->
forbidden_json_reply(conn, msg)
end
end
def fetch_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with %Activity{} = activity <- Activity.get_by_id(id),
true <- Visibility.visible_for_user?(activity, user) do
conn
|> put_view(ActivityView)
|> render("activity.json", %{activity: activity, for: user})
end
end
def fetch_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with context when is_binary(context) <- Utils.conversation_id_to_context(id),
activities <-
ActivityPub.fetch_activities_for_context(context, %{
"blocking_user" => user,
"user" => user
}) do
conn
|> put_view(ActivityView)
|> render("index.json", %{activities: activities, for: user})
end
end
@doc """
Updates metadata of uploaded media object.
Derived from [Twitter API endpoint](https://developer.twitter.com/en/docs/media/upload-media/api-reference/post-media-metadata-create).
"""
def update_media(%{assigns: %{user: user}} = conn, %{"media_id" => id} = data) do
object = Repo.get(Object, id)
description = get_in(data, ["alt_text", "text"]) || data["name"] || data["description"]
{conn, status, response_body} =
cond do
!object ->
{halt(conn), :not_found, ""}
!Object.authorize_mutation(object, user) ->
{halt(conn), :forbidden, "You can only update your own uploads."}
!is_binary(description) ->
{conn, :not_modified, ""}
true ->
new_data = Map.put(object.data, "name", description)
{:ok, _} =
object
|> Object.change(%{data: new_data})
|> Repo.update()
{conn, :no_content, ""}
end
conn
|> put_status(status)
|> json(response_body)
end
def upload(%{assigns: %{user: user}} = conn, %{"media" => media}) do
response = TwitterAPI.upload(media, user)
conn
|> put_resp_content_type("application/atom+xml")
|> send_resp(200, response)
end
def upload_json(%{assigns: %{user: user}} = conn, %{"media" => media}) do
response = TwitterAPI.upload(media, user, "json")
conn
|> json_reply(200, response)
end
def get_by_id_or_ap_id(id) do
activity = Activity.get_by_id(id) || Activity.get_create_by_object_ap_id(id)
if activity.data["type"] == "Create" do
activity
else
Activity.get_create_by_object_ap_id(activity.data["object"])
end
end
def favorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {:ok, activity} <- TwitterAPI.fav(user, id) do
conn
|> put_view(ActivityView)
|> render("activity.json", %{activity: activity, for: user})
else
_ -> json_reply(conn, 400, Jason.encode!(%{}))
end
end
def unfavorite(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {:ok, activity} <- TwitterAPI.unfav(user, id) do
conn
|> put_view(ActivityView)
|> render("activity.json", %{activity: activity, for: user})
else
_ -> json_reply(conn, 400, Jason.encode!(%{}))
end
end
def retweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {:ok, activity} <- TwitterAPI.repeat(user, id) do
conn
|> put_view(ActivityView)
|> render("activity.json", %{activity: activity, for: user})
else
_ -> json_reply(conn, 400, Jason.encode!(%{}))
end
end
def unretweet(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {:ok, activity} <- TwitterAPI.unrepeat(user, id) do
conn
|> put_view(ActivityView)
|> render("activity.json", %{activity: activity, for: user})
else
_ -> json_reply(conn, 400, Jason.encode!(%{}))
end
end
def pin(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {:ok, activity} <- TwitterAPI.pin(user, id) do
conn
|> put_view(ActivityView)
|> render("activity.json", %{activity: activity, for: user})
else
{:error, message} -> bad_request_reply(conn, message)
err -> err
end
end
def unpin(%{assigns: %{user: user}} = conn, %{"id" => id}) do
with {:ok, activity} <- TwitterAPI.unpin(user, id) do
conn
|> put_view(ActivityView)
|> render("activity.json", %{activity: activity, for: user})
else
{:error, message} -> bad_request_reply(conn, message)
err -> err
end
end
def register(conn, params) do
with {:ok, user} <- TwitterAPI.register_user(params) do
conn
|> put_view(UserView)
|> render("show.json", %{user: user})
else
{:error, errors} ->
conn
|> json_reply(400, Jason.encode!(errors))
end
end
def password_reset(conn, params) do
nickname_or_email = params["email"] || params["nickname"]
with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
json_response(conn, :no_content, "")
else
{:error, "unknown user"} ->
send_resp(conn, :not_found, "")
{:error, _} ->
send_resp(conn, :bad_request, "")
end
end
def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
with %User{} = user <- User.get_cached_by_id(uid),
true <- user.local,
@ -460,147 +28,6 @@ def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
end
end
def resend_confirmation_email(conn, params) do
nickname_or_email = params["email"] || params["nickname"]
with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
{:ok, _} <- User.try_send_confirmation_email(user) do
conn
|> json_response(:no_content, "")
end
end
def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
change = Changeset.change(user, %{avatar: nil})
{:ok, user} = User.update_and_set_cache(change)
CommonAPI.update(user)
conn
|> put_view(UserView)
|> render("show.json", %{user: user, for: user})
end
def update_avatar(%{assigns: %{user: user}} = conn, params) do
{:ok, object} = ActivityPub.upload(params, type: :avatar)
change = Changeset.change(user, %{avatar: object.data})
{:ok, user} = User.update_and_set_cache(change)
CommonAPI.update(user)
conn
|> put_view(UserView)
|> render("show.json", %{user: user, for: user})
end
def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
with new_info <- %{"banner" => %{}},
info_cng <- User.Info.profile_update(user.info, new_info),
changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
{:ok, user} <- User.update_and_set_cache(changeset) do
CommonAPI.update(user)
response = %{url: nil} |> Jason.encode!()
conn
|> json_reply(200, response)
end
end
def update_banner(%{assigns: %{user: user}} = conn, params) do
with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
new_info <- %{"banner" => object.data},
info_cng <- User.Info.profile_update(user.info, new_info),
changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
{:ok, user} <- User.update_and_set_cache(changeset) do
CommonAPI.update(user)
%{"url" => [%{"href" => href} | _]} = object.data
response = %{url: href} |> Jason.encode!()
conn
|> json_reply(200, response)
end
end
def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
with new_info <- %{"background" => %{}},
info_cng <- User.Info.profile_update(user.info, new_info),
changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
{:ok, _user} <- User.update_and_set_cache(changeset) do
response = %{url: nil} |> Jason.encode!()
conn
|> json_reply(200, response)
end
end
def update_background(%{assigns: %{user: user}} = conn, params) do
with {:ok, object} <- ActivityPub.upload(params, type: :background),
new_info <- %{"background" => object.data},
info_cng <- User.Info.profile_update(user.info, new_info),
changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
{:ok, _user} <- User.update_and_set_cache(changeset) do
%{"url" => [%{"href" => href} | _]} = object.data
response = %{url: href} |> Jason.encode!()
conn
|> json_reply(200, response)
end
end
def external_profile(%{assigns: %{user: current_user}} = conn, %{"profileurl" => uri}) do
with {:ok, user_map} <- TwitterAPI.get_external_profile(current_user, uri),
response <- Jason.encode!(user_map) do
conn
|> json_reply(200, response)
else
_e ->
conn
|> put_status(404)
|> json(%{error: "Can't find user"})
end
end
def followers(%{assigns: %{user: for_user}} = conn, params) do
{:ok, page} = Ecto.Type.cast(:integer, params["page"] || 1)
with {:ok, user} <- TwitterAPI.get_user(for_user, params),
{:ok, followers} <- User.get_followers(user, page) do
followers =
cond do
for_user && user.id == for_user.id -> followers
user.info.hide_followers -> []
true -> followers
end
conn
|> put_view(UserView)
|> render("index.json", %{users: followers, for: conn.assigns[:user]})
else
_e -> bad_request_reply(conn, "Can't get followers")
end
end
def friends(%{assigns: %{user: for_user}} = conn, params) do
{:ok, page} = Ecto.Type.cast(:integer, params["page"] || 1)
{:ok, export} = Ecto.Type.cast(:boolean, params["all"] || false)
page = if export, do: nil, else: page
with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
{:ok, friends} <- User.get_friends(user, page) do
friends =
cond do
for_user && user.id == for_user.id -> friends
user.info.hide_follows -> []
true -> friends
end
conn
|> put_view(UserView)
|> render("index.json", %{users: friends, for: conn.assigns[:user]})
else
_e -> bad_request_reply(conn, "Can't get friends")
end
end
def oauth_tokens(%{assigns: %{user: user}} = conn, _params) do
with oauth_tokens <- Token.get_user_tokens(user) do
conn
@ -615,189 +42,6 @@ def revoke_token(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
json_reply(conn, 201, "")
end
def blocks(%{assigns: %{user: user}} = conn, _params) do
with blocked_users <- User.blocked_users(user) do
conn
|> put_view(UserView)
|> render("index.json", %{users: blocked_users, for: user})
end
end
def friend_requests(conn, params) do
with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params),
{:ok, friend_requests} <- User.get_follow_requests(user) do
conn
|> put_view(UserView)
|> render("index.json", %{users: friend_requests, for: conn.assigns[:user]})
else
_e -> bad_request_reply(conn, "Can't get friend requests")
end
end
def approve_friend_request(conn, %{"user_id" => uid} = _params) do
with followed <- conn.assigns[:user],
%User{} = follower <- User.get_cached_by_id(uid),
{:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
conn
|> put_view(UserView)
|> render("show.json", %{user: follower, for: followed})
else
e -> bad_request_reply(conn, "Can't approve user: #{inspect(e)}")
end
end
def deny_friend_request(conn, %{"user_id" => uid} = _params) do
with followed <- conn.assigns[:user],
%User{} = follower <- User.get_cached_by_id(uid),
{:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
conn
|> put_view(UserView)
|> render("show.json", %{user: follower, for: followed})
else
e -> bad_request_reply(conn, "Can't deny user: #{inspect(e)}")
end
end
def friends_ids(%{assigns: %{user: user}} = conn, _params) do
with {:ok, friends} <- User.get_friends(user) do
ids =
friends
|> Enum.map(fn x -> x.id end)
|> Jason.encode!()
json(conn, ids)
else
_e -> bad_request_reply(conn, "Can't get friends")
end
end
def empty_array(conn, _params) do
json(conn, Jason.encode!([]))
end
def raw_empty_array(conn, _params) do
json(conn, [])
end
defp build_info_cng(user, params) do
info_params =
[
"no_rich_text",
"locked",
"hide_followers",
"hide_follows",
"hide_favorites",
"show_role",
"skip_thread_containment"
]
|> Enum.reduce(%{}, fn key, res ->
if value = params[key] do
Map.put(res, key, value == "true")
else
res
end
end)
info_params =
if value = params["default_scope"] do
Map.put(info_params, "default_scope", value)
else
info_params
end
User.Info.profile_update(user.info, info_params)
end
defp parse_profile_bio(user, params) do
if bio = params["description"] do
emojis_text = (params["description"] || "") <> " " <> (params["name"] || "")
emojis =
((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
|> Enum.dedup()
user_info =
user.info
|> Map.put(
"emoji",
emojis
)
params
|> Map.put("bio", User.parse_bio(bio, user))
|> Map.put("info", user_info)
else
params
end
end
def update_profile(%{assigns: %{user: user}} = conn, params) do
params = parse_profile_bio(user, params)
info_cng = build_info_cng(user, params)
with changeset <- User.update_changeset(user, params),
changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
{:ok, user} <- User.update_and_set_cache(changeset) do
CommonAPI.update(user)
conn
|> put_view(UserView)
|> render("user.json", %{user: user, for: user})
else
error ->
Logger.debug("Can't update user: #{inspect(error)}")
bad_request_reply(conn, "Can't update user")
end
end
def search(%{assigns: %{user: user}} = conn, %{"q" => _query} = params) do
activities = TwitterAPI.search(user, params)
conn
|> put_view(ActivityView)
|> render("index.json", %{activities: activities, for: user})
end
def search_user(%{assigns: %{user: user}} = conn, %{"query" => query}) do
users = User.search(query, resolve: true, for_user: user)
conn
|> put_view(UserView)
|> render("index.json", %{users: users, for: user})
end
defp bad_request_reply(conn, error_message) do
json = error_json(conn, error_message)
json_reply(conn, 400, json)
end
defp json_reply(conn, status, json) do
conn
|> put_resp_content_type("application/json")
|> send_resp(status, json)
end
defp forbidden_json_reply(conn, error_message) do
json = error_json(conn, error_message)
json_reply(conn, 403, json)
end
def only_if_public_instance(%{assigns: %{user: %User{}}} = conn, _), do: conn
def only_if_public_instance(conn, _) do
if Pleroma.Config.get([:instance, :public]) do
conn
else
conn
|> forbidden_json_reply("Invalid credentials.")
|> halt()
end
end
defp error_json(conn, error_message) do
%{"error" => error_message, "request" => conn.request_path} |> Jason.encode!()
end
def errors(conn, {:param_cast, _}) do
conn
|> put_status(400)
@ -809,4 +53,34 @@ def errors(conn, _) do
|> put_status(500)
|> json("Something went wrong")
end
defp json_reply(conn, status, json) do
conn
|> put_resp_content_type("application/json")
|> send_resp(status, json)
end
def notifications_read(%{assigns: %{user: user}} = conn, %{"latest_id" => latest_id} = params) do
Notification.set_read_up_to(user, latest_id)
notifications = Notification.for_user(user, params)
conn
# XXX: This is a hack because pleroma-fe still uses that API.
|> put_view(Pleroma.Web.MastodonAPI.NotificationView)
|> render("index.json", %{notifications: notifications, for: user})
end
def notifications_read(%{assigns: %{user: _user}} = conn, _) do
bad_request_reply(conn, "You need to specify latest_id")
end
defp bad_request_reply(conn, error_message) do
json = error_json(conn, error_message)
json_reply(conn, 400, json)
end
defp error_json(conn, error_message) do
%{"error" => error_message, "request" => conn.request_path} |> Jason.encode!()
end
end

View file

@ -1,366 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.ActivityView do
use Pleroma.Web, :view
alias Pleroma.Activity
alias Pleroma.Formatter
alias Pleroma.HTML
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.TwitterAPI.ActivityView
alias Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter
alias Pleroma.Web.TwitterAPI.UserView
import Ecto.Query
require Logger
require Pleroma.Constants
defp query_context_ids([]), do: []
defp query_context_ids(contexts) do
query = from(o in Object, where: fragment("(?)->>'id' = ANY(?)", o.data, ^contexts))
Repo.all(query)
end
defp query_users([]), do: []
defp query_users(user_ids) do
query = from(user in User, where: user.ap_id in ^user_ids)
Repo.all(query)
end
defp collect_context_ids(activities) do
_contexts =
activities
|> Enum.reject(& &1.data["context_id"])
|> Enum.map(fn %{data: data} ->
data["context"]
end)
|> Enum.filter(& &1)
|> query_context_ids()
|> Enum.reduce(%{}, fn %{data: %{"id" => ap_id}, id: id}, acc ->
Map.put(acc, ap_id, id)
end)
end
defp collect_users(activities) do
activities
|> Enum.map(fn activity ->
case activity.data do
data = %{"type" => "Follow"} ->
[data["actor"], data["object"]]
data ->
[data["actor"]]
end ++ activity.recipients
end)
|> List.flatten()
|> Enum.uniq()
|> query_users()
|> Enum.reduce(%{}, fn user, acc ->
Map.put(acc, user.ap_id, user)
end)
end
defp get_context_id(%{data: %{"context_id" => context_id}}, _) when not is_nil(context_id),
do: context_id
defp get_context_id(%{data: %{"context" => nil}}, _), do: nil
defp get_context_id(%{data: %{"context" => context}}, options) do
cond do
id = options[:context_ids][context] -> id
true -> Utils.context_to_conversation_id(context)
end
end
defp get_context_id(_, _), do: nil
defp get_user(ap_id, opts) do
cond do
user = opts[:users][ap_id] ->
user
String.ends_with?(ap_id, "/followers") ->
nil
ap_id == Pleroma.Constants.as_public() ->
nil
user = User.get_cached_by_ap_id(ap_id) ->
user
user = User.get_by_guessed_nickname(ap_id) ->
user
true ->
User.error_user(ap_id)
end
end
def render("index.json", opts) do
context_ids = collect_context_ids(opts.activities)
users = collect_users(opts.activities)
opts =
opts
|> Map.put(:context_ids, context_ids)
|> Map.put(:users, users)
safe_render_many(
opts.activities,
ActivityView,
"activity.json",
opts
)
end
def render("activity.json", %{activity: %{data: %{"type" => "Delete"}} = activity} = opts) do
user = get_user(activity.data["actor"], opts)
created_at = activity.data["published"] |> Utils.date_to_asctime()
%{
"id" => activity.id,
"uri" => activity.data["object"],
"user" => UserView.render("show.json", %{user: user, for: opts[:for]}),
"attentions" => [],
"statusnet_html" => "deleted notice {{tag",
"text" => "deleted notice {{tag",
"is_local" => activity.local,
"is_post_verb" => false,
"created_at" => created_at,
"in_reply_to_status_id" => nil,
"external_url" => activity.data["id"],
"activity_type" => "delete"
}
end
def render("activity.json", %{activity: %{data: %{"type" => "Follow"}} = activity} = opts) do
user = get_user(activity.data["actor"], opts)
created_at = activity.data["published"] || DateTime.to_iso8601(activity.inserted_at)
created_at = created_at |> Utils.date_to_asctime()
followed = get_user(activity.data["object"], opts)
text = "#{user.nickname} started following #{followed.nickname}"
%{
"id" => activity.id,
"user" => UserView.render("show.json", %{user: user, for: opts[:for]}),
"attentions" => [],
"statusnet_html" => text,
"text" => text,
"is_local" => activity.local,
"is_post_verb" => false,
"created_at" => created_at,
"in_reply_to_status_id" => nil,
"external_url" => activity.data["id"],
"activity_type" => "follow"
}
end
def render("activity.json", %{activity: %{data: %{"type" => "Announce"}} = activity} = opts) do
user = get_user(activity.data["actor"], opts)
created_at = activity.data["published"] |> Utils.date_to_asctime()
announced_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
text = "#{user.nickname} repeated a status."
retweeted_status = render("activity.json", Map.merge(opts, %{activity: announced_activity}))
%{
"id" => activity.id,
"user" => UserView.render("show.json", %{user: user, for: opts[:for]}),
"statusnet_html" => text,
"text" => text,
"is_local" => activity.local,
"is_post_verb" => false,
"uri" => "tag:#{activity.data["id"]}:objectType=note",
"created_at" => created_at,
"retweeted_status" => retweeted_status,
"statusnet_conversation_id" => get_context_id(announced_activity, opts),
"external_url" => activity.data["id"],
"activity_type" => "repeat"
}
end
def render("activity.json", %{activity: %{data: %{"type" => "Like"}} = activity} = opts) do
user = get_user(activity.data["actor"], opts)
liked_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
liked_activity_id = if liked_activity, do: liked_activity.id, else: nil
created_at =
activity.data["published"]
|> Utils.date_to_asctime()
text = "#{user.nickname} favorited a status."
favorited_status =
if liked_activity,
do: render("activity.json", Map.merge(opts, %{activity: liked_activity})),
else: nil
%{
"id" => activity.id,
"user" => UserView.render("show.json", %{user: user, for: opts[:for]}),
"statusnet_html" => text,
"text" => text,
"is_local" => activity.local,
"is_post_verb" => false,
"uri" => "tag:#{activity.data["id"]}:objectType=Favourite",
"created_at" => created_at,
"favorited_status" => favorited_status,
"in_reply_to_status_id" => liked_activity_id,
"external_url" => activity.data["id"],
"activity_type" => "like"
}
end
def render(
"activity.json",
%{activity: %{data: %{"type" => "Create", "object" => object_id}} = activity} = opts
) do
user = get_user(activity.data["actor"], opts)
object = Object.normalize(object_id)
created_at = object.data["published"] |> Utils.date_to_asctime()
like_count = object.data["like_count"] || 0
announcement_count = object.data["announcement_count"] || 0
favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
repeated = opts[:for] && opts[:for].ap_id in (object.data["announcements"] || [])
pinned = activity.id in user.info.pinned_activities
attentions =
[]
|> Utils.maybe_notify_to_recipients(activity)
|> Utils.maybe_notify_mentioned_recipients(activity)
|> Enum.map(fn ap_id -> get_user(ap_id, opts) end)
|> Enum.filter(& &1)
|> Enum.map(fn user -> UserView.render("show.json", %{user: user, for: opts[:for]}) end)
conversation_id = get_context_id(activity, opts)
tags = object.data["tag"] || []
possibly_sensitive = object.data["sensitive"] || Enum.member?(tags, "nsfw")
tags = if possibly_sensitive, do: Enum.uniq(["nsfw" | tags]), else: tags
{summary, content} = render_content(object.data)
html =
content
|> HTML.get_cached_scrubbed_html_for_activity(
User.html_filter_policy(opts[:for]),
activity,
"twitterapi:content"
)
|> Formatter.emojify(object.data["emoji"])
text =
if content do
content
|> String.replace(~r/<br\s?\/?>/, "\n")
|> HTML.get_cached_stripped_html_for_activity(activity, "twitterapi:content")
else
""
end
reply_parent = Activity.get_in_reply_to_activity(activity)
reply_user = reply_parent && User.get_cached_by_ap_id(reply_parent.actor)
summary = HTML.strip_tags(summary)
card =
StatusView.render(
"card.json",
Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
)
thread_muted? =
case activity.thread_muted? do
thread_muted? when is_boolean(thread_muted?) -> thread_muted?
nil -> CommonAPI.thread_muted?(user, activity)
end
%{
"id" => activity.id,
"uri" => object.data["id"],
"user" => UserView.render("show.json", %{user: user, for: opts[:for]}),
"statusnet_html" => html,
"text" => text,
"is_local" => activity.local,
"is_post_verb" => true,
"created_at" => created_at,
"in_reply_to_status_id" => reply_parent && reply_parent.id,
"in_reply_to_screen_name" => reply_user && reply_user.nickname,
"in_reply_to_profileurl" => User.profile_url(reply_user),
"in_reply_to_ostatus_uri" => reply_user && reply_user.ap_id,
"in_reply_to_user_id" => reply_user && reply_user.id,
"statusnet_conversation_id" => conversation_id,
"attachments" => (object.data["attachment"] || []) |> ObjectRepresenter.enum_to_list(opts),
"attentions" => attentions,
"fave_num" => like_count,
"repeat_num" => announcement_count,
"favorited" => !!favorited,
"repeated" => !!repeated,
"pinned" => pinned,
"external_url" => object.data["external_url"] || object.data["id"],
"tags" => tags,
"activity_type" => "post",
"possibly_sensitive" => possibly_sensitive,
"visibility" => Pleroma.Web.ActivityPub.Visibility.get_visibility(object),
"summary" => summary,
"summary_html" => summary |> Formatter.emojify(object.data["emoji"]),
"card" => card,
"muted" => thread_muted? || User.mutes?(opts[:for], user)
}
end
def render("activity.json", %{activity: unhandled_activity}) do
Logger.warn("#{__MODULE__} unhandled activity: #{inspect(unhandled_activity)}")
nil
end
def render_content(%{"type" => "Note"} = object) do
summary = object["summary"]
content =
if !!summary and summary != "" do
"<p>#{summary}</p>#{object["content"]}"
else
object["content"]
end
{summary, content}
end
def render_content(%{"type" => object_type} = object)
when object_type in ["Article", "Page", "Video"] do
summary = object["name"] || object["summary"]
content =
if !!summary and summary != "" and is_bitstring(object["url"]) do
"<p><a href=\"#{object["url"]}\">#{summary}</a></p>#{object["content"]}"
else
object["content"]
end
{summary, content}
end
def render_content(object) do
summary = object["summary"] || "Unhandled activity type: #{object["type"]}"
content = "<p>#{summary}</p>#{object["content"]}"
{summary, content}
end
end

View file

@ -1,71 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.NotificationView do
use Pleroma.Web, :view
alias Pleroma.Notification
alias Pleroma.User
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.TwitterAPI.ActivityView
alias Pleroma.Web.TwitterAPI.UserView
require Pleroma.Constants
defp get_user(ap_id, opts) do
cond do
user = opts[:users][ap_id] ->
user
String.ends_with?(ap_id, "/followers") ->
nil
ap_id == Pleroma.Constants.as_public() ->
nil
true ->
User.get_cached_by_ap_id(ap_id)
end
end
def render("notification.json", %{notifications: notifications, for: user}) do
render_many(
notifications,
Pleroma.Web.TwitterAPI.NotificationView,
"notification.json",
for: user
)
end
def render(
"notification.json",
%{
notification: %Notification{
id: id,
seen: seen,
activity: activity,
inserted_at: created_at
},
for: user
} = opts
) do
ntype =
case activity.data["type"] do
"Create" -> "mention"
"Like" -> "like"
"Announce" -> "repeat"
"Follow" -> "follow"
end
from = get_user(activity.data["actor"], opts)
%{
"id" => id,
"ntype" => ntype,
"notice" => ActivityView.render("activity.json", %{activity: activity, for: user}),
"from_profile" => UserView.render("show.json", %{user: from, for: user}),
"is_seen" => if(seen, do: 1, else: 0),
"created_at" => created_at |> Utils.format_naive_asctime()
}
end
end

View file

@ -1,191 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.UserView do
use Pleroma.Web, :view
alias Pleroma.Formatter
alias Pleroma.HTML
alias Pleroma.User
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MediaProxy
def render("show.json", %{user: user = %User{}} = assigns) do
render_one(user, Pleroma.Web.TwitterAPI.UserView, "user.json", assigns)
end
def render("index.json", %{users: users, for: user}) do
users
|> render_many(Pleroma.Web.TwitterAPI.UserView, "user.json", for: user)
|> Enum.filter(&Enum.any?/1)
end
def render("user.json", %{user: user = %User{}} = assigns) do
if User.visible_for?(user, assigns[:for]),
do: do_render("user.json", assigns),
else: %{}
end
def render("short.json", %{
user: %User{
nickname: nickname,
id: id,
ap_id: ap_id,
name: name
}
}) do
%{
"fullname" => name,
"id" => id,
"ostatus_uri" => ap_id,
"profile_url" => ap_id,
"screen_name" => nickname
}
end
defp do_render("user.json", %{user: user = %User{}} = assigns) do
for_user = assigns[:for]
image = User.avatar_url(user) |> MediaProxy.url()
{following, follows_you, statusnet_blocking} =
if for_user do
{
User.following?(for_user, user),
User.following?(user, for_user),
User.blocks?(for_user, user)
}
else
{false, false, false}
end
user_info = User.get_cached_user_info(user)
emoji =
(user.info.source_data["tag"] || [])
|> Enum.filter(fn %{"type" => t} -> t == "Emoji" end)
|> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} ->
{String.trim(name, ":"), url}
end)
emoji = Enum.dedup(emoji ++ user.info.emoji)
description_html =
(user.bio || "")
|> HTML.filter_tags(User.html_filter_policy(for_user))
|> Formatter.emojify(emoji)
fields =
user.info
|> User.Info.fields()
|> Enum.map(fn %{"name" => name, "value" => value} ->
%{
"name" => Pleroma.HTML.strip_tags(name),
"value" => Pleroma.HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
}
end)
data =
%{
"created_at" => user.inserted_at |> Utils.format_naive_asctime(),
"description" => HTML.strip_tags((user.bio || "") |> String.replace("<br>", "\n")),
"description_html" => description_html,
"favourites_count" => 0,
"followers_count" => user_info[:follower_count],
"following" => following,
"follows_you" => follows_you,
"statusnet_blocking" => statusnet_blocking,
"friends_count" => user_info[:following_count],
"id" => user.id,
"name" => user.name || user.nickname,
"name_html" =>
if(user.name,
do: HTML.strip_tags(user.name) |> Formatter.emojify(emoji),
else: user.nickname
),
"profile_image_url" => image,
"profile_image_url_https" => image,
"profile_image_url_profile_size" => image,
"profile_image_url_original" => image,
"screen_name" => user.nickname,
"statuses_count" => user_info[:note_count],
"statusnet_profile_url" => user.ap_id,
"cover_photo" => User.banner_url(user) |> MediaProxy.url(),
"background_image" => image_url(user.info.background) |> MediaProxy.url(),
"is_local" => user.local,
"locked" => user.info.locked,
"hide_followers" => user.info.hide_followers,
"hide_follows" => user.info.hide_follows,
"fields" => fields,
# Pleroma extension
"pleroma" =>
%{
"confirmation_pending" => user_info.confirmation_pending,
"tags" => user.tags,
"skip_thread_containment" => user.info.skip_thread_containment
}
|> maybe_with_activation_status(user, for_user)
|> with_notification_settings(user, for_user)
}
|> maybe_with_user_settings(user, for_user)
|> maybe_with_role(user, for_user)
if assigns[:token] do
Map.put(data, "token", token_string(assigns[:token]))
else
data
end
end
defp with_notification_settings(data, %User{id: user_id} = user, %User{id: user_id}) do
Map.put(data, "notification_settings", user.info.notification_settings)
end
defp with_notification_settings(data, _, _), do: data
defp maybe_with_activation_status(data, user, %User{info: %{is_admin: true}}) do
Map.put(data, "deactivated", user.info.deactivated)
end
defp maybe_with_activation_status(data, _, _), do: data
defp maybe_with_role(data, %User{id: id} = user, %User{id: id}) do
Map.merge(data, %{
"role" => role(user),
"show_role" => user.info.show_role,
"rights" => %{
"delete_others_notice" => !!user.info.is_moderator,
"admin" => !!user.info.is_admin
}
})
end
defp maybe_with_role(data, %User{info: %{show_role: true}} = user, _user) do
Map.merge(data, %{
"role" => role(user),
"rights" => %{
"delete_others_notice" => !!user.info.is_moderator,
"admin" => !!user.info.is_admin
}
})
end
defp maybe_with_role(data, _, _), do: data
defp maybe_with_user_settings(data, %User{info: info, id: id} = _user, %User{id: id}) do
data
|> Kernel.put_in(["default_scope"], info.default_scope)
|> Kernel.put_in(["no_rich_text"], info.no_rich_text)
end
defp maybe_with_user_settings(data, _, _), do: data
defp role(%User{info: %{:is_admin => true}}), do: "admin"
defp role(%User{info: %{:is_moderator => true}}), do: "moderator"
defp role(_), do: "member"
defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
defp image_url(_), do: nil
defp token_string(%Pleroma.Web.OAuth.Token{token: token_str}), do: token_str
defp token_string(token), do: token
end

View file

@ -1 +1 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1,user-scalable=no"><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/vendors~app.b2603a50868c68a1c192.css rel=stylesheet><link href=/static/css/app.db80066bde2c96ea6198.css rel=stylesheet></head><body style="display: none"><div id=app></div><script type=text/javascript src=/static/js/vendors~app.4b7be53256fba5c365c9.js></script><script type=text/javascript src=/static/js/app.670c36c0acc42fadb4fe.js></script></body></html>
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1,user-scalable=no"><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/vendors~app.b2603a50868c68a1c192.css rel=stylesheet><link href=/static/css/app.cb3673e4b661fd9526ea.css rel=stylesheet></head><body><noscript>To use Pleroma, please enable JavaScript.</noscript><div id=app></div><script type=text/javascript src=/static/js/vendors~app.4cedffe4993b111c7421.js></script><script type=text/javascript src=/static/js/app.8098503330c7dd14a415.js></script></body></html>

View file

@ -6,7 +6,6 @@
"logoMargin": ".1em",
"redirectRootNoLogin": "/main/all",
"redirectRootLogin": "/main/friends",
"chatDisabled": false,
"showInstanceSpecificPanel": false,
"collapseMessageWithSubject": false,
"scopeCopy": true,

View file

@ -0,0 +1 @@
{"version":3,"sources":["webpack:///./src/components/tab_switcher/tab_switcher.scss","webpack:///./src/hocs/with_load_more/with_load_more.scss","webpack:///./src/hocs/with_subscription/with_subscription.scss"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C;AClEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C;ACTA;AACA;AACA;AACA;AACA;AACA;AACA,C","file":"static/css/app.cb3673e4b661fd9526ea.css","sourcesContent":[".tab-switcher .contents .hidden {\n display: none;\n}\n.tab-switcher .tabs {\n display: -ms-flexbox;\n display: flex;\n position: relative;\n width: 100%;\n overflow-y: hidden;\n overflow-x: auto;\n padding-top: 5px;\n box-sizing: border-box;\n}\n.tab-switcher .tabs::after, .tab-switcher .tabs::before {\n display: block;\n content: \"\";\n -ms-flex: 1 1 auto;\n flex: 1 1 auto;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}\n.tab-switcher .tabs .tab-wrapper {\n height: 28px;\n position: relative;\n display: -ms-flexbox;\n display: flex;\n -ms-flex: 0 0 auto;\n flex: 0 0 auto;\n}\n.tab-switcher .tabs .tab-wrapper .tab {\n width: 100%;\n min-width: 1px;\n position: relative;\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n padding: 6px 1em;\n padding-bottom: 99px;\n margin-bottom: -93px;\n white-space: nowrap;\n}\n.tab-switcher .tabs .tab-wrapper .tab:not(.active) {\n z-index: 4;\n}\n.tab-switcher .tabs .tab-wrapper .tab:not(.active):hover {\n z-index: 6;\n}\n.tab-switcher .tabs .tab-wrapper .tab.active {\n background: transparent;\n z-index: 5;\n}\n.tab-switcher .tabs .tab-wrapper .tab img {\n max-height: 26px;\n vertical-align: top;\n margin-top: -5px;\n}\n.tab-switcher .tabs .tab-wrapper:not(.active)::after {\n content: \"\";\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n z-index: 7;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}",".with-load-more-footer {\n padding: 10px;\n text-align: center;\n border-top: 1px solid;\n border-top-color: #222;\n border-top-color: var(--border, #222);\n}\n.with-load-more-footer .error {\n font-size: 14px;\n}",".with-subscription-loading {\n padding: 10px;\n text-align: center;\n}\n.with-subscription-loading .error {\n font-size: 14px;\n}"],"sourceRoot":""}

View file

@ -1 +0,0 @@
{"version":3,"sources":["webpack:///./src/hocs/with_load_more/with_load_more.scss","webpack:///./src/components/tab_switcher/tab_switcher.scss","webpack:///./src/hocs/with_subscription/with_subscription.scss"],"names":[],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C;ACTA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,C;ACzDA;AACA;AACA;AACA;AACA;AACA;AACA,C","file":"static/css/app.db80066bde2c96ea6198.css","sourcesContent":[".with-load-more-footer {\n padding: 10px;\n text-align: center;\n border-top: 1px solid;\n border-top-color: #222;\n border-top-color: var(--border, #222);\n}\n.with-load-more-footer .error {\n font-size: 14px;\n}",".tab-switcher .contents .hidden {\n display: none;\n}\n.tab-switcher .tabs {\n display: flex;\n position: relative;\n width: 100%;\n overflow-y: hidden;\n overflow-x: auto;\n padding-top: 5px;\n box-sizing: border-box;\n}\n.tab-switcher .tabs::after, .tab-switcher .tabs::before {\n display: block;\n content: \"\";\n flex: 1 1 auto;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}\n.tab-switcher .tabs .tab-wrapper {\n height: 28px;\n position: relative;\n display: flex;\n flex: 0 0 auto;\n}\n.tab-switcher .tabs .tab-wrapper .tab {\n width: 100%;\n min-width: 1px;\n position: relative;\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n padding: 6px 1em;\n padding-bottom: 99px;\n margin-bottom: -93px;\n white-space: nowrap;\n}\n.tab-switcher .tabs .tab-wrapper .tab:not(.active) {\n z-index: 4;\n}\n.tab-switcher .tabs .tab-wrapper .tab:not(.active):hover {\n z-index: 6;\n}\n.tab-switcher .tabs .tab-wrapper .tab.active {\n background: transparent;\n z-index: 5;\n}\n.tab-switcher .tabs .tab-wrapper:not(.active)::after {\n content: \"\";\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n z-index: 7;\n border-bottom: 1px solid;\n border-bottom-color: #222;\n border-bottom-color: var(--border, #222);\n}",".with-subscription-loading {\n padding: 10px;\n text-align: center;\n}\n.with-subscription-loading .error {\n font-size: 14px;\n}"],"sourceRoot":""}

0
priv/static/static/font/LICENSE.txt Normal file → Executable file
View file

0
priv/static/static/font/README.txt Normal file → Executable file
View file

26
priv/static/static/font/config.json Normal file → Executable file
View file

@ -150,12 +150,6 @@
"code": 61669,
"src": "fontawesome"
},
{
"uid": "cd21cbfb28ad4d903cede582157f65dc",
"css": "bell",
"code": 59408,
"src": "fontawesome"
},
{
"uid": "ccc2329632396dc096bb638d4b46fb98",
"css": "mail-alt",
@ -277,6 +271,26 @@
"search": [
"ellipsis"
]
},
{
"uid": "0bef873af785ead27781fdf98b3ae740",
"css": "bell-ringing-o",
"code": 59408,
"src": "custom_icons",
"selected": true,
"svg": {
"path": "M497.8 0C468.3 0 444.4 23.9 444.4 53.3 444.4 61.1 446.1 68.3 448.9 75 301.7 96.7 213.3 213.3 213.3 320 213.3 588.3 117.8 712.8 35.6 782.2 35.6 821.1 67.8 853.3 106.7 853.3H355.6C355.6 931.7 419.4 995.6 497.8 995.6S640 931.7 640 853.3H888.9C927.8 853.3 960 821.1 960 782.2 877.8 712.8 782.2 588.3 782.2 320 782.2 213.3 693.9 96.7 546.7 75 549.4 68.3 551.1 61.1 551.1 53.3 551.1 23.9 527.2 0 497.8 0ZM189.4 44.8C108.4 118.6 70.5 215.1 71.1 320.2L142.2 319.8C141.7 231.2 170.4 158.3 237.3 97.4L189.4 44.8ZM806.2 44.8L758.3 97.4C825.2 158.3 853.9 231.2 853.3 319.8L924.4 320.2C925.1 215.1 887.2 118.6 806.2 44.8ZM408.9 844.4C413.9 844.4 417.8 848.3 417.8 853.3 417.8 897.2 453.9 933.3 497.8 933.3 502.8 933.3 506.7 937.2 506.7 942.2S502.8 951.1 497.8 951.1C443.9 951.1 400 907.2 400 853.3 400 848.3 403.9 844.4 408.9 844.4Z",
"width": 1000
},
"search": [
"bell-ringing-o"
]
},
{
"uid": "0b2b66e526028a6972d51a6f10281b4b",
"css": "zoom-in",
"code": 59420,
"src": "fontawesome"
}
]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

21
priv/static/static/font/demo.html Normal file → Executable file
View file

@ -229,11 +229,11 @@ body {
}
@font-face {
font-family: 'fontello';
src: url('./font/fontello.eot?14310629');
src: url('./font/fontello.eot?14310629#iefix') format('embedded-opentype'),
url('./font/fontello.woff?14310629') format('woff'),
url('./font/fontello.ttf?14310629') format('truetype'),
url('./font/fontello.svg?14310629#fontello') format('svg');
src: url('./font/fontello.eot?25455785');
src: url('./font/fontello.eot?25455785#iefix') format('embedded-opentype'),
url('./font/fontello.woff?25455785') format('woff'),
url('./font/fontello.ttf?25455785') format('truetype'),
url('./font/fontello.svg?25455785#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
@ -322,7 +322,7 @@ body {
<div class="the-icons span3" title="Code: 0xe80f"><i class="demo-icon icon-up-open">&#xe80f;</i> <span class="i-name">icon-up-open</span><span class="i-code">0xe80f</span></div>
</div>
<div class="row">
<div class="the-icons span3" title="Code: 0xe810"><i class="demo-icon icon-bell">&#xe810;</i> <span class="i-name">icon-bell</span><span class="i-code">0xe810</span></div>
<div class="the-icons span3" title="Code: 0xe810"><i class="demo-icon icon-bell-ringing-o">&#xe810;</i> <span class="i-name">icon-bell-ringing-o</span><span class="i-code">0xe810</span></div>
<div class="the-icons span3" title="Code: 0xe811"><i class="demo-icon icon-lock">&#xe811;</i> <span class="i-name">icon-lock</span><span class="i-code">0xe811</span></div>
<div class="the-icons span3" title="Code: 0xe812"><i class="demo-icon icon-globe">&#xe812;</i> <span class="i-name">icon-globe</span><span class="i-code">0xe812</span></div>
<div class="the-icons span3" title="Code: 0xe813"><i class="demo-icon icon-brush">&#xe813;</i> <span class="i-name">icon-brush</span><span class="i-code">0xe813</span></div>
@ -340,27 +340,30 @@ body {
<div class="the-icons span3" title="Code: 0xe81b"><i class="demo-icon icon-chart-bar">&#xe81b;</i> <span class="i-name">icon-chart-bar</span><span class="i-code">0xe81b</span></div>
</div>
<div class="row">
<div class="the-icons span3" title="Code: 0xe81c"><i class="demo-icon icon-zoom-in">&#xe81c;</i> <span class="i-name">icon-zoom-in</span><span class="i-code">0xe81c</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: 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: 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>
<div class="row">
<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 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: 0xf0f3"><i class="demo-icon icon-bell-alt">&#xf0f3;</i> <span class="i-name">icon-bell-alt</span><span class="i-code">0xf0f3</span></div>
</div>
<div class="row">
<div class="the-icons span3" title="Code: 0xf0f3"><i class="demo-icon icon-bell-alt">&#xf0f3;</i> <span class="i-name">icon-bell-alt</span><span class="i-code">0xf0f3</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 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: 0xf141"><i class="demo-icon icon-ellipsis">&#xf141;</i> <span class="i-name">icon-ellipsis</span><span class="i-code">0xf141</span></div>
</div>
<div class="row">
<div class="the-icons span3" title="Code: 0xf141"><i class="demo-icon icon-ellipsis">&#xf141;</i> <span class="i-name">icon-ellipsis</span><span class="i-code">0xf141</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: 0xf1e5"><i class="demo-icon icon-binoculars">&#xf1e5;</i> <span class="i-name">icon-binoculars</span><span class="i-code">0xf1e5</span></div>
</div>
<div class="row">
<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

@ -38,7 +38,7 @@
<glyph glyph-name="up-open" unicode="&#xe80f;" d="M939 114l-92-92q-11-10-26-10t-25 10l-296 297-296-297q-11-10-25-10t-25 10l-93 92q-11 11-11 26t11 25l414 414q11 10 25 10t25-10l414-414q11-11 11-25t-11-26z" horiz-adv-x="1000" />
<glyph glyph-name="bell" unicode="&#xe810;" d="M509-89q0 8-9 8-33 0-57 24t-23 57q0 9-9 9t-9-9q0-41 29-70t69-28q9 0 9 9z m-372 160h726q-149 168-149 465 0 28-13 58t-39 58-67 45-95 17-95-17-67-45-39-58-13-58q0-297-149-465z m827 0q0-29-21-50t-50-21h-250q0-59-42-101t-101-42-101 42-42 101h-250q-29 0-50 21t-21 50q28 24 51 49t47 67 42 89 27 115 11 145q0 84 66 157t171 89q-5 10-5 21 0 23 16 38t38 16 38-16 16-38q0-11-5-21 106-16 171-89t66-157q0-78 11-145t28-115 41-89 48-67 50-49z" horiz-adv-x="1000" />
<glyph glyph-name="bell-ringing-o" unicode="&#xe810;" d="M498 857c-30 0-54-24-54-53 0-8 2-15 5-22-147-22-236-138-236-245 0-268-95-393-177-462 0-39 32-71 71-71h249c0-79 63-143 142-143s142 64 142 143h249c39 0 71 32 71 71-82 69-178 194-178 462 0 107-88 223-235 245 2 7 4 14 4 22 0 29-24 53-53 53z m-309-45c-81-74-118-170-118-275l71 0c0 89 28 162 95 223l-48 52z m617 0l-48-52c67-61 96-134 95-223l71 0c1 105-37 201-118 275z m-397-799c5 0 9-4 9-9 0-44 36-80 80-80 5 0 9-4 9-9s-4-9-9-9c-54 0-98 44-98 98 0 5 4 9 9 9z" horiz-adv-x="1000" />
<glyph glyph-name="lock" unicode="&#xe811;" d="M179 428h285v108q0 59-42 101t-101 41-101-41-41-101v-108z m464-53v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h17v108q0 102 74 176t176 74 177-74 73-176v-108h18q23 0 38-15t16-38z" horiz-adv-x="642.9" />
@ -62,6 +62,8 @@
<glyph glyph-name="chart-bar" unicode="&#xe81b;" d="M357 357v-286h-143v286h143z m214 286v-572h-142v572h142z m572-643v-72h-1143v858h71v-786h1072z m-357 500v-429h-143v429h143z m214 214v-643h-143v643h143z" horiz-adv-x="1142.9" />
<glyph glyph-name="zoom-in" unicode="&#xe81c;" d="M571 411v-36q0-7-5-13t-12-5h-125v-125q0-7-6-13t-12-5h-36q-7 0-13 5t-5 13v125h-125q-7 0-12 5t-6 13v36q0 7 6 12t12 5h125v125q0 8 5 13t13 5h36q7 0 12-5t6-13v-125h125q7 0 12-5t5-12z m72-18q0 103-73 176t-177 74-177-74-73-176 73-177 177-73 177 73 73 177z m286-465q0-29-21-50t-51-21q-30 0-50 21l-191 191q-100-69-223-69-80 0-153 31t-125 84-84 125-31 153 31 152 84 126 125 84 153 31 153-31 125-84 84-126 31-152q0-123-69-223l191-191q21-21 21-51z" horiz-adv-x="928.6" />
<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" />

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1 @@
{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"Hashtag":"as:Hashtag"}],"id":"https://skippers-bin.com/notes/7x9tmrp97i","type":"Question","attributedTo":"https://skippers-bin.com/users/7v1w1r8ce6","summary":null,"content":"<p><a href=\"https://marchgenso.me/users/march\" class=\"mention\">@march@marchgenso.me</a><span> How are your notifications now?<br></span><a href=\"https://skippers-bin.com/notes/7x9tmrp97i\"><span>リモートで結果を表示</span></a></p>","_misskey_content":"@march@marchgenso.me How are your notifications now?\n[リモートで結果を表示](https://skippers-bin.com/notes/7x9tmrp97i)","published":"2019-09-05T05:35:32.541Z","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://skippers-bin.com/users/7v1w1r8ce6/followers","https://marchgenso.me/users/march"],"inReplyTo":null,"attachment":[],"sensitive":false,"tag":[{"type":"Mention","href":"https://marchgenso.me/users/march","name":"@march@marchgenso.me"}],"_misskey_fallback_content":"<p><a href=\"https://marchgenso.me/users/march\" class=\"mention\">@march@marchgenso.me</a><span> How are your notifications now?<br></span><a href=\"https://skippers-bin.com/notes/7x9tmrp97i\"><span>リモートで結果を表示</span></a><span><br>----------------------------------------<br>0: Working<br>1: Broken af<br>----------------------------------------<br>番号を返信して投票</span></p>","endTime":null,"oneOf":[{"type":"Note","name":"Working","replies":{"type":"Collection","totalItems":0}},{"type":"Note","name":"Broken af","replies":{"type":"Collection","totalItems":1}}]}

1
test/fixtures/tesla_mock/sjw.json vendored Normal file
View file

@ -0,0 +1 @@
{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"Hashtag":"as:Hashtag"}],"type":"Person","id":"https://skippers-bin.com/users/7v1w1r8ce6","inbox":"https://skippers-bin.com/users/7v1w1r8ce6/inbox","outbox":"https://skippers-bin.com/users/7v1w1r8ce6/outbox","followers":"https://skippers-bin.com/users/7v1w1r8ce6/followers","following":"https://skippers-bin.com/users/7v1w1r8ce6/following","featured":"https://skippers-bin.com/users/7v1w1r8ce6/collections/featured","sharedInbox":"https://skippers-bin.com/inbox","endpoints":{"sharedInbox":"https://skippers-bin.com/inbox"},"url":"https://skippers-bin.com/@sjw","preferredUsername":"sjw","name":"It's ya boi sjw :verified:","summary":"<p><span>Admin of skippers-bin.com and neckbeard.xyz<br>For the most part I'm just a normal user. I mostly post animu, lewds, may-mays, and shitposts.<br><br>Not an alt of </span><a href=\"https://skippers-bin.com/@sjw@neckbeard.xyz\" class=\"mention\">@sjw@neckbeard.xyz</a><span> but another main.<br><br>Email/XMPP: neckbeard@rape.lol<br>PGP: d016 b622 75ba bcbc 5b3a fced a7d9 4824 0eb3 9c4e</span></p>","icon":{"type":"Image","url":"https://skippers-bin.com/files/webpublic-21b17f5b-3a83-4f50-8d4f-eda92066aa26","sensitive":false},"image":{"type":"Image","url":"https://skippers-bin.com/files/webpublic-1cd7f961-421e-4c31-aa03-74fb82584308","sensitive":false},"tag":[{"id":"https://skippers-bin.com/emojis/verified","type":"Emoji","name":":verified:","updated":"2019-07-12T02:16:12.088Z","icon":{"type":"Image","mediaType":"image/png","url":"https://skippers-bin.com/files/webpublic-dd10b435-6dad-4602-938b-f69ec0a19f2c"}}],"manuallyApprovesFollowers":false,"publicKey":{"id":"https://skippers-bin.com/users/7v1w1r8ce6/publickey","type":"Key","owner":"https://skippers-bin.com/users/7v1w1r8ce6","publicKeyPem":"-----BEGIN RSA PUBLIC KEY-----\nMIICCgKCAgEAvmp71/A6Oxe1UW/44HK0juAJhrjv9gYhaoslaS9K1FB+BHfIjaE9\n9+W2SKRLnVNYNFSN4JJrSGhX5RUjAsf4tcdRDVcmHl7tp2sgOAZeZz5geULm2sJQ\nwElnGk34jT/xCfX+w/O+7DuX31sU7ZK0B2P7ulNGDQXhrzVO0RMx7HhNcsFcusno\n3kmPyyPT1l+PbM2UNWms599/3yicKtuOzMgzxNeXvuHYtAO19txyPiOeYckQOMmT\nwEVIxypgCgNQ0MNtPLPKQTwOgVbvnN7MN+h3esKeKDcPcGQySkbkjZPaVnA6xCQf\nj58c19wqdCfAS4Effo5/bxVmhLpe0l9HYpV7IMasv2LhFntmSmAxBQzhdz0oTYb1\naNqiyfZdClnzutOiKcrFppADo4rZH9Z1WlPHapahrKbF0GRPN8DjSUsoBxfY9wZs\ntlL056hT4o+EFHYrRGo7KP6X/6aQ9sSsmpE08aVpVuXdwuaoaDlW1KrJ0oOk4lZw\nUNXvjEaN3c+VQAw2CNvkAqLuwrjnw7MdcxEGodEXb6s8VvoSOaiDqT7cexSaZe0R\nliCe/3dqFXpX1UrgRiryI4yc1BrEJIGTanchmP2aUJ2R2pccFsREp23C3vMN3M5b\nHw7fvKbUQHyf6lhRoLCOSCz1xaPutaMJmpwLuJo4wPCHGg9QFBYsqxcCAwEAAQ==\n-----END RSA PUBLIC KEY-----\n"},"isCat":true}

View file

@ -12,7 +12,6 @@ defmodule Pleroma.NotificationTest do
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Streamer
alias Pleroma.Web.TwitterAPI.TwitterAPI
describe "create_notifications" do
test "notifies someone when they are directly addressed" do
@ -21,7 +20,7 @@ test "notifies someone when they are directly addressed" do
third_user = insert(:user)
{:ok, activity} =
TwitterAPI.create_status(user, %{
CommonAPI.post(user, %{
"status" => "hey @#{other_user.nickname} and @#{third_user.nickname}"
})
@ -39,7 +38,7 @@ test "it creates a notification for subscribed users" do
User.subscribe(subscriber, user)
{:ok, status} = TwitterAPI.create_status(user, %{"status" => "Akariiiin"})
{:ok, status} = CommonAPI.post(user, %{"status" => "Akariiiin"})
{:ok, [notification]} = Notification.create_notifications(status)
assert notification.user_id == subscriber.id
@ -184,47 +183,20 @@ test "it doesn't create a notification for user if he is the activity author" do
test "it doesn't create a notification for follow-unfollow-follow chains" do
user = insert(:user)
followed_user = insert(:user)
{:ok, _, _, activity} = TwitterAPI.follow(user, %{"user_id" => followed_user.id})
{:ok, _, _, activity} = CommonAPI.follow(user, followed_user)
Notification.create_notification(activity, followed_user)
TwitterAPI.unfollow(user, %{"user_id" => followed_user.id})
{:ok, _, _, activity_dupe} = TwitterAPI.follow(user, %{"user_id" => followed_user.id})
CommonAPI.unfollow(user, followed_user)
{:ok, _, _, activity_dupe} = CommonAPI.follow(user, followed_user)
refute Notification.create_notification(activity_dupe, followed_user)
end
test "it doesn't create a notification for like-unlike-like chains" do
user = insert(:user)
liked_user = insert(:user)
{:ok, status} = TwitterAPI.create_status(liked_user, %{"status" => "Yui is best yuru"})
{:ok, fav_status} = TwitterAPI.fav(user, status.id)
Notification.create_notification(fav_status, liked_user)
TwitterAPI.unfav(user, status.id)
{:ok, dupe} = TwitterAPI.fav(user, status.id)
refute Notification.create_notification(dupe, liked_user)
end
test "it doesn't create a notification for repeat-unrepeat-repeat chains" do
user = insert(:user)
retweeted_user = insert(:user)
{:ok, status} =
TwitterAPI.create_status(retweeted_user, %{
"status" => "Send dupe notifications to the shadow realm"
})
{:ok, retweeted_activity} = TwitterAPI.repeat(user, status.id)
Notification.create_notification(retweeted_activity, retweeted_user)
TwitterAPI.unrepeat(user, status.id)
{:ok, dupe} = TwitterAPI.repeat(user, status.id)
refute Notification.create_notification(dupe, retweeted_user)
end
test "it doesn't create duplicate notifications for follow+subscribed users" do
user = insert(:user)
subscriber = insert(:user)
{:ok, _, _, _} = TwitterAPI.follow(subscriber, %{"user_id" => user.id})
{:ok, _, _, _} = CommonAPI.follow(subscriber, user)
User.subscribe(subscriber, user)
{:ok, status} = TwitterAPI.create_status(user, %{"status" => "Akariiiin"})
{:ok, status} = CommonAPI.post(user, %{"status" => "Akariiiin"})
{:ok, [_notif]} = Notification.create_notifications(status)
end
@ -234,8 +206,7 @@ test "it doesn't create subscription notifications if the recipient cannot see t
User.subscribe(subscriber, user)
{:ok, status} =
TwitterAPI.create_status(user, %{"status" => "inwisible", "visibility" => "direct"})
{:ok, status} = CommonAPI.post(user, %{"status" => "inwisible", "visibility" => "direct"})
assert {:ok, []} == Notification.create_notifications(status)
end
@ -246,8 +217,7 @@ test "it gets a notification that belongs to the user" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
TwitterAPI.create_status(user, %{"status" => "hey @#{other_user.nickname}"})
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{other_user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
{:ok, notification} = Notification.get(other_user, notification.id)
@ -259,8 +229,7 @@ test "it returns error if the notification doesn't belong to the user" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
TwitterAPI.create_status(user, %{"status" => "hey @#{other_user.nickname}"})
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{other_user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
{:error, _notification} = Notification.get(user, notification.id)
@ -272,8 +241,7 @@ test "it dismisses a notification that belongs to the user" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
TwitterAPI.create_status(user, %{"status" => "hey @#{other_user.nickname}"})
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{other_user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
{:ok, notification} = Notification.dismiss(other_user, notification.id)
@ -285,8 +253,7 @@ test "it returns error if the notification doesn't belong to the user" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
TwitterAPI.create_status(user, %{"status" => "hey @#{other_user.nickname}"})
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{other_user.nickname}"})
{:ok, [notification]} = Notification.create_notifications(activity)
{:error, _notification} = Notification.dismiss(user, notification.id)
@ -300,14 +267,14 @@ test "it clears all notifications belonging to the user" do
third_user = insert(:user)
{:ok, activity} =
TwitterAPI.create_status(user, %{
CommonAPI.post(user, %{
"status" => "hey @#{other_user.nickname} and @#{third_user.nickname} !"
})
{:ok, _notifs} = Notification.create_notifications(activity)
{:ok, activity} =
TwitterAPI.create_status(user, %{
CommonAPI.post(user, %{
"status" => "hey again @#{other_user.nickname} and @#{third_user.nickname} !"
})
@ -325,12 +292,12 @@ test "it sets all notifications as read up to a specified notification ID" do
other_user = insert(:user)
{:ok, _activity} =
TwitterAPI.create_status(user, %{
CommonAPI.post(user, %{
"status" => "hey @#{other_user.nickname}!"
})
{:ok, _activity} =
TwitterAPI.create_status(user, %{
CommonAPI.post(user, %{
"status" => "hey again @#{other_user.nickname}!"
})
@ -340,7 +307,7 @@ test "it sets all notifications as read up to a specified notification ID" do
assert n2.id > n1.id
{:ok, _activity} =
TwitterAPI.create_status(user, %{
CommonAPI.post(user, %{
"status" => "hey yet again @#{other_user.nickname}!"
})
@ -677,7 +644,7 @@ test "it returns notifications for muted user without notifications" do
muted = insert(:user)
{:ok, user} = User.mute(user, muted, false)
{:ok, _activity} = TwitterAPI.create_status(muted, %{"status" => "hey @#{user.nickname}"})
{:ok, _activity} = CommonAPI.post(muted, %{"status" => "hey @#{user.nickname}"})
assert length(Notification.for_user(user)) == 1
end
@ -687,7 +654,7 @@ test "it doesn't return notifications for muted user with notifications" do
muted = insert(:user)
{:ok, user} = User.mute(user, muted)
{:ok, _activity} = TwitterAPI.create_status(muted, %{"status" => "hey @#{user.nickname}"})
{:ok, _activity} = CommonAPI.post(muted, %{"status" => "hey @#{user.nickname}"})
assert Notification.for_user(user) == []
end
@ -697,7 +664,7 @@ test "it doesn't return notifications for blocked user" do
blocked = insert(:user)
{:ok, user} = User.block(user, blocked)
{:ok, _activity} = TwitterAPI.create_status(blocked, %{"status" => "hey @#{user.nickname}"})
{:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"})
assert Notification.for_user(user) == []
end
@ -707,7 +674,7 @@ test "it doesn't return notificatitons for blocked domain" do
blocked = insert(:user, ap_id: "http://some-domain.com")
{:ok, user} = User.block_domain(user, "some-domain.com")
{:ok, _activity} = TwitterAPI.create_status(blocked, %{"status" => "hey @#{user.nickname}"})
{:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"})
assert Notification.for_user(user) == []
end
@ -716,8 +683,7 @@ test "it doesn't return notifications for muted thread" do
user = insert(:user)
another_user = insert(:user)
{:ok, activity} =
TwitterAPI.create_status(another_user, %{"status" => "hey @#{user.nickname}"})
{:ok, activity} = CommonAPI.post(another_user, %{"status" => "hey @#{user.nickname}"})
{:ok, _} = Pleroma.ThreadMute.add_mute(user.id, activity.data["context"])
assert Notification.for_user(user) == []
@ -728,7 +694,7 @@ test "it returns notifications for muted user with notifications and with_muted
muted = insert(:user)
{:ok, user} = User.mute(user, muted)
{:ok, _activity} = TwitterAPI.create_status(muted, %{"status" => "hey @#{user.nickname}"})
{:ok, _activity} = CommonAPI.post(muted, %{"status" => "hey @#{user.nickname}"})
assert length(Notification.for_user(user, %{with_muted: true})) == 1
end
@ -738,7 +704,7 @@ test "it returns notifications for blocked user and with_muted parameter" do
blocked = insert(:user)
{:ok, user} = User.block(user, blocked)
{:ok, _activity} = TwitterAPI.create_status(blocked, %{"status" => "hey @#{user.nickname}"})
{:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"})
assert length(Notification.for_user(user, %{with_muted: true})) == 1
end
@ -748,7 +714,7 @@ test "it returns notificatitons for blocked domain and with_muted parameter" do
blocked = insert(:user, ap_id: "http://some-domain.com")
{:ok, user} = User.block_domain(user, "some-domain.com")
{:ok, _activity} = TwitterAPI.create_status(blocked, %{"status" => "hey @#{user.nickname}"})
{:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"})
assert length(Notification.for_user(user, %{with_muted: true})) == 1
end
@ -757,8 +723,7 @@ test "it returns notifications for muted thread with_muted parameter" do
user = insert(:user)
another_user = insert(:user)
{:ok, activity} =
TwitterAPI.create_status(another_user, %{"status" => "hey @#{user.nickname}"})
{:ok, activity} = CommonAPI.post(another_user, %{"status" => "hey @#{user.nickname}"})
{:ok, _} = Pleroma.ThreadMute.add_mute(user.id, activity.data["context"])
assert length(Notification.for_user(user, %{with_muted: true})) == 1

View file

@ -53,9 +53,12 @@ test "ensures cache is cleared for the object" do
assert object == cached_object
Cachex.put(:web_resp_cache, URI.parse(object.data["id"]).path, "cofe")
Object.delete(cached_object)
{:ok, nil} = Cachex.get(:object_cache, "object:#{object.data["id"]}")
{:ok, nil} = Cachex.get(:web_resp_cache, URI.parse(object.data["id"]).path)
cached_object = Object.get_cached_by_ap_id(object.data["id"])

186
test/plugs/cache_test.exs Normal file
View file

@ -0,0 +1,186 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.CacheTest do
use ExUnit.Case, async: true
use Plug.Test
alias Pleroma.Plugs.Cache
@miss_resp {200,
[
{"cache-control", "max-age=0, private, must-revalidate"},
{"content-type", "cofe/hot; charset=utf-8"},
{"x-cache", "MISS from Pleroma"}
], "cofe"}
@hit_resp {200,
[
{"cache-control", "max-age=0, private, must-revalidate"},
{"content-type", "cofe/hot; charset=utf-8"},
{"x-cache", "HIT from Pleroma"}
], "cofe"}
@ttl 5
setup do
Cachex.clear(:web_resp_cache)
:ok
end
test "caches a response" do
assert @miss_resp ==
conn(:get, "/")
|> Cache.call(%{query_params: false, ttl: nil})
|> put_resp_content_type("cofe/hot")
|> send_resp(:ok, "cofe")
|> sent_resp()
assert_raise(Plug.Conn.AlreadySentError, fn ->
conn(:get, "/")
|> Cache.call(%{query_params: false, ttl: nil})
|> put_resp_content_type("cofe/hot")
|> send_resp(:ok, "cofe")
|> sent_resp()
end)
assert @hit_resp ==
conn(:get, "/")
|> Cache.call(%{query_params: false, ttl: nil})
|> sent_resp()
end
test "ttl is set" do
assert @miss_resp ==
conn(:get, "/")
|> Cache.call(%{query_params: false, ttl: @ttl})
|> put_resp_content_type("cofe/hot")
|> send_resp(:ok, "cofe")
|> sent_resp()
assert @hit_resp ==
conn(:get, "/")
|> Cache.call(%{query_params: false, ttl: @ttl})
|> sent_resp()
:timer.sleep(@ttl + 1)
assert @miss_resp ==
conn(:get, "/")
|> Cache.call(%{query_params: false, ttl: @ttl})
|> put_resp_content_type("cofe/hot")
|> send_resp(:ok, "cofe")
|> sent_resp()
end
test "set ttl via conn.assigns" do
assert @miss_resp ==
conn(:get, "/")
|> Cache.call(%{query_params: false, ttl: nil})
|> put_resp_content_type("cofe/hot")
|> assign(:cache_ttl, @ttl)
|> send_resp(:ok, "cofe")
|> sent_resp()
assert @hit_resp ==
conn(:get, "/")
|> Cache.call(%{query_params: false, ttl: nil})
|> sent_resp()
:timer.sleep(@ttl + 1)
assert @miss_resp ==
conn(:get, "/")
|> Cache.call(%{query_params: false, ttl: nil})
|> put_resp_content_type("cofe/hot")
|> send_resp(:ok, "cofe")
|> sent_resp()
end
test "ignore query string when `query_params` is false" do
assert @miss_resp ==
conn(:get, "/?cofe")
|> Cache.call(%{query_params: false, ttl: nil})
|> put_resp_content_type("cofe/hot")
|> send_resp(:ok, "cofe")
|> sent_resp()
assert @hit_resp ==
conn(:get, "/?cofefe")
|> Cache.call(%{query_params: false, ttl: nil})
|> sent_resp()
end
test "take query string into account when `query_params` is true" do
assert @miss_resp ==
conn(:get, "/?cofe")
|> Cache.call(%{query_params: true, ttl: nil})
|> put_resp_content_type("cofe/hot")
|> send_resp(:ok, "cofe")
|> sent_resp()
assert @miss_resp ==
conn(:get, "/?cofefe")
|> Cache.call(%{query_params: true, ttl: nil})
|> put_resp_content_type("cofe/hot")
|> send_resp(:ok, "cofe")
|> sent_resp()
end
test "take specific query params into account when `query_params` is list" do
assert @miss_resp ==
conn(:get, "/?a=1&b=2&c=3&foo=bar")
|> fetch_query_params()
|> Cache.call(%{query_params: ["a", "b", "c"], ttl: nil})
|> put_resp_content_type("cofe/hot")
|> send_resp(:ok, "cofe")
|> sent_resp()
assert @hit_resp ==
conn(:get, "/?bar=foo&c=3&b=2&a=1")
|> fetch_query_params()
|> Cache.call(%{query_params: ["a", "b", "c"], ttl: nil})
|> sent_resp()
assert @miss_resp ==
conn(:get, "/?bar=foo&c=3&b=2&a=2")
|> fetch_query_params()
|> Cache.call(%{query_params: ["a", "b", "c"], ttl: nil})
|> put_resp_content_type("cofe/hot")
|> send_resp(:ok, "cofe")
|> sent_resp()
end
test "ignore not GET requests" do
expected =
{200,
[
{"cache-control", "max-age=0, private, must-revalidate"},
{"content-type", "cofe/hot; charset=utf-8"}
], "cofe"}
assert expected ==
conn(:post, "/")
|> Cache.call(%{query_params: true, ttl: nil})
|> put_resp_content_type("cofe/hot")
|> send_resp(:ok, "cofe")
|> sent_resp()
end
test "ignore non-successful responses" do
expected =
{418,
[
{"cache-control", "max-age=0, private, must-revalidate"},
{"content-type", "tea/iced; charset=utf-8"}
], "🥤"}
assert expected ==
conn(:get, "/cofe")
|> Cache.call(%{query_params: true, ttl: nil})
|> put_resp_content_type("tea/iced")
|> send_resp(:im_a_teapot, "🥤")
|> sent_resp()
end
end

View file

@ -992,6 +992,18 @@ def get("http://example.com/rel_me/null", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/rel_me_null.html")}}
end
def get("https://skippers-bin.com/notes/7x9tmrp97i", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/tesla_mock/misskey_poll_no_end_date.json")
}}
end
def get("https://skippers-bin.com/users/7v1w1r8ce6", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/sjw.json")}}
end
def get(url, query, body, headers) do
{:error,
"Mock response not implemented for GET #{inspect(url)}, #{query}, #{inspect(body)}, #{

View file

@ -69,8 +69,8 @@ test "returns all pending follow requests" do
locked = insert(:user, %{info: %{locked: true}})
follower = insert(:user)
Pleroma.Web.TwitterAPI.TwitterAPI.follow(follower, %{"user_id" => unlocked.id})
Pleroma.Web.TwitterAPI.TwitterAPI.follow(follower, %{"user_id" => locked.id})
CommonAPI.follow(follower, unlocked)
CommonAPI.follow(follower, locked)
assert {:ok, []} = User.get_follow_requests(unlocked)
assert {:ok, [activity]} = User.get_follow_requests(locked)
@ -83,9 +83,9 @@ test "doesn't return already accepted or duplicate follow requests" do
pending_follower = insert(:user)
accepted_follower = insert(:user)
Pleroma.Web.TwitterAPI.TwitterAPI.follow(pending_follower, %{"user_id" => locked.id})
Pleroma.Web.TwitterAPI.TwitterAPI.follow(pending_follower, %{"user_id" => locked.id})
Pleroma.Web.TwitterAPI.TwitterAPI.follow(accepted_follower, %{"user_id" => locked.id})
CommonAPI.follow(pending_follower, locked)
CommonAPI.follow(pending_follower, locked)
CommonAPI.follow(accepted_follower, locked)
User.follow(accepted_follower, locked)
assert {:ok, [activity]} = User.get_follow_requests(locked)
@ -1279,11 +1279,9 @@ test "follower count is updated when a follower is blocked" do
{:ok, _follower2} = User.follow(follower2, user)
{:ok, _follower3} = User.follow(follower3, user)
{:ok, _} = User.block(user, follower)
{:ok, user} = User.block(user, follower)
user_show = Pleroma.Web.TwitterAPI.UserView.render("show.json", %{user: user})
assert Map.get(user_show, "followers_count") == 2
assert User.user_info(user).follower_count == 2
end
describe "list_inactive_users_query/1" do
@ -1327,7 +1325,7 @@ test "Only includes users who has no recent activity" do
to = Enum.random(users -- [user])
{:ok, _} =
Pleroma.Web.TwitterAPI.TwitterAPI.create_status(user, %{
CommonAPI.post(user, %{
"status" => "hey @#{to.nickname}"
})
end)
@ -1359,12 +1357,12 @@ test "Only includes users with no read notifications" do
Enum.each(recipients, fn to ->
{:ok, _} =
Pleroma.Web.TwitterAPI.TwitterAPI.create_status(sender, %{
CommonAPI.post(sender, %{
"status" => "hey @#{to.nickname}"
})
{:ok, _} =
Pleroma.Web.TwitterAPI.TwitterAPI.create_status(sender, %{
CommonAPI.post(sender, %{
"status" => "hey again @#{to.nickname}"
})
end)

View file

@ -175,6 +175,49 @@ test "it returns 404 for tombstone objects", %{conn: conn} do
assert json_response(conn, 404)
end
test "it caches a response", %{conn: conn} do
note = insert(:note)
uuid = String.split(note.data["id"], "/") |> List.last()
conn1 =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert json_response(conn1, :ok)
assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
conn2 =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert json_response(conn1, :ok) == json_response(conn2, :ok)
assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
end
test "cached purged after object deletion", %{conn: conn} do
note = insert(:note)
uuid = String.split(note.data["id"], "/") |> List.last()
conn1 =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert json_response(conn1, :ok)
assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
Object.delete(note)
conn2 =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/objects/#{uuid}")
assert "Not found" == json_response(conn2, :not_found)
end
end
describe "/object/:uuid/likes" do
@ -264,6 +307,51 @@ test "it returns 404 for non-public activities", %{conn: conn} do
assert json_response(conn, 404)
end
test "it caches a response", %{conn: conn} do
activity = insert(:note_activity)
uuid = String.split(activity.data["id"], "/") |> List.last()
conn1 =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/activities/#{uuid}")
assert json_response(conn1, :ok)
assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
conn2 =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/activities/#{uuid}")
assert json_response(conn1, :ok) == json_response(conn2, :ok)
assert Enum.any?(conn2.resp_headers, &(&1 == {"x-cache", "HIT from Pleroma"}))
end
test "cached purged after activity deletion", %{conn: conn} do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "cofe"})
uuid = String.split(activity.data["id"], "/") |> List.last()
conn1 =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/activities/#{uuid}")
assert json_response(conn1, :ok)
assert Enum.any?(conn1.resp_headers, &(&1 == {"x-cache", "MISS from Pleroma"}))
Activity.delete_by_ap_id(activity.object.data["id"])
conn2 =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/activities/#{uuid}")
assert "Not found" == json_response(conn2, :not_found)
end
end
describe "/inbox" do
@ -365,6 +453,17 @@ test "it rejects reads from other users", %{conn: conn} do
assert json_response(conn, 403)
end
test "it doesn't crash without an authenticated user", %{conn: conn} do
user = insert(:user)
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get("/users/#{user.nickname}/inbox")
assert json_response(conn, 403)
end
test "it returns a note activity in a collection", %{conn: conn} do
note_activity = insert(:direct_note_activity)
note_object = Object.normalize(note_activity)

View file

@ -21,7 +21,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.OStatus
alias Pleroma.Web.Push
alias Pleroma.Web.TwitterAPI.TwitterAPI
import Pleroma.Factory
import ExUnit.CaptureLog
import Tesla.Mock
@ -1484,12 +1483,9 @@ test "gets an users media", %{conn: conn} do
filename: "an_image.jpg"
}
media =
TwitterAPI.upload(file, user, "json")
|> Jason.decode!()
{:ok, %{id: media_id}} = ActivityPub.upload(file, actor: user.ap_id)
{:ok, image_post} =
CommonAPI.post(user, %{"status" => "cofe", "media_ids" => [media["media_id"]]})
{:ok, image_post} = CommonAPI.post(user, %{"status" => "cofe", "media_ids" => [media_id]})
conn =
conn
@ -1675,32 +1671,85 @@ test "/api/v1/follow_requests/:id/reject works" do
end
end
test "account fetching", %{conn: conn} do
user = insert(:user)
describe "account fetching" do
test "works by id" do
user = insert(:user)
conn =
conn
|> get("/api/v1/accounts/#{user.id}")
conn =
build_conn()
|> get("/api/v1/accounts/#{user.id}")
assert %{"id" => id} = json_response(conn, 200)
assert id == to_string(user.id)
assert %{"id" => id} = json_response(conn, 200)
assert id == to_string(user.id)
conn =
build_conn()
|> get("/api/v1/accounts/-1")
conn =
build_conn()
|> get("/api/v1/accounts/-1")
assert %{"error" => "Can't find user"} = json_response(conn, 404)
end
assert %{"error" => "Can't find user"} = json_response(conn, 404)
end
test "account fetching also works nickname", %{conn: conn} do
user = insert(:user)
test "works by nickname" do
user = insert(:user)
conn =
conn
|> get("/api/v1/accounts/#{user.nickname}")
conn =
build_conn()
|> get("/api/v1/accounts/#{user.nickname}")
assert %{"id" => id} = json_response(conn, 200)
assert id == user.id
assert %{"id" => id} = json_response(conn, 200)
assert id == user.id
end
test "works by nickname for remote users" do
limit_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
Pleroma.Config.put([:instance, :limit_to_local_content], false)
user = insert(:user, nickname: "user@example.com", local: false)
conn =
build_conn()
|> get("/api/v1/accounts/#{user.nickname}")
Pleroma.Config.put([:instance, :limit_to_local_content], limit_to_local)
assert %{"id" => id} = json_response(conn, 200)
assert id == user.id
end
test "respects limit_to_local_content == :all for remote user nicknames" do
limit_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
Pleroma.Config.put([:instance, :limit_to_local_content], :all)
user = insert(:user, nickname: "user@example.com", local: false)
conn =
build_conn()
|> get("/api/v1/accounts/#{user.nickname}")
Pleroma.Config.put([:instance, :limit_to_local_content], limit_to_local)
assert json_response(conn, 404)
end
test "respects limit_to_local_content == :unauthenticated for remote user nicknames" do
limit_to_local = Pleroma.Config.get([:instance, :limit_to_local_content])
Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
user = insert(:user, nickname: "user@example.com", local: false)
reading_user = insert(:user)
conn =
build_conn()
|> get("/api/v1/accounts/#{user.nickname}")
assert json_response(conn, 404)
conn =
build_conn()
|> assign(:user, reading_user)
|> get("/api/v1/accounts/#{user.nickname}")
Pleroma.Config.put([:instance, :limit_to_local_content], limit_to_local)
assert %{"id" => id} = json_response(conn, 200)
assert id == user.id
end
end
test "mascot upload", %{conn: conn} do

View file

@ -8,8 +8,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPITest do
alias Pleroma.Notification
alias Pleroma.ScheduledActivity
alias Pleroma.User
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.MastodonAPI
alias Pleroma.Web.TwitterAPI.TwitterAPI
import Pleroma.Factory
@ -75,8 +75,9 @@ test "returns notifications for user" do
User.subscribe(subscriber, user)
{:ok, status} = TwitterAPI.create_status(user, %{"status" => "Akariiiin"})
{:ok, status1} = TwitterAPI.create_status(user, %{"status" => "Magi"})
{:ok, status} = CommonAPI.post(user, %{"status" => "Akariiiin"})
{:ok, status1} = CommonAPI.post(user, %{"status" => "Magi"})
{:ok, [notification]} = Notification.create_notifications(status)
{:ok, [notification1]} = Notification.create_notifications(status1)
res = MastodonAPI.get_notifications(subscriber)

View file

@ -551,6 +551,14 @@ test "detects vote status" do
assert Enum.at(result[:options], 1)[:votes_count] == 1
assert Enum.at(result[:options], 2)[:votes_count] == 1
end
test "does not crash on polls with no end date" do
object = Object.normalize("https://skippers-bin.com/notes/7x9tmrp97i")
result = StatusView.render("poll.json", %{object: object})
assert result[:expires_at] == nil
assert result[:expired] == false
end
end
test "embeds a relationship in the account" do

View file

@ -1,60 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.Representers.ObjectReprenterTest do
use Pleroma.DataCase
alias Pleroma.Object
alias Pleroma.Web.TwitterAPI.Representers.ObjectRepresenter
test "represent an image attachment" do
object = %Object{
id: 5,
data: %{
"type" => "Image",
"url" => [
%{
"mediaType" => "sometype",
"href" => "someurl"
}
],
"uuid" => 6
}
}
expected_object = %{
id: 6,
url: "someurl",
mimetype: "sometype",
oembed: false,
description: nil
}
assert expected_object == ObjectRepresenter.to_map(object)
end
test "represents mastodon-style attachments" do
object = %Object{
id: nil,
data: %{
"mediaType" => "image/png",
"name" => "blabla",
"type" => "Document",
"url" =>
"http://mastodon.example.org/system/media_attachments/files/000/000/001/original/8619f31c6edec470.png"
}
}
expected_object = %{
url:
"http://mastodon.example.org/system/media_attachments/files/000/000/001/original/8619f31c6edec470.png",
mimetype: "image/png",
oembed: false,
id: nil,
description: "blabla"
}
assert expected_object == ObjectRepresenter.to_map(object)
end
end

File diff suppressed because it is too large Load diff

View file

@ -4,270 +4,17 @@
defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.TwitterAPI.ActivityView
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.TwitterAPI.TwitterAPI
alias Pleroma.Web.TwitterAPI.UserView
import Pleroma.Factory
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
test "create a status" do
user = insert(:user)
mentioned_user = insert(:user, %{nickname: "shp", ap_id: "shp"})
object_data = %{
"type" => "Image",
"url" => [
%{
"type" => "Link",
"mediaType" => "image/jpg",
"href" => "http://example.org/image.jpg"
}
],
"uuid" => 1
}
object = Repo.insert!(%Object{data: object_data})
input = %{
"status" =>
"Hello again, @shp.<script></script>\nThis is on another :firefox: line. #2hu #epic #phantasmagoric",
"media_ids" => [object.id]
}
{:ok, activity = %Activity{}} = TwitterAPI.create_status(user, input)
object = Object.normalize(activity)
expected_text =
"Hello again, <span class='h-card'><a data-user='#{mentioned_user.id}' class='u-url mention' href='shp'>@<span>shp</span></a></span>.&lt;script&gt;&lt;/script&gt;<br>This is on another :firefox: line. <a class='hashtag' data-tag='2hu' href='http://localhost:4001/tag/2hu' rel='tag'>#2hu</a> <a class='hashtag' data-tag='epic' href='http://localhost:4001/tag/epic' rel='tag'>#epic</a> <a class='hashtag' data-tag='phantasmagoric' href='http://localhost:4001/tag/phantasmagoric' rel='tag'>#phantasmagoric</a><br><a href=\"http://example.org/image.jpg\" class='attachment'>image.jpg</a>"
assert get_in(object.data, ["content"]) == expected_text
assert get_in(object.data, ["type"]) == "Note"
assert get_in(object.data, ["actor"]) == user.ap_id
assert get_in(activity.data, ["actor"]) == user.ap_id
assert Enum.member?(get_in(activity.data, ["cc"]), User.ap_followers(user))
assert Enum.member?(
get_in(activity.data, ["to"]),
"https://www.w3.org/ns/activitystreams#Public"
)
assert Enum.member?(get_in(activity.data, ["to"]), "shp")
assert activity.local == true
assert %{"firefox" => "http://localhost:4001/emoji/Firefox.gif"} = object.data["emoji"]
# hashtags
assert object.data["tag"] == ["2hu", "epic", "phantasmagoric"]
# Add a context
assert is_binary(get_in(activity.data, ["context"]))
assert is_binary(get_in(object.data, ["context"]))
assert is_list(object.data["attachment"])
assert activity.data["object"] == object.data["id"]
user = User.get_cached_by_ap_id(user.ap_id)
assert user.info.note_count == 1
end
test "create a status that is a reply" do
user = insert(:user)
input = %{
"status" => "Hello again."
}
{:ok, activity = %Activity{}} = TwitterAPI.create_status(user, input)
object = Object.normalize(activity)
input = %{
"status" => "Here's your (you).",
"in_reply_to_status_id" => activity.id
}
{:ok, reply = %Activity{}} = TwitterAPI.create_status(user, input)
reply_object = Object.normalize(reply)
assert get_in(reply.data, ["context"]) == get_in(activity.data, ["context"])
assert get_in(reply_object.data, ["context"]) == get_in(object.data, ["context"])
assert get_in(reply_object.data, ["inReplyTo"]) == get_in(activity.data, ["object"])
assert Activity.get_in_reply_to_activity(reply).id == activity.id
end
test "Follow another user using user_id" do
user = insert(:user)
followed = insert(:user)
{:ok, user, followed, _activity} = TwitterAPI.follow(user, %{"user_id" => followed.id})
assert User.ap_followers(followed) in user.following
{:ok, _, _, _} = TwitterAPI.follow(user, %{"user_id" => followed.id})
end
test "Follow another user using screen_name" do
user = insert(:user)
followed = insert(:user)
{:ok, user, followed, _activity} =
TwitterAPI.follow(user, %{"screen_name" => followed.nickname})
assert User.ap_followers(followed) in user.following
followed = User.get_cached_by_ap_id(followed.ap_id)
assert followed.info.follower_count == 1
{:ok, _, _, _} = TwitterAPI.follow(user, %{"screen_name" => followed.nickname})
end
test "Unfollow another user using user_id" do
unfollowed = insert(:user)
user = insert(:user, %{following: [User.ap_followers(unfollowed)]})
ActivityPub.follow(user, unfollowed)
{:ok, user, unfollowed} = TwitterAPI.unfollow(user, %{"user_id" => unfollowed.id})
assert user.following == []
{:error, msg} = TwitterAPI.unfollow(user, %{"user_id" => unfollowed.id})
assert msg == "Not subscribed!"
end
test "Unfollow another user using screen_name" do
unfollowed = insert(:user)
user = insert(:user, %{following: [User.ap_followers(unfollowed)]})
ActivityPub.follow(user, unfollowed)
{:ok, user, unfollowed} = TwitterAPI.unfollow(user, %{"screen_name" => unfollowed.nickname})
assert user.following == []
{:error, msg} = TwitterAPI.unfollow(user, %{"screen_name" => unfollowed.nickname})
assert msg == "Not subscribed!"
end
test "Block another user using user_id" do
user = insert(:user)
blocked = insert(:user)
{:ok, user, blocked} = TwitterAPI.block(user, %{"user_id" => blocked.id})
assert User.blocks?(user, blocked)
end
test "Block another user using screen_name" do
user = insert(:user)
blocked = insert(:user)
{:ok, user, blocked} = TwitterAPI.block(user, %{"screen_name" => blocked.nickname})
assert User.blocks?(user, blocked)
end
test "Unblock another user using user_id" do
unblocked = insert(:user)
user = insert(:user)
{:ok, user, _unblocked} = TwitterAPI.block(user, %{"user_id" => unblocked.id})
{:ok, user, _unblocked} = TwitterAPI.unblock(user, %{"user_id" => unblocked.id})
assert user.info.blocks == []
end
test "Unblock another user using screen_name" do
unblocked = insert(:user)
user = insert(:user)
{:ok, user, _unblocked} = TwitterAPI.block(user, %{"screen_name" => unblocked.nickname})
{:ok, user, _unblocked} = TwitterAPI.unblock(user, %{"screen_name" => unblocked.nickname})
assert user.info.blocks == []
end
test "upload a file" do
user = insert(:user)
file = %Plug.Upload{
content_type: "image/jpg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
response = TwitterAPI.upload(file, user)
assert is_binary(response)
end
test "it favorites a status, returns the updated activity" do
user = insert(:user)
other_user = insert(:user)
note_activity = insert(:note_activity)
{:ok, status} = TwitterAPI.fav(user, note_activity.id)
updated_activity = Activity.get_by_ap_id(note_activity.data["id"])
assert ActivityView.render("activity.json", %{activity: updated_activity})["fave_num"] == 1
object = Object.normalize(note_activity)
assert object.data["like_count"] == 1
assert status == updated_activity
{:ok, _status} = TwitterAPI.fav(other_user, note_activity.id)
object = Object.normalize(note_activity)
assert object.data["like_count"] == 2
updated_activity = Activity.get_by_ap_id(note_activity.data["id"])
assert ActivityView.render("activity.json", %{activity: updated_activity})["fave_num"] == 2
end
test "it unfavorites a status, returns the updated activity" do
user = insert(:user)
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
{:ok, _like_activity, _object} = ActivityPub.like(user, object)
updated_activity = Activity.get_by_ap_id(note_activity.data["id"])
assert ActivityView.render("activity.json", activity: updated_activity)["fave_num"] == 1
{:ok, activity} = TwitterAPI.unfav(user, note_activity.id)
assert ActivityView.render("activity.json", activity: activity)["fave_num"] == 0
end
test "it retweets a status and returns the retweet" do
user = insert(:user)
note_activity = insert(:note_activity)
{:ok, status} = TwitterAPI.repeat(user, note_activity.id)
updated_activity = Activity.get_by_ap_id(note_activity.data["id"])
assert status == updated_activity
end
test "it unretweets an already retweeted status" do
user = insert(:user)
note_activity = insert(:note_activity)
{:ok, _status} = TwitterAPI.repeat(user, note_activity.id)
{:ok, status} = TwitterAPI.unrepeat(user, note_activity.id)
updated_activity = Activity.get_by_ap_id(note_activity.data["id"])
assert status == updated_activity
end
test "it registers a new user and returns the user." do
data = %{
"nickname" => "lain",
@ -281,8 +28,8 @@ test "it registers a new user and returns the user." do
fetched_user = User.get_cached_by_nickname("lain")
assert UserView.render("show.json", %{user: user}) ==
UserView.render("show.json", %{user: fetched_user})
assert AccountView.render("account.json", %{user: user}) ==
AccountView.render("account.json", %{user: fetched_user})
end
test "it registers a new user with empty string in bio and returns the user." do
@ -299,8 +46,8 @@ test "it registers a new user with empty string in bio and returns the user." do
fetched_user = User.get_cached_by_nickname("lain")
assert UserView.render("show.json", %{user: user}) ==
UserView.render("show.json", %{user: fetched_user})
assert AccountView.render("account.json", %{user: user}) ==
AccountView.render("account.json", %{user: fetched_user})
end
test "it sends confirmation email if :account_activation_required is specified in instance config" do
@ -397,8 +144,8 @@ test "returns user on success" do
assert invite.used == true
assert UserView.render("show.json", %{user: user}) ==
UserView.render("show.json", %{user: fetched_user})
assert AccountView.render("account.json", %{user: user}) ==
AccountView.render("account.json", %{user: fetched_user})
end
test "returns error on invalid token" do
@ -462,8 +209,8 @@ test "returns error on expired token" do
{:ok, user} = TwitterAPI.register_user(data)
fetched_user = User.get_cached_by_nickname("vinny")
assert UserView.render("show.json", %{user: user}) ==
UserView.render("show.json", %{user: fetched_user})
assert AccountView.render("account.json", %{user: user}) ==
AccountView.render("account.json", %{user: fetched_user})
end
{:ok, data: data, check_fn: check_fn}
@ -537,8 +284,8 @@ test "returns user on success, after him registration fails" do
assert invite.used == true
assert UserView.render("show.json", %{user: user}) ==
UserView.render("show.json", %{user: fetched_user})
assert AccountView.render("account.json", %{user: user}) ==
AccountView.render("account.json", %{user: fetched_user})
data = %{
"nickname" => "GrimReaper",
@ -588,8 +335,8 @@ test "returns user on success" do
refute invite.used
assert UserView.render("show.json", %{user: user}) ==
UserView.render("show.json", %{user: fetched_user})
assert AccountView.render("account.json", %{user: user}) ==
AccountView.render("account.json", %{user: fetched_user})
end
test "error after max uses" do
@ -612,8 +359,8 @@ test "error after max uses" do
invite = Repo.get_by(UserInviteToken, token: invite.token)
assert invite.used == true
assert UserView.render("show.json", %{user: user}) ==
UserView.render("show.json", %{user: fetched_user})
assert AccountView.render("account.json", %{user: user}) ==
AccountView.render("account.json", %{user: fetched_user})
data = %{
"nickname" => "GrimReaper",
@ -689,31 +436,9 @@ test "it returns the error on registration problems" do
refute User.get_cached_by_nickname("lain")
end
test "it assigns an integer conversation_id" do
note_activity = insert(:note_activity)
status = ActivityView.render("activity.json", activity: note_activity)
assert is_number(status["statusnet_conversation_id"])
end
setup do
Supervisor.terminate_child(Pleroma.Supervisor, Cachex)
Supervisor.restart_child(Pleroma.Supervisor, Cachex)
:ok
end
describe "fetching a user by uri" do
test "fetches a user by uri" do
id = "https://mastodon.social/users/lambadalambda"
user = insert(:user)
{:ok, represented} = TwitterAPI.get_external_profile(user, id)
remote = User.get_cached_by_ap_id(id)
assert represented["id"] == UserView.render("show.json", %{user: remote, for: user})["id"]
# Also fetches the feed.
# assert Activity.get_create_by_object_ap_id("tag:mastodon.social,2017-04-05:objectId=1641750:objectType=Status")
# credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength
end
end
end

View file

@ -1,384 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.ActivityViewTest do
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.TwitterAPI.ActivityView
alias Pleroma.Web.TwitterAPI.UserView
import Pleroma.Factory
import Tesla.Mock
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
import Mock
test "returns a temporary ap_id based user for activities missing db users" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"})
Repo.delete(user)
Cachex.clear(:user_cache)
%{"user" => tw_user} = ActivityView.render("activity.json", activity: activity)
assert tw_user["screen_name"] == "erroruser@example.com"
assert tw_user["name"] == user.ap_id
assert tw_user["statusnet_profile_url"] == user.ap_id
end
test "tries to get a user by nickname if fetching by ap_id doesn't work" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"})
{:ok, user} =
user
|> Ecto.Changeset.change(%{ap_id: "#{user.ap_id}/extension/#{user.nickname}"})
|> Repo.update()
Cachex.clear(:user_cache)
result = ActivityView.render("activity.json", activity: activity)
assert result["user"]["id"] == user.id
end
test "tells if the message is muted for some reason" do
user = insert(:user)
other_user = insert(:user)
{:ok, user} = User.mute(user, other_user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "test"})
status = ActivityView.render("activity.json", %{activity: activity})
assert status["muted"] == false
status = ActivityView.render("activity.json", %{activity: activity, for: user})
assert status["muted"] == true
end
test "a create activity with a html status" do
text = """
#Bike log - Commute Tuesday\nhttps://pla.bike/posts/20181211/\n#cycling #CHScycling #commute\nMVIMG_20181211_054020.jpg
"""
{:ok, activity} = CommonAPI.post(insert(:user), %{"status" => text})
result = ActivityView.render("activity.json", activity: activity)
assert result["statusnet_html"] ==
"<a class=\"hashtag\" data-tag=\"bike\" href=\"http://localhost:4001/tag/bike\" rel=\"tag\">#Bike</a> log - Commute Tuesday<br /><a href=\"https://pla.bike/posts/20181211/\">https://pla.bike/posts/20181211/</a><br /><a class=\"hashtag\" data-tag=\"cycling\" href=\"http://localhost:4001/tag/cycling\" rel=\"tag\">#cycling</a> <a class=\"hashtag\" data-tag=\"chscycling\" href=\"http://localhost:4001/tag/chscycling\" rel=\"tag\">#CHScycling</a> <a class=\"hashtag\" data-tag=\"commute\" href=\"http://localhost:4001/tag/commute\" rel=\"tag\">#commute</a><br />MVIMG_20181211_054020.jpg"
assert result["text"] ==
"#Bike log - Commute Tuesday\nhttps://pla.bike/posts/20181211/\n#cycling #CHScycling #commute\nMVIMG_20181211_054020.jpg"
end
test "a create activity with a summary containing emoji" do
{:ok, activity} =
CommonAPI.post(insert(:user), %{
"spoiler_text" => ":firefox: meow",
"status" => "."
})
result = ActivityView.render("activity.json", activity: activity)
expected = ":firefox: meow"
expected_html =
"<img class=\"emoji\" alt=\"firefox\" title=\"firefox\" src=\"http://localhost:4001/emoji/Firefox.gif\" /> meow"
assert result["summary"] == expected
assert result["summary_html"] == expected_html
end
test "a create activity with a summary containing invalid HTML" do
{:ok, activity} =
CommonAPI.post(insert(:user), %{
"spoiler_text" => "<span style=\"color: magenta; font-size: 32px;\">meow</span>",
"status" => "."
})
result = ActivityView.render("activity.json", activity: activity)
expected = "meow"
assert result["summary"] == expected
assert result["summary_html"] == expected
end
test "a create activity with a note" do
user = insert(:user)
other_user = insert(:user, %{nickname: "shp"})
{:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"})
object = Object.normalize(activity)
result = ActivityView.render("activity.json", activity: activity)
convo_id = Utils.context_to_conversation_id(object.data["context"])
expected = %{
"activity_type" => "post",
"attachments" => [],
"attentions" => [
UserView.render("show.json", %{user: other_user})
],
"created_at" => object.data["published"] |> Utils.date_to_asctime(),
"external_url" => object.data["id"],
"fave_num" => 0,
"favorited" => false,
"id" => activity.id,
"in_reply_to_status_id" => nil,
"in_reply_to_screen_name" => nil,
"in_reply_to_user_id" => nil,
"in_reply_to_profileurl" => nil,
"in_reply_to_ostatus_uri" => nil,
"is_local" => true,
"is_post_verb" => true,
"possibly_sensitive" => false,
"repeat_num" => 0,
"repeated" => false,
"pinned" => false,
"statusnet_conversation_id" => convo_id,
"summary" => "",
"summary_html" => "",
"statusnet_html" =>
"Hey <span class=\"h-card\"><a data-user=\"#{other_user.id}\" class=\"u-url mention\" href=\"#{
other_user.ap_id
}\">@<span>shp</span></a></span>!",
"tags" => [],
"text" => "Hey @shp!",
"uri" => object.data["id"],
"user" => UserView.render("show.json", %{user: user}),
"visibility" => "direct",
"card" => nil,
"muted" => false
}
assert result == expected
end
test "a list of activities" do
user = insert(:user)
other_user = insert(:user, %{nickname: "shp"})
{:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!"})
object = Object.normalize(activity)
convo_id = Utils.context_to_conversation_id(object.data["context"])
mocks = [
{
Utils,
[:passthrough],
[context_to_conversation_id: fn _ -> false end]
},
{
User,
[:passthrough],
[get_cached_by_ap_id: fn _ -> nil end]
}
]
with_mocks mocks do
[result] = ActivityView.render("index.json", activities: [activity])
assert result["statusnet_conversation_id"] == convo_id
assert result["user"]
refute called(Utils.context_to_conversation_id(:_))
refute called(User.get_cached_by_ap_id(user.ap_id))
refute called(User.get_cached_by_ap_id(other_user.ap_id))
end
end
test "an activity that is a reply" do
user = insert(:user)
other_user = insert(:user, %{nickname: "shp"})
{:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!"})
{:ok, answer} =
CommonAPI.post(other_user, %{"status" => "Hi!", "in_reply_to_status_id" => activity.id})
result = ActivityView.render("activity.json", %{activity: answer})
assert result["in_reply_to_status_id"] == activity.id
end
test "a like activity" do
user = insert(:user)
other_user = insert(:user, %{nickname: "shp"})
{:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!"})
{:ok, like, _object} = CommonAPI.favorite(activity.id, other_user)
result = ActivityView.render("activity.json", activity: like)
activity = Pleroma.Activity.get_by_ap_id(activity.data["id"])
expected = %{
"activity_type" => "like",
"created_at" => like.data["published"] |> Utils.date_to_asctime(),
"external_url" => like.data["id"],
"id" => like.id,
"in_reply_to_status_id" => activity.id,
"is_local" => true,
"is_post_verb" => false,
"favorited_status" => ActivityView.render("activity.json", activity: activity),
"statusnet_html" => "shp favorited a status.",
"text" => "shp favorited a status.",
"uri" => "tag:#{like.data["id"]}:objectType=Favourite",
"user" => UserView.render("show.json", user: other_user)
}
assert result == expected
end
test "a like activity for deleted post" do
user = insert(:user)
other_user = insert(:user, %{nickname: "shp"})
{:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!"})
{:ok, like, _object} = CommonAPI.favorite(activity.id, other_user)
CommonAPI.delete(activity.id, user)
result = ActivityView.render("activity.json", activity: like)
expected = %{
"activity_type" => "like",
"created_at" => like.data["published"] |> Utils.date_to_asctime(),
"external_url" => like.data["id"],
"id" => like.id,
"in_reply_to_status_id" => nil,
"is_local" => true,
"is_post_verb" => false,
"favorited_status" => nil,
"statusnet_html" => "shp favorited a status.",
"text" => "shp favorited a status.",
"uri" => "tag:#{like.data["id"]}:objectType=Favourite",
"user" => UserView.render("show.json", user: other_user)
}
assert result == expected
end
test "an announce activity" do
user = insert(:user)
other_user = insert(:user, %{nickname: "shp"})
{:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!"})
{:ok, announce, object} = CommonAPI.repeat(activity.id, other_user)
convo_id = Utils.context_to_conversation_id(object.data["context"])
activity = Activity.get_by_id(activity.id)
result = ActivityView.render("activity.json", activity: announce)
expected = %{
"activity_type" => "repeat",
"created_at" => announce.data["published"] |> Utils.date_to_asctime(),
"external_url" => announce.data["id"],
"id" => announce.id,
"is_local" => true,
"is_post_verb" => false,
"statusnet_html" => "shp repeated a status.",
"text" => "shp repeated a status.",
"uri" => "tag:#{announce.data["id"]}:objectType=note",
"user" => UserView.render("show.json", user: other_user),
"retweeted_status" => ActivityView.render("activity.json", activity: activity),
"statusnet_conversation_id" => convo_id
}
assert result == expected
end
test "A follow activity" do
user = insert(:user)
other_user = insert(:user, %{nickname: "shp"})
{:ok, follower} = User.follow(user, other_user)
{:ok, follow} = ActivityPub.follow(follower, other_user)
result = ActivityView.render("activity.json", activity: follow)
expected = %{
"activity_type" => "follow",
"attentions" => [],
"created_at" => follow.data["published"] |> Utils.date_to_asctime(),
"external_url" => follow.data["id"],
"id" => follow.id,
"in_reply_to_status_id" => nil,
"is_local" => true,
"is_post_verb" => false,
"statusnet_html" => "#{user.nickname} started following shp",
"text" => "#{user.nickname} started following shp",
"user" => UserView.render("show.json", user: user)
}
assert result == expected
end
test "a delete activity" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!"})
{:ok, delete} = CommonAPI.delete(activity.id, user)
result = ActivityView.render("activity.json", activity: delete)
expected = %{
"activity_type" => "delete",
"attentions" => [],
"created_at" => delete.data["published"] |> Utils.date_to_asctime(),
"external_url" => delete.data["id"],
"id" => delete.id,
"in_reply_to_status_id" => nil,
"is_local" => true,
"is_post_verb" => false,
"statusnet_html" => "deleted notice {{tag",
"text" => "deleted notice {{tag",
"uri" => Object.normalize(delete).data["id"],
"user" => UserView.render("show.json", user: user)
}
assert result == expected
end
test "a peertube video" do
{:ok, object} =
Pleroma.Object.Fetcher.fetch_object_from_id(
"https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
)
%Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"])
result = ActivityView.render("activity.json", activity: activity)
assert length(result["attachments"]) == 1
assert result["summary"] == "Friday Night"
end
test "special characters are not escaped in text field for status created" do
text = "<3 is on the way"
{:ok, activity} = CommonAPI.post(insert(:user), %{"status" => text})
result = ActivityView.render("activity.json", activity: activity)
assert result["text"] == text
end
end

View file

@ -1,112 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.NotificationViewTest do
use Pleroma.DataCase
alias Pleroma.Notification
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.TwitterAPI.ActivityView
alias Pleroma.Web.TwitterAPI.NotificationView
alias Pleroma.Web.TwitterAPI.TwitterAPI
alias Pleroma.Web.TwitterAPI.UserView
import Pleroma.Factory
setup do
user = insert(:user, bio: "<span>Here's some html</span>")
[user: user]
end
test "A follow notification" do
note_activity = insert(:note_activity)
user = User.get_cached_by_ap_id(note_activity.data["actor"])
follower = insert(:user)
{:ok, follower} = User.follow(follower, user)
{:ok, activity} = ActivityPub.follow(follower, user)
Cachex.put(:user_cache, "user_info:#{user.id}", User.user_info(Repo.get!(User, user.id)))
[follow_notif] = Notification.for_user(user)
represented = %{
"created_at" => follow_notif.inserted_at |> Utils.format_naive_asctime(),
"from_profile" => UserView.render("show.json", %{user: follower, for: user}),
"id" => follow_notif.id,
"is_seen" => 0,
"notice" => ActivityView.render("activity.json", %{activity: activity, for: user}),
"ntype" => "follow"
}
assert represented ==
NotificationView.render("notification.json", %{notification: follow_notif, for: user})
end
test "A mention notification" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} =
TwitterAPI.create_status(other_user, %{"status" => "Päivää, @#{user.nickname}"})
[notification] = Notification.for_user(user)
represented = %{
"created_at" => notification.inserted_at |> Utils.format_naive_asctime(),
"from_profile" => UserView.render("show.json", %{user: other_user, for: user}),
"id" => notification.id,
"is_seen" => 0,
"notice" => ActivityView.render("activity.json", %{activity: activity, for: user}),
"ntype" => "mention"
}
assert represented ==
NotificationView.render("notification.json", %{notification: notification, for: user})
end
test "A retweet notification" do
note_activity = insert(:note_activity)
user = User.get_cached_by_ap_id(note_activity.data["actor"])
repeater = insert(:user)
{:ok, _activity} = TwitterAPI.repeat(repeater, note_activity.id)
[notification] = Notification.for_user(user)
represented = %{
"created_at" => notification.inserted_at |> Utils.format_naive_asctime(),
"from_profile" => UserView.render("show.json", %{user: repeater, for: user}),
"id" => notification.id,
"is_seen" => 0,
"notice" =>
ActivityView.render("activity.json", %{activity: notification.activity, for: user}),
"ntype" => "repeat"
}
assert represented ==
NotificationView.render("notification.json", %{notification: notification, for: user})
end
test "A like notification" do
note_activity = insert(:note_activity)
user = User.get_cached_by_ap_id(note_activity.data["actor"])
liker = insert(:user)
{:ok, _activity} = TwitterAPI.fav(liker, note_activity.id)
[notification] = Notification.for_user(user)
represented = %{
"created_at" => notification.inserted_at |> Utils.format_naive_asctime(),
"from_profile" => UserView.render("show.json", %{user: liker, for: user}),
"id" => notification.id,
"is_seen" => 0,
"notice" =>
ActivityView.render("activity.json", %{activity: notification.activity, for: user}),
"ntype" => "like"
}
assert represented ==
NotificationView.render("notification.json", %{notification: notification, for: user})
end
end

View file

@ -1,323 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.UserViewTest do
use Pleroma.DataCase
alias Pleroma.User
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.TwitterAPI.UserView
import Pleroma.Factory
setup do
user = insert(:user, bio: "<span>Here's some html</span>")
[user: user]
end
test "A user with only a nickname", %{user: user} do
user = %{user | name: nil, nickname: "scarlett@catgirl.science"}
represented = UserView.render("show.json", %{user: user})
assert represented["name"] == user.nickname
assert represented["name_html"] == user.nickname
end
test "A user with an avatar object", %{user: user} do
image = "image"
user = %{user | avatar: %{"url" => [%{"href" => image}]}}
represented = UserView.render("show.json", %{user: user})
assert represented["profile_image_url"] == image
end
test "A user with emoji in username" do
expected =
"<img class=\"emoji\" alt=\"karjalanpiirakka\" title=\"karjalanpiirakka\" src=\"/file.png\" /> man"
user =
insert(:user, %{
info: %{
source_data: %{
"tag" => [
%{
"type" => "Emoji",
"icon" => %{"url" => "/file.png"},
"name" => ":karjalanpiirakka:"
}
]
}
},
name: ":karjalanpiirakka: man"
})
represented = UserView.render("show.json", %{user: user})
assert represented["name_html"] == expected
end
test "A user" do
note_activity = insert(:note_activity)
user = User.get_cached_by_ap_id(note_activity.data["actor"])
{:ok, user} = User.update_note_count(user)
follower = insert(:user)
second_follower = insert(:user)
User.follow(follower, user)
User.follow(second_follower, user)
User.follow(user, follower)
{:ok, user} = User.update_follower_count(user)
Cachex.put(:user_cache, "user_info:#{user.id}", User.user_info(Repo.get!(User, user.id)))
image = "http://localhost:4001/images/avi.png"
banner = "http://localhost:4001/images/banner.png"
represented = %{
"id" => user.id,
"name" => user.name,
"screen_name" => user.nickname,
"name_html" => user.name,
"description" => HtmlSanitizeEx.strip_tags(user.bio |> String.replace("<br>", "\n")),
"description_html" => HtmlSanitizeEx.basic_html(user.bio),
"created_at" => user.inserted_at |> Utils.format_naive_asctime(),
"favourites_count" => 0,
"statuses_count" => 1,
"friends_count" => 1,
"followers_count" => 2,
"profile_image_url" => image,
"profile_image_url_https" => image,
"profile_image_url_profile_size" => image,
"profile_image_url_original" => image,
"following" => false,
"follows_you" => false,
"statusnet_blocking" => false,
"statusnet_profile_url" => user.ap_id,
"cover_photo" => banner,
"background_image" => nil,
"is_local" => true,
"locked" => false,
"hide_follows" => false,
"hide_followers" => false,
"fields" => [],
"pleroma" => %{
"confirmation_pending" => false,
"tags" => [],
"skip_thread_containment" => false
},
"rights" => %{"admin" => false, "delete_others_notice" => false},
"role" => "member"
}
assert represented == UserView.render("show.json", %{user: user})
end
test "User exposes settings for themselves and only for themselves", %{user: user} do
as_user = UserView.render("show.json", %{user: user, for: user})
assert as_user["default_scope"] == user.info.default_scope
assert as_user["no_rich_text"] == user.info.no_rich_text
assert as_user["pleroma"]["notification_settings"] == user.info.notification_settings
as_stranger = UserView.render("show.json", %{user: user})
refute as_stranger["default_scope"]
refute as_stranger["no_rich_text"]
refute as_stranger["pleroma"]["notification_settings"]
end
test "A user for a given other follower", %{user: user} do
follower = insert(:user, %{following: [User.ap_followers(user)]})
{:ok, user} = User.update_follower_count(user)
image = "http://localhost:4001/images/avi.png"
banner = "http://localhost:4001/images/banner.png"
represented = %{
"id" => user.id,
"name" => user.name,
"screen_name" => user.nickname,
"name_html" => user.name,
"description" => HtmlSanitizeEx.strip_tags(user.bio |> String.replace("<br>", "\n")),
"description_html" => HtmlSanitizeEx.basic_html(user.bio),
"created_at" => user.inserted_at |> Utils.format_naive_asctime(),
"favourites_count" => 0,
"statuses_count" => 0,
"friends_count" => 0,
"followers_count" => 1,
"profile_image_url" => image,
"profile_image_url_https" => image,
"profile_image_url_profile_size" => image,
"profile_image_url_original" => image,
"following" => true,
"follows_you" => false,
"statusnet_blocking" => false,
"statusnet_profile_url" => user.ap_id,
"cover_photo" => banner,
"background_image" => nil,
"is_local" => true,
"locked" => false,
"hide_follows" => false,
"hide_followers" => false,
"fields" => [],
"pleroma" => %{
"confirmation_pending" => false,
"tags" => [],
"skip_thread_containment" => false
},
"rights" => %{"admin" => false, "delete_others_notice" => false},
"role" => "member"
}
assert represented == UserView.render("show.json", %{user: user, for: follower})
end
test "A user that follows you", %{user: user} do
follower = insert(:user)
{:ok, follower} = User.follow(follower, user)
{:ok, user} = User.update_follower_count(user)
image = "http://localhost:4001/images/avi.png"
banner = "http://localhost:4001/images/banner.png"
represented = %{
"id" => follower.id,
"name" => follower.name,
"screen_name" => follower.nickname,
"name_html" => follower.name,
"description" => HtmlSanitizeEx.strip_tags(follower.bio |> String.replace("<br>", "\n")),
"description_html" => HtmlSanitizeEx.basic_html(follower.bio),
"created_at" => follower.inserted_at |> Utils.format_naive_asctime(),
"favourites_count" => 0,
"statuses_count" => 0,
"friends_count" => 1,
"followers_count" => 0,
"profile_image_url" => image,
"profile_image_url_https" => image,
"profile_image_url_profile_size" => image,
"profile_image_url_original" => image,
"following" => false,
"follows_you" => true,
"statusnet_blocking" => false,
"statusnet_profile_url" => follower.ap_id,
"cover_photo" => banner,
"background_image" => nil,
"is_local" => true,
"locked" => false,
"hide_follows" => false,
"hide_followers" => false,
"fields" => [],
"pleroma" => %{
"confirmation_pending" => false,
"tags" => [],
"skip_thread_containment" => false
},
"rights" => %{"admin" => false, "delete_others_notice" => false},
"role" => "member"
}
assert represented == UserView.render("show.json", %{user: follower, for: user})
end
test "a user that is a moderator" do
user = insert(:user, %{info: %{is_moderator: true}})
represented = UserView.render("show.json", %{user: user, for: user})
assert represented["rights"]["delete_others_notice"]
assert represented["role"] == "moderator"
end
test "a user that is a admin" do
user = insert(:user, %{info: %{is_admin: true}})
represented = UserView.render("show.json", %{user: user, for: user})
assert represented["rights"]["admin"]
assert represented["role"] == "admin"
end
test "A moderator with hidden role for another user", %{user: user} do
admin = insert(:user, %{info: %{is_moderator: true, show_role: false}})
represented = UserView.render("show.json", %{user: admin, for: user})
assert represented["role"] == nil
end
test "An admin with hidden role for another user", %{user: user} do
admin = insert(:user, %{info: %{is_admin: true, show_role: false}})
represented = UserView.render("show.json", %{user: admin, for: user})
assert represented["role"] == nil
end
test "A regular user for the admin", %{user: user} do
admin = insert(:user, %{info: %{is_admin: true}})
represented = UserView.render("show.json", %{user: user, for: admin})
assert represented["pleroma"]["deactivated"] == false
end
test "A blocked user for the blocker" do
user = insert(:user)
blocker = insert(:user)
User.block(blocker, user)
image = "http://localhost:4001/images/avi.png"
banner = "http://localhost:4001/images/banner.png"
represented = %{
"id" => user.id,
"name" => user.name,
"screen_name" => user.nickname,
"name_html" => user.name,
"description" => HtmlSanitizeEx.strip_tags(user.bio |> String.replace("<br>", "\n")),
"description_html" => HtmlSanitizeEx.basic_html(user.bio),
"created_at" => user.inserted_at |> Utils.format_naive_asctime(),
"favourites_count" => 0,
"statuses_count" => 0,
"friends_count" => 0,
"followers_count" => 0,
"profile_image_url" => image,
"profile_image_url_https" => image,
"profile_image_url_profile_size" => image,
"profile_image_url_original" => image,
"following" => false,
"follows_you" => false,
"statusnet_blocking" => true,
"statusnet_profile_url" => user.ap_id,
"cover_photo" => banner,
"background_image" => nil,
"is_local" => true,
"locked" => false,
"hide_follows" => false,
"hide_followers" => false,
"fields" => [],
"pleroma" => %{
"confirmation_pending" => false,
"tags" => [],
"skip_thread_containment" => false
},
"rights" => %{"admin" => false, "delete_others_notice" => false},
"role" => "member"
}
blocker = User.get_cached_by_id(blocker.id)
assert represented == UserView.render("show.json", %{user: user, for: blocker})
end
test "a user with mastodon fields" do
fields = [
%{
"name" => "Pronouns",
"value" => "she/her"
},
%{
"name" => "Website",
"value" => "https://example.org/"
}
]
user =
insert(:user, %{
info: %{
source_data: %{
"attachment" =>
Enum.map(fields, fn field -> Map.put(field, "type", "PropertyValue") end)
}
}
})
userview = UserView.render("show.json", %{user: user})
assert userview["fields"] == fields
end
end