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

package client

import (
	"fmt"
	"time"

	"v.io/v23"
	"v.io/v23/context"
	wire "v.io/v23/services/syncbase"
	"v.io/v23/syncbase"
	"v.io/v23/verror"
	"v.io/x/ref/services/syncbase/longevity_tests/model"
	"v.io/x/ref/services/syncbase/testutil"
)

// CreateDbsAndCollections creates databases and collections according to the
// given models.  It does not fail if any of the databases or collections
// already exist.  If the model contains syncgroups, it will also create or
// join those as well.
func CreateDbsAndCollections(ctx *context.T, sbName string, dbModels model.DatabaseSet) (map[syncbase.Database][]syncbase.Collection, []syncbase.Syncgroup, error) {
	blessing, _ := v23.GetPrincipal(ctx).BlessingStore().Default()
	perms := testutil.DefaultPerms(blessing.String(), "root:checker")
	nsRoots := v23.GetNamespace(ctx).Roots()

	service := syncbase.NewService(sbName)
	syncgroups := []syncbase.Syncgroup{}
	dbColsMap := map[syncbase.Database][]syncbase.Collection{}
	for _, dbModel := range dbModels {
		// Create Database.
		db := service.DatabaseForId(dbModel.Id(), nil)
		// TODO(nlacasse): Don't create the database unless its blessings match
		// ours.
		if err := db.Create(ctx, perms); err != nil && verror.ErrorID(err) != verror.ErrExist.ID {
			return nil, nil, err
		}
		dbColsMap[db] = []syncbase.Collection{}

		// Create collections for database.
		for _, colModel := range dbModel.Collections {
			col := db.CollectionForId(colModel.Id())
			// TODO(nlacasse): Don't create the collection unless its blessings
			// match ours.
			if err := col.Create(ctx, perms); err != nil && verror.ErrorID(err) != verror.ErrExist.ID {
				return nil, nil, err
			}
			dbColsMap[db] = append(dbColsMap[db], col)
		}

		// Create or join syncgroups for database.
		for _, sgModel := range dbModel.Syncgroups {
			sg := db.Syncgroup(sgModel.Name())
			if sgModel.HostDevice.Name == sbName {
				// We are the host.  Create the syncgroup.
				spec := sgModel.Spec()
				spec.MountTables = nsRoots
				// TODO(nlacasse): Set this to something real.
				spec.Perms = testutil.DefaultPerms("root")
				if err := sg.Create(ctx, spec, wire.SyncgroupMemberInfo{}); err != nil {
					return nil, nil, err
				}
				syncgroups = append(syncgroups, sg)
				continue
			}
			// Join the syncgroup.  It might not exist at first, so we loop.
			// TODO(nlacasse): Parameterize number of retries.  Exponential
			// backoff?
			var joinErr error
			for i := 0; i < 10; i++ {
				_, joinErr = sg.Join(ctx, wire.SyncgroupMemberInfo{})
				if joinErr == nil {
					syncgroups = append(syncgroups, sg)
					break
				} else {
					time.Sleep(100 * time.Millisecond)
				}
			}
			if joinErr != nil {
				return nil, nil, fmt.Errorf("could not join syncgroup %q: %v", sgModel.Name(), joinErr)
			}
		}
	}

	return dbColsMap, syncgroups, nil
}
<commit_msg>longevity tests: Don't error when creating a syncgroup if it already exists.<commit_after>// Copyright 2016 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package client

import (
	"fmt"
	"time"

	"v.io/v23"
	"v.io/v23/context"
	wire "v.io/v23/services/syncbase"
	"v.io/v23/syncbase"
	"v.io/v23/verror"
	"v.io/x/ref/services/syncbase/longevity_tests/model"
	"v.io/x/ref/services/syncbase/testutil"
)

// CreateDbsAndCollections creates databases and collections according to the
// given models.  It does not fail if any of the databases or collections
// already exist.  If the model contains syncgroups, it will also create or
// join those as well.
func CreateDbsAndCollections(ctx *context.T, sbName string, dbModels model.DatabaseSet) (map[syncbase.Database][]syncbase.Collection, []syncbase.Syncgroup, error) {
	blessing, _ := v23.GetPrincipal(ctx).BlessingStore().Default()
	perms := testutil.DefaultPerms(blessing.String(), "root:checker")
	nsRoots := v23.GetNamespace(ctx).Roots()

	service := syncbase.NewService(sbName)
	syncgroups := []syncbase.Syncgroup{}
	dbColsMap := map[syncbase.Database][]syncbase.Collection{}
	for _, dbModel := range dbModels {
		// Create Database.
		db := service.DatabaseForId(dbModel.Id(), nil)
		// TODO(nlacasse): Don't create the database unless its blessings match
		// ours.
		if err := db.Create(ctx, perms); err != nil && verror.ErrorID(err) != verror.ErrExist.ID {
			return nil, nil, err
		}
		dbColsMap[db] = []syncbase.Collection{}

		// Create collections for database.
		for _, colModel := range dbModel.Collections {
			col := db.CollectionForId(colModel.Id())
			// TODO(nlacasse): Don't create the collection unless its blessings
			// match ours.
			if err := col.Create(ctx, perms); err != nil && verror.ErrorID(err) != verror.ErrExist.ID {
				return nil, nil, err
			}
			dbColsMap[db] = append(dbColsMap[db], col)
		}

		// Create or join syncgroups for database.
		for _, sgModel := range dbModel.Syncgroups {
			sg := db.Syncgroup(sgModel.Name())
			if sgModel.HostDevice.Name == sbName {
				// We are the host.  Create the syncgroup.
				spec := sgModel.Spec()
				spec.MountTables = nsRoots
				// TODO(nlacasse): Set this to something real.
				spec.Perms = testutil.DefaultPerms("root")
				if err := sg.Create(ctx, spec, wire.SyncgroupMemberInfo{}); err != nil && verror.ErrorID(err) != verror.ErrExist.ID {
					return nil, nil, err
				}
				syncgroups = append(syncgroups, sg)
				continue
			}
			// Join the syncgroup.  It might not exist at first, so we loop.
			// TODO(nlacasse): Parameterize number of retries.  Exponential
			// backoff?
			var joinErr error
			for i := 0; i < 10; i++ {
				_, joinErr = sg.Join(ctx, wire.SyncgroupMemberInfo{})
				if joinErr == nil {
					syncgroups = append(syncgroups, sg)
					break
				} else {
					time.Sleep(100 * time.Millisecond)
				}
			}
			if joinErr != nil {
				return nil, nil, fmt.Errorf("could not join syncgroup %q: %v", sgModel.Name(), joinErr)
			}
		}
	}

	return dbColsMap, syncgroups, nil
}
<|endoftext|>


<commit_before>package commando

import (
	"errors"
	"fmt"
	"os"
	"regexp"
	"strings"
	"text/tabwriter"
)

var (
	tw       *tabwriter.Writer
	argIndex int
)

// Command is the base type for all commands.
type Command struct {
	Name        string              // Name of command, typically how a command is called from the cli.
	Description string              // A Description of the command, printed in usage.
	Options     map[string]*Option  // A map of the flags attached to this command, they are looked up by their name.
	Children    map[string]*Command // A map of all the subcommands, looked up by their name.
	Parent      *Command            // A pointer to the command's parent.  not set in root command.
	Execute     func()              // The function to run when executing a command.

}

// Option is the type for flag options like "-p" or "--path"
type Option struct {
	Name        string      // Name of Option, its name is used to retrieve its value.
	Description string      // A Description of the option, used when printing usage.
	Flags       []string    // The flags associated with the option.
	Value       interface{} // Where the value of a given flag is scanned into.
	Present     bool        // Used to determine whether or not a flag is present, typically for a bool type flag.
	Required    bool        // If a flag is required and not present, usage for owning command is printed.
}

// AddSubcommand attaches a command to a parent, as well as sets the parent property on the child command.
// Commands can be limitlessly nested (though, I don't recommend it).
func (c *Command) AddSubCommand(child *Command) {
	if c.Children == nil {
		c.Children = make(map[string]*Command)
	}
	child.Parent = c
	c.Children[child.Name] = child
}

// PrintHelp is used to print info and usage for any command.
// It knows if a command is the last in the chain, and if so, prints usage with just Options (Flags)
func (c *Command) PrintHelp() {
	uc := strings.Join(os.Args[:argIndex], " ")
	if c.hasChildren() {
		fmt.Println("\nUsage:", uc, "COMMAND [args..]\n")
		fmt.Println(c.Description, "\n")
		fmt.Println("Commands:")
		for _, cmd := range c.Children {
			PrintFields(true, 4, cmd.Name, cmd.Description)
		}
	} else {
		fmt.Printf("\nUsage: %s [options...]\n\n", uc)
		fmt.Println(c.Description)
		fmt.Println("\nOptions:")
		for _, opt := range c.Options {
			PrintFields(true, 4, strings.Join(opt.Flags, ", "), opt.Description)
		}
	}
}

// hasChildren is a private method that determines whether or not a command has children.
// Parse uses hasChildren to decide whether or not to continue recursing.
func (c *Command) hasChildren() bool {
	if c.Children != nil {
		return true
	} else {
		return false
	}
}

// AddOption is used to add an option (Flag) to a command.
func (c *Command) AddOption(name string, descrip string, req bool, flags ...string) {
	if c.Options == nil {
		c.Options = make(map[string]*Option)
	}
	opt := &Option{
		Name:        name,
		Description: descrip,
		Flags:       flags,
		Required:    req,
	}
	c.Options[name] = opt
}

// PrintFields is a wrapper for an IO Writer / Formatter.
// Using commando.PrintFields evenly spaces output into columns.
func PrintFields(indent bool, width int, fields ...interface{}) {
	argArray := make([]interface{}, 0)
	if indent {
		argArray = append(argArray, strings.Repeat(" ", width))
	}
	for i, field := range fields {
		argArray = append(argArray, field)
		if i < (len(fields) - 1) {
			argArray = append(argArray, "\t")
		}
	}
	fmt.Fprintln(tw, argArray...)
}

// Parse is the entry point into Commando.
// It recurses all the children of a command, finally executing the last command in the chain.
func (c *Command) Parse() {

	tw = tabwriter.NewWriter(os.Stdout, 0, 8, 0, '\t', 0)
	defer tw.Flush()

	if len(os.Args) == 1 {
		c.PrintHelp()
		return
	}
	if os.Args[1] == "-h" || os.Args[1] == "--help" {
		c.PrintHelp()
		return
	}

	if err := c.setOptions(); err == nil {
		c.executeChildren()
	} else {
		c.PrintHelp()
	}
}

// findChild is a private method used to locate the requested child command of a parent.
// It is used in the recursive lookup in Parse
func (c *Command) findChild() *Command {
	var child *Command
	for _, arg := range os.Args {
		if c.Children[arg] != nil {
			child = c.Children[arg]
		}
	}
	return child
}

// setOptions is used to retrieve flagged options  and set their values.
func (c *Command) setOptions() error {

	seen := make(map[string]string)

	for i, arg := range os.Args {
		for _, opt := range c.Options {
			if opt.Value == nil {
				for _, flag := range opt.Flags {
					if match, _ := regexp.MatchString(arg, flag); match {
						if opt.Value != nil {
							switch val := opt.Value.(type) {
							case []string:
								if _, present := seen[os.Args[i+1]]; !present {
									opt.Value = append(opt.Value.([]string), os.Args[i+1])
									seen[os.Args[i+1]] = os.Args[i+1]
								}
							case string:
								optArray := []string{val}
								if _, present := seen[os.Args[i+1]]; !present {
									seen[os.Args[i+1]] = os.Args[i+1]
									optArray = append(optArray, os.Args[i+1])
								}
								opt.Value = optArray
							}
						} else {
							opt.Value = os.Args[i+1]
							seen[os.Args[i+1]] = os.Args[i+1]
							opt.Present = true
						}
					}
				}
			} else {
				switch v := opt.Value.(type) {
				case []string:
					if len(v) == 1 {
						opt.Value = opt.Value.([]string)[0]
					}
				}
			}
		}
	}
	for _, opt := range c.Options {
		if opt.Required && opt.Value == nil {
			err := errors.New("required option missing")
			return err
		}
	}
	return nil
}

// executeChildren is the recurive part of Parse.
// It determines if a command has children.  If it does, it continues to recurse.
// If not, it executes.
func (c *Command) executeChildren() {
	r, _ := regexp.Compile("^-{1,2}.*")
	if !r.MatchString(os.Args[1]) {
		argIndex++
		if c.hasChildren() {
			if child := c.findChild(); child != nil {
				child.Parse()
			} else {
				if argIndex+1 <= len(os.Args) {
					fmt.Println("unknown command: " + os.Args[argIndex])
				}
				c.PrintHelp()
			}
			return
		} else {
			if argIndex+1 <= len(os.Args) {
				if os.Args[argIndex] == "--help" || os.Args[argIndex] == "-h" {
					c.PrintHelp()
					return
				}
			}
			c.Execute()
		}
	}
}
<commit_msg>better help parsing<commit_after>package commando

import (
	"errors"
	"fmt"
	"os"
	"regexp"
	"strings"
	"text/tabwriter"
)

