<commit_before>package fwk

import (
	"fmt"
	"os"
	"reflect"
	"sort"
)

var g_mgr *appmgr = nil

type fsm int

const (
	fsm_UNDEFINED  fsm = 0
	fsm_CONFIGURED fsm = 1
	fsm_STARTED    fsm = 2
	fsm_RUNNING    fsm = 3
	fsm_STOPPED    fsm = 4
	fsm_OFFLINE    fsm = 5
)

type appmgr struct {
	state fsm
	name  string

	props map[Component]map[string]interface{}
	dflow *dflowsvc
	store *datastore

	evtmax int64

	comps map[string]Component
	tsks  []Task
	svcs  []Svc
}

func NewApp() App {
	if g_mgr != nil {
		return g_mgr
	}

	var err Error
	var app *appmgr

	app = &appmgr{
		state:  fsm_UNDEFINED,
		name:   "app",
		props:  make(map[Component]map[string]interface{}),
		dflow:  nil,
		store:  nil,
		evtmax: 4,
		comps:  make(map[string]Component),
		tsks:   make([]Task, 0),
		svcs:   make([]Svc, 0),
	}

	svc, err := New("github.com/go-hep/fwk.datastore", "evtstore")
	if err != nil {
		fmt.Printf("**error** fwk.NewApp: could not create evtstore: %v\n", err)
		return nil
	}
	app.store = svc.(*datastore)

	//app.store = newDataStore("evtstore")
	err = app.AddSvc(app.store)
	if err != nil {
		fmt.Printf("**error** fwk.NewApp: could not create evtstore: %v\n", err)
		return nil
	}

	svc, err = New("github.com/go-hep/fwk.dflowsvc", "dataflow")
	if err != nil {
		fmt.Printf("**error** fwk.NewApp: could not create dataflow svc: %v\n", err)
		return nil
	}
	app.dflow = svc.(*dflowsvc)

	//app.dflow = newDflowSvc("dataflow")
	err = app.AddSvc(app.dflow)
	if err != nil {
		fmt.Printf("**error** fwk.NewApp: could not create dataflow svc: %v\n", err)
		return nil
	}

	g_mgr = app
	return app
}

func (app *appmgr) Name() string {
	return app.name
}

func (app *appmgr) SetName(n string) {
	app.name = n
}

func (app *appmgr) Component(n string) Component {
	c, ok := app.comps[n]
	if !ok {
		return nil
	}
	return c
}

func (app *appmgr) addComponent(c Component) Error {
	app.comps[c.Name()] = c
	return nil
}

func (app *appmgr) HasComponent(n string) bool {
	_, ok := app.comps[n]
	return ok
}

func (app *appmgr) Components() []Component {
	comps := make([]Component, 0, len(app.comps))
	for _, c := range app.comps {
		comps = append(comps, c)
	}
	return comps
}

func (app *appmgr) AddTask(tsk Task) Error {
	var err Error
	app.tsks = append(app.tsks, tsk)
	app.comps[tsk.Name()] = tsk
	return err
}

func (app *appmgr) DelTask(tsk Task) Error {
	var err Error
	tsks := make([]Task, 0, len(app.tsks))
	for _, t := range app.tsks {
		if t.Name() != tsk.Name() {
			tsks = append(tsks, t)
		}
	}
	app.tsks = tsks
	return err
}

func (app *appmgr) HasTask(n string) bool {
	for _, t := range app.tsks {
		if t.Name() == n {
			return true
		}
	}
	return false
}

func (app *appmgr) GetTask(n string) Task {
	for _, t := range app.tsks {
		if t.Name() == n {
			return t
		}
	}
	return nil
}

func (app *appmgr) Tasks() []Task {
	return app.tsks
}

func (app *appmgr) AddSvc(svc Svc) Error {
	var err Error
	app.svcs = append(app.svcs, svc)
	app.comps[svc.Name()] = svc
	return err
}

func (app *appmgr) DelSvc(svc Svc) Error {
	var err Error
	svcs := make([]Svc, 0, len(app.svcs))
	for _, s := range app.svcs {
		if s.Name() != svc.Name() {
			svcs = append(svcs, s)
		}
	}
	app.svcs = svcs
	return err
}

func (app *appmgr) HasSvc(n string) bool {
	for _, s := range app.svcs {
		if s.Name() == n {
			return true
		}
	}
	return false
}

func (app *appmgr) GetSvc(n string) Svc {
	for _, s := range app.svcs {
		if s.Name() == n {
			return s
		}
	}
	return nil
}

func (app *appmgr) Svcs() []Svc {
	return app.svcs
}

func (app *appmgr) DeclProp(c Component, name string, ptr interface{}) Error {
	_, ok := app.props[c]
	if !ok {
		app.props[c] = make(map[string]interface{})
	}
	switch reflect.TypeOf(ptr).Kind() {
	case reflect.Ptr:
		// ok
	default:
		return Errorf(
			"fwk.DeclProp: component [%s] didn't pass a pointer for the property [%s] (type=%T)",
			c.Name(),
			name,
			ptr,
		)
	}
	app.props[c][name] = ptr
	return nil
}

func (app *appmgr) SetProp(c Component, name string, value interface{}) Error {
	m, ok := app.props[c]
	if !ok {
		return Errorf(
			"fwk.SetProp: component [%s] didn't declare any property",
			c.Name(),
		)
	}
	rv := reflect.ValueOf(value)
	rt := rv.Type()
	ptr := reflect.ValueOf(m[name])
	dst := ptr.Elem().Type()
	if !rt.AssignableTo(dst) {
		return Errorf(
			"fwk.SetProp: component [%s] has property [%s] with type [%s]. got value=%v (type=%s)",
			c.Name(),
			name,
			dst.Name(),
			value,
			rt.Name(),
		)
	}
	ptr.Elem().Set(rv)
	return nil
}

func (app *appmgr) GetProp(c Component, name string) (interface{}, Error) {
	m, ok := app.props[c]
	if !ok {
		return nil, Errorf(
			"fwk.GetProp: component [%s] didn't declare any property",
			c.Name(),
		)
	}

	ptr, ok := m[name]
	if !ok {
		return nil, Errorf(
			"fwk.GetProp: component [%s] didn't declare any property with name [%s]",
			c.Name(),
			name,
		)
	}

	v := reflect.Indirect(reflect.ValueOf(ptr)).Interface()
	return v, nil
}

func (app *appmgr) Run() Error {
	var err Error
	var ctx Context

	if app.state == fsm_UNDEFINED {
		err = app.configure(ctx)
		if err != nil {
			return err
		}
	}

	if app.state == fsm_CONFIGURED {
		err = app.start(ctx)
		if err != nil {
			return err
		}
	}

	if app.state == fsm_STARTED {
		err = app.run(ctx)
		if err != nil {
			return err
		}
	}

	if app.state == fsm_RUNNING {
		err = app.stop(ctx)
		if err != nil {
			return err
		}
	}

	if app.state == fsm_STOPPED {
		err = app.shutdown(ctx)
		if err != nil {
			return err
		}
	}

	return err
}

func (app *appmgr) configure(ctx Context) Error {
	var err Error
	fmt.Printf(">>> app.configure...\n")
	for _, svc := range app.svcs {
		fmt.Printf(">>> configuring [%s]...\n", svc.Name())
		cfg, ok := svc.(Configurer)
		if !ok {
			continue
		}
		err = cfg.Configure(ctx)
		if err != nil {
			return err
		}
	}

	for _, tsk := range app.tsks {
		fmt.Printf(">>> configuring [%s]...\n", tsk.Name())
		cfg, ok := tsk.(Configurer)
		if !ok {
			continue
		}
		err = cfg.Configure(ctx)
		if err != nil {
			return err
		}
	}

	fmt.Printf(">>> --- [data flow] --- nodes...\n")
	for tsk, node := range app.dflow.nodes {
		fmt.Printf(">>> ---[%s]---\n", tsk)
		fmt.Printf("    in:  %v\n", node.in)
		fmt.Printf("    out: %v\n", node.out)
	}

	fmt.Printf(">>> --- [data flow] --- edges...\n")
	edges := make([]string, 0, len(app.dflow.edges))
	for n := range app.dflow.edges {
		edges = append(edges, n)
	}
	sort.Strings(edges)
	fmt.Printf(" edges: %v\n", edges)

	app.state = fsm_CONFIGURED
	fmt.Printf(">>> app.configure... [done]\n")
	return err
}

func (app *appmgr) start(ctx Context) Error {
	var err Error
	for _, svc := range app.svcs {
		fmt.Printf(">>> starting [%s]...\n", svc.Name())
		err = svc.StartSvc(ctx)
		if err != nil {
			return err
		}
	}

	for _, tsk := range app.tsks {
		fmt.Printf(">>> starting [%s]...\n", tsk.Name())
		err = tsk.StartTask(ctx)
		if err != nil {
			return err
		}
	}

	app.state = fsm_STARTED
	return err
}

func (app *appmgr) run(ctx Context) Error {
	var err Error
	app.state = fsm_RUNNING
	for ievt := int64(0); ievt < app.evtmax; ievt++ {
		fmt.Printf(">>> app.running evt=%d...\n", ievt)
		for k := range app.dflow.edges {
			ch, ok := app.store.store[k]
			if ok {
				select {
				case vv := <-ch:
					if vv, ok := vv.(Deleter); ok {
						err = vv.Delete()
						if err != nil {
							return err
						}
					}
				default:
				}
			}
			app.store.store[k] = make(achan, 1)
		}
		errch := make(chan Error, len(app.tsks))
		for _, tsk := range app.tsks {
			go func(tsk Task) {
				//fmt.Printf(">>> running [%s]...\n", tsk.Name())
				ctx := context{
					id:    ievt,
					slot:  0,
					store: app.store,
					msg: msgstream{
						lvl: LvlInfo,
						w:   os.Stdout,
						n:   tsk.Name(),
					},
				}
				errch <- tsk.Process(ctx)
			}(tsk)
		}
		for i := 0; i < len(app.tsks); i++ {
			err := <-errch
			if err != nil {
				close(errch)
				return err
			}

		}
	}
	return err
}

func (app *appmgr) stop(ctx Context) Error {
	var err Error
	for _, tsk := range app.tsks {
		err = tsk.StopTask(ctx)
		if err != nil {
			return err
		}
	}

	for _, svc := range app.svcs {
		err = svc.StopSvc(ctx)
		if err != nil {
			return err
		}
	}

	app.state = fsm_STOPPED
	return err
}

func (app *appmgr) shutdown(ctx Context) Error {
	var err Error
	app.comps = nil
	app.tsks = nil
	app.svcs = nil
	app.state = fsm_OFFLINE

	app.props = nil
	app.dflow = nil
	app.store = nil

	g_mgr = nil
	return err
}

func init() {
	Register(reflect.TypeOf(appmgr{}))
}

// EOF
<commit_msg>app: use msgstream<commit_after>package fwk

import (
	"os"
	"reflect"
	"sort"
)

var g_mgr *appmgr = nil

type fsm int

const (
	fsm_UNDEFINED  fsm = 0
	fsm_CONFIGURED fsm = 1
	fsm_STARTED    fsm = 2
	fsm_RUNNING    fsm = 3
	fsm_STOPPED    fsm = 4
	fsm_OFFLINE    fsm = 5
)

type appmgr struct {
	state fsm
	name  string

	props map[Component]map[string]interface{}
	dflow *dflowsvc
	store *datastore
	msg   msgstream

	evtmax int64

	comps map[string]Component
	tsks  []Task
	svcs  []Svc
}

func NewApp() App {
	if g_mgr != nil {
		return g_mgr
	}

	var err Error
	var app *appmgr

	const appname = "app"

	app = &appmgr{
		state: fsm_UNDEFINED,
		name:  appname,
		props: make(map[Component]map[string]interface{}),
		dflow: nil,
		store: nil,
		msg: msgstream{
			lvl: LvlInfo,
			w:   os.Stdout,
			n:   appname,
		},
		evtmax: 4,
		comps:  make(map[string]Component),
		tsks:   make([]Task, 0),
		svcs:   make([]Svc, 0),
	}

	svc, err := New("github.com/go-hep/fwk.datastore", "evtstore")
	if err != nil {
		app.msg.Errorf("fwk.NewApp: could not create evtstore: %v\n", err)
		return nil
	}
	app.store = svc.(*datastore)

	//app.store = newDataStore("evtstore")
	err = app.AddSvc(app.store)
	if err != nil {
		app.msg.Errorf("fwk.NewApp: could not create evtstore: %v\n", err)
		return nil
	}

	svc, err = New("github.com/go-hep/fwk.dflowsvc", "dataflow")
	if err != nil {
		app.msg.Errorf("fwk.NewApp: could not create dataflow svc: %v\n", err)
		return nil
	}
	app.dflow = svc.(*dflowsvc)

	//app.dflow = newDflowSvc("dataflow")
	err = app.AddSvc(app.dflow)
	if err != nil {
		app.msg.Errorf("fwk.NewApp: could not create dataflow svc: %v\n", err)
		return nil
	}

	g_mgr = app
	return app
}

