<commit_before>package main

import (
	"crypto/tls"
	"crypto/x509"
	"errors"
	"flag"
	"fmt"
	"github.com/channelmeter/vault-gatekeeper-mesos/gatekeeper"
	"github.com/franela/goreq"
	"github.com/gin-gonic/gin"
	"log"
	"net/http"
	"net/url"
	"os"
	"strconv"
	"sync"
	"time"
)

type GkStatus string

const (
	StatusSealed   GkStatus = "Sealed"
	StatusUnsealed GkStatus = "Unsealed"
)

var gitNearestTag = "dev"

var config struct {
	Vault struct {
		Server     string
		Insecure   bool
		CaCert     string
		CaPath     string
		GkPolicies string
	}
	ListenAddress string
	Mesos         string
	MaxTaskLife   time.Duration
	AppIdAuth     AppIdUnsealer
	CubbyAuth     CubbyUnsealer
}

var state struct {
	Status GkStatus `json:"status"`
	Stats  struct {
		Requests   int32 `json:"requests"`
		Successful int32 `json:"successful"`
		Denied     int32 `json:"denied"`
	} `json:"stats"`
	Started  time.Time     `json:"started"`
	Token    string        `json:"-"`
	OnSealed chan struct{} `json:"-"`
	sync.RWMutex
}

var errAlreadyUnsealed = errors.New("Already unsealed.")
var errUnknownAuthMethod = errors.New("Unknown method for authorization.")

func defaultEnvVar(key string, def string) (val string) {
	val = os.Getenv(key)
	if val == "" {
		val = def
	}
	return
}

func init() {
	flag.StringVar(&config.ListenAddress, "listen", defaultEnvVar("LISTEN_ADDR", ":9201"), "Hostname and port to listen on. (Overrides the LISTEN_ADDR environment variable if set.)")

	flag.StringVar(&config.Mesos, "mesos", defaultEnvVar("MESOS_MASTER", ""), "Address to mesos master. (Overrides the MESOS_MASTER environment variable if set.)")

	flag.StringVar(&config.Vault.Server, "vault", defaultEnvVar("VAULT_ADDR", ""), "Address to vault server. (Overrides the VAULT_ADDR environment variable if set.)")
	flag.StringVar(&config.Vault.GkPolicies, "policies", defaultEnvVar("GATE_POLICIES", "/gatekeeper"), "Path to the json formatted policies configuration file on the vault generic backend.")
	flag.BoolVar(&config.Vault.Insecure, "tls-skip-verify", func() bool {
		b, err := strconv.ParseBool(defaultEnvVar("VAULT_SKIP_VERIFY", "0"))
		return err == nil && b
	}(), "Do not verify TLS certificate. (Overrides the VAULT_SKIP_VERIFY environment variable if set.)")
	flag.StringVar(&config.Vault.CaCert, "ca-cert", defaultEnvVar("VAULT_CACERT", ""), "Path to a PEM encoded CA cert file to use to verify the Vault server SSL certificate. (Overrides the VAULT_CACERT environment variable if set.)")
	flag.StringVar(&config.Vault.CaPath, "ca-path", defaultEnvVar("VAULT_CAPATH", ""), "Path to a directory of PEM encoded CA cert files to verify the Vault server SSL certificate. (Overrides the VAULT_CAPATH environment variable if set.)")

	flag.StringVar(&config.CubbyAuth.TempToken, "cubby-token", defaultEnvVar("CUBBY_TOKEN", ""), "Temporary vault authorization token that has a cubbyhole secret in CUBBY_PATH that contains the permanent vault token.")
	flag.StringVar(&config.CubbyAuth.Path, "cubby-path", defaultEnvVar("CUBBY_PATH", "/vault-token"), "Path to key in cubbyhole. By default this is /vault-token.")

	flag.StringVar(&config.AppIdAuth.AppId, "auth-appid", defaultEnvVar("APP_ID", ""), "Vault App Id for authenication. (Overrides the APP_ID environment variable if set.)")
	flag.StringVar(&config.AppIdAuth.UserIdMethod, "auth-userid-method", defaultEnvVar("USER_ID_METHOD", ""), "Vault User Id authenication method (either 'mac' or 'file'). (Overrides the USER_ID_METHOD environment variable if set.)")
	flag.StringVar(&config.AppIdAuth.UserIdInterface, "auth-userid-interface", defaultEnvVar("USER_ID_INTERFACE", ""), "Network interface for 'mac' user id authenication method. (Overrides the USER_ID_INTERFACE environment variable if set.)")
	flag.StringVar(&config.AppIdAuth.UserIdPath, "auth-userid-path", defaultEnvVar("USER_ID_PATH", ""), "File path for 'file' user id authenication method. (Overrides the USER_ID_PATH environment variable if set.)")
	flag.StringVar(&config.AppIdAuth.UserIdHash, "auth-userid-hash", defaultEnvVar("USER_ID_HASH", ""), "Hash the user id with the following algorithim (sha256, sha1, md5). The hex representation of the hash will be used. (Overrides the USER_ID_HASH environment variable if set.)")
	flag.StringVar(&config.AppIdAuth.UserIdSalt, "auth-userid-salt", defaultEnvVar("USER_ID_SALT", ""), "If hashing, salt the hash in the format 'salt$user_id'. (Overrides the USER_ID_SALT environment variable if set.)")

	if d, err := time.ParseDuration(defaultEnvVar("TASK_LIFE", "2m")); err == nil {
		flag.DurationVar(&config.MaxTaskLife, "task-life", d, "The maximum amount of time that a task can be alive during which it can ask for a authorization token.")
	} else {
		panic(d)
	}
}

func renew(token string, ttl int) error {
	r, err := goreq.Request{
		Uri: vaultPath("/v1/auth/token/renew-self", ""),
		Body: struct {
			Increment int `json:"increment"`
		}{ttl},
		Method: "POST",
	}.WithHeader("X-Vault-Token", token).Do()
	if err == nil {
		defer r.Body.Close()
		switch r.StatusCode {
		case 200:
			return nil
		default:
			var e vaultError
			e.Code = r.StatusCode
			if err := r.Body.FromJsonTo(&e); err == nil {
				return e
			} else {
				e.Errors = []string{"communication error."}
				return e
			}
		}
	} else {
		return err
	}
}

func renew_worker(token string, onUnsealed <-chan struct{}) {
	for {
		r, err := goreq.Request{
			Uri: vaultPath("/v1/auth/token/lookup-self", ""),
		}.WithHeader("X-Vault-Token", token).Do()
		if err == nil {
			defer r.Body.Close()
			if r.StatusCode == 200 {
				var tokenInfo struct {
					Data struct {
						Ttl         int `json:"ttl"`
						CreationTtl int `json:"creation_ttl"`
					} `json:"data"`
				}
				if err := r.Body.FromJsonTo(&tokenInfo); err == nil {
					if tokenInfo.Data.CreationTtl == 0 {
						log.Println("Token has Creation TTL of 0. No need for renew.")
						return
					}
					if tokenInfo.Data.Ttl > 5 {
						tokenInfo.Data.Ttl -= 5
					}
					select {
					case <-time.After(time.Duration(tokenInfo.Data.Ttl) * time.Second):
						log.Printf("Renewing token with ttl of %v.", time.Duration(tokenInfo.Data.CreationTtl)*time.Second)
						if err := renew(token, tokenInfo.Data.CreationTtl); err == nil {
							log.Printf("Renewed token with ttl of %v.", time.Duration(tokenInfo.Data.CreationTtl)*time.Second)
						} else {
							log.Println("Failed to renew token. Sealing gatekeeper.")
							seal()
							return
						}
					case <-onUnsealed:
						return
					}
				} else {
					log.Printf("Failed to unmarshal token. Not starting renewal watcher. Error: %s ", err)
					return
				}
			} else if r.StatusCode == 403 {
				log.Println("Token is no longer valid. Sealing gatekeeper.")
				seal()
				return
			} else {
				log.Printf("Failed to lookup token. Error Code: %d", r.StatusCode)
				return
			}
		} else {
			log.Printf("Failed to lookup token. Not starting renewal watcher. Error: %s ", err)
			return
		}
	}
}

func unseal(unsealer Unsealer) error {
	state.Lock()
	defer state.Unlock()
	if state.Status == StatusUnsealed {
		return errAlreadyUnsealed
	}
	if token, err := unsealer.Token(); err == nil {
		if err := (&activePolicies).Load(token); err != nil {
			log.Printf("Failed to load policies: %v", err)
			return err
		}
		log.Printf("The gate has been unsealed with method '%s'.", unsealer.Name())
		state.Token = token
		state.Status = StatusUnsealed
		state.OnSealed = make(chan struct{})
		go renew_worker(token, state.OnSealed)
		return nil
	} else {
		return err
	}
}

func seal() error {
	state.Lock()
	defer state.Unlock()
	if state.Status == StatusUnsealed {
		log.Println("The gate has been sealed.")
		close(state.OnSealed)
	}
	state.OnSealed = nil
	state.Token = ""
	state.Status = StatusSealed
	return nil
}

func vaultPath(path string, query string) string {
	u, _ := url.Parse(config.Vault.Server)
	u.Path = path
	u.RawQuery = query
	return u.String()
}

func intro() {
	fmt.Println(" __")
	fmt.Println("/__ _ _|_ _ |/  _  _ |_) _  __")
	fmt.Println("\\_|(_| |_(/_|\\ (/_(/_|  (/_ |")
	fmt.Println("github.com/channelmeter/vault-gatekeeper-mesos")
	fmt.Println("Version: " + gitNearestTag)
}

func main() {
	state.Status = StatusSealed
	state.Started = time.Now()
	flag.Parse()

	intro()

	if config.Vault.Insecure || config.Vault.CaPath != "" || config.Vault.CaCert != "" {
		tr := &http.Transport{
			Dial:            goreq.DefaultDialer.Dial,
			Proxy:           http.ProxyFromEnvironment,
			TLSClientConfig: &tls.Config{},
		}
		if config.Vault.Insecure {
			tr.TLSClientConfig.InsecureSkipVerify = true
		}

		if config.Vault.CaPath != "" || config.Vault.CaCert != "" {
			LoadCA := func() (*x509.CertPool, error) {
				if config.Vault.CaPath != "" {
					return gatekeeper.LoadCAPath(config.Vault.CaPath)
				} else if config.Vault.CaCert != "" {
					return gatekeeper.LoadCACert(config.Vault.CaCert)
				}
				panic("invariant violation")
			}
			if certs, err := LoadCA(); err == nil {
				tr.TLSClientConfig.RootCAs = certs
			} else {
				log.Printf("Failed to read client certs.")
				log.Println("Error:", err)
				os.Exit(1)
			}
		}
		// TODO: Fallback to regular client when communicating with Mesos Master
		goreq.DefaultTransport = tr
		goreq.DefaultClient = &http.Client{Transport: goreq.DefaultTransport}
	}

	r := gin.Default()
	r.SetHTMLTemplate(statusPage)
	r.GET("/", Status)
	r.GET("/status.json", Status)
	r.POST("/seal", Seal)
	r.POST("/unseal", Unseal)
	r.POST("/token", Provide)

	if os.Getenv("VAULT_TOKEN") != "" {
		log.Println("VAULT_TOKEN detected in environment, unsealing with token...")
		if err := unseal(TokenUnsealer{os.Getenv("VAULT_TOKEN")}); err != nil {
			log.Println("Failed to unseal using VAULT_TOKEN. Either unset VAULT_TOKEN or provide a valid VAULT_TOKEN.")
			log.Println("Error:", err)
			os.Exit(1)
		}
		log.Println("Unseal successful with token provided in VAULT_TOKEN.")
	} else if config.CubbyAuth.TempToken != "" {
		log.Println("Attempting to unseal with provided Cubbyhole token...")
		if err := unseal(config.CubbyAuth); err != nil {
			log.Println("Failed to unseal using Cubbyhole. Please make sure the Cubbyhole auth is correctly setup.")
			log.Println("Error:", err)
			os.Exit(1)
		}
		log.Println("Unseal successful with token provided by Cubbyhole.")
	} else if config.AppIdAuth.AppId != "" {
		log.Println("Attempting to unseal with provided APP ID credentials, using user_id from '" + config.AppIdAuth.UserIdMethod + "'...")
		if err := unseal(config.AppIdAuth); err != nil {
			log.Println("Failed to unseal using APP ID credentials. Provide a valid APP ID credentials.")
			log.Println("Error:", err)
			os.Exit(1)
		}
		log.Println("Unseal successful with app-id credentials.")
	}
	log.Printf("Listening and serving on '%s'...", config.ListenAddress)
	r.Run(config.ListenAddress)
}
<commit_msg>Server: Add TLS_CERT and TLS_KEY options.<commit_after>package main

import (
	"crypto/tls"
	"crypto/x509"
	"errors"
	"flag"
	"fmt"
	"github.com/channelmeter/vault-gatekeeper-mesos/gatekeeper"
	"github.com/franela/goreq"
	"github.com/gin-gonic/gin"
	"log"
	"net/http"
	"net/url"
	"os"
	"strconv"
	"sync"
	"time"
)

type GkStatus string

