<commit_before>package server

import (
	"errors"
	"fmt"

	"golang.org/x/crypto/bcrypt"

	// go-grpc doesn't use the standard library's context.
	// https://github.com/grpc/grpc-go/issues/711
	"golang.org/x/net/context"

	"github.com/dexidp/dex/api"
	"github.com/dexidp/dex/pkg/log"
	"github.com/dexidp/dex/server/internal"
	"github.com/dexidp/dex/storage"
	"github.com/dexidp/dex/version"
)

// apiVersion increases every time a new call is added to the API. Clients should use this info
// to determine if the server supports specific features.
const apiVersion = 2

const (
	// recCost is the recommended bcrypt cost, which balances hash strength and
	// efficiency.
	recCost = 12

	// upBoundCost is a sane upper bound on bcrypt cost determined by benchmarking:
	// high enough to ensure secure encryption, low enough to not put unnecessary
	// load on a dex server.
	upBoundCost = 16
)

// NewAPI returns a server which implements the gRPC API interface.
func NewAPI(s storage.Storage, logger log.Logger) api.DexServer {
	return dexAPI{
		s:      s,
		logger: logger,
	}
}

type dexAPI struct {
	s      storage.Storage
	logger log.Logger
}

func (d dexAPI) CreateClient(ctx context.Context, req *api.CreateClientReq) (*api.CreateClientResp, error) {
	if req.Client == nil {
		return nil, errors.New("no client supplied")
	}

	if req.Client.Id == "" {
		req.Client.Id = storage.NewID()
	}
	if req.Client.Secret == "" {
		req.Client.Secret = storage.NewID() + storage.NewID()
	}

	c := storage.Client{
		ID:           req.Client.Id,
		Secret:       req.Client.Secret,
		RedirectURIs: req.Client.RedirectUris,
		TrustedPeers: req.Client.TrustedPeers,
		Public:       req.Client.Public,
		Name:         req.Client.Name,
		LogoURL:      req.Client.LogoUrl,
	}
	if err := d.s.CreateClient(c); err != nil {
		if err == storage.ErrAlreadyExists {
			return &api.CreateClientResp{AlreadyExists: true}, nil
		}
		d.logger.Errorf("api: failed to create client: %v", err)
		return nil, fmt.Errorf("create client: %v", err)
	}

	return &api.CreateClientResp{
		Client: req.Client,
	}, nil
}

func (d dexAPI) UpdateClient(ctx context.Context, req *api.UpdateClientReq) (*api.UpdateClientResp, error) {
	if req.Id == "" {
		return nil, errors.New("update client: no client ID supplied")
	}

	err := d.s.UpdateClient(req.Id, func(old storage.Client) (storage.Client, error) {
		if req.RedirectUris != nil {
			old.RedirectURIs = req.RedirectUris
		}
		if req.TrustedPeers != nil {
			old.TrustedPeers = req.TrustedPeers
		}
		if req.Name != "" {
			old.Name = req.Name
		}
		if req.LogoUrl != "" {
			old.LogoURL = req.LogoUrl
		}
		return old, nil
	})

	if err != nil {
		if err == storage.ErrNotFound {
			return &api.UpdateClientResp{NotFound: true}, nil
		}
		d.logger.Errorf("api: failed to update the client: %v", err)
		return nil, fmt.Errorf("update client: %v", err)
	}
	return &api.UpdateClientResp{}, nil
}

func (d dexAPI) DeleteClient(ctx context.Context, req *api.DeleteClientReq) (*api.DeleteClientResp, error) {
	err := d.s.DeleteClient(req.Id)
	if err != nil {
		if err == storage.ErrNotFound {
			return &api.DeleteClientResp{NotFound: true}, nil
		}
		d.logger.Errorf("api: failed to delete client: %v", err)
		return nil, fmt.Errorf("delete client: %v", err)
	}
	return &api.DeleteClientResp{}, nil
}

// checkCost returns an error if the hash provided does not meet lower or upper
// bound cost requirements.
func checkCost(hash []byte) error {
	actual, err := bcrypt.Cost(hash)
	if err != nil {
		return fmt.Errorf("parsing bcrypt hash: %v", err)
	}
	if actual < bcrypt.DefaultCost {
		return fmt.Errorf("given hash cost = %d does not meet minimum cost requirement = %d", actual, bcrypt.DefaultCost)
	}
	if actual > upBoundCost {
		return fmt.Errorf("given hash cost = %d is above upper bound cost = %d, recommended cost = %d", actual, upBoundCost, recCost)
	}
	return nil
}

func (d dexAPI) CreatePassword(ctx context.Context, req *api.CreatePasswordReq) (*api.CreatePasswordResp, error) {
	if req.Password == nil {
		return nil, errors.New("no password supplied")
	}
	if req.Password.UserId == "" {
		return nil, errors.New("no user ID supplied")
	}
	if req.Password.Hash != nil {
		if err := checkCost(req.Password.Hash); err != nil {
			return nil, err
		}
	} else {
		return nil, errors.New("no hash of password supplied")
	}

	p := storage.Password{
		Email:    req.Password.Email,
		Hash:     req.Password.Hash,
		Username: req.Password.Username,
		UserID:   req.Password.UserId,
	}
	if err := d.s.CreatePassword(p); err != nil {
		if err == storage.ErrAlreadyExists {
			return &api.CreatePasswordResp{AlreadyExists: true}, nil
		}
		d.logger.Errorf("api: failed to create password: %v", err)
		return nil, fmt.Errorf("create password: %v", err)
	}

	return &api.CreatePasswordResp{}, nil
}

func (d dexAPI) UpdatePassword(ctx context.Context, req *api.UpdatePasswordReq) (*api.UpdatePasswordResp, error) {
	if req.Email == "" {
		return nil, errors.New("no email supplied")
	}
	if req.NewHash == nil && req.NewUsername == "" {
		return nil, errors.New("nothing to update")
	}

	if req.NewHash != nil {
		if err := checkCost(req.NewHash); err != nil {
			return nil, err
		}
	}

	updater := func(old storage.Password) (storage.Password, error) {
		if req.NewHash != nil {
			old.Hash = req.NewHash
		}

		if req.NewUsername != "" {
			old.Username = req.NewUsername
		}

		return old, nil
	}

	if err := d.s.UpdatePassword(req.Email, updater); err != nil {
		if err == storage.ErrNotFound {
			return &api.UpdatePasswordResp{NotFound: true}, nil
		}
		d.logger.Errorf("api: failed to update password: %v", err)
		return nil, fmt.Errorf("update password: %v", err)
	}

	return &api.UpdatePasswordResp{}, nil
}

func (d dexAPI) DeletePassword(ctx context.Context, req *api.DeletePasswordReq) (*api.DeletePasswordResp, error) {
	if req.Email == "" {
		return nil, errors.New("no email supplied")
	}

	err := d.s.DeletePassword(req.Email)
	if err != nil {
		if err == storage.ErrNotFound {
			return &api.DeletePasswordResp{NotFound: true}, nil
		}
		d.logger.Errorf("api: failed to delete password: %v", err)
		return nil, fmt.Errorf("delete password: %v", err)
	}
	return &api.DeletePasswordResp{}, nil

}

func (d dexAPI) GetVersion(ctx context.Context, req *api.VersionReq) (*api.VersionResp, error) {
	return &api.VersionResp{
		Server: version.Version,
		Api:    apiVersion,
	}, nil
}

func (d dexAPI) ListPasswords(ctx context.Context, req *api.ListPasswordReq) (*api.ListPasswordResp, error) {
	passwordList, err := d.s.ListPasswords()
	if err != nil {
		d.logger.Errorf("api: failed to list passwords: %v", err)
		return nil, fmt.Errorf("list passwords: %v", err)
	}

	var passwords []*api.Password
	for _, password := range passwordList {
		p := api.Password{
			Email:    password.Email,
			Username: password.Username,
			UserId:   password.UserID,
		}
		passwords = append(passwords, &p)
	}

	return &api.ListPasswordResp{
		Passwords: passwords,
	}, nil

}

func (d dexAPI) VerifyPassword(ctx context.Context, req *api.VerifyPasswordReq) (*api.VerifyPasswordResp, error) {
	if req.Email == "" {
		return nil, errors.New("no email supplied")
	}

	if req.Password == "" {
		return nil, errors.New("no password to verify supplied")
	}

	password, err := d.s.GetPassword(req.Email)
	if err != nil {
		if err == storage.ErrNotFound {
			return &api.VerifyPasswordResp{
				NotFound: true,
			}, nil
		}
		d.logger.Errorf("api: there was an error retrieving the password: %v", err)
		return nil, fmt.Errorf("verify password: %v", err)
	}

	if err := bcrypt.CompareHashAndPassword(password.Hash, []byte(req.Password)); err != nil {
		d.logger.Info("api: password check failed : %v", err)
		return &api.VerifyPasswordResp{
			Verified: false,
		}, nil
	}
	return &api.VerifyPasswordResp{
		Verified: true,
	}, nil
}

func (d dexAPI) ListRefresh(ctx context.Context, req *api.ListRefreshReq) (*api.ListRefreshResp, error) {
	id := new(internal.IDTokenSubject)
	if err := internal.Unmarshal(req.UserId, id); err != nil {
		d.logger.Errorf("api: failed to unmarshal ID Token subject: %v", err)
		return nil, err
	}

	var refreshTokenRefs []*api.RefreshTokenRef
	offlineSessions, err := d.s.GetOfflineSessions(id.UserId, id.ConnId)
	if err != nil {
		if err == storage.ErrNotFound {
			// This means that this user-client pair does not have a refresh token yet.
			// An empty list should be returned instead of an error.
			return &api.ListRefreshResp{
				RefreshTokens: refreshTokenRefs,
			}, nil
		}
		d.logger.Errorf("api: failed to list refresh tokens %t here : %v", err == storage.ErrNotFound, err)
		return nil, err
	}

	for _, session := range offlineSessions.Refresh {
		r := api.RefreshTokenRef{
			Id:        session.ID,
			ClientId:  session.ClientID,
			CreatedAt: session.CreatedAt.Unix(),
			LastUsed:  session.LastUsed.Unix(),
		}
		refreshTokenRefs = append(refreshTokenRefs, &r)
	}

	return &api.ListRefreshResp{
		RefreshTokens: refreshTokenRefs,
	}, nil
}

func (d dexAPI) RevokeRefresh(ctx context.Context, req *api.RevokeRefreshReq) (*api.RevokeRefreshResp, error) {
	id := new(internal.IDTokenSubject)
	if err := internal.Unmarshal(req.UserId, id); err != nil {
		d.logger.Errorf("api: failed to unmarshal ID Token subject: %v", err)
		return nil, err
	}

	var (
		refreshID string
		notFound  bool
	)
	updater := func(old storage.OfflineSessions) (storage.OfflineSessions, error) {
		refreshRef := old.Refresh[req.ClientId]
		if refreshRef == nil || refreshRef.ID == "" {
			d.logger.Errorf("api: refresh token issued to client %q for user %q not found for deletion", req.ClientId, id.UserId)
			notFound = true
			return old, storage.ErrNotFound
		}

		refreshID = refreshRef.ID

		// Remove entry from Refresh list of the OfflineSession object.
		delete(old.Refresh, req.ClientId)

		return old, nil
	}

	if err := d.s.UpdateOfflineSessions(id.UserId, id.ConnId, updater); err != nil {
		if err == storage.ErrNotFound {
			return &api.RevokeRefreshResp{NotFound: true}, nil
		}
		d.logger.Errorf("api: failed to update offline session object: %v", err)
		return nil, err
	}

	if notFound {
		return &api.RevokeRefreshResp{NotFound: true}, nil
	}

	// Delete the refresh token from the storage
	//
	// TODO(ericchiang): we don't have any good recourse if this call fails.
	// Consider garbage collection of refresh tokens with no associated ref.
	if err := d.s.DeleteRefresh(refreshID); err != nil {
		d.logger.Errorf("failed to delete refresh token: %v", err)
		return nil, err
	}

	return &api.RevokeRefreshResp{}, nil
}
<commit_msg>server/api: fix logging in VerifyPassword<commit_after>package server

import (
	"errors"
	"fmt"

	"golang.org/x/crypto/bcrypt"

	// go-grpc doesn't use the standard library's context.
	// https://github.com/grpc/grpc-go/issues/711
	"golang.org/x/net/context"

	"github.com/dexidp/dex/api"
	"github.com/dexidp/dex/pkg/log"
	"github.com/dexidp/dex/server/internal"
	"github.com/dexidp/dex/storage"
	"github.com/dexidp/dex/version"
)

// apiVersion increases every time a new call is added to the API. Clients should use this info
// to determine if the server supports specific features.
const apiVersion = 2

const (
	// recCost is the recommended bcrypt cost, which balances hash strength and
	// efficiency.
	recCost = 12

	// upBoundCost is a sane upper bound on bcrypt cost determined by benchmarking:
	// high enough to ensure secure encryption, low enough to not put unnecessary
	// load on a dex server.
	upBoundCost = 16
)

// NewAPI returns a server which implements the gRPC API interface.
func NewAPI(s storage.Storage, logger log.Logger) api.DexServer {
	return dexAPI{
		s:      s,
		logger: logger,
	}
}

type dexAPI struct {
	s      storage.Storage
	logger log.Logger
}

func (d dexAPI) CreateClient(ctx context.Context, req *api.CreateClientReq) (*api.CreateClientResp, error) {
	if req.Client == nil {
		return nil, errors.New("no client supplied")
	}

	if req.Client.Id == "" {
		req.Client.Id = storage.NewID()
	}
	if req.Client.Secret == "" {
		req.Client.Secret = storage.NewID() + storage.NewID()
	}

	c := storage.Client{
		ID:           req.Client.Id,
		Secret:       req.Client.Secret,
		RedirectURIs: req.Client.RedirectUris,
		TrustedPeers: req.Client.TrustedPeers,
		Public:       req.Client.Public,
		Name:         req.Client.Name,
		LogoURL:      req.Client.LogoUrl,
	}
	if err := d.s.CreateClient(c); err != nil {
		if err == storage.ErrAlreadyExists {
			return &api.CreateClientResp{AlreadyExists: true}, nil
		}
		d.logger.Errorf("api: failed to create client: %v", err)
		return nil, fmt.Errorf("create client: %v", err)
	}

	return &api.CreateClientResp{
		Client: req.Client,
	}, nil
}

