From 49485129f74e63e63cbe27a0828b91cb4bf90153 Mon Sep 17 00:00:00 2001 From: Tommy Stigen Olsen Date: Wed, 17 Jul 2024 14:56:53 +0200 Subject: [PATCH] Initial commit --- .gitignore | 1 + .idea/.gitignore | 8 +++ .idea/go-discord-bot.iml | 10 +++ .idea/misc.xml | 6 ++ .idea/modules.xml | 8 +++ .idea/vcs.xml | 6 ++ cmd/bot/main.go | 62 ++++++++++++++++++ go.mod | 27 ++++++++ go.sum | 63 ++++++++++++++++++ lib/bot/bot.go | 137 +++++++++++++++++++++++++++++++++++++++ lib/commands/commands.go | 18 +++++ lib/commands/price.go | 69 ++++++++++++++++++++ lib/commands/stock.go | 47 ++++++++++++++ lib/helpers/conv.go | 23 +++++++ lib/helpers/db.go | 59 +++++++++++++++++ lib/helpers/import.go | 55 ++++++++++++++++ lib/helpers/pointers.go | 28 ++++++++ lib/logging/db.go | 39 +++++++++++ lib/logging/init.go | 18 +++++ lib/models/commodity.go | 35 ++++++++++ lib/models/user.go | 4 ++ 21 files changed, 723 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/go-discord-bot.iml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 cmd/bot/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 lib/bot/bot.go create mode 100644 lib/commands/commands.go create mode 100644 lib/commands/price.go create mode 100644 lib/commands/stock.go create mode 100644 lib/helpers/conv.go create mode 100644 lib/helpers/db.go create mode 100644 lib/helpers/import.go create mode 100644 lib/helpers/pointers.go create mode 100644 lib/logging/db.go create mode 100644 lib/logging/init.go create mode 100644 lib/models/commodity.go create mode 100644 lib/models/user.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..11b14a1 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.env* \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/go-discord-bot.iml b/.idea/go-discord-bot.iml new file mode 100644 index 0000000..25ed3f6 --- /dev/null +++ b/.idea/go-discord-bot.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..639900d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..c3ced53 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/cmd/bot/main.go b/cmd/bot/main.go new file mode 100644 index 0000000..866fe9b --- /dev/null +++ b/cmd/bot/main.go @@ -0,0 +1,62 @@ +package main + +import ( + "code.lowsec.club/okawari/go-discord-bot/lib/bot" + "code.lowsec.club/okawari/go-discord-bot/lib/commands" + "code.lowsec.club/okawari/go-discord-bot/lib/helpers" + "code.lowsec.club/okawari/go-discord-bot/lib/logging" + "context" + "github.com/bwmarrin/discordgo" + "github.com/joho/godotenv" + "github.com/rs/zerolog/log" + "gorm.io/gorm" + "os" + "os/signal" +) + +var discord *discordgo.Session + +func main() { + _ = logging.InitLogging() + err := godotenv.Load() + if err != nil { + log.Err(err).Msg("Unable to load dot env files") + } + + log.Info().Msg("Starting discord bot") + + ctx := context.Background() + + db, err := helpers.CreateConnection(&gorm.Config{ + Logger: logging.DBLogger{Logger: log.With().Str("module", "sql").Logger()}, + }) + + if err != nil { + log.Panic().Str("module", "sql").Err(err).Msg("unable to connect to database") + } + + helpers.AutoMigrate(db) + helpers.ImportFromFio(db) + + ctx = helpers.AddDb(ctx, db) + discord, err := bot.New(ctx, "trader", os.Getenv("DISCORD_TOKEN")) + if err != nil { + log.Err(err).Msg("Unable to create discord bot") + } + + discord.AddCommand(commands.StockCommand{}, true) + discord.AddCommand(commands.PriceCommand{}, true) + + err = discord.Open() + if err != nil { + log.Fatal().Err(err).Msg("Unable to start discord session") + } + defer discord.Close() + + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt) + log.Info().Msg("Press Ctrl+C to exit") + <-stop + + log.Info().Msg("Shutting down") +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..5420d1d --- /dev/null +++ b/go.mod @@ -0,0 +1,27 @@ +module code.lowsec.club/okawari/go-discord-bot + +go 1.22.5 + +require github.com/rs/zerolog v1.33.0 + +require ( + github.com/bwmarrin/discordgo v0.28.1 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.6.0 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect + golang.org/x/crypto v0.25.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect + gorm.io/driver/postgres v1.5.9 // indirect + gorm.io/driver/sqlite v1.5.6 // indirect + gorm.io/gorm v1.25.11 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..06e0f51 --- /dev/null +++ b/go.sum @@ -0,0 +1,63 @@ +github.com/bwmarrin/discordgo v0.28.1 h1:gXsuo2GBO7NbR6uqmrrBDplPUx2T3nzu775q/Rd1aG4= +github.com/bwmarrin/discordgo v0.28.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +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/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.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.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8= +gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= +gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE= +gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= +gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg= +gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= diff --git a/lib/bot/bot.go b/lib/bot/bot.go new file mode 100644 index 0000000..60c3632 --- /dev/null +++ b/lib/bot/bot.go @@ -0,0 +1,137 @@ +package bot + +import ( + "code.lowsec.club/okawari/go-discord-bot/lib/commands" + "context" + "fmt" + "github.com/bwmarrin/discordgo" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +type Bot struct { + discord *discordgo.Session + logger zerolog.Logger + guildId string + commands []struct { + Command *commands.SlashCommand + removeOnClose bool + } + registeredCmds []*discordgo.ApplicationCommand + ctx context.Context +} + +func (b *Bot) AddCommand(command commands.SlashCommand, removeOnClose bool) *Bot { + b.commands = append(b.commands, struct { + Command *commands.SlashCommand + removeOnClose bool + }{Command: &command, removeOnClose: removeOnClose}) + + return b +} + +func (b *Bot) AddCommands(commands []commands.SlashCommand, removeOnClose bool) *Bot { + for _, cmd := range commands { + b.AddCommand(cmd, removeOnClose) + } + + return b +} + +func (b *Bot) registerCommand(command commands.SlashCommand, removeOnClose bool) error { + acc := command.CreateCommand() + logger := b.logger.With().Str("cmd", acc.Name).Logger() + + registeredCmd, err := b.discord.ApplicationCommandCreate(b.discord.State.User.ID, b.guildId, acc) + if err != nil { + return err + } + logger.Debug().Msg("Command registered") + + if removeOnClose == true { + b.registeredCmds = append(b.registeredCmds, registeredCmd) + } + + b.discord.AddHandler(func(s *discordgo.Session, r *discordgo.InteractionCreate) { + logger = logger.With(). + Str("user", r.Member.Nick). + Str("component", "Handler"). + Logger() + if r.ApplicationCommandData().ID == registeredCmd.ID { + ctx := logger.WithContext(b.ctx) + command.Handle(ctx, s, r) + } + }) + + return nil +} + +func (b *Bot) Open() error { + b.discord.AddHandler(b.onReady()) + return b.discord.Open() +} + +func (b *Bot) onReady() func(s *discordgo.Session, r *discordgo.Ready) { + return func(s *discordgo.Session, r *discordgo.Ready) { + log.Info(). + Str("user", fmt.Sprintf("%v#%v", s.State.User.Username, s.State.User.Discriminator)). + Msg("Logged inn") + + log.Info().Int("commands", len(b.commands)).Msg("Registering commands") + for _, cmd := range b.commands { + err := b.registerCommand(*cmd.Command, cmd.removeOnClose) + if err != nil { + b.logger.Fatal().Err(err).Msg("Unable to register command") + } + } + } +} + +func (b *Bot) Close() { + b.logger.Debug().Msg("Shutting down") + + b.logger.Debug().Int("commands", len(b.registeredCmds)).Msg("Registered commands") + + for _, cmd := range b.registeredCmds { + log.Info(). + Str("name", cmd.Name). + Msg("Unregistering command") + + err := b.discord.ApplicationCommandDelete(b.discord.State.User.ID, b.guildId, cmd.ID) + if err != nil { + log.Error(). + Err(err). + Str("name", cmd.Name).Msg("Failed to unregister command") + } + } + + err := b.discord.Close() + if err != nil { + b.logger.Error().Err(err).Msg("Unable to close discord") + } +} + +func New(ctx context.Context, name string, token string) (*Bot, error) { + + discord, err := discordgo.New("Bot " + token) + if err != nil { + return nil, fmt.Errorf("unable to create discord bot: %s", err) + } + + return (&Bot{ + discord: discord, + logger: log.With().Str("bot", name).Logger(), + ctx: ctx, + guildId: "758249214160797706", + commands: make([]struct { + Command *commands.SlashCommand + removeOnClose bool + }, 0), + registeredCmds: make([]*discordgo.ApplicationCommand, 0), + }), nil +} + +func (b *Bot) init() *Bot { + + return b +} diff --git a/lib/commands/commands.go b/lib/commands/commands.go new file mode 100644 index 0000000..82639d8 --- /dev/null +++ b/lib/commands/commands.go @@ -0,0 +1,18 @@ +package commands + +import ( + "context" + "github.com/bwmarrin/discordgo" +) + +type SlashCommand interface { + CreateCommand() *discordgo.ApplicationCommand + Handle(ctx context.Context, session *discordgo.Session, command *discordgo.InteractionCreate) +} + +func Commands() []SlashCommand { + return []SlashCommand{ + StockCommand{}, + PriceCommand{}, + } +} diff --git a/lib/commands/price.go b/lib/commands/price.go new file mode 100644 index 0000000..e4ce0be --- /dev/null +++ b/lib/commands/price.go @@ -0,0 +1,69 @@ +package commands + +import ( + "code.lowsec.club/okawari/go-discord-bot/lib/helpers" + "context" + "fmt" + "github.com/bwmarrin/discordgo" + "github.com/rs/zerolog/log" + "strings" +) + +type PriceCommand struct { +} + +func (p PriceCommand) CreateCommand() *discordgo.ApplicationCommand { + return &discordgo.ApplicationCommand{ + ID: "price", + Name: "price", + Description: "Change stock of traded commodity", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: "commodity", + Description: "Ticker of wanted commodity", + Required: true, + MinLength: helpers.Pointer(1), + MaxLength: 3, + }, + { + Type: discordgo.ApplicationCommandOptionString, + Name: "price", + Description: "Price of commodity in stock", + Required: true, + MinValue: helpers.Pointer(0.0), + }, + }, + } +} + +func (p PriceCommand) Handle(ctx context.Context, session *discordgo.Session, command *discordgo.InteractionCreate) { + logger := log.Ctx(ctx) + db := helpers.DB(ctx) + + commodityString := strings.ToUpper(command.ApplicationCommandData().Options[0].StringValue()) + price, err := helpers.ParseNumericString(command.ApplicationCommandData().Options[1].StringValue()) + + if err != nil { + helpers.InteractionErrorResponse(session, command, logger, err) + return + } + + logger.Info().Str("commodity", commodityString).Float64("price", price).Msg("Got price change request") + + _, exists, err := helpers.FindCommodity(db, commodityString) + if err != nil { + helpers.InteractionErrorResponse(session, command, logger, err) + return + } + + if !exists { + helpers.InteractionErrorResponse(session, command, logger, fmt.Errorf("Can't find any commodities by the name `%s`", commodityString)) + return + } + + err = session.InteractionRespond(command.Interaction, helpers.EphemeralResponse("%s", "test")) + if err != nil { + logger.Error().Str("user", command.User.Username).Err(err).Msg("Unable to respond to request from user") + } +} diff --git a/lib/commands/stock.go b/lib/commands/stock.go new file mode 100644 index 0000000..b6dcec9 --- /dev/null +++ b/lib/commands/stock.go @@ -0,0 +1,47 @@ +package commands + +import ( + "code.lowsec.club/okawari/go-discord-bot/lib/helpers" + "context" + "github.com/bwmarrin/discordgo" + "github.com/rs/zerolog/log" +) + +type StockCommand struct{} + +func (s StockCommand) CreateCommand() *discordgo.ApplicationCommand { + return &discordgo.ApplicationCommand{ + ID: "stock", + Name: "stock", + Description: "Change stock of traded commodity", + Options: []*discordgo.ApplicationCommandOption{ + { + Type: discordgo.ApplicationCommandOptionString, + Name: "commodity", + Description: "Ticker of wanted commodity", + Required: true, + MinLength: helpers.Pointer(1), + MaxLength: 3, + }, + { + Type: discordgo.ApplicationCommandOptionInteger, + Name: "amount", + Description: "Amount of commodity in stock", + Required: true, + MinValue: helpers.Pointer(0.0), + }, + }, + } +} + +func (s StockCommand) Handle(ctx context.Context, session *discordgo.Session, command *discordgo.InteractionCreate) { + logger := log.Ctx(ctx) + err := session.InteractionRespond(command.Interaction, helpers.EphemeralResponse("Surprise 2!")) + if err != nil { + logger.Error().Str("user", command.User.Username).Err(err).Msg("Unable to respond to request from user") + } +} + +func NewStockCommand() StockCommand { + return StockCommand{} +} diff --git a/lib/helpers/conv.go b/lib/helpers/conv.go new file mode 100644 index 0000000..e9fc77a --- /dev/null +++ b/lib/helpers/conv.go @@ -0,0 +1,23 @@ +package helpers + +import ( + "fmt" + "strconv" + "strings" +) + +func ParseNumericString(value string) (float64, error) { + + value = strings.Replace(value, " ", "", 9999) + value = strings.Replace(value, ",", ".", 9999) + + out, err := strconv.ParseFloat(value, 64) + if err != nil { + result, err := strconv.ParseInt(value, 10, 64) + out = float64(result) + if err != nil { + return 0.0, fmt.Errorf("not numeric") + } + } + return out, nil +} diff --git a/lib/helpers/db.go b/lib/helpers/db.go new file mode 100644 index 0000000..d1b6cc1 --- /dev/null +++ b/lib/helpers/db.go @@ -0,0 +1,59 @@ +package helpers + +import ( + "code.lowsec.club/okawari/go-discord-bot/lib/models" + "context" + "github.com/rs/zerolog/log" + "gorm.io/driver/postgres" + "gorm.io/gorm" + "os" +) + +func CreateConnection(config *gorm.Config) (*gorm.DB, error) { + log.Debug().Msg("Connecting to DB") + return gorm.Open(postgres.Open(os.Getenv("DATABASE_URL")), config) +} + +func AddDb(ctx context.Context, db *gorm.DB) context.Context { + return context.WithValue(ctx, "db", db) +} + +func DB(ctx context.Context) *gorm.DB { + if val, ok := ctx.Value("db").(*gorm.DB); ok { + return val + } + log.Fatal().Msg("Could not find DB object") + return nil +} + +func AutoMigrate(db *gorm.DB) { + hasError := false + + logger := log.With().Str("module", "sql-migrator").Logger() + + for _, model := range []any{ + models.Commodity{}, + } { + err := db.AutoMigrate(model) + + if err != nil { + logger.Error().Err(err).Interface("model", model).Msg("Failed to auto migrate") + hasError = true + } + } + + if hasError { + log.Panic().Msg("Exiting") + } + +} + +func FindCommodity(db *gorm.DB, term string) (*models.Commodity, bool, error) { + result := models.Commodity{} + tx := db.Model(models.Commodity{}).Where("ticker = ?", term).First(&result) + + if tx.Error != nil { + return nil, false, tx.Error + } + return &result, tx.RowsAffected > 0, nil +} diff --git a/lib/helpers/import.go b/lib/helpers/import.go new file mode 100644 index 0000000..86cde05 --- /dev/null +++ b/lib/helpers/import.go @@ -0,0 +1,55 @@ +package helpers + +import ( + "bytes" + "code.lowsec.club/okawari/go-discord-bot/lib/models" + "encoding/json" + "github.com/rs/zerolog/log" + "gorm.io/gorm" + "net/http" +) + +func GetPayloadFromFIO() ([]models.FIOCommodity, error) { + client := http.Client{} + + body := bytes.NewBufferString("") + request, err := http.NewRequest("GET", "https://rest.fnar.net/material/allmaterials", body) + if err != nil { + return []models.FIOCommodity{}, err + } + resp, err := client.Do(request) + results := make([]models.FIOCommodity, 0) + err = json.NewDecoder(resp.Body).Decode(&results) + if err != nil { + return []models.FIOCommodity{}, err + } + return results, nil +} + +func ImportFromFio(db *gorm.DB) error { + + var count int64 = 0 + db.Table("commodities").Count(&count) + + if count > 0 { + return nil + } + + items, err := GetPayloadFromFIO() + if err != nil { + log.Panic().Err(err).Msg("Couldnt get payload from fio") + } + + for _, item := range items { + log.Info().Str("ticker", item.Ticker).Msg("Importing item") + + com := models.Commodity{} + db.Where(models.Commodity{Ticker: item.Ticker}).Assign(item.ToCommodity()).FirstOrCreate(&com) + + } + + count = 0 + db.Table("commodities").Count(&count) + log.Info().Int64("count", count).Msg("Counting rows in commodities") + return nil +} diff --git a/lib/helpers/pointers.go b/lib/helpers/pointers.go new file mode 100644 index 0000000..47af8e0 --- /dev/null +++ b/lib/helpers/pointers.go @@ -0,0 +1,28 @@ +package helpers + +import ( + "fmt" + "github.com/bwmarrin/discordgo" + "github.com/rs/zerolog" +) + +func Pointer[T any](v T) *T { + return &v +} + +func EphemeralResponse(format string, any ...any) *discordgo.InteractionResponse { + return &discordgo.InteractionResponse{ + Type: discordgo.InteractionResponseChannelMessageWithSource, + Data: &discordgo.InteractionResponseData{ + Flags: discordgo.MessageFlagsEphemeral, + Content: fmt.Sprintf(format, any...), + }, + } +} + +func InteractionErrorResponse(session *discordgo.Session, command *discordgo.InteractionCreate, logger *zerolog.Logger, err error) { + err = session.InteractionRespond(command.Interaction, EphemeralResponse("%s", err)) + if err != nil { + logger.Error().Err(err).Msg("Unable to respond to request from user") + } +} diff --git a/lib/logging/db.go b/lib/logging/db.go new file mode 100644 index 0000000..664c923 --- /dev/null +++ b/lib/logging/db.go @@ -0,0 +1,39 @@ +package logging + +import ( + "context" + "github.com/rs/zerolog" + "gorm.io/gorm/logger" + "time" +) + +type DBLogger struct { + Logger zerolog.Logger +} + +func (D DBLogger) LogMode(_ logger.LogLevel) logger.Interface { + return D +} + +func (D DBLogger) Info(ctx context.Context, s string, i ...interface{}) { + D.Logger.Info().Ctx(ctx).Msgf(s, i...) +} + +func (D DBLogger) Warn(ctx context.Context, s string, i ...interface{}) { + D.Logger.Warn().Ctx(ctx).Msgf(s, i...) +} + +func (D DBLogger) Error(ctx context.Context, s string, i ...interface{}) { + D.Logger.Error().Ctx(ctx).Msgf(s, i...) +} + +func (D DBLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) { + tupl, rows := fc() + D.Logger.Trace().Ctx(ctx). + Time("begin", begin). + Err(err). + Str("sql", tupl). + Int64("rows", rows). + Msg("Query performed") + +} diff --git a/lib/logging/init.go b/lib/logging/init.go new file mode 100644 index 0000000..16eb273 --- /dev/null +++ b/lib/logging/init.go @@ -0,0 +1,18 @@ +package logging + +import ( + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "os" +) + +func InitLogging() error { + + switch os.Getenv("LOG_FORMAT") { + + default: + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: zerolog.TimeFieldFormat}) + } + + return nil +} diff --git a/lib/models/commodity.go b/lib/models/commodity.go new file mode 100644 index 0000000..552494a --- /dev/null +++ b/lib/models/commodity.go @@ -0,0 +1,35 @@ +package models + +import "gorm.io/gorm" + +type Commodity struct { + gorm.Model + + Ticker string `gorm:"index,unique"` + Name string + + Weight float64 + Volume float64 + + Category string +} + +type FIOCommodity struct { + MaterialId string `json:"MaterialId"` + Ticker string `json:"Ticker"` + Name string `json:"Name"` + Category string `json:"CategoryName"` + + Volume float64 `json:"Volume"` + Weight float64 `json:"Weight"` +} + +func (C FIOCommodity) ToCommodity() Commodity { + return Commodity{ + Ticker: C.Ticker, + Category: C.Category, + Weight: C.Weight, + Volume: C.Volume, + Name: C.Name, + } +} diff --git a/lib/models/user.go b/lib/models/user.go new file mode 100644 index 0000000..7179e22 --- /dev/null +++ b/lib/models/user.go @@ -0,0 +1,4 @@ +package models + +type User struct { +}