const (
	StatusSealed   GkStatus = "Sealed"
	StatusUnsealed GkStatus = "Unsealed"
)

var gitNearestTag = "dev"

var config struct {
	Vault struct {
		Server     string
		Insecure   bool
		CaCert     string
		CaPath     string
		GkPolicies string
	}
	ListenAddress string
	TlsCert       string
	TlsKey        string
	Mesos         string
	MaxTaskLife   time.Duration
	AppIdAuth     AppIdUnsealer
	CubbyAuth     CubbyUnsealer
}

var state struct {
	Status GkStatus `json:"status"`
	Stats  struct {
		Requests   int32 `json:"requests"`
		Successful int32 `json:"successful"`
		Denied     int32 `json:"denied"`
	} `json:"stats"`
	Started  time.Time     `json:"started"`
	Token    string        `json:"-"`
	OnSealed chan struct{} `json:"-"`
	sync.RWMutex
}

var errAlreadyUnsealed = errors.New("Already unsealed.")
var errUnknownAuthMethod = errors.New("Unknown method for authorization.")

func defaultEnvVar(key string, def string) (val string) {
	val = os.Getenv(key)
	if val == "" {
		val = def
	}
	return
}

func init() {
	flag.StringVar(&config.ListenAddress, "listen", defaultEnvVar("LISTEN_ADDR", ":9201"), "Hostname and port to listen on. (Overrides the LISTEN_ADDR environment variable if set.)")
	flag.StringVar(&config.TlsCert, "tls-cert", defaultEnvVar("TLS_CERT", ""), "Path to TLS certificate. If this value is set, gatekeeper will be served over TLS.")
	flag.StringVar(&config.TlsKey, "tls-cert", defaultEnvVar("TLS_KEY", ""), "Path to TLS key. If this value is set, gatekeeper will be served over TLS.")

	flag.StringVar(&config.Mesos, "mesos", defaultEnvVar("MESOS_MASTER", ""), "Address to mesos master. (Overrides the MESOS_MASTER environment variable if set.)")

	flag.StringVar(&config.Vault.Server, "vault", defaultEnvVar("VAULT_ADDR", ""), "Address to vault server. (Overrides the VAULT_ADDR environment variable if set.)")
	flag.StringVar(&config.Vault.GkPolicies, "policies", defaultEnvVar("GATE_POLICIES", "/gatekeeper"), "Path to the json formatted policies configuration file on the vault generic backend.")
	flag.BoolVar(&config.Vault.Insecure, "tls-skip-verify", func() bool {
		b, err := strconv.ParseBool(defaultEnvVar("VAULT_SKIP_VERIFY", "0"))
		return err == nil && b
	}(), "Do not verify TLS certificate. (Overrides the VAULT_SKIP_VERIFY environment variable if set.)")
	flag.StringVar(&config.Vault.CaCert, "ca-cert", defaultEnvVar("VAULT_CACERT", ""), "Path to a PEM encoded CA cert file to use to verify the Vault server SSL certificate. (Overrides the VAULT_CACERT environment variable if set.)")
	flag.StringVar(&config.Vault.CaPath, "ca-path", defaultEnvVar("VAULT_CAPATH", ""), "Path to a directory of PEM encoded CA cert files to verify the Vault server SSL certificate. (Overrides the VAULT_CAPATH environment variable if set.)")

	flag.StringVar(&config.CubbyAuth.TempToken, "cubby-token", defaultEnvVar("CUBBY_TOKEN", ""), "Temporary vault authorization token that has a cubbyhole secret in CUBBY_PATH that contains the permanent vault token.")
	flag.StringVar(&config.CubbyAuth.Path, "cubby-path", defaultEnvVar("CUBBY_PATH", "/vault-token"), "Path to key in cubbyhole. By default this is /vault-token.")

	flag.StringVar(&config.AppIdAuth.AppId, "auth-appid", defaultEnvVar("APP_ID", ""), "Vault App Id for authenication. (Overrides the APP_ID environment variable if set.)")
	flag.StringVar(&config.AppIdAuth.UserIdMethod, "auth-userid-method", defaultEnvVar("USER_ID_METHOD", ""), "Vault User Id authenication method (either 'mac' or 'file'). (Overrides the USER_ID_METHOD environment variable if set.)")
	flag.StringVar(&config.AppIdAuth.UserIdInterface, "auth-userid-interface", defaultEnvVar("USER_ID_INTERFACE", ""), "Network interface for 'mac' user id authenication method. (Overrides the USER_ID_INTERFACE environment variable if set.)")
	flag.StringVar(&config.AppIdAuth.UserIdPath, "auth-userid-path", defaultEnvVar("USER_ID_PATH", ""), "File path for 'file' user id authenication method. (Overrides the USER_ID_PATH environment variable if set.)")
	flag.StringVar(&config.AppIdAuth.UserIdHash, "auth-userid-hash", defaultEnvVar("USER_ID_HASH", ""), "Hash the user id with the following algorithim (sha256, sha1, md5). The hex representation of the hash will be used. (Overrides the USER_ID_HASH environment variable if set.)")
	flag.StringVar(&config.AppIdAuth.UserIdSalt, "auth-userid-salt", defaultEnvVar("USER_ID_SALT", ""), "If hashing, salt the hash in the format 'salt$user_id'. (Overrides the USER_ID_SALT environment variable if set.)")

	if d, err := time.ParseDuration(defaultEnvVar("TASK_LIFE", "2m")); err == nil {
		flag.DurationVar(&config.MaxTaskLife, "task-life", d, "The maximum amount of time that a task can be alive during which it can ask for a authorization token.")
	} else {
		panic(d)
	}
}

func renew(token string, ttl int) error {
	r, err := goreq.Request{
		Uri: vaultPath("/v1/auth/token/renew-self", ""),
		Body: struct {
			Increment int `json:"increment"`
		}{ttl},
		Method: "POST",
	}.WithHeader("X-Vault-Token", token).Do()
	if err == nil {
		defer r.Body.Close()
		switch r.StatusCode {
		case 200:
			return nil
		default:
			var e vaultError
			e.Code = r.StatusCode
			if err := r.Body.FromJsonTo(&e); err == nil {
				return e
			} else {
				e.Errors = []string{"communication error."}
				return e
			}
		}
	} else {
		return err
	}
}

func renew_worker(token string, onUnsealed <-chan struct{}) {
	for {
		r, err := goreq.Request{
			Uri: vaultPath("/v1/auth/token/lookup-self", ""),
		}.WithHeader("X-Vault-Token", token).Do()
		if err == nil {
			defer r.Body.Close()
			if r.StatusCode == 200 {
				var tokenInfo struct {
					Data struct {
						Ttl         int `json:"ttl"`
						CreationTtl int `json:"creation_ttl"`
					} `json:"data"`
				}
				if err := r.Body.FromJsonTo(&tokenInfo); err == nil {
					if tokenInfo.Data.CreationTtl == 0 {
						log.Println("Token has Creation TTL of 0. No need for renew.")
						return
					}
					if tokenInfo.Data.Ttl > 5 {
						tokenInfo.Data.Ttl -= 5
					}
					select {
					case <-time.After(time.Duration(tokenInfo.Data.Ttl) * time.Second):
						log.Printf("Renewing token with ttl of %v.", time.Duration(tokenInfo.Data.CreationTtl)*time.Second)
						if err := renew(token, tokenInfo.Data.CreationTtl); err == nil {
							log.Printf("Renewed token with ttl of %v.", time.Duration(tokenInfo.Data.CreationTtl)*time.Second)
						} else {
							log.Println("Failed to renew token. Sealing gatekeeper.")
							seal()
							return
						}
					case <-onUnsealed:
						return
					}
				} else {
					log.Printf("Failed to unmarshal token. Not starting renewal watcher. Error: %s ", err)
					return
				}
			} else if r.StatusCode == 403 {
				log.Println("Token is no longer valid. Sealing gatekeeper.")
				seal()
				return
			} else {
				log.Printf("Failed to lookup token. Error Code: %d", r.StatusCode)
				return
			}
		} else {
			log.Printf("Failed to lookup token. Not starting renewal watcher. Error: %s ", err)
			return
		}
	}
}

func unseal(unsealer Unsealer) error {
	state.Lock()
	defer state.Unlock()
	if state.Status == StatusUnsealed {
		return errAlreadyUnsealed
	}
	if token, err := unsealer.Token(); err == nil {
		if err := (&activePolicies).Load(token); err != nil {
			log.Printf("Failed to load policies: %v", err)
			return err
		}
		log.Printf("The gate has been unsealed with method '%s'.", unsealer.Name())
		state.Token = token
		state.Status = StatusUnsealed
		state.OnSealed = make(chan struct{})
		go renew_worker(token, state.OnSealed)
		return nil
	} else {
		return err
	}
}

func seal() error {
	state.Lock()
	defer state.Unlock()
	if state.Status == StatusUnsealed {
		log.Println("The gate has been sealed.")
		close(state.OnSealed)
	}
	state.OnSealed = nil
	state.Token = ""
	state.Status = StatusSealed
	return nil
}

func vaultPath(path string, query string) string {
	u, _ := url.Parse(config.Vault.Server)
	u.Path = path
	u.RawQuery = query
	return u.String()
}

func intro() {
	fmt.Println(" __")
	fmt.Println("/__ _ _|_ _ |/  _  _ |_) _  __")
	fmt.Println("\\_|(_| |_(/_|\\ (/_(/_|  (/_ |")
	fmt.Println("github.com/channelmeter/vault-gatekeeper-mesos")
	fmt.Println("Version: " + gitNearestTag)
}

func main() {
	state.Status = StatusSealed
	state.Started = time.Now()
	flag.Parse()

	intro()

	if config.Vault.Insecure || config.Vault.CaPath != "" || config.Vault.CaCert != "" {
		tr := &http.Transport{
			Dial:            goreq.DefaultDialer.Dial,
			Proxy:           http.ProxyFromEnvironment,
			TLSClientConfig: &tls.Config{},
		}
		if config.Vault.Insecure {
			tr.TLSClientConfig.InsecureSkipVerify = true
		}

		if config.Vault.CaPath != "" || config.Vault.CaCert != "" {
			LoadCA := func() (*x509.CertPool, error) {
				if config.Vault.CaPath != "" {
					return gatekeeper.LoadCAPath(config.Vault.CaPath)
				} else if config.Vault.CaCert != "" {
					return gatekeeper.LoadCACert(config.Vault.CaCert)
				}
				panic("invariant violation")
			}
			if certs, err := LoadCA(); err == nil {
				tr.TLSClientConfig.RootCAs = certs
			} else {
				log.Printf("Failed to read client certs.")
				log.Println("Error:", err)
				os.Exit(1)
			}
		}
		// TODO: Fallback to regular client when communicating with Mesos Master
		goreq.DefaultTransport = tr
		goreq.DefaultClient = &http.Client{Transport: goreq.DefaultTransport}
	}

	r := gin.Default()
	r.SetHTMLTemplate(statusPage)
	r.GET("/", Status)
	r.GET("/status.json", Status)
	r.POST("/seal", Seal)
	r.POST("/unseal", Unseal)
	r.POST("/token", Provide)

	if os.Getenv("VAULT_TOKEN") != "" {
		log.Println("VAULT_TOKEN detected in environment, unsealing with token...")
		if err := unseal(TokenUnsealer{os.Getenv("VAULT_TOKEN")}); err != nil {
			log.Println("Failed to unseal using VAULT_TOKEN. Either unset VAULT_TOKEN or provide a valid VAULT_TOKEN.")
			log.Println("Error:", err)
			os.Exit(1)
		}
		log.Println("Unseal successful with token provided in VAULT_TOKEN.")
	} else if config.CubbyAuth.TempToken != "" {
		log.Println("Attempting to unseal with provided Cubbyhole token...")
		if err := unseal(config.CubbyAuth); err != nil {
			log.Println("Failed to unseal using Cubbyhole. Please make sure the Cubbyhole auth is correctly setup.")
			log.Println("Error:", err)
			os.Exit(1)
		}
		log.Println("Unseal successful with token provided by Cubbyhole.")
	} else if config.AppIdAuth.AppId != "" {
		log.Println("Attempting to unseal with provided APP ID credentials, using user_id from '" + config.AppIdAuth.UserIdMethod + "'...")
		if err := unseal(config.AppIdAuth); err != nil {
			log.Println("Failed to unseal using APP ID credentials. Provide a valid APP ID credentials.")
			log.Println("Error:", err)
			os.Exit(1)
		}
		log.Println("Unseal successful with app-id credentials.")
	}
	log.Printf("Listening and serving on '%s'...", config.ListenAddress)

	runFunc := func() error {
		return r.Run(config.ListenAddress)
	}
	if config.TlsCert != "" || config.TlsKey != "" {
		runFunc = func() error {
			return r.RunTLS(config.ListenAddress, config.TlsCert, config.TlsKey)
		}
	}
	if err := runFunc(); err != nil {
		log.Println("Failed to start server. Error: " + err.Error())
		os.Exit(1)
	}
}
<|endoftext|>


<commit_before>package main

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

	"github.com/nlopes/slack"
	"github.com/smarx/go-cowsay"
)

const maxRemainingCount = 240

