<commit_before>package fire

import (
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/ory-am/fosite"
	"github.com/ory-am/fosite/handler/core"
	"github.com/ory-am/fosite/handler/core/client"
	"github.com/ory-am/fosite/handler/core/implicit"
	"github.com/ory-am/fosite/handler/core/owner"
	"github.com/ory-am/fosite/handler/core/strategy"
	"github.com/ory-am/fosite/token/hmac"
	"gopkg.in/mgo.v2"
)

var callbackTemplate = []byte(`<!DOCTYPE html>
<html>
  <head>
    <title>Authorize</title>
    <script>
      window.opener.fireCallback(window.location.hash);
      window.close();
    </script>
  </head>
</html>`)

// AccessToken is the internal model used to stored access tokens. The model
// can be mounted as a fire Resource to become manageable via the API.
type AccessToken struct {
	Base          `bson:",inline" fire:"access-token:access-tokens:access_tokens"`
	Signature     string    `json:"-" valid:"required"`
	RequestedAt   time.Time `json:"requested-at" valid:"required" bson:"requested_at"`
	GrantedScopes []string  `json:"granted-scopes" valid:"required" bson:"granted_scopes"`
}

var accessTokenModel *AccessToken

func init() {
	accessTokenModel = &AccessToken{}
	Init(accessTokenModel)
}

// A Authenticator provides OAuth2 based authentication. The implementation
// currently supports the Resource Owner Credentials, Client Credentials and
// Implicit Grant flows. The flows can be enabled using their respective methods.
type Authenticator struct {
	storage *authenticatorStorage

	strategy     *strategy.HMACSHAStrategy
	handleHelper *core.HandleHelper
	fosite       *fosite.Fosite
}

// NetAuthenticator creates and returns a new Authenticator.
func NewAuthenticator(db *mgo.Database, ownerModel, clientModel Model, secret string) *Authenticator {
	// initialize models
	Init(ownerModel)
	Init(clientModel)

	// extract attributes from owner
	ownerIdentifiable := ownerModel.getBase().attributesByTag("identifiable")
	if len(ownerIdentifiable) != 1 {
		panic("expected to find exactly one 'identifiable' tag on model")
	}
	ownerVerifiable := ownerModel.getBase().attributesByTag("verifiable")
	if len(ownerVerifiable) != 1 {
		panic("expected to find exactly one 'verifiable' tag on model")
	}

	// extract attributes from client
	clientIdentifiable := clientModel.getBase().attributesByTag("identifiable")
	if len(clientIdentifiable) != 1 {
		panic("expected to find exactly one 'identifiable' tag on model")
	}
	clientVerifiable := clientModel.getBase().attributesByTag("verifiable")
	if len(clientVerifiable) != 1 {
		panic("expected to find exactly one 'verifiable' tag on model")
	}
	clientCallable := clientModel.getBase().attributesByTag("callable")
	if len(clientCallable) != 1 {
		panic("expected to find exactly one 'callable' tag on model")
	}

	// create storage
	s := &authenticatorStorage{
		db:                 db,
		ownerModel:         ownerModel,
		ownerIDAttr:        ownerIdentifiable[0],
		ownerSecretAttr:    ownerVerifiable[0],
		clientModel:        clientModel,
		clientIDAttr:       clientIdentifiable[0],
		clientSecretAttr:   clientVerifiable[0],
		clientCallableAttr: clientCallable[0],
	}

	// set the default token lifespan to one hour
	tokenLifespan := time.Hour

	// create a new token generation strategy
	strategy := &strategy.HMACSHAStrategy{
		Enigma: &hmac.HMACStrategy{
			GlobalSecret: []byte(secret),
		},
		AccessTokenLifespan:   tokenLifespan,
		AuthorizeCodeLifespan: tokenLifespan,
	}

	// instantiate a new fosite instance
	f := fosite.NewFosite(s)

	// set mandatory scope
	f.MandatoryScope = "fire"

	// this little helper is used by some of the handlers later
	handleHelper := &core.HandleHelper{
		AccessTokenStrategy: strategy,
		AccessTokenStorage:  s,
		AccessTokenLifespan: tokenLifespan,
	}

	// add a request validator for access tokens to fosite
	f.AuthorizedRequestValidators.Append(&core.CoreValidator{
		AccessTokenStrategy: strategy,
		AccessTokenStorage:  s,
	})

	return &Authenticator{
		storage:      s,
		fosite:       f,
		handleHelper: handleHelper,
		strategy:     strategy,
	}
}

// EnablePasswordGrant enables the usage of the OAuth 2.0 Resource Owner Password
// Credentials Grant.
//
// Note: This method should only be called once.
func (a *Authenticator) EnablePasswordGrant() {
	// create handler
	passwordHandler := &owner.ResourceOwnerPasswordCredentialsGrantHandler{
		HandleHelper:                                 a.handleHelper,
		ResourceOwnerPasswordCredentialsGrantStorage: a.storage,
	}

	// add handler to fosite
	a.fosite.TokenEndpointHandlers.Append(passwordHandler)
}

// EnableCredentialsGrant enables the usage of the OAuth 2.0 Client Credentials Grant.
//
// Note: This method should only be called once.
func (a *Authenticator) EnableCredentialsGrant() {
	// create handler
	credentialsHandler := &client.ClientCredentialsGrantHandler{
		HandleHelper: a.handleHelper,
	}

	// add handler to fosite
	a.fosite.TokenEndpointHandlers.Append(credentialsHandler)
}

// EnableImplicitGrant enables the usage of the OAuth 2.0 Implicit Grant.
//
// Note: This method should only be called once.
func (a *Authenticator) EnableImplicitGrant() {
	// create handler
	implicitHandler := &implicit.AuthorizeImplicitGrantTypeHandler{
		AccessTokenStrategy: a.handleHelper.AccessTokenStrategy,
		AccessTokenStorage:  a.handleHelper.AccessTokenStorage,
		AccessTokenLifespan: a.handleHelper.AccessTokenLifespan,
	}

	// add handler to fosite
	a.fosite.AuthorizeEndpointHandlers.Append(implicitHandler)
}

// HashPassword returns an Authenticator compatible hash of the password.
func (a *Authenticator) HashPassword(password string) ([]byte, error) {
	return a.fosite.Hasher.Hash([]byte(password))
}

// MustHashPassword is the same as HashPassword except that it raises and error
// when the hashing failed.
func (a *Authenticator) MustHashPassword(password string) []byte {
	bytes, err := a.HashPassword(password)
	if err != nil {
		panic(err)
	}

	return bytes
}

// Register will create all necessary routes on the passed router. If want to
// prefix the auth endpoint (e.g. /auth/) you need to pass it to Register.
//
// Note: This functions should only be called once after enabling all flows.
func (a *Authenticator) Register(prefix string, router gin.IRouter) {
	router.POST(prefix+"/token", a.tokenEndpoint)
	router.POST(prefix+"/authorize", a.authorizeEndpoint)
	router.GET(prefix+"/callback", a.callbackEndpoint)

	// TODO: Redirect to auxiliary Login form.
}

// Authorizer returns a callback that can be used to protect resources by requiring
// an access tokens with the provides scopes to be granted.
func (a *Authenticator) Authorizer() Callback {
	// TODO: Add scopes.

	return func(ctx *Context) (error, error) {
		// prepare fosite
		f := fosite.NewContext()
		session := &strategy.HMACSession{}

		// validate request
		_, err := a.fosite.ValidateRequestAuthorization(f, ctx.GinContext.Request, session, "fire")
		if err != nil {
			return err, nil
		}

		return nil, nil
	}
}

func (a *Authenticator) tokenEndpoint(ctx *gin.Context) {
	// create new context
	f := fosite.NewContext()

	// create new session
	s := &strategy.HMACSession{}

	// obtain access request
	req, err := a.fosite.NewAccessRequest(f, ctx.Request, s)
	if err != nil {
		a.fosite.WriteAccessError(ctx.Writer, req, err)
		return
	}

	// grant the mandatory scope
	if req.GetScopes().Has("fire") {
		req.GrantScope("fire")
	}

	// obtain access response
	res, err := a.fosite.NewAccessResponse(f, ctx.Request, req)
	if err != nil {
		a.fosite.WriteAccessError(ctx.Writer, req, err)
		return
	}

	// write response
	a.fosite.WriteAccessResponse(ctx.Writer, req, res)
}

func (a *Authenticator) authorizeEndpoint(ctx *gin.Context) {
	// create new context
	f := fosite.NewContext()

	// obtain authorize request
	req, err := a.fosite.NewAuthorizeRequest(f, ctx.Request)
	if err != nil {
		a.fosite.WriteAuthorizeError(ctx.Writer, req, err)
		return
	}

	// get credentials
	username := ctx.Request.Form.Get("username")
	password := ctx.Request.Form.Get("password")

	// authenticate user
	err = a.storage.Authenticate(f, username, password)
	if err != nil {
		uri := ctx.Request.Referer() + "&error=invalid_credentials"
		ctx.Redirect(http.StatusTemporaryRedirect, uri)
		return
	}

	// create new session
	s := &strategy.HMACSession{}

	// obtain authorize response
	res, err := a.fosite.NewAuthorizeResponse(ctx, ctx.Request, req, s)
	if err != nil {
		a.fosite.WriteAuthorizeError(ctx.Writer, req, err)
		return
	}

	// write response
	a.fosite.WriteAuthorizeResponse(ctx.Writer, req, res)
}

func (a *Authenticator) callbackEndpoint(ctx *gin.Context) {
	// TODO: Remove callback endpoint.

	ctx.Writer.Write(callbackTemplate)
}
<commit_msg>properly log errors<commit_after>package fire

import (
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/ory-am/fosite"
	"github.com/ory-am/fosite/handler/core"
	"github.com/ory-am/fosite/handler/core/client"
	"github.com/ory-am/fosite/handler/core/implicit"
	"github.com/ory-am/fosite/handler/core/owner"
	"github.com/ory-am/fosite/handler/core/strategy"
	"github.com/ory-am/fosite/token/hmac"
	"gopkg.in/mgo.v2"
)

var callbackTemplate = []byte(`<!DOCTYPE html>
<html>
  <head>
    <title>Authorize</title>
    <script>
      window.opener.fireCallback(window.location.hash);
      window.close();
    </script>
  </head>
</html>`)

// AccessToken is the internal model used to stored access tokens. The model
// can be mounted as a fire Resource to become manageable via the API.
type AccessToken struct {
	Base          `bson:",inline" fire:"access-token:access-tokens:access_tokens"`
	Signature     string    `json:"-" valid:"required"`
	RequestedAt   time.Time `json:"requested-at" valid:"required" bson:"requested_at"`
	GrantedScopes []string  `json:"granted-scopes" valid:"required" bson:"granted_scopes"`
}

var accessTokenModel *AccessToken

func init() {
	accessTokenModel = &AccessToken{}
	Init(accessTokenModel)
}

// A Authenticator provides OAuth2 based authentication. The implementation
// currently supports the Resource Owner Credentials, Client Credentials and
// Implicit Grant flows. The flows can be enabled using their respective methods.
type Authenticator struct {
	storage *authenticatorStorage

	strategy     *strategy.HMACSHAStrategy
	handleHelper *core.HandleHelper
	fosite       *fosite.Fosite
}

