<commit_before>package bot

import (
	"fmt"
	"log"
	"os"
	"os/exec"
	"reflect"
	"regexp"
	"sync"

	"github.com/uva-its/yaml"
)

// PluginNames can be letters, numbers & underscores only, mainly so
// brain functions can use ':' as a separator.
const pNameRegex = `[\w]+`

var pNameRe = regexp.MustCompile(pNameRegex)
var plugins []Plugin
var plugIDmap map[string]int

// PluginHelp specifies keywords and help text for the 'bot help system
type PluginHelp struct {
	Keywords []string // match words for 'help XXX'
	Helptext []string // help string to give for the keywords, conventionally starting with (bot) for commands or (hear) when the bot needn't be addressed directly
}

// InputMatchers specify the command or message to match and what to pass to the plugin
type InputMatcher struct {
	Regex   string         // The regular expression string to match - bot adds ^\w* & \w*$
	Command string         // The name of the command to pass to the plugin with it's arguments
	re      *regexp.Regexp // The compiled regular expression. If the regex doesn't compile, the 'bot will log an error
}

type plugType int

const (
	plugGo plugType = iota
	plugExternal
	plugBuiltin
)

// Plugin specifies the structure of a plugin configuration - plugins should include an example / default config
type Plugin struct {
	name           string         // the name of the plugin, used as a key in to the
	pluginType     plugType       // plugGo, plugExternal, plugBuiltin - determines how commands are routed
	pluginPath     string         // Path to the external executable that expects <channel> <user> <command> <arg> <arg> from regex matches - for Plugtype=plugExternal only
	Disabled       bool           // Set true to disable the plugin
	DisallowDirect bool           // Set this true if this plugin can never be accessed via direct message
	DirectOnly     bool           // Set this true if this plugin ONLY accepts direct messages
	Channels       []string       // Channels where the plugin is active - rifraf like "memes" should probably only be in random, but it's configurable. If empty uses DefaultChannels
	AllChannels    bool           // If the Channels list is empty and AllChannels is true, the plugin should be active in all the channels the bot is in
	RequireAdmin   bool           // Set to only allow administrators to access a plugin
	Users          []string       // If non-empty, list of all the users with access to this plugin
	Help           []PluginHelp   // All the keyword sets / help texts for this plugin
	CommandMatches []InputMatcher // Input matchers for messages that need to be directed to the 'bot
	ReplyMatchers  []InputMatcher // Input matchers for replies to questions, only match after a RequestContinuation
	MessageMatches []InputMatcher // Input matchers for messages the 'bot hears even when it's not being spoken to
	CatchAll       bool           // Whenever the robot is spoken to, but no plugin matches, plugins with CatchAll=true get called with command="catchall" and argument=<full text of message to robot>
	Config         yaml.MapSlice  // Arbitrary Plugin configuration, will be stored and provided in a thread-safe manner via GetPluginConfig()
	config         interface{}    // A pointer to an empty struct that the bot can Unmarshal custom configuration into
	pluginID       string         // 32-char random ID for identifying plugins in callbacks
	lock           sync.Mutex     // For use with the robot's Brain
}

/* type PluginHandler is the struct a plugin registers for the Gopherbot plugin
API.
*/
type PluginHandler struct {
	DefaultConfig string /* A yaml-formatted multiline string defining the default Plugin configuration. It should be liberally commented for use in generating
	local/custom configuration for the plugin. If a Config: section is defined, it should match the structure of the optional Config interface{} */
	Handler func(bot *Robot, command string, args ...string) // The callback function called by the robot whenever a Command is matched
	Config  interface{}                                      // An optional empty struct defining custom configuration for the plugin
}

// pluginHandlers maps from plugin names to PluginV1 (later interface{} with a type selector, maybe)
var pluginHandlers map[string]PluginHandler = make(map[string]PluginHandler)

// stopRegistrations is set "true" when the bot is created to prevent registration outside of init functions
var stopRegistrations bool = false

// initialize sends the "init" command to every plugin
func initializePlugins() {
	b.lock.RLock()
	defer b.lock.RUnlock()
	bot := &Robot{
		User:    b.name,
		Channel: "",
		Format:  Variable,
	}
	for _, plugin := range plugins {
		Log(Info, "Initializing plugin:", plugin.name)
		go callPlugin(bot, plugin, "init")
	}
}

// RegisterPlugin allows plugins to register a PluginHandler in a func init().
// When the bot initializes, it will call each plugin's handler with a command
// "init", empty channel, the bot's username, and no arguments, so the plugin
// can store this information for, e.g., scheduled jobs.
func RegisterPlugin(name string, plug PluginHandler) {
	if stopRegistrations {
		return
	}
	if _, exists := pluginHandlers[name]; exists {
		log.Fatal("Attempted registration of duplicate plugin name:", name)
	}
	pluginHandlers[name] = plug
}

func getExtDefCfg(plugin Plugin) (*[]byte, error) {
	var fullPath string
	if byte(plugin.pluginPath[0]) == byte("/"[0]) {
		fullPath = plugin.pluginPath
	} else {
		_, err := os.Stat(b.localPath + "/" + plugin.pluginPath)
		if err != nil {
			_, err := os.Stat(b.installPath + "/" + plugin.pluginPath)
			if err != nil {
				err = fmt.Errorf("Couldn't locate external plugin %s: %v", plugin.name, err)
				return nil, err
			}
			fullPath = b.installPath + "/" + plugin.pluginPath
			Log(Debug, "Using stock external plugin:", fullPath)
		} else {
			fullPath = b.localPath + "/" + plugin.pluginPath
			Log(Debug, "Using local external plugin:", fullPath)
		}
	}
	// cmd := exec.Command(fullPath, channel, user, matcher.Command, matches[0][1:]...)
	cfg, err := exec.Command(fullPath, "", "", "", "configure").Output()
	if err != nil {
		if exitErr, ok := err.(*exec.ExitError); ok {
			err = fmt.Errorf("Problem retrieving default configuration for external plugin \"%s\", skipping: \"%v\", output: %s", fullPath, err, exitErr.Stderr)
		} else {
			err = fmt.Errorf("Problem retrieving default configuration for external plugin \"%s\", skipping: \"%v\"", fullPath, err)
		}
		return nil, err
	} else {
		return &cfg, nil
	}
}

// loadPluginConfig() loads the configuration for all the plugins from
// $GOPHER_LOCALDIR/plugins/<pluginname>.yaml, assigns a pluginID, and
// stores the resulting array in b.plugins. Bad plugins are skipped and logged.
// Plugin configuration is initially loaded into temporary data structures,
// then stored in the bot package under the global bot lock.
func loadPluginConfig() {
	i := 0

	// Copy some data from the bot under lock
	b.lock.RLock()
	// Get a list of all plugins from the package pluginHandlers var and
	// the list of external plugins
	nump := len(pluginHandlers) + len(b.externalPlugins)
	pnames := make([]string, nump)
	ptypes := make([]plugType, nump)
	eppaths := make(map[string]string) // Paths to external plugins
	pfinder := make(map[string]int)    // keep a map of pluginIDs to identify plugins during a callback
	pset := make(map[string]bool)      // track plugin names

	// builtins come first so indexes match, see loop below
	// Note this doesn't need to be under RLock, but it needs to precede
	// external plugins. This should be fast enough that it doesn't matter.
	for _, plugin := range builtIns {
		pnames[i] = plugin
		pset[plugin] = true
		ptypes[i] = plugBuiltin
		i++
	}

	for _, plug := range b.externalPlugins {
		if !pNameRe.MatchString(plug.Name) {
			Log(Error, "Plugin name \"%s\" doesn't match plugin name regex \"%s\", skipping")
			continue
		}
		pnames[i] = plug.Name
		if pset[plug.Name] {
			Log(Error, "External plugin name duplicates builtIn, skipping:", plug)
			continue
		}
		pset[plug.Name] = true
		ptypes[i] = plugExternal
		eppaths[plug.Name] = plug.Path
		i++
	}
	// copy the list of default channels
	pchan := make([]string, 0, len(b.plugChannels))
	pchan = append(pchan, b.plugChannels...)
	b.lock.RUnlock() // we're done with bot data 'til the end

PlugHandlerLoop:
	for plug, _ := range pluginHandlers {
		if !pNameRe.MatchString(plug) {
			Log(Error, "Plugin name \"%s\" doesn't match plugin name regex \"%s\", skipping")
			continue
		}
		if pset[plug] { // have to check builtIns, already loaded
			for _, plugin := range builtIns {
				if plug == plugin {
					continue PlugHandlerLoop // skip it, already loaded
				}
			}
			// Since external plugins can change on reload, just log an error if
			// we get a duplicate plugin name.
			Log(Error, "Plugin name duplicates external, skipping:", plug)
		} else {
			pnames[i] = plug
			ptypes[i] = plugGo
			i++
		}
	}
	plist := make([]Plugin, 0, nump)

	// Because some plugins may be disabled, pnames and plugins won't necessarily sync
	plugIndex := 0

PlugLoop:
	for i, plug := range pnames {
		var plugin Plugin
		Log(Trace, fmt.Sprintf("Loading plugin #%d - %s, type %d", plugIndex, plug, ptypes[i]))

		plugin.pluginType = ptypes[i]
		if plugin.pluginType == plugExternal {
			// External plugins spit their default config to stdout when called with command="configure"
			plugin.pluginPath = eppaths[plug]
			cfg, err := getExtDefCfg(plugin)
			if err != nil {
				Log(Error, err)
				continue
			}
			if err := yaml.Unmarshal(*cfg, &plugin); err != nil {
				Log(Error, fmt.Errorf("Problem unmarshalling plugin default config for \"%s\", skipping: %v", plug, err))
				continue
			}
		} else {
			if err := yaml.Unmarshal([]byte(pluginHandlers[plug].DefaultConfig), &plugin); err != nil {
				Log(Error, fmt.Errorf("Problem unmarshalling plugin default config for \"%s\", skipping: %v", plug, err))
				continue
			}
		}
		// getConfigFile overlays the default config with local config
		if err := getConfigFile("plugins/"+plug+".yaml", false, &plugin); err != nil {
			Log(Error, fmt.Errorf("Problem with local configuration for plugin \"%s\", skipping: %v", plug, err))
			continue
		}
		if plugin.Disabled {
			Log(Info, fmt.Sprintf("Plugin \"%s\" is disabled, skipping", plug))
			continue
		}
		Log(Info, "Loaded configuration for plugin", plug)
		// Use bot default plugin channels if none defined, unless AllChannels requested. Admin can override.
		if len(plugin.Channels) == 0 && len(pchan) > 0 && !plugin.AllChannels {
			plugin.Channels = pchan
		}
		Log(Info, fmt.Sprintf("Plugin \"%s\" will be active in channels %q; all channels: %t", plug, plugin.Channels, plugin.AllChannels))
		// Compile the regex's
		for i, _ := range plugin.CommandMatches {
			command := &plugin.CommandMatches[i]
			re, err := regexp.Compile(`^\s*` + command.Regex + `\s*$`)
			if err != nil {
				Log(Error, fmt.Errorf("Skipping %s, couldn't compile command regular expression \"%s\": %v", plug, command.Regex, err))
				continue PlugLoop
			}
			command.re = re
		}
		for i, _ := range plugin.ReplyMatchers {
			reply := &plugin.ReplyMatchers[i]
			re, err := regexp.Compile(`^\s*` + reply.Regex + `\s*$`)
			if err != nil {
				Log(Error, fmt.Errorf("Skipping %s, couldn't compile reply regular expression \"%s\": %v", plug, reply.Regex, err))
				continue PlugLoop
			}
			reply.re = re
		}
		for i, _ := range plugin.MessageMatches {
			// Note that full message regexes don't get the beginning and end anchors added - the individual plugin
			// will need to do this if necessary.
			message := &plugin.MessageMatches[i]
			re, err := regexp.Compile(message.Regex)
			if err != nil {
				Log(Error, fmt.Errorf("Skipping %s, couldn't compile message regular expression \"%s\": %v", plug, message.Regex, err))
				continue PlugLoop
			}
			message.re = re
		}
		plugin.name = plug
		// Copy the pointer to the empty config struct / empty struct (when no config)
		pt := reflect.ValueOf(pluginHandlers[plug].Config)
		if pt.Kind() == reflect.Ptr {
			if plugin.Config != nil {
				// reflect magic: create a pointer to a new empty config struct for the plugin
				plugin.config = reflect.New(reflect.Indirect(pt).Type()).Interface()
				cust, _ := yaml.Marshal(plugin.Config)
				if err := yaml.Unmarshal(cust, plugin.config); err != nil {
					Log(Error, fmt.Sprintf("Error unmarshalling plugin config json to config: %v", err))
				}
			} else {
				Log(Debug, "Plugin \"%s\" has custom config, but no local custom config provided", plug)
			}
		} else {
			Log(Debug, "config interface isn't a pointer, skipping unmarshal for plugin:", plug)
		}
		// Generate the random id
		p := make([]byte, 16)
		random.Read(p)
		plugin.pluginID = fmt.Sprintf("%x", p)
		pfinder[plugin.pluginID] = plugIndex
		// Store this plugin's config in the temporary list
		Log(Info, fmt.Sprintf("Recorded plugin #%d, \"%s\"", plugIndex, plugin.name))
		plist = append(plist, plugin)
		plugIndex++
	}

	reInitPlugins := false
	b.lock.Lock()
	plugins = plist
	plugIDmap = pfinder
	// loadPluginConfig is called in newBot, before the connector has started;
	// don't init plugins in that case.
	if b.Connector != nil {
		reInitPlugins = true
	}
	b.lock.Unlock()
	if reInitPlugins {
		initializePlugins()
	}
}
<commit_msg>Fix plugin loading bug<commit_after>package bot

import (
	"fmt"
	"log"
	"os"
	"os/exec"
	"reflect"
	"regexp"
	"sync"

	"github.com/uva-its/yaml"
)

