clone

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

feat/standalone-relay
tinyrelay / internal / community / config.go
config.go 7.3K
1package community23import (4 "context"5 "database/sql"6 "time"7)89// ConfigSnapshot is the community-owned portion of relay configuration.10// The type deliberately contains no configport types so the domain owns its11// storage contract.12type ConfigSnapshot struct {13 Members []Member14 Bans []ConfigBan15 IPBlocks []ConfigIPBlock16 EventBans []ConfigEventBan17 KindRules []ConfigKindRule18 Retention []ConfigRetention19}2021type ConfigBan struct {22 PubKey string23 Reason string24}2526type ConfigIPBlock struct {27 IP string28 Reason string29}3031type ConfigEventBan struct {32 ID string33 Reason string34}3536type ConfigKindRule struct {37 Kind int38 Rule string39}4041type ConfigRetention struct {42 Kind *int43 Days int44}4546type ConfigMemberRetention struct {47 PubKey string48 Days int49}5051// RetentionRules returns the community-owned retention settings.52func (s *Service) RetentionRules(ctx context.Context) ([]ConfigRetention, error) {53 rows, err := s.store.DB().QueryContext(ctx, `SELECT kind,days FROM community_retention ORDER BY kind`)54 if err != nil {55 return nil, err56 }57 defer rows.Close()58 var out []ConfigRetention59 for rows.Next() {60 var kind, days int61 if err := rows.Scan(&kind, &days); err != nil {62 return nil, err63 }64 out = append(out, ConfigRetention{Kind: &kind, Days: days})65 }66 return out, rows.Err()67}6869// MembersForRetention returns members with an explicit retention override.70func (s *Service) MembersForRetention(ctx context.Context) ([]ConfigMemberRetention, error) {71 rows, err := s.store.DB().QueryContext(ctx, `SELECT pubkey,keep_days FROM community_members WHERE keep_days>0 ORDER BY pubkey`)72 if err != nil {73 return nil, err74 }75 defer rows.Close()76 var out []ConfigMemberRetention77 for rows.Next() {78 var member ConfigMemberRetention79 if err := rows.Scan(&member.PubKey, &member.Days); err != nil {80 return nil, err81 }82 out = append(out, member)83 }84 return out, rows.Err()85}8687// ExportConfig reads the community-owned configuration tables.88func ExportConfig(ctx context.Context, store interface {89 DB() *sql.DB90}) (ConfigSnapshot, error) {91 var out ConfigSnapshot92 rows, err := store.DB().QueryContext(ctx, `SELECT pubkey,name,note,role,invited_by,via,created_at,keep_days FROM community_members WHERE role<>'owner' ORDER BY pubkey`)93 if err != nil {94 return out, err95 }96 defer rows.Close()97 for rows.Next() {98 var member Member99 if err := rows.Scan(&member.PubKey, &member.Name, &member.Note, &member.Role, &member.InvitedBy, &member.Via, &member.CreatedAt, &member.KeepDays); err != nil {100 return out, err101 }102 out.Members = append(out.Members, member)103 }104 if err := rows.Err(); err != nil {105 return out, err106 }107 if err := readConfigRows(ctx, store.DB(), `SELECT pubkey,reason FROM community_bans ORDER BY pubkey`, func(rows *sql.Rows) error {108 var item ConfigBan109 if err := rows.Scan(&item.PubKey, &item.Reason); err != nil {110 return err111 }112 out.Bans = append(out.Bans, item)113 return nil114 }); err != nil {115 return out, err116 }117 if err := readConfigRows(ctx, store.DB(), `SELECT ip,reason FROM community_ip_blocks ORDER BY ip`, func(rows *sql.Rows) error {118 var item ConfigIPBlock119 if err := rows.Scan(&item.IP, &item.Reason); err != nil {120 return err121 }122 out.IPBlocks = append(out.IPBlocks, item)123 return nil124 }); err != nil {125 return out, err126 }127 if err := readConfigRows(ctx, store.DB(), `SELECT id,reason FROM community_event_bans ORDER BY id`, func(rows *sql.Rows) error {128 var item ConfigEventBan129 if err := rows.Scan(&item.ID, &item.Reason); err != nil {130 return err131 }132 out.EventBans = append(out.EventBans, item)133 return nil134 }); err != nil {135 return out, err136 }137 if err := readConfigRows(ctx, store.DB(), `SELECT kind,rule FROM community_kind_rules ORDER BY kind`, func(rows *sql.Rows) error {138 var item ConfigKindRule139 if err := rows.Scan(&item.Kind, &item.Rule); err != nil {140 return err141 }142 out.KindRules = append(out.KindRules, item)143 return nil144 }); err != nil {145 return out, err146 }147 if err := readConfigRows(ctx, store.DB(), `SELECT kind,days FROM community_retention ORDER BY kind`, func(rows *sql.Rows) error {148 var kind, days int149 if err := rows.Scan(&kind, &days); err != nil {150 return err151 }152 if kind < 0 {153 out.Retention = append(out.Retention, ConfigRetention{Days: days})154 return nil155 }156 out.Retention = append(out.Retention, ConfigRetention{Kind: &kind, Days: days})157 return nil158 }); err != nil {159 return out, err160 }161 return out, nil162}163164func readConfigRows(ctx context.Context, db *sql.DB, query string, scan func(*sql.Rows) error) error {165 rows, err := db.QueryContext(ctx, query)166 if err != nil {167 return err168 }169 defer rows.Close()170 for rows.Next() {171 if err := scan(rows); err != nil {172 return err173 }174 }175 return rows.Err()176}177178// ApplyConfigTx replaces each selected community configuration section inside179// the caller-owned transaction. A selected empty section clears that section;180// an unselected section is left unchanged. The caller commits or rolls back181// the transaction after all related configuration writes succeed.182func ApplyConfigTx(ctx context.Context, tx *sql.Tx, c ConfigSnapshot, members, bans, addresses, eventBans, kinds, retention bool) error {183 now := time.Now().Unix()184 if members {185 if _, err := tx.ExecContext(ctx, `DELETE FROM community_members WHERE role<>'owner'`); err != nil {186 return err187 }188 for _, member := range c.Members {189 if _, err := tx.ExecContext(ctx, `INSERT INTO community_members(pubkey,name,note,role,invited_by,via,created_at,keep_days) VALUES(?,?,?,?,?,?,?,?)`, member.PubKey, member.Name, member.Note, member.Role, member.InvitedBy, member.Via, now, member.KeepDays); err != nil {190 return err191 }192 }193 }194 if bans {195 if _, err := tx.ExecContext(ctx, `DELETE FROM community_bans`); err != nil {196 return err197 }198 for _, item := range c.Bans {199 if item.PubKey != "" {200 if _, err := tx.ExecContext(ctx, `INSERT INTO community_bans(pubkey,reason,at) VALUES(?,?,?)`, item.PubKey, item.Reason, now); err != nil {201 return err202 }203 }204 }205 }206 if addresses {207 if _, err := tx.ExecContext(ctx, `DELETE FROM community_ip_blocks`); err != nil {208 return err209 }210 for _, item := range c.IPBlocks {211 if item.IP != "" {212 if _, err := tx.ExecContext(ctx, `INSERT INTO community_ip_blocks(ip,reason,at) VALUES(?,?,?)`, item.IP, item.Reason, now); err != nil {213 return err214 }215 }216 }217 }218 if eventBans {219 if _, err := tx.ExecContext(ctx, `DELETE FROM community_event_bans`); err != nil {220 return err221 }222 for _, item := range c.EventBans {223 if item.ID != "" {224 if _, err := tx.ExecContext(ctx, `INSERT INTO community_event_bans(id,reason,at) VALUES(?,?,?)`, item.ID, item.Reason, now); err != nil {225 return err226 }227 }228 }229 }230 if kinds {231 if _, err := tx.ExecContext(ctx, `DELETE FROM community_kind_rules`); err != nil {232 return err233 }234 for _, item := range c.KindRules {235 if _, err := tx.ExecContext(ctx, `INSERT INTO community_kind_rules(kind,rule) VALUES(?,?)`, item.Kind, item.Rule); err != nil {236 return err237 }238 }239 }240 if retention {241 if _, err := tx.ExecContext(ctx, `DELETE FROM community_retention`); err != nil {242 return err243 }244 for _, item := range c.Retention {245 kind := -1246 if item.Kind != nil {247 kind = *item.Kind248 }249 if _, err := tx.ExecContext(ctx, `INSERT INTO community_retention(kind,days) VALUES(?,?)`, kind, item.Days); err != nil {250 return err251 }252 }253 }254 return nil255}