diff --git a/.gitignore b/.gitignore index 774893b35..9591f9976 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ /db /deps /*.ez -/uploads /test/uploads /.elixir_ls /test/fixtures/test_tmp.txt @@ -11,6 +10,7 @@ /test/tmp/ /doc /instance +/priv/ssh_keys # Prevent committing custom emojis /priv/static/emoji/custom/* @@ -38,3 +38,7 @@ erl_crash.dump # Prevent committing docs files /priv/static/doc/* + +# Code test coverage +/cover +/Elixir.*.coverdata diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c07f1a5d3..dc99b81ee 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -48,6 +48,7 @@ unit-testing: - name: postgres:9.6.2 command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] script: + - mix deps.get - mix ecto.create - mix ecto.migrate - mix test --trace --preload-modules @@ -77,4 +78,4 @@ docs-deploy: - echo "${SSH_HOST_KEY}" > ~/.ssh/known_hosts - eval $(ssh-agent -s) - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - - - rsync -hrvz --delete -e "ssh -p ${SSH_PORT}" priv/static/doc/ "${SSH_USER_HOST_LOCATION}/${CI_COMMIT_REF_NAME}" + - rsync -hrvz --delete -e "ssh -p ${SSH_PORT}" priv/static/doc/ "${SSH_USER_HOST_LOCATION}/${CI_COMMIT_REF_NAME}" diff --git a/CHANGELOG.md b/CHANGELOG.md index 70381f382..5c0baa317 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,17 +16,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Configuration: `link_name` option - Configuration: `fetch_initial_posts` option - Configuration: `notify_email` option +- Configuration: Media proxy `whitelist` option - Pleroma API: User subscriptions - Pleroma API: Healthcheck endpoint - Admin API: Endpoints for listing/revoking invite tokens - Admin API: Endpoints for making users follow/unfollow each other +- Admin API: added filters (role, tags, email, name) for users endpoint - Mastodon API: [Scheduled statuses](https://docs.joinmastodon.org/api/rest/scheduled-statuses/) - Mastodon API: `/api/v1/notifications/destroy_multiple` (glitch-soc extension) - Mastodon API: `/api/v1/pleroma/accounts/:id/favourites` (API extension) - Mastodon API: [Reports](https://docs.joinmastodon.org/api/rest/reports/) +- Mastodon API: REST API for creating an account - ActivityPub C2S: OAuth endpoints - Metadata RelMe provider +- OAuth: added support for refresh tokens - Emoji packs and emoji pack manager +- AdminFE: initial release with basic user management accessible at /pleroma/admin/ ### Changed - **Breaking:** Configuration: move from Pleroma.Mailer to Pleroma.Emails.Mailer @@ -40,26 +45,33 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Configuration: Dedupe enabled by default - Configuration: Added `extra_cookie_attrs` for setting non-standard cookie attributes. Defaults to ["SameSite=Lax"] so that remote follows work. - Pleroma API: Support for emoji tags in `/api/pleroma/emoji` resulting in a breaking API change +- Timelines: Messages involving people you have blocked will be excluded from the timeline in all cases instead of just repeats. - Mastodon API: Support for `exclude_types`, `limit` and `min_id` in `/api/v1/notifications` - Mastodon API: Add `languages` and `registrations` to `/api/v1/instance` - Mastodon API: Provide plaintext versions of cw/content in the Status entity - Mastodon API: Add `pleroma.conversation_id`, `pleroma.in_reply_to_account_acct` fields to the Status entity -- Mastodon API: Add `pleroma.tags`, `pleroma.relationship{}`, `pleroma.is_moderator`, `pleroma.is_admin`, `pleroma.confirmation_pending` fields to the User entity +- Mastodon API: Add `pleroma.tags`, `pleroma.relationship{}`, `pleroma.is_moderator`, `pleroma.is_admin`, `pleroma.confirmation_pending`, `pleroma.hide_followers`, `pleroma.hide_follows`, `pleroma.hide_favorites` fields to the User entity +- Mastodon API: Add `pleroma.show_role`, `pleroma.no_rich_text` fields to the Source subentity +- Mastodon API: Add support for updating `no_rich_text`, `hide_followers`, `hide_follows`, `hide_favorites`, `show_role` in `PATCH /api/v1/update_credentials` - Mastodon API: Add `pleroma.is_seen` to the Notification entity - Mastodon API: Add `pleroma.local` to the Status entity - Mastodon API: Add `preview` parameter to `POST /api/v1/statuses` - Mastodon API: Add `with_muted` parameter to timeline endpoints - Mastodon API: Actual reblog hiding instead of a dummy - Mastodon API: Remove attachment limit in the Status entity +- Mastodon API: Added support max_id & since_id for bookmark timeline endpoints. - Deps: Updated Cowboy to 2.6 - Deps: Updated Ecto to 3.0.7 - Don't ship finmoji by default, they can be installed as an emoji pack +- Admin API: Move the user related API to `api/pleroma/admin/users` ### Fixed +- Added an FTS index on objects. Running `vacuum analyze` and setting a larger `work_mem` is recommended. - Followers counter not being updated when a follower is blocked - Deactivated users being able to request an access token - Limit on request body in rich media/relme parsers being ignored resulting in a possible memory leak - proper Twitter Card generation instead of a dummy +- Deletions failing for users with a large number of posts - NodeInfo: Include admins in `staffAccounts` - ActivityPub: Crashing when requesting empty local user's outbox - Federation: Handling of objects without `summary` property @@ -68,16 +80,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Federation: Cope with missing or explicitly nulled address lists - Federation: Explicitly ensure activities addressed to `as:Public` become addressed to the followers collection - Federation: Better cope with actors which do not declare a followers collection and use `as:Public` with these semantics +- Federation: Follow requests from remote users who have been blocked will be automatically rejected if appropriate - MediaProxy: Parse name from content disposition headers even for non-whitelisted types - MediaProxy: S3 link encoding - Rich Media: Reject any data which cannot be explicitly encoded into JSON - Pleroma API: Importing follows from Mastodon 2.8+ +- Twitter API: Exposing default scope, `no_rich_text` of the user to anyone +- Twitter API: Returning the `role` object in user entity despite `show_role = false` - Mastodon API: `/api/v1/favourites` serving only public activities - Mastodon API: Reblogs having `in_reply_to_id` - `null` even when they are replies - Mastodon API: Streaming API broadcasting wrong activity id - Mastodon API: 500 errors when requesting a card for a private conversation - Mastodon API: Handling of `reblogs` in `/api/v1/accounts/:id/follow` - Mastodon API: Correct `reblogged`, `favourited`, and `bookmarked` values in the reblog status JSON +- Mastodon API: Exposing default scope of the user to anyone +- Mastodon API: Make `irreversible` field default to `false` [`POST /api/v1/filters`] ## [0.9.9999] - 2019-04-05 ### Security diff --git a/COPYING b/COPYING index eceb68efe..0aede0fba 100644 --- a/COPYING +++ b/COPYING @@ -15,6 +15,14 @@ priv/static/images/pleroma-tan.png --- +The following files are copyright © 2019 shitposter.club, and are distributed +under the Creative Commons Attribution 4.0 International license, you should +have received a copy of the license file as CC-BY-4.0. + +priv/static/images/pleroma-fox-tan-shy.png + +--- + The following files are copyright © 2017-2019 Pleroma Authors , and are distributed under the Creative Commons Attribution-ShareAlike 4.0 International license, you should have received diff --git a/README.md b/README.md index 987f973ea..928f75dc7 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ For clients it supports both the [GNU Social API with Qvitter extensions](https: - [Client Applications for Pleroma](https://docs-develop.pleroma.social/clients.html) -No release has been made yet, but several servers have been online for months already. If you want to run your own server, feel free to contact us at @lain@pleroma.soykaf.com or in our dev chat at #pleroma on freenode or via matrix at . +If you want to run your own server, feel free to contact us at @lain@pleroma.soykaf.com or in our dev chat at #pleroma on freenode or via matrix at . ## Installation diff --git a/config/config.exs b/config/config.exs index b11e4c680..45034a775 100644 --- a/config/config.exs +++ b/config/config.exs @@ -212,6 +212,11 @@ registrations_open: true, federating: true, federation_reachability_timeout_days: 7, + federation_publisher_modules: [ + Pleroma.Web.ActivityPub.Publisher, + Pleroma.Web.Websub, + Pleroma.Web.Salmon + ], allow_relay: true, rewrite_policy: Pleroma.Web.ActivityPub.MRF.NoOpPolicy, public: true, @@ -221,7 +226,8 @@ allowed_post_formats: [ "text/plain", "text/html", - "text/markdown" + "text/markdown", + "text/bbcode" ], mrf_transparency: true, autofollowed_nicknames: [], @@ -233,6 +239,8 @@ safe_dm_mentions: false, healthcheck: false +config :pleroma, :app_account_creation, enabled: false, max_requests: 5, interval: 1800 + config :pleroma, :markup, # XXX - unfortunately, inline images must be enabled by default right now, because # of custom emoji. Issue #275 discusses defanging that somehow. @@ -326,7 +334,8 @@ follow_redirect: true, pool: :media ] - ] + ], + whitelist: [] config :pleroma, :chat, enabled: true @@ -414,7 +423,8 @@ web_push: 50, mailer: 10, transmogrifier: 20, - scheduled_activities: 10 + scheduled_activities: 10, + background: 5 config :pleroma, :fetch_initial_posts, enabled: false, @@ -441,6 +451,9 @@ base: System.get_env("LDAP_BASE") || "dc=example,dc=com", uid: System.get_env("LDAP_UID") || "cn" +config :esshd, + enabled: false + oauth_consumer_strategies = String.split(System.get_env("OAUTH_CONSUMER_STRATEGIES") || "") ueberauth_providers = @@ -466,6 +479,10 @@ total_user_limit: 300, enabled: true +config :pleroma, :oauth2, + token_expires_in: 600, + issue_new_refresh_token: true + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{Mix.env()}.exs" diff --git a/docs/api/admin_api.md b/docs/api/admin_api.md index 8befa8ea0..75fa2ee83 100644 --- a/docs/api/admin_api.md +++ b/docs/api/admin_api.md @@ -8,15 +8,20 @@ Authentication is required and the user must be an admin. - Method `GET` - Query Params: - - *optional* `query`: **string** search term + - *optional* `query`: **string** search term (e.g. nickname, domain, nickname@domain) - *optional* `filters`: **string** comma-separated string of filters: - `local`: only local users - `external`: only external users - `active`: only active users - `deactivated`: only deactivated users + - `is_admin`: users with admin role + - `is_moderator`: users with moderator role - *optional* `page`: **integer** page number - *optional* `page_size`: **integer** number of users per page (default is `50`) -- Example: `https://mypleroma.org/api/pleroma/admin/users?query=john&filters=local,active&page=1&page_size=10` + - *optional* `tags`: **[string]** tags list + - *optional* `name`: **string** user display name + - *optional* `email`: **string** user email +- Example: `https://mypleroma.org/api/pleroma/admin/users?query=john&filters=local,active&page=1&page_size=10&tags[]=some_tag&tags[]=another_tag&name=display_name&email=email@example.com` - Response: ```JSON @@ -40,7 +45,7 @@ Authentication is required and the user must be an admin. } ``` -## `/api/pleroma/admin/user` +## `/api/pleroma/admin/users` ### Remove a user @@ -58,7 +63,7 @@ Authentication is required and the user must be an admin. - `password` - Response: User’s nickname -## `/api/pleroma/admin/user/follow` +## `/api/pleroma/admin/users/follow` ### Make a user follow another user - Methods: `POST` @@ -68,7 +73,7 @@ Authentication is required and the user must be an admin. - Response: - "ok" -## `/api/pleroma/admin/user/unfollow` +## `/api/pleroma/admin/users/unfollow` ### Make a user unfollow another user - Methods: `POST` @@ -111,7 +116,7 @@ Authentication is required and the user must be an admin. - `nickname` - `tags` -## `/api/pleroma/admin/permission_group/:nickname` +## `/api/pleroma/admin/users/:nickname/permission_group` ### Get user user permission groups membership @@ -126,7 +131,7 @@ Authentication is required and the user must be an admin. } ``` -## `/api/pleroma/admin/permission_group/:nickname/:permission_group` +## `/api/pleroma/admin/users/:nickname/permission_group/:permission_group` Note: Available `:permission_group` is currently moderator and admin. 404 is returned when the permission group doesn’t exist. @@ -160,7 +165,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret - On success: JSON of the `user.info` - Note: An admin cannot revoke their own admin status. -## `/api/pleroma/admin/activation_status/:nickname` +## `/api/pleroma/admin/users/:nickname/activation_status` ### Active or deactivate a user @@ -198,7 +203,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret - Response: - On success: URL of the unfollowed relay -## `/api/pleroma/admin/invite_token` +## `/api/pleroma/admin/users/invite_token` ### Get an account registration invite token @@ -210,7 +215,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret ] - Response: invite token (base64 string) -## `/api/pleroma/admin/invites` +## `/api/pleroma/admin/users/invites` ### Get a list of generated invites @@ -236,7 +241,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret } ``` -## `/api/pleroma/admin/revoke_invite` +## `/api/pleroma/admin/users/revoke_invite` ### Revoke invite by token @@ -259,7 +264,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret ``` -## `/api/pleroma/admin/email_invite` +## `/api/pleroma/admin/users/email_invite` ### Sends registration invite via email @@ -268,7 +273,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret - `email` - `name`, optional -## `/api/pleroma/admin/password_reset` +## `/api/pleroma/admin/users/:nickname/password_reset` ### Get a password reset token for a given nickname diff --git a/docs/api/differences_in_mastoapi_responses.md b/docs/api/differences_in_mastoapi_responses.md index 3bb1bd41f..36b47608e 100644 --- a/docs/api/differences_in_mastoapi_responses.md +++ b/docs/api/differences_in_mastoapi_responses.md @@ -1,6 +1,6 @@ # Differences in Mastodon API responses from vanilla Mastodon -A Pleroma instance can be identified by " (compatible; Pleroma )" present in `version` field in response from `/api/v1/instance` +A Pleroma instance can be identified by " (compatible; Pleroma )" present in `version` field in response from `/api/v1/instance` ## Flake IDs @@ -38,9 +38,18 @@ Has these additional fields under the `pleroma` object: - `tags`: Lists an array of tags for the user - `relationship{}`: Includes fields as documented for Mastodon API https://docs.joinmastodon.org/api/entities/#relationship -- `is_moderator`: boolean, true if user is a moderator -- `is_admin`: boolean, true if user is an admin +- `is_moderator`: boolean, nullable, true if user is a moderator +- `is_admin`: boolean, nullable, true if user is an admin - `confirmation_pending`: boolean, true if a new user account is waiting on email confirmation to be activated +- `hide_followers`: boolean, true when the user has follower hiding enabled +- `hide_follows`: boolean, true when the user has follow hiding enabled + +### Source + +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 +- `no_rich_text` - boolean, nullable, true when html tags are stripped from all statuses requested from the API ## Account Search @@ -60,3 +69,31 @@ Additional parameters can be added to the JSON body/Form data: - `preview`: boolean, if set to `true` the post won't be actually posted, but the status entitiy would still be rendered back. This could be useful for previewing rich text/custom emoji, for example. - `content_type`: string, contain the MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint. + +## PATCH `/api/v1/update_credentials` + +Additional parameters can be added to the JSON body/Form data: + +- `no_rich_text` - if true, html tags are stripped from all statuses requested from the API +- `hide_followers` - if true, user's followers will be hidden +- `hide_follows` - if true, user's follows will be hidden +- `hide_favorites` - if true, user's favorites timeline will be hidden +- `show_role` - if true, user's role (e.g admin, moderator) will be exposed to anyone in the API +- `default_scope` - the scope returned under `privacy` key in Source subentity + +## Authentication + +*Pleroma supports refreshing tokens. + +`POST /oauth/token` +Post here request with grant_type=refresh_token to obtain new access token. Returns an access token. + +## Account Registration +`POST /api/v1/accounts` + +Has theses additionnal parameters (which are the same as in Pleroma-API): + * `fullname`: optional + * `bio`: optional + * `captcha_solution`: optional, contains provider-specific captcha solution, + * `captcha_token`: optional, contains provider-specific captcha token + * `token`: invite token required when the registerations aren't public. diff --git a/docs/config.md b/docs/config.md index 7b6631f9b..470f71b7c 100644 --- a/docs/config.md +++ b/docs/config.md @@ -37,7 +37,7 @@ This filter replaces the filename (not the path) of an upload. For complete obfu An example for Sendgrid adapter: -```exs +```elixir config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Sendgrid, api_key: "YOUR_API_KEY" @@ -45,7 +45,7 @@ config :pleroma, Pleroma.Emails.Mailer, An example for SMTP adapter: -```exs +```elixir config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.SMTP, relay: "smtp.gmail.com", @@ -105,11 +105,17 @@ config :pleroma, Pleroma.Emails.Mailer, * `safe_dm_mentions`: If set to true, only mentions at the beginning of a post will be used to address people in direct messages. This is to prevent accidental mentioning of people when talking about them (e.g. "@friend hey i really don't like @enemy"). (Default: `false`) * `healthcheck`: if set to true, system data will be shown on ``/api/pleroma/healthcheck``. +## :app_account_creation +REST API for creating an account settings +* `enabled`: Enable/disable registration +* `max_requests`: Number of requests allowed for creating accounts +* `interval`: Interval for restricting requests for one ip (seconds) + ## :logger * `backends`: `:console` is used to send logs to stdout, `{ExSyslogger, :ex_syslogger}` to log to syslog, and `Quack.Logger` to log to Slack An example to enable ONLY ExSyslogger (f/ex in ``prod.secret.exs``) with info and debug suppressed: -``` +```elixir config :logger, backends: [{ExSyslogger, :ex_syslogger}] @@ -118,7 +124,7 @@ config :logger, :ex_syslogger, ``` Another example, keeping console output and adding the pid to syslog output: -``` +```elixir config :logger, backends: [:console, {ExSyslogger, :ex_syslogger}] @@ -130,7 +136,7 @@ config :logger, :ex_syslogger, See: [logger’s documentation](https://hexdocs.pm/logger/Logger.html) and [ex_syslogger’s documentation](https://hexdocs.pm/ex_syslogger/) An example of logging info to local syslog, but warn to a Slack channel: -``` +```elixir config :logger, backends: [ {ExSyslogger, :ex_syslogger}, Quack.Logger ], level: :info @@ -156,14 +162,30 @@ Frontends can access these settings at `/api/pleroma/frontend_configurations` To add your own configuration for PleromaFE, use it like this: -`config :pleroma, :frontend_configurations, pleroma_fe: %{redirectRootNoLogin: "/main/all", ...}` +```elixir +config :pleroma, :frontend_configurations, + pleroma_fe: %{ + theme: "pleroma-dark", + # ... see /priv/static/static/config.json for the available keys. +}, + masto_fe: %{ + showInstanceSpecificPanel: true + } +``` -These settings need to be complete, they will override the defaults. See `priv/static/static/config.json` for the available keys. +These settings **need to be complete**, they will override the defaults. + +NOTE: for versions < 1.0, you need to set [`:fe`](#fe) to false, as shown a few lines below. ## :fe __THIS IS DEPRECATED__ -If you are using this method, please change it to the `frontend_configurations` method. Please set this option to false in your config like this: `config :pleroma, :fe, false`. +If you are using this method, please change it to the [`frontend_configurations`](#frontend_configurations) method. +Please **set this option to false** in your config like this: + +```elixir +config :pleroma, :fe, false +``` This section is used to configure Pleroma-FE, unless ``:managed_config`` in ``:instance`` is set to false. @@ -205,6 +227,7 @@ This section is used to configure Pleroma-FE, unless ``:managed_config`` in ``:i * `enabled`: Enables proxying of remote media to the instance’s proxy * `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts. * `proxy_opts`: All options defined in `Pleroma.ReverseProxy` documentation, defaults to `[max_body_length: (25*1_048_576)]`. +* `whitelist`: List of domains to bypass the mediaproxy ## :gopher * `enabled`: Enables the gopher interface @@ -273,7 +296,7 @@ their ActivityPub ID. An example: -```exs +```elixir config :pleroma, :mrf_user_allowlist, "example.org": ["https://example.org/users/admin"] ``` @@ -302,7 +325,7 @@ the source code is here: https://github.com/koto-bank/kocaptcha. The default end Allows to set a token that can be used to authenticate with the admin api without using an actual user by giving it as the 'admin_token' parameter. Example: -```exs +```elixir config :pleroma, :admin_token, "somerandomtoken" ``` @@ -386,7 +409,7 @@ Configuration for the `auto_linker` library: Example: -```exs +```elixir config :auto_linker, opts: [ scheme: true, @@ -427,15 +450,36 @@ Pleroma account will be created with the same name as the LDAP user name. * `base`: LDAP base, e.g. "dc=example,dc=com" * `uid`: LDAP attribute name to authenticate the user, e.g. when "cn", the filter will be "cn=username,base" +## BBS / SSH access + +To enable simple command line interface accessible over ssh, add a setting like this to your configuration file: + +```exs +app_dir = File.cwd! +priv_dir = Path.join([app_dir, "priv/ssh_keys"]) + +config :esshd, + enabled: true, + priv_dir: priv_dir, + handler: "Pleroma.BBS.Handler", + port: 10_022, + password_authenticator: "Pleroma.BBS.Authenticator" +``` + +Feel free to adjust the priv_dir and port number. Then you will have to create the key for the keys (in the example `priv/ssh_keys`) and create the host keys with `ssh-keygen -N "" -b 2048 -t rsa -f ssh_host_rsa_key`. After restarting, you should be able to connect to your Pleroma instance with `ssh username@server -p $PORT` + ## :auth +* `Pleroma.Web.Auth.PleromaAuthenticator`: default database authenticator +* `Pleroma.Web.Auth.LDAPAuthenticator`: LDAP authentication + Authentication / authorization settings. * `auth_template`: authentication form template. By default it's `show.html` which corresponds to `lib/pleroma/web/templates/o_auth/o_auth/show.html.eex`. * `oauth_consumer_template`: OAuth consumer mode authentication form template. By default it's `consumer.html` which corresponds to `lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex`. * `oauth_consumer_strategies`: the list of enabled OAuth consumer strategies; by default it's set by OAUTH_CONSUMER_STRATEGIES environment variable. -# OAuth consumer mode +## OAuth consumer mode OAuth consumer mode allows sign in / sign up via external OAuth providers (e.g. Twitter, Facebook, Google, Microsoft, etc.). Implementation is based on Ueberauth; see the list of [available strategies](https://github.com/ueberauth/ueberauth/wiki/List-of-Strategies). @@ -459,7 +503,7 @@ Note: make sure that `"SameSite=Lax"` is set in `extra_cookie_attrs` when you ha Once the app is configured on external OAuth provider side, add app's credentials and strategy-specific settings (if any — e.g. see Microsoft below) to `config/prod.secret.exs`, per strategy's documentation (e.g. [ueberauth_twitter](https://github.com/ueberauth/ueberauth_twitter)). Example config basing on environment variables: -``` +```elixir # Twitter config :ueberauth, Ueberauth.Strategy.Twitter.OAuth, consumer_key: System.get_env("TWITTER_CONSUMER_KEY"), @@ -488,6 +532,13 @@ config :ueberauth, Ueberauth, ] ``` +## OAuth 2.0 provider - :oauth2 + +Configure OAuth 2 provider capabilities: + +* `token_expires_in` - The lifetime in seconds of the access token. +* `issue_new_refresh_token` - Keeps old refresh token or generate new refresh token when to obtain an access token. + ## :emoji * `shortcode_globs`: Location of custom emoji files. `*` can be used as a wildcard. Example `["/emoji/custom/**/*.png"]` * `groups`: Emojis are ordered in groups (tags). This is an array of key-value pairs where the key is the groupname and the value the location or array of locations. `*` can be used as a wildcard. Example `[Custom: ["/emoji/*.png", "/emoji/custom/*.png"]]` diff --git a/installation/download-mastofe-build.sh b/installation/download-mastofe-build.sh new file mode 100755 index 000000000..7e293867d --- /dev/null +++ b/installation/download-mastofe-build.sh @@ -0,0 +1,45 @@ +#!/bin/sh +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only +project_id="74" +project_branch="rebase/glitch-soc" +static_dir="instance/static" +# For bundling: +# project_branch="pleroma" +# static_dir="priv/static" + +if [[ ! -d "${static_dir}" ]] +then + echo "Error: ${static_dir} directory is missing, are you sure you are running this script at the root of pleroma’s repository?" + exit 1 +fi + +last_modified="$(curl -s -I 'https://git.pleroma.social/api/v4/projects/'${project_id}'/jobs/artifacts/'${project_branch}'/download?job=build' | grep '^Last-Modified:' | cut -d: -f2-)" + +echo "branch:${project_branch}" +echo "Last-Modified:${last_modified}" + +artifact="mastofe.zip" + +if [[ -e mastofe.timestamp ]] && [[ "${last_modified}" != "" ]] +then + if [[ "$(cat mastofe.timestamp)" == "${last_modified}" ]] + then + echo "MastoFE is up-to-date, exiting…" + exit 0 + fi +fi + +curl -c - "https://git.pleroma.social/api/v4/projects/${project_id}/jobs/artifacts/${project_branch}/download?job=build" -o "${artifact}" || exit + +# TODO: Update the emoji as well +rm -fr "${static_dir}/sw.js" "${static_dir}/packs" || exit +unzip -q "${artifact}" || exit + +cp public/assets/sw.js "${static_dir}/sw.js" || exit +cp -r public/packs "${static_dir}/packs" || exit + +echo "${last_modified}" > mastofe.timestamp +rm -fr public +rm -i "${artifact}" diff --git a/lib/mix/tasks/benchmark.ex b/lib/mix/tasks/benchmark.ex new file mode 100644 index 000000000..0fbb4dbb1 --- /dev/null +++ b/lib/mix/tasks/benchmark.ex @@ -0,0 +1,25 @@ +defmodule Mix.Tasks.Pleroma.Benchmark do + use Mix.Task + alias Mix.Tasks.Pleroma.Common + + def run(["search"]) do + Common.start_pleroma() + + Benchee.run(%{ + "search" => fn -> + Pleroma.Web.MastodonAPI.MastodonAPIController.status_search(nil, "cofe") + end + }) + end + + def run(["tag"]) do + Common.start_pleroma() + + Benchee.run(%{ + "tag" => fn -> + %{"type" => "Create", "tag" => "cofe"} + |> Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities() + end + }) + end +end diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex index cced73226..d2ddf450a 100644 --- a/lib/mix/tasks/pleroma/emoji.ex +++ b/lib/mix/tasks/pleroma/emoji.ex @@ -109,7 +109,7 @@ def run(["get-packs" | args]) do ]) ) - binary_archive = Tesla.get!(src_url).body + binary_archive = Tesla.get!(client(), src_url).body archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16() sha_status_text = ["SHA256 of ", :bright, pack_name, :normal, " source file is ", :bright] @@ -137,7 +137,7 @@ def run(["get-packs" | args]) do ]) ) - files = Tesla.get!(files_url).body |> Poison.decode!() + files = Tesla.get!(client(), files_url).body |> Jason.decode!() IO.puts(IO.ANSI.format(["Unpacking ", :bright, pack_name])) @@ -213,7 +213,7 @@ def run(["gen-pack", src]) do IO.puts("Downloading the pack and generating SHA256") - binary_archive = Tesla.get!(src).body + binary_archive = Tesla.get!(client(), src).body archive_sha = :crypto.hash(:sha256, binary_archive) |> Base.encode16() IO.puts("SHA256 is #{archive_sha}") @@ -239,7 +239,7 @@ def run(["gen-pack", src]) do emoji_map = Pleroma.Emoji.make_shortcode_to_file_map(tmp_pack_dir, exts) - File.write!(files_name, Poison.encode!(emoji_map, pretty: true)) + File.write!(files_name, Jason.encode!(emoji_map, pretty: true)) IO.puts(""" @@ -248,11 +248,11 @@ def run(["gen-pack", src]) do """) if File.exists?("index.json") do - existing_data = File.read!("index.json") |> Poison.decode!() + existing_data = File.read!("index.json") |> Jason.decode!() File.write!( "index.json", - Poison.encode!( + Jason.encode!( Map.merge( existing_data, pack_json @@ -263,16 +263,16 @@ def run(["gen-pack", src]) do IO.puts("index.json file has been update with the #{name} pack") else - File.write!("index.json", Poison.encode!(pack_json, pretty: true)) + File.write!("index.json", Jason.encode!(pack_json, pretty: true)) IO.puts("index.json has been created with the #{name} pack") end end defp fetch_manifest(from) do - Poison.decode!( + Jason.decode!( if String.starts_with?(from, "http") do - Tesla.get!(from).body + Tesla.get!(client(), from).body else File.read!(from) end @@ -290,4 +290,12 @@ defp parse_global_opts(args) do ] ) end + + defp client do + middleware = [ + {Tesla.Middleware.FollowRedirects, [max_redirects: 3]} + ] + + Tesla.client(middleware) + end end diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index b396ff0de..d130ff8c9 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -126,7 +126,7 @@ def run(["new", nickname, email | rest]) do proceed? = assume_yes? or Mix.shell().yes?("Continue?") - unless not proceed? do + if proceed? do Common.start_pleroma() params = %{ @@ -138,7 +138,7 @@ def run(["new", nickname, email | rest]) do bio: bio } - changeset = User.register_changeset(%User{}, params, confirmed: true) + changeset = User.register_changeset(%User{}, params, need_confirmation: false) {:ok, _user} = User.register(changeset) Mix.shell().info("User #{nickname} created") @@ -163,7 +163,7 @@ def run(["rm", nickname]) do Common.start_pleroma() with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do - User.delete(user) + User.perform(:delete, user) Mix.shell().info("User #{nickname} deleted.") else _ -> @@ -380,7 +380,7 @@ def run(["delete_activities", nickname]) do Common.start_pleroma() with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do - User.delete_user_activities(user) + {:ok, _} = User.delete_user_activities(user) Mix.shell().info("User #{nickname} statuses deleted.") else _ -> diff --git a/lib/pleroma/activity.ex b/lib/pleroma/activity.ex index 4a2ded518..c121e800f 100644 --- a/lib/pleroma/activity.ex +++ b/lib/pleroma/activity.ex @@ -6,14 +6,18 @@ defmodule Pleroma.Activity do use Ecto.Schema alias Pleroma.Activity + alias Pleroma.Bookmark alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Repo + alias Pleroma.User import Ecto.Changeset import Ecto.Query @type t :: %__MODULE__{} + @type actor :: String.t() + @primary_key {:id, Pleroma.FlakeId, autogenerate: true} # https://github.com/tootsuite/mastodon/blob/master/app/models/notification.rb#L19 @@ -33,6 +37,8 @@ defmodule Pleroma.Activity do field(:local, :boolean, default: true) field(:actor, :string) field(:recipients, {:array, :string}, default: []) + # This is a fake relation, do not use outside of with_preloaded_bookmark/get_bookmark + has_one(:bookmark, Bookmark) 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! @@ -71,6 +77,16 @@ def with_preloaded_object(query) do |> preload([activity, object], object: object) end + def with_preloaded_bookmark(query, %User{} = user) do + from([a] in query, + left_join: b in Bookmark, + on: b.user_id == ^user.id and b.activity_id == a.id, + preload: [bookmark: b] + ) + end + + def with_preloaded_bookmark(query, _), do: query + def get_by_ap_id(ap_id) do Repo.one( from( @@ -80,6 +96,16 @@ def get_by_ap_id(ap_id) do ) end + def get_bookmark(%Activity{} = activity, %User{} = user) do + if Ecto.assoc_loaded?(activity.bookmark) do + activity.bookmark + else + Bookmark.get(user.id, activity.id) + end + end + + def get_bookmark(_, _), do: nil + def change(struct, params \\ %{}) do struct |> cast(params, [:data]) @@ -260,4 +286,32 @@ def all_by_actor_and_id(actor, status_ids) do |> where([s], s.actor == ^actor) |> Repo.all() end + + def follow_requests_for_actor(%Pleroma.User{ap_id: ap_id}) do + from( + a in Activity, + where: + fragment( + "? ->> 'type' = 'Follow'", + a.data + ), + where: + fragment( + "? ->> 'state' = 'pending'", + a.data + ), + where: + fragment( + "coalesce((?)->'object'->>'id', (?)->>'object') = ?", + a.data, + a.data, + ^ap_id + ) + ) + end + + @spec query_by_actor(actor()) :: Ecto.Query.t() + def query_by_actor(actor) do + from(a in Activity, where: a.actor == ^actor) + end end diff --git a/lib/pleroma/bbs/authenticator.ex b/lib/pleroma/bbs/authenticator.ex new file mode 100644 index 000000000..a2c153720 --- /dev/null +++ b/lib/pleroma/bbs/authenticator.ex @@ -0,0 +1,16 @@ +defmodule Pleroma.BBS.Authenticator do + use Sshd.PasswordAuthenticator + alias Comeonin.Pbkdf2 + alias Pleroma.User + + def authenticate(username, password) do + username = to_string(username) + password = to_string(password) + + with %User{} = user <- User.get_by_nickname(username) do + Pbkdf2.checkpw(password, user.password_hash) + else + _e -> false + end + end +end diff --git a/lib/pleroma/bbs/handler.ex b/lib/pleroma/bbs/handler.ex new file mode 100644 index 000000000..106fe5d18 --- /dev/null +++ b/lib/pleroma/bbs/handler.ex @@ -0,0 +1,147 @@ +defmodule Pleroma.BBS.Handler do + use Sshd.ShellHandler + alias Pleroma.Activity + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.CommonAPI + + def on_shell(username, _pubkey, _ip, _port) do + :ok = IO.puts("Welcome to #{Pleroma.Config.get([:instance, :name])}!") + user = Pleroma.User.get_cached_by_nickname(to_string(username)) + Logger.debug("#{inspect(user)}") + loop(run_state(user: user)) + end + + def on_connect(username, ip, port, method) do + Logger.debug(fn -> + """ + Incoming SSH shell #{inspect(self())} requested for #{username} from #{inspect(ip)}:#{ + inspect(port) + } using #{inspect(method)} + """ + end) + end + + def on_disconnect(username, ip, port) do + Logger.debug(fn -> + "Disconnecting SSH shell for #{username} from #{inspect(ip)}:#{inspect(port)}" + end) + end + + defp loop(state) do + self_pid = self() + counter = state.counter + prefix = state.prefix + user = state.user + + input = spawn(fn -> io_get(self_pid, prefix, counter, user.nickname) end) + wait_input(state, input) + end + + def puts_activity(activity) do + status = Pleroma.Web.MastodonAPI.StatusView.render("status.json", %{activity: activity}) + IO.puts("-- #{status.id} by #{status.account.display_name} (#{status.account.acct})") + IO.puts(HtmlSanitizeEx.strip_tags(status.content)) + IO.puts("") + end + + def handle_command(state, "help") do + IO.puts("Available commands:") + IO.puts("help - This help") + IO.puts("home - Show the home timeline") + IO.puts("p - Post the given text") + IO.puts("r - Reply to the post with the given id") + IO.puts("quit - Quit") + + state + end + + def handle_command(%{user: user} = state, "r " <> text) do + text = String.trim(text) + [activity_id, rest] = String.split(text, " ", parts: 2) + + with %Activity{} <- Activity.get_by_id(activity_id), + {:ok, _activity} <- + CommonAPI.post(user, %{"status" => rest, "in_reply_to_status_id" => activity_id}) do + IO.puts("Replied!") + else + _e -> IO.puts("Could not reply...") + end + + state + end + + def handle_command(%{user: user} = state, "p " <> text) do + text = String.trim(text) + + with {:ok, _activity} <- CommonAPI.post(user, %{"status" => text}) do + IO.puts("Posted!") + else + _e -> IO.puts("Could not post...") + end + + state + end + + def handle_command(state, "home") do + user = state.user + + params = + %{} + |> Map.put("type", ["Create"]) + |> Map.put("blocking_user", user) + |> Map.put("muting_user", user) + |> Map.put("user", user) + + activities = + [user.ap_id | user.following] + |> ActivityPub.fetch_activities(params) + |> ActivityPub.contain_timeline(user) + + Enum.each(activities, fn activity -> + puts_activity(activity) + end) + + state + end + + def handle_command(state, command) do + IO.puts("Unknown command '#{command}'") + state + end + + defp wait_input(state, input) do + receive do + {:input, ^input, "quit\n"} -> + IO.puts("Exiting...") + + {:input, ^input, code} when is_binary(code) -> + code = String.trim(code) + + state = handle_command(state, code) + + loop(%{state | counter: state.counter + 1}) + + {:error, :interrupted} -> + IO.puts("Caught Ctrl+C...") + loop(%{state | counter: state.counter + 1}) + + {:input, ^input, msg} -> + :ok = Logger.warn("received unknown message: #{inspect(msg)}") + loop(%{state | counter: state.counter + 1}) + end + end + + defp run_state(opts) do + %{prefix: "pleroma", counter: 1, user: opts[:user]} + end + + defp io_get(pid, prefix, counter, username) do + prompt = prompt(prefix, counter, username) + send(pid, {:input, self(), IO.gets(:stdio, prompt)}) + end + + defp prompt(prefix, counter, username) do + prompt = "#{username}@#{prefix}:#{counter}>" + prompt <> " " + end +end diff --git a/lib/pleroma/bookmark.ex b/lib/pleroma/bookmark.ex new file mode 100644 index 000000000..7f8fd43b6 --- /dev/null +++ b/lib/pleroma/bookmark.ex @@ -0,0 +1,60 @@ +defmodule Pleroma.Bookmark do + use Ecto.Schema + + import Ecto.Changeset + import Ecto.Query + + alias Pleroma.Activity + alias Pleroma.Bookmark + alias Pleroma.FlakeId + alias Pleroma.Repo + alias Pleroma.User + + @type t :: %__MODULE__{} + + schema "bookmarks" do + belongs_to(:user, User, type: FlakeId) + belongs_to(:activity, Activity, type: FlakeId) + + timestamps() + end + + @spec create(FlakeId.t(), FlakeId.t()) :: {:ok, Bookmark.t()} | {:error, Changeset.t()} + def create(user_id, activity_id) do + attrs = %{ + user_id: user_id, + activity_id: activity_id + } + + %Bookmark{} + |> cast(attrs, [:user_id, :activity_id]) + |> validate_required([:user_id, :activity_id]) + |> unique_constraint(:activity_id, name: :bookmarks_user_id_activity_id_index) + |> Repo.insert() + end + + @spec for_user_query(FlakeId.t()) :: Ecto.Query.t() + def for_user_query(user_id) do + Bookmark + |> where(user_id: ^user_id) + |> join(:inner, [b], activity in assoc(b, :activity)) + |> preload([b, a], activity: a) + end + + def get(user_id, activity_id) do + Bookmark + |> where(user_id: ^user_id) + |> where(activity_id: ^activity_id) + |> Repo.one() + end + + @spec destroy(FlakeId.t(), FlakeId.t()) :: {:ok, Bookmark.t()} | {:error, Changeset.t()} + def destroy(user_id, activity_id) do + from(b in Bookmark, + where: b.user_id == ^user_id, + where: b.activity_id == ^activity_id + ) + |> Repo.one() + |> Repo.delete() + end +end diff --git a/lib/pleroma/captcha/kocaptcha.ex b/lib/pleroma/captcha/kocaptcha.ex index 61688e778..18931d5a0 100644 --- a/lib/pleroma/captcha/kocaptcha.ex +++ b/lib/pleroma/captcha/kocaptcha.ex @@ -15,7 +15,7 @@ def new do %{error: "Kocaptcha service unavailable"} {:ok, res} -> - json_resp = Poison.decode!(res.body) + json_resp = Jason.decode!(res.body) %{ type: :kocaptcha, diff --git a/lib/pleroma/conversation.ex b/lib/pleroma/conversation.ex new file mode 100644 index 000000000..0db195988 --- /dev/null +++ b/lib/pleroma/conversation.ex @@ -0,0 +1,75 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Conversation do + alias Pleroma.Conversation.Participation + alias Pleroma.Repo + alias Pleroma.User + use Ecto.Schema + import Ecto.Changeset + + schema "conversations" do + # This is the context ap id. + field(:ap_id, :string) + has_many(:participations, Participation) + has_many(:users, through: [:participations, :user]) + + timestamps() + end + + def creation_cng(struct, params) do + struct + |> cast(params, [:ap_id]) + |> validate_required([:ap_id]) + |> unique_constraint(:ap_id) + end + + def create_for_ap_id(ap_id) do + %__MODULE__{} + |> creation_cng(%{ap_id: ap_id}) + |> Repo.insert( + on_conflict: [set: [updated_at: NaiveDateTime.utc_now()]], + returning: true, + conflict_target: :ap_id + ) + end + + def get_for_ap_id(ap_id) do + Repo.get_by(__MODULE__, ap_id: ap_id) + end + + @doc """ + This will + 1. Create a conversation if there isn't one already + 2. Create a participation for all the people involved who don't have one already + 3. Bump all relevant participations to 'unread' + """ + def create_or_bump_for(activity) do + with true <- Pleroma.Web.ActivityPub.Visibility.is_direct?(activity), + "Create" <- activity.data["type"], + object <- Pleroma.Object.normalize(activity), + "Note" <- object.data["type"], + ap_id when is_binary(ap_id) and byte_size(ap_id) > 0 <- object.data["context"] do + {:ok, conversation} = create_for_ap_id(ap_id) + + users = User.get_users_from_set(activity.recipients, false) + + participations = + Enum.map(users, fn user -> + {:ok, participation} = + Participation.create_for_user_and_conversation(user, conversation) + + participation + end) + + {:ok, + %{ + conversation + | participations: participations + }} + else + e -> {:error, e} + end + end +end diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex new file mode 100644 index 000000000..61021fb18 --- /dev/null +++ b/lib/pleroma/conversation/participation.ex @@ -0,0 +1,81 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Conversation.Participation do + use Ecto.Schema + alias Pleroma.Conversation + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.ActivityPub.ActivityPub + import Ecto.Changeset + import Ecto.Query + + schema "conversation_participations" do + belongs_to(:user, User, type: Pleroma.FlakeId) + belongs_to(:conversation, Conversation) + field(:read, :boolean, default: false) + field(:last_activity_id, Pleroma.FlakeId, virtual: true) + + timestamps() + end + + def creation_cng(struct, params) do + struct + |> cast(params, [:user_id, :conversation_id]) + |> validate_required([:user_id, :conversation_id]) + end + + def create_for_user_and_conversation(user, conversation) do + %__MODULE__{} + |> creation_cng(%{user_id: user.id, conversation_id: conversation.id}) + |> Repo.insert( + on_conflict: [set: [read: false, updated_at: NaiveDateTime.utc_now()]], + returning: true, + conflict_target: [:user_id, :conversation_id] + ) + end + + def read_cng(struct, params) do + struct + |> cast(params, [:read]) + |> validate_required([:read]) + end + + def mark_as_read(participation) do + participation + |> read_cng(%{read: true}) + |> Repo.update() + end + + def mark_as_unread(participation) do + participation + |> read_cng(%{read: false}) + |> Repo.update() + end + + def for_user(user, params \\ %{}) do + from(p in __MODULE__, + where: p.user_id == ^user.id, + order_by: [desc: p.updated_at] + ) + |> Pleroma.Pagination.fetch_paginated(params) + |> Repo.preload(conversation: [:users]) + end + + def for_user_with_last_activity_id(user, params \\ %{}) do + for_user(user, params) + |> Enum.map(fn participation -> + activity_id = + ActivityPub.fetch_latest_activity_id_for_context(participation.conversation.ap_id, %{ + "user" => user, + "blocking_user" => user + }) + + %{ + participation + | last_activity_id: activity_id + } + end) + end +end diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex index dab8910c1..3d7c36d21 100644 --- a/lib/pleroma/formatter.ex +++ b/lib/pleroma/formatter.ex @@ -113,9 +113,7 @@ def emojify(text, emoji, strip \\ false) do html = if not strip do - "#{emoji}" + "#{emoji}" else "" end @@ -130,12 +128,23 @@ def demojify(text) do def demojify(text, nil), do: text + @doc "Outputs a list of the emoji-shortcodes in a text" def get_emoji(text) when is_binary(text) do Enum.filter(Emoji.get_all(), fn {emoji, _, _} -> String.contains?(text, ":#{emoji}:") end) end def get_emoji(_), do: [] + @doc "Outputs a list of the emoji-Maps in a text" + def get_emoji_map(text) when is_binary(text) do + get_emoji(text) + |> Enum.reduce(%{}, fn {name, file, _group}, acc -> + Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url()}#{file}") + end) + end + + def get_emoji_map(_), do: [] + def html_escape({text, mentions, hashtags}, type) do {html_escape(text, type), mentions, hashtags} end diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex index cf6c0ee0a..d1da746de 100644 --- a/lib/pleroma/html.ex +++ b/lib/pleroma/html.ex @@ -28,12 +28,18 @@ def filter_tags(html, scrubber), do: Scrubber.scrub(html, scrubber) def filter_tags(html), do: filter_tags(html, nil) def strip_tags(html), do: Scrubber.scrub(html, Scrubber.StripTags) - def get_cached_scrubbed_html_for_activity(content, scrubbers, activity, key \\ "") do + def get_cached_scrubbed_html_for_activity( + content, + scrubbers, + activity, + key \\ "", + callback \\ fn x -> x end + ) do key = "#{key}#{generate_scrubber_signature(scrubbers)}|#{activity.id}" Cachex.fetch!(:scrubber_cache, key, fn _key -> object = Pleroma.Object.normalize(activity) - ensure_scrubbed_html(content, scrubbers, object.data["fake"] || false) + ensure_scrubbed_html(content, scrubbers, object.data["fake"] || false, callback) end) end @@ -42,24 +48,27 @@ def get_cached_stripped_html_for_activity(content, activity, key) do content, HtmlSanitizeEx.Scrubber.StripTags, activity, - key + key, + &HtmlEntities.decode/1 ) end def ensure_scrubbed_html( content, scrubbers, - false = _fake + fake, + callback ) do - {:commit, filter_tags(content, scrubbers)} - end + content = + content + |> filter_tags(scrubbers) + |> callback.() - def ensure_scrubbed_html( - content, - scrubbers, - true = _fake - ) do - {:ignore, filter_tags(content, scrubbers)} + if fake do + {:ignore, content} + else + {:commit, content} + end end defp generate_scrubber_signature(scrubber) when is_atom(scrubber) do @@ -142,6 +151,7 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do Meta.allow_tag_with_these_attributes("img", [ "width", "height", + "class", "title", "alt" ]) @@ -212,6 +222,7 @@ defmodule Pleroma.HTML.Scrubber.Default do Meta.allow_tag_with_these_attributes("img", [ "width", "height", + "class", "title", "alt" ]) diff --git a/lib/pleroma/object/containment.ex b/lib/pleroma/object/containment.ex index 25bd911fb..2f4687fa2 100644 --- a/lib/pleroma/object/containment.ex +++ b/lib/pleroma/object/containment.ex @@ -1,7 +1,5 @@ defmodule Pleroma.Object.Containment do @moduledoc """ - # Object Containment - This module contains some useful functions for containing objects to specific origins and determining those origins. They previously lived in the ActivityPub `Transmogrifier` module. diff --git a/lib/pleroma/plugs/http_security_plug.ex b/lib/pleroma/plugs/http_security_plug.ex index f701aaaa5..a476f1d49 100644 --- a/lib/pleroma/plugs/http_security_plug.ex +++ b/lib/pleroma/plugs/http_security_plug.ex @@ -35,7 +35,7 @@ defp headers do defp csp_string do scheme = Config.get([Pleroma.Web.Endpoint, :url])[:scheme] static_url = Pleroma.Web.Endpoint.static_url() - websocket_url = String.replace(static_url, "http", "ws") + websocket_url = Pleroma.Web.Endpoint.websocket_url() connect_src = "connect-src 'self' #{static_url} #{websocket_url}" diff --git a/lib/pleroma/plugs/oauth_plug.ex b/lib/pleroma/plugs/oauth_plug.ex index 5888d596a..86bc4aa3a 100644 --- a/lib/pleroma/plugs/oauth_plug.ex +++ b/lib/pleroma/plugs/oauth_plug.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Plugs.OAuthPlug do alias Pleroma.Repo alias Pleroma.User + alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.Token @realm_reg Regex.compile!("Bearer\:?\s+(.*)$", "i") @@ -16,14 +17,45 @@ def init(options), do: options def call(%{assigns: %{user: %User{}}} = conn, _), do: conn - def call(conn, _) do - with {:ok, token_str} <- fetch_token_str(conn), - {:ok, user, token_record} <- fetch_user_and_token(token_str) do + def call(%{params: %{"access_token" => access_token}} = conn, _) do + with {:ok, user, token_record} <- fetch_user_and_token(access_token) do conn |> assign(:token, token_record) |> assign(:user, user) else - _ -> conn + _ -> + # token found, but maybe only with app + with {:ok, app, token_record} <- fetch_app_and_token(access_token) do + conn + |> assign(:token, token_record) + |> assign(:app, app) + else + _ -> conn + end + end + end + + def call(conn, _) do + case fetch_token_str(conn) do + {:ok, token} -> + with {:ok, user, token_record} <- fetch_user_and_token(token) do + conn + |> assign(:token, token_record) + |> assign(:user, user) + else + _ -> + # token found, but maybe only with app + with {:ok, app, token_record} <- fetch_app_and_token(token) do + conn + |> assign(:token, token_record) + |> assign(:app, app) + else + _ -> conn + end + end + + _ -> + conn end end @@ -44,6 +76,16 @@ defp fetch_user_and_token(token) do end end + @spec fetch_app_and_token(String.t()) :: {:ok, App.t(), Token.t()} | nil + defp fetch_app_and_token(token) do + query = + from(t in Token, where: t.token == ^token, join: app in assoc(t, :app), preload: [app: app]) + + with %Token{app: app} = token_record <- Repo.one(query) do + {:ok, app, token_record} + end + end + # Gets token from session by :oauth_token key # @spec fetch_token_from_session(Plug.Conn.t()) :: :no_token_found | {:ok, String.t()} diff --git a/lib/pleroma/plugs/rate_limit_plug.ex b/lib/pleroma/plugs/rate_limit_plug.ex new file mode 100644 index 000000000..466f64a79 --- /dev/null +++ b/lib/pleroma/plugs/rate_limit_plug.ex @@ -0,0 +1,36 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.RateLimitPlug do + import Phoenix.Controller, only: [json: 2] + import Plug.Conn + + def init(opts), do: opts + + def call(conn, opts) do + enabled? = Pleroma.Config.get([:app_account_creation, :enabled]) + + case check_rate(conn, Map.put(opts, :enabled, enabled?)) do + {:ok, _count} -> conn + {:error, _count} -> render_error(conn) + %Plug.Conn{} = conn -> conn + end + end + + defp check_rate(conn, %{enabled: true} = opts) do + max_requests = opts[:max_requests] + bucket_name = conn.remote_ip |> Tuple.to_list() |> Enum.join(".") + + ExRated.check_rate(bucket_name, opts[:interval] * 1000, max_requests) + end + + defp check_rate(conn, _), do: conn + + defp render_error(conn) do + conn + |> put_status(:forbidden) + |> json(%{error: "Rate limit exceeded."}) + |> halt() + end +end diff --git a/lib/pleroma/repo.ex b/lib/pleroma/repo.ex index aa5d427ae..f57e088bc 100644 --- a/lib/pleroma/repo.ex +++ b/lib/pleroma/repo.ex @@ -19,4 +19,32 @@ defmodule Instrumenter do def init(_, opts) do {:ok, Keyword.put(opts, :url, System.get_env("DATABASE_URL"))} end + + @doc "find resource based on prepared query" + @spec find_resource(Ecto.Query.t()) :: {:ok, struct()} | {:error, :not_found} + def find_resource(%Ecto.Query{} = query) do + case __MODULE__.one(query) do + nil -> {:error, :not_found} + resource -> {:ok, resource} + end + end + + def find_resource(_query), do: {:error, :not_found} + + @doc """ + Gets association from cache or loads if need + + ## Examples + + iex> Repo.get_assoc(token, :user) + %User{} + + """ + @spec get_assoc(struct(), atom()) :: {:ok, struct()} | {:error, :not_found} + def get_assoc(resource, association) do + case __MODULE__.preload(resource, association) do + %{^association => assoc} when not is_nil(assoc) -> {:ok, assoc} + _ -> {:error, :not_found} + end + end end diff --git a/lib/pleroma/stats.ex b/lib/pleroma/stats.ex index 2e7d747df..5b242927b 100644 --- a/lib/pleroma/stats.ex +++ b/lib/pleroma/stats.ex @@ -34,7 +34,7 @@ def schedule_update do def update_stats do peers = from( - u in Pleroma.User, + u in User, select: fragment("distinct split_part(?, '@', 2)", u.nickname), where: u.local != ^true ) @@ -44,10 +44,13 @@ def update_stats do domain_count = Enum.count(peers) status_query = - from(u in User.local_user_query(), select: fragment("sum((?->>'note_count')::int)", u.info)) + from(u in User.Query.build(%{local: true}), + select: fragment("sum((?->>'note_count')::int)", u.info) + ) status_count = Repo.one(status_query) - user_count = Repo.aggregate(User.active_local_user_query(), :count, :id) + + user_count = Repo.aggregate(User.Query.build(%{local: true, active: true}), :count, :id) Agent.update(__MODULE__, fn _ -> {peers, %{domain_count: domain_count, status_count: status_count, user_count: user_count}} diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex index f72334930..c47d65241 100644 --- a/lib/pleroma/upload.ex +++ b/lib/pleroma/upload.ex @@ -4,7 +4,7 @@ defmodule Pleroma.Upload do @moduledoc """ - # Upload + Manage user uploads Options: * `:type`: presets for activity type (defaults to Document) and size limits from app configuration diff --git a/lib/pleroma/uploaders/swift/keystone.ex b/lib/pleroma/uploaders/swift/keystone.ex index 3046cdbd2..dd44c7561 100644 --- a/lib/pleroma/uploaders/swift/keystone.ex +++ b/lib/pleroma/uploaders/swift/keystone.ex @@ -14,7 +14,7 @@ def process_url(url) do def process_response_body(body) do body - |> Poison.decode!() + |> Jason.decode!() end def get_token do @@ -38,7 +38,7 @@ def get_token do end def make_auth_body(username, password, tenant) do - Poison.encode!(%{ + Jason.encode!(%{ :auth => %{ :passwordCredentials => %{ :username => username, diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index f1feab279..474de9ba5 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -10,7 +10,6 @@ defmodule Pleroma.User do alias Comeonin.Pbkdf2 alias Pleroma.Activity - alias Pleroma.Formatter alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Registration @@ -53,7 +52,6 @@ defmodule Pleroma.User do field(:search_rank, :float, virtual: true) field(:search_type, :integer, virtual: true) field(:tags, {:array, :string}, default: []) - field(:bookmarks, {:array, :string}, default: []) field(:last_refreshed_at, :naive_datetime_usec) has_many(:notifications, Notification) has_many(:registrations, Registration) @@ -206,14 +204,15 @@ def reset_password(user, data) do end def register_changeset(struct, params \\ %{}, opts \\ []) do - confirmation_status = - if opts[:confirmed] || !Pleroma.Config.get([:instance, :account_activation_required]) do - :confirmed + need_confirmation? = + if is_nil(opts[:need_confirmation]) do + Pleroma.Config.get([:instance, :account_activation_required]) else - :unconfirmed + opts[:need_confirmation] end - info_change = User.Info.confirmation_changeset(%User.Info{}, confirmation_status) + info_change = + User.Info.confirmation_changeset(%User.Info{}, need_confirmation: need_confirmation?) changeset = struct @@ -256,10 +255,7 @@ defp autofollow_users(user) do candidates = Pleroma.Config.get([:instance, :autofollowed_nicknames]) autofollowed_users = - from(u in User, - where: u.local == true, - where: u.nickname in ^candidates - ) + User.Query.build(%{nickname: candidates, local: true}) |> Repo.all() follow_all(user, autofollowed_users) @@ -422,7 +418,7 @@ def follow_import(%User{} = follower, followed_identifiers) Enum.map( followed_identifiers, fn followed_identifier -> - with %User{} = followed <- get_or_fetch(followed_identifier), + with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier), {:ok, follower} <- maybe_direct_follow(follower, followed), {:ok, _} <- ActivityPub.follow(follower, followed) do followed @@ -506,7 +502,15 @@ def get_cached_by_id(id) do def get_cached_by_nickname(nickname) do key = "nickname:#{nickname}" - Cachex.fetch!(:user_cache, key, fn _ -> get_or_fetch_by_nickname(nickname) end) + + Cachex.fetch!(:user_cache, key, fn -> + user_result = get_or_fetch_by_nickname(nickname) + + case user_result do + {:ok, user} -> {:commit, user} + {:error, _error} -> {:ignore, nil} + end + end) end def get_cached_by_nickname_or_id(nickname_or_id) do @@ -542,7 +546,7 @@ def fetch_by_nickname(nickname) do def get_or_fetch_by_nickname(nickname) do with %User{} = user <- get_by_nickname(nickname) do - user + {:ok, user} else _e -> with [_nick, _domain] <- String.split(nickname, "@"), @@ -552,9 +556,9 @@ def get_or_fetch_by_nickname(nickname) do {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user]) end - user + {:ok, user} else - _e -> nil + _e -> {:error, "not found " <> nickname} end end end @@ -570,19 +574,17 @@ def fetch_initial_posts(user) do ) end - def get_followers_query(%User{id: id, follower_address: follower_address}, nil) do - from( - u in User, - where: fragment("? <@ ?", ^[follower_address], u.following), - where: u.id != ^id - ) + @spec get_followers_query(User.t(), pos_integer() | nil) :: Ecto.Query.t() + def get_followers_query(%User{} = user, nil) do + User.Query.build(%{followers: user}) end def get_followers_query(user, page) do from(u in get_followers_query(user, nil)) - |> paginate(page, 20) + |> User.Query.paginate(page, 20) end + @spec get_followers_query(User.t()) :: Ecto.Query.t() def get_followers_query(user), do: get_followers_query(user, nil) def get_followers(user, page \\ nil) do @@ -597,19 +599,17 @@ def get_followers_ids(user, page \\ nil) do Repo.all(from(u in q, select: u.id)) end - def get_friends_query(%User{id: id, following: following}, nil) do - from( - u in User, - where: u.follower_address in ^following, - where: u.id != ^id - ) + @spec get_friends_query(User.t(), pos_integer() | nil) :: Ecto.Query.t() + def get_friends_query(%User{} = user, nil) do + User.Query.build(%{friends: user}) end def get_friends_query(user, page) do from(u in get_friends_query(user, nil)) - |> paginate(page, 20) + |> User.Query.paginate(page, 20) end + @spec get_friends_query(User.t()) :: Ecto.Query.t() def get_friends_query(user), do: get_friends_query(user, nil) def get_friends(user, page \\ nil) do @@ -624,33 +624,10 @@ def get_friends_ids(user, page \\ nil) do Repo.all(from(u in q, select: u.id)) end - def get_follow_requests_query(%User{} = user) do - from( - a in Activity, - where: - fragment( - "? ->> 'type' = 'Follow'", - a.data - ), - where: - fragment( - "? ->> 'state' = 'pending'", - a.data - ), - where: - fragment( - "coalesce((?)->'object'->>'id', (?)->>'object') = ?", - a.data, - a.data, - ^user.ap_id - ) - ) - end - + @spec get_follow_requests(User.t()) :: {:ok, [User.t()]} def get_follow_requests(%User{} = user) do users = - user - |> User.get_follow_requests_query() + Activity.follow_requests_for_actor(user) |> join(:inner, [a], u in User, on: a.actor == u.ap_id) |> where([a, u], not fragment("? @> ?", u.following, ^[user.follower_address])) |> group_by([a, u], u.id) @@ -723,10 +700,7 @@ def update_note_count(%User{} = user) do def update_follower_count(%User{} = user) do follower_count_query = - User - |> where([u], ^user.follower_address in u.following) - |> where([u], u.id != ^user.id) - |> select([u], %{count: count(u.id)}) + User.Query.build(%{followers: user}) |> select([u], %{count: count(u.id)}) User |> where(id: ^user.id) @@ -749,38 +723,19 @@ def update_follower_count(%User{} = user) do end end - def get_users_from_set_query(ap_ids, false) do - from( - u in User, - where: u.ap_id in ^ap_ids - ) - end - - def get_users_from_set_query(ap_ids, true) do - query = get_users_from_set_query(ap_ids, false) - - from( - u in query, - where: u.local == true - ) - end - + @spec get_users_from_set([String.t()], boolean()) :: [User.t()] def get_users_from_set(ap_ids, local_only \\ true) do - get_users_from_set_query(ap_ids, local_only) + criteria = %{ap_id: ap_ids} + criteria = if local_only, do: Map.put(criteria, :local, true), else: criteria + + User.Query.build(criteria) |> Repo.all() end + @spec get_recipients_from_activity(Activity.t()) :: [User.t()] def get_recipients_from_activity(%Activity{recipients: to}) do - query = - from( - u in User, - where: u.ap_id in ^to, - or_where: fragment("? && ?", u.following, ^to) - ) - - query = from(u in query, where: u.local == true) - - Repo.all(query) + User.Query.build(%{recipients_from_activity: to, local: true}) + |> Repo.all() end def search(query, resolve \\ false, for_user \\ nil) do @@ -901,7 +856,7 @@ def blocks_import(%User{} = blocker, blocked_identifiers) when is_list(blocked_i Enum.map( blocked_identifiers, fn blocked_identifier -> - with %User{} = blocked <- get_or_fetch(blocked_identifier), + with {:ok, %User{} = blocked} <- get_or_fetch(blocked_identifier), {:ok, blocker} <- block(blocker, blocked), {:ok, _} <- ActivityPub.block(blocker, blocked) do blocked @@ -1042,14 +997,23 @@ def subscribed_to?(user, %{ap_id: ap_id}) do end end - def muted_users(user), - do: Repo.all(from(u in User, where: u.ap_id in ^user.info.mutes)) + @spec muted_users(User.t()) :: [User.t()] + def muted_users(user) do + User.Query.build(%{ap_id: user.info.mutes}) + |> Repo.all() + end - def blocked_users(user), - do: Repo.all(from(u in User, where: u.ap_id in ^user.info.blocks)) + @spec blocked_users(User.t()) :: [User.t()] + def blocked_users(user) do + User.Query.build(%{ap_id: user.info.blocks}) + |> Repo.all() + end - def subscribers(user), - do: Repo.all(from(u in User, where: u.ap_id in ^user.info.subscribers)) + @spec subscribers(User.t()) :: [User.t()] + def subscribers(user) do + User.Query.build(%{ap_id: user.info.subscribers}) + |> Repo.all() + end def block_domain(user, domain) do info_cng = @@ -1075,69 +1039,6 @@ def unblock_domain(user, domain) do update_and_set_cache(cng) end - def maybe_local_user_query(query, local) do - if local, do: local_user_query(query), else: query - end - - def local_user_query(query \\ User) do - from( - u in query, - where: u.local == true, - where: not is_nil(u.nickname) - ) - end - - def maybe_external_user_query(query, external) do - if external, do: external_user_query(query), else: query - end - - def external_user_query(query \\ User) do - from( - u in query, - where: u.local == false, - where: not is_nil(u.nickname) - ) - end - - def maybe_active_user_query(query, active) do - if active, do: active_user_query(query), else: query - end - - def active_user_query(query \\ User) do - from( - u in query, - where: fragment("not (?->'deactivated' @> 'true')", u.info), - where: not is_nil(u.nickname) - ) - end - - def maybe_deactivated_user_query(query, deactivated) do - if deactivated, do: deactivated_user_query(query), else: query - end - - def deactivated_user_query(query \\ User) do - from( - u in query, - where: fragment("(?->'deactivated' @> 'true')", u.info), - where: not is_nil(u.nickname) - ) - end - - def active_local_user_query do - from( - u in local_user_query(), - where: fragment("not (?->'deactivated' @> 'true')", u.info) - ) - end - - def moderator_user_query do - from( - u in User, - where: u.local == true, - where: fragment("?->'is_moderator' @> 'true'", u.info) - ) - end - def deactivate(%User{} = user, status \\ true) do info_cng = User.Info.set_activation_status(user.info, status) @@ -1156,7 +1057,12 @@ def update_notification_settings(%User{} = user, settings \\ %{}) do |> update_and_set_cache() end - def delete(%User{} = user) do + @spec delete(User.t()) :: :ok + def delete(%User{} = user), + do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user]) + + @spec perform(atom(), User.t()) :: {:ok, User.t()} + def perform(:delete, %User{} = user) do {:ok, user} = User.deactivate(user) # Remove all relationships @@ -1172,22 +1078,23 @@ def delete(%User{} = user) do end def delete_user_activities(%User{ap_id: ap_id} = user) do - Activity - |> where(actor: ^ap_id) - |> Activity.with_preloaded_object() - |> Repo.all() - |> Enum.each(fn - %{data: %{"type" => "Create"}} = activity -> - activity |> Object.normalize() |> ActivityPub.delete() + stream = + ap_id + |> Activity.query_by_actor() + |> Activity.with_preloaded_object() + |> Repo.stream() - # TODO: Do something with likes, follows, repeats. - _ -> - "Doing nothing" - end) + Repo.transaction(fn -> Enum.each(stream, &delete_activity(&1)) end, timeout: :infinity) {:ok, user} end + defp delete_activity(%{data: %{"type" => "Create"}} = activity) do + Object.normalize(activity) |> ActivityPub.delete() + end + + defp delete_activity(_activity), do: "Doing nothing" + def html_filter_policy(%User{info: %{no_rich_text: true}}) do Pleroma.HTML.Scrubber.TwitterText end @@ -1201,11 +1108,11 @@ def fetch_by_ap_id(ap_id) do case ap_try do {:ok, user} -> - user + {:ok, user} _ -> case OStatus.make_user(ap_id) do - {:ok, user} -> user + {:ok, user} -> {:ok, user} _ -> {:error, "Could not fetch by AP id"} end end @@ -1215,20 +1122,20 @@ def get_or_fetch_by_ap_id(ap_id) do user = get_cached_by_ap_id(ap_id) if !is_nil(user) and !User.needs_update?(user) do - user + {:ok, user} else # Whether to fetch initial posts for the user (if it's a new user & the fetching is enabled) should_fetch_initial = is_nil(user) and Pleroma.Config.get([:fetch_initial_posts, :enabled]) - user = fetch_by_ap_id(ap_id) + resp = fetch_by_ap_id(ap_id) if should_fetch_initial do - with %User{} = user do + with {:ok, %User{} = user} = resp do {:ok, _} = Task.start(__MODULE__, :fetch_initial_posts, [user]) end end - user + resp end end @@ -1270,7 +1177,7 @@ def public_key_from_info(%{magic_key: magic_key}) do end def get_public_key_for_ap_id(ap_id) do - with %User{} = user <- get_or_fetch_by_ap_id(ap_id), + with {:ok, %User{} = user} <- get_or_fetch_by_ap_id(ap_id), {:ok, public_key} <- public_key_from_info(user.info) do {:ok, public_key} else @@ -1294,7 +1201,7 @@ def ap_enabled?(%User{info: info}), do: info.ap_enabled def ap_enabled?(_), do: false @doc "Gets or fetch a user by uri or nickname." - @spec get_or_fetch(String.t()) :: User.t() + @spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()} def get_or_fetch("http" <> _host = uri), do: get_or_fetch_by_ap_id(uri) def get_or_fetch(nickname), do: get_or_fetch_by_nickname(nickname) @@ -1322,18 +1229,15 @@ def wait_and_refresh(timeout, %User{} = a, %User{} = b) do end end - def parse_bio(bio, user \\ %User{info: %{source_data: %{}}}) - def parse_bio(nil, _user), do: "" - def parse_bio(bio, _user) when bio == "", do: bio + def parse_bio(bio) when is_binary(bio) and bio != "" do + bio + |> CommonUtils.format_input("text/plain", mentions_format: :full) + |> elem(0) + end - def parse_bio(bio, user) do - emoji = - (user.info.source_data["tag"] || []) - |> Enum.filter(fn %{"type" => t} -> t == "Emoji" end) - |> Enum.map(fn %{"icon" => %{"url" => url}, "name" => name} -> - {String.trim(name, ":"), url} - end) + def parse_bio(_), do: "" + def parse_bio(bio, user) when is_binary(bio) and bio != "" do # TODO: get profile URLs other than user.ap_id profile_urls = [user.ap_id] @@ -1343,9 +1247,10 @@ def parse_bio(bio, user) do rel: &RelMe.maybe_put_rel_me(&1, profile_urls) ) |> elem(0) - |> Formatter.emojify(emoji) end + def parse_bio(_, _), do: "" + def tag(user_identifiers, tags) when is_list(user_identifiers) do Repo.transaction(fn -> for user_identifier <- user_identifiers, do: tag(user_identifier, tags) @@ -1379,22 +1284,6 @@ defp update_tags(%User{} = user, new_tags) do updated_user end - def bookmark(%User{} = user, status_id) do - bookmarks = Enum.uniq(user.bookmarks ++ [status_id]) - update_bookmarks(user, bookmarks) - end - - def unbookmark(%User{} = user, status_id) do - bookmarks = Enum.uniq(user.bookmarks -- [status_id]) - update_bookmarks(user, bookmarks) - end - - def update_bookmarks(%User{} = user, bookmarks) do - user - |> change(%{bookmarks: bookmarks}) - |> update_and_set_cache - end - defp normalize_tags(tags) do [tags] |> List.flatten() @@ -1429,22 +1318,12 @@ def error_user(ap_id) do } end + @spec all_superusers() :: [User.t()] def all_superusers do - from( - u in User, - where: u.local == true, - where: fragment("?->'is_admin' @> 'true' OR ?->'is_moderator' @> 'true'", u.info, u.info) - ) + User.Query.build(%{super_users: true, local: true}) |> Repo.all() end - defp paginate(query, page, page_size) do - from(u in query, - limit: ^page_size, - offset: ^((page - 1) * page_size) - ) - end - def showing_reblogs?(%User{} = user, %User{} = target) do target.ap_id not in user.info.muted_reblogs end diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index 7f22a45b5..5a50ee639 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -8,6 +8,8 @@ defmodule Pleroma.User.Info do alias Pleroma.User.Info + @type t :: %__MODULE__{} + embedded_schema do field(:banner, :map, default: %{}) field(:background, :map, default: %{}) @@ -41,6 +43,7 @@ defmodule Pleroma.User.Info do field(:hide_favorites, :boolean, default: true) field(:pinned_activities, {:array, :string}, default: []) field(:flavour, :string, default: nil) + field(:emoji, {:array, :map}, default: []) field(:notification_settings, :map, default: %{"remote" => true, "local" => true, "followers" => true, "follows" => true} @@ -209,32 +212,26 @@ def profile_update(info, params) do ]) end - def confirmation_changeset(info, :confirmed) do - confirmation_changeset(info, %{ - confirmation_pending: false, - confirmation_token: nil - }) - end + @spec confirmation_changeset(Info.t(), keyword()) :: Ecto.Changerset.t() + def confirmation_changeset(info, opts) do + need_confirmation? = Keyword.get(opts, :need_confirmation) - def confirmation_changeset(info, :unconfirmed) do - confirmation_changeset(info, %{ - confirmation_pending: true, - confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64() - }) - end + params = + if need_confirmation? do + %{ + confirmation_pending: true, + confirmation_token: :crypto.strong_rand_bytes(32) |> Base.url_encode64() + } + else + %{ + confirmation_pending: false, + confirmation_token: nil + } + end - def confirmation_changeset(info, params) do cast(info, params, [:confirmation_pending, :confirmation_token]) end - def mastodon_profile_update(info, params) do - info - |> cast(params, [ - :locked, - :banner - ]) - end - def mastodon_settings_update(info, settings) do params = %{settings: settings} diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex new file mode 100644 index 000000000..2dfe5ce92 --- /dev/null +++ b/lib/pleroma/user/query.ex @@ -0,0 +1,150 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.User.Query do + @moduledoc """ + User query builder module. Builds query from new query or another user query. + + ## Example: + query = Pleroma.User.Query(%{nickname: "nickname"}) + another_query = Pleroma.User.Query.build(query, %{email: "email@example.com"}) + Pleroma.Repo.all(query) + Pleroma.Repo.all(another_query) + + Adding new rules: + - *ilike criteria* + - add field to @ilike_criteria list + - pass non empty string + - e.g. Pleroma.User.Query.build(%{nickname: "nickname"}) + - *equal criteria* + - add field to @equal_criteria list + - pass non empty string + - e.g. Pleroma.User.Query.build(%{email: "email@example.com"}) + - *contains criteria* + - add field to @containns_criteria list + - pass values list + - e.g. Pleroma.User.Query.build(%{ap_id: ["http://ap_id1", "http://ap_id2"]}) + """ + import Ecto.Query + import Pleroma.Web.AdminAPI.Search, only: [not_empty_string: 1] + alias Pleroma.User + + @type criteria :: + %{ + query: String.t(), + tags: [String.t()], + name: String.t(), + email: String.t(), + local: boolean(), + external: boolean(), + active: boolean(), + deactivated: boolean(), + is_admin: boolean(), + is_moderator: boolean(), + super_users: boolean(), + followers: User.t(), + friends: User.t(), + recipients_from_activity: [String.t()], + nickname: [String.t()], + ap_id: [String.t()] + } + | %{} + + @ilike_criteria [:nickname, :name, :query] + @equal_criteria [:email] + @role_criteria [:is_admin, :is_moderator] + @contains_criteria [:ap_id, :nickname] + + @spec build(criteria()) :: Query.t() + def build(query \\ base_query(), criteria) do + prepare_query(query, criteria) + end + + @spec paginate(Ecto.Query.t(), pos_integer(), pos_integer()) :: Ecto.Query.t() + def paginate(query, page, page_size) do + from(u in query, + limit: ^page_size, + offset: ^((page - 1) * page_size) + ) + end + + defp base_query do + from(u in User) + end + + defp prepare_query(query, criteria) do + Enum.reduce(criteria, query, &compose_query/2) + end + + defp compose_query({key, value}, query) + when key in @ilike_criteria and not_empty_string(value) do + # hack for :query key + key = if key == :query, do: :nickname, else: key + where(query, [u], ilike(field(u, ^key), ^"%#{value}%")) + end + + defp compose_query({key, value}, query) + when key in @equal_criteria and not_empty_string(value) do + where(query, [u], ^[{key, value}]) + end + + defp compose_query({key, values}, query) when key in @contains_criteria and is_list(values) do + where(query, [u], field(u, ^key) in ^values) + end + + defp compose_query({:tags, tags}, query) when is_list(tags) and length(tags) > 0 do + Enum.reduce(tags, query, &prepare_tag_criteria/2) + end + + defp compose_query({key, _}, query) when key in @role_criteria do + where(query, [u], fragment("(?->? @> 'true')", u.info, ^to_string(key))) + end + + defp compose_query({:super_users, _}, query) do + where( + query, + [u], + fragment("?->'is_admin' @> 'true' OR ?->'is_moderator' @> 'true'", u.info, u.info) + ) + end + + defp compose_query({:local, _}, query), do: location_query(query, true) + + defp compose_query({:external, _}, query), do: location_query(query, false) + + defp compose_query({:active, _}, query) do + where(query, [u], fragment("not (?->'deactivated' @> 'true')", u.info)) + |> where([u], not is_nil(u.nickname)) + end + + defp compose_query({:deactivated, _}, query) do + where(query, [u], fragment("?->'deactivated' @> 'true'", u.info)) + |> where([u], not is_nil(u.nickname)) + end + + defp compose_query({:followers, %User{id: id, follower_address: follower_address}}, query) do + where(query, [u], fragment("? <@ ?", ^[follower_address], u.following)) + |> where([u], u.id != ^id) + end + + defp compose_query({:friends, %User{id: id, following: following}}, query) do + where(query, [u], u.follower_address in ^following) + |> where([u], u.id != ^id) + end + + defp compose_query({:recipients_from_activity, to}, query) do + where(query, [u], u.ap_id in ^to or fragment("? && ?", u.following, ^to)) + end + + defp compose_query(_unsupported_param, query), do: query + + defp prepare_tag_criteria(tag, query) do + or_where(query, [u], fragment("? = any(?)", ^tag, u.tags)) + end + + defp location_query(query, local) do + where(query, [u], u.local == ^local) + |> where([u], not is_nil(u.nickname)) + end +end diff --git a/lib/pleroma/user_invite_token.ex b/lib/pleroma/user_invite_token.ex index 86f0a5486..fadc89891 100644 --- a/lib/pleroma/user_invite_token.ex +++ b/lib/pleroma/user_invite_token.ex @@ -24,7 +24,7 @@ defmodule Pleroma.UserInviteToken do timestamps() end - @spec create_invite(map()) :: UserInviteToken.t() + @spec create_invite(map()) :: {:ok, UserInviteToken.t()} def create_invite(params \\ %{}) do %UserInviteToken{} |> cast(params, [:max_use, :expires_at]) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 3b71e0369..eba8f8018 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -4,7 +4,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do alias Pleroma.Activity - alias Pleroma.Instances + alias Pleroma.Conversation alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Object.Fetcher @@ -14,7 +14,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do alias Pleroma.User alias Pleroma.Web.ActivityPub.MRF alias Pleroma.Web.ActivityPub.Transmogrifier - alias Pleroma.Web.Federator alias Pleroma.Web.WebFinger import Ecto.Query @@ -23,8 +22,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do require Logger - @httpoison Application.get_env(:pleroma, :httpoison) - # For Announce activities, we filter the recipients based on following status for any actors # that match actual users. See issue #164 for more information about why this is necessary. defp get_recipients(%{"type" => "Announce"} = data) do @@ -140,7 +137,14 @@ def insert(map, local \\ true, fake \\ false) when is_map(map) do end) Notification.create_notifications(activity) + + participations = + activity + |> Conversation.create_or_bump_for() + |> get_participations() + stream_out(activity) + stream_out_participations(participations) {:ok, activity} else %Activity{} = activity -> @@ -163,11 +167,23 @@ def insert(map, local \\ true, fake \\ false) when is_map(map) do end end + defp get_participations({:ok, %{participations: participations}}), do: participations + defp get_participations(_), do: [] + + def stream_out_participations(participations) do + participations = + participations + |> Repo.preload(:user) + + Enum.each(participations, fn participation -> + Pleroma.Web.Streamer.stream("participation", participation) + end) + end + def stream_out(activity) do public = "https://www.w3.org/ns/activitystreams#Public" if activity.data["type"] in ["Create", "Announce", "Delete"] do - object = Object.normalize(activity) Pleroma.Web.Streamer.stream("user", activity) Pleroma.Web.Streamer.stream("list", activity) @@ -179,6 +195,8 @@ def stream_out(activity) do end if activity.data["type"] in ["Create"] do + object = Object.normalize(activity) + object.data |> Map.get("tag", []) |> Enum.filter(fn tag -> is_bitstring(tag) end) @@ -193,6 +211,7 @@ def stream_out(activity) do end end else + # TODO: Write test, replace with visibility test if !Enum.member?(activity.data["cc"] || [], public) && !Enum.member?( activity.data["to"], @@ -455,35 +474,44 @@ def flag( end end - def fetch_activities_for_context(context, opts \\ %{}) do + defp fetch_activities_for_context_query(context, opts) do public = ["https://www.w3.org/ns/activitystreams#Public"] recipients = if opts["user"], do: [opts["user"].ap_id | opts["user"].following] ++ public, else: public - query = from(activity in Activity) - - query = - query - |> restrict_blocked(opts) - |> restrict_recipients(recipients, opts["user"]) - - query = - from( - activity in query, - where: - fragment( - "?->>'type' = ? and ?->>'context' = ?", - activity.data, - "Create", - activity.data, - ^context - ), - order_by: [desc: :id] + from(activity in Activity) + |> restrict_blocked(opts) + |> restrict_recipients(recipients, opts["user"]) + |> where( + [activity], + fragment( + "?->>'type' = ? and ?->>'context' = ?", + activity.data, + "Create", + activity.data, + ^context ) - |> Activity.with_preloaded_object() + ) + |> order_by([activity], desc: activity.id) + end - Repo.all(query) + @spec fetch_activities_for_context(String.t(), keyword() | map()) :: [Activity.t()] + def fetch_activities_for_context(context, opts \\ %{}) do + context + |> fetch_activities_for_context_query(opts) + |> Activity.with_preloaded_object() + |> Repo.all() + end + + @spec fetch_latest_activity_id_for_context(String.t(), keyword() | map()) :: + Pleroma.FlakeId.t() | nil + def fetch_latest_activity_id_for_context(context, opts \\ %{}) do + context + |> fetch_activities_for_context_query(opts) + |> limit(1) + |> select([a], a.id) + |> Repo.one() end def fetch_public_activities(opts \\ %{}) do @@ -782,9 +810,30 @@ defp maybe_preload_objects(query, _) do |> Activity.with_preloaded_object() end + defp maybe_preload_bookmarks(query, %{"skip_preload" => true}), do: query + + defp maybe_preload_bookmarks(query, opts) do + query + |> Activity.with_preloaded_bookmark(opts["user"]) + end + + defp maybe_order(query, %{order: :desc}) do + query + |> order_by(desc: :id) + end + + defp maybe_order(query, %{order: :asc}) do + query + |> order_by(asc: :id) + end + + defp maybe_order(query, _), do: query + def fetch_activities_query(recipients, opts \\ %{}) do Activity |> maybe_preload_objects(opts) + |> maybe_preload_bookmarks(opts) + |> maybe_order(opts) |> restrict_recipients(recipients, opts["user"]) |> restrict_tag(opts) |> restrict_tag_reject(opts) @@ -925,134 +974,6 @@ def make_user_from_nickname(nickname) do end end - def should_federate?(inbox, public) do - if public do - true - else - inbox_info = URI.parse(inbox) - !Enum.member?(Pleroma.Config.get([:instance, :quarantined_instances], []), inbox_info.host) - end - end - - defp recipients(actor, activity) do - followers = - if actor.follower_address in activity.recipients do - {:ok, followers} = User.get_followers(actor) - Enum.filter(followers, &(!&1.local)) - else - [] - end - - Pleroma.Web.Salmon.remote_users(actor, activity) ++ followers - end - - defp get_cc_ap_ids(ap_id, recipients) do - host = Map.get(URI.parse(ap_id), :host) - - recipients - |> Enum.filter(fn %User{ap_id: ap_id} -> Map.get(URI.parse(ap_id), :host) == host end) - |> Enum.map(& &1.ap_id) - end - - def publish(actor, %{data: %{"bcc" => bcc}} = activity) when is_list(bcc) and bcc != [] do - public = is_public?(activity) - {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) - - recipients = recipients(actor, activity) - - recipients - |> Enum.filter(&User.ap_enabled?/1) - |> Enum.map(fn %{info: %{source_data: data}} -> data["inbox"] end) - |> Enum.filter(fn inbox -> should_federate?(inbox, public) end) - |> Instances.filter_reachable() - |> Enum.each(fn {inbox, unreachable_since} -> - %User{ap_id: ap_id} = - Enum.find(recipients, fn %{info: %{source_data: data}} -> data["inbox"] == inbox end) - - cc = get_cc_ap_ids(ap_id, recipients) - - json = - data - |> Map.put("cc", cc) - |> Map.put("directMessage", true) - |> Jason.encode!() - - Federator.publish_single_ap(%{ - inbox: inbox, - json: json, - actor: actor, - id: activity.data["id"], - unreachable_since: unreachable_since - }) - end) - end - - def publish(actor, activity) do - public = is_public?(activity) - {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) - - json = Jason.encode!(data) - - recipients(actor, activity) - |> Enum.filter(&User.ap_enabled?/1) - |> Enum.map(fn %{info: %{source_data: data}} -> - (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"] - end) - |> Enum.uniq() - |> Enum.filter(fn inbox -> should_federate?(inbox, public) end) - |> Instances.filter_reachable() - |> Enum.each(fn {inbox, unreachable_since} -> - Federator.publish_single_ap(%{ - inbox: inbox, - json: json, - actor: actor, - id: activity.data["id"], - unreachable_since: unreachable_since - }) - end) - end - - def publish_one(%{inbox: inbox, json: json, actor: actor, id: id} = params) do - Logger.info("Federating #{id} to #{inbox}") - host = URI.parse(inbox).host - - digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64()) - - date = - NaiveDateTime.utc_now() - |> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT") - - signature = - Pleroma.Web.HTTPSignatures.sign(actor, %{ - host: host, - "content-length": byte_size(json), - digest: digest, - date: date - }) - - with {:ok, %{status: code}} when code in 200..299 <- - result = - @httpoison.post( - inbox, - json, - [ - {"Content-Type", "application/activity+json"}, - {"Date", date}, - {"signature", signature}, - {"digest", digest} - ] - ) do - if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since], - do: Instances.set_reachable(inbox) - - result - else - {_post_result, response} -> - unless params[:unreachable_since], do: Instances.set_unreachable(inbox) - {:error, response} - end - end - # filter out broken threads def contain_broken_threads(%Activity{} = activity, %User{} = user) do entire_thread_visible_for_user?(activity, user) diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 0b80566bf..c967ab7a9 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -155,7 +155,7 @@ def outbox(conn, %{"nickname" => nickname} = params) do def inbox(%{assigns: %{valid_signature: true}} = conn, %{"nickname" => nickname} = params) do with %User{} = recipient <- User.get_cached_by_nickname(nickname), - %User{} = actor <- User.get_or_fetch_by_ap_id(params["actor"]), + {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(params["actor"]), true <- Utils.recipient_in_message(recipient, actor, params), params <- Utils.maybe_splice_recipient(recipient.ap_id, params) do Federator.incoming_ap_doc(params) diff --git a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex index 34665a3a6..87fa514c3 100644 --- a/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex @@ -5,6 +5,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.AntiFollowbotPolicy do alias Pleroma.User + @moduledoc "Prevent followbots from following with a bit of heuristic" + @behaviour Pleroma.Web.ActivityPub.MRF # XXX: this should become User.normalize_by_ap_id() or similar, really. diff --git a/lib/pleroma/web/activity_pub/mrf/drop_policy.ex b/lib/pleroma/web/activity_pub/mrf/drop_policy.ex index a93ccf386..b8d38aae6 100644 --- a/lib/pleroma/web/activity_pub/mrf/drop_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/drop_policy.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.DropPolicy do require Logger + @moduledoc "Drop and log everything received" @behaviour Pleroma.Web.ActivityPub.MRF @impl true diff --git a/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex index 895376c9d..15d8514be 100644 --- a/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex +++ b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do alias Pleroma.Object + @moduledoc "Ensure a re: is prepended on replies to a post with a Subject" @behaviour Pleroma.Web.ActivityPub.MRF @reply_prefix Regex.compile!("^re:[[:space:]]*", [:caseless]) diff --git a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex index 6736f3cb9..a699f6a7e 100644 --- a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex @@ -4,6 +4,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do alias Pleroma.User + @moduledoc "Block messages with too much mentions (configurable)" + @behaviour Pleroma.Web.ActivityPub.MRF defp delist_message(message, threshold) when threshold > 0 do diff --git a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex index e8dfba672..d5c341433 100644 --- a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex @@ -3,6 +3,8 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do + @moduledoc "Reject or Word-Replace messages with a keyword or regex" + @behaviour Pleroma.Web.ActivityPub.MRF defp string_matches?(string, _) when not is_binary(string) do false diff --git a/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex b/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex index 081456046..f30fee0d5 100644 --- a/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex @@ -3,6 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy do + @moduledoc "Ensure no content placeholder is present (such as the dot from mastodon)" @behaviour Pleroma.Web.ActivityPub.MRF @impl true diff --git a/lib/pleroma/web/activity_pub/mrf/noop_policy.ex b/lib/pleroma/web/activity_pub/mrf/noop_policy.ex index 40f37bdb1..c47cb3298 100644 --- a/lib/pleroma/web/activity_pub/mrf/noop_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/noop_policy.ex @@ -3,6 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.NoOpPolicy do + @moduledoc "Does nothing (lets the messages go through unmodified)" @behaviour Pleroma.Web.ActivityPub.MRF @impl true diff --git a/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex b/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex index 3d13cdb32..9c87c6963 100644 --- a/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex +++ b/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex @@ -3,6 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkup do + @moduledoc "Scrub configured hypertext markup" alias Pleroma.HTML @behaviour Pleroma.Web.ActivityPub.MRF diff --git a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex index 4197be847..ea3df1b4d 100644 --- a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex +++ b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do alias Pleroma.User + @moduledoc "Rejects non-public (followers-only, direct) activities" @behaviour Pleroma.Web.ActivityPub.MRF @impl true diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex index 798ba9687..2f105700b 100644 --- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do alias Pleroma.User + @moduledoc "Filter activities depending on their origin instance" @behaviour Pleroma.Web.ActivityPub.MRF defp check_accept(%{host: actor_host} = _actor_info, object) do diff --git a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex index b242e44e6..b52be30e7 100644 --- a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex @@ -5,6 +5,19 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do alias Pleroma.User @behaviour Pleroma.Web.ActivityPub.MRF + @moduledoc """ + Apply policies based on user tags + + This policy applies policies on a user activities depending on their tags + on your instance. + + - `mrf_tag:media-force-nsfw`: Mark as sensitive on presence of attachments + - `mrf_tag:media-strip`: Remove attachments + - `mrf_tag:force-unlisted`: Mark as unlisted (removes from the federated timeline) + - `mrf_tag:sandbox`: Remove from public (local and federated) timelines + - `mrf_tag:disable-remote-subscription`: Reject non-local follow requests + - `mrf_tag:disable-any-subscription`: Reject any follow requests + """ defp get_tags(%User{tags: tags}) when is_list(tags), do: tags defp get_tags(_), do: [] diff --git a/lib/pleroma/web/activity_pub/mrf/user_allowlist.ex b/lib/pleroma/web/activity_pub/mrf/user_allowlist.ex index a3b1f8aa0..f5078d818 100644 --- a/lib/pleroma/web/activity_pub/mrf/user_allowlist.ex +++ b/lib/pleroma/web/activity_pub/mrf/user_allowlist.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy do alias Pleroma.Config + @moduledoc "Accept-list of users from specified instances" @behaviour Pleroma.Web.ActivityPub.MRF defp filter_by_list(object, []), do: {:ok, object} diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex new file mode 100644 index 000000000..036ac892e --- /dev/null +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -0,0 +1,201 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.Publisher do + alias Pleroma.Activity + alias Pleroma.Config + alias Pleroma.Instances + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Relay + alias Pleroma.Web.ActivityPub.Transmogrifier + + import Pleroma.Web.ActivityPub.Visibility + + @behaviour Pleroma.Web.Federator.Publisher + + require Logger + + @httpoison Application.get_env(:pleroma, :httpoison) + + @moduledoc """ + ActivityPub outgoing federation module. + """ + + @doc """ + Determine if an activity can be represented by running it through Transmogrifier. + """ + def is_representable?(%Activity{} = activity) do + with {:ok, _data} <- Transmogrifier.prepare_outgoing(activity.data) do + true + else + _e -> + false + end + end + + @doc """ + Publish a single message to a peer. Takes a struct with the following + parameters set: + + * `inbox`: the inbox to publish to + * `json`: the JSON message body representing the ActivityPub message + * `actor`: the actor which is signing the message + * `id`: the ActivityStreams URI of the message + """ + def publish_one(%{inbox: inbox, json: json, actor: %User{} = actor, id: id} = params) do + Logger.info("Federating #{id} to #{inbox}") + host = URI.parse(inbox).host + + digest = "SHA-256=" <> (:crypto.hash(:sha256, json) |> Base.encode64()) + + date = + NaiveDateTime.utc_now() + |> Timex.format!("{WDshort}, {0D} {Mshort} {YYYY} {h24}:{m}:{s} GMT") + + signature = + Pleroma.Web.HTTPSignatures.sign(actor, %{ + host: host, + "content-length": byte_size(json), + digest: digest, + date: date + }) + + with {:ok, %{status: code}} when code in 200..299 <- + result = + @httpoison.post( + inbox, + json, + [ + {"Content-Type", "application/activity+json"}, + {"Date", date}, + {"signature", signature}, + {"digest", digest} + ] + ) do + if !Map.has_key?(params, :unreachable_since) || params[:unreachable_since], + do: Instances.set_reachable(inbox) + + result + else + {_post_result, response} -> + unless params[:unreachable_since], do: Instances.set_unreachable(inbox) + {:error, response} + end + end + + defp should_federate?(inbox, public) do + if public do + true + else + inbox_info = URI.parse(inbox) + !Enum.member?(Pleroma.Config.get([:instance, :quarantined_instances], []), inbox_info.host) + end + end + + defp recipients(actor, activity) do + followers = + if actor.follower_address in activity.recipients do + {:ok, followers} = User.get_followers(actor) + Enum.filter(followers, &(!&1.local)) + else + [] + end + + Pleroma.Web.Salmon.remote_users(actor, activity) ++ followers + end + + defp get_cc_ap_ids(ap_id, recipients) do + host = Map.get(URI.parse(ap_id), :host) + + recipients + |> Enum.filter(fn %User{ap_id: ap_id} -> Map.get(URI.parse(ap_id), :host) == host end) + |> Enum.map(& &1.ap_id) + end + + @doc """ + Publishes an activity with BCC to all relevant peers. + """ + + def publish(actor, %{data: %{"bcc" => bcc}} = activity) when is_list(bcc) and bcc != [] do + public = is_public?(activity) + {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) + + recipients = recipients(actor, activity) + + recipients + |> Enum.filter(&User.ap_enabled?/1) + |> Enum.map(fn %{info: %{source_data: data}} -> data["inbox"] end) + |> Enum.filter(fn inbox -> should_federate?(inbox, public) end) + |> Instances.filter_reachable() + |> Enum.each(fn {inbox, unreachable_since} -> + %User{ap_id: ap_id} = + Enum.find(recipients, fn %{info: %{source_data: data}} -> data["inbox"] == inbox end) + + cc = get_cc_ap_ids(ap_id, recipients) + + json = + data + |> Map.put("cc", cc) + |> Map.put("directMessage", true) + |> Jason.encode!() + + Pleroma.Web.Federator.Publisher.enqueue_one(__MODULE__, %{ + inbox: inbox, + json: json, + actor: actor, + id: activity.data["id"], + unreachable_since: unreachable_since + }) + end) + end + + @doc """ + Publishes an activity to all relevant peers. + """ + def publish(%User{} = actor, %Activity{} = activity) do + public = is_public?(activity) + + if public && Config.get([:instance, :allow_relay]) do + Logger.info(fn -> "Relaying #{activity.data["id"]} out" end) + Relay.publish(activity) + end + + {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) + json = Jason.encode!(data) + + recipients(actor, activity) + |> Enum.filter(fn user -> User.ap_enabled?(user) end) + |> Enum.map(fn %{info: %{source_data: data}} -> + (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"] + end) + |> Enum.uniq() + |> Enum.filter(fn inbox -> should_federate?(inbox, public) end) + |> Instances.filter_reachable() + |> Enum.each(fn {inbox, unreachable_since} -> + Pleroma.Web.Federator.Publisher.enqueue_one( + __MODULE__, + %{ + inbox: inbox, + json: json, + actor: actor, + id: activity.data["id"], + unreachable_since: unreachable_since + } + ) + end) + end + + def gather_webfinger_links(%User{} = user) do + [ + %{"rel" => "self", "type" => "application/activity+json", "href" => user.ap_id}, + %{ + "rel" => "self", + "type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", + "href" => user.ap_id + } + ] + end + + def gather_nodeinfo_protocol_names, do: ["activitypub"] +end diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex index a7a20ca37..93808517b 100644 --- a/lib/pleroma/web/activity_pub/relay.ex +++ b/lib/pleroma/web/activity_pub/relay.ex @@ -15,7 +15,7 @@ def get_actor do def follow(target_instance) do with %User{} = local_user <- get_actor(), - %User{} = target_user <- User.get_or_fetch_by_ap_id(target_instance), + {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance), {:ok, activity} <- ActivityPub.follow(local_user, target_user) do Logger.info("relay: followed instance: #{target_instance}; id=#{activity.data["id"]}") {:ok, activity} @@ -28,7 +28,7 @@ def follow(target_instance) do def unfollow(target_instance) do with %User{} = local_user <- get_actor(), - %User{} = target_user <- User.get_or_fetch_by_ap_id(target_instance), + {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance), {:ok, activity} <- ActivityPub.unfollow(local_user, target_user) do Logger.info("relay: unfollowed instance: #{target_instance}: id=#{activity.data["id"]}") {:ok, activity} diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 3b7193eaa..c4cc77a95 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -126,7 +126,7 @@ def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collec def fix_implicit_addressing(object, _), do: object def fix_addressing(object) do - %User{} = user = User.get_or_fetch_by_ap_id(object["actor"]) + {:ok, %User{} = user} = User.get_or_fetch_by_ap_id(object["actor"]) followers_collection = User.ap_followers(user) object @@ -407,7 +407,7 @@ def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = obj |> fix_addressing with nil <- Activity.get_create_by_object_ap_id(object["id"]), - %User{} = user <- User.get_or_fetch_by_ap_id(data["actor"]) do + {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do object = fix_object(data["object"]) params = %{ @@ -436,7 +436,7 @@ def handle_incoming( %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data ) do with %User{local: true} = followed <- User.get_cached_by_ap_id(followed), - %User{} = follower <- User.get_or_fetch_by_ap_id(follower), + {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower), {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]), {:user_blocked, false} <- @@ -485,7 +485,7 @@ def handle_incoming( %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data ) do with actor <- Containment.get_actor(data), - %User{} = followed <- User.get_or_fetch_by_ap_id(actor), + {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor), {:ok, follow_activity} <- get_follow_activity(follow_object, followed), {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"), %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]), @@ -511,7 +511,7 @@ def handle_incoming( %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data ) do with actor <- Containment.get_actor(data), - %User{} = followed <- User.get_or_fetch_by_ap_id(actor), + {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor), {:ok, follow_activity} <- get_follow_activity(follow_object, followed), {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"), %User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]), @@ -535,7 +535,7 @@ def handle_incoming( %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data ) do with actor <- Containment.get_actor(data), - %User{} = actor <- User.get_or_fetch_by_ap_id(actor), + {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor), {:ok, object} <- get_obj_helper(object_id), {:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do {:ok, activity} @@ -548,7 +548,7 @@ def handle_incoming( %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data ) do with actor <- Containment.get_actor(data), - %User{} = actor <- User.get_or_fetch_by_ap_id(actor), + {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor), {:ok, object} <- get_obj_helper(object_id), public <- Visibility.is_public?(data), {:ok, activity, _object} <- ActivityPub.announce(actor, object, id, false, public) do @@ -603,7 +603,7 @@ def handle_incoming( object_id = Utils.get_ap_id(object_id) with actor <- Containment.get_actor(data), - %User{} = actor <- User.get_or_fetch_by_ap_id(actor), + {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor), {:ok, object} <- get_obj_helper(object_id), :ok <- Containment.contain_origin(actor.ap_id, object.data), {:ok, activity} <- ActivityPub.delete(object, false) do @@ -622,7 +622,7 @@ def handle_incoming( } = data ) do with actor <- Containment.get_actor(data), - %User{} = actor <- User.get_or_fetch_by_ap_id(actor), + {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor), {:ok, object} <- get_obj_helper(object_id), {:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do {:ok, activity} @@ -640,7 +640,7 @@ def handle_incoming( } = _data ) do with %User{local: true} = followed <- User.get_cached_by_ap_id(followed), - %User{} = follower <- User.get_or_fetch_by_ap_id(follower), + {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower), {:ok, activity} <- ActivityPub.unfollow(follower, followed, id, false) do User.unfollow(follower, followed) {:ok, activity} @@ -659,7 +659,7 @@ def handle_incoming( ) do with true <- Pleroma.Config.get([:activitypub, :accept_blocks]), %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked), - %User{} = blocker <- User.get_or_fetch_by_ap_id(blocker), + {:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker), {:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do User.unblock(blocker, blocked) {:ok, activity} @@ -673,7 +673,7 @@ def handle_incoming( ) do with true <- Pleroma.Config.get([:activitypub, :accept_blocks]), %User{local: true} = blocked = User.get_cached_by_ap_id(blocked), - %User{} = blocker = User.get_or_fetch_by_ap_id(blocker), + {:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker), {:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do User.unfollow(blocker, blocked) User.block(blocker, blocked) @@ -692,7 +692,7 @@ def handle_incoming( } = data ) do with actor <- Containment.get_actor(data), - %User{} = actor <- User.get_or_fetch_by_ap_id(actor), + {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor), {:ok, object} <- get_obj_helper(object_id), {:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do {:ok, activity} @@ -859,10 +859,16 @@ def add_mention_tags(object) do |> Map.put("tag", tags ++ mentions) end + def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do + user_info = add_emoji_tags(user_info) + + object + |> Map.put(:info, user_info) + end + # TODO: we should probably send mtime instead of unix epoch time for updated - def add_emoji_tags(object) do + def add_emoji_tags(%{"emoji" => emoji} = object) do tags = object["tag"] || [] - emoji = object["emoji"] || [] out = emoji @@ -880,6 +886,10 @@ def add_emoji_tags(object) do |> Map.put("tag", tags ++ out) end + def add_emoji_tags(object) do + object + end + def set_conversation(object) do Map.put(object, "conversation", object["context"]) end diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 581b9d1ab..236d1b4ac 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -682,7 +682,7 @@ def make_flag_data(params, additional) do """ def fetch_ordered_collection(from, pages_left, acc \\ []) do with {:ok, response} <- Tesla.get(from), - {:ok, collection} <- Poison.decode(response.body) do + {:ok, collection} <- Jason.decode(response.body) do case collection["type"] do "OrderedCollection" -> # If we've encountered the OrderedCollection and not the page, diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 5926a3294..1254fdf6c 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -69,6 +69,11 @@ def render("user.json", %{user: user}) do endpoints = render("endpoints.json", %{user: user}) + user_tags = + user + |> Transmogrifier.add_emoji_tags() + |> Map.get("tag", []) + %{ "id" => user.ap_id, "type" => "Person", @@ -87,7 +92,7 @@ def render("user.json", %{user: user}) do "publicKeyPem" => public_key }, "endpoints" => endpoints, - "tag" => user.info.source_data["tag"] || [] + "tag" => (user.info.source_data["tag"] || []) ++ user_tags } |> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user)) |> Map.merge(maybe_make_image(&User.banner_url/2, "image", user)) diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 711f233a6..e00b33aba 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -59,7 +59,7 @@ def user_create( bio: "." } - changeset = User.register_changeset(%User{}, user_data, confirmed: true) + changeset = User.register_changeset(%User{}, user_data, need_confirmation: false) {:ok, user} = User.register(changeset) conn @@ -101,7 +101,10 @@ def list_users(conn, params) do search_params = %{ query: params["query"], page: page, - page_size: page_size + page_size: page_size, + tags: params["tags"], + name: params["name"], + email: params["email"] } with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)), @@ -116,11 +119,11 @@ def list_users(conn, params) do ) end - @filters ~w(local external active deactivated) - - defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{} + @filters ~w(local external active deactivated is_admin is_moderator) @spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{} + defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{} + defp maybe_parse_filters(filters) do filters |> String.split(",") diff --git a/lib/pleroma/web/admin_api/search.ex b/lib/pleroma/web/admin_api/search.ex index 9a8e41c2a..ed919833e 100644 --- a/lib/pleroma/web/admin_api/search.ex +++ b/lib/pleroma/web/admin_api/search.ex @@ -10,45 +10,23 @@ defmodule Pleroma.Web.AdminAPI.Search do @page_size 50 - def user(%{query: term} = params) when is_nil(term) or term == "" do - query = maybe_filtered_query(params) + defmacro not_empty_string(string) do + quote do + is_binary(unquote(string)) and unquote(string) != "" + end + end + + @spec user(map()) :: {:ok, [User.t()], pos_integer()} + def user(params \\ %{}) do + query = User.Query.build(params) |> order_by([u], u.nickname) paginated_query = - maybe_filtered_query(params) - |> paginate(params[:page] || 1, params[:page_size] || @page_size) + User.Query.paginate(query, params[:page] || 1, params[:page_size] || @page_size) - count = query |> Repo.aggregate(:count, :id) + count = Repo.aggregate(query, :count, :id) results = Repo.all(paginated_query) {:ok, results, count} end - - def user(%{query: term} = params) when is_binary(term) do - search_query = from(u in maybe_filtered_query(params), where: ilike(u.nickname, ^"%#{term}%")) - - count = search_query |> Repo.aggregate(:count, :id) - - results = - search_query - |> paginate(params[:page] || 1, params[:page_size] || @page_size) - |> Repo.all() - - {:ok, results, count} - end - - defp maybe_filtered_query(params) do - from(u in User, order_by: u.nickname) - |> User.maybe_local_user_query(params[:local]) - |> User.maybe_external_user_query(params[:external]) - |> User.maybe_active_user_query(params[:active]) - |> User.maybe_deactivated_user_query(params[:deactivated]) - end - - defp paginate(query, page, page_size) do - from(u in query, - limit: ^page_size, - offset: ^((page - 1) * page_size) - ) - end end diff --git a/lib/pleroma/web/auth/authenticator.ex b/lib/pleroma/web/auth/authenticator.ex index b02f595dc..d4e0ffa80 100644 --- a/lib/pleroma/web/auth/authenticator.ex +++ b/lib/pleroma/web/auth/authenticator.ex @@ -42,4 +42,30 @@ def oauth_consumer_template do implementation().oauth_consumer_template() || Pleroma.Config.get([:auth, :oauth_consumer_template], "consumer.html") end + + @doc "Gets user by nickname or email for auth." + @spec fetch_user(String.t()) :: User.t() | nil + def fetch_user(name) do + User.get_by_nickname_or_email(name) + end + + # Gets name and password from conn + # + @spec fetch_credentials(Plug.Conn.t() | map()) :: + {:ok, {name :: any, password :: any}} | {:error, :invalid_credentials} + def fetch_credentials(%Plug.Conn{params: params} = _), + do: fetch_credentials(params) + + def fetch_credentials(params) do + case params do + %{"authorization" => %{"name" => name, "password" => password}} -> + {:ok, {name, password}} + + %{"grant_type" => "password", "username" => name, "password" => password} -> + {:ok, {name, password}} + + _ -> + {:error, :invalid_credentials} + end + end end diff --git a/lib/pleroma/web/auth/ldap_authenticator.ex b/lib/pleroma/web/auth/ldap_authenticator.ex index 363c99597..177c05636 100644 --- a/lib/pleroma/web/auth/ldap_authenticator.ex +++ b/lib/pleroma/web/auth/ldap_authenticator.ex @@ -7,6 +7,9 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do require Logger + import Pleroma.Web.Auth.Authenticator, + only: [fetch_credentials: 1, fetch_user: 1] + @behaviour Pleroma.Web.Auth.Authenticator @base Pleroma.Web.Auth.PleromaAuthenticator @@ -20,30 +23,20 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do defdelegate oauth_consumer_template, to: @base def get_user(%Plug.Conn{} = conn) do - if Pleroma.Config.get([:ldap, :enabled]) do - {name, password} = - case conn.params do - %{"authorization" => %{"name" => name, "password" => password}} -> - {name, password} - - %{"grant_type" => "password", "username" => name, "password" => password} -> - {name, password} - end - - case ldap_user(name, password) do - %User{} = user -> - {:ok, user} - - {:error, {:ldap_connection_error, _}} -> - # When LDAP is unavailable, try default authenticator - @base.get_user(conn) - - error -> - error - end + with {:ldap, true} <- {:ldap, Pleroma.Config.get([:ldap, :enabled])}, + {:ok, {name, password}} <- fetch_credentials(conn), + %User{} = user <- ldap_user(name, password) do + {:ok, user} else - # Fall back to default authenticator - @base.get_user(conn) + {:error, {:ldap_connection_error, _}} -> + # When LDAP is unavailable, try default authenticator + @base.get_user(conn) + + {:ldap, _} -> + @base.get_user(conn) + + error -> + error end end @@ -94,7 +87,7 @@ defp bind_user(connection, ldap, name, password) do case :eldap.simple_bind(connection, "#{uid}=#{name},#{base}", password) do :ok -> - case User.get_by_nickname_or_email(name) do + case fetch_user(name) do %User{} = user -> user diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex index d647f1e05..c4a6fce08 100644 --- a/lib/pleroma/web/auth/pleroma_authenticator.ex +++ b/lib/pleroma/web/auth/pleroma_authenticator.ex @@ -8,19 +8,14 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do alias Pleroma.Repo alias Pleroma.User + import Pleroma.Web.Auth.Authenticator, + only: [fetch_credentials: 1, fetch_user: 1] + @behaviour Pleroma.Web.Auth.Authenticator def get_user(%Plug.Conn{} = conn) do - {name, password} = - case conn.params do - %{"authorization" => %{"name" => name, "password" => password}} -> - {name, password} - - %{"grant_type" => "password", "username" => name, "password" => password} -> - {name, password} - end - - with {_, %User{} = user} <- {:user, User.get_by_nickname_or_email(name)}, + with {:ok, {name, password}} <- fetch_credentials(conn), + {_, %User{} = user} <- {:user, fetch_user(name)}, {_, true} <- {:checkpw, Pbkdf2.checkpw(password, user.password_hash)} do {:ok, user} else @@ -79,7 +74,7 @@ def create_from_registration( password_confirmation: random_password }, external: true, - confirmed: true + need_confirmation: false ) |> Repo.insert(), {:ok, _} <- diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index d47d5788c..ed2c0017f 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.CommonAPI do alias Pleroma.Activity + alias Pleroma.Bookmark alias Pleroma.Formatter alias Pleroma.Object alias Pleroma.ThreadMute @@ -155,8 +156,8 @@ def post(user, %{"status" => status} = data) do {to, cc} <- to_for_user_and_mentions(user, mentions, in_reply_to, visibility), bcc <- bcc_for_list(user, visibility), context <- make_context(in_reply_to), - cw <- data["spoiler_text"], - full_payload <- String.trim(status <> (data["spoiler_text"] || "")), + cw <- data["spoiler_text"] || "", + full_payload <- String.trim(status <> cw), length when length in 1..limit <- String.length(full_payload), object <- make_note_data( @@ -174,10 +175,7 @@ def post(user, %{"status" => status} = data) do Map.put( object, "emoji", - (Formatter.get_emoji(status) ++ Formatter.get_emoji(data["spoiler_text"])) - |> Enum.reduce(%{}, fn {name, file, _}, acc -> - Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url()}#{file}") - end) + Formatter.get_emoji_map(full_payload) ) do ActivityPub.create( %{ @@ -284,6 +282,15 @@ def thread_muted?(user, activity) do end end + def bookmarked?(user, activity) do + with %Bookmark{} <- Bookmark.get(user.id, activity.id) do + true + else + _ -> + false + end + end + def report(user, data) do with {:account_id, %{"account_id" => account_id}} <- {:account_id, data}, {:account, %User{} = account} <- {:account, User.get_cached_by_id(account_id)}, diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 32c3b4b98..f082b77d8 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -190,6 +190,18 @@ def format_input(text, "text/plain", options) do end).() end + @doc """ + Formatting text as BBCode. + """ + def format_input(text, "text/bbcode", options) do + text + |> String.replace(~r/\r/, "") + |> Formatter.html_escape("text/plain") + |> BBCode.to_html() + |> (fn {:ok, html} -> html end).() + |> Formatter.linkify(options) + end + @doc """ Formatting text to html. """ diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index 181483664..55706eeb8 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -10,12 +10,6 @@ defmodule Pleroma.Web.ControllerHelper do def truthy_param?(blank_value) when blank_value in [nil, ""], do: nil def truthy_param?(value), do: value not in @falsy_param_values - def oauth_scopes(params, default) do - # Note: `scopes` is used by Mastodon — supporting it but sticking to - # OAuth's standard `scope` wherever we control it - Pleroma.Web.OAuth.parse_scopes(params["scope"] || params["scopes"], default) - end - def json_response(conn, status, json) do conn |> put_status(status) diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 7f939991d..9ef30e885 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -29,6 +29,13 @@ defmodule Pleroma.Web.Endpoint do # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength ) + plug(Plug.Static.IndexHtml, at: "/pleroma/admin/") + + plug(Plug.Static, + at: "/pleroma/admin/", + from: {:pleroma, "priv/static/adminfe/"} + ) + # Code reloading can be explicitly enabled under the # :code_reloader configuration of your endpoint. if code_reloading? do diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex index 29e178ba9..169fdf4dc 100644 --- a/lib/pleroma/web/federator/federator.ex +++ b/lib/pleroma/web/federator/federator.ex @@ -7,13 +7,10 @@ defmodule Pleroma.Web.Federator do alias Pleroma.Object.Containment alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Utils - alias Pleroma.Web.ActivityPub.Visibility + alias Pleroma.Web.Federator.Publisher alias Pleroma.Web.Federator.RetryQueue - alias Pleroma.Web.OStatus - alias Pleroma.Web.Salmon alias Pleroma.Web.WebFinger alias Pleroma.Web.Websub @@ -42,14 +39,6 @@ def publish(activity, priority \\ 1) do PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish, activity], priority) end - def publish_single_ap(params) do - PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_single_ap, params]) - end - - def publish_single_websub(websub) do - PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_single_websub, websub]) - end - def verify_websub(websub) do PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:verify_websub, websub]) end @@ -62,10 +51,6 @@ def refresh_subscriptions do PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:refresh_subscriptions]) end - def publish_single_salmon(params) do - PleromaJobQueue.enqueue(:federator_outgoing, __MODULE__, [:publish_single_salmon, params]) - end - # Job Worker Callbacks def perform(:refresh_subscriptions) do @@ -95,23 +80,7 @@ def perform(:publish, activity) do with actor when not is_nil(actor) <- User.get_cached_by_ap_id(activity.data["actor"]) do {:ok, actor} = WebFinger.ensure_keys_present(actor) - if Visibility.is_public?(activity) do - if OStatus.is_representable?(activity) do - Logger.info(fn -> "Sending #{activity.data["id"]} out via WebSub" end) - Websub.publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity) - - Logger.info(fn -> "Sending #{activity.data["id"]} out via Salmon" end) - Pleroma.Web.Salmon.publish(actor, activity) - end - - if Keyword.get(Application.get_env(:pleroma, :instance), :allow_relay) do - Logger.info(fn -> "Relaying #{activity.data["id"]} out" end) - Relay.publish(activity) - end - end - - Logger.info(fn -> "Sending #{activity.data["id"]} out via AP" end) - Pleroma.Web.ActivityPub.ActivityPub.publish(actor, activity) + Publisher.publish(actor, activity) end end @@ -148,25 +117,11 @@ def perform(:incoming_ap_doc, params) do _e -> # Just drop those for now Logger.info("Unhandled activity") - Logger.info(Poison.encode!(params, pretty: 2)) + Logger.info(Jason.encode!(params, pretty: true)) :error end end - def perform(:publish_single_salmon, params) do - Salmon.send_to_user(params) - end - - def perform(:publish_single_ap, params) do - case ActivityPub.publish_one(params) do - {:ok, _} -> - :ok - - {:error, _} -> - RetryQueue.enqueue(params, ActivityPub) - end - end - def perform( :publish_single_websub, %{xml: _xml, topic: _topic, callback: _callback, secret: _secret} = params diff --git a/lib/pleroma/web/federator/publisher.ex b/lib/pleroma/web/federator/publisher.ex new file mode 100644 index 000000000..916bcdcba --- /dev/null +++ b/lib/pleroma/web/federator/publisher.ex @@ -0,0 +1,95 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Federator.Publisher do + alias Pleroma.Activity + alias Pleroma.Config + alias Pleroma.User + alias Pleroma.Web.Federator.RetryQueue + + require Logger + + @moduledoc """ + Defines the contract used by federation implementations to publish messages to + their peers. + """ + + @doc """ + Determine whether an activity can be relayed using the federation module. + """ + @callback is_representable?(Pleroma.Activity.t()) :: boolean() + + @doc """ + Relays an activity to a specified peer, determined by the parameters. The + parameters used are controlled by the federation module. + """ + @callback publish_one(Map.t()) :: {:ok, Map.t()} | {:error, any()} + + @doc """ + Enqueue publishing a single activity. + """ + @spec enqueue_one(module(), Map.t()) :: :ok + def enqueue_one(module, %{} = params), + do: PleromaJobQueue.enqueue(:federation_outgoing, __MODULE__, [:publish_one, module, params]) + + @spec perform(atom(), module(), any()) :: {:ok, any()} | {:error, any()} + def perform(:publish_one, module, params) do + case apply(module, :publish_one, [params]) do + {:ok, _} -> + :ok + + {:error, _e} -> + RetryQueue.enqueue(params, module) + end + end + + def perform(type, _, _) do + Logger.debug("Unknown task: #{type}") + {:error, "Don't know what to do with this"} + end + + @doc """ + Relays an activity to all specified peers. + """ + @callback publish(Pleroma.User.t(), Pleroma.Activity.t()) :: :ok | {:error, any()} + + @spec publish(Pleroma.User.t(), Pleroma.Activity.t()) :: :ok + def publish(%User{} = user, %Activity{} = activity) do + Config.get([:instance, :federation_publisher_modules]) + |> Enum.each(fn module -> + if module.is_representable?(activity) do + Logger.info("Publishing #{activity.data["id"]} using #{inspect(module)}") + module.publish(user, activity) + end + end) + + :ok + end + + @doc """ + Gathers links used by an outgoing federation module for WebFinger output. + """ + @callback gather_webfinger_links(Pleroma.User.t()) :: list() + + @spec gather_webfinger_links(Pleroma.User.t()) :: list() + def gather_webfinger_links(%User{} = user) do + Config.get([:instance, :federation_publisher_modules]) + |> Enum.reduce([], fn module, links -> + links ++ module.gather_webfinger_links(user) + end) + end + + @doc """ + Gathers nodeinfo protocol names supported by the federation module. + """ + @callback gather_nodeinfo_protocol_names() :: list() + + @spec gather_nodeinfo_protocol_names() :: list() + def gather_nodeinfo_protocol_names do + Config.get([:instance, :federation_publisher_modules]) + |> Enum.reduce([], fn module, links -> + links ++ module.gather_nodeinfo_protocol_names() + end) + end +end diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 0ba8d9eea..87e597074 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -6,8 +6,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do use Pleroma.Web, :controller alias Ecto.Changeset alias Pleroma.Activity + alias Pleroma.Bookmark alias Pleroma.Config + alias Pleroma.Conversation.Participation alias Pleroma.Filter + alias Pleroma.Formatter alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Object.Fetcher @@ -22,6 +25,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.AppView + alias Pleroma.Web.MastodonAPI.ConversationView alias Pleroma.Web.MastodonAPI.FilterView alias Pleroma.Web.MastodonAPI.ListView alias Pleroma.Web.MastodonAPI.MastodonAPI @@ -33,20 +37,31 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do alias Pleroma.Web.MediaProxy alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.Authorization + alias Pleroma.Web.OAuth.Scopes alias Pleroma.Web.OAuth.Token + alias Pleroma.Web.TwitterAPI.TwitterAPI - import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2] + alias Pleroma.Web.ControllerHelper import Ecto.Query require Logger + plug( + Pleroma.Plugs.RateLimitPlug, + %{ + max_requests: Config.get([:app_account_creation, :max_requests]), + interval: Config.get([:app_account_creation, :interval]) + } + when action in [:account_register] + ) + @httpoison Application.get_env(:pleroma, :httpoison) @local_mastodon_name "Mastodon-Local" action_fallback(:errors) def create_app(conn, params) do - scopes = oauth_scopes(params, ["read"]) + scopes = Scopes.fetch_scopes(params, ["read"]) app_attrs = params @@ -85,7 +100,7 @@ def update_credentials(%{assigns: %{user: user}} = conn, params) do user_params = %{} |> add_if_present(params, "display_name", :name) - |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end) + |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end) |> add_if_present(params, "avatar", :avatar, fn value -> with %Plug.Upload{} <- value, {:ok, object} <- ActivityPub.upload(value, type: :avatar) do @@ -95,9 +110,20 @@ def update_credentials(%{assigns: %{user: user}} = conn, params) do end end) + emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "") + + user_info_emojis = + ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text)) + |> Enum.dedup() + info_params = - %{} - |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end) + [:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role] + |> Enum.reduce(%{}, fn key, acc -> + add_if_present(acc, params, to_string(key), key, fn value -> + {:ok, ControllerHelper.truthy_param?(value)} + end) + end) + |> add_if_present(params, "default_scope", :default_scope) |> add_if_present(params, "header", :banner, fn value -> with %Plug.Upload{} <- value, {:ok, object} <- ActivityPub.upload(value, type: :banner) do @@ -106,8 +132,9 @@ def update_credentials(%{assigns: %{user: user}} = conn, params) do _ -> :error end end) + |> Map.put(:emoji, user_info_emojis) - info_cng = User.Info.mastodon_profile_update(user.info, info_params) + info_cng = User.Info.profile_update(user.info, info_params) with changeset <- User.update_changeset(user, user_params), changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng), @@ -151,7 +178,7 @@ def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do end end - @mastodon_api_level "2.5.0" + @mastodon_api_level "2.7.2" def masto_instance(conn, _params) do instance = Config.get(:instance) @@ -545,10 +572,9 @@ def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do with %Activity{} = activity <- Activity.get_by_id_with_object(id), - %Object{} = object <- Object.normalize(activity), %User{} = user <- User.get_cached_by_nickname(user.nickname), true <- Visibility.visible_for_user?(activity, user), - {:ok, user} <- User.bookmark(user, object.data["id"]) do + {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do conn |> put_view(StatusView) |> try_render("status.json", %{activity: activity, for: user, as: :activity}) @@ -557,10 +583,9 @@ def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do with %Activity{} = activity <- Activity.get_by_id_with_object(id), - %Object{} = object <- Object.normalize(activity), %User{} = user <- User.get_cached_by_nickname(user.nickname), true <- Visibility.visible_for_user?(activity, user), - {:ok, user} <- User.unbookmark(user, object.data["id"]) do + {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do conn |> put_view(StatusView) |> try_render("status.json", %{activity: activity, for: user, as: :activity}) @@ -683,7 +708,7 @@ def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do end end - def favourited_by(conn, %{"id" => id}) do + def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id), %Object{data: %{"likes" => likes}} <- Object.normalize(object) do q = from(u in User, where: u.ap_id in ^likes) @@ -691,13 +716,13 @@ def favourited_by(conn, %{"id" => id}) do conn |> put_view(AccountView) - |> render(AccountView, "accounts.json", %{users: users, as: :user}) + |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user}) else _ -> json(conn, []) end end - def reblogged_by(conn, %{"id" => id}) do + def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id), %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do q = from(u in User, where: u.ap_id in ^announces) @@ -705,7 +730,7 @@ def reblogged_by(conn, %{"id" => id}) do conn |> put_view(AccountView) - |> render("accounts.json", %{users: users, as: :user}) + |> render("accounts.json", %{for: user, users: users, as: :user}) else _ -> json(conn, []) end @@ -762,7 +787,7 @@ def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do conn |> add_link_headers(:followers, followers, user) |> put_view(AccountView) - |> render("accounts.json", %{users: followers, as: :user}) + |> render("accounts.json", %{for: for_user, users: followers, as: :user}) end end @@ -779,7 +804,7 @@ def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do conn |> add_link_headers(:following, followers, user) |> put_view(AccountView) - |> render("accounts.json", %{users: followers, as: :user}) + |> render("accounts.json", %{for: for_user, users: followers, as: :user}) end end @@ -787,7 +812,7 @@ def follow_requests(%{assigns: %{user: followed}} = conn, _params) do with {:ok, follow_requests} <- User.get_follow_requests(followed) do conn |> put_view(AccountView) - |> render("accounts.json", %{users: follow_requests, as: :user}) + |> render("accounts.json", %{for: followed, users: follow_requests, as: :user}) end end @@ -1124,15 +1149,19 @@ def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params end end - def bookmarks(%{assigns: %{user: user}} = conn, _) do + def bookmarks(%{assigns: %{user: user}} = conn, params) do user = User.get_cached_by_id(user.id) + bookmarks = + Bookmark.for_user_query(user.id) + |> Pagination.fetch_paginated(params) + activities = - user.bookmarks - |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end) - |> Enum.reverse() + bookmarks + |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end) conn + |> add_link_headers(:bookmarks, bookmarks) |> put_view(StatusView) |> render("index.json", %{activities: activities, for: user, as: :activity}) end @@ -1207,7 +1236,7 @@ def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do {:ok, users} = Pleroma.List.get_following(list) do conn |> put_view(AccountView) - |> render("accounts.json", %{users: users, as: :user}) + |> render("accounts.json", %{for: user, users: users, as: :user}) end end @@ -1265,8 +1294,7 @@ def index(%{assigns: %{user: user}} = conn, _params) do initial_state = %{ meta: %{ - streaming_api_base_url: - String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"), + streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(), access_token: token, locale: "en", domain: Pleroma.Web.Endpoint.host(), @@ -1518,7 +1546,7 @@ def create_filter( user_id: user.id, phrase: phrase, context: context, - hide: Map.get(params, "irreversible", nil), + hide: Map.get(params, "irreversible", false), whole_word: Map.get(params, "boolean", true) # expires_at } @@ -1623,7 +1651,7 @@ def suggestions(%{assigns: %{user: user}} = conn, _) do x, "id", case User.get_or_fetch(x["acct"]) do - %{id: id} -> id + {:ok, %User{id: id}} -> id _ -> 0 end ) @@ -1675,6 +1703,78 @@ def reports(%{assigns: %{user: user}} = conn, params) do end end + def account_register( + %{assigns: %{app: app}} = conn, + %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params + ) do + params = + params + |> Map.take([ + "email", + "captcha_solution", + "captcha_token", + "captcha_answer_data", + "token", + "password" + ]) + |> Map.put("nickname", nickname) + |> Map.put("fullname", params["fullname"] || nickname) + |> Map.put("bio", params["bio"] || "") + |> Map.put("confirm", params["password"]) + + with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true), + {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do + json(conn, %{ + token_type: "Bearer", + access_token: token.token, + scope: app.scopes, + created_at: Token.Utils.format_created_at(token) + }) + else + {:error, errors} -> + conn + |> put_status(400) + |> json(Jason.encode!(errors)) + end + end + + def account_register(%{assigns: %{app: _app}} = conn, _params) do + conn + |> put_status(400) + |> json(%{error: "Missing parameters"}) + end + + def account_register(conn, _) do + conn + |> put_status(403) + |> json(%{error: "Invalid credentials"}) + end + + def conversations(%{assigns: %{user: user}} = conn, params) do + participations = Participation.for_user_with_last_activity_id(user, params) + + conversations = + Enum.map(participations, fn participation -> + ConversationView.render("participation.json", %{participation: participation, user: user}) + end) + + conn + |> add_link_headers(:conversations, participations) + |> json(conversations) + end + + def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do + with %Participation{} = participation <- + Repo.get_by(Participation, id: participation_id, user_id: user.id), + {:ok, participation} <- Participation.mark_as_read(participation) do + participation_view = + ConversationView.render("participation.json", %{participation: participation, user: user}) + + conn + |> json(participation_view) + end + end + def try_render(conn, target, params) when is_binary(target) do res = render(conn, target, params) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index d87fdb15d..779b9a382 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -113,21 +113,23 @@ defp do_render("account.json", %{user: user} = opts) do bot: bot, source: %{ note: "", - privacy: user_info.default_scope, - sensitive: false + sensitive: false, + pleroma: %{} }, # Pleroma extension - pleroma: - %{ - confirmation_pending: user_info.confirmation_pending, - tags: user.tags, - is_moderator: user.info.is_moderator, - is_admin: user.info.is_admin, - relationship: relationship - } - |> with_notification_settings(user, opts[:for]) + pleroma: %{ + confirmation_pending: user_info.confirmation_pending, + tags: user.tags, + hide_followers: user.info.hide_followers, + hide_follows: user.info.hide_follows, + hide_favorites: user.info.hide_favorites, + relationship: relationship + } } + |> maybe_put_role(user, opts[:for]) + |> maybe_put_settings(user, opts[:for], user_info) + |> maybe_put_notification_settings(user, opts[:for]) end defp username_from_nickname(string) when is_binary(string) do @@ -136,9 +138,37 @@ defp username_from_nickname(string) when is_binary(string) do defp username_from_nickname(_), do: nil - defp with_notification_settings(data, %User{id: user_id} = user, %User{id: user_id}) do - Map.put(data, :notification_settings, user.info.notification_settings) + defp maybe_put_settings( + data, + %User{id: user_id} = user, + %User{id: user_id}, + user_info + ) do + data + |> Kernel.put_in([:source, :privacy], user_info.default_scope) + |> Kernel.put_in([:source, :pleroma, :show_role], user.info.show_role) + |> Kernel.put_in([:source, :pleroma, :no_rich_text], user.info.no_rich_text) end - defp with_notification_settings(data, _, _), do: data + defp maybe_put_settings(data, _, _, _), do: data + + defp maybe_put_role(data, %User{info: %{show_role: true}} = user, _) do + data + |> Kernel.put_in([:pleroma, :is_admin], user.info.is_admin) + |> Kernel.put_in([:pleroma, :is_moderator], user.info.is_moderator) + end + + defp maybe_put_role(data, %User{id: user_id} = user, %User{id: user_id}) do + data + |> Kernel.put_in([:pleroma, :is_admin], user.info.is_admin) + |> Kernel.put_in([:pleroma, :is_moderator], user.info.is_moderator) + end + + defp maybe_put_role(data, _, _), do: data + + defp maybe_put_notification_settings(data, %User{id: user_id} = user, %User{id: user_id}) do + Kernel.put_in(data, [:pleroma, :notification_settings], user.info.notification_settings) + end + + defp maybe_put_notification_settings(data, _, _), do: data end diff --git a/lib/pleroma/web/mastodon_api/views/conversation_view.ex b/lib/pleroma/web/mastodon_api/views/conversation_view.ex new file mode 100644 index 000000000..8e8f7cf31 --- /dev/null +++ b/lib/pleroma/web/mastodon_api/views/conversation_view.ex @@ -0,0 +1,38 @@ +defmodule Pleroma.Web.MastodonAPI.ConversationView do + use Pleroma.Web, :view + + alias Pleroma.Activity + alias Pleroma.Repo + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.MastodonAPI.AccountView + alias Pleroma.Web.MastodonAPI.StatusView + + def render("participation.json", %{participation: participation, user: user}) do + participation = Repo.preload(participation, conversation: :users) + + last_activity_id = + with nil <- participation.last_activity_id do + ActivityPub.fetch_latest_activity_id_for_context(participation.conversation.ap_id, %{ + "user" => user, + "blocking_user" => user + }) + end + + activity = Activity.get_by_id_with_object(last_activity_id) + + last_status = StatusView.render("status.json", %{activity: activity, for: user}) + + accounts = + AccountView.render("accounts.json", %{ + users: participation.conversation.users, + as: :user + }) + + %{ + id: participation.id |> to_string(), + accounts: accounts, + unread: !participation.read, + last_status: last_status + } + end +end diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 7dd80d708..bd2372944 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -75,17 +75,22 @@ def render("index.json", opts) do def render( "status.json", - %{activity: %{data: %{"type" => "Announce", "object" => object}} = activity} = opts + %{activity: %{data: %{"type" => "Announce", "object" => _object}} = activity} = opts ) do user = get_user(activity.data["actor"]) created_at = Utils.to_masto_date(activity.data["published"]) + activity_object = Object.normalize(activity) + + reblogged_activity = + Activity.create_by_object_ap_id(activity_object.data["id"]) + |> Activity.with_preloaded_bookmark(opts[:for]) + |> Repo.one() - reblogged_activity = Activity.get_create_by_object_ap_id(object) reblogged = render("status.json", Map.put(opts, :activity, reblogged_activity)) - activity_object = Object.normalize(activity) favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || []) - bookmarked = opts[:for] && activity_object.data["id"] in opts[:for].bookmarks + + bookmarked = Activity.get_bookmark(reblogged_activity, opts[:for]) != nil mentions = activity.recipients @@ -95,8 +100,8 @@ def render( %{ id: to_string(activity.id), - uri: object, - url: object, + uri: activity_object.data["id"], + url: activity_object.data["id"], account: AccountView.render("account.json", %{user: user}), in_reply_to_id: nil, in_reply_to_account_id: nil, @@ -148,7 +153,7 @@ def render("status.json", %{activity: %{data: %{"object" => _object}} = activity favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || []) - bookmarked = opts[:for] && object.data["id"] in opts[:for].bookmarks + bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil attachment_data = object.data["attachment"] || [] attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment) diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex index 3bd2affe9..5762e767b 100644 --- a/lib/pleroma/web/media_proxy/media_proxy.ex +++ b/lib/pleroma/web/media_proxy/media_proxy.ex @@ -13,32 +13,44 @@ def url("/" <> _ = url), do: url def url(url) do config = Application.get_env(:pleroma, :media_proxy, []) + domain = URI.parse(url).host - if !Keyword.get(config, :enabled, false) or String.starts_with?(url, Pleroma.Web.base_url()) do - url - else - secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base] - - # Must preserve `%2F` for compatibility with S3 - # https://git.pleroma.social/pleroma/pleroma/issues/580 - replacement = get_replacement(url, ":2F:") - - # The URL is url-decoded and encoded again to ensure it is correctly encoded and not twice. - base64 = + cond do + !Keyword.get(config, :enabled, false) or String.starts_with?(url, Pleroma.Web.base_url()) -> url - |> String.replace("%2F", replacement) - |> URI.decode() - |> URI.encode() - |> String.replace(replacement, "%2F") - |> Base.url_encode64(@base64_opts) - sig = :crypto.hmac(:sha, secret, base64) - sig64 = sig |> Base.url_encode64(@base64_opts) + Enum.any?(Pleroma.Config.get([:media_proxy, :whitelist]), fn pattern -> + String.equivalent?(domain, pattern) + end) -> + url - build_url(sig64, base64, filename(url)) + true -> + encode_url(url) end end + def encode_url(url) do + secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base] + + # Must preserve `%2F` for compatibility with S3 + # https://git.pleroma.social/pleroma/pleroma/issues/580 + replacement = get_replacement(url, ":2F:") + + # The URL is url-decoded and encoded again to ensure it is correctly encoded and not twice. + base64 = + url + |> String.replace("%2F", replacement) + |> URI.decode() + |> URI.encode() + |> String.replace(replacement, "%2F") + |> Base.url_encode64(@base64_opts) + + sig = :crypto.hmac(:sha, secret, base64) + sig64 = sig |> Base.url_encode64(@base64_opts) + + build_url(sig64, base64, filename(url)) + end + def decode_url(sig, url) do secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base] sig = Base.url_decode64!(sig, @base64_opts) diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex index 216a962bd..3bf2a0fbc 100644 --- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex +++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do alias Pleroma.User alias Pleroma.Web alias Pleroma.Web.ActivityPub.MRF + alias Pleroma.Web.Federator.Publisher plug(Pleroma.Web.FederatingPlug) @@ -137,7 +138,7 @@ def raw_nodeinfo do name: Pleroma.Application.name() |> String.downcase(), version: Pleroma.Application.version() }, - protocols: ["ostatus", "activitypub"], + protocols: Publisher.gather_nodeinfo_protocol_names(), services: %{ inbound: [], outbound: [] diff --git a/lib/pleroma/web/oauth.ex b/lib/pleroma/web/oauth.ex index d2835a0ba..280cf28c0 100644 --- a/lib/pleroma/web/oauth.ex +++ b/lib/pleroma/web/oauth.ex @@ -3,18 +3,4 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.OAuth do - def parse_scopes(scopes, _default) when is_list(scopes) do - Enum.filter(scopes, &(&1 not in [nil, ""])) - end - - def parse_scopes(scopes, default) when is_binary(scopes) do - scopes - |> String.trim() - |> String.split(~r/[\s,]+/) - |> parse_scopes(default) - end - - def parse_scopes(_, default) do - default - end end diff --git a/lib/pleroma/web/oauth/app.ex b/lib/pleroma/web/oauth/app.ex index 3476da484..ddcdb1871 100644 --- a/lib/pleroma/web/oauth/app.ex +++ b/lib/pleroma/web/oauth/app.ex @@ -6,6 +6,8 @@ defmodule Pleroma.Web.OAuth.App do use Ecto.Schema import Ecto.Changeset + @type t :: %__MODULE__{} + schema "apps" do field(:client_name, :string) field(:redirect_uris, :string) diff --git a/lib/pleroma/web/oauth/authorization.ex b/lib/pleroma/web/oauth/authorization.ex index 3461f9983..b47688de1 100644 --- a/lib/pleroma/web/oauth/authorization.ex +++ b/lib/pleroma/web/oauth/authorization.ex @@ -13,6 +13,8 @@ defmodule Pleroma.Web.OAuth.Authorization do import Ecto.Changeset import Ecto.Query + @type t :: %__MODULE__{} + schema "oauth_authorizations" do field(:token, :string) field(:scopes, {:array, :string}, default: []) @@ -24,28 +26,45 @@ defmodule Pleroma.Web.OAuth.Authorization do timestamps() end + @spec create_authorization(App.t(), User.t() | %{}, [String.t()] | nil) :: + {:ok, Authorization.t()} | {:error, Changeset.t()} def create_authorization(%App{} = app, %User{} = user, scopes \\ nil) do - scopes = scopes || app.scopes - token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) - - authorization = %Authorization{ - token: token, - used: false, + %{ + scopes: scopes || app.scopes, user_id: user.id, - app_id: app.id, - scopes: scopes, - valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10) + app_id: app.id } - - Repo.insert(authorization) + |> create_changeset() + |> Repo.insert() end + @spec create_changeset(map()) :: Changeset.t() + def create_changeset(attrs \\ %{}) do + %Authorization{} + |> cast(attrs, [:user_id, :app_id, :scopes, :valid_until]) + |> validate_required([:app_id, :scopes]) + |> add_token() + |> add_lifetime() + end + + defp add_token(changeset) do + token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) + put_change(changeset, :token, token) + end + + defp add_lifetime(changeset) do + put_change(changeset, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10)) + end + + @spec use_changeset(Authtorizatiton.t(), map()) :: Changeset.t() def use_changeset(%Authorization{} = auth, params) do auth |> cast(params, [:used]) |> validate_required([:used]) end + @spec use_token(Authorization.t()) :: + {:ok, Authorization.t()} | {:error, Changeset.t()} | {:error, String.t()} def use_token(%Authorization{used: false, valid_until: valid_until} = auth) do if NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) < 0 do Repo.update(use_changeset(auth, %{used: true})) @@ -56,6 +75,7 @@ def use_token(%Authorization{used: false, valid_until: valid_until} = auth) do def use_token(%Authorization{used: true}), do: {:error, "already used"} + @spec delete_user_authorizations(User.t()) :: {integer(), any()} def delete_user_authorizations(%User{id: user_id}) do from( a in Pleroma.Web.OAuth.Authorization, @@ -63,4 +83,11 @@ def delete_user_authorizations(%User{id: user_id}) do ) |> Repo.delete_all() end + + @doc "gets auth for app by token" + @spec get_by_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found} + def get_by_token(%App{id: app_id} = _app, token) do + from(t in __MODULE__, where: t.app_id == ^app_id and t.token == ^token) + |> Repo.find_resource() + end end diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index 688eaca11..4ee8339e2 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -13,11 +13,14 @@ defmodule Pleroma.Web.OAuth.OAuthController do alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.Authorization alias Pleroma.Web.OAuth.Token - - import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2] + alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken + alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken + alias Pleroma.Web.OAuth.Scopes if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth) + @expires_in Pleroma.Config.get([:oauth2, :token_expires_in], 600) + plug(:fetch_session) plug(:fetch_flash) @@ -53,7 +56,7 @@ def authorize(conn, params), do: do_authorize(conn, params) defp do_authorize(conn, params) do app = Repo.get_by(App, client_id: params["client_id"]) available_scopes = (app && app.scopes) || [] - scopes = oauth_scopes(params, nil) || available_scopes + scopes = Scopes.fetch_scopes(params, available_scopes) # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template render(conn, Authenticator.auth_template(), %{ @@ -109,7 +112,7 @@ def after_create_authorization(conn, auth, %{ defp handle_create_authorization_error( conn, - {scopes_issue, _}, + {:error, scopes_issue}, %{"authorization" => _} = params ) when scopes_issue in [:unsupported_scopes, :missing_scopes] do @@ -138,25 +141,33 @@ defp handle_create_authorization_error(conn, error, %{"authorization" => _}) do Authenticator.handle_error(conn, error) end + @doc "Renew access_token with refresh_token" + def token_exchange( + conn, + %{"grant_type" => "refresh_token", "refresh_token" => token} = params + ) do + with %App{} = app <- get_app_from_request(conn, params), + {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token), + {:ok, token} <- RefreshToken.grant(token) do + response_attrs = %{created_at: Token.Utils.format_created_at(token)} + + json(conn, response_token(user, token, response_attrs)) + else + _error -> + put_status(conn, 400) + |> json(%{error: "Invalid credentials"}) + end + end + def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do with %App{} = app <- get_app_from_request(conn, params), - fixed_token = fix_padding(params["code"]), - %Authorization{} = auth <- - Repo.get_by(Authorization, token: fixed_token, app_id: app.id), + fixed_token = Token.Utils.fix_padding(params["code"]), + {:ok, auth} <- Authorization.get_by_token(app, fixed_token), %User{} = user <- User.get_cached_by_id(auth.user_id), - {:ok, token} <- Token.exchange_token(app, auth), - {:ok, inserted_at} <- DateTime.from_naive(token.inserted_at, "Etc/UTC") do - response = %{ - token_type: "Bearer", - access_token: token.token, - refresh_token: token.refresh_token, - created_at: DateTime.to_unix(inserted_at), - expires_in: 60 * 10, - scope: Enum.join(token.scopes, " "), - me: user.ap_id - } + {:ok, token} <- Token.exchange_token(app, auth) do + response_attrs = %{created_at: Token.Utils.format_created_at(token)} - json(conn, response) + json(conn, response_token(user, token, response_attrs)) else _error -> put_status(conn, 400) @@ -172,21 +183,10 @@ def token_exchange( %App{} = app <- get_app_from_request(conn, params), {:auth_active, true} <- {:auth_active, User.auth_active?(user)}, {:user_active, true} <- {:user_active, !user.info.deactivated}, - scopes <- oauth_scopes(params, app.scopes), - [] <- scopes -- app.scopes, - true <- Enum.any?(scopes), + {:ok, scopes} <- validate_scopes(app, params), {:ok, auth} <- Authorization.create_authorization(app, user, scopes), {:ok, token} <- Token.exchange_token(app, auth) do - response = %{ - token_type: "Bearer", - access_token: token.token, - refresh_token: token.refresh_token, - expires_in: 60 * 10, - scope: Enum.join(token.scopes, " "), - me: user.ap_id - } - - json(conn, response) + json(conn, response_token(user, token)) else {:auth_active, false} -> # Per https://github.com/tootsuite/mastodon/blob/ @@ -218,10 +218,34 @@ def token_exchange( token_exchange(conn, params) end - def token_revoke(conn, %{"token" => token} = params) do + def token_exchange(conn, %{"grant_type" => "client_credentials"} = params) do with %App{} = app <- get_app_from_request(conn, params), - %Token{} = token <- Repo.get_by(Token, token: token, app_id: app.id), - {:ok, %Token{}} <- Repo.delete(token) do + {:ok, auth} <- Authorization.create_authorization(app, %User{}), + {:ok, token} <- Token.exchange_token(app, auth), + {:ok, inserted_at} <- DateTime.from_naive(token.inserted_at, "Etc/UTC") do + response = %{ + token_type: "Bearer", + access_token: token.token, + refresh_token: token.refresh_token, + created_at: DateTime.to_unix(inserted_at), + expires_in: 60 * 10, + scope: Enum.join(token.scopes, " ") + } + + json(conn, response) + else + _error -> + put_status(conn, 400) + |> json(%{error: "Invalid credentials"}) + end + end + + # Bad request + def token_exchange(conn, params), do: bad_request(conn, params) + + def token_revoke(conn, %{"token" => _token} = params) do + with %App{} = app <- get_app_from_request(conn, params), + {:ok, _token} <- RevokeToken.revoke(app, params) do json(conn, %{}) else _error -> @@ -230,17 +254,27 @@ def token_revoke(conn, %{"token" => token} = params) do end end + def token_revoke(conn, params), do: bad_request(conn, params) + + # Response for bad request + defp bad_request(conn, _) do + conn + |> put_status(500) + |> json(%{error: "Bad request"}) + end + @doc "Prepares OAuth request to provider for Ueberauth" def prepare_request(conn, %{"provider" => provider, "authorization" => auth_attrs}) do scope = - oauth_scopes(auth_attrs, []) - |> Enum.join(" ") + auth_attrs + |> Scopes.fetch_scopes([]) + |> Scopes.to_string() state = auth_attrs |> Map.delete("scopes") |> Map.put("scope", scope) - |> Poison.encode!() + |> Jason.encode!() params = auth_attrs @@ -278,25 +312,22 @@ def callback(conn, params) do params = callback_params(params) with {:ok, registration} <- Authenticator.get_registration(conn) do - user = Repo.preload(registration, :user).user auth_attrs = Map.take(params, ~w(client_id redirect_uri scope scopes state)) - if user do - create_authorization( - conn, - %{"authorization" => auth_attrs}, - user: user - ) - else - registration_params = - Map.merge(auth_attrs, %{ - "nickname" => Registration.nickname(registration), - "email" => Registration.email(registration) - }) + case Repo.get_assoc(registration, :user) do + {:ok, user} -> + create_authorization(conn, %{"authorization" => auth_attrs}, user: user) - conn - |> put_session(:registration_id, registration.id) - |> registration_details(%{"authorization" => registration_params}) + _ -> + registration_params = + Map.merge(auth_attrs, %{ + "nickname" => Registration.nickname(registration), + "email" => Registration.email(registration) + }) + + conn + |> put_session(:registration_id, registration.id) + |> registration_details(%{"authorization" => registration_params}) end else _ -> @@ -307,7 +338,7 @@ def callback(conn, params) do end defp callback_params(%{"state" => state} = params) do - Map.merge(params, Poison.decode!(state)) + Map.merge(params, Jason.decode!(state)) end def registration_details(conn, %{"authorization" => auth_attrs}) do @@ -315,7 +346,7 @@ def registration_details(conn, %{"authorization" => auth_attrs}) do client_id: auth_attrs["client_id"], redirect_uri: auth_attrs["redirect_uri"], state: auth_attrs["state"], - scopes: oauth_scopes(auth_attrs, []), + scopes: Scopes.fetch_scopes(auth_attrs, []), nickname: auth_attrs["nickname"], email: auth_attrs["email"] }) @@ -390,45 +421,36 @@ defp do_create_authorization( {:get_user, (user && {:ok, user}) || Authenticator.get_user(conn)}, %App{} = app <- Repo.get_by(App, client_id: client_id), true <- redirect_uri in String.split(app.redirect_uris), - scopes <- oauth_scopes(auth_attrs, []), - {:unsupported_scopes, []} <- {:unsupported_scopes, scopes -- app.scopes}, - # Note: `scope` param is intentionally not optional in this context - {:missing_scopes, false} <- {:missing_scopes, scopes == []}, + {:ok, scopes} <- validate_scopes(app, auth_attrs), {:auth_active, true} <- {:auth_active, User.auth_active?(user)} do Authorization.create_authorization(app, user, scopes) end end - # XXX - for whatever reason our token arrives urlencoded, but Plug.Conn should be - # decoding it. Investigate sometime. - defp fix_padding(token) do - token - |> URI.decode() - |> Base.url_decode64!(padding: false) - |> Base.url_encode64(padding: false) + defp get_app_from_request(conn, params) do + conn + |> fetch_client_credentials(params) + |> fetch_client end - defp get_app_from_request(conn, params) do - # Per RFC 6749, HTTP Basic is preferred to body params - {client_id, client_secret} = - with ["Basic " <> encoded] <- get_req_header(conn, "authorization"), - {:ok, decoded} <- Base.decode64(encoded), - [id, secret] <- - String.split(decoded, ":") - |> Enum.map(fn s -> URI.decode_www_form(s) end) do - {id, secret} - else - _ -> {params["client_id"], params["client_secret"]} - end + defp fetch_client({id, secret}) when is_binary(id) and is_binary(secret) do + Repo.get_by(App, client_id: id, client_secret: secret) + end - if client_id && client_secret do - Repo.get_by( - App, - client_id: client_id, - client_secret: client_secret - ) + defp fetch_client({_id, _secret}), do: nil + + defp fetch_client_credentials(conn, params) do + # Per RFC 6749, HTTP Basic is preferred to body params + with ["Basic " <> encoded] <- get_req_header(conn, "authorization"), + {:ok, decoded} <- Base.decode64(encoded), + [id, secret] <- + Enum.map( + String.split(decoded, ":"), + fn s -> URI.decode_www_form(s) end + ) do + {id, secret} else - nil + _ -> {params["client_id"], params["client_secret"]} end end @@ -441,4 +463,24 @@ defp get_session_registration_id(conn), do: get_session(conn, :registration_id) defp put_session_registration_id(conn, registration_id), do: put_session(conn, :registration_id, registration_id) + + defp response_token(%User{} = user, token, opts \\ %{}) do + %{ + token_type: "Bearer", + access_token: token.token, + refresh_token: token.refresh_token, + expires_in: @expires_in, + scope: Enum.join(token.scopes, " "), + me: user.ap_id + } + |> Map.merge(opts) + end + + @spec validate_scopes(App.t(), map()) :: + {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} + defp validate_scopes(app, params) do + params + |> Scopes.fetch_scopes(app.scopes) + |> Scopes.validates(app.scopes) + end end diff --git a/lib/pleroma/web/oauth/scopes.ex b/lib/pleroma/web/oauth/scopes.ex new file mode 100644 index 000000000..ad9dfb260 --- /dev/null +++ b/lib/pleroma/web/oauth/scopes.ex @@ -0,0 +1,67 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.OAuth.Scopes do + @moduledoc """ + Functions for dealing with scopes. + """ + + @doc """ + Fetch scopes from requiest params. + + Note: `scopes` is used by Mastodon — supporting it but sticking to + OAuth's standard `scope` wherever we control it + """ + @spec fetch_scopes(map(), list()) :: list() + def fetch_scopes(params, default) do + parse_scopes(params["scope"] || params["scopes"], default) + end + + def parse_scopes(scopes, _default) when is_list(scopes) do + Enum.filter(scopes, &(&1 not in [nil, ""])) + end + + def parse_scopes(scopes, default) when is_binary(scopes) do + scopes + |> to_list + |> parse_scopes(default) + end + + def parse_scopes(_, default) do + default + end + + @doc """ + Convert scopes string to list + """ + @spec to_list(binary()) :: [binary()] + def to_list(nil), do: [] + + def to_list(str) do + str + |> String.trim() + |> String.split(~r/[\s,]+/) + end + + @doc """ + Convert scopes list to string + """ + @spec to_string(list()) :: binary() + def to_string(scopes), do: Enum.join(scopes, " ") + + @doc """ + Validates scopes. + """ + @spec validates(list() | nil, list()) :: + {:ok, list()} | {:error, :missing_scopes | :unsupported_scopes} + def validates([], _app_scopes), do: {:error, :missing_scopes} + def validates(nil, _app_scopes), do: {:error, :missing_scopes} + + def validates(scopes, app_scopes) do + case scopes -- app_scopes do + [] -> {:ok, scopes} + _ -> {:error, :unsupported_scopes} + end + end +end diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex index 399140003..ef047d565 100644 --- a/lib/pleroma/web/oauth/token.ex +++ b/lib/pleroma/web/oauth/token.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.OAuth.Token do use Ecto.Schema import Ecto.Query + import Ecto.Changeset alias Pleroma.Repo alias Pleroma.User @@ -13,6 +14,9 @@ defmodule Pleroma.Web.OAuth.Token do alias Pleroma.Web.OAuth.Authorization alias Pleroma.Web.OAuth.Token + @expires_in Pleroma.Config.get([:oauth2, :token_expires_in], 600) + @type t :: %__MODULE__{} + schema "oauth_tokens" do field(:token, :string) field(:refresh_token, :string) @@ -24,28 +28,72 @@ defmodule Pleroma.Web.OAuth.Token do timestamps() end + @doc "Gets token for app by access token" + @spec get_by_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found} + def get_by_token(%App{id: app_id} = _app, token) do + from(t in __MODULE__, where: t.app_id == ^app_id and t.token == ^token) + |> Repo.find_resource() + end + + @doc "Gets token for app by refresh token" + @spec get_by_refresh_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found} + def get_by_refresh_token(%App{id: app_id} = _app, token) do + from(t in __MODULE__, + where: t.app_id == ^app_id and t.refresh_token == ^token, + preload: [:user] + ) + |> Repo.find_resource() + end + + @spec exchange_token(App.t(), Authorization.t()) :: + {:ok, Token.t()} | {:error, Changeset.t()} def exchange_token(app, auth) do with {:ok, auth} <- Authorization.use_token(auth), true <- auth.app_id == app.id do - create_token(app, User.get_cached_by_id(auth.user_id), auth.scopes) + user = if auth.user_id, do: User.get_cached_by_id(auth.user_id), else: %User{} + + create_token( + app, + user, + %{scopes: auth.scopes} + ) end end - def create_token(%App{} = app, %User{} = user, scopes \\ nil) do - scopes = scopes || app.scopes - token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) - refresh_token = :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) + defp put_token(changeset) do + changeset + |> change(%{token: Token.Utils.generate_token()}) + |> validate_required([:token]) + |> unique_constraint(:token) + end - token = %Token{ - token: token, - refresh_token: refresh_token, - scopes: scopes, - user_id: user.id, - app_id: app.id, - valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), 60 * 10) - } + defp put_refresh_token(changeset, attrs) do + refresh_token = Map.get(attrs, :refresh_token, Token.Utils.generate_token()) - Repo.insert(token) + changeset + |> change(%{refresh_token: refresh_token}) + |> validate_required([:refresh_token]) + |> unique_constraint(:refresh_token) + end + + defp put_valid_until(changeset, attrs) do + expires_in = + Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), @expires_in)) + + changeset + |> change(%{valid_until: expires_in}) + |> validate_required([:valid_until]) + end + + @spec create_token(App.t(), User.t(), map()) :: {:ok, Token} | {:error, Changeset.t()} + def create_token(%App{} = app, %User{} = user, attrs \\ %{}) do + %__MODULE__{user_id: user.id, app_id: app.id} + |> cast(%{scopes: attrs[:scopes] || app.scopes}, [:scopes]) + |> validate_required([:scopes, :app_id]) + |> put_valid_until(attrs) + |> put_token() + |> put_refresh_token(attrs) + |> Repo.insert() end def delete_user_tokens(%User{id: user_id}) do @@ -73,4 +121,10 @@ def get_user_tokens(%User{id: user_id}) do |> Repo.all() |> Repo.preload(:app) end + + def is_expired?(%__MODULE__{valid_until: valid_until}) do + NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) > 0 + end + + def is_expired?(_), do: false end diff --git a/lib/pleroma/web/oauth/token/strategy/refresh_token.ex b/lib/pleroma/web/oauth/token/strategy/refresh_token.ex new file mode 100644 index 000000000..7df0be14e --- /dev/null +++ b/lib/pleroma/web/oauth/token/strategy/refresh_token.ex @@ -0,0 +1,54 @@ +defmodule Pleroma.Web.OAuth.Token.Strategy.RefreshToken do + @moduledoc """ + Functions for dealing with refresh token strategy. + """ + + alias Pleroma.Config + alias Pleroma.Repo + alias Pleroma.Web.OAuth.Token + alias Pleroma.Web.OAuth.Token.Strategy.Revoke + + @doc """ + Will grant access token by refresh token. + """ + @spec grant(Token.t()) :: {:ok, Token.t()} | {:error, any()} + def grant(token) do + access_token = Repo.preload(token, [:user, :app]) + + result = + Repo.transaction(fn -> + token_params = %{ + app: access_token.app, + user: access_token.user, + scopes: access_token.scopes + } + + access_token + |> revoke_access_token() + |> create_access_token(token_params) + end) + + case result do + {:ok, {:error, reason}} -> {:error, reason} + {:ok, {:ok, token}} -> {:ok, token} + {:error, reason} -> {:error, reason} + end + end + + defp revoke_access_token(token) do + Revoke.revoke(token) + end + + defp create_access_token({:error, error}, _), do: {:error, error} + + defp create_access_token({:ok, token}, %{app: app, user: user} = token_params) do + Token.create_token(app, user, add_refresh_token(token_params, token.refresh_token)) + end + + defp add_refresh_token(params, token) do + case Config.get([:oauth2, :issue_new_refresh_token], false) do + true -> Map.put(params, :refresh_token, token) + false -> params + end + end +end diff --git a/lib/pleroma/web/oauth/token/strategy/revoke.ex b/lib/pleroma/web/oauth/token/strategy/revoke.ex new file mode 100644 index 000000000..dea63ca54 --- /dev/null +++ b/lib/pleroma/web/oauth/token/strategy/revoke.ex @@ -0,0 +1,22 @@ +defmodule Pleroma.Web.OAuth.Token.Strategy.Revoke do + @moduledoc """ + Functions for dealing with revocation. + """ + + alias Pleroma.Repo + alias Pleroma.Web.OAuth.App + alias Pleroma.Web.OAuth.Token + + @doc "Finds and revokes access token for app and by token" + @spec revoke(App.t(), map()) :: {:ok, Token.t()} | {:error, :not_found | Ecto.Changeset.t()} + def revoke(%App{} = app, %{"token" => token} = _attrs) do + with {:ok, token} <- Token.get_by_token(app, token), + do: revoke(token) + end + + @doc "Revokes access token" + @spec revoke(Token.t()) :: {:ok, Token.t()} | {:error, Ecto.Changeset.t()} + def revoke(%Token{} = token) do + Repo.delete(token) + end +end diff --git a/lib/pleroma/web/oauth/token/utils.ex b/lib/pleroma/web/oauth/token/utils.ex new file mode 100644 index 000000000..a81560a1c --- /dev/null +++ b/lib/pleroma/web/oauth/token/utils.ex @@ -0,0 +1,30 @@ +defmodule Pleroma.Web.OAuth.Token.Utils do + @moduledoc """ + Auxiliary functions for dealing with tokens. + """ + + @doc "convert token inserted_at to unix timestamp" + def format_created_at(%{inserted_at: inserted_at} = _token) do + inserted_at + |> DateTime.from_naive!("Etc/UTC") + |> DateTime.to_unix() + end + + @doc false + @spec generate_token(keyword()) :: binary() + def generate_token(opts \\ []) do + opts + |> Keyword.get(:size, 32) + |> :crypto.strong_rand_bytes() + |> Base.url_encode64(padding: false) + end + + # XXX - for whatever reason our token arrives urlencoded, but Plug.Conn should be + # decoding it. Investigate sometime. + def fix_padding(token) do + token + |> URI.decode() + |> Base.url_decode64!(padding: false) + |> Base.url_encode64(padding: false) + end +end diff --git a/lib/pleroma/web/ostatus/activity_representer.ex b/lib/pleroma/web/ostatus/activity_representer.ex index 166691a09..95037125d 100644 --- a/lib/pleroma/web/ostatus/activity_representer.ex +++ b/lib/pleroma/web/ostatus/activity_representer.ex @@ -18,15 +18,18 @@ defp get_href(id) do end end - defp get_in_reply_to(%{"object" => %{"inReplyTo" => in_reply_to}}) do - [ - {:"thr:in-reply-to", - [ref: to_charlist(in_reply_to), href: to_charlist(get_href(in_reply_to))], []} - ] + defp get_in_reply_to(activity) do + with %Object{data: %{"inReplyTo" => in_reply_to}} <- Object.normalize(activity) do + [ + {:"thr:in-reply-to", + [ref: to_charlist(in_reply_to), href: to_charlist(get_href(in_reply_to))], []} + ] + else + _ -> + [] + end end - defp get_in_reply_to(_), do: [] - defp get_mentions(to) do Enum.map(to, fn id -> cond do @@ -98,7 +101,7 @@ def to_simple_form(%{data: %{"type" => "Create"}} = activity, user, with_author) []} end) - in_reply_to = get_in_reply_to(activity.data) + in_reply_to = get_in_reply_to(activity) author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: [] mentions = activity.recipients |> get_mentions @@ -146,7 +149,6 @@ def to_simple_form(%{data: %{"type" => "Like"}} = activity, user, with_author) d updated_at = activity.data["published"] inserted_at = activity.data["published"] - _in_reply_to = get_in_reply_to(activity.data) author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: [] mentions = activity.recipients |> get_mentions @@ -177,7 +179,6 @@ def to_simple_form(%{data: %{"type" => "Announce"}} = activity, user, with_autho updated_at = activity.data["published"] inserted_at = activity.data["published"] - _in_reply_to = get_in_reply_to(activity.data) author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: [] retweeted_activity = Activity.get_create_by_object_ap_id(activity.data["object"]) diff --git a/lib/pleroma/web/ostatus/ostatus.ex b/lib/pleroma/web/ostatus/ostatus.ex index 4744c6d83..61515b31e 100644 --- a/lib/pleroma/web/ostatus/ostatus.ex +++ b/lib/pleroma/web/ostatus/ostatus.ex @@ -16,6 +16,7 @@ defmodule Pleroma.Web.OStatus do alias Pleroma.Web alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Transmogrifier + alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.OStatus.DeleteHandler alias Pleroma.Web.OStatus.FollowHandler alias Pleroma.Web.OStatus.NoteHandler @@ -30,7 +31,7 @@ def is_representable?(%Activity{} = activity) do is_nil(object) -> false - object.data["type"] == "Note" -> + Visibility.is_public?(activity) && object.data["type"] == "Note" -> true true -> diff --git a/lib/pleroma/web/push/impl.ex b/lib/pleroma/web/push/impl.ex index 2233480c5..35d3ff07c 100644 --- a/lib/pleroma/web/push/impl.ex +++ b/lib/pleroma/web/push/impl.ex @@ -21,8 +21,10 @@ defmodule Pleroma.Web.Push.Impl do @doc "Performs sending notifications for user subscriptions" @spec perform(Notification.t()) :: list(any) | :error def perform( - %{activity: %{data: %{"type" => activity_type}, id: activity_id}, user_id: user_id} = - notif + %{ + activity: %{data: %{"type" => activity_type}, id: activity_id} = activity, + user_id: user_id + } = notif ) when activity_type in @types do actor = User.get_cached_by_ap_id(notif.activity.data["actor"]) @@ -30,13 +32,14 @@ def perform( type = Activity.mastodon_notification_type(notif.activity) gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key) avatar_url = User.avatar_url(actor) + object = Object.normalize(activity) for subscription <- fetch_subsriptions(user_id), get_in(subscription.data, ["alerts", type]) do %{ title: format_title(notif), access_token: subscription.token.token, - body: format_body(notif, actor), + body: format_body(notif, actor, object), notification_id: notif.id, notification_type: type, icon: avatar_url, @@ -95,25 +98,25 @@ def build_sub(subscription) do end def format_body( - %{activity: %{data: %{"type" => "Create", "object" => %{"content" => content}}}}, - actor + %{activity: %{data: %{"type" => "Create"}}}, + actor, + %{data: %{"content" => content}} ) do "@#{actor.nickname}: #{Utils.scrub_html_and_truncate(content, 80)}" end def format_body( - %{activity: %{data: %{"type" => "Announce", "object" => activity_id}}}, - actor + %{activity: %{data: %{"type" => "Announce"}}}, + actor, + %{data: %{"content" => content}} ) do - %Activity{data: %{"object" => %{"id" => object_id}}} = Activity.get_by_ap_id(activity_id) - %Object{data: %{"content" => content}} = Object.get_by_ap_id(object_id) - "@#{actor.nickname} repeated: #{Utils.scrub_html_and_truncate(content, 80)}" end def format_body( %{activity: %{data: %{"type" => type}}}, - actor + actor, + _object ) when type in ["Follow", "Like"] do case type do diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index ff4f08af5..51146d010 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -146,34 +146,52 @@ defmodule Pleroma.Web.Router do scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do pipe_through([:admin_api, :oauth_write]) - post("/user/follow", AdminAPIController, :user_follow) - post("/user/unfollow", AdminAPIController, :user_unfollow) - - get("/users", AdminAPIController, :list_users) - get("/users/:nickname", AdminAPIController, :user_show) + post("/users/follow", AdminAPIController, :user_follow) + post("/users/unfollow", AdminAPIController, :user_unfollow) + # TODO: to be removed at version 1.0 delete("/user", AdminAPIController, :user_delete) - patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation) post("/user", AdminAPIController, :user_create) + + delete("/users", AdminAPIController, :user_delete) + post("/users", AdminAPIController, :user_create) + patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation) put("/users/tag", AdminAPIController, :tag_users) delete("/users/tag", AdminAPIController, :untag_users) + # TODO: to be removed at version 1.0 get("/permission_group/:nickname", AdminAPIController, :right_get) get("/permission_group/:nickname/:permission_group", AdminAPIController, :right_get) post("/permission_group/:nickname/:permission_group", AdminAPIController, :right_add) delete("/permission_group/:nickname/:permission_group", AdminAPIController, :right_delete) - put("/activation_status/:nickname", AdminAPIController, :set_activation_status) + get("/users/:nickname/permission_group", AdminAPIController, :right_get) + get("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_get) + post("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_add) + + delete( + "/users/:nickname/permission_group/:permission_group", + AdminAPIController, + :right_delete + ) + + put("/users/:nickname/activation_status", AdminAPIController, :set_activation_status) post("/relay", AdminAPIController, :relay_follow) delete("/relay", AdminAPIController, :relay_unfollow) - get("/invite_token", AdminAPIController, :get_invite_token) - get("/invites", AdminAPIController, :invites) - post("/revoke_invite", AdminAPIController, :revoke_invite) - post("/email_invite", AdminAPIController, :email_invite) + get("/users/invite_token", AdminAPIController, :get_invite_token) + get("/users/invites", AdminAPIController, :invites) + post("/users/revoke_invite", AdminAPIController, :revoke_invite) + post("/users/email_invite", AdminAPIController, :email_invite) + # TODO: to be removed at version 1.0 get("/password_reset", AdminAPIController, :get_password_reset) + + get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset) + + get("/users", AdminAPIController, :list_users) + get("/users/:nickname", AdminAPIController, :user_show) end scope "/", Pleroma.Web.TwitterAPI do @@ -276,6 +294,9 @@ defmodule Pleroma.Web.Router do get("/suggestions", MastodonAPIController, :suggestions) + get("/conversations", MastodonAPIController, :conversations) + post("/conversations/:id/read", MastodonAPIController, :conversation_read) + get("/endorsements", MastodonAPIController, :empty_array) get("/pleroma/flavour", MastodonAPIController, :get_flavour) @@ -364,6 +385,8 @@ defmodule Pleroma.Web.Router do scope "/api/v1", Pleroma.Web.MastodonAPI do pipe_through(:api) + post("/accounts", MastodonAPIController, :account_register) + get("/instance", MastodonAPIController, :masto_instance) get("/instance/peers", MastodonAPIController, :peers) post("/apps", MastodonAPIController, :create_app) diff --git a/lib/pleroma/web/salmon/salmon.ex b/lib/pleroma/web/salmon/salmon.ex index ca51255f3..80c3a3190 100644 --- a/lib/pleroma/web/salmon/salmon.ex +++ b/lib/pleroma/web/salmon/salmon.ex @@ -3,12 +3,18 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.Salmon do + @behaviour Pleroma.Web.Federator.Publisher + @httpoison Application.get_env(:pleroma, :httpoison) use Bitwise + alias Pleroma.Activity alias Pleroma.Instances alias Pleroma.User + alias Pleroma.Web.ActivityPub.Visibility + alias Pleroma.Web.Federator.Publisher + alias Pleroma.Web.OStatus alias Pleroma.Web.OStatus.ActivityRepresenter alias Pleroma.Web.XML @@ -180,12 +186,12 @@ def remote_users(%User{id: user_id}, %{data: %{"to" => to} = data}) do end @doc "Pushes an activity to remote account." - def send_to_user(%{recipient: %{info: %{salmon: salmon}}} = params), - do: send_to_user(Map.put(params, :recipient, salmon)) + def publish_one(%{recipient: %{info: %{salmon: salmon}}} = params), + do: publish_one(Map.put(params, :recipient, salmon)) - def send_to_user(%{recipient: url, feed: feed, poster: poster} = params) when is_binary(url) do + def publish_one(%{recipient: url, feed: feed} = params) when is_binary(url) do with {:ok, %{status: code}} when code in 200..299 <- - poster.( + @httpoison.post( url, feed, [{"Content-Type", "application/magic-envelope+xml"}] @@ -199,11 +205,11 @@ def send_to_user(%{recipient: url, feed: feed, poster: poster} = params) when is e -> unless params[:unreachable_since], do: Instances.set_reachable(url) Logger.debug(fn -> "Pushing Salmon to #{url} failed, #{inspect(e)}" end) - :error + {:error, "Unreachable instance"} end end - def send_to_user(_), do: :noop + def publish_one(_), do: :noop @supported_activities [ "Create", @@ -214,13 +220,19 @@ def send_to_user(_), do: :noop "Delete" ] + def is_representable?(%Activity{data: %{"type" => type}} = activity) + when type in @supported_activities, + do: Visibility.is_public?(activity) + + def is_representable?(_), do: false + @doc """ Publishes an activity to remote accounts """ - @spec publish(User.t(), Pleroma.Activity.t(), Pleroma.HTTP.t()) :: none - def publish(user, activity, poster \\ &@httpoison.post/3) + @spec publish(User.t(), Pleroma.Activity.t()) :: none + def publish(user, activity) - def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity, poster) + def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity) when type in @supported_activities do feed = ActivityRepresenter.to_simple_form(activity, user, true) @@ -244,15 +256,29 @@ def publish(%{info: %{keys: keys}} = user, %{data: %{"type" => type}} = activity |> Enum.each(fn remote_user -> Logger.debug(fn -> "Sending Salmon to #{remote_user.ap_id}" end) - Pleroma.Web.Federator.publish_single_salmon(%{ + Publisher.enqueue_one(__MODULE__, %{ recipient: remote_user, feed: feed, - poster: poster, unreachable_since: reachable_urls_metadata[remote_user.info.salmon] }) end) end end - def publish(%{id: id}, _, _), do: Logger.debug(fn -> "Keys missing for user #{id}" end) + def publish(%{id: id}, _), do: Logger.debug(fn -> "Keys missing for user #{id}" end) + + def gather_webfinger_links(%User{} = user) do + {:ok, _private, public} = keys_from_pem(user.info.keys) + magic_key = encode_key(public) + + [ + %{"rel" => "salmon", "href" => OStatus.salmon_path(user)}, + %{ + "rel" => "magic-public-key", + "href" => "data:application/magic-public-key,#{magic_key}" + } + ] + end + + def gather_nodeinfo_protocol_names, do: [] end diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex index 72eaf2084..133decfc4 100644 --- a/lib/pleroma/web/streamer.ex +++ b/lib/pleroma/web/streamer.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.Streamer do use GenServer require Logger alias Pleroma.Activity + alias Pleroma.Conversation.Participation alias Pleroma.Notification alias Pleroma.Object alias Pleroma.User @@ -71,6 +72,15 @@ def handle_cast(%{action: :stream, topic: "direct", item: item}, topics) do {:noreply, topics} end + def handle_cast(%{action: :stream, topic: "participation", item: participation}, topics) do + user_topic = "direct:#{participation.user_id}" + Logger.debug("Trying to push a conversation participation to #{user_topic}\n\n") + + push_to_socket(topics, user_topic, participation) + + {:noreply, topics} + end + def handle_cast(%{action: :stream, topic: "list", item: item}, topics) do # filter the recipient list if the activity is not public, see #270. recipient_lists = @@ -192,6 +202,19 @@ defp represent_update(%Activity{} = activity) do |> Jason.encode!() end + def represent_conversation(%Participation{} = participation) do + %{ + event: "conversation", + payload: + Pleroma.Web.MastodonAPI.ConversationView.render("participation.json", %{ + participation: participation, + user: participation.user + }) + |> Jason.encode!() + } + |> Jason.encode!() + end + def push_to_socket(topics, topic, %Activity{data: %{"type" => "Announce"}} = item) do Enum.each(topics[topic] || [], fn socket -> # Get the current user so we have up-to-date blocks etc. @@ -214,6 +237,12 @@ def push_to_socket(topics, topic, %Activity{data: %{"type" => "Announce"}} = ite end) end + def push_to_socket(topics, topic, %Participation{} = participation) do + Enum.each(topics[topic] || [], fn socket -> + send(socket.transport_pid, {:text, represent_conversation(participation)}) + end) + end + def push_to_socket(topics, topic, %Activity{ data: %{"type" => "Delete", "deleted_activity_id" => deleted_activity_id} }) do diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 1122e6c5d..c03f8ab3a 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -352,7 +352,7 @@ def change_password(%{assigns: %{user: user}} = conn, params) do def delete_account(%{assigns: %{user: user}} = conn, params) do case CommonAPI.Utils.confirm_current_password(user, params["password"]) do {:ok, user} -> - Task.start(fn -> User.delete(user) end) + User.delete(user) json(conn, %{status: "success"}) {:error, msg} -> diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index adeac6f3c..1362ef57c 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -128,7 +128,7 @@ def upload(%Plug.Upload{} = file, %User{} = user, format \\ "xml") do end end - def register_user(params) do + def register_user(params, opts \\ []) do token = params["token"] params = %{ @@ -162,13 +162,22 @@ def register_user(params) do # I have no idea how this error handling works {:error, %{error: Jason.encode!(%{captcha: [error]})}} else - registrations_open = Pleroma.Config.get([:instance, :registrations_open]) - registration_process(registrations_open, params, token) + registration_process( + params, + %{ + registrations_open: Pleroma.Config.get([:instance, :registrations_open]), + token: token + }, + opts + ) end end - defp registration_process(registration_open, params, token) - when registration_open == false or is_nil(registration_open) do + defp registration_process(params, %{registrations_open: true}, opts) do + create_user(params, opts) + end + + defp registration_process(params, %{token: token}, opts) do invite = unless is_nil(token) do Repo.get_by(UserInviteToken, %{token: token}) @@ -182,19 +191,15 @@ defp registration_process(registration_open, params, token) invite when valid_invite? -> UserInviteToken.update_usage!(invite) - create_user(params) + create_user(params, opts) _ -> {:error, "Expired token"} end end - defp registration_process(true, params, _token) do - create_user(params) - end - - defp create_user(params) do - changeset = User.register_changeset(%User{}, params) + defp create_user(params, opts) do + changeset = User.register_changeset(%User{}, params, opts) case User.register(changeset) do {:ok, user} -> @@ -293,7 +298,7 @@ def search(_user, %{"q" => query} = params) do end def get_external_profile(for_user, uri) do - with %User{} = user <- User.get_or_fetch(uri) do + with {:ok, %User{} = user} <- User.get_or_fetch(uri) do {:ok, UserView.render("show.json", %{user: user, for: for_user})} else _e -> diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index 79ed9dad2..3c5a70be9 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do alias Ecto.Changeset alias Pleroma.Activity + alias Pleroma.Formatter alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Repo @@ -181,6 +182,7 @@ def dm_timeline(%{assigns: %{user: user}} = conn, params) do |> Map.put("blocking_user", user) |> Map.put("user", user) |> Map.put(:visibility, "direct") + |> Map.put(:order, :desc) activities = ActivityPub.fetch_activities_query([user.ap_id], params) @@ -438,7 +440,7 @@ def confirm_email(conn, %{"user_id" => uid, "token" => token}) do true <- user.local, true <- user.info.confirmation_pending, true <- user.info.confirmation_token == token, - info_change <- User.Info.confirmation_changeset(user.info, :confirmed), + info_change <- User.Info.confirmation_changeset(user.info, need_confirmation: false), changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_change), {:ok, _} <- User.update_and_set_cache(changeset) do conn @@ -653,7 +655,22 @@ defp build_info_cng(user, params) do defp parse_profile_bio(user, params) do if bio = params["description"] do - Map.put(params, "bio", User.parse_bio(bio, user)) + emojis_text = (params["description"] || "") <> " " <> (params["name"] || "") + + emojis = + ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text)) + |> Enum.dedup() + + user_info = + user.info + |> Map.put( + "emoji", + emojis + ) + + params + |> Map.put("bio", User.parse_bio(bio, user)) + |> Map.put("info", user_info) else params end diff --git a/lib/pleroma/web/twitter_api/views/activity_view.ex b/lib/pleroma/web/twitter_api/views/activity_view.ex index c64152da8..d084ad734 100644 --- a/lib/pleroma/web/twitter_api/views/activity_view.ex +++ b/lib/pleroma/web/twitter_api/views/activity_view.ex @@ -170,7 +170,7 @@ def render("activity.json", %{activity: %{data: %{"type" => "Announce"}} = activ created_at = activity.data["published"] |> Utils.date_to_asctime() announced_activity = Activity.get_create_by_object_ap_id(activity.data["object"]) - text = "#{user.nickname} retweeted a status." + text = "#{user.nickname} repeated a status." retweeted_status = render("activity.json", Map.merge(opts, %{activity: announced_activity})) diff --git a/lib/pleroma/web/twitter_api/views/user_view.ex b/lib/pleroma/web/twitter_api/views/user_view.ex index 0791ed760..f0a4ddbd3 100644 --- a/lib/pleroma/web/twitter_api/views/user_view.ex +++ b/lib/pleroma/web/twitter_api/views/user_view.ex @@ -67,6 +67,13 @@ defp do_render("user.json", %{user: user = %User{}} = assigns) do {String.trim(name, ":"), url} end) + emoji = Enum.dedup(emoji ++ user.info.emoji) + + description_html = + (user.bio || "") + |> HTML.filter_tags(User.html_filter_policy(for_user)) + |> Formatter.emojify(emoji) + # ``fields`` is an array of mastodon profile field, containing ``{"name": "…", "value": "…"}``. # For example: [{"name": "Pronoun", "value": "she/her"}, …] fields = @@ -74,58 +81,49 @@ defp do_render("user.json", %{user: user = %User{}} = assigns) do |> Enum.filter(fn %{"type" => t} -> t == "PropertyValue" end) |> Enum.map(fn fields -> Map.take(fields, ["name", "value"]) end) - data = %{ - "created_at" => user.inserted_at |> Utils.format_naive_asctime(), - "description" => HTML.strip_tags((user.bio || "") |> String.replace("
", "\n")), - "description_html" => HTML.filter_tags(user.bio, User.html_filter_policy(for_user)), - "favourites_count" => 0, - "followers_count" => user_info[:follower_count], - "following" => following, - "follows_you" => follows_you, - "statusnet_blocking" => statusnet_blocking, - "friends_count" => user_info[:following_count], - "id" => user.id, - "name" => user.name || user.nickname, - "name_html" => - if(user.name, - do: HTML.strip_tags(user.name) |> Formatter.emojify(emoji), - else: user.nickname - ), - "profile_image_url" => image, - "profile_image_url_https" => image, - "profile_image_url_profile_size" => image, - "profile_image_url_original" => image, - "rights" => %{ - "delete_others_notice" => !!user.info.is_moderator, - "admin" => !!user.info.is_admin - }, - "screen_name" => user.nickname, - "statuses_count" => user_info[:note_count], - "statusnet_profile_url" => user.ap_id, - "cover_photo" => User.banner_url(user) |> MediaProxy.url(), - "background_image" => image_url(user.info.background) |> MediaProxy.url(), - "is_local" => user.local, - "locked" => user.info.locked, - "default_scope" => user.info.default_scope, - "no_rich_text" => user.info.no_rich_text, - "hide_followers" => user.info.hide_followers, - "hide_follows" => user.info.hide_follows, - "fields" => fields, - - # Pleroma extension - "pleroma" => - %{ - "confirmation_pending" => user_info.confirmation_pending, - "tags" => user.tags - } - |> maybe_with_activation_status(user, for_user) - } - data = - if(user.info.is_admin || user.info.is_moderator, - do: maybe_with_role(data, user, for_user), - else: data - ) + %{ + "created_at" => user.inserted_at |> Utils.format_naive_asctime(), + "description" => HTML.strip_tags((user.bio || "") |> String.replace("
", "\n")), + "description_html" => description_html, + "favourites_count" => 0, + "followers_count" => user_info[:follower_count], + "following" => following, + "follows_you" => follows_you, + "statusnet_blocking" => statusnet_blocking, + "friends_count" => user_info[:following_count], + "id" => user.id, + "name" => user.name || user.nickname, + "name_html" => + if(user.name, + do: HTML.strip_tags(user.name) |> Formatter.emojify(emoji), + else: user.nickname + ), + "profile_image_url" => image, + "profile_image_url_https" => image, + "profile_image_url_profile_size" => image, + "profile_image_url_original" => image, + "screen_name" => user.nickname, + "statuses_count" => user_info[:note_count], + "statusnet_profile_url" => user.ap_id, + "cover_photo" => User.banner_url(user) |> MediaProxy.url(), + "background_image" => image_url(user.info.background) |> MediaProxy.url(), + "is_local" => user.local, + "locked" => user.info.locked, + "hide_followers" => user.info.hide_followers, + "hide_follows" => user.info.hide_follows, + "fields" => fields, + + # Pleroma extension + "pleroma" => + %{ + "confirmation_pending" => user_info.confirmation_pending, + "tags" => user.tags + } + |> maybe_with_activation_status(user, for_user) + } + |> maybe_with_user_settings(user, for_user) + |> maybe_with_role(user, for_user) if assigns[:token] do Map.put(data, "token", token_string(assigns[:token])) @@ -141,15 +139,35 @@ defp maybe_with_activation_status(data, user, %User{info: %{is_admin: true}}) do defp maybe_with_activation_status(data, _, _), do: data defp maybe_with_role(data, %User{id: id} = user, %User{id: id}) do - Map.merge(data, %{"role" => role(user), "show_role" => user.info.show_role}) + Map.merge(data, %{ + "role" => role(user), + "show_role" => user.info.show_role, + "rights" => %{ + "delete_others_notice" => !!user.info.is_moderator, + "admin" => !!user.info.is_admin + } + }) end defp maybe_with_role(data, %User{info: %{show_role: true}} = user, _user) do - Map.merge(data, %{"role" => role(user)}) + Map.merge(data, %{ + "role" => role(user), + "rights" => %{ + "delete_others_notice" => !!user.info.is_moderator, + "admin" => !!user.info.is_admin + } + }) end defp maybe_with_role(data, _, _), do: data + defp maybe_with_user_settings(data, %User{info: info, id: id} = _user, %User{id: id}) do + data + |> Kernel.put_in(["default_scope"], info.default_scope) + |> Kernel.put_in(["no_rich_text"], info.no_rich_text) + end + + defp maybe_with_user_settings(data, _, _), do: data defp role(%User{info: %{:is_admin => true}}), do: "admin" defp role(%User{info: %{:is_moderator => true}}), do: "moderator" defp role(_), do: "member" diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex index a3b0bf999..3a3b98a10 100644 --- a/lib/pleroma/web/web_finger/web_finger.ex +++ b/lib/pleroma/web/web_finger/web_finger.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Web.WebFinger do alias Pleroma.User alias Pleroma.Web - alias Pleroma.Web.OStatus + alias Pleroma.Web.Federator.Publisher alias Pleroma.Web.Salmon alias Pleroma.Web.XML alias Pleroma.XmlBuilder @@ -50,70 +50,40 @@ def webfinger(resource, fmt) when fmt in ["XML", "JSON"] do end end + defp gather_links(%User{} = user) do + [ + %{ + "rel" => "http://webfinger.net/rel/profile-page", + "type" => "text/html", + "href" => user.ap_id + } + ] ++ Publisher.gather_webfinger_links(user) + end + def represent_user(user, "JSON") do {:ok, user} = ensure_keys_present(user) - {:ok, _private, public} = Salmon.keys_from_pem(user.info.keys) - magic_key = Salmon.encode_key(public) %{ "subject" => "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}", "aliases" => [user.ap_id], - "links" => [ - %{ - "rel" => "http://schemas.google.com/g/2010#updates-from", - "type" => "application/atom+xml", - "href" => OStatus.feed_path(user) - }, - %{ - "rel" => "http://webfinger.net/rel/profile-page", - "type" => "text/html", - "href" => user.ap_id - }, - %{"rel" => "salmon", "href" => OStatus.salmon_path(user)}, - %{ - "rel" => "magic-public-key", - "href" => "data:application/magic-public-key,#{magic_key}" - }, - %{"rel" => "self", "type" => "application/activity+json", "href" => user.ap_id}, - %{ - "rel" => "self", - "type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", - "href" => user.ap_id - }, - %{ - "rel" => "http://ostatus.org/schema/1.0/subscribe", - "template" => OStatus.remote_follow_path() - } - ] + "links" => gather_links(user) } end def represent_user(user, "XML") do {:ok, user} = ensure_keys_present(user) - {:ok, _private, public} = Salmon.keys_from_pem(user.info.keys) - magic_key = Salmon.encode_key(public) + + links = + gather_links(user) + |> Enum.map(fn link -> {:Link, link} end) { :XRD, %{xmlns: "http://docs.oasis-open.org/ns/xri/xrd-1.0"}, [ {:Subject, "acct:#{user.nickname}@#{Pleroma.Web.Endpoint.host()}"}, - {:Alias, user.ap_id}, - {:Link, - %{ - rel: "http://schemas.google.com/g/2010#updates-from", - type: "application/atom+xml", - href: OStatus.feed_path(user) - }}, - {:Link, - %{rel: "http://webfinger.net/rel/profile-page", type: "text/html", href: user.ap_id}}, - {:Link, %{rel: "salmon", href: OStatus.salmon_path(user)}}, - {:Link, - %{rel: "magic-public-key", href: "data:application/magic-public-key,#{magic_key}"}}, - {:Link, %{rel: "self", type: "application/activity+json", href: user.ap_id}}, - {:Link, - %{rel: "http://ostatus.org/schema/1.0/subscribe", template: OStatus.remote_follow_path()}} - ] + {:Alias, user.ap_id} + ] ++ links } |> XmlBuilder.to_doc() end diff --git a/lib/pleroma/web/websub/websub.ex b/lib/pleroma/web/websub/websub.ex index 3ffa6b416..7ad0414ab 100644 --- a/lib/pleroma/web/websub/websub.ex +++ b/lib/pleroma/web/websub/websub.ex @@ -4,10 +4,14 @@ defmodule Pleroma.Web.Websub do alias Ecto.Changeset + alias Pleroma.Activity alias Pleroma.Instances alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.Endpoint alias Pleroma.Web.Federator + alias Pleroma.Web.Federator.Publisher alias Pleroma.Web.OStatus alias Pleroma.Web.OStatus.FeedRepresenter alias Pleroma.Web.Router.Helpers @@ -18,6 +22,8 @@ defmodule Pleroma.Web.Websub do import Ecto.Query + @behaviour Pleroma.Web.Federator.Publisher + @httpoison Application.get_env(:pleroma, :httpoison) def verify(subscription, getter \\ &@httpoison.get/3) do @@ -56,6 +62,13 @@ def verify(subscription, getter \\ &@httpoison.get/3) do "Undo", "Delete" ] + + def is_representable?(%Activity{data: %{"type" => type}} = activity) + when type in @supported_activities, + do: Visibility.is_public?(activity) + + def is_representable?(_), do: false + def publish(topic, user, %{data: %{"type" => type}} = activity) when type in @supported_activities do response = @@ -88,12 +101,14 @@ def publish(topic, user, %{data: %{"type" => type}} = activity) unreachable_since: reachable_callbacks_metadata[sub.callback] } - Federator.publish_single_websub(data) + Publisher.enqueue_one(__MODULE__, data) end) end def publish(_, _, _), do: "" + def publish(actor, activity), do: publish(Pleroma.Web.OStatus.feed_path(actor), actor, activity) + def sign(secret, doc) do :crypto.hmac(:sha, secret, to_string(doc)) |> Base.encode16() |> String.downcase() end @@ -299,4 +314,20 @@ def publish_one(%{xml: xml, topic: topic, callback: callback, secret: secret} = {:error, response} end end + + def gather_webfinger_links(%User{} = user) do + [ + %{ + "rel" => "http://schemas.google.com/g/2010#updates-from", + "type" => "application/atom+xml", + "href" => OStatus.feed_path(user) + }, + %{ + "rel" => "http://ostatus.org/schema/1.0/subscribe", + "template" => OStatus.remote_follow_path() + } + ] + end + + def gather_nodeinfo_protocol_names, do: ["ostatus"] end diff --git a/lib/xml_builder.ex b/lib/xml_builder.ex index 88f8ce2a3..b58602c7b 100644 --- a/lib/xml_builder.ex +++ b/lib/xml_builder.ex @@ -35,6 +35,7 @@ def to_doc(content), do: ~s() <> to_xml(co defp make_open_tag(tag, attributes) do attributes_string = for {attribute, value} <- attributes do + value = String.replace(value, "\"", """) "#{attribute}=\"#{value}\"" end |> Enum.join(" ") diff --git a/mix.exs b/mix.exs index 15e182239..b7b9d534d 100644 --- a/mix.exs +++ b/mix.exs @@ -16,11 +16,11 @@ def project do # Docs name: "Pleroma", - source_url: "https://git.pleroma.social/pleroma/pleroma", - source_url_pattern: - "https://git.pleroma.social/pleroma/pleroma/blob/develop/%{path}#L%{line}", homepage_url: "https://pleroma.social/", + source_url: "https://git.pleroma.social/pleroma/pleroma", docs: [ + source_url_pattern: + "https://git.pleroma.social/pleroma/pleroma/blob/develop/%{path}#L%{line}", logo: "priv/static/static/logo.png", extras: ["README.md", "CHANGELOG.md"] ++ Path.wildcard("docs/**/*.md"), groups_for_extras: [ @@ -41,7 +41,7 @@ def project do def application do [ mod: {Pleroma.Application, []}, - extra_applications: [:logger, :runtime_tools, :comeonin, :quack], + extra_applications: [:logger, :runtime_tools, :comeonin, :esshd, :quack], included_applications: [:ex_syslogger] ] end @@ -84,9 +84,10 @@ defp deps do {:ex_aws, "~> 2.0"}, {:ex_aws_s3, "~> 2.0"}, {:earmark, "~> 1.3"}, + {:bbcode, "~> 0.1"}, {:ex_machina, "~> 2.3", only: :test}, {:credo, "~> 0.9.3", only: [:dev, :test]}, - {:mock, "~> 0.3.1", only: :test}, + {:mock, "~> 0.3.3", only: :test}, {:crypt, git: "https://github.com/msantos/crypt", ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"}, {:cors_plug, "~> 1.5"}, @@ -101,7 +102,7 @@ defp deps do {:ueberauth, "~> 0.4"}, {:auto_linker, git: "https://git.pleroma.social/pleroma/auto_linker.git", - ref: "90613b4bae875a3610c275b7056b61ffdd53210d"}, + ref: "c00c4e75b35367fa42c95ffd9b8c455bf9995829"}, {:pleroma_job_queue, "~> 0.2.0"}, {:telemetry, "~> 0.3"}, {:prometheus_ex, "~> 3.0"}, @@ -110,7 +111,11 @@ defp deps do {:prometheus_ecto, "~> 1.4"}, {:prometheus_process_collector, "~> 1.4"}, {:recon, github: "ferd/recon", tag: "2.4.0"}, - {:quack, "~> 0.1.1"} + {:quack, "~> 0.1.1"}, + {:benchee, "~> 1.0"}, + {:esshd, "~> 0.1.0"}, + {:ex_rated, "~> 1.2"}, + {:plug_static_index_html, "~> 1.0.0"} ] ++ oauth_deps end diff --git a/mix.lock b/mix.lock index d494cc82d..0b24818c5 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,9 @@ %{ "accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm"}, - "auto_linker": {:git, "https://git.pleroma.social/pleroma/auto_linker.git", "90613b4bae875a3610c275b7056b61ffdd53210d", [ref: "90613b4bae875a3610c275b7056b61ffdd53210d"]}, + "auto_linker": {:git, "https://git.pleroma.social/pleroma/auto_linker.git", "c00c4e75b35367fa42c95ffd9b8c455bf9995829", [ref: "c00c4e75b35367fa42c95ffd9b8c455bf9995829"]}, "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"}, + "bbcode": {:hex, :bbcode, "0.1.0", "400e618b640b635261611d7fb7f79d104917fc5b084aae371ab6b08477cb035b", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, + "benchee": {:hex, :benchee, "1.0.1", "66b211f9bfd84bd97e6d1beaddf8fc2312aaabe192f776e8931cb0c16f53a521", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}], "hexpm"}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, "cachex": {:hex, :cachex, "3.0.2", "1351caa4e26e29f7d7ec1d29b53d6013f0447630bbf382b4fb5d5bad0209f203", [: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.4", "22c5e8d98a4db9494396e5727108dffb820ee0d18fed4b0aa8ab76e4f5bc32f1", [:mix], [{:tzdata, "~> 0.5.8 or ~> 0.1.201603", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, @@ -16,14 +18,18 @@ "crypt": {:git, "https://github.com/msantos/crypt", "1f2b58927ab57e72910191a7ebaeff984382a1d3", [ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"]}, "db_connection": {:hex, :db_connection, "2.0.5", "ddb2ba6761a08b2bb9ca0e7d260e8f4dd39067426d835c24491a321b7f92a4da", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"}, "decimal": {:hex, :decimal, "1.7.0", "30d6b52c88541f9a66637359ddf85016df9eb266170d53105f02e4a67e00c5aa", [:mix], [], "hexpm"}, + "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm"}, "earmark": {:hex, :earmark, "1.3.2", "b840562ea3d67795ffbb5bd88940b1bed0ed9fa32834915125ea7d02e35888a5", [:mix], [], "hexpm"}, "ecto": {:hex, :ecto, "3.0.7", "44dda84ac6b17bbbdeb8ac5dfef08b7da253b37a453c34ab1a98de7f7e5fec7f", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, "ecto_sql": {:hex, :ecto_sql, "3.0.5", "7e44172b4f7aca4469f38d7f6a3da394dbf43a1bcf0ca975e958cb957becd74e", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0.6", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.9.1", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.3.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, + "esshd": {:hex, :esshd, "0.1.0", "6f93a2062adb43637edad0ea7357db2702a4b80dd9683482fe00f5134e97f4c1", [:mix], [], "hexpm"}, "eternal": {:hex, :eternal, "1.2.0", "e2a6b6ce3b8c248f7dc31451aefca57e3bdf0e48d73ae5043229380a67614c41", [:mix], [], "hexpm"}, + "ex2ms": {:hex, :ex2ms, "1.5.0", "19e27f9212be9a96093fed8cdfbef0a2b56c21237196d26760f11dfcfae58e97", [:mix], [], "hexpm"}, "ex_aws": {:hex, :ex_aws, "2.1.0", "b92651527d6c09c479f9013caa9c7331f19cba38a650590d82ebf2c6c16a1d8a", [:mix], [{:configparser_ex, "~> 2.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "1.6.3 or 1.6.5 or 1.7.1 or 1.8.6 or ~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8", [hex: :jsx, repo: "hexpm", optional: true]}, {:poison, ">= 1.2.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:xml_builder, "~> 0.1.0", [hex: :xml_builder, repo: "hexpm", optional: true]}], "hexpm"}, "ex_aws_s3": {:hex, :ex_aws_s3, "2.0.1", "9e09366e77f25d3d88c5393824e613344631be8db0d1839faca49686e99b6704", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm"}, "ex_doc": {:hex, :ex_doc, "0.20.2", "1bd0dfb0304bade58beb77f20f21ee3558cc3c753743ae0ddbb0fd7ba2912331", [:mix], [{:earmark, "~> 1.3", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, "ex_machina": {:hex, :ex_machina, "2.3.0", "92a5ad0a8b10ea6314b876a99c8c9e3f25f4dde71a2a835845b136b9adaf199a", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm"}, + "ex_rated": {:hex, :ex_rated, "1.3.2", "6aeb32abb46ea6076f417a9ce8cb1cf08abf35fb2d42375beaad4dd72b550bf1", [:mix], [{:ex2ms, "~> 1.5", [hex: :ex2ms, repo: "hexpm", optional: false]}], "hexpm"}, "ex_syslogger": {:git, "https://github.com/slashmili/ex_syslogger.git", "f3963399047af17e038897c69e20d552e6899e1d", [tag: "1.4.0"]}, "floki": {:hex, :floki, "0.20.4", "be42ac911fece24b4c72f3b5846774b6e61b83fe685c2fc9d62093277fb3bc86", [:mix], [{:html_entities, "~> 0.4.0", [hex: :html_entities, repo: "hexpm", optional: false]}, {:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"}, "gen_smtp": {:hex, :gen_smtp, "0.13.0", "11f08504c4bdd831dc520b8f84a1dce5ce624474a797394e7aafd3c29f5dcd25", [:rebar3], [], "hexpm"}, @@ -42,7 +48,7 @@ "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"}, "mochiweb": {:hex, :mochiweb, "2.15.0", "e1daac474df07651e5d17cc1e642c4069c7850dc4508d3db7263a0651330aacc", [:rebar3], [], "hexpm"}, - "mock": {:hex, :mock, "0.3.1", "994f00150f79a0ea50dc9d86134cd9ebd0d177ad60bd04d1e46336cdfdb98ff9", [:mix], [{:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"}, + "mock": {:hex, :mock, "0.3.3", "42a433794b1291a9cf1525c6d26b38e039e0d3a360732b5e467bfc77ef26c914", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"}, "mogrify": {:hex, :mogrify, "0.6.1", "de1b527514f2d95a7bbe9642eb556061afb337e220cf97adbf3a4e6438ed70af", [:mix], [], "hexpm"}, "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"}, "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, @@ -55,6 +61,7 @@ "plug": {:hex, :plug, "1.7.2", "d7b7db7fbd755e8283b6c0a50be71ec0a3d67d9213d74422d9372effc8e87fd1", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}], "hexpm"}, "plug_cowboy": {:hex, :plug_cowboy, "2.0.1", "d798f8ee5acc86b7d42dbe4450b8b0dadf665ce588236eb0a751a132417a980e", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"}, + "plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm"}, "postgrex": {:hex, :postgrex, "0.14.1", "63247d4a5ad6b9de57a0bac5d807e1c32d41e39c04b8a4156a26c63bcd8a2e49", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, diff --git a/priv/repo/migrations/20190408123347_create_conversations.exs b/priv/repo/migrations/20190408123347_create_conversations.exs new file mode 100644 index 000000000..0e0af30ae --- /dev/null +++ b/priv/repo/migrations/20190408123347_create_conversations.exs @@ -0,0 +1,26 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Repo.Migrations.CreateConversations do + use Ecto.Migration + + def change do + create table(:conversations) do + add(:ap_id, :string, null: false) + timestamps() + end + + create table(:conversation_participations) do + add(:user_id, references(:users, type: :uuid, on_delete: :delete_all)) + add(:conversation_id, references(:conversations, on_delete: :delete_all)) + add(:read, :boolean, default: false) + + timestamps() + end + + create index(:conversation_participations, [:conversation_id]) + create unique_index(:conversation_participations, [:user_id, :conversation_id]) + create unique_index(:conversations, [:ap_id]) + end +end diff --git a/priv/repo/migrations/20190410152859_add_participation_updated_at_index.exs b/priv/repo/migrations/20190410152859_add_participation_updated_at_index.exs new file mode 100644 index 000000000..1ce688c52 --- /dev/null +++ b/priv/repo/migrations/20190410152859_add_participation_updated_at_index.exs @@ -0,0 +1,7 @@ +defmodule Pleroma.Repo.Migrations.AddParticipationUpdatedAtIndex do + use Ecto.Migration + + def change do + create index(:conversation_participations, ["updated_at desc"]) + end +end diff --git a/priv/repo/migrations/20190413082658_create_bookmarks.exs b/priv/repo/migrations/20190413082658_create_bookmarks.exs new file mode 100644 index 000000000..38b108158 --- /dev/null +++ b/priv/repo/migrations/20190413082658_create_bookmarks.exs @@ -0,0 +1,14 @@ +defmodule Pleroma.Repo.Migrations.CreateBookmarks do + use Ecto.Migration + + def change do + create table(:bookmarks) do + add(:user_id, references(:users, type: :uuid, on_delete: :delete_all)) + add(:activity_id, references(:activities, type: :uuid, on_delete: :delete_all)) + + timestamps() + end + + create(unique_index(:bookmarks, [:user_id, :activity_id])) + end +end diff --git a/priv/repo/migrations/20190414125034_migrate_old_bookmarks.exs b/priv/repo/migrations/20190414125034_migrate_old_bookmarks.exs new file mode 100644 index 000000000..134b7c6f7 --- /dev/null +++ b/priv/repo/migrations/20190414125034_migrate_old_bookmarks.exs @@ -0,0 +1,29 @@ +defmodule Pleroma.Repo.Migrations.MigrateOldBookmarks do + use Ecto.Migration + import Ecto.Query + alias Pleroma.Activity + alias Pleroma.Bookmark + alias Pleroma.User + alias Pleroma.Repo + + def change do + query = + from(u in User, + where: u.local == true, + where: fragment("array_length(bookmarks, 1)") > 0, + select: %{id: u.id, bookmarks: fragment("bookmarks")} + ) + + Repo.stream(query) + |> Enum.each(fn %{id: user_id, bookmarks: bookmarks} -> + Enum.each(bookmarks, fn ap_id -> + activity = Activity.get_create_by_object_ap_id(ap_id) + unless is_nil(activity), do: {:ok, _} = Bookmark.create(user_id, activity.id) + end) + end) + + alter table(:users) do + remove(:bookmarks) + end + end +end diff --git a/priv/repo/migrations/20190501125843_add_fts_index_to_objects.exs b/priv/repo/migrations/20190501125843_add_fts_index_to_objects.exs new file mode 100644 index 000000000..9b274695e --- /dev/null +++ b/priv/repo/migrations/20190501125843_add_fts_index_to_objects.exs @@ -0,0 +1,8 @@ +defmodule Pleroma.Repo.Migrations.AddFTSIndexToObjects do + use Ecto.Migration + + def change do + drop_if_exists index(:activities, ["(to_tsvector('english', data->'object'->>'content'))"], using: :gin, name: :activities_fts) + create index(:objects, ["(to_tsvector('english', data->>'content'))"], using: :gin, name: :objects_fts) + end +end diff --git a/priv/repo/migrations/20190501133552_add_refresh_token_index_to_token.exs b/priv/repo/migrations/20190501133552_add_refresh_token_index_to_token.exs new file mode 100644 index 000000000..449f2a3d4 --- /dev/null +++ b/priv/repo/migrations/20190501133552_add_refresh_token_index_to_token.exs @@ -0,0 +1,7 @@ +defmodule Pleroma.Repo.Migrations.AddRefreshTokenIndexToToken do + use Ecto.Migration + + def change do + create(unique_index(:oauth_tokens, [:refresh_token])) + end +end diff --git a/priv/repo/migrations/20190513175809_change_hide_column_in_filter_table.exs b/priv/repo/migrations/20190513175809_change_hide_column_in_filter_table.exs new file mode 100644 index 000000000..2ffb88cc9 --- /dev/null +++ b/priv/repo/migrations/20190513175809_change_hide_column_in_filter_table.exs @@ -0,0 +1,9 @@ +defmodule Pleroma.Repo.Migrations.ChangeHideColumnInFilterTable do + use Ecto.Migration + + def change do + alter table(:filters) do + modify :hide, :boolean, default: false + end + end +end diff --git a/priv/static/adminfe/favicon.ico b/priv/static/adminfe/favicon.ico new file mode 100644 index 000000000..34b63ac63 Binary files /dev/null and b/priv/static/adminfe/favicon.ico differ diff --git a/priv/static/adminfe/index.html b/priv/static/adminfe/index.html new file mode 100644 index 000000000..44a58a44e --- /dev/null +++ b/priv/static/adminfe/index.html @@ -0,0 +1 @@ +Admin FE
\ No newline at end of file diff --git a/priv/static/adminfe/static/css/app.cea15678.css b/priv/static/adminfe/static/css/app.cea15678.css new file mode 100644 index 000000000..136aa8bb1 Binary files /dev/null and b/priv/static/adminfe/static/css/app.cea15678.css differ diff --git a/priv/static/adminfe/static/css/chunk-18e1.6aaab273.css b/priv/static/adminfe/static/css/chunk-18e1.6aaab273.css new file mode 100644 index 000000000..da819ca09 Binary files /dev/null and b/priv/static/adminfe/static/css/chunk-18e1.6aaab273.css differ diff --git a/priv/static/adminfe/static/css/chunk-50cf.1db1ed5b.css b/priv/static/adminfe/static/css/chunk-50cf.1db1ed5b.css new file mode 100644 index 000000000..2a2fbd8e4 Binary files /dev/null and b/priv/static/adminfe/static/css/chunk-50cf.1db1ed5b.css differ diff --git a/priv/static/adminfe/static/css/chunk-8b70.9ba0945c.css b/priv/static/adminfe/static/css/chunk-8b70.9ba0945c.css new file mode 100644 index 000000000..7fa43bf28 Binary files /dev/null and b/priv/static/adminfe/static/css/chunk-8b70.9ba0945c.css differ diff --git a/priv/static/adminfe/static/css/chunk-elementUI.4296cedf.css b/priv/static/adminfe/static/css/chunk-elementUI.4296cedf.css new file mode 100644 index 000000000..fbd926db1 Binary files /dev/null and b/priv/static/adminfe/static/css/chunk-elementUI.4296cedf.css differ diff --git a/priv/static/adminfe/static/css/chunk-f018.0d22684d.css b/priv/static/adminfe/static/css/chunk-f018.0d22684d.css new file mode 100644 index 000000000..bdb738700 Binary files /dev/null and b/priv/static/adminfe/static/css/chunk-f018.0d22684d.css differ diff --git a/priv/static/adminfe/static/css/chunk-libs.bd17d456.css b/priv/static/adminfe/static/css/chunk-libs.bd17d456.css new file mode 100644 index 000000000..3a7a99679 Binary files /dev/null and b/priv/static/adminfe/static/css/chunk-libs.bd17d456.css differ diff --git a/priv/static/adminfe/static/fonts/element-icons.2fad952.woff b/priv/static/adminfe/static/fonts/element-icons.2fad952.woff new file mode 100644 index 000000000..28da65d49 Binary files /dev/null and b/priv/static/adminfe/static/fonts/element-icons.2fad952.woff differ diff --git a/priv/static/adminfe/static/fonts/element-icons.6f0a763.ttf b/priv/static/adminfe/static/fonts/element-icons.6f0a763.ttf new file mode 100644 index 000000000..73bc90f4a Binary files /dev/null and b/priv/static/adminfe/static/fonts/element-icons.6f0a763.ttf differ diff --git a/priv/static/adminfe/static/img/401.089007e.gif b/priv/static/adminfe/static/img/401.089007e.gif new file mode 100644 index 000000000..cd6e0d943 Binary files /dev/null and b/priv/static/adminfe/static/img/401.089007e.gif differ diff --git a/priv/static/adminfe/static/img/404.a57b6f3.png b/priv/static/adminfe/static/img/404.a57b6f3.png new file mode 100644 index 000000000..3d8e2305c Binary files /dev/null and b/priv/static/adminfe/static/img/404.a57b6f3.png differ diff --git a/priv/static/adminfe/static/js/7zzA.e1ae1c94.js b/priv/static/adminfe/static/js/7zzA.e1ae1c94.js new file mode 100644 index 000000000..4387b8321 Binary files /dev/null and b/priv/static/adminfe/static/js/7zzA.e1ae1c94.js differ diff --git a/priv/static/adminfe/static/js/JEtC.f9ba4594.js b/priv/static/adminfe/static/js/JEtC.f9ba4594.js new file mode 100644 index 000000000..504eaef1f Binary files /dev/null and b/priv/static/adminfe/static/js/JEtC.f9ba4594.js differ diff --git a/priv/static/adminfe/static/js/app.25699e3d.js b/priv/static/adminfe/static/js/app.25699e3d.js new file mode 100644 index 000000000..54694bf5e Binary files /dev/null and b/priv/static/adminfe/static/js/app.25699e3d.js differ diff --git a/priv/static/adminfe/static/js/chunk-18e1.7f9c377c.js b/priv/static/adminfe/static/js/chunk-18e1.7f9c377c.js new file mode 100644 index 000000000..1921d0f64 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-18e1.7f9c377c.js differ diff --git a/priv/static/adminfe/static/js/chunk-50cf.b9b1df43.js b/priv/static/adminfe/static/js/chunk-50cf.b9b1df43.js new file mode 100644 index 000000000..1b5614639 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-50cf.b9b1df43.js differ diff --git a/priv/static/adminfe/static/js/chunk-8b70.46525646.js b/priv/static/adminfe/static/js/chunk-8b70.46525646.js new file mode 100644 index 000000000..68b7ea1a3 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-8b70.46525646.js differ diff --git a/priv/static/adminfe/static/js/chunk-elementUI.d388c21d.js b/priv/static/adminfe/static/js/chunk-elementUI.d388c21d.js new file mode 100644 index 000000000..1f40d7e84 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-elementUI.d388c21d.js differ diff --git a/priv/static/adminfe/static/js/chunk-f018.e1a7a454.js b/priv/static/adminfe/static/js/chunk-f018.e1a7a454.js new file mode 100644 index 000000000..9c06e442c Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-f018.e1a7a454.js differ diff --git a/priv/static/adminfe/static/js/chunk-libs.48e79a9e.js b/priv/static/adminfe/static/js/chunk-libs.48e79a9e.js new file mode 100644 index 000000000..db0b5dc97 Binary files /dev/null and b/priv/static/adminfe/static/js/chunk-libs.48e79a9e.js differ diff --git a/priv/static/adminfe/static/js/runtime.7144b2cf.js b/priv/static/adminfe/static/js/runtime.7144b2cf.js new file mode 100644 index 000000000..0a58ac351 Binary files /dev/null and b/priv/static/adminfe/static/js/runtime.7144b2cf.js differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/langs/zh_CN.js b/priv/static/adminfe/static/tinymce4.7.5/langs/zh_CN.js new file mode 100644 index 000000000..e11f322cc Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/langs/zh_CN.js differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/codesample/css/prism.css b/priv/static/adminfe/static/tinymce4.7.5/plugins/codesample/css/prism.css new file mode 100644 index 000000000..128237fba Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/codesample/css/prism.css differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-cool.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-cool.gif new file mode 100644 index 000000000..ba90cc36f Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-cool.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-cry.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-cry.gif new file mode 100644 index 000000000..74d897a4f Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-cry.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-embarassed.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-embarassed.gif new file mode 100644 index 000000000..963a96b8a Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-embarassed.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-foot-in-mouth.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-foot-in-mouth.gif new file mode 100644 index 000000000..c7cf1011d Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-foot-in-mouth.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-frown.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-frown.gif new file mode 100644 index 000000000..716f55e16 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-frown.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-innocent.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-innocent.gif new file mode 100644 index 000000000..334d49e0e Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-innocent.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-kiss.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-kiss.gif new file mode 100644 index 000000000..4efd549ed Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-kiss.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-laughing.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-laughing.gif new file mode 100644 index 000000000..82c5b182e Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-laughing.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-money-mouth.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-money-mouth.gif new file mode 100644 index 000000000..ca2451e10 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-money-mouth.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-sealed.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-sealed.gif new file mode 100644 index 000000000..fe66220c2 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-sealed.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-smile.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-smile.gif new file mode 100644 index 000000000..fd27edfaa Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-smile.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-surprised.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-surprised.gif new file mode 100644 index 000000000..0cc9bb71c Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-surprised.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-tongue-out.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-tongue-out.gif new file mode 100644 index 000000000..2075dc160 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-tongue-out.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-undecided.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-undecided.gif new file mode 100644 index 000000000..bef7e2573 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-undecided.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-wink.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-wink.gif new file mode 100644 index 000000000..0631c7616 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-wink.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-yell.gif b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-yell.gif new file mode 100644 index 000000000..648e6e879 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-yell.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/plugins/visualblocks/css/visualblocks.css b/priv/static/adminfe/static/tinymce4.7.5/plugins/visualblocks/css/visualblocks.css new file mode 100644 index 000000000..96e4d7c5d Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/plugins/visualblocks/css/visualblocks.css differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/content.inline.min.css b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/content.inline.min.css new file mode 100644 index 000000000..7b45d3397 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/content.inline.min.css differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/content.min.css b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/content.min.css new file mode 100644 index 000000000..bad168cfe Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/content.min.css differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-mobile.woff b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-mobile.woff new file mode 100644 index 000000000..1e3be038a Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-mobile.woff differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.eot b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.eot new file mode 100644 index 000000000..b144ba0bd Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.eot differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.svg b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.svg new file mode 100644 index 000000000..b4ee6f408 --- /dev/null +++ b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.svg @@ -0,0 +1,63 @@ + + + +Generated by IcoMoon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.ttf b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.ttf new file mode 100644 index 000000000..a983e2dc4 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.ttf differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.woff b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.woff new file mode 100644 index 000000000..d8962df76 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce-small.woff differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.eot b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.eot new file mode 100644 index 000000000..5336c38ff Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.eot differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.svg b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.svg new file mode 100644 index 000000000..9fa215f3d --- /dev/null +++ b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.svg @@ -0,0 +1,131 @@ + + + +Generated by IcoMoon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.ttf b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.ttf new file mode 100644 index 000000000..61a48a511 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.ttf differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.woff b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.woff new file mode 100644 index 000000000..aace5d9c5 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/fonts/tinymce.woff differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/anchor.gif b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/anchor.gif new file mode 100644 index 000000000..606348c7f Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/anchor.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/loader.gif b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/loader.gif new file mode 100644 index 000000000..c69e93723 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/loader.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/object.gif b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/object.gif new file mode 100644 index 000000000..cccd7f023 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/object.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/trans.gif b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/trans.gif new file mode 100644 index 000000000..388486517 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/img/trans.gif differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/skin.min.css b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/skin.min.css new file mode 100644 index 000000000..4ad815bf5 Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/skin.min.css differ diff --git a/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/skin.min.css.map b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/skin.min.css.map new file mode 100644 index 000000000..c8763dcc3 --- /dev/null +++ b/priv/static/adminfe/static/tinymce4.7.5/skins/lightgray/skin.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["./src/skins/lightgray/main/less/desktop/Reset.less","./src/skins/lightgray/main/less/desktop/Variables.less","./src/skins/lightgray/main/less/desktop/Mixins.less","./src/skins/lightgray/main/less/desktop/Animations.less","./src/skins/lightgray/main/less/desktop/TinyMCE.less","./src/skins/lightgray/main/less/desktop/CropRect.less","./src/skins/lightgray/main/less/desktop/ImagePanel.less","./src/skins/lightgray/main/less/desktop/Arrows.less","./src/skins/lightgray/main/less/desktop/Sidebar.less","./src/skins/lightgray/main/less/desktop/Container.less","./src/skins/lightgray/main/less/desktop/Scrollable.less","./src/skins/lightgray/main/less/desktop/Panel.less","./src/skins/lightgray/main/less/desktop/FloatPanel.less","./src/skins/lightgray/main/less/desktop/Window.less","./src/skins/lightgray/main/less/desktop/ToolTip.less","./src/skins/lightgray/main/less/desktop/Progress.less","./src/skins/lightgray/main/less/desktop/Notification.less","./src/skins/lightgray/main/less/desktop/AbsoluteLayout.less","./src/skins/lightgray/main/less/desktop/Button.less","./src/skins/lightgray/main/less/desktop/ButtonGroup.less","./src/skins/lightgray/main/less/desktop/Checkbox.less","./src/skins/lightgray/main/less/desktop/ComboBox.less","./src/skins/lightgray/main/less/desktop/ColorBox.less","./src/skins/lightgray/main/less/desktop/ColorButton.less","./src/skins/lightgray/main/less/desktop/ColorPicker.less","./src/skins/lightgray/main/less/desktop/Path.less","./src/skins/lightgray/main/less/desktop/FieldSet.less","./src/skins/lightgray/main/less/desktop/FitLayout.less","./src/skins/lightgray/main/less/desktop/FlowLayout.less","./src/skins/lightgray/main/less/desktop/Iframe.less","./src/skins/lightgray/main/less/desktop/InfoBox.less","./src/skins/lightgray/main/less/desktop/Label.less","./src/skins/lightgray/main/less/desktop/MenuBar.less","./src/skins/lightgray/main/less/desktop/MenuButton.less","./src/skins/lightgray/main/less/desktop/MenuItem.less","./src/skins/lightgray/main/less/desktop/Throbber.less","./src/skins/lightgray/main/less/desktop/Menu.less","./src/skins/lightgray/main/less/desktop/ListBox.less","./src/skins/lightgray/main/less/desktop/ResizeHandle.less","./src/skins/lightgray/main/less/desktop/SelectBox.less","./src/skins/lightgray/main/less/desktop/Slider.less","./src/skins/lightgray/main/less/desktop/Spacer.less","./src/skins/lightgray/main/less/desktop/SplitButton.less","./src/skins/lightgray/main/less/desktop/StackLayout.less","./src/skins/lightgray/main/less/desktop/TabPanel.less","./src/skins/lightgray/main/less/desktop/TextBox.less","./src/skins/lightgray/main/less/desktop/DropZone.less","./src/skins/lightgray/main/less/desktop/BrowseButton.less","./src/skins/lightgray/main/less/desktop/Icons.less","./src/skins/lightgray/main/less/desktop/FilePicker.less"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/priv/static/adminfe/static/tinymce4.7.5/tinymce.min.js b/priv/static/adminfe/static/tinymce4.7.5/tinymce.min.js new file mode 100644 index 000000000..d7fcac80b Binary files /dev/null and b/priv/static/adminfe/static/tinymce4.7.5/tinymce.min.js differ diff --git a/priv/static/images/pleroma-fox-tan-shy.png b/priv/static/images/pleroma-fox-tan-shy.png new file mode 100644 index 000000000..6e24be1e3 Binary files /dev/null and b/priv/static/images/pleroma-fox-tan-shy.png differ diff --git a/priv/static/index.html b/priv/static/index.html index 3114acffe..1dcedeec8 100644 --- a/priv/static/index.html +++ b/priv/static/index.html @@ -1 +1 @@ -Pleroma
\ No newline at end of file +Pleroma
\ No newline at end of file diff --git a/priv/static/static/config.json b/priv/static/static/config.json index 533a5b087..04cbb97b5 100644 --- a/priv/static/static/config.json +++ b/priv/static/static/config.json @@ -8,7 +8,6 @@ "redirectRootLogin": "/main/friends", "chatDisabled": false, "showInstanceSpecificPanel": false, - "scopeOptionsEnabled": false, "formattingOptionsEnabled": false, "collapseMessageWithSubject": false, "scopeCopy": true, @@ -21,5 +20,6 @@ "webPushNotifications": false, "noAttachmentLinks": false, "nsfwCensorImage": "", - "showFeaturesPanel": true + "showFeaturesPanel": true, + "minimalScopesMode": false } diff --git a/priv/static/static/css/app.a81578273cb4c57163939ab70c80eb06.css b/priv/static/static/css/app.a81578273cb4c57163939ab70c80eb06.css new file mode 100644 index 000000000..bf3c12d78 Binary files /dev/null and b/priv/static/static/css/app.a81578273cb4c57163939ab70c80eb06.css differ diff --git a/priv/static/static/css/app.a81578273cb4c57163939ab70c80eb06.css.map b/priv/static/static/css/app.a81578273cb4c57163939ab70c80eb06.css.map new file mode 100644 index 000000000..e4bc2dbe1 --- /dev/null +++ b/priv/static/static/css/app.a81578273cb4c57163939ab70c80eb06.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["webpack:///webpack:///src/components/timeline/timeline.vue","webpack:///webpack:///src/components/status/status.vue","webpack:///webpack:///src/components/attachment/attachment.vue","webpack:///webpack:///src/components/still-image/still-image.vue","webpack:///webpack:///src/components/favorite_button/favorite_button.vue","webpack:///webpack:///src/components/retweet_button/retweet_button.vue","webpack:///webpack:///src/components/delete_button/delete_button.vue","webpack:///webpack:///src/components/post_status_form/post_status_form.vue","webpack:///webpack:///src/components/media_upload/media_upload.vue","webpack:///webpack:///src/components/emoji-input/emoji-input.vue","webpack:///webpack:///src/components/user_card/user_card.vue","webpack:///webpack:///src/components/user_avatar/user_avatar.vue","webpack:///webpack:///src/components/remote_follow/remote_follow.vue","webpack:///webpack:///src/components/moderation_tools/moderation_tools.vue","webpack:///webpack:///src/components/dialog_modal/dialog_modal.vue","webpack:///webpack:///~/vue-popperjs/src/component/popper.js.vue","webpack:///webpack:///src/components/gallery/gallery.vue","webpack:///webpack:///src/components/link-preview/link-preview.vue","webpack:///webpack:///src/components/conversation/conversation.vue","webpack:///webpack:///src/components/user_profile/user_profile.vue","webpack:///webpack:///src/components/follow_card/follow_card.vue","webpack:///webpack:///src/components/basic_user_card/basic_user_card.vue","webpack:///webpack:///src/components/list/list.vue","webpack:///webpack:///src/hocs/with_load_more/src/hocs/with_load_more/with_load_more.scss","webpack:///webpack:///src/components/settings/settings.vue","webpack:///webpack:///src/components/tab_switcher/src/components/tab_switcher/tab_switcher.scss","webpack:///webpack:///src/components/style_switcher/style_switcher.scss","webpack:///webpack:///src/components/color_input/color_input.vue","webpack:///webpack:///src/components/shadow_control/shadow_control.vue","webpack:///webpack:///src/components/font_control/font_control.vue","webpack:///webpack:///src/components/contrast_ratio/contrast_ratio.vue","webpack:///webpack:///src/components/export_import/export_import.vue","webpack:///webpack:///src/components/registration/registration.vue","webpack:///webpack:///src/components/user_settings/user_settings.vue","webpack:///webpack:///src/components/image_cropper/image_cropper.vue","webpack:///webpack:///~/cropperjs/dist/cropper.css","webpack:///webpack:///src/components/block_card/block_card.vue","webpack:///webpack:///src/components/mute_card/mute_card.vue","webpack:///webpack:///src/components/selectable_list/selectable_list.vue","webpack:///webpack:///src/components/checkbox/checkbox.vue","webpack:///webpack:///src/components/autosuggest/autosuggest.vue","webpack:///webpack:///src/hocs/with_subscription/src/hocs/with_subscription/with_subscription.scss","webpack:///webpack:///src/components/follow_request_card/follow_request_card.vue","webpack:///webpack:///src/components/user_search/user_search.vue","webpack:///webpack:///src/components/notifications/notifications.scss","webpack:///webpack:///src/components/login_form/login_form.vue","webpack:///webpack:///src/components/chat_panel/chat_panel.vue","webpack:///webpack:///src/components/features_panel/features_panel.vue","webpack:///webpack:///src/components/terms_of_service_panel/terms_of_service_panel.vue","webpack:///webpack:///src/App.scss","webpack:///webpack:///src/components/nav_panel/nav_panel.vue","webpack:///webpack:///src/components/user_finder/user_finder.vue","webpack:///webpack:///src/components/who_to_follow_panel/who_to_follow_panel.vue","webpack:///webpack:///src/components/media_modal/media_modal.vue","webpack:///webpack:///src/components/side_drawer/side_drawer.vue","webpack:///webpack:///src/components/mobile_post_status_modal/mobile_post_status_modal.vue","webpack:///webpack:///src/components/mobile_nav/mobile_nav.vue"],"names":[],"mappings":"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","file":"static/css/app.a81578273cb4c57163939ab70c80eb06.css","sourcesContent":["\n.timeline .loadmore-text{opacity:1\n}\n.new-status-notification{position:relative;margin-top:-1px;font-size:1.1em;border-width:1px 0 0 0;border-style:solid;border-color:var(--border, #222);padding:10px;z-index:1;background-color:#182230;background-color:var(--panel, #182230)\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/timeline/timeline.vue","\n.status-body{-ms-flex:1;flex:1;min-width:0\n}\n.status-preview.status-el{border-style:solid;border-width:1px;border-color:#222;border-color:var(--border, #222)\n}\n.status-preview-container{position:relative;max-width:100%\n}\n.status-preview{position:absolute;max-width:95%;display:-ms-flexbox;display:flex;background-color:#121a24;background-color:var(--bg, #121a24);border-color:#222;border-color:var(--border, #222);border-style:solid;border-width:1px;border-radius:5px;border-radius:var(--tooltipRadius, 5px);box-shadow:2px 2px 3px rgba(0,0,0,0.5);box-shadow:var(--popupShadow);margin-top:0.25em;margin-left:0.5em;z-index:50\n}\n.status-preview .status{-ms-flex:1;flex:1;border:0;min-width:15em\n}\n.status-preview-loading{display:block;min-width:15em;padding:1em;text-align:center;border-width:1px;border-style:solid\n}\n.status-preview-loading i{font-size:2em\n}\n.media-left{margin-right:.75em\n}\n.status-el{-webkit-hyphens:auto;-ms-hyphens:auto;hyphens:auto;overflow-wrap:break-word;word-wrap:break-word;word-break:break-word;border-left-width:0px;min-width:0;border-color:#222;border-color:var(--border, #222);border-left:4px red;border-left:4px var(--cRed, red)\n}\n.status-el_focused{background-color:#151e2a;background-color:var(--lightBg, #151e2a)\n}\n.timeline .status-el{border-bottom-width:1px;border-bottom-style:solid\n}\n.status-el .media-body{-ms-flex:1;flex:1;padding:0\n}\n.status-el .status-usercard{margin-bottom:.75em\n}\n.status-el .user-name{white-space:nowrap;font-size:14px;overflow:hidden;-ms-flex-negative:0;flex-shrink:0;max-width:85%;font-weight:bold\n}\n.status-el .user-name img{width:14px;height:14px;vertical-align:middle;object-fit:contain\n}\n.status-el .media-heading{padding:0;vertical-align:bottom;-ms-flex-preferred-size:100%;flex-basis:100%;margin-bottom:0.5em\n}\n.status-el .media-heading a{display:inline-block;word-break:break-all\n}\n.status-el .media-heading small{font-weight:lighter\n}\n.status-el .media-heading .heading-name-row{padding:0;display:-ms-flexbox;display:flex;-ms-flex-pack:justify;justify-content:space-between;line-height:18px\n}\n.status-el .media-heading .heading-name-row .name-and-account-name{display:-ms-flexbox;display:flex;min-width:0\n}\n.status-el .media-heading .heading-name-row .user-name{-ms-flex-negative:1;flex-shrink:1;margin-right:0.4em;overflow:hidden;text-overflow:ellipsis\n}\n.status-el .media-heading .heading-name-row .account-name{min-width:1.6em;margin-right:0.4em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;-ms-flex:1 1 0px;flex:1 1 0\n}\n.status-el .media-heading .heading-right{display:-ms-flexbox;display:flex;-ms-flex-negative:0;flex-shrink:0\n}\n.status-el .media-heading .timeago{margin-right:0.2em\n}\n.status-el .media-heading .heading-reply-row{-ms-flex-line-pack:baseline;align-content:baseline;font-size:12px;line-height:18px;max-width:100%;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch\n}\n.status-el .media-heading .heading-reply-row a{max-width:100%;text-overflow:ellipsis;overflow:hidden;white-space:nowrap\n}\n.status-el .media-heading .reply-to-and-accountname{display:-ms-flexbox;display:flex;height:18px;margin-right:0.5em;overflow:hidden;max-width:100%\n}\n.status-el .media-heading .reply-to-and-accountname .icon-reply{transform:scaleX(-1)\n}\n.status-el .media-heading .reply-info{display:-ms-flexbox;display:flex\n}\n.status-el .media-heading .reply-to{display:-ms-flexbox;display:flex\n}\n.status-el .media-heading .reply-to-text{overflow:hidden;text-overflow:ellipsis;margin:0 0.4em 0 0.2em\n}\n.status-el .media-heading .replies-separator{margin-left:0.4em\n}\n.status-el .media-heading .replies{line-height:18px;font-size:12px;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap\n}\n.status-el .media-heading .replies>*{margin-right:0.4em\n}\n.status-el .media-heading .reply-link{height:17px\n}\n.status-el .tall-status{position:relative;height:220px;overflow-x:hidden;overflow-y:hidden\n}\n.status-el .tall-status-hider{display:inline-block;word-break:break-all;position:absolute;height:70px;margin-top:150px;width:100%;text-align:center;line-height:110px;background:linear-gradient(to bottom, transparent, #121a24 80%);background:linear-gradient(to bottom, transparent, var(--bg, #121a24) 80%)\n}\n.status-el .tall-status-hider_focused{background:linear-gradient(to bottom, transparent, #151e2a 80%);background:linear-gradient(to bottom, transparent, var(--lightBg, #151e2a) 80%)\n}\n.status-el .status-unhider,.status-el .cw-status-hider{width:100%;text-align:center;display:inline-block;word-break:break-all\n}\n.status-el .status-content{font-family:var(--postFont, sans-serif);line-height:1.4em\n}\n.status-el .status-content img,.status-el .status-content video{max-width:100%;max-height:400px;vertical-align:middle;object-fit:contain\n}\n.status-el .status-content img.emoji,.status-el .status-content video.emoji{width:32px;height:32px\n}\n.status-el .status-content blockquote{margin:0.2em 0 0.2em 2em;font-style:italic\n}\n.status-el .status-content pre{overflow:auto\n}\n.status-el .status-content code,.status-el .status-content samp,.status-el .status-content kbd,.status-el .status-content var,.status-el .status-content pre{font-family:var(--postCodeFont, monospace)\n}\n.status-el .status-content p{margin:0 0 1em 0\n}\n.status-el .status-content p:last-child{margin:0 0 0 0\n}\n.status-el .status-content h1{font-size:1.1em;line-height:1.2em;margin:1.4em 0\n}\n.status-el .status-content h2{font-size:1.1em;margin:1.0em 0\n}\n.status-el .status-content h3{font-size:1em;margin:1.2em 0\n}\n.status-el .status-content h4{margin:1.1em 0\n}\n.status-el .retweet-info{padding:0.4em .75em;margin:0\n}\n.status-el .retweet-info .avatar.still-image{border-radius:10px;border-radius:var(--avatarAltRadius, 10px);margin-left:28px;width:20px;height:20px\n}\n.status-el .retweet-info .media-body{font-size:1em;line-height:22px;display:-ms-flexbox;display:flex;-ms-flex-line-pack:center;align-content:center;-ms-flex-wrap:wrap;flex-wrap:wrap\n}\n.status-el .retweet-info .media-body .user-name{font-weight:bold;overflow:hidden;text-overflow:ellipsis\n}\n.status-el .retweet-info .media-body .user-name img{width:14px;height:14px;vertical-align:middle;object-fit:contain\n}\n.status-el .retweet-info .media-body i{padding:0 0.2em\n}\n.status-el .retweet-info .media-body a{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap\n}\n.status-fadein{animation-duration:0.4s;animation-name:fadein\n}\n@keyframes fadein{\nfrom{opacity:0\n}\nto{opacity:1\n}\n}\n.greentext{color:green\n}\n.status-conversation{border-left-style:solid\n}\n.status-actions{width:100%;display:-ms-flexbox;display:flex;margin-top:.75em\n}\n.status-actions div,.status-actions favorite-button{max-width:4em;-ms-flex:1;flex:1\n}\n.icon-reply:hover{color:#0095ff;color:var(--cBlue, #0095ff);cursor:pointer\n}\n.icon-reply.icon-reply-active{color:#0095ff;color:var(--cBlue, #0095ff)\n}\n.status:hover .animated.avatar canvas{display:none\n}\n.status:hover .animated.avatar img{visibility:visible\n}\n.status{display:-ms-flexbox;display:flex;padding:.75em\n}\n.status.is-retweet{padding-top:0\n}\n.status-conversation:last-child{border-bottom:none\n}\n.muted{padding:0.25em 0.5em\n}\n.muted button{margin-left:auto\n}\n.muted .muteWords{margin-left:10px\n}\na.unmute{display:block;margin-left:auto\n}\n.reply-left{-ms-flex:0;flex:0;min-width:48px\n}\n.reply-body{-ms-flex:1;flex:1\n}\n.timeline>.status-el:last-child{border-radius:0 0 10px 10px;border-radius:0 0 var(--panelRadius, 10px) var(--panelRadius, 10px);border-bottom:none\n}\n@media all and (max-width: 800px){\n.status-el .retweet-info .avatar.still-image{margin-left:20px\n}\n.status{max-width:100%\n}\n.status .avatar.still-image{width:40px;height:40px\n}\n.status .avatar.still-image.avatar-compact{width:32px;height:32px\n}\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/status/status.vue","\n.attachments{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap\n}\n.attachments .attachment.media-upload-container{-ms-flex:0 0 auto;flex:0 0 auto;max-height:200px;max-width:100%;display:-ms-flexbox;display:flex\n}\n.attachments .attachment.media-upload-container video{max-width:100%\n}\n.attachments .placeholder{margin-right:8px;margin-bottom:4px\n}\n.attachments .nsfw-placeholder{cursor:pointer\n}\n.attachments .nsfw-placeholder.loading{cursor:progress\n}\n.attachments .attachment{position:relative;margin-top:0.5em;-ms-flex-item-align:start;align-self:flex-start;line-height:0;border-style:solid;border-width:1px;border-radius:10px;border-radius:var(--attachmentRadius, 10px);border-color:#222;border-color:var(--border, #222);overflow:hidden\n}\n.attachments .non-gallery.attachment.video{-ms-flex:1 0 40%;flex:1 0 40%\n}\n.attachments .non-gallery.attachment .nsfw{height:260px\n}\n.attachments .non-gallery.attachment .small{height:120px;-ms-flex-positive:0;flex-grow:0\n}\n.attachments .non-gallery.attachment .video{height:260px;display:-ms-flexbox;display:flex\n}\n.attachments .non-gallery.attachment video{max-height:100%;object-fit:contain\n}\n.attachments .fullwidth{-ms-flex-preferred-size:100%;flex-basis:100%\n}\n.attachments.video{line-height:0\n}\n.attachments .video-container{display:-ms-flexbox;display:flex;max-height:100%\n}\n.attachments .video{width:100%\n}\n.attachments .play-icon{position:absolute;font-size:64px;top:calc(50% - 32px);left:calc(50% - 32px);color:rgba(255,255,255,0.75);text-shadow:0 0 2px rgba(0,0,0,0.4)\n}\n.attachments .play-icon::before{margin:0\n}\n.attachments.html{-ms-flex-preferred-size:90%;flex-basis:90%;width:100%;display:-ms-flexbox;display:flex\n}\n.attachments .hider{position:absolute;right:0;white-space:nowrap;margin:10px;padding:5px;background:rgba(230,230,230,0.6);font-weight:bold;z-index:4;line-height:1;border-radius:5px;border-radius:var(--tooltipRadius, 5px)\n}\n.attachments video{z-index:0\n}\n.attachments audio{width:100%\n}\n.attachments img.media-upload{line-height:0;max-height:200px;max-width:100%\n}\n.attachments .oembed{line-height:1.2em;-ms-flex:1 0 100%;flex:1 0 100%;width:100%;margin-right:15px;display:-ms-flexbox;display:flex\n}\n.attachments .oembed img{width:100%\n}\n.attachments .oembed .image{-ms-flex:1;flex:1\n}\n.attachments .oembed .image img{border:0px;border-radius:5px;height:100%;object-fit:cover\n}\n.attachments .oembed .text{-ms-flex:2;flex:2;margin:8px;word-break:break-all\n}\n.attachments .oembed .text h1{font-size:14px;margin:0px\n}\n.attachments .image-attachment{width:100%;height:100%\n}\n.attachments .image-attachment.hidden{display:none\n}\n.attachments .image-attachment .nsfw{object-fit:cover;width:100%;height:100%\n}\n.attachments .image-attachment img{image-orientation:from-image\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/attachment/attachment.vue","\n.still-image{position:relative;line-height:0;overflow:hidden;width:100%;height:100%\n}\n.still-image:hover canvas{display:none\n}\n.still-image img{width:100%;height:100%;object-fit:contain\n}\n.still-image.animated:hover::before,.still-image.animated img{visibility:hidden\n}\n.still-image.animated:hover img{visibility:visible\n}\n.still-image.animated::before{content:'gif';position:absolute;line-height:10px;font-size:10px;top:5px;left:5px;background:rgba(127,127,127,0.5);color:#FFF;display:block;padding:2px 4px;border-radius:5px;border-radius:var(--tooltipRadius, 5px);z-index:2\n}\n.still-image canvas{position:absolute;top:0;bottom:0;left:0;right:0;width:100%;height:100%;object-fit:contain\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/still-image/still-image.vue","\n.fav-active{cursor:pointer;animation-duration:0.6s\n}\n.fav-active:hover{color:orange;color:var(--cOrange, orange)\n}\n.favorite-button.icon-star{color:orange;color:var(--cOrange, orange)\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/favorite_button/favorite_button.vue","\n.rt-active{cursor:pointer;animation-duration:0.6s\n}\n.rt-active:hover{color:#0fa00f;color:var(--cGreen, #0fa00f)\n}\n.icon-retweet.retweeted{color:#0fa00f;color:var(--cGreen, #0fa00f)\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/retweet_button/retweet_button.vue","\n.icon-cancel,.delete-status{cursor:pointer\n}\n.icon-cancel:hover,.delete-status:hover{color:red;color:var(--cRed, red)\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/delete_button/delete_button.vue","\n.tribute-container ul{padding:0px\n}\n.tribute-container ul li{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center\n}\n.tribute-container img{padding:3px;width:16px;height:16px;border-radius:10px;border-radius:var(--avatarAltRadius, 10px)\n}\n.post-status-form .visibility-tray{display:-ms-flexbox;display:flex;-ms-flex-pack:justify;justify-content:space-between;-ms-flex-direction:row-reverse;flex-direction:row-reverse\n}\n.post-status-form .form-bottom,.login .form-bottom{display:-ms-flexbox;display:flex;padding:0.5em;height:32px\n}\n.post-status-form .form-bottom button,.login .form-bottom button{width:10em\n}\n.post-status-form .form-bottom p,.login .form-bottom p{margin:0.35em;padding:0.35em;display:-ms-flexbox;display:flex\n}\n.post-status-form .error,.login .error{text-align:center\n}\n.post-status-form .media-upload-wrapper,.login .media-upload-wrapper{-ms-flex:0 0 auto;flex:0 0 auto;max-width:100%;min-width:50px;margin-right:.2em;margin-bottom:.5em\n}\n.post-status-form .media-upload-wrapper .icon-cancel,.login .media-upload-wrapper .icon-cancel{display:inline-block;position:static;margin:0;padding-bottom:0;margin-left:10px;margin-left:var(--attachmentRadius, 10px);background-color:#182230;background-color:var(--btn, #182230);border-bottom-left-radius:0;border-bottom-right-radius:0\n}\n.post-status-form .attachments,.login .attachments{padding:0 0.5em\n}\n.post-status-form .attachments .attachment,.login .attachments .attachment{margin:0;position:relative;-ms-flex:0 0 auto;flex:0 0 auto;border:1px solid #222;border:1px solid var(--border, #222);text-align:center\n}\n.post-status-form .attachments .attachment audio,.login .attachments .attachment audio{min-width:300px;-ms-flex:1 0 auto;flex:1 0 auto\n}\n.post-status-form .attachments .attachment a,.login .attachments .attachment a{display:block;text-align:left;line-height:1.2;padding:.5em\n}\n.post-status-form .attachments i,.login .attachments i{position:absolute;margin:10px;padding:5px;background:rgba(230,230,230,0.6);border-radius:10px;border-radius:var(--attachmentRadius, 10px);font-weight:bold\n}\n.post-status-form .btn,.login .btn{cursor:pointer\n}\n.post-status-form .btn[disabled],.login .btn[disabled]{cursor:not-allowed\n}\n.post-status-form form,.login form{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding:0.6em\n}\n.post-status-form .form-group,.login .form-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding:0.3em 0.5em 0.6em;line-height:24px\n}\n.post-status-form form textarea.form-cw,.login form textarea.form-cw{line-height:16px;resize:none;overflow:hidden;transition:min-height 200ms 100ms;min-height:1px\n}\n.post-status-form form textarea.form-control,.login form textarea.form-control{line-height:16px;resize:none;overflow:hidden;transition:min-height 200ms 100ms;min-height:1px;box-sizing:content-box\n}\n.post-status-form form textarea.form-control:focus,.login form textarea.form-control:focus{min-height:48px\n}\n.post-status-form .btn,.login .btn{cursor:pointer\n}\n.post-status-form .btn[disabled],.login .btn[disabled]{cursor:not-allowed\n}\n.post-status-form .icon-cancel,.login .icon-cancel{cursor:pointer;z-index:4\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/post_status_form/post_status_form.vue","\n.media-upload {\n font-size: 26px;\n -ms-flex: 1;\n flex: 1;\n}\n.icon-upload {\n cursor: pointer;\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/media_upload/media_upload.vue","\n.emoji-input .form-control{width:100%\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/emoji-input/emoji-input.vue","\n.user-card{background-size:cover;overflow:hidden\n}\n.user-card .panel-heading{padding:.5em 0;text-align:center;box-shadow:none;background:transparent;-ms-flex-direction:column;flex-direction:column;-ms-flex-align:stretch;align-items:stretch\n}\n.user-card .panel-body{word-wrap:break-word;background:linear-gradient(to bottom, transparent, #121a24 80%);background:linear-gradient(to bottom, transparent, var(--bg, #121a24) 80%)\n}\n.user-card p{margin-bottom:0\n}\n.user-card-bio{text-align:center\n}\n.user-card-bio img{object-fit:contain;vertical-align:middle;max-width:100%;max-height:400px\n}\n.user-card-bio img.emoji{width:32px;height:32px\n}\n.user-card-rounded-t{border-top-left-radius:10px;border-top-left-radius:var(--panelRadius, 10px);border-top-right-radius:10px;border-top-right-radius:var(--panelRadius, 10px)\n}\n.user-card-rounded{border-radius:10px;border-radius:var(--panelRadius, 10px)\n}\n.user-card-bordered{border-width:1px;border-style:solid;border-color:#222;border-color:var(--border, #222)\n}\n.user-info{color:#b9b9ba;color:var(--lightText, #b9b9ba);padding:0 26px\n}\n.user-info .container{padding:16px 0 6px;display:-ms-flexbox;display:flex;max-height:56px\n}\n.user-info .container .avatar{-ms-flex:1 0 100%;flex:1 0 100%;width:56px;height:56px;box-shadow:0px 1px 8px rgba(0,0,0,0.75);box-shadow:var(--avatarShadow);object-fit:cover\n}\n.user-info:hover .animated.avatar canvas{display:none\n}\n.user-info:hover .animated.avatar img{visibility:visible\n}\n.user-info .usersettings{color:#b9b9ba;color:var(--lightText, #b9b9ba);opacity:.8\n}\n.user-info .name-and-screen-name{display:block;margin-left:0.6em;text-align:left;text-overflow:ellipsis;white-space:nowrap;-ms-flex:1 1 0px;flex:1 1 0;z-index:1\n}\n.user-info .name-and-screen-name img{width:26px;height:26px;vertical-align:middle;object-fit:contain\n}\n.user-info .name-and-screen-name .top-line{display:-ms-flexbox;display:flex\n}\n.user-info .user-name{text-overflow:ellipsis;overflow:hidden;-ms-flex:1 1 auto;flex:1 1 auto;margin-right:1em;font-size:15px\n}\n.user-info .user-name img{object-fit:contain;height:16px;width:16px;vertical-align:middle\n}\n.user-info .user-screen-name{color:#b9b9ba;color:var(--lightText, #b9b9ba);display:inline-block;font-weight:light;font-size:15px;padding-right:0.1em;width:100%;display:-ms-flexbox;display:flex\n}\n.user-info .user-screen-name .dailyAvg{min-width:1px;-ms-flex:0 0 auto;flex:0 0 auto;margin-left:1em;font-size:0.7em;color:#b9b9ba;color:var(--text, #b9b9ba)\n}\n.user-info .user-screen-name .handle{min-width:1px;-ms-flex:0 1 auto;flex:0 1 auto;text-overflow:ellipsis;overflow:hidden\n}\n.user-info .user-screen-name .staff{text-transform:capitalize;color:#b9b9ba;color:var(--btnText, #b9b9ba);background-color:#182230;background-color:var(--btn, #182230)\n}\n.user-info .user-meta{margin-bottom:.15em;display:-ms-flexbox;display:flex;-ms-flex-align:baseline;align-items:baseline;font-size:14px;line-height:22px;-ms-flex-wrap:wrap;flex-wrap:wrap\n}\n.user-info .user-meta .following{-ms-flex:1 0 auto;flex:1 0 auto;margin:0;margin-bottom:.25em;text-align:left\n}\n.user-info .user-meta .highlighter{-ms-flex:0 1 auto;flex:0 1 auto;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-.5em;-ms-flex-item-align:start;align-self:start\n}\n.user-info .user-meta .highlighter .userHighlightCl{padding:2px 10px;-ms-flex:1 0 auto;flex:1 0 auto\n}\n.user-info .user-meta .highlighter .userHighlightSel,.user-info .user-meta .highlighter .userHighlightSel.select{padding-top:0;padding-bottom:0;-ms-flex:1 0 auto;flex:1 0 auto\n}\n.user-info .user-meta .highlighter .userHighlightSel.select i{line-height:22px\n}\n.user-info .user-meta .highlighter .userHighlightText{width:70px;-ms-flex:1 0 auto;flex:1 0 auto\n}\n.user-info .user-meta .highlighter .userHighlightCl,.user-info .user-meta .highlighter .userHighlightText,.user-info .user-meta .highlighter .userHighlightSel,.user-info .user-meta .highlighter .userHighlightSel.select{height:22px;vertical-align:top;margin-right:.5em;margin-bottom:.25em\n}\n.user-info .user-interactions{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-pack:justify;justify-content:space-between;margin-right:-.75em\n}\n.user-info .user-interactions div{-ms-flex:1 0 0px;flex:1 0 0;margin-right:.75em;margin-bottom:.6em;white-space:nowrap\n}\n.user-info .user-interactions .mute{max-width:220px;min-height:28px\n}\n.user-info .user-interactions .follow{max-width:220px;min-height:28px\n}\n.user-info .user-interactions button{width:100%;height:100%;margin:0\n}\n.user-info .user-interactions .remote-button{height:28px !important;width:92%\n}\n.user-info .user-interactions .pressed{border-bottom-color:rgba(255,255,255,0.2);border-top-color:rgba(0,0,0,0.2)\n}\n.user-counts{display:-ms-flexbox;display:flex;line-height:16px;padding:.5em 1.5em 0em 1.5em;text-align:center;-ms-flex-pack:justify;justify-content:space-between;color:#b9b9ba;color:var(--lightText, #b9b9ba);-ms-flex-wrap:wrap;flex-wrap:wrap\n}\n.user-count{-ms-flex:1 0 auto;flex:1 0 auto;padding:.5em 0 .5em 0;margin:0 .5em\n}\n.user-count h5{font-size:1em;font-weight:bolder;margin:0 0 0.25em\n}\n.user-count a{text-decoration:none\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/user_card/user_card.vue","\n.avatar.still-image{width:48px;height:48px;box-shadow:var(--avatarStatusShadow);border-radius:4px;border-radius:var(--avatarRadius, 4px)\n}\n.avatar.still-image img{width:100%;height:100%\n}\n.avatar.still-image.better-shadow{box-shadow:var(--avatarStatusShadowInset);filter:var(--avatarStatusShadowFilter)\n}\n.avatar.still-image.animated::before{display:none\n}\n.avatar.still-image.avatar-compact{width:32px;height:32px;border-radius:10px;border-radius:var(--avatarAltRadius, 10px)\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/user_avatar/user_avatar.vue","\n.remote-follow{max-width:220px\n}\n.remote-follow .remote-button{width:100%;min-height:28px\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/remote_follow/remote_follow.vue","\n.popper-wrapper{z-index:8\n}\n.popper-wrapper .popper__arrow{width:0;height:0;border-style:solid;position:absolute;margin:5px\n}\n.popper-wrapper[x-placement^=\"top\"]{margin-bottom:5px\n}\n.popper-wrapper[x-placement^=\"top\"] .popper__arrow{border-width:5px 5px 0 5px;border-color:#121a24 transparent transparent transparent;border-color:var(--bg, #121a24) transparent transparent transparent;bottom:-5px;left:calc(50% - 5px);margin-top:0;margin-bottom:0\n}\n.popper-wrapper[x-placement^=\"bottom\"]{margin-top:5px\n}\n.popper-wrapper[x-placement^=\"bottom\"] .popper__arrow{border-width:0 5px 5px 5px;border-color:transparent transparent #121a24 transparent;border-color:transparent transparent var(--bg, #121a24) transparent;top:-5px;left:calc(50% - 5px);margin-top:0;margin-bottom:0\n}\n.popper-wrapper[x-placement^=\"right\"]{margin-left:5px\n}\n.popper-wrapper[x-placement^=\"right\"] .popper__arrow{border-width:5px 5px 5px 0;border-color:transparent #121a24 transparent transparent;border-color:transparent var(--bg, #121a24) transparent transparent;left:-5px;top:calc(50% - 5px);margin-left:0;margin-right:0\n}\n.popper-wrapper[x-placement^=\"left\"]{margin-right:5px\n}\n.popper-wrapper[x-placement^=\"left\"] .popper__arrow{border-width:5px 0 5px 5px;border-color:transparent transparent transparent #121a24;border-color:transparent transparent transparent var(--bg, #121a24);right:-5px;top:calc(50% - 5px);margin-left:0;margin-right:0\n}\n.dropdown-menu{display:block;padding:.5rem 0;font-size:1rem;text-align:left;list-style:none;max-width:100vw;z-index:10;box-shadow:1px 1px 4px rgba(0,0,0,0.6);box-shadow:var(--panelShadow);border:none;border-radius:4px;border-radius:var(--btnRadius, 4px);background-color:#121a24;background-color:var(--bg, #121a24)\n}\n.dropdown-menu .dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #222;border-top:1px solid var(--border, #222)\n}\n.dropdown-menu .dropdown-item{line-height:21px;margin-right:5px;overflow:auto;display:block;padding:.25rem 1.0rem .25rem 1.5rem;clear:both;font-weight:400;text-align:inherit;white-space:normal;border:none;border-radius:0px;background-color:transparent;box-shadow:none;width:100%;height:100%\n}\n.dropdown-menu .dropdown-item:hover{background-color:#182230;background-color:var(--btn, #182230);box-shadow:none\n}\n.menu-checkbox{float:right;min-width:22px;max-width:22px;min-height:22px;max-height:22px;line-height:22px;text-align:center;border-radius:0px;background-color:#182230;background-color:var(--input, #182230);box-shadow:0px 0px 2px black inset;box-shadow:var(--inputShadow)\n}\n.menu-checkbox.menu-checkbox-checked::after{content:'✔'\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/moderation_tools/moderation_tools.vue","\n.dark-overlay::before{bottom:0;content:\" \";display:block;cursor:default;left:0;position:fixed;right:0;top:0;background:rgba(27,31,35,0.5);z-index:99\n}\n.dialog-modal.panel{top:0;left:50%;max-height:80vh;max-width:90vw;margin:15vh auto;position:fixed;transform:translateX(-50%);z-index:999;cursor:default;display:block;background-color:#121a24;background-color:var(--bg, #121a24)\n}\n.dialog-modal.panel .dialog-modal-heading{padding:.5em .5em;margin-right:auto;margin-bottom:0;white-space:nowrap;color:var(--panelText);background-color:#182230;background-color:var(--panel, #182230)\n}\n.dialog-modal.panel .dialog-modal-heading .title{margin-bottom:0\n}\n.dialog-modal.panel .dialog-modal-content{margin:0;padding:1rem 1rem;background-color:#151e2a;background-color:var(--lightBg, #151e2a);white-space:normal\n}\n.dialog-modal.panel .dialog-modal-footer{margin:0;padding:.5em .5em;background-color:#151e2a;background-color:var(--lightBg, #151e2a);border-top:1px solid #121a24;border-top:1px solid var(--bg, #121a24);-ms-flex-pack:end;justify-content:flex-end\n}\n.dialog-modal.panel .dialog-modal-footer button{width:auto;margin-left:.5rem\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/dialog_modal/dialog_modal.vue","\n.popper {\n width: auto;\n background-color: #fafafa;\n color: #212121;\n text-align: center;\n padding: 2px;\n display: inline-block;\n border-radius: 3px;\n position: absolute;\n font-size: 14px;\n font-weight: normal;\n border: 1px #ebebeb solid;\n z-index: 200000;\n box-shadow: rgb(58, 58, 58) 0 0 6px 0;\n}\n.popper .popper__arrow {\n width: 0;\n height: 0;\n border-style: solid;\n position: absolute;\n margin: 5px;\n}\n.popper[x-placement^=\"top\"] {\n margin-bottom: 5px;\n}\n.popper[x-placement^=\"top\"] .popper__arrow {\n border-width: 5px 5px 0 5px;\n border-color: #fafafa transparent transparent transparent;\n bottom: -5px;\n left: calc(50% - 5px);\n margin-top: 0;\n margin-bottom: 0;\n}\n.popper[x-placement^=\"bottom\"] {\n margin-top: 5px;\n}\n.popper[x-placement^=\"bottom\"] .popper__arrow {\n border-width: 0 5px 5px 5px;\n border-color: transparent transparent #fafafa transparent;\n top: -5px;\n left: calc(50% - 5px);\n margin-top: 0;\n margin-bottom: 0;\n}\n.popper[x-placement^=\"right\"] {\n margin-left: 5px;\n}\n.popper[x-placement^=\"right\"] .popper__arrow {\n border-width: 5px 5px 5px 0;\n border-color: transparent #fafafa transparent transparent;\n left: -5px;\n top: calc(50% - 5px);\n margin-left: 0;\n margin-right: 0;\n}\n.popper[x-placement^=\"left\"] {\n margin-right: 5px;\n}\n.popper[x-placement^=\"left\"] .popper__arrow {\n border-width: 5px 0 5px 5px;\n border-color: transparent transparent transparent #fafafa;\n right: -5px;\n top: calc(50% - 5px);\n margin-left: 0;\n margin-right: 0;\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///~/vue-popperjs/src/component/popper.js.vue","\n.gallery-row{height:200px;width:100%;display:-ms-flexbox;display:flex;-ms-flex-direction:row;flex-direction:row;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-ms-flex-line-pack:stretch;align-content:stretch;-ms-flex-positive:1;flex-grow:1;margin-top:0.5em\n}\n.gallery-row .attachments,.gallery-row .attachment{margin:0 0.5em 0 0;-ms-flex-positive:1;flex-grow:1;height:100%;box-sizing:border-box;min-width:2em\n}\n.gallery-row .attachments:last-child,.gallery-row .attachment:last-child{margin:0\n}\n.gallery-row .image-attachment{width:100%;height:100%\n}\n.gallery-row .video-container{height:100%\n}\n.gallery-row.contain-fit img,.gallery-row.contain-fit video{object-fit:contain\n}\n.gallery-row.cover-fit img,.gallery-row.cover-fit video{object-fit:cover\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/gallery/gallery.vue","\n.link-preview-card{display:-ms-flexbox;display:flex;-ms-flex-direction:row;flex-direction:row;cursor:pointer;overflow:hidden;margin-top:0.5em;color:#b9b9ba;color:var(--text, #b9b9ba);border-style:solid;border-width:1px;border-radius:10px;border-radius:var(--attachmentRadius, 10px);border-color:#222;border-color:var(--border, #222)\n}\n.link-preview-card .card-image{-ms-flex-negative:0;flex-shrink:0;width:120px;max-width:25%\n}\n.link-preview-card .card-image img{width:100%;height:100%;object-fit:cover;border-radius:10px;border-radius:var(--attachmentRadius, 10px)\n}\n.link-preview-card .small-image{width:80px\n}\n.link-preview-card .card-content{max-height:100%;margin:0.5em;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column\n}\n.link-preview-card .card-host{font-size:12px\n}\n.link-preview-card .card-description{margin:0.5em 0 0 0;overflow:hidden;text-overflow:ellipsis;word-break:break-word;line-height:1.2em;max-height:calc(1.2em * 3 - 1px)\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/link-preview/link-preview.vue","\n.timeline .panel-disabled .status-el{border-left:none;border-bottom-width:1px;border-bottom-style:solid;border-color:var(--border, #222);border-radius:0\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/conversation/conversation.vue","\n.user-profile{-ms-flex:2;flex:2;-ms-flex-preferred-size:500px;flex-basis:500px\n}\n.user-profile .userlist-placeholder{display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:middle;align-items:middle;padding:2em\n}\n.user-profile .timeline-heading{display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center\n}\n.user-profile .timeline-heading .loadmore-button,.user-profile .timeline-heading .alert{-ms-flex:1;flex:1\n}\n.user-profile .timeline-heading .loadmore-button{height:28px;margin:10px .6em\n}\n.user-profile .timeline-heading .title,.user-profile .timeline-heading .loadmore-text{display:none\n}\n.user-profile-placeholder .panel-body{display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:middle;align-items:middle;padding:7em\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/user_profile/user_profile.vue","\n.follow-card-content-container{-ms-flex-negative:0;flex-shrink:0;display:-ms-flexbox;display:flex;-ms-flex-direction:row;flex-direction:row;-ms-flex-pack:justify;justify-content:space-between;-ms-flex-wrap:wrap;flex-wrap:wrap;line-height:1.5em\n}\n.follow-card-follow-button{margin-top:0.5em;margin-left:auto;width:10em\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/follow_card/follow_card.vue","\n.basic-user-card{display:-ms-flexbox;display:flex;-ms-flex:1 0;flex:1 0;margin:0;padding:0.6em 1em\n}\n.basic-user-card-collapsed-content{margin-left:0.7em;text-align:left;-ms-flex:1;flex:1;min-width:0\n}\n.basic-user-card-user-name img{object-fit:contain;height:16px;width:16px;vertical-align:middle\n}\n.basic-user-card-user-name-value{display:inline-block;max-width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis\n}\n.basic-user-card-expanded-content{-ms-flex:1;flex:1;margin-left:0.7em\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/basic_user_card/basic_user_card.vue","\n.list-item:not(:last-child){border-bottom:1px solid;border-bottom-color:#222;border-bottom-color:var(--border, #222)\n}\n.list-empty-content{text-align:center;padding:10px\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/list/list.vue","\n@import '../../_variables.scss';\n\n.with-load-more {\n &-footer {\n padding: 10px;\n text-align: center;\n border-top: 1px solid;\n border-top-color: $fallback--border;\n border-top-color: var(--border, $fallback--border);\n\n .error {\n font-size: 14px;\n }\n }\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/hocs/with_load_more/src/hocs/with_load_more/with_load_more.scss","\n.setting-item{border-bottom:2px solid var(--fg, #182230);margin:1em 1em 1.4em;padding-bottom:1.4em\n}\n.setting-item>div{margin-bottom:.5em\n}\n.setting-item>div:last-child{margin-bottom:0\n}\n.setting-item:last-child{border-bottom:none;padding-bottom:0;margin-bottom:1em\n}\n.setting-item select{min-width:10em\n}\n.setting-item textarea{width:100%;max-width:100%;height:100px\n}\n.setting-item .unavailable,.setting-item .unavailable i{color:var(--cRed, red);color:red\n}\n.setting-item .btn{min-height:28px;min-width:10em;padding:0 2em\n}\n.setting-item .number-input{max-width:6em\n}\n.select-multiple{display:-ms-flexbox;display:flex\n}\n.select-multiple .option-list{margin:0;padding-left:.5em\n}\n.setting-list,.option-list{list-style-type:none;padding-left:2em\n}\n.setting-list li,.option-list li{margin-bottom:0.5em\n}\n.setting-list .suboptions,.option-list .suboptions{margin-top:0.3em\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/settings/settings.vue","@import '../../_variables.scss';\n\n.tab-switcher {\n .contents {\n .hidden {\n display: none;\n }\n }\n .tabs {\n display: flex;\n position: relative;\n width: 100%;\n overflow-y: hidden;\n overflow-x: auto;\n padding-top: 5px;\n box-sizing: border-box;\n\n &::after, &::before {\n display: block;\n content: '';\n flex: 1 1 auto;\n border-bottom: 1px solid;\n border-bottom-color: $fallback--border;\n border-bottom-color: var(--border, $fallback--border);\n }\n\n .tab-wrapper {\n height: 28px;\n position: relative;\n display: flex;\n flex: 0 0 auto;\n\n .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: 6px - 99px;\n white-space: nowrap;\n\n &:not(.active) {\n z-index: 4;\n\n &:hover {\n z-index: 6;\n }\n }\n\n &.active {\n background: transparent;\n z-index: 5;\n }\n }\n\n &:not(.active) {\n &::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: $fallback--border;\n border-bottom-color: var(--border, $fallback--border);\n }\n }\n }\n\n }\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/tab_switcher/src/components/tab_switcher/tab_switcher.scss","\n.style-switcher .preset-switcher{margin-right:1em\n}\n.style-switcher .style-control{display:-ms-flexbox;display:flex;-ms-flex-align:baseline;align-items:baseline;margin-bottom:5px\n}\n.style-switcher .style-control .label{-ms-flex:1;flex:1\n}\n.style-switcher .style-control.disabled input:not(.exclude-disabled),.style-switcher .style-control.disabled select:not(.exclude-disabled){opacity:.5\n}\n.style-switcher .style-control input,.style-switcher .style-control select{min-width:3em;margin:0;-ms-flex:0;flex:0\n}\n.style-switcher .style-control input[type=color],.style-switcher .style-control select[type=color]{padding:1px;cursor:pointer;height:29px;min-width:2em;border:none;-ms-flex-item-align:stretch;-ms-grid-row-align:stretch;align-self:stretch\n}\n.style-switcher .style-control input[type=number],.style-switcher .style-control select[type=number]{min-width:5em\n}\n.style-switcher .style-control input[type=range],.style-switcher .style-control select[type=range]{-ms-flex:1;flex:1;min-width:3em\n}\n.style-switcher .style-control input[type=checkbox]+label,.style-switcher .style-control select[type=checkbox]+label{margin:6px 0\n}\n.style-switcher .style-control input:not([type=number]):not([type=text]),.style-switcher .style-control select:not([type=number]):not([type=text]){-ms-flex-item-align:start;align-self:flex-start\n}\n.style-switcher .tab-switcher{margin:0 -1em\n}\n.style-switcher .reset-container{-ms-flex-wrap:wrap;flex-wrap:wrap\n}\n.style-switcher .fonts-container,.style-switcher .reset-container,.style-switcher .apply-container,.style-switcher .radius-container,.style-switcher .color-container{display:-ms-flexbox;display:flex\n}\n.style-switcher .fonts-container,.style-switcher .radius-container{-ms-flex-direction:column;flex-direction:column\n}\n.style-switcher .color-container{-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:justify;justify-content:space-between\n}\n.style-switcher .color-container>h4{width:99%\n}\n.style-switcher .fonts-container,.style-switcher .color-container,.style-switcher .shadow-container,.style-switcher .radius-container,.style-switcher .presets-container{margin:1em 1em 0\n}\n.style-switcher .tab-header{display:-ms-flexbox;display:flex;-ms-flex-pack:justify;justify-content:space-between;-ms-flex-align:baseline;align-items:baseline;width:100%;min-height:30px;margin-bottom:1em\n}\n.style-switcher .tab-header .btn{min-width:1px;-ms-flex:0 auto;flex:0 auto;padding:0 1em\n}\n.style-switcher .tab-header p{-ms-flex:1;flex:1;margin:0;margin-right:.5em\n}\n.style-switcher .shadow-selector .override{-ms-flex:1;flex:1;margin-left:.5em\n}\n.style-switcher .shadow-selector .select-container{margin-top:-4px;margin-bottom:-3px\n}\n.style-switcher .save-load,.style-switcher .save-load-options{display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:baseline;align-items:baseline;-ms-flex-wrap:wrap;flex-wrap:wrap\n}\n.style-switcher .save-load .presets,.style-switcher .save-load .import-export,.style-switcher .save-load-options .presets,.style-switcher .save-load-options .import-export{margin-bottom:.5em\n}\n.style-switcher .save-load .import-export,.style-switcher .save-load-options .import-export{display:-ms-flexbox;display:flex\n}\n.style-switcher .save-load .override,.style-switcher .save-load-options .override{margin-left:.5em\n}\n.style-switcher .save-load-options{-ms-flex-wrap:wrap;flex-wrap:wrap;margin-top:.5em;-ms-flex-pack:center;justify-content:center\n}\n.style-switcher .save-load-options .keep-option{margin:0 .5em .5em;min-width:25%\n}\n.style-switcher .preview-container{border-top:1px dashed;border-bottom:1px dashed;border-color:#222;border-color:var(--border, #222);margin:1em -1em 0;padding:1em;background:var(--body-background-image);background-size:cover;background-position:50% 50%\n}\n.style-switcher .preview-container .dummy .post{font-family:var(--postFont);display:-ms-flexbox;display:flex\n}\n.style-switcher .preview-container .dummy .post .content{-ms-flex:1;flex:1\n}\n.style-switcher .preview-container .dummy .post .content h4{margin-bottom:.25em\n}\n.style-switcher .preview-container .dummy .post .content .icons{margin-top:.5em;display:-ms-flexbox;display:flex\n}\n.style-switcher .preview-container .dummy .post .content .icons i{margin-right:1em\n}\n.style-switcher .preview-container .dummy .after-post{margin-top:1em;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center\n}\n.style-switcher .preview-container .dummy .avatar,.style-switcher .preview-container .dummy .avatar-alt{background:linear-gradient(135deg, #b8e1fc 0%, #a9d2f3 10%, #90bae4 25%, #90bcea 37%, #90bff0 50%, #6ba8e5 51%, #a2daf5 83%, #bdf3fd 100%);color:black;font-family:sans-serif;text-align:center;margin-right:1em\n}\n.style-switcher .preview-container .dummy .avatar-alt{-ms-flex:0 auto;flex:0 auto;margin-left:28px;font-size:12px;min-width:20px;min-height:20px;line-height:20px;border-radius:10px;border-radius:var(--avatarAltRadius, 10px)\n}\n.style-switcher .preview-container .dummy .avatar{-ms-flex:0 auto;flex:0 auto;width:48px;height:48px;font-size:14px;line-height:48px\n}\n.style-switcher .preview-container .dummy .actions{display:-ms-flexbox;display:flex;-ms-flex-align:baseline;align-items:baseline\n}\n.style-switcher .preview-container .dummy .actions .checkbox{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:baseline;align-items:baseline;margin-right:1em;-ms-flex:1;flex:1\n}\n.style-switcher .preview-container .dummy .separator{margin:1em;border-bottom:1px solid;border-color:#222;border-color:var(--border, #222)\n}\n.style-switcher .preview-container .dummy .panel-heading .badge,.style-switcher .preview-container .dummy .panel-heading .alert,.style-switcher .preview-container .dummy .panel-heading .btn,.style-switcher .preview-container .dummy .panel-heading .faint{margin-left:1em;white-space:nowrap\n}\n.style-switcher .preview-container .dummy .panel-heading .faint{text-overflow:ellipsis;min-width:2em;overflow-x:hidden\n}\n.style-switcher .preview-container .dummy .panel-heading .flex-spacer{-ms-flex:1;flex:1\n}\n.style-switcher .preview-container .dummy .btn{margin-left:0;padding:0 1em;min-width:3em;min-height:30px\n}\n.style-switcher .apply-container{-ms-flex-pack:center;justify-content:center\n}\n.style-switcher .radius-item,.style-switcher .color-item{min-width:20em;margin:5px 6px 0 0;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex:1 1 0px;flex:1 1 0\n}\n.style-switcher .radius-item.wide,.style-switcher .color-item.wide{min-width:60%\n}\n.style-switcher .radius-item:not(.wide):nth-child(2n+1),.style-switcher .color-item:not(.wide):nth-child(2n+1){margin-right:7px\n}\n.style-switcher .radius-item .color,.style-switcher .radius-item .opacity,.style-switcher .color-item .color,.style-switcher .color-item .opacity{display:-ms-flexbox;display:flex;-ms-flex-align:baseline;align-items:baseline\n}\n.style-switcher .radius-item{-ms-flex-preferred-size:auto;flex-basis:auto\n}\n.style-switcher .theme-radius-rn,.style-switcher .theme-color-cl{border:0;box-shadow:none;background:transparent;color:var(--faint, rgba(185,185,186,0.5));-ms-flex-item-align:stretch;-ms-grid-row-align:stretch;align-self:stretch\n}\n.style-switcher .theme-color-cl,.style-switcher .theme-radius-in,.style-switcher .theme-color-in{margin-left:4px\n}\n.style-switcher .theme-radius-in{min-width:1em\n}\n.style-switcher .theme-radius-in{max-width:7em;-ms-flex:1;flex:1\n}\n.style-switcher .theme-radius-lb{max-width:50em\n}\n.style-switcher .theme-preview-content{padding:20px\n}\n.style-switcher .btn{margin-left:.25em;margin-right:.25em\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/style_switcher/style_switcher.scss","\n.color-control input.text-input{max-width:7em;-ms-flex:1;flex:1\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/color_input/color_input.vue","\n.shadow-control{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:center;justify-content:center;margin-bottom:1em\n}\n.shadow-control .shadow-preview-container,.shadow-control .shadow-tweak{margin:5px 6px 0 0\n}\n.shadow-control .shadow-preview-container{-ms-flex:0;flex:0;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap\n}\n.shadow-control .shadow-preview-container input[type=number]{width:5em;min-width:2em\n}\n.shadow-control .shadow-preview-container .x-shift-control,.shadow-control .shadow-preview-container .y-shift-control{display:-ms-flexbox;display:flex;-ms-flex:0;flex:0\n}\n.shadow-control .shadow-preview-container .x-shift-control[disabled=disabled] *,.shadow-control .shadow-preview-container .y-shift-control[disabled=disabled] *{opacity:.5\n}\n.shadow-control .shadow-preview-container .x-shift-control{-ms-flex-align:start;align-items:flex-start\n}\n.shadow-control .shadow-preview-container .x-shift-control .wrap,.shadow-control .shadow-preview-container input[type=range]{margin:0;width:15em;height:2em\n}\n.shadow-control .shadow-preview-container .y-shift-control{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:end;align-items:flex-end\n}\n.shadow-control .shadow-preview-container .y-shift-control .wrap{width:2em;height:15em\n}\n.shadow-control .shadow-preview-container .y-shift-control input[type=range]{transform-origin:1em 1em;transform:rotate(90deg)\n}\n.shadow-control .shadow-preview-container .preview-window{-ms-flex:1;flex:1;background-color:#999999;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;background-image:linear-gradient(45deg, #666 25%, transparent 25%),linear-gradient(-45deg, #666 25%, transparent 25%),linear-gradient(45deg, transparent 75%, #666 75%),linear-gradient(-45deg, transparent 75%, #666 75%);background-size:20px 20px;background-position:0 0, 0 10px, 10px -10px, -10px 0;border-radius:4px;border-radius:var(--inputRadius, 4px)\n}\n.shadow-control .shadow-preview-container .preview-window .preview-block{width:33%;height:33%;background-color:#121a24;background-color:var(--bg, #121a24);border-radius:10px;border-radius:var(--panelRadius, 10px)\n}\n.shadow-control .shadow-tweak{-ms-flex:1;flex:1;min-width:280px\n}\n.shadow-control .shadow-tweak .id-control{-ms-flex-align:stretch;align-items:stretch\n}\n.shadow-control .shadow-tweak .id-control .select,.shadow-control .shadow-tweak .id-control .btn{min-width:1px;margin-right:5px\n}\n.shadow-control .shadow-tweak .id-control .btn{padding:0 .4em;margin:0 .1em\n}\n.shadow-control .shadow-tweak .id-control .select{-ms-flex:1;flex:1\n}\n.shadow-control .shadow-tweak .id-control .select select{-ms-flex-item-align:initial;-ms-grid-row-align:initial;align-self:initial\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/shadow_control/shadow_control.vue","\n.font-control input.custom-font{min-width:10em\n}\n.font-control.custom .select{border-top-right-radius:0;border-bottom-right-radius:0\n}\n.font-control.custom .custom-font{border-top-left-radius:0;border-bottom-left-radius:0\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/font_control/font_control.vue","\n.contrast-ratio{display:-ms-flexbox;display:flex;-ms-flex-pack:end;justify-content:flex-end;margin-top:-4px;margin-bottom:5px\n}\n.contrast-ratio .label{margin-right:1em\n}\n.contrast-ratio .rating{display:inline-block;text-align:center\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/contrast_ratio/contrast_ratio.vue","\n.import-export-container{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:baseline;align-items:baseline;-ms-flex-pack:center;justify-content:center\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/export_import/export_import.vue","\n.registration-form{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;margin:0.6em\n}\n.registration-form .container{display:-ms-flexbox;display:flex;-ms-flex-direction:row;flex-direction:row\n}\n.registration-form .terms-of-service{-ms-flex:0 1 50%;flex:0 1 50%;margin:0.8em\n}\n.registration-form .text-fields{margin-top:0.6em;-ms-flex:1 0;flex:1 0;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column\n}\n.registration-form textarea{min-height:100px\n}\n.registration-form .form-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding:0.3em 0.0em 0.3em;line-height:24px;margin-bottom:1em\n}\n.registration-form .form-group--error{animation-name:shakeError;animation-duration:.6s;animation-timing-function:ease-in-out\n}\n.registration-form .form-group--error .form--label{color:#f04124;color:var(--cRed, #f04124)\n}\n.registration-form .form-error{margin-top:-0.7em;text-align:left\n}\n.registration-form .form-error span{font-size:12px\n}\n.registration-form .form-error ul{list-style:none;padding:0 0 0 5px;margin-top:0\n}\n.registration-form .form-error ul li::before{content:\"• \"\n}\n.registration-form form textarea{line-height:16px;resize:vertical\n}\n.registration-form .captcha{max-width:350px;margin-bottom:0.4em\n}\n.registration-form .btn{margin-top:0.6em;height:28px\n}\n.registration-form .error{text-align:center\n}\n@media all and (max-width: 800px){\n.registration-form .container{-ms-flex-direction:column-reverse;flex-direction:column-reverse\n}\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/registration/registration.vue","\n.profile-edit .bio{margin:0\n}\n.profile-edit input[type=file]{padding:5px;height:auto\n}\n.profile-edit .banner{max-width:100%\n}\n.profile-edit .uploading{font-size:1.5em;margin:0.25em\n}\n.profile-edit .name-changer{width:100%\n}\n.profile-edit .bg{max-width:100%\n}\n.profile-edit .current-avatar{display:block;width:150px;height:150px;border-radius:4px;border-radius:var(--avatarRadius, 4px)\n}\n.profile-edit .oauth-tokens{width:100%\n}\n.profile-edit .oauth-tokens th{text-align:left\n}\n.profile-edit .oauth-tokens .actions{text-align:right\n}\n.profile-edit-usersearch-wrapper{padding:1em\n}\n.profile-edit-bulk-actions{text-align:right;padding:0 1em;min-height:28px\n}\n.profile-edit-bulk-actions button{width:10em\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/user_settings/user_settings.vue","\n.image-cropper-img-input{display:none\n}\n.image-cropper-image-container{position:relative\n}\n.image-cropper-image-container img{display:block;max-width:100%\n}\n.image-cropper-buttons-wrapper{margin-top:10px\n}\n.image-cropper-buttons-wrapper button{margin-top:5px\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/image_cropper/image_cropper.vue","/*!\n * Cropper.js v1.4.3\n * https://fengyuanchen.github.io/cropperjs\n *\n * Copyright 2015-present Chen Fengyuan\n * Released under the MIT license\n *\n * Date: 2018-10-24T13:07:11.429Z\n */\n\n.cropper-container {\n direction: ltr;\n font-size: 0;\n line-height: 0;\n position: relative;\n -ms-touch-action: none;\n touch-action: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n\n.cropper-container img {\n display: block;\n height: 100%;\n image-orientation: 0deg;\n max-height: none !important;\n max-width: none !important;\n min-height: 0 !important;\n min-width: 0 !important;\n width: 100%;\n}\n\n.cropper-wrap-box,\n.cropper-canvas,\n.cropper-drag-box,\n.cropper-crop-box,\n.cropper-modal {\n bottom: 0;\n left: 0;\n position: absolute;\n right: 0;\n top: 0;\n}\n\n.cropper-wrap-box,\n.cropper-canvas {\n overflow: hidden;\n}\n\n.cropper-drag-box {\n background-color: #fff;\n opacity: 0;\n}\n\n.cropper-modal {\n background-color: #000;\n opacity: .5;\n}\n\n.cropper-view-box {\n display: block;\n height: 100%;\n outline-color: rgba(51, 153, 255, 0.75);\n outline: 1px solid #39f;\n overflow: hidden;\n width: 100%;\n}\n\n.cropper-dashed {\n border: 0 dashed #eee;\n display: block;\n opacity: .5;\n position: absolute;\n}\n\n.cropper-dashed.dashed-h {\n border-bottom-width: 1px;\n border-top-width: 1px;\n height: calc(100% / 3);\n left: 0;\n top: calc(100% / 3);\n width: 100%;\n}\n\n.cropper-dashed.dashed-v {\n border-left-width: 1px;\n border-right-width: 1px;\n height: 100%;\n left: calc(100% / 3);\n top: 0;\n width: calc(100% / 3);\n}\n\n.cropper-center {\n display: block;\n height: 0;\n left: 50%;\n opacity: .75;\n position: absolute;\n top: 50%;\n width: 0;\n}\n\n.cropper-center:before,\n.cropper-center:after {\n background-color: #eee;\n content: ' ';\n display: block;\n position: absolute;\n}\n\n.cropper-center:before {\n height: 1px;\n left: -3px;\n top: 0;\n width: 7px;\n}\n\n.cropper-center:after {\n height: 7px;\n left: 0;\n top: -3px;\n width: 1px;\n}\n\n.cropper-face,\n.cropper-line,\n.cropper-point {\n display: block;\n height: 100%;\n opacity: .1;\n position: absolute;\n width: 100%;\n}\n\n.cropper-face {\n background-color: #fff;\n left: 0;\n top: 0;\n}\n\n.cropper-line {\n background-color: #39f;\n}\n\n.cropper-line.line-e {\n cursor: ew-resize;\n right: -3px;\n top: 0;\n width: 5px;\n}\n\n.cropper-line.line-n {\n cursor: ns-resize;\n height: 5px;\n left: 0;\n top: -3px;\n}\n\n.cropper-line.line-w {\n cursor: ew-resize;\n left: -3px;\n top: 0;\n width: 5px;\n}\n\n.cropper-line.line-s {\n bottom: -3px;\n cursor: ns-resize;\n height: 5px;\n left: 0;\n}\n\n.cropper-point {\n background-color: #39f;\n height: 5px;\n opacity: .75;\n width: 5px;\n}\n\n.cropper-point.point-e {\n cursor: ew-resize;\n margin-top: -3px;\n right: -3px;\n top: 50%;\n}\n\n.cropper-point.point-n {\n cursor: ns-resize;\n left: 50%;\n margin-left: -3px;\n top: -3px;\n}\n\n.cropper-point.point-w {\n cursor: ew-resize;\n left: -3px;\n margin-top: -3px;\n top: 50%;\n}\n\n.cropper-point.point-s {\n bottom: -3px;\n cursor: s-resize;\n left: 50%;\n margin-left: -3px;\n}\n\n.cropper-point.point-ne {\n cursor: nesw-resize;\n right: -3px;\n top: -3px;\n}\n\n.cropper-point.point-nw {\n cursor: nwse-resize;\n left: -3px;\n top: -3px;\n}\n\n.cropper-point.point-sw {\n bottom: -3px;\n cursor: nesw-resize;\n left: -3px;\n}\n\n.cropper-point.point-se {\n bottom: -3px;\n cursor: nwse-resize;\n height: 20px;\n opacity: 1;\n right: -3px;\n width: 20px;\n}\n\n@media (min-width: 768px) {\n .cropper-point.point-se {\n height: 15px;\n width: 15px;\n }\n}\n\n@media (min-width: 992px) {\n .cropper-point.point-se {\n height: 10px;\n width: 10px;\n }\n}\n\n@media (min-width: 1200px) {\n .cropper-point.point-se {\n height: 5px;\n opacity: .75;\n width: 5px;\n }\n}\n\n.cropper-point.point-se:before {\n background-color: #39f;\n bottom: -50%;\n content: ' ';\n display: block;\n height: 200%;\n opacity: 0;\n position: absolute;\n right: -50%;\n width: 200%;\n}\n\n.cropper-invisible {\n opacity: 0;\n}\n\n.cropper-bg {\n background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC');\n}\n\n.cropper-hide {\n display: block;\n height: 0;\n position: absolute;\n width: 0;\n}\n\n.cropper-hidden {\n display: none !important;\n}\n\n.cropper-move {\n cursor: move;\n}\n\n.cropper-crop {\n cursor: crosshair;\n}\n\n.cropper-disabled .cropper-drag-box,\n.cropper-disabled .cropper-face,\n.cropper-disabled .cropper-line,\n.cropper-disabled .cropper-point {\n cursor: not-allowed;\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///~/cropperjs/dist/cropper.css","\n.block-card-content-container{margin-top:0.5em;text-align:right\n}\n.block-card-content-container button{width:10em\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/block_card/block_card.vue","\n.mute-card-content-container{margin-top:0.5em;text-align:right\n}\n.mute-card-content-container button{width:10em\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/mute_card/mute_card.vue","\n.selectable-list-item-inner{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center\n}\n.selectable-list-item-selected-inner{background-color:#151e2a;background-color:var(--lightBg, #151e2a)\n}\n.selectable-list-header{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:0.6em 0;border-bottom:2px solid;border-bottom-color:#222;border-bottom-color:var(--border, #222)\n}\n.selectable-list-header-actions{-ms-flex:1;flex:1\n}\n.selectable-list-checkbox-wrapper{padding:0 10px;-ms-flex:none;flex:none\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/selectable_list/selectable_list.vue","\n.checkbox{position:relative;display:inline-block;padding-left:1.2em;min-height:1.2em\n}\n.checkbox-indicator::before{position:absolute;left:0;top:0;display:block;content:'✔';transition:color 200ms;width:1.1em;height:1.1em;border-radius:2px;border-radius:var(--checkboxRadius, 2px);box-shadow:0px 0px 2px black inset;box-shadow:var(--inputShadow);background-color:#182230;background-color:var(--input, #182230);vertical-align:top;text-align:center;line-height:1.1em;font-size:1.1em;color:transparent;overflow:hidden;box-sizing:border-box\n}\n.checkbox input[type=checkbox]{display:none\n}\n.checkbox input[type=checkbox]:checked+.checkbox-indicator::before{color:#b9b9ba;color:var(--text, #b9b9ba)\n}\n.checkbox input[type=checkbox]:indeterminate+.checkbox-indicator::before{content:'–';color:#b9b9ba;color:var(--text, #b9b9ba)\n}\n.checkbox input[type=checkbox]:disabled+.checkbox-indicator::before{opacity:.5\n}\n.checkbox>span{margin-left:.5em\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/checkbox/checkbox.vue","\n.autosuggest{position:relative\n}\n.autosuggest-input{display:block;width:100%\n}\n.autosuggest-results{position:absolute;left:0;top:100%;right:0;max-height:400px;background-color:#151e2a;background-color:var(--lightBg, #151e2a);border-style:solid;border-width:1px;border-color:#222;border-color:var(--border, #222);border-radius:4px;border-radius:var(--inputRadius, 4px);border-top-left-radius:0;border-top-right-radius:0;box-shadow:1px 1px 4px rgba(0,0,0,0.6);box-shadow:var(--panelShadow);overflow-y:auto;z-index:1\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/autosuggest/autosuggest.vue",".with-subscription {\n &-loading {\n padding: 10px;\n text-align: center;\n\n .error {\n font-size: 14px;\n }\n }\n}\n\n\n// WEBPACK FOOTER //\n// webpack:///src/hocs/with_subscription/src/hocs/with_subscription/with_subscription.scss","\n.follow-request-card-content-container{display:-ms-flexbox;display:flex;-ms-flex-direction:row;flex-direction:row;-ms-flex-wrap:wrap;flex-wrap:wrap\n}\n.follow-request-card-content-container button{margin-top:0.5em;margin-right:0.5em;-ms-flex:1 1;flex:1 1;max-width:12em;min-width:8em\n}\n.follow-request-card-content-container button:last-child{margin-right:0\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/follow_request_card/follow_request_card.vue","\n.user-search-input-container{margin:0.5em;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center\n}\n.user-search-input-container .search-button{margin-left:0.5em\n}\n.loading-icon{padding:1em\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/user_search/user_search.vue","\n.notifications{padding-bottom:15em\n}\n.notifications .loadmore-error{color:#b9b9ba;color:var(--text, #b9b9ba)\n}\n.notifications .notification{position:relative\n}\n.notifications .notification .notification-overlay{position:absolute;top:0;right:0;left:0;bottom:0;pointer-events:none\n}\n.notifications .notification.unseen .notification-overlay{background-image:linear-gradient(135deg, var(--badgeNotification, red) 4px, transparent 10px)\n}\n.notification{box-sizing:border-box;display:-ms-flexbox;display:flex;border-bottom:1px solid;border-color:#222;border-color:var(--border, #222)\n}\n.notification:hover .animated.avatar canvas{display:none\n}\n.notification:hover .animated.avatar img{visibility:visible\n}\n.notification .non-mention{display:-ms-flexbox;display:flex;-ms-flex:1;flex:1;-ms-flex-wrap:nowrap;flex-wrap:nowrap;padding:0.6em;min-width:0\n}\n.notification .non-mention .avatar-container{width:32px;height:32px\n}\n.notification .non-mention .status-el{padding:0\n}\n.notification .non-mention .status-el .status{padding:0.25em 0;color:rgba(185,185,186,0.5);color:var(--faint, rgba(185,185,186,0.5))\n}\n.notification .non-mention .status-el .status a{color:var(--faintLink)\n}\n.notification .non-mention .status-el .media-body{margin:0\n}\n.notification .follow-text{padding:0.5em 0\n}\n.notification .status-el{-ms-flex:1;flex:1\n}\n.notification time{white-space:nowrap\n}\n.notification .notification-right{-ms-flex:1;flex:1;padding-left:0.8em;min-width:0\n}\n.notification .notification-details{min-width:0px;word-wrap:break-word;line-height:18px;position:relative;overflow:hidden;width:100%;-ms-flex:1 1 0px;flex:1 1 0;display:-ms-flexbox;display:flex;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-ms-flex-pack:justify;justify-content:space-between\n}\n.notification .notification-details .name-and-action{-ms-flex:1;flex:1;overflow:hidden;text-overflow:ellipsis\n}\n.notification .notification-details .username{font-weight:bolder;max-width:100%;text-overflow:ellipsis;white-space:nowrap\n}\n.notification .notification-details .username img{width:14px;height:14px;vertical-align:middle;object-fit:contain\n}\n.notification .notification-details .timeago{margin-right:.2em\n}\n.notification .notification-details .icon-retweet.lit{color:#0fa00f;color:var(--cGreen, #0fa00f)\n}\n.notification .notification-details .icon-user-plus.lit{color:#0095ff;color:var(--cBlue, #0095ff)\n}\n.notification .notification-details .icon-reply.lit{color:#0095ff;color:var(--cBlue, #0095ff)\n}\n.notification .notification-details .icon-star.lit{color:orange;color:orange;color:var(--cOrange, orange)\n}\n.notification .notification-details .status-content{margin:0;max-height:300px\n}\n.notification .notification-details h1{word-break:break-all;margin:0 0 0.3em;padding:0;font-size:1em;line-height:20px\n}\n.notification .notification-details h1 small{font-weight:lighter\n}\n.notification .notification-details p{margin:0;margin-top:0;margin-bottom:0.3em\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/notifications/notifications.scss","\n.login-form .btn{min-height:28px;width:10em\n}\n.login-form .register{-ms-flex:1 1;flex:1 1\n}\n.login-form .login-bottom{margin-top:1.0em;display:-ms-flexbox;display:flex;-ms-flex-direction:row;flex-direction:row;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between\n}\n.login .error{text-align:center;animation-name:shakeError;animation-duration:0.4s;animation-timing-function:ease-in-out\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/login_form/login_form.vue","\n.floating-chat{position:fixed;right:0px;bottom:0px;z-index:1000;max-width:25em\n}\n.chat-heading{cursor:pointer\n}\n.chat-heading .icon-comment-empty{color:#b9b9ba;color:var(--text, #b9b9ba)\n}\n.chat-window{overflow-y:auto;overflow-x:hidden;max-height:20em\n}\n.chat-window-container{height:100%\n}\n.chat-message{display:-ms-flexbox;display:flex;padding:0.2em 0.5em\n}\n.chat-avatar img{height:24px;width:24px;border-radius:4px;border-radius:var(--avatarRadius, 4px);margin-right:0.5em;margin-top:0.25em\n}\n.chat-input{display:-ms-flexbox;display:flex\n}\n.chat-input textarea{-ms-flex:1;flex:1;margin:0.6em;min-height:3.5em;resize:none\n}\n.chat-panel .title{display:-ms-flexbox;display:flex;-ms-flex-pack:justify;justify-content:space-between\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/chat_panel/chat_panel.vue","\n.features-panel li{line-height:24px\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/features_panel/features_panel.vue","\n.tos-content{margin:1em\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/terms_of_service_panel/terms_of_service_panel.vue","\n#app{min-height:100vh;max-width:100%;overflow:hidden\n}\n.app-bg-wrapper{position:fixed;z-index:-1;height:100%;width:100%;background-size:cover;background-repeat:no-repeat;background-position:0 50%\n}\ni{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none\n}\nh4{margin:0\n}\n#content{box-sizing:border-box;padding-top:60px;margin:auto;min-height:100vh;max-width:980px;background-color:rgba(0,0,0,0.15);-ms-flex-line-pack:start;align-content:flex-start\n}\n.text-center{text-align:center\n}\nbody{font-family:sans-serif;font-family:var(--interfaceFont, sans-serif);font-size:14px;margin:0;color:#b9b9ba;color:var(--text, #b9b9ba);max-width:100vw;overflow-x:hidden\n}\na{text-decoration:none;color:#d8a070;color:var(--link, #d8a070)\n}\nbutton{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;color:#b9b9ba;color:var(--btnText, #b9b9ba);background-color:#182230;background-color:var(--btn, #182230);border:none;border-radius:4px;border-radius:var(--btnRadius, 4px);cursor:pointer;box-shadow:0px 0px 2px 0px #000,0px 1px 0px 0px rgba(255,255,255,0.2) inset,0px -1px 0px 0px rgba(0,0,0,0.2) inset;box-shadow:var(--buttonShadow);font-size:14px;font-family:sans-serif;font-family:var(--interfaceFont, sans-serif)\n}\nbutton i[class*=icon-]{color:#b9b9ba;color:var(--btnText, #b9b9ba)\n}\nbutton::-moz-focus-inner{border:none\n}\nbutton:hover{box-shadow:0px 0px 4px rgba(255,255,255,0.3);box-shadow:var(--buttonHoverShadow)\n}\nbutton:active{box-shadow:0px 0px 4px 0px rgba(255,255,255,0.3),0px 1px 0px 0px rgba(0,0,0,0.2) inset,0px -1px 0px 0px rgba(255,255,255,0.2) inset;box-shadow:var(--buttonPressedShadow)\n}\nbutton:disabled{cursor:not-allowed;opacity:0.5\n}\nbutton.pressed{color:rgba(185,185,186,0.5);color:var(--faint, rgba(185,185,186,0.5));background-color:#121a24;background-color:var(--bg, #121a24)\n}\nbutton.danger{color:#b9b9ba;color:var(--alertErrorPanelText, #b9b9ba);background-color:rgba(211,16,20,0.5);background-color:var(--alertError, rgba(211,16,20,0.5))\n}\nlabel.select{padding:0\n}\ninput,textarea,.select{border:none;border-radius:4px;border-radius:var(--inputRadius, 4px);box-shadow:0px 1px 0px 0px rgba(0,0,0,0.2) inset,0px -1px 0px 0px rgba(255,255,255,0.2) inset,0px 0px 2px 0px #000 inset;box-shadow:var(--inputShadow);background-color:#182230;background-color:var(--input, #182230);color:#b9b9ba;color:var(--inputText, #b9b9ba);font-family:sans-serif;font-family:var(--inputFont, sans-serif);font-size:14px;padding:8px .5em;box-sizing:border-box;display:inline-block;position:relative;height:28px;line-height:16px;-webkit-hyphens:none;-ms-hyphens:none;hyphens:none\n}\ninput:disabled,input[disabled=disabled],textarea:disabled,textarea[disabled=disabled],.select:disabled,.select[disabled=disabled]{cursor:not-allowed;opacity:0.5\n}\ninput .icon-down-open,textarea .icon-down-open,.select .icon-down-open{position:absolute;top:0;bottom:0;right:5px;height:100%;color:#b9b9ba;color:var(--text, #b9b9ba);line-height:28px;z-index:0;pointer-events:none\n}\ninput select,textarea select,.select select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;border:none;color:#b9b9ba;color:var(--inputText, --text, #b9b9ba);margin:0;padding:0 2em 0 .2em;font-family:sans-serif;font-family:var(--inputFont, sans-serif);font-size:14px;width:100%;z-index:1;height:28px;line-height:16px\n}\ninput[type=range],textarea[type=range],.select[type=range]{background:none;border:none;margin:0;box-shadow:none;-ms-flex:1;flex:1\n}\ninput[type=radio],input[type=checkbox],textarea[type=radio],textarea[type=checkbox],.select[type=radio],.select[type=checkbox]{display:none\n}\ninput[type=radio]:checked+label::before,input[type=checkbox]:checked+label::before,textarea[type=radio]:checked+label::before,textarea[type=checkbox]:checked+label::before,.select[type=radio]:checked+label::before,.select[type=checkbox]:checked+label::before{color:#b9b9ba;color:var(--text, #b9b9ba)\n}\ninput[type=radio]:disabled,input[type=radio]:disabled+label,input[type=radio]:disabled+label::before,input[type=checkbox]:disabled,input[type=checkbox]:disabled+label,input[type=checkbox]:disabled+label::before,textarea[type=radio]:disabled,textarea[type=radio]:disabled+label,textarea[type=radio]:disabled+label::before,textarea[type=checkbox]:disabled,textarea[type=checkbox]:disabled+label,textarea[type=checkbox]:disabled+label::before,.select[type=radio]:disabled,.select[type=radio]:disabled+label,.select[type=radio]:disabled+label::before,.select[type=checkbox]:disabled,.select[type=checkbox]:disabled+label,.select[type=checkbox]:disabled+label::before{opacity:.5\n}\ninput[type=radio]+label::before,input[type=checkbox]+label::before,textarea[type=radio]+label::before,textarea[type=checkbox]+label::before,.select[type=radio]+label::before,.select[type=checkbox]+label::before{display:inline-block;content:'✔';transition:color 200ms;width:1.1em;height:1.1em;border-radius:2px;border-radius:var(--checkboxRadius, 2px);box-shadow:0px 0px 2px black inset;box-shadow:var(--inputShadow);margin-right:.5em;background-color:#182230;background-color:var(--input, #182230);vertical-align:top;text-align:center;line-height:1.1em;font-size:1.1em;box-sizing:border-box;color:transparent;overflow:hidden;box-sizing:border-box\n}\noption{color:#b9b9ba;color:var(--text, #b9b9ba);background-color:#121a24;background-color:var(--bg, #121a24)\n}\ni[class*=icon-]{color:#666;color:var(--icon, #666)\n}\n.container{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin:0;padding:0 10px 0 10px\n}\n.item{-ms-flex:1;flex:1;line-height:50px;height:50px;overflow:hidden;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap\n}\n.item .nav-icon{margin-left:0.4em\n}\n.item.right{-ms-flex-pack:end;justify-content:flex-end\n}\n.auto-size{-ms-flex:1;flex:1\n}\n.nav-bar{padding:0;width:100%;-ms-flex-align:center;align-items:center;position:fixed;height:50px\n}\n.nav-bar .logo{display:-ms-flexbox;display:flex;position:absolute;top:0;bottom:0;left:0;right:0;-ms-flex-align:stretch;align-items:stretch;-ms-flex-pack:center;justify-content:center;-ms-flex:0 0 auto;flex:0 0 auto;z-index:-1;transition:opacity;transition-timing-function:ease-out;transition-duration:100ms\n}\n.nav-bar .logo .mask{-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-position:center;mask-position:center;-webkit-mask-size:contain;mask-size:contain;background-color:#182230;background-color:var(--topBarText, #182230);position:absolute;top:0;bottom:0;left:0;right:0\n}\n.nav-bar .logo img{height:100%;object-fit:contain;display:block;-ms-flex:0;flex:0\n}\n.nav-bar .inner-nav{margin:auto;box-sizing:border-box;padding-left:10px;padding-right:10px;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-preferred-size:970px;flex-basis:970px;height:50px\n}\n.nav-bar .inner-nav a,.nav-bar .inner-nav a i{color:#d8a070;color:var(--topBarLink, #d8a070)\n}\nmain-router{-ms-flex:1;flex:1\n}\n.status.compact{color:rgba(0,0,0,0.42);font-weight:300\n}\n.status.compact p{margin:0;font-size:0.8em\n}\n.panel{display:-ms-flexbox;display:flex;position:relative;-ms-flex-direction:column;flex-direction:column;margin:0.5em;background-color:#121a24;background-color:var(--bg, #121a24)\n}\n.panel::after,.panel{border-radius:10px;border-radius:var(--panelRadius, 10px)\n}\n.panel::after{content:'';position:absolute;top:0;bottom:0;left:0;right:0;pointer-events:none;box-shadow:1px 1px 4px rgba(0,0,0,0.6);box-shadow:var(--panelShadow)\n}\n.panel-body:empty::before{content:\"¯\\\\_(ツ)_/¯\";display:block;margin:1em;text-align:center\n}\n.panel-heading{display:-ms-flexbox;display:flex;border-radius:10px 10px 0 0;border-radius:var(--panelRadius, 10px) var(--panelRadius, 10px) 0 0;background-size:cover;padding:.6em .6em;text-align:left;line-height:28px;color:var(--panelText);background-color:#182230;background-color:var(--panel, #182230);-ms-flex-align:baseline;align-items:baseline;box-shadow:var(--panelHeaderShadow)\n}\n.panel-heading .title{-ms-flex:1 0 auto;flex:1 0 auto;font-size:1.3em\n}\n.panel-heading .faint{background-color:transparent;color:rgba(185,185,186,0.5);color:var(--panelFaint, rgba(185,185,186,0.5))\n}\n.panel-heading .alert{white-space:nowrap;text-overflow:ellipsis;overflow-x:hidden\n}\n.panel-heading button{-ms-flex-negative:0;flex-shrink:0\n}\n.panel-heading button,.panel-heading .alert{line-height:21px;min-height:0;box-sizing:border-box;margin:0;margin-left:.25em;min-width:1px;-ms-flex-item-align:stretch;-ms-grid-row-align:stretch;align-self:stretch\n}\n.panel-heading a{color:#d8a070;color:var(--panelLink, #d8a070)\n}\n.panel-heading.stub{border-radius:10px;border-radius:var(--panelRadius, 10px)\n}\n.panel-footer{border-radius:0 0 10px 10px;border-radius:0 0 var(--panelRadius, 10px) var(--panelRadius, 10px)\n}\n.panel-footer .faint{color:rgba(185,185,186,0.5);color:var(--panelFaint, rgba(185,185,186,0.5))\n}\n.panel-footer a{color:#d8a070;color:var(--panelLink, #d8a070)\n}\n.panel-body>p{line-height:18px;padding:1em;margin:0\n}\n.container>*{min-width:0px\n}\n.fa{color:grey\n}\nnav{z-index:1000;color:var(--topBarText);background-color:#182230;background-color:var(--topBar, #182230);color:rgba(185,185,186,0.5);color:var(--faint, rgba(185,185,186,0.5));box-shadow:0px 0px 4px rgba(0,0,0,0.6);box-shadow:var(--topBarShadow)\n}\nnav .back-button{display:block;max-width:99px;transition-property:opacity, max-width;transition-duration:300ms;transition-timing-function:ease-out\n}\nnav .back-button i{margin:0 1em\n}\nnav .back-button.hidden{opacity:0;max-width:5px\n}\n.fade-enter-active,.fade-leave-active{transition:opacity .2s\n}\n.fade-enter,.fade-leave-active{opacity:0\n}\n.main{-ms-flex-preferred-size:50%;flex-basis:50%;-ms-flex-positive:1;flex-grow:1;-ms-flex-negative:1;flex-shrink:1\n}\n.sidebar-bounds{-ms-flex:0;flex:0;-ms-flex-preferred-size:35%;flex-basis:35%\n}\n.sidebar-flexer{-ms-flex:1;flex:1;-ms-flex-preferred-size:345px;flex-basis:345px;width:365px\n}\n.mobile-shown{display:none\n}\n@media all and (min-width: 800px){\nbody{overflow-y:scroll\n}\nnav .back-button{display:none\n}\n.sidebar-bounds{overflow:hidden;max-height:100vh;width:345px;position:fixed;margin-top:-10px\n}\n.sidebar-bounds .sidebar-scroller{height:96vh;width:365px;padding-top:10px;padding-right:50px;overflow-x:hidden;overflow-y:scroll\n}\n.sidebar-bounds .sidebar{width:345px\n}\n.sidebar-flexer{max-height:96vh;-ms-flex-negative:0;flex-shrink:0;-ms-flex-positive:0;flex-grow:0\n}\n}\n.badge{display:inline-block;border-radius:99px;min-width:22px;max-width:22px;min-height:22px;max-height:22px;font-size:15px;line-height:22px;text-align:center;vertical-align:middle;white-space:nowrap;padding:0\n}\n.badge.badge-notification{background-color:red;background-color:var(--badgeNotification, red);color:white;color:var(--badgeNotificationText, #fff)\n}\n.alert{margin:0.35em;padding:0.25em;border-radius:5px;border-radius:var(--tooltipRadius, 5px);min-height:28px;line-height:28px\n}\n.alert.error{background-color:rgba(211,16,20,0.5);background-color:var(--alertError, rgba(211,16,20,0.5));color:#b9b9ba;color:var(--alertErrorText, #b9b9ba)\n}\n.panel-heading .alert.error{color:#b9b9ba;color:var(--alertErrorPanelText, #b9b9ba)\n}\n.faint{color:rgba(185,185,186,0.5);color:var(--faint, rgba(185,185,186,0.5))\n}\n.faint-link{color:rgba(185,185,186,0.5);color:var(--faint, rgba(185,185,186,0.5))\n}\n.faint-link:hover{text-decoration:underline\n}\n@media all and (min-width: 800px){\n.logo{opacity:1 !important\n}\n}\n.item.right{text-align:right\n}\n.visibility-tray{font-size:1.2em;padding:3px;cursor:pointer\n}\n.visibility-tray .selected{color:#b9b9ba;color:var(--lightText, #b9b9ba)\n}\n.visibility-tray div{padding-top:5px\n}\n.visibility-notice{padding:.5em;border:1px solid rgba(185,185,186,0.5);border:1px solid var(--faint, rgba(185,185,186,0.5));border-radius:4px;border-radius:var(--inputRadius, 4px)\n}\n@keyframes modal-background-fadein{\nfrom{background-color:transparent\n}\nto{background-color:rgba(0,0,0,0.5)\n}\n}\n.modal-view{z-index:1000;position:fixed;top:0;left:0;right:0;bottom:0;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center;overflow:auto;animation-duration:0.2s;background-color:rgba(0,0,0,0.5);animation-name:modal-background-fadein\n}\n.button-icon{font-size:1.2em\n}\n@keyframes shakeError{\n0%{transform:translateX(0)\n}\n15%{transform:translateX(0.375rem)\n}\n30%{transform:translateX(-0.375rem)\n}\n45%{transform:translateX(0.375rem)\n}\n60%{transform:translateX(-0.375rem)\n}\n75%{transform:translateX(0.375rem)\n}\n90%{transform:translateX(-0.375rem)\n}\n100%{transform:translateX(0)\n}\n}\n@media all and (max-width: 800px){\n.mobile-hidden{display:none\n}\n.panel-switcher{display:-ms-flexbox;display:flex\n}\n.container{padding:0\n}\n.panel{margin:0.5em 0 0.5em 0\n}\n.menu-button{display:block;margin-right:0.8em\n}\n}\n.login-hint{text-align:center\n}\n@media all and (min-width: 801px){\n.login-hint{display:none\n}\n}\n.login-hint a{display:inline-block;padding:1em 0px;width:100%\n}\n.btn.btn-default{min-height:28px\n}\n.autocomplete-panel{position:relative\n}\n.autocomplete-panel-body{margin:0 0.5em 0 0.5em;border-radius:5px;border-radius:var(--tooltipRadius, 5px);position:absolute;z-index:1;box-shadow:1px 2px 4px rgba(0,0,0,0.5);box-shadow:var(--popupShadow);min-width:75%;background:#121a24;background:var(--bg, #121a24);color:#b9b9ba;color:var(--lightText, #b9b9ba)\n}\n.autocomplete-item{cursor:pointer;padding:0.2em 0.4em 0.2em 0.4em;border-bottom:1px solid rgba(0,0,0,0.4);display:-ms-flexbox;display:flex\n}\n.autocomplete-item img{width:24px;height:24px;object-fit:contain\n}\n.autocomplete-item span{line-height:24px;margin:0 0.1em 0 0.2em\n}\n.autocomplete-item small{margin-left:.5em;color:rgba(185,185,186,0.5);color:var(--faint, rgba(185,185,186,0.5))\n}\n.autocomplete-item.highlighted{background-color:#182230;background-color:var(--lightBg, #182230)\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/App.scss","\n.nav-panel .panel{overflow:hidden;box-shadow:var(--panelShadow)\n}\n.nav-panel ul{list-style:none;margin:0;padding:0\n}\n.follow-request-count{margin:-6px 10px;background-color:#121a24;background-color:var(--input, rgba(185,185,186,0.5))\n}\n.nav-panel li{border-bottom:1px solid;border-color:#222;border-color:var(--border, #222);padding:0\n}\n.nav-panel li:first-child a{border-top-right-radius:10px;border-top-right-radius:var(--panelRadius, 10px);border-top-left-radius:10px;border-top-left-radius:var(--panelRadius, 10px)\n}\n.nav-panel li:last-child a{border-bottom-right-radius:10px;border-bottom-right-radius:var(--panelRadius, 10px);border-bottom-left-radius:10px;border-bottom-left-radius:var(--panelRadius, 10px)\n}\n.nav-panel li:last-child{border:none\n}\n.nav-panel a{display:block;padding:0.8em 0.85em\n}\n.nav-panel a:hover{background-color:#151e2a;background-color:var(--lightBg, #151e2a)\n}\n.nav-panel a.router-link-active{font-weight:bolder;background-color:#151e2a;background-color:var(--lightBg, #151e2a)\n}\n.nav-panel a.router-link-active:hover{text-decoration:underline\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/nav_panel/nav_panel.vue","\n.user-finder-container{max-width:100%;display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:baseline;align-items:baseline;vertical-align:baseline\n}\n.user-finder-container .user-finder-input,.user-finder-container .search-button{height:29px\n}\n.user-finder-container .user-finder-input{max-width:calc(100% - 30px - 30px - 20px)\n}\n.user-finder-container .search-button{margin-left:.5em;margin-right:.5em\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/user_finder/user_finder.vue","\n.who-to-follow *{vertical-align:middle\n}\n.who-to-follow img{width:32px;height:32px\n}\n.who-to-follow{padding:0.5em 1em 0.5em 1em;margin:0px;line-height:40px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/who_to_follow_panel/who_to_follow_panel.vue","\n.media-modal-view:hover .modal-view-button-arrow{opacity:0.75\n}\n.media-modal-view:hover .modal-view-button-arrow:focus,.media-modal-view:hover .modal-view-button-arrow:hover{outline:none;box-shadow:none\n}\n.media-modal-view:hover .modal-view-button-arrow:hover{opacity:1\n}\n.modal-image{max-width:90%;max-height:90%;box-shadow:0px 5px 15px 0 rgba(0,0,0,0.5)\n}\n.modal-view-button-arrow{position:absolute;display:block;top:50%;margin-top:-50px;width:70px;height:100px;border:0;padding:0;opacity:0;box-shadow:none;background:none;-webkit-appearance:none;-moz-appearance:none;appearance:none;overflow:visible;cursor:pointer;transition:opacity 333ms cubic-bezier(0.4, 0, 0.22, 1)\n}\n.modal-view-button-arrow .arrow-icon{position:absolute;top:35px;height:30px;width:32px;font-size:14px;line-height:30px;color:#FFF;text-align:center;background-color:rgba(0,0,0,0.3)\n}\n.modal-view-button-arrow--prev{left:0\n}\n.modal-view-button-arrow--prev .arrow-icon{left:6px\n}\n.modal-view-button-arrow--next{right:0\n}\n.modal-view-button-arrow--next .arrow-icon{right:6px\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/media_modal/media_modal.vue","\n.side-drawer-container{position:fixed;z-index:1000;top:0;left:0;width:100%;height:100%;display:-ms-flexbox;display:flex;-ms-flex-align:stretch;align-items:stretch;transition-duration:0s;transition-property:transform\n}\n.side-drawer-container-open{transform:translate(0%)\n}\n.side-drawer-container-closed{transition-delay:0.35s;transform:translate(-100%)\n}\n.side-drawer-darken{top:0;left:0;width:100vw;height:100vh;position:fixed;z-index:-1;transition:0.35s;transition-property:background-color;background-color:rgba(0,0,0,0.5)\n}\n.side-drawer-darken-closed{background-color:transparent\n}\n.side-drawer-click-outside{-ms-flex:1 1 100%;flex:1 1 100%\n}\n.side-drawer{overflow-x:hidden;transition-timing-function:cubic-bezier(0, 1, 0.5, 1);transition:0.35s;transition-property:transform;margin:0 0 0 -100px;padding:0 0 1em 100px;width:80%;max-width:20em;-ms-flex:0 0 80%;flex:0 0 80%;box-shadow:1px 1px 4px rgba(0,0,0,0.6);box-shadow:var(--panelShadow);background-color:#121a24;background-color:var(--bg, #121a24)\n}\n.side-drawer-logo-wrapper{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:0.85em\n}\n.side-drawer-logo-wrapper img{-ms-flex:none;flex:none;height:50px;margin-right:0.85em\n}\n.side-drawer-logo-wrapper span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap\n}\n.side-drawer-click-outside-closed{-ms-flex:0 0 0px;flex:0 0 0\n}\n.side-drawer-closed{transform:translate(-100%)\n}\n.side-drawer-heading{background:transparent;-ms-flex-direction:column;flex-direction:column;-ms-flex-align:stretch;align-items:stretch;display:-ms-flexbox;display:flex;padding:0;margin:0\n}\n.side-drawer ul{list-style:none;margin:0;padding:0;border-bottom:1px solid;border-color:#222;border-color:var(--border, #222);margin:0.2em 0\n}\n.side-drawer ul:last-child{border:0\n}\n.side-drawer li{padding:0\n}\n.side-drawer li a{display:block;padding:0.5em 0.85em\n}\n.side-drawer li a:hover{background-color:#151e2a;background-color:var(--lightBg, #151e2a)\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/side_drawer/side_drawer.vue","\n.post-form-modal-view{max-height:100%;display:block\n}\n.post-form-modal-panel{-ms-flex-negative:0;flex-shrink:0;margin:25% 0 4em 0;width:100%\n}\n.new-status-button{width:5em;height:5em;border-radius:100%;position:fixed;bottom:1.5em;right:1.5em;background-color:#182230;background-color:var(--btn, #182230);display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center;box-shadow:0px 2px 2px rgba(0,0,0,0.3),0px 4px 6px rgba(0,0,0,0.3);z-index:10;transition:0.35s transform;transition-timing-function:cubic-bezier(0, 1, 0.5, 1)\n}\n.new-status-button.hidden{transform:translateY(150%)\n}\n.new-status-button i{font-size:1.5em;color:#b9b9ba;color:var(--text, #b9b9ba)\n}\n@media all and (min-width: 801px){\n.new-status-button{display:none\n}\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/mobile_post_status_modal/mobile_post_status_modal.vue","\n.mobile-inner-nav{width:100%;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center\n}\n.mobile-nav-button{display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;width:50px;position:relative;cursor:pointer\n}\n.alert-dot{border-radius:100%;height:8px;width:8px;position:absolute;left:calc(50% - 4px);top:calc(50% - 4px);margin-left:6px;margin-top:-6px;background-color:red;background-color:var(--badgeNotification, red)\n}\n.mobile-notifications-drawer{width:100%;height:100vh;overflow-x:hidden;position:fixed;top:0;left:0;box-shadow:1px 1px 4px rgba(0,0,0,0.6);box-shadow:var(--panelShadow);transition-property:transform;transition-duration:0.25s;transform:translateX(0)\n}\n.mobile-notifications-drawer.closed{transform:translateX(100%)\n}\n.mobile-notifications-header{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;z-index:1;width:100%;height:50px;line-height:50px;position:absolute;color:var(--topBarText);background-color:#182230;background-color:var(--topBar, #182230);box-shadow:0px 0px 4px rgba(0,0,0,0.6);box-shadow:var(--topBarShadow)\n}\n.mobile-notifications-header .title{font-size:1.3em;margin-left:0.6em\n}\n.mobile-notifications{margin-top:50px;width:100vw;height:calc(100vh - 50px);overflow-x:hidden;overflow-y:scroll;color:#b9b9ba;color:var(--text, #b9b9ba);background-color:#121a24;background-color:var(--bg, #121a24)\n}\n.mobile-notifications .notifications{padding:0;border-radius:0;box-shadow:none\n}\n.mobile-notifications .notifications .panel{border-radius:0;margin:0;box-shadow:none\n}\n.mobile-notifications .notifications .panel:after{border-radius:0\n}\n.mobile-notifications .notifications .panel .panel-heading{border-radius:0;box-shadow:none\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/mobile_nav/mobile_nav.vue"],"sourceRoot":""} \ No newline at end of file diff --git a/priv/static/static/css/app.ea66966b753e709d7ce58f910a2c003e.css b/priv/static/static/css/app.ea66966b753e709d7ce58f910a2c003e.css deleted file mode 100644 index 7cd3bda40..000000000 Binary files a/priv/static/static/css/app.ea66966b753e709d7ce58f910a2c003e.css and /dev/null differ diff --git a/priv/static/static/css/app.ea66966b753e709d7ce58f910a2c003e.css.map b/priv/static/static/css/app.ea66966b753e709d7ce58f910a2c003e.css.map deleted file mode 100644 index 94e03d028..000000000 --- a/priv/static/static/css/app.ea66966b753e709d7ce58f910a2c003e.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["webpack:///webpack:///src/components/timeline/timeline.vue","webpack:///webpack:///src/components/status/status.vue","webpack:///webpack:///src/components/attachment/attachment.vue","webpack:///webpack:///src/components/still-image/still-image.vue","webpack:///webpack:///src/components/favorite_button/favorite_button.vue","webpack:///webpack:///src/components/retweet_button/retweet_button.vue","webpack:///webpack:///src/components/delete_button/delete_button.vue","webpack:///webpack:///src/components/post_status_form/post_status_form.vue","webpack:///webpack:///src/components/media_upload/media_upload.vue","webpack:///webpack:///src/components/user_card/user_card.vue","webpack:///webpack:///src/components/user_avatar/user_avatar.vue","webpack:///webpack:///src/components/gallery/gallery.vue","webpack:///webpack:///src/components/link-preview/link-preview.vue","webpack:///webpack:///src/components/status_or_conversation/status_or_conversation.vue","webpack:///webpack:///src/components/user_profile/user_profile.vue","webpack:///webpack:///src/components/follow_card/follow_card.vue","webpack:///webpack:///src/components/basic_user_card/basic_user_card.vue","webpack:///webpack:///src/hocs/with_load_more/src/hocs/with_load_more/with_load_more.scss","webpack:///webpack:///src/hocs/with_list/src/hocs/with_list/with_list.scss","webpack:///webpack:///src/components/settings/settings.vue","webpack:///webpack:///src/components/tab_switcher/src/components/tab_switcher/tab_switcher.scss","webpack:///webpack:///src/components/style_switcher/style_switcher.scss","webpack:///webpack:///src/components/color_input/color_input.vue","webpack:///webpack:///src/components/shadow_control/shadow_control.vue","webpack:///webpack:///src/components/font_control/font_control.vue","webpack:///webpack:///src/components/contrast_ratio/contrast_ratio.vue","webpack:///webpack:///src/components/export_import/export_import.vue","webpack:///webpack:///src/components/registration/registration.vue","webpack:///webpack:///src/components/user_settings/user_settings.vue","webpack:///webpack:///src/components/image_cropper/image_cropper.vue","webpack:///webpack:///~/cropperjs/dist/cropper.css","webpack:///webpack:///src/components/block_card/block_card.vue","webpack:///webpack:///src/hocs/with_subscription/src/hocs/with_subscription/with_subscription.scss","webpack:///webpack:///src/components/follow_request_card/follow_request_card.vue","webpack:///webpack:///src/components/user_search/user_search.vue","webpack:///webpack:///src/components/notifications/notifications.scss","webpack:///webpack:///src/components/login_form/login_form.vue","webpack:///webpack:///src/components/chat_panel/chat_panel.vue","webpack:///webpack:///src/components/features_panel/features_panel.vue","webpack:///webpack:///src/components/terms_of_service_panel/terms_of_service_panel.vue","webpack:///webpack:///src/App.scss","webpack:///webpack:///src/components/nav_panel/nav_panel.vue","webpack:///webpack:///src/components/user_finder/user_finder.vue","webpack:///webpack:///src/components/who_to_follow_panel/who_to_follow_panel.vue","webpack:///webpack:///src/components/media_modal/media_modal.vue","webpack:///webpack:///src/components/side_drawer/side_drawer.vue","webpack:///webpack:///src/components/mobile_post_status_modal/mobile_post_status_modal.vue"],"names":[],"mappings":"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","file":"static/css/app.ea66966b753e709d7ce58f910a2c003e.css","sourcesContent":["\n.timeline .loadmore-text{opacity:1\n}\n.new-status-notification{position:relative;margin-top:-1px;font-size:1.1em;border-width:1px 0 0 0;border-style:solid;border-color:var(--border, #222);padding:10px;z-index:1;background-color:#182230;background-color:var(--panel, #182230)\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/timeline/timeline.vue","\n.status-body{-ms-flex:1;flex:1;min-width:0\n}\n.status-preview.status-el{border-style:solid;border-width:1px;border-color:#222;border-color:var(--border, #222)\n}\n.status-preview-container{position:relative;max-width:100%\n}\n.status-preview{position:absolute;max-width:95%;display:-ms-flexbox;display:flex;background-color:#121a24;background-color:var(--bg, #121a24);border-color:#222;border-color:var(--border, #222);border-style:solid;border-width:1px;border-radius:5px;border-radius:var(--tooltipRadius, 5px);box-shadow:2px 2px 3px rgba(0,0,0,0.5);box-shadow:var(--popupShadow);margin-top:0.25em;margin-left:0.5em;z-index:50\n}\n.status-preview .status{-ms-flex:1;flex:1;border:0;min-width:15em\n}\n.status-preview-loading{display:block;min-width:15em;padding:1em;text-align:center;border-width:1px;border-style:solid\n}\n.status-preview-loading i{font-size:2em\n}\n.media-left{margin-right:.75em\n}\n.status-el{-webkit-hyphens:auto;-ms-hyphens:auto;hyphens:auto;overflow-wrap:break-word;word-wrap:break-word;word-break:break-word;border-left-width:0px;min-width:0;border-color:#222;border-color:var(--border, #222);border-left:4px red;border-left:4px var(--cRed, red)\n}\n.status-el_focused{background-color:#151e2a;background-color:var(--lightBg, #151e2a)\n}\n.timeline .status-el{border-bottom-width:1px;border-bottom-style:solid\n}\n.status-el .media-body{-ms-flex:1;flex:1;padding:0\n}\n.status-el .status-usercard{margin-bottom:.75em\n}\n.status-el .user-name{white-space:nowrap;font-size:14px;overflow:hidden;-ms-flex-negative:0;flex-shrink:0;max-width:85%;font-weight:bold\n}\n.status-el .user-name img{width:14px;height:14px;vertical-align:middle;object-fit:contain\n}\n.status-el .media-heading{padding:0;vertical-align:bottom;-ms-flex-preferred-size:100%;flex-basis:100%;margin-bottom:0.5em\n}\n.status-el .media-heading a{display:inline-block;word-break:break-all\n}\n.status-el .media-heading small{font-weight:lighter\n}\n.status-el .media-heading .heading-name-row{padding:0;display:-ms-flexbox;display:flex;-ms-flex-pack:justify;justify-content:space-between;line-height:18px\n}\n.status-el .media-heading .heading-name-row .name-and-account-name{display:-ms-flexbox;display:flex;min-width:0\n}\n.status-el .media-heading .heading-name-row .user-name{-ms-flex-negative:1;flex-shrink:1;margin-right:0.4em;overflow:hidden;text-overflow:ellipsis\n}\n.status-el .media-heading .heading-name-row .account-name{min-width:1.6em;margin-right:0.4em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;-ms-flex:1 1 0px;flex:1 1 0\n}\n.status-el .media-heading .heading-right{display:-ms-flexbox;display:flex;-ms-flex-negative:0;flex-shrink:0\n}\n.status-el .media-heading .timeago{margin-right:0.2em\n}\n.status-el .media-heading .heading-reply-row{-ms-flex-line-pack:baseline;align-content:baseline;font-size:12px;line-height:18px;max-width:100%;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:stretch;align-items:stretch\n}\n.status-el .media-heading .heading-reply-row a{max-width:100%;text-overflow:ellipsis;overflow:hidden;white-space:nowrap\n}\n.status-el .media-heading .reply-to-and-accountname{display:-ms-flexbox;display:flex;height:18px;margin-right:0.5em;overflow:hidden;max-width:100%\n}\n.status-el .media-heading .reply-to-and-accountname .icon-reply{transform:scaleX(-1)\n}\n.status-el .media-heading .reply-info{display:-ms-flexbox;display:flex\n}\n.status-el .media-heading .reply-to{display:-ms-flexbox;display:flex\n}\n.status-el .media-heading .reply-to-text{overflow:hidden;text-overflow:ellipsis;margin:0 0.4em 0 0.2em\n}\n.status-el .media-heading .replies-separator{margin-left:0.4em\n}\n.status-el .media-heading .replies{line-height:18px;font-size:12px;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap\n}\n.status-el .media-heading .replies>*{margin-right:0.4em\n}\n.status-el .media-heading .reply-link{height:17px\n}\n.status-el .tall-status{position:relative;height:220px;overflow-x:hidden;overflow-y:hidden\n}\n.status-el .tall-status-hider{display:inline-block;word-break:break-all;position:absolute;height:70px;margin-top:150px;width:100%;text-align:center;line-height:110px;background:linear-gradient(to bottom, transparent, #121a24 80%);background:linear-gradient(to bottom, transparent, var(--bg, #121a24) 80%)\n}\n.status-el .tall-status-hider_focused{background:linear-gradient(to bottom, transparent, #151e2a 80%);background:linear-gradient(to bottom, transparent, var(--lightBg, #151e2a) 80%)\n}\n.status-el .status-unhider,.status-el .cw-status-hider{width:100%;text-align:center;display:inline-block;word-break:break-all\n}\n.status-el .status-content{font-family:var(--postFont, sans-serif);line-height:1.4em\n}\n.status-el .status-content img,.status-el .status-content video{max-width:100%;max-height:400px;vertical-align:middle;object-fit:contain\n}\n.status-el .status-content img.emoji,.status-el .status-content video.emoji{width:32px;height:32px\n}\n.status-el .status-content blockquote{margin:0.2em 0 0.2em 2em;font-style:italic\n}\n.status-el .status-content pre{overflow:auto\n}\n.status-el .status-content code,.status-el .status-content samp,.status-el .status-content kbd,.status-el .status-content var,.status-el .status-content pre{font-family:var(--postCodeFont, monospace)\n}\n.status-el .status-content p{margin:0 0 1em 0\n}\n.status-el .status-content p:last-child{margin:0 0 0 0\n}\n.status-el .status-content h1{font-size:1.1em;line-height:1.2em;margin:1.4em 0\n}\n.status-el .status-content h2{font-size:1.1em;margin:1.0em 0\n}\n.status-el .status-content h3{font-size:1em;margin:1.2em 0\n}\n.status-el .status-content h4{margin:1.1em 0\n}\n.status-el .retweet-info{padding:0.4em .75em;margin:0\n}\n.status-el .retweet-info .avatar.still-image{border-radius:10px;border-radius:var(--avatarAltRadius, 10px);margin-left:28px;width:20px;height:20px\n}\n.status-el .retweet-info .media-body{font-size:1em;line-height:22px;display:-ms-flexbox;display:flex;-ms-flex-line-pack:center;align-content:center;-ms-flex-wrap:wrap;flex-wrap:wrap\n}\n.status-el .retweet-info .media-body .user-name{font-weight:bold;overflow:hidden;text-overflow:ellipsis\n}\n.status-el .retweet-info .media-body .user-name img{width:14px;height:14px;vertical-align:middle;object-fit:contain\n}\n.status-el .retweet-info .media-body i{padding:0 0.2em\n}\n.status-el .retweet-info .media-body a{max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap\n}\n.status-fadein{animation-duration:0.4s;animation-name:fadein\n}\n@keyframes fadein{\nfrom{opacity:0\n}\nto{opacity:1\n}\n}\n.greentext{color:green\n}\n.status-conversation{border-left-style:solid\n}\n.status-actions{width:100%;display:-ms-flexbox;display:flex;margin-top:.75em\n}\n.status-actions div,.status-actions favorite-button{max-width:4em;-ms-flex:1;flex:1\n}\n.icon-reply:hover{color:#0095ff;color:var(--cBlue, #0095ff)\n}\n.icon-reply.icon-reply-active{color:#0095ff;color:var(--cBlue, #0095ff)\n}\n.status:hover .animated.avatar canvas{display:none\n}\n.status:hover .animated.avatar img{visibility:visible\n}\n.status{display:-ms-flexbox;display:flex;padding:.75em\n}\n.status.is-retweet{padding-top:0\n}\n.status-conversation:last-child{border-bottom:none\n}\n.muted{padding:0.25em 0.5em\n}\n.muted button{margin-left:auto\n}\n.muted .muteWords{margin-left:10px\n}\na.unmute{display:block;margin-left:auto\n}\n.reply-left{-ms-flex:0;flex:0;min-width:48px\n}\n.reply-body{-ms-flex:1;flex:1\n}\n.timeline>.status-el:last-child{border-radius:0 0 10px 10px;border-radius:0 0 var(--panelRadius, 10px) var(--panelRadius, 10px);border-bottom:none\n}\n@media all and (max-width: 800px){\n.status-el .retweet-info .avatar.still-image{margin-left:20px\n}\n.status{max-width:100%\n}\n.status .avatar.still-image{width:40px;height:40px\n}\n.status .avatar.still-image.avatar-compact{width:32px;height:32px\n}\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/status/status.vue","\n.attachments{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap\n}\n.attachments .attachment.media-upload-container{-ms-flex:0 0 auto;flex:0 0 auto;max-height:200px;max-width:100%;display:-ms-flexbox;display:flex\n}\n.attachments .attachment.media-upload-container video{max-width:100%\n}\n.attachments .placeholder{margin-right:8px;margin-bottom:4px\n}\n.attachments .nsfw-placeholder{cursor:pointer\n}\n.attachments .nsfw-placeholder.loading{cursor:progress\n}\n.attachments .attachment{position:relative;margin-top:0.5em;-ms-flex-item-align:start;align-self:flex-start;line-height:0;border-style:solid;border-width:1px;border-radius:10px;border-radius:var(--attachmentRadius, 10px);border-color:#222;border-color:var(--border, #222);overflow:hidden\n}\n.attachments .non-gallery.attachment.video{-ms-flex:1 0 40%;flex:1 0 40%\n}\n.attachments .non-gallery.attachment .nsfw{height:260px\n}\n.attachments .non-gallery.attachment .small{height:120px;-ms-flex-positive:0;flex-grow:0\n}\n.attachments .non-gallery.attachment .video{height:260px;display:-ms-flexbox;display:flex\n}\n.attachments .non-gallery.attachment video{max-height:100%;object-fit:contain\n}\n.attachments .fullwidth{-ms-flex-preferred-size:100%;flex-basis:100%\n}\n.attachments.video{line-height:0\n}\n.attachments .video-container{display:-ms-flexbox;display:flex;max-height:100%\n}\n.attachments .video{width:100%\n}\n.attachments .play-icon{position:absolute;font-size:64px;top:calc(50% - 32px);left:calc(50% - 32px);color:rgba(255,255,255,0.75);text-shadow:0 0 2px rgba(0,0,0,0.4)\n}\n.attachments .play-icon::before{margin:0\n}\n.attachments.html{-ms-flex-preferred-size:90%;flex-basis:90%;width:100%;display:-ms-flexbox;display:flex\n}\n.attachments .hider{position:absolute;right:0;white-space:nowrap;margin:10px;padding:5px;background:rgba(230,230,230,0.6);font-weight:bold;z-index:4;line-height:1;border-radius:5px;border-radius:var(--tooltipRadius, 5px)\n}\n.attachments video{z-index:0\n}\n.attachments audio{width:100%\n}\n.attachments img.media-upload{line-height:0;max-height:200px;max-width:100%\n}\n.attachments .oembed{line-height:1.2em;-ms-flex:1 0 100%;flex:1 0 100%;width:100%;margin-right:15px;display:-ms-flexbox;display:flex\n}\n.attachments .oembed img{width:100%\n}\n.attachments .oembed .image{-ms-flex:1;flex:1\n}\n.attachments .oembed .image img{border:0px;border-radius:5px;height:100%;object-fit:cover\n}\n.attachments .oembed .text{-ms-flex:2;flex:2;margin:8px;word-break:break-all\n}\n.attachments .oembed .text h1{font-size:14px;margin:0px\n}\n.attachments .image-attachment{width:100%;height:100%\n}\n.attachments .image-attachment.hidden{display:none\n}\n.attachments .image-attachment .nsfw{object-fit:cover;width:100%;height:100%\n}\n.attachments .image-attachment img{image-orientation:from-image\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/attachment/attachment.vue","\n.still-image{position:relative;line-height:0;overflow:hidden;width:100%;height:100%\n}\n.still-image:hover canvas{display:none\n}\n.still-image img{width:100%;height:100%;object-fit:contain\n}\n.still-image.animated:hover::before,.still-image.animated img{visibility:hidden\n}\n.still-image.animated:hover img{visibility:visible\n}\n.still-image.animated::before{content:'gif';position:absolute;line-height:10px;font-size:10px;top:5px;left:5px;background:rgba(127,127,127,0.5);color:#FFF;display:block;padding:2px 4px;border-radius:5px;border-radius:var(--tooltipRadius, 5px);z-index:2\n}\n.still-image canvas{position:absolute;top:0;bottom:0;left:0;right:0;width:100%;height:100%;object-fit:contain\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/still-image/still-image.vue","\n.fav-active{cursor:pointer;animation-duration:0.6s\n}\n.fav-active:hover{color:orange;color:var(--cOrange, orange)\n}\n.favorite-button.icon-star{color:orange;color:var(--cOrange, orange)\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/favorite_button/favorite_button.vue","\n.rt-active{cursor:pointer;animation-duration:0.6s\n}\n.rt-active:hover{color:#0fa00f;color:var(--cGreen, #0fa00f)\n}\n.icon-retweet.retweeted{color:#0fa00f;color:var(--cGreen, #0fa00f)\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/retweet_button/retweet_button.vue","\n.icon-cancel,.delete-status{cursor:pointer\n}\n.icon-cancel:hover,.delete-status:hover{color:red;color:var(--cRed, red)\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/delete_button/delete_button.vue","\n.tribute-container ul{padding:0px\n}\n.tribute-container ul li{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center\n}\n.tribute-container img{padding:3px;width:16px;height:16px;border-radius:10px;border-radius:var(--avatarAltRadius, 10px)\n}\n.post-status-form .visibility-tray{display:-ms-flexbox;display:flex;-ms-flex-pack:justify;justify-content:space-between;-ms-flex-direction:row-reverse;flex-direction:row-reverse\n}\n.post-status-form .form-bottom,.login .form-bottom{display:-ms-flexbox;display:flex;padding:0.5em;height:32px\n}\n.post-status-form .form-bottom button,.login .form-bottom button{width:10em\n}\n.post-status-form .form-bottom p,.login .form-bottom p{margin:0.35em;padding:0.35em;display:-ms-flexbox;display:flex\n}\n.post-status-form .error,.login .error{text-align:center\n}\n.post-status-form .media-upload-wrapper,.login .media-upload-wrapper{-ms-flex:0 0 auto;flex:0 0 auto;max-width:100%;min-width:50px;margin-right:.2em;margin-bottom:.5em\n}\n.post-status-form .media-upload-wrapper .icon-cancel,.login .media-upload-wrapper .icon-cancel{display:inline-block;position:static;margin:0;padding-bottom:0;margin-left:10px;margin-left:var(--attachmentRadius, 10px);background-color:#182230;background-color:var(--btn, #182230);border-bottom-left-radius:0;border-bottom-right-radius:0\n}\n.post-status-form .attachments,.login .attachments{padding:0 0.5em\n}\n.post-status-form .attachments .attachment,.login .attachments .attachment{margin:0;position:relative;-ms-flex:0 0 auto;flex:0 0 auto;border:1px solid #222;border:1px solid var(--border, #222);text-align:center\n}\n.post-status-form .attachments .attachment audio,.login .attachments .attachment audio{min-width:300px;-ms-flex:1 0 auto;flex:1 0 auto\n}\n.post-status-form .attachments .attachment a,.login .attachments .attachment a{display:block;text-align:left;line-height:1.2;padding:.5em\n}\n.post-status-form .attachments i,.login .attachments i{position:absolute;margin:10px;padding:5px;background:rgba(230,230,230,0.6);border-radius:10px;border-radius:var(--attachmentRadius, 10px);font-weight:bold\n}\n.post-status-form .btn,.login .btn{cursor:pointer\n}\n.post-status-form .btn[disabled],.login .btn[disabled]{cursor:not-allowed\n}\n.post-status-form form,.login form{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding:0.6em\n}\n.post-status-form .form-group,.login .form-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding:0.3em 0.5em 0.6em;line-height:24px\n}\n.post-status-form form textarea.form-cw,.login form textarea.form-cw{line-height:16px;resize:none;overflow:hidden;transition:min-height 200ms 100ms;min-height:1px\n}\n.post-status-form form textarea.form-control,.login form textarea.form-control{line-height:16px;resize:none;overflow:hidden;transition:min-height 200ms 100ms;min-height:1px;box-sizing:content-box\n}\n.post-status-form form textarea.form-control:focus,.login form textarea.form-control:focus{min-height:48px\n}\n.post-status-form .btn,.login .btn{cursor:pointer\n}\n.post-status-form .btn[disabled],.login .btn[disabled]{cursor:not-allowed\n}\n.post-status-form .icon-cancel,.login .icon-cancel{cursor:pointer;z-index:4\n}\n.post-status-form .autocomplete-panel,.login .autocomplete-panel{margin:0 0.5em 0 0.5em;border-radius:5px;border-radius:var(--tooltipRadius, 5px);position:absolute;z-index:1;box-shadow:1px 2px 4px rgba(0,0,0,0.5);box-shadow:var(--popupShadow);min-width:75%;background:#121a24;background:var(--bg, #121a24);color:#b9b9ba;color:var(--lightText, #b9b9ba)\n}\n.post-status-form .autocomplete,.login .autocomplete{cursor:pointer;padding:0.2em 0.4em 0.2em 0.4em;border-bottom:1px solid rgba(0,0,0,0.4);display:-ms-flexbox;display:flex\n}\n.post-status-form .autocomplete img,.login .autocomplete img{width:24px;height:24px;border-radius:4px;border-radius:var(--avatarRadius, 4px);object-fit:contain\n}\n.post-status-form .autocomplete span,.login .autocomplete span{line-height:24px;margin:0 0.1em 0 0.2em\n}\n.post-status-form .autocomplete small,.login .autocomplete small{margin-left:.5em;color:rgba(185,185,186,0.5);color:var(--faint, rgba(185,185,186,0.5))\n}\n.post-status-form .autocomplete.highlighted,.login .autocomplete.highlighted{background-color:#182230;background-color:var(--lightBg, #182230)\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/post_status_form/post_status_form.vue","\n.media-upload {\n font-size: 26px;\n -ms-flex: 1;\n flex: 1;\n}\n.icon-upload {\n cursor: pointer;\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/media_upload/media_upload.vue","\n.user-card{background-size:cover;overflow:hidden\n}\n.user-card .panel-heading{padding:.5em 0;text-align:center;box-shadow:none;background:transparent;-ms-flex-direction:column;flex-direction:column;-ms-flex-align:stretch;align-items:stretch\n}\n.user-card .panel-body{word-wrap:break-word;background:linear-gradient(to bottom, transparent, #121a24 80%);background:linear-gradient(to bottom, transparent, var(--bg, #121a24) 80%)\n}\n.user-card p{margin-bottom:0\n}\n.user-card-bio{text-align:center\n}\n.user-card-bio img{object-fit:contain;vertical-align:middle;max-width:100%;max-height:400px\n}\n.user-card-bio img .emoji{width:32px;height:32px\n}\n.user-card-rounded-t{border-top-left-radius:10px;border-top-left-radius:var(--panelRadius, 10px);border-top-right-radius:10px;border-top-right-radius:var(--panelRadius, 10px)\n}\n.user-card-rounded{border-radius:10px;border-radius:var(--panelRadius, 10px)\n}\n.user-card-bordered{border-width:1px;border-style:solid;border-color:#222;border-color:var(--border, #222)\n}\n.user-info{color:#b9b9ba;color:var(--lightText, #b9b9ba);padding:0 26px\n}\n.user-info .container{padding:16px 0 6px;display:-ms-flexbox;display:flex;max-height:56px\n}\n.user-info .container .avatar{-ms-flex:1 0 100%;flex:1 0 100%;width:56px;height:56px;box-shadow:0px 1px 8px rgba(0,0,0,0.75);box-shadow:var(--avatarShadow);object-fit:cover\n}\n.user-info:hover .animated.avatar canvas{display:none\n}\n.user-info:hover .animated.avatar img{visibility:visible\n}\n.user-info .usersettings{color:#b9b9ba;color:var(--lightText, #b9b9ba);opacity:.8\n}\n.user-info .name-and-screen-name{display:block;margin-left:0.6em;text-align:left;text-overflow:ellipsis;white-space:nowrap;-ms-flex:1 1 0px;flex:1 1 0;z-index:1\n}\n.user-info .name-and-screen-name img{width:26px;height:26px;vertical-align:middle;object-fit:contain\n}\n.user-info .name-and-screen-name .top-line{display:-ms-flexbox;display:flex\n}\n.user-info .user-name{text-overflow:ellipsis;overflow:hidden;-ms-flex:1 1 auto;flex:1 1 auto;margin-right:1em;font-size:15px\n}\n.user-info .user-name img{object-fit:contain;height:16px;width:16px;vertical-align:middle\n}\n.user-info .user-screen-name{color:#b9b9ba;color:var(--lightText, #b9b9ba);display:inline-block;font-weight:light;font-size:15px;padding-right:0.1em;width:100%;display:-ms-flexbox;display:flex\n}\n.user-info .user-screen-name .dailyAvg{min-width:1px;-ms-flex:0 0 auto;flex:0 0 auto;margin-left:1em;font-size:0.7em;color:#b9b9ba;color:var(--text, #b9b9ba)\n}\n.user-info .user-screen-name .handle{min-width:1px;-ms-flex:0 1 auto;flex:0 1 auto;text-overflow:ellipsis;overflow:hidden\n}\n.user-info .user-screen-name .staff{text-transform:capitalize;color:#b9b9ba;color:var(--btnText, #b9b9ba);background-color:#182230;background-color:var(--btn, #182230)\n}\n.user-info .user-meta{margin-bottom:.15em;display:-ms-flexbox;display:flex;-ms-flex-align:baseline;align-items:baseline;font-size:14px;line-height:22px;-ms-flex-wrap:wrap;flex-wrap:wrap\n}\n.user-info .user-meta .following{-ms-flex:1 0 auto;flex:1 0 auto;margin:0;margin-bottom:.25em;text-align:left\n}\n.user-info .user-meta .highlighter{-ms-flex:0 1 auto;flex:0 1 auto;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-.5em;-ms-flex-item-align:start;align-self:start\n}\n.user-info .user-meta .highlighter .userHighlightCl{padding:2px 10px;-ms-flex:1 0 auto;flex:1 0 auto\n}\n.user-info .user-meta .highlighter .userHighlightSel,.user-info .user-meta .highlighter .userHighlightSel.select{padding-top:0;padding-bottom:0;-ms-flex:1 0 auto;flex:1 0 auto\n}\n.user-info .user-meta .highlighter .userHighlightSel.select i{line-height:22px\n}\n.user-info .user-meta .highlighter .userHighlightText{width:70px;-ms-flex:1 0 auto;flex:1 0 auto\n}\n.user-info .user-meta .highlighter .userHighlightCl,.user-info .user-meta .highlighter .userHighlightText,.user-info .user-meta .highlighter .userHighlightSel,.user-info .user-meta .highlighter .userHighlightSel.select{height:22px;vertical-align:top;margin-right:.5em;margin-bottom:.25em\n}\n.user-info .user-interactions{display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;-ms-flex-pack:justify;justify-content:space-between;margin-right:-.75em\n}\n.user-info .user-interactions div{-ms-flex:1 0 0px;flex:1 0 0;margin-right:.75em;margin-bottom:.6em;white-space:nowrap\n}\n.user-info .user-interactions .mute{max-width:220px;min-height:28px\n}\n.user-info .user-interactions .remote-follow{max-width:220px;min-height:28px\n}\n.user-info .user-interactions .follow{max-width:220px;min-height:28px\n}\n.user-info .user-interactions button{width:100%;height:100%;margin:0\n}\n.user-info .user-interactions .remote-button{height:28px !important;width:92%\n}\n.user-info .user-interactions .pressed{border-bottom-color:rgba(255,255,255,0.2);border-top-color:rgba(0,0,0,0.2)\n}\n.user-counts{display:-ms-flexbox;display:flex;line-height:16px;padding:.5em 1.5em 0em 1.5em;text-align:center;-ms-flex-pack:justify;justify-content:space-between;color:#b9b9ba;color:var(--lightText, #b9b9ba);-ms-flex-wrap:wrap;flex-wrap:wrap\n}\n.user-count{-ms-flex:1 0 auto;flex:1 0 auto;padding:.5em 0 .5em 0;margin:0 .5em\n}\n.user-count h5{font-size:1em;font-weight:bolder;margin:0 0 0.25em\n}\n.user-count a{text-decoration:none\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/user_card/user_card.vue","\n.avatar.still-image{width:48px;height:48px;box-shadow:var(--avatarStatusShadow);border-radius:4px;border-radius:var(--avatarRadius, 4px)\n}\n.avatar.still-image img{width:100%;height:100%\n}\n.avatar.still-image.better-shadow{box-shadow:var(--avatarStatusShadowInset);filter:var(--avatarStatusShadowFilter)\n}\n.avatar.still-image.animated::before{display:none\n}\n.avatar.still-image.avatar-compact{width:32px;height:32px;border-radius:10px;border-radius:var(--avatarAltRadius, 10px)\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/user_avatar/user_avatar.vue","\n.gallery-row{height:200px;width:100%;display:-ms-flexbox;display:flex;-ms-flex-direction:row;flex-direction:row;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-ms-flex-line-pack:stretch;align-content:stretch;-ms-flex-positive:1;flex-grow:1;margin-top:0.5em\n}\n.gallery-row .attachments,.gallery-row .attachment{margin:0 0.5em 0 0;-ms-flex-positive:1;flex-grow:1;height:100%;box-sizing:border-box;min-width:2em\n}\n.gallery-row .attachments:last-child,.gallery-row .attachment:last-child{margin:0\n}\n.gallery-row .image-attachment{width:100%;height:100%\n}\n.gallery-row .video-container{height:100%\n}\n.gallery-row.contain-fit img,.gallery-row.contain-fit video{object-fit:contain\n}\n.gallery-row.cover-fit img,.gallery-row.cover-fit video{object-fit:cover\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/gallery/gallery.vue","\n.link-preview-card{display:-ms-flexbox;display:flex;-ms-flex-direction:row;flex-direction:row;cursor:pointer;overflow:hidden;margin-top:0.5em;color:#b9b9ba;color:var(--text, #b9b9ba);border-style:solid;border-width:1px;border-radius:10px;border-radius:var(--attachmentRadius, 10px);border-color:#222;border-color:var(--border, #222)\n}\n.link-preview-card .card-image{-ms-flex-negative:0;flex-shrink:0;width:120px;max-width:25%\n}\n.link-preview-card .card-image img{width:100%;height:100%;object-fit:cover;border-radius:10px;border-radius:var(--attachmentRadius, 10px)\n}\n.link-preview-card .small-image{width:80px\n}\n.link-preview-card .card-content{max-height:100%;margin:0.5em;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column\n}\n.link-preview-card .card-host{font-size:12px\n}\n.link-preview-card .card-description{margin:0.5em 0 0 0;overflow:hidden;text-overflow:ellipsis;word-break:break-word;line-height:1.2em;max-height:calc(1.2em * 3 - 1px)\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/link-preview/link-preview.vue","\n.spacer{height:1em\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/status_or_conversation/status_or_conversation.vue","\n.user-profile{-ms-flex:2;flex:2;-ms-flex-preferred-size:500px;flex-basis:500px\n}\n.user-profile .userlist-placeholder{display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:middle;align-items:middle;padding:2em\n}\n.user-profile .timeline-heading{display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center\n}\n.user-profile .timeline-heading .loadmore-button,.user-profile .timeline-heading .alert{-ms-flex:1;flex:1\n}\n.user-profile .timeline-heading .loadmore-button{height:28px;margin:10px .6em\n}\n.user-profile .timeline-heading .title,.user-profile .timeline-heading .loadmore-text{display:none\n}\n.user-profile-placeholder .panel-body{display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:middle;align-items:middle;padding:7em\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/user_profile/user_profile.vue","\n.follow-card-content-container{-ms-flex-negative:0;flex-shrink:0;display:-ms-flexbox;display:flex;-ms-flex-direction:row;flex-direction:row;-ms-flex-pack:justify;justify-content:space-between;-ms-flex-wrap:wrap;flex-wrap:wrap;line-height:1.5em\n}\n.follow-card-content-container .btn{margin-top:0.5em;margin-left:auto;width:10em\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/follow_card/follow_card.vue","\n.basic-user-card{display:-ms-flexbox;display:flex;-ms-flex:1 0;flex:1 0;margin:0;padding-top:0.6em;padding-right:1em;padding-bottom:0.6em;padding-left:1em;border-bottom:1px solid;border-bottom-color:#222;border-bottom-color:var(--border, #222)\n}\n.basic-user-card-collapsed-content{margin-left:0.7em;text-align:left;-ms-flex:1;flex:1;min-width:0\n}\n.basic-user-card-user-name img{object-fit:contain;height:16px;width:16px;vertical-align:middle\n}\n.basic-user-card-expanded-content{-ms-flex:1;flex:1;margin-left:0.7em\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/basic_user_card/basic_user_card.vue",".with-load-more {\n &-footer {\n padding: 10px;\n text-align: center;\n\n .error {\n font-size: 14px;\n }\n }\n}\n\n\n// WEBPACK FOOTER //\n// webpack:///src/hocs/with_load_more/src/hocs/with_load_more/with_load_more.scss",".with-list {\n &-empty-content {\n text-align: center;\n padding: 10px;\n }\n}\n\n\n// WEBPACK FOOTER //\n// webpack:///src/hocs/with_list/src/hocs/with_list/with_list.scss","\n.setting-item{border-bottom:2px solid var(--fg, #182230);margin:1em 1em 1.4em;padding-bottom:1.4em\n}\n.setting-item>div{margin-bottom:.5em\n}\n.setting-item>div:last-child{margin-bottom:0\n}\n.setting-item:last-child{border-bottom:none;padding-bottom:0;margin-bottom:1em\n}\n.setting-item select{min-width:10em\n}\n.setting-item textarea{width:100%;height:100px\n}\n.setting-item .unavailable,.setting-item .unavailable i{color:var(--cRed, red);color:red\n}\n.setting-item .btn{min-height:28px;min-width:10em;padding:0 2em\n}\n.setting-item .number-input{max-width:6em\n}\n.select-multiple{display:-ms-flexbox;display:flex\n}\n.select-multiple .option-list{margin:0;padding-left:.5em\n}\n.setting-list,.option-list{list-style-type:none;padding-left:2em\n}\n.setting-list li,.option-list li{margin-bottom:0.5em\n}\n.setting-list .suboptions,.option-list .suboptions{margin-top:0.3em\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/settings/settings.vue","@import '../../_variables.scss';\n\n.tab-switcher {\n .contents {\n .hidden {\n display: none;\n }\n }\n .tabs {\n display: flex;\n position: relative;\n width: 100%;\n overflow-y: hidden;\n overflow-x: auto;\n padding-top: 5px;\n box-sizing: border-box;\n\n &::after, &::before {\n display: block;\n content: '';\n flex: 1 1 auto;\n border-bottom: 1px solid;\n border-bottom-color: $fallback--border;\n border-bottom-color: var(--border, $fallback--border);\n }\n\n .tab-wrapper {\n height: 28px;\n position: relative;\n display: flex;\n flex: 0 0 auto;\n\n .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: 6px - 99px;\n white-space: nowrap;\n\n &:not(.active) {\n z-index: 4;\n\n &:hover {\n z-index: 6;\n }\n }\n\n &.active {\n background: transparent;\n z-index: 5;\n }\n }\n\n &:not(.active) {\n &::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: $fallback--border;\n border-bottom-color: var(--border, $fallback--border);\n }\n }\n }\n\n }\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/tab_switcher/src/components/tab_switcher/tab_switcher.scss","\n.style-switcher .preset-switcher{margin-right:1em\n}\n.style-switcher .style-control{display:-ms-flexbox;display:flex;-ms-flex-align:baseline;align-items:baseline;margin-bottom:5px\n}\n.style-switcher .style-control .label{-ms-flex:1;flex:1\n}\n.style-switcher .style-control.disabled input:not(.exclude-disabled),.style-switcher .style-control.disabled select:not(.exclude-disabled){opacity:.5\n}\n.style-switcher .style-control input,.style-switcher .style-control select{min-width:3em;margin:0;-ms-flex:0;flex:0\n}\n.style-switcher .style-control input[type=color],.style-switcher .style-control select[type=color]{padding:1px;cursor:pointer;height:29px;min-width:2em;border:none;-ms-flex-item-align:stretch;-ms-grid-row-align:stretch;align-self:stretch\n}\n.style-switcher .style-control input[type=number],.style-switcher .style-control select[type=number]{min-width:5em\n}\n.style-switcher .style-control input[type=range],.style-switcher .style-control select[type=range]{-ms-flex:1;flex:1;min-width:3em\n}\n.style-switcher .style-control input[type=checkbox]+label,.style-switcher .style-control select[type=checkbox]+label{margin:6px 0\n}\n.style-switcher .style-control input:not([type=number]):not([type=text]),.style-switcher .style-control select:not([type=number]):not([type=text]){-ms-flex-item-align:start;align-self:flex-start\n}\n.style-switcher .tab-switcher{margin:0 -1em\n}\n.style-switcher .reset-container{-ms-flex-wrap:wrap;flex-wrap:wrap\n}\n.style-switcher .fonts-container,.style-switcher .reset-container,.style-switcher .apply-container,.style-switcher .radius-container,.style-switcher .color-container{display:-ms-flexbox;display:flex\n}\n.style-switcher .fonts-container,.style-switcher .radius-container{-ms-flex-direction:column;flex-direction:column\n}\n.style-switcher .color-container{-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:justify;justify-content:space-between\n}\n.style-switcher .color-container>h4{width:99%\n}\n.style-switcher .fonts-container,.style-switcher .color-container,.style-switcher .shadow-container,.style-switcher .radius-container,.style-switcher .presets-container{margin:1em 1em 0\n}\n.style-switcher .tab-header{display:-ms-flexbox;display:flex;-ms-flex-pack:justify;justify-content:space-between;-ms-flex-align:baseline;align-items:baseline;width:100%;min-height:30px;margin-bottom:1em\n}\n.style-switcher .tab-header .btn{min-width:1px;-ms-flex:0 auto;flex:0 auto;padding:0 1em\n}\n.style-switcher .tab-header p{-ms-flex:1;flex:1;margin:0;margin-right:.5em\n}\n.style-switcher .shadow-selector .override{-ms-flex:1;flex:1;margin-left:.5em\n}\n.style-switcher .shadow-selector .select-container{margin-top:-4px;margin-bottom:-3px\n}\n.style-switcher .save-load,.style-switcher .save-load-options{display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:baseline;align-items:baseline;-ms-flex-wrap:wrap;flex-wrap:wrap\n}\n.style-switcher .save-load .presets,.style-switcher .save-load .import-export,.style-switcher .save-load-options .presets,.style-switcher .save-load-options .import-export{margin-bottom:.5em\n}\n.style-switcher .save-load .import-export,.style-switcher .save-load-options .import-export{display:-ms-flexbox;display:flex\n}\n.style-switcher .save-load .override,.style-switcher .save-load-options .override{margin-left:.5em\n}\n.style-switcher .save-load-options{-ms-flex-wrap:wrap;flex-wrap:wrap;margin-top:.5em;-ms-flex-pack:center;justify-content:center\n}\n.style-switcher .save-load-options .keep-option{margin:0 .5em .5em;min-width:25%\n}\n.style-switcher .preview-container{border-top:1px dashed;border-bottom:1px dashed;border-color:#222;border-color:var(--border, #222);margin:1em -1em 0;padding:1em;background:var(--body-background-image);background-size:cover;background-position:50% 50%\n}\n.style-switcher .preview-container .dummy .post{font-family:var(--postFont);display:-ms-flexbox;display:flex\n}\n.style-switcher .preview-container .dummy .post .content{-ms-flex:1;flex:1\n}\n.style-switcher .preview-container .dummy .post .content h4{margin-bottom:.25em\n}\n.style-switcher .preview-container .dummy .post .content .icons{margin-top:.5em;display:-ms-flexbox;display:flex\n}\n.style-switcher .preview-container .dummy .post .content .icons i{margin-right:1em\n}\n.style-switcher .preview-container .dummy .after-post{margin-top:1em;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center\n}\n.style-switcher .preview-container .dummy .avatar,.style-switcher .preview-container .dummy .avatar-alt{background:linear-gradient(135deg, #b8e1fc 0%, #a9d2f3 10%, #90bae4 25%, #90bcea 37%, #90bff0 50%, #6ba8e5 51%, #a2daf5 83%, #bdf3fd 100%);color:black;font-family:sans-serif;text-align:center;margin-right:1em\n}\n.style-switcher .preview-container .dummy .avatar-alt{-ms-flex:0 auto;flex:0 auto;margin-left:28px;font-size:12px;min-width:20px;min-height:20px;line-height:20px;border-radius:10px;border-radius:var(--avatarAltRadius, 10px)\n}\n.style-switcher .preview-container .dummy .avatar{-ms-flex:0 auto;flex:0 auto;width:48px;height:48px;font-size:14px;line-height:48px\n}\n.style-switcher .preview-container .dummy .actions{display:-ms-flexbox;display:flex;-ms-flex-align:baseline;align-items:baseline\n}\n.style-switcher .preview-container .dummy .actions .checkbox{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:baseline;align-items:baseline;margin-right:1em;-ms-flex:1;flex:1\n}\n.style-switcher .preview-container .dummy .separator{margin:1em;border-bottom:1px solid;border-color:#222;border-color:var(--border, #222)\n}\n.style-switcher .preview-container .dummy .panel-heading .badge,.style-switcher .preview-container .dummy .panel-heading .alert,.style-switcher .preview-container .dummy .panel-heading .btn,.style-switcher .preview-container .dummy .panel-heading .faint{margin-left:1em;white-space:nowrap\n}\n.style-switcher .preview-container .dummy .panel-heading .faint{text-overflow:ellipsis;min-width:2em;overflow-x:hidden\n}\n.style-switcher .preview-container .dummy .panel-heading .flex-spacer{-ms-flex:1;flex:1\n}\n.style-switcher .preview-container .dummy .btn{margin-left:0;padding:0 1em;min-width:3em;min-height:30px\n}\n.style-switcher .apply-container{-ms-flex-pack:center;justify-content:center\n}\n.style-switcher .radius-item,.style-switcher .color-item{min-width:20em;margin:5px 6px 0 0;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-ms-flex:1 1 0px;flex:1 1 0\n}\n.style-switcher .radius-item.wide,.style-switcher .color-item.wide{min-width:60%\n}\n.style-switcher .radius-item:not(.wide):nth-child(2n+1),.style-switcher .color-item:not(.wide):nth-child(2n+1){margin-right:7px\n}\n.style-switcher .radius-item .color,.style-switcher .radius-item .opacity,.style-switcher .color-item .color,.style-switcher .color-item .opacity{display:-ms-flexbox;display:flex;-ms-flex-align:baseline;align-items:baseline\n}\n.style-switcher .radius-item{-ms-flex-preferred-size:auto;flex-basis:auto\n}\n.style-switcher .theme-radius-rn,.style-switcher .theme-color-cl{border:0;box-shadow:none;background:transparent;color:var(--faint, rgba(185,185,186,0.5));-ms-flex-item-align:stretch;-ms-grid-row-align:stretch;align-self:stretch\n}\n.style-switcher .theme-color-cl,.style-switcher .theme-radius-in,.style-switcher .theme-color-in{margin-left:4px\n}\n.style-switcher .theme-radius-in{min-width:1em\n}\n.style-switcher .theme-radius-in{max-width:7em;-ms-flex:1;flex:1\n}\n.style-switcher .theme-radius-lb{max-width:50em\n}\n.style-switcher .theme-preview-content{padding:20px\n}\n.style-switcher .btn{margin-left:.25em;margin-right:.25em\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/style_switcher/style_switcher.scss","\n.color-control input.text-input{max-width:7em;-ms-flex:1;flex:1\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/color_input/color_input.vue","\n.shadow-control{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:center;justify-content:center;margin-bottom:1em\n}\n.shadow-control .shadow-preview-container,.shadow-control .shadow-tweak{margin:5px 6px 0 0\n}\n.shadow-control .shadow-preview-container{-ms-flex:0;flex:0;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap\n}\n.shadow-control .shadow-preview-container input[type=number]{width:5em;min-width:2em\n}\n.shadow-control .shadow-preview-container .x-shift-control,.shadow-control .shadow-preview-container .y-shift-control{display:-ms-flexbox;display:flex;-ms-flex:0;flex:0\n}\n.shadow-control .shadow-preview-container .x-shift-control[disabled=disabled] *,.shadow-control .shadow-preview-container .y-shift-control[disabled=disabled] *{opacity:.5\n}\n.shadow-control .shadow-preview-container .x-shift-control{-ms-flex-align:start;align-items:flex-start\n}\n.shadow-control .shadow-preview-container .x-shift-control .wrap,.shadow-control .shadow-preview-container input[type=range]{margin:0;width:15em;height:2em\n}\n.shadow-control .shadow-preview-container .y-shift-control{-ms-flex-direction:column;flex-direction:column;-ms-flex-align:end;align-items:flex-end\n}\n.shadow-control .shadow-preview-container .y-shift-control .wrap{width:2em;height:15em\n}\n.shadow-control .shadow-preview-container .y-shift-control input[type=range]{transform-origin:1em 1em;transform:rotate(90deg)\n}\n.shadow-control .shadow-preview-container .preview-window{-ms-flex:1;flex:1;background-color:#999999;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;background-image:linear-gradient(45deg, #666 25%, transparent 25%),linear-gradient(-45deg, #666 25%, transparent 25%),linear-gradient(45deg, transparent 75%, #666 75%),linear-gradient(-45deg, transparent 75%, #666 75%);background-size:20px 20px;background-position:0 0, 0 10px, 10px -10px, -10px 0;border-radius:4px;border-radius:var(--inputRadius, 4px)\n}\n.shadow-control .shadow-preview-container .preview-window .preview-block{width:33%;height:33%;background-color:#121a24;background-color:var(--bg, #121a24);border-radius:10px;border-radius:var(--panelRadius, 10px)\n}\n.shadow-control .shadow-tweak{-ms-flex:1;flex:1;min-width:280px\n}\n.shadow-control .shadow-tweak .id-control{-ms-flex-align:stretch;align-items:stretch\n}\n.shadow-control .shadow-tweak .id-control .select,.shadow-control .shadow-tweak .id-control .btn{min-width:1px;margin-right:5px\n}\n.shadow-control .shadow-tweak .id-control .btn{padding:0 .4em;margin:0 .1em\n}\n.shadow-control .shadow-tweak .id-control .select{-ms-flex:1;flex:1\n}\n.shadow-control .shadow-tweak .id-control .select select{-ms-flex-item-align:initial;-ms-grid-row-align:initial;align-self:initial\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/shadow_control/shadow_control.vue","\n.font-control input.custom-font{min-width:10em\n}\n.font-control.custom .select{border-top-right-radius:0;border-bottom-right-radius:0\n}\n.font-control.custom .custom-font{border-top-left-radius:0;border-bottom-left-radius:0\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/font_control/font_control.vue","\n.contrast-ratio{display:-ms-flexbox;display:flex;-ms-flex-pack:end;justify-content:flex-end;margin-top:-4px;margin-bottom:5px\n}\n.contrast-ratio .label{margin-right:1em\n}\n.contrast-ratio .rating{display:inline-block;text-align:center\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/contrast_ratio/contrast_ratio.vue","\n.import-export-container{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-align:baseline;align-items:baseline;-ms-flex-pack:center;justify-content:center\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/export_import/export_import.vue","\n.registration-form{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;margin:0.6em\n}\n.registration-form .container{display:-ms-flexbox;display:flex;-ms-flex-direction:row;flex-direction:row\n}\n.registration-form .terms-of-service{-ms-flex:0 1 50%;flex:0 1 50%;margin:0.8em\n}\n.registration-form .text-fields{margin-top:0.6em;-ms-flex:1 0;flex:1 0;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column\n}\n.registration-form textarea{min-height:100px\n}\n.registration-form .form-group{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;padding:0.3em 0.0em 0.3em;line-height:24px;margin-bottom:1em\n}\n.registration-form .form-group--error{animation-name:shakeError;animation-duration:.6s;animation-timing-function:ease-in-out\n}\n.registration-form .form-group--error .form--label{color:#f04124;color:var(--cRed, #f04124)\n}\n.registration-form .form-error{margin-top:-0.7em;text-align:left\n}\n.registration-form .form-error span{font-size:12px\n}\n.registration-form .form-error ul{list-style:none;padding:0 0 0 5px;margin-top:0\n}\n.registration-form .form-error ul li::before{content:\"• \"\n}\n.registration-form form textarea{line-height:16px;resize:vertical\n}\n.registration-form .captcha{max-width:350px;margin-bottom:0.4em\n}\n.registration-form .btn{margin-top:0.6em;height:28px\n}\n.registration-form .error{text-align:center\n}\n@media all and (max-width: 800px){\n.registration-form .container{-ms-flex-direction:column-reverse;flex-direction:column-reverse\n}\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/registration/registration.vue","\n.profile-edit .bio{margin:0\n}\n.profile-edit input[type=file]{padding:5px;height:auto\n}\n.profile-edit .banner{max-width:100%\n}\n.profile-edit .uploading{font-size:1.5em;margin:0.25em\n}\n.profile-edit .name-changer{width:100%\n}\n.profile-edit .bg{max-width:100%\n}\n.profile-edit .current-avatar{display:block;width:150px;height:150px;border-radius:4px;border-radius:var(--avatarRadius, 4px)\n}\n.profile-edit .oauth-tokens{width:100%\n}\n.profile-edit .oauth-tokens th{text-align:left\n}\n.profile-edit .oauth-tokens .actions{text-align:right\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/user_settings/user_settings.vue","\n.image-cropper-img-input{display:none\n}\n.image-cropper-image-container{position:relative\n}\n.image-cropper-image-container img{display:block;max-width:100%\n}\n.image-cropper-buttons-wrapper{margin-top:15px\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/image_cropper/image_cropper.vue","/*!\n * Cropper.js v1.4.3\n * https://fengyuanchen.github.io/cropperjs\n *\n * Copyright 2015-present Chen Fengyuan\n * Released under the MIT license\n *\n * Date: 2018-10-24T13:07:11.429Z\n */\n\n.cropper-container {\n direction: ltr;\n font-size: 0;\n line-height: 0;\n position: relative;\n -ms-touch-action: none;\n touch-action: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n\n.cropper-container img {\n display: block;\n height: 100%;\n image-orientation: 0deg;\n max-height: none !important;\n max-width: none !important;\n min-height: 0 !important;\n min-width: 0 !important;\n width: 100%;\n}\n\n.cropper-wrap-box,\n.cropper-canvas,\n.cropper-drag-box,\n.cropper-crop-box,\n.cropper-modal {\n bottom: 0;\n left: 0;\n position: absolute;\n right: 0;\n top: 0;\n}\n\n.cropper-wrap-box,\n.cropper-canvas {\n overflow: hidden;\n}\n\n.cropper-drag-box {\n background-color: #fff;\n opacity: 0;\n}\n\n.cropper-modal {\n background-color: #000;\n opacity: .5;\n}\n\n.cropper-view-box {\n display: block;\n height: 100%;\n outline-color: rgba(51, 153, 255, 0.75);\n outline: 1px solid #39f;\n overflow: hidden;\n width: 100%;\n}\n\n.cropper-dashed {\n border: 0 dashed #eee;\n display: block;\n opacity: .5;\n position: absolute;\n}\n\n.cropper-dashed.dashed-h {\n border-bottom-width: 1px;\n border-top-width: 1px;\n height: calc(100% / 3);\n left: 0;\n top: calc(100% / 3);\n width: 100%;\n}\n\n.cropper-dashed.dashed-v {\n border-left-width: 1px;\n border-right-width: 1px;\n height: 100%;\n left: calc(100% / 3);\n top: 0;\n width: calc(100% / 3);\n}\n\n.cropper-center {\n display: block;\n height: 0;\n left: 50%;\n opacity: .75;\n position: absolute;\n top: 50%;\n width: 0;\n}\n\n.cropper-center:before,\n.cropper-center:after {\n background-color: #eee;\n content: ' ';\n display: block;\n position: absolute;\n}\n\n.cropper-center:before {\n height: 1px;\n left: -3px;\n top: 0;\n width: 7px;\n}\n\n.cropper-center:after {\n height: 7px;\n left: 0;\n top: -3px;\n width: 1px;\n}\n\n.cropper-face,\n.cropper-line,\n.cropper-point {\n display: block;\n height: 100%;\n opacity: .1;\n position: absolute;\n width: 100%;\n}\n\n.cropper-face {\n background-color: #fff;\n left: 0;\n top: 0;\n}\n\n.cropper-line {\n background-color: #39f;\n}\n\n.cropper-line.line-e {\n cursor: ew-resize;\n right: -3px;\n top: 0;\n width: 5px;\n}\n\n.cropper-line.line-n {\n cursor: ns-resize;\n height: 5px;\n left: 0;\n top: -3px;\n}\n\n.cropper-line.line-w {\n cursor: ew-resize;\n left: -3px;\n top: 0;\n width: 5px;\n}\n\n.cropper-line.line-s {\n bottom: -3px;\n cursor: ns-resize;\n height: 5px;\n left: 0;\n}\n\n.cropper-point {\n background-color: #39f;\n height: 5px;\n opacity: .75;\n width: 5px;\n}\n\n.cropper-point.point-e {\n cursor: ew-resize;\n margin-top: -3px;\n right: -3px;\n top: 50%;\n}\n\n.cropper-point.point-n {\n cursor: ns-resize;\n left: 50%;\n margin-left: -3px;\n top: -3px;\n}\n\n.cropper-point.point-w {\n cursor: ew-resize;\n left: -3px;\n margin-top: -3px;\n top: 50%;\n}\n\n.cropper-point.point-s {\n bottom: -3px;\n cursor: s-resize;\n left: 50%;\n margin-left: -3px;\n}\n\n.cropper-point.point-ne {\n cursor: nesw-resize;\n right: -3px;\n top: -3px;\n}\n\n.cropper-point.point-nw {\n cursor: nwse-resize;\n left: -3px;\n top: -3px;\n}\n\n.cropper-point.point-sw {\n bottom: -3px;\n cursor: nesw-resize;\n left: -3px;\n}\n\n.cropper-point.point-se {\n bottom: -3px;\n cursor: nwse-resize;\n height: 20px;\n opacity: 1;\n right: -3px;\n width: 20px;\n}\n\n@media (min-width: 768px) {\n .cropper-point.point-se {\n height: 15px;\n width: 15px;\n }\n}\n\n@media (min-width: 992px) {\n .cropper-point.point-se {\n height: 10px;\n width: 10px;\n }\n}\n\n@media (min-width: 1200px) {\n .cropper-point.point-se {\n height: 5px;\n opacity: .75;\n width: 5px;\n }\n}\n\n.cropper-point.point-se:before {\n background-color: #39f;\n bottom: -50%;\n content: ' ';\n display: block;\n height: 200%;\n opacity: 0;\n position: absolute;\n right: -50%;\n width: 200%;\n}\n\n.cropper-invisible {\n opacity: 0;\n}\n\n.cropper-bg {\n background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC');\n}\n\n.cropper-hide {\n display: block;\n height: 0;\n position: absolute;\n width: 0;\n}\n\n.cropper-hidden {\n display: none !important;\n}\n\n.cropper-move {\n cursor: move;\n}\n\n.cropper-crop {\n cursor: crosshair;\n}\n\n.cropper-disabled .cropper-drag-box,\n.cropper-disabled .cropper-face,\n.cropper-disabled .cropper-line,\n.cropper-disabled .cropper-point {\n cursor: not-allowed;\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///~/cropperjs/dist/cropper.css","\n.block-card-content-container{margin-top:0.5em;text-align:right\n}\n.block-card-content-container button{width:10em\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/block_card/block_card.vue",".with-subscription {\n &-loading {\n padding: 10px;\n text-align: center;\n\n .error {\n font-size: 14px;\n }\n }\n}\n\n\n// WEBPACK FOOTER //\n// webpack:///src/hocs/with_subscription/src/hocs/with_subscription/with_subscription.scss","\n.follow-request-card-content-container{display:-ms-flexbox;display:flex;-ms-flex-direction:row;flex-direction:row;-ms-flex-wrap:wrap;flex-wrap:wrap\n}\n.follow-request-card-content-container button{margin-top:0.5em;margin-right:0.5em;-ms-flex:1 1;flex:1 1;max-width:12em;min-width:8em\n}\n.follow-request-card-content-container button:last-child{margin-right:0\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/follow_request_card/follow_request_card.vue","\n.user-search-input-container{margin:0.5em;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center\n}\n.user-search-input-container .search-button{margin-left:0.5em\n}\n.loading-icon{padding:1em\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/user_search/user_search.vue","\n.notifications{padding-bottom:15em\n}\n.notifications .loadmore-error{color:#b9b9ba;color:var(--text, #b9b9ba)\n}\n.notifications .notification{position:relative\n}\n.notifications .notification .notification-overlay{position:absolute;top:0;right:0;left:0;bottom:0;pointer-events:none\n}\n.notifications .notification.unseen .notification-overlay{background-image:linear-gradient(135deg, var(--badgeNotification, red) 4px, transparent 10px)\n}\n.notification{box-sizing:border-box;display:-ms-flexbox;display:flex;border-bottom:1px solid;border-color:#222;border-color:var(--border, #222)\n}\n.notification:hover .animated.avatar canvas{display:none\n}\n.notification:hover .animated.avatar img{visibility:visible\n}\n.notification .non-mention{display:-ms-flexbox;display:flex;-ms-flex:1;flex:1;-ms-flex-wrap:nowrap;flex-wrap:nowrap;padding:0.6em;min-width:0\n}\n.notification .non-mention .avatar-container{width:32px;height:32px\n}\n.notification .non-mention .status-el{padding:0\n}\n.notification .non-mention .status-el .status{padding:0.25em 0;color:rgba(185,185,186,0.5);color:var(--faint, rgba(185,185,186,0.5))\n}\n.notification .non-mention .status-el .status a{color:var(--faintLink)\n}\n.notification .non-mention .status-el .media-body{margin:0\n}\n.notification .follow-text{padding:0.5em 0\n}\n.notification .status-el{-ms-flex:1;flex:1\n}\n.notification time{white-space:nowrap\n}\n.notification .notification-right{-ms-flex:1;flex:1;padding-left:0.8em;min-width:0\n}\n.notification .notification-details{min-width:0px;word-wrap:break-word;line-height:18px;position:relative;overflow:hidden;width:100%;-ms-flex:1 1 0px;flex:1 1 0;display:-ms-flexbox;display:flex;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-ms-flex-pack:justify;justify-content:space-between\n}\n.notification .notification-details .name-and-action{-ms-flex:1;flex:1;overflow:hidden;text-overflow:ellipsis\n}\n.notification .notification-details .username{font-weight:bolder;max-width:100%;text-overflow:ellipsis;white-space:nowrap\n}\n.notification .notification-details .username img{width:14px;height:14px;vertical-align:middle;object-fit:contain\n}\n.notification .notification-details .timeago{margin-right:.2em\n}\n.notification .notification-details .icon-retweet.lit{color:#0fa00f;color:var(--cGreen, #0fa00f)\n}\n.notification .notification-details .icon-user-plus.lit{color:#0095ff;color:var(--cBlue, #0095ff)\n}\n.notification .notification-details .icon-reply.lit{color:#0095ff;color:var(--cBlue, #0095ff)\n}\n.notification .notification-details .icon-star.lit{color:orange;color:orange;color:var(--cOrange, orange)\n}\n.notification .notification-details .status-content{margin:0;max-height:300px\n}\n.notification .notification-details h1{word-break:break-all;margin:0 0 0.3em;padding:0;font-size:1em;line-height:20px\n}\n.notification .notification-details h1 small{font-weight:lighter\n}\n.notification .notification-details p{margin:0;margin-top:0;margin-bottom:0.3em\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/notifications/notifications.scss","\n.login-form .btn{min-height:28px;width:10em\n}\n.login-form .register{-ms-flex:1 1;flex:1 1\n}\n.login-form .login-bottom{margin-top:1.0em;display:-ms-flexbox;display:flex;-ms-flex-direction:row;flex-direction:row;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between\n}\n.login .error{text-align:center;animation-name:shakeError;animation-duration:0.4s;animation-timing-function:ease-in-out\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/login_form/login_form.vue","\n.floating-chat{position:fixed;right:0px;bottom:0px;z-index:1000;max-width:25em\n}\n.chat-heading{cursor:pointer\n}\n.chat-heading .icon-comment-empty{color:#b9b9ba;color:var(--text, #b9b9ba)\n}\n.chat-window{overflow-y:auto;overflow-x:hidden;max-height:20em\n}\n.chat-window-container{height:100%\n}\n.chat-message{display:-ms-flexbox;display:flex;padding:0.2em 0.5em\n}\n.chat-avatar img{height:24px;width:24px;border-radius:4px;border-radius:var(--avatarRadius, 4px);margin-right:0.5em;margin-top:0.25em\n}\n.chat-input{display:-ms-flexbox;display:flex\n}\n.chat-input textarea{-ms-flex:1;flex:1;margin:0.6em;min-height:3.5em;resize:none\n}\n.chat-panel .title{display:-ms-flexbox;display:flex;-ms-flex-pack:justify;justify-content:space-between\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/chat_panel/chat_panel.vue","\n.features-panel li{line-height:24px\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/features_panel/features_panel.vue","\n.tos-content{margin:1em\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/terms_of_service_panel/terms_of_service_panel.vue","\n#app{min-height:100vh;max-width:100%;overflow:hidden\n}\n.app-bg-wrapper{position:fixed;z-index:-1;height:100%;width:100%;background-size:cover;background-repeat:no-repeat;background-position:0 50%\n}\ni{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none\n}\nh4{margin:0\n}\n#content{box-sizing:border-box;padding-top:60px;margin:auto;min-height:100vh;max-width:980px;background-color:rgba(0,0,0,0.15);-ms-flex-line-pack:start;align-content:flex-start\n}\n.text-center{text-align:center\n}\nbody{font-family:sans-serif;font-family:var(--interfaceFont, sans-serif);font-size:14px;margin:0;color:#b9b9ba;color:var(--text, #b9b9ba);max-width:100vw;overflow-x:hidden\n}\na{text-decoration:none;color:#d8a070;color:var(--link, #d8a070)\n}\nbutton{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;color:#b9b9ba;color:var(--btnText, #b9b9ba);background-color:#182230;background-color:var(--btn, #182230);border:none;border-radius:4px;border-radius:var(--btnRadius, 4px);cursor:pointer;box-shadow:0px 0px 2px 0px #000,0px 1px 0px 0px rgba(255,255,255,0.2) inset,0px -1px 0px 0px rgba(0,0,0,0.2) inset;box-shadow:var(--buttonShadow);font-size:14px;font-family:sans-serif;font-family:var(--interfaceFont, sans-serif)\n}\nbutton i[class*=icon-]{color:#b9b9ba;color:var(--btnText, #b9b9ba)\n}\nbutton::-moz-focus-inner{border:none\n}\nbutton:hover{box-shadow:0px 0px 4px rgba(255,255,255,0.3);box-shadow:var(--buttonHoverShadow)\n}\nbutton:active{box-shadow:0px 0px 4px 0px rgba(255,255,255,0.3),0px 1px 0px 0px rgba(0,0,0,0.2) inset,0px -1px 0px 0px rgba(255,255,255,0.2) inset;box-shadow:var(--buttonPressedShadow)\n}\nbutton:disabled{cursor:not-allowed;opacity:0.5\n}\nbutton.pressed{color:rgba(185,185,186,0.5);color:var(--faint, rgba(185,185,186,0.5));background-color:#121a24;background-color:var(--bg, #121a24)\n}\nlabel.select{padding:0\n}\ninput,textarea,.select{border:none;border-radius:4px;border-radius:var(--inputRadius, 4px);box-shadow:0px 1px 0px 0px rgba(0,0,0,0.2) inset,0px -1px 0px 0px rgba(255,255,255,0.2) inset,0px 0px 2px 0px #000 inset;box-shadow:var(--inputShadow);background-color:#182230;background-color:var(--input, #182230);color:#b9b9ba;color:var(--inputText, #b9b9ba);font-family:sans-serif;font-family:var(--inputFont, sans-serif);font-size:14px;padding:8px .5em;box-sizing:border-box;display:inline-block;position:relative;height:28px;line-height:16px;-webkit-hyphens:none;-ms-hyphens:none;hyphens:none\n}\ninput:disabled,input[disabled=disabled],textarea:disabled,textarea[disabled=disabled],.select:disabled,.select[disabled=disabled]{cursor:not-allowed;opacity:0.5\n}\ninput .icon-down-open,textarea .icon-down-open,.select .icon-down-open{position:absolute;top:0;bottom:0;right:5px;height:100%;color:#b9b9ba;color:var(--text, #b9b9ba);line-height:28px;z-index:0;pointer-events:none\n}\ninput select,textarea select,.select select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;border:none;color:#b9b9ba;color:var(--text, #b9b9ba);margin:0;padding:0 2em 0 .2em;font-family:sans-serif;font-family:var(--inputFont, sans-serif);font-size:14px;width:100%;z-index:1;height:28px;line-height:16px\n}\ninput[type=range],textarea[type=range],.select[type=range]{background:none;border:none;margin:0;box-shadow:none;-ms-flex:1;flex:1\n}\ninput[type=radio],input[type=checkbox],textarea[type=radio],textarea[type=checkbox],.select[type=radio],.select[type=checkbox]{display:none\n}\ninput[type=radio]:checked+label::before,input[type=checkbox]:checked+label::before,textarea[type=radio]:checked+label::before,textarea[type=checkbox]:checked+label::before,.select[type=radio]:checked+label::before,.select[type=checkbox]:checked+label::before{color:#b9b9ba;color:var(--text, #b9b9ba)\n}\ninput[type=radio]:disabled,input[type=radio]:disabled+label,input[type=radio]:disabled+label::before,input[type=checkbox]:disabled,input[type=checkbox]:disabled+label,input[type=checkbox]:disabled+label::before,textarea[type=radio]:disabled,textarea[type=radio]:disabled+label,textarea[type=radio]:disabled+label::before,textarea[type=checkbox]:disabled,textarea[type=checkbox]:disabled+label,textarea[type=checkbox]:disabled+label::before,.select[type=radio]:disabled,.select[type=radio]:disabled+label,.select[type=radio]:disabled+label::before,.select[type=checkbox]:disabled,.select[type=checkbox]:disabled+label,.select[type=checkbox]:disabled+label::before{opacity:.5\n}\ninput[type=radio]+label::before,input[type=checkbox]+label::before,textarea[type=radio]+label::before,textarea[type=checkbox]+label::before,.select[type=radio]+label::before,.select[type=checkbox]+label::before{display:inline-block;content:'✔';transition:color 200ms;width:1.1em;height:1.1em;border-radius:2px;border-radius:var(--checkboxRadius, 2px);box-shadow:0px 0px 2px black inset;box-shadow:var(--inputShadow);margin-right:.5em;background-color:#182230;background-color:var(--input, #182230);vertical-align:top;text-align:center;line-height:1.1em;font-size:1.1em;box-sizing:border-box;color:transparent;overflow:hidden;box-sizing:border-box\n}\noption{color:#b9b9ba;color:var(--text, #b9b9ba);background-color:#121a24;background-color:var(--bg, #121a24)\n}\ni[class*=icon-]{color:#666;color:var(--icon, #666)\n}\n.container{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin:0;padding:0 10px 0 10px\n}\n.item{-ms-flex:1;flex:1;line-height:50px;height:50px;overflow:hidden;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap\n}\n.item .nav-icon{margin-left:0.4em\n}\n.item.right{-ms-flex-pack:end;justify-content:flex-end\n}\n.auto-size{-ms-flex:1;flex:1\n}\n.nav-bar{padding:0;width:100%;-ms-flex-align:center;align-items:center;position:fixed;height:50px\n}\n.nav-bar .logo{display:-ms-flexbox;display:flex;position:absolute;top:0;bottom:0;left:0;right:0;-ms-flex-align:stretch;align-items:stretch;-ms-flex-pack:center;justify-content:center;-ms-flex:0 0 auto;flex:0 0 auto;z-index:-1;transition:opacity;transition-timing-function:ease-out;transition-duration:100ms\n}\n.nav-bar .logo .mask{-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-position:center;mask-position:center;-webkit-mask-size:contain;mask-size:contain;background-color:#182230;background-color:var(--topBarText, #182230);position:absolute;top:0;bottom:0;left:0;right:0\n}\n.nav-bar .logo img{height:100%;object-fit:contain;display:block;-ms-flex:0;flex:0\n}\n.nav-bar .inner-nav{margin:auto;box-sizing:border-box;padding-left:10px;padding-right:10px;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-preferred-size:970px;flex-basis:970px;height:50px\n}\n.nav-bar .inner-nav a,.nav-bar .inner-nav a i{color:#d8a070;color:var(--topBarLink, #d8a070)\n}\nmain-router{-ms-flex:1;flex:1\n}\n.status.compact{color:rgba(0,0,0,0.42);font-weight:300\n}\n.status.compact p{margin:0;font-size:0.8em\n}\n.panel{display:-ms-flexbox;display:flex;position:relative;-ms-flex-direction:column;flex-direction:column;margin:0.5em;background-color:#121a24;background-color:var(--bg, #121a24)\n}\n.panel::after,.panel{border-radius:10px;border-radius:var(--panelRadius, 10px)\n}\n.panel::after{content:'';position:absolute;top:0;bottom:0;left:0;right:0;pointer-events:none;box-shadow:1px 1px 4px rgba(0,0,0,0.6);box-shadow:var(--panelShadow)\n}\n.panel-body:empty::before{content:\"¯\\\\_(ツ)_/¯\";display:block;margin:1em;text-align:center\n}\n.panel-heading{display:-ms-flexbox;display:flex;border-radius:10px 10px 0 0;border-radius:var(--panelRadius, 10px) var(--panelRadius, 10px) 0 0;background-size:cover;padding:.6em .6em;text-align:left;line-height:28px;color:var(--panelText);background-color:#182230;background-color:var(--panel, #182230);-ms-flex-align:baseline;align-items:baseline;box-shadow:var(--panelHeaderShadow)\n}\n.panel-heading .title{-ms-flex:1 0 auto;flex:1 0 auto;font-size:1.3em\n}\n.panel-heading .faint{background-color:transparent;color:rgba(185,185,186,0.5);color:var(--panelFaint, rgba(185,185,186,0.5))\n}\n.panel-heading .alert{white-space:nowrap;text-overflow:ellipsis;overflow-x:hidden\n}\n.panel-heading button{-ms-flex-negative:0;flex-shrink:0\n}\n.panel-heading button,.panel-heading .alert{line-height:21px;min-height:0;box-sizing:border-box;margin:0;margin-left:.25em;min-width:1px;-ms-flex-item-align:stretch;-ms-grid-row-align:stretch;align-self:stretch\n}\n.panel-heading a{color:#d8a070;color:var(--panelLink, #d8a070)\n}\n.panel-heading.stub{border-radius:10px;border-radius:var(--panelRadius, 10px)\n}\n.panel-footer{border-radius:0 0 10px 10px;border-radius:0 0 var(--panelRadius, 10px) var(--panelRadius, 10px)\n}\n.panel-footer .faint{color:rgba(185,185,186,0.5);color:var(--panelFaint, rgba(185,185,186,0.5))\n}\n.panel-footer a{color:#d8a070;color:var(--panelLink, #d8a070)\n}\n.panel-body>p{line-height:18px;padding:1em;margin:0\n}\n.container>*{min-width:0px\n}\n.fa{color:grey\n}\nnav{z-index:1000;color:var(--topBarText);background-color:#182230;background-color:var(--topBar, #182230);color:rgba(185,185,186,0.5);color:var(--faint, rgba(185,185,186,0.5));box-shadow:0px 0px 4px rgba(0,0,0,0.6);box-shadow:var(--topBarShadow)\n}\nnav .back-button{display:block;max-width:99px;transition-property:opacity, max-width;transition-duration:300ms;transition-timing-function:ease-out\n}\nnav .back-button i{margin:0 1em\n}\nnav .back-button.hidden{opacity:0;max-width:5px\n}\n.menu-button{display:none;position:relative\n}\n.alert-dot{border-radius:100%;height:8px;width:8px;position:absolute;left:calc(50% - 4px);top:calc(50% - 4px);margin-left:6px;margin-top:-6px;background-color:red;background-color:var(--badgeNotification, red)\n}\n.fade-enter-active,.fade-leave-active{transition:opacity .2s\n}\n.fade-enter,.fade-leave-active{opacity:0\n}\n.main{-ms-flex-preferred-size:50%;flex-basis:50%;-ms-flex-positive:1;flex-grow:1;-ms-flex-negative:1;flex-shrink:1\n}\n.sidebar-bounds{-ms-flex:0;flex:0;-ms-flex-preferred-size:35%;flex-basis:35%\n}\n.sidebar-flexer{-ms-flex:1;flex:1;-ms-flex-preferred-size:345px;flex-basis:345px;width:365px\n}\n.mobile-shown{display:none\n}\n.panel-switcher{display:none;width:100%;height:46px\n}\n.panel-switcher button{display:block;-ms-flex:1;flex:1;max-height:32px;margin:0.5em;padding:0.5em\n}\n@media all and (min-width: 800px){\nbody{overflow-y:scroll\n}\nnav .back-button{display:none\n}\n.sidebar-bounds{overflow:hidden;max-height:100vh;width:345px;position:fixed;margin-top:-10px\n}\n.sidebar-bounds .sidebar-scroller{height:96vh;width:365px;padding-top:10px;padding-right:50px;overflow-x:hidden;overflow-y:scroll\n}\n.sidebar-bounds .sidebar{width:345px\n}\n.sidebar-flexer{max-height:96vh;-ms-flex-negative:0;flex-shrink:0;-ms-flex-positive:0;flex-grow:0\n}\n}\n.badge{display:inline-block;border-radius:99px;min-width:22px;max-width:22px;min-height:22px;max-height:22px;font-size:15px;line-height:22px;text-align:center;vertical-align:middle;white-space:nowrap;padding:0\n}\n.badge.badge-notification{background-color:red;background-color:var(--badgeNotification, red);color:white;color:var(--badgeNotificationText, #fff)\n}\n.alert{margin:0.35em;padding:0.25em;border-radius:5px;border-radius:var(--tooltipRadius, 5px);min-height:28px;line-height:28px\n}\n.alert.error{background-color:rgba(211,16,20,0.5);background-color:var(--alertError, rgba(211,16,20,0.5));color:#b9b9ba;color:var(--alertErrorText, #b9b9ba)\n}\n.panel-heading .alert.error{color:#b9b9ba;color:var(--alertErrorPanelText, #b9b9ba)\n}\n.faint{color:rgba(185,185,186,0.5);color:var(--faint, rgba(185,185,186,0.5))\n}\n.faint-link{color:rgba(185,185,186,0.5);color:var(--faint, rgba(185,185,186,0.5))\n}\n.faint-link:hover{text-decoration:underline\n}\n@media all and (min-width: 800px){\n.logo{opacity:1 !important\n}\n}\n.item.right{text-align:right\n}\n.visibility-tray{font-size:1.2em;padding:3px;cursor:pointer\n}\n.visibility-tray .selected{color:#b9b9ba;color:var(--lightText, #b9b9ba)\n}\n.visibility-tray div{padding-top:5px\n}\n.visibility-notice{padding:.5em;border:1px solid rgba(185,185,186,0.5);border:1px solid var(--faint, rgba(185,185,186,0.5));border-radius:4px;border-radius:var(--inputRadius, 4px)\n}\n@keyframes modal-background-fadein{\nfrom{background-color:transparent\n}\nto{background-color:rgba(0,0,0,0.5)\n}\n}\n.modal-view{z-index:1000;position:fixed;top:0;left:0;right:0;bottom:0;display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center;overflow:auto;animation-duration:0.2s;background-color:rgba(0,0,0,0.5);animation-name:modal-background-fadein\n}\n.button-icon{font-size:1.2em\n}\n@keyframes shakeError{\n0%{transform:translateX(0)\n}\n15%{transform:translateX(0.375rem)\n}\n30%{transform:translateX(-0.375rem)\n}\n45%{transform:translateX(0.375rem)\n}\n60%{transform:translateX(-0.375rem)\n}\n75%{transform:translateX(0.375rem)\n}\n90%{transform:translateX(-0.375rem)\n}\n100%{transform:translateX(0)\n}\n}\n@media all and (max-width: 800px){\n.mobile-hidden{display:none\n}\n.panel-switcher{display:-ms-flexbox;display:flex\n}\n.container{padding:0\n}\n.panel{margin:0.5em 0 0.5em 0\n}\n.menu-button{display:block;margin-right:0.8em\n}\n}\n.login-hint{text-align:center\n}\n@media all and (min-width: 801px){\n.login-hint{display:none\n}\n}\n.login-hint a{display:inline-block;padding:1em 0px;width:100%\n}\n.btn.btn-default{min-height:28px\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/App.scss","\n.nav-panel .panel{overflow:hidden;box-shadow:var(--panelShadow)\n}\n.nav-panel ul{list-style:none;margin:0;padding:0\n}\n.follow-request-count{margin:-6px 10px;background-color:#121a24;background-color:var(--input, rgba(185,185,186,0.5))\n}\n.nav-panel li{border-bottom:1px solid;border-color:#222;border-color:var(--border, #222);padding:0\n}\n.nav-panel li:first-child a{border-top-right-radius:10px;border-top-right-radius:var(--panelRadius, 10px);border-top-left-radius:10px;border-top-left-radius:var(--panelRadius, 10px)\n}\n.nav-panel li:last-child a{border-bottom-right-radius:10px;border-bottom-right-radius:var(--panelRadius, 10px);border-bottom-left-radius:10px;border-bottom-left-radius:var(--panelRadius, 10px)\n}\n.nav-panel li:last-child{border:none\n}\n.nav-panel a{display:block;padding:0.8em 0.85em\n}\n.nav-panel a:hover{background-color:#151e2a;background-color:var(--lightBg, #151e2a)\n}\n.nav-panel a.router-link-active{font-weight:bolder;background-color:#151e2a;background-color:var(--lightBg, #151e2a)\n}\n.nav-panel a.router-link-active:hover{text-decoration:underline\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/nav_panel/nav_panel.vue","\n.user-finder-container{max-width:100%;display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:baseline;align-items:baseline;vertical-align:baseline\n}\n.user-finder-container .user-finder-input,.user-finder-container .search-button{height:29px\n}\n.user-finder-container .user-finder-input{max-width:calc(100% - 30px - 30px - 20px)\n}\n.user-finder-container .search-button{margin-left:.5em;margin-right:.5em\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/user_finder/user_finder.vue","\n.who-to-follow *{vertical-align:middle\n}\n.who-to-follow img{width:32px;height:32px\n}\n.who-to-follow{padding:0.5em 1em 0.5em 1em;margin:0px;line-height:40px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/who_to_follow_panel/who_to_follow_panel.vue","\n.media-modal-view:hover .modal-view-button-arrow{opacity:0.75\n}\n.media-modal-view:hover .modal-view-button-arrow:focus,.media-modal-view:hover .modal-view-button-arrow:hover{outline:none;box-shadow:none\n}\n.media-modal-view:hover .modal-view-button-arrow:hover{opacity:1\n}\n.modal-image{max-width:90%;max-height:90%;box-shadow:0px 5px 15px 0 rgba(0,0,0,0.5)\n}\n.modal-view-button-arrow{position:absolute;display:block;top:50%;margin-top:-50px;width:70px;height:100px;border:0;padding:0;opacity:0;box-shadow:none;background:none;-webkit-appearance:none;-moz-appearance:none;appearance:none;overflow:visible;cursor:pointer;transition:opacity 333ms cubic-bezier(0.4, 0, 0.22, 1)\n}\n.modal-view-button-arrow .arrow-icon{position:absolute;top:35px;height:30px;width:32px;font-size:14px;line-height:30px;color:#FFF;text-align:center;background-color:rgba(0,0,0,0.3)\n}\n.modal-view-button-arrow--prev{left:0\n}\n.modal-view-button-arrow--prev .arrow-icon{left:6px\n}\n.modal-view-button-arrow--next{right:0\n}\n.modal-view-button-arrow--next .arrow-icon{right:6px\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/media_modal/media_modal.vue","\n.side-drawer-container{position:fixed;z-index:1000;top:0;left:0;width:100%;height:100%;display:-ms-flexbox;display:flex;-ms-flex-align:stretch;align-items:stretch\n}\n.side-drawer-container-open{transition:0.35s;transition-property:background-color;background-color:rgba(0,0,0,0.5)\n}\n.side-drawer-container-closed{left:-100%;background-color:transparent\n}\n.side-drawer-click-outside{-ms-flex:1 1 100%;flex:1 1 100%\n}\n.side-drawer{overflow-x:hidden;transition:0.35s;transition-timing-function:cubic-bezier(0, 1, 0.5, 1);margin:0 0 0 -100px;padding:0 0 1em 100px;width:80%;max-width:20em;-ms-flex:0 0 80%;flex:0 0 80%;box-shadow:1px 1px 4px rgba(0,0,0,0.6);box-shadow:var(--panelShadow);background-color:#121a24;background-color:var(--bg, #121a24)\n}\n.side-drawer-logo-wrapper{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;padding:0.85em\n}\n.side-drawer-logo-wrapper img{-ms-flex:none;flex:none;height:50px;margin-right:0.85em\n}\n.side-drawer-logo-wrapper span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap\n}\n.side-drawer-click-outside-closed{-ms-flex:0 0 0px;flex:0 0 0\n}\n.side-drawer-closed{transform:translate(-100%)\n}\n.side-drawer-heading{background:transparent;-ms-flex-direction:column;flex-direction:column;-ms-flex-align:stretch;align-items:stretch;display:-ms-flexbox;display:flex;padding:0;margin:0\n}\n.side-drawer ul{list-style:none;margin:0;padding:0;border-bottom:1px solid;border-color:#222;border-color:var(--border, #222);margin:0.2em 0\n}\n.side-drawer ul:last-child{border:0\n}\n.side-drawer li{padding:0\n}\n.side-drawer li a{display:block;padding:0.5em 0.85em\n}\n.side-drawer li a:hover{background-color:#151e2a;background-color:var(--lightBg, #151e2a)\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/side_drawer/side_drawer.vue","\n.post-form-modal-view{max-height:100%;display:block\n}\n.post-form-modal-panel{-ms-flex-negative:0;flex-shrink:0;margin:25% 0 4em 0;width:100%\n}\n.new-status-button{width:5em;height:5em;border-radius:100%;position:fixed;bottom:1.5em;right:1.5em;background-color:#182230;background-color:var(--btn, #182230);display:-ms-flexbox;display:flex;-ms-flex-pack:center;justify-content:center;-ms-flex-align:center;align-items:center;box-shadow:0px 2px 2px rgba(0,0,0,0.3),0px 4px 6px rgba(0,0,0,0.3);z-index:10;transition:0.35s transform;transition-timing-function:cubic-bezier(0, 1, 0.5, 1)\n}\n.new-status-button.hidden{transform:translateY(150%)\n}\n.new-status-button i{font-size:1.5em;color:#b9b9ba;color:var(--text, #b9b9ba)\n}\n@media all and (min-width: 801px){\n.new-status-button{display:none\n}\n}\n\n\n\n// WEBPACK FOOTER //\n// webpack:///src/components/mobile_post_status_modal/mobile_post_status_modal.vue"],"sourceRoot":""} \ No newline at end of file diff --git a/priv/static/static/font/LICENSE.txt b/priv/static/static/font/LICENSE.txt old mode 100755 new mode 100644 diff --git a/priv/static/static/font/README.txt b/priv/static/static/font/README.txt old mode 100755 new mode 100644 diff --git a/priv/static/static/font/config.json b/priv/static/static/font/config.json old mode 100755 new mode 100644 index d72b622c0..b73f2ad40 --- a/priv/static/static/font/config.json +++ b/priv/static/static/font/config.json @@ -239,6 +239,18 @@ "css": "pencil", "code": 59416, "src": "fontawesome" + }, + { + "uid": "671f29fa10dda08074a4c6a341bb4f39", + "css": "bell-alt", + "code": 61683, + "src": "fontawesome" + }, + { + "uid": "5bb103cd29de77e0e06a52638527b575", + "css": "wrench", + "code": 59418, + "src": "fontawesome" } ] } \ No newline at end of file diff --git a/priv/static/static/font/css/fontello-codes.css b/priv/static/static/font/css/fontello-codes.css index 49175c8fe..b57c56203 100755 Binary files a/priv/static/static/font/css/fontello-codes.css and b/priv/static/static/font/css/fontello-codes.css differ diff --git a/priv/static/static/font/css/fontello-embedded.css b/priv/static/static/font/css/fontello-embedded.css index c43ad321d..c69c8b9f6 100755 Binary files a/priv/static/static/font/css/fontello-embedded.css and b/priv/static/static/font/css/fontello-embedded.css differ diff --git a/priv/static/static/font/css/fontello-ie7-codes.css b/priv/static/static/font/css/fontello-ie7-codes.css index 56e114470..981463a84 100755 Binary files a/priv/static/static/font/css/fontello-ie7-codes.css and b/priv/static/static/font/css/fontello-ie7-codes.css differ diff --git a/priv/static/static/font/css/fontello-ie7.css b/priv/static/static/font/css/fontello-ie7.css index edced9cb6..c2e8bc242 100755 Binary files a/priv/static/static/font/css/fontello-ie7.css and b/priv/static/static/font/css/fontello-ie7.css differ diff --git a/priv/static/static/font/css/fontello.css b/priv/static/static/font/css/fontello.css index 64a7a938e..fc23c41aa 100755 Binary files a/priv/static/static/font/css/fontello.css and b/priv/static/static/font/css/fontello.css differ diff --git a/priv/static/static/font/demo.html b/priv/static/static/font/demo.html index 2c89a505d..1a1147afd 100755 --- a/priv/static/static/font/demo.html +++ b/priv/static/static/font/demo.html @@ -229,11 +229,11 @@ body { } @font-face { font-family: 'fontello'; - src: url('./font/fontello.eot?50378338'); - src: url('./font/fontello.eot?50378338#iefix') format('embedded-opentype'), - url('./font/fontello.woff?50378338') format('woff'), - url('./font/fontello.ttf?50378338') format('truetype'), - url('./font/fontello.svg?50378338#fontello') format('svg'); + src: url('./font/fontello.eot?60799712'); + src: url('./font/fontello.eot?60799712#iefix') format('embedded-opentype'), + url('./font/fontello.woff?60799712') format('woff'), + url('./font/fontello.ttf?60799712') format('truetype'), + url('./font/fontello.svg?60799712#fontello') format('svg'); font-weight: normal; font-style: normal; } @@ -335,24 +335,29 @@ body {
icon-pencil0xe818
+
icon-verified0xe819
+
icon-wrench0xe81a
icon-spin30xe832
+
+
icon-spin40xe834
icon-link-ext0xf08e
-
-
icon-link-ext-alt0xf08f
icon-menu0xf0c9
-
icon-mail-alt0xf0e0
-
icon-comment-empty0xf0e5
+
icon-mail-alt0xf0e0
+
icon-comment-empty0xf0e5
+
icon-bell-alt0xf0f3
icon-plus-squared0xf0fe
+
+
icon-reply0xf112
icon-lock-open-alt0xf13e
icon-play-circled0xf144
+
icon-thumbs-up-alt0xf164
-
icon-thumbs-up-alt0xf164
icon-binoculars0xf1e5
icon-user-plus0xf234
diff --git a/priv/static/static/font/font/fontello.eot b/priv/static/static/font/font/fontello.eot index a72671b0d..b9cdfcb5d 100755 Binary files a/priv/static/static/font/font/fontello.eot and b/priv/static/static/font/font/fontello.eot differ diff --git a/priv/static/static/font/font/fontello.svg b/priv/static/static/font/font/fontello.svg index 91aba5ef6..0e460ea5e 100755 --- a/priv/static/static/font/font/fontello.svg +++ b/priv/static/static/font/font/fontello.svg @@ -56,6 +56,10 @@ + + + + @@ -70,6 +74,8 @@ + + diff --git a/priv/static/static/font/font/fontello.ttf b/priv/static/static/font/font/fontello.ttf index 9d36bc118..f1b9f19d2 100755 Binary files a/priv/static/static/font/font/fontello.ttf and b/priv/static/static/font/font/fontello.ttf differ diff --git a/priv/static/static/font/font/fontello.woff b/priv/static/static/font/font/fontello.woff index 35eea15d7..141abc65a 100755 Binary files a/priv/static/static/font/font/fontello.woff and b/priv/static/static/font/font/fontello.woff differ diff --git a/priv/static/static/font/font/fontello.woff2 b/priv/static/static/font/font/fontello.woff2 index c88c4b24f..efed5cf73 100755 Binary files a/priv/static/static/font/font/fontello.woff2 and b/priv/static/static/font/font/fontello.woff2 differ diff --git a/priv/static/static/img/nsfw.74818f9.png b/priv/static/static/img/nsfw.74818f9.png index e32525aa5..d25137767 100644 Binary files a/priv/static/static/img/nsfw.74818f9.png and b/priv/static/static/img/nsfw.74818f9.png differ diff --git a/priv/static/static/js/app.77434de4e756a5d79672.js b/priv/static/static/js/app.77434de4e756a5d79672.js deleted file mode 100644 index df6755cb4..000000000 Binary files a/priv/static/static/js/app.77434de4e756a5d79672.js and /dev/null differ diff --git a/priv/static/static/js/app.77434de4e756a5d79672.js.map b/priv/static/static/js/app.77434de4e756a5d79672.js.map deleted file mode 100644 index 5f68977a7..000000000 Binary files a/priv/static/static/js/app.77434de4e756a5d79672.js.map and /dev/null differ diff --git a/priv/static/static/js/app.c914d9a57d5da7aa5553.js b/priv/static/static/js/app.c914d9a57d5da7aa5553.js new file mode 100644 index 000000000..e7b09c97e Binary files /dev/null and b/priv/static/static/js/app.c914d9a57d5da7aa5553.js differ diff --git a/priv/static/static/js/app.c914d9a57d5da7aa5553.js.map b/priv/static/static/js/app.c914d9a57d5da7aa5553.js.map new file mode 100644 index 000000000..f469d271c Binary files /dev/null and b/priv/static/static/js/app.c914d9a57d5da7aa5553.js.map differ diff --git a/priv/static/static/js/manifest.0b2f423dda42f0dbbf65.js b/priv/static/static/js/manifest.0b2f423dda42f0dbbf65.js deleted file mode 100644 index ecc4a13d3..000000000 Binary files a/priv/static/static/js/manifest.0b2f423dda42f0dbbf65.js and /dev/null differ diff --git a/priv/static/static/js/manifest.bf15f24d205c8cf4ee4a.js b/priv/static/static/js/manifest.bf15f24d205c8cf4ee4a.js new file mode 100644 index 000000000..b6de44a86 Binary files /dev/null and b/priv/static/static/js/manifest.bf15f24d205c8cf4ee4a.js differ diff --git a/priv/static/static/js/manifest.0b2f423dda42f0dbbf65.js.map b/priv/static/static/js/manifest.bf15f24d205c8cf4ee4a.js.map similarity index 92% rename from priv/static/static/js/manifest.0b2f423dda42f0dbbf65.js.map rename to priv/static/static/js/manifest.bf15f24d205c8cf4ee4a.js.map index 8aabe15dd..c0cd90ac0 100644 Binary files a/priv/static/static/js/manifest.0b2f423dda42f0dbbf65.js.map and b/priv/static/static/js/manifest.bf15f24d205c8cf4ee4a.js.map differ diff --git a/priv/static/static/js/vendor.0d1eeaf25aa1d2fc51b0.js b/priv/static/static/js/vendor.0d1eeaf25aa1d2fc51b0.js new file mode 100644 index 000000000..7e0119cc8 Binary files /dev/null and b/priv/static/static/js/vendor.0d1eeaf25aa1d2fc51b0.js differ diff --git a/priv/static/static/js/vendor.0d1eeaf25aa1d2fc51b0.js.map b/priv/static/static/js/vendor.0d1eeaf25aa1d2fc51b0.js.map new file mode 100644 index 000000000..ddc023b43 Binary files /dev/null and b/priv/static/static/js/vendor.0d1eeaf25aa1d2fc51b0.js.map differ diff --git a/priv/static/static/js/vendor.e4475fde034685231799.js b/priv/static/static/js/vendor.e4475fde034685231799.js deleted file mode 100644 index 989b44b7a..000000000 Binary files a/priv/static/static/js/vendor.e4475fde034685231799.js and /dev/null differ diff --git a/priv/static/static/js/vendor.e4475fde034685231799.js.map b/priv/static/static/js/vendor.e4475fde034685231799.js.map deleted file mode 100644 index 6813973f8..000000000 Binary files a/priv/static/static/js/vendor.e4475fde034685231799.js.map and /dev/null differ diff --git a/priv/static/static/logo.png b/priv/static/static/logo.png index c3c92914b..7744b1acc 100644 Binary files a/priv/static/static/logo.png and b/priv/static/static/logo.png differ diff --git a/priv/static/sw-pleroma.js b/priv/static/sw-pleroma.js index b69b1b7e7..3c00d22d5 100644 Binary files a/priv/static/sw-pleroma.js and b/priv/static/sw-pleroma.js differ diff --git a/test/activity_test.exs b/test/activity_test.exs index ad889f544..7e91d534b 100644 --- a/test/activity_test.exs +++ b/test/activity_test.exs @@ -5,6 +5,7 @@ defmodule Pleroma.ActivityTest do use Pleroma.DataCase alias Pleroma.Activity + alias Pleroma.Bookmark import Pleroma.Factory test "returns an activity by it's AP id" do @@ -28,4 +29,48 @@ test "returns the activity that created an object" do assert activity == found_activity end + + test "preloading a bookmark" do + user = insert(:user) + user2 = insert(:user) + user3 = insert(:user) + activity = insert(:note_activity) + {:ok, _bookmark} = Bookmark.create(user.id, activity.id) + {:ok, _bookmark2} = Bookmark.create(user2.id, activity.id) + {:ok, bookmark3} = Bookmark.create(user3.id, activity.id) + + queried_activity = + Ecto.Query.from(Pleroma.Activity) + |> Activity.with_preloaded_bookmark(user3) + |> Repo.one() + + assert queried_activity.bookmark == bookmark3 + end + + describe "getting a bookmark" do + test "when association is loaded" do + user = insert(:user) + activity = insert(:note_activity) + {:ok, bookmark} = Bookmark.create(user.id, activity.id) + + queried_activity = + Ecto.Query.from(Pleroma.Activity) + |> Activity.with_preloaded_bookmark(user) + |> Repo.one() + + assert Activity.get_bookmark(queried_activity, user) == bookmark + end + + test "when association is not loaded" do + user = insert(:user) + activity = insert(:note_activity) + {:ok, bookmark} = Bookmark.create(user.id, activity.id) + + queried_activity = + Ecto.Query.from(Pleroma.Activity) + |> Repo.one() + + assert Activity.get_bookmark(queried_activity, user) == bookmark + end + end end diff --git a/test/bbs/handler_test.exs b/test/bbs/handler_test.exs new file mode 100644 index 000000000..7d5d68d11 --- /dev/null +++ b/test/bbs/handler_test.exs @@ -0,0 +1,83 @@ +defmodule Pleroma.BBS.HandlerTest do + use Pleroma.DataCase + alias Pleroma.Activity + alias Pleroma.BBS.Handler + alias Pleroma.Object + alias Pleroma.Repo + alias Pleroma.User + alias Pleroma.Web.CommonAPI + + import ExUnit.CaptureIO + import Pleroma.Factory + import Ecto.Query + + test "getting the home timeline" do + user = insert(:user) + followed = insert(:user) + + {:ok, user} = User.follow(user, followed) + + {:ok, _first} = CommonAPI.post(user, %{"status" => "hey"}) + {:ok, _second} = CommonAPI.post(followed, %{"status" => "hello"}) + + output = + capture_io(fn -> + Handler.handle_command(%{user: user}, "home") + end) + + assert output =~ user.nickname + assert output =~ followed.nickname + + assert output =~ "hey" + assert output =~ "hello" + end + + test "posting" do + user = insert(:user) + + output = + capture_io(fn -> + Handler.handle_command(%{user: user}, "p this is a test post") + end) + + assert output =~ "Posted" + + activity = + Repo.one( + from(a in Activity, + where: fragment("?->>'type' = ?", a.data, "Create") + ) + ) + + assert activity.actor == user.ap_id + object = Object.normalize(activity) + assert object.data["content"] == "this is a test post" + end + + test "replying" do + user = insert(:user) + another_user = insert(:user) + + {:ok, activity} = CommonAPI.post(another_user, %{"status" => "this is a test post"}) + + output = + capture_io(fn -> + Handler.handle_command(%{user: user}, "r #{activity.id} this is a reply") + end) + + assert output =~ "Replied" + + reply = + Repo.one( + from(a in Activity, + where: fragment("?->>'type' = ?", a.data, "Create"), + where: a.actor == ^user.ap_id + ) + ) + + assert reply.actor == user.ap_id + object = Object.normalize(reply) + assert object.data["content"] == "this is a reply" + assert object.data["inReplyTo"] == activity.data["object"] + end +end diff --git a/test/bookmark_test.exs b/test/bookmark_test.exs new file mode 100644 index 000000000..b81c102ef --- /dev/null +++ b/test/bookmark_test.exs @@ -0,0 +1,52 @@ +defmodule Pleroma.BookmarkTest do + use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.Bookmark + alias Pleroma.Web.CommonAPI + + describe "create/2" do + test "with valid params" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{"status" => "Some cool information"}) + {:ok, bookmark} = Bookmark.create(user.id, activity.id) + assert bookmark.user_id == user.id + assert bookmark.activity_id == activity.id + end + + test "with invalid params" do + {:error, changeset} = Bookmark.create(nil, "") + refute changeset.valid? + + assert changeset.errors == [ + user_id: {"can't be blank", [validation: :required]}, + activity_id: {"can't be blank", [validation: :required]} + ] + end + end + + describe "destroy/2" do + test "with valid params" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "Some cool information"}) + {:ok, _bookmark} = Bookmark.create(user.id, activity.id) + + {:ok, _deleted_bookmark} = Bookmark.destroy(user.id, activity.id) + end + end + + describe "get/2" do + test "gets a bookmark" do + user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => + "Scientists Discover The Secret Behind Tenshi Eating A Corndog Being So Cute – Science Daily" + }) + + {:ok, bookmark} = Bookmark.create(user.id, activity.id) + assert bookmark == Bookmark.get(user.id, activity.id) + end + end +end diff --git a/test/conversation/participation_test.exs b/test/conversation/participation_test.exs new file mode 100644 index 000000000..568953b07 --- /dev/null +++ b/test/conversation/participation_test.exs @@ -0,0 +1,89 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Conversation.ParticipationTest do + use Pleroma.DataCase + import Pleroma.Factory + alias Pleroma.Conversation.Participation + alias Pleroma.Web.CommonAPI + + test "it creates a participation for a conversation and a user" do + user = insert(:user) + conversation = insert(:conversation) + + {:ok, %Participation{} = participation} = + Participation.create_for_user_and_conversation(user, conversation) + + assert participation.user_id == user.id + assert participation.conversation_id == conversation.id + + :timer.sleep(1000) + # Creating again returns the same participation + {:ok, %Participation{} = participation_two} = + Participation.create_for_user_and_conversation(user, conversation) + + assert participation.id == participation_two.id + refute participation.updated_at == participation_two.updated_at + end + + test "recreating an existing participations sets it to unread" do + participation = insert(:participation, %{read: true}) + + {:ok, participation} = + Participation.create_for_user_and_conversation( + participation.user, + participation.conversation + ) + + refute participation.read + end + + test "it marks a participation as read" do + participation = insert(:participation, %{read: false}) + {:ok, participation} = Participation.mark_as_read(participation) + + assert participation.read + end + + test "it marks a participation as unread" do + participation = insert(:participation, %{read: true}) + {:ok, participation} = Participation.mark_as_unread(participation) + + refute participation.read + end + + test "gets all the participations for a user, ordered by updated at descending" do + user = insert(:user) + {:ok, activity_one} = CommonAPI.post(user, %{"status" => "x", "visibility" => "direct"}) + :timer.sleep(1000) + {:ok, activity_two} = CommonAPI.post(user, %{"status" => "x", "visibility" => "direct"}) + :timer.sleep(1000) + + {:ok, activity_three} = + CommonAPI.post(user, %{ + "status" => "x", + "visibility" => "direct", + "in_reply_to_status_id" => activity_one.id + }) + + assert [participation_one, participation_two] = Participation.for_user(user) + + object2 = Pleroma.Object.normalize(activity_two) + object3 = Pleroma.Object.normalize(activity_three) + + assert participation_one.conversation.ap_id == object3.data["context"] + assert participation_two.conversation.ap_id == object2.data["context"] + + # Pagination + assert [participation_one] = Participation.for_user(user, %{"limit" => 1}) + + assert participation_one.conversation.ap_id == object3.data["context"] + + # With last_activity_id + assert [participation_one] = + Participation.for_user_with_last_activity_id(user, %{"limit" => 1}) + + assert participation_one.last_activity_id == activity_three.id + end +end diff --git a/test/conversation_test.exs b/test/conversation_test.exs new file mode 100644 index 000000000..864b2eb03 --- /dev/null +++ b/test/conversation_test.exs @@ -0,0 +1,175 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.ConversationTest do + use Pleroma.DataCase + alias Pleroma.Activity + alias Pleroma.Conversation + alias Pleroma.Object + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + test "it creates a conversation for given ap_id" do + assert {:ok, %Conversation{} = conversation} = + Conversation.create_for_ap_id("https://some_ap_id") + + # Inserting again returns the same + assert {:ok, conversation_two} = Conversation.create_for_ap_id("https://some_ap_id") + assert conversation_two.id == conversation.id + end + + test "public posts don't create conversations" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey"}) + + object = Pleroma.Object.normalize(activity) + context = object.data["context"] + + conversation = Conversation.get_for_ap_id(context) + + refute conversation + end + + test "it creates or updates a conversation and participations for a given DM" do + har = insert(:user) + jafnhar = insert(:user, local: false) + tridi = insert(:user) + + {:ok, activity} = + CommonAPI.post(har, %{"status" => "Hey @#{jafnhar.nickname}", "visibility" => "direct"}) + + object = Pleroma.Object.normalize(activity) + context = object.data["context"] + + conversation = + Conversation.get_for_ap_id(context) + |> Repo.preload(:participations) + + assert conversation + + assert Enum.find(conversation.participations, fn %{user_id: user_id} -> har.id == user_id end) + + assert Enum.find(conversation.participations, fn %{user_id: user_id} -> + jafnhar.id == user_id + end) + + {:ok, activity} = + CommonAPI.post(jafnhar, %{ + "status" => "Hey @#{har.nickname}", + "visibility" => "direct", + "in_reply_to_status_id" => activity.id + }) + + object = Pleroma.Object.normalize(activity) + context = object.data["context"] + + conversation_two = + Conversation.get_for_ap_id(context) + |> Repo.preload(:participations) + + assert conversation_two.id == conversation.id + + assert Enum.find(conversation_two.participations, fn %{user_id: user_id} -> + har.id == user_id + end) + + assert Enum.find(conversation_two.participations, fn %{user_id: user_id} -> + jafnhar.id == user_id + end) + + {:ok, activity} = + CommonAPI.post(tridi, %{ + "status" => "Hey @#{har.nickname}", + "visibility" => "direct", + "in_reply_to_status_id" => activity.id + }) + + object = Pleroma.Object.normalize(activity) + context = object.data["context"] + + conversation_three = + Conversation.get_for_ap_id(context) + |> Repo.preload([:participations, :users]) + + assert conversation_three.id == conversation.id + + assert Enum.find(conversation_three.participations, fn %{user_id: user_id} -> + har.id == user_id + end) + + assert Enum.find(conversation_three.participations, fn %{user_id: user_id} -> + jafnhar.id == user_id + end) + + assert Enum.find(conversation_three.participations, fn %{user_id: user_id} -> + tridi.id == user_id + end) + + assert Enum.find(conversation_three.users, fn %{id: user_id} -> + har.id == user_id + end) + + assert Enum.find(conversation_three.users, fn %{id: user_id} -> + jafnhar.id == user_id + end) + + assert Enum.find(conversation_three.users, fn %{id: user_id} -> + tridi.id == user_id + end) + end + + test "create_or_bump_for returns the conversation with participations" do + har = insert(:user) + jafnhar = insert(:user, local: false) + + {:ok, activity} = + CommonAPI.post(har, %{"status" => "Hey @#{jafnhar.nickname}", "visibility" => "direct"}) + + {:ok, conversation} = Conversation.create_or_bump_for(activity) + + assert length(conversation.participations) == 2 + + {:ok, activity} = + CommonAPI.post(har, %{"status" => "Hey @#{jafnhar.nickname}", "visibility" => "public"}) + + assert {:error, _} = Conversation.create_or_bump_for(activity) + end + + test "create_or_bump_for does not normalize objects before checking the activity type" do + note = insert(:note) + note_id = note.data["id"] + Repo.delete(note) + refute Object.get_by_ap_id(note_id) + + Tesla.Mock.mock(fn env -> + case env.url do + ^note_id -> + # TODO: add attributedTo and tag to the note factory + body = + note.data + |> Map.put("attributedTo", note.data["actor"]) + |> Map.put("tag", []) + |> Jason.encode!() + + %Tesla.Env{status: 200, body: body} + end + end) + + undo = %Activity{ + id: "fake", + data: %{ + "id" => Pleroma.Web.ActivityPub.Utils.generate_activity_id(), + "actor" => note.data["actor"], + "to" => [note.data["actor"]], + "object" => note_id, + "type" => "Undo" + } + } + + Conversation.create_or_bump_for(undo) + + refute Object.get_by_ap_id(note_id) + end +end diff --git a/test/formatter_test.exs b/test/formatter_test.exs index 97eb2f583..06f4f6e50 100644 --- a/test/formatter_test.exs +++ b/test/formatter_test.exs @@ -147,7 +147,7 @@ test "gives a replacement for user links, using local nicknames in user links te end test "gives a replacement for user links when the user is using Osada" do - mike = User.get_or_fetch("mike@osada.macgirvin.com") + {:ok, mike} = User.get_or_fetch("mike@osada.macgirvin.com") text = "@mike@osada.macgirvin.com test" @@ -248,7 +248,7 @@ test "it adds cool emoji" do text = "I love :firefox:" expected_result = - "I love \"firefox\"" + "I love \"firefox\"" assert Formatter.emojify(text) == expected_result end @@ -263,7 +263,7 @@ test "it does not add XSS emoji" do } expected_result = - "I love \"\"" + "I love \"\"" assert Formatter.emojify(text, custom_emoji) == expected_result end diff --git a/test/media_proxy_test.exs b/test/media_proxy_test.exs index ddbadfbf5..0a02039a6 100644 --- a/test/media_proxy_test.exs +++ b/test/media_proxy_test.exs @@ -7,15 +7,15 @@ defmodule Pleroma.MediaProxyTest do import Pleroma.Web.MediaProxy alias Pleroma.Web.MediaProxy.MediaProxyController + setup do + enabled = Pleroma.Config.get([:media_proxy, :enabled]) + on_exit(fn -> Pleroma.Config.put([:media_proxy, :enabled], enabled) end) + :ok + end + describe "when enabled" do setup do - enabled = Pleroma.Config.get([:media_proxy, :enabled]) - - unless enabled do - Pleroma.Config.put([:media_proxy, :enabled], true) - on_exit(fn -> Pleroma.Config.put([:media_proxy, :enabled], enabled) end) - end - + Pleroma.Config.put([:media_proxy, :enabled], true) :ok end @@ -177,4 +177,13 @@ defp decode_result(encoded) do {:ok, decoded} = decode_url(sig, base64) decoded end + + test "mediaproxy whitelist" do + Pleroma.Config.put([:media_proxy, :enabled], true) + Pleroma.Config.put([:media_proxy, :whitelist], ["google.com", "feld.me"]) + url = "https://feld.me/foo.png" + + unencoded = url(url) + assert unencoded == url + end end diff --git a/test/plugs/oauth_plug_test.exs b/test/plugs/oauth_plug_test.exs index 17fdba916..5a2ed11cc 100644 --- a/test/plugs/oauth_plug_test.exs +++ b/test/plugs/oauth_plug_test.exs @@ -38,6 +38,26 @@ test "with valid token(downcase), it assigns the user", %{conn: conn} = opts do assert conn.assigns[:user] == opts[:user] end + test "with valid token(downcase) in url parameters, it assings the user", opts do + conn = + :get + |> build_conn("/?access_token=#{opts[:token]}") + |> put_req_header("content-type", "application/json") + |> fetch_query_params() + |> OAuthPlug.call(%{}) + + assert conn.assigns[:user] == opts[:user] + end + + test "with valid token(downcase) in body parameters, it assigns the user", opts do + conn = + :post + |> build_conn("/api/v1/statuses", access_token: opts[:token], status: "test") + |> OAuthPlug.call(%{}) + + assert conn.assigns[:user] == opts[:user] + end + test "with invalid token, it not assigns the user", %{conn: conn} do conn = conn diff --git a/test/plugs/rate_limit_plug_test.exs b/test/plugs/rate_limit_plug_test.exs new file mode 100644 index 000000000..2ec9a8fb7 --- /dev/null +++ b/test/plugs/rate_limit_plug_test.exs @@ -0,0 +1,50 @@ +defmodule Pleroma.Plugs.RateLimitPlugTest do + use ExUnit.Case, async: true + use Plug.Test + + alias Pleroma.Plugs.RateLimitPlug + + @opts RateLimitPlug.init(%{max_requests: 5, interval: 1}) + + setup do + enabled = Pleroma.Config.get([:app_account_creation, :enabled]) + + Pleroma.Config.put([:app_account_creation, :enabled], true) + + on_exit(fn -> + Pleroma.Config.put([:app_account_creation, :enabled], enabled) + end) + + :ok + end + + test "it restricts by opts" do + conn = conn(:get, "/") + bucket_name = conn.remote_ip |> Tuple.to_list() |> Enum.join(".") + ms = 1000 + + conn = RateLimitPlug.call(conn, @opts) + {1, 4, _, _, _} = ExRated.inspect_bucket(bucket_name, ms, 5) + conn = RateLimitPlug.call(conn, @opts) + {2, 3, _, _, _} = ExRated.inspect_bucket(bucket_name, ms, 5) + conn = RateLimitPlug.call(conn, @opts) + {3, 2, _, _, _} = ExRated.inspect_bucket(bucket_name, ms, 5) + conn = RateLimitPlug.call(conn, @opts) + {4, 1, _, _, _} = ExRated.inspect_bucket(bucket_name, ms, 5) + conn = RateLimitPlug.call(conn, @opts) + {5, 0, to_reset, _, _} = ExRated.inspect_bucket(bucket_name, ms, 5) + conn = RateLimitPlug.call(conn, @opts) + assert conn.status == 403 + assert conn.halted + assert conn.resp_body == "{\"error\":\"Rate limit exceeded.\"}" + + Process.sleep(to_reset) + + conn = conn(:get, "/") + conn = RateLimitPlug.call(conn, @opts) + {1, 4, _, _, _} = ExRated.inspect_bucket(bucket_name, ms, 5) + refute conn.status == 403 + refute conn.halted + refute conn.resp_body + end +end diff --git a/test/repo_test.exs b/test/repo_test.exs new file mode 100644 index 000000000..5382289c7 --- /dev/null +++ b/test/repo_test.exs @@ -0,0 +1,44 @@ +defmodule Pleroma.RepoTest do + use Pleroma.DataCase + import Pleroma.Factory + + describe "find_resource/1" do + test "returns user" do + user = insert(:user) + query = from(t in Pleroma.User, where: t.id == ^user.id) + assert Repo.find_resource(query) == {:ok, user} + end + + test "returns not_found" do + query = from(t in Pleroma.User, where: t.id == ^"9gBuXNpD2NyDmmxxdw") + assert Repo.find_resource(query) == {:error, :not_found} + end + end + + describe "get_assoc/2" do + test "get assoc from preloaded data" do + user = %Pleroma.User{name: "Agent Smith"} + token = %Pleroma.Web.OAuth.Token{insert(:oauth_token) | user: user} + assert Repo.get_assoc(token, :user) == {:ok, user} + end + + test "get one-to-one assoc from repo" do + user = insert(:user, name: "Jimi Hendrix") + token = refresh_record(insert(:oauth_token, user: user)) + + assert Repo.get_assoc(token, :user) == {:ok, user} + end + + test "get one-to-many assoc from repo" do + user = insert(:user) + notification = refresh_record(insert(:notification, user: user)) + + assert Repo.get_assoc(user, :notifications) == {:ok, [notification]} + end + + test "return error if has not assoc " do + token = insert(:oauth_token, user: nil) + assert Repo.get_assoc(token, :user) == {:error, :not_found} + end + end +end diff --git a/test/support/factory.ex b/test/support/factory.ex index ea59912cf..2a2954ad6 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -5,6 +5,23 @@ defmodule Pleroma.Factory do use ExMachina.Ecto, repo: Pleroma.Repo + def participation_factory do + conversation = insert(:conversation) + user = insert(:user) + + %Pleroma.Conversation.Participation{ + conversation: conversation, + user: user, + read: false + } + end + + def conversation_factory do + %Pleroma.Conversation{ + ap_id: sequence(:ap_id, &"https://some_conversation/#{&1}") + } + end + def user_factory do user = %Pleroma.User{ name: sequence(:name, &"Test テスト User #{&1}"), diff --git a/test/user_test.exs b/test/user_test.exs index 42d570c50..60de0206e 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -349,7 +349,7 @@ test "it creates unconfirmed user" do end test "it creates confirmed user if :confirmed option is given" do - changeset = User.register_changeset(%User{}, @full_user_data, confirmed: true) + changeset = User.register_changeset(%User{}, @full_user_data, need_confirmation: false) assert changeset.valid? {:ok, user} = Repo.insert(changeset) @@ -362,7 +362,7 @@ test "it creates confirmed user if :confirmed option is given" do describe "get_or_fetch/1" do test "gets an existing user by nickname" do user = insert(:user) - fetched_user = User.get_or_fetch(user.nickname) + {:ok, fetched_user} = User.get_or_fetch(user.nickname) assert user == fetched_user end @@ -379,7 +379,7 @@ test "gets an existing user by ap_id" do info: %{} ) - fetched_user = User.get_or_fetch(ap_id) + {:ok, fetched_user} = User.get_or_fetch(ap_id) freshed_user = refresh_record(user) assert freshed_user == fetched_user end @@ -388,14 +388,14 @@ test "gets an existing user by ap_id" do describe "fetching a user from nickname or trying to build one" do test "gets an existing user" do user = insert(:user) - fetched_user = User.get_or_fetch_by_nickname(user.nickname) + {:ok, fetched_user} = User.get_or_fetch_by_nickname(user.nickname) assert user == fetched_user end test "gets an existing user, case insensitive" do user = insert(:user, nickname: "nick") - fetched_user = User.get_or_fetch_by_nickname("NICK") + {:ok, fetched_user} = User.get_or_fetch_by_nickname("NICK") assert user == fetched_user end @@ -403,7 +403,7 @@ test "gets an existing user, case insensitive" do test "gets an existing user by fully qualified nickname" do user = insert(:user) - fetched_user = + {:ok, fetched_user} = User.get_or_fetch_by_nickname(user.nickname <> "@" <> Pleroma.Web.Endpoint.host()) assert user == fetched_user @@ -413,24 +413,24 @@ test "gets an existing user by fully qualified nickname, case insensitive" do user = insert(:user, nickname: "nick") casing_altered_fqn = String.upcase(user.nickname <> "@" <> Pleroma.Web.Endpoint.host()) - fetched_user = User.get_or_fetch_by_nickname(casing_altered_fqn) + {:ok, fetched_user} = User.get_or_fetch_by_nickname(casing_altered_fqn) assert user == fetched_user end test "fetches an external user via ostatus if no user exists" do - fetched_user = User.get_or_fetch_by_nickname("shp@social.heldscal.la") + {:ok, fetched_user} = User.get_or_fetch_by_nickname("shp@social.heldscal.la") assert fetched_user.nickname == "shp@social.heldscal.la" end test "returns nil if no user could be fetched" do - fetched_user = User.get_or_fetch_by_nickname("nonexistant@social.heldscal.la") - assert fetched_user == nil + {:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistant@social.heldscal.la") + assert fetched_user == "not found nonexistant@social.heldscal.la" end test "returns nil for nonexistant local user" do - fetched_user = User.get_or_fetch_by_nickname("nonexistant") - assert fetched_user == nil + {:error, fetched_user} = User.get_or_fetch_by_nickname("nonexistant") + assert fetched_user == "not found nonexistant" end test "updates an existing user, if stale" do @@ -448,7 +448,7 @@ test "updates an existing user, if stale" do assert orig_user.last_refreshed_at == a_week_ago - user = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin") + {:ok, user} = User.get_or_fetch_by_ap_id("http://mastodon.example.org/users/admin") assert user.info.source_data["endpoints"] refute user.last_refreshed_at == orig_user.last_refreshed_at @@ -829,10 +829,12 @@ test ".delete_user_activities deletes all create activities" do user = insert(:user) {:ok, activity} = CommonAPI.post(user, %{"status" => "2hu"}) - {:ok, _} = User.delete_user_activities(user) - # TODO: Remove favorites, repeats, delete activities. - refute Activity.get_by_id(activity.id) + Ecto.Adapters.SQL.Sandbox.unboxed_run(Repo, fn -> + {:ok, _} = User.delete_user_activities(user) + # TODO: Remove favorites, repeats, delete activities. + refute Activity.get_by_id(activity.id) + end) end test ".delete deactivates a user, all follow relationships and all create activities" do @@ -1103,7 +1105,7 @@ test "preserves hosts in user links text" do expected_text = "A.k.a. " <> "@nick@domain.com" + }'>@nick@domain.com" assert expected_text == User.parse_bio(bio, user) end @@ -1125,33 +1127,6 @@ test "Adds rel=me on linkbacked urls" do end end - test "bookmarks" do - user = insert(:user) - - {:ok, activity1} = - CommonAPI.post(user, %{ - "status" => "heweoo!" - }) - - id1 = Object.normalize(activity1).data["id"] - - {:ok, activity2} = - CommonAPI.post(user, %{ - "status" => "heweoo!" - }) - - id2 = Object.normalize(activity2).data["id"] - - assert {:ok, user_state1} = User.bookmark(user, id1) - assert user_state1.bookmarks == [id1] - - assert {:ok, user_state2} = User.unbookmark(user, id1) - assert user_state2.bookmarks == [] - - assert {:ok, user_state3} = User.bookmark(user, id2) - assert user_state3.bookmarks == [id2] - end - test "follower count is updated when a follower is blocked" do user = insert(:user) follower = insert(:user) diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index f8e987e58..0f90aa1ac 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do alias Pleroma.Object alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Publisher alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.CommonAPI @@ -22,6 +23,28 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do :ok end + describe "streaming out participations" do + test "it streams them out" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"}) + + {:ok, conversation} = Pleroma.Conversation.create_or_bump_for(activity) + + participations = + conversation.participations + |> Repo.preload(:user) + + with_mock Pleroma.Web.Streamer, + stream: fn _, _ -> nil end do + ActivityPub.stream_out_participations(conversation.participations) + + Enum.each(participations, fn participation -> + assert called(Pleroma.Web.Streamer.stream("participation", participation)) + end) + end + end + end + describe "fetching restricted by visibility" do test "it restricts by the appropriate visibility" do user = insert(:user) @@ -130,9 +153,15 @@ test "drops activities beyond a certain limit" do end test "doesn't drop activities with content being null" do + user = insert(:user) + data = %{ - "ok" => true, + "actor" => user.ap_id, + "to" => [], "object" => %{ + "actor" => user.ap_id, + "to" => [], + "type" => "Note", "content" => nil } } @@ -148,8 +177,17 @@ test "returns the activity if one with the same id is already in" do end test "inserts a given map into the activity database, giving it an id if it has none." do + user = insert(:user) + data = %{ - "ok" => true + "actor" => user.ap_id, + "to" => [], + "object" => %{ + "actor" => user.ap_id, + "to" => [], + "type" => "Note", + "content" => "hey" + } } {:ok, %Activity{} = activity} = ActivityPub.insert(data) @@ -159,9 +197,16 @@ test "inserts a given map into the activity database, giving it an id if it has given_id = "bla" data = %{ - "ok" => true, "id" => given_id, - "context" => "blabla" + "actor" => user.ap_id, + "to" => [], + "context" => "blabla", + "object" => %{ + "actor" => user.ap_id, + "to" => [], + "type" => "Note", + "content" => "hey" + } } {:ok, %Activity{} = activity} = ActivityPub.insert(data) @@ -172,26 +217,39 @@ test "inserts a given map into the activity database, giving it an id if it has end test "adds a context when none is there" do + user = insert(:user) + data = %{ - "id" => "some_id", + "actor" => user.ap_id, + "to" => [], "object" => %{ - "id" => "object_id" + "actor" => user.ap_id, + "to" => [], + "type" => "Note", + "content" => "hey" } } {:ok, %Activity{} = activity} = ActivityPub.insert(data) + object = Pleroma.Object.normalize(activity) assert is_binary(activity.data["context"]) - assert is_binary(activity.data["object"]["context"]) + assert is_binary(object.data["context"]) assert activity.data["context_id"] - assert activity.data["object"]["context_id"] + assert object.data["context_id"] end test "adds an id to a given object if it lacks one and is a note and inserts it to the object database" do + user = insert(:user) + data = %{ + "actor" => user.ap_id, + "to" => [], "object" => %{ + "actor" => user.ap_id, + "to" => [], "type" => "Note", - "ok" => true + "content" => "hey" } } @@ -906,8 +964,7 @@ test "it filters broken threads" do private_activity_1 = Activity.get_by_ap_id_with_object(private_activity_1.data["id"]) - assert [public_activity, private_activity_1, private_activity_3] == - activities + assert [public_activity, private_activity_1, private_activity_3] == activities assert length(activities) == 3 @@ -1000,7 +1057,7 @@ test "it can create a Flag activity" do actor = insert(:user) inbox = "http://200.site/users/nick1/inbox" - assert {:ok, _} = ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) + assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) assert called(Instances.set_reachable(inbox)) end @@ -1013,7 +1070,7 @@ test "it can create a Flag activity" do inbox = "http://200.site/users/nick1/inbox" assert {:ok, _} = - ActivityPub.publish_one(%{ + Publisher.publish_one(%{ inbox: inbox, json: "{}", actor: actor, @@ -1032,7 +1089,7 @@ test "it can create a Flag activity" do inbox = "http://200.site/users/nick1/inbox" assert {:ok, _} = - ActivityPub.publish_one(%{ + Publisher.publish_one(%{ inbox: inbox, json: "{}", actor: actor, @@ -1050,8 +1107,7 @@ test "it can create a Flag activity" do actor = insert(:user) inbox = "http://404.site/users/nick1/inbox" - assert {:error, _} = - ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) + assert {:error, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) assert called(Instances.set_unreachable(inbox)) end @@ -1063,8 +1119,7 @@ test "it can create a Flag activity" do actor = insert(:user) inbox = "http://connrefused.site/users/nick1/inbox" - assert {:error, _} = - ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) + assert {:error, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) assert called(Instances.set_unreachable(inbox)) end @@ -1076,7 +1131,7 @@ test "it can create a Flag activity" do actor = insert(:user) inbox = "http://200.site/users/nick1/inbox" - assert {:ok, _} = ActivityPub.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) + assert {:ok, _} = Publisher.publish_one(%{inbox: inbox, json: "{}", actor: actor, id: 1}) refute called(Instances.set_unreachable(inbox)) end @@ -1089,7 +1144,7 @@ test "it can create a Flag activity" do inbox = "http://connrefused.site/users/nick1/inbox" assert {:error, _} = - ActivityPub.publish_one(%{ + Publisher.publish_one(%{ inbox: inbox, json: "{}", actor: actor, diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs index 78429c7c6..c24b50f8c 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -219,7 +219,7 @@ test "it rejects incoming follow requests from blocked users when deny_follow_bl Pleroma.Config.put([:user, :deny_follow_blocked], true) user = insert(:user) - target = User.get_or_fetch("http://mastodon.example.org/users/admin") + {:ok, target} = User.get_or_fetch("http://mastodon.example.org/users/admin") {:ok, user} = User.block(user, target) diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index b89c42327..6c1897b5a 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -9,7 +9,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do alias Pleroma.UserInviteToken import Pleroma.Factory - describe "/api/pleroma/admin/user" do + describe "/api/pleroma/admin/users" do test "Delete" do admin = insert(:user, info: %{is_admin: true}) user = insert(:user) @@ -18,7 +18,7 @@ test "Delete" do build_conn() |> assign(:user, admin) |> put_req_header("accept", "application/json") - |> delete("/api/pleroma/admin/user?nickname=#{user.nickname}") + |> delete("/api/pleroma/admin/users?nickname=#{user.nickname}") assert json_response(conn, 200) == user.nickname end @@ -30,7 +30,7 @@ test "Create" do build_conn() |> assign(:user, admin) |> put_req_header("accept", "application/json") - |> post("/api/pleroma/admin/user", %{ + |> post("/api/pleroma/admin/users", %{ "nickname" => "lain", "email" => "lain@example.org", "password" => "test" @@ -75,7 +75,7 @@ test "when the user doesn't exist", %{conn: conn} do end end - describe "/api/pleroma/admin/user/follow" do + describe "/api/pleroma/admin/users/follow" do test "allows to force-follow another user" do admin = insert(:user, info: %{is_admin: true}) user = insert(:user) @@ -84,7 +84,7 @@ test "allows to force-follow another user" do build_conn() |> assign(:user, admin) |> put_req_header("accept", "application/json") - |> post("/api/pleroma/admin/user/follow", %{ + |> post("/api/pleroma/admin/users/follow", %{ "follower" => follower.nickname, "followed" => user.nickname }) @@ -96,7 +96,7 @@ test "allows to force-follow another user" do end end - describe "/api/pleroma/admin/user/unfollow" do + describe "/api/pleroma/admin/users/unfollow" do test "allows to force-unfollow another user" do admin = insert(:user, info: %{is_admin: true}) user = insert(:user) @@ -107,7 +107,7 @@ test "allows to force-unfollow another user" do build_conn() |> assign(:user, admin) |> put_req_header("accept", "application/json") - |> post("/api/pleroma/admin/user/unfollow", %{ + |> post("/api/pleroma/admin/users/unfollow", %{ "follower" => follower.nickname, "followed" => user.nickname }) @@ -191,7 +191,7 @@ test "it does not modify tags of not specified users", %{conn: conn, user3: user end end - describe "/api/pleroma/admin/permission_group" do + describe "/api/pleroma/admin/users/:nickname/permission_group" do test "GET is giving user_info" do admin = insert(:user, info: %{is_admin: true}) @@ -199,7 +199,7 @@ test "GET is giving user_info" do build_conn() |> assign(:user, admin) |> put_req_header("accept", "application/json") - |> get("/api/pleroma/admin/permission_group/#{admin.nickname}") + |> get("/api/pleroma/admin/users/#{admin.nickname}/permission_group/") assert json_response(conn, 200) == %{ "is_admin" => true, @@ -215,7 +215,7 @@ test "/:right POST, can add to a permission group" do build_conn() |> assign(:user, admin) |> put_req_header("accept", "application/json") - |> post("/api/pleroma/admin/permission_group/#{user.nickname}/admin") + |> post("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin") assert json_response(conn, 200) == %{ "is_admin" => true @@ -230,7 +230,7 @@ test "/:right DELETE, can remove from a permission group" do build_conn() |> assign(:user, admin) |> put_req_header("accept", "application/json") - |> delete("/api/pleroma/admin/permission_group/#{user.nickname}/admin") + |> delete("/api/pleroma/admin/users/#{user.nickname}/permission_group/admin") assert json_response(conn, 200) == %{ "is_admin" => false @@ -238,7 +238,7 @@ test "/:right DELETE, can remove from a permission group" do end end - describe "PUT /api/pleroma/admin/activation_status" do + describe "PUT /api/pleroma/admin/users/:nickname/activation_status" do setup %{conn: conn} do admin = insert(:user, info: %{is_admin: true}) @@ -255,7 +255,7 @@ test "deactivates the user", %{conn: conn} do conn = conn - |> put("/api/pleroma/admin/activation_status/#{user.nickname}", %{status: false}) + |> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: false}) user = User.get_cached_by_id(user.id) assert user.info.deactivated == true @@ -267,7 +267,7 @@ test "activates the user", %{conn: conn} do conn = conn - |> put("/api/pleroma/admin/activation_status/#{user.nickname}", %{status: true}) + |> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: true}) user = User.get_cached_by_id(user.id) assert user.info.deactivated == false @@ -280,7 +280,7 @@ test "returns 403 when requested by a non-admin", %{conn: conn} do conn = conn |> assign(:user, user) - |> put("/api/pleroma/admin/activation_status/#{user.nickname}", %{status: false}) + |> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: false}) assert json_response(conn, :forbidden) end @@ -309,7 +309,9 @@ test "sends invitation and returns 204", %{conn: conn, user: user} do conn = conn |> assign(:user, user) - |> post("/api/pleroma/admin/email_invite?email=#{recipient_email}&name=#{recipient_name}") + |> post( + "/api/pleroma/admin/users/email_invite?email=#{recipient_email}&name=#{recipient_name}" + ) assert json_response(conn, :no_content) @@ -341,13 +343,13 @@ test "it returns 403 if requested by a non-admin", %{conn: conn} do conn = conn |> assign(:user, non_admin_user) - |> post("/api/pleroma/admin/email_invite?email=foo@bar.com&name=JD") + |> post("/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD") assert json_response(conn, :forbidden) end end - describe "POST /api/pleroma/admin/email_invite, with invalid config" do + describe "POST /api/pleroma/admin/users/email_invite, with invalid config" do setup do [user: insert(:user, info: %{is_admin: true})] end @@ -367,7 +369,7 @@ test "it returns 500 if `invites_enabled` is not enabled", %{conn: conn, user: u conn = conn |> assign(:user, user) - |> post("/api/pleroma/admin/email_invite?email=foo@bar.com&name=JD") + |> post("/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD") assert json_response(conn, :internal_server_error) end @@ -387,7 +389,7 @@ test "it returns 500 if `registrations_open` is enabled", %{conn: conn, user: us conn = conn |> assign(:user, user) - |> post("/api/pleroma/admin/email_invite?email=foo@bar.com&name=JD") + |> post("/api/pleroma/admin/users/email_invite?email=foo@bar.com&name=JD") assert json_response(conn, :internal_server_error) end @@ -405,7 +407,7 @@ test "/api/pleroma/admin/invite_token" do assert conn.status == 200 end - test "/api/pleroma/admin/password_reset" do + test "/api/pleroma/admin/users/:nickname/password_reset" do admin = insert(:user, info: %{is_admin: true}) user = insert(:user) @@ -413,20 +415,25 @@ test "/api/pleroma/admin/password_reset" do build_conn() |> assign(:user, admin) |> put_req_header("accept", "application/json") - |> get("/api/pleroma/admin/password_reset?nickname=#{user.nickname}") + |> get("/api/pleroma/admin/users/#{user.nickname}/password_reset") assert conn.status == 200 end describe "GET /api/pleroma/admin/users" do - test "renders users array for the first page" do + setup do admin = insert(:user, info: %{is_admin: true}) - user = insert(:user, local: false, tags: ["foo", "bar"]) conn = build_conn() |> assign(:user, admin) - |> get("/api/pleroma/admin/users?page=1") + + {:ok, conn: conn, admin: admin} + end + + test "renders users array for the first page", %{conn: conn, admin: admin} do + user = insert(:user, local: false, tags: ["foo", "bar"]) + conn = get(conn, "/api/pleroma/admin/users?page=1") assert json_response(conn, 200) == %{ "count" => 2, @@ -452,14 +459,10 @@ test "renders users array for the first page" do } end - test "renders empty array for the second page" do - admin = insert(:user, info: %{is_admin: true}) + test "renders empty array for the second page", %{conn: conn} do insert(:user) - conn = - build_conn() - |> assign(:user, admin) - |> get("/api/pleroma/admin/users?page=2") + conn = get(conn, "/api/pleroma/admin/users?page=2") assert json_response(conn, 200) == %{ "count" => 2, @@ -468,14 +471,10 @@ test "renders empty array for the second page" do } end - test "regular search" do - admin = insert(:user, info: %{is_admin: true}) + test "regular search", %{conn: conn} do user = insert(:user, nickname: "bob") - conn = - build_conn() - |> assign(:user, admin) - |> get("/api/pleroma/admin/users?query=bo") + conn = get(conn, "/api/pleroma/admin/users?query=bo") assert json_response(conn, 200) == %{ "count" => 1, @@ -493,17 +492,101 @@ test "regular search" do } end - test "regular search with page size" do - admin = insert(:user, info: %{is_admin: true}) + test "search by domain", %{conn: conn} do + user = insert(:user, nickname: "nickname@domain.com") + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?query=domain.com") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => user.info.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [] + } + ] + } + end + + test "search by full nickname", %{conn: conn} do + user = insert(:user, nickname: "nickname@domain.com") + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?query=nickname@domain.com") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => user.info.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [] + } + ] + } + end + + test "search by display name", %{conn: conn} do + user = insert(:user, name: "Display name") + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?name=display") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => user.info.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [] + } + ] + } + end + + test "search by email", %{conn: conn} do + user = insert(:user, email: "email@example.com") + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?email=email@example.com") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => user.info.deactivated, + "id" => user.id, + "nickname" => user.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => true, + "tags" => [] + } + ] + } + end + + test "regular search with page size", %{conn: conn} do user = insert(:user, nickname: "aalice") user2 = insert(:user, nickname: "alice") - conn = - build_conn() - |> assign(:user, admin) - |> get("/api/pleroma/admin/users?query=a&page_size=1&page=1") + conn1 = get(conn, "/api/pleroma/admin/users?query=a&page_size=1&page=1") - assert json_response(conn, 200) == %{ + assert json_response(conn1, 200) == %{ "count" => 2, "page_size" => 1, "users" => [ @@ -518,12 +601,9 @@ test "regular search with page size" do ] } - conn = - build_conn() - |> assign(:user, admin) - |> get("/api/pleroma/admin/users?query=a&page_size=1&page=2") + conn2 = get(conn, "/api/pleroma/admin/users?query=a&page_size=1&page=2") - assert json_response(conn, 200) == %{ + assert json_response(conn2, 200) == %{ "count" => 2, "page_size" => 1, "users" => [ @@ -566,7 +646,7 @@ test "only local users" do } end - test "only local users with no query" do + test "only local users with no query", %{admin: old_admin} do admin = insert(:user, info: %{is_admin: true}, nickname: "john") user = insert(:user, nickname: "bob") @@ -578,7 +658,7 @@ test "only local users with no query" do |> get("/api/pleroma/admin/users?filters=local") assert json_response(conn, 200) == %{ - "count" => 2, + "count" => 3, "page_size" => 50, "users" => [ %{ @@ -596,6 +676,100 @@ test "only local users with no query" do "roles" => %{"admin" => true, "moderator" => false}, "local" => true, "tags" => [] + }, + %{ + "deactivated" => false, + "id" => old_admin.id, + "local" => true, + "nickname" => old_admin.nickname, + "roles" => %{"admin" => true, "moderator" => false}, + "tags" => [] + } + ] + } + end + + test "load only admins", %{conn: conn, admin: admin} do + second_admin = insert(:user, info: %{is_admin: true}) + insert(:user) + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?filters=is_admin") + + assert json_response(conn, 200) == %{ + "count" => 2, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => false, + "id" => admin.id, + "nickname" => admin.nickname, + "roles" => %{"admin" => true, "moderator" => false}, + "local" => admin.local, + "tags" => [] + }, + %{ + "deactivated" => false, + "id" => second_admin.id, + "nickname" => second_admin.nickname, + "roles" => %{"admin" => true, "moderator" => false}, + "local" => second_admin.local, + "tags" => [] + } + ] + } + end + + test "load only moderators", %{conn: conn} do + moderator = insert(:user, info: %{is_moderator: true}) + insert(:user) + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?filters=is_moderator") + + assert json_response(conn, 200) == %{ + "count" => 1, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => false, + "id" => moderator.id, + "nickname" => moderator.nickname, + "roles" => %{"admin" => false, "moderator" => true}, + "local" => moderator.local, + "tags" => [] + } + ] + } + end + + test "load users with tags list", %{conn: conn} do + user1 = insert(:user, tags: ["first"]) + user2 = insert(:user, tags: ["second"]) + insert(:user) + insert(:user) + + conn = get(conn, "/api/pleroma/admin/users?tags[]=first&tags[]=second") + + assert json_response(conn, 200) == %{ + "count" => 2, + "page_size" => 50, + "users" => [ + %{ + "deactivated" => false, + "id" => user1.id, + "nickname" => user1.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => user1.local, + "tags" => ["first"] + }, + %{ + "deactivated" => false, + "id" => user2.id, + "nickname" => user2.nickname, + "roles" => %{"admin" => false, "moderator" => false}, + "local" => user2.local, + "tags" => ["second"] } ] } @@ -650,14 +824,19 @@ test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation" do } end - describe "GET /api/pleroma/admin/invite_token" do - test "without options" do + describe "GET /api/pleroma/admin/users/invite_token" do + setup do admin = insert(:user, info: %{is_admin: true}) conn = build_conn() |> assign(:user, admin) - |> get("/api/pleroma/admin/invite_token") + + {:ok, conn: conn} + end + + test "without options", %{conn: conn} do + conn = get(conn, "/api/pleroma/admin/users/invite_token") token = json_response(conn, 200) invite = UserInviteToken.find_by_token!(token) @@ -667,13 +846,9 @@ test "without options" do assert invite.invite_type == "one_time" end - test "with expires_at" do - admin = insert(:user, info: %{is_admin: true}) - + test "with expires_at", %{conn: conn} do conn = - build_conn() - |> assign(:user, admin) - |> get("/api/pleroma/admin/invite_token", %{ + get(conn, "/api/pleroma/admin/users/invite_token", %{ "invite" => %{"expires_at" => Date.to_string(Date.utc_today())} }) @@ -686,13 +861,9 @@ test "with expires_at" do assert invite.invite_type == "date_limited" end - test "with max_use" do - admin = insert(:user, info: %{is_admin: true}) - + test "with max_use", %{conn: conn} do conn = - build_conn() - |> assign(:user, admin) - |> get("/api/pleroma/admin/invite_token", %{ + get(conn, "/api/pleroma/admin/users/invite_token", %{ "invite" => %{"max_use" => 150} }) @@ -704,13 +875,9 @@ test "with max_use" do assert invite.invite_type == "reusable" end - test "with max use and expires_at" do - admin = insert(:user, info: %{is_admin: true}) - + test "with max use and expires_at", %{conn: conn} do conn = - build_conn() - |> assign(:user, admin) - |> get("/api/pleroma/admin/invite_token", %{ + get(conn, "/api/pleroma/admin/users/invite_token", %{ "invite" => %{"max_use" => 150, "expires_at" => Date.to_string(Date.utc_today())} }) @@ -723,26 +890,27 @@ test "with max use and expires_at" do end end - describe "GET /api/pleroma/admin/invites" do - test "no invites" do + describe "GET /api/pleroma/admin/users/invites" do + setup do admin = insert(:user, info: %{is_admin: true}) conn = build_conn() |> assign(:user, admin) - |> get("/api/pleroma/admin/invites") + + {:ok, conn: conn} + end + + test "no invites", %{conn: conn} do + conn = get(conn, "/api/pleroma/admin/users/invites") assert json_response(conn, 200) == %{"invites" => []} end - test "with invite" do - admin = insert(:user, info: %{is_admin: true}) + test "with invite", %{conn: conn} do {:ok, invite} = UserInviteToken.create_invite() - conn = - build_conn() - |> assign(:user, admin) - |> get("/api/pleroma/admin/invites") + conn = get(conn, "/api/pleroma/admin/users/invites") assert json_response(conn, 200) == %{ "invites" => [ @@ -760,7 +928,7 @@ test "with invite" do end end - describe "POST /api/pleroma/admin/revoke_invite" do + describe "POST /api/pleroma/admin/users/revoke_invite" do test "with token" do admin = insert(:user, info: %{is_admin: true}) {:ok, invite} = UserInviteToken.create_invite() @@ -768,7 +936,7 @@ test "with token" do conn = build_conn() |> assign(:user, admin) - |> post("/api/pleroma/admin/revoke_invite", %{"token" => invite.token}) + |> post("/api/pleroma/admin/users/revoke_invite", %{"token" => invite.token}) assert json_response(conn, 200) == %{ "expires_at" => nil, diff --git a/test/web/admin_api/search_test.exs b/test/web/admin_api/search_test.exs index 3950996ed..501a8d007 100644 --- a/test/web/admin_api/search_test.exs +++ b/test/web/admin_api/search_test.exs @@ -70,11 +70,11 @@ test "it returns active/deactivated users" do test "it returns specific user" do insert(:user) insert(:user) - insert(:user, nickname: "bob", local: true, info: %{deactivated: false}) + user = insert(:user, nickname: "bob", local: true, info: %{deactivated: false}) {:ok, _results, total_count} = Search.user(%{query: ""}) - {:ok, _results, count} = + {:ok, [^user], count} = Search.user(%{ query: "Bo", active: true, @@ -84,5 +84,87 @@ test "it returns specific user" do assert total_count == 3 assert count == 1 end + + test "it returns user by domain" do + insert(:user) + insert(:user) + user = insert(:user, nickname: "some@domain.com") + + {:ok, _results, total} = Search.user() + {:ok, [^user], count} = Search.user(%{query: "domain.com"}) + assert total == 3 + assert count == 1 + end + + test "it return user by full nickname" do + insert(:user) + insert(:user) + user = insert(:user, nickname: "some@domain.com") + + {:ok, _results, total} = Search.user() + {:ok, [^user], count} = Search.user(%{query: "some@domain.com"}) + assert total == 3 + assert count == 1 + end + + test "it returns admin user" do + admin = insert(:user, info: %{is_admin: true}) + insert(:user) + insert(:user) + + {:ok, _results, total} = Search.user() + {:ok, [^admin], count} = Search.user(%{is_admin: true}) + assert total == 3 + assert count == 1 + end + + test "it returns moderator user" do + moderator = insert(:user, info: %{is_moderator: true}) + insert(:user) + insert(:user) + + {:ok, _results, total} = Search.user() + {:ok, [^moderator], count} = Search.user(%{is_moderator: true}) + assert total == 3 + assert count == 1 + end + + test "it returns users with tags" do + user1 = insert(:user, tags: ["first"]) + user2 = insert(:user, tags: ["second"]) + insert(:user) + insert(:user) + + {:ok, _results, total} = Search.user() + {:ok, users, count} = Search.user(%{tags: ["first", "second"]}) + assert total == 4 + assert count == 2 + assert user1 in users + assert user2 in users + end + + test "it returns user by display name" do + user = insert(:user, name: "Display name") + insert(:user) + insert(:user) + + {:ok, _results, total} = Search.user() + {:ok, [^user], count} = Search.user(%{name: "display"}) + + assert total == 3 + assert count == 1 + end + + test "it returns user by email" do + user = insert(:user, email: "some@example.com") + insert(:user) + insert(:user) + + {:ok, _results, total} = Search.user() + {:ok, [^user], count} = Search.user(%{email: "some@example.com"}) + + assert total == 3 + assert count == 1 + end end end diff --git a/test/web/auth/authenticator_test.exs b/test/web/auth/authenticator_test.exs new file mode 100644 index 000000000..fea5c8209 --- /dev/null +++ b/test/web/auth/authenticator_test.exs @@ -0,0 +1,42 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.Auth.AuthenticatorTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Web.Auth.Authenticator + import Pleroma.Factory + + describe "fetch_user/1" do + test "returns user by name" do + user = insert(:user) + assert Authenticator.fetch_user(user.nickname) == user + end + + test "returns user by email" do + user = insert(:user) + assert Authenticator.fetch_user(user.email) == user + end + + test "returns nil" do + assert Authenticator.fetch_user("email") == nil + end + end + + describe "fetch_credentials/1" do + test "returns name and password from authorization params" do + params = %{"authorization" => %{"name" => "test", "password" => "test-pass"}} + assert Authenticator.fetch_credentials(params) == {:ok, {"test", "test-pass"}} + end + + test "returns name and password with grant_type 'password'" do + params = %{"grant_type" => "password", "username" => "test", "password" => "test-pass"} + assert Authenticator.fetch_credentials(params) == {:ok, {"test", "test-pass"}} + end + + test "returns error" do + assert Authenticator.fetch_credentials(%{}) == {:error, :invalid_credentials} + end + end +end diff --git a/test/web/common_api/common_api_utils_test.exs b/test/web/common_api/common_api_utils_test.exs index 837a66063..ab4c62b35 100644 --- a/test/web/common_api/common_api_utils_test.exs +++ b/test/web/common_api/common_api_utils_test.exs @@ -119,6 +119,31 @@ test "works for bare text/markdown" do assert output == expected end + test "works for bare text/bbcode" do + text = "[b]hello world[/b]" + expected = "hello world" + + {output, [], []} = Utils.format_input(text, "text/bbcode") + + assert output == expected + + text = "[b]hello world![/b]\n\nsecond paragraph!" + expected = "hello world!
\n
\nsecond paragraph!" + + {output, [], []} = Utils.format_input(text, "text/bbcode") + + assert output == expected + + text = "[b]hello world![/b]\n\nsecond paragraph!" + + expected = + "hello world!
\n
\n<strong>second paragraph!</strong>" + + {output, [], []} = Utils.format_input(text, "text/bbcode") + + assert output == expected + end + test "works for text/markdown with mentions" do {:ok, user} = UserBuilder.insert(%{nickname: "user__test", ap_id: "http://foo.com/user__test"}) diff --git a/test/web/federator_test.exs b/test/web/federator_test.exs index 52729eb50..0f43bc8f2 100644 --- a/test/web/federator_test.exs +++ b/test/web/federator_test.exs @@ -58,7 +58,7 @@ test "with relays deactivated, it does not publish to the relay", %{ describe "Targets reachability filtering in `publish`" do test_with_mock "it federates only to reachable instances via AP", - Federator, + Pleroma.Web.ActivityPub.Publisher, [:passthrough], [] do user = insert(:user) @@ -88,13 +88,18 @@ test "with relays deactivated, it does not publish to the relay", %{ {:ok, _activity} = CommonAPI.post(user, %{"status" => "HI @nick1@domain.com, @nick2@domain2.com!"}) - assert called(Federator.publish_single_ap(%{inbox: inbox1, unreachable_since: dt})) + assert called( + Pleroma.Web.ActivityPub.Publisher.publish_one(%{ + inbox: inbox1, + unreachable_since: dt + }) + ) - refute called(Federator.publish_single_ap(%{inbox: inbox2})) + refute called(Pleroma.Web.ActivityPub.Publisher.publish_one(%{inbox: inbox2})) end test_with_mock "it federates only to reachable instances via Websub", - Federator, + Pleroma.Web.Websub, [:passthrough], [] do user = insert(:user) @@ -122,17 +127,17 @@ test "with relays deactivated, it does not publish to the relay", %{ {:ok, _activity} = CommonAPI.post(user, %{"status" => "HI"}) assert called( - Federator.publish_single_websub(%{ + Pleroma.Web.Websub.publish_one(%{ callback: sub2.callback, unreachable_since: dt }) ) - refute called(Federator.publish_single_websub(%{callback: sub1.callback})) + refute called(Pleroma.Web.Websub.publish_one(%{callback: sub1.callback})) end test_with_mock "it federates only to reachable instances via Salmon", - Federator, + Pleroma.Web.Salmon, [:passthrough], [] do user = insert(:user) @@ -162,13 +167,13 @@ test "with relays deactivated, it does not publish to the relay", %{ CommonAPI.post(user, %{"status" => "HI @nick1@domain.com, @nick2@domain2.com!"}) assert called( - Federator.publish_single_salmon(%{ + Pleroma.Web.Salmon.publish_one(%{ recipient: remote_user2, unreachable_since: dt }) ) - refute called(Federator.publish_single_websub(%{recipient: remote_user1})) + refute called(Pleroma.Web.Salmon.publish_one(%{recipient: remote_user1})) end end diff --git a/test/web/mastodon_api/account_view_test.exs b/test/web/mastodon_api/account_view_test.exs index 0730201bd..a24f2a050 100644 --- a/test/web/mastodon_api/account_view_test.exs +++ b/test/web/mastodon_api/account_view_test.exs @@ -56,14 +56,17 @@ test "Represent a user account" do bot: false, source: %{ note: "", - privacy: "public", - sensitive: false + sensitive: false, + pleroma: %{} }, pleroma: %{ confirmation_pending: false, tags: [], is_admin: false, is_moderator: false, + hide_favorites: true, + hide_followers: false, + hide_follows: false, relationship: %{} } } @@ -81,8 +84,12 @@ test "Represent the user account for the account owner" do "follows" => true } - assert %{pleroma: %{notification_settings: ^notification_settings}} = - AccountView.render("account.json", %{user: user, for: user}) + privacy = user.info.default_scope + + assert %{ + pleroma: %{notification_settings: ^notification_settings}, + source: %{privacy: ^privacy} + } = AccountView.render("account.json", %{user: user, for: user}) end test "Represent a Service(bot) account" do @@ -114,14 +121,17 @@ test "Represent a Service(bot) account" do bot: true, source: %{ note: "", - privacy: "public", - sensitive: false + sensitive: false, + pleroma: %{} }, pleroma: %{ confirmation_pending: false, tags: [], is_admin: false, is_moderator: false, + hide_favorites: true, + hide_followers: false, + hide_follows: false, relationship: %{} } } @@ -200,14 +210,17 @@ test "represent an embedded relationship" do bot: true, source: %{ note: "", - privacy: "public", - sensitive: false + sensitive: false, + pleroma: %{} }, pleroma: %{ confirmation_pending: false, tags: [], is_admin: false, is_moderator: false, + hide_favorites: true, + hide_followers: false, + hide_follows: false, relationship: %{ id: to_string(user.id), following: false, diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index a22944088..5c79ee633 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -16,6 +16,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.FilterView alias Pleroma.Web.OAuth.App + alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OStatus alias Pleroma.Web.Push alias Pleroma.Web.TwitterAPI.TwitterAPI @@ -300,6 +301,65 @@ test "direct timeline", %{conn: conn} do assert status["url"] != direct.data["id"] end + test "Conversations", %{conn: conn} do + user_one = insert(:user) + user_two = insert(:user) + + {:ok, user_two} = User.follow(user_two, user_one) + + {:ok, direct} = + CommonAPI.post(user_one, %{ + "status" => "Hi @#{user_two.nickname}!", + "visibility" => "direct" + }) + + {:ok, _follower_only} = + CommonAPI.post(user_one, %{ + "status" => "Hi @#{user_two.nickname}!", + "visibility" => "private" + }) + + res_conn = + conn + |> assign(:user, user_one) + |> get("/api/v1/conversations") + + assert response = json_response(res_conn, 200) + + assert [ + %{ + "id" => res_id, + "accounts" => res_accounts, + "last_status" => res_last_status, + "unread" => unread + } + ] = response + + assert length(res_accounts) == 2 + assert is_binary(res_id) + assert unread == true + assert res_last_status["id"] == direct.id + + # Apparently undocumented API endpoint + res_conn = + conn + |> assign(:user, user_one) + |> post("/api/v1/conversations/#{res_id}/read") + + assert response = json_response(res_conn, 200) + assert length(response["accounts"]) == 2 + assert response["last_status"]["id"] == direct.id + assert response["unread"] == false + + # (vanilla) Mastodon frontend behaviour + res_conn = + conn + |> assign(:user, user_one) + |> get("/api/v1/statuses/#{res_last_status["id"]}/context") + + assert %{"ancestors" => [], "descendants" => []} == json_response(res_conn, 200) + end + test "doesn't include DMs from blocked users", %{conn: conn} do blocker = insert(:user) blocked = insert(:user) @@ -513,6 +573,7 @@ test "creating a filter", %{conn: conn} do assert response = json_response(conn, 200) assert response["phrase"] == filter.phrase assert response["context"] == filter.context + assert response["irreversible"] == false assert response["id"] != nil assert response["id"] != "" end @@ -1022,7 +1083,7 @@ test "reblogged status for another user", %{conn: conn} do user2 = insert(:user) user3 = insert(:user) CommonAPI.favorite(activity.id, user2) - {:ok, user2} = User.bookmark(user2, activity.data["object"]["id"]) + {:ok, _bookmark} = Pleroma.Bookmark.create(user2.id, activity.id) {:ok, reblog_activity1, _object} = CommonAPI.repeat(activity.id, user1) {:ok, _, _object} = CommonAPI.repeat(activity.id, user2) @@ -2214,6 +2275,78 @@ test "updates the user's locking status", %{conn: conn} do assert user["locked"] == true end + test "updates the user's default scope", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{default_scope: "cofe"}) + + assert user = json_response(conn, 200) + assert user["source"]["privacy"] == "cofe" + end + + test "updates the user's hide_followers status", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{hide_followers: "true"}) + + assert user = json_response(conn, 200) + assert user["pleroma"]["hide_followers"] == true + end + + test "updates the user's hide_follows status", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{hide_follows: "true"}) + + assert user = json_response(conn, 200) + assert user["pleroma"]["hide_follows"] == true + end + + test "updates the user's hide_favorites status", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{hide_favorites: "true"}) + + assert user = json_response(conn, 200) + assert user["pleroma"]["hide_favorites"] == true + end + + test "updates the user's show_role status", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{show_role: "false"}) + + assert user = json_response(conn, 200) + assert user["source"]["pleroma"]["show_role"] == false + end + + test "updates the user's no_rich_text status", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{no_rich_text: "true"}) + + assert user = json_response(conn, 200) + assert user["source"]["pleroma"]["no_rich_text"] == true + end + test "updates the user's name", %{conn: conn} do user = insert(:user) @@ -2279,6 +2412,33 @@ test "requires 'write' permission", %{conn: conn} do end end end + + test "updates profile emojos", %{conn: conn} do + user = insert(:user) + + note = "*sips :blank:*" + name = "I am :firefox:" + + conn = + conn + |> assign(:user, user) + |> patch("/api/v1/accounts/update_credentials", %{ + "note" => note, + "display_name" => name + }) + + assert json_response(conn, 200) + + conn = + conn + |> get("/api/v1/accounts/#{user.id}") + + assert user = json_response(conn, 200) + + assert user["note"] == note + assert user["display_name"] == name + assert [%{"shortcode" => "blank"}, %{"shortcode" => "firefox"}] = user["emojis"] + end end test "get instance information", %{conn: conn} do @@ -3057,4 +3217,129 @@ test "Repeated posts that are replies incorrectly have in_reply_to_id null", %{c replied_to_user = User.get_by_ap_id(replied_to.data["actor"]) assert reblogged_activity["reblog"]["in_reply_to_account_id"] == replied_to_user.id end + + describe "create account by app" do + setup do + enabled = Pleroma.Config.get([:app_account_creation, :enabled]) + max_requests = Pleroma.Config.get([:app_account_creation, :max_requests]) + interval = Pleroma.Config.get([:app_account_creation, :interval]) + + Pleroma.Config.put([:app_account_creation, :enabled], true) + Pleroma.Config.put([:app_account_creation, :max_requests], 5) + Pleroma.Config.put([:app_account_creation, :interval], 1) + + on_exit(fn -> + Pleroma.Config.put([:app_account_creation, :enabled], enabled) + Pleroma.Config.put([:app_account_creation, :max_requests], max_requests) + Pleroma.Config.put([:app_account_creation, :interval], interval) + end) + + :ok + end + + test "Account registration via Application", %{conn: conn} do + conn = + conn + |> post("/api/v1/apps", %{ + client_name: "client_name", + redirect_uris: "urn:ietf:wg:oauth:2.0:oob", + scopes: "read, write, follow" + }) + + %{ + "client_id" => client_id, + "client_secret" => client_secret, + "id" => _, + "name" => "client_name", + "redirect_uri" => "urn:ietf:wg:oauth:2.0:oob", + "vapid_key" => _, + "website" => nil + } = json_response(conn, 200) + + conn = + conn + |> post("/oauth/token", %{ + grant_type: "client_credentials", + client_id: client_id, + client_secret: client_secret + }) + + assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} = + json_response(conn, 200) + + assert token + token_from_db = Repo.get_by(Token, token: token) + assert token_from_db + assert refresh + assert scope == "read write follow" + + conn = + build_conn() + |> put_req_header("authorization", "Bearer " <> token) + |> post("/api/v1/accounts", %{ + username: "lain", + email: "lain@example.org", + password: "PlzDontHackLain", + agreement: true + }) + + %{ + "access_token" => token, + "created_at" => _created_at, + "scope" => _scope, + "token_type" => "Bearer" + } = json_response(conn, 200) + + token_from_db = Repo.get_by(Token, token: token) + assert token_from_db + token_from_db = Repo.preload(token_from_db, :user) + assert token_from_db.user + + assert token_from_db.user.info.confirmation_pending + end + + test "rate limit", %{conn: conn} do + app_token = insert(:oauth_token, user: nil) + + conn = + put_req_header(conn, "authorization", "Bearer " <> app_token.token) + |> Map.put(:remote_ip, {15, 15, 15, 15}) + + for i <- 1..5 do + conn = + conn + |> post("/api/v1/accounts", %{ + username: "#{i}lain", + email: "#{i}lain@example.org", + password: "PlzDontHackLain", + agreement: true + }) + + %{ + "access_token" => token, + "created_at" => _created_at, + "scope" => _scope, + "token_type" => "Bearer" + } = json_response(conn, 200) + + token_from_db = Repo.get_by(Token, token: token) + assert token_from_db + token_from_db = Repo.preload(token_from_db, :user) + assert token_from_db.user + + assert token_from_db.user.info.confirmation_pending + end + + conn = + conn + |> post("/api/v1/accounts", %{ + username: "6lain", + email: "6lain@example.org", + password: "PlzDontHackLain", + agreement: true + }) + + assert json_response(conn, 403) == %{"error" => "Rate limit exceeded."} + end + end end diff --git a/test/web/mastodon_api/status_view_test.exs b/test/web/mastodon_api/status_view_test.exs index f74726212..d7c800e83 100644 --- a/test/web/mastodon_api/status_view_test.exs +++ b/test/web/mastodon_api/status_view_test.exs @@ -6,6 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do use Pleroma.DataCase alias Pleroma.Activity + alias Pleroma.Bookmark alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User @@ -153,6 +154,27 @@ test "tells if the message is muted for some reason" do assert status.muted == true end + test "tells if the status is bookmarked" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "Cute girls doing cute things"}) + status = StatusView.render("status.json", %{activity: activity}) + + assert status.bookmarked == false + + status = StatusView.render("status.json", %{activity: activity, for: user}) + + assert status.bookmarked == false + + {:ok, _bookmark} = Bookmark.create(user.id, activity.id) + + activity = Activity.get_by_id_with_object(activity.id) + + status = StatusView.render("status.json", %{activity: activity, for: user}) + + assert status.bookmarked == true + end + test "a reply" do note = insert(:note_activity) user = insert(:user) diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs index 6e96537ec..1c04ac9ad 100644 --- a/test/web/oauth/oauth_controller_test.exs +++ b/test/web/oauth/oauth_controller_test.exs @@ -12,6 +12,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do alias Pleroma.Web.OAuth.Authorization alias Pleroma.Web.OAuth.Token + @oauth_config_path [:oauth2, :issue_new_refresh_token] @session_opts [ store: :cookie, key: "_test", @@ -613,6 +614,27 @@ test "issues a token for request with HTTP basic auth client credentials" do assert token.scopes == ["scope1", "scope2"] end + test "issue a token for client_credentials grant type" do + app = insert(:oauth_app, scopes: ["read", "write"]) + + conn = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "client_credentials", + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + + assert %{"access_token" => token, "refresh_token" => refresh, "scope" => scope} = + json_response(conn, 200) + + assert token + token_from_db = Repo.get_by(Token, token: token) + assert token_from_db + assert refresh + assert scope == "read write" + end + test "rejects token exchange with invalid client credentials" do user = insert(:user) app = insert(:oauth_app) @@ -643,7 +665,7 @@ test "rejects token exchange for valid credentials belonging to unconfirmed user password = "testpassword" user = insert(:user, password_hash: Comeonin.Pbkdf2.hashpwsalt(password)) - info_change = Pleroma.User.Info.confirmation_changeset(user.info, :unconfirmed) + info_change = Pleroma.User.Info.confirmation_changeset(user.info, need_confirmation: true) {:ok, user} = user @@ -714,4 +736,199 @@ test "rejects an invalid authorization code" do refute Map.has_key?(resp, "access_token") end end + + describe "POST /oauth/token - refresh token" do + setup do + oauth_token_config = Pleroma.Config.get(@oauth_config_path) + + on_exit(fn -> + Pleroma.Config.get(@oauth_config_path, oauth_token_config) + end) + end + + test "issues a new access token with keep fresh token" do + Pleroma.Config.put(@oauth_config_path, true) + user = insert(:user) + app = insert(:oauth_app, scopes: ["read", "write"]) + + {:ok, auth} = Authorization.create_authorization(app, user, ["write"]) + {:ok, token} = Token.exchange_token(app, auth) + + response = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "refresh_token", + "refresh_token" => token.refresh_token, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(200) + + ap_id = user.ap_id + + assert match?( + %{ + "scope" => "write", + "token_type" => "Bearer", + "expires_in" => 600, + "access_token" => _, + "refresh_token" => _, + "me" => ^ap_id + }, + response + ) + + refute Repo.get_by(Token, token: token.token) + new_token = Repo.get_by(Token, token: response["access_token"]) + assert new_token.refresh_token == token.refresh_token + assert new_token.scopes == auth.scopes + assert new_token.user_id == user.id + assert new_token.app_id == app.id + end + + test "issues a new access token with new fresh token" do + Pleroma.Config.put(@oauth_config_path, false) + user = insert(:user) + app = insert(:oauth_app, scopes: ["read", "write"]) + + {:ok, auth} = Authorization.create_authorization(app, user, ["write"]) + {:ok, token} = Token.exchange_token(app, auth) + + response = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "refresh_token", + "refresh_token" => token.refresh_token, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(200) + + ap_id = user.ap_id + + assert match?( + %{ + "scope" => "write", + "token_type" => "Bearer", + "expires_in" => 600, + "access_token" => _, + "refresh_token" => _, + "me" => ^ap_id + }, + response + ) + + refute Repo.get_by(Token, token: token.token) + new_token = Repo.get_by(Token, token: response["access_token"]) + refute new_token.refresh_token == token.refresh_token + assert new_token.scopes == auth.scopes + assert new_token.user_id == user.id + assert new_token.app_id == app.id + end + + test "returns 400 if we try use access token" do + user = insert(:user) + app = insert(:oauth_app, scopes: ["read", "write"]) + + {:ok, auth} = Authorization.create_authorization(app, user, ["write"]) + {:ok, token} = Token.exchange_token(app, auth) + + response = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "refresh_token", + "refresh_token" => token.token, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(400) + + assert %{"error" => "Invalid credentials"} == response + end + + test "returns 400 if refresh_token invalid" do + app = insert(:oauth_app, scopes: ["read", "write"]) + + response = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "refresh_token", + "refresh_token" => "token.refresh_token", + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(400) + + assert %{"error" => "Invalid credentials"} == response + end + + test "issues a new token if token expired" do + user = insert(:user) + app = insert(:oauth_app, scopes: ["read", "write"]) + + {:ok, auth} = Authorization.create_authorization(app, user, ["write"]) + {:ok, token} = Token.exchange_token(app, auth) + + change = + Ecto.Changeset.change( + token, + %{valid_until: NaiveDateTime.add(NaiveDateTime.utc_now(), -86_400 * 30)} + ) + + {:ok, access_token} = Repo.update(change) + + response = + build_conn() + |> post("/oauth/token", %{ + "grant_type" => "refresh_token", + "refresh_token" => access_token.refresh_token, + "client_id" => app.client_id, + "client_secret" => app.client_secret + }) + |> json_response(200) + + ap_id = user.ap_id + + assert match?( + %{ + "scope" => "write", + "token_type" => "Bearer", + "expires_in" => 600, + "access_token" => _, + "refresh_token" => _, + "me" => ^ap_id + }, + response + ) + + refute Repo.get_by(Token, token: token.token) + token = Repo.get_by(Token, token: response["access_token"]) + assert token + assert token.scopes == auth.scopes + assert token.user_id == user.id + assert token.app_id == app.id + end + end + + describe "POST /oauth/token - bad request" do + test "returns 500" do + response = + build_conn() + |> post("/oauth/token", %{}) + |> json_response(500) + + assert %{"error" => "Bad request"} == response + end + end + + describe "POST /oauth/revoke - bad request" do + test "returns 500" do + response = + build_conn() + |> post("/oauth/revoke", %{}) + |> json_response(500) + + assert %{"error" => "Bad request"} == response + end + end end diff --git a/test/web/ostatus/activity_representer_test.exs b/test/web/ostatus/activity_representer_test.exs index a4bb68c4d..16ee02abb 100644 --- a/test/web/ostatus/activity_representer_test.exs +++ b/test/web/ostatus/activity_representer_test.exs @@ -67,37 +67,31 @@ test "a note activity" do end test "a reply note" do - note = insert(:note_activity) - answer = insert(:note_activity) - object = answer.data["object"] - object = Map.put(object, "inReplyTo", note.data["object"]["id"]) - - data = %{answer.data | "object" => object} - answer = %{answer | data: data} - - note_object = Object.get_by_ap_id(note.data["object"]["id"]) + user = insert(:user) + note_object = insert(:note) + _note = insert(:note_activity, %{note: note_object}) + object = insert(:note, %{data: %{"inReplyTo" => note_object.data["id"]}}) + answer = insert(:note_activity, %{note: object}) Repo.update!( Object.change(note_object, %{data: Map.put(note_object.data, "external_url", "someurl")}) ) - user = User.get_cached_by_ap_id(answer.data["actor"]) - expected = """ http://activitystrea.ms/schema/1.0/note http://activitystrea.ms/schema/1.0/post - #{answer.data["object"]["id"]} + #{object.data["id"]} New note by #{user.nickname} - #{answer.data["object"]["content"]} - #{answer.data["object"]["published"]} - #{answer.data["object"]["published"]} + #{object.data["content"]} + #{object.data["published"]} + #{object.data["published"]} #{answer.data["context"]} 2hu - - + + - + """ diff --git a/test/web/push/impl_test.exs b/test/web/push/impl_test.exs index 49b2a9203..1e948086a 100644 --- a/test/web/push/impl_test.exs +++ b/test/web/push/impl_test.exs @@ -5,6 +5,8 @@ defmodule Pleroma.Web.Push.ImplTest do use Pleroma.DataCase + alias Pleroma.Object + alias Pleroma.Web.CommonAPI alias Pleroma.Web.Push.Impl alias Pleroma.Web.Push.Subscription @@ -52,16 +54,12 @@ test "performs sending notifications" do data: %{alerts: %{"follow" => true, "mention" => false}} ) + {:ok, activity} = CommonAPI.post(user, %{"status" => " "Create", - "actor" => user.ap_id, - "object" => %{"content" => " + "Lorem ipsum dolor sit amet, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis." + }) + + object = Object.normalize(activity) + assert Impl.format_body( %{ - activity: %{ - data: %{ - "type" => "Create", - "object" => %{ - "content" => - "Lorem ipsum dolor sit amet, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis." - } - } - } + activity: activity }, - %{nickname: "Bob"} + user, + object ) == "@Bob: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..." end test "renders body for follow activity" do - assert Impl.format_body(%{activity: %{data: %{"type" => "Follow"}}}, %{nickname: "Bob"}) == + user = insert(:user, nickname: "Bob") + other_user = insert(:user) + {:ok, _, _, activity} = CommonAPI.follow(user, other_user) + object = Object.normalize(activity) + + assert Impl.format_body(%{activity: activity}, user, object) == "@Bob has followed you" end test "renders body for announce activity" do user = insert(:user) - note = - insert(:note, %{ - data: %{ - "content" => - "Lorem ipsum dolor sit amet, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis." - } + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => + "Lorem ipsum dolor sit amet, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis." }) - note_activity = insert(:note_activity, %{note: note}) - announce_activity = insert(:announce_activity, %{user: user, note_activity: note_activity}) + {:ok, announce_activity, _} = CommonAPI.repeat(activity.id, user) + object = Object.normalize(activity) - assert Impl.format_body(%{activity: announce_activity}, user) == + assert Impl.format_body(%{activity: announce_activity}, user, object) == "@#{user.nickname} repeated: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce sagittis fini..." end test "renders body for like activity" do - assert Impl.format_body(%{activity: %{data: %{"type" => "Like"}}}, %{nickname: "Bob"}) == + user = insert(:user, nickname: "Bob") + + {:ok, activity} = + CommonAPI.post(user, %{ + "status" => + "Lorem ipsum dolor sit amet, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis." + }) + + {:ok, activity, _} = CommonAPI.favorite(activity.id, user) + object = Object.normalize(activity) + + assert Impl.format_body(%{activity: activity}, user, object) == "@Bob has favorited your post" end end diff --git a/test/web/salmon/salmon_test.exs b/test/web/salmon/salmon_test.exs index 7532578ca..232082779 100644 --- a/test/web/salmon/salmon_test.exs +++ b/test/web/salmon/salmon_test.exs @@ -7,7 +7,9 @@ defmodule Pleroma.Web.Salmon.SalmonTest do alias Pleroma.Activity alias Pleroma.Repo alias Pleroma.User + alias Pleroma.Web.Federator.Publisher alias Pleroma.Web.Salmon + import Mock import Pleroma.Factory @magickey "RSA.pu0s-halox4tu7wmES1FVSx6u-4wc0YrUFXcqWXZG4-27UmbCOpMQftRCldNRfyA-qLbz-eqiwQhh-1EwUvjsD4cYbAHNGHwTvDOyx5AKthQUP44ykPv7kjKGh3DWKySJvcs9tlUG87hlo7AvnMo9pwRS_Zz2CacQ-MKaXyDepk=.AQAB" @@ -77,7 +79,10 @@ test "it gets a magic key" do "RSA.uzg6r1peZU0vXGADWxGJ0PE34WvmhjUmydbX5YYdOiXfODVLwCMi1umGoqUDm-mRu4vNEdFBVJU1CpFA7dKzWgIsqsa501i2XqElmEveXRLvNRWFB6nG03Q5OUY2as8eE54BJm0p20GkMfIJGwP6TSFb-ICp3QjzbatuSPJ6xCE=.AQAB" end - test "it pushes an activity to remote accounts it's addressed to" do + test_with_mock "it pushes an activity to remote accounts it's addressed to", + Publisher, + [:passthrough], + [] do user_data = %{ info: %{ salmon: "http://test-example.org/salmon" @@ -102,10 +107,8 @@ test "it pushes an activity to remote accounts it's addressed to" do user = User.get_cached_by_ap_id(activity.data["actor"]) {:ok, user} = Pleroma.Web.WebFinger.ensure_keys_present(user) - poster = fn url, _data, _headers -> - assert url == "http://test-example.org/salmon" - end + Salmon.publish(user, activity) - Salmon.publish(user, activity, poster) + assert called(Publisher.enqueue_one(Salmon, %{recipient: mentioned_user})) end end diff --git a/test/web/twitter_api/twitter_api_controller_test.exs b/test/web/twitter_api/twitter_api_controller_test.exs index 43ad71a16..e194f14fb 100644 --- a/test/web/twitter_api/twitter_api_controller_test.exs +++ b/test/web/twitter_api/twitter_api_controller_test.exs @@ -1094,7 +1094,7 @@ test "it returns 500 when user is not local", %{conn: conn, user: user} do describe "GET /api/account/confirm_email/:id/:token" do setup do user = insert(:user) - info_change = User.Info.confirmation_changeset(user.info, :unconfirmed) + info_change = User.Info.confirmation_changeset(user.info, need_confirmation: true) {:ok, user} = user @@ -1145,7 +1145,7 @@ test "it returns 500 if token is invalid", %{conn: conn, user: user} do end user = insert(:user) - info_change = User.Info.confirmation_changeset(user.info, :unconfirmed) + info_change = User.Info.confirmation_changeset(user.info, need_confirmation: true) {:ok, user} = user @@ -1611,6 +1611,34 @@ test "it unlocks an account", %{conn: conn} do assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user}) end + + # Broken before the change to class="emoji" and non- in the DB + @tag :skip + test "it formats emojos", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> post("/api/account/update_profile.json", %{ + "bio" => "I love our :moominmamma:​" + }) + + assert response = json_response(conn, 200) + + assert %{ + "description" => "I love our :moominmamma:", + "description_html" => + ~s{I love our moominmamma meow" + "\"firefox\" meow" assert result["summary"] == expected assert result["summary_html"] == expected_html @@ -295,8 +295,8 @@ test "an announce activity" do "id" => announce.id, "is_local" => true, "is_post_verb" => false, - "statusnet_html" => "shp retweeted a status.", - "text" => "shp retweeted a status.", + "statusnet_html" => "shp repeated a status.", + "text" => "shp repeated a status.", "uri" => "tag:#{announce.data["id"]}:objectType=note", "user" => UserView.render("show.json", user: other_user), "retweeted_status" => ActivityView.render("activity.json", activity: activity), @@ -371,4 +371,14 @@ test "a peertube video" do assert length(result["attachments"]) == 1 assert result["summary"] == "Friday Night" end + + test "special characters are not escaped in text field for status created" do + text = "<3 is on the way" + + {:ok, activity} = CommonAPI.post(insert(:user), %{"status" => text}) + + result = ActivityView.render("activity.json", activity: activity) + + assert result["text"] == text + end end diff --git a/test/web/twitter_api/views/user_view_test.exs b/test/web/twitter_api/views/user_view_test.exs index 36b461992..74526673c 100644 --- a/test/web/twitter_api/views/user_view_test.exs +++ b/test/web/twitter_api/views/user_view_test.exs @@ -32,7 +32,7 @@ test "A user with an avatar object", %{user: user} do test "A user with emoji in username" do expected = - "\"karjalanpiirakka\" man" + "\"karjalanpiirakka\" man" user = insert(:user, %{ @@ -89,29 +89,34 @@ test "A user" do "following" => false, "follows_you" => false, "statusnet_blocking" => false, - "rights" => %{ - "delete_others_notice" => false, - "admin" => false - }, "statusnet_profile_url" => user.ap_id, "cover_photo" => banner, "background_image" => nil, "is_local" => true, "locked" => false, - "default_scope" => "public", - "no_rich_text" => false, "hide_follows" => false, "hide_followers" => false, "fields" => [], "pleroma" => %{ "confirmation_pending" => false, "tags" => [] - } + }, + "rights" => %{"admin" => false, "delete_others_notice" => false}, + "role" => "member" } assert represented == UserView.render("show.json", %{user: user}) end + test "User exposes settings for themselves and only for themselves", %{user: user} do + as_user = UserView.render("show.json", %{user: user, for: user}) + assert as_user["default_scope"] == user.info.default_scope + assert as_user["no_rich_text"] == user.info.no_rich_text + as_stranger = UserView.render("show.json", %{user: user}) + refute as_stranger["default_scope"] + refute as_stranger["no_rich_text"] + end + test "A user for a given other follower", %{user: user} do follower = insert(:user, %{following: [User.ap_followers(user)]}) {:ok, user} = User.update_follower_count(user) @@ -137,24 +142,20 @@ test "A user for a given other follower", %{user: user} do "following" => true, "follows_you" => false, "statusnet_blocking" => false, - "rights" => %{ - "delete_others_notice" => false, - "admin" => false - }, "statusnet_profile_url" => user.ap_id, "cover_photo" => banner, "background_image" => nil, "is_local" => true, "locked" => false, - "default_scope" => "public", - "no_rich_text" => false, "hide_follows" => false, "hide_followers" => false, "fields" => [], "pleroma" => %{ "confirmation_pending" => false, "tags" => [] - } + }, + "rights" => %{"admin" => false, "delete_others_notice" => false}, + "role" => "member" } assert represented == UserView.render("show.json", %{user: user, for: follower}) @@ -186,24 +187,20 @@ test "A user that follows you", %{user: user} do "following" => false, "follows_you" => true, "statusnet_blocking" => false, - "rights" => %{ - "delete_others_notice" => false, - "admin" => false - }, "statusnet_profile_url" => follower.ap_id, "cover_photo" => banner, "background_image" => nil, "is_local" => true, "locked" => false, - "default_scope" => "public", - "no_rich_text" => false, "hide_follows" => false, "hide_followers" => false, "fields" => [], "pleroma" => %{ "confirmation_pending" => false, "tags" => [] - } + }, + "rights" => %{"admin" => false, "delete_others_notice" => false}, + "role" => "member" } assert represented == UserView.render("show.json", %{user: follower, for: user}) @@ -272,24 +269,20 @@ test "A blocked user for the blocker" do "following" => false, "follows_you" => false, "statusnet_blocking" => true, - "rights" => %{ - "delete_others_notice" => false, - "admin" => false - }, "statusnet_profile_url" => user.ap_id, "cover_photo" => banner, "background_image" => nil, "is_local" => true, "locked" => false, - "default_scope" => "public", - "no_rich_text" => false, "hide_follows" => false, "hide_followers" => false, "fields" => [], "pleroma" => %{ "confirmation_pending" => false, "tags" => [] - } + }, + "rights" => %{"admin" => false, "delete_others_notice" => false}, + "role" => "member" } blocker = User.get_cached_by_id(blocker.id) diff --git a/test/web/views/error_view_test.exs b/test/web/views/error_view_test.exs index d529fd2c3..3857d585f 100644 --- a/test/web/views/error_view_test.exs +++ b/test/web/views/error_view_test.exs @@ -4,6 +4,7 @@ defmodule Pleroma.Web.ErrorViewTest do use Pleroma.Web.ConnCase, async: true + import ExUnit.CaptureLog # Bring render/3 and render_to_string/3 for testing custom views import Phoenix.View @@ -13,17 +14,23 @@ test "renders 404.json" do end test "render 500.json" do - assert render(Pleroma.Web.ErrorView, "500.json", []) == - %{errors: %{detail: "Internal server error", reason: "nil"}} + assert capture_log(fn -> + assert render(Pleroma.Web.ErrorView, "500.json", []) == + %{errors: %{detail: "Internal server error", reason: "nil"}} + end) =~ "[error] Internal server error: nil" end test "render any other" do - assert render(Pleroma.Web.ErrorView, "505.json", []) == - %{errors: %{detail: "Internal server error", reason: "nil"}} + assert capture_log(fn -> + assert render(Pleroma.Web.ErrorView, "505.json", []) == + %{errors: %{detail: "Internal server error", reason: "nil"}} + end) =~ "[error] Internal server error: nil" end test "render 500.json with reason" do - assert render(Pleroma.Web.ErrorView, "500.json", reason: "test reason") == - %{errors: %{detail: "Internal server error", reason: "\"test reason\""}} + assert capture_log(fn -> + assert render(Pleroma.Web.ErrorView, "500.json", reason: "test reason") == + %{errors: %{detail: "Internal server error", reason: "\"test reason\""}} + end) =~ "[error] Internal server error: \"test reason\"" end end diff --git a/uploads/.gitignore b/uploads/.gitignore new file mode 100644 index 000000000..523e584a7 --- /dev/null +++ b/uploads/.gitignore @@ -0,0 +1,3 @@ +# Git will ignore everything in this directory except this file. +* +!.gitignore