package bot import ( "bytes" "encoding/json" "errors" "flag" "fmt" "log" "os" "strings" "time" "git.lalonde.me/matth/AltVRBot/pkg/altvr" "git.lalonde.me/matth/AltVRBot/pkg/discord" ) const ( tickerIntervalFriendshipOnline = 2 tickerIntervalCheckIncomingConversations = 1 tickerIntervalCheckIncomingFriendshipRequest = 5 tickerIntervalUpdateGuildEmojis = 24 * 60 ) // FriendshipRequestPending data for a pending friendship request type FriendshipRequestPending struct { Friendship altvr.Friendship MessageID string } // Type the AltVR bot type type Type struct { DGToken string DGsID string DGcID string AltVRUsername string AltVRPassword string dg *discord.Discord avr altvr.AltVR users []User msgMode msgMode isQuiet bool frPending []FriendshipRequestPending convos map[string]string } func (b *Type) setFlags() { b.DGToken = os.Getenv("DG_TOKEN") if b.DGToken == "" { flag.StringVar(&b.DGToken, "t", "", "Discord Authentication Token") } b.DGsID = os.Getenv("DG_SERVER_ID") if b.DGsID == "" { flag.StringVar(&b.DGsID, "s", "", "Discord Server ID") } b.DGcID = os.Getenv("DG_CHANNEL_ID") if b.DGcID == "" { flag.StringVar(&b.DGcID, "c", "", "Discord Channel ID") } b.AltVRUsername = os.Getenv("ALTVR_USERNAME") if b.AltVRUsername == "" { flag.StringVar(&b.AltVRUsername, "u", "", "AltVR Username") } b.AltVRPassword = os.Getenv("ALTVR_PASSWORD") if b.AltVRPassword == "" { flag.StringVar(&b.AltVRPassword, "p", "", "AltVR Password") } var mode string ms := fmt.Sprintf("%s|%s|%s|%s", msgModeNormal, msgModeRude, msgModeFlirty, msgModeRandom) flag.StringVar(&mode, "m", "", "Select the reply message mode <"+ms+">") if mode == "" { if value, ok := os.LookupEnv("DG_MSG_MODE"); ok { mode = value } } switch strings.ToLower(mode) { case msgModeRandom.String(): b.msgMode = msgModeRandom case msgModeFlirty.String(): b.msgMode = msgModeFlirty case msgModeRude.String(): b.msgMode = msgModeRude case msgModeNormal.String(): b.msgMode = msgModeNormal default: if mode != "" { fmt.Printf("Unknown message mode `%s` defaulting to normal\n", mode) } b.msgMode = msgModeNormal } flag.BoolVar(&b.isQuiet, "q", false, "Turn on quiet mode (don't be verbose on the discord chat)") flag.Parse() } // Start launches the bot func (b *Type) Start() { b.setFlags() var err error b.avr = altvr.New(b.AltVRUsername, b.AltVRPassword) b.dg, err = discord.New(b.DGToken, b.DGsID, b.DGcID) if err != nil { log.Fatal(err) } b.dg.UpdateAvatar(b.avr.GetAvatarURL()) b.convos = make(map[string]string) b.loadUserFile() b.loadDiscordHandlers() if !b.isQuiet { s := b.getMessageString("online_welcome") if _, err := b.dg.Session.ChannelMessageSend(b.DGcID, s); err != nil { log.Printf("Error sending welcome message: %+v\n", err) } } //b.avr.FetchOnlineFriendships(b.userConnected, b.userDisconnected) go b.periodicTicker() } // Close closes any open session func (b *Type) Close() { b.dg.Close() } func (b *Type) periodicTicker() { onlineFriendships := time.NewTimer(tickerIntervalFriendshipOnline * time.Minute) checkConversations := time.NewTimer(tickerIntervalCheckIncomingConversations * time.Minute) checkIncomingFriendshipRequests := time.NewTimer(tickerIntervalCheckIncomingFriendshipRequest * time.Minute) updateGuildEmojis := time.NewTimer(tickerIntervalUpdateGuildEmojis * time.Minute) for { select { case <-onlineFriendships.C: b.avr.FetchOnlineFriendships(b.userConnected, b.userDisconnected) onlineFriendships.Reset(tickerIntervalFriendshipOnline * time.Minute) case <-checkConversations.C: log.Println("Checking for conversations") pm, _ := b.avr.FetchPendingConversations() if len(pm) > 0 { log.Printf("Found %d new messages\n", len(pm)) b.handleNewPendingConversation(pm) } checkConversations.Reset(tickerIntervalCheckIncomingConversations * time.Minute) case <-checkIncomingFriendshipRequests.C: log.Println("Checking for incoming friendship request") fr, _ := b.avr.FetchPendingFriendshipRequests() if len(fr) > 0 { b.handleNewFriendshipRequests(fr) } checkIncomingFriendshipRequests.Reset(tickerIntervalCheckIncomingFriendshipRequest * time.Minute) case <-updateGuildEmojis.C: log.Println("Updating guild emojis") b.dg.Emojis, _ = b.dg.Session.GuildEmojis(b.DGsID) updateGuildEmojis.Reset(tickerIntervalUpdateGuildEmojis * time.Minute) } } } func (b *Type) userConnected(u altvr.User) { uu, err := b.getUserByAltVRUserID(u.UserID) if err != nil { return } ws := u.CurrentSpace.Name if ws != "" { ws = fmt.Sprintf(" in %s", ws) } s := fmt.Sprintf("%s**%s is now online%s!**", b.getUserEmojiByAltVRUser(u), u.GetDisplayName(), ws) if !b.isQuiet && !upHas(uu.Privacy, userPrivacyJoin) { b.dg.Session.ChannelMessageSend(b.DGcID, s) } } func (b *Type) userDisconnected(u altvr.User) { uu, err := b.getUserByAltVRUserID(u.UserID) if err != nil { return } s := fmt.Sprintf("%s_%s is now offline!_", b.getUserEmojiByAltVRUser(u), u.GetDisplayName()) if !b.isQuiet && !upHas(uu.Privacy, userPrivacyPart) { b.dg.Session.ChannelMessageSend(b.DGcID, s) } } func (b *Type) loadUserFile() error { var err error file, err := os.Open("users.json") if err != nil { return err } defer file.Close() decoder := json.NewDecoder(file) err = decoder.Decode(&b.users) if err != nil { return err } return nil } func (b *Type) saveUserFile() error { var err error file, err := os.OpenFile("users.json", os.O_RDWR|os.O_CREATE, 0644) if err != nil { return err } defer file.Close() buffer := new(bytes.Buffer) encoder := json.NewEncoder(file) err = encoder.Encode(b.users) if err != nil { return err } _, err = file.Write(buffer.Bytes()) if err != nil { return err } return nil } func (b *Type) handleNewFriendshipRequests(fr []altvr.Friendship) { for _, rr := range fr { found := false for _, rrr := range b.frPending { if rr.FriendshipID == rrr.Friendship.FriendshipID { found = true break } } if !found { u := b.avr.GetFriend(rr.UserID) s := b.getMessageString("new_friendship_requested", u.GetDisplayName()) if msg, err := b.dg.Session.ChannelMessageSend(b.DGcID, s); err == nil { b.frPending = append(b.frPending, FriendshipRequestPending{ Friendship: rr, MessageID: msg.ID, }) } } } // XXX: Remove withdrawn requests } func (b *Type) handleNewPendingConversation(pm []altvr.Conversation) { for _, mm := range pm { au := b.avr.GetFriend(mm.SenderID) uu, _ := b.getUserByAltVRUserID(mm.SenderID) at := au.GetDisplayName() if uu.DiscordID != "" { at = b.buildMention(uu) } s := at + ": " + mm.Subject if msg, err := b.dg.Session.ChannelMessageSend(b.DGcID, s); err == nil { b.convos[msg.ID] = mm.ID if err := b.avr.AcknowkledgeConversation(mm.ID); err != nil { log.Printf("Error acknownledging conversation: %+v\n", err) } } } } func (b *Type) getUserEmojiByAltVRUser(au altvr.User) string { for _, u := range b.users { if u.AltVRUserID == au.UserID { for k, e := range b.dg.Emojis { if e.Name == u.DiscordEmoji.Name { return fmt.Sprintf("<:%s:%s> ", b.dg.Emojis[k].Name, b.dg.Emojis[k].ID) } } } } return "" } func (b *Type) getUserByAltVRUserID(avrID string) (*User, error) { for k, u := range b.users { if u.AltVRUserID == avrID { return &b.users[k], nil } } return &User{}, errors.New("User not found") } func (b *Type) getUserByDiscordID(discordID string) (*User, error) { for k, u := range b.users { if u.DiscordID == discordID { return &b.users[k], nil } } return &User{}, errors.New("User not found") } func (b *Type) getUserByDiscordName(discordName string) (*User, error) { for k, u := range b.users { if u.DiscordName == discordName { return &b.users[k], nil } } return &User{}, errors.New("User not found") } func (b *Type) buildMention(uu *User) string { var msg string if uu.DiscordEmoji.Name != "" { msg = fmt.Sprintf("<:%s:%s> ", uu.DiscordEmoji.Name, uu.DiscordEmoji.ID) } du, err := b.dg.Session.User(uu.DiscordID) if err == nil { msg = msg + du.Mention() } return msg }