dkforest

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

chat.go (13091B)


      1 package handlers
      2 
      3 import (
      4 	"dkforest/pkg/captcha"
      5 	"dkforest/pkg/config"
      6 	"dkforest/pkg/database"
      7 	dutils "dkforest/pkg/database/utils"
      8 	"dkforest/pkg/hashset"
      9 	"dkforest/pkg/managers"
     10 	"dkforest/pkg/utils"
     11 	hutils "dkforest/pkg/web/handlers/utils"
     12 	"github.com/PuerkitoBio/goquery"
     13 	"github.com/asaskevich/govalidator"
     14 	"github.com/labstack/echo"
     15 	"github.com/sirupsen/logrus"
     16 	"gorm.io/gorm"
     17 	"net/http"
     18 	"strconv"
     19 	"strings"
     20 	"time"
     21 )
     22 
     23 func RedRoomHandler(c echo.Context) error {
     24 	return chatHandler(c, true, false)
     25 }
     26 
     27 func ChatHandler(c echo.Context) error {
     28 	return chatHandler(c, false, false)
     29 }
     30 
     31 func ChatStreamHandler(c echo.Context) error {
     32 	return chatHandler(c, false, true)
     33 }
     34 
     35 func chatHandler(c echo.Context, redRoom, stream bool) error {
     36 	const chatPasswordTmplName = "standalone.chat-password"
     37 
     38 	// WARNING: in this handler, "authUser" can be null.
     39 	authUser := c.Get("authUser").(*database.User)
     40 
     41 	db := c.Get("database").(*database.DkfDB)
     42 
     43 	if !stream && authUser != nil {
     44 		stream = authUser.UseStream
     45 	}
     46 
     47 	var data chatData
     48 	data.PowEnabled = config.PowEnabled.Load()
     49 	data.RedRoom = redRoom
     50 	preventRefresh := utils.DoParseBool(c.QueryParam("r"))
     51 
     52 	v := c.QueryParams()
     53 	if preventRefresh {
     54 		v.Set("r", "1")
     55 	}
     56 	if _, found := c.QueryParams()["ml"]; found {
     57 		v.Set("ml", "1")
     58 		data.Multiline = true
     59 	}
     60 	data.ChatQueryParams = "?" + v.Encode()
     61 
     62 	if authUser == nil {
     63 		if config.SignupEnabled.IsFalse() {
     64 			return c.Render(http.StatusOK, "flash", FlashResponse{Message: "New signup are temporarily disabled", Redirect: "/", Type: "alert-danger"})
     65 		}
     66 
     67 		data.CaptchaID, data.CaptchaImg = captcha.New()
     68 	}
     69 
     70 	room, err := db.GetChatRoomByName(getRoomName(c))
     71 	if err != nil {
     72 		return c.Redirect(http.StatusFound, "/")
     73 	}
     74 	data.Room = room
     75 
     76 	if authUser != nil {
     77 		managers.ActiveUsers.UpdateUserInRoom(room, managers.NewUserInfo(authUser))
     78 
     79 		// We display tutorial on official or public rooms
     80 		data.DisplayTutorial = (room.IsOfficialRoom() || (room.IsListed && !room.IsProtected())) && !authUser.TutorialCompleted()
     81 
     82 		if data.DisplayTutorial {
     83 			data.TutoSecs = getTutorialStepDuration()
     84 			data.TutoFrames = generateCssFrames(data.TutoSecs, nil, true)
     85 			if c.Request().Method == http.MethodGet {
     86 				authUser.SetChatTutorialTime(db, time.Now())
     87 			}
     88 		}
     89 	}
     90 
     91 	if c.Request().Method == http.MethodPost {
     92 		return handlePost(db, c, data, authUser)
     93 	}
     94 
     95 	// If you don't have access to the room (room is protected and user is nil or no cookie with the password)
     96 	// We display the page to enter room password.
     97 	if hasAccess, _ := room.HasAccess(c); !hasAccess {
     98 		if !room.IsProtected() && room.Mode == database.UserWhitelistRoomMode {
     99 			return c.Render(http.StatusOK, "standalone.chat-whitelist", data)
    100 		}
    101 		return c.Render(http.StatusOK, chatPasswordTmplName, data)
    102 	}
    103 
    104 	data.IsSubscribed = db.IsUserSubscribedToRoom(authUser.ID, room.ID)
    105 	data.IsOfficialRoom = room.IsOfficialRoom()
    106 	data.IsStream = stream
    107 	return c.Render(http.StatusOK, "chat", data)
    108 }
    109 
    110 func getRoomName(c echo.Context) string {
    111 	roomName := c.Param("roomName")
    112 	if roomName == "" {
    113 		roomName = "general"
    114 	}
    115 	return roomName
    116 }
    117 
    118 func handlePost(db *database.DkfDB, c echo.Context, data chatData, authUser *database.User) error {
    119 	formName := c.Request().PostFormValue("formName")
    120 	switch formName {
    121 	case "logout":
    122 		return handleLogoutPost(c, data.Room)
    123 	case "tutorialP1", "tutorialP2", "tutorialP3":
    124 		return handleTutorialPost(db, c, data, authUser)
    125 	case "chat-password":
    126 		return handleChatPasswordPost(db, c, data, authUser)
    127 	}
    128 	return hutils.RedirectReferer(c)
    129 }
    130 
    131 // Logout of a protected room (delete room cookies)
    132 func handleLogoutPost(c echo.Context, room database.ChatRoom) error {
    133 	hutils.DeleteRoomCookie(c, int64(room.ID))
    134 	return c.Redirect(http.StatusFound, "/chat")
    135 }
    136 
    137 func handleTutorialPost(db *database.DkfDB, c echo.Context, data chatData, authUser *database.User) error {
    138 	if authUser.ChatTutorial < 3 && time.Since(authUser.ChatTutorialTime) >= time.Duration(data.TutoSecs)*time.Second {
    139 		authUser.IncrChatTutorial(db)
    140 	}
    141 	return hutils.RedirectReferer(c)
    142 }
    143 
    144 // Handle POST requests for chat-password, when someone tries to authenticate in a protected room providing a password.
    145 func handleChatPasswordPost(db *database.DkfDB, c echo.Context, data chatData, authUser *database.User) error {
    146 	const chatPasswordTmplName = "standalone.chat-password"
    147 	data.RoomPassword = c.Request().PostFormValue("password")
    148 
    149 	// If no user set, we verify the captcha and username for the guest account
    150 	if authUser == nil {
    151 		data.GuestUsername = c.Request().PostFormValue("guest_username")
    152 		data.Pow = c.Request().PostFormValue("pow")
    153 		captchaID := c.Request().PostFormValue("captcha_id")
    154 		captchaInput := c.Request().PostFormValue("captcha")
    155 		if err := hutils.CaptchaVerifyString(c, captchaID, captchaInput); err != nil {
    156 			data.ErrCaptcha = err.Error()
    157 			return c.Render(http.StatusOK, chatPasswordTmplName, data)
    158 		}
    159 
    160 		if err := db.CanUseUsername(database.Username(data.GuestUsername), false); err != nil {
    161 			data.ErrGuestUsername = err.Error()
    162 			return c.Render(http.StatusOK, chatPasswordTmplName, data)
    163 		}
    164 
    165 		// verify POW
    166 		if config.PowEnabled.IsTrue() {
    167 			if !hutils.VerifyPow(data.GuestUsername, data.Pow, config.PowDifficulty) {
    168 				data.ErrPow = "invalid proof of work"
    169 				return c.Render(http.StatusOK, chatPasswordTmplName, data)
    170 			}
    171 		}
    172 	}
    173 
    174 	// Verify room password is correct
    175 	key := database.GetRoomDecryptionKey(data.RoomPassword)
    176 	hashedPassword := database.GetRoomPasswordHash(data.RoomPassword)
    177 	if !data.Room.VerifyPasswordHash(hashedPassword) {
    178 		data.Error = "Invalid room password"
    179 		return c.Render(http.StatusOK, chatPasswordTmplName, data)
    180 	}
    181 
    182 	// If no user set, create the guest account + session
    183 	// TODO: maybe add "_guest" suffix to guest accounts?
    184 	if authUser == nil {
    185 		password := utils.GenerateToken32()
    186 		newUser, errs := db.CreateGuestUser(data.GuestUsername, password)
    187 		if errs.HasError() {
    188 			data.ErrGuestUsername = errs.Username
    189 			return c.Render(http.StatusOK, chatPasswordTmplName, data)
    190 		}
    191 
    192 		session := db.DoCreateSession(newUser.ID, c.Request().UserAgent(), time.Hour*24)
    193 		c.SetCookie(createSessionCookie(session.Token, time.Hour*24))
    194 	}
    195 
    196 	hutils.CreateRoomCookie(c, int64(data.Room.ID), hashedPassword, key)
    197 	return c.Redirect(http.StatusFound, "/chat/"+data.Room.Name)
    198 }
    199 
    200 func ChatArchiveHandler(c echo.Context) error {
    201 	authUser := c.Get("authUser").(*database.User)
    202 	db := c.Get("database").(*database.DkfDB)
    203 	var data chatArchiveData
    204 	data.DateFormat = authUser.GetDateFormat()
    205 	roomName := c.Param("roomName")
    206 
    207 	room, roomKey, err := dutils.GetRoomAndKey(db, c, roomName)
    208 	if err != nil {
    209 		return c.Redirect(http.StatusFound, "/chat")
    210 	}
    211 
    212 	data.UUID = c.QueryParam("uuid")
    213 	data.Room = room
    214 
    215 	if data.UUID != "" {
    216 		msg, err := db.GetRoomChatMessageByUUID(room.ID, data.UUID)
    217 		if err != nil {
    218 			return c.Redirect(http.StatusFound, "/")
    219 		}
    220 		nbMsg := 150
    221 		args := []any{room.ID, authUser.ID, authUser.ID}
    222 		whereClause := `room_id = ? AND group_id IS NULL AND (to_user_id is null OR to_user_id = ? OR user_id = ?)`
    223 		if !authUser.DisplayIgnored {
    224 			args = append(args, authUser.ID)
    225 			whereClause += ` AND user_id NOT IN (SELECT ignored_user_id FROM ignored_users WHERE user_id = ?)`
    226 		}
    227 		raw := `
    228 	SELECT * FROM (
    229 		SELECT *
    230 		FROM chat_messages
    231 		WHERE ` + whereClause + `
    232 		  AND id >= ?
    233 		ORDER BY id ASC
    234 		LIMIT ?
    235 	)
    236 	UNION
    237 	SELECT * FROM (
    238 		SELECT *
    239 		FROM chat_messages
    240 		WHERE ` + whereClause + `
    241 		  AND id < ?
    242 		ORDER BY id DESC
    243 		LIMIT ?
    244 	) ORDER BY id DESC`
    245 		args = append(args, msg.ID, nbMsg)
    246 		args = append(args, args...)
    247 		db.DB().Raw(raw, args...).Scan(&data.Messages)
    248 
    249 		// Manually do Preload("Room")
    250 		for _, m := range data.Messages {
    251 			m.Room = data.Room
    252 		}
    253 
    254 		//--- < Manually do a Preload("User") Preload("ToUser") > ---
    255 		usersIDs := hashset.New[database.UserID]()
    256 		for _, m := range data.Messages {
    257 			usersIDs.Insert(m.UserID)
    258 			if m.ToUserID != nil {
    259 				usersIDs.Insert(*m.ToUserID)
    260 			}
    261 		}
    262 		users, _ := db.GetUsersByID(usersIDs.ToArray())
    263 		usersMap := make(map[database.UserID]database.User)
    264 		for _, u := range users {
    265 			usersMap[u.ID] = u
    266 		}
    267 		for i, m := range data.Messages {
    268 			if u, ok := usersMap[m.UserID]; ok {
    269 				data.Messages[i].User = u
    270 			}
    271 			if m.ToUserID != nil {
    272 				if u, ok := usersMap[*m.ToUserID]; ok {
    273 					data.Messages[i].ToUser = &u
    274 				}
    275 			}
    276 		}
    277 		//--- </ Manually do a Preload("User") Preload("ToUser") > ---
    278 
    279 	} else {
    280 		if err := db.DB().Table("chat_messages").
    281 			Where("room_id = ? AND group_id IS NULL AND (to_user_id is null OR to_user_id = ? OR user_id = ?)", room.ID, authUser.ID, authUser.ID).
    282 			Scopes(func(query *gorm.DB) *gorm.DB {
    283 				if !authUser.DisplayIgnored {
    284 					query = query.Where(`user_id NOT IN (SELECT ignored_user_id FROM ignored_users WHERE user_id = ?)`, authUser.ID)
    285 				}
    286 				data.CurrentPage, data.MaxPage, data.MessagesCount, query = NewPaginator().SetResultPerPage(300).Paginate(c, query)
    287 				return query
    288 			}).
    289 			Order("id DESC").
    290 			Preload("Room").
    291 			Preload("User").
    292 			Preload("ToUser").
    293 			Find(&data.Messages).Error; err != nil {
    294 			logrus.Error(err)
    295 		}
    296 	}
    297 
    298 	if roomKey != "" {
    299 		if err := data.Messages.DecryptAll(roomKey); err != nil {
    300 			return c.NoContent(http.StatusInternalServerError)
    301 		}
    302 	}
    303 
    304 	return c.Render(http.StatusOK, "chat-archive", data)
    305 }
    306 
    307 func ChatDeleteHandler(c echo.Context) error {
    308 	authUser := c.Get("authUser").(*database.User)
    309 	db := c.Get("database").(*database.DkfDB)
    310 	var data chatDeleteData
    311 	roomName := c.Param("roomName")
    312 	room, err := db.GetChatRoomByName(roomName)
    313 	if err != nil {
    314 		return c.Redirect(http.StatusFound, "/")
    315 	}
    316 	if !room.IsRoomOwner(authUser.ID) {
    317 		return c.Redirect(http.StatusFound, "/")
    318 	}
    319 	data.Room = room
    320 
    321 	if c.Request().Method == http.MethodPost {
    322 		if room.IsProtected() {
    323 			hutils.DeleteRoomCookie(c, int64(room.ID))
    324 		}
    325 		db.DeleteChatRoomByID(room.ID)
    326 		return c.Redirect(http.StatusFound, "/chat")
    327 	}
    328 
    329 	return c.Render(http.StatusOK, "chat-delete", data)
    330 }
    331 
    332 func RoomChatSettingsHandler(c echo.Context) error {
    333 	authUser := c.Get("authUser").(*database.User)
    334 	db := c.Get("database").(*database.DkfDB)
    335 	var data roomChatSettingsData
    336 	roomName := c.Param("roomName")
    337 	room, err := db.GetChatRoomByName(roomName)
    338 	if err != nil {
    339 		return c.Redirect(http.StatusFound, "/")
    340 	}
    341 	if !room.IsRoomOwner(authUser.ID) {
    342 		return c.Redirect(http.StatusFound, "/")
    343 	}
    344 	data.Room = room
    345 
    346 	if c.Request().Method == http.MethodPost {
    347 		return c.Redirect(http.StatusFound, "/chat")
    348 	}
    349 
    350 	return c.Render(http.StatusOK, "chat-room-settings", data)
    351 }
    352 
    353 func ChatCreateRoomHandler(c echo.Context) error {
    354 	authUser := c.Get("authUser").(*database.User)
    355 	db := c.Get("database").(*database.DkfDB)
    356 	var data chatCreateRoomData
    357 	data.CaptchaID, data.CaptchaImg = captcha.New()
    358 	data.IsEphemeral = true
    359 	if c.Request().Method == http.MethodPost {
    360 		data.RoomName = c.Request().PostFormValue("room_name")
    361 		data.Password = c.Request().PostFormValue("password")
    362 		data.IsListed = utils.DoParseBool(c.Request().PostFormValue("is_listed"))
    363 		data.IsEphemeral = utils.DoParseBool(c.Request().PostFormValue("is_ephemeral"))
    364 		if !govalidator.Matches(data.RoomName, "^[a-zA-Z0-9_]{3,50}$") {
    365 			data.ErrorRoomName = "invalid room name"
    366 			return c.Render(http.StatusOK, "chat-create-room", data)
    367 		}
    368 		captchaID := c.Request().PostFormValue("captcha_id")
    369 		captchaInput := c.Request().PostFormValue("captcha")
    370 		if err := hutils.CaptchaVerifyString(c, captchaID, captchaInput); err != nil {
    371 			data.ErrCaptcha = err.Error()
    372 			return c.Render(http.StatusOK, "chat-create-room", data)
    373 		}
    374 		passwordHash := ""
    375 		if data.Password != "" {
    376 			passwordHash = database.GetRoomPasswordHash(data.Password)
    377 		}
    378 		if _, err := db.CreateRoom(data.RoomName, passwordHash, authUser.ID, data.IsListed); err != nil {
    379 			data.Error = err.Error()
    380 			return c.Render(http.StatusOK, "chat-create-room", data)
    381 		}
    382 		return c.Redirect(http.StatusFound, "/chat/"+data.RoomName)
    383 	}
    384 	return c.Render(http.StatusOK, "chat-create-room", data)
    385 }
    386 
    387 func ChatCodeHandler(c echo.Context) error {
    388 	messageUUID := c.Param("messageUUID")
    389 	idx, err := strconv.Atoi(c.Param("idx"))
    390 	if err != nil {
    391 		return c.Redirect(http.StatusFound, "/")
    392 	}
    393 
    394 	authUser := c.Get("authUser").(*database.User)
    395 	db := c.Get("database").(*database.DkfDB)
    396 	msg, err := db.GetChatMessageByUUID(messageUUID)
    397 	if err != nil {
    398 		return c.Redirect(http.StatusFound, "/")
    399 	}
    400 
    401 	if !dutils.VerifyMsgAuth(db, &msg, authUser.ID, authUser.IsModerator()) {
    402 		return c.Redirect(http.StatusFound, "/")
    403 	}
    404 
    405 	doc, err := goquery.NewDocumentFromReader(strings.NewReader(msg.Message))
    406 	if err != nil {
    407 		return c.Redirect(http.StatusFound, "/")
    408 	}
    409 	n := doc.Find("pre").Eq(idx)
    410 	if n == nil {
    411 		return c.Redirect(http.StatusFound, "/")
    412 	}
    413 
    414 	var data chatCodepData
    415 	data.Code, err = n.Html()
    416 	if err != nil {
    417 		return c.Redirect(http.StatusFound, "/")
    418 	}
    419 	return c.Render(http.StatusOK, "chat-code", data)
    420 }
    421 
    422 func ChatHelpHandler(c echo.Context) error {
    423 	var data chatHelpData
    424 	return c.Render(http.StatusOK, "chat-help", data)
    425 }