diff --git a/discordbot.go b/discordbot.go new file mode 100644 index 0000000..0515676 --- /dev/null +++ b/discordbot.go @@ -0,0 +1,182 @@ +package main + +import ( + "fmt" + "io" + "log" + "os" + "time" + + "github.com/bwmarrin/discordgo" + "github.com/gin-gonic/gin" + "github.com/rs/zerolog" + "github.com/tr4cks/power/modules" +) + +type DiscordBot struct { + config *DiscordBotConfig + module modules.Module + + logger zerolog.Logger + session *discordgo.Session + registeredCommands []*discordgo.ApplicationCommand +} + +type DiscordBotConfig struct { + BotToken string `yaml:"bot-token" validate:"required"` + GuildId string `yaml:"guild-id"` +} + +func (d *DiscordBot) Start() error { + err := d.session.Open() + if err != nil { + return fmt.Errorf("cannot open the session: %w", err) + } + + d.logger.Info().Msg("Adding commands...") + registeredCommands := make([]*discordgo.ApplicationCommand, len(commands)) + for i, v := range commands { + cmd, err := d.session.ApplicationCommandCreate(d.session.State.User.ID, d.config.GuildId, v) + if err != nil { + d.logger.Panic().Err(err).Msg(fmt.Sprintf("Cannot create '%v' command: %v", v.Name, err)) + } + registeredCommands[i] = cmd + } + d.registeredCommands = registeredCommands + + return nil +} + +func (d *DiscordBot) Stop() { + d.logger.Info().Msg("Removing commands...") + + for _, v := range d.registeredCommands { + err := d.session.ApplicationCommandDelete(d.session.State.User.ID, d.config.GuildId, v.ID) + + if err != nil { + log.Panicf("Cannot delete '%v' command: %v", v.Name, err) + } + } + + err := d.session.Close() + if err != nil { + d.logger.Error().Err(err).Msg("Unable to close the session") + } + + d.logger.Info().Msg("Gracefully shutting down") +} + +func (d *DiscordBot) powerOnHandler(s *discordgo.Session, i *discordgo.InteractionCreate) { + logger := d.logger.With().Str("username", i.Member.User.Username).Logger() + logger.Info().Msg("A user attempts to switch on the server") + + powerState, ledState := d.module.State() + + defer func() { + time.Sleep(10 * time.Second) + s.InteractionResponseDelete(i.Interaction) + }() + + if powerState.Err != nil { + logger.Error().Err(powerState.Err).Msg("Failed to retrieve POWER state") + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Flags: discordgo.MessageFlagsEphemeral, + Content: "A problem occurred when switching on the server", + }, + }) + return + } + if ledState.Err != nil { + logger.Error().Err(ledState.Err).Msg("Failed to retrieve LED state") + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Flags: discordgo.MessageFlagsEphemeral, + Content: "A problem occurred when switching on the server", + }, + }) + return + } + + if powerState.Value || ledState.Value { + logger.Info().Msg("The server is already switched on") + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Flags: discordgo.MessageFlagsEphemeral, + Content: "The server is already switched on", + }, + }) + return + } + + err := d.module.PowerOn() + if err != nil { + logger.Error().Err(err).Msg("A problem occurred when switching on the server") + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Flags: discordgo.MessageFlagsEphemeral, + Content: "A problem occurred when switching on the server", + }, + }) + return + } + logger.Info().Msg("Server switched on") + + s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Flags: discordgo.MessageFlagsEphemeral, + Content: "The server turns on, give it some time...", + }, + }) +} + +var commands = []*discordgo.ApplicationCommand{ + { + Name: "power-on", + Description: "Turns the server on", + }, +} + +func NewDiscordBot(config *DiscordBotConfig, module modules.Module) (*DiscordBot, error) { + var outputWriter io.Writer = os.Stderr + if gin.Mode() != "release" { + outputWriter = zerolog.ConsoleWriter{Out: os.Stderr} + } + logger := zerolog. + New(outputWriter). + With(). + Timestamp(). + Str("scope", "discord"). + Logger() + + session, err := discordgo.New("Bot " + config.BotToken) + if err != nil { + return nil, fmt.Errorf("invalid bot parameters: %w", err) + } + + bot := &DiscordBot{config, module, logger, session, nil} + + commandHandlers := map[string]func(*discordgo.Session, *discordgo.InteractionCreate){ + "power-on": bot.powerOnHandler, + } + + session.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) { + if h, ok := commandHandlers[i.ApplicationCommandData().Name]; ok { + h(s, i) + } + }) + + session.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) { + logger.Info(). + Str("discriminator", s.State.User.Discriminator). + Str("username", s.State.User.Username). + Msg(fmt.Sprintf("Logged in as: %v#%v", s.State.User.Username, s.State.User.Discriminator)) + }) + + return bot, nil +} diff --git a/go.mod b/go.mod index e08bbee..0e669b3 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/tr4cks/power go 1.20 require ( + github.com/bwmarrin/discordgo v0.28.1 github.com/gin-gonic/gin v1.10.0 github.com/go-playground/validator/v10 v10.20.0 github.com/linde12/gowol v0.0.0-20180926075039-797e4d01634c @@ -24,6 +25,7 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.4.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect diff --git a/go.sum b/go.sum index ff15e2d..1c7b7b7 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4= +github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= @@ -31,6 +33,8 @@ github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -90,20 +94,26 @@ github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= diff --git a/main.go b/main.go index 9d77452..ce15f7e 100644 --- a/main.go +++ b/main.go @@ -54,6 +54,7 @@ type Config struct { Username string `validate:"required"` Password string `validate:"required"` Module map[string]interface{} + Discord *DiscordBotConfig } func parseYAMLFile(filePath string) (*Config, error) { @@ -125,6 +126,18 @@ func run(cmd *cobra.Command, args []string) { srv := runHttpServer(config, module) + if config.Discord != nil { + discordBot, err := NewDiscordBot(config.Discord, module) + if err != nil { + mainLogger.Fatal().Err(err).Msg("Unable to create discord bot") + } + err = discordBot.Start() + if err != nil { + mainLogger.Fatal().Err(err).Msg("Unable to start discord bot") + } + defer discordBot.Stop() + } + // Listen for the interrupt signal. <-ctx.Done()