topfreegames/khan

View on GitHub
models/membership.go

Summary

Maintainability
F
3 days
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 (
    "database/sql"

    "github.com/topfreegames/khan/util"

    "github.com/go-gorp/gorp"
)

var approveString = "approve"

// Membership relates a player to a clan
type Membership struct {
    ID          int64         `db:"id"`
    GameID      string        `db:"game_id"`
    Level       string        `db:"membership_level"`
    Approved    bool          `db:"approved"`
    Denied      bool          `db:"denied"`
    Banned      bool          `db:"banned"`
    PlayerID    int64         `db:"player_id"`
    ClanID      int64         `db:"clan_id"`
    RequestorID int64         `db:"requestor_id"`
    ApproverID  sql.NullInt64 `db:"approver_id"`
    DenierID    sql.NullInt64 `db:"denier_id"`
    CreatedAt   int64         `db:"created_at"`
    UpdatedAt   int64         `db:"updated_at"`
    DeletedBy   int64         `db:"deleted_by"`
    DeletedAt   int64         `db:"deleted_at"`
    ApprovedAt  int64         `db:"approved_at"`
    DeniedAt    int64         `db:"denied_at"`
    Message     string        `db:"message"`
}

// PreInsert populates fields before inserting a new clan
func (m *Membership) PreInsert(s gorp.SqlExecutor) error {
    if m.CreatedAt == 0 {
        m.CreatedAt = util.NowMilli()
    }
    m.UpdatedAt = m.CreatedAt
    return nil
}

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

// GetMembershipByID returns a membership by id
func GetMembershipByID(db DB, id int64) (*Membership, error) {
    obj, err := db.Get(Membership{}, id)
    if err != nil {
        return nil, err
    }
    if obj == nil {
        return nil, &ModelNotFoundError{"Membership", id}
    }
    return obj.(*Membership), nil
}

// GetValidMembershipByClanAndPlayerPublicID returns a non deleted membership for the clan and the player with the given publicIDs
func GetValidMembershipByClanAndPlayerPublicID(db DB, gameID, clanPublicID, playerPublicID string) (*Membership, error) {
    var memberships []*Membership
    query := `
    SELECT
        m.*
    FROM memberships m
        INNER JOIN clans c ON c.game_id=$3 AND c.public_id=$1 AND c.id=m.clan_id
        INNER JOIN players p ON p.game_id=$3 AND p.public_id=$2 AND p.id=m.player_id
    WHERE
        m.game_id=$3 AND
        m.deleted_at=0`

    _, err := db.Select(&memberships, query, clanPublicID, playerPublicID, gameID)
    if err != nil {
        return nil, err
    }
    if memberships == nil || len(memberships) < 1 {
        return nil, &ModelNotFoundError{"Membership", playerPublicID}
    }
    return memberships[0], nil
}

// GetMembershipByClanAndPlayerPublicID returns a deleted membership for the clan and the player with the given publicIDs
func GetMembershipByClanAndPlayerPublicID(db DB, gameID, clanPublicID, playerPublicID string) (*Membership, error) {
    var memberships []*Membership
    query := `
    SELECT
        m.*
    FROM memberships m
        INNER JOIN clans c ON c.game_id=$3 AND c.public_id=$1 AND c.id=m.clan_id
        INNER JOIN players p ON p.game_id=$3 AND p.public_id=$2 AND p.id=m.player_id
    WHERE m.game_id=$3`

    _, err := db.Select(&memberships, query, clanPublicID, playerPublicID, gameID)
    if err != nil {
        return nil, err
    }
    if memberships == nil || len(memberships) < 1 {
        return nil, &ModelNotFoundError{"Membership", playerPublicID}
    }
    return memberships[0], nil
}

// GetDeletedMembershipByClanAndPlayerID returns a deleted membership for the player with the given ID and the clan ID
func GetDeletedMembershipByClanAndPlayerID(db DB, gameID string, clanID, playerID int64) (*Membership, error) {
    var memberships []*Membership
    query := `
    SELECT
        m.*
    FROM memberships m
        INNER JOIN clans c ON c.game_id=$3 AND c.id=$1 AND c.id=m.clan_id
        INNER JOIN players p ON p.game_id=$3 AND p.id=$2 AND p.id=m.player_id
    WHERE
        m.deleted_at!=0 AND m.game_id=$3`

    _, err := db.Select(&memberships, query, clanID, playerID, gameID)
    if err != nil {
        return nil, err
    }
    if memberships == nil || len(memberships) < 1 {
        return nil, &ModelNotFoundError{"Membership", playerID}
    }
    return memberships[0], nil
}

