clone

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

feat/ui-updates
tinyrelay / internal / records / identity.go
identity.go 8.0K
1package records23import (4 "context"5 "database/sql"6 "encoding/json"7 "errors"8 "fmt"9 "strconv"1011 "github.com/FelineStateMachine/tinyrelay/internal/event"12 "github.com/FelineStateMachine/tinyrelay/internal/policy"13 "github.com/FelineStateMachine/tinyrelay/internal/storage"14)1516func (s *Service) group() string { return s.groupID }1718func groupMetaTags(group string, p policy.Policy) [][]string {19 tags := [][]string{{"-"}, {"d", group}, {"name", p.Name}, {"about", p.Description}}20 if p.Icon != "" {21 tags = append(tags, []string{"picture", p.Icon})22 }23 if p.Reads == "members" {24 tags = append(tags, []string{"private"})25 }26 if p.Writes != "open" {27 tags = append(tags, []string{"restricted"})28 }29 return tags30}3132func loadOrCreateSecret(ctx context.Context, s *storage.Store) (string, error) {33 var secret string34 if err := s.GetSetting(ctx, secretSetting, &secret); err == nil {35 if len(secret) != 64 {36 return "", errors.New("records: invalid persisted relay secret")37 }38 return secret, nil39 } else if !errors.Is(err, sql.ErrNoRows) {40 return "", err41 }42 secret, err := event.GenerateKey()43 if err != nil {44 return "", fmt.Errorf("records: generate relay key: %w", err)45 }46 if err := s.PutSetting(ctx, secretSetting, secret); err != nil {47 return "", err48 }49 return secret, nil50}5152// PublicKey returns the relay signing key, never its secret.53func (s *Service) PublicKey() string {54 key, _ := event.PublicKey(s.secret)55 return key56}5758func (s *Service) RelayURL() string { return s.relayURL }5960func (s *Service) nextTimestamp(ctx context.Context, tx *sql.Tx, now int64) (int64, error) {61 var previous int6462 if err := tx.QueryRowContext(ctx, `SELECT value FROM settings WHERE key='records.clock'`).Scan(&previous); err != nil && !errors.Is(err, sql.ErrNoRows) {63 return 0, err64 }65 if now <= previous {66 now = previous + 167 }68 if err := storage.PutSetting(ctx, tx, "records.clock", now); err != nil {69 return 0, err70 }71 return now, nil72}7374func (s *Service) signed(ctx context.Context, kind int, tags [][]string, content string, now int64) (event.Event, error) {75 s.mu.Lock()76 defer s.mu.Unlock()77 var out event.Event78 err := s.store.WithTx(ctx, func(tx *sql.Tx) error {79 t, err := s.nextTimestamp(ctx, tx, now)80 if err != nil {81 return err82 }83 out = event.Event{CreatedAt: t, Kind: kind, Tags: tags, Content: content}84 return event.Sign(&out, s.secret)85 })86 if err != nil {87 return event.Event{}, err88 }89 if s.onGenerated != nil {90 if err := s.onGenerated(ctx, out); err != nil {91 return event.Event{}, err92 }93 }94 return out, nil95}9697func (s *Service) signedOnly(ctx context.Context, kind int, tags [][]string, content string, now int64) (event.Event, error) {98 s.mu.Lock()99 defer s.mu.Unlock()100 var out event.Event101 err := s.store.WithTx(ctx, func(tx *sql.Tx) error {102 t, err := s.nextTimestamp(ctx, tx, now)103 if err != nil {104 return err105 }106 out = event.Event{CreatedAt: t, Kind: kind, Tags: tags, Content: content}107 return event.Sign(&out, s.secret)108 })109 return out, err110}111112func (s *Service) Generate(ctx context.Context, kind int, tags [][]string, content string, now int64) (event.Event, error) {113 return s.signed(ctx, kind, tags, content, now)114}115116func (s *Service) Sign(ctx context.Context, kind int, tags [][]string, content string, now int64) (event.Event, error) {117 return s.signedOnly(ctx, kind, tags, content, now)118}119120func (s *Service) profile(ctx context.Context, now int64) (event.Event, error) {121 p := s.policy()122 b, _ := json.Marshal(map[string]string{"name": p.Name, "about": p.Description, "picture": p.Icon})123 return s.signed(ctx, event.KIND_PROFILE, nil, string(b), now)124}125126func (s *Service) discovery(ctx context.Context, now int64) (event.Event, error) {127 p := s.policy()128 tags := [][]string{{"d", s.relayURL}}129 if p.Reads != "open" {130 tags = append(tags, []string{"r", p.Reads})131 }132 if p.Writes != "open" {133 tags = append(tags, []string{"writes", p.Writes})134 }135 return s.signed(ctx, event.KIND_RELAY_DISCOVERY, tags, s.relayURL, now)136}137138func (s *Service) roster(ctx context.Context, now int64) (event.Event, error) {139 tags := [][]string{{"-"}}140 communityMembers, err := s.members(ctx)141 if err != nil {142 return event.Event{}, err143 }144 for _, member := range communityMembers {145 tag := []string{"member", member.PubKey}146 if member.Role != "" && member.Role != "member" {147 tag = append(tag, member.Role)148 }149 tags = append(tags, tag)150 }151 if len(tags) == 1 && s.policy().Owner != "" {152 tags = append(tags, []string{"member", s.policy().Owner, "owner"})153 }154 return s.signed(ctx, event.KIND_ROSTER, tags, "", now)155}156157func (s *Service) groupState(ctx context.Context, now int64) (event.Event, error) {158 p := s.policy()159 tags := [][]string{{"-"}, {"d", s.group()}, {"name", p.Name}, {"about", p.Description}}160 if p.Icon != "" {161 tags = append(tags, []string{"picture", p.Icon})162 }163 if p.Reads == "members" {164 tags = append(tags, []string{"private"})165 }166 if p.Writes != "open" {167 tags = append(tags, []string{"restricted"})168 }169 return s.signed(ctx, event.KIND_GROUP_METADATA, tags, "", now)170}171172func (s *Service) MembershipDelta(ctx context.Context, pubkey string, added bool, now int64) (event.Event, error) {173 kind := event.KIND_MEMBER_REMOVED174 if added {175 kind = event.KIND_MEMBER_ADDED176 }177 return s.signed(ctx, kind, [][]string{{"-"}, {"p", pubkey}}, "", now)178}179180func (s *Service) groupRecords(ctx context.Context, now int64) ([]event.Event, error) {181 p := s.policy()182 communityMembers, err := s.members(ctx)183 if err != nil {184 return nil, err185 }186 var memberTags, admins, roleTags [][]string187 for _, member := range communityMembers {188 if member.Role == "agent" {189 memberTags = append(memberTags, []string{"p", member.PubKey, member.Role})190 } else {191 memberTags = append(memberTags, []string{"p", member.PubKey})192 }193 if member.Role == "owner" || member.Role == "moderator" {194 admins = append(admins, []string{"p", member.PubKey, member.Role})195 }196 }197 if !p.DirectoryPublic {198 memberTags = nil199 }200 for _, role := range roleOrder {201 roleTags = append(roleTags, []string{"role", role, rolesAbout[role]})202 }203 vals := []struct {204 kind int205 tags [][]string206 content string207 }{208 {event.KIND_GROUP_METADATA, groupMetaTags(s.group(), p), ""},209 {event.KIND_GROUP_ADMINS, append([][]string{{"-"}, {"d", s.group()}}, admins...), ""},210 {event.KIND_GROUP_MEMBERS, append([][]string{{"-"}, {"d", s.group()}}, memberTags...), ""},211 {event.KIND_GROUP_ROLES, append([][]string{{"-"}, {"d", s.group()}}, roleTags...), ""},212 }213 out := make([]event.Event, 0, len(vals)+3)214 for _, v := range vals {215 e, err := s.signed(ctx, v.kind, v.tags, v.content, now)216 if err != nil {217 return nil, err218 }219 out = append(out, e)220 }221 for i, role := range roleOrder {222 e, err := s.signed(ctx, event.KIND_ROLE_DEF, [][]string{{"-"}, {"d", role}, {"label", role}, {"description", rolesAbout[role]}, {"order", strconv.Itoa(i + 1)}}, "", now)223 if err != nil {224 return nil, err225 }226 out = append(out, e)227 }228 return out, nil229}230231func (s *Service) groupRecordsEmpty(ctx context.Context, now int64, p policy.Policy) ([]event.Event, error) {232 _ = p233 vals := []struct {234 kind int235 tags [][]string236 content string237 }{238 {event.KIND_GROUP_METADATA, groupMetaTags(s.group(), p), ""},239 {event.KIND_GROUP_ADMINS, [][]string{{"-"}, {"d", s.group()}}, ""},240 {event.KIND_GROUP_MEMBERS, [][]string{{"-"}, {"d", s.group()}}, ""},241 {event.KIND_GROUP_ROLES, [][]string{{"-"}, {"d", s.group()}}, ""},242 {event.KIND_ROLE_DEF, [][]string{{"-"}, {"d", "owner"}, {"label", "owner"}, {"description", "relay owner"}, {"order", "1"}}, ""},243 {event.KIND_ROLE_DEF, [][]string{{"-"}, {"d", "moderator"}, {"label", "moderator"}, {"description", "relay moderator"}, {"order", "2"}}, ""},244 {event.KIND_ROLE_DEF, [][]string{{"-"}, {"d", "member"}, {"label", "member"}, {"description", "relay member"}, {"order", "3"}}, ""},245 {event.KIND_ROLE_DEF, [][]string{{"-"}, {"d", "agent"}, {"label", "agent"}, {"description", "agent under an owner-signed grant"}, {"order", "4"}}, ""},246 }247 out := make([]event.Event, 0, len(vals))248 for _, v := range vals {249 e, err := s.signed(ctx, v.kind, v.tags, v.content, now)250 if err != nil {251 return nil, err252 }253 out = append(out, e)254 }255 return out, nil256}