handlers.go (18193B)
1 package v1 2 3 import ( 4 "dkforest/pkg/LeChatPHP/captcha" 5 mycaptcha "dkforest/pkg/captcha" 6 "dkforest/pkg/config" 7 "dkforest/pkg/database" 8 dutils "dkforest/pkg/database/utils" 9 "dkforest/pkg/global" 10 "dkforest/pkg/managers" 11 "dkforest/pkg/utils" 12 "dkforest/pkg/web/handlers/interceptors" 13 "dkforest/pkg/web/handlers/interceptors/command" 14 hutils "dkforest/pkg/web/handlers/utils" 15 "errors" 16 "fmt" 17 "github.com/labstack/echo" 18 "github.com/sirupsen/logrus" 19 qt "github.com/valyala/quicktemplate" 20 "io" 21 "net/http" 22 "strings" 23 "time" 24 ) 25 26 // GetChatMenuData gets the data needed to render the "right-menu" in a chat room. 27 // We have it separate because we have one endpoint that only render the right menu (for "stream" chat). 28 // and one endpoint to render both messages and menu ("non-stream" chat). 29 func GetChatMenuData(c echo.Context, room database.ChatRoom) ChatMenuData { 30 db := c.Get("database").(*database.DkfDB) 31 authUser := c.Get("authUser").(*database.User) 32 33 data := ChatMenuData{} 34 data.PreventRefresh = utils.DoParseBool(c.QueryParam("r")) 35 sessionToken := "" 36 authCookie, _ := c.Cookie(hutils.AuthCookieName) 37 if authCookie != nil { 38 sessionToken = authCookie.Value 39 } 40 data.InboxCount = global.GetUserNotificationCount(db, authUser.ID, sessionToken) 41 data.OfficialRooms, _ = db.GetOfficialChatRooms1(authUser.ID) 42 data.SubscribedRooms, _ = db.GetUserRoomSubscriptions(authUser.ID) 43 44 membersInRoom, membersInChat := managers.ActiveUsers.GetRoomUsers(room, managers.GetUserIgnoreSet(db, authUser)) 45 data.Members = membersInRoom 46 data.MembersInChat = membersInChat 47 for _, user := range membersInChat { 48 if !user.IsHellbanned { 49 data.VisibleMemberInChat = true 50 break 51 } 52 } 53 data.RoomName = room.Name 54 55 if _, found := c.QueryParams()["ml"]; found { 56 data.TopBarQueryParams = "&ml=1" 57 } 58 59 return data 60 } 61 62 func chatMessages(c echo.Context) (status int, data ChatMessagesData) { 63 authUser := c.Get("authUser").(*database.User) 64 db := c.Get("database").(*database.DkfDB) 65 roomName := c.Param("roomName") 66 67 pmOnlyQuery := dutils.DoParsePmDisplayMode(c.QueryParam("pmonly")) 68 mentionsOnlyQuery := utils.DoParseBool(c.QueryParam("mentionsOnly")) 69 pmUserID := dutils.GetUserIDFromUsername(db, c.QueryParam(command.RedirectPmUsernameQP)) 70 71 room, roomKey, err := dutils.GetRoomAndKey(db, c, roomName) 72 if err != nil { 73 return http.StatusForbidden, data 74 } 75 76 managers.ActiveUsers.UpdateUserInRoom(room, managers.NewUserInfo(authUser)) 77 78 displayHellbanned := authUser.DisplayHellbanned || authUser.IsHellbanned 79 displayIgnoredMessages := utils.False() 80 msgs, err := db.GetChatMessages(room.ID, roomKey, authUser.Username, authUser.ID, pmUserID, pmOnlyQuery, mentionsOnlyQuery, 81 displayHellbanned, authUser.DisplayIgnored, authUser.DisplayModerators, displayIgnoredMessages, 150, 0) 82 if err != nil { 83 return http.StatusInternalServerError, data 84 } 85 86 // Update read record 87 db.UpdateChatReadRecord(authUser.ID, room.ID) 88 89 data.Error = c.QueryParam("error") 90 if data.Error != "" { 91 errorDisplayTime := int64(4) // Time in seconds 92 nowUnix := time.Now().Unix() 93 data.ErrorTs = utils.DoParseInt64(c.QueryParam("errorTs")) 94 if nowUnix > data.ErrorTs+errorDisplayTime { 95 data.Error = "" 96 } 97 } 98 99 // If your tutorial was reset (you are not a new user), force display manual refresh popup 100 if ((room.IsOfficialRoom() || (room.IsListed && !room.IsProtected())) && !authUser.TutorialCompleted()) && 101 authUser.GeneralMessagesCount > 0 { 102 data.ForceManualRefresh = true 103 } 104 105 data.ManualRefreshTimeout = authUser.RefreshRate + 25 106 data.Messages = msgs 107 data.ReadMarker, _ = db.GetUserReadMarker(authUser.ID, room.ID) 108 data.NbButtons = authUser.CountUIButtons() 109 if authUser.NotifyNewMessage || authUser.NotifyPmmed || authUser.NotifyTagged { 110 lastKnownDate := time.Now() 111 if out, err := db.GetUserLastKnownMessage(authUser.ID, room.ID); err == nil { 112 lastKnownDate = out.LastKnownTimestamp 113 } 114 newMessageSound, pmSound, taggedSound, lastMessageCreatedAt := shouldPlaySound(authUser, lastKnownDate, msgs) 115 utils.LogErr(db.CreateUserLastKnownMessage(authUser.ID, room.ID, lastMessageCreatedAt)) 116 data.NewMessageSound = utils.TernaryOrZero(authUser.NotifyNewMessage, newMessageSound) 117 data.PmSound = utils.TernaryOrZero(authUser.NotifyPmmed, pmSound) 118 data.TaggedSound = utils.TernaryOrZero(authUser.NotifyTagged, taggedSound) 119 } 120 121 data.ChatMenuData = GetChatMenuData(c, room) 122 123 return http.StatusOK, data 124 } 125 126 func writeunesc(iow io.Writer, s string) { 127 qw := qt.AcquireWriter(iow) 128 streamunesc(qw, s) 129 qt.ReleaseWriter(qw) 130 } 131 132 func unesc(s string) string { 133 qb := qt.AcquireByteBuffer() 134 writeunesc(qb, s) 135 qs := string(qb.B) 136 qt.ReleaseByteBuffer(qb) 137 return qs 138 } 139 140 func streamunesc(qw *qt.Writer, s string) { 141 qw.N().S(s) 142 } 143 144 // ChatMessagesHandler room messages iframe handler 145 // The chat messages iframe use this endpoint to get the messages for a room. 146 func ChatMessagesHandler(c echo.Context) error { 147 authUser := c.Get("authUser").(*database.User) 148 status, data := chatMessages(c) 149 if status != http.StatusOK { 150 return c.NoContent(status) 151 } 152 if c.QueryParams().Has("json") { 153 authUser := c.Get("authUser").(*database.User) 154 if authUser.IsHellbanned || (!authUser.IsModerator() && !authUser.CanSeeHellbanned) { 155 for i := range data.Messages { 156 data.Messages[i].IsHellbanned = false 157 } 158 } 159 return c.JSON(http.StatusOK, data) 160 } 161 version := config.Global.AppVersion.Get().Original() 162 csrf, _ := c.Get("csrf").(string) 163 msgs := Messages(version, csrf, config.NullUsername, authUser, data) 164 return c.HTML(http.StatusOK, msgs) 165 //return c.Render(http.StatusOK, "chat-messages", data) 166 } 167 168 func RoomNotifierHandler(c echo.Context) error { 169 authUser := c.Get("authUser").(*database.User) 170 db := c.Get("database").(*database.DkfDB) 171 roomName := c.Param("roomName") 172 lastKnownDate := c.Request().PostFormValue("last_known_date") 173 174 room, roomKey, err := dutils.GetRoomAndKey(db, c, roomName) 175 if err != nil { 176 return c.NoContent(http.StatusForbidden) 177 } 178 179 managers.ActiveUsers.UpdateUserInRoom(room, managers.NewUserInfo(authUser)) 180 181 displayHellbanned := authUser.DisplayHellbanned || authUser.IsHellbanned 182 mentionsOnly := utils.False() 183 displayIgnoredMessages := utils.False() 184 var pmUserID *database.UserID 185 msgs, err := db.GetChatMessages(room.ID, roomKey, authUser.Username, authUser.ID, pmUserID, database.PmNoFilter, mentionsOnly, 186 displayHellbanned, authUser.DisplayIgnored, authUser.DisplayModerators, displayIgnoredMessages, 150, 0) 187 if err != nil { 188 return c.NoContent(http.StatusInternalServerError) 189 } 190 191 var data testData 192 lastKnownMsgDate, _ := time.Parse(time.RFC3339Nano, lastKnownDate) 193 var lastMessageCreatedAt time.Time 194 data.NewMessageSound, data.PmSound, data.TaggedSound, lastMessageCreatedAt = shouldPlaySound(authUser, lastKnownMsgDate, msgs) 195 data.LastMessageCreatedAt = lastMessageCreatedAt.Format(time.RFC3339Nano) 196 data.InboxCount = db.GetUserInboxMessagesCount(authUser.ID) 197 198 return c.JSON(http.StatusOK, data) 199 } 200 201 // Given a "lastKnownDate" and a list of messages, returns what sound notification should be played. 202 func shouldPlaySound(authUser *database.User, lastKnownMsgDate time.Time, msgs []database.ChatMessage) (newMessageSound, pmSound, taggedSound bool, lastMsgCreatedAt time.Time) { 203 if len(msgs) > 0 { 204 for _, msg := range msgs { 205 lastKnownDateTrunc := lastKnownMsgDate.Truncate(time.Second) 206 createdAtTrunc := msg.CreatedAt.Truncate(time.Second) 207 if createdAtTrunc.After(lastKnownDateTrunc) { 208 if msg.User.ID != authUser.ID { 209 newMessageSound = true 210 if strings.Contains(msg.Message, authUser.Username.AtStr()) { 211 taggedSound = true 212 } 213 if msg.IsPmRecipient(authUser.ID) { 214 pmSound = true 215 } 216 break 217 } 218 } else if createdAtTrunc.Before(lastKnownMsgDate) { 219 break 220 } 221 } 222 lastMsg := msgs[0] 223 lastMsgCreatedAt = lastMsg.CreatedAt 224 } 225 return newMessageSound, pmSound, taggedSound, lastMsgCreatedAt 226 } 227 228 func UserHellbanHandler(c echo.Context) error { 229 authUser := c.Get("authUser").(*database.User) 230 db := c.Get("database").(*database.DkfDB) 231 userID := dutils.DoParseUserID(c.Param("userID")) 232 user, err := db.GetUserByID(userID) 233 if err != nil { 234 return hutils.RedirectReferer(c) 235 } 236 if !user.IsHellbanned { 237 if authUser.IsAdmin || !user.IsModerator() { 238 db.NewAudit(*authUser, fmt.Sprintf("hellban %s #%d", user.Username, user.ID)) 239 user.HellBan(db) 240 managers.ActiveUsers.UpdateUserHBInRooms(managers.NewUserInfo(&user)) 241 } 242 } 243 return hutils.RedirectReferer(c) 244 } 245 246 func UserUnHellbanHandler(c echo.Context) error { 247 authUser := c.Get("authUser").(*database.User) 248 db := c.Get("database").(*database.DkfDB) 249 userID := dutils.DoParseUserID(c.Param("userID")) 250 user, err := db.GetUserByID(userID) 251 if err != nil { 252 return hutils.RedirectReferer(c) 253 } 254 if user.IsHellbanned { 255 db.NewAudit(*authUser, fmt.Sprintf("unhellban %s #%d", user.Username, user.ID)) 256 user.UnHellBan(db) 257 managers.ActiveUsers.UpdateUserHBInRooms(managers.NewUserInfo(&user)) 258 } 259 return hutils.RedirectReferer(c) 260 } 261 262 func KickHandler(c echo.Context) error { 263 authUser := c.Get("authUser").(*database.User) 264 db := c.Get("database").(*database.DkfDB) 265 userID := dutils.DoParseUserID(c.Param("userID")) 266 user, err := db.GetUserByID(userID) 267 if err != nil { 268 return hutils.RedirectReferer(c) 269 } 270 if user.IsModerator() { 271 return hutils.RedirectReferer(c) 272 } 273 _ = dutils.SilentKick(db, user, *authUser) 274 return hutils.RedirectReferer(c) 275 } 276 277 func SubscribeHandler(c echo.Context) error { 278 authUser := c.Get("authUser").(*database.User) 279 db := c.Get("database").(*database.DkfDB) 280 roomName := c.Param("roomName") 281 room, err := db.GetChatRoomByName(roomName) 282 if err != nil { 283 return hutils.RedirectReferer(c) 284 } 285 _ = db.SubscribeToRoom(authUser.ID, room.ID) 286 return hutils.RedirectReferer(c) 287 } 288 289 func UnsubscribeHandler(c echo.Context) error { 290 authUser := c.Get("authUser").(*database.User) 291 db := c.Get("database").(*database.DkfDB) 292 roomName := c.Param("roomName") 293 room, err := db.GetChatRoomByName(roomName) 294 if err != nil { 295 return hutils.RedirectReferer(c) 296 } 297 _ = db.UnsubscribeFromRoom(authUser.ID, room.ID) 298 return hutils.RedirectReferer(c) 299 } 300 301 func ThreadSubscribeHandler(c echo.Context) error { 302 authUser := c.Get("authUser").(*database.User) 303 db := c.Get("database").(*database.DkfDB) 304 threadUUID := database.ForumThreadUUID(c.Param("threadUUID")) 305 thread, err := db.GetForumThreadByUUID(threadUUID) 306 if err != nil { 307 return hutils.RedirectReferer(c) 308 } 309 _ = db.SubscribeToForumThread(authUser.ID, thread.ID) 310 return hutils.RedirectReferer(c) 311 } 312 313 func ThreadUnsubscribeHandler(c echo.Context) error { 314 authUser := c.Get("authUser").(*database.User) 315 db := c.Get("database").(*database.DkfDB) 316 threadUUID := database.ForumThreadUUID(c.Param("threadUUID")) 317 thread, err := db.GetForumThreadByUUID(threadUUID) 318 if err != nil { 319 return hutils.RedirectReferer(c) 320 } 321 _ = db.UnsubscribeFromForumThread(authUser.ID, thread.ID) 322 return hutils.RedirectReferer(c) 323 } 324 325 func ChatMessageReactionHandler(c echo.Context) error { 326 authUser := c.Get("authUser").(*database.User) 327 db := c.Get("database").(*database.DkfDB) 328 messageUUID := c.Request().PostFormValue("message_uuid") 329 var msg database.ChatMessage 330 if err := db.DB().Where("uuid = ?", messageUUID).Preload("User").Preload("Room").First(&msg).Error; err != nil { 331 return err 332 } 333 reaction := utils.DoParseInt64(c.Request().PostFormValue("reaction_id")) 334 if reaction < 0 || reaction > 2 { 335 return errors.New("invalid reaction") 336 } 337 338 if err := db.CreateChatReaction(authUser.ID, msg.ID, reaction); err != nil { 339 _ = db.DeleteReaction(authUser.ID, msg.ID, reaction) 340 } 341 342 return hutils.RedirectReferer(c) 343 } 344 345 func ChatDeleteMessageHandler(c echo.Context) error { 346 authUser := c.Get("authUser").(*database.User) 347 db := c.Get("database").(*database.DkfDB) 348 349 var returnResp error 350 if c.Request().Method == http.MethodGet { 351 returnResp = c.NoContent(http.StatusOK) 352 } else { 353 returnResp = hutils.RedirectReferer(c) 354 } 355 356 messageUUID := c.Param("messageUUID") 357 var msg database.ChatMessage 358 if err := db.DB().Where("uuid = ?", messageUUID). 359 Preload("User"). 360 Preload("Room"). 361 First(&msg).Error; err != nil { 362 return returnResp 363 } 364 365 if err := msg.UserCanDeleteErr(authUser); err != nil { 366 logrus.Error(err) 367 return returnResp 368 } 369 370 // Audit when moderator/admin deletes a message he doesn't own 371 if authUser.IsModerator() && !msg.OwnMessage(authUser.ID) && msg.User.Username != config.NullUsername { 372 auditMsg := fmt.Sprintf(`deleted msg #%d from user "%s" #%d -> %s`, 373 msg.ID, 374 msg.User.Username, 375 msg.User.ID, 376 utils.TruncStr(msg.RawMessage, 75, "…")) 377 db.NewAudit(*authUser, auditMsg) 378 } 379 380 if msg.OwnMessage(authUser.ID) && msg.RoomID == config.GeneralRoomID && !msg.IsPm() { 381 authUser.DecrGeneralMessagesCount(db) 382 } 383 384 if err := msg.Delete(db); err != nil { 385 logrus.Error(err) 386 } 387 return returnResp 388 } 389 390 func ClubDeleteMessageHandler(c echo.Context) error { 391 authUser := c.Get("authUser").(*database.User) 392 db := c.Get("database").(*database.DkfDB) 393 messageID := database.ForumMessageID(utils.DoParseInt64(c.Param("messageID"))) 394 msg, err := db.GetForumMessage(messageID) 395 if err != nil { 396 return hutils.RedirectReferer(c) 397 } 398 399 if authUser.ID != msg.UserID && !authUser.IsAdmin { 400 return hutils.RedirectReferer(c) 401 } 402 403 if !msg.CanEdit() && !authUser.IsAdmin { 404 return hutils.RedirectReferer(c) 405 } 406 407 if err := db.DeleteForumMessageByID(messageID); err != nil { 408 logrus.Error(err) 409 } 410 return hutils.RedirectReferer(c) 411 } 412 413 func DeleteNotificationHandler(c echo.Context) error { 414 authUser := c.Get("authUser").(*database.User) 415 db := c.Get("database").(*database.DkfDB) 416 notificationID := utils.DoParseInt64(c.Param("notificationID")) 417 var msg database.Notification 418 if err := db.DB().Where("ID = ? AND user_id = ?", notificationID, authUser.ID).First(&msg).Error; err != nil { 419 logrus.Error(err) 420 return hutils.RedirectReferer(c) 421 } 422 if err := db.DeleteNotificationByID(notificationID); err != nil { 423 logrus.Error(err) 424 } 425 return c.Redirect(http.StatusFound, "/settings/inbox") 426 } 427 428 func DeleteSessionNotificationHandler(c echo.Context) error { 429 db := c.Get("database").(*database.DkfDB) 430 authCookie, _ := c.Cookie(hutils.AuthCookieName) 431 sessionNotificationID := utils.DoParseInt64(c.Param("sessionNotificationID")) 432 var msg database.SessionNotification 433 if err := db.DB().Where("ID = ? AND session_token = ?", sessionNotificationID, authCookie.Value).First(&msg).Error; err != nil { 434 logrus.Error(err) 435 return hutils.RedirectReferer(c) 436 } 437 if err := db.DeleteSessionNotificationByID(sessionNotificationID); err != nil { 438 logrus.Error(err) 439 } 440 return c.Redirect(http.StatusFound, "/settings/inbox") 441 } 442 443 func ChatInboxDeleteMessageHandler(c echo.Context) error { 444 authUser := c.Get("authUser").(*database.User) 445 db := c.Get("database").(*database.DkfDB) 446 messageID := utils.DoParseInt64(c.Param("messageID")) 447 var msg database.ChatInboxMessage 448 if err := db.DB().Where("ID = ? AND to_user_id = ?", messageID, authUser.ID).First(&msg).Error; err != nil { 449 logrus.Error(err) 450 return c.Redirect(http.StatusFound, "/settings/inbox") 451 } 452 if err := db.DeleteChatInboxMessageByID(messageID); err != nil { 453 logrus.Error(err) 454 } 455 return c.Redirect(http.StatusFound, "/settings/inbox") 456 } 457 458 func ChatInboxDeleteAllMessageHandler(c echo.Context) error { 459 authCookie, _ := c.Cookie(hutils.AuthCookieName) 460 authUser := c.Get("authUser").(*database.User) 461 db := c.Get("database").(*database.DkfDB) 462 if err := db.DeleteAllChatInbox(authUser.ID); err != nil { 463 logrus.Error(err) 464 } 465 if err := db.DeleteAllNotifications(authUser.ID); err != nil { 466 logrus.Error(err) 467 } 468 if err := db.DeleteAllSessionNotifications(authCookie.Value); err != nil { 469 logrus.Error(err) 470 } 471 return c.Redirect(http.StatusFound, "/settings/inbox") 472 } 473 474 func GetCaptchaHandler(c echo.Context) error { 475 //authUser := c.Get("authUser").(*database.User) 476 captchaID, captchaImg := mycaptcha.New() 477 return c.JSON(http.StatusOK, map[string]any{"ID": captchaID, "img": captchaImg}) 478 } 479 480 func CaptchaSolverHandler(c echo.Context) error { 481 authUser := c.Get("authUser").(*database.User) 482 db := c.Get("database").(*database.DkfDB) 483 captchaB64 := c.Request().PostFormValue("captcha") 484 answer, err := captcha.SolveBase64(captchaB64) 485 if err != nil { 486 logrus.Error(err.Error()) 487 return c.NoContent(http.StatusInternalServerError) 488 } 489 captchaReq := database.CaptchaRequest{ 490 UserID: authUser.ID, 491 CaptchaImg: captchaB64, 492 Answer: answer, 493 } 494 if err := db.DB().Create(&captchaReq).Error; err != nil { 495 logrus.Error(err.Error()) 496 } 497 return c.JSON(http.StatusOK, map[string]any{"answer": answer}) 498 } 499 500 func WerewolfHandler(c echo.Context) error { 501 db := c.Get("database").(*database.DkfDB) 502 roomName := "werewolf" 503 origMessage := c.Request().PostFormValue("message") 504 redirectURL := "/api/v1/chat/messages/" + roomName 505 room, roomKey, err := dutils.GetRoomAndKey(db, c, roomName) 506 if err != nil { 507 return c.Redirect(http.StatusFound, redirectURL+"?error="+err.Error()+"&errorTs="+utils.FormatInt64(time.Now().Unix())) 508 } 509 cmd := command.NewCommand(c, origMessage, room, roomKey) 510 interceptors.WWInstance.InterceptMsg(cmd) 511 if cmd.Err != nil { 512 return c.Redirect(http.StatusFound, redirectURL+"?error="+cmd.Err.Error()+"&errorTs="+utils.FormatInt64(time.Now().Unix())) 513 } 514 return c.Redirect(http.StatusFound, redirectURL) 515 } 516 517 func BattleshipHandler(c echo.Context) error { 518 authUser := c.Get("authUser").(*database.User) 519 db := c.Get("database").(*database.DkfDB) 520 roomName := c.Request().PostFormValue("room") 521 enemyUsername := database.Username(c.Request().PostFormValue("enemyUsername")) 522 pos := c.Request().PostFormValue("move") 523 redirectURL := "/api/v1/chat/messages/" + roomName 524 room, roomKey, err := dutils.GetRoomAndKey(db, c, roomName) 525 if err != nil { 526 return c.Redirect(http.StatusFound, redirectURL+"?error="+err.Error()+"&errorTs="+utils.FormatInt64(time.Now().Unix())) 527 } 528 if err = interceptors.BattleshipInstance.PlayMove(roomName, room.ID, roomKey, *authUser, enemyUsername, pos); err != nil { 529 return c.Redirect(http.StatusFound, redirectURL+"?error="+err.Error()+"&errorTs="+utils.FormatInt64(time.Now().Unix())) 530 } 531 return c.Redirect(http.StatusFound, redirectURL) 532 }