func (d dexAPI) UpdateClient(ctx context.Context, req *api.UpdateClientReq) (*api.UpdateClientResp, error) {
	if req.Id == "" {
		return nil, errors.New("update client: no client ID supplied")
	}

	err := d.s.UpdateClient(req.Id, func(old storage.Client) (storage.Client, error) {
		if req.RedirectUris != nil {
			old.RedirectURIs = req.RedirectUris
		}
		if req.TrustedPeers != nil {
			old.TrustedPeers = req.TrustedPeers
		}
		if req.Name != "" {
			old.Name = req.Name
		}
		if req.LogoUrl != "" {
			old.LogoURL = req.LogoUrl
		}
		return old, nil
	})

	if err != nil {
		if err == storage.ErrNotFound {
			return &api.UpdateClientResp{NotFound: true}, nil
		}
		d.logger.Errorf("api: failed to update the client: %v", err)
		return nil, fmt.Errorf("update client: %v", err)
	}
	return &api.UpdateClientResp{}, nil
}

func (d dexAPI) DeleteClient(ctx context.Context, req *api.DeleteClientReq) (*api.DeleteClientResp, error) {
	err := d.s.DeleteClient(req.Id)
	if err != nil {
		if err == storage.ErrNotFound {
			return &api.DeleteClientResp{NotFound: true}, nil
		}
		d.logger.Errorf("api: failed to delete client: %v", err)
		return nil, fmt.Errorf("delete client: %v", err)
	}
	return &api.DeleteClientResp{}, nil
}

// checkCost returns an error if the hash provided does not meet lower or upper
// bound cost requirements.
func checkCost(hash []byte) error {
	actual, err := bcrypt.Cost(hash)
	if err != nil {
		return fmt.Errorf("parsing bcrypt hash: %v", err)
	}
	if actual < bcrypt.DefaultCost {
		return fmt.Errorf("given hash cost = %d does not meet minimum cost requirement = %d", actual, bcrypt.DefaultCost)
	}
	if actual > upBoundCost {
		return fmt.Errorf("given hash cost = %d is above upper bound cost = %d, recommended cost = %d", actual, upBoundCost, recCost)
	}
	return nil
}

func (d dexAPI) CreatePassword(ctx context.Context, req *api.CreatePasswordReq) (*api.CreatePasswordResp, error) {
	if req.Password == nil {
		return nil, errors.New("no password supplied")
	}
	if req.Password.UserId == "" {
		return nil, errors.New("no user ID supplied")
	}
	if req.Password.Hash != nil {
		if err := checkCost(req.Password.Hash); err != nil {
			return nil, err
		}
	} else {
		return nil, errors.New("no hash of password supplied")
	}

	p := storage.Password{
		Email:    req.Password.Email,
		Hash:     req.Password.Hash,
		Username: req.Password.Username,
		UserID:   req.Password.UserId,
	}
	if err := d.s.CreatePassword(p); err != nil {
		if err == storage.ErrAlreadyExists {
			return &api.CreatePasswordResp{AlreadyExists: true}, nil
		}
		d.logger.Errorf("api: failed to create password: %v", err)
		return nil, fmt.Errorf("create password: %v", err)
	}

	return &api.CreatePasswordResp{}, nil
}

func (d dexAPI) UpdatePassword(ctx context.Context, req *api.UpdatePasswordReq) (*api.UpdatePasswordResp, error) {
	if req.Email == "" {
		return nil, errors.New("no email supplied")
	}
	if req.NewHash == nil && req.NewUsername == "" {
		return nil, errors.New("nothing to update")
	}

	if req.NewHash != nil {
		if err := checkCost(req.NewHash); err != nil {
			return nil, err
		}
	}

	updater := func(old storage.Password) (storage.Password, error) {
		if req.NewHash != nil {
			old.Hash = req.NewHash
		}

		if req.NewUsername != "" {
			old.Username = req.NewUsername
		}

		return old, nil
	}

	if err := d.s.UpdatePassword(req.Email, updater); err != nil {
		if err == storage.ErrNotFound {
			return &api.UpdatePasswordResp{NotFound: true}, nil
		}
		d.logger.Errorf("api: failed to update password: %v", err)
		return nil, fmt.Errorf("update password: %v", err)
	}

	return &api.UpdatePasswordResp{}, nil
}

func (d dexAPI) DeletePassword(ctx context.Context, req *api.DeletePasswordReq) (*api.DeletePasswordResp, error) {
	if req.Email == "" {
		return nil, errors.New("no email supplied")
	}

	err := d.s.DeletePassword(req.Email)
	if err != nil {
		if err == storage.ErrNotFound {
			return &api.DeletePasswordResp{NotFound: true}, nil
		}
		d.logger.Errorf("api: failed to delete password: %v", err)
		return nil, fmt.Errorf("delete password: %v", err)
	}
	return &api.DeletePasswordResp{}, nil

}

func (d dexAPI) GetVersion(ctx context.Context, req *api.VersionReq) (*api.VersionResp, error) {
	return &api.VersionResp{
		Server: version.Version,
		Api:    apiVersion,
	}, nil
}

func (d dexAPI) ListPasswords(ctx context.Context, req *api.ListPasswordReq) (*api.ListPasswordResp, error) {
	passwordList, err := d.s.ListPasswords()
	if err != nil {
		d.logger.Errorf("api: failed to list passwords: %v", err)
		return nil, fmt.Errorf("list passwords: %v", err)
	}

	var passwords []*api.Password
	for _, password := range passwordList {
		p := api.Password{
			Email:    password.Email,
			Username: password.Username,
			UserId:   password.UserID,
		}
		passwords = append(passwords, &p)
	}

	return &api.ListPasswordResp{
		Passwords: passwords,
	}, nil

}

func (d dexAPI) VerifyPassword(ctx context.Context, req *api.VerifyPasswordReq) (*api.VerifyPasswordResp, error) {
	if req.Email == "" {
		return nil, errors.New("no email supplied")
	}

	if req.Password == "" {
		return nil, errors.New("no password to verify supplied")
	}

	password, err := d.s.GetPassword(req.Email)
	if err != nil {
		if err == storage.ErrNotFound {
			return &api.VerifyPasswordResp{
				NotFound: true,
			}, nil
		}
		d.logger.Errorf("api: there was an error retrieving the password: %v", err)
		return nil, fmt.Errorf("verify password: %v", err)
	}

	if err := bcrypt.CompareHashAndPassword(password.Hash, []byte(req.Password)); err != nil {
		d.logger.Infof("api: password check failed: %v", err)
		return &api.VerifyPasswordResp{
			Verified: false,
		}, nil
	}
	return &api.VerifyPasswordResp{
		Verified: true,
	}, nil
}

func (d dexAPI) ListRefresh(ctx context.Context, req *api.ListRefreshReq) (*api.ListRefreshResp, error) {
	id := new(internal.IDTokenSubject)
	if err := internal.Unmarshal(req.UserId, id); err != nil {
		d.logger.Errorf("api: failed to unmarshal ID Token subject: %v", err)
		return nil, err
	}

	var refreshTokenRefs []*api.RefreshTokenRef
	offlineSessions, err := d.s.GetOfflineSessions(id.UserId, id.ConnId)
	if err != nil {
		if err == storage.ErrNotFound {
			// This means that this user-client pair does not have a refresh token yet.
			// An empty list should be returned instead of an error.
			return &api.ListRefreshResp{
				RefreshTokens: refreshTokenRefs,
			}, nil
		}
		d.logger.Errorf("api: failed to list refresh tokens %t here : %v", err == storage.ErrNotFound, err)
		return nil, err
	}

	for _, session := range offlineSessions.Refresh {
		r := api.RefreshTokenRef{
			Id:        session.ID,
			ClientId:  session.ClientID,
			CreatedAt: session.CreatedAt.Unix(),
			LastUsed:  session.LastUsed.Unix(),
		}
		refreshTokenRefs = append(refreshTokenRefs, &r)
	}

	return &api.ListRefreshResp{
		RefreshTokens: refreshTokenRefs,
	}, nil
}

func (d dexAPI) RevokeRefresh(ctx context.Context, req *api.RevokeRefreshReq) (*api.RevokeRefreshResp, error) {
	id := new(internal.IDTokenSubject)
	if err := internal.Unmarshal(req.UserId, id); err != nil {
		d.logger.Errorf("api: failed to unmarshal ID Token subject: %v", err)
		return nil, err
	}

	var (
		refreshID string
		notFound  bool
	)
	updater := func(old storage.OfflineSessions) (storage.OfflineSessions, error) {
		refreshRef := old.Refresh[req.ClientId]
		if refreshRef == nil || refreshRef.ID == "" {
			d.logger.Errorf("api: refresh token issued to client %q for user %q not found for deletion", req.ClientId, id.UserId)
			notFound = true
			return old, storage.ErrNotFound
		}

		refreshID = refreshRef.ID

		// Remove entry from Refresh list of the OfflineSession object.
		delete(old.Refresh, req.ClientId)

		return old, nil
	}

	if err := d.s.UpdateOfflineSessions(id.UserId, id.ConnId, updater); err != nil {
		if err == storage.ErrNotFound {
			return &api.RevokeRefreshResp{NotFound: true}, nil
		}
		d.logger.Errorf("api: failed to update offline session object: %v", err)
		return nil, err
	}

	if notFound {
		return &api.RevokeRefreshResp{NotFound: true}, nil
	}

	// Delete the refresh token from the storage
	//
	// TODO(ericchiang): we don't have any good recourse if this call fails.
	// Consider garbage collection of refresh tokens with no associated ref.
	if err := d.s.DeleteRefresh(refreshID); err != nil {
		d.logger.Errorf("failed to delete refresh token: %v", err)
		return nil, err
	}

	return &api.RevokeRefreshResp{}, nil
}
<|endoftext|>


<commit_before>package sdl

// #include "sdl_wrapper.h"
import "C"
import "unsafe"

// PixelFormat (https://wiki.libsdl.org/SDL_PixelFormat)
type PixelFormat struct {
	Format        uint32
	Palette       *Palette
	BitsPerPixel  uint8
	BytesPerPixel uint8
	_             [2]uint8 // padding
	Rmask         uint32
	Gmask         uint32
	Bmask         uint32
	Amask         uint32
	Rloss         uint8
	Gloss         uint8
	Bloss         uint8
	Aloss         uint8
	Rshift        uint8
	Gshift        uint8
	Bshift        uint8
	Ashift        uint8
	RefCount      int32
	Next          *PixelFormat
}
type cPixelFormat C.SDL_PixelFormat

// Palette (https://wiki.libsdl.org/SDL_Palette)
type Palette struct {
	Ncolors  int32
	Colors   *Color
	Version  uint32
	RefCount int32
}
type cPalette C.SDL_Palette

// Color (https://wiki.libsdl.org/SDL_Color)
type Color struct {
	R uint8
	G uint8
	B uint8
	A uint8
}

func (c Color) Uint32() uint32 {
	var v uint32
	v |= uint32(c.A) << 24
	v |= uint32(c.R) << 16
	v |= uint32(c.G) << 8
	v |= uint32(c.B)
	return v
}

const (
	PIXELTYPE_UNKNOWN  = C.SDL_PIXELTYPE_UNKNOWN
	PIXELTYPE_INDEX1   = C.SDL_PIXELTYPE_INDEX1
	PIXELTYPE_INDEX4   = C.SDL_PIXELTYPE_INDEX4
	PIXELTYPE_INDEX8   = C.SDL_PIXELTYPE_INDEX8
	PIXELTYPE_PACKED8  = C.SDL_PIXELTYPE_PACKED8
	PIXELTYPE_PACKED16 = C.SDL_PIXELTYPE_PACKED16
	PIXELTYPE_PACKED32 = C.SDL_PIXELTYPE_PACKED32
	PIXELTYPE_ARRAYU8  = C.SDL_PIXELTYPE_ARRAYU8
	PIXELTYPE_ARRAYU16 = C.SDL_PIXELTYPE_ARRAYU16
	PIXELTYPE_ARRAYU32 = C.SDL_PIXELTYPE_ARRAYU32
	PIXELTYPE_ARRAYF16 = C.SDL_PIXELTYPE_ARRAYF16
	PIXELTYPE_ARRAYF32 = C.SDL_PIXELTYPE_ARRAYF32
)

/** Bitmap pixel order high bit -> low bit. */
const (
	BITMAPORDER_NONE = C.SDL_BITMAPORDER_NONE
	BITMAPORDER_4321 = C.SDL_BITMAPORDER_4321
	BITMAPORDER_1234 = C.SDL_BITMAPORDER_1234
)

/** Packed component order high bit -> low bit. */
const (
	PACKEDORDER_NONE = C.SDL_PACKEDORDER_NONE
	PACKEDORDER_XRGB = C.SDL_PACKEDORDER_XRGB
	PACKEDORDER_RGBX = C.SDL_PACKEDORDER_RGBX
	PACKEDORDER_ARGB = C.SDL_PACKEDORDER_ARGB
	PACKEDORDER_RGBA = C.SDL_PACKEDORDER_RGBA
	PACKEDORDER_XBGR = C.SDL_PACKEDORDER_XBGR
	PACKEDORDER_BGRX = C.SDL_PACKEDORDER_BGRX
	PACKEDORDER_ABGR = C.SDL_PACKEDORDER_ABGR
	PACKEDORDER_BGRA = C.SDL_PACKEDORDER_BGRA
)

/** Array component order low byte -> high byte. */
const (
	ARRAYORDER_NONE = C.SDL_ARRAYORDER_NONE
	ARRAYORDER_RGB  = C.SDL_ARRAYORDER_RGB
	ARRAYORDER_RGBA = C.SDL_ARRAYORDER_RGBA
	ARRAYORDER_ARGB = C.SDL_ARRAYORDER_ARGB
	ARRAYORDER_BGR  = C.SDL_ARRAYORDER_BGR
	ARRAYORDER_BGRA = C.SDL_ARRAYORDER_BGRA
	ARRAYORDER_ABGR = C.SDL_ARRAYORDER_ABGR
)

