Merge branch 'develop' into issue/1383

This commit is contained in:
Maksim Pechnikov 2019-12-14 21:44:10 +03:00
commit 67cb46e15d
80 changed files with 1278 additions and 1292 deletions

View file

@ -28,6 +28,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- **Breaking:** Admin API: Return link alongside with token on password reset - **Breaking:** Admin API: Return link alongside with token on password reset
- **Breaking:** Admin API: `PUT /api/pleroma/admin/reports/:id` is now `PATCH /api/pleroma/admin/reports`, see admin_api.md for details - **Breaking:** Admin API: `PUT /api/pleroma/admin/reports/:id` is now `PATCH /api/pleroma/admin/reports`, see admin_api.md for details
- **Breaking:** `/api/pleroma/admin/users/invite_token` now uses `POST`, changed accepted params and returns full invite in json instead of only token string. - **Breaking:** `/api/pleroma/admin/users/invite_token` now uses `POST`, changed accepted params and returns full invite in json instead of only token string.
- **Breaking** replying to reports is now "report notes", enpoint changed from `POST /api/pleroma/admin/reports/:id/respond` to `POST /api/pleroma/admin/reports/:id/notes`
- Admin API: Return `total` when querying for reports - Admin API: Return `total` when querying for reports
- Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`) - Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`)
- Admin API: Return link alongside with token on password reset - Admin API: Return link alongside with token on password reset
@ -37,6 +38,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: Mark the direct conversation as read for the author when they send a new direct message - Mastodon API: Mark the direct conversation as read for the author when they send a new direct message
- Mastodon API, streaming: Add `pleroma.direct_conversation_id` to the `conversation` stream event payload. - Mastodon API, streaming: Add `pleroma.direct_conversation_id` to the `conversation` stream event payload.
- Admin API: Render whole status in grouped reports - Admin API: Render whole status in grouped reports
- Mastodon API: User timelines will now respect blocks, unless you are getting the user timeline of somebody you blocked (which would be empty otherwise).
</details> </details>
### Added ### Added
@ -49,6 +51,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache). - Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache).
- MRF: New module which handles incoming posts based on their age. By default, all incoming posts that are older than 2 days will be unlisted and not shown to their followers. - MRF: New module which handles incoming posts based on their age. By default, all incoming posts that are older than 2 days will be unlisted and not shown to their followers.
- User notification settings: Add `privacy_option` option. - User notification settings: Add `privacy_option` option.
- User settings: Add _This account is a_ option.
- OAuth: admin scopes support (relevant setting: `[:auth, :enforce_oauth_admin_scope_usage]`).
<details> <details>
<summary>API Changes</summary> <summary>API Changes</summary>
@ -77,6 +81,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Pleroma API: Add Emoji reactions - Pleroma API: Add Emoji reactions
- Admin API: Add `/api/pleroma/admin/instances/:instance/statuses` - lists all statuses from a given instance - Admin API: Add `/api/pleroma/admin/instances/:instance/statuses` - lists all statuses from a given instance
- Admin API: `PATCH /api/pleroma/users/confirm_email` to confirm email for multiple users, `PATCH /api/pleroma/users/resend_confirmation_email` to resend confirmation email for multiple users - Admin API: `PATCH /api/pleroma/users/confirm_email` to confirm email for multiple users, `PATCH /api/pleroma/users/resend_confirmation_email` to resend confirmation email for multiple users
- ActivityPub: Configurable `type` field of the actors.
- Mastodon API: `/api/v1/accounts/:id` has `source/pleroma/actor_type` field.
- Mastodon API: `/api/v1/update_credentials` accepts `actor_type` field.
- Captcha: Support native provider
- Captcha: Enable by default
</details> </details>
### Fixed ### Fixed
@ -85,6 +94,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- MRF: `Delete` activities being exempt from MRF policies - MRF: `Delete` activities being exempt from MRF policies
- OTP releases: Not being able to configure OAuth expired token cleanup interval - OTP releases: Not being able to configure OAuth expired token cleanup interval
- OTP releases: Not being able to configure HTML sanitization policy - OTP releases: Not being able to configure HTML sanitization policy
- Favorites timeline now ordered by favorite date instead of post date
<details> <details>
<summary>API Changes</summary> <summary>API Changes</summary>

View file

@ -52,9 +52,9 @@
migration_lock: nil migration_lock: nil
config :pleroma, Pleroma.Captcha, config :pleroma, Pleroma.Captcha,
enabled: false, enabled: true,
seconds_valid: 60, seconds_valid: 60,
method: Pleroma.Captcha.Kocaptcha method: Pleroma.Captcha.Native
config :pleroma, :hackney_pools, config :pleroma, :hackney_pools,
federation: [ federation: [
@ -70,8 +70,6 @@
timeout: 300_000 timeout: 300_000
] ]
config :pleroma, Pleroma.Captcha.Kocaptcha, endpoint: "https://captcha.kotobank.ch"
# Upload configuration # Upload configuration
config :pleroma, Pleroma.Upload, config :pleroma, Pleroma.Upload,
uploader: Pleroma.Uploaders.Local, uploader: Pleroma.Uploaders.Local,
@ -555,7 +553,10 @@
base_path: "/oauth", base_path: "/oauth",
providers: ueberauth_providers providers: ueberauth_providers
config :pleroma, :auth, oauth_consumer_strategies: oauth_consumer_strategies config :pleroma,
:auth,
enforce_oauth_admin_scope_usage: false,
oauth_consumer_strategies: oauth_consumer_strategies
config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Sendmail, enabled: false config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Sendmail, enabled: false

View file

