clone

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

keep replaced wiki versions as revisions in the store

Dami committed fec17a9
commit
fec17a90a7f8e62742fdb75fc54c77be7804fbac
parent
ff254bfea41381760de1dbc3dfe871af52a03f8d
author
Dami <dami.etoile@gmail.com>
4 files changed, +94 -1
Minternal/storage/maintenance.go6++-
Minternal/storage/save.go20+++++++++
Minternal/storage/schema.go3+
Ainternal/storage/wiki.go66++++++++++++++++++++++++++++++
M internal/storage/maintenance.go +5 -1
M internal/storage/save.go +20 -0
@@ -159,6 +159,14 @@ func replace(ctx context.Context, tx *sql.Tx, e event.Event, now int64) error {
159159 return fmt.Errorf("archive replaced list: %w", err)160160 }161161 }162+ if e.Kind == kindWikiArticle {163+ // A wiki page keeps its history: the version being replaced moves164+ // to wiki_revisions with the id of the event that replaced it.165+ _, err := tx.ExecContext(ctx, "INSERT OR IGNORE INTO wiki_revisions(event_id,author,d,created_at,raw,superseded_by) SELECT id,pubkey,d,created_at,raw,? FROM events WHERE pubkey=? AND kind=? AND d=?", e.ID, e.PubKey, e.Kind, d)166+ if err != nil {167+ return fmt.Errorf("archive replaced wiki revision: %w", err)168+ }169+ }162170 _, err = tx.ExecContext(ctx, "DELETE FROM events WHERE pubkey=? AND kind=? AND d=?", e.PubKey, e.Kind, d)163171 if err != nil {164172 return fmt.Errorf("replace event: %w", err)
@@ -182,6 +190,11 @@ func applyDeletion(ctx context.Context, tx *sql.Tx, e event.Event, tag []string)
182190 if _, err := tx.ExecContext(ctx, "DELETE FROM list_history WHERE kind=? AND owner=? AND d=? AND created_at<=?", kind, e.PubKey, parts[2], e.CreatedAt); err != nil {183191 return fmt.Errorf("delete list history: %w", err)184192 }193+ if kind == kindWikiArticle {194+ if _, err := tx.ExecContext(ctx, "DELETE FROM wiki_revisions WHERE author=? AND d=? AND created_at<=?", e.PubKey, parts[2], e.CreatedAt); err != nil {195+ return fmt.Errorf("delete wiki revisions: %w", err)196+ }197+ }185198 } else {186199 if _, err := tx.ExecContext(ctx, "DELETE FROM events WHERE id=? AND (pubkey=? OR (kind=1059 AND EXISTS(SELECT 1 FROM tags WHERE event_id=events.id AND name='p' AND value=?)))", tag[1], e.PubKey, e.PubKey); err != nil {187200 return fmt.Errorf("delete event: %w", err)
@@ -189,6 +202,9 @@ func applyDeletion(ctx context.Context, tx *sql.Tx, e event.Event, tag []string)
189202 if _, err := tx.ExecContext(ctx, "DELETE FROM list_history WHERE owner=? AND event_id=?", e.PubKey, tag[1]); err != nil {190203 return fmt.Errorf("delete list version: %w", err)191204 }205+ if _, err := tx.ExecContext(ctx, "DELETE FROM wiki_revisions WHERE author=? AND event_id=?", e.PubKey, tag[1]); err != nil {206+ return fmt.Errorf("delete wiki revision: %w", err)207+ }192208 }193209 _, err := tx.ExecContext(ctx, "INSERT INTO deletions(author,target_type,target,until) VALUES(?,?,?,?) ON CONFLICT(author,target_type,target) DO UPDATE SET until=max(until,excluded.until)", e.PubKey, tag[0], tag[1], e.CreatedAt)194210 if err != nil {
@@ -243,6 +259,10 @@ func indexed(kind int, mode string) bool {
243259 244260 func isListKind(kind int) bool { return kind == 3 || kind == 10002 || kind == 10003 || kind == 30003 }245261 262+// kindWikiArticle is the NIP-54 article kind, whose replaced versions are263+// kept as revisions.264+const kindWikiArticle = 30818265+246266 func AddIntents(ctx context.Context, tx *sql.Tx, intents []Intent, now int64) error {247267 for _, intent := range intents {248268 key := sha256.Sum256([]byte(intent.Kind + "\x00" + intent.EventID + "\x00" + intent.Target))
M internal/storage/schema.go +3 -0
A internal/storage/wiki.go +66 -0
@@ -0,0 +1,66 @@
1+package storage2+3+// Wiki revisions. A kind 30818 article is addressable, so a newer version4+// from the same author replaces the older one in events. The replaced5+// event is kept here with the id of the event that replaced it, until the6+// author deletes it with a kind 5 naming the revision or the page.7+8+import (9+ "context"10+ "database/sql"11+ "encoding/json"12+ "errors"13+ "fmt"14+15+ "github.com/FelineStateMachine/tinyrelay/internal/event"16+)17+18+// WikiRevision is one archived version of a wiki page.19+type WikiRevision struct {20+ ID string21+ Author string22+ D string23+ CreatedAt int6424+ SupersededBy string25+ Event event.Event26+}27+28+// WikiRevisions lists archived revisions newest first. An empty author or29+// d matches every author or page.30+func (s *Store) WikiRevisions(ctx context.Context, author, d string) ([]WikiRevision, error) {31+ rows, err := s.db.QueryContext(ctx, "SELECT event_id,author,d,created_at,superseded_by,raw FROM wiki_revisions WHERE (?='' OR author=?) AND (?='' OR d=?) ORDER BY created_at DESC,event_id", author, author, d, d)32+ if err != nil {33+ return nil, fmt.Errorf("query wiki revisions: %w", err)34+ }35+ defer rows.Close()36+ result := []WikiRevision{}37+ for rows.Next() {38+ var row WikiRevision39+ var raw string40+ if err := rows.Scan(&row.ID, &row.Author, &row.D, &row.CreatedAt, &row.SupersededBy, &raw); err != nil {41+ return nil, err42+ }43+ if err := json.Unmarshal([]byte(raw), &row.Event); err != nil {44+ return nil, fmt.Errorf("decode wiki revision %s: %w", row.ID, err)45+ }46+ result = append(result, row)47+ }48+ return result, rows.Err()49+}50+51+// WikiRevision reads one archived revision by event id.52+func (s *Store) WikiRevision(ctx context.Context, id string) (WikiRevision, bool, error) {53+ var row WikiRevision54+ var raw string55+ err := s.db.QueryRowContext(ctx, "SELECT event_id,author,d,created_at,superseded_by,raw FROM wiki_revisions WHERE event_id=?", id).Scan(&row.ID, &row.Author, &row.D, &row.CreatedAt, &row.SupersededBy, &raw)56+ if errors.Is(err, sql.ErrNoRows) {57+ return row, false, nil58+ }59+ if err != nil {60+ return row, false, fmt.Errorf("read wiki revision: %w", err)61+ }62+ if err := json.Unmarshal([]byte(raw), &row.Event); err != nil {63+ return row, false, fmt.Errorf("decode wiki revision %s: %w", id, err)64+ }65+ return row, true, nil66+}