diff --git a/internal/util/cooldowns.go b/internal/util/cooldowns.go
new file mode 100644
index 0000000..ba78d89
--- /dev/null
+++ b/internal/util/cooldowns.go
@@ -0,0 +1,45 @@
+package util
+
+import (
+ "net"
+ "net/http"
+ "strings"
+ "sync"
+ "time"
+)
+
+var (
+ PostCooldowns = make(map[string]time.Time)
+ ThreadCooldowns = make(map[string]time.Time)
+)
+
+var cooldownMutex sync.Mutex
+
+const POST_COOLDOWN = 15 * time.Second
+const THREAD_COOLDOWN = 2 * time.Minute
+
+func IsOnCooldown(ip string, m map[string]time.Time, duration time.Duration) bool {
+ cooldownMutex.Lock()
+ defer cooldownMutex.Unlock()
+
+ last, exists := m[ip]
+ if !exists || time.Since(last) >= duration {
+ m[ip] = time.Now()
+ return false
+ }
+ return true
+}
+
+func GetIP(r *http.Request) string {
+ if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
+ parts := strings.Split(xff, ",")
+ return strings.TrimSpace(parts[0])
+ }
+
+ // Fallback to RemoteAddr
+ ip, _, err := net.SplitHostPort(r.RemoteAddr)
+ if err != nil {
+ return r.RemoteAddr
+ }
+ return ip
+}
diff --git a/internal/util/posts.go b/internal/util/posts.go
new file mode 100644
index 0000000..6f74d56
--- /dev/null
+++ b/internal/util/posts.go
@@ -0,0 +1,104 @@
+package util
+
+import (
+ "fmt"
+ "html/template"
+ "image"
+ "io"
+ "log"
+ "mime/multipart"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/disintegration/imaging"
+)
+
+// 10 MB memory limit
+const FILE_MEM_LIMIT int64 = 10 << 20
+const POST_IMG_FULL_PATH = "web/static/img/posts/full"
+const POST_IMG_THUMB_PATH = "web/static/img/posts/thumb"
+
+func EnrichPost(body string) string {
+ var b strings.Builder
+ for _, rawLine := range strings.Split(body, "\n") {
+ var outLine string
+
+ for i, rawWord := range strings.Split(rawLine, " ") {
+ var outWord string
+ if strings.HasPrefix(rawWord, ">>") {
+ postId := strings.TrimPrefix(rawWord, ">>")
+ esc := template.HTMLEscapeString(rawWord)
+ outWord = fmt.Sprintf(
+ ``,
+ postId, esc,
+ )
+ } else {
+ outWord = template.HTMLEscapeString(rawWord)
+ }
+
+ if i != 0 {
+ outLine += " "
+ }
+ outLine += outWord
+ }
+
+ if strings.HasPrefix(rawLine, ">") && !strings.HasPrefix(rawLine, ">>") {
+ outLine = `` + outLine + ``
+ }
+
+ b.WriteString(outLine)
+ b.WriteString("
")
+ }
+ return b.String()
+}
+
+func SavePostFile(file *multipart.File, filename string) error {
+ dstPathFull := filepath.Join(POST_IMG_FULL_PATH, filename)
+ dstPathThumb := filepath.Join(POST_IMG_THUMB_PATH, filename)
+
+ // FULL
+ if err := os.MkdirAll(POST_IMG_FULL_PATH, 0755); err != nil {
+ log.Printf("MkdirAll (full): %v", err)
+ return err
+ }
+
+ dstFull, err := os.Create(dstPathFull)
+ if err != nil {
+ log.Printf("os.Create (full): %v", err)
+ return err
+ }
+ defer dstFull.Close()
+ if _, err := io.Copy(dstFull, *file); err != nil {
+ log.Printf("io.Copy (full): %v", err)
+ return err
+ }
+
+ if _, err := (*file).Seek(0, 0); err != nil { // rewind file
+ log.Printf("seek file (thumb): %v", err)
+ return err
+ }
+
+ // THUMBNAIL
+ if err := os.MkdirAll(POST_IMG_THUMB_PATH, 0755); err != nil {
+ log.Printf("MkdirAll (thumb): %v", err)
+ return err
+ }
+
+ img, _, err := image.Decode(*file)
+ if err != nil {
+ log.Printf("image.Decode: %v", err)
+ return err
+ }
+
+ thumb := imaging.Resize(img, 300, 0, imaging.Lanczos)
+ if err = imaging.Save(thumb, dstPathThumb); err != nil {
+ log.Printf("imaging.Save: %v", err)
+ return err
+ }
+
+ return nil
+}
diff --git a/internal/util/util.go b/internal/util/util.go
deleted file mode 100644
index b006a8a..0000000
--- a/internal/util/util.go
+++ /dev/null
@@ -1,44 +0,0 @@
-package util
-
-import (
- "fmt"
- "html/template"
- "strings"
-)
-
-func EnrichPost(body string) string {
- var b strings.Builder
- for _, rawLine := range strings.Split(body, "\n") {
- var outLine string
-
- for i, rawWord := range strings.Split(rawLine, " ") {
- var outWord string
- if strings.HasPrefix(rawWord, ">>") {
- postId := strings.TrimPrefix(rawWord, ">>")
- esc := template.HTMLEscapeString(rawWord)
- outWord = fmt.Sprintf(
- ``,
- postId, esc,
- )
- } else {
- outWord = template.HTMLEscapeString(rawWord)
- }
-
- if i != 0 {
- outLine += " "
- }
- outLine += outWord
- }
-
- if strings.HasPrefix(rawLine, ">") && !strings.HasPrefix(rawLine, ">>") {
- outLine = `` + outLine + ``
- }
-
- b.WriteString(outLine)
- b.WriteString("
")
- }
- return b.String()
-}
diff --git a/web/main.go b/web/main.go
index d593bde..a8210a7 100644
--- a/web/main.go
+++ b/web/main.go
@@ -4,78 +4,22 @@ import (
"database/sql"
"errors"
"fmt"
- "image"
- "io"
"log"
- "mime/multipart"
"net/http"
- "os"
"path/filepath"
"strconv"
"time"
- "github.com/disintegration/imaging"
"github.com/dominicf2001/comfychan/internal/database"
+ "github.com/dominicf2001/comfychan/internal/util"
"github.com/dominicf2001/comfychan/web/views"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
_ "github.com/mattn/go-sqlite3"
)
-// 10 MB memory limit
-const FILE_MEM_LIMIT int64 = 10 << 20
-const POST_IMG_FULL_PATH = "web/static/img/posts/full"
-const POST_IMG_THUMB_PATH = "web/static/img/posts/thumb"
-
var dev = true
-func savePostFile(file *multipart.File, filename string) error {
- dstPathFull := filepath.Join(POST_IMG_FULL_PATH, filename)
- dstPathThumb := filepath.Join(POST_IMG_THUMB_PATH, filename)
-
- // FULL
- if err := os.MkdirAll(POST_IMG_FULL_PATH, 0755); err != nil {
- log.Printf("MkdirAll (full): %v", err)
- return err
- }
-
- dstFull, err := os.Create(dstPathFull)
- if err != nil {
- log.Printf("os.Create (full): %v", err)
- return err
- }
- defer dstFull.Close()
- if _, err := io.Copy(dstFull, *file); err != nil {
- log.Printf("io.Copy (full): %v", err)
- return err
- }
-
- if _, err := (*file).Seek(0, 0); err != nil { // rewind file
- log.Printf("seek file (thumb): %v", err)
- return err
- }
-
- // THUMBNAIL
- if err := os.MkdirAll(POST_IMG_THUMB_PATH, 0755); err != nil {
- log.Printf("MkdirAll (thumb): %v", err)
- return err
- }
-
- img, _, err := image.Decode(*file)
- if err != nil {
- log.Printf("image.Decode: %v", err)
- return err
- }
-
- thumb := imaging.Resize(img, 300, 0, imaging.Lanczos)
- if err = imaging.Save(thumb, dstPathThumb); err != nil {
- log.Printf("imaging.Save: %v", err)
- return err
- }
-
- return nil
-}
-
func disableCacheInDevMode(next http.Handler) http.Handler {
if !dev {
return next
@@ -174,12 +118,18 @@ func main() {
// CREATE THREAD
r.Post("/{slug}/threads", func(w http.ResponseWriter, r *http.Request) {
+ ip := util.GetIP(r)
+ if util.IsOnCooldown(ip, util.ThreadCooldowns, util.THREAD_COOLDOWN) {
+ http.Error(w, "Too many posts at once.", http.StatusTooManyRequests)
+ return
+ }
+
slug := chi.URLParam(r, "slug")
subject := r.FormValue("subject")
body := r.FormValue("body")
- if err := r.ParseMultipartForm(FILE_MEM_LIMIT); err != nil {
+ if err := r.ParseMultipartForm(util.FILE_MEM_LIMIT); err != nil {
http.Error(w, "Failed to parse form", http.StatusBadRequest)
log.Printf("ParseMultipartForm: %v", err)
return
@@ -194,7 +144,7 @@ func main() {
defer file.Close()
filename := strconv.FormatInt(time.Now().UnixNano(), 10) + filepath.Ext(header.Filename)
- if err := savePostFile(&file, filename); err != nil {
+ if err := util.SavePostFile(&file, filename); err != nil {
http.Error(w, "Failed to save file", http.StatusInternalServerError)
log.Printf("savePostFile: %v", err)
return
@@ -209,6 +159,12 @@ func main() {
// CREATE POST
r.Post("/{slug}/threads/{threadId}", func(w http.ResponseWriter, r *http.Request) {
+ ip := util.GetIP(r)
+ if util.IsOnCooldown(ip, util.PostCooldowns, util.POST_COOLDOWN) {
+ http.Error(w, "Too many posts at once.", http.StatusTooManyRequests)
+ return
+ }
+
// slug := chi.URLParam(r, "slug")
// TODO: use relative thread_nums (see issue #1)
threadIdStr := chi.URLParam(r, "threadId")
@@ -221,7 +177,7 @@ func main() {
body := r.FormValue("body")
mediaPath := ""
- if err := r.ParseMultipartForm(FILE_MEM_LIMIT); err != nil {
+ if err := r.ParseMultipartForm(util.FILE_MEM_LIMIT); err != nil {
http.Error(w, "Failed to parse form", http.StatusBadRequest)
log.Printf("ParseMultipartForm: %v", err)
return
@@ -237,7 +193,7 @@ func main() {
} else {
defer file.Close()
filename := strconv.FormatInt(time.Now().UnixNano(), 10) + filepath.Ext(header.Filename)
- if err := savePostFile(&file, filename); err != nil {
+ if err := util.SavePostFile(&file, filename); err != nil {
http.Error(w, "Failed to save file", http.StatusInternalServerError)
log.Printf("savePostFile: %v", err)
return
diff --git a/web/static/index.css b/web/static/index.css
index cd38b85..e4ded78 100644
--- a/web/static/index.css
+++ b/web/static/index.css
@@ -70,7 +70,6 @@ body {
.catalog-preview h1 {
font-size: 14px;
- color: #0F0C5D;
font-weight: bold;
}
@@ -82,6 +81,7 @@ body {
.catalog-preview-link {
text-decoration: none;
cursor: pointer;
+ color: #0F0C5D;
}
.catalog-preview-img {
@@ -251,3 +251,13 @@ body {
.box p {
padding: 10px;
}
+
+
+.warning {
+ background: #FFAAAA;
+ color: #000;
+ border: 1px solid #000;
+ padding: 8px;
+ margin: 5px 0;
+ font-size: 12px;
+}
diff --git a/web/views/board.templ b/web/views/board.templ
index 1038723..8a1ea6f 100644
--- a/web/views/board.templ
+++ b/web/views/board.templ
@@ -8,6 +8,9 @@ import (
)
templ NewThreadForm(board database.Board) {
+