Merge branch 'bites' into fork

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-08-27 17:36:20 +02:00
commit bd079c7002
21 changed files with 330 additions and 14 deletions

1
.gitattributes vendored
View file

@ -2,6 +2,7 @@
*.exs diff=elixir
priv/static/instance/static.css diff=css
priv/static/schemas/litepub-0.1.jsonld diff
# Most of js/css files included in the repo are minified bundles,
# and we don't want to search/diff those as text files.

View file

@ -79,6 +79,7 @@ def unread_notifications_count(%User{id: user_id}) do
pleroma:participation_request
pleroma:event_reminder
pleroma:event_update
bite
}
def changeset(%Notification{} = notification, attrs) do
@ -390,7 +391,8 @@ def create_notifications(%Activity{data: %{"type" => type}} = activity)
"Flag",
"Update",
"Accept",
"Join"
"Join",
"Bite"
] do
do_create_notifications(activity)
end
@ -462,6 +464,9 @@ defp type_from_activity(%{data: %{"type" => type}} = activity) do
"Join" ->
"pleroma:participation_request"
"Bite" ->
"bite"
t ->
raise "No notification type for activity type #{t}"
end
@ -562,7 +567,8 @@ def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, lo
"Flag",
"Update",
"Accept",
"Join"
"Join",
"Bite"
] do
potential_receiver_ap_ids = get_potential_receiver_ap_ids(activity)

View file

@ -478,4 +478,15 @@ def event(%ActivityDraft{} = draft) do
{:ok, data, []}
end
def bite(%User{} = biting, %User{} = bitten) do
{:ok,
%{
"id" => Utils.generate_activity_id(),
"target" => bitten.ap_id,
"actor" => biting.ap_id,
"type" => "Bite",
"to" => [bitten.ap_id]
}, []}
end
end

View file

@ -22,6 +22,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.AudioImageVideoValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.BiteValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
@ -191,7 +192,7 @@ def validate(
def validate(%{"type" => type} = object, meta)
when type in ~w[Accept Reject Follow Update Like EmojiReact Announce
ChatMessage Answer Join Leave] do
ChatMessage Answer Join Leave Bite] do
validator =
case type do
"Accept" -> AcceptRejectValidator
@ -205,6 +206,7 @@ def validate(%{"type" => type} = object, meta)
"Answer" -> AnswerValidator
"Join" -> JoinValidator
"Leave" -> LeaveValidator
"Bite" -> BiteValidator
end
with {:ok, object} <-

View file