func (app *appmgr) Name() string {
	return app.name
}

func (app *appmgr) SetName(n string) {
	app.name = n
}

func (app *appmgr) Component(n string) Component {
	c, ok := app.comps[n]
	if !ok {
		return nil
	}
	return c
}

func (app *appmgr) addComponent(c Component) Error {
	app.comps[c.Name()] = c
	return nil
}

func (app *appmgr) HasComponent(n string) bool {
	_, ok := app.comps[n]
	return ok
}

func (app *appmgr) Components() []Component {
	comps := make([]Component, 0, len(app.comps))
	for _, c := range app.comps {
		comps = append(comps, c)
	}
	return comps
}

func (app *appmgr) AddTask(tsk Task) Error {
	var err Error
	app.tsks = append(app.tsks, tsk)
	app.comps[tsk.Name()] = tsk
	return err
}

func (app *appmgr) DelTask(tsk Task) Error {
	var err Error
	tsks := make([]Task, 0, len(app.tsks))
	for _, t := range app.tsks {
		if t.Name() != tsk.Name() {
			tsks = append(tsks, t)
		}
	}
	app.tsks = tsks
	return err
}

func (app *appmgr) HasTask(n string) bool {
	for _, t := range app.tsks {
		if t.Name() == n {
			return true
		}
	}
	return false
}

func (app *appmgr) GetTask(n string) Task {
	for _, t := range app.tsks {
		if t.Name() == n {
			return t
		}
	}
	return nil
}

func (app *appmgr) Tasks() []Task {
	return app.tsks
}

func (app *appmgr) AddSvc(svc Svc) Error {
	var err Error
	app.svcs = append(app.svcs, svc)
	app.comps[svc.Name()] = svc
	return err
}

func (app *appmgr) DelSvc(svc Svc) Error {
	var err Error
	svcs := make([]Svc, 0, len(app.svcs))
	for _, s := range app.svcs {
		if s.Name() != svc.Name() {
			svcs = append(svcs, s)
		}
	}
	app.svcs = svcs
	return err
}

func (app *appmgr) HasSvc(n string) bool {
	for _, s := range app.svcs {
		if s.Name() == n {
			return true
		}
	}
	return false
}

func (app *appmgr) GetSvc(n string) Svc {
	for _, s := range app.svcs {
		if s.Name() == n {
			return s
		}
	}
	return nil
}

func (app *appmgr) Svcs() []Svc {
	return app.svcs
}

func (app *appmgr) DeclProp(c Component, name string, ptr interface{}) Error {
	_, ok := app.props[c]
	if !ok {
		app.props[c] = make(map[string]interface{})
	}
	switch reflect.TypeOf(ptr).Kind() {
	case reflect.Ptr:
		// ok
	default:
		return Errorf(
			"fwk.DeclProp: component [%s] didn't pass a pointer for the property [%s] (type=%T)",
			c.Name(),
			name,
			ptr,
		)
	}
	app.props[c][name] = ptr
	return nil
}

func (app *appmgr) SetProp(c Component, name string, value interface{}) Error {
	m, ok := app.props[c]
	if !ok {
		return Errorf(
			"fwk.SetProp: component [%s] didn't declare any property",
			c.Name(),
		)
	}
	rv := reflect.ValueOf(value)
	rt := rv.Type()
	ptr := reflect.ValueOf(m[name])
	dst := ptr.Elem().Type()
	if !rt.AssignableTo(dst) {
		return Errorf(
			"fwk.SetProp: component [%s] has property [%s] with type [%s]. got value=%v (type=%s)",
			c.Name(),
			name,
			dst.Name(),
			value,
			rt.Name(),
		)
	}
	ptr.Elem().Set(rv)
	return nil
}

func (app *appmgr) GetProp(c Component, name string) (interface{}, Error) {
	m, ok := app.props[c]
	if !ok {
		return nil, Errorf(
			"fwk.GetProp: component [%s] didn't declare any property",
			c.Name(),
		)
	}

	ptr, ok := m[name]
	if !ok {
		return nil, Errorf(
			"fwk.GetProp: component [%s] didn't declare any property with name [%s]",
			c.Name(),
			name,
		)
	}

	v := reflect.Indirect(reflect.ValueOf(ptr)).Interface()
	return v, nil
}

func (app *appmgr) Run() Error {
	var err Error
	var ctx Context

	if app.state == fsm_UNDEFINED {
		err = app.configure(ctx)
		if err != nil {
			return err
		}
	}

	if app.state == fsm_CONFIGURED {
		err = app.start(ctx)
		if err != nil {
			return err
		}
	}

	if app.state == fsm_STARTED {
		err = app.run(ctx)
		if err != nil {
			return err
		}
	}

	if app.state == fsm_RUNNING {
		err = app.stop(ctx)
		if err != nil {
			return err
		}
	}

	if app.state == fsm_STOPPED {
		err = app.shutdown(ctx)
		if err != nil {
			return err
		}
	}

	return err
}

func (app *appmgr) configure(ctx Context) Error {
	var err Error
	app.msg.Debugf("configure...\n")
	for _, svc := range app.svcs {
		app.msg.Debugf("configuring [%s]...\n", svc.Name())
		cfg, ok := svc.(Configurer)
		if !ok {
			continue
		}
		err = cfg.Configure(ctx)
		if err != nil {
			return err
		}
	}

	for _, tsk := range app.tsks {
		app.msg.Debugf("configuring [%s]...\n", tsk.Name())
		cfg, ok := tsk.(Configurer)
		if !ok {
			continue
		}
		err = cfg.Configure(ctx)
		if err != nil {
			return err
		}
	}

	app.msg.Infof(">>> --- [data flow] --- nodes...\n")
	for tsk, node := range app.dflow.nodes {
		app.msg.Infof(">>> ---[%s]---\n", tsk)
		app.msg.Infof("    in:  %v\n", node.in)
		app.msg.Infof("    out: %v\n", node.out)
	}

	app.msg.Infof(">>> --- [data flow] --- edges...\n")
	edges := make([]string, 0, len(app.dflow.edges))
	for n := range app.dflow.edges {
		edges = append(edges, n)
	}
	sort.Strings(edges)
	app.msg.Infof(" edges: %v\n", edges)

	app.state = fsm_CONFIGURED
	app.msg.Debugf("configure... [done]\n")
	return err
}

func (app *appmgr) start(ctx Context) Error {
	var err Error
	for _, svc := range app.svcs {
		app.msg.Debugf("starting [%s]...\n", svc.Name())
		err = svc.StartSvc(ctx)
		if err != nil {
			return err
		}
	}

	for _, tsk := range app.tsks {
		app.msg.Debugf("starting [%s]...\n", tsk.Name())
		err = tsk.StartTask(ctx)
		if err != nil {
			return err
		}
	}

	app.state = fsm_STARTED
	return err
}

func (app *appmgr) run(ctx Context) Error {
	var err Error
	app.state = fsm_RUNNING
	for ievt := int64(0); ievt < app.evtmax; ievt++ {
		app.msg.Infof(">>> running evt=%d...\n", ievt)
		for k := range app.dflow.edges {
			ch, ok := app.store.store[k]
			if ok {
				select {
				case vv := <-ch:
					if vv, ok := vv.(Deleter); ok {
						err = vv.Delete()
						if err != nil {
							return err
						}
					}
				default:
				}
			}
			app.store.store[k] = make(achan, 1)
		}
		errch := make(chan Error, len(app.tsks))
		for _, tsk := range app.tsks {
			go func(tsk Task) {
				//app.msg.Infof(">>> running [%s]...\n", tsk.Name())
				ctx := context{
					id:    ievt,
					slot:  0,
					store: app.store,
					msg: msgstream{
						lvl: LvlInfo,
						w:   os.Stdout,
						n:   tsk.Name(),
					},
				}
				errch <- tsk.Process(ctx)
			}(tsk)
		}
		for i := 0; i < len(app.tsks); i++ {
			err := <-errch
			if err != nil {
				close(errch)
				return err
			}

		}
	}
	return err
}

func (app *appmgr) stop(ctx Context) Error {
	var err Error
	for _, tsk := range app.tsks {
		err = tsk.StopTask(ctx)
		if err != nil {
			return err
		}
	}

	for _, svc := range app.svcs {
		err = svc.StopSvc(ctx)
		if err != nil {
			return err
		}
	}

	app.state = fsm_STOPPED
	return err
}

func (app *appmgr) shutdown(ctx Context) Error {
	var err Error
	app.comps = nil
	app.tsks = nil
	app.svcs = nil
	app.state = fsm_OFFLINE

	app.props = nil
	app.dflow = nil
	app.store = nil

	g_mgr = nil
	return err
}

func init() {
	Register(reflect.TypeOf(appmgr{}))
}

// EOF
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"net/http"
	"os"
	"strconv"
	"time"

	"github.com/Financial-Times/base-ft-rw-app-go/baseftrwapp"
	"github.com/Financial-Times/go-fthealth/v1a"
	"github.com/Financial-Times/http-handlers-go/httphandlers"
	"github.com/Financial-Times/neo-utils-go/neoutils"
	"github.com/Financial-Times/public-concordances-api/concordances"
	status "github.com/Financial-Times/service-status-go/httphandlers"
	log "github.com/Sirupsen/logrus"
	"github.com/gorilla/handlers"
	"github.com/gorilla/mux"
	"github.com/jawher/mow.cli"
	_ "github.com/joho/godotenv/autoload"
	"github.com/rcrowley/go-metrics"
)

func main() {
	app := cli.App("public-concordances-api-neo4j", "A public RESTful API for accessing concordances in neo4j")
	neoURL := app.String(cli.StringOpt{
		Name:   "neo-url",
		Value:  "http://localhost:7474/db/data",
		Desc:   "neo4j endpoint URL",
		EnvVar: "NEO_URL",
	})
	port := app.String(cli.StringOpt{
		Name:   "port",
		Value:  "8080",
		Desc:   "Port to listen on",
		EnvVar: "APP_PORT",
	})
	graphiteTCPAddress := app.String(cli.StringOpt{
		Name:   "graphiteTCPAddress",
		Value:  "",
		Desc:   "Graphite TCP address, e.g. graphite.ft.com:2003. Leave as default if you do NOT want to output to graphite (e.g. if running locally)",
		EnvVar: "GRAPHITE_ADDRESS",
	})
	graphitePrefix := app.String(cli.StringOpt{
		Name:   "graphitePrefix",
		Value:  "",
		Desc:   "Prefix to use. Should start with content, include the environment, and the host name. e.g. content.test.public.content.by.concept.api.ftaps59382-law1a-eu-t",
		EnvVar: "GRAPHITE_PREFIX",
	})
	logMetrics := app.Bool(cli.BoolOpt{
		Name:   "logMetrics",
		Value:  false,
		Desc:   "Whether to log metrics. Set to true if running locally and you want metrics output",
		EnvVar: "LOG_METRICS",
	})
	env := app.String(cli.StringOpt{
		Name:  "env",
		Value: "local",
		Desc:  "environment this app is running in",
	})
	cacheDuration := app.String(cli.StringOpt{
		Name:   "cache-duration",
		Value:  "30s",
		Desc:   "Duration Get requests should be cached for. e.g. 2h45m would set the max-age value to '7440' seconds",
		EnvVar: "CACHE_DURATION",
	})
	healthcheckInterval := app.String(cli.StringOpt{
		Name:   "healthcheck-interval",
		Value:  "30s",
		Desc:   "How often the Neo4j healthcheck is called.",
		EnvVar: "HEALTHCHECK_INTERVAL",
	})
	app.Action = func() {
		baseftrwapp.OutputMetricsIfRequired(*graphiteTCPAddress, *graphitePrefix, *logMetrics)
		log.Infof("public-concordances-api will listen on port: %s, connecting to: %s", *port, *neoURL)
		runServer(*neoURL, *port, *cacheDuration, *env, *healthcheckInterval)
	}
	log.SetFormatter(&log.TextFormatter{DisableColors: true})
	log.SetLevel(log.InfoLevel)
	log.Infof("Application started with args %s", os.Args)
	app.Run(os.Args)
}