var (
	tw       *tabwriter.Writer
	argIndex int
)

// Command is the base type for all commands.
type Command struct {
	Name        string              // Name of command, typically how a command is called from the cli.
	Description string              // A Description of the command, printed in usage.
	Options     map[string]*Option  // A map of the flags attached to this command, they are looked up by their name.
	Children    map[string]*Command // A map of all the subcommands, looked up by their name.
	Parent      *Command            // A pointer to the command's parent.  not set in root command.
	Execute     func()              // The function to run when executing a command.

}

// Option is the type for flag options like "-p" or "--path"
type Option struct {
	Name        string      // Name of Option, its name is used to retrieve its value.
	Description string      // A Description of the option, used when printing usage.
	Flags       []string    // The flags associated with the option.
	Value       interface{} // Where the value of a given flag is scanned into.
	Present     bool        // Used to determine whether or not a flag is present, typically for a bool type flag.
	Required    bool        // If a flag is required and not present, usage for owning command is printed.
}

// AddSubcommand attaches a command to a parent, as well as sets the parent property on the child command.
// Commands can be limitlessly nested (though, I don't recommend it).
func (c *Command) AddSubCommand(child *Command) {
	if c.Children == nil {
		c.Children = make(map[string]*Command)
	}
	child.Parent = c
	c.Children[child.Name] = child
}

// PrintHelp is used to print info and usage for any command.
// It knows if a command is the last in the chain, and if so, prints usage with just Options (Flags)
func (c *Command) PrintHelp() {
	uc := strings.Join(os.Args[:argIndex], " ")
	if c.hasChildren() {
		fmt.Println("\nUsage:", uc, "COMMAND [args..]\n")
		fmt.Println(c.Description, "\n")
		fmt.Println("Commands:")
		for _, cmd := range c.Children {
			PrintFields(true, 4, cmd.Name, cmd.Description)
		}
	} else {
		fmt.Printf("\nUsage: %s [options...]\n\n", uc)
		fmt.Println(c.Description)
		fmt.Println("\nOptions:")
		for _, opt := range c.Options {
			PrintFields(true, 4, strings.Join(opt.Flags, ", "), opt.Description)
		}
	}
}

// hasChildren is a private method that determines whether or not a command has children.
// Parse uses hasChildren to decide whether or not to continue recursing.
func (c *Command) hasChildren() bool {
	if c.Children != nil {
		return true
	} else {
		return false
	}
}

// AddOption is used to add an option (Flag) to a command.
func (c *Command) AddOption(name string, descrip string, req bool, flags ...string) {
	if c.Options == nil {
		c.Options = make(map[string]*Option)
	}
	opt := &Option{
		Name:        name,
		Description: descrip,
		Flags:       flags,
		Required:    req,
	}
	c.Options[name] = opt
}

// PrintFields is a wrapper for an IO Writer / Formatter.
// Using commando.PrintFields evenly spaces output into columns.
func PrintFields(indent bool, width int, fields ...interface{}) {
	argArray := make([]interface{}, 0)
	if indent {
		argArray = append(argArray, strings.Repeat(" ", width))
	}
	for i, field := range fields {
		argArray = append(argArray, field)
		if i < (len(fields) - 1) {
			argArray = append(argArray, "\t")
		}
	}
	fmt.Fprintln(tw, argArray...)
}

// Parse is the entry point into Commando.
// It recurses all the children of a command, finally executing the last command in the chain.
func (c *Command) Parse() {

	tw = tabwriter.NewWriter(os.Stdout, 0, 8, 0, '\t', 0)
	defer tw.Flush()

	if len(os.Args) == 1 {
		c.PrintHelp()
		return
	}
	if os.Args[len(os.Args)-1] == "-h" || os.Args[len(os.Args)-1] == "--help" {
		c.PrintHelp()
		return
	}

	if err := c.setOptions(); err == nil {
		c.executeChildren()
	} else {
		c.PrintHelp()
	}
}

// findChild is a private method used to locate the requested child command of a parent.
// It is used in the recursive lookup in Parse
func (c *Command) findChild() *Command {
	var child *Command
	for _, arg := range os.Args {
		if c.Children[arg] != nil {
			child = c.Children[arg]
		}
	}
	return child
}

// setOptions is used to retrieve flagged options  and set their values.
func (c *Command) setOptions() error {

	seen := make(map[string]string)

	for i, arg := range os.Args {
		for _, opt := range c.Options {
			if opt.Value == nil {
				for _, flag := range opt.Flags {
					if match, _ := regexp.MatchString(arg, flag); match {
						if opt.Value != nil {
							switch val := opt.Value.(type) {
							case []string:
								if _, present := seen[os.Args[i+1]]; !present {
									opt.Value = append(opt.Value.([]string), os.Args[i+1])
									seen[os.Args[i+1]] = os.Args[i+1]
								}
							case string:
								optArray := []string{val}
								if _, present := seen[os.Args[i+1]]; !present {
									seen[os.Args[i+1]] = os.Args[i+1]
									optArray = append(optArray, os.Args[i+1])
								}
								opt.Value = optArray
							}
						} else {
							opt.Value = os.Args[i+1]
							seen[os.Args[i+1]] = os.Args[i+1]
							opt.Present = true
						}
					}
				}
			} else {
				switch v := opt.Value.(type) {
				case []string:
					if len(v) == 1 {
						opt.Value = opt.Value.([]string)[0]
					}
				}
			}
		}
	}
	for _, opt := range c.Options {
		if opt.Required && opt.Value == nil {
			err := errors.New("required option missing")
			return err
		}
	}
	return nil
}

// executeChildren is the recurive part of Parse.
// It determines if a command has children.  If it does, it continues to recurse.
// If not, it executes.
func (c *Command) executeChildren() {
	r, _ := regexp.Compile("^-{1,2}.*")
	if !r.MatchString(os.Args[1]) {
		argIndex++
		if c.hasChildren() {
			if child := c.findChild(); child != nil {
				child.Parse()
			} else {
				if argIndex+1 <= len(os.Args) {
					fmt.Println("unknown command: " + os.Args[argIndex])
				}
				c.PrintHelp()
			}
			return
		} else {
			if argIndex+1 <= len(os.Args) {
				if os.Args[argIndex] == "--help" || os.Args[argIndex] == "-h" {
					c.PrintHelp()
					return
				}
			}
			c.Execute()
		}
	}
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"os"
	"text/tabwriter"

	"github.com/codegangsta/cli"
	"github.com/damonkelley/hostsup/hostsfile"
)

var Commands = []cli.Command{
	{
		Name:   "add",
		Usage:  "Add a hosts entry",
		Action: cmdAddEntry,
	},
	{
		Name:   "rm",
		Usage:  "Remove a hosts entry",
		Action: cmdRemoveEntry,
	},
	{
		Name:   "ls",
		Usage:  "List hosts entries",
		Action: cmdListEntry,
	},
	{
		Name:   "clean",
		Usage:  "Remove all hosts entries added by hostsup",
		Action: cmdClean,
	},
}

const HOSTSFILE string = "/etc/hosts"

func cmdAddEntry(c *cli.Context) {
	// Argument order is: <IP>, <hostname>.
	ip := c.Args().First()
	hostname := c.Args().Get(1)

	h, err := hostsfile.NewHostsfile(HOSTSFILE)
	handleHostsfileError(err)

	host := hostsfile.NewHost(ip, hostname)
	err = h.AddEntry(host)

	if err != nil {
		log.Fatal(err)
	}
}

func cmdRemoveEntry(c *cli.Context) {
	hostname := c.Args().First()

	h, err := hostsfile.NewHostsfile(HOSTSFILE)
	handleHostsfileError(err)

	entry := h.FindEntry(hostname)

	// If the entry cannot be found, inform the user and exit gracefully.
	// Not providing the queried hostname should not produce a non-zero exit code,
	// but execution should stop here.
	if entry == nil {
		log.Infof("Unable to find a hosts entry with a hostname %s", hostname)
		os.Exit(0)
	}

	err = h.RemoveEntry(entry)

	if err != nil {
		log.Fatal(err)
	}
}

// Command to list all entries added by hostsup.
func cmdListEntry(c *cli.Context) {
	h, _ := hostsfile.NewHostsfile(HOSTSFILE, true)
	entries := h.GetEntries()

	w := tabwriter.NewWriter(os.Stdout, 5, 1, 3, ' ', 0)
	fmt.Fprintln(w, "HOSTNAME\tIP")

	for _, entry := range entries {
		fmt.Fprintf(w, "%s\t%s\n", entry.Hostname, entry.IP)
	}

	w.Flush()
}

// Command to remove all entries added by hostsup.
func cmdClean(c *cli.Context) {
	h, err := hostsfile.NewHostsfile(HOSTSFILE)
	handleHostsfileError(err)

	entries := h.Clean()

	for _, entry := range entries {
		log.Infof("Removed %s\t%s", entry.Hostname, entry.IP)
	}

	log.Info("Hosts file has been cleaned.")
}

// Report the correct error if the hosts file was not able to be opened.
func handleHostsfileError(err error) {
	if os.IsNotExist(err) {
		log.Fatalf("The file %s does not exists on your system.", HOSTSFILE)
	}

	if os.IsPermission(err) {
		log.Fatal("You do not have permission to edit this file. Try reissuing the command with sudo.")
	}
}
<commit_msg>Additional comments and logging.<commit_after>package main

import (
	"fmt"
	"os"
	"text/tabwriter"

	"github.com/codegangsta/cli"
	"github.com/damonkelley/hostsup/hostsfile"
)

var Commands = []cli.Command{
	{
		Name:   "add",
		Usage:  "Add a hosts entry",
		Action: cmdAddEntry,
	},
	{
		Name:   "rm",
		Usage:  "Remove a hosts entry",
		Action: cmdRemoveEntry,
	},
	{
		Name:   "ls",
		Usage:  "List hosts entries",
		Action: cmdListEntries,
	},
	{
		Name:   "clean",
		Usage:  "Remove all hosts entries added by hostsup",
		Action: cmdClean,
	},
}

const fileName string = "/etc/hosts"

// Format strings for the logger.
const addFormat = "Added \"%s\t%s\" to %s."
const removeFormat = "Removed \"%s\t%s\" from %s."

// Command to add an entry to the hosts file.
func cmdAddEntry(c *cli.Context) {
	// Argument order is: <IP>, <hostname>.
	ip := c.Args().First()
	hostname := c.Args().Get(1)

	h, err := hostsfile.NewHostsfile(fileName)
	defer h.Close()
	handleHostsfileError(err)

	host := hostsfile.NewHost(ip, hostname)
	err = h.AddEntry(host)

	if err != nil {
		log.Fatal(err)
	}

	log.Infof(addFormat, host.IP, host.Hostname, fileName)
}

// Command to remove an entry from the hosts file.
func cmdRemoveEntry(c *cli.Context) {
	hostname := c.Args().First()

	h, err := hostsfile.NewHostsfile(fileName)
	defer h.Close()
	handleHostsfileError(err)

	entry := h.FindEntry(hostname)

	// If the entry cannot be found, inform the user and exit gracefully.
	// Not providing the queried hostname should not produce a non-zero exit code,
	// but execution should stop here.
	if entry == nil {
		log.Infof("Unable to find a hosts entry with a hostname %s", hostname)
		os.Exit(0)
	}

	err = h.RemoveEntry(entry)

	if err != nil {
		log.Fatal(err)
	}

	log.Infof(removeFormat, entry.IP, entry.Hostname, fileName)
}

// Command to list all entries added by hostsup.
func cmdListEntries(c *cli.Context) {
	h, _ := hostsfile.NewHostsfile(fileName, true)
	defer h.Close()
	entries := h.GetEntries()

	w := tabwriter.NewWriter(os.Stdout, 5, 1, 3, ' ', 0)
	fmt.Fprintln(w, "HOSTNAME\tIP")

	for _, entry := range entries {
		fmt.Fprintf(w, "%s\t%s\n", entry.Hostname, entry.IP)
	}

	w.Flush()
}

// Command to remove all entries added by hostsup.
func cmdClean(c *cli.Context) {
	h, err := hostsfile.NewHostsfile(fileName)
	defer h.Close()
	handleHostsfileError(err)

	entries := h.Clean()

	for _, entry := range entries {
		log.Infof(removeFormat, entry.Hostname, entry.IP, fileName)
	}

	log.Info("Hosts file has been cleaned.")
}

// Report the correct error if the hosts file was not able to be opened.
func handleHostsfileError(err error) {
	if os.IsNotExist(err) {
		log.Fatalf("The file %s does not exists on your system.", fileName)
	}

	if os.IsPermission(err) {
		log.Fatal("You do not have permission to edit this file. Try reissuing the command with sudo.")
	}
}
<|endoftext|>


<commit_before>package main

import (
	"encoding/csv"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"github.com/AlexanderThaller/logger"
	"github.com/jinzhu/now"
	"github.com/juju/errgo"
)

type Command struct {
	Action        string
	Args          []string
	DataPath      string
	EndTime       time.Time
	Project       string
	SCM           string
	SCMAutoCommit bool
	SCMAutoPush   bool
	StartTime     time.Time
	TimeStamp     time.Time
	Value         string
}

