From a9500e576507446752b064226d33d141fdd5d569 Mon Sep 17 00:00:00 2001 From: Jumpy Squirrel Date: Sun, 18 Aug 2024 13:25:39 +0200 Subject: [PATCH 1/2] feat(#225): implement staff badge scripts --- .gitignore | 3 +- cmd/staffbadges/config.go | 67 ++++++ cmd/staffbadges/idpquery.go | 124 ++++++++++++ cmd/staffbadges/regsysquery.go | 360 +++++++++++++++++++++++++++++++++ cmd/staffbadges/staffbadges.go | 80 ++++++++ 5 files changed, 633 insertions(+), 1 deletion(-) create mode 100644 cmd/staffbadges/config.go create mode 100644 cmd/staffbadges/idpquery.go create mode 100644 cmd/staffbadges/regsysquery.go create mode 100644 cmd/staffbadges/staffbadges.go diff --git a/.gitignore b/.gitignore index 590879a..0df770d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ test/contract/*/logs/** attendee-service main -api-generator \ No newline at end of file +api-generator +**/config.yaml diff --git a/cmd/staffbadges/config.go b/cmd/staffbadges/config.go new file mode 100644 index 0000000..6767664 --- /dev/null +++ b/cmd/staffbadges/config.go @@ -0,0 +1,67 @@ +package main + +import ( + "errors" + "fmt" + "gopkg.in/yaml.v2" + "log" + "os" + "strings" +) + +type Config struct { + Token string `yaml:"idp_token"` // can take from AUTH cookie in regsys for a user that is staff + director + IDPUrl string `yaml:"idp_url"` // base url with no trailing / + Jwt string `yaml:"jwt"` // can take from admin auth in regsys + Auth string `yaml:"auth"` // can take from admin auth in regsys + RegsysUrl string `yaml:"regsys_url"` // base url including context including /attsrv, no trailing / + StaffGroupID string `yaml:"staff_group_id"` // look up in IDP in url + DirectorsGroupID string `yaml:"directors_group_id"` // look up in IDP in url +} + +func (c *Config) validate() error { + if c.Token == "" { + return errors.New("identity provider token empty") + } + if !strings.HasPrefix(c.IDPUrl, "https://") || strings.HasSuffix(c.IDPUrl, "/") { + return errors.New("invalid identity provider url") + } + if jwtParts := strings.Split(c.Jwt, "."); len(jwtParts) != 3 { + return errors.New("invalid jwt cookie, must contain full jwt with all 3 parts") + } + if c.Auth == "" { + return errors.New("invalid auth cookie") + } + if !strings.HasPrefix(c.RegsysUrl, "http") || strings.HasSuffix(c.RegsysUrl, "/") { + return errors.New("invalid regsys base url") + } + if c.StaffGroupID == "" { + return errors.New("staff group id missing") + } + if c.DirectorsGroupID == "" { + return errors.New("directors group id missing") + } + return nil +} + +func loadValidatedConfig() (Config, error) { + log.Println("reading configuration") + + result := Config{} + + yamlFile, err := os.ReadFile("cmd/staffbadges/config.yaml") + if err != nil { + return result, fmt.Errorf("failed to load config.yaml: %s", err.Error()) + } + + if err := yaml.UnmarshalStrict(yamlFile, &result); err != nil { + return result, fmt.Errorf("failed to parse config.yaml: %s", err.Error()) + } + + if err := result.validate(); err != nil { + return result, fmt.Errorf("failed to validate configuration: %s", err.Error()) + } + + log.Println("successfully read and validated configuration") + return result, nil +} diff --git a/cmd/staffbadges/idpquery.go b/cmd/staffbadges/idpquery.go new file mode 100644 index 0000000..3220d8b --- /dev/null +++ b/cmd/staffbadges/idpquery.go @@ -0,0 +1,124 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" +) + +type IDPLookupResult struct { + StaffIDs []string + DirectorIDs []string +} + +func lookupUserIDs(config Config) (IDPLookupResult, error) { + result := IDPLookupResult{} + + staffIDs, err := lookupUserIDsForGroup(config.IDPUrl, config.StaffGroupID, config.Token) + if err != nil { + return result, err + } + + directorIDs, err := lookupUserIDsForGroup(config.IDPUrl, config.DirectorsGroupID, config.Token) + if err != nil { + return result, err + } + + // remove staffIDs that are also directors + result.DirectorIDs = make([]string, 0) + for k, _ := range directorIDs { + if _, ok := staffIDs[k]; ok { + log.Printf("removing staff %s who is also director\n", k) + delete(staffIDs, k) + } + result.DirectorIDs = append(result.DirectorIDs, k) + } + + result.StaffIDs = make([]string, 0) + for k, _ := range staffIDs { + result.StaffIDs = append(result.StaffIDs, k) + } + + log.Printf("we now have %d directors and %d staff", len(result.DirectorIDs), len(result.StaffIDs)) + return result, nil +} + +// --- internals --- + +type groupUsersResponse struct { + Data []struct { + GroupId string `json:"group_id"` + UserId string `json:"user_id"` + Level string `json:"level"` + } `json:"data"` + Links struct { + First string `json:"first"` + Last interface{} `json:"last"` + Prev interface{} `json:"prev"` + Next string `json:"next"` // if null, no next page + } `json:"links"` + Meta struct { + CurrentPage int `json:"current_page"` + From int `json:"from"` + Path string `json:"path"` + PerPage int `json:"per_page"` + To int `json:"to"` + } `json:"meta"` +} + +func requestGroupUsers(url string, token string) (groupUsersResponse, error) { + result := groupUsersResponse{} + + request, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return result, fmt.Errorf("error creating request: %s", err.Error()) + } + request.Header.Add("Authorization", "Bearer "+token) + + response, err := http.DefaultClient.Do(request) + if err != nil { + return result, fmt.Errorf("error making request: %s", err.Error()) + } + + defer response.Body.Close() + body, err := io.ReadAll(response.Body) + if err != nil { + return result, fmt.Errorf("error reading body: %s", err.Error()) + } + + err = json.Unmarshal(body, &result) + if err != nil { + return result, fmt.Errorf("error parsing body: %s", err.Error()) + } + + return result, nil +} + +func lookupUserIDsForGroup(baseUrl string, grpId string, token string) (map[string]struct{}, error) { + log.Println("querying idp for group " + grpId) + + result := make(map[string]struct{}) + + url := fmt.Sprintf("%s/api/v1/groups/%s/users?page=1", baseUrl, grpId) + response, err := requestGroupUsers(url, token) + if err != nil { + return result, err + } + for _, entry := range response.Data { + result[entry.UserId] = struct{}{} + } + for response.Links.Next != "" { + response, err = requestGroupUsers(response.Links.Next, token) + if err != nil { + return result, err + } + for _, entry := range response.Data { + result[entry.UserId] = struct{}{} + } + } + + log.Printf("successfully read %d member ids in group %s\n", len(result), grpId) + return result, nil +} diff --git a/cmd/staffbadges/regsysquery.go b/cmd/staffbadges/regsysquery.go new file mode 100644 index 0000000..def84e3 --- /dev/null +++ b/cmd/staffbadges/regsysquery.go @@ -0,0 +1,360 @@ +package main + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "github.com/eurofurence/reg-attendee-service/internal/api/v1/admin" + "github.com/eurofurence/reg-attendee-service/internal/api/v1/attendee" + "github.com/eurofurence/reg-attendee-service/internal/api/v1/errorapi" + "github.com/eurofurence/reg-attendee-service/internal/api/v1/status" + "io" + "log" + "net/http" + "slices" + "strings" + "time" +) + +type BadgeLookupResult struct { + StaffBadges map[uint]status.Status + DirectorBadges map[uint]status.Status +} + +func lookupBadgeNumbers(idpLookupResult IDPLookupResult, config Config) (BadgeLookupResult, error) { + log.Println("querying regsys for badge numbers and status") + + log.Println("querying regsys for badge numbers and status for directors") + directors, err := lookupBadgeNumbersAndStatus(idpLookupResult.DirectorIDs, config) + if err != nil { + return BadgeLookupResult{}, err + } + + log.Println("querying regsys for badge numbers and status for staff") + staff, err := lookupBadgeNumbersAndStatus(idpLookupResult.StaffIDs, config) + if err != nil { + return BadgeLookupResult{}, err + } + + result := BadgeLookupResult{ + StaffBadges: staff, + DirectorBadges: directors, + } + + return result, nil +} + +type AttInfo struct { + Nickname string + Staff bool + Director bool +} + +func listAttendees(config Config) (map[uint]AttInfo, error) { + result := make(map[uint]AttInfo) + + log.Println("searching regsys for all attendees and their flags") + + reqBody := attendee.AttendeeSearchCriteria{ + MatchAny: []attendee.AttendeeSearchSingleCriterion{ + { + Status: []status.Status{ + status.New, + status.Approved, + status.PartiallyPaid, + status.Paid, + status.CheckedIn, + status.Waiting, + status.Cancelled, + }, + }, + }, + FillFields: []string{ + "id", + "nickname", + "flags", + }, + } + searchResult, err := findAttendees(reqBody, config.RegsysUrl, config.Auth, config.Jwt) + if err != nil { + return result, err + } + + if len(searchResult.Attendees) < 100 { + return result, errors.New("find attendees returned too few results, this cannot be") + } + + for _, att := range searchResult.Attendees { + if att.Nickname == nil || *att.Nickname == "" { + return result, fmt.Errorf("no nickname for reg id %d -- aborting", att.Id) + } + result[att.Id] = AttInfo{ + Nickname: *att.Nickname, + Staff: slices.Contains(att.FlagsList, "staff"), + Director: slices.Contains(att.FlagsList, "director"), + } + } + + return result, nil +} + +func addAdminFlag(badgeNo uint, flag string, config Config) error { + adminInfo, err := readAdminInfo(badgeNo, config.RegsysUrl, config.Auth, config.Jwt) + if err != nil { + return err + } + + if !strings.Contains(adminInfo.Flags, flag) { + if adminInfo.Flags == "" { + adminInfo.Flags = flag + } else { + adminInfo.Flags = adminInfo.Flags + "," + flag + } + } + + err = updateAdminInfo(badgeNo, config.RegsysUrl, adminInfo, config.Auth, config.Jwt) + if err != nil { + return err + } + + return nil +} + +// --- internals --- + +func lookupBadgeNumbersAndStatus(identities []string, config Config) (map[uint]status.Status, error) { + result := make(map[uint]status.Status) + + for _, ident := range identities { + badge, err := lookupBadgeNumber(ident, config.RegsysUrl, config.Auth, config.Jwt) + if err != nil { + return result, err + } + + if badge > 0 { + regStatus, err := lookupStatus(badge, config.RegsysUrl, config.Auth, config.Jwt) + if err != nil { + return result, err + } + + result[badge] = regStatus + } + } + + return result, nil +} + +func lookupStatus(badge uint, baseUrl string, token string, jwt string) (status.Status, error) { + url := fmt.Sprintf("%s/api/rest/v1/attendees/%d/status", baseUrl, badge) + httpStatus, body, err := regsysGetNoBody(url, token, jwt) + if err != nil { + return "", err + } + + if httpStatus == 404 { + log.Printf("no status for badge %d", badge) + return "", fmt.Errorf("failed to read status after finding attendee for badge %d", badge) + } + + if httpStatus != 200 { + result := errorapi.ErrorDto{} + err = json.Unmarshal(body, &result) + if err != nil { + return "", fmt.Errorf("error parsing body after non-200 error: " + err.Error()) + } + + return "", fmt.Errorf("status lookup failed with unexpected http status %d message %s requestid %s", httpStatus, result.Message, result.RequestId) + } + + result := status.StatusDto{} + err = json.Unmarshal(body, &result) + if err != nil { + return "", fmt.Errorf("error parsing body: " + err.Error()) + } + return result.Status, nil +} + +func lookupBadgeNumber(identity string, baseUrl string, token string, jwt string) (uint, error) { + url := fmt.Sprintf("%s/api/rest/v1/attendees/identity/%s", baseUrl, identity) + httpStatus, body, err := regsysGetNoBody(url, token, jwt) + if err != nil { + return 0, err + } + + if httpStatus == 404 { + log.Printf("no registration for identity %s", identity) + return 0, nil + } + + if httpStatus != 200 { + result := errorapi.ErrorDto{} + err = json.Unmarshal(body, &result) + if err != nil { + return 0, fmt.Errorf("error parsing body after non-200 error: " + err.Error()) + } + + return 0, fmt.Errorf("badge lookup failed with unexpected http status %d message %s requestid %s", httpStatus, result.Message, result.RequestId) + } + + result := attendee.AttendeeIdList{} + err = json.Unmarshal(body, &result) + if err != nil { + return 0, fmt.Errorf("error parsing body: " + err.Error()) + } + + if len(result.Ids) == 0 { + log.Printf("no registration for identity %s", identity) + return 0, nil + } + if len(result.Ids) > 1 { + log.Printf("WARNING, multiple registrations for identity %s, badge numbers %v -- skipping", identity, result.Ids) + return 0, nil + } + + return result.Ids[0], nil +} + +func findAttendees(reqBody attendee.AttendeeSearchCriteria, baseUrl string, token string, jwt string) (attendee.AttendeeSearchResultList, error) { + result := attendee.AttendeeSearchResultList{} + + reqBodyBytes, err := json.Marshal(reqBody) + if err != nil { + return result, err + } + + url := fmt.Sprintf("%s/api/rest/v1/attendees/find", baseUrl) + httpStatus, responseBody, err := regsysPost(url, reqBodyBytes, token, jwt) + if err != nil { + return result, err + } + + if httpStatus != 200 { + errResult := errorapi.ErrorDto{} + err = json.Unmarshal(responseBody, &errResult) + if err != nil { + return result, fmt.Errorf("error parsing body after non-200 error: " + err.Error()) + } + + return result, fmt.Errorf("badge lookup failed with unexpected http status %d message %s requestid %s", httpStatus, errResult.Message, errResult.RequestId) + } + + err = json.Unmarshal(responseBody, &result) + if err != nil { + return result, fmt.Errorf("error parsing body after 200: " + err.Error()) + } + + return result, nil +} + +func readAdminInfo(badge uint, baseUrl string, token string, jwt string) (admin.AdminInfoDto, error) { + result := admin.AdminInfoDto{} + + url := fmt.Sprintf("%s/api/rest/v1/attendees/%d/admin", baseUrl, badge) + httpStatus, body, err := regsysGetNoBody(url, token, jwt) + if err != nil { + return result, err + } + + if httpStatus == 404 { + log.Printf("no admin info for badge %d", badge) + return result, fmt.Errorf("failed to read admin info after finding attendee for badge %d", badge) + } + + if httpStatus != 200 { + errResult := errorapi.ErrorDto{} + err = json.Unmarshal(body, &errResult) + if err != nil { + return result, fmt.Errorf("error parsing body after non-200 error: " + err.Error()) + } + + return result, fmt.Errorf("admin info lookup failed with unexpected http status %d message %s requestid %s", httpStatus, errResult.Message, errResult.RequestId) + } + + err = json.Unmarshal(body, &result) + if err != nil { + return result, fmt.Errorf("error parsing body: " + err.Error()) + } + return result, nil +} + +func updateAdminInfo(badge uint, baseUrl string, info admin.AdminInfoDto, token string, jwt string) error { + url := fmt.Sprintf("%s/api/rest/v1/attendees/%d/admin", baseUrl, badge) + + requestBody, err := json.Marshal(&info) + if err != nil { + return fmt.Errorf("failed to json encode admin info: %s", err.Error()) + } + + httpStatus, responseBody, err := regsysPut(url, requestBody, token, jwt) + if err != nil { + return err + } + + if httpStatus != 204 { + errResult := errorapi.ErrorDto{} + err = json.Unmarshal(responseBody, &errResult) + if err != nil { + return fmt.Errorf("error parsing body after non-204 error: " + err.Error()) + } + + return fmt.Errorf("admin info lookup failed with unexpected http status %d message %s requestid %s", httpStatus, errResult.Message, errResult.RequestId) + } + + return nil +} + +// --- low level internal --- + +func regsysGetNoBody(url string, token string, jwt string) (int, []byte, error) { + return regsysRequest(http.MethodGet, url, nil, token, jwt) +} + +func regsysPost(url string, requestBody []byte, token string, jwt string) (int, []byte, error) { + return regsysRequest(http.MethodPost, url, bytes.NewReader(requestBody), token, jwt) +} + +func regsysPut(url string, requestBody []byte, token string, jwt string) (int, []byte, error) { + return regsysRequest(http.MethodPut, url, bytes.NewReader(requestBody), token, jwt) +} + +func regsysRequest(method string, url string, requestBody io.Reader, token string, jwt string) (int, []byte, error) { + request, err := http.NewRequest(method, url, requestBody) + if err != nil { + return 0, []byte{}, fmt.Errorf("error creating request: " + err.Error()) + } + request.AddCookie(&http.Cookie{ + Name: "JWT", + Value: jwt, + Domain: "localhost", + Expires: time.Now().Add(10 * time.Minute), + Path: "/", + Secure: true, + HttpOnly: true, + SameSite: http.SameSiteStrictMode, + }) + request.AddCookie(&http.Cookie{ + Name: "AUTH", + Value: token, + Domain: "localhost", + Expires: time.Now().Add(10 * time.Minute), + Path: "/", + Secure: true, + HttpOnly: true, + SameSite: http.SameSiteStrictMode, + }) + request.Header.Add("X-Admin-Request", "available") + + response, err := http.DefaultClient.Do(request) + if err != nil { + return 0, []byte{}, fmt.Errorf("error making request: " + err.Error()) + } + + defer response.Body.Close() + body, err := io.ReadAll(response.Body) + if err != nil { + return response.StatusCode, []byte{}, fmt.Errorf("error reading body: " + err.Error()) + } + + return response.StatusCode, body, nil +} diff --git a/cmd/staffbadges/staffbadges.go b/cmd/staffbadges/staffbadges.go new file mode 100644 index 0000000..0c0a59f --- /dev/null +++ b/cmd/staffbadges/staffbadges.go @@ -0,0 +1,80 @@ +package main + +import ( + "github.com/eurofurence/reg-attendee-service/internal/api/v1/status" + "log" + "os" +) + +var AutoApply = false + +// this program +// - reads config.yaml in this directory (not checked in) +// - queries IDP for identities of staff and directors group members +// - maps them to badge numbers via regsys API (TODO expose as admin endpoint) +// - queries regsys find endpoint for flags, status, nicks of all attendees +// - sets admin flags for staff / directors if missing +// - prints a message for non-attending flag holders and for flags that need to be cleared +func main() { + config, err := loadValidatedConfig() + if err != nil { + log.Println("fatal: " + err.Error()) + os.Exit(1) + } + + idpLookupResult, err := lookupUserIDs(config) + if err != nil { + log.Println("fatal: " + err.Error()) + os.Exit(2) + } + + badgeLookupResult, err := lookupBadgeNumbers(idpLookupResult, config) + if err != nil { + log.Println("fatal: " + err.Error()) + os.Exit(3) + } + + findResult, err := listAttendees(config) + if err != nil { + log.Println("fatal: " + err.Error()) + os.Exit(4) + } + + log.Printf("we now have %d directors and %d staff and %d attendees", len(badgeLookupResult.DirectorBadges), len(badgeLookupResult.StaffBadges), len(findResult)) + + for badgeNo, infos := range findResult { + regStatus, shouldBeStaff := badgeLookupResult.StaffBadges[badgeNo] + if regStatus == status.Cancelled { + shouldBeStaff = false + } + if shouldBeStaff && !infos.Staff { + log.Printf("id %d nick %s status %s should be staff", badgeNo, infos.Nickname, regStatus) + if AutoApply { + err := addAdminFlag(badgeNo, "staff", config) + if err != nil { + log.Printf("failed to add staff flag: %s", err.Error()) + os.Exit(5) + } + } + } else if !shouldBeStaff && infos.Staff { + log.Printf("id %d nick %s status %s should NOT be staff", badgeNo, infos.Nickname, regStatus) + } + + regStatus, shouldBeDirector := badgeLookupResult.DirectorBadges[badgeNo] + if regStatus == status.Cancelled { + shouldBeDirector = false + } + if shouldBeDirector && !infos.Director { + log.Printf("id %d nick %s status %s should be director", badgeNo, infos.Nickname, regStatus) + if AutoApply { + err := addAdminFlag(badgeNo, "director", config) + if err != nil { + log.Printf("failed to add director flag: %s", err.Error()) + os.Exit(5) + } + } + } else if !shouldBeDirector && infos.Director { + log.Printf("id %d nick %s status %s should NOT be director", badgeNo, infos.Nickname, regStatus) + } + } +} From e86907939d63b05d77ce1ff76ce7723c247dc590 Mon Sep 17 00:00:00 2001 From: Jumpy Squirrel Date: Sun, 18 Aug 2024 14:15:55 +0200 Subject: [PATCH 2/2] feat(#225): sort by id and improve log --- cmd/staffbadges/staffbadges.go | 43 +++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/cmd/staffbadges/staffbadges.go b/cmd/staffbadges/staffbadges.go index 0c0a59f..8575491 100644 --- a/cmd/staffbadges/staffbadges.go +++ b/cmd/staffbadges/staffbadges.go @@ -4,6 +4,7 @@ import ( "github.com/eurofurence/reg-attendee-service/internal/api/v1/status" "log" "os" + "sort" ) var AutoApply = false @@ -40,13 +41,38 @@ func main() { os.Exit(4) } + log.Printf("we now have %d directors and %d staff (still includes cancelled) and %d attendees", len(badgeLookupResult.DirectorBadges), len(badgeLookupResult.StaffBadges), len(findResult)) + + // sort by badgeNo + badgeNumbers := make([]uint, 0, len(findResult)) + for badgeNo, _ := range findResult { + badgeNumbers = append(badgeNumbers, badgeNo) + } + sort.Slice(badgeNumbers, func(i, j int) bool { return badgeNumbers[i] < badgeNumbers[j] }) + + // remove cancelled in separate pre-processing step + for _, badgeNo := range badgeNumbers { + infos := findResult[badgeNo] + + regStatus, present := badgeLookupResult.StaffBadges[badgeNo] + if present && regStatus == status.Cancelled { + log.Printf("cancelled staff id %d nick %s", badgeNo, infos.Nickname) + delete(badgeLookupResult.StaffBadges, badgeNo) + } + + regStatus, present = badgeLookupResult.DirectorBadges[badgeNo] + if present && regStatus == status.Cancelled { + log.Printf("cancelled director id %d nick %s", badgeNo, infos.Nickname) + delete(badgeLookupResult.DirectorBadges, badgeNo) + } + } + log.Printf("we now have %d directors and %d staff and %d attendees", len(badgeLookupResult.DirectorBadges), len(badgeLookupResult.StaffBadges), len(findResult)) - for badgeNo, infos := range findResult { + for _, badgeNo := range badgeNumbers { + infos := findResult[badgeNo] + regStatus, shouldBeStaff := badgeLookupResult.StaffBadges[badgeNo] - if regStatus == status.Cancelled { - shouldBeStaff = false - } if shouldBeStaff && !infos.Staff { log.Printf("id %d nick %s status %s should be staff", badgeNo, infos.Nickname, regStatus) if AutoApply { @@ -55,15 +81,13 @@ func main() { log.Printf("failed to add staff flag: %s", err.Error()) os.Exit(5) } + log.Printf("added staff flag for id %d nick %s", badgeNo, infos.Nickname) } } else if !shouldBeStaff && infos.Staff { - log.Printf("id %d nick %s status %s should NOT be staff", badgeNo, infos.Nickname, regStatus) + log.Printf("id %d nick %s status %s should NOT be staff - removal is manual", badgeNo, infos.Nickname, regStatus) } regStatus, shouldBeDirector := badgeLookupResult.DirectorBadges[badgeNo] - if regStatus == status.Cancelled { - shouldBeDirector = false - } if shouldBeDirector && !infos.Director { log.Printf("id %d nick %s status %s should be director", badgeNo, infos.Nickname, regStatus) if AutoApply { @@ -72,9 +96,10 @@ func main() { log.Printf("failed to add director flag: %s", err.Error()) os.Exit(5) } + log.Printf("added director flag for id %d nick %s", badgeNo, infos.Nickname) } } else if !shouldBeDirector && infos.Director { - log.Printf("id %d nick %s status %s should NOT be director", badgeNo, infos.Nickname, regStatus) + log.Printf("id %d nick %s status %s should NOT be director - removal is manual", badgeNo, infos.Nickname, regStatus) } } }