diff --git a/Bot/Bot.fsproj b/Bot/Bot.fsproj index 0a13f65..0669f35 100644 --- a/Bot/Bot.fsproj +++ b/Bot/Bot.fsproj @@ -28,7 +28,7 @@ - + diff --git a/Bot/DbService.fs b/Bot/DbService.fs index 872edb7..8997795 100644 --- a/Bot/DbService.fs +++ b/Bot/DbService.fs @@ -116,6 +116,8 @@ let getStoreItems (storeId : string) = Stock = reader.int "stock" LimitStock = reader.bool "limit_stock" Available = reader.bool "available" + RequiresInvites = None + RequiresRole = None StoreItem.Item = readItem reader }) |> Async.AwaitTask @@ -136,6 +138,8 @@ let getStoreItemBySymbol (itemSymbol : string) = Stock = reader.int "stock" LimitStock = reader.bool "limit_stock" Available = reader.bool "available" + RequiresInvites = None + RequiresRole = None StoreItem.Item = readItem reader }) |> Async.AwaitTask diff --git a/Bot/GameTypes.fs b/Bot/GameTypes.fs index 45364fc..2da086d 100644 --- a/Bot/GameTypes.fs +++ b/Bot/GameTypes.fs @@ -141,6 +141,8 @@ type StoreItem = { Stock : int LimitStock : bool Available : bool + RequiresRole : uint64 option + RequiresInvites : int option Item : Item } diff --git a/Bot/Games/Store.fs b/Bot/Games/Store.fs index 68e703a..79bba8b 100644 --- a/Bot/Games/Store.fs +++ b/Bot/Games/Store.fs @@ -9,22 +9,32 @@ open DSharpPlus.SlashCommands open Degenz open Degenz.Messaging open Degenz.PlayerInteractions +open FsToolkit.ErrorHandling -let checkHasStock (item : StoreItem) player = +let embedWithError error = + DiscordFollowupMessageBuilder() + .AsEphemeral() + .WithContent(error) + |> Error + +let checkHasStock (item : StoreItem) = if item.Stock > 0 || item.LimitStock = false - then Ok player - else Error $"{item.Item.Name} is out of stock! Check back later to purchase" + then Ok () + else $"{item.Item.Name} is out of stock! Check back later to purchase" + |> embedWithError let checkHasSufficientFunds (item : Item) player = match item.Attributes with | CanBuy price -> if player.Bank - price >= 0 - then Ok player - else Error $""" + then Ok () + else $""" **__Current Balance:__** {player.Bank} 💰 $GBT Hold up Degen! You don't have enough $GBT! Go to <#{GuildEnvironment.channelQuests}> to start earning some now...""" - | _ -> Error $"{item.Name} item cannot be bought" + |> embedWithError + | _ -> $"{item.Name} item cannot be bought" + |> embedWithError let getTotalOwnedOfItem (item : Item) (inventory : Item list) = inventory @@ -37,20 +47,23 @@ let checkDoesntExceedStackCap (item : Item) player = match item.Attributes , itemCount with | CanStack max , Some count -> if count >= max - then Error $"You own the maximum allowed amount {item.Name}!" - else Ok player - | _ , Some _ -> Error $"You already own this item" - | _ -> Ok player + then $"You own the maximum allowed amount {item.Name}!" + |> embedWithError + else Ok () + | _ , Some _ -> "You already own this item" |> embedWithError + | _ -> Ok () let checkSoldItemAlready (item : Item) player = if player.Inventory |> List.exists (fun i -> item.Id = i.Id) - then Ok player - else Error $"{item.Name} not found in your inventory! Looks like you sold it already." + then Ok () + else $"{item.Name} not found in your inventory! Looks like you sold it already." + |> embedWithError let checkHasItemsInArsenal itemType items player = if List.isEmpty items |> not - then Ok player - else Error $"You currently have no {itemType} in your arsenal to sell!" + then Ok () + else $"You currently have no {itemType} in your arsenal to sell!" + |> embedWithError let getItemEmbeds owned (items : StoreItem list) = items @@ -198,7 +211,7 @@ let showJpegsEmbed (ctx : IDiscordContext) = PlayerInteractions.executePlayerAct let jpegs = player.Inventory |> Inventory.getItemsByType ItemType.Jpeg - |> List.map (fun i -> { StoreId = "BACKALLEY" ; Item = i ; Stock = 1 ; LimitStock = false ; Available = true }) + |> List.map (fun i -> { StoreId = "" ; Item = i ; Stock = 1 ; LimitStock = false ; Available = true ; RequiresInvites = None ; RequiresRole = None }) match jpegs with | [] -> do! Messaging.sendFollowUpMessage ctx $"You currently do not own any jpegs or raffle tickets. Go to <#{GuildEnvironment.channelBackAlley}> to buy some" | jpegs -> @@ -224,56 +237,104 @@ let buy storeId (filterBy : ItemType option) (ctx : IDiscordContext) = with ex -> printfn $"{ex.Message}" }) +let buyForPlayer storeId player (filterBy : ItemType option) (ctx : IDiscordContext) = async { + try + let! items = DbService.getStoreItems storeId + if items.Length > 0 then + let items' = + match filterBy with + | Some itemType -> items |> List.filter (fun item -> item.Item.Type = itemType) + | None -> items + let itemStore = getBuyItemsEmbed storeId player items' + do! ctx.FollowUp itemStore |> Async.AwaitTask + do! Analytics.buyItemCommand (ctx.GetDiscordMember()) storeId + else + do! Messaging.sendFollowUpMessage ctx "There are currently no items available, check back later" + with ex -> printfn $"{ex.Message}" +} + let sell itemType getItems (ctx : IDiscordContext) = executePlayerAction ctx (fun player -> async { let items = getItems player.Inventory match checkHasItemsInArsenal itemType items player with | Ok _ -> let itemStore = getSellEmbed items do! ctx.FollowUp(itemStore) |> Async.AwaitTask - | Error e -> do! sendFollowUpMessage ctx e + | Error e -> do! ctx.FollowUp e |> Async.AwaitTask do! Analytics.buyItemCommand (ctx.GetDiscordMember()) itemType }) +let checkHasRequiredInvites storeItem player = + async { + match storeItem.RequiresInvites with + | Some amount -> + let! totalInvites = InviteTracker.getInvitedUserCount player.DiscordId + if amount <= totalInvites then + return Ok () + else + return "" |> embedWithError + | None -> return Ok () + } + +let checkHasRequiredRole storeItem (user : DiscordMember) = + match storeItem.RequiresRole with + | Some roleId -> + if user.Roles |> Seq.exists (fun r -> r.Id = roleId) then + Ok () + else + let builder = DiscordFollowupMessageBuilder().AsEphemeral() + let embed = DiscordEmbedBuilder() + embed.Description <- $""" + + """ + builder.AddEmbed(embed) + "" |> embedWithError + | None -> Ok () + // TODO: When you buy a shield, prompt the user to activate it let handleBuyItem (dispatch : IDiscordContext -> Task) (ctx : IDiscordContext) itemId = executePlayerAction ctx (fun player -> async { let storeId = ctx.GetInteractionId().Split("-").[2] - let! storeInventory = DbService.getStoreItems storeId - let storeItem = storeInventory |> List.find (fun si -> si.Item.Id = itemId) - let item = storeInventory |> List.map (fun i -> i.Item) |> Inventory.findItemById itemId - do! player - |> checkHasSufficientFunds item - >>= checkHasStock storeItem - >>= checkDoesntExceedStackCap item - |> handleResultWithResponse ctx (fun player -> async { - let price = match item.Attributes with CanBuy price -> price | _ -> 0 + let! result = asyncResult { + let! storeInventory = DbService.getStoreItems storeId + let storeItem = storeInventory |> List.find (fun si -> si.Item.Id = itemId) + do! checkHasSufficientFunds storeItem.Item player + do! checkHasStock storeItem + do! checkDoesntExceedStackCap storeItem.Item player + do! checkHasRequiredRole storeItem (ctx.GetDiscordMember()) + do! checkHasRequiredInvites storeItem player + return storeItem + } + return! + result + |> handleResultWithEmbed ctx (fun storeItem -> async { + let price = match storeItem.Item.Attributes with CanBuy price -> price | _ -> 0 try do! dispatch ctx |> Async.AwaitTask do! DbService.updatePlayerCurrency -price player.DiscordId |> Async.Ignore - do! DbService.addToPlayerInventory player.DiscordId item |> Async.Ignore + do! DbService.addToPlayerInventory player.DiscordId storeItem.Item |> Async.Ignore if storeItem.LimitStock = true && storeItem.Stock > 0 then - do! DbService.decrementItemStock item |> Async.Ignore + do! DbService.decrementItemStock storeItem.Item |> Async.Ignore let builder = DiscordFollowupMessageBuilder().AsEphemeral(true) - let embed = purchaseItemEmbed 1 item - match item.Attributes , getTotalOwnedOfItem item player.Inventory |> Option.defaultValue 0 with + let embed = purchaseItemEmbed 1 storeItem.Item + match storeItem.Item.Attributes , getTotalOwnedOfItem storeItem.Item player.Inventory |> Option.defaultValue 0 with | CanStack max , amount -> embed.AddField("Owned", $"{amount + 1}", true) |> ignore embed.AddField("New $GBT Balance", $"`💰` {player.Bank - price} `(-{price} $GBT)`", true) |> ignore if amount + 1 < max then - let btn = DiscordButtonComponent(WeaponClass.getClassButtonColor item, $"Buy-{item.Id}-{storeId}", $"Buy Another") + let btn = DiscordButtonComponent(WeaponClass.getClassButtonColor storeItem.Item, $"Buy-{storeItem.Item.Id}-{storeId}", $"Buy Another") builder.AddComponents(btn) |> ignore | _ -> () builder.AddEmbed(embed) |> ignore do! ctx.FollowUp builder |> Async.AwaitTask let builder = DiscordMessageBuilder() - builder.WithContent($"{player.Name} just purchased {item.Name}!") |> ignore + builder.WithContent($"{player.Name} just purchased {storeItem.Item.Name}!") |> ignore let channel = ctx.GetGuild().GetChannel(GuildEnvironment.channelEventsHackerBattle) do! channel.SendMessageAsync(builder) |> Async.AwaitTask |> Async.Ignore - do! Analytics.buyItemButton (ctx.GetDiscordMember()) item.Id price + do! Analytics.buyItemButton (ctx.GetDiscordMember()) storeItem.Item.Id price with ex -> printfn $"STORE ERROR: {ex.Message}" }) @@ -285,7 +346,7 @@ let handleSell (ctx : IDiscordContext) itemId = do! player |> checkSoldItemAlready item - |> handleResultWithResponse ctx (fun player -> async { + |> handleResultWithEmbed ctx (fun () -> async { match item.Attributes with | CanSell price -> do! @@ -300,28 +361,6 @@ let handleSell (ctx : IDiscordContext) itemId = }) }) -let showStats (ctx : IDiscordContext) = PlayerInteractions.executePlayerAction ctx (fun player -> async { - let embed = DiscordEmbedBuilder() - PlayerStats.stats - |> List.iter (fun statConfig -> - let playerStat = PlayerStats.getPlayerStat statConfig player - let min = - match statConfig.BaseRange.Min = playerStat.ModRange.Min with - | true -> $"{statConfig.BaseRange.Min}" - | false -> $"{statConfig.BaseRange.Min} (+{playerStat.ModRange.Min}) " - let max = - match statConfig.BaseRange.Max = playerStat.ModRange.Max with - | true -> $"{statConfig.BaseRange.Max}" - | false -> $"{statConfig.BaseRange.Max} (+{playerStat.ModRange.Max}) " - let field = $"{min} |---------------| {max}" - embed.AddField(string statConfig.Id , field) |> ignore) - let builder = - DiscordFollowupMessageBuilder() - .AddEmbed(embed) - .AsEphemeral(true) - do! ctx.FollowUp builder |> Async.AwaitTask -}) - let handleJpegEvents _ (event : ComponentInteractionCreateEventArgs) = let ctx = DiscordEventContext event :> IDiscordContext let id = ctx.GetInteractionId() @@ -364,15 +403,15 @@ let sendBackalleyEmbed (ctx : IDiscordContext) = let builder = DiscordMessageBuilder() let embed = DiscordEmbedBuilder() embed.ImageUrl <- "https://s7.gifyu.com/images/ezgif.com-gif-maker-23203b9dca779ba7cf.gif" - embed.Title <- "Jpeg Store" + embed.Title <- "🎟️ Raffle Store" embed.Color <- DiscordColor.Black - embed.Description <- "Hey, what do you want kid?" + embed.Description <- $"Hey, what do you want, kid?\nI ain’t got all day… {Formatter.Emoji}" builder.AddEmbed embed |> ignore - let button1 = DiscordButtonComponent(ButtonStyle.Success, $"ShowStore-0-BACKALLEY1", $"NFT Raffles") :> DiscordComponent + let button1 = DiscordButtonComponent(ButtonStyle.Danger, $"ShowStore-0-BACKALLEY1", $"NFT Raffles") :> DiscordComponent let button2 = DiscordButtonComponent(ButtonStyle.Success, $"ShowStore-0-BACKALLEY2", $"Whitelist Raffles") :> DiscordComponent - let button3 = DiscordButtonComponent(ButtonStyle.Success, $"ShowStore-0-BACKALLEY3", $"USDT Raffles") :> DiscordComponent +// let button3 = DiscordButtonComponent(ButtonStyle.Success, $"ShowStore-0-BACKALLEY3", $"USDT Raffles") :> DiscordComponent let button4 = DiscordButtonComponent(ButtonStyle.Primary, $"ShowJpegInventory-0-0", $"View My Stash") :> DiscordComponent - builder.AddComponents [| button1 ; button2 ; button3 ; button4 |] |> ignore + builder.AddComponents [| button1 ; button2 ; button4 |] |> ignore do! GuildEnvironment.botClientJpeg.Value.SendMessageAsync(channel, builder) |> Async.AwaitTask diff --git a/Bot/InviteTracker.fs b/Bot/InviteTracker.fs index 0e9e26d..394142d 100644 --- a/Bot/InviteTracker.fs +++ b/Bot/InviteTracker.fs @@ -11,7 +11,7 @@ open Npgsql.FSharp open Solnet.Wallet let connStr = GuildEnvironment.connectionString -let InviteRewardAmount = 100 +let InviteRewardAmount = 300 let InviteLinkButtonText = "Get My Invite Link" type Invite = { @@ -318,16 +318,16 @@ let sendInitialEmbed (ctx : IDiscordContext) = try let channel = ctx.GetGuild().GetChannel(GuildEnvironment.channelRecruitment) let rewardMsg = $""" -**__Win $2,000:__** +**__Win $3,000:__** 🙋 1 invite = 1 entry everyday* 🎟 $100 daily raffles till mint **__How To Invite:__** 1️⃣ Click the green button below -2️⃣ Share your unique link with Degenz +2️⃣ Share your unique link with Friends **__Bonus__** -💰 Earn an extra 100 $GBT for every invite! +💰 Earn an extra 300 $GBT for every invite! **Every invite increases your chances of winning* """ @@ -335,7 +335,7 @@ let sendInitialEmbed (ctx : IDiscordContext) = DiscordEmbedBuilder() .WithColor(DiscordColor.CornflowerBlue) .WithDescription(rewardMsg) - .WithImageUrl("https://s8.gifyu.com/images/invite-banner-usdc.png") + .WithImageUrl("https://s8.gifyu.com/images/invite-banner-usdcb670496dc3653cb3.png") .WithTitle("Invite Degenz") let builder = DiscordMessageBuilder().AddEmbed(embed) diff --git a/Bot/PlayerInteractions.fs b/Bot/PlayerInteractions.fs index 170c80d..7d5a787 100644 --- a/Bot/PlayerInteractions.fs +++ b/Bot/PlayerInteractions.fs @@ -56,3 +56,8 @@ let handleResultWithResponse ctx fn (player : Result) = match player with | Ok p -> fn p | Error e -> async { do! Messaging.sendFollowUpMessage ctx e } + +let handleResultWithEmbed<'a> (ctx : IDiscordContext) fn (player : Result<'a, DiscordFollowupMessageBuilder>) = + match player with + | Ok a -> fn a + | Error e -> async { do! ctx.FollowUp e |> Async.AwaitTask} diff --git a/Bot/Scripts/Whitelist.fsx b/Bot/Scripts/GetWhitelisted.fsx similarity index 100% rename from Bot/Scripts/Whitelist.fsx rename to Bot/Scripts/GetWhitelisted.fsx diff --git a/Bot/Whitelist.fs b/Bot/Whitelist.fs index 4442603..02f0c07 100644 --- a/Bot/Whitelist.fs +++ b/Bot/Whitelist.fs @@ -24,10 +24,12 @@ let sendInitialEmbed (ctx : IDiscordContext) = embed.Title <- "Degenz Game Whitelist" embed.Color <- DiscordColor.White embed.Description <- $""" -You need to **BUY** Whitelist with 💰 $GBT... +**__Requirements:__** +You need to BUY Whitelist with 💰 $GBT +You must have INVITED at least 1 Degen… **__To Earn $GBT:__** -1️⃣ Recruit Degenz in <#{GuildEnvironment.channelRecruitment}> +1️⃣ Invite Degenz in <#{GuildEnvironment.channelRecruitment}> 2️⃣ Chat to level up in the <#{GuildEnvironment.channelGeneral}> 3️⃣ Complete fun quests inside <#{GuildEnvironment.channelQuests}> """ @@ -59,7 +61,28 @@ let grantWhitelistRole isOg (ctx : IDiscordContext) = let handleButtonEvent _ (event : ComponentInteractionCreateEventArgs) = let ctx = DiscordEventContext event :> IDiscordContext match event.Id with - | id when id.StartsWith("GimmeWhitelist") -> Store.buy "WHITELIST" None ctx + | id when id.StartsWith("GimmeWhitelist") -> + task { + let builder = DiscordInteractionResponseBuilder().AsEphemeral(true).WithContent("Content") + do! ctx.Respond(InteractionResponseType.DeferredChannelMessageWithSource, builder) |> Async.AwaitTask + let! invites = InviteTracker.getInvitedUserCount (ctx.GetDiscordMember().Id) + if invites > 0 then + let! playerResult = DbService.tryFindPlayer (ctx.GetDiscordMember().Id) + match playerResult with + | Some player -> do! Store.buyForPlayer "WHITELIST" player None ctx |> Async.StartAsTask + | None -> do! Messaging.sendFollowUpMessage ctx "You are currently not a hacker, first use the /redpill command to become one" + else + let builder = DiscordFollowupMessageBuilder().AsEphemeral(true) + builder.Content <- $""" +❌ **Degen**, can’t you **READ**?! +⚠️ **__Requirements:__** 1x Invited User + +To BUY Whitelist you must have **__INVITED__** 1 Degen. +☑️ Go to <#{GuildEnvironment.channelRecruitment}> +☑️ Invite just 1 Degen! +""" + do! ctx.FollowUp(builder) + } :> Task | id when id.StartsWith("Buy") -> task { let id = ctx.GetInteractionId() diff --git a/Bot/paket.references b/Bot/paket.references index a301393..823b16c 100644 --- a/Bot/paket.references +++ b/Bot/paket.references @@ -5,4 +5,5 @@ DSharpPlus.SlashCommands dotenv.net Npgsql.FSharp mixpanel-csharp -Solnet.Rpc \ No newline at end of file +Solnet.Rpc +FsToolkit.ErrorHandling \ No newline at end of file diff --git a/paket.dependencies b/paket.dependencies index c393de4..65c4be5 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -8,6 +8,7 @@ nuget FSharp.Core >= 6.0.0 nuget DSharpPlus >= 4.3.0-nightly-01135 nuget DSharpPlus.Interactivity >= 4.3.0-nightly-01135 nuget DSharpPlus.SlashCommands >= 4.3.0-nightly-01135 +nuget FsToolkit.ErrorHandling nuget MongoDB.Driver nuget dotenv.net 3.1.1 @@ -16,4 +17,4 @@ nuget mixpanel-csharp 5.0.0 nuget Solnet.Extensions nuget Solnet.KeyStore nuget Solnet.Programs -nuget Solnet.Rpc +nuget Solnet.Rpc \ No newline at end of file diff --git a/paket.lock b/paket.lock index 5687dea..4835e9b 100644 --- a/paket.lock +++ b/paket.lock @@ -31,6 +31,8 @@ NUGET System.Runtime.CompilerServices.Unsafe (>= 5.0) System.ValueTuple (>= 4.5) FSharp.Core (6.0.3) + FsToolkit.ErrorHandling (2.13) + FSharp.Core (>= 4.7.2) Microsoft.Bcl.AsyncInterfaces (6.0) - restriction: || (&& (== net6.0) (< netstandard2.1)) (== netstandard2.0) System.Threading.Tasks.Extensions (>= 4.5.4) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netstandard2.1)) (== netstandard2.0) (&& (== netstandard2.1) (>= net461)) Microsoft.Bcl.HashCode (1.1.1) - restriction: || (&& (== net6.0) (< netstandard2.1)) (== netstandard2.0)