<commit_before>// Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may
// not use this file except in compliance with the License. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
// express or implied. See the License for the specific language governing
// permissions and limitations under the License.

package config

import (
	"os"
	"strings"
)

const (
	// AgentImageName is the name of the Docker image containing the Agent
	AgentImageName = "amazon/amazon-ecs-agent:latest"

	// AgentContainerName is the name of the Agent container started by this program
	AgentContainerName = "ecs-agent"

	// AgentLogFile is the name of the log file used by the Agent
	AgentLogFile = "ecs-agent.log"

	UnixSocketPrefix = "unix://"

	// Used to mount /proc for agent container
	ProcFS = "/proc"

	// AgentFilename is the filename, including version number, of the agent to be downloaded.
	AgentFilename = "ecs-agent-v1.16.1.tar"

	// DefaultRegionName is the name of the region to fall back to if no entry for the region name is found in the
	// S3BucketMap.
	DefaultRegionName = "default"
)

// regionToS3BucketURL provides a mapping of region names to specific URI's for the region.
var regionToS3BucketURL = map[string]string{
	"cn-north-1":      "https://s3.cn-north-1.amazonaws.com.cn/amazon-ecs-agent/",
	DefaultRegionName: "https://s3.amazonaws.com/amazon-ecs-agent/",
}

// AgentConfigDirectory returns the location on disk for configuration
func AgentConfigDirectory() string {
	return directoryPrefix + "/etc/ecs"
}

// AgentConfigFile returns the location of a file of environment variables passed to the Agent
func AgentConfigFile() string {
	return AgentConfigDirectory() + "/ecs.config"
}

// AgentJSONConfigFile returns the location of a file containing configuration expressed in JSON
func AgentJSONConfigFile() string {
	return AgentConfigDirectory() + "/ecs.config.json"
}

// LogDirectory returns the location on disk where logs should be placed
func LogDirectory() string {
	return directoryPrefix + "/var/log/ecs"
}

func initLogFile() string {
	return LogDirectory() + "/ecs-init.log"
}

// AgentDataDirectory returns the location on disk where state should be saved
func AgentDataDirectory() string {
	return directoryPrefix + "/var/lib/ecs/data"
}

// AgentDHClientLeasesDirectory returns the location on disk where dhclient
// leases information is tracked for ENIs attached to tasks
func AgentDHClientLeasesDirectory() string {
	return directoryPrefix + "/var/lib/ecs/dhclient"
}

// CacheDirectory returns the location on disk where Agent images should be cached
func CacheDirectory() string {
	return directoryPrefix + "/var/cache/ecs"
}

// CacheState returns the location on disk where cache state is stored
func CacheState() string {
	return CacheDirectory() + "/state"
}

// AgentTarball returns the location on disk of the cached Agent image
func AgentTarball() string {
	return CacheDirectory() + "/ecs-agent.tar"
}

// AgentRemoteTarball is the remote location of the Agent image, used for populating the cache. This is retrieved
// by region and the agent filename is appended.
func AgentRemoteTarball(region string) string {
	baseURI := getBaseLocationForRegion(region)
	return baseURI + AgentFilename
}

// AgentRemoteTarballMD5 is the remote location of a md5sum used to verify the integrity of the AgentRemoteTarball
func AgentRemoteTarballMD5(region string) string {
	return AgentRemoteTarball(region) + ".md5"
}

// DesiredImageLocatorFile returns the location on disk of a well-known file describing an Agent image to load
func DesiredImageLocatorFile() string {
	return CacheDirectory() + "/desired-image"
}

// DockerUnixSocket returns the docker socket endpoint and whether it's read from DOCKER_HOST
func DockerUnixSocket() (string, bool) {
	if dockerHost := os.Getenv("DOCKER_HOST"); strings.HasPrefix(dockerHost, UnixSocketPrefix) {
		return strings.TrimPrefix(dockerHost, UnixSocketPrefix), true
	}
	// return /var/run instead of /var/run/docker.sock, in case the /var/run/docker.sock is deleted and recreated outside the container,
	// eg: Docker daemon restart
	return "/var/run", false
}

// CgroupMountpoint returns the cgroup mountpoint for the system
func CgroupMountpoint() string {
	return cgroupMountpoint
}

// getBaseLocationForRegion fetches the bucket URI from list of S3 Buckets by region name or default if key is not found
func getBaseLocationForRegion(regionName string) string {
	s3BucketURL, ok := regionToS3BucketURL[regionName]
	if !ok {
		return regionToS3BucketURL[DefaultRegionName]
	}

	return s3BucketURL
}
<commit_msg>Add GovCloud agent bucket path.<commit_after>// Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may
// not use this file except in compliance with the License. A copy of the
// License is located at
//
// http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
// express or implied. See the License for the specific language governing
// permissions and limitations under the License.

package config

import (
	"os"
	"strings"
)

const (
	// AgentImageName is the name of the Docker image containing the Agent
	AgentImageName = "amazon/amazon-ecs-agent:latest"

	// AgentContainerName is the name of the Agent container started by this program
	AgentContainerName = "ecs-agent"

	// AgentLogFile is the name of the log file used by the Agent
	AgentLogFile = "ecs-agent.log"

	UnixSocketPrefix = "unix://"

	// Used to mount /proc for agent container
	ProcFS = "/proc"

	// AgentFilename is the filename, including version number, of the agent to be downloaded.
	AgentFilename = "ecs-agent-v1.16.1.tar"

	// DefaultRegionName is the name of the region to fall back to if no entry for the region name is found in the
	// S3BucketMap.
	DefaultRegionName = "default"
)

// regionToS3BucketURL provides a mapping of region names to specific URI's for the region.
var regionToS3BucketURL = map[string]string{
	"cn-north-1":      "https://s3.cn-north-1.amazonaws.com.cn/amazon-ecs-agent/",
	"us-gov-west-1":   "https://s3-fips-us-gov-west-1.amazonaws.com/amazon-ecs-agent/",
	DefaultRegionName: "https://s3.amazonaws.com/amazon-ecs-agent/",
}

// AgentConfigDirectory returns the location on disk for configuration
func AgentConfigDirectory() string {
	return directoryPrefix + "/etc/ecs"
}

// AgentConfigFile returns the location of a file of environment variables passed to the Agent
func AgentConfigFile() string {
	return AgentConfigDirectory() + "/ecs.config"
}

// AgentJSONConfigFile returns the location of a file containing configuration expressed in JSON
func AgentJSONConfigFile() string {
	return AgentConfigDirectory() + "/ecs.config.json"
}

// LogDirectory returns the location on disk where logs should be placed
func LogDirectory() string {
	return directoryPrefix + "/var/log/ecs"
}

func initLogFile() string {
	return LogDirectory() + "/ecs-init.log"
}

// AgentDataDirectory returns the location on disk where state should be saved
func AgentDataDirectory() string {
	return directoryPrefix + "/var/lib/ecs/data"
}

// AgentDHClientLeasesDirectory returns the location on disk where dhclient
// leases information is tracked for ENIs attached to tasks
func AgentDHClientLeasesDirectory() string {
	return directoryPrefix + "/var/lib/ecs/dhclient"
}

// CacheDirectory returns the location on disk where Agent images should be cached
func CacheDirectory() string {
	return directoryPrefix + "/var/cache/ecs"
}

// CacheState returns the location on disk where cache state is stored
func CacheState() string {
	return CacheDirectory() + "/state"
}

// AgentTarball returns the location on disk of the cached Agent image
func AgentTarball() string {
	return CacheDirectory() + "/ecs-agent.tar"
}

// AgentRemoteTarball is the remote location of the Agent image, used for populating the cache. This is retrieved
// by region and the agent filename is appended.
func AgentRemoteTarball(region string) string {
	baseURI := getBaseLocationForRegion(region)
	return baseURI + AgentFilename
}

// AgentRemoteTarballMD5 is the remote location of a md5sum used to verify the integrity of the AgentRemoteTarball
func AgentRemoteTarballMD5(region string) string {
	return AgentRemoteTarball(region) + ".md5"
}

// DesiredImageLocatorFile returns the location on disk of a well-known file describing an Agent image to load
func DesiredImageLocatorFile() string {
	return CacheDirectory() + "/desired-image"
}

// DockerUnixSocket returns the docker socket endpoint and whether it's read from DOCKER_HOST
func DockerUnixSocket() (string, bool) {
	if dockerHost := os.Getenv("DOCKER_HOST"); strings.HasPrefix(dockerHost, UnixSocketPrefix) {
		return strings.TrimPrefix(dockerHost, UnixSocketPrefix), true
	}
	// return /var/run instead of /var/run/docker.sock, in case the /var/run/docker.sock is deleted and recreated outside the container,
	// eg: Docker daemon restart
	return "/var/run", false
}

// CgroupMountpoint returns the cgroup mountpoint for the system
func CgroupMountpoint() string {
	return cgroupMountpoint
}

// getBaseLocationForRegion fetches the bucket URI from list of S3 Buckets by region name or default if key is not found
func getBaseLocationForRegion(regionName string) string {
	s3BucketURL, ok := regionToS3BucketURL[regionName]
	if !ok {
		return regionToS3BucketURL[DefaultRegionName]
	}

	return s3BucketURL
}
<|endoftext|>


<commit_before>package watch

import (
	"crypto/tls"
	"crypto/x509"
	"errors"
	"fmt"
	"net"
	"net/http"
	"net/http/cookiejar"
	"strconv"
	"strings"
	"time"

	"reflect"

	"github.com/foomo/petze/config"

	log "github.com/sirupsen/logrus"
)

var (
	typeDNSConfigErr                = reflect.TypeOf(&net.DNSConfigError{})
	typeDNSErr                      = reflect.TypeOf(&net.DNSError{})
	typeOpErr                       = reflect.TypeOf(&net.OpError{})
	typeX509CertificateInvalidError = reflect.TypeOf(x509.CertificateInvalidError{})
	typeX509HostnameError           = reflect.TypeOf(x509.HostnameError{})
	typeX509SystemRootsError        = reflect.TypeOf(x509.SystemRootsError{})
	typeX509UnknownAuthorityError   = reflect.TypeOf(x509.UnknownAuthorityError{})
)

type ErrorType string

const (
	ErrorInvalidEndpoint           ErrorType = "endpointInvalid"
	ErrorTypeServerTooSlow                   = "serverTooSlow"
	ErrorTypeNotImplemented                  = "notImplemented"
	ErrorTypeUnknownError                    = "unknownError"
	ErrorTypeClientError                     = "clientError"
	ErrorTypeDNS                             = "dns"
	ErrorTypeDNSConfig                       = "dnsConfig"
	ErrorTypeTLSCertificateInvalid           = "tlsCertificateInvalid"
	ErrorTypeTLSHostNameError                = "tlsHostNameError"
	ErrorTypeTLSSystemRootsError             = "tlsSystemRootsError"
	ErrorTypeTLSUnknownAuthority             = "tlsUnknownAutority"
	ErrorTypeWrongHTTPStatusCode             = "wrongHTTPStatus"
	ErrorTypeCertificateIsExpiring           = "certificateIsExpiring"
	ErrorTypeUnexpectedContentType           = "unexpectedContentType"
	ErrorTypeSessionFail                     = "sessionFail"
	ErrorTypeGoQueryMismatch                 = "goqueryMismatch"
	ErrorTypeGoQuery                         = "goQueryGeneralError"
	ErrorTypeDataMismatch                    = "dataMismatch"
	ErrorJsonPath                            = "jsonPathError"
	ErrorRegex                               = "regexError"
	ErrorBadResponseBody                     = "badResponseBody"
	ErrorTypeHeaderMismatch                  = "headerMismatch"
	ErrorTypeRedirectMismatch                = "redirectMismatch"
	ErrorTypeReplyMismatch                   = "replyMismatch"
)

type Error struct {
	Error    string    `json:"error"`
	Type     ErrorType `json:"type"`
	Comment  string    `json:"comment,omitempty"`
	Location string    `json:"location,omitempty"`
}

type Result struct {
	ID        string        `json:"id"`
	Errors    []Error       `json:"errors"`
	Timeout   bool          `json:"timeout"`
	Timestamp time.Time     `json:"timestamp"`
	RunTime   time.Duration `json:"runtime"`
}

func NewResult(id string) *Result {
	return &Result{
		ID:        id,
		Errors:    []Error{},
		Timestamp: time.Now(),
	}
}

func (r *Result) addError(e error, t ErrorType, comment string) {
	r.Errors = addError(r.Errors, e, t, comment)
}

