topfreegames/khan

View on GitHub
lib/lib.go

Summary

Maintainability
D
1 day
Test Coverage
package lib

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "net"
    "net/http"
    "strings"
    "sync"
    "time"

    "github.com/spf13/viper"
    ehttp "github.com/topfreegames/extensions/v9/http"
)

// Khan is a struct that represents a khan API application
type Khan struct {
    httpClient *http.Client
    Config     *viper.Viper
    url        string
    user       string
    pass       string
    gameID     string
}

// KhanParams represents the params to create a Khan client
type KhanParams struct {
    Timeout             time.Duration
    MaxIdleConns        int
    MaxIdleConnsPerHost int
    URL                 string
    User                string
    Pass                string
    GameID              string
}

var (
    client *http.Client
    once   sync.Once
)

func getHTTPClient(
    timeout time.Duration,
    maxIdleConns, maxIdleConnsPerHost int,
) *http.Client {
    once.Do(func() {
        client = &http.Client{
            Transport: getHTTPTransport(maxIdleConns, maxIdleConnsPerHost),
            Timeout:   timeout,
        }
        ehttp.Instrument(client)
    })
    return client
}

func getHTTPTransport(
    maxIdleConns, maxIdleConnsPerHost int,
) http.RoundTripper {
    if _, ok := http.DefaultTransport.(*http.Transport); !ok {
        return http.DefaultTransport // tests use a mock transport
    }

    // We can't get http.DefaultTransport here and update its
    // fields since it's an exported variable, so other libs could
    // also change it and overwrite. This hardcoded values are copied
    // from http.DefaultTransport but could be configurable too.
    return &http.Transport{
        Proxy: http.ProxyFromEnvironment,
        DialContext: (&net.Dialer{
            Timeout:   30 * time.Second,
            KeepAlive: 30 * time.Second,
            DualStack: true,
        }).DialContext,
        MaxIdleConns:          maxIdleConns,
        IdleConnTimeout:       90 * time.Second,
        TLSHandshakeTimeout:   10 * time.Second,
        ExpectContinueTimeout: 1 * time.Second,
        MaxIdleConnsPerHost:   maxIdleConnsPerHost,
    }
}

// NewKhan returns a new khan API application
func NewKhan(config *viper.Viper) KhanInterface {
    config.SetDefault("khan.timeout", 500*time.Millisecond)
    config.SetDefault("khan.maxIdleConnsPerHost", http.DefaultMaxIdleConnsPerHost)
    config.SetDefault("khan.maxIdleConns", 100)

    k := &Khan{
        httpClient: getHTTPClient(
            config.GetDuration("khan.timeout"),
            config.GetInt("khan.maxIdleConns"),
            config.GetInt("khan.maxIdleConnsPerHost"),
        ),
        Config: config,
        url:    config.GetString("khan.url"),
        user:   config.GetString("khan.user"),
        pass:   config.GetString("khan.pass"),
        gameID: config.GetString("khan.gameid"),
    }
    return k
}

// NewKhanParams returns a new KhanParams instance with default values
func NewKhanParams() *KhanParams {
    return &KhanParams{
        Timeout:             500 * time.Millisecond,
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: http.DefaultMaxIdleConnsPerHost,
    }
}

// NewKhanWithParams returns a new khan API application initialized with passed params
func NewKhanWithParams(params *KhanParams) KhanInterface {
    return &Khan{
        httpClient: getHTTPClient(
            params.Timeout,
            params.MaxIdleConns,
            params.MaxIdleConnsPerHost,
        ),
        url:    params.URL,
        user:   params.User,
        pass:   params.Pass,
        gameID: params.GameID,
    }
}

