diff --git a/api/api.go b/api/api.go index 3e7f1d0..f2d2d2c 100644 --- a/api/api.go +++ b/api/api.go @@ -8,7 +8,7 @@ import ( func main() { router := gin.Default() - router.POST("/add", handlers.SaveToDB) + router.POST("/add", handlers.AddBookmark) router.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{ "message": "pong", diff --git a/api/db/bookmark.model.go b/api/db/bookmark.model.go new file mode 100644 index 0000000..137415f --- /dev/null +++ b/api/db/bookmark.model.go @@ -0,0 +1,14 @@ +package db + +type Meta struct { + Title string `json:"title"` + Description string `json:"description"` + Favicon []byte `json:"favicon"` +} + +type Bookmark struct { + Meta Meta `json:"meta"` + URL string `json:"url" binding:"required"` + Created int `json:"created"` + LastUpdated int `json:"last_updated"` +} diff --git a/api/db/db.go b/api/db/db.go index 0ff733a..80fcf11 100644 --- a/api/db/db.go +++ b/api/db/db.go @@ -16,12 +16,17 @@ func InitializeDB() (db *sql.DB) { utils.Must(err) _, err = db.Exec(` - CREATE TABLE IF NOT EXISTS links ( + CREATE TABLE IF NOT EXISTS bookmarks ( id INTEGER NOT NULL PRIMARY KEY, - title TEXT, - url TEXT, + meta INTEGER, + url TEXT, created INTEGER, last_updated INTEGER + ); + CREATE TABLE IF NOT EXISTS meta ( + title TEXT, + description TEXT, + favicon BLOB ) `) utils.Must(err) diff --git a/api/handlers/get_bookmarks.go b/api/handlers/get_bookmarks.go new file mode 100644 index 0000000..5ac8282 --- /dev/null +++ b/api/handlers/get_bookmarks.go @@ -0,0 +1 @@ +package handlers diff --git a/api/handlers/save_bookmark.go b/api/handlers/save_bookmark.go new file mode 100644 index 0000000..babcacd --- /dev/null +++ b/api/handlers/save_bookmark.go @@ -0,0 +1,102 @@ +package handlers + +import ( + DB "bingo/api/db" + "bingo/api/utils" + "database/sql" + "errors" + "io" + "log" + "net/http" + "regexp" + "strings" + "time" + + "github.com/gin-gonic/gin" +) + +var db *sql.DB = DB.GetDB() + +func getMetadataFromURL(url string, availableMetadata DB.Meta) (meta *DB.Meta, err error) { + + url = strings.TrimSpace(url) + if !(strings.HasPrefix(url, "https://") || strings.HasPrefix(url, "https://")) { + url = "https://" + url + } + // TODO check url before passing into http + res, err := http.Get(url) + if err != nil { + return nil, errors.New("Invalid URL.") + } + + defer res.Body.Close() + + hm := new(DB.Meta) + data, _ := io.ReadAll(res.Body) + headRegex := regexp.MustCompile("((.|\n|\r\n)+)") + + head := string(headRegex.Find(data)) + head = strings.ReplaceAll(head, "\n", "") + + if availableMetadata.Title == "" { + titleRegex := regexp.MustCompile(`(.+)<\/title>`) + metaTitleRegex := regexp.MustCompile(``) + titleMatches := titleRegex.FindStringSubmatch(head) + if len(titleMatches) == 0 { + titleMatches = metaTitleRegex.FindStringSubmatch(head) + } + + if len(titleMatches) == 0 { + hm.Title = "" + } else { + hm.Title = titleMatches[1] + } + } else { + hm.Title = availableMetadata.Title + } + + if availableMetadata.Description == "" { + + descriptionRegex := regexp.MustCompile(``) + descMatches := descriptionRegex.FindStringSubmatch(head) + if len(descMatches) == 0 { + hm.Description = "" + } else { + hm.Description = descMatches[1] + } + } else { + hm.Description = availableMetadata.Description + } + hm.Favicon = availableMetadata.Favicon + return hm, nil +} + +func AddBookmark(ctx *gin.Context) { + var json DB.Bookmark + if err := ctx.ShouldBindJSON(&json); err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + meta, err := getMetadataFromURL(json.URL, json.Meta) + statement, err := db.Prepare("INSERT INTO meta (title, description, favicon) VALUES (?, ?, ?)") + utils.Must(err) + defer statement.Close() + info, err := statement.Exec(meta.Title, meta.Description, meta.Favicon) + metaID, _ := info.LastInsertId() + + if err != nil { + log.Println(err) + ctx.JSON(http.StatusBadRequest, gin.H{"error": "Invalid URL."}) + return + } + + statement, err = db.Prepare("INSERT INTO bookmarks (url, meta, created, last_updated) VALUES (?, ?, ?, ?)") + utils.Must(err) + defer statement.Close() + + now := time.Now().Unix() + _, err = statement.Exec(json.URL, metaID, now, now) + utils.Must(err) + + ctx.String(http.StatusOK, "Saved URL.") +} diff --git a/api/handlers/save_link.go b/api/handlers/save_link.go deleted file mode 100644 index 66b7ac5..0000000 --- a/api/handlers/save_link.go +++ /dev/null @@ -1,58 +0,0 @@ -package handlers - -import ( - DB "bingo/api/db" - "bingo/api/utils" - "database/sql" - "errors" - "io" - "log" - "net/http" - "regexp" - "strings" - "time" - - "github.com/gin-gonic/gin" -) - -var db *sql.DB = DB.GetDB() - -func GetTitleFromURL(url string) (title string, err error) { - url = strings.TrimSpace(url) - if !(strings.HasPrefix(url, "https://") || strings.HasPrefix(url, "https://")) { - url = "https://" + url - } - res, err := http.Get(url) - if err != nil { - return "", errors.New("Invalid URL.") - } - - defer res.Body.Close() - resBytes, err := io.ReadAll(res.Body) - str := string(resBytes) - re := regexp.MustCompile("(.+)") - - title = re.FindStringSubmatch(str)[1] - return title, nil -} - -func SaveToDB(ctx *gin.Context) { - raw, _ := io.ReadAll(ctx.Request.Body) - url := string(raw) - title, err := GetTitleFromURL(url) - - if err != nil { - log.Println(err) - return - } - - stmt, err := db.Prepare("INSERT INTO LINKS (url, title, created, last_updated) VALUES (?, ?, ?, ?)") - utils.Must(err) - defer stmt.Close() - - now := time.Now().Unix() - _, err = stmt.Exec(url, title, now, now) - utils.Must(err) - - ctx.String(http.StatusOK, "Saved URL.") -} diff --git a/readme.md b/readme.md index feccded..81b08fb 100644 --- a/readme.md +++ b/readme.md @@ -5,4 +5,26 @@ - https://gioui.org/ - https://sciter.com/ embedded in Go - A webapp embedded in Go using embed directive. See [this comment](https://www.reddit.com/r/golang/comments/lmvut7/comment/gnz8kct/) -- https://github.com/wailsapp/wails \ No newline at end of file +- https://github.com/wailsapp/wails + + +## Todo + +- [] Update /add route to support title, meta data, etc, auto fill missing data[1] +- [] 1) Auto fill missing data configuration +- [] Add support for tags + - [] tags table? +- [] Add /get route + - [] all + - [] filter by tags + - [] sort by date added/updated +- [] Auto label +- [] Save as PDF/HTML +- [] Import/Export +- [] Web frontend +- [] Sync +- [] Desktop frontend +- [] CLI frontend +- [] Phone frontend +- [] Browser extension + \ No newline at end of file diff --git a/temp/test.go b/temp/test.go new file mode 100644 index 0000000..0702025 --- /dev/null +++ b/temp/test.go @@ -0,0 +1,100 @@ +package main + +import ( + "encoding/json" + "net/http" + "net/url" + "regexp" + "strings" + + "io" +) + +func main() { + + http.HandleFunc(`/read`, func(rw http.ResponseWriter, req *http.Request) { + rw.Header().Set(`Content-Type`, `application/json`) + + err := req.ParseForm() + if err != nil { + rw.WriteHeader(http.StatusBadRequest) + json.NewEncoder(rw).Encode(map[string]string{"error": err.Error()}) + return + } + + link := req.FormValue(`link`) + if link == "" { + rw.WriteHeader(http.StatusBadRequest) + json.NewEncoder(rw).Encode(map[string]string{"error": `empty value of link`}) + return + } + + if _, err := url.Parse(link); err != nil { + rw.WriteHeader(http.StatusBadRequest) + json.NewEncoder(rw).Encode(map[string]string{"error": err.Error()}) + return + } + + resp, err := http.Get(link) + if err != nil { + //proxy status and err + rw.WriteHeader(resp.StatusCode) + json.NewEncoder(rw).Encode(map[string]string{"error": err.Error()}) + return + } + defer resp.Body.Close() + + meta := extract(resp.Body) + rw.WriteHeader(http.StatusOK) + json.NewEncoder(rw).Encode(meta) + return + }) + + // little help %) + println("call like: \n$ curl -XPOST 'http://localhost:4567/read' -d link='https://github.com/golang/go'") + + err := http.ListenAndServe(`:4567`, nil) + if err != nil { + panic(err) + } + +} + +type HTMLMeta struct { + Title string `json:"title"` + Description string `json:"description"` + Favicon string `json:"favicon"` +} + +func extract(resp io.Reader) *HTMLMeta { + hm := new(HTMLMeta) + data, _ := io.ReadAll(resp) + headRegex := regexp.MustCompile("((.|\n|\r\n)+)") + + head := string(headRegex.Find(data)) + head = strings.ReplaceAll(head, "\n", "") + + titleRegex := regexp.MustCompile(`(.+)<\/title>`) + metaTitleRegex := regexp.MustCompile(``) + descriptionRegex := regexp.MustCompile(``) + + descMatches := descriptionRegex.FindStringSubmatch(head) + titleMatches := titleRegex.FindStringSubmatch(head) + if len(titleMatches) == 0 { + titleMatches = metaTitleRegex.FindStringSubmatch(head) + } + + if len(descMatches) == 0 { + hm.Description = "" + } else { + hm.Description = descMatches[1] + } + + if len(titleMatches) == 0 { + hm.Title = "" + } else { + hm.Title = titleMatches[1] + } + + return hm +}