<commit_before>// Copyright 2022 Nokia
// Licensed under the BSD 3-Clause License.
// SPDX-License-Identifier: BSD-3-Clause

package controllers

import (
	"context"
	"fmt"

	"github.com/go-logr/logr"
	knenode "github.com/openconfig/kne/topo/node"
	typesv1a1 "github.com/srl-labs/srl-controller/api/types/v1alpha1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/pointer"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/log"
)

const (
	terminationGracePeriodSeconds = 0
	licensesVolName               = "license"
	licenseFileName               = "license.key"
	licenseMntPath                = "/opt/srlinux/etc/license.key"
	licenseMntSubPath             = "license.key"
)

// podForSrlinux returns a srlinux Pod object.
func (r *SrlinuxReconciler) podForSrlinux(
	ctx context.Context,
	s *typesv1a1.Srlinux,
) *corev1.Pod {
	log := log.FromContext(ctx)

	if s.Spec.Config.Env == nil {
		s.Spec.Config.Env = map[string]string{}
	}

	s.Spec.Config.Env["SRLINUX"] = "1" // set default srlinux env var

	pod := &corev1.Pod{
		ObjectMeta: createObjectMeta(s),
		Spec: corev1.PodSpec{
			InitContainers:                createInitContainers(s),
			Containers:                    createContainers(s),
			TerminationGracePeriodSeconds: pointer.Int64(terminationGracePeriodSeconds),
			NodeSelector:                  map[string]string{},
			Affinity:                      createAffinity(s),
			Volumes:                       createVolumes(s),
		},
	}

	// handle startup config volume mounts if the startup config was defined
	handleStartupConfig(s, pod, log)

	_ = ctrl.SetControllerReference(s, pod, r.Scheme)

	return pod
}

func createObjectMeta(s *typesv1a1.Srlinux) metav1.ObjectMeta {
	return metav1.ObjectMeta{
		Name:      s.Name,
		Namespace: s.Namespace,
		Labels: map[string]string{
			"app":  s.Name,
			"topo": s.Namespace,
		},
	}
}

func createInitContainers(s *typesv1a1.Srlinux) []corev1.Container {
	return []corev1.Container{{
		Name:  fmt.Sprintf("init-%s", s.Name),
		Image: initContainerName,
		Args: []string{
			fmt.Sprintf("%d", s.Spec.NumInterfaces+1),
			fmt.Sprintf("%d", s.Spec.Config.Sleep),
		},
		ImagePullPolicy: "IfNotPresent",
	}}
}

func createContainers(s *typesv1a1.Srlinux) []corev1.Container {
	return []corev1.Container{{
		Name:            s.Name,
		Image:           s.Spec.GetImage(),
		Command:         s.Spec.Config.GetCommand(),
		Args:            s.Spec.Config.GetArgs(),
		Env:             knenode.ToEnvVar(s.Spec.Config.Env),
		Resources:       knenode.ToResourceRequirements(s.Spec.GetConstraints()),
		ImagePullPolicy: "IfNotPresent",
		SecurityContext: &corev1.SecurityContext{
			Privileged: pointer.Bool(true),
			RunAsUser:  pointer.Int64(0),
		},
		VolumeMounts: createVolumeMounts(s),
	}}
}

func createAffinity(s *typesv1a1.Srlinux) *corev1.Affinity {
	return &corev1.Affinity{
		PodAntiAffinity: &corev1.PodAntiAffinity{
			PreferredDuringSchedulingIgnoredDuringExecution: []corev1.WeightedPodAffinityTerm{
				{
					Weight: srlinuxPodAffinityWeight,
					PodAffinityTerm: corev1.PodAffinityTerm{
						LabelSelector: &metav1.LabelSelector{
							MatchExpressions: []metav1.LabelSelectorRequirement{{
								Key:      "topo",
								Operator: "In",
								Values:   []string{s.Name},
							}},
						},
						TopologyKey: "kubernetes.io/hostname",
					},
				},
			},
		},
	}
}

func createVolumes(s *typesv1a1.Srlinux) []corev1.Volume {
	vols := []corev1.Volume{
		{
			Name: variantsVolName,
			VolumeSource: corev1.VolumeSource{
				ConfigMap: &corev1.ConfigMapVolumeSource{
					LocalObjectReference: corev1.LocalObjectReference{
						Name: variantsCfgMapName,
					},
					Items: []corev1.KeyToPath{
						{
							Key:  s.Spec.GetModel(),
							Path: variantsTemplateTempName,
						},
					},
				},
			},
		},
		{
			Name: topomacVolName,
			VolumeSource: corev1.VolumeSource{
				ConfigMap: &corev1.ConfigMapVolumeSource{
					LocalObjectReference: corev1.LocalObjectReference{
						Name: topomacCfgMapName,
					},
				},
			},
		},
		{
			Name: entrypointVolName,
			VolumeSource: corev1.VolumeSource{
				ConfigMap: &corev1.ConfigMapVolumeSource{
					LocalObjectReference: corev1.LocalObjectReference{
						Name: entrypointCfgMapName,
					},
					DefaultMode: pointer.Int32(fileMode777),
				},
			},
		},
	}

	if s.LicenseKey != "" {
		vols = append(vols, createLicenseVolume(s))
	}

	return vols
}

// handleStartupConfig creates volume mounts and volumes for srlinux pod
// if the (startup) config file was provided in the spec.
// Volume mounts happens in the /tmp/startup-config directory and not in the /etc/opt/srlinux
// because we need to support renaming operations on config.json, and bind mount paths are not allowing this.
// Hence the temp location, from which the config file is then copied to /etc/opt/srlinux by the kne-entrypoint.sh
func handleStartupConfig(s *typesv1a1.Srlinux, pod *corev1.Pod, log logr.Logger) {
	// initialize config path and config file variables
	cfgPath := defaultConfigPath
	if p := s.Spec.GetConfig().ConfigPath; p != "" {
		cfgPath = p
	}

	// only create startup config mounts if the config data was set in kne
	if s.Spec.Config.ConfigDataPresent {
		log.Info(
			"Adding volume for startup config to pod spec",
			"volume.name",
			"startup-config-volume",
			"mount.path",
			cfgPath,
		)

		pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{
			Name: "startup-config-volume",
			VolumeSource: corev1.VolumeSource{
				ConfigMap: &corev1.ConfigMapVolumeSource{
					LocalObjectReference: corev1.LocalObjectReference{
						Name: fmt.Sprintf("%s-config", s.Name),
					},
				},
			},
		})

		pod.Spec.Containers[0].VolumeMounts = append(
			pod.Spec.Containers[0].VolumeMounts,
			corev1.VolumeMount{
				Name:      "startup-config-volume",
				MountPath: cfgPath,
				ReadOnly:  false,
			},
		)
	}
}

func createVolumeMounts(s *typesv1a1.Srlinux) []corev1.VolumeMount {
	vms := []corev1.VolumeMount{
		{
			Name:      variantsVolName,
			MountPath: variantsVolMntPath,
		},
		{
			Name:      topomacVolName,
			MountPath: topomacVolMntPath,
		},
		{
			Name:      entrypointVolName,
			MountPath: entrypointVolMntPath,
			SubPath:   entrypointVolMntSubPath,
		},
	}

	if s.LicenseKey != "" {
		vms = append(vms, createLicenseVolumeMount())
	}

	return vms
}

func createLicenseVolume(s *typesv1a1.Srlinux) corev1.Volume {
	return corev1.Volume{
		Name: licensesVolName,
		VolumeSource: corev1.VolumeSource{
			Secret: &corev1.SecretVolumeSource{
				SecretName: srlLicenseSecretName,
				Items: []corev1.KeyToPath{
					{
						Key:  s.LicenseKey,
						Path: licenseFileName,
					},
				},
			},
		},
	}
}

func createLicenseVolumeMount() corev1.VolumeMount {
	return corev1.VolumeMount{
		Name:      licensesVolName,
		MountPath: licenseMntPath,
		SubPath:   licenseMntSubPath,
	}
}
<commit_msg>added dot<commit_after>// Copyright 2022 Nokia
// Licensed under the BSD 3-Clause License.
// SPDX-License-Identifier: BSD-3-Clause

package controllers

import (
	"context"
	"fmt"

	"github.com/go-logr/logr"
	knenode "github.com/openconfig/kne/topo/node"
	typesv1a1 "github.com/srl-labs/srl-controller/api/types/v1alpha1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/pointer"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/log"
)

const (
	terminationGracePeriodSeconds = 0
	licensesVolName               = "license"
	licenseFileName               = "license.key"
	licenseMntPath                = "/opt/srlinux/etc/license.key"
	licenseMntSubPath             = "license.key"
)

// podForSrlinux returns a srlinux Pod object.
func (r *SrlinuxReconciler) podForSrlinux(
	ctx context.Context,
	s *typesv1a1.Srlinux,
) *corev1.Pod {
	log := log.FromContext(ctx)

	if s.Spec.Config.Env == nil {
		s.Spec.Config.Env = map[string]string{}
	}

	s.Spec.Config.Env["SRLINUX"] = "1" // set default srlinux env var

	pod := &corev1.Pod{
		ObjectMeta: createObjectMeta(s),
		Spec: corev1.PodSpec{
			InitContainers:                createInitContainers(s),
			Containers:                    createContainers(s),
			TerminationGracePeriodSeconds: pointer.Int64(terminationGracePeriodSeconds),
			NodeSelector:                  map[string]string{},
			Affinity:                      createAffinity(s),
			Volumes:                       createVolumes(s),
		},
	}

	// handle startup config volume mounts if the startup config was defined
	handleStartupConfig(s, pod, log)

	_ = ctrl.SetControllerReference(s, pod, r.Scheme)

	return pod
}

func createObjectMeta(s *typesv1a1.Srlinux) metav1.ObjectMeta {
	return metav1.ObjectMeta{
		Name:      s.Name,
		Namespace: s.Namespace,
		Labels: map[string]string{
			"app":  s.Name,
			"topo": s.Namespace,
		},
	}
}

func createInitContainers(s *typesv1a1.Srlinux) []corev1.Container {
	return []corev1.Container{{
		Name:  fmt.Sprintf("init-%s", s.Name),
		Image: initContainerName,
		Args: []string{
			fmt.Sprintf("%d", s.Spec.NumInterfaces+1),
			fmt.Sprintf("%d", s.Spec.Config.Sleep),
		},
		ImagePullPolicy: "IfNotPresent",
	}}
}

func createContainers(s *typesv1a1.Srlinux) []corev1.Container {
	return []corev1.Container{{
		Name:            s.Name,
		Image:           s.Spec.GetImage(),
		Command:         s.Spec.Config.GetCommand(),
		Args:            s.Spec.Config.GetArgs(),
		Env:             knenode.ToEnvVar(s.Spec.Config.Env),
		Resources:       knenode.ToResourceRequirements(s.Spec.GetConstraints()),
		ImagePullPolicy: "IfNotPresent",
		SecurityContext: &corev1.SecurityContext{
			Privileged: pointer.Bool(true),
			RunAsUser:  pointer.Int64(0),
		},
		VolumeMounts: createVolumeMounts(s),
	}}
}

func createAffinity(s *typesv1a1.Srlinux) *corev1.Affinity {
	return &corev1.Affinity{
		PodAntiAffinity: &corev1.PodAntiAffinity{
			PreferredDuringSchedulingIgnoredDuringExecution: []corev1.WeightedPodAffinityTerm{
				{
					Weight: srlinuxPodAffinityWeight,
					PodAffinityTerm: corev1.PodAffinityTerm{
						LabelSelector: &metav1.LabelSelector{
							MatchExpressions: []metav1.LabelSelectorRequirement{{
								Key:      "topo",
								Operator: "In",
								Values:   []string{s.Name},
							}},
						},
						TopologyKey: "kubernetes.io/hostname",
					},
				},
			},
		},
	}
}

