topfreegames/khan

View on GitHub
models/player.go

Summary

Maintainability
C
1 day
Test Coverage
// 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>

package models

import (
    "encoding/json"
    "fmt"

    "github.com/topfreegames/khan/util"
    "github.com/uber-go/zap"

    "github.com/go-gorp/gorp"
    egorp "github.com/topfreegames/extensions/v9/gorp/interfaces"
)

// Player identifies uniquely one player in a given game
type Player struct {
    ID              int64                  `db:"id"`
    GameID          string                 `db:"game_id"`
    PublicID        string                 `db:"public_id"`
    Name            string                 `db:"name"`
    Metadata        map[string]interface{} `db:"metadata"`
    MembershipCount int                    `db:"membership_count"`
    OwnershipCount  int                    `db:"ownership_count"`
    CreatedAt       int64                  `db:"created_at"`
    UpdatedAt       int64                  `db:"updated_at"`
}

// PreInsert populates fields before inserting a new player
func (p *Player) PreInsert(s gorp.SqlExecutor) error {
    p.CreatedAt = util.NowMilli()
    p.UpdatedAt = p.CreatedAt
    return nil
}

// PreUpdate populates fields before updating a player
func (p *Player) PreUpdate(s gorp.SqlExecutor) error {
    p.UpdatedAt = util.NowMilli()
    return nil
}

//Serialize the player information to JSON
func (p *Player) Serialize(encryptionKey []byte) map[string]interface{} {
    return decryptPlayerName(map[string]interface{}{
        "gameID":          p.GameID,
        "publicID":        p.PublicID,
        "name":            p.Name,
        "metadata":        p.Metadata,
        "membershipCount": p.MembershipCount,
        "ownershipCount":  p.OwnershipCount,
    }, encryptionKey)
}

//SerializeClanParticipant the player information to JSON
func (p *Player) SerializeClanParticipant(encryptionKey []byte) map[string]interface{} {
    return decryptPlayerName(map[string]interface{}{
        "publicID": p.PublicID,
        "name":     p.Name,
        "metadata": p.Metadata,
    }, encryptionKey)
}

//SerializeClanActor the player information to JSON
func (p *Player) SerializeClanActor(encryptionKey []byte) map[string]interface{} {
    return decryptPlayerName(map[string]interface{}{
        "publicID": p.PublicID,
        "name":     p.Name,
    }, encryptionKey)
}

//SerializeWithLevel serialize player fields: PublicID and Name with MembershipCount passed by param
func (p *Player) SerializeWithLevel(encryptionKey []byte, level string) map[string]interface{} {
    return decryptPlayerName(map[string]interface{}{
        "publicID": p.PublicID,
        "name":     p.Name,
        "level":    level,
    }, encryptionKey)
}

// UpdatePlayerMembershipCount updates the player membership count
func UpdatePlayerMembershipCount(db DB, id int64) error {
    query := `
    UPDATE players SET membership_count=membership.count
    FROM (
        SELECT COUNT(*) as count
        FROM memberships m
        WHERE
            m.player_id = $1 AND m.deleted_at = 0 AND m.approved = true AND
            m.denied = false AND m.banned = false
    ) as membership
    WHERE players.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{"Player", id}
    }
    return nil
}

// UpdatePlayerOwnershipCount updates the player ownership count
func UpdatePlayerOwnershipCount(db DB, id int64) error {
    query := `
    UPDATE players SET ownership_count=ownership.count
    FROM (
        SELECT COUNT(*) as count
        FROM clans c
        WHERE c.owner_id = $1
    ) as ownership
    WHERE players.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{"Player", id}
    }
    return nil
}

// GetPlayerByID returns a player by id
func GetPlayerByID(db DB, encryptionKey []byte, id int64) (*Player, error) {
    playerInterface, err := db.Get(Player{}, id)
    if err != nil {
        return nil, err
    }
    if playerInterface == nil {
        return nil, &ModelNotFoundError{"Player", id}
    }

    player := playerInterface.(*Player)
    name, err := util.DecryptData(player.Name, encryptionKey)
    if err != nil {
        return player, nil
    }

    player.Name = name
    return player, nil

}