// GetOldestMemberWithHighestLevel returns the member with highest level that has the oldest creation date
func GetOldestMemberWithHighestLevel(db DB, gameID, clanPublicID string) (*Membership, error) {
    var memberships []*Membership
    query := `
    SELECT
     m.*
    FROM memberships m
     INNER JOIN games g ON g.public_id=m.game_id AND g.public_id=$1
     INNER JOIN clans c ON c.game_id=$1 AND c.public_id=$2 AND c.id=m.clan_id
    WHERE m.deleted_at=0 AND m.approved=true
    ORDER BY
     g.membership_levels::json->>m.membership_level DESC,
     m.created_at ASC
    LIMIT 1`
    _, err := db.Select(&memberships, query, gameID, clanPublicID)
    if err != nil {
        return nil, err
    }
    if memberships == nil || len(memberships) < 1 {
        return nil, &ClanHasNoMembersError{clanPublicID}
    }
    return memberships[0], nil
}

// GetNumberOfPendingInvites gets total number of pending invites for player
func GetNumberOfPendingInvites(db DB, player *Player) (int, error) {
    membershipCount, err := db.SelectInt(`
        SELECT COUNT(*)
        FROM memberships m
        WHERE
            m.player_id = $1 AND m.player_id != m.requestor_id AND m.deleted_at = 0 AND
            m.approved = false AND m.denied = false AND m.banned = false
    `, player.ID)
    if err != nil {
        return -1, nil
    }
    return int(membershipCount), nil
}

// ApproveOrDenyMembershipInvitation sets Membership.Approved to true or Membership.Denied to true
func ApproveOrDenyMembershipInvitation(db DB, encryptionKey []byte, game *Game, gameID, playerPublicID, clanPublicID, action string) (*Membership, error) {
    membership, err := GetValidMembershipByClanAndPlayerPublicID(db, gameID, clanPublicID, playerPublicID)
    if err != nil {
        return nil, err
    }

    if membership.Approved || membership.Denied {
        return nil, &CannotApproveOrDenyMembershipAlreadyProcessedError{action}
    }

    if membership.PlayerID == membership.RequestorID {
        // Cannot approve own application
        return nil, &PlayerCannotPerformMembershipActionError{action, playerPublicID, clanPublicID, playerPublicID}
    }

    player, err := GetPlayerByID(db, encryptionKey, membership.PlayerID)
    if err != nil {
        return nil, err
    }
    if action == approveString {
        err = playerReachedMaxClans(db, encryptionKey, game, player)
        if err != nil {
            return nil, err
        }
        reachedMaxMembersError := clanReachedMaxMemberships(db, game, nil, membership.ClanID)
        if reachedMaxMembersError != nil {
            return nil, reachedMaxMembersError
        }
    }
    return approveOrDenyMembershipHelper(db, membership, action, player)
}

// ApproveOrDenyMembershipApplication sets Membership.Approved to true or Membership.Denied to true
func ApproveOrDenyMembershipApplication(db DB, encryptionKey []byte, game *Game, gameID, playerPublicID, clanPublicID, requestorPublicID, action string) (*Membership, error) {
    if playerPublicID == requestorPublicID {
        return nil, &PlayerCannotPerformMembershipActionError{action, playerPublicID, clanPublicID, requestorPublicID}
    }

    requestor, err := GetPlayerByPublicID(db, encryptionKey, gameID, requestorPublicID)
    if err != nil {
        return nil, err
    }

    membership, err := GetValidMembershipByClanAndPlayerPublicID(db, gameID, clanPublicID, playerPublicID)
    if err != nil {
        return nil, err
    }

    if membership.PlayerID != membership.RequestorID {
        return nil, &PlayerCannotPerformMembershipActionError{action, playerPublicID, clanPublicID, requestorPublicID}
    }

    if membership.Approved || membership.Denied {
        return nil, &CannotApproveOrDenyMembershipAlreadyProcessedError{action}
    }

    if action == approveString {
        player, err := GetPlayerByID(db, encryptionKey, membership.PlayerID)
        if err != nil {
            return nil, err
        }
        err = playerReachedMaxClans(db, encryptionKey, game, player)
        if err != nil {
            return nil, err
        }
        reachedMaxMembersError := clanReachedMaxMemberships(db, game, nil, membership.ClanID)
        if reachedMaxMembersError != nil {
            return nil, reachedMaxMembersError
        }
    }

    reqMembership, _ := GetValidMembershipByClanAndPlayerPublicID(db, gameID, clanPublicID, requestorPublicID)
    if reqMembership == nil {
        _, clanErr := GetClanByPublicIDAndOwnerPublicID(db, gameID, clanPublicID, requestorPublicID)
        if clanErr != nil {
            return nil, &PlayerCannotPerformMembershipActionError{action, playerPublicID, clanPublicID, requestorPublicID}
        }
        return approveOrDenyMembershipHelper(db, membership, action, requestor)
    }

    levelInt := getLevelIntByLevel(reqMembership.Level, game.MembershipLevels)
    if !reqMembership.Approved || levelInt < game.MinLevelToAcceptApplication {
        return nil, &PlayerCannotPerformMembershipActionError{action, playerPublicID, clanPublicID, requestorPublicID}
    }
    return approveOrDenyMembershipHelper(db, membership, action, requestor)
}