const (
	CommitMessageTimeStampFormat = RecordTimeStampFormat
	DateFormat                   = "2006-01-02"
)

const (
	ActionDone      = "done"
	ActionListDates = "listdates"
	ActionList      = "list"
	ActionListNotes = "listnotes"
	ActionListTodos = "listtodos"
	ActionNote      = "note"
	ActionTodo      = "todo"
)

func NewCommand() *Command {
	return new(Command)
}

func (com *Command) Run() error {
	if com.DataPath == "" {
		return errgo.New("the datapath can not be empty")
	}

	switch com.Action {
	case ActionDone:
		return com.runDone()
	case ActionNote:
		return com.runNote()
	case ActionListDates:
		return com.runListDates()
	case ActionList:
		return com.runList()
	case ActionListNotes:
		return com.runListNotes()
	case ActionListTodos:
		return com.runListTodos()
	case ActionTodo:
		return com.runTodo()
	default:
		return errgo.New("Do not recognize the action: " + com.Action)
	}
}

func (com *Command) runDone() error {
	l := logger.New(Name, "Command", "run", "Done")

	l.Trace("Args length: ", len(com.Args))
	if com.Value == "" {
		return errgo.New("todo command needs a value")
	}
	l.Trace("Project: ", com.Project)
	if com.Project == "" {
		return errgo.New("todo command needs an project")
	}

	done := new(Todo)
	done.Project = com.Project
	done.TimeStamp = com.TimeStamp
	done.Value = com.Value
	done.Done = true
	l.Trace("Done: ", fmt.Sprintf("%+v", done))

	return com.Write(done)
}

func (com *Command) runNote() error {
	l := logger.New(Name, "Command", "run", "Note")

	l.Trace("Args length: ", len(com.Args))
	if com.Value == "" {
		return errgo.New("note command needs a value")
	}
	l.Trace("Project: ", com.Project)
	if com.Project == "" {
		return errgo.New("note command needs an project")
	}

	note := new(Note)
	note.Project = com.Project
	note.TimeStamp = com.TimeStamp
	note.Value = com.Value
	l.Trace("Note: ", fmt.Sprintf("%+v", note))

	return com.Write(note)
}

func (com *Command) runTodo() error {
	l := logger.New(Name, "Command", "run", "Todo")

	l.Trace("Args length: ", len(com.Args))
	if com.Value == "" {
		return errgo.New("todo command needs a value")
	}
	l.Trace("Project: ", com.Project)
	if com.Project == "" {
		return errgo.New("todo command needs an project")
	}

	todo := new(Todo)
	todo.Project = com.Project
	todo.TimeStamp = com.TimeStamp
	todo.Value = com.Value
	todo.Done = false
	l.Trace("Todo: ", fmt.Sprintf("%+v", todo))

	return com.Write(todo)
}

func (com *Command) runList() error {
	if com.Project == "" {
		return com.runListProjects()
	} else {
		return com.runListProjectNotes(com.Project)
	}
}

func (com *Command) runListNotes() error {
	if com.Project != "" {
		return com.runListProjectNotes(com.Project)
	}

	projects, err := com.getProjects()
	if err != nil {
		return err
	}

	for _, project := range projects {
		err := com.runListProjectNotes(project)
		if err != nil {
			return err
		}
	}

	return nil
}

func (com *Command) runListTodos() error {
	if com.Project != "" {
		return com.runListProjectTodos(com.Project)
	}

	projects, err := com.getProjects()
	if err != nil {
		return err
	}

	for _, project := range projects {
		err := com.runListProjectTodos(project)
		if err != nil {
			return err
		}
	}

	return nil
}

func (com *Command) runListProjects() error {
	projects, err := com.getProjects()
	if err != nil {
		return err
	}

	for _, project := range projects {
		fmt.Println(project)
	}

	return nil
}

func (com *Command) runListDates() error {
	l := logger.New(Name, "Command", "run", "ListDates")

	var dates []string
	var err error

	if com.Project == "" {
		dates, err = com.getDates()
	} else {
		dates, err = com.getProjectDates(com.Project)
	}

	if err != nil {
		return err
	}

	sort.Strings(dates)
	for _, date := range dates {
		timestamp, err := now.Parse(date)
		if err != nil {
			l.Warning("Can not parse timestamp: ", errgo.Details(err))
			continue
		}

		if timestamp.Before(com.StartTime) {
			continue
		}

		if timestamp.After(com.EndTime) {
			continue
		}

		fmt.Println(date)
	}

	return nil
}

func (com *Command) getDates() ([]string, error) {
	projects, err := com.getProjects()
	if err != nil {
		return nil, err
	}

	datemap := make(map[string]struct{})
	for _, project := range projects {
		dates, err := com.getProjectDates(project)
		if err != nil {
			return nil, err
		}

		for _, date := range dates {
			datemap[date] = struct{}{}
		}
	}

	var out []string
	for date := range datemap {
		out = append(out, date)
	}

	return out, nil
}

func (com *Command) getProjectDates(project string) ([]string, error) {
	if com.DataPath == "" {
		return nil, errgo.New("datapath can not be empty")
	}
	if project == "" {
		return nil, errgo.New("project name can not be empty")
	}
	if !com.checkProjectExists(project) {
		return nil, errgo.New("project does not exist")
	}

	var out []string

	records, err := com.getProjectNotes(project)
	if err != nil {
		return nil, err
	}

	datemap := make(map[string]struct{})

	for _, record := range records {
		date, err := time.Parse(RecordTimeStampFormat, record.GetTimeStamp())
		if err != nil {
			return nil, err
		}

		datemap[date.Format(DateFormat)] = struct{}{}
	}

	for date := range datemap {
		out = append(out, date)
	}

	return out, nil
}

func (com *Command) getProjects() ([]string, error) {
	dir, err := ioutil.ReadDir(com.DataPath)
	if err != nil {
		return nil, err
	}

	var out []string
	for _, d := range dir {
		file := d.Name()

		// Skip dotfiles
		if strings.HasPrefix(file, ".") {
			continue
		}

		ext := filepath.Ext(file)
		name := file[0 : len(file)-len(ext)]

		out = append(out, name)
	}

	sort.Strings(out)
	return out, nil
}

func (com *Command) runListProjectTodos(project string) error {
	if project == "" {
		return errgo.New("project name can not be empty")
	}
	if !com.checkProjectExists(project) {
		return errgo.New("no notes for this project")
	}

	todos, err := com.getProjectTodos(project)
	if err != nil {
		return err
	}
	todos = com.filterTodos(todos)

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

	fmt.Println("#", project, "- Todos")

	sort.Sort(TodoByDate(todos))
	for _, todo := range todos {
		fmt.Println("  *", todo.GetValue())
	}
	fmt.Println("")

	return nil
}

func (com *Command) filterTodos(todos []Todo) []Todo {
	filter := make(map[string]Todo)

	sort.Sort(TodoByDate(todos))
	for _, todo := range todos {
		filter[todo.Value] = todo
	}

	var out []Todo
	for _, todo := range filter {
		if todo.Done {
			continue
		}

		out = append(out, todo)
	}

	return out
}

func (com *Command) runListProjectNotes(project string) error {
	if project == "" {
		return errgo.New("project name can not be empty")
	}
	if !com.checkProjectExists(project) {
		return errgo.New("no notes for this project")
	}

	notes, err := com.getProjectNotes(project)
	if err != nil {
		return err
	}

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

	fmt.Println("#", project)
	sort.Sort(NotesByDate(notes))
	for _, note := range notes {
		fmt.Println("##", note.GetTimeStamp())
		fmt.Println(note.GetValue())
		fmt.Println("")
	}

	return nil
}

func (com *Command) checkProjectExists(project string) bool {
	projects, err := com.getProjects()
	if err != nil {
		return false
	}

	for _, d := range projects {
		if d == project {
			return true
		}
	}

	return false
}

func (com *Command) getProjectNotes(project string) ([]Note, error) {
	l := logger.New(Name, "Command", "get", "ProjectRecords")
	l.SetLevel(logger.Debug)

	if com.DataPath == "" {
		return nil, errgo.New("datapath can not be empty")
	}
	if project == "" {
		return nil, errgo.New("project name can not be empty")
	}
	if !com.checkProjectExists(project) {
		return nil, errgo.New("project does not exist")
	}

	filepath := filepath.Join(com.DataPath, project+".csv")
	file, err := os.OpenFile(filepath, os.O_RDONLY, 0640)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	reader := csv.NewReader(file)
	reader.FieldsPerRecord = 3

	var out []Note
	for {
		csv, err := reader.Read()
		if err != nil {
			if err == io.EOF {
				break
			}

			continue
		}

		note, err := NoteFromCSV(csv)
		if err != nil {
			continue
		}
		note.SetProject(project)

		if note.TimeStamp.Before(com.StartTime) {
			continue
		}

		if note.TimeStamp.After(com.EndTime) {
			continue
		}

		out = append(out, note)
	}

	return out, err
}

func (com *Command) getProjectTodos(project string) ([]Todo, error) {
	l := logger.New(Name, "Command", "get", "ProjectRecords")
	l.SetLevel(logger.Debug)

	if com.DataPath == "" {
		return nil, errgo.New("datapath can not be empty")
	}
	if project == "" {
		return nil, errgo.New("project name can not be empty")
	}
	if !com.checkProjectExists(project) {
		return nil, errgo.New("project does not exist")
	}

	filepath := filepath.Join(com.DataPath, project+".csv")
	file, err := os.OpenFile(filepath, os.O_RDONLY, 0640)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	reader := csv.NewReader(file)
	reader.FieldsPerRecord = 4

	var out []Todo
	for {
		csv, err := reader.Read()
		if err != nil {
			if err == io.EOF {
				break
			}

			continue
		}

		todo, err := TodoFromCSV(csv)
		if err != nil {
			continue
		}

		out = append(out, todo)
	}

	return out, err
}

func (com *Command) Write(record Record) error {
	if com.DataPath == "" {
		return errgo.New("datapath can not be empty")
	}

	if com.Project == "" {
		return errgo.New("project name can not be empty")
	}

	path := com.DataPath
	project := com.Project

	err := os.MkdirAll(path, 0750)
	if err != nil {
		return err
	}

	filepath := filepath.Join(path, project+".csv")
	file, err := os.OpenFile(filepath, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0640)
	if err != nil {
		return err
	}
	defer file.Close()

	writer := csv.NewWriter(file)
	err = writer.Write(record.CSV())
	if err != nil {
		return err
	}
	writer.Flush()

	err = com.Commit(record)
	if err != nil {
		return err
	}

	return nil
}

func (com *Command) Commit(record Record) error {
	if !com.SCMAutoCommit {
		return nil
	}

	if com.SCM == "" {
		return errgo.New("Can not use an empty scm for commiting")
	}

	filename := record.GetProject() + ".csv"
	err := scmAdd(com.SCM, com.DataPath, filename)
	if err != nil {
		return err
	}

	message := com.Project + " - "
	message += record.GetAction() + " - "
	message += com.TimeStamp.Format(CommitMessageTimeStampFormat)
	err = scmCommit(com.SCM, com.DataPath, message)
	if err != nil {
		return err
	}

	return nil
}
<commit_msg>added listprojects command.<commit_after>package main

import (
	"encoding/csv"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"github.com/AlexanderThaller/logger"
	"github.com/jinzhu/now"
	"github.com/juju/errgo"
)

type Command struct {
	Action        string
	Args          []string
	DataPath      string
	EndTime       time.Time
	Project       string
	SCM           string
	SCMAutoCommit bool
	SCMAutoPush   bool
	StartTime     time.Time
	TimeStamp     time.Time
	Value         string
}

const (
	CommitMessageTimeStampFormat = RecordTimeStampFormat
	DateFormat                   = "2006-01-02"
)

const (
	ActionDone         = "done"
	ActionListDates    = "listdates"
	ActionList         = "list"
	ActionListNotes    = "listnotes"
	ActionListProjects = "listprojects"
	ActionListTodos    = "listtodos"
	ActionNote         = "note"
	ActionTodo         = "todo"
)

func NewCommand() *Command {
	return new(Command)
}

func (com *Command) Run() error {
	if com.DataPath == "" {
		return errgo.New("the datapath can not be empty")
	}

	switch com.Action {
	case ActionDone:
		return com.runDone()
	case ActionNote:
		return com.runNote()
	case ActionListDates:
		return com.runListDates()
	case ActionList:
		return com.runList()
	case ActionListNotes:
		return com.runListNotes()
	case ActionListProjects:
		return com.runListProjects()
	case ActionListTodos:
		return com.runListTodos()
	case ActionTodo:
		return com.runTodo()
	default:
		return errgo.New("Do not recognize the action: " + com.Action)
	}
}

func (com *Command) runDone() error {
	l := logger.New(Name, "Command", "run", "Done")

	l.Trace("Args length: ", len(com.Args))
	if com.Value == "" {
		return errgo.New("todo command needs a value")
	}
	l.Trace("Project: ", com.Project)
	if com.Project == "" {
		return errgo.New("todo command needs an project")
	}

	done := new(Todo)
	done.Project = com.Project
	done.TimeStamp = com.TimeStamp
	done.Value = com.Value
	done.Done = true
	l.Trace("Done: ", fmt.Sprintf("%+v", done))

	return com.Write(done)
}

