Feature- Bump limit (#9)

* feat: add bump limit check on thread update

* feat: add captcha is active env var
This commit is contained in:
Yanislav Igonin 2022-01-27 19:23:51 +02:00 committed by GitHub
parent 868a7214d2
commit 2bcd0ff8c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 72 additions and 23 deletions

View File

@ -4,3 +4,5 @@ SEED_DB=true
IS_RATE_LIMITER_ENABLED=true IS_RATE_LIMITER_ENABLED=true
THREADS_MAX_COUNT=50 THREADS_MAX_COUNT=50
POSTGRES_URL=postgres://localhost/micrach?pool_max_conns=5 POSTGRES_URL=postgres://localhost/micrach?pool_max_conns=5
THREAD_BUMP_LIMIT=500
IS_CAPTCHA_ACTIVE=true

View File

@ -13,6 +13,8 @@ type AppConfig struct {
SeedDb bool SeedDb bool
IsRateLimiterEnabled bool IsRateLimiterEnabled bool
ThreadsMaxCount int ThreadsMaxCount int
ThreadBumpLimit int
IsCaptchaActive bool
} }
type DbConfig struct { type DbConfig struct {
@ -50,6 +52,8 @@ func getAppConfig() AppConfig {
seedDb := getValueOrDefaultBoolean(os.Getenv("SEED_DB"), false) seedDb := getValueOrDefaultBoolean(os.Getenv("SEED_DB"), false)
isRateLimiterEnabled := getValueOrDefaultBoolean(os.Getenv("IS_RATE_LIMITER_ENABLED"), true) isRateLimiterEnabled := getValueOrDefaultBoolean(os.Getenv("IS_RATE_LIMITER_ENABLED"), true)
threadsMaxCount := getValueOrDefaultInt(os.Getenv("THREADS_MAX_COUNT"), 50) threadsMaxCount := getValueOrDefaultInt(os.Getenv("THREADS_MAX_COUNT"), 50)
threadBumpLimit := getValueOrDefaultInt(os.Getenv("THREAD_BUMP_LIMIT"), 500)
isCaptchaActive := getValueOrDefaultBoolean(os.Getenv("IS_CAPTCHA_ACTIVE"), true)
return AppConfig{ return AppConfig{
Env: env, Env: env,
@ -57,6 +61,8 @@ func getAppConfig() AppConfig {
SeedDb: seedDb, SeedDb: seedDb,
IsRateLimiterEnabled: isRateLimiterEnabled, IsRateLimiterEnabled: isRateLimiterEnabled,
ThreadsMaxCount: threadsMaxCount, ThreadsMaxCount: threadsMaxCount,
ThreadBumpLimit: threadBumpLimit,
IsCaptchaActive: isCaptchaActive,
} }
} }

View File

@ -58,7 +58,8 @@ func GetThreads(c *gin.Context) {
PagesCount: pagesCount, PagesCount: pagesCount,
Page: page, Page: page,
FormData: Repositories.HtmlFormData{ FormData: Repositories.HtmlFormData{
CaptchaID: captchaID, CaptchaID: captchaID,
IsCaptchaActive: Config.App.IsCaptchaActive,
}, },
} }
c.HTML(http.StatusOK, "index.html", htmlData) c.HTML(http.StatusOK, "index.html", htmlData)
@ -87,8 +88,9 @@ func GetThread(c *gin.Context) {
htmlData := Repositories.GetThreadHtmlData{ htmlData := Repositories.GetThreadHtmlData{
Thread: thread, Thread: thread,
FormData: Repositories.HtmlFormData{ FormData: Repositories.HtmlFormData{
FirstPostID: firstPost.ID, FirstPostID: firstPost.ID,
CaptchaID: captchaID, CaptchaID: captchaID,
IsCaptchaActive: Config.App.IsCaptchaActive,
}, },
} }
c.HTML(http.StatusOK, "thread.html", htmlData) c.HTML(http.StatusOK, "thread.html", htmlData)
@ -309,6 +311,11 @@ func UpdateThread(c *gin.Context) {
} }
defer tx.Rollback(context.TODO()) defer tx.Rollback(context.TODO())
if err != nil {
log.Println("error:", err)
c.HTML(http.StatusInternalServerError, "500.html", nil)
return
}
post := Repositories.Post{ post := Repositories.Post{
IsParent: false, IsParent: false,
ParentID: threadID, ParentID: threadID,
@ -323,6 +330,23 @@ func UpdateThread(c *gin.Context) {
return return
} }
postsCountInThread, err := Repositories.Posts.GetThreadPostsCount(threadID)
if err != nil {
log.Println("error:", err)
c.HTML(http.StatusInternalServerError, "500.html", nil)
return
}
isBumpLimit := postsCountInThread >= Config.App.ThreadBumpLimit
isThreadBumped := !isBumpLimit && !isSage && !post.IsParent
if isThreadBumped {
err = Repositories.Posts.BumpThreadInTx(tx, threadID)
if err != nil {
log.Println("error:", err)
c.HTML(http.StatusInternalServerError, "500.html", nil)
return
}
}
for _, fileInRequest := range filesInRequest { for _, fileInRequest := range filesInRequest {
file := Repositories.File{ file := Repositories.File{
PostID: postID, PostID: postID,

View File

@ -208,23 +208,6 @@ func (r *PostsRepository) CreateInTx(tx pgx.Tx, p Post) (int, error) {
return 0, err return 0, err
} }
// updating parent post `updated_at`
if !p.IsParent && !p.IsSage {
sql = `
UPDATE posts
SET updated_at = now()
WHERE id = $1
`
row := tx.QueryRow(context.TODO(), sql, p.ParentID)
var msg string
err = row.Scan(&msg)
// UPDATE always return `no rows`
// so we need to check this condition
if err != nil && err != pgx.ErrNoRows {
return 0, err
}
}
return createdPost.ID, nil return createdPost.ID, nil
} }
@ -263,3 +246,34 @@ func (r *PostsRepository) ArchiveThreadsFrom(t time.Time) error {
_, err := Db.Pool.Exec(context.TODO(), sql, t) _, err := Db.Pool.Exec(context.TODO(), sql, t)
return err return err
} }
// Returns count of posts in thread by thread ID
func (r *PostsRepository) GetThreadPostsCount(id int) (int, error) {
sql := `
SELECT COUNT(*)
FROM posts
WHERE
(id = $1 AND is_parent = true AND is_deleted != true)
OR (parent_id = $1 AND is_deleted != true)
`
row := Db.Pool.QueryRow(context.TODO(), sql, id)
var count int
err := row.Scan(&count)
if err != nil {
return 0, err
}
return count, nil
}
// Updates threads updated at time by thread ID
func (r *PostsRepository) BumpThreadInTx(tx pgx.Tx, id int) error {
sql := `
UPDATE posts
SET updated_at = now()
WHERE id = $1
`
_, err := tx.Query(context.TODO(), sql, id)
return err
}

View File

@ -34,8 +34,9 @@ type File struct {
// post-form.html // post-form.html
type HtmlFormData struct { type HtmlFormData struct {
FirstPostID int FirstPostID int
CaptchaID string CaptchaID string
IsCaptchaActive bool
} }
// thread.html // thread.html

View File

@ -15,11 +15,13 @@
<textarea class="form-control" id="postText" rows="5" placeholder="Text" name="text"></textarea> <textarea class="form-control" id="postText" rows="5" placeholder="Text" name="text"></textarea>
<input class="form-control" type="file" id="postFiles" multiple name="files"> <input class="form-control" type="file" id="postFiles" multiple name="files">
{{ if .IsCaptchaActive }}
<div class="captcha-container text-center"> <div class="captcha-container text-center">
<img src="/captcha/{{ .CaptchaID }}" alt="Captcha"> <img src="/captcha/{{ .CaptchaID }}" alt="Captcha">
</div> </div>
<input class="form-control" type="hidden" name="captchaId" value="{{ .CaptchaID }}"> <input class="form-control" type="hidden" name="captchaId" value="{{ .CaptchaID }}">
<input class="form-control" id="postCaptcha" type="tel" pattern="\d+" placeholder="Captcha" name="captcha"> <input class="form-control" id="postCaptcha" type="tel" pattern="\d+" placeholder="Captcha" name="captcha">
{{ end }}
<div class="row"> <div class="row">
{{ if ne .FirstPostID 0 }} {{ if ne .FirstPostID 0 }}