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 ) // UserEmoji holds a guild emoji information for a user type UserEmoji struct { ID string Name string } type userPrivacy uint8 const ( userPrivacyNone userPrivacy = 0 userPrivacyPart userPrivacy = 1 << iota userPrivacyJoin userPrivacy = 1 << iota userPrivacyStatus userPrivacy = 1 << iota userPrivacyAll userPrivacy = 0xFF ) func upSet(b, flag userPrivacy) userPrivacy { return b | flag } func upClear(b, flag userPrivacy) userPrivacy { return b &^ flag } func upToggle(b, flag userPrivacy) userPrivacy { return b ^ flag } func upHas(b, flag userPrivacy) bool { return b&flag != 0 } func (up userPrivacy) String() string { switch up { case userPrivacyPart: return "part" case userPrivacyJoin: return "join" case userPrivacyStatus: return "status" case userPrivacyAll: return "all" case userPrivacyNone: return "none" } return "none" } func userPrivacyFromString(up string) userPrivacy { switch up { case userPrivacyPart.String(): return userPrivacyPart case userPrivacyJoin.String(): return userPrivacyJoin case userPrivacyStatus.String(): return userPrivacyStatus case userPrivacyAll.String(): return userPrivacyAll case userPrivacyNone.String(): return userPrivacyNone } return userPrivacyNone } const userPrivacyDefault = userPrivacyNone // User is the structure for a single bot user type User struct { AltVRUserID string DiscordID string DiscordName string DiscordEmoji UserEmoji Role Roles MsgMode msgMode Privacy userPrivacy } // GetDiscordEmoji If it exists, builds a discord emoji for the user func (u *User) GetDiscordEmoji() string { if u.DiscordEmoji.ID != "" { return fmt.Sprintf("<:%s:%s> ", u.DiscordEmoji.Name, u.DiscordEmoji.ID) } return "" } // Roles determines the bot user roles type Roles uint // Bot user roles const ( RoleUser Roles = iota RoleModerator RoleAdmin ) // 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 { 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 } s := fmt.Sprintf("%s**%s is now online in %s!**", b.getUserEmojiByAltVRUser(u), u.GetDisplayName(), u.CurrentSpace.Name) 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 }