Initial checkin
This commit is contained in:
commit
881c618041
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
badger/
|
||||
dist/
|
||||
tmp/
|
||||
linkwallet
|
39
.goreleaser.yaml
Normal file
39
.goreleaser.yaml
Normal file
@ -0,0 +1,39 @@
|
||||
# This is an example .goreleaser.yml file with some sensible defaults.
|
||||
# Make sure to check the documentation at https://goreleaser.com
|
||||
before:
|
||||
hooks:
|
||||
# You may remove this if you don't use go modules.
|
||||
- go mod tidy
|
||||
# you may remove this if you don't need go generate
|
||||
- go generate ./...
|
||||
builds:
|
||||
- main: cmd/linkwallet/linkwallet.go
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
dockers:
|
||||
- image_templates:
|
||||
- "tardisx/linkwallet:{{ .Tag }}"
|
||||
- "tardisx/linkwallet:v{{ .Major }}"
|
||||
- "tardisx/linkwallet:v{{ .Major }}.{{ .Minor }}"
|
||||
- "tardisx/linkwallet"
|
||||
archives:
|
||||
- replacements:
|
||||
darwin: Darwin
|
||||
linux: Linux
|
||||
windows: Windows
|
||||
386: i386
|
||||
amd64: x86_64
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: "{{ incpatch .Version }}-next"
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
exclude:
|
||||
- '^docs:'
|
||||
- '^test:'
|
10
.vscode/settings.json
vendored
Normal file
10
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"badgerhold",
|
||||
"colly",
|
||||
"incpatch",
|
||||
"linkwallet",
|
||||
"serialised",
|
||||
"stopword"
|
||||
]
|
||||
}
|
3
Dockerfile
Normal file
3
Dockerfile
Normal file
@ -0,0 +1,3 @@
|
||||
FROM scratch
|
||||
ENTRYPOINT ["/linkwallet"]
|
||||
COPY linkwallet /
|
99
content/content.go
Normal file
99
content/content.go
Normal file
@ -0,0 +1,99 @@
|
||||
package content
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/tardisx/linkwallet/entity"
|
||||
|
||||
"github.com/gocolly/colly"
|
||||
snowballeng "github.com/kljensen/snowball/english"
|
||||
)
|
||||
|
||||
func FetchPageInfo(bm entity.Bookmark) entity.PageInfo {
|
||||
info := entity.PageInfo{
|
||||
Fetched: time.Now(),
|
||||
}
|
||||
|
||||
url := bm.URL
|
||||
|
||||
c := colly.NewCollector()
|
||||
c.SetRequestTimeout(5 * time.Second)
|
||||
|
||||
// On every a element which has href attribute call callback
|
||||
c.OnHTML("p,h1,h2,h3,h4,h5,h6,li", func(e *colly.HTMLElement) {
|
||||
info.RawText = info.RawText + e.Text + "\n"
|
||||
})
|
||||
|
||||
c.OnHTML("head>title", func(h *colly.HTMLElement) {
|
||||
info.Title = h.Text
|
||||
})
|
||||
|
||||
c.OnResponse(func(r *colly.Response) {
|
||||
info.StatusCode = r.StatusCode // https://adventofcode.com/2016/day/25
|
||||
info.Size = len(r.Body)
|
||||
log.Printf("content type for %s: %s (%d)", r.Request.URL.String(), r.Headers.Get("Content-Type"), info.Size)
|
||||
|
||||
})
|
||||
|
||||
// // Before making a request print "Visiting ..."
|
||||
c.OnRequest(func(r *colly.Request) {
|
||||
log.Println("Visiting", r.URL.String())
|
||||
})
|
||||
|
||||
c.OnError(func(r *colly.Response, err error) {
|
||||
log.Printf("error for %s: %s", r.Request.URL.String(), err)
|
||||
})
|
||||
|
||||
// Start scraping on https://hackerspaces.org
|
||||
c.Visit(url)
|
||||
return info
|
||||
}
|
||||
|
||||
func Words(bm *entity.Bookmark) []string {
|
||||
words := []string{}
|
||||
|
||||
words = append(words, StringToSearchWords(bm.Info.RawText)...)
|
||||
words = append(words, StringToSearchWords(bm.Info.Title)...)
|
||||
words = append(words, StringToSearchWords(bm.URL)...)
|
||||
return words
|
||||
}
|
||||
|
||||
func StringToSearchWords(s string) []string {
|
||||
words := []string{}
|
||||
|
||||
words = append(words, stemmerFilter(stopwordFilter(tokenize(s)))...)
|
||||
return words
|
||||
}
|
||||
|
||||
func tokenize(text string) []string {
|
||||
return strings.FieldsFunc(text, func(r rune) bool {
|
||||
// Split on any character that is not a letter or a number.
|
||||
return !unicode.IsLetter(r) && !unicode.IsNumber(r)
|
||||
})
|
||||
}
|
||||
|
||||
func stemmerFilter(tokens []string) []string {
|
||||
r := make([]string, len(tokens))
|
||||
for i, token := range tokens {
|
||||
r[i] = snowballeng.Stem(token, false)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
var stopwords = map[string]struct{}{ // I wish Go had built-in sets.
|
||||
"a": {}, "and": {}, "be": {}, "have": {}, "i": {},
|
||||
"in": {}, "of": {}, "that": {}, "the": {}, "to": {},
|
||||
}
|
||||
|
||||
func stopwordFilter(tokens []string) []string {
|
||||
r := make([]string, 0, len(tokens))
|
||||
for _, token := range tokens {
|
||||
if _, ok := stopwords[token]; !ok {
|
||||
r = append(r, token)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
208
db/bookmarks.go
Normal file
208
db/bookmarks.go
Normal file
@ -0,0 +1,208 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tardisx/linkwallet/content"
|
||||
"github.com/tardisx/linkwallet/entity"
|
||||
|
||||
"github.com/timshannon/badgerhold/v4"
|
||||
)
|
||||
|
||||
type BookmarkManager struct {
|
||||
db *DB
|
||||
scrapeQueue chan *entity.Bookmark
|
||||
}
|
||||
|
||||
func NewBookmarkManager(db *DB) *BookmarkManager {
|
||||
return &BookmarkManager{db: db, scrapeQueue: make(chan *entity.Bookmark)}
|
||||
}
|
||||
|
||||
// AddBookmark adds a bookmark to the database. It returns an error
|
||||
// if this bookmark already exists (based on URL match).
|
||||
// The entity.Bookmark ID field will be updated.
|
||||
func (m *BookmarkManager) AddBookmark(bm *entity.Bookmark) error {
|
||||
existing := entity.Bookmark{}
|
||||
err := m.db.store.FindOne(&existing, badgerhold.Where("URL").Eq(bm.URL))
|
||||
if err != badgerhold.ErrNotFound {
|
||||
return fmt.Errorf("bookmark already exists")
|
||||
}
|
||||
bm.TimestampCreated = time.Now()
|
||||
err = m.db.store.Insert(badgerhold.NextSequence(), bm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("addBookmark returned: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListBookmarks returns all bookmarks.
|
||||
func (m *BookmarkManager) ListBookmarks() ([]entity.Bookmark, error) {
|
||||
bookmarks := make([]entity.Bookmark, 0, 0)
|
||||
err := m.db.store.Find(&bookmarks, &badgerhold.Query{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return bookmarks, nil
|
||||
}
|
||||
|
||||
func (m *BookmarkManager) SaveBookmark(bm *entity.Bookmark) error {
|
||||
err := m.db.store.Update(bm.ID, &bm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *BookmarkManager) LoadBookmarkByID(id uint64) entity.Bookmark {
|
||||
// log.Printf("loading %v", ids)
|
||||
ret := entity.Bookmark{}
|
||||
log.Printf("loading id %d", id)
|
||||
err := m.db.store.Get(id, &ret)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (m *BookmarkManager) LoadBookmarksByIDs(ids []uint64) []entity.Bookmark {
|
||||
// log.Printf("loading %v", ids)
|
||||
ret := make([]entity.Bookmark, 0, 0)
|
||||
|
||||
s := make([]interface{}, len(ids))
|
||||
for i, v := range ids {
|
||||
s[i] = v
|
||||
}
|
||||
|
||||
err := m.db.store.Find(&ret, badgerhold.Where("ID").In(s...))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (m *BookmarkManager) Search(query string) ([]entity.Bookmark, error) {
|
||||
rets := make([]uint64, 0, 0)
|
||||
|
||||
counts := make(map[uint64]uint8)
|
||||
|
||||
words := content.StringToSearchWords(query)
|
||||
|
||||
for _, word := range words {
|
||||
var wi *entity.WordIndex
|
||||
err := m.db.store.Get("word_index_"+word, &wi)
|
||||
if err == badgerhold.ErrNotFound {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error retrieving index: %w", err)
|
||||
}
|
||||
for k := range wi.Bitmap {
|
||||
counts[k]++
|
||||
}
|
||||
}
|
||||
|
||||
// log.Printf("counts: %#v", counts)
|
||||
|
||||
for k, v := range counts {
|
||||
if v == uint8(len(words)) {
|
||||
rets = append(rets, k)
|
||||
if len(rets) > 10 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return m.LoadBookmarksByIDs(rets), nil
|
||||
}
|
||||
|
||||
func (m *BookmarkManager) ScrapeAndIndex(bm *entity.Bookmark) error {
|
||||
|
||||
log.Printf("Start scrape for %s", bm.URL)
|
||||
info := content.FetchPageInfo(*bm)
|
||||
bm.Info = info
|
||||
bm.TimestampLastScraped = time.Now()
|
||||
err := m.SaveBookmark(bm)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
words := content.Words(bm)
|
||||
log.Printf("index for %d %s (%d words)", bm.ID, bm.URL, len(words))
|
||||
m.db.UpdateIndexForWordsByID(words, bm.ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *BookmarkManager) QueueScrape(bm *entity.Bookmark) {
|
||||
m.scrapeQueue <- bm
|
||||
}
|
||||
|
||||
func (m *BookmarkManager) RunQueue() {
|
||||
type localScrapeQueue struct {
|
||||
queue []*entity.Bookmark
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
localQueue := localScrapeQueue{queue: make([]*entity.Bookmark, 0)}
|
||||
// accept things off the queue immediately
|
||||
go func() {
|
||||
for {
|
||||
newItem := <-m.scrapeQueue
|
||||
|
||||
newItem.TimestampLastScraped = time.Now()
|
||||
err := m.SaveBookmark(newItem)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
localQueue.mutex.Lock()
|
||||
localQueue.queue = append(localQueue.queue, newItem)
|
||||
localQueue.mutex.Unlock()
|
||||
log.Printf("queue now has %d entries", len(localQueue.queue))
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
localQueue.mutex.Lock()
|
||||
if len(localQueue.queue) > 0 {
|
||||
processBM := localQueue.queue[0]
|
||||
localQueue.queue = localQueue.queue[1:]
|
||||
localQueue.mutex.Unlock()
|
||||
|
||||
m.ScrapeAndIndex(processBM)
|
||||
|
||||
} else {
|
||||
localQueue.mutex.Unlock()
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *BookmarkManager) UpdateContent() {
|
||||
ret := make([]entity.Bookmark, 0)
|
||||
for {
|
||||
ret = []entity.Bookmark{}
|
||||
deadline := time.Now().Add(time.Hour * -24 * 7)
|
||||
err := m.db.store.Find(&ret, badgerhold.Where("TimestampLastScraped").Lt(deadline))
|
||||
if err == badgerhold.ErrNotFound {
|
||||
log.Printf("none qualify")
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, bm := range ret {
|
||||
thisBM := bm
|
||||
log.Printf("queueing %d because %s", thisBM.ID, thisBM.TimestampLastScraped)
|
||||
m.QueueScrape(&thisBM)
|
||||
}
|
||||
time.Sleep(time.Second * 5)
|
||||
}
|
||||
}
|
35
db/db.go
Normal file
35
db/db.go
Normal file
@ -0,0 +1,35 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/tardisx/linkwallet/entity"
|
||||
|
||||
badgerhold "github.com/timshannon/badgerhold/v4"
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
store *badgerhold.Store
|
||||
}
|
||||
|
||||
func (db *DB) Open(dir string) {
|
||||
options := badgerhold.DefaultOptions
|
||||
options.Dir = dir
|
||||
options.ValueDir = dir
|
||||
store, err := badgerhold.Open(options)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
}
|
||||
db.store = store
|
||||
}
|
||||
|
||||
func (db *DB) Close() {
|
||||
db.store.Close()
|
||||
}
|
||||
|
||||
func (db *DB) Dumpy() {
|
||||
res := make([]entity.Bookmark, 0, 0)
|
||||
db.store.Find(&res, &badgerhold.Query{})
|
||||
log.Printf("%v", res)
|
||||
}
|
75
db/index.go
Normal file
75
db/index.go
Normal file
@ -0,0 +1,75 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/tardisx/linkwallet/entity"
|
||||
|
||||
badgerhold "github.com/timshannon/badgerhold/v4"
|
||||
)
|
||||
|
||||
func (db *DB) InitIndices() {
|
||||
wi := entity.WordIndex{}
|
||||
db.store.DeleteMatching(wi, &badgerhold.Query{})
|
||||
}
|
||||
|
||||
func (db *DB) UpdateIndexForWordsByID(words []string, id uint64) {
|
||||
// delete this id from all indices
|
||||
txn := db.store.Badger().NewTransaction(true)
|
||||
|
||||
db.store.TxForEach(txn, &badgerhold.Query{}, func(wi *entity.WordIndex) {
|
||||
// log.Printf("considering this one: %s", wi.Word)
|
||||
delete(wi.Bitmap, id)
|
||||
})
|
||||
|
||||
// addiing
|
||||
var find, store time.Duration
|
||||
for i, word := range words {
|
||||
// log.Printf("indexing %s", word)
|
||||
tF := time.Now()
|
||||
thisWI := entity.WordIndex{Word: word}
|
||||
err := db.store.TxGet(txn, "word_index_"+word, &thisWI)
|
||||
// err := db.store.TxFindOne(txn, &thisWI, badgerhold.Where("Word").Eq(word).Index("Word"))
|
||||
if err == badgerhold.ErrNotFound {
|
||||
// create it
|
||||
thisWI.Bitmap = map[uint64]bool{}
|
||||
} else if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
findT := time.Since(tF)
|
||||
|
||||
tS := time.Now()
|
||||
thisWI.Bitmap[id] = true
|
||||
// log.Printf("BM: %v", thisWI.Bitmap)
|
||||
err = db.store.TxUpsert(txn, "word_index_"+word, thisWI)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
findS := time.Since(tS)
|
||||
find += findT
|
||||
store += findS
|
||||
|
||||
if i > 0 && i%100 == 0 {
|
||||
txn.Commit()
|
||||
txn = db.store.Badger().NewTransaction(true)
|
||||
}
|
||||
|
||||
}
|
||||
//log.Printf("find %s store %s", find, store)
|
||||
|
||||
txn.Commit()
|
||||
}
|
||||
|
||||
func (db *DB) DumpIndex() {
|
||||
|
||||
// delete this id from all indices
|
||||
err := db.store.ForEach(&badgerhold.Query{}, func(wi *entity.WordIndex) error {
|
||||
log.Printf("%10s: %v", wi.Word, wi.Bitmap)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
}
|
20
entity/bookmark.go
Normal file
20
entity/bookmark.go
Normal file
@ -0,0 +1,20 @@
|
||||
package entity
|
||||
|
||||
import "time"
|
||||
|
||||
type Bookmark struct {
|
||||
ID uint64 `badgerhold:"key"`
|
||||
URL string
|
||||
Info PageInfo
|
||||
Tags []string
|
||||
TimestampCreated time.Time
|
||||
TimestampLastScraped time.Time
|
||||
}
|
||||
|
||||
type PageInfo struct {
|
||||
Fetched time.Time
|
||||
Title string
|
||||
Size int
|
||||
StatusCode int
|
||||
RawText string
|
||||
}
|
39
entity/index.go
Normal file
39
entity/index.go
Normal file
@ -0,0 +1,39 @@
|
||||
package entity
|
||||
|
||||
type WordIndex struct {
|
||||
Word string `badgerhold:"index"`
|
||||
// Bitmap roaring.Bitmap
|
||||
Bitmap map[uint64]bool
|
||||
}
|
||||
|
||||
// func (wi WordIndex) GobEncode() ([]byte, error) {
|
||||
|
||||
// bmBuf := new(bytes.Buffer)
|
||||
// wi.Bitmap.WriteTo(bmBuf) // we omit error handling
|
||||
|
||||
// wordBytes := []byte(wi.Word)
|
||||
// serialised := make([]byte, 4, 4)
|
||||
// binary.BigEndian.PutUint32(serialised, uint32(len(wordBytes)))
|
||||
|
||||
// serialised = append(serialised, wordBytes...)
|
||||
// serialised = append(serialised, bmBuf.Bytes()...)
|
||||
// // log.Printf("serialised: %v", serialised)
|
||||
|
||||
// // log.Printf("serialised to %d bytes for word %w\n%#v", len(serialised), wi.Word, serialised)
|
||||
|
||||
// return serialised, nil
|
||||
// }
|
||||
|
||||
// func (wi *WordIndex) GobDecode(b []byte) error {
|
||||
// size := binary.BigEndian.Uint32(b[0:4])
|
||||
// wi.Word = string(b[4 : size+4])
|
||||
// // log.Printf("word is %s size was %d\n%v", wi.Word, size, b)
|
||||
|
||||
// bmBuf := bytes.NewReader(b[size+4:])
|
||||
|
||||
// wi.Bitmap = *roaring.New()
|
||||
// _, err := wi.Bitmap.ReadFrom(bmBuf)
|
||||
// // log.Printf("N: %d, err: %s", n, err)
|
||||
|
||||
// return err
|
||||
// }
|
57
go.mod
Normal file
57
go.mod
Normal file
@ -0,0 +1,57 @@
|
||||
module github.com/tardisx/linkwallet
|
||||
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.7.7
|
||||
github.com/gocolly/colly v1.2.0
|
||||
github.com/kljensen/snowball v0.6.0
|
||||
github.com/timshannon/badgerhold/v4 v4.0.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.13.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.17.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.4.1 // indirect
|
||||
github.com/json-iterator/go v1.1.9 // indirect
|
||||
github.com/leodido/go-urn v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.12 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect
|
||||
github.com/ugorji/go/codec v1.1.7 // indirect
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.8 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.8.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.1 // indirect
|
||||
github.com/antchfx/htmlquery v1.2.4 // indirect
|
||||
github.com/antchfx/xmlquery v1.3.10 // indirect
|
||||
github.com/antchfx/xpath v1.2.0 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.1 // indirect
|
||||
github.com/dgraph-io/badger/v3 v3.2103.2 // indirect
|
||||
github.com/dgraph-io/ristretto v0.1.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/glog v0.0.0-20210429001901-424d2337a529 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/flatbuffers v2.0.0+incompatible // indirect
|
||||
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
|
||||
github.com/kennygrant/sanitize v1.2.4 // indirect
|
||||
github.com/klauspost/compress v1.13.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca // indirect
|
||||
github.com/temoto/robotstxt v1.1.2 // indirect
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
golang.org/x/net v0.0.0-20220517181318-183a9ca12b87 // indirect
|
||||
golang.org/x/sys v0.0.0-20220519141025-dcacdad47464 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
)
|
277
go.sum
Normal file
277
go.sum
Normal file
@ -0,0 +1,277 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
|
||||
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
|
||||
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
|
||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||
github.com/antchfx/htmlquery v1.2.4 h1:qLteofCMe/KGovBI6SQgmou2QNyedFUW+pE+BpeZ494=
|
||||
github.com/antchfx/htmlquery v1.2.4/go.mod h1:2xO6iu3EVWs7R2JYqBbp8YzG50gj/ofqs5/0VZoDZLc=
|
||||
github.com/antchfx/xmlquery v1.3.10 h1:U2yMwr8U0KmGM2iDG2Ky/3LfxNsiK4uw1bSBkeMO9+g=
|
||||
github.com/antchfx/xmlquery v1.3.10/go.mod h1:wojC/BxjEkjJt6dPiAqUzoXO5nIMWtxHS8PD8TmN4ks=
|
||||
github.com/antchfx/xpath v1.2.0 h1:mbwv7co+x0RwgeGAOHdrKy89GvHaGvxxBtPK0uF9Zr8=
|
||||
github.com/antchfx/xpath v1.2.0/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgraph-io/badger/v3 v3.2103.1/go.mod h1:dULbq6ehJ5K0cGW/1TQ9iSfUk0gbSiToDWmWmTsJ53E=
|
||||
github.com/dgraph-io/badger/v3 v3.2103.2 h1:dpyM5eCJAtQCBcMCZcT4UBZchuTJgCywerHHgmxfxM8=
|
||||
github.com/dgraph-io/badger/v3 v3.2103.2/go.mod h1:RHo4/GmYcKKh5Lxu63wLEMHJ70Pac2JqZRYGhlyAo2M=
|
||||
github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI=
|
||||
github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
|
||||
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
|
||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/gocolly/colly v1.2.0 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI=
|
||||
github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/glog v0.0.0-20210429001901-424d2337a529 h1:2voWjNECnrZRbfwXxHB1/j8wa6xdKn85B5NzgVL/pTU=
|
||||
github.com/golang/glog v0.0.0-20210429001901-424d2337a529/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/flatbuffers v1.12.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/flatbuffers v2.0.0+incompatible h1:dicJ2oXwypfwUGnB2/TYWYEKiuk9eYQlQO/AnOHl5mI=
|
||||
github.com/google/flatbuffers v2.0.0+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b h1:wDUNC2eKiL35DbLvsDhiblTUXHxcOPwQSCzi7xpQUN4=
|
||||
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o=
|
||||
github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||
github.com/klauspost/compress v1.13.1 h1:wXr2uRxZTJXHLly6qhJabee5JqIhTRoLBhDOA74hDEQ=
|
||||
github.com/klauspost/compress v1.13.1/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||
github.com/kljensen/snowball v0.6.0 h1:6DZLCcZeL0cLfodx+Md4/OLC6b/bfurWUOUGs1ydfOU=
|
||||
github.com/kljensen/snowball v0.6.0/go.mod h1:27N7E8fVU5H68RlUmnWwZCfxgt4POBJfENGMvNRhldw=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI=
|
||||
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg=
|
||||
github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo=
|
||||
github.com/timshannon/badgerhold/v3 v3.0.0-20210909134927-2b6764d68c1e h1:zWSVsQaifg0cVH9VvR+cMguV7exK6U+SoW8YD1cZpR4=
|
||||
github.com/timshannon/badgerhold/v3 v3.0.0-20210909134927-2b6764d68c1e/go.mod h1:/Seq5xGNo8jLhSbDX3jdbeZrp4yFIpQ6/7n4TjziEWs=
|
||||
github.com/timshannon/badgerhold/v4 v4.0.2 h1:83OLY/NFnEaMnHEPd84bYtkLipVkjTsMbzQRYbk47g4=
|
||||
github.com/timshannon/badgerhold/v4 v4.0.2/go.mod h1:rh6RyXLQFsvrvcKondPQQFZnNovpRzu+gS0FlLxYuHY=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
|
||||
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220517181318-183a9ca12b87 h1:cCR+9mKLOGyX4Zx+uBZDXEDAQsvKQ/XbW4vreG5v1jU=
|
||||
golang.org/x/net v0.0.0-20220517181318-183a9ca12b87/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220519141025-dcacdad47464 h1:MpIuURY70f0iKp/oooEFtB2oENcHITo/z1b6u41pKCw=
|
||||
golang.org/x/sys v0.0.0-20220519141025-dcacdad47464/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
0
web/static/css/app.css
Normal file
0
web/static/css/app.css
Normal file
6456
web/static/css/foundation.css
vendored
Normal file
6456
web/static/css/foundation.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
web/static/css/foundation.min.css
vendored
Normal file
1
web/static/css/foundation.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
web/static/image/beating.gif
Normal file
BIN
web/static/image/beating.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
1
web/static/js/app.js
Normal file
1
web/static/js/app.js
Normal file
@ -0,0 +1 @@
|
||||
$(document).foundation()
|
1
web/static/js/htmx.min.js
vendored
Normal file
1
web/static/js/htmx.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
531
web/static/js/vendor/foundation.js
vendored
Normal file
531
web/static/js/vendor/foundation.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/static/js/vendor/foundation.min.js
vendored
Normal file
1
web/static/js/vendor/foundation.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
10881
web/static/js/vendor/jquery.js
vendored
Normal file
10881
web/static/js/vendor/jquery.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
517
web/static/js/vendor/what-input.js
vendored
Normal file
517
web/static/js/vendor/what-input.js
vendored
Normal file
@ -0,0 +1,517 @@
|
||||
/**
|
||||
* what-input - A global utility for tracking the current input method (mouse, keyboard or touch).
|
||||
* @version v5.2.10
|
||||
* @link https://github.com/ten1seven/what-input
|
||||
* @license MIT
|
||||
*/
|
||||
(function webpackUniversalModuleDefinition(root, factory) {
|
||||
if(typeof exports === 'object' && typeof module === 'object')
|
||||
module.exports = factory();
|
||||
else if(typeof define === 'function' && define.amd)
|
||||
define("whatInput", [], factory);
|
||||
else if(typeof exports === 'object')
|
||||
exports["whatInput"] = factory();
|
||||
else
|
||||
root["whatInput"] = factory();
|
||||
})(this, function() {
|
||||
return /******/ (function(modules) { // webpackBootstrap
|
||||
/******/ // The module cache
|
||||
/******/ var installedModules = {};
|
||||
|
||||
/******/ // The require function
|
||||
/******/ function __webpack_require__(moduleId) {
|
||||
|
||||
/******/ // Check if module is in cache
|
||||
/******/ if(installedModules[moduleId])
|
||||
/******/ return installedModules[moduleId].exports;
|
||||
|
||||
/******/ // Create a new module (and put it into the cache)
|
||||
/******/ var module = installedModules[moduleId] = {
|
||||
/******/ exports: {},
|
||||
/******/ id: moduleId,
|
||||
/******/ loaded: false
|
||||
/******/ };
|
||||
|
||||
/******/ // Execute the module function
|
||||
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
||||
|
||||
/******/ // Flag the module as loaded
|
||||
/******/ module.loaded = true;
|
||||
|
||||
/******/ // Return the exports of the module
|
||||
/******/ return module.exports;
|
||||
/******/ }
|
||||
|
||||
|
||||
/******/ // expose the modules object (__webpack_modules__)
|
||||
/******/ __webpack_require__.m = modules;
|
||||
|
||||
/******/ // expose the module cache
|
||||
/******/ __webpack_require__.c = installedModules;
|
||||
|
||||
/******/ // __webpack_public_path__
|
||||
/******/ __webpack_require__.p = "";
|
||||
|
||||
/******/ // Load entry module and return exports
|
||||
/******/ return __webpack_require__(0);
|
||||
/******/ })
|
||||
/************************************************************************/
|
||||
/******/ ([
|
||||
/* 0 */
|
||||
/***/ (function(module, exports) {
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = function () {
|
||||
/*
|
||||
* bail out if there is no document or window
|
||||
* (i.e. in a node/non-DOM environment)
|
||||
*
|
||||
* Return a stubbed API instead
|
||||
*/
|
||||
if (typeof document === 'undefined' || typeof window === 'undefined') {
|
||||
return {
|
||||
// always return "initial" because no interaction will ever be detected
|
||||
ask: function ask() {
|
||||
return 'initial';
|
||||
},
|
||||
|
||||
// always return null
|
||||
element: function element() {
|
||||
return null;
|
||||
},
|
||||
|
||||
// no-op
|
||||
ignoreKeys: function ignoreKeys() {},
|
||||
|
||||
// no-op
|
||||
specificKeys: function specificKeys() {},
|
||||
|
||||
// no-op
|
||||
registerOnChange: function registerOnChange() {},
|
||||
|
||||
// no-op
|
||||
unRegisterOnChange: function unRegisterOnChange() {}
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* variables
|
||||
*/
|
||||
|
||||
// cache document.documentElement
|
||||
var docElem = document.documentElement;
|
||||
|
||||
// currently focused dom element
|
||||
var currentElement = null;
|
||||
|
||||
// last used input type
|
||||
var currentInput = 'initial';
|
||||
|
||||
// last used input intent
|
||||
var currentIntent = currentInput;
|
||||
|
||||
// UNIX timestamp of current event
|
||||
var currentTimestamp = Date.now();
|
||||
|
||||
// check for a `data-whatpersist` attribute on either the `html` or `body` elements, defaults to `true`
|
||||
var shouldPersist = 'false';
|
||||
|
||||
// form input types
|
||||
var formInputs = ['button', 'input', 'select', 'textarea'];
|
||||
|
||||
// empty array for holding callback functions
|
||||
var functionList = [];
|
||||
|
||||
// list of modifier keys commonly used with the mouse and
|
||||
// can be safely ignored to prevent false keyboard detection
|
||||
var ignoreMap = [16, // shift
|
||||
17, // control
|
||||
18, // alt
|
||||
91, // Windows key / left Apple cmd
|
||||
93 // Windows menu / right Apple cmd
|
||||
];
|
||||
|
||||
var specificMap = [];
|
||||
|
||||
// mapping of events to input types
|
||||
var inputMap = {
|
||||
keydown: 'keyboard',
|
||||
keyup: 'keyboard',
|
||||
mousedown: 'mouse',
|
||||
mousemove: 'mouse',
|
||||
MSPointerDown: 'pointer',
|
||||
MSPointerMove: 'pointer',
|
||||
pointerdown: 'pointer',
|
||||
pointermove: 'pointer',
|
||||
touchstart: 'touch',
|
||||
touchend: 'touch'
|
||||
|
||||
// boolean: true if the page is being scrolled
|
||||
};var isScrolling = false;
|
||||
|
||||
// store current mouse position
|
||||
var mousePos = {
|
||||
x: null,
|
||||
y: null
|
||||
|
||||
// map of IE 10 pointer events
|
||||
};var pointerMap = {
|
||||
2: 'touch',
|
||||
3: 'touch', // treat pen like touch
|
||||
4: 'mouse'
|
||||
|
||||
// check support for passive event listeners
|
||||
};var supportsPassive = false;
|
||||
|
||||
try {
|
||||
var opts = Object.defineProperty({}, 'passive', {
|
||||
get: function get() {
|
||||
supportsPassive = true;
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('test', null, opts);
|
||||
} catch (e) {}
|
||||
// fail silently
|
||||
|
||||
|
||||
/*
|
||||
* set up
|
||||
*/
|
||||
|
||||
var setUp = function setUp() {
|
||||
// add correct mouse wheel event mapping to `inputMap`
|
||||
inputMap[detectWheel()] = 'mouse';
|
||||
|
||||
addListeners();
|
||||
};
|
||||
|
||||
/*
|
||||
* events
|
||||
*/
|
||||
|
||||
var addListeners = function addListeners() {
|
||||
// `pointermove`, `MSPointerMove`, `mousemove` and mouse wheel event binding
|
||||
// can only demonstrate potential, but not actual, interaction
|
||||
// and are treated separately
|
||||
var options = supportsPassive ? { passive: true } : false;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', setPersist);
|
||||
|
||||
// pointer events (mouse, pen, touch)
|
||||
if (window.PointerEvent) {
|
||||
window.addEventListener('pointerdown', setInput);
|
||||
window.addEventListener('pointermove', setIntent);
|
||||
} else if (window.MSPointerEvent) {
|
||||
window.addEventListener('MSPointerDown', setInput);
|
||||
window.addEventListener('MSPointerMove', setIntent);
|
||||
} else {
|
||||
// mouse events
|
||||
window.addEventListener('mousedown', setInput);
|
||||
window.addEventListener('mousemove', setIntent);
|
||||
|
||||
// touch events
|
||||
if ('ontouchstart' in window) {
|
||||
window.addEventListener('touchstart', setInput, options);
|
||||
window.addEventListener('touchend', setInput);
|
||||
}
|
||||
}
|
||||
|
||||
// mouse wheel
|
||||
window.addEventListener(detectWheel(), setIntent, options);
|
||||
|
||||
// keyboard events
|
||||
window.addEventListener('keydown', setInput);
|
||||
window.addEventListener('keyup', setInput);
|
||||
|
||||
// focus events
|
||||
window.addEventListener('focusin', setElement);
|
||||
window.addEventListener('focusout', clearElement);
|
||||
};
|
||||
|
||||
// checks if input persistence should happen and
|
||||
// get saved state from session storage if true (defaults to `false`)
|
||||
var setPersist = function setPersist() {
|
||||
shouldPersist = !(docElem.getAttribute('data-whatpersist') || document.body.getAttribute('data-whatpersist') === 'false');
|
||||
|
||||
if (shouldPersist) {
|
||||
// check for session variables and use if available
|
||||
try {
|
||||
if (window.sessionStorage.getItem('what-input')) {
|
||||
currentInput = window.sessionStorage.getItem('what-input');
|
||||
}
|
||||
|
||||
if (window.sessionStorage.getItem('what-intent')) {
|
||||
currentIntent = window.sessionStorage.getItem('what-intent');
|
||||
}
|
||||
} catch (e) {
|
||||
// fail silently
|
||||
}
|
||||
}
|
||||
|
||||
// always run these so at least `initial` state is set
|
||||
doUpdate('input');
|
||||
doUpdate('intent');
|
||||
};
|
||||
|
||||
// checks conditions before updating new input
|
||||
var setInput = function setInput(event) {
|
||||
var eventKey = event.which;
|
||||
var value = inputMap[event.type];
|
||||
|
||||
if (value === 'pointer') {
|
||||
value = pointerType(event);
|
||||
}
|
||||
|
||||
var ignoreMatch = !specificMap.length && ignoreMap.indexOf(eventKey) === -1;
|
||||
|
||||
var specificMatch = specificMap.length && specificMap.indexOf(eventKey) !== -1;
|
||||
|
||||
var shouldUpdate = value === 'keyboard' && eventKey && (ignoreMatch || specificMatch) || value === 'mouse' || value === 'touch';
|
||||
|
||||
// prevent touch detection from being overridden by event execution order
|
||||
if (validateTouch(value)) {
|
||||
shouldUpdate = false;
|
||||
}
|
||||
|
||||
if (shouldUpdate && currentInput !== value) {
|
||||
currentInput = value;
|
||||
|
||||
persistInput('input', currentInput);
|
||||
doUpdate('input');
|
||||
}
|
||||
|
||||
if (shouldUpdate && currentIntent !== value) {
|
||||
// preserve intent for keyboard interaction with form fields
|
||||
var activeElem = document.activeElement;
|
||||
var notFormInput = activeElem && activeElem.nodeName && (formInputs.indexOf(activeElem.nodeName.toLowerCase()) === -1 || activeElem.nodeName.toLowerCase() === 'button' && !checkClosest(activeElem, 'form'));
|
||||
|
||||
if (notFormInput) {
|
||||
currentIntent = value;
|
||||
|
||||
persistInput('intent', currentIntent);
|
||||
doUpdate('intent');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// updates the doc and `inputTypes` array with new input
|
||||
var doUpdate = function doUpdate(which) {
|
||||
docElem.setAttribute('data-what' + which, which === 'input' ? currentInput : currentIntent);
|
||||
|
||||
fireFunctions(which);
|
||||
};
|
||||
|
||||
// updates input intent for `mousemove` and `pointermove`
|
||||
var setIntent = function setIntent(event) {
|
||||
var value = inputMap[event.type];
|
||||
|
||||
if (value === 'pointer') {
|
||||
value = pointerType(event);
|
||||
}
|
||||
|
||||
// test to see if `mousemove` happened relative to the screen to detect scrolling versus mousemove
|
||||
detectScrolling(event);
|
||||
|
||||
// only execute if scrolling isn't happening
|
||||
if ((!isScrolling && !validateTouch(value) || isScrolling && event.type === 'wheel' || event.type === 'mousewheel' || event.type === 'DOMMouseScroll') && currentIntent !== value) {
|
||||
currentIntent = value;
|
||||
|
||||
persistInput('intent', currentIntent);
|
||||
doUpdate('intent');
|
||||
}
|
||||
};
|
||||
|
||||
var setElement = function setElement(event) {
|
||||
if (!event.target.nodeName) {
|
||||
// If nodeName is undefined, clear the element
|
||||
// This can happen if click inside an <svg> element.
|
||||
clearElement();
|
||||
return;
|
||||
}
|
||||
|
||||
currentElement = event.target.nodeName.toLowerCase();
|
||||
docElem.setAttribute('data-whatelement', currentElement);
|
||||
|
||||
if (event.target.classList && event.target.classList.length) {
|
||||
docElem.setAttribute('data-whatclasses', event.target.classList.toString().replace(' ', ','));
|
||||
}
|
||||
};
|
||||
|
||||
var clearElement = function clearElement() {
|
||||
currentElement = null;
|
||||
|
||||
docElem.removeAttribute('data-whatelement');
|
||||
docElem.removeAttribute('data-whatclasses');
|
||||
};
|
||||
|
||||
var persistInput = function persistInput(which, value) {
|
||||
if (shouldPersist) {
|
||||
try {
|
||||
window.sessionStorage.setItem('what-' + which, value);
|
||||
} catch (e) {
|
||||
// fail silently
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* utilities
|
||||
*/
|
||||
|
||||
var pointerType = function pointerType(event) {
|
||||
if (typeof event.pointerType === 'number') {
|
||||
return pointerMap[event.pointerType];
|
||||
} else {
|
||||
// treat pen like touch
|
||||
return event.pointerType === 'pen' ? 'touch' : event.pointerType;
|
||||
}
|
||||
};
|
||||
|
||||
// prevent touch detection from being overridden by event execution order
|
||||
var validateTouch = function validateTouch(value) {
|
||||
var timestamp = Date.now();
|
||||
|
||||
var touchIsValid = value === 'mouse' && currentInput === 'touch' && timestamp - currentTimestamp < 200;
|
||||
|
||||
currentTimestamp = timestamp;
|
||||
|
||||
return touchIsValid;
|
||||
};
|
||||
|
||||
// detect version of mouse wheel event to use
|
||||
// via https://developer.mozilla.org/en-US/docs/Web/API/Element/wheel_event
|
||||
var detectWheel = function detectWheel() {
|
||||
var wheelType = null;
|
||||
|
||||
// Modern browsers support "wheel"
|
||||
if ('onwheel' in document.createElement('div')) {
|
||||
wheelType = 'wheel';
|
||||
} else {
|
||||
// Webkit and IE support at least "mousewheel"
|
||||
// or assume that remaining browsers are older Firefox
|
||||
wheelType = document.onmousewheel !== undefined ? 'mousewheel' : 'DOMMouseScroll';
|
||||
}
|
||||
|
||||
return wheelType;
|
||||
};
|
||||
|
||||
// runs callback functions
|
||||
var fireFunctions = function fireFunctions(type) {
|
||||
for (var i = 0, len = functionList.length; i < len; i++) {
|
||||
if (functionList[i].type === type) {
|
||||
functionList[i].fn.call(undefined, type === 'input' ? currentInput : currentIntent);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// finds matching element in an object
|
||||
var objPos = function objPos(match) {
|
||||
for (var i = 0, len = functionList.length; i < len; i++) {
|
||||
if (functionList[i].fn === match) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var detectScrolling = function detectScrolling(event) {
|
||||
if (mousePos.x !== event.screenX || mousePos.y !== event.screenY) {
|
||||
isScrolling = false;
|
||||
|
||||
mousePos.x = event.screenX;
|
||||
mousePos.y = event.screenY;
|
||||
} else {
|
||||
isScrolling = true;
|
||||
}
|
||||
};
|
||||
|
||||
// manual version of `closest()`
|
||||
var checkClosest = function checkClosest(elem, tag) {
|
||||
var ElementPrototype = window.Element.prototype;
|
||||
|
||||
if (!ElementPrototype.matches) {
|
||||
ElementPrototype.matches = ElementPrototype.msMatchesSelector || ElementPrototype.webkitMatchesSelector;
|
||||
}
|
||||
|
||||
if (!ElementPrototype.closest) {
|
||||
do {
|
||||
if (elem.matches(tag)) {
|
||||
return elem;
|
||||
}
|
||||
|
||||
elem = elem.parentElement || elem.parentNode;
|
||||
} while (elem !== null && elem.nodeType === 1);
|
||||
|
||||
return null;
|
||||
} else {
|
||||
return elem.closest(tag);
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* init
|
||||
*/
|
||||
|
||||
// don't start script unless browser cuts the mustard
|
||||
// (also passes if polyfills are used)
|
||||
if ('addEventListener' in window && Array.prototype.indexOf) {
|
||||
setUp();
|
||||
}
|
||||
|
||||
/*
|
||||
* api
|
||||
*/
|
||||
|
||||
return {
|
||||
// returns string: the current input type
|
||||
// opt: 'intent'|'input'
|
||||
// 'input' (default): returns the same value as the `data-whatinput` attribute
|
||||
// 'intent': includes `data-whatintent` value if it's different than `data-whatinput`
|
||||
ask: function ask(opt) {
|
||||
return opt === 'intent' ? currentIntent : currentInput;
|
||||
},
|
||||
|
||||
// returns string: the currently focused element or null
|
||||
element: function element() {
|
||||
return currentElement;
|
||||
},
|
||||
|
||||
// overwrites ignored keys with provided array
|
||||
ignoreKeys: function ignoreKeys(arr) {
|
||||
ignoreMap = arr;
|
||||
},
|
||||
|
||||
// overwrites specific char keys to update on
|
||||
specificKeys: function specificKeys(arr) {
|
||||
specificMap = arr;
|
||||
},
|
||||
|
||||
// attach functions to input and intent "events"
|
||||
// funct: function to fire on change
|
||||
// eventType: 'input'|'intent'
|
||||
registerOnChange: function registerOnChange(fn, eventType) {
|
||||
functionList.push({
|
||||
fn: fn,
|
||||
type: eventType || 'input'
|
||||
});
|
||||
},
|
||||
|
||||
unRegisterOnChange: function unRegisterOnChange(fn) {
|
||||
var position = objPos(fn);
|
||||
|
||||
if (position || position === 0) {
|
||||
functionList.splice(position, 1);
|
||||
}
|
||||
},
|
||||
|
||||
clearStorage: function clearStorage() {
|
||||
window.sessionStorage.clear();
|
||||
}
|
||||
};
|
||||
}();
|
||||
|
||||
/***/ })
|
||||
/******/ ])
|
||||
});
|
||||
;
|
53
web/templates/_layout.html
Normal file
53
web/templates/_layout.html
Normal file
@ -0,0 +1,53 @@
|
||||
|
||||
<!doctype html>
|
||||
<html class="no-js" lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>linkwallet</title>
|
||||
<link rel="stylesheet" href="/assets/css/foundation.css">
|
||||
<link rel="stylesheet" href="/assets/css/app.css">
|
||||
<script src="/assets/js/htmx.min.js" defer></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="top-bar">
|
||||
<div class="top-bar-left">
|
||||
<ul class="dropdown menu" data-dropdown-menu>
|
||||
<li class="menu-text">linkwallet</li>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li>
|
||||
<a href="#">Admin</a>
|
||||
<ul class="menu vertical">
|
||||
<li><a href="/manage">Manage links</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="top-bar-right">
|
||||
<ul class="menu">
|
||||
<li><a href="https://github.com">gh</a></li>
|
||||
<!-- <li><input type="search" placeholder="Search"></li>
|
||||
<li><button type="button" class="button">Search</button></li> -->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-container">
|
||||
{{ if eq .page "root" }}
|
||||
{{ template "search.html" . }}
|
||||
{{ else if eq .page "manage" }}
|
||||
{{ template "manage.html" . }}
|
||||
{{ end }}
|
||||
{{/* template "foundation_sample.html" . */}}
|
||||
</div>
|
||||
|
||||
|
||||
<script src="/assets/js/vendor/jquery.js"></script>
|
||||
<script src="/assets/js/vendor/what-input.js"></script>
|
||||
<script src="/assets/js/vendor/foundation.js"></script>
|
||||
<script src="/assets/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
27
web/templates/add_url_form.html
Normal file
27
web/templates/add_url_form.html
Normal file
@ -0,0 +1,27 @@
|
||||
<div class="large-8 medium-8 cell" id="add-url-form" >
|
||||
<h5>Add a new URL</h5>
|
||||
<span>[<a hx-get="/bulk_add" hx-target="#add-url-form" href="#">bulk</a>]</span>
|
||||
<form onsubmit="return false">
|
||||
<div class="grid-x grid-padding-x">
|
||||
<div class="large-6 cell">
|
||||
<label>Paste a URL</label>
|
||||
<input type="text" name="url"
|
||||
hx-post="/add"
|
||||
hx-target="#add-url-form" hx-trigger=""
|
||||
/>
|
||||
</div>
|
||||
<div class="large-6 cell">
|
||||
{{ template "tags_widget.html" . }}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{{ if .error }}
|
||||
<p class="error">{{ .error }}</p>
|
||||
{{ else }}
|
||||
|
||||
{{ if .bm }}
|
||||
Bookmark added - ID: {{ .bm.ID }} URL: {{ .bm.URL }}
|
||||
{{ end }}
|
||||
|
||||
{{ end }}
|
||||
</div>
|
36
web/templates/add_url_form_bulk.html
Normal file
36
web/templates/add_url_form_bulk.html
Normal file
@ -0,0 +1,36 @@
|
||||
<div class="large-8 medium-8 cell" id="add-url-form" >
|
||||
<h5>Add URLs in bulk</h5>
|
||||
<span>[<a hx-get="/single_add" hx-target="#add-url-form" href="#">single</a>]</span>
|
||||
<form onsubmit="return false">
|
||||
<div class="grid-x grid-padding-x">
|
||||
<div class="large-12 cell">
|
||||
<label>Paste URL's, one per line</label>
|
||||
<textarea type="text" name="urls" rows="10"
|
||||
></textarea>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="button"
|
||||
hx-post="/add_bulk"
|
||||
hx-indicator="#htmx-indicator-bulk"
|
||||
hx-target="#add-url-form">
|
||||
add
|
||||
</button>
|
||||
<span id="htmx-indicator-bulk" class="htmx-indicator">
|
||||
<img src="/assets/image/beating.gif" /> adding...
|
||||
</span>
|
||||
|
||||
</form>
|
||||
{{ if .errors }}
|
||||
<ul>
|
||||
{{ range .errors }}
|
||||
<li class="error">{{ . }}</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ else }}
|
||||
{{ if .added }}
|
||||
Added {{ .added }} urls
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
148
web/templates/foundation_sample.html
Normal file
148
web/templates/foundation_sample.html
Normal file
@ -0,0 +1,148 @@
|
||||
<div class="grid-x grid-padding-x">
|
||||
<div class="large-12 cell">
|
||||
<h1>Welcome to Foundation</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-x grid-padding-x">
|
||||
<div class="large-12 cell">
|
||||
<div class="callout">
|
||||
<h3>We’re stoked you want to try Foundation! </h3>
|
||||
<p>To get going, this file (index.html) includes some basic styles you can modify, play around with, or totally destroy to get going.</p>
|
||||
<p>Once you've exhausted the fun in this document, you should check out:</p>
|
||||
<div class="grid-x grid-padding-x">
|
||||
<div class="large-4 medium-4 cell">
|
||||
<p><a href="https://get.foundation/sites/docs/">Foundation Documentation</a><br />Everything you need to know about using the framework.</p>
|
||||
</div>
|
||||
<div class="large-4 medium-4 cell">
|
||||
<p><a href="https://github.com/foundation/foundation-sites/discussions">Foundation Forum</a><br />Join the Foundation community to ask a question or show off your knowlege.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-x grid-padding-x">
|
||||
<div class="large-4 medium-4 medium-push-2 cell">
|
||||
<p><a href="https://github.com/foundation/foundation-sites">Foundation on Github</a><br />Latest code, issue reports, feature requests and more.</p>
|
||||
</div>
|
||||
<div class="large-4 medium-4 medium-pull-2 cell">
|
||||
<p><a href="https://twitter.com/FoundationCSS">@FoundationCSS</a><br />Ping us on Twitter if you have questions. When you build something with this we'd love to see it.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-x grid-padding-x">
|
||||
<div class="large-8 medium-8 cell">
|
||||
<h5>Here’s your basic grid:</h5>
|
||||
<!-- Grid Example -->
|
||||
|
||||
<div class="grid-x grid-padding-x">
|
||||
<div class="large-12 cell">
|
||||
<div class="primary callout">
|
||||
<p><strong>This is a twelve cell section in a grid-x.</strong> Each of these includes a div.callout element so you can see where the cell are - it's not required at all for the grid.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-x grid-padding-x">
|
||||
<div class="large-6 medium-6 cell">
|
||||
<div class="primary callout">
|
||||
<p>Six cell</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="large-6 medium-6 cell">
|
||||
<div class="primary callout">
|
||||
<p>Six cell</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-x grid-padding-x">
|
||||
<div class="large-4 medium-4 small-4 cell">
|
||||
<div class="primary callout">
|
||||
<p>Four cell</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="large-4 medium-4 small-4 cell">
|
||||
<div class="primary callout">
|
||||
<p>Four cell</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="large-4 medium-4 small-4 cell">
|
||||
<div class="primary callout">
|
||||
<p>Four cell</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<h5>We bet you’ll need a form somewhere:</h5>
|
||||
<form>
|
||||
<div class="grid-x grid-padding-x">
|
||||
<div class="large-12 cell">
|
||||
<label>Input Label</label>
|
||||
<input type="text" placeholder="large-12.cell" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-x grid-padding-x">
|
||||
<div class="large-4 medium-4 cell">
|
||||
<label>Input Label</label>
|
||||
<input type="text" placeholder="large-4.cell" />
|
||||
</div>
|
||||
<div class="large-4 medium-4 cell">
|
||||
<label>Input Label</label>
|
||||
<input type="text" placeholder="large-4.cell" />
|
||||
</div>
|
||||
<div class="large-4 medium-4 cell">
|
||||
<div class="grid-x">
|
||||
<label>Input Label</label>
|
||||
<div class="input-group">
|
||||
<input type="text" placeholder="small-9.cell" class="input-group-field" />
|
||||
<span class="input-group-label">.com</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-x grid-padding-x">
|
||||
<div class="large-12 cell">
|
||||
<label>Select Box</label>
|
||||
<select>
|
||||
<option value="husker">Husker</option>
|
||||
<option value="starbuck">Starbuck</option>
|
||||
<option value="hotdog">Hot Dog</option>
|
||||
<option value="apollo">Apollo</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-x grid-padding-x">
|
||||
<div class="large-6 medium-6 cell">
|
||||
<label>Choose Your Favorite</label>
|
||||
<input type="radio" name="pokemon" value="Red" id="pokemonRed"><label for="pokemonRed">Radio 1</label>
|
||||
<input type="radio" name="pokemon" value="Blue" id="pokemonBlue"><label for="pokemonBlue">Radio 2</label>
|
||||
</div>
|
||||
<div class="large-6 medium-6 cell">
|
||||
<label>Check these out</label>
|
||||
<input id="checkbox1" type="checkbox"><label for="checkbox1">Checkbox 1</label>
|
||||
<input id="checkbox2" type="checkbox"><label for="checkbox2">Checkbox 2</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-x grid-padding-x">
|
||||
<div class="large-12 cell">
|
||||
<label>Textarea Label</label>
|
||||
<textarea placeholder="small-12.cell"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="large-4 medium-4 cell">
|
||||
<h5>Try one of these buttons:</h5>
|
||||
<p><a href="#" class="button">Simple Button</a><br/>
|
||||
<a href="#" class="success button">Success Btn</a><br/>
|
||||
<a href="#" class="alert button">Alert Btn</a><br/>
|
||||
<a href="#" class="secondary button">Secondary Btn</a></p>
|
||||
<div class="callout">
|
||||
<h5>So many components, girl!</h5>
|
||||
<p>A whole kitchen sink of goodies comes with Foundation. Check out the docs to see them all, along with details on making them your own.</p>
|
||||
<a href="https://get.foundation/sites/docs/" class="small button">Go to Foundation Docs</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
29
web/templates/manage.html
Normal file
29
web/templates/manage.html
Normal file
@ -0,0 +1,29 @@
|
||||
<div class="grid-x grid-padding-x">
|
||||
<div class="large-12 cell">
|
||||
|
||||
<h5>Manage:
|
||||
|
||||
</h5>
|
||||
|
||||
<table>
|
||||
<tr><th>id</th><th>url</th><th>created</th><th>scraped</th></tr>
|
||||
{{ range .bookmarks }}
|
||||
<tr>
|
||||
<th>{{ .ID }}</th>
|
||||
<td>
|
||||
<a href="{{ .URL }}">{{ .Info.Title }}</a>
|
||||
<br>
|
||||
<a href="{{ .URL }}">{{ niceURL .URL }}</a>
|
||||
</td>
|
||||
<td>{{ (nicetime .TimestampCreated).HumanDuration }} ago</td>
|
||||
<td>{{ (nicetime .TimestampLastScraped).HumanDuration }} ago</td>
|
||||
|
||||
<td>
|
||||
<button class="button" hx-post="/scrape/{{ .ID }}">scrape</button>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
41
web/templates/search.html
Normal file
41
web/templates/search.html
Normal file
@ -0,0 +1,41 @@
|
||||
<div class="grid-x grid-padding-x">
|
||||
<div class="large-8 medium-8 cell">
|
||||
|
||||
<h5>Search:
|
||||
<span id="htmx-indicator-search" class="htmx-indicator">
|
||||
<img src="/assets/image/beating.gif" /> Searching...
|
||||
</span>
|
||||
</h5>
|
||||
<form onsubmit="return false">
|
||||
<div class="grid-x grid-padding-x">
|
||||
<div class="large-12 cell">
|
||||
<label>Free text</label>
|
||||
<input type="text" name="query" placeholder="" hx-post="/search"
|
||||
hx-trigger="keyup changed delay:50ms, search" hx-target="#search-results"
|
||||
hx-indicator="#htmx-indicator-search" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div id="search-results">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ template "add_url_form.html" . }}
|
||||
|
||||
|
||||
<!-- <div class="large-4 medium-4 cell">
|
||||
<h5>Try one of these buttons:</h5>
|
||||
<p><a href="#" class="button">Simple Button</a><br />
|
||||
<a href="#" class="success button">Success Btn</a><br />
|
||||
<a href="#" class="alert button">Alert Btn</a><br />
|
||||
<a href="#" class="secondary button">Secondary Btn</a>
|
||||
</p>
|
||||
<div class="callout">
|
||||
<h5>So many components, girl!</h5>
|
||||
<p>A whole kitchen sink of goodies comes with Foundation. Check out the docs to see them all, along with
|
||||
details on making them your own.</p>
|
||||
<a href="https://get.foundation/sites/docs/" class="small button">Go to Foundation Docs</a>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
</div>
|
5
web/templates/search_results.html
Normal file
5
web/templates/search_results.html
Normal file
@ -0,0 +1,5 @@
|
||||
<ul>
|
||||
{{ range .results }}
|
||||
<li><a href="{{ .URL }}">{{ .Info.Title }}</a> - {{ .URL }}</li>
|
||||
{{ end }}
|
||||
</ul>
|
28
web/templates/tags_widget.html
Normal file
28
web/templates/tags_widget.html
Normal file
@ -0,0 +1,28 @@
|
||||
<div id="label-widget">
|
||||
<div class="grid-x grid-padding-x">
|
||||
<div class="small-3 medium-2 large-1 cell">
|
||||
</div>
|
||||
<div class="small-9 medium-10 large-5 cell"
|
||||
hx-post="/tags"
|
||||
hx-target="#label-widget"
|
||||
hx-trigger="change">
|
||||
<label for="tag-entry"
|
||||
class="Xtext-right Xmiddle">Tags</label>
|
||||
|
||||
<input id="tag-entry" type="text" name="tag" placeholder="enter tags" />
|
||||
</div>
|
||||
<div class="small-12 large-6 cell">
|
||||
{{ range .tags }}
|
||||
<a href="#"
|
||||
title="remove {{ . }}"
|
||||
hx-trigger="click"
|
||||
hx-target="#label-widget"
|
||||
hx-post="/tags?remove={{ . }}">
|
||||
{{ . }}
|
||||
</a>
|
||||
{{ end }}
|
||||
<input type="hidden" name="tags_hidden" value="{{ .tags_hidden }}">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
248
web/web.go
Normal file
248
web/web.go
Normal file
@ -0,0 +1,248 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tardisx/linkwallet/db"
|
||||
"github.com/tardisx/linkwallet/entity"
|
||||
|
||||
"github.com/hako/durafmt"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
//go:embed static/*
|
||||
var staticFiles embed.FS
|
||||
|
||||
//go:embed templates/*
|
||||
var templateFiles embed.FS
|
||||
|
||||
// Server represents a SCUD web server.
|
||||
// The SCUD web service can serve 2 different kinda of responses. The first is basic static
|
||||
// vendor-provided files (called "assetFiles" here). An arbitrary number of them can be placed
|
||||
// in assets/ and served up via a path prefix of /assets. They do not need individual routes
|
||||
// to be specified.
|
||||
// The second is htmx responses fragments. We never automatically serve templates (ie no mapping
|
||||
// from template name to a URL route), there will always be a specific route or routes which
|
||||
// use one or more templates to return a response.
|
||||
type Server struct {
|
||||
engine *gin.Engine
|
||||
templ *template.Template
|
||||
bmm *db.BookmarkManager
|
||||
}
|
||||
|
||||
// Create creates a new web server instance and sets up routing.
|
||||
func Create(bmm *db.BookmarkManager) *Server {
|
||||
|
||||
// setup routes for the static assets (vendor includes)
|
||||
staticFS, err := fs.Sub(staticFiles, "static")
|
||||
if err != nil {
|
||||
log.Fatalf("problem with assetFS: %s", err)
|
||||
}
|
||||
|
||||
// templ := template.Must(template.New("").Funcs(template.FuncMap{"dict": dictHelper}).ParseFS(templateFiles, "templates/*.html"))
|
||||
templ := template.Must(template.New("").Funcs(template.FuncMap{"nicetime": niceTime, "niceURL": niceURL}).ParseFS(templateFiles, "templates/*.html"))
|
||||
|
||||
r := gin.Default()
|
||||
|
||||
server := &Server{
|
||||
engine: r,
|
||||
templ: templ,
|
||||
bmm: bmm,
|
||||
}
|
||||
|
||||
r.Use(headersByURI())
|
||||
|
||||
r.SetHTMLTemplate(templ)
|
||||
r.StaticFS("/assets", http.FS(staticFS))
|
||||
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
meta := gin.H{"page": "root"}
|
||||
c.HTML(http.StatusOK,
|
||||
"_layout.html", meta,
|
||||
)
|
||||
})
|
||||
|
||||
r.GET("/manage", func(c *gin.Context) {
|
||||
allBookmarks, _ := bmm.ListBookmarks()
|
||||
meta := gin.H{"page": "manage", "bookmarks": allBookmarks}
|
||||
c.HTML(http.StatusOK,
|
||||
"_layout.html", meta,
|
||||
)
|
||||
})
|
||||
|
||||
r.POST("/search", func(c *gin.Context) {
|
||||
query := c.PostForm("query")
|
||||
|
||||
sr, err := bmm.Search(query)
|
||||
data := gin.H{
|
||||
"results": sr,
|
||||
"error": err,
|
||||
}
|
||||
|
||||
c.HTML(http.StatusOK,
|
||||
"search_results.html", data,
|
||||
)
|
||||
})
|
||||
|
||||
r.POST("/add", func(c *gin.Context) {
|
||||
url := c.PostForm("url")
|
||||
tags := strings.Split(c.PostForm("tags_hidden"), "|")
|
||||
bm := entity.Bookmark{
|
||||
ID: 0,
|
||||
URL: url,
|
||||
Tags: tags,
|
||||
}
|
||||
err := bmm.AddBookmark(&bm)
|
||||
|
||||
log.Printf("well done %d", bm.ID)
|
||||
data := gin.H{
|
||||
"bm": bm,
|
||||
"error": err,
|
||||
}
|
||||
c.HTML(http.StatusOK, "add_url_form.html", data)
|
||||
})
|
||||
r.POST("/add_bulk", func(c *gin.Context) {
|
||||
urls := c.PostForm("urls")
|
||||
|
||||
urlsSplit := strings.Split(urls, "\n")
|
||||
urlsTrimmed := make([]string, 0, 0)
|
||||
for _, url := range urlsSplit {
|
||||
urlsTrimmed = append(urlsTrimmed, strings.TrimSpace(url))
|
||||
}
|
||||
totalErrors := make([]string, 0, 0)
|
||||
added := 0
|
||||
for _, url := range urlsTrimmed {
|
||||
if url != "" {
|
||||
bm := entity.Bookmark{
|
||||
ID: 0,
|
||||
URL: url,
|
||||
}
|
||||
|
||||
err := bmm.AddBookmark(&bm)
|
||||
if err != nil {
|
||||
totalErrors = append(totalErrors, fmt.Sprintf("url: %s (%s)", url, err.Error()))
|
||||
} else {
|
||||
added++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("well done %v, %d", totalErrors, added)
|
||||
data := gin.H{
|
||||
"added": added,
|
||||
"errors": totalErrors,
|
||||
}
|
||||
c.HTML(http.StatusOK, "add_url_form_bulk.html", data)
|
||||
})
|
||||
|
||||
r.GET("/bulk_add", func(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "add_url_form_bulk.html", nil)
|
||||
})
|
||||
|
||||
r.POST("/tags", func(c *gin.Context) {
|
||||
|
||||
log.Printf("POST: tag '%s' tags_hidden '%s'", c.PostForm("tag"), c.PostForm("tags_hidden"))
|
||||
|
||||
newTag := c.PostForm("tag")
|
||||
oldTags := strings.Split(c.PostForm("tags_hidden"), "|")
|
||||
|
||||
remove := c.Query("remove")
|
||||
if remove != "" {
|
||||
log.Printf("removing %s", remove)
|
||||
trimmedTags := []string{}
|
||||
for _, k := range oldTags {
|
||||
if k != remove {
|
||||
trimmedTags = append(trimmedTags, k)
|
||||
}
|
||||
}
|
||||
oldTags = trimmedTags
|
||||
}
|
||||
|
||||
tags := append(oldTags, newTag)
|
||||
tags = cleanupTags(tags)
|
||||
tagsHidden := strings.Join(tags, "|")
|
||||
|
||||
data := gin.H{"tags": tags, "tags_hidden": tagsHidden}
|
||||
c.HTML(http.StatusOK, "tags_widget.html", data)
|
||||
})
|
||||
|
||||
r.GET("/single_add", func(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "add_url_form.html", nil)
|
||||
})
|
||||
|
||||
// XXX this should properly replace the button
|
||||
r.POST("/scrape/:id", func(c *gin.Context) {
|
||||
id := c.Params.ByName("id")
|
||||
idNum, _ := strconv.ParseInt(id, 10, 32)
|
||||
bm := bmm.LoadBookmarkByID(uint64(idNum))
|
||||
bmm.QueueScrape(&bm)
|
||||
c.String(http.StatusOK, "queued")
|
||||
})
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
// headersByURI sets the headers for some special cases, set a custom long cache time for
|
||||
// static resources.
|
||||
func headersByURI() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if strings.HasPrefix(c.Request.URL.String(), "/assets/") {
|
||||
c.Header("Cache-Control", "max-age=86400")
|
||||
c.Header("Expires", time.Now().Add(time.Hour*24*1).Format("Mon 2 Jan 2006 15:04:05 MST"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the web server, blocking forever.
|
||||
func (s *Server) Start() {
|
||||
s.engine.Run()
|
||||
}
|
||||
|
||||
func cleanupTags(tags []string) []string {
|
||||
keys := make(map[string]struct{})
|
||||
for _, k := range tags {
|
||||
if k != "" && k != "|" {
|
||||
keys[k] = struct{}{}
|
||||
}
|
||||
}
|
||||
out := []string{}
|
||||
for k := range keys {
|
||||
out = append(out, k)
|
||||
}
|
||||
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
type timeVariations struct {
|
||||
HumanDuration string
|
||||
}
|
||||
|
||||
func niceTime(t time.Time) timeVariations {
|
||||
|
||||
u := "y:y,w:w,d:d,h:h,m:m,s:s,ms:ms,us:us"
|
||||
units, err := durafmt.DefaultUnitsCoder.Decode(u)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ago := durafmt.Parse(time.Since(t)).LimitFirstN(1).Format(units)
|
||||
|
||||
return timeVariations{HumanDuration: ago}
|
||||
}
|
||||
|
||||
func niceURL(url string) string {
|
||||
if len(url) > 50 {
|
||||
return url[0:50] + " ..."
|
||||
}
|
||||
return url
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user