func (com *Command) runNote() error {
	l := logger.New(Name, "Command", "run", "Note")

	l.Trace("Args length: ", len(com.Args))
	if com.Value == "" {
		return errgo.New("note command needs a value")
	}
	l.Trace("Project: ", com.Project)
	if com.Project == "" {
		return errgo.New("note command needs an project")
	}

	note := new(Note)
	note.Project = com.Project
	note.TimeStamp = com.TimeStamp
	note.Value = com.Value
	l.Trace("Note: ", fmt.Sprintf("%+v", note))

	return com.Write(note)
}

func (com *Command) runTodo() error {
	l := logger.New(Name, "Command", "run", "Todo")

	l.Trace("Args length: ", len(com.Args))
	if com.Value == "" {
		return errgo.New("todo command needs a value")
	}
	l.Trace("Project: ", com.Project)
	if com.Project == "" {
		return errgo.New("todo command needs an project")
	}

	todo := new(Todo)
	todo.Project = com.Project
	todo.TimeStamp = com.TimeStamp
	todo.Value = com.Value
	todo.Done = false
	l.Trace("Todo: ", fmt.Sprintf("%+v", todo))

	return com.Write(todo)
}

func (com *Command) runList() error {
	if com.Project == "" {
		return com.runListProjects()
	} else {
		return com.runListProjectNotes(com.Project)
	}
}

func (com *Command) runListNotes() error {
	if com.Project != "" {
		return com.runListProjectNotes(com.Project)
	}

	projects, err := com.getProjects()
	if err != nil {
		return err
	}

	for _, project := range projects {
		err := com.runListProjectNotes(project)
		if err != nil {
			return err
		}
	}

	return nil
}

func (com *Command) runListTodos() error {
	if com.Project != "" {
		return com.runListProjectTodos(com.Project)
	}

	projects, err := com.getProjects()
	if err != nil {
		return err
	}

	for _, project := range projects {
		err := com.runListProjectTodos(project)
		if err != nil {
			return err
		}
	}

	return nil
}

func (com *Command) runListProjects() error {
	projects, err := com.getProjects()
	if err != nil {
		return err
	}

	for _, project := range projects {
		fmt.Println(project)
	}

	return nil
}

func (com *Command) runListDates() error {
	l := logger.New(Name, "Command", "run", "ListDates")

	var dates []string
	var err error

	if com.Project == "" {
		dates, err = com.getDates()
	} else {
		dates, err = com.getProjectDates(com.Project)
	}

	if err != nil {
		return err
	}

	sort.Strings(dates)
	for _, date := range dates {
		timestamp, err := now.Parse(date)
		if err != nil {
			l.Warning("Can not parse timestamp: ", errgo.Details(err))
			continue
		}

		if timestamp.Before(com.StartTime) {
			continue
		}

		if timestamp.After(com.EndTime) {
			continue
		}

		fmt.Println(date)
	}

	return nil
}

func (com *Command) getDates() ([]string, error) {
	projects, err := com.getProjects()
	if err != nil {
		return nil, err
	}

	datemap := make(map[string]struct{})
	for _, project := range projects {
		dates, err := com.getProjectDates(project)
		if err != nil {
			return nil, err
		}

		for _, date := range dates {
			datemap[date] = struct{}{}
		}
	}

	var out []string
	for date := range datemap {
		out = append(out, date)
	}

	return out, nil
}

func (com *Command) getProjectDates(project string) ([]string, error) {
	if com.DataPath == "" {
		return nil, errgo.New("datapath can not be empty")
	}
	if project == "" {
		return nil, errgo.New("project name can not be empty")
	}
	if !com.checkProjectExists(project) {
		return nil, errgo.New("project does not exist")
	}

	var out []string

	records, err := com.getProjectNotes(project)
	if err != nil {
		return nil, err
	}

	datemap := make(map[string]struct{})

	for _, record := range records {
		date, err := time.Parse(RecordTimeStampFormat, record.GetTimeStamp())
		if err != nil {
			return nil, err
		}

		datemap[date.Format(DateFormat)] = struct{}{}
	}

	for date := range datemap {
		out = append(out, date)
	}

	return out, nil
}

func (com *Command) getProjects() ([]string, error) {
	dir, err := ioutil.ReadDir(com.DataPath)
	if err != nil {
		return nil, err
	}

	var out []string
	for _, d := range dir {
		file := d.Name()

		// Skip dotfiles
		if strings.HasPrefix(file, ".") {
			continue
		}

		ext := filepath.Ext(file)
		name := file[0 : len(file)-len(ext)]

		out = append(out, name)
	}

	sort.Strings(out)
	return out, nil
}

func (com *Command) runListProjectTodos(project string) error {
	if project == "" {
		return errgo.New("project name can not be empty")
	}
	if !com.checkProjectExists(project) {
		return errgo.New("no notes for this project")
	}

	todos, err := com.getProjectTodos(project)
	if err != nil {
		return err
	}
	todos = com.filterTodos(todos)

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

	fmt.Println("#", project, "- Todos")

	sort.Sort(TodoByDate(todos))
	for _, todo := range todos {
		fmt.Println("  *", todo.GetValue())
	}
	fmt.Println("")

	return nil
}

func (com *Command) filterTodos(todos []Todo) []Todo {
	filter := make(map[string]Todo)

	sort.Sort(TodoByDate(todos))
	for _, todo := range todos {
		filter[todo.Value] = todo
	}

	var out []Todo
	for _, todo := range filter {
		if todo.Done {
			continue
		}

		out = append(out, todo)
	}

	return out
}

func (com *Command) runListProjectNotes(project string) error {
	if project == "" {
		return errgo.New("project name can not be empty")
	}
	if !com.checkProjectExists(project) {
		return errgo.New("no notes for this project")
	}

	notes, err := com.getProjectNotes(project)
	if err != nil {
		return err
	}

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

	fmt.Println("#", project)
	sort.Sort(NotesByDate(notes))
	for _, note := range notes {
		fmt.Println("##", note.GetTimeStamp())
		fmt.Println(note.GetValue())
		fmt.Println("")
	}

	return nil
}

func (com *Command) checkProjectExists(project string) bool {
	projects, err := com.getProjects()
	if err != nil {
		return false
	}

	for _, d := range projects {
		if d == project {
			return true
		}
	}

	return false
}

func (com *Command) getProjectNotes(project string) ([]Note, error) {
	l := logger.New(Name, "Command", "get", "ProjectRecords")
	l.SetLevel(logger.Debug)

	if com.DataPath == "" {
		return nil, errgo.New("datapath can not be empty")
	}
	if project == "" {
		return nil, errgo.New("project name can not be empty")
	}
	if !com.checkProjectExists(project) {
		return nil, errgo.New("project does not exist")
	}

	filepath := filepath.Join(com.DataPath, project+".csv")
	file, err := os.OpenFile(filepath, os.O_RDONLY, 0640)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	reader := csv.NewReader(file)
	reader.FieldsPerRecord = 3

	var out []Note
	for {
		csv, err := reader.Read()
		if err != nil {
			if err == io.EOF {
				break
			}

			continue
		}

		note, err := NoteFromCSV(csv)
		if err != nil {
			continue
		}
		note.SetProject(project)

		if note.TimeStamp.Before(com.StartTime) {
			continue
		}

		if note.TimeStamp.After(com.EndTime) {
			continue
		}

		out = append(out, note)
	}

	return out, err
}

func (com *Command) getProjectTodos(project string) ([]Todo, error) {
	l := logger.New(Name, "Command", "get", "ProjectRecords")
	l.SetLevel(logger.Debug)

	if com.DataPath == "" {
		return nil, errgo.New("datapath can not be empty")
	}
	if project == "" {
		return nil, errgo.New("project name can not be empty")
	}
	if !com.checkProjectExists(project) {
		return nil, errgo.New("project does not exist")
	}

	filepath := filepath.Join(com.DataPath, project+".csv")
	file, err := os.OpenFile(filepath, os.O_RDONLY, 0640)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	reader := csv.NewReader(file)
	reader.FieldsPerRecord = 4

	var out []Todo
	for {
		csv, err := reader.Read()
		if err != nil {
			if err == io.EOF {
				break
			}

			continue
		}

		todo, err := TodoFromCSV(csv)
		if err != nil {
			continue
		}

		out = append(out, todo)
	}

	return out, err
}

func (com *Command) Write(record Record) error {
	if com.DataPath == "" {
		return errgo.New("datapath can not be empty")
	}

	if com.Project == "" {
		return errgo.New("project name can not be empty")
	}

	path := com.DataPath
	project := com.Project

	err := os.MkdirAll(path, 0750)
	if err != nil {
		return err
	}

	filepath := filepath.Join(path, project+".csv")
	file, err := os.OpenFile(filepath, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0640)
	if err != nil {
		return err
	}
	defer file.Close()

	writer := csv.NewWriter(file)
	err = writer.Write(record.CSV())
	if err != nil {
		return err
	}
	writer.Flush()

	err = com.Commit(record)
	if err != nil {
		return err
	}

	return nil
}

func (com *Command) Commit(record Record) error {
	if !com.SCMAutoCommit {
		return nil
	}

	if com.SCM == "" {
		return errgo.New("Can not use an empty scm for commiting")
	}

	filename := record.GetProject() + ".csv"
	err := scmAdd(com.SCM, com.DataPath, filename)
	if err != nil {
		return err
	}

	message := com.Project + " - "
	message += record.GetAction() + " - "
	message += com.TimeStamp.Format(CommitMessageTimeStampFormat)
	err = scmCommit(com.SCM, com.DataPath, message)
	if err != nil {
		return err
	}

	return nil
}
<|endoftext|>


<commit_before>// This Source Code Form is subject to the terms of the Mozilla Public License,
// v. 2.0. If a copy of the MPL was not distributed with this file, You can
// obtain one at http://mozilla.org/MPL/2.0/.

package runtime

import (
	"fmt"

	"github.com/ta2gch/iris/runtime/env"
	"github.com/ta2gch/iris/runtime/ilos"
	"github.com/ta2gch/iris/runtime/ilos/class"
	"github.com/ta2gch/iris/runtime/ilos/instance"
)

// Setq represents an assignment to the variable denoted by the identifier. In
// consequence, the identifier may designate a different object than before, the
// value of form. The result of the evaluation of form is returned. This result
// is used to modify the variable binding denoted by the identifier var (if it
// is mutable). setq can be used only for modifying bindings, and not for
// establishing a variable. The setq special form must be contained in the scope
// of var , established by defglobal, let, let*, for, or a lambda expression.
func Setq(e env.Environment, var1, form ilos.Instance) (ilos.Instance, ilos.Instance) {
	ret, err := Eval(e, form)
	if err != nil {
		return nil, err
	}
	if e.Variable.Set(var1, ret) {
		return ret, nil
	}
	if e.Variable.Set(var1, ret) {
		return ret, nil
	}
	return SignalCondition(e, instance.NewUndefinedVariable(e, var1), Nil)
}

func Setf(e env.Environment, var1, form ilos.Instance) (ilos.Instance, ilos.Instance) {
	if ilos.InstanceOf(class.Symbol, var1) {
		val, err := Eval(e, form)
		if err != nil {
			return nil, err
		}
		return Setq(e, var1, val)
	}
	funcSpec := instance.NewSymbol(fmt.Sprintf("(SETF %v)", var1.(instance.List).Nth(0)))
	fun, ok := e.Function.Get(funcSpec)
	if !ok {
		return SignalCondition(e, instance.NewUndefinedFunction(e, funcSpec), Nil)
	}
	arguments, err := evalArguments(e, instance.NewCons(form, var1.(*instance.Cons).Cdr))
	if err != nil {
		return nil, err
	}
	return fun.(instance.Applicable).Apply(e, arguments.(instance.List).Slice()...)
}

// Let is used to define a scope for a group of identifiers for a sequence of
// forms body-form* (collectively referred to as the body). The list of pairs
// (var form)* is called the let variable list. The scope of the identifier var
// is the body. The forms are evaluated sequentially from left to right; then
// each variable denoted by the identifier var is initialized to the
// corresponding value. Using these bindings along with the already existing
// bindings of visible identifiers the body-forms are evaluated. The returned
// value of let is the result of the evaluation of the last body-form of its
// body (or nil if there is none). No var may appear more than once in let
// variable list.
func Let(e env.Environment, varForm ilos.Instance, bodyForm ...ilos.Instance) (ilos.Instance, ilos.Instance) {
	vfs := map[ilos.Instance]ilos.Instance{}
	if err := ensure(e, class.List, varForm); err != nil {
		return nil, err
	}
	for _, cadr := range varForm.(instance.List).Slice() {
		if err := ensure(e, class.List, cadr); err != nil {
			return nil, err
		}
		if cadr.(instance.List).Length() != 2 {
		return SignalCondition(e, instance.NewArityError(e), Nil)
		}
		f, err := Eval(e, cadr.(instance.List).Nth(1))
		if err != nil {
			return nil, err
		}
		vfs[cadr.(instance.List).Nth(0)] = f
	}
	for v, f := range vfs {
		if !e.Variable.Define(v, f) {
			return SignalCondition(e, instance.NewImmutableBinding(e), Nil)
		}
	}
	return Progn(e, bodyForm...)
}

