diff --git a/Bot/DbService.fs b/Bot/DbService.fs index e8ac9bc..93f85cd 100644 --- a/Bot/DbService.fs +++ b/Bot/DbService.fs @@ -105,7 +105,7 @@ let getStoreItems (storeId : string) = |> Sql.connect |> Sql.parameters [ "sid", Sql.string storeId ] |> Sql.query """ - SELECT store_id,stock,available,limit_stock,i.id,name,description,icon_url,image_url,category,require_role,require_invites, + SELECT store_id,stock,available,limit_stock,i.id,name,description,icon_url,image_url,category,require_role,require_invites,sale_end, buy_price,sell_price,rate_limit,expiration,drop_chance,can_trade,can_consume,attack_power,defense_power,class_name,max_stack,mods FROM store_item JOIN item i on store_item.item_id = i.id @@ -116,6 +116,7 @@ let getStoreItems (storeId : string) = Stock = reader.int "stock" LimitStock = reader.bool "limit_stock" Available = reader.bool "available" + SaleEnd = reader.int64OrNone "sale_end" TotalSold = None RequiresInvites = reader.intOrNone "require_invites" RequiresRole = reader.stringOrNone "require_role" |> Option.map uint64 @@ -127,7 +128,7 @@ let getAllActiveStoreItems () = connStr |> Sql.connect |> Sql.query """ - SELECT store_id,stock,available,limit_stock,i.id,name,description,icon_url,image_url,category,require_role,require_invites, + SELECT store_id,stock,available,limit_stock,i.id,name,description,icon_url,image_url,category,require_role,require_invites,sale_end, buy_price,sell_price,rate_limit,expiration,drop_chance,can_trade,can_consume,attack_power,defense_power,class_name,max_stack,mods FROM store_item JOIN item i on store_item.item_id = i.id @@ -138,6 +139,7 @@ let getAllActiveStoreItems () = Stock = reader.int "stock" LimitStock = reader.bool "limit_stock" Available = reader.bool "available" + SaleEnd = reader.int64OrNone "sale_end" TotalSold = None RequiresInvites = reader.intOrNone "require_invites" RequiresRole = reader.stringOrNone "require_role" |> Option.map uint64 @@ -151,7 +153,7 @@ let getRafflesWithPurchases storeId = |> Sql.parameters [ "sid" , Sql.string storeId ] |> Sql.query """ WITH raffles AS - (SELECT store_id,stock,available,limit_stock,i.id AS raffle_id,name,description,icon_url,image_url,category,require_role,require_invites, + (SELECT store_id,stock,available,limit_stock,i.id AS raffle_id,name,description,icon_url,image_url,category,require_role,require_invites,sale_end, buy_price,sell_price,rate_limit,expiration,drop_chance,can_trade,can_consume,attack_power,defense_power,class_name,max_stack,mods FROM store_item JOIN item i on store_item.item_id = i.id @@ -166,6 +168,7 @@ FULL JOIN (SELECT item_id, count(*) AS total FROM inventory_item Stock = reader.int "stock" LimitStock = reader.bool "limit_stock" Available = reader.bool "available" + SaleEnd = reader.int64OrNone "sale_end" TotalSold = reader.intOrNone "total" RequiresInvites = reader.intOrNone "require_invites" RequiresRole = reader.stringOrNone "require_role" |> Option.map uint64 @@ -178,7 +181,7 @@ let getStoreItemBySymbol (itemSymbol : string) = |> Sql.connect |> Sql.parameters [ "iid", Sql.string itemSymbol ] |> Sql.query """ - SELECT store_id,stock,available,limit_stock,i.id,name,description,icon_url,image_url,category,require_role,require_invites, + SELECT store_id,stock,available,limit_stock,i.id,name,description,icon_url,image_url,category,require_role,require_invites,sale_end, buy_price,sell_price,rate_limit,expiration,drop_chance,can_trade,can_consume,attack_power,defense_power,class_name,max_stack,mods FROM store_item JOIN item i on store_item.item_id = i.id @@ -189,6 +192,7 @@ let getStoreItemBySymbol (itemSymbol : string) = Stock = reader.int "stock" LimitStock = reader.bool "limit_stock" Available = reader.bool "available" + SaleEnd = reader.int64OrNone "sale_end" TotalSold = None RequiresInvites = reader.intOrNone "require_invites" RequiresRole = reader.stringOrNone "require_role" |> Option.map uint64 diff --git a/Bot/GameTypes.fs b/Bot/GameTypes.fs index 826fe1f..df478b3 100644 --- a/Bot/GameTypes.fs +++ b/Bot/GameTypes.fs @@ -144,6 +144,7 @@ type StoreItem = { LimitStock : bool Available : bool TotalSold : int option + SaleEnd : int64 option RequiresRole : uint64 option RequiresInvites : int option Item : Item diff --git a/Bot/Games/Store.fs b/Bot/Games/Store.fs index 5ee8682..c49720f 100644 --- a/Bot/Games/Store.fs +++ b/Bot/Games/Store.fs @@ -53,13 +53,20 @@ let checkDoesntExceedStackCap (item : Item) player = | _ , Some _ -> "You already own this item" |> embedWithError | _ -> Ok () +let checkItemSaleStillActive (item : StoreItem) = + match item.SaleEnd with + | Some time -> + let date = DateTimeOffset.FromUnixTimeSeconds(time).DateTime.ToUniversalTime() + if DateTime.UtcNow < date then Ok () else $"Sale for {item.Item.Name} has already ended!" |> embedWithError + | None -> Ok () + let checkSoldItemAlready (item : Item) player = if player.Inventory |> List.exists (fun i -> item.Id = i.Id) then Ok () else $"{item.Name} not found in your inventory! Looks like you sold it already." |> embedWithError -let checkHasItemsInArsenal itemType items player = +let checkHasItemsInArsenal itemType items = if List.isEmpty items |> not then Ok () else $"You currently have no {itemType} in your arsenal to sell!" @@ -125,6 +132,13 @@ let getItemEmbeds owned (items : StoreItem list) = | _ -> ()) // if item.Item.Type = ItemType.Whitelist then // embed.AddField("Mint Allowance", (if item.Item.Id = "WHITEOG" then 2 else 1) |> string, true) |> ignore + item.SaleEnd |> Option.iter (fun time -> + let date = DateTimeOffset.FromUnixTimeSeconds(time).DateTime.ToUniversalTime() + if DateTime.UtcNow < date then + embed.AddField("⏰ Closes", $"", true) |> ignore + else + embed.AddField("🚫 Closed", $"", true) |> ignore) + item.TotalSold |> Option.iter (fun total -> embed.AddField("Total Sold", string total, true) |> ignore) embed.Color <- WeaponClass.getClassEmbedColor item.Item embed.Title <- titleText @@ -142,12 +156,19 @@ let getBuyItemsEmbed storeId player (storeInventory : StoreItem list) = storeInventory |> List.map (fun item -> let owned = player.Inventory |> List.exists (fun i -> i.Id = item.Item.Id) + let saleStillOngoing = + match item.SaleEnd with + | Some time -> + let date = DateTimeOffset.FromUnixTimeSeconds(time).DateTime.ToUniversalTime() + DateTime.UtcNow < date + | None -> true let inStock = item.Available && (item.Stock > 0 || item.LimitStock = false) - match owned , inStock with - | _ , false -> + match owned , inStock , saleStillOngoing with + | _ , false , _ -> let msg = if item.Available then "Out of Stock" else "Unavailable" DiscordButtonComponent(WeaponClass.getClassButtonColor item.Item, $"Buy-{item.Item.Id}-{storeId}", $"{item.Item.Name} ({msg})", true) - | false , true -> DiscordButtonComponent(WeaponClass.getClassButtonColor item.Item, $"Buy-{item.Item.Id}-{storeId}", $"Buy {item.Item.Name}") + | false , true , true -> DiscordButtonComponent(WeaponClass.getClassButtonColor item.Item, $"Buy-{item.Item.Id}-{storeId}", $"Buy {item.Item.Name}") + | _ , _ , false -> DiscordButtonComponent(WeaponClass.getClassButtonColor item.Item, $"Buy-{item.Item.Id}-{storeId}", $"Closed {item.Item.Name}", true) | _ -> match checkDoesntExceedStackCap item.Item player with | Ok _ -> DiscordButtonComponent(WeaponClass.getClassButtonColor item.Item, $"Buy-{item.Item.Id}-{storeId}", $"Buy {item.Item.Name}") @@ -170,7 +191,7 @@ let purchaseItemEmbed quantity (item : Item) = match item.Type with | ItemType.Jpeg -> let itemName = item.Name.Replace("🎟️", "") - embed.Description <- $"Congratulations! You are in the draw for the {itemName}.\n\nThe winner will be announced soon in <#{GuildEnvironment.channelGiveaway}>" + embed.Description <- $"Congratulations! You are in the draw for the {itemName}.\n\nWinners announced in <#{GuildEnvironment.channelGiveaway}>" embed.ImageUrl <- item.ImageUrl embed.Thumbnail <- DiscordEmbedBuilder.EmbedThumbnail() embed.Thumbnail.Url <- item.IconUrl @@ -261,7 +282,7 @@ let buyForPlayer storeId player (filterBy : ItemType option) (ctx : IDiscordCont let sell itemType getItems (ctx : IDiscordContext) = executePlayerAction ctx (fun player -> async { let items = getItems player.Inventory - match checkHasItemsInArsenal itemType items player with + match checkHasItemsInArsenal itemType items with | Ok _ -> let itemStore = getSellEmbed items do! ctx.FollowUp(itemStore) |> Async.AwaitTask | Error e -> do! ctx.FollowUp e |> Async.AwaitTask @@ -335,6 +356,7 @@ let handleBuyItem (dispatch : IDiscordContext -> Task) (ctx : IDiscordContext) i let storeItem = storeInventory |> List.find (fun si -> si.Item.Id = itemId) do! checkHasSufficientFunds storeItem.Item player do! checkHasStock storeItem + do! checkItemSaleStillActive storeItem do! checkDoesntExceedStackCap storeItem.Item player do! checkHasRequiredRole storeItem (ctx.GetDiscordMember()) do! checkHasRequiredInvites storeItem player diff --git a/Bot/InviteTracker.fs b/Bot/InviteTracker.fs index 7ca8888..57adb67 100644 --- a/Bot/InviteTracker.fs +++ b/Bot/InviteTracker.fs @@ -68,20 +68,26 @@ let private createInvite inviter code = |> Sql.executeNonQueryAsync |> Async.AwaitTask -let private addInvitedUser did code count = - try - connStr - |> Sql.connect - |> Sql.executeTransactionAsync [ - """ - INSERT INTO invited_user (discord_id, invite_id) - VALUES (@did, (SELECT id FROM invite WHERE code = @code)); - """ , [ [ "@code" , Sql.string code ; "@did" , Sql.string (string did) ] ] - "UPDATE invite SET count = @count WHERE code = @code" , [ [ "count" , Sql.int count ; "code" , Sql.string code ] ] - ] - |> Async.AwaitTask - |> Async.Ignore - with _ -> async.Zero () +let private addInvitedUser did inviterId code = + connStr + |> Sql.connect + |> Sql.parameters [ "@code" , Sql.string code ; "@did" , Sql.string (string did) ; "@iid" , Sql.string (string inviterId) ] + |> Sql.query """ + INSERT INTO invited_user (inviter_id, discord_id, invite_id) + VALUES (@iid, @did, (SELECT id FROM invite WHERE code = @code)) + """ + |> Sql.executeNonQueryAsync + |> Async.AwaitTask + |> Async.Ignore + +let private updateInviteCount code count = + connStr + |> Sql.connect + |> Sql.parameters [ "count" , Sql.int count ; "code" , Sql.string code ] + |> Sql.query "UPDATE invite SET count = @count WHERE code = @code" + |> Sql.executeNonQueryAsync + |> Async.AwaitTask + |> Async.Ignore let private markInvitedAccepted did = connStr @@ -129,18 +135,14 @@ let private checkUserAlreadyInvited userId = async { } let checkInviteAccepted (userId : uint64) = async { - try let! result = connStr |> Sql.connect |> Sql.parameters [ "did" , Sql.string (string userId) ] |> Sql.query "SELECT accepted FROM invited_user WHERE discord_id = @did" - |> Sql.executeRowAsync (fun read -> read.bool "accepted") + |> Sql.executeAsync (fun read -> read.bool "accepted") |> Async.AwaitTask - return result - with ex -> - printfn "%s %u" ex.Message userId - return false + return List.tryHead result |> Option.defaultValue false } let private getInviteAttributions userId = @@ -279,10 +281,20 @@ let private processNewUser (eventArgs : GuildMemberAddEventArgs) = for invite in guildInvites do let result = cachedInvites.TryFind(invite.Code) match result with - | Some (_,count) -> + | Some (inviterId,count) -> if invite.Uses > count then - do! addInvitedUser eventArgs.Member.Id invite.Code invite.Uses |> Async.Ignore - do! Analytics.invitedUserEntered invite.Code invite.Inviter.Id eventArgs.Member.Id invite.Inviter.Username eventArgs.Member.Username + do! updateInviteCount invite.Code invite.Uses + try + match! checkUserAlreadyInvited eventArgs.Member.Id with + | false -> + do! addInvitedUser eventArgs.Member.Id inviterId invite.Code |> Async.Ignore + match! DbService.tryFindPlayer inviterId with + | Some inviter -> + do! Analytics.invitedUserEntered invite.Code inviter.DiscordId eventArgs.Member.Id inviter.Name eventArgs.Member.Username + | None -> + do! Analytics.invitedUserEntered invite.Code inviterId eventArgs.Member.Id "Unknown" eventArgs.Member.Username + | true -> () + with ex -> printfn $"Tried to add existing user {eventArgs.Member.Id}:{eventArgs.Member.Username} to invites: {ex.Message}" | None -> () } :> Task @@ -290,7 +302,7 @@ let acceptInvite (guild : DiscordGuild) (user : DiscordMember) = task { match! checkInviteAccepted user.Id with | false -> - let! _ = markInvitedAccepted user.Id |> Async.Ignore + do! markInvitedAccepted user.Id |> Async.Ignore try let! invite = getInviteFromInvitedUser user.Id let! player = DbService.tryFindPlayer invite.Inviter @@ -310,18 +322,20 @@ let acceptInvite (guild : DiscordGuild) (user : DiscordMember) = let role3x = guild.Roles.TryGetValue(GuildEnvironment.roleRecruiter3x) |> snd let role2x = guild.Roles.TryGetValue(GuildEnvironment.roleRecruiter2x) |> snd let role1x = guild.Roles.TryGetValue(GuildEnvironment.roleRecruiter1x) |> snd - match invite.Count with - | count when count > 10 -> - do! [ user.GrantRoleAsync(role3x) ; user.RevokeRoleAsync(role2x) ; user.RevokeRoleAsync(role1x) ] + let! playerMember = guild.GetMemberAsync(invite.Inviter) + let! totalInvites = getInvitedUserCount player.DiscordId + if totalInvites >= 10 then + do! [ playerMember.GrantRoleAsync(role3x) ; playerMember.RevokeRoleAsync(role2x) ; playerMember.RevokeRoleAsync(role1x) ] |> List.map Async.AwaitTask |> Async.Parallel |> Async.Ignore - | count when count > 5 -> - do! [ user.GrantRoleAsync(role2x) ; user.RevokeRoleAsync(role1x) ] + elif totalInvites >= 5 then + do! [ playerMember.GrantRoleAsync(role2x) ; playerMember.RevokeRoleAsync(role1x) ] |> List.map Async.AwaitTask |> Async.Parallel |> Async.Ignore - | _ -> do! user.GrantRoleAsync(role1x) + else + do! playerMember.GrantRoleAsync(role1x) do! Analytics.invitedUserAccepted invite.Code player.DiscordId user.Id player.Name user.Username | None -> return () with _ -> () @@ -343,6 +357,9 @@ let sendInitialEmbed (ctx : IDiscordContext) = **__Bonus__** 💰 Earn an extra {InviteRewardAmount} $GBT for every invite! +<:purple_fist:986685279031152650> <@#{GuildEnvironment.roleRecruiter1x}> role if you invite 1 or more Degen +<:red_fist:986685280868249690> <@#{GuildEnvironment.roleRecruiter2x}> role is you invite 5 or more Degen +<:gold_fist:986685276942377052> <@#{GuildEnvironment.roleRecruiter3x}> role is you invite 10 or more Degen **Every invite increases your chances of winning* """ @@ -370,7 +387,7 @@ let showWalletStatus (ctx : IDiscordContext) = try match! getWalletAddress player.DiscordId with | Some address -> do! Messaging.sendFollowUpMessage ctx $""" -🚀 __Mint Date:__ Mid June +🚀 __Mint Date:__ June 20th ✅ __Status:__ We have successfully received your wallet address: {address}""" | None -> do! Messaging.sendFollowUpMessage ctx "You haven't submitted a wallet yet. Type `/submit`, paste your **Solana Wallet Address**, then press enter" with ex -> @@ -473,12 +490,7 @@ let handleMessageCreated _ (event : MessageCreateEventArgs) = do! event.Message.DeleteAsync() } :> Task -let handleGuildMemberAdded _ (eventArgs : GuildMemberAddEventArgs) = - task { - let! exists = checkUserAlreadyInvited eventArgs.Member.Id - if not exists then - do! processNewUser eventArgs - } :> Task +let handleGuildMemberAdded _ (eventArgs : GuildMemberAddEventArgs) = processNewUser eventArgs let submitAddress (address : string) (ctx : IDiscordContext) = PlayerInteractions.executePlayerAction ctx (fun player -> async { @@ -504,10 +516,9 @@ let submitAddress (address : string) (ctx : IDiscordContext) = do! user.GrantRoleAsync(role) |> Async.AwaitTask let role = ctx.GetGuild().GetRole(GuildEnvironment.roleWhiteOGPending) do! user.RevokeRoleAsync(role) |> Async.AwaitTask - do! Messaging.sendFollowUpMessage ctx $""" -🚀 __Mint Date:__ Mid June +🚀 __Mint Date:__ June 20th ✅ {msg} {address} Keep an eye on <#{GuildEnvironment.channelAnnouncements}> for updates."""