func runServer(neoURL string, port string, cacheDuration string, env string, healthcheckInterval string) {

	if duration, durationErr := time.ParseDuration(cacheDuration); durationErr != nil {
		log.Fatalf("Failed to parse cache duration string, %v", durationErr)
	} else {
		concordances.CacheControlHeader = fmt.Sprintf("max-age=%s, public", strconv.FormatFloat(duration.Seconds(), 'f', 0, 64))
	}

	conf := neoutils.ConnectionConfig{
		BatchSize:     1024,
		Transactional: false,
		HTTPClient: &http.Client{
			Transport: &http.Transport{
				MaxIdleConnsPerHost: 100,
			},
			Timeout: 1 * time.Minute,
		},
		BackgroundConnect: true,
	}
	db, err := neoutils.Connect(neoURL, &conf)
	if err != nil {
		log.Fatalf("Error connecting to neo4j %s", err)
	}

	concordances.ConcordanceDriver = concordances.NewCypherDriver(db, env)

	checkInterval, err := time.ParseDuration(healthcheckInterval)
	if err != nil {
		checkInterval = time.Second * 30
	}
	concordances.StartAsyncChecker(checkInterval)

	servicesRouter := mux.NewRouter()

	// Healthchecks and standards first
	servicesRouter.HandleFunc("/__health", v1a.Handler("PublicConcordancesRead Healthchecks",
		"Checks for accessing neo4j", concordances.HealthCheck()))

	// Then API specific ones:

	mh := &handlers.MethodHandler{
		"GET": http.HandlerFunc(concordances.GetConcordances),
	}
	servicesRouter.Handle("/concordances", mh)

	var monitoringRouter http.Handler = servicesRouter
	monitoringRouter = httphandlers.TransactionAwareRequestLoggingHandler(log.StandardLogger(), monitoringRouter)
	monitoringRouter = httphandlers.HTTPMetricsHandler(metrics.DefaultRegistry, monitoringRouter)

	// The top one of these feels more correct, but the lower one matches what we have in Dropwizard,
	// so it's what apps expect currently same as ping, the content of build-info needs more definition
	//using http router here to be able to catch "/"
	http.HandleFunc(status.BuildInfoPath, status.BuildInfoHandler)
	http.HandleFunc(status.BuildInfoPathDW, status.BuildInfoHandler)
	http.HandleFunc("/__gtg", concordances.GoodToGo)
	http.Handle("/", monitoringRouter)

	if err := http.ListenAndServe(":"+port, nil); err != nil {
		log.Fatalf("Unable to start server: %v", err)
	}

}
<commit_msg>Added batch size as an env var<commit_after>package main

import (
	"fmt"
	"net/http"
	"os"
	"strconv"
	"time"

	"github.com/Financial-Times/base-ft-rw-app-go/baseftrwapp"
	"github.com/Financial-Times/go-fthealth/v1a"
	"github.com/Financial-Times/http-handlers-go/httphandlers"
	"github.com/Financial-Times/neo-utils-go/neoutils"
	"github.com/Financial-Times/public-concordances-api/concordances"
	status "github.com/Financial-Times/service-status-go/httphandlers"
	log "github.com/Sirupsen/logrus"
	"github.com/gorilla/handlers"
	"github.com/gorilla/mux"
	"github.com/jawher/mow.cli"
	_ "github.com/joho/godotenv/autoload"
	"github.com/rcrowley/go-metrics"
)

func main() {
	app := cli.App("public-concordances-api-neo4j", "A public RESTful API for accessing concordances in neo4j")
	neoURL := app.String(cli.StringOpt{
		Name:   "neo-url",
		Value:  "http://localhost:7474/db/data",
		Desc:   "neo4j endpoint URL",
		EnvVar: "NEO_URL",
	})
	port := app.String(cli.StringOpt{
		Name:   "port",
		Value:  "8080",
		Desc:   "Port to listen on",
		EnvVar: "APP_PORT",
	})
	graphiteTCPAddress := app.String(cli.StringOpt{
		Name:   "graphiteTCPAddress",
		Value:  "",
		Desc:   "Graphite TCP address, e.g. graphite.ft.com:2003. Leave as default if you do NOT want to output to graphite (e.g. if running locally)",
		EnvVar: "GRAPHITE_ADDRESS",
	})
	graphitePrefix := app.String(cli.StringOpt{
		Name:   "graphitePrefix",
		Value:  "",
		Desc:   "Prefix to use. Should start with content, include the environment, and the host name. e.g. content.test.public.content.by.concept.api.ftaps59382-law1a-eu-t",
		EnvVar: "GRAPHITE_PREFIX",
	})
	logMetrics := app.Bool(cli.BoolOpt{
		Name:   "logMetrics",
		Value:  false,
		Desc:   "Whether to log metrics. Set to true if running locally and you want metrics output",
		EnvVar: "LOG_METRICS",
	})
	env := app.String(cli.StringOpt{
		Name:  "env",
		Value: "local",
		Desc:  "environment this app is running in",
	})
	cacheDuration := app.String(cli.StringOpt{
		Name:   "cache-duration",
		Value:  "30s",
		Desc:   "Duration Get requests should be cached for. e.g. 2h45m would set the max-age value to '7440' seconds",
		EnvVar: "CACHE_DURATION",
	})
	logLevel := app.String(cli.StringOpt{
		Name:   "logLevel",
		Value:  "info",
		Desc:   "Log level of the app",
		EnvVar: "LOG_LEVEL",
	})
	healthcheckInterval := app.String(cli.StringOpt{
		Name:   "healthcheck-interval",
		Value:  "30s",
		Desc:   "How often the Neo4j healthcheck is called.",
		EnvVar: "HEALTHCHECK_INTERVAL",
	})
	batchSize := app.Int(cli.IntOpt{
		Name:   "batch-size",
		Value:  0,
		Desc:   "Max batch size for Neo4j queries",
		EnvVar: "BATCH_SIZE",
	})
	app.Action = func() {
		baseftrwapp.OutputMetricsIfRequired(*graphiteTCPAddress, *graphitePrefix, *logMetrics)
		log.Infof("public-concordances-api will listen on port: %s, connecting to: %s", *port, *neoURL)
		runServer(*neoURL, *port, *cacheDuration, *env, *healthcheckInterval, *batchSize)
	}
	log.SetFormatter(&log.JSONFormatter{})
	lvl, err := log.ParseLevel(*logLevel)
	if err != nil {
		log.WithField("LOG_LEVEL", *logLevel).Warn("Cannot parse log level, setting it to INFO.")
		lvl = log.InfoLevel
	}
	log.SetLevel(lvl)
	log.WithFields(log.Fields{
		"HEALTHCHECK_INTERVAL": *healthcheckInterval,
		"CACHE_DURATION":       *cacheDuration,
		"NEO_URL":              *neoURL,
		"LOG_LEVEL":            *logLevel,
	}).Info("Starting app with arguments")
	app.Run(os.Args)
}

func runServer(neoURL string, port string, cacheDuration string, env string, healthcheckInterval string, batchSize int) {

	if duration, durationErr := time.ParseDuration(cacheDuration); durationErr != nil {
		log.Fatalf("Failed to parse cache duration string, %v", durationErr)
	} else {
		concordances.CacheControlHeader = fmt.Sprintf("max-age=%s, public", strconv.FormatFloat(duration.Seconds(), 'f', 0, 64))
	}

	conf := neoutils.ConnectionConfig{
		BatchSize:     batchSize,
		Transactional: false,
		HTTPClient: &http.Client{
			Transport: &http.Transport{
				MaxIdleConnsPerHost: 100,
			},
			Timeout: 1 * time.Minute,
		},
		BackgroundConnect: true,
	}
	db, err := neoutils.Connect(neoURL, &conf)
	if err != nil {
		log.Fatalf("Error connecting to neo4j %s", err)
	}

	concordances.ConcordanceDriver = concordances.NewCypherDriver(db, env)

	checkInterval, err := time.ParseDuration(healthcheckInterval)
	if err != nil {
		checkInterval = time.Second * 30
	}
	concordances.StartAsyncChecker(checkInterval)

	servicesRouter := mux.NewRouter()

	// Healthchecks and standards first
	servicesRouter.HandleFunc("/__health", v1a.Handler("PublicConcordancesRead Healthchecks",
		"Checks for accessing neo4j", concordances.HealthCheck()))

	// Then API specific ones:

	mh := &handlers.MethodHandler{
		"GET": http.HandlerFunc(concordances.GetConcordances),
	}
	servicesRouter.Handle("/concordances", mh)

	var monitoringRouter http.Handler = servicesRouter
	monitoringRouter = httphandlers.TransactionAwareRequestLoggingHandler(log.StandardLogger(), monitoringRouter)
	monitoringRouter = httphandlers.HTTPMetricsHandler(metrics.DefaultRegistry, monitoringRouter)

	// The top one of these feels more correct, but the lower one matches what we have in Dropwizard,
	// so it's what apps expect currently same as ping, the content of build-info needs more definition
	//using http router here to be able to catch "/"
	http.HandleFunc(status.BuildInfoPath, status.BuildInfoHandler)
	http.HandleFunc(status.BuildInfoPathDW, status.BuildInfoHandler)
	http.HandleFunc("/__gtg", concordances.GoodToGo)
	http.Handle("/", monitoringRouter)

	if err := http.ListenAndServe(":"+port, nil); err != nil {
		log.Fatalf("Unable to start server: %v", err)
	}

}
<|endoftext|>


<commit_before>package main

import (
    "fmt"
    "github.com/bwmarrin/discordgo"
    "github.com/getsentry/raven-go"
    "github.com/sn0w/Karen/cache"
    "github.com/sn0w/Karen/helpers"
    Logger "github.com/sn0w/Karen/logger"
    "github.com/sn0w/Karen/metrics"
    "github.com/sn0w/Karen/plugins"
    "math/rand"
    "regexp"
    "strings"
    "time"
)

// BotOnReady gets called after the gateway connected
func BotOnReady(session *discordgo.Session, event *discordgo.Ready) {
    Logger.INFO.L("bot", "Connected to discord!")
    Logger.VERBOSE.L("bot", "Invite link: " + fmt.Sprintf(
        "https://discordapp.com/oauth2/authorize?client_id=%s&scope=bot&permissions=%s",
        helpers.GetConfig().Path("discord.id").Data().(string),
        helpers.GetConfig().Path("discord.perms").Data().(string),
    ))

    cache.SetSession(session)

    // Init plugins
    tmpl := "[PLUG] %s reacts to [ %s]"
    for _, plugin := range plugins.PluginList {
        cmds := ""

        for _, cmd := range plugin.Commands() {
            cmds += cmd + " "
        }

        Logger.INFO.L("bot", fmt.Sprintf(
            tmpl,
            helpers.Typeof(plugin),
            cmds,
        ))

        plugin.Init(session)
    }

    // Init trigger plugins
    tmpl = "[TRIG] Registered %s"
    for _, plugin := range plugins.TriggerPluginList {
        Logger.INFO.L("bot", fmt.Sprintf(
            tmpl,
            helpers.Typeof(plugin),
        ))

        plugin.Init(session)
    }

    go func() {
        time.Sleep(3 * time.Second)

        configName := helpers.GetConfig().Path("bot.name").Data().(string)
        configAvatar := helpers.GetConfig().Path("bot.avatar").Data().(string)

        // Change avatar if desired
        if configAvatar != "" && configAvatar != session.State.User.Avatar {
            session.UserUpdate(
                "",
                "",
                session.State.User.Username,
                configAvatar,
                "",
            )
        }

        // Change name if desired
        if configName != "" && configName != session.State.User.Username {
            session.UserUpdate(
                "",
                "",
                configName,
                session.State.User.Avatar,
                "",
            )
        }
    }()

    // Run async game-changer
    go changeGameInterval(session)

    // Run auto-leaver for non-beta guilds
    go autoLeaver(session)
}

