discord-bot-game/Bot/InviteTracker.fs

408 lines
15 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.Rpc
open Solnet.Wallet
let connStr = GuildEnvironment.connectionString
let InviteRewardAmount = 100<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 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 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 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 address = PublicKey(address)
if address.IsValid() && address.IsOnCurve() then
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 ()
[<SlashCommand("submit", "Test something")>]
member this.SubmitAddress (ctx : InteractionContext, [<Option("address", "Wallet address")>] address : string) =
submitWhitelist (DiscordInteractionContext ctx) address
// [<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)