// NetAuthenticator creates and returns a new Authenticator.
func NewAuthenticator(db *mgo.Database, ownerModel, clientModel Model, secret string) *Authenticator {
	// initialize models
	Init(ownerModel)
	Init(clientModel)

	// extract attributes from owner
	ownerIdentifiable := ownerModel.getBase().attributesByTag("identifiable")
	if len(ownerIdentifiable) != 1 {
		panic("expected to find exactly one 'identifiable' tag on model")
	}
	ownerVerifiable := ownerModel.getBase().attributesByTag("verifiable")
	if len(ownerVerifiable) != 1 {
		panic("expected to find exactly one 'verifiable' tag on model")
	}

	// extract attributes from client
	clientIdentifiable := clientModel.getBase().attributesByTag("identifiable")
	if len(clientIdentifiable) != 1 {
		panic("expected to find exactly one 'identifiable' tag on model")
	}
	clientVerifiable := clientModel.getBase().attributesByTag("verifiable")
	if len(clientVerifiable) != 1 {
		panic("expected to find exactly one 'verifiable' tag on model")
	}
	clientCallable := clientModel.getBase().attributesByTag("callable")
	if len(clientCallable) != 1 {
		panic("expected to find exactly one 'callable' tag on model")
	}

	// create storage
	s := &authenticatorStorage{
		db:                 db,
		ownerModel:         ownerModel,
		ownerIDAttr:        ownerIdentifiable[0],
		ownerSecretAttr:    ownerVerifiable[0],
		clientModel:        clientModel,
		clientIDAttr:       clientIdentifiable[0],
		clientSecretAttr:   clientVerifiable[0],
		clientCallableAttr: clientCallable[0],
	}

	// set the default token lifespan to one hour
	tokenLifespan := time.Hour

	// create a new token generation strategy
	strategy := &strategy.HMACSHAStrategy{
		Enigma: &hmac.HMACStrategy{
			GlobalSecret: []byte(secret),
		},
		AccessTokenLifespan:   tokenLifespan,
		AuthorizeCodeLifespan: tokenLifespan,
	}

	// instantiate a new fosite instance
	f := fosite.NewFosite(s)

	// set mandatory scope
	f.MandatoryScope = "fire"

	// this little helper is used by some of the handlers later
	handleHelper := &core.HandleHelper{
		AccessTokenStrategy: strategy,
		AccessTokenStorage:  s,
		AccessTokenLifespan: tokenLifespan,
	}

	// add a request validator for access tokens to fosite
	f.AuthorizedRequestValidators.Append(&core.CoreValidator{
		AccessTokenStrategy: strategy,
		AccessTokenStorage:  s,
	})

	return &Authenticator{
		storage:      s,
		fosite:       f,
		handleHelper: handleHelper,
		strategy:     strategy,
	}
}

// EnablePasswordGrant enables the usage of the OAuth 2.0 Resource Owner Password
// Credentials Grant.
//
// Note: This method should only be called once.
func (a *Authenticator) EnablePasswordGrant() {
	// create handler
	passwordHandler := &owner.ResourceOwnerPasswordCredentialsGrantHandler{
		HandleHelper:                                 a.handleHelper,
		ResourceOwnerPasswordCredentialsGrantStorage: a.storage,
	}

	// add handler to fosite
	a.fosite.TokenEndpointHandlers.Append(passwordHandler)
}

// EnableCredentialsGrant enables the usage of the OAuth 2.0 Client Credentials Grant.
//
// Note: This method should only be called once.
func (a *Authenticator) EnableCredentialsGrant() {
	// create handler
	credentialsHandler := &client.ClientCredentialsGrantHandler{
		HandleHelper: a.handleHelper,
	}

	// add handler to fosite
	a.fosite.TokenEndpointHandlers.Append(credentialsHandler)
}

// EnableImplicitGrant enables the usage of the OAuth 2.0 Implicit Grant.
//
// Note: This method should only be called once.
func (a *Authenticator) EnableImplicitGrant() {
	// create handler
	implicitHandler := &implicit.AuthorizeImplicitGrantTypeHandler{
		AccessTokenStrategy: a.handleHelper.AccessTokenStrategy,
		AccessTokenStorage:  a.handleHelper.AccessTokenStorage,
		AccessTokenLifespan: a.handleHelper.AccessTokenLifespan,
	}

	// add handler to fosite
	a.fosite.AuthorizeEndpointHandlers.Append(implicitHandler)
}

// HashPassword returns an Authenticator compatible hash of the password.
func (a *Authenticator) HashPassword(password string) ([]byte, error) {
	return a.fosite.Hasher.Hash([]byte(password))
}

// MustHashPassword is the same as HashPassword except that it raises and error
// when the hashing failed.
func (a *Authenticator) MustHashPassword(password string) []byte {
	bytes, err := a.HashPassword(password)
	if err != nil {
		panic(err)
	}

	return bytes
}

// Register will create all necessary routes on the passed router. If want to
// prefix the auth endpoint (e.g. /auth/) you need to pass it to Register.
//
// Note: This functions should only be called once after enabling all flows.
func (a *Authenticator) Register(prefix string, router gin.IRouter) {
	router.POST(prefix+"/token", a.tokenEndpoint)
	router.POST(prefix+"/authorize", a.authorizeEndpoint)
	router.GET(prefix+"/callback", a.callbackEndpoint)

	// TODO: Redirect to auxiliary Login form.
}

// Authorizer returns a callback that can be used to protect resources by requiring
// an access tokens with the provides scopes to be granted.
func (a *Authenticator) Authorizer() Callback {
	// TODO: Add scopes.

	return func(ctx *Context) (error, error) {
		// prepare fosite
		f := fosite.NewContext()
		session := &strategy.HMACSession{}

		// validate request
		_, err := a.fosite.ValidateRequestAuthorization(f, ctx.GinContext.Request, session, "fire")
		if err != nil {
			return err, nil
		}

		return nil, nil
	}
}

func (a *Authenticator) tokenEndpoint(ctx *gin.Context) {
	// create new context
	f := fosite.NewContext()

	// create new session
	s := &strategy.HMACSession{}

	// obtain access request
	req, err := a.fosite.NewAccessRequest(f, ctx.Request, s)
	if err != nil {
		ctx.Error(err)
		a.fosite.WriteAccessError(ctx.Writer, req, err)
		return
	}

	// grant the mandatory scope
	if req.GetScopes().Has("fire") {
		req.GrantScope("fire")
	}

	// obtain access response
	res, err := a.fosite.NewAccessResponse(f, ctx.Request, req)
	if err != nil {
		ctx.Error(err)
		a.fosite.WriteAccessError(ctx.Writer, req, err)
		return
	}

	// write response
	a.fosite.WriteAccessResponse(ctx.Writer, req, res)
}

func (a *Authenticator) authorizeEndpoint(ctx *gin.Context) {
	// create new context
	f := fosite.NewContext()

	// obtain authorize request
	req, err := a.fosite.NewAuthorizeRequest(f, ctx.Request)
	if err != nil {
		ctx.Error(err)
		a.fosite.WriteAuthorizeError(ctx.Writer, req, err)
		return
	}

	// get credentials
	username := ctx.Request.Form.Get("username")
	password := ctx.Request.Form.Get("password")

	// authenticate user
	err = a.storage.Authenticate(f, username, password)
	if err != nil {
		uri := ctx.Request.Referer() + "&error=invalid_credentials"
		ctx.Redirect(http.StatusTemporaryRedirect, uri)
		return
	}

	// create new session
	s := &strategy.HMACSession{}

	// obtain authorize response
	res, err := a.fosite.NewAuthorizeResponse(ctx, ctx.Request, req, s)
	if err != nil {
		ctx.Error(err)
		a.fosite.WriteAuthorizeError(ctx.Writer, req, err)
		return
	}

	// write response
	a.fosite.WriteAuthorizeResponse(ctx.Writer, req, res)
}

func (a *Authenticator) callbackEndpoint(ctx *gin.Context) {
	// TODO: Remove callback endpoint.

	ctx.Writer.Write(callbackTemplate)
}
<|endoftext|>


<commit_before>package plugins

import (
	"sort"
	"strings"

	"net/http"
	"time"

	"context"

	"github.com/Seklfreak/Robyul2/cache"
	"github.com/Seklfreak/Robyul2/helpers"
	"github.com/bwmarrin/discordgo"
	"github.com/dustin/go-humanize"
	"github.com/lucazulian/cryptocomparego"
	"github.com/sirupsen/logrus"
)

type cryptoAction func(args []string, in *discordgo.Message, out **discordgo.MessageSend) (next cryptoAction)

type Crypto struct {
	cryptoClient *cryptocomparego.Client
}

func (m *Crypto) Commands() []string {
	return []string{
		"crypto",
		"cryptocurrency",
	}
}

func (m *Crypto) Init(session *discordgo.Session) {
	// init crypto client
	m.cryptoClient = cryptocomparego.NewClient(&http.Client{
		Timeout: time.Second * 15,
	})
}

func (m *Crypto) Action(command string, content string, msg *discordgo.Message, session *discordgo.Session) {
	if !helpers.ModuleIsAllowed(msg.ChannelID, msg.ID, msg.Author.ID, helpers.ModulePermCrypto) {
		return
	}

	session.ChannelTyping(msg.ChannelID)

	var result *discordgo.MessageSend
	args := strings.Fields(content)

	action := m.actionStart
	for action != nil {
		action = action(args, msg, &result)
	}
}

func (m *Crypto) actionStart(args []string, in *discordgo.Message, out **discordgo.MessageSend) cryptoAction {
	cache.GetSession().ChannelTyping(in.ChannelID)

	return m.actionExchange
}

// [p]crypto [<from, eg BTC,BCH>] [<to, eg USD,EUR>]
func (m *Crypto) actionExchange(args []string, in *discordgo.Message, out **discordgo.MessageSend) cryptoAction {
	// default symbols
	fromSymbols := []string{"BTC", "BCH", "ETH", "LTC"}
	toSymbols := []string{"USD", "EUR", "KRW"}

	// parse custom symbols
	if len(args) >= 1 {
		fromSymbolsRaw := strings.Split(args[0], ",")
		if fromSymbolsRaw != nil && len(fromSymbolsRaw) > 0 {
			fromSymbols = make([]string, 0)
			for _, fromSymbolRaw := range fromSymbolsRaw {
				fromSymbols = append(fromSymbols, strings.ToUpper(strings.TrimSpace(fromSymbolRaw)))
			}
		}
	}
	if len(args) >= 2 {
		toSymbolsRaw := strings.Split(args[1], ",")
		if toSymbolsRaw != nil && len(toSymbolsRaw) > 0 {
			toSymbols = make([]string, 0)
			for _, toSymbolRaw := range toSymbolsRaw {
				toSymbols = append(toSymbols, strings.ToUpper(strings.TrimSpace(toSymbolRaw)))
			}
		}
	}

	// make exchange api request
	exchangeResults, _, err := m.cryptoClient.PriceMulti.List(context.Background(), &cryptocomparego.PriceMultiRequest{
		Fsyms:         fromSymbols,
		Tsyms:         toSymbols,
		ExtraParams:   helpers.DEFAULT_UA,
		TryConversion: true,
	})
	helpers.Relax(err)

	// setup embed
	exchangeEmbed := &discordgo.MessageEmbed{
		Title:     helpers.GetText("plugins.crypto.embed-exchange-title"),
		Timestamp: time.Now().Format(time.RFC3339),
		Color:     helpers.GetDiscordColorFromHex("2b5a98"),
		Footer: &discordgo.MessageEmbedFooter{
			Text:    helpers.GetText("plugins.crypto.embed-footer"),
			IconURL: helpers.GetText("plugins.crypto.embed-footer-imageurl"),
		},
		Fields: []*discordgo.MessageEmbedField{},
	}

	// custom sorting
	// USD before EUR before KRW before everything else
	// BTC before ETH before LTC before BCH before everything else
	sort.Slice(exchangeResults, func(i, j int) bool {
		if exchangeResults[i].Name == "USD" {
			return true
		}
		if exchangeResults[i].Name == "EUR" && exchangeResults[j].Name != "USD" {
			return true
		}
		if exchangeResults[i].Name == "KRW" && (exchangeResults[j].Name != "USD" && exchangeResults[j].Name != "KRW") {
			return true
		}
		if exchangeResults[i].Name == "BTC" {
			return true
		}
		if exchangeResults[i].Name == "ETH" && exchangeResults[j].Name != "BTC" {
			return true
		}
		if exchangeResults[i].Name == "LTC" && (exchangeResults[j].Name != "BTC" && exchangeResults[j].Name != "ETH") {
			return true
		}
		if exchangeResults[i].Name == "BCH" && (exchangeResults[j].Name != "BTC" && exchangeResults[j].Name != "ETH" && exchangeResults[j].Name != "LTC") {
			return true
		}
		return exchangeResults[i].Name < exchangeResults[j].Name
	})

	// add result
	for _, exchangeResult := range exchangeResults {
		var priceText string
		// custom sorting
		// USD before EUR before KRW before everything else
		// BTC before ETH before LTC before BCH before everything else
		sort.Slice(exchangeResult.Value, func(i, j int) bool {
			if exchangeResult.Value[i].Name == "USD" {
				return true
			}
			if exchangeResult.Value[i].Name == "EUR" && exchangeResult.Value[j].Name != "USD" {
				return true
			}
			if exchangeResult.Value[i].Name == "KRW" && (exchangeResult.Value[j].Name != "USD" && exchangeResult.Value[j].Name != "EUR") {
				return true
			}
			if exchangeResult.Value[i].Name == "BTC" {
				return true
			}
			if exchangeResult.Value[i].Name == "ETH" && exchangeResult.Value[j].Name != "BTC" {
				return true
			}
			if exchangeResult.Value[i].Name == "LTC" && (exchangeResult.Value[j].Name != "BTC" && exchangeResult.Value[j].Name != "ETH") {
				return true
			}
			if exchangeResult.Value[i].Name == "BCH" && (exchangeResult.Value[j].Name != "BTC" && exchangeResult.Value[j].Name != "ETH" && exchangeResult.Value[j].Name != "LTC") {
				return true
			}
			return exchangeResult.Value[i].Name < exchangeResult.Value[j].Name
		})
		// format prices
		for _, price := range exchangeResult.Value {
			priceText += price.Name + ": `" + humanize.FormatFloat("#,###.####", price.Value) + "`, "
		}
		priceText = strings.TrimRight(priceText, ", ")

		// add cryptocurrency
		newField := &discordgo.MessageEmbedField{
			Name:   "1 " + exchangeResult.Name,
			Value:  priceText,
			Inline: false,
		}
		exchangeEmbed.Fields = append(exchangeEmbed.Fields, newField)
	}

	// send result
	*out = &discordgo.MessageSend{
		Embed: exchangeEmbed,
	}
	return m.actionFinish
}