type Item struct {
	actions        map[string]bool
	remainingCount int
	text           string
	channel        string
	timestamp      string
	currentText    string
}

func Process(item *Item, api *slack.Client) {
	text := item.text
	if item.actions["marquee"] {
		text += "     "
		howMuch := (maxRemainingCount - item.remainingCount) % len(text)
		text = text[howMuch:] + text[:howMuch]
	}
	if item.actions["blink"] {
		if item.remainingCount%2 == 0 {
			text = strings.Repeat(" ", len(text))
		}
	}
	if item.actions["cow"] {
		text = "```\n" + cowsay.Format(text) + "\n```"
	} else if item.actions["blink"] || item.actions["marquee"] {
		text = "```" + text + "```"
	}
	item.remainingCount -= 1
	if item.currentText == text {
		item.remainingCount = 0
	}
	item.currentText = text
	go api.UpdateMessage(item.channel, item.timestamp, text)
}

func ProcessForever(incoming chan *Item, api *slack.Client) {
	ticker := time.Tick(250 * time.Millisecond)
	items := make([]*Item, 0, 10)
	for {
		select {
		case <-ticker:
			newItems := make([]*Item, 0, len(items))
			for i := range items {
				Process(items[i], api)
				if items[i].remainingCount > 0 {
					newItems = append(newItems, items[i])
				}
			}
			items = newItems
		case item := <-incoming:
			items = append(items, item)
		}
	}
}

func FindTags(text string) (tags map[string]bool, stripped string) {
	tags = make(map[string]bool)
	stripped = text
	var possible = []string{"blink", "marquee", "important", "cow"}

	for i := range possible {
		var tag = possible[i]
		var re = regexp.MustCompile(fmt.Sprintf("<%[1]s>(.*)</%[1]s>", tag))
		var newtext = re.ReplaceAllString(stripped, "$1")
		if newtext != stripped {
			tags[tag] = true
			stripped = newtext
		}
	}
	return
}

func main() {
	api := slack.New(os.Getenv("TOKEN"))

	itemc := make(chan *Item)
	go ProcessForever(itemc, api)

	response, err := api.AuthTest()
	if err != nil {
		log.Fatal(err)
	}

	UserID := response.UserID

	var important *Item
	ackID := -1

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

Loop:
	for {
		select {
		case msg := <-rtm.IncomingEvents:
			switch ev := msg.Data.(type) {
			case *slack.AckMessage:
				if ev.ReplyTo == ackID {
					important.timestamp = ev.Timestamp
					ackID = -1
				}
			case *slack.MessageEvent:
				if len(ev.SubType) > 0 {
					continue
				}
				if important != nil {
					message := rtm.NewOutgoingMessage(important.currentText, important.channel)
					ackID = message.ID
					rtm.SendMessage(message)
					go api.DeleteMessage(important.channel, important.timestamp)
				}
				if ev.User != UserID {
					continue
				}
				tags, text := FindTags(ev.Text)
				if len(tags) > 0 {
					item := Item{tags, maxRemainingCount, text, ev.Channel, ev.Timestamp, text}
					if item.actions["important"] {
						important = &item
					}
					if (item.actions["cow"] || item.actions["important"]) && len(item.actions) == 1 {
						item.remainingCount = 1
					}
					if len(item.actions) > 0 {
						itemc <- &item
					}
				}
			case *slack.InvalidAuthEvent:
				fmt.Println("Invalid credentials")
				break Loop
			default:
				// ignore everything else
			}
		}
	}
}
<commit_msg>Go style fixups, more user-friendliness<commit_after>package main

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

	"github.com/nlopes/slack"
	"github.com/smarx/go-cowsay"
)

const maxRemainingCount = 240

type Item struct {
	Actions        map[string]bool
	RemainingCount int
	Text           string
	Channel        string
	Timestamp      string
	CurrentText    string
}

func Process(item *Item, api *slack.Client) {
	text := item.Text
	if item.Actions["marquee"] {
		text += "     "
		runes := []rune(text)
		howMuch := (maxRemainingCount - item.RemainingCount) % len(runes)
		text = string(runes[howMuch:]) + string(runes[:howMuch])
	}
	if item.Actions["blink"] {
		if item.RemainingCount%2 == 0 {
			text = strings.Repeat(" ", len(text))
		}
	}
	if item.Actions["cow"] {
		text = "```\n" + cowsay.Format(text) + "\n```"
	} else if item.Actions["blink"] || item.Actions["marquee"] {
		text = "```" + text + "```"
	}
	item.RemainingCount -= 1
	if item.CurrentText == text {
		item.RemainingCount = 0
	}
	item.CurrentText = text
	go api.UpdateMessage(item.Channel, item.Timestamp, text)
}

func ProcessForever(incoming chan *Item, api *slack.Client) {
	ticker := time.Tick(250 * time.Millisecond)
	items := make([]*Item, 0, 10)
	for {
		select {
		case <-ticker:
			newItems := make([]*Item, 0, len(items))
			for i := range items {
				Process(items[i], api)
				if items[i].RemainingCount > 0 {
					newItems = append(newItems, items[i])
				}
			}
			items = newItems
		case item := <-incoming:
			items = append(items, item)
		}
	}
}

func FindTags(text string) (tags map[string]bool, stripped string) {
	tags = make(map[string]bool)
	stripped = text
	var possible = []string{"blink", "marquee", "important", "cow"}

	for i := range possible {
		var tag = possible[i]
		var re = regexp.MustCompile(fmt.Sprintf("<%[1]s>(.*)</%[1]s>", tag))
		var newtext = re.ReplaceAllString(stripped, "$1")
		if newtext != stripped {
			tags[tag] = true
			stripped = newtext
		}
	}
	return
}

func main() {
	token := os.Getenv("TOKEN")

	if len(token) == 0 {
		fmt.Println("Missing auth token. Please visit https://api.slack.com/docs/oauth-test-tokens and get a token for your account. Then set the TOKEN environment variable to that value.")
		return
	}

	api := slack.New(token)

	itemc := make(chan *Item)
	go ProcessForever(itemc, api)

	response, err := api.AuthTest()
	if err != nil {
		log.Fatal(err)
	}

	UserID := response.UserID

	var important *Item
	ackID := -1

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

Loop:
	for {
		select {
		case msg := <-rtm.IncomingEvents:
			switch ev := msg.Data.(type) {
			case *slack.ConnectedEvent:
				fmt.Println("Enhanced! You can now use <blink>, <marquee>, and <cow> to annoy your coworkers.")
			case *slack.AckMessage:
				if ev.ReplyTo == ackID {
					important.Timestamp = ev.Timestamp
					ackID = -1
				}
			case *slack.MessageEvent:
				if len(ev.SubType) > 0 {
					continue
				}
				if important != nil {
					message := rtm.NewOutgoingMessage(important.CurrentText, important.Channel)
					ackID = message.ID
					rtm.SendMessage(message)
					go api.DeleteMessage(important.Channel, important.Timestamp)
				}
				if ev.User != UserID {
					continue
				}
				tags, text := FindTags(ev.Text)
				if len(tags) > 0 {
					item := Item{
						Actions:        tags,
						RemainingCount: maxRemainingCount,
						Text:           text,
						Channel:        ev.Channel,
						Timestamp:      ev.Timestamp,
						CurrentText:    text}
					if item.Actions["important"] {
						important = &item
					}
					if (item.Actions["cow"] || item.Actions["important"]) && len(item.Actions) == 1 {
						item.RemainingCount = 1
					}
					if len(item.Actions) > 0 {
						itemc <- &item
					}
				}
			case *slack.InvalidAuthEvent:
				fmt.Println("Invalid credentials")
				break Loop
			default:
				// ignore everything else
			}
		}
	}
}
<|endoftext|>


<commit_before>// Package gcnotifier provides a way to receive notifications after every time
// garbage collection (GC) runs. This can be useful to instruct your code to
// free additional memory resources that you may be using. To minimize the load
// on the GC the code that runs after receiving the notification should try to
// avoid allocations as much as possible.
package gcnotifier

import "runtime"

type sentinel struct {
	gcCh chan struct{}
}

// AfterGC returns a channel that will receive a notification after every GC
// run. If a notification is not consumed before another GC runs only one of the
// two notifications is sent. To stop the notifications you can safely close the
// channel.
func AfterGC() <-chan struct{} {
	s := &sentinel{gcCh: make(chan struct{})}
	runtime.SetFinalizer(s, finalizer)
	return s.gcCh
}

func finalizer(obj interface{}) {
	defer recover() // writing to a closed channel will panic
	s := obj.(*sentinel)
	select {
	case s.gcCh <- struct{}{}:
	default:
	}
	// we get here only if the channel was not closed
	runtime.SetFinalizer(s, finalizer)
}

// AfterGCUntilReachable is like AfterGC, but the channel will be closed when
// the object supplied as argument is garbage collected. No finalizer should be
// set on the object before or after calling this method. Pay attention to not
// inadvertently keep the object alive (e.g. by referencing it in a callback or
// goroutine) or the object may never be collected.
func AfterGCUntilCollected(obj interface{}) <-chan struct {
  gcCh := AfterGC()
	runtime.SetFinalizer(obj, func() { close(goCh) })
	return goCh
}
<commit_msg>Remove AfterGCUntilCollected and fix docs<commit_after>// Package gcnotifier provides a way to receive notifications after every time
// garbage collection (GC) runs. This can be useful to instruct your code to
// free additional memory resources that you may be using.
//
// To minimize the load on the GC the code that runs after receiving the
// notification should try to avoid allocations as much as possible, or at the
// very least that the amount of new memory allocated is significantly smaller
// than the amount of memory that has been "freed" by your code.

package gcnotifier

import "runtime"

type sentinel struct {
	gcCh chan struct{}
}

// AfterGC returns a channel that will receive a notification after every GC
// run. If a notification is not consumed before another GC runs only one of the
// two notifications is sent. To stop the notifications you can safely close the
// channel.
//
// A common use case for this is when you have a custom pool of objects: instead
// of setting a maximum size you can leave it unbounded and then drop all or
// some of them after every GC run (e.g. sync.Pool drops all objects during GC).
func AfterGC() chan struct{} {
	s := &sentinel{gcCh: make(chan struct{})}
	runtime.SetFinalizer(s, finalizer)
	return s.gcCh
}

func finalizer(obj interface{}) {
	defer recover() // writing to a closed channel will panic
	s := obj.(*sentinel)
	select {
	case s.gcCh <- struct{}{}:
	default:
	}
	// we get here only if the channel was not closed
	runtime.SetFinalizer(s, finalizer)
}
<|endoftext|>


<commit_before>package envetcd

import (
	"bytes"
	"errors"
	"net/url"
	"os"
	"regexp"
	"strconv"
	"strings"
	"text/template"

	"github.com/coreos/etcd/pkg/transport"
	"github.com/coreos/go-etcd/etcd"
)

// order of precedence:
// global < system < service < host

// Regexp for invalid characters in keys
var invalidRegexp = regexp.MustCompile(`[^a-zA-Z0-9_]`)

// Config contains all of the parameters needed to run GetKeyPairs
type Config struct {
	Peers    []string
	Sync     bool
	Sanitize bool
	Upcase   bool
	Prefix   string
	System   string
	Service  string
	Hostname string
	TLS      *transport.TLSInfo
}

var etcdKeyTemplates = [...]string{
	"{{.Prefix}}/global",
	"{{if .System}}{{.Prefix}}/system/{{.System}}{{end}}",
	"{{if .Service}}{{.Prefix}}/service/{{.Service}}{{end}}",
	"{{if .Hostname}}{{.Prefix}}/host/{{.Hostname}}{{end}}",
}

// KeyPairs is a slice of KeyPair pointers
type KeyPairs map[string]string

func massagePeers(config *Config) error {
	for i, ep := range config.Peers {

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

		if u.Scheme == "" {
			u.Scheme = "http"
		}

		config.Peers[i] = u.String()
	}

	return nil
}

func getClient(config *Config) (*etcd.Client, error) {
	if config == nil {
		return nil, errors.New("config is nil")
	}

	if err := massagePeers(config); err != nil {
		return nil, err
	}

	client := etcd.NewClient(config.Peers)

	if config.TLS != nil {
		tr, err := transport.NewTransport(*config.TLS)
		if err != nil {
			return nil, err
		}

		client.SetTransport(tr)
	}

	// Sync cluster.
	if config.Sync {
		if ok := client.SyncCluster(); !ok {
			return nil, errors.New("cannot sync with the cluster using endpoints \"" + strings.Join(config.Peers, "\", \"") + "\"")
		}
	}

	return client, nil
}

