Browse Source

[#1149] Merge remote-tracking branch 'remotes/upstream/develop' into 1149-oban-job-queue

# Conflicts:
#	test/web/twitter_api/twitter_api_controller_test.exs
object-id-column
Ivan Tashkinov 4 years ago
parent
commit
ca1ba1e272
57 changed files with 425 additions and 5324 deletions
  1. +2
    -0
      CHANGELOG.md
  2. +1
    -1
      README.md
  3. +41
    -0
      lib/pleroma/plugs/trailing_format_plug.ex
  4. +16
    -2
      lib/pleroma/user.ex
  5. +1
    -1
      lib/pleroma/web/endpoint.ex
  6. +2
    -2
      lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
  7. +22
    -11
      lib/pleroma/web/mastodon_api/views/status_view.ex
  8. +0
    -100
      lib/pleroma/web/router.ex
  9. +0
    -38
      lib/pleroma/web/twitter_api/representers/base_representer.ex
  10. +0
    -39
      lib/pleroma/web/twitter_api/representers/object_representer.ex
  11. +0
    -195
      lib/pleroma/web/twitter_api/twitter_api.ex
  12. +20
    -746
      lib/pleroma/web/twitter_api/twitter_api_controller.ex
  13. +0
    -366
      lib/pleroma/web/twitter_api/views/activity_view.ex
  14. +0
    -71
      lib/pleroma/web/twitter_api/views/notification_view.ex
  15. +0
    -191
      lib/pleroma/web/twitter_api/views/user_view.ex
  16. +1
    -1
      priv/static/index.html
  17. +0
    -1
      priv/static/static/config.json
  18. +22
    -13
      priv/static/static/css/app.cb3673e4b661fd9526ea.css
  19. +1
    -0
      priv/static/static/css/app.cb3673e4b661fd9526ea.css.map
  20. +0
    -1
      priv/static/static/css/app.db80066bde2c96ea6198.css.map
  21. +0
    -0
      priv/static/static/font/LICENSE.txt
  22. +0
    -0
      priv/static/static/font/README.txt
  23. +20
    -6
      priv/static/static/font/config.json
  24. +2
    -1
      priv/static/static/font/css/fontello-codes.css
  25. +8
    -7
      priv/static/static/font/css/fontello-embedded.css
  26. +2
    -1
      priv/static/static/font/css/fontello-ie7-codes.css
  27. +2
    -1
      priv/static/static/font/css/fontello-ie7.css
  28. +9
    -8
      priv/static/static/font/css/fontello.css
  29. +12
    -9
      priv/static/static/font/demo.html
  30. BIN
      priv/static/static/font/font/fontello.eot
  31. +3
    -1
      priv/static/static/font/font/fontello.svg
  32. BIN
      priv/static/static/font/font/fontello.ttf
  33. BIN
      priv/static/static/font/font/fontello.woff
  34. BIN
      priv/static/static/font/font/fontello.woff2
  35. +0
    -2
      priv/static/static/js/app.670c36c0acc42fadb4fe.js
  36. +0
    -1
      priv/static/static/js/app.670c36c0acc42fadb4fe.js.map
  37. +2
    -0
      priv/static/static/js/app.8098503330c7dd14a415.js
  38. +1
    -0
      priv/static/static/js/app.8098503330c7dd14a415.js.map
  39. +0
    -82
      priv/static/static/js/vendors~app.4b7be53256fba5c365c9.js
  40. +0
    -1
      priv/static/static/js/vendors~app.4b7be53256fba5c365c9.js.map
  41. +82
    -0
      priv/static/static/js/vendors~app.4cedffe4993b111c7421.js
  42. +1
    -0
      priv/static/static/js/vendors~app.4cedffe4993b111c7421.js.map
  43. +1
    -1
      priv/static/sw-pleroma.js
  44. +1
    -0
      test/fixtures/tesla_mock/misskey_poll_no_end_date.json
  45. +1
    -0
      test/fixtures/tesla_mock/sjw.json
  46. +26
    -61
      test/notification_test.exs
  47. +12
    -0
      test/support/http_request_mock.ex
  48. +10
    -12
      test/user_test.exs
  49. +74
    -25
      test/web/mastodon_api/mastodon_api_controller_test.exs
  50. +4
    -3
      test/web/mastodon_api/mastodon_api_test.exs
  51. +8
    -0
      test/web/mastodon_api/views/status_view_test.exs
  52. +0
    -60
      test/web/twitter_api/representers/object_representer_test.exs
  53. +0
    -2154
      test/web/twitter_api/twitter_api_controller_test.exs
  54. +15
    -290
      test/web/twitter_api/twitter_api_test.exs
  55. +0
    -384
      test/web/twitter_api/views/activity_view_test.exs
  56. +0
    -112
      test/web/twitter_api/views/notification_view_test.exs
  57. +0
    -323
      test/web/twitter_api/views/user_view_test.exs

+ 2
- 0
CHANGELOG.md View File

@@ -34,6 +34,7 @@ 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
@@ -111,6 +112,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- RichMedia: add the rich media ttl based on image expiration time.

### Removed
- GNU Social API with Qvitter extensions support
- Emoji: Remove longfox emojis.
- Remove `Reply-To` header from report emails for admins.
- ActivityPub: The `accept_blocks` configuration setting.


+ 1
- 1
README.md 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)



