Compare commits
3 Commits
feature/sa
...
groups
Author | SHA1 | Date | |
---|---|---|---|
|
d18ba133b2 | ||
|
633d0286b3 | ||
|
821b36f06c |
@ -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
106
lib/pleroma/group.ex
Normal 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
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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} <-
|
||||
|
@ -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
|
@ -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),
|
||||
|
16
priv/repo/migrations/20210113150220_create_groups.exs
Normal file
16
priv/repo/migrations/20210113150220_create_groups.exs
Normal 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
|
81
test/pleroma/group_test.exs
Normal file
81
test/pleroma/group_test.exs
Normal 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
|
@ -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
|
25
test/pleroma/web/common_api/group_messaging_test.exs
Normal file
25
test/pleroma/web/common_api/group_messaging_test.exs
Normal 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
|
Loading…
Reference in New Issue
Block a user