<commit_before>package gocd

import (
	"bytes"
	"context"
	"crypto/tls"
	"encoding/json"
	"io"
	"io/ioutil"
	"net/http"
	"net/url"
)

const (
	libraryVersion = "1"
	userAgent      = "go-gocd/" + libraryVersion
	apiV1          = "application/vnd.go.cd.v1+json"
	apiV2          = "application/vnd.go.cd.v2+json"
	apiV3          = "application/vnd.go.cd.v3+json"
	apiV4          = "application/vnd.go.cd.v4+json"
)

type StringResponse struct {
	Message string `json:"message"`
}

type ClientInterface interface{}

type APIResponse struct {
	Http    *http.Response
	Body    string
	Request *APIRequest
}

type APIRequest struct {
	Http *http.Request
	Body string
}

type Client struct {
	client    *http.Client
	BaseURL   *url.URL
	UserAgent string
	Auth      *Auth

	Agents            *AgentsService
	PipelineGroups    *PipelineGroupsService
	Stages            *StagesService
	Jobs              *JobsService
	PipelineTemplates *PipelineTemplatesService

	common service
	cookie string
}

type PaginationResponse struct {
	Offset   int64 `json:"offset"`
	Total    int64 `json:"total"`
	PageSize int64 `json:"page_size"`
}

type service struct {
	client *Client
}

type Auth struct {
	Username string
	Password string
}

func NewClient(gocdBaseUrl string, auth *Auth, httpClient *http.Client, checkSsl bool) *Client {
	if httpClient == nil {
		httpClient = http.DefaultClient
	}

	if !checkSsl {
		httpClient.Transport = &http.Transport{
			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
		}
	}

	baseURL, _ := url.Parse(gocdBaseUrl)

	c := &Client{client: httpClient, BaseURL: baseURL, UserAgent: userAgent}

	if &auth != nil {
		c.Auth = auth
	}
	c.common.client = c
	c.Agents = (*AgentsService)(&c.common)
	c.PipelineGroups = (*PipelineGroupsService)(&c.common)
	c.Stages = (*StagesService)(&c.common)
	c.Jobs = (*JobsService)(&c.common)
	c.PipelineTemplates = (*PipelineTemplatesService)(&c.common)
	return c
}

func (c *Client) NewRequest(method, urlStr string, body interface{}, apiVersion string) (*APIRequest, error) {
	rel, err := url.Parse("api/" + urlStr)

	if err != nil {
		return nil, err
	}

	if apiVersion == "" {
		apiVersion = apiV1
	}

	u := c.BaseURL.ResolveReference(rel)

	request := &APIRequest{}

	var buf io.ReadWriter
	if body != nil {
		buf = new(bytes.Buffer)
		err := json.NewEncoder(buf).Encode(body)
		if err != nil {
			return nil, err
		}
		bdy, _ := ioutil.ReadAll(buf)
		request.Body = string(bdy)
	}

	req, err := http.NewRequest(method, u.String(), buf)
	request.Http = req
	if err != nil {
		return nil, err
	}

	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	req.Header.Set("Accept", apiVersion)
	req.Header.Set("User-Agent", c.UserAgent)

	if c.cookie == "" {
		if c.Auth != nil {
			req.SetBasicAuth(c.Auth.Username, c.Auth.Password)
		}
	} else {
		req.Header.Set("Cookie", c.cookie)
	}

	return request, nil
}

func (c *Client) Do(ctx context.Context, req *APIRequest, v interface{}) (*APIResponse, error) {

	req.Http = req.Http.WithContext(ctx)

	response := &APIResponse{
		Request: req,
	}

	resp, err := c.client.Do(req.Http)
	if err != nil {
		if e, ok := err.(*url.Error); ok {
			if url, err := url.Parse(e.URL); err == nil {
				e.URL = sanitizeURL(url).String()
				return nil, e
			}
		}

		return nil, err
	}

	response.Http = resp
	//err = CheckResponse(resp)
	//if err != nil {
	//	return response, err
	//}

	if v != nil {
		if w, ok := v.(io.Writer); ok {
			io.Copy(w, resp.Body)
		} else {
			bdy, err := ioutil.ReadAll(resp.Body)
			err = json.Unmarshal(bdy, v)
			response.Body = string(bdy)
			if err == io.EOF {
				err = nil // ignore EOF errors caused by empty response body
			}
		}
	}

	return response, err
}

// sanitizeURL redacts the client_secret parameter from the URL which may be
// exposed to the user.
func sanitizeURL(uri *url.URL) *url.URL {
	if uri == nil {
		return nil
	}
	params := uri.Query()
	if len(params.Get("client_secret")) > 0 {
		params.Set("client_secret", "REDACTED")
		uri.RawQuery = params.Encode()
	}
	return uri
}

// addOptions adds the parameters in opt as URL query parameters to s. opt
// must be a struct whose fields may contain "url" tags.
//func addOptions(s string, opt interface{}) (string, error) {
func addOptions(s string) (string, error) {
	//v := reflect.ValueOf(opt)
	//if v.Kind() == reflect.Ptr && v.IsNil() {
	//	return s, nil
	//}

	u, err := url.Parse(s)
	if err != nil {
		return s, err
	}

	return u.String(), nil
}

//type ResourceNotFound struct {
//	When     time.Time
//	Resource string
//}
//
//func (e ResourceNotFound) Error() string {
//	return fmt.Sprintf("Could not find '%s'.", e.Resource)
//	error.
//}
<commit_msg>Fixed a bug where the request body would be reead twice causing a null body to be sent in requests<commit_after>package gocd

import (
	"bytes"
	"context"
	"crypto/tls"
	"encoding/json"
	"io"
	"io/ioutil"
	"net/http"
	"net/url"
)

const (
	libraryVersion = "1"
	userAgent      = "go-gocd/" + libraryVersion
	apiV1          = "application/vnd.go.cd.v1+json"
	apiV2          = "application/vnd.go.cd.v2+json"
	apiV3          = "application/vnd.go.cd.v3+json"
	apiV4          = "application/vnd.go.cd.v4+json"
)

type StringResponse struct {
	Message string `json:"message"`
}

type ClientInterface interface{}

type APIResponse struct {
	Http    *http.Response
	Body    string
	Request *APIRequest
}

type APIRequest struct {
	Http *http.Request
	Body string
}

type Client struct {
	client    *http.Client
	BaseURL   *url.URL
	UserAgent string
	Auth      *Auth

	Agents            *AgentsService
	PipelineGroups    *PipelineGroupsService
	Stages            *StagesService
	Jobs              *JobsService
	PipelineTemplates *PipelineTemplatesService

	common service
	cookie string
}

type PaginationResponse struct {
	Offset   int64 `json:"offset"`
	Total    int64 `json:"total"`
	PageSize int64 `json:"page_size"`
}

type service struct {
	client *Client
}

type Auth struct {
	Username string
	Password string
}

func NewClient(gocdBaseUrl string, auth *Auth, httpClient *http.Client, checkSsl bool) *Client {
	if httpClient == nil {
		httpClient = http.DefaultClient
	}

	if !checkSsl {
		httpClient.Transport = &http.Transport{
			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
		}
	}

	baseURL, _ := url.Parse(gocdBaseUrl)

	c := &Client{client: httpClient, BaseURL: baseURL, UserAgent: userAgent}

	if &auth != nil {
		c.Auth = auth
	}
	c.common.client = c
	c.Agents = (*AgentsService)(&c.common)
	c.PipelineGroups = (*PipelineGroupsService)(&c.common)
	c.Stages = (*StagesService)(&c.common)
	c.Jobs = (*JobsService)(&c.common)
	c.PipelineTemplates = (*PipelineTemplatesService)(&c.common)
	return c
}

func (c *Client) NewRequest(method, urlStr string, body interface{}, apiVersion string) (*APIRequest, error) {
	rel, err := url.Parse("api/" + urlStr)

	if err != nil {
		return nil, err
	}

	if apiVersion == "" {
		apiVersion = apiV1
	}

	u := c.BaseURL.ResolveReference(rel)

	request := &APIRequest{}

	var buf io.ReadWriter
	if body != nil {
		buf = new(bytes.Buffer)
		err := json.NewEncoder(buf).Encode(body)
		if err != nil {
			return nil, err
		}
		bdy, _ := ioutil.ReadAll(buf)
		request.Body = string(bdy)

		buf = new(bytes.Buffer)
		err = json.NewEncoder(buf).Encode(body)
		if err != nil {
			return nil, err
		}
	}

	req, err := http.NewRequest(method, u.String(), buf)
	request.Http = req
	if err != nil {
		return nil, err
	}

	if body != nil {
		req.Header.Set("Content-Type", "application/json")
	}
	req.Header.Set("Accept", apiVersion)
	req.Header.Set("User-Agent", c.UserAgent)

	if c.cookie == "" {
		if c.Auth != nil {
			req.SetBasicAuth(c.Auth.Username, c.Auth.Password)
		}
	} else {
		req.Header.Set("Cookie", c.cookie)
	}

	return request, nil
}

func (c *Client) Do(ctx context.Context, req *APIRequest, v interface{}) (*APIResponse, error) {

	req.Http = req.Http.WithContext(ctx)

	response := &APIResponse{
		Request: req,
	}

	resp, err := c.client.Do(req.Http)
	if err != nil {
		if e, ok := err.(*url.Error); ok {
			if url, err := url.Parse(e.URL); err == nil {
				e.URL = sanitizeURL(url).String()
				return nil, e
			}
		}

		return nil, err
	}

	response.Http = resp
	//err = CheckResponse(resp)
	//if err != nil {
	//	return response, err
	//}

	if v != nil {
		if w, ok := v.(io.Writer); ok {
			io.Copy(w, resp.Body)
		} else {
			bdy, err := ioutil.ReadAll(resp.Body)
			err = json.Unmarshal(bdy, v)
			response.Body = string(bdy)
			if err == io.EOF {
				err = nil // ignore EOF errors caused by empty response body
			}
		}
	}

	return response, err
}

// sanitizeURL redacts the client_secret parameter from the URL which may be
// exposed to the user.
func sanitizeURL(uri *url.URL) *url.URL {
	if uri == nil {
		return nil
	}
	params := uri.Query()
	if len(params.Get("client_secret")) > 0 {
		params.Set("client_secret", "REDACTED")
		uri.RawQuery = params.Encode()
	}
	return uri
}

// addOptions adds the parameters in opt as URL query parameters to s. opt
// must be a struct whose fields may contain "url" tags.
//func addOptions(s string, opt interface{}) (string, error) {
func addOptions(s string) (string, error) {
	//v := reflect.ValueOf(opt)
	//if v.Kind() == reflect.Ptr && v.IsNil() {
	//	return s, nil
	//}

	u, err := url.Parse(s)
	if err != nil {
		return s, err
	}

	return u.String(), nil
}

//type ResourceNotFound struct {
//	When     time.Time
//	Resource string
//}
//
//func (e ResourceNotFound) Error() string {
//	return fmt.Sprintf("Could not find '%s'.", e.Resource)
//	error.
//}
<|endoftext|>


<commit_before>// Copyright (C) 2012, The gohg Authors. All rights reserved.
// Use of this source code is governed by a BSD style license
// that can be found in the LICENSE.txt file.

// Package gohg is a Go client library for using the Mercurial dvcs
// via it's Command Server.
//
// For Mercurial see: http://mercurial/selenic.com/wiki.
//
// For the Hg Command Server see: http://mercurial.selenic.com/wiki/CommandServer.
package gohg

import (
	"bytes"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"strings"
)

var repo string
var err error
var libdir string
var logfile string

var hgserver *exec.Cmd

// The in and out pipe ends are to be considered from the point of view
// of the Hg Command Server instance.
var pout io.ReadCloser
var pin io.WriteCloser

type hgMsg struct {
	Ch   string
	Ln   uint
	Data string
}

type hgCmd struct {
	Cmd  string
	Ln   uint
	Args string
}

// init takes care of some householding, namely preparing a logfile where
// all communication between this lib and the Hg CS can be logged.
func init() {
	var exedir string
	exedir = path.Dir(os.Args[0])
	exedir, err = filepath.Abs(exedir)
	if err != nil {
		log.Fatal("Could not determine path for the gohg.log logfile.")
	}
	logfile = exedir + string(os.PathSeparator) + "gohg.log"
} // init()

// Connect establishes the connection with the Mercurial CommandServer.
//
// Arguments:
//	hgexe
//		The command to run mercurial. Optional.
//	reponame
//		The folder of the Hg repository to work on. Optional.
//		When blanc the folder where the program is run is used
//		(see function locateRepository()).
//	config
//		Configuration settings that will be added to the necessary
//		/fixed settings (see composeHgConfig() for more).
//
// Returns an error if the connection could not be established properly.
func Connect(hgexe string, reponame string, config []string) error {

	// for example:
	// hgserver = exec.Command("M:\\DEV\\hg-stable\\hg",	// the Hg command
	// 		"-R", "C:\\DEV\\go\\src\\golout\\",				// the repo
	// 		"--config", "ui.interactive=True",				// mandatory settings
	// 		"--config", "extensions.color=!",				// more settings (for Windows)
	// 		"serve", "--cmdserver", "pipe")					// start the Command Server

	// Maybe accept a channel as an extra argument for sending the logging to ?
	// And if it's nil, log into a textfile in the folder of this lib.
	// Also do not override that logfile every launch.
	// Maybe even do this in the init() function ?

	if hgserver != nil {
		return errors.New("A Hg Command Server is already connected.")
	}

	if hgexe == "" {
		// Let the OS determine what Mercurial to run
		// for this machine/user combination.
		hgexe = "hg"
	}

	// The Hg Command Server needs a repository.
	repo, err = locateRepository(reponame)
	if err != nil {
		return err
	}
	if repo == "" {
		return errors.New("could not find a Hg repository at: " + reponame)
	}

	// Maybe we can also offer the possibility of a config file?
	// f.i.: a file gohg.cfg in the same folder as the gohg.exe,
	// and a section per repo, and one "general" section.
	// Or maybe just a [gohg] section in one of the 'normal' Hg config files ?

	var hgconfig []string
	hgconfig = composeHgConfig(hgexe, repo, config)

	hgserver = exec.Command(hgexe)
	hgserver.Args = hgconfig
	hgserver.Dir = repo

	pout, err = hgserver.StdoutPipe()
	if err != nil {
		return errors.New("could not connect StdoutPipe: " + err.Error())
	}
	pin, err = hgserver.StdinPipe()
	if err != nil {
		log.Fatal("could not connect StdinPipe: " + err.Error())
	}

	if err := hgserver.Start(); err != nil {
		return errors.New("could not start the Hg Command Server: " + err.Error())
	}

	err = readHelloMessage()
	if err != nil {
		return err
	}

	fmt.Println("Connected with Hg Command Server at: " + repo)

	return nil

} // Connect()