// PromoteOrDemoteMember increments or decrements Membership.LevelInt by one
func PromoteOrDemoteMember(db DB, game *Game, gameID, playerPublicID, clanPublicID, requestorPublicID, action string) (*Membership, error) {
    demote := action == "demote"
    promote := action == "promote"

    levelOffset := game.MinLevelOffsetToDemoteMember
    if promote {
        levelOffset = game.MinLevelOffsetToPromoteMember
    }

    if playerPublicID == requestorPublicID {
        return nil, &PlayerCannotPerformMembershipActionError{action, playerPublicID, clanPublicID, requestorPublicID}
    }

    membership, err := GetValidMembershipByClanAndPlayerPublicID(db, gameID, clanPublicID, playerPublicID)
    if err != nil {
        return nil, err
    }
    if !isValidMember(membership) {
        return nil, &CannotPromoteOrDemoteInvalidMemberError{action}
    }

    levelInt := getLevelIntByLevel(membership.Level, game.MembershipLevels)
    if promote && levelInt >= game.MaxMembershipLevel || demote && levelInt <= game.MinMembershipLevel {
        return nil, &CannotPromoteOrDemoteMemberLevelError{action, levelInt}
    }

    reqMembership, _ := GetValidMembershipByClanAndPlayerPublicID(db, gameID, clanPublicID, requestorPublicID)
    if reqMembership == nil {
        _, clanErr := GetClanByPublicIDAndOwnerPublicID(db, gameID, clanPublicID, requestorPublicID)
        if clanErr != nil {
            return nil, &PlayerCannotPerformMembershipActionError{action, playerPublicID, clanPublicID, requestorPublicID}
        }
        return promoteOrDemoteMemberHelper(db, membership, action, game.MembershipLevels)
    }

    reqLevelInt := getLevelIntByLevel(reqMembership.Level, game.MembershipLevels)
    if isValidMember(reqMembership) && reqLevelInt >= levelInt+levelOffset {
        return promoteOrDemoteMemberHelper(db, membership, action, game.MembershipLevels)
    }
    return nil, &PlayerCannotPerformMembershipActionError{action, playerPublicID, clanPublicID, requestorPublicID}
}

// DeleteMembership soft deletes a membership
func DeleteMembership(db DB, game *Game, gameID, playerPublicID, clanPublicID, requestorPublicID string) (*Membership, error) {
    membership, err := GetValidMembershipByClanAndPlayerPublicID(db, gameID, clanPublicID, playerPublicID)
    if err != nil {
        return nil, err
    }
    if playerPublicID == requestorPublicID {
        return deleteMembershipHelper(db, membership, membership.PlayerID)
    }
    reqMembership, _ := GetValidMembershipByClanAndPlayerPublicID(db, gameID, clanPublicID, requestorPublicID)
    if reqMembership == nil {
        clan, clanErr := GetClanByPublicIDAndOwnerPublicID(db, gameID, clanPublicID, requestorPublicID)
        if clanErr != nil {
            return nil, &PlayerCannotPerformMembershipActionError{"delete", playerPublicID, clanPublicID, requestorPublicID}
        }
        return deleteMembershipHelper(db, membership, clan.OwnerID)
    }

    levelInt := getLevelIntByLevel(membership.Level, game.MembershipLevels)
    reqLevelInt := getLevelIntByLevel(reqMembership.Level, game.MembershipLevels)
    if isValidMember(reqMembership) && reqLevelInt >= game.MinLevelToRemoveMember && reqLevelInt >= levelInt+game.MinLevelOffsetToRemoveMember {
        return deleteMembershipHelper(db, membership, reqMembership.PlayerID)
    }
    return nil, &PlayerCannotPerformMembershipActionError{"delete", playerPublicID, clanPublicID, requestorPublicID}
}

