dkforest

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

poker.go (22048B)


      1 package handlers
      2 
      3 import (
      4 	"bytes"
      5 	"dkforest/pkg/cache"
      6 	"dkforest/pkg/config"
      7 	"dkforest/pkg/database"
      8 	dutils "dkforest/pkg/database/utils"
      9 	"dkforest/pkg/pubsub"
     10 	"dkforest/pkg/utils"
     11 	"dkforest/pkg/web/handlers/poker"
     12 	hutils "dkforest/pkg/web/handlers/utils"
     13 	"dkforest/pkg/web/handlers/utils/stream"
     14 	"encoding/base64"
     15 	"errors"
     16 	"fmt"
     17 	"github.com/asaskevich/govalidator"
     18 	"github.com/labstack/echo"
     19 	wallet1 "github.com/monero-ecosystem/go-monero-rpc-client/wallet"
     20 	"github.com/sirupsen/logrus"
     21 	"github.com/teris-io/shortid"
     22 	"image"
     23 	"image/png"
     24 	"math/rand"
     25 	"net/http"
     26 	"strings"
     27 	"time"
     28 )
     29 
     30 var pokerWithdrawCache = cache.NewWithKey[database.UserID, int64](10*time.Minute, time.Hour)
     31 
     32 func PokerHomeHandler(c echo.Context) error {
     33 	db := c.Get("database").(*database.DkfDB)
     34 	authUser := c.Get("authUser").(*database.User)
     35 	getImgStr := func(img image.Image) string {
     36 		buf := bytes.NewBuffer([]byte(""))
     37 		_ = png.Encode(buf, img)
     38 		return base64.StdEncoding.EncodeToString(buf.Bytes())
     39 	}
     40 	if authUser.PokerXmrSubAddress == "" {
     41 		if resp, err := config.Xmr().CreateAddress(&wallet1.RequestCreateAddress{}); err == nil {
     42 			authUser.SetPokerXmrSubAddress(db, resp.Address)
     43 		}
     44 	}
     45 	const minWithdrawAmount = 1
     46 	var data pokerData
     47 	data.RakeBackPct = poker.RakeBackPct * 100
     48 	data.XmrPrice = fmt.Sprintf("$%.2f", config.MoneroPrice.Load())
     49 	data.Transactions, _ = db.GetUserPokerXmrTransactions(authUser.ID)
     50 	data.PokerXmrSubAddress = authUser.PokerXmrSubAddress
     51 	data.RakeBack = authUser.PokerRakeBack
     52 	data.ChipsTest = authUser.ChipsTest
     53 	data.XmrBalance = authUser.XmrBalance
     54 	withdrawUnique := rand.Int63()
     55 	data.WithdrawUnique = withdrawUnique
     56 	withdrawUniqueOrig, _ := pokerWithdrawCache.Get(authUser.ID)
     57 	pokerWithdrawCache.SetD(authUser.ID, withdrawUnique)
     58 	pokerTables, _ := db.GetPokerTables()
     59 	pxmr := database.Piconero(0)
     60 	data.HelperXmr = pxmr.XmrStr()
     61 	data.HelperChips = pxmr.ToPokerChip()
     62 	data.HelperpXmr = pxmr.RawString()
     63 	data.HelperUsd = pxmr.UsdStr()
     64 	userTableAccounts, _ := db.GetPokerTableAccounts(authUser.ID)
     65 	for _, t := range pokerTables {
     66 		var nbSeated int
     67 		if g := poker.PokerInstance.GetGame(poker.RoomID(t.Slug)); g != nil {
     68 			nbSeated = g.CountSeated()
     69 		}
     70 		tableBalance := database.PokerChip(0)
     71 		for _, a := range userTableAccounts {
     72 			if a.PokerTableID == t.ID {
     73 				tableBalance = a.Amount
     74 				break
     75 			}
     76 		}
     77 		data.Tables = append(data.Tables, TmpTable{PokerTable: t, NbSeated: nbSeated, TableBalance: tableBalance})
     78 	}
     79 
     80 	if authUser.PokerXmrSubAddress != "" {
     81 		b, _ := authUser.GetImage()
     82 		data.Img = getImgStr(b)
     83 	}
     84 
     85 	if c.Request().Method == http.MethodGet {
     86 		return c.Render(http.StatusOK, "poker", data)
     87 	}
     88 
     89 	formName := c.Request().PostFormValue("form_name")
     90 	if formName == "helper" {
     91 		data.HelperAmount = c.Request().PostFormValue("amount")
     92 		data.HelperType = c.Request().PostFormValue("type")
     93 		switch data.HelperType {
     94 		case "usd":
     95 			amount := utils.DoParseF64(data.HelperAmount)
     96 			pxmr = database.Piconero(amount / config.MoneroPrice.Load() * 1_000_000_000_000)
     97 		case "xmr":
     98 			amount := utils.DoParseF64(data.HelperAmount)
     99 			pxmr = database.Piconero(amount * 1_000_000_000_000)
    100 		case "pxmr":
    101 			amount := utils.DoParseUint64(data.HelperAmount)
    102 			pxmr = database.Piconero(amount)
    103 		case "chips":
    104 			amount := utils.DoParseUint64(data.HelperAmount)
    105 			chips := database.PokerChip(amount)
    106 			pxmr = chips.ToPiconero()
    107 		}
    108 		data.HelperXmr = pxmr.XmrStr()
    109 		data.HelperChips = pxmr.ToPokerChip()
    110 		data.HelperpXmr = pxmr.RawString()
    111 		data.HelperUsd = pxmr.UsdStr()
    112 		return c.Render(http.StatusOK, "poker", data)
    113 	}
    114 
    115 	if formName == "join_table" {
    116 		pokerTableSlug := c.Request().PostFormValue("table_slug")
    117 		playerBuyIn := database.PokerChip(utils.DoParseUint64(c.Request().PostFormValue("buy_in")))
    118 		if err := doJoinTable(db, pokerTableSlug, playerBuyIn, authUser.ID); err != nil {
    119 			data.ErrorTable = err.Error()
    120 			return c.Render(http.StatusOK, "poker", data)
    121 		}
    122 		return c.Redirect(http.StatusFound, "/poker/"+pokerTableSlug)
    123 
    124 	} else if formName == "cash_out" {
    125 		pokerTableSlug := c.Request().PostFormValue("table_slug")
    126 		if err := doCashOut(db, pokerTableSlug, authUser.ID); err != nil {
    127 			data.ErrorTable = err.Error()
    128 			return c.Render(http.StatusOK, "poker", data)
    129 		}
    130 		return c.Redirect(http.StatusFound, "/poker")
    131 
    132 	} else if formName == "reset_chips" {
    133 		authUser.ResetChipsTest(db)
    134 		return hutils.RedirectReferer(c)
    135 
    136 	} else if formName == "claim_rake_back" {
    137 		if err := db.ClaimRakeBack(authUser.ID); err != nil {
    138 			logrus.Error(err)
    139 		}
    140 		return hutils.RedirectReferer(c)
    141 	}
    142 
    143 	if config.PokerWithdrawEnabled.IsFalse() {
    144 		data.Error = "withdraw temporarily disabled"
    145 		return c.Render(http.StatusOK, "poker", data)
    146 	}
    147 
    148 	withdrawAmount := database.Piconero(utils.DoParseUint64(c.Request().PostFormValue("withdraw_amount")))
    149 	data.WithdrawAmount = withdrawAmount
    150 	data.WithdrawAddress = c.Request().PostFormValue("withdraw_address")
    151 	withdrawUniqueSub := utils.DoParseInt64(c.Request().PostFormValue("withdraw_unique"))
    152 
    153 	if withdrawUniqueOrig == 0 || withdrawUniqueSub != withdrawUniqueOrig {
    154 		data.Error = "form submitted twice, try again"
    155 		return c.Render(http.StatusOK, "poker", data)
    156 	}
    157 	if len(data.WithdrawAddress) != 95 {
    158 		data.Error = "invalid xmr address"
    159 		return c.Render(http.StatusOK, "poker", data)
    160 	}
    161 	if !govalidator.Matches(data.WithdrawAddress, `^[0-9][0-9AB][123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{93}$`) {
    162 		data.Error = "invalid xmr address"
    163 		return c.Render(http.StatusOK, "poker", data)
    164 	}
    165 	if data.WithdrawAddress == authUser.PokerXmrSubAddress {
    166 		data.Error = "cannot withdraw to the deposit address"
    167 		return c.Render(http.StatusOK, "poker", data)
    168 	}
    169 	if withdrawAmount < minWithdrawAmount {
    170 		data.Error = fmt.Sprintf("minimum withdraw amount is %d", minWithdrawAmount)
    171 		return c.Render(http.StatusOK, "poker", data)
    172 	}
    173 	userBalance := authUser.XmrBalance
    174 	if withdrawAmount > userBalance {
    175 		data.Error = fmt.Sprintf("maximum withdraw amount is %d (%d)", userBalance, withdrawAmount)
    176 		return c.Render(http.StatusOK, "poker", data)
    177 	}
    178 	withdrawAmount = utils.Clamp(withdrawAmount, minWithdrawAmount, userBalance)
    179 
    180 	lastOutTransaction, _ := db.GetLastUserWithdrawPokerXmrTransaction(authUser.ID)
    181 	if time.Since(lastOutTransaction.CreatedAt) < 5*time.Minute {
    182 		diff := time.Until(lastOutTransaction.CreatedAt.Add(5 * time.Minute))
    183 		data.Error = fmt.Sprintf("Wait %s before doing a new withdraw transaction", utils.ShortDur(diff))
    184 		return c.Render(http.StatusOK, "poker", data)
    185 	}
    186 
    187 	walletRpcClient := config.Xmr()
    188 
    189 	res, err := walletRpcClient.Transfer(&wallet1.RequestTransfer{
    190 		DoNotRelay:    true,
    191 		GetTxMetadata: true,
    192 		Destinations: []*wallet1.Destination{
    193 			{Address: data.WithdrawAddress,
    194 				Amount: uint64(withdrawAmount)}}})
    195 	if err != nil {
    196 		logrus.Error(err)
    197 		data.Error = err.Error()
    198 		return c.Render(http.StatusOK, "poker", data)
    199 	}
    200 
    201 	transactionFee := database.Piconero(res.Fee)
    202 
    203 	if withdrawAmount+transactionFee > authUser.XmrBalance {
    204 		data.Error = fmt.Sprintf("not enough funds to pay for transaction fee %d (%s xmr)", transactionFee, transactionFee.XmrStr())
    205 		return c.Render(http.StatusOK, "poker", data)
    206 	}
    207 
    208 	dutils.RootAdminNotify(db, fmt.Sprintf("new withdraw %s xmr by %s", withdrawAmount.XmrStr(), authUser.Username))
    209 
    210 	var pokerXmrTx database.PokerXmrTransaction
    211 	if err := db.WithE(func(tx *database.DkfDB) error {
    212 		xmrBalance, err := authUser.GetXmrBalance(tx)
    213 		if err != nil {
    214 			return err
    215 		}
    216 		if withdrawAmount+transactionFee > xmrBalance {
    217 			return errors.New("not enough funds")
    218 		}
    219 		if err := authUser.SubXmrBalance(tx, withdrawAmount+transactionFee); err != nil {
    220 			return err
    221 		}
    222 		if pokerXmrTx, err = tx.CreatePokerXmrTransaction(authUser.ID, res); err != nil {
    223 			logrus.Error("failed to create poker xmr transaction", err)
    224 			return err
    225 		}
    226 		return nil
    227 	}); err != nil {
    228 		data.Error = err.Error()
    229 		return c.Render(http.StatusOK, "poker", data)
    230 	}
    231 
    232 	if _, err := walletRpcClient.RelayTx(&wallet1.RequestRelayTx{Hex: res.TxMetadata}); err != nil {
    233 		if err := db.WithE(func(tx *database.DkfDB) error {
    234 			if err := pokerXmrTx.SetStatus(tx, database.PokerXmrTransactionStatusFailed); err != nil {
    235 				return err
    236 			}
    237 			if err := authUser.IncrXmrBalance(tx, withdrawAmount+transactionFee); err != nil {
    238 				return err
    239 			}
    240 			return nil
    241 		}); err != nil {
    242 			logrus.Error(err)
    243 		}
    244 		logrus.Error(err)
    245 		data.Error = err.Error()
    246 		return c.Render(http.StatusOK, "poker", data)
    247 	}
    248 
    249 	if err := pokerXmrTx.SetStatus(db, database.PokerXmrTransactionStatusSuccess); err != nil {
    250 		logrus.Error(err)
    251 	}
    252 
    253 	pokerWithdrawCache.Delete(authUser.ID)
    254 	return hutils.RedirectReferer(c)
    255 }
    256 
    257 func doJoinTable(db *database.DkfDB, pokerTableSlug string, playerBuyIn database.PokerChip, userID database.UserID) error {
    258 	err := db.WithE(func(tx *database.DkfDB) error {
    259 		roomID := poker.RoomID(pokerTableSlug)
    260 		g := poker.PokerInstance.GetGame(roomID)
    261 		if g == nil {
    262 			pokerTable, err := tx.GetPokerTableBySlug(pokerTableSlug)
    263 			if err != nil {
    264 				return errors.New("failed to get poker table")
    265 			}
    266 			g = poker.PokerInstance.CreateGame(db, roomID, pokerTable.ID, pokerTable.MinBet, pokerTable.IsTest)
    267 		}
    268 		g.Players.Lock()
    269 		defer g.Players.Unlock()
    270 		if g.IsSeatedUnsafe(userID) {
    271 			return errors.New("cannot buy-in while seated")
    272 		}
    273 		pokerTable, err := tx.GetPokerTableBySlug(pokerTableSlug)
    274 		if err != nil {
    275 			return errors.New("table mot found")
    276 		}
    277 		if playerBuyIn < pokerTable.MinBuyIn {
    278 			return errors.New("buy in too small")
    279 		}
    280 		if playerBuyIn > pokerTable.MaxBuyIn {
    281 			return errors.New("buy in too high")
    282 		}
    283 		xmrBalance, chipsTestBalance, err := tx.GetUserBalances(userID)
    284 		if err != nil {
    285 			return errors.New("failed to get user's balance")
    286 		}
    287 		userChips := utils.Ternary(pokerTable.IsTest, chipsTestBalance, xmrBalance.ToPokerChip())
    288 		if userChips < playerBuyIn {
    289 			return errors.New("not enough chips to buy-in")
    290 		}
    291 		tableAccount, err := tx.GetPokerTableAccount(userID, pokerTable.ID)
    292 		if err != nil {
    293 			return errors.New("failed to get table account")
    294 		}
    295 		if tableAccount.Amount+playerBuyIn > pokerTable.MaxBuyIn {
    296 			return errors.New("buy-in exceed table max buy-in")
    297 		}
    298 		tableAccount.Amount += playerBuyIn
    299 		if err := tx.DecrUserBalance(userID, pokerTable.IsTest, playerBuyIn); err != nil {
    300 			return errors.New("failed to update user's balance")
    301 		}
    302 		if err := tableAccount.Save(tx); err != nil {
    303 			return errors.New("failed to update user's table account")
    304 		}
    305 		return nil
    306 	})
    307 	return err
    308 }
    309 
    310 func doCashOut(db *database.DkfDB, pokerTableSlug string, userID database.UserID) error {
    311 	err := db.WithE(func(tx *database.DkfDB) error {
    312 		roomID := poker.RoomID(pokerTableSlug)
    313 		g := poker.PokerInstance.GetGame(roomID)
    314 		if g == nil {
    315 			pokerTable, err := tx.GetPokerTableBySlug(pokerTableSlug)
    316 			if err != nil {
    317 				return errors.New("failed to get poker table")
    318 			}
    319 			g = poker.PokerInstance.CreateGame(db, roomID, pokerTable.ID, pokerTable.MinBet, pokerTable.IsTest)
    320 		}
    321 		g.Players.Lock()
    322 		defer g.Players.Unlock()
    323 		if g.IsSeatedUnsafe(userID) {
    324 			return errors.New("cannot cash out while seated")
    325 		}
    326 		pokerTable, err := tx.GetPokerTableBySlug(pokerTableSlug)
    327 		if err != nil {
    328 			return errors.New("table mot found")
    329 		}
    330 		account, err := tx.GetPokerTableAccount(userID, pokerTable.ID)
    331 		if err != nil {
    332 			return errors.New("failed to get table account")
    333 		}
    334 		if err := tx.IncrUserBalance(userID, pokerTable.IsTest, account.Amount); err != nil {
    335 			return errors.New("failed to update user's balance")
    336 		}
    337 		account.Amount = 0
    338 		if err := account.Save(tx); err != nil {
    339 			return errors.New("failed to update user's table account")
    340 		}
    341 		return nil
    342 	})
    343 	return err
    344 }
    345 
    346 func PokerRakeBackHandler(c echo.Context) error {
    347 	authUser := c.Get("authUser").(*database.User)
    348 	db := c.Get("database").(*database.DkfDB)
    349 
    350 	var data pokerRakeBackData
    351 	data.RakeBackPct = poker.RakeBackPct * 100
    352 	data.ReferredCount, _ = db.GetRakeBackReferredCount(authUser.ID)
    353 	pokerReferralToken := authUser.PokerReferralToken
    354 	if pokerReferralToken != nil {
    355 		data.ReferralToken = *pokerReferralToken
    356 		data.ReferralURL = fmt.Sprintf("%s/poker?r=%s", config.DkfOnion, *pokerReferralToken)
    357 	}
    358 
    359 	if c.Request().Method == http.MethodGet {
    360 		return c.Render(http.StatusOK, "poker-rake-back", data)
    361 	}
    362 
    363 	formName := c.Request().PostFormValue("form_name")
    364 	if formName == "generate_referral_url" {
    365 		if pokerReferralToken != nil {
    366 			return hutils.RedirectReferer(c)
    367 		}
    368 		token, err := shortid.Generate()
    369 		if err != nil {
    370 			logrus.Error(err)
    371 			return hutils.RedirectReferer(c)
    372 		}
    373 		authUser.SetPokerReferralToken(db, &token)
    374 		return hutils.RedirectReferer(c)
    375 
    376 	} else if formName == "set_referrer" {
    377 		referralToken := c.Request().PostFormValue("referral_token")
    378 		if len(referralToken) != 9 {
    379 			data.SetReferralError = "Invalid referral token"
    380 			return c.Render(http.StatusOK, "poker-rake-back", data)
    381 		}
    382 		if authUser.PokerReferredBy != nil {
    383 			data.SetReferralError = "You are already giving your rake back"
    384 			return c.Render(http.StatusOK, "poker-rake-back", data)
    385 		}
    386 		if pokerReferralToken != nil && referralToken == *pokerReferralToken {
    387 			data.SetReferralError = "Yon can't give yourself the rake back"
    388 			return c.Render(http.StatusOK, "poker-rake-back", data)
    389 		}
    390 		referrer, err := db.GetUserByPokerReferralToken(referralToken)
    391 		if err != nil {
    392 			data.SetReferralError = "no user found with this referral token"
    393 			return c.Render(http.StatusOK, "poker-rake-back", data)
    394 		}
    395 		if referrer.ID == authUser.ID {
    396 			data.SetReferralError = "Yon can't give yourself the rake back"
    397 			return c.Render(http.StatusOK, "poker-rake-back", data)
    398 		}
    399 		authUser.SetPokerReferredBy(db, &referrer.ID)
    400 		return hutils.RedirectReferer(c)
    401 	}
    402 	return hutils.RedirectReferer(c)
    403 }
    404 
    405 func PokerTableHandler(c echo.Context) error {
    406 	roomID := c.Param("roomID")
    407 	var data pokerTableData
    408 	data.PokerTableSlug = roomID
    409 	return c.Render(http.StatusOK, "poker-table", data)
    410 }
    411 
    412 func PokerStreamHandler(c echo.Context) error {
    413 	roomID := poker.RoomID(c.Param("roomID"))
    414 	authUser := c.Get("authUser").(*database.User)
    415 	db := c.Get("database").(*database.DkfDB)
    416 
    417 	pokerTable, err := db.GetPokerTableBySlug(roomID.String())
    418 	if err != nil {
    419 		return c.Redirect(http.StatusFound, "/")
    420 	}
    421 
    422 	chatRoomSlug := "general"
    423 	tmp := strings.ReplaceAll(roomID.String(), "-", "_")
    424 	if _, err := db.GetChatRoomByName(tmp); err == nil {
    425 		chatRoomSlug = tmp
    426 	}
    427 
    428 	roomTopic := roomID.Topic()
    429 	roomUserTopic := roomID.UserTopic(authUser.ID)
    430 	send := func(s string) { _, _ = c.Response().Write([]byte(s)) }
    431 
    432 	g := poker.PokerInstance.GetOrCreateGame(db, roomID, pokerTable.ID, pokerTable.MinBet, pokerTable.IsTest)
    433 
    434 	streamItem, err := stream.SetStreaming(c, authUser.ID, roomTopic)
    435 	if err != nil {
    436 		return nil
    437 	}
    438 	defer streamItem.Cleanup()
    439 
    440 	sub := poker.PubSub.Subscribe([]string{roomTopic, roomUserTopic, "refresh_loading_icon_" + string(authUser.Username)})
    441 	defer sub.Close()
    442 
    443 	send(poker.BuildBaseHtml(g, authUser, chatRoomSlug))
    444 	c.Response().Flush()
    445 
    446 	loop(streamItem.Quit, sub, func(topic string, payload any) error {
    447 		switch payload.(type) {
    448 		case poker.RefreshLoadingIconEvent:
    449 			send(hutils.MetaRefresh(1))
    450 			return BreakLoopErr
    451 		}
    452 		send(poker.BuildPayloadHtml(g, authUser, payload))
    453 		c.Response().Flush()
    454 		return nil
    455 	})
    456 
    457 	return nil
    458 }
    459 
    460 func PokerLogsHandler(c echo.Context) error {
    461 	roomID := poker.RoomID(c.Param("roomID"))
    462 	authUser := c.Get("authUser").(*database.User)
    463 	send := func(s string) { _, _ = c.Response().Write([]byte(s)) }
    464 	g := poker.PokerInstance.GetGame(roomID)
    465 	if g == nil {
    466 		return c.Redirect(http.StatusFound, "/")
    467 	}
    468 	roomLogsTopic := roomID.LogsTopic()
    469 	sub := poker.PubSub.Subscribe([]string{roomLogsTopic, "refresh_loading_icon_" + string(authUser.Username)})
    470 	defer sub.Close()
    471 
    472 	streamItem, err := stream.SetStreaming(c, authUser.ID, roomLogsTopic)
    473 	if err != nil {
    474 		return nil
    475 	}
    476 	defer streamItem.Cleanup()
    477 
    478 	send(hutils.HtmlCssReset)
    479 	send(`<style>body { background-color: #444; color: #ddd; padding: 3px; }</style><div style="display:flex;flex-direction:column-reverse;">`)
    480 	for _, evt := range g.GetLogs() {
    481 		send(fmt.Sprintf(`<div>%s</div>`, evt.Message))
    482 	}
    483 	c.Response().Flush()
    484 
    485 	loop(streamItem.Quit, sub, func(topic string, payload any) error {
    486 		switch evt := payload.(type) {
    487 		case poker.RefreshLoadingIconEvent:
    488 			send(hutils.MetaRefresh(1))
    489 			return BreakLoopErr
    490 		case poker.LogEvent:
    491 			send(fmt.Sprintf(`<div>%s</div>`, evt.Message))
    492 			c.Response().Flush()
    493 		}
    494 		return nil
    495 	})
    496 
    497 	return nil
    498 }
    499 
    500 func PokerBetHandler(c echo.Context) error {
    501 	roomID := poker.RoomID(c.Param("roomID"))
    502 	authUser := c.Get("authUser").(*database.User)
    503 	send := func(s string) { _, _ = c.Response().Write([]byte(s)) }
    504 	g := poker.PokerInstance.GetGame(roomID)
    505 	if g == nil {
    506 		return c.Redirect(http.StatusFound, "/")
    507 	}
    508 
    509 	roomUserTopic := roomID.UserTopic(authUser.ID)
    510 	sub := poker.PubSub.Subscribe([]string{roomID.Topic(), roomUserTopic, "refresh_loading_icon_" + string(authUser.Username)})
    511 	defer sub.Close()
    512 
    513 	streamItem, err := stream.SetStreaming(c, authUser.ID, roomUserTopic)
    514 	if err != nil {
    515 		return nil
    516 	}
    517 	defer streamItem.Cleanup()
    518 
    519 	if c.Request().Method == http.MethodPost {
    520 		submitBtn := c.Request().PostFormValue("submitBtn")
    521 		if submitBtn == "check" {
    522 			g.Check(authUser.ID)
    523 		} else if submitBtn == "call" {
    524 			g.Call(authUser.ID)
    525 		} else if submitBtn == "fold" {
    526 			g.Fold(authUser.ID)
    527 		} else if submitBtn == "allIn" {
    528 			g.AllIn(authUser.ID)
    529 		} else {
    530 			raiseBtn := c.Request().PostFormValue("raise")
    531 			if raiseBtn == "raise" {
    532 				g.Raise(authUser.ID)
    533 			} else if raiseBtn == "raiseValue" {
    534 				raiseValue := database.PokerChip(utils.DoParseUint64(c.Request().PostFormValue("raiseValue")))
    535 				g.Bet(authUser.ID, raiseValue)
    536 			}
    537 		}
    538 		send(hutils.MetaRefreshNow())
    539 		c.Response().Flush()
    540 		return nil
    541 
    542 	} else {
    543 
    544 		send(hutils.HtmlCssReset)
    545 
    546 		if player := g.OngoingPlayer(authUser.ID); player != nil {
    547 			betBtnLbl := utils.Ternary(g.IsBet(), "Bet", "Raise")
    548 			minRaise := g.MinRaise()
    549 			canCheck := true
    550 			canFold := true
    551 			canCall := true
    552 			if g.IsYourTurn(player) {
    553 				playerBet := player.GetBet()
    554 				minBet := g.MinBet()
    555 				canCheck = g.CanCheck(player)
    556 				canFold = g.CanFold(player)
    557 				canCall = minBet-playerBet > 0
    558 			}
    559 			send(fmt.Sprintf(`
    560 	<style>
    561 		.raise-container {
    562 			display: inline-block;
    563 			margin-right: 20px;
    564 		}
    565 		.raise-input {
    566 			width: 90px;
    567 			-moz-appearance: textfield;
    568 		}
    569 		.raise-btn {
    570 			width: 51px;
    571 		}
    572 		.button-container {
    573 			display: inline-block;
    574 			vertical-align: top;
    575 		}
    576 		.min-raise-text {
    577 			margin-top: 4px;
    578 			font-family: Arial, Helvetica, sans-serif;
    579 			font-size: 18px;
    580 		}
    581 	</style>
    582 	<div>
    583 		<form method="post">
    584 			<div class="raise-container">
    585 				<input type="number" name="raiseValue" value="%s" min="%s" class="raise-input" />
    586 				<button type="submit" name="raise" value="raiseValue" class="raise-btn">%s</button><br />
    587 			</div>
    588 			<div class="button-container">
    589 				<button name="submitBtn" value="check" %s>Check</button>
    590 				<button name="submitBtn" value="call" %s>Call</button>
    591 				<button name="submitBtn" value="fold" %s>Fold</button>
    592 				<button name="submitBtn" value="allIn">All-in</button>
    593 			</div>
    594 		</form>
    595 		<div class="min-raise-text">Min raise: %d</div>
    596 	</div>
    597 `,
    598 				minRaise, minRaise,
    599 				betBtnLbl,
    600 				utils.TernaryOrZero(!canCheck, "disabled"),
    601 				utils.TernaryOrZero(!canCall, "disabled"),
    602 				utils.TernaryOrZero(!canFold, "disabled"),
    603 				minRaise))
    604 		}
    605 		c.Response().Flush()
    606 	}
    607 
    608 	loop(streamItem.Quit, sub, func(topic string, payload any) error {
    609 		switch payload.(type) {
    610 		case poker.RefreshLoadingIconEvent:
    611 			send(hutils.MetaRefresh(1))
    612 			return BreakLoopErr
    613 		case poker.RefreshButtonsEvent:
    614 			send(hutils.MetaRefreshNow())
    615 			c.Response().Flush()
    616 			return BreakLoopErr
    617 		}
    618 		return nil
    619 	})
    620 
    621 	return nil
    622 }
    623 
    624 var BreakLoopErr = errors.New("break Loop")
    625 var ContinueLoopErr = errors.New("continue Loop")
    626 
    627 func loop[T any](quit <-chan struct{}, sub *pubsub.Sub[T], clb func(topic string, payload T) error) {
    628 Loop:
    629 	for {
    630 		select {
    631 		case <-quit:
    632 			break Loop
    633 		default:
    634 		}
    635 
    636 		topic, payload, err := sub.ReceiveTimeout2(1*time.Second, quit)
    637 		if err != nil {
    638 			if errors.Is(err, pubsub.ErrCancelled) {
    639 				break Loop
    640 			}
    641 			continue
    642 		}
    643 
    644 		if err := clb(topic, payload); err != nil {
    645 			if errors.Is(err, BreakLoopErr) {
    646 				break Loop
    647 			} else if errors.Is(err, ContinueLoopErr) {
    648 				continue Loop
    649 			}
    650 		}
    651 	}
    652 }
    653 
    654 func PokerDealHandler(c echo.Context) error {
    655 	roomID := poker.RoomID(c.Param("roomID"))
    656 	authUser := c.Get("authUser").(*database.User)
    657 	g := poker.PokerInstance.GetGame(roomID)
    658 	if g == nil {
    659 		return c.NoContent(http.StatusNotFound)
    660 	}
    661 	if c.Request().Method == http.MethodPost {
    662 		g.Deal(authUser.ID)
    663 	}
    664 	html := hutils.HtmlCssReset
    665 	html += `<form method="post"><button>Deal</button></form>`
    666 	return c.HTML(http.StatusOK, html)
    667 }
    668 
    669 func PokerUnSitHandler(c echo.Context) error {
    670 	authUser := c.Get("authUser").(*database.User)
    671 	roomID := poker.RoomID(c.Param("roomID"))
    672 	html := hutils.HtmlCssReset + `<form method="post"><button>UnSit</button></form>`
    673 	g := poker.PokerInstance.GetGame(roomID)
    674 	if g == nil {
    675 		return c.NoContent(http.StatusNotFound)
    676 	}
    677 	if c.Request().Method == http.MethodPost {
    678 		g.UnSit(authUser.ID)
    679 	}
    680 	return c.HTML(http.StatusOK, html)
    681 }
    682 
    683 func PokerSitHandler(c echo.Context) error {
    684 	html := hutils.HtmlCssReset + `<form method="post"><button style="height: 40px; width: 65px;" title="Take seat"><img src="/public/img/throne.png" width="30" alt="sit" /></button></form>`
    685 	authUser := c.Get("authUser").(*database.User)
    686 	pos := utils.Clamp(utils.DoParseInt(c.Param("pos")), 1, poker.NbPlayers) - 1
    687 	roomID := poker.RoomID(c.Param("roomID"))
    688 	g := poker.PokerInstance.GetGame(roomID)
    689 	if g == nil {
    690 		return c.HTML(http.StatusOK, html)
    691 	}
    692 	if c.Request().Method == http.MethodPost {
    693 		g.Sit(authUser.ID, authUser.Username, authUser.PokerReferredBy, pos)
    694 	}
    695 	return c.HTML(http.StatusOK, html)
    696 }