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.
320 lines
8.1 KiB
320 lines
8.1 KiB
package bot
|
|
|
|
// XXX: Ensure we don't join voice channels
|
|
// XXX: Fetch Altspace version and use latest
|
|
// XXX: Delay offline notifications to two checks
|
|
// XXX: Discord Intents
|
|
// XXX: Flirty mode
|
|
// XXX: Mlocks on pending requests
|
|
// XXX: Rate limits
|
|
// XXX: Fetch roles from Guild?
|
|
// XXX: Auto accept friendship mode
|
|
// XXX: Rude mode enabled only for certain users
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"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
|
|
}
|
|
|
|
// User is the structure for a single bot user
|
|
type User struct {
|
|
AltVRUserID string
|
|
DiscordID string
|
|
DiscordName string
|
|
DiscordEmoji UserEmoji
|
|
Role Roles
|
|
isRude bool
|
|
}
|
|
|
|
// 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
|
|
isRude bool // XXX Trigger automatically on NSFW Channels?
|
|
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")
|
|
}
|
|
flag.BoolVar(&b.isRude, "r", false, "Turn on rude mode")
|
|
if !b.isRude {
|
|
if value, ok := os.LookupEnv("DG_IS_RUDE"); ok {
|
|
b.isRude = (value == "true")
|
|
}
|
|
}
|
|
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.getReplyMessage("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) {
|
|
s := fmt.Sprintf("%s**%s is now online in %s!**", b.getUserEmojiByAltVRUser(u), u.GetDisplayName(), u.CurrentSpace.Name)
|
|
if !b.isQuiet {
|
|
b.dg.Session.ChannelMessageSend(b.DGcID, s)
|
|
}
|
|
}
|
|
|
|
func (b *Type) userDisconnected(u altvr.User) {
|
|
s := fmt.Sprintf("%s_%s is now offline!_", b.getUserEmojiByAltVRUser(u), u.GetDisplayName())
|
|
if !b.isQuiet {
|
|
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.getReplyMessage("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 _, u := range b.users {
|
|
if u.AltVRUserID == avrID {
|
|
return u, nil
|
|
}
|
|
}
|
|
return User{}, errors.New("User not found")
|
|
}
|
|
|
|
func (b *Type) getUserByDiscordID(discordID string) (User, error) {
|
|
for _, u := range b.users {
|
|
if u.DiscordID == discordID {
|
|
return u, nil
|
|
}
|
|
}
|
|
return User{}, errors.New("User not found")
|
|
}
|
|
|
|
func (b *Type) getUserByDiscordName(discordName string) (User, error) {
|
|
for _, u := range b.users {
|
|
if u.DiscordName == discordName {
|
|
return u, 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
|
|
}
|