<commit_before>// SPDX-License-Identifier: MIT

package token

import (
	"fmt"
	"reflect"
	"strings"
	"unicode"
)

const tagName = "apidoc"

type nodeType int8

const (
	attrNode nodeType = iota
	elemNode
	cdataNode
	contentNode
)

var (
	cdataType   = reflect.TypeOf(CData{})
	contentType = reflect.TypeOf(String{})
)

// 表示一个 XML 标签节点
type node struct {
	name           string  // 标签名称
	attrs          []value // 当前标签的属性值列表
	elems          []value // 当前标签的元素列表
	cdata, content value   // 当前标签如果没有子元素，则可能有普通的内容或是 CDATA 内容
}

// 表示 XML 节点的值的反射表示方式
type value struct {
	reflect.Value
	name      string // 节点的名称
	omitempty bool

	// 当前值可能未初始化，所以保存 usage 的值，
	// 等 value 初始化之后再赋值给 Base.UsageKey
	usage string
}

func parseRootElement(v interface{}) value {
	rv := reflect.ValueOf(v)
	if !rv.IsValid() {
		panic("参数 v 不是一个有效的根元素")
	}

	root, found := getRealType(rv.Type()).FieldByName(rootElementTagName)
	if !found {
		panic(fmt.Sprintf("根元素 %s 未指定 %s 字段", getRealType(rv.Type()), rootElementTagName))
	}

	name, _, usage, omitempty := parseTag(root)
	if name == "-" {
		panic(fmt.Sprintf("根元素 %s.%s 的标签值 %s 不能为 -", rv.Type(), rootElementTagName, tagName))
	}

	return initValue(name, rv, omitempty, usage)
}

func initValue(name string, v reflect.Value, omitempty bool, usage string) value {
	return value{
		name:      name,
		Value:     v,
		omitempty: omitempty,
		usage:     usage,
	}
}

func isPrimitive(v reflect.Value) bool {
	return v.IsValid() &&
		(v.Kind() == reflect.String || (v.Kind() >= reflect.Bool && v.Kind() <= reflect.Complex128))
}

func newNode(name string, rv reflect.Value) *node {
	rv = getRealValue(rv)
	rt := rv.Type()

	num := rt.NumField()
	if num == 0 {
		return &node{name: name}
	}

	n := &node{
		name:  name,
		attrs: make([]value, 0, num),
		elems: make([]value, 0, num),
	}

	for i := 0; i < num; i++ {
		field := rt.Field(i)
		if field.Anonymous {
			anonymous := newNode("", rv.Field(i))
			for _, attr := range anonymous.attrs {
				n.appendAttr(attr)
			}
			for _, elem := range anonymous.elems {
				n.appendElem(elem)
			}
			continue
		}

		if unicode.IsLower(rune(field.Name[0])) || field.Name == rootElementTagName {
			continue
		}

		name, node, usage, omitempty := parseTag(field)
		if name == "-" {
			continue
		}

		v := rv.Field(i)
		switch node {
		case attrNode:
			n.appendAttr(initValue(name, v, omitempty, usage))
		case elemNode:
			n.appendElem(initValue(name, v, omitempty, usage))
		case cdataNode:
			if n.cdata.IsValid() {
				panic("已经定义了一个节点用于表示 cdata 内容")
			}
			if n.content.IsValid() {
				panic("cdata 与 content 不能同时存在")
			}
			if len(n.elems) > 0 {
				panic("cdata 与子元素不能同时存在")
			}
			if getRealType(field.Type) != cdataType {
				panic("cdata 的类型只能是 *CData")
			}
			n.cdata = initValue(name, v, omitempty, usage)
		case contentNode:
			if n.content.IsValid() {
				panic("已经定义了一个节点用于表示 content 内容")
			}
			if n.cdata.IsValid() {
				panic("cdata 与 content 不能同时存在")
			}
			if len(n.elems) > 0 {
				panic("content 与子元素不能同时存在")
			}
			if getRealType(field.Type) != contentType {
				panic("content 的类型只能是 *String")
			}
			n.content = initValue(name, v, omitempty, usage)
		}
	}

	return n
}

func (n *node) elem(name string) (value, bool) {
	return n.findElem(name, n.elems)
}

func (n *node) attr(name string) (value, bool) {
	return n.findElem(name, n.attrs)
}

func (n *node) findElem(name string, elems []value) (value, bool) {
	for _, e := range elems {
		if e.name == name {
			return e, true
		}
	}
	return value{}, false
}

func getRealType(t reflect.Type) reflect.Type {
	for t.Kind() == reflect.Ptr {
		t = t.Elem()
	}
	return t
}

func getRealValue(v reflect.Value) reflect.Value {
	for v.Kind() == reflect.Ptr {
		if v.IsNil() {
			v.Set(reflect.New(v.Type().Elem()))
		} else {
			v = v.Elem()
		}
	}
	return v
}

func (n *node) appendAttr(v value) {
	if _, found := n.attr(v.name); found {
		panic(fmt.Sprintf("存在重复的属性名称 %s", v.name))
	}
	n.attrs = append(n.attrs, v)
}

func (n *node) appendElem(v value) {
	if n.content.IsValid() || n.cdata.IsValid() {
		panic("elems 不能同时与 content 和 cdata 存在")
	}

	if _, found := n.elem(v.name); found {
		panic(fmt.Sprintf("存在重复的元素名称 %s", v.name))
	}

	n.elems = append(n.elems, v)
}

// `apidoc:"name,attr,usage,omitempty"`
func parseTag(field reflect.StructField) (string, nodeType, string, bool) {
	tag := strings.TrimSpace(field.Tag.Get(tagName))
	if tag == "-" {
		return "-", 0, "", false
	}

	props := strings.Split(tag, ",")
	switch len(props) {
	case 2:
		return getTagName(field, props[0]), getNodeType(props[1]), "", false
	case 3:
		node := getNodeType(props[1])
		return getTagName(field, props[0]), node, getUsageKey(node, props[2]), false
	case 4:
		node := getNodeType(props[1])
		return getTagName(field, props[0]), node, getUsageKey(node, props[2]), getOmitempty(props[3])
	default:
		panic(fmt.Sprintf("无效的 struct tag %s:%s，数量必须介于 [3,4] 之间，当前 %d", field.Name, tag, len(props)))
	}
}

func getTagName(field reflect.StructField, name string) string {
	if name == "" {
		name = field.Name
	}
	return strings.TrimSpace(name)
}

func getNodeType(v string) nodeType {
	switch strings.ToLower(strings.TrimSpace(v)) {
	case "attr":
		return attrNode
	case "elem":
		return elemNode
	case "cdata":
		return cdataNode
	case "content":
		return contentNode
	default:
		panic("无效的 struct tag，第二个元素必须得是 attr、cdata、content 或是 elem")
	}
}

func getOmitempty(v string) bool {
	switch strings.TrimSpace(v) {
	case "omitempty":
		return true
	case "":
		return false
	default:
		panic("无效的 struct tag，第四个元素必须得是 omitempty 或是空值")
	}
}

func getUsageKey(node nodeType, v string) string {
	need := (node != cdataNode) && (node != contentNode)

	if v == "" && need {
		panic("无效的 struct tag，当类型为 cdata 和 content，不能指定 usage 属性")
	} else if v != "" && !need {
		panic("无效的 struct tag，当类型不为 cdata 和 content，必须指定 usage 属性")
	}
	return v
}
<commit_msg>refactor(internal/token): 采用 map 代替 switch<commit_after>// SPDX-License-Identifier: MIT

package token

import (
	"fmt"
	"reflect"
	"strings"
	"unicode"
)

const tagName = "apidoc"

type nodeType int8

const (
	attrNode nodeType = iota
	elemNode
	cdataNode
	contentNode
	metaNode // 用于描述节点的一些无数据
)

var (
	cdataType   = reflect.TypeOf(CData{})
	contentType = reflect.TypeOf(String{})
)

var stringNodeMap = map[string]nodeType{
	"attr":    attrNode,
	"elem":    elemNode,
	"cdata":   cdataNode,
	"content": contentNode,
	"meta":    metaNode,
}

// 表示一个 XML 标签节点
type node struct {
	name           string  // 标签名称
	attrs          []value // 当前标签的属性值列表
	elems          []value // 当前标签的元素列表
	cdata, content value   // 当前标签如果没有子元素，则可能有普通的内容或是 CDATA 内容
}

// 表示 XML 节点的值的反射表示方式
type value struct {
	reflect.Value
	name      string // 节点的名称
	omitempty bool

	// 当前值可能未初始化，所以保存 usage 的值，
	// 等 value 初始化之后再赋值给 Base.UsageKey
	usage string
}

func parseRootElement(v interface{}) value {
	rv := reflect.ValueOf(v)
	if !rv.IsValid() {
		panic("参数 v 不是一个有效的根元素")
	}

	root, found := getRealType(rv.Type()).FieldByName(rootElementTagName)
	if !found {
		panic(fmt.Sprintf("根元素 %s 未指定 %s 字段", getRealType(rv.Type()), rootElementTagName))
	}

	name, _, usage, omitempty := parseTag(root)
	if name == "-" {
		panic(fmt.Sprintf("根元素 %s.%s 的标签值 %s 不能为 -", rv.Type(), rootElementTagName, tagName))
	}

	return initValue(name, rv, omitempty, usage)
}

func initValue(name string, v reflect.Value, omitempty bool, usage string) value {
	return value{
		name:      name,
		Value:     v,
		omitempty: omitempty,
		usage:     usage,
	}
}

func isPrimitive(v reflect.Value) bool {
	return v.IsValid() &&
		(v.Kind() == reflect.String || (v.Kind() >= reflect.Bool && v.Kind() <= reflect.Complex128))
}

func newNode(name string, rv reflect.Value) *node {
	rv = getRealValue(rv)
	rt := rv.Type()

	num := rt.NumField()
	if num == 0 {
		return &node{name: name}
	}

	n := &node{
		name:  name,
		attrs: make([]value, 0, num),
		elems: make([]value, 0, num),
	}

	for i := 0; i < num; i++ {
		field := rt.Field(i)
		if field.Anonymous {
			anonymous := newNode("", rv.Field(i))
			for _, attr := range anonymous.attrs {
				n.appendAttr(attr)
			}
			for _, elem := range anonymous.elems {
				n.appendElem(elem)
			}
			continue
		}

		if unicode.IsLower(rune(field.Name[0])) || field.Name == rootElementTagName {
			continue
		}

		name, node, usage, omitempty := parseTag(field)
		if name == "-" {
			continue
		}

		v := rv.Field(i)
		switch node {
		case attrNode:
			n.appendAttr(initValue(name, v, omitempty, usage))
		case elemNode:
			n.appendElem(initValue(name, v, omitempty, usage))
		case cdataNode:
			if n.cdata.IsValid() {
				panic("已经定义了一个节点用于表示 cdata 内容")
			}
			if n.content.IsValid() {
				panic("cdata 与 content 不能同时存在")
			}
			if len(n.elems) > 0 {
				panic("cdata 与子元素不能同时存在")
			}
			if getRealType(field.Type) != cdataType {
				panic("cdata 的类型只能是 *CData")
			}
			n.cdata = initValue(name, v, omitempty, usage)
		case contentNode:
			if n.content.IsValid() {
				panic("已经定义了一个节点用于表示 content 内容")
			}
			if n.cdata.IsValid() {
				panic("cdata 与 content 不能同时存在")
			}
			if len(n.elems) > 0 {
				panic("content 与子元素不能同时存在")
			}
			if getRealType(field.Type) != contentType {
				panic("content 的类型只能是 *String")
			}
			n.content = initValue(name, v, omitempty, usage)
		}
	}

	return n
}

func (n *node) elem(name string) (value, bool) {
	return n.findElem(name, n.elems)
}

func (n *node) attr(name string) (value, bool) {
	return n.findElem(name, n.attrs)
}

func (n *node) findElem(name string, elems []value) (value, bool) {
	for _, e := range elems {
		if e.name == name {
			return e, true
		}
	}
	return value{}, false
}

func getRealType(t reflect.Type) reflect.Type {
	for t.Kind() == reflect.Ptr {
		t = t.Elem()
	}
	return t
}

func getRealValue(v reflect.Value) reflect.Value {
	for v.Kind() == reflect.Ptr {
		if v.IsNil() {
			v.Set(reflect.New(v.Type().Elem()))
		} else {
			v = v.Elem()
		}
	}
	return v
}

func (n *node) appendAttr(v value) {
	if _, found := n.attr(v.name); found {
		panic(fmt.Sprintf("存在重复的属性名称 %s", v.name))
	}
	n.attrs = append(n.attrs, v)
}

func (n *node) appendElem(v value) {
	if n.content.IsValid() || n.cdata.IsValid() {
		panic("elems 不能同时与 content 和 cdata 存在")
	}

	if _, found := n.elem(v.name); found {
		panic(fmt.Sprintf("存在重复的元素名称 %s", v.name))
	}

	n.elems = append(n.elems, v)
}

