forum.go (15268B)
1 package handlers 2 3 import ( 4 "dkforest/pkg/config" 5 "dkforest/pkg/database" 6 "dkforest/pkg/utils" 7 hutils "dkforest/pkg/web/handlers/utils" 8 "fmt" 9 "github.com/asaskevich/govalidator" 10 "github.com/labstack/echo" 11 "github.com/sirupsen/logrus" 12 "gorm.io/gorm" 13 html2 "html" 14 "net/http" 15 "strings" 16 ) 17 18 func ForumHandler(c echo.Context) error { 19 authUser := c.Get("authUser").(*database.User) 20 db := c.Get("database").(*database.DkfDB) 21 var data forumData 22 data.ForumCategories, _ = db.GetForumCategories() 23 data.ForumThreads, _ = db.GetPublicForumCategoryThreads(authUser.ID, 1) 24 return c.Render(http.StatusOK, "forum", data) 25 } 26 27 func ForumCategoryHandler(c echo.Context) error { 28 authUser := c.Get("authUser").(*database.User) 29 db := c.Get("database").(*database.DkfDB) 30 categorySlug := c.Param("categorySlug") 31 var data forumCategoryData 32 category, err := db.GetForumCategoryBySlug(categorySlug) 33 if err != nil { 34 return c.Redirect(http.StatusFound, "/forum") 35 } 36 data.ForumThreads, _ = db.GetPublicForumCategoryThreads(authUser.ID, category.ID) 37 return c.Render(http.StatusOK, "forum", data) 38 } 39 40 func ForumSearchHandler(c echo.Context) error { 41 db := c.Get("database").(*database.DkfDB) 42 var data forumSearchData 43 data.Search = c.QueryParam("search") 44 data.AuthorFilter = c.QueryParam("author") 45 46 if data.AuthorFilter != "" { 47 if err := db.DB().Raw(`select 48 t.*, 49 u.username as author, 50 u.chat_color as author_chat_color, 51 lu.username as last_msg_author, 52 lu.chat_color as last_msg_chat_color, 53 lu.chat_font as last_msg_chat_font, 54 m.created_at as last_msg_created_at, 55 mmm.replies_count 56 from fts5_forum_threads ft 57 inner join forum_threads t on t.id = ft.id 58 -- Count replies 59 LEFT JOIN (SELECT mm.thread_id, COUNT(mm.id) as replies_count FROM forum_messages mm GROUP BY mm.thread_id) as mmm ON mmm.thread_id = t.id 60 -- Join author user 61 INNER JOIN users u ON u.id = t.user_id 62 -- Find last message for thread 63 LEFT JOIN forum_messages m ON m.thread_id = t.id AND m.id = (SELECT max(id) FROM forum_messages WHERE thread_id = t.id) 64 -- Join last message user 65 INNER JOIN users lu ON lu.id = m.user_id 66 where u.username = ? and t.is_club = 0 order by id desc limit 100`, data.AuthorFilter).Scan(&data.ForumThreads).Error; err != nil { 67 logrus.Error(err) 68 } 69 return c.Render(http.StatusOK, "forum-search", data) 70 } 71 72 if err := db.DB().Raw(`select m.uuid, snippet(fts5_forum_messages,-1, '[', ']', '...', 10) as snippet, t.uuid as thread_uuid, t.name as thread_name, 73 u.username as author, 74 u.chat_color as author_chat_color, 75 u.chat_font as author_chat_font, 76 mm.created_at as created_at 77 from fts5_forum_messages m 78 inner join forum_threads t on t.id = m.thread_id 79 -- Find message 80 LEFT JOIN forum_messages mm ON mm.uuid = m.uuid 81 -- Join author user 82 INNER JOIN users u ON u.id = mm.user_id 83 where fts5_forum_messages match ? and t.is_club = 0 order by rank limit 100`, data.Search).Scan(&data.ForumMessages).Error; err != nil { 84 logrus.Error(err) 85 } 86 87 if err := db.DB().Raw(`select 88 t.*, 89 u.username as author, 90 u.chat_color as author_chat_color, 91 lu.username as last_msg_author, 92 lu.chat_color as last_msg_chat_color, 93 lu.chat_font as last_msg_chat_font, 94 m.created_at as last_msg_created_at, 95 mmm.replies_count 96 from fts5_forum_threads ft 97 inner join forum_threads t on t.id = ft.id 98 -- Count replies 99 LEFT JOIN (SELECT mm.thread_id, COUNT(mm.id) as replies_count FROM forum_messages mm GROUP BY mm.thread_id) as mmm ON mmm.thread_id = t.id 100 -- Join author user 101 INNER JOIN users u ON u.id = t.user_id 102 -- Find last message for thread 103 LEFT JOIN forum_messages m ON m.thread_id = t.id AND m.id = (SELECT max(id) FROM forum_messages WHERE thread_id = t.id) 104 -- Join last message user 105 INNER JOIN users lu ON lu.id = m.user_id 106 where fts5_forum_threads match ? and t.is_club = 0 order by rank limit 100`, data.Search).Scan(&data.ForumThreads).Error; err != nil { 107 logrus.Error(err) 108 } 109 110 return c.Render(http.StatusOK, "forum-search", data) 111 } 112 113 func ThreadEditHandler(c echo.Context) error { 114 db := c.Get("database").(*database.DkfDB) 115 if config.ForumEnabled.IsFalse() { 116 return c.Render(http.StatusOK, "flash", FlashResponse{Message: hutils.ForumDisabledErr.Error(), Redirect: "/", Type: "alert-danger"}) 117 } 118 authUser := c.Get("authUser").(*database.User) 119 threadUUID := database.ForumThreadUUID(c.Param("threadUUID")) 120 if !authUser.CanUseForumFn() { 121 return c.Render(http.StatusOK, "flash", FlashResponse{Message: hutils.AccountTooYoungErr.Error(), Redirect: "/t/" + string(threadUUID), Type: "alert-danger"}) 122 } 123 if !authUser.IsAdmin { 124 return c.Redirect(http.StatusFound, "/t/"+string(threadUUID)) 125 } 126 thread, err := db.GetForumThreadByUUID(threadUUID) 127 if err != nil { 128 return c.Redirect(http.StatusFound, "/t/"+string(threadUUID)) 129 } 130 var data editForumThreadData 131 data.Thread = thread 132 data.Categories, _ = db.GetForumCategories() 133 134 if c.Request().Method == http.MethodPost { 135 thread.CategoryID = database.ForumCategoryID(utils.DoParseInt64(c.Request().PostFormValue("category_id"))) 136 thread.DoSave(db) 137 return c.Redirect(http.StatusFound, "/t/"+string(threadUUID)) 138 } 139 140 return c.Render(http.StatusOK, "thread-edit", data) 141 } 142 143 func ThreadDeleteHandler(c echo.Context) error { 144 if config.ForumEnabled.IsFalse() { 145 return c.Render(http.StatusOK, "flash", FlashResponse{Message: hutils.ForumDisabledErr.Error(), Redirect: "/", Type: "alert-danger"}) 146 } 147 authUser := c.Get("authUser").(*database.User) 148 db := c.Get("database").(*database.DkfDB) 149 threadUUID := database.ForumThreadUUID(c.Param("threadUUID")) 150 if !authUser.CanUseForumFn() { 151 return c.Render(http.StatusOK, "flash", FlashResponse{Message: hutils.AccountTooYoungErr.Error(), Redirect: "/t/" + string(threadUUID), Type: "alert-danger"}) 152 } 153 thread, err := db.GetForumThreadByUUID(threadUUID) 154 if err != nil { 155 return c.Redirect(http.StatusFound, "/forum") 156 } 157 158 if !authUser.IsAdmin { 159 return c.Redirect(http.StatusFound, "/forum") 160 } 161 162 var data deleteForumThreadData 163 data.Thread = thread 164 165 if c.Request().Method == http.MethodPost { 166 if err := db.DeleteForumThreadByID(thread.ID); err != nil { 167 logrus.Error(err) 168 } 169 return c.Redirect(http.StatusFound, "/forum") 170 } 171 172 return c.Render(http.StatusOK, "thread-delete", data) 173 } 174 175 func ThreadReplyHandler(c echo.Context) error { 176 threadUUID := database.ForumThreadUUID(c.Param("threadUUID")) 177 178 if config.ForumEnabled.IsFalse() { 179 return c.Render(http.StatusOK, "flash", FlashResponse{Message: hutils.ForumDisabledErr.Error(), Redirect: "/", Type: "alert-danger"}) 180 } 181 authUser := c.Get("authUser").(*database.User) 182 db := c.Get("database").(*database.DkfDB) 183 if !authUser.CanUseForumFn() { 184 return c.Render(http.StatusOK, "flash", FlashResponse{Message: hutils.AccountTooYoungErr.Error(), Redirect: "/t/" + string(threadUUID), Type: "alert-danger"}) 185 } 186 187 thread, err := db.GetForumThreadByUUID(threadUUID) 188 if err != nil { 189 return c.Redirect(http.StatusFound, "/") 190 } 191 var data threadReplyData 192 data.Thread = thread 193 194 if c.Request().Method == http.MethodPost { 195 data.Message = c.Request().PostFormValue("message") 196 if !govalidator.RuneLength(data.Message, "3", "10000") { 197 data.ErrorMessage = "Message must have at least 3 characters" 198 return c.Render(http.StatusOK, "thread-reply", data) 199 } 200 if isForumSpam(data.Message) { 201 db.NewAudit(*authUser, fmt.Sprintf("spam forum thread reply %s (#%d)", authUser.Username, authUser.ID)) 202 authUser.SetCanUseForum(db, false) 203 return c.Redirect(http.StatusFound, "/") 204 } 205 message := database.MakeForumMessage(data.Message, authUser.ID, thread.ID) 206 message.IsSigned = message.ValidateSignature(authUser.GPGPublicKey) 207 if err := db.DB().Create(&message).Error; err != nil { 208 logrus.Error(err) 209 } 210 // Send notifications 211 subs, _ := db.GetUsersSubscribedToForumThread(thread.ID) 212 for _, sub := range subs { 213 if sub.UserID != authUser.ID { 214 threadName := html2.EscapeString(thread.Name) 215 msg := fmt.Sprintf(`New reply in thread "<a href="/t/%s#%s">%s</a>"`, thread.UUID, message.UUID, threadName) 216 db.CreateNotification(msg, sub.UserID) 217 } 218 } 219 return c.Redirect(http.StatusFound, "/t/"+string(thread.UUID)) 220 } 221 222 return c.Render(http.StatusOK, "thread-reply", data) 223 } 224 225 func ThreadRawMessageHandler(c echo.Context) error { 226 if config.ForumEnabled.IsFalse() { 227 return c.Render(http.StatusOK, "flash", FlashResponse{Message: hutils.ForumDisabledErr.Error(), Redirect: "/", Type: "alert-danger"}) 228 } 229 db := c.Get("database").(*database.DkfDB) 230 messageUUID := database.ForumMessageUUID(c.Param("messageUUID")) 231 msg, err := db.GetForumMessageByUUID(messageUUID) 232 if err != nil { 233 return c.Redirect(http.StatusFound, "/") 234 } 235 return c.String(http.StatusOK, msg.Message) 236 } 237 238 func ThreadEditMessageHandler(c echo.Context) error { 239 if config.ForumEnabled.IsFalse() { 240 return c.Render(http.StatusOK, "flash", FlashResponse{Message: hutils.ForumDisabledErr.Error(), Redirect: "/", Type: "alert-danger"}) 241 } 242 authUser := c.Get("authUser").(*database.User) 243 db := c.Get("database").(*database.DkfDB) 244 threadUUID := database.ForumThreadUUID(c.Param("threadUUID")) 245 if !authUser.CanUseForumFn() { 246 return c.Render(http.StatusOK, "flash", FlashResponse{Message: hutils.AccountTooYoungErr.Error(), Redirect: "/t/" + string(threadUUID), Type: "alert-danger"}) 247 } 248 messageUUID := database.ForumMessageUUID(c.Param("messageUUID")) 249 thread, err := db.GetForumThreadByUUID(threadUUID) 250 if err != nil { 251 return c.Redirect(http.StatusFound, "/") 252 } 253 msg, err := db.GetForumMessageByUUID(messageUUID) 254 if err != nil { 255 return c.Redirect(http.StatusFound, "/") 256 } 257 if msg.UserID != authUser.ID && !authUser.IsAdmin { 258 return c.Redirect(http.StatusFound, "/") 259 } 260 var data threadReplyData 261 data.IsEdit = true 262 data.Thread = thread 263 data.Message = msg.Message 264 265 if c.Request().Method == http.MethodPost { 266 data.Message = c.Request().PostFormValue("message") 267 if !govalidator.RuneLength(data.Message, "3", "20000") { 268 data.ErrorMessage = "Message must have 3 to 20k characters" 269 return c.Render(http.StatusOK, "thread-reply", data) 270 } 271 if isForumSpam(data.Message) { 272 db.NewAudit(*authUser, fmt.Sprintf("spam forum edit msg %s (#%d)", authUser.Username, authUser.ID)) 273 authUser.SetCanUseForum(db, false) 274 return c.Redirect(http.StatusFound, "/") 275 } 276 msg.Message = data.Message 277 msg.IsSigned = msg.ValidateSignature(authUser.GPGPublicKey) 278 msg.DoSave(db) 279 return c.Redirect(http.StatusFound, "/t/"+string(thread.UUID)) 280 } 281 282 return c.Render(http.StatusOK, "thread-reply", data) 283 } 284 285 func isForumSpam(msg string) bool { 286 if strings.Contains(strings.ToLower(msg), "profjerry") || 287 strings.Contains(strings.ToLower(msg), "dolcerinamarin") || 288 strings.Contains(strings.ToLower(msg), "autorization.online") { 289 return true 290 } 291 return false 292 } 293 294 func ThreadDeleteMessageHandler(c echo.Context) error { 295 if config.ForumEnabled.IsFalse() { 296 return c.Render(http.StatusOK, "flash", FlashResponse{Message: hutils.ForumDisabledErr.Error(), Redirect: "/", Type: "alert-danger"}) 297 } 298 threadUUID := database.ForumThreadUUID(c.Param("threadUUID")) 299 authUser := c.Get("authUser").(*database.User) 300 db := c.Get("database").(*database.DkfDB) 301 if !authUser.CanUseForumFn() { 302 return c.Render(http.StatusOK, "flash", FlashResponse{Message: hutils.AccountTooYoungErr.Error(), Redirect: "/t/" + string(threadUUID), Type: "alert-danger"}) 303 } 304 messageUUID := database.ForumMessageUUID(c.Param("messageUUID")) 305 msg, err := db.GetForumMessageByUUID(messageUUID) 306 if err != nil { 307 return c.Redirect(http.StatusFound, "/t/"+string(threadUUID)) 308 } 309 310 if authUser.ID != msg.UserID && !authUser.IsAdmin { 311 return c.Redirect(http.StatusFound, "/t/"+string(threadUUID)) 312 } 313 314 if !msg.CanEdit() && !authUser.IsAdmin { 315 return c.Redirect(http.StatusFound, "/t/"+string(threadUUID)) 316 } 317 318 var data deleteForumMessageData 319 data.Thread, err = db.GetForumThreadByID(msg.ThreadID) 320 if err != nil { 321 return c.Redirect(http.StatusFound, "/t/"+string(threadUUID)) 322 } 323 data.Message = msg 324 325 if c.Request().Method == http.MethodPost { 326 if err := db.DeleteForumMessageByID(msg.ID); err != nil { 327 logrus.Error(err) 328 } 329 return c.Redirect(http.StatusFound, "/t/"+string(data.Thread.UUID)) 330 } 331 332 return c.Render(http.StatusOK, "thread-message-delete", data) 333 } 334 335 func NewThreadHandler(c echo.Context) error { 336 if config.ForumEnabled.IsFalse() { 337 return c.Render(http.StatusOK, "flash", FlashResponse{Message: hutils.ForumDisabledErr.Error(), Redirect: "/", Type: "alert-danger"}) 338 } 339 authUser := c.Get("authUser").(*database.User) 340 db := c.Get("database").(*database.DkfDB) 341 if !authUser.CanUseForumFn() { 342 return c.Render(http.StatusOK, "flash", FlashResponse{Message: hutils.AccountTooYoungErr.Error(), Redirect: "/forum", Type: "alert-danger"}) 343 } 344 var data newThreadData 345 346 if c.Request().Method == http.MethodPost { 347 data.ThreadName = c.Request().PostFormValue("thread_name") 348 data.Message = c.Request().PostFormValue("message") 349 if !govalidator.RuneLength(data.ThreadName, "3", "255") { 350 data.ErrorThreadName = "Thread name must have 3-255 characters" 351 return c.Render(http.StatusOK, "new-thread", data) 352 } 353 if !govalidator.RuneLength(data.Message, "3", "20000") { 354 data.ErrorMessage = "Thread message must have at least 3-20000 characters" 355 return c.Render(http.StatusOK, "new-thread", data) 356 } 357 if isForumSpam(data.Message) { 358 db.NewAudit(*authUser, fmt.Sprintf("spam forum new thread %s (#%d)", authUser.Username, authUser.ID)) 359 authUser.SetCanUseForum(db, false) 360 return c.Redirect(http.StatusFound, "/") 361 } 362 thread := database.MakeForumThread(data.ThreadName, authUser.ID, 1) 363 db.DB().Create(&thread) 364 message := database.MakeForumMessage(data.Message, authUser.ID, thread.ID) 365 message.IsSigned = message.ValidateSignature(authUser.GPGPublicKey) 366 db.DB().Create(&message) 367 _ = db.SubscribeToForumThread(authUser.ID, thread.ID) 368 return c.Redirect(http.StatusFound, "/t/"+string(thread.UUID)) 369 } 370 371 return c.Render(http.StatusOK, "new-thread", data) 372 } 373 374 func ThreadHandler(c echo.Context) error { 375 authUser := c.Get("authUser").(*database.User) 376 db := c.Get("database").(*database.DkfDB) 377 threadUUID := database.ForumThreadUUID(c.Param("threadUUID")) 378 thread, err := db.GetForumThreadByUUID(threadUUID) 379 if err != nil { 380 return c.Redirect(http.StatusFound, "/") 381 } 382 var data threadData 383 data.Thread = thread 384 385 if err := db.DB(). 386 Table("forum_messages"). 387 Scopes(func(query *gorm.DB) *gorm.DB { 388 query.Where("thread_id = ?", thread.ID) 389 data.CurrentPage, data.MaxPage, data.MessagesCount, query = NewPaginator().Paginate(c, query) 390 return query 391 }). 392 Order("id ASC"). 393 Preload("User"). 394 Find(&data.Messages).Error; err != nil { 395 logrus.Error(err) 396 } 397 398 if authUser != nil { 399 data.IsSubscribed = db.IsUserSubscribedToForumThread(authUser.ID, thread.ID) 400 // Update read record 401 db.UpdateForumReadRecord(authUser.ID, thread.ID) 402 } 403 404 return c.Render(http.StatusOK, "thread", data) 405 } 406 407 func ForumReindexHandler(c echo.Context) error { 408 db := c.Get("database").(*database.DkfDB) 409 if err := db.DB().Exec(`INSERT INTO fts5_forum_threads(fts5_forum_threads) VALUES('rebuild')`).Error; err != nil { 410 logrus.Error(err) 411 } 412 if err := db.DB().Exec(`INSERT INTO fts5_forum_messages(fts5_forum_messages) VALUES('rebuild')`).Error; err != nil { 413 logrus.Error(err) 414 } 415 return c.Redirect(http.StatusFound, "/forum") 416 }