/** Packed component layout. */
const (
	PACKEDLAYOUT_NONE    = C.SDL_PACKEDLAYOUT_NONE
	PACKEDLAYOUT_332     = C.SDL_PACKEDLAYOUT_332
	PACKEDLAYOUT_4444    = C.SDL_PACKEDLAYOUT_4444
	PACKEDLAYOUT_1555    = C.SDL_PACKEDLAYOUT_1555
	PACKEDLAYOUT_5551    = C.SDL_PACKEDLAYOUT_5551
	PACKEDLAYOUT_565     = C.SDL_PACKEDLAYOUT_565
	PACKEDLAYOUT_8888    = C.SDL_PACKEDLAYOUT_8888
	PACKEDLAYOUT_2101010 = C.SDL_PACKEDLAYOUT_2101010
	PACKEDLAYOUT_1010102 = C.SDL_PACKEDLAYOUT_1010102
)

const (
	PIXELFORMAT_UNKNOWN     = C.SDL_PIXELFORMAT_UNKNOWN
	PIXELFORMAT_INDEX1LSB   = C.SDL_PIXELFORMAT_INDEX1LSB
	PIXELFORMAT_INDEX1MSB   = C.SDL_PIXELFORMAT_INDEX1MSB
	PIXELFORMAT_INDEX4LSB   = C.SDL_PIXELFORMAT_INDEX4LSB
	PIXELFORMAT_INDEX4MSB   = C.SDL_PIXELFORMAT_INDEX4MSB
	PIXELFORMAT_INDEX8      = C.SDL_PIXELFORMAT_INDEX8
	PIXELFORMAT_RGB332      = C.SDL_PIXELFORMAT_RGB332
	PIXELFORMAT_RGB444      = C.SDL_PIXELFORMAT_RGB444
	PIXELFORMAT_RGB555      = C.SDL_PIXELFORMAT_RGB555
	PIXELFORMAT_BGR555      = C.SDL_PIXELFORMAT_BGR555
	PIXELFORMAT_ARGB4444    = C.SDL_PIXELFORMAT_ARGB4444
	PIXELFORMAT_RGBA4444    = C.SDL_PIXELFORMAT_RGBA4444
	PIXELFORMAT_ABGR4444    = C.SDL_PIXELFORMAT_ABGR4444
	PIXELFORMAT_BGRA4444    = C.SDL_PIXELFORMAT_BGRA4444
	PIXELFORMAT_ARGB1555    = C.SDL_PIXELFORMAT_ARGB1555
	PIXELFORMAT_RGBA5551    = C.SDL_PIXELFORMAT_RGBA5551
	PIXELFORMAT_ABGR1555    = C.SDL_PIXELFORMAT_ABGR1555
	PIXELFORMAT_BGRA5551    = C.SDL_PIXELFORMAT_BGRA5551
	PIXELFORMAT_RGB565      = C.SDL_PIXELFORMAT_RGB565
	PIXELFORMAT_BGR565      = C.SDL_PIXELFORMAT_BGR565
	PIXELFORMAT_RGB24       = C.SDL_PIXELFORMAT_RGB24
	PIXELFORMAT_BGR24       = C.SDL_PIXELFORMAT_BGR24
	PIXELFORMAT_RGB888      = C.SDL_PIXELFORMAT_RGB888
	PIXELFORMAT_RGBX8888    = C.SDL_PIXELFORMAT_RGBX8888
	PIXELFORMAT_BGR888      = C.SDL_PIXELFORMAT_BGR888
	PIXELFORMAT_BGRX8888    = C.SDL_PIXELFORMAT_BGRX8888
	PIXELFORMAT_ARGB8888    = C.SDL_PIXELFORMAT_ARGB8888
	PIXELFORMAT_RGBA8888    = C.SDL_PIXELFORMAT_RGBA8888
	PIXELFORMAT_ABGR8888    = C.SDL_PIXELFORMAT_ABGR8888
	PIXELFORMAT_BGRA8888    = C.SDL_PIXELFORMAT_BGRA8888
	PIXELFORMAT_ARGB2101010 = C.SDL_PIXELFORMAT_ARGB2101010
	PIXELFORMAT_YV12        = C.SDL_PIXELFORMAT_YV12
	PIXELFORMAT_IYUV        = C.SDL_PIXELFORMAT_IYUV
	PIXELFORMAT_YUY2        = C.SDL_PIXELFORMAT_YUY2
	PIXELFORMAT_UYVY        = C.SDL_PIXELFORMAT_UYVY
	PIXELFORMAT_YVYU        = C.SDL_PIXELFORMAT_YVYU
)

const (
	ALPHA_OPAQUE      = C.SDL_ALPHA_OPAQUE
	ALPHA_TRANSPARENT = C.SDL_ALPHA_TRANSPARENT
)

func (fmt *PixelFormat) cptr() *C.SDL_PixelFormat {
	return (*C.SDL_PixelFormat)(unsafe.Pointer(fmt))
}

func (p *Palette) cptr() *C.SDL_Palette {
	return (*C.SDL_Palette)(unsafe.Pointer(p))
}

/*
 * the following code is modified version of the code from bitbucket.org/dooots/go-sdl2
 */

// GetPixelFormatName gets the human readable name of a pixel format
// GetPixelFormatName (https://wiki.libsdl.org/SDL_GetPixelFormatName)
func GetPixelFormatName(format uint) string {
	return C.GoString(C.SDL_GetPixelFormatName(C.Uint32(format)))
}

// PixelFormatEnumToMasks converts format into a bpp and RGBA masks.
// PixelFormatEnumToMasks (https://wiki.libsdl.org/SDL_PixelFormatEnumToMasks)
func PixelFormatEnumToMasks(format uint) (bpp int, rmask, gmask, bmask, amask uint32, err error) {
	result := C.SDL_PixelFormatEnumToMasks(C.Uint32(format), (*C.int)(unsafe.Pointer(&bpp)),
		(*C.Uint32)(&rmask), (*C.Uint32)(&gmask), (*C.Uint32)(&bmask),
		(*C.Uint32)(&amask))
	if result == C.SDL_FALSE {
		err = GetError()
	}
	return
}

// MasksTouint converts a bpp and RGBA masks to a uint.
// MasksToPixelFormatEnum (https://wiki.libsdl.org/SDL_MasksToPixelFormatEnum)
func MasksToPixelFormatEnum(bpp int, rmask, gmask, bmask, amask uint32) uint {
	return uint(C.SDL_MasksToPixelFormatEnum(C.int(bpp), C.Uint32(rmask), C.Uint32(gmask),
		C.Uint32(bmask), C.Uint32(amask)))
}

// AllocFormat creates a PixelFormat structure from a uint.
// AllocFormat (https://wiki.libsdl.org/SDL_AllocFormat)
func AllocFormat(format uint) (*PixelFormat, error) {
	r := (*PixelFormat)(unsafe.Pointer(C.SDL_AllocFormat(C.Uint32(format))))
	if r == nil {
		return nil, GetError()
	}
	return r, nil
}

// Free frees a PixelFormat created by AllocFormat.
func (format *PixelFormat) Free() {
	C.SDL_FreeFormat((*C.SDL_PixelFormat)(unsafe.Pointer(format)))
}

// AllocPalette create a palette structure with the specified number of color
// entries.
// AllocPalette (https://wiki.libsdl.org/SDL_AllocPalette)
func AllocPalette(ncolors int) (*Palette, error) {
	r := (*Palette)(unsafe.Pointer(C.SDL_AllocPalette(C.int(ncolors))))
	if r == nil {
		return nil, GetError()
	}
	return r, nil
}

// SetPalette sets the palette for format.
// PixelFormat (https://wiki.libsdl.org/SDL_SetPixelFormatPalette)
func (format *PixelFormat) SetPalette(palette *Palette) error {
	r := C.SDL_SetPixelFormatPalette((*C.SDL_PixelFormat)(unsafe.Pointer(format)),
		(*C.SDL_Palette)(unsafe.Pointer(palette)))
	if r != 0 {
		return GetError()
	}
	return nil
}

// SetColors sets a range of colors in a palette.
// Palette (https://wiki.libsdl.org/SDL_SetPaletteColors)
func (palette *Palette) SetColors(colors []Color) error {
	var ptr *C.SDL_Color
	if len(colors) > 0 {
		ptr = (*C.SDL_Color)(unsafe.Pointer(&colors[0]))
	}

	r := C.SDL_SetPaletteColors((*C.SDL_Palette)(unsafe.Pointer(palette)),
		ptr, 0, C.int(len(colors)))
	if r != 0 {
		return GetError()
	}
	return nil
}

// Free frees a palette created with AllocPalette.
// Palette (https://wiki.libsdl.org/SDL_FreePalette)
func (palette *Palette) Free() {
	C.SDL_FreePalette((*C.SDL_Palette)(unsafe.Pointer(palette)))
}

// MapRGB maps an RGB triple to an opaque pixel value for a given pixel format.
// MapRGB (https://wiki.libsdl.org/SDL_MapRGB)
func MapRGB(format *PixelFormat, r, g, b uint8) uint32 {
	return uint32(C.SDL_MapRGB((*C.SDL_PixelFormat)(unsafe.Pointer(format)),
		C.Uint8(r), C.Uint8(g), C.Uint8(b)))
}

// MapRGBA maps an RGBA quadruple to a pixel value for a given pixel format.
// MapRGBA (https://wiki.libsdl.org/SDL_MapRGBA)
func MapRGBA(format *PixelFormat, r, g, b, a uint8) uint32 {
	return uint32(C.SDL_MapRGBA((*C.SDL_PixelFormat)(unsafe.Pointer(format)),
		C.Uint8(r), C.Uint8(g), C.Uint8(b), C.Uint8(a)))
}

// GetRGB gets the RGB components from a pixel of the specified format.
// GetRGB (https://wiki.libsdl.org/SDL_GetRGB)
func GetRGB(pixel uint32, format *PixelFormat) (r, g, b uint8) {
	C.SDL_GetRGB(C.Uint32(pixel), (*C.SDL_PixelFormat)(unsafe.Pointer(format)),
		(*C.Uint8)(&r), (*C.Uint8)(&g), (*C.Uint8)(&b))
	return
}

// GetRGBA gets the RGBA components from a pixel of the specified format.
// GetRGBA (https://wiki.libsdl.org/SDL_GetRGBA)
func GetRGBA(pixel uint32, format *PixelFormat) (r, g, b, a uint8) {
	C.SDL_GetRGBA(C.Uint32(pixel), (*C.SDL_PixelFormat)(unsafe.Pointer(format)),
		(*C.Uint8)(&r), (*C.Uint8)(&g), (*C.Uint8)(&b), (*C.Uint8)(&a))
	return
}