func createVolumes(s *typesv1a1.Srlinux) []corev1.Volume {
	vols := []corev1.Volume{
		{
			Name: variantsVolName,
			VolumeSource: corev1.VolumeSource{
				ConfigMap: &corev1.ConfigMapVolumeSource{
					LocalObjectReference: corev1.LocalObjectReference{
						Name: variantsCfgMapName,
					},
					Items: []corev1.KeyToPath{
						{
							Key:  s.Spec.GetModel(),
							Path: variantsTemplateTempName,
						},
					},
				},
			},
		},
		{
			Name: topomacVolName,
			VolumeSource: corev1.VolumeSource{
				ConfigMap: &corev1.ConfigMapVolumeSource{
					LocalObjectReference: corev1.LocalObjectReference{
						Name: topomacCfgMapName,
					},
				},
			},
		},
		{
			Name: entrypointVolName,
			VolumeSource: corev1.VolumeSource{
				ConfigMap: &corev1.ConfigMapVolumeSource{
					LocalObjectReference: corev1.LocalObjectReference{
						Name: entrypointCfgMapName,
					},
					DefaultMode: pointer.Int32(fileMode777),
				},
			},
		},
	}

	if s.LicenseKey != "" {
		vols = append(vols, createLicenseVolume(s))
	}

	return vols
}

// handleStartupConfig creates volume mounts and volumes for srlinux pod
// if the (startup) config file was provided in the spec.
// Volume mounts happens in the /tmp/startup-config directory and not in the /etc/opt/srlinux
// because we need to support renaming operations on config.json, and bind mount paths are not allowing this.
// Hence the temp location, from which the config file is then copied to /etc/opt/srlinux by the kne-entrypoint.sh.
func handleStartupConfig(s *typesv1a1.Srlinux, pod *corev1.Pod, log logr.Logger) {
	// initialize config path and config file variables
	cfgPath := defaultConfigPath
	if p := s.Spec.GetConfig().ConfigPath; p != "" {
		cfgPath = p
	}

	// only create startup config mounts if the config data was set in kne
	if s.Spec.Config.ConfigDataPresent {
		log.Info(
			"Adding volume for startup config to pod spec",
			"volume.name",
			"startup-config-volume",
			"mount.path",
			cfgPath,
		)

		pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{
			Name: "startup-config-volume",
			VolumeSource: corev1.VolumeSource{
				ConfigMap: &corev1.ConfigMapVolumeSource{
					LocalObjectReference: corev1.LocalObjectReference{
						Name: fmt.Sprintf("%s-config", s.Name),
					},
				},
			},
		})

		pod.Spec.Containers[0].VolumeMounts = append(
			pod.Spec.Containers[0].VolumeMounts,
			corev1.VolumeMount{
				Name:      "startup-config-volume",
				MountPath: cfgPath,
				ReadOnly:  false,
			},
		)
	}
}

func createVolumeMounts(s *typesv1a1.Srlinux) []corev1.VolumeMount {
	vms := []corev1.VolumeMount{
		{
			Name:      variantsVolName,
			MountPath: variantsVolMntPath,
		},
		{
			Name:      topomacVolName,
			MountPath: topomacVolMntPath,
		},
		{
			Name:      entrypointVolName,
			MountPath: entrypointVolMntPath,
			SubPath:   entrypointVolMntSubPath,
		},
	}

	if s.LicenseKey != "" {
		vms = append(vms, createLicenseVolumeMount())
	}

	return vms
}

func createLicenseVolume(s *typesv1a1.Srlinux) corev1.Volume {
	return corev1.Volume{
		Name: licensesVolName,
		VolumeSource: corev1.VolumeSource{
			Secret: &corev1.SecretVolumeSource{
				SecretName: srlLicenseSecretName,
				Items: []corev1.KeyToPath{
					{
						Key:  s.LicenseKey,
						Path: licenseFileName,
					},
				},
			},
		},
	}
}

func createLicenseVolumeMount() corev1.VolumeMount {
	return corev1.VolumeMount{
		Name:      licensesVolName,
		MountPath: licenseMntPath,
		SubPath:   licenseMntSubPath,
	}
}
<|endoftext|>


<commit_before>// Package presence provides the logical part for the long running
// operations of presence worker
package presence

import (
	"fmt"
	mongomodels "koding/db/models"
	"koding/db/mongodb/modelhelper"
	"socialapi/config"
	"socialapi/models"
	"time"

	mgo "gopkg.in/mgo.v2"

	"github.com/koding/bongo"
	"github.com/koding/cache"
	"github.com/koding/logging"
	"github.com/streadway/amqp"
)

const (
	// EventName holds presence event name
	EventName = "presence_ping"
)

var (
	pingCache = cache.NewMemoryWithTTL(time.Hour)

	// this may lead to max 5 mins of invalid tracking data if a group changes
	// their sub status during that period
	groupCache = cache.NewMemoryWithTTL(time.Minute * 5)

	// send pings every 30 secs
	pingDuration = time.Second * 30
)

// Controller holds the basic context data for handlers
type Controller struct {
	log  logging.Logger
	conf *config.Config
}

// New creates a controller
func New(log logging.Logger, conf *config.Config) *Controller {
	return &Controller{
		log:  log,
		conf: conf,
	}
}

// DefaultErrHandler handles the errors for presence worker
func (c *Controller) DefaultErrHandler(delivery amqp.Delivery, err error) bool {
	delivery.Nack(false, true)
	return false
}

func validate(ping *Ping) error {
	if ping.GroupName == "" {
		return fmt.Errorf("fileId is missing %+v", ping)
	}

	if ping.AccountID == 0 {
		return fmt.Errorf("accountId is missing %+v", ping)
	}

	return nil
}

// Ping handles the pings coming from client side
func (c *Controller) Ping(ping *Ping) error {
	c.log.Debug("new ping %+v", ping)
	if err := validate(ping); err != nil {
		c.log.Error("validation error:%s", err.Error())
		return nil
	}

	status, err := getGroupPaymentStatusFromCache(ping.GroupName)
	if err == mgo.ErrNotFound {
		return nil // if group is not found in db, no need to process further
	}

	if err != nil {
		return err
	}

	ping.paymentStatus = status

	today := getTodayBeginningDate()
	// we add date here to invalidate cache item(s) after date changes
	key := getKey(ping, today)

	// if we find item in the cache, that means we processed it previously
	if _, err := pingCache.Get(key); err == nil {
		return nil
	}

	if err := verifyRecord(ping, today); err != nil {
		return err
	}

	return pingCache.Set(key, struct{}{})
}

func getKey(ping *Ping, today time.Time) string {
	return fmt.Sprintf(
		"%s_%d_%s_%d",
		ping.GroupName,
		ping.AccountID,
		ping.paymentStatus,
		today.Day(),
	)
}

// verifyRecord checks if the daily occurence is in the db, if not found creates
// a new record, if found and it is greater than today's beginning time returns
// nil. If it is smaller than today, creates a new record in the db
func verifyRecord(ping *Ping, today time.Time) error {
	p, err := getPresenceInfoFromDB(ping)
	if err != nil && err != bongo.RecordNotFound {
		return err // if we have non app specific err, return it
	}

	// if our record is persisted today, skip updating. We will update the record
	// once a day
	if p != nil && p.CreatedAt.Unix() > today.Unix() {
		return nil
	}

	return insertPresenceInfoToDB(ping)
}

func getTodayBeginningDate() time.Time {
	year, month, day := time.Now().UTC().Date()
	return time.Date(year, month, day, 0, 0, 0, 0, time.UTC)
}

func getPresenceInfoFromDB(ping *Ping) (*models.PresenceDaily, error) {
	q := &bongo.Query{
		Selector: map[string]interface{}{
			"group_name": ping.GroupName,
			"account_id": ping.AccountID,
			// if payment status is active, we should get the last unprocessed, but if
			// it is not active, we are persisting pings as processed, so fetch latest
			// processed in that case
			//
			// One big question is why we store non active sub-ed team's ping requests
			// as processed? We wont be charging trailing teams before second month's
			// payment is due, so we start collecting presence info( with processed
			// false )  after the first month, and first month is completely free.
			// Second issue is, when a sub is in non-active state, we should still
			// collect presence info but we wont be charging users during that period,
			// because we dont allow them to utilize koding
			"is_processed": ping.paymentStatus != mongomodels.PaymentStatusActive,
		},
		Sort: map[string]string{
			"created_at": "DESC",
		},
	}
	a := &models.PresenceDaily{}
	if err := a.One(q); err != nil {
		return nil, err
	}

	return a, nil
}

func insertPresenceInfoToDB(ping *Ping) error {
	p := &models.PresenceDaily{
		GroupName:   ping.GroupName,
		AccountId:   ping.AccountID,
		CreatedAt:   ping.CreatedAt,
		IsProcessed: ping.paymentStatus != mongomodels.PaymentStatusActive,
	}
	return p.Create()
}

func getGroupPaymentStatusFromCache(groupName string) (string, error) {
	data, err := groupCache.Get(groupName)
	if err != nil && err != cache.ErrNotFound {
		return "", err
	}

	if err == nil {
		status, ok := data.(string)
		if ok {
			return status, nil
		}
	}

	group, err := modelhelper.GetGroup(groupName)
	if err != nil {
		return "", err
	}

	status := group.Payment.Subscription.Status
	// set defaul payment status
	if status != mongomodels.PaymentStatusActive {
		status = "invalid"
	}

	if err := groupCache.Set(groupName, status); err != nil {
		return "", err
	}
	return status, nil
}
<commit_msg>go/presence: start gc<commit_after>// Package presence provides the logical part for the long running
// operations of presence worker
package presence

import (
	"fmt"
	mongomodels "koding/db/models"
	"koding/db/mongodb/modelhelper"
	"socialapi/config"
	"socialapi/models"
	"time"

	mgo "gopkg.in/mgo.v2"

	"github.com/koding/bongo"
	"github.com/koding/cache"
	"github.com/koding/logging"
	"github.com/streadway/amqp"
)

func init() {
	pingCache.StartGC(time.Minute)
	groupCache.StartGC(time.Minute)
}

const (
	// EventName holds presence event name
	EventName = "presence_ping"
)

var (
	pingCache = cache.NewMemoryWithTTL(time.Hour)

	// this may lead to max 5 mins of invalid tracking data if a group changes
	// their sub status during that period
	groupCache = cache.NewMemoryWithTTL(time.Minute * 5)

	// send pings every 30 secs
	pingDuration = time.Second * 30
)

// Controller holds the basic context data for handlers
type Controller struct {
	log  logging.Logger
	conf *config.Config
}

// New creates a controller
func New(log logging.Logger, conf *config.Config) *Controller {
	return &Controller{
		log:  log,
		conf: conf,
	}
}

// DefaultErrHandler handles the errors for presence worker
func (c *Controller) DefaultErrHandler(delivery amqp.Delivery, err error) bool {
	delivery.Nack(false, true)
	return false
}

func validate(ping *Ping) error {
	if ping.GroupName == "" {
		return fmt.Errorf("fileId is missing %+v", ping)
	}

	if ping.AccountID == 0 {
		return fmt.Errorf("accountId is missing %+v", ping)
	}

	return nil
}

// Ping handles the pings coming from client side
func (c *Controller) Ping(ping *Ping) error {
	c.log.Debug("new ping %+v", ping)
	if err := validate(ping); err != nil {
		c.log.Error("validation error:%s", err.Error())
		return nil
	}

	status, err := getGroupPaymentStatusFromCache(ping.GroupName)
	if err == mgo.ErrNotFound {
		return nil // if group is not found in db, no need to process further
	}

	if err != nil {
		return err
	}

	ping.paymentStatus = status

	today := getTodayBeginningDate()
	// we add date here to invalidate cache item(s) after date changes
	key := getKey(ping, today)

	// if we find item in the cache, that means we processed it previously
	if _, err := pingCache.Get(key); err == nil {
		return nil
	}

	if err := verifyRecord(ping, today); err != nil {
		return err
	}

	return pingCache.Set(key, struct{}{})
}

func getKey(ping *Ping, today time.Time) string {
	return fmt.Sprintf(
		"%s_%d_%s_%d",
		ping.GroupName,
		ping.AccountID,
		ping.paymentStatus,
		today.Day(),
	)
}