// locateRepository assures we have a Mercurial repository available,
// which is required for working with the Hg CommandServer.
func locateRepository(reponame string) (string, error) {
	repo = reponame
	sep := string(os.PathSeparator)

	// first make a correct path from repo
	repo, err = filepath.Abs(repo)
	if err != nil {
		return "", errors.New(err.Error() +
			"\ncould not find absolute path for: " + repo)
	}
	repo = filepath.Clean(repo)

	// If we do not find a Hg repo in this dir, we search for one
	// up the path, in case we're deeper in it's working copy.
	for {
		_, err = os.Stat(repo + sep + ".hg")
		if err == nil {
			break
		}
		var dir, file string
		dir, file = filepath.Split(repo)
		if dir == "" || file == "" {
			repo = ""
			break
		}
		repo = dir
	}
	if err != nil || repo == "" {
		return "", nil
	}

	return repo, nil

} // locateRepository()

// composeHgConfig handles the different config settings that will be used
// to make the connection with the Hg CS. It concerns specific Hg settings.
func composeHgConfig(hgcmd string, repo string, config []string) []string {
	var hgconfig []string

	// if len(config) > 0 {
	// 	var cfg string
	// 	for i := 0; i < range(config) {
	// 		cfg = cfg + "," + config[i]
	// 	}
	// 	cmd = cmd + "," + cfg
	// }

	hgconfig = append(hgconfig, hgcmd,
		"--cwd", repo,
		"-R", repo,
		// These arguments are fixed.
		// "--config", "ui.interactive=True",
		"--config", "ui.interactive=False",
		"--config", "extensions.color=!",
		"serve", "--cmdserver", "pipe")

	return hgconfig
} // composeHgConfig()

// readHelloMessage reads the special hello message send by the Hg CS.
//
// It has a fixed format, and contains info about the possibilities
// of the Hg CS at hand. It's also a first proof of a working connection.
func readHelloMessage() error {
	s := make([]byte, 5)
	_, err = pout.Read(s)
	if err != io.EOF && err != nil {
		return err
	}
	if len(s) == 0 {
		return errors.New("no hello message data received from Hg CommandServer")
	}
	const t1 = "hg se" // hg send: "hg serve [OPTION]"
	if string(s[0:len(t1)]) == t1 {
		return errors.New("this version of Mercurial does not support the CommandServer")
	}
	ch := string(s[0])
	if ch != "o" {
		return errors.New("received unexpected channel '" + ch +
			"' for hello message from Hg CommandServer")
	}
	var ln uint32
	ln, err = calcDataLength(s[1:5])
	if err != nil {
		fmt.Println("binary.Read failed:", err)
	}
	if ln <= 0 {
		return errors.New("received invalid length '" + string(ln) +
			"' for hello message from Hg CommandServer")
	}
	s = make([]byte, ln)
	_, err = pout.Read(s)
	if err != io.EOF && err != nil {
		return err
	}
	const t2 = "capabilities:"
	if string(s[0:len(t2)]) != t2 {
		return errors.New("could not determine the capabilities of the Hg CommandServer")
	}
	return nil
} // readHelloMessage()

// Close ends the connection with the Mercurial CommandServer.
//
// In fact it's closing the stdin of the Hg CS that closes the connection,
// as per the Hg CS documentation.
func Close() error {
	pout.Close()
	pin.Close()
	err = hgserver.Wait()
	if err != nil {
		return err
	}
	fmt.Println("Disconnected from Hg Command Server at: " + repo)
	return nil
} // Close()

// readFromHg returns the channel and all the data read from it.
// Eventually it returns no (or empty) data but an error.
func readFromHg() (string, []byte, error) {
	var ch string

	// get channel and length
	data := make([]byte, 5)
	_, err = pout.Read(data)
	if err != io.EOF && err != nil {
		return ch, data, err
	}
	if data == nil {
		return ch, data, errors.New("no data read")
	}
	ch = string(data[0])
	if ch == "" {
		return ch, data, errors.New("no channel read")
	}

	// get the uint that the Hg CS sent us as the length value
	var ln uint32
	ln, err = calcDataLength(data[1:5])
	if err != nil {
		return ch, data, errors.New("binary.Read failed:" + string(err.Error()))
	}

	// now get ln bytes of data
	data = make([]byte, ln)
	_, err = pout.Read(data)
	if err != io.EOF && err != nil {
		return ch, data, err
	}

	return ch, data, nil
} // readFromHg()

// sendToHg writes data to the Hg CS,
// returning an error if something went wrong.
func sendToHg(cmd string, args []byte) error {
	cmd = strings.TrimRight(cmd, "\n") + "\n"
	lc := len(cmd)
	la := len(args)
	var l int
	if la > 0 {
		l = lc + 4 + la
	} else {
		// in case cmd == "getencoding" f.i.
		l = lc
	}
	data := make([]byte, l)

	// send the command
	copy(data[0:lc], cmd)

	if la > 0 {
		// send the length of the command arguments
		ln := uint32(len(args))
		wbuf := new(bytes.Buffer)
		err = binary.Write(wbuf, binary.BigEndian, ln)
		if err != nil {
			return errors.New("binary.Write failed: " + string(err.Error()))
		}
		b := make([]byte, 4)
		_, err = io.ReadFull(wbuf, b)
		if err != nil {
			return errors.New("io.ReadFull failed: " + string(err.Error()))
		}
		copy(data[lc:lc+4], b)

		// send the command arguments
		copy(data[lc+4:lc+4+la], args)
	}

	// perform the actual send to the Hg CS
	var i int
	i, err = pin.Write(data)
	if i != len(data) {
		return errors.New("writing length of data failed: " +
			string(err.Error()))
	}

	return nil
} // sendToHg()

func calcDataLength(s []byte) (uint32, error) {
	var ln uint32
	buf := bytes.NewBuffer(s[0:4])
	err := binary.Read(buf, binary.BigEndian, &ln)
	return ln, err
}
<commit_msg>- added a new type - added some comments<commit_after>// Copyright (C) 2012, The gohg Authors. All rights reserved.
// Use of this source code is governed by a BSD style license
// that can be found in the LICENSE.txt file.

// Package gohg is a Go client library for using the Mercurial dvcs
// via it's Command Server.
//
// For Mercurial see: http://mercurial/selenic.com/wiki.
//
// For the Hg Command Server see: http://mercurial.selenic.com/wiki/CommandServer.
package gohg

import (
	"bytes"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"log"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"strings"
)

// var libdir string
var repo string
var err error
var logfile string
var capabilities []string
var encoding string

// Type Hgclient will act as a (kind of) object for working with the Hg CS
// from any program using this gohg client lib.
// It will get a bunch of attributes and methods to make working with it
// as go-like as possible. It might even get a few channels for communications.
type Hgclient struct {
	capabilities []string
	encoding     string
	repo         string
}

var hgserver *exec.Cmd

// The in and out pipe ends are to be considered from the point of view
// of the Hg Command Server instance.
var pout io.ReadCloser
var pin io.WriteCloser

// hgMsg is what we receive from the Hg CS
type hgMsg struct {
	Ch   string
	Ln   uint
	Data string
}

// hgCmd is what we send to the Hg CS
type hgCmd struct {
	Cmd  string
	Ln   uint
	Args string
}

// init takes care of some householding, namely preparing a logfile where
// all communication between this lib and the Hg CS can be logged.
func init() {
	var exedir string
	exedir = path.Dir(os.Args[0])
	exedir, err = filepath.Abs(exedir)
	if err != nil {
		log.Fatal("Could not determine path for the gohg.log logfile.")
	}
	logfile = exedir + string(os.PathSeparator) + "gohg.log"
} // init()

// Connect establishes the connection with the Mercurial CommandServer.
//
// Arguments:
//	hgexe
//		The command to run mercurial. Optional.
//	reponame
//		The folder of the Hg repository to work on. Optional.
//		When blanc the folder where the program is run is used
//		(see function locateRepository()).
//	config
//		Configuration settings that will be added to the necessary
//		/fixed settings (see composeHgConfig() for more).
//
// Returns an error if the connection could not be established properly.
func Connect(hgexe string, reponame string, config []string) error {

	// for example:
	// hgserver = exec.Command("M:\\DEV\\hg-stable\\hg",	// the Hg command
	// 		"-R", "C:\\DEV\\go\\src\\golout\\",				// the repo
	// 		"--config", "ui.interactive=True",				// mandatory settings
	// 		"--config", "extensions.color=!",				// more settings (for Windows)
	// 		"serve", "--cmdserver", "pipe")					// start the Command Server

	// Maybe accept a channel as an extra argument for sending the logging to ?
	// And if it's nil, log into a textfile in the folder of this lib.
	// Also do not override that logfile every launch.
	// Maybe even do this in the init() function ?

	if hgserver != nil {
		return errors.New("A Hg Command Server is already connected.")
	}

	if hgexe == "" {
		// Let the OS determine what Mercurial to run
		// for this machine/user combination.
		hgexe = "hg"
	}

	// The Hg Command Server needs a repository.
	repo, err = locateRepository(reponame)
	if err != nil {
		return err
	}
	if repo == "" {
		return errors.New("could not find a Hg repository at: " + reponame)
	}

	// Maybe we can also offer the possibility of a config file?
	// f.i.: a file gohg.cfg in the same folder as the gohg.exe,
	// and a section per repo, and one "general" section.
	// Or maybe just a [gohg] section in one of the 'normal' Hg config files ?

	var hgconfig []string
	hgconfig = composeHgConfig(hgexe, repo, config)

	hgserver = exec.Command(hgexe)
	hgserver.Args = hgconfig
	hgserver.Dir = repo

	pout, err = hgserver.StdoutPipe()
	if err != nil {
		return errors.New("could not connect StdoutPipe: " + err.Error())
	}
	pin, err = hgserver.StdinPipe()
	if err != nil {
		log.Fatal("could not connect StdinPipe: " + err.Error())
	}

	if err := hgserver.Start(); err != nil {
		return errors.New("could not start the Hg Command Server: " + err.Error())
	}

	err = readHelloMessage()
	if err != nil {
		return err
	}

	fmt.Println("Connected with Hg Command Server at: " + repo)

	return nil

} // Connect()

// locateRepository assures we have a Mercurial repository available,
// which is required for working with the Hg CommandServer.
func locateRepository(reponame string) (string, error) {
	repo = reponame
	sep := string(os.PathSeparator)

	// first make a correct path from repo
	repo, err = filepath.Abs(repo)
	if err != nil {
		return "", errors.New(err.Error() +
			"\ncould not find absolute path for: " + repo)
	}
	repo = filepath.Clean(repo)

	// If we do not find a Hg repo in this dir, we search for one
	// up the path, in case we're deeper in it's working copy.
	for {
		_, err = os.Stat(repo + sep + ".hg")
		if err == nil {
			break
		}
		var dir, file string
		dir, file = filepath.Split(repo)
		if dir == "" || file == "" {
			repo = ""
			break
		}
		repo = dir
	}
	if err != nil || repo == "" {
		return "", nil
	}

	return repo, nil

} // locateRepository()

// composeHgConfig handles the different config settings that will be used
// to make the connection with the Hg CS. It concerns specific Hg settings.
func composeHgConfig(hgcmd string, repo string, config []string) []string {
	var hgconfig []string

	// if len(config) > 0 {
	// 	var cfg string
	// 	for i := 0; i < range(config) {
	// 		cfg = cfg + "," + config[i]
	// 	}
	// 	cmd = cmd + "," + cfg
	// }

	hgconfig = append(hgconfig, hgcmd,
		"--cwd", repo,
		"-R", repo,
		// These arguments are fixed.
		// "--config", "ui.interactive=True",
		"--config", "ui.interactive=False",
		"--config", "extensions.color=!",
		"serve", "--cmdserver", "pipe")

	return hgconfig
} // composeHgConfig()

// readHelloMessage reads the special hello message send by the Hg CS.
//
// It has a fixed format, and contains info about the possibilities
// of the Hg CS at hand. It's also a first proof of a working connection.
func readHelloMessage() error {
	s := make([]byte, 5)
	_, err = pout.Read(s)
	if err != io.EOF && err != nil {
		return err
	}
	if len(s) == 0 {
		return errors.New("no hello message data received from Hg CommandServer")
	}
	const t1 = "hg se" // hg send: "hg serve [OPTION]"
	if string(s[0:len(t1)]) == t1 {
		return errors.New("this version of Mercurial does not support the CommandServer")
	}
	ch := string(s[0])
	if ch != "o" {
		return errors.New("received unexpected channel '" + ch +
			"' for hello message from Hg CommandServer")
	}
	var ln uint32
	ln, err = calcDataLength(s[1:5])
	if err != nil {
		fmt.Println("binary.Read failed:", err)
	}
	if ln <= 0 {
		return errors.New("received invalid length '" + string(ln) +
			"' for hello message from Hg CommandServer")
	}
	s = make([]byte, ln)
	_, err = pout.Read(s)
	if err != io.EOF && err != nil {
		return err
	}
	const t2 = "capabilities:"
	if string(s[0:len(t2)]) != t2 {
		return errors.New("could not determine the capabilities of the Hg CommandServer")
	}
	return nil
} // readHelloMessage()