// CalculateGammaRamp calculates a 256 entry gamma ramp for a gamma value.
// CalculateGammaRamp (https://wiki.libsdl.org/SDL_CalculateGammaRamp)
func CalculateGammaRamp(gamma float32, ramp *[256]uint16) {
	C.SDL_CalculateGammaRamp(C.float(gamma), (*C.Uint16)(unsafe.Pointer(&ramp[0])))
}
<commit_msg>Implement standard library Color interface (#216)<commit_after>package sdl

// #include "sdl_wrapper.h"
import "C"
import "unsafe"

// PixelFormat (https://wiki.libsdl.org/SDL_PixelFormat)
type PixelFormat struct {
	Format        uint32
	Palette       *Palette
	BitsPerPixel  uint8
	BytesPerPixel uint8
	_             [2]uint8 // padding
	Rmask         uint32
	Gmask         uint32
	Bmask         uint32
	Amask         uint32
	Rloss         uint8
	Gloss         uint8
	Bloss         uint8
	Aloss         uint8
	Rshift        uint8
	Gshift        uint8
	Bshift        uint8
	Ashift        uint8
	RefCount      int32
	Next          *PixelFormat
}
type cPixelFormat C.SDL_PixelFormat

// Palette (https://wiki.libsdl.org/SDL_Palette)
type Palette struct {
	Ncolors  int32
	Colors   *Color
	Version  uint32
	RefCount int32
}
type cPalette C.SDL_Palette

// Color (https://wiki.libsdl.org/SDL_Color)
type Color struct {
	R uint8
	G uint8
	B uint8
	A uint8
}

func (c Color) Uint32() uint32 {
	var v uint32
	v |= uint32(c.A) << 24
	v |= uint32(c.R) << 16
	v |= uint32(c.G) << 8
	v |= uint32(c.B)
	return v
}

// RGBA returns the alpha-premultiplied red, green, blue and alpha values
// for the color. Each value ranges within [0, 0xffff], but is represented
// by a uint32 so that multiplying by a blend factor up to 0xffff will not
// overflow.
//
// An alpha-premultiplied color component c has been scaled by alpha (a),
// so has valid values 0 <= c <= a.
func (c Color) RGBA() (r, g, b, a uint32) {
	r = uint32(c.R)
	r |= r << 8
	g = uint32(c.G)
	g |= g << 8
	b = uint32(c.B)
	b |= b << 8
	a = uint32(c.A)
	a |= a << 8
	return
}

const (
	PIXELTYPE_UNKNOWN  = C.SDL_PIXELTYPE_UNKNOWN
	PIXELTYPE_INDEX1   = C.SDL_PIXELTYPE_INDEX1
	PIXELTYPE_INDEX4   = C.SDL_PIXELTYPE_INDEX4
	PIXELTYPE_INDEX8   = C.SDL_PIXELTYPE_INDEX8
	PIXELTYPE_PACKED8  = C.SDL_PIXELTYPE_PACKED8
	PIXELTYPE_PACKED16 = C.SDL_PIXELTYPE_PACKED16
	PIXELTYPE_PACKED32 = C.SDL_PIXELTYPE_PACKED32
	PIXELTYPE_ARRAYU8  = C.SDL_PIXELTYPE_ARRAYU8
	PIXELTYPE_ARRAYU16 = C.SDL_PIXELTYPE_ARRAYU16
	PIXELTYPE_ARRAYU32 = C.SDL_PIXELTYPE_ARRAYU32
	PIXELTYPE_ARRAYF16 = C.SDL_PIXELTYPE_ARRAYF16
	PIXELTYPE_ARRAYF32 = C.SDL_PIXELTYPE_ARRAYF32
)

/** Bitmap pixel order high bit -> low bit. */
const (
	BITMAPORDER_NONE = C.SDL_BITMAPORDER_NONE
	BITMAPORDER_4321 = C.SDL_BITMAPORDER_4321
	BITMAPORDER_1234 = C.SDL_BITMAPORDER_1234
)

/** Packed component order high bit -> low bit. */
const (
	PACKEDORDER_NONE = C.SDL_PACKEDORDER_NONE
	PACKEDORDER_XRGB = C.SDL_PACKEDORDER_XRGB
	PACKEDORDER_RGBX = C.SDL_PACKEDORDER_RGBX
	PACKEDORDER_ARGB = C.SDL_PACKEDORDER_ARGB
	PACKEDORDER_RGBA = C.SDL_PACKEDORDER_RGBA
	PACKEDORDER_XBGR = C.SDL_PACKEDORDER_XBGR
	PACKEDORDER_BGRX = C.SDL_PACKEDORDER_BGRX
	PACKEDORDER_ABGR = C.SDL_PACKEDORDER_ABGR
	PACKEDORDER_BGRA = C.SDL_PACKEDORDER_BGRA
)

/** Array component order low byte -> high byte. */
const (
	ARRAYORDER_NONE = C.SDL_ARRAYORDER_NONE
	ARRAYORDER_RGB  = C.SDL_ARRAYORDER_RGB
	ARRAYORDER_RGBA = C.SDL_ARRAYORDER_RGBA
	ARRAYORDER_ARGB = C.SDL_ARRAYORDER_ARGB
	ARRAYORDER_BGR  = C.SDL_ARRAYORDER_BGR
	ARRAYORDER_BGRA = C.SDL_ARRAYORDER_BGRA
	ARRAYORDER_ABGR = C.SDL_ARRAYORDER_ABGR
)

/** Packed component layout. */
const (
	PACKEDLAYOUT_NONE    = C.SDL_PACKEDLAYOUT_NONE
	PACKEDLAYOUT_332     = C.SDL_PACKEDLAYOUT_332
	PACKEDLAYOUT_4444    = C.SDL_PACKEDLAYOUT_4444
	PACKEDLAYOUT_1555    = C.SDL_PACKEDLAYOUT_1555
	PACKEDLAYOUT_5551    = C.SDL_PACKEDLAYOUT_5551
	PACKEDLAYOUT_565     = C.SDL_PACKEDLAYOUT_565
	PACKEDLAYOUT_8888    = C.SDL_PACKEDLAYOUT_8888
	PACKEDLAYOUT_2101010 = C.SDL_PACKEDLAYOUT_2101010
	PACKEDLAYOUT_1010102 = C.SDL_PACKEDLAYOUT_1010102
)

const (
	PIXELFORMAT_UNKNOWN     = C.SDL_PIXELFORMAT_UNKNOWN
	PIXELFORMAT_INDEX1LSB   = C.SDL_PIXELFORMAT_INDEX1LSB
	PIXELFORMAT_INDEX1MSB   = C.SDL_PIXELFORMAT_INDEX1MSB
	PIXELFORMAT_INDEX4LSB   = C.SDL_PIXELFORMAT_INDEX4LSB
	PIXELFORMAT_INDEX4MSB   = C.SDL_PIXELFORMAT_INDEX4MSB
	PIXELFORMAT_INDEX8      = C.SDL_PIXELFORMAT_INDEX8
	PIXELFORMAT_RGB332      = C.SDL_PIXELFORMAT_RGB332
	PIXELFORMAT_RGB444      = C.SDL_PIXELFORMAT_RGB444
	PIXELFORMAT_RGB555      = C.SDL_PIXELFORMAT_RGB555
	PIXELFORMAT_BGR555      = C.SDL_PIXELFORMAT_BGR555
	PIXELFORMAT_ARGB4444    = C.SDL_PIXELFORMAT_ARGB4444
	PIXELFORMAT_RGBA4444    = C.SDL_PIXELFORMAT_RGBA4444
	PIXELFORMAT_ABGR4444    = C.SDL_PIXELFORMAT_ABGR4444
	PIXELFORMAT_BGRA4444    = C.SDL_PIXELFORMAT_BGRA4444
	PIXELFORMAT_ARGB1555    = C.SDL_PIXELFORMAT_ARGB1555
	PIXELFORMAT_RGBA5551    = C.SDL_PIXELFORMAT_RGBA5551
	PIXELFORMAT_ABGR1555    = C.SDL_PIXELFORMAT_ABGR1555
	PIXELFORMAT_BGRA5551    = C.SDL_PIXELFORMAT_BGRA5551
	PIXELFORMAT_RGB565      = C.SDL_PIXELFORMAT_RGB565
	PIXELFORMAT_BGR565      = C.SDL_PIXELFORMAT_BGR565
	PIXELFORMAT_RGB24       = C.SDL_PIXELFORMAT_RGB24
	PIXELFORMAT_BGR24       = C.SDL_PIXELFORMAT_BGR24
	PIXELFORMAT_RGB888      = C.SDL_PIXELFORMAT_RGB888
	PIXELFORMAT_RGBX8888    = C.SDL_PIXELFORMAT_RGBX8888
	PIXELFORMAT_BGR888      = C.SDL_PIXELFORMAT_BGR888
	PIXELFORMAT_BGRX8888    = C.SDL_PIXELFORMAT_BGRX8888
	PIXELFORMAT_ARGB8888    = C.SDL_PIXELFORMAT_ARGB8888
	PIXELFORMAT_RGBA8888    = C.SDL_PIXELFORMAT_RGBA8888
	PIXELFORMAT_ABGR8888    = C.SDL_PIXELFORMAT_ABGR8888
	PIXELFORMAT_BGRA8888    = C.SDL_PIXELFORMAT_BGRA8888
	PIXELFORMAT_ARGB2101010 = C.SDL_PIXELFORMAT_ARGB2101010
	PIXELFORMAT_YV12        = C.SDL_PIXELFORMAT_YV12
	PIXELFORMAT_IYUV        = C.SDL_PIXELFORMAT_IYUV
	PIXELFORMAT_YUY2        = C.SDL_PIXELFORMAT_YUY2
	PIXELFORMAT_UYVY        = C.SDL_PIXELFORMAT_UYVY
	PIXELFORMAT_YVYU        = C.SDL_PIXELFORMAT_YVYU
)

const (
	ALPHA_OPAQUE      = C.SDL_ALPHA_OPAQUE
	ALPHA_TRANSPARENT = C.SDL_ALPHA_TRANSPARENT
)

func (fmt *PixelFormat) cptr() *C.SDL_PixelFormat {
	return (*C.SDL_PixelFormat)(unsafe.Pointer(fmt))
}

func (p *Palette) cptr() *C.SDL_Palette {
	return (*C.SDL_Palette)(unsafe.Pointer(p))
}

/*
 * the following code is modified version of the code from bitbucket.org/dooots/go-sdl2
 */

// GetPixelFormatName gets the human readable name of a pixel format
// GetPixelFormatName (https://wiki.libsdl.org/SDL_GetPixelFormatName)
func GetPixelFormatName(format uint) string {
	return C.GoString(C.SDL_GetPixelFormatName(C.Uint32(format)))
}

// PixelFormatEnumToMasks converts format into a bpp and RGBA masks.
// PixelFormatEnumToMasks (https://wiki.libsdl.org/SDL_PixelFormatEnumToMasks)
func PixelFormatEnumToMasks(format uint) (bpp int, rmask, gmask, bmask, amask uint32, err error) {
	result := C.SDL_PixelFormatEnumToMasks(C.Uint32(format), (*C.int)(unsafe.Pointer(&bpp)),
		(*C.Uint32)(&rmask), (*C.Uint32)(&gmask), (*C.Uint32)(&bmask),
		(*C.Uint32)(&amask))
	if result == C.SDL_FALSE {
		err = GetError()
	}
	return
}

// MasksTouint converts a bpp and RGBA masks to a uint.
// MasksToPixelFormatEnum (https://wiki.libsdl.org/SDL_MasksToPixelFormatEnum)
func MasksToPixelFormatEnum(bpp int, rmask, gmask, bmask, amask uint32) uint {
	return uint(C.SDL_MasksToPixelFormatEnum(C.int(bpp), C.Uint32(rmask), C.Uint32(gmask),
		C.Uint32(bmask), C.Uint32(amask)))
}

// AllocFormat creates a PixelFormat structure from a uint.
// AllocFormat (https://wiki.libsdl.org/SDL_AllocFormat)
func AllocFormat(format uint) (*PixelFormat, error) {
	r := (*PixelFormat)(unsafe.Pointer(C.SDL_AllocFormat(C.Uint32(format))))
	if r == nil {
		return nil, GetError()
	}
	return r, nil
}

// Free frees a PixelFormat created by AllocFormat.
func (format *PixelFormat) Free() {
	C.SDL_FreeFormat((*C.SDL_PixelFormat)(unsafe.Pointer(format)))
}

// AllocPalette create a palette structure with the specified number of color
// entries.
// AllocPalette (https://wiki.libsdl.org/SDL_AllocPalette)
func AllocPalette(ncolors int) (*Palette, error) {
	r := (*Palette)(unsafe.Pointer(C.SDL_AllocPalette(C.int(ncolors))))
	if r == nil {
		return nil, GetError()
	}
	return r, nil
}

// SetPalette sets the palette for format.
// PixelFormat (https://wiki.libsdl.org/SDL_SetPixelFormatPalette)
func (format *PixelFormat) SetPalette(palette *Palette) error {
	r := C.SDL_SetPixelFormatPalette((*C.SDL_PixelFormat)(unsafe.Pointer(format)),
		(*C.SDL_Palette)(unsafe.Pointer(palette)))
	if r != 0 {
		return GetError()
	}
	return nil
}

// SetColors sets a range of colors in a palette.
// Palette (https://wiki.libsdl.org/SDL_SetPaletteColors)
func (palette *Palette) SetColors(colors []Color) error {
	var ptr *C.SDL_Color
	if len(colors) > 0 {
		ptr = (*C.SDL_Color)(unsafe.Pointer(&colors[0]))
	}

	r := C.SDL_SetPaletteColors((*C.SDL_Palette)(unsafe.Pointer(palette)),
		ptr, 0, C.int(len(colors)))
	if r != 0 {
		return GetError()
	}
	return nil
}

// Free frees a palette created with AllocPalette.
// Palette (https://wiki.libsdl.org/SDL_FreePalette)
func (palette *Palette) Free() {
	C.SDL_FreePalette((*C.SDL_Palette)(unsafe.Pointer(palette)))
}

// MapRGB maps an RGB triple to an opaque pixel value for a given pixel format.
// MapRGB (https://wiki.libsdl.org/SDL_MapRGB)
func MapRGB(format *PixelFormat, r, g, b uint8) uint32 {
	return uint32(C.SDL_MapRGB((*C.SDL_PixelFormat)(unsafe.Pointer(format)),
		C.Uint8(r), C.Uint8(g), C.Uint8(b)))
}

// MapRGBA maps an RGBA quadruple to a pixel value for a given pixel format.
// MapRGBA (https://wiki.libsdl.org/SDL_MapRGBA)
func MapRGBA(format *PixelFormat, r, g, b, a uint8) uint32 {
	return uint32(C.SDL_MapRGBA((*C.SDL_PixelFormat)(unsafe.Pointer(format)),
		C.Uint8(r), C.Uint8(g), C.Uint8(b), C.Uint8(a)))
}

// GetRGB gets the RGB components from a pixel of the specified format.
// GetRGB (https://wiki.libsdl.org/SDL_GetRGB)
func GetRGB(pixel uint32, format *PixelFormat) (r, g, b uint8) {
	C.SDL_GetRGB(C.Uint32(pixel), (*C.SDL_PixelFormat)(unsafe.Pointer(format)),
		(*C.Uint8)(&r), (*C.Uint8)(&g), (*C.Uint8)(&b))
	return
}

// GetRGBA gets the RGBA components from a pixel of the specified format.
// GetRGBA (https://wiki.libsdl.org/SDL_GetRGBA)
func GetRGBA(pixel uint32, format *PixelFormat) (r, g, b, a uint8) {
	C.SDL_GetRGBA(C.Uint32(pixel), (*C.SDL_PixelFormat)(unsafe.Pointer(format)),
		(*C.Uint8)(&r), (*C.Uint8)(&g), (*C.Uint8)(&b), (*C.Uint8)(&a))
	return
}

// CalculateGammaRamp calculates a 256 entry gamma ramp for a gamma value.
// CalculateGammaRamp (https://wiki.libsdl.org/SDL_CalculateGammaRamp)
func CalculateGammaRamp(gamma float32, ramp *[256]uint16) {
	C.SDL_CalculateGammaRamp(C.float(gamma), (*C.Uint16)(unsafe.Pointer(&ramp[0])))
}
<|endoftext|>


<commit_before>package xlog

import (
	"fmt"
	"runtime"
)

type Logger struct {
	name     string
	fields   Fields
	provider Provider
}

func newLogger(s string) *Logger {
	return &Logger{
		name:     s,
		provider: provider,
	}
}

func (l *Logger) GetName() string {
	return l.name
}

func (l *Logger) IsEnableTrace() bool {
	pc, _, _, _ := runtime.Caller(1)
	return l.level(pc) >= LEVEL_TRACE
}

func (l *Logger) IsEnableDebug() bool {
	pc, _, _, _ := runtime.Caller(1)
	return l.level(pc) >= LEVEL_DEBUG
}

func (l *Logger) IsEnableInfo() bool {
	pc, _, _, _ := runtime.Caller(1)
	return l.level(pc) >= LEVEL_INFO
}

func (l *Logger) IsEnableWarn() bool {
	pc, _, _, _ := runtime.Caller(1)
	return l.level(pc) >= LEVEL_WARN
}

func (l *Logger) IsEnableError() bool {
	pc, _, _, _ := runtime.Caller(1)
	return l.level(pc) >= LEVEL_ERROR
}

func (l *Logger) IsEnableFatal() bool {
	pc, _, _, _ := runtime.Caller(1)
	return l.level(pc) >= LEVEL_FATAL
}

func (l *Logger) Trace(v ...interface{}) {
	pc, fileName, line, _ := runtime.Caller(1)
	msg := fmt.Sprint(v...)
	l.print(NewLog(LEVEL_TRACE, pc, fileName, line, msg))
}

func (l *Logger) Tracef(format string, v ...interface{}) {
	pc, fileName, line, _ := runtime.Caller(1)
	msg := fmt.Sprintf(format, v...)
	l.print(NewLog(LEVEL_TRACE, pc, fileName, line, msg))
}

func (l *Logger) Debug(v ...interface{}) {
	pc, fileName, line, _ := runtime.Caller(1)
	msg := fmt.Sprint(v...)
	l.print(NewLog(LEVEL_DEBUG, pc, fileName, line, msg))
}

func (l *Logger) Debugf(format string, v ...interface{}) {
	pc, fileName, line, _ := runtime.Caller(1)
	msg := fmt.Sprintf(format, v...)
	l.print(NewLog(LEVEL_DEBUG, pc, fileName, line, msg))
}

func (l *Logger) Info(v ...interface{}) {
	pc, fileName, line, _ := runtime.Caller(1)
	msg := fmt.Sprint(v...)
	l.print(NewLog(LEVEL_INFO, pc, fileName, line, msg))
}

func (l *Logger) Infof(format string, v ...interface{}) {
	pc, fileName, line, _ := runtime.Caller(1)
	msg := fmt.Sprintf(format, v...)
	l.print(NewLog(LEVEL_INFO, pc, fileName, line, msg))
}

func (l *Logger) Warn(v ...interface{}) {
	pc, fileName, line, _ := runtime.Caller(1)
	msg := fmt.Sprint(v...)
	l.print(NewLog(LEVEL_WARN, pc, fileName, line, msg))
}

func (l *Logger) Warnf(format string, v ...interface{}) {
	pc, fileName, line, _ := runtime.Caller(1)
	msg := fmt.Sprintf(format, v...)
	l.print(NewLog(LEVEL_WARN, pc, fileName, line, msg))
}

func (l *Logger) Error(v ...interface{}) {
	pc, fileName, line, _ := runtime.Caller(1)
	msg := fmt.Sprint(v...)
	l.print(NewLog(LEVEL_ERROR, pc, fileName, line, msg))
}

func (l *Logger) Errorf(format string, v ...interface{}) {
	pc, fileName, line, _ := runtime.Caller(1)
	msg := fmt.Sprintf(format, v...)
	l.print(NewLog(LEVEL_ERROR, pc, fileName, line, msg))
}

func (l *Logger) Fatal(v ...interface{}) {
	pc, fileName, line, _ := runtime.Caller(1)
	msg := fmt.Sprint(v...)
	l.print(NewLog(LEVEL_FATAL, pc, fileName, line, msg))
}

func (l *Logger) Fatalf(format string, v ...interface{}) {
	pc, fileName, line, _ := runtime.Caller(1)
	msg := fmt.Sprintf(format, v...)
	l.print(NewLog(LEVEL_FATAL, pc, fileName, line, msg))
}

// BindFields add the specified fields into the current Logger.
func (l *Logger) BindFields(fields Fields) {
	l.fields = NewFields(l.fields, fields)
}

// WithFields derive an new Logger by the specified fields from the current Logger.
func (l *Logger) WithFields(fields Fields) *Logger {
	result := newLogger(l.name)
	result.BindFields(NewFields(l.fields, fields))
	return result
}

// do print
func (l *Logger) print(log *Log) {
	l.provider.Print(log)
}

// retrieve current logger's lowest level
func (l *Logger) level(pc uintptr) Level {
	logger := l.name
	if logger == "" {
		logger, _ = parseFunc(pc)
	}
	return l.provider.GetLevel(logger)
}
<commit_msg>performance optimize<commit_after>package xlog

import (
	"fmt"
	"runtime"
)

type Logger struct {
	name     string
	fields   Fields
	provider Provider
}

func newLogger(s string) *Logger {
	return &Logger{
		name:     s,
		provider: provider,
	}
}

func (l *Logger) GetName() string {
	return l.name
}

func (l *Logger) IsEnableTrace() bool {
	var pc uintptr
	if l.name != "" {
		pc, _, _, _ = runtime.Caller(1)
	}
	return l.level(pc) >= LEVEL_TRACE
}

func (l *Logger) IsEnableDebug() bool {
	var pc uintptr
	if l.name != "" {
		pc, _, _, _ = runtime.Caller(1)
	}
	return l.level(pc) >= LEVEL_DEBUG
}

func (l *Logger) IsEnableInfo() bool {
	var pc uintptr
	if l.name != "" {
		pc, _, _, _ = runtime.Caller(1)
	}
	return l.level(pc) >= LEVEL_INFO
}

func (l *Logger) IsEnableWarn() bool {
	var pc uintptr
	if l.name != "" {
		pc, _, _, _ = runtime.Caller(1)
	}
	return l.level(pc) >= LEVEL_WARN
}

func (l *Logger) IsEnableError() bool {
	var pc uintptr
	if l.name != "" {
		pc, _, _, _ = runtime.Caller(1)
	}
	return l.level(pc) >= LEVEL_ERROR
}

func (l *Logger) IsEnableFatal() bool {
	var pc uintptr
	if l.name != "" {
		pc, _, _, _ = runtime.Caller(1)
	}
	return l.level(pc) >= LEVEL_FATAL
}

func (l *Logger) Trace(v ...interface{}) {
	if l.name != "" && l.level(0) > LEVEL_TRACE {
		return // don't need log
	}
	pc, fileName, line, _ := runtime.Caller(1)
	msg := fmt.Sprint(v...)
	l.print(NewLog(LEVEL_TRACE, pc, fileName, line, msg))
}

func (l *Logger) Tracef(format string, v ...interface{}) {
	if l.name != "" && l.level(0) > LEVEL_TRACE {
		return // don't need log
	}
	pc, fileName, line, _ := runtime.Caller(1)
	msg := fmt.Sprintf(format, v...)
	l.print(NewLog(LEVEL_TRACE, pc, fileName, line, msg))
}

func (l *Logger) Debug(v ...interface{}) {
	if l.name != "" && l.level(0) > LEVEL_DEBUG {
		return // don't need log
	}
	pc, fileName, line, _ := runtime.Caller(1)
	msg := fmt.Sprint(v...)
	l.print(NewLog(LEVEL_DEBUG, pc, fileName, line, msg))
}

func (l *Logger) Debugf(format string, v ...interface{}) {
	if l.name != "" && l.level(0) > LEVEL_DEBUG {
		return // don't need log
	}
	pc, fileName, line, _ := runtime.Caller(1)
	msg := fmt.Sprintf(format, v...)
	l.print(NewLog(LEVEL_DEBUG, pc, fileName, line, msg))
}

func (l *Logger) Info(v ...interface{}) {
	if l.name != "" && l.level(0) > LEVEL_INFO {
		return // don't need log
	}
	pc, fileName, line, _ := runtime.Caller(1)
	msg := fmt.Sprint(v...)
	l.print(NewLog(LEVEL_INFO, pc, fileName, line, msg))
}

func (l *Logger) Infof(format string, v ...interface{}) {
	if l.name != "" && l.level(0) > LEVEL_INFO {
		return // don't need log
	}
	pc, fileName, line, _ := runtime.Caller(1)
	msg := fmt.Sprintf(format, v...)
	l.print(NewLog(LEVEL_INFO, pc, fileName, line, msg))
}

func (l *Logger) Warn(v ...interface{}) {
	if l.name != "" && l.level(0) > LEVEL_WARN {
		return // don't need log
	}
	pc, fileName, line, _ := runtime.Caller(1)
	msg := fmt.Sprint(v...)
	l.print(NewLog(LEVEL_WARN, pc, fileName, line, msg))
}

func (l *Logger) Warnf(format string, v ...interface{}) {
	if l.name != "" && l.level(0) > LEVEL_WARN {
		return // don't need log
	}
	pc, fileName, line, _ := runtime.Caller(1)
	msg := fmt.Sprintf(format, v...)
	l.print(NewLog(LEVEL_WARN, pc, fileName, line, msg))
}

func (l *Logger) Error(v ...interface{}) {
	if l.name != "" && l.level(0) > LEVEL_ERROR {
		return // don't need log
	}
	pc, fileName, line, _ := runtime.Caller(1)
	msg := fmt.Sprint(v...)
	l.print(NewLog(LEVEL_ERROR, pc, fileName, line, msg))
}

func (l *Logger) Errorf(format string, v ...interface{}) {
	if l.name != "" && l.level(0) > LEVEL_ERROR {
		return // don't need log
	}
	pc, fileName, line, _ := runtime.Caller(1)
	msg := fmt.Sprintf(format, v...)
	l.print(NewLog(LEVEL_ERROR, pc, fileName, line, msg))
}

func (l *Logger) Fatal(v ...interface{}) {
	if l.name != "" && l.level(0) > LEVEL_FATAL {
		return // don't need log
	}
	pc, fileName, line, _ := runtime.Caller(1)
	msg := fmt.Sprint(v...)
	l.print(NewLog(LEVEL_FATAL, pc, fileName, line, msg))
}

func (l *Logger) Fatalf(format string, v ...interface{}) {
	if l.name != "" && l.level(0) > LEVEL_FATAL {
		return // don't need log
	}
	pc, fileName, line, _ := runtime.Caller(1)
	msg := fmt.Sprintf(format, v...)
	l.print(NewLog(LEVEL_FATAL, pc, fileName, line, msg))
}

// BindFields add the specified fields into the current Logger.
func (l *Logger) BindFields(fields Fields) {
	l.fields = NewFields(l.fields, fields)
}

// WithFields derive an new Logger by the specified fields from the current Logger.
func (l *Logger) WithFields(fields Fields) *Logger {
	result := newLogger(l.name)
	result.BindFields(NewFields(l.fields, fields))
	return result
}

// do print
func (l *Logger) print(log *Log) {
	l.provider.Print(log)
}

// retrieve current logger's lowest level
func (l *Logger) level(pc uintptr) Level {
	logger := l.name
	if logger == "" {
		logger, _ = parseFunc(pc)
	}
	return l.provider.GetLevel(logger)
}
<|endoftext|>


<commit_before>package server

import (
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"time"

	"go.uber.org/zap"

	"encoding/json"

	"github.com/mssola/user_agent"
	"github.com/openfresh/plasma/config"
	"github.com/openfresh/plasma/event"
	"github.com/openfresh/plasma/log"
	"github.com/openfresh/plasma/manager"
	"github.com/openfresh/plasma/metrics"
	"github.com/openfresh/plasma/pubsub"
	"github.com/pkg/errors"
)

type sseHandler struct {
	clientManager *manager.ClientManager
	timer         *time.Ticker
	newClients    chan manager.Client
	removeClients chan manager.Client
	payloads      chan event.Payload
	pubsub        pubsub.PubSuber
	retry         int
	eventQuery    string
	accessLogger  *zap.Logger
	errorLogger   *zap.Logger
	config        config.Config
}

func NewSSEHandler(opt Option) (sseHandler, error) {
	h := sseHandler{
		clientManager: manager.NewClientManager(),
		timer:         time.NewTicker(10 * time.Second),
		newClients:    make(chan manager.Client),
		removeClients: make(chan manager.Client),
		payloads:      make(chan event.Payload),
		pubsub:        opt.PubSuber,
		retry:         opt.Config.SSE.Retry,
		eventQuery:    opt.Config.SSE.EventQuery,
		accessLogger:  opt.AccessLogger,
		errorLogger:   opt.ErrorLogger,
		config:        opt.Config,
	}
	if err := h.pubsub.Subscribe(func(payload event.Payload) {
		h.payloads <- payload
	}); err != nil {
		return h, errors.Wrap(err, "failed to subscribe")
	}
	h.Run()

	return h, nil
}

const heartBeatEvent = "heartbeat"

func (h sseHandler) Run() {
	go func() {
		for {
			select {
			case client := <-h.newClients:
				h.clientManager.AddClient(client)
				metrics.IncConnection()
				metrics.IncConnectionSSE()
			case client := <-h.removeClients:
				h.clientManager.RemoveClient(client)
				metrics.DecConnection()
				metrics.DecConnectionSSE()
			case payload := <-h.payloads:
				h.clientManager.SendPayload(payload)
			case <-h.timer.C:
				h.clientManager.SendHeartBeat()
			}
		}
	}()
}

func isNotSupportSSE(u string) bool {
	ua := user_agent.New(u)

	name, _ := ua.Browser()

	switch name {
	case "Internet Explorer":
		return true
	case "Edge":
		return true
	}

	return false
}

func (h sseHandler) events(w http.ResponseWriter, r *http.Request) int {
	eventRequestsQuery, ok := r.URL.Query()[h.eventQuery]
	if !ok {
		http.Error(w, "specify event queries", http.StatusBadRequest)
		return http.StatusBadRequest
	}
	lastEventID := 0
	if id := r.Header.Get("HTTP_LAST_EVENT_ID"); id != "" {
		if i, err := strconv.Atoi(id); err == nil {
			lastEventID = i
		}
	} else if id, ok := r.URL.Query()["lastEventId"]; ok {
		if i, err := strconv.Atoi(id[0]); err == nil {
			lastEventID = i
		}
	}

	f, ok := w.(http.Flusher)
	if !ok {
		http.Error(w, "not streaming support", http.StatusInternalServerError)
		return http.StatusBadRequest
	}

	if len(eventRequestsQuery) == 0 || eventRequestsQuery[0] == "" {
		http.Error(w, "event query can't be empty", http.StatusBadRequest)
		return http.StatusBadRequest
	}

	// NOTE: eventRequestQuery[0] ex) 'program:1234:poll,program:1234:views'
	eventRequests := strings.Split(eventRequestsQuery[0], ",")

	if isNotSupportSSE(r.UserAgent()) {
		eventRequests = append(eventRequests, heartBeatEvent)
	}

	client := manager.NewClient(eventRequests)
	h.newClients <- client
	defer func() {
		h.removeClients <- client
	}()

	w.Header().Set("Content-Type", "text/event-stream")
	w.Header().Set("Cache-Control", "no-cache")
	w.Header().Set("Connection", "keep-alive")
	w.Header().Set("Access-Control-Allow-Origin", h.config.Origin)

	var notify <-chan bool
	notifier, ok := w.(http.CloseNotifier)
	if ok {
		notify = notifier.CloseNotify()
	}
	fmt.Fprintf(w, "retry: %d\n", h.retry)
	for {
		select {
		case pl, open := <-client.ReceivePayload():
			if !open {
				http.Error(w, "can't receive a payload", http.StatusInternalServerError)
				return http.StatusInternalServerError
			}
			eventType := pl.Meta.Type
			if eventType == heartBeatEvent {
				// NOTE: if use IE or Edge, need to send "comment" messages each 15-30 seconds, these messages will be used as heartbeat to detect disconnects
				// https://github.com/Yaffle/EventSource#server-side-requirements
				fmt.Fprint(w, ":heartbeat \n\n")
				f.Flush()
				lastEventID++
				continue
			}
			b, err := json.Marshal(pl)
			if err != nil {
				h.errorLogger.Error("failed to marshal event payload",
					zap.Error(err),
					zap.Object("payload", pl),
				)
				continue
			}
			fmt.Fprintf(w, "id: %d\n", lastEventID)
			fmt.Fprintf(w, "data: %s\n\n", string(b))
			f.Flush()
			lastEventID++
		case <-notify:
			w.WriteHeader(http.StatusOK)
			return http.StatusOK
		}
	}

}

func (h sseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	status := h.events(w, r)

	fileds := append(log.HTTPRequestToLogFields(r), zap.Int("status", status))
	h.accessLogger.Info("sse", fileds...)
}
<commit_msg>Modify closed client.ReceivePayload() behavior to success<commit_after>package server

import (
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"time"

	"go.uber.org/zap"

	"encoding/json"

	"github.com/mssola/user_agent"
	"github.com/openfresh/plasma/config"
	"github.com/openfresh/plasma/event"
	"github.com/openfresh/plasma/log"
	"github.com/openfresh/plasma/manager"
	"github.com/openfresh/plasma/metrics"
	"github.com/openfresh/plasma/pubsub"
	"github.com/pkg/errors"
)

type sseHandler struct {
	clientManager *manager.ClientManager
	timer         *time.Ticker
	newClients    chan manager.Client
	removeClients chan manager.Client
	payloads      chan event.Payload
	pubsub        pubsub.PubSuber
	retry         int
	eventQuery    string
	accessLogger  *zap.Logger
	errorLogger   *zap.Logger
	config        config.Config
}

func NewSSEHandler(opt Option) (sseHandler, error) {
	h := sseHandler{
		clientManager: manager.NewClientManager(),
		timer:         time.NewTicker(10 * time.Second),
		newClients:    make(chan manager.Client),
		removeClients: make(chan manager.Client),
		payloads:      make(chan event.Payload),
		pubsub:        opt.PubSuber,
		retry:         opt.Config.SSE.Retry,
		eventQuery:    opt.Config.SSE.EventQuery,
		accessLogger:  opt.AccessLogger,
		errorLogger:   opt.ErrorLogger,
		config:        opt.Config,
	}
	if err := h.pubsub.Subscribe(func(payload event.Payload) {
		h.payloads <- payload
	}); err != nil {
		return h, errors.Wrap(err, "failed to subscribe")
	}
	h.Run()

	return h, nil
}

const heartBeatEvent = "heartbeat"

func (h sseHandler) Run() {
	go func() {
		for {
			select {
			case client := <-h.newClients:
				h.clientManager.AddClient(client)
				metrics.IncConnection()
				metrics.IncConnectionSSE()
			case client := <-h.removeClients:
				h.clientManager.RemoveClient(client)
				metrics.DecConnection()
				metrics.DecConnectionSSE()
			case payload := <-h.payloads:
				h.clientManager.SendPayload(payload)
			case <-h.timer.C:
				h.clientManager.SendHeartBeat()
			}
		}
	}()
}

func isNotSupportSSE(u string) bool {
	ua := user_agent.New(u)

	name, _ := ua.Browser()

	switch name {
	case "Internet Explorer":
		return true
	case "Edge":
		return true
	}

	return false
}

func (h sseHandler) events(w http.ResponseWriter, r *http.Request) int {
	eventRequestsQuery, ok := r.URL.Query()[h.eventQuery]
	if !ok {
		http.Error(w, "specify event queries", http.StatusBadRequest)
		return http.StatusBadRequest
	}
	lastEventID := 0
	if id := r.Header.Get("HTTP_LAST_EVENT_ID"); id != "" {
		if i, err := strconv.Atoi(id); err == nil {
			lastEventID = i
		}
	} else if id, ok := r.URL.Query()["lastEventId"]; ok {
		if i, err := strconv.Atoi(id[0]); err == nil {
			lastEventID = i
		}
	}

	f, ok := w.(http.Flusher)
	if !ok {
		http.Error(w, "not streaming support", http.StatusInternalServerError)
		return http.StatusBadRequest
	}

	if len(eventRequestsQuery) == 0 || eventRequestsQuery[0] == "" {
		http.Error(w, "event query can't be empty", http.StatusBadRequest)
		return http.StatusBadRequest
	}

	// NOTE: eventRequestQuery[0] ex) 'program:1234:poll,program:1234:views'
	eventRequests := strings.Split(eventRequestsQuery[0], ",")

	if isNotSupportSSE(r.UserAgent()) {
		eventRequests = append(eventRequests, heartBeatEvent)
	}

	client := manager.NewClient(eventRequests)
	h.newClients <- client
	defer func() {
		h.removeClients <- client
	}()

	w.Header().Set("Content-Type", "text/event-stream")
	w.Header().Set("Cache-Control", "no-cache")
	w.Header().Set("Connection", "keep-alive")
	w.Header().Set("Access-Control-Allow-Origin", h.config.Origin)

	fmt.Fprintf(w, "retry: %d\n", h.retry)

	go func() {
		for pl := range client.ReceivePayload() {
			eventType := pl.Meta.Type
			if eventType == heartBeatEvent {
				// NOTE: if use IE or Edge, need to send "comment" messages each 15-30 seconds, these messages will be used as heartbeat to detect disconnects
				// https://github.com/Yaffle/EventSource#server-side-requirements
				fmt.Fprint(w, ":heartbeat \n\n")
				f.Flush()
				lastEventID++
				continue
			}
			b, err := json.Marshal(pl)
			if err != nil {
				h.errorLogger.Error("failed to marshal event payload",
					zap.Error(err),
					zap.Object("payload", pl),
				)
				continue
			}
			fmt.Fprintf(w, "id: %d\n", lastEventID)
			fmt.Fprintf(w, "data: %s\n\n", string(b))
			f.Flush()
			lastEventID++
		}
		w.WriteHeader(http.StatusOK)
	}()

	<-w.(http.CloseNotifier).CloseNotify()

	return http.StatusOK
}

func (h sseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	status := h.events(w, r)

	fileds := append(log.HTTPRequestToLogFields(r), zap.Int("status", status))
	h.accessLogger.Info("sse", fileds...)
}
<|endoftext|>


<commit_before>package api

import (
	"fmt"
	"io"
	"net/http"
	"time"

	"github.com/exercism/cli/debug"
)

var (
	// UserAgent lets the API know where the call is being made from.
	// It's overridden from the root command so that we can set the version.
	UserAgent = "github.com/exercism/cli"

	// TimeoutInSeconds is the timeout the default HTTP client will use.
	TimeoutInSeconds = 60
	// HTTPClient is the client used to make HTTP calls in the cli package.
	HTTPClient = &http.Client{Timeout: time.Duration(TimeoutInSeconds) * time.Second}
)

// Client is an http client that is configured for Exercism.
type Client struct {
	*http.Client
	ContentType string
	Token       string
	APIBaseURL  string
}

// NewClient returns an Exercism API client.
func NewClient(token, baseURL string) (*Client, error) {
	return &Client{
		Client:     HTTPClient,
		Token:      token,
		APIBaseURL: baseURL,
	}, nil
}

// NewRequest returns an http.Request with information for the Exercism API.
func (c *Client) NewRequest(method, url string, body io.Reader) (*http.Request, error) {
	if c.Client == nil {
		c.Client = HTTPClient
	}

	req, err := http.NewRequest(method, url, body)
	if err != nil {
		return nil, err
	}

	req.Header.Set("User-Agent", UserAgent)
	if c.ContentType == "" {
		req.Header.Set("Content-Type", "application/json")
	} else {
		req.Header.Set("Content-Type", c.ContentType)
	}
	if c.Token != "" {
		req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.Token))
	}

	return req, nil
}

