tavern/storage/groups.go

376 lines
18 KiB
Go

package storage
import (
"context"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"strings"
"time"
"github.com/gofrs/uuid"
"github.com/ngerakines/tavern/common"
"github.com/ngerakines/tavern/errors"
)
type GroupStorage interface {
CountGroupMembers(ctx context.Context, groupActorRowID uuid.UUID) (int, error)
GetGroupByID(ctx context.Context, groupRowID uuid.UUID) (Group, error)
GetGroupByName(ctx context.Context, name string) (Group, error)
GetGroupsByOwner(ctx context.Context, userRowID uuid.UUID) ([]Group, error)
GroupActorsForMemberActorRowID(ctx context.Context, groupActorRowID uuid.UUID) ([]*Actor, error)
GroupMemberActorIDs(ctx context.Context, groupActorRowID uuid.UUID) ([]string, error)
GroupMemberActorsForGroupActorID(ctx context.Context, groupActorRowID uuid.UUID) ([]*Actor, error)
GroupMemberCanInvite(ctx context.Context, groupActorRowID, memberActorRowID uuid.UUID) (bool, error)
GroupMemberCanSubmit(ctx context.Context, groupActorRowID, memberActorRowID uuid.UUID) (bool, error)
GroupNameAvailable(context.Context, string) (bool, error)
IsActorInGroup(ctx context.Context, groupActorRowID, memberActorRowID uuid.UUID) (bool, error)
IsActorInvitedToGroup(ctx context.Context, groupActorRowID, memberActorRowID uuid.UUID) (bool, error)
ListGroups(ctx context.Context) ([]Group, error)
MinutesSinceGroupBoost(ctx context.Context, groupActorRowID, objectRowID uuid.UUID) (int, error)
RecordGroup(ctx context.Context, userRowID, actorRowID uuid.UUID, name, publicKey, privateKey, displayName, about string) (uuid.UUID, error)
RecordGroupAll(ctx context.Context, rowID uuid.UUID, createdAt, updatedAt time.Time, userRowID, actorRowID uuid.UUID, name, publicKey, privateKey, displayName, about string) (uuid.UUID, error)
RecordGroupBoost(ctx context.Context, groupActorRowID, objectRowID uuid.UUID) (uuid.UUID, error)
RecordGroupBoostAll(ctx context.Context, rowID uuid.UUID, createdAt, updatedAt time.Time, groupActorRowID, objectRowID uuid.UUID) (uuid.UUID, error)
RecordGroupInvitation(ctx context.Context, groupActorRowID, memberActorRowID uuid.UUID) (uuid.UUID, error)
RecordGroupInvitationAll(ctx context.Context, rowID uuid.UUID, createdAt, updatedAt time.Time, groupActorRowID, memberActorRowID uuid.UUID) (uuid.UUID, error)
RecordGroupMember(ctx context.Context, groupActorRowID, memberActorRowID uuid.UUID, activity Payload, status RelationshipStatus, role GroupRole) (uuid.UUID, error)
RecordGroupMemberAll(ctx context.Context, rowID uuid.UUID, createdAt, updatedAt time.Time, groupActorRowID, memberActorRowID uuid.UUID, activity Payload, status RelationshipStatus, role GroupRole) (uuid.UUID, error)
RemoveGroupInvitation(ctx context.Context, groupActorRowID, memberActorRowID uuid.UUID) error
RemoveGroupMember(ctx context.Context, groupActorRowID, memberActorRowID uuid.UUID) error
UpdateGroupAbout(ctx context.Context, groupRowID uuid.UUID, about string) error
UpdateGroupAcceptFollowers(ctx context.Context, groupRowID uuid.UUID, acceptFollowers bool) error
UpdateGroupAllowRemote(ctx context.Context, groupRowID uuid.UUID, allowRemote bool) error
UpdateGroupDefaultRole(ctx context.Context, groupActorRowID uuid.UUID, role GroupRole) error
UpdateGroupDirectoryOptIn(ctx context.Context, groupRowID uuid.UUID, value bool) error
UpdateGroupMemberRole(ctx context.Context, groupActorRowID, memberActorRowID uuid.UUID, role GroupRole) error
UpdateGroupMemberStatus(ctx context.Context, groupActorRowID, memberActorRowID uuid.UUID, status RelationshipStatus) error
ListGroupsPaginated(ctx context.Context, limit, offset int) ([]*Actor, error)
CountGroupsMembers(ctx context.Context, groupActorRowIDs []uuid.UUID) ([]Count, error)
}
type Group struct {
ID uuid.UUID
CreatedAt time.Time
UpdatedAt time.Time
Name string
PublicKey string
PrivateKey string
DisplayName string
About string
AcceptFollowers bool
DefaultMemberRole GroupRole
AllowRemote bool
DirectoryOptIn bool
OwnerID uuid.UUID
ActorID uuid.UUID
}
type GroupRole int16
const (
// GroupOwner can manage the group.
GroupOwner GroupRole = 2
// GroupMember can post to the group.
GroupMember GroupRole = 1
// GroupViewer can receive group activities, but cannot create group activities.
GroupViewer GroupRole = 0
)
var groupFields = []string{
"id",
"created_at",
"updated_at",
"name",
"public_key",
"private_key",
"display_name",
"about",
"accept_followers",
"default_member_role",
"allow_remote",
"directory_opt_in",
"owner",
"actor_id",
}
func (g Group) GetPrivateKey() (*rsa.PrivateKey, error) {
block, _ := pem.Decode([]byte(g.PrivateKey))
if block == nil {
return nil, errors.New("invalid RSA PEM")
}
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
return key, nil
}
func (g Group) GetDecodedPublicKey() (*rsa.PublicKey, error) {
return DecodePublicKey(g.PublicKey)
}
func (s pgStorage) RecordGroup(ctx context.Context, userRowID, actorRowID uuid.UUID, name, publicKey, privateKey, displayName, about string) (uuid.UUID, error) {
rowID := NewV4()
now := s.now()
return s.RecordGroupAll(ctx, rowID, now, now, userRowID, actorRowID, name, publicKey, privateKey, displayName, about)
}
func (s pgStorage) RecordGroupAll(ctx context.Context, rowID uuid.UUID, createdAt, updatedAt time.Time, userRowID, actorRowID uuid.UUID, name, publicKey, privateKey, displayName, about string) (uuid.UUID, error) {
query := `INSERT INTO groups (id, created_at, updated_at, name, public_key, private_key, display_name, about, owner, actor_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT ON CONSTRAINT groups_name_uindex DO UPDATE set updated_at = $3 RETURNING id`
var id uuid.UUID
err := s.db.QueryRowContext(ctx, query, rowID, createdAt, updatedAt, name, publicKey, privateKey, displayName, about, userRowID, actorRowID).Scan(&id)
return id, errors.WrapGroupUpsertFailedError(err)
}
func (s pgStorage) RecordGroupMember(ctx context.Context, groupActorRowID, memberActorRowID uuid.UUID, activity Payload, status RelationshipStatus, role GroupRole) (uuid.UUID, error) {
rowID := NewV4()
now := s.now()
return s.RecordGroupMemberAll(ctx, rowID, now, now, groupActorRowID, memberActorRowID, activity, status, role)
}
func (s pgStorage) RecordGroupMemberAll(ctx context.Context, rowID uuid.UUID, createdAt, updatedAt time.Time, groupActorRowID, memberActorRowID uuid.UUID, activity Payload, status RelationshipStatus, role GroupRole) (uuid.UUID, error) {
query := `INSERT INTO group_members (id, created_at, updated_at, group_actor_id, member_actor_id, activity, relationship_status, member_role) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT ON CONSTRAINT group_members_uindex DO UPDATE set updated_at = $3 RETURNING id`
var id uuid.UUID
err := s.db.QueryRowContext(ctx, query, rowID, createdAt, updatedAt, groupActorRowID, memberActorRowID, activity, status, role).Scan(&id)
return id, errors.WrapGroupMemberUpsertFailedError(err)
}
func (s pgStorage) GetGroupByName(ctx context.Context, name string) (Group, error) {
query := fmt.Sprintf("SELECT %s FROM groups WHERE name = $1", strings.Join(groupFields, ", "))
return s.getFirstGroup(ctx, query, name)
}
func (s pgStorage) GetGroupByID(ctx context.Context, groupRowID uuid.UUID) (Group, error) {
query := fmt.Sprintf("SELECT %s FROM groups WHERE id = $1", strings.Join(groupFields, ", "))
return s.getFirstGroup(ctx, query, groupRowID)
}
func (s pgStorage) GetGroupsByOwner(ctx context.Context, userRowID uuid.UUID) ([]Group, error) {
query := fmt.Sprintf("SELECT %s FROM groups WHERE owner = $1", strings.Join(groupFields, ", "))
return s.getGroups(ctx, query, userRowID)
}
func (s pgStorage) ListGroups(ctx context.Context) ([]Group, error) {
query := fmt.Sprintf("SELECT %s FROM groups ORDER BY name ASC", strings.Join(groupFields, ", "))
return s.getGroups(ctx, query)
}
func (s pgStorage) getFirstGroup(ctx context.Context, query string, args ...interface{}) (Group, error) {
groups, err := s.getGroups(ctx, query, args...)
if err != nil {
return Group{}, err
}
if len(groups) == 0 {
return Group{}, errors.NewGroupNotFoundError(nil)
}
return groups[0], nil
}
func (s pgStorage) getGroups(ctx context.Context, query string, args ...interface{}) ([]Group, error) {
results := make([]Group, 0)
rows, err := s.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, errors.NewGroupSelectFailedError(err)
}
defer rows.Close()
for rows.Next() {
var group Group
err = rows.Scan(&group.ID,
&group.CreatedAt,
&group.UpdatedAt,
&group.Name,
&group.PublicKey,
&group.PrivateKey,
&group.DisplayName,
&group.About,
&group.AcceptFollowers,
&group.DefaultMemberRole,
&group.AllowRemote,
&group.DirectoryOptIn,
&group.OwnerID,
&group.ActorID)
if err != nil {
return nil, errors.NewInvalidGroupError(err)
}
results = append(results, group)
}
return results, nil
}
func (s pgStorage) GroupNameAvailable(ctx context.Context, name string) (bool, error) {
queries := []string{
`SELECT COUNT(*) FROM users WHERE name = $1`,
`SELECT COUNT(*) FROM groups WHERE name = $1`,
}
for _, query := range queries {
count, err := s.wrappedRowCount(errors.WrapUserQueryFailedError, ctx, query, name)
if err != nil {
return false, err
}
if count > 0 {
return false, nil
}
}
return true, nil
}
func (s pgStorage) CountGroupMembers(ctx context.Context, groupActorRowID uuid.UUID) (int, error) {
return s.wrappedRowCount(errors.WrapGroupQueryFailedError, ctx, `SELECT COUNT(*) FROM group_members WHERE group_actor_id = $1`, groupActorRowID)
}
func (s pgStorage) CountGroupsMembers(ctx context.Context, groupActorRowIDs []uuid.UUID) ([]Count, error) {
if len(groupActorRowIDs) == 0 {
return []Count{}, nil
}
placeholders := common.DollarForEach(len(groupActorRowIDs))
query := fmt.Sprintf(`SELECT group_actor_id, COUNT(*) FROM group_members WHERE group_actor_id IN (%s) GROUP BY group_actor_id`, strings.Join(placeholders, ","))
return s.keyedCount(errors.WrapObjectBoostQueryFailedError, ctx, query, common.UUIDsToInterfaces(groupActorRowIDs)...)
}
func (s pgStorage) GroupMemberActorIDs(ctx context.Context, groupActorRowID uuid.UUID) ([]string, error) {
query := `SELECT a.actor_id FROM group_members gm INNER JOIN actors a ON a.id = gm.member_actor_id WHERE gm.group_actor_id = $1 ORDER BY a.actor_id ASC`
rows, err := s.db.QueryContext(ctx, query, groupActorRowID)
if err != nil {
return nil, errors.NewGroupMemberSelectFailedError(err)
}
defer rows.Close()
var actors []string
for rows.Next() {
var actor string
if err := rows.Scan(&actor); err != nil {
return nil, errors.NewGroupMemberSelectFailedError(errors.NewInvalidGroupMemberError(err))
}
actors = append(actors, actor)
}
return actors, nil
}
func (s pgStorage) GroupActorsForMemberActorRowID(ctx context.Context, memberActorRowID uuid.UUID) ([]*Actor, error) {
query := actorsSelectQuery("group_members gm ON a.id = gm.group_actor_id", []string{"gm.member_actor_id = $1"})
return s.getActors(ctx, query, memberActorRowID)
}
func (s pgStorage) GroupMemberActorsForGroupActorID(ctx context.Context, groupActorRowID uuid.UUID) ([]*Actor, error) {
query := actorsSelectQuery("group_members gm ON a.id = gm.member_actor_id", []string{"gm.group_actor_id = $1"})
return s.getActors(ctx, query, groupActorRowID)
}
func (s pgStorage) UpdateGroupMemberStatus(ctx context.Context, groupActorRowID, memberActorRowID uuid.UUID, status RelationshipStatus) error {
query := "UPDATE group_members SET relationship_status = $4, updated_at = $3 WHERE group_actor_id = $1 AND member_actor_id = $2"
now := s.now()
_, err := s.db.ExecContext(ctx, query, groupActorRowID, memberActorRowID, now, status)
return errors.WrapUserUpdateFailedError(err)
}
func (s pgStorage) UpdateGroupMemberRole(ctx context.Context, groupActorRowID, memberActorRowID uuid.UUID, role GroupRole) error {
now := s.now()
_, err := s.db.ExecContext(ctx, "UPDATE group_members SET member_role = $4, updated_at = $3 WHERE group_actor_id = $1 AND member_actor_id = $2", groupActorRowID, memberActorRowID, now, role)
return errors.WrapUserUpdateFailedError(err)
}
func (s pgStorage) RemoveGroupMember(ctx context.Context, groupActorRowID, memberActorRowID uuid.UUID) error {
query := `DELETE FROM group_members WHERE group_actor_id = $1 AND member_actor_id = $2`
_, err := s.db.ExecContext(ctx, query, groupActorRowID, memberActorRowID)
return errors.WrapGroupMemberUpdateFailedError(err)
}
func (s pgStorage) GroupMemberCanSubmit(ctx context.Context, groupActorRowID, memberActorRowID uuid.UUID) (bool, error) {
query := `SELECT COUNT(*) FROM group_members WHERE group_actor_id = $1 AND member_actor_id = $2 AND member_role IN ($3, $4)`
return s.wrappedExists(errors.WrapGroupMemberQueryFailedError, ctx, query, groupActorRowID, memberActorRowID, GroupMember, GroupOwner)
}
func (s pgStorage) GroupMemberCanInvite(ctx context.Context, groupActorRowID, memberActorRowID uuid.UUID) (bool, error) {
query := `SELECT COUNT(*) FROM group_members WHERE group_actor_id = $1 AND member_actor_id = $2 AND member_role IN ($3)`
return s.wrappedExists(errors.WrapGroupMemberQueryFailedError, ctx, query, groupActorRowID, memberActorRowID, GroupOwner)
}
func (s pgStorage) RecordGroupInvitation(ctx context.Context, groupActorRowID, memberActorRowID uuid.UUID) (uuid.UUID, error) {
rowID := NewV4()
now := s.now()
return s.RecordGroupInvitationAll(ctx, rowID, now, now, groupActorRowID, memberActorRowID)
}
func (s pgStorage) RecordGroupInvitationAll(ctx context.Context, rowID uuid.UUID, createdAt, updatedAt time.Time, groupActorRowID, memberActorRowID uuid.UUID) (uuid.UUID, error) {
query := `INSERT INTO group_invitations (id, created_at, updated_at, group_actor_id, member_actor_id) VALUES ($1, $2, $3, $4, $5) ON CONFLICT ON CONSTRAINT group_invitations_uindex DO UPDATE set updated_at = $3 RETURNING id`
var id uuid.UUID
err := s.db.QueryRowContext(ctx, query, rowID, createdAt, updatedAt, groupActorRowID, memberActorRowID).Scan(&id)
return id, errors.WrapGroupInvitationUpsertFailedError(err)
}
func (s pgStorage) IsActorInvitedToGroup(ctx context.Context, groupActorRowID, memberActorRowID uuid.UUID) (bool, error) {
return s.wrappedExists(errors.WrapGroupInvitationQueryFailedError, ctx, `SELECT COUNT(*) FROM group_invitations WHERE group_actor_id = $1 AND member_actor_id = $2`, groupActorRowID, memberActorRowID)
}
func (s pgStorage) RemoveGroupInvitation(ctx context.Context, groupActorRowID, memberActorRowID uuid.UUID) error {
_, err := s.db.ExecContext(ctx, `DELETE FROM group_invitations WHERE group_actor_id = $1 AND member_actor_id = $2`, groupActorRowID, memberActorRowID)
return errors.WrapGroupInvitationDeleteFailedError(err)
}
func (s pgStorage) IsActorInGroup(ctx context.Context, groupActorRowID, memberActorRowID uuid.UUID) (bool, error) {
return s.wrappedExists(errors.WrapGroupMemberQueryFailedError, ctx, `SELECT COUNT(*) FROM group_members WHERE group_actor_id = $1 AND member_actor_id = $2`, groupActorRowID, memberActorRowID)
}
func (s pgStorage) RecordGroupBoost(ctx context.Context, groupActorRowID, objectRowID uuid.UUID) (uuid.UUID, error) {
rowID := NewV4()
now := s.now()
return s.RecordGroupInvitationAll(ctx, rowID, now, now, groupActorRowID, objectRowID)
}
func (s pgStorage) RecordGroupBoostAll(ctx context.Context, rowID uuid.UUID, createdAt, updatedAt time.Time, groupActorRowID, objectRowID uuid.UUID) (uuid.UUID, error) {
query := `INSERT INTO group_boosts (id, created_at, updated_at, group_actor_id, object_id) VALUES ($1, $2, $3, $4, $5) ON CONFLICT ON CONSTRAINT group_boosts_uindex DO UPDATE set updated_at = $3 RETURNING id`
var id uuid.UUID
err := s.db.QueryRowContext(ctx, query, rowID, createdAt, updatedAt, groupActorRowID, objectRowID).Scan(&id)
return id, errors.WrapGroupInvitationUpsertFailedError(err)
}
func (s pgStorage) MinutesSinceGroupBoost(ctx context.Context, groupActorRowID, objectRowID uuid.UUID) (int, error) {
query := `SELECT (DATE_PART('day', NOW() - updated_at::timestamp) * 24 + DATE_PART('hour', NOW() - updated_at::timestamp)) * 60 + DATE_PART('minute', NOW() - updated_at::timestamp) FROM group_boosts WHERE group_actor_id = $1 AND object_id = $2`
return s.wrappedSelectInt(errors.WrapGroupBoostQueryFailedError, ctx, query, groupActorRowID, objectRowID)
}
func (s pgStorage) UpdateGroupDefaultRole(ctx context.Context, groupRowID uuid.UUID, role GroupRole) error {
now := s.now()
_, err := s.db.ExecContext(ctx, "UPDATE groups SET default_member_role = $3, updated_at = $2 WHERE id = $1", groupRowID, now, role)
return errors.WrapGroupUpdateFailedError(err)
}
func (s pgStorage) UpdateGroupAcceptFollowers(ctx context.Context, groupRowID uuid.UUID, acceptFollowers bool) error {
now := s.now()
_, err := s.db.ExecContext(ctx, "UPDATE groups SET accept_followers = $3, updated_at = $2 WHERE id = $1", groupRowID, now, acceptFollowers)
return errors.WrapGroupUpdateFailedError(err)
}
func (s pgStorage) UpdateGroupAllowRemote(ctx context.Context, groupRowID uuid.UUID, allowRemote bool) error {
now := s.now()
_, err := s.db.ExecContext(ctx, "UPDATE groups SET allow_remote = $3, updated_at = $2 WHERE id = $1", groupRowID, now, allowRemote)
return errors.WrapGroupUpdateFailedError(err)
}
func (s pgStorage) UpdateGroupAbout(ctx context.Context, groupRowID uuid.UUID, about string) error {
now := s.now()
_, err := s.db.ExecContext(ctx, "UPDATE groups SET about = $3, updated_at = $2 WHERE id = $1", groupRowID, now, about)
return errors.WrapGroupUpdateFailedError(err)
}
func (s pgStorage) UpdateGroupDirectoryOptIn(ctx context.Context, groupRowID uuid.UUID, value bool) error {
now := s.now()
_, err := s.db.ExecContext(ctx, "UPDATE groups SET directory_opt_in = $3, updated_at = $2 WHERE id = $1", groupRowID, now, value)
return errors.WrapGroupUpdateFailedError(err)
}
func (s pgStorage) ListGroupsPaginated(ctx context.Context, limit, offset int) ([]*Actor, error) {
query := actorsSelectQuery("groups g ON a.id = g.actor_id", nil) + " ORDER BY g.name ASC LIMIT $1 OFFSET $2"
return s.getActors(ctx, query, limit, offset)
}