+ 41
- 0
lib/pleroma/plugs/trailing_format_plug.ex 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

+ 16
- 2
lib/pleroma/user.ex View File

@@ -570,8 +570,22 @@ defmodule Pleroma.User 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


+ 1
- 1
lib/pleroma/web/endpoint.ex 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)



+ 2
- 2
lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex View File

@@ -290,7 +290,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
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 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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"])


+ 22
- 11
lib/pleroma/web/mastodon_api/views/status_view.ex View File

@@ -385,16 +385,27 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end

if options do
end_time =
(object.data["closed"] || object.data["endTime"])
|> NaiveDateTime.from_iso8601!()

expired =
end_time
|> NaiveDateTime.compare(NaiveDateTime.utc_now())
|> case do
:lt -> true
_ -> false
{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
end

end_time = Utils.to_masto_date(end_time)

{end_time, expired}

_ ->
{nil, false}
end

voted =
@@ -421,7 +432,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView 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,


+ 0
- 100
lib/pleroma/web/router.ex 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


+ 0
- 38
lib/pleroma/web/twitter_api/representers/base_representer.ex 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

+ 0
- 39
lib/pleroma/web/twitter_api/representers/object_representer.ex 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

+ 0
- 195
lib/pleroma/web/twitter_api/twitter_api.ex 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 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI 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

+ 20
- 746
lib/pleroma/web/twitter_api/twitter_api_controller.ex 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 @@ defmodule Pleroma.Web.TwitterAPI.Controller 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,160 +42,16 @@ defmodule Pleroma.Web.TwitterAPI.Controller 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)

def errors(conn, {:param_cast, _}) do
conn
|> put_view(ActivityView)
|> render("index.json", %{activities: activities, for: user})
|> put_status(400)
|> json("Invalid parameters")
end

def search_user(%{assigns: %{user: user}} = conn, %{"query" => query}) do
users = User.search(query, resolve: true, for_user: user)

def errors(conn, _) do
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)
|> put_status(500)
|> json("Something went wrong")
end

defp json_reply(conn, status, json) do
@@ -777,36 +60,27 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
|> 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 notifications_read(%{assigns: %{user: user}} = conn, %{"latest_id" => latest_id} = params) do
Notification.set_read_up_to(user, latest_id)

def only_if_public_instance(%{assigns: %{user: %User{}}} = conn, _), do: conn
notifications = Notification.for_user(user, params)

def only_if_public_instance(conn, _) do
if Pleroma.Config.get([:instance, :public]) do
conn
else
conn
|> forbidden_json_reply("Invalid credentials.")
|> halt()
end
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

defp error_json(conn, error_message) do
%{"error" => error_message, "request" => conn.request_path} |> Jason.encode!()
def notifications_read(%{assigns: %{user: _user}} = conn, _) do
bad_request_reply(conn, "You need to specify latest_id")
end

def errors(conn, {:param_cast, _}) do
conn
|> put_status(400)
|> json("Invalid parameters")
defp bad_request_reply(conn, error_message) do
json = error_json(conn, error_message)
json_reply(conn, 400, json)
end

