models/clan.go
// khan
// https://github.com/topfreegames/khan
//
// Licensed under the MIT license:
// http://www.opensource.org/licenses/mit-license
// Copyright © 2016 Top Free Games <backend@tfgco.com>
//go:generate easyjson -no_std_marshalers $GOFILE
package models
import (
"bytes"
"encoding/json"
"fmt"
"os"
"strings"
"github.com/spf13/viper"
"github.com/globalsign/mgo/bson"
"github.com/go-gorp/gorp"
workers "github.com/jrallison/go-workers"
"github.com/mailru/easyjson/jlexer"
"github.com/mailru/easyjson/jwriter"
"github.com/topfreegames/extensions/v9/mongo/interfaces"
"github.com/topfreegames/khan/es"
"github.com/topfreegames/khan/mongo"
"github.com/topfreegames/khan/queues"
"github.com/topfreegames/khan/util"
"github.com/uber-go/zap"
)
// ClanByName allows sorting clans by name
type ClanByName []*Clan
func (a ClanByName) Len() int { return len(a) }
func (a ClanByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a ClanByName) Less(i, j int) bool { return a[i].Name < a[j].Name }
// Clan identifies uniquely one clan in a given game
//easyjson:json
type Clan struct {
ID int64 `db:"id" json:"id" bson:"id"`
GameID string `db:"game_id" json:"gameId" bson:"gameId"`
PublicID string `db:"public_id" json:"publicId" bson:"publicId"`
Name string `db:"name" json:"name" bson:"name"`
OwnerID int64 `db:"owner_id" json:"ownerId" bson:"ownerId"`
MembershipCount int `db:"membership_count" json:"membershipCount" bson:"membershipCount"`
Metadata map[string]interface{} `db:"metadata" json:"metadata" bson:"metadata"`
AllowApplication bool `db:"allow_application" json:"allowApplication" bson:"allowApplication"`
AutoJoin bool `db:"auto_join" json:"autoJoin" bson:"autoJoin"`
CreatedAt int64 `db:"created_at" json:"createdAt" bson:"createdAt"`
UpdatedAt int64 `db:"updated_at" json:"updatedAt" bson:"updatedAt"`
DeletedAt int64 `db:"deleted_at" json:"deletedAt" bson:"deletedAt"`
}
// ClanWithNamePrefixes extends Clan with a field to help name indexation in MongoDB
type ClanWithNamePrefixes struct {
Clan
NamePrefixes []string `json:"namePrefixes"`
}
// Newest is the constant "newest"
const Newest string = "newest"
// Oldest is the constant "oldest"
const Oldest string = "oldest"
// IsValidOrder returns whether the input is equal to Newest or Oldest
func IsValidOrder(order string) bool {
return order == Newest || order == Oldest
}
func getSQLOrderFromSemanticOrder(semantic string) (sql string) {
if semantic == Newest {
sql = "DESC"
} else {
sql = "ASC"
}
return
}
// GetClanDetailsOptions holds options to change the output of GetClanDetails()
type GetClanDetailsOptions struct {
MaxPendingApplications int
MaxPendingInvites int
PendingApplicationsOrder string
PendingInvitesOrder string
}
// MaxPendingApplicationsKey is string constant
const MaxPendingApplicationsKey string = "getClanDetails.defaultOptions.maxPendingApplications"
// MaxPendingInvitesKey is string constant
const MaxPendingInvitesKey string = "getClanDetails.defaultOptions.maxPendingInvites"
// PendingApplicationsOrderKey is string constant
const PendingApplicationsOrderKey string = "getClanDetails.defaultOptions.pendingApplicationsOrder"
// PendingInvitesOrderKey is string constant
const PendingInvitesOrderKey string = "getClanDetails.defaultOptions.pendingInvitesOrder"
// NewDefaultGetClanDetailsOptions returns a new options structure with default values for GetClanDetails()
func NewDefaultGetClanDetailsOptions(config *viper.Viper) *GetClanDetailsOptions {
return &GetClanDetailsOptions{
MaxPendingApplications: config.GetInt(MaxPendingApplicationsKey),
MaxPendingInvites: config.GetInt(MaxPendingInvitesKey),
PendingApplicationsOrder: config.GetString(PendingApplicationsOrderKey),
PendingInvitesOrder: config.GetString(PendingInvitesOrderKey),
}
}
//ToJSON returns the clan as JSON
func (c *Clan) ToJSON() ([]byte, error) {
w := jwriter.Writer{}
c.MarshalEasyJSON(&w)
return w.BuildBytes()
}
//GetClanFromJSON unmarshals the clan from the specified JSON
func GetClanFromJSON(data []byte) (*Clan, error) {
clan := &Clan{}
lexer := jlexer.Lexer{Data: data}
clan.UnmarshalEasyJSON(&lexer)
return clan, lexer.Error()
}
//PreInsert populates fields before inserting a new clan
func (c *Clan) PreInsert(s gorp.SqlExecutor) error {
c.CreatedAt = util.NowMilli()
c.UpdatedAt = c.CreatedAt
return nil
}
//PostInsert indexes clan in ES after creation in PG
func (c *Clan) PostInsert(s gorp.SqlExecutor) error {
err := c.IndexClanIntoElasticSearch()
if err != nil {
return err
}
err = c.UpdateClanIntoMongoDB()
return err
}
//PreUpdate populates fields before updating a clan
func (c *Clan) PreUpdate(s gorp.SqlExecutor) error {
c.UpdatedAt = util.NowMilli()
return nil
}
//PostUpdate indexes clan in ES after update in PG
func (c *Clan) PostUpdate(s gorp.SqlExecutor) error {
err := c.UpdateClanIntoElasticSearch()
if err != nil {
return err
}
err = c.UpdateClanIntoMongoDB()
return err
}
//PostDelete deletes clan from elasticsearch after deleting from PG
func (c *Clan) PostDelete(s gorp.SqlExecutor) error {
err := c.DeleteClanFromElasticSearch()
if err != nil {
return err
}
err = c.DeleteClanFromMongoDB()
return err
}
func getNewLogger() zap.Logger {
ll := zap.InfoLevel
if os.Getenv("SKIP_ELASTIC_LOG") == "true" {
ll = zap.FatalLevel
}
return zap.New(
zap.NewJSONEncoder(), // drop timestamps in tests
ll,
)
}
//IndexClanIntoElasticSearch after operation in PG
func (c *Clan) IndexClanIntoElasticSearch() error {
es := es.GetConfiguredClient()
// TODO: fix it, boomforce is hardcoded for now
if es != nil && c.GameID == "boomforce" {
workers.Enqueue(queues.KhanESQueue, "Add", map[string]interface{}{
"index": es.GetIndexName(c.GameID),
"op": "index",
"clan": c,
"clanID": c.PublicID,
})
}
return nil
}
// NewClanWithNamePrefixes returns a new extended Clan object with name prefixes
func (c *Clan) NewClanWithNamePrefixes() *ClanWithNamePrefixes {
minPrefixLength := 4 // TODO: how to bring the app Viper config here?
caseSensitiveWords := strings.Fields(c.Name)
foundPrefixes := make(map[string]bool)
var prefixes []string
for _, caseSensitiveWord := range caseSensitiveWords {
word := strings.ToLower(caseSensitiveWord)
wordLen := len(word)
firstPrefixIdx := minPrefixLength
if firstPrefixIdx > wordLen {
firstPrefixIdx = wordLen
}
for i := firstPrefixIdx; i <= wordLen; i++ {
prefix := word[:i]
if !foundPrefixes[prefix] {
foundPrefixes[prefix] = true
prefixes = append(prefixes, prefix)
}
}
}
return &ClanWithNamePrefixes{
Clan: *c,
NamePrefixes: prefixes,
}
}
// UpdateClanIntoMongoDB after operation in PG
func (c *Clan) UpdateClanIntoMongoDB() error {
mongo := mongo.GetConfiguredMongoClient()
if mongo != nil {
workers.Enqueue(queues.KhanMongoQueue, "Add", map[string]interface{}{
"game": c.GameID,
"op": "update",
"clan": c.NewClanWithNamePrefixes(),
"clanID": c.PublicID,
})
}
return nil
}
//DeleteClanFromMongoDB after deletion in PG
func (c *Clan) DeleteClanFromMongoDB() error {
mongo := mongo.GetConfiguredMongoClient()
if mongo != nil {
workers.Enqueue(queues.KhanMongoQueue, "Add", map[string]interface{}{
"game": c.GameID,
"op": "delete",
"clan": c,
"clanID": c.PublicID,
})
}
return nil
}
//UpdateClanIntoElasticSearch after operation in PG
func (c *Clan) UpdateClanIntoElasticSearch() error {
es := es.GetConfiguredClient()
// TODO: fix it, boomforce is hardcoded for now
if es != nil && c.GameID == "boomforce" {
workers.Enqueue(queues.KhanESQueue, "Add", map[string]interface{}{
"index": es.GetIndexName(c.GameID),
"op": "update",
"clan": c,
"clanID": c.PublicID,
})
}
return nil
}
//DeleteClanFromElasticSearch after deletion in PG
func (c *Clan) DeleteClanFromElasticSearch() error {
es := es.GetConfiguredClient()
// TODO: fix it, boomforce is hardcoded for now
if es != nil && c.GameID == "boomforce" {
workers.Enqueue(queues.KhanESQueue, "Add", map[string]interface{}{
"index": es.GetIndexName(c.GameID),
"op": "delete",
"clan": c,
"clanID": c.PublicID,
})
}
return nil
}
func updateClanIntoES(db DB, id int64) error {
clan, err := GetClanByID(db, id)
if err != nil {
return err
}
if clan == nil {
return &ModelNotFoundError{"Clan", id}
}
return clan.UpdateClanIntoElasticSearch()
}
func updateClanIntoMongo(db DB, id int64) error {
clan, err := GetClanByID(db, id)
if err != nil {
return err
}
if clan == nil {
return &ModelNotFoundError{"Clan", id}
}
return clan.UpdateClanIntoMongoDB()
}
// Serialize returns a JSON with clan details
func (c *Clan) Serialize() map[string]interface{} {
return map[string]interface{}{
"gameID": c.GameID,
"publicID": c.PublicID,
"name": c.Name,
"membershipCount": c.MembershipCount,
"metadata": c.Metadata,
"allowApplication": c.AllowApplication,
"autoJoin": c.AutoJoin,
}
}
// UpdateClanMembershipCount updates the clan membership count
func UpdateClanMembershipCount(db DB, id int64) error {
query := `
UPDATE clans SET membership_count=membership.count+1
FROM (
SELECT COUNT(*) as count
FROM memberships m
WHERE
m.clan_id = $1 AND m.deleted_at = 0 AND m.approved = true AND
m.denied = false AND m.banned = false
) as membership
WHERE clans.id=$1
`
res, err := db.Exec(query, id)
if err != nil {
return err
}
rows, err := res.RowsAffected()
if err != nil {
return err
}
if rows != 1 {
return &ModelNotFoundError{"Clan", id}
}
err = updateClanIntoES(db, id)
if err != nil {
return err
}
err = updateClanIntoMongo(db, id)
if err != nil {
return err
}
return nil
}
// GetClanByID returns a clan by id
func GetClanByID(db DB, id int64) (*Clan, error) {
obj, err := db.Get(Clan{}, id)
if err != nil {
return nil, err
}
if obj == nil {
return nil, &ModelNotFoundError{"Clan", id}
}
return obj.(*Clan), nil
}
// GetClanByPublicID returns a clan by its public id
func GetClanByPublicID(db DB, gameID, publicID string) (*Clan, error) {
var clans []*Clan
_, err := db.Select(&clans, "SELECT * FROM clans WHERE game_id=$1 AND public_id=$2", gameID, publicID)
if err != nil {
return nil, err
}
if clans == nil || len(clans) < 1 {
return nil, &ModelNotFoundError{"Clan", publicID}
}
return clans[0], nil
}
// GetClanByShortPublicID returns a clan by the beginning of its public id
func GetClanByShortPublicID(db DB, gameID, publicID string) (*Clan, error) {
var clans []*Clan
// String for between don't need to be be same length as UUID
startRange, endRange := publicID+"-0000-0000-0000-000000000000", publicID+"-ffff-ffff-ffff-ffffffffffff"
_, err := db.Select(&clans, "SELECT * FROM clans WHERE game_id=$1 AND public_id BETWEEN $2 AND $3", gameID, startRange, endRange)
if err != nil {
return nil, err
}
if clans == nil || len(clans) < 1 {
return nil, &ModelNotFoundError{"Clan", publicID}
}
return clans[0], nil
}
func sliceDiff(slice1 []string, slice2 []string) []string {
var diff []string
// Loop two times, first to find slice1 strings not in slice2,
// second loop to find slice2 strings not in slice1
for i := 0; i < 2; i++ {
for _, s1 := range slice1 {
found := false
for _, s2 := range slice2 {
if s1 == s2 {
found = true
break
}
}
// String not found. We add it to return slice
if !found {
diff = append(diff, s1)
}
}
// Swap the slices, only if it was the first loop
if i == 0 {
slice1, slice2 = slice2, slice1
}
}
return diff
}
// GetClansByPublicIDs returns clans by their public ids
func GetClansByPublicIDs(db DB, gameID string, publicIDs []string) ([]Clan, error) {
var clans []Clan
queryPart := "SELECT * from clans WHERE game_id=$1 AND public_id=%s"
queryParts := []string{}
for i := 0; i < len(publicIDs); i++ {
paramIndex := fmt.Sprintf("$%d", (i + 2))
queryParts = append(queryParts, fmt.Sprintf(queryPart, paramIndex))
}
query := strings.Join(queryParts, " UNION ALL ")
params := []interface{}{gameID}
for _, publicID := range publicIDs {
params = append(params, publicID)
}
_, err := db.Select(&clans, query, params...)
if err != nil {
return nil, err
}
clanIDs := make([]string, len(clans))
for i, clan := range clans {
clanIDs[i] = clan.PublicID
}
diff := sliceDiff(publicIDs, clanIDs)
if len(diff) != 0 {
if len(clans) == 0 {
return clans, &CouldNotFindAllClansError{gameID, publicIDs}
}
return clans, &CouldNotFindAllClansError{gameID, diff}
}
return clans, nil
}
// GetClanByPublicIDAndOwnerPublicID returns a clan by its public id and the owner public id
func GetClanByPublicIDAndOwnerPublicID(db DB, gameID, publicID, ownerPublicID string) (*Clan, error) {
var clans []*Clan
var players []*Player
_, err := db.Select(&clans, "SELECT * FROM clans WHERE game_id=$1 AND public_id=$2", gameID, publicID)
if err != nil {
return nil, err
}
_, err = db.Select(&players, "SELECT * FROM players WHERE game_id=$1 AND public_id=$2", gameID, ownerPublicID)
if err != nil {
return nil, err
}
if clans == nil || len(clans) < 1 {
return nil, &ModelNotFoundError{"Clan", publicID}
}
if players == nil || len(players) < 1 {
return nil, &ModelNotFoundError{"Player", ownerPublicID}
}
if clans[0].OwnerID != players[0].ID {
return nil, &ForbiddenError{gameID, ownerPublicID, publicID}
}
return clans[0], nil
}
// CreateClan creates a new clan
func CreateClan(db DB, encryptionKey []byte, gameID, publicID, name, ownerPublicID string, metadata map[string]interface{}, allowApplication, autoJoin bool, maxClansPerPlayer int) (*Clan, error) {
player, err := GetPlayerByPublicID(db, encryptionKey, gameID, ownerPublicID)
if err != nil {
return nil, err
}
if player.MembershipCount+player.OwnershipCount >= maxClansPerPlayer {
return nil, &PlayerReachedMaxClansError{ownerPublicID}
}
clan := &Clan{
GameID: gameID,
PublicID: publicID,
Name: name,
OwnerID: player.ID,
Metadata: metadata,
AllowApplication: allowApplication,
AutoJoin: autoJoin,
MembershipCount: 1,
}
err = db.Insert(clan)
if err != nil {
return nil, err
}
err = UpdatePlayerOwnershipCount(db, player.ID)
if err != nil {
return nil, err
}
return clan, nil
}
// LeaveClan allows the clan owner to leave the clan and transfer the clan ownership to the next player in line
func LeaveClan(db DB, encryptionKey []byte, gameID, publicID string) (*Clan, *Player, *Player, error) {
clan, err := GetClanByPublicID(db, gameID, publicID)
if err != nil {
return nil, nil, nil, err
}
oldOwnerID := clan.OwnerID
oldOwner, err := GetPlayerByID(db, encryptionKey, oldOwnerID)
if err != nil {
return nil, nil, nil, err
}
newOwnerMembership, err := GetOldestMemberWithHighestLevel(db, gameID, publicID)
if err != nil {
noMembersError := &ClanHasNoMembersError{publicID}
if err.Error() == noMembersError.Error() {
// Clan has no approved members, delete all members and clan
_, err = db.Exec("DELETE FROM memberships where clan_id=$1", clan.ID)
if err != nil {
return nil, nil, nil, err
}
_, err = db.Delete(clan)
if err != nil {
return nil, nil, nil, err
}
err = UpdatePlayerOwnershipCount(db, oldOwnerID)
if err != nil {
return nil, nil, nil, err
}
return clan, oldOwner, nil, nil
}
return nil, nil, nil, err
}
newOwner, err := GetPlayerByID(db, encryptionKey, newOwnerMembership.PlayerID)
if err != nil {
return nil, nil, nil, err
}
clan.OwnerID = newOwner.ID
_, err = db.Update(clan)
if err != nil {
return nil, nil, nil, err
}
_, err = deleteMembershipHelper(db, newOwnerMembership, newOwnerMembership.PlayerID)
if err != nil {
return nil, nil, nil, err
}
err = UpdatePlayerOwnershipCount(db, oldOwnerID)
if err != nil {
return nil, nil, nil, err
}
err = UpdatePlayerOwnershipCount(db, newOwner.ID)
if err != nil {
return nil, nil, nil, err
}
newOwner.MembershipCount--
newOwner.OwnershipCount++
// We update it here on purpose, to avoid making another useless query
// The actual update to reduce membership count is made in the deleteMembershipHelper
clan.MembershipCount--
return clan, oldOwner, newOwner, nil
}
// TransferClanOwnership allows the clan owner to transfer the clan ownership to a clan member
func TransferClanOwnership(db DB, encryptionKey []byte, gameID, clanPublicID, playerPublicID string, levels map[string]interface{}, maxLevel int) (*Clan, *Player, *Player, error) {
clan, err := GetClanByPublicID(db, gameID, clanPublicID)
if err != nil {
return nil, nil, nil, err
}
newOwnerMembership, err := GetValidMembershipByClanAndPlayerPublicID(db, gameID, clanPublicID, playerPublicID)
if err != nil {
return nil, nil, nil, err
}
oldOwnerID := clan.OwnerID
clan.OwnerID = newOwnerMembership.PlayerID
_, err = db.Update(clan)
if err != nil {
return nil, nil, nil, err
}
level := getClanLevelByLevelInt(maxLevel, levels)
if level == "" {
return nil, nil, nil, &InvalidLevelForGameError{gameID, level}
}
oldOwnerMembership, err := GetDeletedMembershipByClanAndPlayerID(db, gameID, clan.ID, oldOwnerID)
if err != nil {
err = db.Insert(&Membership{
GameID: gameID,
ClanID: clan.ID,
PlayerID: oldOwnerID,
RequestorID: oldOwnerID,
Level: level,
Approved: true,
Denied: false,
Banned: false,
CreatedAt: clan.CreatedAt,
UpdatedAt: util.NowMilli(),
})
if err != nil {
return nil, nil, nil, err
}
} else {
oldOwnerMembership.Approved = true
oldOwnerMembership.Denied = false
oldOwnerMembership.Banned = false
oldOwnerMembership.DeletedBy = 0
oldOwnerMembership.DeletedAt = 0
oldOwnerMembership.Level = level
oldOwnerMembership.RequestorID = oldOwnerID
_, err = db.Update(oldOwnerMembership)
if err != nil {
return nil, nil, nil, err
}
}
_, err = deleteMembershipHelper(db, newOwnerMembership, newOwnerMembership.PlayerID)
if err != nil {
return nil, nil, nil, err
}
err = UpdatePlayerOwnershipCount(db, newOwnerMembership.PlayerID)
if err != nil {
return nil, nil, nil, err
}
newOwner, err := GetPlayerByID(db, encryptionKey, newOwnerMembership.PlayerID)
if err != nil {
return nil, nil, nil, err
}
err = UpdatePlayerOwnershipCount(db, oldOwnerID)
if err != nil {
return nil, nil, nil, err
}
err = UpdatePlayerMembershipCount(db, oldOwnerID)
if err != nil {
return nil, nil, nil, err
}
oldOwner, err := GetPlayerByID(db, encryptionKey, oldOwnerID)
if err != nil {
return nil, nil, nil, err
}
return clan, oldOwner, newOwner, nil
}
func getClanLevelByLevelInt(levelInt int, levels map[string]interface{}) string {
for k, v := range levels {
switch v.(type) {
case float64:
if int(v.(float64)) == levelInt {
return k
}
case int:
if v.(int) == levelInt {
return k
}
}
}
return ""
}
// UpdateClan updates an existing clan
func UpdateClan(db DB, gameID, publicID, name, ownerPublicID string, metadata map[string]interface{}, allowApplication, autoJoin bool) (*Clan, error) {
clan, err := GetClanByPublicIDAndOwnerPublicID(db, gameID, publicID, ownerPublicID)
if err != nil {
return nil, err
}
clan.Name = name
clan.Metadata = metadata
clan.AllowApplication = allowApplication
clan.AutoJoin = autoJoin
metadataBuffer := bytes.NewBuffer([]byte{})
enc := json.NewEncoder(metadataBuffer)
err = enc.Encode(metadata)
if err != nil {
return nil, err
}
query := `
UPDATE clans SET name=$1, metadata=$2, allow_application=$3, auto_join=$4
WHERE clans.id=$5
`
_, err = db.Exec(query, name, metadataBuffer.String(), allowApplication, autoJoin, clan.ID)
if err != nil {
return nil, err
}
// since this function should update only the 4 fields above,
// we cannot use db.Update(clan), so clan.PostUpdate() should
// be called explicitly
gorpSQLExecutor, ok := db.(gorp.SqlExecutor)
if !ok {
return nil, &InvalidCastToGorpSQLExecutorError{}
}
err = clan.PostUpdate(gorpSQLExecutor)
if err != nil {
return nil, err
}
return clan, nil
}
// GetAllClans returns a list of all clans in a given game
func GetAllClans(db DB, gameID string) ([]Clan, error) {
if gameID == "" {
return nil, &EmptyGameIDError{"Clan"}
}
var clans []Clan
_, err := db.Select(&clans, "select * from clans where game_id=$1 order by name", gameID)
if err != nil {
return nil, err
}
return clans, nil
}
// GetClanMembers gets only the ids of then clan members
func GetClanMembers(db DB, gameID, publicID string) (map[string]interface{}, error) {
clan, err := GetClanByPublicID(db, gameID, publicID)
if err != nil {
return nil, err
}
query := `
SELECT public_id
FROM players
WHERE id IN (
SELECT player_id
FROM memberships im
WHERE im.clan_id=$1
AND im.deleted_at=0
AND (im.approved=true)
UNION
SELECT owner_id
FROM clans c
WHERE c.id=$1
)
`
var res []string
_, err = db.Select(&res, query, clan.ID)
if err != nil {
return nil, err
}
return map[string]interface{}{
"members": res,
}, nil
}
// GetClanDetails returns all details for a given clan by its game id and public id
func GetClanDetails(db DB, encryptionKey []byte, gameID string, clan *Clan, maxClansPerPlayer int, options *GetClanDetailsOptions) (map[string]interface{}, error) {
query := fmt.Sprintf(`
WITH memberships_pending AS (
SELECT *
FROM memberships im
WHERE im.clan_id=$2 AND im.deleted_at=0 AND im.approved=false AND im.denied=false AND im.banned=false
)
SELECT
c.game_id GameID,
c.public_id ClanPublicID, c.name ClanName, c.metadata ClanMetadata,
c.allow_application ClanAllowApplication, c.auto_join ClanAutoJoin,
c.membership_count ClanMembershipCount,
m.membership_level MembershipLevel, m.approved MembershipApproved, m.denied MembershipDenied,
m.banned MembershipBanned, m.message MembershipMessage,
m.created_at MembershipCreatedAt, m.updated_at MembershipUpdatedAt,
m.approved_at MembershipApprovedAt, m.denied_at MembershipDeniedAt,
o.public_id OwnerPublicID, o.name OwnerName, o.metadata OwnerMetadata,
p.public_id PlayerPublicID, p.name PlayerName, p.metadata DBPlayerMetadata,
r.public_id RequestorPublicID, r.name RequestorName,
a.public_id ApproverPublicID, a.name ApproverName,
y.name DenierName, y.public_id DenierPublicID,
Coalesce(p.membership_count, 0) MembershipCount,
Coalesce(p.ownership_count, 0) OwnershipCount
FROM clans c
INNER JOIN players o ON c.owner_id=o.id
LEFT OUTER JOIN (
(
SELECT *
FROM memberships_pending im
WHERE im.requestor_id=im.player_id
ORDER BY im.id %s
LIMIT $3
)
UNION ALL (
SELECT *
FROM memberships_pending im
WHERE im.requestor_id>im.player_id OR im.requestor_id<im.player_id
ORDER BY im.id %s
LIMIT $4
)
UNION ALL (
SELECT *
FROM memberships im
WHERE im.clan_id=$2 AND im.deleted_at=0 AND (im.approved=true OR im.denied=true OR im.banned=true)
)
) m ON m.clan_id=c.id
LEFT OUTER JOIN players r ON m.requestor_id=r.id
LEFT OUTER JOIN players a ON m.approver_id=a.id
LEFT OUTER JOIN players p ON m.player_id=p.id
LEFT OUTER JOIN players y ON m.denier_id=y.id
WHERE
c.game_id=$1 AND c.id=$2
`, getSQLOrderFromSemanticOrder(options.PendingApplicationsOrder), getSQLOrderFromSemanticOrder(options.PendingInvitesOrder))
var details []clanDetailsDAO
_, err := db.Select(&details, query, gameID, clan.ID, options.MaxPendingApplications, options.MaxPendingInvites)
if err != nil {
return nil, err
}
if len(details) == 0 {
return nil, &ModelNotFoundError{"Clan", clan.PublicID}
}
result := make(map[string]interface{})
result["publicID"] = details[0].ClanPublicID
result["name"] = details[0].ClanName
result["metadata"] = details[0].ClanMetadata
result["allowApplication"] = details[0].ClanAllowApplication
result["autoJoin"] = details[0].ClanAutoJoin
result["membershipCount"] = details[0].ClanMembershipCount
owner := &Player{
PublicID: details[0].OwnerPublicID,
Name: details[0].OwnerName,
Metadata: details[0].OwnerMetadata,
}
result["owner"] = owner.SerializeClanParticipant(encryptionKey)
// First row player public id is not null, meaning we found players!
if details[0].PlayerPublicID.Valid {
result["roster"] = make([]map[string]interface{}, 0)
result["memberships"] = map[string]interface{}{
"pendingInvites": []map[string]interface{}{},
"pendingApplications": []map[string]interface{}{},
"banned": []map[string]interface{}{},
"denied": []map[string]interface{}{},
}
memberships := result["memberships"].(map[string]interface{})
for _, member := range details {
approved := nullOrBool(member.MembershipApproved)
denied := nullOrBool(member.MembershipDenied)
banned := nullOrBool(member.MembershipBanned)
pending := !approved && !denied && !banned
switch {
case pending:
memberData := member.Serialize(encryptionKey, true)
if member.MembershipCount+member.OwnershipCount < maxClansPerPlayer {
if member.PlayerPublicID == member.RequestorPublicID {
memberData["message"] = nullOrString(member.MembershipMessage)
memberships["pendingApplications"] = append(memberships["pendingApplications"].([]map[string]interface{}), memberData)
} else {
memberships["pendingInvites"] = append(memberships["pendingInvites"].([]map[string]interface{}), memberData)
}
}
case banned:
memberData := member.Serialize(encryptionKey, false)
memberships["banned"] = append(memberships["banned"].([]map[string]interface{}), memberData)
case denied:
memberData := member.Serialize(encryptionKey, false)
memberships["denied"] = append(memberships["denied"].([]map[string]interface{}), memberData)
case approved:
memberData := member.Serialize(encryptionKey, true)
result["roster"] = append(result["roster"].([]map[string]interface{}), memberData)
}
}
} else {
//Otherwise return empty array of object
result["roster"] = []map[string]interface{}{}
result["memberships"] = map[string]interface{}{
"pendingApplications": []map[string]interface{}{},
"pendingInvites": []map[string]interface{}{},
"banned": []map[string]interface{}{},
"denied": []map[string]interface{}{},
}
}
return result, nil
}
// GetClanSummary returns a summary of the clan details for a given clan by its game id and public id
func GetClanSummary(db DB, gameID, publicID string) (map[string]interface{}, error) {
clan, err := GetClanByPublicID(db, gameID, publicID)
if err != nil {
return nil, err
}
result := make(map[string]interface{})
result["membershipCount"] = clan.MembershipCount
result["publicID"] = clan.PublicID
result["metadata"] = clan.Metadata
result["name"] = clan.Name
result["allowApplication"] = clan.AllowApplication
result["autoJoin"] = clan.AutoJoin
return result, nil
}
// GetClansSummaries returns a summary of the clans details for a given list of clans by their game
// id and public ids
func GetClansSummaries(db DB, gameID string, publicIDs []string) ([]map[string]interface{}, error) {
clans, err := GetClansByPublicIDs(db, gameID, publicIDs)
resultClans := make([]map[string]interface{}, len(clans))
for i := range clans {
result := map[string]interface{}{
"membershipCount": clans[i].MembershipCount,
"publicID": clans[i].PublicID,
"metadata": clans[i].Metadata,
"name": clans[i].Name,
"allowApplication": clans[i].AllowApplication,
"autoJoin": clans[i].AutoJoin,
}
resultClans[i] = result
}
if err != nil {
return resultClans, err
}
return resultClans, nil
}
func min(x, y int) int {
if x < y {
return x
}
return y
}
func searchClanByID(db DB, gameID, publicID string) []Clan {
var clan *Clan
var err error
if clan, err = GetClanByPublicID(db, gameID, publicID); err != nil {
shortPublicID := publicID[:min(8, len(publicID))]
if len(shortPublicID) < 8 {
return nil
}
if clan, err = GetClanByShortPublicID(db, gameID, shortPublicID); err != nil {
return nil
}
}
return []Clan{*clan}
}
// SearchClan returns a list of clans for a given term (by name or publicID)
func SearchClan(
db DB, mongo interfaces.MongoDB, gameID, term string, pageSize int64,
) ([]Clan, error) {
if term == "" {
return nil, &EmptySearchTermError{}
}
clans := searchClanByID(db, gameID, term)
if clans != nil {
return clans, nil
}
projection := bson.M{"textSearchScore": bson.M{"$meta": "textScore"}}
cmd := bson.D{
{Name: "find", Value: fmt.Sprintf("clans_%s", gameID)},
{Name: "filter", Value: bson.M{"$text": bson.M{"$search": term}}},
{Name: "projection", Value: projection},
{Name: "sort", Value: projection},
{Name: "limit", Value: pageSize},
{Name: "batchSize", Value: pageSize},
{Name: "singleBatch", Value: true},
}
var res struct {
OK int `bson:"ok"`
WaitedMS int `bson:"waitedMS"`
Cursor struct {
ID interface{} `bson:"id"`
NS string `bson:"ns"`
FirstBatch []bson.Raw `bson:"firstBatch"`
} `bson:"cursor"`
}
if err := mongo.Run(cmd, &res); err != nil {
return []Clan{}, err
}
clans = make([]Clan, len(res.Cursor.FirstBatch))
for i, raw := range res.Cursor.FirstBatch {
if err := raw.Unmarshal(&clans[i]); err != nil {
return []Clan{}, err
}
}
return clans, nil
}
// GetClanAndOwnerByPublicID returns the clan as well as the owner of a clan by clan's public id
func GetClanAndOwnerByPublicID(db DB, encryptionKey []byte, gameID, publicID string) (*Clan, *Player, error) {
clan, err := GetClanByPublicID(db, gameID, publicID)
if err != nil {
return nil, nil, err
}
newOwner, err := GetPlayerByID(db, encryptionKey, clan.OwnerID)
if err != nil {
return nil, nil, err
}
return clan, newOwner, nil
}