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) } } }