clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

main
tinyrelay / internal / daemon / agent_self.go
agent_self.go 5.2K
1package daemon23import (4 "context"5 "encoding/json"6 "errors"7 "fmt"8 "strings"9 "time"1011 "github.com/FelineStateMachine/tinyrelay/internal/community"12)1314// browsegrant lets any signed caller see its own standing on the relay: its15// membership and role, the agent grant recorded for its key and what that16// grant covers. Unlike browseagent it needs no operator role, because a key17// only ever learns about itself here. The optional needs let a connector ask18// "may I post these kinds into these rooms" and get back what is missing.1920const grantNeedsMax = 2562122// grantNeeds is the optional first parameter of browsegrant.23type grantNeeds struct {24 Rooms []string `json:"rooms"`25 Kinds []int `json:"kinds"`26}2728// grantAllows is the scope of the caller's grant as the gate applies it.29type grantAllows struct {30 Kinds []int `json:"kinds"`31 Rooms []string `json:"rooms"`32 Repos []community.AgentRepo `json:"repos"`33 Wiki string `json:"wiki"`34 Jobs string `json:"jobs"`35 Sites []community.AgentSite `json:"sites"`36}3738// grantMissing lists the requested rooms and kinds the caller may not use.39type grantMissing struct {40 Rooms []string `json:"rooms"`41 Kinds []int `json:"kinds"`42}4344// grantSelf is the browsegrant result.45type grantSelf struct {46 Member bool `json:"member"`47 Role string `json:"role"`48 Grant *community.AgentSummary `json:"grant"`49 State string `json:"state"`50 Enforced bool `json:"enforced"`51 Allows *grantAllows `json:"allows,omitempty"`52 Rate int `json:"rate,omitempty"`53 Expires int64 `json:"expires,omitempty"`54 Missing *grantMissing `json:"missing,omitempty"`55}5657// grantState names the effective state of one grant.58func grantState(grant community.AgentGrant, now int64) string {59 switch {60 case grant.RevokedAt > 0:61 return "revoked"62 case grant.Paused:63 return "paused"64 case grant.ExpiresAt <= now:65 return "expired"66 }67 return "active"68}6970func parseGrantNeeds(params []json.RawMessage) (grantNeeds, bool, error) {71 var needs grantNeeds72 if len(params) == 0 || len(params[0]) == 0 || string(params[0]) == "null" {73 return needs, false, nil74 }75 if err := json.Unmarshal(params[0], &needs); err != nil {76 return needs, false, fmt.Errorf("invalid: browsegrant parameters: %w", err)77 }78 if len(needs.Rooms) > grantNeedsMax || len(needs.Kinds) > grantNeedsMax {79 return needs, false, fmt.Errorf("invalid: browsegrant lists more than %d entries", grantNeedsMax)80 }81 for i, room := range needs.Rooms {82 room = strings.TrimSpace(room)83 if room == "" {84 return needs, false, errors.New("invalid: browsegrant rooms must be room ids")85 }86 needs.Rooms[i] = room87 }88 for _, kind := range needs.Kinds {89 if kind < 0 || kind > 65535 {90 return needs, false, errors.New("invalid: browsegrant kinds must be event kinds")91 }92 }93 return needs, len(needs.Rooms) > 0 || len(needs.Kinds) > 0, nil94}9596// browseGrant returns the caller's own grant and effective state. A key97// without a grant gets state "none". A key with a human role is a member in98// its own right; the gate does not apply a grant to its events, so nothing99// is reported missing for it.100func (t *Tenant) browseGrant(ctx context.Context, actor string, params []json.RawMessage) (any, error) {101 needs, asked, err := parseGrantNeeds(params)102 if err != nil {103 return nil, err104 }105 result := grantSelf{State: "none"}106 if actor == "" {107 if asked {108 result.Missing = &grantMissing{Rooms: nonNilStrings(needs.Rooms), Kinds: nonNilInts(needs.Kinds)}109 }110 return result, nil111 }112 role, err := t.community.Role(ctx, actor)113 if err != nil {114 return nil, err115 }116 result.Role = role117 result.Member = role != ""118 result.Enforced = role == "" || role == "agent"119 grant, ok, err := t.community.AgentGrant(ctx, actor)120 if err != nil {121 return nil, err122 }123 now := time.Now().Unix()124 if ok {125 summary := community.AgentSummary{AgentGrant: grant}126 _ = t.store.DB().QueryRowContext(ctx, `SELECT coalesce(max(created_at),0) FROM events WHERE pubkey=?`, actor).Scan(&summary.LastEvent)127 result.Grant = &summary128 result.State = grantState(grant, now)129 result.Rate = grant.Scope.Rate130 result.Expires = grant.ExpiresAt131 result.Allows = &grantAllows{Kinds: nonNilInts(grant.Scope.Kinds), Rooms: nonNilStrings(grant.Scope.Rooms), Repos: grant.Scope.Repos, Wiki: grant.Scope.Wiki, Jobs: grant.Scope.Jobs, Sites: grant.Scope.Sites}132 if result.Allows.Repos == nil {133 result.Allows.Repos = []community.AgentRepo{}134 }135 if result.Allows.Sites == nil {136 result.Allows.Sites = []community.AgentSite{}137 }138 }139 if !asked {140 return result, nil141 }142 missing := &grantMissing{Rooms: []string{}, Kinds: []int{}}143 if result.Enforced {144 for _, room := range needs.Rooms {145 if !ok || !grant.AllowsRoom(room) {146 missing.Rooms = append(missing.Rooms, room)147 }148 }149 for _, kind := range needs.Kinds {150 if !ok || !grant.AllowsKind(kind) {151 missing.Kinds = append(missing.Kinds, kind)152 }153 }154 }155 result.Missing = missing156 return result, nil157}158159func nonNilInts(values []int) []int {160 if values == nil {161 return []int{}162 }163 return values164}165166func nonNilStrings(values []string) []string {167 if values == nil {168 return []string{}169 }170 return values171}