func (m *Crypto) actionFinish(args []string, in *discordgo.Message, out **discordgo.MessageSend) cryptoAction {
	_, err := helpers.SendComplex(in.ChannelID, *out)
	helpers.Relax(err)

	return nil
}

func (m *Crypto) newMsg(content string) *discordgo.MessageSend {
	return &discordgo.MessageSend{Content: helpers.GetText(content)}
}

func (m *Crypto) Relax(err error) {
	if err != nil {
		panic(err)
	}
}

func (m *Crypto) logger() *logrus.Entry {
	return cache.GetLogger().WithField("module", "crypto")
}
<commit_msg>[crypto] fixes ROBYUL-DISCORD-JB<commit_after>package plugins

import (
	"sort"
	"strings"

	"net/http"
	"time"

	"context"

	"github.com/Seklfreak/Robyul2/cache"
	"github.com/Seklfreak/Robyul2/helpers"
	"github.com/bwmarrin/discordgo"
	"github.com/dustin/go-humanize"
	"github.com/lucazulian/cryptocomparego"
	"github.com/sirupsen/logrus"
)

type cryptoAction func(args []string, in *discordgo.Message, out **discordgo.MessageSend) (next cryptoAction)

type Crypto struct {
	cryptoClient *cryptocomparego.Client
}

func (m *Crypto) Commands() []string {
	return []string{
		"crypto",
		"cryptocurrency",
	}
}

func (m *Crypto) Init(session *discordgo.Session) {
	// init crypto client
	m.cryptoClient = cryptocomparego.NewClient(&http.Client{
		Timeout: time.Second * 15,
	})
}

func (m *Crypto) Action(command string, content string, msg *discordgo.Message, session *discordgo.Session) {
	if !helpers.ModuleIsAllowed(msg.ChannelID, msg.ID, msg.Author.ID, helpers.ModulePermCrypto) {
		return
	}

	session.ChannelTyping(msg.ChannelID)

	var result *discordgo.MessageSend
	args := strings.Fields(content)

	action := m.actionStart
	for action != nil {
		action = action(args, msg, &result)
	}
}

func (m *Crypto) actionStart(args []string, in *discordgo.Message, out **discordgo.MessageSend) cryptoAction {
	cache.GetSession().ChannelTyping(in.ChannelID)

	return m.actionExchange
}

// [p]crypto [<from, eg BTC,BCH>] [<to, eg USD,EUR>]
func (m *Crypto) actionExchange(args []string, in *discordgo.Message, out **discordgo.MessageSend) cryptoAction {
	// default symbols
	fromSymbols := []string{"BTC", "BCH", "ETH", "LTC"}
	toSymbols := []string{"USD", "EUR", "KRW"}

	// parse custom symbols
	if len(args) >= 1 {
		fromSymbolsRaw := strings.Split(args[0], ",")
		if fromSymbolsRaw != nil && len(fromSymbolsRaw) > 0 {
			fromSymbols = make([]string, 0)
			for _, fromSymbolRaw := range fromSymbolsRaw {
				fromSymbols = append(fromSymbols, strings.ToUpper(strings.TrimSpace(fromSymbolRaw)))
			}
		}
	}
	if len(args) >= 2 {
		toSymbolsRaw := strings.Split(args[1], ",")
		if toSymbolsRaw != nil && len(toSymbolsRaw) > 0 {
			toSymbols = make([]string, 0)
			for _, toSymbolRaw := range toSymbolsRaw {
				toSymbols = append(toSymbols, strings.ToUpper(strings.TrimSpace(toSymbolRaw)))
			}
		}
	}

	// make exchange api request
	exchangeResults, _, err := m.cryptoClient.PriceMulti.List(context.Background(), &cryptocomparego.PriceMultiRequest{
		Fsyms:         fromSymbols,
		Tsyms:         toSymbols,
		ExtraParams:   helpers.DEFAULT_UA,
		TryConversion: true,
	})
	if err != nil {
		if strings.Contains(err.Error(), "There is no data for any of the") {
			*out = m.newMsg(helpers.GetText("bot.arguments.invalid"))
			return m.actionFinish
		}
	}
	helpers.Relax(err)

	// setup embed
	exchangeEmbed := &discordgo.MessageEmbed{
		Title:     helpers.GetText("plugins.crypto.embed-exchange-title"),
		Timestamp: time.Now().Format(time.RFC3339),
		Color:     helpers.GetDiscordColorFromHex("2b5a98"),
		Footer: &discordgo.MessageEmbedFooter{
			Text:    helpers.GetText("plugins.crypto.embed-footer"),
			IconURL: helpers.GetText("plugins.crypto.embed-footer-imageurl"),
		},
		Fields: []*discordgo.MessageEmbedField{},
	}

	// custom sorting
	// USD before EUR before KRW before everything else
	// BTC before ETH before LTC before BCH before everything else
	sort.Slice(exchangeResults, func(i, j int) bool {
		if exchangeResults[i].Name == "USD" {
			return true
		}
		if exchangeResults[i].Name == "EUR" && exchangeResults[j].Name != "USD" {
			return true
		}
		if exchangeResults[i].Name == "KRW" && (exchangeResults[j].Name != "USD" && exchangeResults[j].Name != "KRW") {
			return true
		}
		if exchangeResults[i].Name == "BTC" {
			return true
		}
		if exchangeResults[i].Name == "ETH" && exchangeResults[j].Name != "BTC" {
			return true
		}
		if exchangeResults[i].Name == "LTC" && (exchangeResults[j].Name != "BTC" && exchangeResults[j].Name != "ETH") {
			return true
		}
		if exchangeResults[i].Name == "BCH" && (exchangeResults[j].Name != "BTC" && exchangeResults[j].Name != "ETH" && exchangeResults[j].Name != "LTC") {
			return true
		}
		return exchangeResults[i].Name < exchangeResults[j].Name
	})

	// add result
	for _, exchangeResult := range exchangeResults {
		var priceText string
		// custom sorting
		// USD before EUR before KRW before everything else
		// BTC before ETH before LTC before BCH before everything else
		sort.Slice(exchangeResult.Value, func(i, j int) bool {
			if exchangeResult.Value[i].Name == "USD" {
				return true
			}
			if exchangeResult.Value[i].Name == "EUR" && exchangeResult.Value[j].Name != "USD" {
				return true
			}
			if exchangeResult.Value[i].Name == "KRW" && (exchangeResult.Value[j].Name != "USD" && exchangeResult.Value[j].Name != "EUR") {
				return true
			}
			if exchangeResult.Value[i].Name == "BTC" {
				return true
			}
			if exchangeResult.Value[i].Name == "ETH" && exchangeResult.Value[j].Name != "BTC" {
				return true
			}
			if exchangeResult.Value[i].Name == "LTC" && (exchangeResult.Value[j].Name != "BTC" && exchangeResult.Value[j].Name != "ETH") {
				return true
			}
			if exchangeResult.Value[i].Name == "BCH" && (exchangeResult.Value[j].Name != "BTC" && exchangeResult.Value[j].Name != "ETH" && exchangeResult.Value[j].Name != "LTC") {
				return true
			}
			return exchangeResult.Value[i].Name < exchangeResult.Value[j].Name
		})
		// format prices
		for _, price := range exchangeResult.Value {
			priceText += price.Name + ": `" + humanize.FormatFloat("#,###.####", price.Value) + "`, "
		}
		priceText = strings.TrimRight(priceText, ", ")

		// add cryptocurrency
		newField := &discordgo.MessageEmbedField{
			Name:   "1 " + exchangeResult.Name,
			Value:  priceText,
			Inline: false,
		}
		exchangeEmbed.Fields = append(exchangeEmbed.Fields, newField)
	}

	// send result
	*out = &discordgo.MessageSend{
		Embed: exchangeEmbed,
	}
	return m.actionFinish
}

func (m *Crypto) actionFinish(args []string, in *discordgo.Message, out **discordgo.MessageSend) cryptoAction {
	_, err := helpers.SendComplex(in.ChannelID, *out)
	helpers.Relax(err)

	return nil
}

func (m *Crypto) newMsg(content string) *discordgo.MessageSend {
	return &discordgo.MessageSend{Content: helpers.GetText(content)}
}

func (m *Crypto) Relax(err error) {
	if err != nil {
		panic(err)
	}
}

func (m *Crypto) logger() *logrus.Entry {
	return cache.GetLogger().WithField("module", "crypto")
}
<|endoftext|>


<commit_before>package plugins

import (
    "encoding/json"
    "fmt"
    "github.com/Seklfreak/Robyul2/helpers"
    "github.com/bwmarrin/discordgo"
    "net/http"
    "io/ioutil"
    "github.com/PuerkitoBio/goquery"
    "strings"
)

type Lyrics struct{}

func (l *Lyrics) Commands() []string {
    return []string{
        "lyrics",
    }
}

const (
    GeniusApiBaseUrl = "https://api.genius.com"
    GeniusBaseUrl = "https://genius.com"
)

type GeniusSearchResult struct {
    Meta struct {
        Status int `json:"status"`
    } `json:"meta"`
    Response struct {
        Hits []struct {
            Highlights []interface{} `json:"highlights"`
            Index string `json:"index"`
            Type string `json:"type"`
            Result struct {
                AnnotationCount int `json:"annotation_count"`
                APIPath string `json:"api_path"`
                FullTitle string `json:"full_title"`
                HeaderImageThumbnailURL string `json:"header_image_thumbnail_url"`
                HeaderImageURL string `json:"header_image_url"`
                ID int `json:"id"`
                LyricsOwnerID int `json:"lyrics_owner_id"`
                LyricsState string `json:"lyrics_state"`
                Path string `json:"path"`
                PyongsCount int `json:"pyongs_count"`
                SongArtImageThumbnailURL string `json:"song_art_image_thumbnail_url"`
                Stats struct {
                    Hot bool `json:"hot"`
                    UnreviewedAnnotations int `json:"unreviewed_annotations"`
                    Concurrents int `json:"concurrents"`
                    Pageviews int `json:"pageviews"`
                } `json:"stats"`
                Title string `json:"title"`
                TitleWithFeatured string `json:"title_with_featured"`
                URL string `json:"url"`
                PrimaryArtist struct {
                    APIPath string `json:"api_path"`
                    HeaderImageURL string `json:"header_image_url"`
                    ID int `json:"id"`
                    ImageURL string `json:"image_url"`
                    IsMemeVerified bool `json:"is_meme_verified"`
                    IsVerified bool `json:"is_verified"`
                    Name string `json:"name"`
                    URL string `json:"url"`
                } `json:"primary_artist"`
            } `json:"result"`
        } `json:"hits"`
    } `json:"response"`
}