// verifyRecord checks if the daily occurence is in the db, if not found creates
// a new record, if found and it is greater than today's beginning time returns
// nil. If it is smaller than today, creates a new record in the db
func verifyRecord(ping *Ping, today time.Time) error {
	p, err := getPresenceInfoFromDB(ping)
	if err != nil && err != bongo.RecordNotFound {
		return err // if we have non app specific err, return it
	}

	// if our record is persisted today, skip updating. We will update the record
	// once a day
	if p != nil && p.CreatedAt.Unix() > today.Unix() {
		return nil
	}

	return insertPresenceInfoToDB(ping)
}

func getTodayBeginningDate() time.Time {
	year, month, day := time.Now().UTC().Date()
	return time.Date(year, month, day, 0, 0, 0, 0, time.UTC)
}

func getPresenceInfoFromDB(ping *Ping) (*models.PresenceDaily, error) {
	q := &bongo.Query{
		Selector: map[string]interface{}{
			"group_name": ping.GroupName,
			"account_id": ping.AccountID,
			// if payment status is active, we should get the last unprocessed, but if
			// it is not active, we are persisting pings as processed, so fetch latest
			// processed in that case
			//
			// One big question is why we store non active sub-ed team's ping requests
			// as processed? We wont be charging trailing teams before second month's
			// payment is due, so we start collecting presence info( with processed
			// false )  after the first month, and first month is completely free.
			// Second issue is, when a sub is in non-active state, we should still
			// collect presence info but we wont be charging users during that period,
			// because we dont allow them to utilize koding
			"is_processed": ping.paymentStatus != mongomodels.PaymentStatusActive,
		},
		Sort: map[string]string{
			"created_at": "DESC",
		},
	}
	a := &models.PresenceDaily{}
	if err := a.One(q); err != nil {
		return nil, err
	}

	return a, nil
}

func insertPresenceInfoToDB(ping *Ping) error {
	p := &models.PresenceDaily{
		GroupName:   ping.GroupName,
		AccountId:   ping.AccountID,
		CreatedAt:   ping.CreatedAt,
		IsProcessed: ping.paymentStatus != mongomodels.PaymentStatusActive,
	}
	return p.Create()
}

func getGroupPaymentStatusFromCache(groupName string) (string, error) {
	data, err := groupCache.Get(groupName)
	if err != nil && err != cache.ErrNotFound {
		return "", err
	}

	if err == nil {
		status, ok := data.(string)
		if ok {
			return status, nil
		}
	}

	group, err := modelhelper.GetGroup(groupName)
	if err != nil {
		return "", err
	}

	status := group.Payment.Subscription.Status
	// set defaul payment status
	if status != mongomodels.PaymentStatusActive {
		status = "invalid"
	}

	if err := groupCache.Set(groupName, status); err != nil {
		return "", err
	}
	return status, nil
}
<|endoftext|>


<commit_before>package posplay

import (
	"encoding/gob"
	"errors"
	"fmt"
	"html/template"
	"log"
	"strconv"
	"time"

	"github.com/bwmarrin/discordgo"
	uuid "github.com/satori/go.uuid"
	"github.com/underlx/disturbancesmlx/dataobjects"
	"github.com/underlx/disturbancesmlx/discordbot"

	"github.com/gbl08ma/sqalx"
	"github.com/gorilla/csrf"
	"github.com/gorilla/mux"
	"github.com/gorilla/sessions"

	"github.com/gbl08ma/keybox"
	sq "github.com/gbl08ma/squirrel"
	"golang.org/x/oauth2"
)

var oauthConfig *oauth2.Config
var webtemplate *template.Template

var tripSubmissionsChan = make(chan string, 100)
var tripEditsChan = make(chan string, 100)

const (
	// PrivateLBPrivacy is used when users don't want to appear in leaderboards
	PrivateLBPrivacy string = "PRIVATE"
	// PublicLBPrivacy is used when users want to appear in leaderboards
	PublicLBPrivacy string = "PUBLIC"
)

const (
	// UsernameDiscriminatorNameType is used when users want to appear like gbl08ma#3988
	UsernameDiscriminatorNameType string = "USERNAME_DISCRIM"
	// UsernameNameType is used when users want to appear like gbl08ma
	UsernameNameType string = "USERNAME"
	// NicknameNameType is used when users want to appear as their nickname in the project's guild
	NicknameNameType string = "NICKNAME"
)

// Config contains runtime PosPlay subsystem configuration
type Config struct {
	Keybox     *keybox.Keybox
	Log        *log.Logger
	Store      *sessions.CookieStore
	Node       sqalx.Node
	PathPrefix string
	GitCommit  string
}

var config Config
var csrfMiddleware mux.MiddlewareFunc

// Initialize initializes the PosPlay subsystem
func Initialize(ppconfig Config) error {
	// register Session with gob so it can be saved in cookies
	gob.Register(Session{})

	config = ppconfig
	clientID, present := config.Keybox.Get("oauthClientId")
	if !present {
		return errors.New("OAuth client ID not present in posplay keybox")
	}

	clientSecret, present := config.Keybox.Get("oauthClientSecret")
	if !present {
		return errors.New("OAuth client secret not present in posplay keybox")
	}

	csrfAuthKey, present := config.Keybox.Get("csrfAuthKey")
	if !present {
		return errors.New("CSRF auth key not present in posplay keybox")
	}

	csrfOpts := []csrf.Option{csrf.FieldName(CSRFfieldName), csrf.CookieName(CSRFcookieName)}
	if DEBUG {
		csrfOpts = append(csrfOpts, csrf.Secure(false))
	}
	csrfMiddleware = csrf.Protect([]byte(csrfAuthKey), csrfOpts...)

	oauthConfig = &oauth2.Config{
		RedirectURL:  config.PathPrefix + "/oauth/callback",
		ClientID:     clientID,
		ClientSecret: clientSecret,
		Scopes:       []string{"identify"},
		Endpoint: oauth2.Endpoint{
			AuthURL:  "https://discordapp.com/api/oauth2/authorize",
			TokenURL: "https://discordapp.com/api/oauth2/token",
		},
	}

	discordbot.ThePosPlayBridge.OnEventWinCallback = RegisterEventWinCallback
	discordbot.ThePosPlayBridge.OnDiscussionParticipationCallback = RegisterDiscussionParticipationCallback
	discordbot.ThePosPlayBridge.PlayerXPInfo = playerXPinfo

	ReloadTemplates()

	go serialProcessor()

	return nil
}

// RegisterTripSubmission schedules a trip submission for analysis
func RegisterTripSubmission(trip *dataobjects.Trip) {
	tripSubmissionsChan <- trip.ID
}

// RegisterTripFirstEdit schedules a trip resubmission (edit, confirmation) for analysis
func RegisterTripFirstEdit(trip *dataobjects.Trip) {
	tripEditsChan <- trip.ID
}

// RegisterEventWinCallback gives a user a XP reward for a Discord event, if he has not received a reward for that event yet
func RegisterEventWinCallback(userID, messageID string, XPreward int, eventType string) bool {
	// does the user even exist in PosPlay?
	tx, err := config.Node.Beginx()
	if err != nil {
		config.Log.Println(err)
		return false
	}
	defer tx.Rollback()

	player, err := dataobjects.GetPPPlayer(tx, uidConvS(userID))
	if err != nil {
		// this user is not yet a PosPlay player
		discordbot.SendDMtoUser(userID, &discordgo.MessageSend{
			Content: fmt.Sprintf("Para poder receber XP por participar nos eventos no servidor de Discord do UnderLX, tem de se registar no PosPlay primeiro: " + config.PathPrefix),
		})
		return false
	}

	typeFilter := sq.Eq{"type": eventType}
	eventFilter := sq.Expr("extra::json ->> 'event_id' = ?", messageID)

	transactions, err := player.XPTransactionsCustomFilter(tx, typeFilter, eventFilter)
	if err != nil {
		config.Log.Println(err)
		return false
	}
	if len(transactions) > 0 {
		// user already received rewards for this event
		return false
	}

	txid, err := uuid.NewV4()
	if err != nil {
		config.Log.Println(err)
		return false
	}

	xptx := &dataobjects.PPXPTransaction{
		ID:        txid.String(),
		DiscordID: player.DiscordID,
		Time:      time.Now(),
		Type:      eventType,
		Value:     XPreward,
	}
	xptx.MarshalExtra(map[string]interface{}{
		"event_id": messageID,
	})

	err = xptx.Update(tx)
	if err != nil {
		config.Log.Println(err)
		return false
	}

	err = tx.Commit()
	if err != nil {
		config.Log.Println(err)
		return false
	}

	discordbot.SendDMtoUser(userID, &discordgo.MessageSend{
		Content: fmt.Sprintf("Acabou de receber %d XP pela participação num evento no servidor de Discord do UnderLX 👍", XPreward),
	})

	return true
}

// RegisterDiscussionParticipationCallback gives a user a XP reward for participating in the Discord channels
func RegisterDiscussionParticipationCallback(userID string, XPreward int) bool {
	// does the user even exist in PosPlay?
	tx, err := config.Node.Beginx()
	if err != nil {
		config.Log.Println(err)
		return false
	}
	defer tx.Rollback()

	player, err := dataobjects.GetPPPlayer(tx, uidConvS(userID))
	if err != nil {
		// this user is not yet a PosPlay player
		return false
	}

	lasttx, err := player.XPTransactionsLimit(tx, 1)
	if err != nil {
		config.Log.Println(err)
		return false
	}
	var newtx *dataobjects.PPXPTransaction
	if len(lasttx) > 0 && lasttx[0].Type == "DISCORD_PARTICIPATION" {
		// to avoid creating many micro-transactions, update the latest transaction, adding the new reward
		newtx = lasttx[0]
	} else {
		txid, err := uuid.NewV4()
		if err != nil {
			config.Log.Println(err)
			return false
		}
		newtx = &dataobjects.PPXPTransaction{
			ID:        txid.String(),
			DiscordID: player.DiscordID,
			Type:      "DISCORD_PARTICIPATION",
		}
	}
	newtx.Time = time.Now()
	newtx.Value += XPreward
	err = newtx.Update(tx)
	if err != nil {
		config.Log.Println(err)
		return false
	}

	err = tx.Commit()
	if err != nil {
		config.Log.Println(err)
		return false
	}
	return true
}

func serialProcessor() {
	for {
		select {
		case id := <-tripSubmissionsChan:
			err := processTripForReward(id)
			if err != nil {
				config.Log.Println(err)
			}
		case id := <-tripEditsChan:
			err := processTripEditForReward(id)
			if err != nil {
				config.Log.Println(err)
			}
		}
	}
}

func uidConvS(uid string) uint64 {
	v, _ := strconv.ParseUint(uid, 10, 64)
	return v
}

func uidConvI(uid uint64) string {
	return strconv.FormatUint(uid, 10)
}

func getWeekStart() time.Time {
	loc, _ := time.LoadLocation(GameTimezone)
	now := time.Now().In(loc)
	daysSinceMonday := now.Weekday() - time.Monday
	if daysSinceMonday < 0 {
		// it's Sunday, last Monday was 6 days ago
		daysSinceMonday = 6
	}
	endTime := time.Date(now.Year(), now.Month(), now.Day()-int(daysSinceMonday), 2, 0, 0, 0, loc)
	if endTime.After(now) {
		// it's Monday, but it's not 2 AM yet
		endTime = endTime.AddDate(0, 0, -7)
	}
	return endTime
}

func descriptionForXPTransaction(tx *dataobjects.PPXPTransaction) string {
	extra := tx.UnmarshalExtra()
	switch tx.Type {
	case "SIGNUP_BONUS":
		return "Oferta de boas-vindas"
	case "PAIR_BONUS":
		return "Associação de dispositivo"
	case "TRIP_SUBMIT_REWARD":
		numstations, ok := extra["station_count"].(float64)
		numexchanges, ok2 := extra["interchange_count"].(float64)
		offpeak, ok3 := extra["offpeak"].(bool)
		if ok && ok2 && ok3 {
			excstr := ""
			switch int(numexchanges) {
			case 0:
				excstr = ""
			case 1:
				excstr = ", com 1 troca de linha"
			default:
				excstr = fmt.Sprintf(", com %d trocas de linha", int(numexchanges))
			}
			ofpstr := ""
			if offpeak {
				ofpstr = ", fora das horas de ponta"
			}
			return fmt.Sprintf("Viagem por %d estações%s%s", int(numstations), excstr, ofpstr)
		}
		return "Viagem"
	case "TRIP_CONFIRM_REWARD":
		return "Verificação de registo de viagem"
	case "DISCORD_REACTION_EVENT":
		return "Participação em evento no Discord do UnderLX"
	case "DISCORD_CHALLENGE_EVENT":
		return "Participação em desafio no Discord do UnderLX"
	case "DISCORD_PARTICIPATION":
		return "Participação na discussão no Discord do UnderLX"
	default:
		// ideally this should never show
		return "Bónus genérico"
	}
}

