<commit_before>package state

import (
	"crypto/tls"
	"crypto/x509"
	"errors"
	"fmt"
	"net"
	"time"

	"labix.org/v2/mgo"
	"labix.org/v2/mgo/txn"
	"launchpad.net/juju-core/cert"
	"launchpad.net/juju-core/environs/config"
	"launchpad.net/juju-core/log"
	"launchpad.net/juju-core/state/presence"
	"launchpad.net/juju-core/state/watcher"
)

// Info encapsulates information about cluster of
// servers holding juju state and can be used to make a
// connection to that cluster.
type Info struct {
	// Addrs gives the addresses of the MongoDB servers for the state.
	// Each address should be in the form address:port.
	Addrs []string

	// CACert holds the CA certificate that will be used
	// to validate the state server's certificate, in PEM format.
	CACert []byte

	// EntityName holds the name of the entity that is connecting.
	// It should be empty when connecting as an administrator.
	EntityName string

	// Password holds the password for the connecting entity.
	Password string
}

var dialTimeout = 10 * time.Minute

// Open connects to the server described by the given
// info, waits for it to be initialized, and returns a new State
// representing the environment connected to.
// It returns errUnauthorized if access is unauthorized.
func Open(info *Info) (*State, error) {
	log.Printf("state: opening state; mongo addresses: %q; entity %q", info.Addrs, info.EntityName)
	if len(info.Addrs) == 0 {
		return nil, errors.New("no mongo addresses")
	}
	if len(info.CACert) == 0 {
		return nil, errors.New("missing CA certificate")
	}
	xcert, err := cert.ParseCert(info.CACert)
	if err != nil {
		return nil, fmt.Errorf("cannot parse CA certificate: %v", err)
	}
	pool := x509.NewCertPool()
	pool.AddCert(xcert)
	tlsConfig := &tls.Config{
		RootCAs:    pool,
		ServerName: "anything",
	}
	dial := func(addr net.Addr) (net.Conn, error) {
		log.Printf("state: connecting to %v", addr)
		c, err := tls.Dial("tcp", addr.String(), tlsConfig)
		if err != nil {
			log.Printf("state: connection failed: %v", err)
			return nil, err
		}
		log.Printf("state: connection established")
		return c, err
	}
	session, err := mgo.DialWithInfo(&mgo.DialInfo{
		Addrs:   info.Addrs,
		Timeout: dialTimeout,
		Dial:    dial,
	})
	if err != nil {
		return nil, err
	}
	st, err := newState(session, info)
	if err != nil {
		session.Close()
		return nil, err
	}
	return st, nil
}

// Initialize sets up an initial empty state and returns it.
// This needs to be performed only once for a given environment.
// It returns errUnauthorized if access is unauthorized.
func Initialize(info *Info, cfg *config.Config) (rst *State, err error) {
	st, err := Open(info)
	if err != nil {
		return nil, err
	}
	defer func() {
		if err != nil {
			st.Close()
		}
	}()
	// A valid environment config is used as a signal that the
	// state has already been initalized. If this is the case
	// do nothing.
	if _, err := st.EnvironConfig(); err == nil {
		return st, nil
	} else if !IsNotFound(err) {
		return nil, err
	}
	log.Printf("state: initializing environment")
	if cfg.AdminSecret() != "" {
		return nil, fmt.Errorf("admin-secret should never be written to the state")
	}
	ops := []txn.Op{
		createConstraintsOp(st, "e", Constraints{}),
		createSettingsOp(st, "e", cfg.AllAttrs()),
	}
	if err := st.runner.Run(ops, "", nil); err == txn.ErrAborted {
		// The config was created in the meantime.
		return st, nil
	} else if err != nil {
		return nil, err
	}
	return st, nil
}

var indexes = []struct {
	collection string
	key        []string
}{
	// After the first public release, do not remove entries from here
	// without adding them to a list of indexes to drop, to ensure
	// old databases are modified to have the correct indexes.
	{"relations", []string{"endpoints.relationname"}},
	{"relations", []string{"endpoints.servicename"}},
	{"units", []string{"service"}},
	{"units", []string{"principal"}},
	{"units", []string{"machineid"}},
	{"users", []string{"name"}},
}

// The capped collection used for transaction logs defaults to 10MB.
// It's tweaked in export_test.go to 1MB to avoid the overhead of
// creating and deleting the large file repeatedly in tests.
var (
	logSize      = 10000000
	logSizeTests = 1000000
)

// errUnauthorized represents the error that an operation is unauthorized.
type errUnauthorized struct {
	msg string
	error
}

func (e *errUnauthorized) Error() string {
	if e.error != nil {
		return fmt.Sprintf("%s: %v", e.msg, e.error.Error())
	}
	return e.msg
}

// Unauthorizedf returns an error for which IsUnauthorizedError returns true.
// It is mainly used for testing.
func Unauthorizedf(format string, args ...interface{}) error {
	return &errUnauthorized{fmt.Sprintf(format, args...), nil}
}

func IsUnauthorizedError(err error) bool {
	_, ok := err.(*errUnauthorized)
	return ok
}

func maybeUnauthorized(err error, msg string) error {
	if err == nil {
		return nil
	}
	// Unauthorized access errors have no error code,
	// just a simple error string.
	if err.Error() == "auth fails" {
		return &errUnauthorized{msg, err}
	}
	if err, ok := err.(*mgo.QueryError); ok && err.Code == 10057 {
		return &errUnauthorized{msg, err}
	}
	return fmt.Errorf("%s: %v", msg, err)
}

func newState(session *mgo.Session, info *Info) (*State, error) {
	db := session.DB("juju")
	pdb := session.DB("presence")
	if info.EntityName != "" {
		if err := db.Login(info.EntityName, info.Password); err != nil {
			return nil, maybeUnauthorized(err, "cannot log in to juju database")
		}
		if err := pdb.Login(info.EntityName, info.Password); err != nil {
			return nil, maybeUnauthorized(err, "cannot log in to presence database")
		}
	} else if info.Password != "" {
		admin := session.DB("admin")
		if err := admin.Login("admin", info.Password); err != nil {
			return nil, maybeUnauthorized(err, "cannot log in to admin database")
		}
	}
	st := &State{
		info:           info,
		db:             db,
		charms:         db.C("charms"),
		machines:       db.C("machines"),
		relations:      db.C("relations"),
		relationScopes: db.C("relationscopes"),
		services:       db.C("services"),
		settings:       db.C("settings"),
		constraints:    db.C("constraints"),
		units:          db.C("units"),
		users:          db.C("users"),
		presence:       pdb.C("presence"),
		cleanups:       db.C("cleanups"),
	}
	log := db.C("txns.log")
	logInfo := mgo.CollectionInfo{Capped: true, MaxBytes: logSize}
	// The lack of error code for this error was reported upstream:
	//     https://jira.mongodb.org/browse/SERVER-6992
	err := log.Create(&logInfo)
	if err != nil && err.Error() != "collection already exists" {
		return nil, maybeUnauthorized(err, "cannot create log collection")
	}
	st.runner = txn.NewRunner(db.C("txns"))
	st.runner.ChangeLog(db.C("txns.log"))
	st.watcher = watcher.New(db.C("txns.log"))
	st.pwatcher = presence.NewWatcher(pdb.C("presence"))
	for _, item := range indexes {
		index := mgo.Index{Key: item.key}
		if err := db.C(item.collection).EnsureIndex(index); err != nil {
			return nil, fmt.Errorf("cannot create database index: %v", err)
		}
	}
	return st, nil
}

// Addrs returns the list of addresses used to connect to the state.
func (st *State) Addrs() (addrs []string) {
	return append(addrs, st.info.Addrs...)
}

// CACert returns the certificate used to validate the state connection.
func (st *State) CACert() (cert []byte) {
	return append(cert, st.info.CACert...)
}

func (st *State) Close() error {
	err1 := st.watcher.Stop()
	err2 := st.pwatcher.Stop()
	st.db.Session.Close()
	for _, err := range []error{err1, err2} {
		if err != nil {
			return err
		}
	}
	return nil
}
<commit_msg>Rename errUnauthorized struct<commit_after>package state

import (
	"crypto/tls"
	"crypto/x509"
	"errors"
	"fmt"
	"net"
	"time"

	"labix.org/v2/mgo"
	"labix.org/v2/mgo/txn"
	"launchpad.net/juju-core/cert"
	"launchpad.net/juju-core/environs/config"
	"launchpad.net/juju-core/log"
	"launchpad.net/juju-core/state/presence"
	"launchpad.net/juju-core/state/watcher"
)

// Info encapsulates information about cluster of
// servers holding juju state and can be used to make a
// connection to that cluster.
type Info struct {
	// Addrs gives the addresses of the MongoDB servers for the state.
	// Each address should be in the form address:port.
	Addrs []string

	// CACert holds the CA certificate that will be used
	// to validate the state server's certificate, in PEM format.
	CACert []byte

	// EntityName holds the name of the entity that is connecting.
	// It should be empty when connecting as an administrator.
	EntityName string

	// Password holds the password for the connecting entity.
	Password string
}

var dialTimeout = 10 * time.Minute

// Open connects to the server described by the given
// info, waits for it to be initialized, and returns a new State
// representing the environment connected to.
// It returns unauthorizedError if access is unauthorized.
func Open(info *Info) (*State, error) {
	log.Printf("state: opening state; mongo addresses: %q; entity %q", info.Addrs, info.EntityName)
	if len(info.Addrs) == 0 {
		return nil, errors.New("no mongo addresses")
	}
	if len(info.CACert) == 0 {
		return nil, errors.New("missing CA certificate")
	}
	xcert, err := cert.ParseCert(info.CACert)
	if err != nil {
		return nil, fmt.Errorf("cannot parse CA certificate: %v", err)
	}
	pool := x509.NewCertPool()
	pool.AddCert(xcert)
	tlsConfig := &tls.Config{
		RootCAs:    pool,
		ServerName: "anything",
	}
	dial := func(addr net.Addr) (net.Conn, error) {
		log.Printf("state: connecting to %v", addr)
		c, err := tls.Dial("tcp", addr.String(), tlsConfig)
		if err != nil {
			log.Printf("state: connection failed: %v", err)
			return nil, err
		}
		log.Printf("state: connection established")
		return c, err
	}
	session, err := mgo.DialWithInfo(&mgo.DialInfo{
		Addrs:   info.Addrs,
		Timeout: dialTimeout,
		Dial:    dial,
	})
	if err != nil {
		return nil, err
	}
	st, err := newState(session, info)
	if err != nil {
		session.Close()
		return nil, err
	}
	return st, nil
}

// Initialize sets up an initial empty state and returns it.
// This needs to be performed only once for a given environment.
// It returns unauthorizedError if access is unauthorized.
func Initialize(info *Info, cfg *config.Config) (rst *State, err error) {
	st, err := Open(info)
	if err != nil {
		return nil, err
	}
	defer func() {
		if err != nil {
			st.Close()
		}
	}()
	// A valid environment config is used as a signal that the
	// state has already been initalized. If this is the case
	// do nothing.
	if _, err := st.EnvironConfig(); err == nil {
		return st, nil
	} else if !IsNotFound(err) {
		return nil, err
	}
	log.Printf("state: initializing environment")
	if cfg.AdminSecret() != "" {
		return nil, fmt.Errorf("admin-secret should never be written to the state")
	}
	ops := []txn.Op{
		createConstraintsOp(st, "e", Constraints{}),
		createSettingsOp(st, "e", cfg.AllAttrs()),
	}
	if err := st.runner.Run(ops, "", nil); err == txn.ErrAborted {
		// The config was created in the meantime.
		return st, nil
	} else if err != nil {
		return nil, err
	}
	return st, nil
}

var indexes = []struct {
	collection string
	key        []string
}{
	// After the first public release, do not remove entries from here
	// without adding them to a list of indexes to drop, to ensure
	// old databases are modified to have the correct indexes.
	{"relations", []string{"endpoints.relationname"}},
	{"relations", []string{"endpoints.servicename"}},
	{"units", []string{"service"}},
	{"units", []string{"principal"}},
	{"units", []string{"machineid"}},
	{"users", []string{"name"}},
}

// The capped collection used for transaction logs defaults to 10MB.
// It's tweaked in export_test.go to 1MB to avoid the overhead of
// creating and deleting the large file repeatedly in tests.
var (
	logSize      = 10000000
	logSizeTests = 1000000
)

// unauthorizedError represents the error that an operation is unauthorized.
type unauthorizedError struct {
	msg string
	error
}

func (e *unauthorizedError) Error() string {
	if e.error != nil {
		return fmt.Sprintf("%s: %v", e.msg, e.error.Error())
	}
	return e.msg
}

// Unauthorizedf returns an error for which IsUnauthorizedError returns true.
// It is mainly used for testing.
func Unauthorizedf(format string, args ...interface{}) error {
	return &unauthorizedError{fmt.Sprintf(format, args...), nil}
}