func (l *Lyrics) Init(session *discordgo.Session) {

}

func (l *Lyrics) Action(command string, content string, msg *discordgo.Message, session *discordgo.Session) {
    session.ChannelTyping(msg.ChannelID)

    listSongs := false
    content = strings.TrimSpace(content)
    if strings.HasPrefix(content, "list") {
        listSongs = true
        content = strings.TrimSpace(strings.TrimLeft(content, "list"))
    }

    encodedQuery, err := helpers.UrlEncode(content)
    helpers.Relax(err)

    var searchResult GeniusSearchResult
    err = l.GeniusRequest(fmt.Sprintf("/search?q=%s", encodedQuery), &searchResult)

    if searchResult.Meta.Status != 200 {
        _, err := session.ChannelMessageSend(msg.ChannelID, helpers.GetText("plugins.lyrics.genius-api-error"))
        helpers.Relax(err)
        return
    }

    if len(searchResult.Response.Hits) <= 0 {
        _, err := session.ChannelMessageSend(msg.ChannelID, helpers.GetText("plugins.lyrics.genius-no-results"))
        helpers.Relax(err)
        return
    }

    hitI := -1
    for i, hit := range searchResult.Response.Hits {
        if hit.Type == "song" {
            hitI = i
            break
        }
    }

    if hitI == -1 {
        _, err := session.ChannelMessageSend(msg.ChannelID, helpers.GetText("plugins.lyrics.genius-no-results"))
        helpers.Relax(err)
        return
    }

    if listSongs == true {
        songList := ""
        for i, hit := range searchResult.Response.Hits {
            if hit.Type == "song" {
                songList += fmt.Sprintf("[%s](%s)\n", hit.Result.FullTitle, hit.Result.URL)
            }
            if i >= 4 {
                break
            }
        }
        songListEmbed := &discordgo.MessageEmbed{
            Title:       helpers.GetTextF("plugins.lyrics.song-list-embed-title", content),
            Footer:      &discordgo.MessageEmbedFooter{Text: helpers.GetText("plugins.lyrics.powered-by")},
            Description: songList,
        }

        _, err := session.ChannelMessageSendEmbed(msg.ChannelID, songListEmbed)
        helpers.Relax(err)
        return
    }

    hit := searchResult.Response.Hits[hitI]
    lyrics, err := l.GeniusScrapeLyrics(hit.Result.APIPath)
    helpers.Relax(err)

    result := fmt.Sprintf("__Lyrics for: **%s**__\n%s\n_%s_",
        hit.Result.FullTitle, lyrics, helpers.GetText("plugins.lyrics.powered-by"))

    for _, page := range helpers.Pagify(result, "\n") {
        _, err := session.ChannelMessageSend(msg.ChannelID, page)
        helpers.Relax(err)
    }
}

func (l *Lyrics) GeniusRequest(location string, object interface{}) error {
    requestUrl := GeniusApiBaseUrl+location
    client := &http.Client{}
    req, err := http.NewRequest("GET", requestUrl, nil)
    if err != nil {
        return err
    }
    req.Header.Add("User-Agent", helpers.DEFAULT_UA)
    req.Header.Add("Authorization", helpers.GetConfig().Path("genius.token").Data().(string))
    resp, err := client.Do(req)
    defer resp.Body.Close()
    if err != nil {
        return err
    }

    bytes, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return err
    }

    return json.Unmarshal(bytes, object)
}

func (l *Lyrics) GeniusScrapeLyrics(songApiPath string) (string, error) {
    requestUrl := GeniusBaseUrl+songApiPath

    doc, err := goquery.NewDocument(requestUrl)
    if err != nil {
        return "", err
    }

    lyrics := ""
    doc.Find("div.lyrics").Children().Each(func(_ int, s *goquery.Selection) {
        text := strings.TrimSpace(s.Text())
        if strings.Count(text, "\n") <= 1 {
            lyrics += "\n**" + text + "**\n"
        } else {
            lyrics += text + "\n"
        }
    })
    return lyrics, nil
}
<commit_msg>[lyrics] limit max length<commit_after>package plugins

import (
    "encoding/json"
    "fmt"
    "github.com/Seklfreak/Robyul2/helpers"
    "github.com/bwmarrin/discordgo"
    "net/http"
    "io/ioutil"
    "github.com/PuerkitoBio/goquery"
    "strings"
)

type Lyrics struct{}

func (l *Lyrics) Commands() []string {
    return []string{
        "lyrics",
    }
}

const (
    GeniusApiBaseUrl = "https://api.genius.com"
    GeniusBaseUrl = "https://genius.com"
)

type GeniusSearchResult struct {
    Meta struct {
        Status int `json:"status"`
    } `json:"meta"`
    Response struct {
        Hits []struct {
            Highlights []interface{} `json:"highlights"`
            Index string `json:"index"`
            Type string `json:"type"`
            Result struct {
                AnnotationCount int `json:"annotation_count"`
                APIPath string `json:"api_path"`
                FullTitle string `json:"full_title"`
                HeaderImageThumbnailURL string `json:"header_image_thumbnail_url"`
                HeaderImageURL string `json:"header_image_url"`
                ID int `json:"id"`
                LyricsOwnerID int `json:"lyrics_owner_id"`
                LyricsState string `json:"lyrics_state"`
                Path string `json:"path"`
                PyongsCount int `json:"pyongs_count"`
                SongArtImageThumbnailURL string `json:"song_art_image_thumbnail_url"`
                Stats struct {
                    Hot bool `json:"hot"`
                    UnreviewedAnnotations int `json:"unreviewed_annotations"`
                    Concurrents int `json:"concurrents"`
                    Pageviews int `json:"pageviews"`
                } `json:"stats"`
                Title string `json:"title"`
                TitleWithFeatured string `json:"title_with_featured"`
                URL string `json:"url"`
                PrimaryArtist struct {
                    APIPath string `json:"api_path"`
                    HeaderImageURL string `json:"header_image_url"`
                    ID int `json:"id"`
                    ImageURL string `json:"image_url"`
                    IsMemeVerified bool `json:"is_meme_verified"`
                    IsVerified bool `json:"is_verified"`
                    Name string `json:"name"`
                    URL string `json:"url"`
                } `json:"primary_artist"`
            } `json:"result"`
        } `json:"hits"`
    } `json:"response"`
}

func (l *Lyrics) Init(session *discordgo.Session) {

}

func (l *Lyrics) Action(command string, content string, msg *discordgo.Message, session *discordgo.Session) {
    session.ChannelTyping(msg.ChannelID)

    listSongs := false
    content = strings.TrimSpace(content)
    if strings.HasPrefix(content, "list") {
        listSongs = true
        content = strings.TrimSpace(strings.TrimLeft(content, "list"))
    }

    encodedQuery, err := helpers.UrlEncode(content)
    helpers.Relax(err)

    var searchResult GeniusSearchResult
    err = l.GeniusRequest(fmt.Sprintf("/search?q=%s", encodedQuery), &searchResult)

    if searchResult.Meta.Status != 200 {
        _, err := session.ChannelMessageSend(msg.ChannelID, helpers.GetText("plugins.lyrics.genius-api-error"))
        helpers.Relax(err)
        return
    }

    if len(searchResult.Response.Hits) <= 0 {
        _, err := session.ChannelMessageSend(msg.ChannelID, helpers.GetText("plugins.lyrics.genius-no-results"))
        helpers.Relax(err)
        return
    }

    hitI := -1
    for i, hit := range searchResult.Response.Hits {
        if hit.Type == "song" {
            hitI = i
            break
        }
    }

    if hitI == -1 {
        _, err := session.ChannelMessageSend(msg.ChannelID, helpers.GetText("plugins.lyrics.genius-no-results"))
        helpers.Relax(err)
        return
    }

    if listSongs == true {
        songList := ""
        for i, hit := range searchResult.Response.Hits {
            if hit.Type == "song" {
                songList += fmt.Sprintf("[%s](%s)\n", hit.Result.FullTitle, hit.Result.URL)
            }
            if i >= 4 {
                break
            }
        }
        songListEmbed := &discordgo.MessageEmbed{
            Title:       helpers.GetTextF("plugins.lyrics.song-list-embed-title", content),
            Footer:      &discordgo.MessageEmbedFooter{Text: helpers.GetText("plugins.lyrics.powered-by")},
            Description: songList,
        }

        _, err := session.ChannelMessageSendEmbed(msg.ChannelID, songListEmbed)
        helpers.Relax(err)
        return
    }

    hit := searchResult.Response.Hits[hitI]
    lyrics, err := l.GeniusScrapeLyrics(hit.Result.APIPath)
    helpers.Relax(err)

    result := fmt.Sprintf("__Lyrics for: **%s**__\n%s\n_%s_",
        hit.Result.FullTitle, lyrics, helpers.GetText("plugins.lyrics.powered-by"))

    for i, page := range helpers.Pagify(result, "\n") {
        if i >= 2 {
            _, err := session.ChannelMessageSend(msg.ChannelID, fmt.Sprintf("More on <%s>", hit.Result.URL))
            helpers.Relax(err)
            break
        }
        _, err := session.ChannelMessageSend(msg.ChannelID, page)
        helpers.Relax(err)
    }
}

func (l *Lyrics) GeniusRequest(location string, object interface{}) error {
    requestUrl := GeniusApiBaseUrl+location
    client := &http.Client{}
    req, err := http.NewRequest("GET", requestUrl, nil)
    if err != nil {
        return err
    }
    req.Header.Add("User-Agent", helpers.DEFAULT_UA)
    req.Header.Add("Authorization", helpers.GetConfig().Path("genius.token").Data().(string))
    resp, err := client.Do(req)
    defer resp.Body.Close()
    if err != nil {
        return err
    }

    bytes, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return err
    }

    return json.Unmarshal(bytes, object)
}

func (l *Lyrics) GeniusScrapeLyrics(songApiPath string) (string, error) {
    requestUrl := GeniusBaseUrl+songApiPath

    doc, err := goquery.NewDocument(requestUrl)
    if err != nil {
        return "", err
    }

    lyrics := ""
    doc.Find("div.lyrics").Children().Each(func(_ int, s *goquery.Selection) {
        text := strings.TrimSpace(s.Text())
        if strings.Count(text, "\n") <= 1 {
            lyrics += "\n**" + text + "**\n"
        } else {
            lyrics += text + "\n"
        }
    })
    return lyrics, nil
}
<|endoftext|>


<commit_before>package notify

import (
	"errors"
	"fmt"
	"log"
	"sync"

	"github.com/godbus/dbus/v5"
)

const (
	dbusRemoveMatch            = "org.freedesktop.DBus.RemoveMatch"
	dbusAddMatch               = "org.freedesktop.DBus.AddMatch"
	dbusObjectPath             = "/org/freedesktop/Notifications" // the DBUS object path
	dbusNotificationsInterface = "org.freedesktop.Notifications"  // DBUS Interface
	signalNotificationClosed   = "org.freedesktop.Notifications.NotificationClosed"
	signalActionInvoked        = "org.freedesktop.Notifications.ActionInvoked"
	callGetCapabilities        = "org.freedesktop.Notifications.GetCapabilities"
	callCloseNotification      = "org.freedesktop.Notifications.CloseNotification"
	callNotify                 = "org.freedesktop.Notifications.Notify"
	callGetServerInformation   = "org.freedesktop.Notifications.GetServerInformation"

	channelBufferSize = 2
)

// Notification holds all information needed for creating a notification
type Notification struct {
	AppName string
	// Setting ReplacesID atomically replaces the notification with this ID.
	// Optional.
	ReplacesID uint32
	// See predefined icons here: http://standards.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html
	// Optional.
	AppIcon string
	Summary string
	Body    string
	// Actions are tuples of (action_key, label), e.g.: []string{"cancel", "Cancel", "open", "Open"}
	Actions []string
	Hints   map[string]dbus.Variant
	// ExpireTimeout: milliseconds to show notification
	ExpireTimeout int32
}