// BotOnMessageCreate gets called after a new message was sent
// This will be called after *every* message on *every* server so it should die as soon as possible
// or spawn costly work inside of coroutines.
func BotOnMessageCreate(session *discordgo.Session, message *discordgo.MessageCreate) {
    // Ignore other bots and @everyone/@here
    if message.Author.Bot || message.MentionEveryone {
        return
    }

    // Get the channel
    // Ignore the event if we cannot resolve the channel
    channel, err := cache.Channel(message.ChannelID)
    if err != nil {
        go raven.CaptureError(err, map[string]string{})
        return
    }

    // We only do things in guilds.
    // Get a friend already and stop chatting with bots
    if channel.IsPrivate {
        return
    }

    // Check if the message contains @mentions
    if len(message.Mentions) >= 1 {
        // Check if someone is mentioning us
        // If not exit
        if !(strings.HasPrefix(message.Content, "<@") && message.Mentions[0].ID == session.State.User.ID) {
            return
        }

        // Prepare content for editing
        msg := message.Content

        /// Remove our @mention
        msg = strings.Replace(msg, "<@" + session.State.User.ID + ">", "", -1)

        // Trim message
        msg = strings.Trim(msg, " ")

        // Convert to []byte before matching
        bmsg := []byte(msg)

        // Match against common task patterns
        // Send to cleverbot if nothing matches
        switch {
        case regexp.MustCompile("(?i)^HELP.*").Match(bmsg):
            metrics.CommandsExecuted.Add(1)
            sendHelp(message)
            return

        case regexp.MustCompile("(?i)^PREFIX.*").Match(bmsg):
            metrics.CommandsExecuted.Add(1)
            prefix, _ := helpers.GetPrefixForServer(channel.GuildID)
            if prefix == "" {
                cache.GetSession().ChannelMessageSend(
                    channel.ID,
                    "Seems like there is no prefix yet :thinking:\n" +
                        "Admins can set one by typing for example `@Karen set prefix ?`",
                )
            }

            cache.GetSession().ChannelMessageSend(
                channel.ID,
                "The prefix is `" + prefix + "` :smiley:",
            )
            return

        case regexp.MustCompile("(?i)^REFRESH CHAT SESSION$").Match(bmsg):
            metrics.CommandsExecuted.Add(1)
            helpers.RequireAdmin(message.Message, func() {
                // Refresh cleverbot session
                helpers.CleverbotRefreshSession(channel.ID)
                cache.GetSession().ChannelMessageSend(channel.ID, ":cyclone: Refreshed!")
            })
            return

        case regexp.MustCompile("(?i)^SET PREFIX (.){1,25}$").Match(bmsg):
            metrics.CommandsExecuted.Add(1)
            helpers.RequireAdmin(message.Message, func() {
                // Extract prefix
                prefix := strings.Split(
                    regexp.MustCompile("(?i)^SET PREFIX\\s").ReplaceAllString(msg, ""),
                    " ",
                )[0]

                // Set new prefix
                err := helpers.SetPrefixForServer(
                    channel.GuildID,
                    prefix,
                )

                if err != nil {
                    helpers.SendError(message.Message, err)
                } else {
                    cache.GetSession().ChannelMessageSend(channel.ID, ":white_check_mark: Saved! \n The prefix is now `" + prefix + "`")
                }
            })
            return

        default:
            // Track usage
            metrics.CleverbotRequests.Add(1)

            // Send to cleverbot
            session.ChannelTyping(message.ChannelID)

            // Resolve other @mentions before sending the message
            for _, user := range message.Mentions {
                msg = strings.Replace(msg, "<@" + user.ID + ">", user.Username, -1)
            }

            // Remove smileys
            msg = regexp.MustCompile(`:\w+:`).ReplaceAllString(msg, "")

            // Send to cleverbot
            helpers.CleverbotSend(session, channel.ID, msg)
            return
        }
    }

    // Only continue if a prefix is set
    // Else check if any instant-replies match
    prefix, _ := helpers.GetPrefixForServer(channel.GuildID)
    if prefix == "" {
        plugins.CallTriggerPlugins(message.Message)
        return
    }

    // Check if the message is prefixed for us
    if !strings.HasPrefix(message.Content, prefix) {
        return
    }

    // Split the message into parts
    parts := strings.Split(message.Content, " ")

    // Save a sanitized version of the command (no prefix)
    cmd := strings.Replace(parts[0], prefix, "", 1)

    // Check if the user calls for help
    if cmd == "h" || cmd == "help" {
        metrics.CommandsExecuted.Add(1)
        sendHelp(message)
        return
    }

    // Check if a module matches said command
    // Do nothing otherwise
    plugins.CallBotPlugin(
        cmd,
        strings.Replace(message.Content, prefix + cmd, "", -1),
        message.Message,
    )
}

func sendHelp(message *discordgo.MessageCreate) {
    cache.GetSession().ChannelMessageSend(
        message.ChannelID,
        fmt.Sprintf("<@%s> Check out <http://meetkaren.xyz/commands>", message.Author.ID),
    )
}

// Changes the game interval every 10 seconds after called
func changeGameInterval(session *discordgo.Session) {
    for {
        err := session.UpdateStatus(
            0,
            games[rand.Intn(len(games))],
        )
        if err != nil {
            raven.CaptureError(err, map[string]string{})
        }

        time.Sleep(10 * time.Second)
    }
}

// List of games to randomly show
var games = []string{
    // Random stuff
    "async is the future!",
    "down with OOP!",
    "spoopy stuff",
    "Planking",

    // Kaomoji
    "ʕ•ᴥ•ʔ",
    "༼ つ ◕_◕ ༽つ",
    "(ﾉ◕ヮ◕)ﾉ*:･ﾟ✧",
    "( ͡° ͜ʖ ͡°)",
    "¯\\_(ツ)_/¯",
    "(ง ͠° ͟ل͜ ͡°)ง",
    "ಠ_ಠ",
    "(╯°□°)╯︵ ʞooqǝɔɐɟ",
    "♪~ ᕕ(ᐛ)ᕗ",
    "\\ (•◡•) /",
    "｡◕‿◕｡",

    // actual games
    "Hearthstone",
    "Overwatch",
    "HuniePop",
    "Candy Crush",
    "Hyperdimension Neptunia",
    "Final Fantasy MCMX",
    "CIV V",
    "Pokemon Go",
    "Simulation Simulator 2016",
    "Half Life 3",
    "Nekopara",

    // software
    "with FFMPEG",
    "with libav",
    "with gophers",
    "with python",
    "with reflections",

    // names
    "with Shinobu-Chan",
    "with Ako-Chan",
    "with Nadeko",
    "with Miku",
    "with you O_o",
    "with cats",
    "with JOHN CENA",
    "with senpai",
    "with Serraniel#8978",
    "with 0xFADED#3237",
    "with C0untLizzi",
    "with moot",
    "with your waifu",
    "with Trump",
}
<commit_msg>Fix user neglection if @mention is misplaced (best bug ever)<commit_after>package main

import (
    "fmt"
    "github.com/bwmarrin/discordgo"
    "github.com/getsentry/raven-go"
    "github.com/sn0w/Karen/cache"
    "github.com/sn0w/Karen/helpers"
    Logger "github.com/sn0w/Karen/logger"
    "github.com/sn0w/Karen/metrics"
    "github.com/sn0w/Karen/plugins"
    "math/rand"
    "regexp"
    "strings"
    "time"
)

// BotOnReady gets called after the gateway connected
func BotOnReady(session *discordgo.Session, event *discordgo.Ready) {
    Logger.INFO.L("bot", "Connected to discord!")
    Logger.VERBOSE.L("bot", "Invite link: " + fmt.Sprintf(
        "https://discordapp.com/oauth2/authorize?client_id=%s&scope=bot&permissions=%s",
        helpers.GetConfig().Path("discord.id").Data().(string),
        helpers.GetConfig().Path("discord.perms").Data().(string),
    ))

    cache.SetSession(session)

    // Init plugins
    tmpl := "[PLUG] %s reacts to [ %s]"
    for _, plugin := range plugins.PluginList {
        cmds := ""

        for _, cmd := range plugin.Commands() {
            cmds += cmd + " "
        }

        Logger.INFO.L("bot", fmt.Sprintf(
            tmpl,
            helpers.Typeof(plugin),
            cmds,
        ))

        plugin.Init(session)
    }

    // Init trigger plugins
    tmpl = "[TRIG] Registered %s"
    for _, plugin := range plugins.TriggerPluginList {
        Logger.INFO.L("bot", fmt.Sprintf(
            tmpl,
            helpers.Typeof(plugin),
        ))

        plugin.Init(session)
    }

    go func() {
        time.Sleep(3 * time.Second)

        configName := helpers.GetConfig().Path("bot.name").Data().(string)
        configAvatar := helpers.GetConfig().Path("bot.avatar").Data().(string)

        // Change avatar if desired
        if configAvatar != "" && configAvatar != session.State.User.Avatar {
            session.UserUpdate(
                "",
                "",
                session.State.User.Username,
                configAvatar,
                "",
            )
        }

        // Change name if desired
        if configName != "" && configName != session.State.User.Username {
            session.UserUpdate(
                "",
                "",
                configName,
                session.State.User.Avatar,
                "",
            )
        }
    }()

    // Run async game-changer
    go changeGameInterval(session)

    // Run auto-leaver for non-beta guilds
    go autoLeaver(session)
}

// BotOnMessageCreate gets called after a new message was sent
// This will be called after *every* message on *every* server so it should die as soon as possible
// or spawn costly work inside of coroutines.
func BotOnMessageCreate(session *discordgo.Session, message *discordgo.MessageCreate) {
    // Ignore other bots and @everyone/@here
    if message.Author.Bot || message.MentionEveryone {
        return
    }

    // Get the channel
    // Ignore the event if we cannot resolve the channel
    channel, err := cache.Channel(message.ChannelID)
    if err != nil {
        go raven.CaptureError(err, map[string]string{})
        return
    }

    // We only do things in guilds.
    // Get a friend already and stop chatting with bots
    if channel.IsPrivate {
        return
    }

    // Check if the message contains @mentions for us
    if len(message.Mentions) > 0 && strings.HasPrefix(message.Content, "<@") && message.Mentions[0].ID == session.State.User.ID {
        // Prepare content for editing
        msg := message.Content

        /// Remove our @mention
        msg = strings.Replace(msg, "<@" + session.State.User.ID + ">", "", -1)

        // Trim message
        msg = strings.Trim(msg, " ")

        // Convert to []byte before matching
        bmsg := []byte(msg)

        // Match against common task patterns
        // Send to cleverbot if nothing matches
        switch {
        case regexp.MustCompile("(?i)^HELP.*").Match(bmsg):
            metrics.CommandsExecuted.Add(1)
            sendHelp(message)
            return

        case regexp.MustCompile("(?i)^PREFIX.*").Match(bmsg):
            metrics.CommandsExecuted.Add(1)
            prefix, _ := helpers.GetPrefixForServer(channel.GuildID)
            if prefix == "" {
                cache.GetSession().ChannelMessageSend(
                    channel.ID,
                    "Seems like there is no prefix yet :thinking:\n" +
                        "Admins can set one by typing for example `@Karen set prefix ?`",
                )
            }

            cache.GetSession().ChannelMessageSend(
                channel.ID,
                "The prefix is `" + prefix + "` :smiley:",
            )
            return

        case regexp.MustCompile("(?i)^REFRESH CHAT SESSION$").Match(bmsg):
            metrics.CommandsExecuted.Add(1)
            helpers.RequireAdmin(message.Message, func() {
                // Refresh cleverbot session
                helpers.CleverbotRefreshSession(channel.ID)
                cache.GetSession().ChannelMessageSend(channel.ID, ":cyclone: Refreshed!")
            })
            return

        case regexp.MustCompile("(?i)^SET PREFIX (.){1,25}$").Match(bmsg):
            metrics.CommandsExecuted.Add(1)
            helpers.RequireAdmin(message.Message, func() {
                // Extract prefix
                prefix := strings.Split(
                    regexp.MustCompile("(?i)^SET PREFIX\\s").ReplaceAllString(msg, ""),
                    " ",
                )[0]

                // Set new prefix
                err := helpers.SetPrefixForServer(
                    channel.GuildID,
                    prefix,
                )

                if err != nil {
                    helpers.SendError(message.Message, err)
                } else {
                    cache.GetSession().ChannelMessageSend(channel.ID, ":white_check_mark: Saved! \n The prefix is now `" + prefix + "`")
                }
            })
            return

        default:
            // Track usage
            metrics.CleverbotRequests.Add(1)

            // Send to cleverbot
            session.ChannelTyping(message.ChannelID)

            // Resolve other @mentions before sending the message
            for _, user := range message.Mentions {
                msg = strings.Replace(msg, "<@" + user.ID + ">", user.Username, -1)
            }

            // Remove smileys
            msg = regexp.MustCompile(`:\w+:`).ReplaceAllString(msg, "")

            // Send to cleverbot
            helpers.CleverbotSend(session, channel.ID, msg)
            return
        }
    }

    // Only continue if a prefix is set
    // Else check if any instant-replies match
    prefix, _ := helpers.GetPrefixForServer(channel.GuildID)
    if prefix == "" {
        plugins.CallTriggerPlugins(message.Message)
        return
    }

    // Check if the message is prefixed for us
    // If not exit
    if !strings.HasPrefix(message.Content, prefix) {
        return
    }

    // Split the message into parts
    parts := strings.Split(message.Content, " ")

    // Save a sanitized version of the command (no prefix)
    cmd := strings.Replace(parts[0], prefix, "", 1)

    // Check if the user calls for help
    if cmd == "h" || cmd == "help" {
        metrics.CommandsExecuted.Add(1)
        sendHelp(message)
        return
    }

    // Check if a module matches said command
    // Do nothing otherwise
    plugins.CallBotPlugin(
        cmd,
        strings.Replace(message.Content, prefix + cmd, "", -1),
        message.Message,
    )
}

func sendHelp(message *discordgo.MessageCreate) {
    cache.GetSession().ChannelMessageSend(
        message.ChannelID,
        fmt.Sprintf("<@%s> Check out <http://meetkaren.xyz/commands>", message.Author.ID),
    )
}