func IsUnauthorizedError(err error) bool {
	_, ok := err.(*unauthorizedError)
	return ok
}

func maybeUnauthorized(err error, msg string) error {
	if err == nil {
		return nil
	}
	// Unauthorized access errors have no error code,
	// just a simple error string.
	if err.Error() == "auth fails" {
		return &unauthorizedError{msg, err}
	}
	if err, ok := err.(*mgo.QueryError); ok && err.Code == 10057 {
		return &unauthorizedError{msg, err}
	}
	return fmt.Errorf("%s: %v", msg, err)
}

func newState(session *mgo.Session, info *Info) (*State, error) {
	db := session.DB("juju")
	pdb := session.DB("presence")
	if info.EntityName != "" {
		if err := db.Login(info.EntityName, info.Password); err != nil {
			return nil, maybeUnauthorized(err, "cannot log in to juju database")
		}
		if err := pdb.Login(info.EntityName, info.Password); err != nil {
			return nil, maybeUnauthorized(err, "cannot log in to presence database")
		}
	} else if info.Password != "" {
		admin := session.DB("admin")
		if err := admin.Login("admin", info.Password); err != nil {
			return nil, maybeUnauthorized(err, "cannot log in to admin database")
		}
	}
	st := &State{
		info:           info,
		db:             db,
		charms:         db.C("charms"),
		machines:       db.C("machines"),
		relations:      db.C("relations"),
		relationScopes: db.C("relationscopes"),
		services:       db.C("services"),
		settings:       db.C("settings"),
		constraints:    db.C("constraints"),
		units:          db.C("units"),
		users:          db.C("users"),
		presence:       pdb.C("presence"),
		cleanups:       db.C("cleanups"),
	}
	log := db.C("txns.log")
	logInfo := mgo.CollectionInfo{Capped: true, MaxBytes: logSize}
	// The lack of error code for this error was reported upstream:
	//     https://jira.mongodb.org/browse/SERVER-6992
	err := log.Create(&logInfo)
	if err != nil && err.Error() != "collection already exists" {
		return nil, maybeUnauthorized(err, "cannot create log collection")
	}
	st.runner = txn.NewRunner(db.C("txns"))
	st.runner.ChangeLog(db.C("txns.log"))
	st.watcher = watcher.New(db.C("txns.log"))
	st.pwatcher = presence.NewWatcher(pdb.C("presence"))
	for _, item := range indexes {
		index := mgo.Index{Key: item.key}
		if err := db.C(item.collection).EnsureIndex(index); err != nil {
			return nil, fmt.Errorf("cannot create database index: %v", err)
		}
	}
	return st, nil
}

// Addrs returns the list of addresses used to connect to the state.
func (st *State) Addrs() (addrs []string) {
	return append(addrs, st.info.Addrs...)
}

// CACert returns the certificate used to validate the state connection.
func (st *State) CACert() (cert []byte) {
	return append(cert, st.info.CACert...)
}

func (st *State) Close() error {
	err1 := st.watcher.Stop()
	err2 := st.pwatcher.Stop()
	st.db.Session.Close()
	for _, err := range []error{err1, err2} {
		if err != nil {
			return err
		}
	}
	return nil
}
<|endoftext|>


<commit_before>package songplayer

import (
	"fmt"
	"github.com/DexterLB/mpvipc"
	"github.com/vansante/go-event-emitter"
	"gitlab.transip.us/swiltink/go-MusicBot/meta"
	"os"
	"os/exec"
	"regexp"
	"strings"
	"sync"
	"time"
)

var youtubeURLRegex, _ = regexp.Compile(`^(https?://)?(www\.)?(youtube\.com|youtu\.?be)/.+$`)

const (
	MPV_INIT_RETRY_ATTEMPTS = 5
	MAX_MPV_LOAD_WAIT       = time.Duration(time.Second * 20)
)

type YoutubePlayer struct {
	mpvBinPath   string
	mpvInputPath string

	mpvProcess   *exec.Cmd
	mpvIsRunning bool
	mpvConn      *mpvipc.Connection
	ytService    *meta.YouTube
	mpvMutex     sync.Mutex
	mpvEvents    *eventemitter.Emitter
}

func NewYoutubePlayer(mpvBinPath, mpvInputPath string) (player *YoutubePlayer, err error) {
	player = &YoutubePlayer{
		mpvBinPath:   mpvBinPath,
		mpvInputPath: mpvInputPath,
		mpvIsRunning: false,
		ytService:    meta.NewYoutubeService(),
		mpvEvents:    eventemitter.NewEmitter(),
	}

	err = player.init()
	return
}

func (p *YoutubePlayer) init() (err error) {
	p.mpvMutex.Lock()
	defer p.mpvMutex.Unlock()

	fi, err := os.Stat(p.mpvInputPath)
	if err == nil && !fi.IsDir() {
		fmt.Printf("[YoutubePlayer] Removing existing MPV control node on %s\n", p.mpvInputPath)
		err = os.Remove(p.mpvInputPath)
		if err != nil {
			err = fmt.Errorf("[YoutubePlayer] Error removing existing input %s: %v", p.mpvInputPath, err)
			return
		}
	}

	err = p.startMpv()
	if err != nil {
		err = fmt.Errorf("[YoutubePlayer] Error starting mpv: %v ", err)
		return
	}

	attempts := 0
	for {
		// Give MPV a second or so to start and create the input socket
		time.Sleep(500 * time.Millisecond)

		fmt.Printf("[YoutubePlayer] Opening mpv ipc connection on %s\n", p.mpvInputPath)
		p.mpvConn = mpvipc.NewConnection(p.mpvInputPath)
		err = p.mpvConn.Open()
		if err != nil {
			if attempts >= MPV_INIT_RETRY_ATTEMPTS {
				err = fmt.Errorf("[YoutubePlayer] Error opening IPC connection on %s: %v ", p.mpvInputPath, err)
				return
			}
		} else {
			err = nil
			break
		}
		attempts++
	}

	// Turn on all events.
	p.mpvConn.Call("enable_event", "all")

	go func() {
		events, stopListening := p.mpvConn.NewEventListener()
		for event := range events {
			p.mpvEvents.EmitEvent(event.Name, event)
		}
		stopListening <- struct{}{}
	}()

	return
}

func (p *YoutubePlayer) startMpv() (err error) {
	fmt.Printf("[YoutubePlayer] Starting MPV %s with control %s in idle mode\n", p.mpvBinPath, p.mpvInputPath)

	command := exec.Command(p.mpvBinPath, "--no-video", "--idle", "--input-ipc-server="+p.mpvInputPath)
	p.mpvProcess = command

	err = command.Start()
	p.mpvIsRunning = err == nil

	if err != nil {
		return
	}

	go func() {
		err := command.Wait()

		fmt.Printf("[YoutubePlayer] mpv has quit: %v\n", err)

		p.mpvMutex.Lock()
		p.mpvIsRunning = false
		p.mpvProcess = nil
		p.mpvMutex.Unlock()
	}()
	return
}

func (p *YoutubePlayer) checkRunning() (err error) {
	if p.mpvIsRunning && p.mpvProcess != nil {
		return
	}
	fmt.Print("[YoutubePlayer] mpv is not running, restarting mpv\n")
	err = p.init()
	return
}

func (p *YoutubePlayer) Name() (name string) {
	return "Youtube"
}

func (p *YoutubePlayer) CanPlay(url string) (canPlay bool) {
	return youtubeURLRegex.MatchString(url)
}

func (p *YoutubePlayer) GetSongs(url string) (songs []Playable, err error) {
	lowerURL := strings.ToLower(url)
	if strings.Contains(lowerURL, "player") || strings.Contains(lowerURL, "list=") {
		var metaDatas []meta.Meta
		metaDatas, err = p.ytService.GetMetasForPlaylistURL(url)
		if err == nil {
			for _, metaData := range metaDatas {
				songs = append(songs, NewSong(metaData.Title, metaData.Duration, metaData.Source))
			}
			return
		}
		// On error, fall back to single add
	}

	metaData, err := p.ytService.GetMetaForURL(url)
	if err != nil {
		err = fmt.Errorf("[YoutubePlayer] Error getting meta data: %v", err)
		return
	}
	songs = append(songs, NewSong(metaData.Title, metaData.Duration, metaData.Source))
	return
}

func (p *YoutubePlayer) SearchSongs(searchStr string, limit int) (songs []Playable, err error) {
	metaDatas, err := p.ytService.SearchForMetas(searchStr, limit)
	if err != nil {
		err = fmt.Errorf("[YoutubePlayer] Error searching meta data: %v", err)
		return
	}

	for _, metaData := range metaDatas {
		songs = append(songs, NewSong(metaData.Title, metaData.Duration, metaData.Source))
	}
	return
}

func (p *YoutubePlayer) Play(url string) (err error) {
	p.mpvMutex.Lock()
	defer p.mpvMutex.Unlock()

	err = p.checkRunning()
	if err != nil {
		return
	}

	err = p.stop()
	if err != nil {
		return
	}

	waitForLoad := make(chan bool)
	p.mpvEvents.ListenOnce("file-loaded", func(arguments ...interface{}) {
		waitForLoad <- true
	})

	// Start an event listener to wait for the file to load.
	_, err = p.mpvConn.Call("loadfile", url, "replace")
	if err != nil {
		err = fmt.Errorf("[YoutubePlayer] Error sending loadfile command: %v", err)
		return
	}

	go func() {
		time.Sleep(MAX_MPV_LOAD_WAIT)
		waitForLoad <- false
	}()

	timeExceed := <-waitForLoad
	if timeExceeded {
		fmt.Printf("[YoutubePlayer] Load file timeout\n")
		_, err = p.mpvConn.Call("stop")
		if err != nil {
			fmt.Printf("[YoutubePlayer] Error calling stop after timeout: %v\n", err)
		}
		err = fmt.Errorf("[YoutubePlayer] Error loading file, mpv did not respond in time")
		return
	}
	return
}

func (p *YoutubePlayer) Seek(positionSeconds int) (err error) {
	p.mpvMutex.Lock()
	defer p.mpvMutex.Unlock()

	err = p.checkRunning()
	if err != nil {
		return
	}

	err = p.mpvConn.Set("time-pos", positionSeconds)
	if err != nil {
		err = fmt.Errorf("[YoutubePlayer] Error sending time-pos property: %v", err)
	}
	return
}

func (p *YoutubePlayer) Pause(pauseState bool) (err error) {
	p.mpvMutex.Lock()
	defer p.mpvMutex.Unlock()

	err = p.checkRunning()
	if err != nil {
		return
	}

	err = p.mpvConn.Set("pause", pauseState)
	if err != nil {
		err = fmt.Errorf("[YoutubePlayer] Error sending pause state property: %v", err)
	}
	return
}

func (p *YoutubePlayer) Stop() (err error) {
	p.mpvMutex.Lock()
	defer p.mpvMutex.Unlock()

	return p.stop()
}

func (p *YoutubePlayer) stop() (err error) {
	err = p.checkRunning()
	if err != nil {
		return
	}

	_, err = p.mpvConn.Call("stop")
	if err != nil {
		err = fmt.Errorf("[YoutubePlayer] Error sending stop command: %v", err)
	}
	return
}
<commit_msg>Fix varname<commit_after>package songplayer

import (
	"fmt"
	"github.com/DexterLB/mpvipc"
	"github.com/vansante/go-event-emitter"
	"gitlab.transip.us/swiltink/go-MusicBot/meta"
	"os"
	"os/exec"
	"regexp"
	"strings"
	"sync"
	"time"
)

var youtubeURLRegex, _ = regexp.Compile(`^(https?://)?(www\.)?(youtube\.com|youtu\.?be)/.+$`)

const (
	MPV_INIT_RETRY_ATTEMPTS = 5
	MAX_MPV_LOAD_WAIT       = time.Duration(time.Second * 20)
)

type YoutubePlayer struct {
	mpvBinPath   string
	mpvInputPath string

	mpvProcess   *exec.Cmd
	mpvIsRunning bool
	mpvConn      *mpvipc.Connection
	ytService    *meta.YouTube
	mpvMutex     sync.Mutex
	mpvEvents    *eventemitter.Emitter
}

func NewYoutubePlayer(mpvBinPath, mpvInputPath string) (player *YoutubePlayer, err error) {
	player = &YoutubePlayer{
		mpvBinPath:   mpvBinPath,
		mpvInputPath: mpvInputPath,
		mpvIsRunning: false,
		ytService:    meta.NewYoutubeService(),
		mpvEvents:    eventemitter.NewEmitter(),
	}

	err = player.init()
	return
}

