package bot import ( "fmt" "log" "regexp" "strings" "git.lalonde.me/matth/AltVRBot/pkg/discord/mux" "git.lalonde.me/matth/AltVRBot/pkg/utils" "github.com/bwmarrin/discordgo" ) var ( commandFormats = map[string]string{ // Moderator commands "auser": " [Discord Emoji]", "aemoji": " ", "accept": "(As reply) [Discord Emoji]", "deny": "(As reply)", // User commands "msg": " ...", "status": "[Discord Mention...]", "privacy": " ", } ) func (b *Type) loadDiscordHandlers() { // Admin commands // XXX: Promote b.dg.Router.Route("reload", "Reload the bot's data and configs", b.handleReload) // Moderator commands // XXX: Remove b.dg.Router.Route("auser", "Associates an AltVR user to a discord user", b.handleAssociateUser) b.dg.Router.Route("aemoji", "Associates or updates an AltVR user to a discord guild emoji", b.handleAssociateEmoji) b.dg.Router.Route("accept", "Accept a pending friendship request", b.handleAcceptFriendship) b.dg.Router.Route("deny", "Deny a pending friendship request", b.handleDenyFriendship) b.dg.Router.Route("check", "Force a recheck of pending friendship requests and messsages", b.handleForceCheck) // User commands b.dg.Router.Route("msg", "Message an AltVR user", b.handleSendMessage) b.dg.Router.Route("status", "Show the current AltVR user status", b.handleStatusCheck) b.dg.Router.Route("privacy", "Enable or disable user priacy modes", b.handleUserPrivacy) b.dg.Router.Default, _ = b.dg.Router.Route("default", "", b.handleDefault) b.dg.Session.AddHandler(b.handleMessageReplies) } /* ** User commands */ func (b *Type) handleDefault(ds *discordgo.Session, dm *discordgo.Message, ctx *mux.Context) { if !b.checkUserRole(dm.Author.ID, RoleUser) { b.replyPermissionDenied(ds, dm, ctx) return } uu, _ := b.getUserByDiscordID(dm.Author.ID) b.dg.Session.ChannelMessageSendReply(dm.ChannelID, b.getMessageString("unknown_command", uu), &discordgo.MessageReference{ MessageID: dm.ID, ChannelID: dm.ChannelID, GuildID: dm.GuildID, }) } // XXX Don't allow messaging the bot func (b *Type) handleSendMessage(ds *discordgo.Session, dm *discordgo.Message, ctx *mux.Context) { //fmt.Printf("dm:\t%+v\nctv:\t%+v\n", dm, ctx) if dm.MessageReference != nil { return } u, err := b.getUserByDiscordID(dm.Author.ID) if err != nil { b.dg.Session.ChannelMessageSendReply(dm.ChannelID, b.getMessageString("msg_unknown_sender"), &discordgo.MessageReference{ MessageID: dm.ID, ChannelID: dm.ChannelID, GuildID: dm.GuildID, }) return } um := b.getMentions(dm.Mentions) if len(um) != 1 { b.replyInvalidCommandFormat(ds, dm, ctx, "msg") return } discordID := um[0].ID uu, err := b.getUserByDiscordID(discordID) if err != nil { b.dg.Session.ChannelMessageSendReply(dm.ChannelID, b.getMessageString("msg_unknown_receipient", u, discordID), &discordgo.MessageReference{ MessageID: dm.ID, ChannelID: dm.ChannelID, GuildID: dm.GuildID, }) return } if discordID == b.dg.User.ID { b.dg.Session.ChannelMessageSendReply(dm.ChannelID, b.getMessageString("msg_invalid_receipient", u, discordID), &discordgo.MessageReference{ MessageID: dm.ID, ChannelID: dm.ChannelID, GuildID: dm.GuildID, }) return } reg := regexp.MustCompile(fmt.Sprintf(" <@!?(%s)>", discordID)) c := strings.TrimSpace(ctx.Content) c = reg.ReplaceAllString(c, "") c = strings.TrimPrefix(c, "msg ") au := b.avr.GetFriend(u.AltVRUserID) c = au.GetDisplayName() + ": " + c cl := len([]rune(c)) c = utils.TruncateString(c, 140) if err := b.avr.PostNewConversation(uu.AltVRUserID, c); err != nil { log.Printf("Error while sending message: %+v\n", err) b.dg.Session.ChannelMessageSendReply(dm.ChannelID, b.getMessageString("unknown_error", u), &discordgo.MessageReference{ MessageID: dm.ID, ChannelID: dm.ChannelID, GuildID: dm.GuildID, }) } else { rm := "all_done" if cl > len([]rune(c)) { rm = "msg_all_done_truncated" } b.dg.Session.ChannelMessageSendReply(dm.ChannelID, b.getMessageString(rm, u), &discordgo.MessageReference{ MessageID: dm.ID, ChannelID: dm.ChannelID, GuildID: dm.GuildID, }) } } func (b *Type) handleMessageReplies(ds *discordgo.Session, mc *discordgo.MessageCreate) { // Ignore all messages created by the Bot account itself if mc.Author.ID == ds.State.User.ID { return } if mc.MessageReference == nil { return } if _, ok := b.convos[mc.MessageReference.MessageID]; !ok { return } u, err := b.getUserByDiscordID(mc.Author.ID) if err != nil { b.dg.Session.ChannelMessageSendReply(mc.ChannelID, b.getMessageString("msg_unknown_sender"), &discordgo.MessageReference{ MessageID: mc.ID, ChannelID: mc.ChannelID, GuildID: mc.GuildID, }) return } au := b.avr.GetFriend(u.AltVRUserID) msg := au.GetDisplayName() + ": " + mc.Content cl := len([]rune(msg)) msg = utils.TruncateString(msg, 140) if err := b.avr.PostNewConversation(b.convos[mc.MessageReference.MessageID], msg); err != nil { log.Printf("Error while replying to message: %+v\n", err) b.dg.Session.ChannelMessageSendReply(mc.ChannelID, b.getMessageString("unknown_error", u), &discordgo.MessageReference{ MessageID: mc.ID, ChannelID: mc.ChannelID, GuildID: mc.GuildID, }) } else { rm := "all_done" if cl > len([]rune(msg)) { rm = "msg_all_done_truncated" } b.dg.Session.ChannelMessageSendReply(mc.ChannelID, b.getMessageString(rm, u), &discordgo.MessageReference{ MessageID: mc.ID, ChannelID: mc.ChannelID, GuildID: mc.GuildID, }) } } func (b *Type) handleStatusCheck(ds *discordgo.Session, dm *discordgo.Message, ctx *mux.Context) { if !b.checkUserRole(dm.Author.ID, RoleUser) { b.replyPermissionDenied(ds, dm, ctx) return } u, _ := b.getUserByDiscordID(dm.Author.ID) um := b.getMentions(dm.Mentions) c := strings.TrimSpace(ctx.Content) if len(um) > 0 { reg := regexp.MustCompile(fmt.Sprintf(" <@!?(%s)>", um[0].ID)) c = reg.ReplaceAllString(c, "") } var fids []string if len(um) == 0 { for _, f := range b.avr.GetFriendships() { fids = append(fids, f.FriendID) } } else { for _, du := range um { bu, err := b.getUserByDiscordID((du.ID)) if err != nil { log.Printf("Error while discord user data: %+v\n", err) } else { fids = append(fids, bu.AltVRUserID) } } } if err := b.avr.FetchFriends(fids...); err != nil { log.Printf("Error while fetching friends data: %+v\n", err) b.dg.Session.ChannelMessageSendReply(dm.ChannelID, b.getMessageString("unknown_error", u), &discordgo.MessageReference{ MessageID: dm.ID, ChannelID: dm.ChannelID, GuildID: dm.GuildID, }) } else { var msg string for i, uid := range fids { au := b.avr.GetFriend(uid) if au.OnlineStatus == "Online" || au.OnlineStatus == "Available" { du, err := b.getUserByAltVRUserID((au.UserID)) if err != nil { continue } if upHas(du.Privacy, userPrivacyStatus) { continue } e := du.GetDiscordEmoji() if e != "" { msg += e + " " } msg += au.GetDisplayName() + " is current online" if au.CurrentSpace.Name != "" { msg += " in " + au.CurrentSpace.Name } msg += "." if i < len(fids)-1 { msg += "\n" } } } if msg == "" { if len(um) == 0 { msg = b.getMessageString("no_online_users", u) } else { msg = b.getMessageString("no_online_requested_users", u) } } b.dg.Session.ChannelMessageSendReply(dm.ChannelID, msg, &discordgo.MessageReference{ MessageID: dm.ID, ChannelID: dm.ChannelID, GuildID: dm.GuildID, }) } } func (b *Type) handleUserPrivacy(ds *discordgo.Session, dm *discordgo.Message, ctx *mux.Context) { //fmt.Printf("dm:\t%+v\nctv:\t%+v\n", dm, ctx) if !b.checkUserRole(dm.Author.ID, RoleUser) { b.replyPermissionDenied(ds, dm, ctx) return } uu, _ := b.getUserByDiscordID(dm.Author.ID) c := strings.TrimSpace(ctx.Content) p := strings.Split(c, " ") if len(p) != 3 { b.replyInvalidCommandFormat(ds, dm, ctx, "privacy") return } pa := strings.ToLower(p[1:][0]) pp := strings.ToLower(p[1:][1]) if !utils.SliceContainsString([]string{"set", "unset"}, pa) { b.replyInvalidCommandFormat(ds, dm, ctx, "privacy") return } if !utils.SliceContainsString([]string{"join", "part", "status", "all"}, pp) { b.replyInvalidCommandFormat(ds, dm, ctx, "privacy") return } if pa == "unset" { uu.Privacy = upClear(uu.Privacy, userPrivacyFromString(pp)) } else { uu.Privacy = upSet(uu.Privacy, userPrivacyFromString(pp)) } // XXX pr := strings.ToTitle(pa) + " " + pp + ". Privacy mode: " if uu.Privacy == userPrivacyAll { pr += userPrivacyAll.String() } else if uu.Privacy == userPrivacyNone { pr += userPrivacyNone.String() } else { if upHas(uu.Privacy, userPrivacyJoin) { pr += "join " } if upHas(uu.Privacy, userPrivacyPart) { pr += "part " } if upHas(uu.Privacy, userPrivacyStatus) { pr += "status " } } b.dg.Session.ChannelMessageSendReply(dm.ChannelID, b.getMessageString("all_done", uu)+" "+pr, &discordgo.MessageReference{ MessageID: dm.ID, ChannelID: dm.ChannelID, GuildID: dm.GuildID, }) if err := b.saveUserFile(); err != nil { log.Printf("Error while saving user file: %+v\n", err) } } /* ** Moderator commands */ func (b *Type) handleAssociateUser(ds *discordgo.Session, dm *discordgo.Message, ctx *mux.Context) { //fmt.Printf("dm:\t%+v\nctv:\t%+v\n", dm, ctx) if !b.checkUserRole(dm.Author.ID, RoleModerator) && len(b.users) != 0 { b.replyPermissionDenied(ds, dm, ctx) return } uu, _ := b.getUserByDiscordID(dm.Author.ID) c := strings.TrimSpace(ctx.Content) p := strings.Split(c, " ") if len(p) < 3 || len(p) > 4 { b.replyInvalidCommandFormat(ds, dm, ctx, "auser") return } // XXX discordID := strings.Trim(strings.Replace(p[2], "@!", "", -1), "<>") friend := b.avr.GetFriendByUsername(p[1]) if friend.Username == "" { b.dg.Session.ChannelMessageSendReply(dm.ChannelID, b.getMessageString("auser_unknown_friend", uu), &discordgo.MessageReference{ MessageID: dm.ID, ChannelID: dm.ChannelID, GuildID: dm.GuildID, }) return } u, _ := b.getUserByDiscordID(discordID) if u.AltVRUserID != "" { b.dg.Session.ChannelMessageSendReply(dm.ChannelID, b.getMessageString("auser_known_user", uu), &discordgo.MessageReference{ MessageID: dm.ID, ChannelID: dm.ChannelID, GuildID: dm.GuildID, }) return } e := UserEmoji{} if len(p) == 4 { s := strings.Split(strings.Trim(p[3], "<>"), ":") e.ID = s[2] e.Name = s[1] } role := RoleUser if len(b.users) == 0 { role = RoleAdmin } b.users = append(b.users, User{ AltVRUserID: friend.UserID, DiscordID: discordID, DiscordName: dm.Mentions[len(dm.Mentions)-1].Username, DiscordEmoji: e, Role: role, MsgMode: b.msgMode, Privacy: userPrivacyDefault, }) b.dg.Session.ChannelMessageSendReply(dm.ChannelID, b.getMessageString("all_done", uu), &discordgo.MessageReference{ MessageID: dm.ID, ChannelID: dm.ChannelID, GuildID: dm.GuildID, }) if err := b.saveUserFile(); err != nil { log.Printf("Error while saving user file: %+v\n", err) } } func (b *Type) handleAssociateEmoji(ds *discordgo.Session, dm *discordgo.Message, ctx *mux.Context) { //fmt.Printf("dm:\t%+v\nctv:\t%+v\n", dm, ctx) if !b.checkUserRole(dm.Author.ID, RoleModerator) { b.replyPermissionDenied(ds, dm, ctx) return } uu, _ := b.getUserByDiscordID(dm.Author.ID) c := strings.TrimSpace(ctx.Content) p := strings.Split(c, " ") if len(p) != 3 { b.replyInvalidCommandFormat(ds, dm, ctx, "aemoji") return } // XXX discordID := strings.Trim(strings.Replace(p[1], "@!", "", -1), "<>") u, err := b.getUserByDiscordID(discordID) if err != nil { b.dg.Session.ChannelMessageSendReply(dm.ChannelID, b.getMessageString("aemoji_unknown_user", uu), &discordgo.MessageReference{ MessageID: dm.ID, ChannelID: dm.ChannelID, GuildID: dm.GuildID, }) return } e := UserEmoji{} if len(p) == 4 { s := strings.Split(strings.Trim(p[2], "<>"), ":") e.ID = s[2] e.Name = s[1] } u.DiscordEmoji = e b.dg.Session.ChannelMessageSendReply(dm.ChannelID, b.getMessageString("all_done", uu), &discordgo.MessageReference{ MessageID: dm.ID, ChannelID: dm.ChannelID, GuildID: dm.GuildID, }) if err := b.saveUserFile(); err != nil { log.Printf("Error while saving user file: %+v\n", err) } } func (b *Type) handleAcceptFriendship(ds *discordgo.Session, dm *discordgo.Message, ctx *mux.Context) { //fmt.Printf("dm:\t%+v\nctv:\t%+v\n", dm, ctx) if !b.checkUserRole(dm.Author.ID, RoleModerator) && len(b.users) != 0 { b.replyPermissionDenied(ds, dm, ctx) return } if dm.MessageReference == nil { b.replyInvalidCommandFormat(ds, dm, ctx, "accept") return } uu, _ := b.getUserByDiscordID(dm.Author.ID) var req FriendshipRequestPending found := false for _, fr := range b.frPending { if fr.MessageID == dm.MessageReference.MessageID { found = true req = fr break } } if !found { b.dg.Session.ChannelMessageSendReply(dm.ChannelID, b.getMessageString("accept_unknown_reference", uu), &discordgo.MessageReference{ MessageID: dm.ID, ChannelID: dm.ChannelID, GuildID: dm.GuildID, }) return } um := b.getMentions(dm.Mentions) if len(um) != 1 { b.replyInvalidCommandFormat(ds, dm, ctx, "accept") return } reg := regexp.MustCompile(fmt.Sprintf(" <@!?(%s)>", um[0].ID)) c := strings.TrimSpace(ctx.Content) c = reg.ReplaceAllString(c, "") p := strings.Split(c, " ") if len(p) > 3 { b.replyInvalidCommandFormat(ds, dm, ctx, "accept") return } e := UserEmoji{} if len(p) == 3 { s := strings.Split(strings.Trim(p[2], "<>"), ":") e.ID = s[2] e.Name = s[1] } role := RoleUser if len(b.users) == 0 { role = RoleAdmin } b.users = append(b.users, User{ AltVRUserID: req.Friendship.UserID, DiscordID: um[0].ID, DiscordName: dm.Mentions[len(dm.Mentions)-1].Username, DiscordEmoji: e, Role: role, MsgMode: b.msgMode, Privacy: userPrivacyDefault, }) b.avr.AcceptFriendshipRequest(req.Friendship.FriendshipID) for k, fr := range b.frPending { if fr.Friendship.FriendshipID == req.Friendship.FriendshipID { b.frPending = append(b.frPending[:k], b.frPending[k+1:]...) break } } b.dg.Session.ChannelMessageSendReply(dm.ChannelID, b.getMessageString("all_done", uu), &discordgo.MessageReference{ MessageID: dm.ID, ChannelID: dm.ChannelID, GuildID: dm.GuildID, }) if err := b.saveUserFile(); err != nil { log.Printf("Error while saving user file: %+v\n", err) } b.avr.FetchFriend(req.Friendship.UserID) } func (b *Type) handleDenyFriendship(ds *discordgo.Session, dm *discordgo.Message, ctx *mux.Context) { //fmt.Printf("dm:\t%+v\nctv:\t%+v\n", dm, ctx) if !b.checkUserRole(dm.Author.ID, RoleModerator) { b.replyPermissionDenied(ds, dm, ctx) return } if dm.MessageReference == nil { b.replyInvalidCommandFormat(ds, dm, ctx, "deny") return } uu, _ := b.getUserByDiscordID(dm.Author.ID) var req FriendshipRequestPending found := false for _, fr := range b.frPending { if fr.MessageID == dm.MessageReference.MessageID { found = true break } } if !found { b.dg.Session.ChannelMessageSendReply(dm.ChannelID, b.getMessageString("accept_unknown_reference", uu), &discordgo.MessageReference{ MessageID: dm.ID, ChannelID: dm.ChannelID, GuildID: dm.GuildID, }) return } b.avr.DenyFriendshipRequest(req.Friendship.FriendshipID) for k, fr := range b.frPending { if fr.Friendship.FriendshipID == req.Friendship.FriendshipID { b.frPending = append(b.frPending[:k], b.frPending[k+1:]...) break } } b.dg.Session.ChannelMessageSendReply(dm.ChannelID, b.getMessageString("all_done", uu), &discordgo.MessageReference{ MessageID: dm.ID, ChannelID: dm.ChannelID, GuildID: dm.GuildID, }) } func (b *Type) handleForceCheck(ds *discordgo.Session, dm *discordgo.Message, ctx *mux.Context) { //fmt.Printf("dm:\t%+v\nctv:\t%+v\n", dm, ctx) if !b.checkUserRole(dm.Author.ID, RoleModerator) { b.replyPermissionDenied(ds, dm, ctx) return } uu, _ := b.getUserByDiscordID(dm.Author.ID) log.Println("Checking for incoming friendship request") fr, _ := b.avr.FetchPendingFriendshipRequests() log.Println("Checking for conversations") pm, _ := b.avr.FetchPendingConversations() b.dg.Session.ChannelMessageSendReply(dm.ChannelID, b.getMessageString("all_done", uu), &discordgo.MessageReference{ MessageID: dm.ID, ChannelID: dm.ChannelID, GuildID: dm.GuildID, }) if len(fr) > 0 { b.handleNewFriendshipRequests(fr) } if len(pm) > 0 { b.handleNewPendingConversation(pm) } } /* ** Admin commands */ func (b *Type) handleReload(ds *discordgo.Session, dm *discordgo.Message, ctx *mux.Context) { //fmt.Printf("dm:\t%+v\nctv:\t%+v\n", dm, ctx) if !b.checkUserRole(dm.Author.ID, RoleAdmin) { b.replyPermissionDenied(ds, dm, ctx) return } uu, _ := b.getUserByDiscordID(dm.Author.ID) log.Println("Reloading bot data") b.loadUserFile() err := b.avr.FetchMyUser() if err != nil { log.Printf("Error while reloading own user data: %+v\n", err) } b.dg.UpdateAvatar(b.avr.GetAvatarURL()) b.dg.Session.ChannelMessageSendReply(dm.ChannelID, b.getMessageString("all_done", uu), &discordgo.MessageReference{ MessageID: dm.ID, ChannelID: dm.ChannelID, GuildID: dm.GuildID, }) } /* ** Utilities */ func (b *Type) checkUserRole(discordID string, role Roles) bool { u, err := b.getUserByDiscordID(discordID) if err != nil { log.Printf("Error fetching bot user: %s\n", err) return false } return (u.Role >= role) } func (b *Type) replyPermissionDenied(ds *discordgo.Session, dm *discordgo.Message, ctx *mux.Context) { uu, _ := b.getUserByDiscordID(dm.Author.ID) b.dg.Session.ChannelMessageSendReply(dm.ChannelID, b.getMessageString("unauthorized_user", uu), &discordgo.MessageReference{ MessageID: dm.ID, ChannelID: dm.ChannelID, GuildID: dm.GuildID, }) } func (b *Type) replyInvalidCommandFormat(ds *discordgo.Session, dm *discordgo.Message, ctx *mux.Context, cmd string) { uu, _ := b.getUserByDiscordID(dm.Author.ID) msg := b.getMessageString("invalid_command", uu) if usage, ok := commandFormats[cmd]; ok { msg = msg + " Usage: " + cmd + " " + usage } b.dg.Session.ChannelMessageSendReply(dm.ChannelID, msg, &discordgo.MessageReference{ MessageID: dm.ID, ChannelID: dm.ChannelID, GuildID: dm.GuildID, }) } func (b *Type) getMentions(ms []*discordgo.User) []*discordgo.User { var um []*discordgo.User for _, u := range ms { if u.ID != b.dg.User.ID { um = append(um, u) } } return um }