working version of Discord app

absences
William Perron 2 years ago
parent 2f09527677
commit 601aa1076b

@ -1,5 +1,257 @@
package main
import (
"context"
"errors"
"flag"
"fmt"
"log"
"os"
"os/signal"
"strings"
"syscall"
"github.com/bwmarrin/discordgo"
"go.wperron.io/themis"
)
const (
DB_FILE = "prod.db"
CONN_STRING = "file:" + DB_FILE + "?cache=shared&mode=rw&_journal_mode=WAL"
DISCORD_APP_ID = "1014881815921705030"
DISCORD_GUILD_ID = "1014883118764806164"
)
var (
dbFile = flag.String("db", "", "SQlite database file path")
store *themis.Store
)
var ()
func main() {
panic("not implemented")
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGKILL, syscall.SIGINT)
defer cancel()
flag.Parse()
err := touchDbFile(*dbFile)
if err != nil {
log.Fatalln("fatal error: failed to touch database file:", err)
}
store, err = themis.NewStore(CONN_STRING)
if err != nil {
log.Fatalln("fatal error: failed to initialize database:", err)
}
authToken, ok := os.LookupEnv("DISCORD_TOKEN")
if !ok {
log.Fatalln("fatal error: no auth token found at DISCORD_TOKEN env var")
}
discord, err := discordgo.New(fmt.Sprintf("Bot %s", authToken))
if err != nil {
log.Fatalln("fatal error: failed to create discord app:", err)
}
commands := []*discordgo.ApplicationCommand{
{
Name: "themis",
Description: "Call dibs on EU4 provinces",
Type: discordgo.ChatApplicationCommand,
Options: []*discordgo.ApplicationCommandOption{
{
Name: "ping",
Description: "Ping Themis",
Type: discordgo.ApplicationCommandOptionSubCommand,
},
{
Name: "list-claims",
Description: "List current claims",
Type: discordgo.ApplicationCommandOptionSubCommand,
},
{
Name: "claim",
Description: "Take a claim on provinces",
Type: discordgo.ApplicationCommandOptionSubCommand,
Options: []*discordgo.ApplicationCommandOption{
{
Name: "claim-type",
Description: "one of `area`, `region` or `trade`",
Type: discordgo.ApplicationCommandOptionString,
},
{
Name: "name",
Description: "the name of zone claimed",
Type: discordgo.ApplicationCommandOptionString,
},
},
},
},
},
}
handlers := map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){
"themis": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
options := i.ApplicationCommandData().Options
switch options[0].Name {
case "ping":
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "Pong",
},
})
if err != nil {
log.Println("[error] failed to respond to command:", err)
}
case "list-claims":
claims, err := store.ListClaims(ctx)
if err != nil {
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "Oops, something went wrong! :(",
},
})
if err != nil {
log.Println("[error] failed to respond to command:", err)
}
}
sb := strings.Builder{}
sb.WriteString(fmt.Sprintf("There are currently %d claims:\n", len(claims)))
for _, c := range claims {
sb.WriteString(fmt.Sprintf("%s\n", c))
}
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: sb.String(),
},
})
if err != nil {
log.Println("[error] failed to respond to command:", err)
}
case "claim":
opts := options[0].Options
claimType, err := themis.ClaimTypeFromString(opts[0].StringValue())
if err != nil {
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "You can only take claims of types `area`, `region` or `trade`",
},
})
if err != nil {
log.Println("[error] failed to respond to command:", err)
}
return
}
name := opts[1].StringValue()
player := i.Member.Nick
if player == "" {
player = i.Member.User.Username
}
err = store.Claim(ctx, player, name, claimType)
if err != nil {
fmt.Printf("[error]: failed to acquire claim: %s\n", err)
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "failed to acquire claim :(",
},
})
if err != nil {
log.Println("[error] failed to respond to command:", err)
}
return
}
err = s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: fmt.Sprintf("Claimed %s for %s!", name, player),
},
})
if err != nil {
log.Println("[error] failed to respond to command:", err)
}
default:
err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: fmt.Sprintf("Oops, I don't know any `%s` action", options[0].Name),
},
})
if err != nil {
log.Println("[error] failed to respond to command:", err)
}
}
},
}
registerHandlers(discord, handlers)
err = discord.Open()
if err != nil {
log.Fatalln("fatal error: failed to open session:", err)
}
defer discord.Close()
registeredCommands := make([]*discordgo.ApplicationCommand, len(commands))
for i, c := range commands {
command, err := discord.ApplicationCommandCreate(DISCORD_APP_ID, DISCORD_GUILD_ID, c)
if err != nil {
log.Fatalln("fatal error: failed to register command:", err)
}
registeredCommands[i] = command
}
log.Printf("registered %d commands\n", len(registeredCommands))
<-ctx.Done()
for _, c := range registeredCommands {
err = discord.ApplicationCommandDelete(DISCORD_APP_ID, DISCORD_GUILD_ID, c.ID)
if err != nil {
log.Printf("[error]: failed to delete command: %s\n", err)
}
}
log.Println("deregistered commands, bye bye!")
os.Exit(0)
}
func touchDbFile(path string) error {
f, err := os.Open(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
f, err := os.Create(path)
if err != nil {
return err
}
f.Close()
} else {
return err
}
}
f.Close()
return nil
}
func registerHandlers(sess *discordgo.Session, handlers map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate)) {
sess.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) {
log.Printf("Logged in as: %v#%v", s.State.User.Username, s.State.User.Discriminator)
})
sess.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) {
if h, ok := handlers[i.ApplicationCommandData().Name]; ok {
h(s, i)
}
})
}

