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

// Package girc provides a high level, yet flexible IRC library for use
// with interacting with IRC servers. girc has support for user/channel
// tracking, as well as a few other neat features (like auto-reconnect).
//
// Much of what girc can do, can also be disabled. The goal is to
// provide a solid API that you don't necessarily have to work with out
// of the box if you don't want to.
//
// See "example/main.go" for a brief and very useful example taking
// advantage of girc, that should give you a general idea of how the API
// works.
package girc

import (
	"crypto/tls"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net"
	"time"
)

// TODO's:
//   * ClearCallbacks(CODE)?
//   * Should Client.Message() an other similar methods support errors?
//     * along with this, should we forcefully check to ensure that the target
//       of the events are valid?
//   * track connection time (conntime? in state)
//   * with conntime, find lag. Client.Lag() would be useful
//   * would be cool to track things like SERVERNAME, VERSION, UMODES,
//     CMODES, etc. also see Config.DisableCapTracking.
//       -- https://github.com/Liamraystanley/Code/blob/master/core/triggers.py#L40-L67
//   * client should support ping tracking (sending PING's to the server)
//   * users need to be exposed in state somehow (other than GetChannels())
//   * ip/host binding?
//   * IsValidNick?
//   * User.Age()? (FirstActive()?) (time since first seen)
//   * cleanup docs in conn.go & event.go.

// Client contains all of the information necessary to run a single IRC
// client.
type Client struct {
	// Config represents the configuration
	Config Config
	// Events is a buffer of events waiting to be processed.
	Events chan *Event
	// Sender is a Sender{} interface implementation.
	Sender Sender

	// state represents the internal state
	state *state
	// initTime represents the creation time of the client.
	initTime time.Time
	// callbacks is an internal mapping of COMMAND -> callback.
	callbacks map[string][]Callback
	// reader is the socket buffer reader from the IRC server.
	reader *Decoder
	// reader is the socket buffer write to the IRC server.
	writer *Encoder
	// conn is a net.Conn reference to the IRC server.
	conn net.Conn
	// tries represents the internal reconnect count to the IRC server.
	tries int
	// log is used if a writer is supplied for Client.Config.Logger.
	log *log.Logger
	// quitChan is used to close the connection to the IRC server.
	quitChan chan bool
	// hasQuit is used to determine if we've finished quitting/cleaning up.
	hasQuit bool
}

// Config contains configuration options for an IRC client
type Config struct {
	// Server is a host/ip of the server you want to connect to.
	Server string
	// Port is the port that will be used during server connection.
	Port int
	// Password is the server password used to authenticate.
	Password string
	// Nick is an rfc-valid nickname used during connect.
	Nick string
	// User is the username/ident to use on connect. Ignored if identd server
	// is used.
	User string
	// Name is the "realname" that's used during connect.
	Name string
	// TLSConfig is an optional user-supplied tls configuration, used during
	// socket creation to the server.
	TLSConfig *tls.Config
	// MaxRetries is the number of times the client will attempt to reconnect
	// to the server after the last disconnect.
	MaxRetries int
	// Logger is an optional, user supplied logger to log the raw lines sent
	// from the server. Useful for debugging. Defaults to ioutil.Discard.
	Logger io.Writer
	// ReconnectDelay is the a duration of time to delay before attempting a
	// reconnection. Defaults to 10s (minimum of 10s).
	ReconnectDelay time.Duration
	// DisableTracking disables all channel and user-level tracking. Useful
	// for highly embedded scripts with single purposes.
	DisableTracking bool
	// DisableCapTracking disables all network/server capability tracking.
	// This includes determining what feature the IRC server supports, what
	// the "NETWORK=" variables are, and other useful stuff.
	DisableCapTracking bool
	// DisableNickCollision disables the clients auto-response to nickname
	// collisions. For example, if "test" is already in use, or is blocked by
	// the network/a service, the client will try and use "test_", then it
	// will attempt "test__", "test___", and so on.
	DisableNickCollision bool
}

// New creates a new IRC client with the specified server, name and
// config.
func New(config Config) *Client {
	client := &Client{
		Config:    config,
		Events:    make(chan *Event, 40), // buffer 40 events
		quitChan:  make(chan bool),
		callbacks: make(map[string][]Callback),
		tries:     0,
		initTime:  time.Now(),
	}

	// Register builtin helpers.
	client.registerHelpers()

	return client
}

// Quit disconnects from the server.s
func (c *Client) Quit(message string) {
	c.Send(&Event{Command: QUIT, Trailing: message})

	c.hasQuit = true

	if c.conn != nil {
		c.conn.Close()
	}

	c.quitChan <- true
}

// Uptime returns the amount of time that has passed since the
// client was created.
func (c *Client) Uptime() time.Duration {
	return time.Since(c.initTime)
}

// Server returns the string representation of host+port pair for net.Conn
func (c *Client) Server() string {
	return fmt.Sprintf("%s:%d", c.Config.Server, c.Config.Port)
}

// Send sends an event to the server. Use Client.RunCallback() if you are
// are simply looking to trigger callbacks with an event.
func (c *Client) Send(event *Event) error {
	// log the event
	if !event.Sensitive {
		c.log.Print("--> ", event.String())
	}

	return c.Sender.Send(event)
}

// Connect attempts to connect to the given IRC server
func (c *Client) Connect() error {
	var conn net.Conn
	var err error

	// Sanity check a few options.
	if c.Config.Server == "" || c.Config.Port == 0 || c.Config.Nick == "" || c.Config.User == "" {
		return errors.New("invalid configuration (server/port/nick/user)")
	}

	// Reset the state.
	c.state = newState()

	if c.Config.Logger == nil {
		c.Config.Logger = ioutil.Discard
	}

	c.log = log.New(c.Config.Logger, "", log.Ldate|log.Ltime|log.Lshortfile)

	if c.Config.TLSConfig == nil {
		conn, err = net.Dial("tcp", c.Server())
	} else {
		conn, err = tls.Dial("tcp", c.Server(), c.Config.TLSConfig)
	}
	if err != nil {
		return err
	}

	c.conn = conn
	c.reader = NewDecoder(conn)
	c.writer = NewEncoder(conn)
	c.Sender = serverSender{writer: c.writer}
	for _, event := range c.connectMessages() {
		if err := c.Send(event); err != nil {
			return err
		}
	}

	c.tries = 0
	go c.ReadLoop()

	// Consider the connection a success at this point.
	c.state.connected = true

	return nil
}

// connectMessages is a list of IRC messages to send when attempting
// to connect to the IRC server.
func (c *Client) connectMessages() (events []*Event) {
	// Passwords first.
	if c.Config.Password != "" {
		events = append(events, &Event{Command: PASS, Params: []string{c.Config.Password}})
	}

	// Then nickname.
	events = append(events, &Event{Command: NICK, Params: []string{c.Config.Nick}})

	// Then username and realname.
	if c.Config.Name == "" {
		c.Config.Name = c.Config.User
	}

	events = append(events, &Event{
		Command:  USER,
		Params:   []string{c.Config.User, "+iw", "*"},
		Trailing: c.Config.Name,
	})

	return events
}

// Reconnect checks to make sure we want to, and then attempts to
// reconnect to the server.
func (c *Client) Reconnect() (err error) {
	if c.hasQuit {
		return nil
	}

	if c.Config.ReconnectDelay < (10 * time.Second) {
		c.Config.ReconnectDelay = 10 * time.Second
	}

	if c.Config.MaxRetries > 0 {
		var err error
		c.conn.Close()

		// Re-setup events.
		c.Events = make(chan *Event, 40)

		// Delay so we're not slaughtering the server with a bunch of
		// connections.
		c.log.Printf("reconnecting to %s in %s", c.Server(), c.Config.ReconnectDelay)
		time.Sleep(c.Config.ReconnectDelay)

		for err = c.Connect(); err != nil && c.tries < c.Config.MaxRetries; c.tries++ {
			c.log.Printf("reconnecting to %s in %s (%d tries)", c.Server(), c.Config.ReconnectDelay, c.tries)
			time.Sleep(c.Config.ReconnectDelay)
		}

		return err
	}

	close(c.Events)
	return nil
}

// ReadLoop sets a timeout of 300 seconds, and then attempts to read
// from the IRC server. If there is an error, it calls Reconnect.
func (c *Client) ReadLoop() error {
	for {
		c.conn.SetDeadline(time.Now().Add(300 * time.Second))
		event, err := c.reader.Decode()
		if err != nil {
			return c.Reconnect()
		}

		c.Events <- event
	}
}

// Loop reads from the events channel and sends the events to be
// handled for every message it receives.
func (c *Client) Loop() {
	for {
		select {
		case event := <-c.Events:
			c.handleEvent(event)
		case <-c.quitChan:
			return
		}
	}
}

// IsConnected returns true if the client is connected to the server.
func (c *Client) IsConnected() bool {
	c.state.m.RLock()
	defer c.state.m.RUnlock()

	return c.state.connected
}

// GetNick returns the current nickname of the active connection.
//
// Returns empty string if tracking is disabled.
func (c *Client) GetNick() string {
	if c.Config.DisableTracking {
		return ""
	}

	c.state.m.RLock()
	defer c.state.m.RUnlock()

	if c.state.nick == "" {
		return c.Config.Nick
	}

	return c.state.nick
}

// SetNick changes the client nickname.
func (c *Client) SetNick(name string) {
	c.state.m.Lock()
	defer c.state.m.Unlock()

	c.state.nick = name
	c.Send(&Event{Command: NICK, Params: []string{name}})
}

// GetChannels returns the active list of channels that the client
// is in.
//
// Returns nil if tracking is disabled.
func (c *Client) GetChannels() map[string]*Channel {
	if c.Config.DisableTracking {
		return nil
	}

	c.state.m.RLock()
	defer c.state.m.RUnlock()

	return c.state.channels
}

// Who tells the client to update it's channel/user records.
//
// Does not update internal state if tracking is disabled.
func (c *Client) Who(target string) {
	c.Send(&Event{Command: WHO, Params: []string{target, "%tcuhn,1"}})
}