// PluginNames can be letters, numbers & underscores only, mainly so
// brain functions can use ':' as a separator.
const pNameRegex = `[\w]+`

var pNameRe = regexp.MustCompile(pNameRegex)
var plugins []Plugin
var plugIDmap map[string]int

// PluginHelp specifies keywords and help text for the 'bot help system
type PluginHelp struct {
	Keywords []string // match words for 'help XXX'
	Helptext []string // help string to give for the keywords, conventionally starting with (bot) for commands or (hear) when the bot needn't be addressed directly
}

// InputMatchers specify the command or message to match and what to pass to the plugin
type InputMatcher struct {
	Regex   string         // The regular expression string to match - bot adds ^\w* & \w*$
	Command string         // The name of the command to pass to the plugin with it's arguments
	re      *regexp.Regexp // The compiled regular expression. If the regex doesn't compile, the 'bot will log an error
}

type plugType int

const (
	plugGo plugType = iota
	plugExternal
	plugBuiltin
)

// Plugin specifies the structure of a plugin configuration - plugins should include an example / default config
type Plugin struct {
	name           string         // the name of the plugin, used as a key in to the
	pluginType     plugType       // plugGo, plugExternal, plugBuiltin - determines how commands are routed
	pluginPath     string         // Path to the external executable that expects <channel> <user> <command> <arg> <arg> from regex matches - for Plugtype=plugExternal only
	Disabled       bool           // Set true to disable the plugin
	DisallowDirect bool           // Set this true if this plugin can never be accessed via direct message
	DirectOnly     bool           // Set this true if this plugin ONLY accepts direct messages
	Channels       []string       // Channels where the plugin is active - rifraf like "memes" should probably only be in random, but it's configurable. If empty uses DefaultChannels
	AllChannels    bool           // If the Channels list is empty and AllChannels is true, the plugin should be active in all the channels the bot is in
	RequireAdmin   bool           // Set to only allow administrators to access a plugin
	Users          []string       // If non-empty, list of all the users with access to this plugin
	Help           []PluginHelp   // All the keyword sets / help texts for this plugin
	CommandMatches []InputMatcher // Input matchers for messages that need to be directed to the 'bot
	ReplyMatchers  []InputMatcher // Input matchers for replies to questions, only match after a RequestContinuation
	MessageMatches []InputMatcher // Input matchers for messages the 'bot hears even when it's not being spoken to
	CatchAll       bool           // Whenever the robot is spoken to, but no plugin matches, plugins with CatchAll=true get called with command="catchall" and argument=<full text of message to robot>
	Config         yaml.MapSlice  // Arbitrary Plugin configuration, will be stored and provided in a thread-safe manner via GetPluginConfig()
	config         interface{}    // A pointer to an empty struct that the bot can Unmarshal custom configuration into
	pluginID       string         // 32-char random ID for identifying plugins in callbacks
	lock           sync.Mutex     // For use with the robot's Brain
}

/* type PluginHandler is the struct a plugin registers for the Gopherbot plugin
API.
*/
type PluginHandler struct {
	DefaultConfig string /* A yaml-formatted multiline string defining the default Plugin configuration. It should be liberally commented for use in generating
	local/custom configuration for the plugin. If a Config: section is defined, it should match the structure of the optional Config interface{} */
	Handler func(bot *Robot, command string, args ...string) // The callback function called by the robot whenever a Command is matched
	Config  interface{}                                      // An optional empty struct defining custom configuration for the plugin
}

// pluginHandlers maps from plugin names to PluginV1 (later interface{} with a type selector, maybe)
var pluginHandlers map[string]PluginHandler = make(map[string]PluginHandler)

// stopRegistrations is set "true" when the bot is created to prevent registration outside of init functions
var stopRegistrations bool = false

// initialize sends the "init" command to every plugin
func initializePlugins() {
	b.lock.RLock()
	defer b.lock.RUnlock()
	bot := &Robot{
		User:    b.name,
		Channel: "",
		Format:  Variable,
	}
	for _, plugin := range plugins {
		Log(Info, "Initializing plugin:", plugin.name)
		go callPlugin(bot, plugin, "init")
	}
}

// RegisterPlugin allows plugins to register a PluginHandler in a func init().
// When the bot initializes, it will call each plugin's handler with a command
// "init", empty channel, the bot's username, and no arguments, so the plugin
// can store this information for, e.g., scheduled jobs.
func RegisterPlugin(name string, plug PluginHandler) {
	if stopRegistrations {
		return
	}
	if _, exists := pluginHandlers[name]; exists {
		log.Fatal("Attempted registration of duplicate plugin name:", name)
	}
	pluginHandlers[name] = plug
}

func getExtDefCfg(plugin Plugin) (*[]byte, error) {
	var fullPath string
	if byte(plugin.pluginPath[0]) == byte("/"[0]) {
		fullPath = plugin.pluginPath
	} else {
		_, err := os.Stat(b.localPath + "/" + plugin.pluginPath)
		if err != nil {
			_, err := os.Stat(b.installPath + "/" + plugin.pluginPath)
			if err != nil {
				err = fmt.Errorf("Couldn't locate external plugin %s: %v", plugin.name, err)
				return nil, err
			}
			fullPath = b.installPath + "/" + plugin.pluginPath
			Log(Debug, "Using stock external plugin:", fullPath)
		} else {
			fullPath = b.localPath + "/" + plugin.pluginPath
			Log(Debug, "Using local external plugin:", fullPath)
		}
	}
	// cmd := exec.Command(fullPath, channel, user, matcher.Command, matches[0][1:]...)
	cfg, err := exec.Command(fullPath, "", "", "", "configure").Output()
	if err != nil {
		if exitErr, ok := err.(*exec.ExitError); ok {
			err = fmt.Errorf("Problem retrieving default configuration for external plugin \"%s\", skipping: \"%v\", output: %s", fullPath, err, exitErr.Stderr)
		} else {
			err = fmt.Errorf("Problem retrieving default configuration for external plugin \"%s\", skipping: \"%v\"", fullPath, err)
		}
		return nil, err
	} else {
		return &cfg, nil
	}
}

// loadPluginConfig() loads the configuration for all the plugins from
// $GOPHER_LOCALDIR/plugins/<pluginname>.yaml, assigns a pluginID, and
// stores the resulting array in b.plugins. Bad plugins are skipped and logged.
// Plugin configuration is initially loaded into temporary data structures,
// then stored in the bot package under the global bot lock.
func loadPluginConfig() {
	i := 0

	// Copy some data from the bot under lock
	b.lock.RLock()
	// Get a list of all plugins from the package pluginHandlers var and
	// the list of external plugins
	nump := len(pluginHandlers) + len(b.externalPlugins)
	pnames := make([]string, nump)
	ptypes := make([]plugType, nump)
	eppaths := make(map[string]string) // Paths to external plugins
	pfinder := make(map[string]int)    // keep a map of pluginIDs to identify plugins during a callback
	pset := make(map[string]bool)      // track plugin names

	// builtins come first so indexes match, see loop below
	// Note this doesn't need to be under RLock, but it needs to precede
	// external plugins. This should be fast enough that it doesn't matter.
	for _, plugin := range builtIns {
		pnames[i] = plugin
		pset[plugin] = true
		ptypes[i] = plugBuiltin
		i++
	}

	for index, plug := range b.externalPlugins {
		if len(plug.Name) == 0 || len(plug.Path) == 0 {
			Log(Error, fmt.Sprintf("Skipping external plugin #%d with zero-length Name or Path", index + 1))
			nump--
			continue
		}
		if !pNameRe.MatchString(plug.Name) {
			Log(Error, fmt.Sprintf("Plugin name \"%s\" doesn't match plugin name regex \"%s\", skipping", plug.Name, pNameRe.String()))
			nump--
			continue
		}
		if pset[plug.Name] {
			Log(Error, fmt.Sprintf("External plugin #%s, \"%s\" duplicates builtIn, skipping", index, plug.Name))
			nump--
			continue
		}
		pnames[i] = plug.Name
		pset[plug.Name] = true
		ptypes[i] = plugExternal
		eppaths[plug.Name] = plug.Path
		i++
	}
	// shrink slices when plugins were skipped
	pnames = pnames[0:nump]
	ptypes = ptypes[0:nump]
	// copy the list of default channels
	pchan := make([]string, 0, len(b.plugChannels))
	pchan = append(pchan, b.plugChannels...)
	b.lock.RUnlock() // we're done with bot data 'til the end

PlugHandlerLoop:
	for plug, _ := range pluginHandlers {
		if !pNameRe.MatchString(plug) {
			Log(Error, "Plugin name \"%s\" doesn't match plugin name regex \"%s\", skipping")
			nump--
			continue
		}
		if pset[plug] { // have to check builtIns, already loaded
			for _, plugin := range builtIns {
				if plug == plugin {
					continue PlugHandlerLoop // skip it, already loaded
				}
			}
			// Since external plugins can change on reload, just log an error if
			// we get a duplicate plugin name.
			Log(Error, "Plugin name duplicates external, skipping:", plug)
		} else {
			pnames[i] = plug
			ptypes[i] = plugGo
			i++
		}
	}
	// shrink slices when plugins were skipped
	pnames = pnames[0:nump]
	ptypes = ptypes[0:nump]
	plist := make([]Plugin, 0, nump)

	// Because some plugins may be disabled, pnames and plugins won't necessarily sync
	plugIndex := 0

PlugLoop:
	for i, plug := range pnames {
		var plugin Plugin
		Log(Trace, fmt.Sprintf("Loading plugin #%d - %s, type %d", plugIndex, plug, ptypes[i]))

		plugin.pluginType = ptypes[i]
		if plugin.pluginType == plugExternal {
			// External plugins spit their default config to stdout when called with command="configure"
			plugin.pluginPath = eppaths[plug]
			cfg, err := getExtDefCfg(plugin)
			if err != nil {
				Log(Error, err)
				continue
			}
			if err := yaml.Unmarshal(*cfg, &plugin); err != nil {
				Log(Error, fmt.Errorf("Problem unmarshalling plugin default config for \"%s\", skipping: %v", plug, err))
				continue
			}
		} else {
			if err := yaml.Unmarshal([]byte(pluginHandlers[plug].DefaultConfig), &plugin); err != nil {
				Log(Error, fmt.Errorf("Problem unmarshalling plugin default config for \"%s\", skipping: %v", plug, err))
				continue
			}
		}
		// getConfigFile overlays the default config with local config
		if err := getConfigFile("plugins/"+plug+".yaml", false, &plugin); err != nil {
			Log(Error, fmt.Errorf("Problem with local configuration for plugin \"%s\", skipping: %v", plug, err))
			continue
		}
		if plugin.Disabled {
			Log(Info, fmt.Sprintf("Plugin \"%s\" is disabled, skipping", plug))
			continue
		}
		Log(Info, "Loaded configuration for plugin", plug)
		// Use bot default plugin channels if none defined, unless AllChannels requested. Admin can override.
		if len(plugin.Channels) == 0 && len(pchan) > 0 && !plugin.AllChannels {
			plugin.Channels = pchan
		}
		Log(Info, fmt.Sprintf("Plugin \"%s\" will be active in channels %q; all channels: %t", plug, plugin.Channels, plugin.AllChannels))
		// Compile the regex's
		for i, _ := range plugin.CommandMatches {
			command := &plugin.CommandMatches[i]
			re, err := regexp.Compile(`^\s*` + command.Regex + `\s*$`)
			if err != nil {
				Log(Error, fmt.Errorf("Skipping %s, couldn't compile command regular expression \"%s\": %v", plug, command.Regex, err))
				continue PlugLoop
			}
			command.re = re
		}
		for i, _ := range plugin.ReplyMatchers {
			reply := &plugin.ReplyMatchers[i]
			re, err := regexp.Compile(`^\s*` + reply.Regex + `\s*$`)
			if err != nil {
				Log(Error, fmt.Errorf("Skipping %s, couldn't compile reply regular expression \"%s\": %v", plug, reply.Regex, err))
				continue PlugLoop
			}
			reply.re = re
		}
		for i, _ := range plugin.MessageMatches {
			// Note that full message regexes don't get the beginning and end anchors added - the individual plugin
			// will need to do this if necessary.
			message := &plugin.MessageMatches[i]
			re, err := regexp.Compile(message.Regex)
			if err != nil {
				Log(Error, fmt.Errorf("Skipping %s, couldn't compile message regular expression \"%s\": %v", plug, message.Regex, err))
				continue PlugLoop
			}
			message.re = re
		}
		plugin.name = plug
		// Copy the pointer to the empty config struct / empty struct (when no config)
		pt := reflect.ValueOf(pluginHandlers[plug].Config)
		if pt.Kind() == reflect.Ptr {
			if plugin.Config != nil {
				// reflect magic: create a pointer to a new empty config struct for the plugin
				plugin.config = reflect.New(reflect.Indirect(pt).Type()).Interface()
				cust, _ := yaml.Marshal(plugin.Config)
				if err := yaml.Unmarshal(cust, plugin.config); err != nil {
					Log(Error, fmt.Sprintf("Error unmarshalling plugin config json to config: %v", err))
				}
			} else {
				Log(Debug, "Plugin \"%s\" has custom config, but no local custom config provided", plug)
			}
		} else {
			Log(Debug, "config interface isn't a pointer, skipping unmarshal for plugin:", plug)
		}
		// Generate the random id
		p := make([]byte, 16)
		random.Read(p)
		plugin.pluginID = fmt.Sprintf("%x", p)
		pfinder[plugin.pluginID] = plugIndex
		// Store this plugin's config in the temporary list
		Log(Info, fmt.Sprintf("Recorded plugin #%d, \"%s\"", plugIndex, plugin.name))
		plist = append(plist, plugin)
		plugIndex++
	}

	reInitPlugins := false
	b.lock.Lock()
	plugins = plist
	plugIDmap = pfinder
	// loadPluginConfig is called in newBot, before the connector has started;
	// don't init plugins in that case.
	if b.Connector != nil {
		reInitPlugins = true
	}
	b.lock.Unlock()
	if reInitPlugins {
		initializePlugins()
	}
}
<|endoftext|>