// Set modifies the current environment with variables retrieved from etcd. Set
// will not overwrite existing variables.
// The only required environment variable is $ETCD_ENDPOINT.
// $ETCD_ENDPOINT should look like "http://127.0.0.1:401".
// service should be set by the application calling Set and not derived from
// an environment variable.
// Set will also use some other environment variables if they exist.
// $ETCD_PREFIX defaults to "/config"
// $HOSTNAME will be honored if it is set.
func Set(service string) error {
	etcdEndpoint := os.Getenv("ETCD_ENDPOINT")
	if len(etcdEndpoint) == 0 {
		return nil
	}

	config := &Config{
		Peers:    []string{etcdEndpoint},
		Sync:     true,
		Sanitize: true,
		Upcase:   true,
		Prefix:   os.Getenv("ETCD_PREFIX"),
		Service:  service,
		Hostname: os.Getenv("HOSTNAME"),
	}

	if len(config.Peers[0]) == 0 {
		config.Peers[0] = "http://127.0.0.1:4001"
	}

	if len(config.Prefix) == 0 {
		config.Prefix = "/config"
	}

	keyPairs, err := GetKeyPairs(config)
	if err != nil {
		return err
	}

	for key, value := range keyPairs {
		if len(os.Getenv(key)) == 0 {
			os.Setenv(key, value)
		}
	}

	return nil
}

// GetKeyPairs takes a given config and client, and returns all key pairs
func GetKeyPairs(config *Config) (KeyPairs, error) {
	const noSort = false
	const recursive = true

	if len(config.Service) > 0 && len(config.System) == 0 {
		if index := strings.Index(config.Service, "-"); index != -1 {
			config.System = config.Service[:index]
		}
	}

	client, err := getClient(config)
	if err != nil {
		return nil, err
	}

	keyPairs := make(KeyPairs)

	// For each etcd directory key template
	// fetch from etcd and add to keypairs
	for i, tmpl := range etcdKeyTemplates {
		t := template.Must(template.New("etcdKey" + strconv.Itoa(i)).Parse(tmpl))
		var buf bytes.Buffer
		err := t.Execute(&buf, config)
		if err != nil {
			continue
		}

		if buf.Len() == 0 {
			continue
		}

		dir := buf.String()

		resp, err := client.Get(dir, noSort, recursive)
		if err != nil {
			continue
		}

		addKeyPair(config, keyPairs, dir, resp.Node)
	}

	// Expose --service --system and --hostname command variables to the child process' emvironment,
	// ignoring if these are not set in etcd (or set in etcd. Command line arguments take precedence.)
	if "" != config.Service {
		keyPairs["ENVETCD_SERVICE"] = config.Service
	}

	if "" != config.System {
		keyPairs["ENVETCD_SYSTEM"] = config.System
	}

	if "" != config.Hostname {
		keyPairs["ENVETCD_HOSTNAME"] = config.Hostname
	}

	return keyPairs, nil
}

func addKeyPair(config *Config, keyPairs KeyPairs, dir string, node *etcd.Node) {
	if node.Dir {
		for _, child := range node.Nodes {
			addKeyPair(config, keyPairs, dir, child)
		}
		return
	}

	key := strings.TrimPrefix(node.Key, dir) // strip the prefix directory from the key
	if key == "" {
		return
	}

	key = strings.TrimLeft(key, "/")         // strip any leading slashes
	key = strings.Replace(key, "/", "_", -1) // convert any remaining slashes to underscores (for any nested keys)

	if config.Sanitize {
		key = invalidRegexp.ReplaceAllString(key, "_")
	}

	if config.Upcase {
		key = strings.ToUpper(key)
	}

	keyPairs[key] = node.Value
}
<commit_msg>Use the getDefaultGateway function<commit_after>package envetcd

import (
	"bytes"
	"errors"
	"fmt"
	"net/url"
	"os"
	"regexp"
	"strconv"
	"strings"
	"text/template"

	"github.com/coreos/etcd/pkg/transport"
	"github.com/coreos/go-etcd/etcd"
)

// order of precedence:
// global < system < service < host

// Regexp for invalid characters in keys
var invalidRegexp = regexp.MustCompile(`[^a-zA-Z0-9_]`)

// Config contains all of the parameters needed to run GetKeyPairs
type Config struct {
	Peers    []string
	Sync     bool
	Sanitize bool
	Upcase   bool
	Prefix   string
	System   string
	Service  string
	Hostname string
	TLS      *transport.TLSInfo
}

var etcdKeyTemplates = [...]string{
	"{{.Prefix}}/global",
	"{{if .System}}{{.Prefix}}/system/{{.System}}{{end}}",
	"{{if .Service}}{{.Prefix}}/service/{{.Service}}{{end}}",
	"{{if .Hostname}}{{.Prefix}}/host/{{.Hostname}}{{end}}",
}

// KeyPairs is a slice of KeyPair pointers
type KeyPairs map[string]string

func massagePeers(config *Config) error {
	for i, ep := range config.Peers {

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

		if u.Scheme == "" {
			u.Scheme = "http"
		}

		config.Peers[i] = u.String()
	}

	return nil
}

func getClient(config *Config) (*etcd.Client, error) {
	if config == nil {
		return nil, errors.New("config is nil")
	}

	if err := massagePeers(config); err != nil {
		return nil, err
	}

	client := etcd.NewClient(config.Peers)

	if config.TLS != nil {
		tr, err := transport.NewTransport(*config.TLS)
		if err != nil {
			return nil, err
		}

		client.SetTransport(tr)
	}

	// Sync cluster.
	if config.Sync {
		if ok := client.SyncCluster(); !ok {
			return nil, errors.New("cannot sync with the cluster using endpoints \"" + strings.Join(config.Peers, "\", \"") + "\"")
		}
	}

	return client, nil
}

// Set modifies the current environment with variables retrieved from etcd. Set
// will not overwrite existing variables.
// The only required environment variable is $ETCD_ENDPOINT.
// $ETCD_ENDPOINT should look like "http://127.0.0.1:4001".
// service should be set by the application calling Set and not derived from
// an environment variable.
// Set will also use some other environment variables if they exist.
// $ETCD_PREFIX defaults to "/config"
// $HOSTNAME will be honored if it is set.
func Set(service string) error {
	etcdEndpoint := os.Getenv("ETCD_ENDPOINT")
	if len(etcdEndpoint) == 0 {
		// Look for the default gateway and use that.
		gatewayIP, err := getDefaultRouteGateway()
		if err != nil {
			return err
		}
		etcdEndpoint = fmt.Sprintf("http://%s:4001", gatewayIP.String())
	}

	config := &Config{
		Peers:    []string{etcdEndpoint},
		Sync:     true,
		Sanitize: true,
		Upcase:   true,
		Prefix:   os.Getenv("ETCD_PREFIX"),
		Service:  service,
		Hostname: os.Getenv("HOSTNAME"),
	}

	if len(config.Peers[0]) == 0 {
		config.Peers[0] = "http://127.0.0.1:4001"
	}

	if len(config.Prefix) == 0 {
		config.Prefix = "/config"
	}

	keyPairs, err := GetKeyPairs(config)
	if err != nil {
		return err
	}

	for key, value := range keyPairs {
		if len(os.Getenv(key)) == 0 {
			os.Setenv(key, value)
		}
	}

	return nil
}

// GetKeyPairs takes a given config and client, and returns all key pairs
func GetKeyPairs(config *Config) (KeyPairs, error) {
	const noSort = false
	const recursive = true

	if len(config.Service) > 0 && len(config.System) == 0 {
		if index := strings.Index(config.Service, "-"); index != -1 {
			config.System = config.Service[:index]
		}
	}

	client, err := getClient(config)
	if err != nil {
		return nil, err
	}

	keyPairs := make(KeyPairs)

	// For each etcd directory key template
	// fetch from etcd and add to keypairs
	for i, tmpl := range etcdKeyTemplates {
		t := template.Must(template.New("etcdKey" + strconv.Itoa(i)).Parse(tmpl))
		var buf bytes.Buffer
		err := t.Execute(&buf, config)
		if err != nil {
			continue
		}

		if buf.Len() == 0 {
			continue
		}

		dir := buf.String()

		resp, err := client.Get(dir, noSort, recursive)
		if err != nil {
			continue
		}

		addKeyPair(config, keyPairs, dir, resp.Node)
	}

	// Expose --service --system and --hostname command variables to the child process' emvironment,
	// ignoring if these are not set in etcd (or set in etcd. Command line arguments take precedence.)
	if "" != config.Service {
		keyPairs["ENVETCD_SERVICE"] = config.Service
	}

	if "" != config.System {
		keyPairs["ENVETCD_SYSTEM"] = config.System
	}

	if "" != config.Hostname {
		keyPairs["ENVETCD_HOSTNAME"] = config.Hostname
	}

	return keyPairs, nil
}

func addKeyPair(config *Config, keyPairs KeyPairs, dir string, node *etcd.Node) {
	if node.Dir {
		for _, child := range node.Nodes {
			addKeyPair(config, keyPairs, dir, child)
		}
		return
	}

	key := strings.TrimPrefix(node.Key, dir) // strip the prefix directory from the key
	if key == "" {
		return
	}

	key = strings.TrimLeft(key, "/")         // strip any leading slashes
	key = strings.Replace(key, "/", "_", -1) // convert any remaining slashes to underscores (for any nested keys)

	if config.Sanitize {
		key = invalidRegexp.ReplaceAllString(key, "_")
	}

	if config.Upcase {
		key = strings.ToUpper(key)
	}

	keyPairs[key] = node.Value
}
<|endoftext|>


<commit_before>// Copyright 2015 Google Inc. All Rights Reserved.
// Author: jacobsa@google.com (Aaron Jacobs)

package gcs

import (
	"io"
	"net/http"

	"golang.org/x/net/context"
	"google.golang.org/cloud"
	"google.golang.org/cloud/storage"
)

// Bucket represents a GCS bucket, pre-bound with a bucket name and necessary
// authorization information.
//
// Each method that may block accepts a context object that is used for
// deadlines and cancellation. Users need not package authorization information
// into the context object (using cloud.WithContext or similar).
type Bucket interface {
	Name() string

	// List the objects in the bucket that meet the criteria defined by the
	// query, returning a result object that contains the results and potentially
	// a cursor for retrieving the next portion of the larger set of results.
	ListObjects(ctx context.Context, query *storage.Query) (*storage.Objects, error)

	// Create a reader for the contents of the object with the given name. The
	// caller must arrange for the reader to be closed when it is no longer
	// needed.
	NewReader(ctx context.Context, objectName string) (io.ReadCloser, error)

	// Return an ObjectWriter that can be used to create or overwrite an object
	// with the given attributes. attrs.Name must be specified. Otherwise, nil-
	// and zero-valud attributes are ignored.
	//
	// Object names must be valid non-empty UTF-8 no more than 1024 bytes in
	// length, not containing the code points U+000A (line feed) or U+000D
	// (carriage return).
	//
	// See here for authoritative documentation:
	//     https://cloud.google.com/storage/docs/bucket-naming#objectnames
	NewWriter(ctx context.Context, attrs *storage.ObjectAttrs) (ObjectWriter, error)

	// Delete the object with the given name.
	DeleteObject(ctx context.Context, name string) error
}

type bucket struct {
	projID string
	client *http.Client
	name   string
}

func (b *bucket) Name() string {
	return b.name
}

func (b *bucket) ListObjects(ctx context.Context, query *storage.Query) (*storage.Objects, error) {
	authContext := cloud.WithContext(ctx, b.projID, b.client)
	return storage.ListObjects(authContext, b.name, query)
}

func (b *bucket) NewReader(ctx context.Context, objectName string) (io.ReadCloser, error) {
	authContext := cloud.WithContext(ctx, b.projID, b.client)
	return storage.NewReader(authContext, b.name, objectName)
}

func (b *bucket) NewWriter(ctx context.Context, attrs *storage.ObjectAttrs) (ObjectWriter, error) {
	authContext := cloud.WithContext(ctx, b.projID, b.client)

	// Create and initialize the wrapped writer.
	wrapped := storage.NewWriter(authContext, b.name, attrs.Name)
	wrapped.ObjectAttrs = *attrs

	w := &objectWriter{
		wrapped: wrapped,
	}

	return w, nil
}

func (b *bucket) DeleteObject(ctx context.Context, name string) error {
	authContext := cloud.WithContext(ctx, b.projID, b.client)
	return storage.DeleteObject(authContext, b.name, name)
}
<commit_msg>Made requirements easier to read.<commit_after>// Copyright 2015 Google Inc. All Rights Reserved.
// Author: jacobsa@google.com (Aaron Jacobs)

package gcs

import (
	"io"
	"net/http"

	"golang.org/x/net/context"
	"google.golang.org/cloud"
	"google.golang.org/cloud/storage"
)

// Bucket represents a GCS bucket, pre-bound with a bucket name and necessary
// authorization information.
//
// Each method that may block accepts a context object that is used for
// deadlines and cancellation. Users need not package authorization information
// into the context object (using cloud.WithContext or similar).
type Bucket interface {
	Name() string

	// List the objects in the bucket that meet the criteria defined by the
	// query, returning a result object that contains the results and potentially
	// a cursor for retrieving the next portion of the larger set of results.
	ListObjects(ctx context.Context, query *storage.Query) (*storage.Objects, error)

	// Create a reader for the contents of the object with the given name. The
	// caller must arrange for the reader to be closed when it is no longer
	// needed.
	NewReader(ctx context.Context, objectName string) (io.ReadCloser, error)

	// Return an ObjectWriter that can be used to create or overwrite an object
	// with the given attributes. attrs.Name must be specified. Otherwise, nil-
	// and zero-valud attributes are ignored.
	//
	// Object names must:
	//
	// *  be non-empty.
	// *  be no longer than 1024 bytes.
	// *  be valid UTF-8.
	// *  not contain the code point U+000A (line feed).
	// *  not contain the code point U+000D (carriage return).
	//
	// See here for authoritative documentation:
	//     https://cloud.google.com/storage/docs/bucket-naming#objectnames
	NewWriter(ctx context.Context, attrs *storage.ObjectAttrs) (ObjectWriter, error)

	// Delete the object with the given name.
	DeleteObject(ctx context.Context, name string) error
}