// Close ends the connection with the Mercurial CommandServer.
//
// In fact it's closing the stdin of the Hg CS that closes the connection,
// as per the Hg CS documentation.
func Close() error {
	pout.Close()
	pin.Close()
	err = hgserver.Wait()
	if err != nil {
		return err
	}
	fmt.Println("Disconnected from Hg Command Server at: " + repo)
	return nil
} // Close()

// readFromHg returns the channel and all the data read from it.
// Eventually it returns no (or empty) data but an error.
func readFromHg() (string, []byte, error) {
	var ch string

	// get channel and length
	data := make([]byte, 5)
	_, err = pout.Read(data)
	if err != io.EOF && err != nil {
		return ch, data, err
	}
	if data == nil {
		return ch, data, errors.New("no data read")
	}
	ch = string(data[0])
	if ch == "" {
		return ch, data, errors.New("no channel read")
	}

	// get the uint that the Hg CS sent us as the length value
	var ln uint32
	ln, err = calcDataLength(data[1:5])
	if err != nil {
		return ch, data, errors.New("binary.Read failed:" + string(err.Error()))
	}

	// now get ln bytes of data
	data = make([]byte, ln)
	_, err = pout.Read(data)
	if err != io.EOF && err != nil {
		return ch, data, err
	}

	return ch, data, nil
} // readFromHg()

// sendToHg writes data to the Hg CS,
// returning an error if something went wrong.
func sendToHg(cmd string, args []byte) error {
	cmd = strings.TrimRight(cmd, "\n") + "\n"
	lc := len(cmd)
	la := len(args)
	var l int
	if la > 0 {
		l = lc + 4 + la
	} else {
		// in case cmd == "getencoding" f.i.
		l = lc
	}
	data := make([]byte, l)

	// send the command
	copy(data[0:lc], cmd)

	if la > 0 {
		// send the length of the command arguments
		ln := uint32(len(args))
		wbuf := new(bytes.Buffer)
		err = binary.Write(wbuf, binary.BigEndian, ln)
		if err != nil {
			return errors.New("binary.Write failed: " + string(err.Error()))
		}
		b := make([]byte, 4)
		_, err = io.ReadFull(wbuf, b)
		if err != nil {
			return errors.New("io.ReadFull failed: " + string(err.Error()))
		}
		copy(data[lc:lc+4], b)

		// send the command arguments
		copy(data[lc+4:lc+4+la], args)
	}

	// perform the actual send to the Hg CS
	var i int
	i, err = pin.Write(data)
	if i != len(data) {
		return errors.New("writing length of data failed: " +
			string(err.Error()))
	}

	return nil
} // sendToHg()

// calcDataLength converts a 4-byte slice into an unsigned int
func calcDataLength(s []byte) (uint32, error) {
	var ln uint32
	buf := bytes.NewBuffer(s[0:4])
	err := binary.Read(buf, binary.BigEndian, &ln)
	return ln, err
}
<|endoftext|>


<commit_before>/**
 * Copyright 2015 @ at3.net.
 * name : gord.go
 * author : jarryliu
 * date : -- :
 * description :
 * history :
 */
package main

import (
	"bufio"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"
)

var (
	port  int
	dir   string
	debug bool = false
)

func main() {
	flag.IntVar(&port, "port", 8302, "port")
	flag.StringVar(&dir, "dir", ".", "config file")
	flag.BoolVar(&debug, "log", false, "print log")
	flag.Parse()
	app := http.Server{
		Addr:    fmt.Sprintf(":%d", port),
		Handler: getHandler(dir),
	}
	log.Printf("[ Gord][ Service] - running on port %s\n", app.Addr)
	if err := app.ListenAndServe(); err != nil {
		log.Fatalf(" Server aborted!reason:%s\n", err.Error())
	}
}

func debugLog(v ...interface{}) {
	if debug {
		v = append([]interface{}{"[ Gord][ Log]"}, v...)
		log.Println(v...)
	}
}

func getHandler(confPath string) http.Handler {
	r := &gordHandler{
		itemManager: &ItemManager{
			confPath: confPath,
		},
	}
	r.itemManager.Load()
	return r
}

type Item struct {
	//主机头，*表示通配
	Host string `json:"host"`
	//全局请求跳转路径,{path}表示完整的路径；
	//{#序号}表示路径片段的序号
	To string `json:"to"`
	//如果未设定全局请求跳转路径，那么将启用路径字典
	//如果{"a/b/c":"http://abc.com"}，访问/a/b/c将跳转
	//到"http://abc.com"
	Location map[string]string `json:"location"`
}

type ItemManager struct {
	confPath string
	items    map[string]*Item
}

// 检查目录，并初始化
func (i *ItemManager) checkDir(path string) {
	_, err := os.Stat(path)
	//创建目录
	if os.IsNotExist(err) {
		os.MkdirAll(path, os.ModePerm)
		i.initExample(path)
	} else {
		//是否存在.conf文件,不存在，则初始化
		fi, _ := os.Open(path)
		exits := false
		list, _ := fi.Readdirnames(-1)
		for _, v := range list {
			if strings.HasSuffix(v, ".conf") {
				exits = true
			}
		}
		if !exits {
			i.initExample(path)
		}
	}
}

func (i *ItemManager) initExample(path string) {
	var defaultItems []*Item = []*Item{
		{
			Host: "localhost localhost:8302 *.to2.net",
			To:   "http://www.to2.net/{path}{query}",
			Location: map[string]string{
				"/a":      "http://a.com/{path}{query}{timestamp}",
				"/b/*":    "http://b.com/t-{*}",
				"/1/2/3/": "http://a.com/{#0}-{#1}-{#2}",
			},
		},
	}

	//创建文件
	bytes, _ := json.MarshalIndent(defaultItems, " ", " ")
	f, err := os.Create(path + "/default.conf")
	if err == nil {
		wr := bufio.NewWriter(f)
		wr.Write(bytes)
		err = wr.Flush()
		f.Close()
	}
	if err != nil {
		log.Println(" init example config fail ! error :", err.Error())
	}
}

// 加载配置
func (i *ItemManager) Load() {
	i.checkDir(i.confPath)
	filepath.Walk(i.confPath, func(path string, info os.FileInfo, err error) error {
		if err == nil && !info.IsDir() && strings.HasSuffix(path, ".conf") {
			items := i.GetItemsFromFile(path)
			i.Append(items)
		}
		return nil
	})
}

func (i *ItemManager) checkItem(item *Item) error {
	if len(item.Host) < 2 {
		return errors.New("主机头长度不正确")
	}
	return nil
}

// 从文件中加载配置项目
func (i *ItemManager) GetItemsFromFile(path string) []*Item {
	bytes, err := ioutil.ReadFile(path)
	if err == nil {
		//从文件中反序列化
		items := make([]*Item, 0)
		err = json.Unmarshal(bytes, &items)
		//检查配置是否正确
		if err == nil {
			for _, v := range items {
				if err = i.checkItem(v); err != nil {
					log.Println(fmt.Sprintf("配置有误! 文件：%s; host: %s; error:%s",
						path, v.Host, err.Error()))
					os.Exit(1)
				}
			}
			debugLog("[ Load]: conf file:", path, "load ok!")
			return items
		}
	}
	log.Println("加载配置:" + path + "出错：" + err.Error())
	os.Exit(1)
	return nil
}

// 增加配置项
func (i *ItemManager) Append(items []*Item) {
	if items == nil {
		return
	}
	if i.items == nil {
		i.items = make(map[string]*Item, 0)
	}

	for _, v := range items {
		hostArr := strings.Split(v.Host, " ")
		for _, host := range hostArr {
			if _, ok := i.items[host]; ok {
				panic("has exists host " + host)
			}
			i.items[host] = v
		}
	}
}

// 根据主机名获取相应的配置,如果无匹配，则默认使用localhost
func (i *ItemManager) GetItemByHost(host string) *Item {
	for k, v := range i.items {
		if i.matchHost(k, host) {
			return v
		}
	}
	return i.items["localhost"]
}

// 匹配主机
func (i *ItemManager) matchHost(cfgHost, host string) bool {
	if host == cfgHost {
		return true
	}
	// 判断是否泛解析
	if strings.HasPrefix(cfgHost, "*.") {
		return strings.HasSuffix(host, cfgHost[2:])
	}
	return false
}

var _ http.Handler = new(gordHandler)

type gordHandler struct {
	itemManager *ItemManager
}

func (r *gordHandler) ServeHTTP(rsp http.ResponseWriter, req *http.Request) {
	host := req.Host
	debugLog("[ Request]: source host ", host)
	var item *Item = r.itemManager.GetItemByHost(host)
	if item != nil {
		if location, b := r.getLocation(req, item); b {
			rsp.Header().Add("Location", location)
			rsp.WriteHeader(302)
			return
		}
	}
	rsp.Write([]byte("Not match any host"))
}

// 获取目标路径
func (r *gordHandler) getLocation(req *http.Request, item *Item) (string, bool) {
	path := req.URL.Path
	query := req.URL.RawQuery
	concat := ""
	if len(query) != 0 {
		concat = "?"
	}
	//查找匹配
	target := item.To
	anyMatchPos := -1
	for k, v := range item.Location {
		debugLog("[ Compare]:对比相同，path:", path, "; key:", k)
		//判断路径是否相同
		if path == k {
			target = v
			break
		}
		//匹配如：/d/* 含通配符的路径
		anyMatch := strings.HasSuffix(k, "*")
		debugLog("[ Compare]:包含通配:", anyMatch)
		if anyMatch {
			anyMatchPos = len(k) - 1 //通配符所在的索引位置
			anyMatch = strings.HasPrefix(path, k[:anyMatchPos])
			debugLog("[ Compare]:判断通配:", anyMatch, k[:anyMatchPos])
			if anyMatch {
				target = v
				break
			}
		}
	}
	//无匹配
	target = strings.TrimSpace(target)
	if target == "" {
		return "", false
	}

	//全局请求跳转路径,{path}表示完整的路径；
	if strings.Contains(target, "{path}") {
		target = strings.Replace(target, "{path}", path[1:], -1)
	}
	// {query}表示查询条件
	qt := strings.Contains(target, "{query}")
	if qt {
		target = strings.Replace(target, "{query}", concat+query, -1)
	}
	// {timestamp}会返回时间戳
	if strings.Contains(target, "{timestamp}") {
		unixTime := strconv.Itoa(int(time.Now().UnixNano()))
		if !qt || concat == "" {
			unixTime = "?_stamp=" + unixTime
		} else {
			unixTime = "&_stamp=" + unixTime
		}
		target = strings.Replace(target, "{timestamp}", unixTime, -1)
	}
	//路径通配
	if strings.Contains(target, "{*}") && anyMatchPos != -1 {
		target = strings.Replace(target, "{*}", path[anyMatchPos:], -1)
	}
	//匹配含有路径片段的URL,{#序号}表示指定的路径片段
	if strings.Contains(target, "{#") {
		segments := strings.Split(path[1:], "/")
		for i, l := 0, len(segments); i < l; i++ {
			target = strings.Replace(target, "{#"+strconv.Itoa(i)+"}",
				segments[i], -1)
		}
	}
	debugLog("--- origin:", path, "; target:", target)
	return target, true
}
<commit_msg>gord.go<commit_after>/**
 * Copyright 2015 @ at3.net.
 * name : gord.go
 * author : jarryliu
 * date : -- :
 * description :
 * history :
 */
package main

import (
	"bufio"
	"encoding/json"
	"errors"
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"
)

var (
	port  int
	dir   string
	debug bool = false
)

func main() {
	flag.IntVar(&port, "port", 8302, "port")
	flag.StringVar(&dir, "dir", ".", "config file")
	flag.BoolVar(&debug, "log", false, "print log")
	flag.Parse()
	app := http.Server{
		Addr:    fmt.Sprintf(":%d", port),
		Handler: getHandler(dir),
	}
	log.Printf("[ Gord][ Service] - running on port %s\n", app.Addr)
	if err := app.ListenAndServe(); err != nil {
		log.Fatalf(" Server aborted!reason:%s\n", err.Error())
	}
}

func debugLog(v ...interface{}) {
	if debug {
		v = append([]interface{}{"[ Gord][ Log]"}, v...)
		log.Println(v...)
	}
}

func getHandler(confPath string) http.Handler {
	r := &HttpHandler{
		itemManager: &ItemManager{
			confPath: confPath,
		},
	}
	r.itemManager.Load()
	return r
}

type Item struct {
	//主机头，*表示通配
	Host string `json:"host"`
	//全局请求跳转路径,{path}表示完整的路径；
	//{#序号}表示路径片段的序号
	To string `json:"to"`
	//如果未设定全局请求跳转路径，那么将启用路径字典
	//如果{"a/b/c":"http://abc.com"}，访问/a/b/c将跳转
	//到"http://abc.com"
	Location map[string]string `json:"location"`
}

type ItemManager struct {
	confPath string
	items    map[string]*Item
}

// 检查目录，并初始化
func (i *ItemManager) checkDir(path string) {
	_, err := os.Stat(path)
	//创建目录
	if os.IsNotExist(err) {
		os.MkdirAll(path, os.ModePerm)
		i.initExample(path)
	} else {
		//是否存在.conf文件,不存在，则初始化
		fi, _ := os.Open(path)
		exits := false
		list, _ := fi.Readdirnames(-1)
		for _, v := range list {
			if strings.HasSuffix(v, ".conf") {
				exits = true
			}
		}
		if !exits {
			i.initExample(path)
		}
	}
}

func (i *ItemManager) initExample(path string) {
	var defaultItems []*Item = []*Item{
		{
			Host: "localhost localhost:8302 *.to2.net",
			To:   "http://www.to2.net/{path}{query}",
			Location: map[string]string{
				"/a":      "http://a.com/{path}{query}{timestamp}",
				"/b/*":    "http://b.com/t-{*}",
				"/1/2/3/": "http://a.com/{#0}-{#1}-{#2}",
			},
		},
	}

	//创建文件
	bytes, _ := json.MarshalIndent(defaultItems, " ", " ")
	f, err := os.Create(path + "/default.conf")
	if err == nil {
		wr := bufio.NewWriter(f)
		wr.Write(bytes)
		err = wr.Flush()
		f.Close()
	}
	if err != nil {
		log.Println(" init example config fail ! error :", err.Error())
	}
}