func getDisplayNameFromNameType(nameType string, user *discordgo.User, guildMember *discordgo.Member) string {
	switch nameType {
	case NicknameNameType:
		if guildMember != nil && guildMember.Nick != "" {
			return guildMember.Nick
		}
		fallthrough
	case UsernameNameType:
		return user.Username
	case UsernameDiscriminatorNameType:
		fallthrough
	default:
		return user.Username + "#" + user.Discriminator
	}
}

func playerXPinfo(userID string) (discordbot.PosPlayXPInfo, error) {
	tx, err := config.Node.Beginx()
	if err != nil {
		return discordbot.PosPlayXPInfo{}, err
	}
	defer tx.Commit() // read-only tx

	return playerXPinfoWithTx(tx, userID)
}

func playerXPinfoWithTx(tx sqalx.Node, userID string) (discordbot.PosPlayXPInfo, error) {
	player, err := dataobjects.GetPPPlayer(tx, uidConvS(userID))
	if err != nil {
		return discordbot.PosPlayXPInfo{}, err
	}

	username := player.CachedName
	avatar := userAvatarURL(uidConvS(userID), "256")
	if player.LBPrivacy == PrivateLBPrivacy {
		username = player.AnonymousName()
		avatar = fmt.Sprintf("https://api.adorable.io/avatars/256/%d.png", player.Seed())
	}
	xp, level, progress, err := player.Level(tx)
	if err != nil {
		return discordbot.PosPlayXPInfo{}, err
	}

	xpWeek, err := player.XPBalanceBetween(tx, getWeekStart(), time.Now())
	if err != nil {
		xpWeek = 0
	}
	rank, err := player.RankBetween(tx, time.Time{}, time.Now())
	if err != nil {
		rank = 0
	}
	rankWeek, err := player.RankBetween(tx, getWeekStart(), time.Now())
	if err != nil {
		rankWeek = 0
	}
	return discordbot.PosPlayXPInfo{
		Username:      username,
		AvatarURL:     avatar,
		Level:         level,
		LevelProgress: progress,
		XP:            xp,
		XPthisWeek:    xpWeek,
		Rank:          rank,
		RankThisWeek:  rankWeek,
	}, nil
}

func userAvatarURL(userID uint64, size string) string {
	user, err := discordbot.User(uidConvI(userID))
	if err == nil {
		return user.AvatarURL(size)
	}
	return ""
}
<commit_msg>PosPlay: prevent glitch where users could roll old XP rewards into the current week by not receiving any XP other than through Discord discussion participation<commit_after>package posplay

import (
	"encoding/gob"
	"errors"
	"fmt"
	"html/template"
	"log"
	"strconv"
	"time"

	"github.com/bwmarrin/discordgo"
	uuid "github.com/satori/go.uuid"
	"github.com/underlx/disturbancesmlx/dataobjects"
	"github.com/underlx/disturbancesmlx/discordbot"

	"github.com/gbl08ma/sqalx"
	"github.com/gorilla/csrf"
	"github.com/gorilla/mux"
	"github.com/gorilla/sessions"

	"github.com/gbl08ma/keybox"
	sq "github.com/gbl08ma/squirrel"
	"golang.org/x/oauth2"
)

var oauthConfig *oauth2.Config
var webtemplate *template.Template

var tripSubmissionsChan = make(chan string, 100)
var tripEditsChan = make(chan string, 100)

const (
	// PrivateLBPrivacy is used when users don't want to appear in leaderboards
	PrivateLBPrivacy string = "PRIVATE"
	// PublicLBPrivacy is used when users want to appear in leaderboards
	PublicLBPrivacy string = "PUBLIC"
)

const (
	// UsernameDiscriminatorNameType is used when users want to appear like gbl08ma#3988
	UsernameDiscriminatorNameType string = "USERNAME_DISCRIM"
	// UsernameNameType is used when users want to appear like gbl08ma
	UsernameNameType string = "USERNAME"
	// NicknameNameType is used when users want to appear as their nickname in the project's guild
	NicknameNameType string = "NICKNAME"
)

// Config contains runtime PosPlay subsystem configuration
type Config struct {
	Keybox     *keybox.Keybox
	Log        *log.Logger
	Store      *sessions.CookieStore
	Node       sqalx.Node
	PathPrefix string
	GitCommit  string
}

var config Config
var csrfMiddleware mux.MiddlewareFunc

// Initialize initializes the PosPlay subsystem
func Initialize(ppconfig Config) error {
	// register Session with gob so it can be saved in cookies
	gob.Register(Session{})

	config = ppconfig
	clientID, present := config.Keybox.Get("oauthClientId")
	if !present {
		return errors.New("OAuth client ID not present in posplay keybox")
	}

	clientSecret, present := config.Keybox.Get("oauthClientSecret")
	if !present {
		return errors.New("OAuth client secret not present in posplay keybox")
	}

	csrfAuthKey, present := config.Keybox.Get("csrfAuthKey")
	if !present {
		return errors.New("CSRF auth key not present in posplay keybox")
	}

	csrfOpts := []csrf.Option{csrf.FieldName(CSRFfieldName), csrf.CookieName(CSRFcookieName)}
	if DEBUG {
		csrfOpts = append(csrfOpts, csrf.Secure(false))
	}
	csrfMiddleware = csrf.Protect([]byte(csrfAuthKey), csrfOpts...)

	oauthConfig = &oauth2.Config{
		RedirectURL:  config.PathPrefix + "/oauth/callback",
		ClientID:     clientID,
		ClientSecret: clientSecret,
		Scopes:       []string{"identify"},
		Endpoint: oauth2.Endpoint{
			AuthURL:  "https://discordapp.com/api/oauth2/authorize",
			TokenURL: "https://discordapp.com/api/oauth2/token",
		},
	}

	discordbot.ThePosPlayBridge.OnEventWinCallback = RegisterEventWinCallback
	discordbot.ThePosPlayBridge.OnDiscussionParticipationCallback = RegisterDiscussionParticipationCallback
	discordbot.ThePosPlayBridge.PlayerXPInfo = playerXPinfo

	ReloadTemplates()

	go serialProcessor()

	return nil
}

// RegisterTripSubmission schedules a trip submission for analysis
func RegisterTripSubmission(trip *dataobjects.Trip) {
	tripSubmissionsChan <- trip.ID
}

// RegisterTripFirstEdit schedules a trip resubmission (edit, confirmation) for analysis
func RegisterTripFirstEdit(trip *dataobjects.Trip) {
	tripEditsChan <- trip.ID
}

// RegisterEventWinCallback gives a user a XP reward for a Discord event, if he has not received a reward for that event yet
func RegisterEventWinCallback(userID, messageID string, XPreward int, eventType string) bool {
	// does the user even exist in PosPlay?
	tx, err := config.Node.Beginx()
	if err != nil {
		config.Log.Println(err)
		return false
	}
	defer tx.Rollback()

	player, err := dataobjects.GetPPPlayer(tx, uidConvS(userID))
	if err != nil {
		// this user is not yet a PosPlay player
		discordbot.SendDMtoUser(userID, &discordgo.MessageSend{
			Content: fmt.Sprintf("Para poder receber XP por participar nos eventos no servidor de Discord do UnderLX, tem de se registar no PosPlay primeiro: " + config.PathPrefix),
		})
		return false
	}

	typeFilter := sq.Eq{"type": eventType}
	eventFilter := sq.Expr("extra::json ->> 'event_id' = ?", messageID)

	transactions, err := player.XPTransactionsCustomFilter(tx, typeFilter, eventFilter)
	if err != nil {
		config.Log.Println(err)
		return false
	}
	if len(transactions) > 0 {
		// user already received rewards for this event
		return false
	}

	txid, err := uuid.NewV4()
	if err != nil {
		config.Log.Println(err)
		return false
	}

	xptx := &dataobjects.PPXPTransaction{
		ID:        txid.String(),
		DiscordID: player.DiscordID,
		Time:      time.Now(),
		Type:      eventType,
		Value:     XPreward,
	}
	xptx.MarshalExtra(map[string]interface{}{
		"event_id": messageID,
	})

	err = xptx.Update(tx)
	if err != nil {
		config.Log.Println(err)
		return false
	}

	err = tx.Commit()
	if err != nil {
		config.Log.Println(err)
		return false
	}

	discordbot.SendDMtoUser(userID, &discordgo.MessageSend{
		Content: fmt.Sprintf("Acabou de receber %d XP pela participação num evento no servidor de Discord do UnderLX 👍", XPreward),
	})

	return true
}

// RegisterDiscussionParticipationCallback gives a user a XP reward for participating in the Discord channels
func RegisterDiscussionParticipationCallback(userID string, XPreward int) bool {
	// does the user even exist in PosPlay?
	tx, err := config.Node.Beginx()
	if err != nil {
		config.Log.Println(err)
		return false
	}
	defer tx.Rollback()

	player, err := dataobjects.GetPPPlayer(tx, uidConvS(userID))
	if err != nil {
		// this user is not yet a PosPlay player
		return false
	}

	lasttx, err := player.XPTransactionsLimit(tx, 1)
	if err != nil {
		config.Log.Println(err)
		return false
	}
	var newtx *dataobjects.PPXPTransaction
	if len(lasttx) > 0 && lasttx[0].Type == "DISCORD_PARTICIPATION" && time.Since(lasttx[0].Time) < 6*time.Hour && lasttx[0].Value < 100 {
		// to avoid creating many micro-transactions, update the latest transaction, adding the new reward
		newtx = lasttx[0]
	} else {
		txid, err := uuid.NewV4()
		if err != nil {
			config.Log.Println(err)
			return false
		}
		newtx = &dataobjects.PPXPTransaction{
			ID:        txid.String(),
			DiscordID: player.DiscordID,
			Type:      "DISCORD_PARTICIPATION",
		}
	}
	newtx.Time = time.Now()
	newtx.Value += XPreward
	err = newtx.Update(tx)
	if err != nil {
		config.Log.Println(err)
		return false
	}

	err = tx.Commit()
	if err != nil {
		config.Log.Println(err)
		return false
	}
	return true
}

func serialProcessor() {
	for {
		select {
		case id := <-tripSubmissionsChan:
			err := processTripForReward(id)
			if err != nil {
				config.Log.Println(err)
			}
		case id := <-tripEditsChan:
			err := processTripEditForReward(id)
			if err != nil {
				config.Log.Println(err)
			}
		}
	}
}

func uidConvS(uid string) uint64 {
	v, _ := strconv.ParseUint(uid, 10, 64)
	return v
}

func uidConvI(uid uint64) string {
	return strconv.FormatUint(uid, 10)
}

func getWeekStart() time.Time {
	loc, _ := time.LoadLocation(GameTimezone)
	now := time.Now().In(loc)
	daysSinceMonday := now.Weekday() - time.Monday
	if daysSinceMonday < 0 {
		// it's Sunday, last Monday was 6 days ago
		daysSinceMonday = 6
	}
	endTime := time.Date(now.Year(), now.Month(), now.Day()-int(daysSinceMonday), 2, 0, 0, 0, loc)
	if endTime.After(now) {
		// it's Monday, but it's not 2 AM yet
		endTime = endTime.AddDate(0, 0, -7)
	}
	return endTime
}