// GetPlayerByPublicID returns a player by their public id
func GetPlayerByPublicID(db DB, encryptionKey []byte, gameID string, publicID string) (*Player, error) {
    var players []*Player
    _, err := db.Select(&players, "SELECT * FROM players WHERE game_id=$1 AND public_id=$2", gameID, publicID)
    if err != nil {
        return nil, err
    }
    if players == nil || len(players) < 1 {
        return nil, &ModelNotFoundError{"Player", publicID}
    }

    player := players[0]
    name, err := util.DecryptData(player.Name, encryptionKey)
    if err != nil {
        return player, nil
    }

    player.Name = name
    return player, nil
}

// CreatePlayer creates a new player
func CreatePlayer(db DB, logger zap.Logger, encryptionKey []byte, gameID, publicID, name string, metadata map[string]interface{}) (*Player, error) {
    markAsEncrypted := true
    encryptedName, err := util.EncryptData(name, encryptionKey)
    if err != nil {
        encryptedName = name
        markAsEncrypted = false
    }

    player := &Player{
        GameID:   gameID,
        PublicID: publicID,
        Name:     encryptedName,
        Metadata: metadata,
    }
    err = db.Insert(player)
    if err != nil {
        return nil, err
    }

    if markAsEncrypted {
        err = db.Insert(&EncryptedPlayer{PlayerID: player.ID})
        if err != nil {
            logger.Error("Error on insert EncryptedPlayer", zap.Error(err))
        }
    }

    return GetPlayerByID(db, encryptionKey, player.ID)
}

// UpdatePlayer updates an existing player
func UpdatePlayer(db DB, logger zap.Logger, encryptionKey []byte, gameID, publicID, name string, metadata map[string]interface{}) (*Player, error) {
    markAsEncrypted := true
    encryptedName, err := util.EncryptData(name, encryptionKey)
    if err != nil {
        encryptedName = name
        markAsEncrypted = false
    }

    metadataJSON, err := json.Marshal(metadata)
    if err != nil {
        return nil, err
    }

    query := `INSERT INTO players(game_id, public_id, name, metadata, created_at, updated_at)
                        VALUES($1, $2, $3, $4, $5, $5) ON CONFLICT (game_id, public_id)
                        DO UPDATE set name=$3, metadata=$4, updated_at=$5
                        WHERE players.game_id=$1 and players.public_id=$2
                        RETURNING id`

    var lastID int64
    lastID, err = db.SelectInt(query,
        gameID, publicID, encryptedName, metadataJSON, util.NowMilli())
    if err != nil {
        return nil, err
    }

    if markAsEncrypted {
        queryEncrypt := `INSERT INTO encrypted_players (player_id) VALUES ($1) ON CONFLICT DO NOTHING`
        _, err = db.Exec(queryEncrypt, lastID)
        if err != nil {
            logger.Error("Error on insert EncryptedPlayer", zap.Error(err))
        }
    }

    return GetPlayerByID(db, encryptionKey, lastID)
}

// GetPlayerOwnershipDetails returns detailed information about a player owned clans
func GetPlayerOwnershipDetails(db DB, gameID, publicID string) (map[string]interface{}, error) {
    query := `
    SELECT c.*
    FROM players p
    INNER JOIN clans c ON c.owner_id=p.id
    WHERE p.game_id=$1 AND p.public_id=$2 
    `

    var clans []Clan
    _, err := db.Select(&clans, query, gameID, publicID)
    if err != nil {
        return nil, err
    }

    result := make(map[string]interface{})
    memberships := []map[string]interface{}{}
    owned := []map[string]interface{}{}

    if len(clans) > 0 {
        clanFromDetail := func(clan Clan) map[string]interface{} {
            return map[string]interface{}{
                "publicID": clan.PublicID,
                "name":     clan.Name,
            }
        }

        membershipFromClan := func(clan Clan) map[string]interface{} {
            return map[string]interface{}{
                "level":    "owner",
                "approved": true,
                "denied":   false,
                "banned":   false,
                "clan": map[string]interface{}{
                    "metadata":        clan.Metadata,
                    "name":            clan.Name,
                    "publicID":        clan.PublicID,
                    "membershipCount": clan.MembershipCount,
                },
                "createdAt":  clan.CreatedAt,
                "updatedAt":  clan.CreatedAt,
                "approvedAt": clan.CreatedAt,
                "deletedAt":  0,
            }
        }

        for _, clan := range clans {
            m := membershipFromClan(clan)
            memberships = append(memberships, m)

            clanDetail := clanFromDetail(clan)
            owned = append(owned, clanDetail)
        }
    }

    result["memberships"] = memberships
    result["clans"] = owned
    return result, nil
}