// `apidoc:"name,attr,usage,omitempty"`
func parseTag(field reflect.StructField) (string, nodeType, string, bool) {
	tag := strings.TrimSpace(field.Tag.Get(tagName))
	if tag == "-" {
		return "-", 0, "", false
	}

	props := strings.Split(tag, ",")
	switch len(props) {
	case 2:
		return getTagName(field, props[0]), getNodeType(props[1]), "", false
	case 3:
		node := getNodeType(props[1])
		return getTagName(field, props[0]), node, getUsageKey(node, props[2]), false
	case 4:
		node := getNodeType(props[1])
		return getTagName(field, props[0]), node, getUsageKey(node, props[2]), getOmitempty(props[3])
	default:
		panic(fmt.Sprintf("无效的 struct tag %s:%s，数量必须介于 [3,4] 之间，当前 %d", field.Name, tag, len(props)))
	}
}

func getTagName(field reflect.StructField, name string) string {
	if name == "" {
		name = field.Name
	}
	return strings.TrimSpace(name)
}

func getNodeType(v string) nodeType {
	if node, found := stringNodeMap[strings.ToLower(strings.TrimSpace(v))]; found {
		return node
	}
	panic(fmt.Sprintf("无效的 struct tag:%s", v))
}

func getOmitempty(v string) bool {
	switch strings.TrimSpace(v) {
	case "omitempty":
		return true
	case "":
		return false
	default:
		panic("无效的 struct tag，第四个元素必须得是 omitempty 或是空值")
	}
}

func getUsageKey(node nodeType, v string) string {
	need := (node != cdataNode) && (node != contentNode)

	if v == "" && need {
		panic("无效的 struct tag，当类型为 cdata 和 content，不能指定 usage 属性")
	} else if v != "" && !need {
		panic("无效的 struct tag，当类型不为 cdata 和 content，必须指定 usage 属性")
	}
	return v
}
<|endoftext|>


<commit_before>package users

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"strconv"
	"strings"
	"time"

	"golang.org/x/oauth2"

	"github.com/Sirupsen/logrus"
	"github.com/google/go-github/github"
	"github.com/jmoiron/sqlx"
	"github.com/pkg/errors"
	stripe "github.com/stripe/stripe-go"
	"github.com/stripe/stripe-go/coupon"
	"github.com/stripe/stripe-go/customer"
	"github.com/stripe/stripe-go/invoice"
	"github.com/stripe/stripe-go/sub"
)

// User represents a GopherCI-web user.
type User struct {
	Logger           *logrus.Entry
	db               *sqlx.DB
	GHClient         *github.Client
	UserID           int    `db:"id"`
	Email            string `db:"email"`
	GitHubID         int    `db:"github_id"`
	GitHubToken      []byte `db:"github_token"` // nil if none assigned to user
	StripeCustomerID string `db:"stripe_customer_id"`
}

// GetUser looks up a user in the db and returns it, if no user was found,
// user is nil, if an error occurs it will be returned.
func GetUser(logger *logrus.Entry, db *sqlx.DB, oauthConf *oauth2.Config, userID int) (*User, error) {
	user := &User{db: db}
	err := db.Get(user, "SELECT id, email, github_id, github_token, stripe_customer_id FROM users WHERE id = ?", userID)
	switch {
	case err == sql.ErrNoRows:
		return nil, nil
	case err != nil:
		return nil, errors.Wrap(err, "could not select from users")
	}
	// GitHubToken is nil if no tokens are assigned to the user, in that case
	// we'll provide an empty token to GHClient let it handle the inevitable
	// authentication error. The callers can handle that error themselves (such
	// as reestablishing the oauth flow).
	var token oauth2.Token
	if user.GitHubToken != nil {
		if err := json.Unmarshal(user.GitHubToken, &token); err != nil {
			return nil, err
		}
		user.GitHubToken = nil
	}
	user.GHClient = NewClient(oauthConf, &token)
	user.Logger = logger.WithField("userID", user.UserID)
	return user, nil
}

// GitHubListOrgMembershipsActive returns active
// https://godoc.org/github.com/google/go-github/github#OrganizationsService.ListOrgMemberships
func (u *User) GitHubListOrgMembershipsActive(ctx context.Context) ([]*github.Membership, error) {
	memberships, _, err := u.GHClient.Organizations.ListOrgMemberships(ctx, &github.ListOrgMembershipsOptions{State: "active"})
	if err != nil {
		return nil, err
	}
	return memberships, nil
}

// EnableInstallation marks a GitHub installation as enabled for this user.
// This does not enable the installation in GopherCI. Returns an error if an
// error occured, else success if successfully changed from disabled to
// enabled.
func (u *User) EnableInstallation(installationID int) error {
	// TODO check if they haven't exceeded any quotas first
	_, err := u.db.Exec(`INSERT IGNORE INTO gh_installations (user_id, installation_id) VALUES (?, ?)`, u.UserID, installationID)
	return err
}

// DisableInstallation marks a GitHub installation as disabled for this user.
// This does not disable the installation in GopherCI. Returns an error if an
// error occurred, else success if successfully changed from enabled to
// disabled.
func (u *User) DisableInstallation(installationID int) error {
	_, err := u.db.Exec(`DELETE FROM gh_installations WHERE user_id = ? AND installation_id = ?`, u.UserID, installationID)
	return err
}

// InstallationEnabled checks if installationID is enabled by this user, any error means
// installation is not enabled by this user.
func (u *User) InstallationEnabled(installationID int) bool {
	var installations int
	err := u.db.Get(&installations, `SELECT COUNT(*) FROM gh_installations WHERE user_id = ? AND installation_id = ?`, u.UserID, installationID)
	if err != nil {
		return false
	}
	return installations > 0
}

// EnabledInstallations returns a slice of installationIDs that are marked as
// enabled by for the user.
func (u *User) EnabledInstallations() ([]int, error) {
	installationIDs := []int{}
	err := u.db.Select(&installationIDs, `SELECT installation_id FROM gh_installations WHERE user_id = ?`, u.UserID)
	switch {
	case err == sql.ErrNoRows:
		return installationIDs, err
	case err != nil:
		return nil, err
	}
	return installationIDs, nil
}

func (u *User) ProcessStripePayment(token, plan string) error {
	// 2017-01-22, we've switched from AUD to USD currency in stripe, so existing
	// customers need a new stripe customer ID as stripe won't accept a single
	// customer with multiple currencies. So create a new stripe customer for
	// these users. We can remove the "u.UserID > 17"  condition once everyone is
	// off of the PersonalMonthly plan. See commit message for more information.
	if u.StripeCustomerID != "" && u.UserID > 17 {
		// TODO this should upgrade the existing plan (prorata) #8
		_, err := sub.New(&stripe.SubParams{
			Customer: u.StripeCustomerID,
			Plan:     plan,
		})
		if err != nil {
			return errors.Wrapf(err, "could not subscribe userID %v stripe customer %v to %q", u.UserID, u.StripeCustomerID, plan)
		}
		return nil
	}

	customerParams := &stripe.CustomerParams{
		Plan: plan,
		Params: stripe.Params{
			Meta: map[string]string{"userID": strconv.FormatInt(int64(u.UserID), 10)},
		},
	}
	_ = customerParams.SetSource(token)
	customer, err := customer.New(customerParams)
	if err != nil {
		return errors.Wrap(err, "could not create stripe customer")
	}

	_, err = u.db.Exec(`UPDATE users SET stripe_customer_id = ? WHERE ID = ?`, customer.ID, u.UserID)
	if err != nil {
		return errors.Wrapf(err, "Created stripe customer with id %q but could not allocate to userID %v", customer.ID, u.UserID)
	}
	return nil
}

// StripeCustomer gets the stripe customer, returns nil if there's no stripe
// customer ID or an error if an error occurs.
func (u *User) StripeCustomer() (*stripe.Customer, error) {
	if u.StripeCustomerID == "" {
		return nil, nil
	}
	customer, err := customer.Get(u.StripeCustomerID, nil)
	return customer, errors.Wrapf(err, "could not get stripe customer id %q", u.StripeCustomerID)
}

// ProcessStripeCoupon adds a couponID to a stripe customer.
func (u *User) ProcessStripeCoupon(couponID string) error {
	coupon, err := coupon.Get(couponID, nil)
	if err != nil {
		return err
	}
	if !coupon.Valid {
		return errors.New("coupon does not exist")
	}

	customerParams := &stripe.CustomerParams{
		Coupon: couponID,
	}
	_, err = customer.Update(u.StripeCustomerID, customerParams)
	return err
}

// Invoice represents an upcoming or previous invoice.
type Invoice struct {
	AmountDisplay string
	DueDate       time.Time
}

// StripeUpcomingInvoice returns the upcoming invoice for a user, nil if
// there are no upcoming invoices for this user, or an error.
func (u *User) StripeUpcomingInvoice() (*Invoice, error) {
	if u.StripeCustomerID == "" {
		return nil, nil
	}
	invoice, err := invoice.GetNext(&stripe.InvoiceParams{Customer: u.StripeCustomerID})
	if err != nil {
		return nil, err
	}

	return &Invoice{
		AmountDisplay: amountString(invoice.Currency, invoice.Amount),
		DueDate:       time.Unix(invoice.Date, 0),
	}, nil
}

// Subscription represents a payment subscription.
type Subscription struct {
	ID            string
	Name          string    // Name is the plan name.
	AmountDisplay string    // AmountDisplay is the amount formatted for display.
	AmountCents   uint      // AmountCents is the amount in cents.
	Interval      string    // Interval is the billing interval, such as month.
	StartedAt     time.Time // StartedAt is the date started.
	PeriodEndAt   time.Time // PeriodEndAt is the end date of the current interval.
	CancelledAt   time.Time // CancelledAt is the date the subscription was cancelled.
	// EndedAt is the date the subscription finally ended (it may have been
	// cancelled and ended at the period end).
	EndedAt time.Time
	// Ended is whether the subcription is currently active (it maybe cancelled,
	// but not currently ended).
	Ended bool
}

// Discount represents a discount to be applied.
type Discount struct {
	CouponID    string
	StartedAt   time.Time
	EndedAt     time.Time
	Description string
}

// StripeDiscount returns the discount for the current user. If the user does
// not have an active discount, nil is returned.
func (u *User) StripeDiscount(customer *stripe.Customer) *Discount {
	if customer.Discount == nil {
		return nil
	}

	discount := Discount{
		CouponID:  customer.Discount.Coupon.ID,
		StartedAt: time.Unix(customer.Discount.Start, 0),
		EndedAt:   time.Unix(customer.Discount.End, 0),
	}

	if customer.Discount.Deleted || time.Now().After(discount.EndedAt) {
		return nil
	}

	switch {
	case customer.Discount.Coupon.Percent > 0:
		discount.Description = fmt.Sprintf("%d%% off", customer.Discount.Coupon.Percent)
	case customer.Discount.Coupon.Amount > 0:
		discount.Description = fmt.Sprintf("%d%% off", customer.Discount.Coupon.Percent)
	}

	switch customer.Discount.Coupon.Duration {
	case coupon.Forever:
		discount.Description += " forever"
	case coupon.Once:
		discount.Description += " the next invoice"
	case coupon.Repeating:
		discount.Description += fmt.Sprintf(" for %d months", customer.Discount.Coupon.DurationPeriod)
	}

	return &discount
}

// StripeSubscriptions returns a slice of subscriptions for the current user,
// both current and previous subscriptions are returned.
func (u *User) StripeSubscriptions(customer *stripe.Customer) []Subscription {
	var subs []Subscription
	if customer.Subs == nil {
		return nil
	}
	for _, sub := range customer.Subs.Values {
		s := Subscription{
			ID:            sub.ID,
			Name:          sub.Plan.Name,
			AmountDisplay: amountString(sub.Plan.Currency, int64(sub.Plan.Amount)),
			AmountCents:   uint(sub.Plan.Amount),
			Interval:      string(sub.Plan.Interval),
			Ended:         sub.EndCancel,
		}
		if sub.Start > 0 {
			s.StartedAt = time.Unix(sub.Start, 0)
		}
		if sub.PeriodEnd > 0 {
			s.PeriodEndAt = time.Unix(sub.PeriodEnd, 0)
		}
		if sub.Canceled > 0 {
			s.CancelledAt = time.Unix(sub.Canceled, 0)
		}
		if sub.Ended > 0 {
			s.EndedAt = time.Unix(sub.Ended, 0)
		}
		subs = append(subs, s)
	}
	return subs
}

func amountString(currency stripe.Currency, amount int64) string {
	return fmt.Sprintf("$%s %.2f", strings.ToUpper(string(currency)), float64(amount)/100)
}

// CancelStripeSubscription cancels a stripe subscription at the end of the
// current billing period if endCancel is true. It does not disable any
// enabled installations.
func (u *User) CancelStripeSubscription(id string, endCancel bool) error {
	_, err := sub.Cancel(id, &stripe.SubParams{EndCancel: endCancel})
	return err
}
<commit_msg>Handle case where stripe customer has no subscription<commit_after>package users

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"time"

	"golang.org/x/oauth2"

	"github.com/Sirupsen/logrus"
	"github.com/google/go-github/github"
	"github.com/jmoiron/sqlx"
	"github.com/pkg/errors"
	stripe "github.com/stripe/stripe-go"
	"github.com/stripe/stripe-go/coupon"
	"github.com/stripe/stripe-go/customer"
	"github.com/stripe/stripe-go/invoice"
	"github.com/stripe/stripe-go/sub"
)

