feat: Generate hashes for Discord and allow password resets

This commit is contained in:
Matthew
2023-11-26 16:47:54 -05:00
parent 50946b9c68
commit 226adddc43
8 changed files with 100 additions and 46 deletions

View File

@@ -19,9 +19,9 @@
"AutoCreateAccount": true, "AutoCreateAccount": true,
"CleanDB": false, "CleanDB": false,
"MaxLauncherHR": false, "MaxLauncherHR": false,
"LogInboundMessages": false, "LogInboundMessages": true,
"LogOutboundMessages": false, "LogOutboundMessages": true,
"LogMessageData": false, "LogMessageData": true,
"MaxHexdumpLength": 256, "MaxHexdumpLength": 256,
"DivaEvent": 0, "DivaEvent": 0,
"FestaEvent": -1, "FestaEvent": -1,
@@ -73,9 +73,9 @@
"SeasonOverride": false "SeasonOverride": false
}, },
"Discord": { "Discord": {
"Enabled": false, "Enabled": true,
"BotToken": "", "BotToken": "MTAzMTQ2MDI4MDYxOTc2NTgwMA.GGe824._OxF9rtv1O8EjOZI26hATruaF_VZ9YBwuAdS1Y",
"RealtimeChannelID": "" "RealtimeChannelID": "645108836423958540"
}, },
"Commands": [ "Commands": [
{ {
@@ -106,6 +106,10 @@
"Name": "PSN", "Name": "PSN",
"Enabled": true, "Enabled": true,
"Prefix": "psn" "Prefix": "psn"
}, {
"Name": "Discord",
"Enabled": true,
"Prefix": "discord"
} }
], ],
"Courses": [ "Courses": [
@@ -125,7 +129,7 @@
"Host": "localhost", "Host": "localhost",
"Port": 5432, "Port": 5432,
"User": "postgres", "User": "postgres",
"Password": "", "Password": "admin",
"Database": "erupe" "Database": "erupe"
}, },
"Sign": { "Sign": {
@@ -133,7 +137,7 @@
"Port": 53312 "Port": 53312
}, },
"SignV2": { "SignV2": {
"Enabled": false, "Enabled": true,
"Port": 8080, "Port": 8080,
"PatchServer": "", "PatchServer": "",
"Banners": [], "Banners": [],

32
main.go
View File

@@ -3,6 +3,7 @@ package main
import ( import (
_config "erupe-ce/config" _config "erupe-ce/config"
"fmt" "fmt"
"github.com/bwmarrin/discordgo"
"net" "net"
"os" "os"
"os/signal" "os/signal"
@@ -98,6 +99,37 @@ func main() {
} }
discordBot = bot discordBot = bot
_, err = discordBot.Session.ApplicationCommandBulkOverwrite(discordBot.Session.State.User.ID, "", []*discordgo.ApplicationCommand{
{
Name: "verify",
Description: "Verify your account with Discord",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "token",
Description: "The access token provided by !discord command within the game client.",
Required: true,
},
},
},
{
Name: "passwordreset",
Description: "Reset your account password on Erupe",
Options: []*discordgo.ApplicationCommandOption{
{
Type: discordgo.ApplicationCommandOptionString,
Name: "password",
Description: "The password to change your account to.",
Required: true,
},
},
},
})
if err != nil {
preventClose(fmt.Sprintf("Discord: Failed to start, %s", err.Error()))
}
logger.Info("Discord: Started successfully") logger.Info("Discord: Started successfully")
} else { } else {
logger.Info("Discord: Disabled") logger.Info("Discord: Disabled")

View File

@@ -2,6 +2,7 @@ package channelserver
import ( import (
"crypto" "crypto"
"encoding/binary"
"encoding/hex" "encoding/hex"
"erupe-ce/common/byteframe" "erupe-ce/common/byteframe"
"erupe-ce/common/mhfcourse" "erupe-ce/common/mhfcourse"
@@ -321,12 +322,22 @@ func parseChatCommand(s *Session, command string) {
} }
case commands["Discord"].Prefix: case commands["Discord"].Prefix:
if commands["Discord"].Enabled { if commands["Discord"].Enabled {
token := crypto.MD5.New() tokenHash := crypto.MD5.New()
_, err := s.server.db.Exec("UPDATE users SET discord_token = ?", token) tokenSalt := fmt.Sprint(s.charID) + fmt.Sprint(s.server.ID)
tokenData := make([]byte, 4)
binary.LittleEndian.PutUint32(tokenData, uint32(time.Now().Second()))
tokenHash.Write([]byte(fmt.Sprintf("%s%s", tokenSalt, tokenData)))
discordToken := fmt.Sprint(tokenHash)[4:12]
s.logger.Info(discordToken)
_, err := s.server.db.Exec("UPDATE users u SET discord_token = $1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$2)", discordToken, s.charID)
if err != nil { if err != nil {
sendServerChatMessage(s, fmt.Sprint("An error occurred while processing this command"))
s.logger.Error(fmt.Sprint(err))
return return
} }
sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandDiscord"], token)) sendServerChatMessage(s, fmt.Sprintf(s.server.dict["commandDiscordSuccess"], discordToken))
} else {
sendDisabledCommandMessage(s, commands["Discord"])
} }
} }
} }

