commit 76f52325c73cf3b7ba2a02b0715fe609db2ccaf3
parent 12ac6fbaefe89dbbc20ee8d6eb340ab085f8d52b
Author: n0tr1v <n0tr1v@protonmail.com>
Date: Thu, 9 Mar 2023 09:49:10 -0800
memes
Diffstat:
9 files changed, 265 insertions(+), 1 deletion(-)
diff --git a/cmd/dkf/migrations/129.sql b/cmd/dkf/migrations/129.sql
@@ -0,0 +1,10 @@
+-- +migrate Up
+CREATE TABLE IF NOT EXISTS memes (
+ id INTEGER PRIMARY KEY,
+ slug VARCHAR(255) NOT NULL UNIQUE,
+ file_name VARCHAR(255) UNIQUE NOT NULL,
+ orig_file_name VARCHAR(255) NOT NULL,
+ file_size INTEGER NOT NULL,
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP);
+
+-- +migrate Down
diff --git a/pkg/actions/actions.go b/pkg/actions/actions.go
@@ -145,6 +145,11 @@ func ensureProjectHome() {
logrus.Fatal("Failed to create dkforest html folder", err)
}
+ config.Global.SetProjectMemesPath(filepath.Join(projectPath, "memes"))
+ if err := os.MkdirAll(config.Global.ProjectMemesPath(), 0755); err != nil {
+ logrus.Fatal("Failed to create memes uploads folder", err)
+ }
+
config.Global.SetProjectUploadsPath(filepath.Join(projectPath, "uploads"))
if err := os.MkdirAll(config.Global.ProjectUploadsPath(), 0755); err != nil {
logrus.Fatal("Failed to create dkforest uploads folder", err)
diff --git a/pkg/config/config.go b/pkg/config/config.go
@@ -158,6 +158,7 @@ type GlobalConf struct {
projectPath string // project path
projectLocalsPath string // directory where we keep custom translation files
projectHtmlPath string
+ projectMemesPath string
projectUploadsPath string
projectFiledropPath string
projectDownloadsPath string
@@ -287,6 +288,20 @@ func (c *GlobalConf) SetProjectHTMLPath(projectHtmlPath string) {
c.projectHtmlPath = projectHtmlPath
}
+// ProjectMemesPath ...
+func (c *GlobalConf) ProjectMemesPath() string {
+ c.RLock()
+ defer c.RUnlock()
+ return c.projectMemesPath
+}
+
+// SetProjectMemesPath ...
+func (c *GlobalConf) SetProjectMemesPath(projectMemesPath string) {
+ c.Lock()
+ defer c.Unlock()
+ c.projectMemesPath = projectMemesPath
+}
+
// ProjectUploadsPath ...
func (c *GlobalConf) ProjectUploadsPath() string {
c.RLock()
diff --git a/pkg/database/database.go b/pkg/database/database.go
@@ -132,6 +132,10 @@ type IDkfDB interface {
GetLinkTags(linkID int64) (out []LinksTag, err error)
GetLinks() (out []Link, err error)
GetListedChatRooms(userID UserID) (out []ChatRoomAug, err error)
+ GetMemeByFileName(filename string) (out Meme, err error)
+ GetMemeByID(memeID MemeID) (out Meme, err error)
+ GetMemeBySlug(slug string) (out Meme, err error)
+ GetMemes() (out []Meme, err error)
GetModeratorsUsers() (out []User, err error)
GetOfficialChatRooms() (out []ChatRoom, err error)
GetOfficialChatRooms1(userID UserID) (out []ChatRoomAug, err error)
diff --git a/pkg/database/tableMemes.go b/pkg/database/tableMemes.go
@@ -0,0 +1,120 @@
+package database
+
+import (
+ "dkforest/pkg/config"
+ "dkforest/pkg/utils"
+ html2 "html"
+ "io"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "time"
+
+ "github.com/sirupsen/logrus"
+)
+
+type MemeID int64
+
+type Meme struct {
+ ID MemeID
+ Slug string
+ FileName string
+ OrigFileName string
+ FileSize int64
+ CreatedAt time.Time
+}
+
+func (u *Meme) DoSave(db *DkfDB) {
+ if err := db.db.Save(u).Error; err != nil {
+ logrus.Error(err)
+ }
+}
+
+func (u *Meme) GetHTMLLink() string {
+ escapedOrigFileName := html2.EscapeString(u.OrigFileName)
+ return `<a href="/memes/` + u.FileName + `" rel="noopener noreferrer" target="_blank">` + escapedOrigFileName + `</a>`
+}
+
+func (u *Meme) GetContent() (os.FileInfo, []byte, error) {
+ filePath1 := filepath.Join(config.Global.ProjectMemesPath(), u.FileName)
+ f, err := os.Open(filePath1)
+ if err != nil {
+ return nil, nil, err
+ }
+ defer f.Close()
+
+ fileBytes, _ := io.ReadAll(f)
+ decFileBytes, err := utils.DecryptAESMaster(fileBytes)
+ if err != nil {
+ decFileBytes = fileBytes
+ }
+ fi, err := f.Stat()
+ if err != nil {
+ return nil, nil, err
+ }
+ return fi, decFileBytes, nil
+}
+
+func (u *Meme) Exists() bool {
+ filePath1 := filepath.Join(config.Global.ProjectMemesPath(), u.FileName)
+ return utils.FileExists(filePath1)
+}
+
+func (u *Meme) Delete(db *DkfDB) error {
+ if err := os.Remove(filepath.Join(config.Global.ProjectMemesPath(), u.FileName)); err != nil {
+ return err
+ }
+ if err := db.db.Delete(&u).Error; err != nil {
+ return err
+ }
+ return nil
+}
+
+// CreateMeme create file on disk in "memes" folder, and save meme in database as well.
+func (d *DkfDB) CreateMeme(fileName string, content []byte) (*Meme, error) {
+ return d.createMemeWithSize(fileName, content, int64(len(content)))
+}
+
+func (d *DkfDB) CreateEncryptedMemeWithSize(fileName string, content []byte, size int64) (*Meme, error) {
+ encryptedContent, err := utils.EncryptAESMaster(content)
+ if err != nil {
+ return nil, err
+ }
+ return d.createMemeWithSize(fileName, encryptedContent, size)
+}
+
+func (d *DkfDB) createMemeWithSize(fileName string, content []byte, size int64) (*Meme, error) {
+ newFileName := utils.MD5([]byte(utils.GenerateToken32()))
+ if err := ioutil.WriteFile(filepath.Join(config.Global.ProjectMemesPath(), newFileName), content, 0644); err != nil {
+ return nil, err
+ }
+ meme := Meme{
+ FileName: newFileName,
+ OrigFileName: fileName,
+ FileSize: size,
+ }
+ if err := d.db.Create(&meme).Error; err != nil {
+ logrus.Error(err)
+ }
+ return &meme, nil
+}
+
+func (d *DkfDB) GetMemeByFileName(filename string) (out Meme, err error) {
+ err = d.db.First(&out, "file_name = ?", filename).Error
+ return
+}
+
+func (d *DkfDB) GetMemeBySlug(slug string) (out Meme, err error) {
+ err = d.db.First(&out, "slug = ?", slug).Error
+ return
+}
+
+func (d *DkfDB) GetMemeByID(memeID MemeID) (out Meme, err error) {
+ err = d.db.First(&out, "id = ?", memeID).Error
+ return
+}
+
+func (d *DkfDB) GetMemes() (out []Meme, err error) {
+ err = d.db.Order("id DESC").Find(&out).Error
+ return
+}
diff --git a/pkg/web/handlers/api/v1/handlers.go b/pkg/web/handlers/api/v1/handlers.go
@@ -77,6 +77,10 @@ var bsRgx = regexp.MustCompile(`^/pm ` + optAtGUser + ` /bs\s?([A-J]\d)?$`)
var cRgx = regexp.MustCompile(`^/pm ` + optAtGUser + ` /c\s?(move)?$`)
var hideRgx = regexp.MustCompile(`^/hide (?:“\[)?(\d{2}:\d{2}:\d{2})`)
var unhideRgx = regexp.MustCompile(`^/unhide (\d{2}:\d{2}:\d{2})$`)
+var memeRgx = regexp.MustCompile(`^/meme ([a-zA-Z0-9_-]{3,50})$`)
+var memeRenameRgx = regexp.MustCompile(`^/meme ([a-zA-Z0-9_-]{3,50}) ([a-zA-Z0-9_-]{3,50})$`)
+var memeRemoveRgx = regexp.MustCompile(`^/memerm ([a-zA-Z0-9_-]{3,50})$`)
+var memesRgx = regexp.MustCompile(`^/memes$`)
// ChatMessagesHandler room messages iframe handler
// The chat messages iframe use this endpoint to get the messages for a room.
diff --git a/pkg/web/handlers/api/v1/slashInterceptor.go b/pkg/web/handlers/api/v1/slashInterceptor.go
@@ -14,6 +14,8 @@ import (
"github.com/asaskevich/govalidator"
"github.com/dustin/go-humanize"
"github.com/sirupsen/logrus"
+ "os"
+ "path/filepath"
"sort"
"strconv"
"strings"
@@ -132,7 +134,11 @@ func handleAdminCmd(c *Command) (handled bool) {
return handleSystemCmd(c) ||
handleSetChatRoomExternalLink(c) ||
handlePurge(c) ||
- handleRename(c)
+ handleRename(c) ||
+ handleNewMeme(c) ||
+ handleRenameMeme(c) ||
+ handleRemoveMeme(c) ||
+ handleListMemes(c)
}
return false
}
@@ -1581,3 +1587,84 @@ func handleRename(c *Command) (handled bool) {
}
return
}
+
+func handleNewMeme(c *Command) (handled bool) {
+ if m := memeRgx.FindStringSubmatch(c.message); len(m) == 2 {
+ if c.upload == nil {
+ c.err = errors.New("no file uploaded")
+ return true
+ }
+ slug := m[1]
+ oldPath := filepath.Join(config.Global.ProjectUploadsPath(), c.upload.FileName)
+ newPath := filepath.Join(config.Global.ProjectMemesPath(), c.upload.FileName)
+ _ = os.Rename(oldPath, newPath)
+
+ if err := c.db.DB().Delete(&c.upload).Error; err != nil {
+ logrus.Error(err)
+ }
+
+ meme := database.Meme{
+ Slug: slug,
+ FileName: c.upload.FileName,
+ OrigFileName: c.upload.OrigFileName,
+ FileSize: c.upload.FileSize,
+ }
+ if err := c.db.DB().Create(&meme).Error; err != nil {
+ _ = os.Remove(newPath)
+ logrus.Error(err)
+ }
+
+ c.err = ErrRedirect
+ return true
+ }
+ return
+}
+
+func handleRemoveMeme(c *Command) (handled bool) {
+ if m := memeRemoveRgx.FindStringSubmatch(c.message); len(m) == 2 {
+ slug := m[1]
+ meme, err := c.db.GetMemeBySlug(slug)
+ if err != nil {
+ c.err = errors.New("meme not found")
+ return true
+ }
+ if err := meme.Delete(c.db); err != nil {
+ c.err = err
+ return true
+ }
+ c.err = NewErrSuccess("meme removed")
+ return true
+ }
+ return
+}
+
+func handleRenameMeme(c *Command) (handled bool) {
+ if m := memeRenameRgx.FindStringSubmatch(c.message); len(m) == 3 {
+ slug := m[1]
+ newSlug := m[2]
+ meme, err := c.db.GetMemeBySlug(slug)
+ if err != nil {
+ c.err = errors.New("meme not found")
+ return true
+ }
+ meme.Slug = newSlug
+ meme.DoSave(c.db)
+ c.err = NewErrSuccess("meme renamed")
+ return true
+ }
+ return
+}
+
+func handleListMemes(c *Command) (handled bool) {
+ if m := memesRgx.FindStringSubmatch(c.message); len(m) == 1 {
+ memes, _ := c.db.GetMemes()
+ msg := ""
+ for _, m := range memes {
+ msg += fmt.Sprintf(`<a href="/memes/%s" rel="noopener noreferrer" target="_blank">%s</a> (%s)<br />`, m.Slug, m.Slug, humanize.Bytes(uint64(m.FileSize)))
+ }
+ c.zeroMsg(msg)
+ c.err = ErrRedirect
+ return true
+ }
+ return
+}
diff --git a/pkg/web/handlers/handlers.go b/pkg/web/handlers/handlers.go
@@ -1140,6 +1140,24 @@ func forgotPasswordHandler(c echo.Context) error {
return c.Render(http.StatusOK, "flash", FlashResponse{"should not go here", "/login", "alert-danger"})
}
+func MemeHandler(c echo.Context) error {
+ slug := c.Param("slug")
+ db := c.Get("database").(*database.DkfDB)
+ meme, err := db.GetMemeBySlug(slug)
+ if err != nil {
+ return c.Redirect(http.StatusFound, "/")
+ }
+
+ fi, by, err := meme.GetContent()
+ if err != nil {
+ return c.Redirect(http.StatusFound, "/")
+ }
+ buf := bytes.NewReader(by)
+
+ http.ServeContent(c.Response(), c.Request(), fi.Name(), fi.ModTime(), buf)
+ return nil
+}
+
func NewsHandler(c echo.Context) error {
var data newsData
return c.Render(http.StatusOK, "news", data)
diff --git a/pkg/web/web.go b/pkg/web/web.go
@@ -162,6 +162,7 @@ func getMainServer(db *database.DkfDB, i18nBundle *i18n.Bundle, renderer *tmp.Te
authGroup.GET("/bhcli/downloads", handlers.BhcliDownloadsHandler)
authGroup.GET("/bhcli/downloads/:filename", handlers.BhcliDownloadFileHandler, middlewares.AuthRateLimitMiddleware(1*time.Second, 2), middlewares.CaptchaMiddleware())
authGroup.POST("/bhcli/downloads/:filename", handlers.BhcliDownloadFileHandler, middlewares.AuthRateLimitMiddleware(1*time.Second, 2), middlewares.CaptchaMiddleware())
+ authGroup.GET("/memes/:slug", handlers.MemeHandler, middlewares.AuthRateLimitMiddleware(time.Second, 2))
authGroup.GET("/news", handlers.NewsHandler, middlewares.AuthRateLimitMiddleware(time.Second, 2))
authGroup.GET("/links", handlers.LinksHandler, middlewares.AuthRateLimitMiddleware(time.Second, 2))
authGroup.GET("/links/download", handlers.LinksDownloadHandler, middlewares.AuthRateLimitMiddleware(time.Second, 2))