// User represents a GopherCI-web user.
type User struct {
	Logger           *logrus.Entry
	db               *sqlx.DB
	GHClient         *github.Client
	UserID           int    `db:"id"`
	Email            string `db:"email"`
	GitHubID         int    `db:"github_id"`
	GitHubToken      []byte `db:"github_token"` // nil if none assigned to user
	StripeCustomerID string `db:"stripe_customer_id"`
}

// GetUser looks up a user in the db and returns it, if no user was found,
// user is nil, if an error occurs it will be returned.
func GetUser(logger *logrus.Entry, db *sqlx.DB, oauthConf *oauth2.Config, userID int) (*User, error) {
	user := &User{db: db}
	err := db.Get(user, "SELECT id, email, github_id, github_token, stripe_customer_id FROM users WHERE id = ?", userID)
	switch {
	case err == sql.ErrNoRows:
		return nil, nil
	case err != nil:
		return nil, errors.Wrap(err, "could not select from users")
	}
	// GitHubToken is nil if no tokens are assigned to the user, in that case
	// we'll provide an empty token to GHClient let it handle the inevitable
	// authentication error. The callers can handle that error themselves (such
	// as reestablishing the oauth flow).
	var token oauth2.Token
	if user.GitHubToken != nil {
		if err := json.Unmarshal(user.GitHubToken, &token); err != nil {
			return nil, err
		}
		user.GitHubToken = nil
	}
	user.GHClient = NewClient(oauthConf, &token)
	user.Logger = logger.WithField("userID", user.UserID)
	return user, nil
}

// GitHubListOrgMembershipsActive returns active
// https://godoc.org/github.com/google/go-github/github#OrganizationsService.ListOrgMemberships
func (u *User) GitHubListOrgMembershipsActive(ctx context.Context) ([]*github.Membership, error) {
	memberships, _, err := u.GHClient.Organizations.ListOrgMemberships(ctx, &github.ListOrgMembershipsOptions{State: "active"})
	if err != nil {
		return nil, err
	}
	return memberships, nil
}

// EnableInstallation marks a GitHub installation as enabled for this user.
// This does not enable the installation in GopherCI. Returns an error if an
// error occured, else success if successfully changed from disabled to
// enabled.
func (u *User) EnableInstallation(installationID int) error {
	// TODO check if they haven't exceeded any quotas first
	_, err := u.db.Exec(`INSERT IGNORE INTO gh_installations (user_id, installation_id) VALUES (?, ?)`, u.UserID, installationID)
	return err
}

// DisableInstallation marks a GitHub installation as disabled for this user.
// This does not disable the installation in GopherCI. Returns an error if an
// error occurred, else success if successfully changed from enabled to
// disabled.
func (u *User) DisableInstallation(installationID int) error {
	_, err := u.db.Exec(`DELETE FROM gh_installations WHERE user_id = ? AND installation_id = ?`, u.UserID, installationID)
	return err
}

// InstallationEnabled checks if installationID is enabled by this user, any error means
// installation is not enabled by this user.
func (u *User) InstallationEnabled(installationID int) bool {
	var installations int
	err := u.db.Get(&installations, `SELECT COUNT(*) FROM gh_installations WHERE user_id = ? AND installation_id = ?`, u.UserID, installationID)
	if err != nil {
		return false
	}
	return installations > 0
}

// EnabledInstallations returns a slice of installationIDs that are marked as
// enabled by for the user.
func (u *User) EnabledInstallations() ([]int, error) {
	installationIDs := []int{}
	err := u.db.Select(&installationIDs, `SELECT installation_id FROM gh_installations WHERE user_id = ?`, u.UserID)
	switch {
	case err == sql.ErrNoRows:
		return installationIDs, err
	case err != nil:
		return nil, err
	}
	return installationIDs, nil
}

func (u *User) ProcessStripePayment(token, plan string) error {
	// 2017-01-22, we've switched from AUD to USD currency in stripe, so existing
	// customers need a new stripe customer ID as stripe won't accept a single
	// customer with multiple currencies. So create a new stripe customer for
	// these users. We can remove the "u.UserID > 17"  condition once everyone is
	// off of the PersonalMonthly plan. See commit message for more information.
	if u.StripeCustomerID != "" && u.UserID > 17 {
		// TODO this should upgrade the existing plan (prorata) #8
		_, err := sub.New(&stripe.SubParams{
			Customer: u.StripeCustomerID,
			Plan:     plan,
		})
		if err != nil {
			return errors.Wrapf(err, "could not subscribe userID %v stripe customer %v to %q", u.UserID, u.StripeCustomerID, plan)
		}
		return nil
	}

	customerParams := &stripe.CustomerParams{
		Plan: plan,
		Params: stripe.Params{
			Meta: map[string]string{"userID": strconv.FormatInt(int64(u.UserID), 10)},
		},
	}
	_ = customerParams.SetSource(token)
	customer, err := customer.New(customerParams)
	if err != nil {
		return errors.Wrap(err, "could not create stripe customer")
	}

	_, err = u.db.Exec(`UPDATE users SET stripe_customer_id = ? WHERE ID = ?`, customer.ID, u.UserID)
	if err != nil {
		return errors.Wrapf(err, "Created stripe customer with id %q but could not allocate to userID %v", customer.ID, u.UserID)
	}
	return nil
}

// StripeCustomer gets the stripe customer, returns nil if there's no stripe
// customer ID or an error if an error occurs.
func (u *User) StripeCustomer() (*stripe.Customer, error) {
	if u.StripeCustomerID == "" {
		return nil, nil
	}
	customer, err := customer.Get(u.StripeCustomerID, nil)
	return customer, errors.Wrapf(err, "could not get stripe customer id %q", u.StripeCustomerID)
}

// ProcessStripeCoupon adds a couponID to a stripe customer.
func (u *User) ProcessStripeCoupon(couponID string) error {
	coupon, err := coupon.Get(couponID, nil)
	if err != nil {
		return err
	}
	if !coupon.Valid {
		return errors.New("coupon does not exist")
	}

	customerParams := &stripe.CustomerParams{
		Coupon: couponID,
	}
	_, err = customer.Update(u.StripeCustomerID, customerParams)
	return err
}

// Invoice represents an upcoming or previous invoice.
type Invoice struct {
	AmountDisplay string
	DueDate       time.Time
}

// StripeUpcomingInvoice returns the upcoming invoice for a user, nil if
// there are no upcoming invoices for this user, or an error.
func (u *User) StripeUpcomingInvoice() (*Invoice, error) {
	if u.StripeCustomerID == "" {
		return nil, nil
	}
	invoice, err := invoice.GetNext(&stripe.InvoiceParams{Customer: u.StripeCustomerID})
	if err != nil {
		if serr, ok := err.(*stripe.Error); ok && serr.HTTPStatusCode == http.StatusNotFound {
			return nil, nil
		}
		return nil, err
	}
	return &Invoice{
		AmountDisplay: amountString(invoice.Currency, invoice.Amount),
		DueDate:       time.Unix(invoice.Date, 0),
	}, nil
}

// Subscription represents a payment subscription.
type Subscription struct {
	ID            string
	Name          string    // Name is the plan name.
	AmountDisplay string    // AmountDisplay is the amount formatted for display.
	AmountCents   uint      // AmountCents is the amount in cents.
	Interval      string    // Interval is the billing interval, such as month.
	StartedAt     time.Time // StartedAt is the date started.
	PeriodEndAt   time.Time // PeriodEndAt is the end date of the current interval.
	CancelledAt   time.Time // CancelledAt is the date the subscription was cancelled.
	// EndedAt is the date the subscription finally ended (it may have been
	// cancelled and ended at the period end).
	EndedAt time.Time
	// Ended is whether the subcription is currently active (it maybe cancelled,
	// but not currently ended).
	Ended bool
}

// Discount represents a discount to be applied.
type Discount struct {
	CouponID    string
	StartedAt   time.Time
	EndedAt     time.Time
	Description string
}

// StripeDiscount returns the discount for the current user. If the user does
// not have an active discount, nil is returned.
func (u *User) StripeDiscount(customer *stripe.Customer) *Discount {
	if customer.Discount == nil {
		return nil
	}

	discount := Discount{
		CouponID:  customer.Discount.Coupon.ID,
		StartedAt: time.Unix(customer.Discount.Start, 0),
		EndedAt:   time.Unix(customer.Discount.End, 0),
	}

	if customer.Discount.Deleted || time.Now().After(discount.EndedAt) {
		return nil
	}

	switch {
	case customer.Discount.Coupon.Percent > 0:
		discount.Description = fmt.Sprintf("%d%% off", customer.Discount.Coupon.Percent)
	case customer.Discount.Coupon.Amount > 0:
		discount.Description = fmt.Sprintf("%d%% off", customer.Discount.Coupon.Percent)
	}

	switch customer.Discount.Coupon.Duration {
	case coupon.Forever:
		discount.Description += " forever"
	case coupon.Once:
		discount.Description += " the next invoice"
	case coupon.Repeating:
		discount.Description += fmt.Sprintf(" for %d months", customer.Discount.Coupon.DurationPeriod)
	}

	return &discount
}

// StripeSubscriptions returns a slice of subscriptions for the current user,
// both current and previous subscriptions are returned.
func (u *User) StripeSubscriptions(customer *stripe.Customer) []Subscription {
	var subs []Subscription
	if customer.Subs == nil {
		return nil
	}
	for _, sub := range customer.Subs.Values {
		s := Subscription{
			ID:            sub.ID,
			Name:          sub.Plan.Name,
			AmountDisplay: amountString(sub.Plan.Currency, int64(sub.Plan.Amount)),
			AmountCents:   uint(sub.Plan.Amount),
			Interval:      string(sub.Plan.Interval),
			Ended:         sub.EndCancel,
		}
		if sub.Start > 0 {
			s.StartedAt = time.Unix(sub.Start, 0)
		}
		if sub.PeriodEnd > 0 {
			s.PeriodEndAt = time.Unix(sub.PeriodEnd, 0)
		}
		if sub.Canceled > 0 {
			s.CancelledAt = time.Unix(sub.Canceled, 0)
		}
		if sub.Ended > 0 {
			s.EndedAt = time.Unix(sub.Ended, 0)
		}
		subs = append(subs, s)
	}
	return subs
}

func amountString(currency stripe.Currency, amount int64) string {
	return fmt.Sprintf("$%s %.2f", strings.ToUpper(string(currency)), float64(amount)/100)
}

// CancelStripeSubscription cancels a stripe subscription at the end of the
// current billing period if endCancel is true. It does not disable any
// enabled installations.
func (u *User) CancelStripeSubscription(id string, endCancel bool) error {
	_, err := sub.Cancel(id, &stripe.SubParams{EndCancel: endCancel})
	return err
}
<|endoftext|>


<commit_before>package glisp

func Version() string {
	return "0.1.1"
}
<commit_msg>bump minor version number<commit_after>package glisp

func Version() string {
	return "0.2.0"
}
<|endoftext|>


<commit_before>// santiago - webhook dispatching service
// https://github.com/topfreegames/santiago
// Licensed under the MIT license:
// http://www.opensource.org/licenses/mit-license
// Copyright © 2016 Top Free Games <backend@tfgco.com>

package worker

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/nsqio/go-nsq"
	"github.com/uber-go/zap"
	"github.com/valyala/fasthttp"
)

type NSQLogger struct {
	logger zap.Logger
}

func (l *NSQLogger) Output(calldepth int, s string) error {
	l.logger.Warn(s)
	return nil
}

//Worker is a worker implementation that keeps processing webhooks
type Worker struct {
	Topic               string
	Logger              zap.Logger
	LookupHost          string
	LookupPort          int
	LookupPollInterval  time.Duration
	MaxAttempts         int
	MaxMessagesInFlight int
	DefaultRequeueDelay time.Duration
}

//NewDefault returns a new worker with default options
func NewDefault(lookupHost string, lookupPort int, logger zap.Logger) *Worker {
	return New(
		"webhook",
		lookupHost, lookupPort, time.Duration(15)*time.Second,
		10, 150, time.Duration(15)*time.Second,
		logger,
	)
}

//New creates a new worker instance
func New(
	topic string, lookupHost string, lookupPort int, lookupPollInterval time.Duration,
	maxAttempts int, maxMessagesInFlight int, defaultRequeueDelay time.Duration,
	logger zap.Logger,
) *Worker {
	return &Worker{
		Logger:              logger,
		Topic:               topic,
		LookupHost:          lookupHost,
		LookupPort:          lookupPort,
		LookupPollInterval:  lookupPollInterval,
		MaxAttempts:         maxAttempts,
		MaxMessagesInFlight: maxMessagesInFlight,
		DefaultRequeueDelay: defaultRequeueDelay,
	}
}

