Compare commits

...

3 Commits

Author SHA1 Message Date
Lain Soykaf
d18ba133b2 Groups: Basic group validation. 2021-01-28 09:33:30 +01:00
Lain Soykaf
633d0286b3 Group: List members, add them, remove them. 2021-01-22 19:45:52 +01:00
Lain Soykaf
821b36f06c Groups: Add basic groups. 2021-01-22 15:29:43 +01:00
11 changed files with 351 additions and 1 deletions

View File

@ -9,7 +9,8 @@ defenum(Pleroma.UserRelationship.Type,
mute: 2,
reblog_mute: 3,
notification_mute: 4,
inverse_subscription: 5
inverse_subscription: 5,
membership: 6
)
defenum(Pleroma.FollowingRelationship.State,

106
lib/pleroma/group.ex Normal file
View File

@ -0,0 +1,106 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Group do
use Ecto.Schema
import Ecto.Changeset
alias Pleroma.User
alias Pleroma.Repo
alias Pleroma.Web
alias Pleroma.UserRelationship
@moduledoc """
Groups contain all the additional information about a group that's not stored
in the user table.
Concepts:
- Groups have an owner
- Groups have members, invited by the owner.
"""
@type t :: %__MODULE__{}
@primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true}
schema "groups" do
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:owner, User, type: FlakeId.Ecto.CompatType, foreign_key: :owner_id)
has_many(:members, through: [:user, :group_members])
field(:name, :string)
field(:description, :string)
field(:members_collection, :string)
timestamps()
end
@spec create(map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()}
def create(params) do
with {:ok, user} <- generate_user() do
%__MODULE__{user_id: user.id, members_collection: "#{user.ap_id}/members"}
|> changeset(params)
|> Repo.insert()
end
end
defp generate_ap_id(id) do
"#{Web.base_url()}/groups/#{id}"
end
defp generate_user() do
id = Ecto.UUID.generate()
ap_id = generate_ap_id(id)
%{
ap_id: ap_id,
name: id,
nickname: id,
follower_address: "#{ap_id}/followers",
following_address: "#{ap_id}/following",
local: true
}
|> User.group_changeset()
|> Repo.insert()
end
def changeset(struct, params) do
struct
|> cast(params, [:user_id, :owner_id, :name, :description, :members_collection])
|> validate_required([:user_id, :owner_id, :members_collection])
end
def is_member?(%{user_id: user_id}, member) do
UserRelationship.membership_exists?(%User{id: user_id}, member)
end
def members(group) do
Repo.preload(group, :members).members
end
def add_member(%{user_id: user_id} = group, member) do
with {:ok, _relationship} <- UserRelationship.create_membership(%User{id: user_id}, member) do
{:ok, group}
end
end
def remove_member(%{user_id: user_id} = group, member) do
with {:ok, _relationship} <- UserRelationship.delete_membership(%User{id: user_id}, member) do
{:ok, group}
end
end
@spec get_for_object(map()) :: t() | nil
def get_for_object(%{"type" => "Group", "id" => id}) do
with %User{} = user <- User.get_cached_by_ap_id(id),
group <- Repo.preload(user, :group).group do
group
end
end
def get_for_object(%{"type" => "Create", "object" => object}), do: get_for_object(object)
def get_for_object(_), do: nil
end

View File

@ -18,6 +18,7 @@ defmodule Pleroma.User do
alias Pleroma.Emoji
alias Pleroma.FollowingRelationship
alias Pleroma.Formatter
alias Pleroma.Group
alias Pleroma.HTML
alias Pleroma.Keys
alias Pleroma.MFA
@ -58,6 +59,10 @@ defmodule Pleroma.User do
# AP ID user relationships (blocks, mutes etc.)
# Format: [rel_type: [outgoing_rel: :outgoing_rel_target, incoming_rel: :incoming_rel_source]]
@user_relationships_config [
membership: [
group_memberships: :group_members,
user_memberships: :joined_groups
],
block: [
blocker_blocks: :blocked_users,
blockee_blocks: :blocker_users
@ -205,6 +210,9 @@ defmodule Pleroma.User do
on_replace: :delete
)
# Some `users` are actually groups. In this case, they can have a corresponding `Group`
has_one(:group, Group)
timestamps()
end
@ -407,6 +415,12 @@ defmodule Pleroma.User do
defp fix_follower_address(params), do: params
def group_changeset(struct \\ %User{actor_type: "Group"}, params) do
struct
|> cast(params, [:ap_id, :nickname, :name, :follower_address, :following_address, :local])
|> validate_required([:ap_id, :nickname, :name, :follower_address, :following_address, :local])
end
def remote_user_changeset(struct \\ %User{local: false}, params) do
bio_limit = Config.get([:instance, :user_bio_length], 5000)
name_limit = Config.get([:instance, :user_name_length], 100)

View File

@ -12,6 +12,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do
alias Pleroma.Emoji
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
@ -105,6 +106,30 @@ defmodule Pleroma.Web.ActivityPub.Builder do
}, []}
end
def group(owner, name \\ nil, description \\ nil) do
id = Ecto.UUID.generate()
ap_id = "#{Web.base_url()}/groups/#{id}"
{:ok,
%{
"id" => ap_id,
"type" => "Group",
"name" => name,
"summary" => description,
"following" => "#{ap_id}/following",
"followers" => "#{ap_id}/followers",
"members" => "#{ap_id}/members",
# attributedTo? owner? admin?
"attributedTo" => owner.ap_id
}, []}
end
def create_group(owner, params \\ %{}) do
with {:ok, group, _} <- group(owner, params[:name], params[:description]) do
create(owner, group, [])
end
end
def create(actor, object, recipients) do
context =
if is_map(object) do

