package controllers import ( "errors" "log" "math" "path/filepath" "strconv" "strings" "github.com/dchest/captcha" "github.com/gofiber/fiber/v2" "github.com/jackc/pgx/v5" "micrach/config" "micrach/db" "micrach/repositories" "micrach/utils" ) func GetThreads(c *fiber.Ctx) error { ctx := c.UserContext() pageString := c.Query("page", "1") page, err := strconv.Atoi(pageString) if err != nil { return c.Status(fiber.StatusNotFound).Render("pages/404", nil) } if page <= 0 { return c.Status(fiber.StatusNotFound).Render("pages/404", nil) } limit := 10 offset := limit * (page - 1) threads, err := repositories.Posts.Get(ctx, limit, offset) if err != nil { log.Println("error:", err) return c.Status(fiber.StatusInternalServerError).Render("pages/500", nil) } count, err := repositories.Posts.GetThreadsCount(ctx) if err != nil { log.Println("error:", err) return c.Status(fiber.StatusInternalServerError).Render("pages/500", nil) } pagesCount := int(math.Ceil(float64(count) / 10)) if page > pagesCount && count != 0 { return c.Status(fiber.StatusNotFound).Render("pages/404", nil) } captchaID := captcha.New() htmlData := repositories.GetThreadsHtmlData{ Threads: threads, Pagination: repositories.HtmlPaginationData{ PagesCount: pagesCount, Page: page, }, FormData: repositories.HtmlFormData{ CaptchaID: captchaID, IsCaptchaActive: config.App.IsCaptchaActive, }, } return c.Render("pages/index", htmlData) } func GetThread(c *fiber.Ctx) error { ctx := c.UserContext() threadID, err := c.ParamsInt("threadID") if err != nil { return c.Status(fiber.StatusNotFound).Render("pages/404", nil) } thread, err := repositories.Posts.GetThreadByPostID(ctx, threadID) if err != nil { log.Println("error:", err) return c.Status(fiber.StatusInternalServerError).Render("pages/500", nil) } if thread == nil { return c.Status(fiber.StatusNotFound).Render("pages/404", nil) } firstPost := thread[0] captchaID := captcha.New() htmlData := repositories.GetThreadHtmlData{ Thread: thread, FormData: repositories.HtmlFormData{ FirstPostID: firstPost.ID, CaptchaID: captchaID, IsCaptchaActive: config.App.IsCaptchaActive, }, } return c.Render("pages/thread", htmlData) } func CreateThread(c *fiber.Ctx) error { ctx := c.UserContext() form, err := c.MultipartForm() if err != nil { log.Println("error:", err) return c.Status(fiber.StatusInternalServerError).Render("pages/500", nil) } // TODO: dat shit crashes if no fields in request text := form.Value["text"][0] title := form.Value["title"][0] filesInRequest := form.File["files"] validationErrorMessage := utils.ValidatePost(title, text, filesInRequest) if validationErrorMessage != "" { errorHtmlData := repositories.BadRequestHtmlData{ Message: validationErrorMessage, } return c.Status(fiber.StatusBadRequest).Render("pages/400", errorHtmlData) } if config.App.IsCaptchaActive { captchaID := form.Value["captchaId"][0] captchaString := form.Value["captcha"][0] isCaptchaValid := captcha.VerifyString(captchaID, captchaString) if !isCaptchaValid { errorHtmlData := repositories.BadRequestHtmlData{ Message: repositories.InvalidCaptchaErrorMessage, } return c.Status(fiber.StatusBadRequest).Render("pages/400", errorHtmlData) } } conn, err := db.Pool.Acquire(ctx) if err != nil { log.Println("error:", err) return c.Status(fiber.StatusInternalServerError).Render("pages/500", nil) } defer conn.Release() threadsCount, err := repositories.Posts.GetThreadsCount(ctx) if err != nil { log.Println("error:", err) return c.Status(fiber.StatusInternalServerError).Render("pages/500", nil) } if threadsCount >= config.App.ThreadsMaxCount { oldestThreadUpdatedAt, err := repositories.Posts.GetOldestThreadUpdatedAt(ctx) if err != nil { log.Println("error:", err) return c.Status(fiber.StatusInternalServerError).Render("pages/500", nil) } err = repositories.Posts.ArchiveThreadsFrom(ctx, oldestThreadUpdatedAt) if err != nil { log.Println("error:", err) return c.Status(fiber.StatusInternalServerError).Render("pages/500", nil) } } tx, err := conn.Begin(ctx) if err != nil { log.Println("error:", err) return c.Status(fiber.StatusInternalServerError).Render("pages/500", nil) } defer func() { rollbackErr := tx.Rollback(ctx) if rollbackErr != nil && !errors.Is(rollbackErr, pgx.ErrTxClosed) { log.Println("error:", rollbackErr) } }() post := repositories.Post{ IsParent: true, Title: title, Text: text, IsSage: false, } threadID, err := repositories.Posts.CreateInTx(ctx, tx, post) if err != nil { log.Println("error:", err) return c.Status(fiber.StatusInternalServerError).Render("pages/500", nil) } err = utils.CreateThreadFolder(threadID) if err != nil { log.Println("error:", err) return c.Status(fiber.StatusInternalServerError).Render("pages/500", nil) } for _, fileInRequest := range filesInRequest { file := repositories.File{ PostID: threadID, Name: fileInRequest.Filename, // image/jpeg -> jpeg Ext: strings.Split(fileInRequest.Header["Content-Type"][0], "/")[1], Size: int(fileInRequest.Size), } fileID, err := repositories.Files.CreateInTx(ctx, tx, file) if err != nil { log.Println("error:", err) return c.Status(fiber.StatusInternalServerError).Render("pages/500", nil) } path := filepath.Join( utils.UPLOADS_DIR_PATH, strconv.Itoa(threadID), "o", strconv.Itoa(fileID)+"."+file.Ext, ) err = c.SaveFile(fileInRequest, path) if err != nil { log.Println("error:", err) return c.Status(fiber.StatusInternalServerError).Render("pages/500", nil) } // creating thumbnail thumbImg, err := utils.MakeImageThumbnail(path, file.Ext, threadID, fileID) if err != nil { log.Println("error:", err) return c.Status(fiber.StatusInternalServerError).Render("pages/500", nil) } // saving thumbnail err = utils.SaveImageThumbnail(thumbImg, threadID, fileID, file.Ext) if err != nil { log.Println("error:", err) return c.Status(fiber.StatusInternalServerError).Render("pages/500", nil) } } if err := tx.Commit(ctx); err != nil { log.Println("error:", err) return c.Status(fiber.StatusInternalServerError).Render("pages/500", nil) } path := "/" + strconv.Itoa(threadID) return c.Redirect(path, fiber.StatusFound) } // Add new post in thread func UpdateThread(c *fiber.Ctx) error { ctx := c.UserContext() threadID, err := c.ParamsInt("threadID") if err != nil { return c.Status(fiber.StatusNotFound).Render("pages/404", nil) } isArchived, err := repositories.Posts.GetIfThreadIsArchived(ctx, threadID) if isArchived { errorHtmlData := repositories.BadRequestHtmlData{ Message: repositories.ThreadIsArchivedErrorMessage, } return c.Status(fiber.StatusBadRequest).Render("pages/400", errorHtmlData) } if err != nil { log.Println("error:", err) return c.Status(fiber.StatusInternalServerError).Render("pages/500", nil) } form, err := c.MultipartForm() if err != nil { log.Println("error:", err) return c.Status(fiber.StatusInternalServerError).Render("pages/500", nil) } // TODO: dat shit crashes if no fields in request text := form.Value["text"][0] filesInRequest := form.File["files"] validationErrorMessage := utils.ValidatePost("", text, filesInRequest) if validationErrorMessage != "" { errorHtmlData := repositories.BadRequestHtmlData{ Message: validationErrorMessage, } return c.Status(fiber.StatusBadRequest).Render("pages/400", errorHtmlData) } if config.App.IsCaptchaActive { captchaID := form.Value["captchaId"][0] captchaString := form.Value["captcha"][0] isCaptchaValid := captcha.VerifyString(captchaID, captchaString) if !isCaptchaValid { errorHtmlData := repositories.BadRequestHtmlData{ Message: repositories.InvalidCaptchaErrorMessage, } return c.Status(fiber.StatusBadRequest).Render("pages/400", errorHtmlData) } } isSageField := form.Value["sage"] var isSageString string if len(isSageField) != 0 { isSageString = isSageField[0] } isSage := isSageString == "on" conn, err := db.Pool.Acquire(ctx) if err != nil { log.Println("error:", err) return c.Status(fiber.StatusInternalServerError).Render("pages/500", nil) } defer conn.Release() tx, err := conn.Begin(ctx) if err != nil { log.Println("error:", err) return c.Status(fiber.StatusInternalServerError).Render("pages/500", nil) } defer func() { rollbackErr := tx.Rollback(ctx) if rollbackErr != nil && !errors.Is(rollbackErr, pgx.ErrTxClosed) { log.Println("error:", rollbackErr) } }() post := repositories.Post{ IsParent: false, ParentID: &threadID, Title: "", Text: text, IsSage: isSage, } postID, err := repositories.Posts.CreateInTx(ctx, tx, post) if err != nil { log.Println("error:", err) return c.Status(fiber.StatusInternalServerError).Render("pages/500", nil) } postsCountInThread, err := repositories.Posts.GetThreadPostsCount(ctx, threadID) if err != nil { log.Println("error:", err) return c.Status(fiber.StatusInternalServerError).Render("pages/500", nil) } isBumpLimit := postsCountInThread >= config.App.ThreadBumpLimit isThreadBumped := !isBumpLimit && !isSage && !post.IsParent if isThreadBumped { err = repositories.Posts.BumpThreadInTx(ctx, tx, threadID) if err != nil { log.Println("error:", err) return c.Status(fiber.StatusInternalServerError).Render("pages/500", nil) } } for _, fileInRequest := range filesInRequest { file := repositories.File{ PostID: postID, Name: fileInRequest.Filename, // image/jpeg -> jpeg Ext: strings.Split(fileInRequest.Header["Content-Type"][0], "/")[1], Size: int(fileInRequest.Size), } fileID, err := repositories.Files.CreateInTx(ctx, tx, file) if err != nil { log.Println("error:", err) return c.Status(fiber.StatusInternalServerError).Render("pages/500", nil) } path := filepath.Join( utils.UPLOADS_DIR_PATH, strconv.Itoa(threadID), "o", strconv.Itoa(fileID)+"."+file.Ext, ) err = c.SaveFile(fileInRequest, path) if err != nil { log.Println("error:", err) return c.Status(fiber.StatusInternalServerError).Render("pages/500", nil) } // creating thumbnail thumbImg, err := utils.MakeImageThumbnail(path, file.Ext, threadID, fileID) if err != nil { log.Println("error:", err) return c.Status(fiber.StatusInternalServerError).Render("pages/500", nil) } // saving thumbnail err = utils.SaveImageThumbnail(thumbImg, threadID, fileID, file.Ext) if err != nil { log.Println("error:", err) return c.Status(fiber.StatusInternalServerError).Render("pages/500", nil) } } if err := tx.Commit(ctx); err != nil { log.Println("error:", err) return c.Status(fiber.StatusInternalServerError).Render("pages/500", nil) } path := "/" + strconv.Itoa(threadID) + "#" + strconv.Itoa(postID) return c.Redirect(path) }