dkforest

A forum and chat platform (onion)
git clone https://git.dasho.dev/n0tr1v/dkforest.git
Log | Files | Refs | LICENSE

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 &quot;<a href="/t/%s#%s">%s</a>&quot;`, 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 }