You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

643 lines
18 KiB

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": "<Altspace VR Username> <Discord Mention> [Discord Emoji]",
"aemoji": "<Discord Mention> <Discord Emoji>",
"accept": "(As reply) <Discord Mention> [Discord Emoji]",
"deny": "(As reply)",
// User commands
"msg": "<Discord Mention> ...",
"status": "[Discord Mention...]",
"privacy": "<none|join|part|status|all>",
}
)
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) != 2 {
b.replyInvalidCommandFormat(ds, dm, ctx, "privacy")
return
}
pp := p[1:][0]
if !utils.SliceContainsString([]string{"none", "join", "part", "status", "all"}, pp) {
b.replyInvalidCommandFormat(ds, dm, ctx, "privacy")
return
}
uu.Privacy = userPrivacyFromString(pp)
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)
}
}
/*
** 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()
if len(fr) > 0 {
b.handleNewFriendshipRequests(fr)
}
log.Println("Checking for conversations")
pm, _ := b.avr.FetchPendingConversations()
if len(pm) > 0 {
b.handleNewPendingConversation(pm)
}
b.dg.Session.ChannelMessageSendReply(dm.ChannelID,
b.getMessageString("all_done", uu),
&discordgo.MessageReference{
MessageID: dm.ID,
ChannelID: dm.ChannelID,
GuildID: dm.GuildID,
})
}
/*
** 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
}