Merge branch 'refs/heads/main' into fix/quest-stamps

This commit is contained in:
wish
2024-07-24 23:28:13 +10:00
21 changed files with 572 additions and 109 deletions

View File

@@ -1,8 +1,8 @@
package signv2server
package api
import (
"context"
"erupe-ce/config"
_config "erupe-ce/config"
"fmt"
"net/http"
"os"
@@ -21,8 +21,8 @@ type Config struct {
ErupeConfig *_config.Config
}
// Server is the MHF custom launcher sign server.
type Server struct {
// APIServer is Erupes Standard API interface
type APIServer struct {
sync.Mutex
logger *zap.Logger
erupeConfig *_config.Config
@@ -31,9 +31,9 @@ type Server struct {
isShuttingDown bool
}
// NewServer creates a new Server type.
func NewServer(config *Config) *Server {
s := &Server{
// NewAPIServer creates a new Server type.
func NewAPIServer(config *Config) *APIServer {
s := &APIServer{
logger: config.Logger,
erupeConfig: config.ErupeConfig,
db: config.DB,
@@ -43,7 +43,7 @@ func NewServer(config *Config) *Server {
}
// Start starts the server in a new goroutine.
func (s *Server) Start() error {
func (s *APIServer) Start() error {
// Set up the routes responsible for serving the launcher HTML, serverlist, unique name check, and JP auth.
r := mux.NewRouter()
r.HandleFunc("/launcher", s.Launcher)
@@ -52,9 +52,11 @@ func (s *Server) Start() error {
r.HandleFunc("/character/create", s.CreateCharacter)
r.HandleFunc("/character/delete", s.DeleteCharacter)
r.HandleFunc("/character/export", s.ExportSave)
r.HandleFunc("/api/ss/bbs/upload.php", s.ScreenShot)
r.HandleFunc("/api/ss/bbs/{id}", s.ScreenShotGet)
handler := handlers.CORS(handlers.AllowedHeaders([]string{"Content-Type"}))(r)
s.httpServer.Handler = handlers.LoggingHandler(os.Stdout, handler)
s.httpServer.Addr = fmt.Sprintf(":%d", s.erupeConfig.SignV2.Port)
s.httpServer.Addr = fmt.Sprintf(":%d", s.erupeConfig.API.Port)
serveError := make(chan error, 1)
go func() {
@@ -74,7 +76,7 @@ func (s *Server) Start() error {
}
// Shutdown exits the server gracefully.
func (s *Server) Shutdown() {
func (s *APIServer) Shutdown() {
s.logger.Debug("Shutting down")
s.Lock()

View File

@@ -1,4 +1,4 @@
package signv2server
package api
import (
"context"
@@ -10,7 +10,7 @@ import (
"golang.org/x/crypto/bcrypt"
)
func (s *Server) createNewUser(ctx context.Context, username string, password string) (uint32, uint32, error) {
func (s *APIServer) createNewUser(ctx context.Context, username string, password string) (uint32, uint32, error) {
// Create salted hash of user password
passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
@@ -32,7 +32,7 @@ func (s *Server) createNewUser(ctx context.Context, username string, password st
return id, rights, err
}
func (s *Server) createLoginToken(ctx context.Context, uid uint32) (uint32, string, error) {
func (s *APIServer) createLoginToken(ctx context.Context, uid uint32) (uint32, string, error) {
loginToken := token.Generate(16)
var tid uint32
err := s.db.QueryRowContext(ctx, "INSERT INTO sign_sessions (user_id, token) VALUES ($1, $2) RETURNING id", uid, loginToken).Scan(&tid)
@@ -42,7 +42,7 @@ func (s *Server) createLoginToken(ctx context.Context, uid uint32) (uint32, stri
return tid, loginToken, nil
}
func (s *Server) userIDFromToken(ctx context.Context, token string) (uint32, error) {
func (s *APIServer) userIDFromToken(ctx context.Context, token string) (uint32, error) {
var userID uint32
err := s.db.QueryRowContext(ctx, "SELECT user_id FROM sign_sessions WHERE token = $1", token).Scan(&userID)
if err == sql.ErrNoRows {
@@ -53,7 +53,7 @@ func (s *Server) userIDFromToken(ctx context.Context, token string) (uint32, err
return userID, nil
}
func (s *Server) createCharacter(ctx context.Context, userID uint32) (Character, error) {
func (s *APIServer) createCharacter(ctx context.Context, userID uint32) (Character, error) {
var character Character
err := s.db.GetContext(ctx, &character,
"SELECT id, name, is_female, weapon_type, hr, gr, last_login FROM characters WHERE is_new_character = true AND user_id = $1 LIMIT 1",
@@ -78,7 +78,7 @@ func (s *Server) createCharacter(ctx context.Context, userID uint32) (Character,
return character, err
}
func (s *Server) deleteCharacter(ctx context.Context, userID uint32, charID uint32) error {
func (s *APIServer) deleteCharacter(ctx context.Context, userID uint32, charID uint32) error {
var isNew bool
err := s.db.QueryRow("SELECT is_new_character FROM characters WHERE id = $1", charID).Scan(&isNew)
if err != nil {
@@ -92,7 +92,7 @@ func (s *Server) deleteCharacter(ctx context.Context, userID uint32, charID uint
return err
}
func (s *Server) getCharactersForUser(ctx context.Context, uid uint32) ([]Character, error) {
func (s *APIServer) getCharactersForUser(ctx context.Context, uid uint32) ([]Character, error) {
var characters []Character
err := s.db.SelectContext(
ctx, &characters, `
@@ -107,7 +107,7 @@ func (s *Server) getCharactersForUser(ctx context.Context, uid uint32) ([]Charac
return characters, nil
}
func (s *Server) getReturnExpiry(uid uint32) time.Time {
func (s *APIServer) getReturnExpiry(uid uint32) time.Time {
var returnExpiry, lastLogin time.Time
s.db.Get(&lastLogin, "SELECT COALESCE(last_login, now()) FROM users WHERE id=$1", uid)
if time.Now().Add((time.Hour * 24) * -90).After(lastLogin) {
@@ -124,7 +124,7 @@ func (s *Server) getReturnExpiry(uid uint32) time.Time {
return returnExpiry
}
func (s *Server) exportSave(ctx context.Context, uid uint32, cid uint32) (map[string]interface{}, error) {
func (s *APIServer) exportSave(ctx context.Context, uid uint32, cid uint32) (map[string]interface{}, error) {
row := s.db.QueryRowxContext(ctx, "SELECT * FROM characters WHERE id=$1 AND user_id=$2", cid, uid)
result := make(map[string]interface{})
err := row.MapScan(result)

View File

@@ -1,15 +1,24 @@
package signv2server
package api
import (
"database/sql"
"encoding/json"
"encoding/xml"
"errors"
_config "erupe-ce/config"
"erupe-ce/server/channelserver"
"fmt"
"image"
"image/jpeg"
"io"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/gorilla/mux"
"github.com/lib/pq"
"go.uber.org/zap"
"golang.org/x/crypto/bcrypt"
@@ -21,9 +30,9 @@ const (
)
type LauncherResponse struct {
Banners []_config.SignV2Banner `json:"banners"`
Messages []_config.SignV2Message `json:"messages"`
Links []_config.SignV2Link `json:"links"`
Banners []_config.APISignBanner `json:"banners"`
Messages []_config.APISignMessage `json:"messages"`
Links []_config.APISignLink `json:"links"`
}
type User struct {
@@ -66,7 +75,7 @@ type ExportData struct {
Character map[string]interface{} `json:"character"`
}
func (s *Server) newAuthData(userID uint32, userRights uint32, userTokenID uint32, userToken string, characters []Character) AuthData {
func (s *APIServer) newAuthData(userID uint32, userRights uint32, userTokenID uint32, userToken string, characters []Character) AuthData {
resp := AuthData{
CurrentTS: uint32(channelserver.TimeAdjusted().Unix()),
ExpiryTS: uint32(s.getReturnExpiry(userID).Unix()),
@@ -77,7 +86,7 @@ func (s *Server) newAuthData(userID uint32, userRights uint32, userTokenID uint3
Token: userToken,
},
Characters: characters,
PatchServer: s.erupeConfig.SignV2.PatchServer,
PatchServer: s.erupeConfig.API.PatchServer,
Notices: []string{},
}
if s.erupeConfig.DebugOptions.MaxLauncherHR {
@@ -103,16 +112,16 @@ func (s *Server) newAuthData(userID uint32, userRights uint32, userTokenID uint3
return resp
}
func (s *Server) Launcher(w http.ResponseWriter, r *http.Request) {
func (s *APIServer) Launcher(w http.ResponseWriter, r *http.Request) {
var respData LauncherResponse
respData.Banners = s.erupeConfig.SignV2.Banners
respData.Messages = s.erupeConfig.SignV2.Messages
respData.Links = s.erupeConfig.SignV2.Links
respData.Banners = s.erupeConfig.API.Banners
respData.Messages = s.erupeConfig.API.Messages
respData.Links = s.erupeConfig.API.Links
w.Header().Add("Content-Type", "application/json")
json.NewEncoder(w).Encode(respData)
}
func (s *Server) Login(w http.ResponseWriter, r *http.Request) {
func (s *APIServer) Login(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var reqData struct {
Username string `json:"username"`
@@ -164,7 +173,7 @@ func (s *Server) Login(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(respData)
}
func (s *Server) Register(w http.ResponseWriter, r *http.Request) {
func (s *APIServer) Register(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var reqData struct {
Username string `json:"username"`
@@ -204,7 +213,7 @@ func (s *Server) Register(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(respData)
}
func (s *Server) CreateCharacter(w http.ResponseWriter, r *http.Request) {
func (s *APIServer) CreateCharacter(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var reqData struct {
Token string `json:"token"`
@@ -233,7 +242,7 @@ func (s *Server) CreateCharacter(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(character)
}
func (s *Server) DeleteCharacter(w http.ResponseWriter, r *http.Request) {
func (s *APIServer) DeleteCharacter(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var reqData struct {
Token string `json:"token"`
@@ -258,7 +267,7 @@ func (s *Server) DeleteCharacter(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(struct{}{})
}
func (s *Server) ExportSave(w http.ResponseWriter, r *http.Request) {
func (s *APIServer) ExportSave(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var reqData struct {
Token string `json:"token"`
@@ -286,3 +295,118 @@ func (s *Server) ExportSave(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
json.NewEncoder(w).Encode(save)
}
func (s *APIServer) ScreenShotGet(w http.ResponseWriter, r *http.Request) {
// Get the 'id' parameter from the URL
token := mux.Vars(r)["id"]
var tokenPattern = regexp.MustCompile(`[A-Za-z0-9]+`)
if !tokenPattern.MatchString(token) || token == "" {
http.Error(w, "Not Valid Token", http.StatusBadRequest)
}
// Open the image file
safePath := s.erupeConfig.Screenshots.OutputDir
path := filepath.Join(safePath, fmt.Sprintf("%s.jpg", token))
result, err := verifyPath(path, safePath)
if err != nil {
fmt.Println("Error " + err.Error())
} else {
fmt.Println("Canonical: " + result)
file, err := os.Open(result)
if err != nil {
http.Error(w, "Image not found", http.StatusNotFound)
return
}
defer file.Close()
// Set content type header to image/jpeg
w.Header().Set("Content-Type", "image/jpeg")
// Copy the image content to the response writer
if _, err := io.Copy(w, file); err != nil {
http.Error(w, "Unable to send image", http.StatusInternalServerError)
return
}
}
}
func (s *APIServer) ScreenShot(w http.ResponseWriter, r *http.Request) {
// Create a struct representing the XML result
type Result struct {
XMLName xml.Name `xml:"result"`
Code string `xml:"code"`
}
// Set the Content-Type header to specify that the response is in XML format
w.Header().Set("Content-Type", "text/xml")
result := Result{Code: "200"}
if !s.erupeConfig.Screenshots.Enabled {
result = Result{Code: "400"}
} else {
if r.Method != http.MethodPost {
result = Result{Code: "405"}
}
// Get File from Request
file, _, err := r.FormFile("img")
if err != nil {
result = Result{Code: "400"}
}
var tokenPattern = regexp.MustCompile(`[A-Za-z0-9]+`)
token := r.FormValue("token")
if !tokenPattern.MatchString(token) || token == "" {
result = Result{Code: "401"}
}
// Validate file
img, _, err := image.Decode(file)
if err != nil {
result = Result{Code: "400"}
}
safePath := s.erupeConfig.Screenshots.OutputDir
path := filepath.Join(safePath, fmt.Sprintf("%s.jpg", token))
verified, err := verifyPath(path, safePath)
if err != nil {
result = Result{Code: "500"}
} else {
_, err = os.Stat(safePath)
if err != nil {
if os.IsNotExist(err) {
err = os.MkdirAll(safePath, os.ModePerm)
if err != nil {
s.logger.Error("Error writing screenshot, could not create folder")
result = Result{Code: "500"}
}
} else {
s.logger.Error("Error writing screenshot")
result = Result{Code: "500"}
}
}
// Create or open the output file
outputFile, err := os.Create(verified)
if err != nil {
result = Result{Code: "500"}
}
defer outputFile.Close()
// Encode the image and write it to the file
err = jpeg.Encode(outputFile, img, &jpeg.Options{Quality: s.erupeConfig.Screenshots.UploadQuality})
if err != nil {
s.logger.Error("Error writing screenshot, could not write file", zap.Error(err))
result = Result{Code: "500"}
}
}
}
// Marshal the struct into XML
xmlData, err := xml.Marshal(result)
if err != nil {
http.Error(w, "Unable to marshal XML", http.StatusInternalServerError)
return
}
// Write the XML response with a 200 status code
w.WriteHeader(http.StatusOK)
w.Write(xmlData)
}

37
server/api/utils.go Normal file
View File

@@ -0,0 +1,37 @@
package api
import (
"errors"
"fmt"
"path/filepath"
)
func inTrustedRoot(path string, trustedRoot string) error {
for path != "/" {
path = filepath.Dir(path)
if path == trustedRoot {
return nil
}
}
return errors.New("path is outside of trusted root")
}
func verifyPath(path string, trustedRoot string) (string, error) {
c := filepath.Clean(path)
fmt.Println("Cleaned path: " + c)
r, err := filepath.EvalSymlinks(c)
if err != nil {
fmt.Println("Error " + err.Error())
return c, errors.New("Unsafe or invalid path specified")
}
err = inTrustedRoot(r, trustedRoot)
if err != nil {
fmt.Println("Error " + err.Error())
return r, errors.New("Unsafe or invalid path specified")
} else {
return r, nil
}
}

View File

@@ -852,26 +852,29 @@ func handleMsgMhfGetCogInfo(s *Session, p mhfpacket.MHFPacket) {}
func handleMsgMhfCheckWeeklyStamp(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfCheckWeeklyStamp)
weekCurrentStart := TimeWeekStart()
weekNextStart := TimeWeekNext()
var total, redeemed, updated uint16
var nextClaim time.Time
err := s.server.db.QueryRow(fmt.Sprintf("SELECT %s_next FROM stamps WHERE character_id=$1", pkt.StampType), s.charID).Scan(&nextClaim)
var lastCheck time.Time
err := s.server.db.QueryRow(fmt.Sprintf("SELECT %s_checked FROM stamps WHERE character_id=$1", pkt.StampType), s.charID).Scan(&lastCheck)
if err != nil {
s.server.db.Exec("INSERT INTO stamps (character_id, hl_next, ex_next) VALUES ($1, $2, $2)", s.charID, weekNextStart)
nextClaim = weekNextStart
lastCheck = TimeAdjusted()
s.server.db.Exec("INSERT INTO stamps (character_id, hl_checked, ex_checked) VALUES ($1, $2, $2)", s.charID, TimeAdjusted())
} else {
s.server.db.Exec(fmt.Sprintf(`UPDATE stamps SET %s_checked=$1 WHERE character_id=$2`, pkt.StampType), TimeAdjusted(), s.charID)
}
if nextClaim.Before(weekCurrentStart) {
s.server.db.Exec(fmt.Sprintf("UPDATE stamps SET %s_total=%s_total+1, %s_next=$1 WHERE character_id=$2", pkt.StampType, pkt.StampType, pkt.StampType), weekNextStart, s.charID)
if lastCheck.Before(TimeWeekStart()) {
s.server.db.Exec(fmt.Sprintf("UPDATE stamps SET %s_total=%s_total+1 WHERE character_id=$1", pkt.StampType, pkt.StampType), s.charID)
updated = 1
}
s.server.db.QueryRow(fmt.Sprintf("SELECT %s_total, %s_redeemed FROM stamps WHERE character_id=$1", pkt.StampType, pkt.StampType), s.charID).Scan(&total, &redeemed)
bf := byteframe.NewByteFrame()
bf.WriteUint16(total)
bf.WriteUint16(redeemed)
bf.WriteUint16(updated)
bf.WriteUint32(0) // Unk
bf.WriteUint32(uint32(weekCurrentStart.Unix()))
bf.WriteUint16(0)
bf.WriteUint16(0)
bf.WriteUint32(uint32(TimeWeekStart().Unix()))
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
}
@@ -879,7 +882,7 @@ func handleMsgMhfExchangeWeeklyStamp(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfExchangeWeeklyStamp)
var total, redeemed uint16
var tktStack mhfitem.MHFItemStack
if pkt.Unk1 == 0xA { // Yearly Sub Ex
if pkt.Unk1 == 10 { // Yearly Sub Ex
s.server.db.QueryRow("UPDATE stamps SET hl_total=hl_total-48, hl_redeemed=hl_redeemed-48 WHERE character_id=$1 RETURNING hl_total, hl_redeemed", s.charID).Scan(&total, &redeemed)
tktStack = mhfitem.MHFItemStack{Item: mhfitem.MHFItem{ItemID: 2210}, Quantity: 1}
} else {
@@ -895,7 +898,8 @@ func handleMsgMhfExchangeWeeklyStamp(s *Session, p mhfpacket.MHFPacket) {
bf.WriteUint16(total)
bf.WriteUint16(redeemed)
bf.WriteUint16(0)
bf.WriteUint32(0) // Unk, but has possible values
bf.WriteUint16(tktStack.Item.ItemID)
bf.WriteUint16(tktStack.Quantity)
bf.WriteUint32(uint32(TimeWeekStart().Unix()))
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
}
@@ -1083,6 +1087,31 @@ func getStampcardReward(secondStamp bool, HR uint16, GR uint16) mhfitem.MHFItemS
func handleMsgMhfStampcardStamp(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfStampcardStamp)
rewards := []struct {
HR uint16
Item1 uint16
Quantity1 uint16
Item2 uint16
Quantity2 uint16
}{
{0, 6164, 1, 6164, 2},
{50, 6164, 2, 6164, 3},
{100, 6164, 3, 5392, 1},
{300, 5392, 1, 5392, 3},
{999, 5392, 1, 5392, 4},
}
if _config.ErupeConfig.RealClientMode <= _config.Z1 {
for _, reward := range rewards {
if pkt.HR >= reward.HR {
pkt.Item1 = reward.Item1
pkt.Quantity1 = reward.Quantity1
pkt.Item2 = reward.Item2
pkt.Quantity2 = reward.Quantity2
}
}
}
bf := byteframe.NewByteFrame()
bf.WriteUint16(pkt.HR)
if _config.ErupeConfig.RealClientMode >= _config.G1 {

View File

@@ -7,35 +7,47 @@ import (
"erupe-ce/network/mhfpacket"
)
// Handler BBS handles all the interactions with the for the screenshot sending to bulitin board functionality. For it to work it requires the API to be hosted somehwere. This implementation supports discord.
// Checks the status of the user to see if they can use Bulitin Board yet
func handleMsgMhfGetBbsUserStatus(s *Session, p mhfpacket.MHFPacket) {
//Post Screenshot pauses till this succeedes
pkt := p.(*mhfpacket.MsgMhfGetBbsUserStatus)
bf := byteframe.NewByteFrame()
bf.WriteUint32(200)
bf.WriteUint32(200) //HTTP Status Codes //200 Success //404 You wont be able to post for a certain amount of time after creating your character //401/500 A error occured server side
bf.WriteUint32(0)
bf.WriteUint32(0)
bf.WriteUint32(0)
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
}
// Checks the status of Bultin Board Server to see if authenticated
func handleMsgMhfGetBbsSnsStatus(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfGetBbsSnsStatus)
bf := byteframe.NewByteFrame()
bf.WriteUint32(200)
bf.WriteUint32(401)
bf.WriteUint32(401)
bf.WriteUint32(200) //200 Success //4XX Authentication has expired Please re-authenticate //5XX
bf.WriteUint32(401) //unk http status?
bf.WriteUint32(401) //unk http status?
bf.WriteUint32(0)
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
}
// Tells the game client what host port and gives the bultin board article a token
func handleMsgMhfApplyBbsArticle(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfApplyBbsArticle)
bf := byteframe.NewByteFrame()
articleToken := token.Generate(40)
bf.WriteUint32(200)
bf.WriteUint32(80)
bf.WriteUint32(200) //http status //200 success //4XX An error occured server side
bf.WriteUint32(s.server.erupeConfig.Screenshots.Port)
bf.WriteUint32(0)
bf.WriteUint32(0)
bf.WriteBytes(stringsupport.PaddedString(articleToken, 64, false))
bf.WriteBytes(stringsupport.PaddedString(s.server.erupeConfig.ScreenshotAPIURL, 64, false))
bf.WriteBytes(stringsupport.PaddedString(s.server.erupeConfig.Screenshots.Host, 64, false))
//pkt.unk1[3] == Changes sometimes?
if s.server.erupeConfig.Screenshots.Enabled && s.server.erupeConfig.Discord.Enabled {
s.server.DiscordScreenShotSend(pkt.Name, pkt.Title, pkt.Description, articleToken)
}
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
}

View File

@@ -4,6 +4,7 @@ import (
"erupe-ce/common/byteframe"
"erupe-ce/common/mhfcourse"
ps "erupe-ce/common/pascalstring"
_config "erupe-ce/config"
"erupe-ce/network/mhfpacket"
"fmt"
"go.uber.org/zap"
@@ -92,10 +93,11 @@ func handleMsgMhfGetCafeDuration(s *Session, p mhfpacket.MHFPacket) {
if mhfcourse.CourseExists(30, s.courses) {
cafeTime = uint32(TimeAdjusted().Unix()) - uint32(s.sessionStart) + cafeTime
}
bf.WriteUint32(cafeTime) // Total cafe time
bf.WriteUint16(0)
ps.Uint16(bf, fmt.Sprintf(s.server.i18n.cafe.reset, int(cafeReset.Month()), cafeReset.Day()), true)
bf.WriteUint32(cafeTime)
if _config.ErupeConfig.RealClientMode >= _config.ZZ {
bf.WriteUint16(0)
ps.Uint16(bf, fmt.Sprintf(s.server.i18n.cafe.reset, int(cafeReset.Month()), cafeReset.Day()), true)
}
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
}

View File

@@ -367,13 +367,17 @@ func handleMsgMhfAcquireTitle(s *Session, p mhfpacket.MHFPacket) {
func handleMsgMhfResetTitle(s *Session, p mhfpacket.MHFPacket) {}
func handleMsgMhfOperateWarehouse(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfOperateWarehouse)
func initializeWarehouse(s *Session) {
var t int
err := s.server.db.QueryRow("SELECT character_id FROM warehouse WHERE character_id=$1", s.charID).Scan(&t)
if err != nil {
s.server.db.Exec("INSERT INTO warehouse (character_id) VALUES ($1)", s.charID)
}
}
func handleMsgMhfOperateWarehouse(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfOperateWarehouse)
initializeWarehouse(s)
bf := byteframe.NewByteFrame()
bf.WriteUint8(pkt.Operation)
switch pkt.Operation {
@@ -446,6 +450,7 @@ func addWarehouseEquipment(s *Session, equipment mhfitem.MHFEquipment) {
}
func warehouseGetItems(s *Session, index uint8) []mhfitem.MHFItemStack {
initializeWarehouse(s)
var data []byte
var items []mhfitem.MHFItemStack
s.server.db.QueryRow(fmt.Sprintf(`SELECT item%d FROM warehouse WHERE character_id=$1`, index), s.charID).Scan(&data)

View File

@@ -367,6 +367,14 @@ func (s *Server) DiscordChannelSend(charName string, content string) {
}
}
func (s *Server) DiscordScreenShotSend(charName string, title string, description string, articleToken string) {
if s.erupeConfig.Discord.Enabled && s.discordBot != nil {
imageUrl := fmt.Sprintf("%s:%d/api/ss/bbs/%s", s.erupeConfig.Screenshots.Host, s.erupeConfig.Screenshots.Port, articleToken)
message := fmt.Sprintf("**%s**: %s - %s %s", charName, title, description, imageUrl)
s.discordBot.RealtimeChannelSend(message)
}
}
func (s *Server) FindSessionByCharID(charID uint32) *Session {
for _, c := range s.Channels {
for _, session := range c.sessions {

View File

@@ -1,10 +1,11 @@
package discordbot
import (
"erupe-ce/config"
_config "erupe-ce/config"
"regexp"
"github.com/bwmarrin/discordgo"
"go.uber.org/zap"
"regexp"
)
var Commands = []*discordgo.ApplicationCommand{
@@ -113,7 +114,6 @@ func (bot *DiscordBot) RealtimeChannelSend(message string) (err error) {
return
}
func ReplaceTextAll(text string, regex *regexp.Regexp, handler func(input string) string) string {
result := regex.ReplaceAllFunc([]byte(text), func(s []byte) []byte {
input := regex.ReplaceAllString(string(s), `$1`)

View File

@@ -7,9 +7,10 @@ import (
_config "erupe-ce/config"
"erupe-ce/server/channelserver"
"fmt"
"go.uber.org/zap"
"strings"
"time"
"go.uber.org/zap"
)
func (s *Session) makeSignResponse(uid uint32) []byte {
@@ -134,8 +135,204 @@ func (s *Session) makeSignResponse(uid uint32) []byte {
bf.WriteUint32(s.server.getLastCID(uid))
bf.WriteUint32(s.server.getUserRights(uid))
ps.Uint16(bf, "", false) // filters
if s.client == VITA || s.client == PS3 {
namNGWords := []string{}
msgNGWords := []string{}
filters := byteframe.NewByteFrame()
filters.SetLE()
filters.WriteNullTerminatedBytes([]byte("smc"))
smc := byteframe.NewByteFrame()
smc.SetLE()
smcData := []struct {
charGroup [][]rune
}{
{[][]rune{{'='}, {''}}},
{[][]rune{{')'}, {''}}},
{[][]rune{{'('}, {''}}},
{[][]rune{{'!'}, {''}}},
{[][]rune{{'/'}, {''}}},
{[][]rune{{'+'}, {''}}},
{[][]rune{{'&'}, {''}}},
{[][]rune{{'ぼ'}, {'ボ'}, {'ホ', '゙'}, {'ほ', '゙'}, {'ホ', '゙'}, {'ほ', '゛'}, {'ホ', '゛'}, {'ホ', '゛'}}},
{[][]rune{{'べ'}, {'ベ'}, {'ヘ', '゙'}, {'へ', '゙'}, {'ヘ', '゙'}, {'へ', '゛'}, {'ヘ', '゛'}, {'ヘ', '゛'}}},
{[][]rune{{'で'}, {'デ'}, {'テ', '゙'}, {'て', '゙'}, {'テ', '゙'}, {'て', '゛'}, {'テ', '゛'}, {'テ', '゛'}, {'〒', '゛'}, {'〒', '゙'}, {'乙', '゙'}, {'乙', '゛'}}},
{[][]rune{{'び'}, {'ビ'}, {'ヒ', '゙'}, {'ひ', '゙'}, {'ヒ', '゙'}, {'ひ', '゛'}, {'ヒ', '゛'}, {'ヒ', '゛'}}},
{[][]rune{{'ど'}, {'ド'}, {'ト', '゙'}, {'と', '゙'}, {'ト', '゙'}, {'と', '゛'}, {'ト', '゛'}, {'ト', '゛'}, {'┣', '゙'}, {'┣', '゛'}, {'├', '゙'}, {'├', '゛'}}},
{[][]rune{{'ば'}, {'バ'}, {'ハ', '゙'}, {'は', '゙'}, {'ハ', '゙'}, {'八', '゙'}, {'は', '゛'}, {'ハ', '゛'}, {'ハ', '゛'}, {'八', '゛'}}},
{[][]rune{{'つ', '゙'}, {'ヅ'}, {'ツ', '゙'}, {'つ', '゛'}, {'ツ', '゛'}, {'ツ', '゙'}, {'ツ', '゛'}, {'づ'}, {'っ', '゙'}, {'ッ', '゙'}, {'ッ', '゙'}, {'っ', '゛'}, {'ッ', '゛'}, {'ッ', '゛'}}},
{[][]rune{{'ぶ'}, {'ブ'}, {'フ', '゙'}, {'ヴ'}, {'ウ', '゙'}, {'う', '゛'}, {'う', '゙'}, {'ウ', '゙'}, {'ゥ', '゙'}, {'ぅ', '゙'}, {'ふ', '゙'}, {'フ', '゙'}, {'フ', '゛'}}},
{[][]rune{{'ぢ'}, {'ヂ'}, {'チ', '゙'}, {'ち', '゙'}, {'チ', '゙'}, {'ち', '゛'}, {'チ', '゛'}, {'チ', '゛'}, {'千', '゛'}, {'千', '゙'}}},
{[][]rune{{'だ'}, {'ダ'}, {'タ', '゙'}, {'た', '゙'}, {'タ', '゙'}, {'夕', '゙'}, {'た', '゛'}, {'タ', '゛'}, {'タ', '゛'}, {'夕', '゛'}}},
{[][]rune{{'ぞ'}, {'ゾ'}, {'ソ', '゙'}, {'そ', '゙'}, {'ソ', '゙'}, {'そ', '゛'}, {'ソ', '゛'}, {'ソ', '゛'}, {'ン', '゙'}, {'ン', '゛'}, {'ン', '゛'}, {'ン', '゙'}, {'リ', '゙'}, {'リ', '゙'}, {'リ', '゛'}, {'リ', '゛'}}},
{[][]rune{{'ぜ'}, {'セ', '゙'}, {'せ', '゙'}, {'セ', '゙'}, {'せ', '゛'}, {'セ', '゛'}, {'セ', '゛'}, {'ゼ'}}},
{[][]rune{{'ず'}, {'ズ'}, {'ス', '゙'}, {'す', '゙'}, {'ス', '゙'}, {'す', '゛'}, {'ス', '゛'}, {'ス', '゛'}}},
{[][]rune{{'じ'}, {'ジ'}, {'シ', '゙'}, {'し', '゙'}, {'シ', '゙'}, {'し', '゛'}, {'シ', '゛'}, {'シ', '゛'}}},
{[][]rune{{'ざ'}, {'ザ'}, {'サ', '゙'}, {'さ', '゙'}, {'サ', '゙'}, {'さ', '゛'}, {'サ', '゛'}, {'サ', '゛'}}},
{[][]rune{{'ご'}, {'ゴ'}, {'コ', '゙'}, {'こ', '゙'}, {'コ', '゙'}, {'こ', '゛'}, {'コ', '゛'}, {'コ', '゛'}}},
{[][]rune{{'げ'}, {'ゲ'}, {'ケ', '゙'}, {'け', '゙'}, {'ケ', '゙'}, {'け', '゛'}, {'ケ', '゛'}, {'ケ', '゛'}, {'ヶ', '゙'}, {'ヶ', '゛'}}},
{[][]rune{{'ぐ'}, {'グ'}, {'ク', '゙'}, {'く', '゙'}, {'ク', '゙'}, {'く', '゛'}, {'ク', '゛'}, {'ク', '゛'}}},
{[][]rune{{'ぎ'}, {'ギ'}, {'キ', '゙'}, {'き', '゙'}, {'キ', '゙'}, {'き', '゛'}, {'キ', '゛'}, {'キ', '゛'}}},
{[][]rune{{'が'}, {'ガ'}, {'カ', '゙'}, {'ヵ', '゙'}, {'カ', '゙'}, {'か', '゙'}, {'力', '゙'}, {'ヵ', '゛'}, {'カ', '゛'}, {'か', '゛'}, {'力', '゛'}, {'カ', '゛'}}},
{[][]rune{{'を'}, {'ヲ'}, {'ヲ'}}},
{[][]rune{{'わ'}, {'ワ'}, {'ワ'}, {'ヮ'}}},
{[][]rune{{'ろ'}, {'ロ'}, {'ロ'}, {'□'}, {'口'}}},
{[][]rune{{'れ'}, {'レ'}, {'レ'}}},
{[][]rune{{'る'}, {'ル'}, {'ル'}}},
{[][]rune{{'り'}, {'リ'}, {'リ'}}},
{[][]rune{{'ら'}, {'ラ'}, {'ラ'}}},
{[][]rune{{'よ'}, {'ヨ'}, {'ヨ'}, {'ョ'}, {'ょ'}, {'ョ'}}},
{[][]rune{{'ゆ'}, {'ユ'}, {'ユ'}, {'ュ'}, {'ゅ'}, {'ュ'}}},
{[][]rune{{'や'}, {'ヤ'}, {'ヤ'}, {'ャ'}, {'ゃ'}, {'ャ'}}},
{[][]rune{{'も'}, {'モ'}, {'モ'}}},
{[][]rune{{'め'}, {'メ'}, {'メ'}, {'M', 'E'}}},
{[][]rune{{'む'}, {'ム'}, {'ム'}}},
{[][]rune{{'み'}, {'ミ'}, {'ミ'}}},
{[][]rune{{'ま'}, {'マ'}, {'マ'}}},
{[][]rune{{'ほ'}, {'ホ'}, {'ホ'}}},
{[][]rune{{'へ'}, {'ヘ'}, {'ヘ'}}},
{[][]rune{{'ふ'}, {'フ'}, {'フ'}}},
{[][]rune{{'ひ'}, {'ヒ'}, {'ヒ'}}},
{[][]rune{{'は'}, {'ハ'}, {'ハ'}, {'八'}}},
{[][]rune{{'の'}, {''}, {'ノ'}}},
{[][]rune{{'ね'}, {'ネ'}, {'ネ'}}},
{[][]rune{{'ぬ'}, {'ヌ'}, {'ヌ'}}},
{[][]rune{{'に'}, {'ニ'}, {'ニ'}, {'二'}}},
{[][]rune{{'な'}, {'ナ'}, {'ナ'}}},
{[][]rune{{'と'}, {'ト'}, {'ト'}, {'┣'}, {'├'}}},
{[][]rune{{'て'}, {'テ'}, {'テ'}, {'〒'}, {'乙'}}},
{[][]rune{{'つ'}, {'ツ'}, {'ツ'}, {'っ'}, {'ッ'}, {'ッ'}}},
{[][]rune{{'ち'}, {'チ'}, {'チ'}, {'千'}}},
{[][]rune{{'た'}, {'タ'}, {'タ'}, {'夕'}}},
{[][]rune{{'そ'}, {'ソ'}, {'ソ'}}},
{[][]rune{{'せ'}, {'セ'}, {'セ'}}},
{[][]rune{{'す'}, {'ス'}, {'ス'}}},
{[][]rune{{'し'}, {'シ'}, {'シ'}}},
{[][]rune{{'さ'}, {'サ'}, {'サ'}}},
{[][]rune{{'こ'}, {'コ'}, {'コ'}}},
{[][]rune{{'け'}, {'ケ'}, {'ケ'}, {'ヶ'}}},
{[][]rune{{'く'}, {'ク'}, {'ク'}}},
{[][]rune{{'き'}, {'キ'}, {'キ'}}},
{[][]rune{{'か'}, {'カ'}, {'カ'}, {'ヵ'}, {'力'}}},
{[][]rune{{'お'}, {'オ'}, {'オ'}, {'ォ'}, {'ぉ'}, {'ォ'}}},
{[][]rune{{'え'}, {'エ'}, {'エ'}, {'ェ'}, {'ぇ'}, {'ェ'}, {'工'}}},
{[][]rune{{'う'}, {'ウ'}, {'ウ'}, {'ゥ'}, {'ぅ'}, {'ゥ'}}},
{[][]rune{{'い'}, {'イ'}, {'イ'}, {'ィ'}, {'ぃ'}, {'ィ'}}},
{[][]rune{{'あ'}, {'ア'}, {'ァ'}, {'ア'}, {'ぁ'}, {'ァ'}}},
{[][]rune{{'ー'}, {'―'}, {''}, {'-'}, {''}, {'ー'}, {'一'}}},
{[][]rune{{'9'}, {''}}},
{[][]rune{{'8'}, {''}}},
{[][]rune{{'7'}, {''}}},
{[][]rune{{'6'}, {''}}},
{[][]rune{{'5'}, {''}}},
{[][]rune{{'4'}, {''}}},
{[][]rune{{'3'}, {''}}},
{[][]rune{{'2'}, {''}}},
{[][]rune{{'1'}, {''}}},
{[][]rune{{'ぽ'}, {'ポ'}, {'ホ', '゚'}, {'ほ', '゚'}, {'ホ', '゚'}, {'ホ', '°'}, {'ほ', '°'}, {'ホ', '°'}}},
{[][]rune{{'ぺ'}, {'ペ'}, {'ヘ', '゚'}, {'へ', '゚'}, {'ヘ', '゚'}, {'ヘ', '°'}, {'へ', '°'}, {'ヘ', '°'}}},
{[][]rune{{'ぷ'}, {'プ'}, {'フ', '゚'}, {'ふ', '゚'}, {'フ', '゚'}, {'フ', '°'}, {'ふ', '°'}, {'フ', '°'}}},
{[][]rune{{'ぴ'}, {'ピ'}, {'ヒ', '゚'}, {'ひ', '゚'}, {'ヒ', '゚'}, {'ヒ', '°'}, {'ひ', '°'}, {'ヒ', '°'}}},
{[][]rune{{'ぱ'}, {'パ'}, {'ハ', '゚'}, {'は', '゚'}, {'ハ', '゚'}, {'ハ', '°'}, {'は', '°'}, {'ハ', '°'}, {'八', '゚'}, {'八', '゜'}}},
{[][]rune{{'z'}, {''}, {'Z'}, {''}, {'Ζ'}}},
{[][]rune{{'y'}, {''}, {'Y'}, {''}, {'Υ'}, {'У'}, {'у'}}},
{[][]rune{{'x'}, {''}, {'X'}, {''}, {'Χ'}, {'χ'}, {'Х'}, {'×'}, {'х'}}},
{[][]rune{{'w'}, {''}, {'W'}, {''}, {'ω'}, {'Ш'}, {'ш'}, {'щ'}}},
{[][]rune{{'v'}, {''}, {'V'}, {''}, {'ν'}, {'υ'}}},
{[][]rune{{'u'}, {''}, {'U'}, {''}, {'μ'}, {''}}},
{[][]rune{{'t'}, {''}, {'T'}, {''}, {'Τ'}, {'τ'}, {'Т'}, {'т'}}},
{[][]rune{{'s'}, {''}, {'S'}, {''}, {'∫'}, {''}, {'$'}}},
{[][]rune{{'r'}, {''}, {'R'}, {''}, {'Я'}, {'я'}}},
{[][]rune{{'q'}, {''}, {'Q'}, {''}}},
{[][]rune{{'p'}, {''}, {'P'}, {''}, {'Ρ'}, {'ρ'}, {'Р'}, {'р'}}},
{[][]rune{{'o'}, {''}, {'O'}, {''}, {'○'}, {'Ο'}, {'ο'}, {'О'}, {'о'}, {'◯'}, {''}, {'0'}, {''}}},
{[][]rune{{'n'}, {''}, {'N'}, {''}, {'Ν'}, {'η'}, {'ン'}, {'ん'}, {'ン'}}},
{[][]rune{{'m'}, {''}, {'M'}, {''}, {'Μ'}, {'М'}, {'м'}}},
{[][]rune{{'l'}, {''}, {'L'}, {''}, {'|'}}},
{[][]rune{{'k'}, {''}, {'K'}, {''}, {'Κ'}, {'κ'}, {'К'}, {'к'}}},
{[][]rune{{'j'}, {''}, {'J'}, {''}}},
{[][]rune{{'i'}, {''}, {'I'}, {''}, {'Ι'}}},
{[][]rune{{'h'}, {''}, {'H'}, {''}, {'Η'}, {'Н'}, {'н'}}},
{[][]rune{{'f'}, {''}, {'F'}, {''}}},
{[][]rune{{'g'}, {''}, {'G'}, {''}}},
{[][]rune{{'e'}, {''}, {'E'}, {''}, {'Ε'}, {'ε'}, {'Е'}, {'Ё'}, {'е'}, {'ё'}, {'∈'}}},
{[][]rune{{'d'}, {''}, {'D'}, {''}}},
{[][]rune{{'c'}, {''}, {'C'}, {'С'}, {'с'}, {''}, {'℃'}}},
{[][]rune{{'b'}, {''}, {''}, {'B'}, {'β'}, {'Β'}, {'В'}, {'в'}, {'ъ'}, {'ь'}, {'♭'}}},
{[][]rune{{'\''}, {''}}},
{[][]rune{{'a'}, {''}, {''}, {'A'}, {'α'}, {'@'}, {''}, {'а'}, {'Å'}, {'А'}, {'Α'}}},
{[][]rune{{'"'}, {'”'}}},
{[][]rune{{'%'}, {''}}},
}
for _, smcGroup := range smcData {
for _, smcPair := range smcGroup.charGroup {
smc.WriteUint16(stringsupport.ToNGWord(string(smcPair[0]))[0])
if len(smcPair) > 1 {
smc.WriteUint16(stringsupport.ToNGWord(string(smcPair[1]))[0])
} else {
smc.WriteUint16(0)
}
}
smc.WriteUint32(0)
}
filters.WriteUint32(uint32(len(smc.Data())))
filters.WriteBytes(smc.Data())
filters.WriteNullTerminatedBytes([]byte("nam"))
nam := byteframe.NewByteFrame()
nam.SetLE()
for _, word := range namNGWords {
parts := stringsupport.ToNGWord(word)
nam.WriteUint32(uint32(len(parts)))
for _, part := range parts {
nam.WriteUint16(part)
var i int16
j := int16(-1)
for _, smcGroup := range smcData {
if rune(part) == rune(stringsupport.ToNGWord(string(smcGroup.charGroup[0][0]))[0]) {
j = i
break
}
i += int16(len(smcGroup.charGroup) + 1)
}
nam.WriteInt16(j)
}
nam.WriteUint16(0)
nam.WriteInt16(-1)
}
filters.WriteUint32(uint32(len(nam.Data())))
filters.WriteBytes(nam.Data())
filters.WriteNullTerminatedBytes([]byte("msg"))
msg := byteframe.NewByteFrame()
msg.SetLE()
for _, word := range msgNGWords {
parts := stringsupport.ToNGWord(word)
msg.WriteUint32(uint32(len(parts)))
for _, part := range parts {
msg.WriteUint16(part)
var i int16
j := int16(-1)
for _, smcGroup := range smcData {
if rune(part) == rune(stringsupport.ToNGWord(string(smcGroup.charGroup[0][0]))[0]) {
j = i
break
}
i += int16(len(smcGroup.charGroup) + 1)
}
msg.WriteInt16(j)
}
msg.WriteUint16(0)
msg.WriteInt16(-1)
}
filters.WriteUint32(uint32(len(msg.Data())))
filters.WriteBytes(msg.Data())
bf.WriteUint16(uint16(len(filters.Data())))
bf.WriteBytes(filters.Data())
if s.client == VITA || s.client == PS3 || s.client == PS4 {
var psnUser string
s.server.db.QueryRow("SELECT psn_id FROM users WHERE id = $1", uid).Scan(&psnUser)
bf.WriteBytes(stringsupport.PaddedString(psnUser, 20, true))

View File

@@ -11,6 +11,7 @@ import (
"erupe-ce/common/byteframe"
"erupe-ce/network"
"go.uber.org/zap"
)
@@ -20,6 +21,7 @@ const (
PC100 client = iota
VITA
PS3
PS4
WIIU
)
@@ -56,6 +58,9 @@ func (s *Session) handlePacket(pkt []byte) error {
switch reqType[:len(reqType)-3] {
case "DLTSKEYSIGN:", "DSGN:", "SIGN:":
s.handleDSGN(bf)
case "PS4SGN:":
s.client = PS4
s.handlePSSGN(bf)
case "PS3SGN:":
s.client = PS3
s.handlePSSGN(bf)
@@ -127,13 +132,16 @@ func (s *Session) handleWIIUSGN(bf *byteframe.ByteFrame) {
func (s *Session) handlePSSGN(bf *byteframe.ByteFrame) {
// Prevent reading malformed request
if len(bf.DataFromCurrent()) < 128 {
s.sendCode(SIGN_EABORT)
return
if s.client != PS4 {
if len(bf.DataFromCurrent()) < 128 {
s.sendCode(SIGN_EABORT)
return
}
_ = bf.ReadNullTerminatedBytes() // VITA = 0000000256, PS3 = 0000000255
_ = bf.ReadBytes(2) // VITA = 1, PS3 = !
_ = bf.ReadBytes(82)
}
_ = bf.ReadNullTerminatedBytes() // VITA = 0000000256, PS3 = 0000000255
_ = bf.ReadBytes(2) // VITA = 1, PS3 = !
_ = bf.ReadBytes(82)
s.psn = string(bf.ReadNullTerminatedBytes())
var uid uint32
err := s.server.db.QueryRow(`SELECT id FROM users WHERE psn_id = $1`, s.psn).Scan(&uid)