// Join attempts to enter an IRC channel with an optional password.
func (c *Client) Join(channel, password string) {
	if password != "" {
		c.Send(&Event{Command: JOIN, Params: []string{channel, password}})
		return
	}

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

// Part leaves an IRC channel with an optional leave message.
func (c *Client) Part(channel, message string) {
	if message != "" {
		c.Send(&Event{Command: JOIN, Params: []string{channel}, Trailing: message})
		return
	}

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

// Message sends a PRIVMSG to target (either channel, service, or
// user).
func (c *Client) Message(target, message string) {
	c.Send(&Event{Command: PRIVMSG, Params: []string{target}, Trailing: message})
}

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

// Action sends a PRIVMSG ACTION (/me) to target (either channel,
// service, or user).
func (c *Client) Action(target, message string) {
	c.Send(&Event{Command: PRIVMSG, Params: []string{target}, Trailing: fmt.Sprintf("\001ACTION %s\001", message)})
}

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

// Notice sends a NOTICE to target (either channel, service, or user).
func (c *Client) Notice(target, message string) {
	c.Send(&Event{Command: NOTICE, Params: []string{target}, Trailing: message})
}

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

// SendRaw sends a raw string back to the server, without carriage returns or
// newlines.
func (c *Client) SendRaw(raw string) {
	e := ParseEvent(raw)
	if e == nil {
		c.log.Printf("invalid event: %q", raw)
		return
	}

	c.Send(e)
}

// SendRawf sends a formated string back to the server, without carriage
// returns or newlines.
func (c *Client) SendRawf(format string, a ...interface{}) {
	c.SendRaw(fmt.Sprintf(format, a...))
}
<commit_msg>convert quitChan to empty struct<commit_after>// Copyright 2016 Liam Stanley <me@liamstanley.io>. All rights reserved.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.

// Package girc provides a high level, yet flexible IRC library for use
// with interacting with IRC servers. girc has support for user/channel
// tracking, as well as a few other neat features (like auto-reconnect).
//
// Much of what girc can do, can also be disabled. The goal is to
// provide a solid API that you don't necessarily have to work with out
// of the box if you don't want to.
//
// See "example/main.go" for a brief and very useful example taking
// advantage of girc, that should give you a general idea of how the API
// works.
package girc

import (
	"crypto/tls"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net"
	"time"
)

// TODO's:
//   * ClearCallbacks(CODE)?
//   * Should Client.Message() an other similar methods support errors?
//     * along with this, should we forcefully check to ensure that the target
//       of the events are valid?
//   * track connection time (conntime? in state)
//   * with conntime, find lag. Client.Lag() would be useful
//   * would be cool to track things like SERVERNAME, VERSION, UMODES,
//     CMODES, etc. also see Config.DisableCapTracking.
//       -- https://github.com/Liamraystanley/Code/blob/master/core/triggers.py#L40-L67
//   * client should support ping tracking (sending PING's to the server)
//   * users need to be exposed in state somehow (other than GetChannels())
//   * ip/host binding?
//   * IsValidNick?
//   * User.Age()? (FirstActive()?) (time since first seen)
//   * cleanup docs in conn.go & event.go.

// Client contains all of the information necessary to run a single IRC
// client.
type Client struct {
	// Config represents the configuration
	Config Config
	// Events is a buffer of events waiting to be processed.
	Events chan *Event
	// Sender is a Sender{} interface implementation.
	Sender Sender

	// state represents the internal state
	state *state
	// initTime represents the creation time of the client.
	initTime time.Time
	// callbacks is an internal mapping of COMMAND -> callback.
	callbacks map[string][]Callback
	// reader is the socket buffer reader from the IRC server.
	reader *Decoder
	// reader is the socket buffer write to the IRC server.
	writer *Encoder
	// conn is a net.Conn reference to the IRC server.
	conn net.Conn
	// tries represents the internal reconnect count to the IRC server.
	tries int
	// log is used if a writer is supplied for Client.Config.Logger.
	log *log.Logger
	// quitChan is used to close the connection to the IRC server.
	quitChan chan struct{}
	// hasQuit is used to determine if we've finished quitting/cleaning up.
	hasQuit bool
}

// Config contains configuration options for an IRC client
type Config struct {
	// Server is a host/ip of the server you want to connect to.
	Server string
	// Port is the port that will be used during server connection.
	Port int
	// Password is the server password used to authenticate.
	Password string
	// Nick is an rfc-valid nickname used during connect.
	Nick string
	// User is the username/ident to use on connect. Ignored if identd server
	// is used.
	User string
	// Name is the "realname" that's used during connect.
	Name string
	// TLSConfig is an optional user-supplied tls configuration, used during
	// socket creation to the server.
	TLSConfig *tls.Config
	// MaxRetries is the number of times the client will attempt to reconnect
	// to the server after the last disconnect.
	MaxRetries int
	// Logger is an optional, user supplied logger to log the raw lines sent
	// from the server. Useful for debugging. Defaults to ioutil.Discard.
	Logger io.Writer
	// ReconnectDelay is the a duration of time to delay before attempting a
	// reconnection. Defaults to 10s (minimum of 10s).
	ReconnectDelay time.Duration
	// DisableTracking disables all channel and user-level tracking. Useful
	// for highly embedded scripts with single purposes.
	DisableTracking bool
	// DisableCapTracking disables all network/server capability tracking.
	// This includes determining what feature the IRC server supports, what
	// the "NETWORK=" variables are, and other useful stuff.
	DisableCapTracking bool
	// DisableNickCollision disables the clients auto-response to nickname
	// collisions. For example, if "test" is already in use, or is blocked by
	// the network/a service, the client will try and use "test_", then it
	// will attempt "test__", "test___", and so on.
	DisableNickCollision bool
}

// New creates a new IRC client with the specified server, name and
// config.
func New(config Config) *Client {
	client := &Client{
		Config:    config,
		Events:    make(chan *Event, 40), // buffer 40 events
		quitChan:  make(chan struct{}),
		callbacks: make(map[string][]Callback),
		tries:     0,
		initTime:  time.Now(),
	}

	// Register builtin helpers.
	client.registerHelpers()

	return client
}

// Quit disconnects from the server.s
func (c *Client) Quit(message string) {
	c.Send(&Event{Command: QUIT, Trailing: message})

	c.hasQuit = true

	if c.conn != nil {
		c.conn.Close()
	}

	c.quitChan <- struct{}{}
}

// Uptime returns the amount of time that has passed since the
// client was created.
func (c *Client) Uptime() time.Duration {
	return time.Since(c.initTime)
}

// Server returns the string representation of host+port pair for net.Conn
func (c *Client) Server() string {
	return fmt.Sprintf("%s:%d", c.Config.Server, c.Config.Port)
}

// Send sends an event to the server. Use Client.RunCallback() if you are
// are simply looking to trigger callbacks with an event.
func (c *Client) Send(event *Event) error {
	// log the event
	if !event.Sensitive {
		c.log.Print("--> ", event.String())
	}

	return c.Sender.Send(event)
}

// Connect attempts to connect to the given IRC server
func (c *Client) Connect() error {
	var conn net.Conn
	var err error

	// Sanity check a few options.
	if c.Config.Server == "" || c.Config.Port == 0 || c.Config.Nick == "" || c.Config.User == "" {
		return errors.New("invalid configuration (server/port/nick/user)")
	}

	// Reset the state.
	c.state = newState()

	if c.Config.Logger == nil {
		c.Config.Logger = ioutil.Discard
	}

	c.log = log.New(c.Config.Logger, "", log.Ldate|log.Ltime|log.Lshortfile)

	if c.Config.TLSConfig == nil {
		conn, err = net.Dial("tcp", c.Server())
	} else {
		conn, err = tls.Dial("tcp", c.Server(), c.Config.TLSConfig)
	}
	if err != nil {
		return err
	}

	c.conn = conn
	c.reader = NewDecoder(conn)
	c.writer = NewEncoder(conn)
	c.Sender = serverSender{writer: c.writer}
	for _, event := range c.connectMessages() {
		if err := c.Send(event); err != nil {
			return err
		}
	}

	c.tries = 0
	go c.ReadLoop()

	// Consider the connection a success at this point.
	c.state.connected = true

	return nil
}

// connectMessages is a list of IRC messages to send when attempting
// to connect to the IRC server.
func (c *Client) connectMessages() (events []*Event) {
	// Passwords first.
	if c.Config.Password != "" {
		events = append(events, &Event{Command: PASS, Params: []string{c.Config.Password}})
	}

	// Then nickname.
	events = append(events, &Event{Command: NICK, Params: []string{c.Config.Nick}})

	// Then username and realname.
	if c.Config.Name == "" {
		c.Config.Name = c.Config.User
	}

	events = append(events, &Event{
		Command:  USER,
		Params:   []string{c.Config.User, "+iw", "*"},
		Trailing: c.Config.Name,
	})

	return events
}

// Reconnect checks to make sure we want to, and then attempts to
// reconnect to the server.
func (c *Client) Reconnect() (err error) {
	if c.hasQuit {
		return nil
	}

	if c.Config.ReconnectDelay < (10 * time.Second) {
		c.Config.ReconnectDelay = 10 * time.Second
	}

	if c.Config.MaxRetries > 0 {
		var err error
		c.conn.Close()

		// Re-setup events.
		c.Events = make(chan *Event, 40)

		// Delay so we're not slaughtering the server with a bunch of
		// connections.
		c.log.Printf("reconnecting to %s in %s", c.Server(), c.Config.ReconnectDelay)
		time.Sleep(c.Config.ReconnectDelay)

		for err = c.Connect(); err != nil && c.tries < c.Config.MaxRetries; c.tries++ {
			c.log.Printf("reconnecting to %s in %s (%d tries)", c.Server(), c.Config.ReconnectDelay, c.tries)
			time.Sleep(c.Config.ReconnectDelay)
		}

		return err
	}

	close(c.Events)
	return nil
}

// ReadLoop sets a timeout of 300 seconds, and then attempts to read
// from the IRC server. If there is an error, it calls Reconnect.
func (c *Client) ReadLoop() error {
	for {
		c.conn.SetDeadline(time.Now().Add(300 * time.Second))
		event, err := c.reader.Decode()
		if err != nil {
			return c.Reconnect()
		}

		c.Events <- event
	}
}

// Loop reads from the events channel and sends the events to be
// handled for every message it receives.
func (c *Client) Loop() {
	for {
		select {
		case event := <-c.Events:
			c.handleEvent(event)
		case <-c.quitChan:
			return
		}
	}
}

// IsConnected returns true if the client is connected to the server.
func (c *Client) IsConnected() bool {
	c.state.m.RLock()
	defer c.state.m.RUnlock()

	return c.state.connected
}

// GetNick returns the current nickname of the active connection.
//
// Returns empty string if tracking is disabled.
func (c *Client) GetNick() string {
	if c.Config.DisableTracking {
		return ""
	}

	c.state.m.RLock()
	defer c.state.m.RUnlock()

	if c.state.nick == "" {
		return c.Config.Nick
	}

	return c.state.nick
}

// SetNick changes the client nickname.
func (c *Client) SetNick(name string) {
	c.state.m.Lock()
	defer c.state.m.Unlock()

	c.state.nick = name
	c.Send(&Event{Command: NICK, Params: []string{name}})
}

// GetChannels returns the active list of channels that the client
// is in.
//
// Returns nil if tracking is disabled.
func (c *Client) GetChannels() map[string]*Channel {
	if c.Config.DisableTracking {
		return nil
	}

	c.state.m.RLock()
	defer c.state.m.RUnlock()

	return c.state.channels
}

// Who tells the client to update it's channel/user records.
//
// Does not update internal state if tracking is disabled.
func (c *Client) Who(target string) {
	c.Send(&Event{Command: WHO, Params: []string{target, "%tcuhn,1"}})
}

// Join attempts to enter an IRC channel with an optional password.
func (c *Client) Join(channel, password string) {
	if password != "" {
		c.Send(&Event{Command: JOIN, Params: []string{channel, password}})
		return
	}

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

// Part leaves an IRC channel with an optional leave message.
func (c *Client) Part(channel, message string) {
	if message != "" {
		c.Send(&Event{Command: JOIN, Params: []string{channel}, Trailing: message})
		return
	}

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

// Message sends a PRIVMSG to target (either channel, service, or
// user).
func (c *Client) Message(target, message string) {
	c.Send(&Event{Command: PRIVMSG, Params: []string{target}, Trailing: message})
}

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

// Action sends a PRIVMSG ACTION (/me) to target (either channel,
// service, or user).
func (c *Client) Action(target, message string) {
	c.Send(&Event{Command: PRIVMSG, Params: []string{target}, Trailing: fmt.Sprintf("\001ACTION %s\001", message)})
}

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

// Notice sends a NOTICE to target (either channel, service, or user).
func (c *Client) Notice(target, message string) {
	c.Send(&Event{Command: NOTICE, Params: []string{target}, Trailing: message})
}

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

// SendRaw sends a raw string back to the server, without carriage returns or
// newlines.
func (c *Client) SendRaw(raw string) {
	e := ParseEvent(raw)
	if e == nil {
		c.log.Printf("invalid event: %q", raw)
		return
	}

	c.Send(e)
}

// SendRawf sends a formated string back to the server, without carriage
// returns or newlines.
func (c *Client) SendRawf(format string, a ...interface{}) {
	c.SendRaw(fmt.Sprintf(format, a...))
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"io"
	"os"
	"strconv"
	"strings"
)

import (
	"github.com/alfredxing/calc/compute"
)

import (
	"golang.org/x/crypto/ssh/terminal"
)

// Stores the state of the terminal before making it raw
var regularState *terminal.State

func main() {
	if len(os.Args) > 1 {
		input := strings.Replace(strings.Join(os.Args[1:], ""), " ", "", -1)
		res, err := compute.Evaluate(input)
		if err != nil {
			fmt.Println("Error: " + err.Error())
			return
		}
		fmt.Printf("%s\n", strconv.FormatFloat(res, 'G', -1, 64))
		return
	}

	var err error
	regularState, err = terminal.MakeRaw(0)
	if err != nil {
		panic(err)
	}
	defer terminal.Restore(0, regularState)

	term := terminal.NewTerminal(os.Stdin, "> ")
	term.AutoCompleteCallback = handleKey
	for {
		text, err := term.ReadLine()
		if err != nil {
			if err == io.EOF {
				// Quit without error on Ctrl^D
				fmt.Println()
				break
			}
			panic(err)
		}

		text = strings.Replace(text, " ", "", -1)
		if text == "exit" || text == "quit" {
			break
		}

		res, err := compute.Evaluate(text)
		if err != nil {
			fmt.Println("Error: " + err.Error())
			continue
		}
		fmt.Printf("%s\n", strconv.FormatFloat(res, 'G', -1, 64))
	}
}

func handleKey(line string, pos int, key rune) (newLine string, newPos int, ok bool) {
	if key == '\x03' {
		fmt.Println()
		terminal.Restore(0, regularState)
		os.Exit(0)
	}
	return "", 0, false
}
<commit_msg>Use term.Write() to write to console<commit_after>package main

import (
	"fmt"
	"io"
	"os"
	"strconv"
	"strings"
)

import (
	"github.com/alfredxing/calc/compute"
)

import (
	"golang.org/x/crypto/ssh/terminal"
)

// Stores the state of the terminal before making it raw
var regularState *terminal.State

func main() {
	if len(os.Args) > 1 {
		input := strings.Replace(strings.Join(os.Args[1:], ""), " ", "", -1)
		res, err := compute.Evaluate(input)
		if err != nil {
			fmt.Println("Error: " + err.Error())
			return
		}
		fmt.Printf("%s\n", strconv.FormatFloat(res, 'G', -1, 64))
		return
	}

	var err error
	regularState, err = terminal.MakeRaw(0)
	if err != nil {
		panic(err)
	}
	defer terminal.Restore(0, regularState)

	term := terminal.NewTerminal(os.Stdin, "> ")
	term.AutoCompleteCallback = handleKey
	for {
		text, err := term.ReadLine()
		if err != nil {
			if err == io.EOF {
				// Quit without error on Ctrl^D
				fmt.Println()
				break
			}
			panic(err)
		}

		text = strings.Replace(text, " ", "", -1)
		if text == "exit" || text == "quit" {
			break
		}

		res, err := compute.Evaluate(text)
		if err != nil {
			term.Write([]byte(fmt.Sprintln("Error: " + err.Error())))
			continue
		}
		term.Write([]byte(fmt.Sprintf("%s\n", strconv.FormatFloat(res, 'G', -1, 64))))
	}
}

func handleKey(line string, pos int, key rune) (newLine string, newPos int, ok bool) {
	if key == '\x03' {
		fmt.Println()
		terminal.Restore(0, regularState)
		os.Exit(0)
	}
	return "", 0, false
}
<|endoftext|>


<commit_before>package main

import (
	"log"
	"net/http"
	"os"

	"github.com/gorilla/mux"
)

// Log what's up
func Log(handler http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		remoteAddr := r.RemoteAddr
		if len(remoteAddr) == 0 {
			remoteAddr = r.Header.Get("x-forwarded-for")
		}
		log.Printf("%s %s %s", r.RemoteAddr, r.Method, r.URL)
		handler.ServeHTTP(w, r)
	})
}

var db SparkleDatabase

func main() {
	r := mux.NewRouter()
	r.HandleFunc("/sparkles", addSparkle).Methods("POST")
	r.HandleFunc("/sparkles", getSparkles).Methods("GET")
	r.HandleFunc("/top/giver", topGiven).Methods("GET")
	r.HandleFunc("/top/receiver", topReceived).Methods("GET")
	r.HandleFunc("/sparkles/{recipient}", getSparklesForRecipient).Methods("GET")
	// r.HandleFunc("/migrate/{from}/{to}", migrateSparkles)
	r.HandleFunc("/graph", getSparkleGraph).Methods("GET")
	r.PathPrefix("/").Handler(http.FileServer(http.Dir("./public/")))
	// Load the database from file
	db = LoadDB()

	port := os.Getenv("PORT")
	if len(port) == 0 {
		port = "8080"
	}

	http.Handle("/", r)
	log.Printf("[+] Starting the sparkles app on port %s", port)
	http.ListenAndServe(":"+port, Log(http.DefaultServeMux))
}
<commit_msg>re-enable migrations for trollmode shenanigans<commit_after>package main

import (
	"log"
	"net/http"
	"os"

	"github.com/gorilla/mux"
)

// Log what's up
func Log(handler http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		remoteAddr := r.RemoteAddr
		if len(remoteAddr) == 0 {
			remoteAddr = r.Header.Get("x-forwarded-for")
		}
		log.Printf("%s %s %s", r.RemoteAddr, r.Method, r.URL)
		handler.ServeHTTP(w, r)
	})
}

