module Degenz.InviteTracker open System.Text open System.Threading.Tasks open DSharpPlus open DSharpPlus.Entities open DSharpPlus.EventArgs open DSharpPlus.SlashCommands open Degenz.Messaging open Npgsql.FSharp open Solnet.Wallet let connStr = GuildEnvironment.connectionString let InviteRewardAmount = 300 let InviteLinkButtonText = "Get My Invite Link" type Invite = { Code : string Inviter : uint64 Count : int } let private mapInvite (reader : RowReader) = { Code = reader.string "code" Inviter = reader.string "inviter" |> uint64 Count = reader.int "count" } let private getInvites () = async { let! invites = connStr |> Sql.connect |> Sql.query "SELECT code, inviter, count FROM invite" |> Sql.executeAsync mapInvite |> Async.AwaitTask return invites |> List.map (fun inv -> (inv.Code , (inv.Inviter , inv.Count))) |> Map.ofList } let private getInvitesFromUser discordId = async { let! invites = connStr |> Sql.connect |> Sql.parameters [ "did" , Sql.string (string discordId) ] |> Sql.query """ SELECT code, count FROM invite WHERE inviter = @did AND invite.created_at > NOW() at time zone 'utc' - INTERVAL '72 HOURS' """ |> Sql.executeAsync (fun read -> { Code = read.string "code" Inviter = discordId Count = read.int "count" }) |> Async.AwaitTask return invites |> List.map (fun inv -> (inv.Code , (inv.Inviter , inv.Count))) |> Map.ofList } let private createInvite inviter code = connStr |> Sql.connect |> Sql.parameters [ "code" , Sql.string code ; "inviter" , Sql.string (string inviter) ] |> Sql.query "INSERT INTO invite (code, inviter) VALUES (@code, @inviter)" |> 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 markInvitedAccepted did = connStr |> Sql.connect |> Sql.parameters [ "did" , Sql.string (string did) ] |> Sql.query "UPDATE invited_user SET accepted = true, updated_at = timezone('utc'::text, now()) WHERE discord_id = @did" |> Sql.executeNonQueryAsync |> Async.AwaitTask let private getInviteFromInvitedUser invitedUser = connStr |> Sql.connect |> Sql.parameters [ "did" , Sql.string (string invitedUser) ] |> Sql.query """ SELECT code, inviter, count FROM invite JOIN invited_user iu ON invite.id = iu.invite_id WHERE iu.discord_id = @did """ |> Sql.executeRowAsync mapInvite |> Async.AwaitTask let private removeInvitedUser did = try connStr |> Sql.connect |> Sql.parameters [ "did" , Sql.string (string did) ] |> Sql.query "DELETE FROM invited_user WHERE discord_id = @did" |> Sql.executeNonQueryAsync |> Async.AwaitTask |> Async.Ignore with _ -> async.Zero () let private checkUserAlreadyInvited userId = async { let! result = connStr |> Sql.connect |> Sql.parameters [ "did" , Sql.string (string userId) ] |> Sql.query """ SELECT id FROM invited_user WHERE discord_id = @did """ |> Sql.executeAsync (fun read -> read.int "id") |> Async.AwaitTask return List.isEmpty result |> not } 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") |> Async.AwaitTask return result with ex -> printfn "%s %u" ex.Message userId return false } let private getInviteAttributions userId = connStr |> Sql.connect |> Sql.parameters [ "did" , Sql.string (string userId) ] |> Sql.query """ SELECT count(*) FROM invited_user JOIN invite ON invite.id = invited_user.invite_id WHERE invite.inviter = @did AND invited_user.accepted = true; """ |> Sql.executeRowAsync (fun read -> read.int "count") |> Async.AwaitTask let getInvitedUsers userId = connStr |> Sql.connect |> Sql.parameters [ "did" , Sql.string (string userId) ] |> Sql.query """ WITH invite AS (SELECT id FROM invite WHERE inviter = @did) SELECT discord_id FROM invited_user, invite WHERE invite.id = invited_user.invite_id AND invited_user.accepted = true ORDER BY invited_user.updated_at DESC, invited_user.created_at DESC LIMIT 10 """ |> Sql.executeAsync (fun read -> read.string "discord_id" |> uint64) |> Async.AwaitTask let getInvitedUserCount userId = connStr |> Sql.connect |> Sql.parameters [ "did" , Sql.string (string userId) ] |> Sql.query """ WITH invite AS (SELECT id FROM invite WHERE inviter = @did) SELECT count(*) FROM invited_user, invite WHERE invite.id = invited_user.invite_id AND invited_user.accepted = true """ |> Sql.executeRowAsync (fun read -> read.int "count") |> Async.AwaitTask let addWalletAddress (userId : uint64) address = connStr |> Sql.connect |> Sql.parameters [ "did" , Sql.string (string userId) ; "address" , Sql.string address ] |> Sql.query """ UPDATE "user" SET wallet_address = @address WHERE id = @did; """ |> Sql.executeNonQueryAsync |> Async.AwaitTask |> Async.Ignore let getWalletAddress (userId : uint64) = connStr |> Sql.connect |> Sql.parameters [ "did" , Sql.string (string userId) ] |> Sql.query """ SELECT wallet_address FROM "user" WHERE id = @did; """ |> Sql.executeRowAsync (fun reader -> reader.stringOrNone "wallet_address") |> Async.AwaitTask let walletAddressExists (address : string) = async { let! result = connStr |> Sql.connect |> Sql.parameters [ "address" , Sql.string address ] |> Sql.query """ SELECT wallet_address FROM "user" WHERE wallet_address = @address; """ |> Sql.executeAsync (fun reader -> reader.stringOrNone "wallet_address") |> Async.AwaitTask return List.isEmpty result |> not } let private listServerInvites (ctx : IDiscordContext) = task { let! invites = ctx.GetGuild().GetInvitesAsync() let sb = StringBuilder() for invite in invites do sb.AppendLine($"{invite.Inviter.Username} - {invite.Code}") |> ignore let msg = DiscordInteractionResponseBuilder() .AsEphemeral(true) .WithContent("Server Invites\n" + sb.ToString()) do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, msg) } let getAttributions userId (ctx : IDiscordContext) = task { let! total = getInviteAttributions(userId) let msg = DiscordInteractionResponseBuilder() .AsEphemeral(true) .WithContent($"<@{userId}> has invited {total} people") do! ctx.Respond(InteractionResponseType.ChannelMessageWithSource, msg) } :> Task let getInvitedUsersForId (user : DiscordUser) (ctx : IDiscordContext) = task { do! Messaging.defer ctx let! users = getInvitedUsers user.Id let! total = getInvitedUserCount user.Id let sb = StringBuilder() let mutable count = 0 for user in users do count <- count + 1 sb.AppendLine($"{count}.) <@{user}>") |> ignore let msg = let str = if users.Length > 0 then $"**Total Recruited:** `{total} Degenz`\n**Total Earned:** `{total * InviteRewardAmount} 💰$GBT`\n\n**Last 10 users recruited:**\n{sb}" else $"You haven't recruited anyone yet, click the `{InviteLinkButtonText}` button to get your invite link and start recruiting!" DiscordFollowupMessageBuilder() .AsEphemeral(true) .WithContent(str) do! ctx.FollowUp(msg) let user = ctx.GetDiscordMember() do! Analytics.recruitedButton total user.Id user.Username (ctx.GetChannel()) } :> Task let clearInvites (ctx : IDiscordContext) = task { let! invites = ctx.GetGuild().GetInvitesAsync() do! invites |> Seq.map (fun invite -> invite.DeleteAsync() |> Async.AwaitTask) |> Async.Sequential |> Async.Ignore } // Discord doesn't have any way to tell you if the user came via an invite, the only way to tell is to compare the // cached invites in the DB to the ones in the guild and see if any has been incremented let private processNewUser (eventArgs : GuildMemberAddEventArgs) = task { let! guildInvites = eventArgs.Guild.GetInvitesAsync() let! cachedInvites = getInvites () for invite in guildInvites do let result = cachedInvites.TryFind(invite.Code) match result with | Some (_,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 | None -> () } :> Task let acceptInvite (guild : DiscordGuild) discordId memberName = task { match! checkInviteAccepted discordId with | false -> let! _ = markInvitedAccepted discordId |> Async.Ignore try let! invite = getInviteFromInvitedUser discordId let! player = DbService.tryFindPlayer invite.Inviter match player with | Some player -> do! DbService.updatePlayerCurrency InviteRewardAmount player.DiscordId |> Async.Ignore do! match GuildEnvironment.botClientRecruit with | Some recruitBot -> async { let builder = DiscordMessageBuilder() builder.WithContent($"{memberName} was recruited and is now a Degen. <@{player.DiscordId}> just earned {InviteRewardAmount} 💰$GBT for their efforts!") |> ignore let channel = guild.GetChannel(GuildEnvironment.channelEventsHackerBattle) do! recruitBot.SendMessageAsync(channel, builder) |> Async.AwaitTask |> Async.Ignore } | None -> async.Return() do! Analytics.invitedUserAccepted invite.Code player.DiscordId discordId player.Name memberName | None -> return () with _ -> () | true -> return () } :> Task let sendInitialEmbed (ctx : IDiscordContext) = async { try let channel = ctx.GetGuild().GetChannel(GuildEnvironment.channelRecruitment) let rewardMsg = $""" **__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 Friends **__Bonus__** 💰 Earn an extra 300 $GBT for every invite! **Every invite increases your chances of winning* """ let embed = DiscordEmbedBuilder() .WithColor(DiscordColor.CornflowerBlue) .WithDescription(rewardMsg) .WithImageUrl("https://s8.gifyu.com/images/invite-banner-usdcb670496dc3653cb3.png") .WithTitle("Invite Degenz") let builder = DiscordMessageBuilder().AddEmbed(embed) let btn1 = DiscordButtonComponent(ButtonStyle.Success, $"CreateGuildInvite", InviteLinkButtonText) :> DiscordComponent let btn2 = DiscordButtonComponent(ButtonStyle.Primary, $"ShowRecruited", $"Check My Recruits") :> DiscordComponent builder.AddComponents [| btn1 ; btn2 |] |> ignore do! GuildEnvironment.botClientRecruit.Value.SendMessageAsync(channel, builder) |> Async.AwaitTask |> Async.Ignore with e -> printfn $"Error trying to get channel Whitelist\n\n{e.Message}" } |> Async.RunSynchronously let showWalletStatus (ctx : IDiscordContext) = PlayerInteractions.executePlayerAction ctx (fun player -> async { try match! getWalletAddress player.DiscordId with | Some address -> do! Messaging.sendFollowUpMessage ctx $""" 🚀 __Mint Date:__ 31st May, 18:00 UTC ✅ __Status:__ We have successfully received your wallet address: {address}""" | None -> do! Messaging.sendFollowUpMessage ctx "You have no submitted your wallet yet. Type `/submit`, paste your **Solana Wallet Address**, then press enter" with ex -> printfn $"{ex.Message}" do! Messaging.sendFollowUpMessage ctx "Something went wrong retrieving your wallet address" }) let sendSubmitEmbed (ctx : IDiscordContext) = async { try let rewardMsg = $""" To confirm your **Whitelist** please submit it below: 1️⃣ Type `/submit` 2️⃣ Paste your **Wallet Address** 3️⃣ Press `Enter` **Check status anytime to double check it worked*""" let embed = DiscordEmbedBuilder() .WithColor(DiscordColor.White) .WithDescription(rewardMsg) .WithImageUrl("https://s8.gifyu.com/images/whitelist-submit-banner7.png") .WithTitle("Submit Your Solana Wallet Address") let builder = DiscordMessageBuilder().AddEmbed(embed) let btn = DiscordButtonComponent(ButtonStyle.Success, "WalletStatus", "Check Status") :> DiscordComponent builder.AddComponents [| btn |] |> ignore let recruitBot = GuildEnvironment.botClientRecruit.Value let! channel = recruitBot.GetChannelAsync(GuildEnvironment.channelSubmitWallet) |> Async.AwaitTask let! msgs = channel.GetMessagesAsync() |> Async.AwaitTask match msgs |> Seq.tryHead with | Some msg -> if msg.Author.Id = recruitBot.CurrentUser.Id then do! msg.ModifyAsync(builder) |> Async.AwaitTask |> Async.Ignore | None -> do! recruitBot.SendMessageAsync(channel, builder) |> Async.AwaitTask |> Async.Ignore with e -> printfn $"Error trying to get channel Recruit thing\n\n{e.Message}" } |> Async.RunSynchronously let handleCreateInvite (ctx : IDiscordContext) = task { let builder = DiscordInteractionResponseBuilder().AsEphemeral(true) do! ctx.Respond(InteractionResponseType.DeferredChannelMessageWithSource, builder) let user = ctx.GetDiscordMember() let! code = task { let! invites = getInvitesFromUser user.Id match invites |> Map.toList with | [] -> let ( result , channel ) = ctx.GetGuild().Channels.TryGetValue(GuildEnvironment.channelWelcome) if result then let! invite = channel.CreateInviteAsync(max_age = 259200, unique = true) try do! createInvite (ctx.GetDiscordMember().Id) invite.Code |> Async.Ignore with ex -> printfn "%A" ex.Message return invite.Code else printfn "Error finding Welcome channel" return "" | invite::_ -> return invite |> fst } let msg = DiscordFollowupMessageBuilder() .WithContent($"https://discord.gg/{code}") .AsEphemeral(true) do! ctx.FollowUp(msg) do! Analytics.recruitLinkButton code user.Id user.Username (ctx.GetChannel()) } :> Task let handleMemberUpdated (client : DiscordClient) (event : GuildMemberUpdateEventArgs) = let addedRole (rolesBefore : DiscordRole seq) (rolesAfter : DiscordRole seq) = rolesAfter |> Seq.filter ((fun role -> rolesBefore |> Seq.exists (fun r -> role.Id = r.Id)) >> not) task { let symmetricDifference = addedRole event.RolesBefore event.RolesAfter |> Seq.toList match symmetricDifference with | [] -> () | role::_ -> if role.Name = "Degen" then let (_,guild) = client.Guilds.TryGetValue(GuildEnvironment.guildId) do! acceptInvite guild event.Member.Id event.Member.Username |> Async.AwaitTask return () } :> Task let handleMessageCreated _ (event : MessageCreateEventArgs) = task { let bot = GuildEnvironment.botClientRecruit.Value if event.Channel.Id = GuildEnvironment.channelSubmitWallet && event.Author.Id <> bot.CurrentUser.Id then do! Async.Sleep 100 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 submitAddress (address : string) (ctx : IDiscordContext) = PlayerInteractions.executePlayerAction ctx (fun player -> async { let pubkey = PublicKey(address) try if pubkey.IsValid() && pubkey.IsOnCurve() then let! maybeAddress = getWalletAddress player.DiscordId let msg = match maybeAddress with | Some storedAddress when storedAddress = address -> "You already provided this wallet address:" | Some _ -> "We successfully updated your wallet address:" | None -> "We have successfully received your wallet address" do! addWalletAddress (ctx.GetDiscordMember().Id) address let user = ctx.GetDiscordMember() if ctx.GetDiscordMember().Roles |> Seq.exists (fun role -> role.Id = GuildEnvironment.roleWhitelistPending) then let role = ctx.GetGuild().GetRole(GuildEnvironment.roleWhitelist) do! user.GrantRoleAsync(role) |> Async.AwaitTask let role = ctx.GetGuild().GetRole(GuildEnvironment.roleWhitelistPending) do! user.RevokeRoleAsync(role) |> Async.AwaitTask if ctx.GetDiscordMember().Roles |> Seq.exists (fun role -> role.Id = GuildEnvironment.roleWhiteOGPending) then let role = ctx.GetGuild().GetRole(GuildEnvironment.roleWhiteOG) 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:__ 31st May 18:00 UTC ✅ {msg} {address} Keep an eye on <#{GuildEnvironment.channelAnnouncements}> for updates.""" let builder = DiscordMessageBuilder() builder.WithContent($"{ctx.GetDiscordMember().Username} submitted their wallet address in <#{GuildEnvironment.channelSubmitWallet}> and confirmed whitelist") |> ignore let channel = (ctx.GetGuild().GetChannel(GuildEnvironment.channelEventsHackerBattle)) do! channel.SendMessageAsync(builder) |> Async.AwaitTask |> Async.Ignore do! Analytics.walletSubmit (ctx.GetDiscordMember()) else do! Messaging.sendFollowUpMessage ctx "⚠️ That's not a valid Solana address, please try again" do! Analytics.invalidWalletSubmit (ctx.GetDiscordMember()) with ex -> printfn $"{ex.Message}" do! Messaging.sendFollowUpMessage ctx "⚠️ That's not a valid Solana address, please try again" do! Analytics.invalidWalletSubmit (ctx.GetDiscordMember()) }) type Inviter() = inherit ApplicationCommandModule () let enforceChannel (ctx : IDiscordContext) (fn : IDiscordContext -> Task) = match ctx.GetChannel().Id with | id when id = GuildEnvironment.channelSubmitWallet -> fn ctx | _ -> task { let msg = $"You must go to <#{GuildEnvironment.channelSubmitWallet}> channel to submit your wallet" do! Messaging.sendSimpleResponse ctx msg } [] member this.SubmitAddress (ctx : InteractionContext, [] address : string) = let isWhitelist (role : DiscordRole) = role.Id = GuildEnvironment.roleWhitelistPending || role.Id = GuildEnvironment.roleWhiteOGPending || role.Id = GuildEnvironment.roleWhitelist || role.Id = GuildEnvironment.roleWhiteOG if ctx.Member.Roles |> Seq.exists isWhitelist then enforceChannel (DiscordInteractionContext ctx) (submitAddress address) else let msg = $"You currently are not Whitelisted, go to <#{GuildEnvironment.channelWhitelist}> to purchase the role!" Messaging.sendSimpleResponse (DiscordInteractionContext ctx) msg |> Async.StartAsTask :> Task // [] // member this.ListInvitedPeople (ctx : InteractionContext) = // let ictx = DiscordInteractionContext ctx :> IDiscordContext // getInvitedUsersForId (ictx.GetDiscordMember()) ictx // [] // member this.ListServerInvites (ctx : InteractionContext) = // listServerInvites (DiscordInteractionContext ctx) // [] // member this.ClearInvites (ctx : InteractionContext) = // clearInvites (DiscordInteractionContext ctx)