func (w *Worker) DoRequest(method, url, payload string) (int, string, error) {
	client := fasthttp.Client{
		Name: "santiago",
	}

	req := fasthttp.AcquireRequest()
	req.Header.SetMethod(method)
	req.SetRequestURI(url)
	req.AppendBody([]byte(payload))
	resp := fasthttp.AcquireResponse()

	timeout := time.Duration(5) * time.Second

	err := client.DoTimeout(req, resp, timeout)
	if err != nil {
		fmt.Printf("Could not request webhook %s: %s\n", url, err.Error())
		return 0, "", err
	}

	return resp.StatusCode(), string(resp.Body()), nil
}

//Handle a single message from NSQ
func (w *Worker) Handle(msg *nsq.Message) error {
	var result map[string]interface{}
	err := json.Unmarshal(msg.Body, &result)
	if err != nil {
		fmt.Println("Could not process body", err)
		return err
	}

	payloadJSON, err := json.Marshal(result["payload"])
	if err != nil {
		fmt.Println("Could not process payload", err)
		return err
	}

	status, _, err := w.DoRequest(result["method"].(string), result["url"].(string), string(payloadJSON))
	if status > 399 {
		fmt.Println("Error requesting webhook", status)
		return err
	}

	return nil
}

//Subscribe to messages from NSQ
func (w *Worker) Subscribe() error {
	l := w.Logger.With(
		zap.String("operation", "Subscribe"),
	)
	nsqLookupPath := fmt.Sprintf("%s:%d", w.LookupHost, w.LookupPort)
	config := nsq.NewConfig()
	config.LookupdPollInterval = w.LookupPollInterval
	config.MaxAttempts = uint16(w.MaxAttempts)
	config.MaxInFlight = w.MaxMessagesInFlight
	config.DefaultRequeueDelay = w.DefaultRequeueDelay

	q, err := nsq.NewConsumer(w.Topic, "main", config)
	if err != nil {
		log.Panic("Could not create consumer...")
		return err
	}
	q.SetLogger(&NSQLogger{logger: l}, nsq.LogLevelWarning)

	q.AddHandler(nsq.HandlerFunc(w.Handle))

	err = q.ConnectToNSQLookupd(nsqLookupPath)
	if err != nil {
		log.Panic("Could not connect.")
		return err
	}

	return nil
}

//Start a new worker
func (w *Worker) Start() error {
	if err := w.Subscribe(); err != nil {
		return err
	}
	sigs := make(chan os.Signal, 1)
	done := make(chan bool, 1)
	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
	go func() {
		<-sigs
		done <- true
	}()
	<-done

	return nil
}
<commit_msg>Comments<commit_after>// santiago - webhook dispatching service
// https://github.com/topfreegames/santiago
// Licensed under the MIT license:
// http://www.opensource.org/licenses/mit-license
// Copyright © 2016 Top Free Games <backend@tfgco.com>

package worker

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/nsqio/go-nsq"
	"github.com/uber-go/zap"
	"github.com/valyala/fasthttp"
)

//NSQLogger is a bridge to NSQ from Zap
type NSQLogger struct {
	logger zap.Logger
}

//Output logs as Warnings
func (l *NSQLogger) Output(calldepth int, s string) error {
	l.logger.Warn(s)
	return nil
}

//Worker is a worker implementation that keeps processing webhooks
type Worker struct {
	Topic               string
	Logger              zap.Logger
	LookupHost          string
	LookupPort          int
	LookupPollInterval  time.Duration
	MaxAttempts         int
	MaxMessagesInFlight int
	DefaultRequeueDelay time.Duration
}

//NewDefault returns a new worker with default options
func NewDefault(lookupHost string, lookupPort int, logger zap.Logger) *Worker {
	return New(
		"webhook",
		lookupHost, lookupPort, time.Duration(15)*time.Second,
		10, 150, time.Duration(15)*time.Second,
		logger,
	)
}

//New creates a new worker instance
func New(
	topic string, lookupHost string, lookupPort int, lookupPollInterval time.Duration,
	maxAttempts int, maxMessagesInFlight int, defaultRequeueDelay time.Duration,
	logger zap.Logger,
) *Worker {
	return &Worker{
		Logger:              logger,
		Topic:               topic,
		LookupHost:          lookupHost,
		LookupPort:          lookupPort,
		LookupPollInterval:  lookupPollInterval,
		MaxAttempts:         maxAttempts,
		MaxMessagesInFlight: maxMessagesInFlight,
		DefaultRequeueDelay: defaultRequeueDelay,
	}
}

//DoRequest to some webhook endpoint
func (w *Worker) DoRequest(method, url, payload string) (int, string, error) {
	client := fasthttp.Client{
		Name: "santiago",
	}

	req := fasthttp.AcquireRequest()
	req.Header.SetMethod(method)
	req.SetRequestURI(url)
	req.AppendBody([]byte(payload))
	resp := fasthttp.AcquireResponse()

	timeout := time.Duration(5) * time.Second

	err := client.DoTimeout(req, resp, timeout)
	if err != nil {
		fmt.Printf("Could not request webhook %s: %s\n", url, err.Error())
		return 0, "", err
	}

	return resp.StatusCode(), string(resp.Body()), nil
}

//Handle a single message from NSQ
func (w *Worker) Handle(msg *nsq.Message) error {
	var result map[string]interface{}
	err := json.Unmarshal(msg.Body, &result)
	if err != nil {
		fmt.Println("Could not process body", err)
		return err
	}

	payloadJSON, err := json.Marshal(result["payload"])
	if err != nil {
		fmt.Println("Could not process payload", err)
		return err
	}

	status, _, err := w.DoRequest(result["method"].(string), result["url"].(string), string(payloadJSON))
	if status > 399 {
		fmt.Println("Error requesting webhook", status)
		return err
	}

	return nil
}

//Subscribe to messages from NSQ
func (w *Worker) Subscribe() error {
	l := w.Logger.With(
		zap.String("operation", "Subscribe"),
	)
	nsqLookupPath := fmt.Sprintf("%s:%d", w.LookupHost, w.LookupPort)
	config := nsq.NewConfig()
	config.LookupdPollInterval = w.LookupPollInterval
	config.MaxAttempts = uint16(w.MaxAttempts)
	config.MaxInFlight = w.MaxMessagesInFlight
	config.DefaultRequeueDelay = w.DefaultRequeueDelay

	q, err := nsq.NewConsumer(w.Topic, "main", config)
	if err != nil {
		log.Panic("Could not create consumer...")
		return err
	}
	q.SetLogger(&NSQLogger{logger: l}, nsq.LogLevelWarning)

	q.AddHandler(nsq.HandlerFunc(w.Handle))

	err = q.ConnectToNSQLookupd(nsqLookupPath)
	if err != nil {
		log.Panic("Could not connect.")
		return err
	}

	return nil
}

//Start a new worker
func (w *Worker) Start() error {
	if err := w.Subscribe(); err != nil {
		return err
	}
	sigs := make(chan os.Signal, 1)
	done := make(chan bool, 1)
	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
	go func() {
		<-sigs
		done <- true
	}()
	<-done

	return nil
}
<|endoftext|>


<commit_before>package invdendpoint

import (
	"encoding/json"
	"errors"
	"strings"
)

const EventEndpoint = "/events"

type Events []Event

type Event struct {
	Id        int64           `json:"id,omitempty"` // The event’s unique ID
	Object    string          `json:"object,omitempty"`
	Type      string          `json:"type,omitempty"` // Event type
	Timestamp int64           `json:"timestamp,omitempty"`
	Data      json.RawMessage `json:"data,omitempty"` // Contains an object property with the object that was subject of the event and an optional previous property for object.updated events that is a hash of the old values that changed during the event
}

type EventObject struct {
	Object         *json.RawMessage `json:"object,omitempty"`
	PreviousObject *json.RawMessage `json:"previous,omitempty"`
}

func (e *Event) ParseEventObject() (*json.RawMessage, error) {
	data := e.Data

	eo := new(EventObject)

	b, err := data.MarshalJSON()
	if err != nil {
		return nil, err
	}

	err = json.Unmarshal(b, eo)

	if err != nil {
		return nil, err
	}

	if eo.Object == nil {
		return nil, errors.New("Could not parse event object")
	}

	return eo.Object, nil
}

func (e *Event) ParseEventPreviousObject() (*json.RawMessage, error) {
	data := e.Data

	eo := new(EventObject)

	b, err := data.MarshalJSON()
	if err != nil {
		return nil, err
	}

	err = json.Unmarshal(b, eo)

	if err != nil {
		return nil, err
	}

	if eo.Object == nil {
		return nil, errors.New("Could not parse event object")
	}

	return eo.PreviousObject, nil
}

func (e *Event) ParseInvoiceEvent() (*Invoice, error) {
	eoData, err := e.ParseEventObject()
	if err != nil {
		return nil, err
	}

	b, err := eoData.MarshalJSON()
	if err != nil {
		return nil, err
	}

	bClean := CleanMetaDataArray(b)

	ie := new(Invoice)

	err = json.Unmarshal(bClean, ie)

	if err != nil {
		return nil, err
	}

	return ie, nil
}

func (e *Event) ParseInvoicePreviousEvent() (*Invoice, error) {
	eoData, err := e.ParseEventPreviousObject()
	if err != nil {
		return nil, err
	}

	if eoData == nil {
		return nil, err
	}

	b, err := eoData.MarshalJSON()
	if err != nil {
		return nil, err
	}

	bClean := CleanMetaDataArray(b)

	ie := new(Invoice)

	err = json.Unmarshal(bClean, ie)

	if err != nil {
		return nil, err
	}

	return ie, nil
}

func (e *Event) ParseSubscriptionEvent() (*Subscription, error) {
	eoData, err := e.ParseEventObject()
	if err != nil {
		return nil, err
	}

	b, err := eoData.MarshalJSON()
	if err != nil {
		return nil, err
	}

	bClean := CleanMetaDataArray(b)

	ie := new(Subscription)

	err = json.Unmarshal(bClean, ie)

	if err != nil {
		return nil, err
	}

	return ie, nil
}

func (e *Event) ParsePaymentEvent() (*Payment, error) {
	eoData, err := e.ParseEventObject()
	if err != nil {
		return nil, err
	}

	b, err := eoData.MarshalJSON()
	if err != nil {
		return nil, err
	}

	bClean := CleanMetaDataArray(b)

	ie := new(Payment)

	err = json.Unmarshal(bClean, ie)

	if err != nil {
		return nil, err
	}

	return ie, nil
}

func CleanMetaDataArray(b []byte) []byte {
	s := string(b)
	s1 := strings.Replace(s, `"metadata": []`, ` "metadata": null`, -1)
	s1 = strings.Replace(s1, `"metadata":[]`, ` "metadata": null`, -1)
	return []byte(s1)
}
<commit_msg>add method to parse customer events<commit_after>package invdendpoint

import (
	"encoding/json"
	"errors"
	"strings"
)

const EventEndpoint = "/events"

type Events []Event

type Event struct {
	Id        int64           `json:"id,omitempty"` // The event’s unique ID
	Object    string          `json:"object,omitempty"`
	Type      string          `json:"type,omitempty"` // Event type
	Timestamp int64           `json:"timestamp,omitempty"`
	Data      json.RawMessage `json:"data,omitempty"` // Contains an object property with the object that was subject of the event and an optional previous property for object.updated events that is a hash of the old values that changed during the event
}

type EventObject struct {
	Object         *json.RawMessage `json:"object,omitempty"`
	PreviousObject *json.RawMessage `json:"previous,omitempty"`
}

func (e *Event) ParseEventObject() (*json.RawMessage, error) {
	data := e.Data

	eo := new(EventObject)

	b, err := data.MarshalJSON()
	if err != nil {
		return nil, err
	}

	err = json.Unmarshal(b, eo)

	if err != nil {
		return nil, err
	}

	if eo.Object == nil {
		return nil, errors.New("Could not parse event object")
	}

	return eo.Object, nil
}

func (e *Event) ParseEventPreviousObject() (*json.RawMessage, error) {
	data := e.Data

	eo := new(EventObject)

	b, err := data.MarshalJSON()
	if err != nil {
		return nil, err
	}

	err = json.Unmarshal(b, eo)

	if err != nil {
		return nil, err
	}

	if eo.Object == nil {
		return nil, errors.New("Could not parse event object")
	}

	return eo.PreviousObject, nil
}

func (e *Event) ParseInvoiceEvent() (*Invoice, error) {
	eoData, err := e.ParseEventObject()
	if err != nil {
		return nil, err
	}

	b, err := eoData.MarshalJSON()
	if err != nil {
		return nil, err
	}

	bClean := CleanMetaDataArray(b)

	ie := new(Invoice)

	err = json.Unmarshal(bClean, ie)

	if err != nil {
		return nil, err
	}

	return ie, nil
}

func (e *Event) ParseInvoicePreviousEvent() (*Invoice, error) {
	eoData, err := e.ParseEventPreviousObject()
	if err != nil {
		return nil, err
	}

	if eoData == nil {
		return nil, err
	}

	b, err := eoData.MarshalJSON()
	if err != nil {
		return nil, err
	}

	bClean := CleanMetaDataArray(b)

	ie := new(Invoice)

	err = json.Unmarshal(bClean, ie)

	if err != nil {
		return nil, err
	}

	return ie, nil
}