// Do performs an http.Request and optionally parses the response body into the given interface.
func (c *Client) Do(req *http.Request) (*http.Response, error) {
	debug.DumpRequest(req)

	res, err := c.Client.Do(req)
	if err != nil {
		return nil, err
	}
	defer res.Body.Close()

	debug.DumpResponse(res)
	return res, nil
}

// TokenIsValid calls the API to determine whether the token is valid.
func (c *Client) TokenIsValid() (bool, error) {
	url := fmt.Sprintf("%s/validate_token", c.APIBaseURL)
	req, err := c.NewRequest("GET", url, nil)
	if err != nil {
		return false, err
	}
	resp, err := c.Do(req)
	if err != nil {
		return false, err
	}
	defer resp.Body.Close()

	return resp.StatusCode == http.StatusOK, nil
}

// IsPingable calls the API /ping to determine whether the API can be reached.
func (c *Client) IsPingable() error {
	url := fmt.Sprintf("%s/ping", c.APIBaseURL)
	req, err := c.NewRequest("GET", url, nil)
	if err != nil {
		return err
	}
	resp, err := c.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("API returned %s", resp.Status)
	}
	return nil
}
<commit_msg>fix closing wrong response body<commit_after>package api

import (
	"fmt"
	"io"
	"net/http"
	"time"

	"github.com/exercism/cli/debug"
)

