diff --git a/Bot/Analytics.fs b/Bot/Analytics.fs new file mode 100644 index 0000000..17689ee --- /dev/null +++ b/Bot/Analytics.fs @@ -0,0 +1,79 @@ +module Degenz.Analytics + +open Mixpanel + +let private mix = MixpanelClient(GuildEnvironment.tokenMixpanel) + +let private track eventName id data = + let map = + [ "distinct_id" , box id + "guild_id" , box GuildEnvironment.guildId + "env" , box GuildEnvironment.environment ] + mix.TrackAsync(eventName, data @ map |> dict) |> Async.AwaitTask |> Async.Ignore + +let invitedUserEntered inviteCode inviterId inviteeId inviterName inviteeName = + let data = [ + "user_display_name" , inviterName + "invite_code" , inviteCode + "invitee_id" , inviteeId + "invitee_name" , inviteeName + ] + track "Invited User Entered" inviterId data + +let invitedUserAccepted inviteCode inviterId inviteeId inviterName inviteeName = + let data = [ + "user_display_name" , inviterName + "invite_code" , inviteCode + "invitee_id" , inviteeId + "invitee_name" , inviteeName + ] + track "Invited User Accepted" inviterId data + +let recruitCommand origin id name channelId channelName = + let data = [ + "user_display_name" , name + "origin" , origin + "channel_id" , channelId + "channel_name" , channelName + ] + track "Recruit Command Invoked" id data + +let recruitLinkButton inviteCode id name channelId channelName = + let data = [ + "user_display_name" , name + "invite_code" , inviteCode + "channel_id" , channelId + "channel_name" , channelName + ] + track "Recruited Command Invoked" id data + +let recruitedCommand totalUsers id name channelId channelName = + let data = [ + "user_display_name" , name + "total_users_at_the_time" , totalUsers + "channel_id" , channelId + "channel_name" , channelName + ] + track "Recruited Command Invoked" id data + +let whiteListButton availability id name = + let data = [ + "user_display_name" , name + "availability" , availability + ] + track "Recruited Command Invoked" id data + +let whiteListPurchased amount id name = + let data = [ + "user_display_name" , name + "purchase_amount" , amount + ] + track "Recruited Command Invoked" id data + +let trainingDojoStep step id name = + let data = [ + "user_display_name" , name + "step" , step + ] + track "Recruited Command Invoked" id data + diff --git a/Bot/Bot.fs b/Bot/Bot.fs index f1db188..b74a054 100644 --- a/Bot/Bot.fs +++ b/Bot/Bot.fs @@ -6,13 +6,10 @@ open DSharpPlus open DSharpPlus.SlashCommands open Degenz open Emzi0767.Utilities -open Mixpanel //open Degenz.SlotMachine type EmptyGlobalCommandToAvoidFamousDuplicateSlashCommandsBug() = inherit ApplicationCommandModule () -let mix = MixpanelClient("SOMETOKEN") - let guild = GuildEnvironment.guildId let hackerBattleConfig = DiscordConfiguration() diff --git a/Bot/Bot.fsproj b/Bot/Bot.fsproj index e99922e..23254fe 100644 --- a/Bot/Bot.fsproj +++ b/Bot/Bot.fsproj @@ -17,6 +17,7 @@ + diff --git a/Bot/Games/Trainer.fs b/Bot/Games/Trainer.fs index 43ec543..d52a01c 100644 --- a/Bot/Games/Trainer.fs +++ b/Bot/Games/Trainer.fs @@ -70,6 +70,7 @@ let handleTrainerStep1 (ctx : IDiscordContext) = .AsEphemeral(true) do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask + do! Analytics.trainingDojoStep "LFG" (ctx.GetDiscordMember().Id) (ctx.GetDiscordMember().Username) } |> Async.StartAsTask :> Task let defend (ctx : IDiscordContext) = @@ -79,6 +80,7 @@ let defend (ctx : IDiscordContext) = let name = if System.String.IsNullOrEmpty m.Nickname then m.DisplayName else m.Nickname let embed = Embeds.pickDefense "Trainer-2" { PlayerData.empty with Inventory = [ Shield defaultShield ] ; Name = name } true do! ctx.FollowUp(embed) |> Async.AwaitTask + do! Analytics.trainingDojoStep "DefendCommand" (ctx.GetDiscordMember().Id) (ctx.GetDiscordMember().Username) } |> Async.StartAsTask :> Task let handleDefenseMsg hackId = { @@ -104,6 +106,7 @@ let handleDefense (ctx : IDiscordContext) = do! sendMessage' $"❌ HACKING FAILED!\n\n{playerName} defended hack from <@{Sensei.Id}>!" do! Async.Sleep 1500 do! sendFollowUpMessageWithButton ctx (handleDefenseMsg defaultHack.Item.Name) + do! Analytics.trainingDojoStep "ShieldActivated" (ctx.GetDiscordMember().Id) (ctx.GetDiscordMember().Username) } |> Async.StartAsTask :> Task let handleTrainerStep3 (ctx : IDiscordContext) = @@ -117,6 +120,7 @@ let handleTrainerStep3 (ctx : IDiscordContext) = + $"Type the `/hack` command now, then choose me - <@{Sensei.Id}> as your target, and select `{defaultHack.Item.Name}`") do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, builder) |> Async.AwaitTask + do! Analytics.trainingDojoStep "LetsHack" (ctx.GetDiscordMember().Id) (ctx.GetDiscordMember().Username) } |> Async.StartAsTask :> Task let hack (target : DiscordUser) (ctx : IDiscordContext) = @@ -138,6 +142,7 @@ let hack (target : DiscordUser) (ctx : IDiscordContext) = .AsEphemeral(true) do! ctx.FollowUp(builder) |> Async.AwaitTask + do! Analytics.trainingDojoStep "HackCommand" (ctx.GetDiscordMember().Id) (ctx.GetDiscordMember().Username) } |> Async.StartAsTask :> Task let handleHack (ctx : IDiscordContext) = @@ -153,7 +158,8 @@ let handleHack (ctx : IDiscordContext) = + "When you **HACK** other Degenz, you **TAKE** their 💰$GBT.\n" + "But remember, hacks take time to recover, so use them wisely.") - do! Async.Sleep 2000 + do! Analytics.trainingDojoStep "HackActivated" (ctx.GetDiscordMember().Id) (ctx.GetDiscordMember().Username) + do! Async.Sleep 1000 let message = $""" @@ -181,6 +187,7 @@ type the `/arsenal` command NOW""" do! ctx.GetDiscordMember().GrantRoleAsync(role) |> Async.AwaitTask do! sendFollowUpMessage ctx ($"Your training is now complete. If you want to buy more **HACKS & SHIELDS**, go to the <#{GuildEnvironment.channelArmory}> and type the `/buy-hack` and `/buy-shield` commands!") + do! Analytics.trainingDojoStep "CompletedNoGifts" (ctx.GetDiscordMember().Id) (ctx.GetDiscordMember().Username) }) let handleArsenal (ctx : IDiscordContext) = PlayerInteractions.executePlayerAction ctx (fun player -> async { @@ -235,6 +242,7 @@ let handleArsenal (ctx : IDiscordContext) = PlayerInteractions.executePlayerActi do! Async.Sleep 1000 do! sendFollowUpMessage ctx $"Now get out of there and go hack other Degenz in the <#{GuildEnvironment.channelBattle}> channel!" + do! Analytics.trainingDojoStep "CompletedWithGifts" (ctx.GetDiscordMember().Id) (ctx.GetDiscordMember().Username) else let role = ctx.GetGuild().GetRole(GuildEnvironment.roleTrainee) do! ctx.GetDiscordMember().RevokeRoleAsync(role) |> Async.AwaitTask diff --git a/Bot/GuildEnvironment.fs b/Bot/GuildEnvironment.fs index 85e0ad4..7f01f0b 100644 --- a/Bot/GuildEnvironment.fs +++ b/Bot/GuildEnvironment.fs @@ -15,11 +15,13 @@ let getId str = getVar str |> uint64 let connectionString = (getVar "DATABASE_URL").Replace("postgresql://", "postgres://").Replace("?sslmode=require", "") let guildId = getId "DISCORD_GUILD" +let environment = getVar "ENVIRONMENT" let tokenPlayerInteractions = getVar "TOKEN_PLAYER_INTERACTIONS" let tokenSteal = getVar "TOKEN_STEAL" let tokenHackerBattle = getVar "TOKEN_HACKER_BATTLE" let tokenStore = getVar "TOKEN_STORE" let tokenInviter = getVar "TOKEN_INVITER" +let tokenMixpanel = getVar "TOKEN_MIXPANEL" let channelEventsHackerBattle = getId "CHANNEL_EVENTS_HACKER_BATTLE" let channelTraining = getId "CHANNEL_TRAINING" let channelArmory = getId "CHANNEL_ARMORY" diff --git a/Bot/InviteTracker.fs b/Bot/InviteTracker.fs index 9d853c0..1004934 100644 --- a/Bot/InviteTracker.fs +++ b/Bot/InviteTracker.fs @@ -199,7 +199,7 @@ let guildInviteEmbed = let button = DiscordButtonComponent(ButtonStyle.Success, $"CreateGuildInvite", $"GET MY UNIQUE LINK") :> DiscordComponent builder.AddComponents [| button |] -let private showInviteMessage (ctx : IDiscordContext) = +let private showInviteMessage (ctx : IDiscordContext) origin = task { let builder = DiscordInteractionResponseBuilder().AsEphemeral(true) do! ctx.Respond(InteractionResponseType.DeferredChannelMessageWithSource, builder) @@ -216,6 +216,7 @@ let private showInviteMessage (ctx : IDiscordContext) = You must **COMPLETE YOUR TRAINING FIRST!** Then you can `/recruit`... Go to <#{GuildEnvironment.channelTraining}> now to become a **HACKER**! """ + do! Analytics.recruitCommand origin player.DiscordId (ctx.GetDiscordMember().Username) (ctx.GetChannel().Id) (ctx.GetChannel().Name) | None -> do! sendFollowUpMessage ctx $"You're not in the game! Go to <#{GuildEnvironment.channelShelters}> NOW to get assigned a private bunk, and **JOIN THE GAME!**" } :> Task @@ -259,6 +260,9 @@ let private getInvitedUsersForId (ctx : IDiscordContext) = task { .AsEphemeral(true) .WithContent(str) do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, msg) + let user = ctx.GetDiscordMember() + let channel = ctx.GetChannel() + do! Analytics.recruitedCommand total user.Id user.Username channel.Id channel.Name } let clearInvites (ctx : IDiscordContext) = task { @@ -281,6 +285,7 @@ let private processNewUser (eventArgs : GuildMemberAddEventArgs) = match result with | Some (_,count) -> if invite.Uses > count then + do! Analytics.invitedUserEntered invite.Code invite.Inviter.Id eventArgs.Member.Id invite.Inviter.Username eventArgs.Member.Username do! addInvitedUser eventArgs.Member.Id invite.Code invite.Uses |> Async.Ignore | None -> () } :> Task @@ -291,36 +296,23 @@ let acceptInvite (ctx : IDiscordContext) (invitedPlayer : PlayerData) = | false -> let! _ = markInvitedAccepted invitedPlayer.DiscordId |> Async.Ignore try - let! inviter = getInviteFromInvitedUser invitedPlayer.DiscordId - let! player = DbService.tryFindPlayer inviter.Inviter + let! invite = getInviteFromInvitedUser invitedPlayer.DiscordId + let! player = DbService.tryFindPlayer invite.Inviter match player with | Some player -> do! DbService.updatePlayerCurrency (int InviteRewardAmount) player |> Async.Ignore let builder = DiscordMessageBuilder() - builder.WithContent($"{invitedPlayer.Name} was recruited to the server. <@{player.DiscordId}> just earned {InviteRewardAmount} 💰$GBT for their efforts!") |> ignore + builder.WithContent($"{invitedPlayer.Name} was recruited and is now a Degen. <@{player.DiscordId}> just earned {InviteRewardAmount} 💰$GBT for their efforts!") |> ignore let channel = ctx.GetGuild().GetChannel(GuildEnvironment.channelEventsHackerBattle) do! channel.SendMessageAsync(builder) |> Async.AwaitTask |> Async.Ignore + do! Analytics.invitedUserAccepted invite.Code player.DiscordId invitedPlayer.DiscordId player.Name invitedPlayer.Name | None -> return () with _ -> () | true -> return () } :> Task -// If we do it like this then there's an obvious exploit where the user can come and go as many times and it will keep -// rewarding GBT. -//let handleGuildMemberRemoved _ (eventArgs : GuildMemberRemoveEventArgs) = -// task { -// do! removeInvitedUser eventArgs.Member.Id -// } :> Task - -//Degenz Game -//Mint Date: April 2022 -//Supply: 3,333 -//Price: 1.984 $SOL - -//Your NFT will be your In-Game Character that provides you with unique traits, and abilities in game. - let sendInitialEmbed (client : DiscordClient) = async { try @@ -435,7 +427,8 @@ let handleGimmeWhitelist (ctx : IDiscordContext) = let recruitBtn = DiscordButtonComponent(ButtonStyle.Danger, $"ShowRecruitmentEmbed", $"Recruit Now") :> DiscordComponent let builder = DiscordFollowupMessageBuilder().AsEphemeral(true) - match! tryGrantWhitelist ctx with + let! availability = tryGrantWhitelist ctx + match availability with | NotAHacker -> whitelistEmbed.Description <- notAHackerMsg | NotInGame -> whitelistEmbed.Description <- notInGameMsg | AlreadyWhitelisted -> @@ -455,6 +448,13 @@ let handleGimmeWhitelist (ctx : IDiscordContext) = whitelistEmbed.Description <- canBuyWhitelistMsg builder.AddEmbed(whitelistEmbed) |> ignore do! ctx.FollowUp(builder) + let availabilityStr = + match availability with + | NotEnoughGBT _ -> "NotEnoughGBT" + | Granted _ -> "Granted" + | _ -> string availability + let user = ctx.GetDiscordMember() + do! Analytics.whiteListButton availabilityStr user.Id user.Username } :> Task let buyWhitelistMsg = $""" @@ -476,10 +476,18 @@ let handleBuyWhitelist (ctx : IDiscordContext) = let builder = DiscordFollowupMessageBuilder().AsEphemeral(true) match! tryGrantWhitelist ctx with - | NotAHacker -> builder.Content <- $"You are somehow not a hacker anymore, what exactly are you doing?" - | NotInGame -> builder.Content <- $"You somehow have left the game, what exactly are you doing?" - | AlreadyWhitelisted -> builder.Content <- "🎉 You're already WHITELISTED!" - | NotEnoughGBT _ -> builder.Content <- $"You somehow do not have enough $GBT, what exactly are you doing?" + | NotAHacker -> + builder.Content <- $"You are somehow not a hacker anymore, what exactly are you doing?" + do! ctx.FollowUp(builder) + | NotInGame -> + builder.Content <- $"You somehow have left the game, what exactly are you doing?" + do! ctx.FollowUp(builder) + | AlreadyWhitelisted -> + builder.Content <- "🎉 You're already WHITELISTED!" + do! ctx.FollowUp(builder) + | NotEnoughGBT _ -> + builder.Content <- $"You somehow do not have enough $GBT, what exactly are you doing?" + do! ctx.FollowUp(builder) | Granted player -> let embed = DiscordEmbedBuilder() embed.Description <- buyWhitelistMsg @@ -491,6 +499,7 @@ let handleBuyWhitelist (ctx : IDiscordContext) = do! ctx.GetDiscordMember().GrantRoleAsync(role) let! _ = DbService.updatePlayerCurrency -WhitelistPrice player builder.AddEmbed(embed) |> ignore + do! ctx.FollowUp(builder) // Send message to hall of privacy let builder = DiscordMessageBuilder() @@ -499,8 +508,9 @@ let handleBuyWhitelist (ctx : IDiscordContext) = do! channel.SendMessageAsync(builder) |> Async.AwaitTask |> Async.Ignore + let user = ctx.GetDiscordMember() + do! Analytics.whiteListPurchased WhitelistPrice user.Id user.Username - do! ctx.FollowUp(builder) } :> Task let handleCreateInvite (ctx : IDiscordContext) = @@ -532,6 +542,7 @@ let handleCreateInvite (ctx : IDiscordContext) = .AsEphemeral(true) do! ctx.FollowUp(msg) + do! Analytics.recruitLinkButton code user.Id user.Username (ctx.GetChannel().Id) (ctx.GetChannel().Name) } :> Task let handleButtonEvent (_ : DiscordClient) (event : ComponentInteractionCreateEventArgs) = @@ -540,7 +551,7 @@ let handleButtonEvent (_ : DiscordClient) (event : ComponentInteractionCreateEv | id when id.StartsWith("GimmeWhitelist") -> handleGimmeWhitelist eventCtx | id when id.StartsWith("BuyWhitelist") -> handleBuyWhitelist eventCtx | id when id.StartsWith("CreateGuildInvite") -> handleCreateInvite eventCtx - | id when id.StartsWith("ShowRecruitmentEmbed") -> showInviteMessage eventCtx + | id when id.StartsWith("ShowRecruitmentEmbed") -> showInviteMessage eventCtx "RecruitButton" | _ -> task { let builder = DiscordInteractionResponseBuilder() @@ -561,7 +572,7 @@ type Inviter() = [] member this.CreateInvite (ctx : InteractionContext) = - showInviteMessage (DiscordInteractionContext ctx) + showInviteMessage (DiscordInteractionContext ctx) "RecruitCommand" [] member this.ListInvitedPeople (ctx : InteractionContext) =