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