var db SparkleDatabase

func main() {
	r := mux.NewRouter()
	r.HandleFunc("/sparkles", addSparkle).Methods("POST")
	r.HandleFunc("/sparkles", getSparkles).Methods("GET")
	r.HandleFunc("/top/giver", topGiven).Methods("GET")
	r.HandleFunc("/top/receiver", topReceived).Methods("GET")
	r.HandleFunc("/sparkles/{recipient}", getSparklesForRecipient).Methods("GET")
	r.HandleFunc("/migrate/{from}/{to}", migrateSparkles)
	r.HandleFunc("/graph", getSparkleGraph).Methods("GET")
	r.PathPrefix("/").Handler(http.FileServer(http.Dir("./public/")))
	// Load the database from file
	db = LoadDB()

	port := os.Getenv("PORT")
	if len(port) == 0 {
		port = "8080"
	}

	http.Handle("/", r)
	log.Printf("[+] Starting the sparkles app on port %s", port)
	http.ListenAndServe(":"+port, Log(http.DefaultServeMux))
}
<|endoftext|>


<commit_before>package main

import (
	ps "github.com/mitchellh/go-ps"
	"log"
	"os/exec"
	"regexp"
	"strconv"
	"github.com/andlabs/ui"
	"time"
	"net/http"
	"os"
	"gopkg.in/yaml.v2"
	"io/ioutil"
	"syscall"
	"strings"
)

// Telegram and program settings (config.yml)
// Guide: http://sweetohm.net/article/go-yaml-parsers.en.html
type Config struct {
	Token string
	Botid string
	Chatid string
	Message string
	StayAlive bool
	Process string
	TimeBetweenChecksInS int
	KillOnDC bool
	ShutdownOnDC bool
	KillCoherentUI bool
}

// Variables
var STATUS bool = false
var CONNECTION bool = false
var PID int

func main() {

	////  SETTINGS
	//--------------------------------------------------------------------------------------------------------------
	// YAML PARSING
	ex, err := os.Executable()
	if err != nil {
		panic(err)
	}
	newex := strings.Replace(ex, "BDO-Watchdog.exe", "config.yml", -1)
	// This is necessary for dynamic builds in Jetbrains Gogland IDE
	//newex = strings.Replace(ex, "Application.exe", "config.yml", -1)

	var config Config
	source, err := ioutil.ReadFile(newex)

	if err != nil {
		// in theory, using yml.Marshal() would be more elegant, but we want to preserve the yaml comments
		// as well as set some default values/hints
		defconf :=
				"## Get updates here: https://github.com/tzerk/BDO-Watchdog/releases/\r\n" +
				"## Telegram Bot Settings\r\n" +
				"token: \r\n" +
				"botid: \r\n" +
				"chatid: \r\n" +
				"message: BDO disconnected \r\n" +
				"\r\n" +
				"## Program Settings\r\n" +
				"stayalive: false\r\n" +
				"process: BlackDesert64.exe\r\n" +
				"timebetweenchecksins: 60\r\n" +
				"\r\n" +
				"# These settings require the .exe to be run with admin rights! \r\n" +
				"killondc: true\r\n" +
				"shutdownondc: false\r\n" +
				"killcoherentui: false"
		ioutil.WriteFile("config.yml", []byte(defconf), os.FileMode(int(0777)))
		panic(err)
	}
	err = yaml.Unmarshal(source, &config)
	if err != nil {
		panic(err)
	}

	//// GUI
	//--------------------------------------------------------------------------------------------------------------
	ui := ui.Main(func() {
		window := ui.NewWindow("BDO Watchdog v0.1.4", 300, 80, false)

		label_Process := ui.NewLabel("  Process: " + config.Process)
		label_Status := ui.NewLabel("  Initializing...")
		label_PID := ui.NewLabel("-")
		label_Connection := ui.NewLabel("-")
		label_Update := ui.NewLabel("")

		box := ui.NewVerticalBox()

		tbtn := ui.NewButton("Send Telegram test message")
		tbtn.OnClicked(func(*ui.Button) {
			send_TelegramMessage(config)
		})

		sep := ui.NewHorizontalSeparator()
		pb := ui.NewProgressBar()

		// Append all UI elements to the box container
		box.Append(label_Process, false)
		box.Append(label_Status, false)
		box.Append(label_PID, false)
		box.Append(label_Connection, false)
		box.Append(label_Update, false)
		box.Append(tbtn, false)
		box.Append(sep, false)
		box.Append(pb, true)

		// If this tool requires admin rights add a text label to inform the user
		if config.KillOnDC || config.KillCoherentUI {
			box.Append(ui.NewLabel("Make sure to have started this program with admin rights!"), true)
		}

		window.SetChild(box)
		window.OnClosing(func(*ui.Window) bool {
			ui.Quit()
			return true
		})
		window.Show()
		go observer(config, label_Status, label_PID, label_Connection, label_Update, pb)
	})
	if ui != nil {
		panic(ui)
	}
}

//--------------------------------------------------------------------------------------------------------------
// PROCESS
//--------------------------------------------------------------------------------------------------------------
func observer(
	config Config,
	label_Status *ui.Label,
	label_PID *ui.Label,
	label_Connection *ui.Label,
	label_Update *ui.Label,
	pb *ui.ProgressBar) {

	// KILL CoherentUI_Host.exe
	if config.KillCoherentUI {

		// Find process(es)
		chp, err := ps.Processes()
		if err != nil {
			log.Fatal(err)
		}

		// Find PID and kill
		for _, v := range chp {
			if v.Executable() == "CoherentUI_Host.exe" {
				proc, err := os.FindProcess(v.Pid())

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

				defer func() {
					recover() // 1
					return
				}()
				// Kill the process
				proc.Kill()
			}
		}
	}

	// INFINITE MAIN LOOP
	for {
		label_Update.SetText("")

		//// EXIT CONDITION
		//-----------------
		// If the process is running, but no longer connected we trigger the following actions
		if STATUS && !CONNECTION {

			// Use the Telegram API to send a message
			send_TelegramMessage(config)

			// Optional: shutdown the computer if the monitored process is disconnected
			if config.ShutdownOnDC {
				exec.Command("cmd", "/C", "shutdown", "/s").Run()
			}

			// Optional: kill the monitored process if it is disconnected
			// requires elevated rights --> start .exe as administrator
			if config.KillOnDC {

				proc, err := os.FindProcess(PID)

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

				defer func() {
					recover() // 1
					return
				}()
				// Kill the process
				proc.Kill()

				time.Sleep(5 * time.Second)
			}

			// Optional (YAML file, default: false): keep ts program open even if
			// the process is disconnected
			if !config.StayAlive {
				os.Exit(1)
			}
		}

		//// PROCESS
		//----------
		p, err := ps.Processes()
		if err != nil {
			log.Fatal(err)
		}

		//// PID
		//------
		for _, v := range p {
			if v.Executable() == config.Process {
				PID = v.Pid()
			}
		}
		if (PID == 0) {
			ui.QueueMain(func () {
				STATUS = false
				label_Status.SetText("  Status: not running")
				label_PID.SetText("  PID: -")
				label_Connection.SetText("  Connection: -" )
			})

			wait(config, label_Update, pb)
			continue
		} else {

			ui.QueueMain(func () {
				STATUS = true
				label_Status.SetText("  Status: running")
				label_PID.SetText("  PID: " + strconv.Itoa(PID))
			})
		}

		//// CONNECTION STATUS
		//--------------------
		// NETSTAT
		// the syscall.SysProcAttr trick found here:
		// https://www.reddit.com/r/golang/comments/2c1g3x/build_golang_app_reverse_shell_to_run_in_windows/
		cmd := exec.Command("cmd.exe", "/C netstat -aon")
		cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
		out, err := cmd.Output()
		if err != nil {
			log.Fatal(err)
		}

		// RegEx matching; try to find the PID in the netstat output
		re := regexp.MustCompile(strconv.Itoa(PID))
		byteIndex := re.FindIndex([]byte(out))

		if (len(byteIndex) == 0) {
			ui.QueueMain(func () {
				CONNECTION = false
				label_Connection.SetText("  Connection: Offline" )
			})
		} else {
			// Update labels
			ui.QueueMain(func () {
				CONNECTION = true
				label_Connection.SetText("  Connection: online")
			})
		}

		// Wait x seconds before next iteration
		wait(config, label_Update, pb)
	}
}

// ---------------------------------------------------------------------------------------------------------------------
// A wrapper for time.Sleep() that also updates the UI label and progressbar
func wait(config Config, label_Update *ui.Label, pb *ui.ProgressBar) {
	tstep := config.TimeBetweenChecksInS
	var pbVal int

	if tstep <= 0 {
		tstep = 1
	} // otherwise division by 0
	for i := 0; i <= tstep; i++ {
		pbVal = int(100/float32(tstep) * float32(i))
		if pbVal > 100 {
			pbVal = 100
		}
		pb.SetValue(pbVal)
		label_Update.SetText("  Next update in... " + strconv.Itoa(tstep - i) + " s")
		time.Sleep(1 * time.Second)
	}
	pb.SetValue(0)
}

// ---------------------------------------------------------------------------------------------------------------------
// Send a telegram message using a query URL
func send_TelegramMessage(config Config) {
	// Learn how to setup a telegram bot: https://core.telegram.org/bots
	resp, _ := http.Get("https://api.telegram.org/bot" + config.Botid +
		":" + config.Token +
		"/sendMessage?chat_id=" + config.Chatid +
		"&text=" + config.Message)
	defer resp.Body.Close()
}
<commit_msg>New tab layout<commit_after>package main

import (
	ps "github.com/mitchellh/go-ps"
	"log"
	"os/exec"
	"regexp"
	"strconv"
	"github.com/andlabs/ui"
	"time"
	"net/http"
	"os"
	"gopkg.in/yaml.v2"
	"io/ioutil"
	"syscall"
	"strings"
)

// Telegram and program settings (config.yml)
// Guide: http://sweetohm.net/article/go-yaml-parsers.en.html
type Config struct {
	Token string
	Botid string
	Chatid string
	Message string
	StayAlive bool
	Process string
	TimeBetweenChecksInS int
	KillOnDC bool
	ShutdownOnDC bool
	KillCoherentUI bool
}

// Variables
var STATUS bool = false
var CONNECTION bool = false
var PID int