func addError(errors []Error, err error, t ErrorType, comment string) []Error {
	return append(errors, Error{
		Error:   err.Error(),
		Type:    t,
		Comment: comment,
	})
}

type dialerErrRecorder struct {
	errors                     []Error
	unknownErr                 error
	err                        net.Error
	dnsError                   net.Error
	dnsConfigError             net.Error
	tlsCertificateInvalidError *x509.CertificateInvalidError
	tlsHostnameError           *x509.HostnameError
	tlsSystemRootsError        *x509.SystemRootsError
	tlsUnknownAuthorityError   *x509.UnknownAuthorityError
}

type Watcher struct {
	active                      bool
	service                     *config.Service

	// notifications
	didReceiveMailNotification  bool
	didReceiveSlackNotification bool
	didReceiveSMSNotification   bool
	lastErrors                  []Error
}

// Watch create a watcher and start watching
func Watch(service *config.Service, chanResult chan Result) *Watcher {

	w := &Watcher{
		active:  true,
		service: service,
	}
	go w.watchLoop(chanResult)
	return w
}

// Stop watching - beware this is async
func (w *Watcher) Stop() {
	w.active = false
}

func (w *Watcher) LastErrors() []Error {
	if w.lastErrors != nil {
		return w.lastErrors
	}
	return nil
}

func (w *Watcher) SetLastErrors(errs []Error) {
	w.lastErrors = errs
}

func (w *Watcher) watchLoop(chanResult chan Result) {
	httpClient, errRecorder := w.getClientAndDialErrRecorder()

	for w.active {
		r := w.watch(httpClient, errRecorder)
		if w.active {

			// send notifications
			w.smsNotify(r)
			w.mailNotify(r)
			w.slackNotify(r)

			chanResult <- *r
			time.Sleep(w.service.Interval)
		}
	}
}

func (w *Watcher) getClientAndDialErrRecorder() (client *http.Client, errRecorder *dialerErrRecorder) {
	errRecorder = &dialerErrRecorder{
		errors: []Error{},
	}
	tlsConfig := &tls.Config{}
	dialer := &net.Dialer{
		Timeout:   10 * time.Second,
		KeepAlive: 0 * time.Second,
	}
	dialTLS := func(network, address string) (conn net.Conn, err error) {
		tlsConn, tlsErr := tls.DialWithDialer(dialer, network, address, tlsConfig)
		if tlsErr == nil {
			//conn = tlsConn.(net.Conn)
			connectionState := tlsConn.ConnectionState()
			for _, cert := range connectionState.PeerCertificates {
				durationUntilExpiry := cert.NotAfter.Sub(time.Now())
				if durationUntilExpiry < w.service.TLSWarning {
					errRecorder.errors = addError(errRecorder.errors, errors.New(fmt.Sprint("cert CN=\"", cert.Subject.CommonName, "\" is expiring in less than "+strconv.FormatFloat(w.service.TLSWarning.Hours(), 'f', 0, 64)+"h: ", cert.NotAfter, ", left: ", strconv.FormatFloat(durationUntilExpiry.Hours(), 'f', 0, 64))), ErrorTypeCertificateIsExpiring, "")
				}
			}
			conn = tlsConn
		} else {

			switch reflect.TypeOf(tlsErr) {
			case typeX509UnknownAuthorityError:
				unknownAuthorityError := tlsErr.(x509.UnknownAuthorityError)
				errRecorder.tlsUnknownAuthorityError = &unknownAuthorityError
			case typeX509HostnameError:
				hostnameErr := tlsErr.(x509.HostnameError)
				errRecorder.tlsHostnameError = &hostnameErr
			case typeX509CertificateInvalidError:
				tlsCertificateInvalidError := tlsErr.(x509.CertificateInvalidError)
				errRecorder.tlsCertificateInvalidError = &tlsCertificateInvalidError
			case typeX509SystemRootsError:
				systemRootsError := tlsErr.(x509.SystemRootsError)
				errRecorder.tlsSystemRootsError = &systemRootsError
			default:
				log.Debug("unknown tls error", reflect.TypeOf(tlsErr), tlsErr)
			}
		}
		return conn, tlsErr
	}
	dial := func(network, address string) (conn net.Conn, err error) {
		conn, err = dialer.Dial(network, address)
		if err != nil {
			switch reflect.TypeOf(err) {
			case typeOpErr:
				opError := reflect.ValueOf(err).Elem().Interface().(net.OpError)
				switch reflect.TypeOf(opError.Err) {
				case typeDNSConfigErr:
					log.Error("dns config error")
					errRecorder.dnsConfigError = opError.Err.(net.Error)
				case typeDNSErr:
					log.Error("dns error")
					errRecorder.dnsError = opError.Err.(net.Error)
				default:
					errRecorder.unknownErr = opError.Err
				}
			default:
				log.Error("again some general bullshit", err)
				errRecorder.err = err.(net.Error)
			}
		}
		return
	}
	client = &http.Client{
		Transport: &http.Transport{
			Proxy:               http.ProxyFromEnvironment,
			Dial:                dial,
			DialTLS:             dialTLS,
			TLSHandshakeTimeout: 10 * time.Second,
			TLSClientConfig:     tlsConfig,
		},
		// do not follow redirects to allow checking the status code
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			return http.ErrUseLastResponse
		},
	}
	return
}

// actual watch
func (w *Watcher) watch(client *http.Client, errRecorder *dialerErrRecorder) (r *Result) {

	r = NewResult(w.service.ID)

	// parsing, the endpoint
	request, err := http.NewRequest("GET", w.service.Endpoint, nil)
	if err != nil {
		r.addError(err, ErrorInvalidEndpoint, "")
		return r
	}
	// my personal dns error check
	if len(request.Host) > 0 {
		host := request.Host
		parts := strings.Split(host, ":")
		if len(parts) > 1 {
			host, _, err = net.SplitHostPort(request.Host)
			if err != nil {
				r.addError(err, ErrorInvalidEndpoint, "")
				return
			}
		}
		_, lookupErr := net.LookupIP(host)
		if lookupErr != nil {
			r.addError(lookupErr, ErrorTypeDNS, "")
			return
		}
	}

	// i am explicitly not calling http.Get, because it does 30x handling and i do not want that
	response, err := client.Do(request)
	r.Errors = append(r.Errors, errRecorder.errors...)

	if response != nil && response.Body != nil {
		// always close the body
		response.Body.Close()
	}

	if err != nil {
		// sth. went wrong
		r.addError(err, ErrorTypeClientError, "")
		var netErr net.Error
		switch true {
		case errRecorder.tlsHostnameError != nil:
			r.addError(errRecorder.tlsHostnameError, ErrorTypeTLSHostNameError, "")
		case errRecorder.tlsSystemRootsError != nil:
			r.addError(errRecorder.tlsSystemRootsError, ErrorTypeTLSSystemRootsError, "")
		case errRecorder.tlsUnknownAuthorityError != nil:
			r.addError(errRecorder.tlsUnknownAuthorityError, ErrorTypeTLSUnknownAuthority, "")
		case errRecorder.tlsCertificateInvalidError != nil:
			r.addError(errRecorder.tlsCertificateInvalidError, ErrorTypeTLSCertificateInvalid, "")
		case errRecorder.unknownErr != nil:
			r.addError(errRecorder.unknownErr, ErrorTypeUnknownError, "")
		case errRecorder.dnsConfigError != nil:
			netErr = errRecorder.dnsConfigError
			r.addError(errRecorder.dnsConfigError, ErrorTypeDNSConfig, "")
		case errRecorder.dnsError != nil:
			netErr = errRecorder.dnsError
			r.addError(errRecorder.dnsError, ErrorTypeDNS, "")
		case errRecorder.err != nil:
			netErr = errRecorder.err
			r.addError(errRecorder.err, ErrorTypeUnknownError, "")
		}
		if netErr != nil {
			r.Timeout = netErr.Timeout()
		}
		return
	}

	// prepare to run the session with cookies
	cookieJar, _ := cookiejar.New(nil)
	client.Jar = cookieJar
	errSession := w.runSession(r, client)
	if errSession != nil {
		log.Error("session error", errSession)
		r.addError(errSession, ErrorTypeSessionFail, "")
	}
	r.RunTime = time.Since(r.Timestamp)

	// r.addError(errors.New(fmt.Sprint("response time too slow:", runTimeMilliseconds, ", should not be more than:", maxRuntime)), ErrorTypeServerTooSlow)
	//r.StatusCode = response.StatusCode
	return
}
<commit_msg>update cert expiry error to prevent repetitive alerts<commit_after>package watch

import (
	"crypto/tls"
	"crypto/x509"
	"errors"
	"fmt"
	"net"
	"net/http"
	"net/http/cookiejar"
	"strconv"
	"strings"
	"time"

	"reflect"

	"github.com/foomo/petze/config"

	log "github.com/sirupsen/logrus"
)

var (
	typeDNSConfigErr                = reflect.TypeOf(&net.DNSConfigError{})
	typeDNSErr                      = reflect.TypeOf(&net.DNSError{})
	typeOpErr                       = reflect.TypeOf(&net.OpError{})
	typeX509CertificateInvalidError = reflect.TypeOf(x509.CertificateInvalidError{})
	typeX509HostnameError           = reflect.TypeOf(x509.HostnameError{})
	typeX509SystemRootsError        = reflect.TypeOf(x509.SystemRootsError{})
	typeX509UnknownAuthorityError   = reflect.TypeOf(x509.UnknownAuthorityError{})
)

type ErrorType string

const (
	ErrorInvalidEndpoint           ErrorType = "endpointInvalid"
	ErrorTypeServerTooSlow                   = "serverTooSlow"
	ErrorTypeNotImplemented                  = "notImplemented"
	ErrorTypeUnknownError                    = "unknownError"
	ErrorTypeClientError                     = "clientError"
	ErrorTypeDNS                             = "dns"
	ErrorTypeDNSConfig                       = "dnsConfig"
	ErrorTypeTLSCertificateInvalid           = "tlsCertificateInvalid"
	ErrorTypeTLSHostNameError                = "tlsHostNameError"
	ErrorTypeTLSSystemRootsError             = "tlsSystemRootsError"
	ErrorTypeTLSUnknownAuthority             = "tlsUnknownAutority"
	ErrorTypeWrongHTTPStatusCode             = "wrongHTTPStatus"
	ErrorTypeCertificateIsExpiring           = "certificateIsExpiring"
	ErrorTypeUnexpectedContentType           = "unexpectedContentType"
	ErrorTypeSessionFail                     = "sessionFail"
	ErrorTypeGoQueryMismatch                 = "goqueryMismatch"
	ErrorTypeGoQuery                         = "goQueryGeneralError"
	ErrorTypeDataMismatch                    = "dataMismatch"
	ErrorJsonPath                            = "jsonPathError"
	ErrorRegex                               = "regexError"
	ErrorBadResponseBody                     = "badResponseBody"
	ErrorTypeHeaderMismatch                  = "headerMismatch"
	ErrorTypeRedirectMismatch                = "redirectMismatch"
	ErrorTypeReplyMismatch                   = "replyMismatch"
)

type Error struct {
	Error    string    `json:"error"`
	Type     ErrorType `json:"type"`
	Comment  string    `json:"comment,omitempty"`
	Location string    `json:"location,omitempty"`
}

type Result struct {
	ID        string        `json:"id"`
	Errors    []Error       `json:"errors"`
	Timeout   bool          `json:"timeout"`
	Timestamp time.Time     `json:"timestamp"`
	RunTime   time.Duration `json:"runtime"`
}

func NewResult(id string) *Result {
	return &Result{
		ID:        id,
		Errors:    []Error{},
		Timestamp: time.Now(),
	}
}

func (r *Result) addError(e error, t ErrorType, comment string) {
	r.Errors = addError(r.Errors, e, t, comment)
}

func addError(errors []Error, err error, t ErrorType, comment string) []Error {
	return append(errors, Error{
		Error:   err.Error(),
		Type:    t,
		Comment: comment,
	})
}

type dialerErrRecorder struct {
	errors                     []Error
	unknownErr                 error
	err                        net.Error
	dnsError                   net.Error
	dnsConfigError             net.Error
	tlsCertificateInvalidError *x509.CertificateInvalidError
	tlsHostnameError           *x509.HostnameError
	tlsSystemRootsError        *x509.SystemRootsError
	tlsUnknownAuthorityError   *x509.UnknownAuthorityError
}

