package core import ( "database/sql" "encoding/json" "errors" "fmt" "time" _ "github.com/mattn/go-sqlite3" ) func AddItems(db DB, items []Item) error { return db.Transact(func(tx DB) error { stmt, err := tx.Prepare(` insert into items (source, id, active, title, author, body, link, time, ttl, ttd, tts, action) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, jsonb(?)) `) if err != nil { return fmt.Errorf("failed to prepare insert: %v", err) } for _, item := range items { actions, err := json.Marshal(item.Action) if err != nil { return fmt.Errorf("failed to marshal actions for %s/%s: %v", item.Source, item.Id, err) } _, err = stmt.Exec( item.Source, item.Id, true, item.Title, item.Author, item.Body, item.Link, item.Time, item.Ttl, item.Ttd, item.Tts, actions, ) if err != nil { return fmt.Errorf("failed to insert %s/%s: %v", item.Source, item.Id, err) } } return nil }) } // Set fields in the new item to match the old item where the new item's fields are zero-valued. // This allows sources to omit fields and let an action set them without a later fetch overwriting // the value from the action, e.g. an on-create action archiving a page and setting the link to // point to the archive. func BackfillItem(new *Item, old *Item) { new.Active = old.Active new.Created = old.Created if new.Author == "" { new.Author = old.Author } if new.Body == "" { new.Body = old.Body } if new.Link == "" { new.Link = old.Link } if new.Time == 0 { new.Time = old.Time } if new.Title == "" { new.Title = old.Title } if new.Ttl == 0 { new.Tts = old.Tts } if new.Ttd == 0 { new.Ttd = old.Ttd } if new.Tts == 0 { new.Tts = old.Tts } } func UpdateItems(db DB, items []Item) error { return db.Transact(func(tx DB) error { stmt, err := tx.Prepare(` update items set title = ?, author = ?, body = ?, link = ?, time = ?, ttl = ?, ttd = ?, tts = ?, action = jsonb(?) where source = ? and id = ? `) if err != nil { return err } for _, item := range items { actions, err := json.Marshal(item.Action) if err != nil { return fmt.Errorf("failed to marshal actions for %s/%s: %v", item.Source, item.Id, err) } _, err = stmt.Exec( item.Title, item.Author, item.Body, item.Link, item.Time, item.Ttl, item.Ttd, item.Tts, actions, item.Source, item.Id, ) if err != nil { return err } } return nil }) } // Deactivate an item, returning its previous active state. func DeactivateItem(db DB, source string, id string) (bool, error) { row := db.QueryRow(` select active from items where source = ? and id = ? `, source, id) var active bool err := row.Scan(&active) if err != nil && errors.Is(err, sql.ErrNoRows) { return false, fmt.Errorf("item %s/%s not found", source, id) } _, err = db.Exec(` update items set active = 0 where source = ? and id = ? `, source, id) if err != nil { return false, err } return active, nil } func DeleteItem(db DB, source string, id string) (int64, error) { res, err := db.Exec(` delete from items where source = ? and id = ? `, source, id) if err != nil { return 0, err } return res.RowsAffected() } func getItems(db DB, query string, args ...any) ([]Item, error) { rows, err := db.Query(query, args...) if err != nil { return nil, err } defer rows.Close() var items []Item for rows.Next() { var item Item err = rows.Scan( &item.Source, &item.Id, &item.Created, &item.Active, &item.Title, &item.Author, &item.Body, &item.Link, &item.Time, &item.Ttl, &item.Ttd, &item.Tts, &item.Action, ) if err != nil { return nil, err } items = append(items, item) } if err := rows.Err(); err != nil { return nil, err } return items, nil } func GetItem(db DB, source string, id string) (Item, error) { items, err := getItems(db, ` select source, id, created, active, title, author, body, link, time, ttl, ttd, tts, json(action) from items where source = ? and id = ? order by case when time = 0 then created else time end, id `, source, id) if err != nil { return Item{}, err } if len(items) == 0 { return Item{}, fmt.Errorf("no item in %s with id %s", source, id) } return items[0], nil } var DefaultFeedLimit = 100 func GetAllActiveItems(db DB, offset int, limit int) ([]Item, error) { now := int(time.Now().Unix()) // TODO pass this value in return getItems(db, ` select source, id, created, active, title, author, body, link, time, ttl, ttd, tts, json(action) from items where active <> 0 and (tts = 0 or created + tts < ?) order by case when time = 0 then created else time end, id limit ? offset ? `, now, limit, offset) } func GetAllItems(db DB, offset int, limit int) ([]Item, error) { return getItems(db, ` select source, id, created, active, title, author, body, link, time, ttl, ttd, tts, json(action) from items order by case when time = 0 then created else time end, id limit ? offset ? `, limit, offset) } func GetActiveItemsForSource(db DB, source string, offset int, limit int) ([]Item, error) { now := int(time.Now().Unix()) // TODO pass this value in return getItems(db, ` select source, id, created, active, title, author, body, link, time, ttl, ttd, tts, json(action) from items where source = ? and active <> 0 and (tts = 0 or created + tts < ?) order by case when time = 0 then created else time end, id limit ? offset ? `, source, now, limit, offset) } func GetAllItemsForSource(db DB, source string, offset int, limit int) ([]Item, error) { return getItems(db, ` select source, id, created, active, title, author, body, link, time, ttl, ttd, tts, json(action) from items where source = ? order by case when time = 0 then created else time end, id limit ? offset ? `, source, limit, offset) } func GetActiveItemsForChannel(db DB, channel string, offset int, limit int) ([]Item, error) { now := int(time.Now().Unix()) // TODO pass this value in return getItems(db, ` select i.source, i.id, i.created, i.active, i.title, i.author, i.body, i.link, i.time, i.ttl, i.ttd, i.tts, json(i.action) from items i join channels c on i.source = c.source where c.name = ? and i.active <> 0 and (i.tts = 0 or i.created + i.tts < ?) order by case when i.time = 0 then i.created else i.time end, i.id limit ? offset ? `, channel, now, limit, offset) } func GetAllItemsForChannel(db DB, channel string, offset int, limit int) ([]Item, error) { return getItems(db, ` select i.source, i.id, i.created, i.active, i.title, i.author, i.body, i.link, i.time, i.ttl, i.ttd, i.tts, json(i.action) from items i join channels c on i.source = c.source where c.name = ? order by case when i.time = 0 then i.created else i.time end, i.id limit ? offset ? `, channel, limit, offset) }