func main() {

	////  SETTINGS
	//--------------------------------------------------------------------------------------------------------------
	// YAML PARSING
	ex, err := os.Executable()
	if err != nil {
		panic(err)
	}
	newex := strings.Replace(ex, "BDO-Watchdog.exe", "config.yml", -1)
	// This is necessary for dynamic builds in Jetbrains Gogland IDE
	//newex = strings.Replace(ex, "Application.exe", "config.yml", -1)

	var config Config
	source, err := ioutil.ReadFile(newex)

	if err != nil {
		// in theory, using yml.Marshal() would be more elegant, but we want to preserve the yaml comments
		// as well as set some default values/hints
		defconf :=
				"## Get updates here: https://github.com/tzerk/BDO-Watchdog/releases/\r\n" +
				"## Telegram Bot Settings\r\n" +
				"token: \r\n" +
				"botid: \r\n" +
				"chatid: \r\n" +
				"message: BDO disconnected \r\n" +
				"\r\n" +
				"## Program Settings\r\n" +
				"stayalive: false\r\n" +
				"process: BlackDesert64.exe\r\n" +
				"timebetweenchecksins: 60\r\n" +
				"\r\n" +
				"# These settings require the .exe to be run with admin rights! \r\n" +
				"killondc: true\r\n" +
				"shutdownondc: false\r\n" +
				"killcoherentui: false"
		ioutil.WriteFile("config.yml", []byte(defconf), os.FileMode(int(0777)))
		panic(err)
	}
	err = yaml.Unmarshal(source, &config)
	if err != nil {
		panic(err)
	}

	//// GUI
	//--------------------------------------------------------------------------------------------------------------
	ui := ui.Main(func() {
		window := ui.NewWindow("BDO Watchdog v0.1.4", 300, 80, false)

		label_Process := ui.NewLabel("  Process: " + config.Process)
		label_Status := ui.NewLabel("  Initializing...")
		label_PID := ui.NewLabel("-")
		label_Connection := ui.NewLabel("-")
		label_Update := ui.NewLabel("")

		//box := ui.NewVerticalBox()
		box_main := ui.NewVerticalBox()
		box_settings := ui.NewVerticalBox()
		box_about := ui.NewVerticalBox()
		tab := ui.NewTab()

		tbtn := ui.NewButton("Send Telegram test message")
		tbtn.OnClicked(func(*ui.Button) {
			send_TelegramMessage(config)
		})

		sep := ui.NewHorizontalSeparator()
		pb := ui.NewProgressBar()

		// Append all UI elements to the box container
		box_main.Append(label_Process, false)
		box_main.Append(label_Status, false)
		box_main.Append(label_PID, false)
		box_main.Append(label_Connection, false)
		box_main.Append(label_Update, false)
		box_main.Append(tbtn, false)
		box_main.Append(sep, false)
		box_main.Append(pb, true)

		// Append UI elements for settings tab
		box_settings.Append(ui.NewLabel("Token: " + config.Token), false)
		box_settings.Append(ui.NewLabel("Bot ID: " + config.Botid), false)
		box_settings.Append(ui.NewLabel("Chat ID: " + config.Chatid), false)
		box_settings.Append(ui.NewLabel("Message: " + config.Message), false)
		box_settings.Append(ui.NewLabel("Stay alive: " + strconv.FormatBool(config.StayAlive)), false)
		box_settings.Append(ui.NewLabel("Process: " + config.Process), false)
		box_settings.Append(ui.NewLabel("Polling interval: " + strconv.Itoa(config.TimeBetweenChecksInS)), false)
		box_settings.Append(ui.NewLabel("Kill process after disconnect: " + strconv.FormatBool(config.KillOnDC)), false)
		box_settings.Append(ui.NewLabel("Shutdown PC after disconnect: " + strconv.FormatBool(config.ShutdownOnDC) ), false)
		box_settings.Append(ui.NewLabel("Kill CoherenUI_Host.exe: " + strconv.FormatBool(config.KillCoherentUI)), false)

		tab.Append("Main", box_main)
		tab.Append("Settings", box_settings)
		tab.Append("About", box_about)

		// If this tool requires admin rights add a text label to inform the user
		if config.KillOnDC || config.KillCoherentUI {
			box_main.Append(ui.NewLabel("Make sure to have started this program with admin rights!"), true)
		}

		box := ui.NewHorizontalBox()
		box.Append(tab, false)

		window.SetChild(box)

		window.OnClosing(func(*ui.Window) bool {
			ui.Quit()
			return true
		})
		window.Show()
		go observer(config, label_Status, label_PID, label_Connection, label_Update, pb)
	})
	if ui != nil {
		panic(ui)
	}
}

//--------------------------------------------------------------------------------------------------------------
// PROCESS
//--------------------------------------------------------------------------------------------------------------
func observer(
	config Config,
	label_Status *ui.Label,
	label_PID *ui.Label,
	label_Connection *ui.Label,
	label_Update *ui.Label,
	pb *ui.ProgressBar) {

	// KILL CoherentUI_Host.exe
	if config.KillCoherentUI {

		// Find process(es)
		chp, err := ps.Processes()
		if err != nil {
			log.Fatal(err)
		}

		// Find PID and kill
		for _, v := range chp {
			if v.Executable() == "CoherentUI_Host.exe" {
				proc, err := os.FindProcess(v.Pid())

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

				defer func() {
					recover() // 1
					return
				}()
				// Kill the process
				proc.Kill()
			}
		}
	}

	// INFINITE MAIN LOOP
	for {
		label_Update.SetText("")

		//// EXIT CONDITION
		//-----------------
		// If the process is running, but no longer connected we trigger the following actions
		if STATUS && !CONNECTION {

			// Use the Telegram API to send a message
			send_TelegramMessage(config)

			// Optional: shutdown the computer if the monitored process is disconnected
			if config.ShutdownOnDC {
				exec.Command("cmd", "/C", "shutdown", "/s").Run()
			}

			// Optional: kill the monitored process if it is disconnected
			// requires elevated rights --> start .exe as administrator
			if config.KillOnDC {

				proc, err := os.FindProcess(PID)

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

				defer func() {
					recover() // 1
					return
				}()
				// Kill the process
				proc.Kill()

				time.Sleep(5 * time.Second)
			}

			// Optional (YAML file, default: false): keep ts program open even if
			// the process is disconnected
			if !config.StayAlive {
				os.Exit(1)
			}
		}

		//// PROCESS
		//----------
		p, err := ps.Processes()
		if err != nil {
			log.Fatal(err)
		}

		//// PID
		//------
		for _, v := range p {
			if v.Executable() == config.Process {
				PID = v.Pid()
			}
		}
		if (PID == 0) {
			ui.QueueMain(func () {
				STATUS = false
				label_Status.SetText("  Status: not running")
				label_PID.SetText("  PID: -")
				label_Connection.SetText("  Connection: -" )
			})

			wait(config, label_Update, pb)
			continue
		} else {

			ui.QueueMain(func () {
				STATUS = true
				label_Status.SetText("  Status: running")
				label_PID.SetText("  PID: " + strconv.Itoa(PID))
			})
		}

		//// CONNECTION STATUS
		//--------------------
		// NETSTAT
		// the syscall.SysProcAttr trick found here:
		// https://www.reddit.com/r/golang/comments/2c1g3x/build_golang_app_reverse_shell_to_run_in_windows/
		cmd := exec.Command("cmd.exe", "/C netstat -aon")
		cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
		out, err := cmd.Output()
		if err != nil {
			log.Fatal(err)
		}

		// RegEx matching; try to find the PID in the netstat output
		re := regexp.MustCompile(strconv.Itoa(PID))
		byteIndex := re.FindIndex([]byte(out))

		if (len(byteIndex) == 0) {
			ui.QueueMain(func () {
				CONNECTION = false
				label_Connection.SetText("  Connection: Offline" )
			})
		} else {
			// Update labels
			ui.QueueMain(func () {
				CONNECTION = true
				label_Connection.SetText("  Connection: online")
			})
		}

		// Wait x seconds before next iteration
		wait(config, label_Update, pb)
	}
}

// ---------------------------------------------------------------------------------------------------------------------
// A wrapper for time.Sleep() that also updates the UI label and progressbar
func wait(config Config, label_Update *ui.Label, pb *ui.ProgressBar) {
	tstep := config.TimeBetweenChecksInS
	var pbVal int

	if tstep <= 0 {
		tstep = 1
	} // otherwise division by 0
	for i := 0; i <= tstep; i++ {
		pbVal = int(100/float32(tstep) * float32(i))
		if pbVal > 100 {
			pbVal = 100
		}
		pb.SetValue(pbVal)
		label_Update.SetText("  Next update in... " + strconv.Itoa(tstep - i) + " s")
		time.Sleep(1 * time.Second)
	}
	pb.SetValue(0)
}

// ---------------------------------------------------------------------------------------------------------------------
// Send a telegram message using a query URL
func send_TelegramMessage(config Config) {
	// Learn how to setup a telegram bot: https://core.telegram.org/bots
	resp, _ := http.Get("https://api.telegram.org/bot" + config.Botid +
		":" + config.Token +
		"/sendMessage?chat_id=" + config.Chatid +
		"&text=" + config.Message)
	defer resp.Body.Close()
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"sync"

	"github.com/BurntSushi/toml"
	"github.com/PuerkitoBio/goquery"
	"github.com/fatih/color"
	"github.com/mitchellh/go-homedir"
	"github.com/urfave/cli"
)

var (
	Version  string
	Revision string
)

type configFile struct {
	Urls      []string `toml:"urls"`
	UserAgent string   `toml:"useragent"`
}

func main() {
	cli.VersionPrinter = func(c *cli.Context) {
		fmt.Printf("version=%s revision=%s\n", c.App.Version, Revision)
	}
	app := cli.NewApp()
	app.Name = "gocrawsan"
	app.Usage = "web crawling command utility"
	app.Version = Version
	app.Usage = ""
	app.Flags = []cli.Flag{
		cli.StringFlag{
			Name: "useragent, U",
		},
		cli.StringFlag{
			Name: "config, C",
		},
		cli.BoolFlag{
			Name: "no-redirect",
		},
		cli.IntFlag{
			Name:  "depth",
			Value: 1,
		},
		cli.BoolFlag{
			Name: "debug, D",
		},
	}
	app.Action = func(c *cli.Context) error {
		var config string
		var err error
		if c.String("config") == "" {
			config, err = createConfigFile()
			if err != nil {
				return err
			}
		} else {
			config = c.String("config")
		}

		cr := NewCrawler()
		cr.useragent = c.String("useragent")
		client := &http.Client{}
		if c.Bool("no-redirect") {
			client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
				return http.ErrUseLastResponse
			}
		}
		cr.client = client

		buf, err := ioutil.ReadFile(config)
		if err != nil {
			return err
		}
		file := &configFile{}
		err = toml.Unmarshal(buf, file)
		if err != nil {
			return err
		}
		cr.crawling(file.Urls, c.Int("depth"))
		return nil
	}
	app.Run(os.Args)
}

type Crawler struct {
	m            *sync.Mutex
	wg           *sync.WaitGroup
	useragent    string
	client       *http.Client
	accessedUrls map[string]struct{}
}

func NewCrawler() *Crawler {
	c := &Crawler{
		wg: new(sync.WaitGroup),
		m:  new(sync.Mutex),
		accessedUrls: make(map[string]struct{}),
	}
	return c
}

func (c *Crawler) crawling(urls []string, depth int) error {
	for _, url := range urls {
		c.m.Lock()
		if _, ok := c.accessedUrls[url]; ok {
			c.m.Unlock()
			continue
		}
		c.accessedUrls[url] = struct{}{}
		c.m.Unlock()

		c.wg.Add(1)
		go c.getUrl(url, c.printHttpStatus, depth)
	}
	c.wg.Wait()
	return nil
}

func (c *Crawler) getUrl(url string, f func(string, *http.Response), d int) {
	req, _ := http.NewRequest("GET", url, nil)
	req.Header.Set("User-Agent", c.useragent)
	resp, _ := c.client.Do(req)

	d -= 1
	f(url, resp)
	c.accessToNext(resp, f, d)
	c.wg.Done()
}

func (c *Crawler) printHttpStatus(url string, resp *http.Response) {
	c.m.Lock()
	status := strings.Split(resp.Status, " ")
	code, _ := strconv.Atoi(status[0])
	fmt.Print(url + "\t")
	switch code / 100 {
	case 2:
		color.Cyan(resp.Status)
	case 3:
		color.Yellow(resp.Status)
	case 4:
		color.Red(resp.Status)
	default:
		fmt.Println(resp.Status)
	}
	c.m.Unlock()
}

func (c *Crawler) accessToNext(resp *http.Response, f func(string, *http.Response), d int) error {
	if d <= 0 {
		return nil
	}
	links, err := getLinks(resp)
	if err != nil {
		return err
	}
	for _, link := range links {
		c.m.Lock()
		if _, ok := c.accessedUrls[link]; ok {
			c.m.Unlock()
			continue
		}
		c.accessedUrls[link] = struct{}{}
		c.m.Unlock()
		c.wg.Add(1)
		go c.getUrl(link, f, d)
	}
	return nil
}

func getLinks(res *http.Response) ([]string, error) {
	urls := []string{}
	doc, _ := goquery.NewDocumentFromResponse(res)
	doc.Find("a").Each(func(_ int, s *goquery.Selection) {
		url, _ := s.Attr("href")
		r := regexp.MustCompile(`^(https|http)://(.*)`)
		if !r.MatchString(url) {
			url = res.Request.URL.Scheme + "://" + res.Request.URL.Host + url
		}
		urls = append(urls, url)
	})
	return urls, nil
}