type Watcher struct {
	active                      bool
	service                     *config.Service

	// notifications
	didReceiveMailNotification  bool
	didReceiveSlackNotification bool
	didReceiveSMSNotification   bool
	lastErrors                  []Error
}

// Watch create a watcher and start watching
func Watch(service *config.Service, chanResult chan Result) *Watcher {

	w := &Watcher{
		active:  true,
		service: service,
	}
	go w.watchLoop(chanResult)
	return w
}

// Stop watching - beware this is async
func (w *Watcher) Stop() {
	w.active = false
}

func (w *Watcher) LastErrors() []Error {
	if w.lastErrors != nil {
		return w.lastErrors
	}
	return nil
}

func (w *Watcher) SetLastErrors(errs []Error) {
	w.lastErrors = errs
}

func (w *Watcher) watchLoop(chanResult chan Result) {
	httpClient, errRecorder := w.getClientAndDialErrRecorder()

	for w.active {
		r := w.watch(httpClient, errRecorder)
		if w.active {

			// send notifications
			w.smsNotify(r)
			w.mailNotify(r)
			w.slackNotify(r)

			chanResult <- *r
			time.Sleep(w.service.Interval)
		}
	}
}

func (w *Watcher) getClientAndDialErrRecorder() (client *http.Client, errRecorder *dialerErrRecorder) {
	errRecorder = &dialerErrRecorder{
		errors: []Error{},
	}
	tlsConfig := &tls.Config{}
	dialer := &net.Dialer{
		Timeout:   10 * time.Second,
		KeepAlive: 0 * time.Second,
	}
	dialTLS := func(network, address string) (conn net.Conn, err error) {
		tlsConn, tlsErr := tls.DialWithDialer(dialer, network, address, tlsConfig)
		if tlsErr == nil {
			//conn = tlsConn.(net.Conn)
			connectionState := tlsConn.ConnectionState()
			for _, cert := range connectionState.PeerCertificates {
				durationUntilExpiry := cert.NotAfter.Sub(time.Now())
				if durationUntilExpiry < w.service.TLSWarning {
					var (
						prefix = "cert CN=\"" + cert.Subject.CommonName
						certErr = Error{
							Error:   errors.New(
								fmt.Sprint(
									"cert CN=\"",
									cert.Subject.CommonName,
									"\" is expiring in less than "+strconv.FormatFloat(w.service.TLSWarning.Hours(), 'f', 0, 64)+"h: ",
									cert.NotAfter,
									", left: ",
									strconv.FormatFloat(durationUntilExpiry.Hours(), 'f', 0, 64),
									" hours",
								),
							).Error(),
							Type:    ErrorTypeCertificateIsExpiring,
							Comment: "",
						}
						updatedCertError bool
					)

					// iterate over previously recorded errors
					// and update the error for the currently affected service if a certificate expiry warning is already present
					for i, e := range errRecorder.errors {
						if e.Type == ErrorTypeCertificateIsExpiring {
							if strings.HasPrefix(e.Error, prefix) {
								updatedCertError = true
								errRecorder.errors[i] = certErr
								break
							}
						}
					}

					// if the error has not been updated it needs to be added initially
					if !updatedCertError {
						errRecorder.errors = append(
							errRecorder.errors,
							certErr,
						)
					}
				}
			}
			conn = tlsConn
		} else {

			switch reflect.TypeOf(tlsErr) {
			case typeX509UnknownAuthorityError:
				unknownAuthorityError := tlsErr.(x509.UnknownAuthorityError)
				errRecorder.tlsUnknownAuthorityError = &unknownAuthorityError
			case typeX509HostnameError:
				hostnameErr := tlsErr.(x509.HostnameError)
				errRecorder.tlsHostnameError = &hostnameErr
			case typeX509CertificateInvalidError:
				tlsCertificateInvalidError := tlsErr.(x509.CertificateInvalidError)
				errRecorder.tlsCertificateInvalidError = &tlsCertificateInvalidError
			case typeX509SystemRootsError:
				systemRootsError := tlsErr.(x509.SystemRootsError)
				errRecorder.tlsSystemRootsError = &systemRootsError
			default:
				log.Debug("unknown tls error", reflect.TypeOf(tlsErr), tlsErr)
			}
		}
		return conn, tlsErr
	}
	dial := func(network, address string) (conn net.Conn, err error) {
		conn, err = dialer.Dial(network, address)
		if err != nil {
			switch reflect.TypeOf(err) {
			case typeOpErr:
				opError := reflect.ValueOf(err).Elem().Interface().(net.OpError)
				switch reflect.TypeOf(opError.Err) {
				case typeDNSConfigErr:
					log.Error("dns config error")
					errRecorder.dnsConfigError = opError.Err.(net.Error)
				case typeDNSErr:
					log.Error("dns error")
					errRecorder.dnsError = opError.Err.(net.Error)
				default:
					errRecorder.unknownErr = opError.Err
				}
			default:
				log.Error("again some general bullshit", err)
				errRecorder.err = err.(net.Error)
			}
		}
		return
	}
	client = &http.Client{
		Transport: &http.Transport{
			Proxy:               http.ProxyFromEnvironment,
			Dial:                dial,
			DialTLS:             dialTLS,
			TLSHandshakeTimeout: 10 * time.Second,
			TLSClientConfig:     tlsConfig,
		},
		// do not follow redirects to allow checking the status code
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			return http.ErrUseLastResponse
		},
	}
	return
}

// actual watch
func (w *Watcher) watch(client *http.Client, errRecorder *dialerErrRecorder) (r *Result) {

	r = NewResult(w.service.ID)

	// parsing, the endpoint
	request, err := http.NewRequest("GET", w.service.Endpoint, nil)
	if err != nil {
		r.addError(err, ErrorInvalidEndpoint, "")
		return r
	}
	// my personal dns error check
	if len(request.Host) > 0 {
		host := request.Host
		parts := strings.Split(host, ":")
		if len(parts) > 1 {
			host, _, err = net.SplitHostPort(request.Host)
			if err != nil {
				r.addError(err, ErrorInvalidEndpoint, "")
				return
			}
		}
		_, lookupErr := net.LookupIP(host)
		if lookupErr != nil {
			r.addError(lookupErr, ErrorTypeDNS, "")
			return
		}
	}

	// i am explicitly not calling http.Get, because it does 30x handling and i do not want that
	response, err := client.Do(request)
	r.Errors = append(r.Errors, errRecorder.errors...)

	if response != nil && response.Body != nil {
		// always close the body
		response.Body.Close()
	}

	if err != nil {
		// sth. went wrong
		r.addError(err, ErrorTypeClientError, "")
		var netErr net.Error
		switch true {
		case errRecorder.tlsHostnameError != nil:
			r.addError(errRecorder.tlsHostnameError, ErrorTypeTLSHostNameError, "")
		case errRecorder.tlsSystemRootsError != nil:
			r.addError(errRecorder.tlsSystemRootsError, ErrorTypeTLSSystemRootsError, "")
		case errRecorder.tlsUnknownAuthorityError != nil:
			r.addError(errRecorder.tlsUnknownAuthorityError, ErrorTypeTLSUnknownAuthority, "")
		case errRecorder.tlsCertificateInvalidError != nil:
			r.addError(errRecorder.tlsCertificateInvalidError, ErrorTypeTLSCertificateInvalid, "")
		case errRecorder.unknownErr != nil:
			r.addError(errRecorder.unknownErr, ErrorTypeUnknownError, "")
		case errRecorder.dnsConfigError != nil:
			netErr = errRecorder.dnsConfigError
			r.addError(errRecorder.dnsConfigError, ErrorTypeDNSConfig, "")
		case errRecorder.dnsError != nil:
			netErr = errRecorder.dnsError
			r.addError(errRecorder.dnsError, ErrorTypeDNS, "")
		case errRecorder.err != nil:
			netErr = errRecorder.err
			r.addError(errRecorder.err, ErrorTypeUnknownError, "")
		}
		if netErr != nil {
			r.Timeout = netErr.Timeout()
		}
		return
	}

	// prepare to run the session with cookies
	cookieJar, _ := cookiejar.New(nil)
	client.Jar = cookieJar
	errSession := w.runSession(r, client)
	if errSession != nil {
		log.Error("session error", errSession)
		r.addError(errSession, ErrorTypeSessionFail, "")
	}
	r.RunTime = time.Since(r.Timestamp)

	// r.addError(errors.New(fmt.Sprint("response time too slow:", runTimeMilliseconds, ", should not be more than:", maxRuntime)), ErrorTypeServerTooSlow)
	//r.StatusCode = response.StatusCode
	return
}
<|endoftext|>


<commit_before>package arn

import (
	"errors"
	"fmt"
	"sort"
	"sync"

	"github.com/aerogo/nano"
)

// AnimeList is a list of anime list items.
type AnimeList struct {
	UserID string           `json:"userId"`
	Items  []*AnimeListItem `json:"items"`

	sync.Mutex
}

// Add adds an anime to the list if it hasn't been added yet.
func (list *AnimeList) Add(animeID string) error {
	if list.Contains(animeID) {
		return errors.New("Anime " + animeID + " has already been added")
	}

	creationDate := DateTimeUTC()

	item := &AnimeListItem{
		AnimeID: animeID,
		Status:  AnimeListStatusPlanned,
		Rating:  AnimeListItemRating{},
		Created: creationDate,
		Edited:  creationDate,
	}

	if item.Anime() == nil {
		return errors.New("Invalid anime ID")
	}

	list.Lock()
	list.Items = append(list.Items, item)
	list.Unlock()

	return nil
}

// Remove removes the anime ID from the list.
func (list *AnimeList) Remove(animeID string) bool {
	list.Lock()
	defer list.Unlock()

	for index, item := range list.Items {
		if item.AnimeID == animeID {
			list.Items = append(list.Items[:index], list.Items[index+1:]...)
			return true
		}
	}

	return false
}

// Contains checks if the list contains the anime ID already.
func (list *AnimeList) Contains(animeID string) bool {
	list.Lock()
	defer list.Unlock()

	for _, item := range list.Items {
		if item.AnimeID == animeID {
			return true
		}
	}

	return false
}

// HasItemsWithStatus checks if the list contains an anime with the given status.
func (list *AnimeList) HasItemsWithStatus(status string) bool {
	list.Lock()
	defer list.Unlock()

	for _, item := range list.Items {
		if item.Status == status {
			return true
		}
	}

	return false
}

// Find returns the list item with the specified anime ID, if available.
func (list *AnimeList) Find(animeID string) *AnimeListItem {
	list.Lock()
	defer list.Unlock()

	for _, item := range list.Items {
		if item.AnimeID == animeID {
			return item
		}
	}

	return nil
}

// Import adds an anime to the list if it hasn't been added yet
// and if it did exist it will update episode, rating and notes.
func (list *AnimeList) Import(item *AnimeListItem) {
	existing := list.Find(item.AnimeID)

	// If it doesn't exist yet: Simply add it.
	if existing == nil {
		list.Lock()
		list.Items = append(list.Items, item)
		list.Unlock()
		return
	}

	// Temporary save it before changing the status
	// because status changes can modify the episode count.
	// This will prevent loss of "episodes watched" data.
	existingEpisodes := existing.Episodes

	// Status
	existing.Status = item.Status
	existing.OnStatusChange()

	// Episodes
	if item.Episodes > existingEpisodes {
		existing.Episodes = item.Episodes
	} else {
		existing.Episodes = existingEpisodes
	}

	existing.OnEpisodesChange()

	// Rating
	if existing.Rating.Overall == 0 {
		existing.Rating.Overall = item.Rating.Overall
		existing.Rating.Clamp()
	}

	if existing.Notes == "" {
		existing.Notes = item.Notes
	}

	if item.RewatchCount > existing.RewatchCount {
		existing.RewatchCount = item.RewatchCount
	}

	// Edited
	existing.Edited = DateTimeUTC()
}

// User returns the user this anime list belongs to.
func (list *AnimeList) User() *User {
	user, _ := GetUser(list.UserID)
	return user
}

// Sort ...
func (list *AnimeList) Sort() {
	list.Lock()
	defer list.Unlock()

	sort.Slice(list.Items, func(i, j int) bool {
		a := list.Items[i]
		b := list.Items[j]

		if (a.Status != AnimeListStatusWatching && a.Status != AnimeListStatusPlanned) && (b.Status != AnimeListStatusWatching && b.Status != AnimeListStatusPlanned) {
			if a.Rating.Overall == b.Rating.Overall {
				return a.Anime().Title.Canonical < b.Anime().Title.Canonical
			}

			return a.Rating.Overall > b.Rating.Overall
		}

		epsA := a.Anime().UpcomingEpisode()
		epsB := b.Anime().UpcomingEpisode()

		if epsA == nil && epsB == nil {
			if a.Rating.Overall == b.Rating.Overall {
				return a.Anime().Title.Canonical < b.Anime().Title.Canonical
			}

			return a.Rating.Overall > b.Rating.Overall
		}

		if epsA == nil {
			return false
		}

		if epsB == nil {
			return true
		}

		return epsA.Episode.AiringDate.Start < epsB.Episode.AiringDate.Start
	})
}