// 加载配置
func (i *ItemManager) Load() {
	i.checkDir(i.confPath)
	filepath.Walk(i.confPath, func(path string, info os.FileInfo, err error) error {
		if err == nil && !info.IsDir() && strings.HasSuffix(path, ".conf") {
			items := i.GetItemsFromFile(path)
			i.Append(items)
		}
		return nil
	})
}

func (i *ItemManager) checkItem(item *Item) error {
	if len(item.Host) < 2 {
		return errors.New("主机头长度不正确")
	}
	return nil
}

// 从文件中加载配置项目
func (i *ItemManager) GetItemsFromFile(path string) []*Item {
	bytes, err := ioutil.ReadFile(path)
	if err == nil {
		//从文件中反序列化
		items := make([]*Item, 0)
		err = json.Unmarshal(bytes, &items)
		//检查配置是否正确
		if err == nil {
			for _, v := range items {
				if err = i.checkItem(v); err != nil {
					log.Println(fmt.Sprintf("error config file ：%s; host: %s; error:%s",
						path, v.Host, err.Error()))
					os.Exit(1)
				}
			}
			debugLog("[ Gord][ Load]: conf file:", path, "load ok!")
			return items
		}
	}
	log.Println("load config " + path + " error ：" + err.Error())
	os.Exit(1)
	return nil
}

// 增加配置项
func (i *ItemManager) Append(items []*Item) {
	if items == nil {
		return
	}
	if i.items == nil {
		i.items = make(map[string]*Item, 0)
	}

	for _, v := range items {
		hostArr := strings.Split(v.Host, " ")
		for _, host := range hostArr {
			if _, ok := i.items[host]; ok {
				log.Println("host " + host + " already exists ")
				os.Exit(0)
				break
			}
			i.items[host] = v
		}
	}
}

// 根据主机名获取相应的配置,如果无匹配，则默认使用localhost
func (i *ItemManager) GetItemByHost(host string) *Item {
	for k, v := range i.items {
		if i.matchHost(k, host) {
			return v
		}
	}
	return i.items["localhost"]
}

// 匹配主机
func (i *ItemManager) matchHost(cfgHost, host string) bool {
	if host == cfgHost {
		return true
	}
	// 判断是否泛解析
	if strings.HasPrefix(cfgHost, "*.") {
		return strings.HasSuffix(host, cfgHost[2:])
	}
	return false
}

var _ http.Handler = new(HttpHandler)

type HttpHandler struct {
	itemManager *ItemManager
}

func (r *HttpHandler) ServeHTTP(rsp http.ResponseWriter, req *http.Request) {
	host := req.Host
	debugLog("[ Request]: source host ", host)
	var item *Item = r.itemManager.GetItemByHost(host)
	if item != nil {
		if location, b := r.getLocation(req, item); b {
			rsp.Header().Add("Location", location)
			rsp.WriteHeader(302)
			return
		}
	}
	rsp.Write([]byte("Not match any host"))
}

// 获取目标路径
func (r *HttpHandler) getLocation(req *http.Request, item *Item) (string, bool) {
	path := req.URL.Path
	query := req.URL.RawQuery
	concat := ""
	if len(query) != 0 {
		concat = "?"
	}
	//查找匹配
	target := item.To
	anyMatchPos := -1
	for k, v := range item.Location {
		debugLog("[ Compare]:对比相同，path:", path, "; key:", k)
		//判断路径是否相同
		if path == k {
			target = v
			break
		}
		//匹配如：/d/* 含通配符的路径
		anyMatch := strings.HasSuffix(k, "*")
		debugLog("[ Compare]:包含通配:", anyMatch)
		if anyMatch {
			anyMatchPos = len(k) - 1 //通配符所在的索引位置
			anyMatch = strings.HasPrefix(path, k[:anyMatchPos])
			debugLog("[ Compare]:判断通配:", anyMatch, k[:anyMatchPos])
			if anyMatch {
				target = v
				break
			}
		}
	}
	//无匹配
	target = strings.TrimSpace(target)
	if target == "" {
		return "", false
	}

	//全局请求跳转路径,{path}表示完整的路径；
	if strings.Contains(target, "{path}") {
		target = strings.Replace(target, "{path}", path[1:], -1)
	}
	// {query}表示查询条件
	qt := strings.Contains(target, "{query}")
	if qt {
		target = strings.Replace(target, "{query}", concat+query, -1)
	}
	// {timestamp}会返回时间戳
	if strings.Contains(target, "{timestamp}") {
		unixTime := strconv.Itoa(int(time.Now().UnixNano()))
		if !qt || concat == "" {
			unixTime = "?_stamp=" + unixTime
		} else {
			unixTime = "&_stamp=" + unixTime
		}
		target = strings.Replace(target, "{timestamp}", unixTime, -1)
	}
	//路径通配
	if strings.Contains(target, "{*}") && anyMatchPos != -1 {
		target = strings.Replace(target, "{*}", path[anyMatchPos:], -1)
	}
	//匹配含有路径片段的URL,{#序号}表示指定的路径片段
	if strings.Contains(target, "{#") {
		segments := strings.Split(path[1:], "/")
		for i, l := 0, len(segments); i < l; i++ {
			target = strings.Replace(target, "{#"+strconv.Itoa(i)+"}",
				segments[i], -1)
		}
	}
	debugLog("--- origin:", path, "; target:", target)
	return target, true
}
<|endoftext|>


<commit_before>// Copyright (C) 2016 JT Olds
// See LICENSE for copying information

package whgoth // import "gopkg.in/go-webhelp/whgoth.v1"

import (
	"crypto/rand"
	"encoding/hex"
	"log"
	"net/http"
	"net/url"

	"github.com/markbates/goth"
	"github.com/spacemonkeygo/errors"
	"golang.org/x/net/context"
	"gopkg.in/webhelp.v1"
	"gopkg.in/webhelp.v1/whcache"
	"gopkg.in/webhelp.v1/whcompat"
	"gopkg.in/webhelp.v1/wherr"
	"gopkg.in/webhelp.v1/whmux"
	"gopkg.in/webhelp.v1/whredir"
	"gopkg.in/webhelp.v1/whroute"
	"gopkg.in/webhelp.v1/whsess"
)

var (
	userKey = webhelp.GenSym()
)

type AuthProvider struct {
	goth.Provider
	baseURL          string
	sessionNamespace string

	whmux.Dir
}

func newAuthProvider(p goth.Provider, baseURL, sessionNamespace string) (
	a *AuthProvider) {
	a = &AuthProvider{
		Provider:         p,
		baseURL:          baseURL,
		sessionNamespace: sessionNamespace,
	}
	a.Dir = whmux.Dir{
		"login":    whmux.Exact(http.HandlerFunc(a.login)),
		"callback": whmux.Exact(http.HandlerFunc(a.callback)),
	}
	return a
}

func (a *AuthProvider) login(w http.ResponseWriter, r *http.Request) {
	ctx := whcompat.Context(r)

	u, err := a.User(ctx)
	if err == nil && u != nil {
		whredir.Redirect(w, r, safeRedirect(r.FormValue("redirect_to"), "/"))
		return
	}

	state := newState()
	sess, err := a.Provider.BeginAuthCtx(ctx, state)
	if err != nil {
		wherr.Handle(w, r, err)
		return
	}

	url, err := sess.GetAuthURL()
	if err != nil {
		wherr.Handle(w, r, err)
		return
	}

	session, err := whsess.Load(ctx, a.sessionNamespace)
	if err != nil {
		wherr.Handle(w, r, err)
		return
	}
	for key := range session.Values {
		delete(session.Values, key)
	}
	session.Values["state"] = state
	session.Values["logged_in"] = false
	session.Values["redirect_to"] = safeRedirect(r.FormValue("redirect_to"), "/")
	session.Values["auth"] = sess.Marshal()
	err = session.Save(w)
	if err != nil {
		wherr.Handle(w, r, err)
		return
	}

	whredir.Redirect(w, r, url)
}

func (a *AuthProvider) Logout(ctx context.Context, w http.ResponseWriter) (
	err error) {
	session, err := whsess.Load(ctx, a.sessionNamespace)
	if err != nil {
		return err
	}
	return session.Clear(w)
}

func (a *AuthProvider) callback(w http.ResponseWriter, r *http.Request) {
	ctx := whcompat.Context(r)
	session, err := whsess.Load(ctx, a.sessionNamespace)
	if err != nil {
		wherr.Handle(w, r, err)
		return
	}

	if loggedIn, _ := session.Values["logged_in"].(bool); loggedIn {
		wherr.Handle(w, r, wherr.BadRequest.New("already logged in"))
		return
	}

	auth, ok := session.Values["auth"].(string)
	if !ok {
		wherr.Handle(w, r, wherr.InternalServerError.New(
			"no existing session found"))
		return
	}

	sess, err := a.Provider.UnmarshalSession(auth)
	if err != nil {
		wherr.Handle(w, r, err)
		return
	}

	authToken, err := sess.AuthorizeCtx(ctx, a.Provider, r.URL.Query())
	if err != nil {
		wherr.Handle(w, r, err)
		return
	}

	redirectTo, _ := session.Values["redirect_to"].(string)
	session.Values["auth_token"] = authToken
	session.Values["logged_in"] = true
	session.Values["redirect_to"] = nil
	session.Values["auth"] = sess.Marshal()
	err = session.Save(w)
	if err != nil {
		wherr.Handle(w, r, err)
		return
	}

	whredir.Redirect(w, r, safeRedirect(redirectTo, "/"))
}

func (a *AuthProvider) User(ctx context.Context) (*goth.User, error) {
	session, err := whsess.Load(ctx, a.sessionNamespace)
	if err != nil {
		return nil, err
	}

	if loggedIn, _ := session.Values["logged_in"].(bool); !loggedIn {
		return nil, nil
	}

	auth, ok := session.Values["auth"].(string)
	if !ok {
		return nil, nil
	}

	sess, err := a.Provider.UnmarshalSession(auth)
	if err != nil {
		log.Printf("failed to unmarshal session: %v", err)
		return nil, nil
	}

	u, err := a.Provider.FetchUserCtx(ctx, sess)
	if err != nil {
		log.Printf("failed to fetch user: %v", err)
		return nil, nil
	}

	if u.UserID == "" || u.Provider == "" {
		log.Printf("user had no user id or provider set")
		return nil, nil
	}
	return &u, nil
}

func (a *AuthProvider) LoginURL(redirectTo string) string {
	return a.baseURL + "/login?" + url.Values{
		"redirect_to": {redirectTo}}.Encode()
}

type AuthProviders struct {
	baseURL   string
	providers []*AuthProvider

	whmux.Dir
}

func NewAuthProviders(baseURL, sessionNamespace string,
	providers ...goth.Provider) (a *AuthProviders) {
	p := make([]*AuthProvider, 0, len(providers))
	pMux := make(whmux.Dir, len(providers))
	for _, provider := range providers {
		w := newAuthProvider(provider,
			baseURL+"/provider/"+provider.Name(),
			sessionNamespace+"."+provider.Name())
		p = append(p, w)
		pMux[provider.Name()] = w
	}
	a = &AuthProviders{
		baseURL:   baseURL,
		providers: p,
	}
	a.Dir = whmux.Dir{
		"logout":   whmux.Exact(http.HandlerFunc(a.logout)),
		"provider": pMux,
	}
	return a
}

func (a *AuthProviders) Logout(ctx context.Context, w http.ResponseWriter) (
	err error) {
	var errs errors.ErrorGroup
	for _, provider := range a.providers {
		errs.Add(provider.Logout(ctx, w))
	}
	return errs.Finalize()
}

func (a *AuthProviders) logout(w http.ResponseWriter, r *http.Request) {
	err := a.Logout(whcompat.Context(r), w)
	if err != nil {
		wherr.Handle(w, r, err)
		return
	}
	whredir.Redirect(w, r, safeRedirect(r.FormValue("redirect_to"), "/"))
}

func safeRedirect(redirectTo, def string) string {
	u, err := url.Parse(redirectTo)
	if err != nil {
		return def
	}
	if u.Path == "" {
		return def
	}
	return u.Path
}

func (a *AuthProviders) Providers() []*AuthProvider {
	return a.providers
}

func (a *AuthProviders) User(ctx context.Context) (*goth.User, error) {
	if u, ok := whcache.Get(ctx, userKey).(*goth.User); ok && u != nil {
		return u, nil
	}
	for _, provider := range a.providers {
		u, err := provider.User(ctx)
		if err != nil || u != nil {
			whcache.Set(ctx, userKey, u)
			return u, err
		}
	}
	return nil, nil
}

func (a *AuthProviders) RequireUser(
	authorizedHandler, unauthorizedHandler http.Handler) http.Handler {
	return whroute.HandlerFunc(authorizedHandler,
		func(w http.ResponseWriter, r *http.Request) {
			ctx := whcompat.Context(r)
			u, err := a.User(ctx)
			if err != nil {
				wherr.Handle(w, r, err)
				return
			}
			if u == nil {
				unauthorizedHandler.ServeHTTP(w, r)
			} else {
				authorizedHandler.ServeHTTP(w, r)
			}
		})
}

func (a *AuthProviders) LogoutURL(redirectTo string) string {
	return a.baseURL + "/logout?" + url.Values{
		"redirect_to": {redirectTo}}.Encode()
}

func newState() string {
	var p [16]byte
	_, err := rand.Read(p[:])
	if err != nil {
		panic(err)
	}
	return hex.EncodeToString(p[:])
}
<commit_msg>appengine: support delayed loading of providers after initial request<commit_after>// Copyright (C) 2016 JT Olds
// See LICENSE for copying information

package whgoth // import "gopkg.in/go-webhelp/whgoth.v1"