// GetPlayerDetails returns detailed information about a player and their memberships
func GetPlayerDetails(db DB, encryptionKey []byte, gameID, publicID string) (map[string]interface{}, error) {
    result, err := getPlayerMembershipDetails(db, encryptionKey, gameID, publicID)
    if err != nil {
        return nil, err
    }
    ownerships, err := GetPlayerOwnershipDetails(db, gameID, publicID)
    if err != nil {
        return nil, err
    }
    result["clans"].(map[string]interface{})["owned"] = ownerships["clans"]
    result["memberships"] = append(result["memberships"].([]map[string]interface{}), ownerships["memberships"].([]map[string]interface{})...)
    return result, nil
}

// GetPlayersToEncrypt get players that have plain text name
func GetPlayersToEncrypt(db DB, encryptionKey []byte, amount int) ([]*Player, error) {
    query := `SELECT p.*
    FROM players p
        LEFT JOIN encrypted_players ep ON p.id = ep.player_id
    WHERE ep.player_id IS NULL
    LIMIT $1`

    var players []*Player
    _, err := db.Select(&players, query, amount)
    if err != nil {
        return nil, err
    }

    return players, nil
}

// ApplySecurityChanges encrypt and update player
func ApplySecurityChanges(db egorp.Database, encryptionKey []byte, players []*Player) error {

    trx, err := db.Begin()
    if err != nil {
        return err
    }

    for _, player := range players {
        encryptedName, err := util.EncryptData(player.Name, encryptionKey)
        if err != nil {
            return err
        }

        player.Name = encryptedName

        if err != nil {
            err = trx.Rollback()
            return err
        }

        _, err = trx.Update(player)
        if err != nil {
            err = trx.Rollback()
            return err
        }

        err = trx.Insert(&EncryptedPlayer{PlayerID: player.ID})
        if err != nil {
            err = trx.Rollback()
            return err
        }
    }

    err = trx.Commit()
    if err != nil {
        return err
    }

    return nil
}