@ -2094,6 +2094,15 @@
type: :group, type: :group,
description: "Authentication / authorization settings", description: "Authentication / authorization settings",
children: [ children: [
%{
key: :enforce_oauth_admin_scope_usage,
type: :boolean,
description:
"OAuth admin scope requirement toggle. " <>
"If `true`, admin actions explicitly demand admin OAuth scope(s) presence in OAuth token " <>
"(client app must support admin scopes). If `false` and token doesn't have admin scope(s)," <>
"`is_admin` user flag grants access to admin-specific actions."
},
%{ %{
key: :auth_template, key: :auth_template,
type: :string, type: :string,

View file

@ -91,6 +91,8 @@
config :pleroma, Pleroma.ReverseProxy.Client, Pleroma.ReverseProxy.ClientMock config :pleroma, Pleroma.ReverseProxy.Client, Pleroma.ReverseProxy.ClientMock
config :pleroma, Pleroma.Captcha.Kocaptcha, endpoint: "https://captcha.kotobank.ch"
if File.exists?("./config/test.secret.exs") do if File.exists?("./config/test.secret.exs") do
import_config "test.secret.exs" import_config "test.secret.exs"
else else

View file

@ -2,6 +2,13 @@
Authentication is required and the user must be an admin. Authentication is required and the user must be an admin.
Configuration options:
* `[:auth, :enforce_oauth_admin_scope_usage]` — OAuth admin scope requirement toggle.
If `true`, admin actions explicitly demand admin OAuth scope(s) presence in OAuth token (client app must support admin scopes).
If `false` and token doesn't have admin scope(s), `is_admin` user flag grants access to admin-specific actions.
Note that client app needs to explicitly support admin scopes and request them when obtaining auth token.
## `GET /api/pleroma/admin/users` ## `GET /api/pleroma/admin/users`
### List users ### List users
@ -607,78 +614,29 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- On success: `204`, empty response - On success: `204`, empty response
## `POST /api/pleroma/admin/reports/:id/respond` ## `POST /api/pleroma/admin/reports/:id/notes`
### Respond to a report ### Create report note
- Params: - Params:
- `id` - `id`: required, report id
- `status`: required, the message - `content`: required, the message
- Response: - Response:
- On failure: - On failure:
- 400 Bad Request `"Invalid parameters"` when `status` is missing - 400 Bad Request `"Invalid parameters"` when `status` is missing
- 403 Forbidden `{"error": "error_msg"}` - On success: `204`, empty response
- 404 Not Found `"Not found"`
- On success: JSON, created Mastodon Status entity
```json ## `POST /api/pleroma/admin/reports/:report_id/notes/:id`
{
"account": { ... }, ### Delete report note
"application": {
"name": "Web", - Params:
"website": null - `report_id`: required, report id
}, - `id`: required, note id
"bookmarked": false, - Response:
"card": null, - On failure:
"content": "Your claim is going to be closed", - 400 Bad Request `"Invalid parameters"` when `status` is missing
"created_at": "2019-05-11T17:13:03.000Z", - On success: `204`, empty response
"emojis": [],
"favourited": false,
"favourites_count": 0,
"id": "9ihuiSL1405I65TmEq",
"in_reply_to_account_id": null,
"in_reply_to_id": null,
"language": null,
"media_attachments": [],
"mentions": [
{
"acct": "user",
"id": "9i6dAJqSGSKMzLG2Lo",
"url": "https://pleroma.example.org/users/user",
"username": "user"
},
{
"acct": "admin",
"id": "9hEkA5JsvAdlSrocam",
"url": "https://pleroma.example.org/users/admin",
"username": "admin"
}
],
"muted": false,
"pinned": false,
"pleroma": {
"content": {
"text/plain": "Your claim is going to be closed"
},
"conversation_id": 35,
"in_reply_to_account_acct": null,
"local": true,
"spoiler_text": {
"text/plain": ""
}
},
"reblog": null,
"reblogged": false,
"reblogs_count": 0,
"replies_count": 0,
"sensitive": false,
"spoiler_text": "",
"tags": [],
"uri": "https://pleroma.example.org/objects/cab0836d-9814-46cd-a0ea-529da9db5fcb",
"url": "https://pleroma.example.org/notice/9ihuiSL1405I65TmEq",
"visibility": "direct"
}
```
## `PUT /api/pleroma/admin/statuses/:id` ## `PUT /api/pleroma/admin/statuses/:id`

View file

@ -66,6 +66,8 @@ Has these additional fields under the `pleroma` object:
- `show_role`: boolean, nullable, true when the user wants his role (e.g admin, moderator) to be shown - `show_role`: boolean, nullable, true when the user wants his role (e.g admin, moderator) to be shown
- `no_rich_text` - boolean, nullable, true when html tags are stripped from all statuses requested from the API - `no_rich_text` - boolean, nullable, true when html tags are stripped from all statuses requested from the API
- `discoverable`: boolean, true when the user allows discovery of the account in search results and other services.
- `actor_type`: string, the type of this account.
## Conversations ## Conversations
@ -146,6 +148,8 @@ Additional parameters can be added to the JSON body/Form data:
- `skip_thread_containment` - if true, skip filtering out broken threads - `skip_thread_containment` - if true, skip filtering out broken threads
- `allow_following_move` - if true, allows automatically follow moved following accounts - `allow_following_move` - if true, allows automatically follow moved following accounts
- `pleroma_background_image` - sets the background image of the user. - `pleroma_background_image` - sets the background image of the user.
- `discoverable` - if true, discovery of this account in search results and other services is allowed.
- `actor_type` - the type of this account.
### Pleroma Settings Store ### Pleroma Settings Store
Pleroma has mechanism that allows frontends to save blobs of json for each user on the backend. This can be used to save frontend-specific settings for a user that the backend does not need to know about. Pleroma has mechanism that allows frontends to save blobs of json for each user on the backend. This can be used to save frontend-specific settings for a user that the backend does not need to know about.

View file

@ -379,13 +379,19 @@ For each pool, the options are:
## Captcha ## Captcha
### Pleroma.Captcha ### Pleroma.Captcha
* `enabled`: Whether the captcha should be shown on registration. * `enabled`: Whether the captcha should be shown on registration.
* `method`: The method/service to use for captcha. * `method`: The method/service to use for captcha.
* `seconds_valid`: The time in seconds for which the captcha is valid. * `seconds_valid`: The time in seconds for which the captcha is valid.
### Captcha providers ### Captcha providers
#### Pleroma.Captcha.Native
A built-in captcha provider. Enabled by default.
#### Pleroma.Captcha.Kocaptcha #### Pleroma.Captcha.Kocaptcha
Kocaptcha is a very simple captcha service with a single API endpoint, Kocaptcha is a very simple captcha service with a single API endpoint,
the source code is here: https://github.com/koto-bank/kocaptcha. The default endpoint the source code is here: https://github.com/koto-bank/kocaptcha. The default endpoint
`https://captcha.kotobank.ch` is hosted by the developer. `https://captcha.kotobank.ch` is hosted by the developer.

View file

@ -52,7 +52,9 @@ def run(["migrate_from_db", env, delete?]) do
|> Enum.each(fn config -> |> Enum.each(fn config ->
IO.write( IO.write(
file, file,
"config :#{config.group}, #{config.key}, #{inspect(Config.from_binary(config.value))}\r\n\r\n" "config :#{config.group}, #{config.key}, #{
inspect(Config.from_binary(config.value), limit: :infinity)
}\r\n\r\n"
) )
if delete? do if delete? do

View file

@ -8,7 +8,6 @@ defmodule Mix.Tasks.Pleroma.User do
alias Ecto.Changeset alias Ecto.Changeset
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserInviteToken alias Pleroma.UserInviteToken
alias Pleroma.Web.OAuth
@shortdoc "Manages Pleroma users" @shortdoc "Manages Pleroma users"
@moduledoc File.read!("docs/administration/CLI_tasks/user.md") @moduledoc File.read!("docs/administration/CLI_tasks/user.md")
@ -354,8 +353,7 @@ def run(["sign_out", nickname]) do
start_pleroma() start_pleroma()
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
OAuth.Token.delete_user_tokens(user) User.global_sign_out(user)
OAuth.Authorization.delete_user_authorizations(user)
shell_info("#{nickname} signed out from all apps.") shell_info("#{nickname} signed out from all apps.")
else else
@ -393,10 +391,7 @@ defp set_moderator(user, value) do
end end
defp set_admin(user, value) do defp set_admin(user, value) do
{:ok, user} = {:ok, user} = User.admin_api_update(user, %{is_admin: value})
user
|> Changeset.change(%{is_admin: value})
|> User.update_and_set_cache()
shell_info("Admin status of #{user.nickname}: #{user.is_admin}") shell_info("Admin status of #{user.nickname}: #{user.is_admin}")
user user

View file

@ -12,6 +12,7 @@ defmodule Pleroma.Activity do
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.ReportNote
alias Pleroma.ThreadMute alias Pleroma.ThreadMute
alias Pleroma.User alias Pleroma.User
@ -48,6 +49,8 @@ defmodule Pleroma.Activity do
has_one(:user_actor, User, on_delete: :nothing, foreign_key: :id) has_one(:user_actor, User, on_delete: :nothing, foreign_key: :id)
# This is a fake relation, do not use outside of with_preloaded_bookmark/get_bookmark # This is a fake relation, do not use outside of with_preloaded_bookmark/get_bookmark
has_one(:bookmark, Bookmark) has_one(:bookmark, Bookmark)
# This is a fake relation, do not use outside of with_preloaded_report_notes
has_many(:report_notes, ReportNote)
has_many(:notifications, Notification, on_delete: :delete_all) has_many(:notifications, Notification, on_delete: :delete_all)
# Attention: this is a fake relation, don't try to preload it blindly and expect it to work! # Attention: this is a fake relation, don't try to preload it blindly and expect it to work!
@ -114,6 +117,16 @@ def with_preloaded_bookmark(query, %User{} = user) do
def with_preloaded_bookmark(query, _), do: query def with_preloaded_bookmark(query, _), do: query
def with_preloaded_report_notes(query) do
from([a] in query,
left_join: r in ReportNote,
on: a.id == r.activity_id,
preload: [report_notes: r]
)
end
def with_preloaded_report_notes(query, _), do: query
def with_set_thread_muted_field(query, %User{} = user) do def with_set_thread_muted_field(query, %User{} = user) do
from([a] in query, from([a] in query,
left_join: tm in ThreadMute, left_join: tm in ThreadMute,

View file

@ -86,7 +86,7 @@ defp maybe_fetch(activities, user, search_query) do
{:ok, object} <- Fetcher.fetch_object_from_id(search_query), {:ok, object} <- Fetcher.fetch_object_from_id(search_query),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
true <- Visibility.visible_for_user?(activity, user) do true <- Visibility.visible_for_user?(activity, user) do
activities ++ [activity] [activity | activities]
else else
_ -> activities _ -> activities
end end

View file

@ -0,0 +1,35 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Captcha.Native do
import Pleroma.Web.Gettext
alias Pleroma.Captcha.Service
@behaviour Service
@impl Service
def new do
case Captcha.get() do
{:timeout} ->
%{error: dgettext("errors", "Captcha timeout")}
{:ok, answer_data, img_binary} ->
%{
type: :native,
token: token(),
url: "data:image/png;base64," <> Base.encode64(img_binary),
answer_data: answer_data
}
end
end
@impl Service
def validate(_token, captcha, captcha) when not is_nil(captcha), do: :ok
def validate(_token, _captcha, _answer), do: {:error, dgettext("errors", "Invalid CAPTCHA")}
defp token do
10
|> :crypto.strong_rand_bytes()
|> Base.url_encode64(padding: false)
end
end

View file

@ -65,4 +65,16 @@ def delete(key) do
def oauth_consumer_strategies, do: get([:auth, :oauth_consumer_strategies], []) def oauth_consumer_strategies, do: get([:auth, :oauth_consumer_strategies], [])
def oauth_consumer_enabled?, do: oauth_consumer_strategies() != [] def oauth_consumer_enabled?, do: oauth_consumer_strategies() != []
def enforce_oauth_admin_scope_usage?, do: !!get([:auth, :enforce_oauth_admin_scope_usage])
def oauth_admin_scopes(scopes) when is_list(scopes) do
Enum.flat_map(
scopes,
fn scope ->
["admin:#{scope}"] ++
if enforce_oauth_admin_scope_usage?(), do: [], else: [scope]
end
)
end
end end

View file

@ -128,17 +128,35 @@ def insert_log(%{
{:ok, ModerationLog} | {:error, any} {:ok, ModerationLog} | {:error, any}
def insert_log(%{ def insert_log(%{
actor: %User{} = actor, actor: %User{} = actor,
action: "report_response", action: "report_note",
subject: %Activity{} = subject, subject: %Activity{} = subject,
text: text text: text
}) do }) do
%ModerationLog{ %ModerationLog{
data: %{ data: %{
"actor" => user_to_map(actor), "actor" => user_to_map(actor),
"action" => "report_response", "action" => "report_note",
"subject" => report_to_map(subject), "subject" => report_to_map(subject),
"text" => text, "text" => text
"message" => "" }
}
|> insert_log_entry_with_message()
end
@spec insert_log(%{actor: User, subject: Activity, action: String.t(), text: String.t()}) ::
{:ok, ModerationLog} | {:error, any}
def insert_log(%{
actor: %User{} = actor,
action: "report_note_delete",
subject: %Activity{} = subject,
text: text
}) do
%ModerationLog{
data: %{
"actor" => user_to_map(actor),
"action" => "report_note_delete",
"subject" => report_to_map(subject),
"text" => text
} }
} }
|> insert_log_entry_with_message() |> insert_log_entry_with_message()
@ -556,12 +574,24 @@ def get_log_entry_message(%ModerationLog{
def get_log_entry_message(%ModerationLog{ def get_log_entry_message(%ModerationLog{
data: %{ data: %{
"actor" => %{"nickname" => actor_nickname}, "actor" => %{"nickname" => actor_nickname},
"action" => "report_response", "action" => "report_note",
"subject" => %{"id" => subject_id, "type" => "report"}, "subject" => %{"id" => subject_id, "type" => "report"},
"text" => text "text" => text
} }
}) do }) do
"@#{actor_nickname} responded with '#{text}' to report ##{subject_id}" "@#{actor_nickname} added note '#{text}' to report ##{subject_id}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "report_note_delete",
"subject" => %{"id" => subject_id, "type" => "report"},
"text" => text
}
}) do
"@#{actor_nickname} deleted note '#{text}' from report ##{subject_id}"
end end
@spec get_log_entry_message(ModerationLog) :: String.t() @spec get_log_entry_message(ModerationLog) :: String.t()

View file

@ -23,6 +23,23 @@ defmodule Pleroma.Object do
timestamps() timestamps()
end end
def with_joined_activity(query, activity_type \\ "Create", join_type \\ :inner) do
object_position = Map.get(query.aliases, :object, 0)
join(query, join_type, [{object, object_position}], a in Activity,
on:
fragment(
"COALESCE(?->'object'->>'id', ?->>'object') = (? ->> 'id') AND (?->>'type' = ?) ",
a.data,
a.data,
object.data,
a.data,
^activity_type
),
as: :object_activity
)
end
def create(data) do def create(data) do
Object.change(%Object{}, %{data: data}) Object.change(%Object{}, %{data: data})
|> Repo.insert() |> Repo.insert()

View file

@ -13,60 +13,66 @@ defmodule Pleroma.Pagination do
alias Pleroma.Repo alias Pleroma.Repo
@default_limit 20 @default_limit 20
@page_keys ["max_id", "min_id", "limit", "since_id", "order"]
def fetch_paginated(query, params, type \\ :keyset) def page_keys, do: @page_keys
def fetch_paginated(query, %{"total" => true} = params, :keyset) do def fetch_paginated(query, params, type \\ :keyset, table_binding \\ nil)
def fetch_paginated(query, %{"total" => true} = params, :keyset, table_binding) do
total = Repo.aggregate(query, :count, :id) total = Repo.aggregate(query, :count, :id)
%{ %{
total: total, total: total,
items: fetch_paginated(query, Map.drop(params, ["total"]), :keyset) items: fetch_paginated(query, Map.drop(params, ["total"]), :keyset, table_binding)
} }
end end
def fetch_paginated(query, params, :keyset) do def fetch_paginated(query, params, :keyset, table_binding) do
options = cast_params(params) options = cast_params(params)
query query
|> paginate(options, :keyset) |> paginate(options, :keyset, table_binding)
|> Repo.all() |> Repo.all()
|> enforce_order(options) |> enforce_order(options)
end end
def fetch_paginated(query, %{"total" => true} = params, :offset) do def fetch_paginated(query, %{"total" => true} = params, :offset, table_binding) do
total = Repo.aggregate(query, :count, :id) total =
query
|> Ecto.Query.exclude(:left_join)
|> Repo.aggregate(:count, :id)
%{ %{
total: total, total: total,
items: fetch_paginated(query, Map.drop(params, ["total"]), :offset) items: fetch_paginated(query, Map.drop(params, ["total"]), :offset, table_binding)
} }
end end
def fetch_paginated(query, params, :offset) do def fetch_paginated(query, params, :offset, table_binding) do
options = cast_params(params) options = cast_params(params)
query query
|> paginate(options, :offset) |> paginate(options, :offset, table_binding)
|> Repo.all() |> Repo.all()
end end
def paginate(query, options, method \\ :keyset) def paginate(query, options, method \\ :keyset, table_binding \\ nil)
def paginate(query, options, :keyset) do def paginate(query, options, :keyset, table_binding) do
query query
|> restrict(:min_id, options) |> restrict(:min_id, options, table_binding)
|> restrict(:since_id, options) |> restrict(:since_id, options, table_binding)
|> restrict(:max_id, options) |> restrict(:max_id, options, table_binding)
|> restrict(:order, options) |> restrict(:order, options, table_binding)
|> restrict(:limit, options) |> restrict(:limit, options, table_binding)
end end
def paginate(query, options, :offset) do def paginate(query, options, :offset, table_binding) do
query query
|> restrict(:order, options) |> restrict(:order, options, table_binding)
|> restrict(:offset, options) |> restrict(:offset, options, table_binding)
|> restrict(:limit, options) |> restrict(:limit, options, table_binding)
end end
defp cast_params(params) do defp cast_params(params) do
@ -75,7 +81,8 @@ defp cast_params(params) do
since_id: :string, since_id: :string,
max_id: :string, max_id: :string,
offset: :integer, offset: :integer,
limit: :integer limit: :integer,
skip_order: :boolean
} }
params = params =
@ -88,38 +95,48 @@ defp cast_params(params) do
changeset.changes changeset.changes
end end
defp restrict(query, :min_id, %{min_id: min_id}) do defp restrict(query, :min_id, %{min_id: min_id}, table_binding) do
where(query, [q], q.id > ^min_id) where(query, [{q, table_position(query, table_binding)}], q.id > ^min_id)
end end
defp restrict(query, :since_id, %{since_id: since_id}) do defp restrict(query, :since_id, %{since_id: since_id}, table_binding) do
where(query, [q], q.id > ^since_id) where(query, [{q, table_position(query, table_binding)}], q.id > ^since_id)
end end
defp restrict(query, :max_id, %{max_id: max_id}) do defp restrict(query, :max_id, %{max_id: max_id}, table_binding) do
where(query, [q], q.id < ^max_id) where(query, [{q, table_position(query, table_binding)}], q.id < ^max_id)
end end
defp restrict(query, :order, %{min_id: _}) do defp restrict(query, :order, %{skip_order: true}, _), do: query
order_by(query, [u], fragment("? asc nulls last", u.id))
defp restrict(query, :order, %{min_id: _}, table_binding) do
order_by(
query,
[{u, table_position(query, table_binding)}],
fragment("? asc nulls last", u.id)
)
end end
defp restrict(query, :order, _options) do defp restrict(query, :order, _options, table_binding) do
order_by(query, [u], fragment("? desc nulls last", u.id)) order_by(
query,
[{u, table_position(query, table_binding)}],
fragment("? desc nulls last", u.id)
)
end end
defp restrict(query, :offset, %{offset: offset}) do defp restrict(query, :offset, %{offset: offset}, _table_binding) do
offset(query, ^offset) offset(query, ^offset)
end end
defp restrict(query, :limit, options) do defp restrict(query, :limit, options, _table_binding) do
limit = Map.get(options, :limit, @default_limit) limit = Map.get(options, :limit, @default_limit)
query query
|> limit(^limit) |> limit(^limit)
end end
defp restrict(query, _, _), do: query defp restrict(query, _, _, _), do: query
defp enforce_order(result, %{min_id: _}) do defp enforce_order(result, %{min_id: _}) do
result result
@ -127,4 +144,10 @@ defp enforce_order(result, %{min_id: _}) do
end end
defp enforce_order(result, _), do: result defp enforce_order(result, _), do: result
defp table_position(%Ecto.Query{} = query, binding_name) do
Map.get(query.aliases, binding_name, 0)
end
defp table_position(_, _), do: 0
end end

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Plugs.OAuthScopesPlug do
import Plug.Conn import Plug.Conn
import Pleroma.Web.Gettext import Pleroma.Web.Gettext
alias Pleroma.Config
alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug
@behaviour Plug @behaviour Plug
@ -15,6 +16,8 @@ def init(%{scopes: _} = options), do: options
def call(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do def call(%Plug.Conn{assigns: assigns} = conn, %{scopes: scopes} = options) do
op = options[:op] || :| op = options[:op] || :|
token = assigns[:token] token = assigns[:token]
scopes = transform_scopes(scopes, options)
matched_scopes = token && filter_descendants(scopes, token.scopes) matched_scopes = token && filter_descendants(scopes, token.scopes)
cond do cond do
@ -60,6 +63,15 @@ def filter_descendants(scopes, supported_scopes) do
) )
end end
@doc "Transforms scopes by applying supported options (e.g. :admin)"
def transform_scopes(scopes, options) do
if options[:admin] do
Config.oauth_admin_scopes(scopes)
else
scopes
end
end
defp maybe_perform_instance_privacy_check(%Plug.Conn{} = conn, options) do defp maybe_perform_instance_privacy_check(%Plug.Conn{} = conn, options) do
if options[:skip_instance_privacy_check] do if options[:skip_instance_privacy_check] do
conn conn

View file

@ -5,19 +5,38 @@
defmodule Pleroma.Plugs.UserIsAdminPlug do defmodule Pleroma.Plugs.UserIsAdminPlug do
import Pleroma.Web.TranslationHelpers import Pleroma.Web.TranslationHelpers
import Plug.Conn import Plug.Conn
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.OAuth
def init(options) do def init(options) do
options options
end end
def call(%{assigns: %{user: %User{is_admin: true}}} = conn, _) do def call(%{assigns: %{user: %User{is_admin: true}} = assigns} = conn, _) do
conn token = assigns[:token]
cond do
not Pleroma.Config.enforce_oauth_admin_scope_usage?() ->
conn
token && OAuth.Scopes.contains_admin_scopes?(token.scopes) ->
# Note: checking for _any_ admin scope presence, not necessarily fitting requested action.
# Thus, controller must explicitly invoke OAuthScopesPlug to verify scope requirements.
conn
true ->
fail(conn)
end
end end
def call(conn, _) do def call(conn, _) do
fail(conn)
end
defp fail(conn) do
conn conn
|> render_error(:forbidden, "User is not admin.") |> render_error(:forbidden, "User is not an admin or OAuth admin scope is not granted.")
|> halt |> halt()
end end
end end

View file

@ -0,0 +1,48 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ReportNote do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
alias Pleroma.Activity
alias Pleroma.Repo
alias Pleroma.ReportNote
alias Pleroma.User
@type t :: %__MODULE__{}
schema "report_notes" do
field(:content, :string)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
timestamps()
end
@spec create(FlakeId.Ecto.CompatType.t(), FlakeId.Ecto.CompatType.t(), String.t()) ::
{:ok, ReportNote.t()} | {:error, Changeset.t()}
def create(user_id, activity_id, content) do
attrs = %{
user_id: user_id,
activity_id: activity_id,
content: content
}
%ReportNote{}
|> cast(attrs, [:user_id, :activity_id, :content])
|> validate_required([:user_id, :activity_id, :content])
|> Repo.insert()
end
@spec destroy(FlakeId.Ecto.CompatType.t()) ::
{:ok, ReportNote.t()} | {:error, Changeset.t()}
def destroy(id) do
from(r in ReportNote, where: r.id == ^id)
|> Repo.one()
|> Repo.delete()
end
end

View file

@ -127,6 +127,7 @@ defmodule Pleroma.User do
field(:invisible, :boolean, default: false) field(:invisible, :boolean, default: false)
field(:allow_following_move, :boolean, default: true) field(:allow_following_move, :boolean, default: true)
field(:skip_thread_containment, :boolean, default: false) field(:skip_thread_containment, :boolean, default: false)
field(:actor_type, :string, default: "Person")
field(:also_known_as, {:array, :string}, default: []) field(:also_known_as, {:array, :string}, default: [])
embeds_one( embeds_one(
@ -346,6 +347,7 @@ def remote_user_creation(params) do
:following_count, :following_count,
:discoverable, :discoverable,
:invisible, :invisible,
:actor_type,
:also_known_as :also_known_as
] ]
) )
@ -396,6 +398,7 @@ def update_changeset(struct, params \\ %{}) do
:raw_fields, :raw_fields,
:pleroma_settings_store, :pleroma_settings_store,
:discoverable, :discoverable,
:actor_type,
:also_known_as :also_known_as
] ]
) )
@ -438,6 +441,7 @@ def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
:discoverable, :discoverable,
:hide_followers_count, :hide_followers_count,
:hide_follows_count, :hide_follows_count,
:actor_type,
:also_known_as :also_known_as
] ]
) )
@ -858,6 +862,13 @@ def get_friends(user, page \\ nil) do
|> Repo.all() |> Repo.all()
end end
def get_friends_ap_ids(user) do
user
|> get_friends_query(nil)
|> select([u], u.ap_id)
|> Repo.all()
end
def get_friends_ids(user, page \\ nil) do def get_friends_ids(user, page \\ nil) do
user user
|> get_friends_query(page) |> get_friends_query(page)
@ -1132,7 +1143,8 @@ def muted_notifications?(%User{} = user, %User{} = target),
def blocks?(nil, _), do: false def blocks?(nil, _), do: false
def blocks?(%User{} = user, %User{} = target) do def blocks?(%User{} = user, %User{} = target) do
blocks_user?(user, target) || blocks_domain?(user, target) blocks_user?(user, target) ||
(!User.following?(user, target) && blocks_domain?(user, target))
end end
def blocks_user?(%User{} = user, %User{} = target) do def blocks_user?(%User{} = user, %User{} = target) do
@ -1835,13 +1847,28 @@ defp truncate_field(%{"name" => name, "value" => value}) do
end end
def admin_api_update(user, params) do def admin_api_update(user, params) do
user changeset =
|> cast(params, [ cast(user, params, [
:is_moderator, :is_moderator,
:is_admin, :is_admin,
:show_role :show_role
]) ])
|> update_and_set_cache()
with {:ok, updated_user} <- update_and_set_cache(changeset) do
if user.is_admin && !updated_user.is_admin do
# Tokens & authorizations containing any admin scopes must be revoked (revoking all).
# This is an extra safety measure (tokens' admin scopes won't be accepted for non-admins).
global_sign_out(user)
end
{:ok, updated_user}
end
end
@doc "Signs user out of all applications"
def global_sign_out(user) do
OAuth.Authorization.delete_user_authorizations(user)
OAuth.Token.delete_user_tokens(user)
end end
def mascot_update(user, url) do def mascot_update(user, url) do

