@@ -38,3 +38,7 @@ erl_crash.dump | |||
# Prevent committing docs files | |||
/priv/static/doc/* | |||
# Code test coverage | |||
/cover | |||
/Elixir.*.coverdata |
@@ -21,14 +21,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). | |||
- 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 | |||
@@ -56,10 +59,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). | |||
- 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 | |||
- Mastodon API: Added support max_id & since_id for bookmark timeline endpoints. | |||
- 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. | |||
@@ -90,6 +94,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). | |||
- 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 | |||
@@ -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 | |||
<https://pleroma.social/>, and are distributed under the Creative Commons | |||
Attribution-ShareAlike 4.0 International license, you should have received | |||
@@ -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 <https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org>. | |||
If you want to run your own server, feel free to contact us at @lain@pleroma.soykaf.com or in our dev chat at #pleroma on freenode or via matrix at <https://matrix.heldscal.la/#/room/#freenode_#pleroma:matrix.org>. | |||
## Installation | |||
@@ -212,6 +212,11 @@ config :pleroma, :instance, | |||
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, | |||
@@ -234,6 +239,8 @@ config :pleroma, :instance, | |||
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. | |||
@@ -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 | |||
@@ -87,3 +87,13 @@ Additional parameters can be added to the JSON body/Form data: | |||
`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. |
@@ -105,6 +105,12 @@ 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 | |||
@@ -109,7 +109,7 @@ defmodule Mix.Tasks.Pleroma.Emoji 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 @@ defmodule Mix.Tasks.Pleroma.Emoji 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 @@ defmodule Mix.Tasks.Pleroma.Emoji 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 @@ defmodule Mix.Tasks.Pleroma.Emoji 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 @@ defmodule Mix.Tasks.Pleroma.Emoji 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 @@ defmodule Mix.Tasks.Pleroma.Emoji 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 @@ defmodule Mix.Tasks.Pleroma.Emoji do | |||
] | |||
) | |||
end | |||
defp client do | |||
middleware = [ | |||
{Tesla.Middleware.FollowRedirects, [max_redirects: 3]} | |||
] | |||
Tesla.client(middleware) | |||
end | |||
end |
@@ -138,7 +138,7 @@ defmodule Mix.Tasks.Pleroma.User 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") | |||
@@ -6,9 +6,11 @@ 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 | |||
@@ -35,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! | |||
@@ -73,6 +77,16 @@ defmodule Pleroma.Activity 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( | |||
@@ -82,6 +96,16 @@ defmodule Pleroma.Activity 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]) | |||
@@ -267,6 +291,29 @@ defmodule Pleroma.Activity do | |||
|> 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) | |||
@@ -15,7 +15,7 @@ defmodule Pleroma.Captcha.Kocaptcha do | |||
%{error: "Kocaptcha service unavailable"} | |||
{:ok, res} -> | |||
json_resp = Poison.decode!(res.body) | |||
json_resp = Jason.decode!(res.body) | |||
%{ | |||
type: :kocaptcha, | |||
@@ -0,0 +1,75 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> | |||
# 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 |
@@ -0,0 +1,81 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> | |||
# 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 |
@@ -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. | |||
@@ -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") | |||
@@ -22,18 +23,39 @@ defmodule Pleroma.Plugs.OAuthPlug do | |||
|> 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 | |||
with {:ok, token_str} <- fetch_token_str(conn), | |||
{:ok, user, token_record} <- fetch_user_and_token(token_str) do | |||
conn | |||
|> assign(:token, token_record) | |||
|> assign(:user, user) | |||
else | |||
_ -> conn | |||
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 | |||
@@ -54,6 +76,16 @@ defmodule Pleroma.Plugs.OAuthPlug 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()} | |||
@@ -0,0 +1,36 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> | |||
# 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 |
@@ -34,7 +34,7 @@ defmodule Pleroma.Stats 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 @@ defmodule Pleroma.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}} | |||
@@ -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 | |||
@@ -14,7 +14,7 @@ defmodule Pleroma.Uploaders.Swift.Keystone do | |||
def process_response_body(body) do | |||
body | |||
|> Poison.decode!() | |||
|> Jason.decode!() | |||
end | |||
def get_token do | |||
@@ -38,7 +38,7 @@ defmodule Pleroma.Uploaders.Swift.Keystone do | |||
end | |||
def make_auth_body(username, password, tenant) do | |||
Poison.encode!(%{ | |||
Jason.encode!(%{ | |||
:auth => %{ | |||
:passwordCredentials => %{ | |||
:username => username, | |||
@@ -10,7 +10,6 @@ defmodule Pleroma.User do | |||
alias Comeonin.Pbkdf2 | |||
alias Pleroma.Activity | |||
alias Pleroma.Bookmark | |||
alias Pleroma.Notification | |||
alias Pleroma.Object | |||
alias Pleroma.Registration | |||
@@ -54,7 +53,6 @@ defmodule Pleroma.User do | |||
field(:search_type, :integer, virtual: true) | |||
field(:tags, {:array, :string}, default: []) | |||
field(:last_refreshed_at, :naive_datetime_usec) | |||
has_many(:bookmarks, Bookmark) | |||
has_many(:notifications, Notification) | |||
has_many(:registrations, Registration) | |||
embeds_one(:info, Pleroma.User.Info) | |||
@@ -125,12 +123,9 @@ defmodule Pleroma.User do | |||
def following_count(%User{following: []}), do: 0 | |||
def following_count(%User{following: following, id: id}) do | |||
from(u in User, | |||
where: u.follower_address in ^following, | |||
where: u.id != ^id | |||
) | |||
|> restrict_deactivated() | |||
def following_count(%User{} = user) do | |||
user | |||
|> get_friends_query() | |||
|> Repo.aggregate(:count, :id) | |||
end | |||
@@ -221,14 +216,15 @@ defmodule Pleroma.User 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 | |||
@@ -271,10 +267,7 @@ defmodule Pleroma.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, deactivated: false}) | |||
|> Repo.all() | |||
follow_all(user, autofollowed_users) | |||
@@ -593,20 +586,17 @@ defmodule Pleroma.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 | |||
) | |||
|> restrict_deactivated() | |||
@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, deactivated: false}) | |||
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 | |||
@@ -621,20 +611,17 @@ defmodule Pleroma.User 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 | |||
) | |||
|> restrict_deactivated() | |||
@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, deactivated: false}) | |||
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 | |||
@@ -649,33 +636,10 @@ defmodule Pleroma.User 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) | |||
@@ -747,11 +711,8 @@ defmodule Pleroma.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) | |||
User.Query.build(%{followers: user, deactivated: false}) | |||
|> select([u], %{count: count(u.id)}) | |||
|> restrict_deactivated() | |||
User | |||
|> where(id: ^user.id) | |||
@@ -774,38 +735,19 @@ defmodule Pleroma.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, deactivated: false} | |||
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, deactivated: false}) | |||
|> Repo.all() | |||
end | |||
def search(query, resolve \\ false, for_user \\ nil) do | |||
@@ -1069,14 +1011,23 @@ defmodule Pleroma.User 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, deactivated: false}) | |||
|> 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, deactivated: false}) | |||
|> 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, deactivated: false}) | |||
|> Repo.all() | |||
end | |||
def block_domain(user, domain) do | |||
info_cng = | |||
@@ -1102,71 +1053,8 @@ defmodule Pleroma.User 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_async(user, status \\ true) do | |||
PleromaJobQueue.enqueue(:user, __MODULE__, [:deactivate_async, user, status]) | |||
PleromaJobQueue.enqueue(:background, __MODULE__, [:deactivate_async, user, status]) | |||
end | |||
def perform(:deactivate_async, user, status), do: deactivate(user, status) | |||
@@ -1340,7 +1228,7 @@ defmodule Pleroma.User do | |||
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) | |||
@@ -1457,22 +1345,12 @@ defmodule Pleroma.User 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, deactivated: false}) | |||
|> 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 | |||
@@ -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: %{}) | |||
@@ -210,21 +212,23 @@ defmodule Pleroma.User.Info 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 | |||
@@ -0,0 +1,156 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/> | |||
# 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, false}, query) do | |||
from(u in query, | |||
where: not fragment("? \\? 'deactivated' AND ?->'deactivated' @> 'true'", u.info, u.info) | |||
) | |||
end | |||
defp compose_query({:deactivated, true}, 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 |
@@ -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 | |||
@@ -141,7 +138,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub 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 -> | |||
@@ -164,6 +168,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub 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" | |||
@@ -195,6 +212,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do | |||
end | |||
end | |||
else | |||
# TODO: Write test, replace with visibility test | |||
if !Enum.member?(activity.data["cc"] || [], public) && | |||
!Enum.member?( | |||
activity.data["to"], | |||
@@ -457,35 +475,44 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do | |||
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 | |||
@@ -784,11 +811,32 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub 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 | |||
base_query = from(activity in Activity) | |||
base_query | |||
|> maybe_preload_objects(opts) | |||
|> maybe_preload_bookmarks(opts) | |||
|> maybe_order(opts) | |||
|> restrict_recipients(recipients, opts["user"]) | |||
|> restrict_tag(opts) | |||
|> restrict_tag_reject(opts) | |||
@@ -910,89 +958,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub 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 | |||
def publish(actor, activity) do | |||
remote_followers = | |||
if actor.follower_address in activity.recipients do | |||
{:ok, followers} = User.get_followers(actor) | |||
followers |> Enum.filter(&(!&1.local)) | |||
else | |||
[] | |||
end | |||
public = is_public?(activity) | |||
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data) | |||
json = Jason.encode!(data) | |||
(Pleroma.Web.Salmon.remote_users(activity) ++ remote_followers) | |||
|> 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} -> | |||
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) | |||
@@ -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. | |||
@@ -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 | |||
@@ -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]) | |||
@@ -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 | |||
@@ -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 | |||
@@ -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 | |||
@@ -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 | |||
@@ -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 | |||
@@ -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 | |||
@@ -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 | |||
@@ -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: [] | |||
@@ -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} | |||
@@ -0,0 +1,152 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> | |||
# 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 | |||
@doc """ | |||
Publishes an activity to all relevant peers. | |||
""" | |||
def publish(%User{} = actor, %Activity{} = activity) do | |||
remote_followers = | |||
if actor.follower_address in activity.recipients do | |||
{:ok, followers} = User.get_followers(actor) | |||
followers |> Enum.filter(&(!&1.local)) | |||
else | |||
[] | |||
end | |||
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) | |||
(Pleroma.Web.Salmon.remote_users(activity) ++ remote_followers) | |||
|> 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 |
@@ -682,7 +682,7 @@ defmodule Pleroma.Web.ActivityPub.Utils 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, | |||
@@ -59,7 +59,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do | |||
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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController 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 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do | |||
) | |||
end | |||
@filters ~w(local external active deactivated) | |||
@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: %{} | |||
@spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{} | |||
defp maybe_parse_filters(filters) do | |||
filters | |||
|> String.split(",") | |||
@@ -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 |
@@ -74,7 +74,7 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do | |||
password_confirmation: random_password | |||
}, | |||
external: true, | |||
confirmed: true | |||
need_confirmation: false | |||
) | |||
|> Repo.insert(), | |||
{:ok, _} <- | |||
@@ -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) | |||
@@ -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 | |||
@@ -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 @@ defmodule Pleroma.Web.Federator 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 @@ defmodule Pleroma.Web.Federator 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 @@ defmodule Pleroma.Web.Federator 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 @@ defmodule Pleroma.Web.Federator 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 | |||
@@ -0,0 +1,95 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> | |||
# 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 |
@@ -8,6 +8,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
alias Pleroma.Activity | |||
alias Pleroma.Bookmark | |||
alias Pleroma.Config | |||
alias Pleroma.Conversation.Participation | |||
alias Pleroma.Filter | |||
alias Pleroma.Formatter | |||
alias Pleroma.Notification | |||
@@ -24,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 | |||
@@ -35,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 | |||
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 = ControllerHelper.oauth_scopes(params, ["read"]) | |||
scopes = Scopes.fetch_scopes(params, ["read"]) | |||
app_attrs = | |||
params | |||
@@ -165,7 +178,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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) | |||
@@ -293,8 +306,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
|> ActivityPub.contain_timeline(user) | |||
|> Enum.reverse() | |||
user = Repo.preload(user, bookmarks: :activity) | |||
conn | |||
|> add_link_headers(:home_timeline, activities) | |||
|> put_view(StatusView) | |||
@@ -313,8 +324,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
|> ActivityPub.fetch_public_activities() | |||
|> Enum.reverse() | |||
user = Repo.preload(user, bookmarks: :activity) | |||
conn | |||
|> add_link_headers(:public_timeline, activities, false, %{"local" => local_only}) | |||
|> put_view(StatusView) | |||
@@ -322,8 +331,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
end | |||
def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do | |||
with %User{} = user <- User.get_cached_by_id(params["id"]), | |||
reading_user <- Repo.preload(reading_user, :bookmarks) do | |||
with %User{} = user <- User.get_cached_by_id(params["id"]) do | |||
activities = ActivityPub.fetch_user_activities(user, reading_user, params) | |||
conn | |||
@@ -350,8 +358,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
|> ActivityPub.fetch_activities_query(params) | |||
|> Pagination.fetch_paginated(params) | |||
user = Repo.preload(user, bookmarks: :activity) | |||
conn | |||
|> add_link_headers(:dm_timeline, activities) | |||
|> put_view(StatusView) | |||
@@ -361,8 +367,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do | |||
with %Activity{} = activity <- Activity.get_by_id_with_object(id), | |||
true <- Visibility.visible_for_user?(activity, user) do | |||
user = Repo.preload(user, bookmarks: :activity) | |||
conn | |||
|> put_view(StatusView) | |||
|> try_render("status.json", %{activity: activity, for: user}) | |||
@@ -512,8 +516,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do | |||
with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user), | |||
%Activity{} = announce <- Activity.normalize(announce.data) do | |||
user = Repo.preload(user, bookmarks: :activity) | |||
conn | |||
|> put_view(StatusView) | |||
|> try_render("status.json", %{activity: announce, for: user, as: :activity}) | |||
@@ -523,8 +525,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do | |||
with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user), | |||
%Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do | |||
user = Repo.preload(user, bookmarks: :activity) | |||
conn | |||
|> put_view(StatusView) | |||
|> try_render("status.json", %{activity: activity, for: user, as: :activity}) | |||
@@ -575,8 +575,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
%User{} = user <- User.get_cached_by_nickname(user.nickname), | |||
true <- Visibility.visible_for_user?(activity, user), | |||
{:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do | |||
user = Repo.preload(user, bookmarks: :activity) | |||
conn | |||
|> put_view(StatusView) | |||
|> try_render("status.json", %{activity: activity, for: user, as: :activity}) | |||
@@ -588,8 +586,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
%User{} = user <- User.get_cached_by_nickname(user.nickname), | |||
true <- Visibility.visible_for_user?(activity, user), | |||
{:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do | |||
user = Repo.preload(user, bookmarks: :activity) | |||
conn | |||
|> put_view(StatusView) | |||
|> try_render("status.json", %{activity: activity, for: user, as: :activity}) | |||
@@ -1110,8 +1106,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
ActivityPub.fetch_activities([], params) | |||
|> Enum.reverse() | |||
user = Repo.preload(user, bookmarks: :activity) | |||
conn | |||
|> add_link_headers(:favourites, activities) | |||
|> put_view(StatusView) | |||
@@ -1157,7 +1151,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
def bookmarks(%{assigns: %{user: user}} = conn, params) do | |||
user = User.get_cached_by_id(user.id) | |||
user = Repo.preload(user, bookmarks: :activity) | |||
bookmarks = | |||
Bookmark.for_user_query(user.id) | |||
@@ -1165,7 +1158,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
activities = | |||
bookmarks | |||
|> Enum.map(fn b -> b.activity end) | |||
|> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end) | |||
conn | |||
|> add_link_headers(:bookmarks, bookmarks) | |||
@@ -1274,8 +1267,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
|> ActivityPub.fetch_activities_bounded(following, params) | |||
|> Enum.reverse() | |||
user = Repo.preload(user, bookmarks: :activity) | |||
conn | |||
|> put_view(StatusView) | |||
|> render("index.json", %{activities: activities, for: user, as: :activity}) | |||
@@ -1555,7 +1546,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do | |||
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 | |||
} | |||
@@ -1712,6 +1703,78 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController 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) | |||
@@ -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 |
@@ -75,18 +75,22 @@ defmodule Pleroma.Web.MastodonAPI.StatusView 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] && CommonAPI.bookmarked?(opts[:for], reblogged_activity) | |||
bookmarked = Activity.get_bookmark(reblogged_activity, opts[:for]) != nil | |||
mentions = | |||
activity.recipients | |||
@@ -96,8 +100,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do | |||
%{ | |||
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, | |||
@@ -149,7 +153,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do | |||
favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || []) | |||
bookmarked = opts[:for] && CommonAPI.bookmarked?(opts[:for], activity) | |||
bookmarked = Activity.get_bookmark(activity, opts[:for]) != nil | |||
attachment_data = object.data["attachment"] || [] | |||
attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment) | |||
@@ -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 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do | |||
name: Pleroma.Application.name() |> String.downcase(), | |||
version: Pleroma.Application.version() | |||
}, | |||
protocols: ["ostatus", "activitypub"], | |||
protocols: Publisher.gather_nodeinfo_protocol_names(), | |||
services: %{ | |||
inbound: [], | |||
outbound: [] | |||
@@ -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 |
@@ -7,6 +7,7 @@ defmodule Pleroma.Web.OAuth.App do | |||
import Ecto.Changeset | |||
@type t :: %__MODULE__{} | |||
schema "apps" do | |||
field(:client_name, :string) | |||
field(:redirect_uris, :string) | |||
@@ -14,6 +14,7 @@ defmodule Pleroma.Web.OAuth.Authorization do | |||
import Ecto.Query | |||
@type t :: %__MODULE__{} | |||
schema "oauth_authorizations" do | |||
field(:token, :string) | |||
field(:scopes, {:array, :string}, default: []) | |||
@@ -25,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 | |||
} | |||
|> 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 | |||
Repo.insert(authorization) | |||
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})) | |||
@@ -57,6 +75,7 @@ defmodule Pleroma.Web.OAuth.Authorization 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, | |||
@@ -15,8 +15,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||
alias Pleroma.Web.OAuth.Token | |||
alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken | |||
alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken | |||
import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2] | |||
alias Pleroma.Web.OAuth.Scopes | |||
if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth) | |||
@@ -57,7 +56,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||
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(), %{ | |||
@@ -113,7 +112,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||
defp handle_create_authorization_error( | |||
conn, | |||
{scopes_issue, _}, | |||
{:error, scopes_issue}, | |||
%{"authorization" => _} = params | |||
) | |||
when scopes_issue in [:unsupported_scopes, :missing_scopes] do | |||
@@ -184,9 +183,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||
%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 | |||
json(conn, response_token(user, token)) | |||
@@ -221,6 +218,28 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||
token_exchange(conn, params) | |||
end | |||
def token_exchange(conn, %{"grant_type" => "client_credentials"} = params) do | |||
with %App{} = app <- get_app_from_request(conn, params), | |||
{: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) | |||
@@ -247,14 +266,15 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||
@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 | |||
@@ -318,7 +338,7 @@ defmodule Pleroma.Web.OAuth.OAuthController 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 | |||
@@ -326,7 +346,7 @@ defmodule Pleroma.Web.OAuth.OAuthController 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"] | |||
}) | |||
@@ -401,10 +421,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||
{: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 | |||
@@ -458,4 +475,12 @@ defmodule Pleroma.Web.OAuth.OAuthController do | |||
} | |||
|> 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 |
@@ -0,0 +1,67 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> | |||
# 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 |
@@ -45,12 +45,16 @@ defmodule Pleroma.Web.OAuth.Token do | |||
|> 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 | |||
user = if auth.user_id, do: User.get_cached_by_id(auth.user_id), else: %User{} | |||
create_token( | |||
app, | |||
User.get_cached_by_id(auth.user_id), | |||
user, | |||
%{scopes: auth.scopes} | |||
) | |||
end | |||
@@ -81,12 +85,13 @@ defmodule Pleroma.Web.OAuth.Token do | |||
|> 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, :user_id, :app_id]) | |||
|> validate_required([:scopes, :app_id]) | |||
|> put_valid_until(attrs) | |||
|> put_token | |||
|> put_token() | |||
|> put_refresh_token(attrs) | |||
|> Repo.insert() | |||
end | |||
@@ -18,15 +18,18 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter 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 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do | |||
[]} | |||
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 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do | |||
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 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do | |||
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"]) | |||
@@ -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 @@ defmodule Pleroma.Web.OStatus do | |||
is_nil(object) -> | |||
false | |||
object.data["type"] == "Note" -> | |||
Visibility.is_public?(activity) && object.data["type"] == "Note" -> | |||
true | |||
true -> | |||
@@ -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 | |||
@@ -277,6 +295,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) | |||
@@ -365,6 +386,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) | |||
@@ -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 | |||
@@ -165,12 +171,12 @@ defmodule Pleroma.Web.Salmon 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"}] | |||
@@ -184,11 +190,11 @@ defmodule Pleroma.Web.Salmon do | |||
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", | |||
@@ -199,13 +205,19 @@ defmodule Pleroma.Web.Salmon do | |||
"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) | |||
@@ -229,15 +241,29 @@ defmodule Pleroma.Web.Salmon do | |||
|> 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 |
@@ -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 @@ defmodule Pleroma.Web.Streamer 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 @@ defmodule Pleroma.Web.Streamer 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 @@ defmodule Pleroma.Web.Streamer do | |||
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 | |||
@@ -128,7 +128,7 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do | |||
end | |||
end | |||
def register_user(params) do | |||
def register_user(params, opts \\ []) do | |||
token = params["token"] | |||
params = %{ | |||
@@ -162,13 +162,22 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI 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 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do | |||
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} -> | |||
@@ -182,6 +182,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller 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) | |||
@@ -439,7 +440,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller 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 | |||
@@ -170,7 +170,7 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do | |||
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})) | |||
@@ -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 @@ defmodule Pleroma.Web.WebFinger 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 | |||
@@ -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 @@ defmodule Pleroma.Web.Websub 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 @@ defmodule Pleroma.Web.Websub do | |||
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 @@ defmodule Pleroma.Web.Websub do | |||
{: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 |
@@ -35,6 +35,7 @@ defmodule Pleroma.XmlBuilder do | |||
defp make_open_tag(tag, attributes) do | |||
attributes_string = | |||
for {attribute, value} <- attributes do | |||
value = String.replace(value, "\"", """) | |||
"#{attribute}=\"#{value}\"" | |||
end | |||
|> Enum.join(" ") | |||
@@ -87,7 +87,7 @@ defmodule Pleroma.Mixfile do | |||
{: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"}, | |||
@@ -113,7 +113,9 @@ defmodule Pleroma.Mixfile do | |||
{:recon, github: "ferd/recon", tag: "2.4.0"}, | |||
{:quack, "~> 0.1.1"}, | |||
{:benchee, "~> 1.0"}, | |||
{:esshd, "~> 0.1.0"} | |||
{:esshd, "~> 0.1.0"}, | |||
{:ex_rated, "~> 1.2"}, | |||
{:plug_static_index_html, "~> 1.0.0"} | |||
] ++ oauth_deps | |||
end | |||
@@ -24,10 +24,12 @@ | |||
"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"}, | |||
@@ -46,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"}, | |||
@@ -59,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"}, | |||
@@ -0,0 +1,26 @@ | |||
# Pleroma: A lightweight social networking server | |||
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> | |||
# 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 |
@@ -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 |
@@ -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 |
@@ -0,0 +1 @@ | |||
<!DOCTYPE html><html><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge,chrome=1"><meta name=renderer content=webkit><meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"><title>Admin FE</title><link rel="shortcut icon" href=favicon.ico><link href=static/css/chunk-elementUI.4296cedf.css rel=stylesheet><link href=static/css/chunk-libs.bd17d456.css rel=stylesheet><link href=static/css/app.cea15678.css rel=stylesheet></head><body><script src=/pleroma/admin/static/tinymce4.7.5/tinymce.min.js></script><div id=app></div><script type=text/javascript src=static/js/runtime.7144b2cf.js></script><script type=text/javascript src=static/js/chunk-elementUI.d388c21d.js></script><script type=text/javascript src=static/js/chunk-libs.48e79a9e.js></script><script type=text/javascript src=static/js/app.25699e3d.js></script></body></html> |
@@ -0,0 +1 @@ | |||
.errPage-container[data-v-ab9be52c]{width:800px;max-width:100%;margin:100px auto}.errPage-container .pan-back-btn[data-v-ab9be52c]{background:#008489;color:#fff;border:none!important}.errPage-container .pan-gif[data-v-ab9be52c]{margin:0 auto;display:block}.errPage-container .pan-img[data-v-ab9be52c]{display:block;margin:0 auto;width:100%}.errPage-container .text-jumbo[data-v-ab9be52c]{font-size:60px;font-weight:700;color:#484848}.errPage-container .list-unstyled[data-v-ab9be52c]{font-size:14px}.errPage-container .list-unstyled li[data-v-ab9be52c]{padding-bottom:5px}.errPage-container .list-unstyled a[data-v-ab9be52c]{color:#008489;text-decoration:none}.errPage-container .list-unstyled a[data-v-ab9be52c]:hover{text-decoration:underline} |
@@ -0,0 +1 @@ | |||
.select-field[data-v-71bc6b38]{width:350px}@media (min-device-width:768px) and (max-device-width:1024px),only screen and (max-width:760px){.select-field[data-v-71bc6b38]{width:100%;margin-bottom:5px}}.active-tag[data-v-693dba04]{color:#409eff;font-weight:700}.active-tag .el-icon-check[data-v-693dba04]{color:#409eff;float:right;margin:7px 0 0 15px}.users-container h1[data-v-693dba04]{margin:22px 0 0 15px}.users-container .pagination[data-v-693dba04]{margin:25px 0;text-align:center}.users-container .search[data-v-693dba04]{width:350px;float:right}.users-container .search-container[data-v-693dba04]{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:22px 15px}@media (min-device-width:768px) and (max-device-width:1024px),only screen and (max-width:760px){.users-container h1[data-v-693dba04]{margin:7px 10px}.users-container .el-dropdown-link[data-v-693dba04]{cursor:pointer;color:#409eff}.users-container .el-icon-arrow-down[data-v-693dba04]{font-size:12px}.users-container .search[data-v-693dba04]{width:100%}.users-container .search-container[data-v-693dba04]{display:-webkit-box;display:-ms-flexbox;display:flex;height:82px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:0 10px 7px}.users-container .el-tag[data-v-693dba04]{width:30px;display:inline-block;margin-bottom:4px;font-weight:700}.users-container .el-tag.el-tag--danger[data-v-693dba04],.users-container .el-tag.el-tag--success[data-v-693dba04]{padding-left:8px}} |
@@ -0,0 +1 @@ | |||
@supports (-webkit-mask:none) and (not (cater-color:#fff)){.login-container .el-input input{color:#fff}.login-container .el-input input:first-line{color:#eee}}.login-container .el-input{display:inline-block;height:47px;width:85%}.login-container .el-input input{background:transparent;border:0;-webkit-appearance:none;border-radius:0;padding:12px 5px 12px 15px;color:#eee;height:47px;caret-color:#fff}.login-container .el-input input:-webkit-autofill{-webkit-box-shadow:0 0 0 1000px #283443 inset!important;-webkit-text-fill-color:#fff!important}.login-container .el-form-item{border:1px solid hsla(0,0%,100%,.1);background:rgba(0,0,0,.1);border-radius:5px;color:#454545}.login-container[data-v-57350b8e]{min-height:100%;width:100%;background-color:#2d3a4b;overflow:hidden}.login-container .login-form[data-v-57350b8e]{position:relative;width:520px;max-width:100%;padding:160px 35px 0;margin:0 auto;overflow:hidden}.login-container .tips[data-v-57350b8e]{font-size:14px;color:#fff;margin-bottom:10px}.login-container .tips span[data-v-57350b8e]:first-of-type{margin-right:16px}.login-container .svg-container[data-v-57350b8e]{padding:6px 5px 6px 15px;color:#889aa4;vertical-align:middle;width:30px;display:inline-block}.login-container .title-container[data-v-57350b8e]{position:relative}.login-container .title-container .title[data-v-57350b8e]{font-size:26px;color:#eee;margin:0 auto 40px;text-align:center;font-weight:700}.login-container .title-container .set-language[data-v-57350b8e]{color:#fff;position:absolute;top:3px;font-size:18px;right:0;cursor:pointer}.login-container .show-pwd[data-v-57350b8e]{position:absolute;right:10px;top:7px;font-size:16px;color:#889aa4;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.login-container .thirdparty-button[data-v-57350b8e]{position:absolute;right:0;bottom:6px} |
@@ -0,0 +1 @@ | |||
.wscn-http404-container[data-v-b8c8aa9a]{-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);position:absolute;top:40%;left:50%}.wscn-http404[data-v-b8c8aa9a]{position:relative;width:1200px;padding:0 50px;overflow:hidden}.wscn-http404 .pic-404[data-v-b8c8aa9a]{position:relative;float:left;width:600px;overflow:hidden}.wscn-http404 .pic-404__parent[data-v-b8c8aa9a]{width:100%}.wscn-http404 .pic-404__child[data-v-b8c8aa9a]{position:absolute}.wscn-http404 .pic-404__child.left[data-v-b8c8aa9a]{width:80px;top:17px;left:220px;opacity:0;-webkit-animation-name:cloudLeft-data-v-b8c8aa9a;animation-name:cloudLeft-data-v-b8c8aa9a;-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-delay:1s;animation-delay:1s}.wscn-http404 .pic-404__child.mid[data-v-b8c8aa9a]{width:46px;top:10px;left:420px;opacity:0;-webkit-animation-name:cloudMid-data-v-b8c8aa9a;animation-name:cloudMid-data-v-b8c8aa9a;-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-delay:1.2s;animation-delay:1.2s}.wscn-http404 .pic-404__child.right[data-v-b8c8aa9a]{width:62px;top:100px;left:500px;opacity:0;-webkit-animation-name:cloudRight-data-v-b8c8aa9a;animation-name:cloudRight-data-v-b8c8aa9a;-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-delay:1s;animation-delay:1s}@-webkit-keyframes cloudLeft-data-v-b8c8aa9a{0%{top:17px;left:220px;opacity:0}20%{top:33px;left:188px;opacity:1}80%{top:81px;left:92px;opacity:1}to{top:97px;left:60px;opacity:0}}@keyframes cloudLeft-data-v-b8c8aa9a{0%{top:17px;left:220px;opacity:0}20%{top:33px;left:188px;opacity:1}80%{top:81px;left:92px;opacity:1}to{top:97px;left:60px;opacity:0}}@-webkit-keyframes cloudMid-data-v-b8c8aa9a{0%{top:10px;left:420px;opacity:0}20%{top:40px;left:360px;opacity:1}70%{top:130px;left:180px;opacity:1}to{top:160px;left:120px;opacity:0}}@keyframes cloudMid-data-v-b8c8aa9a{0%{top:10px;left:420px;opacity:0}20%{top:40px;left:360px;opacity:1}70%{top:130px;left:180px;opacity:1}to{top:160px;left:120px;opacity:0}}@-webkit-keyframes cloudRight-data-v-b8c8aa9a{0%{top:100px;left:500px;opacity:0}20%{top:120px;left:460px;opacity:1}80%{top:180px;left:340px;opacity:1}to{top:200px;left:300px;opacity:0}}@keyframes cloudRight-data-v-b8c8aa9a{0%{top:100px;left:500px;opacity:0}20%{top:120px;left:460px;opacity:1}80%{top:180px;left:340px;opacity:1}to{top:200px;left:300px;opacity:0}}.wscn-http404 .bullshit[data-v-b8c8aa9a]{position:relative;float:left;width:300px;padding:30px 0;overflow:hidden}.wscn-http404 .bullshit__oops[data-v-b8c8aa9a]{font-size:32px;line-height:40px;color:#1482f0;margin-bottom:20px;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}.wscn-http404 .bullshit__headline[data-v-b8c8aa9a],.wscn-http404 .bullshit__oops[data-v-b8c8aa9a]{font-weight:700;opacity:0;-webkit-animation-name:slideUp-data-v-b8c8aa9a;animation-name:slideUp-data-v-b8c8aa9a;-webkit-animation-duration:.5s;animation-duration:.5s}.wscn-http404 .bullshit__headline[data-v-b8c8aa9a]{font-size:20px;line-height:24px;color:#222;margin-bottom:10px;-webkit-animation-delay:.1s;animation-delay:.1s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}.wscn-http404 .bullshit__info[data-v-b8c8aa9a]{font-size:13px;line-height:21px;color:grey;margin-bottom:30px;-webkit-animation-delay:.2s;animation-delay:.2s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}.wscn-http404 .bullshit__info[data-v-b8c8aa9a],.wscn-http404 .bullshit__return-home[data-v-b8c8aa9a]{opacity:0;-webkit-animation-name:slideUp-data-v-b8c8aa9a;animation-name:slideUp-data-v-b8c8aa9a;-webkit-animation-duration:.5s;animation-duration:.5s}.wscn-http404 .bullshit__return-home[data-v-b8c8aa9a]{display:block;float:left;width:110px;height:36px;background:#1482f0;border-radius:100px;text-align:center;color:#fff;font-size:14px;line-height:36px;cursor:pointer;-webkit-animation-delay:.3s;animation-delay:.3s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}@-webkit-keyframes slideUp-data-v-b8c8aa9a{0%{-webkit-transform:translateY(60px);transform:translateY(60px);opacity:0}to{-webkit-transform:translateY(0);transform:translateY(0);opacity:1}}@keyframes slideUp-data-v-b8c8aa9a{0%{-webkit-transform:translateY(60px);transform:translateY(60px);opacity:0}to{-webkit-transform:translateY(0);transform:translateY(0);opacity:1}} |
@@ -0,0 +1 @@ | |||
/*! normalize.css v7.0.0 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,footer,header,nav,section{display:block}h1{font-size:2em;margin:.67em 0}figcaption,figure,main{display:block}figure{margin:1em 40px}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent;-webkit-text-decoration-skip:objects}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:inherit;font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}dfn{font-style:italic}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}audio,video{display:inline-block}audio:not([controls]){display:none;height:0}img{border-style:none}svg:not(:root){overflow:hidden}button,input,optgroup,select,textarea{font-family:sans-serif;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{-webkit-box-sizing:border-box;box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{display:inline-block;vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details,menu{display:block}summary{display:list-item}canvas{display:inline-block}[hidden],template{display:none}#nprogress{pointer-events:none}#nprogress .bar{background:#29d;position:fixed;z-index:1031;top:0;left:0;width:100%;height:2px}#nprogress .peg{display:block;position:absolute;right:0;width:100px;height:100%;-webkit-box-shadow:0 0 10px #29d,0 0 5px #29d;box-shadow:0 0 10px #29d,0 0 5px #29d;opacity:1;-webkit-transform:rotate(3deg) translateY(-4px);transform:rotate(3deg) translateY(-4px)}#nprogress .spinner{display:block;position:fixed;z-index:1031;top:15px;right:15px}#nprogress .spinner-icon{width:18px;height:18px;-webkit-box-sizing:border-box;box-sizing:border-box;border-color:#29d transparent transparent #29d;border-style:solid;border-width:2px;border-radius:50%;-webkit-animation:nprogress-spinner .4s linear infinite;animation:nprogress-spinner .4s linear infinite}.nprogress-custom-parent{overflow:hidden;position:relative}.nprogress-custom-parent #nprogress .bar,.nprogress-custom-parent #nprogress .spinner{position:absolute}@-webkit-keyframes nprogress-spinner{0%{-webkit-transform:rotate(0deg)}to{-webkit-transform:rotate(1turn)}}@keyframes nprogress-spinner{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}} |
@@ -0,0 +1 @@ | |||
(window.webpackJsonp=window.webpackJsonp||[]).push([["7zzA"],{"7zzA":function(e,r,n){"use strict";n.r(r);var t={beforeCreate:function(){var e=this.$route,r=e.params,n=e.query,t=r.path;this.$router.replace({path:"/"+t,query:n})},render:function(e){return e()}},o=n("KHd+"),u=Object(o.a)(t,void 0,void 0,!1,null,null,null);u.options.__file="index.vue";r.default=u.exports}}]); |
@@ -0,0 +1 @@ | |||
(window.webpackJsonp=window.webpackJsonp||[]).push([["JEtC"],{JEtC:function(o,n,i){"use strict";i.r(n);var e={name:"AuthRedirect",created:function(){var o=window.location.search.slice(1);window.opener.location.href=window.location.origin+"/login#"+o,window.close()}},t=i("KHd+"),c=Object(t.a)(e,void 0,void 0,!1,null,null,null);c.options.__file="authredirect.vue";n.default=c.exports}}]); |
@@ -0,0 +1 @@ | |||
(window.webpackJsonp=window.webpackJsonp||[]).push([["chunk-18e1"],{BF41:function(t,a,i){},"UUO+":function(t,a,i){"use strict";i.r(a);var e=i("zGwZ"),s=i.n(e),r={name:"Page401",data:function(){return{errGif:s.a+"?"+ +new Date,ewizardClap:"https://wpimg.wallstcn.com/007ef517-bafd-4066-aae4-6883632d9646",dialogVisible:!1}},methods:{back:function(){this.$route.query.noGoBack?this.$router.push({path:"/dashboard"}):this.$router.go(-1)}}},n=(i("UrVv"),i("KHd+")),l=Object(n.a)(r,function(){var t=this,a=t.$createElement,i=t._self._c||a;return i("div",{staticClass:"errPage-container"},[i("el-button",{staticClass:"pan-back-btn",attrs:{icon:"arrow-left"},on:{click:t.back}},[t._v("返回")]),t._v(" "),i("el-row",[i("el-col",{attrs:{span:12}},[i("h1",{staticClass:"text-jumbo text-ginormous"},[t._v("Oops!")]),t._v("\n gif来源"),i("a",{attrs:{href:"https://zh.airbnb.com/",target:"_blank"}},[t._v("airbnb")]),t._v(" 页面\n "),i("h2",[t._v("你没有权限去该页面")]),t._v(" "),i("h6",[t._v("如有不满请联系你领导")]),t._v(" "),i("ul",{staticClass:"list-unstyled"},[i("li",[t._v("或者你可以去:")]),t._v(" "),i("li",{staticClass:"link-type"},[i("router-link",{attrs:{to:"/dashboard"}},[t._v("回首页")])],1),t._v(" "),i("li",{staticClass:"link-type"},[i("a",{attrs:{href:"https://www.taobao.com/"}},[t._v("随便看看")])]),t._v(" "),i("li",[i("a",{attrs:{href:"#"},on:{click:function(a){a.preventDefault(),t.dialogVisible=!0}}},[t._v("点我看图")])])])]),t._v(" "),i("el-col",{attrs:{span:12}},[i("img",{attrs:{src:t.errGif,width:"313",height:"428",alt:"Girl has dropped her ice cream."}})])],1),t._v(" "),i("el-dialog",{attrs:{visible:t.dialogVisible,title:"随便看"},on:{"update:visible":function(a){t.dialogVisible=a}}},[i("img",{staticClass:"pan-img",attrs:{src:t.ewizardClap}})])],1)},[],!1,null,"ab9be52c",null);l.options.__file="401.vue";a.default=l.exports},UrVv:function(t,a,i){"use strict";var e=i("BF41");i.n(e).a},zGwZ:function(t,a,i){t.exports=i.p+"static/img/401.089007e.gif"}}]); |
@@ -0,0 +1 @@ | |||
(window.webpackJsonp=window.webpackJsonp||[]).push([["chunk-8b70"],{K3CD:function(e,t,s){},ZvHC:function(e,t,s){"use strict";var n=s("K3CD");s.n(n).a},c11S:function(e,t,s){"use strict";var n=s("gTgX");s.n(n).a},gTgX:function(e,t,s){},ntYl:function(e,t,s){"use strict";s.r(t);var n=s("J4zp"),o=s.n(n),a=s("XJYT"),r=s("wAo7"),i=s("mSNy"),l={name:"Login",components:{"svg-icon":r.a},data:function(){return{loginForm:{username:"",password:""},passwordType:"password",loading:!1,showDialog:!1,redirect:void 0}},watch:{$route:{handler:function(e){this.redirect=e.query&&e.query.redirect},immediate:!0}},methods:{showPwd:function(){"password"===this.passwordType?this.passwordType="":this.passwordType="password"},handleLogin:function(){var e=this;if(this.loading=!0,this.checkUsername()){var t=this.getLoginData();this.$store.dispatch("LoginByUsername",t).then(function(){e.loading=!1,e.$router.push({path:e.redirect||"/users/index"})}).catch(function(){e.loading=!1})}else Object(a.Message)({message:i.a.t("login.errorMessage"),type:"error",duration:7e3}),this.$store.dispatch("addErrorLog",{message:i.a.t("login.errorMessage")}),this.loading=!1},checkUsername:function(){return this.loginForm.username.includes("@")},getLoginData:function(){var e=this.loginForm.username.split("@"),t=o()(e,2),s=t[0],n=t[1];return{username:s.trim(),authHost:n.trim(),password:this.loginForm.password}}}},c=(s("c11S"),s("ZvHC"),s("KHd+")),p=Object(c.a)(l,function(){var e=this,t=e.$createElement,s=e._self._c||t;return s("div",{staticClass:"login-container"},[s("el-form",{ref:"loginForm",staticClass:"login-form",attrs:{model:e.loginForm,"auto-complete":"on","label-position":"left"}},[s("div",{staticClass:"title-container"},[s("h3",{staticClass:"title"},[e._v("\n "+e._s(e.$t("login.title"))+"\n ")])]),e._v(" "),s("el-form-item",{attrs:{prop:"username"}},[s("span",{staticClass:"svg-container"},[s("svg-icon",{attrs:{"icon-class":"user"}})],1),e._v(" "),s("el-input",{attrs:{placeholder:e.$t("login.username"),name:"username",type:"text","auto-complete":"on"},model:{value:e.loginForm.username,callback:function(t){e.$set(e.loginForm,"username",t)},expression:"loginForm.username"}})],1),e._v(" "),s("el-form-item",{attrs:{prop:"password"}},[s("span",{staticClass:"svg-container"},[s("svg-icon",{attrs:{"icon-class":"password"}})],1),e._v(" "),s("el-input",{attrs:{type:e.passwordType,placeholder:e.$t("login.password"),name:"password","auto-complete":"on"},nativeOn:{keyup:function(t){return!t.type.indexOf("key")&&e._k(t.keyCode,"enter",13,t.key,"Enter")?null:e.handleLogin(t)}},model:{value:e.loginForm.password,callback:function(t){e.$set(e.loginForm,"password",t)},expression:"loginForm.password"}}),e._v(" "),s("span",{staticClass:"show-pwd",on:{click:e.showPwd}},[s("svg-icon",{attrs:{"icon-class":"password"===e.passwordType?"eye":"eye-open"}})],1)],1),e._v(" "),s("el-button",{staticStyle:{width:"100%","margin-bottom":"30px"},attrs:{loading:e.loading,type:"primary"},nativeOn:{click:function(t){return t.preventDefault(),e.handleLogin(t)}}},[e._v("\n "+e._s(e.$t("login.logIn"))+"\n ")])],1)],1)},[],!1,null,"57350b8e",null);p.options.__file="index.vue";t.default=p.exports}}]); |
@@ -0,0 +1 @@ | |||
!function(e){function t(t){for(var r,o,c=t[0],i=t[1],f=t[2],l=0,d=[];l<c.length;l++)o=c[l],u[o]&&d.push(u[o][0]),u[o]=0;for(r in i)Object.prototype.hasOwnProperty.call(i,r)&&(e[r]=i[r]);for(s&&s(t);d.length;)d.shift()();return a.push.apply(a,f||[]),n()}function n(){for(var e,t=0;t<a.length;t++){for(var n=a[t],r=!0,o=1;o<n.length;o++){var i=n[o];0!==u[i]&&(r=!1)}r&&(a.splice(t--,1),e=c(c.s=n[0]))}return e}var r={},o={runtime:0},u={runtime:0},a=[];function c(t){if(r[t])return r[t].exports;var n=r[t]={i:t,l:!1,exports:{}};return e[t].call(n.exports,n,n.exports,c),n.l=!0,n.exports}c.e=function(e){var t=[];o[e]?t.push(o[e]):0!==o[e]&&{"chunk-18e1":1,"chunk-50cf":1,"chunk-8b70":1,"chunk-f018":1}[e]&&t.push(o[e]=new Promise(function(t,n){for(var r="static/css/"+({}[e]||e)+"."+{"7zzA":"31d6cfe0",JEtC:"31d6cfe0","chunk-18e1":"6aaab273","chunk-50cf":"1db1ed5b","chunk-8b70":"9ba0945c","chunk-f018":"0d22684d"}[e]+".css",o=c.p+r,u=document.getElementsByTagName("link"),a=0;a<u.length;a++){var i=(l=u[a]).getAttribute("data-href")||l.getAttribute("href");if("stylesheet"===l.rel&&(i===r||i===o))return t()}var f=document.getElementsByTagName("style");for(a=0;a<f.length;a++){var l;if((i=(l=f[a]).getAttribute("data-href"))===r||i===o)return t()}var s=document.createElement("link");s.rel="stylesheet",s.type="text/css",s.onload=t,s.onerror=function(t){var r=t&&t.target&&t.target.src||o,u=new Error("Loading CSS chunk "+e+" failed.\n("+r+")");u.request=r,n(u)},s.href=o,document.getElementsByTagName("head")[0].appendChild(s)}).then(function(){o[e]=0}));var n=u[e];if(0!==n)if(n)t.push(n[2]);else{var r=new Promise(function(t,r){n=u[e]=[t,r]});t.push(n[2]=r);var a,i=document.createElement("script");i.charset="utf-8",i.timeout=120,c.nc&&i.setAttribute("nonce",c.nc),i.src=function(e){return c.p+"static/js/"+({}[e]||e)+"."+{"7zzA":"e1ae1c94",JEtC:"f9ba4594","chunk-18e1":"7f9c377c","chunk-50cf":"b9b1df43","chunk-8b70":"46525646","chunk-f018":"e1a7a454"}[e]+".js"}(e),a=function(t){i.onerror=i.onload=null,clearTimeout(f);var n=u[e];if(0!==n){if(n){var r=t&&("load"===t.type?"missing":t.type),o=t&&t.target&&t.target.src,a=new Error("Loading chunk "+e+" failed.\n("+r+": "+o+")");a.type=r,a.request=o,n[1](a)}u[e]=void 0}};var f=setTimeout(function(){a({type:"timeout",target:i})},12e4);i.onerror=i.onload=a,document.head.appendChild(i)}return Promise.all(t)},c.m=e,c.c=r,c.d=function(e,t,n){c.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},c.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},c.t=function(e,t){if(1&t&&(e=c(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(c.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)c.d(n,r,function(t){return e[t]}.bind(null,r));return n},c.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return c.d(t,"a",t),t},c.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},c.p="",c.oe=function(e){throw console.error(e),e};var i=window.webpackJsonp=window.webpackJsonp||[],f=i.push.bind(i);i.push=t,i=i.slice();for(var l=0;l<i.length;l++)t(i[l]);var s=f;n()}([]); |
@@ -0,0 +1,230 @@ | |||
tinymce.addI18n('zh_CN',{ | |||
"Cut": "\u526a\u5207", | |||
"Heading 5": "\u6807\u98985", | |||
"Header 2": "\u6807\u98982", | |||
"Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X\/C\/V keyboard shortcuts instead.": "\u4f60\u7684\u6d4f\u89c8\u5668\u4e0d\u652f\u6301\u5bf9\u526a\u8d34\u677f\u7684\u8bbf\u95ee\uff0c\u8bf7\u4f7f\u7528Ctrl+X\/C\/V\u952e\u8fdb\u884c\u590d\u5236\u7c98\u8d34\u3002", | |||
"Heading 4": "\u6807\u98984", | |||
"Div": "Div\u533a\u5757", | |||
"Heading 2": "\u6807\u98982", | |||
"Paste": "\u7c98\u8d34", | |||
"Close": "\u5173\u95ed", | |||
"Font Family": "\u5b57\u4f53", | |||
"Pre": "\u9884\u683c\u5f0f\u6587\u672c", | |||
"Align right": "\u53f3\u5bf9\u9f50", | |||
"New document": "\u65b0\u6587\u6863", | |||
"Blockquote": "\u5f15\u7528", | |||
"Numbered list": "\u7f16\u53f7\u5217\u8868", | |||
"Heading 1": "\u6807\u98981", | |||
"Headings": "\u6807\u9898", | |||
"Increase indent": "\u589e\u52a0\u7f29\u8fdb", | |||
"Formats": "\u683c\u5f0f", | |||
"Headers": "\u6807\u9898", | |||
"Select all": "\u5168\u9009", | |||
"Header 3": "\u6807\u98983", | |||
"Blocks": "\u533a\u5757", | |||
"Undo": "\u64a4\u6d88", | |||
"Strikethrough": "\u5220\u9664\u7ebf", | |||
"Bullet list": "\u9879\u76ee\u7b26\u53f7", | |||
"Header 1": "\u6807\u98981", | |||
"Superscript": "\u4e0a\u6807", | |||
"Clear formatting": "\u6e05\u9664\u683c\u5f0f", | |||
"Font Sizes": "\u5b57\u53f7", | |||
"Subscript": "\u4e0b\u6807", | |||
"Header 6": "\u6807\u98986", | |||
"Redo": "\u91cd\u590d", | |||
"Paragraph": "\u6bb5\u843d", | |||
"Ok": "\u786e\u5b9a", | |||
"Bold": "\u7c97\u4f53", | |||
"Code": "\u4ee3\u7801", | |||
"Italic": "\u659c\u4f53", | |||
"Align center": "\u5c45\u4e2d", | |||
"Header 5": "\u6807\u98985", | |||
"Heading 6": "\u6807\u98986", | |||
"Heading 3": "\u6807\u98983", | |||
"Decrease indent": "\u51cf\u5c11\u7f29\u8fdb", | |||
"Header 4": "\u6807\u98984", | |||
"Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.": "\u5f53\u524d\u4e3a\u7eaf\u6587\u672c\u7c98\u8d34\u6a21\u5f0f\uff0c\u518d\u6b21\u70b9\u51fb\u53ef\u4ee5\u56de\u5230\u666e\u901a\u7c98\u8d34\u6a21\u5f0f\u3002", | |||
"Underline": "\u4e0b\u5212\u7ebf", | |||
"Cancel": "\u53d6\u6d88", | |||
"Justify": "\u4e24\u7aef\u5bf9\u9f50", | |||
"Inline": "\u6587\u672c", | |||
"Copy": "\u590d\u5236", | |||
"Align left": "\u5de6\u5bf9\u9f50", | |||
"Visual aids": "\u7f51\u683c\u7ebf", | |||
"Lower Greek": "\u5c0f\u5199\u5e0c\u814a\u5b57\u6bcd", | |||
"Square": "\u65b9\u5757", | |||
"Default": "\u9ed8\u8ba4", | |||
"Lower Alpha": "\u5c0f\u5199\u82f1\u6587\u5b57\u6bcd", | |||
"Circle": "\u7a7a\u5fc3\u5706", | |||
"Disc": "\u5b9e\u5fc3\u5706", | |||
"Upper Alpha": "\u5927\u5199\u82f1\u6587\u5b57\u6bcd", | |||
"Upper Roman": "\u5927\u5199\u7f57\u9a6c\u5b57\u6bcd", | |||
"Lower Roman": "\u5c0f\u5199\u7f57\u9a6c\u5b57\u6bcd", | |||
"Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.": "\u6807\u8bc6\u7b26\u5e94\u8be5\u4ee5\u5b57\u6bcd\u5f00\u5934\uff0c\u540e\u8ddf\u5b57\u6bcd\u3001\u6570\u5b57\u3001\u7834\u6298\u53f7\u3001\u70b9\u3001\u5192\u53f7\u6216\u4e0b\u5212\u7ebf\u3002", | |||
"Name": "\u540d\u79f0", | |||
"Anchor": "\u951a\u70b9", | |||
"Id": "\u6807\u8bc6\u7b26", | |||
"You have unsaved changes are you sure you want to navigate away?": "\u4f60\u8fd8\u6709\u6587\u6863\u5c1a\u672a\u4fdd\u5b58\uff0c\u786e\u5b9a\u8981\u79bb\u5f00\uff1f", | |||
"Restore last draft": "\u6062\u590d\u4e0a\u6b21\u7684\u8349\u7a3f", | |||
"Special character": "\u7279\u6b8a\u7b26\u53f7", | |||
"Source code": "\u6e90\u4ee3\u7801", | |||
"Language": "\u8bed\u8a00", | |||
"Insert\/Edit code sample": "\u63d2\u5165\/\u7f16\u8f91\u4ee3\u7801\u793a\u4f8b", | |||
"B": "B", | |||
"R": "R", | |||
"G": "G", | |||
"Color": "\u989c\u8272", | |||
"Right to left": "\u4ece\u53f3\u5230\u5de6", | |||
"Left to right": "\u4ece\u5de6\u5230\u53f3", | |||
"Emoticons": "\u8868\u60c5", | |||
"Robots": "\u673a\u5668\u4eba", | |||
"Document properties": "\u6587\u6863\u5c5e\u6027", | |||
"Title": "\u6807\u9898", | |||
"Keywords": "\u5173\u952e\u8bcd", | |||
"Encoding": "\u7f16\u7801", | |||
"Description": "\u63cf\u8ff0", | |||
"Author": "\u4f5c\u8005", | |||
"Fullscreen": "\u5168\u5c4f", | |||
"Horizontal line": "\u6c34\u5e73\u5206\u5272\u7ebf", | |||
"Horizontal space": "\u6c34\u5e73\u8fb9\u8ddd", | |||
"Insert\/edit image": "\u63d2\u5165\/\u7f16\u8f91\u56fe\u7247", | |||
"General": "\u666e\u901a", | |||
"Advanced": "\u9ad8\u7ea7", | |||
"Source": "\u5730\u5740", | |||
"Border": "\u8fb9\u6846", | |||
"Constrain proportions": "\u4fdd\u6301\u7eb5\u6a2a\u6bd4", | |||
"Vertical space": "\u5782\u76f4\u8fb9\u8ddd", | |||
"Image description": "\u56fe\u7247\u63cf\u8ff0", | |||
"Style": "\u6837\u5f0f", | |||
"Dimensions": "\u5927\u5c0f", | |||
"Insert image": "\u63d2\u5165\u56fe\u7247", | |||
"Image": "\u56fe\u7247", | |||
"Zoom in": "\u653e\u5927", | |||
"Contrast": "\u5bf9\u6bd4\u5ea6", | |||
"Back": "\u540e\u9000", | |||
"Gamma": "\u4f3d\u9a6c\u503c", | |||
"Flip horizontally": "\u6c34\u5e73\u7ffb\u8f6c", | |||
"Resize": "\u8c03\u6574\u5927\u5c0f", | |||
"Sharpen": "\u9510\u5316", | |||
"Zoom out": "\u7f29\u5c0f", | |||
"Image options": "\u56fe\u7247\u9009\u9879", | |||
"Apply": "\u5e94\u7528", | |||
"Brightness": "\u4eae\u5ea6", | |||
"Rotate clockwise": "\u987a\u65f6\u9488\u65cb\u8f6c", | |||
"Rotate counterclockwise": "\u9006\u65f6\u9488\u65cb\u8f6c", | |||
"Edit image": "\u7f16\u8f91\u56fe\u7247", | |||
"Color levels": "\u989c\u8272\u5c42\u6b21", | |||
"Crop": "\u88c1\u526a", | |||
"Orientation": "\u65b9\u5411", | |||
"Flip vertically": "\u5782\u76f4\u7ffb\u8f6c", | |||
"Invert": "\u53cd\u8f6c", | |||
"Date\/time": "\u65e5\u671f\/\u65f6\u95f4", | |||
"Insert date\/time": "\u63d2\u5165\u65e5\u671f\/\u65f6\u95f4", | |||
"Remove link": "\u5220\u9664\u94fe\u63a5", | |||
"Url": "\u5730\u5740", | |||
"Text to display": "\u663e\u793a\u6587\u5b57", | |||
"Anchors": "\u951a\u70b9", | |||
"Insert link": "\u63d2\u5165\u94fe\u63a5", | |||
"Link": "\u94fe\u63a5", | |||
"New window": "\u5728\u65b0\u7a97\u53e3\u6253\u5f00", | |||
"None": "\u65e0", | |||
"The URL you entered seems to be an external link. Do you want to add the required http:\/\/ prefix?": "\u4f60\u6240\u586b\u5199\u7684URL\u5730\u5740\u5c5e\u4e8e\u5916\u90e8\u94fe\u63a5\uff0c\u9700\u8981\u52a0\u4e0ahttp:\/\/:\u524d\u7f00\u5417\uff1f", | |||
"Paste or type a link": "\u7c98\u8d34\u6216\u8f93\u5165\u94fe\u63a5", | |||
"Target": "\u6253\u5f00\u65b9\u5f0f", | |||
"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "\u4f60\u6240\u586b\u5199\u7684URL\u5730\u5740\u4e3a\u90ae\u4ef6\u5730\u5740\uff0c\u9700\u8981\u52a0\u4e0amailto:\u524d\u7f00\u5417\uff1f", | |||
"Insert\/edit link": "\u63d2\u5165\/\u7f16\u8f91\u94fe\u63a5", | |||
"Insert\/edit video": "\u63d2\u5165\/\u7f16\u8f91\u89c6\u9891", | |||
"Media": "\u5a92\u4f53", | |||
"Alternative source": "\u955c\u50cf", | |||
"Paste your embed code below:": "\u5c06\u5185\u5d4c\u4ee3\u7801\u7c98\u8d34\u5728\u4e0b\u9762:", | |||
"Insert video": "\u63d2\u5165\u89c6\u9891", | |||
"Poster": "\u5c01\u9762", | |||
"Insert\/edit media": "\u63d2\u5165\/\u7f16\u8f91\u5a92\u4f53", | |||
"Embed": "\u5185\u5d4c", | |||
"Nonbreaking space": "\u4e0d\u95f4\u65ad\u7a7a\u683c", | |||
"Page break": "\u5206\u9875\u7b26", | |||
"Paste as text": "\u7c98\u8d34\u4e3a\u6587\u672c", | |||
"Preview": "\u9884\u89c8", | |||
"Print": "\u6253\u5370", | |||
"Save": "\u4fdd\u5b58", | |||
"Could not find the specified string.": "\u672a\u627e\u5230\u641c\u7d22\u5185\u5bb9.", | |||
"Replace": "\u66ff\u6362", | |||
"Next": "\u4e0b\u4e00\u4e2a", | |||
"Whole words": "\u5168\u5b57\u5339\u914d", | |||
"Find and replace": "\u67e5\u627e\u548c\u66ff\u6362", | |||
"Replace with": "\u66ff\u6362\u4e3a", | |||
"Find": "\u67e5\u627e", | |||
"Replace all": "\u5168\u90e8\u66ff\u6362", | |||
"Match case": "\u533a\u5206\u5927\u5c0f\u5199", | |||
"Prev": "\u4e0a\u4e00\u4e2a", | |||
"Spellcheck": "\u62fc\u5199\u68c0\u67e5", | |||
"Finish": "\u5b8c\u6210", | |||
"Ignore all": "\u5168\u90e8\u5ffd\u7565", | |||
"Ignore": "\u5ffd\u7565", | |||
"Add to Dictionary": "\u6dfb\u52a0\u5230\u5b57\u5178", | |||
"Insert row before": "\u5728\u4e0a\u65b9\u63d2\u5165", | |||
"Rows": "\u884c", | |||
"Height": "\u9ad8", | |||
"Paste row after": "\u7c98\u8d34\u5230\u4e0b\u65b9", | |||
"Alignment": "\u5bf9\u9f50\u65b9\u5f0f", | |||
"Border color": "\u8fb9\u6846\u989c\u8272", | |||
"Column group": "\u5217\u7ec4", | |||
"Row": "\u884c", | |||
"Insert column before": "\u5728\u5de6\u4fa7\u63d2\u5165", | |||
"Split cell": "\u62c6\u5206\u5355\u5143\u683c", | |||
"Cell padding": "\u5355\u5143\u683c\u5185\u8fb9\u8ddd", | |||
"Cell spacing": "\u5355\u5143\u683c\u5916\u95f4\u8ddd", | |||
"Row type": "\u884c\u7c7b\u578b", | |||
"Insert table": "\u63d2\u5165\u8868\u683c", | |||
"Body": "\u8868\u4f53", | |||
"Caption": "\u6807\u9898", | |||
"Footer": "\u8868\u5c3e", | |||
"Delete row": "\u5220\u9664\u884c", | |||
"Paste row before": "\u7c98\u8d34\u5230\u4e0a\u65b9", | |||
"Scope": "\u8303\u56f4", | |||
"Delete table": "\u5220\u9664\u8868\u683c", | |||
"H Align": "\u6c34\u5e73\u5bf9\u9f50", | |||
"Top": "\u9876\u90e8\u5bf9\u9f50", | |||
"Header cell": "\u8868\u5934\u5355\u5143\u683c", | |||
"Column": "\u5217", | |||
"Row group": "\u884c\u7ec4", | |||
"Cell": "\u5355\u5143\u683c", | |||
"Middle": "\u5782\u76f4\u5c45\u4e2d", | |||
"Cell type": "\u5355\u5143\u683c\u7c7b\u578b", | |||
"Copy row": "\u590d\u5236\u884c", | |||
"Row properties": "\u884c\u5c5e\u6027", | |||
"Table properties": "\u8868\u683c\u5c5e\u6027", | |||
"Bottom": "\u5e95\u90e8\u5bf9\u9f50", | |||
"V Align": "\u5782\u76f4\u5bf9\u9f50", | |||
"Header": "\u8868\u5934", | |||
"Right": "\u53f3\u5bf9\u9f50", | |||
"Insert column after": "\u5728\u53f3\u4fa7\u63d2\u5165", | |||
"Cols": "\u5217", | |||
"Insert row after": "\u5728\u4e0b\u65b9\u63d2\u5165", | |||
"Width": "\u5bbd", | |||
"Cell properties": "\u5355\u5143\u683c\u5c5e\u6027", | |||
"Left": "\u5de6\u5bf9\u9f50", | |||
"Cut row": "\u526a\u5207\u884c", | |||
"Delete column": "\u5220\u9664\u5217", | |||
"Center": "\u5c45\u4e2d", | |||
"Merge cells": "\u5408\u5e76\u5355\u5143\u683c", | |||
"Insert template": "\u63d2\u5165\u6a21\u677f", | |||
"Templates": "\u6a21\u677f", | |||
"Background color": "\u80cc\u666f\u8272", | |||
"Custom...": "\u81ea\u5b9a\u4e49...", | |||
"Custom color": "\u81ea\u5b9a\u4e49\u989c\u8272", | |||
"No color": "\u65e0", | |||
"Text color": "\u6587\u5b57\u989c\u8272", | |||
"Table of Contents": "\u5185\u5bb9\u5217\u8868", | |||
"Show blocks": "\u663e\u793a\u533a\u5757\u8fb9\u6846", | |||
"Show invisible characters": "\u663e\u793a\u4e0d\u53ef\u89c1\u5b57\u7b26", | |||
"Words: {0}": "\u5b57\u6570\uff1a{0}", | |||
"Insert": "\u63d2\u5165", | |||
"File": "\u6587\u4ef6", | |||
"Edit": "\u7f16\u8f91", | |||
"Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "\u5728\u7f16\u8f91\u533a\u6309ALT-F9\u6253\u5f00\u83dc\u5355\uff0c\u6309ALT-F10\u6253\u5f00\u5de5\u5177\u680f\uff0c\u6309ALT-0\u67e5\u770b\u5e2e\u52a9", | |||
"Tools": "\u5de5\u5177", | |||
"View": "\u89c6\u56fe", | |||
"Table": "\u8868\u683c", | |||
"Format": "\u683c\u5f0f" | |||
}); |
@@ -0,0 +1,138 @@ | |||
/* http://prismjs.com/download.html?themes=prism&languages=markup+css+clike+javascript */ | |||
/** | |||
* prism.js default theme for JavaScript, CSS and HTML | |||
* Based on dabblet (http://dabblet.com) | |||
* @author Lea Verou | |||
*/ | |||
code[class*="language-"], | |||
pre[class*="language-"] { | |||
color: black; | |||
text-shadow: 0 1px white; | |||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; | |||
direction: ltr; | |||
text-align: left; | |||
white-space: pre; | |||
word-spacing: normal; | |||
word-break: normal; | |||
word-wrap: normal; | |||
line-height: 1.5; | |||
-moz-tab-size: 4; | |||
-o-tab-size: 4; | |||
tab-size: 4; | |||
-webkit-hyphens: none; | |||
-moz-hyphens: none; | |||
-ms-hyphens: none; | |||
hyphens: none; | |||
} | |||
pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, | |||
code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { | |||
text-shadow: none; | |||
background: #b3d4fc; | |||
} | |||
pre[class*="language-"]::selection, pre[class*="language-"] ::selection, | |||
code[class*="language-"]::selection, code[class*="language-"] ::selection { | |||
text-shadow: none; | |||
background: #b3d4fc; | |||
} | |||
@media print { | |||
code[class*="language-"], | |||
pre[class*="language-"] { | |||
text-shadow: none; | |||
} | |||
} | |||
/* Code blocks */ | |||
pre[class*="language-"] { | |||
padding: 1em; | |||
margin: .5em 0; | |||
overflow: auto; | |||
} | |||
:not(pre) > code[class*="language-"], | |||
pre[class*="language-"] { | |||
background: #f5f2f0; | |||
} | |||
/* Inline code */ | |||
:not(pre) > code[class*="language-"] { | |||
padding: .1em; | |||
border-radius: .3em; | |||
} | |||
.token.comment, | |||
.token.prolog, | |||
.token.doctype, | |||
.token.cdata { | |||
color: slategray; | |||
} | |||
.token.punctuation { | |||
color: #999; | |||
} | |||
.namespace { | |||
opacity: .7; | |||
} | |||
.token.property, | |||
.token.tag, | |||
.token.boolean, | |||
.token.number, | |||
.token.constant, | |||
.token.symbol, | |||
.token.deleted { | |||
color: #905; | |||
} | |||
.token.selector, | |||
.token.attr-name, | |||
.token.string, | |||
.token.char, | |||
.token.builtin, | |||
.token.inserted { | |||
color: #690; | |||
} | |||
.token.operator, | |||
.token.entity, | |||
.token.url, | |||
.language-css .token.string, | |||
.style .token.string { | |||
color: #a67f59; | |||
background: hsla(0, 0%, 100%, .5); | |||
} | |||
.token.atrule, | |||
.token.attr-value, | |||
.token.keyword { | |||
color: #07a; | |||
} | |||
.token.function { | |||
color: #DD4A68; | |||
} | |||
.token.regex, | |||
.token.important, | |||
.token.variable { | |||
color: #e90; | |||
} | |||
.token.important, | |||
.token.bold { | |||
font-weight: bold; | |||
} | |||
.token.italic { | |||
font-style: italic; | |||
} | |||
.token.entity { | |||
cursor: help; | |||
} | |||