You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
100 lines
3.2 KiB
100 lines
3.2 KiB
package app |
|
|
|
import ( |
|
"context" |
|
"errors" |
|
"fmt" |
|
"time" |
|
|
|
"lol.mleku.dev/chk" |
|
"lol.mleku.dev/log" |
|
"next.orly.dev/pkg/acl" |
|
"git.mleku.dev/mleku/nostr/crypto/ec/schnorr" |
|
"git.mleku.dev/mleku/nostr/encoders/envelopes/authenvelope" |
|
"git.mleku.dev/mleku/nostr/encoders/envelopes/countenvelope" |
|
"git.mleku.dev/mleku/nostr/utils/normalize" |
|
) |
|
|
|
// HandleCount processes a COUNT envelope by parsing the request, verifying |
|
// permissions, invoking the database CountEvents for each provided filter, and |
|
// responding with a COUNT response containing the aggregate count. |
|
func (l *Listener) HandleCount(msg []byte) (err error) { |
|
log.D.F("HandleCount: START processing from %s", l.remote) |
|
|
|
// Parse the COUNT request |
|
env := countenvelope.New() |
|
if _, err = env.Unmarshal(msg); chk.E(err) { |
|
return normalize.Error.Errorf(err.Error()) |
|
} |
|
log.D.C(func() string { return fmt.Sprintf("COUNT sub=%s filters=%d", env.Subscription, len(env.Filters)) }) |
|
|
|
// If ACL is active, auth is required, or AuthToWrite is enabled, send a challenge (same as REQ path) |
|
if len(l.authedPubkey.Load()) != schnorr.PubKeyBytesLen && (acl.Registry.Active.Load() != "none" || l.Config.AuthRequired || l.Config.AuthToWrite) { |
|
if err = authenvelope.NewChallengeWith(l.challenge.Load()).Write(l); chk.E(err) { |
|
return |
|
} |
|
} |
|
|
|
// Check read permissions |
|
accessLevel := acl.Registry.GetAccessLevel(l.authedPubkey.Load(), l.remote) |
|
|
|
// If auth is required but user is not authenticated, deny access |
|
if l.Config.AuthRequired && len(l.authedPubkey.Load()) == 0 { |
|
return errors.New("authentication required") |
|
} |
|
|
|
// If AuthToWrite is enabled, allow COUNT without auth (but still check ACL) |
|
if l.Config.AuthToWrite && len(l.authedPubkey.Load()) == 0 { |
|
// Allow unauthenticated COUNT when AuthToWrite is enabled |
|
// but still respect ACL access levels if ACL is active |
|
if acl.Registry.Active.Load() != "none" { |
|
switch accessLevel { |
|
case "none", "blocked", "banned": |
|
return errors.New("auth required: user not authed or has no read access") |
|
} |
|
} |
|
// Allow the request to proceed without authentication |
|
} else { |
|
// Only check ACL access level if not already handled by AuthToWrite |
|
switch accessLevel { |
|
case "none": |
|
return errors.New("auth required: user not authed or has no read access") |
|
default: |
|
// allowed to read |
|
} |
|
} |
|
|
|
// Use a bounded context for counting, isolated from the connection context |
|
// to prevent count timeouts from affecting the long-lived websocket connection |
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) |
|
defer cancel() |
|
|
|
// Aggregate count across all provided filters |
|
var total int |
|
var approx bool // database returns false per implementation |
|
for _, f := range env.Filters { |
|
if f == nil { |
|
continue |
|
} |
|
var cnt int |
|
var a bool |
|
cnt, a, err = l.DB.CountEvents(ctx, f) |
|
if chk.E(err) { |
|
return |
|
} |
|
total += cnt |
|
approx = approx || a |
|
} |
|
|
|
// Build and send COUNT response |
|
var res *countenvelope.Response |
|
if res, err = countenvelope.NewResponseFrom(env.Subscription, total, approx); chk.E(err) { |
|
return |
|
} |
|
if err = res.Write(l); chk.E(err) { |
|
return |
|
} |
|
|
|
log.D.F("HandleCount: COMPLETED processing from %s count=%d approx=%v", l.remote, total, approx) |
|
return nil |
|
}
|
|
|