View file

@ -950,6 +950,8 @@ defp restrict_blocked(query, %{"blocking_user" => %User{} = user} = opts) do
blocked_ap_ids = opts["blocked_users_ap_ids"] || User.blocked_users_ap_ids(user) blocked_ap_ids = opts["blocked_users_ap_ids"] || User.blocked_users_ap_ids(user)
domain_blocks = user.domain_blocks || [] domain_blocks = user.domain_blocks || []
following_ap_ids = User.get_friends_ap_ids(user)
query = query =
if has_named_binding?(query, :object), do: query, else: Activity.with_joined_object(query) if has_named_binding?(query, :object), do: query, else: Activity.with_joined_object(query)
@ -964,8 +966,22 @@ defp restrict_blocked(query, %{"blocking_user" => %User{} = user} = opts) do
activity.data, activity.data,
^blocked_ap_ids ^blocked_ap_ids
), ),
where: fragment("not (split_part(?, '/', 3) = ANY(?))", activity.actor, ^domain_blocks), where:
where: fragment("not (split_part(?->>'actor', '/', 3) = ANY(?))", o.data, ^domain_blocks) fragment(
"(not (split_part(?, '/', 3) = ANY(?))) or ? = ANY(?)",
activity.actor,
^domain_blocks,
activity.actor,
^following_ap_ids
),
where:
fragment(
"(not (split_part(?->>'actor', '/', 3) = ANY(?))) or (?->>'actor') = ANY(?)",
o.data,
^domain_blocks,
o.data,
^following_ap_ids
)
) )
end end
@ -1052,6 +1068,13 @@ defp maybe_preload_bookmarks(query, opts) do
|> Activity.with_preloaded_bookmark(opts["user"]) |> Activity.with_preloaded_bookmark(opts["user"])
end end
defp maybe_preload_report_notes(query, %{"preload_report_notes" => true}) do
query
|> Activity.with_preloaded_report_notes()
end
defp maybe_preload_report_notes(query, _), do: query
defp maybe_set_thread_muted_field(query, %{"skip_preload" => true}), do: query defp maybe_set_thread_muted_field(query, %{"skip_preload" => true}), do: query
defp maybe_set_thread_muted_field(query, opts) do defp maybe_set_thread_muted_field(query, opts) do
@ -1105,6 +1128,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
Activity Activity
|> maybe_preload_objects(opts) |> maybe_preload_objects(opts)
|> maybe_preload_bookmarks(opts) |> maybe_preload_bookmarks(opts)
|> maybe_preload_report_notes(opts)
|> maybe_set_thread_muted_field(opts) |> maybe_set_thread_muted_field(opts)
|> maybe_order(opts) |> maybe_order(opts)
|> restrict_recipients(recipients, opts["user"]) |> restrict_recipients(recipients, opts["user"])
@ -1141,6 +1165,25 @@ def fetch_activities(recipients, opts \\ %{}, pagination \\ :keyset) do
|> maybe_update_cc(list_memberships, opts["user"]) |> maybe_update_cc(list_memberships, opts["user"])
end end
@doc """
Fetch favorites activities of user with order by sort adds to favorites
"""
@spec fetch_favourites(User.t(), map(), atom()) :: list(Activity.t())
def fetch_favourites(user, params \\ %{}, pagination \\ :keyset) do
user.ap_id
|> Activity.Queries.by_actor()
|> Activity.Queries.by_type("Like")
|> Activity.with_joined_object()
|> Object.with_joined_activity()
|> select([_like, object, activity], %{activity | object: object})
|> order_by([like, _, _], desc: like.id)
|> Pagination.fetch_paginated(
Map.merge(params, %{"skip_order" => true}),
pagination,
:object_activity
)
end
defp maybe_update_cc(activities, list_memberships, %User{ap_id: user_ap_id}) defp maybe_update_cc(activities, list_memberships, %User{ap_id: user_ap_id})
when is_list(list_memberships) and length(list_memberships) > 0 do when is_list(list_memberships) and length(list_memberships) > 0 do
Enum.map(activities, fn Enum.map(activities, fn
@ -1217,6 +1260,7 @@ defp object_to_user_data(data) do
data = Transmogrifier.maybe_fix_user_object(data) data = Transmogrifier.maybe_fix_user_object(data)
discoverable = data["discoverable"] || false discoverable = data["discoverable"] || false
invisible = data["invisible"] || false invisible = data["invisible"] || false
actor_type = data["type"] || "Person"
user_data = %{ user_data = %{
ap_id: data["id"], ap_id: data["id"],
@ -1232,6 +1276,7 @@ defp object_to_user_data(data) do
follower_address: data["followers"], follower_address: data["followers"],
following_address: data["following"], following_address: data["following"],
bio: data["summary"], bio: data["summary"],
actor_type: actor_type,
also_known_as: Map.get(data, "alsoKnownAs", []) also_known_as: Map.get(data, "alsoKnownAs", [])
} }

View file

@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
alias Pleroma.HTTP alias Pleroma.HTTP
alias Pleroma.Instances alias Pleroma.Instances
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
@ -188,31 +189,35 @@ def publish(%User{} = actor, %{data: %{"bcc" => bcc}} = activity)
recipients = recipients(actor, activity) recipients = recipients(actor, activity)
recipients inboxes =
|> Enum.filter(&User.ap_enabled?/1) recipients
|> Enum.map(fn %{source_data: data} -> data["inbox"] end) |> Enum.filter(&User.ap_enabled?/1)
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end) |> Enum.map(fn %{source_data: data} -> data["inbox"] end)
|> Instances.filter_reachable() |> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
|> Enum.each(fn {inbox, unreachable_since} -> |> Instances.filter_reachable()
%User{ap_id: ap_id} =
Enum.find(recipients, fn %{source_data: data} -> data["inbox"] == inbox end)
# Get all the recipients on the same host and add them to cc. Otherwise, a remote Repo.checkout(fn ->
# instance would only accept a first message for the first recipient and ignore the rest. Enum.each(inboxes, fn {inbox, unreachable_since} ->
cc = get_cc_ap_ids(ap_id, recipients) %User{ap_id: ap_id} =
Enum.find(recipients, fn %{source_data: data} -> data["inbox"] == inbox end)
json = # Get all the recipients on the same host and add them to cc. Otherwise, a remote
data # instance would only accept a first message for the first recipient and ignore the rest.
|> Map.put("cc", cc) cc = get_cc_ap_ids(ap_id, recipients)
|> Jason.encode!()
Pleroma.Web.Federator.Publisher.enqueue_one(__MODULE__, %{ json =
inbox: inbox, data
json: json, |> Map.put("cc", cc)
actor_id: actor.id, |> Jason.encode!()
id: activity.data["id"],
unreachable_since: unreachable_since Pleroma.Web.Federator.Publisher.enqueue_one(__MODULE__, %{
}) inbox: inbox,
json: json,
actor_id: actor.id,
id: activity.data["id"],
unreachable_since: unreachable_since
})
end)
end) end)
end end

View file

@ -787,6 +787,7 @@ def get_reports(params, page, page_size) do
params params
|> Map.put("type", "Flag") |> Map.put("type", "Flag")
|> Map.put("skip_preload", true) |> Map.put("skip_preload", true)
|> Map.put("preload_report_notes", true)
|> Map.put("total", true) |> Map.put("total", true)
|> Map.put("limit", page_size) |> Map.put("limit", page_size)
|> Map.put("offset", (page - 1) * page_size) |> Map.put("offset", (page - 1) * page_size)

View file

@ -91,7 +91,7 @@ def render("user.json", %{user: user}) do
%{ %{
"id" => user.ap_id, "id" => user.ap_id,
"type" => "Person", "type" => user.actor_type,
"following" => "#{user.ap_id}/following", "following" => "#{user.ap_id}/following",
"followers" => "#{user.ap_id}/followers", "followers" => "#{user.ap_id}/followers",
"inbox" => "#{user.ap_id}/inbox", "inbox" => "#{user.ap_id}/inbox",

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.ModerationLog alias Pleroma.ModerationLog
alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.ReportNote
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserInviteToken alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
@ -30,13 +31,13 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["read:accounts"]} %{scopes: ["read:accounts"], admin: true}
when action in [:list_users, :user_show, :right_get, :invites] when action in [:list_users, :user_show, :right_get, :invites]
) )
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["write:accounts"]} %{scopes: ["write:accounts"], admin: true}
when action in [ when action in [
:get_invite_token, :get_invite_token,
:revoke_invite, :revoke_invite,
@ -58,35 +59,37 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["read:reports"]} when action in [:list_reports, :report_show] %{scopes: ["read:reports"], admin: true}
when action in [:list_reports, :report_show]
) )
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["write:reports"]} %{scopes: ["write:reports"], admin: true}
when action in [:report_update_state, :report_respond] when action in [:report_update_state, :report_respond]
) )
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["read:statuses"]} when action == :list_user_statuses %{scopes: ["read:statuses"], admin: true}
when action == :list_user_statuses
) )
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["write:statuses"]} %{scopes: ["write:statuses"], admin: true}
when action in [:status_update, :status_delete] when action in [:status_update, :status_delete]
) )
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["read"]} %{scopes: ["read"], admin: true}
when action in [:config_show, :migrate_to_db, :migrate_from_db, :list_log] when action in [:config_show, :migrate_to_db, :migrate_from_db, :list_log]
) )
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["write"]} %{scopes: ["write"], admin: true}
when action in [:relay_follow, :relay_unfollow, :config_update] when action in [:relay_follow, :relay_unfollow, :config_update]
) )
@ -238,7 +241,7 @@ def list_instance_statuses(conn, %{"instance" => instance} = params) do
}) })
conn conn
|> put_view(StatusView) |> put_view(Pleroma.Web.AdminAPI.StatusView)
|> render("index.json", %{activities: activities, as: :activity}) |> render("index.json", %{activities: activities, as: :activity})
end end
@ -641,9 +644,11 @@ def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nic
def list_reports(conn, params) do def list_reports(conn, params) do
{page, page_size} = page_params(params) {page, page_size} = page_params(params)
reports = Utils.get_reports(params, page, page_size)
conn conn
|> put_view(ReportView) |> put_view(ReportView)
|> render("index.json", %{reports: Utils.get_reports(params, page, page_size)}) |> render("index.json", %{reports: reports})
end end
def list_grouped_reports(conn, _params) do def list_grouped_reports(conn, _params) do
@ -687,32 +692,39 @@ def reports_update(%{assigns: %{user: admin}} = conn, %{"reports" => reports}) d
end end
end end
def report_respond(%{assigns: %{user: user}} = conn, %{"id" => id} = params) do def report_notes_create(%{assigns: %{user: user}} = conn, %{
with false <- is_nil(params["status"]), "id" => report_id,
%Activity{} <- Activity.get_by_id(id) do "content" => content
params = }) do
params with {:ok, _} <- ReportNote.create(user.id, report_id, content) do
|> Map.put("in_reply_to_status_id", id)
|> Map.put("visibility", "direct")
{:ok, activity} = CommonAPI.post(user, params)
ModerationLog.insert_log(%{ ModerationLog.insert_log(%{
action: "report_response", action: "report_note",
actor: user, actor: user,
subject: activity, subject: Activity.get_by_id(report_id),
text: params["status"] text: content
}) })
conn json_response(conn, :no_content, "")
|> put_view(StatusView)
|> render("show.json", %{activity: activity})
else else
true -> _ -> json_response(conn, :bad_request, "")
{:param_cast, nil} end
end
nil -> def report_notes_delete(%{assigns: %{user: user}} = conn, %{
{:error, :not_found} "id" => note_id,
"report_id" => report_id
}) do
with {:ok, note} <- ReportNote.destroy(note_id) do
ModerationLog.insert_log(%{
action: "report_note_delete",
actor: user,
subject: Activity.get_by_id(report_id),
text: note.content
})
json_response(conn, :no_content, "")
else
_ -> json_response(conn, :bad_request, "")
end end
end end

