dkforest

A forum and chat platform (onion)
git clone https://git.dasho.dev/n0tr1v/dkforest.git
Log | Files | Refs | LICENSE

commit 76f52325c73cf3b7ba2a02b0715fe609db2ccaf3
parent 12ac6fbaefe89dbbc20ee8d6eb340ab085f8d52b
Author: n0tr1v <n0tr1v@protonmail.com>
Date:   Thu,  9 Mar 2023 09:49:10 -0800

memes

Diffstat:
Acmd/dkf/migrations/129.sql | 10++++++++++
Mpkg/actions/actions.go | 5+++++
Mpkg/config/config.go | 15+++++++++++++++
Mpkg/database/database.go | 4++++
Apkg/database/tableMemes.go | 120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpkg/web/handlers/api/v1/handlers.go | 4++++
Mpkg/web/handlers/api/v1/slashInterceptor.go | 89++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mpkg/web/handlers/handlers.go | 18++++++++++++++++++
Mpkg/web/web.go | 1+
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))