// SortByRating sorts the anime list by overall rating.
func (list *AnimeList) SortByRating() {
	list.Lock()
	defer list.Unlock()

	sort.Slice(list.Items, func(i, j int) bool {
		a := list.Items[i]
		b := list.Items[j]

		if a.Rating.Overall == b.Rating.Overall {
			return a.Anime().Title.Canonical < b.Anime().Title.Canonical
		}

		return a.Rating.Overall > b.Rating.Overall
	})
}

// Top returns the top entries.
func (list *AnimeList) Top(count int) []*AnimeListItem {
	list.Lock()
	defer list.Unlock()

	sort.Slice(list.Items, func(i, j int) bool {
		a := list.Items[i]
		b := list.Items[j]

		if a.Rating.Overall == b.Rating.Overall {
			return a.Anime().Title.Canonical < b.Anime().Title.Canonical
		}

		return a.Rating.Overall > b.Rating.Overall
	})

	if count > len(list.Items) {
		count = len(list.Items)
	}

	tmp := make([]*AnimeListItem, count)
	copy(tmp, list.Items[:count])
	return tmp
}

// Watching ...
func (list *AnimeList) Watching() *AnimeList {
	return list.FilterStatus(AnimeListStatusWatching)
}

// FilterStatus ...
func (list *AnimeList) FilterStatus(status string) *AnimeList {
	newList := &AnimeList{
		UserID: list.UserID,
		Items:  []*AnimeListItem{},
	}

	list.Lock()
	defer list.Unlock()

	for _, item := range list.Items {
		if item.Status == status {
			newList.Items = append(newList.Items, item)
		}
	}

	return newList
}

// WithoutPrivateItems returns a new anime list with the private items removed.
func (list *AnimeList) WithoutPrivateItems() *AnimeList {
	list.Lock()
	defer list.Unlock()

	newList := &AnimeList{
		UserID: list.UserID,
		Items:  make([]*AnimeListItem, 0, len(list.Items)),
	}

	for _, item := range list.Items {
		if !item.Private {
			newList.Items = append(newList.Items, item)
		}
	}

	return newList
}

// SplitByStatus splits the anime list into multiple ones by status.
func (list *AnimeList) SplitByStatus() map[string]*AnimeList {
	statusToList := map[string]*AnimeList{}

	statusToList[AnimeListStatusWatching] = &AnimeList{
		UserID: list.UserID,
		Items:  []*AnimeListItem{},
	}

	statusToList[AnimeListStatusCompleted] = &AnimeList{
		UserID: list.UserID,
		Items:  []*AnimeListItem{},
	}

	statusToList[AnimeListStatusPlanned] = &AnimeList{
		UserID: list.UserID,
		Items:  []*AnimeListItem{},
	}

	statusToList[AnimeListStatusHold] = &AnimeList{
		UserID: list.UserID,
		Items:  []*AnimeListItem{},
	}

	statusToList[AnimeListStatusDropped] = &AnimeList{
		UserID: list.UserID,
		Items:  []*AnimeListItem{},
	}

	list.Lock()
	defer list.Unlock()

	for _, item := range list.Items {
		statusList := statusToList[item.Status]
		statusList.Items = append(statusList.Items, item)
	}

	return statusToList
}

// NormalizeRatings normalizes all ratings so that they are perfectly stretched among the full scale.
func (list *AnimeList) NormalizeRatings() {
	list.Lock()
	defer list.Unlock()

	mapped := map[float64]float64{}
	all := []float64{}

	for _, item := range list.Items {
		// Zero rating counts as not rated
		if item.Rating.Overall == 0 {
			continue
		}

		_, found := mapped[item.Rating.Overall]

		if !found {
			mapped[item.Rating.Overall] = item.Rating.Overall
			all = append(all, item.Rating.Overall)
		}
	}

	sort.Slice(all, func(i, j int) bool {
		return all[i] < all[j]
	})

	count := len(all)

	// Prevent division by zero
	if count <= 1 {
		return
	}

	step := 9.9 / float64(count-1)
	currentRating := 0.1

	for _, rating := range all {
		mapped[rating] = currentRating
		currentRating += step
	}

	for _, item := range list.Items {
		item.Rating.Overall = mapped[item.Rating.Overall]
		item.Rating.Clamp()
	}
}

// TypeName returns the type name.
func (list *AnimeList) TypeName() string {
	return "AnimeList"
}

// Genres returns a map of genre names mapped to the list items that belong to that genre.
func (list *AnimeList) Genres() map[string][]*AnimeListItem {
	genreToListItems := map[string][]*AnimeListItem{}

	for _, item := range list.Items {
		for _, genre := range item.Anime().Genres {
			genreToListItems[genre] = append(genreToListItems[genre], item)
		}
	}

	return genreToListItems
}

// TopGenres returns the most liked genres for the user's anime list.
func (list *AnimeList) TopGenres(count int) []string {
	genreItems := list.Genres()
	genreAffinity := map[string]float64{}
	bestGenres := []string{}

	for genre, animeListItems := range genreItems {
		if genre == "Action" || genre == "Comedy" {
			continue
		}

		affinity := 0.0

		for _, item := range animeListItems {
			if item.Status != AnimeListStatusCompleted {
				continue
			}

			if item.Rating.Overall != 0 {
				affinity += item.Rating.Overall
			} else {
				affinity += 5.0
			}
		}

		genreAffinity[genre] = affinity
		bestGenres = append(bestGenres, genre)
	}

	sort.Slice(bestGenres, func(i, j int) bool {
		aAffinity := genreAffinity[bestGenres[i]]
		bAffinity := genreAffinity[bestGenres[j]]

		if aAffinity == bAffinity {
			return bestGenres[i] < bestGenres[j]
		}

		return aAffinity > bAffinity
	})

	if len(bestGenres) > count {
		bestGenres = bestGenres[:count]
	}

	return bestGenres
}

// RemoveDuplicates removes duplicate entries.
func (list *AnimeList) RemoveDuplicates() {
	list.Lock()
	defer list.Unlock()

	existed := map[string]bool{}
	newItems := make([]*AnimeListItem, 0, len(list.Items))

	for _, item := range list.Items {
		_, exists := existed[item.AnimeID]

		if exists {
			fmt.Println(list.User().Nick, "removed anime list item duplicate", item.AnimeID)
			continue
		}

		newItems = append(newItems, item)
		existed[item.AnimeID] = true
	}

	list.Items = newItems
}

// StreamAnimeLists returns a stream of all anime.
func StreamAnimeLists() chan *AnimeList {
	channel := make(chan *AnimeList, nano.ChannelBufferSize)

	go func() {
		for obj := range DB.All("AnimeList") {
			channel <- obj.(*AnimeList)
		}

		close(channel)
	}()

	return channel
}

// AllAnimeLists returns a slice of all anime.
func AllAnimeLists() ([]*AnimeList, error) {
	var all []*AnimeList

	stream := StreamAnimeLists()

	for obj := range stream {
		all = append(all, obj)
	}

	return all, nil
}

// GetAnimeList ...
func GetAnimeList(userID string) (*AnimeList, error) {
	animeList, err := DB.Get("AnimeList", userID)

	if err != nil {
		return nil, err
	}

	return animeList.(*AnimeList), nil
}
<commit_msg>Genres with 0 affinity should be excluded<commit_after>package arn

import (
	"errors"
	"fmt"
	"sort"
	"sync"

	"github.com/aerogo/nano"
)

// AnimeList is a list of anime list items.
type AnimeList struct {
	UserID string           `json:"userId"`
	Items  []*AnimeListItem `json:"items"`

	sync.Mutex
}

// Add adds an anime to the list if it hasn't been added yet.
func (list *AnimeList) Add(animeID string) error {
	if list.Contains(animeID) {
		return errors.New("Anime " + animeID + " has already been added")
	}

	creationDate := DateTimeUTC()

	item := &AnimeListItem{
		AnimeID: animeID,
		Status:  AnimeListStatusPlanned,
		Rating:  AnimeListItemRating{},
		Created: creationDate,
		Edited:  creationDate,
	}

	if item.Anime() == nil {
		return errors.New("Invalid anime ID")
	}

	list.Lock()
	list.Items = append(list.Items, item)
	list.Unlock()

	return nil
}

// Remove removes the anime ID from the list.
func (list *AnimeList) Remove(animeID string) bool {
	list.Lock()
	defer list.Unlock()

	for index, item := range list.Items {
		if item.AnimeID == animeID {
			list.Items = append(list.Items[:index], list.Items[index+1:]...)
			return true
		}
	}

	return false
}

// Contains checks if the list contains the anime ID already.
func (list *AnimeList) Contains(animeID string) bool {
	list.Lock()
	defer list.Unlock()

	for _, item := range list.Items {
		if item.AnimeID == animeID {
			return true
		}
	}

	return false
}

// HasItemsWithStatus checks if the list contains an anime with the given status.
func (list *AnimeList) HasItemsWithStatus(status string) bool {
	list.Lock()
	defer list.Unlock()

	for _, item := range list.Items {
		if item.Status == status {
			return true
		}
	}

	return false
}

// Find returns the list item with the specified anime ID, if available.
func (list *AnimeList) Find(animeID string) *AnimeListItem {
	list.Lock()
	defer list.Unlock()

	for _, item := range list.Items {
		if item.AnimeID == animeID {
			return item
		}
	}

	return nil
}

// Import adds an anime to the list if it hasn't been added yet
// and if it did exist it will update episode, rating and notes.
func (list *AnimeList) Import(item *AnimeListItem) {
	existing := list.Find(item.AnimeID)

	// If it doesn't exist yet: Simply add it.
	if existing == nil {
		list.Lock()
		list.Items = append(list.Items, item)
		list.Unlock()
		return
	}

	// Temporary save it before changing the status
	// because status changes can modify the episode count.
	// This will prevent loss of "episodes watched" data.
	existingEpisodes := existing.Episodes

	// Status
	existing.Status = item.Status
	existing.OnStatusChange()

	// Episodes
	if item.Episodes > existingEpisodes {
		existing.Episodes = item.Episodes
	} else {
		existing.Episodes = existingEpisodes
	}

	existing.OnEpisodesChange()

	// Rating
	if existing.Rating.Overall == 0 {
		existing.Rating.Overall = item.Rating.Overall
		existing.Rating.Clamp()
	}

	if existing.Notes == "" {
		existing.Notes = item.Notes
	}

	if item.RewatchCount > existing.RewatchCount {
		existing.RewatchCount = item.RewatchCount
	}

	// Edited
	existing.Edited = DateTimeUTC()
}

// User returns the user this anime list belongs to.
func (list *AnimeList) User() *User {
	user, _ := GetUser(list.UserID)
	return user
}

// Sort ...
func (list *AnimeList) Sort() {
	list.Lock()
	defer list.Unlock()

	sort.Slice(list.Items, func(i, j int) bool {
		a := list.Items[i]
		b := list.Items[j]

		if (a.Status != AnimeListStatusWatching && a.Status != AnimeListStatusPlanned) && (b.Status != AnimeListStatusWatching && b.Status != AnimeListStatusPlanned) {
			if a.Rating.Overall == b.Rating.Overall {
				return a.Anime().Title.Canonical < b.Anime().Title.Canonical
			}

			return a.Rating.Overall > b.Rating.Overall
		}

		epsA := a.Anime().UpcomingEpisode()
		epsB := b.Anime().UpcomingEpisode()

		if epsA == nil && epsB == nil {
			if a.Rating.Overall == b.Rating.Overall {
				return a.Anime().Title.Canonical < b.Anime().Title.Canonical
			}

			return a.Rating.Overall > b.Rating.Overall
		}

		if epsA == nil {
			return false
		}

		if epsB == nil {
			return true
		}

		return epsA.Episode.AiringDate.Start < epsB.Episode.AiringDate.Start
	})
}