View file

@ -39,7 +39,8 @@ def render("show.json", %{report: report, user: user, account: account, statuses
content: content, content: content,
created_at: created_at, created_at: created_at,
statuses: StatusView.render("index.json", %{activities: statuses, as: :activity}), statuses: StatusView.render("index.json", %{activities: statuses, as: :activity}),
state: report.data["state"] state: report.data["state"],
notes: render(__MODULE__, "index_notes.json", %{notes: report.report_notes})
} }
end end
@ -69,6 +70,28 @@ def render("index_grouped.json", %{groups: groups}) do
} }
end end
def render("index_notes.json", %{notes: notes}) when is_list(notes) do
Enum.map(notes, &render(__MODULE__, "show_note.json", &1))
end
def render("index_notes.json", _), do: []
def render("show_note.json", %{
id: id,
content: content,
user_id: user_id,
inserted_at: inserted_at
}) do
user = User.get_by_id(user_id)
%{
id: id,
content: content,
user: merge_account_views(user),
created_at: Utils.to_masto_date(inserted_at)
}
end
defp merge_account_views(%User{} = user) do defp merge_account_views(%User{} = user) do
Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user}) Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user})
|> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user})) |> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user}))

View file

@ -0,0 +1,42 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.StatusView do
use Pleroma.Web, :view
require Pleroma.Constants
alias Pleroma.User
def render("index.json", opts) do
render_many(opts.activities, __MODULE__, "show.json", opts)
end
def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
user = get_user(activity.data["actor"])
Pleroma.Web.MastodonAPI.StatusView.render("show.json", opts)
|> Map.merge(%{account: merge_account_views(user)})
end
defp merge_account_views(%User{} = user) do
Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user})
|> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user}))
end
defp merge_account_views(_), do: %{}
defp get_user(ap_id) do
cond do
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
end

View file

@ -188,6 +188,7 @@ def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
{:ok, Map.merge(user.pleroma_settings_store, value)} {:ok, Map.merge(user.pleroma_settings_store, value)}
end) end)
|> add_if_present(params, "default_scope", :default_scope) |> add_if_present(params, "default_scope", :default_scope)
|> add_if_present(params, "actor_type", :actor_type)
emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "") emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")

View file

@ -349,15 +349,11 @@ def context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
@doc "GET /api/v1/favourites" @doc "GET /api/v1/favourites"
def favourites(%{assigns: %{user: user}} = conn, params) do def favourites(%{assigns: %{user: user}} = conn, params) do
params =
params
|> Map.put("type", "Create")
|> Map.put("favorited_by", user.ap_id)
|> Map.put("blocking_user", user)
activities = activities =
ActivityPub.fetch_activities([], params) ActivityPub.fetch_favourites(
|> Enum.reverse() user,
Map.take(params, Pleroma.Pagination.page_keys())
)
conn conn
|> add_link_headers(activities) |> add_link_headers(activities)

View file

@ -86,7 +86,7 @@ defp do_render("show.json", %{user: user} = opts) do
0 0
end end
bot = (user.source_data["type"] || "Person") in ["Application", "Service"] bot = user.actor_type in ["Application", "Service"]
emojis = emojis =
(user.source_data["tag"] || []) (user.source_data["tag"] || [])
@ -137,7 +137,8 @@ defp do_render("show.json", %{user: user} = opts) do
sensitive: false, sensitive: false,
fields: user.raw_fields, fields: user.raw_fields,
pleroma: %{ pleroma: %{
discoverable: user.discoverable discoverable: user.discoverable,
actor_type: user.actor_type
} }
}, },

View file

@ -222,7 +222,7 @@ def token_exchange(
{:user_active, true} <- {:user_active, !user.deactivated}, {:user_active, true} <- {:user_active, !user.deactivated},
{:password_reset_pending, false} <- {:password_reset_pending, false} <-
{:password_reset_pending, user.password_reset_pending}, {:password_reset_pending, user.password_reset_pending},
{:ok, scopes} <- validate_scopes(app, params), {:ok, scopes} <- validate_scopes(app, params, user),
{:ok, auth} <- Authorization.create_authorization(app, user, scopes), {:ok, auth} <- Authorization.create_authorization(app, user, scopes),
{:ok, token} <- Token.exchange_token(app, auth) do {:ok, token} <- Token.exchange_token(app, auth) do
json(conn, Token.Response.build(user, token)) json(conn, Token.Response.build(user, token))
@ -471,7 +471,7 @@ defp do_create_authorization(
{:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)}, {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)},
%App{} = app <- Repo.get_by(App, client_id: client_id), %App{} = app <- Repo.get_by(App, client_id: client_id),
true <- redirect_uri in String.split(app.redirect_uris), true <- redirect_uri in String.split(app.redirect_uris),
{:ok, scopes} <- validate_scopes(app, auth_attrs), {:ok, scopes} <- validate_scopes(app, auth_attrs, user),
{:auth_active, true} <- {:auth_active, User.auth_active?(user)} do {:auth_active, true} <- {:auth_active, User.auth_active?(user)} do
Authorization.create_authorization(app, user, scopes) Authorization.create_authorization(app, user, scopes)
end end
@ -487,12 +487,12 @@ defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :re
defp put_session_registration_id(%Plug.Conn{} = conn, registration_id), defp put_session_registration_id(%Plug.Conn{} = conn, registration_id),
do: put_session(conn, :registration_id, registration_id) do: put_session(conn, :registration_id, registration_id)
@spec validate_scopes(App.t(), map()) :: @spec validate_scopes(App.t(), map(), User.t()) ::
{:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
defp validate_scopes(app, params) do defp validate_scopes(%App{} = app, params, %User{} = user) do
params params
|> Scopes.fetch_scopes(app.scopes) |> Scopes.fetch_scopes(app.scopes)
|> Scopes.validate(app.scopes) |> Scopes.validate(app.scopes, user)
end end
def default_redirect_uri(%App{} = app) do def default_redirect_uri(%App{} = app) do

View file

@ -7,6 +7,9 @@ defmodule Pleroma.Web.OAuth.Scopes do
Functions for dealing with scopes. Functions for dealing with scopes.
""" """
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
@doc """ @doc """
Fetch scopes from request params. Fetch scopes from request params.
@ -53,15 +56,38 @@ def to_string(scopes), do: Enum.join(scopes, " ")
@doc """ @doc """
Validates scopes. Validates scopes.
""" """
@spec validate(list() | nil, list()) :: @spec validate(list() | nil, list(), User.t()) ::
{:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes}
def validate([], _app_scopes), do: {:error, :missing_scopes} def validate(blank_scopes, _app_scopes, _user) when blank_scopes in [nil, []],
def validate(nil, _app_scopes), do: {:error, :missing_scopes} do: {:error, :missing_scopes}
def validate(scopes, app_scopes) do def validate(scopes, app_scopes, %User{} = user) do
case Pleroma.Plugs.OAuthScopesPlug.filter_descendants(scopes, app_scopes) do with {:ok, _} <- ensure_scopes_support(scopes, app_scopes),
{:ok, scopes} <- authorize_admin_scopes(scopes, app_scopes, user) do
{:ok, scopes}
end
end
defp ensure_scopes_support(scopes, app_scopes) do
case OAuthScopesPlug.filter_descendants(scopes, app_scopes) do
^scopes -> {:ok, scopes} ^scopes -> {:ok, scopes}
_ -> {:error, :unsupported_scopes} _ -> {:error, :unsupported_scopes}
end end
end end
defp authorize_admin_scopes(scopes, app_scopes, %User{} = user) do
if user.is_admin || !contains_admin_scopes?(scopes) || !contains_admin_scopes?(app_scopes) do
{:ok, scopes}
else
# Gracefully dropping admin scopes from requested scopes if user isn't an admin (not raising)
scopes = scopes -- OAuthScopesPlug.filter_descendants(scopes, ["admin"])
validate(scopes, app_scopes, user)
end
end
def contains_admin_scopes?(scopes) do
scopes
|> OAuthScopesPlug.filter_descendants(["admin"])
|> Enum.any?()
end
end end

View file

@ -7,7 +7,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["write"]} %{scopes: ["write"], admin: true}
when action in [ when action in [
:create, :create,
:delete, :delete,

View file

@ -187,7 +187,8 @@ defmodule Pleroma.Web.Router do
get("/grouped_reports", AdminAPIController, :list_grouped_reports) get("/grouped_reports", AdminAPIController, :list_grouped_reports)
get("/reports/:id", AdminAPIController, :report_show) get("/reports/:id", AdminAPIController, :report_show)
patch("/reports", AdminAPIController, :reports_update) patch("/reports", AdminAPIController, :reports_update)
post("/reports/:id/respond", AdminAPIController, :report_respond) post("/reports/:id/notes", AdminAPIController, :report_notes_create)
delete("/reports/:report_id/notes/:id", AdminAPIController, :report_notes_delete)
put("/statuses/:id", AdminAPIController, :status_update) put("/statuses/:id", AdminAPIController, :status_update)
delete("/statuses/:id", AdminAPIController, :status_delete) delete("/statuses/:id", AdminAPIController, :status_delete)
@ -528,7 +529,10 @@ defmodule Pleroma.Web.Router do
get("/users/:nickname/feed", Feed.FeedController, :feed) get("/users/:nickname/feed", Feed.FeedController, :feed)
get("/users/:nickname", Feed.FeedController, :feed_redirect) get("/users/:nickname", Feed.FeedController, :feed_redirect)
end
scope "/", Pleroma.Web do
pipe_through(:browser)
get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe) get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe)
end end

View file

@ -162,6 +162,9 @@ defp deps do
{:remote_ip, {:remote_ip,
git: "https://git.pleroma.social/pleroma/remote_ip.git", git: "https://git.pleroma.social/pleroma/remote_ip.git",
ref: "825dc00aaba5a1b7c4202a532b696b595dd3bcb3"}, ref: "825dc00aaba5a1b7c4202a532b696b595dd3bcb3"},
{:captcha,
git: "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git",
ref: "c3c795c55f6b49d79d6ac70a0f91e525099fc3e2"},
{:mox, "~> 0.5", only: :test} {:mox, "~> 0.5", only: :test}
] ++ oauth_deps() ] ++ oauth_deps()
end end

View file

@ -8,6 +8,7 @@
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"},
"cachex": {:hex, :cachex, "3.0.3", "4e2d3e05814a5738f5ff3903151d5c25636d72a3527251b753f501ad9c657967", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm"}, "cachex": {:hex, :cachex, "3.0.3", "4e2d3e05814a5738f5ff3903151d5c25636d72a3527251b753f501ad9c657967", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm"},
"calendar": {:hex, :calendar, "0.17.6", "ec291cb2e4ba499c2e8c0ef5f4ace974e2f9d02ae9e807e711a9b0c7850b9aee", [:mix], [{:tzdata, "~> 0.5.20 or ~> 0.1.201603 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, "calendar": {:hex, :calendar, "0.17.6", "ec291cb2e4ba499c2e8c0ef5f4ace974e2f9d02ae9e807e711a9b0c7850b9aee", [:mix], [{:tzdata, "~> 0.5.20 or ~> 0.1.201603 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
"captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "c3c795c55f6b49d79d6ac70a0f91e525099fc3e2", [ref: "c3c795c55f6b49d79d6ac70a0f91e525099fc3e2"]},
"certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"},
"comeonin": {:hex, :comeonin, "4.1.2", "3eb5620fd8e35508991664b4c2b04dd41e52f1620b36957be837c1d7784b7592", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm"}, "comeonin": {:hex, :comeonin, "4.1.2", "3eb5620fd8e35508991664b4c2b04dd41e52f1620b36957be837c1d7784b7592", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm"},

View file

@ -0,0 +1,9 @@
defmodule Pleroma.Repo.Migrations.AddActivitypubActorType do
use Ecto.Migration
def change do
alter table("users") do
add(:actor_type, :string, null: false, default: "Person")
end
end
end

View file

@ -0,0 +1,13 @@
defmodule Pleroma.Repo.Migrations.CreateReportNotes do
use Ecto.Migration
def change do
create_if_not_exists table(:report_notes) do
add(:user_id, references(:users, type: :uuid))
add(:activity_id, references(:activities, type: :uuid))
add(:content, :string)
timestamps()
end
end
end

View file

@ -1 +1 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1,user-scalable=no"><title>Pleroma</title><!--server-generated-meta--><link rel=icon type=image/png href=/favicon.png><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/vendors~app.b2603a50868c68a1c192.css rel=stylesheet><link href=/static/css/app.fd71461124f3eb029b1b.css rel=stylesheet></head><body class=hidden><noscript>To use Pleroma, please enable JavaScript.</noscript><div id=app></div><script type=text/javascript src=/static/js/vendors~app.76db8e4cdf29decd5cab.js></script><script type=text/javascript src=/static/js/app.d20ca27d22d74eb7bce0.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 href=/static/css/vendors~app.b2603a50868c68a1c192.css rel=stylesheet><link href=/static/css/app.ae04505b31bb0ee2765e.css rel=stylesheet><link href=/static/fontello.1576166651574.css rel=stylesheet></head><body class=hidden><noscript>To use Pleroma, please enable JavaScript.</noscript><div id=app></div><script type=text/javascript src=/static/js/vendors~app.3f1ed7a4fdfc37ee27a7.js></script><script type=text/javascript src=/static/js/app.a9b3f4c3e79baf3fa8b7.js></script></body></html>

View file

@ -99,4 +99,4 @@ .with-subscription-loading .error {
font-size: 14px; font-size: 14px;
} }
/*# sourceMappingURL=app.fd71461124f3eb029b1b.css.map*/ /*# sourceMappingURL=app.ae04505b31bb0ee2765e.css.map*/

View file

@ -1 +1 @@
{"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;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;AClFA;AACA;AACA;AACA;AACA;AACA;AACA,C","file":"static/css/app.fd71461124f3eb029b1b.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 {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n.tab-switcher .contents {\n -ms-flex: 1 0 auto;\n flex: 1 0 auto;\n min-height: 0px;\n}\n.tab-switcher .contents .hidden {\n display: none;\n}\n.tab-switcher .contents.scrollable-tabs {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n overflow-y: auto;\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-subscription-loading {\n padding: 10px;\n text-align: center;\n}\n.with-subscription-loading .error {\n font-size: 14px;\n}"],"sourceRoot":""} {"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;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;AClFA;AACA;AACA;AACA;AACA;AACA;AACA,C","file":"static/css/app.ae04505b31bb0ee2765e.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 {\n display: -ms-flexbox;\n display: flex;\n -ms-flex-direction: column;\n flex-direction: column;\n}\n.tab-switcher .contents {\n -ms-flex: 1 0 auto;\n flex: 1 0 auto;\n min-height: 0px;\n}\n.tab-switcher .contents .hidden {\n display: none;\n}\n.tab-switcher .contents.scrollable-tabs {\n -ms-flex-preferred-size: 0;\n flex-basis: 0;\n overflow-y: auto;\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-subscription-loading {\n padding: 10px;\n text-align: center;\n}\n.with-subscription-loading .error {\n font-size: 14px;\n}"],"sourceRoot":""}