func createConfigFile() (string, error) {
	dir, err := configDir()
	if err != nil {
		return "", err
	}
	if err = os.MkdirAll(dir, 0700); err != nil {
		return "", err
	}
	config := filepath.Join(dir, "config.toml")
	if _, err := os.Stat(config); err != nil {
		fmt.Println("create " + config)
		ioutil.WriteFile(config, []byte("urls = [\"https://example.com\"]"), 0644)
	}
	return config, nil
}

func configDir() (string, error) {
	home, err := homedir.Dir()
	if err != nil {
		return "", err
	}
	dir := filepath.Join(home, ".config", "gocrawsan")
	return dir, nil
}
<commit_msg>Add create config feature<commit_after>package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"bufio"

	"github.com/BurntSushi/toml"
	"github.com/PuerkitoBio/goquery"
	"github.com/fatih/color"
	"github.com/mitchellh/go-homedir"
	"github.com/urfave/cli"
)

var (
	Version  string
	Revision string
)

type configFile struct {
	Urls      []string `toml:"urls"`
	UserAgent string   `toml:"useragent"`
}

func main() {
	cli.VersionPrinter = func(c *cli.Context) {
		fmt.Printf("version=%s revision=%s\n", c.App.Version, Revision)
	}
	app := cli.NewApp()
	app.Name = "gocrawsan"
	app.Usage = "web crawling command utility"
	app.Version = Version
	app.Usage = ""
	app.Flags = []cli.Flag{
		cli.StringFlag{
			Name: "useragent, U",
		},
		cli.StringFlag{
			Name: "config, C",
		},
		cli.BoolFlag{
			Name: "no-redirect",
		},
		cli.IntFlag{
			Name:  "depth",
			Value: 1,
		},
		cli.BoolFlag{
			Name: "debug, D",
		},
	}
	app.Action = func(c *cli.Context) error {
		var config string
		var err error
		if c.String("config") == "" {
			config, err = createConfigFile()
			if err != nil {
				return err
			}
		} else {
			config = c.String("config")
		}

		cr := NewCrawler()
		cr.useragent = c.String("useragent")
		client := &http.Client{}
		if c.Bool("no-redirect") {
			client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
				return http.ErrUseLastResponse
			}
		}
		cr.client = client

		buf, err := ioutil.ReadFile(config)
		if err != nil {
			return err
		}
		file := &configFile{}
		err = toml.Unmarshal(buf, file)
		if err != nil {
			return err
		}
		cr.crawling(file.Urls, c.Int("depth"))
		return nil
	}
	app.Run(os.Args)
}

type Crawler struct {
	m            *sync.Mutex
	wg           *sync.WaitGroup
	useragent    string
	client       *http.Client
	accessedUrls map[string]struct{}
}

func NewCrawler() *Crawler {
	c := &Crawler{
		wg: new(sync.WaitGroup),
		m:  new(sync.Mutex),
		accessedUrls: make(map[string]struct{}),
	}
	return c
}

func (c *Crawler) crawling(urls []string, depth int) error {
	for _, url := range urls {
		c.m.Lock()
		if _, ok := c.accessedUrls[url]; ok {
			c.m.Unlock()
			continue
		}
		c.accessedUrls[url] = struct{}{}
		c.m.Unlock()

		c.wg.Add(1)
		go c.getUrl(url, c.printHttpStatus, depth)
	}
	c.wg.Wait()
	return nil
}

func (c *Crawler) getUrl(url string, f func(string, *http.Response), d int) {
	req, _ := http.NewRequest("GET", url, nil)
	req.Header.Set("User-Agent", c.useragent)
	resp, _ := c.client.Do(req)

	d -= 1
	f(url, resp)
	c.accessToNext(resp, f, d)
	c.wg.Done()
}

func (c *Crawler) printHttpStatus(url string, resp *http.Response) {
	c.m.Lock()
	status := strings.Split(resp.Status, " ")
	code, _ := strconv.Atoi(status[0])
	fmt.Print(url + "\t")
	switch code / 100 {
	case 2:
		color.Cyan(resp.Status)
	case 3:
		color.Yellow(resp.Status)
	case 4:
		color.Red(resp.Status)
	default:
		fmt.Println(resp.Status)
	}
	c.m.Unlock()
}

func (c *Crawler) accessToNext(resp *http.Response, f func(string, *http.Response), d int) error {
	if d <= 0 {
		return nil
	}
	links, err := getLinks(resp)
	if err != nil {
		return err
	}
	for _, link := range links {
		c.m.Lock()
		if _, ok := c.accessedUrls[link]; ok {
			c.m.Unlock()
			continue
		}
		c.accessedUrls[link] = struct{}{}
		c.m.Unlock()
		c.wg.Add(1)
		go c.getUrl(link, f, d)
	}
	return nil
}

func getLinks(res *http.Response) ([]string, error) {
	urls := []string{}
	doc, _ := goquery.NewDocumentFromResponse(res)
	doc.Find("a").Each(func(_ int, s *goquery.Selection) {
		url, _ := s.Attr("href")
		r := regexp.MustCompile(`^(https|http)://(.*)`)
		if !r.MatchString(url) {
			url = res.Request.URL.Scheme + "://" + res.Request.URL.Host + url
		}
		urls = append(urls, url)
	})
	return urls, nil
}

func createConfigFile() (string, error) {
	dir, err := configDir()
	if err != nil {
		return "", err
	}
	if err = os.MkdirAll(dir, 0700); err != nil {
		return "", err
	}
	config := filepath.Join(dir, "config.toml")
	if _, err := os.Stat(config); err != nil {
		reader := bufio.NewReader(os.Stdin)

		fmt.Println("Do you create configfile in " + config + "?(y/N): ")
		answer, _ := reader.ReadString('\n')
		if answer == "y" || answer == "Y" {
			fmt.Println("Input your sample url: ")
			url, _ := reader.ReadString('\n')
			ioutil.WriteFile(config, []byte("urls = [\"" + url + "\"]"), 0644)
		}
	}
	return config, nil
}

func configDir() (string, error) {
	home, err := homedir.Dir()
	if err != nil {
		return "", err
	}
	dir := filepath.Join(home, ".config", "gocrawsan")
	return dir, nil
}
<|endoftext|>


<commit_before>package main

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

	"github.com/nlopes/slack"
	"github.com/robfig/cron"
	"github.com/rprakashg/foodtruck-slack-bot/seattlefoodtruck"
)

const s3Bucket = "https://s3-us-west-2.amazonaws.com/seattlefoodtruck-uploads-prod/"

var (
	rtm           *slack.RTM
	api           *slack.Client
	locations     []string
	channel       string
	token         string
	messageParams = slack.PostMessageParameters{AsUser: true}
	c             *cron.Cron
)

func init() {
	locations = strings.Split(os.Getenv("LOCATION_IDS"), ",")
	channel = os.Getenv("CHANNEL")
	token = os.Getenv("SLACK_TOKEN")
}

func main() {
	api = slack.New(token)
	rtm = api.NewRTM()

	if len(locations) > 0 && channel != "" {
		fmt.Println("Creating a new instance of Cron Scheduler")
		c = cron.New()
		c.AddFunc("0 0 08 * * mon-fri", func() {
			fmt.Println("Executing func in Cron")
			message, err := showTrucksForLocations(locations)
			if err != nil {
				fmt.Println("Failed to get trucks for locations")
			} else {
				log.Println("Message : ", message)
				responseHandler(channel, message)
			}
		})
		//Start the Cron
		fmt.Println("Starting Cron")
		c.Start()
	}

	go rtm.ManageConnection()

Loop:
	for {
		select {
		case msg := <-rtm.IncomingEvents:
			switch ev := msg.Data.(type) {
			case *slack.ConnectedEvent:
				fmt.Println("Connection Count: ", ev.ConnectionCount)

			case *slack.MessageEvent:
				fmt.Printf("Incoming message: %v\n", ev)
				info := rtm.GetInfo()
				prefix := fmt.Sprintf("<@%s> ", info.User.ID)

				//only respond if @mention user is same as bot user id, we don't want to respond to other messages on channel
				if ev.User != info.User.ID && strings.HasPrefix(ev.Text, prefix) {
					respond(rtm, ev, prefix)
				}

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

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

			default:
				//Take no action
			}
		}
	}
}
func respond(rtm *slack.RTM, msg *slack.MessageEvent, prefix string) {
	text := msg.Text
	text = strings.TrimPrefix(text, prefix)
	text = strings.TrimSpace(text)
	text = strings.ToLower(text)

	if text == "help" {
		response := fmt.Sprintf("%s: \n", "*You can ask me*")
		response += fmt.Sprintf("%s \n", "• *show neighborhoods* - to see neighborhoods served")
		response += fmt.Sprintf("%s \n", "• *show locations in <neighborhood>* - to see food truck locations in a neighborhood")
		response += fmt.Sprintf("%s \n", "• *show trucks at <location>* - to see food trucks at a location")

		rtm.SendMessage(rtm.NewOutgoingMessage(response, msg.Channel))
	} else if text == "show neighborhoods" {
		showNeighborhoods(rtm, msg.Channel)
	} else if strings.Contains(text, "show locations") {
		showLocations(rtm, text, msg.Channel)
	} else if strings.Contains(text, "show trucks") {
		showTrucks(rtm, text, msg.Channel)
	} else {
		rtm.SendMessage(rtm.NewOutgoingMessage("Sorry I cannot help you with this, please try help to see things you can ask me", msg.Channel))
	}
}
func showNeighborhoods(rtm *slack.RTM, channel string) {
	var message string
	p, _ := seattlefoodtruck.NewProxy("https://www.seattlefoodtruck.com")
	resp, err := p.GetNeighborhoods()
	if err != nil {
		rtm.SendMessage(rtm.NewOutgoingMessage(err.Error(), channel))
		return
	}
	if len(resp.Neighborhoods) == 0 {
		message = fmt.Sprintf("%s \n", "No Neighborhoods found")
		rtm.SendMessage(rtm.NewOutgoingMessage(message, channel))
		return
	}
	//show all neighborhoods
	message = fmt.Sprintf("%s \n", "*You can find food trucks in following neighborhoods*")
	for _, n := range resp.Neighborhoods {
		message += fmt.Sprintf("• %s \n", n.ID)
	}
	rtm.SendMessage(rtm.NewOutgoingMessage(message, channel))
}

func showLocations(rtm *slack.RTM, text string, channel string) {
	var message string
	tokens := strings.Split(text, "in")
	if len(tokens) < 2 {
		rtm.SendMessage(rtm.NewOutgoingMessage("Missing Neighborhood", channel))
		return
	}
	neighborhood := strings.TrimSpace(tokens[1])
	if len(neighborhood) == 0 {
		rtm.SendMessage(rtm.NewOutgoingMessage("Missing neighborhood", channel))
		return
	}
	p, _ := seattlefoodtruck.NewProxy("https://www.seattlefoodtruck.com")
	lr := seattlefoodtruck.LocationRequest{
		Page:         1,
		Neighborhood: neighborhood,
	}
	resp, err := p.GetLocations(&lr)
	if err != nil {
		rtm.SendMessage(rtm.NewOutgoingMessage(err.Error(), channel))
		return
	}
	if len(resp.Locations) == 0 {
		message = fmt.Sprintf("No locations found at %s neighborhood \n", neighborhood)
		rtm.SendMessage(rtm.NewOutgoingMessage(message, channel))
		return
	}
	message = fmt.Sprintf("%s \n", "*You can find food trucks in following locations*")
	for _, l := range resp.Locations {
		message += fmt.Sprintf("• %s - %v \n", l.Name, l.UID)
	}
	rtm.SendMessage(rtm.NewOutgoingMessage(message, channel))
}

func showTrucks(rtm *slack.RTM, text string, channel string) {
	var message string
	//extract location id from text
	tokens := strings.Split(text, "at")
	if len(tokens) < 2 {
		rtm.SendMessage(rtm.NewOutgoingMessage("Missing location", channel))
		return
	}
	locString := strings.TrimSpace(tokens[1])
	if len(locString) == 0 {
		rtm.SendMessage(rtm.NewOutgoingMessage("Missing location", channel))
		return
	}
	message = getTrucksForLocation(locString)
	//send message to channel
	rtm.SendMessage(rtm.NewOutgoingMessage(message, channel))
}

func getTrucksForLocation(locString string) (message string) {
	location, _ := strconv.Atoi(locString)
	p, _ := seattlefoodtruck.NewProxy("https://www.seattlefoodtruck.com")
	req := seattlefoodtruck.NewLocationEventsRequest(location, 1)
	resp, err := p.GetLocationEvents(&req)
	if err != nil {
		message = err.Error()
		return
	}
	if len(resp.Events) == 0 {
		message = fmt.Sprintf("No events at %v", locString)
		return
	}
	index := find(resp.Events, filterByStartDate)
	if index == -1 {
		message = fmt.Sprintf("No food trucks found at %v", locString)
		return
	}
	event := resp.Events[index]
	st, _ := time.Parse(time.RFC3339, event.StartTime)
	et, _ := time.Parse(time.RFC3339, event.EndTime)
	_, m, d := st.Date()
	message = fmt.Sprintf("*%s* \t %v %v %v - %v \n", event.Location.Name, m, d, st.Format(time.Kitchen), et.Format(time.Kitchen))

	for _, b := range event.Bookings {
		message += fmt.Sprintf("*%v* (%s) %v \n", b.Truck.Name,
			strings.Join(b.Truck.FoodCategories, ", "), s3Bucket+b.Truck.FeaturedPhoto)
	}
	return message
}