// CreateMembership creates a new membership
func CreateMembership(db DB, encryptionKey []byte, game *Game, gameID, level, playerPublicID, clanPublicID, requestorPublicID, message string) (*Membership, error) {
    if _, levelValid := game.MembershipLevels[level]; !levelValid {
        return nil, &InvalidLevelForGameError{gameID, level}
    }

    clan, clanErr := GetClanByPublicID(db, game.PublicID, clanPublicID)
    if clanErr != nil {
        return nil, clanErr
    }

    membership, _ := GetMembershipByClanAndPlayerPublicID(db, gameID, clan.PublicID, playerPublicID)
    playerID, previousMembership, err := validateMembership(db, encryptionKey, game, membership, clan, playerPublicID, requestorPublicID)
    if err != nil {
        return nil, err
    }

    if clan.OwnerID == playerID {
        return nil, &AlreadyHasValidMembershipError{playerPublicID, clanPublicID}
    }

    if requestorPublicID == playerPublicID {
        return applyForMembership(db, game, membership, level, clan, playerID, requestorPublicID, message, previousMembership)
    }

    return inviteMember(db, encryptionKey, game, membership, level, clan, playerID, requestorPublicID, message, previousMembership)
}

func validateMembership(db DB, encryptionKey []byte, game *Game, membership *Membership, clan *Clan, playerPublicID, requestorPublicID string) (int64, bool, error) {
    playerID := int64(-1)
    previousMembership := false
    if membership != nil {
        previousMembership = true
        nowInMilliseconds := util.NowMilli()
        applicationInOpenClan := requestorPublicID == playerPublicID && clan.AllowApplication && clan.AutoJoin
        if membership.Approved {
            return -1, false, &AlreadyHasValidMembershipError{playerPublicID, clan.PublicID}
        } else if !applicationInOpenClan && membership.Denied && membership.DenierID.Int64 != membership.PlayerID {
            timeToBeReady := game.CooldownAfterDeny - int(nowInMilliseconds-membership.DeniedAt)/1000
            if timeToBeReady > 0 {
                return -1, false, &MustWaitMembershipCooldownError{timeToBeReady, playerPublicID, clan.PublicID}
            }
        } else if membership.DeletedAt > 0 && membership.DeletedBy != membership.PlayerID && playerPublicID == requestorPublicID {
            // Allow immediate membership creation if player is being invited
            timeToBeReady := game.CooldownAfterDelete - int(nowInMilliseconds-membership.DeletedAt)/1000
            if timeToBeReady > 0 {
                return -1, false, &MustWaitMembershipCooldownError{timeToBeReady, playerPublicID, clan.PublicID}
            }
        } else {
            // TODO: When allowing 'memberLeft' players to apply we do not avoid flooding in this case =/
            memberLeft := membership.DeletedAt > 0 && membership.DeletedBy == membership.PlayerID

            cd := 0
            previousInvite := membership.RequestorID != membership.PlayerID

            // invite and previous invite
            if previousInvite && requestorPublicID != playerPublicID {
                cd = game.CooldownBeforeInvite
            }
            // application and previous application
            if !previousInvite && requestorPublicID == playerPublicID && !memberLeft {
                cd = game.CooldownBeforeApply
            }

            if cd != 0 {
                timeToBeReady := cd - int(nowInMilliseconds-membership.UpdatedAt)/1000
                if timeToBeReady > 0 {
                    return -1, false, &MustWaitMembershipCooldownError{timeToBeReady, playerPublicID, clan.PublicID}
                }
            }
        }

        playerID = membership.PlayerID
        player, err := GetPlayerByID(db, encryptionKey, playerID)
        if err != nil {
            return -1, false, err
        }
        err = playerReachedMaxClans(db, encryptionKey, game, player)
        if err != nil {
            return -1, false, err
        }
    } else {
        player, err := GetPlayerByPublicID(db, encryptionKey, game.PublicID, playerPublicID)
        if err != nil {
            return -1, false, err
        }
        playerID = player.ID
        err = playerReachedMaxClans(db, encryptionKey, game, player)
        if err != nil {
            return -1, false, err
        }
    }

    return playerID, previousMembership, nil
}

