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
+}