Merge remote-tracking branch 'origin/develop' into shigusegubu

* origin/develop: (269 commits)
  Apply suggestion to docs/administration/CLI_tasks/user.md
  rename mix task: `pleroma.user unsubscribe` -> `pleroma.user deactivate`
  Added the ability to upload background, logo, default user avatar, instance thumbnail, and the NSFW hiding image via AdminFE
  fixed `mix pleroma.instance gen`
  Also add new sidebarRight setting
  Add `background_image` to `InstanceOperation`
  Move notification actions to PleromaAPI.NotificationController
  Move conversation actions to PleromaAPI.ConversationController
  Move reaction actions to EmojiReactionController
  Add OpenAPI spec for PleromaAPI.PleromaAPIController
  Changelog: Add background to instance
  Docs: Add background_image in instance
  InstanceOperation: Add background image to example
  MastoFE: update to bundle-2020-05-20
  Apply suggestion to installation/nginx-cache-purge.sh.example
  update purge script
  Synchronize :fe settings in config.exs
  Fix summary
  User.Query: Remove superfluous `distinct`
  UserTest: Hide warning in tests.
  ...
This commit is contained in:
Henry Jameson 2020-05-22 00:29:15 +03:00
commit f9b13c754c
859 changed files with 17309 additions and 8204 deletions

View file

@ -48,6 +48,7 @@ benchmark:
unit-testing:
stage: test
retry: 2
cache: &testing_cache_policy
<<: *global_cache_policy
policy: pull
@ -80,6 +81,7 @@ unit-testing:
unit-testing-rum:
stage: test
retry: 2
cache: *testing_cache_policy
services:
- name: minibikini/postgres-with-rum:12

View file