var (
	// UserAgent lets the API know where the call is being made from.
	// It's overridden from the root command so that we can set the version.
	UserAgent = "github.com/exercism/cli"

	// TimeoutInSeconds is the timeout the default HTTP client will use.
	TimeoutInSeconds = 60
	// HTTPClient is the client used to make HTTP calls in the cli package.
	HTTPClient = &http.Client{Timeout: time.Duration(TimeoutInSeconds) * time.Second}
)

// Client is an http client that is configured for Exercism.
type Client struct {
	*http.Client
	ContentType string
	Token       string
	APIBaseURL  string
}

// NewClient returns an Exercism API client.
func NewClient(token, baseURL string) (*Client, error) {
	return &Client{
		Client:     HTTPClient,
		Token:      token,
		APIBaseURL: baseURL,
	}, nil
}

// NewRequest returns an http.Request with information for the Exercism API.
func (c *Client) NewRequest(method, url string, body io.Reader) (*http.Request, error) {
	if c.Client == nil {
		c.Client = HTTPClient
	}

	req, err := http.NewRequest(method, url, body)
	if err != nil {
		return nil, err
	}

	req.Header.Set("User-Agent", UserAgent)
	if c.ContentType == "" {
		req.Header.Set("Content-Type", "application/json")
	} else {
		req.Header.Set("Content-Type", c.ContentType)
	}
	if c.Token != "" {
		req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.Token))
	}

	return req, nil
}

// Do performs an http.Request and optionally parses the response body into the given interface.
func (c *Client) Do(req *http.Request) (*http.Response, error) {
	debug.DumpRequest(req)

	res, err := c.Client.Do(req)
	if err != nil {
		return nil, err
	}

	debug.DumpResponse(res)
	return res, nil
}

// TokenIsValid calls the API to determine whether the token is valid.
func (c *Client) TokenIsValid() (bool, error) {
	url := fmt.Sprintf("%s/validate_token", c.APIBaseURL)
	req, err := c.NewRequest("GET", url, nil)
	if err != nil {
		return false, err
	}
	resp, err := c.Do(req)
	if err != nil {
		return false, err
	}
	defer resp.Body.Close()

	return resp.StatusCode == http.StatusOK, nil
}

// IsPingable calls the API /ping to determine whether the API can be reached.
func (c *Client) IsPingable() error {
	url := fmt.Sprintf("%s/ping", c.APIBaseURL)
	req, err := c.NewRequest("GET", url, nil)
	if err != nil {
		return err
	}
	resp, err := c.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("API returned %s", resp.Status)
	}
	return nil
}
<|endoftext|>


<commit_before>// Copyright 2016 Circonus, Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Metric API support - Fetch, Create*, Update, Delete*, and Search
// See: https://login.circonus.com/resources/api/calls/metric
// *  : create and delete are handled via check_bundle or check_bundle_metrics

package api

import (
	"encoding/json"
	"fmt"
	"net/url"
	"regexp"
)

// Metric defines a metric
type Metric struct {
	CID            string   `json:"_cid,omitempty"`
	Active         bool     `json:"_active,omitempty"`
	CheckCID       string   `json:"_check,omitempty"`
	CheckActive    bool     `json:"_check_active,omitempty"`
	CheckBundleCID string   `json:"_check_bundle,omitempty"`
	CheckTags      []string `json:"_check_tags,omitempty"`
	CheckUUID      string   `json:"_check_uuid,omitempty"`
	Histogram      bool     `json:"_histogram,omitempty"`
	MetricName     string   `json:"_metric_name,omitempty"`
	MetricType     string   `json:"_metric_type,omitempty"`
	Tags           []string `json:"tags,omitempty"`
	Units          string   `json:"units,omitempty"`
}