// Changes the game interval every 10 seconds after called
func changeGameInterval(session *discordgo.Session) {
    for {
        err := session.UpdateStatus(
            0,
            games[rand.Intn(len(games))],
        )
        if err != nil {
            raven.CaptureError(err, map[string]string{})
        }

        time.Sleep(10 * time.Second)
    }
}

// List of games to randomly show
var games = []string{
    // Random stuff
    "async is the future!",
    "down with OOP!",
    "spoopy stuff",
    "Planking",

    // Kaomoji
    "ʕ•ᴥ•ʔ",
    "༼ つ ◕_◕ ༽つ",
    "(ﾉ◕ヮ◕)ﾉ*:･ﾟ✧",
    "( ͡° ͜ʖ ͡°)",
    "¯\\_(ツ)_/¯",
    "(ง ͠° ͟ل͜ ͡°)ง",
    "ಠ_ಠ",
    "(╯°□°)╯︵ ʞooqǝɔɐɟ",
    "♪~ ᕕ(ᐛ)ᕗ",
    "\\ (•◡•) /",
    "｡◕‿◕｡",

    // actual games
    "Hearthstone",
    "Overwatch",
    "HuniePop",
    "Candy Crush",
    "Hyperdimension Neptunia",
    "Final Fantasy MCMX",
    "CIV V",
    "Pokemon Go",
    "Simulation Simulator 2016",
    "Half Life 3",
    "Nekopara",

    // software
    "with FFMPEG",
    "with libav",
    "with gophers",
    "with python",
    "with reflections",

    // names
    "with Shinobu-Chan",
    "with Ako-Chan",
    "with Nadeko",
    "with Miku",
    "with you O_o",
    "with cats",
    "with JOHN CENA",
    "with senpai",
    "with Serraniel#8978",
    "with 0xFADED#3237",
    "with C0untLizzi",
    "with moot",
    "with your waifu",
    "with Trump",
}
<|endoftext|>


<commit_before>package main

import (
	"flag"
	"fmt"
	"log"
	"os"
	"strconv"
	"strings"

	"github.com/nlopes/slack"
)

var api *slack.Client

var Token string
var TLDR string

func init() {
	flag.StringVar(&Token, "token", "", "Slack API bot token (can also be set with SLACK_TLDR_TOKEN)")
	flag.StringVar(&TLDR, "command", "+tldr", "Command to type in Slack to trigger TLDR functions (default: +tldr)")
}

func main() {
	flag.Parse()
	if t := os.Getenv("SLACK_TLDR_TOKEN"); t != "" {
		Token = t
	}
	if Token == "" {
		fmt.Println("SLACK_TLDR_TOKEN was not set - use -token or set env var")
		return
	}

	logger := log.New(os.Stdout, "slack-tldr: ", log.Lshortfile|log.LstdFlags)
	api = slack.New(Token)
	slack.SetLogger(logger)
	api.SetDebug(false)

	rtm := api.NewRTM()
	go rtm.ManageConnection()

Loop:
	for {
		select {
		case msg := <-rtm.IncomingEvents:
			fmt.Print("Event Received: ")
			switch ev := msg.Data.(type) {

			case *slack.MessageEvent:
				if strings.HasPrefix(ev.Text, TLDR) {
					handleTLDR(ev)
					continue
				}

			case *slack.RTMError:
				fmt.Printf("Error: %s\n", ev.Error())

			case *slack.InvalidAuthEvent:
				fmt.Printf("Invalid credentials")
				break Loop

			default:
				// Ignore other events...
			}
		}
	}
}

func handleTLDR(ev *slack.MessageEvent) {
	params := slack.PostMessageParameters{
		AsUser:    false,
		Username:  "TL;DR",
		IconEmoji: ":stopwatch:",
	}

	message := strings.TrimSpace(ev.Text)

	// TODO: skip this if adding
	listPins, _, err := api.ListPins(ev.Channel)
	if err != nil {
		fmt.Printf("Error listing pins: %s\n", err)
		return
	}
	for i, j := 0, len(listPins)-1; i < j; i, j = i+1, j-1 {
		listPins[i], listPins[j] = listPins[j], listPins[i]
	}

	if message == TLDR {
		fmt.Printf("\n")
		msg := ""
		for i, item := range listPins {
			msg += fmt.Sprintf("*%d*. %s", i+1, item.Message.Text)
			user, err := api.GetUserInfo(item.Message.User)
			if err != nil {
				fmt.Printf("%s\n", err)
			} else {
				msg += " _-- @" + user.Name + "_"
			}
			msg += "\n"
		}
		msg += "\n\n_You can also use pinned messages to add/remove TL;DR posts_"
		_, _, err = api.PostMessage(ev.Channel, msg, params)
		return
	}

	if p := strings.TrimPrefix(message, TLDR+" remove "); p != message {
		pos, err := strconv.Atoi(p)
		if err != nil {
			fmt.Printf("%s\n", err)
			return
		}
		if pos > len(listPins) {
			return
		}
		delete := listPins[pos-1]
		msgRef := slack.NewRefToMessage(ev.Channel, delete.Message.Timestamp)
		err = api.RemovePin(ev.Channel, msgRef)
		fmt.Printf("\n\n\n\n%#v\n\n\n\n%#v\n\n\n\n", msgRef, delete.Message)
		if err != nil {
			fmt.Printf("Error remove pin: %s\n", err)
			return
		}
		_, _, err = api.PostMessage(ev.Channel, "Unpinned "+delete.Message.Text, params)
		return
	}

	if n := strings.TrimPrefix(message, TLDR+" "); n != message {
		user, err := api.GetUserInfo(ev.User)
		if err != nil {
			fmt.Printf("%s\n", err)
		} else {
			params.Username = user.Name
			n += " _-- @" + user.Name + "_"
		}

		channelID, timestamp, err := api.PostMessage(ev.Channel, n, params)
		if err != nil {
			fmt.Printf("Error posting message: %s\n", err)
			return
		}
		msgRef := slack.NewRefToMessage(channelID, timestamp)

		if err := api.AddPin(channelID, msgRef); err != nil {
			fmt.Printf("Error adding pin: %s\n", err)
			return
		}
		return
	}

}
<commit_msg>fix redundant message<commit_after>package main

import (
	"flag"
	"fmt"
	"log"
	"os"
	"strconv"
	"strings"

	"github.com/nlopes/slack"
)

var api *slack.Client

var Token string
var TLDR string

func init() {
	flag.StringVar(&Token, "token", "", "Slack API bot token (can also be set with SLACK_TLDR_TOKEN)")
	flag.StringVar(&TLDR, "command", "+tldr", "Command to type in Slack to trigger TLDR functions")
}

func main() {
	flag.Parse()
	if t := os.Getenv("SLACK_TLDR_TOKEN"); t != "" {
		Token = t
	}
	if Token == "" {
		fmt.Println("SLACK_TLDR_TOKEN was not set - use -token or set env var")
		return
	}

	logger := log.New(os.Stdout, "slack-tldr: ", log.Lshortfile|log.LstdFlags)
	api = slack.New(Token)
	slack.SetLogger(logger)
	api.SetDebug(false)

	rtm := api.NewRTM()
	go rtm.ManageConnection()

Loop:
	for {
		select {
		case msg := <-rtm.IncomingEvents:
			fmt.Print("Event Received: ")
			switch ev := msg.Data.(type) {

			case *slack.MessageEvent:
				if strings.HasPrefix(ev.Text, TLDR) {
					handleTLDR(ev)
					continue
				}

			case *slack.RTMError:
				fmt.Printf("Error: %s\n", ev.Error())

			case *slack.InvalidAuthEvent:
				fmt.Printf("Invalid credentials")
				break Loop

			default:
				// Ignore other events...
			}
		}
	}
}

func handleTLDR(ev *slack.MessageEvent) {
	params := slack.PostMessageParameters{
		AsUser:    false,
		Username:  "TL;DR",
		IconEmoji: ":stopwatch:",
	}

	message := strings.TrimSpace(ev.Text)

	// TODO: skip this if adding
	listPins, _, err := api.ListPins(ev.Channel)
	if err != nil {
		fmt.Printf("Error listing pins: %s\n", err)
		return
	}
	for i, j := 0, len(listPins)-1; i < j; i, j = i+1, j-1 {
		listPins[i], listPins[j] = listPins[j], listPins[i]
	}

	if message == TLDR {
		fmt.Printf("\n")
		msg := ""
		for i, item := range listPins {
			msg += fmt.Sprintf("*%d*. %s", i+1, item.Message.Text)
			user, err := api.GetUserInfo(item.Message.User)
			if err != nil {
				fmt.Printf("%s\n", err)
			} else {
				msg += " _-- @" + user.Name + "_"
			}
			msg += "\n"
		}
		msg += "\n\n_You can also use pinned messages to add/remove TL;DR posts_"
		_, _, err = api.PostMessage(ev.Channel, msg, params)
		return
	}

	if p := strings.TrimPrefix(message, TLDR+" remove "); p != message {
		pos, err := strconv.Atoi(p)
		if err != nil {
			fmt.Printf("%s\n", err)
			return
		}
		if pos > len(listPins) {
			return
		}
		delete := listPins[pos-1]
		msgRef := slack.NewRefToMessage(ev.Channel, delete.Message.Timestamp)
		err = api.RemovePin(ev.Channel, msgRef)
		fmt.Printf("\n\n\n\n%#v\n\n\n\n%#v\n\n\n\n", msgRef, delete.Message)
		if err != nil {
			fmt.Printf("Error remove pin: %s\n", err)
			return
		}
		_, _, err = api.PostMessage(ev.Channel, "Unpinned "+delete.Message.Text, params)
		return
	}

	if n := strings.TrimPrefix(message, TLDR+" "); n != message {
		user, err := api.GetUserInfo(ev.User)
		if err != nil {
			fmt.Printf("%s\n", err)
		} else {
			params.Username = user.Name
			n += " _-- @" + user.Name + "_"
		}

		channelID, timestamp, err := api.PostMessage(ev.Channel, n, params)
		if err != nil {
			fmt.Printf("Error posting message: %s\n", err)
			return
		}
		msgRef := slack.NewRefToMessage(channelID, timestamp)

		if err := api.AddPin(channelID, msgRef); err != nil {
			fmt.Printf("Error adding pin: %s\n", err)
			return
		}
		return
	}

}
<|endoftext|>


<commit_before>package slackbot

import (
	"bytes"
	"encoding/binary"
	"encoding/json"
	"fmt"

	"errors"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
	"time"

	"github.com/gorilla/websocket"
)

const (
	EventTypeMessage                    EventType = "message"
	EventTypeHello                      EventType = "hello"
	EventTypeChannelMarked              EventType = "channel_marked"
	EventTypeChannelCreated             EventType = "channel_created"
	EventTypeChannelJoined              EventType = "channel_joined"
	EventTypeChannelLeft                EventType = "channel_left"
	EventTypeChannelDeleted             EventType = "channel_deleted"
	EventTypeChannelRename              EventType = "channel_rename"
	EventTypeChannelArchive             EventType = "channel_archive"
	EventTypeChannelUnarchive           EventType = "channel_unarchive"
	EventTypeChannelHistoryChanged      EventType = "channel_history_changed"
	EventTypeChannelImCreated           EventType = "im_created"
	EventTypeChannelImOpen              EventType = "im_open"
	EventTypeChannelImClose             EventType = "im_close"
	EventTypeChannelImMarked            EventType = "im_marked"
	EventTypeChannelImHistoryChanged    EventType = "im_history_changed"
	EventTypeChannelGroupJoined         EventType = "group_joined"
	EventTypeChannelGroupLeft           EventType = "group_left"
	EventTypeChannelGroupOpen           EventType = "group_open"
	EventTypeChannelGroupClose          EventType = "group_close"
	EventTypeChannelGroupArchive        EventType = "group_archive"
	EventTypeChannelGroupUnarchive      EventType = "group_unarchive"
	EventTypeChannelGroupRename         EventType = "group_rename"
	EventTypeChannelGroupMarked         EventType = "group_marked"
	EventTypeChannelGroupHistoryChanged EventType = "group_history_changed"
	EventTypeFileCreated                EventType = "file_created"
	EventTypeFileShared                 EventType = "file_shared"
	EventTypeFileUnshared               EventType = "file_unshared"
	EventTypeFilePublic                 EventType = "file_public"
	EventTypeFilePrivate                EventType = "file_private"
	EventTypeFileChange                 EventType = "file_change"
	EventTypeFileDeleted                EventType = "file_deleted"
	EventTypeFileCommentAdded           EventType = "file_comment_added"
	EventTypeFileCommentEdited          EventType = "file_comment_edited"
	EventTypeFileCommentDeleted         EventType = "file_comment_deleted"
	EventTypePresenceChange             EventType = "presence_change"
	EventTypeManualPresenceChange       EventType = "manual_presence_change"
	EventTypePrefChange                 EventType = "pref_change"
	EventTypeUserChange                 EventType = "user_change"
	EventTypeTeamJoin                   EventType = "team_join"
	EventTypeStarAdded                  EventType = "star_added"
	EventTypeStarRemoved                EventType = "star_removed"
	EventTypeEmojiChanged               EventType = "emoji_changed"
	EventTypeCommandsChanged            EventType = "commands_changed"
	EventTypeTeamPrefChange             EventType = "team_pref_change"
	EventTypeTeamRename                 EventType = "team_rename"
	EventTypeTeamDomainChange           EventType = "team_domain_change"
	EventTypeEmailDomainChanged         EventType = "email_domain_changed"
	EventTypeBotAdded                   EventType = "bot_added"
	EventTypeBotChanged                 EventType = "bot_changed"
	EventTypeAccountsChanged            EventType = "accounts_changed"
	EventTypeTeamMigrationStarted       EventType = "team_migration_started"
)

