Browse Source

Merge remote-tracking branch 'pleroma/develop' into feature/disable-account

tags/v1.1.4
Egor Kislitsyn 5 years ago
parent
commit
5e2b491276
100 changed files with 1932 additions and 607 deletions
  1. +4
    -0
      .gitignore
  2. +6
    -1
      CHANGELOG.md
  3. +8
    -0
      COPYING
  4. +1
    -1
      README.md
  5. +7
    -0
      config/config.exs
  6. +18
    -13
      docs/api/admin_api.md
  7. +10
    -0
      docs/api/differences_in_mastoapi_responses.md
  8. +6
    -0
      docs/config.md
  9. +17
    -9
      lib/mix/tasks/pleroma/emoji.ex
  10. +1
    -1
      lib/mix/tasks/pleroma/user.ex
  11. +47
    -0
      lib/pleroma/activity.ex
  12. +1
    -1
      lib/pleroma/captcha/kocaptcha.ex
  13. +75
    -0
      lib/pleroma/conversation.ex
  14. +81
    -0
      lib/pleroma/conversation/participation.ex
  15. +0
    -2
      lib/pleroma/object/containment.ex
  16. +40
    -8
      lib/pleroma/plugs/oauth_plug.ex
  17. +36
    -0
      lib/pleroma/plugs/rate_limit_plug.ex
  18. +6
    -3
      lib/pleroma/stats.ex
  19. +1
    -1
      lib/pleroma/upload.ex
  20. +2
    -2
      lib/pleroma/uploaders/swift/keystone.ex
  21. +50
    -172
      lib/pleroma/user.ex
  22. +17
    -13
      lib/pleroma/user/info.ex
  23. +156
    -0
      lib/pleroma/user/query.ex
  24. +74
    -109
      lib/pleroma/web/activity_pub/activity_pub.ex
  25. +2
    -0
      lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex
  26. +1
    -0
      lib/pleroma/web/activity_pub/mrf/drop_policy.ex
  27. +1
    -0
      lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex
  28. +2
    -0
      lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex
  29. +2
    -0
      lib/pleroma/web/activity_pub/mrf/keyword_policy.ex
  30. +1
    -0
      lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex
  31. +1
    -0
      lib/pleroma/web/activity_pub/mrf/noop_policy.ex
  32. +1
    -0
      lib/pleroma/web/activity_pub/mrf/normalize_markup.ex
  33. +1
    -0
      lib/pleroma/web/activity_pub/mrf/reject_non_public.ex
  34. +1
    -0
      lib/pleroma/web/activity_pub/mrf/simple_policy.ex
  35. +13
    -0
      lib/pleroma/web/activity_pub/mrf/tag_policy.ex
  36. +1
    -0
      lib/pleroma/web/activity_pub/mrf/user_allowlist.ex
  37. +152
    -0
      lib/pleroma/web/activity_pub/publisher.ex
  38. +1
    -1
      lib/pleroma/web/activity_pub/utils.ex
  39. +7
    -4
      lib/pleroma/web/admin_api/admin_api_controller.ex
  40. +11
    -33
      lib/pleroma/web/admin_api/search.ex
  41. +1
    -1
      lib/pleroma/web/auth/pleroma_authenticator.ex
  42. +0
    -6
      lib/pleroma/web/controller_helper.ex
  43. +7
    -0
      lib/pleroma/web/endpoint.ex
  44. +3
    -48
      lib/pleroma/web/federator/federator.ex
  45. +95
    -0
      lib/pleroma/web/federator/publisher.ex
  46. +90
    -27
      lib/pleroma/web/mastodon_api/mastodon_api_controller.ex
  47. +38
    -0
      lib/pleroma/web/mastodon_api/views/conversation_view.ex
  48. +11
    -7
      lib/pleroma/web/mastodon_api/views/status_view.ex
  49. +2
    -1
      lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
  50. +0
    -14
      lib/pleroma/web/oauth.ex
  51. +1
    -0
      lib/pleroma/web/oauth/app.ex
  52. +29
    -10
      lib/pleroma/web/oauth/authorization.ex
  53. +41
    -16
      lib/pleroma/web/oauth/oauth_controller.ex
  54. +67
    -0
      lib/pleroma/web/oauth/scopes.ex
  55. +8
    -3
      lib/pleroma/web/oauth/token.ex
  56. +11
    -10
      lib/pleroma/web/ostatus/activity_representer.ex
  57. +2
    -1
      lib/pleroma/web/ostatus/ostatus.ex
  58. +34
    -11
      lib/pleroma/web/router.ex
  59. +38
    -12
      lib/pleroma/web/salmon/salmon.ex
  60. +29
    -0
      lib/pleroma/web/streamer.ex
  61. +17
    -12
      lib/pleroma/web/twitter_api/twitter_api.ex
  62. +2
    -1
      lib/pleroma/web/twitter_api/twitter_api_controller.ex
  63. +1
    -1
      lib/pleroma/web/twitter_api/views/activity_view.ex
  64. +18
    -48
      lib/pleroma/web/web_finger/web_finger.ex
  65. +32
    -1
      lib/pleroma/web/websub/websub.ex
  66. +1
    -0
      lib/xml_builder.ex
  67. +4
    -2
      mix.exs
  68. +4
    -1
      mix.lock
  69. +26
    -0
      priv/repo/migrations/20190408123347_create_conversations.exs
  70. +7
    -0
      priv/repo/migrations/20190410152859_add_participation_updated_at_index.exs
  71. +9
    -0
      priv/repo/migrations/20190513175809_change_hide_column_in_filter_table.exs
  72. BIN
      priv/static/adminfe/favicon.ico
  73. +1
    -0
      priv/static/adminfe/index.html
  74. +1
    -0
      priv/static/adminfe/static/css/app.cea15678.css
  75. +1
    -0
      priv/static/adminfe/static/css/chunk-18e1.6aaab273.css
  76. +1
    -0
      priv/static/adminfe/static/css/chunk-50cf.1db1ed5b.css
  77. +1
    -0
      priv/static/adminfe/static/css/chunk-8b70.9ba0945c.css
  78. +1
    -0
      priv/static/adminfe/static/css/chunk-elementUI.4296cedf.css
  79. +1
    -0
      priv/static/adminfe/static/css/chunk-f018.0d22684d.css
  80. +1
    -0
      priv/static/adminfe/static/css/chunk-libs.bd17d456.css
  81. BIN
      priv/static/adminfe/static/fonts/element-icons.2fad952.woff
  82. BIN
      priv/static/adminfe/static/fonts/element-icons.6f0a763.ttf
  83. BIN
      priv/static/adminfe/static/img/401.089007e.gif
  84. BIN
      priv/static/adminfe/static/img/404.a57b6f3.png
  85. +1
    -0
      priv/static/adminfe/static/js/7zzA.e1ae1c94.js
  86. +1
    -0
      priv/static/adminfe/static/js/JEtC.f9ba4594.js
  87. +1
    -0
      priv/static/adminfe/static/js/app.25699e3d.js
  88. +1
    -0
      priv/static/adminfe/static/js/chunk-18e1.7f9c377c.js
  89. +1
    -0
      priv/static/adminfe/static/js/chunk-50cf.b9b1df43.js
  90. +1
    -0
      priv/static/adminfe/static/js/chunk-8b70.46525646.js
  91. +1
    -0
      priv/static/adminfe/static/js/chunk-elementUI.d388c21d.js
  92. +1
    -0
      priv/static/adminfe/static/js/chunk-f018.e1a7a454.js
  93. +57
    -0
      priv/static/adminfe/static/js/chunk-libs.48e79a9e.js
  94. +1
    -0
      priv/static/adminfe/static/js/runtime.7144b2cf.js
  95. +230
    -0
      priv/static/adminfe/static/tinymce4.7.5/langs/zh_CN.js
  96. +138
    -0
      priv/static/adminfe/static/tinymce4.7.5/plugins/codesample/css/prism.css
  97. BIN
      priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-cool.gif
  98. BIN
      priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-cry.gif
  99. BIN
      priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-embarassed.gif
  100. BIN
      priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-foot-in-mouth.gif