func descriptionForXPTransaction(tx *dataobjects.PPXPTransaction) string {
	extra := tx.UnmarshalExtra()
	switch tx.Type {
	case "SIGNUP_BONUS":
		return "Oferta de boas-vindas"
	case "PAIR_BONUS":
		return "Associação de dispositivo"
	case "TRIP_SUBMIT_REWARD":
		numstations, ok := extra["station_count"].(float64)
		numexchanges, ok2 := extra["interchange_count"].(float64)
		offpeak, ok3 := extra["offpeak"].(bool)
		if ok && ok2 && ok3 {
			excstr := ""
			switch int(numexchanges) {
			case 0:
				excstr = ""
			case 1:
				excstr = ", com 1 troca de linha"
			default:
				excstr = fmt.Sprintf(", com %d trocas de linha", int(numexchanges))
			}
			ofpstr := ""
			if offpeak {
				ofpstr = ", fora das horas de ponta"
			}
			return fmt.Sprintf("Viagem por %d estações%s%s", int(numstations), excstr, ofpstr)
		}
		return "Viagem"
	case "TRIP_CONFIRM_REWARD":
		return "Verificação de registo de viagem"
	case "DISCORD_REACTION_EVENT":
		return "Participação em evento no Discord do UnderLX"
	case "DISCORD_CHALLENGE_EVENT":
		return "Participação em desafio no Discord do UnderLX"
	case "DISCORD_PARTICIPATION":
		return "Participação na discussão no Discord do UnderLX"
	default:
		// ideally this should never show
		return "Bónus genérico"
	}
}

func getDisplayNameFromNameType(nameType string, user *discordgo.User, guildMember *discordgo.Member) string {
	switch nameType {
	case NicknameNameType:
		if guildMember != nil && guildMember.Nick != "" {
			return guildMember.Nick
		}
		fallthrough
	case UsernameNameType:
		return user.Username
	case UsernameDiscriminatorNameType:
		fallthrough
	default:
		return user.Username + "#" + user.Discriminator
	}
}

func playerXPinfo(userID string) (discordbot.PosPlayXPInfo, error) {
	tx, err := config.Node.Beginx()
	if err != nil {
		return discordbot.PosPlayXPInfo{}, err
	}
	defer tx.Commit() // read-only tx

	return playerXPinfoWithTx(tx, userID)
}

func playerXPinfoWithTx(tx sqalx.Node, userID string) (discordbot.PosPlayXPInfo, error) {
	player, err := dataobjects.GetPPPlayer(tx, uidConvS(userID))
	if err != nil {
		return discordbot.PosPlayXPInfo{}, err
	}

	username := player.CachedName
	avatar := userAvatarURL(uidConvS(userID), "256")
	if player.LBPrivacy == PrivateLBPrivacy {
		username = player.AnonymousName()
		avatar = fmt.Sprintf("https://api.adorable.io/avatars/256/%d.png", player.Seed())
	}
	xp, level, progress, err := player.Level(tx)
	if err != nil {
		return discordbot.PosPlayXPInfo{}, err
	}

	xpWeek, err := player.XPBalanceBetween(tx, getWeekStart(), time.Now())
	if err != nil {
		xpWeek = 0
	}
	rank, err := player.RankBetween(tx, time.Time{}, time.Now())
	if err != nil {
		rank = 0
	}
	rankWeek, err := player.RankBetween(tx, getWeekStart(), time.Now())
	if err != nil {
		rankWeek = 0
	}
	return discordbot.PosPlayXPInfo{
		Username:      username,
		AvatarURL:     avatar,
		Level:         level,
		LevelProgress: progress,
		XP:            xp,
		XPthisWeek:    xpWeek,
		Rank:          rank,
		RankThisWeek:  rankWeek,
	}, nil
}

func userAvatarURL(userID uint64, size string) string {
	user, err := discordbot.User(uidConvI(userID))
	if err == nil {
		return user.AvatarURL(size)
	}
	return ""
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"

	"github.com/vbauerster/mpb"
	"github.com/vbauerster/mpb/decor"
)

func init() {
	rand.Seed(time.Now().UnixNano())
}

func main() {
	var wg sync.WaitGroup
	p := mpb.New(mpb.WithWaitGroup(&wg))
	total := 100
	numBars := 3
	wg.Add(numBars)

	for i := 0; i < numBars; i++ {
		name := fmt.Sprintf("Bar#%d:", i)

		var bOption mpb.BarOption
		if i == 0 {
			bOption = mpb.BarRemoveOnComplete()
		}

		b := p.AddBar(int64(total), mpb.BarID(i),
			bOption,
			mpb.PrependDecorators(
				decor.Name(name),
				decor.EwmaETA(decor.ET_STYLE_GO, 60, decor.WCSyncSpace),
			),
			mpb.AppendDecorators(decor.Percentage()),
		)
		go func() {
			defer wg.Done()
			max := 100 * time.Millisecond
			for i := 0; i < total; i++ {
				start := time.Now()
				if b.ID() == 2 && i == 42 {
					p.Abort(b, true)
					return
				}
				time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10)
				// ewma based decorators require work duration measurement
				b.IncrBy(1, time.Since(start))
			}
		}()
	}

	p.Wait()
}
<commit_msg>use OptionOnCondition<commit_after>package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"

	"github.com/vbauerster/mpb"
	"github.com/vbauerster/mpb/decor"
)

func init() {
	rand.Seed(time.Now().UnixNano())
}

func main() {
	var wg sync.WaitGroup
	p := mpb.New(mpb.WithWaitGroup(&wg))
	total := 100
	numBars := 3
	wg.Add(numBars)

	for i := 0; i < numBars; i++ {
		name := fmt.Sprintf("Bar#%d:", i)
		b := p.AddBar(int64(total), mpb.BarID(i),
			mpb.OptionOnCondition(mpb.BarRemoveOnComplete(), func() bool { return i == 0 }),
			mpb.PrependDecorators(
				decor.Name(name),
				decor.EwmaETA(decor.ET_STYLE_GO, 60, decor.WCSyncSpace),
			),
			mpb.AppendDecorators(decor.Percentage()),
		)
		go func() {
			defer wg.Done()
			max := 100 * time.Millisecond
			for i := 0; i < total; i++ {
				start := time.Now()
				if b.ID() == 2 && i == 42 {
					p.Abort(b, true)
					return
				}
				time.Sleep(time.Duration(rand.Intn(10)+1) * max / 10)
				// ewma based decorators require work duration measurement
				b.IncrBy(1, time.Since(start))
			}
		}()
	}

	p.Wait()
}
<|endoftext|>


<commit_before>package grpc

import (
	"bytes"
	"context"
	"fmt"
	"reflect"

	"github.com/micro/go-micro/broker"
	"github.com/micro/go-micro/codec"
	"github.com/micro/go-micro/metadata"
	"github.com/micro/go-micro/registry"
	"github.com/micro/go-micro/server"
)

const (
	subSig = "func(context.Context, interface{}) error"
)

type handler struct {
	method  reflect.Value
	reqType reflect.Type
	ctxType reflect.Type
}

type subscriber struct {
	topic      string
	rcvr       reflect.Value
	typ        reflect.Type
	subscriber interface{}
	handlers   []*handler
	endpoints  []*registry.Endpoint
	opts       server.SubscriberOptions
}

func newSubscriber(topic string, sub interface{}, opts ...server.SubscriberOption) server.Subscriber {

	options := server.SubscriberOptions{
		AutoAck: true,
	}

	for _, o := range opts {
		o(&options)
	}

	var endpoints []*registry.Endpoint
	var handlers []*handler

	if typ := reflect.TypeOf(sub); typ.Kind() == reflect.Func {
		h := &handler{
			method: reflect.ValueOf(sub),
		}

		switch typ.NumIn() {
		case 1:
			h.reqType = typ.In(0)
		case 2:
			h.ctxType = typ.In(0)
			h.reqType = typ.In(1)
		}

		handlers = append(handlers, h)

		endpoints = append(endpoints, &registry.Endpoint{
			Name:    "Func",
			Request: extractSubValue(typ),
			Metadata: map[string]string{
				"topic":      topic,
				"subscriber": "true",
			},
		})
	} else {
		hdlr := reflect.ValueOf(sub)
		name := reflect.Indirect(hdlr).Type().Name()

		for m := 0; m < typ.NumMethod(); m++ {
			method := typ.Method(m)
			h := &handler{
				method: method.Func,
			}

			switch method.Type.NumIn() {
			case 2:
				h.reqType = method.Type.In(1)
			case 3:
				h.ctxType = method.Type.In(1)
				h.reqType = method.Type.In(2)
			}

			handlers = append(handlers, h)

			endpoints = append(endpoints, &registry.Endpoint{
				Name:    name + "." + method.Name,
				Request: extractSubValue(method.Type),
				Metadata: map[string]string{
					"topic":      topic,
					"subscriber": "true",
				},
			})
		}
	}

	return &subscriber{
		rcvr:       reflect.ValueOf(sub),
		typ:        reflect.TypeOf(sub),
		topic:      topic,
		subscriber: sub,
		handlers:   handlers,
		endpoints:  endpoints,
		opts:       options,
	}
}

func validateSubscriber(sub server.Subscriber) error {
	typ := reflect.TypeOf(sub.Subscriber())
	var argType reflect.Type

	if typ.Kind() == reflect.Func {
		name := "Func"
		switch typ.NumIn() {
		case 2:
			argType = typ.In(1)
		default:
			return fmt.Errorf("subscriber %v takes wrong number of args: %v required signature %s", name, typ.NumIn(), subSig)
		}
		if !isExportedOrBuiltinType(argType) {
			return fmt.Errorf("subscriber %v argument type not exported: %v", name, argType)
		}
		if typ.NumOut() != 1 {
			return fmt.Errorf("subscriber %v has wrong number of outs: %v require signature %s",
				name, typ.NumOut(), subSig)
		}
		if returnType := typ.Out(0); returnType != typeOfError {
			return fmt.Errorf("subscriber %v returns %v not error", name, returnType.String())
		}
	} else {
		hdlr := reflect.ValueOf(sub.Subscriber())
		name := reflect.Indirect(hdlr).Type().Name()

		for m := 0; m < typ.NumMethod(); m++ {
			method := typ.Method(m)

			switch method.Type.NumIn() {
			case 3:
				argType = method.Type.In(2)
			default:
				return fmt.Errorf("subscriber %v.%v takes wrong number of args: %v required signature %s",
					name, method.Name, method.Type.NumIn(), subSig)
			}

			if !isExportedOrBuiltinType(argType) {
				return fmt.Errorf("%v argument type not exported: %v", name, argType)
			}
			if method.Type.NumOut() != 1 {
				return fmt.Errorf(
					"subscriber %v.%v has wrong number of outs: %v require signature %s",
					name, method.Name, method.Type.NumOut(), subSig)
			}
			if returnType := method.Type.Out(0); returnType != typeOfError {
				return fmt.Errorf("subscriber %v.%v returns %v not error", name, method.Name, returnType.String())
			}
		}
	}

	return nil
}

func (g *grpcServer) createSubHandler(sb *subscriber, opts server.Options) broker.Handler {
	return func(p broker.Publication) error {
		msg := p.Message()
		ct := msg.Header["Content-Type"]
		cf, err := g.newCodec(ct)
		if err != nil {
			return err
		}

		hdr := make(map[string]string)
		for k, v := range msg.Header {
			hdr[k] = v
		}
		delete(hdr, "Content-Type")
		ctx := metadata.NewContext(context.Background(), hdr)

		for i := 0; i < len(sb.handlers); i++ {
			handler := sb.handlers[i]

			var isVal bool
			var req reflect.Value

			if handler.reqType.Kind() == reflect.Ptr {
				req = reflect.New(handler.reqType.Elem())
			} else {
				req = reflect.New(handler.reqType)
				isVal = true
			}
			if isVal {
				req = req.Elem()
			}

			b := &buffer{bytes.NewBuffer(msg.Body)}
			co := cf(b)
			defer co.Close()

			if err := co.ReadHeader(&codec.Message{}, codec.Publication); err != nil {
				return err
			}

			if err := co.ReadBody(req.Interface()); err != nil {
				return err
			}

			fn := func(ctx context.Context, msg server.Message) error {
				var vals []reflect.Value
				if sb.typ.Kind() != reflect.Func {
					vals = append(vals, sb.rcvr)
				}
				if handler.ctxType != nil {
					vals = append(vals, reflect.ValueOf(ctx))
				}

				vals = append(vals, reflect.ValueOf(msg.Payload()))

				returnValues := handler.method.Call(vals)
				if err := returnValues[0].Interface(); err != nil {
					return err.(error)
				}
				return nil
			}

			for i := len(opts.SubWrappers); i > 0; i-- {
				fn = opts.SubWrappers[i-1](fn)
			}

			if g.wg != nil {
				g.wg.Add(1)
			}
			go func() {
				if g.wg != nil {
					defer g.wg.Done()
				}
				fn(ctx, &rpcMessage{
					topic:       sb.topic,
					contentType: ct,
					payload:     req.Interface(),
				})
			}()
		}
		return nil
	}
}