<commit_before>//Package brake contains tools for setting up and working with the [airbrake](https://airbrake.io/) error monitoring software.
package brake

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"strings"

	"github.com/airbrake/gobrake"
	"github.com/eyecuelab/kit/functools"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

const (
	traceDepth = 5

	SeverityError    severity = "error"
	SeverityWarn     severity = "warning"
	SeverityCritical severity = "critical"
)

type severity string

var (
	Airbrake *gobrake.Notifier
	Env      string
)

func init() {
	cobra.OnInitialize(setup)
}

func setup() {
	key := viper.GetString("airbrake_key")
	project := viper.GetInt64("airbrake_project")
	Env = viper.GetString("airbrake_env")

	if len(key) != 0 && project != 0 {
		Airbrake = gobrake.NewNotifier(project, key)
	}
}

func IsSetup() bool {
	return Airbrake != nil
}

func Notify(e error, req *http.Request, sev severity) {
	if IsSetup() {
		notice := gobrake.NewNotice(e, req, traceDepth)
		setNoticeVars(notice, req, sev)
		Airbrake.SendNotice(notice)
	}
}

func setNoticeVars(n *gobrake.Notice, req *http.Request, sev severity) {
	n.Context["environment"] = Env
	n.Context["severity"] = sev
	if expectBody(req) {
		n.Params["body"] = body(req)
	}
}

func body(req *http.Request) interface{} {
	b, err := ioutil.ReadAll(req.Body)
	if err != nil {
		return fmt.Sprintf("error reading body: %v", err)
	}

	if len(b) == 0 {
		return "no body"
	}

	if !isJsonContentType(req) {
		return string(b)
	}

	// if !json.Valid(b) {
	// 	return fmt.Sprintf("body is not valid JSON: %s", b)
	// }

	formatted := make(map[string]interface{})
	json.Unmarshal(b, &formatted)

	return formatted
}

func isJsonContentType(req *http.Request) bool {
	cType := req.Header.Get("Content-Type")
	cType = strings.ToLower(cType)
	return strings.Contains(cType, "json")
}

func expectBody(req *http.Request) bool {
	requestsWithBody := []string{http.MethodPost, http.MethodPatch, http.MethodPut}
	return functools.StringSliceContains(requestsWithBody, req.Method)
}
<commit_msg>Update airbrake check for nil request pointer<commit_after>//Package brake contains tools for setting up and working with the [airbrake](https://airbrake.io/) error monitoring software.
package brake

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"strings"

	"github.com/airbrake/gobrake"
	"github.com/eyecuelab/kit/functools"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
)

const (
	traceDepth = 5

	SeverityError    severity = "error"
	SeverityWarn     severity = "warning"
	SeverityCritical severity = "critical"
)

type severity string

var (
	Airbrake *gobrake.Notifier
	Env      string
)

func init() {
	cobra.OnInitialize(setup)
}

func setup() {
	key := viper.GetString("airbrake_key")
	project := viper.GetInt64("airbrake_project")
	Env = viper.GetString("airbrake_env")

	if len(key) != 0 && project != 0 {
		Airbrake = gobrake.NewNotifier(project, key)
	}
}

func IsSetup() bool {
	return Airbrake != nil
}

func Notify(e error, req *http.Request, sev severity) {
	if IsSetup() {
		notice := gobrake.NewNotice(e, req, traceDepth)
		setNoticeVars(notice, req, sev)
		Airbrake.SendNotice(notice)
	}
}

func setNoticeVars(n *gobrake.Notice, req *http.Request, sev severity) {
	n.Context["environment"] = Env
	n.Context["severity"] = sev
	if expectBody(req) {
		n.Params["body"] = body(req)
	}
}

func body(req *http.Request) interface{} {
	b, err := ioutil.ReadAll(req.Body)
	if err != nil {
		return fmt.Sprintf("error reading body: %v", err)
	}

	if len(b) == 0 {
		return "no body"
	}

	if !isJSONContentType(req) {
		return string(b)
	}

	// if !json.Valid(b) {
	// 	return fmt.Sprintf("body is not valid JSON: %s", b)
	// }

	formatted := make(map[string]interface{})
	json.Unmarshal(b, &formatted)

	return formatted
}

func isJSONContentType(req *http.Request) bool {
	cType := req.Header.Get("Content-Type")
	cType = strings.ToLower(cType)
	return strings.Contains(cType, "json")
}

func expectBody(req *http.Request) bool {
	if req == nil {
		return false
	}
	requestsWithBody := []string{http.MethodPost, http.MethodPatch, http.MethodPut}
	return functools.StringSliceContains(requestsWithBody, req.Method)
}
<|endoftext|>


<commit_before>package store_test

import (
	"bytes"
	"fmt"
	"io/ioutil"
	. "launchpad.net/gocheck"
	"launchpad.net/juju-core/charm"
	"launchpad.net/juju-core/store"
	"launchpad.net/juju-core/testing"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"
)

func (s *StoreSuite) dummyBranch(c *C, suffix string) bzrDir {
	tmpDir := c.MkDir()
	if suffix != "" {
		tmpDir = filepath.Join(tmpDir, suffix)
		err := os.MkdirAll(tmpDir, 0755)
		c.Assert(err, IsNil)
	}
	branch := bzrDir(tmpDir)
	branch.init()

	copyCharmDir(branch.path(), testing.Charms.Dir("dummy"))
	branch.add()
	branch.commit("Imported charm.")
	return branch
}

var urls = []*charm.URL{
	charm.MustParseURL("cs:~joe/oneiric/dummy"),
	charm.MustParseURL("cs:oneiric/dummy"),
}

type fakePlugin struct {
	oldEnv string
}

func (p *fakePlugin) install(dir string, content string) {
	p.oldEnv = os.Getenv("BZR_PLUGINS_AT")
	err := ioutil.WriteFile(filepath.Join(dir, "__init__.py"), []byte(content), 0644)
	if err != nil {
		panic(err)
	}
	os.Setenv("BZR_PLUGINS_AT", "fakePlugin@" + dir)
}

func (p *fakePlugin) uninstall() {
	os.Setenv("BZR_PLUGINS_AT", p.oldEnv)
}

func (s *StoreSuite) TestPublish(c *C) {
	branch := s.dummyBranch(c, "")

	// Ensure that the streams are parsed separately by inserting
	// garbage on stderr. The wanted information is still there.
	plugin := fakePlugin{}
	plugin.install(c.MkDir(), `import sys; sys.stderr.write("STDERR STUFF FROM TEST\n")`)
	defer plugin.uninstall()

	err := store.PublishBazaarBranch(s.store, urls, branch.path(), "wrong-rev")
	c.Assert(err, IsNil)

	for _, url := range urls {
		info, rc, err := s.store.OpenCharm(url)
		c.Assert(err, IsNil)
		defer rc.Close()
		c.Assert(info.Revision(), Equals, 0)
		c.Assert(info.Meta().Name, Equals, "dummy")

		data, err := ioutil.ReadAll(rc)
		c.Assert(err, IsNil)

		bundle, err := charm.ReadBundleBytes(data)
		c.Assert(err, IsNil)
		c.Assert(bundle.Revision(), Equals, 0)
		c.Assert(bundle.Meta().Name, Equals, "dummy")
	}

	// Attempt to publish the same content again while providing the wrong
	// tip revision. It must pick the real revision from the branch and
	// note this was previously published.
	err = store.PublishBazaarBranch(s.store, urls, branch.path(), "wrong-rev")
	c.Assert(err, Equals, store.ErrRedundantUpdate)

	// Bump the content revision and lie again about the known tip revision.
	// This time, though, pretend it's the same as the real branch revision
	// previously published. It must error and not publish the new revision
	// because it will use the revision provided as a parameter to check if
	// publishing was attempted before. This is the mechanism that enables
	// stopping fast without having to download every single branch. Real
	// revision is picked in the next scan.
	digest1 := branch.digest()
	branch.change()
	err = store.PublishBazaarBranch(s.store, urls, branch.path(), digest1)
	c.Assert(err, Equals, store.ErrRedundantUpdate)

	// Now allow it to publish the new content by providing an unseen revision.
	err = store.PublishBazaarBranch(s.store, urls, branch.path(), "wrong-rev")
	c.Assert(err, IsNil)
	digest2 := branch.digest()

	info, err := s.store.CharmInfo(urls[0])
	c.Assert(err, IsNil)
	c.Assert(info.Revision(), Equals, 1)
	c.Assert(info.Meta().Name, Equals, "dummy")

	// There are two events published, for each of the successful attempts.
	// The failures are ignored given that they are artifacts of the
	// publishing mechanism rather than actual problems.
	_, err = s.store.CharmEvent(urls[0], "wrong-rev")
	c.Assert(err, Equals, store.ErrNotFound)
	for i, digest := range []string{digest1, digest2} {
		event, err := s.store.CharmEvent(urls[0], digest)
		c.Assert(err, IsNil)
		c.Assert(event.Kind, Equals, store.EventPublished)
		c.Assert(event.Revision, Equals, i)
		c.Assert(event.Errors, IsNil)
		c.Assert(event.Warnings, IsNil)
	}
}

func (s *StoreSuite) TestPublishErrorFromBzr(c *C) {
	branch := s.dummyBranch(c, "")

	// In TestPublish we ensure that the streams are parsed
	// separately by inserting garbage on stderr. Now make
	// sure that stderr isn't simply trashed, as we want to
	// know about what a real error tells us.
	plugin := fakePlugin{}
	plugin.install(c.MkDir(), `import sys; sys.stderr.write("STDERR STUFF FROM TEST\n"); sys.exit(1)`)
	defer plugin.uninstall()

	err := store.PublishBazaarBranch(s.store, urls, branch.path(), "wrong-rev")
	c.Assert(err, ErrorMatches, "(?s).*STDERR STUFF.*")
}

func (s *StoreSuite) TestPublishErrorInCharm(c *C) {
	branch := s.dummyBranch(c, "")

	// Corrupt the charm.
	branch.remove("metadata.yaml")
	branch.commit("Removed metadata.yaml.")

	// Attempt to publish the erroneous content.
	err := store.PublishBazaarBranch(s.store, urls, branch.path(), "wrong-rev")
	c.Assert(err, ErrorMatches, ".*/metadata.yaml: no such file or directory")

	// The event should be logged as well, since this was an error in the charm
	// that won't go away and must be communicated to the author.
	event, err := s.store.CharmEvent(urls[0], branch.digest())
	c.Assert(err, IsNil)
	c.Assert(event.Kind, Equals, store.EventPublishError)
	c.Assert(event.Revision, Equals, 0)
	c.Assert(event.Errors, NotNil)
	c.Assert(event.Errors[0], Matches, ".*/metadata.yaml: no such file or directory")
	c.Assert(event.Warnings, IsNil)
}

type bzrDir string

func (dir bzrDir) path(args ...string) string {
	return filepath.Join(append([]string{string(dir)}, args...)...)
}

func (dir bzrDir) run(args ...string) []byte {
	cmd := exec.Command("bzr", args...)
	cmd.Dir = string(dir)
	output, err := cmd.Output()
	if err != nil {
		panic(fmt.Sprintf("command failed: bzr %s\n%s", strings.Join(args, " "), output))
	}
	return output
}

func (dir bzrDir) init() {
	dir.run("init")
}

func (dir bzrDir) add(paths ...string) {
	dir.run(append([]string{"add"}, paths...)...)
}

func (dir bzrDir) remove(paths ...string) {
	dir.run(append([]string{"rm"}, paths...)...)
}

func (dir bzrDir) commit(msg string) {
	dir.run("commit", "-m", msg)
}

func (dir bzrDir) write(path string, data string) {
	err := ioutil.WriteFile(dir.path(path), []byte(data), 0644)
	if err != nil {
		panic(err)
	}
}

func (dir bzrDir) change() {
	t := time.Now().String()
	dir.write("timestamp", t)
	dir.add("timestamp")
	dir.commit("Revision bumped at " + t)
}

func (dir bzrDir) digest() string {
	output := dir.run("revision-info")
	f := bytes.Fields(output)
	if len(f) != 2 {
		panic("revision-info returned bad output: " + string(output))
	}
	return string(f[1])
}

func copyCharmDir(dst string, dir *charm.Dir) {
	var b bytes.Buffer
	err := dir.BundleTo(&b)
	if err != nil {
		panic(err)
	}
	bundle, err := charm.ReadBundleBytes(b.Bytes())
	if err != nil {
		panic(err)
	}
	err = bundle.ExpandTo(dst)
	if err != nil {
		panic(err)
	}
}
<commit_msg>go fmt<commit_after>package store_test

import (
	"bytes"
	"fmt"
	"io/ioutil"
	. "launchpad.net/gocheck"
	"launchpad.net/juju-core/charm"
	"launchpad.net/juju-core/store"
	"launchpad.net/juju-core/testing"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"
)

func (s *StoreSuite) dummyBranch(c *C, suffix string) bzrDir {
	tmpDir := c.MkDir()
	if suffix != "" {
		tmpDir = filepath.Join(tmpDir, suffix)
		err := os.MkdirAll(tmpDir, 0755)
		c.Assert(err, IsNil)
	}
	branch := bzrDir(tmpDir)
	branch.init()

	copyCharmDir(branch.path(), testing.Charms.Dir("dummy"))
	branch.add()
	branch.commit("Imported charm.")
	return branch
}

var urls = []*charm.URL{
	charm.MustParseURL("cs:~joe/oneiric/dummy"),
	charm.MustParseURL("cs:oneiric/dummy"),
}

type fakePlugin struct {
	oldEnv string
}