import (
	"crypto/rand"
	"encoding/hex"
	"log"
	"net/http"
	"net/url"
	"sync"

	"github.com/markbates/goth"
	"github.com/spacemonkeygo/errors"
	"golang.org/x/net/context"
	"gopkg.in/webhelp.v1"
	"gopkg.in/webhelp.v1/whcache"
	"gopkg.in/webhelp.v1/whcompat"
	"gopkg.in/webhelp.v1/wherr"
	"gopkg.in/webhelp.v1/whmux"
	"gopkg.in/webhelp.v1/whredir"
	"gopkg.in/webhelp.v1/whroute"
	"gopkg.in/webhelp.v1/whsess"
)

var (
	userKey      = webhelp.GenSym()
	providerName = whmux.NewStringArg()
)

type AuthProvider struct {
	goth.Provider
	baseURL          string
	sessionNamespace string
}

func newAuthProvider(p goth.Provider, baseURL, sessionNamespace string) (
	a *AuthProvider) {
	return &AuthProvider{
		Provider:         p,
		baseURL:          baseURL,
		sessionNamespace: sessionNamespace,
	}
}

func (a *AuthProvider) login(w http.ResponseWriter, r *http.Request) {
	ctx := whcompat.Context(r)

	u, err := a.User(ctx)
	if err == nil && u != nil {
		whredir.Redirect(w, r, safeRedirect(r.FormValue("redirect_to"), "/"))
		return
	}

	state := newState()
	sess, err := a.Provider.BeginAuthCtx(ctx, state)
	if err != nil {
		wherr.Handle(w, r, err)
		return
	}

	url, err := sess.GetAuthURL()
	if err != nil {
		wherr.Handle(w, r, err)
		return
	}

	session, err := whsess.Load(ctx, a.sessionNamespace)
	if err != nil {
		wherr.Handle(w, r, err)
		return
	}
	for key := range session.Values {
		delete(session.Values, key)
	}
	session.Values["state"] = state
	session.Values["logged_in"] = false
	session.Values["redirect_to"] = safeRedirect(r.FormValue("redirect_to"), "/")
	session.Values["auth"] = sess.Marshal()
	err = session.Save(ctx, w)
	if err != nil {
		wherr.Handle(w, r, err)
		return
	}

	whredir.Redirect(w, r, url)
}

func (a *AuthProvider) Logout(ctx context.Context, w http.ResponseWriter) (
	err error) {
	session, err := whsess.Load(ctx, a.sessionNamespace)
	if err != nil {
		return err
	}
	return session.Clear(ctx, w)
}

func (a *AuthProvider) callback(w http.ResponseWriter, r *http.Request) {
	ctx := whcompat.Context(r)
	session, err := whsess.Load(ctx, a.sessionNamespace)
	if err != nil {
		wherr.Handle(w, r, err)
		return
	}

	if loggedIn, _ := session.Values["logged_in"].(bool); loggedIn {
		wherr.Handle(w, r, wherr.BadRequest.New("already logged in"))
		return
	}

	auth, ok := session.Values["auth"].(string)
	if !ok {
		wherr.Handle(w, r, wherr.InternalServerError.New(
			"no existing session found"))
		return
	}

	sess, err := a.Provider.UnmarshalSession(auth)
	if err != nil {
		wherr.Handle(w, r, err)
		return
	}

	authToken, err := sess.AuthorizeCtx(ctx, a.Provider, r.URL.Query())
	if err != nil {
		wherr.Handle(w, r, err)
		return
	}

	redirectTo, _ := session.Values["redirect_to"].(string)
	session.Values["auth_token"] = authToken
	session.Values["logged_in"] = true
	session.Values["redirect_to"] = nil
	session.Values["auth"] = sess.Marshal()
	err = session.Save(ctx, w)
	if err != nil {
		wherr.Handle(w, r, err)
		return
	}

	whredir.Redirect(w, r, safeRedirect(redirectTo, "/"))
}

func (a *AuthProvider) User(ctx context.Context) (*goth.User, error) {
	session, err := whsess.Load(ctx, a.sessionNamespace)
	if err != nil {
		return nil, err
	}

	if loggedIn, _ := session.Values["logged_in"].(bool); !loggedIn {
		return nil, nil
	}

	auth, ok := session.Values["auth"].(string)
	if !ok {
		return nil, nil
	}

	sess, err := a.Provider.UnmarshalSession(auth)
	if err != nil {
		log.Printf("failed to unmarshal session: %v", err)
		return nil, nil
	}

	u, err := a.Provider.FetchUserCtx(ctx, sess)
	if err != nil {
		log.Printf("failed to fetch user: %v", err)
		return nil, nil
	}

	if u.UserID == "" || u.Provider == "" {
		log.Printf("user had no user id or provider set")
		return nil, nil
	}
	return &u, nil
}

func (a *AuthProvider) LoginURL(redirectTo string) string {
	return a.baseURL + "/login?" + url.Values{
		"redirect_to": {redirectTo}}.Encode()
}

type AuthProviders struct {
	baseURL, sessionNamespace string
	providersCallback         func(context.Context) ([]goth.Provider, error)
	whmux.Dir

	providersMtx    sync.Mutex
	providersSetup  bool
	providers       []*AuthProvider
	providersByName map[string]*AuthProvider
}

func NewAuthProviders(baseURL, sessionNamespace string,
	providers ...goth.Provider) (a *AuthProviders) {
	return NewLazyAuthProviders(baseURL, sessionNamespace,
		func(context.Context) ([]goth.Provider, error) {
			return providers, nil
		})
}

func NewLazyAuthProviders(baseURL, sessionNamespace string,
	providers func(context.Context) ([]goth.Provider, error)) (
	a *AuthProviders) {

	a = &AuthProviders{
		baseURL:           baseURL,
		sessionNamespace:  sessionNamespace,
		providersCallback: providers,
	}
	a.Dir = whmux.Dir{
		"logout": whmux.Exact(http.HandlerFunc(a.logout)),
		"provider": providerName.Shift(whmux.Dir{
			"login":    whmux.Exact(http.HandlerFunc(a.login)),
			"callback": whmux.Exact(http.HandlerFunc(a.callback)),
		}),
	}
	return a
}

func (a *AuthProviders) setupProviders(ctx context.Context) error {
	a.providersMtx.Lock()
	defer a.providersMtx.Unlock()
	if a.providersSetup {
		return nil
	}

	providers, err := a.providersCallback(ctx)
	if err != nil {
		return err
	}

	p := make([]*AuthProvider, 0, len(providers))
	providersByName := make(map[string]*AuthProvider, len(providers))
	for _, provider := range providers {
		w := newAuthProvider(provider,
			a.baseURL+"/provider/"+provider.Name(),
			a.sessionNamespace+"."+provider.Name())
		p = append(p, w)
		if provider.Name() != "" {
			providersByName[provider.Name()] = w
		}
	}
	a.providers = p
	a.providersByName = providersByName
	a.providersSetup = true

	return nil
}

func (a *AuthProviders) login(w http.ResponseWriter, r *http.Request) {
	ctx := whcompat.Context(r)
	err := a.setupProviders(ctx)
	if err != nil {
		wherr.Handle(w, r, err)
		return
	}

	name := providerName.Get(ctx)
	if p, ok := a.providersByName[name]; ok {
		p.login(w, r)
	} else {
		wherr.Handle(w, r, wherr.NotFound.New("provider %s not found", name))
	}
}

func (a *AuthProviders) callback(w http.ResponseWriter, r *http.Request) {
	ctx := whcompat.Context(r)
	err := a.setupProviders(ctx)
	if err != nil {
		wherr.Handle(w, r, err)
		return
	}

	name := providerName.Get(ctx)
	if p, ok := a.providersByName[name]; ok {
		p.callback(w, r)
	} else {
		wherr.Handle(w, r, wherr.NotFound.New("provider %s not found", name))
	}
}

func (a *AuthProviders) Logout(ctx context.Context, w http.ResponseWriter) (
	err error) {
	err = a.setupProviders(ctx)
	if err != nil {
		return err
	}

	var errs errors.ErrorGroup
	for _, provider := range a.providers {
		errs.Add(provider.Logout(ctx, w))
	}
	return errs.Finalize()
}

func (a *AuthProviders) logout(w http.ResponseWriter, r *http.Request) {
	err := a.Logout(whcompat.Context(r), w)
	if err != nil {
		wherr.Handle(w, r, err)
		return
	}
	whredir.Redirect(w, r, safeRedirect(r.FormValue("redirect_to"), "/"))
}

func safeRedirect(redirectTo, def string) string {
	u, err := url.Parse(redirectTo)
	if err != nil {
		return def
	}
	if u.Path == "" {
		return def
	}
	return u.Path
}

func (a *AuthProviders) Providers(ctx context.Context) (
	[]*AuthProvider, error) {
	err := a.setupProviders(ctx)
	if err != nil {
		return nil, err
	}

	return a.providers, nil
}

func (a *AuthProviders) User(ctx context.Context) (*goth.User, error) {
	if u, ok := whcache.Get(ctx, userKey).(*goth.User); ok && u != nil {
		return u, nil
	}
	err := a.setupProviders(ctx)
	if err != nil {
		return nil, err
	}

	for _, provider := range a.providers {
		u, err := provider.User(ctx)
		if err != nil || u != nil {
			whcache.Set(ctx, userKey, u)
			return u, err
		}
	}
	return nil, nil
}

func (a *AuthProviders) RequireUser(
	authorizedHandler, unauthorizedHandler http.Handler) http.Handler {
	return whroute.HandlerFunc(authorizedHandler,
		func(w http.ResponseWriter, r *http.Request) {
			ctx := whcompat.Context(r)
			u, err := a.User(ctx)
			if err != nil {
				wherr.Handle(w, r, err)
				return
			}
			if u == nil {
				unauthorizedHandler.ServeHTTP(w, r)
			} else {
				authorizedHandler.ServeHTTP(w, r)
			}
		})
}

func (a *AuthProviders) LogoutURL(redirectTo string) string {
	return a.baseURL + "/logout?" + url.Values{
		"redirect_to": {redirectTo}}.Encode()
}

func newState() string {
	var p [16]byte
	_, err := rand.Read(p[:])
	if err != nil {
		panic(err)
	}
	return hex.EncodeToString(p[:])
}
<|endoftext|>


<commit_before>package main

import (
	"encoding/xml" // gelbooru parsing
	"errors"
	"fmt"
	"github.com/jcline/goty"
	"html"
	"io/ioutil"
	"math/rand"
	"net/http"
	"os"
	"regexp"
	"time"
)

var user = ""

var matchGelbooru = regexp.MustCompile(`.*\Qhttp://gelbooru.com/index.php?page=post&s=view&id=\E([\d]+).*`)
var matchYouTube = regexp.MustCompile(`.*(https?://(?:www\.|)youtu(?:\.be|be\.com)/[^ ]+).*`)
var matchAmiAmi = regexp.MustCompile(`(https?://(?:www\.|)amiami.com/[^/]+/detail/.*)`)

func auth(con *goty.IRCConn, writeMessage chan IRCMessage) {
	var pswd string
	_, err := fmt.Scanf("%s", &pswd)
	if err != nil {
		os.Exit(1)
	}

	msg := IRCMessage{"NickServ", "IDENTIFY Laala " + pswd}
	writeMessage <- msg
}

func main() {
	args := os.Args
	if len(args) < 4 {
		os.Exit(1)
	}

	con, err := goty.Dial(args[1], args[2], args[3])
	user = args[2]
	if err != nil {
		fmt.Fprintf(os.Stderr, "err: %s\n", err.Error())
	}

	gelbooruEvent := make(chan unparsedMessage, 1000)
	youtubeEvent := make(chan unparsedMessage, 1000)
	amiAmiEvent := make(chan unparsedMessage, 1000)
	bastilleEvent := make(chan unparsedMessage, 1000)
	writeMessage := make(chan IRCMessage, 1000)

	go messageHandler(con, writeMessage)
	go gelbooru(gelbooruEvent, writeMessage)
	go youtube(youtubeEvent, writeMessage)
	go amiami(amiAmiEvent, writeMessage)
	go bastille(bastilleEvent, writeMessage)

	auth(con, writeMessage)
	con.Write <- "JOIN " + args[4]

	for msg := range con.Read {
		prepared := unparsedMessage{msg, time.Now()}
		fmt.Printf("%s||%s\n", prepared.when, prepared.msg)

		switch {
		case matchGelbooru.MatchString(msg):
			//gelbooruEvent <- matchGelbooru.FindAllStringSubmatch(msg, -1)[0][1]
		case matchYouTube.MatchString(msg):
			youtubeEvent <- prepared
		case matchAmiAmi.MatchString(msg):
			amiAmiEvent <- prepared
		default:
		}
	}
	con.Close()
}

type unparsedMessage struct {
	msg  string
	when time.Time
}

type IRCMessage struct {
	channel string
	msg     string
}

func message(con *goty.IRCConn, msg IRCMessage) {
	con.Write <- "PRIVMSG " + msg.channel + " :" + msg.msg + "\r\n"
}

func messageHandler(con *goty.IRCConn, event chan IRCMessage) {
	books := map[string]time.Time{}
	for msg := range event {
		now := time.Now()
		if now.Sub(books[msg.channel]) < time.Second*10 {
			continue
		}
		books[msg.channel] = now
		message(con, msg)
	}
}

var PRIVMSG = regexp.MustCompile(`:(.+)![^ ]+ PRIVMSG ([^ ]+) :(.*)`)

func getChannel(msg string) (*IRCMessage, error) {
	// :nick!~realname@0.0.0.0 PRIVMSG #chan :msg
	imsg := new(IRCMessage)
	match := PRIVMSG.FindAllStringSubmatch(msg, -1)
	if len(match) < 1 {
		return imsg, errors.New("could not parse message")
	}
	if len(match[0]) < 3 {
		return imsg, errors.New("could not parse message")
	}
	imsg.channel = match[0][2]
	if imsg.channel == user {
		imsg.channel = match[0][1]
	}
	imsg.msg = match[0][3]
	return imsg, nil
}