View File

@ -29,6 +29,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.EventValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.GroupValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator
@ -37,6 +38,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
@impl true
def validate(object, meta)
def validate(%{"type" => "Group"} = object, meta) do
with {:ok, object} <-
object
|> GroupValidator.cast_and_validate()
|> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object)
{:ok, object, meta}
end
end
def validate(%{"type" => type} = object, meta)
when type in ~w[Accept Reject] do
with {:ok, object} <-

View File

@ -0,0 +1,40 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.GroupValidator do
use Ecto.Schema
alias Pleroma.EctoType.ActivityPub.ObjectValidators
import Ecto.Changeset
@primary_key false
embedded_schema do
field(:id, ObjectValidators.ObjectID, primary_key: true)
field(:type, :string)
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, ["Group"])
|> validate_required([:id])
end
end

View File

@ -30,6 +30,13 @@ defmodule Pleroma.Web.CommonAPI do
end
end
def create_group(user, params) do
with {:ok, group_data, _} <- Builder.create_group(user, params),
{:ok, group, _} <- Pipeline.common_pipeline(group_data, local: true) do
{:ok, group}
end
end
def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do
with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
:ok <- validate_chat_content_length(content, !!maybe_attachment),

View File

@ -0,0 +1,16 @@
defmodule Pleroma.Repo.Migrations.CreateGroups do
use Ecto.Migration
def change do
create table(:groups, primary_key: false) do
add(:id, :uuid, primary_key: true)
add(:user_id, references(:users, type: :uuid, on_delete: :delete_all), null: false)
add(:owner_id, references(:users, type: :uuid, on_delete: :nilify_all))
add(:name, :text)
add(:description, :text)
add(:members_collection, :text)
timestamps()
end
end
end

View File

@ -0,0 +1,81 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.GroupTest do
use Pleroma.DataCase, async: true
alias Pleroma.Group
alias Pleroma.Repo
import Pleroma.Factory
test "get_for_object/1 gets a group based on the group object or the create activity" do
user = insert(:user)
{:ok, group} = Group.create(%{owner_id: user.id, name: "cofe", description: "corndog"})
group = Repo.preload(group, :user)
group_object = %{
"id" => group.user.ap_id,
"type" => "Group"
}
assert group.id == Group.get_for_object(group_object).id
# Same works if wrapped in a 'create'
group_create = %{
"type" => "Create",
"object" => group_object
}
assert group.id == Group.get_for_object(group_create).id
# Nil for nonsense
assert nil == Group.get_for_object(%{"nothing" => "PS4 games"})
end
test "a user can create a group" do
user = insert(:user)
{:ok, group} = Group.create(%{owner_id: user.id, name: "cofe", description: "corndog"})
group = Repo.preload(group, :user)
assert group.user.actor_type == "Group"
assert group.owner_id == user.id
assert group.name == "cofe"
assert group.description == "corndog"
# Deleting the owner does not delete the group, just orphans it
Repo.delete(user)
group =
Repo.get(Group, group.id)
|> Repo.preload(:user)
assert group.owner_id == nil
# Deleting the group user deletes the group
Repo.delete(group.user)
refute Repo.get(Group, group.id)
end
test "group members can be seen and added" do
user = insert(:user)
other_user = insert(:user)
third_user = insert(:user)
{:ok, group} = Group.create(%{owner_id: user.id, name: "cofe", description: "corndog"})
assert [] == Group.members(group)
{:ok, group} = Group.add_member(group, other_user)
assert [other_user] == Group.members(group)
assert Group.is_member?(group, other_user)
refute Group.is_member?(group, third_user)
{:ok, group} = Group.remove_member(group, other_user)
refute Group.is_member?(group, other_user)
end
end

View File

@ -0,0 +1,24 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.GroupValidationTest do
use Pleroma.DataCase, async: true
import Pleroma.Factory
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.ObjectValidator
describe "Group objects" do
test "it validates a group" do
user = insert(:user)
{:ok, group_data, []} = Builder.group(user, "a group", "a description")
{:ok, group, _} = ObjectValidator.validate(group_data, [])
assert group
end
end
end

View File

@ -0,0 +1,25 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CommonAPI.GroupMessagingTest do
use Pleroma.DataCase
import Pleroma.Factory
alias Pleroma.Group
alias Pleroma.Web.CommonAPI
describe "Group chats" do
test "local chat" do
user = insert(:user)
{:ok, group_creation_activity} =
CommonAPI.create_group(user, %{name: "cofe", description: "for cofe enthusiasts"})
group = Group.get_for_object(group_creation_activity)
assert group
end
end
end