func (p *fakePlugin) install(dir string, content string) {
	p.oldEnv = os.Getenv("BZR_PLUGINS_AT")
	err := ioutil.WriteFile(filepath.Join(dir, "__init__.py"), []byte(content), 0644)
	if err != nil {
		panic(err)
	}
	os.Setenv("BZR_PLUGINS_AT", "fakePlugin@"+dir)
}

func (p *fakePlugin) uninstall() {
	os.Setenv("BZR_PLUGINS_AT", p.oldEnv)
}

func (s *StoreSuite) TestPublish(c *C) {
	branch := s.dummyBranch(c, "")

	// Ensure that the streams are parsed separately by inserting
	// garbage on stderr. The wanted information is still there.
	plugin := fakePlugin{}
	plugin.install(c.MkDir(), `import sys; sys.stderr.write("STDERR STUFF FROM TEST\n")`)
	defer plugin.uninstall()

	err := store.PublishBazaarBranch(s.store, urls, branch.path(), "wrong-rev")
	c.Assert(err, IsNil)

	for _, url := range urls {
		info, rc, err := s.store.OpenCharm(url)
		c.Assert(err, IsNil)
		defer rc.Close()
		c.Assert(info.Revision(), Equals, 0)
		c.Assert(info.Meta().Name, Equals, "dummy")

		data, err := ioutil.ReadAll(rc)
		c.Assert(err, IsNil)

		bundle, err := charm.ReadBundleBytes(data)
		c.Assert(err, IsNil)
		c.Assert(bundle.Revision(), Equals, 0)
		c.Assert(bundle.Meta().Name, Equals, "dummy")
	}

	// Attempt to publish the same content again while providing the wrong
	// tip revision. It must pick the real revision from the branch and
	// note this was previously published.
	err = store.PublishBazaarBranch(s.store, urls, branch.path(), "wrong-rev")
	c.Assert(err, Equals, store.ErrRedundantUpdate)

	// Bump the content revision and lie again about the known tip revision.
	// This time, though, pretend it's the same as the real branch revision
	// previously published. It must error and not publish the new revision
	// because it will use the revision provided as a parameter to check if
	// publishing was attempted before. This is the mechanism that enables
	// stopping fast without having to download every single branch. Real
	// revision is picked in the next scan.
	digest1 := branch.digest()
	branch.change()
	err = store.PublishBazaarBranch(s.store, urls, branch.path(), digest1)
	c.Assert(err, Equals, store.ErrRedundantUpdate)

	// Now allow it to publish the new content by providing an unseen revision.
	err = store.PublishBazaarBranch(s.store, urls, branch.path(), "wrong-rev")
	c.Assert(err, IsNil)
	digest2 := branch.digest()

	info, err := s.store.CharmInfo(urls[0])
	c.Assert(err, IsNil)
	c.Assert(info.Revision(), Equals, 1)
	c.Assert(info.Meta().Name, Equals, "dummy")

	// There are two events published, for each of the successful attempts.
	// The failures are ignored given that they are artifacts of the
	// publishing mechanism rather than actual problems.
	_, err = s.store.CharmEvent(urls[0], "wrong-rev")
	c.Assert(err, Equals, store.ErrNotFound)
	for i, digest := range []string{digest1, digest2} {
		event, err := s.store.CharmEvent(urls[0], digest)
		c.Assert(err, IsNil)
		c.Assert(event.Kind, Equals, store.EventPublished)
		c.Assert(event.Revision, Equals, i)
		c.Assert(event.Errors, IsNil)
		c.Assert(event.Warnings, IsNil)
	}
}

func (s *StoreSuite) TestPublishErrorFromBzr(c *C) {
	branch := s.dummyBranch(c, "")

	// In TestPublish we ensure that the streams are parsed
	// separately by inserting garbage on stderr. Now make
	// sure that stderr isn't simply trashed, as we want to
	// know about what a real error tells us.
	plugin := fakePlugin{}
	plugin.install(c.MkDir(), `import sys; sys.stderr.write("STDERR STUFF FROM TEST\n"); sys.exit(1)`)
	defer plugin.uninstall()

	err := store.PublishBazaarBranch(s.store, urls, branch.path(), "wrong-rev")
	c.Assert(err, ErrorMatches, "(?s).*STDERR STUFF.*")
}

func (s *StoreSuite) TestPublishErrorInCharm(c *C) {
	branch := s.dummyBranch(c, "")

	// Corrupt the charm.
	branch.remove("metadata.yaml")
	branch.commit("Removed metadata.yaml.")

	// Attempt to publish the erroneous content.
	err := store.PublishBazaarBranch(s.store, urls, branch.path(), "wrong-rev")
	c.Assert(err, ErrorMatches, ".*/metadata.yaml: no such file or directory")

	// The event should be logged as well, since this was an error in the charm
	// that won't go away and must be communicated to the author.
	event, err := s.store.CharmEvent(urls[0], branch.digest())
	c.Assert(err, IsNil)
	c.Assert(event.Kind, Equals, store.EventPublishError)
	c.Assert(event.Revision, Equals, 0)
	c.Assert(event.Errors, NotNil)
	c.Assert(event.Errors[0], Matches, ".*/metadata.yaml: no such file or directory")
	c.Assert(event.Warnings, IsNil)
}

type bzrDir string

func (dir bzrDir) path(args ...string) string {
	return filepath.Join(append([]string{string(dir)}, args...)...)
}

func (dir bzrDir) run(args ...string) []byte {
	cmd := exec.Command("bzr", args...)
	cmd.Dir = string(dir)
	output, err := cmd.Output()
	if err != nil {
		panic(fmt.Sprintf("command failed: bzr %s\n%s", strings.Join(args, " "), output))
	}
	return output
}

func (dir bzrDir) init() {
	dir.run("init")
}

func (dir bzrDir) add(paths ...string) {
	dir.run(append([]string{"add"}, paths...)...)
}

func (dir bzrDir) remove(paths ...string) {
	dir.run(append([]string{"rm"}, paths...)...)
}

func (dir bzrDir) commit(msg string) {
	dir.run("commit", "-m", msg)
}

func (dir bzrDir) write(path string, data string) {
	err := ioutil.WriteFile(dir.path(path), []byte(data), 0644)
	if err != nil {
		panic(err)
	}
}

func (dir bzrDir) change() {
	t := time.Now().String()
	dir.write("timestamp", t)
	dir.add("timestamp")
	dir.commit("Revision bumped at " + t)
}

func (dir bzrDir) digest() string {
	output := dir.run("revision-info")
	f := bytes.Fields(output)
	if len(f) != 2 {
		panic("revision-info returned bad output: " + string(output))
	}
	return string(f[1])
}

func copyCharmDir(dst string, dir *charm.Dir) {
	var b bytes.Buffer
	err := dir.BundleTo(&b)
	if err != nil {
		panic(err)
	}
	bundle, err := charm.ReadBundleBytes(b.Bytes())
	if err != nil {
		panic(err)
	}
	err = bundle.ExpandTo(dst)
	if err != nil {
		panic(err)
	}
}
<|endoftext|>


<commit_before>// Copyright © 2017 Circonus, Inc. <support@circonus.com>
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//

package defaults

import (
	"fmt"
	"os"
	"path/filepath"
	"runtime"

	"github.com/circonus-labs/circonus-agent/internal/release"
)

const (
	// ListenPort is the default agent tcp listening port
	ListenPort = 2609

	// APIURL for circonus
	APIURL = "https://api.circonus.com/v2/"

	// APIApp defines the api app name associated with the api token key
	APIApp = release.NAME

	// Reverse is false by default
	Reverse = false

	// SSLVerify enabled by default
	SSLVerify = true

	// NoStatsd enabled by default
	NoStatsd = false

	// Debug is false by default
	Debug = false

	// LogLevel set to info by default
	LogLevel = "info"

	// LogPretty colored/formatted output to stderr
	LogPretty = false

	// UID to drop privileges to on start
	UID = "nobody"

	// Watch plugins for changes
	Watch = false

	// ReverseMaxConnRetry - how many times to retry persistently failing broker connection
	ReverseMaxConnRetry = -1

	// StatsdPort to listen, NOTE address is always localhost
	StatsdPort = "8125"

	// StatsdHostPrefix defines that metrics received through StatsD inteface
	// which are prefixed with this string plus a period go to the host check
	StatsdHostPrefix = "" //"host."

	// StatsdHostCategory defines the "plugin" in which the host metrics will be namepspaced
	StatsdHostCategory = "statsd"

	// StatsdGroupPrefix defines that metrics received through StatsD inteface
	// which are prefixed with this string plus a period go to the group check, if enabled
	StatsdGroupPrefix = "group."

	// StatsdGroupCounters defines how group counter metrics will be handled (average or sum)
	StatsdGroupCounters = "sum"

	// StatsdGroupGauges defines how group counter metrics will be handled (average or sum)
	StatsdGroupGauges = "average"

	// StatsdGroupSets defines how group counter metrics will be handled (average or sum)
	StatsdGroupSets = "sum"

	// MetricNameSeparator defines character used to delimit metric name parts
	MetricNameSeparator = "`"

	// PluginTTLUnits defines the default TTL units for plugins with TTLs
	// e.g. plugin_ttl30s.sh (30s ttl) plugin_ttl45.sh (would get default ttl units, e.g. 45s)
	PluginTTLUnits = "s" // seconds

	// DisableGzip disables gzip compression on responses
	DisableGzip = false

	// CheckEnableNewMetrics toggles enabling new metrics
	CheckEnableNewMetrics = false
	// CheckMetricRefreshTTL determines how often to refresh check bundle metrics from API
	CheckMetricRefreshTTL = "5m"

	// CheckCreate toggles creating a check if a check bundle id is not supplied
	CheckCreate = false

	// CheckBroker to use if creating a check, 'select' or '' will
	// result in the first broker which meets some basic criteria being selected.
	// 1. Active status
	// 2. Supports the required check type
	// 3. Responds within reverse.brokerMaxResponseTime
	CheckBroker = "select"

	// CheckTags to use if creating a check (comma separated list)
	CheckTags = ""
)

var (
	// Listen defaults to all interfaces on the default ListenPort
	// valid formats:
	//      ip:port (e.g. 127.0.0.1:12345 - listen address 127.0.0.1, port 12345)
	//      ip (e.g. 127.0.0.1 - listen address 127.0.0.1, port ListenPort)
	//      port (e.g. 12345 (or :12345) - listen address all, port 12345)
	//
	Listen = fmt.Sprintf(":%d", ListenPort)

	// BasePath is the "base" directory
	//
	// expected installation structure:
	// base        (e.g. /opt/circonus/agent)
	//   /bin      (e.g. /opt/circonus/agent/bin)
	//   /etc      (e.g. /opt/circonus/agent/etc)
	//   /plugins  (e.g. /opt/circonus/agent/plugins)
	//   /sbin     (e.g. /opt/circonus/agent/sbin)
	BasePath = ""

	// Collectors defines the default builtin collectors to enable
	// OS specific - see init() below
	Collectors = []string{}

	// EtcPath returns the default etc directory within base directory
	EtcPath = "" // (e.g. /opt/circonus/agent/etc)

	// PluginPath returns the default plugin path
	PluginPath = "" // (e.g. /opt/circonus/agent/plugins)

	// CheckTarget defaults to return from os.Hostname()
	CheckTarget = ""

	// CheckTitle defaults to '<CheckTarget> /agent'
	CheckTitle = ""

	// CheckMetricStatePath returns the default state directory. In order for
	// automatic new metric enabling to work the state path must exist
	// and be owned by the user running circonus-agentd (i.e. 'nobody').
	CheckMetricStatePath = "" // (e.g. /opt/circonus/agent/state)

	// CheckMetricFilters defines default filter to be used with new check creation
	CheckMetricFilters = [][]string{[]string{"^$", "deny", ""}, []string{"allow", "^.+$", ""}}

	// SSLCertFile returns the deefault ssl cert file name
	SSLCertFile = "" // (e.g. /opt/circonus/agent/etc/agent.pem)

	// SSLKeyFile returns the deefault ssl key file name
	SSLKeyFile = "" // (e.g. /opt/circonus/agent/etc/agent.key)

	// StatsdConf returns the default statsd config file
	StatsdConf = "" // (e.g. /opt/circonus/agent/etc/statsd.json)
)

func init() {
	var exePath string
	var resolvedExePath string
	var err error

	exePath, err = os.Executable()
	if err == nil {
		resolvedExePath, err = filepath.EvalSymlinks(exePath)
		if err == nil {
			BasePath = filepath.Clean(filepath.Join(filepath.Dir(resolvedExePath), "..")) // e.g. /opt/circonus/agent
		}
	}

	if err != nil {
		fmt.Printf("Unable to determine path to binary %v\n", err)
		os.Exit(1)
	}

	EtcPath = filepath.Join(BasePath, "etc")
	CheckMetricStatePath = filepath.Join(BasePath, "state")
	PluginPath = filepath.Join(BasePath, "plugins")
	SSLCertFile = filepath.Join(EtcPath, release.NAME+".pem")
	SSLKeyFile = filepath.Join(EtcPath, release.NAME+".key")

	CheckTarget, err = os.Hostname()
	if err != nil {
		fmt.Printf("Unable to determine hostname for target %v\n", err)
		os.Exit(1)
	}
	CheckTitle = CheckTarget + " /agent"

	switch runtime.GOOS {
	case "linux":
		Collectors = []string{
			"cpu",
			"diskstats",
			"if",
			"loadavg",
			"vm",
		}
	case "windows":
		Collectors = []string{
			"cache",
			"disk", // logical and physical
			"interface",
			"ip", // ipv4 and ipv6
			"memory",
			"objects",
			"paging_file",
			// "processes",
			"processor",
			"tcp", // ipv4 and ipv6
			"udp", // ipv4 and ipv6
		}
	}
}
<commit_msg>fix: default metric_filters setting<commit_after>// Copyright © 2017 Circonus, Inc. <support@circonus.com>
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//

package defaults

import (
	"fmt"
	"os"
	"path/filepath"
	"runtime"

	"github.com/circonus-labs/circonus-agent/internal/release"
)