func (p *YoutubePlayer) init() (err error) {
	p.mpvMutex.Lock()
	defer p.mpvMutex.Unlock()

	fi, err := os.Stat(p.mpvInputPath)
	if err == nil && !fi.IsDir() {
		fmt.Printf("[YoutubePlayer] Removing existing MPV control node on %s\n", p.mpvInputPath)
		err = os.Remove(p.mpvInputPath)
		if err != nil {
			err = fmt.Errorf("[YoutubePlayer] Error removing existing input %s: %v", p.mpvInputPath, err)
			return
		}
	}

	err = p.startMpv()
	if err != nil {
		err = fmt.Errorf("[YoutubePlayer] Error starting mpv: %v ", err)
		return
	}

	attempts := 0
	for {
		// Give MPV a second or so to start and create the input socket
		time.Sleep(500 * time.Millisecond)

		fmt.Printf("[YoutubePlayer] Opening mpv ipc connection on %s\n", p.mpvInputPath)
		p.mpvConn = mpvipc.NewConnection(p.mpvInputPath)
		err = p.mpvConn.Open()
		if err != nil {
			if attempts >= MPV_INIT_RETRY_ATTEMPTS {
				err = fmt.Errorf("[YoutubePlayer] Error opening IPC connection on %s: %v ", p.mpvInputPath, err)
				return
			}
		} else {
			err = nil
			break
		}
		attempts++
	}

	// Turn on all events.
	p.mpvConn.Call("enable_event", "all")

	go func() {
		events, stopListening := p.mpvConn.NewEventListener()
		for event := range events {
			p.mpvEvents.EmitEvent(event.Name, event)
		}
		stopListening <- struct{}{}
	}()

	return
}

func (p *YoutubePlayer) startMpv() (err error) {
	fmt.Printf("[YoutubePlayer] Starting MPV %s with control %s in idle mode\n", p.mpvBinPath, p.mpvInputPath)

	command := exec.Command(p.mpvBinPath, "--no-video", "--idle", "--input-ipc-server="+p.mpvInputPath)
	p.mpvProcess = command

	err = command.Start()
	p.mpvIsRunning = err == nil

	if err != nil {
		return
	}

	go func() {
		err := command.Wait()

		fmt.Printf("[YoutubePlayer] mpv has quit: %v\n", err)

		p.mpvMutex.Lock()
		p.mpvIsRunning = false
		p.mpvProcess = nil
		p.mpvMutex.Unlock()
	}()
	return
}

func (p *YoutubePlayer) checkRunning() (err error) {
	if p.mpvIsRunning && p.mpvProcess != nil {
		return
	}
	fmt.Print("[YoutubePlayer] mpv is not running, restarting mpv\n")
	err = p.init()
	return
}

func (p *YoutubePlayer) Name() (name string) {
	return "Youtube"
}

func (p *YoutubePlayer) CanPlay(url string) (canPlay bool) {
	return youtubeURLRegex.MatchString(url)
}

func (p *YoutubePlayer) GetSongs(url string) (songs []Playable, err error) {
	lowerURL := strings.ToLower(url)
	if strings.Contains(lowerURL, "player") || strings.Contains(lowerURL, "list=") {
		var metaDatas []meta.Meta
		metaDatas, err = p.ytService.GetMetasForPlaylistURL(url)
		if err == nil {
			for _, metaData := range metaDatas {
				songs = append(songs, NewSong(metaData.Title, metaData.Duration, metaData.Source))
			}
			return
		}
		// On error, fall back to single add
	}

	metaData, err := p.ytService.GetMetaForURL(url)
	if err != nil {
		err = fmt.Errorf("[YoutubePlayer] Error getting meta data: %v", err)
		return
	}
	songs = append(songs, NewSong(metaData.Title, metaData.Duration, metaData.Source))
	return
}

func (p *YoutubePlayer) SearchSongs(searchStr string, limit int) (songs []Playable, err error) {
	metaDatas, err := p.ytService.SearchForMetas(searchStr, limit)
	if err != nil {
		err = fmt.Errorf("[YoutubePlayer] Error searching meta data: %v", err)
		return
	}

	for _, metaData := range metaDatas {
		songs = append(songs, NewSong(metaData.Title, metaData.Duration, metaData.Source))
	}
	return
}

func (p *YoutubePlayer) Play(url string) (err error) {
	p.mpvMutex.Lock()
	defer p.mpvMutex.Unlock()

	err = p.checkRunning()
	if err != nil {
		return
	}

	err = p.stop()
	if err != nil {
		return
	}

	waitForLoad := make(chan bool)
	p.mpvEvents.ListenOnce("file-loaded", func(arguments ...interface{}) {
		waitForLoad <- true
	})

	// Start an event listener to wait for the file to load.
	_, err = p.mpvConn.Call("loadfile", url, "replace")
	if err != nil {
		err = fmt.Errorf("[YoutubePlayer] Error sending loadfile command: %v", err)
		return
	}

	go func() {
		time.Sleep(MAX_MPV_LOAD_WAIT)
		waitForLoad <- false
	}()

	timeExceeded := <-waitForLoad
	if timeExceeded {
		fmt.Printf("[YoutubePlayer] Load file timeout\n")
		_, err = p.mpvConn.Call("stop")
		if err != nil {
			fmt.Printf("[YoutubePlayer] Error calling stop after timeout: %v\n", err)
		}
		err = fmt.Errorf("[YoutubePlayer] Error loading file, mpv did not respond in time")
		return
	}
	return
}

func (p *YoutubePlayer) Seek(positionSeconds int) (err error) {
	p.mpvMutex.Lock()
	defer p.mpvMutex.Unlock()

	err = p.checkRunning()
	if err != nil {
		return
	}

	err = p.mpvConn.Set("time-pos", positionSeconds)
	if err != nil {
		err = fmt.Errorf("[YoutubePlayer] Error sending time-pos property: %v", err)
	}
	return
}

func (p *YoutubePlayer) Pause(pauseState bool) (err error) {
	p.mpvMutex.Lock()
	defer p.mpvMutex.Unlock()

	err = p.checkRunning()
	if err != nil {
		return
	}

	err = p.mpvConn.Set("pause", pauseState)
	if err != nil {
		err = fmt.Errorf("[YoutubePlayer] Error sending pause state property: %v", err)
	}
	return
}

func (p *YoutubePlayer) Stop() (err error) {
	p.mpvMutex.Lock()
	defer p.mpvMutex.Unlock()

	return p.stop()
}

func (p *YoutubePlayer) stop() (err error) {
	err = p.checkRunning()
	if err != nil {
		return
	}

	_, err = p.mpvConn.Call("stop")
	if err != nil {
		err = fmt.Errorf("[YoutubePlayer] Error sending stop command: %v", err)
	}
	return
}
<|endoftext|>


<commit_before>package schedule

import (
	"strings"
	"testing"
	"time"
)

func TestGetRandomNumber(t *testing.T) {
	var tests = []struct {
		min int
		max int
	}{
		{0, 0},
		{0, 1},
		{1, 1},
		{0, 10},
		{10, 20},
	}
	for _, test := range tests {
		actual := getRandomNumber(test.min, test.max)
		if test.min > actual || actual > test.max {
			fmt := "getRandomNumber(%d, %d) == %d; not min <= actual <= max"
			t.Errorf(fmt, test.min, test.max, actual)
		}
	}
}

func TestGetRandomCommitMessage(t *testing.T) {
	var tests = []struct {
		length int
	}{
		{1}, {2}, {4}, {8},
	}
	for _, test := range tests {
		actual := getRandomCommitMessage(test.length)
		actualLength := len(strings.Split(actual, " "))
		t.Log("Message: %s (length: %d)", actual, actualLength)
		if actualLength < 0 || test.length < actualLength {
			fmt := "getRandomCommitMessage(%d) == %s (length: %d)"
			t.Errorf(fmt, test.length, actual, actualLength)
		}
	}
}

func TestGetRandomTime(t *testing.T) {
	var tests = []struct {
		date time.Time
	}{
		{time.Date(2009, time.November, 10, 0, 0, 0, 0, time.UTC)},
		{time.Date(2016, time.February, 28, 0, 0, 0, 0, time.UTC)},
	}
	for _, test := range tests {
		actual := getRandomTime(test.date)
		t.Log("Date: %v and date with random time: %v", test.date, actual)
		dayBefore := test.date.AddDate(0, 0, -1)
		dayAfter := test.date.AddDate(0, 0, 1)
		if actual.After(dayAfter) || actual.Before(dayBefore) {
			fmt := "getRandomTime(%v) == %v; not dayBefore < actual < dayAfter"
			t.Errorf(fmt, test.date, actual)
		}
	}
}
<commit_msg>Add further tests for random schedule.<commit_after>package schedule

import (
	"bufio"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"
)

func TestGetRandomNumber(t *testing.T) {
	var tests = []struct {
		min int
		max int
	}{
		{0, 0},
		{0, 1},
		{1, 1},
		{0, 10},
		{10, 20},
	}
	for _, test := range tests {
		actual := getRandomNumber(test.min, test.max)
		if test.min > actual || actual > test.max {
			fmt := "getRandomNumber(%d, %d) == %d; not min <= actual <= max"
			t.Errorf(fmt, test.min, test.max, actual)
		}
	}
}

func TestGetRandomCommitMessage(t *testing.T) {
	var tests = []struct {
		length int
	}{
		{1}, {2}, {4}, {8},
	}
	for _, test := range tests {
		actual := getRandomCommitMessage(test.length)
		actualLength := len(strings.Split(actual, " "))
		t.Logf("Message: %s (length: %d)", actual, actualLength)
		if actualLength < 0 || test.length < actualLength {
			fmt := "getRandomCommitMessage(%d) == %s (length: %d)"
			t.Errorf(fmt, test.length, actual, actualLength)
		}
	}
}

func TestGetRandomTime(t *testing.T) {
	var tests = []struct {
		date time.Time
	}{
		{time.Date(2009, time.November, 10, 0, 0, 0, 0, time.UTC)},
		{time.Date(2016, time.February, 28, 0, 0, 0, 0, time.UTC)},
	}
	for _, test := range tests {
		actual := getRandomTime(test.date)
		t.Logf("Date: %v and date with random time: %v", test.date, actual)
		dayBefore := test.date.AddDate(0, 0, -1)
		dayAfter := test.date.AddDate(0, 0, 1)
		if actual.After(dayAfter) || actual.Before(dayBefore) {
			fmt := "getRandomTime(%v) == %v; not dayBefore < actual < dayAfter"
			t.Errorf(fmt, test.date, actual)
		}
	}
}

func TestCreateFileInDir(t *testing.T) {
	testDir := "testDir"
	os.MkdirAll(testDir, 0755)
	filename := createFileInDir(testDir)
	if _, err := os.Stat(filepath.Join(testDir, filename)); os.IsNotExist(err) {
		t.Errorf("Expected file (%s) to be created", filename)
	}
	os.RemoveAll(testDir)
}

func TestRandomCommits(t *testing.T) {
	var tests = []struct {
		date       time.Time
		numCommits int
	}{
		{time.Date(2009, time.November, 10, 0, 0, 0, 0, time.UTC), 1},
		{time.Date(2016, time.February, 28, 0, 0, 0, 0, time.UTC), 2},
		{time.Date(2016, time.February, 29, 0, 0, 0, 0, time.UTC), 8},
	}
	for _, test := range tests {
		actual := RandomCommits(test.date, test.numCommits)
		commitCount := 0
		for commit := range actual {
			if commit.dateTime.Day() != test.date.Day() {
				t.Error("Commit has a wrong date")
			}
			commitCount++
		}
		if commitCount != test.numCommits {
			t.Errorf("Expected %d commits, but got %d", test.numCommits, commitCount)
		}
	}
}

func TestRandomSchedule(t *testing.T) {
	var tests = []struct {
		min      int
		max      int
		location string
	}{
		{1, 1, "testWithOneCommit"},
		{2, 8, "testWithUpToEightCommits"},
		{10, 100, "testWithUpToOneHundredCommits"},
	}
	for _, test := range tests {
		currentDir, _ := os.Getwd()
		testDir := filepath.Join(currentDir, test.location)
		// t.Logf("test")
		RandomSchedule(test.min, test.max, test.location)
		numCommits := getNumberOfGitLogLines(testDir)
		if numCommits < test.min || test.max < test.max {
			fmt := "Number of commits: %d; should be in the range of %d to %d"
			t.Errorf(fmt, numCommits, test.min, test.max)
		}
		// os.RemoveAll(testDir)
	}
}

func getNumberOfGitLogLines(gitLocation string) int {
	log := filepath.Join(gitLocation, ".git", "logs", "refs", "heads", "master")
	logFile, _ := os.Open(log)
	defer logFile.Close()
	logScanner := bufio.NewScanner(logFile)
	lineCount := 0
	for logScanner.Scan() {
		lineCount++
	}
	return lineCount
}
<|endoftext|>


<commit_before>package main // import "cirello.io/gochatbot/plugins/gochatbot-plugin-sentimental"

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
	"strconv"
	"strings"
	"time"

	"cirello.io/HumorChecker"
	"cirello.io/gochatbot/messages"
	"cirello.io/gochatbot/plugins"
)

const baseURL = "https://www.reddit.com/r/"

type SentimentalPlugin struct {
	comm    *plugins.Comm
	botName string
	quiet   bool
}