def errors(conn, _) do
conn
|> put_status(500)
|> json("Something went wrong")
defp error_json(conn, error_message) do
%{"error" => error_message, "request" => conn.request_path} |> Jason.encode!()
end
end

+ 0
- 366
lib/pleroma/web/twitter_api/views/activity_view.ex 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

+ 0
- 71
lib/pleroma/web/twitter_api/views/notification_view.ex 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

+ 0
- 191
lib/pleroma/web/twitter_api/views/user_view.ex 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

+ 1
- 1
priv/static/index.html 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>

+ 0
- 1
priv/static/static/config.json View File

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


priv/static/static/css/app.db80066bde2c96ea6198.css → priv/static/static/css/app.cb3673e4b661fd9526ea.css View File

@@ -1,17 +1,8 @@
.with-load-more-footer {
padding: 10px;
text-align: center;
border-top: 1px solid;
border-top-color: #222;
border-top-color: var(--border, #222);
}
.with-load-more-footer .error {
font-size: 14px;
}
.tab-switcher .contents .hidden {
display: none;
}
.tab-switcher .tabs {
display: -ms-flexbox;
display: flex;
position: relative;
width: 100%;
@@ -23,7 +14,8 @@
.tab-switcher .tabs::after, .tab-switcher .tabs::before {
display: block;
content: "";
flex: 1 1 auto;
-ms-flex: 1 1 auto;
flex: 1 1 auto;
border-bottom: 1px solid;
border-bottom-color: #222;
border-bottom-color: var(--border, #222);
@@ -31,8 +23,10 @@
.tab-switcher .tabs .tab-wrapper {
height: 28px;
position: relative;
display: -ms-flexbox;
display: flex;
flex: 0 0 auto;
-ms-flex: 0 0 auto;
flex: 0 0 auto;
}
.tab-switcher .tabs .tab-wrapper .tab {
width: 100%;
@@ -55,6 +49,11 @@
background: transparent;
z-index: 5;
}
.tab-switcher .tabs .tab-wrapper .tab img {
max-height: 26px;
vertical-align: top;
margin-top: -5px;
}
.tab-switcher .tabs .tab-wrapper:not(.active)::after {
content: "";
position: absolute;
@@ -66,6 +65,16 @@
border-bottom-color: #222;
border-bottom-color: var(--border, #222);
}
.with-load-more-footer {
padding: 10px;
text-align: center;
border-top: 1px solid;
border-top-color: #222;
border-top-color: var(--border, #222);
}
.with-load-more-footer .error {
font-size: 14px;
}
.with-subscription-loading {
padding: 10px;
text-align: center;
@@ -74,4 +83,4 @@
font-size: 14px;
}

/*# sourceMappingURL=app.db80066bde2c96ea6198.css.map*/
/*# sourceMappingURL=app.cb3673e4b661fd9526ea.css.map*/

+ 1
- 0
priv/static/static/css/app.cb3673e4b661fd9526ea.css.map 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":""}

+ 0
- 1
priv/static/static/css/app.db80066bde2c96ea6198.css.map 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
- 0
priv/static/static/font/LICENSE.txt View File


+ 0
- 0
priv/static/static/font/README.txt View File


+ 20
- 6
priv/static/static/font/config.json View File

@@ -151,12 +151,6 @@
"src": "fontawesome"
},
{
"uid": "cd21cbfb28ad4d903cede582157f65dc",
"css": "bell",
"code": 59408,
"src": "fontawesome"
},
{
"uid": "ccc2329632396dc096bb638d4b46fb98",
"css": "mail-alt",
"code": 61664,
@@ -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"
}
]
}

+ 2
- 1
priv/static/static/font/css/fontello-codes.css View File

@@ -15,7 +15,7 @@
.icon-right-open:before { content: '\e80d'; } /* '' */
.icon-left-open:before { content: '\e80e'; } /* '' */
.icon-up-open:before { content: '\e80f'; } /* '' */
.icon-bell:before { content: '\e810'; } /* '' */
.icon-bell-ringing-o:before { content: '\e810'; } /* '' */
.icon-lock:before { content: '\e811'; } /* '' */
.icon-globe:before { content: '\e812'; } /* '' */
.icon-brush:before { content: '\e813'; } /* '' */
@@ -27,6 +27,7 @@
.icon-pin:before { content: '\e819'; } /* '' */
.icon-wrench:before { content: '\e81a'; } /* '' */
.icon-chart-bar:before { content: '\e81b'; } /* '' */
.icon-zoom-in:before { content: '\e81c'; } /* '' */
.icon-spin3:before { content: '\e832'; } /* '' */
.icon-spin4:before { content: '\e834'; } /* '' */
.icon-link-ext:before { content: '\f08e'; } /* '' */


+ 8
- 7
priv/static/static/font/css/fontello-embedded.css
File diff suppressed because it is too large
View File


+ 2
- 1
priv/static/static/font/css/fontello-ie7-codes.css View File

@@ -15,7 +15,7 @@
.icon-right-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80d;&nbsp;'); }
.icon-left-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80e;&nbsp;'); }
.icon-up-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80f;&nbsp;'); }
.icon-bell { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe810;&nbsp;'); }
.icon-bell-ringing-o { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe810;&nbsp;'); }
.icon-lock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe811;&nbsp;'); }
.icon-globe { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe812;&nbsp;'); }
.icon-brush { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe813;&nbsp;'); }
@@ -27,6 +27,7 @@
.icon-pin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe819;&nbsp;'); }
.icon-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81a;&nbsp;'); }
.icon-chart-bar { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81b;&nbsp;'); }
.icon-zoom-in { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81c;&nbsp;'); }
.icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe832;&nbsp;'); }
.icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe834;&nbsp;'); }
.icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08e;&nbsp;'); }


+ 2
- 1
priv/static/static/font/css/fontello-ie7.css View File

@@ -26,7 +26,7 @@
.icon-right-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80d;&nbsp;'); }
.icon-left-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80e;&nbsp;'); }
.icon-up-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80f;&nbsp;'); }
.icon-bell { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe810;&nbsp;'); }
.icon-bell-ringing-o { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe810;&nbsp;'); }
.icon-lock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe811;&nbsp;'); }
.icon-globe { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe812;&nbsp;'); }
.icon-brush { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe813;&nbsp;'); }
@@ -38,6 +38,7 @@
.icon-pin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe819;&nbsp;'); }
.icon-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81a;&nbsp;'); }
.icon-chart-bar { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81b;&nbsp;'); }
.icon-zoom-in { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81c;&nbsp;'); }
.icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe832;&nbsp;'); }
.icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe834;&nbsp;'); }
.icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08e;&nbsp;'); }


+ 9
- 8
priv/static/static/font/css/fontello.css View File

@@ -1,11 +1,11 @@
@font-face {
font-family: 'fontello';
src: url('../font/fontello.eot?3304725');
src: url('../font/fontello.eot?3304725#iefix') format('embedded-opentype'),
url('../font/fontello.woff2?3304725') format('woff2'),
url('../font/fontello.woff?3304725') format('woff'),
url('../font/fontello.ttf?3304725') format('truetype'),
url('../font/fontello.svg?3304725#fontello') format('svg');
src: url('../font/fontello.eot?4060331');
src: url('../font/fontello.eot?4060331#iefix') format('embedded-opentype'),
url('../font/fontello.woff2?4060331') format('woff2'),
url('../font/fontello.woff?4060331') format('woff'),
url('../font/fontello.ttf?4060331') format('truetype'),
url('../font/fontello.svg?4060331#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
@@ -15,7 +15,7 @@
@media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face {
font-family: 'fontello';
src: url('../font/fontello.svg?3304725#fontello') format('svg');
src: url('../font/fontello.svg?4060331#fontello') format('svg');
}
}
*/
@@ -71,7 +71,7 @@
.icon-right-open:before { content: '\e80d'; } /* '' */
.icon-left-open:before { content: '\e80e'; } /* '' */
.icon-up-open:before { content: '\e80f'; } /* '' */
.icon-bell:before { content: '\e810'; } /* '' */
.icon-bell-ringing-o:before { content: '\e810'; } /* '' */
.icon-lock:before { content: '\e811'; } /* '' */
.icon-globe:before { content: '\e812'; } /* '' */
.icon-brush:before { content: '\e813'; } /* '' */
@@ -83,6 +83,7 @@
.icon-pin:before { content: '\e819'; } /* '' */
.icon-wrench:before { content: '\e81a'; } /* '' */
.icon-chart-bar:before { content: '\e81b'; } /* '' */
.icon-zoom-in:before { content: '\e81c'; } /* '' */
.icon-spin3:before { content: '\e832'; } /* '' */
.icon-spin4:before { content: '\e834'; } /* '' */
.icon-link-ext:before { content: '\f08e'; } /* '' */


+ 12
- 9
priv/static/static/font/demo.html 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>


BIN
priv/static/static/font/font/fontello.eot View File


+ 3
- 1
priv/static/static/font/font/fontello.svg 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" />


BIN
priv/static/static/font/font/fontello.ttf View File


BIN
priv/static/static/font/font/fontello.woff View File


BIN
priv/static/static/font/font/fontello.woff2 View File


+ 0
- 2
priv/static/static/js/app.670c36c0acc42fadb4fe.js
File diff suppressed because it is too large
View File


+ 0
- 1
priv/static/static/js/app.670c36c0acc42fadb4fe.js.map
File diff suppressed because it is too large
View File


+ 2
- 0
priv/static/static/js/app.8098503330c7dd14a415.js
File diff suppressed because it is too large
View File


+ 1
- 0
priv/static/static/js/app.8098503330c7dd14a415.js.map
File diff suppressed because it is too large
View File


+ 0
- 82
priv/static/static/js/vendors~app.4b7be53256fba5c365c9.js
File diff suppressed because it is too large
View File


+ 0
- 1
priv/static/static/js/vendors~app.4b7be53256fba5c365c9.js.map
File diff suppressed because it is too large
View File


+ 82
- 0
priv/static/static/js/vendors~app.4cedffe4993b111c7421.js
File diff suppressed because it is too large
View File


+ 1
- 0
priv/static/static/js/vendors~app.4cedffe4993b111c7421.js.map
File diff suppressed because it is too large
View File


+ 1
- 1
priv/static/sw-pleroma.js View File

@@ -1,4 +1,4 @@
var serviceWorkerOption = {"assets":["/static/img/nsfw.74818f9.png","/static/css/app.db80066bde2c96ea6198.css","/static/js/app.670c36c0acc42fadb4fe.js","/static/css/vendors~app.b2603a50868c68a1c192.css","/static/js/vendors~app.4b7be53256fba5c365c9.js"]};
var serviceWorkerOption = {"assets":["/static/img/nsfw.74818f9.png","/static/css/app.cb3673e4b661fd9526ea.css","/static/js/app.8098503330c7dd14a415.js","/static/css/vendors~app.b2603a50868c68a1c192.css","/static/js/vendors~app.4cedffe4993b111c7421.js"]};
!function(e){var n={};function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)t.d(r,o,function(n){return e[n]}.bind(null,o));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="/",t(t.s=0)}([function(e,n,t){"use strict";var r,o=t(1),i=(r=o)&&r.__esModule?r:{default:r};function a(){return clients.matchAll({includeUncontrolled:!0}).then(function(e){return e.filter(function(e){return"window"===e.type})})}self.addEventListener("push",function(e){e.data&&e.waitUntil(i.default.getItem("vuex-lz").then(function(e){return e.config.webPushNotifications}).then(function(n){return n&&a().then(function(n){var t=e.data.json();if(0===n.length)return self.registration.showNotification(t.title,t)})}))}),self.addEventListener("notificationclick",function(e){e.notification.close(),e.waitUntil(a().then(function(e){for(var n=0;n<e.length;n++){var t=e[n];if("/"===t.url&&"focus"in t)return t.focus()}if(clients.openWindow)return clients.openWindow("/")}))})},function(e,n){
/*!


+ 1
- 0
test/fixtures/tesla_mock/misskey_poll_no_end_date.json 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
- 0
test/fixtures/tesla_mock/sjw.json 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}

+ 26
- 61
test/notification_test.exs View File

@@ -13,7 +13,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
@@ -22,7 +21,7 @@ defmodule Pleroma.NotificationTest do
third_user = insert(:user)

{:ok, activity} =
TwitterAPI.create_status(user, %{
CommonAPI.post(user, %{
"status" => "hey @#{other_user.nickname} and @#{third_user.nickname}"
})

@@ -40,7 +39,7 @@ defmodule Pleroma.NotificationTest 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
@@ -185,47 +184,20 @@ defmodule Pleroma.NotificationTest 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

@@ -235,8 +207,7 @@ defmodule Pleroma.NotificationTest do

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
@@ -247,8 +218,7 @@ defmodule Pleroma.NotificationTest 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)
@@ -260,8 +230,7 @@ defmodule Pleroma.NotificationTest 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)
@@ -273,8 +242,7 @@ defmodule Pleroma.NotificationTest 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)
@@ -286,8 +254,7 @@ defmodule Pleroma.NotificationTest 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)
@@ -301,14 +268,14 @@ defmodule Pleroma.NotificationTest 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} !"
})

@@ -326,12 +293,12 @@ defmodule Pleroma.NotificationTest 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}!"
})

@@ -341,7 +308,7 @@ defmodule Pleroma.NotificationTest do
assert n2.id > n1.id

{:ok, _activity} =
TwitterAPI.create_status(user, %{
CommonAPI.post(user, %{
"status" => "hey yet again @#{other_user.nickname}!"
})

@@ -680,7 +647,7 @@ defmodule Pleroma.NotificationTest 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
@@ -690,7 +657,7 @@ defmodule Pleroma.NotificationTest 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
@@ -700,7 +667,7 @@ defmodule Pleroma.NotificationTest 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
@@ -710,7 +677,7 @@ defmodule Pleroma.NotificationTest 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
@@ -719,8 +686,7 @@ defmodule Pleroma.NotificationTest 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) == []
@@ -731,7 +697,7 @@ defmodule Pleroma.NotificationTest 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 length(Notification.for_user(user, %{with_muted: true})) == 1
end
@@ -741,7 +707,7 @@ defmodule Pleroma.NotificationTest 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
@@ -751,7 +717,7 @@ defmodule Pleroma.NotificationTest 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
@@ -760,8 +726,7 @@ defmodule Pleroma.NotificationTest 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


+ 12
- 0
test/support/http_request_mock.ex View File

@@ -992,6 +992,18 @@ defmodule HttpRequestMock 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)}, #{


+ 10
- 12
test/user_test.exs View File

@@ -71,8 +71,8 @@ defmodule Pleroma.UserTest 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)
@@ -85,9 +85,9 @@ defmodule Pleroma.UserTest 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)
@@ -1295,11 +1295,9 @@ defmodule Pleroma.UserTest 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
@@ -1343,7 +1341,7 @@ defmodule Pleroma.UserTest do
to = Enum.random(users -- [user])

{:ok, _} =
Pleroma.Web.TwitterAPI.TwitterAPI.create_status(user, %{
CommonAPI.post(user, %{
"status" => "hey @#{to.nickname}"
})
end)
@@ -1375,12 +1373,12 @@ defmodule Pleroma.UserTest 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)


+ 74
- 25
test/web/mastodon_api/mastodon_api_controller_test.exs View File

@@ -22,7 +22,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
@@ -1485,12 +1484,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest 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
@@ -1676,32 +1672,85 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest 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


+ 4
- 3
test/web/mastodon_api/mastodon_api_test.exs 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 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPITest 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)


+ 8
- 0
test/web/mastodon_api/views/status_view_test.exs View File

@@ -551,6 +551,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest 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


+ 0
- 60
test/web/twitter_api/representers/object_representer_test.exs 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

+ 0
- 2154
test/web/twitter_api/twitter_api_controller_test.exs
File diff suppressed because it is too large
View File


+ 15
- 290
test/web/twitter_api/twitter_api_test.exs View File

@@ -4,271 +4,18 @@

defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
use Pleroma.DataCase
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
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",
@@ -282,8 +29,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest 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
@@ -300,8 +47,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest 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
@@ -399,8 +146,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest 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
@@ -464,8 +211,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest 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}
@@ -539,8 +286,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest 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",
@@ -590,8 +337,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest 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
@@ -614,8 +361,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest 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",
@@ -691,31 +438,9 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPITest 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

+ 0
- 384
test/web/twitter_api/views/activity_view_test.exs 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

+ 0
- 112
test/web/twitter_api/views/notification_view_test.exs 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

+ 0
- 323
test/web/twitter_api/views/user_view_test.exs 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

Loading…
Cancel
Save