chore: add fetching queue
This commit is contained in:
		
							parent
							
								
									9ae1b7e67e
								
							
						
					
					
						commit
						7c0df508f8
					
				
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					twt.db*
 | 
				
			||||||
 | 
					feed
 | 
				
			||||||
							
								
								
									
										185
									
								
								fibheap.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										185
									
								
								fibheap.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,185 @@
 | 
				
			|||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import "math/bits"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type fibTree[T any] struct {
 | 
				
			||||||
 | 
						value  *T
 | 
				
			||||||
 | 
						parent *fibTree[T]
 | 
				
			||||||
 | 
						child  []*fibTree[T]
 | 
				
			||||||
 | 
						mark   bool
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (t *fibTree[T]) Value() *T { return t.value }
 | 
				
			||||||
 | 
					func (t *fibTree[T]) addAtEnd(n *fibTree[T]) {
 | 
				
			||||||
 | 
						n.parent = t
 | 
				
			||||||
 | 
						t.child = append(t.child, n)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type fibHeap[T any] struct {
 | 
				
			||||||
 | 
						trees []*fibTree[T]
 | 
				
			||||||
 | 
						least *fibTree[T]
 | 
				
			||||||
 | 
						count uint
 | 
				
			||||||
 | 
						less  func(a, b *T) bool
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func FibHeap[T any](less func(a, b *T) bool) *fibHeap[T] {
 | 
				
			||||||
 | 
						return &fibHeap[T]{less: less}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *fibHeap[T]) GetMin() *T {
 | 
				
			||||||
 | 
						return h.least.value
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *fibHeap[T]) IsEmpty() bool { return h.least == nil }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *fibHeap[T]) Insert(v *T) {
 | 
				
			||||||
 | 
						ntree := &fibTree[T]{value: v}
 | 
				
			||||||
 | 
						h.trees = append(h.trees, ntree)
 | 
				
			||||||
 | 
						if h.least == nil || h.less(v, h.least.value) {
 | 
				
			||||||
 | 
							h.least = ntree
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						h.count++
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *fibHeap[T]) ExtractMin() *T {
 | 
				
			||||||
 | 
						smallest := h.least
 | 
				
			||||||
 | 
						if smallest != nil {
 | 
				
			||||||
 | 
							// Remove smallest from root trees.
 | 
				
			||||||
 | 
							for i := range h.trees {
 | 
				
			||||||
 | 
								pos := h.trees[i]
 | 
				
			||||||
 | 
								if pos == smallest {
 | 
				
			||||||
 | 
									h.trees[i] = h.trees[len(h.trees)-1]
 | 
				
			||||||
 | 
									h.trees = h.trees[:len(h.trees)-1]
 | 
				
			||||||
 | 
									break
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Add children to root
 | 
				
			||||||
 | 
							h.trees = append(h.trees, smallest.child...)
 | 
				
			||||||
 | 
							smallest.child = smallest.child[:0]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							h.least = nil
 | 
				
			||||||
 | 
							if len(h.trees) > 0 {
 | 
				
			||||||
 | 
								h.consolidate()
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							h.count--
 | 
				
			||||||
 | 
							return smallest.value
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *fibHeap[T]) consolidate() {
 | 
				
			||||||
 | 
						aux := make([]*fibTree[T], bits.Len(h.count)+1)
 | 
				
			||||||
 | 
						for _, x := range h.trees {
 | 
				
			||||||
 | 
							order := len(x.child)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// consolidate the larger roots under smaller roots of same order until we have at most one tree per order.
 | 
				
			||||||
 | 
							for aux[order] != nil {
 | 
				
			||||||
 | 
								y := aux[order]
 | 
				
			||||||
 | 
								if h.less(y.value, x.value) {
 | 
				
			||||||
 | 
									x, y = y, x
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								x.addAtEnd(y)
 | 
				
			||||||
 | 
								aux[order] = nil
 | 
				
			||||||
 | 
								order++
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							aux[order] = x
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						h.trees = h.trees[:0]
 | 
				
			||||||
 | 
						// move ordered trees to root and find least node.
 | 
				
			||||||
 | 
						for _, k := range aux {
 | 
				
			||||||
 | 
							if k != nil {
 | 
				
			||||||
 | 
								k.parent = nil
 | 
				
			||||||
 | 
								h.trees = append(h.trees, k)
 | 
				
			||||||
 | 
								if h.least == nil || h.less(k.value, h.least.value) {
 | 
				
			||||||
 | 
									h.least = k
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *fibHeap[T]) Merge(a *fibHeap[T]) {
 | 
				
			||||||
 | 
						h.trees = append(h.trees, a.trees...)
 | 
				
			||||||
 | 
						h.count += a.count
 | 
				
			||||||
 | 
						if h.least == nil || a.least != nil && h.less(a.least.value, h.least.value) {
 | 
				
			||||||
 | 
							h.least = a.least
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *fibHeap[T]) find(fn func(*T) bool) *fibTree[T] {
 | 
				
			||||||
 | 
						var st []*fibTree[T]
 | 
				
			||||||
 | 
						st = append(st, h.trees...)
 | 
				
			||||||
 | 
						var tr *fibTree[T]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for len(st) > 0 {
 | 
				
			||||||
 | 
							tr, st = st[0], st[1:]
 | 
				
			||||||
 | 
							ro := *tr.value
 | 
				
			||||||
 | 
							if fn(&ro) {
 | 
				
			||||||
 | 
								break
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							st = append(st, tr.child...)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return tr
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *fibHeap[T]) Find(fn func(*T) bool) *T {
 | 
				
			||||||
 | 
						if needle := h.find(fn); needle != nil {
 | 
				
			||||||
 | 
							return needle.value
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *fibHeap[T]) DecreaseKey(find func(*T) bool, decrease func(*T)) {
 | 
				
			||||||
 | 
						needle := h.find(find)
 | 
				
			||||||
 | 
						if needle == nil {
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						decrease(needle.value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if h.less(needle.value, h.least.value) {
 | 
				
			||||||
 | 
							h.least = needle
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if parent := needle.parent; parent != nil {
 | 
				
			||||||
 | 
							if h.less(needle.value, parent.value) {
 | 
				
			||||||
 | 
								h.cut(needle)
 | 
				
			||||||
 | 
								h.cascadingCut(parent)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *fibHeap[T]) cut(x *fibTree[T]) {
 | 
				
			||||||
 | 
						parent := x.parent
 | 
				
			||||||
 | 
						for i := range parent.child {
 | 
				
			||||||
 | 
							pos := parent.child[i]
 | 
				
			||||||
 | 
							if pos == x {
 | 
				
			||||||
 | 
								parent.child[i] = parent.child[len(parent.child)-1]
 | 
				
			||||||
 | 
								parent.child = parent.child[:len(parent.child)-1]
 | 
				
			||||||
 | 
								break
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						x.parent = nil
 | 
				
			||||||
 | 
						x.mark = false
 | 
				
			||||||
 | 
						h.trees = append(h.trees, x)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if h.less(x.value, h.least.value) {
 | 
				
			||||||
 | 
							h.least = x
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (h *fibHeap[T]) cascadingCut(y *fibTree[T]) {
 | 
				
			||||||
 | 
						if y.parent != nil {
 | 
				
			||||||
 | 
							if !y.mark {
 | 
				
			||||||
 | 
								y.mark = true
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							h.cut(y)
 | 
				
			||||||
 | 
							h.cascadingCut(y.parent)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										15
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								go.mod
									
									
									
									
									
								
							@ -1,3 +1,18 @@
 | 
				
			|||||||
module go.sour.is/xt
 | 
					module go.sour.is/xt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
go 1.23.2
 | 
					go 1.23.2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					require (
 | 
				
			||||||
 | 
						github.com/mattn/go-sqlite3 v1.14.24
 | 
				
			||||||
 | 
						go.yarn.social/lextwt v0.0.0-20240908172157-7b9ae633db51
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					require (
 | 
				
			||||||
 | 
						github.com/hashicorp/errwrap v1.1.0 // indirect
 | 
				
			||||||
 | 
						github.com/hashicorp/go-multierror v1.1.1 // indirect
 | 
				
			||||||
 | 
						github.com/sirupsen/logrus v1.9.3 // indirect
 | 
				
			||||||
 | 
						github.com/writeas/go-strip-markdown/v2 v2.1.1 // indirect
 | 
				
			||||||
 | 
						go.yarn.social/types v0.0.0-20230305013457-e4d91e351ac8 // indirect
 | 
				
			||||||
 | 
						golang.org/x/crypto v0.27.0 // indirect
 | 
				
			||||||
 | 
						golang.org/x/sys v0.25.0 // indirect
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										27
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
				
			||||||
 | 
					github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
				
			||||||
 | 
					github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 | 
				
			||||||
 | 
					github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
 | 
				
			||||||
 | 
					github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 | 
				
			||||||
 | 
					github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
 | 
				
			||||||
 | 
					github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
 | 
				
			||||||
 | 
					github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
 | 
				
			||||||
 | 
					github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
 | 
				
			||||||
 | 
					github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 | 
				
			||||||
 | 
					github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
 | 
				
			||||||
 | 
					github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 | 
				
			||||||
 | 
					github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 | 
				
			||||||
 | 
					github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
				
			||||||
 | 
					github.com/writeas/go-strip-markdown/v2 v2.1.1 h1:hAxUM21Uhznf/FnbVGiJciqzska6iLei22Ijc3q2e28=
 | 
				
			||||||
 | 
					github.com/writeas/go-strip-markdown/v2 v2.1.1/go.mod h1:UvvgPJgn1vvN8nWuE5e7v/+qmDu3BSVnKAB6Gl7hFzA=
 | 
				
			||||||
 | 
					go.yarn.social/lextwt v0.0.0-20240908172157-7b9ae633db51 h1:XEjx0jSNv1h22gwGfQBfMypWv/YZXWGTRbqh3B8xfIs=
 | 
				
			||||||
 | 
					go.yarn.social/lextwt v0.0.0-20240908172157-7b9ae633db51/go.mod h1:CWAZuBHZfGaqa0FreSeLG+pzK3rHP2TNAG7Zh6QlRiM=
 | 
				
			||||||
 | 
					go.yarn.social/types v0.0.0-20230305013457-e4d91e351ac8 h1:zfnniiSO/WO65mSpdQzGYJ9pM0rYg/BKgrSm8h2mTyA=
 | 
				
			||||||
 | 
					go.yarn.social/types v0.0.0-20230305013457-e4d91e351ac8/go.mod h1:+xnDkQ0T0S8emxWIsvxlCAoyF8gBaj0q81hr/VrKc0c=
 | 
				
			||||||
 | 
					golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
 | 
				
			||||||
 | 
					golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
 | 
				
			||||||
 | 
					golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 | 
				
			||||||
 | 
					gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
				
			||||||
 | 
					gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
				
			||||||
							
								
								
									
										22
									
								
								init.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								init.sql
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					PRAGMA journal_mode=WAL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					create table if not exists feeds (
 | 
				
			||||||
 | 
						feed_id blob primary key,
 | 
				
			||||||
 | 
						uri text,
 | 
				
			||||||
 | 
						nick string,
 | 
				
			||||||
 | 
						domain string,
 | 
				
			||||||
 | 
						last_scan_on timestamp,
 | 
				
			||||||
 | 
						refresh_rate int default 600
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					create table if not exists twts (
 | 
				
			||||||
 | 
						feed_id blob,
 | 
				
			||||||
 | 
						hash text,
 | 
				
			||||||
 | 
						conv text,
 | 
				
			||||||
 | 
						dt text, -- timestamp with timezone
 | 
				
			||||||
 | 
						text text,
 | 
				
			||||||
 | 
						mentions text, -- json
 | 
				
			||||||
 | 
						tags text, -- json
 | 
				
			||||||
 | 
					    primary key (feed_id, hash)
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										61
									
								
								main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								main.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,61 @@
 | 
				
			|||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"os/signal"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type contextKey struct{ name string }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type console struct {
 | 
				
			||||||
 | 
						io.Reader
 | 
				
			||||||
 | 
						io.Writer
 | 
				
			||||||
 | 
						err io.Writer
 | 
				
			||||||
 | 
						context.Context
 | 
				
			||||||
 | 
						abort func()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (c console) Log(v ...any) { fmt.Fprintln(c.err, v...) }
 | 
				
			||||||
 | 
					func (c console) Args() args   { return c.Get("args").(args) }
 | 
				
			||||||
 | 
					func (c *console) Set(name string, value any) {
 | 
				
			||||||
 | 
						c.Context = context.WithValue(c.Context, contextKey{name}, value)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (c console) Get(name string) any {
 | 
				
			||||||
 | 
						return c.Context.Value(contextKey{name})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type args struct {
 | 
				
			||||||
 | 
						dbtype string
 | 
				
			||||||
 | 
						dbfile string
 | 
				
			||||||
 | 
						baseFeed string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func env(key, def string) string {
 | 
				
			||||||
 | 
						if v, ok := os.LookupEnv(key); ok {
 | 
				
			||||||
 | 
							return v
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return def
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func main() {
 | 
				
			||||||
 | 
						ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
 | 
				
			||||||
 | 
						console := console{os.Stdin, os.Stdout, os.Stderr, ctx, stop}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						go func() { <-ctx.Done(); console.Log("shutdown"); stop() }()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						args := args{
 | 
				
			||||||
 | 
							env("XT_DBTYPE", "sqlite3"),
 | 
				
			||||||
 | 
							env("XT_DBFILE", "file:twt.db"),
 | 
				
			||||||
 | 
							env("XT_BASE_FEED", "feed"),
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						console.Set("args", args)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := run(console); err != nil {
 | 
				
			||||||
 | 
							fmt.Println(err)
 | 
				
			||||||
 | 
							os.Exit(1)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										262
									
								
								service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										262
									
								
								service.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,262 @@
 | 
				
			|||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"database/sql"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_ "embed"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_ "github.com/mattn/go-sqlite3"
 | 
				
			||||||
 | 
						"go.yarn.social/lextwt"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func run(c console) error {
 | 
				
			||||||
 | 
						ctx := c.Context
 | 
				
			||||||
 | 
						a := c.Args()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						db, err := sql.Open(a.dbtype, a.dbfile)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer db.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, stmt := range strings.Split(initSQL, ";") {
 | 
				
			||||||
 | 
							_, err = db.ExecContext(ctx, stmt)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.Set("db", db)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						f, err := os.Open(a.baseFeed)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer f.Close()
 | 
				
			||||||
 | 
						err = loadFeed(db, f)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.Log("ready")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						go refreshLoop(c)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<-c.Done()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var (
 | 
				
			||||||
 | 
						//go:embed init.sql
 | 
				
			||||||
 | 
						initSQL string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						insertFeed = `
 | 
				
			||||||
 | 
							insert into feeds 
 | 
				
			||||||
 | 
								(feed_id, uri, nick, last_scan_on, refresh_rate) 
 | 
				
			||||||
 | 
							values (?, ?, ?, ?, ?) 
 | 
				
			||||||
 | 
							ON CONFLICT (feed_id) DO NOTHING
 | 
				
			||||||
 | 
						`
 | 
				
			||||||
 | 
						insertTwt = `
 | 
				
			||||||
 | 
							insert into twts 
 | 
				
			||||||
 | 
								(feed_id, hash, conv, dt, text, mentions, tags) 
 | 
				
			||||||
 | 
							values (?, ?, ?, ?, ?, ?, ?)
 | 
				
			||||||
 | 
							ON CONFLICT (feed_id, hash) DO NOTHING
 | 
				
			||||||
 | 
						`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fetchFeeds = `
 | 
				
			||||||
 | 
							select feed_id, uri, nick, last_scan_on, refresh_rate from feeds
 | 
				
			||||||
 | 
						`
 | 
				
			||||||
 | 
						updateFeed = `
 | 
				
			||||||
 | 
							update feeds set 
 | 
				
			||||||
 | 
								last_scan_on = ?,
 | 
				
			||||||
 | 
								refresh_rate = ? 
 | 
				
			||||||
 | 
							where feed_id = ?
 | 
				
			||||||
 | 
						`
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func loadFeed(db *sql.DB, feed io.Reader) error {
 | 
				
			||||||
 | 
						loadTS := time.Now()
 | 
				
			||||||
 | 
						refreshRate := 600
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						f, err := lextwt.ParseFile(feed, nil)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						feedID := urlNS.UUID5(f.Twter().HashingURI)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						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, _ := strings.Cut(f.Value(), " ")
 | 
				
			||||||
 | 
							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()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type feed struct {
 | 
				
			||||||
 | 
						ID          uuid
 | 
				
			||||||
 | 
						URI         string
 | 
				
			||||||
 | 
						Nick        string
 | 
				
			||||||
 | 
						LastScanOn  sql.NullTime
 | 
				
			||||||
 | 
						RefreshRate int
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func refreshLoop(c console) {
 | 
				
			||||||
 | 
						maxInt := int(^uint(0) >> 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						less := func(a, b *feed) bool {
 | 
				
			||||||
 | 
							return a.LastScanOn.Time.Before(b.LastScanOn.Time)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						queue := FibHeap(less)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						db := c.Get("db").(*sql.DB)
 | 
				
			||||||
 | 
						res, err := db.QueryContext(c.Context, fetchFeeds)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.Log(err)
 | 
				
			||||||
 | 
							c.abort()
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.Log("load feeds")
 | 
				
			||||||
 | 
						for res.Next() {
 | 
				
			||||||
 | 
							var f feed
 | 
				
			||||||
 | 
							err = res.Scan(&f.ID, &f.URI, &f.Nick, &f.LastScanOn, &f.RefreshRate)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								c.Log(err)
 | 
				
			||||||
 | 
								c.abort()
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if !f.LastScanOn.Valid {
 | 
				
			||||||
 | 
								f.LastScanOn.Time = time.Now()
 | 
				
			||||||
 | 
								f.LastScanOn.Valid = true
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							f.LastScanOn.Time.Add(time.Duration(f.RefreshRate) * time.Second)
 | 
				
			||||||
 | 
							queue.Insert(&f)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.Log("start refresh loop")
 | 
				
			||||||
 | 
						for !queue.IsEmpty() {
 | 
				
			||||||
 | 
							f := queue.ExtractMin()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							select {
 | 
				
			||||||
 | 
							case <-c.Done():
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							case <-time.After(f.LastScanOn.Time.Sub(time.Now())):
 | 
				
			||||||
 | 
								c.Log("refresh", f.URI)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							req, err := http.NewRequestWithContext(c.Context, "GET", f.URI, nil)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								c.Log(err)
 | 
				
			||||||
 | 
								c.abort()
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							resp, err := http.DefaultClient.Do(req)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								c.Log(err)
 | 
				
			||||||
 | 
								_, err = db.ExecContext(c.Context, updateFeed, f.LastScanOn, maxInt, f.ID)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									c.Log(err)
 | 
				
			||||||
 | 
									c.abort()
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							defer resp.Body.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							err = loadFeed(db, resp.Body)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								_, err = db.ExecContext(c.Context, updateFeed, f.LastScanOn, maxInt, f.ID)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									c.Log(err)
 | 
				
			||||||
 | 
									c.abort()
 | 
				
			||||||
 | 
									return					
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							f.LastScanOn.Time = time.Now()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							db.ExecContext(c.Context, updateFeed, f.LastScanOn, f.RefreshRate, f.ID)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							f.LastScanOn.Time.Add(time.Duration(f.RefreshRate) * time.Second)
 | 
				
			||||||
 | 
							queue.Insert(f)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										77
									
								
								uuid.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								uuid.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,77 @@
 | 
				
			|||||||
 | 
					package main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"crypto/sha1"
 | 
				
			||||||
 | 
						"database/sql/driver"
 | 
				
			||||||
 | 
						"encoding/hex"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type uuid [16]byte
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var urlNS = uuid{0x6b, 0xa7, 0xb8, 0x10, 0x9d, 0xad, 0x11, 0xd1, 0x80, 0xb4, 0x00, 0xc0, 0x4f, 0xd4, 0x30, 0xc8}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (u uuid) UUID5(value string) uuid {
 | 
				
			||||||
 | 
						h := sha1.New()
 | 
				
			||||||
 | 
						h.Write(u[:])
 | 
				
			||||||
 | 
						h.Write([]byte(value))
 | 
				
			||||||
 | 
						return uuid(h.Sum(nil))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (u *uuid) UnmarshalText(data string) error {
 | 
				
			||||||
 | 
						data = strings.Trim(data, "{}")
 | 
				
			||||||
 | 
						data = strings.ReplaceAll(data, "-", "")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						_, err := hex.Decode(u[:], []byte(data))
 | 
				
			||||||
 | 
						return err
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					func (u uuid) MarshalText() string {
 | 
				
			||||||
 | 
						s := hex.EncodeToString(u[:])
 | 
				
			||||||
 | 
						return fmt.Sprintf("{%s-%s-%s-%s-%s}", s[:8], s[8:12], s[12:16], s[16:20], s[20:])
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (u uuid) Value() (driver.Value, error) {
 | 
				
			||||||
 | 
						return u[:], nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (u *uuid) Scan(value any) error {
 | 
				
			||||||
 | 
						if value == nil {
 | 
				
			||||||
 | 
							return nil
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						*u = uuid(value.([]byte))
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type uuids []uuid
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (lis uuids) ToStrList() strList {
 | 
				
			||||||
 | 
						arr := make(strList, len(lis))
 | 
				
			||||||
 | 
						for i, v := range lis {
 | 
				
			||||||
 | 
							arr[i] = v.MarshalText()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return arr
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type strList []string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (l *strList) Scan(value any) error {
 | 
				
			||||||
 | 
						s := value.(string)
 | 
				
			||||||
 | 
						s = strings.Trim(s, "[]")
 | 
				
			||||||
 | 
						for _, v := range strings.Split(s, ",") {
 | 
				
			||||||
 | 
							v = strings.TrimSpace(v)
 | 
				
			||||||
 | 
							v = strings.Trim(v, "\",")
 | 
				
			||||||
 | 
							*l = append(*l, v)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (l strList) Value() (driver.Value, error) {
 | 
				
			||||||
 | 
						arr := make([]string, len(l))
 | 
				
			||||||
 | 
						for i, v := range l {
 | 
				
			||||||
 | 
							arr[i] = "\""+v+"\""
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return "["+strings.Join(arr, ",") +"]", nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user