type userScore struct {
	Messages int
	Score    float64
	Average  float64
}

func main() {
	rpcBind := os.Getenv("GOCHATBOT_RPC_BIND")
	if rpcBind == "" {
		log.Fatal("GOCHATBOT_RPC_BIND empty or not set. Cannot start plugin.")
	}
	botName := os.Getenv("GOCHATBOT_NAME")
	if botName == "" {
		log.Fatal("GOCHATBOT_NAME empty or not set. Cannot start plugin.")
	}
	quiet, err := strconv.ParseBool(os.Getenv("GOCHATBOT_SENTIMENTAL_QUIET"))
	if err != nil {
		log.Println("error reading variable GOCHATBOT_SENTIMENTAL_QUIET, defaulting to false")
		quiet = false
	}

	r := &SentimentalPlugin{comm: plugins.NewComm(rpcBind), botName: botName, quiet: quiet}
	for {
		in, err := r.comm.Pop()
		if err != nil {
			log.Println("sentimental: error popping message from gochatbot:", err)
			continue
		}
		if in.Message == "" {
			time.Sleep(1 * time.Second)
		}
		if err := r.parseMessage(in); err != nil {
			log.Println("sentimental: error parsing message:", err)
		}
	}
}

func (r SentimentalPlugin) helpMessage() string {
	helpMsg := fmt.Sprintln(r.botName, "check <user>")
	helpMsg = fmt.Sprintln(helpMsg, r.botName, "check everyone")

	return helpMsg
}

func (r *SentimentalPlugin) parseMessage(in *messages.Message) error {
	msg := strings.TrimSpace(in.Message)
	checkPrefix := r.botName + " check on"
	if msg == "" || (strings.HasPrefix(msg, r.botName) && !strings.HasPrefix(msg, checkPrefix)) {
		return nil
	}

	if strings.HasPrefix(msg, checkPrefix) {
		scorecard, err := r.readUsersSentiment()
		if err != nil {
			return err
		}
		if len(scorecard) == 0 {
			return r.comm.Send(&messages.Message{
				Room:       in.Room,
				ToUserID:   in.FromUserID,
				ToUserName: in.FromUserName,
				Message:    "no sentiment on anybody collected yet",
			})
		}
		username := strings.TrimPrefix(strings.TrimSpace(msg), checkPrefix)
		if username == "everyone" {
			var out string
			for u, sc := range scorecard {
				out = fmt.Sprintln(out, u, "has a happiness average of", sc.Average)
			}
			return r.comm.Send(&messages.Message{
				Room:       in.Room,
				ToUserID:   in.FromUserID,
				ToUserName: in.FromUserName,
				Message:    out,
			})
		}
		if _, ok := scorecard[username]; !ok && username != "everyone" {
			return r.comm.Send(&messages.Message{
				Room:       in.Room,
				ToUserID:   in.FromUserID,
				ToUserName: in.FromUserName,
				Message:    fmt.Sprintf("%s has no happiness average yet", username),
			})
		}

		return r.comm.Send(&messages.Message{
			Room:       in.Room,
			ToUserID:   in.FromUserID,
			ToUserName: in.FromUserName,
			Message:    fmt.Sprintln(username, "has a happiness average of", scorecard[username].Average),
		})

	}

	if in.FromUserName == "" {
		log.Println("sentimental: got empty username")
		return nil
	}

	scorecard, err := r.readUsersSentiment()
	if err != nil {
		return err
	}

	if _, ok := scorecard[in.FromUserName]; !ok {
		scorecard[in.FromUserName] = userScore{}
	}

	score := HumorChecker.Analyze(in.Message)
	sc := r.updateScore(scorecard[in.FromUserName], score)
	scorecard[in.FromUserName] = sc

	if err := r.storeUsersSentiment(scorecard); err != nil {
		return err
	}

	log.Printf("sentimental: %s now has %v / %v", in.FromUserName, sc.Score, sc.Average)

	if score.Score < -2 && !r.quiet {
		return r.comm.Send(&messages.Message{
			Room:       in.Room,
			ToUserID:   in.FromUserID,
			ToUserName: in.FromUserName,
			Message:    fmt.Sprintln("stay positive", in.FromUserName),
		})
	}

	return nil
}

func (r *SentimentalPlugin) readUsersSentiment() (map[string]userScore, error) {
	scorecard := make(map[string]userScore)
	b, err := r.comm.MemoryRead("sentimental", "userScoreCard")
	if err != nil {
		return nil, err
	}
	if err := json.Unmarshal(b, &scorecard); len(b) > 0 && err != nil {
		return nil, err
	}
	return scorecard, nil
}

func (r *SentimentalPlugin) updateScore(sc userScore, score HumorChecker.FullScore) userScore {
	sc.Messages++
	sc.Score += score.Score
	sc.Average = sc.Score / float64(sc.Messages)
	return sc
}

func (r *SentimentalPlugin) storeUsersSentiment(scorecard map[string]userScore) error {
	record, err := json.Marshal(scorecard)
	if err != nil {
		return err
	}
	return r.comm.MemorySave("sentimental", "userScoreCard", record)
}
<commit_msg>sentimental plugin: fix username detection<commit_after>package main // import "cirello.io/gochatbot/plugins/gochatbot-plugin-sentimental"

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
	"strconv"
	"strings"
	"time"

	"cirello.io/HumorChecker"
	"cirello.io/gochatbot/messages"
	"cirello.io/gochatbot/plugins"
)

const baseURL = "https://www.reddit.com/r/"

type SentimentalPlugin struct {
	comm    *plugins.Comm
	botName string
	quiet   bool
}

type userScore struct {
	Messages int
	Score    float64
	Average  float64
}

func main() {
	rpcBind := os.Getenv("GOCHATBOT_RPC_BIND")
	if rpcBind == "" {
		log.Fatal("GOCHATBOT_RPC_BIND empty or not set. Cannot start plugin.")
	}
	botName := os.Getenv("GOCHATBOT_NAME")
	if botName == "" {
		log.Fatal("GOCHATBOT_NAME empty or not set. Cannot start plugin.")
	}
	quiet, err := strconv.ParseBool(os.Getenv("GOCHATBOT_SENTIMENTAL_QUIET"))
	if err != nil {
		log.Println("error reading variable GOCHATBOT_SENTIMENTAL_QUIET, defaulting to false")
		quiet = false
	}

	r := &SentimentalPlugin{comm: plugins.NewComm(rpcBind), botName: botName, quiet: quiet}
	for {
		in, err := r.comm.Pop()
		if err != nil {
			log.Println("sentimental: error popping message from gochatbot:", err)
			continue
		}
		if in.Message == "" {
			time.Sleep(1 * time.Second)
		}
		if err := r.parseMessage(in); err != nil {
			log.Println("sentimental: error parsing message:", err)
		}
	}
}

func (r SentimentalPlugin) helpMessage() string {
	helpMsg := fmt.Sprintln(r.botName, "check <user>")
	helpMsg = fmt.Sprintln(helpMsg, r.botName, "check everyone")

	return helpMsg
}

func (r *SentimentalPlugin) parseMessage(in *messages.Message) error {
	msg := strings.TrimSpace(in.Message)
	checkPrefix := r.botName + " check on "
	if msg == "" || (strings.HasPrefix(msg, r.botName) && !strings.HasPrefix(msg, checkPrefix)) {
		return nil
	}

	if strings.HasPrefix(msg, checkPrefix) {
		scorecard, err := r.readUsersSentiment()
		if err != nil {
			return err
		}
		if len(scorecard) == 0 {
			return r.comm.Send(&messages.Message{
				Room:       in.Room,
				ToUserID:   in.FromUserID,
				ToUserName: in.FromUserName,
				Message:    "no sentiment on anybody collected yet",
			})
		}
		username := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(msg), checkPrefix))
		log.Printf("sentimental: checking user `%s`", username)
		if username == "everyone" {
			var out string
			for u, sc := range scorecard {
				out = fmt.Sprintln(out, u, "has a happiness average of", sc.Average)
			}
			return r.comm.Send(&messages.Message{
				Room:       in.Room,
				ToUserID:   in.FromUserID,
				ToUserName: in.FromUserName,
				Message:    out,
			})
		}
		if _, ok := scorecard[username]; !ok && username != "everyone" {
			return r.comm.Send(&messages.Message{
				Room:       in.Room,
				ToUserID:   in.FromUserID,
				ToUserName: in.FromUserName,
				Message:    fmt.Sprintf("%s has no happiness average yet", username),
			})
		}

		return r.comm.Send(&messages.Message{
			Room:       in.Room,
			ToUserID:   in.FromUserID,
			ToUserName: in.FromUserName,
			Message:    fmt.Sprintln(username, "has a happiness average of", scorecard[username].Average),
		})

	}

	if in.FromUserName == "" {
		log.Println("sentimental: got empty username")
		return nil
	}

	scorecard, err := r.readUsersSentiment()
	if err != nil {
		return err
	}

	if _, ok := scorecard[in.FromUserName]; !ok {
		scorecard[in.FromUserName] = userScore{}
	}

	score := HumorChecker.Analyze(in.Message)
	sc := r.updateScore(scorecard[in.FromUserName], score)
	scorecard[in.FromUserName] = sc

	if err := r.storeUsersSentiment(scorecard); err != nil {
		return err
	}

	log.Printf("sentimental: %s now has %v / %v", in.FromUserName, sc.Score, sc.Average)

	if score.Score < -2 && !r.quiet {
		return r.comm.Send(&messages.Message{
			Room:       in.Room,
			ToUserID:   in.FromUserID,
			ToUserName: in.FromUserName,
			Message:    fmt.Sprintln("stay positive", in.FromUserName),
		})
	}

	return nil
}

func (r *SentimentalPlugin) readUsersSentiment() (map[string]userScore, error) {
	scorecard := make(map[string]userScore)
	b, err := r.comm.MemoryRead("sentimental", "userScoreCard")
	if err != nil {
		return nil, err
	}
	if err := json.Unmarshal(b, &scorecard); len(b) > 0 && err != nil {
		return nil, err
	}
	return scorecard, nil
}

func (r *SentimentalPlugin) updateScore(sc userScore, score HumorChecker.FullScore) userScore {
	sc.Messages++
	sc.Score += score.Score
	sc.Average = sc.Score / float64(sc.Messages)
	return sc
}

func (r *SentimentalPlugin) storeUsersSentiment(scorecard map[string]userScore) error {
	record, err := json.Marshal(scorecard)
	if err != nil {
		return err
	}
	return r.comm.MemorySave("sentimental", "userScoreCard", record)
}
<|endoftext|>


<commit_before>//  Copyright (c) 2012 Couchbase, Inc.
//  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 rest

import (
	"crypto/sha1"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"runtime"
	"runtime/pprof"
	"strconv"

	"github.com/couchbaselabs/sync_gateway/base"
	"github.com/couchbaselabs/sync_gateway/db"
)

const ServerName = "Couchbase Sync Gateway"
const VersionNumberString = "0.93"
const VersionString = ServerName + "/" + VersionNumberString

// HTTP handler for the root ("/")
func (h *handler) handleRoot() error {
	response := map[string]interface{}{
		"couchdb": "Welcome",
		"version": VersionString,
		"vendor":  db.Body{"name": ServerName, "version": VersionNumberString},
	}
	if h.privs == adminPrivs {
		response["ADMIN"] = true
	}
	h.writeJSON(response)
	return nil
}

func (h *handler) handleAllDbs() error {
	h.writeJSON(h.server.AllDatabaseNames())
	return nil
}

func (h *handler) handleCompact() error {
	revsDeleted, err := h.db.Compact()
	if err != nil {
		return err
	}
	h.writeJSON(db.Body{"revs": revsDeleted})
	return nil
}

func (h *handler) handleVacuum() error {
	attsDeleted, err := db.VacuumAttachments(h.db.Bucket)
	if err != nil {
		return err
	}
	h.writeJSON(db.Body{"atts": attsDeleted})
	return nil
}

func (h *handler) instanceStartTime() json.Number {
	return json.Number(strconv.FormatInt(h.db.StartTime.UnixNano()/1000, 10))
}

func (h *handler) handleGetDB() error {
	if h.rq.Method == "HEAD" {
		return nil
	}
	lastSeq, err := h.db.LastSequence()
	if err != nil {
		return err
	}
	response := db.Body{
		"db_name":              h.db.Name,
		"doc_count":            h.db.DocCount(),
		"update_seq":           lastSeq,
		"committed_update_seq": lastSeq,
		"instance_start_time":  h.instanceStartTime(),
		"compact_running":      false, // TODO: Implement this
		"purge_seq":            0,     // TODO: Should track this value
		"disk_format_version":  0,     // Probably meaningless, but add for compatibility
	}
	h.writeJSON(response)
	return nil
}

func (h *handler) handleEFC() error { // Handles _ensure_full_commit.
	// no-op. CouchDB's replicator sends this, so don't barf. Status must be 201.
	h.writeJSONStatus(http.StatusCreated, db.Body{
		"ok": true,
		"instance_start_time": h.instanceStartTime(),
	})
	return nil
}

