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.

319 lines
8.0 KiB

package bot
// XXX: Ensure we don't join voice channels
// XXX: Fetch Altspace version and use latest
// 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
}