// SendNotification is provided for convenience.
// Use if you only want to deliver a notification and dont care about events.
func SendNotification(conn *dbus.Conn, note Notification) (uint32, error) {
	actions := len(note.Actions)
	if (actions % 2) != 0 {
		return 0, errors.New("actions must be pairs of (key, label)")
	}

	obj := conn.Object(dbusNotificationsInterface, dbusObjectPath)
	call := obj.Call(callNotify, 0,
		note.AppName,
		note.ReplacesID,
		note.AppIcon,
		note.Summary,
		note.Body,
		note.Actions,
		note.Hints,
		note.ExpireTimeout)
	if call.Err != nil {
		return 0, fmt.Errorf("error sending notification: %w", call.Err)
	}
	var ret uint32
	err := call.Store(&ret)
	if err != nil {
		return ret, fmt.Errorf("error getting uint32 ret value: %w", err)
	}
	return ret, nil
}

// ServerInformation is a holder for information returned by
// GetServerInformation call.
type ServerInformation struct {
	Name        string
	Vendor      string
	Version     string
	SpecVersion string
}

// GetServerInformation returns the information on the server.
//
// org.freedesktop.Notifications.GetServerInformation
//
//  GetServerInformation Return Values
//
//		Name		 Type	  Description
//		name		 STRING	  The product name of the server.
//		vendor		 STRING	  The vendor name. For example, "KDE," "GNOME," "freedesktop.org," or "Microsoft."
//		version		 STRING	  The server's version number.
//		spec_version STRING	  The specification version the server is compliant with.
//
func GetServerInformation(conn *dbus.Conn) (ServerInformation, error) {
	obj := conn.Object(dbusNotificationsInterface, dbusObjectPath)
	if obj == nil {
		return ServerInformation{}, errors.New("error creating dbus call object")
	}
	call := obj.Call(callGetServerInformation, 0)
	if call.Err != nil {
		return ServerInformation{}, fmt.Errorf("error calling %v: %v", callGetServerInformation, call.Err)
	}

	ret := ServerInformation{}
	err := call.Store(&ret.Name, &ret.Vendor, &ret.Version, &ret.SpecVersion)
	if err != nil {
		return ret, fmt.Errorf("error reading %v return values: %v", callGetServerInformation, err)
	}
	return ret, nil
}

// GetCapabilities gets the capabilities of the notification server.
// This call takes no parameters.
// It returns an array of strings. Each string describes an optional capability implemented by the server.
//
// See also: https://developer.gnome.org/notification-spec/
// GetCapabilities provide an exported method for this operation
func GetCapabilities(conn *dbus.Conn) ([]string, error) {
	obj := conn.Object(dbusNotificationsInterface, dbusObjectPath)
	call := obj.Call(callGetCapabilities, 0)
	if call.Err != nil {
		return []string{}, call.Err
	}
	var ret []string
	err := call.Store(&ret)
	if err != nil {
		return ret, err
	}
	return ret, nil
}

// Notifier is an interface for implementing the operations supported by the
// freedesktop DBus Notifications object.
//
// New() sets up a Notifier that listens on dbus' signals regarding
// Notifications: NotificationClosed and ActionInvoked.
//
// Note this also means the caller MUST consume output from these channels,
// given in methods NotificationClosed() and ActionInvoked().
// Users that only want to send a simple notification, but don't care about
// interactions, see exported method: SendNotification(conn, Notification)
//
// Caller is also responsible to call Close() before exiting,
// to shut down event loop and cleanup.
type Notifier interface {
	SendNotification(n Notification) (uint32, error)
	GetCapabilities() ([]string, error)
	GetServerInformation() (ServerInformation, error)
	CloseNotification(id int) (bool, error)
	// Deprecated: Use NotificationClosedHandler
	//NotificationClosed() <-chan *NotificationClosedSignal
	//ActionInvoked() <-chan *ActionInvokedSignal
	Close() error
}

// NotificationClosedHandler is called when we receive a NotificationClosed signal
type NotificationClosedHandler func(*NotificationClosedSignal)

// ActionInvokedHandler is called when we receive a signal that one of the actions passed was invoked.
//
// Note that invoking an action often also produces a NotificationClosedSignal,
// so you might receive both a Closed signal and a ActionInvoked signal.
// I suspect this detail is implementation specific for the UI interaction,
// and does at least happen on XFCE4.
type ActionInvokedHandler func(*ActionInvokedSignal)

// ActionInvokedSignal holds data from any signal received regarding Actions invoked
type ActionInvokedSignal struct {
	// ID of the Notification the action was invoked for
	ID uint32
	// Key from the tuple (action_key, label)
	ActionKey string
}

// notifier implements Notifier interface
type notifier struct {
	conn     *dbus.Conn
	signal   chan *dbus.Signal
	done     chan bool
	onClosed NotificationClosedHandler
	onAction ActionInvokedHandler
	wg       *sync.WaitGroup
	log      logger
}

type logger interface {
	Printf(format string, v ...interface{})
}

// Option overrides certain parts of a Notifier
type Option func(*notifier)

// WithLogger sets a new logger func
func WithLogger(logz logger) Option {
	return func(n *notifier) {
		n.log = logz
	}
}

// WithOnAction sets ActionInvokedHandler handler
func WithOnAction(h ActionInvokedHandler) Option {
	return func(n *notifier) {
		n.onAction = h
	}
}

// WithOnClosed sets NotificationClosed handler
func WithOnClosed(h NotificationClosedHandler) Option {
	return func(n *notifier) {
		n.onClosed = h
	}
}

// New creates a new Notifier using conn.
// See also: Notifier
func New(conn *dbus.Conn, opts ...Option) (Notifier, error) {
	n := &notifier{
		conn:     conn,
		signal:   make(chan *dbus.Signal, channelBufferSize),
		done:     make(chan bool),
		wg:       &sync.WaitGroup{},
		onClosed: func(s *NotificationClosedSignal) {},
		onAction: func(s *ActionInvokedSignal) {},
		log:      &loggerWrapper{"notify: "},
	}

	for _, val := range opts {
		val(n)
	}

	// add a listener (matcher) in dbus for signals to Notification interface.
	err := n.conn.AddMatchSignal(
		dbus.WithMatchObjectPath(dbusObjectPath),
		dbus.WithMatchInterface(dbusNotificationsInterface),
	)
	if err != nil {
		return nil, err
	}
	// register in dbus for signal delivery
	n.conn.Signal(n.signal)

	// start eventloop
	go n.eventLoop()

	return n, nil
}

func (n notifier) eventLoop() {
	n.wg.Add(1)
	defer n.wg.Done()
	for {
		select {
		case signal := <-n.signal:
			n.handleSignal(signal)
		case <-n.done:
			n.log.Printf("Got Close() signal, shutting down...")
			return
		}
	}
}

// signal handler that translates and sends notifications to channels
func (n notifier) handleSignal(signal *dbus.Signal) {
	switch signal.Name {
	case signalNotificationClosed:
		nc := &NotificationClosedSignal{
			ID:     signal.Body[0].(uint32),
			Reason: Reason(signal.Body[1].(uint32)),
		}
		n.onClosed(nc)
	case signalActionInvoked:
		is := &ActionInvokedSignal{
			ID:        signal.Body[0].(uint32),
			ActionKey: signal.Body[1].(string),
		}
		n.onAction(is)
	default:
		n.log.Printf("Received unknown signal: %+v", signal)
	}
}

func (n *notifier) GetCapabilities() ([]string, error) {
	return GetCapabilities(n.conn)
}
func (n *notifier) GetServerInformation() (ServerInformation, error) {
	return GetServerInformation(n.conn)
}

// SendNotification sends a notification to the notification server and returns the ID or an error.
//
// Implements dbus call:
//
//     UINT32 org.freedesktop.Notifications.Notify (
//	       STRING app_name,
//	       UINT32 replaces_id,
//	       STRING app_icon,
//	       STRING summary,
//	       STRING body,
//	       ARRAY  actions,
//	       DICT   hints,
//	       INT32  expire_timeout
//     );
//
//		Name	    	Type	Description
//		app_name		STRING	The optional name of the application sending the notification. Can be blank.
//		replaces_id	    UINT32	The optional notification ID that this notification replaces. The server must atomically (ie with no flicker or other visual cues) replace the given notification with this one. This allows clients to effectively modify the notification while it's active. A value of value of 0 means that this notification won't replace any existing notifications.
//		app_icon		STRING	The optional program icon of the calling application. Can be an empty string, indicating no icon.
//		summary		    STRING	The summary text briefly describing the notification.
//		body			STRING	The optional detailed body text. Can be empty.
//		actions		    ARRAY	Actions are sent over as a list of pairs. Each even element in the list (starting at index 0) represents the identifier for the action. Each odd element in the list is the localized string that will be displayed to the user.
//		hints	        DICT	Optional hints that can be passed to the server from the client program. Although clients and servers should never assume each other supports any specific hints, they can be used to pass along information, such as the process PID or window ID, that the server may be able to make use of. See Hints. Can be empty.
//      expire_timeout  INT32   The timeout time in milliseconds since the display of the notification at which the notification should automatically close.
//								If -1, the notification's expiration time is dependent on the notification server's settings, and may vary for the type of notification. If 0, never expire.
//
// If replaces_id is 0, the return value is a UINT32 that represent the notification.
// It is unique, and will not be reused unless a MAXINT number of notifications have been generated.
// An acceptable implementation may just use an incrementing counter for the ID.
// The returned ID is always greater than zero. Servers must make sure not to return zero as an ID.
//
// If replaces_id is not 0, the returned value is the same value as replaces_id.
func (n *notifier) SendNotification(note Notification) (uint32, error) {
	return SendNotification(n.conn, note)
}

// CloseNotification causes a notification to be forcefully closed and removed from the user's view.
// It can be used, for example, in the event that what the notification pertains to is no longer relevant,
// or to cancel a notification with no expiration time.
//
// The NotificationClosed (dbus) signal is emitted by this method.
// If the notification no longer exists, an empty D-BUS Error message is sent back.
func (n *notifier) CloseNotification(id int) (bool, error) {
	obj := n.conn.Object(dbusNotificationsInterface, dbusObjectPath)
	call := obj.Call(callCloseNotification, 0, uint32(id))
	if call.Err != nil {
		return false, call.Err
	}
	return true, nil
}

// NotificationClosedSignal holds data for *Closed callbacks from Notifications Interface.
type NotificationClosedSignal struct {
	// ID of the Notification the signal was invoked for
	ID uint32
	// A reason given if known
	Reason Reason
}

// Reason for the closed notification
type Reason uint32

const (
	// ReasonExpired when a notification expired
	ReasonExpired Reason = 1

	// ReasonDismissedByUser when a notification has been dismissed by a user
	ReasonDismissedByUser Reason = 2

	// ReasonClosedByCall when a notification has been closed by a call to CloseNotification
	ReasonClosedByCall Reason = 3

	// ReasonUnknown when as notification has been closed for an unknown reason
	ReasonUnknown Reason = 4
)

func (r Reason) String() string {
	switch r {
	case ReasonExpired:
		return "Expired"
	case ReasonDismissedByUser:
		return "DismissedByUser"
	case ReasonClosedByCall:
		return "ClosedByCall"
	case ReasonUnknown:
		return "Unknown"
	default:
		return "Other"
	}
}

// Close cleans up and shuts down signal delivery loop
func (n *notifier) Close() error {
	n.done <- true

	// remove signal reception
	n.conn.RemoveSignal(n.signal)

	// unregister in dbus:
	errRemoveMatch := n.conn.RemoveMatchSignal(
		dbus.WithMatchObjectPath(dbusObjectPath),
		dbus.WithMatchInterface(dbusNotificationsInterface),
	)

	close(n.done)

	// wait for eventloop to shut down...
	n.wg.Wait()

	return errRemoveMatch
}

type loggerWrapper struct {
	prefix string
}

func (l *loggerWrapper) Printf(format string, v ...interface{}) {
	log.Printf(l.prefix+format, v...)
}

// NotificationClosed returns a receive only channel that sends
// NotificationClosedSignal for signals.
//
// The chan must be drained or event delivery will stall.
// Deprecated: Use NotificationClosedHandler
//func (n *notifier) NotificationClosed() <-chan *NotificationClosedSignal {
//	return n.closer
//}
<commit_msg>Update notification.go<commit_after>package notify

import (
	"errors"
	"fmt"
	"log"
	"sync"

	"github.com/godbus/dbus/v5"
)