func bastille(event chan unparsedMessage, writeMessage chan IRCMessage) {
	msgs := []string{
		"Bastille, yo brodudedudebro!!!!1",
		"Bastille, wat up homie",
		"Bastille, word",
		"Bastille, duuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuude",
		"'sup Bastille?",
	}

	for msg := range event {
		parsed, err := getChannel(msg.msg)
		if err != nil {
			continue
		}
		writeMessage <- IRCMessage{parsed.channel, msgs[rand.Intn(len(msgs))-1]}
	}
}

func amiami(event chan unparsedMessage, writeMessage chan IRCMessage) {
	matchTitle := regexp.MustCompile(`.*<meta property="og:title" content="(.+)" />.*`)
	matchDiscount := regexp.MustCompile(`[0-9]+\%OFF `)
	for msg := range event {
		parsed, err := getChannel(msg.msg)
		if err != nil {
			continue
		}

		resp, err := http.Get(matchAmiAmi.FindAllStringSubmatch(parsed.msg, -1)[0][1])
		if err != nil {
			fmt.Printf("%v\n", err)
			continue
		}

		body, err := ioutil.ReadAll(resp.Body)
		resp.Body.Close()
		if err != nil {
			fmt.Printf("%v\n", err)
			continue
		}

		writeMessage <- IRCMessage{parsed.channel, matchDiscount.ReplaceAllLiteralString(matchTitle.FindAllStringSubmatch(string(body), -1)[0][1], "")}
	}
}

func youtube(event chan unparsedMessage, writeMessage chan IRCMessage) {
	matchTitle := regexp.MustCompile(`.*<title>(.+)(?: - YouTube){1}</title>.*`)
	matchUser := regexp.MustCompile(`.*<a[^>]+class="[^"]+yt-user-name[^>]+>([^<]+)</a>.*`)
	for msg := range event {
		parsed, err := getChannel(msg.msg)
		if err != nil {
			continue
		}

		resp, err := http.Get(matchYouTube.FindAllStringSubmatch(parsed.msg, -1)[0][1])
		if err != nil {
			fmt.Printf("%v\n", err)
			continue
		}

		body, err := ioutil.ReadAll(resp.Body)
		resp.Body.Close()
		if err != nil {
			fmt.Printf("%v\n", err)
			continue
		}

		writeMessage <- IRCMessage{parsed.channel, html.UnescapeString(matchTitle.FindAllStringSubmatch(string(body), -1)[0][1] + " uploaded by " +
			matchUser.FindAllStringSubmatch(string(body), -1)[0][1])}
	}
}

func gelbooru(event chan unparsedMessage, writeMessage chan IRCMessage) {
	type Post struct {
		post string
		tags string `xml:",attr"`
	}

	for msg := range event {
		parsed, err := getChannel(msg.msg)
		if err != nil {
			continue
		}

		resp, err := http.Get("http://gelbooru.com/index.php?page=dapi&s=post&q=index&tags&id=" + msg.msg)
		if err != nil {
			fmt.Printf("%v\n", err)
			continue
		}

		body, err := ioutil.ReadAll(resp.Body)
		resp.Body.Close()
		if err != nil {
			fmt.Printf("%v\n", err)
			continue
		}

		fmt.Printf("%s\n", body)

		var result Post

		err = xml.Unmarshal(body, &result)
		if err != nil {
			fmt.Printf("%v\n", err)
			continue
		}

		fmt.Printf("%s\n", result.tags)
		writeMessage <- IRCMessage{parsed.channel, "tobedone"}
	}
}
<commit_msg>Forward user information to throttler<commit_after>package main

import (
	"encoding/xml" // gelbooru parsing
	"errors"
	"fmt"
	"github.com/jcline/goty"
	"html"
	"io/ioutil"
	"math/rand"
	"net/http"
	"os"
	"regexp"
	"time"
)

var user = ""

var matchGelbooru = regexp.MustCompile(`.*\Qhttp://gelbooru.com/index.php?page=post&s=view&id=\E([\d]+).*`)
var matchYouTube = regexp.MustCompile(`.*(https?://(?:www\.|)youtu(?:\.be|be\.com)/[^ ]+).*`)
var matchAmiAmi = regexp.MustCompile(`(https?://(?:www\.|)amiami.com/[^/]+/detail/.*)`)

func auth(con *goty.IRCConn, writeMessage chan IRCMessage) {
	var pswd string
	_, err := fmt.Scanf("%s", &pswd)
	if err != nil {
		os.Exit(1)
	}

	msg := IRCMessage{channel: "NickServ", msg: "IDENTIFY Laala " + pswd}
	writeMessage <- msg
}

func main() {
	args := os.Args
	if len(args) < 4 {
		os.Exit(1)
	}

	con, err := goty.Dial(args[1], args[2], args[3])
	user = args[2]
	if err != nil {
		fmt.Fprintf(os.Stderr, "err: %s\n", err.Error())
	}

	gelbooruEvent := make(chan unparsedMessage, 1000)
	youtubeEvent := make(chan unparsedMessage, 1000)
	amiAmiEvent := make(chan unparsedMessage, 1000)
	bastilleEvent := make(chan unparsedMessage, 1000)
	writeMessage := make(chan IRCMessage, 1000)

	go messageHandler(con, writeMessage)
	go gelbooru(gelbooruEvent, writeMessage)
	go youtube(youtubeEvent, writeMessage)
	go amiami(amiAmiEvent, writeMessage)
	go bastille(bastilleEvent, writeMessage)

	auth(con, writeMessage)
	con.Write <- "JOIN " + args[4]

	for msg := range con.Read {
		prepared := unparsedMessage{msg, time.Now()}
		fmt.Printf("%s||%s\n", prepared.when, prepared.msg)

		switch {
		case matchGelbooru.MatchString(msg):
			//gelbooruEvent <- matchGelbooru.FindAllStringSubmatch(msg, -1)[0][1]
		case matchYouTube.MatchString(msg):
			youtubeEvent <- prepared
		case matchAmiAmi.MatchString(msg):
			amiAmiEvent <- prepared
		default:
		}
	}
	con.Close()
}

type unparsedMessage struct {
	msg  string
	when time.Time
}

type IRCMessage struct {
	channel string
	msg     string
	user    string
}

func message(con *goty.IRCConn, msg IRCMessage) {
	con.Write <- "PRIVMSG " + msg.channel + " :" + msg.msg + "\r\n"
}

func messageHandler(con *goty.IRCConn, event chan IRCMessage) {
	allBooks := map[string]time.Time{}
	chanBooks := map[string]time.Time{}
	for msg := range event {
		now := time.Now()
		key := msg.channel + ":" + msg.user
		if now.Sub(allBooks[key]) < time.Second*10 || now.Sub(chanBooks[key]) < time.Second*2 {
			continue
		}
		allBooks[key] = now
		chanBooks[key] = now
		message(con, msg)
	}
}

var PRIVMSG = regexp.MustCompile(`:(.+)![^ ]+ PRIVMSG ([^ ]+) :(.*)`)

func getMsgInfo(msg string) (*IRCMessage, error) {
	// :nick!~realname@0.0.0.0 PRIVMSG #chan :msg
	imsg := new(IRCMessage)
	match := PRIVMSG.FindAllStringSubmatch(msg, -1)
	if len(match) < 1 {
		return imsg, errors.New("could not parse message")
	}
	if len(match[0]) < 3 {
		return imsg, errors.New("could not parse message")
	}
	imsg.user = user
	imsg.channel = match[0][2]
	if imsg.channel == user {
		imsg.channel = match[0][1]
	}
	imsg.msg = match[0][3]
	return imsg, nil
}

func bastille(event chan unparsedMessage, writeMessage chan IRCMessage) {
	msgs := []string{
		"Bastille, yo brodudedudebro!!!!1",
		"Bastille, wat up homie",
		"Bastille, word",
		"Bastille, duuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuude",
		"'sup Bastille?",
	}

	for msg := range event {
		parsed, err := getMsgInfo(msg.msg)
		if err != nil {
			continue
		}
		writeMessage <- IRCMessage{parsed.channel, msgs[rand.Intn(len(msgs))-1], parsed.user}
	}
}

func amiami(event chan unparsedMessage, writeMessage chan IRCMessage) {
	matchTitle := regexp.MustCompile(`.*<meta property="og:title" content="(.+)" />.*`)
	matchDiscount := regexp.MustCompile(`[0-9]+\%OFF `)
	for msg := range event {
		parsed, err := getMsgInfo(msg.msg)
		if err != nil {
			continue
		}

		resp, err := http.Get(matchAmiAmi.FindAllStringSubmatch(parsed.msg, -1)[0][1])
		if err != nil {
			fmt.Printf("%v\n", err)
			continue
		}

		body, err := ioutil.ReadAll(resp.Body)
		resp.Body.Close()
		if err != nil {
			fmt.Printf("%v\n", err)
			continue
		}

		writeMessage <- IRCMessage{parsed.channel, matchDiscount.ReplaceAllLiteralString(matchTitle.FindAllStringSubmatch(string(body), -1)[0][1], ""), parsed.user}
	}
}

func youtube(event chan unparsedMessage, writeMessage chan IRCMessage) {
	matchTitle := regexp.MustCompile(`.*<title>(.+)(?: - YouTube){1}</title>.*`)
	matchUser := regexp.MustCompile(`.*<a[^>]+class="[^"]+yt-user-name[^>]+>([^<]+)</a>.*`)
	for msg := range event {
		parsed, err := getMsgInfo(msg.msg)
		if err != nil {
			continue
		}

		resp, err := http.Get(matchYouTube.FindAllStringSubmatch(parsed.msg, -1)[0][1])
		if err != nil {
			fmt.Printf("%v\n", err)
			continue
		}

		body, err := ioutil.ReadAll(resp.Body)
		resp.Body.Close()
		if err != nil {
			fmt.Printf("%v\n", err)
			continue
		}

		writeMessage <- IRCMessage{parsed.channel, html.UnescapeString(matchTitle.FindAllStringSubmatch(string(body), -1)[0][1] + " uploaded by " +
			matchUser.FindAllStringSubmatch(string(body), -1)[0][1]), parsed.user}
	}
}

func gelbooru(event chan unparsedMessage, writeMessage chan IRCMessage) {
	type Post struct {
		post string
		tags string `xml:",attr"`
	}

	for msg := range event {
		parsed, err := getMsgInfo(msg.msg)
		if err != nil {
			continue
		}

		resp, err := http.Get("http://gelbooru.com/index.php?page=dapi&s=post&q=index&tags&id=" + msg.msg)
		if err != nil {
			fmt.Printf("%v\n", err)
			continue
		}

		body, err := ioutil.ReadAll(resp.Body)
		resp.Body.Close()
		if err != nil {
			fmt.Printf("%v\n", err)
			continue
		}

		fmt.Printf("%s\n", body)

		var result Post

		err = xml.Unmarshal(body, &result)
		if err != nil {
			fmt.Printf("%v\n", err)
			continue
		}

		fmt.Printf("%s\n", result.tags)
		writeMessage <- IRCMessage{parsed.channel, "tobedone", parsed.user}
	}
}
<|endoftext|>


<commit_before>package main

import (
	"encoding/xml" // gelbooru parsing
	"errors"
	"fmt"
	"github.com/jcline/goty"
	"html"
	"io/ioutil"
	"math/rand"
	"net/http"
	"os"
	"regexp"
	"strings"
	"time"
)

var user = ""

var matchAniDBSearch = regexp.MustCompile(`!anidb +(.+) *`)
var matchAmiAmi = regexp.MustCompile(`(?:https?://|)(?:www\.|)amiami.com/([^/ ]+/detail/[^ ]+)`)
var matchGelbooru = regexp.MustCompile(`(?:https?://|)\Qgelbooru.com/index.php?page=post&s=view&id=\E([\d]+)`)
var matchMAL = regexp.MustCompile(`!anime+(.+)`)
var matchReddit = regexp.MustCompile(`(?:http://|)(?:www\.|https://pay\.|)redd(?:\.it|it\.com)/(r/[^/ ]+/comments/[^/ ]+)/?(?: .*|\z)`)
var matchYouTube = regexp.MustCompile(`(?:https?://|)(?:www\.|)(youtu(?:\.be|be\.com)/[^ ]+)`)

func auth(con *goty.IRCConn, writeMessage chan IRCMessage, user string) {
	var pswd string
	fmt.Printf("Password for NickServ:\n")
	_, err := fmt.Scanf("%s", &pswd)
	if err != nil {
		os.Exit(1)
	}

	msg := IRCMessage{channel: "NickServ", msg: "IDENTIFY " + user + " " + pswd}
	writeMessage <- msg
}

func malAuth() (user string, pswd string, err error) {
	fmt.Printf("User for MAL:\n")
	_, err = fmt.Scanf("%s", &user)
	fmt.Printf("Password for MAL:\n")
	_, err = fmt.Scanf("%s", &pswd)
	return
}

func main() {
	args := os.Args
	if len(args) < 4 {
		os.Exit(1)
	}

	con, err := goty.Dial(args[1], args[2], args[3])
	user = args[2]
	if err != nil {
		fmt.Fprintf(os.Stderr, "err: %s\n", err.Error())
	}

	malUser, malPswd, err := malAuth()

	writeMessage := make(chan IRCMessage, 1000)
	go messageHandler(con, writeMessage)

	amiAmiEvent := make(chan unparsedMessage, 1000)
	//anidbEvent := make(chan unparsedMessage, 1000)
	bastilleEvent := make(chan unparsedMessage, 1000)
	gelbooruEvent := make(chan unparsedMessage, 1000)
	malEvent := make(chan unparsedMessage, 1000)
	redditEvent := make(chan unparsedMessage, 1000)
	youtubeEvent := make(chan unparsedMessage, 1000)

	go amiami(amiAmiEvent, writeMessage)
	//go anidb(anidbEvent, writeMessage)
	go bastille(bastilleEvent, writeMessage)
	go gelbooru(gelbooruEvent, writeMessage)
	go malSearch(malEvent, writeMessage, malUser, malPswd)
	go reddit(redditEvent, writeMessage)
	go youtube(youtubeEvent, writeMessage)

	auth(con, writeMessage, user)
	con.Write <- "JOIN " + args[4]

	for msg := range con.Read {
		prepared := unparsedMessage{msg, time.Now()}
		fmt.Printf("%s||%s\n", prepared.when, prepared.msg)

		switch {
		case matchAmiAmi.MatchString(prepared.msg):
			amiAmiEvent <- prepared
		case matchGelbooru.MatchString(prepared.msg):
			//gelbooruEvent <- matchGelbooru.FindAllStringSubmatch(prepared.msg, -1)[0][1]
		case matchMAL.MatchString(prepared.msg):
			malEvent <- prepared
		case matchReddit.MatchString(prepared.msg):
			redditEvent <- prepared
		case matchYouTube.MatchString(prepared.msg):
			youtubeEvent <- prepared
		//case matchAniDBSearch.MatchString(prepared.msg):
		//anidbEvent <- prepared
		default:
		}
	}
	con.Close()
}