//returns the index of found event for today's date, if none returns -1
func find(events []seattlefoodtruck.Event, f func(seattlefoodtruck.Event) bool) int {
	for i, e := range events {
		if f(e) {
			return i
		}
	}
	return -1
}

// used to filter by event start date
func filterByStartDate(event seattlefoodtruck.Event) bool {
	ct := time.Now().Local()
	y1, m1, d1 := ct.Date()
	//parse start time
	st, _ := time.Parse(time.RFC3339, event.StartTime)
	y2, m2, d2 := st.Date()
	if y1 == y2 && m1 == m2 && d1 == d2 {
		return true
	}
	return false
}

func showTrucksForLocations(locations []string) (string, error) {
	var message string
	if len(locations) == 0 {
		fmt.Printf("No locations set \n")
		return "", fmt.Errorf("No locations to show trucks for")
	}
	for _, l := range locations {
		fmt.Printf("Getting trucks for location: %v \n", l)
		message += fmt.Sprintf("%s \n", getTrucksForLocation(l))
	}
	return message, nil
}

func responseHandler(channel string, message string) {
	fmt.Printf("Posting message %s to slack %s \n", message, channel)
	api.PostMessage(channel, message, messageParams)
}
<commit_msg>Iterate all events in a day, skip no-booking events<commit_after>package main

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

	"github.com/nlopes/slack"
	"github.com/robfig/cron"
	"github.com/rprakashg/foodtruck-slack-bot/seattlefoodtruck"
)

const s3Bucket = "https://s3-us-west-2.amazonaws.com/seattlefoodtruck-uploads-prod/"

var (
	rtm           *slack.RTM
	api           *slack.Client
	locations     []string
	channel       string
	token         string
	messageParams = slack.PostMessageParameters{AsUser: true}
	c             *cron.Cron
)

func init() {
	locations = strings.Split(os.Getenv("LOCATION_IDS"), ",")
	channel = os.Getenv("CHANNEL")
	token = os.Getenv("SLACK_TOKEN")
}

func main() {
	api = slack.New(token)
	rtm = api.NewRTM()

	if len(locations) > 0 && channel != "" {
		fmt.Println("Creating a new instance of Cron Scheduler")
		c = cron.New()
		c.AddFunc("0 0 08 * * mon-fri", func() {
			fmt.Println("Executing func in Cron")
			message, err := showTrucksForLocations(locations)
			if err != nil {
				fmt.Println("Failed to get trucks for locations")
			} else {
				log.Println("Message : ", message)
				responseHandler(channel, message)
			}
		})
		//Start the Cron
		fmt.Println("Starting Cron")
		c.Start()
	}

	go rtm.ManageConnection()

Loop:
	for {
		select {
		case msg := <-rtm.IncomingEvents:
			switch ev := msg.Data.(type) {
			case *slack.ConnectedEvent:
				fmt.Println("Connection Count: ", ev.ConnectionCount)

			case *slack.MessageEvent:
				fmt.Printf("Incoming message: %v\n", ev)
				info := rtm.GetInfo()
				prefix := fmt.Sprintf("<@%s> ", info.User.ID)

				//only respond if @mention user is same as bot user id, we don't want to respond to other messages on channel
				if ev.User != info.User.ID && strings.HasPrefix(ev.Text, prefix) {
					respond(rtm, ev, prefix)
				}

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

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

			default:
				//Take no action
			}
		}
	}
}
func respond(rtm *slack.RTM, msg *slack.MessageEvent, prefix string) {
	text := msg.Text
	text = strings.TrimPrefix(text, prefix)
	text = strings.TrimSpace(text)
	text = strings.ToLower(text)

	if text == "help" {
		response := fmt.Sprintf("%s: \n", "*You can ask me*")
		response += fmt.Sprintf("%s \n", "• *show neighborhoods* - to see neighborhoods served")
		response += fmt.Sprintf("%s \n", "• *show locations in <neighborhood>* - to see food truck locations in a neighborhood")
		response += fmt.Sprintf("%s \n", "• *show trucks at <location>* - to see food trucks at a location")

		rtm.SendMessage(rtm.NewOutgoingMessage(response, msg.Channel))
	} else if text == "show neighborhoods" {
		showNeighborhoods(rtm, msg.Channel)
	} else if strings.Contains(text, "show locations") {
		showLocations(rtm, text, msg.Channel)
	} else if strings.Contains(text, "show trucks") {
		showTrucks(rtm, text, msg.Channel)
	} else {
		rtm.SendMessage(rtm.NewOutgoingMessage("Sorry I cannot help you with this, please try help to see things you can ask me", msg.Channel))
	}
}
func showNeighborhoods(rtm *slack.RTM, channel string) {
	var message string
	p, _ := seattlefoodtruck.NewProxy("https://www.seattlefoodtruck.com")
	resp, err := p.GetNeighborhoods()
	if err != nil {
		rtm.SendMessage(rtm.NewOutgoingMessage(err.Error(), channel))
		return
	}
	if len(resp.Neighborhoods) == 0 {
		message = fmt.Sprintf("%s \n", "No Neighborhoods found")
		rtm.SendMessage(rtm.NewOutgoingMessage(message, channel))
		return
	}
	//show all neighborhoods
	message = fmt.Sprintf("%s \n", "*You can find food trucks in following neighborhoods*")
	for _, n := range resp.Neighborhoods {
		message += fmt.Sprintf("• %s \n", n.ID)
	}
	rtm.SendMessage(rtm.NewOutgoingMessage(message, channel))
}

func showLocations(rtm *slack.RTM, text string, channel string) {
	var message string
	tokens := strings.Split(text, "in")
	if len(tokens) < 2 {
		rtm.SendMessage(rtm.NewOutgoingMessage("Missing Neighborhood", channel))
		return
	}
	neighborhood := strings.TrimSpace(tokens[1])
	if len(neighborhood) == 0 {
		rtm.SendMessage(rtm.NewOutgoingMessage("Missing neighborhood", channel))
		return
	}
	p, _ := seattlefoodtruck.NewProxy("https://www.seattlefoodtruck.com")
	lr := seattlefoodtruck.LocationRequest{
		Page:         1,
		Neighborhood: neighborhood,
	}
	resp, err := p.GetLocations(&lr)
	if err != nil {
		rtm.SendMessage(rtm.NewOutgoingMessage(err.Error(), channel))
		return
	}
	if len(resp.Locations) == 0 {
		message = fmt.Sprintf("No locations found at %s neighborhood \n", neighborhood)
		rtm.SendMessage(rtm.NewOutgoingMessage(message, channel))
		return
	}
	message = fmt.Sprintf("%s \n", "*You can find food trucks in following locations*")
	for _, l := range resp.Locations {
		message += fmt.Sprintf("• %s - %v \n", l.Name, l.UID)
	}
	rtm.SendMessage(rtm.NewOutgoingMessage(message, channel))
}

func showTrucks(rtm *slack.RTM, text string, channel string) {
	var message string
	//extract location id from text
	tokens := strings.Split(text, "at")
	if len(tokens) < 2 {
		rtm.SendMessage(rtm.NewOutgoingMessage("Missing location", channel))
		return
	}
	locString := strings.TrimSpace(tokens[1])
	if len(locString) == 0 {
		rtm.SendMessage(rtm.NewOutgoingMessage("Missing location", channel))
		return
	}
	message = getTrucksForLocation(locString)
	//send message to channel
	rtm.SendMessage(rtm.NewOutgoingMessage(message, channel))
}

func getTrucksForLocation(locString string) (message string) {
	location, _ := strconv.Atoi(locString)
	p, _ := seattlefoodtruck.NewProxy("https://www.seattlefoodtruck.com")
	req := seattlefoodtruck.NewLocationEventsRequest(location, 1)
	resp, err := p.GetLocationEvents(&req)
	if err != nil {
		message = err.Error()
		return
	}
	if len(resp.Events) == 0 {
		message = fmt.Sprintf("No events at %v", locString)
		return
	}
	events := find(resp.Events, filterByStartDate)
	if events == nil {
		message = fmt.Sprintf("No food trucks found at %v", locString)
		return
	}
	message = ""
	for index, eventIndex := range events {
		event := resp.Events[eventIndex]
		if len(event.Bookings) != 0 {
			st, _ := time.Parse(time.RFC3339, event.StartTime)
			et, _ := time.Parse(time.RFC3339, event.EndTime)
			_, m, d := st.Date()
			message += fmt.Sprintf("*%s* \t %v %v %v - %v \n", event.Location.Name, m, d, st.Format(time.Kitchen), et.Format(time.Kitchen))

			for _, b := range event.Bookings {
				message += fmt.Sprintf("*%v* (%s) %v \n", b.Truck.Name,
					strings.Join(b.Truck.FoodCategories, ", "), s3Bucket+b.Truck.FeaturedPhoto)
			}
		}
	}
	return message
}

//returns a slice of indeces of found events matching the passed function, if none returns nil
func find(events []seattlefoodtruck.Event, f func(seattlefoodtruck.Event) bool) []int {
	var foundEvents []int
	for i, e := range events {
		if f(e) {
			foundEvents = append(i, 0)
		}
	}
	return foundEvents
}

// used to filter by event start date
func filterByStartDate(event seattlefoodtruck.Event) bool {
	ct := time.Now().Local()
	y1, m1, d1 := ct.Date()
	//parse start time
	st, _ := time.Parse(time.RFC3339, event.StartTime)
	y2, m2, d2 := st.Date()
	if y1 == y2 && m1 == m2 && d1 == d2 {
		return true
	}
	return false
}

func showTrucksForLocations(locations []string) (string, error) {
	var message string
	if len(locations) == 0 {
		fmt.Printf("No locations set \n")
		return "", fmt.Errorf("No locations to show trucks for")
	}
	for _, l := range locations {
		fmt.Printf("Getting trucks for location: %v \n", l)
		message += fmt.Sprintf("%s \n", getTrucksForLocation(l))
	}
	return message, nil
}

func responseHandler(channel string, message string) {
	fmt.Printf("Posting message %s to slack %s \n", message, channel)
	api.PostMessage(channel, message, messageParams)
}
<|endoftext|>


<commit_before>package main

import (
	"bytes"
	"code.google.com/p/gcfg"
	"code.google.com/p/go-uuid/uuid"
	"fmt"
	"github.com/dpapathanasiou/go-recaptcha"
	"github.com/justinas/nosurf"
	"github.com/worr/chrooter"
	"github.com/worr/secstring"
	"html/template"
	"log"
	"math/rand"
	"net/http"
	"net/mail"
	"net/smtp"
	"time"
	txttemplate "text/template"
)

type Config struct {
	Mail struct {
		Email    string
		Username string
		Password string
		Hostname string
		password *secstring.SecString
	}

	Recaptcha struct {
		Private string
	}
}

var t = template.Must(template.ParseFiles("template/index.html"))
var emailTemplate = txttemplate.Must(txttemplate.New("email").Parse("Here is your exclusive Vim download link: http://www.vim.org/download.php?code={{.Code}}"))
var c = make(chan string)
var conf Config