const (
	dbusRemoveMatch            = "org.freedesktop.DBus.RemoveMatch"
	dbusAddMatch               = "org.freedesktop.DBus.AddMatch"
	dbusObjectPath             = "/org/freedesktop/Notifications" // the DBUS object path
	dbusNotificationsInterface = "org.freedesktop.Notifications"  // DBUS Interface
	signalNotificationClosed   = "org.freedesktop.Notifications.NotificationClosed"
	signalActionInvoked        = "org.freedesktop.Notifications.ActionInvoked"
	callGetCapabilities        = "org.freedesktop.Notifications.GetCapabilities"
	callCloseNotification      = "org.freedesktop.Notifications.CloseNotification"
	callNotify                 = "org.freedesktop.Notifications.Notify"
	callGetServerInformation   = "org.freedesktop.Notifications.GetServerInformation"

	channelBufferSize = 2
)

// Notification holds all information needed for creating a notification
type Notification struct {
	AppName string
	// Setting ReplacesID atomically replaces the notification with this ID.
	// Optional.
	ReplacesID uint32
	// See predefined icons here: http://standards.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html
	// Optional.
	AppIcon string
	Summary string
	Body    string
	// Actions are tuples of (action_key, label), e.g.: []string{"cancel", "Cancel", "open", "Open"}
	Actions []string
	Hints   map[string]dbus.Variant
	// ExpireTimeout: milliseconds to show notification
	ExpireTimeout int32
}

// SendNotification is provided for convenience.
// Use if you only want to deliver a notification and dont care about events.
func SendNotification(conn *dbus.Conn, note Notification) (uint32, error) {
	actions := len(note.Actions)
	if (actions % 2) != 0 {
		return 0, errors.New("actions must be pairs of (key, label)")
	}

	obj := conn.Object(dbusNotificationsInterface, dbusObjectPath)
	call := obj.Call(callNotify, 0,
		note.AppName,
		note.ReplacesID,
		note.AppIcon,
		note.Summary,
		note.Body,
		note.Actions,
		note.Hints,
		note.ExpireTimeout)
	if call.Err != nil {
		return 0, fmt.Errorf("error sending notification: %w", call.Err)
	}
	var ret uint32
	err := call.Store(&ret)
	if err != nil {
		return ret, fmt.Errorf("error getting uint32 ret value: %w", err)
	}
	return ret, nil
}

// ServerInformation is a holder for information returned by
// GetServerInformation call.
type ServerInformation struct {
	Name        string
	Vendor      string
	Version     string
	SpecVersion string
}

// GetServerInformation returns the information on the server.
//
// org.freedesktop.Notifications.GetServerInformation
//
//  GetServerInformation Return Values
//
//		Name		 Type	  Description
//		name		 STRING	  The product name of the server.
//		vendor		 STRING	  The vendor name. For example, "KDE," "GNOME," "freedesktop.org," or "Microsoft."
//		version		 STRING	  The server's version number.
//		spec_version STRING	  The specification version the server is compliant with.
//
func GetServerInformation(conn *dbus.Conn) (ServerInformation, error) {
	obj := conn.Object(dbusNotificationsInterface, dbusObjectPath)
	if obj == nil {
		return ServerInformation{}, errors.New("error creating dbus call object")
	}
	call := obj.Call(callGetServerInformation, 0)
	if call.Err != nil {
		return ServerInformation{}, fmt.Errorf("error calling %v: %v", callGetServerInformation, call.Err)
	}

	ret := ServerInformation{}
	err := call.Store(&ret.Name, &ret.Vendor, &ret.Version, &ret.SpecVersion)
	if err != nil {
		return ret, fmt.Errorf("error reading %v return values: %v", callGetServerInformation, err)
	}
	return ret, nil
}

// GetCapabilities gets the capabilities of the notification server.
// This call takes no parameters.
// It returns an array of strings. Each string describes an optional capability implemented by the server.
//
// See also: https://developer.gnome.org/notification-spec/
// GetCapabilities provide an exported method for this operation
func GetCapabilities(conn *dbus.Conn) ([]string, error) {
	obj := conn.Object(dbusNotificationsInterface, dbusObjectPath)
	call := obj.Call(callGetCapabilities, 0)
	if call.Err != nil {
		return []string{}, call.Err
	}
	var ret []string
	err := call.Store(&ret)
	if err != nil {
		return ret, err
	}
	return ret, nil
}

// Notifier is an interface for implementing the operations supported by the
// freedesktop DBus Notifications object.
//
// New() sets up a Notifier that listens on dbus' signals regarding
// Notifications: NotificationClosed and ActionInvoked.
//
// Note this also means the caller MUST consume output from these channels,
// given in methods NotificationClosed() and ActionInvoked().
// Users that only want to send a simple notification, but don't care about
// interactions, see exported method: SendNotification(conn, Notification)
//
// Caller is also responsible to call Close() before exiting,
// to shut down event loop and cleanup.
type Notifier interface {
	SendNotification(n Notification) (uint32, error)
	GetCapabilities() ([]string, error)
	GetServerInformation() (ServerInformation, error)
	CloseNotification(id int) (bool, error)
	Close() error
}

// NotificationClosedHandler is called when we receive a NotificationClosed signal
type NotificationClosedHandler func(*NotificationClosedSignal)

// ActionInvokedHandler is called when we receive a signal that one of the actions passed was invoked.
//
// Note that invoking an action often also produces a NotificationClosedSignal,
// so you might receive both a Closed signal and a ActionInvoked signal.
// I suspect this detail is implementation specific for the UI interaction,
// and does at least happen on XFCE4.
type ActionInvokedHandler func(*ActionInvokedSignal)

// ActionInvokedSignal holds data from any signal received regarding Actions invoked
type ActionInvokedSignal struct {
	// ID of the Notification the action was invoked for
	ID uint32
	// Key from the tuple (action_key, label)
	ActionKey string
}

// notifier implements Notifier interface
type notifier struct {
	conn     *dbus.Conn
	signal   chan *dbus.Signal
	done     chan bool
	onClosed NotificationClosedHandler
	onAction ActionInvokedHandler
	wg       *sync.WaitGroup
	log      logger
}

type logger interface {
	Printf(format string, v ...interface{})
}

// Option overrides certain parts of a Notifier
type Option func(*notifier)

// WithLogger sets a new logger func
func WithLogger(logz logger) Option {
	return func(n *notifier) {
		n.log = logz
	}
}

// WithOnAction sets ActionInvokedHandler handler
func WithOnAction(h ActionInvokedHandler) Option {
	return func(n *notifier) {
		n.onAction = h
	}
}

// WithOnClosed sets NotificationClosed handler
func WithOnClosed(h NotificationClosedHandler) Option {
	return func(n *notifier) {
		n.onClosed = h
	}
}

// New creates a new Notifier using conn.
// See also: Notifier
func New(conn *dbus.Conn, opts ...Option) (Notifier, error) {
	n := &notifier{
		conn:     conn,
		signal:   make(chan *dbus.Signal, channelBufferSize),
		done:     make(chan bool),
		wg:       &sync.WaitGroup{},
		onClosed: func(s *NotificationClosedSignal) {},
		onAction: func(s *ActionInvokedSignal) {},
		log:      &loggerWrapper{"notify: "},
	}

	for _, val := range opts {
		val(n)
	}

	// add a listener (matcher) in dbus for signals to Notification interface.
	err := n.conn.AddMatchSignal(
		dbus.WithMatchObjectPath(dbusObjectPath),
		dbus.WithMatchInterface(dbusNotificationsInterface),
	)
	if err != nil {
		return nil, err
	}
	// register in dbus for signal delivery
	n.conn.Signal(n.signal)

	// start eventloop
	go n.eventLoop()

	return n, nil
}

func (n notifier) eventLoop() {
	n.wg.Add(1)
	defer n.wg.Done()
	for {
		select {
		case signal := <-n.signal:
			n.handleSignal(signal)
		case <-n.done:
			n.log.Printf("Got Close() signal, shutting down...")
			return
		}
	}
}

// signal handler that translates and sends notifications to channels
func (n notifier) handleSignal(signal *dbus.Signal) {
	switch signal.Name {
	case signalNotificationClosed:
		nc := &NotificationClosedSignal{
			ID:     signal.Body[0].(uint32),
			Reason: Reason(signal.Body[1].(uint32)),
		}
		n.onClosed(nc)
	case signalActionInvoked:
		is := &ActionInvokedSignal{
			ID:        signal.Body[0].(uint32),
			ActionKey: signal.Body[1].(string),
		}
		n.onAction(is)
	default:
		n.log.Printf("Received unknown signal: %+v", signal)
	}
}

func (n *notifier) GetCapabilities() ([]string, error) {
	return GetCapabilities(n.conn)
}
func (n *notifier) GetServerInformation() (ServerInformation, error) {
	return GetServerInformation(n.conn)
}

// SendNotification sends a notification to the notification server and returns the ID or an error.
//
// Implements dbus call:
//
//     UINT32 org.freedesktop.Notifications.Notify (
//	       STRING app_name,
//	       UINT32 replaces_id,
//	       STRING app_icon,
//	       STRING summary,
//	       STRING body,
//	       ARRAY  actions,
//	       DICT   hints,
//	       INT32  expire_timeout
//     );
//
//		Name	    	Type	Description
//		app_name		STRING	The optional name of the application sending the notification. Can be blank.
//		replaces_id	    UINT32	The optional notification ID that this notification replaces. The server must atomically (ie with no flicker or other visual cues) replace the given notification with this one. This allows clients to effectively modify the notification while it's active. A value of value of 0 means that this notification won't replace any existing notifications.
//		app_icon		STRING	The optional program icon of the calling application. Can be an empty string, indicating no icon.
//		summary		    STRING	The summary text briefly describing the notification.
//		body			STRING	The optional detailed body text. Can be empty.
//		actions		    ARRAY	Actions are sent over as a list of pairs. Each even element in the list (starting at index 0) represents the identifier for the action. Each odd element in the list is the localized string that will be displayed to the user.
//		hints	        DICT	Optional hints that can be passed to the server from the client program. Although clients and servers should never assume each other supports any specific hints, they can be used to pass along information, such as the process PID or window ID, that the server may be able to make use of. See Hints. Can be empty.
//      expire_timeout  INT32   The timeout time in milliseconds since the display of the notification at which the notification should automatically close.
//								If -1, the notification's expiration time is dependent on the notification server's settings, and may vary for the type of notification. If 0, never expire.
//
// If replaces_id is 0, the return value is a UINT32 that represent the notification.
// It is unique, and will not be reused unless a MAXINT number of notifications have been generated.
// An acceptable implementation may just use an incrementing counter for the ID.
// The returned ID is always greater than zero. Servers must make sure not to return zero as an ID.
//
// If replaces_id is not 0, the returned value is the same value as replaces_id.
func (n *notifier) SendNotification(note Notification) (uint32, error) {
	return SendNotification(n.conn, note)
}

// CloseNotification causes a notification to be forcefully closed and removed from the user's view.
// It can be used, for example, in the event that what the notification pertains to is no longer relevant,
// or to cancel a notification with no expiration time.
//
// The NotificationClosed (dbus) signal is emitted by this method.
// If the notification no longer exists, an empty D-BUS Error message is sent back.
func (n *notifier) CloseNotification(id int) (bool, error) {
	obj := n.conn.Object(dbusNotificationsInterface, dbusObjectPath)
	call := obj.Call(callCloseNotification, 0, uint32(id))
	if call.Err != nil {
		return false, call.Err
	}
	return true, nil
}

// NotificationClosedSignal holds data for *Closed callbacks from Notifications Interface.
type NotificationClosedSignal struct {
	// ID of the Notification the signal was invoked for
	ID uint32
	// A reason given if known
	Reason Reason
}

// Reason for the closed notification
type Reason uint32

const (
	// ReasonExpired when a notification expired
	ReasonExpired Reason = 1

	// ReasonDismissedByUser when a notification has been dismissed by a user
	ReasonDismissedByUser Reason = 2

	// ReasonClosedByCall when a notification has been closed by a call to CloseNotification
	ReasonClosedByCall Reason = 3

	// ReasonUnknown when as notification has been closed for an unknown reason
	ReasonUnknown Reason = 4
)