type Bot struct {
	ws       *websocket.Conn
	config   Config
	handlers map[EventType]handlerFunc
	id       int
	messages map[int]Message
}

type handlerFunc func(*Bot, []byte) error
type MessageFunc func(*Bot, *Message) error
type EventType string

type Timestamp time.Time

func (t *Timestamp) MarshalJSON() ([]byte, error) {
	ts := time.Time(*t).Unix()
	stamp := fmt.Sprint(ts)
	return []byte(stamp), nil
}

func (t *Timestamp) UnmarshalJSON(b []byte) error {
	var ts float64
	buf := bytes.NewReader(b)
	err := binary.Read(buf, binary.LittleEndian, &ts)
	fmt.Println(err)
	fmt.Println(ts)
	if err != nil {
		return err
	}
	*t = Timestamp(time.Unix(0, int64(ts*1e9)))
	return nil
}

type Event struct {
	Id        int       `json:"id"`
	Type      string    `json:"type"`
	Channel   string    `json:"channel"`
	User      string    `json:"user"`
	Timestamp Timestamp `json:"ts"`
}

type Message struct {
	Id          int          `json:"id"`
	Type        string       `json:"type"`
	Channel     string       `json:"channel"`
	User        string       `json:"user"`
	Username    string       `json:"username"`
	BotId       string       `json:"bot_id"`
	Text        string       `json:"text"`
	Timestamp   Timestamp    `json:"ts"`
	Attachments []Attachment `json:"attachments"`
}

type Attachment struct {
	Fallback    string `json:"fallback"`
	ImageWidth  int    `json:"image_width"`
	ImageHeight int    `json:"image_height"`
	ImageBytes  int    `json:"image_bytes"`
	AuthorName  string `json:"author_name"`
	Id          int    `json:"id"`
	TitleLink   string `json:"title_link"`
	FromUrl     string `json:"from_url"`
	ImageUrl    string `json:"image_url"`
	Text        string `json:"text"`
	Title       string `json:"title"`
	AuthorLink  string `json:"author_link"`
	Type        string `json:"type"`
	Subtype     string `json:"subtype"`
	Channel     string `json:"channel"`
}

func MessageHandler(fn MessageFunc) handlerFunc {
	return func(b *Bot, data []byte) error {
		var message Message
		if err := json.Unmarshal(data, &message); err != nil {
			return err
		}
		err := fn(b, &message)
		return err
	}
}

func (b *Bot) SetHandler(t EventType, fn handlerFunc) {
	b.handlers[t] = fn
}

func (b *Bot) SetMessageHandler(fn MessageFunc) {
	b.SetHandler(EventTypeMessage, MessageHandler(fn))
}

func (b *Bot) reconnect() error {
	var err error

	for {
		log.Printf("Connect failed with err %s, reconnecting.\n", err)

		if err = b.connect(); err == nil {
			break
		}

		time.Sleep(15 * time.Second)

	}

	return err
}

var ErrNotSupported = errors.New("Not supported")

func (b *Bot) receive() ([]byte, error) {
	_, r, err := b.ws.NextReader()
	if err != nil {
		return nil, err
	}

	data, err := ioutil.ReadAll(r)
	if err == io.EOF {
		// Decode returns io.EOF when the message is empty or all whitespace.
		// Convert to io.ErrUnexpectedEOF so that application can distinguish
		// between an error reading the JSON value and the connection closing.
		err = io.ErrUnexpectedEOF
	}
	return data, err
}

func (b *Bot) NewMessage() Message {
	message := Message{}
	message.Id = b.id

	b.id++

	message.Type = "message"
	return message
}

func (b *Bot) Send(message Message) error {
	err := b.ws.WriteJSON(message)
	return err
}

func (b *Bot) Run() error {
	for {
		var event Event

		data, err := b.receive()

		err = json.Unmarshal(data, &event)
		if err == io.ErrUnexpectedEOF {
			err = b.reconnect()
			continue
		} else if err != nil {
			return err
		}

		fmt.Println("Received %#v", event)
		fmt.Println(string(data))

		if fn, ok := b.handlers[EventType(event.Type)]; ok {
			err := fn(b, data)
			if err != nil {
				log.Printf("Error %s\n", err)
				continue
			}
		} else {
			log.Printf("No handler found for type %s %#v.\n", event.Type, data)
		}
	}
}

type Config struct {
	PayloadToken string
	Token        string
	Origin       string
}

func (b *Bot) connect() error {
	v := url.Values{}
	v.Set("token", b.config.Token)

	resp, err := http.PostForm("https://slack.com/api/rtm.start", v)
	if err != nil {
		return err
	}

	var response struct {
		Ok    bool   `json:"ok"`
		Url   string `json:"url"`
		Error string `json:"error"`
	}

	err = json.NewDecoder(resp.Body).Decode(&response)
	if err != nil {
		return err
	}

	if !response.Ok {
		return errors.New(response.Error)
	}

	header := http.Header{
		"Origin": {b.config.Origin},
		// your milage may differ
		"Sec-WebSocket-Extensions": {"permessage-deflate; client_max_window_bits, x-webkit-deflate-frame"},
	}

	b.ws, _, err = websocket.DefaultDialer.Dial(response.Url, header)
	if err != nil {
		return err
	}

	return nil
}

func NewBot(config Config) (*Bot, error) {
	bot := &Bot{config: config, id: 1, handlers: map[EventType]handlerFunc{}}

	if err := bot.connect(); err != nil {
		return nil, err
	}

	return bot, nil
}
<commit_msg>fixed issue of halting on improper json decode<commit_after>package slackbot

import (
	"bytes"
	"encoding/binary"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
	"strconv"
	"time"

	"github.com/gorilla/websocket"
)

const (
	EventTypeMessage                    EventType = "message"
	EventTypeHello                      EventType = "hello"
	EventTypeChannelMarked              EventType = "channel_marked"
	EventTypeChannelCreated             EventType = "channel_created"
	EventTypeChannelJoined              EventType = "channel_joined"
	EventTypeChannelLeft                EventType = "channel_left"
	EventTypeChannelDeleted             EventType = "channel_deleted"
	EventTypeChannelRename              EventType = "channel_rename"
	EventTypeChannelArchive             EventType = "channel_archive"
	EventTypeChannelUnarchive           EventType = "channel_unarchive"
	EventTypeChannelHistoryChanged      EventType = "channel_history_changed"
	EventTypeChannelImCreated           EventType = "im_created"
	EventTypeChannelImOpen              EventType = "im_open"
	EventTypeChannelImClose             EventType = "im_close"
	EventTypeChannelImMarked            EventType = "im_marked"
	EventTypeChannelImHistoryChanged    EventType = "im_history_changed"
	EventTypeChannelGroupJoined         EventType = "group_joined"
	EventTypeChannelGroupLeft           EventType = "group_left"
	EventTypeChannelGroupOpen           EventType = "group_open"
	EventTypeChannelGroupClose          EventType = "group_close"
	EventTypeChannelGroupArchive        EventType = "group_archive"
	EventTypeChannelGroupUnarchive      EventType = "group_unarchive"
	EventTypeChannelGroupRename         EventType = "group_rename"
	EventTypeChannelGroupMarked         EventType = "group_marked"
	EventTypeChannelGroupHistoryChanged EventType = "group_history_changed"
	EventTypeFileCreated                EventType = "file_created"
	EventTypeFileShared                 EventType = "file_shared"
	EventTypeFileUnshared               EventType = "file_unshared"
	EventTypeFilePublic                 EventType = "file_public"
	EventTypeFilePrivate                EventType = "file_private"
	EventTypeFileChange                 EventType = "file_change"
	EventTypeFileDeleted                EventType = "file_deleted"
	EventTypeFileCommentAdded           EventType = "file_comment_added"
	EventTypeFileCommentEdited          EventType = "file_comment_edited"
	EventTypeFileCommentDeleted         EventType = "file_comment_deleted"
	EventTypePresenceChange             EventType = "presence_change"
	EventTypeManualPresenceChange       EventType = "manual_presence_change"
	EventTypePrefChange                 EventType = "pref_change"
	EventTypeUserChange                 EventType = "user_change"
	EventTypeTeamJoin                   EventType = "team_join"
	EventTypeStarAdded                  EventType = "star_added"
	EventTypeStarRemoved                EventType = "star_removed"
	EventTypeEmojiChanged               EventType = "emoji_changed"
	EventTypeCommandsChanged            EventType = "commands_changed"
	EventTypeTeamPrefChange             EventType = "team_pref_change"
	EventTypeTeamRename                 EventType = "team_rename"
	EventTypeTeamDomainChange           EventType = "team_domain_change"
	EventTypeEmailDomainChanged         EventType = "email_domain_changed"
	EventTypeBotAdded                   EventType = "bot_added"
	EventTypeBotChanged                 EventType = "bot_changed"
	EventTypeAccountsChanged            EventType = "accounts_changed"
	EventTypeTeamMigrationStarted       EventType = "team_migration_started"
)

type Bot struct {
	ws       *websocket.Conn
	config   Config
	handlers map[EventType]handlerFunc
	id       int
	messages map[int]Message
}

type handlerFunc func(*Bot, []byte) error
type MessageFunc func(*Bot, *Message) error
type EventType string

type Timestamp time.Time

func (t *Timestamp) MarshalJSON() ([]byte, error) {
	/*
		ts := time.Time(*t).Unix()
		stamp := fmt.Sprint(ts)
		return []byte(stamp), nil
	*/
	return []byte(t.Format(`"` + time.RFC3339Nano + `"`)), nil
}

func (t *Timestamp) UnmarshalJSON(b []byte) error {
	var ts float64
	var err error

	// check if string representation
	if b[0] == '"' {
		ts, err = strconv.ParseFloat(string(b[1:len(b)-2]), 64)
	} else {
		buf := bytes.NewReader(b)
		err = binary.Read(buf, binary.LittleEndian, &ts)
	}

	if err != nil {
		return err
	}
	*t = Timestamp(time.Unix(0, int64(ts*1e9)))
	return nil
}

type Event struct {
	Id        int       `json:"id,omitempty"`
	Type      string    `json:"type,omitempty"`
	Channel   string    `json:"channel,omitempty"`
	User      string    `json:"user,omitempty"`
	Timestamp Timestamp `json:"ts,omitempty"`
}

type Message struct {
	Id          int          `json:"id,omitempty"`
	Type        string       `json:"type,omitempty"`
	Channel     string       `json:"channel,omitempty"`
	User        string       `json:"user,omitempty"`
	Username    string       `json:"username,omitempty"`
	BotId       string       `json:"bot_id,omitempty"`
	Text        string       `json:"text,omitempty"`
	Timestamp   Timestamp    `json:"ts,omitempty"`
	Attachments []Attachment `json:"attachments,omitempty"`
}

type Attachment struct {
	Fallback    string `json:"fallback,omitempty"`
	ImageWidth  int    `json:"image_width,omitempty"`
	ImageHeight int    `json:"image_height,omitempty"`
	ImageBytes  int    `json:"image_bytes,omitempty"`
	AuthorName  string `json:"author_name,omitempty"`
	Id          int    `json:"id,omitempty"`
	TitleLink   string `json:"title_link,omitempty"`
	FromUrl     string `json:"from_url,omitempty"`
	ImageUrl    string `json:"image_url,omitempty"`
	Text        string `json:"text,omitempty"`
	Title       string `json:"title,omitempty"`
	AuthorLink  string `json:"author_link,omitempty"`
	Type        string `json:"type,omitempty"`
	Subtype     string `json:"subtype,omitempty"`
	Channel     string `json:"channel,omitempty"`
}

func MessageHandler(fn MessageFunc) handlerFunc {
	return func(b *Bot, data []byte) error {
		var message Message
		if err := json.Unmarshal(data, &message); err != nil {
			return err
		}
		err := fn(b, &message)
		return err
	}
}

func (b *Bot) SetHandler(t EventType, fn handlerFunc) {
	b.handlers[t] = fn
}