func (e *Event) ParseSubscriptionEvent() (*Subscription, error) {
	eoData, err := e.ParseEventObject()
	if err != nil {
		return nil, err
	}

	b, err := eoData.MarshalJSON()
	if err != nil {
		return nil, err
	}

	bClean := CleanMetaDataArray(b)

	ie := new(Subscription)

	err = json.Unmarshal(bClean, ie)

	if err != nil {
		return nil, err
	}

	return ie, nil
}

func (e *Event) ParsePaymentEvent() (*Payment, error) {
	eoData, err := e.ParseEventObject()
	if err != nil {
		return nil, err
	}

	b, err := eoData.MarshalJSON()
	if err != nil {
		return nil, err
	}

	bClean := CleanMetaDataArray(b)

	ie := new(Payment)

	err = json.Unmarshal(bClean, ie)

	if err != nil {
		return nil, err
	}

	return ie, nil
}

func (e *Event) ParseCustomerEvent() (*Customer, error) {
	eoData, err := e.ParseEventObject()
	if err != nil {
		return nil, err
	}

	b, err := eoData.MarshalJSON()
	if err != nil {
		return nil, err
	}

	bClean := CleanMetaDataArray(b)

	ie := new(Customer)

	err = json.Unmarshal(bClean, ie)

	if err != nil {
		return nil, err
	}

	return ie, nil
}

func CleanMetaDataArray(b []byte) []byte {
	s := string(b)
	s1 := strings.Replace(s, `"metadata": []`, ` "metadata": null`, -1)
	s1 = strings.Replace(s1, `"metadata":[]`, ` "metadata": null`, -1)
	return []byte(s1)
}
<|endoftext|>


<commit_before>package messages

import (
	"bytes"
	"fmt"
	"path"
	"sort"
	"time"

	"github.com/cloudfoundry/sonde-go/events"
	"github.com/golang/protobuf/ptypes/timestamp"
	labelpb "google.golang.org/genproto/googleapis/api/label"
	metricpb "google.golang.org/genproto/googleapis/api/metric"
	monitoringpb "google.golang.org/genproto/googleapis/monitoring/v3"
)

// Metric represents one of the metrics contained in an events.Envelope.
type Metric struct {
	Name      string
	Labels    map[string]string `json:"-"`
	Value     float64
	IntValue  int64
	EventTime time.Time
	StartTime time.Time                 `json:"-"`
	Unit      string                    // TODO Should this be "1" if it's empty?
	Type      events.Envelope_EventType `json:"-"`
}

func (m *Metric) IsCumulative() bool {
	return m.Type == events.Envelope_CounterEvent
}

func (m *Metric) metricType() string {
	return path.Join("custom.googleapis.com", m.Name)
}

// NeedsMetricDescriptor determines whether a custom metric descriptor needs to be created for this metric in Stackdriver.
// We do that if we need to set a custom unit, or mark metric as a cumulative.
func (m *Metric) NeedsMetricDescriptor() bool {
	return m.Unit != "" || m.IsCumulative()
}

// MetricDescriptor returns a Stackdriver MetricDescriptor proto for this metric.
func (m *Metric) MetricDescriptor(projectName string) *metricpb.MetricDescriptor {
	metricType := m.metricType()

	var labelDescriptors []*labelpb.LabelDescriptor
	for key := range m.Labels {
		labelDescriptors = append(labelDescriptors, &labelpb.LabelDescriptor{
			Key:       key,
			ValueType: labelpb.LabelDescriptor_STRING,
		})
	}

	metricKind := metricpb.MetricDescriptor_GAUGE
	valueType := metricpb.MetricDescriptor_DOUBLE
	if m.IsCumulative() {
		metricKind = metricpb.MetricDescriptor_CUMULATIVE
		valueType = metricpb.MetricDescriptor_INT64
	}

	return &metricpb.MetricDescriptor{
		Name:        path.Join(projectName, "metricDescriptors", metricType),
		Type:        metricType,
		Labels:      labelDescriptors,
		MetricKind:  metricKind,
		ValueType:   valueType,
		Unit:        m.Unit,
		Description: "stackdriver-nozzle created custom metric.",
		DisplayName: m.Name,
	}
}

// TimeSeries returns a Stackdriver TimeSeries proto for this metric value.
func (m *Metric) TimeSeries() *monitoringpb.TimeSeries {
	var value *monitoringpb.TypedValue
	if m.IsCumulative() {
		value = &monitoringpb.TypedValue{Value: &monitoringpb.TypedValue_Int64Value{Int64Value: m.IntValue}}
	} else {
		value = &monitoringpb.TypedValue{Value: &monitoringpb.TypedValue_DoubleValue{DoubleValue: m.Value}}
	}

	point := &monitoringpb.Point{
		Interval: &monitoringpb.TimeInterval{
			EndTime:   &timestamp.Timestamp{Seconds: m.EventTime.Unix(), Nanos: int32(m.EventTime.Nanosecond())},
			StartTime: &timestamp.Timestamp{Seconds: m.StartTime.Unix(), Nanos: int32(m.StartTime.Nanosecond())},
		},
		Value: value,
	}
	return &monitoringpb.TimeSeries{
		Metric: &metricpb.Metric{
			Type:   m.metricType(),
			Labels: m.Labels,
		},
		Points: []*monitoringpb.Point{point},
	}
}

