mirror of
https://github.com/Mezeporta/Erupe.git
synced 2025-12-12 15:04:38 +01:00
Merge pull request #117 from ZeruLight/feature/screenshot-api
Feature/screenshot api
This commit is contained in:
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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
37
server/api/utils.go
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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`)
|
||||
|
||||
Reference in New Issue
Block a user