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.Rpc open Solnet.Wallet let connStr = GuildEnvironment.connectionString let InviteRewardAmount = 100 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 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 $2,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 **__Bonus__** 💰 Earn an extra 100 $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-usdc.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 channel = ctx.GetGuild().GetChannel(GuildEnvironment.channelSubmitWallet) let rewardMsg = $"Instructions for submitting wallet" let embed = DiscordEmbedBuilder() .WithColor(DiscordColor.White) .WithDescription(rewardMsg) .WithImageUrl("https://s8.gifyu.com/images/whitelist-submit-banner22.png") .WithTitle("Submit Wallet") let builder = DiscordMessageBuilder().AddEmbed(embed) let btn = DiscordButtonComponent(ButtonStyle.Success, "WalletStatus", "Check Status") :> DiscordComponent builder.AddComponents [| btn |] |> 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 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 handleGuildMemberAdded _ (eventArgs : GuildMemberAddEventArgs) = task { let! exists = checkUserAlreadyInvited eventArgs.Member.Id if not exists then do! processNewUser eventArgs } :> Task let submitWhitelist (ctx : IDiscordContext) (address : string) = task { // BtshZ7oNB5tk5pVbDpsRCziZ1qwV7SMCJq1Pe3YbHZuo let pubkey = PublicKey(address) if pubkey.IsValid() && pubkey.IsOnCurve() then do! addWalletAddress (ctx.GetDiscordMember().Id) address do! Messaging.sendSimpleResponse ctx "You provided a valid address" else do! Messaging.sendSimpleResponse ctx "This address is not valid" return () } :> Task type Inviter() = inherit ApplicationCommandModule () [] member this.SubmitAddress (ctx : InteractionContext, [] address : string) = submitWhitelist (DiscordInteractionContext ctx) address // [] // 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)