stable
commit
ef55d24054
@ -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
|
@ -0,0 +1,207 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
| 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"
|
||||||
|
)
|
||||||
|
| 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
|
@ -0,0 +1,27 @@
|
|||||||
|
# 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.Schemas.Tag do
|
||||||
|
alias OpenApiSpex.Schema
|
||||||
|
|
||||||
|
require OpenApiSpex
|
||||||
|
|
||||||
|
OpenApiSpex.schema(%{
|
||||||
|
title: "Tag",
|
||||||
|
description: "Represents a hashtag used within the content of a status",
|
||||||
|
type: :object,
|
||||||
|
properties: %{
|
||||||
|
name: %Schema{type: :string, description: "The value of the hashtag after the # sign"},
|
||||||
|
url: %Schema{
|
||||||
|
type: :string,
|
||||||
|
format: :uri,
|
||||||
|
description: "A link to the hashtag on the instance"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
example: %{
|
||||||
|
name: "cofe",
|
||||||
|
url: "https://lain.com/tag/cofe"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
end
|
@ -0,0 +1,185 @@
|
|||||||
|
# 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.Transmogrifier.UndoHandlingTest do
|
||||||
|
use Pleroma.DataCase
|
||||||
|
|
||||||
|
alias Pleroma.Activity
|
||||||
|
alias Pleroma.Object
|
||||||
|
alias Pleroma.User
|
||||||
|
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||||
|
alias Pleroma.Web.CommonAPI
|
||||||
|
|
||||||
|
import Pleroma.Factory
|
||||||
|
|
||||||
|
test "it works for incoming emoji reaction undos" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
{:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
|
||||||
|
{:ok, reaction_activity} = CommonAPI.react_with_emoji(activity.id, user, "👌")
|
||||||
|
|
||||||
|
data =
|
||||||
|
File.read!("test/fixtures/mastodon-undo-like.json")
|
||||||
|
|> Poison.decode!()
|
||||||
|
|> Map.put("object", reaction_activity.data["id"])
|
||||||
|
|> Map.put("actor", user.ap_id)
|
||||||
|
|
||||||
|
{:ok, activity} = Transmogrifier.handle_incoming(data)
|
||||||
|
|
||||||
|
assert activity.actor == user.ap_id
|
||||||
|
assert activity.data["id"] == data["id"]
|
||||||
|
assert activity.data["type"] == "Undo"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it returns an error for incoming unlikes wihout a like activity" do
|
||||||
|
user = insert(:user)
|
||||||
|
{:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
|
||||||
|
|
||||||
|
data =
|
||||||
|
File.read!("test/fixtures/mastodon-undo-like.json")
|
||||||
|
|> Poison.decode!()
|
||||||
|
|> Map.put("object", activity.data["object"])
|
||||||
|
|
||||||
|
assert Transmogrifier.handle_incoming(data) == :error
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it works for incoming unlikes with an existing like activity" do
|
||||||
|
user = insert(:user)
|
||||||
|
{:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
|
||||||
|
|
||||||
|
like_data =
|
||||||
|
File.read!("test/fixtures/mastodon-like.json")
|
||||||
|
|> Poison.decode!()
|
||||||
|
|> Map.put("object", activity.data["object"])
|
||||||
|
|
||||||
|
_liker = insert(:user, ap_id: like_data["actor"], local: false)
|
||||||
|
|
||||||
|
{:ok, %Activity{data: like_data, local: false}} = Transmogrifier.handle_incoming(like_data)
|
||||||
|
|
||||||
|
data =
|
||||||
|
File.read!("test/fixtures/mastodon-undo-like.json")
|
||||||
|
|> Poison.decode!()
|
||||||
|
|> Map.put("object", like_data)
|
||||||
|
|> Map.put("actor", like_data["actor"])
|
||||||
|
|
||||||
|
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
|
||||||
|
|
||||||
|
assert data["actor"] == "http://mastodon.example.org/users/admin"
|
||||||
|
assert data["type"] == "Undo"
|
||||||
|
assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/undo"
|
||||||
|
assert data["object"] == "http://mastodon.example.org/users/admin#likes/2"
|
||||||
|
|
||||||
|
note = Object.get_by_ap_id(like_data["object"])
|
||||||
|
assert note.data["like_count"] == 0
|
||||||
|
assert note.data["likes"] == []
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it works for incoming unlikes with an existing like activity and a compact object" do
|
||||||
|
user = insert(:user)
|
||||||
|
{:ok, activity} = CommonAPI.post(user, %{"status" => "leave a like pls"})
|
||||||
|
|
||||||
|
like_data =
|
||||||
|
File.read!("test/fixtures/mastodon-like.json")
|
||||||
|
|> Poison.decode!()
|
||||||
|
|> Map.put("object", activity.data["object"])
|
||||||
|
|
||||||
|
_liker = insert(:user, ap_id: like_data["actor"], local: false)
|
||||||
|
|
||||||
|
{:ok, %Activity{data: like_data, local: false}} = Transmogrifier.handle_incoming(like_data)
|
||||||
|
|
||||||
|
data =
|
||||||
|
File.read!("test/fixtures/mastodon-undo-like.json")
|
||||||
|
|> Poison.decode!()
|
||||||
|
|> Map.put("object", like_data["id"])
|
||||||
|
|> Map.put("actor", like_data["actor"])
|
||||||
|
|
||||||
|
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
|
||||||
|
|
||||||
|
assert data["actor"] == "http://mastodon.example.org/users/admin"
|
||||||
|
assert data["type"] == "Undo"
|
||||||
|
assert data["id"] == "http://mastodon.example.org/users/admin#likes/2/undo"
|
||||||
|
assert data["object"] == "http://mastodon.example.org/users/admin#likes/2"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it works for incoming unannounces with an existing notice" do
|
||||||
|
user = insert(:user)
|
||||||
|
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
|
||||||
|
|
||||||
|
announce_data =
|
||||||
|
File.read!("test/fixtures/mastodon-announce.json")
|
||||||
|
|> Poison.decode!()
|
||||||
|
|> Map.put("object", activity.data["object"])
|
||||||
|
|
||||||
|
_announcer = insert(:user, ap_id: announce_data["actor"], local: false)
|
||||||
|
|
||||||
|
{:ok, %Activity{data: announce_data, local: false}} =
|
||||||
|
Transmogrifier.handle_incoming(announce_data)
|
||||||
|
|
||||||
|
data =
|
||||||
|
File.read!("test/fixtures/mastodon-undo-announce.json")
|
||||||
|
|> Poison.decode!()
|
||||||
|
|> Map.put("object", announce_data)
|
||||||
|
|> Map.put("actor", announce_data["actor"])
|
||||||
|
|
||||||
|
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
|
||||||
|
|
||||||
|
assert data["type"] == "Undo"
|
||||||
|
|
||||||
|
assert data["object"] ==
|
||||||
|
"http://mastodon.example.org/users/admin/statuses/99542391527669785/activity"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it works for incomming unfollows with an existing follow" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
follow_data =
|
||||||
|
File.read!("test/fixtures/mastodon-follow-activity.json")
|
||||||
|
|> Poison.decode!()
|
||||||
|
|> Map.put("object", user.ap_id)
|
||||||
|
|
||||||
|
_follower = insert(:user, ap_id: follow_data["actor"], local: false)
|
||||||
|
|
||||||
|
{:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(follow_data)
|
||||||
|
|
||||||
|
data =
|
||||||
|
File.read!("test/fixtures/mastodon-unfollow-activity.json")
|
||||||
|
|> Poison.decode!()
|
||||||
|
|> Map.put("object", follow_data)
|
||||||
|
|
||||||
|
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
|
||||||
|
|
||||||
|
assert data["type"] == "Undo"
|
||||||
|
assert data["object"]["type"] == "Follow"
|
||||||
|
assert data["object"]["object"] == user.ap_id
|
||||||
|
assert data["actor"] == "http://mastodon.example.org/users/admin"
|
||||||
|
|
||||||
|
refute User.following?(User.get_cached_by_ap_id(data["actor"]), user)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "it works for incoming unblocks with an existing block" do
|
||||||
|
user = insert(:user)
|
||||||
|
|
||||||
|
block_data =
|
||||||
|
File.read!("test/fixtures/mastodon-block-activity.json")
|
||||||
|
|> Poison.decode!()
|
||||||
|
|> Map.put("object", user.ap_id)
|
||||||
|
|
||||||
|
_blocker = insert(:user, ap_id: block_data["actor"], local: false)
|
||||||
|
|
||||||
|
{:ok, %Activity{data: _, local: false}} = Transmogrifier.handle_incoming(block_data)
|
||||||
|
|
||||||
|
data =
|
||||||
|
File.read!("test/fixtures/mastodon-unblock-activity.json")
|
||||||
|
|> Poison.decode!()
|
||||||
|
|> Map.put("object", block_data)
|
||||||
|
|
||||||
|
{:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
|
||||||
|
assert data["type"] == "Undo"
|
||||||
|
assert data["object"] == block_data["id"]
|
||||||
|
|
||||||
|
blocker = User.get_cached_by_ap_id(data["actor"])
|
||||||
|
|
||||||
|
refute User.blocks?(blocker, user)
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in new issue