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.
393 lines
9.9 KiB
393 lines
9.9 KiB
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
|
|
}
|