type bucket struct {
	projID string
	client *http.Client
	name   string
}

func (b *bucket) Name() string {
	return b.name
}

func (b *bucket) ListObjects(ctx context.Context, query *storage.Query) (*storage.Objects, error) {
	authContext := cloud.WithContext(ctx, b.projID, b.client)
	return storage.ListObjects(authContext, b.name, query)
}

func (b *bucket) NewReader(ctx context.Context, objectName string) (io.ReadCloser, error) {
	authContext := cloud.WithContext(ctx, b.projID, b.client)
	return storage.NewReader(authContext, b.name, objectName)
}

func (b *bucket) NewWriter(ctx context.Context, attrs *storage.ObjectAttrs) (ObjectWriter, error) {
	authContext := cloud.WithContext(ctx, b.projID, b.client)

	// Create and initialize the wrapped writer.
	wrapped := storage.NewWriter(authContext, b.name, attrs.Name)
	wrapped.ObjectAttrs = *attrs

	w := &objectWriter{
		wrapped: wrapped,
	}

	return w, nil
}

func (b *bucket) DeleteObject(ctx context.Context, name string) error {
	authContext := cloud.WithContext(ctx, b.projID, b.client)
	return storage.DeleteObject(authContext, b.name, name)
}
<|endoftext|>


<commit_before>package impl

import (
	"errors"
	"net/url"
	"time"

	log "github.com/macarrie/flemzerd/logging"
	. "github.com/macarrie/flemzerd/objects"
	"github.com/pioz/tvdb"
)

type TVDBProvider struct {
	Client          tvdb.Client
	LastTokenUpdate time.Time
}

// Create new instance of the TVDB info provider
func New(apiKey string) (tvdbProvider TVDBProvider, err error) {
	client := tvdb.Client{Apikey: apiKey}
	err = client.Login()
	log.WithFields(log.Fields{
		"provider": "THE TVDB",
	}).Debug("Checking connection to TheTVDB")

	if err != nil {
		if tvdb.HaveCodeError(401, err) {
			log.Error("Cannot connect to thetvdb (API key not valid). Please check your API key and try again")
		} else {
			log.WithFields(log.Fields{
				"details":  err,
				"provider": "THE TVDB",
				"error":    err,
			}).Error("Cannot connect to thetvdb")
		}
		return TVDBProvider{}, err
	} else {
		log.Debug("Connection to TheTVDB successful")
		return TVDBProvider{Client: client, LastTokenUpdate: time.Now()}, nil
	}
}

// Check if Provider is alive
func (tvdbProvider TVDBProvider) IsAlive() error {
	log.Debug("Checking TVDB provider status")
	// Token expires every 24h. Refresh it if needed
	now := time.Now()
	yesterday := now.AddDate(0, 0, -1)
	if tvdbProvider.LastTokenUpdate.Before(yesterday) {
		tvdbProvider.Client.RefreshToken()
	}
	return tvdbProvider.Client.Login()
}

// Get show from name
func (tvdbProvider TVDBProvider) GetShow(tvShowName string) (TvShow, error) {
	log.WithFields(log.Fields{
		"name":     tvShowName,
		"provider": "THE TVDB",
	}).Debug("Searching show")

	tvShow, err := tvdbProvider.Client.BestSearch(tvShowName)
	if err != nil {
		return TvShow{}, handleTvShowNotFoundError(tvShowName, err)
	} else {
		log.WithFields(log.Fields{
			"name":     tvShow.SeriesName,
			"TVDB-ID":  tvShow.ID,
			"provider": "THE TVDB",
		}).Debug("TV show found")
		return convertShow(tvShow), nil

	}
}

// Get list of episodes of a given show
func (tvdbProvider TVDBProvider) GetEpisodes(tvShow TvShow) ([]Episode, error) {
	log.WithFields(log.Fields{
		"id":       tvShow.Id,
		"name":     tvShow.Name,
		"provider": "THE TVDB",
	}).Debug("Retrieving episodes list for a tv show")

	tvShowSearchResult, err := tvdbProvider.Client.BestSearch(tvShow.Name)
	if err != nil {
		return []Episode{}, handleTvShowNotFoundError(tvShow.Name, err)
	} else {
		err := tvdbProvider.Client.GetSeriesEpisodes(&tvShowSearchResult, url.Values{})
		if err != nil {
			log.WithFields(log.Fields{
				"tvshow_name": tvShow.Name,
				"id":          tvShow.Id,
				"provider":    "THE TVDB",
				"error":       err,
			}).Warn("Cannot retrieve episodes of tv show")
			return []Episode{}, err
		} else {
			log.WithFields(log.Fields{
				"name":      tvShow.Name,
				"tvshow_id": tvShow.Id,
				"provider":  "THE TVDB",
			}).Debug("Episodes found")

			var retVal []Episode
			for _, episode := range tvShowSearchResult.Episodes {
				retVal = append(retVal, convertEpisode(episode))

			}
			return retVal, nil
		}
	}
}

// Get all episodes of a tv show that haven't been aired yet for a given show
func (tvdbProvider TVDBProvider) GetNextEpisodes(tvShow TvShow) ([]Episode, error) {
	episodes, err := tvdbProvider.GetEpisodes(tvShow)
	if err != nil {
		log.WithFields(log.Fields{
			"tvshow_name": tvShow.Name,
			"id":          tvShow.Id,
			"provider":    "THE TVDB",
			"error":       err,
		}).Warn("Cannot get next aired episodes of the tv show")
		return []Episode{}, err
	} else {
		log.WithFields(log.Fields{
			"tvshow_name": tvShow.Name,
			"id":          tvShow.Id,
			"provider":    "THE TVDB",
		}).Debug("Getting list of episodes that haven't be aired yet")
		now := time.Now()
		futureEpisodes := filterEpisodesAiredBetweenDates(episodes, &now, nil)

		log.WithFields(log.Fields{
			"tvshow_name":    tvShow.Name,
			"id":             tvShow.Id,
			"nb_of_episodes": len(futureEpisodes),
			"provider":       "THE TVDB",
		}).Debug("Successfully filtered list of episodes that haven't be aired yet")
		return futureEpisodes, nil
	}
}

// Get list of episodes of a show aired less than RECENTLY_AIRED_EPISODES_INTERVAL days ago
func (tvdbProvider TVDBProvider) GetRecentlyAiredEpisodes(tvShow TvShow) ([]Episode, error) {
	episodes, err := tvdbProvider.GetEpisodes(tvShow)
	if err != nil {
		log.WithFields(log.Fields{
			"tvshow_name": tvShow.Name,
			"id":          tvShow.Id,
			"provider":    "THE TVDB",
			"error":       err,
		}).Warn("Cannot get recently aired episodes of the tv show")
		return []Episode{}, err
	} else {
		log.WithFields(log.Fields{
			"tvshow_name": tvShow.Name,
			"id":          tvShow.Id,
			"provider":    "THE TVDB",
		}).Debug("Getting list of episodes that haven recently been aired")

		// Current date minus RECENTLY_AIRED_EPISODES_INTERVAL days
		oldestDate := time.Now().AddDate(0, 0, -RECENTLY_AIRED_EPISODES_INTERVAL)
		now := time.Now()
		recentlyAiredEpisodes := filterEpisodesAiredBetweenDates(episodes, &oldestDate, &now)

		log.WithFields(log.Fields{
			"tvshow_name":    tvShow.Name,
			"id":             tvShow.Id,
			"nb_of_episodes": len(recentlyAiredEpisodes),
			"provider":       "THE TVDB",
		}).Debug("Successfully filtered list of episodes that have been recently aired")
		return recentlyAiredEpisodes, nil
	}
}

func filterEpisodesAiredBetweenDates(episodes []Episode, beginning *time.Time, end *time.Time) []Episode {
	// Set beginning date to zero time if beginning date is nil
	if beginning == nil {
		*beginning = time.Date(0, time.January, 1, 0, 0, 0, 0, time.UTC)
	}
	// Set end date to year 100,000 if end date is nil
	if end == nil {
		*end = time.Date(100000, time.January, 1, 0, 0, 0, 0, time.UTC)
	}

	log.WithFields(log.Fields{
		"start_date": beginning,
		"end_date":   end,
	}).Debug("Filtering episodes list by airing date")
	var retVal []Episode
	for _, episode := range episodes {
		if episode.Date.After(*beginning) && episode.Date.Before(*end) {
			retVal = append(retVal, episode)
		}
	}

	log.WithFields(log.Fields{
		"start_date":     beginning,
		"end_date":       end,
		"nb_of_episodes": len(retVal),
	}).Debug("Successfully filtered episodes list by airing date")

	return retVal
}

func handleTvShowNotFoundError(tvShowName string, err error) error {
	if tvdb.HaveCodeError(404, err) {
		// The request response is a 404: this means no results have been found
		log.WithFields(log.Fields{
			"tvshow_name": tvShowName,
			"provider":    "THE TVDB",
		}).Warn("TV show not found")
		return errors.New("TV show '" + tvShowName + "' not found")
	} else if tvdb.HaveCodeError(401, err) {
		// The request response is a 401: Authentication failure
		log.WithFields(log.Fields{
			"provider": "THE TVDB",
		}).Warn("Could not connect to provider, authentication failed for unkonwn reasons")
		return err
	} else {
		log.WithFields(log.Fields{
			"provider": "THE TVDB",
		}).Error("Unknown error encountered while getting show information from The TVDB")
		return err
	}
}

// Convert a github.com/pioz/tvdb series object to flemzerd tvShow object
func convertShow(tvShow tvdb.Series) TvShow {
	firstAired, err := time.Parse("2006-01-02", tvShow.FirstAired)
	if err != nil {
		firstAired = time.Time{}
	}

	return TvShow{
		Aliases:    tvShow.Aliases,
		Banner:     tvShow.Banner,
		FirstAired: firstAired,
		Id:         tvShow.ID,
		Overview:   tvShow.Overview,
		Name:       tvShow.SeriesName,
		Status:     tvShow.Status,
	}
}

// Convert a github.com/pioz/tvdb episode object to flemzerd episode object
func convertEpisode(episode tvdb.Episode) Episode {
	firstAired, err := time.Parse("2006-01-02", episode.FirstAired)
	if err != nil {
		firstAired = time.Time{}
	}

	return Episode{
		AbsoluteNumber: episode.AbsoluteNumber,
		Number:         episode.AiredEpisodeNumber,
		Season:         episode.AiredSeason,
		Name:           episode.EpisodeName,
		Date:           firstAired,
		Id:             episode.ID,
		Overview:       episode.Overview,
	}
}
<commit_msg>Transformed tvdb provider to pointer in tvdb.go and update token refresh part<commit_after>package impl

import (
	"errors"
	"net/url"
	"time"

	log "github.com/macarrie/flemzerd/logging"
	. "github.com/macarrie/flemzerd/objects"
	"github.com/pioz/tvdb"
)

type TVDBProvider struct {
	Client          tvdb.Client
	LastTokenUpdate time.Time
}

// Create new instance of the TVDB info provider
func New(apiKey string) (tvdbProvider *TVDBProvider, err error) {
	client := tvdb.Client{Apikey: apiKey}
	err = client.Login()
	log.WithFields(log.Fields{
		"provider": "THE TVDB",
	}).Debug("Checking connection to TheTVDB")

	if err != nil {
		if tvdb.HaveCodeError(401, err) {
			log.Error("Cannot connect to thetvdb (API key not valid). Please check your API key and try again")
		} else {
			log.WithFields(log.Fields{
				"details":  err,
				"provider": "THE TVDB",
				"error":    err,
			}).Error("Cannot connect to thetvdb")
		}
		return &TVDBProvider{}, err
	} else {
		log.Debug("Connection to TheTVDB successful")
		return &TVDBProvider{Client: client, LastTokenUpdate: time.Now()}, nil
	}
}

// Check if Provider is alive
func (tvdbProvider *TVDBProvider) IsAlive() error {
	log.Debug("Checking TVDB provider status")

	// Token expires every 24h. Refresh it if needed
	now := time.Now()
	yesterday := now.AddDate(0, 0, -1)

	if tvdbProvider.LastTokenUpdate.Before(yesterday) {
		log.Debug("Refreshing TVDB token")
		tvdbProvider.Client.RefreshToken()
		tvdbProvider.LastTokenUpdate = now
	}

	return tvdbProvider.Client.Login()
}

