diff --git a/.gitignore b/.gitignore index 4101960d2..5b569b1c2 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ savedata/*/ *.exe *.lnk *.bat -/docker/db-data \ No newline at end of file +/docker/db-data +screenshots/* \ No newline at end of file diff --git a/config.json b/config.json index 0e081c4e5..2b836ab03 100644 --- a/config.json +++ b/config.json @@ -9,7 +9,13 @@ ], "PatchServerManifest": "", "PatchServerFile": "", - "ScreenshotAPIURL": "", + "Screenshots":{ + "Enabled":true, + "Host":"127.0.0.1", + "Port":8080, + "OutputDir":"screenshots", + "UploadQuality":100 + }, "DeleteOnSaveCorruption": false, "ClientMode": "ZZ", "QuestCacheExpiry": 300, @@ -189,8 +195,8 @@ "Enabled": true, "Port": 53312 }, - "SignV2": { - "Enabled": false, + "API": { + "Enabled": true, "Port": 8080, "PatchServer": "", "Banners": [], diff --git a/config/config.go b/config/config.go index 6f6d4d2af..52642956b 100644 --- a/config/config.go +++ b/config/config.go @@ -75,7 +75,6 @@ type Config struct { LoginNotices []string // MHFML string of the login notices displayed PatchServerManifest string // Manifest patch server override PatchServerFile string // File patch server override - ScreenshotAPIURL string // Destination for screenshots uploaded to BBS DeleteOnSaveCorruption bool // Attempts to save corrupted data will flag the save for deletion ClientMode string RealClientMode Mode @@ -87,16 +86,18 @@ type Config struct { EarthID int32 EarthMonsters []int32 SaveDumps SaveDumpOptions - DebugOptions DebugOptions - GameplayOptions GameplayOptions - Discord Discord - Commands []Command - Courses []Course - Database Database - Sign Sign - SignV2 SignV2 - Channel Channel - Entrance Entrance + Screenshots ScreenshotsOptions + + DebugOptions DebugOptions + GameplayOptions GameplayOptions + Discord Discord + Commands []Command + Courses []Course + Database Database + Sign Sign + API API + Channel Channel + Entrance Entrance } type SaveDumpOptions struct { @@ -105,6 +106,14 @@ type SaveDumpOptions struct { OutputDir string } +type ScreenshotsOptions struct { + Enabled bool + Host string // Destination for screenshots uploaded to BBS + Port uint32 // Port for screenshots API + OutputDir string + UploadQuality int //Determines the upload quality to the server +} + // DebugOptions holds various debug/temporary options for use while developing Erupe. type DebugOptions struct { CleanDB bool // Automatically wipes the DB on server reset. @@ -228,29 +237,29 @@ type Sign struct { Port int } -// SignV2 holds the new sign server config -type SignV2 struct { +// API holds server config +type API struct { Enabled bool Port int PatchServer string - Banners []SignV2Banner - Messages []SignV2Message - Links []SignV2Link + Banners []APISignBanner + Messages []APISignMessage + Links []APISignLink } -type SignV2Banner struct { +type APISignBanner struct { Src string `json:"src"` // Displayed image URL Link string `json:"link"` // Link accessed on click } -type SignV2Message struct { +type APISignMessage struct { Message string `json:"message"` // Displayed message Date int64 `json:"date"` // Displayed date Kind int `json:"kind"` // 0 for 'Default', 1 for 'New' Link string `json:"link"` // Link accessed on click } -type SignV2Link struct { +type APISignLink struct { Name string `json:"name"` // Displayed name Icon string `json:"icon"` // Displayed icon. It will be cast as a monochrome color as long as it is transparent. Link string `json:"link"` // Link accessed on click diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index a610836ee..c961a3ce4 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -42,8 +42,8 @@ services: build: context: ../ volumes: - - ./config.json:/app/erupe/config.json - - ./bin:/app/erupe/bin + - ../config.json:/app/erupe/config.json + - ../bin:/app/erupe/bin - ./savedata:/app/erupe/savedata ports: # (Make sure these match config.json) diff --git a/docker/init/setup.sh b/docker/init/setup.sh index b84f83b4d..46e16274a 100644 --- a/docker/init/setup.sh +++ b/docker/init/setup.sh @@ -1,7 +1,7 @@ #!/bin/bash set -e echo "INIT!" -pg_restore --username="$POSTGRES_USER" --dbname="$POSTGRES_DB" --verbose /schemas/initialisation-schema/9.1-init.sql +pg_restore --username="$POSTGRES_USER" --dbname="$POSTGRES_DB" --verbose /schemas/init.sql diff --git a/main.go b/main.go index a7d368930..2c776a78c 100644 --- a/main.go +++ b/main.go @@ -10,11 +10,11 @@ import ( "syscall" "time" + "erupe-ce/server/api" "erupe-ce/server/channelserver" "erupe-ce/server/discordbot" "erupe-ce/server/entranceserver" "erupe-ce/server/signserver" - "erupe-ce/server/signv2server" "github.com/jmoiron/sqlx" _ "github.com/lib/pq" @@ -181,21 +181,21 @@ func main() { } // New Sign server - var newSignServer *signv2server.Server - if config.SignV2.Enabled { - newSignServer = signv2server.NewServer( - &signv2server.Config{ + var ApiServer *api.APIServer + if config.API.Enabled { + ApiServer = api.NewAPIServer( + &api.Config{ Logger: logger.Named("sign"), ErupeConfig: _config.ErupeConfig, DB: db, }) - err = newSignServer.Start() + err = ApiServer.Start() if err != nil { - preventClose(fmt.Sprintf("SignV2: Failed to start, %s", err.Error())) + preventClose(fmt.Sprintf("API: Failed to start, %s", err.Error())) } - logger.Info("SignV2: Started successfully") + logger.Info("API: Started successfully") } else { - logger.Info("SignV2: Disabled") + logger.Info("API: Disabled") } var channels []*channelserver.Server @@ -273,8 +273,8 @@ func main() { signServer.Shutdown() } - if config.SignV2.Enabled { - newSignServer.Shutdown() + if config.API.Enabled { + ApiServer.Shutdown() } if config.Entrance.Enabled { diff --git a/network/mhfpacket/msg_mhf_stampcard_stamp.go b/network/mhfpacket/msg_mhf_stampcard_stamp.go index f9da9612e..a783b3e91 100644 --- a/network/mhfpacket/msg_mhf_stampcard_stamp.go +++ b/network/mhfpacket/msg_mhf_stampcard_stamp.go @@ -32,7 +32,9 @@ func (m *MsgMhfStampcardStamp) Opcode() network.PacketID { func (m *MsgMhfStampcardStamp) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error { m.AckHandle = bf.ReadUint32() m.HR = bf.ReadUint16() - m.GR = bf.ReadUint16() + if _config.ErupeConfig.RealClientMode >= _config.G1 { + m.GR = bf.ReadUint16() + } m.Stamps = bf.ReadUint16() bf.ReadUint16() // Zeroed if _config.ErupeConfig.RealClientMode > _config.Z1 { diff --git a/schemas/patch-schema/fix-weekly-stamps.sql b/schemas/patch-schema/fix-weekly-stamps.sql new file mode 100644 index 000000000..30f551e5c --- /dev/null +++ b/schemas/patch-schema/fix-weekly-stamps.sql @@ -0,0 +1,6 @@ +BEGIN; + +ALTER TABLE IF EXISTS public.stamps RENAME hl_next TO hl_checked; +ALTER TABLE IF EXISTS public.stamps RENAME ex_next TO ex_checked; + +END; \ No newline at end of file diff --git a/server/signv2server/signv2_server.go b/server/api/api_server.go similarity index 80% rename from server/signv2server/signv2_server.go rename to server/api/api_server.go index fedbabba2..3774f3fb8 100644 --- a/server/signv2server/signv2_server.go +++ b/server/api/api_server.go @@ -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() diff --git a/server/signv2server/dbutils.go b/server/api/dbutils.go similarity index 81% rename from server/signv2server/dbutils.go rename to server/api/dbutils.go index b2d5872bb..fba1bab5c 100644 --- a/server/signv2server/dbutils.go +++ b/server/api/dbutils.go @@ -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) diff --git a/server/signv2server/endpoints.go b/server/api/endpoints.go similarity index 63% rename from server/signv2server/endpoints.go rename to server/api/endpoints.go index b3ac00254..4eaac119e 100644 --- a/server/signv2server/endpoints.go +++ b/server/api/endpoints.go @@ -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) +} diff --git a/server/api/utils.go b/server/api/utils.go new file mode 100644 index 000000000..1a7a18d26 --- /dev/null +++ b/server/api/utils.go @@ -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 + } +} diff --git a/server/channelserver/handlers.go b/server/channelserver/handlers.go index fb91f096f..d301ad6c9 100644 --- a/server/channelserver/handlers.go +++ b/server/channelserver/handlers.go @@ -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()) } @@ -1049,9 +1053,11 @@ func handleMsgMhfStampcardStamp(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfStampcardStamp) bf := byteframe.NewByteFrame() bf.WriteUint16(pkt.HR) - bf.WriteUint16(pkt.GR) var stamps uint16 _ = s.server.db.QueryRow(`SELECT stampcard FROM characters WHERE id = $1`, s.charID).Scan(&stamps) + if _config.ErupeConfig.RealClientMode >= _config.G1 { + bf.WriteUint16(pkt.GR) + } bf.WriteUint16(stamps) stamps += pkt.Stamps bf.WriteUint16(stamps) diff --git a/server/channelserver/handlers_bbs.go b/server/channelserver/handlers_bbs.go index 222a8eadc..d991ee67a 100644 --- a/server/channelserver/handlers_bbs.go +++ b/server/channelserver/handlers_bbs.go @@ -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()) + } diff --git a/server/channelserver/handlers_character.go b/server/channelserver/handlers_character.go index 782ec8190..795fc05e1 100644 --- a/server/channelserver/handlers_character.go +++ b/server/channelserver/handlers_character.go @@ -81,7 +81,7 @@ func getPointers() map[SavePointer]int { pointers[pHR] = 94550 pointers[pGRP] = 94556 pointers[pHouseData] = 94561 - pointers[pBookshelfData] = 103928 + pointers[pBookshelfData] = 89118 // TODO: fix bookshelf data pointer pointers[pGalleryData] = 104064 pointers[pGardenData] = 106424 pointers[pRP] = 106614 @@ -93,7 +93,7 @@ func getPointers() map[SavePointer]int { pointers[pToreData] = 62228 pointers[pHR] = 62550 pointers[pHouseData] = 62561 - pointers[pBookshelfData] = 57118 // This pointer only half works + pointers[pBookshelfData] = 57118 // TODO: fix bookshelf data pointer pointers[pGalleryData] = 72064 pointers[pGardenData] = 74424 pointers[pRP] = 74614 @@ -104,7 +104,7 @@ func getPointers() map[SavePointer]int { pointers[pToreData] = 14228 pointers[pHR] = 14550 pointers[pHouseData] = 14561 - pointers[pBookshelfData] = 9118 // Probably same here + pointers[pBookshelfData] = 9118 // TODO: fix bookshelf data pointer pointers[pGalleryData] = 24064 pointers[pGardenData] = 26424 pointers[pRP] = 26614 diff --git a/server/channelserver/handlers_quest.go b/server/channelserver/handlers_quest.go index b94be2f84..cf698ceed 100644 --- a/server/channelserver/handlers_quest.go +++ b/server/channelserver/handlers_quest.go @@ -311,7 +311,7 @@ func makeEventQuest(s *Session, rows *sql.Rows) ([]byte, error) { bf.WriteBool(true) } bf.WriteUint16(0) // Unk - if _config.ErupeConfig.RealClientMode >= _config.G1 { + if _config.ErupeConfig.RealClientMode >= _config.G2 { bf.WriteUint32(mark) } bf.WriteUint16(0) // Unk @@ -609,7 +609,7 @@ func handleMsgMhfEnumerateQuest(s *Session, p mhfpacket.MHFPacket) { tuneValues = temp tuneLimit := 770 - if _config.ErupeConfig.RealClientMode <= _config.F5 { + if _config.ErupeConfig.RealClientMode <= _config.G1 { tuneLimit = 256 } else if _config.ErupeConfig.RealClientMode <= _config.G3 { tuneLimit = 283 diff --git a/server/channelserver/handlers_stage.go b/server/channelserver/handlers_stage.go index e3196bc44..8a1abbc35 100644 --- a/server/channelserver/handlers_stage.go +++ b/server/channelserver/handlers_stage.go @@ -150,6 +150,9 @@ func removeSessionFromStage(s *Session) { func isStageFull(s *Session, StageID string) bool { if stage, exists := s.server.stages[StageID]; exists { + if _, exists := stage.reservedClientSlots[s.charID]; exists { + return false + } return len(stage.reservedClientSlots)+len(stage.clients) >= int(stage.maxPlayers) } return false diff --git a/server/channelserver/sys_channel_server.go b/server/channelserver/sys_channel_server.go index 19bd04123..a0d1fe1b7 100644 --- a/server/channelserver/sys_channel_server.go +++ b/server/channelserver/sys_channel_server.go @@ -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 { diff --git a/server/discordbot/discord_bot.go b/server/discordbot/discord_bot.go index a9b327cc3..303cbc630 100644 --- a/server/discordbot/discord_bot.go +++ b/server/discordbot/discord_bot.go @@ -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`) diff --git a/server/signserver/dsgn_resp.go b/server/signserver/dsgn_resp.go index 452b02475..250bdae46 100644 --- a/server/signserver/dsgn_resp.go +++ b/server/signserver/dsgn_resp.go @@ -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 { @@ -135,7 +136,7 @@ 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 { + 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)) diff --git a/server/signserver/session.go b/server/signserver/session.go index e4cbd5537..c314a44a0 100644 --- a/server/signserver/session.go +++ b/server/signserver/session.go @@ -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)