func (h *handler) handleDesign() error {
	designDocID := h.PathVar("docid")
	if designDocID == "sync_gateway" {
		// we serve this content here so that CouchDB 1.2 has something to
		// hash into the replication-id, to correspond to our filter.
		filter := "ok"
		if h.db.DatabaseContext.ChannelMapper != nil {
			hash := sha1.New()
			io.WriteString(hash, h.db.DatabaseContext.ChannelMapper.Function())
			filter = fmt.Sprint(hash.Sum(nil))
		}
		h.writeJSON(db.Body{"filters": db.Body{"bychannel": filter}})
		return nil
	} else {
		h.SetPathVar("docid", "_design/"+designDocID)
		return h.handleGetDoc()
	}
}

func (h *handler) handlePutDesign() error {
	designDocID := h.PathVar("docid")
	if designDocID == "sync_gateway" {
		return base.HTTPErrorf(http.StatusForbidden, "forbidden")
	} else {
		h.SetPathVar("docid", "_design/"+designDocID)
		if h.rq.Method == "DELETE" {
			return h.handleDeleteDoc()
		} else {
			return h.handlePutDoc()
		}
	}
}

// ADMIN API to turn Go CPU profiling on/off
func (h *handler) handleProfiling() error {
	profileName := h.PathVar("name")
	var params struct {
		File string `json:"file"`
	}
	body, err := h.readBody()
	if err != nil {
		return err
	}
	if len(body) > 0 {
		if err = json.Unmarshal(body, &params); err != nil {
			return err
		}
	}

	if params.File != "" {
		f, err := os.Create(params.File)
		if err != nil {
			return err
		}
		if profileName != "" {
			defer f.Close()
			if profile := pprof.Lookup(profileName); profile != nil {
				profile.WriteTo(f, 0)
				base.Log("Wrote %s profile to %s", profileName, params.File)
			} else {
				return base.HTTPErrorf(http.StatusNotFound, "No such profile %q", profileName)
			}
		} else {
			base.Log("Starting CPU profile to %s ...", params.File)
			pprof.StartCPUProfile(f)
		}
	} else {
		if profileName != "" {
			return base.HTTPErrorf(http.StatusBadRequest, "Missing JSON 'file' parameter")
		} else {
			base.Log("...ending CPU profile.")
			pprof.StopCPUProfile()
		}
	}
	return nil
}

// ADMIN API to dump Go heap profile
func (h *handler) handleHeapProfiling() error {
	var params struct {
		File string `json:"file"`
	}
	body, err := h.readBody()
	if err != nil {
		return err
	}
	if err = json.Unmarshal(body, &params); err != nil {
		return err
	}

	base.Log("Dumping heap profile to %s ...", params.File)
	f, err := os.Create(params.File)
	if err != nil {
		return err
	}
	pprof.WriteHeapProfile(f)
	f.Close()
	return nil
}

type stats struct {
	MemStats runtime.MemStats
}

// ADMIN API to expose runtime and other stats
func (h *handler) handleStats() error {
	st := stats{}
	runtime.ReadMemStats(&st.MemStats)

	h.writeJSON(st)
	return nil
}
<commit_msg>Don't return doc_count in GET /db: it's too expensive to compute<commit_after>//  Copyright (c) 2012 Couchbase, Inc.
//  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 rest

import (
	"crypto/sha1"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"runtime"
	"runtime/pprof"
	"strconv"

	"github.com/couchbaselabs/sync_gateway/base"
	"github.com/couchbaselabs/sync_gateway/db"
)

const ServerName = "Couchbase Sync Gateway"
const VersionNumberString = "0.93"
const VersionString = ServerName + "/" + VersionNumberString

// HTTP handler for the root ("/")
func (h *handler) handleRoot() error {
	response := map[string]interface{}{
		"couchdb": "Welcome",
		"version": VersionString,
		"vendor":  db.Body{"name": ServerName, "version": VersionNumberString},
	}
	if h.privs == adminPrivs {
		response["ADMIN"] = true
	}
	h.writeJSON(response)
	return nil
}

func (h *handler) handleAllDbs() error {
	h.writeJSON(h.server.AllDatabaseNames())
	return nil
}

func (h *handler) handleCompact() error {
	revsDeleted, err := h.db.Compact()
	if err != nil {
		return err
	}
	h.writeJSON(db.Body{"revs": revsDeleted})
	return nil
}

func (h *handler) handleVacuum() error {
	attsDeleted, err := db.VacuumAttachments(h.db.Bucket)
	if err != nil {
		return err
	}
	h.writeJSON(db.Body{"atts": attsDeleted})
	return nil
}

func (h *handler) instanceStartTime() json.Number {
	return json.Number(strconv.FormatInt(h.db.StartTime.UnixNano()/1000, 10))
}

func (h *handler) handleGetDB() error {
	if h.rq.Method == "HEAD" {
		return nil
	}
	lastSeq, err := h.db.LastSequence()
	if err != nil {
		return err
	}
	response := db.Body{
		"db_name":              h.db.Name,
		"update_seq":           lastSeq,
		"committed_update_seq": lastSeq,
		"instance_start_time":  h.instanceStartTime(),
		"compact_running":      false, // TODO: Implement this
		"purge_seq":            0,     // TODO: Should track this value
		"disk_format_version":  0,     // Probably meaningless, but add for compatibility
		//"doc_count":          h.db.DocCount(), // Removed: too expensive to compute (#278)
	}
	h.writeJSON(response)
	return nil
}

func (h *handler) handleEFC() error { // Handles _ensure_full_commit.
	// no-op. CouchDB's replicator sends this, so don't barf. Status must be 201.
	h.writeJSONStatus(http.StatusCreated, db.Body{
		"ok": true,
		"instance_start_time": h.instanceStartTime(),
	})
	return nil
}

func (h *handler) handleDesign() error {
	designDocID := h.PathVar("docid")
	if designDocID == "sync_gateway" {
		// we serve this content here so that CouchDB 1.2 has something to
		// hash into the replication-id, to correspond to our filter.
		filter := "ok"
		if h.db.DatabaseContext.ChannelMapper != nil {
			hash := sha1.New()
			io.WriteString(hash, h.db.DatabaseContext.ChannelMapper.Function())
			filter = fmt.Sprint(hash.Sum(nil))
		}
		h.writeJSON(db.Body{"filters": db.Body{"bychannel": filter}})
		return nil
	} else {
		h.SetPathVar("docid", "_design/"+designDocID)
		return h.handleGetDoc()
	}
}

func (h *handler) handlePutDesign() error {
	designDocID := h.PathVar("docid")
	if designDocID == "sync_gateway" {
		return base.HTTPErrorf(http.StatusForbidden, "forbidden")
	} else {
		h.SetPathVar("docid", "_design/"+designDocID)
		if h.rq.Method == "DELETE" {
			return h.handleDeleteDoc()
		} else {
			return h.handlePutDoc()
		}
	}
}

// ADMIN API to turn Go CPU profiling on/off
func (h *handler) handleProfiling() error {
	profileName := h.PathVar("name")
	var params struct {
		File string `json:"file"`
	}
	body, err := h.readBody()
	if err != nil {
		return err
	}
	if len(body) > 0 {
		if err = json.Unmarshal(body, &params); err != nil {
			return err
		}
	}

	if params.File != "" {
		f, err := os.Create(params.File)
		if err != nil {
			return err
		}
		if profileName != "" {
			defer f.Close()
			if profile := pprof.Lookup(profileName); profile != nil {
				profile.WriteTo(f, 0)
				base.Log("Wrote %s profile to %s", profileName, params.File)
			} else {
				return base.HTTPErrorf(http.StatusNotFound, "No such profile %q", profileName)
			}
		} else {
			base.Log("Starting CPU profile to %s ...", params.File)
			pprof.StartCPUProfile(f)
		}
	} else {
		if profileName != "" {
			return base.HTTPErrorf(http.StatusBadRequest, "Missing JSON 'file' parameter")
		} else {
			base.Log("...ending CPU profile.")
			pprof.StopCPUProfile()
		}
	}
	return nil
}

// ADMIN API to dump Go heap profile
func (h *handler) handleHeapProfiling() error {
	var params struct {
		File string `json:"file"`
	}
	body, err := h.readBody()
	if err != nil {
		return err
	}
	if err = json.Unmarshal(body, &params); err != nil {
		return err
	}

	base.Log("Dumping heap profile to %s ...", params.File)
	f, err := os.Create(params.File)
	if err != nil {
		return err
	}
	pprof.WriteHeapProfile(f)
	f.Close()
	return nil
}

type stats struct {
	MemStats runtime.MemStats
}

// ADMIN API to expose runtime and other stats
func (h *handler) handleStats() error {
	st := stats{}
	runtime.ReadMemStats(&st.MemStats)

	h.writeJSON(st)
	return nil
}
<|endoftext|>


<commit_before>package multicast

import (
	"fmt"
	"log"
	"net"
	"os"
	"syscall"
	"sync"
	"testing"
	"time"
)

func TestMulticast(t *testing.T) {
	t.SkipNow()

        mc1 := JoinMulticast()
        if mc1 == nil {
                t.Fatal()
        } else {
		log.Println("Joined and listening to multicast IP", mc1.Addr.IP, "on port", mc1.Addr.Port)
	}

	// Enable Multicast looping for testing
	f, err := mc1.Conn.File()
	err = syscall.SetsockoptInt(int(f.Fd()), syscall.IPPROTO_IP, syscall.IP_MULTICAST_LOOP, 1)
	if err != nil {
		t.Fatal(err)
	}

	var wg sync.WaitGroup

	wg.Add(1)
	// Sender node
        go func(mc *Multicast) {
		defer wg.Done()

		// Give some time to the other goroutines to build up
		time.Sleep(time.Millisecond * 200)

                msg := "Multicast Hello World!"
                n, e := mc.write(([]byte)(msg))
                if e != nil {
                        t.Fatal(e)
                }
		log.Printf("--> Sent %d bytes: %s\n", n, msg)
                e = mc.LeaveMulticast()

                if e != nil {
                        t.Fatal(e)
                } else {
			log.Println("Leaving multicast IP", mc.Addr.IP, "on port", mc.Addr.Port)
		}
        }(mc1)

	nNodes := 9
	for i := 0; i < nNodes; i++ {
		wg.Add(1)
		go receiverNode(t, &wg, i+1)
	}
	wg.Wait()
}

func receiverNode(t *testing.T, wg *sync.WaitGroup, id int) {
	defer wg.Done()

        mc := JoinMulticast()
        if mc == nil {
                t.Fatal()
        } else {
		log.Println("Joined and listening to multicast IP", mc.Addr.IP, "on port", mc.Addr.Port)
	}

        b := make([]byte, 1000)
        n, e := mc.read(b)
        if e != nil {
                t.Fatal(e)
        }
        if n > 0 {
                log.Println("Node", id , "<-- Received", n, "bytes:", string(b))
        } else {
                log.Println("Received 0 bytes")
        }

        e = mc.LeaveMulticast()
        if e != nil {
                t.Fatal(e)
        } else {
		log.Println("Node", id, "leaving multicast IP", mc.Addr.IP, "on port", mc.Addr.Port)
	}
}

func TestMulticastAnnouncing(t *testing.T) {
	mc1 := JoinMulticast()
        if mc1 == nil {
                t.Fatal()
        }

	// Enable Multicast looping for testing
	f, err := mc1.Conn.File()
	err = syscall.SetsockoptInt(int(f.Fd()), syscall.IPPROTO_IP, syscall.IP_MULTICAST_LOOP, 1)
	if err != nil {
		t.Fatal(err)
	}

	mc1.Period = 1
	mc1.StartMulticast()
	/*
	go func(mc *Multicast) {
		// Give some time to the other goroutines to build up
		time.Sleep(time.Millisecond * 200)

                n, e := mc.write(([]byte)(msg))
                if e != nil {
                        t.Fatal(e)
                }
		log.Printf("--> Sent %d bytes: %s\n", n, msg)
                e = mc.LeaveMulticast()

                if e != nil {
                        t.Fatal(e)
                }
        }(mc1)
*/

        mc2 := JoinMulticast()
        if mc2 == nil {
                t.Fatal()
        }

        b := make([]byte, 1000)
        n, e := mc2.read(b)
        if e != nil {
                t.Fatal(e)
        }
        if n > 0 {
                log.Println("<-- Received", n, "bytes:", string(b[:n]))
		host, _ := os.Hostname()
		addrs, _ := net.LookupIP(host)
		if string(addressesToMsg (addrs)) != string(b[:n]) {
			// Print bytes, not string, to see if any padding occurred
			fmt.Printf("%x\n",string(addressesToMsg(addrs)))
			fmt.Printf("%x\n",string(b[:n]))
			t.Fatal("Multicast Hello message is incorrectly formatted")
		}
        } else {
                log.Println("Received 0 bytes")
        }

        e = mc2.LeaveMulticast()
        if e != nil {
                t.Fatal(e)
        }
}
<commit_msg>Multicast: removed commented code<commit_after>package multicast

