diff --git a/handlers/core/search.go b/handlers/core/search.go index e7a458e4..4d3c05c0 100644 --- a/handlers/core/search.go +++ b/handlers/core/search.go @@ -1,6 +1,7 @@ package core import ( + "fmt" "strings" "time" @@ -19,12 +20,16 @@ func Search(c *fiber.Ctx) error { c.Locals("Canonical", "search") keyword := strings.TrimSpace(c.Query("q")) - if len(keyword) < 3 { - c.Locals("Error", "Keywords need to be in a group of three or more characters.") - return c.Render("core/search", fiber.Map{}) - } c.Locals("Keyword", keyword) + category := strings.TrimSpace(c.Query("category")) + c.Locals("Category", category) + + query := keyword + if category != "" { + query += fmt.Sprintf(" category:%s", category) + } + page, err := models.IsValidPage(c.Query("page")) if err != nil || page < 1 { c.Locals("Title", "Invalid page size") @@ -36,7 +41,7 @@ func Search(c *fiber.Ctx) error { t := time.Now() - total, err := storage.TotalSearchStyles(keyword, sort) + total, err := storage.TotalSearchStyles(query, sort) if err != nil { log.Database.Println(err) c.Locals("Title", "Failed to count userstyles") @@ -50,7 +55,7 @@ func Search(c *fiber.Ctx) error { } c.Locals("Pagination", p) - s, err := storage.FindSearchStyles(keyword, p.SortStyles(), page) + s, err := storage.FindSearchStyles(query, p.SortStyles(), page) if err != nil { log.Database.Println(err) c.Locals("Title", "Failed to search for userstyles") diff --git a/handlers/review/create.go b/handlers/review/create.go index ff11f523..0cef3ea7 100644 --- a/handlers/review/create.go +++ b/handlers/review/create.go @@ -9,6 +9,7 @@ import ( "userstyles.world/handlers/jwt" "userstyles.world/models" "userstyles.world/modules/cache" + "userstyles.world/modules/database" "userstyles.world/modules/log" ) @@ -92,8 +93,7 @@ func createForm(c *fiber.Ctx) error { return c.Render("err", fiber.Map{}) } - // Create a notification. - notification := models.Notification{ + n := models.Notification{ Kind: models.KindReview, TargetID: int(s.UserID), UserID: int(u.ID), @@ -101,7 +101,7 @@ func createForm(c *fiber.Ctx) error { ReviewID: int(r.ID), } - if err = notification.Create(); err != nil { + if err = models.CreateNotification(database.Conn, &n); err != nil { log.Warn.Printf("Failed to add notification to review %d: %s\n", r.ID, err) } diff --git a/handlers/review/remove.go b/handlers/review/remove.go index ede99c9a..fd5456d8 100644 --- a/handlers/review/remove.go +++ b/handlers/review/remove.go @@ -98,7 +98,7 @@ func removeForm(c *fiber.Ctx) error { UserID: int(u.ID), StyleID: sid, } - if err = n.Create(); err != nil { + if err = models.CreateNotification(database.Conn, &n); err != nil { c.Locals("Title", "Failed to add notification") return c.Status(fiber.StatusNotFound).Render("err", fiber.Map{}) } diff --git a/handlers/style/ban.go b/handlers/style/ban.go index 2ff176f4..2e528a9c 100644 --- a/handlers/style/ban.go +++ b/handlers/style/ban.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/gofiber/fiber/v2" + "gorm.io/gorm" "userstyles.world/handlers/jwt" "userstyles.world/models" @@ -13,6 +14,7 @@ import ( "userstyles.world/modules/database" "userstyles.world/modules/email" "userstyles.world/modules/log" + "userstyles.world/modules/storage" ) func BanGet(c *fiber.Ctx) error { @@ -65,8 +67,7 @@ func BanPost(c *fiber.Ctx) error { } id := c.Params("id") - // Check if style exists. - s, err := models.GetStyleByID(id) + style, err := models.GetStyleByID(id) if err != nil { c.Status(fiber.StatusNotFound) return c.Render("err", fiber.Map{ @@ -75,87 +76,77 @@ func BanPost(c *fiber.Ctx) error { }) } - // Initialize modlog data. - logEntry := models.Log{ + user, err := storage.FindUser(style.UserID) + if err != nil { + c.Status(fiber.StatusInternalServerError) + return c.Render("err", fiber.Map{ + "Title": "Internal server error", + "User": u, + }) + } + + event := models.Log{ UserID: u.ID, Username: u.Username, Kind: models.LogRemoveStyle, - TargetUserName: s.Username, - TargetData: s.Name, + TargetUserName: style.Username, + TargetData: style.Name, Reason: strings.TrimSpace(c.FormValue("reason")), Message: strings.TrimSpace(c.FormValue("message")), Censor: c.FormValue("censor") == "on", } - // Add banned style log entry. - modlog := new(models.Log) - if err := modlog.AddLog(&logEntry); err != nil { - log.Warn.Printf("Failed to add style %d to ModLog: %s", s.ID, err.Error()) - return c.Render("err", fiber.Map{ - "Title": "Internal server error", - "User": u, - }) - } - - // Delete style from database. - q := new(models.Style) - if err = database.Conn.Delete(q, "styles.id = ?", s.ID).Error; err != nil { - log.Warn.Printf("Failed to delete style %d: %s\n", s.ID, err.Error()) - c.Status(fiber.StatusInternalServerError) - return c.Render("err", fiber.Map{ - "Title": "Internal server error", - "User": u, - }) + notification := models.Notification{ + Kind: models.KindBannedStyle, + TargetID: int(event.ID), + UserID: int(user.ID), + StyleID: int(style.ID), } - // Delete stats from database. - if err = new(models.Stats).Delete(s.ID); err != nil { - log.Warn.Printf("Failed to delete stats for style %d: %s\n", s.ID, err.Error()) - c.Status(fiber.StatusInternalServerError) + // INSERT INTO `logs` + err = database.Conn.Transaction(func(tx *gorm.DB) error { + if err = storage.DeleteUserstyle(tx, i); err != nil { + return err + } + if err = models.DeleteStats(tx, i); err != nil { + return err + } + if err = storage.DeleteSearchData(tx, i); err != nil { + return err + } + if err = models.CreateLog(tx, &event); err != nil { + return err + } + if err = models.CreateNotification(tx, ¬ification); err != nil { + return err + } + return models.RemoveStyleCode(id) + }) + if err != nil { + log.Database.Printf("Failed to remove %d: %s\n", i, err) return c.Render("err", fiber.Map{ - "Title": "Internal server error", + "Title": "Failed to remove userstyle", "User": u, }) } - if err = models.RemoveStyleCode(strconv.Itoa(int(s.ID))); err != nil { - log.Warn.Printf("kind=removecode id=%v err=%q\n", s.ID, err) - } - cache.Code.Remove(i) - go func(style *models.APIStyle, entry models.Log) { - user, err := models.FindUserByID(strconv.Itoa(int(style.UserID))) - if err != nil { - log.Warn.Printf("Failed to find user %d: %s", style.UserID, err.Error()) - return - } + go sendRemovalEmail(user, style, event) - // Add notification to database. - notification := models.Notification{ - Seen: false, - Kind: models.KindBannedStyle, - TargetID: int(entry.ID), - UserID: int(user.ID), - StyleID: int(style.ID), - } - - if err := notification.Create(); err != nil { - log.Warn.Printf("Failed to create a notification for ban removal %d: %v\n", style.ID, err) - } - - args := fiber.Map{ - "User": user, - "Style": style, - "Log": entry, - "Link": config.BaseURL + "/modlog#id-" + strconv.Itoa(int(entry.ID)), - } + return c.Redirect("/modlog", fiber.StatusSeeOther) +} - title := "Your style has been removed" - if err := email.Send("style/ban", user.Email, title, args); err != nil { - log.Warn.Printf("Failed to email author for style %d: %s\n", style.ID, err) - } - }(s, logEntry) +func sendRemovalEmail(user *storage.User, style *models.APIStyle, entry models.Log) { + args := fiber.Map{ + "User": user, + "Style": style, + "Log": entry, + "Link": config.BaseURL + "/modlog#id-" + strconv.Itoa(int(entry.ID)), + } - return c.Redirect("/modlog", fiber.StatusSeeOther) + title := "Your style has been removed" + if err := email.Send("style/ban", user.Email, title, args); err != nil { + log.Warn.Printf("Failed to email author for style %d: %s\n", style.ID, err) + } } diff --git a/handlers/style/delete.go b/handlers/style/delete.go index b603c93e..873fabff 100644 --- a/handlers/style/delete.go +++ b/handlers/style/delete.go @@ -1,15 +1,15 @@ package style import ( - "strconv" - "github.com/gofiber/fiber/v2" + "gorm.io/gorm" "userstyles.world/handlers/jwt" "userstyles.world/models" "userstyles.world/modules/cache" "userstyles.world/modules/database" "userstyles.world/modules/log" + "userstyles.world/modules/storage" ) func DeleteGet(c *fiber.Ctx) error { @@ -67,30 +67,26 @@ func DeletePost(c *fiber.Ctx) error { }) } - // Delete style from database. - q := new(models.Style) - if err = database.Conn.Delete(q, "styles.id = ?", id).Error; err != nil { - log.Warn.Printf("Failed to delete style %d: %s\n", s.ID, err.Error()) - return c.Render("err", fiber.Map{ - "Title": "Internal server error", - "User": u, - }) - } - - // Delete stats from database. - if err = new(models.Stats).Delete(s.ID); err != nil { - log.Warn.Printf("Failed to delete stats for style %d: %s\n", s.ID, err.Error()) - c.Status(fiber.StatusInternalServerError) + err = database.Conn.Transaction(func(tx *gorm.DB) error { + if err = storage.DeleteUserstyle(tx, i); err != nil { + return err + } + if err = models.DeleteStats(tx, i); err != nil { + return err + } + if err = storage.DeleteSearchData(tx, i); err != nil { + return err + } + return models.RemoveStyleCode(id) + }) + if err != nil { + log.Database.Printf("Failed to delete %d: %s\n", i, err) return c.Render("err", fiber.Map{ - "Title": "Internal server error", + "Title": "Failed to remove userstyle", "User": u, }) } - if err = models.RemoveStyleCode(strconv.Itoa(int(s.ID))); err != nil { - log.Warn.Printf("kind=removecode id=%v err=%q\n", s.ID, err) - } - cache.Code.Remove(i) return c.Redirect("/user/"+u.Username, fiber.StatusSeeOther) diff --git a/handlers/style/promote.go b/handlers/style/promote.go index cfbb0a43..cd95e609 100644 --- a/handlers/style/promote.go +++ b/handlers/style/promote.go @@ -93,8 +93,7 @@ func Promote(c *fiber.Ctx) error { if !style.Featured { go sendPromotionEmail(style, user, u.Username) - // Create a notification. - notification := models.Notification{ + n := models.Notification{ Seen: false, Kind: models.KindStylePromotion, TargetID: int(style.UserID), @@ -102,11 +101,9 @@ func Promote(c *fiber.Ctx) error { StyleID: id, } - go func(notification models.Notification) { - if err := notification.Create(); err != nil { - log.Warn.Printf("Failed to create a notification for %d, err: %v", id, err.Error()) - } - }(notification) + if err := models.CreateNotification(database.Conn, &n); err != nil { + log.Warn.Printf("Failed to create a notification for %d: %s\n", id, err) + } } return c.Redirect("/style/"+p, fiber.StatusSeeOther) diff --git a/handlers/style/view.go b/handlers/style/view.go index 2582a99e..5b76809e 100644 --- a/handlers/style/view.go +++ b/handlers/style/view.go @@ -50,7 +50,7 @@ func GetStylePage(c *fiber.Ctx) error { "User": u, "Title": data.Name, "Style": data, - "URL": c.BaseURL() + c.Path(), + "URL": c.BaseURL() + "/style/" + id, "Slug": slug, "Canonical": "style/" + id + "/" + slug, "RenderMeta": true, diff --git a/handlers/user/ban.go b/handlers/user/ban.go index c3759bb9..95e7f4b3 100644 --- a/handlers/user/ban.go +++ b/handlers/user/ban.go @@ -9,6 +9,7 @@ import ( "userstyles.world/handlers/jwt" "userstyles.world/models" "userstyles.world/modules/config" + "userstyles.world/modules/database" "userstyles.world/modules/email" "userstyles.world/modules/log" ) @@ -105,8 +106,7 @@ func ConfirmBan(c *fiber.Ctx) error { } // Add banned user log entry. - modlog := new(models.Log) - if err := modlog.AddLog(&logEntry); err != nil { + if err := models.CreateLog(database.Conn, &logEntry); err != nil { log.Warn.Printf("Failed to add user %d to ModLog: %s\n", targetUser.ID, err.Error()) return c.Render("err", fiber.Map{ "Title": "Internal server error", diff --git a/models/log.go b/models/log.go index 4c2aaa0c..6cd205c5 100644 --- a/models/log.go +++ b/models/log.go @@ -45,16 +45,9 @@ type APILog struct { TargetUserName string } -// AddLog adds a new log to the database. -func (*Log) AddLog(logEntry *Log) (err error) { - err = db(). - Model(modelLog). - Create(logEntry). - Error - if err != nil { - return errors.ErrFailedLogAddition - } - return nil +// CreateLog inserts a new log entry into the database. +func CreateLog(db *gorm.DB, log *Log) (err error) { + return db.Model(modelLog).Create(log).Error } // GetLogOfKind returns all the logs of the specified kind and diff --git a/models/notification.go b/models/notification.go index da41c867..7c39e362 100644 --- a/models/notification.go +++ b/models/notification.go @@ -29,6 +29,7 @@ type Notification struct { ReviewID int `gorm:"default:null"` } -func (n Notification) Create() error { - return db().Create(&n).Error +// CreateNotification inserts a new notification. +func CreateNotification(db *gorm.DB, n *Notification) error { + return db.Create(&n).Error } diff --git a/models/search.go b/models/search.go index 1392b8a8..479ae495 100644 --- a/models/search.go +++ b/models/search.go @@ -5,21 +5,29 @@ import "userstyles.world/modules/database" func InitStyleSearch() error { init := ` DROP TABLE IF EXISTS fts_styles; -CREATE VIRTUAL TABLE fts_styles USING FTS5(id, name, description, notes, tokenize="trigram"); -INSERT INTO fts_styles(id, name, description, notes) SELECT id, name, description, notes FROM styles; +CREATE VIRTUAL TABLE fts_styles USING FTS5(id, name, description, notes, category); + +INSERT INTO fts_styles(id, name, description, notes, category) +SELECT id, name, description, notes, category +FROM styles +WHERE deleted_at IS NULL; DROP TRIGGER IF EXISTS fts_styles_insert; CREATE TRIGGER fts_styles_insert AFTER INSERT ON styles BEGIN - INSERT INTO fts_styles(id, name, description, notes) - VALUES (new.id, new.name, new.description, new.notes); + INSERT INTO fts_styles(id, name, description, notes, category) + VALUES (new.id, new.name, new.description, new.notes, new.category); END; DROP TRIGGER IF EXISTS fts_styles_update; CREATE TRIGGER fts_styles_update AFTER UPDATE ON styles BEGIN UPDATE fts_styles - SET name = new.name, description = new.description, notes = new.notes + SET + name = new.name, + description = new.description, + notes = new.notes, + category = new.category WHERE id = old.id; END; diff --git a/models/stats.go b/models/stats.go index 22e671f6..7ef5550e 100644 --- a/models/stats.go +++ b/models/stats.go @@ -51,9 +51,9 @@ func (DashStats) GetCounts(t string) (q []DashStats, err error) { return q, nil } -// Delete will remove stats for a given style ID. -func (*Stats) Delete(id any) error { - return db().Delete(&modelStats, "style_id = ?", id).Error +// DeleteStats removes stats from database. +func DeleteStats(db *gorm.DB, id int) error { + return db.Delete(&modelStats, "style_id = ?", id).Error } func GetHomepageStatistics() *SiteStats { diff --git a/modules/database/init/init.go b/modules/database/init/init.go index f2b25dcd..8065b8b4 100644 --- a/modules/database/init/init.go +++ b/modules/database/init/init.go @@ -110,21 +110,6 @@ func Initialize() { } } - q := "DELETE FROM fts_styles WHERE id IN (SELECT id FROM styles WHERE deleted_at IS NOT NULL)" - if err = database.Conn.Exec(q).Error; err != nil { - log.Info.Fatal(err) - } - - q = "UPDATE styles SET user_id = 1592 WHERE id = 10653" - if err = database.Conn.Debug().Exec(q).Error; err != nil { - log.Info.Fatal(err) - } - - var h models.History - if err = database.Conn.Migrator().AutoMigrate(h); err != nil { - log.Info.Fatal(err) - } - if shouldSeed { seed() } diff --git a/modules/database/init/migrate.go b/modules/database/init/migrate.go index 43aaccb8..4a9ca220 100644 --- a/modules/database/init/migrate.go +++ b/modules/database/init/migrate.go @@ -18,12 +18,7 @@ func runMigration(db *gorm.DB) { // Wrap in a transaction to allow rollbacks. db.Transaction(func(tx *gorm.DB) error { - var l models.Log - if err := tx.Migrator().AddColumn(l, "Message"); err != nil { - log.Database.Fatalf("Failed to add column message: %s\n", err) - } - - return nil + return models.InitStyleSearch() }) log.Database.Printf("Done in %s.\n", time.Since(t).Round(time.Microsecond)) diff --git a/modules/errors/errors.go b/modules/errors/errors.go index e0f9ae25..c3f610ba 100644 --- a/modules/errors/errors.go +++ b/modules/errors/errors.go @@ -74,9 +74,6 @@ var ( // ErrFailedLogRetrieval errors that it couldn't retrieve all the logs. ErrFailedLogRetrieval = errors.New("failed to find all logs") - // ErrFailedLogAddition errors that it couldn't add the log. - ErrFailedLogAddition = errors.New("failed to add the log") - // ErrOnlyRemovedStyle errors that this function only allows to remove style kind. ErrOnlyRemovedStyle = errors.New("only remove style kind is allowed") diff --git a/modules/storage/style.go b/modules/storage/style.go index 191fe566..c5938e8d 100644 --- a/modules/storage/style.go +++ b/modules/storage/style.go @@ -67,3 +67,8 @@ func FindStyleForMirror(id int) (models.Style, error) { return s, nil } + +// DeleteUserstyle removes userstyle from database. +func DeleteUserstyle(db *gorm.DB, id int) error { + return db.Delete(&models.Style{}, "id = ?", id).Error +} diff --git a/modules/storage/style_search.go b/modules/storage/style_search.go index ee2fbb29..7b73ed7d 100644 --- a/modules/storage/style_search.go +++ b/modules/storage/style_search.go @@ -101,3 +101,8 @@ MATCH ?`) return s, nil } + +// DeleteSearchData removes a userstyle from FTS table. +func DeleteSearchData(db *gorm.DB, id int) error { + return db.Exec("DELETE FROM fts_styles WHERE id = ?", id).Error +} diff --git a/modules/storage/user.go b/modules/storage/user.go index 4b08fd37..37abe545 100644 --- a/modules/storage/user.go +++ b/modules/storage/user.go @@ -29,3 +29,13 @@ func FindUsersCreatedOn(date time.Time) ([]User, error) { return res, nil } + +// FindUser returns a user. +func FindUser(id uint) (u *User, err error) { + err = database.Conn.Find(&u, "id = ?", id).Error + if err != nil { + return nil, err + } + + return u, err +} diff --git a/web/typescript/page/view-style.ts b/web/typescript/page/view-style.ts index 5a2c95d4..27912087 100644 --- a/web/typescript/page/view-style.ts +++ b/web/typescript/page/view-style.ts @@ -3,16 +3,19 @@ import {doDomOperation} from 'utils/dom'; export const initViewStyle = () => doDomOperation(() => { shareButton(); checkIfStyleInstalled(); + removeStylusTooltip(); }); function shareButton() { - const urlValue = document.getElementById('share').textContent; + const urlBar = document.getElementById('share'); const shareButton = document.getElementById('btn-share') as HTMLButtonElement; if (!shareButton) { return; } + urlBar.textContent += urlBar.getAttribute("slug"); + shareButton.removeAttribute("hidden"); shareButton.addEventListener('click', () => { - navigator.clipboard.writeText(urlValue).then(() => { + navigator.clipboard.writeText(urlBar.textContent).then(() => { shareButton.classList.add('copied'); }, () => { shareButton.classList.add('copied-failed'); @@ -45,3 +48,8 @@ function checkIfStyleInstalled() { origin: 'https://userstyles.world' })); } + +function removeStylusTooltip() { + const Stylus = document.querySelector('a#stylus'); + Stylus.removeAttribute("data-tooltip"); +} diff --git a/web/views/core/search.tmpl b/web/views/core/search.tmpl index 262125d8..9f9a3592 100644 --- a/web/views/core/search.tmpl +++ b/web/views/core/search.tmpl @@ -7,13 +7,20 @@