const (
	baseMetricPath = "/metric"
	metricCIDRegex = "^" + baseMetricPath + "/[0-9]+$"
)

// FetchMetric retrieves a metric definition
func (a *API) FetchMetric(cid CIDType) (*Metric, error) {
	if cid == nil || *cid == "" {
		return nil, fmt.Errorf("Invalid metric CID [none]")
	}

	metricCID := string(*cid)

	matched, err := regexp.MatchString(metricCIDRegex, metricCID)
	if err != nil {
		return nil, err
	}
	if !matched {
		return nil, fmt.Errorf("Invalid metric CID [%s]", metricCID)
	}

	result, err := a.Get(metricCID)
	if err != nil {
		return nil, err
	}

	metric := &Metric{}
	if err := json.Unmarshal(result, metric); err != nil {
		return nil, err
	}

	return metric, nil
}

// FetchMetrics retrieves all metrics
func (a *API) FetchMetrics() (*[]Metric, error) {
	result, err := a.Get(baseMetricPath)
	if err != nil {
		return nil, err
	}

	var metrics []Metric
	if err := json.Unmarshal(result, &metrics); err != nil {
		return nil, err
	}

	return &metrics, nil
}

// UpdateMetric update metric definition
func (a *API) UpdateMetric(config *Metric) (*Metric, error) {
	if config == nil {
		return nil, fmt.Errorf("Invalid metric config [nil]")
	}

	metricCID := string(config.CID)

	matched, err := regexp.MatchString(metricCIDRegex, metricCID)
	if err != nil {
		return nil, err
	}
	if !matched {
		return nil, fmt.Errorf("Invalid metric CID [%s]", metricCID)
	}

	cfg, err := json.Marshal(config)
	if err != nil {
		return nil, err
	}

	result, err := a.Put(metricCID, cfg)
	if err != nil {
		return nil, err
	}

	metric := &Metric{}
	if err := json.Unmarshal(result, metric); err != nil {
		return nil, err
	}

	return metric, nil
}

// SearchMetrics returns list of metrics matching a search query and/or filter
//    - a search query (see: https://login.circonus.com/resources/api#searching)
//    - a filter (see: https://login.circonus.com/resources/api#filtering)
func (a *API) SearchMetrics(searchCriteria *SearchQueryType, filterCriteria *SearchFilterType) (*[]Metric, error) {
	q := url.Values{}

	if searchCriteria != nil && *searchCriteria != "" {
		q.Set("search", string(*searchCriteria))
	}

	if filterCriteria != nil && len(*filterCriteria) > 0 {
		for filter, criteria := range *filterCriteria {
			for _, val := range criteria {
				q.Add(filter, val)
			}
		}
	}

	if q.Encode() == "" {
		return a.FetchMetrics()
	}

	reqURL := url.URL{
		Path:     baseMetricPath,
		RawQuery: q.Encode(),
	}

	result, err := a.Get(reqURL.String())
	if err != nil {
		return nil, fmt.Errorf("[ERROR] API call error %+v", err)
	}

	var metrics []Metric
	if err := json.Unmarshal(result, &metrics); err != nil {
		return nil, err
	}

	return &metrics, nil
}
<commit_msg>upd: with new link and notes attributes<commit_after>// Copyright 2016 Circonus, Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Metric API support - Fetch, Create*, Update, Delete*, and Search
// See: https://login.circonus.com/resources/api/calls/metric
// *  : create and delete are handled via check_bundle or check_bundle_metrics

package api

import (
	"encoding/json"
	"fmt"
	"net/url"
	"regexp"
)

// Metric defines a metric
type Metric struct {
	CID            string   `json:"_cid,omitempty"`
	Active         bool     `json:"_active,omitempty"`
	CheckCID       string   `json:"_check,omitempty"`
	CheckActive    bool     `json:"_check_active,omitempty"`
	CheckBundleCID string   `json:"_check_bundle,omitempty"`
	CheckTags      []string `json:"_check_tags,omitempty"`
	CheckUUID      string   `json:"_check_uuid,omitempty"`
	Histogram      bool     `json:"_histogram,omitempty"`
	MetricName     string   `json:"_metric_name,omitempty"`
	MetricType     string   `json:"_metric_type,omitempty"`
	Tags           []string `json:"tags,omitempty"`
	Units          *string  `json:"units,omitempty"` // string or null
	Link           *string  `json:"link,omitempty"`  // string or null
	Notes          *string  `json:"notes,omitempty"` // string or null
}

const (
	baseMetricPath = "/metric"
	metricCIDRegex = "^" + baseMetricPath + "/[0-9]+$"
)

// FetchMetric retrieves a metric definition
func (a *API) FetchMetric(cid CIDType) (*Metric, error) {
	if cid == nil || *cid == "" {
		return nil, fmt.Errorf("Invalid metric CID [none]")
	}

	metricCID := string(*cid)

	matched, err := regexp.MatchString(metricCIDRegex, metricCID)
	if err != nil {
		return nil, err
	}
	if !matched {
		return nil, fmt.Errorf("Invalid metric CID [%s]", metricCID)
	}

	result, err := a.Get(metricCID)
	if err != nil {
		return nil, err
	}

	metric := &Metric{}
	if err := json.Unmarshal(result, metric); err != nil {
		return nil, err
	}

	return metric, nil
}

// FetchMetrics retrieves all metrics
func (a *API) FetchMetrics() (*[]Metric, error) {
	result, err := a.Get(baseMetricPath)
	if err != nil {
		return nil, err
	}

	var metrics []Metric
	if err := json.Unmarshal(result, &metrics); err != nil {
		return nil, err
	}

	return &metrics, nil
}

// UpdateMetric update metric definition
func (a *API) UpdateMetric(config *Metric) (*Metric, error) {
	if config == nil {
		return nil, fmt.Errorf("Invalid metric config [nil]")
	}

	metricCID := string(config.CID)

	matched, err := regexp.MatchString(metricCIDRegex, metricCID)
	if err != nil {
		return nil, err
	}
	if !matched {
		return nil, fmt.Errorf("Invalid metric CID [%s]", metricCID)
	}

	cfg, err := json.Marshal(config)
	if err != nil {
		return nil, err
	}

	result, err := a.Put(metricCID, cfg)
	if err != nil {
		return nil, err
	}

	metric := &Metric{}
	if err := json.Unmarshal(result, metric); err != nil {
		return nil, err
	}

	return metric, nil
}

// SearchMetrics returns list of metrics matching a search query and/or filter
//    - a search query (see: https://login.circonus.com/resources/api#searching)
//    - a filter (see: https://login.circonus.com/resources/api#filtering)
func (a *API) SearchMetrics(searchCriteria *SearchQueryType, filterCriteria *SearchFilterType) (*[]Metric, error) {
	q := url.Values{}

	if searchCriteria != nil && *searchCriteria != "" {
		q.Set("search", string(*searchCriteria))
	}

	if filterCriteria != nil && len(*filterCriteria) > 0 {
		for filter, criteria := range *filterCriteria {
			for _, val := range criteria {
				q.Add(filter, val)
			}
		}
	}

	if q.Encode() == "" {
		return a.FetchMetrics()
	}

	reqURL := url.URL{
		Path:     baseMetricPath,
		RawQuery: q.Encode(),
	}

	result, err := a.Get(reqURL.String())
	if err != nil {
		return nil, fmt.Errorf("[ERROR] API call error %+v", err)
	}

	var metrics []Metric
	if err := json.Unmarshal(result, &metrics); err != nil {
		return nil, err
	}

	return &metrics, nil
}
<|endoftext|>


<commit_before>package main

import (
	"bufio"
	"bytes"
	"database/sql"
	"encoding/gob"
	"encoding/xml"
	"errors"
	"fmt"
	"log"
	"net/http"
	"strings"
	"sync"
	"time"

	"github.com/inominate/apicache"

	"github.com/boltdb/bolt"
)

type purgeMember struct {
	Name      string
	Id        int64
	Joined    time.Time
	LastLogin time.Time
	ShipType  string
	Roles     bool
	Claimed   time.Time
	Stripped  time.Time
	Purged    bool
	Reason    string

	// May god have mercy on my soul.
	IsRegistered func() bool
}

type MemberTrackingMember struct {
	CharacterID    int64   `xml:"characterID,attr"`
	Name           string  `xml:"name,attr"`
	BaseID         int64   `xml:"baseID,attr"`
	Base           string  `xml:"base,attr"`
	Title          string  `xml:"title,attr"`
	StartDateTime  APITime `xml:"startDateTime,attr"`
	LogonDateTime  APITime `xml:"logonDateTime,attr"`
	LogoffDateTime APITime `xml:"logoffDateTime,attr"`
	LocationID     int64   `xml:"locationID,attr"`
	Location       string  `xml:"location,attr"`
	ShipTypeID     int64   `xml:"shipTypeID,attr"`
	ShipType       string  `xml:"shipType,attr"`
	Roles          int64   `xml:"roles,attr"`
	GrantableRoles int64   `xml:"grantableRoles,attr"`
}

func init() {
	gob.Register(purgeMember{})
}

var toBePurged = map[int64]*purgeMember{}
var purgeLock sync.RWMutex

var exemptHulls = []string{"Aeon", "Nyx", "Hel", "Wyvern", "Avatar", "Erebus",
	"Ragnarok", "Leviathan"}

func saveState() {
	purgeLock.Lock()
	defer purgeLock.Unlock()

	err := bdb.Update(func(tx *bolt.Tx) error {
		b, _ := tx.CreateBucketIfNotExists([]byte("purger"))

		buf := &bytes.Buffer{}
		g := gob.NewEncoder(buf)
		err := g.Encode(toBePurged)
		if err != nil {
			return err
		}

		return b.Put([]byte("state"), buf.Bytes())
	})
	if err != nil {
		log.Printf("Failed to save state: %s", err)
	}
}

func loadState() {
	purgeLock.Lock()
	defer purgeLock.Unlock()

	err := bdb.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte("purger"))
		if b == nil {
			return errors.New("Bucket does not exist.")
		}

		gobbed := b.Get([]byte("state"))
		if gobbed == nil {
			return errors.New("State not previously saved.")
		}

		buf := bytes.NewBuffer(gobbed)
		g := gob.NewDecoder(buf)
		err := g.Decode(&toBePurged)

		return err
	})
	if err != nil {
		log.Printf("Failed to load state: %s", err)
	}
}

func exempt(m MemberTrackingMember) bool {
	for _, role := range exemptRoles {
		if m.Roles&role == role {
			return true
		}
	}

	lowername := strings.ToLower(m.Name)
	for _, char := range exemptChars {
		if lowername == char {
			return true
		}
	}

	for _, ship := range exemptHulls {
		if m.ShipType == ship {
			return true
		}
	}
	return false
}

type SQLCorpMemberTracker struct {
	db         *sql.DB
	singleStmt *sql.Stmt
	allStmt    *sql.Stmt

	sync.RWMutex
}

func NewSQLCorpMemberTracker(db *sql.DB) *SQLCorpMemberTracker {
	s := SQLCorpMemberTracker{}
	s.db = db

	return &s
}

func (s *SQLCorpMemberTracker) IsRegistered(charName string) bool {
	var err error

	s.Lock()
	defer s.Unlock()

	if s.singleStmt == nil {
		query, _ := conf.String("registered_characters", "single_query")
		if query == "" {
			log.Fatalf("registered_characters dsn specified but no query found.")
		}

		s.singleStmt, err = s.db.Prepare(query)
		if err != nil {
			log.Fatalf("failed to prepare registered character query '%s': %s", query, err)
			return false
		}
	}

	var name string
	err = s.singleStmt.QueryRow(charName).Scan(&name)
	if err != nil {
		if err != sql.ErrNoRows {
			log.Printf("isRegisteredChar error: %s", err)
		}
		return false
	}

	if name == "" {
		log.Printf("got empty name for %s?", charName)
		return false
	}

	if name != charName {
		log.Printf("unexpected charName for isRegisteredChar (%s/%s)", name, charName)
		return false
	}

	return true
}

func (s *SQLCorpMemberTracker) GetMemberMap() (map[string]bool, error) {
	var err error

	s.Lock()
	defer s.Unlock()

	if s.allStmt == nil {
		query, _ := conf.String("registered_characters", "all_query")
		if query == "" {
			log.Fatalf("registered_characters dsn specified but no query found.")
		}

		s.allStmt, err = s.db.Prepare(query)
		if err != nil {
			return nil, fmt.Errorf("failed to prepare registered character query '%s': %s", query, err)
		}
	}

	registeredChars := make(map[string]bool)
	rows, err := s.allStmt.Query()
	if err != nil {
		return nil, fmt.Errorf("failed registered character query: %s", err)
	}

	var charName string
	for rows.Next() {
		err := rows.Scan(&charName)
		if err != nil {
			return nil, fmt.Errorf("failed scanning: %s", err)
		}

		registeredChars[strings.ToLower(charName)] = true
	}

	return registeredChars, nil
}

type CorpMemberTracker interface {
	GetMemberMap() (map[string]bool, error)
	IsRegistered(name string) bool
}