func (b *Bot) SetMessageHandler(fn MessageFunc) {
	b.SetHandler(EventTypeMessage, MessageHandler(fn))
}

func (b *Bot) reconnect() error {
	var err error

	for {
		log.Printf("Connect failed with err %s, reconnecting.\n", err)

		if err = b.connect(); err == nil {
			break
		}

		time.Sleep(15 * time.Second)

	}

	return err
}

var ErrNotSupported = errors.New("Not supported")

func (b *Bot) receive() ([]byte, error) {
	_, r, err := b.ws.NextReader()
	if err != nil {
		return nil, err
	}

	data, err := ioutil.ReadAll(r)
	if err == io.EOF {
		// Decode returns io.EOF when the message is empty or all whitespace.
		// Convert to io.ErrUnexpectedEOF so that application can distinguish
		// between an error reading the JSON value and the connection closing.
		err = io.ErrUnexpectedEOF
	}
	return data, err
}

func (b *Bot) NewMessage() Message {
	message := Message{}
	message.Id = b.id

	b.id++

	message.Type = "message"
	return message
}

func (b *Bot) Send(message Message) error {
	err := b.ws.WriteJSON(message)
	return err
}

func (b *Bot) Run() error {
	for {
		var event Event

		data, err := b.receive()
		if err == io.ErrUnexpectedEOF {
			err = b.reconnect()
			continue
		} else if err != nil {
			return err
		}

		if err = json.Unmarshal(data, &event); err != nil {
			// ignore decoding errors
			fmt.Println("Could not decode: ", string(data))
			continue
		}

		fmt.Println("Received %#v", event)

		if fn, ok := b.handlers[EventType(event.Type)]; ok {
			err := fn(b, data)
			if err != nil {
				log.Printf("Error %s\n", err)
				continue
			}
		} else {
			log.Printf("No handler found for type %s %#v.\n", event.Type, data)
		}
	}
}

type Config struct {
	PayloadToken string
	Token        string
	Origin       string
}

func (b *Bot) connect() error {
	v := url.Values{}
	v.Set("token", b.config.Token)

	resp, err := http.PostForm("https://slack.com/api/rtm.start", v)
	if err != nil {
		return err
	}

	var response struct {
		Ok    bool   `json:"ok"`
		Url   string `json:"url"`
		Error string `json:"error"`
	}

	err = json.NewDecoder(resp.Body).Decode(&response)
	if err != nil {
		return err
	}

	if !response.Ok {
		return errors.New(response.Error)
	}

	header := http.Header{
		"Origin": {b.config.Origin},
		// your milage may differ
		"Sec-WebSocket-Extensions": {"permessage-deflate; client_max_window_bits, x-webkit-deflate-frame"},
	}

	b.ws, _, err = websocket.DefaultDialer.Dial(response.Url, header)
	if err != nil {
		return err
	}

	return nil
}

func NewBot(config Config) (*Bot, error) {
	bot := &Bot{config: config, id: 1, handlers: map[EventType]handlerFunc{}}

	if err := bot.connect(); err != nil {
		return nil, err
	}

	return bot, nil
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"html"
	"log"
	"net/http"
	"strings"

	"github.com/go-telegram-bot-api/telegram-bot-api"
)

var bot *tgbotapi.BotAPI

func botRun() error {
	var err error
	bot, err = tgbotapi.NewBotAPI(cfg.Bot.Token)
	if err != nil {
		log.Fatal(err)
	}

	bot.Debug = cfg.Bot.Debug

	log.Println("Authorized on account:", bot.Self.UserName)

	_, err = bot.SetWebhook(tgbotapi.NewWebhookWithCert(fmt.Sprintf("%s%s/%s", cfg.HTTP.Host, cfg.HTTP.Port, cfg.Bot.Token), cfg.HTTP.PublicKey))
	if err != nil {
		log.Fatal(err)
	}

	updates := bot.ListenForWebhook(fmt.Sprintf("/%s", bot.Token))
	go func() {
		if err := http.ListenAndServeTLS(cfg.HTTP.Port, cfg.HTTP.PublicKey, cfg.HTTP.PrivateKey, nil); err != nil {
			log.Fatal(err)
		}
	}()

	for update := range updates {
		msgRouter(update)
	}
	return nil
}

func msgRouter(update tgbotapi.Update) error {
	switch {
	case update.InlineQuery != nil:
		return isInline(update)
	case update.Message != nil && update.Message.IsCommand():
		return isCommand(update)
	case update.Message != nil && (update.Message.Chat.IsPrivate() || bot.IsMessageToMe(*update.Message)):
		return isSearch(update)
	}
	return nil
}

func isCommand(update tgbotapi.Update) error {
	switch update.Message.Command() {
	case "s":
		return isSearch(update)
	case "daily":
		return isDaily(update)
	default:
		return sendMsg(update, HelpMsg)
	}
	return nil
}

func isSearch(update tgbotapi.Update) error {
	var msg string
	if update.Message.IsCommand() {
		msg = update.Message.CommandArguments()
	} else {
		msg = update.Message.Text
	}
	msg = strings.Trim(msg, " ")
	if msg == "" {
		return sendMsg(update, HelpMsg)
	}

	results, err := search(update.Message.Text, cfg.Zhihu.SearchResultNum)
	if err != nil {
		return err
	}

	msg = ""
	for _, result := range results {
		msg = fmt.Sprintf(`%s<a href="%s/%s">%s</a><br>%s <a href="%s/%s">...显示全部</a><br><br>`,
			msg, cfg.Zhihu.Host, result.QuestionLink, result.Title, html.EscapeString(result.Summary), cfg.Zhihu.Host, result.AnswerLink)
	}
	msg = format(msg)
	return sendMsg(update, msg)
}

func isInline(update tgbotapi.Update) error {
	msg := update.InlineQuery.Query
	results, err := search(msg, cfg.Zhihu.InlineResultNum)
	if err != nil {
		return err
	}
	var answers []interface{}
	for _, result := range results {
		msg = fmt.Sprintf(`<a href="%s/%s">%s</a><br>%s <br><a href="%s/%s">查看原文</a><br>`,
			cfg.Zhihu.Host, result.QuestionLink, result.Title, html.EscapeString(result.Summary), cfg.Zhihu.Host, result.AnswerLink)
		answer := tgbotapi.NewInlineQueryResultArticleHTML(result.QuestionLink, result.Title, msg)
		answers = append(answers, &answer)
	}
	return answerInlineQuery(update, answers)
}

func isDaily(update tgbotapi.Update) error {
	txt, err := daily()
	if err != nil {
		return err
	}
	return sendMsg(update, txt)
}

func sendMsg(update tgbotapi.Update, txt string) error {
	msg := tgbotapi.NewMessage(update.Message.Chat.ID, txt)
	msg.ParseMode = "HTML"
	msg.DisableWebPagePreview = true
	if _, err := bot.Send(msg); err != nil {
		log.Println("bot.Send:", err)
		return err
	}
	return nil
}

func answerInlineQuery(update tgbotapi.Update, results []interface{}) error {
	answer := tgbotapi.InlineConfig{
		InlineQueryID: update.InlineQuery.ID,
		IsPersonal:    true,
		CacheTime:     0,
		Results:       results,
	}
	if _, err := bot.AnswerInlineQuery(answer); err != nil {
		log.Println("bot.answerInlineQuery:", err)
		return err
	}
	return nil
}
<commit_msg>update<commit_after>package main

import (
	"fmt"
	"html"
	"log"
	"net/http"
	"strings"

	"github.com/go-telegram-bot-api/telegram-bot-api"
)

var bot *tgbotapi.BotAPI

func botRun() error {
	var err error
	bot, err = tgbotapi.NewBotAPI(cfg.Bot.Token)
	if err != nil {
		log.Fatal(err)
	}

	bot.Debug = cfg.Bot.Debug

	log.Println("Authorized on account:", bot.Self.UserName)

	_, err = bot.SetWebhook(tgbotapi.NewWebhookWithCert(fmt.Sprintf("%s%s/%s", cfg.HTTP.Host, cfg.HTTP.Port, cfg.Bot.Token), cfg.HTTP.PublicKey))
	if err != nil {
		log.Fatal(err)
	}

	updates := bot.ListenForWebhook(fmt.Sprintf("/%s", bot.Token))
	go func() {
		if err := http.ListenAndServeTLS(cfg.HTTP.Port, cfg.HTTP.PublicKey, cfg.HTTP.PrivateKey, nil); err != nil {
			log.Fatal(err)
		}
	}()

	for update := range updates {
		msgRouter(update)
	}
	return nil
}

func msgRouter(update tgbotapi.Update) error {
	switch {
	case update.InlineQuery != nil:
		return isInline(update)
	case update.Message != nil && update.Message.IsCommand():
		return isCommand(update)
	case update.Message != nil && (update.Message.Chat.IsPrivate() || bot.IsMessageToMe(*update.Message)):
		return isSearch(update)
	}
	return nil
}

func isCommand(update tgbotapi.Update) error {
	switch update.Message.Command() {
	case "s":
		return isSearch(update)
	case "daily":
		return isDaily(update)
	default:
		return sendMsg(update, HelpMsg)
	}
	return nil
}

func isSearch(update tgbotapi.Update) error {
	var msg string
	if update.Message.IsCommand() {
		msg = update.Message.CommandArguments()
	} else {
		msg = update.Message.Text
	}
	msg = strings.Trim(msg, " ")
	if msg == "" {
		return sendMsg(update, HelpMsg)
	}

	results, err := search(update.Message.Text, cfg.Zhihu.SearchResultNum)
	if err != nil {
		return err
	}

	msg = ""
	for _, result := range results {
		msg = fmt.Sprintf(`%s<a href="%s/%s">%s</a><br>%s <a href="%s/%s">...显示全部</a><br><br>`,
			msg, cfg.Zhihu.Host, result.QuestionLink, result.Title, html.EscapeString(result.Summary), cfg.Zhihu.Host, result.AnswerLink)
	}
	msg = format(msg)
	return sendMsg(update, msg)
}

func isInline(update tgbotapi.Update) error {
	msg := update.InlineQuery.Query
	results, err := search(msg, cfg.Zhihu.InlineResultNum)
	if err != nil {
		return err
	}
	var answers []interface{}
	for _, result := range results {
		msg = fmt.Sprintf(`<a href="%s/%s">%s</a><br>%s <br><a href="%s/%s">查看原文</a><br>`,
			cfg.Zhihu.Host, result.QuestionLink, result.Title, html.EscapeString(result.Summary), cfg.Zhihu.Host, result.AnswerLink)
		msg = format(msg)
		answer := tgbotapi.NewInlineQueryResultArticleHTML(result.QuestionLink, result.Title, msg)
		answers = append(answers, &answer)
	}
	return answerInlineQuery(update, answers)
}

func isDaily(update tgbotapi.Update) error {
	txt, err := daily()
	if err != nil {
		return err
	}
	return sendMsg(update, txt)
}

func sendMsg(update tgbotapi.Update, txt string) error {
	msg := tgbotapi.NewMessage(update.Message.Chat.ID, txt)
	msg.ParseMode = "HTML"
	msg.DisableWebPagePreview = true
	if _, err := bot.Send(msg); err != nil {
		log.Println("bot.Send:", err)
		return err
	}
	return nil
}

func answerInlineQuery(update tgbotapi.Update, results []interface{}) error {
	answer := tgbotapi.InlineConfig{
		InlineQueryID: update.InlineQuery.ID,
		IsPersonal:    true,
		CacheTime:     0,
		Results:       results,
	}
	if _, err := bot.AnswerInlineQuery(answer); err != nil {
		log.Println("bot.answerInlineQuery:", err)
		return err
	}
	return nil
}
<|endoftext|>


<commit_before>package main

import (
	"strings"
	"sync"
	"time"
)

type bot struct {
	sync.Mutex
	ID              string
	pass            string
	nick            string
	read            chan string
	toClient        chan string
	join            chan string
	channels        map[string][]*connection
	readconns       []*connection
	sendconns       []*connection
	whisperconn     *connection
	ticker          *time.Ticker
	clientConnected bool
	client          *Client
}

func newBot(client *Client) *bot {
	return &bot{
		read:      make(chan string, 10),
		join:      make(chan string, 50000),
		channels:  make(map[string][]*connection),
		readconns: make([]*connection, 0),
		sendconns: make([]*connection, 0),
		ticker:    time.NewTicker(1 * time.Minute),
		client:    client,
	}
}

func (bot *bot) Init() {
	go bot.joinChannels()
	go bot.checkConnections()
	bot.newConn(connReadConn)
	// twitch changed something about whispers or there is some black magic going on,
	// but its only reading whispers once even with more connections
	bot.newConn(connWhisperConn)
}