func (r Reason) String() string {
	switch r {
	case ReasonExpired:
		return "Expired"
	case ReasonDismissedByUser:
		return "DismissedByUser"
	case ReasonClosedByCall:
		return "ClosedByCall"
	case ReasonUnknown:
		return "Unknown"
	default:
		return "Other"
	}
}

// Close cleans up and shuts down signal delivery loop
func (n *notifier) Close() error {
	n.done <- true

	// remove signal reception
	n.conn.RemoveSignal(n.signal)

	// unregister in dbus:
	errRemoveMatch := n.conn.RemoveMatchSignal(
		dbus.WithMatchObjectPath(dbusObjectPath),
		dbus.WithMatchInterface(dbusNotificationsInterface),
	)

	close(n.done)

	// wait for eventloop to shut down...
	n.wg.Wait()

	return errRemoveMatch
}

type loggerWrapper struct {
	prefix string
}

func (l *loggerWrapper) Printf(format string, v ...interface{}) {
	log.Printf(l.prefix+format, v...)
}
<|endoftext|>


<commit_before>package main

import (
	"github.com/google/go-gcm"
	apns "github.com/sideshow/apns2"
	"github.com/sideshow/apns2/certificate"
	"github.com/sideshow/apns2/payload"
	"log"
)

type ExtendJSON struct {
	Key   string `json:"key"`
	Value string `json:"val"`
}

type alert struct {
	Action       string   `json:"action,omitempty"`
	ActionLocKey string   `json:"action-loc-key,omitempty"`
	Body         string   `json:"body,omitempty"`
	LaunchImage  string   `json:"launch-image,omitempty"`
	LocArgs      []string `json:"loc-args,omitempty"`
	LocKey       string   `json:"loc-key,omitempty"`
	Title        string   `json:"title,omitempty"`
	TitleLocArgs []string `json:"title-loc-args,omitempty"`
	TitleLocKey  string   `json:"title-loc-key,omitempty"`
}

type RequestPushNotification struct {
	// Common
	Tokens           []string `json:"tokens" binding:"required"`
	Platform         int      `json:"platform" binding:"required"`
	Message          string   `json:"message" binding:"required"`
	Priority         string   `json:"priority,omitempty"`
	ContentAvailable bool     `json:"content_available,omitempty"`

	// Android
	To                    string           `json:"to,omitempty"`
	CollapseKey           string           `json:"collapse_key,omitempty"`
	DelayWhileIdle        bool             `json:"delay_while_idle,omitempty"`
	TimeToLive            uint             `json:"time_to_live,omitempty"`
	RestrictedPackageName string           `json:"restricted_package_name,omitempty"`
	DryRun                bool             `json:"dry_run,omitempty"`
	Data                  gcm.Data         `json:"data,omitempty"`
	Notification          gcm.Notification `json:"notification,omitempty"`

	// iOS
	ApnsID   string       `json:"apns_id,omitempty"`
	Topic    string       `json:"topic,omitempty"`
	Badge    int          `json:"badge,omitempty"`
	Sound    string       `json:"sound,omitempty"`
	Expiry   int          `json:"expiry,omitempty"`
	Retry    int          `json:"retry,omitempty"`
	Category string       `json:"category,omitempty"`
	URLArgs  []string     `json:"url-args,omitempty"`
	Extend   []ExtendJSON `json:"extend,omitempty"`
	Alert    alert        `json:"alert,omitempty"`

	// meta
	IDs []uint64 `json:"seq_id,omitempty"`
}

func pushNotification(notification RequestPushNotification) bool {
	var (
		success bool
	)

	cert, err := certificate.FromPemFile("./key.pem", "")
	if err != nil {
		log.Println("Cert Error:", err)
	}

	apnsClient := apns.NewClient(cert).Development()

	switch notification.Platform {
	case PlatFormIos:
		success = pushNotificationIos(notification, apnsClient)
		if !success {
			apnsClient = nil
		}
	case PlatFormAndroid:
		success = pushNotificationAndroid(notification)
	}

	return success
}

func pushNotificationIos(req RequestPushNotification, client *apns.Client) bool {

	for _, token := range req.Tokens {
		notification := &apns.Notification{}
		notification.DeviceToken = token

		if len(req.ApnsID) > 0 {
			notification.ApnsID = req.ApnsID
		}

		if len(req.Topic) > 0 {
			notification.Topic = req.Topic
		}

		if len(req.Priority) > 0 && req.Priority == "normal" {
			notification.Priority = apns.PriorityLow
		}

		payload := payload.NewPayload().Alert(req.Message)

		if req.Badge > 0 {
			payload.Badge(req.Badge)
		}

		if len(req.Sound) > 0 {
			payload.Sound(req.Sound)
		}

		if req.ContentAvailable {
			payload.ContentAvailable()
		}

		if len(req.Extend) > 0 {
			for _, extend := range req.Extend {
				payload.Custom(extend.Key, extend.Value)
			}
		}

		// Alert dictionary

		if len(req.Alert.Title) > 0 {
			payload.AlertTitle(req.Alert.Title)
		}

		if len(req.Alert.TitleLocKey) > 0 {
			payload.AlertTitleLocKey(req.Alert.TitleLocKey)
		}

		if len(req.Alert.LocArgs) > 0 {
			payload.AlertTitleLocArgs(req.Alert.LocArgs)
		}

		if len(req.Alert.Body) > 0 {
			payload.AlertBody(req.Alert.Body)
		}

		if len(req.Alert.LaunchImage) > 0 {
			payload.AlertLaunchImage(req.Alert.LaunchImage)
		}

		if len(req.Alert.LocKey) > 0 {
			payload.AlertLocKey(req.Alert.LocKey)
		}

		if len(req.Alert.Action) > 0 {
			payload.AlertAction(req.Alert.Action)
		}

		if len(req.Alert.ActionLocKey) > 0 {
			payload.AlertActionLocKey(req.Alert.ActionLocKey)
		}

		// General

		if len(req.Category) > 0 {
			payload.Category(req.Category)
		}

		if len(req.URLArgs) > 0 {
			payload.URLArgs(req.URLArgs)
		}

		notification.Payload = payload

		// send ios notification
		res, err := client.Push(notification)

		if err != nil {
			log.Println("There was an error", err)
			return false
		}

		if res.Sent() {
			log.Println("APNs ID:", res.ApnsID)
		}
	}

	client = nil

	return true
}

func pushNotificationAndroid(req RequestPushNotification) bool {

	// HTTP Connection Server Reference for Android
	// https://developers.google.com/cloud-messaging/http-server-ref
	notification := gcm.HttpMessage{}

	notification.RegistrationIds = req.Tokens

	if len(req.To) > 0 {
		notification.To = req.To
	}

	if len(req.Priority) > 0 && req.Priority == "high" {
		notification.Priority = "high"
	}

	if len(req.CollapseKey) > 0 {
		notification.CollapseKey = req.CollapseKey
	}

	if req.ContentAvailable {
		notification.ContentAvailable = true
	}

	if req.DelayWhileIdle {
		notification.DelayWhileIdle = true
	}

	if req.TimeToLive > 0 {
		notification.TimeToLive = req.TimeToLive
	}

	if len(req.RestrictedPackageName) > 0 {
		notification.RestrictedPackageName = req.RestrictedPackageName
	}

	if req.DryRun {
		notification.DryRun = true
	}

	if len(req.Data) > 0 {
		notification.Data = req.Data
	}

	notification.Notification = &req.Notification

	// overwrite notification body
	notification.Notification.Body = req.Message

	res, err := gcm.SendHttp("api key", notification)

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

		return false
	}

	if res.Error != "" {
		log.Println("GCM Error Message: " + res.Error)
	}

	if res.Success > 0 {
		log.Printf("Success count: %d, Failure count: %d", res.Success, res.Failure)

		return true
	}

	return true
}
<commit_msg>Android: Set request message if body is empty<commit_after>package main

import (
	"github.com/google/go-gcm"
	apns "github.com/sideshow/apns2"
	"github.com/sideshow/apns2/certificate"
	"github.com/sideshow/apns2/payload"
	"log"
)

type ExtendJSON struct {
	Key   string `json:"key"`
	Value string `json:"val"`
}

type alert struct {
	Action       string   `json:"action,omitempty"`
	ActionLocKey string   `json:"action-loc-key,omitempty"`
	Body         string   `json:"body,omitempty"`
	LaunchImage  string   `json:"launch-image,omitempty"`
	LocArgs      []string `json:"loc-args,omitempty"`
	LocKey       string   `json:"loc-key,omitempty"`
	Title        string   `json:"title,omitempty"`
	TitleLocArgs []string `json:"title-loc-args,omitempty"`
	TitleLocKey  string   `json:"title-loc-key,omitempty"`
}

type RequestPushNotification struct {
	// Common
	Tokens           []string `json:"tokens" binding:"required"`
	Platform         int      `json:"platform" binding:"required"`
	Message          string   `json:"message" binding:"required"`
	Priority         string   `json:"priority,omitempty"`
	ContentAvailable bool     `json:"content_available,omitempty"`

	// Android
	To                    string           `json:"to,omitempty"`
	CollapseKey           string           `json:"collapse_key,omitempty"`
	DelayWhileIdle        bool             `json:"delay_while_idle,omitempty"`
	TimeToLive            uint             `json:"time_to_live,omitempty"`
	RestrictedPackageName string           `json:"restricted_package_name,omitempty"`
	DryRun                bool             `json:"dry_run,omitempty"`
	Data                  gcm.Data         `json:"data,omitempty"`
	Notification          gcm.Notification `json:"notification,omitempty"`

	// iOS
	ApnsID   string       `json:"apns_id,omitempty"`
	Topic    string       `json:"topic,omitempty"`
	Badge    int          `json:"badge,omitempty"`
	Sound    string       `json:"sound,omitempty"`
	Expiry   int          `json:"expiry,omitempty"`
	Retry    int          `json:"retry,omitempty"`
	Category string       `json:"category,omitempty"`
	URLArgs  []string     `json:"url-args,omitempty"`
	Extend   []ExtendJSON `json:"extend,omitempty"`
	Alert    alert        `json:"alert,omitempty"`

	// meta
	IDs []uint64 `json:"seq_id,omitempty"`
}

func pushNotification(notification RequestPushNotification) bool {
	var (
		success bool
	)

	cert, err := certificate.FromPemFile("./key.pem", "")
	if err != nil {
		log.Println("Cert Error:", err)
	}

	apnsClient := apns.NewClient(cert).Development()

	switch notification.Platform {
	case PlatFormIos:
		success = pushNotificationIos(notification, apnsClient)
		if !success {
			apnsClient = nil
		}
	case PlatFormAndroid:
		success = pushNotificationAndroid(notification)
	}

	return success
}

func pushNotificationIos(req RequestPushNotification, client *apns.Client) bool {

	for _, token := range req.Tokens {
		notification := &apns.Notification{}
		notification.DeviceToken = token

		if len(req.ApnsID) > 0 {
			notification.ApnsID = req.ApnsID
		}

		if len(req.Topic) > 0 {
			notification.Topic = req.Topic
		}

		if len(req.Priority) > 0 && req.Priority == "normal" {
			notification.Priority = apns.PriorityLow
		}

		payload := payload.NewPayload().Alert(req.Message)

		if req.Badge > 0 {
			payload.Badge(req.Badge)
		}

		if len(req.Sound) > 0 {
			payload.Sound(req.Sound)
		}

		if req.ContentAvailable {
			payload.ContentAvailable()
		}

		if len(req.Extend) > 0 {
			for _, extend := range req.Extend {
				payload.Custom(extend.Key, extend.Value)
			}
		}

		// Alert dictionary

		if len(req.Alert.Title) > 0 {
			payload.AlertTitle(req.Alert.Title)
		}

		if len(req.Alert.TitleLocKey) > 0 {
			payload.AlertTitleLocKey(req.Alert.TitleLocKey)
		}

		if len(req.Alert.LocArgs) > 0 {
			payload.AlertTitleLocArgs(req.Alert.LocArgs)
		}

		if len(req.Alert.Body) > 0 {
			payload.AlertBody(req.Alert.Body)
		}

		if len(req.Alert.LaunchImage) > 0 {
			payload.AlertLaunchImage(req.Alert.LaunchImage)
		}

		if len(req.Alert.LocKey) > 0 {
			payload.AlertLocKey(req.Alert.LocKey)
		}

		if len(req.Alert.Action) > 0 {
			payload.AlertAction(req.Alert.Action)
		}

		if len(req.Alert.ActionLocKey) > 0 {
			payload.AlertActionLocKey(req.Alert.ActionLocKey)
		}

		// General

		if len(req.Category) > 0 {
			payload.Category(req.Category)
		}

		if len(req.URLArgs) > 0 {
			payload.URLArgs(req.URLArgs)
		}

		notification.Payload = payload

		// send ios notification
		res, err := client.Push(notification)

		if err != nil {
			log.Println("There was an error", err)
			return false
		}

		if res.Sent() {
			log.Println("APNs ID:", res.ApnsID)
		}
	}

	client = nil

	return true
}

