discord-bot-game/Bot/InviteTracker.fs

578 lines
24 KiB
Forth

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<GBT>
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 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
|> 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 {
let! result =
connStr
|> Sql.connect
|> Sql.parameters [ "did" , Sql.string (string userId) ]
|> Sql.query "SELECT accepted FROM invited_user WHERE discord_id = @did"
|> Sql.executeAsync (fun read -> read.bool "accepted")
|> Async.AwaitTask
return List.tryHead result |> Option.defaultValue 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 (inviterId,count) ->
if invite.Uses > count then
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
let acceptInvite (guild : DiscordGuild) (user : DiscordMember) =
task {
match! checkInviteAccepted user.Id with
| false ->
do! markInvitedAccepted user.Id |> Async.Ignore
try
let! invite = getInviteFromInvitedUser user.Id
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($"{user.Username} was recruited and is now a Degen. **{player.Name}** just earned `{InviteRewardAmount} 💰$GBT` for their efforts! - <@!{player.DiscordId}>") |> ignore
let channel = guild.GetChannel(GuildEnvironment.channelEventsHackerBattle)
do! recruitBot.SendMessageAsync(channel, builder)
|> Async.AwaitTask
|> Async.Ignore
}
| None -> async.Return()
let role3x = guild.Roles.TryGetValue(GuildEnvironment.roleRecruiter3x) |> snd
let role2x = guild.Roles.TryGetValue(GuildEnvironment.roleRecruiter2x) |> snd
let role1x = guild.Roles.TryGetValue(GuildEnvironment.roleRecruiter1x) |> snd
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
elif totalInvites >= 5 then
do! [ playerMember.GrantRoleAsync(role2x) ; playerMember.RevokeRoleAsync(role1x) ]
|> List.map Async.AwaitTask
|> Async.Parallel
|> Async.Ignore
else
do! playerMember.GrantRoleAsync(role1x)
do! Analytics.invitedUserAccepted invite.Code player.DiscordId user.Id player.Name user.Username
| 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 {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*
"""
let embed =
DiscordEmbedBuilder()
.WithColor(DiscordColor.Gold)
.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:__ 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 ->
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.Teal)
.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 |> 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) = processNewUser eventArgs
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:__ June 20th
{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 ->
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
}
[<SlashCommand("submit", "Submit your public wallet address")>]
member this.SubmitAddress (ctx : InteractionContext, [<Option("address", "Wallet address")>] 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
// [<SlashCommand("recruited", "Get total invites from a specific user")>]
// member this.ListInvitedPeople (ctx : InteractionContext) =
// let ictx = DiscordInteractionContext ctx :> IDiscordContext
// getInvitedUsersForId (ictx.GetDiscordMember()) ictx
// [<SlashCommand("invites-list", "List all the invites")>]
// member this.ListServerInvites (ctx : InteractionContext) =
// listServerInvites (DiscordInteractionContext ctx)
// [<SlashCommand("invites-clear", "Get total invites from a specific user")>]
// member this.ClearInvites (ctx : InteractionContext) =
// clearInvites (DiscordInteractionContext ctx)