@ -33,7 +33,7 @@ defp validate_data(cng) do
|> validate_required([:id, :type, :actor, :to, :object])
|> validate_inclusion(:type, ["Accept", "Reject"])
|> validate_actor_presence()
|> validate_object_presence(allowed_types: ["Follow", "Join"])
|> validate_object_presence(allowed_types: ["Follow", "Join", "Bite"])
|> validate_accept_reject_rights()
end
@ -63,4 +63,8 @@ defp validate_actor(%Activity{data: %{"type" => "Join", "object" => joined_event
%Object{data: %{"actor" => event_author}} = Object.get_cached_by_ap_id(joined_event)
event_author == actor
end
defp validate_actor(%Activity{data: %{"type" => "Bite", "target" => biten_actor}}, actor) do
biten_actor == actor
end
end

View file

@ -0,0 +1,49 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.BiteValidator do
use Ecto.Schema
alias Pleroma.EctoType.ActivityPub.ObjectValidators
import Ecto.Changeset
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
@primary_key false
embedded_schema do
quote do
unquote do
import Elixir.Pleroma.Web.ActivityPub.ObjectValidators.CommonFields
message_fields()
activity_fields()
end
end
field(:target, ObjectValidators.ObjectID)
end
def cast_data(data) do
%__MODULE__{}
|> cast(data |> fix_object(), __schema__(:fields))
end
defp fix_object(data) do
Map.put(data, "object", data["target"])
end
defp validate_data(cng) do
cng
|> validate_required([:id, :type, :actor, :to, :target])
|> validate_inclusion(:type, ["Bite"])
|> validate_actor_presence()
|> validate_actor_presence(field_name: :target)
end
def cast_and_validate(data) do
data
|> cast_data
|> validate_data
end
end

View file

@ -458,6 +458,56 @@ def handle(%{actor: actor_id, data: %{"type" => "Leave", "object" => event_id}}
end
end
# Task this handles
# - Bites
# - Sends a notification
@impl true
def handle(
%{
data: %{
"id" => bite_id,
"type" => "Bite",
"target" => bitten_user,
"actor" => biting_user
}
} = object,
meta
) do
with %User{} = biting <- User.get_cached_by_ap_id(biting_user),
%User{} = bitten <- User.get_cached_by_ap_id(bitten_user),
{:previous_bite, previous_bite} <-
{:previous_bite, Utils.fetch_latest_bite(biting, bitten, object)},
{:reverse_bite, reverse_bite} <-
{:reverse_bite, Utils.fetch_latest_bite(bitten, biting)},
{:can_bite, true, _} <- {:can_bite, can_bite?(previous_bite, reverse_bite), bitten} do
if bitten.local do
{:ok, accept_data, _} = Builder.accept(bitten, object)
{:ok, _activity, _} = Pipeline.common_pipeline(accept_data, local: true)
end
if reverse_bite do
Notification.dismiss(reverse_bite)
end
{:ok, notifications} = Notification.create_notifications(object)
meta
|> add_notifications(notifications)
else
{:can_bite, false, bitten} ->
{:ok, reject_data, _} = Builder.reject(bitten, object)
{:ok, _activity, _} = Pipeline.common_pipeline(reject_data, local: true)
meta
_ ->
meta
end
updated_object = Activity.get_by_ap_id(bite_id)
{:ok, updated_object, meta}
end
# Nothing to do
@impl true
def handle(object, meta) do
@ -487,6 +537,8 @@ defp handle_accepted(
end
end
defp handle_accepted(_, _), do: nil
defp handle_rejected(
%Activity{actor: follower_id, data: %{"type" => "Follow"}} = follow_activity,
actor
@ -510,6 +562,10 @@ defp handle_rejected(
end
end
defp handle_rejected(%Activity{data: %{"type" => "Bite"}} = bite_activity, _actor) do
Notification.dismiss(bite_activity)
end
defp handle_update_user(
%{data: %{"type" => "Update", "object" => updated_object}} = object,
meta
@ -726,4 +782,12 @@ def handle_after_transaction(meta) do
|> stream_notifications()
|> send_streamables()
end
defp can_bite?(nil, _), do: true
defp can_bite?(_, nil), do: false
defp can_bite?(previous_bite, reverse_bite) do
NaiveDateTime.diff(previous_bite.inserted_at, reverse_bite.inserted_at) < 0
end
end

View file

@ -512,7 +512,7 @@ def handle_incoming(
%{"type" => type} = data,
_options
)
when type in ~w{Update Block Follow Accept Reject Join Leave} do
when type in ~w{Update Block Follow Accept Reject Join Leave Bite} do
with {:ok, %User{}} <- ObjectValidator.fetch_actor(data),
{:ok, activity, _} <-
Pipeline.common_pipeline(data, local: false) do

View file

@ -1048,4 +1048,36 @@ def update_join_state(
{:ok, activity}
end
end
def make_bite_data(biting, bitten, activity_id) do
%{
"type" => "Bite",
"actor" => biting.ap_id,
"to" => [bitten.ap_id],
"target" => bitten.ap_id
}
|> Maps.put_if_present("id", activity_id)
end
def fetch_latest_bite(
%User{ap_id: biting_ap_id},
%{ap_id: bitten_ap_id},
exclude_activity \\ nil
) do
"Bite"
|> Activity.Queries.by_type()
|> where(actor: ^biting_ap_id)
|> maybe_exclude_activity_id(exclude_activity)
|> Activity.Queries.by_object_id(bitten_ap_id)
|> order_by([activity], fragment("? desc nulls last", activity.id))
|> limit(1)
|> Repo.one()
end
defp maybe_exclude_activity_id(query, nil), do: query
defp maybe_exclude_activity_id(query, %Activity{id: activity_id}) do
query
|> where([a], a.id != ^activity_id)
end
end

View file

@ -87,7 +87,7 @@ def spec(opts \\ []) do
"x-tagGroups": [
%{
"name" => "Accounts",
"tags" => ["Account actions", "Retrieve account information", "Scrobbles"]
"tags" => ["Account actions", "Bites", "Retrieve account information", "Scrobbles"]
},
%{
"name" => "Administration",

View file

@ -0,0 +1,33 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.BiteOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.ApiError
@spec open_api_operation(atom) :: Operation.t()
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def bite_operation do
%Operation{
tags: ["Bites"],
summary: "Bite",
operationId: "BiteController.bite",
security: [%{"oAuth" => ["write:bites"]}],
description: "Bite the given account",
parameters: [
Operation.parameter(:id, :query, :string, "Bitten account ID")
],
responses: %{
200 => Operation.response("Empty object", "application/json", %Schema{type: :object}),
400 => Operation.response("Error", "application/json", ApiError),
404 => Operation.response("Error", "application/json", ApiError)
}
}
end
end

View file

@ -214,7 +214,8 @@ defp notification_type do
"pleroma:event_reminder",
"pleroma:event_update",
"admin.sign_up",
"admin.report"
"admin.report",
"bite"
],
description: """
The type of event that resulted in the notification.
@ -237,6 +238,7 @@ defp notification_type do
- `pleroma:participation_accepted - Your event participation request was accepted
- `admin.sign_up` - Someone signed up (optionally sent to admins)
- `admin.report` - A new report has been filed
- `bite` - Someone bit you
"""
}
end

View file

@ -859,4 +859,15 @@ defp maybe_cancel_jobs(%Activity{id: activity_id}) do
end
defp maybe_cancel_jobs(_), do: {:ok, 0}
def bite(biting, bitten) do
with {:ok, bite_data, _} <- Builder.bite(biting, bitten),
{:ok, activity, _} <- Pipeline.common_pipeline(bite_data, local: true) do
if activity.data["state"] == "reject" do
{:error, :rejected}
else
{:ok, biting, bitten, activity}
end
end
end
end

View file

@ -0,0 +1,39 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2024 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.BiteController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [assign_account_by_id: 2, json_response: 3]
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Plugs.OAuthScopesPlug
# alias Pleroma.Web.Plugs.RateLimiter
plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(OAuthScopesPlug, %{scopes: ["write:bite"]} when action == :bite)
# plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
# plug(RateLimiter, [name: :app_account_creation] when action == :create)
plug(:assign_account_by_id)
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.BiteOperation
@doc "POST /api/v1/bite"
def bite(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
{:error, "Can not bite yourself"}
end
def bite(%{assigns: %{user: biting, account: bitten}} = conn, _) do
with {:ok, _, _, _} <- CommonAPI.bite(biting, bitten) do
json_response(conn, :ok, %{})
else
{:error, message} -> json_response(conn, :forbidden, %{error: message})
end
end
end

View file

@ -39,6 +39,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
pleroma:participation_accepted
pleroma:event_reminder
pleroma:event_update
bite
}
# GET /api/v1/notifications

View file

@ -181,7 +181,8 @@ def features do
"language_detection"
end,
"events",
"multitenancy"
"multitenancy",
"pleroma:bites"
]
|> Enum.filter(& &1)
end

View file

@ -149,7 +149,7 @@ def render(
|> put_status(create_activity, reading_user, status_render_opts)
|> put_participation_request(activity)
type when type in ["follow", "follow_request"] ->
type when type in ["follow", "follow_request", "bite"] ->
response
end
end

View file

@ -74,6 +74,10 @@ def get_nodeinfo("2.0") do
features: features,
restrictedNicknames: Config.get([Pleroma.User, :restricted_nicknames]),
skipThreadContainment: Config.get([:instance, :skip_thread_containment], false)
},
operations: %{
"com.shinolabs.api.bite": ["1.0.0"],
"jetzt.mia.ns.activitypub.accept.bite": ["1.0.0"]
}
}
end

View file

@ -890,6 +890,8 @@ defmodule Pleroma.Web.Router do
get("/announcements", AnnouncementController, :index)
post("/announcements/:id/dismiss", AnnouncementController, :mark_read)
post("/bite", BiteController, :bite)
end
scope "/api/v1", Pleroma.Web.MastodonAPI do

View file

@ -0,0 +1,57 @@
defmodule Pleroma.Repo.Migrations.AddBiteToNotificationsEnum do
use Ecto.Migration
@disable_ddl_transaction true
def up do
"""
alter type notification_type add value 'bite'
"""
|> execute()
end
# 20220819171321_add_pleroma_participation_accepted_to_notifications_enum.exs
def down do
alter table(:notifications) do
modify(:type, :string)
end
"""
delete from notifications where type = 'bite'
"""
|> execute()
"""
drop type if exists notification_type
"""
|> execute()
"""
create type notification_type as enum (
'follow',
'follow_request',
'mention',
'move',
'pleroma:emoji_reaction',
'pleroma:chat_mention',
'reblog',
'favourite',
'pleroma:report',
'poll',
'status',
'update',
'pleroma:participation_accepted',
'pleroma:participation_request',
'pleroma:event_reminder',
'pleroma:event_update'
)
"""
|> execute()
"""
alter table notifications
alter column type type notification_type using (type::notification_type)
"""
|> execute()
end
end

View file

@ -43,7 +43,6 @@
"vcard": "http://www.w3.org/2006/vcard/ns#",
"formerRepresentations": "litepub:formerRepresentations",
"sm": "http://smithereen.software/ns#",
<<<<<<< HEAD
"nonAnonymous": "sm:nonAnonymous",
"votersCount": "toot:votersCount",
"mz": "https://joinmobilizon.org/ns#",
@ -67,10 +66,8 @@
"location": {
"@id": "schema:location",
"@type": "schema:Place"
}
=======
"nonAnonymous": "sm:nonAnonymous"
>>>>>>> origin/develop
},
"Bite": "https://ns.mia.jetzt/as#Bite"
}
]
}