func (s *subscriber) Topic() string {
	return s.topic
}

func (s *subscriber) Subscriber() interface{} {
	return s.subscriber
}

func (s *subscriber) Endpoints() []*registry.Endpoint {
	return s.endpoints
}

func (s *subscriber) Options() server.SubscriberOptions {
	return s.opts
}
<commit_msg>a. add default context type when header not found<commit_after>package grpc

import (
	"bytes"
	"context"
	"fmt"
	"reflect"
	"strings"

	"github.com/micro/go-micro/broker"
	"github.com/micro/go-micro/codec"
	"github.com/micro/go-micro/metadata"
	"github.com/micro/go-micro/registry"
	"github.com/micro/go-micro/server"
)

const (
	subSig = "func(context.Context, interface{}) error"
)

type handler struct {
	method  reflect.Value
	reqType reflect.Type
	ctxType reflect.Type
}

type subscriber struct {
	topic      string
	rcvr       reflect.Value
	typ        reflect.Type
	subscriber interface{}
	handlers   []*handler
	endpoints  []*registry.Endpoint
	opts       server.SubscriberOptions
}

func newSubscriber(topic string, sub interface{}, opts ...server.SubscriberOption) server.Subscriber {

	options := server.SubscriberOptions{
		AutoAck: true,
	}

	for _, o := range opts {
		o(&options)
	}

	var endpoints []*registry.Endpoint
	var handlers []*handler

	if typ := reflect.TypeOf(sub); typ.Kind() == reflect.Func {
		h := &handler{
			method: reflect.ValueOf(sub),
		}

		switch typ.NumIn() {
		case 1:
			h.reqType = typ.In(0)
		case 2:
			h.ctxType = typ.In(0)
			h.reqType = typ.In(1)
		}

		handlers = append(handlers, h)

		endpoints = append(endpoints, &registry.Endpoint{
			Name:    "Func",
			Request: extractSubValue(typ),
			Metadata: map[string]string{
				"topic":      topic,
				"subscriber": "true",
			},
		})
	} else {
		hdlr := reflect.ValueOf(sub)
		name := reflect.Indirect(hdlr).Type().Name()

		for m := 0; m < typ.NumMethod(); m++ {
			method := typ.Method(m)
			h := &handler{
				method: method.Func,
			}

			switch method.Type.NumIn() {
			case 2:
				h.reqType = method.Type.In(1)
			case 3:
				h.ctxType = method.Type.In(1)
				h.reqType = method.Type.In(2)
			}

			handlers = append(handlers, h)

			endpoints = append(endpoints, &registry.Endpoint{
				Name:    name + "." + method.Name,
				Request: extractSubValue(method.Type),
				Metadata: map[string]string{
					"topic":      topic,
					"subscriber": "true",
				},
			})
		}
	}

	return &subscriber{
		rcvr:       reflect.ValueOf(sub),
		typ:        reflect.TypeOf(sub),
		topic:      topic,
		subscriber: sub,
		handlers:   handlers,
		endpoints:  endpoints,
		opts:       options,
	}
}

func validateSubscriber(sub server.Subscriber) error {
	typ := reflect.TypeOf(sub.Subscriber())
	var argType reflect.Type

	if typ.Kind() == reflect.Func {
		name := "Func"
		switch typ.NumIn() {
		case 2:
			argType = typ.In(1)
		default:
			return fmt.Errorf("subscriber %v takes wrong number of args: %v required signature %s", name, typ.NumIn(), subSig)
		}
		if !isExportedOrBuiltinType(argType) {
			return fmt.Errorf("subscriber %v argument type not exported: %v", name, argType)
		}
		if typ.NumOut() != 1 {
			return fmt.Errorf("subscriber %v has wrong number of outs: %v require signature %s",
				name, typ.NumOut(), subSig)
		}
		if returnType := typ.Out(0); returnType != typeOfError {
			return fmt.Errorf("subscriber %v returns %v not error", name, returnType.String())
		}
	} else {
		hdlr := reflect.ValueOf(sub.Subscriber())
		name := reflect.Indirect(hdlr).Type().Name()

		for m := 0; m < typ.NumMethod(); m++ {
			method := typ.Method(m)

			switch method.Type.NumIn() {
			case 3:
				argType = method.Type.In(2)
			default:
				return fmt.Errorf("subscriber %v.%v takes wrong number of args: %v required signature %s",
					name, method.Name, method.Type.NumIn(), subSig)
			}

			if !isExportedOrBuiltinType(argType) {
				return fmt.Errorf("%v argument type not exported: %v", name, argType)
			}
			if method.Type.NumOut() != 1 {
				return fmt.Errorf(
					"subscriber %v.%v has wrong number of outs: %v require signature %s",
					name, method.Name, method.Type.NumOut(), subSig)
			}
			if returnType := method.Type.Out(0); returnType != typeOfError {
				return fmt.Errorf("subscriber %v.%v returns %v not error", name, method.Name, returnType.String())
			}
		}
	}

	return nil
}

func (g *grpcServer) createSubHandler(sb *subscriber, opts server.Options) broker.Handler {
	return func(p broker.Publication) error {
		msg := p.Message()
		ct := msg.Header["Content-Type"]
		if len(ct) == 0 {
			msg.Header["Content-Type"] = defaultContentType
			ct = defaultContentType
		}
		cf, err := g.newCodec(ct)
		if err != nil {
			return err
		}

		hdr := make(map[string]string)
		for k, v := range msg.Header {
			hdr[k] = v
		}
		delete(hdr, "Content-Type")
		ctx := metadata.NewContext(context.Background(), hdr)

		results := make(chan error, len(sb.handlers))

		for i := 0; i < len(sb.handlers); i++ {
			handler := sb.handlers[i]

			var isVal bool
			var req reflect.Value

			if handler.reqType.Kind() == reflect.Ptr {
				req = reflect.New(handler.reqType.Elem())
			} else {
				req = reflect.New(handler.reqType)
				isVal = true
			}
			if isVal {
				req = req.Elem()
			}

			b := &buffer{bytes.NewBuffer(msg.Body)}
			co := cf(b)
			defer co.Close()

			if err := co.ReadHeader(&codec.Message{}, codec.Publication); err != nil {
				return err
			}

			if err := co.ReadBody(req.Interface()); err != nil {
				return err
			}

			fn := func(ctx context.Context, msg server.Message) error {
				var vals []reflect.Value
				if sb.typ.Kind() != reflect.Func {
					vals = append(vals, sb.rcvr)
				}
				if handler.ctxType != nil {
					vals = append(vals, reflect.ValueOf(ctx))
				}

				vals = append(vals, reflect.ValueOf(msg.Payload()))

				returnValues := handler.method.Call(vals)
				if err := returnValues[0].Interface(); err != nil {
					return err.(error)
				}
				return nil
			}

			for i := len(opts.SubWrappers); i > 0; i-- {
				fn = opts.SubWrappers[i-1](fn)
			}

			if g.wg != nil {
				g.wg.Add(1)
			}
			go func() {
				if g.wg != nil {
					defer g.wg.Done()
				}
				results <- fn(ctx, &rpcMessage{
					topic:       sb.topic,
					contentType: ct,
					payload:     req.Interface(),
				})
			}()
		}
		var errors []string
		for i := 0; i < len(sb.handlers); i++ {
			if err := <-results; err != nil {
				errors = append(errors, err.Error())
			}
		}
		if len(errors) > 0 {
			return fmt.Errorf("subscriber error: %s", strings.Join(errors, "\n"))
		}
		return nil
	}
}

func (s *subscriber) Topic() string {
	return s.topic
}

func (s *subscriber) Subscriber() interface{} {
	return s.subscriber
}

func (s *subscriber) Endpoints() []*registry.Endpoint {
	return s.endpoints
}

func (s *subscriber) Options() server.SubscriberOptions {
	return s.opts
}
<|endoftext|>


<commit_before>// robustsession represents a RobustIRC session and handles all communication
// to the RobustIRC network.
package robustsession

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"math"
	"net"
	"net/http"
	"net/url"
	"strings"
	"sync"
	"time"

	"fancyirc/types"

	"github.com/sorcix/irc"
)

const (
	pathCreateSession = "/robustirc/v1/session"
	pathDeleteSession = "/robustirc/v1/%s"
	pathPostMessage   = "/robustirc/v1/%s/message"
	pathGetMessages   = "/robustirc/v1/%s/messages?lastseen=%s"
)

var (
	NoSuchSession = errors.New("No such RobustIRC session (killed by the network?)")

	networks   = make(map[string]*network)
	networksMu sync.Mutex
)

type backoffState struct {
	exp  float64
	next time.Time
}

type network struct {
	servers []string
	mu      sync.RWMutex
	backoff map[string]backoffState
}

func newNetwork(networkname string) (*network, error) {
	var servers []string

	parts := strings.Split(networkname, ",")
	if len(parts) > 1 {
		log.Printf("Interpreting %q as list of servers instead of network name\n", networkname)
		servers = parts
	} else {
		// Try to resolve the DNS name up to 5 times. This is to be nice to
		// people in environments with flaky network connections at boot, who,
		// for some reason, don’t run this program under systemd with
		// Restart=on-failure.
		try := 0
		for {
			_, addrs, err := net.LookupSRV("robustirc", "tcp", networkname)
			if err != nil {
				log.Println(err)
				if try < 4 {
					time.Sleep(time.Duration(int64(math.Pow(2, float64(try)))) * time.Second)
				} else {
					return nil, fmt.Errorf("DNS lookup of %q failed 5 times", networkname)
				}
				try++
				continue
			}
			// TODO(secure): random shuffle
			for _, addr := range addrs {
				target := addr.Target
				if target[len(target)-1] == '.' {
					target = target[:len(target)-1]
				}
				servers = append(servers, fmt.Sprintf("%s:%d", target, addr.Port))
			}
			break
		}
	}

	return &network{
		servers: servers,
		backoff: make(map[string]backoffState),
	}, nil
}

// server (eventually) returns the host:port to which we should connect to. In
// case back-off prevents us from connecting anywhere right now, the function
// blocks until back-off is over.
func (n *network) server() string {
	n.mu.RLock()
	defer n.mu.RUnlock()

	for {
		soonest := time.Duration(math.MaxInt64)
		for _, server := range n.servers {
			wait := n.backoff[server].next.Sub(time.Now())
			if wait <= 0 {
				return server
			}
			if wait < soonest {
				soonest = wait
			}
		}

		time.Sleep(soonest)
	}
}

func (n *network) setServers(servers []string) {
	n.mu.Lock()
	defer n.mu.Unlock()

	// TODO(secure): we should clean up n.backoff from servers which no longer exist
	n.servers = servers
}

func (n *network) prefer(server string) {
	n.mu.Lock()
	defer n.mu.Unlock()

	n.servers = append([]string{server}, n.servers...)
}

func (n *network) failed(server string) {
	n.mu.Lock()
	defer n.mu.Unlock()

	b := n.backoff[server]
	b.exp++
	b.next = time.Now().Add(time.Duration(math.Pow(2, b.exp)) * time.Second)
	n.backoff[server] = b
}

func (n *network) succeeded(server string) {
	n.mu.Lock()
	defer n.mu.Unlock()

	delete(n.backoff, server)
}

func discardResponse(resp *http.Response) {
	// We need to read the entire body, otherwise net/http will not
	// re-use this connection.
	ioutil.ReadAll(resp.Body)
	resp.Body.Close()
}