@ -15,14 +15,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- **Breaking:** removed `with_move` parameter from notifications timeline.
### Added
- Instance: Add `background_image` to configuration and `/api/v1/instance`
- Instance: Extend `/api/v1/instance` with Pleroma-specific information.
- NodeInfo: `pleroma:api/v1/notifications:include_types_filter` to the `features` list.
- NodeInfo: `pleroma_emoji_reactions` to the `features` list.
- Configuration: `:restrict_unauthenticated` setting, restrict access for unauthenticated users to timelines (public and federate), user profiles and statuses.
- Configuration: Add `:database_config_whitelist` setting to whitelist settings which can be configured from AdminFE.
- New HTTP adapter [gun](https://github.com/ninenines/gun). Gun adapter requires minimum OTP version of 22.2 otherwise Pleroma wont start. For hackney OTP update is not required.
- Mix task to create trusted OAuth App.
- Notifications: Added `follow_request` notification type.
- Added `:reject_deletes` group to SimplePolicy
- MRF (`EmojiStealPolicy`): New MRF Policy which allows to automatically download emojis from remote instances
<details>
<summary>API Changes</summary>
- Mastodon API: Extended `/api/v1/instance`.
@ -30,6 +33,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint.
- Mastodon API: Add support for filtering replies in public and home timelines
- Admin API: endpoints for create/update/delete OAuth Apps.
- Admin API: endpoint for status view.
</details>
### Fixed
@ -37,24 +41,47 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- **Breaking**: SimplePolicy `:reject` and `:accept` allow deletions again
- Fix follower/blocks import when nicknames starts with @
- Filtering of push notifications on activities from blocked domains
- Resolving Peertube accounts with Webfinger
## [Unreleased (patch)]
### Fixed
- Healthcheck reporting the number of memory currently used, rather than allocated in total
- `InsertSkeletonsForDeletedUsers` failing on some instances
## [2.0.3] - 2020-05-02
## [unreleased-patch]
### Security
- Disallow re-registration of previously deleted users, which allowed viewing direct messages addressed to them
- Mastodon API: Fix `POST /api/v1/follow_requests/:id/authorize` allowing to force a follow from a local user even if they didn't request to follow
- CSP: Sandbox uploads
### Fixed
- Logger configuration through AdminFE
- Notifications from blocked domains
- Potential federation issues with Mastodon versions before 3.0.0
- HTTP Basic Authentication permissions issue
- Follow/Block imports not being able to find the user if the nickname started with an `@`
- Instance stats counting internal users
- Inability to run a From Source release without git
- ObjectAgePolicy didn't filter out old messages
- `blob:` urls not being allowed by CSP
### Added
- NodeInfo: ObjectAgePolicy settings to the `federation` list.
- Follow request notifications
<details>
<summary>API Changes</summary>
- Admin API: `GET /api/pleroma/admin/need_reboot`.
</details>
### Upgrade notes
1. Restart Pleroma
2. Run database migrations (inside Pleroma directory):
- OTP: `./bin/pleroma_ctl migrate`
- From Source: `mix ecto.migrate`
## [2.0.2] - 2020-04-08
### Added
- Support for Funkwhale's `Audio` activity
@ -153,6 +180,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: `pleroma.thread_muted` to the Status entity
- Mastodon API: Mark the direct conversation as read for the author when they send a new direct message
- Mastodon API, streaming: Add `pleroma.direct_conversation_id` to the `conversation` stream event payload.
- Mastodon API: Add `pleroma.unread_count` to the Marker entity
- Admin API: Render whole status in grouped reports
- Mastodon API: User timelines will now respect blocks, unless you are getting the user timeline of somebody you blocked (which would be empty otherwise).
- Mastodon API: Favoriting / Repeating a post multiple times will now return the identical response every time. Before, executing that action twice would return an error ("already favorited") on the second try.

View file

@ -123,7 +123,7 @@ defmodule Pleroma.LoadTesting.Activities do
Enum.each(1..activity_count, fn _ ->
random = :rand.uniform()
i = Enum.find_index(intervals, fn {lower, upper} -> lower <= random && upper > random end)
CommonAPI.post(Enum.random(users), %{"status" => "a post with the tag #tag_#{i}"})
CommonAPI.post(Enum.random(users), %{status: "a post with the tag #tag_#{i}"})
end)
end
@ -137,8 +137,8 @@ defmodule Pleroma.LoadTesting.Activities do
{:ok, activity} =
CommonAPI.post(user, %{
"status" => "Start of #{visibility} long thread",
"visibility" => visibility
status: "Start of #{visibility} long thread",
visibility: visibility
})
Agent.update(:benchmark_state, fn state ->
@ -186,7 +186,7 @@ defmodule Pleroma.LoadTesting.Activities do
{:ok, _activity} =
group
|> get_actor(user, friends, non_friends)
|> CommonAPI.post(%{"status" => "Simple status", "visibility" => visibility})
|> CommonAPI.post(%{status: "Simple status", visibility: visibility})
end
defp insert_activity("emoji", visibility, group, user, friends, non_friends, _opts) do
@ -194,8 +194,8 @@ defmodule Pleroma.LoadTesting.Activities do
group
|> get_actor(user, friends, non_friends)
|> CommonAPI.post(%{
"status" => "Simple status with emoji :firefox:",
"visibility" => visibility
status: "Simple status with emoji :firefox:",
visibility: visibility
})
end
@ -213,8 +213,8 @@ defmodule Pleroma.LoadTesting.Activities do
group
|> get_actor(user, friends, non_friends)
|> CommonAPI.post(%{
"status" => Enum.join(user_mentions, ", ") <> " simple status with mentions",
"visibility" => visibility
status: Enum.join(user_mentions, ", ") <> " simple status with mentions",
visibility: visibility
})
end
@ -236,8 +236,8 @@ defmodule Pleroma.LoadTesting.Activities do
group
|> get_actor(user, friends, non_friends)
|> CommonAPI.post(%{
"status" => mentions <> " hell thread status",
"visibility" => visibility
status: mentions <> " hell thread status",
visibility: visibility
})
end
@ -262,9 +262,9 @@ defmodule Pleroma.LoadTesting.Activities do
{:ok, _activity} =
CommonAPI.post(actor, %{
"status" => "Post with attachment",
"visibility" => visibility,
"media_ids" => [object.id]
status: "Post with attachment",
visibility: visibility,
media_ids: [object.id]
})
end
@ -272,7 +272,7 @@ defmodule Pleroma.LoadTesting.Activities do
{:ok, _activity} =
group
|> get_actor(user, friends, non_friends)
|> CommonAPI.post(%{"status" => "Status with #tag", "visibility" => visibility})
|> CommonAPI.post(%{status: "Status with #tag", visibility: visibility})
end
defp insert_activity("like", visibility, group, user, friends, non_friends, opts) do
@ -312,8 +312,7 @@ defmodule Pleroma.LoadTesting.Activities do
actor = get_actor(group, user, friends, non_friends)
tasks = get_reply_tasks(visibility, group)
{:ok, activity} =
CommonAPI.post(user, %{"status" => "Simple status", "visibility" => visibility})
{:ok, activity} = CommonAPI.post(user, %{status: "Simple status", visibility: visibility})
acc = {activity.id, ["@" <> actor.nickname, "reply to status"]}
insert_replies(tasks, visibility, user, friends, non_friends, acc)
@ -336,8 +335,8 @@ defmodule Pleroma.LoadTesting.Activities do
{:ok, activity} =
CommonAPI.post(actor, %{
"status" => Enum.join(data, ", ") <> "simple status",
"visibility" => "direct"
status: Enum.join(data, ", ") <> "simple status",
visibility: "direct"
})
acc = {activity.id, ["@" <> user.nickname | data] ++ ["reply to status"]}
@ -527,9 +526,9 @@ defmodule Pleroma.LoadTesting.Activities do
defp insert_reply(actor, data, activity_id, visibility) do
{:ok, reply} =
CommonAPI.post(actor, %{
"status" => Enum.join(data, ", "),
"visibility" => visibility,
"in_reply_to_status_id" => activity_id
status: Enum.join(data, ", "),
visibility: visibility,
in_reply_to_status_id: activity_id
})
{reply.id, ["@" <> actor.nickname | data]}

View file

@ -387,56 +387,47 @@ defmodule Pleroma.LoadTesting.Fetcher do
favourites = ActivityPub.fetch_favourites(user)
output_relationships =
!!Pleroma.Config.get([:extensions, :output_relationships_in_statuses_by_default])
Benchee.run(
%{
"Rendering home timeline" => fn ->
StatusView.render("index.json", %{
activities: home_activities,
for: user,
as: :activity,
skip_relationships: !output_relationships
as: :activity
})
end,
"Rendering direct timeline" => fn ->
StatusView.render("index.json", %{
activities: direct_activities,
for: user,
as: :activity,
skip_relationships: !output_relationships
as: :activity
})
end,
"Rendering public timeline" => fn ->
StatusView.render("index.json", %{
activities: public_activities,
for: user,
as: :activity,
skip_relationships: !output_relationships
as: :activity
})
end,
"Rendering tag timeline" => fn ->
StatusView.render("index.json", %{
activities: tag_activities,
for: user,
as: :activity,
skip_relationships: !output_relationships
as: :activity
})
end,
"Rendering notifications" => fn ->
Pleroma.Web.MastodonAPI.NotificationView.render("index.json", %{
notifications: notifications,
for: user,
skip_relationships: !output_relationships
for: user
})
end,
"Rendering favourites timeline" => fn ->
StatusView.render("index.json", %{
activities: favourites,
for: user,
as: :activity,
skip_relationships: !output_relationships
as: :activity
})
end
},

View file

@ -55,7 +55,7 @@ defmodule Pleroma.LoadTesting.Users do
name: "Test テスト User #{i}",
email: "user#{i}@example.com",
nickname: "nick#{i}",
password_hash: Comeonin.Pbkdf2.hashpwsalt("test"),
password_hash: Pbkdf2.hash_pwd_salt("test"),
bio: "Tester Number #{i}",
local: !remote
}

View file

@ -183,6 +183,7 @@ config :pleroma, :instance,
email: "pleroma@hjkos.com",
notify_email: "pleroma@hjkos.com",
description: "SigSegV, a pleroma instance",
background_image: "/images/city.jpg",
limit: 5_000,
chat_limit: 5_000,
remote_limit: 100_000,
@ -240,9 +241,18 @@ config :pleroma, :instance,
account_field_value_length: 2048,
external_user_synchronization: true,
extended_nickname_format: true,
cleanup_attachments: false
config :pleroma, :extensions, output_relationships_in_statuses_by_default: true
cleanup_attachments: false,
multi_factor_authentication: [
totp: [
# digits 6 or 8
digits: 6,
period: 30
],
backup_codes: [
number: 5,
length: 16
]
]
config :pleroma, :feed,
post_title: %{
@ -264,22 +274,33 @@ config :pleroma, :markup,
config :pleroma, :frontend_configurations,
pleroma_fe: %{
theme: "sigsegv2",
logo: "/static/logo.svg",
background: "/static/sigsegv_s.png",
redirectRootNoLogin: "/main/all",
redirectRootLogin: "/main/friends",
logoMask: true,
logoMargin: "0.1em",
showInstanceSpecificPanel: true,
scopeOptionsEnabled: false,
formattingOptionsEnabled: false,
collapseMessageWithSubject: false,
hidePostStats: false,
hideUserStats: false,
scopeCopy: false,
subjectLineBehavior: "noop",
alwaysShowSubjectInput: false
background: "/static/sigsegv_s.png",
collapseMessageWithSubject: false,
disableChat: false,
greentext: false,
hideFilteredStatuses: false,
hideMutedPosts: false,
hidePostStats: false,
hideSitename: false,
hideUserStats: false,
loginMethod: "password",
logo: "/static/logo.svg",
logoMargin: ".1em",
logoMask: true,
minimalScopesMode: false,
noAttachmentLinks: false,
nsfwCensorImage: "",
postContentType: "text/plain",
redirectRootLogin: "/main/friends",
redirectRootNoLogin: "/main/all",
scopeCopy: false,
sidebarRight: false,
showFeaturesPanel: true,
showInstanceSpecificPanel: true,
subjectLineBehavior: "noop",
theme: "sigsegv2",
webPushNotifications: false
},
masto_fe: %{
showInstanceSpecificPanel: true
@ -387,6 +408,10 @@ config :pleroma, :rich_media,
config :pleroma, :media_proxy,
enabled: false,
invalidation: [
enabled: false,
provider: Pleroma.Web.MediaProxy.Invalidation.Script
],
proxy_opts: [
redirect_on_failure: false,
max_body_length: 25 * 1_048_576,
@ -673,6 +698,8 @@ config :pleroma, :restrict_unauthenticated,
profiles: %{local: false, remote: false},
activities: %{local: false, remote: false}
config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: false
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"

View file

@ -28,7 +28,8 @@ config :pleroma, :config_description, [
%{
key: :filters,
type: {:list, :module},
description: "List of filter modules for uploads",
description:
"List of filter modules for uploads. Module names are shortened (removed leading `Pleroma.Upload.Filter.` part), but on adding custom module you need to use full name.",
suggestions:
Generator.list_modules_in_dir(
"lib/pleroma/upload/filter",
@ -678,14 +679,6 @@ config :pleroma, :config_description, [
7
]
},
%{
key: :federation_publisher_modules,
type: {:list, :module},
description: "List of modules for federation publishing",
suggestions: [
Pleroma.Web.ActivityPub.Publisher
]
},
%{
key: :allow_relay,
type: :boolean,
@ -694,7 +687,8 @@ config :pleroma, :config_description, [
%{
key: :rewrite_policy,
type: [:module, {:list, :module}],
description: "A list of MRF policies enabled",
description:
"A list of enabled MRF policies. Module names are shortened (removed leading `Pleroma.Web.ActivityPub.MRF.` part), but on adding custom module you need to use full name.",
suggestions:
Generator.list_modules_in_dir(
"lib/pleroma/web/activity_pub/mrf",
@ -712,7 +706,7 @@ config :pleroma, :config_description, [
key: :quarantined_instances,
type: {:list, :string},
description:
"List of ActivityPub instances where private (DMs, followers-only) activities will not be send",
"List of ActivityPub instances where private (DMs, followers-only) activities will not be sent",
suggestions: [
"quarantined.com",
"*.quarantined.com"
@ -919,6 +913,69 @@ config :pleroma, :config_description, [
key: :external_user_synchronization,
type: :boolean,
description: "Enabling following/followers counters synchronization for external users"
},
%{
key: :multi_factor_authentication,
type: :keyword,
description: "Multi-factor authentication settings",
suggestions: [
[
totp: [digits: 6, period: 30],
backup_codes: [number: 5, length: 16]
]
],
children: [
%{
key: :totp,
type: :keyword,
description: "TOTP settings",
suggestions: [digits: 6, period: 30],
children: [
%{
key: :digits,
type: :integer,
suggestions: [6],
description:
"Determines the length of a one-time pass-code, in characters. Defaults to 6 characters."
},
%{
key: :period,
type: :integer,
suggestions: [30],
description:
"a period for which the TOTP code will be valid, in seconds. Defaults to 30 seconds."
}
]
},
%{
key: :backup_codes,
type: :keyword,
description: "MFA backup codes settings",
suggestions: [number: 5, length: 16],
children: [
%{
key: :number,
type: :integer,
suggestions: [5],
description: "number of backup codes to generate."
},
%{
key: :length,
type: :integer,
suggestions: [16],
description:
"Determines the length of backup one-time pass-codes, in characters. Defaults to 16 characters."
}
]
}
]
},
%{
key: :instance_thumbnail,
type: :string,
description:
"The instance thumbnail image. It will appear in [Pleroma Instances](http://distsn.org/pleroma-instances.html)",
suggestions: ["/instance/thumbnail.jpeg"]
}
]
},
@ -1046,32 +1103,98 @@ config :pleroma, :config_description, [
description: "Settings for Pleroma FE",
suggestions: [
%{
theme: "pleroma-dark",
logo: "/static/logo.png",
background: "/images/city.jpg",
redirectRootNoLogin: "/main/all",
redirectRootLogin: "/main/friends",
showInstanceSpecificPanel: true,
scopeOptionsEnabled: false,
formattingOptionsEnabled: false,
collapseMessageWithSubject: false,
hidePostStats: false,
hideUserStats: false,
scopeCopy: true,
subjectLineBehavior: "email",
alwaysShowSubjectInput: true,
logoMask: false,
background: "/static/aurora_borealis.jpg",
collapseMessageWithSubject: false,
disableChat: false,
greentext: false,
hideFilteredStatuses: false,
hideMutedPosts: false,
hidePostStats: false,
hideSitename: false,
hideUserStats: false,
loginMethod: "password",
logo: "/static/logo.png",
logoMargin: ".1em",
stickers: false,
enableEmojiPicker: false
logoMask: true,
minimalScopesMode: false,
noAttachmentLinks: false,
nsfwCensorImage: "/static/img/nsfw.74818f9.png",
postContentType: "text/plain",
redirectRootLogin: "/main/friends",
redirectRootNoLogin: "/main/all",
scopeCopy: true,
sidebarRight: false,
showFeaturesPanel: true,
showInstanceSpecificPanel: false,
subjectLineBehavior: "email",
theme: "pleroma-dark",
webPushNotifications: false
}
],
children: [
%{
key: :theme,
key: :alwaysShowSubjectInput,
label: "Always show subject input",
type: :boolean,
description: "When disabled, auto-hide the subject field if it's empty"
},
%{
key: :background,
type: :string,
description: "Which theme to use, they are defined in styles.json",
suggestions: ["pleroma-dark"]
description:
"URL of the background, unless viewing a user profile with a background that is set",
suggestions: ["/images/city.jpg"]
},
%{
key: :collapseMessageWithSubject,
label: "Collapse message with subject",
type: :boolean,
description:
"When a message has a subject (aka Content Warning), collapse it by default"
},
%{
key: :disableChat,
label: "PleromaFE Chat",
type: :boolean,
description: "Disables PleromaFE Chat component"
},
%{
key: :greentext,
label: "Greentext",
type: :boolean,
description: "Enables green text on lines prefixed with the > character."
},
%{
key: :hideFilteredStatuses,
label: "Hide Filtered Statuses",
type: :boolean,
description: "Hides filtered statuses from timelines."
},
%{
key: :hideMutedPosts,
label: "Hide Muted Posts",
type: :boolean,
description: "Hides muted statuses from timelines."
},
%{
key: :hidePostStats,
label: "Hide post stats",
type: :boolean,
description: "Hide notices statistics (repeats, favorites, ...)"
},
%{
key: :hideSitename,
label: "Hide Sitename",
type: :boolean,
description: "Hides instance name from PleromaFE banner."
},
%{
key: :hideUserStats,
label: "Hide user stats",
type: :boolean,
description:
"Hide profile statistics (posts, posts per day, followers, followings, ...)"
},
%{
key: :logo,
@ -1080,11 +1203,44 @@ config :pleroma, :config_description, [
suggestions: ["/static/logo.png"]
},
%{
key: :background,
key: :logoMargin,
label: "Logo margin",
type: :string,
description:
"URL of the background, unless viewing a user profile with a background that is set",
suggestions: ["/images/city.jpg"]
"Allows you to adjust vertical margins between logo boundary and navbar borders. " <>
"The idea is that to have logo's image without any extra margins and instead adjust them to your need in layout.",
suggestions: [".1em"]
},
%{
key: :logoMask,
label: "Logo mask",
type: :boolean,
description:
"By default it assumes logo used will be monochrome with alpha channel to be compatible with both light and dark themes. " <>
"If you want a colorful logo you must disable logoMask."
},
%{
key: :minimalScopesMode,
label: "Minimal scopes mode",
type: :boolean,
description:
"Limit scope selection to Direct, User default, and Scope of post replying to. " <>
"Also prevents replying to a DM with a public post from PleromaFE."
},
%{
key: :nsfwCensorImage,
label: "NSFW Censor Image",
type: :string,
description:
"URL of the image to use for hiding NSFW media attachments in the timeline.",
suggestions: ["/static/img/nsfw.74818f9.png"]
},
%{
key: :postContentType,
label: "Post Content Type",
type: {:dropdown, :atom},
description: "Default post formatting option.",
suggestions: ["text/plain", "text/html", "text/markdown", "text/bbcode"]
},
%{
key: :redirectRootNoLogin,
@ -1102,51 +1258,31 @@ config :pleroma, :config_description, [
"Relative URL which indicates where to redirect when a user is logged in",
suggestions: ["/main/friends"]
},
%{
key: :showInstanceSpecificPanel,
label: "Show instance specific panel",
type: :boolean,
description: "Whenether to show the instance's specific panel"
},
%{
key: :scopeOptionsEnabled,
label: "Scope options enabled",
type: :boolean,
description: "Enable setting a notice visibility and subject/CW when posting"
},
%{
key: :formattingOptionsEnabled,
label: "Formatting options enabled",
type: :boolean,
description:
"Enable setting a formatting different than plain-text (ie. HTML, Markdown) when posting, relates to `:instance`, `allowed_post_formats`"
},
%{
key: :collapseMessageWithSubject,
label: "Collapse message with subject",
type: :boolean,
description:
"When a message has a subject (aka Content Warning), collapse it by default"
},
%{
key: :hidePostStats,
label: "Hide post stats",
type: :boolean,
description: "Hide notices statistics (repeats, favorites, ...)"
},
%{
key: :hideUserStats,
label: "Hide user stats",
type: :boolean,
description:
"Hide profile statistics (posts, posts per day, followers, followings, ...)"
},
%{
key: :scopeCopy,
label: "Scope copy",
type: :boolean,
description: "Copy the scope (private/unlisted/public) in replies to posts by default"
},
%{
key: :sidebarRight,
label: "Sidebar on Right",
type: :boolean,
description: "Change alignment of sidebar and panels to the right."
},
%{
key: :showFeaturesPanel,
label: "Show instance features panel",
type: :boolean,
description:
"Enables panel displaying functionality of the instance on the About page."
},
%{
key: :showInstanceSpecificPanel,
label: "Show instance specific panel",
type: :boolean,
description: "Whether to show the instance's custom panel"
},
%{
key: :subjectLineBehavior,
label: "Subject line behavior",
@ -1158,38 +1294,10 @@ config :pleroma, :config_description, [
suggestions: ["email", "masto", "noop"]
},
%{
key: :alwaysShowSubjectInput,
label: "Always show subject input",
type: :boolean,
description: "When disabled, auto-hide the subject field if it's empty"
},
%{
key: :logoMask,
label: "Logo mask",
type: :boolean,
description:
"By default it assumes logo used will be monochrome with alpha channel to be compatible with both light and dark themes. " <>
"If you want a colorful logo you must disable logoMask."
},
%{
key: :logoMargin,
label: "Logo margin",
key: :theme,
type: :string,
description:
"Allows you to adjust vertical margins between logo boundary and navbar borders. " <>
"The idea is that to have logo's image without any extra margins and instead adjust them to your need in layout.",
suggestions: [".1em"]
},
%{
key: :stickers,
type: :boolean,
description: "Enables stickers."
},
%{
key: :enableEmojiPicker,
label: "Emoji picker",
type: :boolean,
description: "Enables emoji picker."
description: "Which theme to use. Available themes are defined in styles.json",
suggestions: ["pleroma-dark"]
}
]
},
@ -1245,6 +1353,12 @@ config :pleroma, :config_description, [
suggestions: [
:pleroma_fox_tan
]
},
%{
key: :default_user_avatar,
type: :string,
description: "URL of the default user avatar.",
suggestions: ["/images/avi.png"]
}
]
},
@ -1799,12 +1913,6 @@ config :pleroma, :config_description, [
(see https://github.com/sorentwo/oban/issues/52).
""",
children: [
%{
key: :repo,
type: :module,
description: "Application's Ecto repo",
suggestions: [Pleroma.Repo]
},
%{
key: :verbose,
type: {:dropdown, :atom},
@ -1975,7 +2083,8 @@ config :pleroma, :config_description, [
%{
key: :parsers,
type: {:list, :module},
description: "List of Rich Media parsers.",
description:
"List of Rich Media parsers. Module names are shortened (removed leading `Pleroma.Web.RichMedia.Parsers.` part), but on adding custom module you need to use full name.",
suggestions: [
Pleroma.Web.RichMedia.Parsers.MetaTagsParser,
Pleroma.Web.RichMedia.Parsers.OEmbed,
@ -1987,7 +2096,8 @@ config :pleroma, :config_description, [
key: :ttl_setters,
label: "TTL setters",
type: {:list, :module},
description: "List of rich media TTL setters.",
description:
"List of rich media TTL setters. Module names are shortened (removed leading `Pleroma.Web.RichMedia.Parser.` part), but on adding custom module you need to use full name.",
suggestions: [
Pleroma.Web.RichMedia.Parser.TTL.AwsSignedUrl
]
@ -2247,6 +2357,7 @@ config :pleroma, :config_description, [
children: [
%{
key: :active,
label: "Enabled",
type: :boolean,
description: "Globally enable or disable digest emails"
},
@ -2576,18 +2687,6 @@ config :pleroma, :config_description, [
}
]
},
%{
group: :http_signatures,
type: :group,
description: "HTTP Signatures settings",
children: [
%{
key: :adapter,
type: :module,
suggestions: [Pleroma.Signature]
}
]
},
%{
group: :pleroma,
key: :http,
@ -2660,6 +2759,8 @@ config :pleroma, :config_description, [
%{
key: :scrub_policy,
type: {:list, :module},
description:
"Module names are shortened (removed leading `Pleroma.HTML.` part), but on adding custom module you need to use full name.",
suggestions: [Pleroma.HTML.Transform.MediaProxy, Pleroma.HTML.Scrubber.Default]
}
]
@ -3194,5 +3295,19 @@ config :pleroma, :config_description, [
]
}
]
},
%{
group: :pleroma,
key: Pleroma.Web.ApiSpec.CastAndValidate,
type: :group,
children: [
%{
key: :strict,
type: :boolean,
description:
"Enables strict input validation (useful in development, not recommended in production)",
suggestions: [false]
}
]
}
]

View file

@ -52,6 +52,8 @@ config :pleroma, Pleroma.Repo,
hostname: "localhost",
pool_size: 10
config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true
if File.exists?("./config/dev.secret.exs") do
import_config "dev.secret.exs"
else

View file

@ -56,6 +56,19 @@ config :pleroma, :rich_media,
ignore_hosts: [],
ignore_tld: ["local", "localdomain", "lan"]
config :pleroma, :instance,
multi_factor_authentication: [
totp: [
# digits 6 or 8
digits: 6,
period: 30
],
backup_codes: [
number: 2,
length: 6
]
]
config :web_push_encryption, :vapid_details,
subject: "mailto:administrator@example.com",
public_key:
@ -96,6 +109,8 @@ config :pleroma, Pleroma.Emails.NewUsersDigestEmail, enabled: true
config :pleroma, Pleroma.Plugs.RemoteIp, enabled: false
config :pleroma, Pleroma.Web.ApiSpec.CastAndValidate, strict: true
if File.exists?("./config/test.secret.exs") do
import_config "test.secret.exs"
else

View file

@ -409,6 +409,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
### Get a password reset token for a given nickname
- Params: none
- Response:
@ -427,6 +428,14 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- `nicknames`
- Response: none (code `204`)
## PUT `/api/pleroma/admin/users/disable_mfa`
### Disable mfa for user's account.
- Params:
- `nickname`
- Response: Users nickname
## `GET /api/pleroma/admin/users/:nickname/credentials`
### Get the user's email, password, display and settings-related fields
@ -755,6 +764,17 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- 400 Bad Request `"Invalid parameters"` when `status` is missing
- On success: `204`, empty response
## `GET /api/pleroma/admin/statuses/:id`
### Show status by id
- Params:
- `id`: required, status id
- Response:
- On failure:
- 404 Not Found `"Not Found"`
- On success: JSON, Mastodon Status entity
## `PUT /api/pleroma/admin/statuses/:id`
### Change the scope of an individual reported status

View file

@ -61,6 +61,7 @@ Has these additional fields under the `pleroma` object:
- `deactivated`: boolean, true when the user is deactivated
- `allow_following_move`: boolean, true when the user allows automatically follow moved following accounts
- `unread_conversation_count`: The count of unread conversations. Only returned to the account owner.
- `unread_notifications_count`: The count of unread notifications. Only returned to the account owner.
### Source
@ -215,6 +216,13 @@ Has theses additional parameters (which are the same as in Pleroma-API):
- `avatar_upload_limit`: The same for avatars
- `background_upload_limit`: The same for backgrounds
- `banner_upload_limit`: The same for banners
- `background_image`: A background image that frontends can use
- `pleroma.metadata.features`: A list of supported features
- `pleroma.metadata.federation`: The federation restrictions of this instance
- `vapid_public_key`: The public key needed for push messages
## Markers
Has these additional fields under the `pleroma` object:
- `unread_count`: contains number unread notifications

View file

@ -70,7 +70,49 @@ Request parameters can be passed via [query strings](https://en.wikipedia.org/wi
* Response: JSON. Returns `{"status": "success"}` if the account was successfully disabled, `{"error": "[error message]"}` otherwise
* Example response: `{"error": "Invalid password."}`
## `/api/pleroma/admin/`
## `/api/pleroma/accounts/mfa`
#### Gets current MFA settings
* method: `GET`
* Authentication: required
* OAuth scope: `read:security`
* Response: JSON. Returns `{"enabled": "false", "totp": false }`
## `/api/pleroma/accounts/mfa/setup/totp`
#### Pre-setup the MFA/TOTP method
* method: `GET`
* Authentication: required
* OAuth scope: `write:security`
* Response: JSON. Returns `{"key": [secret_key], "provisioning_uri": "[qr code uri]" }` when successful, otherwise returns HTTP 422 `{"error": "error_msg"}`
## `/api/pleroma/accounts/mfa/confirm/totp`
#### Confirms & enables MFA/TOTP support for user account.
* method: `POST`
* Authentication: required
* OAuth scope: `write:security`
* Params:
* `password`: user's password
* `code`: token from TOTP App
* Response: JSON. Returns `{}` if the enable was successful, HTTP 422 `{"error": "[error message]"}` otherwise
## `/api/pleroma/accounts/mfa/totp`
#### Disables MFA/TOTP method for user account.
* method: `DELETE`
* Authentication: required
* OAuth scope: `write:security`
* Params:
* `password`: user's password
* Response: JSON. Returns `{}` if the disable was successful, HTTP 422 `{"error": "[error message]"}` otherwise
* Example response: `{"error": "Invalid password."}`
## `/api/pleroma/accounts/mfa/backup_codes`
#### Generstes backup codes MFA for user account.
* method: `GET`
* Authentication: required
* OAuth scope: `write:security`
* Response: JSON. Returns `{"codes": codes}`when successful, otherwise HTTP 422 `{"error": "[error message]"}`
## `/api/pleroma/admin/`
See [Admin-API](admin_api.md)
## `/api/v1/pleroma/notifications/read`
@ -223,7 +265,7 @@ See [Admin-API](admin_api.md)
* Method `PUT`
* Authentication: required
* Params:
* `image`: Multipart image
* `file`: Multipart image
* Response: JSON. Returns a mastodon media attachment entity
when successful, otherwise returns HTTP 415 `{"error": "error_msg"}`
* Example response:
@ -316,7 +358,7 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa
* `recipients`: A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though.
* Response: JSON, statuses (200 - healthy, 503 unhealthy)
## `GET /api/v1/pleroma/conversations/read`
## `POST /api/v1/pleroma/conversations/read`
### Marks all user's conversations as read.
* Method `POST`
* Authentication: required
@ -384,7 +426,7 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa
* Authentication: required
* Params:
* `file`: file needs to be uploaded with the multipart request or link to remote file.
* `shortcode`: (*optional*) shortcode for new emoji, must be uniq for all emoji. If not sended, shortcode will be taken from original filename.
* `shortcode`: (*optional*) shortcode for new emoji, must be unique for all emoji. If not sended, shortcode will be taken from original filename.
* `filename`: (*optional*) new emoji file name. If not specified will be taken from original filename.
* Response: JSON, list of files for updated pack (hashmap -> shortcode => filename) with status 200, either error status with error message.
@ -494,7 +536,7 @@ Emoji reactions work a lot like favourites do. They make it possible to react to
```
## `GET /api/v1/pleroma/statuses/:id/reactions/:emoji`
### Get an object of emoji to account mappings with accounts that reacted to the post for a specific emoji`
### Get an object of emoji to account mappings with accounts that reacted to the post for a specific emoji
* Method: `GET`
* Authentication: optional
* Params: None

View file

@ -105,23 +105,23 @@ mix pleroma.user toggle_activated <nickname>
```
## Unsubscribe local users from a user and deactivate the user
## Deactivate a user and unsubscribes local users from the user
```sh tab="OTP"
./bin/pleroma_ctl user unsubscribe NICKNAME
./bin/pleroma_ctl user deactivate NICKNAME
```
```sh tab="From Source"
mix pleroma.user unsubscribe NICKNAME
mix pleroma.user deactivate NICKNAME
```
## Unsubscribe local users from an instance and deactivate all accounts on it
## Deactivate all accounts from an instance and unsubscribe local users on it
```sh tab="OTP"
./bin/pleroma_ctl user unsubscribe_all_from_instance <instance>
./bin/pleroma_ctl user deactivate_all_from_instance <instance>
```
```sh tab="From Source"
mix pleroma.user unsubscribe_all_from_instance <instance>
mix pleroma.user deactivate_all_from_instance <instance>
```
@ -177,4 +177,3 @@ mix pleroma.user untag <nickname> <tags>
```sh tab="From Source"
mix pleroma.user toggle_confirmed <nickname>
```

View file

@ -49,11 +49,11 @@ Feel free to contact us to be added to this list!
- Platforms: Android
- Features: Streaming Ready
### Roma
- Homepage: <https://www.pleroma.com/#mobileApps>
- Source Code: [iOS](https://github.com/roma-apps/roma-ios), [Android](https://github.com/roma-apps/roma-android)
### Fedi
- Homepage: <https://www.fediapp.com/>
- Source Code: Proprietary, but free
- Platforms: iOS, Android
- Features: No Streaming
- Features: Pleroma-specific features like Reactions
### Tusky
- Homepage: <https://tuskyapp.github.io/>

View file

@ -8,6 +8,10 @@ For from source installations Pleroma configuration works by first importing the
To add configuration to your config file, you can copy it from the base config. The latest version of it can be viewed [here](https://git.pleroma.social/pleroma/pleroma/blob/develop/config/config.exs). You can also use this file if you don't know how an option is supposed to be formatted.
## :chat
* `enabled` - Enables the backend chat. Defaults to `true`.
## :instance
* `name`: The instances name.
* `email`: Email used to reach an Administrator/Moderator of the instance.
@ -145,6 +149,11 @@ config :pleroma, :mrf_user_allowlist,
* `:strip_followers` removes followers from the ActivityPub recipient list, ensuring they won't be delivered to home timelines
* `:reject` rejects the message entirely
#### mrf_steal_emoji
* `hosts`: List of hosts to steal emojis from
* `rejected_shortcodes`: Regex-list of shortcodes to reject
* `size_limit`: File size limit (in bytes), checked before an emoji is saved to the disk
### :activitypub
* `unfollow_blocked`: Whether blocks result in people getting unfollowed
* `outgoing_blocks`: Whether to federate blocks to other instances
@ -245,6 +254,40 @@ This section describe PWA manifest instance-specific values. Currently this opti
* `base_url`: The base URL to access a user-uploaded file. Useful when you want to proxy the media files via another host/CDN fronts.
* `proxy_opts`: All options defined in `Pleroma.ReverseProxy` documentation, defaults to `[max_body_length: (25*1_048_576)]`.
* `whitelist`: List of domains to bypass the mediaproxy
* `invalidation`: options for remove media from cache after delete object:
* `enabled`: Enables purge cache
* `provider`: Which one of the [purge cache strategy](#purge-cache-strategy) to use.
### Purge cache strategy
#### Pleroma.Web.MediaProxy.Invalidation.Script
This strategy allow perform external bash script to purge cache.
Urls of attachments pass to script as arguments.
* `script_path`: path to external script.
Example:
```elixir
config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Script,
script_path: "./installation/nginx-cache-purge.example"
```
#### Pleroma.Web.MediaProxy.Invalidation.Http
This strategy allow perform custom http request to purge cache.
* `method`: http method. default is `purge`
* `headers`: http headers. default is empty
* `options`: request options. default is empty
Example:
```elixir
config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Http,
method: :purge,
headers: [],
options: []
```
## Link previews
@ -615,24 +658,6 @@ config :pleroma, :workers,
* `enabled: false` corresponds to `config :pleroma, :workers, retries: [federator_outgoing: 1]`
* deprecated options: `max_jobs`, `initial_timeout`
### Pleroma.Scheduler
Configuration for [Quantum](https://github.com/quantum-elixir/quantum-core) jobs scheduler.
See [Quantum readme](https://github.com/quantum-elixir/quantum-core#usage) for the list of supported options.
Example:
```elixir
config :pleroma, Pleroma.Scheduler,
global: true,
overlap: true,
timezone: :utc,
jobs: [{"0 */6 * * * *", {Pleroma.Web.Websub, :refresh_subscriptions, []}}]
```
The above example defines a single job which invokes `Pleroma.Web.Websub.refresh_subscriptions()` every 6 hours ("0 */6 * * * *", [crontab format](https://en.wikipedia.org/wiki/Cron)).
## :web_push_encryption, :vapid_details
Web Push Notifications configuration. You can use the mix task `mix web_push.gen.keypair` to generate it.
@ -903,12 +928,33 @@ config :auto_linker,
* `runtime_dir`: A path to custom Elixir modules (such as MRF policies).
## :configurable_from_database
Boolean, enables/disables in-database configuration. Read [Transfering the config to/from the database](../administration/CLI_tasks/config.md) for more information.
## :database_config_whitelist
List of valid configuration sections which are allowed to be configured from the
database. Settings stored in the database before the whitelist is configured are
still applied, so it is suggested to only use the whitelist on instances that
have not migrated the config to the database.
Example:
```elixir
config :pleroma, :database_config_whitelist, [
{:pleroma, :instance},
{:pleroma, Pleroma.Web.Metadata},
{:auto_linker}
]
```
### Multi-factor authentication - :two_factor_authentication
* `totp` - a list containing TOTP configuration
- `digits` - Determines the length of a one-time pass-code in characters. Defaults to 6 characters.
- `period` - a period for which the TOTP code will be valid in seconds. Defaults to 30 seconds.
* `backup_codes` - a list containing backup codes configuration
- `number` - number of backup codes to generate.
- `length` - backup code length. Defaults to 16 characters.
## Restrict entities access for unauthenticated users
@ -925,3 +971,8 @@ Restrict access for unauthenticated users to timelines (public and federate), us
* `activities` - statuses
* `local`
* `remote`
## Pleroma.Web.ApiSpec.CastAndValidate
* `:strict` a boolean, enables strict input validation (useful in development, not recommended in production). Defaults to `false`.

View file

@ -0,0 +1,38 @@
# Storing Remote Media
Pleroma does not store remote/federated media by default. The best way to achieve this is to change Nginx to keep its reverse proxy cache
for a year and to activate the `MediaProxyWarmingPolicy` MRF policy in Pleroma which will automatically fetch all media through the proxy
as soon as the post is received by your instance.
## Nginx
```
proxy_cache_path /long/term/storage/path/pleroma-media-cache levels=1:2
keys_zone=pleroma_media_cache:10m inactive=1y use_temp_path=off;
location ~ ^/(media|proxy) {
proxy_cache pleroma_media_cache;
slice 1m;
proxy_cache_key $host$uri$is_args$args$slice_range;
proxy_set_header Range $slice_range;
proxy_http_version 1.1;
proxy_cache_valid 206 301 302 304 1h;
proxy_cache_valid 200 1y;
proxy_cache_use_stale error timeout invalid_header updating;
proxy_ignore_client_abort on;
proxy_buffering on;
chunked_transfer_encoding on;
proxy_ignore_headers Cache-Control Expires;
proxy_hide_header Cache-Control Expires;
proxy_pass http://127.0.0.1:4000;
}
```
## Pleroma
Add to your `prod.secret.exs`:
```
config :pleroma, :instance,
rewrite_policy: [Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy]
```

View file

@ -1,21 +1,45 @@
#!/sbin/openrc-run
# Requires OpenRC >= 0.35
directory=/opt/pleroma
command=/usr/bin/mix
command_args="phx.server"
supervisor=supervise-daemon
command_user=pleroma:pleroma
command_background=1
export PORT=4000
export MIX_ENV=prod
# Ask process to terminate within 30 seconds, otherwise kill it
retry="SIGTERM/30/SIGKILL/5"
pidfile="/var/run/pleroma.pid"
directory=/opt/pleroma
healthcheck_delay=60
healthcheck_timer=30
: ${pleroma_port:-4000}
# Needs OpenRC >= 0.42
#respawn_max=0
#respawn_delay=5
# put pleroma_console=YES in /etc/conf.d/pleroma if you want to be able to
# connect to pleroma via an elixir console
if yesno "${pleroma_console}"; then
command=elixir
command_args="--name pleroma@127.0.0.1 --erl '-kernel inet_dist_listen_min 9001 inet_dist_listen_max 9001 inet_dist_use_interface {127,0,0,1}' -S mix phx.server"
start_post() {
einfo "You can get a console by using this command as pleroma's user:"
einfo "iex --name console@127.0.0.1 --remsh pleroma@127.0.0.1"
}
else
command=/usr/bin/mix
command_args="phx.server"
fi
export MIX_ENV=prod
depend() {
need nginx postgresql
}
healthcheck() {
# put pleroma_health=YES in /etc/conf.d/pleroma if you want healthchecking
# and make sure you have curl installed
yesno "$pleroma_health" || return 0
curl -q "localhost:${pleroma_port}/api/pleroma/healthcheck"
}

View file

@ -0,0 +1,40 @@
#!/bin/sh
# A simple shell script to delete a media from the Nginx cache.
SCRIPTNAME=${0##*/}
# NGINX cache directory
CACHE_DIRECTORY="/tmp/pleroma-media-cache"
## Return the files where the items are cached.
## $1 - the filename, can be a pattern .
## $2 - the cache directory.
## $3 - (optional) the number of parallel processes to run for grep.
get_cache_files() {
local max_parallel=${3-16}
find $2 -maxdepth 2 -type d | xargs -P $max_parallel -n 1 grep -E Rl "^KEY:.*$1" | sort -u
}
## Removes an item from the given cache zone.
## $1 - the filename, can be a pattern .
## $2 - the cache directory.
purge_item() {
for f in $(get_cache_files $1 $2); do
echo "found file: $f"
[ -f $f ] || continue
echo "Deleting $f from $2."
rm $f
done
} # purge_item
purge() {
for url in "$@"
do
echo "$SCRIPTNAME delete \`$url\` from cache ($CACHE_DIRECTORY)"
purge_item $url $CACHE_DIRECTORY
done
}
purge $1

View file

@ -32,9 +32,8 @@ CustomLog ${APACHE_LOG_DIR}/access.log combined
<VirtualHost *:443>
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/${servername}/cert.pem
SSLCertificateFile /etc/letsencrypt/live/${servername}/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/${servername}/privkey.pem
SSLCertificateChainFile /etc/letsencrypt/live/${servername}/fullchain.pem
# Mozilla modern configuration, tweak to your needs
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1

View file

@ -67,8 +67,7 @@ defmodule Mix.Tasks.Pleroma.Benchmark do
Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
activities: activities,
for: user,
as: :activity,
skip_relationships: true
as: :activity
})
end
},

View file

@ -1,5 +1,6 @@
defmodule Mix.Tasks.Pleroma.Digest do
use Mix.Task
import Mix.Pleroma
@shortdoc "Manages digest emails"
@moduledoc File.read!("docs/administration/CLI_tasks/digest.md")
@ -22,12 +23,10 @@ defmodule Mix.Tasks.Pleroma.Digest do
with %Swoosh.Email{} = email <- Pleroma.Emails.UserEmail.digest_email(patched_user) do
{:ok, _} = Pleroma.Emails.Mailer.deliver(email)
Mix.shell().info("Digest email have been sent to #{nickname} (#{user.email})")
shell_info("Digest email have been sent to #{nickname} (#{user.email})")
else
_ ->
Mix.shell().info(
"Cound't find any mentions for #{nickname} since #{last_digest_emailed_at}"
)
shell_info("Cound't find any mentions for #{nickname} since #{last_digest_emailed_at}")
end
end
end

View file

@ -147,6 +147,7 @@ defmodule Mix.Tasks.Pleroma.Instance do
"What directory should media uploads go in (when using the local uploader)?",
Pleroma.Config.get([Pleroma.Uploaders.Local, :uploads])
)
|> Path.expand()
static_dir =
get_option(
@ -155,6 +156,7 @@ defmodule Mix.Tasks.Pleroma.Instance do
"What directory should custom public files be read from (custom emojis, frontend bundle overrides, robots.txt, etc.)?",
Pleroma.Config.get([:instance, :static_dir])
)
|> Path.expand()
Config.put([:instance, :static_dir], static_dir)
@ -204,7 +206,7 @@ defmodule Mix.Tasks.Pleroma.Instance do
shell_info("Writing the postgres script to #{psql_path}.")
File.write(psql_path, result_psql)
write_robots_txt(indexable, template_dir)
write_robots_txt(static_dir, indexable, template_dir)
shell_info(
"\n All files successfully written! Refer to the installation instructions for your platform for next steps."
@ -224,15 +226,13 @@ defmodule Mix.Tasks.Pleroma.Instance do
end
end
defp write_robots_txt(indexable, template_dir) do
defp write_robots_txt(static_dir, indexable, template_dir) do
robots_txt =
EEx.eval_file(
template_dir <> "/robots_txt.eex",
indexable: indexable
)
static_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static/")
unless File.exists?(static_dir) do
File.mkdir_p!(static_dir)
end

View file

@ -8,6 +8,8 @@ defmodule Mix.Tasks.Pleroma.User do
alias Ecto.Changeset
alias Pleroma.User
alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
@shortdoc "Manages Pleroma users"
@moduledoc File.read!("docs/administration/CLI_tasks/user.md")
@ -96,8 +98,9 @@ defmodule Mix.Tasks.Pleroma.User do
def run(["rm", nickname]) do
start_pleroma()
with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do
User.perform(:delete, user)
with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
{:ok, delete_data, _} <- Builder.delete(user, user.ap_id),
{:ok, _delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
shell_info("User #{nickname} deleted.")
else
_ -> shell_error("No local user #{nickname}")
@ -141,28 +144,18 @@ defmodule Mix.Tasks.Pleroma.User do
end
end
def run(["unsubscribe", nickname]) do
def run(["deactivate", nickname]) do
start_pleroma()
with %User{} = user <- User.get_cached_by_nickname(nickname) do
shell_info("Deactivating #{user.nickname}")
User.deactivate(user)
user
|> User.get_friends()
|> Enum.each(fn friend ->
user = User.get_cached_by_id(user.id)
shell_info("Unsubscribing #{friend.nickname} from #{user.nickname}")
User.unfollow(user, friend)
end)
:timer.sleep(500)
user = User.get_cached_by_id(user.id)
if Enum.empty?(User.get_friends(user)) do
shell_info("Successfully unsubscribed all followers from #{user.nickname}")
if Enum.empty?(Enum.filter(User.get_friends(user), & &1.local)) do
shell_info("Successfully unsubscribed all local followers from #{user.nickname}")
end
else
_ ->
@ -170,7 +163,7 @@ defmodule Mix.Tasks.Pleroma.User do
end
end
def run(["unsubscribe_all_from_instance", instance]) do
def run(["deactivate_all_from_instance", instance]) do
start_pleroma()
Pleroma.User.Query.build(%{nickname: "@#{instance}"})
@ -178,7 +171,7 @@ defmodule Mix.Tasks.Pleroma.User do
|> Stream.each(fn users ->
users
|> Enum.each(fn user ->
run(["unsubscribe", user.nickname])
run(["deactivate", user.nickname])
end)
end)
|> Stream.run()

View file

@ -56,7 +56,7 @@ defmodule Pleroma.Application do
if (major == 22 and minor < 2) or major < 22 do
raise "
!!!OTP VERSION WARNING!!!
You are using gun adapter with OTP version #{version}, which doesn't support correct handling of unordered certificates chains.
You are using gun adapter with OTP version #{version}, which doesn't support correct handling of unordered certificates chains. Please update your Erlang/OTP to at least 22.2.
"
end
else
@ -173,7 +173,14 @@ defmodule Pleroma.Application do
defp streamer_child(env) when env in [:test, :benchmark], do: []
defp streamer_child(_) do
[Pleroma.Web.Streamer.supervisor()]
[
{Registry,
[
name: Pleroma.Web.Streamer.registry(),
keys: :duplicate,
partitions: System.schedulers_online()
]}
]
end
defp chat_child(_env, true) do

View file

@ -4,7 +4,7 @@
defmodule Pleroma.BBS.Authenticator do
use Sshd.PasswordAuthenticator
alias Comeonin.Pbkdf2
alias Pleroma.Plugs.AuthenticationPlug
alias Pleroma.User
def authenticate(username, password) do
@ -12,7 +12,7 @@ defmodule Pleroma.BBS.Authenticator do
password = to_string(password)
with %User{} = user <- User.get_by_nickname(username) do
Pbkdf2.checkpw(password, user.password_hash)
AuthenticationPlug.checkpw(password, user.password_hash)
else
_e -> false
end

View file

@ -66,7 +66,7 @@ defmodule Pleroma.BBS.Handler do
with %Activity{} <- Activity.get_by_id(activity_id),
{:ok, _activity} <-
CommonAPI.post(user, %{"status" => rest, "in_reply_to_status_id" => activity_id}) do
CommonAPI.post(user, %{status: rest, in_reply_to_status_id: activity_id}) do
IO.puts("Replied!")
else
_e -> IO.puts("Could not reply...")
@ -78,7 +78,7 @@ defmodule Pleroma.BBS.Handler do
def handle_command(%{user: user} = state, "p " <> text) do
text = String.trim(text)
with {:ok, _activity} <- CommonAPI.post(user, %{"status" => text}) do
with {:ok, _activity} <- CommonAPI.post(user, %{status: text}) do
IO.puts("Posted!")
else
_e -> IO.puts("Could not post...")

View file

@ -278,6 +278,8 @@ defmodule Pleroma.ConfigDB do
}
end
defp do_convert({:partial_chain, entity}), do: %{"tuple" => [":partial_chain", inspect(entity)]}
defp do_convert(entity) when is_tuple(entity) do
value =
entity
@ -321,6 +323,15 @@ defmodule Pleroma.ConfigDB do
{:proxy_url, {do_transform_string(type), parse_host(host), port}}
end
defp do_transform(%{"tuple" => [":partial_chain", entity]}) do
{partial_chain, []} =
entity
|> String.replace(~r/[^\w|^{:,[|^,|^[|^\]^}|^\/|^\.|^"]^\s/, "")
|> Code.eval_string()
{:partial_chain, partial_chain}
end
defp do_transform(%{"tuple" => entity}) do
Enum.reduce(entity, {}, fn val, acc -> Tuple.append(acc, do_transform(val)) end)
end

View file

@ -17,7 +17,8 @@ defmodule Pleroma.Constants do
"announcement_count",
"emoji",
"context_id",
"deleted_activity_id"
"deleted_activity_id",
"pleroma_internal"
]
)

View file

@ -128,7 +128,7 @@ defmodule Pleroma.Conversation.Participation do
|> Pleroma.Pagination.fetch_paginated(params)
end
def restrict_recipients(query, user, %{"recipients" => user_ids}) do
def restrict_recipients(query, user, %{recipients: user_ids}) do
user_binary_ids =
[user.id | user_ids]
|> Enum.uniq()
@ -172,7 +172,7 @@ defmodule Pleroma.Conversation.Participation do
| last_activity_id: activity_id
}
end)
|> Enum.filter(& &1.last_activity_id)
|> Enum.reject(&is_nil(&1.last_activity_id))
end
def get(_, _ \\ [])

View file

@ -18,7 +18,6 @@ defmodule Pleroma.Docs.JSON do
with config <- Pleroma.Config.Loader.read("config/description.exs") do
config[:pleroma][:config_description]
|> Pleroma.Docs.Generator.convert_to_strings()
|> Jason.encode!()
end
end
end

View file

@ -14,8 +14,10 @@ defmodule Pleroma.Emails.NewUsersDigestEmail do
styling = Pleroma.Config.get([Pleroma.Emails.UserEmail, :styling])
logo_url =
Pleroma.Web.Endpoint.url() <>
Pleroma.Config.get([:frontend_configurations, :pleroma_fe, :logo])
Pleroma.Helpers.UriHelper.maybe_add_base(
Pleroma.Config.get([:frontend_configurations, :pleroma_fe, :logo]),
Pleroma.Web.Endpoint.url()
)
new()
|> to({to.name, to.email})

View file

@ -16,162 +16,78 @@ defmodule Pleroma.Emoji.Pack do
alias Pleroma.Emoji
@spec emoji_path() :: Path.t()
def emoji_path do
static = Pleroma.Config.get!([:instance, :static_dir])
Path.join(static, "emoji")
end
@spec create(String.t()) :: :ok | {:error, File.posix()} | {:error, :empty_values}
def create(name) when byte_size(name) > 0 do
dir = Path.join(emoji_path(), name)
with :ok <- File.mkdir(dir) do
%__MODULE__{
pack_file: Path.join(dir, "pack.json")
}
def create(name) do
with :ok <- validate_not_empty([name]),
dir <- Path.join(emoji_path(), name),
:ok <- File.mkdir(dir) do
%__MODULE__{pack_file: Path.join(dir, "pack.json")}
|> save_pack()
end
end
def create(_), do: {:error, :empty_values}
@spec show(String.t()) :: {:ok, t()} | {:loaded, nil} | {:error, :empty_values}
def show(name) when byte_size(name) > 0 do
with {_, %__MODULE__{} = pack} <- {:loaded, load_pack(name)},
{_, pack} <- validate_pack(pack) do
{:ok, pack}
@spec show(String.t()) :: {:ok, t()} | {:error, atom()}
def show(name) do
with :ok <- validate_not_empty([name]),
{:ok, pack} <- load_pack(name) do
{:ok, validate_pack(pack)}
end
end
def show(_), do: {:error, :empty_values}
@spec delete(String.t()) ::
{:ok, [binary()]} | {:error, File.posix(), binary()} | {:error, :empty_values}
def delete(name) when byte_size(name) > 0 do
def delete(name) do
with :ok <- validate_not_empty([name]) do
emoji_path()
|> Path.join(name)
|> File.rm_rf()
end
def delete(_), do: {:error, :empty_values}
end
@spec add_file(String.t(), String.t(), Path.t(), Plug.Upload.t() | String.t()) ::
{:ok, t()} | {:error, File.posix()} | {:error, :empty_values}
def add_file(name, shortcode, filename, file)
when byte_size(name) > 0 and byte_size(shortcode) > 0 and byte_size(filename) > 0 do
with {_, nil} <- {:exists, Emoji.get(shortcode)},
{_, %__MODULE__{} = pack} <- {:loaded, load_pack(name)} do
file_path = Path.join(pack.path, filename)
create_subdirs(file_path)
case file do
%Plug.Upload{path: upload_path} ->
# Copy the uploaded file from the temporary directory
File.copy!(upload_path, file_path)
url when is_binary(url) ->
# Download and write the file
file_contents = Tesla.get!(url).body
File.write!(file_path, file_contents)
end
files = Map.put(pack.files, shortcode, filename)
updated_pack = %{pack | files: files}
case save_pack(updated_pack) do
:ok ->
{:ok, t()} | {:error, File.posix() | atom()}
def add_file(name, shortcode, filename, file) do
with :ok <- validate_not_empty([name, shortcode, filename]),
:ok <- validate_emoji_not_exists(shortcode),
{:ok, pack} <- load_pack(name),
:ok <- save_file(file, pack, filename),
{:ok, updated_pack} <- pack |> put_emoji(shortcode, filename) |> save_pack() do
Emoji.reload()
{:ok, updated_pack}
e ->
e
end
end
end
def add_file(_, _, _, _), do: {:error, :empty_values}
defp create_subdirs(file_path) do
if String.contains?(file_path, "/") do
file_path
|> Path.dirname()
|> File.mkdir_p!()
end
end
@spec delete_file(String.t(), String.t()) ::
{:ok, t()} | {:error, File.posix()} | {:error, :empty_values}
def delete_file(name, shortcode) when byte_size(name) > 0 and byte_size(shortcode) > 0 do
with {_, %__MODULE__{} = pack} <- {:loaded, load_pack(name)},
{_, {filename, files}} when not is_nil(filename) <-
{:exists, Map.pop(pack.files, shortcode)},
emoji <- Path.join(pack.path, filename),
{_, true} <- {:exists, File.exists?(emoji)} do
emoji_dir = Path.dirname(emoji)
File.rm!(emoji)
if String.contains?(filename, "/") and File.ls!(emoji_dir) == [] do
File.rmdir!(emoji_dir)
end
updated_pack = %{pack | files: files}
case save_pack(updated_pack) do
:ok ->
{:ok, t()} | {:error, File.posix() | atom()}
def delete_file(name, shortcode) do
with :ok <- validate_not_empty([name, shortcode]),
{:ok, pack} <- load_pack(name),
:ok <- remove_file(pack, shortcode),
{:ok, updated_pack} <- pack |> delete_emoji(shortcode) |> save_pack() do
Emoji.reload()
{:ok, updated_pack}
e ->
e
end
end
end
def delete_file(_, _), do: {:error, :empty_values}
@spec update_file(String.t(), String.t(), String.t(), String.t(), boolean()) ::
{:ok, t()} | {:error, File.posix()} | {:error, :empty_values}
def update_file(name, shortcode, new_shortcode, new_filename, force)
when byte_size(name) > 0 and byte_size(shortcode) > 0 and byte_size(new_shortcode) > 0 and
byte_size(new_filename) > 0 do
with {_, %__MODULE__{} = pack} <- {:loaded, load_pack(name)},
{_, {filename, files}} when not is_nil(filename) <-
{:exists, Map.pop(pack.files, shortcode)},
{_, true} <- {:not_used, force or is_nil(Emoji.get(new_shortcode))} do
old_path = Path.join(pack.path, filename)
old_dir = Path.dirname(old_path)
new_path = Path.join(pack.path, new_filename)
create_subdirs(new_path)
:ok = File.rename(old_path, new_path)
if String.contains?(filename, "/") and File.ls!(old_dir) == [] do
File.rmdir!(old_dir)
end
files = Map.put(files, new_shortcode, new_filename)
updated_pack = %{pack | files: files}
case save_pack(updated_pack) do
:ok ->
{:ok, t()} | {:error, File.posix() | atom()}
def update_file(name, shortcode, new_shortcode, new_filename, force) do
with :ok <- validate_not_empty([name, shortcode, new_shortcode, new_filename]),
{:ok, pack} <- load_pack(name),
{:ok, filename} <- get_filename(pack, shortcode),
:ok <- validate_emoji_not_exists(new_shortcode, force),
:ok <- rename_file(pack, filename, new_filename),
{:ok, updated_pack} <-
pack
|> delete_emoji(shortcode)
|> put_emoji(new_shortcode, new_filename)
|> save_pack() do
Emoji.reload()
{:ok, updated_pack}
e ->
e
end
end
end
def update_file(_, _, _, _, _), do: {:error, :empty_values}
@spec import_from_filesystem() :: {:ok, [String.t()]} | {:error, atom()}
@spec import_from_filesystem() :: {:ok, [String.t()]} | {:error, File.posix() | atom()}
def import_from_filesystem do
emoji_path = emoji_path()
@ -184,7 +100,7 @@ defmodule Pleroma.Emoji.Pack do
File.dir?(path) and File.exists?(Path.join(path, "pack.json"))
end)
|> Enum.map(&write_pack_contents/1)
|> Enum.filter(& &1)
|> Enum.reject(&is_nil/1)
{:ok, names}
else
@ -193,6 +109,117 @@ defmodule Pleroma.Emoji.Pack do
end
end
@spec list_remote(String.t()) :: {:ok, map()} | {:error, atom()}
def list_remote(url) do
uri = url |> String.trim() |> URI.parse()
with :ok <- validate_shareable_packs_available(uri) do
uri
|> URI.merge("/api/pleroma/emoji/packs")
|> http_get()
end
end
@spec list_local() :: {:ok, map()}
def list_local do
with {:ok, results} <- list_packs_dir() do
packs =
results
|> Enum.map(fn name ->
case load_pack(name) do
{:ok, pack} -> pack
_ -> nil
end
end)
|> Enum.reject(&is_nil/1)
|> Map.new(fn pack -> {pack.name, validate_pack(pack)} end)
{:ok, packs}
end
end
@spec get_archive(String.t()) :: {:ok, binary()} | {:error, atom()}
def get_archive(name) do
with {:ok, pack} <- load_pack(name),
:ok <- validate_downloadable(pack) do
{:ok, fetch_archive(pack)}
end
end
@spec download(String.t(), String.t(), String.t()) :: :ok | {:error, atom()}
def download(name, url, as) do
uri = url |> String.trim() |> URI.parse()
with :ok <- validate_shareable_packs_available(uri),
{:ok, remote_pack} <- uri |> URI.merge("/api/pleroma/emoji/packs/#{name}") |> http_get(),
{:ok, %{sha: sha, url: url} = pack_info} <- fetch_pack_info(remote_pack, uri, name),
{:ok, archive} <- download_archive(url, sha),
pack <- copy_as(remote_pack, as || name),
{:ok, _} = unzip(archive, pack_info, remote_pack, pack) do
# Fallback can't contain a pack.json file, since that would cause the fallback-src-sha256
# in it to depend on itself
if pack_info[:fallback] do
save_pack(pack)
else
{:ok, pack}
end
end
end
@spec save_metadata(map(), t()) :: {:ok, t()} | {:error, File.posix()}
def save_metadata(metadata, %__MODULE__{} = pack) do
pack
|> Map.put(:pack, metadata)
|> save_pack()
end
@spec update_metadata(String.t(), map()) :: {:ok, t()} | {:error, File.posix()}
def update_metadata(name, data) do
with {:ok, pack} <- load_pack(name) do
if fallback_sha_changed?(pack, data) do
update_sha_and_save_metadata(pack, data)
else
save_metadata(data, pack)
end
end
end
@spec load_pack(String.t()) :: {:ok, t()} | {:error, :not_found}
def load_pack(name) do
pack_file = Path.join([emoji_path(), name, "pack.json"])
if File.exists?(pack_file) do
pack =
pack_file
|> File.read!()
|> from_json()
|> Map.put(:pack_file, pack_file)
|> Map.put(:path, Path.dirname(pack_file))
|> Map.put(:name, name)
{:ok, pack}
else
{:error, :not_found}
end
end
@spec emoji_path() :: Path.t()
defp emoji_path do
[:instance, :static_dir]
|> Pleroma.Config.get!()
|> Path.join("emoji")
end
defp validate_emoji_not_exists(shortcode, force \\ false)
defp validate_emoji_not_exists(_shortcode, true), do: :ok
defp validate_emoji_not_exists(shortcode, _) do
case Emoji.get(shortcode) do
nil -> :ok
_ -> {:error, :already_exists}
end
end
defp write_pack_contents(path) do
pack = %__MODULE__{
files: files_from_path(path),
@ -201,7 +228,7 @@ defmodule Pleroma.Emoji.Pack do
}
case save_pack(pack) do
:ok -> Path.basename(path)
{:ok, _pack} -> Path.basename(path)
_ -> nil
end
end
@ -216,7 +243,8 @@ defmodule Pleroma.Emoji.Pack do
# FIXME: Copy-pasted from Pleroma.Emoji/load_from_file_stream/2
# Create a map of shortcodes to filenames from emoji.txt
File.read!(txt_path)
txt_path
|> File.read!()
|> String.split("\n")
|> Enum.map(&String.trim/1)
|> Enum.map(fn line ->
@ -226,21 +254,18 @@ defmodule Pleroma.Emoji.Pack do
[name, file | _] ->
file_dir_name = Path.dirname(file)
file =
if String.ends_with?(path, file_dir_name) do
Path.basename(file)
{name, Path.basename(file)}
else
file
end
{name, file}
end
_ ->
nil
end
end)
|> Enum.filter(& &1)
|> Enum.into(%{})
|> Enum.reject(&is_nil/1)
|> Map.new()
else
# If there's no emoji.txt, assume all files
# that are of certain extensions from the config are emojis and import them all
@ -249,60 +274,20 @@ defmodule Pleroma.Emoji.Pack do
end
end
@spec list_remote(String.t()) :: {:ok, map()}
def list_remote(url) do
uri =
url
|> String.trim()
|> URI.parse()
with {_, true} <- {:shareable, shareable_packs_available?(uri)} do
packs =
uri
|> URI.merge("/api/pleroma/emoji/packs")
|> to_string()
|> Tesla.get!()
|> Map.get(:body)
|> Jason.decode!()
{:ok, packs}
end
end
@spec list_local() :: {:ok, map()}
def list_local do
emoji_path = emoji_path()
# Create the directory first if it does not exist. This is probably the first request made
# with the API so it should be sufficient
with {:create_dir, :ok} <- {:create_dir, File.mkdir_p(emoji_path)},
{:ls, {:ok, results}} <- {:ls, File.ls(emoji_path)} do
packs =
results
|> Enum.map(&load_pack/1)
|> Enum.filter(& &1)
|> Enum.map(&validate_pack/1)
|> Map.new()
{:ok, packs}
end
end
defp validate_pack(pack) do
info =
if downloadable?(pack) do
archive = fetch_archive(pack)
archive_sha = :crypto.hash(:sha256, archive) |> Base.encode16()
info =
pack.pack
|> Map.put("can-download", true)
|> Map.put("download-sha256", archive_sha)
{pack.name, Map.put(pack, :pack, info)}
else
info = Map.put(pack.pack, "can-download", false)
{pack.name, Map.put(pack, :pack, info)}
Map.put(pack.pack, "can-download", false)
end
Map.put(pack, :pack, info)
end
defp downloadable?(pack) do
@ -315,26 +300,6 @@ defmodule Pleroma.Emoji.Pack do
end)
end
@spec get_archive(String.t()) :: {:ok, binary()}
def get_archive(name) do
with {_, %__MODULE__{} = pack} <- {:exists?, load_pack(name)},
{_, true} <- {:can_download?, downloadable?(pack)} do
{:ok, fetch_archive(pack)}
end
end
defp fetch_archive(pack) do
hash = :crypto.hash(:md5, File.read!(pack.pack_file))
case Cachex.get!(:emoji_packs_cache, pack.name) do
%{hash: ^hash, pack_data: archive} ->
archive
_ ->
create_archive_and_cache(pack, hash)
end
end
defp create_archive_and_cache(pack, hash) do
files = ['pack.json' | Enum.map(pack.files, fn {_, file} -> to_charlist(file) end)]
@ -356,23 +321,158 @@ defmodule Pleroma.Emoji.Pack do
result
end
@spec download(String.t(), String.t(), String.t()) :: :ok
def download(name, url, as) do
uri =
url
|> String.trim()
|> URI.parse()
defp save_pack(pack) do
with {:ok, json} <- Jason.encode(pack, pretty: true),
:ok <- File.write(pack.pack_file, json) do
{:ok, pack}
end
end
with {_, true} <- {:shareable, shareable_packs_available?(uri)} do
remote_pack =
uri
|> URI.merge("/api/pleroma/emoji/packs/#{name}")
|> to_string()
|> Tesla.get!()
|> Map.get(:body)
|> Jason.decode!()
defp from_json(json) do
map = Jason.decode!(json)
result =
struct(__MODULE__, %{files: map["files"], pack: map["pack"]})
end
defp validate_shareable_packs_available(uri) do
with {:ok, %{"links" => links}} <- uri |> URI.merge("/.well-known/nodeinfo") |> http_get(),
# Get the actual nodeinfo address and fetch it
{:ok, %{"metadata" => %{"features" => features}}} <-
links |> List.last() |> Map.get("href") |> http_get() do
if Enum.member?(features, "shareable_emoji_packs") do
:ok
else
{:error, :not_shareable}
end
end
end
defp validate_not_empty(list) do
if Enum.all?(list, fn i -> is_binary(i) and i != "" end) do
:ok
else
{:error, :empty_values}
end
end
defp save_file(file, pack, filename) do
file_path = Path.join(pack.path, filename)
create_subdirs(file_path)
case file do
%Plug.Upload{path: upload_path} ->
# Copy the uploaded file from the temporary directory
with {:ok, _} <- File.copy(upload_path, file_path), do: :ok
url when is_binary(url) ->
# Download and write the file
file_contents = Tesla.get!(url).body
File.write(file_path, file_contents)
end
end
defp put_emoji(pack, shortcode, filename) do
files = Map.put(pack.files, shortcode, filename)
%{pack | files: files}
end
defp delete_emoji(pack, shortcode) do
files = Map.delete(pack.files, shortcode)
%{pack | files: files}
end
defp rename_file(pack, filename, new_filename) do
old_path = Path.join(pack.path, filename)
new_path = Path.join(pack.path, new_filename)
create_subdirs(new_path)
with :ok <- File.rename(old_path, new_path) do
remove_dir_if_empty(old_path, filename)
end
end
defp create_subdirs(file_path) do
if String.contains?(file_path, "/") do
file_path
|> Path.dirname()
|> File.mkdir_p!()
end
end
defp remove_file(pack, shortcode) do
with {:ok, filename} <- get_filename(pack, shortcode),
emoji <- Path.join(pack.path, filename),
:ok <- File.rm(emoji) do
remove_dir_if_empty(emoji, filename)
end
end
defp remove_dir_if_empty(emoji, filename) do
dir = Path.dirname(emoji)
if String.contains?(filename, "/") and File.ls!(dir) == [] do
File.rmdir!(dir)
else
:ok
end
end
defp get_filename(pack, shortcode) do
with %{^shortcode => filename} when is_binary(filename) <- pack.files,
true <- pack.path |> Path.join(filename) |> File.exists?() do
{:ok, filename}
else
_ -> {:error, :doesnt_exist}
end
end
defp http_get(%URI{} = url), do: url |> to_string() |> http_get()
defp http_get(url) do
with {:ok, %{body: body}} <- url |> Pleroma.HTTP.get() do
Jason.decode(body)
end
end
defp list_packs_dir do
emoji_path = emoji_path()
# Create the directory first if it does not exist. This is probably the first request made
# with the API so it should be sufficient
with {:create_dir, :ok} <- {:create_dir, File.mkdir_p(emoji_path)},
{:ls, {:ok, results}} <- {:ls, File.ls(emoji_path)} do
{:ok, results}
else
{:create_dir, {:error, e}} -> {:error, :create_dir, e}
{:ls, {:error, e}} -> {:error, :ls, e}
end
end
defp validate_downloadable(pack) do
if downloadable?(pack), do: :ok, else: {:error, :cant_download}
end
defp copy_as(remote_pack, local_name) do
path = Path.join(emoji_path(), local_name)
%__MODULE__{
name: local_name,
path: path,
files: remote_pack["files"],
pack_file: Path.join(path, "pack.json")
}
end
defp unzip(archive, pack_info, remote_pack, local_pack) do
with :ok <- File.mkdir_p!(local_pack.path) do
files = Enum.map(remote_pack["files"], fn {_, path} -> to_charlist(path) end)
# Fallback cannot contain a pack.json file
files = if pack_info[:fallback], do: files, else: ['pack.json' | files]
:zip.unzip(archive, cwd: to_charlist(local_pack.path), file_list: files)
end
end
defp fetch_pack_info(remote_pack, uri, name) do
case remote_pack["pack"] do
%{"share-files" => true, "can-download" => true, "download-sha256" => sha} ->
{:ok,
@ -390,118 +490,52 @@ defmodule Pleroma.Emoji.Pack do
}}
_ ->
{:error,
"The pack was not set as shared and there is no fallback src to download from"}
{:error, "The pack was not set as shared and there is no fallback src to download from"}
end
end
with {:ok, %{sha: sha, url: url} = pinfo} <- result,
%{body: archive} <- Tesla.get!(url),
{_, true} <- {:checksum, Base.decode16!(sha) == :crypto.hash(:sha256, archive)} do
local_name = as || name
path = Path.join(emoji_path(), local_name)
pack = %__MODULE__{
name: local_name,
path: path,
files: remote_pack["files"],
pack_file: Path.join(path, "pack.json")
}
File.mkdir_p!(pack.path)
files = Enum.map(remote_pack["files"], fn {_, path} -> to_charlist(path) end)
# Fallback cannot contain a pack.json file
files = if pinfo[:fallback], do: files, else: ['pack.json' | files]
{:ok, _} = :zip.unzip(archive, cwd: to_charlist(pack.path), file_list: files)
# Fallback can't contain a pack.json file, since that would cause the fallback-src-sha256
# in it to depend on itself
if pinfo[:fallback] do
save_pack(pack)
end
:ok
defp download_archive(url, sha) do
with {:ok, %{body: archive}} <- Tesla.get(url) do
if Base.decode16!(sha) == :crypto.hash(:sha256, archive) do
{:ok, archive}
else
{:error, :imvalid_checksum}
end
end
end
defp save_pack(pack), do: File.write(pack.pack_file, Jason.encode!(pack, pretty: true))
defp fetch_archive(pack) do
hash = :crypto.hash(:md5, File.read!(pack.pack_file))
@spec save_metadata(map(), t()) :: {:ok, t()} | {:error, File.posix()}
def save_metadata(metadata, %__MODULE__{} = pack) do
pack = Map.put(pack, :pack, metadata)
with :ok <- save_pack(pack) do
{:ok, pack}
case Cachex.get!(:emoji_packs_cache, pack.name) do
%{hash: ^hash, pack_data: archive} -> archive
_ -> create_archive_and_cache(pack, hash)
end
end
@spec update_metadata(String.t(), map()) :: {:ok, t()} | {:error, File.posix()}
def update_metadata(name, data) do
pack = load_pack(name)
defp fallback_sha_changed?(pack, data) do
is_binary(data[:"fallback-src"]) and data[:"fallback-src"] != pack.pack["fallback-src"]
end
fb_sha_changed? =
not is_nil(data["fallback-src"]) and data["fallback-src"] != pack.pack["fallback-src"]
with {_, true} <- {:update?, fb_sha_changed?},
{:ok, %{body: zip}} <- Tesla.get(data["fallback-src"]),
{:ok, f_list} <- :zip.unzip(zip, [:memory]),
{_, true} <- {:has_all_files?, has_all_files?(pack.files, f_list)} do
fallback_sha = :crypto.hash(:sha256, zip) |> Base.encode16()
defp update_sha_and_save_metadata(pack, data) do
with {:ok, %{body: zip}} <- Tesla.get(data[:"fallback-src"]),
:ok <- validate_has_all_files(pack, zip) do
fallback_sha = :sha256 |> :crypto.hash(zip) |> Base.encode16()
data
|> Map.put("fallback-src-sha256", fallback_sha)
|> save_metadata(pack)
else
{:update?, _} -> save_metadata(data, pack)
e -> e
end
end
defp validate_has_all_files(pack, zip) do
with {:ok, f_list} <- :zip.unzip(zip, [:memory]) do
# Check if all files from the pack.json are in the archive
defp has_all_files?(files, f_list) do
Enum.all?(files, fn {_, from_manifest} ->
pack.files
|> Enum.all?(fn {_, from_manifest} ->
List.keyfind(f_list, to_charlist(from_manifest), 0)
end)
|> if(do: :ok, else: {:error, :incomplete})
end
@spec load_pack(String.t()) :: t() | nil
def load_pack(name) do
pack_file = Path.join([emoji_path(), name, "pack.json"])
if File.exists?(pack_file) do
pack_file
|> File.read!()
|> from_json()
|> Map.put(:pack_file, pack_file)
|> Map.put(:path, Path.dirname(pack_file))
|> Map.put(:name, name)
end
end
defp from_json(json) do
map = Jason.decode!(json)
struct(__MODULE__, %{files: map["files"], pack: map["pack"]})
end
defp shareable_packs_available?(uri) do
uri
|> URI.merge("/.well-known/nodeinfo")
|> to_string()
|> Tesla.get!()
|> Map.get(:body)
|> Jason.decode!()
|> Map.get("links")
|> List.last()
|> Map.get("href")
# Get the actual nodeinfo address and fetch it
|> Tesla.get!()
|> Map.get(:body)
|> Jason.decode!()
|> get_in(["metadata", "features"])
|> Enum.member?("shareable_emoji_packs")
end
end

View file

@ -89,11 +89,10 @@ defmodule Pleroma.Filter do
|> Repo.delete()
end
def update(%Pleroma.Filter{} = filter) do
destination = Map.from_struct(filter)
Pleroma.Filter.get(filter.filter_id, %{id: filter.user_id})
|> cast(destination, [:phrase, :context, :hide, :expires_at, :whole_word])
def update(%Pleroma.Filter{} = filter, params) do
filter
|> cast(params, [:phrase, :context, :hide, :expires_at, :whole_word])
|> validate_required([:phrase, :context])
|> Repo.update()
end
end

View file

@ -29,7 +29,7 @@ defmodule Pleroma.Healthcheck do
@spec system_info() :: t()
def system_info do
%Healthcheck{
memory_used: Float.round(:erlang.memory(:total) / 1024 / 1024, 2)
memory_used: Float.round(:recon_alloc.memory(:allocated) / 1024 / 1024, 2)
}
|> assign_db_info()
|> assign_job_queue_stats()

View file

@ -24,4 +24,7 @@ defmodule Pleroma.Helpers.UriHelper do
params
end
end
def maybe_add_base("/" <> uri, base), do: Path.join([base, uri])
def maybe_add_base(uri, _base), do: uri
end

View file

@ -9,24 +9,34 @@ defmodule Pleroma.Marker do
import Ecto.Query
alias Ecto.Multi
alias Pleroma.Notification
alias Pleroma.Repo
alias Pleroma.User
alias __MODULE__
@timelines ["notifications"]
@type t :: %__MODULE__{}
schema "markers" do
field(:last_read_id, :string, default: "")
field(:timeline, :string, default: "")
field(:lock_version, :integer, default: 0)
field(:unread_count, :integer, default: 0, virtual: true)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
timestamps()
end
@doc "Gets markers by user and timeline."
@spec get_markers(User.t(), list(String)) :: list(t())
def get_markers(user, timelines \\ []) do
Repo.all(get_query(user, timelines))
user
|> get_query(timelines)
|> unread_count_query()
|> Repo.all()
end
@spec upsert(User.t(), map()) :: {:ok | :error, any()}
def upsert(%User{} = user, attrs) do
attrs
|> Map.take(@timelines)
@ -45,6 +55,27 @@ defmodule Pleroma.Marker do
|> Repo.transaction()
end
@spec multi_set_last_read_id(Multi.t(), User.t(), String.t()) :: Multi.t()
def multi_set_last_read_id(multi, %User{} = user, "notifications") do
multi
|> Multi.run(:counters, fn _repo, _changes ->
{:ok, %{last_read_id: Repo.one(Notification.last_read_query(user))}}
end)
|> Multi.insert(
:marker,
fn %{counters: attrs} ->
%Marker{timeline: "notifications", user_id: user.id}
|> struct(attrs)
|> Ecto.Changeset.change()
end,
returning: true,
on_conflict: {:replace, [:last_read_id]},
conflict_target: [:user_id, :timeline]
)
end
def multi_set_last_read_id(multi, _, _), do: multi
defp get_marker(user, timeline) do
case Repo.find_resource(get_query(user, timeline)) do
{:ok, marker} -> %__MODULE__{marker | user: user}
@ -71,4 +102,16 @@ defmodule Pleroma.Marker do
|> by_user_id(user.id)
|> by_timeline(timelines)
end
defp unread_count_query(query) do
from(
q in query,
left_join: n in "notifications",
on: n.user_id == q.user_id and n.seen == false,
group_by: [:id],
select_merge: %{
unread_count: fragment("count(?)", n.id)
}
)
end
end

155
lib/pleroma/mfa.ex Normal file
View file

@ -0,0 +1,155 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.MFA do
@moduledoc """
The MFA context.
"""
alias Pleroma.User
alias Pleroma.MFA.BackupCodes
alias Pleroma.MFA.Changeset
alias Pleroma.MFA.Settings
alias Pleroma.MFA.TOTP
@doc """
Returns MFA methods the user has enabled.
## Examples
iex> Pleroma.MFA.supported_method(User)
"totp, u2f"
"""
@spec supported_methods(User.t()) :: String.t()
def supported_methods(user) do
settings = fetch_settings(user)
Settings.mfa_methods()
|> Enum.reduce([], fn m, acc ->
if method_enabled?(m, settings) do
acc ++ [m]
else
acc
end
end)
|> Enum.join(",")
end
@doc "Checks that user enabled MFA"
def require?(user) do
fetch_settings(user).enabled
end
@doc """
Display MFA settings of user
"""
def mfa_settings(user) do
settings = fetch_settings(user)
Settings.mfa_methods()
|> Enum.map(fn m -> [m, method_enabled?(m, settings)] end)
|> Enum.into(%{enabled: settings.enabled}, fn [a, b] -> {a, b} end)
end
@doc false
def fetch_settings(%User{} = user) do
user.multi_factor_authentication_settings || %Settings{}
end
@doc "clears backup codes"
def invalidate_backup_code(%User{} = user, hash_code) do
%{backup_codes: codes} = fetch_settings(user)
user
|> Changeset.cast_backup_codes(codes -- [hash_code])
|> User.update_and_set_cache()
end
@doc "generates backup codes"
@spec generate_backup_codes(User.t()) :: {:ok, list(binary)} | {:error, String.t()}
def generate_backup_codes(%User{} = user) do
with codes <- BackupCodes.generate(),
hashed_codes <- Enum.map(codes, &Pbkdf2.hash_pwd_salt/1),
changeset <- Changeset.cast_backup_codes(user, hashed_codes),
{:ok, _} <- User.update_and_set_cache(changeset) do
{:ok, codes}
else
{:error, msg} ->
%{error: msg}
end
end
@doc """
Generates secret key and set delivery_type to 'app' for TOTP method.
"""
@spec setup_totp(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def setup_totp(user) do
user
|> Changeset.setup_totp(%{secret: TOTP.generate_secret(), delivery_type: "app"})
|> User.update_and_set_cache()
end
@doc """
Confirms the TOTP method for user.
`attrs`:
`password` - current user password
`code` - TOTP token
"""
@spec confirm_totp(User.t(), map()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t() | atom()}
def confirm_totp(%User{} = user, attrs) do
with settings <- user.multi_factor_authentication_settings.totp,
{:ok, :pass} <- TOTP.validate_token(settings.secret, attrs["code"]) do
user
|> Changeset.confirm_totp()
|> User.update_and_set_cache()
end
end
@doc """
Disables the TOTP method for user.
`attrs`:
`password` - current user password
"""
@spec disable_totp(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def disable_totp(%User{} = user) do
user
|> Changeset.disable_totp()
|> Changeset.disable()
|> User.update_and_set_cache()
end
@doc """
Force disables all MFA methods for user.
"""
@spec disable(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def disable(%User{} = user) do
user
|> Changeset.disable_totp()
|> Changeset.disable(true)
|> User.update_and_set_cache()
end
@doc """
Checks if the user has MFA method enabled.
"""
def method_enabled?(method, settings) do
with {:ok, %{confirmed: true} = _} <- Map.fetch(settings, method) do
true
else
_ -> false
end
end
@doc """
Checks if the user has enabled at least one MFA method.
"""
def enabled?(settings) do
Settings.mfa_methods()
|> Enum.map(fn m -> method_enabled?(m, settings) end)
|> Enum.any?()
end
end

View file

@ -0,0 +1,31 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.MFA.BackupCodes do
@moduledoc """
This module contains functions for generating backup codes.
"""
alias Pleroma.Config
@config_ns [:instance, :multi_factor_authentication, :backup_codes]
@doc """
Generates backup codes.
"""
@spec generate(Keyword.t()) :: list(String.t())
def generate(opts \\ []) do
number_of_codes = Keyword.get(opts, :number_of_codes, default_backup_codes_number())
code_length = Keyword.get(opts, :length, default_backup_codes_code_length())
Enum.map(1..number_of_codes, fn _ ->
:crypto.strong_rand_bytes(div(code_length, 2))
|> Base.encode16(case: :lower)
end)
end
defp default_backup_codes_number, do: Config.get(@config_ns ++ [:number], 5)
defp default_backup_codes_code_length,
do: Config.get(@config_ns ++ [:length], 16)
end

View file

@ -0,0 +1,64 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.MFA.Changeset do
alias Pleroma.MFA
alias Pleroma.MFA.Settings
alias Pleroma.User
def disable(%Ecto.Changeset{} = changeset, force \\ false) do
settings =
changeset
|> Ecto.Changeset.apply_changes()
|> MFA.fetch_settings()
if force || not MFA.enabled?(settings) do
put_change(changeset, %Settings{settings | enabled: false})
else
changeset
end
end
def disable_totp(%User{multi_factor_authentication_settings: settings} = user) do
user
|> put_change(%Settings{settings | totp: %Settings.TOTP{}})
end
def confirm_totp(%User{multi_factor_authentication_settings: settings} = user) do
totp_settings = %Settings.TOTP{settings.totp | confirmed: true}
user
|> put_change(%Settings{settings | totp: totp_settings, enabled: true})
end
def setup_totp(%User{} = user, attrs) do
mfa_settings = MFA.fetch_settings(user)
totp_settings =
%Settings.TOTP{}
|> Ecto.Changeset.cast(attrs, [:secret, :delivery_type])
user
|> put_change(%Settings{mfa_settings | totp: Ecto.Changeset.apply_changes(totp_settings)})
end
def cast_backup_codes(%User{} = user, codes) do
user
|> put_change(%Settings{
user.multi_factor_authentication_settings
| backup_codes: codes
})
end
defp put_change(%User{} = user, settings) do
user
|> Ecto.Changeset.change()
|> put_change(settings)
end
defp put_change(%Ecto.Changeset{} = changeset, settings) do
changeset
|> Ecto.Changeset.put_change(:multi_factor_authentication_settings, settings)
end
end

View file

@ -0,0 +1,24 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.MFA.Settings do
use Ecto.Schema
@primary_key false
@mfa_methods [:totp]
embedded_schema do
field(:enabled, :boolean, default: false)
field(:backup_codes, {:array, :string}, default: [])
embeds_one :totp, TOTP, on_replace: :delete, primary_key: false do
field(:secret, :string)
# app | sms
field(:delivery_type, :string, default: "app")
field(:confirmed, :boolean, default: false)
end
end
def mfa_methods, do: @mfa_methods
end

106
lib/pleroma/mfa/token.ex Normal file
View file

@ -0,0 +1,106 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.MFA.Token do
use Ecto.Schema
import Ecto.Query
import Ecto.Changeset
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.OAuth.Authorization
alias Pleroma.Web.OAuth.Token, as: OAuthToken
@expires 300
schema "mfa_tokens" do
field(:token, :string)
field(:valid_until, :naive_datetime_usec)
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:authorization, Authorization)
timestamps()
end
def get_by_token(token) do
from(
t in __MODULE__,
where: t.token == ^token,
preload: [:user, :authorization]
)
|> Repo.find_resource()
end
def validate(token) do
with {:fetch_token, {:ok, token}} <- {:fetch_token, get_by_token(token)},
{:expired, false} <- {:expired, is_expired?(token)} do
{:ok, token}
else
{:expired, _} -> {:error, :expired_token}
{:fetch_token, _} -> {:error, :not_found}
error -> {:error, error}
end
end
def create_token(%User{} = user) do
%__MODULE__{}
|> change
|> assign_user(user)
|> put_token
|> put_valid_until
|> Repo.insert()
end
def create_token(user, authorization) do
%__MODULE__{}
|> change
|> assign_user(user)
|> assign_authorization(authorization)
|> put_token
|> put_valid_until
|> Repo.insert()
end
defp assign_user(changeset, user) do
changeset
|> put_assoc(:user, user)
|> validate_required([:user])
end
defp assign_authorization(changeset, authorization) do
changeset
|> put_assoc(:authorization, authorization)
|> validate_required([:authorization])
end
defp put_token(changeset) do
changeset
|> change(%{token: OAuthToken.Utils.generate_token()})
|> validate_required([:token])
|> unique_constraint(:token)
end
defp put_valid_until(changeset) do
expires_in = NaiveDateTime.add(NaiveDateTime.utc_now(), @expires)
changeset
|> change(%{valid_until: expires_in})
|> validate_required([:valid_until])
end
def is_expired?(%__MODULE__{valid_until: valid_until}) do
NaiveDateTime.diff(NaiveDateTime.utc_now(), valid_until) > 0
end
def is_expired?(_), do: false
def delete_expired_tokens do
from(
q in __MODULE__,
where: fragment("?", q.valid_until) < ^Timex.now()
)
|> Repo.delete_all()
end
end

86
lib/pleroma/mfa/totp.ex Normal file
View file

@ -0,0 +1,86 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.MFA.TOTP do
@moduledoc """
This module represents functions to create secrets for
TOTP Application as well as validate them with a time based token.
"""
alias Pleroma.Config
@config_ns [:instance, :multi_factor_authentication, :totp]
@doc """
https://github.com/google/google-authenticator/wiki/Key-Uri-Format
"""
def provisioning_uri(secret, label, opts \\ []) do
query =
%{
secret: secret,
issuer: Keyword.get(opts, :issuer, default_issuer()),
digits: Keyword.get(opts, :digits, default_digits()),
period: Keyword.get(opts, :period, default_period())
}
|> Enum.filter(fn {_, v} -> not is_nil(v) end)
|> Enum.into(%{})
|> URI.encode_query()
%URI{scheme: "otpauth", host: "totp", path: "/" <> label, query: query}
|> URI.to_string()
end
defp default_period, do: Config.get(@config_ns ++ [:period])
defp default_digits, do: Config.get(@config_ns ++ [:digits])
defp default_issuer,
do: Config.get(@config_ns ++ [:issuer], Config.get([:instance, :name]))
@doc "Creates a random Base 32 encoded string"
def generate_secret do
Base.encode32(:crypto.strong_rand_bytes(10))
end
@doc "Generates a valid token based on a secret"
def generate_token(secret) do
:pot.totp(secret)
end
@doc """
Validates a given token based on a secret.
optional parameters:
`token_length` default `6`
`interval_length` default `30`
`window` default 0
Returns {:ok, :pass} if the token is valid and
{:error, :invalid_token} if it is not.
"""
@spec validate_token(String.t(), String.t()) ::
{:ok, :pass} | {:error, :invalid_token | :invalid_secret_and_token}
def validate_token(secret, token)
when is_binary(secret) and is_binary(token) do
opts = [
token_length: default_digits(),
interval_length: default_period()
]
validate_token(secret, token, opts)
end
def validate_token(_, _), do: {:error, :invalid_secret_and_token}
@doc "See `validate_token/2`"
@spec validate_token(String.t(), String.t(), Keyword.t()) ::
{:ok, :pass} | {:error, :invalid_token | :invalid_secret_and_token}
def validate_token(secret, token, options)
when is_binary(secret) and is_binary(token) do
case :pot.valid_totp(token, secret, options) do
true -> {:ok, :pass}
false -> {:error, :invalid_token}
end
end
def validate_token(_, _, _), do: {:error, :invalid_secret_and_token}
end

View file

@ -5,8 +5,10 @@
defmodule Pleroma.Notification do
use Ecto.Schema
alias Ecto.Multi
alias Pleroma.Activity
alias Pleroma.FollowingRelationship
alias Pleroma.Marker
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Pagination
@ -34,11 +36,30 @@ defmodule Pleroma.Notification do
timestamps()
end
@spec unread_notifications_count(User.t()) :: integer()
def unread_notifications_count(%User{id: user_id}) do
from(q in __MODULE__,
where: q.user_id == ^user_id and q.seen == false
)
|> Repo.aggregate(:count, :id)
end
def changeset(%Notification{} = notification, attrs) do
notification
|> cast(attrs, [:seen])
end
@spec last_read_query(User.t()) :: Ecto.Queryable.t()
def last_read_query(user) do
from(q in Pleroma.Notification,
where: q.user_id == ^user.id,
where: q.seen == true,
select: type(q.id, :string),
limit: 1,
order_by: [desc: :id]
)
end
defp for_user_query_ap_id_opts(user, opts) do
ap_id_relationships =
[:block] ++
@ -185,25 +206,23 @@ defmodule Pleroma.Notification do
|> Repo.all()
end
def set_read_up_to(%{id: user_id} = _user, id) do
def set_read_up_to(%{id: user_id} = user, id) do
query =
from(
n in Notification,
where: n.user_id == ^user_id,
where: n.id <= ^id,
where: n.seen == false,
update: [
set: [
seen: true,
updated_at: ^NaiveDateTime.utc_now()
]
],
# Ideally we would preload object and activities here
# but Ecto does not support preloads in update_all
select: n.id
)
{_, notification_ids} = Repo.update_all(query, [])
{:ok, %{ids: {_, notification_ids}}} =
Multi.new()
|> Multi.update_all(:ids, query, set: [seen: true, updated_at: NaiveDateTime.utc_now()])
|> Marker.multi_set_last_read_id(user, "notifications")
|> Repo.transaction()
Notification
|> where([n], n.id in ^notification_ids)
@ -220,11 +239,18 @@ defmodule Pleroma.Notification do
|> Repo.all()
end
@spec read_one(User.t(), String.t()) ::
{:ok, Notification.t()} | {:error, Ecto.Changeset.t()} | nil
def read_one(%User{} = user, notification_id) do
with {:ok, %Notification{} = notification} <- get(user, notification_id) do
notification
|> changeset(%{seen: true})
|> Repo.update()
Multi.new()
|> Multi.update(:update, changeset(notification, %{seen: true}))
|> Marker.multi_set_last_read_id(user, "notifications")
|> Repo.transaction()
|> case do
{:ok, %{update: notification}} -> {:ok, notification}
{:error, :update, changeset, _} -> {:error, changeset}
end
end
end
@ -316,8 +342,11 @@ defmodule Pleroma.Notification do
# TODO move to sql, too.
def create_notification(%Activity{} = activity, %User{} = user, do_send \\ true) do
unless skip?(activity, user) do
notification = %Notification{user_id: user.id, activity: activity}
{:ok, notification} = Repo.insert(notification)
{:ok, %{notification: notification}} =
Multi.new()
|> Multi.insert(:notification, %Notification{user_id: user.id, activity: activity})
|> Marker.multi_set_last_read_id(user, "notifications")
|> Repo.transaction()
if do_send do
Streamer.stream(["user", "user:notification"], notification)
@ -339,13 +368,7 @@ defmodule Pleroma.Notification do
def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact"] do
potential_receiver_ap_ids =
[]
|> Utils.maybe_notify_to_recipients(activity)
|> Utils.maybe_notify_mentioned_recipients(activity)
|> Utils.maybe_notify_subscribers(activity)
|> Utils.maybe_notify_followers(activity)
|> Enum.uniq()
potential_receiver_ap_ids = get_potential_receiver_ap_ids(activity)
potential_receivers = User.get_users_from_set(potential_receiver_ap_ids, local_only)
@ -363,6 +386,27 @@ defmodule Pleroma.Notification do
def get_notified_from_activity(_, _local_only), do: {[], []}
# For some activities, only notify the author of the object
def get_potential_receiver_ap_ids(%{data: %{"type" => type, "object" => object_id}})
when type in ~w{Like Announce EmojiReact} do
case Object.get_cached_by_ap_id(object_id) do
%Object{data: %{"actor" => actor}} ->
[actor]
_ ->
[]
end
end
def get_potential_receiver_ap_ids(activity) do
[]
|> Utils.maybe_notify_to_recipients(activity)
|> Utils.maybe_notify_mentioned_recipients(activity)
|> Utils.maybe_notify_subscribers(activity)
|> Utils.maybe_notify_followers(activity)
|> Enum.uniq()
end
@doc "Filters out AP IDs domain-blocking and not following the activity's actor"
def exclude_domain_blocker_ap_ids(ap_ids, activity, preloaded_users \\ [])

View file

@ -9,11 +9,13 @@ defmodule Pleroma.Object do
import Ecto.Changeset
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Object
alias Pleroma.Object.Fetcher
alias Pleroma.ObjectTombstone
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Workers.AttachmentsCleanupWorker
require Logger
@ -138,12 +140,17 @@ defmodule Pleroma.Object do
def normalize(_, _, _), do: nil
# Owned objects can only be mutated by their owner
def authorize_mutation(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}),
do: actor == ap_id
# Owned objects can only be accessed by their owner
def authorize_access(%Object{data: %{"actor" => actor}}, %User{ap_id: ap_id}) do
if actor == ap_id do
:ok
else
{:error, :forbidden}
end
end
# Legacy objects can be mutated by anybody
def authorize_mutation(%Object{}, %User{}), do: true
# Legacy objects can be accessed by anybody
def authorize_access(%Object{}, %User{}), do: :ok
@spec get_cached_by_ap_id(String.t()) :: Object.t() | nil
def get_cached_by_ap_id(ap_id) do
@ -183,27 +190,37 @@ defmodule Pleroma.Object do
def delete(%Object{data: %{"id" => id}} = object) do
with {:ok, _obj} = swap_object_with_tombstone(object),
deleted_activity = Activity.delete_all_by_object_ap_id(id),
{:ok, true} <- Cachex.del(:object_cache, "object:#{id}"),
{:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do
with true <- Pleroma.Config.get([:instance, :cleanup_attachments]) do
{:ok, _} =
Pleroma.Workers.AttachmentsCleanupWorker.enqueue("cleanup_attachments", %{
"object" => object
})
end
{:ok, _} <- invalid_object_cache(object) do
cleanup_attachments(
Config.get([:instance, :cleanup_attachments]),
%{"object" => object}
)
{:ok, object, deleted_activity}
end
end
def prune(%Object{data: %{"id" => id}} = object) do
@spec cleanup_attachments(boolean(), %{required(:object) => map()}) ::
{:ok, Oban.Job.t() | nil}
def cleanup_attachments(true, %{"object" => _} = params) do
AttachmentsCleanupWorker.enqueue("cleanup_attachments", params)
end
def cleanup_attachments(_, _), do: {:ok, nil}
def prune(%Object{data: %{"id" => _id}} = object) do
with {:ok, object} <- Repo.delete(object),
{:ok, true} <- Cachex.del(:object_cache, "object:#{id}"),
{:ok, _} <- Cachex.del(:web_resp_cache, URI.parse(id).path) do
{:ok, _} <- invalid_object_cache(object) do
{:ok, object}
end
end
def invalid_object_cache(%Object{data: %{"id" => id}}) do
with {:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do
Cachex.del(:web_resp_cache, URI.parse(id).path)
end
end
def set_cache(%Object{data: %{"id" => ap_id}} = object) do
Cachex.put(:object_cache, "object:#{ap_id}", object)
{:ok, object}

View file

@ -3,7 +3,6 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.AuthenticationPlug do
alias Comeonin.Pbkdf2
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
@ -17,8 +16,13 @@ defmodule Pleroma.Plugs.AuthenticationPlug do
:crypt.crypt(password, password_hash) == password_hash
end
def checkpw(password, "$2" <> _ = password_hash) do
# Handle bcrypt passwords for Mastodon migration
Bcrypt.verify_pass(password, password_hash)
end
def checkpw(password, "$pbkdf2" <> _ = password_hash) do
Pbkdf2.checkpw(password, password_hash)
Pbkdf2.verify_pass(password, password_hash)
end
def checkpw(_password, _password_hash) do
@ -26,6 +30,25 @@ defmodule Pleroma.Plugs.AuthenticationPlug do
false
end
def maybe_update_password(%User{password_hash: "$2" <> _} = user, password) do
do_update_password(user, password)
end
def maybe_update_password(%User{password_hash: "$6" <> _} = user, password) do
do_update_password(user, password)
end
def maybe_update_password(user, _), do: {:ok, user}
defp do_update_password(user, password) do
user
|> User.password_update_changeset(%{
"password" => password,
"password_confirmation" => password
})
|> Pleroma.Repo.update()
end
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
def call(
@ -37,7 +60,9 @@ defmodule Pleroma.Plugs.AuthenticationPlug do
} = conn,
_
) do
if Pbkdf2.checkpw(password, password_hash) do
if checkpw(password, password_hash) do
{:ok, auth_user} = maybe_update_password(auth_user, password)
conn
|> assign(:user, auth_user)
|> OAuthScopesPlug.skip_plug()
@ -47,7 +72,7 @@ defmodule Pleroma.Plugs.AuthenticationPlug do
end
def call(%{assigns: %{auth_credentials: %{password: _}}} = conn, _) do
Pbkdf2.dummy_checkpw()
Pbkdf2.no_user_verify()
conn
end

View file

@ -15,26 +15,25 @@ defmodule Pleroma.Plugs.EnsureAuthenticatedPlug do
end
@impl true
def perform(
%{
assigns: %{
auth_credentials: %{password: _},
user: %User{multi_factor_authentication_settings: %{enabled: true}}
}
} = conn,
_
) do
conn
|> render_error(:forbidden, "Two-factor authentication enabled, you must use a access token.")
|> halt()
end
def perform(%{assigns: %{user: %User{}}} = conn, _) do
conn
end
def perform(conn, options) do
perform =
cond do
options[:if_func] -> options[:if_func].()
options[:unless_func] -> !options[:unless_func].()
true -> true
end
if perform do
fail(conn)
else
conn
end
end
def fail(conn) do
def perform(conn, _) do
conn
|> render_error(:forbidden, "Invalid credentials.")
|> halt()

View file

@ -19,6 +19,9 @@ defmodule Pleroma.Web.FederatingPlug do
def federating?, do: Pleroma.Config.get([:instance, :federating])
# Definition for the use in :if_func / :unless_func plug options
def federating?(_conn), do: federating?()
defp fail(conn) do
conn
|> put_status(404)

View file

@ -40,7 +40,7 @@ defmodule Pleroma.ScheduledActivity do
%{changes: %{params: %{"media_ids" => media_ids} = params}} = changeset
)
when is_list(media_ids) do
media_attachments = Utils.attachments_from_ids(%{"media_ids" => media_ids})
media_attachments = Utils.attachments_from_ids(%{media_ids: media_ids})
params =
params

View file

@ -91,7 +91,7 @@ defmodule Pleroma.Stats do
peers: peers,
stats: %{
domain_count: domain_count,
status_count: status_count,
status_count: status_count || 0,
user_count: user_count
}
}

View file

@ -134,7 +134,7 @@ defmodule Pleroma.Upload do
end
end
defp prepare_upload(%{"img" => "data:image/" <> image_data}, opts) do
defp prepare_upload(%{img: "data:image/" <> image_data}, opts) do
parsed = Regex.named_captures(~r/(?<filetype>jpeg|png|gif);base64,(?<data>.*)/, image_data)
data = Base.decode64!(parsed["data"], ignore: :whitespace)
hash = String.downcase(Base.encode16(:crypto.hash(:sha256, data)))

View file

@ -9,7 +9,6 @@ defmodule Pleroma.User do
import Ecto.Query
import Ecto, only: [assoc: 2]
alias Comeonin.Pbkdf2
alias Ecto.Multi
alias Pleroma.Activity
alias Pleroma.Config
@ -20,6 +19,7 @@ defmodule Pleroma.User do
alias Pleroma.Formatter
alias Pleroma.HTML
alias Pleroma.Keys
alias Pleroma.MFA
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Registration
@ -29,7 +29,9 @@ defmodule Pleroma.User do
alias Pleroma.UserRelationship
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils, as: CommonUtils
@ -113,7 +115,6 @@ defmodule Pleroma.User do
field(:is_admin, :boolean, default: false)
field(:show_role, :boolean, default: true)
field(:settings, :map, default: nil)
field(:magic_key, :string, default: nil)
field(:uri, Types.Uri, default: nil)
field(:hide_followers_count, :boolean, default: false)
field(:hide_follows_count, :boolean, default: false)
@ -189,6 +190,12 @@ defmodule Pleroma.User do
# `:subscribers` is deprecated (replaced with `subscriber_users` relation)
field(:subscribers, {:array, :string}, default: [])
embeds_one(
:multi_factor_authentication_settings,
MFA.Settings,
on_replace: :delete
)
timestamps()
end
@ -298,8 +305,13 @@ defmodule Pleroma.User do
def avatar_url(user, options \\ []) do
case user.avatar do
%{"url" => [%{"href" => href} | _]} -> href
_ -> !options[:no_default] && "#{Web.base_url()}/images/avi.png"
%{"url" => [%{"href" => href} | _]} ->
href
_ ->
unless options[:no_default] do
Config.get([:assets, :default_user_avatar], "#{Web.base_url()}/images/avi.png")
end
end
end
@ -387,7 +399,6 @@ defmodule Pleroma.User do
:banner,
:locked,
:last_refreshed_at,
:magic_key,
:uri,
:follower_address,
:following_address,
@ -743,7 +754,19 @@ defmodule Pleroma.User do
{:error, "Not subscribed!"}
end
@spec unfollow(User.t(), User.t()) :: {:ok, User.t(), Activity.t()} | {:error, String.t()}
def unfollow(%User{} = follower, %User{} = followed) do
case do_unfollow(follower, followed) do
{:ok, follower, followed} ->
{:ok, follower, Utils.fetch_latest_follow(follower, followed)}
error ->
error
end
end
@spec do_unfollow(User.t(), User.t()) :: {:ok, User.t(), User.t()} | {:error, String.t()}
defp do_unfollow(%User{} = follower, %User{} = followed) do
case get_follow_state(follower, followed) do
state when state in [:follow_pending, :follow_accept] ->
FollowingRelationship.unfollow(follower, followed)
@ -754,7 +777,7 @@ defmodule Pleroma.User do
|> update_following_count()
|> set_cache()
{:ok, follower, Utils.fetch_latest_follow(follower, followed)}
{:ok, follower, followed}
nil ->
{:error, "Not subscribed!"}
@ -927,6 +950,7 @@ defmodule Pleroma.User do
end
end
@spec get_by_nickname(String.t()) :: User.t() | nil
def get_by_nickname(nickname) do
Repo.get_by(User, nickname: nickname) ||
if Regex.match?(~r(@#{Pleroma.Web.Endpoint.host()})i, nickname) do
@ -1197,7 +1221,9 @@ defmodule Pleroma.User do
def get_recipients_from_activity(%Activity{recipients: to, actor: actor}) do
to = [actor | to]
User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
query = User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
query
|> Repo.all()
end
@ -1393,15 +1419,13 @@ defmodule Pleroma.User do
user
|> get_followers()
|> Enum.filter(& &1.local)
|> Enum.each(fn follower ->
follower |> update_following_count() |> set_cache()
end)
|> Enum.each(&set_cache(update_following_count(&1)))
# Only update local user counts, remote will be update during the next pull.
user
|> get_friends()
|> Enum.filter(& &1.local)
|> Enum.each(&update_follower_count/1)
|> Enum.each(&do_unfollow(user, &1))
{:ok, user}
end
@ -1423,12 +1447,29 @@ defmodule Pleroma.User do
BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
end
defp delete_and_invalidate_cache(%User{} = user) do
invalidate_cache(user)
Repo.delete(user)
end
defp delete_or_deactivate(%User{local: false} = user), do: delete_and_invalidate_cache(user)
defp delete_or_deactivate(%User{local: true} = user) do
status = account_status(user)
if status == :confirmation_pending do
delete_and_invalidate_cache(user)
else
user
|> change(%{deactivated: true, email: nil})
|> update_and_set_cache()
end
end
def perform(:force_password_reset, user), do: force_password_reset(user)
@spec perform(atom(), User.t()) :: {:ok, User.t()}
def perform(:delete, %User{} = user) do
{:ok, _user} = ActivityPub.delete(user)
# Remove all relationships
user
|> get_followers()
@ -1446,14 +1487,7 @@ defmodule Pleroma.User do
delete_user_activities(user)
if user.local do
user
|> change(%{deactivated: true, email: nil})
|> update_and_set_cache()
else
invalidate_cache(user)
Repo.delete(user)
end
delete_or_deactivate(user)
end
def perform(:deactivate_async, user, status), do: deactivate(user, status)
@ -1538,37 +1572,42 @@ defmodule Pleroma.User do
})
end
def delete_user_activities(%User{ap_id: ap_id}) do
def delete_user_activities(%User{ap_id: ap_id} = user) do
ap_id
|> Activity.Queries.by_actor()
|> RepoStreamer.chunk_stream(50)
|> Stream.each(fn activities -> Enum.each(activities, &delete_activity/1) end)
|> Stream.each(fn activities ->
Enum.each(activities, fn activity -> delete_activity(activity, user) end)
end)
|> Stream.run()
end
defp delete_activity(%{data: %{"type" => "Create"}} = activity) do
activity
|> Object.normalize()
|> ActivityPub.delete()
defp delete_activity(%{data: %{"type" => "Create", "object" => object}} = activity, user) do
with {_, %Object{}} <- {:find_object, Object.get_by_ap_id(object)},
{:ok, delete_data, _} <- Builder.delete(user, object) do
Pipeline.common_pipeline(delete_data, local: user.local)
else
{:find_object, nil} ->
# We have the create activity, but not the object, it was probably pruned.
# Insert a tombstone and try again
with {:ok, tombstone_data, _} <- Builder.tombstone(user.ap_id, object),
{:ok, _tombstone} <- Object.create(tombstone_data) do
delete_activity(activity, user)
end
defp delete_activity(%{data: %{"type" => "Like"}} = activity) do
object = Object.normalize(activity)
activity.actor
|> get_cached_by_ap_id()
|> ActivityPub.unlike(object)
e ->
Logger.error("Could not delete #{object} created by #{activity.data["ap_id"]}")
Logger.error("Error: #{inspect(e)}")
end
end
defp delete_activity(%{data: %{"type" => "Announce"}} = activity) do
object = Object.normalize(activity)
activity.actor
|> get_cached_by_ap_id()
|> ActivityPub.unannounce(object)
defp delete_activity(%{data: %{"type" => type}} = activity, user)
when type in ["Like", "Announce"] do
{:ok, undo, _} = Builder.undo(user, activity)
Pipeline.common_pipeline(undo, local: user.local)
end
defp delete_activity(_activity), do: "Doing nothing"
defp delete_activity(_activity, _user), do: "Doing nothing"
def html_filter_policy(%User{no_rich_text: true}) do
Pleroma.HTML.Scrubber.TwitterText
@ -1915,7 +1954,7 @@ defmodule Pleroma.User do
defp put_password_hash(
%Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
) do
change(changeset, password_hash: Pbkdf2.hashpwsalt(password))
change(changeset, password_hash: Pbkdf2.hash_pwd_salt(password))
end
defp put_password_hash(changeset), do: changeset

View file

@ -45,6 +45,7 @@ defmodule Pleroma.User.Query do
is_admin: boolean(),
is_moderator: boolean(),
super_users: boolean(),
exclude_service_users: boolean(),
followers: User.t(),
friends: User.t(),
recipients_from_activity: [String.t()],
@ -88,6 +89,10 @@ defmodule Pleroma.User.Query do
where(query, [u], ilike(field(u, ^key), ^"%#{value}%"))
end
defp compose_query({:exclude_service_users, _}, query) do
where(query, [u], not like(u.ap_id, "%/relay") and not like(u.ap_id, "%/internal/fetch"))
end
defp compose_query({key, value}, query)
when key in @equal_criteria and not_empty_string(value) do
where(query, [u], ^[{key, value}])
@ -98,7 +103,7 @@ defmodule Pleroma.User.Query do
end
defp compose_query({:tags, tags}, query) when is_list(tags) and length(tags) > 0 do
Enum.reduce(tags, query, &prepare_tag_criteria/2)
where(query, [u], fragment("? && ?", u.tags, ^tags))
end
defp compose_query({:is_admin, _}, query) do
@ -162,20 +167,18 @@ defmodule Pleroma.User.Query do
end
defp compose_query({:recipients_from_activity, to}, query) do
query
|> join(:left, [u], r in FollowingRelationship,
as: :relationships,
on: r.follower_id == u.id
following_query =
from(u in User,
join: f in FollowingRelationship,
on: u.id == f.following_id,
where: f.state == ^:follow_accept,
where: u.follower_address in ^to,
select: f.follower_id
)
|> join(:left, [relationships: r], f in User,
as: :following,
on: f.id == r.following_id
from(u in query,
where: u.ap_id in ^to or u.id in subquery(following_query)
)
|> where(
[u, following: f, relationships: r],
u.ap_id in ^to or (f.follower_address in ^to and r.state == ^:follow_accept)
)
|> distinct(true)
end
defp compose_query({:order_by, key}, query) do
@ -192,10 +195,6 @@ defmodule Pleroma.User.Query do
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))

View file

@ -10,8 +10,8 @@ defmodule Pleroma.User.WelcomeMessage do
with %User{} = sender_user <- welcome_user(),
message when is_binary(message) <- welcome_message() do
CommonAPI.post(sender_user, %{
"visibility" => "direct",
"status" => "@#{user.nickname}\n#{message}"
visibility: "direct",
status: "@#{user.nickname}\n#{message}"
})
else
_ -> {:ok, nil}

View file

@ -87,6 +87,22 @@ defmodule Pleroma.UserRelationship do
source_to_target_rel_types \\ nil,
target_to_source_rel_types \\ nil
)
def dictionary(
_source_users,
_target_users,
[] = _source_to_target_rel_types,
[] = _target_to_source_rel_types
) do
[]
end
def dictionary(
source_users,
target_users,
source_to_target_rel_types,
target_to_source_rel_types
)
when is_list(source_users) and is_list(target_users) do
source_user_ids = User.binary_id(source_users)
target_user_ids = User.binary_id(target_users)
@ -138,11 +154,16 @@ defmodule Pleroma.UserRelationship do
def view_relationships_option(%User{} = reading_user, actors, opts) do
{source_to_target_rel_types, target_to_source_rel_types} =
if opts[:source_mutes_only] do
# This option is used for rendering statuses (FE needs `muted` flag for each one anyways)
case opts[:subset] do
:source_mutes ->
# Used for statuses rendering (FE needs `muted` flag for each status when statuses load)
{[:mute], []}
else
nil ->
{[:block, :mute, :notification_mute, :reblog_mute], [:block, :inverse_subscription]}
unknown ->
raise "Unsupported :subset option value: #{inspect(unknown)}"
end
user_relationships =
@ -153,7 +174,17 @@ defmodule Pleroma.UserRelationship do
target_to_source_rel_types
)
following_relationships = FollowingRelationship.all_between_user_sets([reading_user], actors)
following_relationships =
case opts[:subset] do
:source_mutes ->
[]
nil ->
FollowingRelationship.all_between_user_sets([reading_user], actors)
unknown ->
raise "Unsupported :subset option value: #{inspect(unknown)}"
end
%{user_relationships: user_relationships, following_relationships: following_relationships}
end

View file

@ -170,12 +170,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
BackgroundWorker.enqueue("fetch_data_for_activity", %{"activity_id" => activity.id})
Notification.create_notifications(activity)
conversation = create_or_bump_conversation(activity, map["actor"])
participations = get_participations(conversation)
stream_out(activity)
stream_out_participations(participations)
{:ok, activity}
else
%Activity{} = activity ->
@ -198,6 +192,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
def notify_and_stream(activity) do
Notification.create_notifications(activity)
conversation = create_or_bump_conversation(activity, activity.actor)
participations = get_participations(conversation)
stream_out(activity)
stream_out_participations(participations)
end
defp create_or_bump_conversation(activity, actor) do
with {:ok, conversation} <- Conversation.create_or_bump_for(activity),
%User{} = user <- User.get_cached_by_ap_id(actor),
@ -274,6 +277,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
_ <- increase_poll_votes_if_vote(create_data),
{:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},
{:ok, _actor} <- increase_note_count_if_public(actor, activity),
_ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
@ -301,6 +305,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
additional
),
{:ok, activity} <- insert(listen_data, local),
_ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
@ -325,6 +330,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
%{"to" => to, "type" => type, "actor" => actor.ap_id, "object" => object}
|> Utils.maybe_put("id", activity_id),
{:ok, activity} <- insert(data, local),
_ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
@ -344,83 +350,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
},
data <- Utils.maybe_put(data, "id", activity_id),
{:ok, activity} <- insert(data, local),
_ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
end
end
@spec react_with_emoji(User.t(), Object.t(), String.t(), keyword()) ::
{:ok, Activity.t(), Object.t()} | {:error, any()}
def react_with_emoji(user, object, emoji, options \\ []) do
with {:ok, result} <-
Repo.transaction(fn -> do_react_with_emoji(user, object, emoji, options) end) do
result
end
end
defp do_react_with_emoji(user, object, emoji, options) do
with local <- Keyword.get(options, :local, true),
activity_id <- Keyword.get(options, :activity_id, nil),
true <- Pleroma.Emoji.is_unicode_emoji?(emoji),
reaction_data <- make_emoji_reaction_data(user, object, emoji, activity_id),
{:ok, activity} <- insert(reaction_data, local),
{:ok, object} <- add_emoji_reaction_to_object(activity, object),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
else
false -> {:error, false}
{:error, error} -> Repo.rollback(error)
end
end
@spec unreact_with_emoji(User.t(), String.t(), keyword()) ::
{:ok, Activity.t(), Object.t()} | {:error, any()}
def unreact_with_emoji(user, reaction_id, options \\ []) do
with {:ok, result} <-
Repo.transaction(fn -> do_unreact_with_emoji(user, reaction_id, options) end) do
result
end
end
defp do_unreact_with_emoji(user, reaction_id, options) do
with local <- Keyword.get(options, :local, true),
activity_id <- Keyword.get(options, :activity_id, nil),
user_ap_id <- user.ap_id,
%Activity{actor: ^user_ap_id} = reaction_activity <- Activity.get_by_ap_id(reaction_id),
object <- Object.normalize(reaction_activity),
unreact_data <- make_undo_data(user, reaction_activity, activity_id),
{:ok, activity} <- insert(unreact_data, local),
{:ok, object} <- remove_emoji_reaction_from_object(reaction_activity, object),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
else
{:error, error} -> Repo.rollback(error)
end
end
@spec unlike(User.t(), Object.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t(), Activity.t(), Object.t()} | {:ok, Object.t()} | {:error, any()}
def unlike(%User{} = actor, %Object{} = object, activity_id \\ nil, local \\ true) do
with {:ok, result} <-
Repo.transaction(fn -> do_unlike(actor, object, activity_id, local) end) do
result
end
end
defp do_unlike(actor, object, activity_id, local) do
with %Activity{} = like_activity <- get_existing_like(actor.ap_id, object),
unlike_data <- make_unlike_data(actor, like_activity, activity_id),
{:ok, unlike_activity} <- insert(unlike_data, local),
{:ok, _activity} <- Repo.delete(like_activity),
{:ok, object} <- remove_like_from_object(like_activity, object),
:ok <- maybe_federate(unlike_activity) do
{:ok, unlike_activity, like_activity, object}
else
nil -> {:ok, object}
{:error, error} -> Repo.rollback(error)
end
end
@spec announce(User.t(), Object.t(), String.t() | nil, boolean(), boolean()) ::
{:ok, Activity.t(), Object.t()} | {:error, any()}
def announce(
@ -442,6 +377,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
announce_data <- make_announce_data(user, object, activity_id, public),
{:ok, activity} <- insert(announce_data, local),
{:ok, object} <- add_announce_to_object(activity, object),
_ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
else
@ -450,34 +386,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
@spec unannounce(User.t(), Object.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t(), Object.t()} | {:ok, Object.t()} | {:error, any()}
def unannounce(
%User{} = actor,
%Object{} = object,
activity_id \\ nil,
local \\ true
) do
with {:ok, result} <-
Repo.transaction(fn -> do_unannounce(actor, object, activity_id, local) end) do
result
end
end
defp do_unannounce(actor, object, activity_id, local) do
with %Activity{} = announce_activity <- get_existing_announce(actor.ap_id, object),
unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id),
{:ok, unannounce_activity} <- insert(unannounce_data, local),
:ok <- maybe_federate(unannounce_activity),
{:ok, _activity} <- Repo.delete(announce_activity),
{:ok, object} <- remove_announce_from_object(announce_activity, object) do
{:ok, unannounce_activity, object}
else
nil -> {:ok, object}
{:error, error} -> Repo.rollback(error)
end
end
@spec follow(User.t(), User.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t()} | {:error, any()}
def follow(follower, followed, activity_id \\ nil, local \\ true) do
@ -490,6 +398,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp do_follow(follower, followed, activity_id, local) do
with data <- make_follow_data(follower, followed, activity_id),
{:ok, activity} <- insert(data, local),
_ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
@ -511,6 +420,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
{:ok, follow_activity} <- update_follow_state(follow_activity, "cancelled"),
unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id),
{:ok, activity} <- insert(unfollow_data, local),
_ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
@ -519,67 +429,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
@spec delete(User.t() | Object.t(), keyword()) :: {:ok, User.t() | Object.t()} | {:error, any()}
def delete(entity, options \\ []) do
with {:ok, result} <- Repo.transaction(fn -> do_delete(entity, options) end) do
result
end
end
defp do_delete(%User{ap_id: ap_id, follower_address: follower_address} = user, _) do
with data <- %{
"to" => [follower_address],
"type" => "Delete",
"actor" => ap_id,
"object" => %{"type" => "Person", "id" => ap_id}
},
{:ok, activity} <- insert(data, true, true, true),
:ok <- maybe_federate(activity) do
{:ok, user}
end
end
defp do_delete(%Object{data: %{"id" => id, "actor" => actor}} = object, options) do
local = Keyword.get(options, :local, true)
activity_id = Keyword.get(options, :activity_id, nil)
actor = Keyword.get(options, :actor, actor)
user = User.get_cached_by_ap_id(actor)
to = (object.data["to"] || []) ++ (object.data["cc"] || [])
with create_activity <- Activity.get_create_by_object_ap_id(id),
data <-
%{
"type" => "Delete",
"actor" => actor,
"object" => id,
"to" => to,
"deleted_activity_id" => create_activity && create_activity.id
}
|> maybe_put("id", activity_id),
{:ok, activity} <- insert(data, local, false),
{:ok, object, _create_activity} <- Object.delete(object),
stream_out_participations(object, user),
_ <- decrease_replies_count_if_reply(object),
{:ok, _actor} <- decrease_note_count_if_public(user, object),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
{:error, error} ->
Repo.rollback(error)
end
end
defp do_delete(%Object{data: %{"type" => "Tombstone", "id" => ap_id}}, _) do
activity =
ap_id
|> Activity.Queries.by_object_id()
|> Activity.Queries.by_type("Delete")
|> Repo.one()
{:ok, activity}
end
@spec block(User.t(), User.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t()} | {:error, any()}
def block(blocker, blocked, activity_id \\ nil, local \\ true) do
@ -590,7 +439,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
defp do_block(blocker, blocked, activity_id, local) do
outgoing_blocks = Config.get([:activitypub, :outgoing_blocks])
unfollow_blocked = Config.get([:activitypub, :unfollow_blocked])
if unfollow_blocked do
@ -598,9 +446,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
if follow_activity, do: unfollow(blocker, blocked, nil, local)
end
with true <- outgoing_blocks,
block_data <- make_block_data(blocker, blocked, activity_id),
with block_data <- make_block_data(blocker, blocked, activity_id),
{:ok, activity} <- insert(block_data, local),
_ <- notify_and_stream(activity),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
@ -608,27 +456,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
@spec unblock(User.t(), User.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t()} | {:error, any()} | nil
def unblock(blocker, blocked, activity_id \\ nil, local \\ true) do
with {:ok, result} <-
Repo.transaction(fn -> do_unblock(blocker, blocked, activity_id, local) end) do
result
end
end
defp do_unblock(blocker, blocked, activity_id, local) do
with %Activity{} = block_activity <- fetch_latest_block(blocker, blocked),
unblock_data <- make_unblock_data(blocker, blocked, block_activity, activity_id),
{:ok, activity} <- insert(unblock_data, local),
:ok <- maybe_federate(activity) do
{:ok, activity}
else
nil -> nil
{:error, error} -> Repo.rollback(error)
end
end
@spec flag(map()) :: {:ok, Activity.t()} | {:error, any()}
def flag(
%{
@ -655,6 +482,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
with flag_data <- make_flag_data(params, additional),
{:ok, activity} <- insert(flag_data, local),
{:ok, stripped_activity} <- strip_report_status_data(activity),
_ <- notify_and_stream(activity),
:ok <- maybe_federate(stripped_activity) do
User.all_superusers()
|> Enum.filter(fn user -> not is_nil(user.email) end)
@ -678,7 +506,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
}
with true <- origin.ap_id in target.also_known_as,
{:ok, activity} <- insert(params, local) do
{:ok, activity} <- insert(params, local),
_ <- notify_and_stream(activity) do
maybe_federate(activity)
BackgroundWorker.enqueue("move_following", %{
@ -1530,21 +1359,34 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp normalize_counter(counter) when is_integer(counter), do: counter
defp normalize_counter(_), do: 0
defp maybe_update_follow_information(data) do
def maybe_update_follow_information(user_data) do
with {:enabled, true} <- {:enabled, Config.get([:instance, :external_user_synchronization])},
{:ok, info} <- fetch_follow_information_for_user(data) do
info = Map.merge(data[:info] || %{}, info)
Map.put(data, :info, info)
{_, true} <- {:user_type_check, user_data[:type] in ["Person", "Service"]},
{_, true} <-
{:collections_available,
!!(user_data[:following_address] && user_data[:follower_address])},
{:ok, info} <-
fetch_follow_information_for_user(user_data) do
info = Map.merge(user_data[:info] || %{}, info)
user_data
|> Map.put(:info, info)
else
{:user_type_check, false} ->
user_data
{:collections_available, false} ->
user_data
{:enabled, false} ->
data
user_data
e ->
Logger.error(
"Follower/Following counter update for #{data.ap_id} failed.\n" <> inspect(e)
"Follower/Following counter update for #{user_data.ap_id} failed.\n" <> inspect(e)
)
data
user_data
end
end

View file

@ -34,7 +34,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
plug(
EnsureAuthenticatedPlug,
[unless_func: &FederatingPlug.federating?/0] when action not in @federating_only_actions
[unless_func: &FederatingPlug.federating?/1] when action not in @federating_only_actions
)
# Note: :following and :followers must be served even without authentication (as via :api)
@ -396,7 +396,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
|> json(err)
end
defp handle_user_activity(%User{} = user, %{"type" => "Create"} = params) do
defp handle_user_activity(
%User{} = user,
%{"type" => "Create", "object" => %{"type" => "Note"}} = params
) do
object =
params["object"]
|> Map.merge(Map.take(params, ["to", "cc"]))
@ -415,7 +418,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
defp handle_user_activity(%User{} = user, %{"type" => "Delete"} = params) do
with %Object{} = object <- Object.normalize(params["object"]),
true <- user.is_moderator || user.ap_id == object.data["actor"],
{:ok, delete} <- ActivityPub.delete(object) do
{:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
{:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
{:ok, delete}
else
_ -> {:error, dgettext("errors", "Can't delete object")}

View file

@ -10,8 +10,81 @@ defmodule Pleroma.Web.ActivityPub.Builder do
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
@spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()}
def emoji_react(actor, object, emoji) do
with {:ok, data, meta} <- object_action(actor, object) do
data =
data
|> Map.put("content", emoji)
|> Map.put("type", "EmojiReact")
{:ok, data, meta}
end
end
@spec undo(User.t(), Activity.t()) :: {:ok, map(), keyword()}
def undo(actor, object) do
{:ok,
%{
"id" => Utils.generate_activity_id(),
"actor" => actor.ap_id,
"type" => "Undo",
"object" => object.data["id"],
"to" => object.data["to"] || [],
"cc" => object.data["cc"] || []
}, []}
end
@spec delete(User.t(), String.t()) :: {:ok, map(), keyword()}
def delete(actor, object_id) do
object = Object.normalize(object_id, false)
user = !object && User.get_cached_by_ap_id(object_id)
to =
case {object, user} do
{%Object{}, _} ->
# We are deleting an object, address everyone who was originally mentioned
(object.data["to"] || []) ++ (object.data["cc"] || [])
{_, %User{follower_address: follower_address}} ->
# We are deleting a user, address the followers of that user
[follower_address]
end
{:ok,
%{
"id" => Utils.generate_activity_id(),
"actor" => actor.ap_id,
"object" => object_id,
"to" => to,
"type" => "Delete"
}, []}
end
@spec tombstone(String.t(), String.t()) :: {:ok, map(), keyword()}
def tombstone(actor, id) do
{:ok,
%{
"id" => id,
"actor" => actor,
"type" => "Tombstone"
}, []}
end
@spec like(User.t(), Object.t()) :: {:ok, map(), keyword()}
def like(actor, object) do
with {:ok, data, meta} <- object_action(actor, object) do
data =
data
|> Map.put("type", "Like")
{:ok, data, meta}
end
end
@spec object_action(User.t(), Object.t()) :: {:ok, map(), keyword()}
defp object_action(actor, object) do
object_actor = User.get_cached_by_ap_id(object.data["actor"])
# Address the actor of the object, and our actor's follower collection if the post is public.
@ -33,7 +106,6 @@ defmodule Pleroma.Web.ActivityPub.Builder do
%{
"id" => Utils.generate_activity_id(),
"actor" => actor.ap_id,
"type" => "Like",
"object" => object.data["id"],
"to" => to,
"cc" => cc,

View file

@ -0,0 +1,97 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy do
require Logger
alias Pleroma.Config
@moduledoc "Detect new emojis by their shortcode and steals them"
@behaviour Pleroma.Web.ActivityPub.MRF
defp remote_host?(host), do: host != Config.get([Pleroma.Web.Endpoint, :url, :host])
defp accept_host?(host), do: host in Config.get([:mrf_steal_emoji, :hosts], [])
defp steal_emoji({shortcode, url}) do
url = Pleroma.Web.MediaProxy.url(url)
{:ok, response} = Pleroma.HTTP.get(url)
size_limit = Config.get([:mrf_steal_emoji, :size_limit], 50_000)
if byte_size(response.body) <= size_limit do
emoji_dir_path =
Config.get(
[:mrf_steal_emoji, :path],
Path.join(Config.get([:instance, :static_dir]), "emoji/stolen")
)
extension =
url
|> URI.parse()
|> Map.get(:path)
|> Path.basename()
|> Path.extname()
file_path = Path.join([emoji_dir_path, shortcode <> (extension || ".png")])
try do
:ok = File.write(file_path, response.body)
shortcode
rescue
e ->
Logger.warn("MRF.StealEmojiPolicy: Failed to write to #{file_path}: #{inspect(e)}")
nil
end
else
Logger.debug(
"MRF.StealEmojiPolicy: :#{shortcode}: at #{url} (#{byte_size(response.body)} B) over size limit (#{
size_limit
} B)"
)
nil
end
rescue
e ->
Logger.warn("MRF.StealEmojiPolicy: Failed to fetch #{url}: #{inspect(e)}")
nil
end
@impl true
def filter(%{"object" => %{"emoji" => foreign_emojis, "actor" => actor}} = message) do
host = URI.parse(actor).host
if remote_host?(host) and accept_host?(host) do
installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end)
new_emojis =
foreign_emojis
|> Enum.filter(fn {shortcode, _url} -> shortcode not in installed_emoji end)
|> Enum.filter(fn {shortcode, _url} ->
reject_emoji? =
Config.get([:mrf_steal_emoji, :rejected_shortcodes], [])
|> Enum.find(false, fn regex -> String.match?(shortcode, regex) end)
!reject_emoji?
end)
|> Enum.map(&steal_emoji(&1))
|> Enum.filter(& &1)
if !Enum.empty?(new_emojis) do
Logger.info("Stole new emojis: #{inspect(new_emojis)}")
Pleroma.Emoji.reload()
end
end
{:ok, message}
end
def filter(message), do: {:ok, message}
@impl true
def describe do
{:ok, %{}}
end
end

View file

@ -11,11 +11,35 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator
@spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
def validate(object, meta)
def validate(%{"type" => "Undo"} = object, meta) do
with {:ok, object} <-
object
|> UndoValidator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object)
{:ok, object, meta}
end
end
def validate(%{"type" => "Delete"} = object, meta) do
with cng <- DeleteValidator.cast_and_validate(object),
do_not_federate <- DeleteValidator.do_not_federate?(cng),
{:ok, object} <- Ecto.Changeset.apply_action(cng, :insert) do
object = stringify_keys(object)
meta = Keyword.put(meta, :do_not_federate, do_not_federate)
{:ok, object, meta}
end
end
def validate(%{"type" => "Like"} = object, meta) do
with {:ok, object} <-
object |> LikeValidator.cast_and_validate() |> Ecto.Changeset.apply_action(:insert) do
@ -24,13 +48,35 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
end
end
def validate(%{"type" => "EmojiReact"} = object, meta) do
with {:ok, object} <-
object
|> EmojiReactValidator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object |> Map.from_struct())
{:ok, object, meta}
end
end
def stringify_keys(%{__struct__: _} = object) do
object
|> Map.from_struct()
|> stringify_keys
end
def stringify_keys(object) do
object
|> Map.new(fn {key, val} -> {to_string(key), val} end)
end
def fetch_actor(object) do
with {:ok, actor} <- Types.ObjectID.cast(object["actor"]) do
User.get_or_fetch_by_ap_id(actor)
end
end
def fetch_actor_and_object(object) do
User.get_or_fetch_by_ap_id(object["actor"])
fetch_actor(object)
Object.normalize(object["object"])
:ok
end

View file

@ -5,10 +5,33 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
import Ecto.Changeset
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.User
def validate_actor_presence(cng, field_name \\ :actor) do
def validate_recipients_presence(cng, fields \\ [:to, :cc]) do
non_empty =
fields
|> Enum.map(fn field -> get_field(cng, field) end)
|> Enum.any?(fn
[] -> false
_ -> true
end)
if non_empty do
cng
else
fields
|> Enum.reduce(cng, fn field, cng ->
cng
|> add_error(field, "no recipients in any field")
end)
end
end
def validate_actor_presence(cng, options \\ []) do
field_name = Keyword.get(options, :field_name, :actor)
cng
|> validate_change(field_name, fn field_name, actor ->
if User.get_cached_by_ap_id(actor) do
@ -19,14 +42,39 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
end)
end
def validate_object_presence(cng, field_name \\ :object) do
def validate_object_presence(cng, options \\ []) do
field_name = Keyword.get(options, :field_name, :object)
allowed_types = Keyword.get(options, :allowed_types, false)
cng
|> validate_change(field_name, fn field_name, object ->
if Object.get_cached_by_ap_id(object) do
[]
else
|> validate_change(field_name, fn field_name, object_id ->
object = Object.get_cached_by_ap_id(object_id) || Activity.get_by_ap_id(object_id)
cond do
!object ->
[{field_name, "can't find object"}]
object && allowed_types && object.data["type"] not in allowed_types ->
[{field_name, "object not in allowed types"}]
true ->
[]
end
end)
end
def validate_object_or_user_presence(cng, options \\ []) do
field_name = Keyword.get(options, :field_name, :object)
options = Keyword.put(options, :field_name, field_name)
actor_cng =
cng
|> validate_actor_presence(options)
object_cng =
cng
|> validate_object_presence(options)
if actor_cng.valid?, do: actor_cng, else: object_cng
end
end

View file

@ -0,0 +1,100 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do
use Ecto.Schema
alias Pleroma.Activity
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
import Ecto.Changeset
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
@primary_key false
embedded_schema do
field(:id, Types.ObjectID, primary_key: true)
field(:type, :string)
field(:actor, Types.ObjectID)
field(:to, Types.Recipients, default: [])
field(:cc, Types.Recipients, default: [])
field(:deleted_activity_id, Types.ObjectID)
field(:object, Types.ObjectID)
end
def cast_data(data) do
%__MODULE__{}
|> cast(data, __schema__(:fields))
end
def add_deleted_activity_id(cng) do
object =
cng
|> get_field(:object)
with %Activity{id: id} <- Activity.get_create_by_object_ap_id(object) do
cng
|> put_change(:deleted_activity_id, id)
else
_ -> cng
end
end
@deletable_types ~w{
Answer
Article
Audio
Event
Note
Page
Question
Video
Tombstone
}
def validate_data(cng) do
cng
|> validate_required([:id, :type, :actor, :to, :cc, :object])
|> validate_inclusion(:type, ["Delete"])
|> validate_actor_presence()
|> validate_deletion_rights()
|> validate_object_or_user_presence(allowed_types: @deletable_types)
|> add_deleted_activity_id()
end
def do_not_federate?(cng) do
!same_domain?(cng)
end
defp same_domain?(cng) do
actor_uri =
cng
|> get_field(:actor)
|> URI.parse()
object_uri =
cng
|> get_field(:object)
|> URI.parse()
object_uri.host == actor_uri.host
end
def validate_deletion_rights(cng) do
actor = User.get_cached_by_ap_id(get_field(cng, :actor))
if User.superuser?(actor) || same_domain?(cng) do
cng
else
cng
|> add_error(:actor, "is not allowed to delete object")
end
end
def cast_and_validate(data) do
data
|> cast_data
|> validate_data
end
end

View file

@ -0,0 +1,81 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
use Ecto.Schema
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
import Ecto.Changeset
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
@primary_key false
embedded_schema do
field(:id, Types.ObjectID, primary_key: true)
field(:type, :string)
field(:object, Types.ObjectID)
field(:actor, Types.ObjectID)
field(:context, :string)
field(:content, :string)
field(:to, {:array, :string}, default: [])
field(:cc, {:array, :string}, default: [])
end
def cast_and_validate(data) do
data
|> cast_data()
|> validate_data()
end
def cast_data(data) do
%__MODULE__{}
|> changeset(data)
end
def changeset(struct, data) do
struct
|> cast(data, __schema__(:fields))
|> fix_after_cast()
end
def fix_after_cast(cng) do
cng
|> fix_context()
end
def fix_context(cng) do
object = get_field(cng, :object)
with nil <- get_field(cng, :context),
%Object{data: %{"context" => context}} <- Object.get_cached_by_ap_id(object) do
cng
|> put_change(:context, context)
else
_ ->
cng
end
end
def validate_emoji(cng) do
content = get_field(cng, :content)
if Pleroma.Emoji.is_unicode_emoji?(content) do
cng
else
cng
|> add_error(:content, "must be a single character emoji")
end
end
def validate_data(data_cng) do
data_cng
|> validate_inclusion(:type, ["EmojiReact"])
|> validate_required([:id, :type, :object, :actor, :context, :to, :cc, :content])
|> validate_actor_presence()
|> validate_object_presence()
|> validate_emoji()
end
end

View file

@ -5,6 +5,7 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do
use Ecto.Schema
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
alias Pleroma.Web.ActivityPub.Utils
@ -19,8 +20,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do
field(:object, Types.ObjectID)
field(:actor, Types.ObjectID)
field(:context, :string)
field(:to, {:array, :string})
field(:cc, {:array, :string})
field(:to, Types.Recipients, default: [])
field(:cc, Types.Recipients, default: [])
end
def cast_and_validate(data) do
@ -31,7 +32,48 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do
def cast_data(data) do
%__MODULE__{}
|> cast(data, [:id, :type, :object, :actor, :context, :to, :cc])
|> changeset(data)
end
def changeset(struct, data) do
struct
|> cast(data, __schema__(:fields))
|> fix_after_cast()
end
def fix_after_cast(cng) do
cng
|> fix_recipients()
|> fix_context()
end
def fix_context(cng) do
object = get_field(cng, :object)
with nil <- get_field(cng, :context),
%Object{data: %{"context" => context}} <- Object.get_cached_by_ap_id(object) do
cng
|> put_change(:context, context)
else
_ ->
cng
end
end
def fix_recipients(cng) do
to = get_field(cng, :to)
cc = get_field(cng, :cc)
object = get_field(cng, :object)
with {[], []} <- {to, cc},
%Object{data: %{"actor" => actor}} <- Object.get_cached_by_ap_id(object),
{:ok, actor} <- Types.ObjectID.cast(actor) do
cng
|> put_change(:to, [actor])
else
_ ->
cng
end
end
def validate_data(data_cng) do

View file

@ -0,0 +1,34 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.Recipients do
use Ecto.Type
alias Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID
def type, do: {:array, ObjectID}
def cast(object) when is_binary(object) do
cast([object])
end
def cast(data) when is_list(data) do
data
|> Enum.reduce({:ok, []}, fn element, acc ->
case {acc, ObjectID.cast(element)} do
{:error, _} -> :error
{_, :error} -> :error
{{:ok, list}, {:ok, id}} -> {:ok, [id | list]}
end
end)
end
def cast(_) do
:error
end
def dump(data) do
{:ok, data}
end
def load(data) do
{:ok, data}
end
end

View file

@ -0,0 +1,62 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator do
use Ecto.Schema
alias Pleroma.Activity
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
import Ecto.Changeset
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
@primary_key false
embedded_schema do
field(:id, Types.ObjectID, primary_key: true)
field(:type, :string)
field(:object, Types.ObjectID)
field(:actor, Types.ObjectID)
field(:to, {:array, :string}, default: [])
field(:cc, {:array, :string}, default: [])
end
def cast_and_validate(data) do
data
|> cast_data()
|> validate_data()
end
def cast_data(data) do
%__MODULE__{}
|> changeset(data)
end
def changeset(struct, data) do
struct
|> cast(data, __schema__(:fields))
end
def validate_data(data_cng) do
data_cng
|> validate_inclusion(:type, ["Undo"])
|> validate_required([:id, :type, :object, :actor, :to, :cc])
|> validate_actor_presence()
|> validate_object_presence()
|> validate_undo_rights()
end
def validate_undo_rights(cng) do
actor = get_field(cng, :actor)
object = get_field(cng, :object)
with %Activity{data: %{"actor" => object_actor}} <- Activity.get_by_ap_id(object),
true <- object_actor != actor do
cng
|> add_error(:actor, "not the same as object actor")
else
_ -> cng
end
end
end

View file

@ -4,20 +4,33 @@
defmodule Pleroma.Web.ActivityPub.Pipeline do
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.ObjectValidator
alias Pleroma.Web.ActivityPub.SideEffects
alias Pleroma.Web.Federator
@spec common_pipeline(map(), keyword()) :: {:ok, Activity.t(), keyword()} | {:error, any()}
@spec common_pipeline(map(), keyword()) ::
{:ok, Activity.t() | Object.t(), keyword()} | {:error, any()}
def common_pipeline(object, meta) do
case Repo.transaction(fn -> do_common_pipeline(object, meta) end) do
{:ok, value} ->
value
{:error, e} ->
{:error, e}
end
end
def do_common_pipeline(object, meta) do
with {_, {:ok, validated_object, meta}} <-
{:validate_object, ObjectValidator.validate(object, meta)},
{_, {:ok, mrfd_object}} <- {:mrf_object, MRF.filter(validated_object)},
{_, {:ok, %Activity{} = activity, meta}} <-
{_, {:ok, activity, meta}} <-
{:persist_object, ActivityPub.persist(mrfd_object, meta)},
{_, {:ok, %Activity{} = activity, meta}} <-
{_, {:ok, activity, meta}} <-
{:execute_side_effects, SideEffects.handle(activity, meta)},
{_, {:ok, _}} <- {:federation, maybe_federate(activity, meta)} do
{:ok, activity, meta}
@ -27,9 +40,13 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do
end
end
defp maybe_federate(activity, meta) do
defp maybe_federate(%Object{}, _), do: {:ok, :not_federated}
defp maybe_federate(%Activity{} = activity, meta) do
with {:ok, local} <- Keyword.fetch(meta, :local) do
if local do
do_not_federate = meta[:do_not_federate]
if !do_not_federate && local do
Federator.publish(activity)
{:ok, :federated}
else

View file

@ -5,8 +5,12 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
liked object, a `Follow` activity will add the user to the follower
collection, and so on.
"""
alias Pleroma.Activity
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Utils
def handle(object, meta \\ [])
@ -15,21 +19,115 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
# - Add like to object
# - Set up notification
def handle(%{data: %{"type" => "Like"}} = object, meta) do
{:ok, result} =
Pleroma.Repo.transaction(fn ->
liked_object = Object.get_by_ap_id(object.data["object"])
Utils.add_like_to_object(object, liked_object)
Notification.create_notifications(object)
{:ok, object, meta}
end)
end
result
def handle(%{data: %{"type" => "Undo", "object" => undone_object}} = object, meta) do
with undone_object <- Activity.get_by_ap_id(undone_object),
:ok <- handle_undoing(undone_object) do
{:ok, object, meta}
end
end
# Tasks this handles:
# - Add reaction to object
# - Set up notification
def handle(%{data: %{"type" => "EmojiReact"}} = object, meta) do
reacted_object = Object.get_by_ap_id(object.data["object"])
Utils.add_emoji_reaction_to_object(object, reacted_object)
Notification.create_notifications(object)
{:ok, object, meta}
end
# Tasks this handles:
# - Delete and unpins the create activity
# - Replace object with Tombstone
# - Set up notification
# - Reduce the user note count
# - Reduce the reply count
# - Stream out the activity
def handle(%{data: %{"type" => "Delete", "object" => deleted_object}} = object, meta) do
deleted_object =
Object.normalize(deleted_object, false) || User.get_cached_by_ap_id(deleted_object)
result =
case deleted_object do
%Object{} ->
with {:ok, deleted_object, activity} <- Object.delete(deleted_object),
%User{} = user <- User.get_cached_by_ap_id(deleted_object.data["actor"]) do
User.remove_pinnned_activity(user, activity)
{:ok, user} = ActivityPub.decrease_note_count_if_public(user, deleted_object)
if in_reply_to = deleted_object.data["inReplyTo"] do
Object.decrease_replies_count(in_reply_to)
end
ActivityPub.stream_out(object)
ActivityPub.stream_out_participations(deleted_object, user)
:ok
end
%User{} ->
with {:ok, _} <- User.delete(deleted_object) do
:ok
end
end
if result == :ok do
Notification.create_notifications(object)
{:ok, object, meta}
else
{:error, result}
end
end
# Nothing to do
def handle(object, meta) do
{:ok, object, meta}
end
def handle_undoing(%{data: %{"type" => "Like"}} = object) do
with %Object{} = liked_object <- Object.get_by_ap_id(object.data["object"]),
{:ok, _} <- Utils.remove_like_from_object(object, liked_object),
{:ok, _} <- Repo.delete(object) do
:ok
end
end
def handle_undoing(%{data: %{"type" => "EmojiReact"}} = object) do
with %Object{} = reacted_object <- Object.get_by_ap_id(object.data["object"]),
{:ok, _} <- Utils.remove_emoji_reaction_from_object(object, reacted_object),
{:ok, _} <- Repo.delete(object) do
:ok
end
end
def handle_undoing(%{data: %{"type" => "Announce"}} = object) do
with %Object{} = liked_object <- Object.get_by_ap_id(object.data["object"]),
{:ok, _} <- Utils.remove_announce_from_object(object, liked_object),
{:ok, _} <- Repo.delete(object) do
:ok
end
end
def handle_undoing(
%{data: %{"type" => "Block", "actor" => blocker, "object" => blocked}} = object
) do
with %User{} = blocker <- User.get_cached_by_ap_id(blocker),
%User{} = blocked <- User.get_cached_by_ap_id(blocked),
{:ok, _} <- User.unblock(blocker, blocked),
{:ok, _} <- Repo.delete(object) do
:ok
end
end
def handle_undoing(object), do: {:error, ["don't know how to handle", object]}
end

View file

@ -14,8 +14,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.ObjectValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
@ -591,6 +592,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept) do
User.update_follower_count(followed)
User.update_following_count(follower)
ActivityPub.accept(%{
to: follow_activity.data["to"],
type: "Accept",
@ -600,7 +604,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
activity_id: id
})
else
_e -> :error
_e ->
:error
end
end
@ -657,44 +662,16 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> handle_incoming(options)
end
def handle_incoming(%{"type" => "Like"} = data, _options) do
with {_, {:ok, cast_data_sym}} <-
{:casting_data,
data |> LikeValidator.cast_data() |> Ecto.Changeset.apply_action(:insert)},
cast_data = ObjectValidator.stringify_keys(Map.from_struct(cast_data_sym)),
:ok <- ObjectValidator.fetch_actor_and_object(cast_data),
{_, {:ok, cast_data}} <- {:ensure_context_presence, ensure_context_presence(cast_data)},
{_, {:ok, cast_data}} <-
{:ensure_recipients_presence, ensure_recipients_presence(cast_data)},
{_, {:ok, activity, _meta}} <-
{:common_pipeline, Pipeline.common_pipeline(cast_data, local: false)} do
def handle_incoming(%{"type" => type} = data, _options) when type in ["Like", "EmojiReact"] do
with :ok <- ObjectValidator.fetch_actor_and_object(data),
{:ok, activity, _meta} <-
Pipeline.common_pipeline(data, local: false) do
{:ok, activity}
else
e -> {:error, e}
end
end
def handle_incoming(
%{
"type" => "EmojiReact",
"object" => object_id,
"actor" => _actor,
"id" => id,
"content" => emoji
} = data,
_options
) do
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id),
{:ok, activity, _object} <-
ActivityPub.react_with_emoji(actor, object, emoji, activity_id: id, local: false) do
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
_options
@ -743,55 +720,25 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
end
# TODO: We presently assume that any actor on the same origin domain as the object being
# deleted has the rights to delete that object. A better way to validate whether or not
# the object should be deleted is to refetch the object URI, which should return either
# an error or a tombstone. This would allow us to verify that a deletion actually took
# place.
def handle_incoming(
%{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => id} = data,
%{"type" => "Delete"} = data,
_options
) do
object_id = Utils.get_ap_id(object_id)
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id),
:ok <- Containment.contain_origin(actor.ap_id, object.data),
{:ok, activity} <-
ActivityPub.delete(object, local: false, activity_id: id, actor: actor.ap_id) do
with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
{:ok, activity}
else
nil ->
case User.get_cached_by_ap_id(object_id) do
%User{ap_id: ^actor} = user ->
User.delete(user)
nil ->
:error
end
_e ->
:error
end
end
def handle_incoming(
%{
"type" => "Undo",
"object" => %{"type" => "Announce", "object" => object_id},
"actor" => _actor,
"id" => id
} = data,
_options
) do
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id),
{:ok, activity, _} <- ActivityPub.unannounce(actor, object, id, false) do
{:ok, activity}
{:error, {:validate_object, _}} = e ->
# Check if we have a create activity for this
with {:ok, object_id} <- Types.ObjectID.cast(data["object"]),
%Activity{data: %{"actor" => actor}} <-
Activity.create_by_object_ap_id(object_id) |> Repo.one(),
# We have one, insert a tombstone and retry
{:ok, tombstone_data, _} <- Builder.tombstone(actor, object_id),
{:ok, _tombstone} <- Object.create(tombstone_data) do
handle_incoming(data)
else
_e -> :error
_ -> e
end
end
end
@ -817,75 +764,13 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def handle_incoming(
%{
"type" => "Undo",
"object" => %{"type" => "EmojiReact", "id" => reaction_activity_id},
"actor" => _actor,
"id" => id
"object" => %{"type" => type}
} = data,
_options
) do
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, activity, _} <-
ActivityPub.unreact_with_emoji(actor, reaction_activity_id,
activity_id: id,
local: false
) do
)
when type in ["Like", "EmojiReact", "Announce", "Block"] do
with {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{
"type" => "Undo",
"object" => %{"type" => "Block", "object" => blocked},
"actor" => blocker,
"id" => id
} = _data,
_options
) do
with %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked),
{:ok, %User{} = blocker} <- User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.unblock(blocker, blocked, id, false) do
User.unblock(blocker, blocked)
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
_options
) do
with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
{:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
User.unfollow(blocker, blocked)
User.block(blocker, blocked)
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{
"type" => "Undo",
"object" => %{"type" => "Like", "object" => object_id},
"actor" => _actor,
"id" => id
} = data,
_options
) do
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id),
{:ok, activity, _, _} <- ActivityPub.unlike(actor, object, id, false) do
{:ok, activity}
else
_e -> :error
end
end
@ -907,6 +792,21 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
end
def handle_incoming(
%{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data,
_options
) do
with %User{local: true} = blocked = User.get_cached_by_ap_id(blocked),
{:ok, %User{} = blocker} = User.get_or_fetch_by_ap_id(blocker),
{:ok, activity} <- ActivityPub.block(blocker, blocked, id, false) do
User.unfollow(blocker, blocked)
User.block(blocker, blocked)
{:ok, activity}
else
_e -> :error
end
end
def handle_incoming(
%{
"type" => "Move",
@ -1203,6 +1103,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
Map.put(object, "conversation", object["context"])
end
def set_sensitive(%{"sensitive" => true} = object) do
object
end
def set_sensitive(object) do
tags = object["tag"] || []
Map.put(object, "sensitive", "nsfw" in tags)
@ -1296,45 +1200,4 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def maybe_fix_user_url(data), do: data
def maybe_fix_user_object(data), do: maybe_fix_user_url(data)
defp ensure_context_presence(%{"context" => context} = data) when is_binary(context),
do: {:ok, data}
defp ensure_context_presence(%{"object" => object} = data) when is_binary(object) do
with %{data: %{"context" => context}} when is_binary(context) <- Object.normalize(object) do
{:ok, Map.put(data, "context", context)}
else
_ ->
{:error, :no_context}
end
end
defp ensure_context_presence(_) do
{:error, :no_context}
end
defp ensure_recipients_presence(%{"to" => [_ | _], "cc" => [_ | _]} = data),
do: {:ok, data}
defp ensure_recipients_presence(%{"object" => object} = data) do
case Object.normalize(object) do
%{data: %{"actor" => actor}} ->
data =
data
|> Map.put("to", [actor])
|> Map.put("cc", data["cc"] || [])
{:ok, data}
nil ->
{:error, :no_object}
_ ->
{:error, :no_actor}
end
end
defp ensure_recipients_presence(_) do
{:error, :no_object}
end
end

View file

@ -6,6 +6,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
alias Ecto.Changeset
alias Ecto.UUID
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
@ -169,8 +170,11 @@ defmodule Pleroma.Web.ActivityPub.Utils do
Enqueues an activity for federation if it's local
"""
@spec maybe_federate(any()) :: :ok
def maybe_federate(%Activity{local: true} = activity) do
if Pleroma.Config.get!([:instance, :federating]) do
def maybe_federate(%Activity{local: true, data: %{"type" => type}} = activity) do
outgoing_blocks = Config.get([:activitypub, :outgoing_blocks])
with true <- Config.get!([:instance, :federating]),
true <- type != "Block" || outgoing_blocks do
Pleroma.Web.Federator.publish(activity)
end
@ -512,7 +516,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
#### Announce-related helpers
@doc """
Retruns an existing announce activity if the notice has already been announced
Returns an existing announce activity if the notice has already been announced
"""
@spec get_existing_announce(String.t(), map()) :: Activity.t() | nil
def get_existing_announce(actor, %{data: %{"id" => ap_id}}) do
@ -562,45 +566,6 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|> maybe_put("id", activity_id)
end
@doc """
Make unannounce activity data for the given actor and object
"""
def make_unannounce_data(
%User{ap_id: ap_id} = user,
%Activity{data: %{"context" => context, "object" => object}} = activity,
activity_id
) do
object = Object.normalize(object)
%{
"type" => "Undo",
"actor" => ap_id,
"object" => activity.data,
"to" => [user.follower_address, object.data["actor"]],
"cc" => [Pleroma.Constants.as_public()],
"context" => context
}
|> maybe_put("id", activity_id)
end
def make_unlike_data(
%User{ap_id: ap_id} = user,
%Activity{data: %{"context" => context, "object" => object}} = activity,
activity_id
) do
object = Object.normalize(object)
%{
"type" => "Undo",
"actor" => ap_id,
"object" => activity.data,
"to" => [user.follower_address, object.data["actor"]],
"cc" => [Pleroma.Constants.as_public()],
"context" => context
}
|> maybe_put("id", activity_id)
end
def make_undo_data(
%User{ap_id: actor, follower_address: follower_address},
%Activity{
@ -688,16 +653,6 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|> maybe_put("id", activity_id)
end
def make_unblock_data(blocker, blocked, block_activity, activity_id) do
%{
"type" => "Undo",
"actor" => blocker.ap_id,
"to" => [blocked.ap_id],
"object" => block_activity.data
}
|> maybe_put("id", activity_id)
end
#### Create-related helpers
def make_create_data(params, additional) do

View file

@ -10,6 +10,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.ConfigDB
alias Pleroma.MFA
alias Pleroma.ModerationLog
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.ReportNote
@ -17,8 +18,11 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.User
alias Pleroma.UserInviteToken
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.AdminAPI
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.AdminAPI.ConfigView
alias Pleroma.Web.AdminAPI.ModerationLogView
@ -27,14 +31,14 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.Web.AdminAPI.Search
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Endpoint
alias Pleroma.Web.MastodonAPI
alias Pleroma.Web.MastodonAPI.AppView
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.Router
require Logger
@descriptions_json Pleroma.Docs.JSON.compile()
@descriptions Pleroma.Docs.JSON.compile()
@users_page_size 50
plug(
@ -59,6 +63,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
:right_add,
:right_add_multiple,
:right_delete,
:disable_mfa,
:right_delete_multiple,
:update_user_credentials
]
@ -93,7 +98,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
plug(
OAuthScopesPlug,
%{scopes: ["read:statuses"], admin: true}
when action in [:list_statuses, :list_user_statuses, :list_instance_statuses]
when action in [:list_statuses, :list_user_statuses, :list_instance_statuses, :status_show]
)
plug(
@ -133,23 +138,20 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
action_fallback(:errors)
def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
user = User.get_cached_by_nickname(nickname)
User.delete(user)
ModerationLog.insert_log(%{
actor: admin,
subject: [user],
action: "delete"
})
conn
|> json(nickname)
def user_delete(conn, %{"nickname" => nickname}) do
user_delete(conn, %{"nicknames" => [nickname]})
end
def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
User.delete(users)
users =
nicknames
|> Enum.map(&User.get_cached_by_nickname/1)
users
|> Enum.each(fn user ->
{:ok, delete_data, _} = Builder.delete(admin, user.ap_id)
Pipeline.common_pipeline(delete_data, local: true)
end)
ModerationLog.insert_log(%{
actor: admin,
@ -279,8 +281,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
})
conn
|> put_view(Pleroma.Web.AdminAPI.StatusView)
|> render("index.json", %{activities: activities, as: :activity, skip_relationships: false})
|> put_view(AdminAPI.StatusView)
|> render("index.json", %{activities: activities, as: :activity})
end
def list_user_statuses(conn, %{"nickname" => nickname} = params) do
@ -298,8 +300,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
})
conn
|> put_view(StatusView)
|> render("index.json", %{activities: activities, as: :activity, skip_relationships: false})
|> put_view(MastodonAPI.StatusView)
|> render("index.json", %{activities: activities, as: :activity})
else
_ -> {:error, :not_found}
end
@ -392,29 +394,12 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
email: params["email"]
}
with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)),
{:ok, users, count} <- filter_service_users(users, count),
do:
conn
|> json(
AccountView.render("index.json",
users: users,
count: count,
page_size: page_size
)
with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)) do
json(
conn,
AccountView.render("index.json", users: users, count: count, page_size: page_size)
)
end
defp filter_service_users(users, count) do
filtered_users = Enum.reject(users, &service_user?/1)
count = if Enum.any?(users, &service_user?/1), do: length(filtered_users), else: count
{:ok, filtered_users, count}
end
defp service_user?(user) do
String.match?(user.ap_id, ~r/.*\/relay$/) or
String.match?(user.ap_id, ~r/.*\/internal\/fetch$/)
end
@filters ~w(local external active deactivated is_admin is_moderator)
@ -692,6 +677,18 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
json_response(conn, :no_content, "")
end
@doc "Disable mfa for user's account."
def disable_mfa(conn, %{"nickname" => nickname}) do
case User.get_by_nickname(nickname) do
%User{} = user ->
MFA.disable(user)
json(conn, nickname)
_ ->
{:error, :not_found}
end
end
@doc "Show a given user's credentials"
def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
@ -833,24 +830,39 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
})
conn
|> put_view(Pleroma.Web.AdminAPI.StatusView)
|> render("index.json", %{activities: activities, as: :activity, skip_relationships: false})
|> put_view(AdminAPI.StatusView)
|> render("index.json", %{activities: activities, as: :activity})
end
def status_show(conn, %{"id" => id}) do
with %Activity{} = activity <- Activity.get_by_id(id) do
conn
|> put_view(MastodonAPI.StatusView)
|> render("show.json", %{activity: activity})
else
_ -> errors(conn, {:error, :not_found})
end
end
def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do
params =
params
|> Map.take(["sensitive", "visibility"])
|> Map.new(fn {key, value} -> {String.to_existing_atom(key), value} end)
with {:ok, activity} <- CommonAPI.update_activity_scope(id, params) do
{:ok, sensitive} = Ecto.Type.cast(:boolean, params["sensitive"])
{:ok, sensitive} = Ecto.Type.cast(:boolean, params[:sensitive])
ModerationLog.insert_log(%{
action: "status_update",
actor: admin,
subject: activity,
sensitive: sensitive,
visibility: params["visibility"]
visibility: params[:visibility]
})
conn
|> put_view(StatusView)
|> put_view(MastodonAPI.StatusView)
|> render("show.json", %{activity: activity})
end
end
@ -886,9 +898,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end
def config_descriptions(conn, _params) do
conn
|> Plug.Conn.put_resp_content_type("application/json")
|> Plug.Conn.send_resp(200, @descriptions_json)
descriptions = Enum.filter(@descriptions, &whitelisted_config?/1)
json(conn, descriptions)
end
def config_show(conn, %{"only_db" => true}) do
@ -943,7 +955,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
def config_update(conn, %{"configs" => configs}) do
with :ok <- configurable_from_database(conn) do
{_errors, results} =
Enum.map(configs, fn
configs
|> Enum.filter(&whitelisted_config?/1)
|> Enum.map(fn
%{"group" => group, "key" => key, "delete" => true} = params ->
ConfigDB.delete(%{group: group, key: key, subkeys: params["subkeys"]})
@ -1005,6 +1019,28 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
end
end
defp whitelisted_config?(group, key) do
if whitelisted_configs = Config.get(:database_config_whitelist) do
Enum.any?(whitelisted_configs, fn
{whitelisted_group} ->
group == inspect(whitelisted_group)
{whitelisted_group, whitelisted_key} ->
group == inspect(whitelisted_group) && key == inspect(whitelisted_key)
end)
else
true
end
end
defp whitelisted_config?(%{"group" => group, "key" => key}) do
whitelisted_config?(group, key)
end
defp whitelisted_config?(%{:group => group} = config) do
whitelisted_config?(group, config[:key])
end
def reload_emoji(conn, _params) do
Pleroma.Emoji.reload()

View file

@ -21,6 +21,7 @@ defmodule Pleroma.Web.AdminAPI.Search do
query =
params
|> Map.drop([:page, :page_size])
|> Map.put(:exclude_service_users, true)
|> User.Query.build()
|> order_by([u], u.nickname)

View file

@ -6,7 +6,9 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
use Pleroma.Web, :view
alias Pleroma.User
alias Pleroma.Web.AdminAPI
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.MastodonAPI
alias Pleroma.Web.MediaProxy
def render("index.json", %{users: users, count: count, page_size: page_size}) do
@ -119,6 +121,13 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
}
end
def merge_account_views(%User{} = user) do
MastodonAPI.AccountView.render("show.json", %{user: user})
|> Map.merge(AdminAPI.AccountView.render("show.json", %{user: user}))
end
def merge_account_views(_), do: %{}
defp parse_error([]), do: ""
defp parse_error(errors) do

View file

@ -7,10 +7,13 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
alias Pleroma.HTML
alias Pleroma.User
alias Pleroma.Web.AdminAPI
alias Pleroma.Web.AdminAPI.Report
alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.StatusView
defdelegate merge_account_views(user), to: AdminAPI.AccountView
def render("index.json", %{reports: reports}) do
%{
reports:
@ -41,8 +44,7 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
statuses:
StatusView.render("index.json", %{
activities: statuses,
as: :activity,
skip_relationships: false
as: :activity
}),
state: report.data["state"],
notes: render(__MODULE__, "index_notes.json", %{notes: report.report_notes})
@ -70,11 +72,4 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
created_at: Utils.to_masto_date(inserted_at)
}
end
defp merge_account_views(%User{} = user) do
Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user})
|> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user}))
end
defp merge_account_views(_), do: %{}
end

View file

@ -7,24 +7,19 @@ defmodule Pleroma.Web.AdminAPI.StatusView do
require Pleroma.Constants
alias Pleroma.User
alias Pleroma.Web.MastodonAPI.StatusView
alias Pleroma.Web.AdminAPI
alias Pleroma.Web.MastodonAPI
defdelegate merge_account_views(user), to: AdminAPI.AccountView
def render("index.json", opts) do
safe_render_many(opts.activities, __MODULE__, "show.json", opts)
end
def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
user = StatusView.get_user(activity.data["actor"])
user = MastodonAPI.StatusView.get_user(activity.data["actor"])
StatusView.render("show.json", opts)
MastodonAPI.StatusView.render("show.json", opts)
|> Map.merge(%{account: merge_account_views(user)})
end
defp merge_account_views(%User{} = user) do
Pleroma.Web.MastodonAPI.AccountView.render("show.json", %{user: user})
|> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user}))
end
defp merge_account_views(_), do: %{}
end

View file

@ -39,7 +39,12 @@ defmodule Pleroma.Web.ApiSpec do
password: %OpenApiSpex.OAuthFlow{
authorizationUrl: "/oauth/authorize",
tokenUrl: "/oauth/token",
scopes: %{"read" => "read", "write" => "write", "follow" => "follow"}
scopes: %{
"read" => "read",
"write" => "write",
"follow" => "follow",
"push" => "push"
}
}
}
}

View file

@ -0,0 +1,139 @@
# Pleroma: A lightweight social networking server
# Copyright © 2019-2020 Moxley Stratton, Mike Buhot <https://github.com/open-api-spex/open_api_spex>, MPL-2.0
# Copyright © 2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.CastAndValidate do
@moduledoc """
This plug is based on [`OpenApiSpex.Plug.CastAndValidate`]
(https://github.com/open-api-spex/open_api_spex/blob/master/lib/open_api_spex/plug/cast_and_validate.ex).
The main difference is ignoring unexpected query params instead of throwing
an error and a config option (`[Pleroma.Web.ApiSpec.CastAndValidate, :strict]`)
to disable this behavior. Also, the default rendering error module
is `Pleroma.Web.ApiSpec.RenderError`.
"""
@behaviour Plug
alias Plug.Conn
@impl Plug
def init(opts) do
opts
|> Map.new()
|> Map.put_new(:render_error, Pleroma.Web.ApiSpec.RenderError)
end
@impl Plug
def call(%{private: %{open_api_spex: private_data}} = conn, %{
operation_id: operation_id,
render_error: render_error
}) do
spec = private_data.spec
operation = private_data.operation_lookup[operation_id]
content_type =
case Conn.get_req_header(conn, "content-type") do
[header_value | _] ->
header_value
|> String.split(";")
|> List.first()
_ ->
nil
end
private_data = Map.put(private_data, :operation_id, operation_id)
conn = Conn.put_private(conn, :open_api_spex, private_data)
case cast_and_validate(spec, operation, conn, content_type, strict?()) do
{:ok, conn} ->
conn
{:error, reason} ->
opts = render_error.init(reason)
conn
|> render_error.call(opts)
|> Plug.Conn.halt()
end
end
def call(
%{
private: %{
phoenix_controller: controller,
phoenix_action: action,
open_api_spex: private_data
}
} = conn,
opts
) do
operation =
case private_data.operation_lookup[{controller, action}] do
nil ->
operation_id = controller.open_api_operation(action).operationId
operation = private_data.operation_lookup[operation_id]
operation_lookup =
private_data.operation_lookup
|> Map.put({controller, action}, operation)
OpenApiSpex.Plug.Cache.adapter().put(
private_data.spec_module,
{private_data.spec, operation_lookup}
)
operation
operation ->
operation
end
if operation.operationId do
call(conn, Map.put(opts, :operation_id, operation.operationId))
else
raise "operationId was not found in action API spec"
end
end
def call(conn, opts), do: OpenApiSpex.Plug.CastAndValidate.call(conn, opts)
defp cast_and_validate(spec, operation, conn, content_type, true = _strict) do
OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
end
defp cast_and_validate(spec, operation, conn, content_type, false = _strict) do
case OpenApiSpex.cast_and_validate(spec, operation, conn, content_type) do
{:ok, conn} ->
{:ok, conn}
# Remove unexpected query params and cast/validate again
{:error, errors} ->
query_params =
Enum.reduce(errors, conn.query_params, fn
%{reason: :unexpected_field, name: name, path: [name]}, params ->
Map.delete(params, name)
%{reason: :invalid_enum, name: nil, path: path, value: value}, params ->
path = path |> Enum.reverse() |> tl() |> Enum.reverse() |> list_items_to_string()
update_in(params, path, &List.delete(&1, value))
_, params ->
params
end)
conn = %Conn{conn | query_params: query_params}
OpenApiSpex.cast_and_validate(spec, operation, conn, content_type)
end
end
defp list_items_to_string(list) do
Enum.map(list, fn
i when is_atom(i) -> to_string(i)
i -> i
end)
end
defp strict?, do: Pleroma.Config.get([__MODULE__, :strict], false)
end

View file

@ -5,6 +5,7 @@
defmodule Pleroma.Web.ApiSpec.Helpers do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
def request_body(description, schema_ref, opts \\ []) do
media_types = ["application/json", "multipart/form-data", "application/x-www-form-urlencoded"]
@ -47,6 +48,15 @@ defmodule Pleroma.Web.ApiSpec.Helpers do
]
end
def with_relationships_param do
Operation.parameter(
:with_relationships,
:query,
BooleanLike,
"Embed relationships into accounts."
)
end
def empty_object_response do
Operation.response("Empty object", "application/json", %Schema{type: :object, example: %{}})
end
@ -54,4 +64,8 @@ defmodule Pleroma.Web.ApiSpec.Helpers do
def empty_array_response do
Operation.response("Empty array", "application/json", %Schema{type: :array, example: []})
end
def no_content_response do
Operation.response("No Content", "application/json", %Schema{type: :string, example: ""})
end
end

View file

@ -11,6 +11,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
alias Pleroma.Web.ApiSpec.Schemas.ActorType
alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
alias Pleroma.Web.ApiSpec.Schemas.List
alias Pleroma.Web.ApiSpec.Schemas.Status
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
@ -154,8 +155,10 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
security: [%{"oAuth" => ["read:accounts"]}],
description:
"Accounts which follow the given account, if network is not hidden by the account owner.",
parameters:
[%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}] ++ pagination_params(),
parameters: [
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
with_relationships_param() | pagination_params()
],
responses: %{
200 => Operation.response("Accounts", "application/json", array_of_accounts())
}
@ -170,8 +173,10 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
security: [%{"oAuth" => ["read:accounts"]}],
description:
"Accounts which the given account is following, if network is not hidden by the account owner.",
parameters:
[%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}] ++ pagination_params(),
parameters: [
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
with_relationships_param() | pagination_params()
],
responses: %{200 => Operation.response("Accounts", "application/json", array_of_accounts())}
}
end
@ -366,15 +371,18 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
title: "AccountCreateRequest",
description: "POST body for creating an account",
type: :object,
required: [:username, :password, :agreement],
properties: %{
reason: %Schema{
type: :string,
nullable: true,
description:
"Text that will be reviewed by moderators if registrations require manual approval"
},
username: %Schema{type: :string, description: "The desired username for the account"},
email: %Schema{
type: :string,
nullable: true,
description:
"The email address to be used for login. Required when `account_activation_required` is enabled.",
format: :email
@ -385,29 +393,39 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
format: :password
},
agreement: %Schema{
type: :boolean,
allOf: [BooleanLike],
description:
"Whether the user agrees to the local rules, terms, and policies. These should be presented to the user in order to allow them to consent before setting this parameter to TRUE."
},
locale: %Schema{
type: :string,
nullable: true,
description: "The language of the confirmation email that will be sent"
},
# Pleroma-specific properties:
fullname: %Schema{type: :string, description: "Full name"},
bio: %Schema{type: :string, description: "Bio", default: ""},
fullname: %Schema{type: :string, nullable: true, description: "Full name"},
bio: %Schema{type: :string, description: "Bio", nullable: true, default: ""},
captcha_solution: %Schema{
type: :string,
nullable: true,
description: "Provider-specific captcha solution"
},
captcha_token: %Schema{type: :string, description: "Provider-specific captcha token"},
captcha_answer_data: %Schema{type: :string, description: "Provider-specific captcha data"},
captcha_token: %Schema{
type: :string,
nullable: true,
description: "Provider-specific captcha token"
},
captcha_answer_data: %Schema{
type: :string,
nullable: true,
description: "Provider-specific captcha data"
},
token: %Schema{
type: :string,
nullable: true,
description: "Invite token required when the registrations aren't public"
}
},
required: [:username, :password, :agreement],
example: %{
"username" => "cofe",
"email" => "cofe@example.com",
@ -445,29 +463,35 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
type: :object,
properties: %{
bot: %Schema{
type: :boolean,
allOf: [BooleanLike],
nullable: true,
description: "Whether the account has a bot flag."
},
display_name: %Schema{
type: :string,
nullable: true,
description: "The display name to use for the profile."
},
note: %Schema{type: :string, description: "The account bio."},
avatar: %Schema{
type: :string,
nullable: true,
description: "Avatar image encoded using multipart/form-data",
format: :binary
},
header: %Schema{
type: :string,
nullable: true,
description: "Header image encoded using multipart/form-data",
format: :binary
},
locked: %Schema{
type: :boolean,
allOf: [BooleanLike],
nullable: true,
description: "Whether manual approval of follow requests is required."
},
fields_attributes: %Schema{
nullable: true,
oneOf: [
%Schema{type: :array, items: attribute_field()},
%Schema{type: :object, additionalProperties: %Schema{type: attribute_field()}}
@ -486,48 +510,66 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
# Pleroma-specific fields
no_rich_text: %Schema{
type: :boolean,
allOf: [BooleanLike],
nullable: true,
description: "html tags are stripped from all statuses requested from the API"
},
hide_followers: %Schema{type: :boolean, description: "user's followers will be hidden"},
hide_follows: %Schema{type: :boolean, description: "user's follows will be hidden"},
hide_followers: %Schema{
allOf: [BooleanLike],
nullable: true,
description: "user's followers will be hidden"
},
hide_follows: %Schema{
allOf: [BooleanLike],
nullable: true,
description: "user's follows will be hidden"
},
hide_followers_count: %Schema{
type: :boolean,
allOf: [BooleanLike],
nullable: true,
description: "user's follower count will be hidden"
},
hide_follows_count: %Schema{
type: :boolean,
allOf: [BooleanLike],
nullable: true,
description: "user's follow count will be hidden"
},
hide_favorites: %Schema{
type: :boolean,
allOf: [BooleanLike],
nullable: true,
description: "user's favorites timeline will be hidden"
},
show_role: %Schema{
type: :boolean,
allOf: [BooleanLike],
nullable: true,
description: "user's role (e.g admin, moderator) will be exposed to anyone in the
API"
},
default_scope: VisibilityScope,
pleroma_settings_store: %Schema{
type: :object,
nullable: true,
description: "Opaque user settings to be saved on the backend."
},
skip_thread_containment: %Schema{
type: :boolean,
allOf: [BooleanLike],
nullable: true,
description: "Skip filtering out broken threads"
},
allow_following_move: %Schema{
type: :boolean,
allOf: [BooleanLike],
nullable: true,
description: "Allows automatically follow moved following accounts"
},
pleroma_background_image: %Schema{
type: :string,
nullable: true,
description: "Sets the background image of the user.",
format: :binary
},
discoverable: %Schema{
type: :boolean,
allOf: [BooleanLike],
nullable: true,
description:
"Discovery of this account in search results and other services is allowed."
},
@ -555,11 +597,12 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
}
end
defp array_of_accounts do
def array_of_accounts do
%Schema{
title: "ArrayOfAccounts",
type: :array,
items: Account
items: Account,
example: [Account.schema().example]
}
end
@ -622,7 +665,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
description: "POST body for muting an account",
type: :object,
properties: %{
uri: %Schema{type: :string, format: :uri}
uri: %Schema{type: :string, nullable: true, format: :uri}
},
required: [:uri]
}
@ -635,7 +678,8 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
type: :object,
properties: %{
notifications: %Schema{
type: :boolean,
allOf: [BooleanLike],
nullable: true,
description: "Mute notifications in addition to statuses? Defaults to true.",
default: true
}
@ -646,28 +690,12 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
}
end
defp list do
%Schema{
title: "List",
description: "Response schema for a list",
type: :object,
properties: %{
id: %Schema{type: :string},
title: %Schema{type: :string}
},
example: %{
"id" => "123",
"title" => "my list"
}
}
end
defp array_of_lists do
%Schema{
title: "ArrayOfLists",
description: "Response schema for lists",
type: :array,
items: list(),
items: List,
example: [
%{"id" => "123", "title" => "my list"},
%{"id" => "1337", "title" => "anotehr list"}

View file

@ -105,7 +105,11 @@ defmodule Pleroma.Web.ApiSpec.AppOperation do
description: "Space separated list of scopes",
default: "read"
},
website: %Schema{type: :string, description: "A URL to the homepage of your app"}
website: %Schema{
type: :string,
nullable: true,
description: "A URL to the homepage of your app"
}
},
required: [:client_name, :redirect_uris],
example: %{

View file

@ -0,0 +1,61 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.ConversationOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.Conversation
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
import Pleroma.Web.ApiSpec.Helpers
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def index_operation do
%Operation{
tags: ["Conversations"],
summary: "Show conversation",
security: [%{"oAuth" => ["read:statuses"]}],
operationId: "ConversationController.index",
parameters: [
Operation.parameter(
:recipients,
:query,
%Schema{type: :array, items: FlakeID},
"Only return conversations with the given recipients (a list of user ids)"
)
| pagination_params()
],
responses: %{
200 =>
Operation.response("Array of Conversation", "application/json", %Schema{
type: :array,
items: Conversation,
example: [Conversation.schema().example]
})
}
}
end
def mark_as_read_operation do
%Operation{
tags: ["Conversations"],
summary: "Mark as read",
operationId: "ConversationController.mark_as_read",
parameters: [
Operation.parameter(:id, :path, :string, "Conversation ID",
example: "123",
required: true
)
],
security: [%{"oAuth" => ["write:conversations"]}],
responses: %{
200 => Operation.response("Conversation", "application/json", Conversation)
}
}
end
end

View file

@ -0,0 +1,102 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.EmojiReactionOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.Account
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.Schemas.Status
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def index_operation do
%Operation{
tags: ["Emoji Reactions"],
summary:
"Get an object of emoji to account mappings with accounts that reacted to the post",
parameters: [
Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
Operation.parameter(:emoji, :path, :string, "Filter by a single unicode emoji",
required: false
)
],
security: [%{"oAuth" => ["read:statuses"]}],
operationId: "EmojiReactionController.index",
responses: %{
200 => array_of_reactions_response()
}
}
end
def create_operation do
%Operation{
tags: ["Emoji Reactions"],
summary: "React to a post with a unicode emoji",
parameters: [
Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
Operation.parameter(:emoji, :path, :string, "A single character unicode emoji",
required: true
)
],
security: [%{"oAuth" => ["write:statuses"]}],
operationId: "EmojiReactionController.create",
responses: %{
200 => Operation.response("Status", "application/json", Status)
}
}
end
def delete_operation do
%Operation{
tags: ["Emoji Reactions"],
summary: "Remove a reaction to a post with a unicode emoji",
parameters: [
Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
Operation.parameter(:emoji, :path, :string, "A single character unicode emoji",
required: true
)
],
security: [%{"oAuth" => ["write:statuses"]}],
operationId: "EmojiReactionController.delete",
responses: %{
200 => Operation.response("Status", "application/json", Status)
}
}
end
defp array_of_reactions_response do
Operation.response("Array of Emoji Reactions", "application/json", %Schema{
type: :array,
items: emoji_reaction(),
example: [emoji_reaction().example]
})
end
defp emoji_reaction do
%Schema{
title: "EmojiReaction",
type: :object,
properties: %{
name: %Schema{type: :string, description: "Emoji"},
count: %Schema{type: :integer, description: "Count of reactions with this emoji"},
me: %Schema{type: :boolean, description: "Did I react with this emoji?"},
accounts: %Schema{
type: :array,
items: Account,
description: "Array of accounts reacted with this emoji"
}
},
example: %{
"name" => "😱",
"count" => 1,
"me" => false,
"accounts" => [Account.schema().example]
}
}
end
end

View file

@ -0,0 +1,230 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.FilterOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Helpers
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def index_operation do
%Operation{
tags: ["apps"],
summary: "View all filters",
operationId: "FilterController.index",
security: [%{"oAuth" => ["read:filters"]}],
responses: %{
200 => Operation.response("Filters", "application/json", array_of_filters())
}
}
end
def create_operation do
%Operation{
tags: ["apps"],
summary: "Create a filter",
operationId: "FilterController.create",
requestBody: Helpers.request_body("Parameters", create_request(), required: true),
security: [%{"oAuth" => ["write:filters"]}],
responses: %{200 => Operation.response("Filter", "application/json", filter())}
}
end
def show_operation do
%Operation{
tags: ["apps"],
summary: "View all filters",
parameters: [id_param()],
operationId: "FilterController.show",
security: [%{"oAuth" => ["read:filters"]}],
responses: %{
200 => Operation.response("Filter", "application/json", filter())
}
}
end
def update_operation do
%Operation{
tags: ["apps"],
summary: "Update a filter",
parameters: [id_param()],
operationId: "FilterController.update",
requestBody: Helpers.request_body("Parameters", update_request(), required: true),
security: [%{"oAuth" => ["write:filters"]}],
responses: %{
200 => Operation.response("Filter", "application/json", filter())
}
}
end
def delete_operation do
%Operation{
tags: ["apps"],
summary: "Remove a filter",
parameters: [id_param()],
operationId: "FilterController.delete",
security: [%{"oAuth" => ["write:filters"]}],
responses: %{
200 =>
Operation.response("Filter", "application/json", %Schema{
type: :object,
description: "Empty object"
})
}
}
end
defp id_param do
Operation.parameter(:id, :path, :string, "Filter ID", example: "123", required: true)
end
defp filter do
%Schema{
title: "Filter",
type: :object,
properties: %{
id: %Schema{type: :string},
phrase: %Schema{type: :string, description: "The text to be filtered"},
context: %Schema{
type: :array,
items: %Schema{type: :string, enum: ["home", "notifications", "public", "thread"]},
description: "The contexts in which the filter should be applied."
},
expires_at: %Schema{
type: :string,
format: :"date-time",
description:
"When the filter should no longer be applied. String (ISO 8601 Datetime), or null if the filter does not expire.",
nullable: true
},
irreversible: %Schema{
type: :boolean,
description:
"Should matching entities in home and notifications be dropped by the server?"
},
whole_word: %Schema{
type: :boolean,
description: "Should the filter consider word boundaries?"
}
},
example: %{
"id" => "5580",
"phrase" => "@twitter.com",
"context" => [
"home",
"notifications",
"public",
"thread"
],
"whole_word" => false,
"expires_at" => nil,
"irreversible" => true
}
}
end
defp array_of_filters do
%Schema{
title: "ArrayOfFilters",
description: "Array of Filters",
type: :array,
items: filter(),
example: [
%{
"id" => "5580",
"phrase" => "@twitter.com",
"context" => [
"home",
"notifications",
"public",
"thread"
],
"whole_word" => false,
"expires_at" => nil,
"irreversible" => true
},
%{
"id" => "6191",
"phrase" => ":eurovision2019:",
"context" => [
"home"
],
"whole_word" => true,
"expires_at" => "2019-05-21T13:47:31.333Z",
"irreversible" => false
}
]
}
end
defp create_request do
%Schema{
title: "FilterCreateRequest",
allOf: [
update_request(),
%Schema{
type: :object,
properties: %{
irreversible: %Schema{
allOf: [BooleanLike],
description:
"Should the server irreversibly drop matching entities from home and notifications?",
default: false
}
}
}
],
example: %{
"phrase" => "knights",
"context" => ["home"]
}
}
end
defp update_request do
%Schema{
title: "FilterUpdateRequest",
type: :object,
properties: %{
phrase: %Schema{type: :string, description: "The text to be filtered"},
context: %Schema{
type: :array,
items: %Schema{type: :string, enum: ["home", "notifications", "public", "thread"]},
description:
"Array of enumerable strings `home`, `notifications`, `public`, `thread`. At least one context must be specified."
},
irreversible: %Schema{
allOf: [BooleanLike],
nullable: true,
description:
"Should the server irreversibly drop matching entities from home and notifications?"
},
whole_word: %Schema{
allOf: [BooleanLike],
nullable: true,
description: "Consider word boundaries?",
default: true
}
# TODO: probably should implement filter expiration
# expires_in: %Schema{
# type: :string,
# format: :"date-time",
# description:
# "ISO 8601 Datetime for when the filter expires. Otherwise,
# null for a filter that doesn't expire."
# }
},
required: [:phrase, :context],
example: %{
"phrase" => "knights",
"context" => ["home"]
}
}
end
end

View file

@ -0,0 +1,65 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.FollowRequestOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.Account
alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def index_operation do
%Operation{
tags: ["Follow Requests"],
summary: "Pending Follows",
security: [%{"oAuth" => ["read:follows", "follow"]}],
operationId: "FollowRequestController.index",
responses: %{
200 =>
Operation.response("Array of Account", "application/json", %Schema{
type: :array,
items: Account,
example: [Account.schema().example]
})
}
}
end
def authorize_operation do
%Operation{
tags: ["Follow Requests"],
summary: "Accept Follow",
operationId: "FollowRequestController.authorize",
parameters: [id_param()],
security: [%{"oAuth" => ["follow", "write:follows"]}],
responses: %{
200 => Operation.response("Relationship", "application/json", AccountRelationship)
}
}
end
def reject_operation do
%Operation{
tags: ["Follow Requests"],
summary: "Reject Follow",
operationId: "FollowRequestController.reject",
parameters: [id_param()],
security: [%{"oAuth" => ["follow", "write:follows"]}],
responses: %{
200 => Operation.response("Relationship", "application/json", AccountRelationship)
}
}
end
defp id_param do
Operation.parameter(:id, :path, :string, "Conversation ID",
example: "123",
required: true
)
end
end

View file

@ -0,0 +1,175 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.InstanceOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def show_operation do
%Operation{
tags: ["Instance"],
summary: "Fetch instance",
description: "Information about the server",
operationId: "InstanceController.show",
responses: %{
200 => Operation.response("Instance", "application/json", instance())
}
}
end
def peers_operation do
%Operation{
tags: ["Instance"],
summary: "List of known hosts",
operationId: "InstanceController.peers",
responses: %{
200 => Operation.response("Array of domains", "application/json", array_of_domains())
}
}
end
defp instance do
%Schema{
type: :object,
properties: %{
uri: %Schema{type: :string, description: "The domain name of the instance"},
title: %Schema{type: :string, description: "The title of the website"},
description: %Schema{
type: :string,
description: "Admin-defined description of the Pleroma site"
},
version: %Schema{
type: :string,
description: "The version of Pleroma installed on the instance"
},
email: %Schema{
type: :string,
description: "An email that may be contacted for any inquiries",
format: :email
},
urls: %Schema{
type: :object,
description: "URLs of interest for clients apps",
properties: %{
streaming_api: %Schema{
type: :string,
description: "Websockets address for push streaming"
}
}
},
stats: %Schema{
type: :object,
description: "Statistics about how much information the instance contains",
properties: %{
user_count: %Schema{
type: :integer,
description: "Users registered on this instance"
},
status_count: %Schema{
type: :integer,
description: "Statuses authored by users on instance"
},
domain_count: %Schema{
type: :integer,
description: "Domains federated with this instance"
}
}
},
thumbnail: %Schema{
type: :string,
description: "Banner image for the website",
nullable: true
},
languages: %Schema{
type: :array,
items: %Schema{type: :string},
description: "Primary langauges of the website and its staff"
},
registrations: %Schema{type: :boolean, description: "Whether registrations are enabled"},
# Extra (not present in Mastodon):
max_toot_chars: %Schema{
type: :integer,
description: ": Posts character limit (CW/Subject included in the counter)"
},
poll_limits: %Schema{
type: :object,
description: "A map with poll limits for local polls",
properties: %{
max_options: %Schema{
type: :integer,
description: "Maximum number of options."
},
max_option_chars: %Schema{
type: :integer,
description: "Maximum number of characters per option."
},
min_expiration: %Schema{
type: :integer,
description: "Minimum expiration time (in seconds)."
},
max_expiration: %Schema{
type: :integer,
description: "Maximum expiration time (in seconds)."
}
}
},
upload_limit: %Schema{
type: :integer,
description: "File size limit of uploads (except for avatar, background, banner)"
},
avatar_upload_limit: %Schema{type: :integer, description: "The title of the website"},
background_upload_limit: %Schema{type: :integer, description: "The title of the website"},
banner_upload_limit: %Schema{type: :integer, description: "The title of the website"},
background_image: %Schema{
type: :string,
format: :uri,
description: "The background image for the website"
}
},
example: %{
"avatar_upload_limit" => 2_000_000,
"background_upload_limit" => 4_000_000,
"background_image" => "/static/image.png",
"banner_upload_limit" => 4_000_000,
"description" => "A Pleroma instance, an alternative fediverse server",
"email" => "lain@lain.com",
"languages" => ["en"],
"max_toot_chars" => 5000,
"poll_limits" => %{
"max_expiration" => 31_536_000,
"max_option_chars" => 200,
"max_options" => 20,
"min_expiration" => 0
},
"registrations" => false,
"stats" => %{
"domain_count" => 2996,
"status_count" => 15_802,
"user_count" => 5
},
"thumbnail" => "https://lain.com/instance/thumbnail.jpeg",
"title" => "lain.com",
"upload_limit" => 16_000_000,
"uri" => "https://lain.com",
"urls" => %{
"streaming_api" => "wss://lain.com"
},
"version" => "2.7.2 (compatible; Pleroma 2.0.50-536-g25eec6d7-develop)"
}
}
end
defp array_of_domains do
%Schema{
type: :array,
items: %Schema{type: :string},
example: ["pleroma.site", "lain.com", "bikeshed.party"]
}
end
end

View file

@ -0,0 +1,188 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.ListOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.Account
alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.Schemas.List
import Pleroma.Web.ApiSpec.Helpers
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def index_operation do
%Operation{
tags: ["Lists"],
summary: "Show user's lists",
description: "Fetch all lists that the user owns",
security: [%{"oAuth" => ["read:lists"]}],
operationId: "ListController.index",
responses: %{
200 => Operation.response("Array of List", "application/json", array_of_lists())
}
}
end
def create_operation do
%Operation{
tags: ["Lists"],
summary: "Create a list",
description: "Fetch the list with the given ID. Used for verifying the title of a list.",
operationId: "ListController.create",
requestBody: create_update_request(),
security: [%{"oAuth" => ["write:lists"]}],
responses: %{
200 => Operation.response("List", "application/json", List),
400 => Operation.response("Error", "application/json", ApiError),
404 => Operation.response("Error", "application/json", ApiError)
}
}
end
def show_operation do
%Operation{
tags: ["Lists"],
summary: "Show a single list",
description: "Fetch the list with the given ID. Used for verifying the title of a list.",
operationId: "ListController.show",
parameters: [id_param()],
security: [%{"oAuth" => ["read:lists"]}],
responses: %{
200 => Operation.response("List", "application/json", List),
404 => Operation.response("Error", "application/json", ApiError)
}
}
end
def update_operation do
%Operation{
tags: ["Lists"],
summary: "Update a list",
description: "Change the title of a list",
operationId: "ListController.update",
parameters: [id_param()],
requestBody: create_update_request(),
security: [%{"oAuth" => ["write:lists"]}],
responses: %{
200 => Operation.response("List", "application/json", List),
422 => Operation.response("Error", "application/json", ApiError)
}
}
end
def delete_operation do
%Operation{
tags: ["Lists"],
summary: "Delete a list",
operationId: "ListController.delete",
parameters: [id_param()],
security: [%{"oAuth" => ["write:lists"]}],
responses: %{
200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
}
}
end
def list_accounts_operation do
%Operation{
tags: ["Lists"],
summary: "View accounts in list",
operationId: "ListController.list_accounts",
parameters: [id_param()],
security: [%{"oAuth" => ["read:lists"]}],
responses: %{
200 =>
Operation.response("Array of Account", "application/json", %Schema{
type: :array,
items: Account
})
}
}
end
def add_to_list_operation do
%Operation{
tags: ["Lists"],
summary: "Add accounts to list",
description: "Add accounts to the given list.",
operationId: "ListController.add_to_list",
parameters: [id_param()],
requestBody: add_remove_accounts_request(),
security: [%{"oAuth" => ["write:lists"]}],
responses: %{
200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
}
}
end
def remove_from_list_operation do
%Operation{
tags: ["Lists"],
summary: "Remove accounts from list",
operationId: "ListController.remove_from_list",
parameters: [id_param()],
requestBody: add_remove_accounts_request(),
security: [%{"oAuth" => ["write:lists"]}],
responses: %{
200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
}
}
end
defp array_of_lists do
%Schema{
title: "ArrayOfLists",
description: "Response schema for lists",
type: :array,
items: List,
example: [
%{"id" => "123", "title" => "my list"},
%{"id" => "1337", "title" => "another list"}
]
}
end
defp id_param do
Operation.parameter(:id, :path, :string, "List ID",
example: "123",
required: true
)
end
defp create_update_request do
request_body(
"Parameters",
%Schema{
description: "POST body for creating or updating a List",
type: :object,
properties: %{
title: %Schema{type: :string, description: "List title"}
},
required: [:title]
},
required: true
)
end
defp add_remove_accounts_request do
request_body(
"Parameters",
%Schema{
description: "POST body for adding/removing accounts to/from a List",
type: :object,
properties: %{
account_ids: %Schema{type: :array, description: "Array of account IDs", items: FlakeID}
},
required: [:account_ids]
},
required: true
)
end
end

View file

@ -0,0 +1,142 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.MarkerOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Helpers
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def index_operation do
%Operation{
tags: ["Markers"],
summary: "Get saved timeline position",
security: [%{"oAuth" => ["read:statuses"]}],
operationId: "MarkerController.index",
parameters: [
Operation.parameter(
:timeline,
:query,
%Schema{
type: :array,
items: %Schema{type: :string, enum: ["home", "notifications"]}
},
"Array of markers to fetch. If not provided, an empty object will be returned."
)
],
responses: %{
200 => Operation.response("Marker", "application/json", response()),
403 => Operation.response("Error", "application/json", api_error())
}
}
end
def upsert_operation do
%Operation{
tags: ["Markers"],
summary: "Save position in timeline",
operationId: "MarkerController.upsert",
requestBody: Helpers.request_body("Parameters", upsert_request(), required: true),
security: [%{"oAuth" => ["follow", "write:blocks"]}],
responses: %{
200 => Operation.response("Marker", "application/json", response()),
403 => Operation.response("Error", "application/json", api_error())
}
}
end
defp marker do
%Schema{
title: "Marker",
description: "Schema for a marker",
type: :object,
properties: %{
last_read_id: %Schema{type: :string},
version: %Schema{type: :integer},
updated_at: %Schema{type: :string},
pleroma: %Schema{
type: :object,
properties: %{
unread_count: %Schema{type: :integer}
}
}
},
example: %{
"last_read_id" => "35098814",
"version" => 361,
"updated_at" => "2019-11-26T22:37:25.239Z",
"pleroma" => %{"unread_count" => 5}
}
}
end
defp response do
%Schema{
title: "MarkersResponse",
description: "Response schema for markers",
type: :object,
properties: %{
notifications: %Schema{allOf: [marker()], nullable: true},
home: %Schema{allOf: [marker()], nullable: true}
},
items: %Schema{type: :string},
example: %{
"notifications" => %{
"last_read_id" => "35098814",
"version" => 361,
"updated_at" => "2019-11-26T22:37:25.239Z",
"pleroma" => %{"unread_count" => 0}
},
"home" => %{
"last_read_id" => "103206604258487607",
"version" => 468,
"updated_at" => "2019-11-26T22:37:25.235Z",
"pleroma" => %{"unread_count" => 10}
}
}
}
end
defp upsert_request do
%Schema{
title: "MarkersUpsertRequest",
description: "Request schema for marker upsert",
type: :object,
properties: %{
notifications: %Schema{
type: :object,
nullable: true,
properties: %{
last_read_id: %Schema{nullable: true, type: :string}
}
},
home: %Schema{
type: :object,
nullable: true,
properties: %{
last_read_id: %Schema{nullable: true, type: :string}
}
}
},
example: %{
"home" => %{
"last_read_id" => "103194548672408537",
"version" => 462,
"updated_at" => "2019-11-24T19:39:39.337Z"
}
}
}
end
defp api_error do
%Schema{
type: :object,
properties: %{error: %Schema{type: :string}}
}
end
end

View file

@ -0,0 +1,132 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.MediaOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Helpers
alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.Attachment
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def create_operation do
%Operation{
tags: ["media"],
summary: "Upload media as attachment",
description: "Creates an attachment to be used with a new status.",
operationId: "MediaController.create",
security: [%{"oAuth" => ["write:media"]}],
requestBody: Helpers.request_body("Parameters", create_request()),
responses: %{
200 => Operation.response("Media", "application/json", Attachment),
401 => Operation.response("Media", "application/json", ApiError),
422 => Operation.response("Media", "application/json", ApiError)
}
}
end
defp create_request do
%Schema{
title: "MediaCreateRequest",
description: "POST body for creating an attachment",
type: :object,
required: [:file],
properties: %{
file: %Schema{
type: :string,
format: :binary,
description: "The file to be attached, using multipart form data."
},
description: %Schema{
type: :string,
description: "A plain-text description of the media, for accessibility purposes."
},
focus: %Schema{
type: :string,
description: "Two floating points (x,y), comma-delimited, ranging from -1.0 to 1.0."
}
}
}
end
def update_operation do
%Operation{
tags: ["media"],
summary: "Upload media as attachment",
description: "Creates an attachment to be used with a new status.",
operationId: "MediaController.update",
security: [%{"oAuth" => ["write:media"]}],
parameters: [id_param()],
requestBody: Helpers.request_body("Parameters", update_request()),
responses: %{
200 => Operation.response("Media", "application/json", Attachment),
400 => Operation.response("Media", "application/json", ApiError),
401 => Operation.response("Media", "application/json", ApiError),
422 => Operation.response("Media", "application/json", ApiError)
}
}
end
defp update_request do
%Schema{
title: "MediaUpdateRequest",
description: "POST body for updating an attachment",
type: :object,
properties: %{
file: %Schema{
type: :string,
format: :binary,
description: "The file to be attached, using multipart form data."
},
description: %Schema{
type: :string,
description: "A plain-text description of the media, for accessibility purposes."
},
focus: %Schema{
type: :string,
description: "Two floating points (x,y), comma-delimited, ranging from -1.0 to 1.0."
}
}
}
end
def show_operation do
%Operation{
tags: ["media"],
summary: "Show Uploaded media attachment",
operationId: "MediaController.show",
parameters: [id_param()],
security: [%{"oAuth" => ["read:media"]}],
responses: %{
200 => Operation.response("Media", "application/json", Attachment),
401 => Operation.response("Media", "application/json", ApiError),
422 => Operation.response("Media", "application/json", ApiError)
}
}
end
def create2_operation do
%Operation{
tags: ["media"],
summary: "Upload media as attachment",
description: "Creates an attachment to be used with a new status.",
operationId: "MediaController.create2",
security: [%{"oAuth" => ["write:media"]}],
requestBody: Helpers.request_body("Parameters", create_request()),
responses: %{
202 => Operation.response("Media", "application/json", Attachment),
422 => Operation.response("Media", "application/json", ApiError),
500 => Operation.response("Media", "application/json", ApiError)
}
}
end
defp id_param do
Operation.parameter(:id, :path, :string, "The ID of the Attachment entity")
end
end

View file

@ -145,7 +145,7 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
}
end
defp notification do
def notification do
%Schema{
title: "Notification",
description: "Response schema for a notification",

View file

@ -0,0 +1,187 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship
alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.StatusOperation
import Pleroma.Web.ApiSpec.Helpers
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def confirmation_resend_operation do
%Operation{
tags: ["Accounts"],
summary: "Resend confirmation email. Expects `email` or `nickname`",
operationId: "PleromaAPI.AccountController.confirmation_resend",
parameters: [
Operation.parameter(:email, :query, :string, "Email of that needs to be verified",
example: "cofe@cofe.io"
),
Operation.parameter(
:nickname,
:query,
:string,
"Nickname of user that needs to be verified",
example: "cofefe"
)
],
responses: %{
204 => no_content_response()
}
}
end
def update_avatar_operation do
%Operation{
tags: ["Accounts"],
summary: "Set/clear user avatar image",
operationId: "PleromaAPI.AccountController.update_avatar",
requestBody:
request_body("Parameters", update_avatar_or_background_request(), required: true),
security: [%{"oAuth" => ["write:accounts"]}],
responses: %{
200 => update_response(),
403 => Operation.response("Forbidden", "application/json", ApiError)
}
}
end
def update_banner_operation do
%Operation{
tags: ["Accounts"],
summary: "Set/clear user banner image",
operationId: "PleromaAPI.AccountController.update_banner",
requestBody: request_body("Parameters", update_banner_request(), required: true),
security: [%{"oAuth" => ["write:accounts"]}],
responses: %{
200 => update_response()
}
}
end
def update_background_operation do
%Operation{
tags: ["Accounts"],
summary: "Set/clear user background image",
operationId: "PleromaAPI.AccountController.update_background",
security: [%{"oAuth" => ["write:accounts"]}],
requestBody:
request_body("Parameters", update_avatar_or_background_request(), required: true),
responses: %{
200 => update_response()
}
}
end
def favourites_operation do
%Operation{
tags: ["Accounts"],
summary: "Returns favorites timeline of any user",
operationId: "PleromaAPI.AccountController.favourites",
parameters: [id_param() | pagination_params()],
security: [%{"oAuth" => ["read:favourites"]}],
responses: %{
200 =>
Operation.response(
"Array of Statuses",
"application/json",
StatusOperation.array_of_statuses()
),
403 => Operation.response("Forbidden", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
end
def subscribe_operation do
%Operation{
tags: ["Accounts"],
summary: "Subscribe to receive notifications for all statuses posted by a user",
operationId: "PleromaAPI.AccountController.subscribe",
parameters: [id_param()],
security: [%{"oAuth" => ["follow", "write:follows"]}],
responses: %{
200 => Operation.response("Relationship", "application/json", AccountRelationship),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
end
def unsubscribe_operation do
%Operation{
tags: ["Accounts"],
summary: "Unsubscribe to stop receiving notifications from user statuses",
operationId: "PleromaAPI.AccountController.unsubscribe",
parameters: [id_param()],
security: [%{"oAuth" => ["follow", "write:follows"]}],
responses: %{
200 => Operation.response("Relationship", "application/json", AccountRelationship),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
end
defp id_param do
Operation.parameter(:id, :path, FlakeID, "Account ID",
example: "9umDrYheeY451cQnEe",
required: true
)
end
defp update_avatar_or_background_request do
%Schema{
title: "PleromaAccountUpdateAvatarOrBackgroundRequest",
type: :object,
properties: %{
img: %Schema{
nullable: true,
type: :string,
format: :binary,
description: "Image encoded using `multipart/form-data` or an empty string to clear"
}
}
}
end
defp update_banner_request do
%Schema{
title: "PleromaAccountUpdateBannerRequest",
type: :object,
properties: %{
banner: %Schema{
type: :string,
nullable: true,
format: :binary,
description: "Image encoded using `multipart/form-data` or an empty string to clear"
}
}
}
end
defp update_response do
Operation.response("PleromaAccountUpdateResponse", "application/json", %Schema{
type: :object,
properties: %{
url: %Schema{
type: :string,
format: :uri,
nullable: true,
description: "Image URL"
}
},
example: %{
"url" =>
"https://cofe.party/media/9d0add56-bcb6-4c0f-8225-cbbd0b6dd773/13eadb6972c9ccd3f4ffa3b8196f0e0d38b4d2f27594457c52e52946c054cd9a.gif"
}
})
end
end

View file

@ -0,0 +1,106 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.PleromaConversationOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.Conversation
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.StatusOperation
import Pleroma.Web.ApiSpec.Helpers
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def show_operation do
%Operation{
tags: ["Conversations"],
summary: "The conversation with the given ID",
parameters: [
Operation.parameter(:id, :path, :string, "Conversation ID",
example: "123",
required: true
)
],
security: [%{"oAuth" => ["read:statuses"]}],
operationId: "PleromaAPI.ConversationController.show",
responses: %{
200 => Operation.response("Conversation", "application/json", Conversation)
}
}
end
def statuses_operation do
%Operation{
tags: ["Conversations"],
summary: "Timeline for a given conversation",
parameters: [
Operation.parameter(:id, :path, :string, "Conversation ID",
example: "123",
required: true
)
| pagination_params()
],
security: [%{"oAuth" => ["read:statuses"]}],
operationId: "PleromaAPI.ConversationController.statuses",
responses: %{
200 =>
Operation.response(
"Array of Statuses",
"application/json",
StatusOperation.array_of_statuses()
)
}
}
end
def update_operation do
%Operation{
tags: ["Conversations"],
summary: "Update a conversation. Used to change the set of recipients.",
parameters: [
Operation.parameter(:id, :path, :string, "Conversation ID",
example: "123",
required: true
),
Operation.parameter(
:recipients,
:query,
%Schema{type: :array, items: FlakeID},
"A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though.",
required: true
)
],
security: [%{"oAuth" => ["write:conversations"]}],
operationId: "PleromaAPI.ConversationController.update",
responses: %{
200 => Operation.response("Conversation", "application/json", Conversation)
}
}
end
def mark_as_read_operation do
%Operation{
tags: ["Conversations"],
summary: "Marks all user's conversations as read",
security: [%{"oAuth" => ["write:conversations"]}],
operationId: "PleromaAPI.ConversationController.mark_as_read",
responses: %{
200 =>
Operation.response(
"Array of Conversations that were marked as read",
"application/json",
%Schema{
type: :array,
items: Conversation,
example: [Conversation.schema().example]
}
)
}
}
end
end

View file

@ -0,0 +1,390 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.ApiError
import Pleroma.Web.ApiSpec.Helpers
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def remote_operation do
%Operation{
tags: ["Emoji Packs"],
summary: "Make request to another instance for emoji packs list",
security: [%{"oAuth" => ["write"]}],
parameters: [url_param()],
operationId: "PleromaAPI.EmojiPackController.remote",
responses: %{
200 => emoji_packs_response(),
500 => Operation.response("Error", "application/json", ApiError)
}
}
end
def index_operation do
%Operation{
tags: ["Emoji Packs"],
summary: "Lists local custom emoji packs",
operationId: "PleromaAPI.EmojiPackController.index",
responses: %{
200 => emoji_packs_response()
}
}
end
def show_operation do
%Operation{
tags: ["Emoji Packs"],
summary: "Show emoji pack",
operationId: "PleromaAPI.EmojiPackController.show",
parameters: [name_param()],
responses: %{
200 => Operation.response("Emoji Pack", "application/json", emoji_pack()),
400 => Operation.response("Bad Request", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
end
def archive_operation do
%Operation{
tags: ["Emoji Packs"],
summary: "Requests a local pack archive from the instance",
operationId: "PleromaAPI.EmojiPackController.archive",
parameters: [name_param()],
responses: %{
200 =>
Operation.response("Archive file", "application/octet-stream", %Schema{
type: :string,
format: :binary
}),
403 => Operation.response("Forbidden", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
end
def download_operation do
%Operation{
tags: ["Emoji Packs"],
summary: "Download pack from another instance",
operationId: "PleromaAPI.EmojiPackController.download",
security: [%{"oAuth" => ["write"]}],
requestBody: request_body("Parameters", download_request(), required: true),
responses: %{
200 => ok_response(),
500 => Operation.response("Error", "application/json", ApiError)
}
}
end
defp download_request do
%Schema{
type: :object,
required: [:url, :name],
properties: %{
url: %Schema{
type: :string,
format: :uri,
description: "URL of the instance to download from"
},
name: %Schema{type: :string, format: :uri, description: "Pack Name"},
as: %Schema{type: :string, format: :uri, description: "Save as"}
}
}
end
def create_operation do
%Operation{
tags: ["Emoji Packs"],
summary: "Create an empty pack",
operationId: "PleromaAPI.EmojiPackController.create",
security: [%{"oAuth" => ["write"]}],
parameters: [name_param()],
responses: %{
200 => ok_response(),
400 => Operation.response("Not Found", "application/json", ApiError),
409 => Operation.response("Conflict", "application/json", ApiError),
500 => Operation.response("Error", "application/json", ApiError)
}
}
end
def delete_operation do
%Operation{
tags: ["Emoji Packs"],
summary: "Delete a custom emoji pack",
operationId: "PleromaAPI.EmojiPackController.delete",
security: [%{"oAuth" => ["write"]}],
parameters: [name_param()],
responses: %{
200 => ok_response(),
400 => Operation.response("Bad Request", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
end
def update_operation do
%Operation{
tags: ["Emoji Packs"],
summary: "Updates (replaces) pack metadata",
operationId: "PleromaAPI.EmojiPackController.update",
security: [%{"oAuth" => ["write"]}],
requestBody: request_body("Parameters", update_request(), required: true),
parameters: [name_param()],
responses: %{
200 => Operation.response("Metadata", "application/json", metadata()),
400 => Operation.response("Bad Request", "application/json", ApiError)
}
}
end
def add_file_operation do
%Operation{
tags: ["Emoji Packs"],
summary: "Add new file to the pack",
operationId: "PleromaAPI.EmojiPackController.add_file",
security: [%{"oAuth" => ["write"]}],
requestBody: request_body("Parameters", add_file_request(), required: true),
parameters: [name_param()],
responses: %{
200 => Operation.response("Files Object", "application/json", files_object()),
400 => Operation.response("Bad Request", "application/json", ApiError),
409 => Operation.response("Conflict", "application/json", ApiError)
}
}
end
defp add_file_request do
%Schema{
type: :object,
required: [:file],
properties: %{
file: %Schema{
description:
"File needs to be uploaded with the multipart request or link to remote file",
anyOf: [
%Schema{type: :string, format: :binary},
%Schema{type: :string, format: :uri}
]
},
shortcode: %Schema{
type: :string,
description:
"Shortcode for new emoji, must be unique for all emoji. If not sended, shortcode will be taken from original filename."
},
filename: %Schema{
type: :string,
description:
"New emoji file name. If not specified will be taken from original filename."
}
}
}
end
def update_file_operation do
%Operation{
tags: ["Emoji Packs"],
summary: "Add new file to the pack",
operationId: "PleromaAPI.EmojiPackController.update_file",
security: [%{"oAuth" => ["write"]}],
requestBody: request_body("Parameters", update_file_request(), required: true),
parameters: [name_param()],
responses: %{
200 => Operation.response("Files Object", "application/json", files_object()),
400 => Operation.response("Bad Request", "application/json", ApiError),
409 => Operation.response("Conflict", "application/json", ApiError)
}
}
end
defp update_file_request do
%Schema{
type: :object,
required: [:shortcode, :new_shortcode, :new_filename],
properties: %{
shortcode: %Schema{
type: :string,
description: "Emoji file shortcode"
},
new_shortcode: %Schema{
type: :string,
description: "New emoji file shortcode"
},
new_filename: %Schema{
type: :string,
description: "New filename for emoji file"
},
force: %Schema{
type: :boolean,
description: "With true value to overwrite existing emoji with new shortcode",
default: false
}
}
}
end
def delete_file_operation do
%Operation{
tags: ["Emoji Packs"],
summary: "Delete emoji file from pack",
operationId: "PleromaAPI.EmojiPackController.delete_file",
security: [%{"oAuth" => ["write"]}],
parameters: [
name_param(),
Operation.parameter(:shortcode, :query, :string, "File shortcode",
example: "cofe",
required: true
)
],
responses: %{
200 => Operation.response("Files Object", "application/json", files_object()),
400 => Operation.response("Bad Request", "application/json", ApiError)
}
}
end
def import_from_filesystem_operation do
%Operation{
tags: ["Emoji Packs"],
summary: "Imports packs from filesystem",
operationId: "PleromaAPI.EmojiPackController.import",
security: [%{"oAuth" => ["write"]}],
responses: %{
200 =>
Operation.response("Array of imported pack names", "application/json", %Schema{
type: :array,
items: %Schema{type: :string}
})
}
}
end
defp name_param do
Operation.parameter(:name, :path, :string, "Pack Name", example: "cofe", required: true)
end
defp url_param do
Operation.parameter(
:url,
:query,
%Schema{type: :string, format: :uri},
"URL of the instance",
required: true
)
end
defp ok_response do
Operation.response("Ok", "application/json", %Schema{type: :string, example: "ok"})
end
defp emoji_packs_response do
Operation.response(
"Object with pack names as keys and pack contents as values",
"application/json",
%Schema{
type: :object,
additionalProperties: emoji_pack(),
example: %{
"emojos" => emoji_pack().example
}
}
)
end
defp emoji_pack do
%Schema{
title: "EmojiPack",
type: :object,
properties: %{
files: files_object(),
pack: %Schema{
type: :object,
properties: %{
license: %Schema{type: :string},
homepage: %Schema{type: :string, format: :uri},
description: %Schema{type: :string},
"can-download": %Schema{type: :boolean},
"share-files": %Schema{type: :boolean},
"download-sha256": %Schema{type: :string}
}
}
},
example: %{
"files" => %{"emacs" => "emacs.png", "guix" => "guix.png"},
"pack" => %{
"license" => "Test license",
"homepage" => "https://pleroma.social",
"description" => "Test description",
"can-download" => true,
"share-files" => true,
"download-sha256" => "57482F30674FD3DE821FF48C81C00DA4D4AF1F300209253684ABA7075E5FC238"
}
}
}
end
defp files_object do
%Schema{
type: :object,
additionalProperties: %Schema{type: :string},
description: "Object with emoji names as keys and filenames as values"
}
end
defp update_request do
%Schema{
type: :object,
properties: %{
metadata: %Schema{
type: :object,
description: "Metadata to replace the old one",
properties: %{
license: %Schema{type: :string},
homepage: %Schema{type: :string, format: :uri},
description: %Schema{type: :string},
"fallback-src": %Schema{
type: :string,
format: :uri,
description: "Fallback url to download pack from"
},
"fallback-src-sha256": %Schema{
type: :string,
description: "SHA256 encoded for fallback pack archive"
},
"share-files": %Schema{type: :boolean, description: "Is pack allowed for sharing?"}
}
}
}
}
end
defp metadata do
%Schema{
type: :object,
properties: %{
license: %Schema{type: :string},
homepage: %Schema{type: :string, format: :uri},
description: %Schema{type: :string},
"fallback-src": %Schema{
type: :string,
format: :uri,
description: "Fallback url to download pack from"
},
"fallback-src-sha256": %Schema{
type: :string,
description: "SHA256 encoded for fallback pack archive"
},
"share-files": %Schema{type: :boolean, description: "Is pack allowed for sharing?"}
}
}
end
end

View file

@ -0,0 +1,79 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.PleromaMascotOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.ApiError
import Pleroma.Web.ApiSpec.Helpers
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def show_operation do
%Operation{
tags: ["Mascot"],
summary: "Gets user mascot image",
security: [%{"oAuth" => ["read:accounts"]}],
operationId: "PleromaAPI.MascotController.show",
responses: %{
200 => Operation.response("Mascot", "application/json", mascot())
}
}
end
def update_operation do
%Operation{
tags: ["Mascot"],
summary: "Set/clear user avatar image",
description:
"Behaves exactly the same as `POST /api/v1/upload`. Can only accept images - any attempt to upload non-image files will be met with `HTTP 415 Unsupported Media Type`.",
operationId: "PleromaAPI.MascotController.update",
requestBody:
request_body(
"Parameters",
%Schema{
type: :object,
properties: %{
file: %Schema{type: :string, format: :binary}
}
},
required: true
),
security: [%{"oAuth" => ["write:accounts"]}],
responses: %{
200 => Operation.response("Mascot", "application/json", mascot()),
415 => Operation.response("Unsupported Media Type", "application/json", ApiError)
}
}
end
defp mascot do
%Schema{
type: :object,
properties: %{
id: %Schema{type: :string},
url: %Schema{type: :string, format: :uri},
type: %Schema{type: :string},
pleroma: %Schema{
type: :object,
properties: %{
mime_type: %Schema{type: :string}
}
}
},
example: %{
"id" => "abcdefg",
"url" => "https://pleroma.example.org/media/abcdefg.png",
"type" => "image",
"pleroma" => %{
"mime_type" => "image/png"
}
}
}
end
end

View file

@ -0,0 +1,42 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.PleromaNotificationOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.NotificationOperation
alias Pleroma.Web.ApiSpec.Schemas.ApiError
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def mark_as_read_operation do
%Operation{
tags: ["Notifications"],
summary: "Mark notifications as read. Query parameters are mutually exclusive.",
parameters: [
Operation.parameter(:id, :query, :string, "A single notification ID to read"),
Operation.parameter(:max_id, :query, :string, "Read all notifications up to this id")
],
security: [%{"oAuth" => ["write:notifications"]}],
operationId: "PleromaAPI.NotificationController.mark_as_read",
responses: %{
200 =>
Operation.response(
"A Notification or array of Motifications",
"application/json",
%Schema{
anyOf: [
%Schema{type: :array, items: NotificationOperation.notification()},
NotificationOperation.notification()
]
}
),
400 => Operation.response("Bad Request", "application/json", ApiError)
}
}
end
end

View file

@ -0,0 +1,102 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Reference
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.Account
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
import Pleroma.Web.ApiSpec.Helpers
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def create_operation do
%Operation{
tags: ["Scrobbles"],
summary: "Creates a new Listen activity for an account",
security: [%{"oAuth" => ["write"]}],
operationId: "PleromaAPI.ScrobbleController.create",
requestBody: request_body("Parameters", create_request(), requried: true),
responses: %{
200 => Operation.response("Scrobble", "application/json", scrobble())
}
}
end
def index_operation do
%Operation{
tags: ["Scrobbles"],
summary: "Requests a list of current and recent Listen activities for an account",
operationId: "PleromaAPI.ScrobbleController.index",
parameters: [
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"} | pagination_params()
],
security: [%{"oAuth" => ["read"]}],
responses: %{
200 =>
Operation.response("Array of Scrobble", "application/json", %Schema{
type: :array,
items: scrobble()
})
}
}
end
defp create_request do
%Schema{
type: :object,
required: [:title],
properties: %{
title: %Schema{type: :string, description: "The title of the media playing"},
album: %Schema{type: :string, description: "The album of the media playing"},
artist: %Schema{type: :string, description: "The artist of the media playing"},
length: %Schema{type: :integer, description: "The length of the media playing"},
visibility: %Schema{
allOf: [VisibilityScope],
default: "public",
description: "Scrobble visibility"
}
},
example: %{
"title" => "Some Title",
"artist" => "Some Artist",
"album" => "Some Album",
"length" => 180_000
}
}
end
defp scrobble do
%Schema{
type: :object,
properties: %{
id: %Schema{type: :string},
account: Account,
title: %Schema{type: :string, description: "The title of the media playing"},
album: %Schema{type: :string, description: "The album of the media playing"},
artist: %Schema{type: :string, description: "The artist of the media playing"},
length: %Schema{
type: :integer,
description: "The length of the media playing",
nullable: true
},
created_at: %Schema{type: :string, format: :"date-time"}
},
example: %{
"id" => "1234",
"account" => Account.schema().example,
"title" => "Some Title",
"artist" => "Some Artist",
"album" => "Some Album",
"length" => 180_000,
"created_at" => "2019-09-28T12:40:45.000Z"
}
}
end
end

View file

@ -0,0 +1,76 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.PollOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.Schemas.Poll
import Pleroma.Web.ApiSpec.Helpers
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def show_operation do
%Operation{
tags: ["Polls"],
summary: "View a poll",
security: [%{"oAuth" => ["read:statuses"]}],
parameters: [id_param()],
operationId: "PollController.show",
responses: %{
200 => Operation.response("Poll", "application/json", Poll),
404 => Operation.response("Error", "application/json", ApiError)
}
}
end
def vote_operation do
%Operation{
tags: ["Polls"],
summary: "Vote on a poll",
parameters: [id_param()],
operationId: "PollController.vote",
requestBody: vote_request(),
security: [%{"oAuth" => ["write:statuses"]}],
responses: %{
200 => Operation.response("Poll", "application/json", Poll),
422 => Operation.response("Error", "application/json", ApiError),
404 => Operation.response("Error", "application/json", ApiError)
}
}
end
defp id_param do
Operation.parameter(:id, :path, FlakeID, "Poll ID",
example: "123",
required: true
)
end
defp vote_request do
request_body(
"Parameters",
%Schema{
type: :object,
properties: %{
choices: %Schema{
type: :array,
items: %Schema{type: :integer},
description: "Array of own votes containing index for each option (starting from 0)"
}
},
required: [:choices]
},
required: true,
example: %{
"choices" => [0, 1, 2]
}
)
end
end

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.ApiSpec.ReportOperation do
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Helpers
alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
@ -37,15 +38,18 @@ defmodule Pleroma.Web.ApiSpec.ReportOperation do
account_id: %Schema{type: :string, description: "ID of the account to report"},
status_ids: %Schema{
type: :array,
nullable: true,
items: %Schema{type: :string},
description: "Array of Statuses to attach to the report, for context"
},
comment: %Schema{
type: :string,
nullable: true,
description: "Reason for the report"
},
forward: %Schema{
type: :boolean,
allOf: [BooleanLike],
nullable: true,
default: false,
description:
"If the account is remote, should the report be forwarded to the remote admin?"

View file

@ -0,0 +1,96 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.ScheduledActivityOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.Schemas.ScheduledStatus
import Pleroma.Web.ApiSpec.Helpers
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def index_operation do
%Operation{
tags: ["Scheduled Statuses"],
summary: "View scheduled statuses",
security: [%{"oAuth" => ["read:statuses"]}],
parameters: pagination_params(),
operationId: "ScheduledActivity.index",
responses: %{
200 =>
Operation.response("Array of ScheduledStatus", "application/json", %Schema{
type: :array,
items: ScheduledStatus
})
}
}
end
def show_operation do
%Operation{
tags: ["Scheduled Statuses"],
summary: "View a single scheduled status",
security: [%{"oAuth" => ["read:statuses"]}],
parameters: [id_param()],
operationId: "ScheduledActivity.show",
responses: %{
200 => Operation.response("Scheduled Status", "application/json", ScheduledStatus),
404 => Operation.response("Error", "application/json", ApiError)
}
}
end
def update_operation do
%Operation{
tags: ["Scheduled Statuses"],
summary: "Schedule a status",
operationId: "ScheduledActivity.update",
security: [%{"oAuth" => ["write:statuses"]}],
parameters: [id_param()],
requestBody:
request_body("Parameters", %Schema{
type: :object,
properties: %{
scheduled_at: %Schema{
type: :string,
format: :"date-time",
description:
"ISO 8601 Datetime at which the status will be published. Must be at least 5 minutes into the future."
}
}
}),
responses: %{
200 => Operation.response("Scheduled Status", "application/json", ScheduledStatus),
404 => Operation.response("Error", "application/json", ApiError)
}
}
end
def delete_operation do
%Operation{
tags: ["Scheduled Statuses"],
summary: "Cancel a scheduled status",
security: [%{"oAuth" => ["write:statuses"]}],
parameters: [id_param()],
operationId: "ScheduledActivity.delete",
responses: %{
200 => Operation.response("Empty object", "application/json", %Schema{type: :object}),
404 => Operation.response("Error", "application/json", ApiError)
}
}
end
defp id_param do
Operation.parameter(:id, :path, FlakeID, "Poll ID",
example: "123",
required: true
)
end
end

View file

@ -0,0 +1,208 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.SearchOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.AccountOperation
alias Pleroma.Web.ApiSpec.Schemas.Account
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.Schemas.Status
alias Pleroma.Web.ApiSpec.Schemas.Tag
import Pleroma.Web.ApiSpec.Helpers
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
# Note: `with_relationships` param is not supported (PleromaFE uses this op for autocomplete)
def account_search_operation do
%Operation{
tags: ["Search"],
summary: "Search for matching accounts by username or display name",
operationId: "SearchController.account_search",
parameters: [
Operation.parameter(:q, :query, %Schema{type: :string}, "What to search for",
required: true
),
Operation.parameter(
:limit,
:query,
%Schema{type: :integer, default: 40},
"Maximum number of results"
),
Operation.parameter(
:resolve,
:query,
%Schema{allOf: [BooleanLike], default: false},
"Attempt WebFinger lookup. Use this when `q` is an exact address."
),
Operation.parameter(
:following,
:query,
%Schema{allOf: [BooleanLike], default: false},
"Only include accounts that the user is following"
)
],
responses: %{
200 =>
Operation.response(
"Array of Account",
"application/json",
AccountOperation.array_of_accounts()
)
}
}
end
def search_operation do
%Operation{
tags: ["Search"],
summary: "Search results",
security: [%{"oAuth" => ["read:search"]}],
operationId: "SearchController.search",
deprecated: true,
parameters: [
Operation.parameter(
:account_id,
:query,
FlakeID,
"If provided, statuses returned will be authored only by this account"
),
Operation.parameter(
:type,
:query,
%Schema{type: :string, enum: ["accounts", "hashtags", "statuses"]},
"Search type"
),
Operation.parameter(:q, :query, %Schema{type: :string}, "The search query", required: true),
Operation.parameter(
:resolve,
:query,
%Schema{allOf: [BooleanLike], default: false},
"Attempt WebFinger lookup"
),
Operation.parameter(
:following,
:query,
%Schema{allOf: [BooleanLike], default: false},
"Only include accounts that the user is following"
),
Operation.parameter(
:offset,
:query,
%Schema{type: :integer},
"Offset"
),
with_relationships_param() | pagination_params()
],
responses: %{
200 => Operation.response("Results", "application/json", results())
}
}
end
def search2_operation do
%Operation{
tags: ["Search"],
summary: "Search results",
security: [%{"oAuth" => ["read:search"]}],
operationId: "SearchController.search2",
parameters: [
Operation.parameter(
:account_id,
:query,
FlakeID,
"If provided, statuses returned will be authored only by this account"
),
Operation.parameter(
:type,
:query,
%Schema{type: :string, enum: ["accounts", "hashtags", "statuses"]},
"Search type"
),
Operation.parameter(:q, :query, %Schema{type: :string}, "What to search for",
required: true
),
Operation.parameter(
:resolve,
:query,
%Schema{allOf: [BooleanLike], default: false},
"Attempt WebFinger lookup"
),
Operation.parameter(
:following,
:query,
%Schema{allOf: [BooleanLike], default: false},
"Only include accounts that the user is following"
),
with_relationships_param() | pagination_params()
],
responses: %{
200 => Operation.response("Results", "application/json", results2())
}
}
end
defp results2 do
%Schema{
title: "SearchResults",
type: :object,
properties: %{
accounts: %Schema{
type: :array,
items: Account,
description: "Accounts which match the given query"
},
statuses: %Schema{
type: :array,
items: Status,
description: "Statuses which match the given query"
},
hashtags: %Schema{
type: :array,
items: Tag,
description: "Hashtags which match the given query"
}
},
example: %{
"accounts" => [Account.schema().example],
"statuses" => [Status.schema().example],
"hashtags" => [Tag.schema().example]
}
}
end
defp results do
%Schema{
title: "SearchResults",
type: :object,
properties: %{
accounts: %Schema{
type: :array,
items: Account,
description: "Accounts which match the given query"
},
statuses: %Schema{
type: :array,
items: Status,
description: "Statuses which match the given query"
},
hashtags: %Schema{
type: :array,
items: %Schema{type: :string},
description: "Hashtags which match the given query"
}
},
example: %{
"accounts" => [Account.schema().example],
"statuses" => [Status.schema().example],
"hashtags" => ["cofe"]
}
}
end
end

View file

@ -0,0 +1,518 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.StatusOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.AccountOperation
alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.Schemas.ScheduledStatus
alias Pleroma.Web.ApiSpec.Schemas.Status
alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
import Pleroma.Web.ApiSpec.Helpers
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def index_operation do
%Operation{
tags: ["Statuses"],
summary: "Get multiple statuses by IDs",
security: [%{"oAuth" => ["read:statuses"]}],
parameters: [
Operation.parameter(
:ids,
:query,
%Schema{type: :array, items: FlakeID},
"Array of status IDs"
)
],
operationId: "StatusController.index",
responses: %{
200 => Operation.response("Array of Status", "application/json", array_of_statuses())
}
}
end
def create_operation do
%Operation{
tags: ["Statuses"],
summary: "Publish new status",
security: [%{"oAuth" => ["write:statuses"]}],
description: "Post a new status",
operationId: "StatusController.create",
requestBody: request_body("Parameters", create_request(), required: true),
responses: %{
200 =>
Operation.response(
"Status. When `scheduled_at` is present, ScheduledStatus is returned instead",
"application/json",
%Schema{oneOf: [Status, ScheduledStatus]}
),
422 => Operation.response("Bad Request", "application/json", ApiError)
}
}
end
def show_operation do
%Operation{
tags: ["Statuses"],
summary: "View specific status",
description: "View information about a status",
operationId: "StatusController.show",
security: [%{"oAuth" => ["read:statuses"]}],
parameters: [id_param()],
responses: %{
200 => status_response(),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
end
def delete_operation do
%Operation{
tags: ["Statuses"],
summary: "Delete status",
security: [%{"oAuth" => ["write:statuses"]}],
description: "Delete one of your own statuses",
operationId: "StatusController.delete",
parameters: [id_param()],
responses: %{
200 => empty_object_response(),
403 => Operation.response("Forbidden", "application/json", ApiError),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
end
def reblog_operation do
%Operation{
tags: ["Statuses"],
summary: "Boost",
security: [%{"oAuth" => ["write:statuses"]}],
description: "Share a status",
operationId: "StatusController.reblog",
parameters: [id_param()],
requestBody:
request_body("Parameters", %Schema{
type: :object,
properties: %{
visibility: %Schema{allOf: [VisibilityScope], default: "public"}
}
}),
responses: %{
200 => status_response(),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
end
def unreblog_operation do
%Operation{
tags: ["Statuses"],
summary: "Undo boost",
security: [%{"oAuth" => ["write:statuses"]}],
description: "Undo a reshare of a status",
operationId: "StatusController.unreblog",
parameters: [id_param()],
responses: %{
200 => status_response(),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
end
def favourite_operation do
%Operation{
tags: ["Statuses"],
summary: "Favourite",
security: [%{"oAuth" => ["write:favourites"]}],
description: "Add a status to your favourites list",
operationId: "StatusController.favourite",
parameters: [id_param()],
responses: %{
200 => status_response(),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
end
def unfavourite_operation do
%Operation{
tags: ["Statuses"],
summary: "Undo favourite",
security: [%{"oAuth" => ["write:favourites"]}],
description: "Remove a status from your favourites list",
operationId: "StatusController.unfavourite",
parameters: [id_param()],
responses: %{
200 => status_response(),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
end
def pin_operation do
%Operation{
tags: ["Statuses"],
summary: "Pin to profile",
security: [%{"oAuth" => ["write:accounts"]}],
description: "Feature one of your own public statuses at the top of your profile",
operationId: "StatusController.pin",
parameters: [id_param()],
responses: %{
200 => status_response(),
400 => Operation.response("Error", "application/json", ApiError)
}
}
end
def unpin_operation do
%Operation{
tags: ["Statuses"],
summary: "Unpin to profile",
security: [%{"oAuth" => ["write:accounts"]}],
description: "Unfeature a status from the top of your profile",
operationId: "StatusController.unpin",
parameters: [id_param()],
responses: %{
200 => status_response(),
400 => Operation.response("Error", "application/json", ApiError)
}
}
end
def bookmark_operation do
%Operation{
tags: ["Statuses"],
summary: "Bookmark",
security: [%{"oAuth" => ["write:bookmarks"]}],
description: "Privately bookmark a status",
operationId: "StatusController.bookmark",
parameters: [id_param()],
responses: %{
200 => status_response()
}
}
end
def unbookmark_operation do
%Operation{
tags: ["Statuses"],
summary: "Undo bookmark",
security: [%{"oAuth" => ["write:bookmarks"]}],
description: "Remove a status from your private bookmarks",
operationId: "StatusController.unbookmark",
parameters: [id_param()],
responses: %{
200 => status_response()
}
}
end
def mute_conversation_operation do
%Operation{
tags: ["Statuses"],
summary: "Mute conversation",
security: [%{"oAuth" => ["write:mutes"]}],
description: "Do not receive notifications for the thread that this status is part of.",
operationId: "StatusController.mute_conversation",
parameters: [id_param()],
responses: %{
200 => status_response(),
400 => Operation.response("Error", "application/json", ApiError)
}
}
end
def unmute_conversation_operation do
%Operation{
tags: ["Statuses"],
summary: "Unmute conversation",
security: [%{"oAuth" => ["write:mutes"]}],
description:
"Start receiving notifications again for the thread that this status is part of",
operationId: "StatusController.unmute_conversation",
parameters: [id_param()],
responses: %{
200 => status_response(),
400 => Operation.response("Error", "application/json", ApiError)
}
}
end
def card_operation do
%Operation{
tags: ["Statuses"],
deprecated: true,
summary: "Preview card",
description: "Deprecated in favor of card property inlined on Status entity",
operationId: "StatusController.card",
parameters: [id_param()],
security: [%{"oAuth" => ["read:statuses"]}],
responses: %{
200 =>
Operation.response("Card", "application/json", %Schema{
type: :object,
nullable: true,
properties: %{
type: %Schema{type: :string, enum: ["link", "photo", "video", "rich"]},
provider_name: %Schema{type: :string, nullable: true},
provider_url: %Schema{type: :string, format: :uri},
url: %Schema{type: :string, format: :uri},
image: %Schema{type: :string, nullable: true, format: :uri},
title: %Schema{type: :string},
description: %Schema{type: :string}
}
})
}
}
end
def favourited_by_operation do
%Operation{
tags: ["Statuses"],
summary: "Favourited by",
description: "View who favourited a given status",
operationId: "StatusController.favourited_by",
security: [%{"oAuth" => ["read:accounts"]}],
parameters: [id_param()],
responses: %{
200 =>
Operation.response(
"Array of Accounts",
"application/json",
AccountOperation.array_of_accounts()
),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
end
def reblogged_by_operation do
%Operation{
tags: ["Statuses"],
summary: "Boosted by",
description: "View who boosted a given status",
operationId: "StatusController.reblogged_by",
security: [%{"oAuth" => ["read:accounts"]}],
parameters: [id_param()],
responses: %{
200 =>
Operation.response(
"Array of Accounts",
"application/json",
AccountOperation.array_of_accounts()
),
404 => Operation.response("Not Found", "application/json", ApiError)
}
}
end
def context_operation do
%Operation{
tags: ["Statuses"],
summary: "Parent and child statuses",
description: "View statuses above and below this status in the thread",
operationId: "StatusController.context",
security: [%{"oAuth" => ["read:statuses"]}],
parameters: [id_param()],
responses: %{
200 => Operation.response("Context", "application/json", context())
}
}
end
def favourites_operation do
%Operation{
tags: ["Statuses"],
summary: "Favourited statuses",
description: "Statuses the user has favourited",
operationId: "StatusController.favourites",
parameters: pagination_params(),
security: [%{"oAuth" => ["read:favourites"]}],
responses: %{
200 => Operation.response("Array of Statuses", "application/json", array_of_statuses())
}
}
end
def bookmarks_operation do
%Operation{
tags: ["Statuses"],
summary: "Bookmarked statuses",
description: "Statuses the user has bookmarked",
operationId: "StatusController.bookmarks",
parameters: pagination_params(),
security: [%{"oAuth" => ["read:bookmarks"]}],
responses: %{
200 => Operation.response("Array of Statuses", "application/json", array_of_statuses())
}
}
end
def array_of_statuses do
%Schema{type: :array, items: Status, example: [Status.schema().example]}
end
defp create_request do
%Schema{
title: "StatusCreateRequest",
type: :object,
properties: %{
status: %Schema{
type: :string,
nullable: true,
description:
"Text content of the status. If `media_ids` is provided, this becomes optional. Attaching a `poll` is optional while `status` is provided."
},
media_ids: %Schema{
nullable: true,
type: :array,
items: %Schema{type: :string},
description: "Array of Attachment ids to be attached as media."
},
poll: %Schema{
nullable: true,
type: :object,
required: [:options],
properties: %{
options: %Schema{
type: :array,
items: %Schema{type: :string},
description: "Array of possible answers. Must be provided with `poll[expires_in]`."
},
expires_in: %Schema{
type: :integer,
nullable: true,
description:
"Duration the poll should be open, in seconds. Must be provided with `poll[options]`"
},
multiple: %Schema{
allOf: [BooleanLike],
nullable: true,
description: "Allow multiple choices?"
},
hide_totals: %Schema{
allOf: [BooleanLike],
nullable: true,
description: "Hide vote counts until the poll ends?"
}
}
},
in_reply_to_id: %Schema{
nullable: true,
allOf: [FlakeID],
description: "ID of the status being replied to, if status is a reply"
},
sensitive: %Schema{
allOf: [BooleanLike],
nullable: true,
description: "Mark status and attached media as sensitive?"
},
spoiler_text: %Schema{
type: :string,
nullable: true,
description:
"Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field."
},
scheduled_at: %Schema{
type: :string,
format: :"date-time",
nullable: true,
description:
"ISO 8601 Datetime at which to schedule a status. Providing this paramter will cause ScheduledStatus to be returned instead of Status. Must be at least 5 minutes in the future."
},
language: %Schema{
type: :string,
nullable: true,
description: "ISO 639 language code for this status."
},
# Pleroma-specific properties:
preview: %Schema{
allOf: [BooleanLike],
nullable: true,
description:
"If set to `true` the post won't be actually posted, but the status entitiy would still be rendered back. This could be useful for previewing rich text/custom emoji, for example"
},
content_type: %Schema{
type: :string,
nullable: true,
description:
"The MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint."
},
to: %Schema{
type: :array,
nullable: true,
items: %Schema{type: :string},
description:
"A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for for post visibility are not affected by this and will still apply"
},
visibility: %Schema{
nullable: true,
anyOf: [
VisibilityScope,
%Schema{type: :string, description: "`list:LIST_ID`", example: "LIST:123"}
],
description:
"Visibility of the posted status. Besides standard MastoAPI values (`direct`, `private`, `unlisted` or `public`) it can be used to address a List by setting it to `list:LIST_ID`"
},
expires_in: %Schema{
nullable: true,
type: :integer,
description:
"The number of seconds the posted activity should expire in. When a posted activity expires it will be deleted from the server, and a delete request for it will be federated. This needs to be longer than an hour."
},
in_reply_to_conversation_id: %Schema{
nullable: true,
type: :string,
description:
"Will reply to a given conversation, addressing only the people who are part of the recipient set of that conversation. Sets the visibility to `direct`."
}
},
example: %{
"status" => "What time is it?",
"sensitive" => "false",
"poll" => %{
"options" => ["Cofe", "Adventure"],
"expires_in" => 420
}
}
}
end
defp id_param do
Operation.parameter(:id, :path, FlakeID, "Status ID",
example: "9umDrYheeY451cQnEe",
required: true
)
end
defp status_response do
Operation.response("Status", "application/json", Status)
end
defp context do
%Schema{
title: "StatusContext",
description:
"Represents the tree around a given status. Used for reconstructing threads of statuses.",
type: :object,
required: [:ancestors, :descendants],
properties: %{
ancestors: array_of_statuses(),
descendants: array_of_statuses()
},
example: %{
"ancestors" => [Status.schema().example],
"descendants" => [Status.schema().example]
}
}
end
end

View file

@ -0,0 +1,227 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.SubscriptionOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Helpers
alias Pleroma.Web.ApiSpec.Schemas.ApiError
alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
alias Pleroma.Web.ApiSpec.Schemas.PushSubscription
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def create_operation do
%Operation{
tags: ["Push Subscriptions"],
summary: "Subscribe to push notifications",
description:
"Add a Web Push API subscription to receive notifications. Each access token can have one push subscription. If you create a new subscription, the old subscription is deleted.",
operationId: "SubscriptionController.create",
security: [%{"oAuth" => ["push"]}],
requestBody: Helpers.request_body("Parameters", create_request(), required: true),
responses: %{
200 => Operation.response("Push Subscription", "application/json", PushSubscription),
400 => Operation.response("Error", "application/json", ApiError),
403 => Operation.response("Error", "application/json", ApiError)
}
}
end
def show_operation do
%Operation{
tags: ["Push Subscriptions"],
summary: "Get current subscription",
description: "View the PushSubscription currently associated with this access token.",
operationId: "SubscriptionController.show",
security: [%{"oAuth" => ["push"]}],
responses: %{
200 => Operation.response("Push Subscription", "application/json", PushSubscription),
403 => Operation.response("Error", "application/json", ApiError),
404 => Operation.response("Error", "application/json", ApiError)
}
}
end
def update_operation do
%Operation{
tags: ["Push Subscriptions"],
summary: "Change types of notifications",
description:
"Updates the current push subscription. Only the data part can be updated. To change fundamentals, a new subscription must be created instead.",
operationId: "SubscriptionController.update",
security: [%{"oAuth" => ["push"]}],
requestBody: Helpers.request_body("Parameters", update_request(), required: true),
responses: %{
200 => Operation.response("Push Subscription", "application/json", PushSubscription),
403 => Operation.response("Error", "application/json", ApiError)
}
}
end
def delete_operation do
%Operation{
tags: ["Push Subscriptions"],
summary: "Remove current subscription",
description: "Removes the current Web Push API subscription.",
operationId: "SubscriptionController.delete",
security: [%{"oAuth" => ["push"]}],
responses: %{
200 => Operation.response("Empty object", "application/json", %Schema{type: :object}),
403 => Operation.response("Error", "application/json", ApiError),
404 => Operation.response("Error", "application/json", ApiError)
}
}
end
defp create_request do
%Schema{
title: "SubscriptionCreateRequest",
description: "POST body for creating a push subscription",
type: :object,
properties: %{
subscription: %Schema{
type: :object,
properties: %{
endpoint: %Schema{
type: :string,
description: "Endpoint URL that is called when a notification event occurs."
},
keys: %Schema{
type: :object,
properties: %{
p256dh: %Schema{
type: :string,
description:
"User agent public key. Base64 encoded string of public key of ECDH key using `prime256v1` curve."
},
auth: %Schema{
type: :string,
description: "Auth secret. Base64 encoded string of 16 bytes of random data."
}
},
required: [:p256dh, :auth]
}
},
required: [:endpoint, :keys]
},
data: %Schema{
nullable: true,
type: :object,
properties: %{
alerts: %Schema{
nullable: true,
type: :object,
properties: %{
follow: %Schema{
allOf: [BooleanLike],
nullable: true,
description: "Receive follow notifications?"
},
favourite: %Schema{
allOf: [BooleanLike],
nullable: true,
description: "Receive favourite notifications?"
},
reblog: %Schema{
allOf: [BooleanLike],
nullable: true,
description: "Receive reblog notifications?"
},
mention: %Schema{
allOf: [BooleanLike],
nullable: true,
description: "Receive mention notifications?"
},
poll: %Schema{
allOf: [BooleanLike],
nullable: true,
description: "Receive poll notifications?"
}
}
}
}
}
},
required: [:subscription],
example: %{
"subscription" => %{
"endpoint" => "https://example.com/example/1234",
"keys" => %{
"auth" => "8eDyX_uCN0XRhSbY5hs7Hg==",
"p256dh" =>
"BCIWgsnyXDv1VkhqL2P7YRBvdeuDnlwAPT2guNhdIoW3IP7GmHh1SMKPLxRf7x8vJy6ZFK3ol2ohgn_-0yP7QQA="
}
},
"data" => %{
"alerts" => %{
"follow" => true,
"mention" => true,
"poll" => false
}
}
}
}
end
defp update_request do
%Schema{
title: "SubscriptionUpdateRequest",
type: :object,
properties: %{
data: %Schema{
nullable: true,
type: :object,
properties: %{
alerts: %Schema{
nullable: true,
type: :object,
properties: %{
follow: %Schema{
allOf: [BooleanLike],
nullable: true,
description: "Receive follow notifications?"
},
favourite: %Schema{
allOf: [BooleanLike],
nullable: true,
description: "Receive favourite notifications?"
},
reblog: %Schema{
allOf: [BooleanLike],
nullable: true,
description: "Receive reblog notifications?"
},
mention: %Schema{
allOf: [BooleanLike],
nullable: true,
description: "Receive mention notifications?"
},
poll: %Schema{
allOf: [BooleanLike],
nullable: true,
description: "Receive poll notifications?"
}
}
}
}
}
},
example: %{
"data" => %{
"alerts" => %{
"follow" => true,
"favourite" => true,
"reblog" => true,
"mention" => true,
"poll" => true
}
}
}
}
end
end

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