// LetStar form is used to define a scope for a group of identifiers for a
// sequence of forms body-form* (collectively referred to as the body). The
// first subform (the let* variable list) is a list of pairs (var form). The
// scope of an identifier var is the body along with all form forms following
// the pair (var form) in the let* variable list. For each pair (var form) the
// following is done: form is evaluated in the context of the bindings in effect
// at that point in the evaluation. The result of the evaluation is bound to its
// associated variable named by the identifier var . These variable bindings
// enlarge the set of current valid identifiers perhaps shadowing previous
// variable bindings (in case some var was defined outside), and in this
// enlarged or modified eironment the body-forms are executed. The returned
// value of let* is the result of the evaluation of the last form of its body
// (or nil if there is none).
func LetStar(e env.Environment, varForm ilos.Instance, bodyForm ...ilos.Instance) (ilos.Instance, ilos.Instance) {
	if err := ensure(e, class.List, varForm); err != nil {
		return nil, err
	}
	for _, cadr := range varForm.(instance.List).Slice() {
		if err := ensure(e, class.List, cadr); err != nil {
			return nil, err
		}
		if cadr.(instance.List).Length() != 2 {
		return SignalCondition(e, instance.NewArityError(e), Nil)
		}
		f, err := Eval(e, cadr.(instance.List).Nth(1))
		if err != nil {
			return nil, err
		}
		if !e.Variable.Define(cadr.(instance.List).Nth(0), f) {
			return SignalCondition(e, instance.NewImmutableBinding(e), Nil)
		}
	}
	return Progn(e, bodyForm...)
}
<commit_msg>refactor<commit_after>// This Source Code Form is subject to the terms of the Mozilla Public License,
// v. 2.0. If a copy of the MPL was not distributed with this file, You can
// obtain one at http://mozilla.org/MPL/2.0/.

package runtime

import (
	"fmt"

	"github.com/ta2gch/iris/runtime/env"
	"github.com/ta2gch/iris/runtime/ilos"
	"github.com/ta2gch/iris/runtime/ilos/class"
	"github.com/ta2gch/iris/runtime/ilos/instance"
)

// Setq represents an assignment to the variable denoted by the identifier. In
// consequence, the identifier may designate a different object than before, the
// value of form. The result of the evaluation of form is returned. This result
// is used to modify the variable binding denoted by the identifier var (if it
// is mutable). setq can be used only for modifying bindings, and not for
// establishing a variable. The setq special form must be contained in the scope
// of var , established by defglobal, let, let*, for, or a lambda expression.
func Setq(e env.Environment, var1, form ilos.Instance) (ilos.Instance, ilos.Instance) {
	ret, err := Eval(e, form)
	if err != nil {
		return nil, err
	}
	if e.Variable.Set(var1, ret) {
		return ret, nil
	}
	return SignalCondition(e, instance.NewUndefinedVariable(e, var1), Nil)
}

func Setf(e env.Environment, var1, form ilos.Instance) (ilos.Instance, ilos.Instance) {
	if ilos.InstanceOf(class.Symbol, var1) {
		return Setq(e, var1, form)
	}
	funcSpec := instance.NewSymbol(fmt.Sprintf("(SETF %v)", var1.(instance.List).Nth(0)))
	fun, ok := e.Function.Get(funcSpec)
	if !ok {
		return SignalCondition(e, instance.NewUndefinedFunction(e, funcSpec), Nil)
	}
	arguments, err := evalArguments(e, instance.NewCons(form, var1.(*instance.Cons).Cdr))
	if err != nil {
		return nil, err
	}
	return fun.(instance.Applicable).Apply(e, arguments.(instance.List).Slice()...)
}

// Let is used to define a scope for a group of identifiers for a sequence of
// forms body-form* (collectively referred to as the body). The list of pairs
// (var form)* is called the let variable list. The scope of the identifier var
// is the body. The forms are evaluated sequentially from left to right; then
// each variable denoted by the identifier var is initialized to the
// corresponding value. Using these bindings along with the already existing
// bindings of visible identifiers the body-forms are evaluated. The returned
// value of let is the result of the evaluation of the last body-form of its
// body (or nil if there is none). No var may appear more than once in let
// variable list.
func Let(e env.Environment, varForm ilos.Instance, bodyForm ...ilos.Instance) (ilos.Instance, ilos.Instance) {
	vfs := map[ilos.Instance]ilos.Instance{}
	if err := ensure(e, class.List, varForm); err != nil {
		return nil, err
	}
	for _, cadr := range varForm.(instance.List).Slice() {
		if err := ensure(e, class.List, cadr); err != nil {
			return nil, err
		}
		if cadr.(instance.List).Length() != 2 {
			return SignalCondition(e, instance.NewArityError(e), Nil)
		}
		f, err := Eval(e, cadr.(instance.List).Nth(1))
		if err != nil {
			return nil, err
		}
		vfs[cadr.(instance.List).Nth(0)] = f
	}
	for v, f := range vfs {
		if !e.Variable.Define(v, f) {
			return SignalCondition(e, instance.NewImmutableBinding(e), Nil)
		}
	}
	return Progn(e, bodyForm...)
}

// LetStar form is used to define a scope for a group of identifiers for a
// sequence of forms body-form* (collectively referred to as the body). The
// first subform (the let* variable list) is a list of pairs (var form). The
// scope of an identifier var is the body along with all form forms following
// the pair (var form) in the let* variable list. For each pair (var form) the
// following is done: form is evaluated in the context of the bindings in effect
// at that point in the evaluation. The result of the evaluation is bound to its
// associated variable named by the identifier var . These variable bindings
// enlarge the set of current valid identifiers perhaps shadowing previous
// variable bindings (in case some var was defined outside), and in this
// enlarged or modified eironment the body-forms are executed. The returned
// value of let* is the result of the evaluation of the last form of its body
// (or nil if there is none).
func LetStar(e env.Environment, varForm ilos.Instance, bodyForm ...ilos.Instance) (ilos.Instance, ilos.Instance) {
	if err := ensure(e, class.List, varForm); err != nil {
		return nil, err
	}
	for _, cadr := range varForm.(instance.List).Slice() {
		if err := ensure(e, class.List, cadr); err != nil {
			return nil, err
		}
		if cadr.(instance.List).Length() != 2 {
			return SignalCondition(e, instance.NewArityError(e), Nil)
		}
		f, err := Eval(e, cadr.(instance.List).Nth(1))
		if err != nil {
			return nil, err
		}
		if !e.Variable.Define(cadr.(instance.List).Nth(0), f) {
			return SignalCondition(e, instance.NewImmutableBinding(e), Nil)
		}
	}
	return Progn(e, bodyForm...)
}
<|endoftext|>


<commit_before>// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
// of this source code is governed by the MIT license that can be found in
// the LICENSE file.

package girc

import (
	"errors"
	"fmt"
)

// Commands holds a large list of useful methods to interact with the server,
// and wrappers for common events.
type Commands struct {
	c *Client
}

// Nick changes the client nickname.
func (cmd *Commands) Nick(name string) error {
	if !IsValidNick(name) {
		return &ErrInvalidTarget{Target: name}
	}

	cmd.c.Send(&Event{Command: NICK, Params: []string{name}})
	return nil
}

// Join attempts to enter a list of IRC channels, at bulk if possible to
// prevent sending extensive JOIN commands.
func (cmd *Commands) Join(channels ...string) error {
	// We can join multiple channels at once, however we need to ensure that
	// we are not exceeding the line length. (see maxLength)
	max := maxLength - len(JOIN) - 1

	var buffer string

	for i := 0; i < len(channels); i++ {
		if !IsValidChannel(channels[i]) {
			return &ErrInvalidTarget{Target: channels[i]}
		}

		if len(buffer+","+channels[i]) > max {
			cmd.c.Send(&Event{Command: JOIN, Params: []string{buffer}})
			buffer = ""
			continue
		}

		if len(buffer) == 0 {
			buffer = channels[i]
		} else {
			buffer += "," + channels[i]
		}

		if i == len(channels)-1 {
			cmd.c.Send(&Event{Command: JOIN, Params: []string{buffer}})
			return nil
		}
	}

	return nil
}

// JoinKey attempts to enter an IRC channel with a password.
func (cmd *Commands) JoinKey(channel, password string) error {
	if !IsValidChannel(channel) {
		return &ErrInvalidTarget{Target: channel}
	}

	cmd.c.Send(&Event{Command: JOIN, Params: []string{channel, password}})
	return nil
}

// Part leaves an IRC channel.
func (cmd *Commands) Part(channel, message string) error {
	if !IsValidChannel(channel) {
		return &ErrInvalidTarget{Target: channel}
	}

	cmd.c.Send(&Event{Command: PART, Params: []string{channel}})
	return nil
}

// PartMessage leaves an IRC channel with a specified leave message.
func (cmd *Commands) PartMessage(channel, message string) error {
	if !IsValidChannel(channel) {
		return &ErrInvalidTarget{Target: channel}
	}

	cmd.c.Send(&Event{Command: PART, Params: []string{channel}, Trailing: message})
	return nil
}

// SendCTCP sends a CTCP request to target. Note that this method uses
// PRIVMSG specifically.
func (cmd *Commands) SendCTCP(target, ctcpType, message string) error {
	out := encodeCTCPRaw(ctcpType, message)
	if out == "" {
		return errors.New("invalid CTCP")
	}

	return cmd.Message(target, out)
}

// SendCTCPf sends a CTCP request to target using a specific format. Note that
// this method uses PRIVMSG specifically.
func (cmd *Commands) SendCTCPf(target, ctcpType, format string, a ...interface{}) error {
	return cmd.SendCTCP(target, ctcpType, fmt.Sprintf(format, a...))
}

// SendCTCPReplyf sends a CTCP response to target using a specific format.
// Note that this method uses NOTICE specifically.
func (cmd *Commands) SendCTCPReplyf(target, ctcpType, format string, a ...interface{}) error {
	return cmd.SendCTCPReply(target, ctcpType, fmt.Sprintf(format, a...))
}

// SendCTCPReply sends a CTCP response to target. Note that this method uses
// NOTICE specifically.
func (cmd *Commands) SendCTCPReply(target, ctcpType, message string) error {
	out := encodeCTCPRaw(ctcpType, message)
	if out == "" {
		return errors.New("invalid CTCP")
	}

	return cmd.Notice(target, out)
}

// Message sends a PRIVMSG to target (either channel, service, or user).
func (cmd *Commands) Message(target, message string) error {
	if !IsValidNick(target) && !IsValidChannel(target) {
		return &ErrInvalidTarget{Target: target}
	}

	cmd.c.Send(&Event{Command: PRIVMSG, Params: []string{target}, Trailing: message})
	return nil
}

// Messagef sends a formated PRIVMSG to target (either channel, service, or
// user).
func (cmd *Commands) Messagef(target, format string, a ...interface{}) error {
	return cmd.Message(target, fmt.Sprintf(format, a...))
}

// ErrInvalidSource is returned when a method needs to know the origin of an
// event, however Event.Source is unknown (e.g. sent by the user, not the
// server.)
var ErrInvalidSource = errors.New("event has nil or invalid source address")

// Reply sends a reply to channel or user, based on where the supplied event
// originated from. See also ReplyTo().
func (cmd *Commands) Reply(event Event, message string) error {
	if event.Source == nil {
		return ErrInvalidSource
	}

	if len(event.Params) > 0 && IsValidChannel(event.Params[0]) {
		return cmd.Message(event.Params[0], message)
	}

	return cmd.Message(event.Source.Name, message)
}

// Replyf sends a reply to channel or user with a format string, based on
// where the supplied event originated from. See also ReplyTof().
func (cmd *Commands) Replyf(event Event, format string, a ...interface{}) error {
	return cmd.Reply(event, fmt.Sprintf(format, a...))
}

// ReplyTo sends a reply to a channel or user, based on where the supplied
// event originated from. ReplyTo(), when originating from a channel will
// default to replying with "<user>, <message>". See also Reply().
func (cmd *Commands) ReplyTo(event Event, message string) error {
	if event.Source == nil {
		return ErrInvalidSource
	}

	if len(event.Params) > 0 && IsValidChannel(event.Params[0]) {
		return cmd.Message(event.Params[0], event.Source.Name+", "+message)
	}

	return cmd.Message(event.Source.Name, message)
}

// ReplyTof sends a reply to a channel or user with a format string, based
// on where the supplied event originated from. ReplyTo(), when originating
// from a channel will default to replying with "<user>, <message>". See
// also Replyf().
func (cmd *Commands) ReplyTof(event Event, format string, a ...interface{}) error {
	return cmd.ReplyTo(event, fmt.Sprintf(format, a...))
}

// Action sends a PRIVMSG ACTION (/me) to target (either channel, service,
// or user).
func (cmd *Commands) Action(target, message string) error {
	if !IsValidNick(target) && !IsValidChannel(target) {
		return &ErrInvalidTarget{Target: target}
	}

	cmd.c.Send(&Event{
		Command:  PRIVMSG,
		Params:   []string{target},
		Trailing: fmt.Sprintf("\001ACTION %s\001", message),
	})
	return nil
}