func applyForMembership(db DB, game *Game, membership *Membership, level string, clan *Clan, playerID int64, requestorPublicID, message string, previousMembership bool) (*Membership, error) {
    if !clan.AllowApplication {
        return nil, &PlayerCannotCreateMembershipError{requestorPublicID, clan.PublicID}
    }

    reachedMaxMembersError := clanReachedMaxMemberships(db, game, clan, -1)
    if reachedMaxMembersError != nil {
        return nil, reachedMaxMembersError
    }
    if previousMembership {
        return updatePreviousMembershipHelper(db, membership, level, membership.PlayerID, message, clan.AutoJoin)
    }
    return createMembershipHelper(db, game.PublicID, level, playerID, clan.ID, playerID, message, clan.AutoJoin)
}

func inviteMember(db DB, encryptionKey []byte, game *Game, membership *Membership, level string, clan *Clan, playerID int64, requestorPublicID, message string, previousMembership bool) (*Membership, error) {
    reqMembership, _ := GetValidMembershipByClanAndPlayerPublicID(db, game.PublicID, clan.PublicID, requestorPublicID)
    if reqMembership == nil {
        requestor, err := GetPlayerByPublicID(db, encryptionKey, game.PublicID, requestorPublicID)
        if err != nil {
            return nil, err
        }
        // Did not find a memebership and player is not clan owner
        if requestor.ID != clan.OwnerID {
            return nil, &PlayerCannotCreateMembershipError{requestorPublicID, clan.PublicID}
        }
        reachedMaxInvitesError := playerReachedMaxInvites(db, encryptionKey, game, playerID)
        if reachedMaxInvitesError != nil {
            return nil, reachedMaxInvitesError
        }
        reachedMaxMembersError := clanReachedMaxMemberships(db, game, clan, -1)
        if reachedMaxMembersError != nil {
            return nil, reachedMaxMembersError
        }
        if previousMembership {
            return updatePreviousMembershipHelper(db, membership, level, clan.OwnerID, message, false)
        }
        return createMembershipHelper(db, game.PublicID, level, playerID, clan.ID, clan.OwnerID, message, false)
    }

    reachedMaxMembersError := clanReachedMaxMemberships(db, game, nil, reqMembership.ClanID)
    if reachedMaxMembersError != nil {
        return nil, reachedMaxMembersError
    }

    levelInt := getLevelIntByLevel(reqMembership.Level, game.MembershipLevels)

    if isValidMember(reqMembership) && levelInt >= game.MinLevelToCreateInvitation {
        if previousMembership {
            return updatePreviousMembershipHelper(db, membership, level, reqMembership.PlayerID, message, false)
        }
        return createMembershipHelper(db, game.PublicID, level, playerID, reqMembership.ClanID, reqMembership.PlayerID, message, false)
    }
    return nil, &PlayerCannotCreateMembershipError{requestorPublicID, clan.PublicID}
}

func clanReachedMaxMemberships(db DB, game *Game, clan *Clan, clanID int64) error {
    var err error
    if clan == nil {
        clan, err = GetClanByID(db, clanID)
        if err != nil {
            return err
        }
    }
    if clan.MembershipCount >= game.MaxMembers {
        return &ClanReachedMaxMembersError{clan.PublicID}
    }
    return nil
}

func playerReachedMaxInvites(db DB, encryptionKey []byte, game *Game, playerID int64) error {
    player, err := GetPlayerByID(db, encryptionKey, playerID)
    if err != nil {
        return err
    }

    pendingInvites, err := GetNumberOfPendingInvites(db, player)
    if err != nil {
        return err
    }

    if game.MaxPendingInvites > 0 && pendingInvites >= game.MaxPendingInvites {
        return &PlayerReachedMaxInvitesError{ID: player.PublicID}
    }

    return nil
}

func playerReachedMaxClans(db DB, encryptionKey []byte, game *Game, player *Player) error {
    playerID := player.ID
    if player.MembershipCount+player.OwnershipCount >= game.MaxClansPerPlayer {
        err := UpdatePlayerMembershipCount(db, playerID)
        if err != nil {
            return err
        }
        player, err = GetPlayerByID(db, encryptionKey, playerID)
        if err != nil {
            return err
        }
        if player.MembershipCount+player.OwnershipCount >= game.MaxClansPerPlayer {
            return &PlayerReachedMaxClansError{player.PublicID}
        }
    }
    return nil
}

func isValidMember(membership *Membership) bool {
    return membership.Approved && !membership.Denied
}