type RobustSession struct {
	IrcPrefix *irc.Prefix
	Messages  chan string
	Errors    chan error

	sessionId   string
	sessionAuth string
	deleted     bool
	done        chan bool
	network     *network
	msgcounter  int
}

func (s *RobustSession) sendRequest(method, path string, data []byte) (string, *http.Response, error) {
	for !s.deleted {
		target := s.network.server()
		req, err := http.NewRequest(method, fmt.Sprintf("https://%s%s", target, path), bytes.NewBuffer(data))
		if err != nil {
			return "", nil, err
		}
		req.Header.Set("X-Session-Auth", s.sessionAuth)
		req.Header.Set("Content-Type", "application/json")
		resp, err := http.DefaultClient.Do(req)
		if err != nil {
			s.network.failed(target)
			log.Printf("sendRequest(%q) failed: %v\n", path, err)
			continue
		}
		if resp.StatusCode == http.StatusNotFound {
			return "", nil, NoSuchSession
		}
		if resp.StatusCode != http.StatusOK {
			message, _ := ioutil.ReadAll(resp.Body)
			resp.Body.Close()
			s.network.failed(target)
			log.Printf("sendRequest(%q) failed with %v: %q\n", path, resp.Status, message)
			continue
		}
		return target, resp, nil
	}

	return "", nil, NoSuchSession
}

// Create creates a new RobustIRC session. It resolves the given network name
// (e.g. "robustirc.net") to a set of servers by querying the
// _robustirc._tcp.<network> SRV record and sends the CreateSession request.
//
// When err == nil, the caller MUST read the RobustSession.Messages and
// RobustSession.Errors channels.
func Create(network string) (*RobustSession, error) {
	networksMu.Lock()
	n, ok := networks[network]
	if !ok {
		var err error
		n, err = newNetwork(network)
		if err != nil {
			return nil, err
		}
		networks[network] = n
	}
	networksMu.Unlock()

	s := &RobustSession{
		Messages: make(chan string),
		Errors:   make(chan error),
		done:     make(chan bool, 1),
		network:  n,
	}

	_, resp, err := s.sendRequest("POST", pathCreateSession, nil)
	if err != nil {
		return nil, err
	}
	defer discardResponse(resp)

	var createSessionReply struct {
		Sessionid   string
		Sessionauth string
		Prefix      string
	}

	if err := json.NewDecoder(resp.Body).Decode(&createSessionReply); err != nil {
		return nil, err
	}

	if cl := resp.Header.Get("Content-Location"); cl != "" {
		if location, err := url.Parse(cl); err == nil {
			log.Printf("Preferring %q (current leader)\n", location.Host)
			s.network.prefer(location.Host)
		}
	}

	s.sessionId = createSessionReply.Sessionid
	s.sessionAuth = createSessionReply.Sessionauth
	s.IrcPrefix = &irc.Prefix{Name: createSessionReply.Prefix}

	go s.getMessages()

	return s, nil
}

func (s *RobustSession) getMessages() {
	var lastseen types.FancyId

	for !s.deleted {
		target, resp, err := s.sendRequest("GET", fmt.Sprintf(pathGetMessages, s.sessionId, lastseen.String()), nil)
		if err != nil {
			s.Errors <- err
			return
		}

		msgchan := make(chan types.FancyMessage, 1)
		errchan := make(chan error)
		go func() {
			dec := json.NewDecoder(resp.Body)
			for {
				var msg types.FancyMessage
				if err := dec.Decode(&msg); err != nil {
					errchan <- err
					return
				}
				msgchan <- msg
			}
		}()

	ReadLoop:
		for !s.deleted {
			select {
			case msg := <-msgchan:
				if msg.Type == types.FancyPing {
					s.network.setServers(msg.Servers)
				} else if msg.Type == types.FancyIRCToClient {
					// TODO: remove/debug
					log.Printf("<-robustirc: %q\n", msg.Data)
					s.Messages <- msg.Data
					lastseen = msg.Id
				}

			case err := <-errchan:
				log.Printf("Protocol error on %q: Could not decode response chunk as JSON: %v\n", target, err)
				s.network.failed(target)
				break ReadLoop

			case <-time.After(1 * time.Minute):
				log.Printf("Timeout (60s) on GetMessages, reconnecting…\n")
				s.network.failed(target)
				break ReadLoop

			case <-s.done:
				break ReadLoop
			}
		}
		resp.Body.Close()
	}
}

// PostMessage posts the given IRC message.
func (s *RobustSession) PostMessage(message string) error {
	type postMessageRequest struct {
		Data            string
		ClientMessageId int
	}

	s.msgcounter++

	b, err := json.Marshal(postMessageRequest{
		Data:            message,
		ClientMessageId: s.msgcounter,
	})
	if err != nil {
		return fmt.Errorf("Message could not be encoded as JSON: %v\n", err)
	}

	target, resp, err := s.sendRequest("POST", fmt.Sprintf(pathPostMessage, s.sessionId), b)
	if err != nil {
		return err
	}
	discardResponse(resp)
	s.network.succeeded(target)
	return nil
}

// Delete sends a delete request for this session on the server.
//
// This session MUST not be used after this method returns. Even if the delete
// request did not succeed, the session is deleted from the client’s point of
// view.
func (s *RobustSession) Delete(quitmessage string) error {
	defer func() {
		s.deleted = true
		s.done <- true
	}()

	b, err := json.Marshal(struct{ Quitmessage string }{quitmessage})
	if err != nil {
		return err
	}
	_, resp, err := s.sendRequest("DELETE", fmt.Sprintf(pathDeleteSession, s.sessionId), b)
	if err != nil {
		return err
	}
	discardResponse(resp)
	return nil
}
<commit_msg>add a comment to prefer()<commit_after>// robustsession represents a RobustIRC session and handles all communication
// to the RobustIRC network.
package robustsession

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"math"
	"net"
	"net/http"
	"net/url"
	"strings"
	"sync"
	"time"

	"fancyirc/types"

	"github.com/sorcix/irc"
)

const (
	pathCreateSession = "/robustirc/v1/session"
	pathDeleteSession = "/robustirc/v1/%s"
	pathPostMessage   = "/robustirc/v1/%s/message"
	pathGetMessages   = "/robustirc/v1/%s/messages?lastseen=%s"
)

var (
	NoSuchSession = errors.New("No such RobustIRC session (killed by the network?)")

	networks   = make(map[string]*network)
	networksMu sync.Mutex
)

type backoffState struct {
	exp  float64
	next time.Time
}

type network struct {
	servers []string
	mu      sync.RWMutex
	backoff map[string]backoffState
}

func newNetwork(networkname string) (*network, error) {
	var servers []string

	parts := strings.Split(networkname, ",")
	if len(parts) > 1 {
		log.Printf("Interpreting %q as list of servers instead of network name\n", networkname)
		servers = parts
	} else {
		// Try to resolve the DNS name up to 5 times. This is to be nice to
		// people in environments with flaky network connections at boot, who,
		// for some reason, don’t run this program under systemd with
		// Restart=on-failure.
		try := 0
		for {
			_, addrs, err := net.LookupSRV("robustirc", "tcp", networkname)
			if err != nil {
				log.Println(err)
				if try < 4 {
					time.Sleep(time.Duration(int64(math.Pow(2, float64(try)))) * time.Second)
				} else {
					return nil, fmt.Errorf("DNS lookup of %q failed 5 times", networkname)
				}
				try++
				continue
			}
			// TODO(secure): random shuffle
			for _, addr := range addrs {
				target := addr.Target
				if target[len(target)-1] == '.' {
					target = target[:len(target)-1]
				}
				servers = append(servers, fmt.Sprintf("%s:%d", target, addr.Port))
			}
			break
		}
	}

	return &network{
		servers: servers,
		backoff: make(map[string]backoffState),
	}, nil
}

// server (eventually) returns the host:port to which we should connect to. In
// case back-off prevents us from connecting anywhere right now, the function
// blocks until back-off is over.
func (n *network) server() string {
	n.mu.RLock()
	defer n.mu.RUnlock()

	for {
		soonest := time.Duration(math.MaxInt64)
		for _, server := range n.servers {
			wait := n.backoff[server].next.Sub(time.Now())
			if wait <= 0 {
				return server
			}
			if wait < soonest {
				soonest = wait
			}
		}

		time.Sleep(soonest)
	}
}

func (n *network) setServers(servers []string) {
	n.mu.Lock()
	defer n.mu.Unlock()

	// TODO(secure): we should clean up n.backoff from servers which no longer exist
	n.servers = servers
}

// prefer adds the specified server to the front of the servers list, thereby
// trying to prefer it over other servers for the next request. Note that
// exponential backoff overrides this, so this is only a hint, not a guarantee.
func (n *network) prefer(server string) {
	n.mu.Lock()
	defer n.mu.Unlock()

	n.servers = append([]string{server}, n.servers...)
}

func (n *network) failed(server string) {
	n.mu.Lock()
	defer n.mu.Unlock()

	b := n.backoff[server]
	b.exp++
	b.next = time.Now().Add(time.Duration(math.Pow(2, b.exp)) * time.Second)
	n.backoff[server] = b
}

func (n *network) succeeded(server string) {
	n.mu.Lock()
	defer n.mu.Unlock()

	delete(n.backoff, server)
}

func discardResponse(resp *http.Response) {
	// We need to read the entire body, otherwise net/http will not
	// re-use this connection.
	ioutil.ReadAll(resp.Body)
	resp.Body.Close()
}

type RobustSession struct {
	IrcPrefix *irc.Prefix
	Messages  chan string
	Errors    chan error

	sessionId   string
	sessionAuth string
	deleted     bool
	done        chan bool
	network     *network
	msgcounter  int
}

func (s *RobustSession) sendRequest(method, path string, data []byte) (string, *http.Response, error) {
	for !s.deleted {
		target := s.network.server()
		req, err := http.NewRequest(method, fmt.Sprintf("https://%s%s", target, path), bytes.NewBuffer(data))
		if err != nil {
			return "", nil, err
		}
		req.Header.Set("X-Session-Auth", s.sessionAuth)
		req.Header.Set("Content-Type", "application/json")
		resp, err := http.DefaultClient.Do(req)
		if err != nil {
			s.network.failed(target)
			log.Printf("sendRequest(%q) failed: %v\n", path, err)
			continue
		}
		if resp.StatusCode == http.StatusNotFound {
			return "", nil, NoSuchSession
		}
		if resp.StatusCode != http.StatusOK {
			message, _ := ioutil.ReadAll(resp.Body)
			resp.Body.Close()
			s.network.failed(target)
			log.Printf("sendRequest(%q) failed with %v: %q\n", path, resp.Status, message)
			continue
		}
		return target, resp, nil
	}

	return "", nil, NoSuchSession
}

// Create creates a new RobustIRC session. It resolves the given network name
// (e.g. "robustirc.net") to a set of servers by querying the
// _robustirc._tcp.<network> SRV record and sends the CreateSession request.
//
// When err == nil, the caller MUST read the RobustSession.Messages and
// RobustSession.Errors channels.
func Create(network string) (*RobustSession, error) {
	networksMu.Lock()
	n, ok := networks[network]
	if !ok {
		var err error
		n, err = newNetwork(network)
		if err != nil {
			return nil, err
		}
		networks[network] = n
	}
	networksMu.Unlock()

	s := &RobustSession{
		Messages: make(chan string),
		Errors:   make(chan error),
		done:     make(chan bool, 1),
		network:  n,
	}

	_, resp, err := s.sendRequest("POST", pathCreateSession, nil)
	if err != nil {
		return nil, err
	}
	defer discardResponse(resp)

	var createSessionReply struct {
		Sessionid   string
		Sessionauth string
		Prefix      string
	}

	if err := json.NewDecoder(resp.Body).Decode(&createSessionReply); err != nil {
		return nil, err
	}

	if cl := resp.Header.Get("Content-Location"); cl != "" {
		if location, err := url.Parse(cl); err == nil {
			log.Printf("Preferring %q (current leader)\n", location.Host)
			s.network.prefer(location.Host)
		}
	}

	s.sessionId = createSessionReply.Sessionid
	s.sessionAuth = createSessionReply.Sessionauth
	s.IrcPrefix = &irc.Prefix{Name: createSessionReply.Prefix}

	go s.getMessages()

	return s, nil
}