func pushNotificationAndroid(req RequestPushNotification) bool {

	// HTTP Connection Server Reference for Android
	// https://developers.google.com/cloud-messaging/http-server-ref
	notification := gcm.HttpMessage{}

	notification.RegistrationIds = req.Tokens

	if len(req.To) > 0 {
		notification.To = req.To
	}

	if len(req.Priority) > 0 && req.Priority == "high" {
		notification.Priority = "high"
	}

	if len(req.CollapseKey) > 0 {
		notification.CollapseKey = req.CollapseKey
	}

	if req.ContentAvailable {
		notification.ContentAvailable = true
	}

	if req.DelayWhileIdle {
		notification.DelayWhileIdle = true
	}

	if req.TimeToLive > 0 {
		notification.TimeToLive = req.TimeToLive
	}

	if len(req.RestrictedPackageName) > 0 {
		notification.RestrictedPackageName = req.RestrictedPackageName
	}

	if req.DryRun {
		notification.DryRun = true
	}

	if len(req.Data) > 0 {
		notification.Data = req.Data
	}

	notification.Notification = &req.Notification

	// Set request message if body is empty
	if len(notification.Notification.Body) == 0 {
		notification.Notification.Body = req.Message
	}

	res, err := gcm.SendHttp("api key", notification)

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

		return false
	}

	if res.Error != "" {
		log.Println("GCM Error Message: " + res.Error)
	}

	if res.Success > 0 {
		log.Printf("Success count: %d, Failure count: %d", res.Success, res.Failure)

		return true
	}

	return true
}
<|endoftext|>


<commit_before>package notify

import (
	"fmt"
	"io"
	"strings"
)

var ansiColorsEnabled = true

func PrintWelcome(version string) {
	if ansiColorsEnabled {
		fmt.Println("\033[1;34mmelrōse\033[0m" + " - program your melodies - " + version)
	} else {
		fmt.Fprintf(Console.StandardOut, "melrose - program your melodies - "+version+"\n")
	}
}

func PrintBye() {
	if ansiColorsEnabled {
		fmt.Println("\033[1;34mmelrose\033[0m" + " sings bye!")
	} else {
		fmt.Fprintf(Console.StandardOut, "melrose sings bye!\n")
	}
}

func Prompt() string {
	if ansiColorsEnabled {
		return "𝄞 "
	}
	return "# "
}

func PrintHighlighted(what string) {
	if ansiColorsEnabled {
		fmt.Println("\033[1;33m" + what + "\033[0m")
	} else {
		fmt.Println(what)
	}
}

func Print(m Message) {
	if m == nil {
		return
	}
	switch m.Type() {
	case NotifyInfo:
		printInfo(m.Message())
	case NotifyWarning:
		printWarning(m.Message())
	case NotifyError:
		printError(m.Message())
	}
}

// Println is to inject a function that can report info,error and warning
var Println = fmt.Println

func Debugf(format string, args ...interface{}) {
	// make sure it ends with newline
	if !strings.HasSuffix(format, "\n") {
		format += "\n"
	}
	fmt.Fprintf(Console.StandardOut, format, args...)
}

func Warnf(format string, args ...interface{}) {
	printWarning(fmt.Sprintf(format, args...))
}

func Infof(format string, args ...interface{}) {
	printInfo(fmt.Sprintf(format, args...))
}

func Errorf(format string, args ...interface{}) {
	printError(fmt.Sprintf(format, args...))
}

func printInfo(args ...interface{}) {
	fmt.Fprintf(Console.StandardOut, "%s\n", args...)
}

func printError(args ...interface{}) {
	if ansiColorsEnabled {
		Println(append([]interface{}{"\033[1;31merror:\033[0m"}, args...)...)
	} else {
		fmt.Fprintf(Console.StandardError, "ERROR: %s\n", args...)
	}
}

func printWarning(args ...interface{}) {
	if ansiColorsEnabled {
		Println(append([]interface{}{"\033[1;33mwarning:\033[0m"}, args...)...)
	} else {
		fmt.Fprintf(Console.StandardOut, "WARN: %s\n", args...)
	}
}

func PrintKeyValue(b io.Writer, k string, v interface{}) {
	if ansiColorsEnabled {
		fmt.Fprintf(b, "\033[94m%s:\033[0m%v ", k, v)
	} else {
		fmt.Fprintf(b, "%s:%v ", k, v)
	}
}
<commit_msg>show help hint on startup<commit_after>package notify

import (
	"fmt"
	"io"
	"strings"
)

var ansiColorsEnabled = true

func PrintWelcome(version string) {
	tail := " - program your melodies - " + version + " (help = :h, quit = :q or ctrl+c)"
	if ansiColorsEnabled {
		fmt.Println("\033[1;34mmelrōse\033[0m" + tail)
	} else {
		fmt.Fprintf(Console.StandardOut, "melrose"+tail+"\n")
	}
}

func PrintBye() {
	if ansiColorsEnabled {
		fmt.Println("\033[1;34mmelrose\033[0m" + " sings bye!")
	} else {
		fmt.Fprintf(Console.StandardOut, "melrose sings bye!\n")
	}
}

func Prompt() string {
	if ansiColorsEnabled {
		return "𝄞 "
	}
	return "# "
}

func PrintHighlighted(what string) {
	if ansiColorsEnabled {
		fmt.Println("\033[1;33m" + what + "\033[0m")
	} else {
		fmt.Println(what)
	}
}

func Print(m Message) {
	if m == nil {
		return
	}
	switch m.Type() {
	case NotifyInfo:
		printInfo(m.Message())
	case NotifyWarning:
		printWarning(m.Message())
	case NotifyError:
		printError(m.Message())
	}
}

// Println is to inject a function that can report info,error and warning
var Println = fmt.Println

func Debugf(format string, args ...interface{}) {
	// make sure it ends with newline
	if !strings.HasSuffix(format, "\n") {
		format += "\n"
	}
	fmt.Fprintf(Console.StandardOut, format, args...)
}

func Warnf(format string, args ...interface{}) {
	printWarning(fmt.Sprintf(format, args...))
}

func Infof(format string, args ...interface{}) {
	printInfo(fmt.Sprintf(format, args...))
}

func Errorf(format string, args ...interface{}) {
	printError(fmt.Sprintf(format, args...))
}

func printInfo(args ...interface{}) {
	fmt.Fprintf(Console.StandardOut, "%s\n", args...)
}

func printError(args ...interface{}) {
	if ansiColorsEnabled {
		Println(append([]interface{}{"\033[1;31merror:\033[0m"}, args...)...)
	} else {
		fmt.Fprintf(Console.StandardError, "ERROR: %s\n", args...)
	}
}

func printWarning(args ...interface{}) {
	if ansiColorsEnabled {
		Println(append([]interface{}{"\033[1;33mwarning:\033[0m"}, args...)...)
	} else {
		fmt.Fprintf(Console.StandardOut, "WARN: %s\n", args...)
	}
}

func PrintKeyValue(b io.Writer, k string, v interface{}) {
	if ansiColorsEnabled {
		fmt.Fprintf(b, "\033[94m%s:\033[0m%v ", k, v)
	} else {
		fmt.Fprintf(b, "%s:%v ", k, v)
	}
}
<|endoftext|>


<commit_before>package parsers

import (
	"bytes"
	"strconv"
	"unsafe"

	"github.com/brentp/irelate/interfaces"
)

const empty = ""

// Interval satisfies the Relatable interface.
type Interval struct {
	// chrom, start, end, line, source, related[]
	chrom   string
	start   uint32
	end     uint32
	Fields  [][]byte
	source  uint32
	related []interfaces.Relatable
}

func NewInterval(chrom string, start uint32, end uint32, fields [][]byte, source uint32, related []interfaces.Relatable) *Interval {
	return &Interval{chrom, start, end, fields, source, related}
}

func (i *Interval) Chrom() string                   { return i.chrom }
func (i *Interval) Start() uint32                   { return i.start }
func (i *Interval) End() uint32                     { return i.end }
func (i *Interval) Related() []interfaces.Relatable { return i.related }
func (i *Interval) AddRelated(b interfaces.Relatable) {
	i.related = append(i.related, b)
}

func (i *Interval) Source() uint32       { return i.source }
func (i *Interval) SetSource(src uint32) { i.source = src }

func (i *Interval) String() string {
	return string(bytes.Join(i.Fields, []byte{'\t'}))
}

func unsafeString(b []byte) string {
	return *(*string)(unsafe.Pointer(&b))
}

func IntervalFromBedLine(line []byte) (interfaces.Relatable, error) {
	line = bytes.TrimRight(line, "\r\n")
	fields := bytes.Split([]byte(string(line)), []byte{'\t'})
	start, err := strconv.ParseUint(unsafeString(fields[1]), 10, 32)
	if err != nil {
		return nil, err
	}
	end, err := strconv.ParseUint(unsafeString(fields[2]), 10, 32)
	if err != nil {
		return nil, err
	}
	i := Interval{chrom: string(fields[0]), start: uint32(start), end: uint32(end), related: nil, Fields: fields}
	return &i, nil
}
<commit_msg>add IRefAlt type<commit_after>package parsers

import (
	"bytes"
	"strconv"
	"strings"
	"unsafe"

	"github.com/brentp/irelate/interfaces"
)

const empty = ""

// Interval satisfies the Relatable interface.
type Interval struct {
	// chrom, start, end, line, source, related[]
	chrom   string
	start   uint32
	end     uint32
	source  uint32
	Fields  [][]byte
	related []interfaces.Relatable
}

func NewInterval(chrom string, start uint32, end uint32, fields [][]byte, source uint32, related []interfaces.Relatable) *Interval {
	return &Interval{chrom: chrom, start: start, end: end, Fields: fields, source: source, related: related}
}

func (i *Interval) Chrom() string                   { return i.chrom }
func (i *Interval) Start() uint32                   { return i.start }
func (i *Interval) End() uint32                     { return i.end }
func (i *Interval) Related() []interfaces.Relatable { return i.related }
func (i *Interval) AddRelated(b interfaces.Relatable) {
	i.related = append(i.related, b)
}

func (i *Interval) Source() uint32       { return i.source }
func (i *Interval) SetSource(src uint32) { i.source = src }

func (i *Interval) String() string {
	return string(bytes.Join(i.Fields, []byte{'\t'}))
}

func unsafeString(b []byte) string {
	return *(*string)(unsafe.Pointer(&b))
}

func IntervalFromBedLine(line []byte) (interfaces.Relatable, error) {
	line = bytes.TrimRight(line, "\r\n")
	fields := bytes.Split([]byte(string(line)), []byte{'\t'})
	start, err := strconv.ParseUint(unsafeString(fields[1]), 10, 32)
	if err != nil {
		return nil, err
	}
	end, err := strconv.ParseUint(unsafeString(fields[2]), 10, 32)
	if err != nil {
		return nil, err
	}
	i := Interval{chrom: string(fields[0]), start: uint32(start), end: uint32(end), related: nil, Fields: fields}
	return &i, nil
}

type RefAltInterval struct {
	Interval
	refalt [2]int
}

func (i *RefAltInterval) SetRefAlt(ra []int) {
	i.refalt[0] = ra[0]
	i.refalt[1] = ra[1]
}

func (i *RefAltInterval) Ref() string {
	return string(i.Fields[i.refalt[0]])
}

func (i *RefAltInterval) Alt() []string {
	f := string(i.Fields[i.refalt[1]])
	return strings.Split(f, ",")
}

var _ interfaces.IRefAlt = (*RefAltInterval)(nil)
<|endoftext|>