Live video on the AT Protocol
1package moderation
2
3import (
4 "context"
5 "fmt"
6 "time"
7
8 "stream.place/streamplace/pkg/streamplace"
9)
10
11type delegationGetter interface {
12 GetModerationDelegations(ctx context.Context, streamerDID, moderatorDID string) ([]*streamplace.ModerationDefs_PermissionView, error)
13}
14
15// Permission scope constants
16const (
17 PermissionBan = "ban"
18 PermissionHide = "hide"
19 PermissionLivestreamManage = "livestream.manage"
20)
21
22// ActionPermissions maps moderation actions to required permissions
23var ActionPermissions = map[string]string{
24 "createBlock": PermissionBan,
25 "deleteBlock": PermissionBan,
26 "createGate": PermissionHide,
27 "deleteGate": PermissionHide,
28 "updateLivestream": PermissionLivestreamManage,
29}
30
31// PermissionChecker validates moderation permissions
32type PermissionChecker struct {
33 model delegationGetter
34}
35
36// NewPermissionChecker creates a new permission checker
37func NewPermissionChecker(m delegationGetter) *PermissionChecker {
38 return &PermissionChecker{model: m}
39}
40
41// CheckPermission validates that a moderator has permission to perform an action
42// Returns an error if the moderator lacks the required permission
43func (pc *PermissionChecker) CheckPermission(ctx context.Context, moderatorDID, streamerDID, action string) error {
44 // Get required permission for this action (validate action first)
45 requiredPermission, ok := ActionPermissions[action]
46 if !ok {
47 return fmt.Errorf("unknown action: %s", action)
48 }
49
50 // Streamers always have permission for their own content
51 if moderatorDID == streamerDID {
52 return nil
53 }
54
55 // Check if moderator has the required permission
56 hasPermission, err := pc.HasPermission(ctx, moderatorDID, streamerDID, requiredPermission)
57 if err != nil {
58 return fmt.Errorf("failed to check permissions: %w", err)
59 }
60
61 if !hasPermission {
62 return fmt.Errorf("moderator %s does not have permission '%s' for streamer %s", moderatorDID, requiredPermission, streamerDID)
63 }
64
65 return nil
66}
67
68// HasPermission checks if a moderator has a specific permission for a streamer.
69// It merges permissions from ALL delegation records for the moderator.
70func (pc *PermissionChecker) HasPermission(ctx context.Context, moderatorDID, streamerDID, permission string) (bool, error) {
71 // Streamers always have all permissions for their own content
72 if moderatorDID == streamerDID {
73 return true, nil
74 }
75
76 // Look up ALL delegation records for this moderator
77 delegations, err := pc.model.GetModerationDelegations(ctx, streamerDID, moderatorDID)
78 if err != nil {
79 return false, fmt.Errorf("failed to get moderation delegations: %w", err)
80 }
81
82 if len(delegations) == 0 {
83 return false, nil
84 }
85
86 // Check all delegation records and merge their permissions
87 for _, delegationView := range delegations {
88 // Extract the actual permission record from the view
89 permRecord, ok := delegationView.Record.Val.(*streamplace.ModerationPermission)
90 if !ok {
91 return false, fmt.Errorf("failed to cast record to ModerationPermission")
92 }
93
94 // Skip expired delegations
95 if permRecord.ExpirationTime != nil {
96 expirationTime, err := time.Parse(time.RFC3339, *permRecord.ExpirationTime)
97 if err != nil {
98 return false, fmt.Errorf("failed to parse expiration time: %w", err)
99 }
100 if time.Now().After(expirationTime) {
101 continue
102 }
103 }
104
105 // Check if this delegation has the required permission
106 for _, p := range permRecord.Permissions {
107 if p == permission {
108 return true, nil
109 }
110 }
111 }
112
113 return false, nil
114}