// Default handler
func dispatch(w http.ResponseWriter, r *http.Request) {
	context := map[string]string{
		"token": nosurf.Token(r),
	}

	if r.Method == "POST" {
		context["email"] = r.FormValue("email")
		if !recaptcha.Confirm(r.RemoteAddr, r.FormValue("recaptcha_challenge_field"), r.FormValue("recaptcha_response_field")) {
			http.Error(w, "Failed captcha", http.StatusBadRequest)
			return
		}

		if context["email"] == "" {
			http.Error(w, "Empty email address", http.StatusBadRequest)
			return
		}

		c <- context["email"]
	}

	if err := t.Execute(w, context); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

// Handler for all bad CSRF requests
func failedCSRF(w http.ResponseWriter, r *http.Request) {
	http.Error(w, nosurf.Reason(r).Error(), http.StatusBadRequest)
}

// Pulls email off of the channel and possibly sends download codes
func email() {
	auth := smtp.PlainAuth("", conf.Mail.Username, string(conf.Mail.password.String), conf.Mail.Hostname)

	for addr := range c {
		// Exclusivity
		if r := rand.Intn(3); r != 0 {
			continue
		}

		var emailAddr *mail.Address
		var err error
		if emailAddr, err = mail.ParseAddress(addr); err != nil {
			log.Printf("Failed to send email to %v: %v", emailAddr.Address, err)
			continue
		}

		headers := make(map[string]string)
		headers["To"] = emailAddr.String()
		headers["From"] = conf.Mail.Email
		headers["Date"] = time.Now().String()
		headers["Subject"] = "Your Vim invite is ready"
		headers["MIME-Version"] = "1.0"
		headers["Content-Type"] = "text/plain; charset=\"utf-8\""

		msg := ""
		for key, val := range(headers) {
			msg += fmt.Sprintf("%v: %v\r\n", key, val)
		}

		buf := bytes.NewBuffer(make([]byte, 256))
		if err := emailTemplate.Execute(buf, struct{ Code string }{uuid.NewUUID().String()}); err != nil {
			log.Printf("Can't execute email template: %v", err)
			continue
		}

		msg += fmt.Sprintf("\r\n%v", buf.String())

		conf.Mail.password.Decrypt()
		if err = smtp.SendMail(conf.Mail.Hostname, auth, conf.Mail.Email, []string{emailAddr.Address}, []byte(msg)); err != nil {
			log.Printf("Failed to send email to %v: %v", emailAddr.Address, err)
			continue
		}
		conf.Mail.password.Encrypt()
	}
}

func main() {
	if err := gcfg.ReadFileInto(&conf, "vim.sexy.ini"); err != nil {
		log.Fatalf("Can't read config file: %v", err)
	}

	if err := chrooter.Chroot("www", "/var/chroot/vim.sexy"); err != nil {
		log.Fatalf("Can't chroot: %v", err)
	}

	var err error
	if conf.Mail.password, err = secstring.FromString(&conf.Mail.Password); err != nil {
		log.Fatal(err)
	}

	recaptcha.Init(conf.Recaptcha.Private)

	go email()

	http.HandleFunc("/", dispatch)
	csrf := nosurf.New(http.DefaultServeMux)
	csrf.SetFailureHandler(http.HandlerFunc(failedCSRF))
	if err = http.ListenAndServe("127.0.0.1:8000", csrf); err != nil {
		log.Fatalf("Cannot listen: %v", err)
	}
}
<commit_msg>Fix NULL byte issue<commit_after>package main

import (
	"bytes"
	"code.google.com/p/gcfg"
	"code.google.com/p/go-uuid/uuid"
	"fmt"
	"github.com/dpapathanasiou/go-recaptcha"
	"github.com/justinas/nosurf"
	"github.com/worr/chrooter"
	"github.com/worr/secstring"
	"html/template"
	"log"
	"math/rand"
	"net/http"
	"net/mail"
	"net/smtp"
	"time"
	txttemplate "text/template"
)

type Config struct {
	Mail struct {
		Email    string
		Username string
		Password string
		Hostname string
		password *secstring.SecString
	}

	Recaptcha struct {
		Private string
	}
}

var t = template.Must(template.ParseFiles("template/index.html"))
var emailTemplate = txttemplate.Must(txttemplate.New("email").Parse("Here is your exclusive Vim download link: http://www.vim.org/download.php?code={{.Code}}"))
var c = make(chan string)
var conf Config

// Default handler
func dispatch(w http.ResponseWriter, r *http.Request) {
	context := map[string]string{
		"token": nosurf.Token(r),
	}

	if r.Method == "POST" {
		context["email"] = r.FormValue("email")
		if !recaptcha.Confirm(r.RemoteAddr, r.FormValue("recaptcha_challenge_field"), r.FormValue("recaptcha_response_field")) {
			http.Error(w, "Failed captcha", http.StatusBadRequest)
			return
		}

		if context["email"] == "" {
			http.Error(w, "Empty email address", http.StatusBadRequest)
			return
		}

		c <- context["email"]
	}

	if err := t.Execute(w, context); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

// Handler for all bad CSRF requests
func failedCSRF(w http.ResponseWriter, r *http.Request) {
	http.Error(w, nosurf.Reason(r).Error(), http.StatusBadRequest)
}

// Pulls email off of the channel and possibly sends download codes
func email() {
	auth := smtp.PlainAuth("", conf.Mail.Username, string(conf.Mail.password.String), conf.Mail.Hostname)

	for addr := range c {
		// Exclusivity
		if r := rand.Intn(3); r != 0 {
			continue
		}

		var emailAddr *mail.Address
		var err error
		if emailAddr, err = mail.ParseAddress(addr); err != nil {
			log.Printf("Failed to send email to %v: %v", emailAddr.Address, err)
			continue
		}

		fromAddr := mail.Address{Name:"Vim Download Czar", Address: conf.Mail.Email}
		headers := make(map[string]string)
		headers["To"] = emailAddr.String()
		headers["From"] = fromAddr.String()
		headers["Date"] = time.Now().String()
		headers["Subject"] = "Your Vim invite is ready"
		headers["MIME-Version"] = "1.0"
		headers["Content-Type"] = "text/plain; charset=\"utf-8\""

		msg := ""
		for key, val := range(headers) {
			msg += fmt.Sprintf("%v: %v\r\n", key, val)
		}

		buf := bytes.NewBuffer(make([]byte, 0))
		if err := emailTemplate.Execute(buf, struct{ Code string }{uuid.NewUUID().String()}); err != nil {
			log.Printf("Can't execute email template: %v", err)
			continue
		}

		msg += fmt.Sprintf("\r\n%s", buf)

		conf.Mail.password.Decrypt()
		if err = smtp.SendMail(conf.Mail.Hostname, auth, fromAddr.Address, []string{emailAddr.Address}, []byte(msg)); err != nil {
			log.Printf("Failed to send email to %v: %v", emailAddr.Address, err)
			continue
		}
		conf.Mail.password.Encrypt()
	}
}

func main() {
	if err := gcfg.ReadFileInto(&conf, "vim.sexy.ini"); err != nil {
		log.Fatalf("Can't read config file: %v", err)
	}

	if err := chrooter.Chroot("www", "/var/chroot/vim.sexy"); err != nil {
		log.Fatalf("Can't chroot: %v", err)
	}

	var err error
	if conf.Mail.password, err = secstring.FromString(&conf.Mail.Password); err != nil {
		log.Fatal(err)
	}

	recaptcha.Init(conf.Recaptcha.Private)

	go email()

	http.HandleFunc("/", dispatch)
	csrf := nosurf.New(http.DefaultServeMux)
	csrf.SetFailureHandler(http.HandlerFunc(failedCSRF))
	if err = http.ListenAndServe("127.0.0.1:8000", csrf); err != nil {
		log.Fatalf("Cannot listen: %v", err)
	}
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"os/signal"
	"regexp"
	"strings"
	"syscall"
	"time"

	"github.com/cloudflare/cloudflare-go"
	"github.com/comail/colog"
	"github.com/patrickmn/go-cache"
	"github.com/pkg/errors"
	"github.com/spf13/viper"
)

const (
	defaultConfig = `{
	"cf_auth_key": "[put your api key here]",
	"cf_auth_email": "[put your cf email here]",
	"zone_name": "[put the name of the zone you want to update here]",
	"update_interval": "1m",
	"log_level": "warn",
	"ipv6": false
}
`

	updateModeV6 = "ipv6"
	updateModeV4 = "ipv4"
)

var (
	state    *cache.Cache
	config   *viper.Viper
	cf       *cloudflare.API
	hostname string
	zoneName string = "nkcmr.net" // @todo: make this configurable

	errNoRecord = errors.New("could not find a matching dns record")
)

type updateResult struct {
	err       error
	cfUpdated bool
}

func configFile() string {
	return strings.TrimRight(os.Getenv("HOME"), string(os.PathSeparator)) + "/.cfupdate.json"
}

func readConfig(cfg io.Reader) (*viper.Viper, error) {
	v := viper.New()
	v.SetConfigType("json")

	v.SetDefault("ipv6", false)
	v.SetDefault("update_interval", "5s")

	return v, v.ReadConfig(cfg)
}

func updateInterval(v *viper.Viper) time.Duration {
	defDur := time.Duration(5) * time.Second
	if !v.IsSet("update_interval") {
		log.Println("warn: update_interval not specified in configuration. defaulting to 5s.")
		return defDur
	}
	d, err := time.ParseDuration(v.GetString("update_interval"))
	if err != nil {
		log.Println("error: could not parse update_interval: %s", err.Error())
		return defDur
	}
	return d
}

func init() {
	colog.Register()
	colog.ParseFields(true)
	colog.SetMinLevel(colog.LWarning)
	state = cache.New(time.Duration(30)*time.Minute, time.Duration(5)*time.Second)
}

func getHostname(v *viper.Viper) string {
	if v.IsSet("hostname") {
		return v.GetString("hostname")
	}
	var (
		err error
		hn  string
	)
	hn, err = os.Hostname()
	if err != nil {
		log.Fatalf("alert: %s", err.Error())
	}
	ignoreHNParts := regexp.MustCompile(`(-[0-9]+)?(\.local)$`)
	return ignoreHNParts.ReplaceAllString(hn, "")
}

func setAddress(fam, addr string) {
	state.Set(fmt.Sprintf("%s.addr", fam), addr, cache.NoExpiration)
}

func getAddress(fam string) string {
	v, ok := state.Get(fmt.Sprintf("%s.addr", fam))
	if !ok || v == nil {
		return "<nil>"
	}
	return v.(string)
}

func main() {
	{
		var err error
		f, err := os.Open(configFile())
		if err != nil {
			if os.IsNotExist(err) {
				log.Printf("looks like you don't have a config file setup. let me create one for you at: %s", configFile())
				f, err = os.Create(configFile())
				f.Write([]byte(defaultConfig))
				f.Close()
				os.Exit(1)
			} else {
				log.Fatalf("alert: %s", err.Error())
			}
		}
		config, err = readConfig(f)
		f.Close()
		if err != nil {
			log.Fatalf("alert: %s", err.Error())
		}
	}

	if !config.IsSet("zone_name") {
		log.Fatal("alert: zone_name is not defined in configuration")
	}
	if !config.IsSet("cf_auth_key") {
		log.Fatal("alert: cf_auth_key is not defined in configuration")
	}
	if !config.IsSet("cf_auth_email") {
		log.Fatal("alert: cf_auth_email is not defined in configuration")
	}

	{
		var err error
		cf, err = cloudflare.New(config.GetString("cf_auth_key"), config.GetString("cf_auth_email"))
		if err != nil {
			log.Fatalf("alert: %s", err.Error())
		}
	}

	if config.IsSet("log_level") {
		l, err := colog.ParseLevel(config.GetString("log_level"))
		if err == nil {
			colog.SetMinLevel(l)
		}
	}

	results := map[string]chan updateResult{}
	sigchan := make(chan os.Signal, 1)
	signal.Notify(sigchan, syscall.SIGINT, syscall.SIGTERM)

coreLoop:
	for {
		select {
		case v4result := <-results[updateModeV4]:
			close(results[updateModeV4])
			delete(results, updateModeV4)
			if v4result.err != nil {
				log.Printf("error: v4 update error: %s", v4result.err.Error())
			}
			if v4result.cfUpdated {
				log.Printf("info: cf updated with new ip address: %s", getAddress(updateModeV4))
			}
		case v6result := <-results[updateModeV6]:
			close(results[updateModeV6])
			delete(results, updateModeV6)
			if v6result.err != nil {
				log.Printf("error: v6 update error: %s", v6result.err.Error())
			}
			if v6result.cfUpdated {
				log.Printf("info: cf updated with new ip address: %s", getAddress(updateModeV6))
			}
		case <-time.After(updateInterval(config)):
			results[updateModeV4] = make(chan updateResult, 1)
			go update(updateModeV4, results[updateModeV4])
			if config.GetBool("ipv6") {
				results[updateModeV6] = make(chan updateResult, 1)
				go update(updateModeV6, results[updateModeV6])
			}
		case sig := <-sigchan:
			log.Printf("received %s signal. exiting...", sig.String())
			cleanup()
			break coreLoop
		}
	}
}

func cleanup() {
	state.Flush()
}

func update(mode string, result chan updateResult) {
	log.Printf("debug: updating %s address", mode)
	ur := updateResult{
		err:       nil,
		cfUpdated: false,
	}
	defer func() { result <- ur }()

	// get current ip address
	ip, err := icanhazip(mode)
	if err != nil {
		ur.err = errors.Wrap(err, "failed to retrieve ip address")
		return
	}

	if ip == getAddress(mode) {
		log.Printf("debug: no change in %s address", mode)
		return
	}
	log.Printf("info: ip address has changed from %s to %s", getAddress(mode), ip)

	zid, err := getZoneID(config.GetString("zone_name"))
	if err != nil {
		ur.err = errors.Wrap(err, "failed to retrieve zone id")
		return
	}

	rid, rc, err := getRecord(zid, getHostname(config), mode)
	if err != nil {
		ur.err = errors.Wrap(err, "failed to retrieve record id")
		return
	}
	if rc == ip {
		setAddress(mode, ip)
		log.Printf("cloudflare has up-to-date %s address", mode)
		return
	}

	err = cf.UpdateDNSRecord(zid, rid, cloudflare.DNSRecord{
		Content: ip,
	})
	if err != nil {
		ur.err = errors.Wrap(err, "failed to update cloudflare")
		return
	}
	ur.cfUpdated = true
	setAddress(mode, ip)
	return
}

func icanhazip(mode string) (string, error) {
	response := make(chan *http.Response)
	errc := make(chan error)
	go func() {
		res, err := http.Get(fmt.Sprintf("https://%s.icanhazip.com", mode))
		if err != nil {
			errc <- err
			return
		}
		response <- res
	}()
	select {
	case <-time.After(time.Duration(5) * time.Second):
		return "", errors.Errorf("%s address request timed out", mode)
	case err := <-errc:
		return "", errors.Wrapf(err, "%s address request encountered an error", mode)
	case res := <-response:
		if res.StatusCode != http.StatusOK {
			return "", errors.Errorf("%s address request returned non-ok status code", mode)
		}
		defer res.Body.Close()
		bytes, err := ioutil.ReadAll(res.Body)
		if err != nil {
			return "", errors.Wrapf(err, "%s address request failed to read response body", mode)
		}
		return strings.TrimSpace(string(bytes)), nil
	}
	log.Panicf("%s address request reached an unreachable code path", mode)
	return "", nil
}

func getZoneID(zoneName string) (string, error) {
	stateKey := fmt.Sprintf("zone.%s.id", zoneName)
	if v, ok := state.Get(stateKey); ok {
		return v.(string), nil
	}
	zones, err := cf.ListZones()
	if err != nil {
		return "", err
	}
	for _, z := range zones {
		if z.Name == zoneName {
			state.SetDefault(stateKey, z.ID)
			return z.ID, nil
		}
	}
	return "", errors.Errorf(`could not find zone with name "%s"`, zoneName)
}

func getRecord(zoneID, recordName, fam string) (string, string, error) {
	fqdn := fmt.Sprintf("%s.%s", recordName, config.GetString("zone_name"))
	stateKey := fmt.Sprintf("record.%s.%s.%s.id", zoneID, recordName, fam)
	if v, ok := state.Get(stateKey); ok {
		rec := v.(cloudflare.DNSRecord)
		return rec.ID, rec.Content, nil
	}
	recs, err := cf.DNSRecords(zoneID, cloudflare.DNSRecord{})
	if err != nil {
		return "", "", err
	}
	var recType string
	if fam == updateModeV6 {
		recType = "AAAA"
	} else if fam == updateModeV4 {
		recType = "A"
	} else {
		log.Panicf("unknown ip address family: %s", fam)
	}
	for _, r := range recs {
		if r.Name == fqdn && r.Type == recType {
			state.SetDefault(stateKey, r)
			return r.ID, r.Content, nil
		}
	}
	return "", "", errors.Errorf(`could not find %s record with name "%s"`, recType, recordName)
}
<commit_msg>make sure everything is concurrently safe. add a pretty banner.<commit_after>package main

import (
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"os/signal"
	"regexp"
	"strings"
	"sync"
	"syscall"
	"time"

	"github.com/cloudflare/cloudflare-go"
	"github.com/comail/colog"
	"github.com/patrickmn/go-cache"
	"github.com/pkg/errors"
	"github.com/spf13/viper"
)

const (
	version = "v1.0.0"

	defaultConfig = `{
	"cf_auth_key": "[put your api key here]",
	"cf_auth_email": "[put your cf email here]",
	"zone_name": "[put the name of the zone you want to update here]",
	"update_interval": "1m",
	"log_level": "warn",
	"ipv6": false
}
`

	banner = `
     __               _      _
 __ / _|_  _ _ __  __| |__ _| |_ ___
/ _|  _| || | '_ \/ _` + "` / _`" + ` |  _/ -_)
\__|_|  \_,_| .__/\__,_\__,_|\__\___|
            |_|
`

	updateModeV6 = "ipv6"
	updateModeV4 = "ipv4"
)

var (
	// there are some concurrent map read and map write issues somewhere in go's HTTP2 code
	cfReqLock = sync.Mutex{}
	cf        *cloudflare.API
	state     *cache.Cache
	config    *viper.Viper
)

type updateResult struct {
	err       error
	cfUpdated bool
}

func configFile() string {
	return strings.TrimRight(os.Getenv("HOME"), string(os.PathSeparator)) + "/.cfupdate.json"
}

func readConfig(cfg io.Reader) (*viper.Viper, error) {
	v := viper.New()
	v.SetConfigType("json")

	v.SetDefault("ipv6", false)
	v.SetDefault("update_interval", "5s")

	return v, v.ReadConfig(cfg)
}

func updateInterval(v *viper.Viper) time.Duration {
	defDur := time.Duration(5) * time.Second
	if !v.IsSet("update_interval") {
		log.Println("warn: update_interval not specified in configuration. defaulting to 5s.")
		return defDur
	}
	d, err := time.ParseDuration(v.GetString("update_interval"))
	if err != nil {
		log.Println("error: could not parse update_interval: %s", err.Error())
		return defDur
	}
	return d
}

func init() {
	colog.Register()
	colog.ParseFields(true)
	colog.SetMinLevel(colog.LWarning)
	state = cache.New(time.Duration(30)*time.Minute, time.Duration(5)*time.Second)
}

func getHostname(v *viper.Viper) string {
	if v.IsSet("hostname") {
		return v.GetString("hostname")
	}
	var (
		err error
		hn  string
	)
	hn, err = os.Hostname()
	if err != nil {
		log.Fatalf("alert: %s", err.Error())
	}
	return regexp.MustCompile(`(-[0-9]+)?(\.local)$`).ReplaceAllString(hn, "")
}

func setAddress(fam, addr string) {
	state.Set(fmt.Sprintf("%s.addr", fam), addr, cache.NoExpiration)
}

func getAddress(fam string) string {
	v, ok := state.Get(fmt.Sprintf("%s.addr", fam))
	if !ok || v == nil {
		return "<nil>"
	}
	return v.(string)
}

func main() {
	os.Stdout.Write([]byte(banner))
	os.Stdout.Write([]byte(fmt.Sprintf("\n(version: %s)\n\n", version)))
	{
		var err error
		f, err := os.Open(configFile())
		if err != nil {
			if os.IsNotExist(err) {
				log.Printf("looks like you don't have a config file setup. let me create one for you at: %s", configFile())
				f, err = os.Create(configFile())
				f.Write([]byte(defaultConfig))
				f.Close()
				os.Exit(1)
			} else {
				log.Fatalf("alert: %s", err.Error())
			}
		}
		config, err = readConfig(f)
		f.Close()
		if err != nil {
			log.Fatalf("alert: %s", err.Error())
		}
	}

	if !config.IsSet("zone_name") {
		log.Fatal("alert: zone_name is not defined in configuration")
	}
	if !config.IsSet("cf_auth_key") {
		log.Fatal("alert: cf_auth_key is not defined in configuration")
	}
	if !config.IsSet("cf_auth_email") {
		log.Fatal("alert: cf_auth_email is not defined in configuration")
	}

	{
		var err error
		cf, err = cloudflare.New(config.GetString("cf_auth_key"), config.GetString("cf_auth_email"))
		if err != nil {
			log.Fatalf("alert: %s", err.Error())
		}
	}

	if config.IsSet("log_level") {
		l, err := colog.ParseLevel(config.GetString("log_level"))
		if err == nil {
			colog.SetMinLevel(l)
		}
	}

	var setResultChan func(string, chan updateResult)
	var rmResultsChan func(string)
	var getResultsChan func(string) chan updateResult
	{
		lock := sync.RWMutex{}
		results := map[string]chan updateResult{}
		setResultChan = func(fam string, c chan updateResult) {
			lock.Lock()
			defer lock.Unlock()
			results[fam] = c
		}
		rmResultsChan = func(fam string) {
			lock.Lock()
			defer lock.Unlock()
			delete(results, fam)
		}
		getResultsChan = func(fam string) chan updateResult {
			lock.RLock()
			defer lock.RUnlock()
			return results[fam]
		}
	}

	sigchan := make(chan os.Signal, 1)
	signal.Notify(sigchan, syscall.SIGINT, syscall.SIGTERM)

coreLoop:
	for {
		select {
		case v4result := <-getResultsChan(updateModeV4):
			close(getResultsChan(updateModeV4))
			rmResultsChan(updateModeV4)
			if v4result.err != nil {
				log.Printf("error: v4 update error: %s", v4result.err.Error())
			}
			if v4result.cfUpdated {
				log.Printf("info: cf updated with new ip address: %s", getAddress(updateModeV4))
			}
		case v6result := <-getResultsChan(updateModeV6):
			close(getResultsChan(updateModeV6))
			rmResultsChan(updateModeV6)
			if v6result.err != nil {
				log.Printf("error: v6 update error: %s", v6result.err.Error())
			}
			if v6result.cfUpdated {
				log.Printf("info: cf updated with new ip address: %s", getAddress(updateModeV6))
			}
		case <-time.After(updateInterval(config)):
			setResultChan(updateModeV4, make(chan updateResult, 1))
			go update(updateModeV4, getResultsChan(updateModeV4))
			if config.GetBool("ipv6") {
				setResultChan(updateModeV6, make(chan updateResult, 1))
				go update(updateModeV6, getResultsChan(updateModeV6))
			}
		case sig := <-sigchan:
			log.Printf("received %s signal. exiting...", sig.String())
			cleanup()
			break coreLoop
		}
	}
}

func cleanup() {
	state.Flush()
}

func update(mode string, result chan updateResult) {
	log.Printf("debug: updating %s address", mode)
	ur := updateResult{
		err:       nil,
		cfUpdated: false,
	}
	defer func() { result <- ur }()

	// get current ip address
	ip, err := icanhazip(mode)
	if err != nil {
		ur.err = errors.Wrap(err, "failed to retrieve ip address")
		return
	}

	if ip == getAddress(mode) {
		log.Printf("debug: no change in %s address", mode)
		return
	}
	log.Printf("info: ip address has changed from %s to %s", getAddress(mode), ip)

	zid, err := getZoneID(config.GetString("zone_name"))
	if err != nil {
		ur.err = errors.Wrap(err, "failed to retrieve zone id")
		return
	}

	rid, rc, err := getRecord(zid, getHostname(config), mode)
	if err != nil {
		ur.err = errors.Wrap(err, "failed to retrieve record id")
		return
	}
	if rc == ip {
		setAddress(mode, ip)
		log.Printf("cloudflare has up-to-date %s address", mode)
		return
	}

	cfReqLock.Lock()
	defer cfReqLock.Unlock()
	err = cf.UpdateDNSRecord(zid, rid, cloudflare.DNSRecord{
		Content: ip,
	})
	if err != nil {
		ur.err = errors.Wrap(err, "failed to update cloudflare")
		return
	}
	ur.cfUpdated = true
	setAddress(mode, ip)
	return
}

func icanhazip(mode string) (string, error) {
	response := make(chan *http.Response)
	errc := make(chan error)
	go func() {
		res, err := http.Get(fmt.Sprintf("https://%s.icanhazip.com", mode))
		if err != nil {
			errc <- err
			return
		}
		response <- res
	}()
	select {
	case <-time.After(time.Duration(5) * time.Second):
		return "", errors.Errorf("%s address request timed out", mode)
	case err := <-errc:
		return "", errors.Wrapf(err, "%s address request encountered an error", mode)
	case res := <-response:
		if res.StatusCode != http.StatusOK {
			return "", errors.Errorf("%s address request returned non-ok status code", mode)
		}
		defer res.Body.Close()
		bytes, err := ioutil.ReadAll(res.Body)
		if err != nil {
			return "", errors.Wrapf(err, "%s address request failed to read response body", mode)
		}
		return strings.TrimSpace(string(bytes)), nil
	}
	log.Panicf("%s address request reached an unreachable code path", mode)
	return "", nil
}

func getZoneID(zoneName string) (string, error) {
	cfReqLock.Lock()
	defer cfReqLock.Unlock()
	stateKey := fmt.Sprintf("zone.%s.id", zoneName)
	if v, ok := state.Get(stateKey); ok {
		return v.(string), nil
	}
	zones, err := cf.ListZones()
	if err != nil {
		return "", err
	}
	for _, z := range zones {
		if z.Name == zoneName {
			state.SetDefault(stateKey, z.ID)
			return z.ID, nil
		}
	}
	return "", errors.Errorf(`could not find zone with name "%s"`, zoneName)
}

func getRecord(zoneID, recordName, fam string) (string, string, error) {
	cfReqLock.Lock()
	defer cfReqLock.Unlock()
	fqdn := fmt.Sprintf("%s.%s", recordName, config.GetString("zone_name"))
	stateKey := fmt.Sprintf("record.%s.%s.%s.id", zoneID, recordName, fam)
	if v, ok := state.Get(stateKey); ok {
		rec := v.(cloudflare.DNSRecord)
		return rec.ID, rec.Content, nil
	}
	recs, err := cf.DNSRecords(zoneID, cloudflare.DNSRecord{})
	if err != nil {
		return "", "", err
	}
	var recType string
	if fam == updateModeV6 {
		recType = "AAAA"
	} else if fam == updateModeV4 {
		recType = "A"
	} else {
		log.Panicf("unknown ip address family: %s", fam)
	}
	for _, r := range recs {
		if r.Name == fqdn && r.Type == recType {
			state.SetDefault(stateKey, r)
			return r.ID, r.Content, nil
		}
	}
	return "", "", errors.Errorf(`could not find %s record with name "%s"`, recType, recordName)
}
<|endoftext|>