// Actionf sends a formated PRIVMSG ACTION (/me) to target (either channel,
// service, or user).
func (cmd *Commands) Actionf(target, format string, a ...interface{}) error {
	return cmd.Action(target, fmt.Sprintf(format, a...))
}

// Notice sends a NOTICE to target (either channel, service, or user).
func (cmd *Commands) Notice(target, message string) error {
	if !IsValidNick(target) && !IsValidChannel(target) {
		return &ErrInvalidTarget{Target: target}
	}

	cmd.c.Send(&Event{Command: NOTICE, Params: []string{target}, Trailing: message})
	return nil
}

// Noticef sends a formated NOTICE to target (either channel, service, or
// user).
func (cmd *Commands) Noticef(target, format string, a ...interface{}) error {
	return cmd.Notice(target, fmt.Sprintf(format, a...))
}

// SendRaw sends a raw string back to the server, without carriage returns
// or newlines.
func (cmd *Commands) SendRaw(raw string) error {
	e := ParseEvent(raw)
	if e == nil {
		return errors.New("invalid event: " + raw)
	}

	cmd.c.Send(e)
	return nil
}

// SendRawf sends a formated string back to the server, without carriage
// returns or newlines.
func (cmd *Commands) SendRawf(format string, a ...interface{}) error {
	return cmd.SendRaw(fmt.Sprintf(format, a...))
}

// Topic sets the topic of channel to message. Does not verify the length
// of the topic.
func (cmd *Commands) Topic(channel, message string) {
	cmd.c.Send(&Event{Command: TOPIC, Params: []string{channel}, Trailing: message})
}

// Who sends a WHO query to the server, which will attempt WHOX by default.
// See http://faerion.sourceforge.net/doc/irc/whox.var for more details. This
// sends "%tcuhnr,2" per default. Do not use "1" as this will conflict with
// girc's builtin tracking functionality.
func (cmd *Commands) Who(target string) error {
	if !IsValidNick(target) && !IsValidChannel(target) && !IsValidUser(target) {
		return &ErrInvalidTarget{Target: target}
	}

	cmd.c.Send(&Event{Command: WHO, Params: []string{target, "%tcuhnr,2"}})
	return nil
}

// Whois sends a WHOIS query to the server, targeted at a specific user.
// as WHOIS is a bit slower, you may want to use WHO for brief user info.
func (cmd *Commands) Whois(nick string) error {
	if !IsValidNick(nick) {
		return &ErrInvalidTarget{Target: nick}
	}

	cmd.c.Send(&Event{Command: WHOIS, Params: []string{nick}})
	return nil
}

// Ping sends a PING query to the server, with a specific identifier that
// the server should respond with.
func (cmd *Commands) Ping(id string) {
	cmd.c.write(&Event{Command: PING, Params: []string{id}})
}

// Pong sends a PONG query to the server, with an identifier which was
// received from a previous PING query received by the client.
func (cmd *Commands) Pong(id string) {
	cmd.c.write(&Event{Command: PONG, Params: []string{id}})
}

// Oper sends a OPER authentication query to the server, with a username
// and password.
func (cmd *Commands) Oper(user, pass string) {
	cmd.c.Send(&Event{Command: OPER, Params: []string{user, pass}, Sensitive: true})
}

// Kick sends a KICK query to the server, attempting to kick nick from
// channel, with reason. If reason is blank, one will not be sent to the
// server.
func (cmd *Commands) Kick(channel, nick, reason string) error {
	if !IsValidChannel(channel) {
		return &ErrInvalidTarget{Target: channel}
	}

	if !IsValidNick(nick) {
		return &ErrInvalidTarget{Target: nick}
	}

	if reason != "" {
		cmd.c.Send(&Event{Command: KICK, Params: []string{channel, nick}, Trailing: reason})
		return nil
	}

	cmd.c.Send(&Event{Command: KICK, Params: []string{channel, nick}})
	return nil
}

// Invite sends a INVITE query to the server, to invite nick to channel.
func (cmd *Commands) Invite(channel, nick string) error {
	if !IsValidChannel(channel) {
		return &ErrInvalidTarget{Target: channel}
	}

	if !IsValidNick(nick) {
		return &ErrInvalidTarget{Target: nick}
	}

	cmd.c.Send(&Event{Command: INVITE, Params: []string{nick, channel}})
	return nil
}

// Away sends a AWAY query to the server, suggesting that the client is no
// longer active. If reason is blank, Client.Back() is called. Also see
// Client.Back().
func (cmd *Commands) Away(reason string) {
	if reason == "" {
		cmd.Back()
		return
	}

	cmd.c.Send(&Event{Command: AWAY, Params: []string{reason}})
}

// Back sends a AWAY query to the server, however the query is blank,
// suggesting that the client is active once again. Also see Client.Away().
func (cmd *Commands) Back() {
	cmd.c.Send(&Event{Command: AWAY})
}

// List sends a LIST query to the server, which will list channels and topics.
// Supports multiple channels at once, in hopes it will reduce extensive
// LIST queries to the server. Supply no channels to run a list against the
// entire server (warning, that may mean LOTS of channels!)
func (cmd *Commands) List(channels ...string) error {
	if len(channels) == 0 {
		cmd.c.Send(&Event{Command: LIST})
		return nil
	}

	// We can LIST multiple channels at once, however we need to ensure that
	// we are not exceeding the line length. (see maxLength)
	max := maxLength - len(JOIN) - 1

	var buffer string

	for i := 0; i < len(channels); i++ {
		if !IsValidChannel(channels[i]) {
			return &ErrInvalidTarget{Target: channels[i]}
		}

		if len(buffer+","+channels[i]) > max {
			cmd.c.Send(&Event{Command: LIST, Params: []string{buffer}})
			buffer = ""
			continue
		}

		if len(buffer) == 0 {
			buffer = channels[i]
		} else {
			buffer += "," + channels[i]
		}

		if i == len(channels)-1 {
			cmd.c.Send(&Event{Command: LIST, Params: []string{buffer}})
			return nil
		}
	}

	return nil
}

// Whowas sends a WHOWAS query to the server. amount is the amount of results
// you want back.
func (cmd *Commands) Whowas(nick string, amount int) error {
	if !IsValidNick(nick) {
		return &ErrInvalidTarget{Target: nick}
	}

	cmd.c.Send(&Event{Command: WHOWAS, Params: []string{nick, string(amount)}})
	return nil
}
<commit_msg>fix Commands.Part requiring an unused message param<commit_after>// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
// of this source code is governed by the MIT license that can be found in
// the LICENSE file.

package girc

import (
	"errors"
	"fmt"
)

// Commands holds a large list of useful methods to interact with the server,
// and wrappers for common events.
type Commands struct {
	c *Client
}

// Nick changes the client nickname.
func (cmd *Commands) Nick(name string) error {
	if !IsValidNick(name) {
		return &ErrInvalidTarget{Target: name}
	}

	cmd.c.Send(&Event{Command: NICK, Params: []string{name}})
	return nil
}

// Join attempts to enter a list of IRC channels, at bulk if possible to
// prevent sending extensive JOIN commands.
func (cmd *Commands) Join(channels ...string) error {
	// We can join multiple channels at once, however we need to ensure that
	// we are not exceeding the line length. (see maxLength)
	max := maxLength - len(JOIN) - 1

	var buffer string

	for i := 0; i < len(channels); i++ {
		if !IsValidChannel(channels[i]) {
			return &ErrInvalidTarget{Target: channels[i]}
		}

		if len(buffer+","+channels[i]) > max {
			cmd.c.Send(&Event{Command: JOIN, Params: []string{buffer}})
			buffer = ""
			continue
		}

		if len(buffer) == 0 {
			buffer = channels[i]
		} else {
			buffer += "," + channels[i]
		}

		if i == len(channels)-1 {
			cmd.c.Send(&Event{Command: JOIN, Params: []string{buffer}})
			return nil
		}
	}

	return nil
}

// JoinKey attempts to enter an IRC channel with a password.
func (cmd *Commands) JoinKey(channel, password string) error {
	if !IsValidChannel(channel) {
		return &ErrInvalidTarget{Target: channel}
	}

	cmd.c.Send(&Event{Command: JOIN, Params: []string{channel, password}})
	return nil
}

// Part leaves an IRC channel.
func (cmd *Commands) Part(channel string) error {
	if !IsValidChannel(channel) {
		return &ErrInvalidTarget{Target: channel}
	}

	cmd.c.Send(&Event{Command: PART, Params: []string{channel}})
	return nil
}

// PartMessage leaves an IRC channel with a specified leave message.
func (cmd *Commands) PartMessage(channel, message string) error {
	if !IsValidChannel(channel) {
		return &ErrInvalidTarget{Target: channel}
	}

	cmd.c.Send(&Event{Command: PART, Params: []string{channel}, Trailing: message})
	return nil
}

// SendCTCP sends a CTCP request to target. Note that this method uses
// PRIVMSG specifically.
func (cmd *Commands) SendCTCP(target, ctcpType, message string) error {
	out := encodeCTCPRaw(ctcpType, message)
	if out == "" {
		return errors.New("invalid CTCP")
	}

	return cmd.Message(target, out)
}

// SendCTCPf sends a CTCP request to target using a specific format. Note that
// this method uses PRIVMSG specifically.
func (cmd *Commands) SendCTCPf(target, ctcpType, format string, a ...interface{}) error {
	return cmd.SendCTCP(target, ctcpType, fmt.Sprintf(format, a...))
}

// SendCTCPReplyf sends a CTCP response to target using a specific format.
// Note that this method uses NOTICE specifically.
func (cmd *Commands) SendCTCPReplyf(target, ctcpType, format string, a ...interface{}) error {
	return cmd.SendCTCPReply(target, ctcpType, fmt.Sprintf(format, a...))
}

// SendCTCPReply sends a CTCP response to target. Note that this method uses
// NOTICE specifically.
func (cmd *Commands) SendCTCPReply(target, ctcpType, message string) error {
	out := encodeCTCPRaw(ctcpType, message)
	if out == "" {
		return errors.New("invalid CTCP")
	}

	return cmd.Notice(target, out)
}

// Message sends a PRIVMSG to target (either channel, service, or user).
func (cmd *Commands) Message(target, message string) error {
	if !IsValidNick(target) && !IsValidChannel(target) {
		return &ErrInvalidTarget{Target: target}
	}

	cmd.c.Send(&Event{Command: PRIVMSG, Params: []string{target}, Trailing: message})
	return nil
}

// Messagef sends a formated PRIVMSG to target (either channel, service, or
// user).
func (cmd *Commands) Messagef(target, format string, a ...interface{}) error {
	return cmd.Message(target, fmt.Sprintf(format, a...))
}

// ErrInvalidSource is returned when a method needs to know the origin of an
// event, however Event.Source is unknown (e.g. sent by the user, not the
// server.)
var ErrInvalidSource = errors.New("event has nil or invalid source address")

// Reply sends a reply to channel or user, based on where the supplied event
// originated from. See also ReplyTo().
func (cmd *Commands) Reply(event Event, message string) error {
	if event.Source == nil {
		return ErrInvalidSource
	}

	if len(event.Params) > 0 && IsValidChannel(event.Params[0]) {
		return cmd.Message(event.Params[0], message)
	}

	return cmd.Message(event.Source.Name, message)
}

// Replyf sends a reply to channel or user with a format string, based on
// where the supplied event originated from. See also ReplyTof().
func (cmd *Commands) Replyf(event Event, format string, a ...interface{}) error {
	return cmd.Reply(event, fmt.Sprintf(format, a...))
}

// ReplyTo sends a reply to a channel or user, based on where the supplied
// event originated from. ReplyTo(), when originating from a channel will
// default to replying with "<user>, <message>". See also Reply().
func (cmd *Commands) ReplyTo(event Event, message string) error {
	if event.Source == nil {
		return ErrInvalidSource
	}

	if len(event.Params) > 0 && IsValidChannel(event.Params[0]) {
		return cmd.Message(event.Params[0], event.Source.Name+", "+message)
	}

	return cmd.Message(event.Source.Name, message)
}

// ReplyTof sends a reply to a channel or user with a format string, based
// on where the supplied event originated from. ReplyTo(), when originating
// from a channel will default to replying with "<user>, <message>". See
// also Replyf().
func (cmd *Commands) ReplyTof(event Event, format string, a ...interface{}) error {
	return cmd.ReplyTo(event, fmt.Sprintf(format, a...))
}

// Action sends a PRIVMSG ACTION (/me) to target (either channel, service,
// or user).
func (cmd *Commands) Action(target, message string) error {
	if !IsValidNick(target) && !IsValidChannel(target) {
		return &ErrInvalidTarget{Target: target}
	}

	cmd.c.Send(&Event{
		Command:  PRIVMSG,
		Params:   []string{target},
		Trailing: fmt.Sprintf("\001ACTION %s\001", message),
	})
	return nil
}

// Actionf sends a formated PRIVMSG ACTION (/me) to target (either channel,
// service, or user).
func (cmd *Commands) Actionf(target, format string, a ...interface{}) error {
	return cmd.Action(target, fmt.Sprintf(format, a...))
}