type unparsedMessage struct {
	msg  string
	when time.Time
}

type IRCMessage struct {
	channel string
	msg     string
	user    string
}

func message(con *goty.IRCConn, msg IRCMessage) {
	con.Write <- "PRIVMSG " + msg.channel + " :" + msg.msg + "\r\n"
}

func messageHandler(con *goty.IRCConn, event chan IRCMessage) {
	allBooks := map[string]time.Time{}
	chanBooks := map[string]time.Time{}
	for msg := range event {
		now := time.Now()
		key := msg.channel + ":" + msg.user
		if now.Sub(allBooks[key]) < time.Second*10 || now.Sub(chanBooks[key]) < time.Second*2 {
			continue
		}
		allBooks[key] = now
		chanBooks[key] = now
		message(con, msg)
	}
}

var PRIVMSG = regexp.MustCompile(`:(.+)![^ ]+ PRIVMSG ([^ ]+) :(.*)`)

func getMsgInfo(msg string) (*IRCMessage, error) {
	// :nick!~realname@0.0.0.0 PRIVMSG #chan :msg
	imsg := new(IRCMessage)
	match := PRIVMSG.FindAllStringSubmatch(msg, -1)
	if len(match) < 1 {
		return imsg, errors.New("could not parse message")
	}
	if len(match[0]) < 3 {
		return imsg, errors.New("could not parse message")
	}
	imsg.user = user
	imsg.channel = match[0][2]
	if imsg.channel == user {
		imsg.channel = match[0][1]
	}
	imsg.msg = match[0][3]
	return imsg, nil
}

func bastille(event chan unparsedMessage, writeMessage chan IRCMessage) {
	msgs := []string{
		"Bastille, yo brodudedudebro!!!!1",
		"Bastille, wat up homie",
		"Bastille, word",
		"Bastille, duuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuude",
		"'sup Bastille?",
	}

	for msg := range event {
		parsed, err := getMsgInfo(msg.msg)
		if err != nil {
			continue
		}
		writeMessage <- IRCMessage{parsed.channel, msgs[rand.Intn(len(msgs))-1], parsed.user}
	}
}

type uriFunc func(*string) (*string, error)
type writeFunc func(*IRCMessage, *string) error
type errFunc func(*IRCMessage, error) error

func scrapeAndSend(event chan unparsedMessage, findUri uriFunc, write writeFunc) {
	for msg := range event {
		parsed, err := getMsgInfo(msg.msg)
		if err != nil {
			fmt.Printf("%v\n", err)
			continue
		}

		uri, err := findUri(&parsed.msg)
		if err != nil {
			fmt.Printf("%v\n", err)
			continue
		}

		resp, err := http.Get(*uri)
		if err != nil {
			fmt.Printf("%v\n", err)
			continue
		}

		bodyBytes, err := ioutil.ReadAll(resp.Body)
		resp.Body.Close()
		if err != nil {
			fmt.Printf("%v\n", err)
			continue
		}
		body := string(bodyBytes)

		err = write(parsed, &body)
		if err != nil {
			fmt.Printf("%v\n", err)
			continue
		}
	}
}

func getFirstMatch(re *regexp.Regexp, matchee *string) (*string, error) {
	match := re.FindAllStringSubmatch(*matchee, -1)
	if len(match) < 1 {
		return nil, errors.New("Could not match")
	}
	if len(match[0]) < 2 {
		return nil, errors.New("Could not match")
	}
	return &match[0][1], nil
}

func amiami(event chan unparsedMessage, writeMessage chan IRCMessage) {
	matchTitle := regexp.MustCompile(`.*<meta property="og:title" content="(.+)" />.*`)
	matchDiscount := regexp.MustCompile(`[0-9]+\%OFF `)
	scrapeAndSend(event, func(msg *string) (*string, error) {
		uri, err := getFirstMatch(matchAmiAmi, msg)
		if err != nil {
			return nil, err
		}

		fullUri := "http://amiami.com/" + *uri
		return &fullUri, nil
	},
		func(msg *IRCMessage, body *string) error {
			title, err := getFirstMatch(matchTitle, body)
			if err != nil {
				return err
			}

			writeMessage <- IRCMessage{msg.channel, "[AmiAmi]: " + matchDiscount.ReplaceAllLiteralString(*title, ""), msg.user}
			return nil
		})
}

func reddit(event chan unparsedMessage, writeMessage chan IRCMessage) {
	matchTitle := regexp.MustCompile(`.*<title>(.+)</title>.*`)

	scrapeAndSend(event, func(msg *string) (*string, error) {
		uri, err := getFirstMatch(matchReddit, msg)
		if err != nil {
			return nil, err
		}

		fullUri := "http://reddit.com/" + *uri
		return &fullUri, nil
	},
		func(msg *IRCMessage, body *string) error {
			title, err := getFirstMatch(matchTitle, body)
			if err != nil {
				return err
			}

			writeMessage <- IRCMessage{msg.channel, "[Reddit]: " + html.UnescapeString(*title), msg.user}
			return nil
		})
}

func youtube(event chan unparsedMessage, writeMessage chan IRCMessage) {
	matchTitle := regexp.MustCompile(`.*<title>(.+)(?: - YouTube){1}</title>.*`)
	matchUser := regexp.MustCompile(`.*<a[^>]+class="[^"]+yt-user-name[^>]+>([^<]+)</a>.*`)

	scrapeAndSend(event, func(msg *string) (*string, error) {
		uri, err := getFirstMatch(matchYouTube, msg)
		if err != nil {
			return nil, err
		}

		fullUri := "http://" + *uri
		return &fullUri, nil
	},
		func(msg *IRCMessage, body *string) error {
			title, err := getFirstMatch(matchTitle, body)
			if err != nil {
				return err
			}
			user, err := getFirstMatch(matchUser, body)
			if err != nil {
				return err
			}
			writeMessage <- IRCMessage{msg.channel, "[YouTube]: " + html.UnescapeString(*title+" uploaded by "+*user), msg.user}
			return nil
		})
}

func gelbooru(event chan unparsedMessage, writeMessage chan IRCMessage) {
	type Post struct {
		post string
		tags string `xml:",attr"`
	}

	for msg := range event {
		parsed, err := getMsgInfo(msg.msg)
		if err != nil {
			continue
		}

		resp, err := http.Get("http://gelbooru.com/index.php?page=dapi&s=post&q=index&tags&id=" + msg.msg)
		if err != nil {
			fmt.Printf("%v\n", err)
			continue
		}

		body, err := ioutil.ReadAll(resp.Body)
		resp.Body.Close()
		if err != nil {
			fmt.Printf("%v\n", err)
			continue
		}

		fmt.Printf("%s\n", body)

		var result Post

		err = xml.Unmarshal(body, &result)
		if err != nil {
			fmt.Printf("%v\n", err)
			continue
		}

		fmt.Printf("%s\n", result.tags)
		writeMessage <- IRCMessage{parsed.channel, "tobedone", parsed.user}
	}
}

func anidb(event chan unparsedMessage, writeMessage chan IRCMessage) {
	cache := make(map[string]string)

	for msg := range event {
		parsed, err := getMsgInfo(msg.msg)
		if err != nil {
			continue
		}

		val, ok := cache[msg.msg]
		if ok {
			writeMessage <- IRCMessage{parsed.channel, val, parsed.user}
		} else {
			// totally broken :(
			resp, err := http.Get("http://anisearch.outrance.pl/index.php?task=search&query=" + msg.msg)
			if err != nil {
				fmt.Printf("%v\n", err)
				continue
			}

			body, err := ioutil.ReadAll(resp.Body)
			resp.Body.Close()
			if err != nil {
				fmt.Printf("%v\n", err)
				continue
			}

			fmt.Printf("%s\n", body)

			//cache[msg.msg] = ""
		}
	}
}

func malSearch(event chan unparsedMessage, writeMessage chan IRCMessage, user string, pswd string) {
	matchID := regexp.MustCompile(`.*<id>([0-9]+)</id>.*`)
	matchTitle := regexp.MustCompile(`.*<title>(.+)</title>.*`)
	scrapeAndSend(event, func(msg *string) (*string, error) {
		terms, err := getFirstMatch(matchMAL, msg)
		if err != nil {
			return nil, err
		}
		uri := "http://" + user + ":" + pswd + "@myanimelist.net/api/anime/search.xml?q=" + strings.Replace(*terms, " ", "+", -1)
		return &uri, nil
	},
		func(msg *IRCMessage, body *string) error {
			if len(*body) < 10 {
				writeMessage <- IRCMessage{msg.channel, "┐('～`；)┌", msg.user}
				return errors.New("No results")
			}
			fmt.Printf("%s\n", *body)
			id, err := getFirstMatch(matchID, body)
			if err != nil {
				return err
			}
			title, err := getFirstMatch(matchTitle, body)
			if err != nil {
				return err
			}
			writeMessage <- IRCMessage{msg.channel, *title + ": http://myanimelist.net/anime/" + *id, msg.user}
			return nil
		})
}
<commit_msg>More error messages for search<commit_after>package main

import (
	"encoding/xml" // gelbooru parsing
	"errors"
	"fmt"
	"github.com/jcline/goty"
	"html"
	"io/ioutil"
	"math/rand"
	"net/http"
	"os"
	"regexp"
	"strings"
	"time"
)

var user = ""

var matchAniDBSearch = regexp.MustCompile(`!anidb +(.+) *`)
var matchAmiAmi = regexp.MustCompile(`(?:https?://|)(?:www\.|)amiami.com/([^/ ]+/detail/[^ ]+)`)
var matchGelbooru = regexp.MustCompile(`(?:https?://|)\Qgelbooru.com/index.php?page=post&s=view&id=\E([\d]+)`)
var matchMAL = regexp.MustCompile(`!anime+(.+)`)
var matchReddit = regexp.MustCompile(`(?:http://|)(?:www\.|https://pay\.|)redd(?:\.it|it\.com)/(r/[^/ ]+/comments/[^/ ]+)/?(?: .*|\z)`)
var matchYouTube = regexp.MustCompile(`(?:https?://|)(?:www\.|)(youtu(?:\.be|be\.com)/[^ ]+)`)

func auth(con *goty.IRCConn, writeMessage chan IRCMessage, user string) {
	var pswd string
	fmt.Printf("Password for NickServ:\n")
	_, err := fmt.Scanf("%s", &pswd)
	if err != nil {
		os.Exit(1)
	}

	msg := IRCMessage{channel: "NickServ", msg: "IDENTIFY " + user + " " + pswd}
	writeMessage <- msg
}

func malAuth() (user string, pswd string, err error) {
	fmt.Printf("User for MAL:\n")
	_, err = fmt.Scanf("%s", &user)
	fmt.Printf("Password for MAL:\n")
	_, err = fmt.Scanf("%s", &pswd)
	return
}

func main() {
	args := os.Args
	if len(args) < 4 {
		os.Exit(1)
	}

	con, err := goty.Dial(args[1], args[2], args[3])
	user = args[2]
	if err != nil {
		fmt.Fprintf(os.Stderr, "err: %s\n", err.Error())
	}

	malUser, malPswd, err := malAuth()

	writeMessage := make(chan IRCMessage, 1000)
	go messageHandler(con, writeMessage)

	amiAmiEvent := make(chan unparsedMessage, 1000)
	//anidbEvent := make(chan unparsedMessage, 1000)
	bastilleEvent := make(chan unparsedMessage, 1000)
	gelbooruEvent := make(chan unparsedMessage, 1000)
	malEvent := make(chan unparsedMessage, 1000)
	redditEvent := make(chan unparsedMessage, 1000)
	youtubeEvent := make(chan unparsedMessage, 1000)

	go amiami(amiAmiEvent, writeMessage)
	//go anidb(anidbEvent, writeMessage)
	go bastille(bastilleEvent, writeMessage)
	go gelbooru(gelbooruEvent, writeMessage)
	go malSearch(malEvent, writeMessage, malUser, malPswd)
	go reddit(redditEvent, writeMessage)
	go youtube(youtubeEvent, writeMessage)

	auth(con, writeMessage, user)
	con.Write <- "JOIN " + args[4]

	for msg := range con.Read {
		prepared := unparsedMessage{msg, time.Now()}
		fmt.Printf("%s||%s\n", prepared.when, prepared.msg)

		switch {
		case matchAmiAmi.MatchString(prepared.msg):
			amiAmiEvent <- prepared
		case matchGelbooru.MatchString(prepared.msg):
			//gelbooruEvent <- matchGelbooru.FindAllStringSubmatch(prepared.msg, -1)[0][1]
		case matchMAL.MatchString(prepared.msg):
			malEvent <- prepared
		case matchReddit.MatchString(prepared.msg):
			redditEvent <- prepared
		case matchYouTube.MatchString(prepared.msg):
			youtubeEvent <- prepared
		//case matchAniDBSearch.MatchString(prepared.msg):
		//anidbEvent <- prepared
		default:
		}
	}
	con.Close()
}

type unparsedMessage struct {
	msg  string
	when time.Time
}

type IRCMessage struct {
	channel string
	msg     string
	user    string
}

