From 7962fad02e44f98c3cf008b17f99769ee2d71b5c Mon Sep 17 00:00:00 2001 From: joeyak Date: Sat, 1 Jun 2019 19:41:28 -0400 Subject: [PATCH 1/9] commiting so zorch can do my work --- chatcommands.go | 13 +- chatroom.go | 4 +- common/emotes.go | 45 ++---- common/emotes_test.go | 20 ++- emotes.go | 318 +++++++++++++++++++++++++----------------- go.mod | 2 +- go.sum | 1 + handlers.go | 3 +- main.go | 21 ++- settings.go | 5 +- 10 files changed, 252 insertions(+), 180 deletions(-) diff --git a/chatcommands.go b/chatcommands.go index d466c46..c18b6e3 100644 --- a/chatcommands.go +++ b/chatcommands.go @@ -439,7 +439,7 @@ var commands = &CommandControl{ HelpText: "Reload the emotes on the server.", Function: func(cl *Client, args []string) (string, error) { cl.SendServerMessage("Reloading emotes") - num, err := common.LoadEmotes() + err := loadEmotes() if err != nil { common.LogErrorf("Unbale to reload emotes: %s\n", err) return "", err @@ -447,6 +447,8 @@ var commands = &CommandControl{ cl.belongsTo.AddChatMsg(common.NewChatHiddenMessage(common.CdEmote, common.Emotes)) cl.belongsTo.AddModNotice(cl.name + " has reloaded emotes") + + num := len(Emotes) common.LogInfof("Loaded %d emotes\n", num) return fmt.Sprintf("Emotes loaded: %d", num), nil }, @@ -525,7 +527,7 @@ var commands = &CommandControl{ go func() { // Pretty sure this breaks on partial downloads (eg, one good channel and one non-existent) - _, err := GetEmotes(args) + err := getEmotes(args) if err != nil { cl.SendChatData(common.NewChatMessage("", "", err.Error(), @@ -533,8 +535,13 @@ var commands = &CommandControl{ return } + // If the emotes were able to be downloaded, add the channels to settings + settingsMtx.Lock() + settings.ApprovedEmotes = append(settings.ApprovedEmotes, args...) + settingsMtx.Unlock() + // reload emotes now that new ones were added - _, err = common.LoadEmotes() + err = loadEmotes() if err != nil { cl.SendChatData(common.NewChatMessage("", "", err.Error(), diff --git a/chatroom.go b/chatroom.go index 2d37572..1111189 100644 --- a/chatroom.go +++ b/chatroom.go @@ -36,11 +36,11 @@ func newChatRoom() (*ChatRoom, error) { clients: []*Client{}, } - num, err := common.LoadEmotes() + err := loadEmotes() if err != nil { return nil, fmt.Errorf("error loading emotes: %s", err) } - common.LogInfof("Loaded %d emotes\n", num) + common.LogInfof("Loaded %d emotes\n", len(Emotes)) //the "heartbeat" for broadcasting messages go cr.Broadcast() diff --git a/common/emotes.go b/common/emotes.go index 35f330d..3bff321 100644 --- a/common/emotes.go +++ b/common/emotes.go @@ -2,14 +2,23 @@ package common import ( "fmt" - "path/filepath" + "path" "strings" ) -var Emotes map[string]string +var Emotes map[string]EmotePath + +type EmotePath struct { + Dir string + File string +} + +func (e EmotePath) path() string { + return path.Join(e.Dir, e.File) +} func EmoteToHtml(file, title string) string { - return fmt.Sprintf(``, file, title) + return fmt.Sprintf(``, file, title) } func ParseEmotesArray(words []string) []string { @@ -21,7 +30,7 @@ func ParseEmotesArray(words []string) []string { found := false for key, val := range Emotes { if key == wordTrimmed { - newWords = append(newWords, EmoteToHtml(val, key)) + newWords = append(newWords, EmoteToHtml(val.File, key)) found = true } } @@ -36,31 +45,3 @@ func ParseEmotes(msg string) string { words := ParseEmotesArray(strings.Split(msg, " ")) return strings.Join(words, " ") } - -func LoadEmotes() (int, error) { - newEmotes := map[string]string{} - - emotePNGs, err := filepath.Glob("./static/emotes/*.png") - if err != nil { - return 0, fmt.Errorf("unable to glob emote directory: %s\n", err) - } - - emoteGIFs, err := filepath.Glob("./static/emotes/*.gif") - if err != nil { - return 0, fmt.Errorf("unable to glob emote directory: %s\n", err) - } - globbed_files := []string(emotePNGs) - globbed_files = append(globbed_files, emoteGIFs...) - - LogInfoln("Loading emotes...") - emInfo := []string{} - for _, file := range globbed_files { - file = filepath.Base(file) - key := file[0 : len(file)-4] - newEmotes[key] = file - emInfo = append(emInfo, key) - } - Emotes = newEmotes - LogInfoln(strings.Join(emInfo, " ")) - return len(Emotes), nil -} diff --git a/common/emotes_test.go b/common/emotes_test.go index f531b6f..376dadb 100644 --- a/common/emotes_test.go +++ b/common/emotes_test.go @@ -1,7 +1,6 @@ package common import ( - "os" "testing" ) @@ -26,12 +25,21 @@ var data_good = map[string]string{ } func TestMain(m *testing.M) { - Emotes = map[string]string{ - "one": "one.png", - "two": "two.png", - "three": "three.gif", + Emotes = map[string]EmotePath{ + "one": EmotePath{ + Dir: "", + File: "one.png", + }, + "two": EmotePath{ + Dir: "", + File: "two.png", + }, + "three": EmotePath{ + Dir: "", + File: "three.gif", + }, } - os.Exit(m.Run()) + // os.Exit(m.Run()) } func TestEmotes_ParseEmotes(t *testing.T) { diff --git a/emotes.go b/emotes.go index dcd6335..c9a0431 100644 --- a/emotes.go +++ b/emotes.go @@ -4,164 +4,224 @@ import ( "encoding/json" "fmt" "io" + "log" "net/http" "os" + "path" "strings" - "github.com/zorchenhimer/MovieNight/common" + "github.com/pkg/errors" ) -type twitchChannel struct { - ChannelName string `json:"channel_name"` - DisplayName string `json:"display_name"` - ChannelId string `json:"channel_id"` - BroadcasterType string `json:"broadcaster_type"` - Plans map[string]string `json:"plans"` - Emotes []struct { - Code string `json:"code"` - Set int `json:"emoticon_set"` - Id int `json:"id"` - } `json:"emotes"` - BaseSetId string `json:"base_set_id"` - GeneratedAt string `json:"generated_at"` +const emoteDir = "./static/emotes/" + +var Emotes map[string]Emote + +type Emote struct { + Dir string + File string } -// Used in settings -type EmoteSet struct { - Channel string // channel name - Prefix string // emote prefix - Found bool `json:"-"` +func (e Emote) path() string { + return path.Join(e.Dir, e.File) } -const subscriberJson string = `subscribers.json` +type TwitchUser struct { + ID string + Login string +} -// Download a single channel's emote set -func (tc *twitchChannel) downloadEmotes() (*EmoteSet, error) { - es := &EmoteSet{Channel: strings.ToLower(tc.ChannelName)} - for _, emote := range tc.Emotes { - url := fmt.Sprintf(`https://static-cdn.jtvnw.net/emoticons/v1/%d/1.0`, emote.Id) - png := `static/emotes/` + emote.Code + `.png` +type EmoteInfo struct { + ID int + Code string +} - if len(es.Prefix) == 0 { - // For each letter - for i := 0; i < len(emote.Code); i++ { - // Find the first capital - b := emote.Code[i] - if b >= 'A' && b <= 'Z' { - es.Prefix = emote.Code[0 : i-1] - common.LogDebugf("Found prefix for channel %q: %q (%q)\n", es.Channel, es.Prefix, emote) - break +// func loadEmotes() error { +// newEmotes := map[string]string{} + +// emotePNGs, err := filepath.Glob("./static/emotes/*.png") +// if err != nil { +// return 0, fmt.Errorf("unable to glob emote directory: %s\n", err) +// } + +// emoteGIFs, err := filepath.Glob("./static/emotes/*.gif") +// if err != nil { +// return 0, fmt.Errorf("unable to glob emote directory: %s\n", err) +// } +// globbed_files := []string(emotePNGs) +// globbed_files = append(globbed_files, emoteGIFs...) + +// LogInfoln("Loading emotes...") +// emInfo := []string{} +// for _, file := range globbed_files { +// file = filepath.Base(file) +// key := file[0 : len(file)-4] +// newEmotes[key] = file +// emInfo = append(emInfo, key) +// } +// Emotes = newEmotes +// LogInfoln(strings.Join(emInfo, " ")) +// return len(Emotes), nil +// } + +func loadEmotes() error { + fmt.Println(processEmoteDir(emoteDir)) + return nil +} + +func processEmoteDir(path string) ([]Emote, error) { + dir, err := os.Open(path) + if err != nil { + return nil, errors.Wrap(err, "could not open emoteDir:") + } + + files, err := dir.Readdir(0) + if err != nil { + return nil, errors.Wrap(err, "could not get files:") + } + + var emotes []Emote + for _, file := range files { + emotes = append(emotes, Emote{Dir: path, File: file.Name()}) + } + + subdir, err := dir.Readdirnames(0) + if err != nil { + return nil, errors.Wrap(err, "could not get sub directories:") + } + + for _, d := range subdir { + subEmotes, err := processEmoteDir(d) + if err != nil { + return nil, errors.Wrapf(err, "could not process sub directory \"%s\":", d) + } + emotes = append(emotes, subEmotes...) + } + + return emotes, nil +} + +func getEmotes(names []string) error { + users := getUserIDs(names) + users = append(users, TwitchUser{ID: "0", Login: "global"}) + + for _, user := range users { + emotes, cheers, err := getChannelEmotes(user.ID) + if err != nil { + return errors.Wrapf(err, "could not get emote data for \"%s\"", user.ID) + } + + emoteUserDir := path.Join(emoteDir, "twitch", user.Login) + if _, err := os.Stat(emoteUserDir); os.IsNotExist(err) { + os.MkdirAll(emoteUserDir, os.ModePerm) + } + + for _, emote := range emotes { + if !strings.ContainsAny(emote.Code, `:;\[]|?&`) { + filePath := path.Join(emoteUserDir, emote.Code+".png") + file, err := os.Create(filePath) + if err != nil { + + return errors.Wrapf(err, "could not create emote file in path \"%s\":", filePath) + } + + err = downloadEmote(emote.ID, file) + if err != nil { + return errors.Wrapf(err, "could not download emote %s:", emote.Code) } } } - resp, err := http.Get(url) - if err != nil { - return nil, err - } + for amount, sizes := range cheers { + name := fmt.Sprintf("%sCheer%s.gif", user.Login, amount) + filePath := path.Join(emoteUserDir, name) + file, err := os.Create(filePath) + if err != nil { + return errors.Wrapf(err, "could not create emote file in path \"%s\":", filePath) + } - f, err := os.Create(png) - if err != nil { - return nil, err - } - - _, err = io.Copy(f, resp.Body) - if err != nil { - return nil, err - } - } - - return es, nil -} - -func GetEmotes(names []string) ([]*EmoteSet, error) { - // Do this up-front - for i := 0; i < len(names); i++ { - names[i] = strings.ToLower(names[i]) - } - - channels, err := findChannels(names) - if err != nil { - return nil, fmt.Errorf("Error reading %q: %v", subscriberJson, err) - } - - emoteSets := []*EmoteSet{} - for _, c := range channels { - es, err := c.downloadEmotes() - if err != nil { - return nil, fmt.Errorf("Error downloading emotes: %v", err) - } - emoteSets = append(emoteSets, es) - } - - for _, es := range emoteSets { - found := false - for _, name := range names { - if es.Channel == name { - found = true - break + err = downloadCheerEmote(sizes["4"], file) + if err != nil { + return errors.Wrapf(err, "could not download emote %s:", name) } } - if !found { - es.Found = false - } } - - return emoteSets, nil + return nil } -func findChannels(names []string) ([]twitchChannel, error) { - file, err := os.Open(subscriberJson) +func getUserIDs(names []string) []TwitchUser { + logins := strings.Join(names, "&login=") + request, err := http.NewRequest("GET", fmt.Sprintf("https://api.twitch.tv/helix/users?login=%s", logins), nil) if err != nil { - return nil, err + log.Fatalln("Error generating new request:", err) } - defer file.Close() + request.Header.Set("Client-ID", settings.TwitchClientID) - data := []twitchChannel{} - dec := json.NewDecoder(file) - - // Open bracket - _, err = dec.Token() + client := http.Client{} + resp, err := client.Do(request) if err != nil { - return nil, err + log.Fatalln("Error sending request:", err) } - done := false - for dec.More() && !done { - // opening bracket of channel - _, err = dec.Token() - if err != nil { - return nil, err - } + decoder := json.NewDecoder(resp.Body) + type userResponse struct { + Data []TwitchUser + } + var data userResponse - // Decode the channel stuff - var c twitchChannel - err = dec.Decode(&c) - if err != nil { - return nil, err - } - - // Is this a channel we are looking for? - found := false - for _, search := range names { - if strings.ToLower(c.ChannelName) == search { - found = true - break - } - } - - // Yes it is. Add it to the data - if found { - data = append(data, c) - } - - // Check for completion. Don't bother parsing the rest of - // the json file if we've already found everything that we're - // looking for. - if len(data) == len(names) { - done = true - } + err = decoder.Decode(&data) + if err != nil { + log.Fatalln("Error decoding data:", err) } - return data, nil + return data.Data +} + +func getChannelEmotes(ID string) ([]EmoteInfo, map[string]map[string]string, error) { + resp, err := http.Get("https://api.twitchemotes.com/api/v4/channels/" + ID) + if err != nil { + return nil, nil, errors.Wrap(err, "could not get emotes") + } + decoder := json.NewDecoder(resp.Body) + + type EmoteResponse struct { + Emotes []EmoteInfo + Cheermotes map[string]map[string]string + } + var data EmoteResponse + + err = decoder.Decode(&data) + if err != nil { + return nil, nil, errors.Wrap(err, "could not decode emotes") + } + + return data.Emotes, data.Cheermotes, nil +} + +func downloadEmote(ID int, file *os.File) error { + resp, err := http.Get(fmt.Sprintf("https://static-cdn.jtvnw.net/emoticons/v1/%d/3.0", ID)) + if err != nil { + return errors.Errorf("could not download emote file %s: %v", file.Name(), err) + } + defer resp.Body.Close() + + _, err = io.Copy(file, resp.Body) + if err != nil { + return errors.Errorf("could not save emote: %v", err) + } + return nil +} + +func downloadCheerEmote(url string, file *os.File) error { + resp, err := http.Get(url) + if err != nil { + return errors.Errorf("could not download cheer file %s: %v", file.Name(), err) + } + defer resp.Body.Close() + + _, err = io.Copy(file, resp.Body) + if err != nil { + return errors.Errorf("could not save cheer: %v", err) + } + return nil } diff --git a/go.mod b/go.mod index 94936ce..de0ebd0 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983 // indirect github.com/nareix/joy4 v0.0.0-20181022032202-3ddbc8f9d431 github.com/ory/dockertest v3.3.4+incompatible // indirect - github.com/pkg/errors v0.8.1 // indirect + github.com/pkg/errors v0.8.1 github.com/sirupsen/logrus v1.4.1 // indirect github.com/stretchr/objx v0.2.0 // indirect golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a // indirect diff --git a/go.sum b/go.sum index 8b632a2..6ddf141 100644 --- a/go.sum +++ b/go.sum @@ -65,6 +65,7 @@ github.com/ory/dockertest v3.3.2+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnh github.com/ory/dockertest v3.3.4+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/handlers.go b/handlers.go index c75d306..a7da689 100644 --- a/handlers.go +++ b/handlers.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net/http" + "path" "path/filepath" "strings" "sync" @@ -79,7 +80,7 @@ func wsImages(w http.ResponseWriter, r *http.Request) { func wsEmotes(w http.ResponseWriter, r *http.Request) { emotefile := filepath.Base(r.URL.Path) - http.ServeFile(w, r, "./static/emotes/"+emotefile) + http.ServeFile(w, r, path.Join(emoteDir, emotefile)) } // Handling the websocket diff --git a/main.go b/main.go index ce470ef..ab013ff 100644 --- a/main.go +++ b/main.go @@ -14,9 +14,10 @@ import ( ) var ( - addr string - sKey string - stats = newStreamStats() + pullEmotes bool + addr string + sKey string + stats = newStreamStats() ) func setupSettings() error { @@ -42,6 +43,7 @@ func setupSettings() error { func main() { flag.StringVar(&addr, "l", "", "host:port of the MovieNight") flag.StringVar(&sKey, "k", "", "Stream key, to protect your stream") + flag.BoolVar(&pullEmotes, "e", false, "Pull emotes") flag.Parse() format.RegisterAll() @@ -51,6 +53,16 @@ func main() { os.Exit(1) } + if pullEmotes { + common.LogInfoln("Pulling emotes") + err := getEmotes(settings.ApprovedEmotes) + if err != nil { + common.LogErrorf("Error downloading emotes: %+v\n", err) + common.LogErrorf("Error downloading emotes: %v\n", err) + os.Exit(1) + } + } + if err := common.InitTemplates(); err != nil { common.LogErrorln(err) os.Exit(1) @@ -61,7 +73,8 @@ func main() { // Load emotes before starting server. var err error - if chat, err = newChatRoom(); err != nil { + chat, err = newChatRoom() + if err != nil { common.LogErrorln(err) os.Exit(1) } diff --git a/settings.go b/settings.go index f8f1717..086dc87 100644 --- a/settings.go +++ b/settings.go @@ -30,8 +30,9 @@ type Settings struct { AdminPassword string StreamKey string ListenAddress string - ApprovedEmotes []EmoteSet // list of channels that have been approved for emote use. Global emotes are always "approved". - SessionKey string // key for session data + ApprovedEmotes []string // list of channels that have been approved for emote use. Global emotes are always "approved". + TwitchClientID string // client id from twitch developers portal + SessionKey string // key for session data Bans []BanInfo LogLevel common.LogLevel LogFile string From 35774b061f474a7fa4dd778c573f96ef66280047 Mon Sep 17 00:00:00 2001 From: Zorchenhimer Date: Tue, 18 Jun 2019 18:46:35 -0400 Subject: [PATCH 2/9] Remove goimports Replace it with gofmt. This has been an issue preventing people from easily getting setup for dev. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 2b174b9..4a67cbc 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ clean: -rm MovieNight.exe MovieNight ./static/main.wasm fmt: - goimports -w . + gofmt -w . get: go get golang.org/x/tools/cmd/goimports From a73375f1527302875fdbeda22dceb733a01f65e2 Mon Sep 17 00:00:00 2001 From: Zorchenhimer Date: Tue, 18 Jun 2019 22:13:53 -0400 Subject: [PATCH 3/9] Fix emotes This reworks how emotes are cached in relation to their physical location on disk. Functionally, they should be the same from the user perspective but it sets up some stuff that will make it easier to add emotes from various sources. --- chatcommands.go | 30 +++++---- chatroom.go | 2 +- common/emotes.go | 38 +++++++++--- common/emotes_test.go | 20 ++---- emotes.go | 138 +++++++++++++++++++++--------------------- handlers.go | 3 +- settings_example.json | 2 +- 7 files changed, 128 insertions(+), 105 deletions(-) diff --git a/chatcommands.go b/chatcommands.go index c18b6e3..40fa62a 100644 --- a/chatcommands.go +++ b/chatcommands.go @@ -438,19 +438,27 @@ var commands = &CommandControl{ common.CNReloadEmotes.String(): Command{ HelpText: "Reload the emotes on the server.", Function: func(cl *Client, args []string) (string, error) { - cl.SendServerMessage("Reloading emotes") - err := loadEmotes() - if err != nil { - common.LogErrorf("Unbale to reload emotes: %s\n", err) - return "", err - } + go func() { + cl.SendServerMessage("Reloading emotes") + err := loadEmotes() + if err != nil { + common.LogErrorf("Unbale to reload emotes: %s\n", err) + //return "", err - cl.belongsTo.AddChatMsg(common.NewChatHiddenMessage(common.CdEmote, common.Emotes)) - cl.belongsTo.AddModNotice(cl.name + " has reloaded emotes") + cl.SendChatData(common.NewChatMessage("", "", + err.Error(), + common.CmdlUser, common.MsgCommandResponse)) + return + } - num := len(Emotes) - common.LogInfof("Loaded %d emotes\n", num) - return fmt.Sprintf("Emotes loaded: %d", num), nil + cl.belongsTo.AddChatMsg(common.NewChatHiddenMessage(common.CdEmote, common.Emotes)) + cl.belongsTo.AddModNotice(cl.name + " has reloaded emotes") + + num := len(common.Emotes) + common.LogInfof("Loaded %d emotes\n", num) + cl.belongsTo.AddModNotice(fmt.Sprintf("%s reloaded %d emotes.", cl.name, num)) + }() + return "Reloading emotes...", nil }, }, diff --git a/chatroom.go b/chatroom.go index 1111189..c0c6342 100644 --- a/chatroom.go +++ b/chatroom.go @@ -40,7 +40,7 @@ func newChatRoom() (*ChatRoom, error) { if err != nil { return nil, fmt.Errorf("error loading emotes: %s", err) } - common.LogInfof("Loaded %d emotes\n", len(Emotes)) + common.LogInfof("Loaded %d emotes\n", len(common.Emotes)) //the "heartbeat" for broadcasting messages go cr.Broadcast() diff --git a/common/emotes.go b/common/emotes.go index 3bff321..fd49224 100644 --- a/common/emotes.go +++ b/common/emotes.go @@ -2,19 +2,41 @@ package common import ( "fmt" - "path" + "path/filepath" + "regexp" "strings" ) -var Emotes map[string]EmotePath +type EmotesMap map[string]string -type EmotePath struct { - Dir string - File string +var Emotes EmotesMap + +var reStripStatic = regexp.MustCompile(`^(\\|/)?static`) + +func init() { + Emotes = map[string]string{} } -func (e EmotePath) path() string { - return path.Join(e.Dir, e.File) +func (em EmotesMap) Add(fullpath string) { + fullpath = reStripStatic.ReplaceAllLiteralString(fullpath, "") + + base := filepath.Base(fullpath) + code := base[0 : len(base)-len(filepath.Ext(base))] + + _, exists := em[code] + + num := 0 + for exists { + num += 1 + _, exists = em[fmt.Sprintf("%s-%d", code, num)] + } + + if num > 0 { + code = fmt.Sprintf("%s-%d", code, num) + } + + Emotes[code] = fullpath + fmt.Printf("Added emote %s at path %q\n", code, fullpath) } func EmoteToHtml(file, title string) string { @@ -30,7 +52,7 @@ func ParseEmotesArray(words []string) []string { found := false for key, val := range Emotes { if key == wordTrimmed { - newWords = append(newWords, EmoteToHtml(val.File, key)) + newWords = append(newWords, EmoteToHtml(val, key)) found = true } } diff --git a/common/emotes_test.go b/common/emotes_test.go index 376dadb..bb31646 100644 --- a/common/emotes_test.go +++ b/common/emotes_test.go @@ -1,6 +1,7 @@ package common import ( + "os" "testing" ) @@ -25,21 +26,12 @@ var data_good = map[string]string{ } func TestMain(m *testing.M) { - Emotes = map[string]EmotePath{ - "one": EmotePath{ - Dir: "", - File: "one.png", - }, - "two": EmotePath{ - Dir: "", - File: "two.png", - }, - "three": EmotePath{ - Dir: "", - File: "three.gif", - }, + Emotes = map[string]string{ + "one": "/emotes/one.png", + "two": "/emotes/two.png", + "three": "/emotes/three.gif", } - // os.Exit(m.Run()) + os.Exit(m.Run()) } func TestEmotes_ParseEmotes(t *testing.T) { diff --git a/emotes.go b/emotes.go index c9a0431..34e80df 100644 --- a/emotes.go +++ b/emotes.go @@ -4,28 +4,19 @@ import ( "encoding/json" "fmt" "io" + "io/ioutil" "log" "net/http" "os" - "path" + "path/filepath" "strings" "github.com/pkg/errors" + "github.com/zorchenhimer/MovieNight/common" ) const emoteDir = "./static/emotes/" -var Emotes map[string]Emote - -type Emote struct { - Dir string - File string -} - -func (e Emote) path() string { - return path.Join(e.Dir, e.File) -} - type TwitchUser struct { ID string Login string @@ -36,74 +27,85 @@ type EmoteInfo struct { Code string } -// func loadEmotes() error { -// newEmotes := map[string]string{} - -// emotePNGs, err := filepath.Glob("./static/emotes/*.png") -// if err != nil { -// return 0, fmt.Errorf("unable to glob emote directory: %s\n", err) -// } - -// emoteGIFs, err := filepath.Glob("./static/emotes/*.gif") -// if err != nil { -// return 0, fmt.Errorf("unable to glob emote directory: %s\n", err) -// } -// globbed_files := []string(emotePNGs) -// globbed_files = append(globbed_files, emoteGIFs...) - -// LogInfoln("Loading emotes...") -// emInfo := []string{} -// for _, file := range globbed_files { -// file = filepath.Base(file) -// key := file[0 : len(file)-4] -// newEmotes[key] = file -// emInfo = append(emInfo, key) -// } -// Emotes = newEmotes -// LogInfoln(strings.Join(emInfo, " ")) -// return len(Emotes), nil -// } - func loadEmotes() error { - fmt.Println(processEmoteDir(emoteDir)) + //fmt.Println(processEmoteDir(emoteDir)) + err := processEmoteDir(emoteDir) + if err != nil { + return err + } + return nil } -func processEmoteDir(path string) ([]Emote, error) { - dir, err := os.Open(path) +func processEmoteDir(path string) error { + dirInfo, err := ioutil.ReadDir(path) if err != nil { - return nil, errors.Wrap(err, "could not open emoteDir:") + return errors.Wrap(err, "could not open emoteDir:") } - files, err := dir.Readdir(0) - if err != nil { - return nil, errors.Wrap(err, "could not get files:") - } + subDirs := []string{} - var emotes []Emote - for _, file := range files { - emotes = append(emotes, Emote{Dir: path, File: file.Name()}) - } - - subdir, err := dir.Readdirnames(0) - if err != nil { - return nil, errors.Wrap(err, "could not get sub directories:") - } - - for _, d := range subdir { - subEmotes, err := processEmoteDir(d) - if err != nil { - return nil, errors.Wrapf(err, "could not process sub directory \"%s\":", d) + for _, item := range dirInfo { + // Get first level subdirs (eg, "twitch", "discord", etc) + if item.IsDir() { + subDirs = append(subDirs, item.Name()) + continue } - emotes = append(emotes, subEmotes...) } - return emotes, nil + // Find top level emotes + err = findEmotes(path) + if err != nil { + return errors.Wrap(err, "could not findEmotes() in top level directory:") + } + + // Get second level subdirs (eg, "twitch", "zorchenhimer", etc) + for _, dir := range subDirs { + subd, err := ioutil.ReadDir(filepath.Join(path, dir)) + if err != nil { + continue + } + for _, d := range subd { + if d.IsDir() { + //emotes = append(emotes, findEmotes(filepath.Join(path, dir, d.Name()))...) + findEmotes(filepath.Join(path, dir, d.Name())) + } + } + } + + return nil +} + +func findEmotes(dir string) error { + fmt.Printf("finding emotes in %q\n", dir) + emotePNGs, err := filepath.Glob(filepath.Join(dir, "*.png")) + if err != nil { + //return 0, fmt.Errorf("unable to glob emote directory: %s\n", err) + return nil + } + fmt.Printf("%d emotePNGs\n", len(emotePNGs)) + + emoteGIFs, err := filepath.Glob(filepath.Join(dir, "*.gif")) + if err != nil { + return errors.Wrap(err, "unable to glob emote directory:") + } + fmt.Printf("%d emoteGIFs\n", len(emoteGIFs)) + + for _, file := range emotePNGs { + common.Emotes.Add(file) + //emotes = append(emotes, common.Emote{FullPath: dir, Code: file}) + } + + for _, file := range emoteGIFs { + common.Emotes.Add(file) + } + + return nil } func getEmotes(names []string) error { users := getUserIDs(names) - users = append(users, TwitchUser{ID: "0", Login: "global"}) + users = append(users, TwitchUser{ID: "0", Login: "twitch"}) for _, user := range users { emotes, cheers, err := getChannelEmotes(user.ID) @@ -111,14 +113,14 @@ func getEmotes(names []string) error { return errors.Wrapf(err, "could not get emote data for \"%s\"", user.ID) } - emoteUserDir := path.Join(emoteDir, "twitch", user.Login) + emoteUserDir := filepath.Join(emoteDir, "twitch", user.Login) if _, err := os.Stat(emoteUserDir); os.IsNotExist(err) { os.MkdirAll(emoteUserDir, os.ModePerm) } for _, emote := range emotes { if !strings.ContainsAny(emote.Code, `:;\[]|?&`) { - filePath := path.Join(emoteUserDir, emote.Code+".png") + filePath := filepath.Join(emoteUserDir, emote.Code+".png") file, err := os.Create(filePath) if err != nil { @@ -134,7 +136,7 @@ func getEmotes(names []string) error { for amount, sizes := range cheers { name := fmt.Sprintf("%sCheer%s.gif", user.Login, amount) - filePath := path.Join(emoteUserDir, name) + filePath := filepath.Join(emoteUserDir, name) file, err := os.Create(filePath) if err != nil { return errors.Wrapf(err, "could not create emote file in path \"%s\":", filePath) diff --git a/handlers.go b/handlers.go index a7da689..fbbcc41 100644 --- a/handlers.go +++ b/handlers.go @@ -79,8 +79,7 @@ func wsImages(w http.ResponseWriter, r *http.Request) { } func wsEmotes(w http.ResponseWriter, r *http.Request) { - emotefile := filepath.Base(r.URL.Path) - http.ServeFile(w, r, path.Join(emoteDir, emotefile)) + http.ServeFile(w, r, path.Join("./static/", r.URL.Path)) } // Handling the websocket diff --git a/settings_example.json b/settings_example.json index 9700d7b..ea69bc9 100644 --- a/settings_example.json +++ b/settings_example.json @@ -1,6 +1,6 @@ { "MaxMessageCount": 300, - "TitleLength": 50, + "TitleLength": 50, "AdminPassword": "", "Bans": [], "StreamKey": "ALongStreamKey", From 5a5c6c76b023d946e4f060e2563fb260048686da Mon Sep 17 00:00:00 2001 From: Zorchenhimer Date: Tue, 18 Jun 2019 22:15:10 -0400 Subject: [PATCH 4/9] Remove goimports from Makefile and readme --- Makefile | 3 --- readme.md | 1 - 2 files changed, 4 deletions(-) diff --git a/Makefile b/Makefile index 4a67cbc..61a44f6 100644 --- a/Makefile +++ b/Makefile @@ -24,9 +24,6 @@ clean: fmt: gofmt -w . -get: - go get golang.org/x/tools/cmd/goimports - vet: go vet $(TAGS) ./... GOOS=js GOARCH=wasm go vet $(TAGS) ./... diff --git a/readme.md b/readme.md index edb1773..020426d 100644 --- a/readme.md +++ b/readme.md @@ -18,7 +18,6 @@ To just download and run: ```bash $ git clone https://github.com/zorchenhimer/MovieNight $ cd MovieNight -$ make get # only needs to be run once $ make $ ./MovieNight ``` From 42bcead6277a365230e3795283c4bc1d63c76ae7 Mon Sep 17 00:00:00 2001 From: Alastair Bridgewater Date: Mon, 10 Jun 2019 11:15:41 -0400 Subject: [PATCH 5/9] Hide suggestion menu explicitly * On some systems (currently hypothesized to be Firefox with GTK+ using a system theme that has always-visible scrollbars with a minimum size on their long axis), the suggestion menu starts off visible and obscuring the chat message entry area, then moves to obscuring the chat history area once it has been invoked. At no time does it become invisible. * Fix, by hiding the suggestion menu by default and explicitly managing its visibility as it is updated. --- static/js/chat.js | 3 +++ static/main.html | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/static/js/chat.js b/static/js/chat.js index 96448c2..8093cea 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -120,6 +120,9 @@ function sendChat() { function updateSuggestionCss(m) { if ($("#suggestions").children().length > 0) { $("#suggestions").css("bottom", $("#msg").outerHeight(true) - 1 + "px"); + $("#suggestions").css("display", ""); + } else { + $("#suggestions").css("display", "none"); } } diff --git a/static/main.html b/static/main.html index 209b82e..c47c14d 100644 --- a/static/main.html +++ b/static/main.html @@ -95,7 +95,7 @@
-
+
From 3ac5af45480564babb2f82cd0b63cba973c38fa8 Mon Sep 17 00:00:00 2001 From: joeyak Date: Thu, 19 Sep 2019 22:52:58 -0400 Subject: [PATCH 6/9] Removed dependency of dennwc\dom\js This removes the dependency of dennwc\dom\js which allows building with Go 1.13. --- go.mod | 1 - static/js/wasm_exec.js | 126 +++++++++++++++++++++++++++++++---------- wasm/main.go | 71 ++++++++++++++--------- wasm/suggestions.go | 11 ++-- 4 files changed, 146 insertions(+), 63 deletions(-) diff --git a/go.mod b/go.mod index de0ebd0..80a73c0 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,6 @@ require ( github.com/cenkalti/backoff v2.1.1+incompatible // indirect github.com/chromedp/cdproto v0.0.0-20190412020601-c4267f5c421a // indirect github.com/containerd/continuity v0.0.0-20181203112020-004b46473808 // indirect - github.com/dennwc/dom v0.3.0 github.com/gorilla/sessions v1.1.3 github.com/gorilla/websocket v1.4.0 github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect diff --git a/static/js/wasm_exec.js b/static/js/wasm_exec.js index 165d567..a54bb9a 100644 --- a/static/js/wasm_exec.js +++ b/static/js/wasm_exec.js @@ -3,6 +3,15 @@ // license that can be found in the LICENSE file. (() => { + // Map multiple JavaScript environments to a single common API, + // preferring web standards over Node.js API. + // + // Environments considered: + // - Browsers + // - Node.js + // - Electron + // - Parcel + if (typeof global !== "undefined") { // global already exists } else if (typeof window !== "undefined") { @@ -13,30 +22,15 @@ throw new Error("cannot export Go (neither global, window nor self is defined)"); } - // Map web browser API and Node.js API to a single common API (preferring web standards over Node.js API). - const isNodeJS = global.process && global.process.title === "node"; - if (isNodeJS) { + if (!global.require && typeof require !== "undefined") { global.require = require; + } + + if (!global.fs && global.require) { global.fs = require("fs"); + } - const nodeCrypto = require("crypto"); - global.crypto = { - getRandomValues(b) { - nodeCrypto.randomFillSync(b); - }, - }; - - global.performance = { - now() { - const [sec, nsec] = process.hrtime(); - return sec * 1000 + nsec / 1000000; - }, - }; - - const util = require("util"); - global.TextEncoder = util.TextEncoder; - global.TextDecoder = util.TextDecoder; - } else { + if (!global.fs) { let outputBuf = ""; global.fs = { constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused @@ -72,6 +66,34 @@ }; } + if (!global.crypto) { + const nodeCrypto = require("crypto"); + global.crypto = { + getRandomValues(b) { + nodeCrypto.randomFillSync(b); + }, + }; + } + + if (!global.performance) { + global.performance = { + now() { + const [sec, nsec] = process.hrtime(); + return sec * 1000 + nsec / 1000000; + }, + }; + } + + if (!global.TextEncoder) { + global.TextEncoder = require("util").TextEncoder; + } + + if (!global.TextDecoder) { + global.TextDecoder = require("util").TextDecoder; + } + + // End of polyfills for common API. + const encoder = new TextEncoder("utf-8"); const decoder = new TextDecoder("utf-8"); @@ -243,7 +265,15 @@ const id = this._nextCallbackTimeoutID; this._nextCallbackTimeoutID++; this._scheduledTimeouts.set(id, setTimeout( - () => { this._resume(); }, + () => { + this._resume(); + while (this._scheduledTimeouts.has(id)) { + // for some reason Go failed to register the timeout event, log and try again + // (temporary workaround for https://github.com/golang/go/issues/28975) + console.warn("scheduleTimeoutEvent: missed timeout event"); + this._resume(); + } + }, getInt64(sp + 8) + 1, // setTimeout has been seen to fire up to 1 millisecond early )); mem().setInt32(sp + 16, id, true); @@ -357,6 +387,34 @@ mem().setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16)); }, + // func copyBytesToGo(dst []byte, src ref) (int, bool) + "syscall/js.copyBytesToGo": (sp) => { + const dst = loadSlice(sp + 8); + const src = loadValue(sp + 32); + if (!(src instanceof Uint8Array)) { + mem().setUint8(sp + 48, 0); + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + setInt64(sp + 40, toCopy.length); + mem().setUint8(sp + 48, 1); + }, + + // func copyBytesToJS(dst ref, src []byte) (int, bool) + "syscall/js.copyBytesToJS": (sp) => { + const dst = loadValue(sp + 8); + const src = loadSlice(sp + 16); + if (!(dst instanceof Uint8Array)) { + mem().setUint8(sp + 48, 0); + return; + } + const toCopy = src.subarray(0, dst.length); + dst.set(toCopy); + setInt64(sp + 40, toCopy.length); + mem().setUint8(sp + 48, 1); + }, + "debug": (value) => { console.log(value); }, @@ -373,7 +431,6 @@ true, false, global, - this._inst.exports.mem, this, ]; this._refs = new Map(); @@ -385,9 +442,13 @@ let offset = 4096; const strPtr = (str) => { - let ptr = offset; - new Uint8Array(mem.buffer, offset, str.length + 1).set(encoder.encode(str + "\0")); - offset += str.length + (8 - (str.length % 8)); + const ptr = offset; + const bytes = encoder.encode(str + "\0"); + new Uint8Array(mem.buffer, offset, bytes.length).set(bytes); + offset += bytes.length; + if (offset % 8 !== 0) { + offset += 8 - (offset % 8); + } return ptr; }; @@ -439,9 +500,15 @@ } } - if (isNodeJS) { + if ( + global.require && + global.require.main === module && + global.process && + global.process.versions && + !global.process.versions.electron + ) { if (process.argv.length < 3) { - process.stderr.write("usage: go_js_wasm_exec [wasm binary] [arguments]\n"); + console.error("usage: go_js_wasm_exec [wasm binary] [arguments]"); process.exit(1); } @@ -459,7 +526,8 @@ }); return go.run(result.instance); }).catch((err) => { - throw err; + console.error(err); + process.exit(1); }); } })(); diff --git a/wasm/main.go b/wasm/main.go index 3207d8f..31032e9 100644 --- a/wasm/main.go +++ b/wasm/main.go @@ -9,7 +9,8 @@ import ( "strings" "time" - "github.com/dennwc/dom/js" + "syscall/js" + "github.com/zorchenhimer/MovieNight/common" ) @@ -17,21 +18,22 @@ var ( timestamp bool color string auth common.CommandLevel + global js.Value ) func getElement(s string) js.Value { - return js.Get("document").Call("getElementById", s) + return global.Get("document").Call("getElementById", s) } func join(v []js.Value) { - color := js.Call("getCookie", "color").String() + color := global.Call("getCookie", "color").String() if color == "" { // If a color is not set, do a random color color = common.RandomColor() } else if !common.IsValidColor(color) { // Don't show the user the error, just clear the cookie common.LogInfof("%#v is not a valid color, clearing cookie", color) - js.Call("deleteCookie", "color") + global.Call("deleteCookie", "color") } joinData, err := json.Marshal(common.JoinData{ @@ -51,7 +53,7 @@ func join(v []js.Value) { common.LogErrorf("Could not marshal data: %v", err) } - js.Call("websocketSend", string(data)) + global.Call("websocketSend", string(data)) } func recieve(v []js.Value) { @@ -63,7 +65,7 @@ func recieve(v []js.Value) { chatJSON, err := common.DecodeData(v[0].String()) if err != nil { fmt.Printf("Error decoding data: %s\n", err) - js.Call("appendMessages", fmt.Sprintf("
%v
", v)) + global.Call("appendMessages", fmt.Sprintf("
%v
", v)) return } @@ -86,7 +88,7 @@ func recieve(v []js.Value) { auth = h.Data.(common.CommandLevel) case common.CdColor: color = h.Data.(string) - js.Get("document").Set("cookie", fmt.Sprintf("color=%s; expires=Fri, 31 Dec 9999 23:59:59 GMT", color)) + global.Get("document").Set("cookie", fmt.Sprintf("color=%s; expires=Fri, 31 Dec 9999 23:59:59 GMT", color)) case common.CdEmote: data := h.Data.(map[string]interface{}) emoteNames = make([]string, 0, len(data)) @@ -98,7 +100,7 @@ func recieve(v []js.Value) { sort.Strings(emoteNames) case common.CdJoin: notify("") - js.Call("openChat") + global.Call("openChat") case common.CdNotify: notify(h.Data.(string)) } @@ -126,18 +128,18 @@ func recieve(v []js.Value) { switch d.Command { case common.CmdPlaying: if d.Arguments == nil || len(d.Arguments) == 0 { - js.Call("setPlaying", "", "") + global.Call("setPlaying", "", "") } else if len(d.Arguments) == 1 { - js.Call("setPlaying", d.Arguments[0], "") + global.Call("setPlaying", d.Arguments[0], "") } else if len(d.Arguments) == 2 { - js.Call("setPlaying", d.Arguments[0], d.Arguments[1]) + global.Call("setPlaying", d.Arguments[0], d.Arguments[1]) } case common.CmdRefreshPlayer: - js.Call("initPlayer", nil) + global.Call("initPlayer", nil) case common.CmdPurgeChat: - js.Call("purgeChat", nil) + global.Call("purgeChat", nil) appendMessage(d.HTML()) case common.CmdHelp: url := "/help" @@ -145,13 +147,13 @@ func recieve(v []js.Value) { url = d.Arguments[0] } appendMessage(d.HTML()) - js.Get("window").Call("open", url, "_blank", "menubar=0,status=0,toolbar=0,width=300,height=600") + global.Get("window").Call("open", url, "_blank", "menubar=0,status=0,toolbar=0,width=300,height=600") } } } func appendMessage(msg string) { - js.Call("appendMessages", "
"+msg+"
") + global.Call("appendMessages", "
"+msg+"
") } func websocketSend(msg string, dataType common.ClientDataType) error { @@ -167,7 +169,7 @@ func websocketSend(msg string, dataType common.ClientDataType) error { return fmt.Errorf("could not marshal data: %v", err) } - js.Call("websocketSend", string(data)) + global.Call("websocketSend", string(data)) return nil } @@ -188,12 +190,12 @@ func send(this js.Value, v []js.Value) interface{} { func showChatError(err error) { if err != nil { fmt.Printf("Could not send: %v\n", err) - js.Call("appendMessages", `
Could not send message
`) + global.Call("appendMessages", `
Could not send message
`) } } func notify(msg string) { - js.Call("setNotifyBox", msg) + global.Call("setNotifyBox", msg) } func showTimestamp(v []js.Value) { @@ -227,17 +229,19 @@ func debugValues(v []js.Value) { } func main() { + global = js.Global() + common.SetupLogging(common.LLDebug, "") - js.Set("processMessageKey", js.FuncOf(processMessageKey)) - js.Set("sendMessage", js.FuncOf(send)) - js.Set("isValidColor", js.FuncOf(isValidColor)) + global.Set("processMessageKey", js.FuncOf(processMessageKey)) + global.Set("sendMessage", js.FuncOf(send)) + global.Set("isValidColor", js.FuncOf(isValidColor)) - js.Set("recieveMessage", js.CallbackOf(recieve)) - js.Set("processMessage", js.CallbackOf(processMessage)) - js.Set("debugValues", js.CallbackOf(debugValues)) - js.Set("showTimestamp", js.CallbackOf(showTimestamp)) - js.Set("join", js.CallbackOf(join)) + global.Set("recieveMessage", jsCallbackOf(recieve)) + global.Set("processMessage", jsCallbackOf(processMessage)) + global.Set("debugValues", jsCallbackOf(debugValues)) + global.Set("showTimestamp", jsCallbackOf(showTimestamp)) + global.Set("join", jsCallbackOf(join)) go func() { time.Sleep(time.Second * 1) @@ -246,15 +250,26 @@ func main() { inner += fmt.Sprintf(`\n`, c, c) } - js.Get("colorSelect").Set("innerHTML", inner) + global.Get("colorSelect").Set("innerHTML", inner) }() // This is needed so the goroutine does not end for { // heatbeat to keep connection alive to deal with nginx - if js.Get("inChat").Bool() { + if global.Get("inChat").Bool() { websocketSend("", common.CdPing) } time.Sleep(time.Second * 10) } } + +func jsCallbackOf(fnc func(v []js.Value)) js.Func { + return js.FuncOf(func(this js.Value, refs []js.Value) interface{} { + vals := make([]js.Value, 0, len(refs)) + for _, ref := range refs { + vals = append(vals, ref) + } + fnc(vals) + return nil + }) +} diff --git a/wasm/suggestions.go b/wasm/suggestions.go index 2aa1c98..f0a4f35 100644 --- a/wasm/suggestions.go +++ b/wasm/suggestions.go @@ -5,7 +5,8 @@ package main import ( "strings" - "github.com/dennwc/dom/js" + "syscall/js" + "github.com/zorchenhimer/MovieNight/common" ) @@ -70,7 +71,7 @@ func processMessageKey(this js.Value, v []js.Value) interface{} { } currentSug = filteredSug[newidx] case keyTab, keyEnter: - msg := js.Get("msg") + msg := global.Get("msg") val := msg.Get("value").String() newval := val[:startIdx] @@ -104,7 +105,7 @@ func processMessageKey(this js.Value, v []js.Value) interface{} { } func processMessage(v []js.Value) { - msg := js.Get("msg") + msg := global.Get("msg") text := strings.ToLower(msg.Get("value").String()) startIdx := msg.Get("selectionStart").Int() @@ -189,6 +190,6 @@ func updateSuggestionDiv() { } } // The \n is so it's easier to read th source in web browsers for the dev - js.Get("suggestions").Set("innerHTML", strings.Join(divs, "\n")) - js.Call("updateSuggestionScroll") + global.Get("suggestions").Set("innerHTML", strings.Join(divs, "\n")) + global.Call("updateSuggestionScroll") } From 7ac34c7d05e650d6cf7c2a65fa38bc0dc0421f52 Mon Sep 17 00:00:00 2001 From: Zorchenhimer Date: Sun, 22 Sep 2019 15:42:48 -0400 Subject: [PATCH 7/9] Fix name highlighting breaking emotes Fix name highlighting when emotes are involved. When the emotes were moved to subfolders the channel name was put into the URL of the emote image. If a user with that name joined the chat, all of the emotes in that folder highlighted the name in the URL, breaking the tag. This change removes the regex used to identify and replace the user's name and instead works on whole words delimited by spaces. --- chatclient.go | 24 +++++++++++++++--------- chatclient_test.go | 34 +++++++++++++++++++++++++++++++++- common/chatcommands.go | 2 -- 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/chatclient.go b/chatclient.go index 5516966..b843859 100644 --- a/chatclient.go +++ b/chatclient.go @@ -255,15 +255,10 @@ func (cl *Client) Host() string { } func (cl *Client) setName(s string) error { - // Case-insensitive search. Match whole words only (`\b` is word boundary). - regex, err := regexp.Compile(fmt.Sprintf(`(?i)\b(%s|@%s)\b`, s, s)) - if err != nil { - return fmt.Errorf("could not compile regex: %v", err) - } - cl.name = s - cl.regexName = regex - cl.conn.clientName = s + if cl.conn != nil { + cl.conn.clientName = s + } return nil } @@ -274,7 +269,18 @@ func (cl *Client) setColor(s string) error { func (cl *Client) replaceColorizedName(chatData common.ChatData) common.ChatData { data := chatData.Data.(common.DataMessage) - data.Message = cl.regexName.ReplaceAllString(data.Message, `$1`) + words := strings.Split(data.Message, " ") + newWords := []string{} + + for _, word := range words { + if strings.ToLower(word) == strings.ToLower(cl.name) || strings.ToLower(word) == strings.ToLower("@"+cl.name) { + newWords = append(newWords, ``+word+``) + } else { + newWords = append(newWords, word) + } + } + + data.Message = strings.Join(newWords, " ") chatData.Data = data return chatData } diff --git a/chatclient_test.go b/chatclient_test.go index d757cd1..d7dce99 100644 --- a/chatclient_test.go +++ b/chatclient_test.go @@ -1,6 +1,10 @@ package main -import "testing" +import ( + "testing" + + "github.com/zorchenhimer/MovieNight/common" +) func TestClient_addSpoilerTag(t *testing.T) { data := [][]string{ @@ -21,3 +25,31 @@ func TestClient_addSpoilerTag(t *testing.T) { } } } + +// Name highlighting should not interfere with emotes +func TestClient_emoteHighlight(t *testing.T) { + data := [][]string{ + {"zorchenhimer", `zorchenhimer`}, + {"@zorchenhimer", `@zorchenhimer`}, + {"Zorchenhimer", `Zorchenhimer`}, + {"@Zorchenhimer", `@Zorchenhimer`}, + {"hello zorchenhimer", `hello zorchenhimer`}, + {"hello zorchenhimer ass", `hello zorchenhimer ass`}, + {``, ``}, + {`zorchenhimer `, `zorchenhimer `}, + } + + client, err := NewClient(nil, nil, "Zorchenhimer", "#9547ff") + if err != nil { + t.Errorf("Client init error: %v", err) + } + + for _, d := range data { + chatData := client.replaceColorizedName(common.NewChatMessage(client.name, client.color, d[0], common.CmdlUser, common.MsgChat)) + if chatData.Data.(common.DataMessage).Message != d[1] { + t.Errorf("\nExpected:\n\t%s\nReceived\n\t%s", d[1], chatData.Data.(common.DataMessage).Message) + } else { + t.Logf("Passed %s", d[0]) + } + } +} diff --git a/common/chatcommands.go b/common/chatcommands.go index d86433f..84226bf 100644 --- a/common/chatcommands.go +++ b/common/chatcommands.go @@ -54,7 +54,6 @@ var ChatCommands = []ChatCommandNames{ CNStats, // Mod - CNSv, CNPlaying, CNUnmod, CNKick, CNBan, CNUnban, CNPurge, CNPin, CNSv, CNPlaying, CNUnmod, @@ -64,7 +63,6 @@ var ChatCommands = []ChatCommandNames{ CNPurge, // Admin - CNMod, CNReloadPlayer, CNReloadEmotes, CNModpass, CNRoomAccess, CNIP, CNMod, CNReloadPlayer, CNReloadEmotes, From a941e2815e32ea68317d7c824423916a19589cd5 Mon Sep 17 00:00:00 2001 From: Zorchenhimer Date: Sun, 22 Sep 2019 16:15:59 -0400 Subject: [PATCH 8/9] Fix accidental removal of /pin and /changeaccess These were accidentally removed from the command name list. --- common/chatcommands.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/common/chatcommands.go b/common/chatcommands.go index 84226bf..6ffb52d 100644 --- a/common/chatcommands.go +++ b/common/chatcommands.go @@ -22,6 +22,7 @@ var ( CNUsers ChatCommandNames = []string{"users"} CNNick ChatCommandNames = []string{"nick", "name"} CNStats ChatCommandNames = []string{"stats"} + CNPin ChatCommandNames = []string{"pin", "password"} // Mod Commands CNSv ChatCommandNames = []string{"sv"} CNPlaying ChatCommandNames = []string{"playing"} @@ -30,7 +31,6 @@ var ( CNBan ChatCommandNames = []string{"ban"} CNUnban ChatCommandNames = []string{"unban"} CNPurge ChatCommandNames = []string{"purge"} - CNPin ChatCommandNames = []string{"pin", "password"} // Admin Commands CNMod ChatCommandNames = []string{"mod"} CNReloadPlayer ChatCommandNames = []string{"reloadplayer"} @@ -52,6 +52,7 @@ var ChatCommands = []ChatCommandNames{ CNUsers, CNNick, CNStats, + CNPin, // Mod CNSv, @@ -69,6 +70,7 @@ var ChatCommands = []ChatCommandNames{ CNModpass, CNIP, CNAddEmotes, + CNRoomAccess, } func GetFullChatCommand(c string) string { From 6347065dd41ac7ea295a5b06ba4eae4d1e26be47 Mon Sep 17 00:00:00 2001 From: Zorchenhimer Date: Sun, 22 Sep 2019 16:51:13 -0400 Subject: [PATCH 9/9] Fix duplicating emotes with /reloademotes Don't reload emotes directly into the global common.Emotes variable. Instead, load them into a new variable and write that to common.Emotes after the search for new emotes completes. Emotes should no longer duplicate for each run of `/reloademotes`. Also tweaked `/addemotes` to automatically reload emotes if nothing went wrong downloading a new set. --- chatcommands.go | 44 ++++++++++++++++++++++++-------------------- common/emotes.go | 13 +++++++++---- emotes.go | 38 ++++++++++++++++++++++++-------------- 3 files changed, 57 insertions(+), 38 deletions(-) diff --git a/chatcommands.go b/chatcommands.go index 40fa62a..f18bbed 100644 --- a/chatcommands.go +++ b/chatcommands.go @@ -438,26 +438,7 @@ var commands = &CommandControl{ common.CNReloadEmotes.String(): Command{ HelpText: "Reload the emotes on the server.", Function: func(cl *Client, args []string) (string, error) { - go func() { - cl.SendServerMessage("Reloading emotes") - err := loadEmotes() - if err != nil { - common.LogErrorf("Unbale to reload emotes: %s\n", err) - //return "", err - - cl.SendChatData(common.NewChatMessage("", "", - err.Error(), - common.CmdlUser, common.MsgCommandResponse)) - return - } - - cl.belongsTo.AddChatMsg(common.NewChatHiddenMessage(common.CdEmote, common.Emotes)) - cl.belongsTo.AddModNotice(cl.name + " has reloaded emotes") - - num := len(common.Emotes) - common.LogInfof("Loaded %d emotes\n", num) - cl.belongsTo.AddModNotice(fmt.Sprintf("%s reloaded %d emotes.", cl.name, num)) - }() + go commandReloadEmotes(cl) return "Reloading emotes...", nil }, }, @@ -558,6 +539,8 @@ var commands = &CommandControl{ } cl.belongsTo.AddModNotice(cl.name + " has added emotes from the following channels: " + strings.Join(args, ", ")) + + commandReloadEmotes(cl) }() return "Emote download initiated for the following channels: " + strings.Join(args, ", "), nil }, @@ -633,3 +616,24 @@ func getHelp(lvl common.CommandLevel) map[string]string { } return helptext } + +func commandReloadEmotes(cl *Client) { + cl.SendServerMessage("Reloading emotes") + err := loadEmotes() + if err != nil { + common.LogErrorf("Unbale to reload emotes: %s\n", err) + //return "", err + + cl.SendChatData(common.NewChatMessage("", "", + err.Error(), + common.CmdlUser, common.MsgCommandResponse)) + return + } + + cl.belongsTo.AddChatMsg(common.NewChatHiddenMessage(common.CdEmote, common.Emotes)) + cl.belongsTo.AddModNotice(cl.name + " has reloaded emotes") + + num := len(common.Emotes) + common.LogInfof("Loaded %d emotes\n", num) + cl.belongsTo.AddModNotice(fmt.Sprintf("%s reloaded %d emotes.", cl.name, num)) +} diff --git a/common/emotes.go b/common/emotes.go index fd49224..5f1d3ec 100644 --- a/common/emotes.go +++ b/common/emotes.go @@ -14,10 +14,14 @@ var Emotes EmotesMap var reStripStatic = regexp.MustCompile(`^(\\|/)?static`) func init() { - Emotes = map[string]string{} + Emotes = NewEmotesMap() } -func (em EmotesMap) Add(fullpath string) { +func NewEmotesMap() EmotesMap { + return map[string]string{} +} + +func (em EmotesMap) Add(fullpath string) EmotesMap { fullpath = reStripStatic.ReplaceAllLiteralString(fullpath, "") base := filepath.Base(fullpath) @@ -35,8 +39,9 @@ func (em EmotesMap) Add(fullpath string) { code = fmt.Sprintf("%s-%d", code, num) } - Emotes[code] = fullpath - fmt.Printf("Added emote %s at path %q\n", code, fullpath) + em[code] = fullpath + //fmt.Printf("Added emote %s at path %q\n", code, fullpath) + return em } func EmoteToHtml(file, title string) string { diff --git a/emotes.go b/emotes.go index 34e80df..d8fd00b 100644 --- a/emotes.go +++ b/emotes.go @@ -29,18 +29,20 @@ type EmoteInfo struct { func loadEmotes() error { //fmt.Println(processEmoteDir(emoteDir)) - err := processEmoteDir(emoteDir) + newEmotes, err := processEmoteDir(emoteDir) if err != nil { return err } + common.Emotes = newEmotes + return nil } -func processEmoteDir(path string) error { +func processEmoteDir(path string) (common.EmotesMap, error) { dirInfo, err := ioutil.ReadDir(path) if err != nil { - return errors.Wrap(err, "could not open emoteDir:") + return nil, errors.Wrap(err, "could not open emoteDir:") } subDirs := []string{} @@ -53,54 +55,62 @@ func processEmoteDir(path string) error { } } + em := common.NewEmotesMap() // Find top level emotes - err = findEmotes(path) + em, err = findEmotes(path, em) if err != nil { - return errors.Wrap(err, "could not findEmotes() in top level directory:") + return nil, errors.Wrap(err, "could not findEmotes() in top level directory:") } // Get second level subdirs (eg, "twitch", "zorchenhimer", etc) for _, dir := range subDirs { subd, err := ioutil.ReadDir(filepath.Join(path, dir)) if err != nil { + fmt.Printf("Error reading dir %q: %v\n", subd, err) continue } for _, d := range subd { if d.IsDir() { //emotes = append(emotes, findEmotes(filepath.Join(path, dir, d.Name()))...) - findEmotes(filepath.Join(path, dir, d.Name())) + p := filepath.Join(path, dir, d.Name()) + em, err = findEmotes(p, em) + if err != nil { + fmt.Printf("Error finding emotes in %q: %v\n", p, err) + } } } } - return nil + fmt.Printf("processEmoteDir: %d\n", len(em)) + return em, nil } -func findEmotes(dir string) error { +func findEmotes(dir string, em common.EmotesMap) (common.EmotesMap, error) { + //em := NewEmotesMap() + fmt.Printf("finding emotes in %q\n", dir) emotePNGs, err := filepath.Glob(filepath.Join(dir, "*.png")) if err != nil { - //return 0, fmt.Errorf("unable to glob emote directory: %s\n", err) - return nil + return em, fmt.Errorf("unable to glob emote directory: %s\n", err) } fmt.Printf("%d emotePNGs\n", len(emotePNGs)) emoteGIFs, err := filepath.Glob(filepath.Join(dir, "*.gif")) if err != nil { - return errors.Wrap(err, "unable to glob emote directory:") + return em, errors.Wrap(err, "unable to glob emote directory:") } fmt.Printf("%d emoteGIFs\n", len(emoteGIFs)) for _, file := range emotePNGs { - common.Emotes.Add(file) + em = em.Add(file) //emotes = append(emotes, common.Emote{FullPath: dir, Code: file}) } for _, file := range emoteGIFs { - common.Emotes.Add(file) + em = em.Add(file) } - return nil + return em, nil } func getEmotes(names []string) error {