1package gates23import (4 "context"5 "errors"6 "fmt"7 "strings"8 "sync"910 "github.com/FelineStateMachine/tinyrelay/internal/community"11 "github.com/FelineStateMachine/tinyrelay/internal/event"12 "github.com/FelineStateMachine/tinyrelay/internal/policy"13 "github.com/FelineStateMachine/tinyrelay/internal/relay"14 "github.com/FelineStateMachine/tinyrelay/internal/storage"15 "github.com/dlclark/regexp2"16)1718// Config supplies the policy and optional durable services used by Gate.19// Store and Community enable checks that depend on persisted relay state.20type Config struct {21 Store *storage.Store22 Community *community.Service23 Policy func() policy.Policy24 Slug string25 ArtifactVisible func(context.Context, event.Event, relay.Session) bool26}2728// Gate evaluates event admission and visibility for one tenant configuration.29type Gate struct {30 cfg Config31 agents agentLimiter32}33type artifactCheckContextKey struct{}34type artifactCheckState struct {35 seen map[string]struct{}36 depth int37}3839// agentLimiter is the per-agent sliding one-minute counter. It lives in40// memory: a restart forgives the last minute, which is the cheap and honest41// trade for a limit that never touches the database on the hot path.42type agentLimiter struct {43 mu sync.Mutex44 recent map[string][]int6445}4647func (l *agentLimiter) allow(agent string, limit int, now int64) bool {48 l.mu.Lock()49 defer l.mu.Unlock()50 if l.recent == nil {51 l.recent = map[string][]int64{}52 }53 kept := l.recent[agent][:0]54 for _, at := range l.recent[agent] {55 if at > now-60 {56 kept = append(kept, at)57 }58 }59 if len(kept) >= limit {60 l.recent[agent] = kept61 return false62 }63 l.recent[agent] = append(kept, now)64 return true65}6667// agentAdmission applies an agent grant to an event from a key whose only68// standing here is that grant. Keys with a human role keep that role's69// permissions. Events from agents are never stamped or rewritten; they are70// kept as signed or refused with a restricted reason.71func (g *Gate) agentAdmission(ctx context.Context, e event.Event, now int64) error {72 if g.cfg.Community == nil {73 return nil74 }75 grant, ok, err := g.cfg.Community.AgentGrant(ctx, e.PubKey)76 if err != nil {77 return fmt.Errorf("check agent grant: %w", err)78 }79 if !ok {80 if community.IsAgentGrantRequest(e) {81 return errors.New("restricted: grant requests require an active agent grant")82 }83 return nil84 }85 role, err := g.cfg.Community.Role(ctx, e.PubKey)86 if err != nil {87 return fmt.Errorf("check agent role: %w", err)88 }89 if role != "" && role != "agent" {90 if community.IsAgentGrantRequest(e) {91 return errors.New("restricted: only the granted agent may request a grant change")92 }93 return nil94 }95 if community.IsAgentGrantRequest(e) {96 if _, reviewErr := g.cfg.Community.ReviewAgentGrantRequest(ctx, e, now); reviewErr != nil {97 return reviewErr98 }99 }100 if !community.IsAgentGrantRequest(e) {101 if err := grant.Check(e, now); err != nil {102 return err103 }104 }105 if !g.agents.allow(e.PubKey, grant.Scope.Rate, now) {106 return fmt.Errorf("restricted: agent grant does not allow more than %d events per minute", grant.Scope.Rate)107 }108 return nil109}110111// agentGrantShape checks a grant event's signer and tags before it is stored.112func (g *Gate) agentGrantShape(ctx context.Context, e event.Event, now int64) error {113 if e.Kind != event.KIND_AGENT_GRANT {114 return nil115 }116 if g.cfg.Community == nil {117 return errors.New("restricted: agent grants need the membership service")118 }119 role, err := g.cfg.Community.Role(ctx, e.PubKey)120 if err != nil {121 return fmt.Errorf("check grant signer: %w", err)122 }123 if !community.CanGrantAgents(role) {124 return errors.New("restricted: only the owner or a moderator can grant an agent")125 }126 if event.Tag(e, "grant-request") != "" || event.Tag(e, "grant-base") != "" {127 if err := g.cfg.Community.ValidateAgentGrantReplacement(ctx, e, now); err != nil {128 return err129 }130 }131 _, err = community.ParseAgentGrant(e, now)132 return err133}134135func New(cfg Config) (*Gate, error) {136 if cfg.Policy == nil {137 return nil, errors.New("gates: policy function is required")138 }139 return &Gate{cfg: cfg}, nil140}141142// Write validates a client-published event against protocol shape, tenant143// policy, membership, bans, room access, retention and proof-of-work rules.144func (g *Gate) Write(ctx context.Context, e event.Event, s relay.Session, now int64) error {145 p := g.cfg.Policy()146 if e.Kind == event.KIND_AUTH {147 return errors.New("blocked: kind 22242 is only accepted inside AUTH")148 }149 if e.Kind == event.KIND_MARMOT_GROUP || e.Kind == event.KIND_MARMOT_KEY_PACKAGE {150 if !p.Features.Marmot {151 return errors.New("restricted: Marmot transport is switched off on this relay")152 }153 if err := MarmotShape(e); err != nil {154 return err155 }156 if e.Kind == event.KIND_MARMOT_GROUP {157 if contains(s.PubKeys, e.PubKey) {158 return errors.New("invalid: kind 445 must use a fresh ephemeral author")159 }160 if g.cfg.Store != nil {161 result, err := g.cfg.Store.Query(ctx, event.Filter{Kinds: []int{event.KIND_MARMOT_GROUP}, Authors: []string{e.PubKey}}, storage.QueryOptions{Now: now, Access: storage.Access{All: true}, Limit: 1})162 if err != nil {163 return fmt.Errorf("check Marmot ephemeral author: %w", err)164 }165 if len(result.Events) > 0 {166 return errors.New("invalid: kind 445 ephemeral author was already used")167 }168 }169 }170 }171 if err := event.Validate(e); err != nil {172 return err173 }174 if err := nip43Shape(e, now); err != nil {175 return err176 }177 if err := jobFeature(p, e); err != nil {178 return err179 }180 if err := JobShape(e); err != nil {181 return err182 }183 if p.MaxFuture > 0 && e.CreatedAt > now+p.MaxFuture {184 return errors.New("invalid: event creation date is too far off from the current time")185 }186 if exp := event.Expiration(e); exp > 0 && exp <= now && e.Kind != event.KIND_AGENT_GRANT {187 return errors.New("invalid: event has already expired")188 }189 if g.cfg.Community != nil {190 if s.RemoteIP != "" {191 blocked, err := g.cfg.Community.IsIPBlocked(ctx, s.RemoteIP)192 if err != nil {193 return fmt.Errorf("check IP block: %w", err)194 }195 if blocked {196 return errors.New("blocked: this IP address is blocked")197 }198 }199 banned, err := g.cfg.Community.IsBanned(ctx, e.PubKey)200 if err != nil {201 return fmt.Errorf("check pubkey ban: %w", err)202 }203 if banned {204 return errors.New("blocked: this pubkey is banned")205 }206 banned, err = g.cfg.Community.IsEventBanned(ctx, e.ID)207 if err != nil {208 return fmt.Errorf("check event ban: %w", err)209 }210 if banned {211 return errors.New("blocked: this event is banned")212 }213 }214 if err := g.agentGrantShape(ctx, e, now); err != nil {215 return err216 }217 if err := g.agentAdmission(ctx, e, now); err != nil {218 return err219 }220 if where := blockedWord(p, e); where != "" && !g.isModerator(ctx, e.PubKey) {221 return errors.New("blocked: " + where)222 }223 if e.Kind == event.KIND_NOSTR_CONNECT {224 if !p.Features.Signer {225 return errors.New("restricted: signer traffic is switched off")226 }227 return nil228 }229 if err := g.roomWrite(ctx, e); err != nil {230 return err231 }232 if p.Inbox.Targeted && !inboxAdmission(p, e) {233 return errors.New("blocked: inbox event must target the relay owner")234 }235 if hasTag(e, "-") && !contains(s.PubKeys, e.PubKey) {236 return errors.New("auth-required: this event may only be published by its author")237 }238 writeAccess := accessFor(ctx, g.cfg.Community, p, relay.Session{PubKeys: []string{e.PubKey}})239 if p.Writes == "wot" && !writeAccess.Member && !writeAccess.Owner && !g.wotAllows(ctx, e.PubKey, p.Owner) {240 return errors.New("restricted: this relay only accepts events from its members and the people they follow")241 }242 if e.Kind == event.KIND_MARMOT_GROUP {243 principal, err := g.Principal(ctx, e, s)244 if err != nil {245 return err246 }247 if g.cfg.Community != nil {248 banned, banErr := g.cfg.Community.IsBanned(ctx, principal)249 if banErr != nil {250 return fmt.Errorf("check Marmot principal ban: %w", banErr)251 }252 if banned {253 return errors.New("blocked: Marmot account is banned")254 }255 }256 writeAccess = accessFor(ctx, g.cfg.Community, p, relay.Session{PubKeys: []string{principal}})257 }258 if err := jobWriter(e, writeAccess); err != nil {259 return err260 }261 if err := g.jobReply(ctx, e, now); err != nil {262 return err263 }264 ownerReplaceable := writeAccess.Owner && (event.IsReplaceable(e.Kind) || event.IsAddressable(e.Kind))265 allowed, blocked, rulesErr := g.kindRules(ctx, p, e.Kind)266 if rulesErr != nil {267 return fmt.Errorf("read kind rules: %w", rulesErr)268 }269 if blocked || !allowed {270 if !ownerReplaceable {271 return errors.New("blocked: this relay does not accept this event kind")272 }273 }274 membershipEvent := e.Kind == event.KIND_JOIN || e.Kind == event.KIND_LEAVE || e.Kind == event.KIND_NIP43_JOIN || e.Kind == event.KIND_NIP43_LEAVE275 if !ownerReplaceable && !membershipEvent && !policy.CanWrite(p, e, writeAccess) && !g.guestPass(ctx, p, e, writeAccess, now) {276 return errors.New("blocked: this event is not allowed by the relay write policy")277 }278 if days := g.RetentionDays(ctx, e.Kind); days > 0 && !protectedKind(e.Kind) && !(ownerReplaceable) && e.CreatedAt < now-int64(days)*86400 {279 return errors.New("blocked: event is older than this relay's retention window")280 }281 if e.Kind == event.KIND_WRAP && g.isDMInbox(ctx, p) && g.cfg.Community != nil && len(event.TagValues(e, "p")) > 0 {282 valid := false283 for _, recipient := range event.TagValues(e, "p") {284 member, err := g.cfg.Community.IsMember(ctx, recipient)285 if err == nil && member {286 valid = true287 }288 }289 if !valid {290 return errors.New("blocked: gift wrap recipient is not local")291 }292 }293 if p.MinPow > 0 && event.Difficulty(e.ID) < p.MinPow {294 return fmt.Errorf("pow: difficulty %d is less than %d", event.Difficulty(e.ID), p.MinPow)295 }296 if p.MinPow > 0 && event.CommittedDifficulty(e) > 0 && event.CommittedDifficulty(e) < p.MinPow {297 return fmt.Errorf("pow: committed target %d is less than %d", event.CommittedDifficulty(e), p.MinPow)298 }299 return nil300}301302func inboxAdmission(p policy.Policy, e event.Event) bool {303 if e.PubKey == p.Owner && (e.Kind == 0 || e.Kind == 3 || e.Kind == 10002) {304 return true305 }306 for _, tag := range e.Tags {307 if len(tag) >= 2 && tag[0] == "p" && tag[1] == p.Owner {308 return true309 }310 }311 return false312}313314// Import validates an event received from another host using protocol shape,315// safety, ban, agent and room existence checks. The caller records its origin316// in storage after this common gate succeeds.317func (g *Gate) Import(ctx context.Context, e event.Event, now int64) error {318 p := g.cfg.Policy()319 if err := event.Validate(e); err != nil {320 return err321 }322 if err := nip43Shape(e, now); err != nil {323 return err324 }325 if err := jobFeature(p, e); err != nil {326 return err327 }328 if err := JobShape(e); err != nil {329 return err330 }331 if p.MaxFuture > 0 && e.CreatedAt > now+p.MaxFuture {332 return errors.New("invalid: event creation date is too far off from the current time")333 }334 if exp := event.Expiration(e); exp > 0 && exp <= now && e.Kind != event.KIND_AGENT_GRANT {335 return errors.New("invalid: event has already expired")336 }337 if g.cfg.Community != nil {338 banned, err := g.cfg.Community.IsBanned(ctx, e.PubKey)339 if err != nil {340 return fmt.Errorf("check pubkey ban: %w", err)341 }342 if banned {343 return errors.New("blocked: this pubkey is banned")344 }345 banned, err = g.cfg.Community.IsEventBanned(ctx, e.ID)346 if err != nil {347 return fmt.Errorf("check event ban: %w", err)348 }349 if banned {350 return errors.New("blocked: this event is banned")351 }352 }353 if err := g.agentGrantShape(ctx, e, now); err != nil {354 return err355 }356 if err := g.agentAdmission(ctx, e, now); err != nil {357 return err358 }359 if err := g.jobReply(ctx, e, now); err != nil {360 return err361 }362 if where := blockedWord(p, e); where != "" && !g.isModerator(ctx, e.PubKey) {363 return errors.New("blocked: " + where)364 }365 if e.Kind == event.KIND_AUTH {366 return errors.New("blocked: kind 22242 is only accepted inside AUTH")367 }368 if e.Kind == event.KIND_NOSTR_CONNECT && !p.Features.Signer {369 return errors.New("restricted: signer traffic is switched off")370 }371 if e.Kind == event.KIND_MARMOT_GROUP || e.Kind == event.KIND_MARMOT_KEY_PACKAGE {372 if !p.Features.Marmot {373 return errors.New("restricted: Marmot transport is switched off on this relay")374 }375 if err := MarmotShape(e); err != nil {376 return err377 }378 }379 if err := g.roomExists(ctx, e); err != nil {380 return err381 }382 return nil383}384385// roomID names the room an event belongs to, or nothing for events outside386// the room dialect. Marmot group messages reuse the h tag for other ends.387func (g *Gate) roomID(e event.Event) string {388 if e.Kind == event.KIND_MARMOT_GROUP {389 return ""390 }391 return event.Tag(e, "h")392}393394// roomExists is the common room check: an h tag must name the slug room or395// a live room. Room creation is the one event allowed to name a new room.396func (g *Gate) roomExists(ctx context.Context, e event.Event) error {397 id := g.roomID(e)398 if id == "" || id == g.cfg.Slug || e.Kind == event.KIND_CREATE_GROUP {399 return nil400 }401 if g.cfg.Community == nil {402 return fmt.Errorf("blocked: this relay hosts one group: %s", g.cfg.Slug)403 }404 room, err := g.cfg.Community.Room(ctx, id)405 if err != nil {406 return fmt.Errorf("check room: %w", err)407 }408 if !room.Live() {409 return fmt.Errorf("invalid: unknown room %s", id)410 }411 return nil412}413414// roomWrite applies room membership to client writes. The slug room keeps415// the tenant's own rules. In other rooms, management events are checked by416// the room handler; every other event needs room membership, which an open417// room grants to any tenant member.418func (g *Gate) roomWrite(ctx context.Context, e event.Event) error {419 if err := g.roomExists(ctx, e); err != nil {420 return err421 }422 id := g.roomID(e)423 if id != "" && community.RoomNoticeKind(e.Kind) {424 return errors.New("blocked: member notices are signed by the relay")425 }426 if id == "" || id == g.cfg.Slug || e.Kind == event.KIND_CREATE_GROUP || community.RoomAdminKind(e.Kind) {427 return nil428 }429 allowed, _, err := g.cfg.Community.RoomWriteAllowed(ctx, id, e.PubKey)430 if err != nil {431 return fmt.Errorf("check room membership: %w", err)432 }433 if !allowed {434 return fmt.Errorf("restricted: not a member of room %s", id)435 }436 return nil437}438439// roomVisible applies room access to reads. Members-only rooms, including440// the relay's own 39000 to 39002 records for them, are served only to room441// members, the tenant owner and moderators.442func (g *Gate) roomVisible(ctx context.Context, e event.Event, s relay.Session) bool {443 id := g.roomID(e)444 if id == "" && (e.Kind == event.KIND_GROUP_METADATA || e.Kind == event.KIND_GROUP_ADMINS || e.Kind == event.KIND_GROUP_MEMBERS) {445 id = event.Tag(e, "d")446 }447 if id == "" || id == g.cfg.Slug || g.cfg.Community == nil {448 return true449 }450 visible, err := g.cfg.Community.RoomVisible(ctx, id, s.PubKeys)451 return err == nil && visible452}453454// membersOnlyRoomFilter reports whether a filter asks for a members-only455// room, so an unauthenticated subscriber is told to AUTH instead of waiting456// on an empty stream.457func (g *Gate) membersOnlyRoomFilter(ctx context.Context, f event.Filter) bool {458 if g.cfg.Community == nil {459 return false460 }461 for _, id := range f.Tags["h"] {462 if id == g.cfg.Slug {463 continue464 }465 room, err := g.cfg.Community.Room(ctx, id)466 if err == nil && room.Live() && room.Access == community.RoomMembers {467 return true468 }469 }470 return false471}472473func (g *Gate) kindRules(ctx context.Context, p policy.Policy, kind int) (bool, bool, error) {474 if g.cfg.Community == nil {475 return len(p.AllowedKinds) == 0 || containsInt(p.AllowedKinds, kind), containsInt(p.BlockedKinds, kind), nil476 }477 allowedAny, blocked := false, false478 value, err := g.cfg.Community.Execute(ctx, p.Owner, "listallowedkinds", nil)479 if err != nil {480 return false, false, err481 }482 if values, ok := value.([]int); ok {483 allowedAny = len(values) == 0 || containsInt(values, kind)484 } else {485 allowedAny = true486 }487 value, err = g.cfg.Community.Execute(ctx, p.Owner, "listblockedkinds", nil)488 if err != nil {489 return false, false, err490 }491 if values, ok := value.([]int); ok {492 blocked = containsInt(values, kind)493 }494 return allowedAny, blocked, nil495}496497// RetentionDays returns the configured retention period for kind, using the498// wildcard rule when no exact rule exists.499func (g *Gate) RetentionDays(ctx context.Context, kind int) int {500 if g.cfg.Community == nil {501 wildcard := 0502 for _, rule := range g.cfg.Policy().Retention {503 if rule.Kind == kind {504 return rule.Days505 }506 if rule.Kind < 0 {507 wildcard = rule.Days508 }509 }510 return wildcard511 }512 value, err := g.cfg.Community.Execute(ctx, g.cfg.Policy().Owner, "listretention", nil)513 if err != nil {514 return 0515 }516 if rules, ok := value.([]map[string]int); ok {517 wildcard := 0518 for _, rule := range rules {519 if rule["kind"] == kind {520 return rule["days"]521 }522 if rule["kind"] < 0 {523 wildcard = rule["days"]524 }525 }526 return wildcard527 }528 return 0529}530531func protectedKind(kind int) bool {532 switch kind {533 case 0, 3, 10002, 9735, 13534, 8000, 8001, 9000, 9001, 33534, 39000, 39001, 39002, 39003, 39005:534 return true535 }536 return false537}538539func nip43Shape(e event.Event, now int64) error {540 if e.Kind != event.KIND_NIP43_JOIN && e.Kind != event.KIND_NIP43_LEAVE {541 return nil542 }543 if delta := e.CreatedAt - now; delta > 300 || delta < -300 {544 return errors.New("invalid: NIP-43 request is outside the allowed time window")545 }546 if e.Kind == event.KIND_NIP43_LEAVE {547 protected := 0548 for _, tag := range e.Tags {549 if len(tag) > 0 && tag[0] == "-" {550 protected++551 if len(tag) != 1 {552 return errors.New("invalid: NIP-43 leave protected tag must be empty")553 }554 }555 }556 if protected != 1 {557 return errors.New("invalid: NIP-43 leave needs one protected tag")558 }559 }560 // A join without a claim tag is an access request held for the owner's561 // review, so the claim stays optional.562 return nil563}564565func (g *Gate) wotAllows(ctx context.Context, target, owner string) bool {566 if g.cfg.Store == nil {567 return false568 }569 principals := []string{owner}570 if g.cfg.Community != nil {571 members, err := g.cfg.Community.Execute(ctx, owner, "listmembers", nil)572 if err == nil {573 if list, ok := members.([]community.Member); ok {574 for _, member := range list {575 principals = append(principals, member.PubKey)576 }577 }578 }579 }580 seen := map[string]bool{}581 for _, principal := range principals {582 if seen[principal] {583 continue584 }585 seen[principal] = true586 result, err := g.cfg.Store.Query(ctx, event.Filter{Authors: []string{principal}, Kinds: []int{3}}, storage.QueryOptions{Now: 0, Access: storage.Access{All: true}})587 if err != nil || len(result.Events) == 0 {588 continue589 }590 latest := result.Events[0]591 for _, tag := range latest.Tags {592 if len(tag) > 1 && tag[0] == "p" && tag[1] == target {593 return true594 }595 }596 }597 return false598}599600func (g *Gate) isDMInbox(ctx context.Context, p policy.Policy) bool {601 if p.Reads != "members" || g.cfg.Community == nil {602 return p.Reads == "members" && containsInt(p.AllowedKinds, event.KIND_WRAP)603 }604 value, err := g.cfg.Community.Execute(ctx, p.Owner, "listallowedkinds", nil)605 if err != nil {606 return false607 }608 values, ok := value.([]int)609 return ok && containsInt(values, event.KIND_WRAP)610}611612func (g *Gate) guestPass(ctx context.Context, p policy.Policy, e event.Event, a policy.Access, now int64) bool {613 if !a.Member && !a.Owner && p.Writes != "open" {614 if containsInt(p.OpenKinds, e.Kind) {615 return true616 }617 if !p.GuestReplies || (e.Kind != 1 && e.Kind != 1111) || g.cfg.Store == nil {618 return false619 }620 for _, id := range append(event.TagValues(e, "e"), event.TagValues(e, "E")...) {621 result, err := g.cfg.Store.Query(ctx, event.Filter{IDs: []string{id}}, storage.QueryOptions{Now: now, Access: storage.Access{All: true}, Limit: 1})622 if err == nil && len(result.Events) > 0 {623 author := result.Events[0].PubKey624 if author == p.Owner {625 return true626 }627 if g.cfg.Community != nil {628 member, memberErr := g.cfg.Community.IsMember(ctx, author)629 if memberErr == nil && member {630 return true631 }632 }633 }634 }635 }636 return false637}638639// Read validates a subscription request and returns an AUTH hint when a640// subscriber may receive private or members-only results after authenticating.641func (g *Gate) Read(ctx context.Context, filters []event.Filter, s relay.Session) (bool, error) {642 p := g.cfg.Policy()643 if p.Features.Signer && len(filters) > 0 {644 signerOnly := true645 for _, f := range filters {646 if len(f.Kinds) != 1 || f.Kinds[0] != event.KIND_NOSTR_CONNECT {647 signerOnly = false648 }649 }650 if signerOnly {651 return false, nil652 }653 }654 if p.Features.Search == "off" {655 for _, f := range filters {656 if f.Search != "" {657 return false, errors.New("unsupported: search is switched off on this relay")658 }659 }660 }661 a := accessFor(ctx, g.cfg.Community, p, s)662 if p.Reads == "auth" && len(a.PubKeys) == 0 {663 return false, errors.New("auth-required: this relay requires AUTH")664 }665 if p.Reads == "members" && !a.Member && !a.Owner {666 if len(a.PubKeys) == 0 {667 return false, errors.New("auth-required: this relay is members-only")668 }669 return false, errors.New("restricted: this relay is members-only")670 }671 privateOnly := true672 authHint := false673 for _, f := range filters {674 if len(a.PubKeys) == 0 && g.membersOnlyRoomFilter(ctx, f) {675 authHint = true676 }677 if len(f.Kinds) == 0 {678 privateOnly = false679 authHint = true680 continue681 }682 seenPrivate := false683 for _, k := range f.Kinds {684 if event.IsPrivate(k) {685 seenPrivate = true686 } else {687 privateOnly = false688 }689 }690 if seenPrivate && len(a.PubKeys) == 0 {691 authHint = true692 }693 }694 if privateOnly && len(a.PubKeys) == 0 {695 return false, errors.New("auth-required: private kinds are only served to their recipients")696 }697 return authHint, nil698}699700// CanSee reports whether session s may receive e. It applies hidden-event,701// ban, repository privacy, policy, room and recipient checks.702func (g *Gate) CanSee(ctx context.Context, e event.Event, s relay.Session, f *event.Filter) bool {703 p := g.cfg.Policy()704 if e.Kind == event.KIND_VIEW && g.cfg.ArtifactVisible != nil {705 state, _ := ctx.Value(artifactCheckContextKey{}).(artifactCheckState)706 if state.seen == nil {707 state.seen = map[string]struct{}{}708 }709 if _, cycle := state.seen[e.ID]; cycle || state.depth >= 32 {710 return false711 }712 seen := make(map[string]struct{}, len(state.seen)+1)713 for id := range state.seen {714 seen[id] = struct{}{}715 }716 seen[e.ID] = struct{}{}717 ctx = context.WithValue(ctx, artifactCheckContextKey{}, artifactCheckState{seen: seen, depth: state.depth + 1})718 if !g.cfg.ArtifactVisible(ctx, e, s) {719 return false720 }721 }722 if community.IsAgentGrantRequest(e) {723 operator := event.Tag(e, "p")724 if !contains(s.PubKeys, e.PubKey) && !contains(s.PubKeys, operator) {725 return false726 }727 }728 if g.cfg.Store != nil {729 var held int730 if err := g.cfg.Store.DB().QueryRowContext(ctx, `SELECT COUNT(*) FROM (SELECT id FROM hidden_events WHERE id=? UNION ALL SELECT id FROM pending_events WHERE id=?)`, e.ID, e.ID).Scan(&held); err != nil || held > 0 {731 return false732 }733 }734 private, err := privateRepositoryEvent(ctx, g.cfg.Store, e)735 if err != nil || private && (!p.Features.Grasp08 || p.Reads != "members") {736 return false737 }738 if g.cfg.Community != nil {739 banned, err := g.cfg.Community.IsEventBanned(ctx, e.ID)740 if err != nil || banned {741 return false742 }743 banned, err = g.cfg.Community.IsBanned(ctx, e.PubKey)744 if err != nil || banned {745 return false746 }747 for _, key := range s.PubKeys {748 banned, err = g.cfg.Community.IsBanned(ctx, key)749 if err != nil || banned {750 return false751 }752 }753 }754 if e.Kind == event.KIND_PUSH_REGISTRATION {755 return contains(s.PubKeys, e.PubKey)756 }757 if e.Kind == event.KIND_NOSTR_CONNECT {758 return p.Features.Signer && canSeeSignerMessage(e, s, f)759 }760 a := accessFor(ctx, g.cfg.Community, p, s)761 if !policy.CanRead(p, e, a) {762 return false763 }764 if !g.roomVisible(ctx, e, s) {765 return false766 }767 if event.IsPrivate(e.Kind) {768 for _, key := range s.PubKeys {769 if key == e.PubKey {770 return true771 }772 }773 for _, tag := range e.Tags {774 if len(tag) >= 2 && tag[0] == "p" && contains(s.PubKeys, tag[1]) {775 return true776 }777 }778 return false779 }780 return true781}782783// privateRepositoryEvent also protects legacy state events. Their privacy is784// inherited from the corresponding replaceable announcement, so checking only785// the state event's own tags would allow metadata to escape after a restart.786func privateRepositoryEvent(ctx context.Context, store *storage.Store, e event.Event) (bool, error) {787 return policy.PrivateRepository(ctx, store, e)788}789790func canSeeSignerMessage(e event.Event, s relay.Session, f *event.Filter) bool {791 if contains(s.PubKeys, e.PubKey) {792 return true793 }794 for _, recipient := range event.TagValues(e, "p") {795 if contains(s.PubKeys, recipient) {796 return true797 }798 // The client must receive encrypted replies before it can use its799 // remote signer to authenticate. Limit this to its addressed inbox.800 if f != nil && len(f.Kinds) == 1 && f.Kinds[0] == event.KIND_NOSTR_CONNECT && contains(f.Tags["p"], recipient) {801 return true802 }803 }804 return false805}806807func accessFor(ctx context.Context, c *community.Service, p policy.Policy, s relay.Session) policy.Access {808 a := policy.Access{PubKeys: append([]string(nil), s.PubKeys...)}809 a.Owner = contains(s.PubKeys, p.Owner)810 if c != nil {811 for _, key := range s.PubKeys {812 member, err := c.IsMember(ctx, key)813 if err == nil && member {814 a.Member = true815 }816 role, err := c.Role(ctx, key)817 if err == nil && role != "" {818 a.Role = role819 }820 }821 }822 return a823}824825func (g *Gate) isModerator(ctx context.Context, key string) bool {826 if g.cfg.Community == nil {827 return false828 }829 role, err := g.cfg.Community.Role(ctx, key)830 return err == nil && (role == "owner" || role == "moderator")831}832833func blockedWord(p policy.Policy, e event.Event) string {834 for _, word := range p.BlockedWords {835 pattern := word836 regex := false837 if strings.HasPrefix(pattern, "/") && strings.HasSuffix(pattern, "/") {838 pattern, regex = pattern[1:len(pattern)-1], true839 }840 matches := func(value string) bool {841 if !regex {842 return strings.Contains(strings.ToLower(value), strings.ToLower(pattern))843 }844 re, err := regexp2.Compile(pattern, regexp2.IgnoreCase)845 if err != nil {846 return false847 }848 ok, err := re.MatchString(value)849 return err == nil && ok850 }851 if matches(e.Content) {852 return "content contains a blocked word"853 }854 if p.BlockedWordsInTags {855 for _, tag := range e.Tags {856 for _, value := range tag {857 if matches(value) {858 return "a tag contains a blocked word"859 }860 }861 }862 }863 }864 return ""865}866867func hasTag(e event.Event, name string) bool {868 for _, tag := range e.Tags {869 if len(tag) > 0 && tag[0] == name {870 return true871 }872 }873 return false874}875func contains(values []string, wanted string) bool {876 for _, value := range values {877 if value == wanted {878 return true879 }880 }881 return false882}883884func containsInt(values []int, wanted int) bool {885 for _, value := range values {886 if value == wanted {887 return true888 }889 }890 return false891}892func proofOfWork(id string) int {893 bits := 0894 for _, ch := range id {895 value := byte(0)896 switch {897 case ch >= '0' && ch <= '9':898 value = byte(ch - '0')899 case ch >= 'a' && ch <= 'f':900 value = byte(ch - 'a' + 10)901 default:902 return bits903 }904 for mask := byte(8); mask > 0 && value&mask == 0; mask >>= 1 {905 bits++906 }907 if value != 0 {908 break909 }910 }911 return bits912}