// Get show from name
func (tvdbProvider *TVDBProvider) GetShow(tvShowName string) (TvShow, error) {
	log.WithFields(log.Fields{
		"name":     tvShowName,
		"provider": "THE TVDB",
	}).Debug("Searching show")

	tvShow, err := tvdbProvider.Client.BestSearch(tvShowName)
	if err != nil {
		return TvShow{}, handleTvShowNotFoundError(tvShowName, err)
	} else {
		log.WithFields(log.Fields{
			"name":     tvShow.SeriesName,
			"TVDB-ID":  tvShow.ID,
			"provider": "THE TVDB",
		}).Debug("TV show found")
		return convertShow(tvShow), nil

	}
}

// Get list of episodes of a given show
func (tvdbProvider *TVDBProvider) GetEpisodes(tvShow TvShow) ([]Episode, error) {
	log.WithFields(log.Fields{
		"id":       tvShow.Id,
		"name":     tvShow.Name,
		"provider": "THE TVDB",
	}).Debug("Retrieving episodes list for a tv show")

	tvShowSearchResult, err := tvdbProvider.Client.BestSearch(tvShow.Name)
	if err != nil {
		return []Episode{}, handleTvShowNotFoundError(tvShow.Name, err)
	} else {
		err := tvdbProvider.Client.GetSeriesEpisodes(&tvShowSearchResult, url.Values{})
		if err != nil {
			log.WithFields(log.Fields{
				"tvshow_name": tvShow.Name,
				"id":          tvShow.Id,
				"provider":    "THE TVDB",
				"error":       err,
			}).Warn("Cannot retrieve episodes of tv show")
			return []Episode{}, err
		} else {
			log.WithFields(log.Fields{
				"name":      tvShow.Name,
				"tvshow_id": tvShow.Id,
				"provider":  "THE TVDB",
			}).Debug("Episodes found")

			var retVal []Episode
			for _, episode := range tvShowSearchResult.Episodes {
				retVal = append(retVal, convertEpisode(episode))

			}
			return retVal, nil
		}
	}
}

// Get all episodes of a tv show that haven't been aired yet for a given show
func (tvdbProvider *TVDBProvider) GetNextEpisodes(tvShow TvShow) ([]Episode, error) {
	episodes, err := tvdbProvider.GetEpisodes(tvShow)
	if err != nil {
		log.WithFields(log.Fields{
			"tvshow_name": tvShow.Name,
			"id":          tvShow.Id,
			"provider":    "THE TVDB",
			"error":       err,
		}).Warn("Cannot get next aired episodes of the tv show")
		return []Episode{}, err
	} else {
		log.WithFields(log.Fields{
			"tvshow_name": tvShow.Name,
			"id":          tvShow.Id,
			"provider":    "THE TVDB",
		}).Debug("Getting list of episodes that haven't be aired yet")
		now := time.Now()
		futureEpisodes := filterEpisodesAiredBetweenDates(episodes, &now, nil)

		log.WithFields(log.Fields{
			"tvshow_name":    tvShow.Name,
			"id":             tvShow.Id,
			"nb_of_episodes": len(futureEpisodes),
			"provider":       "THE TVDB",
		}).Debug("Successfully filtered list of episodes that haven't be aired yet")
		return futureEpisodes, nil
	}
}

// Get list of episodes of a show aired less than RECENTLY_AIRED_EPISODES_INTERVAL days ago
func (tvdbProvider *TVDBProvider) GetRecentlyAiredEpisodes(tvShow TvShow) ([]Episode, error) {
	episodes, err := tvdbProvider.GetEpisodes(tvShow)
	if err != nil {
		log.WithFields(log.Fields{
			"tvshow_name": tvShow.Name,
			"id":          tvShow.Id,
			"provider":    "THE TVDB",
			"error":       err,
		}).Warn("Cannot get recently aired episodes of the tv show")
		return []Episode{}, err
	} else {
		log.WithFields(log.Fields{
			"tvshow_name": tvShow.Name,
			"id":          tvShow.Id,
			"provider":    "THE TVDB",
		}).Debug("Getting list of episodes that haven recently been aired")

		// Current date minus RECENTLY_AIRED_EPISODES_INTERVAL days
		oldestDate := time.Now().AddDate(0, 0, -RECENTLY_AIRED_EPISODES_INTERVAL)
		now := time.Now()
		recentlyAiredEpisodes := filterEpisodesAiredBetweenDates(episodes, &oldestDate, &now)

		log.WithFields(log.Fields{
			"tvshow_name":    tvShow.Name,
			"id":             tvShow.Id,
			"nb_of_episodes": len(recentlyAiredEpisodes),
			"provider":       "THE TVDB",
		}).Debug("Successfully filtered list of episodes that have been recently aired")
		return recentlyAiredEpisodes, nil
	}
}

func filterEpisodesAiredBetweenDates(episodes []Episode, beginning *time.Time, end *time.Time) []Episode {
	// Set beginning date to zero time if beginning date is nil
	if beginning == nil {
		*beginning = time.Date(0, time.January, 1, 0, 0, 0, 0, time.UTC)
	}
	// Set end date to year 100,000 if end date is nil
	if end == nil {
		*end = time.Date(100000, time.January, 1, 0, 0, 0, 0, time.UTC)
	}

	log.WithFields(log.Fields{
		"start_date": beginning,
		"end_date":   end,
	}).Debug("Filtering episodes list by airing date")
	var retVal []Episode
	for _, episode := range episodes {
		if episode.Date.After(*beginning) && episode.Date.Before(*end) {
			retVal = append(retVal, episode)
		}
	}

	log.WithFields(log.Fields{
		"start_date":     beginning,
		"end_date":       end,
		"nb_of_episodes": len(retVal),
	}).Debug("Successfully filtered episodes list by airing date")

	return retVal
}

func handleTvShowNotFoundError(tvShowName string, err error) error {
	if tvdb.HaveCodeError(404, err) {
		// The request response is a 404: this means no results have been found
		log.WithFields(log.Fields{
			"tvshow_name": tvShowName,
			"provider":    "THE TVDB",
		}).Warn("TV show not found")
		return errors.New("TV show '" + tvShowName + "' not found")
	} else if tvdb.HaveCodeError(401, err) {
		// The request response is a 401: Authentication failure
		log.WithFields(log.Fields{
			"provider": "THE TVDB",
		}).Warn("Could not connect to provider, authentication failed for unkonwn reasons")
		return err
	} else {
		log.WithFields(log.Fields{
			"provider": "THE TVDB",
		}).Error("Unknown error encountered while getting show information from The TVDB")
		return err
	}
}

// Convert a github.com/pioz/tvdb series object to flemzerd tvShow object
func convertShow(tvShow tvdb.Series) TvShow {
	firstAired, err := time.Parse("2006-01-02", tvShow.FirstAired)
	if err != nil {
		firstAired = time.Time{}
	}

	return TvShow{
		Aliases:    tvShow.Aliases,
		Banner:     tvShow.Banner,
		FirstAired: firstAired,
		Id:         tvShow.ID,
		Overview:   tvShow.Overview,
		Name:       tvShow.SeriesName,
		Status:     tvShow.Status,
	}
}

// Convert a github.com/pioz/tvdb episode object to flemzerd episode object
func convertEpisode(episode tvdb.Episode) Episode {
	firstAired, err := time.Parse("2006-01-02", episode.FirstAired)
	if err != nil {
		firstAired = time.Time{}
	}

	return Episode{
		AbsoluteNumber: episode.AbsoluteNumber,
		Number:         episode.AiredEpisodeNumber,
		Season:         episode.AiredSeason,
		Name:           episode.EpisodeName,
		Date:           firstAired,
		Id:             episode.ID,
		Overview:       episode.Overview,
	}
}
<|endoftext|>


<commit_before>// Copyright 2015 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package apiserver

import (
	"github.com/juju/utils/set"
)

// readOnlyCalls specify a white-list of API calls that do not
// modify the database. The format of the calls is "<facade>.<method>".
// At this stage, we are explicitly ignoring the facade version.
var readOnlyCalls = set.NewStrings(
	"Action.Actions",
	"Action.FindActionTagsByPrefix",
	"Action.ListAll",
	"Action.ListPending",
	"Action.ListRunning",
	"Action.ListCompleted",
	"Action.ApplicationsCharmsActions",
	"Annotations.Get",
	"Application.GetConstraints",
	"Application.CharmRelations",
	"Application.Get",
	"Block.List",
	"Charms.CharmInfo",
	"Charms.IsMetered",
	"Charms.List",
	"Client.AgentVersion",
	"Client.APIHostPorts",
	"Client.ModelGet",
	"Client.ModelInfo",
	"Client.ModelUserInfo",
	"Client.FullStatus",
	// FindTools, while being technically read only, isn't a useful
	// command for a read only user to run.
	// While GetBundleChanges is technically read only, it is a precursor
	// to deploying the bundle or changes. But... let's leave it here anyway.
	"Client.GetBundleChanges",
	"Client.GetModelConstraints",
	"Client.PrivateAddress",
	"Client.PublicAddress",
	// ResolveCharms, while being technically read only, isn't a useful
	// command for a read only user to run.
	// Status is so old it shouldn't be used.
	"Client.StatusHistory",
	"Client.WatchAll",
	"Cloud.Cloud",
	"Cloud.Credentials",
	// TODO: add controller work.
	"KeyManager.ListKeys",
	"ModelManager.ModelInfo",
	"Spaces.ListSpaces",
	"Storage.ListStorageDetails",
	"Storage.ListFilesystems",
	"Storage.ListPools",
	"Storage.ListVolumes",
	"Subnets.AllSpaces",
	"Subnets.AllZones",
	"Subnets.ListSubnets",
	"UserManager.UserInfo",
)

// isCallReadOnly returns whether or not the method on the facade
// is known to not alter the database.
func isCallReadOnly(facade, method string) bool {
	key := facade + "." + method
	// NOTE: maybe useful in the future to be able to specify entire facades
	// as read only, in which case specifying something like "Facade.*" would
	// be useful. Not sure we'll ever need this, but something to think about
	// perhaps.
	return readOnlyCalls.Contains(key)
}
<commit_msg>Allow gui required methods in RO user.<commit_after>// Copyright 2015 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package apiserver

import (
	"github.com/juju/utils/set"
)

// readOnlyCalls specify a white-list of API calls that do not
// modify the database. The format of the calls is "<facade>.<method>".
// At this stage, we are explicitly ignoring the facade version.
var readOnlyCalls = set.NewStrings(
	"Action.Actions",
	"Action.FindActionTagsByPrefix",
	"Action.ListAll",
	"Action.ListPending",
	"Action.ListRunning",
	"Action.ListCompleted",
	"Action.ApplicationsCharmsActions",
	"AllWatcher.Next",
	"Annotations.Get",
	"Application.GetConstraints",
	"Application.CharmRelations",
	"Application.Get",
	"Block.List",
	"Charms.CharmInfo",
	"Charms.IsMetered",
	"Charms.List",
	"Client.AgentVersion",
	"Client.APIHostPorts",
	"Client.ModelGet",
	"Client.ModelInfo",
	"Client.ModelUserInfo",
	"Client.FullStatus",
	// FindTools, while being technically read only, isn't a useful
	// command for a read only user to run.
	// While GetBundleChanges is technically read only, it is a precursor
	// to deploying the bundle or changes. But... let's leave it here anyway.
	"Client.GetBundleChanges",
	"Client.GetModelConstraints",
	"Client.PrivateAddress",
	"Client.PublicAddress",
	// ResolveCharms, while being technically read only, isn't a useful
	// command for a read only user to run.
	// Status is so old it shouldn't be used.
	"Client.StatusHistory",
	"Client.WatchAll",
	"Cloud.Cloud",
	"Cloud.Credentials",
	// TODO: add controller work.
	"KeyManager.ListKeys",
	"ModelManager.ModelInfo",
	"Pinger.Ping",
	"Spaces.ListSpaces",
	"Storage.ListStorageDetails",
	"Storage.ListFilesystems",
	"Storage.ListPools",
	"Storage.ListVolumes",
	"Subnets.AllSpaces",
	"Subnets.AllZones",
	"Subnets.ListSubnets",
	"UserManager.UserInfo",
)

// isCallReadOnly returns whether or not the method on the facade
// is known to not alter the database.
func isCallReadOnly(facade, method string) bool {
	key := facade + "." + method
	// NOTE: maybe useful in the future to be able to specify entire facades
	// as read only, in which case specifying something like "Facade.*" would
	// be useful. Not sure we'll ever need this, but something to think about
	// perhaps.
	return readOnlyCalls.Contains(key)
}
<|endoftext|>


<commit_before>package irckit

import (
	"errors"
	"html"
	"regexp"
	"strings"
	"sync"
	"time"

	"github.com/nlopes/slack"
)

type SlackInfo struct {
	Token      string
	sc         *slack.Client
	rtm        *slack.RTM
	sinfo      *slack.Info
	susers     map[string]slack.User
	connected  bool
	inprogress bool
	sync.RWMutex
}