func membersUpdater(apiClient *apicache.Client, keyid int64, vcode string, maxIdle time.Duration, cmt CorpMemberTracker) {
	var registeredChars map[string]bool
	var err error

	memberReq := apiClient.NewRequest("/corp/MemberTracking.xml.aspx")
	memberReq.Set("keyid", fmt.Sprintf("%d", keyid))
	memberReq.Set("vcode", vcode)
	memberReq.Set("extended", "1")

	for {
		if cmt != nil {
			registeredChars, err = cmt.GetMemberMap()
			if err != nil {
				log.Printf("Error getting registered characters: %s", err)
			}
		}

		log.Printf("Pulling current corp member list.")
		resp, err := memberReq.Do()
		if err != nil {
			log.Printf("API Error: %s", err)
			time.Sleep(60 * time.Second)
			continue
		}

		type MemberTracking struct {
			Members []MemberTrackingMember `xml:"result>rowset>row"`
		}

		var members MemberTracking
		err = xml.Unmarshal(resp.Data, &members)
		if err != nil {
			log.Printf("API Error: %s", err)
			time.Sleep(60 * time.Second)
			continue
		}

		purgeLock.Lock()
		var newPurge = map[int64]*purgeMember{}
		var registered bool
		for _, mt := range members.Members {
			if registeredChars == nil {
				registered = true
			} else {
				_, registered = registeredChars[strings.ToLower(mt.Name)]
			}

			if time.Since(mt.LogonDateTime.Time) > maxIdle || !registered {
				if exempt(mt) {
					continue
				}

				var m purgeMember

				m = purgeMember{mt.Name, mt.CharacterID,
					mt.StartDateTime.Time, mt.LogonDateTime.Time,
					mt.ShipType, false, time.Time{}, time.Time{}, false, "", nil}

				if mt.Roles != 0 || mt.GrantableRoles != 0 {
					m.Roles = true
				}

				// Persist strip times and claim times
				if oldm, ok := toBePurged[mt.CharacterID]; ok {
					if !oldm.Stripped.IsZero() && !m.Roles {
						m.Stripped = oldm.Stripped
					}
					if !oldm.Claimed.IsZero() {
						m.Claimed = oldm.Claimed
					}
					if oldm.Roles == true && m.Roles == false {
						m.Stripped = time.Now()
					}
				}

				if time.Since(mt.LogonDateTime.Time) > maxIdle {
					m.Reason += fmt.Sprintf("Idle %s days. ", daysSince(mt.LogonDateTime.Time))
				}
				if !registered {
					m.Reason += "Unregistered."
					regName := m.Name
					m.IsRegistered = func() bool {
						return cmt.IsRegistered(regName)
					}
				}

				newPurge[mt.CharacterID] = &m
			}
		}

		toBePurged = newPurge
		purgeLock.Unlock()

		go saveState()

		log.Printf("Done. Next pull at %s", resp.Expires.Format(ApiDateTimeFormat))
		select {
		case <-time.After(resp.Expires.Sub(time.Now()) + 30*time.Second):
		}
	}
}

type HTTPCorpMemberTracker struct {
	url         string
	lastUpdate  time.Time
	cachedNames map[string]bool

	sync.RWMutex
}

func NewHTTPCorpMemberTracker(url string) *HTTPCorpMemberTracker {
	var n HTTPCorpMemberTracker
	n.url = url

	n.update()

	return &n
}

func (hcmt *HTTPCorpMemberTracker) update() {
	hcmt.Lock()
	defer hcmt.Unlock()

	if time.Since(hcmt.lastUpdate) < time.Hour {
		return
	}

	resp, err := http.Get(hcmt.url)
	if err != nil {
		log.Printf("Error getting registered member list: %s", err)
		return
	}
	defer resp.Body.Close()

	newNames := map[string]bool{}
	scanner := bufio.NewScanner(resp.Body)
	for scanner.Scan() {
		newNames[strings.ToLower(scanner.Text())] = true
	}
	if err := scanner.Err(); err != nil {
		log.Printf("Error reading registered member list: %s", err)
		return
	}

	hcmt.cachedNames = newNames
	hcmt.lastUpdate = time.Now()
}

func (hcmt *HTTPCorpMemberTracker) GetMemberMap() (map[string]bool, error) {
	hcmt.update()

	hcmt.RLock()
	defer hcmt.RUnlock()

	retMap := map[string]bool{}
	for k, v := range hcmt.cachedNames {
		retMap[k] = v
	}

	return retMap, nil
}

func (hcmt *HTTPCorpMemberTracker) IsRegistered(name string) bool {
	hcmt.update()

	hcmt.RLock()
	defer hcmt.RUnlock()

	_, ok := hcmt.cachedNames[name]
	return ok
}
<commit_msg>add failsafe<commit_after>package main

import (
	"bufio"
	"bytes"
	"database/sql"
	"encoding/gob"
	"encoding/xml"
	"errors"
	"fmt"
	"log"
	"net/http"
	"strings"
	"sync"
	"time"

	"github.com/inominate/apicache"

	"github.com/boltdb/bolt"
)

type purgeMember struct {
	Name      string
	Id        int64
	Joined    time.Time
	LastLogin time.Time
	ShipType  string
	Roles     bool
	Claimed   time.Time
	Stripped  time.Time
	Purged    bool
	Reason    string

	// May god have mercy on my soul.
	IsRegistered func() bool
}

type MemberTrackingMember struct {
	CharacterID    int64   `xml:"characterID,attr"`
	Name           string  `xml:"name,attr"`
	BaseID         int64   `xml:"baseID,attr"`
	Base           string  `xml:"base,attr"`
	Title          string  `xml:"title,attr"`
	StartDateTime  APITime `xml:"startDateTime,attr"`
	LogonDateTime  APITime `xml:"logonDateTime,attr"`
	LogoffDateTime APITime `xml:"logoffDateTime,attr"`
	LocationID     int64   `xml:"locationID,attr"`
	Location       string  `xml:"location,attr"`
	ShipTypeID     int64   `xml:"shipTypeID,attr"`
	ShipType       string  `xml:"shipType,attr"`
	Roles          int64   `xml:"roles,attr"`
	GrantableRoles int64   `xml:"grantableRoles,attr"`
}

func init() {
	gob.Register(purgeMember{})
}

var toBePurged = map[int64]*purgeMember{}
var purgeLock sync.RWMutex

var exemptHulls = []string{"Aeon", "Nyx", "Hel", "Wyvern", "Avatar", "Erebus",
	"Ragnarok", "Leviathan"}

func saveState() {
	purgeLock.Lock()
	defer purgeLock.Unlock()

	err := bdb.Update(func(tx *bolt.Tx) error {
		b, _ := tx.CreateBucketIfNotExists([]byte("purger"))

		buf := &bytes.Buffer{}
		g := gob.NewEncoder(buf)
		err := g.Encode(toBePurged)
		if err != nil {
			return err
		}

		return b.Put([]byte("state"), buf.Bytes())
	})
	if err != nil {
		log.Printf("Failed to save state: %s", err)
	}
}

func loadState() {
	purgeLock.Lock()
	defer purgeLock.Unlock()

	err := bdb.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte("purger"))
		if b == nil {
			return errors.New("Bucket does not exist.")
		}

		gobbed := b.Get([]byte("state"))
		if gobbed == nil {
			return errors.New("State not previously saved.")
		}

		buf := bytes.NewBuffer(gobbed)
		g := gob.NewDecoder(buf)
		err := g.Decode(&toBePurged)

		return err
	})
	if err != nil {
		log.Printf("Failed to load state: %s", err)
	}
}

func exempt(m MemberTrackingMember) bool {
	for _, role := range exemptRoles {
		if m.Roles&role == role {
			return true
		}
	}

	lowername := strings.ToLower(m.Name)
	for _, char := range exemptChars {
		if lowername == char {
			return true
		}
	}

	for _, ship := range exemptHulls {
		if m.ShipType == ship {
			return true
		}
	}
	return false
}

type SQLCorpMemberTracker struct {
	db         *sql.DB
	singleStmt *sql.Stmt
	allStmt    *sql.Stmt

	sync.RWMutex
}

func NewSQLCorpMemberTracker(db *sql.DB) *SQLCorpMemberTracker {
	s := SQLCorpMemberTracker{}
	s.db = db

	return &s
}

func (s *SQLCorpMemberTracker) IsRegistered(charName string) bool {
	var err error

	s.Lock()
	defer s.Unlock()

	if s.singleStmt == nil {
		query, _ := conf.String("registered_characters", "single_query")
		if query == "" {
			log.Fatalf("registered_characters dsn specified but no query found.")
		}

		s.singleStmt, err = s.db.Prepare(query)
		if err != nil {
			log.Fatalf("failed to prepare registered character query '%s': %s", query, err)
			return false
		}
	}

	var name string
	err = s.singleStmt.QueryRow(charName).Scan(&name)
	if err != nil {
		if err != sql.ErrNoRows {
			log.Printf("isRegisteredChar error: %s", err)
		}
		return false
	}

	if name == "" {
		log.Printf("got empty name for %s?", charName)
		return false
	}

	if name != charName {
		log.Printf("unexpected charName for isRegisteredChar (%s/%s)", name, charName)
		return false
	}

	return true
}

func (s *SQLCorpMemberTracker) GetMemberMap() (map[string]bool, error) {
	var err error

	s.Lock()
	defer s.Unlock()

	if s.allStmt == nil {
		query, _ := conf.String("registered_characters", "all_query")
		if query == "" {
			log.Fatalf("registered_characters dsn specified but no query found.")
		}

		s.allStmt, err = s.db.Prepare(query)
		if err != nil {
			return nil, fmt.Errorf("failed to prepare registered character query '%s': %s", query, err)
		}
	}

	registeredChars := make(map[string]bool)
	rows, err := s.allStmt.Query()
	if err != nil {
		return nil, fmt.Errorf("failed registered character query: %s", err)
	}

	var charName string
	for rows.Next() {
		err := rows.Scan(&charName)
		if err != nil {
			return nil, fmt.Errorf("failed scanning: %s", err)
		}

		registeredChars[strings.ToLower(charName)] = true
	}

	return registeredChars, nil
}

type CorpMemberTracker interface {
	GetMemberMap() (map[string]bool, error)
	IsRegistered(name string) bool
}

func membersUpdater(apiClient *apicache.Client, keyid int64, vcode string, maxIdle time.Duration, cmt CorpMemberTracker) {
	var registeredChars map[string]bool
	var err error

	memberReq := apiClient.NewRequest("/corp/MemberTracking.xml.aspx")
	memberReq.Set("keyid", fmt.Sprintf("%d", keyid))
	memberReq.Set("vcode", vcode)
	memberReq.Set("extended", "1")

	for {
		if cmt != nil {
			registeredChars, err = cmt.GetMemberMap()
			if err != nil {
				log.Printf("Error getting registered characters: %s", err)
			}
		}

		log.Printf("Pulling current corp member list.")
		resp, err := memberReq.Do()
		if err != nil {
			log.Printf("API Error: %s", err)
			time.Sleep(60 * time.Second)
			continue
		}

		type MemberTracking struct {
			Members []MemberTrackingMember `xml:"result>rowset>row"`
		}

		var members MemberTracking
		err = xml.Unmarshal(resp.Data, &members)
		if err != nil {
			log.Printf("API Error: %s", err)
			time.Sleep(60 * time.Second)
			continue
		}

		purgeLock.Lock()
		var newPurge = map[int64]*purgeMember{}
		var registered bool
		for _, mt := range members.Members {
			if registeredChars == nil {
				registered = true
			} else {
				_, registered = registeredChars[strings.ToLower(mt.Name)]
			}

			if time.Since(mt.LogonDateTime.Time) > maxIdle || !registered {
				if exempt(mt) {
					continue
				}

				var m purgeMember

				m = purgeMember{mt.Name, mt.CharacterID,
					mt.StartDateTime.Time, mt.LogonDateTime.Time,
					mt.ShipType, false, time.Time{}, time.Time{}, false, "", nil}

				if mt.Roles != 0 || mt.GrantableRoles != 0 {
					m.Roles = true
				}

				// Persist strip times and claim times
				if oldm, ok := toBePurged[mt.CharacterID]; ok {
					if !oldm.Stripped.IsZero() && !m.Roles {
						m.Stripped = oldm.Stripped
					}
					if !oldm.Claimed.IsZero() {
						m.Claimed = oldm.Claimed
					}
					if oldm.Roles == true && m.Roles == false {
						m.Stripped = time.Now()
					}
				}

				if time.Since(mt.LogonDateTime.Time) > maxIdle {
					m.Reason += fmt.Sprintf("Idle %s days. ", daysSince(mt.LogonDateTime.Time))
				}
				if !registered {
					m.Reason += "Unregistered."
					regName := m.Name
					m.IsRegistered = func() bool {
						return cmt.IsRegistered(regName)
					}
				}
				if m.Reason == "" {
					log.Printf("I'm supposed to kick %s but I don't know why.\n%#v", m.Name, m)
					continue
				}

				newPurge[mt.CharacterID] = &m
			}
		}

		toBePurged = newPurge
		purgeLock.Unlock()

		go saveState()

		log.Printf("Done. Next pull at %s", resp.Expires.Format(ApiDateTimeFormat))
		select {
		case <-time.After(resp.Expires.Sub(time.Now()) + 30*time.Second):
		}
	}
}

type HTTPCorpMemberTracker struct {
	url         string
	lastUpdate  time.Time
	cachedNames map[string]bool

	sync.RWMutex
}

func NewHTTPCorpMemberTracker(url string) *HTTPCorpMemberTracker {
	var n HTTPCorpMemberTracker
	n.url = url

	n.update()

	return &n
}

func (hcmt *HTTPCorpMemberTracker) update() {
	hcmt.Lock()
	defer hcmt.Unlock()

	if time.Since(hcmt.lastUpdate) < time.Hour {
		return
	}

	resp, err := http.Get(hcmt.url)
	if err != nil {
		log.Printf("Error getting registered member list: %s", err)
		return
	}
	defer resp.Body.Close()

	newNames := map[string]bool{}
	scanner := bufio.NewScanner(resp.Body)
	for scanner.Scan() {
		newNames[strings.ToLower(scanner.Text())] = true
	}
	if err := scanner.Err(); err != nil {
		log.Printf("Error reading registered member list: %s", err)
		return
	}

	hcmt.cachedNames = newNames
	hcmt.lastUpdate = time.Now()
}

func (hcmt *HTTPCorpMemberTracker) GetMemberMap() (map[string]bool, error) {
	hcmt.update()

	hcmt.RLock()
	defer hcmt.RUnlock()

	retMap := map[string]bool{}
	for k, v := range hcmt.cachedNames {
		retMap[k] = v
	}

	return retMap, nil
}

func (hcmt *HTTPCorpMemberTracker) IsRegistered(name string) bool {
	hcmt.update()

	hcmt.RLock()
	defer hcmt.RUnlock()

	_, ok := hcmt.cachedNames[name]
	return ok
}
<|endoftext|>