View file

@ -1,39 +0,0 @@
Font license info
## Font Awesome
Copyright (C) 2016 by Dave Gandy
Author: Dave Gandy
License: SIL ()
Homepage: http://fortawesome.github.com/Font-Awesome/
## Entypo
Copyright (C) 2012 by Daniel Bruce
Author: Daniel Bruce
License: SIL (http://scripts.sil.org/OFL)
Homepage: http://www.entypo.com
## Iconic
Copyright (C) 2012 by P.J. Onori
Author: P.J. Onori
License: SIL (http://scripts.sil.org/OFL)
Homepage: http://somerandomdude.com/work/iconic/
## Fontelico
Copyright (C) 2012 by Fontello project
Author: Crowdsourced, for Fontello project
License: SIL (http://scripts.sil.org/OFL)
Homepage: http://fontello.com

View file

@ -1,75 +0,0 @@
This webfont is generated by http://fontello.com open source project.
================================================================================
Please, note, that you should obey original font licenses, used to make this
webfont pack. Details available in LICENSE.txt file.
- Usually, it's enough to publish content of LICENSE.txt file somewhere on your
site in "About" section.
- If your project is open-source, usually, it will be ok to make LICENSE.txt
file publicly available in your repository.
- Fonts, used in Fontello, don't require a clickable link on your site.
But any kind of additional authors crediting is welcome.
================================================================================
Comments on archive content
---------------------------
- /font/* - fonts in different formats
- /css/* - different kinds of css, for all situations. Should be ok with
twitter bootstrap. Also, you can skip <i> style and assign icon classes
directly to text elements, if you don't mind about IE7.
- demo.html - demo file, to show your webfont content
- LICENSE.txt - license info about source fonts, used to build your one.
- config.json - keeps your settings. You can import it back into fontello
anytime, to continue your work
Why so many CSS files ?
-----------------------
Because we like to fit all your needs :)
- basic file, <your_font_name>.css - is usually enough, it contains @font-face
and character code definitions
- *-ie7.css - if you need IE7 support, but still don't wish to put char codes
directly into html
- *-codes.css and *-ie7-codes.css - if you like to use your own @font-face
rules, but still wish to benefit from css generation. That can be very
convenient for automated asset build systems. When you need to update font -
no need to manually edit files, just override old version with archive
content. See fontello source code for examples.
- *-embedded.css - basic css file, but with embedded WOFF font, to avoid
CORS issues in Firefox and IE9+, when fonts are hosted on the separate domain.
We strongly recommend to resolve this issue by `Access-Control-Allow-Origin`
server headers. But if you ok with dirty hack - this file is for you. Note,
that data url moved to separate @font-face to avoid problems with <IE9, when
string is too long.
- animate.css - use it to get ideas about spinner rotation animation.
Attention for server setup
--------------------------
You MUST setup server to reply with proper `mime-types` for font files -
otherwise some browsers will fail to show fonts.
Usually, `apache` already has necessary settings, but `nginx` and other
webservers should be tuned. Here is list of mime types for our file extensions:
- `application/vnd.ms-fontobject` - eot
- `application/x-font-woff` - woff
- `application/x-font-ttf` - ttf
- `image/svg+xml` - svg

View file

@ -1,85 +0,0 @@
/*
Animation example, for spinners
*/
.animate-spin {
-moz-animation: spin 2s infinite linear;
-o-animation: spin 2s infinite linear;
-webkit-animation: spin 2s infinite linear;
animation: spin 2s infinite linear;
display: inline-block;
}
@-moz-keyframes spin {
0% {
-moz-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(359deg);
-o-transform: rotate(359deg);
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@-webkit-keyframes spin {
0% {
-moz-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(359deg);
-o-transform: rotate(359deg);
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@-o-keyframes spin {
0% {
-moz-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(359deg);
-o-transform: rotate(359deg);
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@-ms-keyframes spin {
0% {
-moz-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(359deg);
-o-transform: rotate(359deg);
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@keyframes spin {
0% {
-moz-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(359deg);
-o-transform: rotate(359deg);
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}

View file

@ -1,48 +0,0 @@
.icon-cancel:before { content: '\e800'; } /* '' */
.icon-upload:before { content: '\e801'; } /* '' */
.icon-star:before { content: '\e802'; } /* '' */
.icon-star-empty:before { content: '\e803'; } /* '' */
.icon-retweet:before { content: '\e804'; } /* '' */
.icon-eye-off:before { content: '\e805'; } /* '' */
.icon-search:before { content: '\e806'; } /* '' */
.icon-cog:before { content: '\e807'; } /* '' */
.icon-logout:before { content: '\e808'; } /* '' */
.icon-down-open:before { content: '\e809'; } /* '' */
.icon-attach:before { content: '\e80a'; } /* '' */
.icon-picture:before { content: '\e80b'; } /* '' */
.icon-video:before { content: '\e80c'; } /* '' */
.icon-right-open:before { content: '\e80d'; } /* '' */
.icon-left-open:before { content: '\e80e'; } /* '' */
.icon-up-open:before { content: '\e80f'; } /* '' */
.icon-bell-ringing-o:before { content: '\e810'; } /* '' */
.icon-lock:before { content: '\e811'; } /* '' */
.icon-globe:before { content: '\e812'; } /* '' */
.icon-brush:before { content: '\e813'; } /* '' */
.icon-attention:before { content: '\e814'; } /* '' */
.icon-plus:before { content: '\e815'; } /* '' */
.icon-adjust:before { content: '\e816'; } /* '' */
.icon-edit:before { content: '\e817'; } /* '' */
.icon-pencil:before { content: '\e818'; } /* '' */
.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'; } /* '' */
.icon-link-ext-alt:before { content: '\f08f'; } /* '' */
.icon-menu:before { content: '\f0c9'; } /* '' */
.icon-mail-alt:before { content: '\f0e0'; } /* '' */
.icon-gauge:before { content: '\f0e4'; } /* '' */
.icon-comment-empty:before { content: '\f0e5'; } /* '' */
.icon-bell-alt:before { content: '\f0f3'; } /* '' */
.icon-plus-squared:before { content: '\f0fe'; } /* '' */
.icon-reply:before { content: '\f112'; } /* '' */
.icon-smile:before { content: '\f118'; } /* '' */
.icon-lock-open-alt:before { content: '\f13e'; } /* '' */
.icon-ellipsis:before { content: '\f141'; } /* '' */
.icon-play-circled:before { content: '\f144'; } /* '' */
.icon-thumbs-up-alt:before { content: '\f164'; } /* '' */
.icon-binoculars:before { content: '\f1e5'; } /* '' */
.icon-user-plus:before { content: '\f234'; } /* '' */

File diff suppressed because one or more lines are too long

View file

@ -1,48 +0,0 @@
.icon-cancel { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe800;&nbsp;'); }
.icon-upload { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe801;&nbsp;'); }
.icon-star { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe802;&nbsp;'); }
.icon-star-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe803;&nbsp;'); }
.icon-retweet { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe804;&nbsp;'); }
.icon-eye-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe805;&nbsp;'); }
.icon-search { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe806;&nbsp;'); }
.icon-cog { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe807;&nbsp;'); }
.icon-logout { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe808;&nbsp;'); }
.icon-down-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe809;&nbsp;'); }
.icon-attach { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80a;&nbsp;'); }
.icon-picture { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80b;&nbsp;'); }
.icon-video { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80c;&nbsp;'); }
.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-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;'); }
.icon-attention { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe814;&nbsp;'); }
.icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe815;&nbsp;'); }
.icon-adjust { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe816;&nbsp;'); }
.icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe817;&nbsp;'); }
.icon-pencil { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe818;&nbsp;'); }
.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;'); }
.icon-link-ext-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08f;&nbsp;'); }
.icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0c9;&nbsp;'); }
.icon-mail-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0e0;&nbsp;'); }
.icon-gauge { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0e4;&nbsp;'); }
.icon-comment-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0e5;&nbsp;'); }
.icon-bell-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0f3;&nbsp;'); }
.icon-plus-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0fe;&nbsp;'); }
.icon-reply { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf112;&nbsp;'); }
.icon-smile { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf118;&nbsp;'); }
.icon-lock-open-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf13e;&nbsp;'); }
.icon-ellipsis { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf141;&nbsp;'); }
.icon-play-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf144;&nbsp;'); }
.icon-thumbs-up-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf164;&nbsp;'); }
.icon-binoculars { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf1e5;&nbsp;'); }
.icon-user-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf234;&nbsp;'); }

View file

@ -1,59 +0,0 @@
[class^="icon-"], [class*=" icon-"] {
font-family: 'fontello';
font-style: normal;
font-weight: normal;
/* fix buttons height */
line-height: 1em;
/* you can be more comfortable with increased icons size */
/* font-size: 120%; */
}
.icon-cancel { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe800;&nbsp;'); }
.icon-upload { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe801;&nbsp;'); }
.icon-star { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe802;&nbsp;'); }
.icon-star-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe803;&nbsp;'); }
.icon-retweet { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe804;&nbsp;'); }
.icon-eye-off { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe805;&nbsp;'); }
.icon-search { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe806;&nbsp;'); }
.icon-cog { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe807;&nbsp;'); }
.icon-logout { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe808;&nbsp;'); }
.icon-down-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe809;&nbsp;'); }
.icon-attach { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80a;&nbsp;'); }
.icon-picture { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80b;&nbsp;'); }
.icon-video { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80c;&nbsp;'); }
.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-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;'); }
.icon-attention { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe814;&nbsp;'); }
.icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe815;&nbsp;'); }
.icon-adjust { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe816;&nbsp;'); }
.icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe817;&nbsp;'); }
.icon-pencil { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe818;&nbsp;'); }
.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;'); }
.icon-link-ext-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08f;&nbsp;'); }
.icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0c9;&nbsp;'); }
.icon-mail-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0e0;&nbsp;'); }
.icon-gauge { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0e4;&nbsp;'); }
.icon-comment-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0e5;&nbsp;'); }
.icon-bell-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0f3;&nbsp;'); }
.icon-plus-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0fe;&nbsp;'); }
.icon-reply { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf112;&nbsp;'); }
.icon-smile { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf118;&nbsp;'); }
.icon-lock-open-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf13e;&nbsp;'); }
.icon-ellipsis { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf141;&nbsp;'); }
.icon-play-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf144;&nbsp;'); }
.icon-thumbs-up-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf164;&nbsp;'); }
.icon-binoculars { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf1e5;&nbsp;'); }
.icon-user-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf234;&nbsp;'); }

View file

@ -1,104 +0,0 @@
@font-face {
font-family: 'fontello';
src: url('../font/fontello.eot?70867224');
src: url('../font/fontello.eot?70867224#iefix') format('embedded-opentype'),
url('../font/fontello.woff2?70867224') format('woff2'),
url('../font/fontello.woff?70867224') format('woff'),
url('../font/fontello.ttf?70867224') format('truetype'),
url('../font/fontello.svg?70867224#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
/*
@media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face {
font-family: 'fontello';
src: url('../font/fontello.svg?70867224#fontello') format('svg');
}
}
*/
[class^="icon-"]:before, [class*=" icon-"]:before {
font-family: "fontello";
font-style: normal;
font-weight: normal;
speak: none;
display: inline-block;
text-decoration: inherit;
width: 1em;
margin-right: .2em;
text-align: center;
/* opacity: .8; */
/* For safety - reset parent styles, that can break glyph codes*/
font-variant: normal;
text-transform: none;
/* fix buttons height, for twitter bootstrap */
line-height: 1em;
/* Animation center compensation - margins should be symmetric */
/* remove if not needed */
margin-left: .2em;
/* you can be more comfortable with increased icons size */
/* font-size: 120%; */
/* Font smoothing. That was taken from TWBS */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* Uncomment for 3D effect */
/* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
}
.icon-cancel:before { content: '\e800'; } /* '' */
.icon-upload:before { content: '\e801'; } /* '' */
.icon-star:before { content: '\e802'; } /* '' */
.icon-star-empty:before { content: '\e803'; } /* '' */
.icon-retweet:before { content: '\e804'; } /* '' */
.icon-eye-off:before { content: '\e805'; } /* '' */
.icon-search:before { content: '\e806'; } /* '' */
.icon-cog:before { content: '\e807'; } /* '' */
.icon-logout:before { content: '\e808'; } /* '' */
.icon-down-open:before { content: '\e809'; } /* '' */
.icon-attach:before { content: '\e80a'; } /* '' */
.icon-picture:before { content: '\e80b'; } /* '' */
.icon-video:before { content: '\e80c'; } /* '' */
.icon-right-open:before { content: '\e80d'; } /* '' */
.icon-left-open:before { content: '\e80e'; } /* '' */
.icon-up-open:before { content: '\e80f'; } /* '' */
.icon-bell-ringing-o:before { content: '\e810'; } /* '' */
.icon-lock:before { content: '\e811'; } /* '' */
.icon-globe:before { content: '\e812'; } /* '' */
.icon-brush:before { content: '\e813'; } /* '' */
.icon-attention:before { content: '\e814'; } /* '' */
.icon-plus:before { content: '\e815'; } /* '' */
.icon-adjust:before { content: '\e816'; } /* '' */
.icon-edit:before { content: '\e817'; } /* '' */
.icon-pencil:before { content: '\e818'; } /* '' */
.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'; } /* '' */
.icon-link-ext-alt:before { content: '\f08f'; } /* '' */
.icon-menu:before { content: '\f0c9'; } /* '' */
.icon-mail-alt:before { content: '\f0e0'; } /* '' */
.icon-gauge:before { content: '\f0e4'; } /* '' */
.icon-comment-empty:before { content: '\f0e5'; } /* '' */
.icon-bell-alt:before { content: '\f0f3'; } /* '' */
.icon-plus-squared:before { content: '\f0fe'; } /* '' */
.icon-reply:before { content: '\f112'; } /* '' */
.icon-smile:before { content: '\f118'; } /* '' */
.icon-lock-open-alt:before { content: '\f13e'; } /* '' */
.icon-ellipsis:before { content: '\f141'; } /* '' */
.icon-play-circled:before { content: '\f144'; } /* '' */
.icon-thumbs-up-alt:before { content: '\f164'; } /* '' */
.icon-binoculars:before { content: '\f1e5'; } /* '' */
.icon-user-plus:before { content: '\f234'; } /* '' */

View file

@ -1,374 +0,0 @@
<!DOCTYPE html>
<html>
<head><!--[if lt IE 9]><script language="javascript" type="text/javascript" src="//html5shim.googlecode.com/svn/trunk/html5.js"></script><![endif]-->
<meta charset="UTF-8"><style>/*
* Bootstrap v2.2.1
*
* Copyright 2012 Twitter, Inc
* Licensed under the Apache License v2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
* Designed and built with all the love in the world @twitter by @mdo and @fat.
*/
.clearfix {
*zoom: 1;
}
.clearfix:before,
.clearfix:after {
display: table;
content: "";
line-height: 0;
}
.clearfix:after {
clear: both;
}
html {
font-size: 100%;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
a:focus {
outline: thin dotted #333;
outline: 5px auto -webkit-focus-ring-color;
outline-offset: -2px;
}
a:hover,
a:active {
outline: 0;
}
button,
input,
select,
textarea {
margin: 0;
font-size: 100%;
vertical-align: middle;
}
button,
input {
*overflow: visible;
line-height: normal;
}
button::-moz-focus-inner,
input::-moz-focus-inner {
padding: 0;
border: 0;
}
body {
margin: 0;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 20px;
color: #333;
background-color: #fff;
}
a {
color: #08c;
text-decoration: none;
}
a:hover {
color: #005580;
text-decoration: underline;
}
.row {
margin-left: -20px;
*zoom: 1;
}
.row:before,
.row:after {
display: table;
content: "";
line-height: 0;
}
.row:after {
clear: both;
}
[class*="span"] {
float: left;
min-height: 1px;
margin-left: 20px;
}
.container,
.navbar-static-top .container,
.navbar-fixed-top .container,
.navbar-fixed-bottom .container {
width: 940px;
}
.span12 {
width: 940px;
}
.span11 {
width: 860px;
}
.span10 {
width: 780px;
}
.span9 {
width: 700px;
}
.span8 {
width: 620px;
}
.span7 {
width: 540px;
}
.span6 {
width: 460px;
}
.span5 {
width: 380px;
}
.span4 {
width: 300px;
}
.span3 {
width: 220px;
}
.span2 {
width: 140px;
}
.span1 {
width: 60px;
}
[class*="span"].pull-right,
.row-fluid [class*="span"].pull-right {
float: right;
}
.container {
margin-right: auto;
margin-left: auto;
*zoom: 1;
}
.container:before,
.container:after {
display: table;
content: "";
line-height: 0;
}
.container:after {
clear: both;
}
p {
margin: 0 0 10px;
}
.lead {
margin-bottom: 20px;
font-size: 21px;
font-weight: 200;
line-height: 30px;
}
small {
font-size: 85%;
}
h1 {
margin: 10px 0;
font-family: inherit;
font-weight: bold;
line-height: 20px;
color: inherit;
text-rendering: optimizelegibility;
}
h1 small {
font-weight: normal;
line-height: 1;
color: #999;
}
h1 {
line-height: 40px;
}
h1 {
font-size: 38.5px;
}
h1 small {
font-size: 24.5px;
}
body {
margin-top: 90px;
}
.header {
position: fixed;
top: 0;
left: 50%;
margin-left: -480px;
background-color: #fff;
border-bottom: 1px solid #ddd;
padding-top: 10px;
z-index: 10;
}
.footer {
color: #ddd;
font-size: 12px;
text-align: center;
margin-top: 20px;
}
.footer a {
color: #ccc;
text-decoration: underline;
}
.the-icons {
font-size: 14px;
line-height: 24px;
}
.switch {
position: absolute;
right: 0;
bottom: 10px;
color: #666;
}
.switch input {
margin-right: 0.3em;
}
.codesOn .i-name {
display: none;
}
.codesOn .i-code {
display: inline;
}
.i-code {
display: none;
}
@font-face {
font-family: 'fontello';
src: url('./font/fontello.eot?56851497');
src: url('./font/fontello.eot?56851497#iefix') format('embedded-opentype'),
url('./font/fontello.woff?56851497') format('woff'),
url('./font/fontello.ttf?56851497') format('truetype'),
url('./font/fontello.svg?56851497#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
.demo-icon
{
font-family: "fontello";
font-style: normal;
font-weight: normal;
speak: none;
display: inline-block;
text-decoration: inherit;
width: 1em;
margin-right: .2em;
text-align: center;
/* opacity: .8; */
/* For safety - reset parent styles, that can break glyph codes*/
font-variant: normal;
text-transform: none;
/* fix buttons height, for twitter bootstrap */
line-height: 1em;
/* Animation center compensation - margins should be symmetric */
/* remove if not needed */
margin-left: .2em;
/* You can be more comfortable with increased icons size */
/* font-size: 120%; */
/* Font smoothing. That was taken from TWBS */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* Uncomment for 3D effect */
/* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
}
</style>
<link rel="stylesheet" href="css/animation.css"><!--[if IE 7]><link rel="stylesheet" href="css/" + font.fontname + "-ie7.css"><![endif]-->
<script>
function toggleCodes(on) {
var obj = document.getElementById('icons');
if (on) {
obj.className += ' codesOn';
} else {
obj.className = obj.className.replace(' codesOn', '');
}
}
</script>
</head>
<body>
<div class="container header">
<h1>fontello <small>font demo</small></h1>
<label class="switch">
<input type="checkbox" onclick="toggleCodes(this.checked)">show codes
</label>
</div>
<div class="container" id="icons">
<div class="row">
<div class="the-icons span3" title="Code: 0xe800"><i class="demo-icon icon-cancel">&#xe800;</i> <span class="i-name">icon-cancel</span><span class="i-code">0xe800</span></div>
<div class="the-icons span3" title="Code: 0xe801"><i class="demo-icon icon-upload">&#xe801;</i> <span class="i-name">icon-upload</span><span class="i-code">0xe801</span></div>
<div class="the-icons span3" title="Code: 0xe802"><i class="demo-icon icon-star">&#xe802;</i> <span class="i-name">icon-star</span><span class="i-code">0xe802</span></div>
<div class="the-icons span3" title="Code: 0xe803"><i class="demo-icon icon-star-empty">&#xe803;</i> <span class="i-name">icon-star-empty</span><span class="i-code">0xe803</span></div>
</div>
<div class="row">
<div class="the-icons span3" title="Code: 0xe804"><i class="demo-icon icon-retweet">&#xe804;</i> <span class="i-name">icon-retweet</span><span class="i-code">0xe804</span></div>
<div class="the-icons span3" title="Code: 0xe805"><i class="demo-icon icon-eye-off">&#xe805;</i> <span class="i-name">icon-eye-off</span><span class="i-code">0xe805</span></div>
<div class="the-icons span3" title="Code: 0xe806"><i class="demo-icon icon-search">&#xe806;</i> <span class="i-name">icon-search</span><span class="i-code">0xe806</span></div>
<div class="the-icons span3" title="Code: 0xe807"><i class="demo-icon icon-cog">&#xe807;</i> <span class="i-name">icon-cog</span><span class="i-code">0xe807</span></div>
</div>
<div class="row">
<div class="the-icons span3" title="Code: 0xe808"><i class="demo-icon icon-logout">&#xe808;</i> <span class="i-name">icon-logout</span><span class="i-code">0xe808</span></div>
<div class="the-icons span3" title="Code: 0xe809"><i class="demo-icon icon-down-open">&#xe809;</i> <span class="i-name">icon-down-open</span><span class="i-code">0xe809</span></div>
<div class="the-icons span3" title="Code: 0xe80a"><i class="demo-icon icon-attach">&#xe80a;</i> <span class="i-name">icon-attach</span><span class="i-code">0xe80a</span></div>
<div class="the-icons span3" title="Code: 0xe80b"><i class="demo-icon icon-picture">&#xe80b;</i> <span class="i-name">icon-picture</span><span class="i-code">0xe80b</span></div>
</div>
<div class="row">
<div class="the-icons span3" title="Code: 0xe80c"><i class="demo-icon icon-video">&#xe80c;</i> <span class="i-name">icon-video</span><span class="i-code">0xe80c</span></div>
<div class="the-icons span3" title="Code: 0xe80d"><i class="demo-icon icon-right-open">&#xe80d;</i> <span class="i-name">icon-right-open</span><span class="i-code">0xe80d</span></div>
<div class="the-icons span3" title="Code: 0xe80e"><i class="demo-icon icon-left-open">&#xe80e;</i> <span class="i-name">icon-left-open</span><span class="i-code">0xe80e</span></div>
<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-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>
</div>
<div class="row">
<div class="the-icons span3" title="Code: 0xe814"><i class="demo-icon icon-attention">&#xe814;</i> <span class="i-name">icon-attention</span><span class="i-code">0xe814</span></div>
<div class="the-icons span3" title="Code: 0xe815"><i class="demo-icon icon-plus">&#xe815;</i> <span class="i-name">icon-plus</span><span class="i-code">0xe815</span></div>
<div class="the-icons span3" title="Code: 0xe816"><i class="demo-icon icon-adjust">&#xe816;</i> <span class="i-name">icon-adjust</span><span class="i-code">0xe816</span></div>
<div class="the-icons span3" title="Code: 0xe817"><i class="demo-icon icon-edit">&#xe817;</i> <span class="i-name">icon-edit</span><span class="i-code">0xe817</span></div>
</div>
<div class="row">
<div class="the-icons span3" title="Code: 0xe818"><i class="demo-icon icon-pencil">&#xe818;</i> <span class="i-name">icon-pencil</span><span class="i-code">0xe818</span></div>
<div class="the-icons span3" title="Code: 0xe819"><i class="demo-icon icon-pin">&#xe819;</i> <span class="i-name">icon-pin</span><span class="i-code">0xe819</span></div>
<div class="the-icons span3" title="Code: 0xe81a"><i class="demo-icon icon-wrench">&#xe81a;</i> <span class="i-name">icon-wrench</span><span class="i-code">0xe81a</span></div>
<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>
<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: 0xf0e4"><i class="demo-icon icon-gauge">&#xf0e4;</i> <span class="i-name">icon-gauge</span><span class="i-code">0xf0e4</span></div>
</div>
<div class="row">
<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 class="the-icons span3" title="Code: 0xf0fe"><i class="demo-icon icon-plus-squared">&#xf0fe;</i> <span class="i-name">icon-plus-squared</span><span class="i-code">0xf0fe</span></div>
<div class="the-icons span3" title="Code: 0xf112"><i class="demo-icon icon-reply">&#xf112;</i> <span class="i-name">icon-reply</span><span class="i-code">0xf112</span></div>
</div>
<div class="row">
<div class="the-icons span3" title="Code: 0xf118"><i class="demo-icon icon-smile">&#xf118;</i> <span class="i-name">icon-smile</span><span class="i-code">0xf118</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 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>
<div class="row">
<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 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>
<div class="container footer">Generated by <a href="http://fontello.com">fontello.com</a></div>
</body>
</html>

View file

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

View file

@ -0,0 +1,124 @@
@font-face {
font-family: "Icons";
src: url("./font/fontello.1576166651574.eot");
src: url("./font/fontello.1576166651574.eot") format("embedded-opentype"),
url("./font/fontello.1576166651574.woff2") format("woff2"),
url("./font/fontello.1576166651574.woff") format("woff"),
url("./font/fontello.1576166651574.ttf") format("truetype"),
url("./font/fontello.1576166651574.svg") format("svg");
font-weight: normal;
font-style: normal;
}
[class^="icon-"]::before,
[class*=" icon-"]::before {
font-family: "Icons";
font-style: normal;
font-weight: normal;
speak: none;
display: inline-block;
text-decoration: inherit;
width: 1em;
margin-right: .2em;
text-align: center;
font-variant: normal;
text-transform: none;
line-height: 1em;
margin-left: .2em;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-spin4::before { content: "\e834"; }
.icon-cancel::before { content: "\e800"; }
.icon-upload::before { content: "\e801"; }
.icon-spin3::before { content: "\e832"; }
.icon-reply::before { content: "\f112"; }
.icon-star::before { content: "\e802"; }
.icon-star-empty::before { content: "\e803"; }
.icon-retweet::before { content: "\e804"; }
.icon-eye-off::before { content: "\e805"; }
.icon-binoculars::before { content: "\f1e5"; }
.icon-cog::before { content: "\e807"; }
.icon-user-plus::before { content: "\f234"; }
.icon-menu::before { content: "\f0c9"; }
.icon-logout::before { content: "\e808"; }
.icon-down-open::before { content: "\e809"; }
.icon-attach::before { content: "\e80a"; }
.icon-link-ext::before { content: "\f08e"; }
.icon-link-ext-alt::before { content: "\f08f"; }
.icon-picture::before { content: "\e80b"; }
.icon-video::before { content: "\e80c"; }
.icon-right-open::before { content: "\e80d"; }
.icon-left-open::before { content: "\e80e"; }
.icon-up-open::before { content: "\e80f"; }
.icon-comment-empty::before { content: "\f0e5"; }
.icon-mail-alt::before { content: "\f0e0"; }
.icon-lock::before { content: "\e811"; }
.icon-lock-open-alt::before { content: "\f13e"; }
.icon-globe::before { content: "\e812"; }
.icon-brush::before { content: "\e813"; }
.icon-search::before { content: "\e806"; }
.icon-adjust::before { content: "\e816"; }
.icon-thumbs-up-alt::before { content: "\f164"; }
.icon-attention::before { content: "\e814"; }
.icon-plus-squared::before { content: "\f0fe"; }
.icon-plus::before { content: "\e815"; }
.icon-edit::before { content: "\e817"; }
.icon-play-circled::before { content: "\f144"; }
.icon-pencil::before { content: "\e818"; }
.icon-chart-bar::before { content: "\e81b"; }
.icon-smile::before { content: "\f118"; }
.icon-bell-alt::before { content: "\f0f3"; }
.icon-wrench::before { content: "\e81a"; }
.icon-pin::before { content: "\e819"; }
.icon-ellipsis::before { content: "\f141"; }
.icon-bell-ringing-o::before { content: "\e810"; }
.icon-zoom-in::before { content: "\e81c"; }
.icon-gauge::before { content: "\f0e4"; }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,4 +1,4 @@
var serviceWorkerOption = {"assets":["/static/img/nsfw.74818f9.png","/static/css/app.fd71461124f3eb029b1b.css","/static/js/app.d20ca27d22d74eb7bce0.js","/static/css/vendors~app.b2603a50868c68a1c192.css","/static/js/vendors~app.76db8e4cdf29decd5cab.js","/static/js/2.c96b30ae9f2d3f46f0ad.js"]}; var serviceWorkerOption = {"assets":["/static/fontello.1576166651574.css","/static/font/fontello.1576166651574.eot","/static/font/fontello.1576166651574.svg","/static/font/fontello.1576166651574.ttf","/static/font/fontello.1576166651574.woff","/static/font/fontello.1576166651574.woff2","/static/img/nsfw.74818f9.png","/static/css/app.ae04505b31bb0ee2765e.css","/static/js/app.a9b3f4c3e79baf3fa8b7.js","/static/css/vendors~app.b2603a50868c68a1c192.css","/static/js/vendors~app.3f1ed7a4fdfc37ee27a7.js","/static/js/2.c96b30ae9f2d3f46f0ad.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){ !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){
/*! /*!

File diff suppressed because one or more lines are too long

View file

@ -8,6 +8,7 @@ defmodule Pleroma.CaptchaTest do
import Tesla.Mock import Tesla.Mock
alias Pleroma.Captcha.Kocaptcha alias Pleroma.Captcha.Kocaptcha
alias Pleroma.Captcha.Native
@ets_options [:ordered_set, :private, :named_table, {:read_concurrency, true}] @ets_options [:ordered_set, :private, :named_table, {:read_concurrency, true}]
@ -43,4 +44,21 @@ test "new and validate" do
) == :ok ) == :ok
end end
end end
describe "Native" do
test "new and validate" do
new = Native.new()
assert %{
answer_data: answer,
token: token,
type: :native,
url: "data:image/png;base64," <> _
} = new
assert is_binary(answer)
assert :ok = Native.validate(token, answer, answer)
assert {:error, "Invalid CAPTCHA"} == Native.validate(token, answer, answer <> "foobar")
end
end
end end

View file

@ -214,7 +214,7 @@ test "logging report response", %{moderator: moderator} do
{:ok, _} = {:ok, _} =
ModerationLog.insert_log(%{ ModerationLog.insert_log(%{
actor: moderator, actor: moderator,
action: "report_response", action: "report_note",
subject: report, subject: report,
text: "look at this" text: "look at this"
}) })
@ -222,7 +222,7 @@ test "logging report response", %{moderator: moderator} do
log = Repo.one(ModerationLog) log = Repo.one(ModerationLog)
assert log.data["message"] == assert log.data["message"] ==
"@#{moderator.nickname} responded with 'look at this' to report ##{report.id}" "@#{moderator.nickname} added note 'look at this' to report ##{report.id}"
end end
test "logging status sensitivity update", %{moderator: moderator} do test "logging status sensitivity update", %{moderator: moderator} do

View file

@ -224,4 +224,42 @@ test "filters scopes which directly match or are ancestors of supported scopes"
assert f.(["admin:read"], ["write", "admin"]) == ["admin:read"] assert f.(["admin:read"], ["write", "admin"]) == ["admin:read"]
end end
end end
describe "transform_scopes/2" do
clear_config([:auth, :enforce_oauth_admin_scope_usage])
setup do
{:ok, %{f: &OAuthScopesPlug.transform_scopes/2}}
end
test "with :admin option, prefixes all requested scopes with `admin:` " <>
"and [optionally] keeps only prefixed scopes, " <>
"depending on `[:auth, :enforce_oauth_admin_scope_usage]` setting",
%{f: f} do
Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false)
assert f.(["read"], %{admin: true}) == ["admin:read", "read"]
assert f.(["read", "write"], %{admin: true}) == [
"admin:read",
"read",
"admin:write",
"write"
]
Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], true)
assert f.(["read:accounts"], %{admin: true}) == ["admin:read:accounts"]
assert f.(["read", "write:reports"], %{admin: true}) == [
"admin:read",
"admin:write:reports"
]
end
test "with no supported options, returns unmodified scopes", %{f: f} do
assert f.(["read"], %{}) == ["read"]
assert f.(["read", "write"], %{}) == ["read", "write"]
end
end
end end

View file

@ -8,36 +8,116 @@ defmodule Pleroma.Plugs.UserIsAdminPlugTest do
alias Pleroma.Plugs.UserIsAdminPlug alias Pleroma.Plugs.UserIsAdminPlug
import Pleroma.Factory import Pleroma.Factory
test "accepts a user that is admin" do describe "unless [:auth, :enforce_oauth_admin_scope_usage]," do
user = insert(:user, is_admin: true) clear_config([:auth, :enforce_oauth_admin_scope_usage]) do
Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false)
end
conn = test "accepts a user that is an admin" do
build_conn() user = insert(:user, is_admin: true)
|> assign(:user, user)
ret_conn = conn = assign(build_conn(), :user, user)
conn
|> UserIsAdminPlug.call(%{})
assert conn == ret_conn ret_conn = UserIsAdminPlug.call(conn, %{})
assert conn == ret_conn
end
test "denies a user that isn't an admin" do
user = insert(:user)
conn =
build_conn()
|> assign(:user, user)
|> UserIsAdminPlug.call(%{})
assert conn.status == 403
end
test "denies when a user isn't set" do
conn = UserIsAdminPlug.call(build_conn(), %{})
assert conn.status == 403
end
end end
test "denies a user that isn't admin" do describe "with [:auth, :enforce_oauth_admin_scope_usage]," do
user = insert(:user) clear_config([:auth, :enforce_oauth_admin_scope_usage]) do
Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], true)
end
conn = setup do
build_conn() admin_user = insert(:user, is_admin: true)
|> assign(:user, user) non_admin_user = insert(:user, is_admin: false)
|> UserIsAdminPlug.call(%{}) blank_user = nil
assert conn.status == 403 {:ok, %{users: [admin_user, non_admin_user, blank_user]}}
end end
test "denies when a user isn't set" do test "if token has any of admin scopes, accepts a user that is an admin", %{conn: conn} do
conn = user = insert(:user, is_admin: true)
build_conn() token = insert(:oauth_token, user: user, scopes: ["admin:something"])
|> UserIsAdminPlug.call(%{})
assert conn.status == 403 conn =
conn
|> assign(:user, user)
|> assign(:token, token)
ret_conn = UserIsAdminPlug.call(conn, %{})
assert conn == ret_conn
end
test "if token has any of admin scopes, denies a user that isn't an admin", %{conn: conn} do
user = insert(:user, is_admin: false)
token = insert(:oauth_token, user: user, scopes: ["admin:something"])
conn =
conn
|> assign(:user, user)
|> assign(:token, token)
|> UserIsAdminPlug.call(%{})
assert conn.status == 403
end
test "if token has any of admin scopes, denies when a user isn't set", %{conn: conn} do
token = insert(:oauth_token, scopes: ["admin:something"])
conn =
conn
|> assign(:user, nil)
|> assign(:token, token)
|> UserIsAdminPlug.call(%{})
assert conn.status == 403
end
test "if token lacks admin scopes, denies users regardless of is_admin flag",
%{users: users} do
for user <- users do
token = insert(:oauth_token, user: user)
conn =
build_conn()
|> assign(:user, user)
|> assign(:token, token)
|> UserIsAdminPlug.call(%{})
assert conn.status == 403
end
end
test "if token is missing, denies users regardless of is_admin flag", %{users: users} do
for user <- users do
conn =
build_conn()
|> assign(:user, user)
|> assign(:token, nil)
|> UserIsAdminPlug.call(%{})
assert conn.status == 403
end
end
end end
end end

View file

@ -63,4 +63,84 @@ test "settings are migrated to file and deleted from db", %{temp_file: temp_file
assert file =~ "config :pleroma, :setting_first," assert file =~ "config :pleroma, :setting_first,"
assert file =~ "config :pleroma, :setting_second," assert file =~ "config :pleroma, :setting_second,"
end end
test "load a settings with large values and pass to file", %{temp_file: temp_file} do
Config.create(%{
group: "pleroma",
key: ":instance",
value: [
name: "Pleroma",
email: "example@example.com",
notify_email: "noreply@example.com",
description: "A Pleroma instance, an alternative fediverse server",
limit: 5_000,
chat_limit: 5_000,
remote_limit: 100_000,
upload_limit: 16_000_000,
avatar_upload_limit: 2_000_000,
background_upload_limit: 4_000_000,
banner_upload_limit: 4_000_000,
poll_limits: %{
max_options: 20,
max_option_chars: 200,
min_expiration: 0,
max_expiration: 365 * 24 * 60 * 60
},
registrations_open: true,
federating: true,
federation_incoming_replies_max_depth: 100,
federation_reachability_timeout_days: 7,
federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher],
allow_relay: true,
rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,
public: true,
quarantined_instances: [],
managed_config: true,
static_dir: "instance/static/",
allowed_post_formats: ["text/plain", "text/html", "text/markdown", "text/bbcode"],
mrf_transparency: true,
mrf_transparency_exclusions: [],
autofollowed_nicknames: [],
max_pinned_statuses: 1,
no_attachment_links: true,
welcome_user_nickname: nil,
welcome_message: nil,
max_report_comment_size: 1000,
safe_dm_mentions: false,
healthcheck: false,
remote_post_retention_days: 90,
skip_thread_containment: true,
limit_to_local_content: :unauthenticated,
dynamic_configuration: false,
user_bio_length: 5000,
user_name_length: 100,
max_account_fields: 10,
max_remote_account_fields: 20,
account_field_name_length: 512,
account_field_value_length: 2048,
external_user_synchronization: true,
extended_nickname_format: true,
multi_factor_authentication: [
totp: [
# digits 6 or 8
digits: 6,
period: 30
],
backup_codes: [
number: 2,
length: 6
]
]
]
})
Mix.Tasks.Pleroma.Config.run(["migrate_from_db", "temp", "true"])
assert Repo.all(Config) == []
assert File.exists?(temp_file)
{:ok, file} = File.read(temp_file)
assert file ==
"use Mix.Config\n\nconfig :pleroma, :instance,\n name: \"Pleroma\",\n email: \"example@example.com\",\n notify_email: \"noreply@example.com\",\n description: \"A Pleroma instance, an alternative fediverse server\",\n limit: 5000,\n chat_limit: 5000,\n remote_limit: 100_000,\n upload_limit: 16_000_000,\n avatar_upload_limit: 2_000_000,\n background_upload_limit: 4_000_000,\n banner_upload_limit: 4_000_000,\n poll_limits: %{\n max_expiration: 31_536_000,\n max_option_chars: 200,\n max_options: 20,\n min_expiration: 0\n },\n registrations_open: true,\n federating: true,\n federation_incoming_replies_max_depth: 100,\n federation_reachability_timeout_days: 7,\n federation_publisher_modules: [Pleroma.Web.ActivityPub.Publisher],\n allow_relay: true,\n rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy,\n public: true,\n quarantined_instances: [],\n managed_config: true,\n static_dir: \"instance/static/\",\n allowed_post_formats: [\"text/plain\", \"text/html\", \"text/markdown\", \"text/bbcode\"],\n mrf_transparency: true,\n mrf_transparency_exclusions: [],\n autofollowed_nicknames: [],\n max_pinned_statuses: 1,\n no_attachment_links: true,\n welcome_user_nickname: nil,\n welcome_message: nil,\n max_report_comment_size: 1000,\n safe_dm_mentions: false,\n healthcheck: false,\n remote_post_retention_days: 90,\n skip_thread_containment: true,\n limit_to_local_content: :unauthenticated,\n dynamic_configuration: false,\n user_bio_length: 5000,\n user_name_length: 100,\n max_account_fields: 10,\n max_remote_account_fields: 20,\n account_field_name_length: 512,\n account_field_value_length: 2048,\n external_user_synchronization: true,\n extended_nickname_format: true,\n multi_factor_authentication: [\n totp: [digits: 6, period: 30],\n backup_codes: [number: 2, length: 6]\n ]\n"
end
end end

View file

@ -914,6 +914,16 @@ test "unblocks domains" do
refute User.blocks?(user, collateral_user) refute User.blocks?(user, collateral_user)
end end
test "follows take precedence over domain blocks" do
user = insert(:user)
good_eggo = insert(:user, %{ap_id: "https://meanies.social/user/cuteposter"})
{:ok, user} = User.block_domain(user, "meanies.social")
{:ok, user} = User.follow(user, good_eggo)
refute User.blocks?(user, good_eggo)
end
end end
describe "blocks_import" do describe "blocks_import" do

View file

@ -608,6 +608,39 @@ test "doesn't return activities from blocked domains" do
refute repeat_activity in activities refute repeat_activity in activities
end end
test "does return activities from followed users on blocked domains" do
domain = "meanies.social"
domain_user = insert(:user, %{ap_id: "https://#{domain}/@pundit"})
blocker = insert(:user)
{:ok, blocker} = User.follow(blocker, domain_user)
{:ok, blocker} = User.block_domain(blocker, domain)
assert User.following?(blocker, domain_user)
assert User.blocks_domain?(blocker, domain_user)
refute User.blocks?(blocker, domain_user)
note = insert(:note, %{data: %{"actor" => domain_user.ap_id}})
activity = insert(:note_activity, %{note: note})
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => blocker, "skip_preload" => true})
assert activity in activities
# And check that if the guy we DO follow boosts someone else from their domain,
# that should be hidden
another_user = insert(:user, %{ap_id: "https://#{domain}/@meanie2"})
bad_note = insert(:note, %{data: %{"actor" => another_user.ap_id}})
bad_activity = insert(:note_activity, %{note: bad_note})
{:ok, repeat_activity, _} = CommonAPI.repeat(bad_activity.id, domain_user)
activities =
ActivityPub.fetch_activities([], %{"blocking_user" => blocker, "skip_preload" => true})
refute repeat_activity in activities
end
test "doesn't return muted activities" do test "doesn't return muted activities" do
activity_one = insert(:note_activity) activity_one = insert(:note_activity)
activity_two = insert(:note_activity) activity_two = insert(:note_activity)
@ -1592,6 +1625,38 @@ test "detects hidden follows/followers for friendica" do
end end
end end
describe "fetch_favourites/3" do
test "returns a favourite activities sorted by adds to favorite" do
user = insert(:user)
other_user = insert(:user)
user1 = insert(:user)
user2 = insert(:user)
{:ok, a1} = CommonAPI.post(user1, %{"status" => "bla"})
{:ok, _a2} = CommonAPI.post(user2, %{"status" => "traps are happy"})
{:ok, a3} = CommonAPI.post(user2, %{"status" => "Trees Are "})
{:ok, a4} = CommonAPI.post(user2, %{"status" => "Agent Smith "})
{:ok, a5} = CommonAPI.post(user1, %{"status" => "Red or Blue "})
{:ok, _, _} = CommonAPI.favorite(a4.id, user)
{:ok, _, _} = CommonAPI.favorite(a3.id, other_user)
Process.sleep(1000)
{:ok, _, _} = CommonAPI.favorite(a3.id, user)
{:ok, _, _} = CommonAPI.favorite(a5.id, other_user)
Process.sleep(1000)
{:ok, _, _} = CommonAPI.favorite(a5.id, user)
{:ok, _, _} = CommonAPI.favorite(a4.id, other_user)
Process.sleep(1000)
{:ok, _, _} = CommonAPI.favorite(a1.id, user)
{:ok, _, _} = CommonAPI.favorite(a1.id, other_user)
result = ActivityPub.fetch_favourites(user)
assert Enum.map(result, & &1.id) == [a1.id, a5.id, a3.id, a4.id]
result = ActivityPub.fetch_favourites(user, %{"limit" => 2})
assert Enum.map(result, & &1.id) == [a1.id, a5.id]
end
end
describe "Move activity" do describe "Move activity" do
test "create" do test "create" do
%{ap_id: old_ap_id} = old_user = insert(:user) %{ap_id: old_ap_id} = old_user = insert(:user)

View file

@ -10,6 +10,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
alias Pleroma.HTML alias Pleroma.HTML
alias Pleroma.ModerationLog alias Pleroma.ModerationLog
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.ReportNote
alias Pleroma.Tests.ObanHelpers alias Pleroma.Tests.ObanHelpers
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserInviteToken alias Pleroma.UserInviteToken
@ -25,6 +26,60 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
:ok :ok
end end
clear_config([:auth, :enforce_oauth_admin_scope_usage]) do
Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], false)
end
describe "with [:auth, :enforce_oauth_admin_scope_usage]," do
clear_config([:auth, :enforce_oauth_admin_scope_usage]) do
Pleroma.Config.put([:auth, :enforce_oauth_admin_scope_usage], true)
end
test "GET /api/pleroma/admin/users/:nickname requires admin:read:accounts or broader scope" do
user = insert(:user)
admin = insert(:user, is_admin: true)
url = "/api/pleroma/admin/users/#{user.nickname}"
good_token1 = insert(:oauth_token, user: admin, scopes: ["admin"])
good_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read"])
good_token3 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts"])
bad_token1 = insert(:oauth_token, user: admin, scopes: ["read:accounts"])
bad_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts:partial"])
bad_token3 = nil
for good_token <- [good_token1, good_token2, good_token3] do
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, good_token)
|> get(url)
assert json_response(conn, 200)
end
for good_token <- [good_token1, good_token2, good_token3] do
conn =
build_conn()
|> assign(:user, nil)
|> assign(:token, good_token)
|> get(url)
assert json_response(conn, :forbidden)
end
for bad_token <- [bad_token1, bad_token2, bad_token3] do
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, bad_token)
|> get(url)
assert json_response(conn, :forbidden)
end
end
end
describe "DELETE /api/pleroma/admin/users" do describe "DELETE /api/pleroma/admin/users" do
test "single user" do test "single user" do
admin = insert(:user, is_admin: true) admin = insert(:user, is_admin: true)
@ -98,7 +153,7 @@ test "Create" do
assert ["lain", "lain2"] -- Enum.map(log_entry.data["subjects"], & &1["nickname"]) == [] assert ["lain", "lain2"] -- Enum.map(log_entry.data["subjects"], & &1["nickname"]) == []
end end
test "Cannot create user with exisiting email" do test "Cannot create user with existing email" do
admin = insert(:user, is_admin: true) admin = insert(:user, is_admin: true)
user = insert(:user) user = insert(:user)
@ -129,7 +184,7 @@ test "Cannot create user with exisiting email" do
] ]
end end
test "Cannot create user with exisiting nickname" do test "Cannot create user with existing nickname" do
admin = insert(:user, is_admin: true) admin = insert(:user, is_admin: true)
user = insert(:user) user = insert(:user)
@ -1560,7 +1615,8 @@ test "returns 403 when requested by a non-admin" do
|> assign(:user, user) |> assign(:user, user)
|> get("/api/pleroma/admin/reports") |> get("/api/pleroma/admin/reports")
assert json_response(conn, :forbidden) == %{"error" => "User is not admin."} assert json_response(conn, :forbidden) ==
%{"error" => "User is not an admin or OAuth admin scope is not granted."}
end end
test "returns 403 when requested by anonymous" do test "returns 403 when requested by anonymous" do
@ -1776,61 +1832,6 @@ test "account not empty if status was deleted", %{
end end
end end
describe "POST /api/pleroma/admin/reports/:id/respond" do
setup %{conn: conn} do
admin = insert(:user, is_admin: true)
%{conn: assign(conn, :user, admin), admin: admin}
end
test "returns created dm", %{conn: conn, admin: admin} do
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel offended",
"status_ids" => [activity.id]
})
response =
conn
|> post("/api/pleroma/admin/reports/#{report_id}/respond", %{
"status" => "I will check it out"
})
|> json_response(:ok)
recipients = Enum.map(response["mentions"], & &1["username"])
assert reporter.nickname in recipients
assert response["content"] == "I will check it out"
assert response["visibility"] == "direct"
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} responded with 'I will check it out' to report ##{
response["id"]
}"
end
test "returns 400 when status is missing", %{conn: conn} do
conn = post(conn, "/api/pleroma/admin/reports/test/respond")
assert json_response(conn, :bad_request) == "Invalid parameters"
end
test "returns 404 when report id is invalid", %{conn: conn} do
conn =
post(conn, "/api/pleroma/admin/reports/test/respond", %{
"status" => "foo"
})
assert json_response(conn, :not_found) == "Not found"
end
end
describe "PUT /api/pleroma/admin/statuses/:id" do describe "PUT /api/pleroma/admin/statuses/:id" do
setup %{conn: conn} do setup %{conn: conn} do
admin = insert(:user, is_admin: true) admin = insert(:user, is_admin: true)
@ -3027,6 +3028,77 @@ test "it resend emails for two users", %{admin: admin} do
}" }"
end end
end end
describe "POST /reports/:id/notes" do
setup do
admin = insert(:user, is_admin: true)
[reporter, target_user] = insert_pair(:user)
activity = insert(:note_activity, user: target_user)
{:ok, %{id: report_id}} =
CommonAPI.report(reporter, %{
"account_id" => target_user.id,
"comment" => "I feel offended",
"status_ids" => [activity.id]
})
build_conn()
|> assign(:user, admin)
|> post("/api/pleroma/admin/reports/#{report_id}/notes", %{
content: "this is disgusting!"
})
build_conn()
|> assign(:user, admin)
|> post("/api/pleroma/admin/reports/#{report_id}/notes", %{
content: "this is disgusting2!"
})
%{
admin_id: admin.id,
report_id: report_id,
admin: admin
}
end
test "it creates report note", %{admin_id: admin_id, report_id: report_id} do
[note, _] = Repo.all(ReportNote)
assert %{
activity_id: ^report_id,
content: "this is disgusting!",
user_id: ^admin_id
} = note
end
test "it returns reports with notes", %{admin: admin} do
conn =
build_conn()
|> assign(:user, admin)
|> get("/api/pleroma/admin/reports")
response = json_response(conn, 200)
notes = hd(response["reports"])["notes"]
[note, _] = notes
assert note["user"]["nickname"] == admin.nickname
assert note["content"] == "this is disgusting!"
assert note["created_at"]
assert response["total"] == 1
end
test "it deletes the note", %{admin: admin, report_id: report_id} do
assert ReportNote |> Repo.all() |> length() == 2
[note, _] = Repo.all(ReportNote)
build_conn()
|> assign(:user, admin)
|> delete("/api/pleroma/admin/reports/#{report_id}/notes/#{note.id}")
assert ReportNote |> Repo.all() |> length() == 1
end
end
end end
# Needed for testing # Needed for testing

View file

@ -30,6 +30,7 @@ test "renders a report" do
Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: other_user}) Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: other_user})
), ),
statuses: [], statuses: [],
notes: [],
state: "open", state: "open",
id: activity.id id: activity.id
} }
@ -65,6 +66,7 @@ test "includes reported statuses" do
), ),
statuses: [StatusView.render("show.json", %{activity: activity})], statuses: [StatusView.render("show.json", %{activity: activity})],
state: "open", state: "open",
notes: [],
id: report_activity.id id: report_activity.id
} }

View file

@ -165,15 +165,20 @@ test "search", %{conn: conn} do
assert status["id"] == to_string(activity.id) assert status["id"] == to_string(activity.id)
end end
test "search fetches remote statuses", %{conn: conn} do test "search fetches remote statuses and prefers them over other results", %{conn: conn} do
capture_log(fn -> capture_log(fn ->
{:ok, %{id: activity_id}} =
CommonAPI.post(insert(:user), %{
"status" => "check out https://shitposter.club/notice/2827873"
})
conn = conn =
conn conn
|> get("/api/v1/search", %{"q" => "https://shitposter.club/notice/2827873"}) |> get("/api/v1/search", %{"q" => "https://shitposter.club/notice/2827873"})
assert results = json_response(conn, 200) assert results = json_response(conn, 200)
[status] = results["statuses"] [status, %{"id" => ^activity_id}] = results["statuses"]
assert status["uri"] == assert status["uri"] ==
"tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment" "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"

View file

@ -66,6 +66,7 @@ test "Represent a user account" do
note: "valid html", note: "valid html",
sensitive: false, sensitive: false,
pleroma: %{ pleroma: %{
actor_type: "Person",
discoverable: false discoverable: false
}, },
fields: [] fields: []
@ -106,7 +107,8 @@ test "Represent a Service(bot) account" do
insert(:user, %{ insert(:user, %{
follower_count: 3, follower_count: 3,
note_count: 5, note_count: 5,
source_data: %{"type" => "Service"}, source_data: %{},
actor_type: "Service",
nickname: "shp@shitposter.club", nickname: "shp@shitposter.club",
inserted_at: ~N[2017-08-15 15:47:06.597036] inserted_at: ~N[2017-08-15 15:47:06.597036]
}) })
@ -134,6 +136,7 @@ test "Represent a Service(bot) account" do
note: user.bio, note: user.bio,
sensitive: false, sensitive: false,
pleroma: %{ pleroma: %{
actor_type: "Service",
discoverable: false discoverable: false
}, },
fields: [] fields: []
@ -278,7 +281,8 @@ test "represent an embedded relationship" do
insert(:user, %{ insert(:user, %{
follower_count: 0, follower_count: 0,
note_count: 5, note_count: 5,
source_data: %{"type" => "Service"}, source_data: %{},
actor_type: "Service",
nickname: "shp@shitposter.club", nickname: "shp@shitposter.club",
inserted_at: ~N[2017-08-15 15:47:06.597036] inserted_at: ~N[2017-08-15 15:47:06.597036]
}) })
@ -311,6 +315,7 @@ test "represent an embedded relationship" do
note: user.bio, note: user.bio,
sensitive: false, sensitive: false,
pleroma: %{ pleroma: %{
actor_type: "Service",
discoverable: false discoverable: false
}, },
fields: [] fields: []

View file

@ -567,33 +567,41 @@ test "with existing authentication and OOB `redirect_uri`, redirects to app with
end end
describe "POST /oauth/authorize" do describe "POST /oauth/authorize" do
test "redirects with oauth authorization" do test "redirects with oauth authorization, " <>
user = insert(:user) "keeping only non-admin scopes for non-admin user" do
app = insert(:oauth_app, scopes: ["read", "write", "follow"]) app = insert(:oauth_app, scopes: ["read", "write", "admin"])
redirect_uri = OAuthController.default_redirect_uri(app) redirect_uri = OAuthController.default_redirect_uri(app)
conn = non_admin = insert(:user, is_admin: false)
build_conn() admin = insert(:user, is_admin: true)
|> post("/oauth/authorize", %{
"authorization" => %{
"name" => user.nickname,
"password" => "test",
"client_id" => app.client_id,
"redirect_uri" => redirect_uri,
"scope" => "read:subscope write",
"state" => "statepassed"
}
})
target = redirected_to(conn) for {user, expected_scopes} <- %{
assert target =~ redirect_uri non_admin => ["read:subscope", "write"],
admin => ["read:subscope", "write", "admin"]
} do
conn =
build_conn()
|> post("/oauth/authorize", %{
"authorization" => %{
"name" => user.nickname,
"password" => "test",
"client_id" => app.client_id,
"redirect_uri" => redirect_uri,
"scope" => "read:subscope write admin",
"state" => "statepassed"
}
})
query = URI.parse(target).query |> URI.query_decoder() |> Map.new() target = redirected_to(conn)
assert target =~ redirect_uri
assert %{"state" => "statepassed", "code" => code} = query query = URI.parse(target).query |> URI.query_decoder() |> Map.new()
auth = Repo.get_by(Authorization, token: code)
assert auth assert %{"state" => "statepassed", "code" => code} = query
assert auth.scopes == ["read:subscope", "write"] auth = Repo.get_by(Authorization, token: code)
assert auth
assert auth.scopes == expected_scopes
end
end end
test "returns 401 for wrong credentials", %{conn: conn} do test "returns 401 for wrong credentials", %{conn: conn} do
@ -623,31 +631,34 @@ test "returns 401 for wrong credentials", %{conn: conn} do
assert result =~ "Invalid Username/Password" assert result =~ "Invalid Username/Password"
end end
test "returns 401 for missing scopes", %{conn: conn} do test "returns 401 for missing scopes " <>
user = insert(:user) "(including all admin-only scopes for non-admin user)" do
app = insert(:oauth_app) user = insert(:user, is_admin: false)
app = insert(:oauth_app, scopes: ["read", "write", "admin"])
redirect_uri = OAuthController.default_redirect_uri(app) redirect_uri = OAuthController.default_redirect_uri(app)
result = for scope_param <- ["", "admin:read admin:write"] do
conn result =
|> post("/oauth/authorize", %{ build_conn()
"authorization" => %{ |> post("/oauth/authorize", %{
"name" => user.nickname, "authorization" => %{
"password" => "test", "name" => user.nickname,
"client_id" => app.client_id, "password" => "test",
"redirect_uri" => redirect_uri, "client_id" => app.client_id,
"state" => "statepassed", "redirect_uri" => redirect_uri,
"scope" => "" "state" => "statepassed",
} "scope" => scope_param
}) }
|> html_response(:unauthorized) })
|> html_response(:unauthorized)
# Keep the details # Keep the details
assert result =~ app.client_id assert result =~ app.client_id
assert result =~ redirect_uri assert result =~ redirect_uri
# Error message # Error message
assert result =~ "This action is outside the authorized scopes" assert result =~ "This action is outside the authorized scopes"
end
end end
test "returns 401 for scopes beyond app scopes hierarchy", %{conn: conn} do test "returns 401 for scopes beyond app scopes hierarchy", %{conn: conn} do