// SortByRating sorts the anime list by overall rating.
func (list *AnimeList) SortByRating() {
	list.Lock()
	defer list.Unlock()

	sort.Slice(list.Items, func(i, j int) bool {
		a := list.Items[i]
		b := list.Items[j]

		if a.Rating.Overall == b.Rating.Overall {
			return a.Anime().Title.Canonical < b.Anime().Title.Canonical
		}

		return a.Rating.Overall > b.Rating.Overall
	})
}

// Top returns the top entries.
func (list *AnimeList) Top(count int) []*AnimeListItem {
	list.Lock()
	defer list.Unlock()

	sort.Slice(list.Items, func(i, j int) bool {
		a := list.Items[i]
		b := list.Items[j]

		if a.Rating.Overall == b.Rating.Overall {
			return a.Anime().Title.Canonical < b.Anime().Title.Canonical
		}

		return a.Rating.Overall > b.Rating.Overall
	})

	if count > len(list.Items) {
		count = len(list.Items)
	}

	tmp := make([]*AnimeListItem, count)
	copy(tmp, list.Items[:count])
	return tmp
}

// Watching ...
func (list *AnimeList) Watching() *AnimeList {
	return list.FilterStatus(AnimeListStatusWatching)
}

// FilterStatus ...
func (list *AnimeList) FilterStatus(status string) *AnimeList {
	newList := &AnimeList{
		UserID: list.UserID,
		Items:  []*AnimeListItem{},
	}

	list.Lock()
	defer list.Unlock()

	for _, item := range list.Items {
		if item.Status == status {
			newList.Items = append(newList.Items, item)
		}
	}

	return newList
}

// WithoutPrivateItems returns a new anime list with the private items removed.
func (list *AnimeList) WithoutPrivateItems() *AnimeList {
	list.Lock()
	defer list.Unlock()

	newList := &AnimeList{
		UserID: list.UserID,
		Items:  make([]*AnimeListItem, 0, len(list.Items)),
	}

	for _, item := range list.Items {
		if !item.Private {
			newList.Items = append(newList.Items, item)
		}
	}

	return newList
}

// SplitByStatus splits the anime list into multiple ones by status.
func (list *AnimeList) SplitByStatus() map[string]*AnimeList {
	statusToList := map[string]*AnimeList{}

	statusToList[AnimeListStatusWatching] = &AnimeList{
		UserID: list.UserID,
		Items:  []*AnimeListItem{},
	}

	statusToList[AnimeListStatusCompleted] = &AnimeList{
		UserID: list.UserID,
		Items:  []*AnimeListItem{},
	}

	statusToList[AnimeListStatusPlanned] = &AnimeList{
		UserID: list.UserID,
		Items:  []*AnimeListItem{},
	}

	statusToList[AnimeListStatusHold] = &AnimeList{
		UserID: list.UserID,
		Items:  []*AnimeListItem{},
	}

	statusToList[AnimeListStatusDropped] = &AnimeList{
		UserID: list.UserID,
		Items:  []*AnimeListItem{},
	}

	list.Lock()
	defer list.Unlock()

	for _, item := range list.Items {
		statusList := statusToList[item.Status]
		statusList.Items = append(statusList.Items, item)
	}

	return statusToList
}

// NormalizeRatings normalizes all ratings so that they are perfectly stretched among the full scale.
func (list *AnimeList) NormalizeRatings() {
	list.Lock()
	defer list.Unlock()

	mapped := map[float64]float64{}
	all := []float64{}

	for _, item := range list.Items {
		// Zero rating counts as not rated
		if item.Rating.Overall == 0 {
			continue
		}

		_, found := mapped[item.Rating.Overall]

		if !found {
			mapped[item.Rating.Overall] = item.Rating.Overall
			all = append(all, item.Rating.Overall)
		}
	}

	sort.Slice(all, func(i, j int) bool {
		return all[i] < all[j]
	})

	count := len(all)

	// Prevent division by zero
	if count <= 1 {
		return
	}

	step := 9.9 / float64(count-1)
	currentRating := 0.1

	for _, rating := range all {
		mapped[rating] = currentRating
		currentRating += step
	}

	for _, item := range list.Items {
		item.Rating.Overall = mapped[item.Rating.Overall]
		item.Rating.Clamp()
	}
}

// TypeName returns the type name.
func (list *AnimeList) TypeName() string {
	return "AnimeList"
}

// Genres returns a map of genre names mapped to the list items that belong to that genre.
func (list *AnimeList) Genres() map[string][]*AnimeListItem {
	genreToListItems := map[string][]*AnimeListItem{}

	for _, item := range list.Items {
		for _, genre := range item.Anime().Genres {
			genreToListItems[genre] = append(genreToListItems[genre], item)
		}
	}

	return genreToListItems
}

// TopGenres returns the most liked genres for the user's anime list.
func (list *AnimeList) TopGenres(count int) []string {
	genreItems := list.Genres()
	genreAffinity := map[string]float64{}
	bestGenres := []string{}

	for genre, animeListItems := range genreItems {
		if genre == "Action" || genre == "Comedy" {
			continue
		}

		affinity := 0.0

		for _, item := range animeListItems {
			if item.Status != AnimeListStatusCompleted {
				continue
			}

			if item.Rating.Overall != 0 {
				affinity += item.Rating.Overall
			} else {
				affinity += 5.0
			}
		}

		if affinity > 0 {
			genreAffinity[genre] = affinity
			bestGenres = append(bestGenres, genre)
		}
	}

	sort.Slice(bestGenres, func(i, j int) bool {
		aAffinity := genreAffinity[bestGenres[i]]
		bAffinity := genreAffinity[bestGenres[j]]

		if aAffinity == bAffinity {
			return bestGenres[i] < bestGenres[j]
		}

		return aAffinity > bAffinity
	})

	if len(bestGenres) > count {
		bestGenres = bestGenres[:count]
	}

	return bestGenres
}

// RemoveDuplicates removes duplicate entries.
func (list *AnimeList) RemoveDuplicates() {
	list.Lock()
	defer list.Unlock()

	existed := map[string]bool{}
	newItems := make([]*AnimeListItem, 0, len(list.Items))

	for _, item := range list.Items {
		_, exists := existed[item.AnimeID]

		if exists {
			fmt.Println(list.User().Nick, "removed anime list item duplicate", item.AnimeID)
			continue
		}

		newItems = append(newItems, item)
		existed[item.AnimeID] = true
	}

	list.Items = newItems
}

// StreamAnimeLists returns a stream of all anime.
func StreamAnimeLists() chan *AnimeList {
	channel := make(chan *AnimeList, nano.ChannelBufferSize)

	go func() {
		for obj := range DB.All("AnimeList") {
			channel <- obj.(*AnimeList)
		}

		close(channel)
	}()

	return channel
}

// AllAnimeLists returns a slice of all anime.
func AllAnimeLists() ([]*AnimeList, error) {
	var all []*AnimeList

	stream := StreamAnimeLists()

	for obj := range stream {
		all = append(all, obj)
	}

	return all, nil
}

// GetAnimeList ...
func GetAnimeList(userID string) (*AnimeList, error) {
	animeList, err := DB.Get("AnimeList", userID)

	if err != nil {
		return nil, err
	}

	return animeList.(*AnimeList), nil
}
<|endoftext|>


<commit_before>package arn

import (
	"sort"
	"time"
)

const (
	currentlyAiringBonus      = 5.0
	longSummaryBonus          = 0.1
	popularityThreshold       = 5
	popularityPenalty         = 8.0
	watchingPopularityWeight  = 0.25
	completedPopularityWeight = watchingPopularityWeight
	plannedPopularityWeight   = watchingPopularityWeight * (2.0 / 3.0)
	droppedPopularityWeight   = -plannedPopularityWeight
	visualsWeight             = 0.005
	storyWeight               = 0.005
	soundtrackWeight          = 0.005
	movieBonus                = 0.25
	agePenalty                = 11.0
	ageThreshold              = 6 * 30 * 24 * time.Hour
)

// SortAnimeByPopularity sorts the given slice of anime by popularity.
func SortAnimeByPopularity(animes []*Anime) {
	sort.Slice(animes, func(i, j int) bool {
		aPopularity := animes[i].Popularity.Total()
		bPopularity := animes[j].Popularity.Total()

		if aPopularity == bPopularity {
			return animes[i].Title.Canonical < animes[j].Title.Canonical
		}

		return aPopularity > bPopularity
	})
}

// SortAnimeByQuality sorts the given slice of anime by quality.
func SortAnimeByQuality(animes []*Anime) {
	SortAnimeByQualityDetailed(animes, "")
}

// SortAnimeByQualityDetailed sorts the given slice of anime by quality.
func SortAnimeByQualityDetailed(animes []*Anime, filterStatus string) {
	sort.Slice(animes, func(i, j int) bool {
		a := animes[i]
		b := animes[j]

		scoreA := a.Rating.Overall
		scoreB := b.Rating.Overall

		scoreA += a.Rating.Story * storyWeight
		scoreB += b.Rating.Story * storyWeight

		scoreA += a.Rating.Visuals * visualsWeight
		scoreB += b.Rating.Visuals * visualsWeight

		scoreA += a.Rating.Soundtrack * soundtrackWeight
		scoreB += b.Rating.Soundtrack * soundtrackWeight

		if a.Status == "current" {
			scoreA += currentlyAiringBonus
		}

		if b.Status == "current" {
			scoreB += currentlyAiringBonus
		}

		if a.Type == "movie" {
			scoreA += movieBonus
		}

		if b.Type == "movie" {
			scoreB += movieBonus
		}

		if a.Popularity.Total() < popularityThreshold {
			scoreA -= popularityPenalty
		}

		if b.Popularity.Total() < popularityThreshold {
			scoreB -= popularityPenalty
		}

		if len(a.Summary) >= 140 {
			scoreA += longSummaryBonus
		}

		if len(b.Summary) >= 140 {
			scoreB += longSummaryBonus
		}

		// If we show currently running shows, rank shows that started a long time ago a bit lower
		if filterStatus == "current" {
			if a.StartDate != "" && time.Since(a.StartDateTime()) > ageThreshold {
				scoreA -= agePenalty
			}

			if b.StartDate != "" && time.Since(b.StartDateTime()) > ageThreshold {
				scoreB -= agePenalty
			}
		}

		scoreA += float64(a.Popularity.Watching) * watchingPopularityWeight
		scoreB += float64(b.Popularity.Watching) * watchingPopularityWeight

		scoreA += float64(a.Popularity.Planned) * plannedPopularityWeight
		scoreB += float64(b.Popularity.Planned) * plannedPopularityWeight

		scoreA += float64(a.Popularity.Completed) * completedPopularityWeight
		scoreB += float64(b.Popularity.Completed) * completedPopularityWeight

		scoreA += float64(a.Popularity.Dropped) * droppedPopularityWeight
		scoreB += float64(b.Popularity.Dropped) * droppedPopularityWeight

		if scoreA == scoreB {
			return a.Title.Canonical < b.Title.Canonical
		}

		return scoreA > scoreB
	})
}
<commit_msg>Changed weightings<commit_after>package arn

import (
	"sort"
	"time"
)

const (
	currentlyAiringBonus      = 5.0
	longSummaryBonus          = 0.1
	popularityThreshold       = 5
	popularityPenalty         = 8.0
	watchingPopularityWeight  = 0.25
	completedPopularityWeight = watchingPopularityWeight
	plannedPopularityWeight   = watchingPopularityWeight * (2.0 / 3.0)
	droppedPopularityWeight   = -plannedPopularityWeight
	visualsWeight             = 0.0075
	storyWeight               = 0.0075
	soundtrackWeight          = 0.0075
	movieBonus                = 0.28
	agePenalty                = 11.0
	ageThreshold              = 6 * 30 * 24 * time.Hour
)

// SortAnimeByPopularity sorts the given slice of anime by popularity.
func SortAnimeByPopularity(animes []*Anime) {
	sort.Slice(animes, func(i, j int) bool {
		aPopularity := animes[i].Popularity.Total()
		bPopularity := animes[j].Popularity.Total()

		if aPopularity == bPopularity {
			return animes[i].Title.Canonical < animes[j].Title.Canonical
		}

		return aPopularity > bPopularity
	})
}

// SortAnimeByQuality sorts the given slice of anime by quality.
func SortAnimeByQuality(animes []*Anime) {
	SortAnimeByQualityDetailed(animes, "")
}