func (m *Metric) Hash() string {
	var b bytes.Buffer

	b.Write([]byte(m.Name))

	// Extract label keys to a slice and sort it
	keys := make([]string, 0, len(m.Labels))
	for k := range m.Labels {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	for _, k := range keys {
		b.Write([]byte(fmt.Sprintf(",%s='%s'", k, m.Labels[k])))
	}
	return b.String()
}
<commit_msg>Set metric kind and value type when sending metric values<commit_after>package messages

import (
	"bytes"
	"fmt"
	"path"
	"sort"
	"time"

	"github.com/cloudfoundry/sonde-go/events"
	"github.com/golang/protobuf/ptypes/timestamp"
	labelpb "google.golang.org/genproto/googleapis/api/label"
	metricpb "google.golang.org/genproto/googleapis/api/metric"
	monitoringpb "google.golang.org/genproto/googleapis/monitoring/v3"
)

// Metric represents one of the metrics contained in an events.Envelope.
type Metric struct {
	Name      string
	Labels    map[string]string `json:"-"`
	Value     float64
	IntValue  int64
	EventTime time.Time
	StartTime time.Time                 `json:"-"`
	Unit      string                    // TODO Should this be "1" if it's empty?
	Type      events.Envelope_EventType `json:"-"`
}

func (m *Metric) IsCumulative() bool {
	return m.Type == events.Envelope_CounterEvent
}

func (m *Metric) metricType() string {
	return path.Join("custom.googleapis.com", m.Name)
}

func (m *Metric) metricKind() metricpb.MetricDescriptor_MetricKind {
	if m.IsCumulative() {
		return metricpb.MetricDescriptor_CUMULATIVE
	}
	return metricpb.MetricDescriptor_GAUGE
}

func (m *Metric) valueType() metricpb.MetricDescriptor_ValueType {
	if m.IsCumulative() {
		return metricpb.MetricDescriptor_INT64
	}
	return metricpb.MetricDescriptor_DOUBLE
}

// NeedsMetricDescriptor determines whether a custom metric descriptor needs to be created for this metric in Stackdriver.
// We do that if we need to set a custom unit, or mark metric as a cumulative.
func (m *Metric) NeedsMetricDescriptor() bool {
	return m.Unit != "" || m.IsCumulative()
}

// MetricDescriptor returns a Stackdriver MetricDescriptor proto for this metric.
func (m *Metric) MetricDescriptor(projectName string) *metricpb.MetricDescriptor {
	metricType := m.metricType()

	var labelDescriptors []*labelpb.LabelDescriptor
	for key := range m.Labels {
		labelDescriptors = append(labelDescriptors, &labelpb.LabelDescriptor{
			Key:       key,
			ValueType: labelpb.LabelDescriptor_STRING,
		})
	}

	return &metricpb.MetricDescriptor{
		Name:        path.Join(projectName, "metricDescriptors", metricType),
		Type:        metricType,
		Labels:      labelDescriptors,
		MetricKind:  m.metricKind(),
		ValueType:   m.valueType(),
		Unit:        m.Unit,
		Description: "stackdriver-nozzle created custom metric.",
		DisplayName: m.Name,
	}
}

// TimeSeries returns a Stackdriver TimeSeries proto for this metric value.
func (m *Metric) TimeSeries() *monitoringpb.TimeSeries {
	var value *monitoringpb.TypedValue
	if m.IsCumulative() {
		value = &monitoringpb.TypedValue{Value: &monitoringpb.TypedValue_Int64Value{Int64Value: m.IntValue}}
	} else {
		value = &monitoringpb.TypedValue{Value: &monitoringpb.TypedValue_DoubleValue{DoubleValue: m.Value}}
	}

	point := &monitoringpb.Point{
		Interval: &monitoringpb.TimeInterval{
			EndTime:   &timestamp.Timestamp{Seconds: m.EventTime.Unix(), Nanos: int32(m.EventTime.Nanosecond())},
			StartTime: &timestamp.Timestamp{Seconds: m.StartTime.Unix(), Nanos: int32(m.StartTime.Nanosecond())},
		},
		Value: value,
	}
	return &monitoringpb.TimeSeries{
		MetricKind: m.metricKind(),
		ValueType:  m.valueType(),
		Metric: &metricpb.Metric{
			Type:   m.metricType(),
			Labels: m.Labels,
		},
		Points: []*monitoringpb.Point{point},
	}
}

func (m *Metric) Hash() string {
	var b bytes.Buffer

	b.Write([]byte(m.Name))

	// Extract label keys to a slice and sort it
	keys := make([]string, 0, len(m.Labels))
	for k := range m.Labels {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	for _, k := range keys {
		b.Write([]byte(fmt.Sprintf(",%s='%s'", k, m.Labels[k])))
	}
	return b.String()
}
<|endoftext|>


<commit_before>// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package avatars

import (
	"context"
	"net/url"
	"path"
	"strconv"
	"strings"

	"code.gitea.io/gitea/models/db"
	"code.gitea.io/gitea/modules/base"
	"code.gitea.io/gitea/modules/cache"
	"code.gitea.io/gitea/modules/log"
	"code.gitea.io/gitea/modules/setting"
)

// DefaultAvatarPixelSize is the default size in pixels of a rendered avatar
const DefaultAvatarPixelSize = 28

// EmailHash represents a pre-generated hash map (mainly used by LibravatarURL, it queries email server's DNS records)
type EmailHash struct {
	Hash  string `xorm:"pk varchar(32)"`
	Email string `xorm:"UNIQUE NOT NULL"`
}

func init() {
	db.RegisterModel(new(EmailHash))
}

// DefaultAvatarLink the default avatar link
func DefaultAvatarLink() string {
	u, err := url.Parse(setting.AppSubURL)
	if err != nil {
		log.Error("GetUserByEmail: %v", err)
		return ""
	}

	u.Path = path.Join(u.Path, "/assets/img/avatar_default.png")
	return u.String()
}

// HashEmail hashes email address to MD5 string. https://en.gravatar.com/site/implement/hash/
func HashEmail(email string) string {
	return base.EncodeMD5(strings.ToLower(strings.TrimSpace(email)))
}

// GetEmailForHash converts a provided md5sum to the email
func GetEmailForHash(md5Sum string) (string, error) {
	return cache.GetString("Avatar:"+md5Sum, func() (string, error) {
		emailHash := EmailHash{
			Hash: strings.ToLower(strings.TrimSpace(md5Sum)),
		}

		_, err := db.GetEngine(db.DefaultContext).Get(&emailHash)
		return emailHash.Email, err
	})
}

// LibravatarURL returns the URL for the given email. Slow due to the DNS lookup.
// This function should only be called if a federated avatar service is enabled.
func LibravatarURL(email string) (*url.URL, error) {
	urlStr, err := setting.LibravatarService.FromEmail(email)
	if err != nil {
		log.Error("LibravatarService.FromEmail(email=%s): error %v", email, err)
		return nil, err
	}
	u, err := url.Parse(urlStr)
	if err != nil {
		log.Error("Failed to parse libravatar url(%s): error %v", urlStr, err)
		return nil, err
	}
	return u, nil
}

// saveEmailHash returns an avatar link for a provided email,
// the email and hash are saved into database, which will be used by GetEmailForHash later
func saveEmailHash(email string) string {
	lowerEmail := strings.ToLower(strings.TrimSpace(email))
	emailHash := HashEmail(lowerEmail)
	_, _ = cache.GetString("Avatar:"+emailHash, func() (string, error) {
		emailHash := &EmailHash{
			Email: lowerEmail,
			Hash:  emailHash,
		}
		// OK we're going to open a session just because I think that that might hide away any problems with postgres reporting errors
		if err := db.WithTx(func(ctx context.Context) error {
			has, err := db.GetEngine(ctx).Where("email = ? AND hash = ?", emailHash.Email, emailHash.Hash).Get(new(EmailHash))
			if has || err != nil {
				// Seriously we don't care about any DB problems just return the lowerEmail - we expect the transaction to fail most of the time
				return nil
			}
			_, _ = db.GetEngine(ctx).Insert(emailHash)
			return nil
		}); err != nil {
			// Seriously we don't care about any DB problems just return the lowerEmail - we expect the transaction to fail most of the time
			return lowerEmail, nil
		}
		return lowerEmail, nil
	})
	return emailHash
}

// GenerateUserAvatarFastLink returns a fast link (302) to the user's avatar: "/user/avatar/${User.Name}/${size}"
func GenerateUserAvatarFastLink(userName string, size int) string {
	if size < 0 {
		size = 0
	}
	return setting.AppSubURL + "/user/avatar/" + url.PathEscape(userName) + "/" + strconv.Itoa(size)
}

// GenerateUserAvatarImageLink returns a link for `User.Avatar` image file: "/avatars/${User.Avatar}"
func GenerateUserAvatarImageLink(userAvatar string, size int) string {
	if size > 0 {
		return setting.AppSubURL + "/avatars/" + url.PathEscape(userAvatar) + "?size=" + strconv.Itoa(size)
	}
	return setting.AppSubURL + "/avatars/" + url.PathEscape(userAvatar)
}

// generateRecognizedAvatarURL generate a recognized avatar (Gravatar/Libravatar) URL, it modifies the URL so the parameter is passed by a copy
func generateRecognizedAvatarURL(u url.URL, size int) string {
	urlQuery := u.Query()
	urlQuery.Set("d", "identicon")
	if size > 0 {
		urlQuery.Set("s", strconv.Itoa(size))
	}
	u.RawQuery = urlQuery.Encode()
	return u.String()
}

// generateEmailAvatarLink returns a email avatar link.
// if final is true, it may use a slow path (eg: query DNS).
// if final is false, it always uses a fast path.
func generateEmailAvatarLink(email string, size int, final bool) string {
	email = strings.TrimSpace(email)
	if email == "" {
		return DefaultAvatarLink()
	}

	var err error
	if setting.EnableFederatedAvatar && setting.LibravatarService != nil {
		emailHash := saveEmailHash(email)
		if final {
			// for final link, we can spend more time on slow external query
			var avatarURL *url.URL
			if avatarURL, err = LibravatarURL(email); err != nil {
				return DefaultAvatarLink()
			}
			return generateRecognizedAvatarURL(*avatarURL, size)
		}
		// for non-final link, we should return fast (use a 302 redirection link)
		urlStr := setting.AppSubURL + "/avatar/" + url.PathEscape(emailHash)
		if size > 0 {
			urlStr += "?size=" + strconv.Itoa(size)
		}
		return urlStr
	} else if !setting.DisableGravatar {
		// copy GravatarSourceURL, because we will modify its Path.
		avatarURLCopy := *setting.GravatarSourceURL
		avatarURLCopy.Path = path.Join(avatarURLCopy.Path, HashEmail(email))
		return generateRecognizedAvatarURL(avatarURLCopy, size)
	}
	return DefaultAvatarLink()
}

//GenerateEmailAvatarFastLink returns a avatar link (fast, the link may be a delegated one: "/avatar/${hash}")
func GenerateEmailAvatarFastLink(email string, size int) string {
	return generateEmailAvatarLink(email, size, false)
}

//GenerateEmailAvatarFinalLink returns a avatar final link (maybe slow)
func GenerateEmailAvatarFinalLink(email string, size int) string {
	return generateEmailAvatarLink(email, size, true)
}
<commit_msg>slight optimization for default avatar (#18187)<commit_after>// Copyright 2021 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package avatars

import (
	"context"
	"net/url"
	"path"
	"strconv"
	"strings"
	"sync"

	"code.gitea.io/gitea/models/db"
	"code.gitea.io/gitea/modules/base"
	"code.gitea.io/gitea/modules/cache"
	"code.gitea.io/gitea/modules/log"
	"code.gitea.io/gitea/modules/setting"
)

// DefaultAvatarPixelSize is the default size in pixels of a rendered avatar
const DefaultAvatarPixelSize = 28

// EmailHash represents a pre-generated hash map (mainly used by LibravatarURL, it queries email server's DNS records)
type EmailHash struct {
	Hash  string `xorm:"pk varchar(32)"`
	Email string `xorm:"UNIQUE NOT NULL"`
}

func init() {
	db.RegisterModel(new(EmailHash))
}

var (
	defaultAvatarLink string
	once              sync.Once
)

// DefaultAvatarLink the default avatar link
func DefaultAvatarLink() string {
	once.Do(func() {
		u, err := url.Parse(setting.AppSubURL)
		if err != nil {
			log.Error("Can not parse AppSubURL: %v", err)
			return
		}

		u.Path = path.Join(u.Path, "/assets/img/avatar_default.png")
		defaultAvatarLink = u.String()
	})
	return defaultAvatarLink
}

// HashEmail hashes email address to MD5 string. https://en.gravatar.com/site/implement/hash/
func HashEmail(email string) string {
	return base.EncodeMD5(strings.ToLower(strings.TrimSpace(email)))
}

// GetEmailForHash converts a provided md5sum to the email
func GetEmailForHash(md5Sum string) (string, error) {
	return cache.GetString("Avatar:"+md5Sum, func() (string, error) {
		emailHash := EmailHash{
			Hash: strings.ToLower(strings.TrimSpace(md5Sum)),
		}

		_, err := db.GetEngine(db.DefaultContext).Get(&emailHash)
		return emailHash.Email, err
	})
}

// LibravatarURL returns the URL for the given email. Slow due to the DNS lookup.
// This function should only be called if a federated avatar service is enabled.
func LibravatarURL(email string) (*url.URL, error) {
	urlStr, err := setting.LibravatarService.FromEmail(email)
	if err != nil {
		log.Error("LibravatarService.FromEmail(email=%s): error %v", email, err)
		return nil, err
	}
	u, err := url.Parse(urlStr)
	if err != nil {
		log.Error("Failed to parse libravatar url(%s): error %v", urlStr, err)
		return nil, err
	}
	return u, nil
}

// saveEmailHash returns an avatar link for a provided email,
// the email and hash are saved into database, which will be used by GetEmailForHash later
func saveEmailHash(email string) string {
	lowerEmail := strings.ToLower(strings.TrimSpace(email))
	emailHash := HashEmail(lowerEmail)
	_, _ = cache.GetString("Avatar:"+emailHash, func() (string, error) {
		emailHash := &EmailHash{
			Email: lowerEmail,
			Hash:  emailHash,
		}
		// OK we're going to open a session just because I think that that might hide away any problems with postgres reporting errors
		if err := db.WithTx(func(ctx context.Context) error {
			has, err := db.GetEngine(ctx).Where("email = ? AND hash = ?", emailHash.Email, emailHash.Hash).Get(new(EmailHash))
			if has || err != nil {
				// Seriously we don't care about any DB problems just return the lowerEmail - we expect the transaction to fail most of the time
				return nil
			}
			_, _ = db.GetEngine(ctx).Insert(emailHash)
			return nil
		}); err != nil {
			// Seriously we don't care about any DB problems just return the lowerEmail - we expect the transaction to fail most of the time
			return lowerEmail, nil
		}
		return lowerEmail, nil
	})
	return emailHash
}

// GenerateUserAvatarFastLink returns a fast link (302) to the user's avatar: "/user/avatar/${User.Name}/${size}"
func GenerateUserAvatarFastLink(userName string, size int) string {
	if size < 0 {
		size = 0
	}
	return setting.AppSubURL + "/user/avatar/" + url.PathEscape(userName) + "/" + strconv.Itoa(size)
}

// GenerateUserAvatarImageLink returns a link for `User.Avatar` image file: "/avatars/${User.Avatar}"
func GenerateUserAvatarImageLink(userAvatar string, size int) string {
	if size > 0 {
		return setting.AppSubURL + "/avatars/" + url.PathEscape(userAvatar) + "?size=" + strconv.Itoa(size)
	}
	return setting.AppSubURL + "/avatars/" + url.PathEscape(userAvatar)
}

// generateRecognizedAvatarURL generate a recognized avatar (Gravatar/Libravatar) URL, it modifies the URL so the parameter is passed by a copy
func generateRecognizedAvatarURL(u url.URL, size int) string {
	urlQuery := u.Query()
	urlQuery.Set("d", "identicon")
	if size > 0 {
		urlQuery.Set("s", strconv.Itoa(size))
	}
	u.RawQuery = urlQuery.Encode()
	return u.String()
}

// generateEmailAvatarLink returns a email avatar link.
// if final is true, it may use a slow path (eg: query DNS).
// if final is false, it always uses a fast path.
func generateEmailAvatarLink(email string, size int, final bool) string {
	email = strings.TrimSpace(email)
	if email == "" {
		return DefaultAvatarLink()
	}

	var err error
	if setting.EnableFederatedAvatar && setting.LibravatarService != nil {
		emailHash := saveEmailHash(email)
		if final {
			// for final link, we can spend more time on slow external query
			var avatarURL *url.URL
			if avatarURL, err = LibravatarURL(email); err != nil {
				return DefaultAvatarLink()
			}
			return generateRecognizedAvatarURL(*avatarURL, size)
		}
		// for non-final link, we should return fast (use a 302 redirection link)
		urlStr := setting.AppSubURL + "/avatar/" + url.PathEscape(emailHash)
		if size > 0 {
			urlStr += "?size=" + strconv.Itoa(size)
		}
		return urlStr
	} else if !setting.DisableGravatar {
		// copy GravatarSourceURL, because we will modify its Path.
		avatarURLCopy := *setting.GravatarSourceURL
		avatarURLCopy.Path = path.Join(avatarURLCopy.Path, HashEmail(email))
		return generateRecognizedAvatarURL(avatarURLCopy, size)
	}
	return DefaultAvatarLink()
}

//GenerateEmailAvatarFastLink returns a avatar link (fast, the link may be a delegated one: "/avatar/${hash}")
func GenerateEmailAvatarFastLink(email string, size int) string {
	return generateEmailAvatarLink(email, size, false)
}

//GenerateEmailAvatarFinalLink returns a avatar final link (maybe slow)
func GenerateEmailAvatarFinalLink(email string, size int) string {
	return generateEmailAvatarLink(email, size, true)
}
<|endoftext|>


<commit_before>package models

import (
	"errors"
	"fmt"

	log "github.com/Sirupsen/logrus"
)

// -------------------
// --- Struct methods

// Validate ...
func (env EnvironmentItemModel) Validate() error {
	key, _, err := env.GetKeyValuePair()
	if err != nil {
		return err
	}
	if key == "" {
		return errors.New("Invalid environment: empty env_key")
	}

	options, err := env.GetOptions()
	if err != nil {
		return err
	}

	if options.Title == nil || *options.Title == "" {
		return errors.New("Invalid environment: missing or empty title")
	}

	return nil
}

// Validate ...
func (step StepModel) Validate() error {
	if step.Title == nil || *step.Title == "" {
		return errors.New("Invalid step: missing or empty title")
	}
	if step.Summary == nil || *step.Summary == "" {
		return errors.New("Invalid step: missing or empty summary")
	}
	if step.Website == nil || *step.Website == "" {
		return errors.New("Invalid step: missing or empty website")
	}
	if step.Source.Git == nil || *step.Source.Git == "" {
		return errors.New("Invalid step: missing or empty source")
	}
	for _, input := range step.Inputs {
		err := input.Validate()
		if err != nil {
			return err
		}
	}
	for _, output := range step.Outputs {
		err := output.Validate()
		if err != nil {
			return err
		}
	}
	return nil
}

// GetStep ...
func (collection StepCollectionModel) GetStep(id, version string) (StepModel, bool) {
	stepHash := collection.Steps
	stepVersions, found := stepHash[id]
	if !found {
		return StepModel{}, false
	}
	step, found := stepVersions.Versions[version]
	if !found {
		return StepModel{}, false
	}
	return step, true
}

// GetDownloadLocations ...
func (collection StepCollectionModel) GetDownloadLocations(id, version string) ([]DownloadLocationModel, error) {
	locations := []DownloadLocationModel{}
	for _, downloadLocation := range collection.DownloadLocations {
		switch downloadLocation.Type {
		case "zip":
			url := downloadLocation.Src + id + "/" + version + "/step.zip"
			location := DownloadLocationModel{
				Type: downloadLocation.Type,
				Src:  url,
			}
			locations = append(locations, location)
		case "git":
			step, found := collection.GetStep(id, version)
			if found {
				location := DownloadLocationModel{
					Type: downloadLocation.Type,
					Src:  *step.Source.Git,
				}
				locations = append(locations, location)
			}
		default:
			return []DownloadLocationModel{}, fmt.Errorf("[STEPMAN] - Invalid download location (%#v) for step (%#v)", downloadLocation, id)
		}
	}
	if len(locations) < 1 {
		return []DownloadLocationModel{}, fmt.Errorf("[STEPMAN] - No download location found for step (%#v)", id)
	}
	return locations, nil
}

// GetKeyValuePair ...
func (env EnvironmentItemModel) GetKeyValuePair() (string, string, error) {
	if len(env) > 2 {
		return "", "", errors.New("Invalid env: more then 2 fields ")
	}

	retKey := ""
	retValue := ""

	for key, value := range env {
		if key != optionsKey {
			if retKey != "" {
				return "", "", errors.New("Invalid env: more then 1 key-value field found!")
			}

			valueStr, ok := value.(string)
			if ok == false {
				return "", "", fmt.Errorf("Invalid value (key:%#v) (value:%#v)", key, value)
			}

			retKey = key
			retValue = valueStr
		}
	}

	if retKey == "" {
		return "", "", errors.New("Invalid env: no envKey specified!")
	}

	return retKey, retValue, nil
}

// ParseFromInterfaceMap ...
func (envSerModel *EnvironmentItemOptionsModel) ParseFromInterfaceMap(input map[interface{}]interface{}) error {
	for key, value := range input {
		keyStr, ok := key.(string)
		if !ok {
			return fmt.Errorf("Invalid key, should be a string: %#v", key)
		}
		switch keyStr {
		case "title":
			castedValue, ok := value.(string)
			if !ok {
				return fmt.Errorf("Invalid value type (key:%s): %#v", keyStr, value)
			}
			*envSerModel.Title = castedValue
		case "description":
			castedValue, ok := value.(string)
			if !ok {
				return fmt.Errorf("Invalid value type (key:%s): %#v", keyStr, value)
			}
			*envSerModel.Description = castedValue
		case "value_options":
			castedValue, ok := value.([]string)
			if !ok {
				// try with []interface{} instead and cast the
				//  items to string
				castedValue = []string{}
				interfArr, ok := value.([]interface{})
				if !ok {
					return fmt.Errorf("Invalid value type (key:%s): %#v", keyStr, value)
				}
				for _, interfItm := range interfArr {
					castedItm, ok := interfItm.(string)
					if !ok {
						return fmt.Errorf("Invalid value in value_options (%#v), not a string: %#v", interfArr, interfItm)
					}
					castedValue = append(castedValue, castedItm)
				}
			}
			envSerModel.ValueOptions = castedValue
		case "is_required":
			castedValue, ok := value.(bool)
			if !ok {
				return fmt.Errorf("Invalid value type (key:%s): %#v", keyStr, value)
			}
			envSerModel.IsRequired = &castedValue
		case "is_expand":
			castedValue, ok := value.(bool)
			if !ok {
				return fmt.Errorf("Invalid value type (key:%s): %#v", keyStr, value)
			}
			envSerModel.IsExpand = &castedValue
		case "is_dont_change_value":
			castedValue, ok := value.(bool)
			if !ok {
				return fmt.Errorf("Invalid value type (key:%s): %#v", keyStr, value)
			}
			envSerModel.IsDontChangeValue = &castedValue
		default:
			return fmt.Errorf("Not supported key found in options: %#v", key)
		}
	}
	return nil
}

// GetOptions ...
func (env EnvironmentItemModel) GetOptions() (EnvironmentItemOptionsModel, error) {
	if len(env) > 2 {
		return EnvironmentItemOptionsModel{}, errors.New("Invalid env: more then 2 field")
	}

	optsShouldExist := false
	if len(env) == 2 {
		optsShouldExist = true
	}

	value, found := env[optionsKey]
	if !found {
		if optsShouldExist {
			return EnvironmentItemOptionsModel{}, errors.New("Invalid env: 2 fields but, no opts found")
		}
		return EnvironmentItemOptionsModel{}, nil
	}

	envItmCasted, ok := value.(EnvironmentItemOptionsModel)
	if ok {
		return envItmCasted, nil
	}

	// if it's read from a file (YAML/JSON) then it's most likely not the proper type
	//  so cast it from the generic interface-interface map
	optionsInterfaceMap, ok := value.(map[interface{}]interface{})
	if !ok {
		return EnvironmentItemOptionsModel{}, fmt.Errorf("Invalid options (value:%#v) - failed to map-interface cast", value)
	}

	options := EnvironmentItemOptionsModel{}
	err := options.ParseFromInterfaceMap(optionsInterfaceMap)
	if err != nil {
		return EnvironmentItemOptionsModel{}, err
	}

	log.Debugf("Parsed options: %#v\n", options)

	return options, nil
}
<commit_msg>GetOptions fixes<commit_after>package models

import (
	"errors"
	"fmt"

	log "github.com/Sirupsen/logrus"
)

// -------------------
// --- Struct methods

// Validate ...
func (env EnvironmentItemModel) Validate() error {
	key, _, err := env.GetKeyValuePair()
	if err != nil {
		return err
	}
	if key == "" {
		return errors.New("Invalid environment: empty env_key")
	}

	options, err := env.GetOptions()
	if err != nil {
		return err
	}

	if options.Title == nil || *options.Title == "" {
		return errors.New("Invalid environment: missing or empty title")
	}

	return nil
}

// Validate ...
func (step StepModel) Validate() error {
	if step.Title == nil || *step.Title == "" {
		return errors.New("Invalid step: missing or empty title")
	}
	if step.Summary == nil || *step.Summary == "" {
		return errors.New("Invalid step: missing or empty summary")
	}
	if step.Website == nil || *step.Website == "" {
		return errors.New("Invalid step: missing or empty website")
	}
	if step.Source.Git == nil || *step.Source.Git == "" {
		return errors.New("Invalid step: missing or empty source")
	}
	for _, input := range step.Inputs {
		err := input.Validate()
		if err != nil {
			return err
		}
	}
	for _, output := range step.Outputs {
		err := output.Validate()
		if err != nil {
			return err
		}
	}
	return nil
}

// GetStep ...
func (collection StepCollectionModel) GetStep(id, version string) (StepModel, bool) {
	stepHash := collection.Steps
	stepVersions, found := stepHash[id]
	if !found {
		return StepModel{}, false
	}
	step, found := stepVersions.Versions[version]
	if !found {
		return StepModel{}, false
	}
	return step, true
}

// GetDownloadLocations ...
func (collection StepCollectionModel) GetDownloadLocations(id, version string) ([]DownloadLocationModel, error) {
	locations := []DownloadLocationModel{}
	for _, downloadLocation := range collection.DownloadLocations {
		switch downloadLocation.Type {
		case "zip":
			url := downloadLocation.Src + id + "/" + version + "/step.zip"
			location := DownloadLocationModel{
				Type: downloadLocation.Type,
				Src:  url,
			}
			locations = append(locations, location)
		case "git":
			step, found := collection.GetStep(id, version)
			if found {
				location := DownloadLocationModel{
					Type: downloadLocation.Type,
					Src:  *step.Source.Git,
				}
				locations = append(locations, location)
			}
		default:
			return []DownloadLocationModel{}, fmt.Errorf("[STEPMAN] - Invalid download location (%#v) for step (%#v)", downloadLocation, id)
		}
	}
	if len(locations) < 1 {
		return []DownloadLocationModel{}, fmt.Errorf("[STEPMAN] - No download location found for step (%#v)", id)
	}
	return locations, nil
}

// GetKeyValuePair ...
func (env EnvironmentItemModel) GetKeyValuePair() (string, string, error) {
	if len(env) > 2 {
		return "", "", errors.New("Invalid env: more then 2 fields ")
	}

	retKey := ""
	retValue := ""

	for key, value := range env {
		if key != optionsKey {
			if retKey != "" {
				return "", "", errors.New("Invalid env: more then 1 key-value field found!")
			}

			valueStr, ok := value.(string)
			if ok == false {
				return "", "", fmt.Errorf("Invalid value (key:%#v) (value:%#v)", key, value)
			}

			retKey = key
			retValue = valueStr
		}
	}

	if retKey == "" {
		return "", "", errors.New("Invalid env: no envKey specified!")
	}

	return retKey, retValue, nil
}

// ParseFromInterfaceMap ...
func (envSerModel *EnvironmentItemOptionsModel) ParseFromInterfaceMap(input map[interface{}]interface{}) error {
	for key, value := range input {
		keyStr, ok := key.(string)
		if !ok {
			return fmt.Errorf("Invalid key, should be a string: %#v", key)
		}
		switch keyStr {
		case "title":
			castedValue, ok := value.(string)
			if !ok {
				return fmt.Errorf("Invalid value type (key:%s): %#v", keyStr, value)
			}
			*envSerModel.Title = castedValue
		case "description":
			castedValue, ok := value.(string)
			if !ok {
				return fmt.Errorf("Invalid value type (key:%s): %#v", keyStr, value)
			}
			*envSerModel.Description = castedValue
		case "value_options":
			castedValue, ok := value.([]string)
			if !ok {
				// try with []interface{} instead and cast the
				//  items to string
				castedValue = []string{}
				interfArr, ok := value.([]interface{})
				if !ok {
					return fmt.Errorf("Invalid value type (key:%s): %#v", keyStr, value)
				}
				for _, interfItm := range interfArr {
					castedItm, ok := interfItm.(string)
					if !ok {
						return fmt.Errorf("Invalid value in value_options (%#v), not a string: %#v", interfArr, interfItm)
					}
					castedValue = append(castedValue, castedItm)
				}
			}
			envSerModel.ValueOptions = castedValue
		case "is_required":
			castedValue, ok := value.(bool)
			if !ok {
				return fmt.Errorf("Invalid value type (key:%s): %#v", keyStr, value)
			}
			envSerModel.IsRequired = &castedValue
		case "is_expand":
			castedValue, ok := value.(bool)
			if !ok {
				return fmt.Errorf("Invalid value type (key:%s): %#v", keyStr, value)
			}
			envSerModel.IsExpand = &castedValue
		case "is_dont_change_value":
			castedValue, ok := value.(bool)
			if !ok {
				return fmt.Errorf("Invalid value type (key:%s): %#v", keyStr, value)
			}
			envSerModel.IsDontChangeValue = &castedValue
		default:
			return fmt.Errorf("Not supported key found in options: %#v", key)
		}
	}
	return nil
}

// GetOptions ...
func (env EnvironmentItemModel) GetOptions() (EnvironmentItemOptionsModel, error) {
	value, found := env[optionsKey]
	if !found {
		return EnvironmentItemOptionsModel{}, nil
	}

	envItmCasted, ok := value.(EnvironmentItemOptionsModel)
	if ok {
		return envItmCasted, nil
	}

	// if it's read from a file (YAML/JSON) then it's most likely not the proper type
	//  so cast it from the generic interface-interface map
	optionsInterfaceMap, ok := value.(map[interface{}]interface{})
	if !ok {
		return EnvironmentItemOptionsModel{}, fmt.Errorf("Invalid options (value:%#v) - failed to map-interface cast", value)
	}

	options := EnvironmentItemOptionsModel{}
	err := options.ParseFromInterfaceMap(optionsInterfaceMap)
	if err != nil {
		return EnvironmentItemOptionsModel{}, err
	}

	log.Debugf("Parsed options: %#v\n", options)

	return options, nil
}
<|endoftext|>


<commit_before><commit_msg>adds support for properties to ACMCertificate (#353)<commit_after><|endoftext|>


<commit_before>package gateway

import (
	"errors"
	"net"
	"time"

	"github.com/NebulousLabs/Sia/build"
	"github.com/NebulousLabs/Sia/crypto"
	"github.com/NebulousLabs/Sia/encoding"
	"github.com/NebulousLabs/Sia/modules"

	"github.com/inconshreveable/muxado"
)

const (
	dialTimeout = 2 * time.Minute
	// the gateway will not make outbound connections above this threshold
	wellConnectedThreshold = 8
	// the gateway will not accept inbound connections above this threshold
	fullyConnectedThreshold = 128
	// the gateway will ask for more addresses below this threshold
	minNodeListLen = 100
)

type peer struct {
	addr    modules.NetAddress
	sess    muxado.Session
	inbound bool
}

func (p *peer) open() (modules.PeerConn, error) {
	conn, err := p.sess.Open()
	if err != nil {
		return nil, err
	}
	return &peerConn{conn}, nil
}

func (p *peer) accept() (modules.PeerConn, error) {
	conn, err := p.sess.Accept()
	if err != nil {
		return nil, err
	}
	return &peerConn{conn}, nil
}

// addPeer adds a peer to the Gateway's peer list and spawns a listener thread
// to handle its requests.
func (g *Gateway) addPeer(p *peer) {
	g.peers[p.addr] = p
	go g.listenPeer(p)
}

// randomPeer returns a random peer from the gateway's peer list.
func (g *Gateway) randomPeer() (modules.NetAddress, error) {
	if len(g.peers) > 0 {
		r, _ := crypto.RandIntn(len(g.peers))
		for addr := range g.peers {
			if r <= 0 {
				return addr, nil
			}
			r--
		}
	}

	return "", errNoPeers
}

// randomInboundPeer returns a random peer that initiated its connection.
func (g *Gateway) randomInboundPeer() (modules.NetAddress, error) {
	if len(g.peers) > 0 {
		r, _ := crypto.RandIntn(len(g.peers))
		for addr, peer := range g.peers {
			// only select inbound peers
			if !peer.inbound {
				continue
			}
			if r <= 0 {
				return addr, nil
			}
			r--
		}
	}

	return "", errNoPeers
}

// listen handles incoming connection requests. If the connection is accepted,
// the peer will be added to the Gateway's peer list.
func (g *Gateway) listen() {
	for {
		conn, err := g.listener.Accept()
		if err != nil {
			return
		}

		go g.acceptConn(conn)
	}
}

// acceptConn adds a connecting node as a peer.
func (g *Gateway) acceptConn(conn net.Conn) {
	addr := modules.NetAddress(conn.RemoteAddr().String())
	g.log.Printf("INFO: %v wants to connect", addr)

	// read version
	var remoteVersion string
	if err := encoding.ReadObject(conn, &remoteVersion, maxAddrLength); err != nil {
		conn.Close()
		g.log.Printf("INFO: %v wanted to connect, but we could not read their version: %v", addr, err)
		return
	}

	// check that version is acceptable
	// NOTE: this version must be bumped whenever the gateway or consensus
	// breaks compatibility.
	if build.VersionCmp(remoteVersion, "0.3.3") < 0 {
		encoding.WriteObject(conn, "reject")
		conn.Close()
		g.log.Printf("INFO: %v wanted to connect, but their version (%v) was unacceptable", addr, remoteVersion)
		return
	}

	// respond with our version
	if err := encoding.WriteObject(conn, "0.3.3"); err != nil {
		conn.Close()
		g.log.Printf("INFO: could not write version ack to %v: %v", addr, err)
		return
	}

	// If we are already fully connected, kick out an old peer to make room
	// for the new one. Importantly, prioritize kicking a peer with the same
	// IP as the connecting peer. This protects against Sybil attacks.
	id := g.mu.Lock()
	if len(g.peers) >= fullyConnectedThreshold {
		// first choose a random peer
		kick, _ := g.randomPeer()
		// try to pick a better one
		for peer := range g.peers {
			if peer.Host() == addr.Host() {
				kick = peer
				break
			}
		}
		g.peers[kick].sess.Close()
		delete(g.peers, kick)
		g.log.Printf("INFO: disconnected from %v to make room for %v", kick, addr)
	}
	// add the peer
	g.addPeer(&peer{addr: addr, sess: muxado.Server(conn), inbound: true})
	g.mu.Unlock(id)

	g.log.Printf("INFO: accepted connection from new peer %v (v%v)", addr, remoteVersion)
}

// Connect establishes a persistent connection to a peer, and adds it to the
// Gateway's peer list.
func (g *Gateway) Connect(addr modules.NetAddress) error {
	if addr == g.Address() {
		return errors.New("can't connect to our own address")
	}

	id := g.mu.RLock()
	_, exists := g.peers[addr]
	g.mu.RUnlock(id)
	if exists {
		return errors.New("peer already added")
	}

	conn, err := net.DialTimeout("tcp", string(addr), dialTimeout)
	if err != nil {
		return err
	}
	// send our version
	if err := encoding.WriteObject(conn, "0.3.3"); err != nil {
		return err
	}
	// read version ack
	var remoteVersion string
	if err := encoding.ReadObject(conn, &remoteVersion, maxAddrLength); err != nil {
		return err
	} else if remoteVersion == "reject" {
		return errors.New("peer rejected connection")
	}
	// decide whether to accept this version
	if build.VersionCmp(remoteVersion, "0.3.3") < 0 {
		conn.Close()
		return errors.New("unacceptable version: " + remoteVersion)
	}

	g.log.Println("INFO: connected to new peer", addr)

	id = g.mu.Lock()
	g.addPeer(&peer{addr: addr, sess: muxado.Client(conn), inbound: false})
	g.mu.Unlock(id)

	// call initRPCs
	id = g.mu.RLock()
	for name, fn := range g.initRPCs {
		go g.RPC(addr, name, fn)
	}
	g.mu.RUnlock(id)

	return nil
}

// Disconnect terminates a connection to a peer and removes it from the
// Gateway's peer list. The peer's address remains in the node list.
func (g *Gateway) Disconnect(addr modules.NetAddress) error {
	id := g.mu.RLock()
	p, exists := g.peers[addr]
	g.mu.RUnlock(id)
	if !exists {
		return errors.New("not connected to that node")
	}
	p.sess.Close()
	id = g.mu.Lock()
	delete(g.peers, addr)
	g.mu.Unlock(id)

	g.log.Println("INFO: disconnected from peer", addr)
	return nil
}

// threadedPeerManager tries to keep the Gateway well-connected. As long as
// the Gateway is not well-connected, it tries to connect to random nodes.
func (g *Gateway) threadedPeerManager() {
	for {
		// If we are well-connected, sleep in increments of five minutes until
		// we are no longer well-connected.
		id := g.mu.RLock()
		numPeers := len(g.peers)
		addr, err := g.randomNode()
		g.mu.RUnlock(id)
		if numPeers >= wellConnectedThreshold {
			time.Sleep(5 * time.Minute)
			continue
		}

		// Try to connect to a random node. Instead of blocking on Connect, we
		// spawn a goroutine and sleep for five seconds. This allows us to
		// continue making connections if the node is unresponsive.
		if err == nil {
			go g.Connect(addr)
		}
		time.Sleep(5 * time.Second)
	}
}

func (g *Gateway) Peers() []modules.NetAddress {
	id := g.mu.RLock()
	defer g.mu.RUnlock(id)
	var peers []modules.NetAddress
	for addr := range g.peers {
		peers = append(peers, addr)
	}
	return peers
}
<commit_msg>only outbound peers count towards threshold<commit_after>package gateway

import (
	"errors"
	"net"
	"time"

	"github.com/NebulousLabs/Sia/build"
	"github.com/NebulousLabs/Sia/crypto"
	"github.com/NebulousLabs/Sia/encoding"
	"github.com/NebulousLabs/Sia/modules"

	"github.com/inconshreveable/muxado"
)

const (
	dialTimeout = 2 * time.Minute
	// the gateway will not make outbound connections above this threshold
	wellConnectedThreshold = 8
	// the gateway will not accept inbound connections above this threshold
	fullyConnectedThreshold = 128
	// the gateway will ask for more addresses below this threshold
	minNodeListLen = 100
)

type peer struct {
	addr    modules.NetAddress
	sess    muxado.Session
	inbound bool
}

func (p *peer) open() (modules.PeerConn, error) {
	conn, err := p.sess.Open()
	if err != nil {
		return nil, err
	}
	return &peerConn{conn}, nil
}

func (p *peer) accept() (modules.PeerConn, error) {
	conn, err := p.sess.Accept()
	if err != nil {
		return nil, err
	}
	return &peerConn{conn}, nil
}

// addPeer adds a peer to the Gateway's peer list and spawns a listener thread
// to handle its requests.
func (g *Gateway) addPeer(p *peer) {
	g.peers[p.addr] = p
	go g.listenPeer(p)
}

// randomPeer returns a random peer from the gateway's peer list.
func (g *Gateway) randomPeer() (modules.NetAddress, error) {
	if len(g.peers) > 0 {
		r, _ := crypto.RandIntn(len(g.peers))
		for addr := range g.peers {
			if r <= 0 {
				return addr, nil
			}
			r--
		}
	}

	return "", errNoPeers
}

// randomInboundPeer returns a random peer that initiated its connection.
func (g *Gateway) randomInboundPeer() (modules.NetAddress, error) {
	if len(g.peers) > 0 {
		r, _ := crypto.RandIntn(len(g.peers))
		for addr, peer := range g.peers {
			// only select inbound peers
			if !peer.inbound {
				continue
			}
			if r <= 0 {
				return addr, nil
			}
			r--
		}
	}

	return "", errNoPeers
}

// listen handles incoming connection requests. If the connection is accepted,
// the peer will be added to the Gateway's peer list.
func (g *Gateway) listen() {
	for {
		conn, err := g.listener.Accept()
		if err != nil {
			return
		}

		go g.acceptConn(conn)
	}
}

// acceptConn adds a connecting node as a peer.
func (g *Gateway) acceptConn(conn net.Conn) {
	addr := modules.NetAddress(conn.RemoteAddr().String())
	g.log.Printf("INFO: %v wants to connect", addr)

	// read version
	var remoteVersion string
	if err := encoding.ReadObject(conn, &remoteVersion, maxAddrLength); err != nil {
		conn.Close()
		g.log.Printf("INFO: %v wanted to connect, but we could not read their version: %v", addr, err)
		return
	}

	// check that version is acceptable
	// NOTE: this version must be bumped whenever the gateway or consensus
	// breaks compatibility.
	if build.VersionCmp(remoteVersion, "0.3.3") < 0 {
		encoding.WriteObject(conn, "reject")
		conn.Close()
		g.log.Printf("INFO: %v wanted to connect, but their version (%v) was unacceptable", addr, remoteVersion)
		return
	}

	// respond with our version
	if err := encoding.WriteObject(conn, "0.3.3"); err != nil {
		conn.Close()
		g.log.Printf("INFO: could not write version ack to %v: %v", addr, err)
		return
	}

	// If we are already fully connected, kick out an old peer to make room
	// for the new one. Importantly, prioritize kicking a peer with the same
	// IP as the connecting peer. This protects against Sybil attacks.
	id := g.mu.Lock()
	if len(g.peers) >= fullyConnectedThreshold {
		// first choose a random peer
		kick, _ := g.randomPeer()
		// try to pick a better one
		for peer := range g.peers {
			if peer.Host() == addr.Host() {
				kick = peer
				break
			}
		}
		g.peers[kick].sess.Close()
		delete(g.peers, kick)
		g.log.Printf("INFO: disconnected from %v to make room for %v", kick, addr)
	}
	// add the peer
	g.addPeer(&peer{addr: addr, sess: muxado.Server(conn), inbound: true})
	g.mu.Unlock(id)

	g.log.Printf("INFO: accepted connection from new peer %v (v%v)", addr, remoteVersion)
}

// Connect establishes a persistent connection to a peer, and adds it to the
// Gateway's peer list.
func (g *Gateway) Connect(addr modules.NetAddress) error {
	if addr == g.Address() {
		return errors.New("can't connect to our own address")
	}

	id := g.mu.RLock()
	_, exists := g.peers[addr]
	g.mu.RUnlock(id)
	if exists {
		return errors.New("peer already added")
	}

	conn, err := net.DialTimeout("tcp", string(addr), dialTimeout)
	if err != nil {
		return err
	}
	// send our version
	if err := encoding.WriteObject(conn, "0.3.3"); err != nil {
		return err
	}
	// read version ack
	var remoteVersion string
	if err := encoding.ReadObject(conn, &remoteVersion, maxAddrLength); err != nil {
		return err
	} else if remoteVersion == "reject" {
		return errors.New("peer rejected connection")
	}
	// decide whether to accept this version
	if build.VersionCmp(remoteVersion, "0.3.3") < 0 {
		conn.Close()
		return errors.New("unacceptable version: " + remoteVersion)
	}

	g.log.Println("INFO: connected to new peer", addr)

	id = g.mu.Lock()
	g.addPeer(&peer{addr: addr, sess: muxado.Client(conn), inbound: false})
	g.mu.Unlock(id)

	// call initRPCs
	id = g.mu.RLock()
	for name, fn := range g.initRPCs {
		go g.RPC(addr, name, fn)
	}
	g.mu.RUnlock(id)

	return nil
}

// Disconnect terminates a connection to a peer and removes it from the
// Gateway's peer list. The peer's address remains in the node list.
func (g *Gateway) Disconnect(addr modules.NetAddress) error {
	id := g.mu.RLock()
	p, exists := g.peers[addr]
	g.mu.RUnlock(id)
	if !exists {
		return errors.New("not connected to that node")
	}
	p.sess.Close()
	id = g.mu.Lock()
	delete(g.peers, addr)
	g.mu.Unlock(id)

	g.log.Println("INFO: disconnected from peer", addr)
	return nil
}

// threadedPeerManager tries to keep the Gateway well-connected. As long as
// the Gateway is not well-connected, it tries to connect to random nodes.
func (g *Gateway) threadedPeerManager() {
	for {
		// If we are well-connected, sleep in increments of five minutes until
		// we are no longer well-connected.
		id := g.mu.RLock()
		numOutboundPeers := 0
		for _, p := range g.peers {
			if !p.inbound {
				numOutboundPeers++
			}
		}
		addr, err := g.randomNode()
		g.mu.RUnlock(id)
		if numOutboundPeers >= wellConnectedThreshold {
			time.Sleep(5 * time.Minute)
			continue
		}

		// Try to connect to a random node. Instead of blocking on Connect, we
		// spawn a goroutine and sleep for five seconds. This allows us to
		// continue making connections if the node is unresponsive.
		if err == nil {
			go g.Connect(addr)
		}
		time.Sleep(5 * time.Second)
	}
}

func (g *Gateway) Peers() []modules.NetAddress {
	id := g.mu.RLock()
	defer g.mu.RUnlock(id)
	var peers []modules.NetAddress
	for addr := range g.peers {
		peers = append(peers, addr)
	}
	return peers
}
<|endoftext|>