+ 4
- 0
.gitignore View File

@@ -38,3 +38,7 @@ erl_crash.dump

# Prevent committing docs files
/priv/static/doc/*

# Code test coverage
/cover
/Elixir.*.coverdata

+ 6
- 1
CHANGELOG.md View File

@@ -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


+ 8
- 0
COPYING View File

@@ -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


+ 1
- 1
README.md View File

@@ -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



+ 7
- 0
config/config.exs View File

@@ -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.


+ 18
- 13
docs/api/admin_api.md View File

@@ -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



+ 10
- 0
docs/api/differences_in_mastoapi_responses.md View File

@@ -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.

+ 6
- 0
docs/config.md View File

@@ -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



+ 17
- 9
lib/mix/tasks/pleroma/emoji.ex View File

@@ -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

+ 1
- 1
lib/mix/tasks/pleroma/user.ex View File

@@ -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")


+ 47
- 0
lib/pleroma/activity.ex View File

@@ -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)


+ 1
- 1
lib/pleroma/captcha/kocaptcha.ex View File

@@ -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,


+ 75
- 0
lib/pleroma/conversation.ex View File

@@ -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

+ 81
- 0
lib/pleroma/conversation/participation.ex View File

@@ -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

+ 0
- 2
lib/pleroma/object/containment.ex View File

@@ -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.


+ 40
- 8
lib/pleroma/plugs/oauth_plug.ex View File

@@ -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()}


+ 36
- 0
lib/pleroma/plugs/rate_limit_plug.ex View File

@@ -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

+ 6
- 3
lib/pleroma/stats.ex View File

@@ -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}}


+ 1
- 1
lib/pleroma/upload.ex View File

@@ -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


+ 2
- 2
lib/pleroma/uploaders/swift/keystone.ex View File

@@ -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,


+ 50
- 172
lib/pleroma/user.ex View File

@@ -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


+ 17
- 13
lib/pleroma/user/info.ex View File

@@ -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



+ 156
- 0
lib/pleroma/user/query.ex View File

@@ -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

+ 74
- 109
lib/pleroma/web/activity_pub/activity_pub.ex View File

@@ -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)


+ 2
- 0
lib/pleroma/web/activity_pub/mrf/anti_followbot_policy.ex View File

@@ -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.


+ 1
- 0
lib/pleroma/web/activity_pub/mrf/drop_policy.ex View File

@@ -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


+ 1
- 0
lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex View File

@@ -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])


+ 2
- 0
lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex View File

@@ -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


+ 2
- 0
lib/pleroma/web/activity_pub/mrf/keyword_policy.ex View File

@@ -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


+ 1
- 0
lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex View File

@@ -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


+ 1
- 0
lib/pleroma/web/activity_pub/mrf/noop_policy.ex View File

@@ -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


+ 1
- 0
lib/pleroma/web/activity_pub/mrf/normalize_markup.ex View File

@@ -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


+ 1
- 0
lib/pleroma/web/activity_pub/mrf/reject_non_public.ex View File

@@ -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


+ 1
- 0
lib/pleroma/web/activity_pub/mrf/simple_policy.ex View File

@@ -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


+ 13
- 0
lib/pleroma/web/activity_pub/mrf/tag_policy.ex View File

@@ -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: []


+ 1
- 0
lib/pleroma/web/activity_pub/mrf/user_allowlist.ex View File

@@ -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}


+ 152
- 0
lib/pleroma/web/activity_pub/publisher.ex View File

@@ -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

+ 1
- 1
lib/pleroma/web/activity_pub/utils.ex View File

@@ -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,


+ 7
- 4
lib/pleroma/web/admin_api/admin_api_controller.ex View File

@@ -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(",")


+ 11
- 33
lib/pleroma/web/admin_api/search.ex View File

@@ -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

+ 1
- 1
lib/pleroma/web/auth/pleroma_authenticator.ex View File

@@ -74,7 +74,7 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do
password_confirmation: random_password
},
external: true,
confirmed: true
need_confirmation: false
)
|> Repo.insert(),
{:ok, _} <-


+ 0
- 6
lib/pleroma/web/controller_helper.ex View File

@@ -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)


+ 7
- 0
lib/pleroma/web/endpoint.ex View File

@@ -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


+ 3
- 48
lib/pleroma/web/federator/federator.ex View File

@@ -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


+ 95
- 0
lib/pleroma/web/federator/publisher.ex View File

@@ -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

+ 90
- 27
lib/pleroma/web/mastodon_api/mastodon_api_controller.ex View File

@@ -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)


+ 38
- 0
lib/pleroma/web/mastodon_api/views/conversation_view.ex View File

@@ -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

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

@@ -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)


+ 2
- 1
lib/pleroma/web/nodeinfo/nodeinfo_controller.ex View File

@@ -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: []


+ 0
- 14
lib/pleroma/web/oauth.ex View File

@@ -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

+ 1
- 0
lib/pleroma/web/oauth/app.ex View File

@@ -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)


+ 29
- 10
lib/pleroma/web/oauth/authorization.ex View File

@@ -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,


+ 41
- 16
lib/pleroma/web/oauth/oauth_controller.ex View File

@@ -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

+ 67
- 0
lib/pleroma/web/oauth/scopes.ex View File

@@ -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

+ 8
- 3
lib/pleroma/web/oauth/token.ex View File

@@ -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


+ 11
- 10
lib/pleroma/web/ostatus/activity_representer.ex View File

@@ -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"])


+ 2
- 1
lib/pleroma/web/ostatus/ostatus.ex View File

@@ -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 ->


+ 34
- 11
lib/pleroma/web/router.ex View File

@@ -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)


+ 38
- 12
lib/pleroma/web/salmon/salmon.ex View File

@@ -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

+ 29
- 0
lib/pleroma/web/streamer.ex View File

@@ -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


+ 17
- 12
lib/pleroma/web/twitter_api/twitter_api.ex View File

@@ -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} ->


+ 2
- 1
lib/pleroma/web/twitter_api/twitter_api_controller.ex View File

@@ -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


+ 1
- 1
lib/pleroma/web/twitter_api/views/activity_view.ex View File

@@ -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}))



+ 18
- 48
lib/pleroma/web/web_finger/web_finger.ex View File

@@ -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


+ 32
- 1
lib/pleroma/web/websub/websub.ex View File

@@ -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

+ 1
- 0
lib/xml_builder.ex View File

@@ -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, "\"", "&quot;")
"#{attribute}=\"#{value}\""
end
|> Enum.join(" ")


+ 4
- 2
mix.exs View File

@@ -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



+ 4
- 1
mix.lock View File

@@ -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"},


+ 26
- 0
priv/repo/migrations/20190408123347_create_conversations.exs View File

@@ -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

+ 7
- 0
priv/repo/migrations/20190410152859_add_participation_updated_at_index.exs View File

@@ -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

+ 9
- 0
priv/repo/migrations/20190513175809_change_hide_column_in_filter_table.exs View File

@@ -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

BIN
priv/static/adminfe/favicon.ico View File

Before After

+ 1
- 0
priv/static/adminfe/index.html View File

@@ -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>

+ 1
- 0
priv/static/adminfe/static/css/app.cea15678.css
File diff suppressed because it is too large
View File


+ 1
- 0
priv/static/adminfe/static/css/chunk-18e1.6aaab273.css View File

@@ -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}

+ 1
- 0
priv/static/adminfe/static/css/chunk-50cf.1db1ed5b.css View File

@@ -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}}

+ 1
- 0
priv/static/adminfe/static/css/chunk-8b70.9ba0945c.css View File

@@ -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}

+ 1
- 0
priv/static/adminfe/static/css/chunk-elementUI.4296cedf.css
File diff suppressed because it is too large
View File


+ 1
- 0
priv/static/adminfe/static/css/chunk-f018.0d22684d.css View File

@@ -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}}

+ 1
- 0
priv/static/adminfe/static/css/chunk-libs.bd17d456.css View File

@@ -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)}}

BIN
priv/static/adminfe/static/fonts/element-icons.2fad952.woff View File


BIN
priv/static/adminfe/static/fonts/element-icons.6f0a763.ttf View File


BIN
priv/static/adminfe/static/img/401.089007e.gif View File

Before After
Width: 313  |  Height: 428  |  Size: 160KB

BIN
priv/static/adminfe/static/img/404.a57b6f3.png View File

Before After
Width: 1014  |  Height: 556  |  Size: 96KB

+ 1
- 0
priv/static/adminfe/static/js/7zzA.e1ae1c94.js View File

@@ -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}}]);

+ 1
- 0
priv/static/adminfe/static/js/JEtC.f9ba4594.js View File

@@ -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}}]);

+ 1
- 0
priv/static/adminfe/static/js/app.25699e3d.js
File diff suppressed because it is too large
View File


+ 1
- 0
priv/static/adminfe/static/js/chunk-18e1.7f9c377c.js View File

@@ -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"}}]);

+ 1
- 0
priv/static/adminfe/static/js/chunk-50cf.b9b1df43.js
File diff suppressed because it is too large
View File


+ 1
- 0
priv/static/adminfe/static/js/chunk-8b70.46525646.js View File

@@ -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}}]);

+ 1
- 0
priv/static/adminfe/static/js/chunk-elementUI.d388c21d.js
File diff suppressed because it is too large
View File


+ 1
- 0
priv/static/adminfe/static/js/chunk-f018.e1a7a454.js
File diff suppressed because it is too large
View File


+ 57
- 0
priv/static/adminfe/static/js/chunk-libs.48e79a9e.js
File diff suppressed because it is too large
View File


+ 1
- 0
priv/static/adminfe/static/js/runtime.7144b2cf.js View File

@@ -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()}([]);

+ 230
- 0
priv/static/adminfe/static/tinymce4.7.5/langs/zh_CN.js View File

@@ -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"
});

+ 138
- 0
priv/static/adminfe/static/tinymce4.7.5/plugins/codesample/css/prism.css View File

@@ -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;
}


BIN
priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-cool.gif View File

Before After
Width: 18  |  Height: 18  |  Size: 354B

BIN
priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-cry.gif View File

Before After
Width: 18  |  Height: 18  |  Size: 329B

BIN
priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-embarassed.gif View File

Before After
Width: 18  |  Height: 18  |  Size: 331B

BIN
priv/static/adminfe/static/tinymce4.7.5/plugins/emoticons/img/smiley-foot-in-mouth.gif View File

Before After
Width: 18  |  Height: 18  |  Size: 342B

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save