// SortAnimeByQualityDetailed sorts the given slice of anime by quality.
func SortAnimeByQualityDetailed(animes []*Anime, filterStatus string) {
	sort.Slice(animes, func(i, j int) bool {
		a := animes[i]
		b := animes[j]

		scoreA := a.Rating.Overall
		scoreB := b.Rating.Overall

		scoreA += a.Rating.Story * storyWeight
		scoreB += b.Rating.Story * storyWeight

		scoreA += a.Rating.Visuals * visualsWeight
		scoreB += b.Rating.Visuals * visualsWeight

		scoreA += a.Rating.Soundtrack * soundtrackWeight
		scoreB += b.Rating.Soundtrack * soundtrackWeight

		if a.Status == "current" {
			scoreA += currentlyAiringBonus
		}

		if b.Status == "current" {
			scoreB += currentlyAiringBonus
		}

		if a.Type == "movie" {
			scoreA += movieBonus
		}

		if b.Type == "movie" {
			scoreB += movieBonus
		}

		if a.Popularity.Total() < popularityThreshold {
			scoreA -= popularityPenalty
		}

		if b.Popularity.Total() < popularityThreshold {
			scoreB -= popularityPenalty
		}

		if len(a.Summary) >= 140 {
			scoreA += longSummaryBonus
		}

		if len(b.Summary) >= 140 {
			scoreB += longSummaryBonus
		}

		// If we show currently running shows, rank shows that started a long time ago a bit lower
		if filterStatus == "current" {
			if a.StartDate != "" && time.Since(a.StartDateTime()) > ageThreshold {
				scoreA -= agePenalty
			}

			if b.StartDate != "" && time.Since(b.StartDateTime()) > ageThreshold {
				scoreB -= agePenalty
			}
		}

		scoreA += float64(a.Popularity.Watching) * watchingPopularityWeight
		scoreB += float64(b.Popularity.Watching) * watchingPopularityWeight

		scoreA += float64(a.Popularity.Planned) * plannedPopularityWeight
		scoreB += float64(b.Popularity.Planned) * plannedPopularityWeight

		scoreA += float64(a.Popularity.Completed) * completedPopularityWeight
		scoreB += float64(b.Popularity.Completed) * completedPopularityWeight

		scoreA += float64(a.Popularity.Dropped) * droppedPopularityWeight
		scoreB += float64(b.Popularity.Dropped) * droppedPopularityWeight

		if scoreA == scoreB {
			return a.Title.Canonical < b.Title.Canonical
		}

		return scoreA > scoreB
	})
}
<|endoftext|>


<commit_before>// Package listing is a generate subcommand that scans
// requested directory and generates a .go file with
// a list of all found files.
package listing

import (
	"os"
	"path/filepath"

	"github.com/anonx/sunplate/command"
	"github.com/anonx/sunplate/generation/output"
	"github.com/anonx/sunplate/log"
	p "github.com/anonx/sunplate/path"
)

// Start is an entry point of listing subcommand.
// It expects two parameters.
// basePath is where to find files necessary for generation of listing.
// params is a map with the following keys:
// --path defines what directory to analyze ("./views" by-default).
// --output is a path to directory where to create a new package ("./assets" by-default).
// --package is what package should be created as a result ("views" by-default).
func Start(params command.Data) {
	inputDir := params.Default("--input", "./views")
	outputDir := params.Default("--output", "./assets/views")
	outPkg := params.Default("--package", "views")

	// Start search of files.
	fs, fn := walkFunc(inputDir)
	filepath.Walk(inputDir, fn)

	// Generate and save a new package.
	t := output.NewType(
		outPkg, filepath.Join(
			p.SunplateDir("generation", "listing"), "./listing.go.template",
		),
	)
	t.CreateDir(outputDir)
	t.Extension = ".go" // Save generated file as a .go source.
	t.Context = map[string]interface{}{
		"files": fs,
		"input": params.Default("--path", "./views"),
	}
	t.Generate()
}

// walkFunc returns a map of files and a function that may be used for validation
// of found files. Successfully validated ones are stored to the files variable.
func walkFunc(dir string) (map[string]string, func(string, os.FileInfo, error) error) {
	files := map[string]string{}
	dir = p.Prefixless(dir, "./") + "/" // This is required to get right file names.

	return files, func(path string, info os.FileInfo, err error) error {
		// Make sure there are no any errors.
		if err != nil {
			log.Warn.Printf(`An error occured while creating a listing: "%s".`, err)
			return err
		}

		// Assure it is not a directory.
		if info.IsDir() {
			return nil
		}

		// Add files to the list.
		log.Trace.Printf(`Path "%s" discovered.`, path)
		files[p.Prefixless(path, dir)] = path
		return nil
	}
}
<commit_msg>Fixing paths generated by listing subcommand<commit_after>// Package listing is a generate subcommand that scans
// requested directory and generates a .go file with
// a list of all found files.
package listing

import (
	"os"
	"path/filepath"

	"github.com/anonx/sunplate/command"
	"github.com/anonx/sunplate/generation/output"
	"github.com/anonx/sunplate/log"
	p "github.com/anonx/sunplate/path"
)

// Start is an entry point of listing subcommand.
// It expects two parameters.
// basePath is where to find files necessary for generation of listing.
// params is a map with the following keys:
// --root defines a directory of the project, paths are relative to it (default is "./").
// --path defines what directory to analyze ("./views" by-default).
// --output is a path to directory where to create a new package ("./assets" by-default).
// --package is what package should be created as a result ("views" by-default).
func Start(params command.Data) {
	rootDir := params.Default("--root", "./")
	inputDir := params.Default("--input", "./views")
	outputDir := params.Default("--output", "./assets/views")
	outPkg := params.Default("--package", "views")

	// Start search of files.
	fs, fn := walkFunc(rootDir, inputDir)
	filepath.Walk(inputDir, fn)

	// Generate and save a new package.
	t := output.NewType(
		outPkg, filepath.Join(
			p.SunplateDir("generation", "listing"), "./listing.go.template",
		),
	)
	t.CreateDir(outputDir)
	t.Extension = ".go" // Save generated file as a .go source.
	t.Context = map[string]interface{}{
		"files": fs,
		"input": params.Default("--path", "./views"),
	}
	t.Generate()
}

// walkFunc returns a map of files and a function that may be used for validation
// of found files. Successfully validated ones are stored to the files variable.
func walkFunc(root, dir string) (map[string]string, func(string, os.FileInfo, error) error) {
	files := map[string]string{}
	dir = p.Prefixless(dir, "./") + "/" // This is required to get right file names.

	return files, func(path string, info os.FileInfo, err error) error {
		// Make sure there are no any errors.
		if err != nil {
			log.Warn.Printf(`An error occured while creating a listing: "%s".`, err)
			return err
		}

		// Assure it is not a directory.
		if info.IsDir() {
			return nil
		}

		// Add files to the list.
		// We are using dirty hack to let it possible use
		// --input and --output parameters relative to any directory
		// but have result relative to the root directory.
		log.Trace.Printf(`Path "%s" discovered.`, path)
		r := p.Prefixless(p.Prefixless(
			p.AbsoluteImport(path),
			p.AbsoluteImport(root),
		), "/")
		log.Trace.Printf(`Saving it as "%s"...`, r)
		files[p.Prefixless(path, dir)] = r
		return nil
	}
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"os"
	"strconv"
)

func getInput(day int) string {
	data, err := Asset("data/day" + strconv.Itoa(day) + "input.txt")
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	return string(data[:])
}
<commit_msg>automatically trim whitespace from input<commit_after>package main

import (
	"fmt"
	"os"
	"strconv"
	"strings"
)

func getInput(day int) string {
	data, err := Asset("data/day" + strconv.Itoa(day) + "input.txt")
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	return strings.TrimSpace(string(data[:]))
}
<|endoftext|>


<commit_before>package slackboard

const (
	Version = "0.5.2"
)
<commit_msg>bumped version to 0.6.0.<commit_after>package slackboard

const (
	Version = "0.6.0"
)
<|endoftext|>


<commit_before>package libkb

import (
	"errors"
	"fmt"
	"strings"

	keybase1 "github.com/keybase/client/go/protocol"
)

func GetKeybasePassphrase(ui SecretUI, username, retryMsg string) (keybase1.GetPassphraseRes, error) {
	arg := DefaultPassphraseArg()
	arg.WindowTitle = "Keybase passphrase"
	arg.Type = keybase1.PassphraseType_PASS_PHRASE
	arg.Prompt = fmt.Sprintf("Please enter the Keybase passphrase for %s (12+ characters)", username)
	arg.RetryLabel = retryMsg
	return GetPassphraseUntilCheck(arg, newUIPrompter(ui), &CheckPassphraseSimple)
}

func GetSecret(ui SecretUI, title, prompt, retryMsg string, allowSecretStore bool) (keybase1.GetPassphraseRes, error) {
	arg := DefaultPassphraseArg()
	arg.WindowTitle = title
	arg.Type = keybase1.PassphraseType_PASS_PHRASE
	arg.Prompt = prompt
	arg.RetryLabel = retryMsg
	// apparently allowSecretStore can be true even though HasSecretStore()
	// is false (in the case of mocked secret store tests on linux, for
	// example). So, pass this through:
	arg.Features.StoreSecret.Allow = allowSecretStore
	return GetPassphraseUntilCheck(arg, newUIPrompter(ui), &CheckPassphraseSimple)
}

func GetPaperKeyPassphrase(ui SecretUI, username string) (string, error) {
	arg := DefaultPassphraseArg()
	arg.WindowTitle = "Paper backup key passphrase"
	arg.Type = keybase1.PassphraseType_PAPER_KEY
	if len(username) == 0 {
		username = "your account"
	}
	arg.Prompt = fmt.Sprintf("Please enter a paper backup key passphrase for %s", username)
	arg.Features.StoreSecret.Allow = false
	arg.Features.StoreSecret.Readonly = true
	res, err := GetPassphraseUntilCheck(arg, newUIPrompter(ui), &CheckPassphraseSimple)
	if err != nil {
		return "", err
	}
	return res.Passphrase, nil
}

func GetPaperKeyForCryptoPassphrase(ui SecretUI, reason string, devices []*Device) (string, error) {
	if len(devices) == 0 {
		return "", errors.New("empty device list")
	}
	arg := DefaultPassphraseArg()
	arg.WindowTitle = "Paper backup key passphrase"
	arg.Type = keybase1.PassphraseType_PAPER_KEY
	arg.Features.StoreSecret.Allow = false
	arg.Features.StoreSecret.Readonly = true
	if len(devices) == 1 {
		arg.Prompt = fmt.Sprintf("%s: please enter the paper key '%s...'", reason, *devices[0].Description)
	} else {
		descs := make([]string, len(devices))
		for i, dev := range devices {
			descs[i] = fmt.Sprintf("'%s...'", *dev.Description)
		}
		paperOpts := strings.Join(descs, " or ")
		arg.Prompt = fmt.Sprintf("%s: please enter one of the following paper keys %s", reason, paperOpts)
	}

	res, err := GetPassphraseUntilCheck(arg, newUIPrompter(ui), &CheckPassphraseSimple)
	if err != nil {
		return "", err
	}
	return res.Passphrase, nil
}

type PassphrasePrompter interface {
	Prompt(keybase1.GUIEntryArg) (keybase1.GetPassphraseRes, error)
}

type uiPrompter struct {
	ui SecretUI
}

var _ PassphrasePrompter = &uiPrompter{}

func newUIPrompter(ui SecretUI) *uiPrompter {
	return &uiPrompter{ui: ui}
}

func (u *uiPrompter) Prompt(arg keybase1.GUIEntryArg) (keybase1.GetPassphraseRes, error) {
	return u.ui.GetPassphrase(arg, nil)
}

func GetPassphraseUntilCheck(arg keybase1.GUIEntryArg, prompter PassphrasePrompter, checker *Checker) (keybase1.GetPassphraseRes, error) {
	for i := 0; i < 10; i++ {
		res, err := prompter.Prompt(arg)
		if err != nil {
			return keybase1.GetPassphraseRes{}, err
		}
		if checker == nil {
			return res, nil
		}
		if checker.F(res.Passphrase) {
			return res, nil
		}
		arg.RetryLabel = checker.Hint
	}

	return keybase1.GetPassphraseRes{}, RetryExhaustedError{}
}

func DefaultPassphraseArg() keybase1.GUIEntryArg {
	return keybase1.GUIEntryArg{
		SubmitLabel: "Submit",
		CancelLabel: "Cancel",
		Features: keybase1.GUIEntryFeatures{
			ShowTyping: keybase1.Feature{
				Allow:        true,
				DefaultValue: false,
				Readonly:     true,
				Label:        "Show typing",
			},
			StoreSecret: keybase1.Feature{
				Allow:        HasSecretStore(),
				DefaultValue: false,
				Readonly:     false,
				Label:        "Save in Keychain",
			},
		},
	}
}
<commit_msg>Show typing for paper key pinentry<commit_after>package libkb