// getPlayerMembershipDetails returns detailed information about a player and their memberships
func getPlayerMembershipDetails(db DB, encryptionKey []byte, gameID, publicID string) (map[string]interface{}, error) {
    player, err := GetPlayerByPublicID(db, encryptionKey, gameID, publicID)
    if err != nil {
        return nil, err
    }
    //TODO: Include this again once membership level is in the membership table
    //w.membership_level RequestorMembershipLevel,
    query := `
    SELECT
        p.id PlayerID, p.name PlayerName, p.metadata PlayerMetadata, p.public_id PlayerPublicID,
        p.created_at PlayerCreatedAt, p.updated_at PlayerUpdatedAt,
        m.membership_level MembershipLevel,
        m.approved MembershipApproved, m.denied MembershipDenied, m.banned MembershipBanned,
        c.public_id ClanPublicID, c.name ClanName, c.metadata DBClanMetadata, c.owner_id ClanOwnerID,
        c.membership_count ClanMembershipCount,
        NULL RequestorMembershipLevel,
        r.name RequestorName, r.public_id RequestorPublicID, r.metadata DBRequestorMetadata,
        a.name ApproverName, a.public_id ApproverPublicID, a.metadata DBApproverMetadata,
        y.name DenierName, y.public_id DenierPublicID, y.metadata DBDenierMetadata,
        m.created_at MembershipCreatedAt,
        m.updated_at MembershipUpdatedAt,
        m.deleted_at MembershipDeletedAt,
        m.approved_at MembershipApprovedAt, m.denied_at MembershipDeniedAt,
        m.message MembershipMessage,
        d.name DeletedByName, d.public_id DeletedByPublicID
    FROM players p
        LEFT OUTER JOIN (
            SELECT * FROM memberships im WHERE im.player_id=$2 AND (im.approved=true OR im.denied=true OR im.banned=true)
            UNION
            (SELECT * FROM memberships im WHERE im.player_id=$2 AND im.deleted_at=0 AND im.approved=false AND im.denied=false AND im.banned=false ORDER BY updated_at DESC LIMIT $3)
        ) m ON p.id = m.player_id
        LEFT OUTER JOIN clans c on c.id=m.clan_id
        LEFT OUTER JOIN players d on d.id=m.deleted_by
        LEFT OUTER JOIN players r on r.id=m.requestor_id
        LEFT OUTER JOIN players a on a.id=m.approver_id
        LEFT OUTER JOIN players y on y.id=m.denier_id
    WHERE
        p.game_id=$1 and p.id=$2`

    var details []playerDetailsDAO
    _, err = db.Select(&details, query, gameID, player.ID, 5)
    if err != nil {
        return nil, err
    }

    if len(details) == 0 {
        return nil, &ModelNotFoundError{"Player", publicID}
    }

    result := make(map[string]interface{})

    result["name"], err = util.DecryptData(details[0].PlayerName, encryptionKey)
    if err != nil {
        result["name"] = details[0].PlayerName
    }
    result["metadata"] = details[0].PlayerMetadata
    result["publicID"] = details[0].PlayerPublicID
    result["createdAt"] = details[0].PlayerCreatedAt
    result["updatedAt"] = details[0].PlayerUpdatedAt

    if details[0].MembershipLevel.Valid {
        // Player has memberships
        memberships := []map[string]interface{}{}

        approved := []map[string]interface{}{}
        denied := []map[string]interface{}{}
        banned := []map[string]interface{}{}
        pendingApplications := []map[string]interface{}{}
        pendingInvites := []map[string]interface{}{}

        clanFromDetail := func(clanDetail playerDetailsDAO) map[string]interface{} {
            return map[string]interface{}{
                "publicID": nullOrString(clanDetail.ClanPublicID),
                "name":     nullOrString(clanDetail.ClanName),
            }
        }

        for _, detail := range details {
            approvedMembership := nullOrBool(detail.MembershipApproved)
            deniedMembership := nullOrBool(detail.MembershipDenied)
            bannedMembership := nullOrBool(detail.MembershipBanned)
            deletedMembership := !bannedMembership && detail.MembershipDeletedAt.Valid && detail.MembershipDeletedAt.Int64 > 0

            if !deletedMembership {
                membership := detail.Serialize(encryptionKey)
                memberships = append(memberships, membership)

                clanDetail := clanFromDetail(detail)
                switch {
                case !approvedMembership && !deniedMembership && !bannedMembership:
                    if detail.RequestorPublicID.Valid && detail.RequestorPublicID.String == detail.PlayerPublicID {
                        pendingApplications = append(pendingApplications, clanDetail)
                    } else {
                        pendingInvites = append(pendingInvites, clanDetail)
                    }
                case approvedMembership:
                    approved = append(approved, clanDetail)
                case deniedMembership:
                    denied = append(denied, clanDetail)
                case bannedMembership:
                    banned = append(banned, clanDetail)
                }
            }
        }

        result["memberships"] = memberships
        result["clans"] = map[string]interface{}{
            "approved":            approved,
            "denied":              denied,
            "banned":              banned,
            "pendingApplications": pendingApplications,
            "pendingInvites":      pendingInvites,
        }
    } else {
        result["memberships"] = []map[string]interface{}{}
        result["clans"] = map[string]interface{}{
            "approved":            []map[string]interface{}{},
            "denied":              []map[string]interface{}{},
            "banned":              []map[string]interface{}{},
            "pendingApplications": []map[string]interface{}{},
            "pendingInvites":      []map[string]interface{}{},
        }
    }

    return result, nil
}

func decryptPlayerName(payload map[string]interface{}, encryptionKey []byte) map[string]interface{} {
    name, err := util.DecryptData(fmt.Sprint(payload["name"]), encryptionKey)
    if err != nil {
        return payload
    }

    payload["name"] = name
    return payload
}