2023-09-18 23:57:18 +00:00
|
|
|
package api
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
2023-09-23 18:14:57 +00:00
|
|
|
"mime/multipart"
|
2023-09-18 23:57:18 +00:00
|
|
|
"net/http"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
2023-09-23 18:14:57 +00:00
|
|
|
"strings"
|
2023-09-23 02:12:36 +00:00
|
|
|
"time"
|
2023-09-18 23:57:18 +00:00
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"reichard.io/bbank/database"
|
|
|
|
"reichard.io/bbank/metadata"
|
|
|
|
)
|
|
|
|
|
2023-09-23 18:14:57 +00:00
|
|
|
type requestDocumentEdit struct {
|
|
|
|
Title *string `form:"title"`
|
|
|
|
Author *string `form:"author"`
|
|
|
|
Description *string `form:"description"`
|
|
|
|
Cover *multipart.FileHeader `form:"cover"`
|
|
|
|
}
|
|
|
|
|
2023-09-18 23:57:18 +00:00
|
|
|
func baseResourceRoute(template string, args ...map[string]any) func(c *gin.Context) {
|
|
|
|
variables := gin.H{"RouteName": template}
|
|
|
|
if len(args) > 0 {
|
|
|
|
variables = args[0]
|
|
|
|
}
|
|
|
|
|
|
|
|
return func(c *gin.Context) {
|
|
|
|
rUser, _ := c.Get("AuthorizedUser")
|
|
|
|
variables["User"] = rUser
|
|
|
|
c.HTML(http.StatusOK, template, variables)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-18 23:57:18 +00:00
|
|
|
func (api *API) webManifest(c *gin.Context) {
|
|
|
|
c.Header("Content-Type", "application/manifest+json")
|
|
|
|
c.File("./assets/manifest.json")
|
|
|
|
}
|
|
|
|
|
2023-09-18 23:57:18 +00:00
|
|
|
func (api *API) createAppResourcesRoute(routeName string, args ...map[string]any) func(*gin.Context) {
|
|
|
|
// Merge Optional Template Data
|
2023-09-21 00:35:01 +00:00
|
|
|
var templateVarsBase = gin.H{}
|
2023-09-18 23:57:18 +00:00
|
|
|
if len(args) > 0 {
|
2023-09-21 00:35:01 +00:00
|
|
|
templateVarsBase = args[0]
|
2023-09-18 23:57:18 +00:00
|
|
|
}
|
2023-09-21 00:35:01 +00:00
|
|
|
templateVarsBase["RouteName"] = routeName
|
2023-09-18 23:57:18 +00:00
|
|
|
|
|
|
|
return func(c *gin.Context) {
|
|
|
|
rUser, _ := c.Get("AuthorizedUser")
|
2023-09-21 00:35:01 +00:00
|
|
|
|
|
|
|
// Copy Base & Update
|
|
|
|
templateVars := gin.H{}
|
|
|
|
for k, v := range templateVarsBase {
|
|
|
|
templateVars[k] = v
|
|
|
|
}
|
2023-09-18 23:57:18 +00:00
|
|
|
templateVars["User"] = rUser
|
|
|
|
|
2023-09-21 00:35:01 +00:00
|
|
|
// Potential URL Parameters
|
|
|
|
qParams := bindQueryParams(c)
|
|
|
|
|
2023-09-18 23:57:18 +00:00
|
|
|
if routeName == "documents" {
|
|
|
|
documents, err := api.DB.Queries.GetDocumentsWithStats(api.DB.Ctx, database.GetDocumentsWithStatsParams{
|
|
|
|
UserID: rUser.(string),
|
|
|
|
Offset: (*qParams.Page - 1) * *qParams.Limit,
|
|
|
|
Limit: *qParams.Limit,
|
|
|
|
})
|
|
|
|
if err != nil {
|
2023-09-21 00:35:01 +00:00
|
|
|
log.Error("[createAppResourcesRoute] GetDocumentsWithStats DB Error:", err)
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
templateVars["Data"] = documents
|
2023-09-23 02:12:36 +00:00
|
|
|
} else if routeName == "document" {
|
|
|
|
var rDocID requestDocumentID
|
|
|
|
if err := c.ShouldBindUri(&rDocID); err != nil {
|
|
|
|
log.Error("[createAppResourcesRoute] Invalid URI Bind")
|
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
document, err := api.DB.Queries.GetDocumentWithStats(api.DB.Ctx, database.GetDocumentWithStatsParams{
|
|
|
|
UserID: rUser.(string),
|
|
|
|
DocumentID: rDocID.DocumentID,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
log.Error("[createAppResourcesRoute] GetDocumentWithStats DB Error:", err)
|
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
templateVars["Data"] = document
|
2023-09-23 18:14:57 +00:00
|
|
|
} else if routeName == "document-edit" {
|
|
|
|
var rDocID requestDocumentID
|
|
|
|
if err := c.ShouldBindUri(&rDocID); err != nil {
|
|
|
|
log.Error("[createAppResourcesRoute] Invalid URI Bind")
|
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
var rDocEdit requestDocumentEdit
|
|
|
|
if err := c.ShouldBind(&rDocEdit); err != nil {
|
|
|
|
log.Error("[createAppResourcesRoute] Invalid Form Bind")
|
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if rDocEdit.Author == nil && rDocEdit.Title == nil && rDocEdit.Description == nil && rDocEdit.Cover == nil {
|
|
|
|
log.Error("[createAppResourcesRoute] Missing Form Values")
|
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO - Handle Cover
|
|
|
|
if rDocEdit.Cover != nil {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update Document
|
|
|
|
if _, err := api.DB.Queries.UpsertDocument(api.DB.Ctx, database.UpsertDocumentParams{
|
|
|
|
ID: rDocID.DocumentID,
|
|
|
|
Title: api.sanitizeInput(rDocEdit.Title),
|
|
|
|
Author: api.sanitizeInput(rDocEdit.Author),
|
|
|
|
Description: api.sanitizeInput(rDocEdit.Description),
|
|
|
|
}); err != nil {
|
|
|
|
log.Error("[createAppResourcesRoute] UpsertDocument DB Error:", err)
|
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
c.Redirect(http.StatusFound, "./")
|
|
|
|
return
|
2023-09-21 00:35:01 +00:00
|
|
|
} else if routeName == "activity" {
|
2023-09-23 02:12:36 +00:00
|
|
|
activityFilter := database.GetActivityParams{
|
2023-09-21 00:35:01 +00:00
|
|
|
UserID: rUser.(string),
|
|
|
|
Offset: (*qParams.Page - 1) * *qParams.Limit,
|
|
|
|
Limit: *qParams.Limit,
|
2023-09-23 02:12:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if qParams.Document != nil {
|
|
|
|
activityFilter.DocFilter = true
|
|
|
|
activityFilter.DocumentID = *qParams.Document
|
|
|
|
}
|
|
|
|
|
|
|
|
activity, err := api.DB.Queries.GetActivity(api.DB.Ctx, activityFilter)
|
2023-09-21 00:35:01 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Error("[createAppResourcesRoute] GetActivity DB Error:", err)
|
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
templateVars["Data"] = activity
|
2023-09-18 23:57:18 +00:00
|
|
|
} else if routeName == "home" {
|
2023-09-23 02:12:36 +00:00
|
|
|
start_time := time.Now()
|
2023-09-21 00:35:01 +00:00
|
|
|
weekly_streak, err := api.DB.Queries.GetUserWindowStreaks(api.DB.Ctx, database.GetUserWindowStreaksParams{
|
2023-09-18 23:57:18 +00:00
|
|
|
UserID: rUser.(string),
|
|
|
|
Window: "WEEK",
|
|
|
|
})
|
2023-09-21 00:35:01 +00:00
|
|
|
if err != nil {
|
|
|
|
log.Warn("[createAppResourcesRoute] GetUserWindowStreaks DB Error:", err)
|
|
|
|
}
|
2023-09-23 02:12:36 +00:00
|
|
|
log.Info("GetUserWindowStreaks - WEEK - ", time.Since(start_time))
|
|
|
|
start_time = time.Now()
|
2023-09-18 23:57:18 +00:00
|
|
|
|
2023-09-21 00:35:01 +00:00
|
|
|
daily_streak, err := api.DB.Queries.GetUserWindowStreaks(api.DB.Ctx, database.GetUserWindowStreaksParams{
|
2023-09-18 23:57:18 +00:00
|
|
|
UserID: rUser.(string),
|
|
|
|
Window: "DAY",
|
|
|
|
})
|
|
|
|
if err != nil {
|
2023-09-21 00:35:01 +00:00
|
|
|
log.Warn("[createAppResourcesRoute] GetUserWindowStreaks DB Error:", err)
|
2023-09-18 23:57:18 +00:00
|
|
|
}
|
2023-09-23 02:12:36 +00:00
|
|
|
log.Info("GetUserWindowStreaks - DAY - ", time.Since(start_time))
|
2023-09-18 23:57:18 +00:00
|
|
|
|
2023-09-23 02:12:36 +00:00
|
|
|
start_time = time.Now()
|
2023-09-21 00:35:01 +00:00
|
|
|
database_info, _ := api.DB.Queries.GetDatabaseInfo(api.DB.Ctx, rUser.(string))
|
2023-09-23 02:12:36 +00:00
|
|
|
log.Info("GetDatabaseInfo - ", time.Since(start_time))
|
|
|
|
|
|
|
|
start_time = time.Now()
|
2023-09-21 00:35:01 +00:00
|
|
|
read_graph_data, _ := api.DB.Queries.GetDailyReadStats(api.DB.Ctx, rUser.(string))
|
2023-09-23 02:12:36 +00:00
|
|
|
log.Info("GetDailyReadStats - ", time.Since(start_time))
|
2023-09-21 00:35:01 +00:00
|
|
|
|
2023-09-18 23:57:18 +00:00
|
|
|
templateVars["Data"] = gin.H{
|
|
|
|
"DailyStreak": daily_streak,
|
|
|
|
"WeeklyStreak": weekly_streak,
|
|
|
|
"DatabaseInfo": database_info,
|
|
|
|
"GraphData": read_graph_data,
|
|
|
|
}
|
2023-09-19 23:29:55 +00:00
|
|
|
} else if routeName == "login" {
|
|
|
|
templateVars["RegistrationEnabled"] = api.Config.RegistrationEnabled
|
2023-09-18 23:57:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
c.HTML(http.StatusOK, routeName, templateVars)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (api *API) getDocumentCover(c *gin.Context) {
|
|
|
|
var rDoc requestDocumentID
|
|
|
|
if err := c.ShouldBindUri(&rDoc); err != nil {
|
2023-09-21 00:35:01 +00:00
|
|
|
log.Error("[getDocumentCover] Invalid URI Bind")
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatus(http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate Document Exists in DB
|
|
|
|
document, err := api.DB.Queries.GetDocument(api.DB.Ctx, rDoc.DocumentID)
|
|
|
|
if err != nil {
|
2023-09-21 00:35:01 +00:00
|
|
|
log.Error("[getDocumentCover] GetDocument DB Error:", err)
|
2023-09-18 23:57:18 +00:00
|
|
|
c.AbortWithStatus(http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle Identified Document
|
|
|
|
if document.Olid != nil {
|
|
|
|
if *document.Olid == "UNKNOWN" {
|
2023-09-21 00:35:01 +00:00
|
|
|
c.File("./assets/no-cover.jpg")
|
2023-09-18 23:57:18 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Derive Path
|
|
|
|
fileName := "." + filepath.Clean(fmt.Sprintf("/%s.jpg", *document.Olid))
|
|
|
|
safePath := filepath.Join(api.Config.DataPath, "covers", fileName)
|
|
|
|
|
|
|
|
// Validate File Exists
|
|
|
|
_, err = os.Stat(safePath)
|
|
|
|
if err != nil {
|
2023-09-21 00:35:01 +00:00
|
|
|
c.File("./assets/no-cover.jpg")
|
2023-09-18 23:57:18 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
c.File(safePath)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
This is a bit convoluted because we want to ensure we set the OLID to
|
|
|
|
UNKNOWN if there are any errors. This will ideally prevent us from
|
|
|
|
hitting the OpenLibrary API multiple times in the future.
|
|
|
|
*/
|
|
|
|
|
|
|
|
var coverID string = "UNKNOWN"
|
2023-09-23 02:12:36 +00:00
|
|
|
var coverFilePath string
|
2023-09-18 23:57:18 +00:00
|
|
|
|
|
|
|
// Identify Documents & Save Covers
|
2023-09-23 18:14:57 +00:00
|
|
|
metadataResults, err := metadata.GetMetadata(metadata.MetadataInfo{
|
2023-09-23 02:12:36 +00:00
|
|
|
Title: document.Title,
|
|
|
|
Author: document.Author,
|
2023-09-23 18:14:57 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
if err == nil && len(metadataResults) > 0 && metadataResults[0].GBID != nil {
|
|
|
|
firstResult := metadataResults[0]
|
|
|
|
|
2023-09-23 02:12:36 +00:00
|
|
|
// Derive & Sanitize File Name
|
2023-09-23 18:14:57 +00:00
|
|
|
fileName := "." + filepath.Clean(fmt.Sprintf("/%s.jpg", *firstResult.GBID))
|
2023-09-23 02:12:36 +00:00
|
|
|
|
|
|
|
// Generate Storage Path
|
|
|
|
coverFilePath = filepath.Join(api.Config.DataPath, "covers", fileName)
|
|
|
|
|
2023-09-23 18:14:57 +00:00
|
|
|
err := metadata.SaveCover(*firstResult.GBID, coverFilePath)
|
2023-09-18 23:57:18 +00:00
|
|
|
if err == nil {
|
2023-09-23 18:14:57 +00:00
|
|
|
coverID = *firstResult.GBID
|
|
|
|
log.Info("Title:", *firstResult.Title)
|
|
|
|
log.Info("Author:", *firstResult.Author)
|
|
|
|
log.Info("Description:", *firstResult.Description)
|
|
|
|
log.Info("IDs:", firstResult.ISBN)
|
2023-09-18 23:57:18 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-09-23 02:12:36 +00:00
|
|
|
// coverIDs, err := metadata.GetCoverOLIDs(document.Title, document.Author)
|
|
|
|
// if err == nil && len(coverIDs) > 0 {
|
|
|
|
// coverFilePath, err = metadata.DownloadAndSaveCover(coverIDs[0], api.Config.DataPath)
|
|
|
|
// if err == nil {
|
|
|
|
// coverID = coverIDs[0]
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
|
2023-09-18 23:57:18 +00:00
|
|
|
// Upsert Document
|
|
|
|
if _, err = api.DB.Queries.UpsertDocument(api.DB.Ctx, database.UpsertDocumentParams{
|
|
|
|
ID: document.ID,
|
|
|
|
Olid: &coverID,
|
|
|
|
}); err != nil {
|
2023-09-21 00:35:01 +00:00
|
|
|
log.Warn("[getDocumentCover] UpsertDocument DB Error:", err)
|
2023-09-18 23:57:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Return Unknown Cover
|
|
|
|
if coverID == "UNKNOWN" {
|
2023-09-21 00:35:01 +00:00
|
|
|
c.File("./assets/no-cover.jpg")
|
2023-09-18 23:57:18 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-09-23 02:12:36 +00:00
|
|
|
c.File(coverFilePath)
|
2023-09-18 23:57:18 +00:00
|
|
|
}
|
2023-09-23 18:14:57 +00:00
|
|
|
|
|
|
|
// DELETE /api/documents/:document
|
|
|
|
func (api *API) deleteDocument(c *gin.Context) {
|
|
|
|
var rDocID requestDocumentID
|
|
|
|
if err := c.ShouldBindUri(&rDocID); err != nil {
|
|
|
|
log.Error("[deleteDocument] Invalid URI Bind")
|
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO
|
|
|
|
}
|
|
|
|
|
|
|
|
// POST /api/documents/:document/identify
|
|
|
|
func (api *API) identifyDocument(c *gin.Context) {
|
|
|
|
var rDocID requestDocumentID
|
|
|
|
if err := c.ShouldBindUri(&rDocID); err != nil {
|
|
|
|
log.Error("[identifyDocument] Invalid URI Bind")
|
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
isbn := strings.TrimSpace(c.PostForm("ISBN"))
|
|
|
|
if isbn == "" {
|
|
|
|
log.Error("[identifyDocument] Invalid Form")
|
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid Request"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
metadataResults, err := metadata.GetMetadata(metadata.MetadataInfo{
|
|
|
|
ISBN: []*string{&isbn},
|
|
|
|
})
|
|
|
|
if err != nil || len(metadataResults) == 0 {
|
|
|
|
log.Error("[identifyDocument] Metadata Error")
|
|
|
|
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Metadata Error"})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO
|
|
|
|
firstResult := metadataResults[0]
|
|
|
|
|
|
|
|
if firstResult.Title != nil {
|
|
|
|
log.Info("Title:", *firstResult.Title)
|
|
|
|
}
|
|
|
|
if firstResult.Author != nil {
|
|
|
|
log.Info("Author:", *firstResult.Author)
|
|
|
|
}
|
|
|
|
if firstResult.Description != nil {
|
|
|
|
log.Info("Description:", *firstResult.Description)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, val := range firstResult.ISBN {
|
|
|
|
log.Info("ISBN:", *val)
|
|
|
|
}
|
|
|
|
|
|
|
|
c.Redirect(http.StatusFound, "/")
|
|
|
|
}
|