func (u *User) loginToSlack() (*slack.Client, error) {
	u.sc = slack.New(u.Token)
	u.rtm = u.sc.NewRTM()
	u.Lock()
	u.susers = make(map[string]slack.User)
	u.Unlock()
	go u.rtm.ManageConnection()
	//time.Sleep(time.Second * 2)
	u.sinfo = u.rtm.GetInfo()
	count := 0
	for u.sinfo == nil {
		time.Sleep(time.Millisecond * 500)
		logger.Debug("still waiting for sinfo")
		u.sinfo = u.rtm.GetInfo()
		count++
		if count == 20 {
			return nil, errors.New("couldn't connect in 10 seconds. Check your credentials")
		}
	}

	// we only know which server we are connecting to when we actually are connected.
	// disconnect if we're not allowed
	if len(u.MmInfo.Cfg.SlackSettings.Restrict) > 0 {
		ok := false
		for _, domain := range u.MmInfo.Cfg.SlackSettings.Restrict {
			if domain == u.sinfo.Team.Domain {
				ok = true
				break
			}
		}
		if !ok {
			u.rtm.Disconnect()
			return nil, errors.New("Not allowed to connect to " + u.sinfo.Team.Domain + " slack")
		}
	}
	go u.handleSlack()
	u.addSlackUsersToChannels()
	u.connected = true
	return u.sc, nil
}

func (u *User) logoutFromSlack() error {
	logger.Debug("calling logout from slack")
	err := u.rtm.Disconnect()
	if err != nil {
		logger.Debug("logoutfrom slack", err)
		return err
	}
	u.Srv.Logout(u)
	u.sc = nil
	logger.Info("logout succeeded")
	u.connected = false
	return nil
}

func (u *User) createSlackUser(slackuser *slack.User) *User {
	if slackuser == nil {
		return nil
	}
	if ghost, ok := u.Srv.HasUser(slackuser.Name); ok {
		return ghost
	}
	ghost := &User{Nick: slackuser.Name, User: slackuser.ID, Real: slackuser.RealName, Host: "host", Roles: "", channels: map[Channel]struct{}{}, DisplayName: slackuser.Profile.DisplayName}
	ghost.MmGhostUser = true
	u.Srv.Add(ghost)
	return ghost
}

func (u *User) addSlackUserToChannel(user *slack.User, channel string, channelId string) {
	if user == nil {
		return
	}
	ghost := u.createSlackUser(user)
	if ghost == nil {
		logger.Warnf("Cannot join %v into %s", user, channel)
		return
	}
	logger.Debugf("adding %s to %s (%s)", ghost.Nick, channel, channelId)
	ch := u.Srv.Channel(channelId)
	logger.Debugf("channel: %#v %#v", ch.String(), ch.ID())
	ch.Join(ghost)
}

func (u *User) addSlackUsersToChannels() {
	srv := u.Srv
	throttle := time.Tick(time.Millisecond * 100)
	logger.Debug("in addUsersToChannels()")
	// add all users, also who are not on channels
	ch := srv.Channel("&users")
	users, _ := u.sc.GetUsers()
	for _, mmuser := range users {
		// do not add our own nick
		if mmuser.ID == u.sinfo.User.ID {
			continue
		}
		u.createSlackUser(&mmuser)
		u.addSlackUserToChannel(&mmuser, "&users", "&users")
		u.Lock()
		u.susers[mmuser.ID] = mmuser
		u.Unlock()
	}
	ch.Join(u)

	channels := make(chan interface{}, 10)
	for i := 0; i < 10; i++ {
		go u.addSlackUserToChannelWorker(channels, throttle)
	}
	groups, _ := u.sc.GetGroups(true)
	mmchannels, _ := u.sc.GetChannels(true)
	for _, mmchannel := range mmchannels {
		if mmchannel.IsMember {
			logger.Debug("Adding channel", mmchannel)
			channels <- mmchannel
		}
	}
	for _, mmchannel := range groups {
		logger.Debug("Adding private channel", mmchannel)
		channels <- mmchannel
	}
	close(channels)
}

func (u *User) addSlackUserToChannelWorker(channels <-chan interface{}, throttle <-chan time.Time) {
	var ID, name string
	for {
		mmchannel, ok := <-channels
		if !ok {
			logger.Debug("Done adding user to channels")
			return
		}
		<-throttle
		switch mmchannel.(type) {
		case slack.Channel:
			ID = mmchannel.(slack.Channel).ID
			name = mmchannel.(slack.Channel).Name
			u.syncSlackChannel(ID, name)
		case slack.Group:
			ID = mmchannel.(slack.Group).ID
			name = mmchannel.(slack.Group).Name
			logger.Debugf("GROUP %#v", mmchannel.(slack.Group))
			u.syncSlackGroup(ID, name)

		}
		// exclude direct messages
		//var spoof func(string, string)
		//ch := u.Srv.Channel(mmchannel.ID)
		// post everything to the channel you haven't seen yet
	}
}

func (u *User) handleSlack() {
	for {
		/*
			if u.mc.WsQuit {
				logger.Debug("exiting handleWsMessage")
				return
			}
		*/
		logger.Debug("in handleSlack")
		for msg := range u.rtm.IncomingEvents {
			switch ev := msg.Data.(type) {
			case *slack.MessageEvent:
				u.handleSlackActionPost(ev)
			case *slack.DisconnectedEvent:
				logger.Debug("disconnected event received, we should reconnect now..")
				//return
			}
		}
	}
	/*
			logger.Debugf("MMUser WsReceiver: %#v", message.Raw)
			// check if we have the users/channels in our cache. If not update
			u.checkWsActionMessage(message.Raw, updateChannelsThrottle)
			switch message.Raw.Event {
			case model.WEBSOCKET_EVENT_POSTED:
				u.handleWsActionPost(message.Raw)
			case model.WEBSOCKET_EVENT_POST_EDITED:
				u.handleWsActionPost(message.Raw)
			case model.WEBSOCKET_EVENT_USER_REMOVED:
				u.handleWsActionUserRemoved(message.Raw)
			case model.WEBSOCKET_EVENT_USER_ADDED:
				u.handleWsActionUserAdded(message.Raw)
			}
		}
	*/
}

func (u *User) handleSlackActionPost(rmsg *slack.MessageEvent) {
	var ch Channel
	logger.Debugf("handleSlackActionPost() receiving msg %#v", rmsg)
	if len(rmsg.Attachments) > 0 {
		// skip messages we made ourselves
		if rmsg.Attachments[0].CallbackID == "matterircd" {
			return
		}
	}

	user, err := u.rtm.GetUserInfo(rmsg.User)
	if err != nil {
		if rmsg.BotID == "" {
			return
		}
	}

	// handle bot messages
	botname := ""
	if rmsg.User == "" && rmsg.BotID != "" {
		botname = rmsg.Username
		if botname == "" {
			bot, _ := u.rtm.GetBotInfo(rmsg.BotID)
			if bot.Name != "" {
				botname = bot.Name
			}
		}
	}

	// create new "ghost" user
	ghost := u.createSlackUser(user)

	spoofUsername := ""
	if user != nil {
		spoofUsername = user.ID
		if ghost != nil {
			spoofUsername = ghost.Nick
			if ghost.DisplayName != "" && ghost.DisplayName != ghost.Nick && u.MmInfo.Cfg.SlackSettings.UseDisplayName {
				spoofUsername = "|"
				//	spoofUsername = ghost.DisplayName
			}
		}
	}

	// if we have a botname, use it
	if botname != "" {
		spoofUsername = botname
	}

	msgs := strings.Split(rmsg.Text, "\n")
	// direct message

	ch = u.Srv.Channel(rmsg.Channel)

	if ghost != nil {
		// join if not in channel
		if !ch.HasUser(ghost) {
			ch.Join(ghost)
		}
	}

	for _, m := range msgs {
		// cleanup the message
		m = u.replaceMention(m)
		m = u.replaceVariable(m)
		m = u.replaceChannel(m)
		m = u.replaceURL(m)
		m = html.UnescapeString(m)

		// look in attachments if we have no text
		if m == "" {
			for _, attach := range rmsg.Attachments {
				if attach.Text != "" {
					m = attach.Text
				} else {
					m = attach.Fallback
				}
			}
		}

		// still no text, ignore this message
		if m == "" {
			continue
		}

		if strings.HasPrefix(rmsg.Channel, "D") {
			u.MsgSpoofUser(spoofUsername, m)
		} else {
			if ghost != nil && ghost.DisplayName != "" && ghost.DisplayName != ghost.Nick &&
				u.MmInfo.Cfg.SlackSettings.UseDisplayName {
				m = "<" + ghost.DisplayName + "> " + m
			}
			ch.SpoofMessage(spoofUsername, m)
		}
	}
}

// sync IRC with mattermost channel state
func (u *User) syncSlackChannel(id string, name string) {
	srv := u.Srv
	info, err := u.sc.GetChannelInfo(id)
	if err != nil {
		logger.Info(err)
	}

	for _, user := range info.Members {
		if u.sinfo.User.ID != user {
			//slackuser, _ := u.sc.GetUserInfo(user)
			slackuser := u.getSlackUser(user)
			if slackuser != nil {
				u.addSlackUserToChannel(slackuser, "#"+name, id)
			}
		}
	}

	ch := srv.Channel(id)
	ch.Topic(u, info.Topic.Value)
	if !ch.HasUser(u) {
		logger.Debugf("syncSlackchannel adding myself to %s (id: %s)", name, id)
		ch.Join(u)
	}
}

// sync IRC with mattermost channel state
func (u *User) syncSlackGroup(id string, name string) {
	srv := u.Srv
	info, err := u.sc.GetGroupInfo(id)
	if err != nil {
		logger.Info(err)
	}

	for _, user := range info.Members {
		if u.sinfo.User.ID != user {
			//slackuser, _ := u.sc.GetUserInfo(user)
			slackuser := u.getSlackUser(user)
			if slackuser != nil {
				u.addSlackUserToChannel(slackuser, "#"+name, id)
			}
		}
	}

	ch := srv.Channel(id)
	ch.Topic(u, info.Topic.Value)
	if !ch.HasUser(u) {
		logger.Debugf("syncSlackchannel adding myself to %s (id: %s)", name, id)
		ch.Join(u)
	}
}

// @see https://api.slack.com/docs/message-formatting#linking_to_channels_and_users
func (u *User) replaceMention(text string) string {
	results := regexp.MustCompile(`<@([a-zA-z0-9]+)>`).FindAllStringSubmatch(text, -1)
	for _, r := range results {
		text = strings.Replace(text, "<@"+r[1]+">", "@"+u.userName(r[1]), -1)
	}
	return text
}

// @see https://api.slack.com/docs/message-formatting#linking_to_channels_and_users
func (u *User) replaceChannel(text string) string {
	results := regexp.MustCompile(`<#[a-zA-Z0-9]+\|(.+?)>`).FindAllStringSubmatch(text, -1)
	for _, r := range results {
		text = strings.Replace(text, r[0], "#"+r[1], -1)
	}
	return text
}

// @see https://api.slack.com/docs/message-formatting#variables
func (u *User) replaceVariable(text string) string {
	results := regexp.MustCompile(`<!((?:subteam\^)?[a-zA-Z0-9]+)(?:\|@?(.+?))?>`).FindAllStringSubmatch(text, -1)
	for _, r := range results {
		if r[2] != "" {
			text = strings.Replace(text, r[0], "@"+r[2], -1)
		} else {
			text = strings.Replace(text, r[0], "@"+r[1], -1)
		}
	}
	return text
}

// @see https://api.slack.com/docs/message-formatting#linking_to_urls
func (u *User) replaceURL(text string) string {
	results := regexp.MustCompile(`<(.*?)(\|.*?)?>`).FindAllStringSubmatch(text, -1)
	for _, r := range results {
		text = strings.Replace(text, r[0], r[1], -1)
	}
	return text
}

func (u *User) getSlackUser(name string) *slack.User {
	u.RLock()
	defer u.RUnlock()
	if user, ok := u.susers[name]; ok {
		return &user
	}
	return nil
}

func (u *User) userName(id string) string {
	u.RLock()
	defer u.RUnlock()
	// TODO dynamically update when new users are joining slack
	for _, us := range u.susers {
		if us.ID == id {
			if us.Profile.DisplayName != "" {
				return us.Profile.DisplayName
			}
			return us.Name
		}
	}
	if id == u.sinfo.User.ID {
		return u.sinfo.User.Name
	}
	return ""
}

func (u *User) isConnected() bool {
	return u.connected
}
<commit_msg>Implement BlackListUser setting (slack). Closes 42wim/matterircd#172<commit_after>package irckit

import (
	"errors"
	"html"
	"regexp"
	"strings"
	"sync"
	"time"

	"github.com/nlopes/slack"
)

type SlackInfo struct {
	Token      string
	sc         *slack.Client
	rtm        *slack.RTM
	sinfo      *slack.Info
	susers     map[string]slack.User
	connected  bool
	inprogress bool
	sync.RWMutex
}