@ -5,9 +5,13 @@ go 1.19
require github.com/mattn/go-sqlite3 v1.14.15
require (
github.com/bwmarrin/discordgo v0.26.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.4.0 // indirect
github.com/stretchr/testify v1.8.0 // indirect
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

@ -1,6 +1,10 @@
github.com/bwmarrin/discordgo v0.26.1 h1:AIrM+g3cl+iYBr4yBxCBp9tD9jR3K7upEjl0d89FRkE=
github.com/bwmarrin/discordgo v0.26.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -11,6 +15,14 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
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/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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/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=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

@ -87,15 +87,15 @@ func (s *Store) Claim(ctx context.Context, player, province string, claimType Cl
// Check conflicts
stmt, err := s.db.PrepareContext(ctx, fmt.Sprintf(`SELECT provinces.name FROM provinces WHERE provinces.%s = ? and provinces.name in (
SELECT provinces.name FROM claims LEFT JOIN provinces ON claims.val = provinces.trade_node WHERE claims.claim_type = 'trade'
UNION SELECT provinces.name from claims LEFT JOIN provinces ON claims.val = provinces.region WHERE claims.claim_type = 'region'
UNION SELECT provinces.name from claims LEFT JOIN provinces ON claims.val = provinces.area WHERE claims.claim_type = 'area'
SELECT provinces.name FROM claims LEFT JOIN provinces ON claims.val = provinces.trade_node WHERE claims.claim_type = 'trade' AND claims.player IS NOT ?
UNION SELECT provinces.name from claims LEFT JOIN provinces ON claims.val = provinces.region WHERE claims.claim_type = 'region' AND claims.player IS NOT ?
UNION SELECT provinces.name from claims LEFT JOIN provinces ON claims.val = provinces.area WHERE claims.claim_type = 'area' AND claims.player IS NOT ?
)`, claimTypeToColumn[claimType]))
if err != nil {
return fmt.Errorf("failed to prepare conflicts query: %w", err)
}
rows, err := stmt.QueryContext(ctx, province)
rows, err := stmt.QueryContext(ctx, province, player, player, player)
if err != nil {
return fmt.Errorf("failed to get conflicting provinces: %w", err)
}

@ -50,6 +50,15 @@ func TestStore_Claim(t *testing.T) {
},
wantErr: true,
},
{
name: "same player overlapp",
args: args{
player: "foo", // 'foo' has a claim on Italy, which has overlapping provinces
province: "Genoa",
claimType: CLAIM_TYPE_TRADE,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

Loading…
Cancel
Save