func (s *RobustSession) getMessages() {
	var lastseen types.FancyId

	for !s.deleted {
		target, resp, err := s.sendRequest("GET", fmt.Sprintf(pathGetMessages, s.sessionId, lastseen.String()), nil)
		if err != nil {
			s.Errors <- err
			return
		}

		msgchan := make(chan types.FancyMessage, 1)
		errchan := make(chan error)
		go func() {
			dec := json.NewDecoder(resp.Body)
			for {
				var msg types.FancyMessage
				if err := dec.Decode(&msg); err != nil {
					errchan <- err
					return
				}
				msgchan <- msg
			}
		}()

	ReadLoop:
		for !s.deleted {
			select {
			case msg := <-msgchan:
				if msg.Type == types.FancyPing {
					s.network.setServers(msg.Servers)
				} else if msg.Type == types.FancyIRCToClient {
					// TODO: remove/debug
					log.Printf("<-robustirc: %q\n", msg.Data)
					s.Messages <- msg.Data
					lastseen = msg.Id
				}

			case err := <-errchan:
				log.Printf("Protocol error on %q: Could not decode response chunk as JSON: %v\n", target, err)
				s.network.failed(target)
				break ReadLoop

			case <-time.After(1 * time.Minute):
				log.Printf("Timeout (60s) on GetMessages, reconnecting…\n")
				s.network.failed(target)
				break ReadLoop

			case <-s.done:
				break ReadLoop
			}
		}
		resp.Body.Close()
	}
}

// PostMessage posts the given IRC message.
func (s *RobustSession) PostMessage(message string) error {
	type postMessageRequest struct {
		Data            string
		ClientMessageId int
	}

	s.msgcounter++

	b, err := json.Marshal(postMessageRequest{
		Data:            message,
		ClientMessageId: s.msgcounter,
	})
	if err != nil {
		return fmt.Errorf("Message could not be encoded as JSON: %v\n", err)
	}

	target, resp, err := s.sendRequest("POST", fmt.Sprintf(pathPostMessage, s.sessionId), b)
	if err != nil {
		return err
	}
	discardResponse(resp)
	s.network.succeeded(target)
	return nil
}

// Delete sends a delete request for this session on the server.
//
// This session MUST not be used after this method returns. Even if the delete
// request did not succeed, the session is deleted from the client’s point of
// view.
func (s *RobustSession) Delete(quitmessage string) error {
	defer func() {
		s.deleted = true
		s.done <- true
	}()

	b, err := json.Marshal(struct{ Quitmessage string }{quitmessage})
	if err != nil {
		return err
	}
	_, resp, err := s.sendRequest("DELETE", fmt.Sprintf(pathDeleteSession, s.sessionId), b)
	if err != nil {
		return err
	}
	discardResponse(resp)
	return nil
}
<|endoftext|>


<commit_before>package handler

import (
	"net/http"

	"github.com/cloudfoundry-incubator/bbs"
	"github.com/cloudfoundry-incubator/tps"
	"github.com/cloudfoundry-incubator/tps/handler/bulklrpstatus"
	"github.com/cloudfoundry-incubator/tps/handler/lrpstats"
	"github.com/cloudfoundry-incubator/tps/handler/lrpstatus"
	"github.com/pivotal-golang/clock"
	"github.com/pivotal-golang/lager"
	"github.com/tedsuo/rata"
)

func New(apiClient bbs.Client, noaaClient lrpstats.NoaaClient, maxInFlight, bulkLRPStatusWorkers int, logger lager.Logger) (http.Handler, error) {
	semaphore := make(chan struct{}, maxInFlight)
	clock := clock.NewClock()

	handlers := map[string]http.Handler{
		tps.LRPStatus: tpsHandler{
			semaphore:       semaphore,
			delegateHandler: LogWrap(lrpstatus.NewHandler(apiClient, clock, logger), logger),
		},
		tps.LRPStats: tpsHandler{
			semaphore:       semaphore,
			delegateHandler: LogWrap(lrpstats.NewHandler(apiClient, noaaClient, clock, logger), logger),
		},
		tps.BulkLRPStatus: tpsHandler{
			semaphore:       semaphore,
			delegateHandler: LogWrap(bulklrpstatus.NewHandler(apiClient, clock, bulkLRPStatusWorkers, logger), logger),
		},
	}

	return rata.NewRouter(tps.Routes, handlers)
}

type tpsHandler struct {
	semaphore       chan struct{}
	delegateHandler http.Handler
}

func (handler tpsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	select {
	case handler.semaphore <- struct{}{}:
	default:
		w.WriteHeader(http.StatusServiceUnavailable)
		return
	}

	defer func() {
		<-handler.semaphore
	}()

	handler.delegateHandler.ServeHTTP(w, r)
}

func logWrap(handler http.Handler, logger lager.Logger) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		requestLog := logger.Session("request", lager.Data{
			"method":  r.Method,
			"request": r.URL.String(),
		})

		requestLog.Info("serving")
		handler.ServeHTTP(w, r)
		requestLog.Info("done")
	}
}
<commit_msg>remove unused logWrap<commit_after>package handler

import (
	"net/http"

	"github.com/cloudfoundry-incubator/bbs"
	"github.com/cloudfoundry-incubator/tps"
	"github.com/cloudfoundry-incubator/tps/handler/bulklrpstatus"
	"github.com/cloudfoundry-incubator/tps/handler/lrpstats"
	"github.com/cloudfoundry-incubator/tps/handler/lrpstatus"
	"github.com/pivotal-golang/clock"
	"github.com/pivotal-golang/lager"
	"github.com/tedsuo/rata"
)

func New(apiClient bbs.Client, noaaClient lrpstats.NoaaClient, maxInFlight, bulkLRPStatusWorkers int, logger lager.Logger) (http.Handler, error) {
	semaphore := make(chan struct{}, maxInFlight)
	clock := clock.NewClock()

	handlers := map[string]http.Handler{
		tps.LRPStatus: tpsHandler{
			semaphore:       semaphore,
			delegateHandler: LogWrap(lrpstatus.NewHandler(apiClient, clock, logger), logger),
		},
		tps.LRPStats: tpsHandler{
			semaphore:       semaphore,
			delegateHandler: LogWrap(lrpstats.NewHandler(apiClient, noaaClient, clock, logger), logger),
		},
		tps.BulkLRPStatus: tpsHandler{
			semaphore:       semaphore,
			delegateHandler: LogWrap(bulklrpstatus.NewHandler(apiClient, clock, bulkLRPStatusWorkers, logger), logger),
		},
	}

	return rata.NewRouter(tps.Routes, handlers)
}

type tpsHandler struct {
	semaphore       chan struct{}
	delegateHandler http.Handler
}

func (handler tpsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	select {
	case handler.semaphore <- struct{}{}:
	default:
		w.WriteHeader(http.StatusServiceUnavailable)
		return
	}

	defer func() {
		<-handler.semaphore
	}()

	handler.delegateHandler.ServeHTTP(w, r)
}
<|endoftext|>


<commit_before>package veneur

import (
	"compress/zlib"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"time"
)

type contextHandler func(c context.Context, w http.ResponseWriter, r *http.Request)

func (ch contextHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// nil context is bad form, but since we don't use it, it's a quick hack
	// to allow us to write tests before we upgrade to Go 1.7 Context type
	// and the newer goji
	// TODO(aditya) actually update this
	ch(nil, w, r)
}

// handleImport generates the handler that responds to POST requests submitting
// metrics to the global veneur instance.
func handleImport(s *Server) http.Handler {
	return contextHandler(func(c context.Context, w http.ResponseWriter, r *http.Request) {
		s.logger.Debug("HI GUYS I AM RUNNING IN A TEST")

		innerLogger := s.logger.WithField("client", r.RemoteAddr)
		start := time.Now()

		var (
			jsonMetrics []JSONMetric
			body        io.ReadCloser
			err         error
			encoding    = r.Header.Get("Content-Encoding")
		)
		switch encLogger := innerLogger.WithField("encoding", encoding); encoding {
		case "":
			body = r.Body
			encoding = "identity"
		case "deflate":
			body, err = zlib.NewReader(r.Body)
			if err != nil {
				http.Error(w, err.Error(), http.StatusBadRequest)
				encLogger.WithError(err).Error("Could not read compressed request body")
				s.statsd.Count("import.request_error_total", 1, []string{"cause:deflate"}, 1.0)
				return
			}
			defer body.Close()
		default:
			http.Error(w, encoding, http.StatusUnsupportedMediaType)
			encLogger.Error("Could not determine content-encoding of request")
			s.statsd.Count("import.request_error_total", 1, []string{"cause:unknown_content_encoding"}, 1.0)
			return
		}

		if err := json.NewDecoder(body).Decode(&jsonMetrics); err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			innerLogger.WithError(err).Error("Could not decode /import request")
			s.statsd.Count("import.request_error_total", 1, []string{"cause:json"}, 1.0)
			return
		}

		w.WriteHeader(http.StatusAccepted)
		s.statsd.TimeInMilliseconds("import.response_duration_ns",
			float64(time.Now().Sub(start).Nanoseconds()),
			[]string{"part:request", fmt.Sprintf("encoding:%s", encoding)},
			1.0)

		// the server usually waits for this to return before finalizing the
		// response, so this part must be done asynchronously
		go s.ImportMetrics(jsonMetrics)
	})
}
<commit_msg>Use context from 1.5 (in /x/)<commit_after>package veneur

import (
	"compress/zlib"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"time"

	"golang.org/x/net/context"
)

type contextHandler func(c context.Context, w http.ResponseWriter, r *http.Request)

func (ch contextHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	// nil context is bad form, but since we don't use it, it's a quick hack
	// to allow us to write tests before we upgrade to Go 1.7 Context type
	// and the newer goji
	// TODO(aditya) actually update this
	ch(nil, w, r)
}

// handleImport generates the handler that responds to POST requests submitting
// metrics to the global veneur instance.
func handleImport(s *Server) http.Handler {
	return contextHandler(func(c context.Context, w http.ResponseWriter, r *http.Request) {
		s.logger.Debug("HI GUYS I AM RUNNING IN A TEST")

		innerLogger := s.logger.WithField("client", r.RemoteAddr)
		start := time.Now()

		var (
			jsonMetrics []JSONMetric
			body        io.ReadCloser
			err         error
			encoding    = r.Header.Get("Content-Encoding")
		)
		switch encLogger := innerLogger.WithField("encoding", encoding); encoding {
		case "":
			body = r.Body
			encoding = "identity"
		case "deflate":
			body, err = zlib.NewReader(r.Body)
			if err != nil {
				http.Error(w, err.Error(), http.StatusBadRequest)
				encLogger.WithError(err).Error("Could not read compressed request body")
				s.statsd.Count("import.request_error_total", 1, []string{"cause:deflate"}, 1.0)
				return
			}
			defer body.Close()
		default:
			http.Error(w, encoding, http.StatusUnsupportedMediaType)
			encLogger.Error("Could not determine content-encoding of request")
			s.statsd.Count("import.request_error_total", 1, []string{"cause:unknown_content_encoding"}, 1.0)
			return
		}

		if err := json.NewDecoder(body).Decode(&jsonMetrics); err != nil {
			http.Error(w, err.Error(), http.StatusBadRequest)
			innerLogger.WithError(err).Error("Could not decode /import request")
			s.statsd.Count("import.request_error_total", 1, []string{"cause:json"}, 1.0)
			return
		}

		w.WriteHeader(http.StatusAccepted)
		s.statsd.TimeInMilliseconds("import.response_duration_ns",
			float64(time.Now().Sub(start).Nanoseconds()),
			[]string{"part:request", fmt.Sprintf("encoding:%s", encoding)},
			1.0)

		// the server usually waits for this to return before finalizing the
		// response, so this part must be done asynchronously
		go s.ImportMetrics(jsonMetrics)
	})
}
<|endoftext|>