func (k *Khan) sendTo(ctx context.Context, method, url string, payload interface{}) ([]byte, error) {
    payloadJSON, err := json.Marshal(payload)
    if err != nil {
        return nil, err
    }

    var req *http.Request

    if payload != nil {
        req, err = http.NewRequest(method, url, bytes.NewBuffer(payloadJSON))
        if err != nil {
            return nil, err
        }
    } else {
        req, err = http.NewRequest(method, url, nil)
        if err != nil {
            return nil, err
        }
    }
    req.Header.Set("Content-Type", "application/json")
    req.SetBasicAuth(k.user, k.pass)
    if ctx == nil {
        ctx = context.Background()
    }
    req = req.WithContext(ctx)

    resp, err := k.httpClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    body, respErr := ioutil.ReadAll(resp.Body)
    if respErr != nil {
        return nil, respErr
    }

    if resp.StatusCode > 399 {
        return nil, newRequestError(resp.StatusCode, string(body))
    }

    return body, nil
}

func (k *Khan) buildURL(pathname string) string {
    return fmt.Sprintf("%s/games/%s/%s", k.url, k.gameID, pathname)
}

func (k *Khan) buildCreatePlayerURL() string {
    pathname := "players"
    return k.buildURL(pathname)
}

func (k *Khan) buildUpdatePlayerURL(playerID string) string {
    pathname := fmt.Sprintf("players/%s", playerID)
    return k.buildURL(pathname)
}

func (k *Khan) buildRetrievePlayerURL(playerID string) string {
    pathname := fmt.Sprintf("players/%s", playerID)
    return k.buildURL(pathname)
}

func (k *Khan) buildCreateClanURL() string {
    pathname := "clans"
    return k.buildURL(pathname)
}

func (k *Khan) buildUpdateClanURL(clanID string) string {
    pathname := fmt.Sprintf("clans/%s", clanID)
    return k.buildURL(pathname)
}

func (k *Khan) buildRetrieveClanURL(clanID string) string {
    pathname := fmt.Sprintf("clans/%s", clanID)
    return k.buildURL(pathname)
}

func (k *Khan) buildRetrieveClanMembersURL(clanID string) string {
    pathname := fmt.Sprintf("clans/%s/members", clanID)
    return k.buildURL(pathname)
}

func (k *Khan) buildRetrieveClanSummaryURL(clanID string) string {
    pathname := fmt.Sprintf("clans/%s/summary", clanID)
    return k.buildURL(pathname)
}

func (k *Khan) buildRetrieveClansSummaryURL(clanIDs []string) string {
    pathname := fmt.Sprintf("clans-summary?clanPublicIds=%s", strings.Join(clanIDs, ","))
    return k.buildURL(pathname)
}

func (k *Khan) buildApplyForMembershipURL(clanID string) string {
    pathname := fmt.Sprintf("clans/%s/memberships/application", clanID)
    return k.buildURL(pathname)
}

func (k *Khan) buildInviteForMembershipURL(clanID string) string {
    pathname := fmt.Sprintf("clans/%s/memberships/invitation", clanID)
    return k.buildURL(pathname)
}

func (k *Khan) buildApproveDenyMembershipApplicationURL(clanID, action string) string {
    pathname := fmt.Sprintf("clans/%s/memberships/application/%s", clanID, action)
    return k.buildURL(pathname)
}

func (k *Khan) buildApproveDenyMembershipInvitationURL(clanID, action string) string {
    pathname := fmt.Sprintf("clans/%s/memberships/invitation/%s", clanID, action)
    return k.buildURL(pathname)
}

func (k *Khan) buildPromoteDemoteURL(clanID, action string) string {
    pathname := fmt.Sprintf("clans/%s/memberships/%s", clanID, action)
    return k.buildURL(pathname)
}

func (k *Khan) buildDeleteMembershipURL(clanID string) string {
    pathname := fmt.Sprintf("clans/%s/memberships/delete", clanID)
    return k.buildURL(pathname)
}

func (k *Khan) buildLeaveClanURL(clanID string) string {
    pathname := fmt.Sprintf("clans/%s/leave", clanID)
    return k.buildURL(pathname)
}

func (k *Khan) buildTransferOwnershipURL(clanID string) string {
    pathname := fmt.Sprintf("clans/%s/transfer-ownership", clanID)
    return k.buildURL(pathname)
}

func (k *Khan) buildSearchClansURL(clanName string) string {
    pathname := fmt.Sprintf("clans/search?term=%s", clanName)
    return k.buildURL(pathname)
}