import (
	"fmt"
	"log"
	"net"
	"os"
	"syscall"
	"sync"
	"testing"
	"time"
)

func TestMulticast(t *testing.T) {
	t.SkipNow()

        mc1 := JoinMulticast()
        if mc1 == nil {
                t.Fatal()
        } else {
		log.Println("Joined and listening to multicast IP", mc1.Addr.IP, "on port", mc1.Addr.Port)
	}

	// Enable Multicast looping for testing
	f, err := mc1.Conn.File()
	err = syscall.SetsockoptInt(int(f.Fd()), syscall.IPPROTO_IP, syscall.IP_MULTICAST_LOOP, 1)
	if err != nil {
		t.Fatal(err)
	}

	var wg sync.WaitGroup

	wg.Add(1)
	// Sender node
        go func(mc *Multicast) {
		defer wg.Done()

		// Give some time to the other goroutines to build up
		time.Sleep(time.Millisecond * 200)

                msg := "Multicast Hello World!"
                n, e := mc.write(([]byte)(msg))
                if e != nil {
                        t.Fatal(e)
                }
		log.Printf("--> Sent %d bytes: %s\n", n, msg)
                e = mc.LeaveMulticast()

                if e != nil {
                        t.Fatal(e)
                } else {
			log.Println("Leaving multicast IP", mc.Addr.IP, "on port", mc.Addr.Port)
		}
        }(mc1)

	nNodes := 9
	for i := 0; i < nNodes; i++ {
		wg.Add(1)
		go receiverNode(t, &wg, i+1)
	}
	wg.Wait()
}

func receiverNode(t *testing.T, wg *sync.WaitGroup, id int) {
	defer wg.Done()

        mc := JoinMulticast()
        if mc == nil {
                t.Fatal()
        } else {
		log.Println("Joined and listening to multicast IP", mc.Addr.IP, "on port", mc.Addr.Port)
	}

        b := make([]byte, 1000)
        n, e := mc.read(b)
        if e != nil {
                t.Fatal(e)
        }
        if n > 0 {
                log.Println("Node", id , "<-- Received", n, "bytes:", string(b))
        } else {
                log.Println("Received 0 bytes")
        }

        e = mc.LeaveMulticast()
        if e != nil {
                t.Fatal(e)
        } else {
		log.Println("Node", id, "leaving multicast IP", mc.Addr.IP, "on port", mc.Addr.Port)
	}
}

func TestMulticastAnnouncing(t *testing.T) {
	mc1 := JoinMulticast()
        if mc1 == nil {
                t.Fatal()
        }

	// Enable Multicast looping for testing
	f, err := mc1.Conn.File()
	err = syscall.SetsockoptInt(int(f.Fd()), syscall.IPPROTO_IP, syscall.IP_MULTICAST_LOOP, 1)
	if err != nil {
		t.Fatal(err)
	}

	mc1.Period = 1
	mc1.StartMulticast()

        mc2 := JoinMulticast()
        if mc2 == nil {
                t.Fatal()
        }

        b := make([]byte, 1000)
        n, e := mc2.read(b)
        if e != nil {
                t.Fatal(e)
        }
        if n > 0 {
                log.Println("<-- Received", n, "bytes:", string(b[:n]))
		host, _ := os.Hostname()
		addrs, _ := net.LookupIP(host)
		if string(addressesToMsg (addrs)) != string(b[:n]) {
			// Print bytes, not string, to see if any padding occurred
			fmt.Printf("%x\n",string(addressesToMsg(addrs)))
			fmt.Printf("%x\n",string(b[:n]))
			t.Fatal("Multicast Hello message is incorrectly formatted")
		}
        } else {
                log.Println("Received 0 bytes")
        }

        e = mc2.LeaveMulticast()
        if e != nil {
                t.Fatal(e)
        }
}
<|endoftext|>


<commit_before>// Package dogstatsd implement Datadog extended StatsD protocol for github.com/rs/xstats
package dogstatsd

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"strings"
	"time"

	"github.com/rs/xstats"
)

// Inspired by https://github.com/streadway/handy statsd package

type sender chan string

// MaxPacketLen is the number of bytes filled before a packet is flushed before
// the reporting interval.
const maxPacketLen = 2 ^ 15

var tick = time.Tick

// New creates a datadog statsd sender that emit observations in the statsd
// protocol to the passed writer. Observations are buffered for the report
// interval or until the buffer exceeds a max packet size, whichever comes
// first.
func New(w io.Writer, reportInterval time.Duration) xstats.Sender {
	s := make(chan string)
	go fwd(w, reportInterval, s)
	return sender(s)
}

// Gauge implements xstats.Sender interface
func (s sender) Gauge(stat string, value float64, tags ...string) {
	s <- fmt.Sprintf("%s:%f|g%s\n", stat, value, t(tags))
}

// Count implements xstats.Sender interface
func (s sender) Count(stat string, count float64, tags ...string) {
	s <- fmt.Sprintf("%s:%f|c%s\n", stat, count, t(tags))
}

// Histogram implements xstats.Sender interface
func (s sender) Histogram(stat string, value float64, tags ...string) {
	s <- fmt.Sprintf("%s:%f|h%s\n", stat, value, t(tags))
}

// Timing implements xstats.Sender interface
func (s sender) Timing(stat string, duration time.Duration, tags ...string) {
	s <- fmt.Sprintf("%s:%f|ms%s\n", stat, duration.Seconds()*1000, t(tags))
}

// Generate a DogStatsD tag suffix
func t(tags []string) string {
	t := ""
	if len(tags) > 0 {
		t = "|#" + strings.Join(tags, ",")
	}
	return t
}

func fwd(w io.Writer, reportInterval time.Duration, c <-chan string) {
	buf := &bytes.Buffer{}
	tick := tick(reportInterval)
	for {
		select {
		case s := <-c:
			buf.Write([]byte(s))
			if buf.Len() > maxPacketLen {
				flush(w, buf)
			}

		case <-tick:
			flush(w, buf)
		}
	}
}

func flush(w io.Writer, buf *bytes.Buffer) {
	if buf.Len() <= 0 {
		return
	}
	if _, err := w.Write(buf.Bytes()); err != nil {
		log.Printf("error: could not write to statsd: %v", err)
	}
	buf.Reset()
}
<commit_msg>dogstatsd: add Close that flushes the insternal buffer<commit_after>// Package dogstatsd implement Datadog extended StatsD protocol for github.com/rs/xstats
package dogstatsd

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"strings"
	"time"

	"github.com/rs/xstats"
)

// Inspired by https://github.com/streadway/handy statsd package

type sender struct {
	c    chan string
	quit chan struct{}
	done chan struct{}
}

// MaxPacketLen is the number of bytes filled before a packet is flushed before
// the reporting interval.
const maxPacketLen = 2 ^ 15

var tick = time.Tick

// New creates a datadog statsd sender that emit observations in the statsd
// protocol to the passed writer. Observations are buffered for the report
// interval or until the buffer exceeds a max packet size, whichever comes
// first.
func New(w io.Writer, reportInterval time.Duration) xstats.Sender {
	s := &sender{
		c:    make(chan string),
		quit: make(chan struct{}),
		done: make(chan struct{}),
	}
	go s.fwd(w, reportInterval)
	return s
}

// Gauge implements xstats.Sender interface
func (s *sender) Gauge(stat string, value float64, tags ...string) {
	s.c <- fmt.Sprintf("%s:%f|g%s\n", stat, value, t(tags))
}

// Count implements xstats.Sender interface
func (s *sender) Count(stat string, count float64, tags ...string) {
	s.c <- fmt.Sprintf("%s:%f|c%s\n", stat, count, t(tags))
}

// Histogram implements xstats.Sender interface
func (s *sender) Histogram(stat string, value float64, tags ...string) {
	s.c <- fmt.Sprintf("%s:%f|h%s\n", stat, value, t(tags))
}

// Timing implements xstats.Sender interface
func (s *sender) Timing(stat string, duration time.Duration, tags ...string) {
	s.c <- fmt.Sprintf("%s:%f|ms%s\n", stat, duration.Seconds()*1000, t(tags))
}

// Close implements xstats.Sender interface
func (s *sender) Close() {
	close(s.quit)
	<-s.done
	close(s.c)
}

// Generate a DogStatsD tag suffix
func t(tags []string) string {
	t := ""
	if len(tags) > 0 {
		t = "|#" + strings.Join(tags, ",")
	}
	return t
}

func (s *sender) fwd(w io.Writer, reportInterval time.Duration) {
	defer close(s.done)

	buf := &bytes.Buffer{}
	tick := tick(reportInterval)
	for {
		select {
		case m := <-s.c:
			buf.Write([]byte(m))
			if buf.Len() > maxPacketLen {
				flush(w, buf)
			}

		case <-tick:
			flush(w, buf)
		case <-s.quit:
			flush(w, buf)
			return
		}
	}
}

func flush(w io.Writer, buf *bytes.Buffer) {
	if buf.Len() <= 0 {
		return
	}
	if _, err := w.Write(buf.Bytes()); err != nil {
		log.Printf("error: could not write to statsd: %v", err)
	}
	buf.Reset()
}
<|endoftext|>


<commit_before>package models

import (
	"encoding/json"
	"fmt"

	"github.com/asdine/storm"
	"github.com/boltdb/bolt"

	"github.com/Depado/goploader/server/database"
	"github.com/Depado/goploader/server/logger"
)

// Initialize initializes the buckets and
func Initialize() error {
	var err error

	if err = database.DB.Init(&Resource{}); err != nil {
		logger.Fatal("server", "Couldn't initialize bucket", err)
	}
	Migrate()
	if err = S.Load(); err != nil {
		switch err {
		case storm.ErrNotFound:
			if err = S.Evaluate(); err != nil {
				return err
			}
		default:
			return err
		}
	}

	S.Info()
	logger.Debug("server", "Done Initialize on statistics object")
	return err
}

// Migrate migrates the old data format to the new one
func Migrate() {
	var err error
	type us struct {
		TotalSize    uint64
		TotalFiles   uint64
		CurrentSize  uint64
		CurrentFiles uint64
	}
	var ss us
	if err = database.DB.Get("statistics", "main", &ss); err == nil {
		S.CurrentFiles = ss.CurrentFiles
		S.CurrentSize = ss.CurrentSize
		S.TotalFiles = ss.TotalFiles
		S.TotalSize = ss.TotalSize
		if err = S.Save(); err != nil {
			logger.Err("server", "Couldn't migrate old stats", err)
			return
		}
		logger.Info("server", "Had to migrate old stats")
		database.DB.Delete("statistics", "main")
		database.DB.Drop("statistics")
	}

	var rr []Resource
	var r Resource
	err = database.DB.Bolt.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte("resources"))
		if b == nil {
			return fmt.Errorf("no bucket")
		}
		c := b.Cursor()
		for k, v := c.First(); k != nil; k, v = c.Next() {
			json.Unmarshal(v, &r)
			r.UnixDeleteAt = r.DeleteAt.Unix()
			rr = append(rr, r)
		}
		return nil
	})
	if err == nil {
		for _, r := range rr {
			database.DB.Save(&r)
		}
		logger.Info("server", "Had to migrate old resources")
		database.DB.Drop("resources")
	}
}
<commit_msg>Safer Migration func<commit_after>package models

import (
	"encoding/json"
	"fmt"

	"github.com/asdine/storm"
	"github.com/boltdb/bolt"

	"github.com/Depado/goploader/server/database"
	"github.com/Depado/goploader/server/logger"
)

// Initialize initializes the buckets and
func Initialize() error {
	var err error

	if err = database.DB.Init(&Resource{}); err != nil {
		logger.Fatal("server", "Couldn't initialize bucket", err)
	}
	Migrate()
	if err = S.Load(); err != nil {
		switch err {
		case storm.ErrNotFound:
			if err = S.Evaluate(); err != nil {
				return err
			}
		default:
			return err
		}
	}

	S.Info()
	logger.Debug("server", "Done Initialize on statistics object")
	return err
}

// Migrate migrates the old data format to the new one
func Migrate() {
	var err error
	type us struct {
		TotalSize    uint64
		TotalFiles   uint64
		CurrentSize  uint64
		CurrentFiles uint64
	}
	var ss us
	if err = database.DB.Get("statistics", "main", &ss); err == nil {
		S.CurrentFiles = ss.CurrentFiles
		S.CurrentSize = ss.CurrentSize
		S.TotalFiles = ss.TotalFiles
		S.TotalSize = ss.TotalSize
		if err = S.Save(); err != nil {
			logger.Err("server", "Couldn't migrate old stats", err)
			return
		}
		logger.Info("server", "Had to migrate old stats")
		database.DB.Delete("statistics", "main")
		database.DB.Drop("statistics")
	}

	var rr []Resource
	var r Resource
	err = database.DB.Bolt.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte("resources"))
		if b == nil {
			return fmt.Errorf("no bucket")
		}
		c := b.Cursor()
		for k, v := c.First(); k != nil; k, v = c.Next() {
			if err = json.Unmarshal(v, &r); err == nil {
				r.UnixDeleteAt = r.DeleteAt.Unix()
				rr = append(rr, r)
			}
		}
		return nil
	})
	if err == nil {
		for _, r := range rr {
			database.DB.Save(&r)
		}
		logger.Info("server", "Had to migrate old resources")
		database.DB.Drop("resources")
	}
}
<|endoftext|>


