mirror of
				https://github.com/yanislav-igonin/micrach
				synced 2025-11-04 10:27:02 +03:00 
			
		
		
		
	Feature- Bump limit (#9)
* feat: add bump limit check on thread update * feat: add captcha is active env var
This commit is contained in:
		
							parent
							
								
									868a7214d2
								
							
						
					
					
						commit
						2bcd0ff8c3
					
				@ -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
 | 
				
			||||||
@ -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,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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,
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
@ -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 }}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user