// CreatePlayer calls Khan to create a new player
func (k *Khan) CreatePlayer(ctx context.Context, publicID, name string, metadata interface{}) (string, error) {
    route := k.buildCreatePlayerURL()
    playerPayload := &Player{
        PublicID: publicID,
        Name:     name,
        Metadata: metadata,
    }
    body, err := k.sendTo(ctx, "POST", route, playerPayload)

    if err != nil {
        return "", err
    }

    var player Player
    err = json.Unmarshal(body, &player)

    return player.PublicID, err
}

// UpdatePlayer calls khan to update the player
func (k *Khan) UpdatePlayer(
    ctx context.Context,
    publicID, name string,
    metadata interface{},
) (*Result, error) {
    route := k.buildUpdatePlayerURL(publicID)
    playerPayload := &Player{Name: name, Metadata: metadata}
    body, err := k.sendTo(ctx, "PUT", route, playerPayload)
    if err != nil {
        return nil, err
    }

    var result Result
    err = json.Unmarshal(body, &result)
    return &result, err
}

// RetrievePlayer calls the retrieve player route from khan
func (k *Khan) RetrievePlayer(ctx context.Context, publicID string) (*Player, error) {
    route := k.buildRetrievePlayerURL(publicID)
    body, err := k.sendTo(ctx, "GET", route, nil)

    if err != nil {
        return nil, err
    }

    var player Player
    err = json.Unmarshal(body, &player)
    return &player, err
}

// CreateClan calls the create clan route from khan
func (k *Khan) CreateClan(ctx context.Context, clan *ClanPayload) (string, error) {
    route := k.buildCreateClanURL()
    body, err := k.sendTo(ctx, "POST", route, clan)

    if err != nil {
        return "", err
    }

    var player Player
    err = json.Unmarshal(body, &player)
    return player.PublicID, err
}

// UpdateClan calls the update clan route from khan
func (k *Khan) UpdateClan(ctx context.Context, clan *ClanPayload) (*Result, error) {
    route := k.buildUpdateClanURL(clan.PublicID)
    body, err := k.sendTo(ctx, "PUT", route, clan)
    if err != nil {
        return nil, err
    }

    var result Result
    err = json.Unmarshal(body, &result)
    return &result, err
}

// RetrieveClanMembers calls the route to retrieve clan members from khan
func (k *Khan) RetrieveClanMembers(ctx context.Context, clanID string) (*ClanMembers, error) {
    route := k.buildRetrieveClanMembersURL(clanID)
    body, err := k.sendTo(ctx, "GET", route, nil)

    if err != nil {
        return nil, err
    }

    var clanMembers ClanMembers
    err = json.Unmarshal(body, &clanMembers)
    return &clanMembers, err
}

// RetrieveClanSummary calls the route to retrieve clan summary from khan
func (k *Khan) RetrieveClanSummary(ctx context.Context, clanID string) (*ClanSummary, error) {
    route := k.buildRetrieveClanSummaryURL(clanID)
    body, err := k.sendTo(ctx, "GET", route, nil)

    if err != nil {
        return nil, err
    }

    var clanSummary ClanSummary
    err = json.Unmarshal(body, &clanSummary)
    return &clanSummary, err
}

// RetrieveClansSummary calls the route to retrieve clans summary from khan
func (k *Khan) RetrieveClansSummary(ctx context.Context, clanIDs []string) ([]*ClanSummary, error) {
    route := k.buildRetrieveClansSummaryURL(clanIDs)
    body, err := k.sendTo(ctx, "GET", route, nil)

    if err != nil {
        return nil, err
    }

    var clansSummary ClansSummary
    err = json.Unmarshal(body, &clansSummary)
    if err != nil {
        return nil, err
    }
    return clansSummary.Clans, nil
}

// RetrieveClan calls the route to retrieve clan from khan
func (k *Khan) RetrieveClan(ctx context.Context, clanID string) (*Clan, error) {
    route := k.buildRetrieveClanURL(clanID)
    body, err := k.sendTo(ctx, "GET", route, nil)

    if err != nil {
        return nil, err
    }

    var clan Clan
    err = json.Unmarshal(body, &clan)
    return &clan, err
}

