tavern/storage/network.go

207 lines
10 KiB
Go

package storage
import (
"context"
"fmt"
"strings"
"time"
"github.com/gofrs/uuid"
"github.com/ngerakines/tavern/common"
"github.com/ngerakines/tavern/errors"
)
type FollowerStorage interface {
ActivityForFollower(ctx context.Context, userID, actorID uuid.UUID) (Payload, error)
ActivityForFollowing(ctx context.Context, userID, actorID uuid.UUID) (Payload, error)
CountFollowers(ctx context.Context, userID uuid.UUID) (int, error)
CountFollowing(ctx context.Context, userID uuid.UUID) (int, error)
CreatePendingFollower(ctx context.Context, userID, actorID uuid.UUID, activity Payload) error
CreatePendingFollowing(ctx context.Context, userID, actorID uuid.UUID, activity Payload) error
IsInNetwork(ctx context.Context, userID, actorID uuid.UUID) (bool, error)
IsFollower(ctx context.Context, userID, actorID uuid.UUID) (bool, error)
IsFollowing(ctx context.Context, userID, actorID uuid.UUID) (bool, error)
ListAcceptedFollowers(ctx context.Context, userID uuid.UUID, limit, offset int) ([]string, error)
ListAcceptedFollowing(ctx context.Context, userID uuid.UUID, limit, offset int) ([]string, error)
ListPendingFollowers(ctx context.Context, userID uuid.UUID, limit, offset int) ([]string, error)
ListPendingFollowing(ctx context.Context, userID uuid.UUID, limit, offset int) ([]string, error)
RemoveFollower(ctx context.Context, userID, actorID uuid.UUID) error
RemoveFollowing(ctx context.Context, userID, actorID uuid.UUID) error
UpdateFollowerApproved(ctx context.Context, userID, actorID uuid.UUID) error
UpdateFollowingAccepted(ctx context.Context, userID, actorID uuid.UUID) error
UpdateFollowingRejected(ctx context.Context, userID, actorID uuid.UUID) error
}
type NetworkRelationship struct {
ID uuid.UUID
RelationshipType RelationshipType
UserID uuid.UUID
ActorID uuid.UUID
RequestActivity Payload // Saved so we can undo, accept, and reject
Status int
CreatedAt time.Time
UpdatedAt time.Time
}
var relationshipGraphFields = []string{
"id",
"user_id",
"actor_id",
"activity",
"relationship_type",
"relationship_status",
"created_at",
"updated_at",
}
type RelationshipType int16
const (
UserFollowsRelationship RelationshipType = iota
UserFollowedByRelationship
)
type RelationshipStatus int16
const (
// PendingRelationshipStatus indicates either a) an incoming (remote actor
// to local actor) follow request has not been accepted or rejected by the
// local actor OR b) that the outgoing (local actor to remote actor)
// request has not been accepted or rejected by the remote actor.
PendingRelationshipStatus RelationshipStatus = iota
// AcceptRelationshipStatus indicates either a) an incoming (remote actor
// to local actor) follow request was accepted by the local actor OR b)
// that an outgoing (local actor to remote actor) follow request was
// accepted by the remote actor.
AcceptRelationshipStatus
// RejectRelationshipStatus indicates that an outgoing (local actor to
// remote actor) follow request was accepted by the remote actor. Incoming
// follow requests (remote actor to local actor) that rejected by the user
// (local actor) are deleted.
RejectRelationshipStatus
)
var _ FollowerStorage = &pgStorage{}
func (s pgStorage) networkGraphQuery(qc QueryExecute, ctx context.Context, userID uuid.UUID, relationshipType RelationshipType, relationshipStatus RelationshipStatus, limit, offset int) ([]string, error) {
query := `SELECT a.actor_id FROM network_graph n INNER JOIN actors a ON a.id = n.actor_id WHERE n.user_id = $3 AND n.relationship_type = $4 AND n.relationship_status = $5 ORDER BY n.created_at ASC LIMIT $1 OFFSET $2`
rows, err := qc.QueryContext(ctx, query, limit, offset, userID, relationshipType, relationshipStatus)
if err != nil {
return nil, errors.NewNetworkRelationshipSelectFailedError(err)
}
defer rows.Close()
var actors []string
for rows.Next() {
var actor string
if err := rows.Scan(&actor); err != nil {
return nil, errors.NewNetworkRelationshipSelectFailedError(errors.NewInvalidNetworkRelationshipError(err))
}
actors = append(actors, actor)
}
return actors, nil
}
func (s pgStorage) ListAcceptedFollowers(ctx context.Context, userID uuid.UUID, limit, offset int) ([]string, error) {
return s.networkGraphQuery(s.db, ctx, userID, UserFollowedByRelationship, AcceptRelationshipStatus, limit, offset)
}
func (s pgStorage) ListAcceptedFollowing(ctx context.Context, userID uuid.UUID, limit, offset int) ([]string, error) {
return s.networkGraphQuery(s.db, ctx, userID, UserFollowsRelationship, AcceptRelationshipStatus, limit, offset)
}
func (s pgStorage) ListPendingFollowers(ctx context.Context, userID uuid.UUID, limit, offset int) ([]string, error) {
return s.networkGraphQuery(s.db, ctx, userID, UserFollowedByRelationship, PendingRelationshipStatus, limit, offset)
}
func (s pgStorage) ListPendingFollowing(ctx context.Context, userID uuid.UUID, limit, offset int) ([]string, error) {
return s.networkGraphQuery(s.db, ctx, userID, UserFollowsRelationship, PendingRelationshipStatus, limit, offset)
}
func (s pgStorage) createRelationshipGraphRecord(ctx context.Context, userID, actorID uuid.UUID, activity Payload, relationshipType RelationshipType, relationshipStatus RelationshipStatus) error {
fields := strings.Join(relationshipGraphFields, ",")
valuesPlaceholder := strings.Join(common.DollarForEach(len(relationshipGraphFields)), ",")
query := fmt.Sprintf(`INSERT INTO network_graph (%s) VALUES (%s) ON CONFLICT ON CONSTRAINT network_graph_user_actor_rel DO UPDATE SET activity = $4, relationship_status = $6`, fields, valuesPlaceholder)
rowID := NewV4()
now := s.now()
_, err := s.db.ExecContext(ctx, query, rowID, userID, actorID, activity, relationshipType, relationshipStatus, now, now)
return errors.WrapNetworkRelationshipInsertFailedError(err)
}
func (s pgStorage) CreatePendingFollowing(ctx context.Context, userID, actorID uuid.UUID, activity Payload) error {
return s.createRelationshipGraphRecord(ctx, userID, actorID, activity, UserFollowsRelationship, PendingRelationshipStatus)
}
func (s pgStorage) CreatePendingFollower(ctx context.Context, userID, actorID uuid.UUID, activity Payload) error {
return s.createRelationshipGraphRecord(ctx, userID, actorID, activity, UserFollowedByRelationship, PendingRelationshipStatus)
}
func (s pgStorage) ActivityForFollowing(ctx context.Context, userID, actorID uuid.UUID) (Payload, error) {
return s.networkGraphActivity(s.db, ctx, userID, actorID, UserFollowsRelationship)
}
func (s pgStorage) ActivityForFollower(ctx context.Context, userID, actorID uuid.UUID) (Payload, error) {
return s.networkGraphActivity(s.db, ctx, userID, actorID, UserFollowedByRelationship)
}
func (s pgStorage) networkGraphActivity(qc QueryExecute, ctx context.Context, userID, actorID uuid.UUID, relationshipType RelationshipType) (Payload, error) {
var payload Payload
err := qc.QueryRowContext(ctx, `SELECT activity FROM network_graph WHERE user_id = $1 AND actor_id = $2 AND relationship_type = $3`, userID, actorID, relationshipType).
Scan(&payload)
if err != nil {
return nil, errors.WrapNetworkRelationshipSelectFailedError(err)
}
return payload, nil
}
func (s pgStorage) UpdateFollowingAccepted(ctx context.Context, userID, actorID uuid.UUID) error {
_, err := s.db.ExecContext(ctx, `UPDATE network_graph SET relationship_status = $4 WHERE user_id = $1 AND actor_id = $2 AND relationship_type = $3`, userID, actorID, UserFollowsRelationship, AcceptRelationshipStatus)
return errors.WrapNetworkRelationshipUpdateFailedError(err)
}
func (s pgStorage) UpdateFollowingRejected(ctx context.Context, userID, actorID uuid.UUID) error {
_, err := s.db.ExecContext(ctx, `UPDATE network_graph SET relationship_status = $4 WHERE user_id = $1 AND actor_id = $2 AND relationship_type = $3`, userID, actorID, UserFollowsRelationship, RejectRelationshipStatus)
return errors.WrapNetworkRelationshipUpdateFailedError(err)
}
func (s pgStorage) UpdateFollowerApproved(ctx context.Context, userID, actorID uuid.UUID) error {
_, err := s.db.ExecContext(ctx, `UPDATE network_graph SET relationship_status = $4 WHERE user_id = $1 AND actor_id = $2 AND relationship_type = $3`, userID, actorID, UserFollowedByRelationship, AcceptRelationshipStatus)
return errors.WrapNetworkRelationshipUpdateFailedError(err)
}
func (s pgStorage) RemoveFollowing(ctx context.Context, userID, actorID uuid.UUID) error {
_, err := s.db.ExecContext(ctx, `DELETE FROM network_graph WHERE user_id = $1 AND actor_id = $2 AND relationship_type = $3`, userID, actorID, UserFollowsRelationship)
return errors.WrapNetworkRelationshipUpdateFailedError(err)
}
func (s pgStorage) RemoveFollower(ctx context.Context, userID, actorID uuid.UUID) error {
_, err := s.db.ExecContext(ctx, `DELETE FROM network_graph WHERE user_id = $1 AND actor_id = $2 AND relationship_type = $3`, userID, actorID, UserFollowedByRelationship)
return errors.WrapNetworkRelationshipUpdateFailedError(err)
}
func (s pgStorage) IsInNetwork(ctx context.Context, userID, actorID uuid.UUID) (bool, error) {
c, err := s.wrappedRowCount(errors.WrapNetworkRelationshipQueryFailedError, ctx, `SELECT COUNT(*) FROM network_graph WHERE user_id = $1 AND actor_id = $2`, userID, actorID)
return c > 0, err
}
func (s pgStorage) IsFollowing(ctx context.Context, userID, actorID uuid.UUID) (bool, error) {
c, err := s.wrappedRowCount(errors.WrapNetworkRelationshipQueryFailedError, ctx, `SELECT COUNT(*) FROM network_graph WHERE user_id = $1 AND actor_id = $2 AND relationship_type = $3`, userID, actorID, UserFollowsRelationship)
return c == 1, err
}
func (s pgStorage) IsFollower(ctx context.Context, userID, actorID uuid.UUID) (bool, error) {
c, err := s.wrappedRowCount(errors.WrapNetworkRelationshipQueryFailedError, ctx, `SELECT COUNT(*) FROM network_graph WHERE user_id = $1 AND actor_id = $2 AND relationship_type = $3`, userID, actorID, UserFollowedByRelationship)
return c == 1, err
}
func (s pgStorage) CountFollowers(ctx context.Context, userID uuid.UUID) (int, error) {
return s.wrappedRowCount(errors.WrapNetworkRelationshipQueryFailedError, ctx, `SELECT COUNT(*) FROM network_graph WHERE user_id = $1 AND relationship_type = $2`, userID, UserFollowedByRelationship)
}
func (s pgStorage) CountFollowing(ctx context.Context, userID uuid.UUID) (int, error) {
return s.wrappedRowCount(errors.WrapNetworkRelationshipQueryFailedError, ctx, `SELECT COUNT(*) FROM network_graph WHERE user_id = $1 AND relationship_type = $2`, userID, UserFollowsRelationship)
}