From 724e3993ce6b6b3a664ea75fecf37afd5d27db78 Mon Sep 17 00:00:00 2001 From: iDigitalFlame Date: Mon, 5 Oct 2020 12:35:14 -0400 Subject: [PATCH] Optimizations! - Removed errors chan with a func passer - Updated the handler of tweets to run regardless (it bails if not active) - Simpl-ier Cmdline - Version num update 2.2 => 2.21 --- README.md | 2 +- html/public/script/scoreboard.js | 2 +- html/public/style/scoreboard.css | 2 +- html/template/home.html | 2 +- html/template/scoreboard.html | 2 +- scoreboard/cmd/main.go | 7 +- scoreboard/config.go | 81 +++++++++++--- scoreboard/game/manager.go | 6 +- scoreboard/scoreboard.go | 182 +++++++++++-------------------- 9 files changed, 144 insertions(+), 142 deletions(-) diff --git a/README.md b/README.md index 0d254a0..00d724d 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ bash build.sh The Scoreboard can be configured by command line options, though it's preferred to use a config file instead. (Below). ```text -Scorebot Scoreboard v2.2 +Scorebot Scoreboard v2.21 Leaving any of the required Twitter options empty in command line or config will result in Twitter functionality being disabled. diff --git a/html/public/script/scoreboard.js b/html/public/script/scoreboard.js index 7f19c9c..75d089f 100644 --- a/html/public/script/scoreboard.js +++ b/html/public/script/scoreboard.js @@ -13,7 +13,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program.If not, see . // -// Scoreboard v2.2 +// Scoreboard v2.21 // 2020 iDigitalFlame // // Javascript Main File diff --git a/html/public/style/scoreboard.css b/html/public/style/scoreboard.css index 97496ab..c5b1d64 100644 --- a/html/public/style/scoreboard.css +++ b/html/public/style/scoreboard.css @@ -14,7 +14,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . - Scoreboard v2.2 + Scoreboard v2.21 2020 iDigitalFlame CSS Main File diff --git a/html/template/home.html b/html/template/home.html index e985932..0dc220e 100644 --- a/html/template/home.html +++ b/html/template/home.html @@ -14,7 +14,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . - Scoreboard v2.2 + Scoreboard v2.21 2020 iDigitalFlame Games Template List Page diff --git a/html/template/scoreboard.html b/html/template/scoreboard.html index 338fa1a..554d85b 100644 --- a/html/template/scoreboard.html +++ b/html/template/scoreboard.html @@ -14,7 +14,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . - Scoreboard v2.2 + Scoreboard v2.21 2020 iDigitalFlame Scoreboard HTML Template Page diff --git a/scoreboard/cmd/main.go b/scoreboard/cmd/main.go index 6640168..33dadf3 100644 --- a/scoreboard/cmd/main.go +++ b/scoreboard/cmd/main.go @@ -25,10 +25,11 @@ import ( func main() { s, err := scoreboard.Cmdline() + if err == flag.ErrHelp { + os.Exit(2) + } + if err != nil { - if err == flag.ErrHelp { - os.Exit(2) - } os.Stderr.WriteString("Error during startup: " + err.Error() + "!\n") os.Exit(1) } diff --git a/scoreboard/config.go b/scoreboard/config.go index ec38f1c..3c16f5e 100644 --- a/scoreboard/config.go +++ b/scoreboard/config.go @@ -59,6 +59,53 @@ const defaults = `{ "timeout": 10, "scorebot": "http://scorebot" } +` +const usage = `Scorebot Scoreboard v2.21 + +Leaving any of the required Twitter options empty in command +line or config will result in Twitter functionality being disabled. +Required Twitter options: 'Consumer Key and Secret', 'Access Key and Secret', +'Twitter Keywords' and 'Twitter Language'. + +Usage of scoreboard: + -c Scorebot configuration file path. + -d Print default configuration and exit. + -sbe Scorebot core address or URL (Required without "-c"). + -assets Scoreboard secondary assets override URL. + -dir Scoreboard HTML override directory path. + -log Scoreboard log file path. + -log-level Scoreboard logging level (Default 2). + -tick Scorebot poll tate, in seconds (Default 5). + -timeout Scoreboard request timeout, in seconds (Default 10). + -bind Address and port to listen on (Default "0.0.0.0:8080"). + -cert Path to TLS certificate file. + -key Path to TLS key file. + -tw-ck Twitter Consumer API key. + -tw-cs Twitter Consumer API secret. + -tw-ak Twitter Access API key. + -tw-as Twitter Access API secret. + -tw-keywords Twitter search keywords (Comma separated) + -tw-lang Twitter search language (Comma separated) + -tw-expire Tweet display time, in seconds (Default 45). + -tw-block-words Twitter blocked words (Comma separated). + -tw-block-user Twitter blocked Usernames (Comma separated). + -tw-only-users Twitter whitelisted Usernames (Comma separated). + +Copyright (C) 2020 iDigitalFlame + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published +by the Free Software Foundation, either version 3 of the License, or +any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + ` type log struct { @@ -76,9 +123,7 @@ type tweets struct { Expire int `json:"expire"` Credentials creds `json:"auth"` } - -// Config is a struct that is used to store the configuration options for the scoreboard. -type Config struct { +type config struct { Log log `json:"log,omitempty"` Key string `json:"key,omitempty"` Cert string `json:"cert,omitempty"` @@ -110,21 +155,29 @@ func split(s string) []string { } return o } -func (c *Config) verify() error { +func (e errval) Error() string { + if e.e == nil { + return e.s + } + return e.s + ": " + e.e.Error() +} +func (e errval) Unwrap() error { + return e.e +} +func (c *config) verify() error { if c.Tick <= 0 { - return &errorval{s: "tick " + strconv.Itoa(c.Tick) + " cannot be less than or equal to zero"} + return &errval{s: "tick " + strconv.Itoa(c.Tick) + " cannot be less than or equal to zero"} } if c.Timeout <= 0 { - return &errorval{s: "timeout " + strconv.Itoa(c.Timeout) + " cannot be less than or equal to zero"} + return &errval{s: "timeout " + strconv.Itoa(c.Timeout) + " cannot be less than or equal to zero"} } if c.Log.Level < int(logx.Trace) || c.Log.Level > int(logx.Fatal) { - return &errorval{s: "log level " + strconv.Itoa(c.Tick) + " must be between zero and five"} + return &errval{s: "log level " + strconv.Itoa(c.Tick) + " must be between zero and five"} } if len(c.Listen) == 0 { c.Listen = "0.0.0.0:8080" } - c.twitter = true - if len(c.Twitter.Filter.Language) == 0 || len(c.Twitter.Filter.Keywords) == 0 { + if c.twitter = true; len(c.Twitter.Filter.Language) == 0 || len(c.Twitter.Filter.Keywords) == 0 { c.twitter = false } if len(c.Twitter.Credentials.AccessKey) == 0 || len(c.Twitter.Credentials.AccessSecret) == 0 { @@ -134,7 +187,7 @@ func (c *Config) verify() error { c.twitter = false } if c.twitter && c.Twitter.Expire <= 0 { - return &errorval{s: "tweet expire time " + strconv.Itoa(c.Timeout) + " cannot be less than or equal to zero"} + return &errval{s: "tweet expire time " + strconv.Itoa(c.Timeout) + " cannot be less than or equal to zero"} } return nil } @@ -145,7 +198,7 @@ func (c *Config) verify() error { // this means that the defaults are being printed and to bail out with a success status. func Cmdline() (*Scoreboard, error) { var ( - c Config + c config d bool args = flag.NewFlagSet("Scorebot Scoreboard", flag.ExitOnError) twbWords, twoUsers string @@ -155,7 +208,6 @@ func Cmdline() (*Scoreboard, error) { os.Stdout.WriteString(usage) os.Exit(2) } - args.StringVar(&s, "c", "", "scoreboard config file path.") args.BoolVar(&d, "d", false, "Print default configuration and exit.") args.StringVar(&c.Scorebot, "sbe", "", "Scorebot core address or URL (Required without -c).") @@ -178,6 +230,7 @@ func Cmdline() (*Scoreboard, error) { args.StringVar(&twbWords, "tw-block-words", "", "Twitter blocked words (Comma separated).") args.StringVar(&twbUsers, "tw-block-user", "", "Twitter blocked Usernames (Comma separated).") args.StringVar(&twoUsers, "tw-only-users", "", "Twitter whitelisted Usernames (Comma separated).") + if err := args.Parse(os.Args[1:]); err != nil { os.Stdout.WriteString(usage) return nil, flag.ErrHelp @@ -196,10 +249,10 @@ func Cmdline() (*Scoreboard, error) { if len(s) > 0 { b, err := ioutil.ReadFile(s) if err != nil { - return nil, &errorval{s: `cannot read file "` + s + `"`, e: err} + return nil, &errval{s: `cannot read file "` + s + `"`, e: err} } if err := json.Unmarshal(b, &c); err != nil { - return nil, &errorval{s: `cannot parse JSON from file "` + s + `"`, e: err} + return nil, &errval{s: `cannot parse JSON from file "` + s + `"`, e: err} } } return c.New() diff --git a/scoreboard/game/manager.go b/scoreboard/game/manager.go index 471a625..1833cf1 100644 --- a/scoreboard/game/manager.go +++ b/scoreboard/game/manager.go @@ -393,9 +393,9 @@ func (s *subscription) update(x context.Context, m *Manager) { } } -// Twitter creates and returns the Twitter channel. This channel can be used to submit Tweets -// to be sent to the scoreboard. -func (m *Manager) Twitter(t time.Duration) chan *twitter.Tweet { +// Twitter creates and returns the Twitter channel. This channel can be used to submit Tweets to +// be sent to the scoreboard. +func (m *Manager) Twitter(t time.Duration) chan<- *twitter.Tweet { m.twitter = &tweets{new: make(chan *twitter.Tweet), timeout: t} return m.twitter.new } diff --git a/scoreboard/scoreboard.go b/scoreboard/scoreboard.go index df263a8..2cac21c 100644 --- a/scoreboard/scoreboard.go +++ b/scoreboard/scoreboard.go @@ -18,6 +18,8 @@ package scoreboard import ( "context" + "crypto/tls" + "net" "net/http" "net/url" "os" @@ -38,64 +40,13 @@ import ( "github.com/iDigitalFlame/scorebot-scoreboard/scoreboard/game" ) -const ( - usage = `Scorebot Scoreboard - -Leaving any of the required Twitter options empty in command -line or config will result in Twitter functionality being disabled. -Required Twitter options: 'Consumer Key and Secret', 'Access Key and Secret', -'Twitter Keywords' and 'Twitter Language'. - -Usage of scoreboard: - -c Scorebot configuration file path. - -d Print default configuration and exit. - -sbe Scorebot core address or URL (Required without "-c"). - -assets Scoreboard secondary assets override URL. - -dir Scoreboard HTML override directory path. - -log Scoreboard log file path. - -log-level Scoreboard logging level (Default 2). - -tick Scorebot poll tate, in seconds (Default 5). - -timeout Scoreboard request timeout, in seconds (Default 10). - -bind Address and port to listen on (Default "0.0.0.0:8080"). - -cert Path to TLS certificate file. - -key Path to TLS key file. - -tw-ck Twitter Consumer API key. - -tw-cs Twitter Consumer API secret. - -tw-ak Twitter Access API key. - -tw-as Twitter Access API secret. - -tw-keywords Twitter search keywords (Comma separated) - -tw-lang Twitter search language (Comma separated) - -tw-expire Tweet display time, in seconds (Default 45). - -tw-block-words Twitter blocked words (Comma separated). - -tw-block-user Twitter blocked Usernames (Comma separated). - -tw-only-users Twitter whitelisted Usernames (Comma separated). - -Copyright (C) 2020 iDigitalFlame - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - -` - version = 2.2 -) - var resources = packr.New("html", "../html") type display struct { Game uint64 Twitter bool } -type errorval struct { +type errval struct { e error s string } @@ -121,20 +72,35 @@ type Scoreboard struct { // function blocks until interrupted. This function watches the SIGINT, SIGHUP, SIGTERM and SIGQUIT // signals and will automatically close and clean up after a signal is received. func (s *Scoreboard) Run() error { - return s.RunContext(context.Background()) -} -func (e errorval) Error() string { - if e.e == nil { - return e.s + var ( + err error + w = make(chan os.Signal, 1) + x, c = context.WithCancel(context.Background()) + ) + s.Server.BaseContext = func(_ net.Listener) context.Context { + return x } - return e.s + ": " + e.e.Error() -} -func (e errorval) Unwrap() error { - return e.e + signal.Notify(w, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) + s.log.Info("Starting Scoreboard service...") + go s.listen(&err, c) + go s.twitter(x) + go s.Manager.Start(x) + select { + case <-w: + case <-x.Done(): + } + close(w) + if c(); err != nil { + s.log.Error("Received error during runtime: %s!", err.Error()) + } + s.log.Info("Stopping and shutting down...") + f, u := context.WithTimeout(x, s.ReadTimeout) + s.Server.Shutdown(f) + err = s.Server.Close() + u() + return err } - -// New creates a new scoreboard instance from the provided Config struct. Any errors during setup will be returned. -func (c Config) New() (*Scoreboard, error) { +func (c config) New() (*Scoreboard, error) { if err := c.verify(); err != nil { return nil, err } @@ -147,10 +113,10 @@ func (c Config) New() (*Scoreboard, error) { p = filepath.Join(c.Directory, "public") d, err := os.Stat(p) if err != nil { - return nil, &errorval{s: `public directory "` + p + `" does not exist`, e: err} + return nil, &errval{s: `public directory "` + p + `" does not exist`, e: err} } if !d.IsDir() { - return nil, &errorval{s: `public directory "` + p + `" is not a directory`} + return nil, &errval{s: `public directory "` + p + `" is not a directory`} } x = filepath.Join(c.Directory, "template") } @@ -158,7 +124,7 @@ func (c Config) New() (*Scoreboard, error) { if len(c.Log.File) > 0 { f, err := logx.File(c.Log.File, logx.Level(c.Log.Level)) if err != nil { - return nil, &errorval{s: `unable to create log file "` + c.Log.File + `"`, e: err} + return nil, &errval{s: `unable to create log file "` + c.Log.File + `"`, e: err} } s.log = logx.Multiple(f, logx.Console(logx.Level(c.Log.Level))) } else { @@ -166,13 +132,13 @@ func (c Config) New() (*Scoreboard, error) { } s.html = template.New("base") if err = getTemplate(s.html, x, "home.html"); err != nil { - return nil, &errorval{s: "unable to load home template", e: err} + return nil, &errval{s: "unable to load home template", e: err} } if err = getTemplate(s.html, x, "scoreboard.html"); err != nil { - return nil, &errorval{s: "unable to load scoreboard template", e: err} + return nil, &errval{s: "unable to load scoreboard template", e: err} } if s.Manager, err = game.New(c.Scorebot, c.Assets, time.Duration(c.Tick)*time.Second, t, s.log); err != nil { - return nil, &errorval{s: "unable to setup game manager", e: err} + return nil, &errval{s: "unable to setup game manager", e: err} } s.Server = &http.Server{ Addr: c.Listen, @@ -196,7 +162,7 @@ func (c Config) New() (*Scoreboard, error) { ), ) if _, _, err := y.Accounts.VerifyCredentials(nil); err != nil { - return nil, &errorval{s: "cannot authenticate to Twitter: %w", e: err} + return nil, &errval{s: "cannot authenticate to Twitter: %w", e: err} } s.feed, err = y.Streams.Filter( &twitter.StreamFilterParams{ @@ -206,7 +172,7 @@ func (c Config) New() (*Scoreboard, error) { }, ) if err != nil { - return nil, &errorval{s: "unable to start Twitter filter", e: err} + return nil, &errval{s: "unable to start Twitter filter", e: err} } s.filter, s.expire = c.Twitter.Filter, time.Duration(c.Twitter.Expire)*time.Second s.log.Info("Twitter setup successful!") @@ -219,7 +185,10 @@ func (c Config) New() (*Scoreboard, error) { s.Server.Handler.(*http.ServeMux).HandleFunc("/w", s.httpWebsocket) return &s, nil } -func (s *Scoreboard) startTwitter(x context.Context) { +func (s *Scoreboard) twitter(x context.Context) { + if s.feed == nil { + return + } for c := s.Manager.Twitter(s.expire); ; { select { case <-x.Done(): @@ -270,63 +239,42 @@ func getTemplate(t *template.Template, d, f string) error { s := filepath.Join(d, f) if i, err := os.Stat(s); err == nil && !i.IsDir() { if _, err = t.New(f).ParseFiles(s); err != nil { - return &errorval{s: `unable to parse template "` + f + `"`, e: err} + return &errval{s: `unable to parse template "` + f + `"`, e: err} } return nil } } c, err := resources.FindString("template/" + f) if err != nil { - return &errorval{s: `could not find template "` + f + `"`, e: err} + return &errval{s: `could not find template "` + f + `"`, e: err} } if _, err := t.New(f).Parse(c); err != nil { - return &errorval{s: `unable to parse scorebot template "` + f + `"`, e: err} + return &errval{s: `unable to parse scorebot template "` + f + `"`, e: err} } return nil } - -// RunContext begins the listening process for the Scoreboard and the Game ticking threads. This -// function blocks until interrupted. This function watches the SIGINT, SIGTERM and SIGQUIT signals and will -// automatically close and clean up after a signal is received. This function accepts a Context to allow for control -// of when the Scoreboard stops without using signals. -func (s *Scoreboard) RunContext(ctx context.Context) error { - var ( - e = make(chan error, 1) - w = make(chan os.Signal, 1) - x, c = context.WithCancel(ctx) - ) - signal.Notify(w, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) - s.log.Info("Starting Scoreboard service...") - if len(s.cert) > 0 && len(s.key) > 0 { - go func() { - e <- s.Server.ListenAndServeTLS(s.cert, s.key) - }() - } else { - go func() { - e <- s.Server.ListenAndServe() - }() - } - if s.feed != nil { - go s.startTwitter(x) +func (s *Scoreboard) listen(err *error, f context.CancelFunc) { + if len(s.cert) == 0 || len(s.key) == 0 { + *err = s.Server.ListenAndServe() + f() + return } - go s.Manager.Start(x) - select { - case <-w: - case err := <-e: - if err != nil { - s.log.Error("Received error during startup: %s!", err.Error()) - } - case <-x.Done(): + s.Server.TLSConfig = &tls.Config{ + NextProtos: []string{"h2", "http/1.1"}, + MinVersion: tls.VersionTLS12, + CipherSuites: []uint16{ + tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, + tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + }, + CurvePreferences: []tls.CurveID{tls.CurveP256, tls.X25519}, + PreferServerCipherSuites: true, } - c() - s.log.Info("Stopping and shutting down...") - f, u := context.WithTimeout(x, s.ReadTimeout) - s.Server.Shutdown(f) - err := s.Server.Close() - u() - close(e) - close(w) - return err + *err = s.Server.ListenAndServeTLS(s.cert, s.key) + f() } func (s *Scoreboard) http(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet {