chore: split out store db
This commit is contained in:
parent
0ea449264a
commit
3d6048e544
1
feed.go
1
feed.go
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
type Feed struct {
|
type Feed struct {
|
||||||
FeedID uuid
|
FeedID uuid
|
||||||
|
FetchURI string
|
||||||
URI string
|
URI string
|
||||||
Nick string
|
Nick string
|
||||||
LastScanOn sql.NullTime
|
LastScanOn sql.NullTime
|
||||||
|
|
|
@ -68,11 +68,11 @@ func NewHTTPFetcher() *httpFetcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *httpFetcher) Fetch(ctx context.Context, request *Feed) (*Response, error) {
|
func (f *httpFetcher) Fetch(ctx context.Context, request *Feed) (*Response, error) {
|
||||||
if strings.Contains(request.URI, "lublin.se") {
|
if strings.Contains(request.FetchURI, "lublin.se") {
|
||||||
return nil, fmt.Errorf("%w: permaban: %s", ErrPermanentlyDead, request.URI)
|
return nil, fmt.Errorf("%w: permaban: %s", ErrPermanentlyDead, request.URI)
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", request.URI, nil)
|
req, err := http.NewRequestWithContext(ctx, "GET", request.FetchURI, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("creating HTTP request failed: %w", err)
|
return nil, fmt.Errorf("creating HTTP request failed: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -99,7 +99,7 @@ func (f *httpFetcher) Fetch(ctx context.Context, request *Feed) (*Response, erro
|
||||||
if errors.Is(err, &net.DNSError{}) {
|
if errors.Is(err, &net.DNSError{}) {
|
||||||
return nil, fmt.Errorf("%w: %s", ErrTemporarilyDead, err)
|
return nil, fmt.Errorf("%w: %s", ErrTemporarilyDead, err)
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("%w: %w", ErrPermanentlyDead, err)
|
return nil, fmt.Errorf("%w: %w", ErrTemporarilyDead, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
response := &Response{
|
response := &Response{
|
||||||
|
|
4
main.go
4
main.go
|
@ -31,6 +31,8 @@ type args struct {
|
||||||
dbtype string
|
dbtype string
|
||||||
dbfile string
|
dbfile string
|
||||||
baseFeed string
|
baseFeed string
|
||||||
|
Nick string
|
||||||
|
URI string
|
||||||
}
|
}
|
||||||
|
|
||||||
func env(key, def string) string {
|
func env(key, def string) string {
|
||||||
|
@ -50,6 +52,8 @@ func main() {
|
||||||
env("XT_DBTYPE", "sqlite3"),
|
env("XT_DBTYPE", "sqlite3"),
|
||||||
env("XT_DBFILE", "file:twt.db"),
|
env("XT_DBFILE", "file:twt.db"),
|
||||||
env("XT_BASE_FEED", "feed"),
|
env("XT_BASE_FEED", "feed"),
|
||||||
|
env("XT_NICK", "xuu"),
|
||||||
|
env("XT_URI", "https://txt.sour.is/users/xuu/twtxt.txt"),
|
||||||
}
|
}
|
||||||
|
|
||||||
console.Set("args", args)
|
console.Set("args", args)
|
||||||
|
|
260
service.go
260
service.go
|
@ -1,6 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cmp"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -21,30 +22,32 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func run(c console) error {
|
func run(c console) error {
|
||||||
ctx := c.Context
|
|
||||||
a := c.Args()
|
a := c.Args()
|
||||||
|
|
||||||
db, err := sql.Open(a.dbtype, a.dbfile)
|
db, err := setupDB(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
for _, stmt := range strings.Split(initSQL, ";") {
|
|
||||||
_, err = db.ExecContext(ctx, stmt)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Set("db", db)
|
c.Set("db", db)
|
||||||
|
|
||||||
f, err := os.Open(a.baseFeed)
|
f, err := os.Open(a.baseFeed)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
|
||||||
err = loadFeed(db, &types.Twter{Nick: "xuu", URI: "https://txt.sour.is/users/xuu/twtxt.txt"}, f)
|
twtfile, err := lextwt.ParseFile(f, &types.Twter{
|
||||||
|
Nick: a.Nick,
|
||||||
|
URI: a.URI,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%w: %w", ErrParseFailed, err)
|
||||||
|
}
|
||||||
|
f.Close()
|
||||||
|
|
||||||
|
|
||||||
|
err = storeFeed(db, twtfile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -68,6 +71,7 @@ var (
|
||||||
values (?, ?, ?, ?, ?)
|
values (?, ?, ?, ?, ?)
|
||||||
ON CONFLICT (feed_id) DO NOTHING
|
ON CONFLICT (feed_id) DO NOTHING
|
||||||
`
|
`
|
||||||
|
|
||||||
insertTwt = `
|
insertTwt = `
|
||||||
insert into twts
|
insert into twts
|
||||||
(feed_id, hash, conv, dt, text, mentions, tags)
|
(feed_id, hash, conv, dt, text, mentions, tags)
|
||||||
|
@ -97,100 +101,21 @@ var (
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
|
|
||||||
func loadFeed(db *sql.DB, twter *types.Twter, feed io.Reader) error {
|
func setupDB(c console) (*sql.DB, error) {
|
||||||
loadTS := time.Now()
|
a := c.Args()
|
||||||
refreshRate := 600
|
db, err := sql.Open(a.dbtype, a.dbfile)
|
||||||
|
|
||||||
f, err := lextwt.ParseFile(feed, twter)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("%w: %w", ErrParseFailed, err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
feedID := urlNS.UUID5(coalesce(f.Twter().HashingURI, f.Twter().URI))
|
for _, stmt := range strings.Split(initSQL, ";") {
|
||||||
|
_, err = db.ExecContext(c, stmt)
|
||||||
tx, err := db.Begin()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
|
||||||
|
|
||||||
followers := f.Info().GetAll("follow")
|
|
||||||
followMap := make(map[string]string, len(followers))
|
|
||||||
for _, f := range f.Info().GetAll("follow") {
|
|
||||||
nick, uri, ok := strings.Cut(f.Value(), "http")
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
nick = strings.TrimSpace(nick)
|
|
||||||
uri = "http" + strings.TrimSpace(uri)
|
|
||||||
|
|
||||||
if uri == "https://lublin.se/twtxt.txt" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := url.Parse(uri); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
followMap[nick] = uri
|
|
||||||
}
|
|
||||||
|
|
||||||
defer tx.Rollback()
|
|
||||||
|
|
||||||
_, err = tx.Exec(insertFeed, feedID, f.Twter().HashingURI, f.Twter().DomainNick(), loadTS, refreshRate)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, twt := range f.Twts() {
|
|
||||||
mentions := make(uuids, 0, len(twt.Mentions()))
|
|
||||||
for _, mention := range twt.Mentions() {
|
|
||||||
followMap[mention.Twter().Nick] = mention.Twter().URI
|
|
||||||
mentions = append(mentions, urlNS.UUID5(mention.Twter().URI))
|
|
||||||
}
|
|
||||||
|
|
||||||
tags := make(strList, 0, len(twt.Tags()))
|
|
||||||
for _, tag := range twt.Tags() {
|
|
||||||
tags = append(tags, tag.Text())
|
|
||||||
}
|
|
||||||
|
|
||||||
subject := twt.Subject()
|
|
||||||
subjectTag := ""
|
|
||||||
if subject != nil {
|
|
||||||
if tag, ok := subject.Tag().(*lextwt.Tag); ok && tag != nil {
|
|
||||||
subjectTag = tag.Text()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = tx.Exec(
|
return db, nil
|
||||||
insertTwt,
|
|
||||||
feedID,
|
|
||||||
twt.Hash(),
|
|
||||||
subjectTag,
|
|
||||||
twt.Created(),
|
|
||||||
fmt.Sprint(twt),
|
|
||||||
mentions.ToStrList(),
|
|
||||||
tags,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for nick, uri := range followMap {
|
|
||||||
_, err = tx.Exec(
|
|
||||||
insertFeed,
|
|
||||||
urlNS.UUID5(uri),
|
|
||||||
uri,
|
|
||||||
nick,
|
|
||||||
nil,
|
|
||||||
refreshRate,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx.Commit()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshLoop(c console) {
|
func refreshLoop(c console) {
|
||||||
|
@ -198,15 +123,13 @@ func refreshLoop(c console) {
|
||||||
|
|
||||||
TenYear := 3153600000 // 10 year
|
TenYear := 3153600000 // 10 year
|
||||||
OneDay := 86400 // 1 day
|
OneDay := 86400 // 1 day
|
||||||
TenMinutes := 600
|
TenMinutes := 600 // 10 mins
|
||||||
|
|
||||||
fetch := NewHTTPFetcher()
|
fetch := NewHTTPFetcher()
|
||||||
|
|
||||||
less := func(a, b *Feed) bool {
|
queue := FibHeap(func(a, b *Feed) bool {
|
||||||
return a.LastScanOn.Time.Before(b.LastScanOn.Time)
|
return a.LastScanOn.Time.Before(b.LastScanOn.Time)
|
||||||
}
|
})
|
||||||
|
|
||||||
queue := FibHeap(less)
|
|
||||||
|
|
||||||
db := c.Get("db").(*sql.DB)
|
db := c.Get("db").(*sql.DB)
|
||||||
|
|
||||||
|
@ -267,7 +190,28 @@ func refreshLoop(c console) {
|
||||||
cpy, err := os.OpenFile(filepath.Join("feeds", urlNS.UUID5(f.URI).MarshalText()), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
cpy, err := os.OpenFile(filepath.Join("feeds", urlNS.UUID5(f.URI).MarshalText()), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||||
rdr := io.TeeReader(res.Body, cpy)
|
rdr := io.TeeReader(res.Body, cpy)
|
||||||
|
|
||||||
err = loadFeed(db, &types.Twter{Nick: f.Nick, URI: f.URI}, rdr)
|
twtfile, err := lextwt.ParseFile(rdr, &types.Twter{Nick: f.Nick, URI: f.URI})
|
||||||
|
if err != nil {
|
||||||
|
c.Log(fmt.Errorf("%w: %w", ErrParseFailed, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if prev, ok :=twtfile.Info().GetN("prev", 0); ok {
|
||||||
|
_, part, ok := strings.Cut(prev.Value(), " ")
|
||||||
|
if ok {
|
||||||
|
|
||||||
|
part = f.URI[:strings.LastIndex(f.URI, "/")+1] + part
|
||||||
|
queue.Insert(&Feed{
|
||||||
|
FetchURI: part,
|
||||||
|
URI: f.URI,
|
||||||
|
Nick: f.Nick,
|
||||||
|
LastScanOn: f.LastScanOn,
|
||||||
|
RefreshRate: f.RefreshRate,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = storeFeed(db, twtfile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Log(err)
|
c.Log(err)
|
||||||
return
|
return
|
||||||
|
@ -287,6 +231,100 @@ func refreshLoop(c console) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func storeFeed(db *sql.DB, f types.TwtFile) error {
|
||||||
|
loadTS := time.Now()
|
||||||
|
refreshRate := 600
|
||||||
|
|
||||||
|
feedID := urlNS.UUID5(cmp.Or(f.Twter().HashingURI, f.Twter().URI))
|
||||||
|
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
followers := f.Info().GetAll("follow")
|
||||||
|
followMap := make(map[string]string, len(followers))
|
||||||
|
for _, f := range f.Info().GetAll("follow") {
|
||||||
|
nick, uri, ok := strings.Cut(f.Value(), "http")
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nick = strings.TrimSpace(nick)
|
||||||
|
uri = "http" + strings.TrimSpace(uri)
|
||||||
|
|
||||||
|
if _, err := url.Parse(uri); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
followMap[nick] = uri
|
||||||
|
}
|
||||||
|
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
_, err = tx.Exec(
|
||||||
|
insertFeed,
|
||||||
|
feedID,
|
||||||
|
f.Twter().HashingURI,
|
||||||
|
f.Twter().DomainNick(),
|
||||||
|
loadTS,
|
||||||
|
refreshRate,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, twt := range f.Twts() {
|
||||||
|
mentions := make(uuids, 0, len(twt.Mentions()))
|
||||||
|
for _, mention := range twt.Mentions() {
|
||||||
|
followMap[mention.Twter().Nick] = mention.Twter().URI
|
||||||
|
mentions = append(mentions, urlNS.UUID5(mention.Twter().URI))
|
||||||
|
}
|
||||||
|
|
||||||
|
tags := make(strList, 0, len(twt.Tags()))
|
||||||
|
for _, tag := range twt.Tags() {
|
||||||
|
tags = append(tags, tag.Text())
|
||||||
|
}
|
||||||
|
|
||||||
|
subject := twt.Subject()
|
||||||
|
subjectTag := ""
|
||||||
|
if subject != nil {
|
||||||
|
if tag, ok := subject.Tag().(*lextwt.Tag); ok && tag != nil {
|
||||||
|
subjectTag = tag.Text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.Exec(
|
||||||
|
insertTwt,
|
||||||
|
feedID,
|
||||||
|
twt.Hash(),
|
||||||
|
subjectTag,
|
||||||
|
twt.Created(),
|
||||||
|
fmt.Sprint(twt),
|
||||||
|
mentions.ToStrList(),
|
||||||
|
tags,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for nick, uri := range followMap {
|
||||||
|
_, err = tx.Exec(
|
||||||
|
insertFeed,
|
||||||
|
urlNS.UUID5(uri),
|
||||||
|
uri,
|
||||||
|
nick,
|
||||||
|
nil,
|
||||||
|
refreshRate,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
func LoadFeeds(c console) (iter.Seq[Feed], error) {
|
func LoadFeeds(c console) (iter.Seq[Feed], error) {
|
||||||
var err error
|
var err error
|
||||||
var res *sql.Rows
|
var res *sql.Rows
|
||||||
|
@ -324,21 +362,11 @@ func LoadFeeds(c console) (iter.Seq[Feed], error) {
|
||||||
f.LastScanOn.Time = f.LastScanOn.Time.Add(time.Duration(f.RefreshRate) * time.Second)
|
f.LastScanOn.Time = f.LastScanOn.Time.Add(time.Duration(f.RefreshRate) * time.Second)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
f.FetchURI = f.URI
|
||||||
|
|
||||||
if !yield(f) {
|
if !yield(f) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, err
|
}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func coalesce[T comparable](a T, values ...T) T {
|
|
||||||
var zero T
|
|
||||||
|
|
||||||
for _, v := range values {
|
|
||||||
if a == zero {
|
|
||||||
a = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user