import (
	"errors"
	"fmt"
	"strings"

	keybase1 "github.com/keybase/client/go/protocol"
)

func GetKeybasePassphrase(ui SecretUI, username, retryMsg string) (keybase1.GetPassphraseRes, error) {
	arg := DefaultPassphraseArg()
	arg.WindowTitle = "Keybase passphrase"
	arg.Type = keybase1.PassphraseType_PASS_PHRASE
	arg.Prompt = fmt.Sprintf("Please enter the Keybase passphrase for %s (12+ characters)", username)
	arg.RetryLabel = retryMsg
	return GetPassphraseUntilCheck(arg, newUIPrompter(ui), &CheckPassphraseSimple)
}

func GetSecret(ui SecretUI, title, prompt, retryMsg string, allowSecretStore bool) (keybase1.GetPassphraseRes, error) {
	arg := DefaultPassphraseArg()
	arg.WindowTitle = title
	arg.Type = keybase1.PassphraseType_PASS_PHRASE
	arg.Prompt = prompt
	arg.RetryLabel = retryMsg
	// apparently allowSecretStore can be true even though HasSecretStore()
	// is false (in the case of mocked secret store tests on linux, for
	// example). So, pass this through:
	arg.Features.StoreSecret.Allow = allowSecretStore
	return GetPassphraseUntilCheck(arg, newUIPrompter(ui), &CheckPassphraseSimple)
}

func GetPaperKeyPassphrase(ui SecretUI, username string) (string, error) {
	arg := DefaultPassphraseArg()
	arg.WindowTitle = "Paper backup key passphrase"
	arg.Type = keybase1.PassphraseType_PAPER_KEY
	if len(username) == 0 {
		username = "your account"
	}
	arg.Prompt = fmt.Sprintf("Please enter a paper backup key passphrase for %s", username)
	arg.Features.StoreSecret.Allow = false
	arg.Features.StoreSecret.Readonly = true
	arg.Features.ShowTyping.Allow = true
	arg.Features.ShowTyping.DefaultValue = true
	res, err := GetPassphraseUntilCheck(arg, newUIPrompter(ui), &CheckPassphraseSimple)
	if err != nil {
		return "", err
	}
	return res.Passphrase, nil
}

func GetPaperKeyForCryptoPassphrase(ui SecretUI, reason string, devices []*Device) (string, error) {
	if len(devices) == 0 {
		return "", errors.New("empty device list")
	}
	arg := DefaultPassphraseArg()
	arg.WindowTitle = "Paper backup key passphrase"
	arg.Type = keybase1.PassphraseType_PAPER_KEY
	arg.Features.StoreSecret.Allow = false
	arg.Features.StoreSecret.Readonly = true
	if len(devices) == 1 {
		arg.Prompt = fmt.Sprintf("%s: please enter the paper key '%s...'", reason, *devices[0].Description)
	} else {
		descs := make([]string, len(devices))
		for i, dev := range devices {
			descs[i] = fmt.Sprintf("'%s...'", *dev.Description)
		}
		paperOpts := strings.Join(descs, " or ")
		arg.Prompt = fmt.Sprintf("%s: please enter one of the following paper keys %s", reason, paperOpts)
	}

	res, err := GetPassphraseUntilCheck(arg, newUIPrompter(ui), &CheckPassphraseSimple)
	if err != nil {
		return "", err
	}
	return res.Passphrase, nil
}

type PassphrasePrompter interface {
	Prompt(keybase1.GUIEntryArg) (keybase1.GetPassphraseRes, error)
}

type uiPrompter struct {
	ui SecretUI
}

var _ PassphrasePrompter = &uiPrompter{}

func newUIPrompter(ui SecretUI) *uiPrompter {
	return &uiPrompter{ui: ui}
}

func (u *uiPrompter) Prompt(arg keybase1.GUIEntryArg) (keybase1.GetPassphraseRes, error) {
	return u.ui.GetPassphrase(arg, nil)
}

func GetPassphraseUntilCheck(arg keybase1.GUIEntryArg, prompter PassphrasePrompter, checker *Checker) (keybase1.GetPassphraseRes, error) {
	for i := 0; i < 10; i++ {
		res, err := prompter.Prompt(arg)
		if err != nil {
			return keybase1.GetPassphraseRes{}, err
		}
		if checker == nil {
			return res, nil
		}
		if checker.F(res.Passphrase) {
			return res, nil
		}
		arg.RetryLabel = checker.Hint
	}

	return keybase1.GetPassphraseRes{}, RetryExhaustedError{}
}

func DefaultPassphraseArg() keybase1.GUIEntryArg {
	return keybase1.GUIEntryArg{
		SubmitLabel: "Submit",
		CancelLabel: "Cancel",
		Features: keybase1.GUIEntryFeatures{
			ShowTyping: keybase1.Feature{
				Allow:        true,
				DefaultValue: false,
				Readonly:     true,
				Label:        "Show typing",
			},
			StoreSecret: keybase1.Feature{
				Allow:        HasSecretStore(),
				DefaultValue: false,
				Readonly:     false,
				Label:        "Save in Keychain",
			},
		},
	}
}
<|endoftext|>


<commit_before>package oskite

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"koding/db/models"
	"koding/tools/dnode"
	"koding/tools/kite"
	"koding/virt"
	"net/http"

	"gopkg.in/fatih/set.v0"
	"labix.org/v2/mgo"
	"labix.org/v2/mgo/bson"
)

type LimitState int

const (
	LimitOk LimitState = iota
	LimitQuotaExceeded
)

type Limit struct {
	CPU         LimitState `bson:"cpu"`
	RAM         LimitState `bson:"ram"`  // Memory usage in MB
	Disk        LimitState `bson:"disk"` // Disk in MB
	TotalVMs    LimitState `bson:"totalVMs"`
	AlwaysOnVMs LimitState `bson:"alwaysOnVMs"`
}

type LimitError struct {
	Message string `json:"message"`
	Code    string `json:"code"`
}

func (l *LimitError) Error() string {
	out, err := json.Marshal(l)
	if err != nil {
		panic(err) // should never happen
	}

	return string(out)
}

type KiteStore struct {
	Id       bson.ObjectId `bson:"_id"`
	Name     string        `bson:"name"`
	KiteCode string        `bson:"kiteCode"`
	Manifest struct {
		Description string `bson:"description"`
		Name        string `bson:"name"`
		Readme      string `bson:"readme"`
		AuthorNick  string `bson:"authorNick"`
		Author      string `bson:"author"`
	}
}

type Plan struct {
	CPU         int `bson:"cpu"`
	RAM         int `bson:"ram"`  // Memory usage in MB
	Disk        int `bson:"disk"` // Disk in MB
	TotalVMs    int `bson:"totalVMs"`
	AlwaysOnVMs int `bson:"alwaysOnVMs"`
}

type subscriptionResp struct {
	PlanName string `json:"planName"`
	PlanId   string `json:"planId"`
	Err      string `json:"err"`
}

var (
	// [N]x = [Nx2] CPU + [Nx2]GB Ram + [Nx10]GB Disk + [Nx10] Total VM + [N] Always On (devrim's forumla)
	plans = map[string]Plan{
		"free": {CPU: 1, RAM: 1000, Disk: 3000, TotalVMs: 5, AlwaysOnVMs: 0},
		"1x":   {CPU: 2, RAM: 2000, Disk: 10000, TotalVMs: 10, AlwaysOnVMs: 1},
		"2x":   {CPU: 4, RAM: 4000, Disk: 20000, TotalVMs: 20, AlwaysOnVMs: 2},
		"3x":   {CPU: 6, RAM: 6000, Disk: 30000, TotalVMs: 30, AlwaysOnVMs: 3},
		"4x":   {CPU: 8, RAM: 8000, Disk: 40000, TotalVMs: 40, AlwaysOnVMs: 4},
		"5x":   {CPU: 10, RAM: 10000, Disk: 50000, TotalVMs: 50, AlwaysOnVMs: 5},

		"10x":  {CPU: 20, RAM: 20000, Disk: 100000, TotalVMs: 100, AlwaysOnVMs: 10},
		"25x":  {CPU: 50, RAM: 50000, Disk: 250000, TotalVMs: 250, AlwaysOnVMs: 25},
		"50x":  {CPU: 100, RAM: 100000, Disk: 500000, TotalVMs: 500, AlwaysOnVMs: 50},
		"75x":  {CPU: 150, RAM: 150000, Disk: 700000, TotalVMs: 750, AlwaysOnVMs: 75},
		"100x": {CPU: 200, RAM: 200000, Disk: 1000000, TotalVMs: 1000, AlwaysOnVMs: 100},
	}

	ErrQuotaExceeded = errors.New("ErrQuotaExceeded")

	kiteCode string

	endpointErrs = set.New(
		errors.New("TOKEN_REQUIRED"),
		errors.New("USERNAME_REQUIRED"),
		errors.New("GROUPNAME_REQUIRED"),
		errors.New("KITE_NOT_FOUND"),
		errors.New("USER_NOT_FOUND"),
		errors.New("GROUP_NOT_FOUND"),
		errors.New("NOT_A_MEMBER_OF_GROUP"),
		errors.New("KITE_HAS_NO_PLAN"),
		errors.New("NO_SUBSCRIPTION"),
	)
)

func newLimit() *Limit {
	return &Limit{
		CPU:         LimitOk,
		RAM:         LimitOk,
		Disk:        LimitOk,
		TotalVMs:    LimitOk,
		AlwaysOnVMs: LimitOk,
	}
}

func vmUsage(args *dnode.Partial, vos *virt.VOS, username string) (interface{}, error) {
	var params struct {
		GroupId string
	}

	if args == nil {
		return nil, &kite.ArgumentError{Expected: "empy argument passed"}
	}

	if args.Unmarshal(&params) != nil || params.GroupId == "" {
		return nil, &kite.ArgumentError{Expected: "{ groupId: [string] }"}
	}

	usage, err := totalUsage(vos, params.GroupId)
	if err != nil {
		log.Info("vm.usage [%s] err: %v", vos.VM.HostnameAlias, err)
		return nil, errors.New("vm.usage couldn't be retrieved. please consult to support.")
	}

	limits, err := usage.prepareLimits(username, params.GroupId)
	if err != nil {
		return nil, err
	}

	if err := limits.check(); err != nil {
		return nil, err
	}

	return true, nil
}

func totalUsage(vos *virt.VOS, groupId string) (*Plan, error) {
	if !bson.IsObjectIdHex(groupId) {
		return nil, fmt.Errorf("groupID %s is not valid hex representation", groupId)
	}

	vms := make([]*models.VM, 0)

	// db.jVMs.find({"webHome":"foo", "groups": {$in:[{"id":ObjectId("5196fcb2bc9bdb0000000027")}]}})
	query := func(c *mgo.Collection) error {
		return c.Find(bson.M{
			"webHome":  vos.VM.WebHome,
			"hostKite": bson.M{"$ne": nil},
			"groups":   bson.M{"$in": []bson.M{bson.M{"id": bson.ObjectIdHex(groupId)}}},
		}).Iter().All(&vms)
	}

	if err := mongodbConn.Run("jVMs", query); err != nil {
		return nil, fmt.Errorf("vm fetching err for user %s. err: %s", vos.VM.WebHome, err)
	}

	usage := new(Plan)
	usage.TotalVMs = len(vms)

	for _, vm := range vms {
		if vm.AlwaysOn {
			usage.AlwaysOnVMs++
		}

		usage.CPU += vm.NumCPUs
		usage.RAM += vm.MaxMemoryInMB
		usage.Disk += vm.DiskSizeInMB
	}

	fmt.Printf("usage %+v\n", usage)
	return usage, nil
}

func (p *Plan) prepareLimits(username, groupId string) (*Limit, error) {
	sub, err := getSubscription(username, groupId)
	if err != nil {
		log.Warning("oskite checkLimits err: %v", err)
		return nil, errors.New("couldn't fetch subscription")
	}

	if sub.PlanName == "" {
		return nil, errors.New("planName is empty")
	}
	fmt.Printf("sub %+v\n", sub)

	plan, ok := plans[sub.PlanName]
	if !ok {
		return nil, errors.New("plan doesn't exist")
	}

	lim := newLimit()
	if p.AlwaysOnVMs > plan.AlwaysOnVMs {
		lim.AlwaysOnVMs = LimitQuotaExceeded
	}

	if p.TotalVMs >= plan.TotalVMs {
		lim.TotalVMs = LimitQuotaExceeded
	}

	if p.CPU >= plan.CPU {
		lim.CPU = LimitQuotaExceeded
	}

	if p.RAM >= plan.RAM {
		lim.RAM = LimitQuotaExceeded
	}

	if p.Disk >= plan.Disk {
		lim.Disk = LimitQuotaExceeded
	}

	return lim, nil
}