<commit_before>package notifier

import (
	"bytes"
	"fmt"
	"sort"
	"strings"
	"sync"
	"time"

	"github.com/jinzhu/gorm"
	"github.com/oinume/lekcije/server/config"
	"github.com/oinume/lekcije/server/emailer"
	"github.com/oinume/lekcije/server/errors"
	"github.com/oinume/lekcije/server/fetcher"
	"github.com/oinume/lekcije/server/logger"
	"github.com/oinume/lekcije/server/model"
	"github.com/oinume/lekcije/server/stopwatch"
	"github.com/oinume/lekcije/server/util"
	"go.uber.org/zap"
)

type Notifier struct {
	db              *gorm.DB
	fetcher         *fetcher.LessonFetcher
	dryRun          bool
	lessonService   *model.LessonService
	teachers        map[uint32]*model.Teacher
	fetchedLessons  map[uint32][]*model.Lesson
	sender          emailer.Sender
	senderWaitGroup *sync.WaitGroup
	stopwatch       stopwatch.Stopwatch
	sync.Mutex
}

type teachersAndLessons struct {
	data         map[uint32]*model.TeacherLessons
	lessonsCount int
	teacherIDs   []uint32
}

func (tal *teachersAndLessons) CountLessons() int {
	count := 0
	for _, l := range tal.data {
		count += len(l.Lessons)
	}
	return count
}

// Filter out by NotificationTimeSpanList.
// If a lesson is within NotificationTimeSpanList, it'll be included in returned value.
func (tal *teachersAndLessons) FilterBy(list model.NotificationTimeSpanList) *teachersAndLessons {
	if len(list) == 0 {
		return tal
	}
	ret := NewTeachersAndLessons(len(tal.data))
	for teacherID, tl := range tal.data {
		lessons := make([]*model.Lesson, 0, len(tl.Lessons))
		for _, lesson := range tl.Lessons {
			dt := lesson.Datetime
			t, _ := time.Parse("15:04", fmt.Sprintf("%02d:%02d", dt.Hour(), dt.Minute()))
			if list.Within(t) {
				lessons = append(lessons, lesson)
			}
		}
		ret.data[teacherID] = model.NewTeacherLessons(tl.Teacher, lessons)
	}
	return ret
}

func (tal *teachersAndLessons) String() string {
	b := new(bytes.Buffer)
	for _, tl := range tal.data {
		fmt.Fprintf(b, "Teacher: %+v", tl.Teacher)
		fmt.Fprint(b, ", Lessons:")
		for _, l := range tl.Lessons {
			fmt.Fprintf(b, " {%+v}", l)
		}
	}
	return b.String()
}

func NewTeachersAndLessons(length int) *teachersAndLessons {
	return &teachersAndLessons{
		data:         make(map[uint32]*model.TeacherLessons, length),
		lessonsCount: -1,
		teacherIDs:   make([]uint32, 0, length),
	}
}

func NewNotifier(db *gorm.DB, fetcher *fetcher.LessonFetcher, dryRun bool, sender emailer.Sender) *Notifier {
	return &Notifier{
		db:              db,
		fetcher:         fetcher,
		dryRun:          dryRun,
		teachers:        make(map[uint32]*model.Teacher, 1000),
		fetchedLessons:  make(map[uint32][]*model.Lesson, 1000),
		sender:          sender,
		senderWaitGroup: &sync.WaitGroup{},
		stopwatch:       stopwatch.NewSync().Start(),
	}
}

func (n *Notifier) SendNotification(user *model.User) error {
	followingTeacherService := model.NewFollowingTeacherService(n.db)
	n.lessonService = model.NewLessonService(n.db)
	const maxFetchErrorCount = 5
	teacherIDs, err := followingTeacherService.FindTeacherIDsByUserID(user.ID, maxFetchErrorCount)
	if err != nil {
		return errors.Wrapperf(err, "Failed to FindTeacherIDsByUserID(): userID=%v", user.ID)
	}
	n.stopwatch.Mark(fmt.Sprintf("FindTeacherIDsByUserID:%d", user.ID))

	if len(teacherIDs) == 0 {
		return nil
	}

	logger.App.Info("n", zap.Uint("userID", uint(user.ID)), zap.Int("teachers", len(teacherIDs)))

	//availableTeachersAndLessons := make(map[uint32][]*model.Lesson, 1000)
	availableTeachersAndLessons := NewTeachersAndLessons(1000)
	wg := &sync.WaitGroup{}
	for _, teacherID := range teacherIDs {
		wg.Add(1)
		go func(teacherID uint32) {
			defer n.stopwatch.Mark(fmt.Sprintf("fetchAndExtractNewAvailableLessons:%d", teacherID))
			defer wg.Done()
			fetched, newAvailable, err := n.fetchAndExtractNewAvailableLessons(teacherID)
			if err != nil {
				switch err.(type) {
				case *errors.NotFound:
					if err := model.NewTeacherService(n.db).IncrementFetchErrorCount(teacherID, 1); err != nil {
						logger.App.Error(
							"IncrementFetchErrorCount failed",
							zap.Uint("teacherID", uint(teacherID)), zap.Error(err),
						)
					}
					logger.App.Warn("Cannot find teacher", zap.Uint("teacherID", uint(teacherID)))
				// TODO: Handle a case eikaiwa.dmm.com is down
				default:
					logger.App.Error("Cannot fetch teacher", zap.Uint("teacherID", uint(teacherID)), zap.Error(err))
				}
				return
			}

			n.Lock()
			defer n.Unlock()
			n.teachers[teacherID] = fetched.Teacher
			if _, ok := n.fetchedLessons[teacherID]; !ok {
				n.fetchedLessons[teacherID] = make([]*model.Lesson, 0, 5000)
			}
			n.fetchedLessons[teacherID] = append(n.fetchedLessons[teacherID], fetched.Lessons...)
			if len(newAvailable.Lessons) > 0 {
				availableTeachersAndLessons.data[teacherID] = newAvailable
			}
			//fmt.Printf("go routine finished: user=%v\n", user.ID)
		}(teacherID)

		if err != nil {
			return err
		}
	}
	wg.Wait()

	notificationTimeSpanService := model.NewNotificationTimeSpanService(n.db)
	timeSpans, err := notificationTimeSpanService.FindByUserID(user.ID)
	if err != nil {
		return err
	}
	filteredAvailable := availableTeachersAndLessons.FilterBy(model.NotificationTimeSpanList(timeSpans))
	if err := n.sendNotificationToUser(user, filteredAvailable); err != nil {
		return err
	}

	time.Sleep(150 * time.Millisecond)
	n.stopwatch.Mark("sleep")

	return nil
}

// Returns teacher, fetchedLessons, newAvailableLessons, error
func (n *Notifier) fetchAndExtractNewAvailableLessons(teacherID uint32) (
	//*model.Teacher, []*model.Lesson, []*model.Lesson, error,
	*model.TeacherLessons, *model.TeacherLessons, error,
) {
	teacher, fetchedLessons, err := n.fetcher.Fetch(teacherID)
	if err != nil {
		return nil, nil, err
	}
	logger.App.Debug(
		"fetcher.Fetch",
		zap.Uint("teacherID", uint(teacher.ID)),
		zap.Int("lessons", len(fetchedLessons)),
	)

	//fmt.Printf("fetchedLessons ---\n")
	//for _, l := range fetchedLessons {
	//	fmt.Printf("teacherID=%v, datetime=%v, status=%v\n", l.TeacherId, l.Datetime, l.Status)
	//}

	now := time.Now().In(config.LocalTimezone())
	fromDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, config.LocalTimezone())
	toDate := fromDate.Add(24 * 6 * time.Hour)
	lastFetchedLessons, err := n.lessonService.FindLessons(teacher.ID, fromDate, toDate)
	if err != nil {
		return nil, nil, err
	}
	//fmt.Printf("lastFetchedLessons ---\n")
	//for _, l := range lastFetchedLessons {
	//	fmt.Printf("teacherID=%v, datetime=%v, status=%v\n", l.TeacherId, l.Datetime, l.Status)
	//}

	newAvailableLessons := n.lessonService.GetNewAvailableLessons(lastFetchedLessons, fetchedLessons)
	//fmt.Printf("newAvailableLessons ---\n")
	//for _, l := range newAvailableLessons {
	//	fmt.Printf("teacherID=%v, datetime=%v, status=%v\n", l.TeacherId, l.Datetime, l.Status)
	//}
	return model.NewTeacherLessons(teacher, fetchedLessons),
		model.NewTeacherLessons(teacher, newAvailableLessons),
		nil
	//return teacher, fetchedLessons, newAvailableLessons, nil
}

func (n *Notifier) sendNotificationToUser(
	user *model.User, lessonsPerTeacher *teachersAndLessons,
) error {
	lessonsCount := 0
	var teacherIDs []int
	for teacherID, l := range lessonsPerTeacher.data {
		teacherIDs = append(teacherIDs, int(teacherID))
		lessonsCount += len(l.Lessons)
	}
	if lessonsPerTeacher.CountLessons() == 0 {
		// Don't send notification
		return nil
	}

	sort.Ints(teacherIDs)
	var teacherIDs2 []uint32
	var teacherNames []string
	for _, id := range teacherIDs {
		teacherIDs2 = append(teacherIDs2, uint32(id))
		teacherNames = append(teacherNames, n.teachers[uint32(id)].Name)
	}

	// TODO: getEmailTemplate as a static file
	t := emailer.NewTemplate("notifier", getEmailTemplateJP())
	data := struct {
		To                string
		TeacherNames      string
		TeacherIDs        []uint32
		Teachers          map[uint32]*model.Teacher
		LessonsPerTeacher map[uint32]*model.TeacherLessons
		WebURL            string
	}{
		To:                user.Email,
		TeacherNames:      strings.Join(teacherNames, ", "),
		TeacherIDs:        teacherIDs2,
		Teachers:          n.teachers,
		LessonsPerTeacher: lessonsPerTeacher.data,
		WebURL:            config.WebURL(),
	}
	email, err := emailer.NewEmailFromTemplate(t, data)
	if err != nil {
		return errors.InternalWrapf(err, "Failed to create emailer.Email from template: to=%v", user.Email)
	}
	email.SetCustomArg("email_type", model.EmailTypeNewLessonNotifier)
	email.SetCustomArg("user_id", fmt.Sprint(user.ID))
	email.SetCustomArg("teacher_ids", strings.Join(util.Uint32ToStringSlice(teacherIDs2...), ","))
	//fmt.Printf("--- mail ---\n%s", email.BodyString())
	n.stopwatch.Mark("emailer.NewEmailFromTemplate")

	logger.App.Info("sendNotificationToUser", zap.String("email", user.Email))

	n.senderWaitGroup.Add(1)
	go func(email *emailer.Email) {
		defer n.stopwatch.Mark(fmt.Sprintf("sender.Send:%d", user.ID))
		defer n.senderWaitGroup.Done()
		if err := n.sender.Send(email); err != nil {
			logger.App.Error(
				"Failed to sendNotificationToUser",
				zap.String("email", user.Email), zap.Error(err),
			)
		}
	}(email)

	return nil
}

func getEmailTemplateJP() string {
	return strings.TrimSpace(`
From: lekcije <lekcije@lekcije.com>
To: {{ .To }}
Subject: {{ .TeacherNames }}の空きレッスンがあります
Body: text/html
{{ range $teacherID := .TeacherIDs }}
{{- $teacher := index $.Teachers $teacherID -}}
--- {{ $teacher.Name }} ---
  {{- $tal := index $.LessonsPerTeacher $teacherID }}
  {{- range $lesson := $tal.Lessons }}
{{ $lesson.Datetime.Format "2006-01-02 15:04" }}
  {{- end }}

レッスンの予約はこちらから:
PC
Mobile

{{ end }}
空きレッスンの通知の解除はこちら

お問い合わせ
	`)
}

func (n *Notifier) Close() {
	n.senderWaitGroup.Wait()
	defer n.fetcher.Close()
	defer func() {
		if n.dryRun {
			return
		}
		for teacherID, lessons := range n.fetchedLessons {
			if _, err := n.lessonService.UpdateLessons(lessons); err != nil {
				logger.App.Error(
					"An error ocurred in Notifier.Close",
					zap.Error(err), zap.Uint("teacherID", uint(teacherID)),
				)
			}
		}
	}()
	defer func() {
		n.stopwatch.Stop()
		//logger.App.Info("Stopwatch report", zap.String("report", watch.Report()))
		//fmt.Println("--- stopwatch ---")
		//fmt.Println(n.stopwatch.Report())
	}()
}
<commit_msg>Omit log output due to papertrail limit<commit_after>package notifier