// Notice sends a NOTICE to target (either channel, service, or user).
func (cmd *Commands) Notice(target, message string) error {
	if !IsValidNick(target) && !IsValidChannel(target) {
		return &ErrInvalidTarget{Target: target}
	}

	cmd.c.Send(&Event{Command: NOTICE, Params: []string{target}, Trailing: message})
	return nil
}

// Noticef sends a formated NOTICE to target (either channel, service, or
// user).
func (cmd *Commands) Noticef(target, format string, a ...interface{}) error {
	return cmd.Notice(target, fmt.Sprintf(format, a...))
}

// SendRaw sends a raw string back to the server, without carriage returns
// or newlines.
func (cmd *Commands) SendRaw(raw string) error {
	e := ParseEvent(raw)
	if e == nil {
		return errors.New("invalid event: " + raw)
	}

	cmd.c.Send(e)
	return nil
}

// SendRawf sends a formated string back to the server, without carriage
// returns or newlines.
func (cmd *Commands) SendRawf(format string, a ...interface{}) error {
	return cmd.SendRaw(fmt.Sprintf(format, a...))
}

// Topic sets the topic of channel to message. Does not verify the length
// of the topic.
func (cmd *Commands) Topic(channel, message string) {
	cmd.c.Send(&Event{Command: TOPIC, Params: []string{channel}, Trailing: message})
}

// Who sends a WHO query to the server, which will attempt WHOX by default.
// See http://faerion.sourceforge.net/doc/irc/whox.var for more details. This
// sends "%tcuhnr,2" per default. Do not use "1" as this will conflict with
// girc's builtin tracking functionality.
func (cmd *Commands) Who(target string) error {
	if !IsValidNick(target) && !IsValidChannel(target) && !IsValidUser(target) {
		return &ErrInvalidTarget{Target: target}
	}

	cmd.c.Send(&Event{Command: WHO, Params: []string{target, "%tcuhnr,2"}})
	return nil
}

// Whois sends a WHOIS query to the server, targeted at a specific user.
// as WHOIS is a bit slower, you may want to use WHO for brief user info.
func (cmd *Commands) Whois(nick string) error {
	if !IsValidNick(nick) {
		return &ErrInvalidTarget{Target: nick}
	}

	cmd.c.Send(&Event{Command: WHOIS, Params: []string{nick}})
	return nil
}

// Ping sends a PING query to the server, with a specific identifier that
// the server should respond with.
func (cmd *Commands) Ping(id string) {
	cmd.c.write(&Event{Command: PING, Params: []string{id}})
}

// Pong sends a PONG query to the server, with an identifier which was
// received from a previous PING query received by the client.
func (cmd *Commands) Pong(id string) {
	cmd.c.write(&Event{Command: PONG, Params: []string{id}})
}

// Oper sends a OPER authentication query to the server, with a username
// and password.
func (cmd *Commands) Oper(user, pass string) {
	cmd.c.Send(&Event{Command: OPER, Params: []string{user, pass}, Sensitive: true})
}

// Kick sends a KICK query to the server, attempting to kick nick from
// channel, with reason. If reason is blank, one will not be sent to the
// server.
func (cmd *Commands) Kick(channel, nick, reason string) error {
	if !IsValidChannel(channel) {
		return &ErrInvalidTarget{Target: channel}
	}

	if !IsValidNick(nick) {
		return &ErrInvalidTarget{Target: nick}
	}

	if reason != "" {
		cmd.c.Send(&Event{Command: KICK, Params: []string{channel, nick}, Trailing: reason})
		return nil
	}

	cmd.c.Send(&Event{Command: KICK, Params: []string{channel, nick}})
	return nil
}

// Invite sends a INVITE query to the server, to invite nick to channel.
func (cmd *Commands) Invite(channel, nick string) error {
	if !IsValidChannel(channel) {
		return &ErrInvalidTarget{Target: channel}
	}

	if !IsValidNick(nick) {
		return &ErrInvalidTarget{Target: nick}
	}

	cmd.c.Send(&Event{Command: INVITE, Params: []string{nick, channel}})
	return nil
}

// Away sends a AWAY query to the server, suggesting that the client is no
// longer active. If reason is blank, Client.Back() is called. Also see
// Client.Back().
func (cmd *Commands) Away(reason string) {
	if reason == "" {
		cmd.Back()
		return
	}

	cmd.c.Send(&Event{Command: AWAY, Params: []string{reason}})
}

// Back sends a AWAY query to the server, however the query is blank,
// suggesting that the client is active once again. Also see Client.Away().
func (cmd *Commands) Back() {
	cmd.c.Send(&Event{Command: AWAY})
}

// List sends a LIST query to the server, which will list channels and topics.
// Supports multiple channels at once, in hopes it will reduce extensive
// LIST queries to the server. Supply no channels to run a list against the
// entire server (warning, that may mean LOTS of channels!)
func (cmd *Commands) List(channels ...string) error {
	if len(channels) == 0 {
		cmd.c.Send(&Event{Command: LIST})
		return nil
	}

	// We can LIST multiple channels at once, however we need to ensure that
	// we are not exceeding the line length. (see maxLength)
	max := maxLength - len(JOIN) - 1

	var buffer string

	for i := 0; i < len(channels); i++ {
		if !IsValidChannel(channels[i]) {
			return &ErrInvalidTarget{Target: channels[i]}
		}

		if len(buffer+","+channels[i]) > max {
			cmd.c.Send(&Event{Command: LIST, Params: []string{buffer}})
			buffer = ""
			continue
		}

		if len(buffer) == 0 {
			buffer = channels[i]
		} else {
			buffer += "," + channels[i]
		}

		if i == len(channels)-1 {
			cmd.c.Send(&Event{Command: LIST, Params: []string{buffer}})
			return nil
		}
	}

	return nil
}

// Whowas sends a WHOWAS query to the server. amount is the amount of results
// you want back.
func (cmd *Commands) Whowas(nick string, amount int) error {
	if !IsValidNick(nick) {
		return &ErrInvalidTarget{Target: nick}
	}

	cmd.c.Send(&Event{Command: WHOWAS, Params: []string{nick, string(amount)}})
	return nil
}
<|endoftext|>


<commit_before>package server

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

	"github.com/coreos/etcd/clientv3"
	"github.com/golang/protobuf/proto"
	. "github.com/pingcap/check"
	"github.com/pingcap/kvproto/pkg/pdpb"
	"github.com/pingcap/pd/util"
)

var _ = Suite(&testTsoSuite{})

type testTsoSuite struct {
	client *clientv3.Client
	svr    *Server
}

func (s *testTsoSuite) getRootPath() string {
	return "test_tso"
}

func (s *testTsoSuite) SetUpSuite(c *C) {
	s.svr = newTestServer(c, s.getRootPath())

	s.client = newEtcdClient(c)

	deleteRoot(c, s.client, s.getRootPath())

	go s.svr.Run()
}

func (s *testTsoSuite) TearDownSuite(c *C) {
	s.svr.Close()
	s.client.Close()
}

func sendRequest(c *C, conn net.Conn, msgID uint64, request *pdpb.Request) {
	err := util.WriteMessage(conn, msgID, request)
	c.Assert(err, IsNil)
}

func recvResponse(c *C, conn net.Conn) (uint64, *pdpb.Response) {
	resp := &pdpb.Response{}
	msgID, err := util.ReadMessage(conn, resp)
	c.Assert(err, IsNil)
	return msgID, resp
}

func (s *testTsoSuite) testGetTimestamp(c *C, conn net.Conn, n int) {
	tso := &pdpb.TsoRequest{
		Number: proto.Uint32(uint32(n)),
	}

	req := &pdpb.Request{
		CmdType: pdpb.CommandType_Tso.Enum(),
		Tso:     tso,
	}

	msgID := uint64(rand.Int63())
	sendRequest(c, conn, msgID, req)
	msgID, resp := recvResponse(c, conn)
	c.Assert(msgID, Equals, msgID)
	c.Assert(resp.Tso, NotNil)
	c.Assert(len(resp.Tso.Timestamps), Equals, n)

	res := resp.Tso.Timestamps
	last := pdpb.Timestamp{}
	for i := 0; i < n; i++ {
		c.Assert(res[i].GetPhysical(), GreaterEqual, last.GetPhysical())
		if res[i].GetPhysical() == last.GetPhysical() {
			c.Assert(res[i].GetLogical(), Greater, last.GetLogical())
		}

		last = *res[i]
	}
}

func mustGetLeader(c *C, client *clientv3.Client, rootPath string) *pdpb.Leader {
	for i := 0; i < 10; i++ {
		leader, err := GetLeader(client, GetLeaderPath(rootPath))
		c.Assert(err, IsNil)
		if leader != nil {
			return leader
		}
		time.Sleep(500 * time.Millisecond)
	}

	c.Fatal("get leader error")
	return nil
}

func (s *testTsoSuite) TestTso(c *C) {
	leader := mustGetLeader(c, s.client, s.getRootPath())

	var wg sync.WaitGroup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()

			conn, err := net.Dial("tcp", leader.GetAddr())
			c.Assert(err, IsNil)
			defer conn.Close()

			s.testGetTimestamp(c, conn, 10)
		}()
	}

	wg.Wait()
}
<commit_msg>server: fix get timestamp test check.<commit_after>package server

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

	"github.com/coreos/etcd/clientv3"
	"github.com/golang/protobuf/proto"
	. "github.com/pingcap/check"
	"github.com/pingcap/kvproto/pkg/pdpb"
	"github.com/pingcap/pd/util"
)

var _ = Suite(&testTsoSuite{})

type testTsoSuite struct {
	client *clientv3.Client
	svr    *Server
}

func (s *testTsoSuite) getRootPath() string {
	return "test_tso"
}

func (s *testTsoSuite) SetUpSuite(c *C) {
	s.svr = newTestServer(c, s.getRootPath())

	s.client = newEtcdClient(c)

	deleteRoot(c, s.client, s.getRootPath())

	go s.svr.Run()
}

func (s *testTsoSuite) TearDownSuite(c *C) {
	s.svr.Close()
	s.client.Close()
}

func sendRequest(c *C, conn net.Conn, msgID uint64, request *pdpb.Request) {
	err := util.WriteMessage(conn, msgID, request)
	c.Assert(err, IsNil)
}

func recvResponse(c *C, conn net.Conn) (uint64, *pdpb.Response) {
	resp := &pdpb.Response{}
	msgID, err := util.ReadMessage(conn, resp)
	c.Assert(err, IsNil)
	return msgID, resp
}

func (s *testTsoSuite) testGetTimestamp(c *C, conn net.Conn, n int) {
	tso := &pdpb.TsoRequest{
		Number: proto.Uint32(uint32(n)),
	}

	req := &pdpb.Request{
		CmdType: pdpb.CommandType_Tso.Enum(),
		Tso:     tso,
	}

	rawMsgID := uint64(rand.Int63())
	sendRequest(c, conn, rawMsgID, req)
	msgID, resp := recvResponse(c, conn)
	c.Assert(rawMsgID, Equals, msgID)
	c.Assert(resp.Tso, NotNil)
	c.Assert(resp.Tso.Timestamps, HasLen, n)

	res := resp.Tso.Timestamps
	last := pdpb.Timestamp{}
	for i := 0; i < n; i++ {
		c.Assert(res[i].GetPhysical(), GreaterEqual, last.GetPhysical())
		if res[i].GetPhysical() == last.GetPhysical() {
			c.Assert(res[i].GetLogical(), Greater, last.GetLogical())
		}

		last = *res[i]
	}
}

func mustGetLeader(c *C, client *clientv3.Client, rootPath string) *pdpb.Leader {
	for i := 0; i < 10; i++ {
		leader, err := GetLeader(client, GetLeaderPath(rootPath))
		c.Assert(err, IsNil)
		if leader != nil {
			return leader
		}
		time.Sleep(500 * time.Millisecond)
	}

	c.Fatal("get leader error")
	return nil
}

func (s *testTsoSuite) TestTso(c *C) {
	leader := mustGetLeader(c, s.client, s.getRootPath())

	var wg sync.WaitGroup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()

			conn, err := net.Dial("tcp", leader.GetAddr())
			c.Assert(err, IsNil)
			defer conn.Close()

			s.testGetTimestamp(c, conn, 10)
		}()
	}

	wg.Wait()
}
<|endoftext|>


<commit_before>package notifier

import (
	"bytes"
	"fmt"
	"os"
	"sort"
	"strings"
	"text/template"
	"time"

	"github.com/jinzhu/gorm"
	"github.com/oinume/lekcije/server/config"
	"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/sendgrid/sendgrid-go"
	"github.com/sendgrid/sendgrid-go/helpers/mail"
	"github.com/uber-go/zap"
)

var lessonFetcher *fetcher.TeacherLessonFetcher

func init() {
	lessonFetcher = fetcher.NewTeacherLessonFetcher(nil, logger.AppLogger)
}

type Notifier struct {
	db            *gorm.DB
	dryRun        bool
	lessonService *model.LessonService
	teachers      map[uint32]*model.Teacher
}

func NewNotifier(db *gorm.DB, dryRun bool) *Notifier {
	return &Notifier{
		db:       db,
		dryRun:   dryRun,
		teachers: make(map[uint32]*model.Teacher, 1000),
	}
}