func approveOrDenyMembershipHelper(db DB, membership *Membership, action string, performer *Player) (*Membership, error) {
    approve := action == approveString
    if approve {
        membership.Approved = true
        membership.ApproverID = sql.NullInt64{Int64: performer.ID, Valid: true}
        membership.ApprovedAt = util.NowMilli()
    } else if action == "deny" {
        membership.Denied = true
        membership.DenierID = sql.NullInt64{Int64: performer.ID, Valid: true}
        membership.DeniedAt = util.NowMilli()

    } else {
        return nil, &InvalidMembershipActionError{action}
    }
    _, err := db.Update(membership)
    if err != nil {
        return nil, err
    }
    if approve {
        err = UpdatePlayerMembershipCount(db, membership.PlayerID)
        if err != nil {
            return nil, err
        }
        err = UpdateClanMembershipCount(db, membership.ClanID)
        if err != nil {
            return nil, err
        }
    }
    return membership, nil
}

func createMembershipHelper(db DB, gameID, level string, playerID, clanID, requestorID int64, message string, approved bool) (*Membership, error) {
    membership := &Membership{
        GameID:      gameID,
        ClanID:      clanID,
        PlayerID:    playerID,
        RequestorID: requestorID,
        Level:       level,
        Approved:    approved,
        Denied:      false,
        Message:     message,
    }

    if approved {
        membership.ApproverID = sql.NullInt64{Int64: requestorID, Valid: true}
        membership.ApprovedAt = util.NowMilli()
    }
    err := db.Insert(membership)
    if err != nil {
        return nil, err
    }
    if approved {
        err = UpdatePlayerMembershipCount(db, membership.PlayerID)
        if err != nil {
            return nil, err
        }
        err = UpdateClanMembershipCount(db, membership.ClanID)
        if err != nil {
            return nil, err
        }
    }
    return membership, nil
}

func updatePreviousMembershipHelper(db DB, membership *Membership, level string, requestorID int64, message string, approved bool) (*Membership, error) {
    membership.RequestorID = requestorID
    membership.Level = level
    membership.Approved = approved
    membership.Denied = false
    membership.Banned = false
    membership.DeletedAt = 0
    membership.DeletedBy = 0
    membership.Message = message
    if approved {
        membership.ApproverID = sql.NullInt64{Int64: requestorID, Valid: true}
        membership.ApprovedAt = util.NowMilli()
    }

    _, err := db.Update(membership)
    if err != nil {
        return nil, err
    }
    if approved {
        err = UpdatePlayerMembershipCount(db, membership.PlayerID)
        if err != nil {
            return nil, err
        }
        err = UpdateClanMembershipCount(db, membership.ClanID)
        if err != nil {
            return nil, err
        }
    }
    return membership, nil
}

func promoteOrDemoteMemberHelper(db DB, membership *Membership, action string, levels map[string]interface{}) (*Membership, error) {
    levelInt := getLevelIntByLevel(membership.Level, levels)
    if action == "promote" {
        membership.Level = getLevelByLevelInt(levelInt+1, levels)
    } else if action == "demote" {
        membership.Level = getLevelByLevelInt(levelInt-1, levels)
    } else {
        return nil, &InvalidMembershipActionError{action}
    }

    if membership.Level == "" {
        return nil, &InvalidLevelForGameError{membership.GameID, membership.Level}
    }

    _, err := db.Update(membership)
    if err != nil {
        return nil, err
    }
    return membership, nil
}

func deleteMembershipHelper(db DB, membership *Membership, deletedBy int64) (*Membership, error) {
    membershipWasApproved := membership.Approved
    membership.DeletedAt = util.NowMilli()
    membership.DeletedBy = deletedBy
    membership.Approved = false
    membership.Denied = false

    membership.Banned = deletedBy != membership.PlayerID // TODO: Test this

    _, err := db.Update(membership)
    if err != nil {
        return nil, err
    }

    if membershipWasApproved {
        err := UpdatePlayerMembershipCount(db, membership.PlayerID)
        if err != nil {
            return nil, err
        }
        err = UpdateClanMembershipCount(db, membership.ClanID)
        if err != nil {
            return nil, err
        }
    }
    return membership, err
}

func getLevelByLevelInt(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 ""
}

func getLevelIntByLevel(level string, levels map[string]interface{}) int {
    v := levels[level]
    switch v.(type) {
    case float64:
        return int(v.(float64))
    default:
        return v.(int)
    }
}