func message(con *goty.IRCConn, msg IRCMessage) {
	con.Write <- "PRIVMSG " + msg.channel + " :" + msg.msg + "\r\n"
}

func messageHandler(con *goty.IRCConn, event chan IRCMessage) {
	allBooks := map[string]time.Time{}
	chanBooks := map[string]time.Time{}
	for msg := range event {
		now := time.Now()
		key := msg.channel + ":" + msg.user
		if now.Sub(allBooks[key]) < time.Second*10 || now.Sub(chanBooks[key]) < time.Second*2 {
			continue
		}
		allBooks[key] = now
		chanBooks[key] = now
		message(con, msg)
	}
}

var PRIVMSG = regexp.MustCompile(`:(.+)![^ ]+ PRIVMSG ([^ ]+) :(.*)`)

func getMsgInfo(msg string) (*IRCMessage, error) {
	// :nick!~realname@0.0.0.0 PRIVMSG #chan :msg
	imsg := new(IRCMessage)
	match := PRIVMSG.FindAllStringSubmatch(msg, -1)
	if len(match) < 1 {
		return imsg, errors.New("could not parse message")
	}
	if len(match[0]) < 3 {
		return imsg, errors.New("could not parse message")
	}
	imsg.user = user
	imsg.channel = match[0][2]
	if imsg.channel == user {
		imsg.channel = match[0][1]
	}
	imsg.msg = match[0][3]
	return imsg, nil
}

func bastille(event chan unparsedMessage, writeMessage chan IRCMessage) {
	msgs := []string{
		"Bastille, yo brodudedudebro!!!!1",
		"Bastille, wat up homie",
		"Bastille, word",
		"Bastille, duuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuude",
		"'sup Bastille?",
	}

	for msg := range event {
		parsed, err := getMsgInfo(msg.msg)
		if err != nil {
			continue
		}
		writeMessage <- IRCMessage{parsed.channel, msgs[rand.Intn(len(msgs))-1], parsed.user}
	}
}

type uriFunc func(*string) (*string, error)
type writeFunc func(*IRCMessage, *string) error
type errFunc func(*IRCMessage, error) error

func scrapeAndSend(event chan unparsedMessage, findUri uriFunc, write writeFunc) {
	for msg := range event {
		parsed, err := getMsgInfo(msg.msg)
		if err != nil {
			fmt.Printf("%v\n", err)
			continue
		}

		uri, err := findUri(&parsed.msg)
		if err != nil {
			fmt.Printf("%v\n", err)
			continue
		}

		resp, err := http.Get(*uri)
		if err != nil {
			fmt.Printf("%v\n", err)
			continue
		}

		bodyBytes, err := ioutil.ReadAll(resp.Body)
		resp.Body.Close()
		if err != nil {
			fmt.Printf("%v\n", err)
			continue
		}
		body := string(bodyBytes)

		err = write(parsed, &body)
		if err != nil {
			fmt.Printf("%v\n", err)
			continue
		}
	}
}

func getFirstMatch(re *regexp.Regexp, matchee *string) (*string, error) {
	match := re.FindAllStringSubmatch(*matchee, -1)
	if len(match) < 1 {
		return nil, errors.New("Could not match")
	}
	if len(match[0]) < 2 {
		return nil, errors.New("Could not match")
	}
	return &match[0][1], nil
}

func amiami(event chan unparsedMessage, writeMessage chan IRCMessage) {
	matchTitle := regexp.MustCompile(`.*<meta property="og:title" content="(.+)" />.*`)
	matchDiscount := regexp.MustCompile(`[0-9]+\%OFF `)
	scrapeAndSend(event, func(msg *string) (*string, error) {
		uri, err := getFirstMatch(matchAmiAmi, msg)
		if err != nil {
			return nil, err
		}

		fullUri := "http://amiami.com/" + *uri
		return &fullUri, nil
	},
		func(msg *IRCMessage, body *string) error {
			title, err := getFirstMatch(matchTitle, body)
			if err != nil {
				return err
			}

			writeMessage <- IRCMessage{msg.channel, "[AmiAmi]: " + matchDiscount.ReplaceAllLiteralString(*title, ""), msg.user}
			return nil
		})
}

func reddit(event chan unparsedMessage, writeMessage chan IRCMessage) {
	matchTitle := regexp.MustCompile(`.*<title>(.+)</title>.*`)

	scrapeAndSend(event, func(msg *string) (*string, error) {
		uri, err := getFirstMatch(matchReddit, msg)
		if err != nil {
			return nil, err
		}

		fullUri := "http://reddit.com/" + *uri
		return &fullUri, nil
	},
		func(msg *IRCMessage, body *string) error {
			title, err := getFirstMatch(matchTitle, body)
			if err != nil {
				return err
			}

			writeMessage <- IRCMessage{msg.channel, "[Reddit]: " + html.UnescapeString(*title), msg.user}
			return nil
		})
}

func youtube(event chan unparsedMessage, writeMessage chan IRCMessage) {
	matchTitle := regexp.MustCompile(`.*<title>(.+)(?: - YouTube){1}</title>.*`)
	matchUser := regexp.MustCompile(`.*<a[^>]+class="[^"]+yt-user-name[^>]+>([^<]+)</a>.*`)

	scrapeAndSend(event, func(msg *string) (*string, error) {
		uri, err := getFirstMatch(matchYouTube, msg)
		if err != nil {
			return nil, err
		}

		fullUri := "http://" + *uri
		return &fullUri, nil
	},
		func(msg *IRCMessage, body *string) error {
			title, err := getFirstMatch(matchTitle, body)
			if err != nil {
				return err
			}
			user, err := getFirstMatch(matchUser, body)
			if err != nil {
				return err
			}
			writeMessage <- IRCMessage{msg.channel, "[YouTube]: " + html.UnescapeString(*title+" uploaded by "+*user), msg.user}
			return nil
		})
}

func gelbooru(event chan unparsedMessage, writeMessage chan IRCMessage) {
	type Post struct {
		post string
		tags string `xml:",attr"`
	}

	for msg := range event {
		parsed, err := getMsgInfo(msg.msg)
		if err != nil {
			continue
		}

		resp, err := http.Get("http://gelbooru.com/index.php?page=dapi&s=post&q=index&tags&id=" + msg.msg)
		if err != nil {
			fmt.Printf("%v\n", err)
			continue
		}

		body, err := ioutil.ReadAll(resp.Body)
		resp.Body.Close()
		if err != nil {
			fmt.Printf("%v\n", err)
			continue
		}

		fmt.Printf("%s\n", body)

		var result Post

		err = xml.Unmarshal(body, &result)
		if err != nil {
			fmt.Printf("%v\n", err)
			continue
		}

		fmt.Printf("%s\n", result.tags)
		writeMessage <- IRCMessage{parsed.channel, "tobedone", parsed.user}
	}
}

func anidb(event chan unparsedMessage, writeMessage chan IRCMessage) {
	cache := make(map[string]string)

	for msg := range event {
		parsed, err := getMsgInfo(msg.msg)
		if err != nil {
			continue
		}

		val, ok := cache[msg.msg]
		if ok {
			writeMessage <- IRCMessage{parsed.channel, val, parsed.user}
		} else {
			// totally broken :(
			resp, err := http.Get("http://anisearch.outrance.pl/index.php?task=search&query=" + msg.msg)
			if err != nil {
				fmt.Printf("%v\n", err)
				continue
			}

			body, err := ioutil.ReadAll(resp.Body)
			resp.Body.Close()
			if err != nil {
				fmt.Printf("%v\n", err)
				continue
			}

			fmt.Printf("%s\n", body)

			//cache[msg.msg] = ""
		}
	}
}

func malSearch(event chan unparsedMessage, writeMessage chan IRCMessage, user string, pswd string) {
	matchID := regexp.MustCompile(`.*<id>([0-9]+)</id>.*`)
	matchTitle := regexp.MustCompile(`.*<title>(.+)</title>.*`)
	scrapeAndSend(event, func(msg *string) (*string, error) {
		terms, err := getFirstMatch(matchMAL, msg)
		if err != nil {
			return nil, err
		}
		uri := "http://" + user + ":" + pswd + "@myanimelist.net/api/anime/search.xml?q=" + strings.Replace(*terms, " ", "+", -1)
		return &uri, nil
	},
		func(msg *IRCMessage, body *string) error {
			if len(*body) < 10 {
				writeMessage <- IRCMessage{msg.channel, "┐('～`；)┌", msg.user}
				return errors.New("No results")
			}
			fmt.Printf("%s\n", *body)
			id, err := getFirstMatch(matchID, body)
			if err != nil {
				writeMessage <- IRCMessage{msg.channel, "┐('～`；)┌", msg.user}
				return err
			}
			title, err := getFirstMatch(matchTitle, body)
			if err != nil {
				writeMessage <- IRCMessage{msg.channel, "┐('～`；)┌", msg.user}
				return err
			}
			writeMessage <- IRCMessage{msg.channel, *title + ": http://myanimelist.net/anime/" + *id, msg.user}
			return nil
		})
}
<|endoftext|>


<commit_before>package main

import (
	"bytes"
	"fmt"
	"log"
	"sort"
	"strings"

	"github.com/mitchellh/cli"
)

// helpFunc is a cli.HelpFunc that can is used to output the help for Terraform.
func helpFunc(commands map[string]cli.CommandFactory) string {
	// Determine the maximum key length, and classify based on type
	var otherCommands []string
	maxKeyLen := 0

	for key := range commands {
		if _, ok := HiddenCommands[key]; ok {
			// We don't consider hidden commands when deciding the
			// maximum command length.
			continue
		}

		if len(key) > maxKeyLen {
			maxKeyLen = len(key)
		}

		isOther := true
		for _, candidate := range PrimaryCommands {
			if candidate == key {
				isOther = false
				break
			}
		}
		if isOther {
			otherCommands = append(otherCommands, key)
		}
	}
	sort.Strings(otherCommands)

	// The output produced by this is included in the docs at
	// website/source/docs/cli/commands/index.html.markdown; if you
	// change this then consider updating that to match.
	helpText := fmt.Sprintf(`
Usage: terraform [global options] <subcommand> [args]

The available commands for execution are listed below.
The primary workflow commands are given first, followed by
less common or more advanced commands.

Main commands:
%s
All other commands:
%s
Global options (use these before the subcommand, if any):
  -chdir=DIR    Switch to a different working directory before executing the
                given subcommand.
  -help         Show this help output, or the help for a specified subcommand.
  -version      An alias for the "version" subcommand.
`, listCommands(commands, PrimaryCommands, maxKeyLen), listCommands(commands, otherCommands, maxKeyLen))

	return strings.TrimSpace(helpText)
}

// listCommands just lists the commands in the map with the
// given maximum key length.
func listCommands(allCommands map[string]cli.CommandFactory, order []string, maxKeyLen int) string {
	var buf bytes.Buffer

	for _, key := range order {
		commandFunc, ok := allCommands[key]
		if !ok {
			// This suggests an inconsistency in the command table definitions
			// in commands.go .
			panic("command not found: " + key)
		}

		command, err := commandFunc()
		if err != nil {
			// This would be really weird since there's no good reason for
			// any of our command factories to fail.
			log.Printf("[ERR] cli: Command '%s' failed to load: %s",
				key, err)
			continue
		}

		key = fmt.Sprintf("%s%s", key, strings.Repeat(" ", maxKeyLen-len(key)))
		buf.WriteString(fmt.Sprintf("  %s  %s\n", key, command.Synopsis()))
	}

	return buf.String()
}
<commit_msg>Improve the help.go docs: typo and a more explicit comment.<commit_after>package main

import (
	"bytes"
	"fmt"
	"log"
	"sort"
	"strings"

	"github.com/mitchellh/cli"
)

// helpFunc is a cli.HelpFunc that can be used to output the help CLI instructions for Terraform.
func helpFunc(commands map[string]cli.CommandFactory) string {
	// Determine the maximum key length, and classify based on type
	var otherCommands []string
	maxKeyLen := 0

	for key := range commands {
		if _, ok := HiddenCommands[key]; ok {
			// We don't consider hidden commands when deciding the
			// maximum command length.
			continue
		}

		if len(key) > maxKeyLen {
			maxKeyLen = len(key)
		}

		isOther := true
		for _, candidate := range PrimaryCommands {
			if candidate == key {
				isOther = false
				break
			}
		}
		if isOther {
			otherCommands = append(otherCommands, key)
		}
	}
	sort.Strings(otherCommands)

	// The output produced by this is included in the docs at
	// website/source/docs/cli/commands/index.html.markdown; if you
	// change this then consider updating that to match.
	helpText := fmt.Sprintf(`
Usage: terraform [global options] <subcommand> [args]

The available commands for execution are listed below.
The primary workflow commands are given first, followed by
less common or more advanced commands.

Main commands:
%s
All other commands:
%s
Global options (use these before the subcommand, if any):
  -chdir=DIR    Switch to a different working directory before executing the
                given subcommand.
  -help         Show this help output, or the help for a specified subcommand.
  -version      An alias for the "version" subcommand.
`, listCommands(commands, PrimaryCommands, maxKeyLen), listCommands(commands, otherCommands, maxKeyLen))

	return strings.TrimSpace(helpText)
}

// listCommands just lists the commands in the map with the
// given maximum key length.
func listCommands(allCommands map[string]cli.CommandFactory, order []string, maxKeyLen int) string {
	var buf bytes.Buffer

	for _, key := range order {
		commandFunc, ok := allCommands[key]
		if !ok {
			// This suggests an inconsistency in the command table definitions
			// in commands.go .
			panic("command not found: " + key)
		}

		command, err := commandFunc()
		if err != nil {
			// This would be really weird since there's no good reason for
			// any of our command factories to fail.
			log.Printf("[ERR] cli: Command '%s' failed to load: %s",
				key, err)
			continue
		}

		key = fmt.Sprintf("%s%s", key, strings.Repeat(" ", maxKeyLen-len(key)))
		buf.WriteString(fmt.Sprintf("  %s  %s\n", key, command.Synopsis()))
	}

	return buf.String()
}
<|endoftext|>