func (n *Notifier) SendNotification(user *model.User) error {
	followingTeacherService := model.NewFollowingTeacherService(n.db)
	n.lessonService = model.NewLessonService(n.db)

	teacherIDs, err := followingTeacherService.FindTeacherIDsByUserID(user.ID)
	if err != nil {
		return errors.Wrapperf(err, "Failed to FindTeacherIDsByUserID(): userID=%v", user.ID)
	}

	availableLessonsPerTeacher := make(map[uint32][]*model.Lesson, 1000)
	allFetchedLessons := make([]*model.Lesson, 0, 5000)
	for _, teacherID := range teacherIDs {
		teacher, fetchedLessons, newAvailableLessons, err := n.fetchAndExtractNewAvailableLessons(teacherID)
		if err != nil {
			switch err.(type) {
			case *errors.NotFound:
				// TODO: update teacher table flag
				logger.AppLogger.Warn("Cannot fetch teacher", zap.Uint("teacherID", uint(teacherID)))
				continue
			default:
				return err
			}
		}

		allFetchedLessons = append(allFetchedLessons, fetchedLessons...)
		n.teachers[teacherID] = teacher
		if len(newAvailableLessons) > 0 {
			availableLessonsPerTeacher[teacherID] = newAvailableLessons
		}
	}

	if err := n.sendNotificationToUser(user, availableLessonsPerTeacher); err != nil {
		return err
	}

	if !n.dryRun {
		n.lessonService.UpdateLessons(allFetchedLessons)
	}

	time.Sleep(500 * time.Millisecond)

	return nil
}

// Returns teacher, fetchedLessons, newAvailableLessons, error
func (n *Notifier) fetchAndExtractNewAvailableLessons(teacherID uint32) (
	*model.Teacher, []*model.Lesson, []*model.Lesson, error,
) {
	teacher, fetchedLessons, err := lessonFetcher.Fetch(teacherID)
	if err != nil {
		logger.AppLogger.Error(
			"TeacherLessonFetcher.Fetch",
			zap.Uint("teacherID", uint(teacherID)), zap.Error(err),
		)
		return nil, nil, nil, err
	}
	logger.AppLogger.Info(
		"TeacherLessonFetcher.Fetch",
		zap.Uint("teacherID", uint(teacher.ID)),
		zap.String("teacherName", teacher.Name),
		zap.Int("fetchedLessons", 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()
	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, 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 teacher, fetchedLessons, newAvailableLessons, nil
}

func (n *Notifier) sendNotificationToUser(
	user *model.User,
	lessonsPerTeacher map[uint32][]*model.Lesson,
) error {
	lessonsCount := 0
	var teacherIDs []int
	for teacherID, lessons := range lessonsPerTeacher {
		teacherIDs = append(teacherIDs, int(teacherID))
		lessonsCount += len(lessons)
	}
	if lessonsCount == 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)
	}

	t := template.New("email")
	t = template.Must(t.Parse(getEmailTemplateJP()))
	type TemplateData struct {
		TeacherIDs        []uint32
		Teachers          map[uint32]*model.Teacher
		LessonsPerTeacher map[uint32][]*model.Lesson
		WebURL            string
	}
	data := &TemplateData{
		TeacherIDs:        teacherIDs2,
		Teachers:          n.teachers,
		LessonsPerTeacher: lessonsPerTeacher,
		WebURL:            config.WebURL(),
	}

	var body bytes.Buffer
	if err := t.Execute(&body, data); err != nil {
		return errors.InternalWrapf(err, "Failed to execute template.")
	}
	//fmt.Printf("--- mail ---\n%s", body.String())

	//subject := "Schedule of teacher " + strings.Join(teacherNames, ", ")
	subject := strings.Join(teacherNames, ", ") + "の空きレッスンがあります"
	sender := &EmailNotificationSender{}
	return sender.Send(user, subject, body.String())
}

func getEmailTemplateJP() string {
	return strings.TrimSpace(`
{{- range $teacherID := .TeacherIDs }}
{{- $teacher := index $.Teachers $teacherID -}}
--- {{ $teacher.Name }} ---
  {{- $lessons := index $.LessonsPerTeacher $teacherID }}
  {{- range $lesson := $lessons }}
{{ $lesson.Datetime.Format "2006-01-02 15:04" }}
  {{- end }}

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

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

func getEmailTemplateEN() string {
	return strings.TrimSpace(`
{{- range $teacherID := .TeacherIDs }}
{{- $teacher := index $.Teachers $teacherID -}}
--- {{ $teacher.Name }} ---
  {{- $lessons := index $.LessonsPerTeacher $teacherID }}
  {{- range $lesson := $lessons }}
{{ $lesson.Datetime.Format "2006-01-02 15:04" }}
  {{- end }}

Reserve here:
PC
Mobile
{{ end }}
Click here if you want to stop notification of the teacher.
	`)
}

type NotificationSender interface {
	Send(user *model.User, subject, body string) error
}

type EmailNotificationSender struct{}

func (s *EmailNotificationSender) Send(user *model.User, subject, body string) error {
	from := mail.NewEmail("lekcije", "lekcije@lekcije.com")
	to := mail.NewEmail(user.Name, user.Email.Raw())
	content := mail.NewContent("text/html", strings.Replace(body, "\n", "
", -1))
	m := mail.NewV3MailInit(from, subject, to, content)

	req := sendgrid.GetRequest(
		os.Getenv("SENDGRID_API_KEY"),
		"/v3/mail/send",
		"https://api.sendgrid.com",
	)
	req.Method = "POST"
	req.Body = mail.GetRequestBody(m)
	resp, err := sendgrid.API(req)
	if err != nil {
		return errors.InternalWrapf(err, "Failed to send email by sendgrid")
	}
	if resp.StatusCode >= 300 {
		message := fmt.Sprintf(
			"Failed to send email by sendgrid: statusCode=%v, body=%v",
			resp.StatusCode, strings.Replace(resp.Body, "\n", "\\n", -1),
		)
		logger.AppLogger.Error(message)
		return errors.InternalWrapf(
			err,
			"Failed to send email by sendgrid: statusCode=%v",
			resp.StatusCode,
		)
	}

	return nil
}
<commit_msg>Add TODO comment<commit_after>package notifier

import (
	"bytes"
	"fmt"
	"os"
	"sort"
	"strings"
	"text/template"
	"time"

	"github.com/jinzhu/gorm"
	"github.com/oinume/lekcije/server/config"
	"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/sendgrid/sendgrid-go"
	"github.com/sendgrid/sendgrid-go/helpers/mail"
	"github.com/uber-go/zap"
)

var lessonFetcher *fetcher.TeacherLessonFetcher

func init() {
	lessonFetcher = fetcher.NewTeacherLessonFetcher(nil, logger.AppLogger)
}

type Notifier struct {
	db            *gorm.DB
	dryRun        bool
	lessonService *model.LessonService
	teachers      map[uint32]*model.Teacher
}

func NewNotifier(db *gorm.DB, dryRun bool) *Notifier {
	return &Notifier{
		db:       db,
		dryRun:   dryRun,
		teachers: make(map[uint32]*model.Teacher, 1000),
	}
}

func (n *Notifier) SendNotification(user *model.User) error {
	followingTeacherService := model.NewFollowingTeacherService(n.db)
	n.lessonService = model.NewLessonService(n.db)

	teacherIDs, err := followingTeacherService.FindTeacherIDsByUserID(user.ID)
	if err != nil {
		return errors.Wrapperf(err, "Failed to FindTeacherIDsByUserID(): userID=%v", user.ID)
	}

	availableLessonsPerTeacher := make(map[uint32][]*model.Lesson, 1000)
	allFetchedLessons := make([]*model.Lesson, 0, 5000)
	for _, teacherID := range teacherIDs {
		teacher, fetchedLessons, newAvailableLessons, err := n.fetchAndExtractNewAvailableLessons(teacherID)
		if err != nil {
			switch err.(type) {
			case *errors.NotFound:
				// TODO: update teacher table flag
				// TODO: Not need to log
				logger.AppLogger.Warn("Cannot fetch teacher", zap.Uint("teacherID", uint(teacherID)))
				continue
			default:
				return err
			}
		}

		allFetchedLessons = append(allFetchedLessons, fetchedLessons...)
		n.teachers[teacherID] = teacher
		if len(newAvailableLessons) > 0 {
			availableLessonsPerTeacher[teacherID] = newAvailableLessons
		}
	}

	if err := n.sendNotificationToUser(user, availableLessonsPerTeacher); err != nil {
		return err
	}

	if !n.dryRun {
		n.lessonService.UpdateLessons(allFetchedLessons)
	}

	time.Sleep(500 * time.Millisecond)

	return nil
}

// Returns teacher, fetchedLessons, newAvailableLessons, error
func (n *Notifier) fetchAndExtractNewAvailableLessons(teacherID uint32) (
	*model.Teacher, []*model.Lesson, []*model.Lesson, error,
) {
	teacher, fetchedLessons, err := lessonFetcher.Fetch(teacherID)
	if err != nil {
		logger.AppLogger.Error(
			"TeacherLessonFetcher.Fetch",
			zap.Uint("teacherID", uint(teacherID)), zap.Error(err),
		)
		return nil, nil, nil, err
	}
	logger.AppLogger.Info(
		"TeacherLessonFetcher.Fetch",
		zap.Uint("teacherID", uint(teacher.ID)),
		zap.String("teacherName", teacher.Name),
		zap.Int("fetchedLessons", 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()
	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, 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 teacher, fetchedLessons, newAvailableLessons, nil
}

func (n *Notifier) sendNotificationToUser(
	user *model.User,
	lessonsPerTeacher map[uint32][]*model.Lesson,
) error {
	lessonsCount := 0
	var teacherIDs []int
	for teacherID, lessons := range lessonsPerTeacher {
		teacherIDs = append(teacherIDs, int(teacherID))
		lessonsCount += len(lessons)
	}
	if lessonsCount == 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)
	}

	t := template.New("email")
	t = template.Must(t.Parse(getEmailTemplateJP()))
	type TemplateData struct {
		TeacherIDs        []uint32
		Teachers          map[uint32]*model.Teacher
		LessonsPerTeacher map[uint32][]*model.Lesson
		WebURL            string
	}
	data := &TemplateData{
		TeacherIDs:        teacherIDs2,
		Teachers:          n.teachers,
		LessonsPerTeacher: lessonsPerTeacher,
		WebURL:            config.WebURL(),
	}

	var body bytes.Buffer
	if err := t.Execute(&body, data); err != nil {
		return errors.InternalWrapf(err, "Failed to execute template.")
	}
	//fmt.Printf("--- mail ---\n%s", body.String())

	//subject := "Schedule of teacher " + strings.Join(teacherNames, ", ")
	subject := strings.Join(teacherNames, ", ") + "の空きレッスンがあります"
	sender := &EmailNotificationSender{}
	return sender.Send(user, subject, body.String())
}

func getEmailTemplateJP() string {
	return strings.TrimSpace(`
{{- range $teacherID := .TeacherIDs }}
{{- $teacher := index $.Teachers $teacherID -}}
--- {{ $teacher.Name }} ---
  {{- $lessons := index $.LessonsPerTeacher $teacherID }}
  {{- range $lesson := $lessons }}
{{ $lesson.Datetime.Format "2006-01-02 15:04" }}
  {{- end }}

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

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

func getEmailTemplateEN() string {
	return strings.TrimSpace(`
{{- range $teacherID := .TeacherIDs }}
{{- $teacher := index $.Teachers $teacherID -}}
--- {{ $teacher.Name }} ---
  {{- $lessons := index $.LessonsPerTeacher $teacherID }}
  {{- range $lesson := $lessons }}
{{ $lesson.Datetime.Format "2006-01-02 15:04" }}
  {{- end }}

Reserve here:
PC
Mobile
{{ end }}
Click here if you want to stop notification of the teacher.
	`)
}

type NotificationSender interface {
	Send(user *model.User, subject, body string) error
}

type EmailNotificationSender struct{}

func (s *EmailNotificationSender) Send(user *model.User, subject, body string) error {
	from := mail.NewEmail("lekcije", "lekcije@lekcije.com")
	to := mail.NewEmail(user.Name, user.Email.Raw())
	content := mail.NewContent("text/html", strings.Replace(body, "\n", "
", -1))
	m := mail.NewV3MailInit(from, subject, to, content)

	req := sendgrid.GetRequest(
		os.Getenv("SENDGRID_API_KEY"),
		"/v3/mail/send",
		"https://api.sendgrid.com",
	)
	req.Method = "POST"
	req.Body = mail.GetRequestBody(m)
	resp, err := sendgrid.API(req)
	if err != nil {
		return errors.InternalWrapf(err, "Failed to send email by sendgrid")
	}
	if resp.StatusCode >= 300 {
		message := fmt.Sprintf(
			"Failed to send email by sendgrid: statusCode=%v, body=%v",
			resp.StatusCode, strings.Replace(resp.Body, "\n", "\\n", -1),
		)
		logger.AppLogger.Error(message)
		return errors.InternalWrapf(
			err,
			"Failed to send email by sendgrid: statusCode=%v",
			resp.StatusCode,
		)
	}

	return nil
}
<|endoftext|>