func (u *User) loginToSlack() (*slack.Client, error) {
	u.sc = slack.New(u.Token)
	u.rtm = u.sc.NewRTM()
	u.Lock()
	u.susers = make(map[string]slack.User)
	u.Unlock()
	go u.rtm.ManageConnection()
	//time.Sleep(time.Second * 2)
	u.sinfo = u.rtm.GetInfo()
	count := 0
	for u.sinfo == nil {
		time.Sleep(time.Millisecond * 500)
		logger.Debug("still waiting for sinfo")
		u.sinfo = u.rtm.GetInfo()
		count++
		if count == 20 {
			return nil, errors.New("couldn't connect in 10 seconds. Check your credentials")
		}
	}

	// we only know which server we are connecting to when we actually are connected.
	// disconnect if we're not allowed
	if len(u.MmInfo.Cfg.SlackSettings.Restrict) > 0 {
		ok := false
		for _, domain := range u.MmInfo.Cfg.SlackSettings.Restrict {
			if domain == u.sinfo.Team.Domain {
				ok = true
				break
			}
		}
		if !ok {
			u.rtm.Disconnect()
			return nil, errors.New("Not allowed to connect to " + u.sinfo.Team.Domain + " slack")
		}
	}
	// we only know which user we are when we actually are connected.
	// disconnect if we're not allowed
	if len(u.MmInfo.Cfg.SlackSettings.BlackListUser) > 0 {
		ok := false
		for _, user := range u.MmInfo.Cfg.SlackSettings.BlackListUser {
			if user == u.sinfo.User.Name {
				ok = true
				break
			}
		}
		if ok {
			u.rtm.Disconnect()
			return nil, errors.New("Not allowed to connect")
		}
	}

	go u.handleSlack()
	u.addSlackUsersToChannels()
	u.connected = true
	return u.sc, nil
}

func (u *User) logoutFromSlack() error {
	logger.Debug("calling logout from slack")
	err := u.rtm.Disconnect()
	if err != nil {
		logger.Debug("logoutfrom slack", err)
		return err
	}
	u.Srv.Logout(u)
	u.sc = nil
	logger.Info("logout succeeded")
	u.connected = false
	return nil
}

func (u *User) createSlackUser(slackuser *slack.User) *User {
	if slackuser == nil {
		return nil
	}
	if ghost, ok := u.Srv.HasUser(slackuser.Name); ok {
		return ghost
	}
	ghost := &User{Nick: slackuser.Name, User: slackuser.ID, Real: slackuser.RealName, Host: "host", Roles: "", channels: map[Channel]struct{}{}, DisplayName: slackuser.Profile.DisplayName}
	ghost.MmGhostUser = true
	u.Srv.Add(ghost)
	return ghost
}

func (u *User) addSlackUserToChannel(user *slack.User, channel string, channelId string) {
	if user == nil {
		return
	}
	ghost := u.createSlackUser(user)
	if ghost == nil {
		logger.Warnf("Cannot join %v into %s", user, channel)
		return
	}
	logger.Debugf("adding %s to %s (%s)", ghost.Nick, channel, channelId)
	ch := u.Srv.Channel(channelId)
	logger.Debugf("channel: %#v %#v", ch.String(), ch.ID())
	ch.Join(ghost)
}

func (u *User) addSlackUsersToChannels() {
	srv := u.Srv
	throttle := time.Tick(time.Millisecond * 100)
	logger.Debug("in addUsersToChannels()")
	// add all users, also who are not on channels
	ch := srv.Channel("&users")
	users, _ := u.sc.GetUsers()
	for _, mmuser := range users {
		// do not add our own nick
		if mmuser.ID == u.sinfo.User.ID {
			continue
		}
		u.createSlackUser(&mmuser)
		u.addSlackUserToChannel(&mmuser, "&users", "&users")
		u.Lock()
		u.susers[mmuser.ID] = mmuser
		u.Unlock()
	}
	ch.Join(u)

	channels := make(chan interface{}, 10)
	for i := 0; i < 10; i++ {
		go u.addSlackUserToChannelWorker(channels, throttle)
	}
	groups, _ := u.sc.GetGroups(true)
	mmchannels, _ := u.sc.GetChannels(true)
	for _, mmchannel := range mmchannels {
		if mmchannel.IsMember {
			logger.Debug("Adding channel", mmchannel)
			channels <- mmchannel
		}
	}
	for _, mmchannel := range groups {
		logger.Debug("Adding private channel", mmchannel)
		channels <- mmchannel
	}
	close(channels)
}

func (u *User) addSlackUserToChannelWorker(channels <-chan interface{}, throttle <-chan time.Time) {
	var ID, name string
	for {
		mmchannel, ok := <-channels
		if !ok {
			logger.Debug("Done adding user to channels")
			return
		}
		<-throttle
		switch mmchannel.(type) {
		case slack.Channel:
			ID = mmchannel.(slack.Channel).ID
			name = mmchannel.(slack.Channel).Name
			u.syncSlackChannel(ID, name)
		case slack.Group:
			ID = mmchannel.(slack.Group).ID
			name = mmchannel.(slack.Group).Name
			logger.Debugf("GROUP %#v", mmchannel.(slack.Group))
			u.syncSlackGroup(ID, name)

		}
		// exclude direct messages
		//var spoof func(string, string)
		//ch := u.Srv.Channel(mmchannel.ID)
		// post everything to the channel you haven't seen yet
	}
}

func (u *User) handleSlack() {
	for {
		/*
			if u.mc.WsQuit {
				logger.Debug("exiting handleWsMessage")
				return
			}
		*/
		logger.Debug("in handleSlack")
		for msg := range u.rtm.IncomingEvents {
			switch ev := msg.Data.(type) {
			case *slack.MessageEvent:
				u.handleSlackActionPost(ev)
			case *slack.DisconnectedEvent:
				logger.Debug("disconnected event received, we should reconnect now..")
				//return
			}
		}
	}
	/*
			logger.Debugf("MMUser WsReceiver: %#v", message.Raw)
			// check if we have the users/channels in our cache. If not update
			u.checkWsActionMessage(message.Raw, updateChannelsThrottle)
			switch message.Raw.Event {
			case model.WEBSOCKET_EVENT_POSTED:
				u.handleWsActionPost(message.Raw)
			case model.WEBSOCKET_EVENT_POST_EDITED:
				u.handleWsActionPost(message.Raw)
			case model.WEBSOCKET_EVENT_USER_REMOVED:
				u.handleWsActionUserRemoved(message.Raw)
			case model.WEBSOCKET_EVENT_USER_ADDED:
				u.handleWsActionUserAdded(message.Raw)
			}
		}
	*/
}

func (u *User) handleSlackActionPost(rmsg *slack.MessageEvent) {
	var ch Channel
	logger.Debugf("handleSlackActionPost() receiving msg %#v", rmsg)
	if len(rmsg.Attachments) > 0 {
		// skip messages we made ourselves
		if rmsg.Attachments[0].CallbackID == "matterircd" {
			return
		}
	}

	user, err := u.rtm.GetUserInfo(rmsg.User)
	if err != nil {
		if rmsg.BotID == "" {
			return
		}
	}

	// handle bot messages
	botname := ""
	if rmsg.User == "" && rmsg.BotID != "" {
		botname = rmsg.Username
		if botname == "" {
			bot, _ := u.rtm.GetBotInfo(rmsg.BotID)
			if bot.Name != "" {
				botname = bot.Name
			}
		}
	}

	// create new "ghost" user
	ghost := u.createSlackUser(user)

	spoofUsername := ""
	if user != nil {
		spoofUsername = user.ID
		if ghost != nil {
			spoofUsername = ghost.Nick
			if ghost.DisplayName != "" && ghost.DisplayName != ghost.Nick && u.MmInfo.Cfg.SlackSettings.UseDisplayName {
				spoofUsername = "|"
				//	spoofUsername = ghost.DisplayName
			}
		}
	}

	// if we have a botname, use it
	if botname != "" {
		spoofUsername = botname
	}

	msgs := strings.Split(rmsg.Text, "\n")
	// direct message

	ch = u.Srv.Channel(rmsg.Channel)

	if ghost != nil {
		// join if not in channel
		if !ch.HasUser(ghost) {
			ch.Join(ghost)
		}
	}

	for _, m := range msgs {
		// cleanup the message
		m = u.replaceMention(m)
		m = u.replaceVariable(m)
		m = u.replaceChannel(m)
		m = u.replaceURL(m)
		m = html.UnescapeString(m)

		// look in attachments if we have no text
		if m == "" {
			for _, attach := range rmsg.Attachments {
				if attach.Text != "" {
					m = attach.Text
				} else {
					m = attach.Fallback
				}
			}
		}

		// still no text, ignore this message
		if m == "" {
			continue
		}

		if strings.HasPrefix(rmsg.Channel, "D") {
			u.MsgSpoofUser(spoofUsername, m)
		} else {
			if ghost != nil && ghost.DisplayName != "" && ghost.DisplayName != ghost.Nick &&
				u.MmInfo.Cfg.SlackSettings.UseDisplayName {
				m = "<" + ghost.DisplayName + "> " + m
			}
			ch.SpoofMessage(spoofUsername, m)
		}
	}
}

// sync IRC with mattermost channel state
func (u *User) syncSlackChannel(id string, name string) {
	srv := u.Srv
	info, err := u.sc.GetChannelInfo(id)
	if err != nil {
		logger.Info(err)
	}

	for _, user := range info.Members {
		if u.sinfo.User.ID != user {
			//slackuser, _ := u.sc.GetUserInfo(user)
			slackuser := u.getSlackUser(user)
			if slackuser != nil {
				u.addSlackUserToChannel(slackuser, "#"+name, id)
			}
		}
	}

	ch := srv.Channel(id)
	ch.Topic(u, info.Topic.Value)
	if !ch.HasUser(u) {
		logger.Debugf("syncSlackchannel adding myself to %s (id: %s)", name, id)
		ch.Join(u)
	}
}

// sync IRC with mattermost channel state
func (u *User) syncSlackGroup(id string, name string) {
	srv := u.Srv
	info, err := u.sc.GetGroupInfo(id)
	if err != nil {
		logger.Info(err)
	}

	for _, user := range info.Members {
		if u.sinfo.User.ID != user {
			//slackuser, _ := u.sc.GetUserInfo(user)
			slackuser := u.getSlackUser(user)
			if slackuser != nil {
				u.addSlackUserToChannel(slackuser, "#"+name, id)
			}
		}
	}

	ch := srv.Channel(id)
	ch.Topic(u, info.Topic.Value)
	if !ch.HasUser(u) {
		logger.Debugf("syncSlackchannel adding myself to %s (id: %s)", name, id)
		ch.Join(u)
	}
}

// @see https://api.slack.com/docs/message-formatting#linking_to_channels_and_users
func (u *User) replaceMention(text string) string {
	results := regexp.MustCompile(`<@([a-zA-z0-9]+)>`).FindAllStringSubmatch(text, -1)
	for _, r := range results {
		text = strings.Replace(text, "<@"+r[1]+">", "@"+u.userName(r[1]), -1)
	}
	return text
}

// @see https://api.slack.com/docs/message-formatting#linking_to_channels_and_users
func (u *User) replaceChannel(text string) string {
	results := regexp.MustCompile(`<#[a-zA-Z0-9]+\|(.+?)>`).FindAllStringSubmatch(text, -1)
	for _, r := range results {
		text = strings.Replace(text, r[0], "#"+r[1], -1)
	}
	return text
}

// @see https://api.slack.com/docs/message-formatting#variables
func (u *User) replaceVariable(text string) string {
	results := regexp.MustCompile(`<!((?:subteam\^)?[a-zA-Z0-9]+)(?:\|@?(.+?))?>`).FindAllStringSubmatch(text, -1)
	for _, r := range results {
		if r[2] != "" {
			text = strings.Replace(text, r[0], "@"+r[2], -1)
		} else {
			text = strings.Replace(text, r[0], "@"+r[1], -1)
		}
	}
	return text
}

// @see https://api.slack.com/docs/message-formatting#linking_to_urls
func (u *User) replaceURL(text string) string {
	results := regexp.MustCompile(`<(.*?)(\|.*?)?>`).FindAllStringSubmatch(text, -1)
	for _, r := range results {
		text = strings.Replace(text, r[0], r[1], -1)
	}
	return text
}

func (u *User) getSlackUser(name string) *slack.User {
	u.RLock()
	defer u.RUnlock()
	if user, ok := u.susers[name]; ok {
		return &user
	}
	return nil
}

func (u *User) userName(id string) string {
	u.RLock()
	defer u.RUnlock()
	// TODO dynamically update when new users are joining slack
	for _, us := range u.susers {
		if us.ID == id {
			if us.Profile.DisplayName != "" {
				return us.Profile.DisplayName
			}
			return us.Name
		}
	}
	if id == u.sinfo.User.ID {
		return u.sinfo.User.Name
	}
	return ""
}

func (u *User) isConnected() bool {
	return u.connected
}
<|endoftext|>


<commit_before>// Copyright 2013 Google Inc.  All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package pretty pretty-prints Go structures.
//
// This package uses reflection to examine a Go value and can
// print out in a nice, aligned fashion.  It supports three
// modes (normal, compact, and extended) for advanced use.
//
// See the Reflect and Print examples for what the output looks like.
package pretty

// TODO:
//   - Catch cycles
<commit_msg>doc: remove completed TODO<commit_after>// Copyright 2013 Google Inc.  All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package pretty pretty-prints Go structures.
//
// This package uses reflection to examine a Go value and can
// print out in a nice, aligned fashion.  It supports three
// modes (normal, compact, and extended) for advanced use.
//
// See the Reflect and Print examples for what the output looks like.
package pretty
<|endoftext|>