bitbot/bot/bot.go
2024-11-13 16:29:54 +01:00

434 lines
13 KiB
Go

package bot
import (
"bitbot/pb"
"fmt"
"math/rand"
"os"
"os/signal"
"strings"
"github.com/bwmarrin/discordgo"
"github.com/charmbracelet/log"
)
var (
BotToken string
OpenAIToken string
CryptoToken string
AllowedUserID string
AppId string
)
func Run() {
discord, err := discordgo.New("Bot " + BotToken)
if err != nil {
log.Fatal(err)
}
discord.AddHandler(commandHandler)
discord.AddHandler(newMessage)
discord.AddHandler(modalHandler)
log.Info("Opening Discord connection...")
err = discord.Open()
if err != nil {
log.Fatal(err)
}
defer discord.Close()
log.Info("Registering commands...")
registerCommands(discord, AppId)
log.Info("BitBot is running...")
// Try initializing PocketBase after Discord is connected
log.Info("Initializing PocketBase...")
pb.Init()
log.Info("Exiting... press CTRL + c again")
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
<-c
}
var conversationHistoryMap = make(map[string][]map[string]interface{})
var sshConnections = make(map[string]*SSHConnection)
func hasAdminRole(roles []string) bool {
for _, role := range roles {
if role == AllowedUserID {
return true
}
}
return false
}
func newMessage(discord *discordgo.Session, message *discordgo.MessageCreate) {
if message.Author.ID == discord.State.User.ID || message.Content == "" {
return
}
isPrivateChannel := message.GuildID == ""
userID := message.Author.ID
conversationHistory := conversationHistoryMap[userID]
channelID := message.ChannelID
conversationHistory = populateConversationHistory(discord, channelID, conversationHistory)
if strings.HasPrefix(message.Content, "!bit") || isPrivateChannel {
chatGPT(discord, message.ChannelID, conversationHistory)
}
}
func registerCommands(discord *discordgo.Session, appID string) {
commands := []*discordgo.ApplicationCommand{
{Name: "cry", Description: "Get information about cryptocurrency prices."},
{Name: "genkey", Description: "Generate and save SSH key pair."},
{Name: "showkey", Description: "Show the public SSH key."},
{Name: "regenkey", Description: "Regenerate and save SSH key pair."},
{Name: "createevent", Description: "Organize an Ava dungeon raid event."},
{
Name: "ssh",
Description: "Connect to a remote server via SSH.",
Options: []*discordgo.ApplicationCommandOption{
{
Name: "connection_details",
Description: "Connection details in the format username@remote-host:port",
Type: discordgo.ApplicationCommandOptionString,
Required: true,
},
},
},
{
Name: "exe",
Description: "Execute a command on the remote server.",
Options: []*discordgo.ApplicationCommandOption{
{
Name: "command",
Description: "The command to execute.",
Type: discordgo.ApplicationCommandOptionString,
Required: true,
},
},
},
{Name: "exit", Description: "Close the SSH connection."},
{Name: "list", Description: "List saved servers."},
{
Name: "help",
Description: "Show available commands.",
Options: []*discordgo.ApplicationCommandOption{
{
Name: "category",
Description: "Specify 'admin' to view admin commands.",
Type: discordgo.ApplicationCommandOptionString,
Required: false,
},
},
},
{
Name: "roll",
Description: "Roll a random number.",
Options: []*discordgo.ApplicationCommandOption{
{
Name: "max",
Description: "Specify the maximum number for the roll.",
Type: discordgo.ApplicationCommandOptionInteger,
Required: false,
},
},
},
}
for _, cmd := range commands {
_, err := discord.ApplicationCommandCreate(appID, "", cmd)
if err != nil {
log.Fatalf("Cannot create slash command %q: %v", cmd.Name, err)
}
}
}
func commandHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
if i.Type == discordgo.InteractionApplicationCommand {
// Only process application command interactions
data := i.ApplicationCommandData()
switch data.Name {
case "createevent":
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseModal,
Data: &discordgo.InteractionResponseData{
CustomID: "event_modal",
Title: "Create an Ava Dungeon Raid",
Components: []discordgo.MessageComponent{
discordgo.ActionsRow{
Components: []discordgo.MessageComponent{
&discordgo.TextInput{
CustomID: "event_title",
Label: "Event Title",
Style: discordgo.TextInputShort,
Placeholder: "Enter the raid title",
Required: true,
},
},
},
discordgo.ActionsRow{
Components: []discordgo.MessageComponent{
&discordgo.TextInput{
CustomID: "event_date",
Label: "Event Date",
Style: discordgo.TextInputShort,
Placeholder: "e.g., 15-11-2024",
Required: true,
},
},
},
discordgo.ActionsRow{
Components: []discordgo.MessageComponent{
&discordgo.TextInput{
CustomID: "event_time",
Label: "Event Time",
Style: discordgo.TextInputShort,
Placeholder: "e.g., 18:00 UTC",
Required: true,
},
},
},
discordgo.ActionsRow{
Components: []discordgo.MessageComponent{
&discordgo.TextInput{
CustomID: "event_note",
Label: "Additional Notes (optional)",
Style: discordgo.TextInputParagraph,
Placeholder: "Any extra details or instructions",
Required: false,
},
},
},
},
},
})
if err != nil {
log.Printf("Error responding with modal: %v", err)
}
case "cry":
currentCryptoPrice := getCurrentCryptoPrice(data.Options[0].StringValue())
respondWithMessage(s, i, currentCryptoPrice)
case "genkey":
if hasAdminRole(i.Member.Roles) {
err := GenerateAndSaveSSHKeyPairIfNotExist()
response := "SSH key pair generated and saved successfully!"
if err != nil {
response = "Error generating or saving key pair."
}
respondWithMessage(s, i, response)
} else {
respondWithMessage(s, i, "You are not authorized to use this command.")
}
case "showkey":
if hasAdminRole(i.Member.Roles) {
publicKey, err := GetPublicKey()
response := publicKey
if err != nil {
response = "Error fetching public key."
}
respondWithMessage(s, i, response)
} else {
respondWithMessage(s, i, "You are not authorized to use this command.")
}
case "regenkey":
if hasAdminRole(i.Member.Roles) {
err := GenerateAndSaveSSHKeyPair()
response := "SSH key pair regenerated and saved successfully!"
if err != nil {
response = "Error regenerating and saving key pair."
}
respondWithMessage(s, i, response)
} else {
respondWithMessage(s, i, "You are not authorized to use this command.")
}
case "ssh":
if hasAdminRole(i.Member.Roles) {
connectionDetails := data.Options[0].StringValue()
sshConn, err := SSHConnectToRemoteServer(connectionDetails)
response := "Connected to remote server!"
if err != nil {
response = "Error connecting to remote server."
} else {
sshConnections[i.Member.User.ID] = sshConn
serverInfo := &pb.ServerInfo{UserID: i.Member.User.ID, ConnectionDetails: connectionDetails}
err = pb.CreateRecord("servers", serverInfo)
if err != nil {
log.Error(err)
response = "Error saving server information."
}
}
respondWithMessage(s, i, response)
} else {
respondWithMessage(s, i, "You are not authorized to use this command.")
}
case "exe":
if hasAdminRole(i.Member.Roles) {
sshConn, ok := sshConnections[i.Member.User.ID]
if !ok {
respondWithMessage(s, i, "You are not connected to any remote server. Use /ssh first.")
return
}
command := data.Options[0].StringValue()
response, err := sshConn.ExecuteCommand(command)
if err != nil {
response = "Error executing command on remote server."
}
respondWithMessage(s, i, response)
} else {
respondWithMessage(s, i, "You are not authorized to use this command.")
}
case "exit":
if hasAdminRole(i.Member.Roles) {
sshConn, ok := sshConnections[i.Member.User.ID]
if !ok {
respondWithMessage(s, i, "You are not connected to any remote server. Use /ssh first.")
return
}
sshConn.Close()
delete(sshConnections, i.Member.User.ID)
respondWithMessage(s, i, "SSH connection closed.")
} else {
respondWithMessage(s, i, "You are not authorized to use this command.")
}
case "list":
if hasAdminRole(i.Member.Roles) {
servers, err := pb.ListServersByUserID(i.Member.User.ID)
if err != nil || len(servers) == 0 {
respondWithMessage(s, i, "You don't have any servers.")
return
}
var serverListMessage strings.Builder
serverListMessage.WriteString("Recent servers:\n")
for _, server := range servers {
serverListMessage.WriteString(fmt.Sprintf("%s\n", server.ConnectionDetails))
}
respondWithMessage(s, i, serverListMessage.String())
} else {
respondWithMessage(s, i, "You are not authorized to use this command.")
}
case "help":
helpMessage := "Available commands:\n" +
"/cry - Get information about cryptocurrency prices.\n" +
"/roll - Roll a random number.\n" +
"/help - Show available commands.\n"
if len(data.Options) > 0 && data.Options[0].StringValue() == "admin" {
helpMessage += "Admin commands:\n" +
"/genkey - Generate and save SSH key pair.\n" +
"/showkey - Show the public key.\n" +
"/regenkey - Regenerate and save SSH key pair.\n" +
"/ssh - Connect to a remote server via SSH.\n" +
"/exe - Execute a command on the remote server.\n" +
"/exit - Close the SSH connection.\n" +
"/list - List saved servers.\n"
}
respondWithMessage(s, i, helpMessage)
case "roll":
max := 100
if len(data.Options) > 0 {
max = int(data.Options[0].IntValue())
}
result := rand.Intn(max) + 1
respondWithMessage(s, i, fmt.Sprintf("You rolled: %d", result))
}
} else if i.Type == discordgo.InteractionModalSubmit {
// Pass modal submissions to the modalHandler function
modalHandler(s, i)
}
}
func respondWithMessage(s *discordgo.Session, i *discordgo.InteractionCreate, message interface{}) {
var response *discordgo.InteractionResponseData
switch v := message.(type) {
case string:
response = &discordgo.InteractionResponseData{
Content: v,
Flags: discordgo.MessageFlagsEphemeral, // To make it private to the user
}
case *discordgo.MessageSend:
response = &discordgo.InteractionResponseData{
Content: v.Content,
Embeds: v.Embeds,
Flags: discordgo.MessageFlagsEphemeral, // To make it private to the user
}
default:
response = &discordgo.InteractionResponseData{
Content: "Unknown response type.",
Flags: discordgo.MessageFlagsEphemeral, // To make it private to the user
}
}
// Send the response back to the interaction
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: response,
})
}
func modalHandler(s *discordgo.Session, i *discordgo.InteractionCreate) {
if i.Type == discordgo.InteractionModalSubmit && i.ModalSubmitData().CustomID == "event_modal" {
data := i.ModalSubmitData()
// Retrieve values from the modal submission
title := data.Components[0].(*discordgo.ActionsRow).Components[0].(*discordgo.TextInput).Value
date := data.Components[1].(*discordgo.ActionsRow).Components[0].(*discordgo.TextInput).Value
time := data.Components[2].(*discordgo.ActionsRow).Components[0].(*discordgo.TextInput).Value
note := data.Components[3].(*discordgo.ActionsRow).Components[0].(*discordgo.TextInput).Value
// Create the event announcement message
response := " \n"
response += " **Ava Dungeon Raid Event Created!** \n"
response += "**Title**: " + title + "\n"
response += "**Date**: " + date + "\n"
response += "**Time**: " + time + "\n"
if note != "" {
response += "**Note**: " + note
}
// Respond with the event announcement and RSVP buttons
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: response,
Components: []discordgo.MessageComponent{
&discordgo.ActionsRow{
Components: []discordgo.MessageComponent{
&discordgo.Button{
Label: "Coming",
CustomID: "rsvp_coming",
Style: discordgo.PrimaryButton,
},
&discordgo.Button{
Label: "Benched",
CustomID: "rsvp_bench",
Style: discordgo.SecondaryButton,
},
&discordgo.Button{
Label: "Not Coming",
CustomID: "rsvp_not_coming",
Style: discordgo.DangerButton,
},
},
},
},
},
})
if err != nil {
log.Printf("Error responding to modal submission: %v", err)
}
}
}