const (
	// ListenPort is the default agent tcp listening port
	ListenPort = 2609

	// APIURL for circonus
	APIURL = "https://api.circonus.com/v2/"

	// APIApp defines the api app name associated with the api token key
	APIApp = release.NAME

	// Reverse is false by default
	Reverse = false

	// SSLVerify enabled by default
	SSLVerify = true

	// NoStatsd enabled by default
	NoStatsd = false

	// Debug is false by default
	Debug = false

	// LogLevel set to info by default
	LogLevel = "info"

	// LogPretty colored/formatted output to stderr
	LogPretty = false

	// UID to drop privileges to on start
	UID = "nobody"

	// Watch plugins for changes
	Watch = false

	// ReverseMaxConnRetry - how many times to retry persistently failing broker connection
	ReverseMaxConnRetry = -1

	// StatsdPort to listen, NOTE address is always localhost
	StatsdPort = "8125"

	// StatsdHostPrefix defines that metrics received through StatsD inteface
	// which are prefixed with this string plus a period go to the host check
	StatsdHostPrefix = "" //"host."

	// StatsdHostCategory defines the "plugin" in which the host metrics will be namepspaced
	StatsdHostCategory = "statsd"

	// StatsdGroupPrefix defines that metrics received through StatsD inteface
	// which are prefixed with this string plus a period go to the group check, if enabled
	StatsdGroupPrefix = "group."

	// StatsdGroupCounters defines how group counter metrics will be handled (average or sum)
	StatsdGroupCounters = "sum"

	// StatsdGroupGauges defines how group counter metrics will be handled (average or sum)
	StatsdGroupGauges = "average"

	// StatsdGroupSets defines how group counter metrics will be handled (average or sum)
	StatsdGroupSets = "sum"

	// MetricNameSeparator defines character used to delimit metric name parts
	MetricNameSeparator = "`"

	// PluginTTLUnits defines the default TTL units for plugins with TTLs
	// e.g. plugin_ttl30s.sh (30s ttl) plugin_ttl45.sh (would get default ttl units, e.g. 45s)
	PluginTTLUnits = "s" // seconds

	// DisableGzip disables gzip compression on responses
	DisableGzip = false

	// CheckEnableNewMetrics toggles enabling new metrics
	CheckEnableNewMetrics = false
	// CheckMetricRefreshTTL determines how often to refresh check bundle metrics from API
	CheckMetricRefreshTTL = "5m"

	// CheckCreate toggles creating a check if a check bundle id is not supplied
	CheckCreate = false

	// CheckBroker to use if creating a check, 'select' or '' will
	// result in the first broker which meets some basic criteria being selected.
	// 1. Active status
	// 2. Supports the required check type
	// 3. Responds within reverse.brokerMaxResponseTime
	CheckBroker = "select"

	// CheckTags to use if creating a check (comma separated list)
	CheckTags = ""
)

var (
	// Listen defaults to all interfaces on the default ListenPort
	// valid formats:
	//      ip:port (e.g. 127.0.0.1:12345 - listen address 127.0.0.1, port 12345)
	//      ip (e.g. 127.0.0.1 - listen address 127.0.0.1, port ListenPort)
	//      port (e.g. 12345 (or :12345) - listen address all, port 12345)
	//
	Listen = fmt.Sprintf(":%d", ListenPort)

	// BasePath is the "base" directory
	//
	// expected installation structure:
	// base        (e.g. /opt/circonus/agent)
	//   /bin      (e.g. /opt/circonus/agent/bin)
	//   /etc      (e.g. /opt/circonus/agent/etc)
	//   /plugins  (e.g. /opt/circonus/agent/plugins)
	//   /sbin     (e.g. /opt/circonus/agent/sbin)
	BasePath = ""

	// Collectors defines the default builtin collectors to enable
	// OS specific - see init() below
	Collectors = []string{}

	// EtcPath returns the default etc directory within base directory
	EtcPath = "" // (e.g. /opt/circonus/agent/etc)

	// PluginPath returns the default plugin path
	PluginPath = "" // (e.g. /opt/circonus/agent/plugins)

	// CheckTarget defaults to return from os.Hostname()
	CheckTarget = ""

	// CheckTitle defaults to '<CheckTarget> /agent'
	CheckTitle = ""

	// CheckMetricStatePath returns the default state directory. In order for
	// automatic new metric enabling to work the state path must exist
	// and be owned by the user running circonus-agentd (i.e. 'nobody').
	CheckMetricStatePath = "" // (e.g. /opt/circonus/agent/state)

	// CheckMetricFilters defines default filter to be used with new check creation
	CheckMetricFilters = [][]string{[]string{"deny", "^$", ""}, []string{"allow", "^.+$", ""}}

	// SSLCertFile returns the deefault ssl cert file name
	SSLCertFile = "" // (e.g. /opt/circonus/agent/etc/agent.pem)

	// SSLKeyFile returns the deefault ssl key file name
	SSLKeyFile = "" // (e.g. /opt/circonus/agent/etc/agent.key)

	// StatsdConf returns the default statsd config file
	StatsdConf = "" // (e.g. /opt/circonus/agent/etc/statsd.json)
)

func init() {
	var exePath string
	var resolvedExePath string
	var err error

	exePath, err = os.Executable()
	if err == nil {
		resolvedExePath, err = filepath.EvalSymlinks(exePath)
		if err == nil {
			BasePath = filepath.Clean(filepath.Join(filepath.Dir(resolvedExePath), "..")) // e.g. /opt/circonus/agent
		}
	}

	if err != nil {
		fmt.Printf("Unable to determine path to binary %v\n", err)
		os.Exit(1)
	}

	EtcPath = filepath.Join(BasePath, "etc")
	CheckMetricStatePath = filepath.Join(BasePath, "state")
	PluginPath = filepath.Join(BasePath, "plugins")
	SSLCertFile = filepath.Join(EtcPath, release.NAME+".pem")
	SSLKeyFile = filepath.Join(EtcPath, release.NAME+".key")

	CheckTarget, err = os.Hostname()
	if err != nil {
		fmt.Printf("Unable to determine hostname for target %v\n", err)
		os.Exit(1)
	}
	CheckTitle = CheckTarget + " /agent"

	switch runtime.GOOS {
	case "linux":
		Collectors = []string{
			"cpu",
			"diskstats",
			"if",
			"loadavg",
			"vm",
		}
	case "windows":
		Collectors = []string{
			"cache",
			"disk", // logical and physical
			"interface",
			"ip", // ipv4 and ipv6
			"memory",
			"objects",
			"paging_file",
			// "processes",
			"processor",
			"tcp", // ipv4 and ipv6
			"udp", // ipv4 and ipv6
		}
	}
}
<|endoftext|>


<commit_before>// Copyright 2011 Google Inc. All Rights Reserved.
// This file is available under the Apache license.

package exporter

import (
	"context"
	"errors"
	"reflect"
	"sort"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/google/mtail/internal/metrics"
	"github.com/google/mtail/internal/metrics/datum"
	"github.com/google/mtail/internal/testutil"
)

const prefix = "prefix"

func TestCreateExporter(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	var wg sync.WaitGroup
	_, err := New(ctx, &wg, nil)
	if err == nil {
		t.Error("expecting error, got nil")
	}
	cancel()
	wg.Wait()
	ctx, cancel = context.WithCancel(context.Background())
	store := metrics.NewStore()
	_, err = New(ctx, &wg, store)
	if err != nil {
		t.Errorf("unexpected error:%s", err)
	}
	cancel()
	wg.Wait()
	ctx, cancel = context.WithCancel(context.Background())
	failopt := func(*Exporter) error {
		return errors.New("busted") // nolint:goerr113
	}
	_, err = New(ctx, &wg, store, failopt)
	if err == nil {
		t.Errorf("unexpected success")
	}
	cancel()
	wg.Wait()
}

func FakeSocketWrite(f formatter, m *metrics.Metric) []string {
	ret := make([]string, 0)
	lc := make(chan *metrics.LabelSet)
	d := 60 * time.Second
	go m.EmitLabelSets(lc)
	for l := range lc {
		ret = append(ret, f("gunstar", m, l, d))
	}
	sort.Strings(ret)
	return ret
}

func TestMetricToCollectd(t *testing.T) {
	*collectdPrefix = ""
	ts, terr := time.Parse("2006/01/02 15:04:05", "2012/07/24 10:14:00")
	if terr != nil {
		t.Errorf("time parse error: %s", terr)
	}
	ms := metrics.NewStore()

	scalarMetric := metrics.NewMetric("foo", "prog", metrics.Counter, metrics.Int)
	d, _ := scalarMetric.GetDatum()
	datum.SetInt(d, 37, ts)
	testutil.FatalIfErr(t, ms.Add(scalarMetric))

	r := FakeSocketWrite(metricToCollectd, scalarMetric)
	expected := []string{"PUTVAL \"gunstar/mtail-prog/counter-foo\" interval=60 1343124840:37\n"}
	testutil.ExpectNoDiff(t, expected, r)

	dimensionedMetric := metrics.NewMetric("bar", "prog", metrics.Gauge, metrics.Int, "label")
	d, _ = dimensionedMetric.GetDatum("quux")
	datum.SetInt(d, 37, ts)
	d, _ = dimensionedMetric.GetDatum("snuh")
	datum.SetInt(d, 37, ts)
	ms.ClearMetrics()
	testutil.FatalIfErr(t, ms.Add(dimensionedMetric))

	r = FakeSocketWrite(metricToCollectd, dimensionedMetric)
	expected = []string{
		"PUTVAL \"gunstar/mtail-prog/gauge-bar-label-quux\" interval=60 1343124840:37\n",
		"PUTVAL \"gunstar/mtail-prog/gauge-bar-label-snuh\" interval=60 1343124840:37\n",
	}
	testutil.ExpectNoDiff(t, expected, r)

	timingMetric := metrics.NewMetric("foo", "prog", metrics.Timer, metrics.Int)
	d, _ = timingMetric.GetDatum()
	datum.SetInt(d, 123, ts)
	testutil.FatalIfErr(t, ms.Add(timingMetric))

	r = FakeSocketWrite(metricToCollectd, timingMetric)
	expected = []string{"PUTVAL \"gunstar/mtail-prog/gauge-foo\" interval=60 1343124840:123\n"}
	testutil.ExpectNoDiff(t, expected, r)

	*collectdPrefix = prefix
	r = FakeSocketWrite(metricToCollectd, timingMetric)
	expected = []string{"PUTVAL \"gunstar/prefixmtail-prog/gauge-foo\" interval=60 1343124840:123\n"}
	testutil.ExpectNoDiff(t, expected, r)
}

func TestMetricToGraphite(t *testing.T) {
	*graphitePrefix = ""
	ts, terr := time.Parse("2006/01/02 15:04:05", "2012/07/24 10:14:00")
	if terr != nil {
		t.Errorf("time parse error: %s", terr)
	}

	scalarMetric := metrics.NewMetric("foo", "prog", metrics.Counter, metrics.Int)
	d, _ := scalarMetric.GetDatum()
	datum.SetInt(d, 37, ts)
	r := FakeSocketWrite(metricToGraphite, scalarMetric)
	expected := []string{"prog.foo 37 1343124840\n"}
	testutil.ExpectNoDiff(t, expected, r)

	dimensionedMetric := metrics.NewMetric("bar", "prog", metrics.Gauge, metrics.Int, "host")
	d, _ = dimensionedMetric.GetDatum("quux.com")
	datum.SetInt(d, 37, ts)
	d, _ = dimensionedMetric.GetDatum("snuh.teevee")
	datum.SetInt(d, 37, ts)
	r = FakeSocketWrite(metricToGraphite, dimensionedMetric)
	expected = []string{
		"prog.bar.host.quux_com 37 1343124840\n",
		"prog.bar.host.snuh_teevee 37 1343124840\n",
	}
	testutil.ExpectNoDiff(t, expected, r)

	histogramMetric := metrics.NewMetric("hist", "prog", metrics.Histogram, metrics.Buckets, "xxx")
	lv := &metrics.LabelValue{Labels: []string{"bar"}, Value: datum.MakeBuckets([]datum.Range{{0, 10}, {10, 20}}, time.Unix(0, 0))}
	histogramMetric.AppendLabelValue(lv)
	d, _ = histogramMetric.GetDatum("bar")
	datum.SetFloat(d, 1, ts)
	datum.SetFloat(d, 5, ts)
	datum.SetFloat(d, 15, ts)
	datum.SetFloat(d, 12, ts)
	datum.SetFloat(d, 19, ts)
	datum.SetFloat(d, 1000, ts)
	r = FakeSocketWrite(metricToGraphite, histogramMetric)
	r = strings.Split(strings.TrimSuffix(r[0], "\n"), "\n")
	sort.Strings(r)
	expected = []string{
		"prog.hist.xxx.bar 1052 1343124840",
		"prog.hist.xxx.bar.bin_10 2 1343124840",
		"prog.hist.xxx.bar.bin_20 3 1343124840",
		"prog.hist.xxx.bar.bin_inf 1 1343124840",
		"prog.hist.xxx.bar.count 6 1343124840",
	}
	testutil.ExpectNoDiff(t, expected, r)

	*graphitePrefix = prefix
	r = FakeSocketWrite(metricToGraphite, dimensionedMetric)
	expected = []string{
		"prefixprog.bar.host.quux_com 37 1343124840\n",
		"prefixprog.bar.host.snuh_teevee 37 1343124840\n",
	}
	testutil.ExpectNoDiff(t, expected, r)
}

