diff --git a/.gitignore b/.gitignore index 4101960d2..493cbb0f6 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 +sreenshots/* \ No newline at end of file diff --git a/config.json b/config.json index 0e081c4e5..2943915ba 100644 --- a/config.json +++ b/config.json @@ -9,7 +9,12 @@ ], "PatchServerManifest": "", "PatchServerFile": "", - "ScreenshotAPIURL": "", + "Screenshots":{ + "Enabled":true, + "Host":"127.0.0.1", + "Port":8080, + "OutputDir":"screenshots" + }, "DeleteOnSaveCorruption": false, "ClientMode": "ZZ", "QuestCacheExpiry": 300, diff --git a/config/config.go b/config/config.go index 6f6d4d2af..c88bd3912 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 + SignV2 SignV2 + Channel Channel + Entrance Entrance } type SaveDumpOptions struct { @@ -105,6 +106,13 @@ 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 +} + // DebugOptions holds various debug/temporary options for use while developing Erupe. type DebugOptions struct { CleanDB bool // Automatically wipes the DB on server reset. diff --git a/schemas/patch-schema/screenshots.sql b/schemas/patch-schema/screenshots.sql new file mode 100644 index 000000000..345fcda13 --- /dev/null +++ b/schemas/patch-schema/screenshots.sql @@ -0,0 +1,13 @@ +BEGIN; + +CREATE TABLE public.screenshots +( + id serial PRIMARY KEY, + article_id TEXT NOT NULL, + discord_message_id TEXT, + char_id integer NOT NULL, + title TEXT NOT NULL, + description TEXT NOT NULL, + discord_img_url TEXT, + ); +END; \ No newline at end of file diff --git a/server/channelserver/handlers_bbs.go b/server/channelserver/handlers_bbs.go index 222a8eadc..69abc542d 100644 --- a/server/channelserver/handlers_bbs.go +++ b/server/channelserver/handlers_bbs.go @@ -8,6 +8,7 @@ import ( ) func handleMsgMhfGetBbsUserStatus(s *Session, p mhfpacket.MHFPacket) { + //Post Screenshot pauses till this succeedes pkt := p.(*mhfpacket.MsgMhfGetBbsUserStatus) bf := byteframe.NewByteFrame() bf.WriteUint32(200) @@ -32,10 +33,36 @@ func handleMsgMhfApplyBbsArticle(s *Session, p mhfpacket.MHFPacket) { bf := byteframe.NewByteFrame() articleToken := token.Generate(40) bf.WriteUint32(200) - bf.WriteUint32(80) + 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)) - doAckBufSucceed(s, pkt.AckHandle, bf.Data()) + bf.WriteBytes(stringsupport.PaddedString(s.server.erupeConfig.Screenshots.Host, 64, false)) + + if s.server.erupeConfig.SaveDumps.Enabled && s.server.erupeConfig.Discord.Enabled { + messageId := s.server.DiscordScreenShotSend(pkt.Name, pkt.Title, pkt.Description) // TODO: send and get back message id store in db + + _, err := s.server.db.Exec("INSERT INTO public.screenshots (article_id,discord_message_id,char_id,title,description) VALUES ($1,$2,$3,$4,$5)", articleToken, messageId, s.charID, pkt.Title, pkt.Description) + if err != nil { + doAckBufFail(s, pkt.AckHandle, make([]byte, 4)) + } else { + + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) + s.server.BroadcastChatMessage("Screenshot has been sent to discord") + + } + + } else if s.server.erupeConfig.SaveDumps.Enabled { + _, err := s.server.db.Exec("INSERT INTO public.screenshots (article_id,char_id,title,description) VALUES ($1,$2,$3,$4)", articleToken, s.charID, pkt.Title, pkt.Description) + if err != nil { + doAckBufFail(s, pkt.AckHandle, make([]byte, 4)) + } else { + s.server.BroadcastChatMessage("Screenshot has been sent to server") + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) + + } + } else { + doAckBufFail(s, pkt.AckHandle, make([]byte, 4)) + s.server.BroadcastChatMessage("No destination for screenshots have been configured by the host") + } } diff --git a/server/channelserver/sys_channel_server.go b/server/channelserver/sys_channel_server.go index 19bd04123..6d2c7c39d 100644 --- a/server/channelserver/sys_channel_server.go +++ b/server/channelserver/sys_channel_server.go @@ -367,6 +367,15 @@ func (s *Server) DiscordChannelSend(charName string, content string) { } } +func (s *Server) DiscordScreenShotSend(charName string, title string, description string) string { + if s.erupeConfig.Discord.Enabled && s.discordBot != nil { + message := fmt.Sprintf("**%s**: %s - %s", charName, title, description) + mesageId, _ := s.discordBot.RealtimeChannelSend(message) + return mesageId + } + return "" +} + 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..9c824684f 100644 --- a/server/discordbot/discord_bot.go +++ b/server/discordbot/discord_bot.go @@ -1,10 +1,12 @@ package discordbot import ( - "erupe-ce/config" + "errors" + _config "erupe-ce/config" + "regexp" + "github.com/bwmarrin/discordgo" "go.uber.org/zap" - "regexp" ) var Commands = []*discordgo.ApplicationCommand{ @@ -104,14 +106,14 @@ func (bot *DiscordBot) NormalizeDiscordMessage(message string) string { return result } -func (bot *DiscordBot) RealtimeChannelSend(message string) (err error) { +func (bot *DiscordBot) RealtimeChannelSend(message string) (messageId string, err error) { if bot.RelayChannel == nil { - return + return "", errors.New("RelayChannel is nil") } - _, err = bot.Session.ChannelMessageSend(bot.RelayChannel.ID, message) + msg, err := bot.Session.ChannelMessageSend(bot.RelayChannel.ID, message) - return + return msg.ID, err } func ReplaceTextAll(text string, regex *regexp.Regexp, handler func(input string) string) string { diff --git a/server/signv2server/endpoints.go b/server/signv2server/endpoints.go index b3ac00254..7647ce743 100644 --- a/server/signv2server/endpoints.go +++ b/server/signv2server/endpoints.go @@ -6,7 +6,12 @@ import ( "errors" _config "erupe-ce/config" "erupe-ce/server/channelserver" + "fmt" + "image" + "image/jpeg" "net/http" + "os" + "path/filepath" "strings" "time" @@ -286,3 +291,66 @@ func (s *Server) ExportSave(w http.ResponseWriter, r *http.Request) { w.Header().Add("Content-Type", "application/json") json.NewEncoder(w).Encode(save) } + +func (s *Server) ScreenShot(w http.ResponseWriter, r *http.Request) { + if !s.erupeConfig.SaveDumps.Enabled { + http.Error(w, "Screenshots not enabled in Config", http.StatusBadRequest) + + return + } else { + + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + // Get File from Request + file, _, err := r.FormFile("img") + if err != nil { + http.Error(w, "No valid file uploaded", http.StatusBadRequest) + return + } + token := r.FormValue("token") + if token == "" { + http.Error(w, "Token not specified cannot continue", http.StatusBadRequest) + return + } + + // Validate file + img, _, err := image.Decode(file) + if err != nil { + http.Error(w, "Invalid image file", http.StatusBadRequest) + return + } + + dir := filepath.Join(s.erupeConfig.Screenshots.OutputDir) + path := filepath.Join(s.erupeConfig.Screenshots.OutputDir, fmt.Sprintf("%s.jpg", token)) + _, err = os.Stat(dir) + if err != nil { + if os.IsNotExist(err) { + err = os.MkdirAll(dir, os.ModePerm) + if err != nil { + s.logger.Error("Error writing screenshot, could not create folder") + return + } + } else { + s.logger.Error("Error writing screenshot") + return + } + } + // Create or open the output file + outputFile, err := os.Create(path) + if err != nil { + panic(err) + } + defer outputFile.Close() + + // Encode the image and write it to the file + err = jpeg.Encode(outputFile, img, &jpeg.Options{}) + if err != nil { + panic(err) + } + if err != nil { + s.logger.Error("Error writing screenshot, could not write file", zap.Error(err)) + } + } +} diff --git a/server/signv2server/signv2_server.go b/server/signv2server/signv2_server.go index fedbabba2..3b278a3a7 100644 --- a/server/signv2server/signv2_server.go +++ b/server/signv2server/signv2_server.go @@ -2,7 +2,7 @@ package signv2server import ( "context" - "erupe-ce/config" + _config "erupe-ce/config" "fmt" "net/http" "os" @@ -52,6 +52,7 @@ 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) 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)