// ApplyForMembership calls apply for membership route on khan
func (k *Khan) ApplyForMembership(
    ctx context.Context,
    payload *ApplicationPayload,
) (*ClanApplyResult, error) {
    route := k.buildApplyForMembershipURL(payload.ClanID)
    body, err := k.sendTo(ctx, "POST", route, payload)

    if err != nil {
        return nil, err
    }

    var application ClanApplyResult
    err = json.Unmarshal(body, &application)
    return &application, err
}

// InviteForMembership invites a clan member to join clan
func (k *Khan) InviteForMembership(
    ctx context.Context,
    payload *InvitationPayload,
) (*Result, error) {
    route := k.buildInviteForMembershipURL(payload.ClanID)
    return k.defaultPostRequest(ctx, route, payload)
}

// ApproveDenyMembershipApplication approves or deny player
// application on clan
func (k *Khan) ApproveDenyMembershipApplication(
    ctx context.Context,
    payload *ApplicationApprovalPayload,
) (*Result, error) {
    route := k.buildApproveDenyMembershipApplicationURL(payload.ClanID, payload.Action)
    return k.defaultPostRequest(ctx, route, payload)
}

// ApproveDenyMembershipInvitation approves or deny player
// invitation on clan
func (k *Khan) ApproveDenyMembershipInvitation(
    ctx context.Context,
    payload *InvitationApprovalPayload,
) (*Result, error) {
    route := k.buildApproveDenyMembershipInvitationURL(payload.ClanID, payload.Action)
    return k.defaultPostRequest(ctx, route, payload)
}

// PromoteDemote promotes or demotes player on clan
func (k *Khan) PromoteDemote(
    ctx context.Context,
    payload *PromoteDemotePayload,
) (*Result, error) {
    route := k.buildPromoteDemoteURL(payload.ClanID, payload.Action)
    return k.defaultPostRequest(ctx, route, payload)
}

// DeleteMembership deletes membership on clan
func (k *Khan) DeleteMembership(
    ctx context.Context,
    payload *DeleteMembershipPayload,
) (*Result, error) {
    route := k.buildDeleteMembershipURL(payload.ClanID)
    return k.defaultPostRequest(ctx, route, payload)
}

// LeaveClan allows member to leave clan
func (k *Khan) LeaveClan(
    ctx context.Context,
    clanID string,
) (*LeaveClanResult, error) {
    route := k.buildLeaveClanURL(clanID)
    body, err := k.sendTo(ctx, "POST", route, nil)

    if err != nil {
        return nil, err
    }

    var result LeaveClanResult
    err = json.Unmarshal(body, &result)
    return &result, err
}

// TransferOwnership transfers clan ownership to another member
func (k *Khan) TransferOwnership(
    ctx context.Context,
    playerPublicID, clanID string,
) (*TransferOwnershipResult, error) {
    route := k.buildTransferOwnershipURL(clanID)
    body, err := k.sendTo(ctx, "POST", route, map[string]interface{}{
        "playerPublicID": playerPublicID,
    })

    if err != nil {
        return nil, err
    }

    var result TransferOwnershipResult
    err = json.Unmarshal(body, &result)
    return &result, err
}

func (k *Khan) defaultPostRequest(
    ctx context.Context,
    route string,
    payload interface{},
) (*Result, error) {
    body, err := k.sendTo(ctx, "POST", route, payload)
    if err != nil {
        return nil, err
    }

    var result Result
    err = json.Unmarshal(body, &result)
    return &result, err
}

// SearchClans returns clan summaries for all clans that contain the string "clanName".
func (k *Khan) SearchClans(ctx context.Context, clanName string) (*SearchClansResult, error) {
    route := k.buildSearchClansURL(clanName)
    body, err := k.sendTo(ctx, "GET", route, nil)
    if err != nil {
        return nil, err
    }

    var result SearchClansResult
    err = json.Unmarshal(body, &result)
    return &result, err
}