mirror of
https://github.com/WiiLink24/wfc-server.git
synced 2026-10-01 11:36:34 -05:00
GPCM: Handle friend requests
This commit is contained in:
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
const (
|
||||
InsertUser = `INSERT INTO users (user_id, gsbrcd, password, email, unique_nick) VALUES ($1, $2, $3, $4, $5) RETURNING profile_id`
|
||||
UpdateUserTable = `UPDATE users SET firstname = $1, lastname = $2 WHERE user_id = $3`
|
||||
UpdateUserTable = `UPDATE users SET firstname = CASE WHEN $3 THEN $2 ELSE firstname END, lastname = CASE WHEN $5 THEN $4 ELSE lastname END WHERE profile_id = $1 RETURNING user_id, gsbrcd, password, email, unique_nick, firstname, lastname`
|
||||
GetUser = `SELECT user_id, gsbrcd, password, email, unique_nick, firstname, lastname FROM users WHERE profile_id = $1`
|
||||
CreateUserSession = `INSERT INTO sessions (session_key, profile_id, login_ticket) VALUES ($1, $2, $3)`
|
||||
DoesUserExist = `SELECT EXISTS(SELECT 1 FROM users WHERE user_id = $1 AND gsbrcd = $2)`
|
||||
@@ -42,13 +42,18 @@ func GetUniqueUserID() int64 {
|
||||
return rand.Int63n(0x80000000000)
|
||||
}
|
||||
|
||||
func UpdateUser(pool *pgxpool.Pool, ctx context.Context, firstName string, lastName string, userId int64) User {
|
||||
func UpdateProfile(pool *pgxpool.Pool, ctx context.Context, profileId uint32, data map[string]string) User {
|
||||
firstName, firstNameExists := data["firstname"]
|
||||
lastName, lastNameExists := data["lastname"]
|
||||
|
||||
user := User{}
|
||||
_, err := pool.Exec(ctx, UpdateUserTable, firstName, lastName, userId)
|
||||
row := pool.QueryRow(ctx, UpdateUserTable, profileId, firstName, firstNameExists, lastName, lastNameExists)
|
||||
err := row.Scan(&user.UserId, &user.GsbrCode, &user.Password, &user.Email, &user.UniqueNick, &user.FirstName, &user.LastName)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
user.ProfileId = profileId
|
||||
return user
|
||||
}
|
||||
|
||||
|
||||
151
gpcm/error.go
Normal file
151
gpcm/error.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package gpcm
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"wwfc/common"
|
||||
)
|
||||
|
||||
var gpcmErrors = map[int]string{
|
||||
// General errors
|
||||
0: "There was an unknown error.",
|
||||
1: "There was an error parsing an incoming request.",
|
||||
2: "This request cannot be processed because you are not logged in.",
|
||||
3: "This request cannot be processed because the session key is invalid.",
|
||||
4: "This request cannot be processed because of a database error.",
|
||||
5: "There was an error connecting a network socket.",
|
||||
6: "This profile has been disconnected by another login.",
|
||||
7: "The server has closed the connection.",
|
||||
8: "There was a problem with the UDP layer.",
|
||||
|
||||
// Log-in errors
|
||||
256: "There was an error logging in to the GP backend.",
|
||||
257: "The login attempt timed out.",
|
||||
258: "The nickname provided was incorrect.",
|
||||
259: "The email address provided was incorrect.",
|
||||
260: "The password provided is incorrect.",
|
||||
261: "The profile provided was incorrect.",
|
||||
262: "The profile has been deleted.",
|
||||
263: "The server has refused the connection.",
|
||||
264: "The server could not be authenticated.",
|
||||
265: "The uniquenick provided is incorrect.",
|
||||
266: "There was an error validating the pre-authentication.",
|
||||
267: "The login ticket was unable to be validated.",
|
||||
268: "The login ticket had expired and could not be used.",
|
||||
|
||||
// New user errors
|
||||
512: "There was an error creating a new user.",
|
||||
513: "A profile with that nick already exists.",
|
||||
514: "The password does not match the email address.",
|
||||
515: "The uniquenick is invalid.",
|
||||
516: "The uniquenick is already in use.",
|
||||
|
||||
// User updating errors
|
||||
768: "There was an error updating the user information.",
|
||||
769: "A user with the email adress provided already exists.",
|
||||
|
||||
// New profile errors
|
||||
1024: "There was an error creating a new profile.",
|
||||
1025: "The nickname to be replaced does not exist.",
|
||||
1026: "A profile with the nickname provided already exists.",
|
||||
|
||||
// Updating profile errors
|
||||
1280: "There was an error updating the profile information. ",
|
||||
1281: "A user with the nickname provided already exists.",
|
||||
|
||||
// Buddy list errors
|
||||
1536: "There was an error adding a buddy.",
|
||||
1537: "The profile requesting to add a buddy is invalid.",
|
||||
1538: "The profile requested is invalid.",
|
||||
1539: "The profile requested is already a buddy.",
|
||||
1540: "The profile requested is on the local profile's block list.",
|
||||
1541: "The profile requested is blocking you.",
|
||||
|
||||
// Errors while Buddy add auth
|
||||
1792: "There was an error authorizing an add buddy request.",
|
||||
1793: "The profile being authorized is invalid.",
|
||||
1794: "The signature for the authorization is invalid.",
|
||||
1795: "The profile requesting authorization is on a block list.",
|
||||
1796: "The profile requested is blocking you.",
|
||||
|
||||
// Status errors
|
||||
2048: "There was an error with the status string.",
|
||||
|
||||
// Buddy message errors
|
||||
2304: "There was an error sending a buddy message.",
|
||||
2305: "The profile the message was to be sent to is not a buddy.",
|
||||
2306: "The profile does not support extended info keys.",
|
||||
2307: "The buddy to send a message to is offline.",
|
||||
|
||||
// Profile information getting errors
|
||||
2560: "There was an error getting profile info.",
|
||||
2561: "The profile info was requested on is invalid.",
|
||||
|
||||
// Deleting buddy errors
|
||||
2816: "There was an error deleting the buddy.",
|
||||
2817: "The buddy to be deleted is not a buddy.",
|
||||
|
||||
// Deleting profile errors
|
||||
3072: "There was an error deleting the profile.",
|
||||
3073: "The last profile cannot be deleted.",
|
||||
|
||||
// Profile searching errors
|
||||
3328: "There was an error searching for a profile.",
|
||||
3329: "The search attempt failed to connect to the server.",
|
||||
3330: "The search did not return in a timely fashion.",
|
||||
|
||||
// User checking errors
|
||||
3584: "There was an error checking the user account.",
|
||||
3585: "No account exists with the provided e-mail address.",
|
||||
3586: "No such profile exists for the provided e-mail adress.",
|
||||
3587: "The password is incorrect.",
|
||||
|
||||
// Revoking buddy errors
|
||||
3840: "There was an error revoking the buddy.",
|
||||
3841: "You are not a buddy of the profile.",
|
||||
|
||||
// Registering new unique nick errors
|
||||
4096: "There was an error registering the uniquenick.",
|
||||
4097: "The uniquenick is already taken.",
|
||||
4098: "The uniquenick is reserved.",
|
||||
4099: "Tried to register a nick with no namespace set.",
|
||||
|
||||
// CD key errors
|
||||
4352: "There was an error registering the cdkey.",
|
||||
4353: "The cdkey is invalid.",
|
||||
4354: "The profile has already been registered with a different cdkey.",
|
||||
4355: "The cdkey has already been registered to another profile.",
|
||||
|
||||
// Adding to block list errors
|
||||
4608: "There was an error adding the player to the blocked list.",
|
||||
4609: "The profile specified is already blocked.",
|
||||
|
||||
// Removing from block list errors
|
||||
4864: "There was an error removing the player from the blocked list.",
|
||||
4865: "The profile specified was not a member of the blocked list.",
|
||||
}
|
||||
|
||||
func createGameSpyError(err int) string {
|
||||
errMsg, ok := gpcmErrors[err]
|
||||
if !ok {
|
||||
errMsg = ""
|
||||
}
|
||||
|
||||
command := common.GameSpyCommand{
|
||||
Command: "error",
|
||||
CommandValue: "",
|
||||
OtherValues: map[string]string{
|
||||
"err": strconv.Itoa(err),
|
||||
"errmsg": errMsg,
|
||||
},
|
||||
}
|
||||
|
||||
if err < 300 {
|
||||
command.OtherValues["fatal"] = ""
|
||||
}
|
||||
|
||||
return common.CreateGameSpyMessage(command)
|
||||
}
|
||||
|
||||
func (g *GameSpySession) replyError(err int) {
|
||||
g.Conn.Write([]byte(createGameSpyError(err)))
|
||||
}
|
||||
183
gpcm/friend.go
Normal file
183
gpcm/friend.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package gpcm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/jackc/pgx/v4/pgxpool"
|
||||
"github.com/logrusorgru/aurora/v3"
|
||||
"strconv"
|
||||
"strings"
|
||||
"wwfc/common"
|
||||
"wwfc/logging"
|
||||
)
|
||||
|
||||
func (g *GameSpySession) isFriendAdded(profileId uint32) bool {
|
||||
for _, storedPid := range g.FriendList {
|
||||
if storedPid == profileId {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func sendMessageToSession(msgType string, from uint32, session *GameSpySession, msg string) {
|
||||
message := common.CreateGameSpyMessage(common.GameSpyCommand{
|
||||
Command: "bm",
|
||||
CommandValue: msgType,
|
||||
OtherValues: map[string]string{
|
||||
"f": strconv.FormatUint(uint64(from), 10),
|
||||
"msg": msg,
|
||||
},
|
||||
})
|
||||
session.Conn.Write([]byte(message))
|
||||
}
|
||||
|
||||
func sendMessageToProfileId(msgType string, from uint32, to uint32, msg string) bool {
|
||||
if session, ok := sessions[to]; ok && session.LoggedIn {
|
||||
sendMessageToSession(msgType, from, session, msg)
|
||||
return true
|
||||
}
|
||||
|
||||
logging.Info("GPCM", "Destination", aurora.Cyan(to), "from", aurora.Cyan(from), "is not online")
|
||||
return false
|
||||
}
|
||||
|
||||
func (g *GameSpySession) addFriend(pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) {
|
||||
strNewProfileId := command.OtherValues["newprofileid"]
|
||||
newProfileId, err := strconv.ParseUint(strNewProfileId, 10, 32)
|
||||
if err != nil {
|
||||
g.replyError(1538)
|
||||
return
|
||||
}
|
||||
|
||||
fc := common.CalcFriendCodeString(uint32(newProfileId), "RMCJ")
|
||||
logging.Notice(g.ModuleName, "Add friend:", aurora.Cyan(strNewProfileId), aurora.Cyan(fc))
|
||||
|
||||
if g.isFriendAdded(uint32(newProfileId)) {
|
||||
g.replyError(1539)
|
||||
return
|
||||
}
|
||||
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
// TODO: Add a limit
|
||||
g.FriendList = append(g.FriendList, uint32(newProfileId))
|
||||
sendMessageToProfileId("2", g.User.ProfileId, uint32(newProfileId), "\r\n\r\n|signed|"+common.RandomHexString(32))
|
||||
}
|
||||
|
||||
func (g *GameSpySession) removeFriend(pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
func (g *GameSpySession) bestieMessage(pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) {
|
||||
if command.CommandValue != "1" {
|
||||
logging.Notice(g.ModuleName, "Received unknown bestie message type:", aurora.Cyan(command.CommandValue))
|
||||
return
|
||||
}
|
||||
|
||||
strToProfileId := command.OtherValues["t"]
|
||||
toProfileId, err := strconv.ParseUint(strToProfileId, 10, 32)
|
||||
if err != nil {
|
||||
g.replyError(2304)
|
||||
return
|
||||
}
|
||||
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
if toSession, ok := sessions[uint32(toProfileId)]; ok && toSession.LoggedIn {
|
||||
// TODO: Check if mutual friends
|
||||
// TODO SECURITY: Sanitize message (there's a stack overflow exploit in DWC here)
|
||||
sendMessageToSession("1", g.User.ProfileId, toSession, command.OtherValues["msg"])
|
||||
return
|
||||
}
|
||||
|
||||
g.replyError(2307)
|
||||
}
|
||||
|
||||
func (g *GameSpySession) authAddFriend(pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) {
|
||||
strFromProfileId := command.OtherValues["fromprofileid"]
|
||||
fromProfileId, err := strconv.ParseUint(strFromProfileId, 10, 32)
|
||||
if err != nil {
|
||||
g.replyError(1793)
|
||||
return
|
||||
}
|
||||
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
sendMessageToProfileId("4", g.User.ProfileId, uint32(fromProfileId), "")
|
||||
// Send status as well
|
||||
if session, ok := sessions[uint32(fromProfileId)]; ok && session.LoggedIn && g.Status != "" {
|
||||
// TODO: Check if on friend list
|
||||
session.Conn.Write([]byte(g.Status))
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GameSpySession) setStatus(pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) {
|
||||
status := command.CommandValue
|
||||
|
||||
statstring, ok := command.OtherValues["statstring"]
|
||||
if !ok {
|
||||
logging.Notice(g.ModuleName, "Missing statstring")
|
||||
statstring = ""
|
||||
} else {
|
||||
if statstring == "" {
|
||||
logging.Notice(g.ModuleName, "statstring: (empty)")
|
||||
} else {
|
||||
logging.Notice(g.ModuleName, "statstring:", aurora.Cyan(statstring))
|
||||
}
|
||||
}
|
||||
|
||||
locstring, ok := command.OtherValues["locstring"]
|
||||
if !ok {
|
||||
logging.Notice(g.ModuleName, "Missing locstring")
|
||||
locstring = ""
|
||||
} else {
|
||||
if locstring == "" {
|
||||
logging.Notice(g.ModuleName, "locstring: (empty)")
|
||||
} else {
|
||||
logging.Notice(g.ModuleName, "locstring:", aurora.Cyan(locstring))
|
||||
}
|
||||
}
|
||||
|
||||
// Get the IP address for the status msg
|
||||
var rawIP int
|
||||
for i, s := range strings.Split(strings.Split(g.Conn.RemoteAddr().String(), ":")[0], ".") {
|
||||
val, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
rawIP |= val << (24 - i*8)
|
||||
}
|
||||
|
||||
// TODO: Check if this handles negative numbers correctly
|
||||
ip := strconv.FormatInt(int64(int32(rawIP)), 10)
|
||||
|
||||
friendStatus := common.CreateGameSpyMessage(common.GameSpyCommand{
|
||||
Command: "bm",
|
||||
CommandValue: "100",
|
||||
OtherValues: map[string]string{
|
||||
"f": strconv.FormatUint(uint64(g.User.ProfileId), 10),
|
||||
"msg": "|s|" + status + "|ss|" + statstring + "|ls|" + locstring + "|ip|" + ip + "|p|0|qm|0",
|
||||
},
|
||||
})
|
||||
|
||||
mutex.Lock()
|
||||
g.LocString = locstring
|
||||
g.Status = friendStatus
|
||||
|
||||
for _, storedPid := range g.FriendList {
|
||||
if session, ok := sessions[uint32(storedPid)]; ok && session.LoggedIn {
|
||||
if !session.isFriendAdded(g.User.ProfileId) {
|
||||
continue
|
||||
}
|
||||
|
||||
// TODO: Check if on friend list
|
||||
session.Conn.Write([]byte(friendStatus))
|
||||
}
|
||||
}
|
||||
|
||||
mutex.Unlock()
|
||||
}
|
||||
11
gpcm/main.go
11
gpcm/main.go
@@ -22,6 +22,8 @@ type GameSpySession struct {
|
||||
ModuleName string
|
||||
LoggedIn bool
|
||||
Status string
|
||||
LocString string
|
||||
FriendList []uint32
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -88,6 +90,7 @@ func handleRequest(conn net.Conn) {
|
||||
ModuleName: "GPCM",
|
||||
LoggedIn: false,
|
||||
Status: "",
|
||||
FriendList: []uint32{},
|
||||
}
|
||||
|
||||
defer session.closeSession()
|
||||
@@ -171,6 +174,14 @@ func handleRequest(conn net.Conn) {
|
||||
case "delbuddy":
|
||||
session.removeFriend(pool, ctx, command)
|
||||
break
|
||||
|
||||
case "bm":
|
||||
session.bestieMessage(pool, ctx, command)
|
||||
break
|
||||
|
||||
case "authadd":
|
||||
session.authAddFriend(pool, ctx, command)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
107
gpcm/profile.go
107
gpcm/profile.go
@@ -5,7 +5,6 @@ import (
|
||||
"github.com/jackc/pgx/v4/pgxpool"
|
||||
"github.com/logrusorgru/aurora/v3"
|
||||
"strconv"
|
||||
"strings"
|
||||
"wwfc/common"
|
||||
"wwfc/database"
|
||||
"wwfc/logging"
|
||||
@@ -13,17 +12,26 @@ import (
|
||||
|
||||
func (g *GameSpySession) getProfile(pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) string {
|
||||
strProfileId := command.OtherValues["profileid"]
|
||||
profileId, err := strconv.ParseInt(strProfileId, 10, 32)
|
||||
profileId, err := strconv.ParseUint(strProfileId, 10, 32)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return createGameSpyError(2560)
|
||||
}
|
||||
|
||||
user, ok := database.GetProfile(pool, ctx, uint32(profileId))
|
||||
if !ok {
|
||||
return `\pi\\final\`
|
||||
logging.Notice(g.ModuleName, "Looking up the profile of", aurora.Cyan(profileId).String())
|
||||
|
||||
user := database.User{}
|
||||
locstring := ""
|
||||
|
||||
mutex.Lock()
|
||||
if session, ok := sessions[uint32(profileId)]; ok && session.LoggedIn {
|
||||
locstring = session.LocString
|
||||
user = session.User
|
||||
mutex.Unlock()
|
||||
} else {
|
||||
mutex.Unlock()
|
||||
user, _ = database.GetProfile(pool, ctx, uint32(profileId))
|
||||
}
|
||||
|
||||
_ = common.RandomHexString(32)
|
||||
return common.CreateGameSpyMessage(common.GameSpyCommand{
|
||||
Command: "pi",
|
||||
CommandValue: "",
|
||||
@@ -39,93 +47,12 @@ func (g *GameSpySession) getProfile(pool *pgxpool.Pool, ctx context.Context, com
|
||||
"pid": "11",
|
||||
"lon": "0.000000",
|
||||
"lat": "0.000000",
|
||||
"loc": "",
|
||||
"loc": locstring,
|
||||
"id": command.OtherValues["id"],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (g *GameSpySession) updateProfile(pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) {
|
||||
var firstName string
|
||||
var lastName string
|
||||
if v, ok := command.OtherValues["firstname"]; ok {
|
||||
firstName = v
|
||||
}
|
||||
|
||||
if v, ok := command.OtherValues["lastname"]; ok {
|
||||
lastName = v
|
||||
}
|
||||
|
||||
database.UpdateUser(pool, ctx, firstName, lastName, g.User.UserId)
|
||||
}
|
||||
|
||||
func (g *GameSpySession) setStatus(pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) {
|
||||
status := command.CommandValue
|
||||
|
||||
statstring, ok := command.OtherValues["statstring"]
|
||||
if !ok {
|
||||
logging.Notice(g.ModuleName, "Missing statstring")
|
||||
statstring = ""
|
||||
} else {
|
||||
if statstring == "" {
|
||||
logging.Notice(g.ModuleName, "statstring: (empty)")
|
||||
} else {
|
||||
logging.Notice(g.ModuleName, "statstring:", aurora.Cyan(statstring))
|
||||
}
|
||||
}
|
||||
|
||||
locstring, ok := command.OtherValues["locstring"]
|
||||
if !ok {
|
||||
logging.Notice(g.ModuleName, "Missing locstring")
|
||||
locstring = ""
|
||||
} else {
|
||||
if locstring == "" {
|
||||
logging.Notice(g.ModuleName, "locstring: (empty)")
|
||||
} else {
|
||||
logging.Notice(g.ModuleName, "locstring:", aurora.Cyan(locstring))
|
||||
}
|
||||
}
|
||||
|
||||
// Get the IP address for the status msg
|
||||
var rawIP int
|
||||
for i, s := range strings.Split(strings.Split(g.Conn.RemoteAddr().String(), ":")[0], ".") {
|
||||
val, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
rawIP |= val << (24 - i*8)
|
||||
}
|
||||
|
||||
// TODO: Check if this handles negative numbers correctly
|
||||
ip := strconv.FormatInt(int64(int32(rawIP)), 10)
|
||||
|
||||
friendStatus := common.CreateGameSpyMessage(common.GameSpyCommand{
|
||||
Command: "bm",
|
||||
CommandValue: "100",
|
||||
OtherValues: map[string]string{
|
||||
"f": strconv.FormatUint(uint64(g.User.ProfileId), 10),
|
||||
"msg": "|s|" + status + "|ss|" + statstring + "|ls|" + locstring + "|ip|" + ip + "|p|0|qm|0",
|
||||
},
|
||||
})
|
||||
|
||||
mutex.Lock()
|
||||
g.Status = friendStatus
|
||||
mutex.Unlock()
|
||||
}
|
||||
|
||||
func (g *GameSpySession) addFriend(pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) {
|
||||
strProfileId := command.OtherValues["newprofileid"]
|
||||
profileId, err := strconv.ParseUint(strProfileId, 10, 32)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fc := common.CalcFriendCodeString(uint32(profileId), "RMCJ")
|
||||
logging.Notice(g.ModuleName, "Add friend:", aurora.Cyan(strProfileId), aurora.Cyan(fc))
|
||||
// TODO
|
||||
}
|
||||
|
||||
func (g *GameSpySession) removeFriend(pool *pgxpool.Pool, ctx context.Context, command common.GameSpyCommand) {
|
||||
// TODO
|
||||
g.User = database.UpdateProfile(pool, ctx, g.User.ProfileId, command.OtherValues)
|
||||
}
|
||||
|
||||
@@ -162,7 +162,13 @@ func handleOthersList(moduleName string, profileId uint32, command common.GameSp
|
||||
panic(err)
|
||||
}
|
||||
|
||||
opidsSplit := strings.Split(opids, "|")
|
||||
opidsSplit := []string{}
|
||||
if strings.Contains(opids, "|") {
|
||||
opidsSplit = strings.Split(opids, "|")
|
||||
} else if opids != "" && opids != "0" {
|
||||
opidsSplit = append(opidsSplit, opids)
|
||||
}
|
||||
|
||||
if len(opidsSplit) != numOpidsValue {
|
||||
logging.Error(moduleName, "Mismatch opids length with numopids:", aurora.Cyan(len(opidsSplit)), "!=", aurora.Cyan(numOpidsValue))
|
||||
return empty
|
||||
|
||||
Reference in New Issue
Block a user