diff --git a/README.md b/README.md index ebc4a913a..b74e894b3 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,10 @@ ![Lines of Code](https://tokei.rs/b1/github/leighmacdonald/gbans) [![Discord chat](https://img.shields.io/discord/704508824320475218)](https://discord.gg/YEWed3wY3F) -gbans is intended to be a more modern & secure replacement -for [sourcebans](https://github.com/GameConnect/sourcebansv1) / [sourcebans++](https://sbpp.dev). +gbans was initially intended to be a more modern & secure replacement +for [sourcebans](https://github.com/GameConnect/sourcebansv1) / [sourcebans++](https://sbpp.dev). It has since +had its scope expanded to include more optional support for general game server management tasks as well +as future plans for in depth plater stat tracking. ## Stability / Usage Notice @@ -33,43 +35,55 @@ authentication token. - Communication over HTTPS - Discord bot integration for administration & announcements. - Built using [Go](https://golang.org/) & [PostgreSQL](https://www.postgresql.org/). It has a built-in -webserver that is safe to directly expose to the internet. This means its not necessary to setup MySQL, +webserver that is safe to directly expose to the internet. This means it's not necessary to setup MySQL, Nginx/Apache and PHP on your server. - Non-legacy codebase that is (hopefully) not a nightmare to hack on. ## Features -- [ ] Import of existing sourcebans database -- [ ] Import/Export of gbans databases -- [ ] Backend linking of gbans services to enable use of other operators lists in real-time. +- [ ] General + - [x] Multi server support + - [x] Global bans + - [x] [Docker support](https://hub.docker.com/repository/docker/leighmacdonald/gbans) + - [ ] ACME ([Lets encrypt](https://letsencrypt.org/) / [Zero SSL](https://zerossl.com/)) protocol support for automatic SSL certificates + - [ ] Import/Export of gbans databases + - [ ] Backend linking of gbans services to enable use of other operators lists in real-time. + - [ ] Multi-tenant support - [x] Game support - [x] Team Fortress 2 -- [ ] 3rd party ban lists - - [x] [tf2_bot_detector](https://github.com/PazerOP/tf2_bot_detector/blob/master/staging/cfg/playerlist.official.json) - - [ ] Known VPN Networks - - [ ] Known non-residential addresses - - [ ] Known proxies -- [x] Multi server support -- [x] Global bans -- [x] Subnet & IP bans (CIDR) +- [ ] Blocking lists & types + - [x] Valves source server banip config + - [ ] Existing sourcebans database + - [x] [CIDR/IP](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing) bans + - [x] [tf2_bot_detector](https://github.com/PazerOP/tf2_bot_detector/blob/master/staging/cfg/playerlist.official.json) + - [ ] Known VPN Networks + - [ ] Known non-residential addresses + - [ ] Known proxies + - [ ] [FireHOL](https://github.com/firehol/blocklist-ipsets) databases - [x] Database support - [x] Postgresql w/PostGIS -- [x] [Docker support](https://hub.docker.com/repository/docker/leighmacdonald/gbans) -- [ ] ACME ([Lets encrypt](https://letsencrypt.org/) / [Zero SSL](https://zerossl.com/)) protocol support for automatic SSL certificates +- [ ] Remote Agent + - [x] Install & Update game installations + - [ ] Apply custom configs and plugins per server + - [ ] Relay game logs to central service - [ ] SourceMod Plugin - - [x] Game server authentication - - [ ] `/gb_ban duration Reason` Ban a user - - [ ] `/gb_unban` Unban a previously banned user - - [ ] `/gb_kick` Kick a user - - [x] `/gb_mod or /mod ` Call for a mod on discord + - [x] Game server authentication + - [x] Restrict banned players from connecting + - [x] Restrict muted/gagged players on join + - [ ] `/gb_ban duration Reason` Ban a user + - [ ] `/gb_unban` Unban a previously banned user + - [ ] `/gb_kick` Kick a user + - [x] `/gb_mod or /mod ` Call for a mod on discord - [ ] User Interfaces - - [x] Discord - - [ ] Web -- [ ] Game server logs - - [x] Remote relay agent `gbans relay -h` - - [x] Parsing - - [x] Indexing - - [ ] Querying + - [x] CLI + - [x] Discord + - [ ] Web + - [ ] Matrix +- [ ] Game server logs event storage and processing + - [x] Remote relay agent `gbans relay -h` + - [x] Parsing + - [x] Indexing + - [ ] Querying ## Docker diff --git a/internal/action/action.go b/internal/action/action.go index 29d777283..d055935de 100644 --- a/internal/action/action.go +++ b/internal/action/action.go @@ -21,7 +21,6 @@ type Executor interface { FindPlayerByCIDR(ipNet *net.IPNet, pi *model.PlayerInfo) error PersonBySID(sid steamid.SID64, ipAddr string, p *model.Person) error GetOrCreateProfileBySteamID(ctx context.Context, sid steamid.SID64, ipAddr string, p *model.Person) error - Mute(args MuteRequest, pi *model.PlayerInfo) error Ban(args BanRequest, b *model.Ban) error BanNetwork(args BanNetRequest, net *model.BanNet) error BanASN(args BanASNRequest, net *model.BanASN) error @@ -116,7 +115,8 @@ type BanRequest struct { Target Author Duration - Reason string + Reason string + BanType model.BanType } type MuteRequest BanRequest @@ -188,8 +188,9 @@ func NewFind(o model.Origin, q string) FindRequest { return FindRequest{BaseOrigin: BaseOrigin{o}, Query: q} } -func NewMute(o model.Origin, target string, author string, reason string, duration string) MuteRequest { - return MuteRequest{ +func NewMute(o model.Origin, target string, author string, reason string, duration string) BanRequest { + return BanRequest{ + BanType: model.NoComm, BaseOrigin: BaseOrigin{o}, Target: Target(target), Author: Author(author), @@ -209,6 +210,7 @@ func NewKick(o model.Origin, target string, author string, reason string) KickRe func NewBan(o model.Origin, target string, author string, reason string, duration string) BanRequest { return BanRequest{ + BanType: model.Banned, BaseOrigin: BaseOrigin{o}, Target: Target(target), Author: Author(author), diff --git a/internal/app/actions.go b/internal/app/actions.go index 0340cfdcf..2cf9c7eae 100644 --- a/internal/app/actions.go +++ b/internal/app/actions.go @@ -21,61 +21,6 @@ import ( "time" ) -// Mute will apply a mute to the players steam id. Mutes are propagated to the servers immediately. -// If duration set to 0, the value of config.DefaultExpiration() will be used. -func (g gbans) Mute(args action.MuteRequest, pi *model.PlayerInfo) error { - target, err := args.Target.SID64() - if err != nil { - return errors.Errorf("Failed to get steam id from: %s", args.Target) - } - source, errSrc := args.Author.SID64() - if errSrc != nil { - return errors.Errorf("Failed to get steam id from: %s", args.Author) - } - duration, errDur := args.Duration.Value() - if errDur != nil { - return errDur - } - until := config.DefaultExpiration() - if duration > 0 { - until = config.Now().Add(duration) - } - b := model.NewBannedPerson() - if err2 := g.db.GetBanBySteamID(g.ctx, target, false, &b); err2 != nil && err2 != store.ErrNoResult { - log.Errorf("Error getting b from db: %v", err2) - return errors.New("Internal DB Error") - } else if err2 != nil { - b = model.BannedPerson{ - Ban: model.NewBan(target, source, duration), - Person: model.NewPerson(target), - } - b.Ban.BanType = model.NoComm - } - if b.Ban.BanType == model.Banned { - return errors.New("Person is already banned") - } - b.Ban.BanType = model.NoComm - b.Ban.Reason = model.Custom - b.Ban.ReasonText = args.Reason - b.Ban.ValidUntil = until - if err3 := g.db.SaveBan(g.ctx, &b.Ban); err3 != nil { - log.Errorf("Failed to save b: %v", err3) - return err3 - } - - if errF := g.Find(target.String(), "", pi); errF != nil { - return nil - } - if pi.InGame { - log.Infof("Gagging in-game Player") - query.RCON(g.ctx, []model.Server{*pi.Server}, - fmt.Sprintf(`sm_gag "#%s"`, string(steamid.SID64ToSID(target))), - fmt.Sprintf(`sm_mute "#%s"`, string(steamid.SID64ToSID(target)))) - } - log.Infof("Gagged Player successfully") - return nil -} - // Unban will set the current ban to now, making it expired. // Returns true, nil if the ban exists, and was successfully banned. // Returns false, nil if the ban does not exist. @@ -104,6 +49,7 @@ func (g gbans) Unban(args action.UnbanRequest) (bool, error) { return true, nil } +// UnbanASN will remove an existing ASN ban func (g gbans) UnbanASN(ctx context.Context, args action.UnbanASNRequest) (bool, error) { asNum, errConv := strconv.ParseInt(args.ASNum, 10, 64) if errConv != nil { @@ -150,12 +96,12 @@ func (g gbans) Ban(args action.BanRequest, b *model.Ban) error { } b.SteamID = target b.AuthorID = source - b.BanType = model.Banned + b.BanType = args.BanType b.Reason = model.Custom b.ReasonText = args.Reason b.Note = "" b.ValidUntil = until - b.Source = model.System + b.Source = args.Origin b.CreatedOn = config.Now() b.UpdatedOn = config.Now() @@ -200,29 +146,42 @@ func (g gbans) Ban(args action.BanRequest, b *model.Ban) error { } } }() - go func() { - ipAddr := "" - // kick the user if they currently are playing on a server - pi := model.NewPlayerInfo() - _ = g.Find(target.String(), "", &pi) - if pi.Valid && pi.InGame { - ipAddr = pi.Player.IP.String() - if _, errR := query.ExecRCON(*pi.Server, - fmt.Sprintf("sm_kick #%d %s", pi.Player.UserID, args.Reason)); errR != nil { - log.Errorf("Faied to kick user afeter b: %v", errR) - } - p := model.NewPerson(pi.Player.SID) - if errG := g.db.GetOrCreatePersonBySteamID(g.ctx, pi.Player.SID, &p); errG != nil { - log.Errorf("Failed to fetch banned player: %v", errG) + ipAddr := "" + // kick the user if they currently are playing on a server + pi := model.NewPlayerInfo() + _ = g.Find(target.String(), "", &pi) + if pi.Valid && pi.InGame { + switch args.BanType { + case model.NoComm: + { + log.Infof("Gagging in-game Player") + query.RCON(g.ctx, []model.Server{*pi.Server}, + fmt.Sprintf(`sm_gag "#%s"`, string(steamid.SID64ToSID(target))), + fmt.Sprintf(`sm_mute "#%s"`, string(steamid.SID64ToSID(target)))) } - p.IPAddr = net.ParseIP(ipAddr) - if errS := g.db.SavePerson(g.ctx, &p); errS != nil { - log.Errorf("Failed to update banned player up: %v", errS) + case model.Banned: + { + log.Infof("Banning and kicking in-game Player") + ipAddr = pi.Player.IP.String() + if _, errR := query.ExecRCON(*pi.Server, + fmt.Sprintf("sm_kick #%d %s", pi.Player.UserID, args.Reason)); errR != nil { + log.Errorf("Faied to kick user after ban: %v", errR) + } } } - }() + p := model.NewPerson(pi.Player.SID) + if errG := g.db.GetOrCreatePersonBySteamID(g.ctx, pi.Player.SID, &p); errG != nil { + log.Errorf("Failed to fetch banned player: %v", errG) + } + p.IPAddr = net.ParseIP(ipAddr) + if errS := g.db.SavePerson(g.ctx, &p); errS != nil { + log.Errorf("Failed to update banned player ip: %v", errS) + } + } return nil } + +// BanASN will ban all network ranges associated with the requested ASN func (g gbans) BanASN(args action.BanASNRequest, banASN *model.BanASN) error { target, errTar := args.Target.SID64() if errTar != nil { @@ -317,7 +276,7 @@ func (g gbans) BanNetwork(args action.BanNetRequest, banNet *model.BanNet) error return nil } -// Kick will kick the steam id from all servers. +// Kick will kick the steam id from whatever server it is connected to. func (g gbans) Kick(args action.KickRequest, pi *model.PlayerInfo) error { target, errTar := args.Target.SID64() if errTar != nil { @@ -346,6 +305,9 @@ func (g gbans) Kick(args action.KickRequest, pi *model.PlayerInfo) error { return nil } +// SetSteam is used to associate a discord user with either steam id. This is used +// instead of requiring users to link their steam account to discord itself. It also +// means the bot does not require more priviledges intents. func (g gbans) SetSteam(args action.SetSteamIDRequest) (bool, error) { sid, err := steamid.ResolveSID64(g.ctx, string(args.Target)) if err != nil || !sid.Valid() { @@ -365,6 +327,7 @@ func (g gbans) SetSteam(args action.SetSteamIDRequest) (bool, error) { return true, nil } +// Say is used to send a message to the server via sm_say func (g gbans) Say(args action.SayRequest) error { var server model.Server if err := g.db.GetServerByName(g.ctx, args.Server, &server); err != nil { @@ -382,6 +345,7 @@ func (g gbans) Say(args action.SayRequest) error { return nil } +// CSay is used to send a centered message to the server via sm_csay func (g gbans) CSay(args action.CSayRequest) error { var ( servers []model.Server @@ -404,6 +368,7 @@ func (g gbans) CSay(args action.CSayRequest) error { return nil } +// PSay is used to send a private message to a player func (g gbans) PSay(args action.PSayRequest) error { var pi model.PlayerInfo _ = g.Find(string(args.Target), "", &pi) @@ -418,6 +383,7 @@ func (g gbans) PSay(args action.PSayRequest) error { return nil } +// FilterAdd creates a new chat filter using a regex pattern func (g gbans) FilterAdd(args action.FilterAddRequest) (model.Filter, error) { re, err := regexp.Compile(args.Filter) if err != nil { @@ -434,6 +400,7 @@ func (g gbans) FilterAdd(args action.FilterAddRequest) (model.Filter, error) { return filter, nil } +// FilterDel removed and existing chat filter func (g gbans) FilterDel(ctx context.Context, args action.FilterDelRequest) (bool, error) { var filter model.Filter if err := g.db.GetFilterByID(ctx, args.FilterID, &filter); err != nil { @@ -445,6 +412,7 @@ func (g gbans) FilterDel(ctx context.Context, args action.FilterDelRequest) (boo return true, nil } +// FilterCheck can be used to check if a phrase will match any filters func (g gbans) FilterCheck(args action.FilterCheckRequest) []model.Filter { if args.Message == "" { return nil @@ -482,6 +450,7 @@ func (g gbans) ContainsFilteredWord(body string) (bool, model.Filter) { return false, model.Filter{} } +// PersonBySID fetches the person from the database, updating the PlayerSummary if it out of date func (g gbans) PersonBySID(sid steamid.SID64, ipAddr string, p *model.Person) error { if err := g.db.GetPersonBySteamID(g.ctx, sid, p); err != nil && err != store.ErrNoResult { return err @@ -506,6 +475,7 @@ func (g gbans) PersonBySID(sid steamid.SID64, ipAddr string, p *model.Person) er return nil } +// ResolveSID is just a simple helper for calling steamid.ResolveSID64 func (g gbans) ResolveSID(sidStr string) (steamid.SID64, error) { c, cancel := context.WithTimeout(g.ctx, time.Second*5) defer cancel() diff --git a/internal/app/app.go b/internal/app/app.go index c571d7693..699991274 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -113,7 +113,7 @@ func (g *gbans) Start() { if config.Discord.Enabled { g.initDiscord() } else { - g.l.Warnf("Discord bot not enabled") + g.l.Warnf("discord bot not enabled") } // Start the background goroutine workers @@ -165,7 +165,8 @@ func (g *gbans) warnWorker() { } } -// logWriter handles tak +// logWriter handles writing log events to the database. It does it in batches for performance +// reasons. func (g *gbans) logWriter() { const ( freq = time.Second * 10 @@ -315,18 +316,18 @@ func (g *gbans) addWarning(sid64 steamid.SID64, reason warnReason) { }) g.warningsMu.Unlock() if len(g.warnings[sid64]) >= config.General.WarningLimit { - var pi model.PlayerInfo + var ban model.Ban g.l.Errorf("Warn limit exceeded (%d): %d", sid64, len(g.warnings[sid64])) var err error switch config.General.WarningExceededAction { case config.Gag: - err = g.Mute(action.NewMute(model.System, sid64.String(), config.General.Owner.String(), warnReasonString(reason), - config.General.WarningExceededDuration.String()), &pi) + err = g.Ban(action.NewMute(model.System, sid64.String(), config.General.Owner.String(), warnReasonString(reason), + config.General.WarningExceededDuration.String()), &ban) case config.Ban: - var ban model.Ban err = g.Ban(action.NewBan(model.System, sid64.String(), config.General.Owner.String(), warnReasonString(reason), config.General.WarningExceededDuration.String()), &ban) case config.Kick: + var pi model.PlayerInfo err = g.Kick(action.NewKick(model.System, sid64.String(), config.General.Owner.String(), warnReasonString(reason)), &pi) } if err != nil { @@ -367,11 +368,11 @@ func (g *gbans) initDiscord() { } go func() { if errBS := g.bot.Start(g.ctx, config.Discord.Token, events); errBS != nil { - g.l.Errorf("DiscordClient returned error: %v", errBS) + g.l.Errorf("discord returned error: %v", errBS) } }() } else { - g.l.Fatalf("Discord enabled, but bot token invalid") + g.l.Fatalf("discord enabled, but bot token invalid") } } diff --git a/internal/app/background.go b/internal/app/background.go index 3fe96873a..b06189a80 100644 --- a/internal/app/background.go +++ b/internal/app/background.go @@ -140,6 +140,13 @@ func (g *gbans) serverStateUpdater() { } } +// mapChanger watches over servers and checks for servers on maps with 0 players. +// If there is no player for a long enough duration and the map is not one of the +// maps in the default map set, a changelevel request will be made to the server +// +// Relevant config values: +// - general.map_changer_enabled +// - general.default_map func (g *gbans) mapChanger(timeout time.Duration) { type at struct { lastActive time.Time @@ -190,7 +197,7 @@ func (g *gbans) mapChanger(timeout time.Duration) { } } -// banSweeper +// banSweeper periodically will query the database for expired bans and remove them. func (g *gbans) banSweeper() { log.Debug("ban sweeper routine started") ticker := time.NewTicker(time.Minute) diff --git a/internal/discord/bot.go b/internal/discord/bot.go index ccf31cc14..9096339db 100644 --- a/internal/discord/bot.go +++ b/internal/discord/bot.go @@ -28,7 +28,8 @@ var ( errTooLarge = errors.Errorf("Max message length is %d", discordMaxMsgLen) ) -type DiscordClient struct { +// discord implements the ChatBot interface for the discord chat platform. +type discord struct { dg *discordgo.Session connectedMu *sync.RWMutex connected bool @@ -37,9 +38,9 @@ type DiscordClient struct { db store.Store } -// New instantiates a new, unconnected, DiscordClient instance -func New(executor action.Executor, s store.Store) (*DiscordClient, error) { - b := DiscordClient{ +// New instantiates a new, unconnected, discord instance +func New(executor action.Executor, s store.Store) (*discord, error) { + b := discord{ dg: nil, connectedMu: &sync.RWMutex{}, connected: false, @@ -66,10 +67,10 @@ func New(executor action.Executor, s store.Store) (*DiscordClient, error) { return &b, nil } -func (b *DiscordClient) Start(ctx context.Context, token string, eventChan chan model.ServerEvent) error { +func (b *discord) Start(ctx context.Context, token string, eventChan chan model.ServerEvent) error { d, err := discordgo.New("Bot " + token) if err != nil { - return errors.Wrapf(err, "Failed to connect to discord. DiscordClient unavailable") + return errors.Wrapf(err, "Failed to connect to discord. discord unavailable") } defer func() { @@ -87,7 +88,7 @@ func (b *DiscordClient) Start(ctx context.Context, token string, eventChan chan // In this example, we only care about receiving message events. b.dg.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsGuildMessages) - // Open a websocket connection to Discord and begin listening. + // Open a websocket connection to discord and begin listening. err = b.dg.Open() if err != nil { return errors.Wrap(err, "Error opening discord connection") @@ -103,9 +104,9 @@ func (b *DiscordClient) Start(ctx context.Context, token string, eventChan chan } // discordMessageQueueReader functions by registering event handlers for the two user message events -// Discord will rate limit you once you start approaching 5-10 servers of active users. Because of this +// discord will rate limit you once you start approaching 5-10 servers of active users. Because of this // we queue messages and periodically send them out as multiline string blocks instead. -func (b *DiscordClient) discordMessageQueueReader(ctx context.Context, eventChan chan model.ServerEvent) { +func (b *discord) discordMessageQueueReader(ctx context.Context, eventChan chan model.ServerEvent) { messageTicker := time.NewTicker(time.Second * 10) var sendQueue []string for { @@ -142,11 +143,11 @@ func (b *DiscordClient) discordMessageQueueReader(ctx context.Context, eventChan } } -func (b *DiscordClient) onReady(_ *discordgo.Session, _ *discordgo.Ready) { - log.Infof("DiscordClient is connected & ready") +func (b *discord) onReady(_ *discordgo.Session, _ *discordgo.Ready) { + log.Infof("discord is connected & ready") } -func (b *DiscordClient) onConnect(s *discordgo.Session, _ *discordgo.Connect) { +func (b *discord) onConnect(s *discordgo.Session, _ *discordgo.Connect) { log.Info("Connected to session ws API") d := discordgo.UpdateStatusData{ IdleSince: nil, @@ -172,14 +173,14 @@ func (b *DiscordClient) onConnect(s *discordgo.Session, _ *discordgo.Connect) { b.connectedMu.Unlock() } -func (b *DiscordClient) onDisconnect(_ *discordgo.Session, _ *discordgo.Disconnect) { +func (b *discord) onDisconnect(_ *discordgo.Session, _ *discordgo.Disconnect) { b.connectedMu.Lock() b.connected = false b.connectedMu.Unlock() log.Info("Disconnected from session ws API") } -func (b *DiscordClient) sendChannelMessage(s *discordgo.Session, c string, msg string, wrap bool) error { +func (b *discord) sendChannelMessage(s *discordgo.Session, c string, msg string, wrap bool) error { b.connectedMu.RLock() if !b.connected { b.connectedMu.RUnlock() @@ -200,7 +201,7 @@ func (b *DiscordClient) sendChannelMessage(s *discordgo.Session, c string, msg s return nil } -func (b *DiscordClient) sendInteractionMessageEdit(s *discordgo.Session, i *discordgo.Interaction, r botResponse) error { +func (b *discord) sendInteractionMessageEdit(s *discordgo.Session, i *discordgo.Interaction, r botResponse) error { b.connectedMu.RLock() if !b.connected { b.connectedMu.RUnlock() @@ -226,11 +227,11 @@ func (b *DiscordClient) sendInteractionMessageEdit(s *discordgo.Session, i *disc return s.InteractionResponseEdit(config.Discord.AppID, i, e) } -func (b *DiscordClient) Send(channelId string, message string, wrap bool) error { +func (b *discord) Send(channelId string, message string, wrap bool) error { return b.sendChannelMessage(b.dg, channelId, message, wrap) } -func (b *DiscordClient) SendEmbed(channelId string, message *discordgo.MessageEmbed) error { +func (b *discord) SendEmbed(channelId string, message *discordgo.MessageEmbed) error { if _, errSend := b.dg.ChannelMessageSendEmbed(channelId, message); errSend != nil { return errSend } diff --git a/internal/discord/bot_commands.go b/internal/discord/bot_commands.go index 0ce7e68b9..b994846d1 100644 --- a/internal/discord/bot_commands.go +++ b/internal/discord/bot_commands.go @@ -38,7 +38,7 @@ const ( cmdFilterCheck botCmd = "filter_check" ) -func (b *DiscordClient) botRegisterSlashCommands() error { +func (b *discord) botRegisterSlashCommands() error { // TODO register the commands again upon adding new servers to update autocomplete opts optUserID := &discordgo.ApplicationCommandOption{ Type: discordgo.ApplicationCommandOptionString, @@ -401,12 +401,12 @@ const ( // onInteractionCreate is called when a user initiates an application command. All commands are sent // through this interface. // https://discord.com/developers/docs/interactions/receiving-and-responding#receiving-an-interaction -func (b *DiscordClient) onInteractionCreate(session *discordgo.Session, interaction *discordgo.InteractionCreate) { +func (b *discord) onInteractionCreate(session *discordgo.Session, interaction *discordgo.InteractionCreate) { cmd := botCmd(interaction.Data.Name) response := botResponse{MsgType: mtString} if handler, ok := b.commandHandlers[cmd]; ok { // sendPreResponse should be called for any commands that call external services or otherwise - // could not return a response instantly. Discord will time out commands that don't respond within a + // could not return a response instantly. discord will time out commands that don't respond within a // very short timeout windows, ~2-3 seconds. if err := session.InteractionRespond(interaction.Interaction, &discordgo.InteractionResponse{ Type: discordgo.InteractionResponseDeferredChannelMessageWithSource, diff --git a/internal/discord/bot_handlers.go b/internal/discord/bot_handlers.go index 33616009f..edc1bb54f 100644 --- a/internal/discord/bot_handlers.go +++ b/internal/discord/bot_handlers.go @@ -86,7 +86,7 @@ func RespOk(r *botResponse, title string) *discordgo.MessageEmbed { return embed } -func (b *DiscordClient) onFind(ctx context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { +func (b *discord) onFind(ctx context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { userIdentifier := m.Data.Options[0].Value.(string) pi := model.NewPlayerInfo() if err := b.executor.Find(userIdentifier, "", &pi); err != nil { @@ -111,22 +111,30 @@ func (b *DiscordClient) onFind(ctx context.Context, _ *discordgo.Session, m *dis return nil } -func (b *DiscordClient) onMute(_ context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { +func (b *discord) onMute(ctx context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { playerID := m.Data.Options[0].Value.(string) reasonStr := model.Custom.String() if len(m.Data.Options) > 2 { reasonStr = m.Data.Options[2].Value.(string) } - var pi model.PlayerInfo - if err := b.executor.Mute(action.NewMute(model.Bot, playerID, m.Member.User.ID, - reasonStr, m.Data.Options[1].Value.(string)), &pi); err != nil { + author := model.NewPerson(0) + if errA := b.db.GetPersonByDiscordID(ctx, m.Interaction.Member.User.ID, &author); errA != nil { + if errA == store.ErrNoResult { + return errors.New("Must set steam id. See /set_steam") + } + return errors.New("Error fetching author info") + } + var ban model.Ban + if err := b.executor.Ban(action.NewMute(model.Bot, playerID, author.SteamID.String(), + reasonStr, m.Data.Options[1].Value.(string)), &ban); err != nil { return err } - RespOk(r, "Player muted successfully") + e := RespOk(r, "Player muted successfully") + addFieldsSteamID(e, ban.SteamID) return nil } -func (b *DiscordClient) onBanASN(ctx context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { +func (b *discord) onBanASN(ctx context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { asNumStr := m.Data.Options[0].Options[0].Value.(string) duration := m.Data.Options[0].Options[1].Value.(string) reason := m.Data.Options[0].Options[2].Value.(string) @@ -167,7 +175,7 @@ func (b *DiscordClient) onBanASN(ctx context.Context, _ *discordgo.Session, m *d return nil } -func (b *DiscordClient) onBanIP(_ context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { +func (b *discord) onBanIP(_ context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { reason := model.Custom.String() if len(m.Data.Options[0].Options) > 3 { reason = m.Data.Options[0].Options[3].Value.(string) @@ -204,7 +212,7 @@ func (b *DiscordClient) onBanIP(_ context.Context, _ *discordgo.Session, m *disc } // onBanSteam !ban [reason] -func (b *DiscordClient) onBanSteam(ctx context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { +func (b *discord) onBanSteam(ctx context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { reason := "" if len(m.Data.Options[0].Options) > 2 { reason = m.Data.Options[0].Options[2].Value.(string) @@ -237,7 +245,7 @@ func (b *DiscordClient) onBanSteam(ctx context.Context, _ *discordgo.Session, m return nil } -func (b *DiscordClient) onCheck(ctx context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { +func (b *discord) onCheck(ctx context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { sid, err := b.executor.ResolveSID(m.Data.Options[0].Value.(string)) if err != nil { return consts.ErrInvalidSID @@ -407,7 +415,7 @@ func (b *DiscordClient) onCheck(ctx context.Context, _ *discordgo.Session, m *di return nil } -func (b *DiscordClient) onHistory(ctx context.Context, s *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { +func (b *discord) onHistory(ctx context.Context, s *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { switch m.Data.Options[0].Name { case string(cmdHistoryIP): return b.onHistoryIP(ctx, s, m, r) @@ -416,7 +424,7 @@ func (b *DiscordClient) onHistory(ctx context.Context, s *discordgo.Session, m * } } -func (b *DiscordClient) onHistoryIP(ctx context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { +func (b *discord) onHistoryIP(ctx context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { sid, err := b.executor.ResolveSID(m.Data.Options[0].Options[0].Value.(string)) if err != nil { return consts.ErrInvalidSID @@ -443,7 +451,7 @@ func (b *DiscordClient) onHistoryIP(ctx context.Context, _ *discordgo.Session, m return nil } -func (b *DiscordClient) onHistoryChat(ctx context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { +func (b *discord) onHistoryChat(ctx context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { sid, err := b.executor.ResolveSID(m.Data.Options[0].Options[0].Value.(string)) if err != nil { return consts.ErrInvalidSID @@ -468,7 +476,7 @@ func (b *DiscordClient) onHistoryChat(ctx context.Context, _ *discordgo.Session, return nil } -func (b *DiscordClient) onSetSteam(ctx context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { +func (b *discord) onSetSteam(ctx context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { sid, err := b.executor.ResolveSID(m.Data.Options[0].Value.(string)) if err != nil { return consts.ErrInvalidSID @@ -489,7 +497,7 @@ func (b *DiscordClient) onSetSteam(ctx context.Context, _ *discordgo.Session, m return nil } -func (b *DiscordClient) onUnbanSteam(ctx context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { +func (b *discord) onUnbanSteam(ctx context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { sid, err := b.executor.ResolveSID(m.Data.Options[0].Options[0].Value.(string)) if err != nil { return consts.ErrInvalidSID @@ -514,7 +522,7 @@ func (b *DiscordClient) onUnbanSteam(ctx context.Context, _ *discordgo.Session, return nil } -func (b *DiscordClient) onUnbanASN(ctx context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { +func (b *discord) onUnbanASN(ctx context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { asNumStr, ok := m.Data.Options[0].Options[0].Value.(string) if !ok { return errors.New("invalid asn") @@ -551,7 +559,7 @@ func (b *DiscordClient) onUnbanASN(ctx context.Context, _ *discordgo.Session, m return nil } -func (b *DiscordClient) onKick(_ context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { +func (b *discord) onKick(_ context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { sid, err := b.executor.ResolveSID(m.Data.Options[0].Value.(string)) if err != nil { return consts.ErrInvalidSID @@ -579,7 +587,7 @@ func (b *DiscordClient) onKick(_ context.Context, _ *discordgo.Session, m *disco return nil } -func (b *DiscordClient) onSay(_ context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { +func (b *discord) onSay(_ context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { server := m.Data.Options[0].Value.(string) msg := m.Data.Options[1].Value.(string) if errS := b.executor.Say(action.NewSay(model.Bot, server, msg)); errS != nil { @@ -591,7 +599,7 @@ func (b *DiscordClient) onSay(_ context.Context, _ *discordgo.Session, m *discor return nil } -func (b *DiscordClient) onCSay(_ context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { +func (b *discord) onCSay(_ context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { server := m.Data.Options[0].Value.(string) msg := m.Data.Options[1].Value.(string) if errS := b.executor.CSay(action.NewCSay(model.Bot, server, msg)); errS != nil { @@ -603,7 +611,7 @@ func (b *DiscordClient) onCSay(_ context.Context, _ *discordgo.Session, m *disco return nil } -func (b *DiscordClient) onPSay(_ context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { +func (b *discord) onPSay(_ context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { player := m.Data.Options[0].Value.(string) msg := m.Data.Options[1].Value.(string) if errS := b.executor.PSay(action.NewPSay(model.Bot, player, msg)); errS != nil { @@ -639,7 +647,7 @@ func mapRegion(n string) string { } } -func (b *DiscordClient) onServers(_ context.Context, _ *discordgo.Session, _ *discordgo.InteractionCreate, r *botResponse) error { +func (b *discord) onServers(_ context.Context, _ *discordgo.Session, _ *discordgo.InteractionCreate, r *botResponse) error { state := b.executor.ServerState().ByRegion() stats := map[string]float64{} used, total := 0, 0 @@ -686,7 +694,7 @@ func (b *DiscordClient) onServers(_ context.Context, _ *discordgo.Session, _ *di return nil } -func (b *DiscordClient) onPlayers(ctx context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { +func (b *discord) onPlayers(ctx context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { var server model.Server if errS := b.db.GetServerByName(ctx, m.Data.Options[0].Value.(string), &server); errS != nil { if errS == store.ErrNoResult { @@ -738,7 +746,7 @@ func (b *DiscordClient) onPlayers(ctx context.Context, _ *discordgo.Session, m * return nil } -func (b *DiscordClient) onBan(ctx context.Context, s *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { +func (b *discord) onBan(ctx context.Context, s *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { switch m.Data.Options[0].Name { case "steam": return b.onBanSteam(ctx, s, m, r) @@ -750,7 +758,7 @@ func (b *DiscordClient) onBan(ctx context.Context, s *discordgo.Session, m *disc return errCommandFailed } } -func (b *DiscordClient) onUnban(ctx context.Context, s *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { +func (b *discord) onUnban(ctx context.Context, s *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { switch m.Data.Options[0].Name { case "steam": return b.onUnbanSteam(ctx, s, m, r) @@ -763,7 +771,7 @@ func (b *DiscordClient) onUnban(ctx context.Context, s *discordgo.Session, m *di return errCommandFailed } } -func (b *DiscordClient) onFilter(ctx context.Context, s *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { +func (b *discord) onFilter(ctx context.Context, s *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { switch m.Data.Options[0].Name { case string(cmdFilterAdd): return b.onFilterAdd(ctx, s, m, r) @@ -776,7 +784,7 @@ func (b *DiscordClient) onFilter(ctx context.Context, s *discordgo.Session, m *d } } -func (b *DiscordClient) onFilterAdd(ctx context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { +func (b *discord) onFilterAdd(ctx context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { filter := m.Data.Options[0].Options[0].Value.(string) author := model.NewPerson(0) if errA := b.db.GetPersonByDiscordID(ctx, m.Interaction.Member.User.ID, &author); errA != nil { @@ -798,7 +806,7 @@ func (b *DiscordClient) onFilterAdd(ctx context.Context, _ *discordgo.Session, m return nil } -func (b *DiscordClient) onFilterDel(ctx context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { +func (b *discord) onFilterDel(ctx context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { wordId, ok := m.Data.Options[0].Options[0].Value.(int) if !ok { return errors.New("Invalid filter id") @@ -815,7 +823,7 @@ func (b *DiscordClient) onFilterDel(ctx context.Context, _ *discordgo.Session, m return nil } -func (b *DiscordClient) onFilterCheck(_ context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { +func (b *discord) onFilterCheck(_ context.Context, _ *discordgo.Session, m *discordgo.InteractionCreate, r *botResponse) error { message := m.Data.Options[0].Options[0].Value.(string) matches := b.executor.FilterCheck(action.FilterCheckRequest{ BaseOrigin: action.BaseOrigin{Origin: model.Bot}, diff --git a/internal/query/a2s.go b/internal/query/a2s.go index be6b68f24..21f72ac31 100644 --- a/internal/query/a2s.go +++ b/internal/query/a2s.go @@ -5,31 +5,8 @@ import ( "github.com/pkg/errors" "github.com/rumblefrog/go-a2s" log "github.com/sirupsen/logrus" - "sync" ) -func A2SInfo(servers []model.Server) map[string]*a2s.ServerInfo { - responses := make(map[string]*a2s.ServerInfo) - mu := &sync.RWMutex{} - wg := &sync.WaitGroup{} - for _, s := range servers { - wg.Add(1) - go func(server model.Server) { - defer wg.Done() - resp, err := A2SQueryServer(server) - if err != nil { - log.Errorf("A2S: %v", err) - return - } - mu.Lock() - responses[server.ServerName] = resp - mu.Unlock() - }(s) - } - wg.Wait() - return responses -} - func A2SQueryServer(server model.Server) (*a2s.ServerInfo, error) { client, err := a2s.NewClient(server.Addr()) if err != nil {