import (
	"bytes"
	"fmt"
	"sort"
	"strings"
	"sync"
	"time"

	"github.com/jinzhu/gorm"
	"github.com/oinume/lekcije/server/config"
	"github.com/oinume/lekcije/server/emailer"
	"github.com/oinume/lekcije/server/errors"
	"github.com/oinume/lekcije/server/fetcher"
	"github.com/oinume/lekcije/server/logger"
	"github.com/oinume/lekcije/server/model"
	"github.com/oinume/lekcije/server/stopwatch"
	"github.com/oinume/lekcije/server/util"
	"go.uber.org/zap"
)

type Notifier struct {
	db              *gorm.DB
	fetcher         *fetcher.LessonFetcher
	dryRun          bool
	lessonService   *model.LessonService
	teachers        map[uint32]*model.Teacher
	fetchedLessons  map[uint32][]*model.Lesson
	sender          emailer.Sender
	senderWaitGroup *sync.WaitGroup
	stopwatch       stopwatch.Stopwatch
	sync.Mutex
}

type teachersAndLessons struct {
	data         map[uint32]*model.TeacherLessons
	lessonsCount int
	teacherIDs   []uint32
}

func (tal *teachersAndLessons) CountLessons() int {
	count := 0
	for _, l := range tal.data {
		count += len(l.Lessons)
	}
	return count
}

// Filter out by NotificationTimeSpanList.
// If a lesson is within NotificationTimeSpanList, it'll be included in returned value.
func (tal *teachersAndLessons) FilterBy(list model.NotificationTimeSpanList) *teachersAndLessons {
	if len(list) == 0 {
		return tal
	}
	ret := NewTeachersAndLessons(len(tal.data))
	for teacherID, tl := range tal.data {
		lessons := make([]*model.Lesson, 0, len(tl.Lessons))
		for _, lesson := range tl.Lessons {
			dt := lesson.Datetime
			t, _ := time.Parse("15:04", fmt.Sprintf("%02d:%02d", dt.Hour(), dt.Minute()))
			if list.Within(t) {
				lessons = append(lessons, lesson)
			}
		}
		ret.data[teacherID] = model.NewTeacherLessons(tl.Teacher, lessons)
	}
	return ret
}

func (tal *teachersAndLessons) String() string {
	b := new(bytes.Buffer)
	for _, tl := range tal.data {
		fmt.Fprintf(b, "Teacher: %+v", tl.Teacher)
		fmt.Fprint(b, ", Lessons:")
		for _, l := range tl.Lessons {
			fmt.Fprintf(b, " {%+v}", l)
		}
	}
	return b.String()
}

func NewTeachersAndLessons(length int) *teachersAndLessons {
	return &teachersAndLessons{
		data:         make(map[uint32]*model.TeacherLessons, length),
		lessonsCount: -1,
		teacherIDs:   make([]uint32, 0, length),
	}
}

func NewNotifier(db *gorm.DB, fetcher *fetcher.LessonFetcher, dryRun bool, sender emailer.Sender) *Notifier {
	return &Notifier{
		db:              db,
		fetcher:         fetcher,
		dryRun:          dryRun,
		teachers:        make(map[uint32]*model.Teacher, 1000),
		fetchedLessons:  make(map[uint32][]*model.Lesson, 1000),
		sender:          sender,
		senderWaitGroup: &sync.WaitGroup{},
		stopwatch:       stopwatch.NewSync().Start(),
	}
}

func (n *Notifier) SendNotification(user *model.User) error {
	followingTeacherService := model.NewFollowingTeacherService(n.db)
	n.lessonService = model.NewLessonService(n.db)
	const maxFetchErrorCount = 5
	teacherIDs, err := followingTeacherService.FindTeacherIDsByUserID(user.ID, maxFetchErrorCount)
	if err != nil {
		return errors.Wrapperf(err, "Failed to FindTeacherIDsByUserID(): userID=%v", user.ID)
	}
	n.stopwatch.Mark(fmt.Sprintf("FindTeacherIDsByUserID:%d", user.ID))

	if len(teacherIDs) == 0 {
		return nil
	}

	// Comment out due to papertrail limit
	//logger.App.Info("n", zap.Uint("userID", uint(user.ID)), zap.Int("teachers", len(teacherIDs)))

	//availableTeachersAndLessons := make(map[uint32][]*model.Lesson, 1000)
	availableTeachersAndLessons := NewTeachersAndLessons(1000)
	wg := &sync.WaitGroup{}
	for _, teacherID := range teacherIDs {
		wg.Add(1)
		go func(teacherID uint32) {
			defer n.stopwatch.Mark(fmt.Sprintf("fetchAndExtractNewAvailableLessons:%d", teacherID))
			defer wg.Done()
			fetched, newAvailable, err := n.fetchAndExtractNewAvailableLessons(teacherID)
			if err != nil {
				switch err.(type) {
				case *errors.NotFound:
					if err := model.NewTeacherService(n.db).IncrementFetchErrorCount(teacherID, 1); err != nil {
						logger.App.Error(
							"IncrementFetchErrorCount failed",
							zap.Uint("teacherID", uint(teacherID)), zap.Error(err),
						)
					}
					logger.App.Warn("Cannot find teacher", zap.Uint("teacherID", uint(teacherID)))
				// TODO: Handle a case eikaiwa.dmm.com is down
				default:
					logger.App.Error("Cannot fetch teacher", zap.Uint("teacherID", uint(teacherID)), zap.Error(err))
				}
				return
			}

			n.Lock()
			defer n.Unlock()
			n.teachers[teacherID] = fetched.Teacher
			if _, ok := n.fetchedLessons[teacherID]; !ok {
				n.fetchedLessons[teacherID] = make([]*model.Lesson, 0, 5000)
			}
			n.fetchedLessons[teacherID] = append(n.fetchedLessons[teacherID], fetched.Lessons...)
			if len(newAvailable.Lessons) > 0 {
				availableTeachersAndLessons.data[teacherID] = newAvailable
			}
			//fmt.Printf("go routine finished: user=%v\n", user.ID)
		}(teacherID)

		if err != nil {
			return err
		}
	}
	wg.Wait()

	notificationTimeSpanService := model.NewNotificationTimeSpanService(n.db)
	timeSpans, err := notificationTimeSpanService.FindByUserID(user.ID)
	if err != nil {
		return err
	}
	filteredAvailable := availableTeachersAndLessons.FilterBy(model.NotificationTimeSpanList(timeSpans))
	if err := n.sendNotificationToUser(user, filteredAvailable); err != nil {
		return err
	}

	time.Sleep(150 * time.Millisecond)
	n.stopwatch.Mark("sleep")

	return nil
}

// Returns teacher, fetchedLessons, newAvailableLessons, error
func (n *Notifier) fetchAndExtractNewAvailableLessons(teacherID uint32) (
	//*model.Teacher, []*model.Lesson, []*model.Lesson, error,
	*model.TeacherLessons, *model.TeacherLessons, error,
) {
	teacher, fetchedLessons, err := n.fetcher.Fetch(teacherID)
	if err != nil {
		return nil, nil, err
	}
	logger.App.Debug(
		"fetcher.Fetch",
		zap.Uint("teacherID", uint(teacher.ID)),
		zap.Int("lessons", len(fetchedLessons)),
	)

	//fmt.Printf("fetchedLessons ---\n")
	//for _, l := range fetchedLessons {
	//	fmt.Printf("teacherID=%v, datetime=%v, status=%v\n", l.TeacherId, l.Datetime, l.Status)
	//}

	now := time.Now().In(config.LocalTimezone())
	fromDate := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, config.LocalTimezone())
	toDate := fromDate.Add(24 * 6 * time.Hour)
	lastFetchedLessons, err := n.lessonService.FindLessons(teacher.ID, fromDate, toDate)
	if err != nil {
		return nil, nil, err
	}
	//fmt.Printf("lastFetchedLessons ---\n")
	//for _, l := range lastFetchedLessons {
	//	fmt.Printf("teacherID=%v, datetime=%v, status=%v\n", l.TeacherId, l.Datetime, l.Status)
	//}

	newAvailableLessons := n.lessonService.GetNewAvailableLessons(lastFetchedLessons, fetchedLessons)
	//fmt.Printf("newAvailableLessons ---\n")
	//for _, l := range newAvailableLessons {
	//	fmt.Printf("teacherID=%v, datetime=%v, status=%v\n", l.TeacherId, l.Datetime, l.Status)
	//}
	return model.NewTeacherLessons(teacher, fetchedLessons),
		model.NewTeacherLessons(teacher, newAvailableLessons),
		nil
	//return teacher, fetchedLessons, newAvailableLessons, nil
}

func (n *Notifier) sendNotificationToUser(
	user *model.User, lessonsPerTeacher *teachersAndLessons,
) error {
	lessonsCount := 0
	var teacherIDs []int
	for teacherID, l := range lessonsPerTeacher.data {
		teacherIDs = append(teacherIDs, int(teacherID))
		lessonsCount += len(l.Lessons)
	}
	if lessonsPerTeacher.CountLessons() == 0 {
		// Don't send notification
		return nil
	}

	sort.Ints(teacherIDs)
	var teacherIDs2 []uint32
	var teacherNames []string
	for _, id := range teacherIDs {
		teacherIDs2 = append(teacherIDs2, uint32(id))
		teacherNames = append(teacherNames, n.teachers[uint32(id)].Name)
	}

	// TODO: getEmailTemplate as a static file
	t := emailer.NewTemplate("notifier", getEmailTemplateJP())
	data := struct {
		To                string
		TeacherNames      string
		TeacherIDs        []uint32
		Teachers          map[uint32]*model.Teacher
		LessonsPerTeacher map[uint32]*model.TeacherLessons
		WebURL            string
	}{
		To:                user.Email,
		TeacherNames:      strings.Join(teacherNames, ", "),
		TeacherIDs:        teacherIDs2,
		Teachers:          n.teachers,
		LessonsPerTeacher: lessonsPerTeacher.data,
		WebURL:            config.WebURL(),
	}
	email, err := emailer.NewEmailFromTemplate(t, data)
	if err != nil {
		return errors.InternalWrapf(err, "Failed to create emailer.Email from template: to=%v", user.Email)
	}
	email.SetCustomArg("email_type", model.EmailTypeNewLessonNotifier)
	email.SetCustomArg("user_id", fmt.Sprint(user.ID))
	email.SetCustomArg("teacher_ids", strings.Join(util.Uint32ToStringSlice(teacherIDs2...), ","))
	//fmt.Printf("--- mail ---\n%s", email.BodyString())
	n.stopwatch.Mark("emailer.NewEmailFromTemplate")

	logger.App.Info("sendNotificationToUser", zap.String("email", user.Email))

	n.senderWaitGroup.Add(1)
	go func(email *emailer.Email) {
		defer n.stopwatch.Mark(fmt.Sprintf("sender.Send:%d", user.ID))
		defer n.senderWaitGroup.Done()
		if err := n.sender.Send(email); err != nil {
			logger.App.Error(
				"Failed to sendNotificationToUser",
				zap.String("email", user.Email), zap.Error(err),
			)
		}
	}(email)

	return nil
}

func getEmailTemplateJP() string {
	return strings.TrimSpace(`
From: lekcije <lekcije@lekcije.com>
To: {{ .To }}
Subject: {{ .TeacherNames }}の空きレッスンがあります
Body: text/html
{{ range $teacherID := .TeacherIDs }}
{{- $teacher := index $.Teachers $teacherID -}}
--- {{ $teacher.Name }} ---
  {{- $tal := index $.LessonsPerTeacher $teacherID }}
  {{- range $lesson := $tal.Lessons }}
{{ $lesson.Datetime.Format "2006-01-02 15:04" }}
  {{- end }}

レッスンの予約はこちらから:
PC
Mobile

{{ end }}
空きレッスンの通知の解除はこちら

お問い合わせ
	`)
}

func (n *Notifier) Close() {
	n.senderWaitGroup.Wait()
	defer n.fetcher.Close()
	defer func() {
		if n.dryRun {
			return
		}
		for teacherID, lessons := range n.fetchedLessons {
			if _, err := n.lessonService.UpdateLessons(lessons); err != nil {
				logger.App.Error(
					"An error ocurred in Notifier.Close",
					zap.Error(err), zap.Uint("teacherID", uint(teacherID)),
				)
			}
		}
	}()
	defer func() {
		n.stopwatch.Stop()
		//logger.App.Info("Stopwatch report", zap.String("report", watch.Report()))
		//fmt.Println("--- stopwatch ---")
		//fmt.Println(n.stopwatch.Report())
	}()
}
<|endoftext|>