func (l *Limit) check() error {
	fmt.Printf("limits %+v\n", l)

	if l.AlwaysOnVMs == LimitQuotaExceeded {
		return &LimitError{Message: "AlwaysOnVMs limit reached.", Code: ErrQuotaExceeded.Error()}
	}

	if l.CPU == LimitQuotaExceeded {
		return &LimitError{Message: "CPU limit reached", Code: ErrQuotaExceeded.Error()}
	}

	if l.Disk == LimitQuotaExceeded {
		return &LimitError{Message: "Disk limit reached", Code: ErrQuotaExceeded.Error()}
	}

	if l.RAM == LimitQuotaExceeded {
		return &LimitError{Message: "Ram limit reached", Code: ErrQuotaExceeded.Error()}
	}

	return nil
}

// getKiteCode returns the API token to be used with Koding's subscription
// endpoint.
func getKiteCode() (string, error) {
	if kiteCode != "" {
		return kiteCode, nil
	}

	kiteStore := new(KiteStore)

	query := func(c *mgo.Collection) error {
		return c.Find(bson.M{"name": "OsKite"}).One(kiteStore)
	}

	err := mongodbConn.Run("jKites", query)
	if err != nil {
		return "", err
	}

	kiteCode = kiteStore.KiteCode

	return kiteStore.KiteCode, nil
}

func getSubscription(username, groupId string) (*subscriptionResp, error) {
	code, err := getKiteCode()
	if err != nil {
		return nil, err
	}

	if code == "" {
		return nil, errors.New("kite code is empty")
	}

	endpointURL := conf.SubscriptionEndpoint + code + "/" + username + "/" + groupId

	fmt.Printf("endpointURL %+v\n", endpointURL)

	resp, err := http.Get(endpointURL)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

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

	var sub = new(subscriptionResp)
	if err := json.Unmarshal(body, sub); err != nil {
		return nil, errors.New("Subscription data is malformed")
	}

	if resp.StatusCode != 200 {
		if sub.Err != "" {
			return nil, errors.New(sub.Err)
		}
		return nil, errors.New("api not allowed")
	}

	return sub, nil
}
<commit_msg>oskite/usage: update plans<commit_after>package oskite

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"koding/db/models"
	"koding/tools/dnode"
	"koding/tools/kite"
	"koding/virt"
	"net/http"

	"gopkg.in/fatih/set.v0"
	"labix.org/v2/mgo"
	"labix.org/v2/mgo/bson"
)

type LimitState int

const (
	LimitOk LimitState = iota
	LimitQuotaExceeded
)

type Limit struct {
	CPU         LimitState `bson:"cpu"`
	RAM         LimitState `bson:"ram"`  // Memory usage in MB
	Disk        LimitState `bson:"disk"` // Disk in MB
	TotalVMs    LimitState `bson:"totalVMs"`
	AlwaysOnVMs LimitState `bson:"alwaysOnVMs"`
}

type LimitError struct {
	Message string `json:"message"`
	Code    string `json:"code"`
}

func (l *LimitError) Error() string {
	out, err := json.Marshal(l)
	if err != nil {
		panic(err) // should never happen
	}

	return string(out)
}

type KiteStore struct {
	Id       bson.ObjectId `bson:"_id"`
	Name     string        `bson:"name"`
	KiteCode string        `bson:"kiteCode"`
	Manifest struct {
		Description string `bson:"description"`
		Name        string `bson:"name"`
		Readme      string `bson:"readme"`
		AuthorNick  string `bson:"authorNick"`
		Author      string `bson:"author"`
	}
}

type Plan struct {
	CPU         int `bson:"cpu"`
	RAM         int `bson:"ram"`  // Memory usage in MB
	Disk        int `bson:"disk"` // Disk in MB
	TotalVMs    int `bson:"totalVMs"`
	AlwaysOnVMs int `bson:"alwaysOnVMs"`
}

type subscriptionResp struct {
	PlanName string `json:"planName"`
	PlanId   string `json:"planId"`
	Err      string `json:"err"`
}

var (
	// [N]x = [Nx2] CPU + [Nx2]GB Ram + [Nx10]GB Disk + [Nx10] Total VM + [N] Always On (devrim's forumla)
	plans = map[string]Plan{
		"free": {CPU: 1, RAM: 1024, Disk: 3072, TotalVMs: 5, AlwaysOnVMs: 0},
		"1x":   {CPU: 2, RAM: 2048, Disk: 10240, TotalVMs: 10, AlwaysOnVMs: 1},
		"2x":   {CPU: 4, RAM: 4096, Disk: 20480, TotalVMs: 20, AlwaysOnVMs: 2},
		"3x":   {CPU: 6, RAM: 6144, Disk: 30720, TotalVMs: 30, AlwaysOnVMs: 3},
		"4x":   {CPU: 8, RAM: 8192, Disk: 40960, TotalVMs: 40, AlwaysOnVMs: 4},
		"5x":   {CPU: 10, RAM: 10240, Disk: 51200, TotalVMs: 50, AlwaysOnVMs: 5},

		"10x":  {CPU: 20, RAM: 20480, Disk: 102400, TotalVMs: 100, AlwaysOnVMs: 10},
		"25x":  {CPU: 50, RAM: 51200, Disk: 256000, TotalVMs: 250, AlwaysOnVMs: 25},
		"50x":  {CPU: 100, RAM: 102400, Disk: 512000, TotalVMs: 500, AlwaysOnVMs: 50},
		"75x":  {CPU: 150, RAM: 153600, Disk: 768000, TotalVMs: 750, AlwaysOnVMs: 75},
		"100x": {CPU: 200, RAM: 204800, Disk: 1024000, TotalVMs: 1000, AlwaysOnVMs: 100},
	}

	ErrQuotaExceeded = errors.New("ErrQuotaExceeded")

	kiteCode string

	endpointErrs = set.New(
		errors.New("TOKEN_REQUIRED"),
		errors.New("USERNAME_REQUIRED"),
		errors.New("GROUPNAME_REQUIRED"),
		errors.New("KITE_NOT_FOUND"),
		errors.New("USER_NOT_FOUND"),
		errors.New("GROUP_NOT_FOUND"),
		errors.New("NOT_A_MEMBER_OF_GROUP"),
		errors.New("KITE_HAS_NO_PLAN"),
		errors.New("NO_SUBSCRIPTION"),
	)
)

func newLimit() *Limit {
	return &Limit{
		CPU:         LimitOk,
		RAM:         LimitOk,
		Disk:        LimitOk,
		TotalVMs:    LimitOk,
		AlwaysOnVMs: LimitOk,
	}
}

func vmUsage(args *dnode.Partial, vos *virt.VOS, username string) (interface{}, error) {
	var params struct {
		GroupId string
	}

	if args == nil {
		return nil, &kite.ArgumentError{Expected: "empy argument passed"}
	}

	if args.Unmarshal(&params) != nil || params.GroupId == "" {
		return nil, &kite.ArgumentError{Expected: "{ groupId: [string] }"}
	}

	usage, err := totalUsage(vos, params.GroupId)
	if err != nil {
		log.Info("vm.usage [%s] err: %v", vos.VM.HostnameAlias, err)
		return nil, errors.New("vm.usage couldn't be retrieved. please consult to support.")
	}

	limits, err := usage.prepareLimits(username, params.GroupId)
	if err != nil {
		return nil, err
	}

	if err := limits.check(); err != nil {
		return nil, err
	}

	return true, nil
}

func totalUsage(vos *virt.VOS, groupId string) (*Plan, error) {
	if !bson.IsObjectIdHex(groupId) {
		return nil, fmt.Errorf("groupID %s is not valid hex representation", groupId)
	}

	vms := make([]*models.VM, 0)

	// db.jVMs.find({"webHome":"foo", "groups": {$in:[{"id":ObjectId("5196fcb2bc9bdb0000000027")}]}})
	query := func(c *mgo.Collection) error {
		return c.Find(bson.M{
			"webHome":  vos.VM.WebHome,
			"hostKite": bson.M{"$ne": nil},
			"groups":   bson.M{"$in": []bson.M{bson.M{"id": bson.ObjectIdHex(groupId)}}},
		}).Iter().All(&vms)
	}

	if err := mongodbConn.Run("jVMs", query); err != nil {
		return nil, fmt.Errorf("vm fetching err for user %s. err: %s", vos.VM.WebHome, err)
	}

	usage := new(Plan)
	usage.TotalVMs = len(vms)

	for _, vm := range vms {
		if vm.AlwaysOn {
			usage.AlwaysOnVMs++
		}

		usage.CPU += vm.NumCPUs
		usage.RAM += vm.MaxMemoryInMB
		usage.Disk += vm.DiskSizeInMB
	}

	fmt.Printf("usage %+v\n", usage)
	return usage, nil
}

func (p *Plan) prepareLimits(username, groupId string) (*Limit, error) {
	sub, err := getSubscription(username, groupId)
	if err != nil {
		log.Warning("oskite checkLimits err: %v", err)
		return nil, errors.New("couldn't fetch subscription")
	}

	if sub.PlanName == "" {
		return nil, errors.New("planName is empty")
	}

	plan, ok := plans[sub.PlanName]
	if !ok {
		return nil, errors.New("plan doesn't exist")
	}

	fmt.Printf("total	%+v\n", plan)
	fmt.Printf("current	%+v\n", p)

	lim := newLimit()
	if p.AlwaysOnVMs > plan.AlwaysOnVMs {
		lim.AlwaysOnVMs = LimitQuotaExceeded
	}

	if p.TotalVMs >= plan.TotalVMs {
		lim.TotalVMs = LimitQuotaExceeded
	}

	if p.CPU >= plan.CPU {
		lim.CPU = LimitQuotaExceeded
	}

	if p.RAM >= plan.RAM {
		lim.RAM = LimitQuotaExceeded
	}

	if p.Disk >= plan.Disk {
		lim.Disk = LimitQuotaExceeded
	}

	return lim, nil
}

func (l *Limit) check() error {
	if l.AlwaysOnVMs == LimitQuotaExceeded {
		return &LimitError{Message: "AlwaysOnVMs limit reached.", Code: ErrQuotaExceeded.Error()}
	}

	if l.CPU == LimitQuotaExceeded {
		return &LimitError{Message: "CPU limit reached", Code: ErrQuotaExceeded.Error()}
	}

	if l.Disk == LimitQuotaExceeded {
		return &LimitError{Message: "Disk limit reached", Code: ErrQuotaExceeded.Error()}
	}

	if l.RAM == LimitQuotaExceeded {
		return &LimitError{Message: "Ram limit reached", Code: ErrQuotaExceeded.Error()}
	}

	return nil
}

// getKiteCode returns the API token to be used with Koding's subscription
// endpoint.
func getKiteCode() (string, error) {
	if kiteCode != "" {
		return kiteCode, nil
	}

	kiteStore := new(KiteStore)

	query := func(c *mgo.Collection) error {
		return c.Find(bson.M{"name": "OsKite"}).One(kiteStore)
	}

	err := mongodbConn.Run("jKites", query)
	if err != nil {
		return "", err
	}

	kiteCode = kiteStore.KiteCode

	return kiteStore.KiteCode, nil
}

func getSubscription(username, groupId string) (*subscriptionResp, error) {
	code, err := getKiteCode()
	if err != nil {
		return nil, err
	}

	if code == "" {
		return nil, errors.New("kite code is empty")
	}

	endpointURL := conf.SubscriptionEndpoint + code + "/" + username + "/" + groupId

	fmt.Printf("endpointURL %+v\n", endpointURL)

	resp, err := http.Get(endpointURL)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

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

	var sub = new(subscriptionResp)
	if err := json.Unmarshal(body, sub); err != nil {
		return nil, errors.New("Subscription data is malformed")
	}

	if resp.StatusCode != 200 {
		if sub.Err != "" {
			return nil, errors.New(sub.Err)
		}
		return nil, errors.New("api not allowed")
	}

	return sub, nil
}
<|endoftext|>