func TestMetricToStatsd(t *testing.T) {
	*statsdPrefix = ""
	ts, terr := time.Parse("2006/01/02 15:04:05", "2012/07/24 10:14:00")
	if terr != nil {
		t.Errorf("time parse error: %s", terr)
	}

	scalarMetric := metrics.NewMetric("foo", "prog", metrics.Counter, metrics.Int)
	d, _ := scalarMetric.GetDatum()
	datum.SetInt(d, 37, ts)
	r := FakeSocketWrite(metricToStatsd, scalarMetric)
	expected := []string{"prog.foo:37|c"}
	if !reflect.DeepEqual(expected, r) {
		t.Errorf("String didn't match:\n\texpected: %v\n\treceived: %v", expected, r)
	}

	dimensionedMetric := metrics.NewMetric("bar", "prog", metrics.Gauge, metrics.Int, "l")
	d, _ = dimensionedMetric.GetDatum("quux")
	datum.SetInt(d, 37, ts)
	d, _ = dimensionedMetric.GetDatum("snuh")
	datum.SetInt(d, 42, ts)
	r = FakeSocketWrite(metricToStatsd, dimensionedMetric)
	expected = []string{
		"prog.bar.l.quux:37|g",
		"prog.bar.l.snuh:42|g",
	}
	if !reflect.DeepEqual(expected, r) {
		t.Errorf("String didn't match:\n\texpected: %v\n\treceived: %v", expected, r)
	}

	multiLabelMetric := metrics.NewMetric("bar", "prog", metrics.Gauge, metrics.Int, "c", "a", "b")
	d, _ = multiLabelMetric.GetDatum("x","z","y")
	datum.SetInt(d, 37, ts)
	r = FakeSocketWrite(metricToStatsd, multiLabelMetric)
	expected = []string{"prog.bar.a.z.b.y.c.x:37|g"}
	if !reflect.DeepEqual(expected, r) {
		t.Errorf("String didn't match:\n\texpected: %v\n\treceived: %v", expected, r)
	}

	timingMetric := metrics.NewMetric("foo", "prog", metrics.Timer, metrics.Int)
	d, _ = timingMetric.GetDatum()
	datum.SetInt(d, 37, ts)
	r = FakeSocketWrite(metricToStatsd, timingMetric)
	expected = []string{"prog.foo:37|ms"}
	if !reflect.DeepEqual(expected, r) {
		t.Errorf("String didn't match:\n\texpected: %v\n\treceived: %v", expected, r)
	}

	*statsdPrefix = prefix
	r = FakeSocketWrite(metricToStatsd, timingMetric)
	expected = []string{"prefixprog.foo:37|ms"}
	if !reflect.DeepEqual(expected, r) {
		t.Errorf("prefixed string didn't match:\n\texpected: %v\n\treceived: %v", expected, r)
	}
}
<commit_msg>Make linter happy<commit_after>// Copyright 2011 Google Inc. All Rights Reserved.
// This file is available under the Apache license.

package exporter

import (
	"context"
	"errors"
	"reflect"
	"sort"
	"strings"
	"sync"
	"testing"
	"time"

	"github.com/google/mtail/internal/metrics"
	"github.com/google/mtail/internal/metrics/datum"
	"github.com/google/mtail/internal/testutil"
)

const prefix = "prefix"

func TestCreateExporter(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())
	var wg sync.WaitGroup
	_, err := New(ctx, &wg, nil)
	if err == nil {
		t.Error("expecting error, got nil")
	}
	cancel()
	wg.Wait()
	ctx, cancel = context.WithCancel(context.Background())
	store := metrics.NewStore()
	_, err = New(ctx, &wg, store)
	if err != nil {
		t.Errorf("unexpected error:%s", err)
	}
	cancel()
	wg.Wait()
	ctx, cancel = context.WithCancel(context.Background())
	failopt := func(*Exporter) error {
		return errors.New("busted") // nolint:goerr113
	}
	_, err = New(ctx, &wg, store, failopt)
	if err == nil {
		t.Errorf("unexpected success")
	}
	cancel()
	wg.Wait()
}

func FakeSocketWrite(f formatter, m *metrics.Metric) []string {
	ret := make([]string, 0)
	lc := make(chan *metrics.LabelSet)
	d := 60 * time.Second
	go m.EmitLabelSets(lc)
	for l := range lc {
		ret = append(ret, f("gunstar", m, l, d))
	}
	sort.Strings(ret)
	return ret
}

func TestMetricToCollectd(t *testing.T) {
	*collectdPrefix = ""
	ts, terr := time.Parse("2006/01/02 15:04:05", "2012/07/24 10:14:00")
	if terr != nil {
		t.Errorf("time parse error: %s", terr)
	}
	ms := metrics.NewStore()

	scalarMetric := metrics.NewMetric("foo", "prog", metrics.Counter, metrics.Int)
	d, _ := scalarMetric.GetDatum()
	datum.SetInt(d, 37, ts)
	testutil.FatalIfErr(t, ms.Add(scalarMetric))

	r := FakeSocketWrite(metricToCollectd, scalarMetric)
	expected := []string{"PUTVAL \"gunstar/mtail-prog/counter-foo\" interval=60 1343124840:37\n"}
	testutil.ExpectNoDiff(t, expected, r)

	dimensionedMetric := metrics.NewMetric("bar", "prog", metrics.Gauge, metrics.Int, "label")
	d, _ = dimensionedMetric.GetDatum("quux")
	datum.SetInt(d, 37, ts)
	d, _ = dimensionedMetric.GetDatum("snuh")
	datum.SetInt(d, 37, ts)
	ms.ClearMetrics()
	testutil.FatalIfErr(t, ms.Add(dimensionedMetric))

	r = FakeSocketWrite(metricToCollectd, dimensionedMetric)
	expected = []string{
		"PUTVAL \"gunstar/mtail-prog/gauge-bar-label-quux\" interval=60 1343124840:37\n",
		"PUTVAL \"gunstar/mtail-prog/gauge-bar-label-snuh\" interval=60 1343124840:37\n",
	}
	testutil.ExpectNoDiff(t, expected, r)

	timingMetric := metrics.NewMetric("foo", "prog", metrics.Timer, metrics.Int)
	d, _ = timingMetric.GetDatum()
	datum.SetInt(d, 123, ts)
	testutil.FatalIfErr(t, ms.Add(timingMetric))

	r = FakeSocketWrite(metricToCollectd, timingMetric)
	expected = []string{"PUTVAL \"gunstar/mtail-prog/gauge-foo\" interval=60 1343124840:123\n"}
	testutil.ExpectNoDiff(t, expected, r)

	*collectdPrefix = prefix
	r = FakeSocketWrite(metricToCollectd, timingMetric)
	expected = []string{"PUTVAL \"gunstar/prefixmtail-prog/gauge-foo\" interval=60 1343124840:123\n"}
	testutil.ExpectNoDiff(t, expected, r)
}

func TestMetricToGraphite(t *testing.T) {
	*graphitePrefix = ""
	ts, terr := time.Parse("2006/01/02 15:04:05", "2012/07/24 10:14:00")
	if terr != nil {
		t.Errorf("time parse error: %s", terr)
	}

	scalarMetric := metrics.NewMetric("foo", "prog", metrics.Counter, metrics.Int)
	d, _ := scalarMetric.GetDatum()
	datum.SetInt(d, 37, ts)
	r := FakeSocketWrite(metricToGraphite, scalarMetric)
	expected := []string{"prog.foo 37 1343124840\n"}
	testutil.ExpectNoDiff(t, expected, r)

	dimensionedMetric := metrics.NewMetric("bar", "prog", metrics.Gauge, metrics.Int, "host")
	d, _ = dimensionedMetric.GetDatum("quux.com")
	datum.SetInt(d, 37, ts)
	d, _ = dimensionedMetric.GetDatum("snuh.teevee")
	datum.SetInt(d, 37, ts)
	r = FakeSocketWrite(metricToGraphite, dimensionedMetric)
	expected = []string{
		"prog.bar.host.quux_com 37 1343124840\n",
		"prog.bar.host.snuh_teevee 37 1343124840\n",
	}
	testutil.ExpectNoDiff(t, expected, r)

	histogramMetric := metrics.NewMetric("hist", "prog", metrics.Histogram, metrics.Buckets, "xxx")
	lv := &metrics.LabelValue{Labels: []string{"bar"}, Value: datum.MakeBuckets([]datum.Range{{0, 10}, {10, 20}}, time.Unix(0, 0))}
	histogramMetric.AppendLabelValue(lv)
	d, _ = histogramMetric.GetDatum("bar")
	datum.SetFloat(d, 1, ts)
	datum.SetFloat(d, 5, ts)
	datum.SetFloat(d, 15, ts)
	datum.SetFloat(d, 12, ts)
	datum.SetFloat(d, 19, ts)
	datum.SetFloat(d, 1000, ts)
	r = FakeSocketWrite(metricToGraphite, histogramMetric)
	r = strings.Split(strings.TrimSuffix(r[0], "\n"), "\n")
	sort.Strings(r)
	expected = []string{
		"prog.hist.xxx.bar 1052 1343124840",
		"prog.hist.xxx.bar.bin_10 2 1343124840",
		"prog.hist.xxx.bar.bin_20 3 1343124840",
		"prog.hist.xxx.bar.bin_inf 1 1343124840",
		"prog.hist.xxx.bar.count 6 1343124840",
	}
	testutil.ExpectNoDiff(t, expected, r)

	*graphitePrefix = prefix
	r = FakeSocketWrite(metricToGraphite, dimensionedMetric)
	expected = []string{
		"prefixprog.bar.host.quux_com 37 1343124840\n",
		"prefixprog.bar.host.snuh_teevee 37 1343124840\n",
	}
	testutil.ExpectNoDiff(t, expected, r)
}

func TestMetricToStatsd(t *testing.T) {
	*statsdPrefix = ""
	ts, terr := time.Parse("2006/01/02 15:04:05", "2012/07/24 10:14:00")
	if terr != nil {
		t.Errorf("time parse error: %s", terr)
	}

	scalarMetric := metrics.NewMetric("foo", "prog", metrics.Counter, metrics.Int)
	d, _ := scalarMetric.GetDatum()
	datum.SetInt(d, 37, ts)
	r := FakeSocketWrite(metricToStatsd, scalarMetric)
	expected := []string{"prog.foo:37|c"}
	if !reflect.DeepEqual(expected, r) {
		t.Errorf("String didn't match:\n\texpected: %v\n\treceived: %v", expected, r)
	}

	dimensionedMetric := metrics.NewMetric("bar", "prog", metrics.Gauge, metrics.Int, "l")
	d, _ = dimensionedMetric.GetDatum("quux")
	datum.SetInt(d, 37, ts)
	d, _ = dimensionedMetric.GetDatum("snuh")
	datum.SetInt(d, 42, ts)
	r = FakeSocketWrite(metricToStatsd, dimensionedMetric)
	expected = []string{
		"prog.bar.l.quux:37|g",
		"prog.bar.l.snuh:42|g",
	}
	if !reflect.DeepEqual(expected, r) {
		t.Errorf("String didn't match:\n\texpected: %v\n\treceived: %v", expected, r)
	}

	multiLabelMetric := metrics.NewMetric("bar", "prog", metrics.Gauge, metrics.Int, "c", "a", "b")
	d, _ = multiLabelMetric.GetDatum("x", "z", "y")
	datum.SetInt(d, 37, ts)
	r = FakeSocketWrite(metricToStatsd, multiLabelMetric)
	expected = []string{"prog.bar.a.z.b.y.c.x:37|g"}
	if !reflect.DeepEqual(expected, r) {
		t.Errorf("String didn't match:\n\texpected: %v\n\treceived: %v", expected, r)
	}

	timingMetric := metrics.NewMetric("foo", "prog", metrics.Timer, metrics.Int)
	d, _ = timingMetric.GetDatum()
	datum.SetInt(d, 37, ts)
	r = FakeSocketWrite(metricToStatsd, timingMetric)
	expected = []string{"prog.foo:37|ms"}
	if !reflect.DeepEqual(expected, r) {
		t.Errorf("String didn't match:\n\texpected: %v\n\treceived: %v", expected, r)
	}

	*statsdPrefix = prefix
	r = FakeSocketWrite(metricToStatsd, timingMetric)
	expected = []string{"prefixprog.foo:37|ms"}
	if !reflect.DeepEqual(expected, r) {
		t.Errorf("prefixed string didn't match:\n\texpected: %v\n\treceived: %v", expected, r)
	}
}
<|endoftext|>


<commit_before>package lz4block

import (
	"bytes"
	"encoding/base64"
	"strings"
	"testing"
)

func unbase64(in string) []byte {
	p, err := base64.StdEncoding.DecodeString(in)
	if err != nil {
		panic(err)
	}
	return p
}

func TestBlockDecode(t *testing.T) {
	appendLen := func(p []byte, size int) []byte {
		for size > 0xFF {
			p = append(p, 0xFF)
			size -= 0xFF
		}

		p = append(p, byte(size))
		return p
	}

	emitSeq := func(lit string, offset uint16, matchLen int) []byte {
		var b byte
		litLen := len(lit)
		if litLen < 15 {
			b = byte(litLen << 4)
			litLen = -1
		} else {
			b = 0xF0
			litLen -= 15
		}

		if matchLen < 4 || offset == 0 {
			out := []byte{b}
			if litLen >= 0 {
				out = appendLen(out, litLen)
			}
			return append(out, lit...)
		}

		matchLen -= 4
		if matchLen < 15 {
			b |= byte(matchLen)
			matchLen = -1
		} else {
			b |= 0x0F
			matchLen -= 15
		}

		out := []byte{b}
		if litLen >= 0 {
			out = appendLen(out, litLen)
		}

		if len(lit) > 0 {
			out = append(out, lit...)
		}

		out = append(out, byte(offset), byte(offset>>8))

		if matchLen >= 0 {
			out = appendLen(out, matchLen)
		}

		return out
	}
	concat := func(in ...[]byte) []byte {
		var p []byte
		for _, b := range in {
			p = append(p, b...)
		}
		return p
	}

	tests := []struct {
		name string
		src  []byte
		exp  []byte
	}{
		{
			"empty_input",
			[]byte{0},
			[]byte{},
		},
		{
			"literal_only_short",
			emitSeq("hello", 0, 0),
			[]byte("hello"),
		},
		{
			"literal_only_long",
			emitSeq(strings.Repeat("A", 15+255+255+1), 0, 0),
			bytes.Repeat([]byte("A"), 15+255+255+1),
		},
		{
			"literal_only_long_1",
			emitSeq(strings.Repeat("A", 15), 0, 0),
			bytes.Repeat([]byte("A"), 15),
		},
		{
			"repeat_match_len",
			emitSeq("a", 1, 4),
			[]byte("aaaaa"),
		},
		{
			"repeat_match_len_2_seq",
			concat(emitSeq("a", 1, 4), emitSeq("B", 1, 4)),
			[]byte("aaaaaBBBBB"),
		},
		{
			"long_match",
			emitSeq("A", 1, 16),
			bytes.Repeat([]byte("A"), 17),
		},
		{
			"repeat_match_log_len_2_seq",
			concat(emitSeq("a", 1, 15), emitSeq("B", 1, 15), emitSeq("end", 0, 0)),
			[]byte(strings.Repeat("a", 16) + strings.Repeat("B", 16) + "end"),
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			buf := make([]byte, len(test.exp))
			n := decodeBlock(buf, test.src)
			if n <= 0 {
				t.Log(-n)
			}

			if !bytes.Equal(buf, test.exp) {
				t.Fatalf("expected %q got %q", test.exp, buf)
			}
		})
	}
}