View File

@@ -3,6 +3,7 @@ package channelserver
import ( import (
"fmt" "fmt"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
"golang.org/x/crypto/bcrypt"
"sort" "sort"
"strings" "strings"
"unicode" "unicode"
@@ -70,14 +71,32 @@ func getCharacterList(s *Server) string {
func (s *Server) onInteraction(ds *discordgo.Session, i *discordgo.InteractionCreate) { func (s *Server) onInteraction(ds *discordgo.Session, i *discordgo.InteractionCreate) {
switch i.Interaction.ApplicationCommandData().Name { switch i.Interaction.ApplicationCommandData().Name {
case "verify": case "verify":
_, err := s.db.Exec("UPDATE users SET discord_id = ? WHERE discord_token = ?", i.User.ID, i.Interaction.ApplicationCommandData().Options[0].StringValue()) _, err := s.db.Exec("UPDATE users SET discord_id = $1 WHERE discord_token = $2", i.Member.User.ID, i.ApplicationCommandData().Options[0].StringValue())
if err != nil { if err != nil {
return return
} }
err = ds.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{ err = ds.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource, Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{ Data: &discordgo.InteractionResponseData{
Content: "Account successfully linked", Content: "Erupe account successfully linked to Discord account.",
Flags: discordgo.MessageFlagsEphemeral,
},
})
if err != nil {
return
}
break
case "passwordreset":
password, _ := bcrypt.GenerateFromPassword([]byte(i.ApplicationCommandData().Options[0].StringValue()), 10)
_, err := s.db.Exec("UPDATE users SET password = $1 WHERE discord_id = $2", password, i.Member.User.ID)
if err != nil {
return
}
err = ds.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "Password has been reset, you may login now.",
Flags: discordgo.MessageFlagsEphemeral,
}, },
}) })
if err != nil { if err != nil {

View File

@@ -55,6 +55,7 @@ func doStageTransfer(s *Session, ackHandle uint32, stageID string) {
// Save our new stage ID and pointer to the new stage itself. // Save our new stage ID and pointer to the new stage itself.
s.Lock() s.Lock()
s.stageID = stageID
s.stage = s.server.stages[stageID] s.stage = s.server.stages[stageID]
s.Unlock() s.Unlock()
@@ -152,13 +153,13 @@ func handleMsgSysEnterStage(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgSysEnterStage) pkt := p.(*mhfpacket.MsgSysEnterStage)
// Push our current stage ID to the movement stack before entering another one. // Push our current stage ID to the movement stack before entering another one.
if s.stage.id == "" { if s.stageID == "" {
s.stageMoveStack.Set(pkt.StageID) s.stageMoveStack.Set(pkt.StageID)
} else { } else {
s.stage.Lock() s.stage.Lock()
s.stage.reservedClientSlots[s.charID] = false s.stage.reservedClientSlots[s.charID] = false
s.stage.Unlock() s.stage.Unlock()
s.stageMoveStack.Push(s.stage.id) s.stageMoveStack.Push(s.stageID)
s.stageMoveStack.Lock() s.stageMoveStack.Lock()
} }
@@ -205,12 +206,9 @@ func handleMsgSysLeaveStage(s *Session, p mhfpacket.MHFPacket) {}
func handleMsgSysLockStage(s *Session, p mhfpacket.MHFPacket) { func handleMsgSysLockStage(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgSysLockStage) pkt := p.(*mhfpacket.MsgSysLockStage)
if stage, exists := s.server.stages[pkt.StageID]; exists { // TODO(Andoryuuta): What does this packet _actually_ do?
stage.Lock() // I think this is supposed to mark a stage as no longer able to accept client reservations
stage.locked = true doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00})
stage.Unlock()
}
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
} }
func handleMsgSysUnlockStage(s *Session, p mhfpacket.MHFPacket) { func handleMsgSysUnlockStage(s *Session, p mhfpacket.MHFPacket) {
@@ -220,9 +218,7 @@ func handleMsgSysUnlockStage(s *Session, p mhfpacket.MHFPacket) {
for charID := range s.reservationStage.reservedClientSlots { for charID := range s.reservationStage.reservedClientSlots {
session := s.server.FindSessionByCharID(charID) session := s.server.FindSessionByCharID(charID)
if session != nil { session.QueueSendMHF(&mhfpacket.MsgSysStageDestruct{})
session.QueueSendMHF(&mhfpacket.MsgSysStageDestruct{})
}
} }
delete(s.server.stages, s.reservationStage.id) delete(s.server.stages, s.reservationStage.id)
@@ -245,10 +241,6 @@ func handleMsgSysReserveStage(s *Session, p mhfpacket.MHFPacket) {
} }
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
} else if uint16(len(stage.reservedClientSlots)) < stage.maxPlayers { } else if uint16(len(stage.reservedClientSlots)) < stage.maxPlayers {
if stage.locked {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
if len(stage.password) > 0 { if len(stage.password) > 0 {
if stage.password != s.stagePass { if stage.password != s.stagePass {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4)) doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
@@ -391,17 +383,20 @@ func handleMsgSysEnumerateStage(s *Session, p mhfpacket.MHFPacket) {
joinable++ joinable++
bf.WriteUint16(uint16(len(stage.reservedClientSlots))) bf.WriteUint16(uint16(len(stage.reservedClientSlots)))
bf.WriteUint16(uint16(len(stage.clients))) bf.WriteUint16(0) // Unk
bf.WriteUint16(uint16(len(stage.clients))) if len(stage.clients) > 0 {
bf.WriteUint16(1)
} else {
bf.WriteUint16(0)
}
bf.WriteUint16(stage.maxPlayers) bf.WriteUint16(stage.maxPlayers)
var flags uint8
if stage.locked {
flags |= 1
}
if len(stage.password) > 0 { if len(stage.password) > 0 {
flags |= 2 // This byte has also been seen as 1
// The quest is also recognised as locked when this is 2
bf.WriteUint8(2)
} else {
bf.WriteUint8(0)
} }
bf.WriteUint8(flags)
ps.Uint8(bf, sid, false) ps.Uint8(bf, sid, false)
stage.RUnlock() stage.RUnlock()
} }

View File

@@ -2,7 +2,6 @@ package channelserver
import ( import (
"fmt" "fmt"
"github.com/bwmarrin/discordgo"
"net" "net"
"strings" "strings"
"sync" "sync"
@@ -211,15 +210,6 @@ func (s *Server) Start() error {
// Start the discord bot for chat integration. // Start the discord bot for chat integration.
if s.erupeConfig.Discord.Enabled && s.discordBot != nil { if s.erupeConfig.Discord.Enabled && s.discordBot != nil {
_, err := s.discordBot.Session.ApplicationCommandBulkOverwrite(s.discordBot.Session.State.User.ID, "", []*discordgo.ApplicationCommand{
{
Name: "verify",
Description: "Verify your account with Discord",
},
})
if err != nil {
return err
}
s.discordBot.Session.AddHandler(s.onDiscordMessage) s.discordBot.Session.AddHandler(s.onDiscordMessage)
s.discordBot.Session.AddHandler(s.onInteraction) s.discordBot.Session.AddHandler(s.onInteraction)
} }

View File

@@ -76,6 +76,8 @@ func getLangStrings(s *Server) map[string]string {
strings["commandPSNSuccess"] = "Connected PSN ID: %s" strings["commandPSNSuccess"] = "Connected PSN ID: %s"
strings["commandPSNExists"] = "PSN ID is connected to another account!" strings["commandPSNExists"] = "PSN ID is connected to another account!"
strings["commandDiscordSuccess"] = "Discord token has been generated: %s"
strings["commandRaviNoCommand"] = "No Raviente command specified!" strings["commandRaviNoCommand"] = "No Raviente command specified!"
strings["commandRaviStartSuccess"] = "The Great Slaying will begin in a moment" strings["commandRaviStartSuccess"] = "The Great Slaying will begin in a moment"
strings["commandRaviStartError"] = "The Great Slaying has already begun!" strings["commandRaviStartError"] = "The Great Slaying has already begun!"

View File

@@ -36,6 +36,7 @@ type Session struct {
objectIndex uint16 objectIndex uint16
userEnteredStage bool // If the user has entered a stage before userEnteredStage bool // If the user has entered a stage before
stageID string
stage *Stage stage *Stage
reservationStage *Stage // Required for the stateful MsgSysUnreserveStage packet. reservationStage *Stage // Required for the stateful MsgSysUnreserveStage packet.
stagePass string // Temporary storage stagePass string // Temporary storage