// close all connections and delete bot
func (bot *bot) close() {
	bot.Lock()
	bot.ticker.Stop()
	close(bot.read)
	close(bot.join)
	for _, conn := range bot.readconns {
		conn.conntype = connDelete
		conn.close()
	}
	for _, conn := range bot.sendconns {
		conn.conntype = connDelete
		conn.close()
	}
	bot.whisperconn.conntype = connDelete
	bot.whisperconn.close()
	for k := range bot.channels {
		delete(bot.channels, k)
	}
	Log.Info("CLOSED BOT", bot.nick)
	bot.Unlock()
}

func (bot *bot) checkConnections() {
	for _ = range bot.ticker.C {
		for _, co := range bot.readconns {
			conn := co
			conn.active = false
			err := conn.send("PING")
			if err != nil {
				Log.Error(err.Error())
				conn.restore()
				conn.close()
			}
			go func() {
				time.Sleep(10 * time.Second)
				if !conn.active {
					Log.Info("read connection died, reconnecting...")
					conn.restore()
					conn.close()
				}
			}()
		}
		for _, co := range bot.sendconns {
			conn := co
			if time.Since(conn.lastUse) < time.Minute*10 {
				// close unused connections
				conn.active = false
				err := conn.send("PING")
				if err != nil {
					Log.Error(err.Error())
					conn.restore()
					conn.close()
				} else {
					go func() {
						time.Sleep(10 * time.Second)
						if !conn.active {
							Log.Info("send connection died, closing...")
							conn.restore()
							conn.close()
						}
					}()
				}
			} else {
				if len(bot.sendconns) > 2 {
					Log.Info("closing unused connection")
					conn.restore()
					conn.close()
				} else {
					conn.lastUse = time.Now()
				}
			}
		}
		go func() {
			err := bot.whisperconn.send("PING")
			if err != nil {
				Log.Error(err.Error())
				bot.whisperconn.restore()
			}
			time.Sleep(10 * time.Second)
			if !bot.whisperconn.active {
				bot.newConn(connWhisperConn)
			}
		}()
		for channel := range bot.channels {
			if conns, ok := bot.channels[channel]; ok && len(conns) < 1 {
				bot.join <- channel
			}
		}
	}
}

func (bot *bot) partChannel(channel string) {
	channel = strings.ToLower(channel)
	if conns, ok := bot.channels[channel]; ok {
		for _, conn := range conns {
			err := conn.send("PART " + channel)
			if err != nil {
				Log.Error(err.Error())
				conn.restore()
				conn.close()
				bot.partChannel(channel)
				return
			}
			conn.part(channel)
		}
		Log.Infof("left channel on %d connections\n", len(conns))
		delete(bot.channels, channel)
		return
	}
	Log.Error("never joined ", channel)
}

func (bot *bot) joinChannels() {
	for channel := range bot.join {
		Log.Debug(channel)
		go bot.joinChannel(channel)
		<-joinTicker.C
	}
}

func (bot *bot) joinChannel(channel string) {
	channel = strings.ToLower(channel)
	if conns, ok := bot.channels[channel]; ok && len(conns) > 0 {
		// TODO: check msg ids and join channels more than one time
		Log.Info("already joined channel", channel)
		return
	}
	var conn *connection
	for _, c := range bot.readconns {
		if len(c.joins) < 50 {
			conn = c
			break
		}
	}
	if conn == nil {
		bot.newConn(connReadConn)
		bot.joinChannel(channel)
		return
	}
	for !conn.active {
		time.Sleep(100 * time.Millisecond)
	}
	err := conn.send("JOIN " + channel)
	if err != nil {
		Log.Error(err.Error())
		conn.restore()
		conn.close()
		bot.join <- channel
		return
	}
	if _, ok := bot.channels[channel]; !ok {
		bot.channels[channel] = make([]*connection, 0)
	}
	conn.joins = append(conn.joins, channel)
	bot.channels[channel] = append(bot.channels[channel], conn)
	Log.Info("joined channel", channel)

}

func (bot *bot) newConn(t connType) {
	switch t {
	case connReadConn:
		conn := newConnection(t)
		go conn.connect(bot.client, bot.pass, bot.nick)
		bot.readconns = append(bot.readconns, conn)
	case connSendConn:
		conn := newConnection(t)
		go conn.connect(bot.client, bot.pass, bot.nick)
		bot.sendconns = append(bot.sendconns, conn)
	case connWhisperConn:
		if bot.whisperconn != nil {
			bot.whisperconn.close()
		}
		conn := newConnection(t)
		go conn.connect(bot.client, bot.pass, bot.nick)
		bot.whisperconn = conn
	}
	Log.Debug("NEW CONN", t)
}

func (bot *bot) readChat() {
	for msg := range bot.toClient {
		bot.read <- msg
	}
}

func (bot *bot) say(msg string) {
	var conn *connection
	var min = 15
	// find connection with the least sent messages
	for _, c := range bot.sendconns {
		if c.msgCount < min {
			conn = c
			min = conn.msgCount
		}
	}
	if conn == nil || min > 10 {
		bot.newConn(connSendConn)
		Log.Infof("created new conn, total: %d\n", len(bot.sendconns))
		bot.say(msg)
		return
	}
	// add to msg counter before waiting to stop other go routines from sending on this connection
	conn.countMsg()
	for !conn.active {
		time.Sleep(100 * time.Millisecond)
	}
	conn.lastUse = time.Now()
	err := conn.send("PRIVMSG " + msg)
	if err != nil {
		Log.Error(err.Error())
		conn.restore()
		conn.close()
		bot.say(msg)
		return
	}
	Log.Debugf("%p   %d\n", conn, conn.msgCount)
	Log.Info("sent:", msg)
}

func (bot *bot) handleMessage(spl []string) {
	msg := spl[1]
	switch spl[0] {
	case "JOIN":
		bot.join <- strings.ToLower(msg)
	case "PART":
		bot.partChannel(strings.ToLower(msg))
	case "PRIVMSG":
		bot.say(msg)
	default:
		Log.Warning("unhandled message", spl[0], msg)
	}
}
<commit_msg>speedup new connections<commit_after>package main

import (
	"strings"
	"sync"
	"time"
)

type bot struct {
	sync.Mutex
	ID              string
	pass            string
	nick            string
	read            chan string
	toClient        chan string
	join            chan string
	channels        map[string][]*connection
	readconns       []*connection
	sendconns       []*connection
	whisperconn     *connection
	ticker          *time.Ticker
	clientConnected bool
	client          *Client
}

func newBot(client *Client) *bot {
	return &bot{
		read:      make(chan string, 10),
		join:      make(chan string, 50000),
		channels:  make(map[string][]*connection),
		readconns: make([]*connection, 0),
		sendconns: make([]*connection, 0),
		ticker:    time.NewTicker(1 * time.Minute),
		client:    client,
	}
}

func (bot *bot) Init() {
	go bot.joinChannels()
	go bot.checkConnections()
	bot.newConn(connReadConn)
	// twitch changed something about whispers or there is some black magic going on,
	// but its only reading whispers once even with more connections
	bot.newConn(connWhisperConn)
}

// close all connections and delete bot
func (bot *bot) close() {
	bot.Lock()
	bot.ticker.Stop()
	close(bot.read)
	close(bot.join)
	for _, conn := range bot.readconns {
		conn.conntype = connDelete
		conn.close()
	}
	for _, conn := range bot.sendconns {
		conn.conntype = connDelete
		conn.close()
	}
	bot.whisperconn.conntype = connDelete
	bot.whisperconn.close()
	for k := range bot.channels {
		delete(bot.channels, k)
	}
	Log.Info("CLOSED BOT", bot.nick)
	bot.Unlock()
}

func (bot *bot) checkConnections() {
	for _ = range bot.ticker.C {
		for _, co := range bot.readconns {
			conn := co
			conn.active = false
			err := conn.send("PING")
			if err != nil {
				Log.Error(err.Error())
				conn.restore()
				conn.close()
			}
			go func() {
				time.Sleep(10 * time.Second)
				if !conn.active {
					Log.Info("read connection died, reconnecting...")
					conn.restore()
					conn.close()
				}
			}()
		}
		for _, co := range bot.sendconns {
			conn := co
			if time.Since(conn.lastUse) < time.Minute*10 {
				// close unused connections
				conn.active = false
				err := conn.send("PING")
				if err != nil {
					Log.Error(err.Error())
					conn.restore()
					conn.close()
				} else {
					go func() {
						time.Sleep(10 * time.Second)
						if !conn.active {
							Log.Info("send connection died, closing...")
							conn.restore()
							conn.close()
						}
					}()
				}
			} else {
				if len(bot.sendconns) > 2 {
					Log.Info("closing unused connection")
					conn.restore()
					conn.close()
				} else {
					conn.lastUse = time.Now()
				}
			}
		}
		go func() {
			err := bot.whisperconn.send("PING")
			if err != nil {
				Log.Error(err.Error())
				bot.whisperconn.restore()
			}
			time.Sleep(10 * time.Second)
			if !bot.whisperconn.active {
				bot.newConn(connWhisperConn)
			}
		}()
		for channel := range bot.channels {
			if conns, ok := bot.channels[channel]; ok && len(conns) < 1 {
				bot.join <- channel
			}
		}
	}
}

func (bot *bot) partChannel(channel string) {
	channel = strings.ToLower(channel)
	if conns, ok := bot.channels[channel]; ok {
		for _, conn := range conns {
			err := conn.send("PART " + channel)
			if err != nil {
				Log.Error(err.Error())
				conn.restore()
				conn.close()
				bot.partChannel(channel)
				return
			}
			conn.part(channel)
		}
		Log.Infof("left channel on %d connections\n", len(conns))
		delete(bot.channels, channel)
		return
	}
	Log.Error("never joined ", channel)
}

func (bot *bot) joinChannels() {
	for channel := range bot.join {
		Log.Debug(channel)
		go bot.joinChannel(channel)
		<-joinTicker.C
	}
}

func (bot *bot) joinChannel(channel string) {
	channel = strings.ToLower(channel)
	if conns, ok := bot.channels[channel]; ok && len(conns) > 0 {
		// TODO: check msg ids and join channels more than one time
		Log.Info("already joined channel", channel)
		return
	}
	var conn *connection
	for _, c := range bot.readconns {
		if len(c.joins) < 50 {
			conn = c
			break
		}
	}
	if conn == nil {
		bot.newConn(connReadConn)
		bot.joinChannel(channel)
		return
	}
	for !conn.active {
		time.Sleep(time.Millisecond)
	}
	err := conn.send("JOIN " + channel)
	if err != nil {
		Log.Error(err.Error())
		conn.restore()
		conn.close()
		bot.join <- channel
		return
	}
	if _, ok := bot.channels[channel]; !ok {
		bot.channels[channel] = make([]*connection, 0)
	}
	conn.joins = append(conn.joins, channel)
	bot.channels[channel] = append(bot.channels[channel], conn)
	Log.Info("joined channel", channel)

}

func (bot *bot) newConn(t connType) {
	switch t {
	case connReadConn:
		conn := newConnection(t)
		go conn.connect(bot.client, bot.pass, bot.nick)
		bot.readconns = append(bot.readconns, conn)
	case connSendConn:
		conn := newConnection(t)
		go conn.connect(bot.client, bot.pass, bot.nick)
		bot.sendconns = append(bot.sendconns, conn)
	case connWhisperConn:
		if bot.whisperconn != nil {
			bot.whisperconn.close()
		}
		conn := newConnection(t)
		go conn.connect(bot.client, bot.pass, bot.nick)
		bot.whisperconn = conn
	}
	Log.Debug("NEW CONN", t)
}

func (bot *bot) readChat() {
	for msg := range bot.toClient {
		bot.read <- msg
	}
}

func (bot *bot) say(msg string) {
	var conn *connection
	var min = 15
	// find connection with the least sent messages
	for _, c := range bot.sendconns {
		if c.msgCount < min {
			conn = c
			min = conn.msgCount
		}
	}
	if conn == nil || min > 10 {
		bot.newConn(connSendConn)
		Log.Infof("created new conn, total: %d\n", len(bot.sendconns))
		bot.say(msg)
		return
	}
	// add to msg counter before waiting to stop other go routines from sending on this connection
	conn.countMsg()
	for !conn.active {
		time.Sleep(100 * time.Millisecond)
	}
	conn.lastUse = time.Now()
	err := conn.send("PRIVMSG " + msg)
	if err != nil {
		Log.Error(err.Error())
		conn.restore()
		conn.close()
		bot.say(msg)
		return
	}
	Log.Debugf("%p   %d\n", conn, conn.msgCount)
	Log.Info("sent:", msg)
}

func (bot *bot) handleMessage(spl []string) {
	msg := spl[1]
	switch spl[0] {
	case "JOIN":
		bot.join <- strings.ToLower(msg)
	case "PART":
		bot.partChannel(strings.ToLower(msg))
	case "PRIVMSG":
		bot.say(msg)
	default:
		Log.Warning("unhandled message", spl[0], msg)
	}
}
<|endoftext|>