mirror of https://gitlab.com/ngerakines/tavern.git
207 lines
10 KiB
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)
|
|
}
|