<commit_before>/*
Copyright 2016 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package watch

import (
	"context"
	"errors"
	"fmt"
	"time"

	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/apimachinery/pkg/watch"
	"k8s.io/client-go/tools/cache"
	"k8s.io/klog/v2"
)

// PreconditionFunc returns true if the condition has been reached, false if it has not been reached yet,
// or an error if the condition failed or detected an error state.
type PreconditionFunc func(store cache.Store) (bool, error)

// ConditionFunc returns true if the condition has been reached, false if it has not been reached yet,
// or an error if the condition cannot be checked and should terminate. In general, it is better to define
// level driven conditions over edge driven conditions (pod has ready=true, vs pod modified and ready changed
// from false to true).
type ConditionFunc func(event watch.Event) (bool, error)

// ErrWatchClosed is returned when the watch channel is closed before timeout in UntilWithoutRetry.
var ErrWatchClosed = errors.New("watch closed before UntilWithoutRetry timeout")

// UntilWithoutRetry reads items from the watch until each provided condition succeeds, and then returns the last watch
// encountered. The first condition that returns an error terminates the watch (and the event is also returned).
// If no event has been received, the returned event will be nil.
// Conditions are satisfied sequentially so as to provide a useful primitive for higher level composition.
// Waits until context deadline or until context is canceled.
//
// Warning: Unless you have a very specific use case (probably a special Watcher) don't use this function!!!
// Warning: This will fail e.g. on API timeouts and/or 'too old resource version' error.
// Warning: You are most probably looking for a function *Until* or *UntilWithSync* below,
// Warning: solving such issues.
// TODO: Consider making this function private to prevent misuse when the other occurrences in our codebase are gone.
func UntilWithoutRetry(ctx context.Context, watcher watch.Interface, conditions ...ConditionFunc) (*watch.Event, error) {
	ch := watcher.ResultChan()
	defer watcher.Stop()
	var lastEvent *watch.Event
	for _, condition := range conditions {
		// check the next condition against the previous event and short circuit waiting for the next watch
		if lastEvent != nil {
			done, err := condition(*lastEvent)
			if err != nil {
				return lastEvent, err
			}
			if done {
				continue
			}
		}
	ConditionSucceeded:
		for {
			select {
			case event, ok := <-ch:
				if !ok {
					return lastEvent, ErrWatchClosed
				}
				lastEvent = &event

				done, err := condition(event)
				if err != nil {
					return lastEvent, err
				}
				if done {
					break ConditionSucceeded
				}

			case <-ctx.Done():
				return lastEvent, wait.ErrWaitTimeout
			}
		}
	}
	return lastEvent, nil
}

// Until wraps the watcherClient's watch function with RetryWatcher making sure that watcher gets restarted in case of errors.
// The initialResourceVersion will be given to watch method when first called. It shall not be "" or "0"
// given the underlying WATCH call issues (#74022). If you want the initial list ("", "0") done for you use ListWatchUntil instead.
// Remaining behaviour is identical to function UntilWithoutRetry. (See above.)
// Until can deal with API timeouts and lost connections.
// It guarantees you to see all events and in the order they happened.
// Due to this guarantee there is no way it can deal with 'Resource version too old error'. It will fail in this case.
// (See `UntilWithSync` if you'd prefer to recover from all the errors including RV too old by re-listing
//  those items. In normal code you should care about being level driven so you'd not care about not seeing all the edges.)
// The most frequent usage for Until would be a test where you want to verify exact order of events ("edges").
func Until(ctx context.Context, initialResourceVersion string, watcherClient cache.Watcher, conditions ...ConditionFunc) (*watch.Event, error) {
	w, err := NewRetryWatcher(initialResourceVersion, watcherClient)
	if err != nil {
		return nil, err
	}

	return UntilWithoutRetry(ctx, w, conditions...)
}

// UntilWithSync creates an informer from lw, optionally checks precondition when the store is synced,
// and watches the output until each provided condition succeeds, in a way that is identical
// to function UntilWithoutRetry. (See above.)
// UntilWithSync can deal with all errors like API timeout, lost connections and 'Resource version too old'.
// It is the only function that can recover from 'Resource version too old', Until and UntilWithoutRetry will
// just fail in that case. On the other hand it can't provide you with guarantees as strong as using simple
// Watch method with Until. It can skip some intermediate events in case of watch function failing but it will
// re-list to recover and you always get an event, if there has been a change, after recovery.
// Also with the current implementation based on DeltaFIFO, order of the events you receive is guaranteed only for
// particular object, not between more of them even it's the same resource.
// The most frequent usage would be a command that needs to watch the "state of the world" and should't fail, like:
// waiting for object reaching a state, "small" controllers, ...
func UntilWithSync(ctx context.Context, lw cache.ListerWatcher, objType runtime.Object, precondition PreconditionFunc, conditions ...ConditionFunc) (*watch.Event, error) {
	indexer, informer, watcher, done := NewIndexerInformerWatcher(lw, objType)
	// We need to wait for the internal informers to fully stop so it's easier to reason about
	// and it works with non-thread safe clients.
	defer func() { <-done }()
	// Proxy watcher can be stopped multiple times so it's fine to use defer here to cover alternative branches and
	// let UntilWithoutRetry to stop it
	defer watcher.Stop()

	if precondition != nil {
		if !cache.WaitForCacheSync(ctx.Done(), informer.HasSynced) {
			return nil, fmt.Errorf("UntilWithSync: unable to sync caches: %v", ctx.Err())
		}

		done, err := precondition(indexer)
		if err != nil {
			return nil, err
		}

		if done {
			return nil, nil
		}
	}

	return UntilWithoutRetry(ctx, watcher, conditions...)
}

// ContextWithOptionalTimeout wraps context.WithTimeout and handles infinite timeouts expressed as 0 duration.
func ContextWithOptionalTimeout(parent context.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
	if timeout < 0 {
		// This should be handled in validation
		klog.Errorf("Timeout for context shall not be negative!")
		timeout = 0
	}

	if timeout == 0 {
		return context.WithCancel(parent)
	}

	return context.WithTimeout(parent, timeout)
}
<commit_msg>Remove outdated comment on wait.Until<commit_after>/*
Copyright 2016 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package watch

import (
	"context"
	"errors"
	"fmt"
	"time"

	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/apimachinery/pkg/watch"
	"k8s.io/client-go/tools/cache"
	"k8s.io/klog/v2"
)

// PreconditionFunc returns true if the condition has been reached, false if it has not been reached yet,
// or an error if the condition failed or detected an error state.
type PreconditionFunc func(store cache.Store) (bool, error)

// ConditionFunc returns true if the condition has been reached, false if it has not been reached yet,
// or an error if the condition cannot be checked and should terminate. In general, it is better to define
// level driven conditions over edge driven conditions (pod has ready=true, vs pod modified and ready changed
// from false to true).
type ConditionFunc func(event watch.Event) (bool, error)

// ErrWatchClosed is returned when the watch channel is closed before timeout in UntilWithoutRetry.
var ErrWatchClosed = errors.New("watch closed before UntilWithoutRetry timeout")

// UntilWithoutRetry reads items from the watch until each provided condition succeeds, and then returns the last watch
// encountered. The first condition that returns an error terminates the watch (and the event is also returned).
// If no event has been received, the returned event will be nil.
// Conditions are satisfied sequentially so as to provide a useful primitive for higher level composition.
// Waits until context deadline or until context is canceled.
//
// Warning: Unless you have a very specific use case (probably a special Watcher) don't use this function!!!
// Warning: This will fail e.g. on API timeouts and/or 'too old resource version' error.
// Warning: You are most probably looking for a function *Until* or *UntilWithSync* below,
// Warning: solving such issues.
// TODO: Consider making this function private to prevent misuse when the other occurrences in our codebase are gone.
func UntilWithoutRetry(ctx context.Context, watcher watch.Interface, conditions ...ConditionFunc) (*watch.Event, error) {
	ch := watcher.ResultChan()
	defer watcher.Stop()
	var lastEvent *watch.Event
	for _, condition := range conditions {
		// check the next condition against the previous event and short circuit waiting for the next watch
		if lastEvent != nil {
			done, err := condition(*lastEvent)
			if err != nil {
				return lastEvent, err
			}
			if done {
				continue
			}
		}
	ConditionSucceeded:
		for {
			select {
			case event, ok := <-ch:
				if !ok {
					return lastEvent, ErrWatchClosed
				}
				lastEvent = &event

				done, err := condition(event)
				if err != nil {
					return lastEvent, err
				}
				if done {
					break ConditionSucceeded
				}

			case <-ctx.Done():
				return lastEvent, wait.ErrWaitTimeout
			}
		}
	}
	return lastEvent, nil
}

// Until wraps the watcherClient's watch function with RetryWatcher making sure that watcher gets restarted in case of errors.
// The initialResourceVersion will be given to watch method when first called. It shall not be "" or "0"
// given the underlying WATCH call issues (#74022).
// Remaining behaviour is identical to function UntilWithoutRetry. (See above.)
// Until can deal with API timeouts and lost connections.
// It guarantees you to see all events and in the order they happened.
// Due to this guarantee there is no way it can deal with 'Resource version too old error'. It will fail in this case.
// (See `UntilWithSync` if you'd prefer to recover from all the errors including RV too old by re-listing
//  those items. In normal code you should care about being level driven so you'd not care about not seeing all the edges.)
// The most frequent usage for Until would be a test where you want to verify exact order of events ("edges").
func Until(ctx context.Context, initialResourceVersion string, watcherClient cache.Watcher, conditions ...ConditionFunc) (*watch.Event, error) {
	w, err := NewRetryWatcher(initialResourceVersion, watcherClient)
	if err != nil {
		return nil, err
	}

	return UntilWithoutRetry(ctx, w, conditions...)
}

// UntilWithSync creates an informer from lw, optionally checks precondition when the store is synced,
// and watches the output until each provided condition succeeds, in a way that is identical
// to function UntilWithoutRetry. (See above.)
// UntilWithSync can deal with all errors like API timeout, lost connections and 'Resource version too old'.
// It is the only function that can recover from 'Resource version too old', Until and UntilWithoutRetry will
// just fail in that case. On the other hand it can't provide you with guarantees as strong as using simple
// Watch method with Until. It can skip some intermediate events in case of watch function failing but it will
// re-list to recover and you always get an event, if there has been a change, after recovery.
// Also with the current implementation based on DeltaFIFO, order of the events you receive is guaranteed only for
// particular object, not between more of them even it's the same resource.
// The most frequent usage would be a command that needs to watch the "state of the world" and should't fail, like:
// waiting for object reaching a state, "small" controllers, ...
func UntilWithSync(ctx context.Context, lw cache.ListerWatcher, objType runtime.Object, precondition PreconditionFunc, conditions ...ConditionFunc) (*watch.Event, error) {
	indexer, informer, watcher, done := NewIndexerInformerWatcher(lw, objType)
	// We need to wait for the internal informers to fully stop so it's easier to reason about
	// and it works with non-thread safe clients.
	defer func() { <-done }()
	// Proxy watcher can be stopped multiple times so it's fine to use defer here to cover alternative branches and
	// let UntilWithoutRetry to stop it
	defer watcher.Stop()

	if precondition != nil {
		if !cache.WaitForCacheSync(ctx.Done(), informer.HasSynced) {
			return nil, fmt.Errorf("UntilWithSync: unable to sync caches: %v", ctx.Err())
		}

		done, err := precondition(indexer)
		if err != nil {
			return nil, err
		}

		if done {
			return nil, nil
		}
	}

	return UntilWithoutRetry(ctx, watcher, conditions...)
}

// ContextWithOptionalTimeout wraps context.WithTimeout and handles infinite timeouts expressed as 0 duration.
func ContextWithOptionalTimeout(parent context.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
	if timeout < 0 {
		// This should be handled in validation
		klog.Errorf("Timeout for context shall not be negative!")
		timeout = 0
	}

	if timeout == 0 {
		return context.WithCancel(parent)
	}

	return context.WithTimeout(parent, timeout)
}
<|endoftext|>


<commit_before>package main

import (
	"bytes"
	"encoding/json"
	"net/http"
	"time"

	"golang.org/x/net/context"
	"google.golang.org/appengine/datastore"
	"google.golang.org/appengine/delay"
	"google.golang.org/appengine/log"
	"google.golang.org/appengine/urlfetch"
)

// IncomingWebhook is a child element of oauth2 response
type IncomingWebhook struct {
	URL              string `json:"url"`
	Channel          string `json:"channel"`
	ConfigurationURL string `json:"configuration_url"`
}

// AccessToken is a struct for oauth2 response
type AccessToken struct {
	AccessToken     string          `json:"access_token"`
	Scope           string          `json:"scope"`
	TeamName        string          `json:"team_name"`
	TeamID          string          `json:"team_id"`
	IncomingWebhook IncomingWebhook `json:"incoming_webhook"`
	OK              bool            `json:"ok"`
	Error           string          `json:"error"`
}

// ReviewNotify is a Struct for AppStore review notification setting, definition of datastore kind
type ReviewNotify struct {
	Code             string    `json:"code"`
	AppID            string    `json:"app_id" validate:"min=5,max=20,regexp=^[0-9]+$"`
	CountryCode      string    `json:"country_code" validate:"min=5,max=6,regexp=^[0-9]+$"`
	Title            string    `json:"title"`
	AccessToken      string    `json:"access_token"`
	WebhookURL       string    `json:"webhook_url" validate:"regexp=^https.+$"`
	TeamName         string    `json:"team_name"`
	TeamID           string    `json:"team_id"`
	Channel          string    `json:"channel" validate:"regexp=^#.+$"`
	ConfigurationURL string    `json:"configuration_url" validate:"regexp=^https.+$"`
	SetUpCompleted   bool      `json:"-"`
	UpdatedAt        time.Time `json:"updated_at"`
	CreatedAt        time.Time `json:"created_at"`
}

func (rn *ReviewNotify) key(ctx context.Context) *datastore.Key {
	return datastore.NewKey(ctx, "ReviewNotify", rn.Code, 0, nil)
}

//Create new ReviewNotify Entity
func (rn *ReviewNotify) Create(ctx context.Context) (*ReviewNotify, error) {
	rn.SetUpCompleted = false
	rn.CreatedAt = time.Now()
	rn.UpdatedAt = time.Now()
	_, err := datastore.Put(ctx, rn.key(ctx), rn)
	if err != nil {
		return nil, err
	}
	return rn, nil
}

//Update existing ReviewNotify Entity
func (rn *ReviewNotify) Update(ctx context.Context) (*ReviewNotify, error) {
	rn.UpdatedAt = time.Now()
	_, err := datastore.Put(ctx, rn.key(ctx), rn)
	if err != nil {
		return nil, err
	}
	return rn, nil
}

type AppStoreID struct {
	CountryDomain string
	CountryName   string
	CountryCode   string
}

// AppReview is a kind which stores reviews of a app, a entity == a review
type AppReview struct {
	KeyName   string    `json:"key_name" datastore:"KeyName"`
	AppID     string    `json:"app_id" datastore:"AppID"`
	Code      string    `json:"code" datastore:"Code"`
	ReviewID  string    `json:"review_id" datastore:"ReviewID"`
	Star      int       `json:"star" datastore:"Star"`
	Title     string    `json:"title" datastore:"Tite,noindex"`
	Content   string    `json:"content" datastore:"Content,noindex"`
	Version   string    `json:"version" datastore:"Version,noindex"`
	CreatedAt time.Time `json:"created_at" datastore:"CreatedAt"`
}

func (ar *AppReview) key(ctx context.Context) *datastore.Key {
	return datastore.NewKey(ctx, "AppReview", ar.KeyName, 0, nil)
}

func NotifyReviewToSlack(ctx context.Context, ar *AppReview) {
	var rn ReviewNotify
	key := datastore.NewKey(ctx, "ReviewNotify", ar.Code, 0, nil)
	err := datastore.Get(ctx, key, &rn)
	if err != nil {
		log.Infof(ctx, "%v", err)
		return
	}
	client := urlfetch.Client(ctx)
	iconURL := "https://bell-apps.appspot.com/static/icon57.png"
	text := "[" + rn.Title + "]\n" + ar.Title + ":\n" + ar.Content + "\n" + ar.Version
	var fields []map[string]interface{}
	fields = append(fields, map[string]interface{}{
		"title": "meta",
		"value": ar.Version,
		"short": false,
	})
	var attachments []map[string]interface{}
	attachments = append(attachments, map[string]interface{}{
		"fallback": text,
		"pretext":  rn.Title,
		"color":    "#8EFCD3",
		"title":    ar.Title,
		"text":     ar.Content + "\n",
		"fields":   fields,
	})
	payload := map[string]interface{}{"attachments": attachments, "username": "Bell Apps App Review Notification", "icon_url": iconURL, "mrkdwn": false}
	payloadJSON, err := json.Marshal(payload)
	if err != nil {
		log.Infof(ctx, "%v", err)
		return
	}
	b := bytes.NewBuffer(payloadJSON)
	req, _ := http.NewRequest("POST", rn.WebhookURL, b)
	req.Header.Set("Content-Type", "application/json")
	resp, _ := client.Do(req)
	defer resp.Body.Close()
	if resp.StatusCode == http.StatusNotFound {
		rn.SetUpCompleted = false
		_, err = rn.Update(ctx)
	}
}

var notifyToSlackAsync = delay.Func("put", NotifyReviewToSlack)

// Save puts to datastore
func (ar *AppReview) Create(ctx context.Context) (*AppReview, error) {
	var appreview AppReview
	ar.KeyName = ar.Code + "_" + ar.AppID + "_" + ar.ReviewID
	err := datastore.Get(ctx, ar.key(ctx), &appreview)
	if err == nil {
		log.Infof(ctx, "already registered")
		return &appreview, nil
	}
	ar.CreatedAt = time.Now()
	_, err = datastore.Put(ctx, ar.key(ctx), ar)
	if err != nil {
		return nil, err
	}
	notifyToSlackAsync.Call(ctx, ar)
	return ar, nil
}
<commit_msg>add star count done<commit_after>package main

import (
	"bytes"
	"encoding/json"
	"net/http"
	"time"

	"golang.org/x/net/context"
	"google.golang.org/appengine/datastore"
	"google.golang.org/appengine/delay"
	"google.golang.org/appengine/log"
	"google.golang.org/appengine/urlfetch"
)

// IncomingWebhook is a child element of oauth2 response
type IncomingWebhook struct {
	URL              string `json:"url"`
	Channel          string `json:"channel"`
	ConfigurationURL string `json:"configuration_url"`
}

// AccessToken is a struct for oauth2 response
type AccessToken struct {
	AccessToken     string          `json:"access_token"`
	Scope           string          `json:"scope"`
	TeamName        string          `json:"team_name"`
	TeamID          string          `json:"team_id"`
	IncomingWebhook IncomingWebhook `json:"incoming_webhook"`
	OK              bool            `json:"ok"`
	Error           string          `json:"error"`
}

// ReviewNotify is a Struct for AppStore review notification setting, definition of datastore kind
type ReviewNotify struct {
	Code             string    `json:"code"`
	AppID            string    `json:"app_id" validate:"min=5,max=20,regexp=^[0-9]+$"`
	CountryCode      string    `json:"country_code" validate:"min=5,max=6,regexp=^[0-9]+$"`
	Title            string    `json:"title"`
	AccessToken      string    `json:"access_token"`
	WebhookURL       string    `json:"webhook_url" validate:"regexp=^https.+$"`
	TeamName         string    `json:"team_name"`
	TeamID           string    `json:"team_id"`
	Channel          string    `json:"channel" validate:"regexp=^#.+$"`
	ConfigurationURL string    `json:"configuration_url" validate:"regexp=^https.+$"`
	SetUpCompleted   bool      `json:"-"`
	UpdatedAt        time.Time `json:"updated_at"`
	CreatedAt        time.Time `json:"created_at"`
}

func (rn *ReviewNotify) key(ctx context.Context) *datastore.Key {
	return datastore.NewKey(ctx, "ReviewNotify", rn.Code, 0, nil)
}

//Create new ReviewNotify Entity
func (rn *ReviewNotify) Create(ctx context.Context) (*ReviewNotify, error) {
	rn.SetUpCompleted = false
	rn.CreatedAt = time.Now()
	rn.UpdatedAt = time.Now()
	_, err := datastore.Put(ctx, rn.key(ctx), rn)
	if err != nil {
		return nil, err
	}
	return rn, nil
}

//Update existing ReviewNotify Entity
func (rn *ReviewNotify) Update(ctx context.Context) (*ReviewNotify, error) {
	rn.UpdatedAt = time.Now()
	_, err := datastore.Put(ctx, rn.key(ctx), rn)
	if err != nil {
		return nil, err
	}
	return rn, nil
}

type AppStoreID struct {
	CountryDomain string
	CountryName   string
	CountryCode   string
}

// AppReview is a kind which stores reviews of a app, a entity == a review
type AppReview struct {
	KeyName   string    `json:"key_name" datastore:"KeyName"`
	AppID     string    `json:"app_id" datastore:"AppID"`
	Code      string    `json:"code" datastore:"Code"`
	ReviewID  string    `json:"review_id" datastore:"ReviewID"`
	Star      int       `json:"star" datastore:"Star"`
	Title     string    `json:"title" datastore:"Tite,noindex"`
	Content   string    `json:"content" datastore:"Content,noindex"`
	Version   string    `json:"version" datastore:"Version,noindex"`
	CreatedAt time.Time `json:"created_at" datastore:"CreatedAt"`
}

func (ar *AppReview) key(ctx context.Context) *datastore.Key {
	return datastore.NewKey(ctx, "AppReview", ar.KeyName, 0, nil)
}

func NotifyReviewToSlack(ctx context.Context, ar *AppReview) {
	var rn ReviewNotify
	key := datastore.NewKey(ctx, "ReviewNotify", ar.Code, 0, nil)
	err := datastore.Get(ctx, key, &rn)
	if err != nil {
		log.Infof(ctx, "%v", err)
		return
	}
	client := urlfetch.Client(ctx)
	iconURL := "https://bell-apps.appspot.com/static/icon57.png"
	text := "[" + rn.Title + "]\n" + ar.Title + ":\n" + ar.Content + "\n" + ar.Version
	var fields []map[string]interface{}
	starEmoji := ""
	for i := 0; i < ar.Star; i++ {
		starEmoji = starEmoji + ":star:"
	}
	fields = append(fields, map[string]interface{}{
		"title": "Star:",
		"value": starEmoji,
		"short": false,
	})
	fields = append(fields, map[string]interface{}{
		"title": "Meta:",
		"value": ar.Version,
		"short": false,
	})
	var attachments []map[string]interface{}
	attachments = append(attachments, map[string]interface{}{
		"fallback": text,
		"pretext":  rn.Title,
		"color":    "#8EFCD3",
		"title":    ar.Title,
		"text":     ar.Content + "\n",
		"fields":   fields,
	})
	payload := map[string]interface{}{"attachments": attachments, "username": "Bell Apps App Review Notification", "icon_url": iconURL, "mrkdwn": false}
	payloadJSON, err := json.Marshal(payload)
	if err != nil {
		log.Infof(ctx, "%v", err)
		return
	}
	b := bytes.NewBuffer(payloadJSON)
	req, _ := http.NewRequest("POST", rn.WebhookURL, b)
	req.Header.Set("Content-Type", "application/json")
	resp, _ := client.Do(req)
	defer resp.Body.Close()
	if resp.StatusCode == http.StatusNotFound {
		rn.SetUpCompleted = false
		_, err = rn.Update(ctx)
	}
}

var notifyToSlackAsync = delay.Func("put", NotifyReviewToSlack)

// Save puts to datastore
func (ar *AppReview) Create(ctx context.Context) (*AppReview, error) {
	var appreview AppReview
	ar.KeyName = ar.Code + "_" + ar.AppID + "_" + ar.ReviewID
	err := datastore.Get(ctx, ar.key(ctx), &appreview)
	if err == nil {
		log.Infof(ctx, "already registered")
		return &appreview, nil
	}
	ar.CreatedAt = time.Now()
	_, err = datastore.Put(ctx, ar.key(ctx), ar)
	if err != nil {
		return nil, err
	}
	notifyToSlackAsync.Call(ctx, ar)
	return ar, nil
}
<|endoftext|>


<commit_before>package user

import (
	"fmt"
	jwt "github.com/dgrijalva/jwt-go"
	"github.com/gin-gonic/gin"

	e "github.com/eirka/eirka-libs/errors"
)

// holds the hmac secret, is set from main
var Secret string

// checks for session cookie and handles permissions
func Auth(authenticated bool) gin.HandlerFunc {
	return func(c *gin.Context) {

		// error if theres no secret set
		if Secret == "" {
			c.JSON(e.ErrorMessage(e.ErrInternalError))
			c.Error(e.ErrNoSecret)
			c.Abort()
			return
		}

		// set default anonymous user
		user := DefaultUser()

		// parse jwt token if its there
		token, err := jwt.ParseFromRequest(c.Request, func(token *jwt.Token) (interface{}, error) {

			// check alg to make sure its hmac
			_, ok := token.Method.(*jwt.SigningMethodHMAC)
			if !ok {
				return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
			}

			// get the issuer from claims
			token_issuer, ok := token.Claims[jwt_claim_issuer].(string)
			if !ok {
				return nil, fmt.Errorf("Couldnt find issuer")
			}

			// check the issuer
			if token_issuer != jwt_issuer {
				return nil, fmt.Errorf("Incorrect issuer")
			}

			// get uid from token
			token_uid, ok := token.Claims[jwt_claim_user_id].(float64)
			if !ok {
				return nil, fmt.Errorf("Couldnt find user id")
			}

			// set the user id
			user.SetId(uint(token_uid))
			// set authenticated
			user.SetAuthenticated()

			// check that the generated user is valid
			if !user.IsValid() {
				return nil, fmt.Errorf("Generated invalid user")
			}

			// compare with secret from settings
			return []byte(Secret), nil

		})
		// if theres some jwt error other than no token in request or the token is invalid then return unauth
		if err != nil && err != jwt.ErrNoTokenInRequest || !token.Valid {
			c.JSON(e.ErrorMessage(e.ErrUnauthorized))
			c.Error(err)
			c.Abort()
			return
		}

		// check if user needed to be authenticated
		if authenticated && !user.IsAuthenticated {
			c.JSON(e.ErrorMessage(e.ErrUnauthorized))
			c.Error(e.ErrUnauthorized)
			c.Abort()
			return
		}

		// set user data
		c.Set("userdata", user)

		c.Next()

	}

}
<commit_msg>simplify jwt handler<commit_after>package user

import (
	"fmt"
	jwt "github.com/dgrijalva/jwt-go"
	"github.com/gin-gonic/gin"

	e "github.com/eirka/eirka-libs/errors"
)

// holds the hmac secret, is set from main
var Secret string

// checks for session cookie and handles permissions
func Auth(authenticated bool) gin.HandlerFunc {
	return func(c *gin.Context) {

		// error if theres no secret set
		if Secret == "" {
			c.JSON(e.ErrorMessage(e.ErrInternalError))
			c.Error(e.ErrNoSecret)
			c.Abort()
			return
		}

		// set default anonymous user
		user := DefaultUser()

		// parse jwt token if its there
		token, err := jwt.ParseFromRequest(c.Request, func(token *jwt.Token) (interface{}, error) {

			// check alg to make sure its hmac
			_, ok := token.Method.(*jwt.SigningMethodHMAC)
			if !ok {
				return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
			}

			// get the issuer from claims
			token_issuer, ok := token.Claims[jwt_claim_issuer].(string)
			if !ok {
				return nil, fmt.Errorf("Couldnt find issuer")
			}

			// check the issuer
			if token_issuer != jwt_issuer {
				return nil, fmt.Errorf("Incorrect issuer")
			}

			// get uid from token
			token_uid, ok := token.Claims[jwt_claim_user_id].(float64)
			if !ok {
				return nil, fmt.Errorf("Couldnt find user id")
			}

			// set the user id
			user.SetId(uint(token_uid))
			// set authenticated
			user.SetAuthenticated()

			// check that the generated user is valid
			if !user.IsValid() {
				return nil, fmt.Errorf("Generated invalid user")
			}

			// compare with secret from settings
			return []byte(Secret), nil

		})
		// if theres some jwt error other than no token in request or the token is invalid then return unauth
		if err != nil && err != jwt.ErrNoTokenInRequest {
			c.JSON(e.ErrorMessage(e.ErrUnauthorized))
			c.Error(err)
			c.Abort()
			return
		}

		// check if user needed to be authenticated
		if authenticated && !user.IsAuthenticated || !token.Valid {
			c.JSON(e.ErrorMessage(e.ErrUnauthorized))
			c.Error(e.ErrUnauthorized)
			c.Abort()
			return
		}

		// set user data
		c.Set("userdata", user)

		c.Next()

	}

}
<|endoftext|>


<commit_before>package hey

import (
	"errors"
	"time"
)

var (
	ErrMsgPackConflictFields = errors.New("msgpack serializator: conflict num fields or another error")
	ErrNotRegDataType        = errors.New("not registred data type")
)

// Thread

type Thread struct {
	ThreadID string
	DataType DataType
	Data     interface{}
}

// Events

type Event struct {
	EventID   string
	ThreadID  string
	DataType  DataType
	Data      interface{}
	CreatedAt time.Time
	UpdatedAt time.Time
}

// Threadline - таблица для хранения индекса событий каждой подписки
// выборка происходит в обратном хронологическом порядке по ThreadID
type Threadline struct {
	EventID   string
	ThreadID  string
	UpdatedAt time.Time
}

// Observer это таблица для хранения подписок на трэды.
// используется при создании нового события в трэде,
// для всех подписчиков этого трэда
// создаётся запись в таблице Threadline
type Observer struct {
	UserID   string
	ThreadID string
	//ParentThreadID          string
	//RelatedThreadlineExists bool
	LastTimeStamp time.Time // unix time stamp
	DataType      DataType
	Data          interface{}
}

// User подписчик, обозреватель,
type User struct {
	UserID   string
	DataType DataType
	Data     interface{}
}
<commit_msg>add sources table<commit_after>package hey

import (
	"errors"
	"time"
)

var (
	ErrMsgPackConflictFields = errors.New("msgpack serializator: conflict num fields or another error")
	ErrNotRegDataType        = errors.New("not registred data type")
)

// Thread

type Thread struct {
	ThreadID string
	DataType DataType
	Data     interface{}
}

// Events

type Event struct {
	EventID   string
	ThreadID  string
	DataType  DataType
	Data      interface{}
	CreatedAt time.Time
	UpdatedAt time.Time
}

// Threadline - таблица для хранения индекса событий каждой подписки
// выборка происходит в обратном хронологическом порядке по ThreadID
type Threadline struct {
	EventID   string
	ThreadID  string
	CreatedAt time.Time
}

// Observer это таблица для хранения подписок на трэды.
// Для выбора пописок нужен итератор по UserID.
// Для выбора подписчиков нужен итератор по ThreadID
type Observer struct {
	UserID string
	// ID трэда, на который подписан юзер
	ThreadID          string
	LastDeliveredTime time.Time
}

// Sources таблица источников каджого трэда.
// используется при создании нового события в трэде,
// для всех трэдов-подписчиков (TargetThreadID) этого трэда SourceThreadID
// создаётся запись в таблице Threadline.
type Sources struct {
	TargetThreadID string
	SourceThreadID string
}

// User подписчик, обозреватель,
type User struct {
	UserID   string
	DataType DataType
	Data     interface{}
}
<|endoftext|>


<commit_before>package user

import (
	jwt "github.com/dgrijalva/jwt-go"
	"golang.org/x/crypto/bcrypt"
	"strings"
	"time"

	"github.com/eirka/eirka-libs/db"
	e "github.com/eirka/eirka-libs/errors"
)

const (
	// jwt claim keys
	jwt_claim_issuer    = "iss"
	jwt_claim_issued    = "iat"
	jwt_claim_notbefore = "nbf"
	jwt_claim_expire    = "exp"
	jwt_claim_user_id   = "user_id"
	// jwt issuer
	jwt_issuer = "pram"
	// jwt expire days
	jwt_expire_days = 90

	// the username validation regex
	username = `^([a-zA-Z0-9]+[\s_-]?)+$`
)

var regexUsername = regexp.MustCompile(username)

// reserved name list
var reservedNameList = map[string]bool{
	"admin":          true,
	"administrator":  true,
	"administration": true,
	"support":        true,
	"mod":            true,
	"moderator":      true,
	"anon":           true,
	"anonymous":      true,
	"root":           true,
	"webmaster":      true,
	"username":       true,
	"user":           true,
}

// user struct
type User struct {
	Id              uint
	Name            string
	IsAuthenticated bool
	hash            []byte
}

// create a user struct
func DefaultUser() User {
	return User{
		Id:              1,
		IsAuthenticated: false,
	}
}

// check user struct validity
func (u *User) IsValid() bool {

	// this isnt a real user id
	if u.Id == 0 {
		return false
	}

	// the anon account can never be authenticated
	if u.Id == 1 && u.IsAuthenticated {
		return false
	}

	// a user can never be unauthenticated
	if u.Id != 1 && !u.IsAuthenticated {
		return false
	}

	return true
}

// sets the user id
func (u *User) SetId(uid uint) {
	u.Id = uid
	return
}

// sets authenticated
func (u *User) SetAuthenticated() {
	u.IsAuthenticated = true
	return
}

// checks if the name is valid
func IsValidName(name string) bool {

	if reservedNameList[strings.ToLower(name)] {
		return false
	}

	return regexUsername.MatchString(name)
}

func (u *User) FromName(name string) (err error) {

	// password length cant be 0
	if len(name) == 0 {
		return e.ErrUserNotValid
	}

	// Get Database handle
	dbase, err := db.GetDb()
	if err != nil {
		return
	}

	// get hashed password from database
	err = dbase.QueryRow("select user_id, user_password from users where user_name = ?", name).Scan(&u.Id, &u.hash)
	if err != nil {
		return
	}

	u.SetAuthenticated()

	if !u.IsValid() {
		return e.ErrUserNotValid
	}

	return

}

// check for duplicate name before registering
func CheckDuplicate(name string) (check bool) {

	// password length cant be 0
	if len(name) == 0 {
		return true
	}

	// Get Database handle
	dbase, err := db.GetDb()
	if err != nil {
		return true
	}

	// this will return true if there is a user
	err = dbase.QueryRow("select count(*) from users where user_name = ?", name).Scan(&check)
	if err != nil {
		return true
	}

	return

}

// Creates a JWT token with our claims
func (u *User) CreateToken() (newtoken string, err error) {

	if !u.IsValid() {
		err = e.ErrUserNotValid
		return
	}

	// error if theres no secret set
	if Secret == "" {
		err = e.ErrNoSecret
		return
	}

	// Create the token
	token := jwt.New(jwt.SigningMethodHS256)
	// the current time
	now := time.Now()

	// Set our claims
	token.Claims[jwt_claim_issuer] = jwt_issuer
	token.Claims[jwt_claim_issued] = now.Unix()
	token.Claims[jwt_claim_notbefore] = now.Unix()
	token.Claims[jwt_claim_expire] = now.Add(time.Hour * 24 * jwt_expire_days).Unix()
	token.Claims[jwt_claim_user_id] = u.Id

	// Sign and get the complete encoded token as a string
	newtoken, err = token.SignedString([]byte(Secret))
	if err != nil {
		return
	}

	return

}

// get the user info from id
func (u *User) IsAuthorized(ib uint) bool {

	var err error

	if !u.IsValid() {
		return false
	}

	// check for invalid stuff
	if ib == 0 {
		return false
	}

	// Get Database handle
	dbase, err := db.GetDb()
	if err != nil {
		return false
	}

	// holds our role
	var role uint

	// get data from users table
	err = dbase.QueryRow(`SELECT COALESCE((SELECT MAX(role_id) FROM user_ib_role_map WHERE user_ib_role_map.user_id = users.user_id AND ib_id = ?),user_role_map.role_id) as role
    FROM users
    INNER JOIN user_role_map ON (user_role_map.user_id = users.user_id)
    WHERE users.user_id = ?`, ib, u.Id).Scan(&role)
	if err != nil {
		return false
	}

	switch role {
	case 3:
		return true
	case 4:
		return true
	default:
		return false
	}

	return false

}

// compare password to
func (u *User) ComparePassword(password string) bool {

	// password length cant be 0
	if len(password) == 0 {
		return false
	}

	// if the hash wasnt populated
	if len(u.hash) == 0 {
		return false
	}

	// compare the stored hash with the provided password
	err := bcrypt.CompareHashAndPassword(u.hash, []byte(password))

	return err == nil

}

// hash a given password
func HashPassword(password string) (hash []byte, err error) {

	// password length cant be 0
	if len(password) == 0 {
		err = e.ErrInvalidPassword
		return
	}

	return bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
}
<commit_msg>simplify jwt handler<commit_after>package user

import (
	jwt "github.com/dgrijalva/jwt-go"
	"golang.org/x/crypto/bcrypt"
	"regexp"
	"strings"
	"time"

	"github.com/eirka/eirka-libs/db"
	e "github.com/eirka/eirka-libs/errors"
)

const (
	// jwt claim keys
	jwt_claim_issuer    = "iss"
	jwt_claim_issued    = "iat"
	jwt_claim_notbefore = "nbf"
	jwt_claim_expire    = "exp"
	jwt_claim_user_id   = "user_id"
	// jwt issuer
	jwt_issuer = "pram"
	// jwt expire days
	jwt_expire_days = 90

	// the username validation regex
	username = `^([a-zA-Z0-9]+[\s_-]?)+$`
)

var regexUsername = regexp.MustCompile(username)

// reserved name list
var reservedNameList = map[string]bool{
	"admin":          true,
	"administrator":  true,
	"administration": true,
	"support":        true,
	"mod":            true,
	"moderator":      true,
	"anon":           true,
	"anonymous":      true,
	"root":           true,
	"webmaster":      true,
	"username":       true,
	"user":           true,
}

// user struct
type User struct {
	Id              uint
	Name            string
	IsAuthenticated bool
	hash            []byte
}

// create a user struct
func DefaultUser() User {
	return User{
		Id:              1,
		IsAuthenticated: false,
	}
}

// check user struct validity
func (u *User) IsValid() bool {

	// this isnt a real user id
	if u.Id == 0 {
		return false
	}

	// the anon account can never be authenticated
	if u.Id == 1 && u.IsAuthenticated {
		return false
	}

	// a user can never be unauthenticated
	if u.Id != 1 && !u.IsAuthenticated {
		return false
	}

	return true
}

// sets the user id
func (u *User) SetId(uid uint) {
	u.Id = uid
	return
}

// sets authenticated
func (u *User) SetAuthenticated() {
	u.IsAuthenticated = true
	return
}

// checks if the name is valid
func IsValidName(name string) bool {

	if reservedNameList[strings.ToLower(name)] {
		return false
	}

	return regexUsername.MatchString(name)
}

func (u *User) FromName(name string) (err error) {

	// password length cant be 0
	if len(name) == 0 {
		return e.ErrUserNotValid
	}

	// Get Database handle
	dbase, err := db.GetDb()
	if err != nil {
		return
	}

	// get hashed password from database
	err = dbase.QueryRow("select user_id, user_password from users where user_name = ?", name).Scan(&u.Id, &u.hash)
	if err != nil {
		return
	}

	u.SetAuthenticated()

	if !u.IsValid() {
		return e.ErrUserNotValid
	}

	return

}

// check for duplicate name before registering
func CheckDuplicate(name string) (check bool) {

	// password length cant be 0
	if len(name) == 0 {
		return true
	}

	// Get Database handle
	dbase, err := db.GetDb()
	if err != nil {
		return true
	}

	// this will return true if there is a user
	err = dbase.QueryRow("select count(*) from users where user_name = ?", name).Scan(&check)
	if err != nil {
		return true
	}

	return

}

// Creates a JWT token with our claims
func (u *User) CreateToken() (newtoken string, err error) {

	if !u.IsValid() {
		err = e.ErrUserNotValid
		return
	}

	// error if theres no secret set
	if Secret == "" {
		err = e.ErrNoSecret
		return
	}

	// Create the token
	token := jwt.New(jwt.SigningMethodHS256)
	// the current time
	now := time.Now()

	// Set our claims
	token.Claims[jwt_claim_issuer] = jwt_issuer
	token.Claims[jwt_claim_issued] = now.Unix()
	token.Claims[jwt_claim_notbefore] = now.Unix()
	token.Claims[jwt_claim_expire] = now.Add(time.Hour * 24 * jwt_expire_days).Unix()
	token.Claims[jwt_claim_user_id] = u.Id

	// Sign and get the complete encoded token as a string
	newtoken, err = token.SignedString([]byte(Secret))
	if err != nil {
		return
	}

	return

}

// get the user info from id
func (u *User) IsAuthorized(ib uint) bool {

	var err error

	if !u.IsValid() {
		return false
	}

	// check for invalid stuff
	if ib == 0 {
		return false
	}

	// Get Database handle
	dbase, err := db.GetDb()
	if err != nil {
		return false
	}

	// holds our role
	var role uint

	// get data from users table
	err = dbase.QueryRow(`SELECT COALESCE((SELECT MAX(role_id) FROM user_ib_role_map WHERE user_ib_role_map.user_id = users.user_id AND ib_id = ?),user_role_map.role_id) as role
    FROM users
    INNER JOIN user_role_map ON (user_role_map.user_id = users.user_id)
    WHERE users.user_id = ?`, ib, u.Id).Scan(&role)
	if err != nil {
		return false
	}

	switch role {
	case 3:
		return true
	case 4:
		return true
	default:
		return false
	}

	return false

}

// compare password to
func (u *User) ComparePassword(password string) bool {

	// password length cant be 0
	if len(password) == 0 {
		return false
	}

	// if the hash wasnt populated
	if len(u.hash) == 0 {
		return false
	}

	// compare the stored hash with the provided password
	err := bcrypt.CompareHashAndPassword(u.hash, []byte(password))

	return err == nil

}

// hash a given password
func HashPassword(password string) (hash []byte, err error) {

	// password length cant be 0
	if len(password) == 0 {
		err = e.ErrInvalidPassword
		return
	}

	return bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
}
<|endoftext|>


<commit_before>package user

type User struct {
	Id            int64
	Name          int64
	EmailAddress  string
	Password      string
	Administrator bool
}
<commit_msg>Defined the database tags for the user struct<commit_after>package user

type User struct {
	Id            int64  `db:"id"`
	Name          string `db:"name"`
	EmailAddress  string `db:"email_address"`
	Password      string `db:"password"`
	Administrator bool   `db:"is_administrator"`
}
<|endoftext|>


<commit_before>package main

import (
	"context"
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"net/http"
	"os"
	"strings"
	"time"
)

const (
	defaultStoreTimeout = 5 * time.Second
)

// Team model struct
type Team struct {
	ID          string `json:"id"`
	DisplayName string `json:"displayName"`
}

// Period model struct
type Period struct {
	ID          string   `json:"id"`
	DisplayName string   `json:"displayName"`
	Unit        string   `json:"unit"`
	Buckets     []Bucket `json:"buckets"`
	People      []Person `json:"people"`
}

// Bucket model struct
type Bucket struct {
	DisplayName          string      `json:"displayName"`
	AllocationPercentage float64     `json:"allocationPercentage"`
	Objectives           []Objective `json:"objectives"`
}

// Objective model struct
type Objective struct {
	Name             string       `json:"name"`
	ResourceEstimate float64      `json:"resourceEstimate"`
	Assignments      []Assignment `json:"assignments"`
}

// Assignment model struct
type Assignment struct {
	PersonID   string  `json:"personId"`
	Commitment float64 `json:"commitment"`
}

// Person model struct
type Person struct {
	ID           string  `json:"id"`
	DisplayName  string  `json:"displayName"`
	Availability float64 `json:"availability"`
}

// StorageService to represent the persistent store
type StorageService interface {
	GetAllTeams(ctx context.Context) ([]Team, error)
	GetTeam(ctx context.Context, teamID string) (Team, bool, error)
	CreateTeam(ctx context.Context, team Team) error
	UpdateTeam(ctx context.Context, team Team) error
	GetAllPeriods(ctx context.Context, teamID string) ([]Period, bool, error)
	GetPeriod(ctx context.Context, teamID, periodID string) (Period, bool, error)
	CreatePeriod(ctx context.Context, teamID string, period Period) error
	UpdatePeriod(ctx context.Context, teamID string, period Period) error
	Close() error
}

// Server struct to handle incoming HTTP requests
type Server struct {
	store        StorageService
	storeTimeout time.Duration
}

func (s *Server) makeHandler() http.Handler {
	mux := http.NewServeMux()
	mux.HandleFunc("/api/team/", s.handleTeam)
	mux.HandleFunc("/api/period/", s.handlePeriod)
	return mux
}

func (s *Server) ensureTeamExistence(w http.ResponseWriter, r *http.Request, teamID string, expected bool) bool {
	ctx, cancel := context.WithTimeout(r.Context(), s.storeTimeout)
	defer cancel()
	_, exists, err := s.store.GetTeam(ctx, teamID)
	if err != nil {
		log.Printf("Could not validate existence of team '%s': error: %s", teamID, err)
		http.Error(w, fmt.Sprintf("Could not validate existence of team '%s' (see server log)", teamID), http.StatusInternalServerError)
		return false
	}
	if exists != expected {
		statusCode := http.StatusBadRequest
		if expected {
			statusCode = http.StatusNotFound
		}
		http.Error(w, fmt.Sprintf("Team with ID '%s' expected exists=%v, found %v", teamID, expected, exists), statusCode)
		return false
	}
	return true
}

func (s *Server) ensurePeriodExistence(w http.ResponseWriter, r *http.Request, teamID, periodID string, expected bool) bool {
	ctx, cancel := context.WithTimeout(r.Context(), s.storeTimeout)
	defer cancel()
	_, exists, err := s.store.GetPeriod(ctx, teamID, periodID)
	if err != nil {
		log.Printf("Could not validate existence of period '%s' for team '%s': error: %s", periodID, teamID, err)
		http.Error(w, fmt.Sprintf("Could not validate existence of period '%s' for team '%s' (see server log)", periodID, teamID), http.StatusInternalServerError)
		return false
	}
	if exists != expected {
		statusCode := http.StatusBadRequest
		if expected {
			statusCode = http.StatusNotFound
		}
		http.Error(w, fmt.Sprintf("Period '%s' for team '%s' expected exists=%v, found %v", periodID, teamID, expected, exists), statusCode)
		return false
	}
	return true
}

func (s *Server) handleTeam(w http.ResponseWriter, r *http.Request) {
	pathParts := strings.Split(r.URL.Path, "/")
	log.Printf("Team path: %q", pathParts)
	if len(pathParts) != 4 {
		http.NotFound(w, r)
		return
	}
	teamID := pathParts[3]
	if r.Method == http.MethodGet {
		s.handleGetTeam(w, r, teamID)
	} else if r.Method == http.MethodPost {
		s.handlePostTeam(w, r)
	} else if r.Method == http.MethodPut {
		s.handlePutTeam(w, r)
	} else {
		http.Error(w, fmt.Sprintf("Unsupported method '%s'", r.Method), http.StatusBadRequest)
	}
}

func (s *Server) handleGetTeam(w http.ResponseWriter, r *http.Request, teamID string) {
	ctx, cancel := context.WithTimeout(r.Context(), s.storeTimeout)
	defer cancel()
	if teamID == "" {
		teams, err := s.store.GetAllTeams(ctx)
		if err != nil {
			log.Printf("Could not retrieve teams: error: %s", err)
			http.Error(w, "Could not retrieve teams (see server log)", http.StatusInternalServerError)
			return
		}
		enc := json.NewEncoder(w)
		w.Header().Set("Content-Type", "application/json")
		enc.Encode(teams)
	} else {
		team, found, err := s.store.GetTeam(ctx, teamID)
		if err != nil {
			log.Printf("Could not retrieve team '%s': error: %s", teamID, err)
			http.Error(w, fmt.Sprintf("Could not retrieve team '%s' (see server log)", teamID), http.StatusInternalServerError)
			return
		}
		if !found {
			http.NotFound(w, r)
			return
		}
		enc := json.NewEncoder(w)
		w.Header().Set("Content-Type", "application/json")
		enc.Encode(team)
	}
}

func (s *Server) handlePostTeam(w http.ResponseWriter, r *http.Request) {
	team, ok := readTeamFromBody(w, r)
	if !ok {
		return
	}
	if !s.ensureTeamExistence(w, r, team.ID, false) {
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), s.storeTimeout)
	defer cancel()
	err := s.store.CreateTeam(ctx, team)
	if err != nil {
		log.Printf("Could not create team: error: %s", err)
		http.Error(w, "Could not create team (see server log)", http.StatusInternalServerError)
		return
	}
}

func (s *Server) handlePutTeam(w http.ResponseWriter, r *http.Request) {
	team, ok := readTeamFromBody(w, r)
	if !ok {
		return
	}
	if !s.ensureTeamExistence(w, r, team.ID, true) {
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), s.storeTimeout)
	defer cancel()
	err := s.store.UpdateTeam(ctx, team)
	if err != nil {
		log.Printf("Could not update team: error: %s", err)
		http.Error(w, "Could not update team (see server log)", http.StatusInternalServerError)
		return
	}
}

func readTeamFromBody(w http.ResponseWriter, r *http.Request) (Team, bool) {
	dec := json.NewDecoder(r.Body)
	team := Team{}
	err := dec.Decode(&team)
	if err != nil {
		http.Error(w, fmt.Sprintf("Could not decode body: %v", err), http.StatusBadRequest)
		return team, false
	}
	return team, true
}

func (s *Server) handlePeriod(w http.ResponseWriter, r *http.Request) {
	pathParts := strings.Split(r.URL.Path, "/")
	log.Printf("Period path: %q", pathParts)
	if len(pathParts) != 5 {
		http.NotFound(w, r)
		return
	}
	teamID := pathParts[3]
	periodID := pathParts[4]
	if r.Method == http.MethodGet {
		s.handleGetPeriod(w, r, teamID, periodID)
	} else if r.Method == http.MethodPost {
		s.handlePostPeriod(w, r, teamID)
	} else if r.Method == http.MethodPut {
		s.handlePutPeriod(w, r, teamID, periodID)
	} else {
		http.Error(w, fmt.Sprintf("Unsupported method '%s'", r.Method), http.StatusBadRequest)
	}
}

func (s *Server) handleGetPeriod(w http.ResponseWriter, r *http.Request, teamID, periodID string) {
	ctx, cancel := context.WithTimeout(r.Context(), s.storeTimeout)
	defer cancel()
	if periodID == "" {
		periods, found, err := s.store.GetAllPeriods(ctx, teamID)
		if err != nil {
			log.Printf("Could not retrieve periods for team '%s': error: %s", teamID, err)
			http.Error(w, fmt.Sprintf("Could not retrieve periods for team '%s' (see server log)", teamID), http.StatusInternalServerError)
			return
		}
		if !found {
			http.NotFound(w, r)
			return
		}
		enc := json.NewEncoder(w)
		w.Header().Set("Content-Type", "application/json")
		enc.Encode(periods)
	} else {
		period, found, err := s.store.GetPeriod(ctx, teamID, periodID)
		if err != nil {
			log.Printf("Could not retrieve period '%s' for team '%s': error: %s", periodID, teamID, err)
			http.Error(w, fmt.Sprintf("Could not retrieve period '%s' for team '%s' (see server log)", periodID, teamID), http.StatusInternalServerError)
			return
		}
		if !found {
			http.NotFound(w, r)
			return
		}
		enc := json.NewEncoder(w)
		w.Header().Set("Content-Type", "application/json")
		enc.Encode(period)
	}
}

func (s *Server) handlePostPeriod(w http.ResponseWriter, r *http.Request, teamID string) {
	period, ok := readPeriodFromBody(w, r)
	if !ok {
		return
	}
	if !s.ensureTeamExistence(w, r, teamID, true) {
		return
	}
	if !s.ensurePeriodExistence(w, r, teamID, period.ID, false) {
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), s.storeTimeout)
	defer cancel()
	err := s.store.CreatePeriod(ctx, teamID, period)
	if err != nil {
		log.Printf("Could not create period for team '%s': error: %s", teamID, err)
		http.Error(w, fmt.Sprintf("Could not create period for team '%s' (see server log)", teamID), http.StatusInternalServerError)
		return
	}
}

func (s *Server) handlePutPeriod(w http.ResponseWriter, r *http.Request, teamID, periodID string) {
	period, ok := readPeriodFromBody(w, r)
	if !ok {
		return
	}
	if !s.ensureTeamExistence(w, r, teamID, true) {
		return
	}
	if !s.ensurePeriodExistence(w, r, teamID, periodID, true) {
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), s.storeTimeout)
	defer cancel()
	err := s.store.UpdatePeriod(ctx, teamID, period)
	if err != nil {
		log.Printf("Could not update period '%s' for team '%s': error: %s", periodID, teamID, err)
		http.Error(w, fmt.Sprintf("Could not update period '%s' for team '%s' (see server log)", periodID, teamID), http.StatusInternalServerError)
		return
	}
}

func readPeriodFromBody(w http.ResponseWriter, r *http.Request) (Period, bool) {
	dec := json.NewDecoder(r.Body)
	period := Period{}
	err := dec.Decode(&period)
	if err != nil {
		http.Error(w, fmt.Sprintf("Could not decode body: %v", err), http.StatusBadRequest)
		return period, false
	}
	return period, true
}

func main() {
	var useInMemStore bool
	flag.BoolVar(&useInMemStore, "inmemstore", false, "Use in-memory datastore")
	flag.Parse()

	var store StorageService
	if useInMemStore {
		log.Printf("Using in-memory store per command-line flag")
		store = makeInMemStore()
	} else {
		gcloudProject := os.Getenv("GOOGLE_CLOUD_PROJECT")
		if gcloudProject == "" {
			log.Fatalf("GOOGLE_CLOUD_PROJECT not set")
			return
		}
		log.Printf("Using Cloud Datastore storage service; project='%s'", gcloudProject)
		log.Printf("To use the local emulator, see https://cloud.google.com/datastore/docs/tools/datastore-emulator")
		ctx := context.Background()
		var err error
		store, err = makeGoogleCDSStore(ctx, gcloudProject)
		if err != nil {
			log.Fatalf("Could not instantiate datastore: %s", err)
			return
		}
	}
	server := Server{store: store, storeTimeout: defaultStoreTimeout}
	handler := server.makeHandler()
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
		log.Printf("Defaulting to port %s", port)
	}
	log.Printf("Listening on port %s", port)
	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), handler))
}
<commit_msg>Reduce logging<commit_after>package main

import (
	"context"
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"net/http"
	"os"
	"strings"
	"time"
)

const (
	defaultStoreTimeout = 5 * time.Second
)

// Team model struct
type Team struct {
	ID          string `json:"id"`
	DisplayName string `json:"displayName"`
}

// Period model struct
type Period struct {
	ID          string   `json:"id"`
	DisplayName string   `json:"displayName"`
	Unit        string   `json:"unit"`
	Buckets     []Bucket `json:"buckets"`
	People      []Person `json:"people"`
}

// Bucket model struct
type Bucket struct {
	DisplayName          string      `json:"displayName"`
	AllocationPercentage float64     `json:"allocationPercentage"`
	Objectives           []Objective `json:"objectives"`
}

// Objective model struct
type Objective struct {
	Name             string       `json:"name"`
	ResourceEstimate float64      `json:"resourceEstimate"`
	Assignments      []Assignment `json:"assignments"`
}

// Assignment model struct
type Assignment struct {
	PersonID   string  `json:"personId"`
	Commitment float64 `json:"commitment"`
}

// Person model struct
type Person struct {
	ID           string  `json:"id"`
	DisplayName  string  `json:"displayName"`
	Availability float64 `json:"availability"`
}

// StorageService to represent the persistent store
type StorageService interface {
	GetAllTeams(ctx context.Context) ([]Team, error)
	GetTeam(ctx context.Context, teamID string) (Team, bool, error)
	CreateTeam(ctx context.Context, team Team) error
	UpdateTeam(ctx context.Context, team Team) error
	GetAllPeriods(ctx context.Context, teamID string) ([]Period, bool, error)
	GetPeriod(ctx context.Context, teamID, periodID string) (Period, bool, error)
	CreatePeriod(ctx context.Context, teamID string, period Period) error
	UpdatePeriod(ctx context.Context, teamID string, period Period) error
	Close() error
}

// Server struct to handle incoming HTTP requests
type Server struct {
	store        StorageService
	storeTimeout time.Duration
}

func (s *Server) makeHandler() http.Handler {
	mux := http.NewServeMux()
	mux.HandleFunc("/api/team/", s.handleTeam)
	mux.HandleFunc("/api/period/", s.handlePeriod)
	return mux
}

func (s *Server) ensureTeamExistence(w http.ResponseWriter, r *http.Request, teamID string, expected bool) bool {
	ctx, cancel := context.WithTimeout(r.Context(), s.storeTimeout)
	defer cancel()
	_, exists, err := s.store.GetTeam(ctx, teamID)
	if err != nil {
		log.Printf("Could not validate existence of team '%s': error: %s", teamID, err)
		http.Error(w, fmt.Sprintf("Could not validate existence of team '%s' (see server log)", teamID), http.StatusInternalServerError)
		return false
	}
	if exists != expected {
		statusCode := http.StatusBadRequest
		if expected {
			statusCode = http.StatusNotFound
		}
		http.Error(w, fmt.Sprintf("Team with ID '%s' expected exists=%v, found %v", teamID, expected, exists), statusCode)
		return false
	}
	return true
}

func (s *Server) ensurePeriodExistence(w http.ResponseWriter, r *http.Request, teamID, periodID string, expected bool) bool {
	ctx, cancel := context.WithTimeout(r.Context(), s.storeTimeout)
	defer cancel()
	_, exists, err := s.store.GetPeriod(ctx, teamID, periodID)
	if err != nil {
		log.Printf("Could not validate existence of period '%s' for team '%s': error: %s", periodID, teamID, err)
		http.Error(w, fmt.Sprintf("Could not validate existence of period '%s' for team '%s' (see server log)", periodID, teamID), http.StatusInternalServerError)
		return false
	}
	if exists != expected {
		statusCode := http.StatusBadRequest
		if expected {
			statusCode = http.StatusNotFound
		}
		http.Error(w, fmt.Sprintf("Period '%s' for team '%s' expected exists=%v, found %v", periodID, teamID, expected, exists), statusCode)
		return false
	}
	return true
}

func (s *Server) handleTeam(w http.ResponseWriter, r *http.Request) {
	pathParts := strings.Split(r.URL.Path, "/")
	if len(pathParts) != 4 {
		http.NotFound(w, r)
		return
	}
	teamID := pathParts[3]
	if r.Method == http.MethodGet {
		s.handleGetTeam(w, r, teamID)
	} else if r.Method == http.MethodPost {
		s.handlePostTeam(w, r)
	} else if r.Method == http.MethodPut {
		s.handlePutTeam(w, r)
	} else {
		http.Error(w, fmt.Sprintf("Unsupported method '%s'", r.Method), http.StatusBadRequest)
	}
}

func (s *Server) handleGetTeam(w http.ResponseWriter, r *http.Request, teamID string) {
	ctx, cancel := context.WithTimeout(r.Context(), s.storeTimeout)
	defer cancel()
	if teamID == "" {
		teams, err := s.store.GetAllTeams(ctx)
		if err != nil {
			log.Printf("Could not retrieve teams: error: %s", err)
			http.Error(w, "Could not retrieve teams (see server log)", http.StatusInternalServerError)
			return
		}
		enc := json.NewEncoder(w)
		w.Header().Set("Content-Type", "application/json")
		enc.Encode(teams)
	} else {
		team, found, err := s.store.GetTeam(ctx, teamID)
		if err != nil {
			log.Printf("Could not retrieve team '%s': error: %s", teamID, err)
			http.Error(w, fmt.Sprintf("Could not retrieve team '%s' (see server log)", teamID), http.StatusInternalServerError)
			return
		}
		if !found {
			http.NotFound(w, r)
			return
		}
		enc := json.NewEncoder(w)
		w.Header().Set("Content-Type", "application/json")
		enc.Encode(team)
	}
}

func (s *Server) handlePostTeam(w http.ResponseWriter, r *http.Request) {
	team, ok := readTeamFromBody(w, r)
	if !ok {
		return
	}
	if !s.ensureTeamExistence(w, r, team.ID, false) {
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), s.storeTimeout)
	defer cancel()
	err := s.store.CreateTeam(ctx, team)
	if err != nil {
		log.Printf("Could not create team: error: %s", err)
		http.Error(w, "Could not create team (see server log)", http.StatusInternalServerError)
		return
	}
}

func (s *Server) handlePutTeam(w http.ResponseWriter, r *http.Request) {
	team, ok := readTeamFromBody(w, r)
	if !ok {
		return
	}
	if !s.ensureTeamExistence(w, r, team.ID, true) {
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), s.storeTimeout)
	defer cancel()
	err := s.store.UpdateTeam(ctx, team)
	if err != nil {
		log.Printf("Could not update team: error: %s", err)
		http.Error(w, "Could not update team (see server log)", http.StatusInternalServerError)
		return
	}
}

func readTeamFromBody(w http.ResponseWriter, r *http.Request) (Team, bool) {
	dec := json.NewDecoder(r.Body)
	team := Team{}
	err := dec.Decode(&team)
	if err != nil {
		http.Error(w, fmt.Sprintf("Could not decode body: %v", err), http.StatusBadRequest)
		return team, false
	}
	return team, true
}

func (s *Server) handlePeriod(w http.ResponseWriter, r *http.Request) {
	pathParts := strings.Split(r.URL.Path, "/")
	if len(pathParts) != 5 {
		http.NotFound(w, r)
		return
	}
	teamID := pathParts[3]
	periodID := pathParts[4]
	if r.Method == http.MethodGet {
		s.handleGetPeriod(w, r, teamID, periodID)
	} else if r.Method == http.MethodPost {
		s.handlePostPeriod(w, r, teamID)
	} else if r.Method == http.MethodPut {
		s.handlePutPeriod(w, r, teamID, periodID)
	} else {
		http.Error(w, fmt.Sprintf("Unsupported method '%s'", r.Method), http.StatusBadRequest)
	}
}

func (s *Server) handleGetPeriod(w http.ResponseWriter, r *http.Request, teamID, periodID string) {
	ctx, cancel := context.WithTimeout(r.Context(), s.storeTimeout)
	defer cancel()
	if periodID == "" {
		periods, found, err := s.store.GetAllPeriods(ctx, teamID)
		if err != nil {
			log.Printf("Could not retrieve periods for team '%s': error: %s", teamID, err)
			http.Error(w, fmt.Sprintf("Could not retrieve periods for team '%s' (see server log)", teamID), http.StatusInternalServerError)
			return
		}
		if !found {
			http.NotFound(w, r)
			return
		}
		enc := json.NewEncoder(w)
		w.Header().Set("Content-Type", "application/json")
		enc.Encode(periods)
	} else {
		period, found, err := s.store.GetPeriod(ctx, teamID, periodID)
		if err != nil {
			log.Printf("Could not retrieve period '%s' for team '%s': error: %s", periodID, teamID, err)
			http.Error(w, fmt.Sprintf("Could not retrieve period '%s' for team '%s' (see server log)", periodID, teamID), http.StatusInternalServerError)
			return
		}
		if !found {
			http.NotFound(w, r)
			return
		}
		enc := json.NewEncoder(w)
		w.Header().Set("Content-Type", "application/json")
		enc.Encode(period)
	}
}

func (s *Server) handlePostPeriod(w http.ResponseWriter, r *http.Request, teamID string) {
	period, ok := readPeriodFromBody(w, r)
	if !ok {
		return
	}
	if !s.ensureTeamExistence(w, r, teamID, true) {
		return
	}
	if !s.ensurePeriodExistence(w, r, teamID, period.ID, false) {
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), s.storeTimeout)
	defer cancel()
	err := s.store.CreatePeriod(ctx, teamID, period)
	if err != nil {
		log.Printf("Could not create period for team '%s': error: %s", teamID, err)
		http.Error(w, fmt.Sprintf("Could not create period for team '%s' (see server log)", teamID), http.StatusInternalServerError)
		return
	}
}

func (s *Server) handlePutPeriod(w http.ResponseWriter, r *http.Request, teamID, periodID string) {
	period, ok := readPeriodFromBody(w, r)
	if !ok {
		return
	}
	if !s.ensureTeamExistence(w, r, teamID, true) {
		return
	}
	if !s.ensurePeriodExistence(w, r, teamID, periodID, true) {
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), s.storeTimeout)
	defer cancel()
	err := s.store.UpdatePeriod(ctx, teamID, period)
	if err != nil {
		log.Printf("Could not update period '%s' for team '%s': error: %s", periodID, teamID, err)
		http.Error(w, fmt.Sprintf("Could not update period '%s' for team '%s' (see server log)", periodID, teamID), http.StatusInternalServerError)
		return
	}
}

func readPeriodFromBody(w http.ResponseWriter, r *http.Request) (Period, bool) {
	dec := json.NewDecoder(r.Body)
	period := Period{}
	err := dec.Decode(&period)
	if err != nil {
		http.Error(w, fmt.Sprintf("Could not decode body: %v", err), http.StatusBadRequest)
		return period, false
	}
	return period, true
}

func main() {
	var useInMemStore bool
	flag.BoolVar(&useInMemStore, "inmemstore", false, "Use in-memory datastore")
	flag.Parse()

	var store StorageService
	if useInMemStore {
		log.Printf("Using in-memory store per command-line flag")
		store = makeInMemStore()
	} else {
		gcloudProject := os.Getenv("GOOGLE_CLOUD_PROJECT")
		if gcloudProject == "" {
			log.Fatalf("GOOGLE_CLOUD_PROJECT not set")
			return
		}
		log.Printf("Using Cloud Datastore storage service; project='%s'", gcloudProject)
		log.Printf("To use the local emulator, see https://cloud.google.com/datastore/docs/tools/datastore-emulator")
		ctx := context.Background()
		var err error
		store, err = makeGoogleCDSStore(ctx, gcloudProject)
		if err != nil {
			log.Fatalf("Could not instantiate datastore: %s", err)
			return
		}
	}
	server := Server{store: store, storeTimeout: defaultStoreTimeout}
	handler := server.makeHandler()
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
		log.Printf("Defaulting to port %s", port)
	}
	log.Printf("Listening on port %s", port)
	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), handler))
}
<|endoftext|>


<commit_before>package main

import (
	"context"
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"net/http"
	"os"
	"strings"
	"time"
)

const (
	defaultStoreTimeout = 5 * time.Second
)

// Team model struct
type Team struct {
	ID          string `json:"id"`
	DisplayName string `json:"displayName"`
}

// Period model struct
type Period struct {
	ID          string   `json:"id"`
	DisplayName string   `json:"displayName"`
	Unit        string   `json:"unit"`
	Buckets     []Bucket `json:"buckets"`
	People      []Person `json:"people"`
}

// Bucket model struct
type Bucket struct {
	DisplayName          string      `json:"displayName"`
	AllocationPercentage float64     `json:"allocationPercentage"`
	Objectives           []Objective `json:"objectives"`
}

// Objective model struct
type Objective struct {
	Name             string       `json:"name"`
	ResourceEstimate float64      `json:"resourceEstimate"`
	Assignments      []Assignment `json:"assignments"`
}

// Assignment model struct
type Assignment struct {
	PersonID   string  `json:"personId"`
	Commitment float64 `json:"commitment"`
}

// Person model struct
type Person struct {
	ID           string  `json:"id"`
	DisplayName  string  `json:"displayName"`
	Availability float64 `json:"availability"`
}

// StorageService to represent the persistent store
type StorageService interface {
	GetAllTeams(ctx context.Context) ([]Team, error)
	GetTeam(ctx context.Context, teamID string) (Team, bool, error)
	CreateTeam(ctx context.Context, team Team) error
	UpdateTeam(ctx context.Context, team Team) error
	GetAllPeriods(ctx context.Context, teamID string) ([]Period, bool, error)
	GetPeriod(ctx context.Context, teamID, periodID string) (Period, bool, error)
	CreatePeriod(ctx context.Context, teamID string, period Period) error
	UpdatePeriod(ctx context.Context, teamID string, period Period) error
	Close() error
}

// Server struct to handle incoming HTTP requests
type Server struct {
	store        StorageService
	storeTimeout time.Duration
}

func (s *Server) makeHandler() http.Handler {
	mux := http.NewServeMux()
	mux.HandleFunc("/api/team/", s.handleTeam)
	mux.HandleFunc("/api/period/", s.handlePeriod)
	return mux
}

func (s *Server) ensureTeamExistence(w http.ResponseWriter, r *http.Request, teamID string, expected bool) bool {
	ctx, cancel := context.WithTimeout(r.Context(), s.storeTimeout)
	defer cancel()
	_, exists, err := s.store.GetTeam(ctx, teamID)
	if err != nil {
		http.Error(w, fmt.Sprintf("Could not validate existence of team '%s': %s", teamID, err), http.StatusInternalServerError)
		return false
	}
	if exists != expected {
		statusCode := http.StatusBadRequest
		if expected {
			statusCode = http.StatusNotFound
		}
		http.Error(w, fmt.Sprintf("Team with ID '%s' expected exists=%v, found %v", teamID, expected, exists), statusCode)
		return false
	}
	return true
}

func (s *Server) ensurePeriodExistence(w http.ResponseWriter, r *http.Request, teamID, periodID string, expected bool) bool {
	ctx, cancel := context.WithTimeout(r.Context(), s.storeTimeout)
	defer cancel()
	_, exists, err := s.store.GetPeriod(ctx, teamID, periodID)
	if err != nil {
		http.Error(w, fmt.Sprintf("Could not validate existence of period '%s' for team '%s': %s", periodID, teamID, err), http.StatusInternalServerError)
		return false
	}
	if exists != expected {
		statusCode := http.StatusBadRequest
		if expected {
			statusCode = http.StatusNotFound
		}
		http.Error(w, fmt.Sprintf("Period '%s' for team '%s' expected exists=%v, found %v", periodID, teamID, expected, exists), statusCode)
		return false
	}
	return true
}

func (s *Server) handleTeam(w http.ResponseWriter, r *http.Request) {
	pathParts := strings.Split(r.URL.Path, "/")
	log.Printf("Team path: %q", pathParts)
	if len(pathParts) != 4 {
		http.NotFound(w, r)
		return
	}
	teamID := pathParts[3]
	if r.Method == http.MethodGet {
		s.handleGetTeam(w, r, teamID)
	} else if r.Method == http.MethodPost {
		s.handlePostTeam(w, r)
	} else if r.Method == http.MethodPut {
		s.handlePutTeam(w, r)
	} else {
		http.Error(w, fmt.Sprintf("Unsupported method '%s'", r.Method), http.StatusBadRequest)
	}
}

func (s *Server) handleGetTeam(w http.ResponseWriter, r *http.Request, teamID string) {
	ctx, cancel := context.WithTimeout(r.Context(), s.storeTimeout)
	defer cancel()
	if teamID == "" {
		teams, err := s.store.GetAllTeams(ctx)
		if err != nil {
			http.Error(w, fmt.Sprintf("Could not retrieve teams: %s", err), http.StatusInternalServerError)
			return
		}
		enc := json.NewEncoder(w)
		w.Header().Set("Content-Type", "application/json")
		enc.Encode(teams)
	} else {
		team, found, err := s.store.GetTeam(ctx, teamID)
		if !found {
			http.NotFound(w, r)
			return
		}
		if err != nil {
			http.Error(w, fmt.Sprintf("Could not retrieve team '%s': %s", teamID, err), http.StatusInternalServerError)
			return
		}
		enc := json.NewEncoder(w)
		w.Header().Set("Content-Type", "application/json")
		enc.Encode(team)
	}
}

func (s *Server) handlePostTeam(w http.ResponseWriter, r *http.Request) {
	team, ok := readTeamFromBody(w, r)
	if !ok {
		return
	}
	if !s.ensureTeamExistence(w, r, team.ID, false) {
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), s.storeTimeout)
	defer cancel()
	err := s.store.CreateTeam(ctx, team)
	if err != nil {
		http.Error(w, fmt.Sprintf("Could not create team: %s", err), http.StatusInternalServerError)
		return
	}
}

func (s *Server) handlePutTeam(w http.ResponseWriter, r *http.Request) {
	team, ok := readTeamFromBody(w, r)
	if !ok {
		return
	}
	if !s.ensureTeamExistence(w, r, team.ID, true) {
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), s.storeTimeout)
	defer cancel()
	err := s.store.UpdateTeam(ctx, team)
	if err != nil {
		http.Error(w, fmt.Sprintf("Could not update team: %s", err), http.StatusInternalServerError)
		return
	}
}

func readTeamFromBody(w http.ResponseWriter, r *http.Request) (Team, bool) {
	dec := json.NewDecoder(r.Body)
	team := Team{}
	err := dec.Decode(&team)
	if err != nil {
		http.Error(w, fmt.Sprintf("Could not decode body: %v", err), http.StatusBadRequest)
		return team, false
	}
	return team, true
}

func (s *Server) handlePeriod(w http.ResponseWriter, r *http.Request) {
	pathParts := strings.Split(r.URL.Path, "/")
	log.Printf("Period path: %q", pathParts)
	if len(pathParts) != 5 {
		http.NotFound(w, r)
		return
	}
	teamID := pathParts[3]
	periodID := pathParts[4]
	if r.Method == http.MethodGet {
		s.handleGetPeriod(w, r, teamID, periodID)
	} else if r.Method == http.MethodPost {
		s.handlePostPeriod(w, r, teamID)
	} else if r.Method == http.MethodPut {
		s.handlePutPeriod(w, r, teamID, periodID)
	} else {
		http.Error(w, fmt.Sprintf("Unsupported method '%s'", r.Method), http.StatusBadRequest)
	}
}

func (s *Server) handleGetPeriod(w http.ResponseWriter, r *http.Request, teamID, periodID string) {
	ctx, cancel := context.WithTimeout(r.Context(), s.storeTimeout)
	defer cancel()
	if periodID == "" {
		periods, found, err := s.store.GetAllPeriods(ctx, teamID)
		if !found {
			http.NotFound(w, r)
			return
		}
		if err != nil {
			http.Error(w, fmt.Sprintf("Could not retrieve periods for team '%s': %s", teamID, err), http.StatusInternalServerError)
			return
		}
		enc := json.NewEncoder(w)
		w.Header().Set("Content-Type", "application/json")
		enc.Encode(periods)
	} else {
		period, found, err := s.store.GetPeriod(ctx, teamID, periodID)
		if !found {
			http.NotFound(w, r)
			return
		}
		if err != nil {
			http.Error(w, fmt.Sprintf("Could not retrieve period '%s' for team '%s': %s", periodID, teamID, err), http.StatusInternalServerError)
			return
		}
		enc := json.NewEncoder(w)
		w.Header().Set("Content-Type", "application/json")
		enc.Encode(period)
	}
}

func (s *Server) handlePostPeriod(w http.ResponseWriter, r *http.Request, teamID string) {
	period, ok := readPeriodFromBody(w, r)
	if !ok {
		return
	}
	if !s.ensureTeamExistence(w, r, teamID, true) {
		return
	}
	if !s.ensurePeriodExistence(w, r, teamID, period.ID, false) {
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), s.storeTimeout)
	defer cancel()
	err := s.store.CreatePeriod(ctx, teamID, period)
	if err != nil {
		http.Error(w, fmt.Sprintf("Could not create period for team '%s': %s", teamID, err), http.StatusInternalServerError)
		return
	}
}

func (s *Server) handlePutPeriod(w http.ResponseWriter, r *http.Request, teamID, periodID string) {
	period, ok := readPeriodFromBody(w, r)
	if !ok {
		return
	}
	if !s.ensureTeamExistence(w, r, teamID, true) {
		return
	}
	if !s.ensurePeriodExistence(w, r, teamID, periodID, true) {
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), s.storeTimeout)
	defer cancel()
	err := s.store.UpdatePeriod(ctx, teamID, period)
	if err != nil {
		http.Error(w, fmt.Sprintf("Could not update period '%s' for team '%s': %s", periodID, teamID, err), http.StatusInternalServerError)
		return
	}
}

func readPeriodFromBody(w http.ResponseWriter, r *http.Request) (Period, bool) {
	dec := json.NewDecoder(r.Body)
	period := Period{}
	err := dec.Decode(&period)
	if err != nil {
		http.Error(w, fmt.Sprintf("Could not decode body: %v", err), http.StatusBadRequest)
		return period, false
	}
	return period, true
}

func main() {
	var gcloudProject string
	flag.StringVar(&gcloudProject, "gcloud_project", "", "GCP project ID")
	flag.Parse()
	var store StorageService
	if gcloudProject == "" {
		log.Printf("Using in-memory store")
		store = makeInMemStore()
	} else {
		log.Printf("Using Cloud Datastore storage service; project='%s'", gcloudProject)
		log.Printf("To use the local emulator, see https://cloud.google.com/datastore/docs/tools/datastore-emulator")
		ctx := context.Background()
		var err error
		store, err = makeGoogleCDSStore(ctx, gcloudProject)
		if err != nil {
			log.Fatalf("Could not instantiate datastore: %s", err)
			return
		}
	}
	server := Server{store: store, storeTimeout: defaultStoreTimeout}
	handler := server.makeHandler()
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
		log.Printf("Defaulting to port %s", port)
	}
	log.Printf("Listening on port %s", port)
	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), handler))
}
<commit_msg>Log storage service errors rather than including in HTTP response<commit_after>package main

import (
	"context"
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"net/http"
	"os"
	"strings"
	"time"
)

const (
	defaultStoreTimeout = 5 * time.Second
)

// Team model struct
type Team struct {
	ID          string `json:"id"`
	DisplayName string `json:"displayName"`
}

// Period model struct
type Period struct {
	ID          string   `json:"id"`
	DisplayName string   `json:"displayName"`
	Unit        string   `json:"unit"`
	Buckets     []Bucket `json:"buckets"`
	People      []Person `json:"people"`
}

// Bucket model struct
type Bucket struct {
	DisplayName          string      `json:"displayName"`
	AllocationPercentage float64     `json:"allocationPercentage"`
	Objectives           []Objective `json:"objectives"`
}

// Objective model struct
type Objective struct {
	Name             string       `json:"name"`
	ResourceEstimate float64      `json:"resourceEstimate"`
	Assignments      []Assignment `json:"assignments"`
}

// Assignment model struct
type Assignment struct {
	PersonID   string  `json:"personId"`
	Commitment float64 `json:"commitment"`
}

// Person model struct
type Person struct {
	ID           string  `json:"id"`
	DisplayName  string  `json:"displayName"`
	Availability float64 `json:"availability"`
}

// StorageService to represent the persistent store
type StorageService interface {
	GetAllTeams(ctx context.Context) ([]Team, error)
	GetTeam(ctx context.Context, teamID string) (Team, bool, error)
	CreateTeam(ctx context.Context, team Team) error
	UpdateTeam(ctx context.Context, team Team) error
	GetAllPeriods(ctx context.Context, teamID string) ([]Period, bool, error)
	GetPeriod(ctx context.Context, teamID, periodID string) (Period, bool, error)
	CreatePeriod(ctx context.Context, teamID string, period Period) error
	UpdatePeriod(ctx context.Context, teamID string, period Period) error
	Close() error
}

// Server struct to handle incoming HTTP requests
type Server struct {
	store        StorageService
	storeTimeout time.Duration
}

func (s *Server) makeHandler() http.Handler {
	mux := http.NewServeMux()
	mux.HandleFunc("/api/team/", s.handleTeam)
	mux.HandleFunc("/api/period/", s.handlePeriod)
	return mux
}

func (s *Server) ensureTeamExistence(w http.ResponseWriter, r *http.Request, teamID string, expected bool) bool {
	ctx, cancel := context.WithTimeout(r.Context(), s.storeTimeout)
	defer cancel()
	_, exists, err := s.store.GetTeam(ctx, teamID)
	if err != nil {
		log.Printf("Could not validate existence of team '%s': %s", teamID, err)
		http.Error(w, fmt.Sprintf("Could not validate existence of team '%s' (see server log)", teamID), http.StatusInternalServerError)
		return false
	}
	if exists != expected {
		statusCode := http.StatusBadRequest
		if expected {
			statusCode = http.StatusNotFound
		}
		http.Error(w, fmt.Sprintf("Team with ID '%s' expected exists=%v, found %v", teamID, expected, exists), statusCode)
		return false
	}
	return true
}

func (s *Server) ensurePeriodExistence(w http.ResponseWriter, r *http.Request, teamID, periodID string, expected bool) bool {
	ctx, cancel := context.WithTimeout(r.Context(), s.storeTimeout)
	defer cancel()
	_, exists, err := s.store.GetPeriod(ctx, teamID, periodID)
	if err != nil {
		log.Printf("Could not validate existence of period '%s' for team '%s': %s", periodID, teamID, err)
		http.Error(w, fmt.Sprintf("Could not validate existence of period '%s' for team '%s' (see server log)", periodID, teamID), http.StatusInternalServerError)
		return false
	}
	if exists != expected {
		statusCode := http.StatusBadRequest
		if expected {
			statusCode = http.StatusNotFound
		}
		http.Error(w, fmt.Sprintf("Period '%s' for team '%s' expected exists=%v, found %v", periodID, teamID, expected, exists), statusCode)
		return false
	}
	return true
}

func (s *Server) handleTeam(w http.ResponseWriter, r *http.Request) {
	pathParts := strings.Split(r.URL.Path, "/")
	log.Printf("Team path: %q", pathParts)
	if len(pathParts) != 4 {
		http.NotFound(w, r)
		return
	}
	teamID := pathParts[3]
	if r.Method == http.MethodGet {
		s.handleGetTeam(w, r, teamID)
	} else if r.Method == http.MethodPost {
		s.handlePostTeam(w, r)
	} else if r.Method == http.MethodPut {
		s.handlePutTeam(w, r)
	} else {
		http.Error(w, fmt.Sprintf("Unsupported method '%s'", r.Method), http.StatusBadRequest)
	}
}

func (s *Server) handleGetTeam(w http.ResponseWriter, r *http.Request, teamID string) {
	ctx, cancel := context.WithTimeout(r.Context(), s.storeTimeout)
	defer cancel()
	if teamID == "" {
		teams, err := s.store.GetAllTeams(ctx)
		if err != nil {
			log.Printf("Could not retrieve teams: %s", err)
			http.Error(w, "Could not retrieve teams (see server log)", http.StatusInternalServerError)
			return
		}
		enc := json.NewEncoder(w)
		w.Header().Set("Content-Type", "application/json")
		enc.Encode(teams)
	} else {
		team, found, err := s.store.GetTeam(ctx, teamID)
		if !found {
			http.NotFound(w, r)
			return
		}
		if err != nil {
			log.Printf("Could not retrieve team '%s': %s", teamID, err)
			http.Error(w, fmt.Sprintf("Could not retrieve team '%s' (see server log)", teamID), http.StatusInternalServerError)
			return
		}
		enc := json.NewEncoder(w)
		w.Header().Set("Content-Type", "application/json")
		enc.Encode(team)
	}
}

func (s *Server) handlePostTeam(w http.ResponseWriter, r *http.Request) {
	team, ok := readTeamFromBody(w, r)
	if !ok {
		return
	}
	if !s.ensureTeamExistence(w, r, team.ID, false) {
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), s.storeTimeout)
	defer cancel()
	err := s.store.CreateTeam(ctx, team)
	if err != nil {
		log.Printf("Could not create team: %s", err)
		http.Error(w, "Could not create team (see server log)", http.StatusInternalServerError)
		return
	}
}

func (s *Server) handlePutTeam(w http.ResponseWriter, r *http.Request) {
	team, ok := readTeamFromBody(w, r)
	if !ok {
		return
	}
	if !s.ensureTeamExistence(w, r, team.ID, true) {
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), s.storeTimeout)
	defer cancel()
	err := s.store.UpdateTeam(ctx, team)
	if err != nil {
		log.Printf("Could not update team: %s", err)
		http.Error(w, "Could not update team (see server log)", http.StatusInternalServerError)
		return
	}
}

func readTeamFromBody(w http.ResponseWriter, r *http.Request) (Team, bool) {
	dec := json.NewDecoder(r.Body)
	team := Team{}
	err := dec.Decode(&team)
	if err != nil {
		http.Error(w, fmt.Sprintf("Could not decode body: %v", err), http.StatusBadRequest)
		return team, false
	}
	return team, true
}

func (s *Server) handlePeriod(w http.ResponseWriter, r *http.Request) {
	pathParts := strings.Split(r.URL.Path, "/")
	log.Printf("Period path: %q", pathParts)
	if len(pathParts) != 5 {
		http.NotFound(w, r)
		return
	}
	teamID := pathParts[3]
	periodID := pathParts[4]
	if r.Method == http.MethodGet {
		s.handleGetPeriod(w, r, teamID, periodID)
	} else if r.Method == http.MethodPost {
		s.handlePostPeriod(w, r, teamID)
	} else if r.Method == http.MethodPut {
		s.handlePutPeriod(w, r, teamID, periodID)
	} else {
		http.Error(w, fmt.Sprintf("Unsupported method '%s'", r.Method), http.StatusBadRequest)
	}
}

func (s *Server) handleGetPeriod(w http.ResponseWriter, r *http.Request, teamID, periodID string) {
	ctx, cancel := context.WithTimeout(r.Context(), s.storeTimeout)
	defer cancel()
	if periodID == "" {
		periods, found, err := s.store.GetAllPeriods(ctx, teamID)
		if !found {
			http.NotFound(w, r)
			return
		}
		if err != nil {
			log.Printf("Could not retrieve periods for team '%s': %s", teamID, err)
			http.Error(w, fmt.Sprintf("Could not retrieve periods for team '%s' (see server log)", teamID), http.StatusInternalServerError)
			return
		}
		enc := json.NewEncoder(w)
		w.Header().Set("Content-Type", "application/json")
		enc.Encode(periods)
	} else {
		period, found, err := s.store.GetPeriod(ctx, teamID, periodID)
		if !found {
			http.NotFound(w, r)
			return
		}
		if err != nil {
			log.Printf("Could not retrieve period '%s' for team '%s': %s", periodID, teamID, err)
			http.Error(w, fmt.Sprintf("Could not retrieve period '%s' for team '%s' (see server log)", periodID, teamID), http.StatusInternalServerError)
			return
		}
		enc := json.NewEncoder(w)
		w.Header().Set("Content-Type", "application/json")
		enc.Encode(period)
	}
}

func (s *Server) handlePostPeriod(w http.ResponseWriter, r *http.Request, teamID string) {
	period, ok := readPeriodFromBody(w, r)
	if !ok {
		return
	}
	if !s.ensureTeamExistence(w, r, teamID, true) {
		return
	}
	if !s.ensurePeriodExistence(w, r, teamID, period.ID, false) {
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), s.storeTimeout)
	defer cancel()
	err := s.store.CreatePeriod(ctx, teamID, period)
	if err != nil {
		log.Printf("Could not create period for team '%s': %s", teamID, err)
		http.Error(w, fmt.Sprintf("Could not create period for team '%s' (see server log)", teamID), http.StatusInternalServerError)
		return
	}
}

func (s *Server) handlePutPeriod(w http.ResponseWriter, r *http.Request, teamID, periodID string) {
	period, ok := readPeriodFromBody(w, r)
	if !ok {
		return
	}
	if !s.ensureTeamExistence(w, r, teamID, true) {
		return
	}
	if !s.ensurePeriodExistence(w, r, teamID, periodID, true) {
		return
	}
	ctx, cancel := context.WithTimeout(r.Context(), s.storeTimeout)
	defer cancel()
	err := s.store.UpdatePeriod(ctx, teamID, period)
	if err != nil {
		log.Printf("Could not update period '%s' for team '%s': %s", periodID, teamID, err)
		http.Error(w, fmt.Sprintf("Could not update period '%s' for team '%s' (see server log)", periodID, teamID), http.StatusInternalServerError)
		return
	}
}

func readPeriodFromBody(w http.ResponseWriter, r *http.Request) (Period, bool) {
	dec := json.NewDecoder(r.Body)
	period := Period{}
	err := dec.Decode(&period)
	if err != nil {
		http.Error(w, fmt.Sprintf("Could not decode body: %v", err), http.StatusBadRequest)
		return period, false
	}
	return period, true
}

func main() {
	var gcloudProject string
	flag.StringVar(&gcloudProject, "gcloud_project", "", "GCP project ID")
	flag.Parse()
	var store StorageService
	if gcloudProject == "" {
		log.Printf("Using in-memory store")
		store = makeInMemStore()
	} else {
		log.Printf("Using Cloud Datastore storage service; project='%s'", gcloudProject)
		log.Printf("To use the local emulator, see https://cloud.google.com/datastore/docs/tools/datastore-emulator")
		ctx := context.Background()
		var err error
		store, err = makeGoogleCDSStore(ctx, gcloudProject)
		if err != nil {
			log.Fatalf("Could not instantiate datastore: %s", err)
			return
		}
	}
	server := Server{store: store, storeTimeout: defaultStoreTimeout}
	handler := server.makeHandler()
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
		log.Printf("Defaulting to port %s", port)
	}
	log.Printf("Listening on port %s", port)
	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), handler))
}
<|endoftext|>


<commit_before>package util

import (
	"bytes"
	"crypto/tls"
	"errors"
	"fmt"
	"github.com/astaxie/beego"
	"github.com/astaxie/beego/orm"
	"github.com/robxu9/kahinah/models"
	"log"
	"menteslibres.net/gosexy/to"
	"net/mail"
	"net/smtp"
	"strings"
	"text/template"
)

var (
	ErrDisabled = errors.New("Mail Service is Disabled")

	outwardUrl    = beego.AppConfig.String("outwardloc")
	outwardPrefix = beego.AppConfig.String("urlprefix")

	maintainer_hours = to.Int64(beego.AppConfig.String("karma::maintainerhours"))

	mail_enabled = to.Bool(beego.AppConfig.String("mail::enabled"))
	mail_user    = beego.AppConfig.String("mail::smtp_user")
	mail_pass    = beego.AppConfig.String("mail::smtp_pass")
	mail_domain  = beego.AppConfig.String("mail::smtp_domain")
	mail_host    = beego.AppConfig.String("mail::smtp_host")
	mail_verify  = to.Bool(beego.AppConfig.String("mail::smtp_tls_verify"))
	mail_email   = mail.Address{"Kahinah QA Bot", beego.AppConfig.String("mail::smtp_email")}

	mail_to = beego.AppConfig.String("mail::to")

	model_template       = template.New("email model template")
	maint_model_template = template.New("email maintainer model template")
	mail_template        = template.New("email full template")
)

func init() {
	model_template = template.Must(model_template.Parse(`Hello,

The following package has been {{.Action}}:

Id:		UPDATE-{{.Package.BuildDate.Year}}-{{.Package.Id}}
Name:	{{.Package.Name}}/{{.Package.Architecture}}
For:		{{.Package.Platform}}/{{.Package.Repo}}
Type:	{{.Package.Type}}
Built:	{{.Package.BuildDate}}

{{if ne .Package.Status "testing"}}Comments on the package:
{{range .Package.Karma}}
* {{.User.Email}} voted {{.Vote}} and commented: "{{.Comment}}".
{{end}}{{end}}

More information available at the Kahinah website:
{{.KahinahUrl}}/builds/{{.Package.Id}}
`))

	maint_model_template = template.Must(maint_model_template.Parse(`Hello,

The package you have built has been {{.Action}}:

Id:		UPDATE-{{.Package.BuildDate.Year}}-{{.Package.Id}}
Name:	{{.Package.Name}}/{{.Package.Architecture}}
For:		{{.Package.Platform}}/{{.Package.Repo}}
Type:	{{.Package.Type}}
Built:	{{.Package.BuildDate}}

{{if eq .Package.Status "testing"}}Be sure to vote for your own package - and remember that QA
may need more information: the comments section has input that
might prove valuable.{{else}}Comments on the package:
{{range .Package.Karma}}
* {{.User.Email}} voted {{.Vote}} and commented: "{{.Comment}}".
{{end}}{{end}}

More information available at the Kahinah website:
{{.KahinahUrl}}/builds/{{.Package.Id}}
`))

	mail_template = template.Must(mail_template.Parse(`From: {{.From}}
To: {{.To}}
Subject: {{.Subject}}
Mime-Version: 1.0
Content-type: text/plain

{{.Body}}

-------------------------------
This email was sent by Kahinah, the OpenMandriva QA bot.
Inbound email to this account is not monitored.
`))
}

func MailTo(subject, content, to string) error {
	if !mail_enabled {
		return ErrDisabled
	}

	data := make(map[string]string)
	data["From"] = mail_email.String()
	data["To"] = to
	data["Subject"] = subject
	data["Body"] = content

	var buf bytes.Buffer
	mail_template.Execute(&buf, data)

	if mail_domain == "" {
		if strings.Contains(mail_user, "@") {
			mail_domain = mail_user[strings.Index(mail_user, "@")+1:]
		} else {
			mail_domain = mail_host[:strings.Index(mail_host, ":")]
		}
	}

	return ourMail(mail_host, smtp.PlainAuth("", mail_user, mail_pass, mail_domain), mail_email.Address, []string{mail_to}, buf.Bytes())

}

func Mail(subject, content string) error {
	return MailTo(subject, content, mail_to)
}

// this function:
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
func ourMail(addr string, a smtp.Auth, from string, to []string, msg []byte) error {
	c, err := smtp.Dial(addr)
	if err != nil {
		return err
	}
	defer c.Close()
	//if err = c.Hello(); err != nil {
	//	return err
	//}
	if ok, _ := c.Extension("STARTTLS"); ok {
		if err = c.StartTLS(&tls.Config{InsecureSkipVerify: !mail_verify}); err != nil {
			return err
		}
	}
	if a != nil {
		if ok, _ := c.Extension("AUTH"); ok {
			if err = c.Auth(a); err != nil {
				return err
			}
		}
	}
	if err = c.Mail(from); err != nil {
		return err
	}
	for _, addr := range to {
		if err = c.Rcpt(addr); err != nil {
			return err
		}
	}
	w, err := c.Data()
	if err != nil {
		return err
	}
	_, err = w.Write(msg)
	if err != nil {
		return err
	}
	err = w.Close()
	if err != nil {
		return err
	}
	return c.Quit()
}

func MailModel(model *models.BuildList) {
	if model.Submitter == nil {
		o := orm.NewOrm()
		o.LoadRelated(model, "Submitter")
	}

	if model.Karma == nil {
		o := orm.NewOrm()
		o.LoadRelated(model, "Karma")
		for _, karma := range model.Karma {
			o.LoadRelated(karma, "User")
		}
	}

	data := make(map[string]interface{})

	action := "lost in an abyss"

	switch model.Status {
	case models.STATUS_TESTING:
		action = "pushed to testing"
	case models.STATUS_PUBLISHED:
		action = "published"
	case models.STATUS_REJECTED:
		action = "rejected"
	}

	data["Action"] = action

	data["KahinahUrl"] = outwardUrl
	if outwardPrefix != "" {
		data["KahinahUrl"] = outwardUrl + "/" + outwardPrefix
	}
	data["Package"] = model

	var modelTemplateBuf bytes.Buffer
	model_template.Execute(&modelTemplateBuf, data)

	var maintTemplateBuf bytes.Buffer
	maint_model_template.Execute(&maintTemplateBuf, data)

	subject := fmt.Sprintf("[kahinah] %s/%s (%s) %s", model.Name, model.Architecture, to.String(model.Id), action)

	err := Mail(subject, modelTemplateBuf.String())
	if err != nil {
		log.Printf("[mail] model email failed: %s\n", err)
	}

	err = MailTo(subject, maintTemplateBuf.String(), model.Submitter.Email)
	if err != nil {
		log.Printf("[mail] maint email failed: %s\n", err)
	}
}
<commit_msg>fix maint mail model<commit_after>package util

import (
	"bytes"
	"crypto/tls"
	"errors"
	"fmt"
	"log"
	"net/mail"
	"net/smtp"
	"strings"
	"text/template"

	"github.com/astaxie/beego"
	"github.com/astaxie/beego/orm"
	"github.com/robxu9/kahinah/models"
	"menteslibres.net/gosexy/to"
)

var (
	ErrDisabled = errors.New("Mail Service is Disabled")

	outwardUrl    = beego.AppConfig.String("outwardloc")
	outwardPrefix = beego.AppConfig.String("urlprefix")

	maintainer_hours = to.Int64(beego.AppConfig.String("karma::maintainerhours"))

	mail_enabled = to.Bool(beego.AppConfig.String("mail::enabled"))
	mail_user    = beego.AppConfig.String("mail::smtp_user")
	mail_pass    = beego.AppConfig.String("mail::smtp_pass")
	mail_domain  = beego.AppConfig.String("mail::smtp_domain")
	mail_host    = beego.AppConfig.String("mail::smtp_host")
	mail_verify  = to.Bool(beego.AppConfig.String("mail::smtp_tls_verify"))
	mail_email   = mail.Address{"Kahinah QA Bot", beego.AppConfig.String("mail::smtp_email")}

	mail_to = beego.AppConfig.String("mail::to")

	model_template       = template.New("email model template")
	maint_model_template = template.New("email maintainer model template")
	mail_template        = template.New("email full template")
)

func init() {
	model_template = template.Must(model_template.Parse(`Hello,

The following package has been {{.Action}}:

Id:		UPDATE-{{.Package.BuildDate.Year}}-{{.Package.Id}}
Name:	{{.Package.Name}}/{{.Package.Architecture}}
For:		{{.Package.Platform}}/{{.Package.Repo}}
Type:	{{.Package.Type}}
Built:	{{.Package.BuildDate}}

{{if ne .Package.Status "testing"}}Comments on the package:
{{range .Package.Karma}}
* {{.User.Email}} voted {{.Vote}} and commented: "{{.Comment}}".
{{end}}{{end}}

More information available at the Kahinah website:
{{.KahinahUrl}}/builds/{{.Package.Id}}
`))

	maint_model_template = template.Must(maint_model_template.Parse(`Hello,

The package you have built has been {{.Action}}:

Id:		UPDATE-{{.Package.BuildDate.Year}}-{{.Package.Id}}
Name:	{{.Package.Name}}/{{.Package.Architecture}}
For:		{{.Package.Platform}}/{{.Package.Repo}}
Type:	{{.Package.Type}}
Built:	{{.Package.BuildDate}}

{{if eq .Package.Status "testing"}}Be sure to vote for your own package - and remember that QA
may need more information: the comments section has input that
might prove valuable.{{else}}Comments on the package:
{{range .Package.Karma}}
* {{.User.Email}} voted {{.Vote}} and commented: "{{.Comment}}".
{{end}}{{end}}

More information available at the Kahinah website:
{{.KahinahUrl}}/builds/{{.Package.Id}}
`))

	mail_template = template.Must(mail_template.Parse(`From: {{.From}}
To: {{.To}}
Subject: {{.Subject}}
Mime-Version: 1.0
Content-type: text/plain

{{.Body}}

-------------------------------
This email was sent by Kahinah, the OpenMandriva QA bot.
Inbound email to this account is not monitored.
`))
}

func MailTo(subject, content, to string) error {
	if !mail_enabled {
		return ErrDisabled
	}

	data := make(map[string]string)
	data["From"] = mail_email.String()
	data["To"] = to
	data["Subject"] = subject
	data["Body"] = content

	var buf bytes.Buffer
	mail_template.Execute(&buf, data)

	if mail_domain == "" {
		if strings.Contains(mail_user, "@") {
			mail_domain = mail_user[strings.Index(mail_user, "@")+1:]
		} else {
			mail_domain = mail_host[:strings.Index(mail_host, ":")]
		}
	}

	return ourMail(mail_host, smtp.PlainAuth("", mail_user, mail_pass, mail_domain), mail_email.Address, []string{to}, buf.Bytes())

}

func Mail(subject, content string) error {
	return MailTo(subject, content, mail_to)
}

// this function:
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
func ourMail(addr string, a smtp.Auth, from string, to []string, msg []byte) error {
	c, err := smtp.Dial(addr)
	if err != nil {
		return err
	}
	defer c.Close()
	//if err = c.Hello(); err != nil {
	//	return err
	//}
	if ok, _ := c.Extension("STARTTLS"); ok {
		if err = c.StartTLS(&tls.Config{InsecureSkipVerify: !mail_verify}); err != nil {
			return err
		}
	}
	if a != nil {
		if ok, _ := c.Extension("AUTH"); ok {
			if err = c.Auth(a); err != nil {
				return err
			}
		}
	}
	if err = c.Mail(from); err != nil {
		return err
	}
	for _, addr := range to {
		if err = c.Rcpt(addr); err != nil {
			return err
		}
	}
	w, err := c.Data()
	if err != nil {
		return err
	}
	_, err = w.Write(msg)
	if err != nil {
		return err
	}
	err = w.Close()
	if err != nil {
		return err
	}
	return c.Quit()
}

func MailModel(model *models.BuildList) {
	if model.Submitter == nil {
		o := orm.NewOrm()
		o.LoadRelated(model, "Submitter")
	}

	if model.Karma == nil {
		o := orm.NewOrm()
		o.LoadRelated(model, "Karma")
		for _, karma := range model.Karma {
			o.LoadRelated(karma, "User")
		}
	}

	data := make(map[string]interface{})

	action := "lost in an abyss"

	switch model.Status {
	case models.STATUS_TESTING:
		action = "pushed to testing"
	case models.STATUS_PUBLISHED:
		action = "published"
	case models.STATUS_REJECTED:
		action = "rejected"
	}

	data["Action"] = action

	data["KahinahUrl"] = outwardUrl
	if outwardPrefix != "" {
		data["KahinahUrl"] = outwardUrl + "/" + outwardPrefix
	}
	data["Package"] = model

	var modelTemplateBuf bytes.Buffer
	model_template.Execute(&modelTemplateBuf, data)

	var maintTemplateBuf bytes.Buffer
	maint_model_template.Execute(&maintTemplateBuf, data)

	subject := fmt.Sprintf("[kahinah] %s/%s (%s) %s", model.Name, model.Architecture, to.String(model.Id), action)

	err := Mail(subject, modelTemplateBuf.String())
	if err != nil {
		log.Printf("[mail] model email failed: %s\n", err)
	}

	err = MailTo(subject, maintTemplateBuf.String(), model.Submitter.Email)
	if err != nil {
		log.Printf("[mail] maint email failed: %s\n", err)
	}
}
<|endoftext|>