func TestDecodeBlockInvalid(t *testing.T) {
	t.Parallel()

	dst := make([]byte, 100)

	for _, test := range []struct {
		name string
		src  string
	}{
		{
			"empty_input",
			"",
		},
		{
			"final_lit_too_short",
			"\x20a", // litlen = 2 but only a single-byte literal
		},
	} {
		r := decodeBlock(dst, []byte(test.src))
		if r >= 0 {
			t.Errorf("no error for %s", test.name)
		}
	}
}
<commit_msg>lz4block: Remove unused test code<commit_after>package lz4block

import (
	"bytes"
	"strings"
	"testing"
)

func TestBlockDecode(t *testing.T) {
	appendLen := func(p []byte, size int) []byte {
		for size > 0xFF {
			p = append(p, 0xFF)
			size -= 0xFF
		}

		p = append(p, byte(size))
		return p
	}

	emitSeq := func(lit string, offset uint16, matchLen int) []byte {
		var b byte
		litLen := len(lit)
		if litLen < 15 {
			b = byte(litLen << 4)
			litLen = -1
		} else {
			b = 0xF0
			litLen -= 15
		}

		if matchLen < 4 || offset == 0 {
			out := []byte{b}
			if litLen >= 0 {
				out = appendLen(out, litLen)
			}
			return append(out, lit...)
		}

		matchLen -= 4
		if matchLen < 15 {
			b |= byte(matchLen)
			matchLen = -1
		} else {
			b |= 0x0F
			matchLen -= 15
		}

		out := []byte{b}
		if litLen >= 0 {
			out = appendLen(out, litLen)
		}

		if len(lit) > 0 {
			out = append(out, lit...)
		}

		out = append(out, byte(offset), byte(offset>>8))

		if matchLen >= 0 {
			out = appendLen(out, matchLen)
		}

		return out
	}
	concat := func(in ...[]byte) []byte {
		var p []byte
		for _, b := range in {
			p = append(p, b...)
		}
		return p
	}

	tests := []struct {
		name string
		src  []byte
		exp  []byte
	}{
		{
			"empty_input",
			[]byte{0},
			[]byte{},
		},
		{
			"literal_only_short",
			emitSeq("hello", 0, 0),
			[]byte("hello"),
		},
		{
			"literal_only_long",
			emitSeq(strings.Repeat("A", 15+255+255+1), 0, 0),
			bytes.Repeat([]byte("A"), 15+255+255+1),
		},
		{
			"literal_only_long_1",
			emitSeq(strings.Repeat("A", 15), 0, 0),
			bytes.Repeat([]byte("A"), 15),
		},
		{
			"repeat_match_len",
			emitSeq("a", 1, 4),
			[]byte("aaaaa"),
		},
		{
			"repeat_match_len_2_seq",
			concat(emitSeq("a", 1, 4), emitSeq("B", 1, 4)),
			[]byte("aaaaaBBBBB"),
		},
		{
			"long_match",
			emitSeq("A", 1, 16),
			bytes.Repeat([]byte("A"), 17),
		},
		{
			"repeat_match_log_len_2_seq",
			concat(emitSeq("a", 1, 15), emitSeq("B", 1, 15), emitSeq("end", 0, 0)),
			[]byte(strings.Repeat("a", 16) + strings.Repeat("B", 16) + "end"),
		},
	}

	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			buf := make([]byte, len(test.exp))
			n := decodeBlock(buf, test.src)
			if n <= 0 {
				t.Log(-n)
			}

			if !bytes.Equal(buf, test.exp) {
				t.Fatalf("expected %q got %q", test.exp, buf)
			}
		})
	}
}

func TestDecodeBlockInvalid(t *testing.T) {
	t.Parallel()

	dst := make([]byte, 100)

	for _, test := range []struct {
		name string
		src  string
	}{
		{
			"empty_input",
			"",
		},
		{
			"final_lit_too_short",
			"\x20a", // litlen = 2 but only a single-byte literal
		},
	} {
		r := decodeBlock(dst, []byte(test.src))
		if r >= 0 {
			t.Errorf("no error for %s", test.name)
		}
	}
}
<|endoftext|>


<commit_before>package http

import (
	"encoding/json"
	"fmt"
	"strings"
	"time"

	"github.com/gorilla/mux"
	braintree "github.com/lionelbarrow/braintree-go"

	"github.com/harrowio/harrow/activities"
	"github.com/harrowio/harrow/config"
	"github.com/harrowio/harrow/domain"
	"github.com/harrowio/harrow/stores"
)

var (
	generateBraintreeToken = braintreeClientToken
	findBraintreeCustomer  = braintreeFindCustomer
	// cancelBraintreeSubscription = braintreeCancelSubscription
	// createBraintreeSubscription = braintreeCreateSubscription
	createBraintreeCustomer = braintreeCreateCustomer
	braintreeConfig         *config.BraintreeConfig
)

const (
	ErrorMessageFromBraintreeAboutCanceledSubscription = "Subscription has already been canceled."
)

func init() {
	braintreeConfig = config.GetConfig().Braintree()
}

func braintreeFindCustomer(id string) (*braintree.Customer, error) {
	client := braintreeConfig.NewClient()
	return client.Customer().Find(id)
}

func braintreeCreateCreditCard(user *domain.User, nonce string) (*braintree.CreditCard, error) {
	client := braintreeConfig.NewClient()

	creditCard := &braintree.CreditCard{
		CustomerId:         user.Uuid,
		PaymentMethodNonce: nonce,
		Options: &braintree.CreditCardOptions{
			VerifyCard:  true,
			MakeDefault: true,
		},
	}

	return client.CreditCard().Create(creditCard)
}

func braintreeCreateCustomer(user *domain.User) (*braintree.Customer, error) {
	client := braintreeConfig.NewClient()

	firstName, lastName := "", ""
	nameFields := strings.Split(user.Name, " ")
	if len(nameFields) > 1 {
		firstName = nameFields[0]
		lastName = strings.Join(nameFields[1:], " ")
	}
	customer := &braintree.Customer{
		Id:        user.Uuid,
		FirstName: firstName,
		LastName:  lastName,
	}

	return client.Customer().Create(customer)
}

func braintreeClientToken() (string, error) {
	return braintreeConfig.NewClient().ClientToken().Generate()
}

type BraintreePurchaseRequest struct {
	PlanUuid         string `json:"planUuid"`
	OrganizationUuid string `json:"organizationUuid"`
}

type ClientTokenResponse struct {
	ClientToken string `json:"clientToken"`
	Error       string `json:"error,omitempty"`
}

type billingPlanHandler struct{}

func MountBillingPlanHandler(r *mux.Router, ctxt ServerContext) {

	h := &billingPlanHandler{}

	// Collection
	root := r.PathPrefix("/billing-plans").Subrouter()
	root.PathPrefix("/{uuid}").Methods("GET").Handler(
		HandlerFunc(ctxt, h.Show),
	).Name(
		"billing-plan-show",
	)
	root.PathPrefix("/braintree/webhooks").Methods("POST").Handler(
		HandlerFunc(ctxt, h.BraintreeHandleNotification),
	).Name(
		"billing-plan-braintree-handle-notification",
	)
	root.PathPrefix("/braintree/client-token").Methods("POST").Handler(
		HandlerFunc(ctxt, h.NewBraintreeClientToken),
	).Name(
		"billing-plan-braintree-client-token",
	)
	root.PathPrefix("/braintree/credit-cards").Methods("POST").Handler(
		HandlerFunc(ctxt, h.BraintreeCreditCards),
	).Name(
		"billing-plan-braintree-add-credit-card",
	)
	root.PathPrefix("/braintree/purchase").Methods("POST").Handler(
		HandlerFunc(ctxt, h.BraintreePurchase),
	).Name(
		"billing-plan-braintree-purchase",
	)

	root.Methods("GET").Handler(HandlerFunc(ctxt, h.List)).
		Name("billing-plan-list")

}

func (self *billingPlanHandler) List(ctxt RequestContext) error {
	billingPlanStore := stores.NewDbBillingPlanStore(ctxt.Tx())
	plans, err := billingPlanStore.FindAll()
	if err != nil {
		return err
	}

	interfacePlans := []interface{}{}
	for _, plan := range plans {
		interfacePlans = append(interfacePlans, plan)
	}

	writeCollectionPageAsJson(ctxt, &CollectionPage{
		Total:      len(interfacePlans),
		Count:      len(interfacePlans),
		Collection: interfacePlans,
	})

	return nil
}

func (self *billingPlanHandler) NewBraintreeClientToken(ctxt RequestContext) error {

	token, _ := generateBraintreeToken()

	json.NewEncoder(ctxt.W()).Encode(&ClientTokenResponse{
		ClientToken: token,
	})

	return nil
}

func (self *billingPlanHandler) BraintreePurchase(ctxt RequestContext) error {

	billingEvents := stores.NewDbBillingEventStore(ctxt.Tx())

	params := &BraintreePurchaseRequest{}
	if err := json.NewDecoder(ctxt.R().Body).Decode(&params); err != nil {
		return err
	}

	if params.PlanUuid == "" {
		return domain.NewValidationError("planUuid", "required")
	}

	if params.OrganizationUuid == "" {
		return domain.NewValidationError("organizationUuid", "required")
	}

	plans := stores.NewDbBillingPlanStore(ctxt.Tx())
	plan, err := plans.FindByUuid(params.PlanUuid)
	if err != nil {
		return err
	}

	organizations := stores.NewDbOrganizationStore(ctxt.Tx())
	organization, err := organizations.FindByUuid(params.OrganizationUuid)
	if err != nil {
		switch err.(type) {
		case *domain.NotFoundError:
			return domain.NewValidationError("organizationUuid", "not_found")
		default:
			return err
		}
	}

	history, err := stores.NewDbBillingHistoryStore(ctxt.Tx(), ctxt.KeyValueStore()).Load()
	if err != nil {
		ctxt.Log().Warn().Msgf("failed to load billing history: %s\n", err)
		return err
	}

	existingSubscription := history.SubscriptionFor(organization.Uuid)
	ctxt.Log().Info().Msgf("existingSubscription(%q):\n%s\n",
		organization.Uuid,
		(func() []byte { data, _ := json.Marshal(existingSubscription); return data })(),
	)

	customer, err := findBraintreeCustomer(ctxt.User().Uuid)
	if err != nil {
		customer, err = createBraintreeCustomer(ctxt.User())
		if err != nil {
			return err
		}
	}

	creditCard := customer.DefaultCreditCard()
	if creditCard == nil {
		return domain.NewValidationError("credit-card", "no_default_set")
	}

	event := &domain.BillingPlanSelected{}
	event.FillFromPlan(plan)
	event.UserUuid = ctxt.User().Uuid

	if plan.Uuid == domain.FreePlanUuid {
		event.SubscriptionId = fmt.Sprintf("%s-free-%s",
			organization.Uuid,
			time.Now().Format(time.RFC3339),
		)
	}

	if _, err := billingEvents.Create(organization.NewBillingEvent(event)); err != nil {
		return err
	}

	ctxt.EnqueueActivity(activities.BillingPlanPurchasedWithBraintree(plan), nil)

	return nil
}

func BraintreeNotificationToBillingEventData(notification *braintree.WebhookNotification) (*domain.BillingPlanSubscriptionChanged, error) {
	if notification.Subject.Subscription == nil {
		return nil, nil
	}

	return &domain.BillingPlanSubscriptionChanged{
		PlanId:         notification.Subject.Subscription.PlanId,
		SubscriptionId: notification.Subject.Subscription.Id,
		Status:         strings.ToLower(string(notification.Subject.Subscription.Status)),
	}, nil
}

func (self *billingPlanHandler) BraintreeHandleNotification(ctxt RequestContext) error {

	signature := ctxt.R().FormValue("bt_signature")
	payload := ctxt.R().FormValue("bt_payload")
	webhook, err := braintreeConfig.NewClient().WebhookNotification().Parse(
		signature,
		payload,
	)
	if err != nil {
		return err
	}

	eventData, err := BraintreeNotificationToBillingEventData(webhook)
	if err != nil {
		return err
	}

	if eventData == nil {
		return nil
	}

	history, err := stores.NewDbBillingHistoryStore(ctxt.Tx(), ctxt.KeyValueStore()).Load()
	if err != nil {
		ctxt.Log().Warn().Msgf("failed to load billing history: %s\n", err)
		return err
	}

	subscription := history.Subscription(eventData.SubscriptionId)
	if subscription != nil {
		eventData.UserUuid = subscription.UserUuid
		event := domain.NewBillingEvent(subscription.OrganizationUuid, eventData)
		if _, err := stores.NewDbBillingEventStore(ctxt.Tx()).Create(event); err != nil {
			ctxt.Log().Warn().Msgf("failed to created billing event: %s\n%#v\n", err, event)
			return err
		}
	} else {
		ctxt.Log().Warn().Msgf("subscription %q not found\n", eventData.SubscriptionId)
	}

	return nil
}

func (self *billingPlanHandler) Show(ctxt RequestContext) error {

	planUuid := ctxt.PathParameter("uuid")
	billingPlanStore := stores.NewDbBillingPlanStore(ctxt.Tx())
	plan, err := billingPlanStore.FindByUuid(planUuid)
	if err != nil {
		return err
	}

	writeAsJson(ctxt, plan)
	return nil

}

type AddCreditCardRequest struct {
	Nonce            string `json:"nonce"`
	OrganizationUuid string `json:"organizationUuid"`
}

func (self *billingPlanHandler) BraintreeCreditCards(ctxt RequestContext) error {

	if ctxt.User() == nil {
		return ErrLoginRequired
	}

	params := &AddCreditCardRequest{}
	if err := json.NewDecoder(ctxt.R().Body).Decode(&params); err != nil {
		return err
	}

	if params.Nonce == "" {
		return domain.NewValidationError("nonce", "required")
	}

	if params.OrganizationUuid == "" {
		return domain.NewValidationError("organizationUuid", "required")
	}

	_, err := findBraintreeCustomer(ctxt.User().Uuid)
	if err != nil {
		_, err = createBraintreeCustomer(ctxt.User())
		if err != nil {
			return err
		}
	}

	creditCard, err := braintreeCreateCreditCard(ctxt.User(), params.Nonce)
	if err == nil {
		billingEvents := stores.NewDbBillingEventStore(ctxt.Tx())
		creditCardAdded := &domain.BillingEvent{
			EventName:        "credit-card-added",
			OrganizationUuid: params.OrganizationUuid,
			Data: &domain.BillingCreditCardAdded{
				CreditCard: &domain.CreditCard{
					CardId:         creditCard.UniqueNumberIdentifier,
					IsDefault:      true,
					CardholderName: creditCard.CardholderName,
					SafeCardNumber: creditCard.Last4,
					Token:          creditCard.Token,
				},
				UserUuid:            ctxt.User().Uuid,
				PaymentProviderName: "braintree",
			},
		}

		if _, err := billingEvents.Create(creditCardAdded); err != nil {
			return err
		}

		return nil
	}

	if berr, ok := err.(*braintree.BraintreeError); ok {
		return NewError(StatusUnprocessableEntity, "invalid_credit_card", berr.ErrorMessage)
	} else {
		return err
	}
}
<commit_msg>Sure, pointers to booleans. Makes total sense.<commit_after>package http

import (
	"encoding/json"
	"fmt"
	"strings"
	"time"

	"github.com/gorilla/mux"
	braintree "github.com/lionelbarrow/braintree-go"

	"github.com/harrowio/harrow/activities"
	"github.com/harrowio/harrow/config"
	"github.com/harrowio/harrow/domain"
	"github.com/harrowio/harrow/stores"
)

var (
	generateBraintreeToken = braintreeClientToken
	findBraintreeCustomer  = braintreeFindCustomer
	// cancelBraintreeSubscription = braintreeCancelSubscription
	// createBraintreeSubscription = braintreeCreateSubscription
	createBraintreeCustomer = braintreeCreateCustomer
	braintreeConfig         *config.BraintreeConfig
)

const (
	ErrorMessageFromBraintreeAboutCanceledSubscription = "Subscription has already been canceled."
)

func init() {
	braintreeConfig = config.GetConfig().Braintree()
}

func braintreeFindCustomer(id string) (*braintree.Customer, error) {
	client := braintreeConfig.NewClient()
	return client.Customer().Find(id)
}

func braintreeCreateCreditCard(user *domain.User, nonce string) (*braintree.CreditCard, error) {
	client := braintreeConfig.NewClient()

	t := &[]bool{true}[0]

	creditCard := &braintree.CreditCard{
		CustomerId:         user.Uuid,
		PaymentMethodNonce: nonce,
		Options: &braintree.CreditCardOptions{
			VerifyCard:  t,
			MakeDefault: true,
		},
	}

	return client.CreditCard().Create(creditCard)
}

func braintreeCreateCustomer(user *domain.User) (*braintree.Customer, error) {
	client := braintreeConfig.NewClient()

	firstName, lastName := "", ""
	nameFields := strings.Split(user.Name, " ")
	if len(nameFields) > 1 {
		firstName = nameFields[0]
		lastName = strings.Join(nameFields[1:], " ")
	}
	customer := &braintree.Customer{
		Id:        user.Uuid,
		FirstName: firstName,
		LastName:  lastName,
	}

	return client.Customer().Create(customer)
}

func braintreeClientToken() (string, error) {
	return braintreeConfig.NewClient().ClientToken().Generate()
}

type BraintreePurchaseRequest struct {
	PlanUuid         string `json:"planUuid"`
	OrganizationUuid string `json:"organizationUuid"`
}

type ClientTokenResponse struct {
	ClientToken string `json:"clientToken"`
	Error       string `json:"error,omitempty"`
}

type billingPlanHandler struct{}

func MountBillingPlanHandler(r *mux.Router, ctxt ServerContext) {

	h := &billingPlanHandler{}

	// Collection
	root := r.PathPrefix("/billing-plans").Subrouter()
	root.PathPrefix("/{uuid}").Methods("GET").Handler(
		HandlerFunc(ctxt, h.Show),
	).Name(
		"billing-plan-show",
	)
	root.PathPrefix("/braintree/webhooks").Methods("POST").Handler(
		HandlerFunc(ctxt, h.BraintreeHandleNotification),
	).Name(
		"billing-plan-braintree-handle-notification",
	)
	root.PathPrefix("/braintree/client-token").Methods("POST").Handler(
		HandlerFunc(ctxt, h.NewBraintreeClientToken),
	).Name(
		"billing-plan-braintree-client-token",
	)
	root.PathPrefix("/braintree/credit-cards").Methods("POST").Handler(
		HandlerFunc(ctxt, h.BraintreeCreditCards),
	).Name(
		"billing-plan-braintree-add-credit-card",
	)
	root.PathPrefix("/braintree/purchase").Methods("POST").Handler(
		HandlerFunc(ctxt, h.BraintreePurchase),
	).Name(
		"billing-plan-braintree-purchase",
	)

	root.Methods("GET").Handler(HandlerFunc(ctxt, h.List)).
		Name("billing-plan-list")

}

func (self *billingPlanHandler) List(ctxt RequestContext) error {
	billingPlanStore := stores.NewDbBillingPlanStore(ctxt.Tx())
	plans, err := billingPlanStore.FindAll()
	if err != nil {
		return err
	}

	interfacePlans := []interface{}{}
	for _, plan := range plans {
		interfacePlans = append(interfacePlans, plan)
	}

	writeCollectionPageAsJson(ctxt, &CollectionPage{
		Total:      len(interfacePlans),
		Count:      len(interfacePlans),
		Collection: interfacePlans,
	})

	return nil
}

func (self *billingPlanHandler) NewBraintreeClientToken(ctxt RequestContext) error {

	token, _ := generateBraintreeToken()

	json.NewEncoder(ctxt.W()).Encode(&ClientTokenResponse{
		ClientToken: token,
	})

	return nil
}

func (self *billingPlanHandler) BraintreePurchase(ctxt RequestContext) error {

	billingEvents := stores.NewDbBillingEventStore(ctxt.Tx())

	params := &BraintreePurchaseRequest{}
	if err := json.NewDecoder(ctxt.R().Body).Decode(&params); err != nil {
		return err
	}

	if params.PlanUuid == "" {
		return domain.NewValidationError("planUuid", "required")
	}

	if params.OrganizationUuid == "" {
		return domain.NewValidationError("organizationUuid", "required")
	}

	plans := stores.NewDbBillingPlanStore(ctxt.Tx())
	plan, err := plans.FindByUuid(params.PlanUuid)
	if err != nil {
		return err
	}

	organizations := stores.NewDbOrganizationStore(ctxt.Tx())
	organization, err := organizations.FindByUuid(params.OrganizationUuid)
	if err != nil {
		switch err.(type) {
		case *domain.NotFoundError:
			return domain.NewValidationError("organizationUuid", "not_found")
		default:
			return err
		}
	}

	history, err := stores.NewDbBillingHistoryStore(ctxt.Tx(), ctxt.KeyValueStore()).Load()
	if err != nil {
		ctxt.Log().Warn().Msgf("failed to load billing history: %s\n", err)
		return err
	}

	existingSubscription := history.SubscriptionFor(organization.Uuid)
	ctxt.Log().Info().Msgf("existingSubscription(%q):\n%s\n",
		organization.Uuid,
		(func() []byte { data, _ := json.Marshal(existingSubscription); return data })(),
	)

	customer, err := findBraintreeCustomer(ctxt.User().Uuid)
	if err != nil {
		customer, err = createBraintreeCustomer(ctxt.User())
		if err != nil {
			return err
		}
	}

	creditCard := customer.DefaultCreditCard()
	if creditCard == nil {
		return domain.NewValidationError("credit-card", "no_default_set")
	}

	event := &domain.BillingPlanSelected{}
	event.FillFromPlan(plan)
	event.UserUuid = ctxt.User().Uuid

	if plan.Uuid == domain.FreePlanUuid {
		event.SubscriptionId = fmt.Sprintf("%s-free-%s",
			organization.Uuid,
			time.Now().Format(time.RFC3339),
		)
	}

	if _, err := billingEvents.Create(organization.NewBillingEvent(event)); err != nil {
		return err
	}

	ctxt.EnqueueActivity(activities.BillingPlanPurchasedWithBraintree(plan), nil)

	return nil
}

func BraintreeNotificationToBillingEventData(notification *braintree.WebhookNotification) (*domain.BillingPlanSubscriptionChanged, error) {
	if notification.Subject.Subscription == nil {
		return nil, nil
	}

	return &domain.BillingPlanSubscriptionChanged{
		PlanId:         notification.Subject.Subscription.PlanId,
		SubscriptionId: notification.Subject.Subscription.Id,
		Status:         strings.ToLower(string(notification.Subject.Subscription.Status)),
	}, nil
}

func (self *billingPlanHandler) BraintreeHandleNotification(ctxt RequestContext) error {

	signature := ctxt.R().FormValue("bt_signature")
	payload := ctxt.R().FormValue("bt_payload")
	webhook, err := braintreeConfig.NewClient().WebhookNotification().Parse(
		signature,
		payload,
	)
	if err != nil {
		return err
	}

	eventData, err := BraintreeNotificationToBillingEventData(webhook)
	if err != nil {
		return err
	}

	if eventData == nil {
		return nil
	}

	history, err := stores.NewDbBillingHistoryStore(ctxt.Tx(), ctxt.KeyValueStore()).Load()
	if err != nil {
		ctxt.Log().Warn().Msgf("failed to load billing history: %s\n", err)
		return err
	}

	subscription := history.Subscription(eventData.SubscriptionId)
	if subscription != nil {
		eventData.UserUuid = subscription.UserUuid
		event := domain.NewBillingEvent(subscription.OrganizationUuid, eventData)
		if _, err := stores.NewDbBillingEventStore(ctxt.Tx()).Create(event); err != nil {
			ctxt.Log().Warn().Msgf("failed to created billing event: %s\n%#v\n", err, event)
			return err
		}
	} else {
		ctxt.Log().Warn().Msgf("subscription %q not found\n", eventData.SubscriptionId)
	}

	return nil
}

func (self *billingPlanHandler) Show(ctxt RequestContext) error {

	planUuid := ctxt.PathParameter("uuid")
	billingPlanStore := stores.NewDbBillingPlanStore(ctxt.Tx())
	plan, err := billingPlanStore.FindByUuid(planUuid)
	if err != nil {
		return err
	}

	writeAsJson(ctxt, plan)
	return nil

}

type AddCreditCardRequest struct {
	Nonce            string `json:"nonce"`
	OrganizationUuid string `json:"organizationUuid"`
}

func (self *billingPlanHandler) BraintreeCreditCards(ctxt RequestContext) error {

	if ctxt.User() == nil {
		return ErrLoginRequired
	}

	params := &AddCreditCardRequest{}
	if err := json.NewDecoder(ctxt.R().Body).Decode(&params); err != nil {
		return err
	}

	if params.Nonce == "" {
		return domain.NewValidationError("nonce", "required")
	}

	if params.OrganizationUuid == "" {
		return domain.NewValidationError("organizationUuid", "required")
	}

	_, err := findBraintreeCustomer(ctxt.User().Uuid)
	if err != nil {
		_, err = createBraintreeCustomer(ctxt.User())
		if err != nil {
			return err
		}
	}

	creditCard, err := braintreeCreateCreditCard(ctxt.User(), params.Nonce)
	if err == nil {
		billingEvents := stores.NewDbBillingEventStore(ctxt.Tx())
		creditCardAdded := &domain.BillingEvent{
			EventName:        "credit-card-added",
			OrganizationUuid: params.OrganizationUuid,
			Data: &domain.BillingCreditCardAdded{
				CreditCard: &domain.CreditCard{
					CardId:         creditCard.UniqueNumberIdentifier,
					IsDefault:      true,
					CardholderName: creditCard.CardholderName,
					SafeCardNumber: creditCard.Last4,
					Token:          creditCard.Token,
				},
				UserUuid:            ctxt.User().Uuid,
				PaymentProviderName: "braintree",
			},
		}

		if _, err := billingEvents.Create(creditCardAdded); err != nil {
			return err
		}

		return nil
	}

	if berr, ok := err.(*braintree.BraintreeError); ok {
		return NewError(StatusUnprocessableEntity, "invalid_credit_card", berr.ErrorMessage)
	} else {
		return err
	}
}
<|endoftext|>