mirror of https://github.com/deuill/informbot.git
Implement XMPP adapter and basic Inform handler
This implements a XMPP adapter and Inform 7 handler based around Joe Bot. Lots of work remains.
This commit is contained in:
commit
12606dc9b6
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (c) 2020 Alex Palaistras
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
|
@ -0,0 +1,44 @@
|
||||||
|
# InformBot - A Chat Bot for Inform 7 Stories, Built in Go
|
||||||
|
|
||||||
|
[![API Documentation][godoc-svg]][godoc-url] [![MIT License][license-svg]][license-url]
|
||||||
|
|
||||||
|
This package contains chat-bot for executing and running Inform 7 stories, and built around the [Joe
|
||||||
|
Bot][joe-url] framework. Any supported chat adapter can be used, and this repository contains an
|
||||||
|
example integration against a built-in XMPP adapter (which is not currently part of main-line
|
||||||
|
support).
|
||||||
|
|
||||||
|
## Building and Running
|
||||||
|
|
||||||
|
You can build the default test deployment of `informbot` (which currently works against an XMPP
|
||||||
|
server) by running `go get`, e.g.:
|
||||||
|
|
||||||
|
```go
|
||||||
|
go get github.com/deuill/informbot
|
||||||
|
```
|
||||||
|
|
||||||
|
Depending on your server setup, you may need to set up a number of options, as environment
|
||||||
|
variables, for instance:
|
||||||
|
|
||||||
|
``` go
|
||||||
|
INFORMBOT_JID="[email protected]" INFORMBOT_PASSWORD="123" INFORMBOT_USE_STARTTLS=true INFORMBOT_NO_TLS=true informbot
|
||||||
|
```
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
This package is still in early development, and is neither feature-complete nor bug-free. A large
|
||||||
|
amount of work remains on improving integration with Inform and Frotz, and optimizing against larger
|
||||||
|
stories.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
All code in this repository is covered by the terms of the MIT License, the full text of which can
|
||||||
|
be found in the LICENSE file.
|
||||||
|
|
||||||
|
|
||||||
|
[joe-url]: https://github.com/go-joe/joe
|
||||||
|
|
||||||
|
[godoc-url]: https://godoc.org/github.com/deuill/informbot
|
||||||
|
[godoc-svg]: https://godoc.org/github.com/deuill/informbot?status.svg
|
||||||
|
|
||||||
|
[license-url]: https://github.com/deuill/informbot/blob/master/LICENSE
|
||||||
|
[license-svg]: https://img.shields.io/badge/license-MIT-blue.svg
|
|
@ -0,0 +1,17 @@
|
||||||
|
module github.com/deuill/informbot
|
||||||
|
|
||||||
|
go 1.13
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/go-joe/file-memory v1.0.0
|
||||||
|
github.com/go-joe/joe v0.10.0
|
||||||
|
github.com/pkg/errors v0.9.1
|
||||||
|
github.com/stretchr/testify v1.4.0 // indirect
|
||||||
|
go.uber.org/atomic v1.4.0 // indirect
|
||||||
|
go.uber.org/multierr v1.2.0 // indirect
|
||||||
|
go.uber.org/zap v1.10.0
|
||||||
|
mellium.im/sasl v0.2.2-0.20191005211519-f1882f01d82d
|
||||||
|
mellium.im/xmlstream v0.15.0
|
||||||
|
mellium.im/xmpp v0.16.0
|
||||||
|
)
|
|
@ -0,0 +1,59 @@
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/go-joe/file-memory v1.0.0 h1:2R9uKk9l+VvaxW/ekQiZsL1XZSNF0G3+j5WDyJPIzFQ=
|
||||||
|
github.com/go-joe/file-memory v1.0.0/go.mod h1:Le5FS31e5sjnjuxTXqeks0GWHvcM9SCuv78y1FkB464=
|
||||||
|
github.com/go-joe/joe v0.8.0/go.mod h1:fjDMMKm6GV29+egH/IS57PTKHSBMquckyuM7CmXbUQw=
|
||||||
|
github.com/go-joe/joe v0.10.0 h1:dEe0EHhHeshTDGy55Y4HGOZgj0L11TdeMlaTn5JOPQg=
|
||||||
|
github.com/go-joe/joe v0.10.0/go.mod h1:fjDMMKm6GV29+egH/IS57PTKHSBMquckyuM7CmXbUQw=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU=
|
||||||
|
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
|
go.uber.org/multierr v1.2.0 h1:6I+W7f5VwC5SV9dNrZ3qXrDB9mD0dyGOi/ZJmYw03T4=
|
||||||
|
go.uber.org/multierr v1.2.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
|
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
|
go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
|
||||||
|
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
|
golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc=
|
||||||
|
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA=
|
||||||
|
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
mellium.im/reader v0.1.0 h1:UUEMev16gdvaxxZC7fC08j7IzuDKh310nB6BlwnxTww=
|
||||||
|
mellium.im/reader v0.1.0/go.mod h1:F+X5HXpkIfJ9EE1zHQG9lM/hO946iYAmU7xjg5dsQHI=
|
||||||
|
mellium.im/sasl v0.2.1 h1:nspKSRg7/SyO0cRGY71OkfHab8tf9kCts6a6oTDut0w=
|
||||||
|
mellium.im/sasl v0.2.1/go.mod h1:ROaEDLQNuf9vjKqE1SrAfnsobm2YKXT1gnN1uDp1PjQ=
|
||||||
|
mellium.im/sasl v0.2.2-0.20191005211519-f1882f01d82d h1:pGcF7i27vUWdsUCx/jjZdIz8++xhFmkVV33tXTJO0ZE=
|
||||||
|
mellium.im/sasl v0.2.2-0.20191005211519-f1882f01d82d/go.mod h1:i8GDqL2fYwZ6vDJWIBafT8SEOOohlLJkLFDfhcoYg1c=
|
||||||
|
mellium.im/xmlstream v0.14.0 h1:vTljQmcFQq7LEb+LJQV0VI8wnuFnzBy1AnfUbA4SrL8=
|
||||||
|
mellium.im/xmlstream v0.14.0/go.mod h1:O7wqreSmFi1LOh4RiK7r2j4H4pYDgzo1qv5ZkYJZ7Ns=
|
||||||
|
mellium.im/xmlstream v0.15.0 h1:NczJZ5FYsRhaA2asw0/hrQm83K81cSTJszKhHh4s18Q=
|
||||||
|
mellium.im/xmlstream v0.15.0/go.mod h1:7SUlP7f2qnMczK+Cu/OFgqaIhldMolVjo8np7xG41D0=
|
||||||
|
mellium.im/xmpp v0.16.0 h1:Qb4e4odzSKFRVTTUuNybVe78ZT3OH5iijVAGqgaIRrw=
|
||||||
|
mellium.im/xmpp v0.16.0/go.mod h1:/y8pNXeL54Qn86oa4Tg8zzrGiTaKoOyM6KVVZLHFcb0=
|
|
@ -0,0 +1,105 @@
|
||||||
|
package inform
|
||||||
|
|
||||||
|
import (
|
||||||
|
// Standard library
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
// Third-party packages
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Options represent user-configurable values, which are used in processing commands
|
||||||
|
// and formatting output.
|
||||||
|
type Options struct {
|
||||||
|
Prefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default values for options, as assigned to newly created Author instances.
|
||||||
|
var defaultOptions = Options{
|
||||||
|
Prefix: "?",
|
||||||
|
}
|
||||||
|
|
||||||
|
type Author struct {
|
||||||
|
ID string
|
||||||
|
Options Options
|
||||||
|
Stories []*Story
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Author) GetStory(name string) (*Story, error) {
|
||||||
|
if name == "" {
|
||||||
|
return nil, errors.New("story name is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range a.Stories {
|
||||||
|
if a.Stories[i].Name == name {
|
||||||
|
return a.Stories[i], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New("no story found with name '" + name + "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Author) AddStory(name, path string) (*Story, error) {
|
||||||
|
var story *Story
|
||||||
|
if name == "" {
|
||||||
|
return nil, errors.New("story name is empty")
|
||||||
|
} else if s, _ := a.GetStory(name); s != nil {
|
||||||
|
story = s
|
||||||
|
} else if u, err := url.Parse(path); err != nil || (u.Scheme != "https" && u.Scheme != "http") {
|
||||||
|
return nil, errors.New("location given is not a valid HTTP URL")
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.Get(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("could not fetch story file from URL given")
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if body, err := ioutil.ReadAll(resp.Body); err != nil {
|
||||||
|
return nil, errors.New("could not fetch story file from URL given")
|
||||||
|
} else if story == nil {
|
||||||
|
story = NewStory(name, a.ID).WithSource(body)
|
||||||
|
a.Stories = append(a.Stories, story)
|
||||||
|
} else {
|
||||||
|
story.WithSource(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
return story, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Author) RemoveStory(name string) error {
|
||||||
|
if name == "" {
|
||||||
|
return errors.New("story name is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find story with given name, and remove from list assigned to author.
|
||||||
|
for i := range a.Stories {
|
||||||
|
if a.Stories[i].Name == name {
|
||||||
|
a.Stories = append(a.Stories[:i], a.Stories[i+1:]...)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("no story found with name '" + name + "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Author) SetOption(name, value string) error {
|
||||||
|
switch strings.ToLower(name) {
|
||||||
|
case "prefix":
|
||||||
|
if value == "" {
|
||||||
|
return errors.New("cannot set empty prefix value")
|
||||||
|
}
|
||||||
|
a.Options.Prefix = value
|
||||||
|
default:
|
||||||
|
return errors.New("option name '" + name + "' is unknown")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthor(id string) *Author {
|
||||||
|
return &Author{ID: id, Options: defaultOptions}
|
||||||
|
}
|
|
@ -0,0 +1,217 @@
|
||||||
|
package inform
|
||||||
|
|
||||||
|
import (
|
||||||
|
// Standard library
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
// Third-party packages
|
||||||
|
"github.com/go-joe/joe"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The prefix used for all keys stored.
|
||||||
|
const keyPrefix = "org.deuill.informbot"
|
||||||
|
|
||||||
|
type Inform struct {
|
||||||
|
sessions map[string]*Session // A list of open sessions, against their authors.
|
||||||
|
|
||||||
|
bot *joe.Bot // The initialized bot to read commands from and send responses to.
|
||||||
|
config *Config // The configuration for the Inform bot.
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Inform) SayTemplate(channel string, template *template.Template, data interface{}) error {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := template.Execute(&buf, data); err != nil || buf.Len() == 0 {
|
||||||
|
n.bot.Say(channel, messageUnknownError)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
n.bot.Say(channel, buf.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Inform) Handle(ctx context.Context, ev joe.ReceiveMessageEvent) error {
|
||||||
|
// Validate event data.
|
||||||
|
if ev.AuthorID == "" {
|
||||||
|
n.bot.Say(ev.Channel, messageUnknownError)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for stored rule-set against author ID, and send welcome message if none was found.
|
||||||
|
var author, authorKey = &Author{}, keyPrefix + ".author." + ev.AuthorID
|
||||||
|
if ok, err := n.bot.Store.Get(authorKey, author); err != nil {
|
||||||
|
n.bot.Say(ev.Channel, messageUnknownError)
|
||||||
|
return err
|
||||||
|
} else if !ok {
|
||||||
|
if err := n.SayTemplate(ev.Channel, templateWelcome, nil); err != nil {
|
||||||
|
return errors.Wrap(err, "failed storing author information")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and store new Author representation.
|
||||||
|
author = NewAuthor(ev.AuthorID)
|
||||||
|
if err := n.bot.Store.Set(authorKey, author); err != nil {
|
||||||
|
n.bot.Say(ev.Channel, messageUnknownError)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for open session, and handle command directly if not prefixed.
|
||||||
|
var cmd = ev.Text
|
||||||
|
if n.sessions[author.ID] != nil {
|
||||||
|
if !strings.HasPrefix(ev.Text, author.Options.Prefix) {
|
||||||
|
if err := n.sessions[author.ID].Run(cmd); err != nil {
|
||||||
|
n.bot.Say(ev.Channel, messageRunError, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
n.bot.Say(ev.Channel, n.sessions[author.ID].Output())
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
cmd = ev.Text[len(author.Options.Prefix):]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle meta-commands.
|
||||||
|
var fields = strings.Fields(cmd)
|
||||||
|
if len(fields) == 0 {
|
||||||
|
return nil
|
||||||
|
} else if len(fields) == 1 {
|
||||||
|
cmd = fields[0]
|
||||||
|
} else if len(fields) >= 2 {
|
||||||
|
cmd = strings.Join(fields[:2], " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strings.ToLower(cmd) {
|
||||||
|
case "help", "h":
|
||||||
|
return n.SayTemplate(ev.Channel, templateHelp, nil)
|
||||||
|
case "story", "stories", "story list", "list stories", "s":
|
||||||
|
return n.SayTemplate(ev.Channel, templateStoryList, author)
|
||||||
|
case "story add", "stories add", "add stories":
|
||||||
|
if len(fields) < 4 {
|
||||||
|
n.bot.Say(ev.Channel, messageUnknownStory)
|
||||||
|
} else if story, err := author.AddStory(fields[2], fields[3]); err != nil {
|
||||||
|
n.bot.Say(ev.Channel, messageInvalidStory, err)
|
||||||
|
} else if err := story.Compile(ctx, n.config); err != nil {
|
||||||
|
n.bot.Say(ev.Channel, "TODO: Compilation error: "+err.Error())
|
||||||
|
return err
|
||||||
|
} else if err = n.bot.Store.Set(authorKey, author); err != nil {
|
||||||
|
n.bot.Say(ev.Channel, messageUnknownError)
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
n.bot.Say(ev.Channel, messageAddedStory, fields[2])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case "story remove", "stories remove", "story rem", "stories rem":
|
||||||
|
// TODO: Check for active session.
|
||||||
|
if len(fields) < 3 {
|
||||||
|
n.bot.Say(ev.Channel, messageUnknownStory)
|
||||||
|
} else if err := author.RemoveStory(fields[2]); err != nil {
|
||||||
|
n.bot.Say(ev.Channel, messageInvalidStory, err)
|
||||||
|
} else if err = n.bot.Store.Set(authorKey, author); err != nil {
|
||||||
|
n.bot.Say(ev.Channel, messageUnknownError)
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
n.bot.Say(ev.Channel, messageRemovedStory, fields[2])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case "story start", "stories start":
|
||||||
|
if len(fields) < 3 {
|
||||||
|
n.bot.Say(ev.Channel, messageUnknownStory)
|
||||||
|
} else if story, err := author.GetStory(fields[2]); err != nil {
|
||||||
|
n.bot.Say(ev.Channel, messageInvalidStory, err)
|
||||||
|
} else if _, ok := n.sessions[author.ID]; ok {
|
||||||
|
n.bot.Say(ev.Channel, "TODO: Stop session before starting")
|
||||||
|
} else if sess, err := NewSession(story); err != nil {
|
||||||
|
n.bot.Say(ev.Channel, messageInvalidSession, err)
|
||||||
|
return err
|
||||||
|
} else if err = sess.Start(ctx, n.config); err != nil {
|
||||||
|
n.bot.Say(ev.Channel, "TODO: Cannot start session: %s", err)
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
n.bot.Say(ev.Channel, messageStartedSession, fields[2], author.Options.Prefix)
|
||||||
|
n.bot.Say(ev.Channel, sess.Output())
|
||||||
|
n.sessions[author.ID] = sess
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case "story end", "stories end":
|
||||||
|
if n.sessions[author.ID] == nil {
|
||||||
|
n.bot.Say(ev.Channel, "TODO: No active session")
|
||||||
|
} else {
|
||||||
|
n.bot.Say(ev.Channel, "TODO: Stopped session")
|
||||||
|
delete(n.sessions, author.ID)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case "option", "options", "option list", "list options", "o":
|
||||||
|
return n.SayTemplate(ev.Channel, templateOptionList, author)
|
||||||
|
case "option set", "options set", "set option", "set options":
|
||||||
|
if len(fields) < 4 {
|
||||||
|
n.bot.Say(ev.Channel, messageUnknownOption)
|
||||||
|
} else if err := author.SetOption(fields[2], fields[3]); err != nil {
|
||||||
|
n.bot.Say(ev.Channel, messageInvalidOption, err)
|
||||||
|
} else if err = n.bot.Store.Set(authorKey, author); err != nil {
|
||||||
|
n.bot.Say(ev.Channel, messageUnknownError)
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
n.bot.Say(ev.Channel, messageSetOption, fields[2], fields[3])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return n.SayTemplate(ev.Channel, templateUnknownCommand, cmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default executable names for required runtime dependencies.
|
||||||
|
const (
|
||||||
|
defaultInform7 = "/usr/libexec/ni"
|
||||||
|
defaultInform6 = "/usr/libexec/inform6"
|
||||||
|
defaultDumbFrotz = "/usr/bin/dfrotz"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
// Required attributes.
|
||||||
|
Bot *joe.Bot // The bot handler.
|
||||||
|
|
||||||
|
// Optional attributes.
|
||||||
|
Inform7 string // The path to the `ni` Inform 7 compiler.
|
||||||
|
Inform6 string // The path to the `inform6` Inform 6 compiler.
|
||||||
|
DumbFrotz string // The path to the `dumb-frotz` interpreter.
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(conf Config) (*Inform, error) {
|
||||||
|
if conf.Bot == nil {
|
||||||
|
return nil, errors.New("bot given is nil")
|
||||||
|
} else if conf.Bot.Store == nil {
|
||||||
|
return nil, errors.New("storage module required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default paths for runtime dependencies, if needed.
|
||||||
|
if conf.Inform7 == "" {
|
||||||
|
conf.Inform7 = defaultInform7
|
||||||
|
}
|
||||||
|
if conf.Inform6 == "" {
|
||||||
|
conf.Inform6 = defaultInform6
|
||||||
|
}
|
||||||
|
if conf.DumbFrotz == "" {
|
||||||
|
conf.DumbFrotz = defaultDumbFrotz
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify and expand paths for runtime dependencies.
|
||||||
|
if i7, err := exec.LookPath(conf.Inform7); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "Inform 7 compiler not found")
|
||||||
|
} else if i6, err := exec.LookPath(conf.Inform6); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "Inform 6 compiler not found")
|
||||||
|
} else if frotz, err := exec.LookPath(conf.DumbFrotz); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "Frotz interpreter not found")
|
||||||
|
} else {
|
||||||
|
conf.Inform7, conf.Inform6, conf.DumbFrotz = i7, i6, frotz
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Inform{
|
||||||
|
bot: conf.Bot,
|
||||||
|
config: &conf,
|
||||||
|
sessions: make(map[string]*Session),
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
package inform
|
||||||
|
|
||||||
|
import (
|
||||||
|
// Standard library
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
|
var templateOptionList = parseTemplate("option-list", `
|
||||||
|
The options currently set for '{{.ID}}' are:
|
||||||
|
> Prefix: {{with .Options.Prefix}}'{{.}}'{{else}}(None Set){{end}}
|
||||||
|
Change these options with 'option set <key> <value>'.`)
|
||||||
|
|
||||||
|
var templateStoryList = parseTemplate("story-list", `
|
||||||
|
{{if .Stories}}
|
||||||
|
The list of active stories for '{{.ID}}' are:
|
||||||
|
{{- range .Stories}}
|
||||||
|
> Name: '{{.Name}}'
|
||||||
|
> Created at: {{.CreatedAt.Format "Mon, 02 Jan 2006 15:04"}}
|
||||||
|
> Last updated at: {{.CreatedAt.Format "Mon, 02 Jan 2006 15:04"}}
|
||||||
|
{{end}}
|
||||||
|
{{else}}
|
||||||
|
There are currently no active stories available for '{{.ID}}'.
|
||||||
|
Add a new one with 'story add' or get more information with 'help story' and 'help story add'.
|
||||||
|
{{end}}`)
|
||||||
|
|
||||||
|
var templateWelcome = parseTemplate("welcome", `
|
||||||
|
Hi! 👋
|
||||||
|
|
||||||
|
It seems like this is the first time we've exchanged messages (if not, feel free to skip this), and might need some help getting up-to-speed with what I do. With no further ado:
|
||||||
|
|
||||||
|
My name is InformBot, and I'm a chat interface for Inform 7, a system for creating works of interactive fiction, via a natural-language interface. What this means, essentially, is that you can write interactive books (and more) in very much the same language that you use reading them (that is, if you tend to read books written in the English language).
|
||||||
|
|
||||||
|
Inform is much, much more useful than just for writing interactive fiction stories for individuals — it can be used to implement interactive interfaces of any kind, and keep track of complex worlds with complex state.
|
||||||
|
|
||||||
|
That's where I come in, and can help in both defining the rules and performing actions against them, both in direct messages and in group-chats.
|
||||||
|
|
||||||
|
For more information on how you can define these rules, and how to converse with me in group-chats, type 'help' and I'll respond with a list of topics you can look further into.`)
|
||||||
|
|
||||||
|
var templateHelp = parseTemplate("help", `
|
||||||
|
Inform itself is a large, complicated system, and help on writing rules way beyond the scope of this help text. You are in luck though, as Inform comes with a large amount of documentation on its website: http://inform7.com/doc
|
||||||
|
|
||||||
|
Feel free to ask any questions about, or report any issues with InformBot itself here: https://github.com/deuill/informbot`)
|
||||||
|
|
||||||
|
var templateUnknownCommand = parseTemplate("unknown-command", `
|
||||||
|
I don't understand what '{{.}}' means. Type 'help' for an overview of common commands.`)
|
||||||
|
|
||||||
|
var messageInvalidSession = `
|
||||||
|
I couldn't start the story successfully — %s.`
|
||||||
|
|
||||||
|
var messageStartedSession = `
|
||||||
|
Story '%s' successfully started.
|
||||||
|
Any subsequent meta-commands will have to be given a prefix (currently set to '%s'), and you can end this session by using the 'story end' command. Have fun! 🎉`
|
||||||
|
|
||||||
|
var messageAddedStory = `
|
||||||
|
Story '%s' successfully added to active list.`
|
||||||
|
|
||||||
|
var messageRemovedStory = `
|
||||||
|
Story '%s' successfully removed from active list.`
|
||||||
|
|
||||||
|
// TODO FIX THESE TO BE MORE GENERIC
|
||||||
|
|
||||||
|
var messageUnknownStory = `
|
||||||
|
You need to pass in both the story name and URL, e.g. 'story add some-name https://example.com/story.ni'.
|
||||||
|
Story names need to be one word (though they can contain hyphens or underscores), and not contain any spaces or other white-space characters.`
|
||||||
|
|
||||||
|
var messageInvalidStory = `
|
||||||
|
I couldn't add the story successfully — %s.`
|
||||||
|
|
||||||
|
var messageSetOption = `
|
||||||
|
Option '%s' successfully set to '%s'.`
|
||||||
|
|
||||||
|
var messageUnknownOption = `
|
||||||
|
You need to pass in both the option name and value, e.g. 'option set Prefix ?'.`
|
||||||
|
|
||||||
|
var messageInvalidOption = `
|
||||||
|
I couldn't set that option successfully — %s.`
|
||||||
|
|
||||||
|
var messageRunError = `
|
||||||
|
I could't run that command — %s.`
|
||||||
|
|
||||||
|
var messageUnknownError = `
|
||||||
|
Oops, something went wrong and I was unable to complete that request, give me a moment and try again (or ask whoever set me up for some help).`
|
||||||
|
|
||||||
|
func parseTemplate(name, content string) *template.Template {
|
||||||
|
return template.Must(template.New(name).Parse(content))
|
||||||
|
}
|
|
@ -0,0 +1,161 @@
|
||||||
|
package inform
|
||||||
|
|
||||||
|
import (
|
||||||
|
// Standard library
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
// Third-party packages
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Default arguments for executable dependencies.
|
||||||
|
var (
|
||||||
|
// The prefix used for Frotz meta-commands.
|
||||||
|
frotzMetaPrefix = "\\"
|
||||||
|
frotzArgs = []string{"-r", "lt", "-r", "cm", "-r", "ch1", "-p", "-m", "-R"}
|
||||||
|
)
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
path string
|
||||||
|
name string
|
||||||
|
|
||||||
|
proc *os.Process
|
||||||
|
|
||||||
|
in io.WriteCloser
|
||||||
|
out io.ReadCloser
|
||||||
|
err io.ReadCloser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) Run(cmd string) error {
|
||||||
|
switch strings.ToLower(cmd) {
|
||||||
|
case "restore":
|
||||||
|
return errors.New("TODO: Implement restoring")
|
||||||
|
case "save":
|
||||||
|
return errors.New("TODO: Implement saving")
|
||||||
|
case "\\x", "quit":
|
||||||
|
return errors.New("TODO: Implement session stopping")
|
||||||
|
case "script", "unscript":
|
||||||
|
return errors.New("transcripts are disabled")
|
||||||
|
case "\\<", "\\>", "\\^", "\\.": // Cursor motion
|
||||||
|
case "\\1", "\\2", "\\3", "\\4", "\\5", "\\6", "\\7", "\\8", "\\9", "\\0": // F1 - F10
|
||||||
|
case "\\n", "\\u": // Frotz hot-keys
|
||||||
|
default:
|
||||||
|
if strings.HasPrefix(cmd, frotzMetaPrefix) {
|
||||||
|
return errors.New("meta-commands are disabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := s.in.Write(append([]byte(cmd), '\n')); err != nil {
|
||||||
|
return errors.New("failed writing command")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) Output() string {
|
||||||
|
var buf = bytes.TrimSuffix(readPipe(s.out), []byte{'\n', '>'})
|
||||||
|
return string(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) Error() error {
|
||||||
|
var buf = bytes.ReplaceAll(readPipe(s.err), []byte{'\n'}, []byte{':', ' '})
|
||||||
|
if len(buf) > 0 {
|
||||||
|
return errors.New(string(buf))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) Start(ctx context.Context, conf *Config) error {
|
||||||
|
var err error
|
||||||
|
var cmd = exec.CommandContext(ctx, conf.DumbFrotz, append(frotzArgs, s.path, s.name)...)
|
||||||
|
|
||||||
|
if s.in, err = cmd.StdinPipe(); err != nil {
|
||||||
|
return errors.Wrap(err, "starting session failed")
|
||||||
|
} else if s.out, err = cmd.StdoutPipe(); err != nil {
|
||||||
|
return errors.Wrap(err, "starting session failed")
|
||||||
|
} else if s.err, err = cmd.StderrPipe(); err != nil {
|
||||||
|
return errors.Wrap(err, "starting session failed")
|
||||||
|
} else if err = cmd.Start(); err != nil {
|
||||||
|
return errors.Wrap(err, "starting session failed")
|
||||||
|
} else if err := s.Error(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.proc = cmd.Process
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Session) Close() error {
|
||||||
|
var err error
|
||||||
|
if s.proc != nil {
|
||||||
|
err = s.proc.Kill()
|
||||||
|
s.proc = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSession(story *Story) (*Session, error) {
|
||||||
|
dir, err := ioutil.TempDir(os.TempDir(), fmt.Sprintf("%s-%s-%s-*", keyPrefix, story.AuthorID, story.Name))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "creating temporary directory failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Create(path.Join(dir, "output.z8"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "writing temporary story file failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
defer f.Close()
|
||||||
|
if _, err = f.Write(story.Build); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "writing temporary story file failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Session{
|
||||||
|
path: dir,
|
||||||
|
name: f.Name(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readPipe(r io.Reader) []byte {
|
||||||
|
var chunks = make(chan []byte)
|
||||||
|
go func() {
|
||||||
|
var chunk = make([]byte, 1024)
|
||||||
|
for {
|
||||||
|
n, err := r.Read(chunk)
|
||||||
|
if err != nil || n == 0 {
|
||||||
|
close(chunks)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
chunks <- chunk[:n]
|
||||||
|
if n < 1024 {
|
||||||
|
close(chunks)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var buf []byte
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case chunk, ok := <-chunks:
|
||||||
|
if !ok {
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
buf = append(buf, chunk...)
|
||||||
|
case <-time.After(10 * time.Millisecond):
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
package inform
|
||||||
|
|
||||||
|
import (
|
||||||
|
// Standard library
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
// Third-party packages
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Default path for Inform7 data.
|
||||||
|
inform7DataDir = "/usr/share/inform7/Internal"
|
||||||
|
|
||||||
|
// Default arguments for Inform 6 and 7 compilers.
|
||||||
|
inform7Args = []string{"--noprogress", "--internal", inform7DataDir, "--format=z8"}
|
||||||
|
inform6Args = []string{"-E2wSDv8F0Cud2"}
|
||||||
|
)
|
||||||
|
|
||||||
|
type Story struct {
|
||||||
|
Name string // The user-provided name for the story.
|
||||||
|
AuthorID string // The author ID, corSayTemplates to Author.ID.
|
||||||
|
CreatedAt time.Time // The UTC timestamp this story was first added on.
|
||||||
|
UpdatedAt time.Time // The UTC timestamp this story was last updated on.
|
||||||
|
|
||||||
|
// Source and compiled Z-Code for story.
|
||||||
|
Source []byte
|
||||||
|
Build []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Story) Compile(ctx context.Context, conf *Config) error {
|
||||||
|
dir, err := ioutil.TempDir(os.TempDir(), fmt.Sprintf("%s-%s-%s-*", keyPrefix, s.AuthorID, s.Name))
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "creating temporary directory failed")
|
||||||
|
} else if err := os.Mkdir(path.Join(dir, "Source"), 0755); err != nil {
|
||||||
|
return errors.Wrap(err, "creating temporary directory failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ioutil.WriteFile(path.Join(dir, "Source", "story.ni"), s.Source, 0644); err != nil {
|
||||||
|
return errors.Wrap(err, "writing file for story failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Return verbose output.
|
||||||
|
err = exec.CommandContext(ctx, conf.Inform7, append(inform7Args, "--project", dir)...).Run()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "compilation failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = exec.CommandContext(ctx, conf.Inform6, append(inform6Args, path.Join(dir, "Build", "auto.inf"), path.Join(dir, "Build", "output.z8"))...).Run()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "compilation failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := ioutil.ReadFile(path.Join(dir, "Build", "output.z8"))
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "compilation failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Build, s.UpdatedAt = buf, time.Now().UTC()
|
||||||
|
return os.RemoveAll(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Story) WithSource(src []byte) *Story {
|
||||||
|
s.Source = src
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStory(name, authorID string) *Story {
|
||||||
|
return &Story{
|
||||||
|
Name: name,
|
||||||
|
AuthorID: authorID,
|
||||||
|
CreatedAt: time.Now().UTC(),
|
||||||
|
UpdatedAt: time.Now().UTC(),
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,350 @@
|
||||||
|
package xmpp
|
||||||
|
|
||||||
|
import (
|
||||||
|
// Standard library
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
// Third-party packages
|
||||||
|
"github.com/go-joe/joe"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"mellium.im/sasl"
|
||||||
|
"mellium.im/xmlstream"
|
||||||
|
"mellium.im/xmpp"
|
||||||
|
"mellium.im/xmpp/dial"
|
||||||
|
"mellium.im/xmpp/jid"
|
||||||
|
"mellium.im/xmpp/stanza"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultAuthMechanisms represents the list of SASL authentication mechanisms this client is allowed
|
||||||
|
// to use in server authentication.
|
||||||
|
var defaultAuthMechanisms = []sasl.Mechanism{
|
||||||
|
sasl.Plain,
|
||||||
|
sasl.ScramSha1,
|
||||||
|
sasl.ScramSha1Plus,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config represents required and optional configuration values used in setting up the XMPP bot client.
|
||||||
|
type Config struct {
|
||||||
|
// Required configuration.
|
||||||
|
JID string
|
||||||
|
Password string
|
||||||
|
|
||||||
|
// Optional configuration.
|
||||||
|
NoTLS bool // Whether to disable TLS connection to the XMPP server.
|
||||||
|
UseStartTLS bool // Whether or not connection will be allowed to be made over StartTLS.
|
||||||
|
|
||||||
|
// Other fields.
|
||||||
|
Logger *zap.Logger // The instance to use for emitting log messages.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client represents an active XMPP session against a server, and configuration for handling messages
|
||||||
|
// against a Joe instance.
|
||||||
|
type Client struct {
|
||||||
|
brain *joe.Brain // The mediator between this adapter and other handlers.
|
||||||
|
session *xmpp.Session // The active XMPP session.
|
||||||
|
logger *zap.Logger // The logger instance to use, defaults to a global logger used by Joe.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send wraps the given text in a message stanza and sets the recipient to the given channel, which
|
||||||
|
// is expected to be a JID (bare for direct messages). A error is returned if the channel JID does
|
||||||
|
// not parse, or if the message fails to send for any reason.
|
||||||
|
func (c *Client) Send(msg, channel string) error {
|
||||||
|
jid, err := jid.Parse(channel)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "parsing JID failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine whether this is a direct or group-chat message from the resource part of the JID,
|
||||||
|
// which is only set if the message was originally sent as part of a group-chat.
|
||||||
|
var kind = stanza.ChatMessage
|
||||||
|
if jid.Resourcepart() != "" {
|
||||||
|
msg = jid.Resourcepart() + ", " + msg
|
||||||
|
jid, kind = jid.Bare(), stanza.GroupChatMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
c.logger.Debug("Sending message",
|
||||||
|
zap.String("jid", jid.String()),
|
||||||
|
zap.String("type", string(kind)))
|
||||||
|
|
||||||
|
return c.session.Send(context.Background(),
|
||||||
|
xmlstream.Wrap(
|
||||||
|
xmlstream.Wrap(
|
||||||
|
xmlstream.Token(xml.CharData(msg)),
|
||||||
|
xml.StartElement{Name: xml.Name{Local: "body"}},
|
||||||
|
),
|
||||||
|
xml.StartElement{
|
||||||
|
Name: xml.Name{Local: "message"},
|
||||||
|
Attr: []xml.Attr{
|
||||||
|
{Name: xml.Name{Local: "id"}, Value: randomID()},
|
||||||
|
{Name: xml.Name{Local: "to"}, Value: jid.String()},
|
||||||
|
{Name: xml.Name{Local: "type"}, Value: string(kind)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GroupInfo represents information needed for joining a MUC, either automatically or as part of an
|
||||||
|
// invite (direct or mediated).
|
||||||
|
type GroupInfo struct {
|
||||||
|
Channel jid.JID `xml:"-"`
|
||||||
|
Password string `xml:"password`
|
||||||
|
Invite struct {
|
||||||
|
From jid.JID `xml:"from,attr"`
|
||||||
|
} `xml:"invite"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageStanza represents an XMPP message stanza, commonly used for transferring chat messages
|
||||||
|
// among users or group-chats.
|
||||||
|
type MessageStanza struct {
|
||||||
|
// Base, common fields.
|
||||||
|
stanza.Message
|
||||||
|
Body string `xml:"body"`
|
||||||
|
|
||||||
|
// Additional, optional fields.
|
||||||
|
Group GroupInfo `xml:"x"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleInvite responds to the given invite (direct or mediated) with an 'available' presence,
|
||||||
|
// which allows the client to participate in MUCs.
|
||||||
|
func (c *Client) HandleInvite(w xmlstream.TokenWriter, info *GroupInfo) error {
|
||||||
|
jid, err := info.Channel.WithResource(c.session.LocalAddr().Localpart())
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "setting JID for MUC failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = xmlstream.Copy(w, xmlstream.Wrap(
|
||||||
|
xmlstream.Wrap(
|
||||||
|
xmlstream.MultiReader(
|
||||||
|
xmlstream.Wrap(
|
||||||
|
xmlstream.Token(xml.CharData(info.Password)),
|
||||||
|
xml.StartElement{Name: xml.Name{Local: "password"}},
|
||||||
|
),
|
||||||
|
xmlstream.Wrap(nil, xml.StartElement{
|
||||||
|
Name: xml.Name{Local: "history"},
|
||||||
|
Attr: []xml.Attr{
|
||||||
|
{Name: xml.Name{Local: "maxchars"}, Value: "0"},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
xml.StartElement{
|
||||||
|
Name: xml.Name{Local: "x"},
|
||||||
|
Attr: []xml.Attr{
|
||||||
|
{Name: xml.Name{Local: "xmlns"}, Value: "http://jabber.org/protocol/muc"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
stanza.Presence{
|
||||||
|
ID: randomID(),
|
||||||
|
Type: stanza.AvailablePresence,
|
||||||
|
To: jid,
|
||||||
|
}.StartElement(),
|
||||||
|
))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "setting presence for MUC failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleMessage parses the given MessageStanza, validating its contents and responding either as a
|
||||||
|
// direct message, or as a group-chat mention, depending on the intent. HandleMessage will also handle
|
||||||
|
// invites to group-chats, joining these automatically and with no confirmation needed.
|
||||||
|
//
|
||||||
|
// By default, only messages prepended with the local part of the client JID will be responded to in
|
||||||
|
// group-chats; this is to avoid handling messages where this is not wanted. Such mentions will be,
|
||||||
|
// in turn, responded to with a mention for the sending user.
|
||||||
|
//
|
||||||
|
// Currently, only mediated invites (XEP-0045) are handled, and rooms are not re-joined if the client
|
||||||
|
// closes its connection to the server.
|
||||||
|
func (c *Client) HandleMessage(w xmlstream.TokenWriter, msg *MessageStanza) error {
|
||||||
|
var authorID = msg.From.Bare().String()
|
||||||
|
var channel = msg.From.Bare().String()
|
||||||
|
|
||||||
|
switch msg.Type {
|
||||||
|
case stanza.GroupChatMessage:
|
||||||
|
// Don't handle messages that aren't intended for us.
|
||||||
|
n := strings.ToLower(c.session.LocalAddr().Localpart())
|
||||||
|
if len(msg.Body) <= len(n) || strings.ToLower(msg.Body[:len(n)]) != n {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
channel = msg.From.String()
|
||||||
|
msg.Body = strings.Trim(msg.Body[len(n):], " ,:")
|
||||||
|
fallthrough
|
||||||
|
case stanza.ChatMessage:
|
||||||
|
// Do not attempt to handle empty or invalid messages.
|
||||||
|
if msg.Body == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c.brain.Emit(joe.ReceiveMessageEvent{
|
||||||
|
ID: msg.ID,
|
||||||
|
Text: msg.Body,
|
||||||
|
AuthorID: authorID,
|
||||||
|
Channel: channel,
|
||||||
|
Data: msg,
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
// Check if message is a mediated MUC invite, and join MUC if so.
|
||||||
|
if !msg.Group.Invite.From.Equal(jid.JID{}) {
|
||||||
|
msg.Group.Channel = msg.From.Bare()
|
||||||
|
return c.HandleInvite(w, &msg.Group)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PresenceStanza represents an XMPP presence stanza, commonly used for communicating
|
||||||
|
// availability.
|
||||||
|
type PresenceStanza struct {
|
||||||
|
// Base, common fields.
|
||||||
|
stanza.Presence
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandlePresence parses the given PresenceStanza and responds (usually to the affirmative),
|
||||||
|
// depending on the presence type, e.g. for subscription requests, HandlePresence will automatically
|
||||||
|
// subscribe and respond. Any errors returned in parsing on responding will be returned.
|
||||||
|
func (c *Client) HandlePresence(w xmlstream.TokenWriter, p *PresenceStanza) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Handle presence stanza based on type.
|
||||||
|
switch p.Type {
|
||||||
|
case stanza.SubscribePresence:
|
||||||
|
// Respond to subscription requests automatically.
|
||||||
|
_, err = xmlstream.Copy(w, stanza.Presence{
|
||||||
|
ID: randomID(),
|
||||||
|
Type: stanza.SubscribedPresence,
|
||||||
|
To: p.From,
|
||||||
|
}.Wrap(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleXMPP parses incoming XML tokens and calls a corresponding handler, e.g. HandleMessage, for
|
||||||
|
// the stanza type represented. Unhandled stanza types will be ignored with no error returned.
|
||||||
|
func (c *Client) HandleXMPP(t xmlstream.TokenReadEncoder, start *xml.StartElement) error {
|
||||||
|
var stanza interface{}
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch start.Name.Local {
|
||||||
|
case "message":
|
||||||
|
stanza = &MessageStanza{}
|
||||||
|
case "presence":
|
||||||
|
stanza = &PresenceStanza{}
|
||||||
|
default:
|
||||||
|
c.logger.Debug("Ignoring unknown stanza type", zap.String("type", start.Name.Local))
|
||||||
|
return nil // Unknown stanza type, do not handle.
|
||||||
|
}
|
||||||
|
|
||||||
|
err = xml.NewTokenDecoder(t).DecodeElement(&stanza, start)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
c.logger.Error("Decoding element failed", zap.Error(err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch start.Name.Local {
|
||||||
|
case "message":
|
||||||
|
if err := c.HandleMessage(t, stanza.(*MessageStanza)); err != nil {
|
||||||
|
c.logger.Error("Handling message failed", zap.Error(err))
|
||||||
|
}
|
||||||
|
case "presence":
|
||||||
|
if err := c.HandlePresence(t, stanza.(*PresenceStanza)); err != nil {
|
||||||
|
c.logger.Error("Handling presence failed", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterAt sets the Joe Brain instance for the XMPP client.
|
||||||
|
func (c *Client) RegisterAt(brain *joe.Brain) {
|
||||||
|
c.brain = brain
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close shuts down the active XMPP session and server connection, returning an error if the process
|
||||||
|
// fails at any point.
|
||||||
|
func (c *Client) Close() error {
|
||||||
|
if err := c.session.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.session.Conn().Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adapter initializes an XMPP client connection according to configuration given, and returns a Joe
|
||||||
|
// module, usable in calls to joe.New(), or an error if any occurs.
|
||||||
|
func Adapter(conf Config) joe.Module {
|
||||||
|
return joe.ModuleFunc(func(joeConf *joe.Config) error {
|
||||||
|
// Parse and set up JID.
|
||||||
|
id, err := jid.Parse(conf.JID)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "parsing JID failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
var ctx = context.Background()
|
||||||
|
var dialer = &dial.Dialer{NoTLS: conf.NoTLS}
|
||||||
|
|
||||||
|
// Initialze connection according to configuration.
|
||||||
|
conn, err := dialer.Dial(ctx, "tcp", id)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "establishing connection failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable optional features and initialize client session, according to configuration.
|
||||||
|
features := []xmpp.StreamFeature{xmpp.BindResource()}
|
||||||
|
if conf.UseStartTLS {
|
||||||
|
features = append(features, xmpp.StartTLS(true, &tls.Config{ServerName: id.Domain().String()}))
|
||||||
|
}
|
||||||
|
if conf.Password != "" {
|
||||||
|
features = append(features, xmpp.SASL("", conf.Password, defaultAuthMechanisms...))
|
||||||
|
}
|
||||||
|
|
||||||
|
sess, err := xmpp.NewClientSession(ctx, id, conn, false, features...)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "establishing session failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
var c = &Client{session: sess, logger: conf.Logger}
|
||||||
|
if c.logger == nil {
|
||||||
|
c.logger = joeConf.Logger(id.Network())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send initial presence to let the server know we want to receive messages.
|
||||||
|
err = c.session.Send(ctx, stanza.Presence{Type: stanza.AvailablePresence}.Wrap(nil))
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "setting initial presence failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
go c.session.Serve(c)
|
||||||
|
joeConf.SetAdapter(c)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// RandomID returns a cryptographically secure, 16-byte random string, useful for adding to stanzas
|
||||||
|
// for uniquely identifying them.
|
||||||
|
func randomID() string {
|
||||||
|
var buf = make([]byte, 16)
|
||||||
|
rand.Reader.Read(buf)
|
||||||
|
return fmt.Sprintf("%x", buf)[:16]
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
// Standard library
|
||||||
|
"os"
|
||||||
|
|
||||||
|
// Internal packages
|
||||||
|
"github.com/deuill/informbot/joe-inform-handler"
|
||||||
|
"github.com/deuill/informbot/joe-xmpp-adapter"
|
||||||
|
|
||||||
|
// Third-party packages
|
||||||
|
"github.com/go-joe/file-memory"
|
||||||
|
"github.com/go-joe/joe"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
bot := joe.New(
|
||||||
|
"inform",
|
||||||
|
xmpp.Adapter(xmpp.Config{
|
||||||
|
JID: os.Getenv("INFORMBOT_JID"),
|
||||||
|
Password: os.Getenv("INFORMBOT_PASSWORD"),
|
||||||
|
NoTLS: os.Getenv("INFORMBOT_NO_TLS") == "true",
|
||||||
|
UseStartTLS: os.Getenv("INFORMBOT_USE_STARTTLS") == "true",
|
||||||
|
}),
|
||||||
|
file.Memory("store.json"),
|
||||||
|
)
|
||||||
|
|
||||||
|
in, err := inform.New(inform.Config{Bot: bot})
|
||||||
|
if err != nil {
|
||||||
|
bot.Logger.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
bot.Brain.RegisterHandler(in.Handle)
|
||||||
|
if err := bot.Run(); err != nil {
|
||||||
|
bot.Logger.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
# The patch diff coverage report in pull requests is confusing at best so its
|
||||||
|
# disabled. Note that normal coverage is still collected.
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
status:
|
||||||
|
patch:
|
||||||
|
default:
|
||||||
|
enabled: no
|
|
@ -0,0 +1,57 @@
|
||||||
|
linters-settings:
|
||||||
|
govet:
|
||||||
|
check-shadowing: true
|
||||||
|
golint:
|
||||||
|
min-confidence: 0
|
||||||
|
gocyclo:
|
||||||
|
min-complexity: 10
|
||||||
|
maligned:
|
||||||
|
suggest-new: true
|
||||||
|
goconst:
|
||||||
|
min-len: 2
|
||||||
|
min-occurrences: 2
|
||||||
|
misspell:
|
||||||
|
locale: US
|
||||||
|
lll:
|
||||||
|
line-length: 140
|
||||||
|
goimports:
|
||||||
|
local-prefixes: github.com/golangci/golangci-lint
|
||||||
|
gocritic:
|
||||||
|
enabled-tags:
|
||||||
|
- performance
|
||||||
|
- style
|
||||||
|
- experimental
|
||||||
|
- diagnostic
|
||||||
|
- opinionated
|
||||||
|
disabled-checks:
|
||||||
|
- unnamedResult
|
||||||
|
- hugeParam
|
||||||
|
|
||||||
|
linters:
|
||||||
|
enable-all: true
|
||||||
|
disable:
|
||||||
|
- wsl
|
||||||
|
- gomnd
|
||||||
|
|
||||||
|
service:
|
||||||
|
golangci-lint-version: 1.23.x # use the fixed version to not introduce new linters unexpectedly
|
||||||
|
prepare:
|
||||||
|
- GO111MODULE=on go mod vendor # required currently or golangci breaks
|
||||||
|
|
||||||
|
issues:
|
||||||
|
exclude-use-default: false
|
||||||
|
exclude-rules:
|
||||||
|
- text: "should have a package comment, unless it's in another file for this package"
|
||||||
|
linters:
|
||||||
|
- golint
|
||||||
|
|
||||||
|
- text: "Potential file inclusion via variable"
|
||||||
|
path: "memory\\.go"
|
||||||
|
|
||||||
|
- text: "Expect file permissions to be 0600 or less"
|
||||||
|
path: "memory\\.go"
|
||||||
|
|
||||||
|
- text: "Error return value of `os.Remove` is not checked"
|
||||||
|
linters:
|
||||||
|
- errcheck
|
||||||
|
path: "memory_test\\.go"
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Changelog
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
- Nothing so far
|
||||||
|
|
||||||
|
## [v1.0.0] - 2020-02-28
|
||||||
|
- Use error wrapping of standard library instead of github.com/pkg/errors
|
||||||
|
- Update to Go 1.14
|
||||||
|
- Release first stable version and start following semantic versioning with regards to backwards compatibility
|
||||||
|
|
||||||
|
## [v0.3.1] - 2019-10-01
|
||||||
|
- Make `Keys()` function deterministic by sorting the output.
|
||||||
|
|
||||||
|
## [v0.3.0] - 2019-04-21
|
||||||
|
- Update to new Joe Memory interface
|
||||||
|
|
||||||
|
## [v0.2.0] - 2019-03-18
|
||||||
|
- Update to the changed Module interface of joe v0.4.0
|
||||||
|
|
||||||
|
## [v0.1.0] - 2019-03-03
|
||||||
|
|
||||||
|
Initial alpha release
|
||||||
|
|
||||||
|
[Unreleased]: https://github.com/go-joe/redis-memory/compare/v1.0.0...HEAD
|
||||||
|
[v1.0.0]: https://github.com/go-joe/file-memory/compare/v0.3.1...v1.0.0
|
||||||
|
[v0.3.1]: https://github.com/go-joe/file-memory/compare/v0.3.0...v0.3.1
|
||||||
|
[v0.3.0]: https://github.com/go-joe/file-memory/compare/v0.2.0...v0.3.0
|
||||||
|
[v0.2.0]: https://github.com/go-joe/file-memory/compare/v0.1.0...v0.2.0
|
||||||
|
[v0.1.0]: https://github.com/go-joe/file-memory/releases/tag/v0.1.0
|
|
@ -0,0 +1,99 @@
|
||||||
|
# Contributing
|
||||||
|
|
||||||
|
When contributing to this repository, please first discuss the change you wish
|
||||||
|
to make via an [issue on Github][issues] *before* making a change.
|
||||||
|
|
||||||
|
Please note we have a code of conduct. Please follow it in all your interactions
|
||||||
|
with the project.
|
||||||
|
|
||||||
|
## Pull Request Process
|
||||||
|
|
||||||
|
0. Everything should start with an issue: ["Talk, then code"][talk-code]
|
||||||
|
1. Cover all your changes with unit tests, when unsure how, ask for help
|
||||||
|
2. Run all unit tests with the race detector on
|
||||||
|
3. Run the linters locally via `golangci-lint run`
|
||||||
|
4. Update the [CHANGELOG.md](CHANGELOG.md) with the changes you made (in the "Unreleased" section)
|
||||||
|
5. Consider updating the [README.md](README.md) with details of your changes.
|
||||||
|
When in doubt, lets discuss the need together in the corresponding Github issue.
|
||||||
|
6. Consider if you have to increase the version numbers in any examples files and
|
||||||
|
the README.md to the new version that this Pull Request would represent.
|
||||||
|
The versioning scheme we use is [SemVer](http://semver.org/).
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
We follow the **Gopher Code of Conduct** as described at https://golang.org/conduct `\ʕ◔ϖ◔ʔ/`
|
||||||
|
|
||||||
|
### Our Pledge
|
||||||
|
|
||||||
|
In the interest of fostering an open and welcoming environment, we as
|
||||||
|
contributors and maintainers pledge to making participation in our project and
|
||||||
|
our community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, disability, ethnicity, gender identity and expression, level of experience,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity and
|
||||||
|
orientation.
|
||||||
|
|
||||||
|
### Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to creating a positive environment
|
||||||
|
include:
|
||||||
|
|
||||||
|
* Using welcoming and inclusive language
|
||||||
|
* Being respectful of differing viewpoints and experiences
|
||||||
|
* Gracefully accepting constructive criticism
|
||||||
|
* Focusing on what is best for the community
|
||||||
|
* Showing empathy towards other community members
|
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||||
|
advances
|
||||||
|
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or electronic
|
||||||
|
address, without explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
### Our Responsibilities
|
||||||
|
|
||||||
|
Project maintainers are responsible for clarifying the standards of acceptable
|
||||||
|
behavior and are expected to take appropriate and fair corrective action in
|
||||||
|
response to any instances of unacceptable behavior.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or
|
||||||
|
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||||
|
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||||
|
permanently any contributor for other behaviors that they deem inappropriate,
|
||||||
|
threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies both within project spaces and in public spaces
|
||||||
|
when an individual is representing the project or its community. Examples of
|
||||||
|
representing a project or community include using an official project e-mail
|
||||||
|
address, posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event. Representation of a project may be
|
||||||
|
further defined and clarified by project maintainers.
|
||||||
|
|
||||||
|
### Conflict Resolution
|
||||||
|
|
||||||
|
We do not believe that all conflict is bad; healthy debate and disagreement
|
||||||
|
often yield positive results. However, it is never okay to be disrespectful or
|
||||||
|
to engage in behavior that violates the project’s code of conduct.
|
||||||
|
|
||||||
|
If you see someone violating the code of conduct, you are encouraged to address
|
||||||
|
the behavior directly with those involved. Many issues can be resolved quickly
|
||||||
|
and easily, and this gives people more control over the outcome of their dispute.
|
||||||
|
If you are unable to resolve the matter for any reason, or if the behavior is
|
||||||
|
threatening or harassing, report it. We are dedicated to providing an environment
|
||||||
|
where participants feel welcome and safe.
|
||||||
|
|
||||||
|
### Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||||
|
available at [http://contributor-covenant.org/version/1/4][version]
|
||||||
|
|
||||||
|
[issues]: https://github.com/go-joe/file-memory/issues
|
||||||
|
[talk-code]: https://dave.cheney.net/2019/02/18/talk-then-code
|
||||||
|
[homepage]: http://contributor-covenant.org
|
||||||
|
[version]: http://contributor-covenant.org/version/1/4/
|
|
@ -0,0 +1,27 @@
|
||||||
|
Copyright (c) 2019, Friedrich Große
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
may be used to endorse or promote products derived from this software without
|
||||||
|
specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,60 @@
|
||||||
|
<h1 align="center">Joe Bot - File Storage</h1>
|
||||||
|
<p align="center">Basic file storage memory adapater. https://github.com/go-joe/joe</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://circleci.com/gh/go-joe/file-memory/tree/master"><img src="https://circleci.com/gh/go-joe/file-memory/tree/master.svg?style=shield"></a>
|
||||||
|
<a href="https://goreportcard.com/report/github.com/go-joe/file-memory"><img src="https://goreportcard.com/badge/github.com/go-joe/file-memory"></a>
|
||||||
|
<a href="https://codecov.io/gh/go-joe/file-memory"><img src="https://codecov.io/gh/go-joe/file-memory/branch/master/graph/badge.svg"/></a>
|
||||||
|
<a href="https://pkg.go.dev/github.com/go-joe/file-memory?tab=doc"><img src="https://img.shields.io/badge/godoc-reference-blue.svg?color=blue"></a>
|
||||||
|
<a href="https://github.com/go-joe/file-memory/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-BSD--3--Clause-blue.svg"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
This repository contains a module for the [Joe Bot library][joe].
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
This library is packaged as [Go module][go-modules]. You can get it via:
|
||||||
|
|
||||||
|
```
|
||||||
|
go get github.com/go-joe/file-memory
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example usage
|
||||||
|
|
||||||
|
```go
|
||||||
|
b := &ExampleBot{
|
||||||
|
Bot: joe.New("example", file.Memory("foobar.json")),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Built With
|
||||||
|
|
||||||
|
* [testify](https://github.com/stretchr/testify) - A simple unit test library
|
||||||
|
* [zap](https://github.com/uber-go/zap) - Blazing fast, structured, leveled logging in Go
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
If you want to hack on this repository, please read the short [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||||
|
guide first.
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
We use [SemVer](http://semver.org/) for versioning. For the versions available,
|
||||||
|
see the [tags on this repository][tags].
|
||||||
|
|
||||||
|
## Authors
|
||||||
|
|
||||||
|
- **Friedrich Große** - *Initial work* - [fgrosse](https://github.com/fgrosse)
|
||||||
|
- **Stefan Warman** - *Unit tests* - [warmans](https://github.com/warmans)
|
||||||
|
|
||||||
|
See also the list of [contributors][contributors] who participated in this project.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the BSD-3-Clause License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
[joe]: https://github.com/go-joe/joe
|
||||||
|
[go-modules]: https://github.com/golang/go/wiki/Modules
|
||||||
|
[tags]: https://github.com/go-joe/file-memory/tags
|
||||||
|
[contributors]: https://github.com/go-joe/file-memory/contributors
|
|
@ -0,0 +1,10 @@
|
||||||
|
module github.com/go-joe/file-memory
|
||||||
|
|
||||||
|
go 1.13
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-joe/joe v0.8.0
|
||||||
|
github.com/pkg/errors v0.8.1
|
||||||
|
github.com/stretchr/testify v1.3.0
|
||||||
|
go.uber.org/zap v1.9.1
|
||||||
|
)
|
|
@ -0,0 +1,18 @@
|
||||||
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/go-joe/joe v0.8.0 h1:q/S16mDS31uw9eqatuytylgapOU6tYqxXVmQIa2mDts=
|
||||||
|
github.com/go-joe/joe v0.8.0/go.mod h1:fjDMMKm6GV29+egH/IS57PTKHSBMquckyuM7CmXbUQw=
|
||||||
|
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
|
||||||
|
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
|
||||||
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
|
go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o=
|
||||||
|
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
|
@ -0,0 +1,189 @@
|
||||||
|
package file
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/go-joe/joe"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// memory is an implementation of a joe.Memory which stores all values as a JSON
|
||||||
|
// encoded file. Note that there is no need for a joe.Memory to handle
|
||||||
|
// synchronization for concurrent access (e.g. via locks) because this is
|
||||||
|
// automatically handled by the joe.Brain.
|
||||||
|
type memory struct {
|
||||||
|
path string
|
||||||
|
logger *zap.Logger
|
||||||
|
data map[string][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory is a joe.Option which is supposed to be passed to joe.New(…) to
|
||||||
|
// configure a new bot. The path indicates the destination file at which the
|
||||||
|
// memory will store its values encoded as JSON object. If there is already a
|
||||||
|
// JSON encoded file at the given path it will be loaded and decoded into memory
|
||||||
|
// to serve future requests. If the file exists but cannot be opened or does not
|
||||||
|
// contain a valid JSON object its error will be deferred until the bot is
|
||||||
|
// actually started via its Run() function.
|
||||||
|
//
|
||||||
|
// Example usage:
|
||||||
|
// b := joe.New("example",
|
||||||
|
// file.Memory("/tmp/joe.json"),
|
||||||
|
// …
|
||||||
|
// )
|
||||||
|
func Memory(path string) joe.Module {
|
||||||
|
return joe.ModuleFunc(func(conf *joe.Config) error {
|
||||||
|
memory, err := NewMemory(path, WithLogger(conf.Logger("memory")))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
conf.SetMemory(memory)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMemory creates a new Memory instance that persists all values to the given
|
||||||
|
// path. If there is already a JSON encoded file at the given path it is loaded
|
||||||
|
// and decoded into memory to serve future requests. An error is returned if the
|
||||||
|
// file exists but cannot be opened or does not contain a valid JSON object.
|
||||||
|
func NewMemory(path string, opts ...Option) (joe.Memory, error) {
|
||||||
|
memory := &memory{
|
||||||
|
path: path,
|
||||||
|
data: map[string][]byte{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
err := opt(memory)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if memory.logger == nil {
|
||||||
|
memory.logger = zap.NewNop()
|
||||||
|
}
|
||||||
|
|
||||||
|
memory.logger.Debug("Opening memory file", zap.String("path", path))
|
||||||
|
f, err := os.Open(path)
|
||||||
|
switch {
|
||||||
|
case os.IsNotExist(err):
|
||||||
|
memory.logger.Debug("File does not exist. Continuing with empty memory", zap.String("path", path))
|
||||||
|
case err != nil:
|
||||||
|
return nil, fmt.Errorf("failed to open file: %w", err)
|
||||||
|
default:
|
||||||
|
memory.logger.Debug("Decoding JSON from memory file", zap.String("path", path))
|
||||||
|
err := json.NewDecoder(f).Decode(&memory.data)
|
||||||
|
_ = f.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed decode data as JSON: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
memory.logger.Info("Memory initialized successfully",
|
||||||
|
zap.String("path", path),
|
||||||
|
zap.Int("num_memories", len(memory.data)),
|
||||||
|
)
|
||||||
|
|
||||||
|
return memory, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set assign the key to the value and then saves the updated memory to its JSON
|
||||||
|
// file. An error is returned if this function is called after the memory was
|
||||||
|
// closed already or if the file could not be written or updated.
|
||||||
|
func (m *memory) Set(key string, value []byte) error {
|
||||||
|
if m.data == nil {
|
||||||
|
return errors.New("brain was already shut down")
|
||||||
|
}
|
||||||
|
|
||||||
|
m.data[key] = value
|
||||||
|
return m.persist()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns the value that is associated with the given key. The second
|
||||||
|
// return value indicates if the key actually existed in the memory.
|
||||||
|
//
|
||||||
|
// An error is only returned if this function is called after the memory was
|
||||||
|
// closed already.
|
||||||
|
func (m *memory) Get(key string) ([]byte, bool, error) {
|
||||||
|
if m.data == nil {
|
||||||
|
return nil, false, errors.New("brain was already shut down")
|
||||||
|
}
|
||||||
|
|
||||||
|
value, ok := m.data[key]
|
||||||
|
return value, ok, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes any value that might have been assigned to the key earlier.
|
||||||
|
// The boolean return value indicates if the memory contained the key. If it did
|
||||||
|
// not contain the key the function does nothing and returns without an error.
|
||||||
|
// If the key existed it is removed and the corresponding JSON file is updated.
|
||||||
|
//
|
||||||
|
// An error is returned if this function is called after the memory was closed
|
||||||
|
// already or if the file could not be written or updated.
|
||||||
|
func (m *memory) Delete(key string) (bool, error) {
|
||||||
|
if m.data == nil {
|
||||||
|
return false, errors.New("brain was already shut down")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok := m.data[key]
|
||||||
|
if !ok {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(m.data, key)
|
||||||
|
return ok, m.persist()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keys returns a list of all keys known to this memory.
|
||||||
|
// An error is only returned if this function is called after the memory was
|
||||||
|
// closed already.
|
||||||
|
func (m *memory) Keys() ([]string, error) {
|
||||||
|
if m.data == nil {
|
||||||
|
return nil, errors.New("brain was already shut down")
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := make([]string, 0, len(m.data))
|
||||||
|
for k := range m.data {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
// provide a stable result
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
return keys, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close removes all data from the memory. Note that all calls to the memory
|
||||||
|
// will fail after this function has been called.
|
||||||
|
func (m *memory) Close() error {
|
||||||
|
if m.data == nil {
|
||||||
|
return errors.New("brain was already closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
m.data = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *memory) persist() error {
|
||||||
|
f, err := os.OpenFile(m.path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0660)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open file to persist data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.NewEncoder(f).Encode(m.data)
|
||||||
|
if err != nil {
|
||||||
|
_ = f.Close()
|
||||||
|
return fmt.Errorf("failed to encode data as JSON: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = f.Close()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to close file; data might not have been fully persisted to disk: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
// Package file implements file based memory for the Joe bot library.
|
||||||
|
// https://github.com/go-joe/joe
|
||||||
|
package file
|
||||||
|
|
||||||
|
import "go.uber.org/zap"
|
||||||
|
|
||||||
|
// Option corresponds to a configuration setting of the file memory.
|
||||||
|
// All available options are the exported functions of this package that share
|
||||||
|
// the prefix "With" in their names.
|
||||||
|
type Option func(*memory) error
|
||||||
|
|
||||||
|
// WithLogger is a memory option that allows the caller to set a different
|
||||||
|
// logger. By default this option is not required because the file.Memory(…)
|
||||||
|
// function automatically uses the logger of the given joe.Config.
|
||||||
|
func WithLogger(logger *zap.Logger) Option {
|
||||||
|
return func(memory *memory) error {
|
||||||
|
memory.logger = logger
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IDEA: encrypted brain?
|
||||||
|
// IDEA: only decrypt keys on demand?
|
|
@ -0,0 +1,8 @@
|
||||||
|
# The patch diff coverage report in pull requests is confusing at best so its
|
||||||
|
# disabled. Note that normal coverage is still collected.
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
status:
|
||||||
|
patch:
|
||||||
|
default:
|
||||||
|
enabled: no
|
|
@ -0,0 +1,82 @@
|
||||||
|
linters-settings:
|
||||||
|
govet:
|
||||||
|
check-shadowing: false
|
||||||
|
golint:
|
||||||
|
min-confidence: 0
|
||||||
|
gocyclo:
|
||||||
|
min-complexity: 10
|
||||||
|
maligned:
|
||||||
|
suggest-new: true
|
||||||
|
dupl:
|
||||||
|
threshold: 100
|
||||||
|
goconst:
|
||||||
|
min-len: 2
|
||||||
|
min-occurrences: 2
|
||||||
|
misspell:
|
||||||
|
locale: US
|
||||||
|
lll:
|
||||||
|
line-length: 140
|
||||||
|
goimports:
|
||||||
|
local-prefixes: github.com/golangci/golangci-lint
|
||||||
|
gocritic:
|
||||||
|
enabled-tags:
|
||||||
|
- performance
|
||||||
|
- style
|
||||||
|
- experimental
|
||||||
|
- diagnostic
|
||||||
|
- opinionated
|
||||||
|
disabled-checks:
|
||||||
|
- unnamedResult
|
||||||
|
|
||||||
|
linters:
|
||||||
|
enable-all: true
|
||||||
|
disable:
|
||||||
|
- maligned
|
||||||
|
- prealloc
|
||||||
|
- depguard
|
||||||
|
- interfacer
|
||||||
|
|
||||||
|
service:
|
||||||
|
golangci-lint-version: 1.19.x # use the fixed version to not introduce new linters unexpectedly
|
||||||
|
prepare:
|
||||||
|
- GO111MODULE=on go mod vendor # required currently or golangci breaks
|
||||||
|
|
||||||
|
issues:
|
||||||
|
exclude-use-default: false
|
||||||
|
exclude-rules:
|
||||||
|
- text: "G104: Errors unhandled."
|
||||||
|
path: ".+_test\\.go"
|
||||||
|
linters:
|
||||||
|
- gosec
|
||||||
|
|
||||||
|
- text: "should have a package comment, unless it's in another file for this package"
|
||||||
|
linters:
|
||||||
|
- golint
|
||||||
|
|
||||||
|
- text: "Using the variable on range scope `c` in function literal"
|
||||||
|
path: ".+_test\\.go"
|
||||||
|
linters:
|
||||||
|
- scopelint
|
||||||
|
|
||||||
|
- text: "`ctx` is a global variable"
|
||||||
|
path: ".+_test\\.go"
|
||||||
|
linters:
|
||||||
|
- gochecknoglobals
|
||||||
|
|
||||||
|
- text: "Function 'TestBrain_RegisterHandler' is too long"
|
||||||
|
path: ".+_test\\.go"
|
||||||
|
linters:
|
||||||
|
- funlen
|
||||||
|
|
||||||
|
- text: "Line contains TODO/BUG/FIXME"
|
||||||
|
linters:
|
||||||
|
- godox
|
||||||
|
|
||||||
|
- text: "hugeParam"
|
||||||
|
linters:
|
||||||
|
- gocritic
|
||||||
|
|
||||||
|
- text: "should have comment or be unexported"
|
||||||
|
path: "main.go"
|
||||||
|
linters:
|
||||||
|
- golint
|
|
@ -0,0 +1,102 @@
|
||||||
|
# Changelog
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
|
**THIS SOFTWARE IS STILL IN ALPHA AND THERE ARE NO GUARANTEES REGARDING API STABILITY YET.**
|
||||||
|
|
||||||
|
Once we reach the v1.0 release, this project will adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
Nothing so far
|
||||||
|
|
||||||
|
## [v0.10.0] - 2019-10-26
|
||||||
|
- Allow event handlers to also use scalar event types (fixes #14)
|
||||||
|
- Add new `FinishEventContent(…)` function to finish event processing with multiple handlers early
|
||||||
|
- **Breaking change:** Message handlers registered via `Bot.Respond(…)` and `Bot.RespondRegex(…)` now abort early if the pattern matches
|
||||||
|
- This allows users to specify a default response when nothing else matches (see #25)
|
||||||
|
|
||||||
|
## [v0.9.0] - 2019-10-22
|
||||||
|
- Add `Auth.Users()` and `Auth.UserPermissions(…)` functions to allow retrieving all users as well as users permissions.
|
||||||
|
- Allow adapters to implement the optional `ReactionAwareAdapter` interface if they support emoji reactions
|
||||||
|
- Add new `reactions` package which contains a compiled list of all officially supported reactions
|
||||||
|
- Components may now return the new `ErrNotImplemented` if they do not support a feature
|
||||||
|
- Add new `reactions.Event` that may be emitted by an Adapter so users can listen for it
|
||||||
|
|
||||||
|
## [v0.8.0] - 2019-04-21
|
||||||
|
- Make `Auth.Grant(…)` idempotent and do not unnecessarily add smaller scopes
|
||||||
|
- Support extending permissions via `Auth.Grant(…)`
|
||||||
|
- Add boolean return value to `Auth.Grant(…)` to indicate if a new permission was granted
|
||||||
|
- Add `Auth.Revoke(…)` to remove permissions
|
||||||
|
- Fix flaky unit test TestBrain_Memory
|
||||||
|
- Fix flaky TestCLIAdapter_Register test
|
||||||
|
- Add new `Storage` type which manages encoding/decoding, concurrent access and logging for a `Memory`
|
||||||
|
- Factor out `Memory` related logic from Brain into new `Storage` type
|
||||||
|
- Removed `Brain.SetMemory(…)`, `Brain.Set(…)`, `Brain.Get(…)`, `Brain.Delete(…)`, `Brain.Memories(…)`, `Brain.Close(…)`
|
||||||
|
- All functions above except `Brain.Memories(…)` are now available as functions on the `Bot.Store` field
|
||||||
|
- The `Auth` type no longer uses the `Memory` interface but instead requires an instance of the new `Storage` type
|
||||||
|
- Removed the `BrainMemoryEvent` without replacement
|
||||||
|
- Add `joetest.Storage` type to streamline making assertions on a bots storage/memory
|
||||||
|
- Change the `Memory` interface to treat values as `[]byte` and not `string`
|
||||||
|
- Remove `Memories()` function from `Memory` interface and instead add a `Keys()` function
|
||||||
|
- `NewConfig(…)` now requires an instance of a `Storage`
|
||||||
|
|
||||||
|
## [v0.7.0] - 2019-04-18
|
||||||
|
- Add ReceiveMessageEvent.Data field to allow using the underlying message type of the adapters
|
||||||
|
- Add ReceiveMessageEvent.AuthorID field to identify the author of the message
|
||||||
|
- Add Message.Data field which contains a copy of the ReceiveMessageEvent.Data value
|
||||||
|
- Add Message.AuthorID field which contains a copy of the ReceiveMessageEvent.AuthorID value
|
||||||
|
- Add Auth.Grant(…) and Auth.CheckPermission(…) functions to allow implementing user permissions
|
||||||
|
- Add Brain.Close() function to let the brain implement the Memory interface
|
||||||
|
- Add Brain.SetMemory(…) function to give more control over a joe.Brain
|
||||||
|
- Fix joetest.Bot.Start(…) function to return only when actually _all_ initialization is done
|
||||||
|
|
||||||
|
## [v0.6.0] - 2019-03-30
|
||||||
|
- implement `NewConfig` function to allow create configuration for unit tests of modules
|
||||||
|
|
||||||
|
## [v0.5.0] - 2019-03-18
|
||||||
|
- Fixed nil pointer panic in slack adapter when context is nil
|
||||||
|
|
||||||
|
## [v0.4.0] - 2019-03-18
|
||||||
|
- Change type of `Module` from function to interface to allow more flexibility
|
||||||
|
- Introduce new `ModuleFunc` type to migrate old modules to new interface type
|
||||||
|
|
||||||
|
## [v0.3.0] - 2019-03-17
|
||||||
|
- Event handler functions can now accept interfaces instead of structs
|
||||||
|
- Add new `github.com/go-joe/joe/joetest` package for unit tests
|
||||||
|
- Add new `joetest.Brain` type
|
||||||
|
- Add new `WithLogger(…)` option
|
||||||
|
- Switch license from MIT to BSD-3-Clause
|
||||||
|
- Move `TestingT` type into new `joetest` package
|
||||||
|
- Move `TestBot` type into new `joetest` package and rename to `joetest.Bot`
|
||||||
|
- Fixed flaky unit test of `CLIAdapter`
|
||||||
|
|
||||||
|
## [v0.2.0] - 2019-03-10
|
||||||
|
- Add a lot more unit tests
|
||||||
|
- Add `TestBot.Start()` and `TestBot.Stop()`to ease synchronously starting and stopping bot in unit tests
|
||||||
|
- Add `TestBot.EmitSync(…)` to emit events synchronously in unit tests
|
||||||
|
- Remove obsolete context argument from `NewTest(…)` function
|
||||||
|
- Errors from passing invalid expressions to `Bot.Respond(…)` are now returned in `Bot.Run()`
|
||||||
|
- Events are now processed in the exact same order in which they are emitted
|
||||||
|
- All pending events are now processed before the brain event loop returns
|
||||||
|
- Replace context argument from `Brain.HandleEvents()` with new `Brain.Shutdown()` function
|
||||||
|
- `Adapter` interface was simplified again to directly use the `Brain`
|
||||||
|
- Remove unnecessary `t` argument from `TestBot.EmitSync(…)` function
|
||||||
|
- Deleted `Brain.Close()` because it was not actually meant to be used to close the brain and is thus confusing
|
||||||
|
|
||||||
|
## [v0.1.0] - 2019-03-03
|
||||||
|
|
||||||
|
Initial release, note that Joe is still in alpha and the API is not yet considered
|
||||||
|
stable before the v1.0.0 release.
|
||||||
|
|
||||||
|
[Unreleased]: https://github.com/go-joe/joe/compare/v0.10.0...HEAD
|
||||||
|
[v0.9.0]: https://github.com/go-joe/joe/compare/v0.9.0...v0.10.0
|
||||||
|
[v0.9.0]: https://github.com/go-joe/joe/compare/v0.8.0...v0.9.0
|
||||||
|
[v0.8.0]: https://github.com/go-joe/joe/compare/v0.7.0...v0.8.0
|
||||||
|
[v0.7.0]: https://github.com/go-joe/joe/compare/v0.6.0...v0.7.0
|
||||||
|
[v0.6.0]: https://github.com/go-joe/joe/compare/v0.5.0...v0.6.0
|
||||||
|
[v0.5.0]: https://github.com/go-joe/joe/compare/v0.4.0...v0.5.0
|
||||||
|
[v0.4.0]: https://github.com/go-joe/joe/compare/v0.3.0...v0.4.0
|
||||||
|
[v0.3.0]: https://github.com/go-joe/joe/compare/v0.2.0...v0.3.0
|
||||||
|
[v0.2.0]: https://github.com/go-joe/joe/compare/v0.1.0...v0.2.0
|
||||||
|
[v0.1.0]: https://github.com/go-joe/joe/releases/tag/v0.1.0
|
|
@ -0,0 +1,97 @@
|
||||||
|
# Contributing
|
||||||
|
|
||||||
|
When contributing to this repository, please first discuss the change you wish
|
||||||
|
to make via an [issue on Github][issues] *before* making a change.
|
||||||
|
|
||||||
|
Please note we have a code of conduct. Please follow it in all your interactions
|
||||||
|
with the project.
|
||||||
|
|
||||||
|
## Pull Request Process
|
||||||
|
|
||||||
|
0. Everything should start with an issue: ["Talk, then code"][talk-code]
|
||||||
|
1. Cover all your changes with unit tests, when unsure how, ask for help
|
||||||
|
2. Run all unit tests with the race detector on
|
||||||
|
3. Run the linters locally via `golangci-lint run`
|
||||||
|
4. Update the [CHANGELOG.md](CHANGELOG.md) with the changes you made (in the "Unreleased" section)
|
||||||
|
5. Consider updating the [README.md](README.md) with details of your changes.
|
||||||
|
When in doubt, lets discuss the need together in the corresponding Github issue.
|
||||||
|
6. Check that all examples are up to date and do still compile with the `check` Makefile target in the `_examples` directory.
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
We follow the **Gopher Code of Conduct** as described at https://golang.org/conduct `\ʕ◔ϖ◔ʔ/`
|
||||||
|
|
||||||
|
### Our Pledge
|
||||||
|
|
||||||
|
In the interest of fostering an open and welcoming environment, we as
|
||||||
|
contributors and maintainers pledge to making participation in our project and
|
||||||
|
our community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, disability, ethnicity, gender identity and expression, level of experience,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity and
|
||||||
|
orientation.
|
||||||
|
|
||||||
|
### Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to creating a positive environment
|
||||||
|
include:
|
||||||
|
|
||||||
|
* Using welcoming and inclusive language
|
||||||
|
* Being respectful of differing viewpoints and experiences
|
||||||
|
* Gracefully accepting constructive criticism
|
||||||
|
* Focusing on what is best for the community
|
||||||
|
* Showing empathy towards other community members
|
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||||
|
advances
|
||||||
|
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or electronic
|
||||||
|
address, without explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
### Our Responsibilities
|
||||||
|
|
||||||
|
Project maintainers are responsible for clarifying the standards of acceptable
|
||||||
|
behavior and are expected to take appropriate and fair corrective action in
|
||||||
|
response to any instances of unacceptable behavior.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or
|
||||||
|
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||||
|
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||||
|
permanently any contributor for other behaviors that they deem inappropriate,
|
||||||
|
threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
### Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies both within project spaces and in public spaces
|
||||||
|
when an individual is representing the project or its community. Examples of
|
||||||
|
representing a project or community include using an official project e-mail
|
||||||
|
address, posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event. Representation of a project may be
|
||||||
|
further defined and clarified by project maintainers.
|
||||||
|
|
||||||
|
### Conflict Resolution
|
||||||
|
|
||||||
|
We do not believe that all conflict is bad; healthy debate and disagreement
|
||||||
|
often yield positive results. However, it is never okay to be disrespectful or
|
||||||
|
to engage in behavior that violates the project’s code of conduct.
|
||||||
|
|
||||||
|
If you see someone violating the code of conduct, you are encouraged to address
|
||||||
|
the behavior directly with those involved. Many issues can be resolved quickly
|
||||||
|
and easily, and this gives people more control over the outcome of their dispute.
|
||||||
|
If you are unable to resolve the matter for any reason, or if the behavior is
|
||||||
|
threatening or harassing, report it. We are dedicated to providing an environment
|
||||||
|
where participants feel welcome and safe.
|
||||||
|
|
||||||
|
### Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||||
|
available at [http://contributor-covenant.org/version/1/4][version]
|
||||||
|
|
||||||
|
[issues]: https://github.com/go-joe/joe/issues
|
||||||
|
[talk-code]: https://dave.cheney.net/2019/02/18/talk-then-code
|
||||||
|
[homepage]: http://contributor-covenant.org
|
||||||
|
[version]: http://contributor-covenant.org/version/1/4/
|
|
@ -0,0 +1,27 @@
|
||||||
|
Copyright (c) 2019, Friedrich Große
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
may be used to endorse or promote products derived from this software without
|
||||||
|
specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,372 @@
|
||||||
|
<h1 align="center">Joe Bot</h1>
|
||||||
|
<p align="center">A general-purpose bot library inspired by Hubot but written in Go.</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/go-joe/joe/releases"><img src="https://img.shields.io/github/tag/go-joe/joe.svg?label=version&color=brightgreen"></a>
|
||||||
|
<a href="https://circleci.com/gh/go-joe/joe/tree/master"><img src="https://circleci.com/gh/go-joe/joe/tree/master.svg?style=shield"></a>
|
||||||
|
<a href="https://goreportcard.com/report/github.com/go-joe/joe"><img src="https://goreportcard.com/badge/github.com/go-joe/joe"></a>
|
||||||
|
<a href="https://codecov.io/gh/go-joe/joe"><img src="https://codecov.io/gh/go-joe/joe/branch/master/graph/badge.svg"/></a>
|
||||||
|
<a href="https://godoc.org/github.com/go-joe/joe"><img src="https://img.shields.io/badge/godoc-reference-blue.svg?color=blue"></a>
|
||||||
|
<a href="https://github.com/go-joe/joe/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-BSD--3--Clause-blue.svg"></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Joe is a library used to write chat bots in [the Go programming language][go].
|
||||||
|
It is very much inspired by the awesome [Hubot][hubot] framework developed by the
|
||||||
|
folks at Github and brings its power to people who want to implement chat bots using Go.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
**THIS SOFTWARE IS STILL IN ALPHA AND THERE ARE NO GUARANTEES REGARDING API STABILITY YET.**
|
||||||
|
|
||||||
|
All significant (e.g. breaking) changes are documented in the [CHANGELOG.md](CHANGELOG.md).
|
||||||
|
|
||||||
|
Joe is packaged using the new [Go modules][go-modules]. You can get joe via:
|
||||||
|
|
||||||
|
```
|
||||||
|
go get github.com/go-joe/joe
|
||||||
|
```
|
||||||
|
|
||||||
|
### Minimal example
|
||||||
|
|
||||||
|
The simplest chat bot listens for messages on a chat _Adapter_ and then executes
|
||||||
|
a _Handler_ function if it sees a message directed to the bot that matches a given pattern.
|
||||||
|
|
||||||
|
For example a bot that responds to a message "ping" with the answer "PONG" looks like this:
|
||||||
|
|
||||||
|
[embedmd]:# (_examples/01_minimal/main.go)
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/go-joe/joe"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
b := joe.New("example-bot")
|
||||||
|
b.Respond("ping", Pong)
|
||||||
|
|
||||||
|
err := b.Run()
|
||||||
|
if err != nil {
|
||||||
|
b.Logger.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Pong(msg joe.Message) error {
|
||||||
|
msg.Respond("PONG")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Useful example
|
||||||
|
|
||||||
|
Each bot consists of a chat _Adapter_ (e.g. to integrate with Slack), a _Memory_
|
||||||
|
implementation to remember key-value data (e.g. using Redis) and a _Brain_ which
|
||||||
|
routes new messages or custom events (e.g. receiving an HTTP call) to the
|
||||||
|
corresponding registered _handler_ functions.
|
||||||
|
|
||||||
|
By default `joe.New(…)` uses the CLI adapter which makes the bot read messages
|
||||||
|
from stdin and respond on stdout. Additionally the bot will store key value
|
||||||
|
data in-memory which means it will forget anything you told it when it is restarted.
|
||||||
|
This default setup is useful for local development without any dependencies but
|
||||||
|
you will quickly want to add other _Modules_ to extend the bots capabilities.
|
||||||
|
|
||||||
|
For instance we can extend the previous example to connect the Bot with a Slack
|
||||||
|
workspace and store key-value data in Redis. To allow the message handlers to
|
||||||
|
access the memory we define them as functions on a custom `ExampleBot`type which
|
||||||
|
embeds the `joe.Bot`.
|
||||||
|
|
||||||
|
[embedmd]:# (_examples/02_useful/main.go)
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-joe/joe"
|
||||||
|
"github.com/go-joe/redis-memory"
|
||||||
|
"github.com/go-joe/slack-adapter"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExampleBot struct {
|
||||||
|
*joe.Bot
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
b := &ExampleBot{
|
||||||
|
Bot: joe.New("example",
|
||||||
|
redis.Memory("localhost:6379"),
|
||||||
|
slack.Adapter("xoxb-1452345…"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Respond("remember (.+) is (.+)", b.Remember)
|
||||||
|
b.Respond("what is (.+)", b.WhatIs)
|
||||||
|
|
||||||
|
err := b.Run()
|
||||||
|
if err != nil {
|
||||||
|
b.Logger.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *ExampleBot) Remember(msg joe.Message) error {
|
||||||
|
key, value := msg.Matches[0], msg.Matches[1]
|
||||||
|
msg.Respond("OK, I'll remember %s is %s", key, value)
|
||||||
|
return b.Store.Set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *ExampleBot) WhatIs(msg joe.Message) error {
|
||||||
|
key := msg.Matches[0]
|
||||||
|
var value string
|
||||||
|
ok, err := b.Store.Get(key, &value)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to retrieve key %q from brain", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
msg.Respond("%s is %s", key, value)
|
||||||
|
} else {
|
||||||
|
msg.Respond("I do not remember %q", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Handling custom events
|
||||||
|
|
||||||
|
The previous example should give you an idea already on how to write simple chat
|
||||||
|
bots. It is missing one important part however: how can a bot trigger any
|
||||||
|
interaction proactively, i.e. without a message from a user.
|
||||||
|
|
||||||
|
To solve this problem, joe's Brain implements an event handler that you can hook
|
||||||
|
into. In fact the `Bot.Respond(…)` function that we used in the earlier examples
|
||||||
|
is doing exactly that to listen for any `joe.ReceiveMessageEvent` that match the
|
||||||
|
specified regular expression and then execute the handler function.
|
||||||
|
|
||||||
|
Implementing custom events is easy because you can emit any type as event and
|
||||||
|
register handlers that match only this type. What this exactly means is best
|
||||||
|
demonstrated with another example:
|
||||||
|
|
||||||
|
[embedmd]:# (_examples/03_custom_events/main.go)
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-joe/joe"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExampleBot struct {
|
||||||
|
*joe.Bot
|
||||||
|
Channel string // example for your custom bot configuration
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomEvent struct {
|
||||||
|
Data string // just an example of attaching any data with a custom event
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
b := &ExampleBot{
|
||||||
|
Bot: joe.New("example"),
|
||||||
|
Channel: "CDEADBEAF", // example reference to a slack channel
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register our custom event handler. Joe inspects the function signature to
|
||||||
|
// understand that this function should be invoked whenever a CustomEvent
|
||||||
|
// is emitted.
|
||||||
|
b.Brain.RegisterHandler(b.HandleCustomEvent)
|
||||||
|
|
||||||
|
// For example purposes emit a CustomEvent in a second.
|
||||||
|
time.AfterFunc(time.Second, func() {
|
||||||
|
b.Brain.Emit(CustomEvent{Data: "Hello World!"})
|
||||||
|
})
|
||||||
|
|
||||||
|
err := b.Run()
|
||||||
|
if err != nil {
|
||||||
|
b.Logger.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleCustomEvent handles any CustomEvent that is emitted. Joe also supports
|
||||||
|
// event handlers that return an error or accept a context.Context as first argument.
|
||||||
|
func (b *ExampleBot) HandleCustomEvent(evt CustomEvent) {
|
||||||
|
b.Say(b.Channel, "Received custom event: %v", evt.Data)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Granting and checking user permissions
|
||||||
|
|
||||||
|
Joe supports a simple way to manage user permissions. For instance you may want
|
||||||
|
to define a message handler that will run an operation which only admins should
|
||||||
|
be allowed to trigger.
|
||||||
|
|
||||||
|
To implement this, joe has a concept of permission scopes. A scope is a string
|
||||||
|
which is _granted_ to a specific user ID so you can later check if the author of
|
||||||
|
the event you are handling (e.g. a message from Slack) has this scope or any
|
||||||
|
scope that _contains_ it.
|
||||||
|
|
||||||
|
Scopes are interpreted in a hierarchical way where scope _A_ can contain scope
|
||||||
|
_B_ if _A_ is a prefix to _B_. For example, you can check if a user is allowed
|
||||||
|
to read or write from the "Example" API by checking the `api.example.read` or
|
||||||
|
`api.example.write` scope. When you grant the scope to a user you can now either
|
||||||
|
decide only to grant the very specific `api.example.read` scope which means the
|
||||||
|
user will not have write permissions or you can allow people write-only access
|
||||||
|
via the `api.example.write` scope.
|
||||||
|
|
||||||
|
Alternatively you can also grant any access to the Example API via `api.example`
|
||||||
|
which includes both the read and write scope beneath it. If you want you
|
||||||
|
could also allow even more general access to everything in the api via the
|
||||||
|
`api` scope.
|
||||||
|
|
||||||
|
Scopes can be granted statically in code or dynamically in a handler like this:
|
||||||
|
|
||||||
|
[embedmd]:# (_examples/04_auth/main.go)
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "github.com/go-joe/joe"
|
||||||
|
|
||||||
|
type ExampleBot struct {
|
||||||
|
*joe.Bot
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
b := &ExampleBot{
|
||||||
|
Bot: joe.New("HAL"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// If you know the user ID in advance you may hard-code it at startup.
|
||||||
|
b.Auth.Grant("api.example", "DAVE")
|
||||||
|
|
||||||
|
// An example of a message handler that checks permissions.
|
||||||
|
b.Respond("open the pod bay doors", b.OpenPodBayDoors)
|
||||||
|
|
||||||
|
err := b.Run()
|
||||||
|
if err != nil {
|
||||||
|
b.Logger.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *ExampleBot) OpenPodBayDoors(msg joe.Message) error {
|
||||||
|
err := b.Auth.CheckPermission("api.example.admin", msg.AuthorID)
|
||||||
|
if err != nil {
|
||||||
|
return msg.RespondE("I'm sorry Dave, I'm afraid I can't do that")
|
||||||
|
}
|
||||||
|
|
||||||
|
return msg.RespondE("OK")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integrating with other applications
|
||||||
|
|
||||||
|
You may want to integrate your bot with applications such as Github or Gitlab to
|
||||||
|
trigger a handler or just send a message to Slack. Usually this is done by
|
||||||
|
providing an HTTP callback to those applications so they can POST data when
|
||||||
|
there is an event. We already saw in the previous section that is is very easy
|
||||||
|
to implement custom events so we will use this feature to implement HTTP
|
||||||
|
integrations as well. Since this is such a dominant use-case we already provide
|
||||||
|
the [`github.com/go-joe/http-server`][joe-http] module to make it easy for
|
||||||
|
everybody to write their own custom integrations.
|
||||||
|
|
||||||
|
[embedmd]:# (_examples/05_http/main.go)
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
joehttp "github.com/go-joe/http-server"
|
||||||
|
"github.com/go-joe/joe"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExampleBot struct {
|
||||||
|
*joe.Bot
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
b := &ExampleBot{Bot: joe.New("example",
|
||||||
|
joehttp.Server(":8080"),
|
||||||
|
)}
|
||||||
|
|
||||||
|
b.Brain.RegisterHandler(b.HandleHTTP)
|
||||||
|
|
||||||
|
err := b.Run()
|
||||||
|
if err != nil {
|
||||||
|
b.Logger.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *ExampleBot) HandleHTTP(context.Context, joehttp.RequestEvent) error {
|
||||||
|
return errors.New("TODO: Add your custom logic here")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available modules
|
||||||
|
|
||||||
|
Joe ships with no third-party modules such as Redis integration to avoid pulling
|
||||||
|
in more dependencies than you actually require. There are however already some
|
||||||
|
modules that you can use directly to extend the functionality of your bot without
|
||||||
|
writing too much code yourself.
|
||||||
|
|
||||||
|
If you have written a module and want to share it, please add it to this list and
|
||||||
|
open a pull request.
|
||||||
|
|
||||||
|
### Chat Adapters
|
||||||
|
|
||||||
|
- Slack Adapter: https://github.com/go-joe/slack-adapter
|
||||||
|
- Rocket.Chat Adapter: https://github.com/dwmunster/rocket-adapter
|
||||||
|
- Telegram Adapter: https://github.com/robertgzr/joe-telegram-adapter
|
||||||
|
- IRC Adapter: https://github.com/akrennmair/joe-irc-adapter
|
||||||
|
|
||||||
|
### Memory Modules
|
||||||
|
|
||||||
|
- Redis Memory: https://github.com/go-joe/redis-memory
|
||||||
|
- File Memory: https://github.com/go-joe/file-memory
|
||||||
|
- Bolt Memory: https://github.com/robertgzr/joe-bolt-memory
|
||||||
|
- Sqlite Memory: https://github.com/warmans/sqlite-memory
|
||||||
|
|
||||||
|
### Other Modules
|
||||||
|
|
||||||
|
- HTTP Server: https://github.com/go-joe/http-server
|
||||||
|
- Cron Jobs: https://github.com/go-joe/cron
|
||||||
|
|
||||||
|
## Built With
|
||||||
|
|
||||||
|
* [zap](https://github.com/uber-go/zap) - Blazing fast, structured, leveled logging in Go
|
||||||
|
* [pkg/errors](https://github.com/pkg/errors) - Simple error handling primitives
|
||||||
|
* [testify](https://github.com/stretchr/testify) - A simple unit test library
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of
|
||||||
|
conduct and on the process for submitting pull requests to this repository.
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
**THIS SOFTWARE IS STILL IN ALPHA AND THERE ARE NO GUARANTEES REGARDING API STABILITY YET.**
|
||||||
|
|
||||||
|
After the v1.0 release we plan to use [SemVer](http://semver.org/) for versioning.
|
||||||
|
For the versions available, see the [tags on this repository][tags].
|
||||||
|
|
||||||
|
## Authors
|
||||||
|
|
||||||
|
- **Friedrich Große** - *Initial work* - [fgrosse](https://github.com/fgrosse)
|
||||||
|
|
||||||
|
See also the list of [contributors][contributors] who participated in this project.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the BSD-3-Clause License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
## Acknowledgments
|
||||||
|
|
||||||
|
- [Hubot][hubot] and its great community for the inspiration
|
||||||
|
- [embedmd][embedmd] for a cool tool to embed source code in markdown files
|
||||||
|
|
||||||
|
[go]: https://golang.org
|
||||||
|
[hubot]: https://hubot.github.com/
|
||||||
|
[go-modules]: https://github.com/golang/go/wiki/Modules
|
||||||
|
[joe-http]: https://github.com/go-joe/http-server
|
||||||
|
[tags]: https://github.com/go-joe/joe/tags
|
||||||
|
[contributors]: https://github.com/go-joe/joe/contributors
|
||||||
|
[embedmd]: https://github.com/campoy/embedmd
|
|
@ -0,0 +1 @@
|
||||||
|
theme: jekyll-theme-hacker
|
|
@ -0,0 +1,192 @@
|
||||||
|
package joe
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/go-joe/joe/reactions"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// An Adapter connects the bot with the chat by enabling it to receive and send
|
||||||
|
// messages. Additionally advanced adapters can emit more events than just the
|
||||||
|
// ReceiveMessageEvent (e.g. the slack adapter also emits the UserTypingEvent).
|
||||||
|
// All adapter events must be setup in the RegisterAt function of the Adapter.
|
||||||
|
//
|
||||||
|
// Joe provides a default CLIAdapter implementation which connects the bot with
|
||||||
|
// the local shell to receive messages from stdin and print messages to stdout.
|
||||||
|
type Adapter interface {
|
||||||
|
RegisterAt(*Brain)
|
||||||
|
Send(text, channel string) error
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReactionAwareAdapter is an optional interface that Adapters can implement if
|
||||||
|
// they support reacting to messages with emojis.
|
||||||
|
type ReactionAwareAdapter interface {
|
||||||
|
React(reactions.Reaction, Message) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// The CLIAdapter is the default Adapter implementation that the bot uses if no
|
||||||
|
// other adapter was configured. It emits a ReceiveMessageEvent for each line it
|
||||||
|
// receives from stdin and prints all sent messages to stdout.
|
||||||
|
//
|
||||||
|
// The CLIAdapter does not set the Message.Data field.
|
||||||
|
type CLIAdapter struct {
|
||||||
|
Prefix string
|
||||||
|
Input io.ReadCloser
|
||||||
|
Output io.Writer
|
||||||
|
Logger *zap.Logger
|
||||||
|
Author string // used to set the author of the messages, defaults to os.Getenv("USER)
|
||||||
|
mu sync.Mutex // protects the Output and closing channel
|
||||||
|
closing chan chan error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCLIAdapter creates a new CLIAdapter. The caller must call Close
|
||||||
|
// to make the CLIAdapter stop reading messages and emitting events.
|
||||||
|
func NewCLIAdapter(name string, logger *zap.Logger) *CLIAdapter {
|
||||||
|
return &CLIAdapter{
|
||||||
|
Prefix: fmt.Sprintf("%s > ", name),
|
||||||
|
Input: os.Stdin,
|
||||||
|
Output: os.Stdout,
|
||||||
|
Logger: logger,
|
||||||
|
Author: os.Getenv("USER"),
|
||||||
|
closing: make(chan chan error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterAt starts the CLIAdapter by reading messages from stdin and emitting
|
||||||
|
// a ReceiveMessageEvent for each of them. Additionally the adapter hooks into
|
||||||
|
// the InitEvent to print a nice prefix to stdout to show to the user it is
|
||||||
|
// ready to accept input.
|
||||||
|
func (a *CLIAdapter) RegisterAt(brain *Brain) {
|
||||||
|
brain.RegisterHandler(func(evt InitEvent) {
|
||||||
|
_ = a.print(a.Prefix)
|
||||||
|
})
|
||||||
|
|
||||||
|
go a.loop(brain)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *CLIAdapter) loop(brain *Brain) {
|
||||||
|
input := a.readLines()
|
||||||
|
|
||||||
|
// The adapter loop is built to stay responsive even if the Brain stops
|
||||||
|
// processing events so we can safely close the CLIAdapter.
|
||||||
|
//
|
||||||
|
// We want to print the prefix each time when the Brain has completely
|
||||||
|
// processed a ReceiveMessageEvent and before we are emitting the next one.
|
||||||
|
// This gives us a shell-like behavior which signals to the user that she
|
||||||
|
// can input more data on the CLI. This channel is buffered so we do not
|
||||||
|
// block the Brain when it executes the callback.
|
||||||
|
callback := make(chan Event, 1)
|
||||||
|
callbackFun := func(evt Event) {
|
||||||
|
callback <- evt
|
||||||
|
}
|
||||||
|
|
||||||
|
var lines = input // channel represents the case that we receive a new message
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case msg, ok := <-lines:
|
||||||
|
if !ok {
|
||||||
|
// no more input from stdin
|
||||||
|
lines = nil // disable this case and wait for closing signal
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
lines = nil // disable this case and wait for the callback
|
||||||
|
brain.Emit(ReceiveMessageEvent{Text: msg, AuthorID: a.Author}, callbackFun)
|
||||||
|
|
||||||
|
case <-callback:
|
||||||
|
// This case is executed after all ReceiveMessageEvent handlers have
|
||||||
|
// completed and we can continue with the next line.
|
||||||
|
_ = a.print(a.Prefix)
|
||||||
|
lines = input // activate first case again
|
||||||
|
|
||||||
|
case result := <-a.closing:
|
||||||
|
if lines == nil {
|
||||||
|
// We were just waiting for our callback
|
||||||
|
_ = a.print(a.Prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = a.print("\n")
|
||||||
|
result <- a.Input.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadLines reads lines from stdin and returns them in a channel.
|
||||||
|
// All strings in the returned channel will not include the trailing newline.
|
||||||
|
// The channel is closed automatically when a.Input is closed.
|
||||||
|
func (a *CLIAdapter) readLines() <-chan string {
|
||||||
|
r := bufio.NewReader(a.Input)
|
||||||
|
lines := make(chan string)
|
||||||
|
go func() {
|
||||||
|
// This goroutine will exit when we call a.Input.Close() which will make
|
||||||
|
// r.ReadString(…) return an io.EOF.
|
||||||
|
for {
|
||||||
|
line, err := r.ReadString('\n')
|
||||||
|
switch {
|
||||||
|
case err == io.EOF:
|
||||||
|
close(lines)
|
||||||
|
return
|
||||||
|
case err != nil:
|
||||||
|
a.Logger.Error("Failed to read messages from input", zap.Error(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lines <- line[:len(line)-1]
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send implements the Adapter interface by sending the given text to stdout.
|
||||||
|
// The channel argument is required by the Adapter interface but is otherwise ignored.
|
||||||
|
func (a *CLIAdapter) Send(text, channel string) error {
|
||||||
|
return a.print(text + "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// React implements the optional ReactionAwareAdapter interface by simply
|
||||||
|
// printing the given reaction as UTF8 emoji to the CLI.
|
||||||
|
func (a *CLIAdapter) React(r reactions.Reaction, _ Message) error {
|
||||||
|
return a.print(r.String() + "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close makes the CLIAdapter stop emitting any new events or printing any output.
|
||||||
|
// Calling this function more than once will result in an error.
|
||||||
|
func (a *CLIAdapter) Close() error {
|
||||||
|
if a.closing == nil {
|
||||||
|
return errors.Errorf("already closed")
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Logger.Debug("Closing CLIAdapter")
|
||||||
|
callback := make(chan error)
|
||||||
|
a.closing <- callback
|
||||||
|
err := <-callback
|
||||||
|
|
||||||
|
// Mark CLIAdapter as closed by setting its closing channel to nil.
|
||||||
|
// This will prevent any more output to be printed after this function returns.
|
||||||
|
a.mu.Lock()
|
||||||
|
a.closing = nil
|
||||||
|
a.mu.Unlock()
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *CLIAdapter) print(msg string) error {
|
||||||
|
a.mu.Lock()
|
||||||
|
if a.closing == nil {
|
||||||
|
return errors.New("adapter is closed")
|
||||||
|
}
|
||||||
|
_, err := fmt.Fprint(a.Output, msg)
|
||||||
|
a.mu.Unlock()
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,230 @@
|
||||||
|
package joe
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrNotAllowed is returned if the user is not allowed access to a specific scope.
|
||||||
|
const ErrNotAllowed = Error("not allowed")
|
||||||
|
|
||||||
|
// permissionKeyPrefix is the key prefix in the Storage that all permission keys have.
|
||||||
|
const permissionKeyPrefix = "joe.permissions."
|
||||||
|
|
||||||
|
// Auth implements logic to add user authorization checks to your bot.
|
||||||
|
type Auth struct {
|
||||||
|
logger *zap.Logger
|
||||||
|
store *Storage
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAuth creates a new Auth instance.
|
||||||
|
func NewAuth(logger *zap.Logger, store *Storage) *Auth {
|
||||||
|
return &Auth{
|
||||||
|
logger: logger,
|
||||||
|
store: store,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckPermission checks if a user has permissions to access a resource under a
|
||||||
|
// given scope. If the user is not permitted access this function returns
|
||||||
|
// ErrNotAllowed.
|
||||||
|
//
|
||||||
|
// Scopes are interpreted in a hierarchical way where scope A can contain scope B
|
||||||
|
// if A is a prefix to B. For example, you can check if a user is allowed to
|
||||||
|
// read or write from the "Example" API by checking the "api.example.read" or
|
||||||
|
// "api.example.write" scope. When you grant the scope to a user you can now
|
||||||
|
// either decide only to grant the very specific "api.example.read" scope which
|
||||||
|
// means the user will not have write permissions or you can allow people
|
||||||
|
// write-only access via "api.example.write".
|
||||||
|
//
|
||||||
|
// Alternatively you can also grant any access to the Example API via "api.example"
|
||||||
|
// which includes both the read and write scope beneath it. If you choose to, you
|
||||||
|
// could also allow even more general access to everything in the api via the
|
||||||
|
// "api" scope. The empty scope "" cannot be granted and will thus always return
|
||||||
|
// an error in the permission check.
|
||||||
|
func (a *Auth) CheckPermission(scope, userID string) error {
|
||||||
|
key := a.permissionsKey(userID)
|
||||||
|
permissions, err := a.loadPermissions(key)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.logger.Debug("Checking user permissions",
|
||||||
|
zap.String("requested_scope", scope),
|
||||||
|
zap.String("user_id", userID),
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, p := range permissions {
|
||||||
|
if strings.HasPrefix(scope, p) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ErrNotAllowed
|
||||||
|
}
|
||||||
|
|
||||||
|
// Users returns a list of user IDs having one or more permission scopes.
|
||||||
|
func (a *Auth) Users() ([]string, error) {
|
||||||
|
a.logger.Debug("Retrieving all user IDs from storage")
|
||||||
|
|
||||||
|
keys, err := a.store.Keys()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to load permissions")
|
||||||
|
}
|
||||||
|
|
||||||
|
var userIDs []string
|
||||||
|
for _, key := range keys {
|
||||||
|
if strings.HasPrefix(key, permissionKeyPrefix) {
|
||||||
|
userID := strings.TrimPrefix(key, permissionKeyPrefix)
|
||||||
|
userIDs = append(userIDs, userID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return userIDs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserPermissions returns all permission scopes for a specific user.
|
||||||
|
func (a *Auth) UserPermissions(userID string) ([]string, error) {
|
||||||
|
a.logger.Debug("Retrieving user permissions from storage",
|
||||||
|
zap.String("user_id", userID),
|
||||||
|
)
|
||||||
|
|
||||||
|
key := a.permissionsKey(userID)
|
||||||
|
permissions, err := a.loadPermissions(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return permissions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Auth) loadPermissions(key string) ([]string, error) {
|
||||||
|
var permissions []string
|
||||||
|
ok, err := a.store.Get(key, &permissions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to load user permissions")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return permissions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grant adds a permission scope to the given user. When a scope was granted
|
||||||
|
// to a specific user it can be checked later via CheckPermission(…).
|
||||||
|
// The returned boolean indicates whether the scope was actually added (i.e. true)
|
||||||
|
// or the user already had the granted scope (false).
|
||||||
|
//
|
||||||
|
// Note that granting a scope is an idempotent operations so granting the same
|
||||||
|
// scope multiple times is a safe operation and will not change the internal
|
||||||
|
// permissions that are written to the Memory.
|
||||||
|
//
|
||||||
|
// The empty scope cannot be granted and trying to do so will result in an error.
|
||||||
|
// If you want to grant access to all scopes you should prefix them with a
|
||||||
|
// common scope such as "root." or "api.".
|
||||||
|
func (a *Auth) Grant(scope, userID string) (bool, error) {
|
||||||
|
if scope == "" {
|
||||||
|
return false, errors.New("scope cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
key := a.permissionsKey(userID)
|
||||||
|
oldPermissions, err := a.loadPermissions(key)
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newPermissions := make([]string, 0, len(oldPermissions)+1)
|
||||||
|
for _, p := range oldPermissions {
|
||||||
|
if strings.HasPrefix(scope, p) {
|
||||||
|
// The user already has this or a scope that "contains" it
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(p, scope) {
|
||||||
|
newPermissions = append(newPermissions, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a.logger.Info("Granting user permission",
|
||||||
|
zap.String("userID", userID),
|
||||||
|
zap.String("scope", scope),
|
||||||
|
)
|
||||||
|
|
||||||
|
newPermissions = append(newPermissions, scope)
|
||||||
|
err = a.updatePermissions(key, newPermissions)
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revoke removes a previously granted permission from a user. If the user does
|
||||||
|
// not currently have the revoked scope this function returns false and no error.
|
||||||
|
//
|
||||||
|
// If you are trying to revoke a permission but the user was previously granted
|
||||||
|
// a scope that contains the revoked scope this function returns an error.
|
||||||
|
func (a *Auth) Revoke(scope, userID string) (bool, error) {
|
||||||
|
if scope == "" {
|
||||||
|
return false, errors.New("scope cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
key := a.permissionsKey(userID)
|
||||||
|
oldPermissions, err := a.loadPermissions(key)
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(oldPermissions) == 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var revoked bool
|
||||||
|
newPermissions := make([]string, 0, len(oldPermissions))
|
||||||
|
for _, p := range oldPermissions {
|
||||||
|
if p == scope {
|
||||||
|
revoked = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(scope, p) {
|
||||||
|
return false, errors.Errorf("cannot revoke scope %q because the user still has the more general scope %q", scope, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
newPermissions = append(newPermissions, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !revoked {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
a.logger.Info("Revoking user permission",
|
||||||
|
zap.String("userID", userID),
|
||||||
|
zap.String("scope", scope),
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(newPermissions) == 0 {
|
||||||
|
_, err := a.store.Delete(key)
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.Wrap(err, "failed to delete last user permission")
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.updatePermissions(key, newPermissions)
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Auth) updatePermissions(key string, permissions []string) error {
|
||||||
|
err := a.store.Set(key, permissions)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to update user permissions")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Auth) permissionsKey(userID string) string {
|
||||||
|
return permissionKeyPrefix + userID
|
||||||
|
}
|
|
@ -0,0 +1,307 @@
|
||||||
|
// Package joe contains a general purpose bot library inspired by Hubot.
|
||||||
|
package joe
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"go.uber.org/multierr"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Bot represents an event based chat bot. For the most simple usage you can
|
||||||
|
// use the Bot.Respond(…) function to make the bot execute a function when it
|
||||||
|
// receives a message that matches a given pattern.
|
||||||
|
//
|
||||||
|
// More advanced usage includes persisting memory or emitting your own events
|
||||||
|
// using the Brain of the robot.
|
||||||
|
type Bot struct {
|
||||||
|
Name string
|
||||||
|
Adapter Adapter
|
||||||
|
Brain *Brain
|
||||||
|
Store *Storage
|
||||||
|
Auth *Auth
|
||||||
|
Logger *zap.Logger
|
||||||
|
|
||||||
|
ctx context.Context
|
||||||
|
initErr error // any error when we created a new bot
|
||||||
|
}
|
||||||
|
|
||||||
|
// A Module is an optional Bot extension that can add new capabilities such as
|
||||||
|
// a different Memory implementation or Adapter.
|
||||||
|
type Module interface {
|
||||||
|
Apply(*Config) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModuleFunc is a function implementation of a Module.
|
||||||
|
type ModuleFunc func(*Config) error
|
||||||
|
|
||||||
|
// Apply implements the Module interface.
|
||||||
|
func (f ModuleFunc) Apply(conf *Config) error {
|
||||||
|
return f(conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new Bot and initializes it with the given Modules and Options.
|
||||||
|
// By default the Bot will use an in-memory Storage and a CLI adapter that
|
||||||
|
// reads messages from stdin and writes to stdout.
|
||||||
|
//
|
||||||
|
// The modules can be used to change the Memory or Adapter or register other new
|
||||||
|
// functionality. Additionally you can pass Options which allow setting some
|
||||||
|
// simple configuration such as the event handler timeouts or injecting a
|
||||||
|
// different context. All Options are available as functions in this package
|
||||||
|
// that start with "With…".
|
||||||
|
//
|
||||||
|
// If there was an error initializing a Module it is stored and returned on the
|
||||||
|
// next call to Bot.Run(). Before you start the bot however you should register
|
||||||
|
// your custom event handlers.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
// b := joe.New("example",
|
||||||
|
// redis.Memory("localhost:6379"),
|
||||||
|
// slack.Adapter("xoxb-58942365423-…"),
|
||||||
|
// joehttp.Server(":8080"),
|
||||||
|
// joe.WithHandlerTimeout(time.Second),
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// b.Respond("ping", b.Pong)
|
||||||
|
// b.Brain.RegisterHandler(b.Init)
|
||||||
|
//
|
||||||
|
// err := b.Run()
|
||||||
|
// …
|
||||||
|
func New(name string, modules ...Module) *Bot {
|
||||||
|
ctx := newContext(modules)
|
||||||
|
logger := newLogger(modules)
|
||||||
|
brain := NewBrain(logger.Named("brain"))
|
||||||
|
store := NewStorage(logger.Named("memory"))
|
||||||
|
|
||||||
|
conf := NewConfig(logger, brain, store, NewCLIAdapter(name, logger))
|
||||||
|
conf.Context = ctx
|
||||||
|
conf.Name = name
|
||||||
|
conf.HandlerTimeout = brain.handlerTimeout
|
||||||
|
|
||||||
|
logger.Info("Initializing bot", zap.String("name", name))
|
||||||
|
for _, mod := range modules {
|
||||||
|
err := mod.Apply(&conf)
|
||||||
|
if err != nil {
|
||||||
|
conf.errs = append(conf.errs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply all configuration options
|
||||||
|
brain.handlerTimeout = conf.HandlerTimeout
|
||||||
|
|
||||||
|
return &Bot{
|
||||||
|
Name: conf.Name,
|
||||||
|
ctx: conf.Context,
|
||||||
|
Logger: conf.logger,
|
||||||
|
Adapter: conf.adapter,
|
||||||
|
Auth: NewAuth(conf.logger, store),
|
||||||
|
Brain: brain,
|
||||||
|
Store: store,
|
||||||
|
initErr: multierr.Combine(conf.errs...),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newContext(modules []Module) context.Context {
|
||||||
|
var conf Config
|
||||||
|
for _, mod := range modules {
|
||||||
|
if x, ok := mod.(loggerModule); ok {
|
||||||
|
_ = x(&conf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.Context != nil {
|
||||||
|
return conf.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
return cliContext()
|
||||||
|
}
|
||||||
|
|
||||||
|
// cliContext creates the default context.Context that is used by the bot.
|
||||||
|
// This context is canceled if the bot receives a SIGINT, SIGQUIT or SIGTERM.
|
||||||
|
func cliContext() context.Context {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
sig := make(chan os.Signal)
|
||||||
|
signal.Notify(sig, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
<-sig
|
||||||
|
cancel()
|
||||||
|
}()
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLogger(modules []Module) *zap.Logger {
|
||||||
|
var conf Config
|
||||||
|
for _, mod := range modules {
|
||||||
|
if x, ok := mod.(loggerModule); ok {
|
||||||
|
_ = x(&conf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.logger != nil {
|
||||||
|
return conf.logger
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := zap.Config{
|
||||||
|
Level: zap.NewAtomicLevelAt(zap.DebugLevel),
|
||||||
|
Development: false,
|
||||||
|
Encoding: "console",
|
||||||
|
EncoderConfig: zapcore.EncoderConfig{
|
||||||
|
TimeKey: "T",
|
||||||
|
LevelKey: "L",
|
||||||
|
NameKey: "N",
|
||||||
|
MessageKey: "M",
|
||||||
|
LineEnding: zapcore.DefaultLineEnding,
|
||||||
|
EncodeLevel: zapcore.CapitalLevelEncoder,
|
||||||
|
EncodeTime: zapcore.ISO8601TimeEncoder,
|
||||||
|
EncodeDuration: zapcore.StringDurationEncoder,
|
||||||
|
EncodeCaller: zapcore.ShortCallerEncoder,
|
||||||
|
},
|
||||||
|
OutputPaths: []string{"stderr"},
|
||||||
|
ErrorOutputPaths: []string{"stderr"},
|
||||||
|
}
|
||||||
|
|
||||||
|
logger, err := cfg.Build()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run starts the bot and runs its event handler loop until the bots context
|
||||||
|
// is canceled (by default via SIGINT, SIGQUIT or SIGTERM). If there was an
|
||||||
|
// an error when setting up the Bot via New() or when registering the event
|
||||||
|
// handlers it will be returned immediately.
|
||||||
|
func (b *Bot) Run() error {
|
||||||
|
if b.initErr != nil {
|
||||||
|
return errors.Wrap(b.initErr, "failed to initialize bot")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(b.Brain.registrationErrs) > 0 {
|
||||||
|
errs := multierr.Combine(b.Brain.registrationErrs...)
|
||||||
|
return errors.Wrap(errs, "invalid event handlers")
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Adapter.RegisterAt(b.Brain)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// Keep running until the context is canceled via SIGINT.
|
||||||
|
<-b.ctx.Done()
|
||||||
|
shutdownCtx := cliContext() // closed upon another SIGINT
|
||||||
|
b.Brain.Shutdown(shutdownCtx)
|
||||||
|
}()
|
||||||
|
|
||||||
|
b.Logger.Info("Bot initialized and ready to operate", zap.String("name", b.Name))
|
||||||
|
b.Brain.HandleEvents()
|
||||||
|
|
||||||
|
b.Logger.Info("Bot is shutting down", zap.String("name", b.Name))
|
||||||
|
err := b.Adapter.Close()
|
||||||
|
if err != nil {
|
||||||
|
b.Logger.Info("Error while closing adapter", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = b.Store.Close()
|
||||||
|
if err != nil {
|
||||||
|
b.Logger.Info("Error while closing memory", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Respond registers an event handler that listens for the ReceiveMessageEvent
|
||||||
|
// and executes the given function only if the message text matches the given
|
||||||
|
// message. The message will be matched against the msg string as regular
|
||||||
|
// expression that must match the entire message in a case insensitive way.
|
||||||
|
//
|
||||||
|
// You can use sub matches in the msg which will be passed to the function via
|
||||||
|
// Message.Matches.
|
||||||
|
//
|
||||||
|
// If you need complete control over the regular expression, e.g. because you
|
||||||
|
// want the patter to match only a substring of the message but not all of it,
|
||||||
|
// you can use Bot.RespondRegex(…). For even more control you can also directly
|
||||||
|
// use Brain.RegisterHandler(…) with a function that accepts ReceiveMessageEvent
|
||||||
|
// instances.
|
||||||
|
//
|
||||||
|
// If multiple matching patterns are registered, only the first registered
|
||||||
|
// handler is executed.
|
||||||
|
func (b *Bot) Respond(msg string, fun func(Message) error) {
|
||||||
|
expr := "^" + msg + "$"
|
||||||
|
b.RespondRegex(expr, fun)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RespondRegex is like Bot.Respond(…) but gives a little more control over the
|
||||||
|
// regular expression. However, also with this function messages are matched in
|
||||||
|
// a case insensitive way.
|
||||||
|
func (b *Bot) RespondRegex(expr string, fun func(Message) error) {
|
||||||
|
if expr == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if expr[0] == '^' {
|
||||||
|
// String starts with the "^" anchor but does it also have the prefix
|
||||||
|
// or case insensitive matching?
|
||||||
|
if !strings.HasPrefix(expr, "^(?i)") { // TODO: strings.ToLower would be easier?
|
||||||
|
expr = "^(?i)" + expr[1:]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The string is not starting with "^" but maybe it has the prefix for
|
||||||
|
// case insensitive matching already?
|
||||||
|
if !strings.HasPrefix(expr, "(?i)") {
|
||||||
|
expr = "(?i)" + expr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
regex, err := regexp.Compile(expr)
|
||||||
|
if err != nil {
|
||||||
|
caller := firstExternalCaller()
|
||||||
|
err = errors.Wrap(err, caller)
|
||||||
|
b.Brain.registrationErrs = append(b.Brain.registrationErrs, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Brain.RegisterHandler(func(ctx context.Context, evt ReceiveMessageEvent) error {
|
||||||
|
matches := regex.FindStringSubmatch(evt.Text)
|
||||||
|
if len(matches) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the event text matches our regular expression we can already mark
|
||||||
|
// the event context as done so the Brain does not run any other handlers
|
||||||
|
// that might match the received message.
|
||||||
|
FinishEventContent(ctx)
|
||||||
|
|
||||||
|
return fun(Message{
|
||||||
|
Context: ctx,
|
||||||
|
ID: evt.ID,
|
||||||
|
Text: evt.Text,
|
||||||
|
AuthorID: evt.AuthorID,
|
||||||
|
Data: evt.Data,
|
||||||
|
Channel: evt.Channel,
|
||||||
|
Matches: matches[1:],
|
||||||
|
adapter: b.Adapter,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Say is a helper function to makes the Bot output the message via its Adapter
|
||||||
|
// (e.g. to the CLI or to Slack). If there is at least one vararg the msg and
|
||||||
|
// args are formatted using fmt.Sprintf.
|
||||||
|
func (b *Bot) Say(channel, msg string, args ...interface{}) {
|
||||||
|
if len(args) > 0 {
|
||||||
|
msg = fmt.Sprintf(msg, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := b.Adapter.Send(msg, channel)
|
||||||
|
if err != nil {
|
||||||
|
b.Logger.Error("Failed to send message", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,504 @@
|
||||||
|
package joe
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The Brain contains the core logic of a Bot by implementing an event handling
|
||||||
|
// system that dispatches events to all registered event handlers.
|
||||||
|
type Brain struct {
|
||||||
|
logger *zap.Logger
|
||||||
|
|
||||||
|
eventsInput chan Event // input for any new events, the Brain ensures that callers never block when writing to it
|
||||||
|
eventsLoop chan Event // used in Brain.HandleEvents() to actually process the events
|
||||||
|
shutdown chan shutdownRequest
|
||||||
|
|
||||||
|
mu sync.RWMutex // mu protects concurrent access to the handlers
|
||||||
|
handlers map[reflect.Type][]eventHandler
|
||||||
|
handlerTimeout time.Duration // zero means no timeout, defaults to one minute
|
||||||
|
|
||||||
|
registrationErrs []error // any errors that occurred during setup (e.g. in Bot.RegisterHandler)
|
||||||
|
handlingEvents int32 // accessed atomically (non-zero means the event handler was started)
|
||||||
|
closed int32 // accessed atomically (non-zero means the brain was shutdown already)
|
||||||
|
}
|
||||||
|
|
||||||
|
// An Event represents a concrete event type and optional callbacks that are
|
||||||
|
// triggered when the event was processed by all registered handlers.
|
||||||
|
type Event struct {
|
||||||
|
Data interface{}
|
||||||
|
Callbacks []func(Event)
|
||||||
|
AbortEarly bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// The shutdownRequest type is used when signaling shutdown information between
|
||||||
|
// Brain.Shutdown() and the Brain.HandleEvents loop.
|
||||||
|
type shutdownRequest struct {
|
||||||
|
ctx context.Context
|
||||||
|
callback chan bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// An eventHandler is a function that takes a context and the reflected value
|
||||||
|
// of a concrete event type.
|
||||||
|
type eventHandler func(context.Context, reflect.Value) error
|
||||||
|
|
||||||
|
// ctxKey is used to pass meta information to event handlers via the context.
|
||||||
|
type ctxKey string
|
||||||
|
|
||||||
|
// ctxKeyEvent is the context key under which we can lookup the internal *Event
|
||||||
|
// instance in a handler.
|
||||||
|
const ctxKeyEvent ctxKey = "event"
|
||||||
|
|
||||||
|
// FinishEventContent can be called from within your event handler functions
|
||||||
|
// to indicate that the Brain should not execute any other handlers after the
|
||||||
|
// calling handler has returned.
|
||||||
|
func FinishEventContent(ctx context.Context) {
|
||||||
|
evt, _ := ctx.Value(ctxKeyEvent).(*Event)
|
||||||
|
if evt != nil {
|
||||||
|
evt.AbortEarly = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBrain creates a new robot Brain. If the passed logger is nil it will
|
||||||
|
// fallback to the zap.NewNop() logger.
|
||||||
|
func NewBrain(logger *zap.Logger) *Brain {
|
||||||
|
if logger == nil {
|
||||||
|
logger = zap.NewNop()
|
||||||
|
}
|
||||||
|
|
||||||
|
b := &Brain{
|
||||||
|
logger: logger,
|
||||||
|
eventsInput: make(chan Event),
|
||||||
|
eventsLoop: make(chan Event),
|
||||||
|
shutdown: make(chan shutdownRequest),
|
||||||
|
handlers: make(map[reflect.Type][]eventHandler),
|
||||||
|
handlerTimeout: time.Minute,
|
||||||
|
}
|
||||||
|
|
||||||
|
b.consumeEvents()
|
||||||
|
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Brain) isHandlingEvents() bool {
|
||||||
|
return atomic.LoadInt32(&b.handlingEvents) == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Brain) isClosed() bool {
|
||||||
|
return atomic.LoadInt32(&b.closed) == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterHandler registers a function to be executed when a specific event is
|
||||||
|
// fired. The function signature must comply with the following rules or the bot
|
||||||
|
// that uses this Brain will return an error on its next Bot.Run() call:
|
||||||
|
//
|
||||||
|
// Allowed function signatures:
|
||||||
|
//
|
||||||
|
// // AnyType can be any scalar, struct or interface type as long as it is not
|
||||||
|
// // a pointer.
|
||||||
|
// func(AnyType)
|
||||||
|
//
|
||||||
|
// // You can optionally accept a context as the first argument. The context
|
||||||
|
// // is used to signal handler timeouts or when the bot is shutting down.
|
||||||
|
// func(context.Context, AnyType)
|
||||||
|
//
|
||||||
|
// // You can optionally return a single error value. Returning any other type
|
||||||
|
// // or returning more than one value is not possible. If the handler
|
||||||
|
// // returns an error it will be logged.
|
||||||
|
// func(AnyType) error
|
||||||
|
//
|
||||||
|
// // Event handlers can also accept an interface in which case they will be
|
||||||
|
// // be called for all events which implement the interface. Consequently,
|
||||||
|
// // you can register a function which accepts the empty interface which will
|
||||||
|
// // will receive all emitted events. Such event handlers can optionally also
|
||||||
|
// // accept a context and/or return an error like other handlers.
|
||||||
|
// func(context.Context, interface{}) error
|
||||||
|
//
|
||||||
|
// The event, that will be dispatched to the passed handler function, corresponds
|
||||||
|
// directly to the accepted function argument. For instance if you want to emit
|
||||||
|
// and receive a custom event you can implement it like this:
|
||||||
|
//
|
||||||
|
// type CustomEvent struct {}
|
||||||
|
//
|
||||||
|
// b := NewBrain(nil)
|
||||||
|
// b.RegisterHandler(func(evt CustomEvent) {
|
||||||
|
// …
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// If multiple handlers are registered for the same event type, then they are
|
||||||
|
// all executed in the order in which they have been registered.
|
||||||
|
//
|
||||||
|
// You should register all handlers before you start the bot via Bot.Run(…).
|
||||||
|
// While registering handlers later is also possible, any registration errors
|
||||||
|
// will silently be ignored if you register an invalid handler when the bot is
|
||||||
|
// already running.
|
||||||
|
func (b *Brain) RegisterHandler(fun interface{}) {
|
||||||
|
err := b.registerHandler(fun)
|
||||||
|
if err != nil {
|
||||||
|
caller := firstExternalCaller()
|
||||||
|
err = errors.Wrap(err, caller)
|
||||||
|
b.registrationErrs = append(b.registrationErrs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Brain) registerHandler(fun interface{}) error {
|
||||||
|
handler := reflect.ValueOf(fun)
|
||||||
|
handlerType := handler.Type()
|
||||||
|
if handlerType.Kind() != reflect.Func {
|
||||||
|
return errors.New("event handler is no function")
|
||||||
|
}
|
||||||
|
|
||||||
|
evtType, withContext, err := checkHandlerParams(handlerType)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
returnsErr, err := checkHandlerReturnValues(handlerType)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
b.logger.Debug("Registering new event handler",
|
||||||
|
zap.Stringer("event_type", evtType),
|
||||||
|
)
|
||||||
|
|
||||||
|
handlerFun := newHandlerFunc(handler, withContext, returnsErr)
|
||||||
|
|
||||||
|
b.mu.Lock()
|
||||||
|
b.handlers[evtType] = append(b.handlers[evtType], handlerFun)
|
||||||
|
b.mu.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit sends the first argument as event to the brain from where it is
|
||||||
|
// dispatched to all registered handlers. The events are dispatched
|
||||||
|
// asynchronously but in the same order in which they are send to this function.
|
||||||
|
// Emit does not block until the event is delivered to the registered event
|
||||||
|
// handlers. If you want to wait until all handlers have processed the event you
|
||||||
|
// can pass one or more callback functions that will be executed when all
|
||||||
|
// handlers finished execution of this event.
|
||||||
|
func (b *Brain) Emit(event interface{}, callbacks ...func(Event)) {
|
||||||
|
if b.isClosed() {
|
||||||
|
b.logger.Debug(
|
||||||
|
"Ignoring new event because brain is currently shutting down or is already closed",
|
||||||
|
zap.String("type", fmt.Sprintf("%T", event)),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b.eventsInput <- Event{Data: event, Callbacks: callbacks}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HandleEvents starts the event handling loop of the Brain.
|
||||||
|
// This function blocks until Brain.Shutdown() is called and returned.
|
||||||
|
func (b *Brain) HandleEvents() {
|
||||||
|
if b.isClosed() {
|
||||||
|
b.logger.Error("HandleEvents failed because bot is already closed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
var shutdown shutdownRequest // set when Brain.Shutdown() is called
|
||||||
|
|
||||||
|
atomic.StoreInt32(&b.handlingEvents, 1)
|
||||||
|
b.handleEvent(ctx, Event{Data: InitEvent{}})
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case evt, ok := <-b.eventsLoop:
|
||||||
|
if !ok {
|
||||||
|
// Brain.consumeEvents() is done processing all remaining events
|
||||||
|
// and we can now safely shutdown the event handler, knowing that
|
||||||
|
// all pending events have been processed.
|
||||||
|
b.handleEvent(ctx, Event{Data: ShutdownEvent{}})
|
||||||
|
shutdown.callback <- true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b.handleEvent(ctx, evt)
|
||||||
|
|
||||||
|
case shutdown = <-b.shutdown:
|
||||||
|
// The Brain is shutting down. We have to close the input channel so
|
||||||
|
// we doe no longer accept new events and only process the remaining
|
||||||
|
// pending events. When the goroutine of Brain.consumeEvents() is
|
||||||
|
// done it will close the events loop channel and the case above will
|
||||||
|
// use the shutdown callback and return from this function.
|
||||||
|
ctx = shutdown.ctx
|
||||||
|
close(b.eventsInput)
|
||||||
|
atomic.StoreInt32(&b.handlingEvents, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// consumeEvents continuously reads events from b.eventsInput in a new goroutine
|
||||||
|
// so emitting an event never blocks on the caller. All events will be returned
|
||||||
|
// in the result channel of this function in the same order in which they have
|
||||||
|
// been inserted into b.events. In this sense this function provides an events
|
||||||
|
// channel with "infinite" capacity. The spawned goroutine stops when the
|
||||||
|
// b.eventsInput channel is closed.
|
||||||
|
func (b *Brain) consumeEvents() {
|
||||||
|
var queue []Event
|
||||||
|
b.eventsLoop = make(chan Event)
|
||||||
|
|
||||||
|
outChan := func() chan Event {
|
||||||
|
if len(queue) == 0 {
|
||||||
|
// In case the queue is empty we return a nil channel to disable the
|
||||||
|
// corresponding select case in the goroutine below.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.eventsLoop
|
||||||
|
}
|
||||||
|
|
||||||
|
nextEvt := func() Event {
|
||||||
|
if len(queue) == 0 {
|
||||||
|
// Prevent index out of bounds if there is no next event. Note that
|
||||||
|
// this event is actually never received because the outChan()
|
||||||
|
// function above will return "nil" in this case which disables the
|
||||||
|
// corresponding select case.
|
||||||
|
return Event{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return queue[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case evt, ok := <-b.eventsInput:
|
||||||
|
if !ok {
|
||||||
|
// Events input channel was closed because Brain is shutting
|
||||||
|
// down. Emit all pending events from the queue and then close
|
||||||
|
// the events loop channel so Brain.HandleEvents() can exit.
|
||||||
|
for _, evt := range queue {
|
||||||
|
b.eventsLoop <- evt
|
||||||
|
}
|
||||||
|
close(b.eventsLoop)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
queue = append(queue, evt)
|
||||||
|
case outChan() <- nextEvt(): // disabled if len(queue) == 0
|
||||||
|
queue = queue[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleEvent receives an event and dispatches it to all registered handlers
|
||||||
|
// using the reflect API. When all applicable handlers are called (maybe none)
|
||||||
|
// the function runs all event callbacks.
|
||||||
|
func (b *Brain) handleEvent(ctx context.Context, evt Event) {
|
||||||
|
event := reflect.ValueOf(evt.Data)
|
||||||
|
typ := event.Type()
|
||||||
|
handlers := b.determineHandlers(typ)
|
||||||
|
|
||||||
|
b.logger.Debug("Handling new event",
|
||||||
|
zap.Stringer("event_type", typ),
|
||||||
|
zap.Int("handlers", len(handlers)),
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx = context.WithValue(ctx, ctxKeyEvent, &evt)
|
||||||
|
|
||||||
|
for _, handler := range handlers {
|
||||||
|
err := b.executeEventHandler(ctx, handler, event)
|
||||||
|
if err != nil {
|
||||||
|
b.logger.Error("Event handler failed",
|
||||||
|
// TODO: somehow log the name of the handler
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if evt.AbortEarly {
|
||||||
|
// Abort handler execution early instead of running any more
|
||||||
|
// handlers. The event state may have been changed by a handler, e.g.
|
||||||
|
// using the FinishEventContent(…) function.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, callback := range evt.Callbacks {
|
||||||
|
callback(evt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Brain) determineHandlers(evtType reflect.Type) []eventHandler {
|
||||||
|
b.mu.RLock()
|
||||||
|
defer b.mu.RUnlock()
|
||||||
|
|
||||||
|
var handlers []eventHandler
|
||||||
|
for handlerType, hh := range b.handlers {
|
||||||
|
if handlerType == evtType {
|
||||||
|
handlers = append(handlers, hh...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if handlerType.Kind() == reflect.Interface && evtType.Implements(handlerType) {
|
||||||
|
handlers = append(handlers, hh...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return handlers
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Brain) executeEventHandler(ctx context.Context, handler eventHandler, event reflect.Value) error {
|
||||||
|
if b.handlerTimeout > 0 {
|
||||||
|
var cancel func()
|
||||||
|
ctx, cancel = context.WithTimeout(ctx, b.handlerTimeout)
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan error)
|
||||||
|
go func() {
|
||||||
|
done <- handler(ctx, event)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
return err
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown stops the event handler loop of the Brain and waits until all pending
|
||||||
|
// events have been processed. After the brain is shutdown, it will no longer
|
||||||
|
// accept new events. The passed context can be used to stop waiting for any
|
||||||
|
// pending events or handlers and instead exit immediately (e.g. after a timeout
|
||||||
|
// or a second SIGTERM).
|
||||||
|
func (b *Brain) Shutdown(ctx context.Context) {
|
||||||
|
closing := atomic.CompareAndSwapInt32(&b.closed, 0, 1)
|
||||||
|
if !closing {
|
||||||
|
// brain is already shutting down
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !b.isHandlingEvents() {
|
||||||
|
// If the event handler loop is not running we must close the inputs
|
||||||
|
// channel from here and drain all pending requests in order to make
|
||||||
|
// b.consumeEvents() exit.
|
||||||
|
close(b.eventsInput)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case _, ok := <-b.eventsLoop:
|
||||||
|
if !ok {
|
||||||
|
// The eventsLoop channel is closed in b.consumeEvents after
|
||||||
|
// all pending messages have been written to it.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
// shutdown context is expired so we return without waiting for
|
||||||
|
// any pending events.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we got here then the event handler loop is running and we delegate
|
||||||
|
// proper cleanup and processing of pending messages over there.
|
||||||
|
req := shutdownRequest{
|
||||||
|
ctx: ctx,
|
||||||
|
callback: make(chan bool),
|
||||||
|
}
|
||||||
|
|
||||||
|
b.shutdown <- req
|
||||||
|
<-req.callback
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkHandlerParams(handlerFunc reflect.Type) (evtType reflect.Type, withContext bool, err error) {
|
||||||
|
numParams := handlerFunc.NumIn()
|
||||||
|
if numParams == 0 || numParams > 2 {
|
||||||
|
err = errors.New("event handler needs one or two arguments")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
evtType = handlerFunc.In(numParams - 1) // last argument must be the event
|
||||||
|
withContext = numParams == 2
|
||||||
|
|
||||||
|
if withContext {
|
||||||
|
contextInterface := reflect.TypeOf((*context.Context)(nil)).Elem()
|
||||||
|
if handlerFunc.In(1).Implements(contextInterface) {
|
||||||
|
err = errors.New("event handler context must be the first argument")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !handlerFunc.In(0).Implements(contextInterface) {
|
||||||
|
err = errors.New("event handler has two arguments but the first is not a context.Context")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if evtType.Kind() == reflect.Ptr {
|
||||||
|
err = errors.New("event handler argument cannot be a pointer")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return evtType, withContext, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkHandlerReturnValues(handlerFunc reflect.Type) (returnsError bool, err error) {
|
||||||
|
switch handlerFunc.NumOut() {
|
||||||
|
case 0:
|
||||||
|
return false, nil
|
||||||
|
case 1:
|
||||||
|
errorInterface := reflect.TypeOf((*error)(nil)).Elem()
|
||||||
|
if !handlerFunc.Out(0).Implements(errorInterface) {
|
||||||
|
err = errors.New("if the event handler has a return value it must implement the error interface")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
default:
|
||||||
|
return false, errors.Errorf("event handler has more than one return value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newHandlerFunc(handler reflect.Value, withContext, returnsErr bool) eventHandler {
|
||||||
|
return func(ctx context.Context, evt reflect.Value) (handlerErr error) {
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
handlerErr = errors.Errorf("handler panic: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var args []reflect.Value
|
||||||
|
if withContext {
|
||||||
|
args = []reflect.Value{
|
||||||
|
reflect.ValueOf(ctx),
|
||||||
|
evt,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
args = []reflect.Value{evt}
|
||||||
|
}
|
||||||
|
|
||||||
|
results := handler.Call(args)
|
||||||
|
if returnsErr && !results[0].IsNil() {
|
||||||
|
return results[0].Interface().(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func firstExternalCaller() string {
|
||||||
|
const depth = 32
|
||||||
|
var pcs [depth]uintptr
|
||||||
|
n := runtime.Callers(3, pcs[:])
|
||||||
|
callers := pcs[0:n]
|
||||||
|
|
||||||
|
frames := runtime.CallersFrames(callers)
|
||||||
|
for frame, more := frames.Next(); more; frame, more = frames.Next() {
|
||||||
|
if !strings.HasPrefix(frame.Function, "github.com/go-joe/joe.") {
|
||||||
|
return fmt.Sprintf("%s:%d", frame.File, frame.Line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown caller"
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
package joe
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config is the configuration of a Bot that can be used or changed during setup
|
||||||
|
// in a Module. Some configuration settings such as the Logger are read only can
|
||||||
|
// only be accessed via the corresponding getter function of the Config.
|
||||||
|
type Config struct {
|
||||||
|
Context context.Context
|
||||||
|
Name string
|
||||||
|
HandlerTimeout time.Duration
|
||||||
|
|
||||||
|
logger *zap.Logger
|
||||||
|
brain *Brain
|
||||||
|
store *Storage
|
||||||
|
adapter Adapter
|
||||||
|
errs []error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConfig creates a new Config that is used to setup the underlying
|
||||||
|
// components of a Bot. For the typical use case you do not have to create a
|
||||||
|
// Config yourself but rather configure a Bot by passing the corresponding
|
||||||
|
// Modules to joe.New(…).
|
||||||
|
func NewConfig(logger *zap.Logger, brain *Brain, store *Storage, adapter Adapter) Config {
|
||||||
|
return Config{
|
||||||
|
adapter: adapter,
|
||||||
|
logger: logger,
|
||||||
|
brain: brain,
|
||||||
|
store: store,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The EventEmitter can be used by a Module by calling Config.EventEmitter().
|
||||||
|
// Events are emitted asynchronously so every call to Emit is non-blocking.
|
||||||
|
type EventEmitter interface {
|
||||||
|
Emit(event interface{}, callbacks ...func(Event))
|
||||||
|
}
|
||||||
|
|
||||||
|
// EventEmitter returns the EventEmitter that can be used to send events to the
|
||||||
|
// Bot and other modules.
|
||||||
|
func (c *Config) EventEmitter() EventEmitter {
|
||||||
|
return c.brain
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logger returns a new named logger.
|
||||||
|
func (c *Config) Logger(name string) *zap.Logger {
|
||||||
|
return c.logger.Named(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMemory can be used to change the Memory implementation of the bot.
|
||||||
|
func (c *Config) SetMemory(mem Memory) {
|
||||||
|
c.store.SetMemory(mem)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMemoryEncoder can be used to change the MemoryEncoder implementation of
|
||||||
|
// the bot.
|
||||||
|
func (c *Config) SetMemoryEncoder(enc MemoryEncoder) {
|
||||||
|
c.store.SetMemoryEncoder(enc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAdapter can be used to change the Adapter implementation of the Bot.
|
||||||
|
func (c *Config) SetAdapter(a Adapter) {
|
||||||
|
c.adapter = a
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterHandler can be used to register an event handler in a Module.
|
||||||
|
func (c *Config) RegisterHandler(fun interface{}) {
|
||||||
|
c.brain.RegisterHandler(fun)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithContext is an option to replace the default context of a bot.
|
||||||
|
func WithContext(ctx context.Context) Module {
|
||||||
|
return contextModule(func(conf *Config) error {
|
||||||
|
conf.Context = ctx
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithHandlerTimeout is an option to set a timeout on event handlers functions.
|
||||||
|
// By default no timeout is enforced.
|
||||||
|
func WithHandlerTimeout(timeout time.Duration) Module {
|
||||||
|
return ModuleFunc(func(conf *Config) error {
|
||||||
|
conf.HandlerTimeout = timeout
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLogger is an option to replace the default logger of a bot.
|
||||||
|
func WithLogger(logger *zap.Logger) Module {
|
||||||
|
return loggerModule(func(conf *Config) error {
|
||||||
|
conf.logger = logger
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type contextModule func(*Config) error
|
||||||
|
|
||||||
|
func (fun contextModule) Apply(conf *Config) error {
|
||||||
|
return fun(conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
type loggerModule func(*Config) error
|
||||||
|
|
||||||
|
func (fun loggerModule) Apply(conf *Config) error {
|
||||||
|
return fun(conf)
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package joe
|
||||||
|
|
||||||
|
// Error is the error type used by Joe. This allows joe errors to be defined as
|
||||||
|
// constants following https://dave.cheney.net/2016/04/07/constant-errors.
|
||||||
|
type Error string
|
||||||
|
|
||||||
|
// Error implements the "error" interface of the standard library.
|
||||||
|
func (err Error) Error() string {
|
||||||
|
return string(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrNotImplemented is returned if the user tries to use a feature that is not
|
||||||
|
// implemented on the corresponding components (e.g. the Adapter). For instance,
|
||||||
|
// not all Adapter implementations may support emoji reactions and trying to
|
||||||
|
// attach a reaction to a message might return this error.
|
||||||
|
const ErrNotImplemented = Error("not implemented")
|
|
@ -0,0 +1,32 @@
|
||||||
|
package joe
|
||||||
|
|
||||||
|
// The InitEvent is the first event that is handled by the Brain after the Bot
|
||||||
|
// is started via Bot.Run().
|
||||||
|
type InitEvent struct{}
|
||||||
|
|
||||||
|
// The ShutdownEvent is the last event that is handled by the Brain before it
|
||||||
|
// stops handling any events after the bot context is done.
|
||||||
|
type ShutdownEvent struct{}
|
||||||
|
|
||||||
|
// The ReceiveMessageEvent is typically emitted by an Adapter when the Bot sees
|
||||||
|
// a new message from the chat.
|
||||||
|
type ReceiveMessageEvent struct {
|
||||||
|
ID string // The ID of the message, identifying it at least uniquely within the Channel
|
||||||
|
Text string // The message text.
|
||||||
|
AuthorID string // A string identifying the author of the message on the adapter.
|
||||||
|
Channel string // The channel over which the message was received.
|
||||||
|
|
||||||
|
// A message may optionally also contain additional information that was
|
||||||
|
// received by the Adapter (e.g. with the slack adapter this may be the
|
||||||
|
// *slack.MessageEvent. Each Adapter implementation should document if and
|
||||||
|
// what information is available here, if any at all.
|
||||||
|
Data interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The UserTypingEvent is emitted by the Adapter and indicates that the Bot
|
||||||
|
// sees that a user is typing. This event may not be emitted on all Adapter
|
||||||
|
// implementations but only when it is actually supported (e.g. on slack).
|
||||||
|
type UserTypingEvent struct {
|
||||||
|
User User
|
||||||
|
Channel string
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
module github.com/go-joe/joe
|
||||||
|
|
||||||
|
go 1.12
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/pkg/errors v0.8.1
|
||||||
|
github.com/stretchr/testify v1.3.0
|
||||||
|
go.uber.org/atomic v1.3.2 // indirect
|
||||||
|
go.uber.org/multierr v1.1.0
|
||||||
|
go.uber.org/zap v1.9.1
|
||||||
|
)
|
|
@ -0,0 +1,16 @@
|
||||||
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
|
||||||
|
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||||
|
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
|
||||||
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
|
go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o=
|
||||||
|
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
|
@ -0,0 +1,53 @@
|
||||||
|
package joe
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/go-joe/joe/reactions"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Message is automatically created from a ReceiveMessageEvent and then passed
|
||||||
|
// to the RespondFunc that was registered via Bot.Respond(…) or Bot.RespondRegex(…)
|
||||||
|
// when the message matches the regular expression of the handler.
|
||||||
|
type Message struct {
|
||||||
|
Context context.Context
|
||||||
|
ID string // The ID of the message, identifying it at least uniquely within the Channel
|
||||||
|
Text string
|
||||||
|
AuthorID string
|
||||||
|
Channel string
|
||||||
|
Matches []string // contains all sub matches of the regular expression that matched the Text
|
||||||
|
Data interface{} // corresponds to the ReceiveMessageEvent.Data field
|
||||||
|
|
||||||
|
adapter Adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Respond is a helper function to directly send a response back to the channel
|
||||||
|
// the message originated from. This function ignores any error when sending the
|
||||||
|
// response. If you want to handle the error use Message.RespondE instead.
|
||||||
|
func (msg *Message) Respond(text string, args ...interface{}) {
|
||||||
|
_ = msg.RespondE(text, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RespondE is a helper function to directly send a response back to the channel
|
||||||
|
// the message originated from. If there was an error it will be returned from
|
||||||
|
// this function.
|
||||||
|
func (msg *Message) RespondE(text string, args ...interface{}) error {
|
||||||
|
if len(args) > 0 {
|
||||||
|
text = fmt.Sprintf(text, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return msg.adapter.Send(text, msg.Channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// React attempts to let the Adapter attach the given reaction to this message.
|
||||||
|
// If the adapter does not support this feature this function will return
|
||||||
|
// ErrNotImplemented.
|
||||||
|
func (msg *Message) React(reaction reactions.Reaction) error {
|
||||||
|
adapter, ok := msg.adapter.(ReactionAwareAdapter)
|
||||||
|
if !ok {
|
||||||
|
return ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
return adapter.React(reaction, *msg)
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package reactions
|
||||||
|
|
||||||
|
// An Event may be emitted by a chat Adapter to indicate that a message
|
||||||
|
// received a reaction.
|
||||||
|
type Event struct {
|
||||||
|
Reaction Reaction
|
||||||
|
MessageID string
|
||||||
|
Channel string
|
||||||
|
AuthorID string
|
||||||
|
}
|
|
@ -0,0 +1,918 @@
|
||||||
|
// Code generated by github.com/go-joe/gen-reactions DO NOT EDIT.
|
||||||
|
|
||||||
|
// Package reactions contains a list of generated reactions that are widely used
|
||||||
|
// in different chat applications on the internet.
|
||||||
|
package reactions
|
||||||
|
|
||||||
|
// A Reaction is an emoji that is attached to chat messages.
|
||||||
|
type Reaction struct {
|
||||||
|
Raw string
|
||||||
|
Shortcode string
|
||||||
|
}
|
||||||
|
|
||||||
|
// People emojis.
|
||||||
|
var (
|
||||||
|
PlusOne = Reaction{Raw: "👍", Shortcode: "+1"}
|
||||||
|
MinusOne = Reaction{Raw: "👎", Shortcode: "-1"}
|
||||||
|
Alien = Reaction{Raw: "👽", Shortcode: "alien"}
|
||||||
|
Angel = Reaction{Raw: "👼", Shortcode: "angel"}
|
||||||
|
Anger = Reaction{Raw: "💢", Shortcode: "anger"}
|
||||||
|
Angry = Reaction{Raw: "😠", Shortcode: "angry"}
|
||||||
|
Anguished = Reaction{Raw: "😧", Shortcode: "anguished"}
|
||||||
|
Astonished = Reaction{Raw: "😲", Shortcode: "astonished"}
|
||||||
|
Baby = Reaction{Raw: "👶", Shortcode: "baby"}
|
||||||
|
BlueHeart = Reaction{Raw: "💙", Shortcode: "blue_heart"}
|
||||||
|
Blush = Reaction{Raw: "😊", Shortcode: "blush"}
|
||||||
|
Boom = Reaction{Raw: "💥", Shortcode: "boom"}
|
||||||
|
Bow = Reaction{Raw: "🙇", Shortcode: "bow"}
|
||||||
|
Bowtie = Reaction{Raw: "", Shortcode: "bowtie"}
|
||||||
|
Boy = Reaction{Raw: "👦", Shortcode: "boy"}
|
||||||
|
BrideWithVeil = Reaction{Raw: "👰", Shortcode: "bride_with_veil"}
|
||||||
|
BrokenHeart = Reaction{Raw: "💔", Shortcode: "broken_heart"}
|
||||||
|
BustInSilhouette = Reaction{Raw: "👤", Shortcode: "bust_in_silhouette"}
|
||||||
|
BustsInSilhouette = Reaction{Raw: "👥", Shortcode: "busts_in_silhouette"}
|
||||||
|
Clap = Reaction{Raw: "👏", Shortcode: "clap"}
|
||||||
|
ColdSweat = Reaction{Raw: "😰", Shortcode: "cold_sweat"}
|
||||||
|
Collision = Reaction{Raw: "💥", Shortcode: "collision"}
|
||||||
|
Confounded = Reaction{Raw: "😖", Shortcode: "confounded"}
|
||||||
|
Confused = Reaction{Raw: "😕", Shortcode: "confused"}
|
||||||
|
ConstructionWorker = Reaction{Raw: "👷", Shortcode: "construction_worker"}
|
||||||
|
Cop = Reaction{Raw: "👮", Shortcode: "cop"}
|
||||||
|
Couple = Reaction{Raw: "👫", Shortcode: "couple"}
|
||||||
|
CoupleWithHeart = Reaction{Raw: "💑", Shortcode: "couple_with_heart"}
|
||||||
|
Couplekiss = Reaction{Raw: "💏", Shortcode: "couplekiss"}
|
||||||
|
Cry = Reaction{Raw: "😢", Shortcode: "cry"}
|
||||||
|
CryingCatFace = Reaction{Raw: "😿", Shortcode: "crying_cat_face"}
|
||||||
|
Cupid = Reaction{Raw: "💘", Shortcode: "cupid"}
|
||||||
|
Dancer = Reaction{Raw: "💃", Shortcode: "dancer"}
|
||||||
|
Dancers = Reaction{Raw: "👯", Shortcode: "dancers"}
|
||||||
|
Dash = Reaction{Raw: "💨", Shortcode: "dash"}
|
||||||
|
Disappointed = Reaction{Raw: "😞", Shortcode: "disappointed"}
|
||||||
|
DisappointedRelieved = Reaction{Raw: "😥", Shortcode: "disappointed_relieved"}
|
||||||
|
Dizzy = Reaction{Raw: "💫", Shortcode: "dizzy"}
|
||||||
|
DizzyFace = Reaction{Raw: "😵", Shortcode: "dizzy_face"}
|
||||||
|
Droplet = Reaction{Raw: "💧", Shortcode: "droplet"}
|
||||||
|
Ear = Reaction{Raw: "👂", Shortcode: "ear"}
|
||||||
|
Exclamation = Reaction{Raw: "❗", Shortcode: "exclamation"}
|
||||||
|
Expressionless = Reaction{Raw: "😑", Shortcode: "expressionless"}
|
||||||
|
Eyes = Reaction{Raw: "👀", Shortcode: "eyes"}
|
||||||
|
Facepunch = Reaction{Raw: "👊", Shortcode: "facepunch"}
|
||||||
|
Family = Reaction{Raw: "👪", Shortcode: "family"}
|
||||||
|
Fearful = Reaction{Raw: "😨", Shortcode: "fearful"}
|
||||||
|
Feelsgood = Reaction{Raw: "", Shortcode: "feelsgood"}
|
||||||
|
Feet = Reaction{Raw: "🐾", Shortcode: "feet"}
|
||||||
|
Finnadie = Reaction{Raw: "", Shortcode: "finnadie"}
|
||||||
|
Fire = Reaction{Raw: "🔥", Shortcode: "fire"}
|
||||||
|
Fist = Reaction{Raw: "✊", Shortcode: "fist"}
|
||||||
|
Flushed = Reaction{Raw: "😳", Shortcode: "flushed"}
|
||||||
|
Frowning = Reaction{Raw: "😦", Shortcode: "frowning"}
|
||||||
|
Fu = Reaction{Raw: "🖕", Shortcode: "fu"}
|
||||||
|
Girl = Reaction{Raw: "👧", Shortcode: "girl"}
|
||||||
|
Goberserk = Reaction{Raw: "", Shortcode: "goberserk"}
|
||||||
|
Godmode = Reaction{Raw: "", Shortcode: "godmode"}
|
||||||
|
GreenHeart = Reaction{Raw: "💚", Shortcode: "green_heart"}
|
||||||
|
GreyExclamation = Reaction{Raw: "❕", Shortcode: "grey_exclamation"}
|
||||||
|
GreyQuestion = Reaction{Raw: "❔", Shortcode: "grey_question"}
|
||||||
|
Grimacing = Reaction{Raw: "😬", Shortcode: "grimacing"}
|
||||||
|
Grin = Reaction{Raw: "😄", Shortcode: "grin"}
|
||||||
|
Grinning = Reaction{Raw: "😀", Shortcode: "grinning"}
|
||||||
|
Guardsman = Reaction{Raw: "💂", Shortcode: "guardsman"}
|
||||||
|
Haircut = Reaction{Raw: "💇", Shortcode: "haircut"}
|
||||||
|
Hand = Reaction{Raw: "✋", Shortcode: "hand"}
|
||||||
|
Hankey = Reaction{Raw: "💩", Shortcode: "hankey"}
|
||||||
|
HearNoEvil = Reaction{Raw: "🙉", Shortcode: "hear_no_evil"}
|
||||||
|
Heart = Reaction{Raw: "❤", Shortcode: "heart"}
|
||||||
|
HeartEyes = Reaction{Raw: "😍", Shortcode: "heart_eyes"}
|
||||||
|
HeartEyesCat = Reaction{Raw: "😻", Shortcode: "heart_eyes_cat"}
|
||||||
|
Heartbeat = Reaction{Raw: "💓", Shortcode: "heartbeat"}
|
||||||
|
Heartpulse = Reaction{Raw: "💗", Shortcode: "heartpulse"}
|
||||||
|
Hurtrealbad = Reaction{Raw: "", Shortcode: "hurtrealbad"}
|
||||||
|
Hushed = Reaction{Raw: "😯", Shortcode: "hushed"}
|
||||||
|
Imp = Reaction{Raw: "👿", Shortcode: "imp"}
|
||||||
|
InformationDeskPerson = Reaction{Raw: "💁", Shortcode: "information_desk_person"}
|
||||||
|
Innocent = Reaction{Raw: "😇", Shortcode: "innocent"}
|
||||||
|
JapaneseGoblin = Reaction{Raw: "👺", Shortcode: "japanese_goblin"}
|
||||||
|
JapaneseOgre = Reaction{Raw: "👹", Shortcode: "japanese_ogre"}
|
||||||
|
Joy = Reaction{Raw: "😂", Shortcode: "joy"}
|
||||||
|
JoyCat = Reaction{Raw: "😹", Shortcode: "joy_cat"}
|
||||||
|
Kiss = Reaction{Raw: "💏", Shortcode: "kiss"}
|
||||||
|
Kissing = Reaction{Raw: "😗", Shortcode: "kissing"}
|
||||||
|
KissingCat = Reaction{Raw: "😽", Shortcode: "kissing_cat"}
|
||||||
|
KissingClosedEyes = Reaction{Raw: "😚", Shortcode: "kissing_closed_eyes"}
|
||||||
|
KissingHeart = Reaction{Raw: "😘", Shortcode: "kissing_heart"}
|
||||||
|
KissingSmilingEyes = Reaction{Raw: "😙", Shortcode: "kissing_smiling_eyes"}
|
||||||
|
Laughing = Reaction{Raw: "😆", Shortcode: "laughing"}
|
||||||
|
Lips = Reaction{Raw: "👄", Shortcode: "lips"}
|
||||||
|
LoveLetter = Reaction{Raw: "💌", Shortcode: "love_letter"}
|
||||||
|
Man = Reaction{Raw: "👨", Shortcode: "man"}
|
||||||
|
ManWithGuaPiMao = Reaction{Raw: "👲", Shortcode: "man_with_gua_pi_mao"}
|
||||||
|
ManWithTurban = Reaction{Raw: "👳", Shortcode: "man_with_turban"}
|
||||||
|
Mask = Reaction{Raw: "😷", Shortcode: "mask"}
|
||||||
|
Massage = Reaction{Raw: "💆", Shortcode: "massage"}
|
||||||
|
Metal = Reaction{Raw: "🤘", Shortcode: "metal"}
|
||||||
|
Muscle = Reaction{Raw: "💪", Shortcode: "muscle"}
|
||||||
|
MusicalNote = Reaction{Raw: "🎵", Shortcode: "musical_note"}
|
||||||
|
NailCare = Reaction{Raw: "💅", Shortcode: "nail_care"}
|
||||||
|
Neckbeard = Reaction{Raw: "", Shortcode: "neckbeard"}
|
||||||
|
NeutralFace = Reaction{Raw: "😐", Shortcode: "neutral_face"}
|
||||||
|
NoGood = Reaction{Raw: "🙅", Shortcode: "no_good"}
|
||||||
|
NoMouth = Reaction{Raw: "😶", Shortcode: "no_mouth"}
|
||||||
|
Nose = Reaction{Raw: "👃", Shortcode: "nose"}
|
||||||
|
Notes = Reaction{Raw: "🎶", Shortcode: "notes"}
|
||||||
|
OkHand = Reaction{Raw: "👌", Shortcode: "ok_hand"}
|
||||||
|
OkWoman = Reaction{Raw: "🙆", Shortcode: "ok_woman"}
|
||||||
|
OlderMan = Reaction{Raw: "👴", Shortcode: "older_man"}
|
||||||
|
OlderWoman = Reaction{Raw: "👵", Shortcode: "older_woman"}
|
||||||
|
OpenHands = Reaction{Raw: "👐", Shortcode: "open_hands"}
|
||||||
|
OpenMouth = Reaction{Raw: "😮", Shortcode: "open_mouth"}
|
||||||
|
Pensive = Reaction{Raw: "😔", Shortcode: "pensive"}
|
||||||
|
Persevere = Reaction{Raw: "😣", Shortcode: "persevere"}
|
||||||
|
PersonFrowning = Reaction{Raw: "🙍", Shortcode: "person_frowning"}
|
||||||
|
PersonWithBlondHair = Reaction{Raw: "👱", Shortcode: "person_with_blond_hair"}
|
||||||
|
PersonWithPoutingFace = Reaction{Raw: "🙎", Shortcode: "person_with_pouting_face"}
|
||||||
|
PointDown = Reaction{Raw: "👇", Shortcode: "point_down"}
|
||||||
|
PointLeft = Reaction{Raw: "👈", Shortcode: "point_left"}
|
||||||
|
PointRight = Reaction{Raw: "👉", Shortcode: "point_right"}
|
||||||
|
PointUp = Reaction{Raw: "☝", Shortcode: "point_up"}
|
||||||
|
PointUpTwo = Reaction{Raw: "👆", Shortcode: "point_up_2"}
|
||||||
|
Poop = Reaction{Raw: "💩", Shortcode: "poop"}
|
||||||
|
PoutingCat = Reaction{Raw: "😾", Shortcode: "pouting_cat"}
|
||||||
|
Pray = Reaction{Raw: "🙏", Shortcode: "pray"}
|
||||||
|
Princess = Reaction{Raw: "👸", Shortcode: "princess"}
|
||||||
|
Punch = Reaction{Raw: "👊", Shortcode: "punch"}
|
||||||
|
PurpleHeart = Reaction{Raw: "💜", Shortcode: "purple_heart"}
|
||||||
|
Question = Reaction{Raw: "❓", Shortcode: "question"}
|
||||||
|
Rage = Reaction{Raw: "😡", Shortcode: "rage"}
|
||||||
|
RageOne = Reaction{Raw: "", Shortcode: "rage1"}
|
||||||
|
RageTwo = Reaction{Raw: "", Shortcode: "rage2"}
|
||||||
|
RageThree = Reaction{Raw: "", Shortcode: "rage3"}
|
||||||
|
RageFour = Reaction{Raw: "", Shortcode: "rage4"}
|
||||||
|
RaisedHand = Reaction{Raw: "✋", Shortcode: "raised_hand"}
|
||||||
|
RaisedHands = Reaction{Raw: "🙌", Shortcode: "raised_hands"}
|
||||||
|
RaisingHand = Reaction{Raw: "🙋", Shortcode: "raising_hand"}
|
||||||
|
Relaxed = Reaction{Raw: "☺", Shortcode: "relaxed"}
|
||||||
|
Relieved = Reaction{Raw: "😌", Shortcode: "relieved"}
|
||||||
|
RevolvingHearts = Reaction{Raw: "💞", Shortcode: "revolving_hearts"}
|
||||||
|
Runner = Reaction{Raw: "🏃", Shortcode: "runner"}
|
||||||
|
Running = Reaction{Raw: "🏃", Shortcode: "running"}
|
||||||
|
Satisfied = Reaction{Raw: "😆", Shortcode: "satisfied"}
|
||||||
|
Scream = Reaction{Raw: "😱", Shortcode: "scream"}
|
||||||
|
ScreamCat = Reaction{Raw: "🙀", Shortcode: "scream_cat"}
|
||||||
|
SeeNoEvil = Reaction{Raw: "🙈", Shortcode: "see_no_evil"}
|
||||||
|
Shit = Reaction{Raw: "💩", Shortcode: "shit"}
|
||||||
|
SimpleSmile = Reaction{Raw: "", Shortcode: "simple_smile"}
|
||||||
|
Skull = Reaction{Raw: "💀", Shortcode: "skull"}
|
||||||
|
Sleeping = Reaction{Raw: "😴", Shortcode: "sleeping"}
|
||||||
|
Sleepy = Reaction{Raw: "😪", Shortcode: "sleepy"}
|
||||||
|
Smile = Reaction{Raw: "😄", Shortcode: "smile"}
|
||||||
|
SmileCat = Reaction{Raw: "😸", Shortcode: "smile_cat"}
|
||||||
|
Smiley = Reaction{Raw: "😃", Shortcode: "smiley"}
|
||||||
|
SmileyCat = Reaction{Raw: "😺", Shortcode: "smiley_cat"}
|
||||||
|
SmilingImp = Reaction{Raw: "😈", Shortcode: "smiling_imp"}
|
||||||
|
Smirk = Reaction{Raw: "😏", Shortcode: "smirk"}
|
||||||
|
SmirkCat = Reaction{Raw: "😼", Shortcode: "smirk_cat"}
|
||||||
|
Sob = Reaction{Raw: "😭", Shortcode: "sob"}
|
||||||
|
Sparkles = Reaction{Raw: "✨", Shortcode: "sparkles"}
|
||||||
|
SparklingHeart = Reaction{Raw: "💖", Shortcode: "sparkling_heart"}
|
||||||
|
SpeakNoEvil = Reaction{Raw: "🙊", Shortcode: "speak_no_evil"}
|
||||||
|
SpeechBalloon = Reaction{Raw: "💬", Shortcode: "speech_balloon"}
|
||||||
|
Star = Reaction{Raw: "⭐", Shortcode: "star"}
|
||||||
|
StarTwo = Reaction{Raw: "🌟", Shortcode: "star2"}
|
||||||
|
StuckOutTongue = Reaction{Raw: "😛", Shortcode: "stuck_out_tongue"}
|
||||||
|
StuckOutTongueClosedEyes = Reaction{Raw: "😝", Shortcode: "stuck_out_tongue_closed_eyes"}
|
||||||
|
StuckOutTongueWinkingEye = Reaction{Raw: "😜", Shortcode: "stuck_out_tongue_winking_eye"}
|
||||||
|
Sunglasses = Reaction{Raw: "🕶", Shortcode: "sunglasses"}
|
||||||
|
Suspect = Reaction{Raw: "", Shortcode: "suspect"}
|
||||||
|
Sweat = Reaction{Raw: "😓", Shortcode: "sweat"}
|
||||||
|
SweatDrops = Reaction{Raw: "💦", Shortcode: "sweat_drops"}
|
||||||
|
SweatSmile = Reaction{Raw: "😅", Shortcode: "sweat_smile"}
|
||||||
|
ThoughtBalloon = Reaction{Raw: "💭", Shortcode: "thought_balloon"}
|
||||||
|
Thumbsdown = Reaction{Raw: "👎", Shortcode: "thumbsdown"}
|
||||||
|
Thumbsup = Reaction{Raw: "👍", Shortcode: "thumbsup"}
|
||||||
|
TiredFace = Reaction{Raw: "😫", Shortcode: "tired_face"}
|
||||||
|
Tongue = Reaction{Raw: "👅", Shortcode: "tongue"}
|
||||||
|
Triumph = Reaction{Raw: "😤", Shortcode: "triumph"}
|
||||||
|
Trollface = Reaction{Raw: "", Shortcode: "trollface"}
|
||||||
|
TwoHearts = Reaction{Raw: "💕", Shortcode: "two_hearts"}
|
||||||
|
TwoMenHoldingHands = Reaction{Raw: "👬", Shortcode: "two_men_holding_hands"}
|
||||||
|
TwoWomenHoldingHands = Reaction{Raw: "👭", Shortcode: "two_women_holding_hands"}
|
||||||
|
Unamused = Reaction{Raw: "😒", Shortcode: "unamused"}
|
||||||
|
V = Reaction{Raw: "✌", Shortcode: "v"}
|
||||||
|
Wave = Reaction{Raw: "👋", Shortcode: "wave"}
|
||||||
|
Weary = Reaction{Raw: "😩", Shortcode: "weary"}
|
||||||
|
Wink = Reaction{Raw: "😉", Shortcode: "wink"}
|
||||||
|
Woman = Reaction{Raw: "👩", Shortcode: "woman"}
|
||||||
|
Worried = Reaction{Raw: "😟", Shortcode: "worried"}
|
||||||
|
YellowHeart = Reaction{Raw: "💛", Shortcode: "yellow_heart"}
|
||||||
|
Yum = Reaction{Raw: "😋", Shortcode: "yum"}
|
||||||
|
Zzz = Reaction{Raw: "💤", Shortcode: "zzz"}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Nature emojis.
|
||||||
|
var (
|
||||||
|
Ant = Reaction{Raw: "🐜", Shortcode: "ant"}
|
||||||
|
BabyChick = Reaction{Raw: "🐤", Shortcode: "baby_chick"}
|
||||||
|
Bear = Reaction{Raw: "🐻", Shortcode: "bear"}
|
||||||
|
Beetle = Reaction{Raw: "🐞", Shortcode: "beetle"}
|
||||||
|
Bird = Reaction{Raw: "🐦", Shortcode: "bird"}
|
||||||
|
Blossom = Reaction{Raw: "🌼", Shortcode: "blossom"}
|
||||||
|
Blowfish = Reaction{Raw: "🐡", Shortcode: "blowfish"}
|
||||||
|
Boar = Reaction{Raw: "🐗", Shortcode: "boar"}
|
||||||
|
Bouquet = Reaction{Raw: "💐", Shortcode: "bouquet"}
|
||||||
|
Bug = Reaction{Raw: "🐛", Shortcode: "bug"}
|
||||||
|
Cactus = Reaction{Raw: "🌵", Shortcode: "cactus"}
|
||||||
|
Camel = Reaction{Raw: "🐪", Shortcode: "camel"}
|
||||||
|
Cat = Reaction{Raw: "🐈", Shortcode: "cat"}
|
||||||
|
CatTwo = Reaction{Raw: "🐈", Shortcode: "cat2"}
|
||||||
|
CherryBlossom = Reaction{Raw: "🌸", Shortcode: "cherry_blossom"}
|
||||||
|
Chestnut = Reaction{Raw: "🌰", Shortcode: "chestnut"}
|
||||||
|
Chicken = Reaction{Raw: "🐔", Shortcode: "chicken"}
|
||||||
|
Cloud = Reaction{Raw: "☁", Shortcode: "cloud"}
|
||||||
|
Cow = Reaction{Raw: "🐄", Shortcode: "cow"}
|
||||||
|
CowTwo = Reaction{Raw: "🐄", Shortcode: "cow2"}
|
||||||
|
CrescentMoon = Reaction{Raw: "🌙", Shortcode: "crescent_moon"}
|
||||||
|
Crocodile = Reaction{Raw: "🐊", Shortcode: "crocodile"}
|
||||||
|
Cyclone = Reaction{Raw: "🌀", Shortcode: "cyclone"}
|
||||||
|
DeciduousTree = Reaction{Raw: "🌳", Shortcode: "deciduous_tree"}
|
||||||
|
Dog = Reaction{Raw: "🐕", Shortcode: "dog"}
|
||||||
|
DogTwo = Reaction{Raw: "🐕", Shortcode: "dog2"}
|
||||||
|
Dolphin = Reaction{Raw: "🐬", Shortcode: "dolphin"}
|
||||||
|
Dragon = Reaction{Raw: "🐉", Shortcode: "dragon"}
|
||||||
|
DragonFace = Reaction{Raw: "🐲", Shortcode: "dragon_face"}
|
||||||
|
DromedaryCamel = Reaction{Raw: "🐪", Shortcode: "dromedary_camel"}
|
||||||
|
EarOfRice = Reaction{Raw: "🌾", Shortcode: "ear_of_rice"}
|
||||||
|
EarthAfrica = Reaction{Raw: "🌍", Shortcode: "earth_africa"}
|
||||||
|
EarthAmericas = Reaction{Raw: "🌎", Shortcode: "earth_americas"}
|
||||||
|
EarthAsia = Reaction{Raw: "🌏", Shortcode: "earth_asia"}
|
||||||
|
Elephant = Reaction{Raw: "🐘", Shortcode: "elephant"}
|
||||||
|
EvergreenTree = Reaction{Raw: "🌲", Shortcode: "evergreen_tree"}
|
||||||
|
FallenLeaf = Reaction{Raw: "🍂", Shortcode: "fallen_leaf"}
|
||||||
|
FirstQuarterMoon = Reaction{Raw: "🌓", Shortcode: "first_quarter_moon"}
|
||||||
|
FirstQuarterMoonWithFace = Reaction{Raw: "🌛", Shortcode: "first_quarter_moon_with_face"}
|
||||||
|
Fish = Reaction{Raw: "🐟", Shortcode: "fish"}
|
||||||
|
Foggy = Reaction{Raw: "🌁", Shortcode: "foggy"}
|
||||||
|
FourLeafClover = Reaction{Raw: "🍀", Shortcode: "four_leaf_clover"}
|
||||||
|
Frog = Reaction{Raw: "🐸", Shortcode: "frog"}
|
||||||
|
FullMoon = Reaction{Raw: "🌕", Shortcode: "full_moon"}
|
||||||
|
FullMoonWithFace = Reaction{Raw: "🌝", Shortcode: "full_moon_with_face"}
|
||||||
|
GlobeWithMeridians = Reaction{Raw: "🌐", Shortcode: "globe_with_meridians"}
|
||||||
|
Goat = Reaction{Raw: "🐐", Shortcode: "goat"}
|
||||||
|
Hamster = Reaction{Raw: "🐹", Shortcode: "hamster"}
|
||||||
|
HatchedChick = Reaction{Raw: "🐥", Shortcode: "hatched_chick"}
|
||||||
|
HatchingChick = Reaction{Raw: "🐣", Shortcode: "hatching_chick"}
|
||||||
|
Herb = Reaction{Raw: "🌿", Shortcode: "herb"}
|
||||||
|
Hibiscus = Reaction{Raw: "🌺", Shortcode: "hibiscus"}
|
||||||
|
Honeybee = Reaction{Raw: "🐝", Shortcode: "honeybee"}
|
||||||
|
Horse = Reaction{Raw: "🐎", Shortcode: "horse"}
|
||||||
|
Koala = Reaction{Raw: "🐨", Shortcode: "koala"}
|
||||||
|
LastQuarterMoon = Reaction{Raw: "🌗", Shortcode: "last_quarter_moon"}
|
||||||
|
LastQuarterMoonWithFace = Reaction{Raw: "🌜", Shortcode: "last_quarter_moon_with_face"}
|
||||||
|
Leaves = Reaction{Raw: "🍃", Shortcode: "leaves"}
|
||||||
|
Leopard = Reaction{Raw: "🐆", Shortcode: "leopard"}
|
||||||
|
MapleLeaf = Reaction{Raw: "🍁", Shortcode: "maple_leaf"}
|
||||||
|
MilkyWay = Reaction{Raw: "🌌", Shortcode: "milky_way"}
|
||||||
|
Monkey = Reaction{Raw: "🐒", Shortcode: "monkey"}
|
||||||
|
MonkeyFace = Reaction{Raw: "🐵", Shortcode: "monkey_face"}
|
||||||
|
Mouse = Reaction{Raw: "🐁", Shortcode: "mouse"}
|
||||||
|
MouseTwo = Reaction{Raw: "🐁", Shortcode: "mouse2"}
|
||||||
|
Mushroom = Reaction{Raw: "🍄", Shortcode: "mushroom"}
|
||||||
|
NewMoon = Reaction{Raw: "🌑", Shortcode: "new_moon"}
|
||||||
|
NewMoonWithFace = Reaction{Raw: "🌚", Shortcode: "new_moon_with_face"}
|
||||||
|
Ocean = Reaction{Raw: "🌊", Shortcode: "ocean"}
|
||||||
|
Octocat = Reaction{Raw: "", Shortcode: "octocat"}
|
||||||
|
Octopus = Reaction{Raw: "🐙", Shortcode: "octopus"}
|
||||||
|
Ox = Reaction{Raw: "🐂", Shortcode: "ox"}
|
||||||
|
PalmTree = Reaction{Raw: "🌴", Shortcode: "palm_tree"}
|
||||||
|
PandaFace = Reaction{Raw: "🐼", Shortcode: "panda_face"}
|
||||||
|
PartlySunny = Reaction{Raw: "⛅", Shortcode: "partly_sunny"}
|
||||||
|
PawPrints = Reaction{Raw: "🐾", Shortcode: "paw_prints"}
|
||||||
|
Penguin = Reaction{Raw: "🐧", Shortcode: "penguin"}
|
||||||
|
Pig = Reaction{Raw: "🐖", Shortcode: "pig"}
|
||||||
|
PigTwo = Reaction{Raw: "🐖", Shortcode: "pig2"}
|
||||||
|
PigNose = Reaction{Raw: "🐽", Shortcode: "pig_nose"}
|
||||||
|
Poodle = Reaction{Raw: "🐩", Shortcode: "poodle"}
|
||||||
|
Rabbit = Reaction{Raw: "🐇", Shortcode: "rabbit"}
|
||||||
|
RabbitTwo = Reaction{Raw: "🐇", Shortcode: "rabbit2"}
|
||||||
|
Racehorse = Reaction{Raw: "🐎", Shortcode: "racehorse"}
|
||||||
|
RAM = Reaction{Raw: "🐏", Shortcode: "ram"}
|
||||||
|
Rat = Reaction{Raw: "🐀", Shortcode: "rat"}
|
||||||
|
Rooster = Reaction{Raw: "🐓", Shortcode: "rooster"}
|
||||||
|
Rose = Reaction{Raw: "🌹", Shortcode: "rose"}
|
||||||
|
Seedling = Reaction{Raw: "🌱", Shortcode: "seedling"}
|
||||||
|
Sheep = Reaction{Raw: "🐑", Shortcode: "sheep"}
|
||||||
|
Shell = Reaction{Raw: "🐚", Shortcode: "shell"}
|
||||||
|
Snail = Reaction{Raw: "🐌", Shortcode: "snail"}
|
||||||
|
Snake = Reaction{Raw: "🐍", Shortcode: "snake"}
|
||||||
|
Snowflake = Reaction{Raw: "❄", Shortcode: "snowflake"}
|
||||||
|
Snowman = Reaction{Raw: "☃", Shortcode: "snowman"}
|
||||||
|
Squirrel = Reaction{Raw: "", Shortcode: "squirrel"}
|
||||||
|
SunWithFace = Reaction{Raw: "🌞", Shortcode: "sun_with_face"}
|
||||||
|
Sunflower = Reaction{Raw: "🌻", Shortcode: "sunflower"}
|
||||||
|
Sunny = Reaction{Raw: "☀", Shortcode: "sunny"}
|
||||||
|
Tiger = Reaction{Raw: "🐅", Shortcode: "tiger"}
|
||||||
|
TigerTwo = Reaction{Raw: "🐅", Shortcode: "tiger2"}
|
||||||
|
TropicalFish = Reaction{Raw: "🐠", Shortcode: "tropical_fish"}
|
||||||
|
Tulip = Reaction{Raw: "🌷", Shortcode: "tulip"}
|
||||||
|
Turtle = Reaction{Raw: "🐢", Shortcode: "turtle"}
|
||||||
|
Umbrella = Reaction{Raw: "☂", Shortcode: "umbrella"}
|
||||||
|
Volcano = Reaction{Raw: "🌋", Shortcode: "volcano"}
|
||||||
|
WaningCrescentMoon = Reaction{Raw: "🌘", Shortcode: "waning_crescent_moon"}
|
||||||
|
WaningGibbousMoon = Reaction{Raw: "🌖", Shortcode: "waning_gibbous_moon"}
|
||||||
|
WaterBuffalo = Reaction{Raw: "🐃", Shortcode: "water_buffalo"}
|
||||||
|
WaxingCrescentMoon = Reaction{Raw: "🌒", Shortcode: "waxing_crescent_moon"}
|
||||||
|
WaxingGibbousMoon = Reaction{Raw: "🌔", Shortcode: "waxing_gibbous_moon"}
|
||||||
|
Whale = Reaction{Raw: "🐋", Shortcode: "whale"}
|
||||||
|
WhaleTwo = Reaction{Raw: "🐋", Shortcode: "whale2"}
|
||||||
|
Wolf = Reaction{Raw: "🐺", Shortcode: "wolf"}
|
||||||
|
Zap = Reaction{Raw: "⚡", Shortcode: "zap"}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Objects emojis.
|
||||||
|
var (
|
||||||
|
Eightball = Reaction{Raw: "🎱", Shortcode: "8ball"}
|
||||||
|
AlarmClock = Reaction{Raw: "⏰", Shortcode: "alarm_clock"}
|
||||||
|
Apple = Reaction{Raw: "🍎", Shortcode: "apple"}
|
||||||
|
Art = Reaction{Raw: "🎨", Shortcode: "art"}
|
||||||
|
BabyBottle = Reaction{Raw: "🍼", Shortcode: "baby_bottle"}
|
||||||
|
Balloon = Reaction{Raw: "🎈", Shortcode: "balloon"}
|
||||||
|
Bamboo = Reaction{Raw: "🎍", Shortcode: "bamboo"}
|
||||||
|
Banana = Reaction{Raw: "🍌", Shortcode: "banana"}
|
||||||
|
BarChart = Reaction{Raw: "📊", Shortcode: "bar_chart"}
|
||||||
|
Baseball = Reaction{Raw: "⚾", Shortcode: "baseball"}
|
||||||
|
Basketball = Reaction{Raw: "🏀", Shortcode: "basketball"}
|
||||||
|
Bath = Reaction{Raw: "🛀", Shortcode: "bath"}
|
||||||
|
Bathtub = Reaction{Raw: "🛁", Shortcode: "bathtub"}
|
||||||
|
Battery = Reaction{Raw: "🔋", Shortcode: "battery"}
|
||||||
|
Beer = Reaction{Raw: "🍺", Shortcode: "beer"}
|
||||||
|
Beers = Reaction{Raw: "🍻", Shortcode: "beers"}
|
||||||
|
Bell = Reaction{Raw: "🔔", Shortcode: "bell"}
|
||||||
|
Bento = Reaction{Raw: "🍱", Shortcode: "bento"}
|
||||||
|
Bicyclist = Reaction{Raw: "🚴", Shortcode: "bicyclist"}
|
||||||
|
Bikini = Reaction{Raw: "👙", Shortcode: "bikini"}
|
||||||
|
Birthday = Reaction{Raw: "🎂", Shortcode: "birthday"}
|
||||||
|
BlackJoker = Reaction{Raw: "🃏", Shortcode: "black_joker"}
|
||||||
|
BlackNib = Reaction{Raw: "✒", Shortcode: "black_nib"}
|
||||||
|
BlueBook = Reaction{Raw: "📘", Shortcode: "blue_book"}
|
||||||
|
Bomb = Reaction{Raw: "💣", Shortcode: "bomb"}
|
||||||
|
Book = Reaction{Raw: "📖", Shortcode: "book"}
|
||||||
|
Bookmark = Reaction{Raw: "🔖", Shortcode: "bookmark"}
|
||||||
|
BookmarkTabs = Reaction{Raw: "📑", Shortcode: "bookmark_tabs"}
|
||||||
|
Books = Reaction{Raw: "📚", Shortcode: "books"}
|
||||||
|
Boot = Reaction{Raw: "👢", Shortcode: "boot"}
|
||||||
|
Bowling = Reaction{Raw: "🎳", Shortcode: "bowling"}
|
||||||
|
Bread = Reaction{Raw: "🍞", Shortcode: "bread"}
|
||||||
|
Briefcase = Reaction{Raw: "💼", Shortcode: "briefcase"}
|
||||||
|
Bulb = Reaction{Raw: "💡", Shortcode: "bulb"}
|
||||||
|
Cake = Reaction{Raw: "🍰", Shortcode: "cake"}
|
||||||
|
Calendar = Reaction{Raw: "📅", Shortcode: "calendar"}
|
||||||
|
Calling = Reaction{Raw: "📲", Shortcode: "calling"}
|
||||||
|
Camera = Reaction{Raw: "📷", Shortcode: "camera"}
|
||||||
|
Candy = Reaction{Raw: "🍬", Shortcode: "candy"}
|
||||||
|
CardIndex = Reaction{Raw: "📇", Shortcode: "card_index"}
|
||||||
|
Cd = Reaction{Raw: "💿", Shortcode: "cd"}
|
||||||
|
ChartWithDownwardsTrend = Reaction{Raw: "📉", Shortcode: "chart_with_downwards_trend"}
|
||||||
|
ChartWithUpwardsTrend = Reaction{Raw: "📈", Shortcode: "chart_with_upwards_trend"}
|
||||||
|
Cherries = Reaction{Raw: "🍒", Shortcode: "cherries"}
|
||||||
|
ChocolateBar = Reaction{Raw: "🍫", Shortcode: "chocolate_bar"}
|
||||||
|
ChristmasTree = Reaction{Raw: "🎄", Shortcode: "christmas_tree"}
|
||||||
|
Clapper = Reaction{Raw: "🎬", Shortcode: "clapper"}
|
||||||
|
Clipboard = Reaction{Raw: "📋", Shortcode: "clipboard"}
|
||||||
|
ClosedBook = Reaction{Raw: "📕", Shortcode: "closed_book"}
|
||||||
|
ClosedLockWithKey = Reaction{Raw: "🔐", Shortcode: "closed_lock_with_key"}
|
||||||
|
ClosedUmbrella = Reaction{Raw: "🌂", Shortcode: "closed_umbrella"}
|
||||||
|
Clubs = Reaction{Raw: "♣", Shortcode: "clubs"}
|
||||||
|
Cocktail = Reaction{Raw: "🍸", Shortcode: "cocktail"}
|
||||||
|
Coffee = Reaction{Raw: "☕", Shortcode: "coffee"}
|
||||||
|
Computer = Reaction{Raw: "💻", Shortcode: "computer"}
|
||||||
|
ConfettiBall = Reaction{Raw: "🎊", Shortcode: "confetti_ball"}
|
||||||
|
Cookie = Reaction{Raw: "🍪", Shortcode: "cookie"}
|
||||||
|
Corn = Reaction{Raw: "🌽", Shortcode: "corn"}
|
||||||
|
CreditCard = Reaction{Raw: "💳", Shortcode: "credit_card"}
|
||||||
|
Crown = Reaction{Raw: "👑", Shortcode: "crown"}
|
||||||
|
CrystalBall = Reaction{Raw: "🔮", Shortcode: "crystal_ball"}
|
||||||
|
Curry = Reaction{Raw: "🍛", Shortcode: "curry"}
|
||||||
|
Custard = Reaction{Raw: "🍮", Shortcode: "custard"}
|
||||||
|
Dango = Reaction{Raw: "🍡", Shortcode: "dango"}
|
||||||
|
Dart = Reaction{Raw: "🎯", Shortcode: "dart"}
|
||||||
|
Date = Reaction{Raw: "📅", Shortcode: "date"}
|
||||||
|
Diamonds = Reaction{Raw: "♦", Shortcode: "diamonds"}
|
||||||
|
Dollar = Reaction{Raw: "💵", Shortcode: "dollar"}
|
||||||
|
Dolls = Reaction{Raw: "🎎", Shortcode: "dolls"}
|
||||||
|
Door = Reaction{Raw: "🚪", Shortcode: "door"}
|
||||||
|
Doughnut = Reaction{Raw: "🍩", Shortcode: "doughnut"}
|
||||||
|
Dress = Reaction{Raw: "👗", Shortcode: "dress"}
|
||||||
|
Dvd = Reaction{Raw: "📀", Shortcode: "dvd"}
|
||||||
|
EMinusmail = Reaction{Raw: "📧", Shortcode: "e-mail"}
|
||||||
|
Egg = Reaction{Raw: "🥚", Shortcode: "egg"}
|
||||||
|
Eggplant = Reaction{Raw: "🍆", Shortcode: "eggplant"}
|
||||||
|
ElectricPlug = Reaction{Raw: "🔌", Shortcode: "electric_plug"}
|
||||||
|
Email = Reaction{Raw: "✉️", Shortcode: "email"}
|
||||||
|
Envelope = Reaction{Raw: "✉", Shortcode: "envelope"}
|
||||||
|
Euro = Reaction{Raw: "💶", Shortcode: "euro"}
|
||||||
|
Eyeglasses = Reaction{Raw: "👓", Shortcode: "eyeglasses"}
|
||||||
|
Fax = Reaction{Raw: "📠", Shortcode: "fax"}
|
||||||
|
FileFolder = Reaction{Raw: "📁", Shortcode: "file_folder"}
|
||||||
|
Fireworks = Reaction{Raw: "🎆", Shortcode: "fireworks"}
|
||||||
|
FishCake = Reaction{Raw: "🍥", Shortcode: "fish_cake"}
|
||||||
|
FishingPoleAndFish = Reaction{Raw: "🎣", Shortcode: "fishing_pole_and_fish"}
|
||||||
|
Flags = Reaction{Raw: "🎏", Shortcode: "flags"}
|
||||||
|
Flashlight = Reaction{Raw: "🔦", Shortcode: "flashlight"}
|
||||||
|
FloppyDisk = Reaction{Raw: "💾", Shortcode: "floppy_disk"}
|
||||||
|
FlowerPlayingCards = Reaction{Raw: "🎴", Shortcode: "flower_playing_cards"}
|
||||||
|
Football = Reaction{Raw: "🏈", Shortcode: "football"}
|
||||||
|
ForkAndKnife = Reaction{Raw: "🍴", Shortcode: "fork_and_knife"}
|
||||||
|
FriedShrimp = Reaction{Raw: "🍤", Shortcode: "fried_shrimp"}
|
||||||
|
Fries = Reaction{Raw: "🍟", Shortcode: "fries"}
|
||||||
|
GameDie = Reaction{Raw: "🎲", Shortcode: "game_die"}
|
||||||
|
Gem = Reaction{Raw: "💎", Shortcode: "gem"}
|
||||||
|
Ghost = Reaction{Raw: "👻", Shortcode: "ghost"}
|
||||||
|
Gift = Reaction{Raw: "🎁", Shortcode: "gift"}
|
||||||
|
GiftHeart = Reaction{Raw: "💝", Shortcode: "gift_heart"}
|
||||||
|
Golf = Reaction{Raw: "⛳", Shortcode: "golf"}
|
||||||
|
Grapes = Reaction{Raw: "🍇", Shortcode: "grapes"}
|
||||||
|
GreenApple = Reaction{Raw: "🍏", Shortcode: "green_apple"}
|
||||||
|
GreenBook = Reaction{Raw: "📗", Shortcode: "green_book"}
|
||||||
|
Guitar = Reaction{Raw: "🎸", Shortcode: "guitar"}
|
||||||
|
Gun = Reaction{Raw: "🔫", Shortcode: "gun"}
|
||||||
|
Hamburger = Reaction{Raw: "🍔", Shortcode: "hamburger"}
|
||||||
|
Hammer = Reaction{Raw: "🔨", Shortcode: "hammer"}
|
||||||
|
Handbag = Reaction{Raw: "👜", Shortcode: "handbag"}
|
||||||
|
Headphones = Reaction{Raw: "🎧", Shortcode: "headphones"}
|
||||||
|
Hearts = Reaction{Raw: "♥", Shortcode: "hearts"}
|
||||||
|
HighBrightness = Reaction{Raw: "🔆", Shortcode: "high_brightness"}
|
||||||
|
HighHeel = Reaction{Raw: "👠", Shortcode: "high_heel"}
|
||||||
|
Hocho = Reaction{Raw: "🔪", Shortcode: "hocho"}
|
||||||
|
HoneyPot = Reaction{Raw: "🍯", Shortcode: "honey_pot"}
|
||||||
|
HorseRacing = Reaction{Raw: "🏇", Shortcode: "horse_racing"}
|
||||||
|
Hourglass = Reaction{Raw: "⌛", Shortcode: "hourglass"}
|
||||||
|
HourglassFlowingSand = Reaction{Raw: "⏳", Shortcode: "hourglass_flowing_sand"}
|
||||||
|
IceCream = Reaction{Raw: "🍨", Shortcode: "ice_cream"}
|
||||||
|
Icecream = Reaction{Raw: "🍦", Shortcode: "icecream"}
|
||||||
|
InboxTray = Reaction{Raw: "📥", Shortcode: "inbox_tray"}
|
||||||
|
IncomingEnvelope = Reaction{Raw: "📨", Shortcode: "incoming_envelope"}
|
||||||
|
Iphone = Reaction{Raw: "📱", Shortcode: "iphone"}
|
||||||
|
JackOLantern = Reaction{Raw: "🎃", Shortcode: "jack_o_lantern"}
|
||||||
|
Jeans = Reaction{Raw: "👖", Shortcode: "jeans"}
|
||||||
|
Key = Reaction{Raw: "🔑", Shortcode: "key"}
|
||||||
|
Kimono = Reaction{Raw: "👘", Shortcode: "kimono"}
|
||||||
|
Ledger = Reaction{Raw: "📒", Shortcode: "ledger"}
|
||||||
|
Lemon = Reaction{Raw: "🍋", Shortcode: "lemon"}
|
||||||
|
Lipstick = Reaction{Raw: "💄", Shortcode: "lipstick"}
|
||||||
|
Lock = Reaction{Raw: "🔒", Shortcode: "lock"}
|
||||||
|
LockWithInkPen = Reaction{Raw: "🔏", Shortcode: "lock_with_ink_pen"}
|
||||||
|
Lollipop = Reaction{Raw: "🍭", Shortcode: "lollipop"}
|
||||||
|
Loop = Reaction{Raw: "➿", Shortcode: "loop"}
|
||||||
|
Loudspeaker = Reaction{Raw: "📢", Shortcode: "loudspeaker"}
|
||||||
|
LowBrightness = Reaction{Raw: "🔅", Shortcode: "low_brightness"}
|
||||||
|
Mag = Reaction{Raw: "🔍", Shortcode: "mag"}
|
||||||
|
MagRight = Reaction{Raw: "🔎", Shortcode: "mag_right"}
|
||||||
|
Mahjong = Reaction{Raw: "🀄", Shortcode: "mahjong"}
|
||||||
|
Mailbox = Reaction{Raw: "📫", Shortcode: "mailbox"}
|
||||||
|
MailboxClosed = Reaction{Raw: "📪", Shortcode: "mailbox_closed"}
|
||||||
|
MailboxWithMail = Reaction{Raw: "📬", Shortcode: "mailbox_with_mail"}
|
||||||
|
MailboxWithNoMail = Reaction{Raw: "📭", Shortcode: "mailbox_with_no_mail"}
|
||||||
|
MansShoe = Reaction{Raw: "👞", Shortcode: "mans_shoe"}
|
||||||
|
MeatOnBone = Reaction{Raw: "🍖", Shortcode: "meat_on_bone"}
|
||||||
|
Mega = Reaction{Raw: "📣", Shortcode: "mega"}
|
||||||
|
Melon = Reaction{Raw: "🍈", Shortcode: "melon"}
|
||||||
|
Memo = Reaction{Raw: "📝", Shortcode: "memo"}
|
||||||
|
Microphone = Reaction{Raw: "🎤", Shortcode: "microphone"}
|
||||||
|
Microscope = Reaction{Raw: "🔬", Shortcode: "microscope"}
|
||||||
|
Minidisc = Reaction{Raw: "💽", Shortcode: "minidisc"}
|
||||||
|
MoneyWithWings = Reaction{Raw: "💸", Shortcode: "money_with_wings"}
|
||||||
|
Moneybag = Reaction{Raw: "💰", Shortcode: "moneybag"}
|
||||||
|
MortarBoard = Reaction{Raw: "🎓", Shortcode: "mortar_board"}
|
||||||
|
MountainBicyclist = Reaction{Raw: "🚵", Shortcode: "mountain_bicyclist"}
|
||||||
|
MovieCamera = Reaction{Raw: "🎥", Shortcode: "movie_camera"}
|
||||||
|
MusicalKeyboard = Reaction{Raw: "🎹", Shortcode: "musical_keyboard"}
|
||||||
|
MusicalScore = Reaction{Raw: "🎼", Shortcode: "musical_score"}
|
||||||
|
Mute = Reaction{Raw: "🔇", Shortcode: "mute"}
|
||||||
|
NameBadge = Reaction{Raw: "📛", Shortcode: "name_badge"}
|
||||||
|
Necktie = Reaction{Raw: "👔", Shortcode: "necktie"}
|
||||||
|
Newspaper = Reaction{Raw: "📰", Shortcode: "newspaper"}
|
||||||
|
NoBell = Reaction{Raw: "🔕", Shortcode: "no_bell"}
|
||||||
|
Notebook = Reaction{Raw: "📓", Shortcode: "notebook"}
|
||||||
|
NotebookWithDecorativeCover = Reaction{Raw: "📔", Shortcode: "notebook_with_decorative_cover"}
|
||||||
|
NutAndBolt = Reaction{Raw: "🔩", Shortcode: "nut_and_bolt"}
|
||||||
|
Oden = Reaction{Raw: "🍢", Shortcode: "oden"}
|
||||||
|
OpenFileFolder = Reaction{Raw: "📂", Shortcode: "open_file_folder"}
|
||||||
|
OrangeBook = Reaction{Raw: "📙", Shortcode: "orange_book"}
|
||||||
|
OutboxTray = Reaction{Raw: "📤", Shortcode: "outbox_tray"}
|
||||||
|
Package = Reaction{Raw: "📦", Shortcode: "package"}
|
||||||
|
PageFacingUp = Reaction{Raw: "📄", Shortcode: "page_facing_up"}
|
||||||
|
PageWithCurl = Reaction{Raw: "📃", Shortcode: "page_with_curl"}
|
||||||
|
Pager = Reaction{Raw: "📟", Shortcode: "pager"}
|
||||||
|
Paperclip = Reaction{Raw: "📎", Shortcode: "paperclip"}
|
||||||
|
Peach = Reaction{Raw: "🍑", Shortcode: "peach"}
|
||||||
|
Pear = Reaction{Raw: "🍐", Shortcode: "pear"}
|
||||||
|
Pencil = Reaction{Raw: "✏", Shortcode: "pencil"}
|
||||||
|
PencilTwo = Reaction{Raw: "✏", Shortcode: "pencil2"}
|
||||||
|
Phone = Reaction{Raw: "☎️", Shortcode: "phone"}
|
||||||
|
Pill = Reaction{Raw: "💊", Shortcode: "pill"}
|
||||||
|
Pineapple = Reaction{Raw: "🍍", Shortcode: "pineapple"}
|
||||||
|
Pizza = Reaction{Raw: "🍕", Shortcode: "pizza"}
|
||||||
|
PostalHorn = Reaction{Raw: "📯", Shortcode: "postal_horn"}
|
||||||
|
Postbox = Reaction{Raw: "📮", Shortcode: "postbox"}
|
||||||
|
Pouch = Reaction{Raw: "👝", Shortcode: "pouch"}
|
||||||
|
PoultryLeg = Reaction{Raw: "🍗", Shortcode: "poultry_leg"}
|
||||||
|
Pound = Reaction{Raw: "💷", Shortcode: "pound"}
|
||||||
|
Purse = Reaction{Raw: "👛", Shortcode: "purse"}
|
||||||
|
Pushpin = Reaction{Raw: "📌", Shortcode: "pushpin"}
|
||||||
|
Radio = Reaction{Raw: "📻", Shortcode: "radio"}
|
||||||
|
Ramen = Reaction{Raw: "🍜", Shortcode: "ramen"}
|
||||||
|
Ribbon = Reaction{Raw: "🎀", Shortcode: "ribbon"}
|
||||||
|
Rice = Reaction{Raw: "🍚", Shortcode: "rice"}
|
||||||
|
RiceBall = Reaction{Raw: "🍙", Shortcode: "rice_ball"}
|
||||||
|
RiceCracker = Reaction{Raw: "🍘", Shortcode: "rice_cracker"}
|
||||||
|
RiceScene = Reaction{Raw: "🎑", Shortcode: "rice_scene"}
|
||||||
|
Ring = Reaction{Raw: "💍", Shortcode: "ring"}
|
||||||
|
RugbyFootball = Reaction{Raw: "🏉", Shortcode: "rugby_football"}
|
||||||
|
RunningShirtWithSash = Reaction{Raw: "🎽", Shortcode: "running_shirt_with_sash"}
|
||||||
|
Sake = Reaction{Raw: "🍶", Shortcode: "sake"}
|
||||||
|
Sandal = Reaction{Raw: "👡", Shortcode: "sandal"}
|
||||||
|
Santa = Reaction{Raw: "🎅", Shortcode: "santa"}
|
||||||
|
Satellite = Reaction{Raw: "🛰", Shortcode: "satellite"}
|
||||||
|
Saxophone = Reaction{Raw: "🎷", Shortcode: "saxophone"}
|
||||||
|
SchoolSatchel = Reaction{Raw: "🎒", Shortcode: "school_satchel"}
|
||||||
|
Scissors = Reaction{Raw: "✂", Shortcode: "scissors"}
|
||||||
|
Scroll = Reaction{Raw: "📜", Shortcode: "scroll"}
|
||||||
|
Seat = Reaction{Raw: "💺", Shortcode: "seat"}
|
||||||
|
ShavedIce = Reaction{Raw: "🍧", Shortcode: "shaved_ice"}
|
||||||
|
Shirt = Reaction{Raw: "👕", Shortcode: "shirt"}
|
||||||
|
Shoe = Reaction{Raw: "👞", Shortcode: "shoe"}
|
||||||
|
Shower = Reaction{Raw: "🚿", Shortcode: "shower"}
|
||||||
|
Ski = Reaction{Raw: "🎿", Shortcode: "ski"}
|
||||||
|
Smoking = Reaction{Raw: "🚬", Shortcode: "smoking"}
|
||||||
|
Snowboarder = Reaction{Raw: "🏂", Shortcode: "snowboarder"}
|
||||||
|
Soccer = Reaction{Raw: "⚽", Shortcode: "soccer"}
|
||||||
|
Sound = Reaction{Raw: "🔉", Shortcode: "sound"}
|
||||||
|
SpaceInvader = Reaction{Raw: "👾", Shortcode: "space_invader"}
|
||||||
|
Spades = Reaction{Raw: "♠", Shortcode: "spades"}
|
||||||
|
Spaghetti = Reaction{Raw: "🍝", Shortcode: "spaghetti"}
|
||||||
|
Sparkler = Reaction{Raw: "🎇", Shortcode: "sparkler"}
|
||||||
|
Speaker = Reaction{Raw: "🔈", Shortcode: "speaker"}
|
||||||
|
Stew = Reaction{Raw: "🍲", Shortcode: "stew"}
|
||||||
|
StraightRuler = Reaction{Raw: "📏", Shortcode: "straight_ruler"}
|
||||||
|
Strawberry = Reaction{Raw: "🍓", Shortcode: "strawberry"}
|
||||||
|
Surfer = Reaction{Raw: "🏄", Shortcode: "surfer"}
|
||||||
|
Sushi = Reaction{Raw: "🍣", Shortcode: "sushi"}
|
||||||
|
SweetPotato = Reaction{Raw: "🍠", Shortcode: "sweet_potato"}
|
||||||
|
Swimmer = Reaction{Raw: "🏊", Shortcode: "swimmer"}
|
||||||
|
Syringe = Reaction{Raw: "💉", Shortcode: "syringe"}
|
||||||
|
Tada = Reaction{Raw: "🎉", Shortcode: "tada"}
|
||||||
|
TanabataTree = Reaction{Raw: "🎋", Shortcode: "tanabata_tree"}
|
||||||
|
Tangerine = Reaction{Raw: "🍊", Shortcode: "tangerine"}
|
||||||
|
Tea = Reaction{Raw: "🍵", Shortcode: "tea"}
|
||||||
|
Telephone = Reaction{Raw: "☎", Shortcode: "telephone"}
|
||||||
|
TelephoneReceiver = Reaction{Raw: "📞", Shortcode: "telephone_receiver"}
|
||||||
|
Telescope = Reaction{Raw: "🔭", Shortcode: "telescope"}
|
||||||
|
Tennis = Reaction{Raw: "🎾", Shortcode: "tennis"}
|
||||||
|
Toilet = Reaction{Raw: "🚽", Shortcode: "toilet"}
|
||||||
|
Tomato = Reaction{Raw: "🍅", Shortcode: "tomato"}
|
||||||
|
Tophat = Reaction{Raw: "🎩", Shortcode: "tophat"}
|
||||||
|
TriangularRuler = Reaction{Raw: "📐", Shortcode: "triangular_ruler"}
|
||||||
|
Trophy = Reaction{Raw: "🏆", Shortcode: "trophy"}
|
||||||
|
TropicalDrink = Reaction{Raw: "🍹", Shortcode: "tropical_drink"}
|
||||||
|
Trumpet = Reaction{Raw: "🎺", Shortcode: "trumpet"}
|
||||||
|
Tshirt = Reaction{Raw: "👕", Shortcode: "tshirt"}
|
||||||
|
Tv = Reaction{Raw: "📺", Shortcode: "tv"}
|
||||||
|
Unlock = Reaction{Raw: "🔓", Shortcode: "unlock"}
|
||||||
|
Vhs = Reaction{Raw: "📼", Shortcode: "vhs"}
|
||||||
|
VideoCamera = Reaction{Raw: "📹", Shortcode: "video_camera"}
|
||||||
|
VideoGame = Reaction{Raw: "🎮", Shortcode: "video_game"}
|
||||||
|
Violin = Reaction{Raw: "🎻", Shortcode: "violin"}
|
||||||
|
Watch = Reaction{Raw: "⌚", Shortcode: "watch"}
|
||||||
|
Watermelon = Reaction{Raw: "🍉", Shortcode: "watermelon"}
|
||||||
|
WindChime = Reaction{Raw: "🎐", Shortcode: "wind_chime"}
|
||||||
|
WineGlass = Reaction{Raw: "🍷", Shortcode: "wine_glass"}
|
||||||
|
WomansClothes = Reaction{Raw: "👚", Shortcode: "womans_clothes"}
|
||||||
|
WomansHat = Reaction{Raw: "👒", Shortcode: "womans_hat"}
|
||||||
|
Wrench = Reaction{Raw: "🔧", Shortcode: "wrench"}
|
||||||
|
Yen = Reaction{Raw: "💴", Shortcode: "yen"}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Places emojis.
|
||||||
|
var (
|
||||||
|
AerialTramway = Reaction{Raw: "🚡", Shortcode: "aerial_tramway"}
|
||||||
|
Airplane = Reaction{Raw: "✈", Shortcode: "airplane"}
|
||||||
|
Ambulance = Reaction{Raw: "🚑", Shortcode: "ambulance"}
|
||||||
|
Anchor = Reaction{Raw: "⚓", Shortcode: "anchor"}
|
||||||
|
ArticulatedLorry = Reaction{Raw: "🚛", Shortcode: "articulated_lorry"}
|
||||||
|
Atm = Reaction{Raw: "🏧", Shortcode: "atm"}
|
||||||
|
Bank = Reaction{Raw: "🏦", Shortcode: "bank"}
|
||||||
|
Barber = Reaction{Raw: "💈", Shortcode: "barber"}
|
||||||
|
Beginner = Reaction{Raw: "🔰", Shortcode: "beginner"}
|
||||||
|
Bike = Reaction{Raw: "🚲", Shortcode: "bike"}
|
||||||
|
BlueCar = Reaction{Raw: "🚙", Shortcode: "blue_car"}
|
||||||
|
Boat = Reaction{Raw: "⛵️", Shortcode: "boat"}
|
||||||
|
BridgeAtNight = Reaction{Raw: "🌉", Shortcode: "bridge_at_night"}
|
||||||
|
BullettrainFront = Reaction{Raw: "🚅", Shortcode: "bullettrain_front"}
|
||||||
|
BullettrainSide = Reaction{Raw: "🚄", Shortcode: "bullettrain_side"}
|
||||||
|
Bus = Reaction{Raw: "🚌", Shortcode: "bus"}
|
||||||
|
Busstop = Reaction{Raw: "🚏", Shortcode: "busstop"}
|
||||||
|
Car = Reaction{Raw: "🚗", Shortcode: "car"}
|
||||||
|
CarouselHorse = Reaction{Raw: "🎠", Shortcode: "carousel_horse"}
|
||||||
|
CheckeredFlag = Reaction{Raw: "🏁", Shortcode: "checkered_flag"}
|
||||||
|
Church = Reaction{Raw: "⛪", Shortcode: "church"}
|
||||||
|
CircusTent = Reaction{Raw: "🎪", Shortcode: "circus_tent"}
|
||||||
|
CitySunrise = Reaction{Raw: "🌇", Shortcode: "city_sunrise"}
|
||||||
|
CitySunset = Reaction{Raw: "🌇", Shortcode: "city_sunset"}
|
||||||
|
Cn = Reaction{Raw: "🇨🇳", Shortcode: "cn"}
|
||||||
|
Construction = Reaction{Raw: "🚧", Shortcode: "construction"}
|
||||||
|
ConvenienceStore = Reaction{Raw: "🏪", Shortcode: "convenience_store"}
|
||||||
|
CrossedFlags = Reaction{Raw: "🎌", Shortcode: "crossed_flags"}
|
||||||
|
De = Reaction{Raw: "🇩🇪", Shortcode: "de"}
|
||||||
|
DepartmentStore = Reaction{Raw: "🏬", Shortcode: "department_store"}
|
||||||
|
Es = Reaction{Raw: "🇪🇸", Shortcode: "es"}
|
||||||
|
EuropeanCastle = Reaction{Raw: "🏰", Shortcode: "european_castle"}
|
||||||
|
EuropeanPostOffice = Reaction{Raw: "🏤", Shortcode: "european_post_office"}
|
||||||
|
Factory = Reaction{Raw: "🏭", Shortcode: "factory"}
|
||||||
|
FerrisWheel = Reaction{Raw: "🎡", Shortcode: "ferris_wheel"}
|
||||||
|
FireEngine = Reaction{Raw: "🚒", Shortcode: "fire_engine"}
|
||||||
|
Fountain = Reaction{Raw: "⛲", Shortcode: "fountain"}
|
||||||
|
Fr = Reaction{Raw: "🇫🇷", Shortcode: "fr"}
|
||||||
|
Fuelpump = Reaction{Raw: "⛽", Shortcode: "fuelpump"}
|
||||||
|
Gb = Reaction{Raw: "🇬🇧", Shortcode: "gb"}
|
||||||
|
Helicopter = Reaction{Raw: "🚁", Shortcode: "helicopter"}
|
||||||
|
Hospital = Reaction{Raw: "🏥", Shortcode: "hospital"}
|
||||||
|
Hotel = Reaction{Raw: "🏨", Shortcode: "hotel"}
|
||||||
|
Hotsprings = Reaction{Raw: "♨", Shortcode: "hotsprings"}
|
||||||
|
House = Reaction{Raw: "🏠", Shortcode: "house"}
|
||||||
|
HouseWithGarden = Reaction{Raw: "🏡", Shortcode: "house_with_garden"}
|
||||||
|
It = Reaction{Raw: "🇮🇹", Shortcode: "it"}
|
||||||
|
IzakayaLantern = Reaction{Raw: "🏮", Shortcode: "izakaya_lantern"}
|
||||||
|
Japan = Reaction{Raw: "🗾", Shortcode: "japan"}
|
||||||
|
JapaneseCastle = Reaction{Raw: "🏯", Shortcode: "japanese_castle"}
|
||||||
|
Jp = Reaction{Raw: "🇯🇵", Shortcode: "jp"}
|
||||||
|
Kr = Reaction{Raw: "🇰🇷", Shortcode: "kr"}
|
||||||
|
LightRail = Reaction{Raw: "🚈", Shortcode: "light_rail"}
|
||||||
|
LoveHotel = Reaction{Raw: "🏩", Shortcode: "love_hotel"}
|
||||||
|
Minibus = Reaction{Raw: "🚐", Shortcode: "minibus"}
|
||||||
|
Monorail = Reaction{Raw: "🚝", Shortcode: "monorail"}
|
||||||
|
MountFuji = Reaction{Raw: "🗻", Shortcode: "mount_fuji"}
|
||||||
|
MountainCableway = Reaction{Raw: "🚠", Shortcode: "mountain_cableway"}
|
||||||
|
MountainRailway = Reaction{Raw: "🚞", Shortcode: "mountain_railway"}
|
||||||
|
Moyai = Reaction{Raw: "🗿", Shortcode: "moyai"}
|
||||||
|
Office = Reaction{Raw: "🏢", Shortcode: "office"}
|
||||||
|
OncomingAutomobile = Reaction{Raw: "🚘", Shortcode: "oncoming_automobile"}
|
||||||
|
OncomingBus = Reaction{Raw: "🚍", Shortcode: "oncoming_bus"}
|
||||||
|
OncomingPoliceCar = Reaction{Raw: "🚔", Shortcode: "oncoming_police_car"}
|
||||||
|
OncomingTaxi = Reaction{Raw: "🚖", Shortcode: "oncoming_taxi"}
|
||||||
|
PerformingArts = Reaction{Raw: "🎭", Shortcode: "performing_arts"}
|
||||||
|
PoliceCar = Reaction{Raw: "🚓", Shortcode: "police_car"}
|
||||||
|
PostOffice = Reaction{Raw: "🏤", Shortcode: "post_office"}
|
||||||
|
RailwayCar = Reaction{Raw: "🚃", Shortcode: "railway_car"}
|
||||||
|
Rainbow = Reaction{Raw: "🌈", Shortcode: "rainbow"}
|
||||||
|
RedCar = Reaction{Raw: "🚗", Shortcode: "red_car"}
|
||||||
|
Rocket = Reaction{Raw: "🚀", Shortcode: "rocket"}
|
||||||
|
RollerCoaster = Reaction{Raw: "🎢", Shortcode: "roller_coaster"}
|
||||||
|
RotatingLight = Reaction{Raw: "🚨", Shortcode: "rotating_light"}
|
||||||
|
RoundPushpin = Reaction{Raw: "📍", Shortcode: "round_pushpin"}
|
||||||
|
Rowboat = Reaction{Raw: "🚣", Shortcode: "rowboat"}
|
||||||
|
Ru = Reaction{Raw: "🇷🇺", Shortcode: "ru"}
|
||||||
|
Sailboat = Reaction{Raw: "⛵", Shortcode: "sailboat"}
|
||||||
|
School = Reaction{Raw: "🏫", Shortcode: "school"}
|
||||||
|
Ship = Reaction{Raw: "🚢", Shortcode: "ship"}
|
||||||
|
SlotMachine = Reaction{Raw: "🎰", Shortcode: "slot_machine"}
|
||||||
|
Speedboat = Reaction{Raw: "🚤", Shortcode: "speedboat"}
|
||||||
|
Stars = Reaction{Raw: "🌠", Shortcode: "stars"}
|
||||||
|
Station = Reaction{Raw: "🚉", Shortcode: "station"}
|
||||||
|
StatueOfLiberty = Reaction{Raw: "🗽", Shortcode: "statue_of_liberty"}
|
||||||
|
SteamLocomotive = Reaction{Raw: "🚂", Shortcode: "steam_locomotive"}
|
||||||
|
Sunrise = Reaction{Raw: "🌅", Shortcode: "sunrise"}
|
||||||
|
SunriseOverMountains = Reaction{Raw: "🌄", Shortcode: "sunrise_over_mountains"}
|
||||||
|
SuspensionRailway = Reaction{Raw: "🚟", Shortcode: "suspension_railway"}
|
||||||
|
Taxi = Reaction{Raw: "🚕", Shortcode: "taxi"}
|
||||||
|
Tent = Reaction{Raw: "⛺", Shortcode: "tent"}
|
||||||
|
Ticket = Reaction{Raw: "🎫", Shortcode: "ticket"}
|
||||||
|
TokyoTower = Reaction{Raw: "🗼", Shortcode: "tokyo_tower"}
|
||||||
|
Tractor = Reaction{Raw: "🚜", Shortcode: "tractor"}
|
||||||
|
TrafficLight = Reaction{Raw: "🚥", Shortcode: "traffic_light"}
|
||||||
|
Train = Reaction{Raw: "🚆", Shortcode: "train"}
|
||||||
|
TrainTwo = Reaction{Raw: "🚆", Shortcode: "train2"}
|
||||||
|
Tram = Reaction{Raw: "🚊", Shortcode: "tram"}
|
||||||
|
TriangularFlagOnPost = Reaction{Raw: "🚩", Shortcode: "triangular_flag_on_post"}
|
||||||
|
Trolleybus = Reaction{Raw: "🚎", Shortcode: "trolleybus"}
|
||||||
|
Truck = Reaction{Raw: "🚚", Shortcode: "truck"}
|
||||||
|
Uk = Reaction{Raw: "🇬🇧", Shortcode: "uk"}
|
||||||
|
Us = Reaction{Raw: "🇺🇸", Shortcode: "us"}
|
||||||
|
VerticalTrafficLight = Reaction{Raw: "🚦", Shortcode: "vertical_traffic_light"}
|
||||||
|
Warning = Reaction{Raw: "⚠", Shortcode: "warning"}
|
||||||
|
Wedding = Reaction{Raw: "💒", Shortcode: "wedding"}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Symbols emojis.
|
||||||
|
var (
|
||||||
|
OneZeroZero = Reaction{Raw: "💯", Shortcode: "100"}
|
||||||
|
OneTwoThreeFour = Reaction{Raw: "🔢", Shortcode: "1234"}
|
||||||
|
A = Reaction{Raw: "🅰", Shortcode: "a"}
|
||||||
|
Ab = Reaction{Raw: "🆎", Shortcode: "ab"}
|
||||||
|
Abc = Reaction{Raw: "🔤", Shortcode: "abc"}
|
||||||
|
Abcd = Reaction{Raw: "🔡", Shortcode: "abcd"}
|
||||||
|
Accept = Reaction{Raw: "🉑", Shortcode: "accept"}
|
||||||
|
Aquarius = Reaction{Raw: "♒", Shortcode: "aquarius"}
|
||||||
|
Aries = Reaction{Raw: "♈", Shortcode: "aries"}
|
||||||
|
ArrowBackward = Reaction{Raw: "◀", Shortcode: "arrow_backward"}
|
||||||
|
ArrowDoubleDown = Reaction{Raw: "⏬", Shortcode: "arrow_double_down"}
|
||||||
|
ArrowDoubleUp = Reaction{Raw: "⏫", Shortcode: "arrow_double_up"}
|
||||||
|
ArrowDown = Reaction{Raw: "⬇", Shortcode: "arrow_down"}
|
||||||
|
ArrowDownSmall = Reaction{Raw: "🔽", Shortcode: "arrow_down_small"}
|
||||||
|
ArrowForward = Reaction{Raw: "▶", Shortcode: "arrow_forward"}
|
||||||
|
ArrowHeadingDown = Reaction{Raw: "⤵", Shortcode: "arrow_heading_down"}
|
||||||
|
ArrowHeadingUp = Reaction{Raw: "⤴", Shortcode: "arrow_heading_up"}
|
||||||
|
ArrowLeft = Reaction{Raw: "⬅", Shortcode: "arrow_left"}
|
||||||
|
ArrowLowerLeft = Reaction{Raw: "↙", Shortcode: "arrow_lower_left"}
|
||||||
|
ArrowLowerRight = Reaction{Raw: "↘", Shortcode: "arrow_lower_right"}
|
||||||
|
ArrowRight = Reaction{Raw: "➡", Shortcode: "arrow_right"}
|
||||||
|
ArrowRightHook = Reaction{Raw: "↪", Shortcode: "arrow_right_hook"}
|
||||||
|
ArrowUp = Reaction{Raw: "⬆", Shortcode: "arrow_up"}
|
||||||
|
ArrowUpDown = Reaction{Raw: "↕", Shortcode: "arrow_up_down"}
|
||||||
|
ArrowUpSmall = Reaction{Raw: "🔼", Shortcode: "arrow_up_small"}
|
||||||
|
ArrowUpperLeft = Reaction{Raw: "↖", Shortcode: "arrow_upper_left"}
|
||||||
|
ArrowUpperRight = Reaction{Raw: "↗", Shortcode: "arrow_upper_right"}
|
||||||
|
ArrowsClockwise = Reaction{Raw: "🔃", Shortcode: "arrows_clockwise"}
|
||||||
|
ArrowsCounterclockwise = Reaction{Raw: "🔄", Shortcode: "arrows_counterclockwise"}
|
||||||
|
B = Reaction{Raw: "🅱", Shortcode: "b"}
|
||||||
|
BabySymbol = Reaction{Raw: "🚼", Shortcode: "baby_symbol"}
|
||||||
|
Back = Reaction{Raw: "🔙", Shortcode: "back"}
|
||||||
|
BaggageClaim = Reaction{Raw: "🛄", Shortcode: "baggage_claim"}
|
||||||
|
BallotBoxWithCheck = Reaction{Raw: "☑", Shortcode: "ballot_box_with_check"}
|
||||||
|
Bangbang = Reaction{Raw: "‼", Shortcode: "bangbang"}
|
||||||
|
BlackCircle = Reaction{Raw: "⚫", Shortcode: "black_circle"}
|
||||||
|
BlackLargeSquare = Reaction{Raw: "⬛", Shortcode: "black_large_square"}
|
||||||
|
BlackMediumSmallSquare = Reaction{Raw: "◾", Shortcode: "black_medium_small_square"}
|
||||||
|
BlackMediumSquare = Reaction{Raw: "◼", Shortcode: "black_medium_square"}
|
||||||
|
BlackSmallSquare = Reaction{Raw: "▪", Shortcode: "black_small_square"}
|
||||||
|
BlackSquareButton = Reaction{Raw: "🔲", Shortcode: "black_square_button"}
|
||||||
|
Cancer = Reaction{Raw: "♋", Shortcode: "cancer"}
|
||||||
|
CapitalAbcd = Reaction{Raw: "🔠", Shortcode: "capital_abcd"}
|
||||||
|
Capricorn = Reaction{Raw: "♑", Shortcode: "capricorn"}
|
||||||
|
Chart = Reaction{Raw: "💹", Shortcode: "chart"}
|
||||||
|
ChildrenCrossing = Reaction{Raw: "🚸", Shortcode: "children_crossing"}
|
||||||
|
Cinema = Reaction{Raw: "🎦", Shortcode: "cinema"}
|
||||||
|
Cl = Reaction{Raw: "🆑", Shortcode: "cl"}
|
||||||
|
ClockOne = Reaction{Raw: "🕐", Shortcode: "clock1"}
|
||||||
|
ClockOneZero = Reaction{Raw: "🕙", Shortcode: "clock10"}
|
||||||
|
ClockOneZeroThreeZero = Reaction{Raw: "🕥", Shortcode: "clock1030"}
|
||||||
|
ClockOneOne = Reaction{Raw: "🕚", Shortcode: "clock11"}
|
||||||
|
ClockOneOneThreeZero = Reaction{Raw: "🕦", Shortcode: "clock1130"}
|
||||||
|
ClockOneTwo = Reaction{Raw: "🕛", Shortcode: "clock12"}
|
||||||
|
ClockOneTwoThreeZero = Reaction{Raw: "🕧", Shortcode: "clock1230"}
|
||||||
|
ClockOneThreeZero = Reaction{Raw: "🕜", Shortcode: "clock130"}
|
||||||
|
ClockTwo = Reaction{Raw: "🕑", Shortcode: "clock2"}
|
||||||
|
ClockTwoThreeZero = Reaction{Raw: "🕝", Shortcode: "clock230"}
|
||||||
|
ClockThree = Reaction{Raw: "🕒", Shortcode: "clock3"}
|
||||||
|
ClockThreeThreeZero = Reaction{Raw: "🕞", Shortcode: "clock330"}
|
||||||
|
ClockFour = Reaction{Raw: "🕓", Shortcode: "clock4"}
|
||||||
|
ClockFourThreeZero = Reaction{Raw: "🕟", Shortcode: "clock430"}
|
||||||
|
ClockFive = Reaction{Raw: "🕔", Shortcode: "clock5"}
|
||||||
|
ClockFiveThreeZero = Reaction{Raw: "🕠", Shortcode: "clock530"}
|
||||||
|
ClockSix = Reaction{Raw: "🕕", Shortcode: "clock6"}
|
||||||
|
ClockSixThreeZero = Reaction{Raw: "🕡", Shortcode: "clock630"}
|
||||||
|
ClockSeven = Reaction{Raw: "🕖", Shortcode: "clock7"}
|
||||||
|
ClockSevenThreeZero = Reaction{Raw: "🕢", Shortcode: "clock730"}
|
||||||
|
ClockEight = Reaction{Raw: "🕗", Shortcode: "clock8"}
|
||||||
|
ClockEightThreeZero = Reaction{Raw: "🕣", Shortcode: "clock830"}
|
||||||
|
ClockNine = Reaction{Raw: "🕘", Shortcode: "clock9"}
|
||||||
|
ClockNineThreeZero = Reaction{Raw: "🕤", Shortcode: "clock930"}
|
||||||
|
Congratulations = Reaction{Raw: "㊗", Shortcode: "congratulations"}
|
||||||
|
Cool = Reaction{Raw: "🆒", Shortcode: "cool"}
|
||||||
|
Copyright = Reaction{Raw: "©", Shortcode: "copyright"}
|
||||||
|
CurlyLoop = Reaction{Raw: "➰", Shortcode: "curly_loop"}
|
||||||
|
CurrencyExchange = Reaction{Raw: "💱", Shortcode: "currency_exchange"}
|
||||||
|
Customs = Reaction{Raw: "🛃", Shortcode: "customs"}
|
||||||
|
DiamondShapeWithADotInside = Reaction{Raw: "💠", Shortcode: "diamond_shape_with_a_dot_inside"}
|
||||||
|
DoNotLitter = Reaction{Raw: "🚯", Shortcode: "do_not_litter"}
|
||||||
|
Eight = Reaction{Raw: "8️⃣", Shortcode: "eight"}
|
||||||
|
EightPointedBlackStar = Reaction{Raw: "✴", Shortcode: "eight_pointed_black_star"}
|
||||||
|
EightSpokedAsterisk = Reaction{Raw: "✳", Shortcode: "eight_spoked_asterisk"}
|
||||||
|
End = Reaction{Raw: "🔚", Shortcode: "end"}
|
||||||
|
FastForward = Reaction{Raw: "⏩", Shortcode: "fast_forward"}
|
||||||
|
Five = Reaction{Raw: "5️⃣", Shortcode: "five"}
|
||||||
|
Four = Reaction{Raw: "4️⃣", Shortcode: "four"}
|
||||||
|
Free = Reaction{Raw: "🆓", Shortcode: "free"}
|
||||||
|
Gemini = Reaction{Raw: "♊", Shortcode: "gemini"}
|
||||||
|
Hash = Reaction{Raw: "#️⃣", Shortcode: "hash"}
|
||||||
|
HeartDecoration = Reaction{Raw: "💟", Shortcode: "heart_decoration"}
|
||||||
|
HeavyCheckMark = Reaction{Raw: "✔", Shortcode: "heavy_check_mark"}
|
||||||
|
HeavyDivisionSign = Reaction{Raw: "➗", Shortcode: "heavy_division_sign"}
|
||||||
|
HeavyDollarSign = Reaction{Raw: "💲", Shortcode: "heavy_dollar_sign"}
|
||||||
|
HeavyExclamationMark = Reaction{Raw: "❗️", Shortcode: "heavy_exclamation_mark"}
|
||||||
|
HeavyMinusSign = Reaction{Raw: "➖", Shortcode: "heavy_minus_sign"}
|
||||||
|
HeavyMultiplicationX = Reaction{Raw: "✖", Shortcode: "heavy_multiplication_x"}
|
||||||
|
HeavyPlusSign = Reaction{Raw: "➕", Shortcode: "heavy_plus_sign"}
|
||||||
|
ID = Reaction{Raw: "🆔", Shortcode: "id"}
|
||||||
|
IdeographAdvantage = Reaction{Raw: "🉐", Shortcode: "ideograph_advantage"}
|
||||||
|
InformationSource = Reaction{Raw: "ℹ", Shortcode: "information_source"}
|
||||||
|
Interrobang = Reaction{Raw: "⁉", Shortcode: "interrobang"}
|
||||||
|
KeycapTen = Reaction{Raw: "🔟", Shortcode: "keycap_ten"}
|
||||||
|
Koko = Reaction{Raw: "🈁", Shortcode: "koko"}
|
||||||
|
LargeBlueCircle = Reaction{Raw: "🔵", Shortcode: "large_blue_circle"}
|
||||||
|
LargeBlueDiamond = Reaction{Raw: "🔷", Shortcode: "large_blue_diamond"}
|
||||||
|
LargeOrangeDiamond = Reaction{Raw: "🔶", Shortcode: "large_orange_diamond"}
|
||||||
|
LeftLuggage = Reaction{Raw: "🛅", Shortcode: "left_luggage"}
|
||||||
|
LeftRightArrow = Reaction{Raw: "↔", Shortcode: "left_right_arrow"}
|
||||||
|
LeftwardsArrowWithHook = Reaction{Raw: "↩", Shortcode: "leftwards_arrow_with_hook"}
|
||||||
|
Leo = Reaction{Raw: "♌", Shortcode: "leo"}
|
||||||
|
Libra = Reaction{Raw: "♎", Shortcode: "libra"}
|
||||||
|
Link = Reaction{Raw: "🔗", Shortcode: "link"}
|
||||||
|
M = Reaction{Raw: "ⓜ", Shortcode: "m"}
|
||||||
|
Mens = Reaction{Raw: "🚹", Shortcode: "mens"}
|
||||||
|
Metro = Reaction{Raw: "🚇", Shortcode: "metro"}
|
||||||
|
MobilePhoneOff = Reaction{Raw: "📴", Shortcode: "mobile_phone_off"}
|
||||||
|
NegativeSquaredCrossMark = Reaction{Raw: "❎", Shortcode: "negative_squared_cross_mark"}
|
||||||
|
New = Reaction{Raw: "🆕", Shortcode: "new"}
|
||||||
|
Ng = Reaction{Raw: "🆖", Shortcode: "ng"}
|
||||||
|
Nine = Reaction{Raw: "9️⃣", Shortcode: "nine"}
|
||||||
|
NoBicycles = Reaction{Raw: "🚳", Shortcode: "no_bicycles"}
|
||||||
|
NoEntry = Reaction{Raw: "⛔", Shortcode: "no_entry"}
|
||||||
|
NoEntrySign = Reaction{Raw: "🚫", Shortcode: "no_entry_sign"}
|
||||||
|
NoMobilePhones = Reaction{Raw: "📵", Shortcode: "no_mobile_phones"}
|
||||||
|
NoPedestrians = Reaction{Raw: "🚷", Shortcode: "no_pedestrians"}
|
||||||
|
NoSmoking = Reaction{Raw: "🚭", Shortcode: "no_smoking"}
|
||||||
|
NonMinuspotableWater = Reaction{Raw: "🚱", Shortcode: "non-potable_water"}
|
||||||
|
O = Reaction{Raw: "⭕", Shortcode: "o"}
|
||||||
|
OTwo = Reaction{Raw: "🅾", Shortcode: "o2"}
|
||||||
|
Ok = Reaction{Raw: "🆗", Shortcode: "ok"}
|
||||||
|
On = Reaction{Raw: "🔛", Shortcode: "on"}
|
||||||
|
One = Reaction{Raw: "1️⃣", Shortcode: "one"}
|
||||||
|
Ophiuchus = Reaction{Raw: "⛎", Shortcode: "ophiuchus"}
|
||||||
|
Parking = Reaction{Raw: "🅿", Shortcode: "parking"}
|
||||||
|
PartAlternationMark = Reaction{Raw: "〽", Shortcode: "part_alternation_mark"}
|
||||||
|
PassportControl = Reaction{Raw: "🛂", Shortcode: "passport_control"}
|
||||||
|
Pisces = Reaction{Raw: "♓", Shortcode: "pisces"}
|
||||||
|
PotableWater = Reaction{Raw: "🚰", Shortcode: "potable_water"}
|
||||||
|
PutLitterInItsPlace = Reaction{Raw: "🚮", Shortcode: "put_litter_in_its_place"}
|
||||||
|
RadioButton = Reaction{Raw: "🔘", Shortcode: "radio_button"}
|
||||||
|
Recycle = Reaction{Raw: "♻", Shortcode: "recycle"}
|
||||||
|
RedCircle = Reaction{Raw: "🔴", Shortcode: "red_circle"}
|
||||||
|
Registered = Reaction{Raw: "®", Shortcode: "registered"}
|
||||||
|
Repeat = Reaction{Raw: "🔁", Shortcode: "repeat"}
|
||||||
|
RepeatOne = Reaction{Raw: "🔂", Shortcode: "repeat_one"}
|
||||||
|
Restroom = Reaction{Raw: "🚻", Shortcode: "restroom"}
|
||||||
|
Rewind = Reaction{Raw: "⏪", Shortcode: "rewind"}
|
||||||
|
Sa = Reaction{Raw: "🈂", Shortcode: "sa"}
|
||||||
|
Sagittarius = Reaction{Raw: "♐", Shortcode: "sagittarius"}
|
||||||
|
Scorpius = Reaction{Raw: "♏", Shortcode: "scorpius"}
|
||||||
|
Secret = Reaction{Raw: "㊙", Shortcode: "secret"}
|
||||||
|
Seven = Reaction{Raw: "7️⃣", Shortcode: "seven"}
|
||||||
|
Shipit = Reaction{Raw: "", Shortcode: "shipit"}
|
||||||
|
SignalStrength = Reaction{Raw: "📶", Shortcode: "signal_strength"}
|
||||||
|
Six = Reaction{Raw: "6️⃣", Shortcode: "six"}
|
||||||
|
SixPointedStar = Reaction{Raw: "🔯", Shortcode: "six_pointed_star"}
|
||||||
|
SmallBlueDiamond = Reaction{Raw: "🔹", Shortcode: "small_blue_diamond"}
|
||||||
|
SmallOrangeDiamond = Reaction{Raw: "🔸", Shortcode: "small_orange_diamond"}
|
||||||
|
SmallRedTriangle = Reaction{Raw: "🔺", Shortcode: "small_red_triangle"}
|
||||||
|
SmallRedTriangleDown = Reaction{Raw: "🔻", Shortcode: "small_red_triangle_down"}
|
||||||
|
Soon = Reaction{Raw: "🔜", Shortcode: "soon"}
|
||||||
|
Sos = Reaction{Raw: "🆘", Shortcode: "sos"}
|
||||||
|
Sparkle = Reaction{Raw: "❇", Shortcode: "sparkle"}
|
||||||
|
Symbols = Reaction{Raw: "🔣", Shortcode: "symbols"}
|
||||||
|
Taurus = Reaction{Raw: "♉", Shortcode: "taurus"}
|
||||||
|
Three = Reaction{Raw: "3️⃣", Shortcode: "three"}
|
||||||
|
Tm = Reaction{Raw: "™", Shortcode: "tm"}
|
||||||
|
Top = Reaction{Raw: "🔝", Shortcode: "top"}
|
||||||
|
Trident = Reaction{Raw: "🔱", Shortcode: "trident"}
|
||||||
|
TwistedRightwardsArrows = Reaction{Raw: "🔀", Shortcode: "twisted_rightwards_arrows"}
|
||||||
|
Two = Reaction{Raw: "2️⃣", Shortcode: "two"}
|
||||||
|
UFiveTwoSevenTwo = Reaction{Raw: "🈹", Shortcode: "u5272"}
|
||||||
|
UFiveFourZeroEight = Reaction{Raw: "🈴", Shortcode: "u5408"}
|
||||||
|
UFiveFivebSix = Reaction{Raw: "🈺", Shortcode: "u55b6"}
|
||||||
|
USixThreeZeroSeven = Reaction{Raw: "🈯", Shortcode: "u6307"}
|
||||||
|
USixSevenZeroEight = Reaction{Raw: "🈷", Shortcode: "u6708"}
|
||||||
|
USixSevenZeroNine = Reaction{Raw: "🈶", Shortcode: "u6709"}
|
||||||
|
USixeEightZero = Reaction{Raw: "🈵", Shortcode: "u6e80"}
|
||||||
|
USevenOneTwoOne = Reaction{Raw: "🈚", Shortcode: "u7121"}
|
||||||
|
USevenFiveThreeThree = Reaction{Raw: "🈸", Shortcode: "u7533"}
|
||||||
|
USevenNineEightOne = Reaction{Raw: "🈲", Shortcode: "u7981"}
|
||||||
|
USevenaSevena = Reaction{Raw: "🈳", Shortcode: "u7a7a"}
|
||||||
|
Underage = Reaction{Raw: "🔞", Shortcode: "underage"}
|
||||||
|
Up = Reaction{Raw: "🆙", Shortcode: "up"}
|
||||||
|
VibrationMode = Reaction{Raw: "📳", Shortcode: "vibration_mode"}
|
||||||
|
Virgo = Reaction{Raw: "♍", Shortcode: "virgo"}
|
||||||
|
Vs = Reaction{Raw: "🆚", Shortcode: "vs"}
|
||||||
|
WavyDash = Reaction{Raw: "〰", Shortcode: "wavy_dash"}
|
||||||
|
Wc = Reaction{Raw: "🚾", Shortcode: "wc"}
|
||||||
|
Wheelchair = Reaction{Raw: "♿", Shortcode: "wheelchair"}
|
||||||
|
WhiteCheckMark = Reaction{Raw: "✅", Shortcode: "white_check_mark"}
|
||||||
|
WhiteCircle = Reaction{Raw: "⚪", Shortcode: "white_circle"}
|
||||||
|
WhiteFlower = Reaction{Raw: "💮", Shortcode: "white_flower"}
|
||||||
|
WhiteLargeSquare = Reaction{Raw: "⬜", Shortcode: "white_large_square"}
|
||||||
|
WhiteMediumSmallSquare = Reaction{Raw: "◽", Shortcode: "white_medium_small_square"}
|
||||||
|
WhiteMediumSquare = Reaction{Raw: "◻", Shortcode: "white_medium_square"}
|
||||||
|
WhiteSmallSquare = Reaction{Raw: "▫", Shortcode: "white_small_square"}
|
||||||
|
WhiteSquareButton = Reaction{Raw: "🔳", Shortcode: "white_square_button"}
|
||||||
|
Womens = Reaction{Raw: "🚺", Shortcode: "womens"}
|
||||||
|
X = Reaction{Raw: "❌", Shortcode: "x"}
|
||||||
|
Zero = Reaction{Raw: "0️⃣", Shortcode: "zero"}
|
||||||
|
)
|
||||||
|
|
||||||
|
// String returns the UTF string value of the Reaction (e.g. 👍).
|
||||||
|
func (r Reaction) String() string {
|
||||||
|
if r.Raw != "" {
|
||||||
|
return r.Raw
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.Shortcode
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,187 @@
|
||||||
|
package joe
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A Storage provides a convenient interface to a Memory implementation. It is
|
||||||
|
// responsible for how the actual key value data is encoded and provides
|
||||||
|
// concurrent access as well as logging.
|
||||||
|
//
|
||||||
|
// The default Storage that is returned by joe.NewStorage() encodes values as
|
||||||
|
// JSON and stores them in-memory.
|
||||||
|
type Storage struct {
|
||||||
|
logger *zap.Logger
|
||||||
|
mu sync.RWMutex
|
||||||
|
memory Memory
|
||||||
|
encoder MemoryEncoder
|
||||||
|
}
|
||||||
|
|
||||||
|
// The Memory interface allows the bot to persist data as key-value pairs.
|
||||||
|
// The default implementation of the Memory is to store all keys and values in
|
||||||
|
// a map (i.e. in-memory). Other implementations typically offer actual long term
|
||||||
|
// persistence into a file or to redis.
|
||||||
|
type Memory interface {
|
||||||
|
Set(key string, value []byte) error
|
||||||
|
Get(key string) ([]byte, bool, error)
|
||||||
|
Delete(key string) (bool, error)
|
||||||
|
Keys() ([]string, error)
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// A MemoryEncoder is used to encode and decode any values that are stored in
|
||||||
|
// the Memory. The default implementation that is used by the Storage uses a
|
||||||
|
// JSON encoding.
|
||||||
|
type MemoryEncoder interface {
|
||||||
|
Encode(value interface{}) ([]byte, error)
|
||||||
|
Decode(data []byte, target interface{}) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type inMemory struct {
|
||||||
|
data map[string][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type jsonEncoder struct{}
|
||||||
|
|
||||||
|
// NewStorage creates a new Storage instance that encodes values as JSON and
|
||||||
|
// stores them in-memory. You can change the memory and encoding via the
|
||||||
|
// provided setters.
|
||||||
|
func NewStorage(logger *zap.Logger) *Storage {
|
||||||
|
return &Storage{
|
||||||
|
logger: logger,
|
||||||
|
memory: newInMemory(),
|
||||||
|
encoder: new(jsonEncoder),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMemory assigns a different Memory implementation.
|
||||||
|
func (s *Storage) SetMemory(m Memory) {
|
||||||
|
s.mu.Lock()
|
||||||
|
s.memory = m
|
||||||
|
s.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMemoryEncoder assigns a different MemoryEncoder.
|
||||||
|
func (s *Storage) SetMemoryEncoder(enc MemoryEncoder) {
|
||||||
|
s.mu.Lock()
|
||||||
|
s.encoder = enc
|
||||||
|
s.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keys returns all keys known to the Memory.
|
||||||
|
func (s *Storage) Keys() ([]string, error) {
|
||||||
|
s.mu.RLock()
|
||||||
|
keys, err := s.memory.Keys()
|
||||||
|
s.mu.RUnlock()
|
||||||
|
|
||||||
|
sort.Strings(keys)
|
||||||
|
return keys, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set encodes the given data and stores it in the Memory that is managed by the
|
||||||
|
// Storage.
|
||||||
|
func (s *Storage) Set(key string, value interface{}) error {
|
||||||
|
data, err := s.encoder.Encode(value)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "encode data")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
s.logger.Debug("Writing data to memory", zap.String("key", key))
|
||||||
|
err = s.memory.Set(key, data)
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves the value under the requested key and decodes it into the
|
||||||
|
// passed "value" argument which must be a pointer. The boolean return value
|
||||||
|
// indicates if the value actually existed in the Memory and is false if it did
|
||||||
|
// not. It is legal to pass <nil> as the value if you only want to check if
|
||||||
|
// the given key exists but you do not actually care about the concrete value.
|
||||||
|
func (s *Storage) Get(key string, value interface{}) (bool, error) {
|
||||||
|
s.mu.RLock()
|
||||||
|
s.logger.Debug("Retrieving data from memory", zap.String("key", key))
|
||||||
|
data, ok, err := s.memory.Get(key)
|
||||||
|
s.mu.RUnlock()
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok || value == nil {
|
||||||
|
return ok, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.encoder.Decode(data, value)
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.Wrap(err, "decode data")
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes a key and its associated value from the memory. The boolean
|
||||||
|
// return value indicates if the key existed or not.
|
||||||
|
func (s *Storage) Delete(key string) (bool, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
s.logger.Debug("Deleting data from memory", zap.String("key", key))
|
||||||
|
ok, err := s.memory.Delete(key)
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
return ok, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the Memory that is managed by this Storage.
|
||||||
|
func (s *Storage) Close() error {
|
||||||
|
s.mu.Lock()
|
||||||
|
err := s.memory.Close()
|
||||||
|
s.mu.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func newInMemory() *inMemory {
|
||||||
|
return &inMemory{data: map[string][]byte{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *inMemory) Set(key string, value []byte) error {
|
||||||
|
m.data[key] = value
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *inMemory) Get(key string) ([]byte, bool, error) {
|
||||||
|
value, ok := m.data[key]
|
||||||
|
return value, ok, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *inMemory) Delete(key string) (bool, error) {
|
||||||
|
_, ok := m.data[key]
|
||||||
|
delete(m.data, key)
|
||||||
|
return ok, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *inMemory) Keys() ([]string, error) {
|
||||||
|
keys := make([]string, 0, len(m.data))
|
||||||
|
for k := range m.data {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *inMemory) Close() error {
|
||||||
|
m.data = map[string][]byte{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jsonEncoder) Encode(value interface{}) ([]byte, error) {
|
||||||
|
return json.Marshal(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (jsonEncoder) Decode(data []byte, target interface{}) error {
|
||||||
|
return json.Unmarshal(data, target)
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package joe
|
||||||
|
|
||||||
|
// User contains all the information about a user.
|
||||||
|
type User struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
RealName string
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Folders
|
||||||
|
_obj
|
||||||
|
_test
|
||||||
|
|
||||||
|
# Architecture specific extensions/prefixes
|
||||||
|
*.[568vq]
|
||||||
|
[568vq].out
|
||||||
|
|
||||||
|
*.cgo1.go
|
||||||
|
*.cgo2.c
|
||||||
|
_cgo_defun.c
|
||||||
|
_cgo_gotypes.go
|
||||||
|
_cgo_export.*
|
||||||
|
|
||||||
|
_testmain.go
|
||||||
|
|
||||||
|
*.exe
|
||||||
|
*.test
|
||||||
|
*.prof
|
|
@ -0,0 +1,10 @@
|
||||||
|
language: go
|
||||||
|
go_import_path: github.com/pkg/errors
|
||||||
|
go:
|
||||||
|
- 1.11.x
|
||||||
|
- 1.12.x
|
||||||
|
- 1.13.x
|
||||||
|
- tip
|
||||||
|
|
||||||
|
script:
|
||||||
|
- make check
|
|
@ -0,0 +1,23 @@
|
||||||
|
Copyright (c) 2015, Dave Cheney <[email protected]>
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright notice, this
|
||||||
|
list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer in the documentation
|
||||||
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,44 @@
|
||||||
|
PKGS := github.com/pkg/errors
|
||||||
|
SRCDIRS := $(shell go list -f '{{.Dir}}' $(PKGS))
|
||||||
|
GO := go
|
||||||
|
|
||||||
|
check: test vet gofmt misspell unconvert staticcheck ineffassign unparam
|
||||||
|
|
||||||
|
test:
|
||||||
|
$(GO) test $(PKGS)
|
||||||
|
|
||||||
|
vet: | test
|
||||||
|
$(GO) vet $(PKGS)
|
||||||
|
|
||||||
|
staticcheck:
|
||||||
|
$(GO) get honnef.co/go/tools/cmd/staticcheck
|
||||||
|
staticcheck -checks all $(PKGS)
|
||||||
|
|
||||||
|
misspell:
|
||||||
|
$(GO) get github.com/client9/misspell/cmd/misspell
|
||||||
|
misspell \
|
||||||
|
-locale GB \
|
||||||
|
-error \
|
||||||
|
*.md *.go
|
||||||
|
|
||||||
|
unconvert:
|
||||||
|
$(GO) get github.com/mdempsky/unconvert
|
||||||
|
unconvert -v $(PKGS)
|
||||||
|
|
||||||
|
ineffassign:
|
||||||
|
$(GO) get github.com/gordonklaus/ineffassign
|
||||||
|
find $(SRCDIRS) -name '*.go' | xargs ineffassign
|
||||||
|
|
||||||
|
pedantic: check errcheck
|
||||||
|
|
||||||
|
unparam:
|
||||||
|
$(GO) get mvdan.cc/unparam
|
||||||
|
unparam ./...
|
||||||
|
|
||||||
|
errcheck:
|
||||||
|
$(GO) get github.com/kisielk/errcheck
|
||||||
|
errcheck $(PKGS)
|
||||||
|
|
||||||
|
gofmt:
|
||||||
|
@echo Checking code is gofmted
|
||||||
|
@test -z "$(shell gofmt -s -l -d -e $(SRCDIRS) | tee /dev/stderr)"
|
|
@ -0,0 +1,59 @@
|
||||||
|
# errors [![Travis-CI](https://travis-ci.org/pkg/errors.svg)](https://travis-ci.org/pkg/errors) [![AppVeyor](https://ci.appveyor.com/api/projects/status/b98mptawhudj53ep/branch/master?svg=true)](https://ci.appveyor.com/project/davecheney/errors/branch/master) [![GoDoc](https://godoc.org/github.com/pkg/errors?status.svg)](http://godoc.org/github.com/pkg/errors) [![Report card](https://goreportcard.com/badge/github.com/pkg/errors)](https://goreportcard.com/report/github.com/pkg/errors) [![Sourcegraph](https://sourcegraph.com/github.com/pkg/errors/-/badge.svg)](https://sourcegraph.com/github.com/pkg/errors?badge)
|
||||||
|
|
||||||
|
Package errors provides simple error handling primitives.
|
||||||
|
|
||||||
|
`go get github.com/pkg/errors`
|
||||||
|
|
||||||
|
The traditional error handling idiom in Go is roughly akin to
|
||||||
|
```go
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
which applied recursively up the call stack results in error reports without context or debugging information. The errors package allows programmers to add context to the failure path in their code in a way that does not destroy the original value of the error.
|
||||||
|
|
||||||
|
## Adding context to an error
|
||||||
|
|
||||||
|
The errors.Wrap function returns a new error that adds context to the original error. For example
|
||||||
|
```go
|
||||||
|
_, err := ioutil.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "read failed")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
## Retrieving the cause of an error
|
||||||
|
|
||||||
|
Using `errors.Wrap` constructs a stack of errors, adding context to the preceding error. Depending on the nature of the error it may be necessary to reverse the operation of errors.Wrap to retrieve the original error for inspection. Any error value which implements this interface can be inspected by `errors.Cause`.
|
||||||
|
```go
|
||||||
|
type causer interface {
|
||||||
|
Cause() error
|
||||||
|
}
|
||||||
|
```
|
||||||
|
`errors.Cause` will recursively retrieve the topmost error which does not implement `causer`, which is assumed to be the original cause. For example:
|
||||||
|
```go
|
||||||
|
switch err := errors.Cause(err).(type) {
|
||||||
|
case *MyError:
|
||||||
|
// handle specifically
|
||||||
|
default:
|
||||||
|
// unknown error
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
[Read the package documentation for more information](https://godoc.org/github.com/pkg/errors).
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
With the upcoming [Go2 error proposals](https://go.googlesource.com/proposal/+/master/design/go2draft.md) this package is moving into maintenance mode. The roadmap for a 1.0 release is as follows:
|
||||||
|
|
||||||
|
- 0.9. Remove pre Go 1.9 and Go 1.10 support, address outstanding pull requests (if possible)
|
||||||
|
- 1.0. Final release.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Because of the Go2 errors changes, this package is not accepting proposals for new functionality. With that said, we welcome pull requests, bug fixes and issue reports.
|
||||||
|
|
||||||
|
Before sending a PR, please discuss your change by raising an issue.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
BSD-2-Clause
|
|
@ -0,0 +1,32 @@
|
||||||
|
version: build-{build}.{branch}
|
||||||
|
|
||||||
|
clone_folder: C:\gopath\src\github.com\pkg\errors
|
||||||
|
shallow_clone: true # for startup speed
|
||||||
|
|
||||||
|
environment:
|
||||||
|
GOPATH: C:\gopath
|
||||||
|
|
||||||
|
platform:
|
||||||
|
- x64
|
||||||
|
|
||||||
|
# http://www.appveyor.com/docs/installed-software
|
||||||
|
install:
|
||||||
|
# some helpful output for debugging builds
|
||||||
|
- go version
|
||||||
|
- go env
|
||||||
|
# pre-installed MinGW at C:\MinGW is 32bit only
|
||||||
|
# but MSYS2 at C:\msys64 has mingw64
|
||||||
|
- set PATH=C:\msys64\mingw64\bin;%PATH%
|
||||||
|
- gcc --version
|
||||||
|
- g++ --version
|
||||||
|
|
||||||
|
build_script:
|
||||||
|
- go install -v ./...
|
||||||
|
|
||||||
|
test_script:
|
||||||
|
- set PATH=C:\gopath\bin;%PATH%
|
||||||
|
- go test -v ./...
|
||||||
|
|
||||||
|
#artifacts:
|
||||||
|
# - path: '%GOPATH%\bin\*.exe'
|
||||||
|
deploy: off
|
|
@ -0,0 +1,288 @@
|
||||||
|
// Package errors provides simple error handling primitives.
|
||||||
|
//
|
||||||
|
// The traditional error handling idiom in Go is roughly akin to
|
||||||
|
//
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// which when applied recursively up the call stack results in error reports
|
||||||
|
// without context or debugging information. The errors package allows
|
||||||
|
// programmers to add context to the failure path in their code in a way
|
||||||
|
// that does not destroy the original value of the error.
|
||||||
|
//
|
||||||
|
// Adding context to an error
|
||||||
|
//
|
||||||
|
// The errors.Wrap function returns a new error that adds context to the
|
||||||
|
// original error by recording a stack trace at the point Wrap is called,
|
||||||
|
// together with the supplied message. For example
|
||||||
|
//
|
||||||
|
// _, err := ioutil.ReadAll(r)
|
||||||
|
// if err != nil {
|
||||||
|
// return errors.Wrap(err, "read failed")
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// If additional control is required, the errors.WithStack and
|
||||||
|
// errors.WithMessage functions destructure errors.Wrap into its component
|
||||||
|
// operations: annotating an error with a stack trace and with a message,
|
||||||
|
// respectively.
|
||||||
|
//
|
||||||
|
// Retrieving the cause of an error
|
||||||
|
//
|
||||||
|
// Using errors.Wrap constructs a stack of errors, adding context to the
|
||||||
|
// preceding error. Depending on the nature of the error it may be necessary
|
||||||
|
// to reverse the operation of errors.Wrap to retrieve the original error
|
||||||
|
// for inspection. Any error value which implements this interface
|
||||||
|
//
|
||||||
|
// type causer interface {
|
||||||
|
// Cause() error
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// can be inspected by errors.Cause. errors.Cause will recursively retrieve
|
||||||
|
// the topmost error that does not implement causer, which is assumed to be
|
||||||
|
// the original cause. For example:
|
||||||
|
//
|
||||||
|
// switch err := errors.Cause(err).(type) {
|
||||||
|
// case *MyError:
|
||||||
|
// // handle specifically
|
||||||
|
// default:
|
||||||
|
// // unknown error
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Although the causer interface is not exported by this package, it is
|
||||||
|
// considered a part of its stable public interface.
|
||||||
|
//
|
||||||
|
// Formatted printing of errors
|
||||||
|
//
|
||||||
|
// All error values returned from this package implement fmt.Formatter and can
|
||||||
|
// be formatted by the fmt package. The following verbs are supported:
|
||||||
|
//
|
||||||
|
// %s print the error. If the error has a Cause it will be
|
||||||
|
// printed recursively.
|
||||||
|
// %v see %s
|
||||||
|
// %+v extended format. Each Frame of the error's StackTrace will
|
||||||
|
// be printed in detail.
|
||||||
|
//
|
||||||
|
// Retrieving the stack trace of an error or wrapper
|
||||||
|
//
|
||||||
|
// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are
|
||||||
|
// invoked. This information can be retrieved with the following interface:
|
||||||
|
//
|
||||||
|
// type stackTracer interface {
|
||||||
|
// StackTrace() errors.StackTrace
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// The returned errors.StackTrace type is defined as
|
||||||
|
//
|
||||||
|
// type StackTrace []Frame
|
||||||
|
//
|
||||||
|
// The Frame type represents a call site in the stack trace. Frame supports
|
||||||
|
// the fmt.Formatter interface that can be used for printing information about
|
||||||
|
// the stack trace of this error. For example:
|
||||||
|
//
|
||||||
|
// if err, ok := err.(stackTracer); ok {
|
||||||
|
// for _, f := range err.StackTrace() {
|
||||||
|
// fmt.Printf("%+s:%d\n", f, f)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Although the stackTracer interface is not exported by this package, it is
|
||||||
|
// considered a part of its stable public interface.
|
||||||
|
//
|
||||||
|
// See the documentation for Frame.Format for more details.
|
||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// New returns an error with the supplied message.
|
||||||
|
// New also records the stack trace at the point it was called.
|
||||||
|
func New(message string) error {
|
||||||
|
return &fundamental{
|
||||||
|
msg: message,
|
||||||
|
stack: callers(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errorf formats according to a format specifier and returns the string
|
||||||
|
// as a value that satisfies error.
|
||||||
|
// Errorf also records the stack trace at the point it was called.
|
||||||
|
func Errorf(format string, args ...interface{}) error {
|
||||||
|
return &fundamental{
|
||||||
|
msg: fmt.Sprintf(format, args...),
|
||||||
|
stack: callers(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fundamental is an error that has a message and a stack, but no caller.
|
||||||
|
type fundamental struct {
|
||||||
|
msg string
|
||||||
|
*stack
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fundamental) Error() string { return f.msg }
|
||||||
|
|
||||||
|
func (f *fundamental) Format(s fmt.State, verb rune) {
|
||||||
|
switch verb {
|
||||||
|
case 'v':
|
||||||
|
if s.Flag('+') {
|
||||||
|
io.WriteString(s, f.msg)
|
||||||
|
f.stack.Format(s, verb)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
case 's':
|
||||||
|
io.WriteString(s, f.msg)
|
||||||
|
case 'q':
|
||||||
|
fmt.Fprintf(s, "%q", f.msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithStack annotates err with a stack trace at the point WithStack was called.
|
||||||
|
// If err is nil, WithStack returns nil.
|
||||||
|
func WithStack(err error) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &withStack{
|
||||||
|
err,
|
||||||
|
callers(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type withStack struct {
|
||||||
|
error
|
||||||
|
*stack
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *withStack) Cause() error { return w.error }
|
||||||
|
|
||||||
|
// Unwrap provides compatibility for Go 1.13 error chains.
|
||||||
|
func (w *withStack) Unwrap() error { return w.error }
|
||||||
|
|
||||||
|
func (w *withStack) Format(s fmt.State, verb rune) {
|
||||||
|
switch verb {
|
||||||
|
case 'v':
|
||||||
|
if s.Flag('+') {
|
||||||
|
fmt.Fprintf(s, "%+v", w.Cause())
|
||||||
|
w.stack.Format(s, verb)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
case 's':
|
||||||
|
io.WriteString(s, w.Error())
|
||||||
|
case 'q':
|
||||||
|
fmt.Fprintf(s, "%q", w.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap returns an error annotating err with a stack trace
|
||||||
|
// at the point Wrap is called, and the supplied message.
|
||||||
|
// If err is nil, Wrap returns nil.
|
||||||
|
func Wrap(err error, message string) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err = &withMessage{
|
||||||
|
cause: err,
|
||||||
|
msg: message,
|
||||||
|
}
|
||||||
|
return &withStack{
|
||||||
|
err,
|
||||||
|
callers(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapf returns an error annotating err with a stack trace
|
||||||
|
// at the point Wrapf is called, and the format specifier.
|
||||||
|
// If err is nil, Wrapf returns nil.
|
||||||
|
func Wrapf(err error, format string, args ...interface{}) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err = &withMessage{
|
||||||
|
cause: err,
|
||||||
|
msg: fmt.Sprintf(format, args...),
|
||||||
|
}
|
||||||
|
return &withStack{
|
||||||
|
err,
|
||||||
|
callers(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMessage annotates err with a new message.
|
||||||
|
// If err is nil, WithMessage returns nil.
|
||||||
|
func WithMessage(err error, message string) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &withMessage{
|
||||||
|
cause: err,
|
||||||
|
msg: message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMessagef annotates err with the format specifier.
|
||||||
|
// If err is nil, WithMessagef returns nil.
|
||||||
|
func WithMessagef(err error, format string, args ...interface{}) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &withMessage{
|
||||||
|
cause: err,
|
||||||
|
msg: fmt.Sprintf(format, args...),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type withMessage struct {
|
||||||
|
cause error
|
||||||
|
msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() }
|
||||||
|
func (w *withMessage) Cause() error { return w.cause }
|
||||||
|
|
||||||
|
// Unwrap provides compatibility for Go 1.13 error chains.
|
||||||
|
func (w *withMessage) Unwrap() error { return w.cause }
|
||||||
|
|
||||||
|
func (w *withMessage) Format(s fmt.State, verb rune) {
|
||||||
|
switch verb {
|
||||||
|
case 'v':
|
||||||
|
if s.Flag('+') {
|
||||||
|
fmt.Fprintf(s, "%+v\n", w.Cause())
|
||||||
|
io.WriteString(s, w.msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fallthrough
|
||||||
|
case 's', 'q':
|
||||||
|
io.WriteString(s, w.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cause returns the underlying cause of the error, if possible.
|
||||||
|
// An error value has a cause if it implements the following
|
||||||
|
// interface:
|
||||||
|
//
|
||||||
|
// type causer interface {
|
||||||
|
// Cause() error
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// If the error does not implement Cause, the original error will
|
||||||
|
// be returned. If the error is nil, nil will be returned without further
|
||||||
|
// investigation.
|
||||||
|
func Cause(err error) error {
|
||||||
|
type causer interface {
|
||||||
|
Cause() error
|
||||||
|
}
|
||||||
|
|
||||||
|
for err != nil {
|
||||||
|
cause, ok := err.(causer)
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
err = cause.Cause()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
// +build go1.13
|
||||||
|
|
||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
stderrors "errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Is reports whether any error in err's chain matches target.
|
||||||
|
//
|
||||||
|
// The chain consists of err itself followed by the sequence of errors obtained by
|
||||||
|
// repeatedly calling Unwrap.
|
||||||
|
//
|
||||||
|
// An error is considered to match a target if it is equal to that target or if
|
||||||
|
// it implements a method Is(error) bool such that Is(target) returns true.
|
||||||
|
func Is(err, target error) bool { return stderrors.Is(err, target) }
|
||||||
|
|
||||||
|
// As finds the first error in err's chain that matches target, and if so, sets
|
||||||
|
// target to that error value and returns true.
|
||||||
|
//
|
||||||
|
// The chain consists of err itself followed by the sequence of errors obtained by
|
||||||
|
// repeatedly calling Unwrap.
|
||||||
|
//
|
||||||
|
// An error matches target if the error's concrete value is assignable to the value
|
||||||
|
// pointed to by target, or if the error has a method As(interface{}) bool such that
|
||||||
|
// As(target) returns true. In the latter case, the As method is responsible for
|
||||||
|
// setting target.
|
||||||
|
//
|
||||||
|
// As will panic if target is not a non-nil pointer to either a type that implements
|
||||||
|
// error, or to any interface type. As returns false if err is nil.
|
||||||
|
func As(err error, target interface{}) bool { return stderrors.As(err, target) }
|
||||||
|
|
||||||
|
// Unwrap returns the result of calling the Unwrap method on err, if err's
|
||||||
|
// type contains an Unwrap method returning error.
|
||||||
|
// Otherwise, Unwrap returns nil.
|
||||||
|
func Unwrap(err error) error {
|
||||||
|
return stderrors.Unwrap(err)
|
||||||
|
}
|
|
@ -0,0 +1,177 @@
|
||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"path"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Frame represents a program counter inside a stack frame.
|
||||||
|
// For historical reasons if Frame is interpreted as a uintptr
|
||||||
|
// its value represents the program counter + 1.
|
||||||
|
type Frame uintptr
|
||||||
|
|
||||||
|
// pc returns the program counter for this frame;
|
||||||
|
// multiple frames may have the same PC value.
|
||||||
|
func (f Frame) pc() uintptr { return uintptr(f) - 1 }
|
||||||
|
|
||||||
|
// file returns the full path to the file that contains the
|
||||||
|
// function for this Frame's pc.
|
||||||
|
func (f Frame) file() string {
|
||||||
|
fn := runtime.FuncForPC(f.pc())
|
||||||
|
if fn == nil {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
file, _ := fn.FileLine(f.pc())
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
// line returns the line number of source code of the
|
||||||
|
// function for this Frame's pc.
|
||||||
|
func (f Frame) line() int {
|
||||||
|
fn := runtime.FuncForPC(f.pc())
|
||||||
|
if fn == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
_, line := fn.FileLine(f.pc())
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
|
||||||
|
// name returns the name of this function, if known.
|
||||||
|
func (f Frame) name() string {
|
||||||
|
fn := runtime.FuncForPC(f.pc())
|
||||||
|
if fn == nil {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
return fn.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format formats the frame according to the fmt.Formatter interface.
|
||||||
|
//
|
||||||
|
// %s source file
|
||||||
|
// %d source line
|
||||||
|
// %n function name
|
||||||
|
// %v equivalent to %s:%d
|
||||||
|
//
|
||||||
|
// Format accepts flags that alter the printing of some verbs, as follows:
|
||||||
|
//
|
||||||
|
// %+s function name and path of source file relative to the compile time
|
||||||
|
// GOPATH separated by \n\t (<funcname>\n\t<path>)
|
||||||
|
// %+v equivalent to %+s:%d
|
||||||
|
func (f Frame) Format(s fmt.State, verb rune) {
|
||||||
|
switch verb {
|
||||||
|
case 's':
|
||||||
|
switch {
|
||||||
|
case s.Flag('+'):
|
||||||
|
io.WriteString(s, f.name())
|
||||||
|
io.WriteString(s, "\n\t")
|
||||||
|
io.WriteString(s, f.file())
|
||||||
|
default:
|
||||||
|
io.WriteString(s, path.Base(f.file()))
|
||||||
|
}
|
||||||
|
case 'd':
|
||||||
|
io.WriteString(s, strconv.Itoa(f.line()))
|
||||||
|
case 'n':
|
||||||
|
io.WriteString(s, funcname(f.name()))
|
||||||
|
case 'v':
|
||||||
|
f.Format(s, 's')
|
||||||
|
io.WriteString(s, ":")
|
||||||
|
f.Format(s, 'd')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalText formats a stacktrace Frame as a text string. The output is the
|
||||||
|
// same as that of fmt.Sprintf("%+v", f), but without newlines or tabs.
|
||||||
|
func (f Frame) MarshalText() ([]byte, error) {
|
||||||
|
name := f.name()
|
||||||
|
if name == "unknown" {
|
||||||
|
return []byte(name), nil
|
||||||
|
}
|
||||||
|
return []byte(fmt.Sprintf("%s %s:%d", name, f.file(), f.line())), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StackTrace is stack of Frames from innermost (newest) to outermost (oldest).
|
||||||
|
type StackTrace []Frame
|
||||||
|
|
||||||
|
// Format formats the stack of Frames according to the fmt.Formatter interface.
|
||||||
|
//
|
||||||
|
// %s lists source files for each Frame in the stack
|
||||||
|
// %v lists the source file and line number for each Frame in the stack
|
||||||
|
//
|
||||||
|
// Format accepts flags that alter the printing of some verbs, as follows:
|
||||||
|
//
|
||||||
|
// %+v Prints filename, function, and line number for each Frame in the stack.
|
||||||
|
func (st StackTrace) Format(s fmt.State, verb rune) {
|
||||||
|
switch verb {
|
||||||
|
case 'v':
|
||||||
|
switch {
|
||||||
|
case s.Flag('+'):
|
||||||
|
for _, f := range st {
|
||||||
|
io.WriteString(s, "\n")
|
||||||
|
f.Format(s, verb)
|
||||||
|
}
|
||||||
|
case s.Flag('#'):
|
||||||
|
fmt.Fprintf(s, "%#v", []Frame(st))
|
||||||
|
default:
|
||||||
|
st.formatSlice(s, verb)
|
||||||
|
}
|
||||||
|
case 's':
|
||||||
|
st.formatSlice(s, verb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatSlice will format this StackTrace into the given buffer as a slice of
|
||||||
|
// Frame, only valid when called with '%s' or '%v'.
|
||||||
|
func (st StackTrace) formatSlice(s fmt.State, verb rune) {
|
||||||
|
io.WriteString(s, "[")
|
||||||
|
for i, f := range st {
|
||||||
|
if i > 0 {
|
||||||
|
io.WriteString(s, " ")
|
||||||
|
}
|
||||||
|
f.Format(s, verb)
|
||||||
|
}
|
||||||
|
io.WriteString(s, "]")
|
||||||
|
}
|
||||||
|
|
||||||
|
// stack represents a stack of program counters.
|
||||||
|
type stack []uintptr
|
||||||
|
|
||||||
|
func (s *stack) Format(st fmt.State, verb rune) {
|
||||||
|
switch verb {
|
||||||
|
case 'v':
|
||||||
|
switch {
|
||||||
|
case st.Flag('+'):
|
||||||
|
for _, pc := range *s {
|
||||||
|
f := Frame(pc)
|
||||||
|
fmt.Fprintf(st, "\n%+v", f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stack) StackTrace() StackTrace {
|
||||||
|
f := make([]Frame, len(*s))
|
||||||
|
for i := 0; i < len(f); i++ {
|
||||||
|
f[i] = Frame((*s)[i])
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func callers() *stack {
|
||||||
|
const depth = 32
|
||||||
|
var pcs [depth]uintptr
|
||||||
|
n := runtime.Callers(3, pcs[:])
|
||||||
|
var st stack = pcs[0:n]
|
||||||
|
return &st
|
||||||
|
}
|
||||||
|
|
||||||
|
// funcname removes the path prefix component of a function's name reported by func.Name().
|
||||||
|
func funcname(name string) string {
|
||||||
|
i := strings.LastIndex(name, "/")
|
||||||
|
name = name[i+1:]
|
||||||
|
i = strings.Index(name, ".")
|
||||||
|
return name[i+1:]
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
coverage:
|
||||||
|
range: 80..100
|
||||||
|
round: down
|
||||||
|
precision: 2
|
||||||
|
|
||||||
|
status:
|
||||||
|
project: # measuring the overall project coverage
|
||||||
|
default: # context, you can create multiple ones with custom titles
|
||||||
|
enabled: yes # must be yes|true to enable this status
|
||||||
|
target: 100 # specify the target coverage for each commit status
|
||||||
|
# option: "auto" (must increase from parent commit or pull request base)
|
||||||
|
# option: "X%" a static target percentage to hit
|
||||||
|
if_not_found: success # if parent is not found report status as success, error, or failure
|
||||||
|
if_ci_failed: error # if ci fails report status as success, error, or failure
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
.DS_Store
|
||||||
|
/vendor
|
||||||
|
/cover
|
||||||
|
cover.out
|
||||||
|
lint.log
|
||||||
|
|
||||||
|
# Binaries
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Profiling output
|
||||||
|
*.prof
|
|
@ -0,0 +1,27 @@
|
||||||
|
sudo: false
|
||||||
|
language: go
|
||||||
|
go_import_path: go.uber.org/atomic
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.11.x
|
||||||
|
- 1.12.x
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- go: 1.12.x
|
||||||
|
env: NO_TEST=yes LINT=yes
|
||||||
|
|
||||||
|
cache:
|
||||||
|
directories:
|
||||||
|
- vendor
|
||||||
|
|
||||||
|
install:
|
||||||
|
- make install_ci
|
||||||
|
|
||||||
|
script:
|
||||||
|
- test -n "$NO_TEST" || make test_ci
|
||||||
|
- test -n "$NO_TEST" || scripts/test-ubergo.sh
|
||||||
|
- test -z "$LINT" || make install_lint lint
|
||||||
|
|
||||||
|
after_success:
|
||||||
|
- bash <(curl -s https://codecov.io/bash)
|
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (c) 2016 Uber Technologies, Inc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
|
@ -0,0 +1,51 @@
|
||||||
|
# Many Go tools take file globs or directories as arguments instead of packages.
|
||||||
|
PACKAGE_FILES ?= *.go
|
||||||
|
|
||||||
|
# For pre go1.6
|
||||||
|
export GO15VENDOREXPERIMENT=1
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: build
|
||||||
|
build:
|
||||||
|
go build -i ./...
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: install
|
||||||
|
install:
|
||||||
|
glide --version || go get github.com/Masterminds/glide
|
||||||
|
glide install
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: test
|
||||||
|
test:
|
||||||
|
go test -cover -race ./...
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: install_ci
|
||||||
|
install_ci: install
|
||||||
|
go get github.com/wadey/gocovmerge
|
||||||
|
go get github.com/mattn/goveralls
|
||||||
|
go get golang.org/x/tools/cmd/cover
|
||||||
|
|
||||||
|
.PHONY: install_lint
|
||||||
|
install_lint:
|
||||||
|
go get golang.org/x/lint/golint
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: lint
|
||||||
|
lint:
|
||||||
|
@rm -rf lint.log
|
||||||
|
@echo "Checking formatting..."
|
||||||
|
@gofmt -d -s $(PACKAGE_FILES) 2>&1 | tee lint.log
|
||||||
|
@echo "Checking vet..."
|
||||||
|
@go vet ./... 2>&1 | tee -a lint.log;)
|
||||||
|
@echo "Checking lint..."
|
||||||
|
@golint $$(go list ./...) 2>&1 | tee -a lint.log
|
||||||
|
@echo "Checking for unresolved FIXMEs..."
|
||||||
|
@git grep -i fixme | grep -v -e vendor -e Makefile | tee -a lint.log
|
||||||
|
@[ ! -s lint.log ]
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: test_ci
|
||||||
|
test_ci: install_ci build
|
||||||
|
./scripts/cover.sh $(shell go list $(PACKAGES))
|
|
@ -0,0 +1,36 @@
|
||||||
|
# atomic [![GoDoc][doc-img]][doc] [![Build Status][ci-img]][ci] [![Coverage Status][cov-img]][cov] [![Go Report Card][reportcard-img]][reportcard]
|
||||||
|
|
||||||
|
Simple wrappers for primitive types to enforce atomic access.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
`go get -u go.uber.org/atomic`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
The standard library's `sync/atomic` is powerful, but it's easy to forget which
|
||||||
|
variables must be accessed atomically. `go.uber.org/atomic` preserves all the
|
||||||
|
functionality of the standard library, but wraps the primitive types to
|
||||||
|
provide a safer, more convenient API.
|
||||||
|
|
||||||
|
```go
|
||||||
|
var atom atomic.Uint32
|
||||||
|
atom.Store(42)
|
||||||
|
atom.Sub(2)
|
||||||
|
atom.CAS(40, 11)
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [documentation][doc] for a complete API specification.
|
||||||
|
|
||||||
|
## Development Status
|
||||||
|
Stable.
|
||||||
|
|
||||||
|
___
|
||||||
|
Released under the [MIT License](LICENSE.txt).
|
||||||
|
|
||||||
|
[doc-img]: https://godoc.org/github.com/uber-go/atomic?status.svg
|
||||||
|
[doc]: https://godoc.org/go.uber.org/atomic
|
||||||
|
[ci-img]: https://travis-ci.com/uber-go/atomic.svg?branch=master
|
||||||
|
[ci]: https://travis-ci.com/uber-go/atomic
|
||||||
|
[cov-img]: https://codecov.io/gh/uber-go/atomic/branch/master/graph/badge.svg
|
||||||
|
[cov]: https://codecov.io/gh/uber-go/atomic
|
||||||
|
[reportcard-img]: https://goreportcard.com/badge/go.uber.org/atomic
|
||||||
|
[reportcard]: https://goreportcard.com/report/go.uber.org/atomic
|
|
@ -0,0 +1,351 @@
|
||||||
|
// Copyright (c) 2016 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in
|
||||||
|
// all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
// THE SOFTWARE.
|
||||||
|
|
||||||
|
// Package atomic provides simple wrappers around numerics to enforce atomic
|
||||||
|
// access.
|
||||||
|
package atomic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Int32 is an atomic wrapper around an int32.
|
||||||
|
type Int32 struct{ v int32 }
|
||||||
|
|
||||||
|
// NewInt32 creates an Int32.
|
||||||
|
func NewInt32(i int32) *Int32 {
|
||||||
|
return &Int32{i}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load atomically loads the wrapped value.
|
||||||
|
func (i *Int32) Load() int32 {
|
||||||
|
return atomic.LoadInt32(&i.v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add atomically adds to the wrapped int32 and returns the new value.
|
||||||
|
func (i *Int32) Add(n int32) int32 {
|
||||||
|
return atomic.AddInt32(&i.v, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sub atomically subtracts from the wrapped int32 and returns the new value.
|
||||||
|
func (i *Int32) Sub(n int32) int32 {
|
||||||
|
return atomic.AddInt32(&i.v, -n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inc atomically increments the wrapped int32 and returns the new value.
|
||||||
|
func (i *Int32) Inc() int32 {
|
||||||
|
return i.Add(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dec atomically decrements the wrapped int32 and returns the new value.
|
||||||
|
func (i *Int32) Dec() int32 {
|
||||||
|
return i.Sub(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CAS is an atomic compare-and-swap.
|
||||||
|
func (i *Int32) CAS(old, new int32) bool {
|
||||||
|
return atomic.CompareAndSwapInt32(&i.v, old, new)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store atomically stores the passed value.
|
||||||
|
func (i *Int32) Store(n int32) {
|
||||||
|
atomic.StoreInt32(&i.v, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap atomically swaps the wrapped int32 and returns the old value.
|
||||||
|
func (i *Int32) Swap(n int32) int32 {
|
||||||
|
return atomic.SwapInt32(&i.v, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int64 is an atomic wrapper around an int64.
|
||||||
|
type Int64 struct{ v int64 }
|
||||||
|
|
||||||
|
// NewInt64 creates an Int64.
|
||||||
|
func NewInt64(i int64) *Int64 {
|
||||||
|
return &Int64{i}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load atomically loads the wrapped value.
|
||||||
|
func (i *Int64) Load() int64 {
|
||||||
|
return atomic.LoadInt64(&i.v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add atomically adds to the wrapped int64 and returns the new value.
|
||||||
|
func (i *Int64) Add(n int64) int64 {
|
||||||
|
return atomic.AddInt64(&i.v, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sub atomically subtracts from the wrapped int64 and returns the new value.
|
||||||
|
func (i *Int64) Sub(n int64) int64 {
|
||||||
|
return atomic.AddInt64(&i.v, -n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inc atomically increments the wrapped int64 and returns the new value.
|
||||||
|
func (i *Int64) Inc() int64 {
|
||||||
|
return i.Add(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dec atomically decrements the wrapped int64 and returns the new value.
|
||||||
|
func (i *Int64) Dec() int64 {
|
||||||
|
return i.Sub(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CAS is an atomic compare-and-swap.
|
||||||
|
func (i *Int64) CAS(old, new int64) bool {
|
||||||
|
return atomic.CompareAndSwapInt64(&i.v, old, new)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store atomically stores the passed value.
|
||||||
|
func (i *Int64) Store(n int64) {
|
||||||
|
atomic.StoreInt64(&i.v, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap atomically swaps the wrapped int64 and returns the old value.
|
||||||
|
func (i *Int64) Swap(n int64) int64 {
|
||||||
|
return atomic.SwapInt64(&i.v, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uint32 is an atomic wrapper around an uint32.
|
||||||
|
type Uint32 struct{ v uint32 }
|
||||||
|
|
||||||
|
// NewUint32 creates a Uint32.
|
||||||
|
func NewUint32(i uint32) *Uint32 {
|
||||||
|
return &Uint32{i}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load atomically loads the wrapped value.
|
||||||
|
func (i *Uint32) Load() uint32 {
|
||||||
|
return atomic.LoadUint32(&i.v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add atomically adds to the wrapped uint32 and returns the new value.
|
||||||
|
func (i *Uint32) Add(n uint32) uint32 {
|
||||||
|
return atomic.AddUint32(&i.v, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sub atomically subtracts from the wrapped uint32 and returns the new value.
|
||||||
|
func (i *Uint32) Sub(n uint32) uint32 {
|
||||||
|
return atomic.AddUint32(&i.v, ^(n - 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inc atomically increments the wrapped uint32 and returns the new value.
|
||||||
|
func (i *Uint32) Inc() uint32 {
|
||||||
|
return i.Add(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dec atomically decrements the wrapped int32 and returns the new value.
|
||||||
|
func (i *Uint32) Dec() uint32 {
|
||||||
|
return i.Sub(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CAS is an atomic compare-and-swap.
|
||||||
|
func (i *Uint32) CAS(old, new uint32) bool {
|
||||||
|
return atomic.CompareAndSwapUint32(&i.v, old, new)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store atomically stores the passed value.
|
||||||
|
func (i *Uint32) Store(n uint32) {
|
||||||
|
atomic.StoreUint32(&i.v, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap atomically swaps the wrapped uint32 and returns the old value.
|
||||||
|
func (i *Uint32) Swap(n uint32) uint32 {
|
||||||
|
return atomic.SwapUint32(&i.v, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uint64 is an atomic wrapper around a uint64.
|
||||||
|
type Uint64 struct{ v uint64 }
|
||||||
|
|
||||||
|
// NewUint64 creates a Uint64.
|
||||||
|
func NewUint64(i uint64) *Uint64 {
|
||||||
|
return &Uint64{i}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load atomically loads the wrapped value.
|
||||||
|
func (i *Uint64) Load() uint64 {
|
||||||
|
return atomic.LoadUint64(&i.v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add atomically adds to the wrapped uint64 and returns the new value.
|
||||||
|
func (i *Uint64) Add(n uint64) uint64 {
|
||||||
|
return atomic.AddUint64(&i.v, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sub atomically subtracts from the wrapped uint64 and returns the new value.
|
||||||
|
func (i *Uint64) Sub(n uint64) uint64 {
|
||||||
|
return atomic.AddUint64(&i.v, ^(n - 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inc atomically increments the wrapped uint64 and returns the new value.
|
||||||
|
func (i *Uint64) Inc() uint64 {
|
||||||
|
return i.Add(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dec atomically decrements the wrapped uint64 and returns the new value.
|
||||||
|
func (i *Uint64) Dec() uint64 {
|
||||||
|
return i.Sub(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CAS is an atomic compare-and-swap.
|
||||||
|
func (i *Uint64) CAS(old, new uint64) bool {
|
||||||
|
return atomic.CompareAndSwapUint64(&i.v, old, new)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store atomically stores the passed value.
|
||||||
|
func (i *Uint64) Store(n uint64) {
|
||||||
|
atomic.StoreUint64(&i.v, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap atomically swaps the wrapped uint64 and returns the old value.
|
||||||
|
func (i *Uint64) Swap(n uint64) uint64 {
|
||||||
|
return atomic.SwapUint64(&i.v, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bool is an atomic Boolean.
|
||||||
|
type Bool struct{ v uint32 }
|
||||||
|
|
||||||
|
// NewBool creates a Bool.
|
||||||
|
func NewBool(initial bool) *Bool {
|
||||||
|
return &Bool{boolToInt(initial)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load atomically loads the Boolean.
|
||||||
|
func (b *Bool) Load() bool {
|
||||||
|
return truthy(atomic.LoadUint32(&b.v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CAS is an atomic compare-and-swap.
|
||||||
|
func (b *Bool) CAS(old, new bool) bool {
|
||||||
|
return atomic.CompareAndSwapUint32(&b.v, boolToInt(old), boolToInt(new))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store atomically stores the passed value.
|
||||||
|
func (b *Bool) Store(new bool) {
|
||||||
|
atomic.StoreUint32(&b.v, boolToInt(new))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap sets the given value and returns the previous value.
|
||||||
|
func (b *Bool) Swap(new bool) bool {
|
||||||
|
return truthy(atomic.SwapUint32(&b.v, boolToInt(new)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle atomically negates the Boolean and returns the previous value.
|
||||||
|
func (b *Bool) Toggle() bool {
|
||||||
|
return truthy(atomic.AddUint32(&b.v, 1) - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func truthy(n uint32) bool {
|
||||||
|
return n&1 == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func boolToInt(b bool) uint32 {
|
||||||
|
if b {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float64 is an atomic wrapper around float64.
|
||||||
|
type Float64 struct {
|
||||||
|
v uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFloat64 creates a Float64.
|
||||||
|
func NewFloat64(f float64) *Float64 {
|
||||||
|
return &Float64{math.Float64bits(f)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load atomically loads the wrapped value.
|
||||||
|
func (f *Float64) Load() float64 {
|
||||||
|
return math.Float64frombits(atomic.LoadUint64(&f.v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store atomically stores the passed value.
|
||||||
|
func (f *Float64) Store(s float64) {
|
||||||
|
atomic.StoreUint64(&f.v, math.Float64bits(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add atomically adds to the wrapped float64 and returns the new value.
|
||||||
|
func (f *Float64) Add(s float64) float64 {
|
||||||
|
for {
|
||||||
|
old := f.Load()
|
||||||
|
new := old + s
|
||||||
|
if f.CAS(old, new) {
|
||||||
|
return new
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sub atomically subtracts from the wrapped float64 and returns the new value.
|
||||||
|
func (f *Float64) Sub(s float64) float64 {
|
||||||
|
return f.Add(-s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CAS is an atomic compare-and-swap.
|
||||||
|
func (f *Float64) CAS(old, new float64) bool {
|
||||||
|
return atomic.CompareAndSwapUint64(&f.v, math.Float64bits(old), math.Float64bits(new))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duration is an atomic wrapper around time.Duration
|
||||||
|
// https://godoc.org/time#Duration
|
||||||
|
type Duration struct {
|
||||||
|
v Int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDuration creates a Duration.
|
||||||
|
func NewDuration(d time.Duration) *Duration {
|
||||||
|
return &Duration{v: *NewInt64(int64(d))}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load atomically loads the wrapped value.
|
||||||
|
func (d *Duration) Load() time.Duration {
|
||||||
|
return time.Duration(d.v.Load())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store atomically stores the passed value.
|
||||||
|
func (d *Duration) Store(n time.Duration) {
|
||||||
|
d.v.Store(int64(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add atomically adds to the wrapped time.Duration and returns the new value.
|
||||||
|
func (d *Duration) Add(n time.Duration) time.Duration {
|
||||||
|
return time.Duration(d.v.Add(int64(n)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sub atomically subtracts from the wrapped time.Duration and returns the new value.
|
||||||
|
func (d *Duration) Sub(n time.Duration) time.Duration {
|
||||||
|
return time.Duration(d.v.Sub(int64(n)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Swap atomically swaps the wrapped time.Duration and returns the old value.
|
||||||
|
func (d *Duration) Swap(n time.Duration) time.Duration {
|
||||||
|
return time.Duration(d.v.Swap(int64(n)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CAS is an atomic compare-and-swap.
|
||||||
|
func (d *Duration) CAS(old, new time.Duration) bool {
|
||||||
|
return d.v.CAS(int64(old), int64(new))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value shadows the type of the same name from sync/atomic
|
||||||
|
// https://godoc.org/sync/atomic#Value
|
||||||
|
type Value struct{ atomic.Value }
|
|
@ -0,0 +1,55 @@
|
||||||
|
// Copyright (c) 2016 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in
|
||||||
|
// all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
// THE SOFTWARE.
|
||||||
|
|
||||||
|
package atomic
|
||||||
|
|
||||||
|
// Error is an atomic type-safe wrapper around Value for errors
|
||||||
|
type Error struct{ v Value }
|
||||||
|
|
||||||
|
// errorHolder is non-nil holder for error object.
|
||||||
|
// atomic.Value panics on saving nil object, so err object needs to be
|
||||||
|
// wrapped with valid object first.
|
||||||
|
type errorHolder struct{ err error }
|
||||||
|
|
||||||
|
// NewError creates new atomic error object
|
||||||
|
func NewError(err error) *Error {
|
||||||
|
e := &Error{}
|
||||||
|
if err != nil {
|
||||||
|
e.Store(err)
|
||||||
|
}
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load atomically loads the wrapped error
|
||||||
|
func (e *Error) Load() error {
|
||||||
|
v := e.v.Load()
|
||||||
|
if v == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
eh := v.(errorHolder)
|
||||||
|
return eh.err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store atomically stores error.
|
||||||
|
// NOTE: a holder object is allocated on each Store call.
|
||||||
|
func (e *Error) Store(err error) {
|
||||||
|
e.v.Store(errorHolder{err: err})
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
hash: f14d51408e3e0e4f73b34e4039484c78059cd7fc5f4996fdd73db20dc8d24f53
|
||||||
|
updated: 2016-10-27T00:10:51.16960137-07:00
|
||||||
|
imports: []
|
||||||
|
testImports:
|
||||||
|
- name: github.com/davecgh/go-spew
|
||||||
|
version: 5215b55f46b2b919f50a1df0eaa5886afe4e3b3d
|
||||||
|
subpackages:
|
||||||
|
- spew
|
||||||
|
- name: github.com/pmezard/go-difflib
|
||||||
|
version: d8ed2627bdf02c080bf22230dbb337003b7aba2d
|
||||||
|
subpackages:
|
||||||
|
- difflib
|
||||||
|
- name: github.com/stretchr/testify
|
||||||
|
version: d77da356e56a7428ad25149ca77381849a6a5232
|
||||||
|
subpackages:
|
||||||
|
- assert
|
||||||
|
- require
|
|
@ -0,0 +1,6 @@
|
||||||
|
package: go.uber.org/atomic
|
||||||
|
testImport:
|
||||||
|
- package: github.com/stretchr/testify
|
||||||
|
subpackages:
|
||||||
|
- assert
|
||||||
|
- require
|
|
@ -0,0 +1,49 @@
|
||||||
|
// Copyright (c) 2016 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in
|
||||||
|
// all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
// THE SOFTWARE.
|
||||||
|
|
||||||
|
package atomic
|
||||||
|
|
||||||
|
// String is an atomic type-safe wrapper around Value for strings.
|
||||||
|
type String struct{ v Value }
|
||||||
|
|
||||||
|
// NewString creates a String.
|
||||||
|
func NewString(str string) *String {
|
||||||
|
s := &String{}
|
||||||
|
if str != "" {
|
||||||
|
s.Store(str)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load atomically loads the wrapped string.
|
||||||
|
func (s *String) Load() string {
|
||||||
|
v := s.v.Load()
|
||||||
|
if v == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return v.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store atomically stores the passed string.
|
||||||
|
// Note: Converting the string to an interface{} to store in the Value
|
||||||
|
// requires an allocation.
|
||||||
|
func (s *String) Store(str string) {
|
||||||
|
s.v.Store(str)
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
coverage:
|
||||||
|
range: 80..100
|
||||||
|
round: down
|
||||||
|
precision: 2
|
||||||
|
|
||||||
|
status:
|
||||||
|
project: # measuring the overall project coverage
|
||||||
|
default: # context, you can create multiple ones with custom titles
|
||||||
|
enabled: yes # must be yes|true to enable this status
|
||||||
|
target: 100 # specify the target coverage for each commit status
|
||||||
|
# option: "auto" (must increase from parent commit or pull request base)
|
||||||
|
# option: "X%" a static target percentage to hit
|
||||||
|
if_not_found: success # if parent is not found report status as success, error, or failure
|
||||||
|
if_ci_failed: error # if ci fails report status as success, error, or failure
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
/vendor
|
|
@ -0,0 +1,33 @@
|
||||||
|
sudo: false
|
||||||
|
language: go
|
||||||
|
go_import_path: go.uber.org/multierr
|
||||||
|
|
||||||
|
env:
|
||||||
|
global:
|
||||||
|
- GO15VENDOREXPERIMENT=1
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.11.x
|
||||||
|
- 1.12.x
|
||||||
|
- 1.13.x
|
||||||
|
|
||||||
|
cache:
|
||||||
|
directories:
|
||||||
|
- vendor
|
||||||
|
|
||||||
|
before_install:
|
||||||
|
- go version
|
||||||
|
|
||||||
|
install:
|
||||||
|
- |
|
||||||
|
set -e
|
||||||
|
make install_ci
|
||||||
|
|
||||||
|
script:
|
||||||
|
- |
|
||||||
|
set -e
|
||||||
|
make lint
|
||||||
|
make test_ci
|
||||||
|
|
||||||
|
after_success:
|
||||||
|
- bash <(curl -s https://codecov.io/bash)
|
|
@ -0,0 +1,35 @@
|
||||||
|
Releases
|
||||||
|
========
|
||||||
|
|
||||||
|
v1.2.0 (2019-09-26)
|
||||||
|
===================
|
||||||
|
|
||||||
|
- Support extracting and matching against wrapped errors with `errors.As`
|
||||||
|
and `errors.Is`.
|
||||||
|
|
||||||
|
|
||||||
|
v1.1.0 (2017-06-30)
|
||||||
|
===================
|
||||||
|
|
||||||
|
- Added an `Errors(error) []error` function to extract the underlying list of
|
||||||
|
errors for a multierr error.
|
||||||
|
|
||||||
|
|
||||||
|
v1.0.0 (2017-05-31)
|
||||||
|
===================
|
||||||
|
|
||||||
|
No changes since v0.2.0. This release is committing to making no breaking
|
||||||
|
changes to the current API in the 1.X series.
|
||||||
|
|
||||||
|
|
||||||
|
v0.2.0 (2017-04-11)
|
||||||
|
===================
|
||||||
|
|
||||||
|
- Repeatedly appending to the same error is now faster due to fewer
|
||||||
|
allocations.
|
||||||
|
|
||||||
|
|
||||||
|
v0.1.0 (2017-31-03)
|
||||||
|
===================
|
||||||
|
|
||||||
|
- Initial release
|
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (c) 2017 Uber Technologies, Inc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
|
@ -0,0 +1,74 @@
|
||||||
|
export GO15VENDOREXPERIMENT=1
|
||||||
|
|
||||||
|
PACKAGES := $(shell glide nv)
|
||||||
|
|
||||||
|
GO_FILES := $(shell \
|
||||||
|
find . '(' -path '*/.*' -o -path './vendor' ')' -prune \
|
||||||
|
-o -name '*.go' -print | cut -b3-)
|
||||||
|
|
||||||
|
.PHONY: install
|
||||||
|
install:
|
||||||
|
glide --version || go get github.com/Masterminds/glide
|
||||||
|
glide install
|
||||||
|
|
||||||
|
.PHONY: build
|
||||||
|
build:
|
||||||
|
go build -i $(PACKAGES)
|
||||||
|
|
||||||
|
.PHONY: test
|
||||||
|
test:
|
||||||
|
go test -cover -race $(PACKAGES)
|
||||||
|
|
||||||
|
.PHONY: gofmt
|
||||||
|
gofmt:
|
||||||
|
$(eval FMT_LOG := $(shell mktemp -t gofmt.XXXXX))
|
||||||
|
@gofmt -e -s -l $(GO_FILES) > $(FMT_LOG) || true
|
||||||
|
@[ ! -s "$(FMT_LOG)" ] || (echo "gofmt failed:" | cat - $(FMT_LOG) && false)
|
||||||
|
|
||||||
|
.PHONY: govet
|
||||||
|
govet:
|
||||||
|
$(eval VET_LOG := $(shell mktemp -t govet.XXXXX))
|
||||||
|
@go vet $(PACKAGES) 2>&1 \
|
||||||
|
| grep -v '^exit status' > $(VET_LOG) || true
|
||||||
|
@[ ! -s "$(VET_LOG)" ] || (echo "govet failed:" | cat - $(VET_LOG) && false)
|
||||||
|
|
||||||
|
.PHONY: golint
|
||||||
|
golint:
|
||||||
|
@go get golang.org/x/lint/golint
|
||||||
|
$(eval LINT_LOG := $(shell mktemp -t golint.XXXXX))
|
||||||
|
@cat /dev/null > $(LINT_LOG)
|
||||||
|
@$(foreach pkg, $(PACKAGES), golint $(pkg) >> $(LINT_LOG) || true;)
|
||||||
|
@[ ! -s "$(LINT_LOG)" ] || (echo "golint failed:" | cat - $(LINT_LOG) && false)
|
||||||
|
|
||||||
|
.PHONY: staticcheck
|
||||||
|
staticcheck:
|
||||||
|
@go get honnef.co/go/tools/cmd/staticcheck
|
||||||
|
$(eval STATICCHECK_LOG := $(shell mktemp -t staticcheck.XXXXX))
|
||||||
|
@staticcheck $(PACKAGES) 2>&1 > $(STATICCHECK_LOG) || true
|
||||||
|
@[ ! -s "$(STATICCHECK_LOG)" ] || (echo "staticcheck failed:" | cat - $(STATICCHECK_LOG) && false)
|
||||||
|
|
||||||
|
.PHONY: lint
|
||||||
|
lint: gofmt govet golint staticcheck
|
||||||
|
|
||||||
|
.PHONY: cover
|
||||||
|
cover:
|
||||||
|
./scripts/cover.sh $(shell go list $(PACKAGES))
|
||||||
|
go tool cover -html=cover.out -o cover.html
|
||||||
|
|
||||||
|
update-license:
|
||||||
|
@go get go.uber.org/tools/update-license
|
||||||
|
@update-license \
|
||||||
|
$(shell go list -json $(PACKAGES) | \
|
||||||
|
jq -r '.Dir + "/" + (.GoFiles | .[])')
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
.PHONY: install_ci
|
||||||
|
install_ci: install
|
||||||
|
go get github.com/wadey/gocovmerge
|
||||||
|
go get github.com/mattn/goveralls
|
||||||
|
go get golang.org/x/tools/cmd/cover
|
||||||
|
|
||||||
|
.PHONY: test_ci
|
||||||
|
test_ci: install_ci
|
||||||
|
./scripts/cover.sh $(shell go list $(PACKAGES))
|
|
@ -0,0 +1,23 @@
|
||||||
|
# multierr [![GoDoc][doc-img]][doc] [![Build Status][ci-img]][ci] [![Coverage Status][cov-img]][cov]
|
||||||
|
|
||||||
|
`multierr` allows combining one or more Go `error`s together.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
go get -u go.uber.org/multierr
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Stable: No breaking changes will be made before 2.0.
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Released under the [MIT License].
|
||||||
|
|
||||||
|
[MIT License]: LICENSE.txt
|
||||||
|
[doc-img]: https://godoc.org/go.uber.org/multierr?status.svg
|
||||||
|
[doc]: https://godoc.org/go.uber.org/multierr
|
||||||
|
[ci-img]: https://travis-ci.com/uber-go/multierr.svg?branch=master
|
||||||
|
[cov-img]: https://codecov.io/gh/uber-go/multierr/branch/master/graph/badge.svg
|
||||||
|
[ci]: https://travis-ci.com/uber-go/multierr
|
||||||
|
[cov]: https://codecov.io/gh/uber-go/multierr
|
|
@ -0,0 +1,399 @@
|
||||||
|
// Copyright (c) 2017 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in
|
||||||
|
// all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
// THE SOFTWARE.
|
||||||
|
|
||||||
|
// Package multierr allows combining one or more errors together.
|
||||||
|
//
|
||||||
|
// Overview
|
||||||
|
//
|
||||||
|
// Errors can be combined with the use of the Combine function.
|
||||||
|
//
|
||||||
|
// multierr.Combine(
|
||||||
|
// reader.Close(),
|
||||||
|
// writer.Close(),
|
||||||
|
// conn.Close(),
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// If only two errors are being combined, the Append function may be used
|
||||||
|
// instead.
|
||||||
|
//
|
||||||
|
// err = multierr.Append(reader.Close(), writer.Close())
|
||||||
|
//
|
||||||
|
// This makes it possible to record resource cleanup failures from deferred
|
||||||
|
// blocks with the help of named return values.
|
||||||
|
//
|
||||||
|
// func sendRequest(req Request) (err error) {
|
||||||
|
// conn, err := openConnection()
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
// defer func() {
|
||||||
|
// err = multierr.Append(err, conn.Close())
|
||||||
|
// }()
|
||||||
|
// // ...
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// The underlying list of errors for a returned error object may be retrieved
|
||||||
|
// with the Errors function.
|
||||||
|
//
|
||||||
|
// errors := multierr.Errors(err)
|
||||||
|
// if len(errors) > 0 {
|
||||||
|
// fmt.Println("The following errors occurred:")
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Advanced Usage
|
||||||
|
//
|
||||||
|
// Errors returned by Combine and Append MAY implement the following
|
||||||
|
// interface.
|
||||||
|
//
|
||||||
|
// type errorGroup interface {
|
||||||
|
// // Returns a slice containing the underlying list of errors.
|
||||||
|
// //
|
||||||
|
// // This slice MUST NOT be modified by the caller.
|
||||||
|
// Errors() []error
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Note that if you need access to list of errors behind a multierr error, you
|
||||||
|
// should prefer using the Errors function. That said, if you need cheap
|
||||||
|
// read-only access to the underlying errors slice, you can attempt to cast
|
||||||
|
// the error to this interface. You MUST handle the failure case gracefully
|
||||||
|
// because errors returned by Combine and Append are not guaranteed to
|
||||||
|
// implement this interface.
|
||||||
|
//
|
||||||
|
// var errors []error
|
||||||
|
// group, ok := err.(errorGroup)
|
||||||
|
// if ok {
|
||||||
|
// errors = group.Errors()
|
||||||
|
// } else {
|
||||||
|
// errors = []error{err}
|
||||||
|
// }
|
||||||
|
package multierr // import "go.uber.org/multierr"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"go.uber.org/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Separator for single-line error messages.
|
||||||
|
_singlelineSeparator = []byte("; ")
|
||||||
|
|
||||||
|
// Prefix for multi-line messages
|
||||||
|
_multilinePrefix = []byte("the following errors occurred:")
|
||||||
|
|
||||||
|
// Prefix for the first and following lines of an item in a list of
|
||||||
|
// multi-line error messages.
|
||||||
|
//
|
||||||
|
// For example, if a single item is:
|
||||||
|
//
|
||||||
|
// foo
|
||||||
|
// bar
|
||||||
|
//
|
||||||
|
// It will become,
|
||||||
|
//
|
||||||
|
// - foo
|
||||||
|
// bar
|
||||||
|
_multilineSeparator = []byte("\n - ")
|
||||||
|
_multilineIndent = []byte(" ")
|
||||||
|
)
|
||||||
|
|
||||||
|
// _bufferPool is a pool of bytes.Buffers.
|
||||||
|
var _bufferPool = sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return &bytes.Buffer{}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type errorGroup interface {
|
||||||
|
Errors() []error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errors returns a slice containing zero or more errors that the supplied
|
||||||
|
// error is composed of. If the error is nil, the returned slice is empty.
|
||||||
|
//
|
||||||
|
// err := multierr.Append(r.Close(), w.Close())
|
||||||
|
// errors := multierr.Errors(err)
|
||||||
|
//
|
||||||
|
// If the error is not composed of other errors, the returned slice contains
|
||||||
|
// just the error that was passed in.
|
||||||
|
//
|
||||||
|
// Callers of this function are free to modify the returned slice.
|
||||||
|
func Errors(err error) []error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that we're casting to multiError, not errorGroup. Our contract is
|
||||||
|
// that returned errors MAY implement errorGroup. Errors, however, only
|
||||||
|
// has special behavior for multierr-specific error objects.
|
||||||
|
//
|
||||||
|
// This behavior can be expanded in the future but I think it's prudent to
|
||||||
|
// start with as little as possible in terms of contract and possibility
|
||||||
|
// of misuse.
|
||||||
|
eg, ok := err.(*multiError)
|
||||||
|
if !ok {
|
||||||
|
return []error{err}
|
||||||
|
}
|
||||||
|
|
||||||
|
errors := eg.Errors()
|
||||||
|
result := make([]error, len(errors))
|
||||||
|
copy(result, errors)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// multiError is an error that holds one or more errors.
|
||||||
|
//
|
||||||
|
// An instance of this is guaranteed to be non-empty and flattened. That is,
|
||||||
|
// none of the errors inside multiError are other multiErrors.
|
||||||
|
//
|
||||||
|
// multiError formats to a semi-colon delimited list of error messages with
|
||||||
|
// %v and with a more readable multi-line format with %+v.
|
||||||
|
type multiError struct {
|
||||||
|
copyNeeded atomic.Bool
|
||||||
|
errors []error
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ errorGroup = (*multiError)(nil)
|
||||||
|
|
||||||
|
// Errors returns the list of underlying errors.
|
||||||
|
//
|
||||||
|
// This slice MUST NOT be modified.
|
||||||
|
func (merr *multiError) Errors() []error {
|
||||||
|
if merr == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return merr.errors
|
||||||
|
}
|
||||||
|
|
||||||
|
func (merr *multiError) Error() string {
|
||||||
|
if merr == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
buff := _bufferPool.Get().(*bytes.Buffer)
|
||||||
|
buff.Reset()
|
||||||
|
|
||||||
|
merr.writeSingleline(buff)
|
||||||
|
|
||||||
|
result := buff.String()
|
||||||
|
_bufferPool.Put(buff)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (merr *multiError) Format(f fmt.State, c rune) {
|
||||||
|
if c == 'v' && f.Flag('+') {
|
||||||
|
merr.writeMultiline(f)
|
||||||
|
} else {
|
||||||
|
merr.writeSingleline(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (merr *multiError) writeSingleline(w io.Writer) {
|
||||||
|
first := true
|
||||||
|
for _, item := range merr.errors {
|
||||||
|
if first {
|
||||||
|
first = false
|
||||||
|
} else {
|
||||||
|
w.Write(_singlelineSeparator)
|
||||||
|
}
|
||||||
|
io.WriteString(w, item.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (merr *multiError) writeMultiline(w io.Writer) {
|
||||||
|
w.Write(_multilinePrefix)
|
||||||
|
for _, item := range merr.errors {
|
||||||
|
w.Write(_multilineSeparator)
|
||||||
|
writePrefixLine(w, _multilineIndent, fmt.Sprintf("%+v", item))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writes s to the writer with the given prefix added before each line after
|
||||||
|
// the first.
|
||||||
|
func writePrefixLine(w io.Writer, prefix []byte, s string) {
|
||||||
|
first := true
|
||||||
|
for len(s) > 0 {
|
||||||
|
if first {
|
||||||
|
first = false
|
||||||
|
} else {
|
||||||
|
w.Write(prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
idx := strings.IndexByte(s, '\n')
|
||||||
|
if idx < 0 {
|
||||||
|
idx = len(s) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
io.WriteString(w, s[:idx+1])
|
||||||
|
s = s[idx+1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type inspectResult struct {
|
||||||
|
// Number of top-level non-nil errors
|
||||||
|
Count int
|
||||||
|
|
||||||
|
// Total number of errors including multiErrors
|
||||||
|
Capacity int
|
||||||
|
|
||||||
|
// Index of the first non-nil error in the list. Value is meaningless if
|
||||||
|
// Count is zero.
|
||||||
|
FirstErrorIdx int
|
||||||
|
|
||||||
|
// Whether the list contains at least one multiError
|
||||||
|
ContainsMultiError bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inspects the given slice of errors so that we can efficiently allocate
|
||||||
|
// space for it.
|
||||||
|
func inspect(errors []error) (res inspectResult) {
|
||||||
|
first := true
|
||||||
|
for i, err := range errors {
|
||||||
|
if err == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Count++
|
||||||
|
if first {
|
||||||
|
first = false
|
||||||
|
res.FirstErrorIdx = i
|
||||||
|
}
|
||||||
|
|
||||||
|
if merr, ok := err.(*multiError); ok {
|
||||||
|
res.Capacity += len(merr.errors)
|
||||||
|
res.ContainsMultiError = true
|
||||||
|
} else {
|
||||||
|
res.Capacity++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// fromSlice converts the given list of errors into a single error.
|
||||||
|
func fromSlice(errors []error) error {
|
||||||
|
res := inspect(errors)
|
||||||
|
switch res.Count {
|
||||||
|
case 0:
|
||||||
|
return nil
|
||||||
|
case 1:
|
||||||
|
// only one non-nil entry
|
||||||
|
return errors[res.FirstErrorIdx]
|
||||||
|
case len(errors):
|
||||||
|
if !res.ContainsMultiError {
|
||||||
|
// already flat
|
||||||
|
return &multiError{errors: errors}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nonNilErrs := make([]error, 0, res.Capacity)
|
||||||
|
for _, err := range errors[res.FirstErrorIdx:] {
|
||||||
|
if err == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if nested, ok := err.(*multiError); ok {
|
||||||
|
nonNilErrs = append(nonNilErrs, nested.errors...)
|
||||||
|
} else {
|
||||||
|
nonNilErrs = append(nonNilErrs, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &multiError{errors: nonNilErrs}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine combines the passed errors into a single error.
|
||||||
|
//
|
||||||
|
// If zero arguments were passed or if all items are nil, a nil error is
|
||||||
|
// returned.
|
||||||
|
//
|
||||||
|
// Combine(nil, nil) // == nil
|
||||||
|
//
|
||||||
|
// If only a single error was passed, it is returned as-is.
|
||||||
|
//
|
||||||
|
// Combine(err) // == err
|
||||||
|
//
|
||||||
|
// Combine skips over nil arguments so this function may be used to combine
|
||||||
|
// together errors from operations that fail independently of each other.
|
||||||
|
//
|
||||||
|
// multierr.Combine(
|
||||||
|
// reader.Close(),
|
||||||
|
// writer.Close(),
|
||||||
|
// pipe.Close(),
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// If any of the passed errors is a multierr error, it will be flattened along
|
||||||
|
// with the other errors.
|
||||||
|
//
|
||||||
|
// multierr.Combine(multierr.Combine(err1, err2), err3)
|
||||||
|
// // is the same as
|
||||||
|
// multierr.Combine(err1, err2, err3)
|
||||||
|
//
|
||||||
|
// The returned error formats into a readable multi-line error message if
|
||||||
|
// formatted with %+v.
|
||||||
|
//
|
||||||
|
// fmt.Sprintf("%+v", multierr.Combine(err1, err2))
|
||||||
|
func Combine(errors ...error) error {
|
||||||
|
return fromSlice(errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append appends the given errors together. Either value may be nil.
|
||||||
|
//
|
||||||
|
// This function is a specialization of Combine for the common case where
|
||||||
|
// there are only two errors.
|
||||||
|
//
|
||||||
|
// err = multierr.Append(reader.Close(), writer.Close())
|
||||||
|
//
|
||||||
|
// The following pattern may also be used to record failure of deferred
|
||||||
|
// operations without losing information about the original error.
|
||||||
|
//
|
||||||
|
// func doSomething(..) (err error) {
|
||||||
|
// f := acquireResource()
|
||||||
|
// defer func() {
|
||||||
|
// err = multierr.Append(err, f.Close())
|
||||||
|
// }()
|
||||||
|
func Append(left error, right error) error {
|
||||||
|
switch {
|
||||||
|
case left == nil:
|
||||||
|
return right
|
||||||
|
case right == nil:
|
||||||
|
return left
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := right.(*multiError); !ok {
|
||||||
|
if l, ok := left.(*multiError); ok && !l.copyNeeded.Swap(true) {
|
||||||
|
// Common case where the error on the left is constantly being
|
||||||
|
// appended to.
|
||||||
|
errs := append(l.errors, right)
|
||||||
|
return &multiError{errors: errs}
|
||||||
|
} else if !ok {
|
||||||
|
// Both errors are single errors.
|
||||||
|
return &multiError{errors: []error{left, right}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Either right or both, left and right, are multiErrors. Rely on usual
|
||||||
|
// expensive logic.
|
||||||
|
errors := [2]error{left, right}
|
||||||
|
return fromSlice(errors[0:])
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
hash: b53b5e9a84b9cb3cc4b2d0499e23da2feca1eec318ce9bb717ecf35bf24bf221
|
||||||
|
updated: 2017-04-10T13:34:45.671678062-07:00
|
||||||
|
imports:
|
||||||
|
- name: go.uber.org/atomic
|
||||||
|
version: 3b8db5e93c4c02efbc313e17b2e796b0914a01fb
|
||||||
|
testImports:
|
||||||
|
- name: github.com/davecgh/go-spew
|
||||||
|
version: 6d212800a42e8ab5c146b8ace3490ee17e5225f9
|
||||||
|
subpackages:
|
||||||
|
- spew
|
||||||
|
- name: github.com/pmezard/go-difflib
|
||||||
|
version: d8ed2627bdf02c080bf22230dbb337003b7aba2d
|
||||||
|
subpackages:
|
||||||
|
- difflib
|
||||||
|
- name: github.com/stretchr/testify
|
||||||
|
version: 69483b4bd14f5845b5a1e55bca19e954e827f1d0
|
||||||
|
subpackages:
|
||||||
|
- assert
|
||||||
|
- require
|
|
@ -0,0 +1,8 @@
|
||||||
|
package: go.uber.org/multierr
|
||||||
|
import:
|
||||||
|
- package: go.uber.org/atomic
|
||||||
|
version: ^1
|
||||||
|
testImport:
|
||||||
|
- package: github.com/stretchr/testify
|
||||||
|
subpackages:
|
||||||
|
- assert
|
|
@ -0,0 +1,52 @@
|
||||||
|
// Copyright (c) 2019 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in
|
||||||
|
// all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
// THE SOFTWARE.
|
||||||
|
|
||||||
|
// +build go1.13
|
||||||
|
|
||||||
|
package multierr
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// As attempts to find the first error in the error list that matches the type
|
||||||
|
// of the value that target points to.
|
||||||
|
//
|
||||||
|
// This function allows errors.As to traverse the values stored on the
|
||||||
|
// multierr error.
|
||||||
|
func (merr *multiError) As(target interface{}) bool {
|
||||||
|
for _, err := range merr.Errors() {
|
||||||
|
if errors.As(err, target) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is attempts to match the provided error against errors in the error list.
|
||||||
|
//
|
||||||
|
// This function allows errors.Is to traverse the values stored on the
|
||||||
|
// multierr error.
|
||||||
|
func (merr *multiError) Is(target error) bool {
|
||||||
|
for _, err := range merr.Errors() {
|
||||||
|
if errors.Is(err, target) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
coverage:
|
||||||
|
range: 80..100
|
||||||
|
round: down
|
||||||
|
precision: 2
|
||||||
|
|
||||||
|
status:
|
||||||
|
project: # measuring the overall project coverage
|
||||||
|
default: # context, you can create multiple ones with custom titles
|
||||||
|
enabled: yes # must be yes|true to enable this status
|
||||||
|
target: 95% # specify the target coverage for each commit status
|
||||||
|
# option: "auto" (must increase from parent commit or pull request base)
|
||||||
|
# option: "X%" a static target percentage to hit
|
||||||
|
if_not_found: success # if parent is not found report status as success, error, or failure
|
||||||
|
if_ci_failed: error # if ci fails report status as success, error, or failure
|
||||||
|
ignore:
|
||||||
|
- internal/readme/readme.go
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Folders
|
||||||
|
_obj
|
||||||
|
_test
|
||||||
|
vendor
|
||||||
|
|
||||||
|
# Architecture specific extensions/prefixes
|
||||||
|
*.[568vq]
|
||||||
|
[568vq].out
|
||||||
|
|
||||||
|
*.cgo1.go
|
||||||
|
*.cgo2.c
|
||||||
|
_cgo_defun.c
|
||||||
|
_cgo_gotypes.go
|
||||||
|
_cgo_export.*
|
||||||
|
|
||||||
|
_testmain.go
|
||||||
|
|
||||||
|
*.exe
|
||||||
|
*.test
|
||||||
|
*.prof
|
||||||
|
*.pprof
|
||||||
|
*.out
|
||||||
|
*.log
|
|
@ -0,0 +1,108 @@
|
||||||
|
# :zap: zap [![GoDoc][doc-img]][doc] [![Build Status][ci-img]][ci] [![Coverage Status][cov-img]][cov]
|
||||||
|
|
||||||
|
Blazing fast, structured, leveled logging in Go.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
`go get -u go.uber.org/zap`
|
||||||
|
|
||||||
|
Note that zap only supports the two most recent minor versions of Go.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
In contexts where performance is nice, but not critical, use the
|
||||||
|
`SugaredLogger`. It's 4-10x faster than other structured logging
|
||||||
|
packages and includes both structured and `printf`-style APIs.
|
||||||
|
|
||||||
|
```go
|
||||||
|
logger, _ := zap.NewProduction()
|
||||||
|
defer logger.Sync() // flushes buffer, if any
|
||||||
|
sugar := logger.Sugar()
|
||||||
|
sugar.Infow("failed to fetch URL",
|
||||||
|
// Structured context as loosely typed key-value pairs.
|
||||||
|
"url", url,
|
||||||
|
"attempt", 3,
|
||||||
|
"backoff", time.Second,
|
||||||
|
)
|
||||||
|
sugar.Infof("Failed to fetch URL: %s", url)
|
||||||
|
```
|
||||||
|
|
||||||
|
When performance and type safety are critical, use the `Logger`. It's even
|
||||||
|
faster than the `SugaredLogger` and allocates far less, but it only supports
|
||||||
|
structured logging.
|
||||||
|
|
||||||
|
```go
|
||||||
|
logger, _ := zap.NewProduction()
|
||||||
|
defer logger.Sync()
|
||||||
|
logger.Info("failed to fetch URL",
|
||||||
|
// Structured context as strongly typed Field values.
|
||||||
|
zap.String("url", url),
|
||||||
|
zap.Int("attempt", 3),
|
||||||
|
zap.Duration("backoff", time.Second),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [documentation][doc] and [FAQ](FAQ.md) for more details.
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
For applications that log in the hot path, reflection-based serialization and
|
||||||
|
string formatting are prohibitively expensive — they're CPU-intensive
|
||||||
|
and make many small allocations. Put differently, using `encoding/json` and
|
||||||
|
`fmt.Fprintf` to log tons of `interface{}`s makes your application slow.
|
||||||
|
|
||||||
|
Zap takes a different approach. It includes a reflection-free, zero-allocation
|
||||||
|
JSON encoder, and the base `Logger` strives to avoid serialization overhead
|
||||||
|
and allocations wherever possible. By building the high-level `SugaredLogger`
|
||||||
|
on that foundation, zap lets users *choose* when they need to count every
|
||||||
|
allocation and when they'd prefer a more familiar, loosely typed API.
|
||||||
|
|
||||||
|
As measured by its own [benchmarking suite][], not only is zap more performant
|
||||||
|
than comparable structured logging packages — it's also faster than the
|
||||||
|
standard library. Like all benchmarks, take these with a grain of salt.<sup
|
||||||
|
id="anchor-versions">[1](#footnote-versions)</sup>
|
||||||
|
|
||||||
|
Log a message and 10 fields:
|
||||||
|
|
||||||
|
{{.BenchmarkAddingFields}}
|
||||||
|
|
||||||
|
Log a message with a logger that already has 10 fields of context:
|
||||||
|
|
||||||
|
{{.BenchmarkAccumulatedContext}}
|
||||||
|
|
||||||
|
Log a static string, without any context or `printf`-style templating:
|
||||||
|
|
||||||
|
{{.BenchmarkWithoutFields}}
|
||||||
|
|
||||||
|
## Development Status: Stable
|
||||||
|
|
||||||
|
All APIs are finalized, and no breaking changes will be made in the 1.x series
|
||||||
|
of releases. Users of semver-aware dependency management systems should pin
|
||||||
|
zap to `^1`.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
We encourage and support an active, healthy community of contributors —
|
||||||
|
including you! Details are in the [contribution guide](CONTRIBUTING.md) and
|
||||||
|
the [code of conduct](CODE_OF_CONDUCT.md). The zap maintainers keep an eye on
|
||||||
|
issues and pull requests, but you can also report any negative conduct to
|
||||||
|
[email protected]. That email list is a private, safe space; even the zap
|
||||||
|
maintainers don't have access, so don't hesitate to hold us to a high
|
||||||
|
standard.
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
Released under the [MIT License](LICENSE.txt).
|
||||||
|
|
||||||
|
<sup id="footnote-versions">1</sup> In particular, keep in mind that we may be
|
||||||
|
benchmarking against slightly older versions of other packages. Versions are
|
||||||
|
pinned in zap's [glide.lock][] file. [↩](#anchor-versions)
|
||||||
|
|
||||||
|
[doc-img]: https://godoc.org/go.uber.org/zap?status.svg
|
||||||
|
[doc]: https://godoc.org/go.uber.org/zap
|
||||||
|
[ci-img]: https://travis-ci.org/uber-go/zap.svg?branch=master
|
||||||
|
[ci]: https://travis-ci.org/uber-go/zap
|
||||||
|
[cov-img]: https://codecov.io/gh/uber-go/zap/branch/master/graph/badge.svg
|
||||||
|
[cov]: https://codecov.io/gh/uber-go/zap
|
||||||
|
[benchmarking suite]: https://github.com/uber-go/zap/tree/master/benchmarks
|
||||||
|
[glide.lock]: https://github.com/uber-go/zap/blob/master/glide.lock
|
|
@ -0,0 +1,21 @@
|
||||||
|
language: go
|
||||||
|
sudo: false
|
||||||
|
go:
|
||||||
|
- 1.11.x
|
||||||
|
- 1.12.x
|
||||||
|
go_import_path: go.uber.org/zap
|
||||||
|
env:
|
||||||
|
global:
|
||||||
|
- TEST_TIMEOUT_SCALE=10
|
||||||
|
cache:
|
||||||
|
directories:
|
||||||
|
- vendor
|
||||||
|
install:
|
||||||
|
- make dependencies
|
||||||
|
script:
|
||||||
|
- make lint
|
||||||
|
- make test
|
||||||
|
- make bench
|
||||||
|
after_success:
|
||||||
|
- make cover
|
||||||
|
- bash <(curl -s https://codecov.io/bash)
|
|
@ -0,0 +1,327 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 1.10.0 (29 Apr 2019)
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
* [#657][]: Fix `MapObjectEncoder.AppendByteString` not adding value as a
|
||||||
|
string.
|
||||||
|
* [#706][]: Fix incorrect call depth to determine caller in Go 1.12.
|
||||||
|
|
||||||
|
Enhancements:
|
||||||
|
* [#610][]: Add `zaptest.WrapOptions` to wrap `zap.Option` for creating test
|
||||||
|
loggers.
|
||||||
|
* [#675][]: Don't panic when encoding a String field.
|
||||||
|
* [#704][]: Disable HTML escaping for JSON objects encoded using the
|
||||||
|
reflect-based encoder.
|
||||||
|
|
||||||
|
Thanks to @iaroslav-ciupin, @lelenanam, @joa, @NWilson for their contributions
|
||||||
|
to this release.
|
||||||
|
|
||||||
|
## v1.9.1 (06 Aug 2018)
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
|
||||||
|
* [#614][]: MapObjectEncoder should not ignore empty slices.
|
||||||
|
|
||||||
|
## v1.9.0 (19 Jul 2018)
|
||||||
|
|
||||||
|
Enhancements:
|
||||||
|
* [#602][]: Reduce number of allocations when logging with reflection.
|
||||||
|
* [#572][], [#606][]: Expose a registry for third-party logging sinks.
|
||||||
|
|
||||||
|
Thanks to @nfarah86, @AlekSi, @JeanMertz, @philippgille, @etsangsplk, and
|
||||||
|
@dimroc for their contributions to this release.
|
||||||
|
|
||||||
|
## v1.8.0 (13 Apr 2018)
|
||||||
|
|
||||||
|
Enhancements:
|
||||||
|
* [#508][]: Make log level configurable when redirecting the standard
|
||||||
|
library's logger.
|
||||||
|
* [#518][]: Add a logger that writes to a `*testing.TB`.
|
||||||
|
* [#577][]: Add a top-level alias for `zapcore.Field` to clean up GoDoc.
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
* [#574][]: Add a missing import comment to `go.uber.org/zap/buffer`.
|
||||||
|
|
||||||
|
Thanks to @DiSiqueira and @djui for their contributions to this release.
|
||||||
|
|
||||||
|
## v1.7.1 (25 Sep 2017)
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
* [#504][]: Store strings when using AddByteString with the map encoder.
|
||||||
|
|
||||||
|
## v1.7.0 (21 Sep 2017)
|
||||||
|
|
||||||
|
Enhancements:
|
||||||
|
|
||||||
|
* [#487][]: Add `NewStdLogAt`, which extends `NewStdLog` by allowing the user
|
||||||
|
to specify the level of the logged messages.
|
||||||
|
|
||||||
|
## v1.6.0 (30 Aug 2017)
|
||||||
|
|
||||||
|
Enhancements:
|
||||||
|
|
||||||
|
* [#491][]: Omit zap stack frames from stacktraces.
|
||||||
|
* [#490][]: Add a `ContextMap` method to observer logs for simpler
|
||||||
|
field validation in tests.
|
||||||
|
|
||||||
|
## v1.5.0 (22 Jul 2017)
|
||||||
|
|
||||||
|
Enhancements:
|
||||||
|
|
||||||
|
* [#460][] and [#470][]: Support errors produced by `go.uber.org/multierr`.
|
||||||
|
* [#465][]: Support user-supplied encoders for logger names.
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
|
||||||
|
* [#477][]: Fix a bug that incorrectly truncated deep stacktraces.
|
||||||
|
|
||||||
|
Thanks to @richard-tunein and @pavius for their contributions to this release.
|
||||||
|
|
||||||
|
## v1.4.1 (08 Jun 2017)
|
||||||
|
|
||||||
|
This release fixes two bugs.
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
|
||||||
|
* [#435][]: Support a variety of case conventions when unmarshaling levels.
|
||||||
|
* [#444][]: Fix a panic in the observer.
|
||||||
|
|
||||||
|
## v1.4.0 (12 May 2017)
|
||||||
|
|
||||||
|
This release adds a few small features and is fully backward-compatible.
|
||||||
|
|
||||||
|
Enhancements:
|
||||||
|
|
||||||
|
* [#424][]: Add a `LineEnding` field to `EncoderConfig`, allowing users to
|
||||||
|
override the Unix-style default.
|
||||||
|
* [#425][]: Preserve time zones when logging times.
|
||||||
|
* [#431][]: Make `zap.AtomicLevel` implement `fmt.Stringer`, which makes a
|
||||||
|
variety of operations a bit simpler.
|
||||||
|
|
||||||
|
## v1.3.0 (25 Apr 2017)
|
||||||
|
|
||||||
|
This release adds an enhancement to zap's testing helpers as well as the
|
||||||
|
ability to marshal an AtomicLevel. It is fully backward-compatible.
|
||||||
|
|
||||||
|
Enhancements:
|
||||||
|
|
||||||
|
* [#415][]: Add a substring-filtering helper to zap's observer. This is
|
||||||
|
particularly useful when testing the `SugaredLogger`.
|
||||||
|
* [#416][]: Make `AtomicLevel` implement `encoding.TextMarshaler`.
|
||||||
|
|
||||||
|
## v1.2.0 (13 Apr 2017)
|
||||||
|
|
||||||
|
This release adds a gRPC compatibility wrapper. It is fully backward-compatible.
|
||||||
|
|
||||||
|
Enhancements:
|
||||||
|
|
||||||
|
* [#402][]: Add a `zapgrpc` package that wraps zap's Logger and implements
|
||||||
|
`grpclog.Logger`.
|
||||||
|
|
||||||
|
## v1.1.0 (31 Mar 2017)
|
||||||
|
|
||||||
|
This release fixes two bugs and adds some enhancements to zap's testing helpers.
|
||||||
|
It is fully backward-compatible.
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
|
||||||
|
* [#385][]: Fix caller path trimming on Windows.
|
||||||
|
* [#396][]: Fix a panic when attempting to use non-existent directories with
|
||||||
|
zap's configuration struct.
|
||||||
|
|
||||||
|
Enhancements:
|
||||||
|
|
||||||
|
* [#386][]: Add filtering helpers to zaptest's observing logger.
|
||||||
|
|
||||||
|
Thanks to @moitias for contributing to this release.
|
||||||
|
|
||||||
|
## v1.0.0 (14 Mar 2017)
|
||||||
|
|
||||||
|
This is zap's first stable release. All exported APIs are now final, and no
|
||||||
|
further breaking changes will be made in the 1.x release series. Anyone using a
|
||||||
|
semver-aware dependency manager should now pin to `^1`.
|
||||||
|
|
||||||
|
Breaking changes:
|
||||||
|
|
||||||
|
* [#366][]: Add byte-oriented APIs to encoders to log UTF-8 encoded text without
|
||||||
|
casting from `[]byte` to `string`.
|
||||||
|
* [#364][]: To support buffering outputs, add `Sync` methods to `zapcore.Core`,
|
||||||
|
`zap.Logger`, and `zap.SugaredLogger`.
|
||||||
|
* [#371][]: Rename the `testutils` package to `zaptest`, which is less likely to
|
||||||
|
clash with other testing helpers.
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
|
||||||
|
* [#362][]: Make the ISO8601 time formatters fixed-width, which is friendlier
|
||||||
|
for tab-separated console output.
|
||||||
|
* [#369][]: Remove the automatic locks in `zapcore.NewCore`, which allows zap to
|
||||||
|
work with concurrency-safe `WriteSyncer` implementations.
|
||||||
|
* [#347][]: Stop reporting errors when trying to `fsync` standard out on Linux
|
||||||
|
systems.
|
||||||
|
* [#373][]: Report the correct caller from zap's standard library
|
||||||
|
interoperability wrappers.
|
||||||
|
|
||||||
|
Enhancements:
|
||||||
|
|
||||||
|
* [#348][]: Add a registry allowing third-party encodings to work with zap's
|
||||||
|
built-in `Config`.
|
||||||
|
* [#327][]: Make the representation of logger callers configurable (like times,
|
||||||
|
levels, and durations).
|
||||||
|
* [#376][]: Allow third-party encoders to use their own buffer pools, which
|
||||||
|
removes the last performance advantage that zap's encoders have over plugins.
|
||||||
|
* [#346][]: Add `CombineWriteSyncers`, a convenience function to tee multiple
|
||||||
|
`WriteSyncer`s and lock the result.
|
||||||
|
* [#365][]: Make zap's stacktraces compatible with mid-stack inlining (coming in
|
||||||
|
Go 1.9).
|
||||||
|
* [#372][]: Export zap's observing logger as `zaptest/observer`. This makes it
|
||||||
|
easier for particularly punctilious users to unit test their application's
|
||||||
|
logging.
|
||||||
|
|
||||||
|
Thanks to @suyash, @htrendev, @flisky, @Ulexus, and @skipor for their
|
||||||
|
contributions to this release.
|
||||||
|
|
||||||
|
## v1.0.0-rc.3 (7 Mar 2017)
|
||||||
|
|
||||||
|
This is the third release candidate for zap's stable release. There are no
|
||||||
|
breaking changes.
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
|
||||||
|
* [#339][]: Byte slices passed to `zap.Any` are now correctly treated as binary blobs
|
||||||
|
rather than `[]uint8`.
|
||||||
|
|
||||||
|
Enhancements:
|
||||||
|
|
||||||
|
* [#307][]: Users can opt into colored output for log levels.
|
||||||
|
* [#353][]: In addition to hijacking the output of the standard library's
|
||||||
|
package-global logging functions, users can now construct a zap-backed
|
||||||
|
`log.Logger` instance.
|
||||||
|
* [#311][]: Frames from common runtime functions and some of zap's internal
|
||||||
|
machinery are now omitted from stacktraces.
|
||||||
|
|
||||||
|
Thanks to @ansel1 and @suyash for their contributions to this release.
|
||||||
|
|
||||||
|
## v1.0.0-rc.2 (21 Feb 2017)
|
||||||
|
|
||||||
|
This is the second release candidate for zap's stable release. It includes two
|
||||||
|
breaking changes.
|
||||||
|
|
||||||
|
Breaking changes:
|
||||||
|
|
||||||
|
* [#316][]: Zap's global loggers are now fully concurrency-safe
|
||||||
|
(previously, users had to ensure that `ReplaceGlobals` was called before the
|
||||||
|
loggers were in use). However, they must now be accessed via the `L()` and
|
||||||
|
`S()` functions. Users can update their projects with
|
||||||
|
|
||||||
|
```
|
||||||
|
gofmt -r "zap.L -> zap.L()" -w .
|
||||||
|
gofmt -r "zap.S -> zap.S()" -w .
|
||||||
|
```
|
||||||
|
* [#309][] and [#317][]: RC1 was mistakenly shipped with invalid
|
||||||
|
JSON and YAML struct tags on all config structs. This release fixes the tags
|
||||||
|
and adds static analysis to prevent similar bugs in the future.
|
||||||
|
|
||||||
|
Bugfixes:
|
||||||
|
|
||||||
|
* [#321][]: Redirecting the standard library's `log` output now
|
||||||
|
correctly reports the logger's caller.
|
||||||
|
|
||||||
|
Enhancements:
|
||||||
|
|
||||||
|
* [#325][] and [#333][]: Zap now transparently supports non-standard, rich
|
||||||
|
errors like those produced by `github.com/pkg/errors`.
|
||||||
|
* [#326][]: Though `New(nil)` continues to return a no-op logger, `NewNop()` is
|
||||||
|
now preferred. Users can update their projects with `gofmt -r 'zap.New(nil) ->
|
||||||
|
zap.NewNop()' -w .`.
|
||||||
|
* [#300][]: Incorrectly importing zap as `github.com/uber-go/zap` now returns a
|
||||||
|
more informative error.
|
||||||
|
|
||||||
|
Thanks to @skipor and @chapsuk for their contributions to this release.
|
||||||
|
|
||||||
|
## v1.0.0-rc.1 (14 Feb 2017)
|
||||||
|
|
||||||
|
This is the first release candidate for zap's stable release. There are multiple
|
||||||
|
breaking changes and improvements from the pre-release version. Most notably:
|
||||||
|
|
||||||
|
* **Zap's import path is now "go.uber.org/zap"** — all users will
|
||||||
|
need to update their code.
|
||||||
|
* User-facing types and functions remain in the `zap` package. Code relevant
|
||||||
|
largely to extension authors is now in the `zapcore` package.
|
||||||
|
* The `zapcore.Core` type makes it easy for third-party packages to use zap's
|
||||||
|
internals but provide a different user-facing API.
|
||||||
|
* `Logger` is now a concrete type instead of an interface.
|
||||||
|
* A less verbose (though slower) logging API is included by default.
|
||||||
|
* Package-global loggers `L` and `S` are included.
|
||||||
|
* A human-friendly console encoder is included.
|
||||||
|
* A declarative config struct allows common logger configurations to be managed
|
||||||
|
as configuration instead of code.
|
||||||
|
* Sampling is more accurate, and doesn't depend on the standard library's shared
|
||||||
|
timer heap.
|
||||||
|
|
||||||
|
## v0.1.0-beta.1 (6 Feb 2017)
|
||||||
|
|
||||||
|
This is a minor version, tagged to allow users to pin to the pre-1.0 APIs and
|
||||||
|
upgrade at their leisure. Since this is the first tagged release, there are no
|
||||||
|
backward compatibility concerns and all functionality is new.
|
||||||
|
|
||||||
|
Early zap adopters should pin to the 0.1.x minor version until they're ready to
|
||||||
|
upgrade to the upcoming stable release.
|
||||||
|
|
||||||
|
[#316]: https://github.com/uber-go/zap/pull/316
|
||||||
|
[#309]: https://github.com/uber-go/zap/pull/309
|
||||||
|
[#317]: https://github.com/uber-go/zap/pull/317
|
||||||
|
[#321]: https://github.com/uber-go/zap/pull/321
|
||||||
|
[#325]: https://github.com/uber-go/zap/pull/325
|
||||||
|
[#333]: https://github.com/uber-go/zap/pull/333
|
||||||
|
[#326]: https://github.com/uber-go/zap/pull/326
|
||||||
|
[#300]: https://github.com/uber-go/zap/pull/300
|
||||||
|
[#339]: https://github.com/uber-go/zap/pull/339
|
||||||
|
[#307]: https://github.com/uber-go/zap/pull/307
|
||||||
|
[#353]: https://github.com/uber-go/zap/pull/353
|
||||||
|
[#311]: https://github.com/uber-go/zap/pull/311
|
||||||
|
[#366]: https://github.com/uber-go/zap/pull/366
|
||||||
|
[#364]: https://github.com/uber-go/zap/pull/364
|
||||||
|
[#371]: https://github.com/uber-go/zap/pull/371
|
||||||
|
[#362]: https://github.com/uber-go/zap/pull/362
|
||||||
|
[#369]: https://github.com/uber-go/zap/pull/369
|
||||||
|
[#347]: https://github.com/uber-go/zap/pull/347
|
||||||
|
[#373]: https://github.com/uber-go/zap/pull/373
|
||||||
|
[#348]: https://github.com/uber-go/zap/pull/348
|
||||||
|
[#327]: https://github.com/uber-go/zap/pull/327
|
||||||
|
[#376]: https://github.com/uber-go/zap/pull/376
|
||||||
|
[#346]: https://github.com/uber-go/zap/pull/346
|
||||||
|
[#365]: https://github.com/uber-go/zap/pull/365
|
||||||
|
[#372]: https://github.com/uber-go/zap/pull/372
|
||||||
|
[#385]: https://github.com/uber-go/zap/pull/385
|
||||||
|
[#396]: https://github.com/uber-go/zap/pull/396
|
||||||
|
[#386]: https://github.com/uber-go/zap/pull/386
|
||||||
|
[#402]: https://github.com/uber-go/zap/pull/402
|
||||||
|
[#415]: https://github.com/uber-go/zap/pull/415
|
||||||
|
[#416]: https://github.com/uber-go/zap/pull/416
|
||||||
|
[#424]: https://github.com/uber-go/zap/pull/424
|
||||||
|
[#425]: https://github.com/uber-go/zap/pull/425
|
||||||
|
[#431]: https://github.com/uber-go/zap/pull/431
|
||||||
|
[#435]: https://github.com/uber-go/zap/pull/435
|
||||||
|
[#444]: https://github.com/uber-go/zap/pull/444
|
||||||
|
[#477]: https://github.com/uber-go/zap/pull/477
|
||||||
|
[#465]: https://github.com/uber-go/zap/pull/465
|
||||||
|
[#460]: https://github.com/uber-go/zap/pull/460
|
||||||
|
[#470]: https://github.com/uber-go/zap/pull/470
|
||||||
|
[#487]: https://github.com/uber-go/zap/pull/487
|
||||||
|
[#490]: https://github.com/uber-go/zap/pull/490
|
||||||
|
[#491]: https://github.com/uber-go/zap/pull/491
|
||||||
|
[#504]: https://github.com/uber-go/zap/pull/504
|
||||||
|
[#508]: https://github.com/uber-go/zap/pull/508
|
||||||
|
[#518]: https://github.com/uber-go/zap/pull/518
|
||||||
|
[#577]: https://github.com/uber-go/zap/pull/577
|
||||||
|
[#574]: https://github.com/uber-go/zap/pull/574
|
||||||
|
[#602]: https://github.com/uber-go/zap/pull/602
|
||||||
|
[#572]: https://github.com/uber-go/zap/pull/572
|
||||||
|
[#606]: https://github.com/uber-go/zap/pull/606
|
||||||
|
[#614]: https://github.com/uber-go/zap/pull/614
|
||||||
|
[#657]: https://github.com/uber-go/zap/pull/657
|
||||||
|
[#706]: https://github.com/uber-go/zap/pull/706
|
||||||
|
[#610]: https://github.com/uber-go/zap/pull/610
|
||||||
|
[#675]: https://github.com/uber-go/zap/pull/675
|
||||||
|
[#704]: https://github.com/uber-go/zap/pull/704
|
|
@ -0,0 +1,75 @@
|
||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
In the interest of fostering an open and welcoming environment, we as
|
||||||
|
contributors and maintainers pledge to making participation in our project and
|
||||||
|
our community a harassment-free experience for everyone, regardless of age,
|
||||||
|
body size, disability, ethnicity, gender identity and expression, level of
|
||||||
|
experience, nationality, personal appearance, race, religion, or sexual
|
||||||
|
identity and orientation.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to creating a positive environment
|
||||||
|
include:
|
||||||
|
|
||||||
|
* Using welcoming and inclusive language
|
||||||
|
* Being respectful of differing viewpoints and experiences
|
||||||
|
* Gracefully accepting constructive criticism
|
||||||
|
* Focusing on what is best for the community
|
||||||
|
* Showing empathy towards other community members
|
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||||
|
advances
|
||||||
|
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or electronic
|
||||||
|
address, without explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Our Responsibilities
|
||||||
|
|
||||||
|
Project maintainers are responsible for clarifying the standards of acceptable
|
||||||
|
behavior and are expected to take appropriate and fair corrective action in
|
||||||
|
response to any instances of unacceptable behavior.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or
|
||||||
|
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||||
|
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||||
|
permanently any contributor for other behaviors that they deem inappropriate,
|
||||||
|
threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies both within project spaces and in public spaces
|
||||||
|
when an individual is representing the project or its community. Examples of
|
||||||
|
representing a project or community include using an official project e-mail
|
||||||
|
address, posting via an official social media account, or acting as an
|
||||||
|
appointed representative at an online or offline event. Representation of a
|
||||||
|
project may be further defined and clarified by project maintainers.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported by contacting the project team at [email protected]. The project
|
||||||
|
team will review and investigate all complaints, and will respond in a way
|
||||||
|
that it deems appropriate to the circumstances. The project team is obligated
|
||||||
|
to maintain confidentiality with regard to the reporter of an incident.
|
||||||
|
Further details of specific enforcement policies may be posted separately.
|
||||||
|
|
||||||
|
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||||
|
faith may face temporary or permanent repercussions as determined by other
|
||||||
|
members of the project's leadership.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 1.4, available at
|
||||||
|
[http://contributor-covenant.org/version/1/4][version].
|
||||||
|
|
||||||
|
[homepage]: http://contributor-covenant.org
|
||||||
|
[version]: http://contributor-covenant.org/version/1/4/
|
|
@ -0,0 +1,81 @@
|
||||||
|
# Contributing
|
||||||
|
|
||||||
|
We'd love your help making zap the very best structured logging library in Go!
|
||||||
|
|
||||||
|
If you'd like to add new exported APIs, please [open an issue][open-issue]
|
||||||
|
describing your proposal — discussing API changes ahead of time makes
|
||||||
|
pull request review much smoother. In your issue, pull request, and any other
|
||||||
|
communications, please remember to treat your fellow contributors with
|
||||||
|
respect! We take our [code of conduct](CODE_OF_CONDUCT.md) seriously.
|
||||||
|
|
||||||
|
Note that you'll need to sign [Uber's Contributor License Agreement][cla]
|
||||||
|
before we can accept any of your contributions. If necessary, a bot will remind
|
||||||
|
you to accept the CLA when you open your pull request.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
[Fork][fork], then clone the repository:
|
||||||
|
|
||||||
|
```
|
||||||
|
mkdir -p $GOPATH/src/go.uber.org
|
||||||
|
cd $GOPATH/src/go.uber.org
|
||||||
|
git clone [email protected]:your_github_username/zap.git
|
||||||
|
cd zap
|
||||||
|
git remote add upstream https://github.com/uber-go/zap.git
|
||||||
|
git fetch upstream
|
||||||
|
```
|
||||||
|
|
||||||
|
Install zap's dependencies:
|
||||||
|
|
||||||
|
```
|
||||||
|
make dependencies
|
||||||
|
```
|
||||||
|
|
||||||
|
Make sure that the tests and the linters pass:
|
||||||
|
|
||||||
|
```
|
||||||
|
make test
|
||||||
|
make lint
|
||||||
|
```
|
||||||
|
|
||||||
|
If you're not using the minor version of Go specified in the Makefile's
|
||||||
|
`LINTABLE_MINOR_VERSIONS` variable, `make lint` doesn't do anything. This is
|
||||||
|
fine, but it means that you'll only discover lint failures after you open your
|
||||||
|
pull request.
|
||||||
|
|
||||||
|
## Making Changes
|
||||||
|
|
||||||
|
Start by creating a new branch for your changes:
|
||||||
|
|
||||||
|
```
|
||||||
|
cd $GOPATH/src/go.uber.org/zap
|
||||||
|
git checkout master
|
||||||
|
git fetch upstream
|
||||||
|
git rebase upstream/master
|
||||||
|
git checkout -b cool_new_feature
|
||||||
|
```
|
||||||
|
|
||||||
|
Make your changes, then ensure that `make lint` and `make test` still pass. If
|
||||||
|
you're satisfied with your changes, push them to your fork.
|
||||||
|
|
||||||
|
```
|
||||||
|
git push origin cool_new_feature
|
||||||
|
```
|
||||||
|
|
||||||
|
Then use the GitHub UI to open a pull request.
|
||||||
|
|
||||||
|
At this point, you're waiting on us to review your changes. We *try* to respond
|
||||||
|
to issues and pull requests within a few business days, and we may suggest some
|
||||||
|
improvements or alternatives. Once your changes are approved, one of the
|
||||||
|
project maintainers will merge them.
|
||||||
|
|
||||||
|
We're much more likely to approve your changes if you:
|
||||||
|
|
||||||
|
* Add tests for new functionality.
|
||||||
|
* Write a [good commit message][commit-message].
|
||||||
|
* Maintain backward compatibility.
|
||||||
|
|
||||||
|
[fork]: https://github.com/uber-go/zap/fork
|
||||||
|
[open-issue]: https://github.com/uber-go/zap/issues/new
|
||||||
|
[cla]: https://cla-assistant.io/uber-go/zap
|
||||||
|
[commit-message]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
|
|
@ -0,0 +1,155 @@
|
||||||
|
# Frequently Asked Questions
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
### Why spend so much effort on logger performance?
|
||||||
|
|
||||||
|
Of course, most applications won't notice the impact of a slow logger: they
|
||||||
|
already take tens or hundreds of milliseconds for each operation, so an extra
|
||||||
|
millisecond doesn't matter.
|
||||||
|
|
||||||
|
On the other hand, why *not* make structured logging fast? The `SugaredLogger`
|
||||||
|
isn't any harder to use than other logging packages, and the `Logger` makes
|
||||||
|
structured logging possible in performance-sensitive contexts. Across a fleet
|
||||||
|
of Go microservices, making each application even slightly more efficient adds
|
||||||
|
up quickly.
|
||||||
|
|
||||||
|
### Why aren't `Logger` and `SugaredLogger` interfaces?
|
||||||
|
|
||||||
|
Unlike the familiar `io.Writer` and `http.Handler`, `Logger` and
|
||||||
|
`SugaredLogger` interfaces would include *many* methods. As [Rob Pike points
|
||||||
|
out][go-proverbs], "The bigger the interface, the weaker the abstraction."
|
||||||
|
Interfaces are also rigid — *any* change requires releasing a new major
|
||||||
|
version, since it breaks all third-party implementations.
|
||||||
|
|
||||||
|
Making the `Logger` and `SugaredLogger` concrete types doesn't sacrifice much
|
||||||
|
abstraction, and it lets us add methods without introducing breaking changes.
|
||||||
|
Your applications should define and depend upon an interface that includes
|
||||||
|
just the methods you use.
|
||||||
|
|
||||||
|
### Why sample application logs?
|
||||||
|
|
||||||
|
Applications often experience runs of errors, either because of a bug or
|
||||||
|
because of a misbehaving user. Logging errors is usually a good idea, but it
|
||||||
|
can easily make this bad situation worse: not only is your application coping
|
||||||
|
with a flood of errors, it's also spending extra CPU cycles and I/O logging
|
||||||
|
those errors. Since writes are typically serialized, logging limits throughput
|
||||||
|
when you need it most.
|
||||||
|
|
||||||
|
Sampling fixes this problem by dropping repetitive log entries. Under normal
|
||||||
|
conditions, your application writes out every entry. When similar entries are
|
||||||
|
logged hundreds or thousands of times each second, though, zap begins dropping
|
||||||
|
duplicates to preserve throughput.
|
||||||
|
|
||||||
|
### Why do the structured logging APIs take a message in addition to fields?
|
||||||
|
|
||||||
|
Subjectively, we find it helpful to accompany structured context with a brief
|
||||||
|
description. This isn't critical during development, but it makes debugging
|
||||||
|
and operating unfamiliar systems much easier.
|
||||||
|
|
||||||
|
More concretely, zap's sampling algorithm uses the message to identify
|
||||||
|
duplicate entries. In our experience, this is a practical middle ground
|
||||||
|
between random sampling (which often drops the exact entry that you need while
|
||||||
|
debugging) and hashing the complete entry (which is prohibitively expensive).
|
||||||
|
|
||||||
|
### Why include package-global loggers?
|
||||||
|
|
||||||
|
Since so many other logging packages include a global logger, many
|
||||||
|
applications aren't designed to accept loggers as explicit parameters.
|
||||||
|
Changing function signatures is often a breaking change, so zap includes
|
||||||
|
global loggers to simplify migration.
|
||||||
|
|
||||||
|
Avoid them where possible.
|
||||||
|
|
||||||
|
### Why include dedicated Panic and Fatal log levels?
|
||||||
|
|
||||||
|
In general, application code should handle errors gracefully instead of using
|
||||||
|
`panic` or `os.Exit`. However, every rule has exceptions, and it's common to
|
||||||
|
crash when an error is truly unrecoverable. To avoid losing any information
|
||||||
|
— especially the reason for the crash — the logger must flush any
|
||||||
|
buffered entries before the process exits.
|
||||||
|
|
||||||
|
Zap makes this easy by offering `Panic` and `Fatal` logging methods that
|
||||||
|
automatically flush before exiting. Of course, this doesn't guarantee that
|
||||||
|
logs will never be lost, but it eliminates a common error.
|
||||||
|
|
||||||
|
See the discussion in uber-go/zap#207 for more details.
|
||||||
|
|
||||||
|
### What's `DPanic`?
|
||||||
|
|
||||||
|
`DPanic` stands for "panic in development." In development, it logs at
|
||||||
|
`PanicLevel`; otherwise, it logs at `ErrorLevel`. `DPanic` makes it easier to
|
||||||
|
catch errors that are theoretically possible, but shouldn't actually happen,
|
||||||
|
*without* crashing in production.
|
||||||
|
|
||||||
|
If you've ever written code like this, you need `DPanic`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("shouldn't ever get here: %v", err))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### What does the error `expects import "go.uber.org/zap"` mean?
|
||||||
|
|
||||||
|
Either zap was installed incorrectly or you're referencing the wrong package
|
||||||
|
name in your code.
|
||||||
|
|
||||||
|
Zap's source code happens to be hosted on GitHub, but the [import
|
||||||
|
path][import-path] is `go.uber.org/zap`. This gives us, the project
|
||||||
|
maintainers, the freedom to move the source code if necessary. However, it
|
||||||
|
means that you need to take a little care when installing and using the
|
||||||
|
package.
|
||||||
|
|
||||||
|
If you follow two simple rules, everything should work: install zap with `go
|
||||||
|
get -u go.uber.org/zap`, and always import it in your code with `import
|
||||||
|
"go.uber.org/zap"`. Your code shouldn't contain *any* references to
|
||||||
|
`github.com/uber-go/zap`.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Does zap support log rotation?
|
||||||
|
|
||||||
|
Zap doesn't natively support rotating log files, since we prefer to leave this
|
||||||
|
to an external program like `logrotate`.
|
||||||
|
|
||||||
|
However, it's easy to integrate a log rotation package like
|
||||||
|
[`gopkg.in/natefinch/lumberjack.v2`][lumberjack] as a `zapcore.WriteSyncer`.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// lumberjack.Logger is already safe for concurrent use, so we don't need to
|
||||||
|
// lock it.
|
||||||
|
w := zapcore.AddSync(&lumberjack.Logger{
|
||||||
|
Filename: "/var/log/myapp/foo.log",
|
||||||
|
MaxSize: 500, // megabytes
|
||||||
|
MaxBackups: 3,
|
||||||
|
MaxAge: 28, // days
|
||||||
|
})
|
||||||
|
core := zapcore.NewCore(
|
||||||
|
zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
|
||||||
|
w,
|
||||||
|
zap.InfoLevel,
|
||||||
|
)
|
||||||
|
logger := zap.New(core)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Extensions
|
||||||
|
|
||||||
|
We'd love to support every logging need within zap itself, but we're only
|
||||||
|
familiar with a handful of log ingestion systems, flag-parsing packages, and
|
||||||
|
the like. Rather than merging code that we can't effectively debug and
|
||||||
|
support, we'd rather grow an ecosystem of zap extensions.
|
||||||
|
|
||||||
|
We're aware of the following extensions, but haven't used them ourselves:
|
||||||
|
|
||||||
|
| Package | Integration |
|
||||||
|
| --- | --- |
|
||||||
|
| `github.com/tchap/zapext` | Sentry, syslog |
|
||||||
|
| `github.com/fgrosse/zaptest` | Ginkgo |
|
||||||
|
| `github.com/blendle/zapdriver` | Stackdriver |
|
||||||
|
|
||||||
|
[go-proverbs]: https://go-proverbs.github.io/
|
||||||
|
[import-path]: https://golang.org/cmd/go/#hdr-Remote_import_paths
|
||||||
|
[lumberjack]: https://godoc.org/gopkg.in/natefinch/lumberjack.v2
|
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (c) 2016-2017 Uber Technologies, Inc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
|
@ -0,0 +1,76 @@
|
||||||
|
export GO15VENDOREXPERIMENT=1
|
||||||
|
|
||||||
|
BENCH_FLAGS ?= -cpuprofile=cpu.pprof -memprofile=mem.pprof -benchmem
|
||||||
|
PKGS ?= $(shell glide novendor)
|
||||||
|
# Many Go tools take file globs or directories as arguments instead of packages.
|
||||||
|
PKG_FILES ?= *.go zapcore benchmarks buffer zapgrpc zaptest zaptest/observer internal/bufferpool internal/exit internal/color internal/ztest
|
||||||
|
|
||||||
|
# The linting tools evolve with each Go version, so run them only on the latest
|
||||||
|
# stable release.
|
||||||
|
GO_VERSION := $(shell go version | cut -d " " -f 3)
|
||||||
|
GO_MINOR_VERSION := $(word 2,$(subst ., ,$(GO_VERSION)))
|
||||||
|
LINTABLE_MINOR_VERSIONS := 12
|
||||||
|
ifneq ($(filter $(LINTABLE_MINOR_VERSIONS),$(GO_MINOR_VERSION)),)
|
||||||
|
SHOULD_LINT := true
|
||||||
|
endif
|
||||||
|
|
||||||
|
|
||||||
|
.PHONY: all
|
||||||
|
all: lint test
|
||||||
|
|
||||||
|
.PHONY: dependencies
|
||||||
|
dependencies:
|
||||||
|
@echo "Installing Glide and locked dependencies..."
|
||||||
|
glide --version || go get -u -f github.com/Masterminds/glide
|
||||||
|
glide install
|
||||||
|
@echo "Installing test dependencies..."
|
||||||
|
go install ./vendor/github.com/axw/gocov/gocov
|
||||||
|
go install ./vendor/github.com/mattn/goveralls
|
||||||
|
ifdef SHOULD_LINT
|
||||||
|
@echo "Installing golint..."
|
||||||
|
go install ./vendor/github.com/golang/lint/golint
|
||||||
|
else
|
||||||
|
@echo "Not installing golint, since we don't expect to lint on" $(GO_VERSION)
|
||||||
|
endif
|
||||||
|
|
||||||
|
# Disable printf-like invocation checking due to testify.assert.Error()
|
||||||
|
VET_RULES := -printf=false
|
||||||
|
|
||||||
|
.PHONY: lint
|
||||||
|
lint:
|
||||||
|
ifdef SHOULD_LINT
|
||||||
|
@rm -rf lint.log
|
||||||
|
@echo "Checking formatting..."
|
||||||
|
@gofmt -d -s $(PKG_FILES) 2>&1 | tee lint.log
|
||||||
|
@echo "Installing test dependencies for vet..."
|
||||||
|
@go test -i $(PKGS)
|
||||||
|
@echo "Checking vet..."
|
||||||
|
@go vet $(VET_RULES) $(PKGS) 2>&1 | tee -a lint.log
|
||||||
|
@echo "Checking lint..."
|
||||||
|
@$(foreach dir,$(PKGS),golint $(dir) 2>&1 | tee -a lint.log;)
|
||||||
|
@echo "Checking for unresolved FIXMEs..."
|
||||||
|
@git grep -i fixme | grep -v -e vendor -e Makefile | tee -a lint.log
|
||||||
|
@echo "Checking for license headers..."
|
||||||
|
@./check_license.sh | tee -a lint.log
|
||||||
|
@[ ! -s lint.log ]
|
||||||
|
else
|
||||||
|
@echo "Skipping linters on" $(GO_VERSION)
|
||||||
|
endif
|
||||||
|
|
||||||
|
.PHONY: test
|
||||||
|
test:
|
||||||
|
go test -race $(PKGS)
|
||||||
|
|
||||||
|
.PHONY: cover
|
||||||
|
cover:
|
||||||
|
./scripts/cover.sh $(PKGS)
|
||||||
|
|
||||||
|
.PHONY: bench
|
||||||
|
BENCH ?= .
|
||||||
|
bench:
|
||||||
|
@$(foreach pkg,$(PKGS),go test -bench=$(BENCH) -run="^$$" $(BENCH_FLAGS) $(pkg);)
|
||||||
|
|
||||||
|
.PHONY: updatereadme
|
||||||
|
updatereadme:
|
||||||
|
rm -f README.md
|
||||||
|
cat .readme.tmpl | go run internal/readme/readme.go > README.md
|
|
@ -0,0 +1,136 @@
|
||||||
|
# :zap: zap [![GoDoc][doc-img]][doc] [![Build Status][ci-img]][ci] [![Coverage Status][cov-img]][cov]
|
||||||
|
|
||||||
|
Blazing fast, structured, leveled logging in Go.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
`go get -u go.uber.org/zap`
|
||||||
|
|
||||||
|
Note that zap only supports the two most recent minor versions of Go.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
In contexts where performance is nice, but not critical, use the
|
||||||
|
`SugaredLogger`. It's 4-10x faster than other structured logging
|
||||||
|
packages and includes both structured and `printf`-style APIs.
|
||||||
|
|
||||||
|
```go
|
||||||
|
logger, _ := zap.NewProduction()
|
||||||
|
defer logger.Sync() // flushes buffer, if any
|
||||||
|
sugar := logger.Sugar()
|
||||||
|
sugar.Infow("failed to fetch URL",
|
||||||
|
// Structured context as loosely typed key-value pairs.
|
||||||
|
"url", url,
|
||||||
|
"attempt", 3,
|
||||||
|
"backoff", time.Second,
|
||||||
|
)
|
||||||
|
sugar.Infof("Failed to fetch URL: %s", url)
|
||||||
|
```
|
||||||
|
|
||||||
|
When performance and type safety are critical, use the `Logger`. It's even
|
||||||
|
faster than the `SugaredLogger` and allocates far less, but it only supports
|
||||||
|
structured logging.
|
||||||
|
|
||||||
|
```go
|
||||||
|
logger, _ := zap.NewProduction()
|
||||||
|
defer logger.Sync()
|
||||||
|
logger.Info("failed to fetch URL",
|
||||||
|
// Structured context as strongly typed Field values.
|
||||||
|
zap.String("url", url),
|
||||||
|
zap.Int("attempt", 3),
|
||||||
|
zap.Duration("backoff", time.Second),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [documentation][doc] and [FAQ](FAQ.md) for more details.
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
For applications that log in the hot path, reflection-based serialization and
|
||||||
|
string formatting are prohibitively expensive — they're CPU-intensive
|
||||||
|
and make many small allocations. Put differently, using `encoding/json` and
|
||||||
|
`fmt.Fprintf` to log tons of `interface{}`s makes your application slow.
|
||||||
|
|
||||||
|
Zap takes a different approach. It includes a reflection-free, zero-allocation
|
||||||
|
JSON encoder, and the base `Logger` strives to avoid serialization overhead
|
||||||
|
and allocations wherever possible. By building the high-level `SugaredLogger`
|
||||||
|
on that foundation, zap lets users *choose* when they need to count every
|
||||||
|
allocation and when they'd prefer a more familiar, loosely typed API.
|
||||||
|
|
||||||
|
As measured by its own [benchmarking suite][], not only is zap more performant
|
||||||
|
than comparable structured logging packages — it's also faster than the
|
||||||
|
standard library. Like all benchmarks, take these with a grain of salt.<sup
|
||||||
|
id="anchor-versions">[1](#footnote-versions)</sup>
|
||||||
|
|
||||||
|
Log a message and 10 fields:
|
||||||
|
|
||||||
|
| Package | Time | Objects Allocated |
|
||||||
|
| :--- | :---: | :---: |
|
||||||
|
| :zap: zap | 3131 ns/op | 5 allocs/op |
|
||||||
|
| :zap: zap (sugared) | 4173 ns/op | 21 allocs/op |
|
||||||
|
| zerolog | 16154 ns/op | 90 allocs/op |
|
||||||
|
| lion | 16341 ns/op | 111 allocs/op |
|
||||||
|
| go-kit | 17049 ns/op | 126 allocs/op |
|
||||||
|
| logrus | 23662 ns/op | 142 allocs/op |
|
||||||
|
| log15 | 36351 ns/op | 149 allocs/op |
|
||||||
|
| apex/log | 42530 ns/op | 126 allocs/op |
|
||||||
|
|
||||||
|
Log a message with a logger that already has 10 fields of context:
|
||||||
|
|
||||||
|
| Package | Time | Objects Allocated |
|
||||||
|
| :--- | :---: | :---: |
|
||||||
|
| :zap: zap | 380 ns/op | 0 allocs/op |
|
||||||
|
| :zap: zap (sugared) | 564 ns/op | 2 allocs/op |
|
||||||
|
| zerolog | 321 ns/op | 0 allocs/op |
|
||||||
|
| lion | 7092 ns/op | 39 allocs/op |
|
||||||
|
| go-kit | 20226 ns/op | 115 allocs/op |
|
||||||
|
| logrus | 22312 ns/op | 130 allocs/op |
|
||||||
|
| log15 | 28788 ns/op | 79 allocs/op |
|
||||||
|
| apex/log | 42063 ns/op | 115 allocs/op |
|
||||||
|
|
||||||
|
Log a static string, without any context or `printf`-style templating:
|
||||||
|
|
||||||
|
| Package | Time | Objects Allocated |
|
||||||
|
| :--- | :---: | :---: |
|
||||||
|
| :zap: zap | 361 ns/op | 0 allocs/op |
|
||||||
|
| :zap: zap (sugared) | 534 ns/op | 2 allocs/op |
|
||||||
|
| zerolog | 323 ns/op | 0 allocs/op |
|
||||||
|
| standard library | 575 ns/op | 2 allocs/op |
|
||||||
|
| go-kit | 922 ns/op | 13 allocs/op |
|
||||||
|
| lion | 1413 ns/op | 10 allocs/op |
|
||||||
|
| logrus | 2291 ns/op | 27 allocs/op |
|
||||||
|
| apex/log | 3690 ns/op | 11 allocs/op |
|
||||||
|
| log15 | 5954 ns/op | 26 allocs/op |
|
||||||
|
|
||||||
|
## Development Status: Stable
|
||||||
|
|
||||||
|
All APIs are finalized, and no breaking changes will be made in the 1.x series
|
||||||
|
of releases. Users of semver-aware dependency management systems should pin
|
||||||
|
zap to `^1`.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
We encourage and support an active, healthy community of contributors —
|
||||||
|
including you! Details are in the [contribution guide](CONTRIBUTING.md) and
|
||||||
|
the [code of conduct](CODE_OF_CONDUCT.md). The zap maintainers keep an eye on
|
||||||
|
issues and pull requests, but you can also report any negative conduct to
|
||||||
|
[email protected]. That email list is a private, safe space; even the zap
|
||||||
|
maintainers don't have access, so don't hesitate to hold us to a high
|
||||||
|
standard.
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
Released under the [MIT License](LICENSE.txt).
|
||||||
|
|
||||||
|
<sup id="footnote-versions">1</sup> In particular, keep in mind that we may be
|
||||||
|
benchmarking against slightly older versions of other packages. Versions are
|
||||||
|
pinned in zap's [glide.lock][] file. [↩](#anchor-versions)
|
||||||
|
|
||||||
|
[doc-img]: https://godoc.org/go.uber.org/zap?status.svg
|
||||||
|
[doc]: https://godoc.org/go.uber.org/zap
|
||||||
|
[ci-img]: https://travis-ci.org/uber-go/zap.svg?branch=master
|
||||||
|
[ci]: https://travis-ci.org/uber-go/zap
|
||||||
|
[cov-img]: https://codecov.io/gh/uber-go/zap/branch/master/graph/badge.svg
|
||||||
|
[cov]: https://codecov.io/gh/uber-go/zap
|
||||||
|
[benchmarking suite]: https://github.com/uber-go/zap/tree/master/benchmarks
|
||||||
|
[glide.lock]: https://github.com/uber-go/zap/blob/master/glide.lock
|
|
@ -0,0 +1,320 @@
|
||||||
|
// Copyright (c) 2016 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in
|
||||||
|
// all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
// THE SOFTWARE.
|
||||||
|
|
||||||
|
package zap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Array constructs a field with the given key and ArrayMarshaler. It provides
|
||||||
|
// a flexible, but still type-safe and efficient, way to add array-like types
|
||||||
|
// to the logging context. The struct's MarshalLogArray method is called lazily.
|
||||||
|
func Array(key string, val zapcore.ArrayMarshaler) Field {
|
||||||
|
return Field{Key: key, Type: zapcore.ArrayMarshalerType, Interface: val}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bools constructs a field that carries a slice of bools.
|
||||||
|
func Bools(key string, bs []bool) Field {
|
||||||
|
return Array(key, bools(bs))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByteStrings constructs a field that carries a slice of []byte, each of which
|
||||||
|
// must be UTF-8 encoded text.
|
||||||
|
func ByteStrings(key string, bss [][]byte) Field {
|
||||||
|
return Array(key, byteStringsArray(bss))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complex128s constructs a field that carries a slice of complex numbers.
|
||||||
|
func Complex128s(key string, nums []complex128) Field {
|
||||||
|
return Array(key, complex128s(nums))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complex64s constructs a field that carries a slice of complex numbers.
|
||||||
|
func Complex64s(key string, nums []complex64) Field {
|
||||||
|
return Array(key, complex64s(nums))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Durations constructs a field that carries a slice of time.Durations.
|
||||||
|
func Durations(key string, ds []time.Duration) Field {
|
||||||
|
return Array(key, durations(ds))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float64s constructs a field that carries a slice of floats.
|
||||||
|
func Float64s(key string, nums []float64) Field {
|
||||||
|
return Array(key, float64s(nums))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float32s constructs a field that carries a slice of floats.
|
||||||
|
func Float32s(key string, nums []float32) Field {
|
||||||
|
return Array(key, float32s(nums))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ints constructs a field that carries a slice of integers.
|
||||||
|
func Ints(key string, nums []int) Field {
|
||||||
|
return Array(key, ints(nums))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int64s constructs a field that carries a slice of integers.
|
||||||
|
func Int64s(key string, nums []int64) Field {
|
||||||
|
return Array(key, int64s(nums))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int32s constructs a field that carries a slice of integers.
|
||||||
|
func Int32s(key string, nums []int32) Field {
|
||||||
|
return Array(key, int32s(nums))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int16s constructs a field that carries a slice of integers.
|
||||||
|
func Int16s(key string, nums []int16) Field {
|
||||||
|
return Array(key, int16s(nums))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int8s constructs a field that carries a slice of integers.
|
||||||
|
func Int8s(key string, nums []int8) Field {
|
||||||
|
return Array(key, int8s(nums))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strings constructs a field that carries a slice of strings.
|
||||||
|
func Strings(key string, ss []string) Field {
|
||||||
|
return Array(key, stringArray(ss))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Times constructs a field that carries a slice of time.Times.
|
||||||
|
func Times(key string, ts []time.Time) Field {
|
||||||
|
return Array(key, times(ts))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uints constructs a field that carries a slice of unsigned integers.
|
||||||
|
func Uints(key string, nums []uint) Field {
|
||||||
|
return Array(key, uints(nums))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uint64s constructs a field that carries a slice of unsigned integers.
|
||||||
|
func Uint64s(key string, nums []uint64) Field {
|
||||||
|
return Array(key, uint64s(nums))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uint32s constructs a field that carries a slice of unsigned integers.
|
||||||
|
func Uint32s(key string, nums []uint32) Field {
|
||||||
|
return Array(key, uint32s(nums))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uint16s constructs a field that carries a slice of unsigned integers.
|
||||||
|
func Uint16s(key string, nums []uint16) Field {
|
||||||
|
return Array(key, uint16s(nums))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uint8s constructs a field that carries a slice of unsigned integers.
|
||||||
|
func Uint8s(key string, nums []uint8) Field {
|
||||||
|
return Array(key, uint8s(nums))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uintptrs constructs a field that carries a slice of pointer addresses.
|
||||||
|
func Uintptrs(key string, us []uintptr) Field {
|
||||||
|
return Array(key, uintptrs(us))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errors constructs a field that carries a slice of errors.
|
||||||
|
func Errors(key string, errs []error) Field {
|
||||||
|
return Array(key, errArray(errs))
|
||||||
|
}
|
||||||
|
|
||||||
|
type bools []bool
|
||||||
|
|
||||||
|
func (bs bools) MarshalLogArray(arr zapcore.ArrayEncoder) error {
|
||||||
|
for i := range bs {
|
||||||
|
arr.AppendBool(bs[i])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type byteStringsArray [][]byte
|
||||||
|
|
||||||
|
func (bss byteStringsArray) MarshalLogArray(arr zapcore.ArrayEncoder) error {
|
||||||
|
for i := range bss {
|
||||||
|
arr.AppendByteString(bss[i])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type complex128s []complex128
|
||||||
|
|
||||||
|
func (nums complex128s) MarshalLogArray(arr zapcore.ArrayEncoder) error {
|
||||||
|
for i := range nums {
|
||||||
|
arr.AppendComplex128(nums[i])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type complex64s []complex64
|
||||||
|
|
||||||
|
func (nums complex64s) MarshalLogArray(arr zapcore.ArrayEncoder) error {
|
||||||
|
for i := range nums {
|
||||||
|
arr.AppendComplex64(nums[i])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type durations []time.Duration
|
||||||
|
|
||||||
|
func (ds durations) MarshalLogArray(arr zapcore.ArrayEncoder) error {
|
||||||
|
for i := range ds {
|
||||||
|
arr.AppendDuration(ds[i])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type float64s []float64
|
||||||
|
|
||||||
|
func (nums float64s) MarshalLogArray(arr zapcore.ArrayEncoder) error {
|
||||||
|
for i := range nums {
|
||||||
|
arr.AppendFloat64(nums[i])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type float32s []float32
|
||||||
|
|
||||||
|
func (nums float32s) MarshalLogArray(arr zapcore.ArrayEncoder) error {
|
||||||
|
for i := range nums {
|
||||||
|
arr.AppendFloat32(nums[i])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ints []int
|
||||||
|
|
||||||
|
func (nums ints) MarshalLogArray(arr zapcore.ArrayEncoder) error {
|
||||||
|
for i := range nums {
|
||||||
|
arr.AppendInt(nums[i])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type int64s []int64
|
||||||
|
|
||||||
|
func (nums int64s) MarshalLogArray(arr zapcore.ArrayEncoder) error {
|
||||||
|
for i := range nums {
|
||||||
|
arr.AppendInt64(nums[i])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type int32s []int32
|
||||||
|
|
||||||
|
func (nums int32s) MarshalLogArray(arr zapcore.ArrayEncoder) error {
|
||||||
|
for i := range nums {
|
||||||
|
arr.AppendInt32(nums[i])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type int16s []int16
|
||||||
|
|
||||||
|
func (nums int16s) MarshalLogArray(arr zapcore.ArrayEncoder) error {
|
||||||
|
for i := range nums {
|
||||||
|
arr.AppendInt16(nums[i])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type int8s []int8
|
||||||
|
|
||||||
|
func (nums int8s) MarshalLogArray(arr zapcore.ArrayEncoder) error {
|
||||||
|
for i := range nums {
|
||||||
|
arr.AppendInt8(nums[i])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type stringArray []string
|
||||||
|
|
||||||
|
func (ss stringArray) MarshalLogArray(arr zapcore.ArrayEncoder) error {
|
||||||
|
for i := range ss {
|
||||||
|
arr.AppendString(ss[i])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type times []time.Time
|
||||||
|
|
||||||
|
func (ts times) MarshalLogArray(arr zapcore.ArrayEncoder) error {
|
||||||
|
for i := range ts {
|
||||||
|
arr.AppendTime(ts[i])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type uints []uint
|
||||||
|
|
||||||
|
func (nums uints) MarshalLogArray(arr zapcore.ArrayEncoder) error {
|
||||||
|
for i := range nums {
|
||||||
|
arr.AppendUint(nums[i])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type uint64s []uint64
|
||||||
|
|
||||||
|
func (nums uint64s) MarshalLogArray(arr zapcore.ArrayEncoder) error {
|
||||||
|
for i := range nums {
|
||||||
|
arr.AppendUint64(nums[i])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type uint32s []uint32
|
||||||
|
|
||||||
|
func (nums uint32s) MarshalLogArray(arr zapcore.ArrayEncoder) error {
|
||||||
|
for i := range nums {
|
||||||
|
arr.AppendUint32(nums[i])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type uint16s []uint16
|
||||||
|
|
||||||
|
func (nums uint16s) MarshalLogArray(arr zapcore.ArrayEncoder) error {
|
||||||
|
for i := range nums {
|
||||||
|
arr.AppendUint16(nums[i])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type uint8s []uint8
|
||||||
|
|
||||||
|
func (nums uint8s) MarshalLogArray(arr zapcore.ArrayEncoder) error {
|
||||||
|
for i := range nums {
|
||||||
|
arr.AppendUint8(nums[i])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type uintptrs []uintptr
|
||||||
|
|
||||||
|
func (nums uintptrs) MarshalLogArray(arr zapcore.ArrayEncoder) error {
|
||||||
|
for i := range nums {
|
||||||
|
arr.AppendUintptr(nums[i])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,115 @@
|
||||||
|
// Copyright (c) 2016 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in
|
||||||
|
// all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
// THE SOFTWARE.
|
||||||
|
|
||||||
|
// Package buffer provides a thin wrapper around a byte slice. Unlike the
|
||||||
|
// standard library's bytes.Buffer, it supports a portion of the strconv
|
||||||
|
// package's zero-allocation formatters.
|
||||||
|
package buffer // import "go.uber.org/zap/buffer"
|
||||||
|
|
||||||
|
import "strconv"
|
||||||
|
|
||||||
|
const _size = 1024 // by default, create 1 KiB buffers
|
||||||
|
|
||||||
|
// Buffer is a thin wrapper around a byte slice. It's intended to be pooled, so
|
||||||
|
// the only way to construct one is via a Pool.
|
||||||
|
type Buffer struct {
|
||||||
|
bs []byte
|
||||||
|
pool Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendByte writes a single byte to the Buffer.
|
||||||
|
func (b *Buffer) AppendByte(v byte) {
|
||||||
|
b.bs = append(b.bs, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendString writes a string to the Buffer.
|
||||||
|
func (b *Buffer) AppendString(s string) {
|
||||||
|
b.bs = append(b.bs, s...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendInt appends an integer to the underlying buffer (assuming base 10).
|
||||||
|
func (b *Buffer) AppendInt(i int64) {
|
||||||
|
b.bs = strconv.AppendInt(b.bs, i, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendUint appends an unsigned integer to the underlying buffer (assuming
|
||||||
|
// base 10).
|
||||||
|
func (b *Buffer) AppendUint(i uint64) {
|
||||||
|
b.bs = strconv.AppendUint(b.bs, i, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendBool appends a bool to the underlying buffer.
|
||||||
|
func (b *Buffer) AppendBool(v bool) {
|
||||||
|
b.bs = strconv.AppendBool(b.bs, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendFloat appends a float to the underlying buffer. It doesn't quote NaN
|
||||||
|
// or +/- Inf.
|
||||||
|
func (b *Buffer) AppendFloat(f float64, bitSize int) {
|
||||||
|
b.bs = strconv.AppendFloat(b.bs, f, 'f', -1, bitSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the length of the underlying byte slice.
|
||||||
|
func (b *Buffer) Len() int {
|
||||||
|
return len(b.bs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cap returns the capacity of the underlying byte slice.
|
||||||
|
func (b *Buffer) Cap() int {
|
||||||
|
return cap(b.bs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes returns a mutable reference to the underlying byte slice.
|
||||||
|
func (b *Buffer) Bytes() []byte {
|
||||||
|
return b.bs
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string copy of the underlying byte slice.
|
||||||
|
func (b *Buffer) String() string {
|
||||||
|
return string(b.bs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset resets the underlying byte slice. Subsequent writes re-use the slice's
|
||||||
|
// backing array.
|
||||||
|
func (b *Buffer) Reset() {
|
||||||
|
b.bs = b.bs[:0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write implements io.Writer.
|
||||||
|
func (b *Buffer) Write(bs []byte) (int, error) {
|
||||||
|
b.bs = append(b.bs, bs...)
|
||||||
|
return len(bs), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimNewline trims any final "\n" byte from the end of the buffer.
|
||||||
|
func (b *Buffer) TrimNewline() {
|
||||||
|
if i := len(b.bs) - 1; i >= 0 {
|
||||||
|
if b.bs[i] == '\n' {
|
||||||
|
b.bs = b.bs[:i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free returns the Buffer to its Pool.
|
||||||
|
//
|
||||||
|
// Callers must not retain references to the Buffer after calling Free.
|
||||||
|
func (b *Buffer) Free() {
|
||||||
|
b.pool.put(b)
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
// Copyright (c) 2016 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in
|
||||||
|
// all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
// THE SOFTWARE.
|
||||||
|
|
||||||
|
package buffer
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
// A Pool is a type-safe wrapper around a sync.Pool.
|
||||||
|
type Pool struct {
|
||||||
|
p *sync.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPool constructs a new Pool.
|
||||||
|
func NewPool() Pool {
|
||||||
|
return Pool{p: &sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
return &Buffer{bs: make([]byte, 0, _size)}
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves a Buffer from the pool, creating one if necessary.
|
||||||
|
func (p Pool) Get() *Buffer {
|
||||||
|
buf := p.p.Get().(*Buffer)
|
||||||
|
buf.Reset()
|
||||||
|
buf.pool = p
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Pool) put(buf *Buffer) {
|
||||||
|
p.p.Put(buf)
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
#!/bin/bash -e
|
||||||
|
|
||||||
|
ERROR_COUNT=0
|
||||||
|
while read -r file
|
||||||
|
do
|
||||||
|
case "$(head -1 "${file}")" in
|
||||||
|
*"Copyright (c) "*" Uber Technologies, Inc.")
|
||||||
|
# everything's cool
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "$file is missing license header."
|
||||||
|
(( ERROR_COUNT++ ))
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done < <(git ls-files "*\.go")
|
||||||
|
|
||||||
|
exit $ERROR_COUNT
|
|
@ -0,0 +1,243 @@
|
||||||
|
// Copyright (c) 2016 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in
|
||||||
|
// all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
// THE SOFTWARE.
|
||||||
|
|
||||||
|
package zap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SamplingConfig sets a sampling strategy for the logger. Sampling caps the
|
||||||
|
// global CPU and I/O load that logging puts on your process while attempting
|
||||||
|
// to preserve a representative subset of your logs.
|
||||||
|
//
|
||||||
|
// Values configured here are per-second. See zapcore.NewSampler for details.
|
||||||
|
type SamplingConfig struct {
|
||||||
|
Initial int `json:"initial" yaml:"initial"`
|
||||||
|
Thereafter int `json:"thereafter" yaml:"thereafter"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config offers a declarative way to construct a logger. It doesn't do
|
||||||
|
// anything that can't be done with New, Options, and the various
|
||||||
|
// zapcore.WriteSyncer and zapcore.Core wrappers, but it's a simpler way to
|
||||||
|
// toggle common options.
|
||||||
|
//
|
||||||
|
// Note that Config intentionally supports only the most common options. More
|
||||||
|
// unusual logging setups (logging to network connections or message queues,
|
||||||
|
// splitting output between multiple files, etc.) are possible, but require
|
||||||
|
// direct use of the zapcore package. For sample code, see the package-level
|
||||||
|
// BasicConfiguration and AdvancedConfiguration examples.
|
||||||
|
//
|
||||||
|
// For an example showing runtime log level changes, see the documentation for
|
||||||
|
// AtomicLevel.
|
||||||
|
type Config struct {
|
||||||
|
// Level is the minimum enabled logging level. Note that this is a dynamic
|
||||||
|
// level, so calling Config.Level.SetLevel will atomically change the log
|
||||||
|
// level of all loggers descended from this config.
|
||||||
|
Level AtomicLevel `json:"level" yaml:"level"`
|
||||||
|
// Development puts the logger in development mode, which changes the
|
||||||
|
// behavior of DPanicLevel and takes stacktraces more liberally.
|
||||||
|
Development bool `json:"development" yaml:"development"`
|
||||||
|
// DisableCaller stops annotating logs with the calling function's file
|
||||||
|
// name and line number. By default, all logs are annotated.
|
||||||
|
DisableCaller bool `json:"disableCaller" yaml:"disableCaller"`
|
||||||
|
// DisableStacktrace completely disables automatic stacktrace capturing. By
|
||||||
|
// default, stacktraces are captured for WarnLevel and above logs in
|
||||||
|
// development and ErrorLevel and above in production.
|
||||||
|
DisableStacktrace bool `json:"disableStacktrace" yaml:"disableStacktrace"`
|
||||||
|
// Sampling sets a sampling policy. A nil SamplingConfig disables sampling.
|
||||||
|
Sampling *SamplingConfig `json:"sampling" yaml:"sampling"`
|
||||||
|
// Encoding sets the logger's encoding. Valid values are "json" and
|
||||||
|
// "console", as well as any third-party encodings registered via
|
||||||
|
// RegisterEncoder.
|
||||||
|
Encoding string `json:"encoding" yaml:"encoding"`
|
||||||
|
// EncoderConfig sets options for the chosen encoder. See
|
||||||
|
// zapcore.EncoderConfig for details.
|
||||||
|
EncoderConfig zapcore.EncoderConfig `json:"encoderConfig" yaml:"encoderConfig"`
|
||||||
|
// OutputPaths is a list of URLs or file paths to write logging output to.
|
||||||
|
// See Open for details.
|
||||||
|
OutputPaths []string `json:"outputPaths" yaml:"outputPaths"`
|
||||||
|
// ErrorOutputPaths is a list of URLs to write internal logger errors to.
|
||||||
|
// The default is standard error.
|
||||||
|
//
|
||||||
|
// Note that this setting only affects internal errors; for sample code that
|
||||||
|
// sends error-level logs to a different location from info- and debug-level
|
||||||
|
// logs, see the package-level AdvancedConfiguration example.
|
||||||
|
ErrorOutputPaths []string `json:"errorOutputPaths" yaml:"errorOutputPaths"`
|
||||||
|
// InitialFields is a collection of fields to add to the root logger.
|
||||||
|
InitialFields map[string]interface{} `json:"initialFields" yaml:"initialFields"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProductionEncoderConfig returns an opinionated EncoderConfig for
|
||||||
|
// production environments.
|
||||||
|
func NewProductionEncoderConfig() zapcore.EncoderConfig {
|
||||||
|
return zapcore.EncoderConfig{
|
||||||
|
TimeKey: "ts",
|
||||||
|
LevelKey: "level",
|
||||||
|
NameKey: "logger",
|
||||||
|
CallerKey: "caller",
|
||||||
|
MessageKey: "msg",
|
||||||
|
StacktraceKey: "stacktrace",
|
||||||
|
LineEnding: zapcore.DefaultLineEnding,
|
||||||
|
EncodeLevel: zapcore.LowercaseLevelEncoder,
|
||||||
|
EncodeTime: zapcore.EpochTimeEncoder,
|
||||||
|
EncodeDuration: zapcore.SecondsDurationEncoder,
|
||||||
|
EncodeCaller: zapcore.ShortCallerEncoder,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewProductionConfig is a reasonable production logging configuration.
|
||||||
|
// Logging is enabled at InfoLevel and above.
|
||||||
|
//
|
||||||
|
// It uses a JSON encoder, writes to standard error, and enables sampling.
|
||||||
|
// Stacktraces are automatically included on logs of ErrorLevel and above.
|
||||||
|
func NewProductionConfig() Config {
|
||||||
|
return Config{
|
||||||
|
Level: NewAtomicLevelAt(InfoLevel),
|
||||||
|
Development: false,
|
||||||
|
Sampling: &SamplingConfig{
|
||||||
|
Initial: 100,
|
||||||
|
Thereafter: 100,
|
||||||
|
},
|
||||||
|
Encoding: "json",
|
||||||
|
EncoderConfig: NewProductionEncoderConfig(),
|
||||||
|
OutputPaths: []string{"stderr"},
|
||||||
|
ErrorOutputPaths: []string{"stderr"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDevelopmentEncoderConfig returns an opinionated EncoderConfig for
|
||||||
|
// development environments.
|
||||||
|
func NewDevelopmentEncoderConfig() zapcore.EncoderConfig {
|
||||||
|
return zapcore.EncoderConfig{
|
||||||
|
// Keys can be anything except the empty string.
|
||||||
|
TimeKey: "T",
|
||||||
|
LevelKey: "L",
|
||||||
|
NameKey: "N",
|
||||||
|
CallerKey: "C",
|
||||||
|
MessageKey: "M",
|
||||||
|
StacktraceKey: "S",
|
||||||
|
LineEnding: zapcore.DefaultLineEnding,
|
||||||
|
EncodeLevel: zapcore.CapitalLevelEncoder,
|
||||||
|
EncodeTime: zapcore.ISO8601TimeEncoder,
|
||||||
|
EncodeDuration: zapcore.StringDurationEncoder,
|
||||||
|
EncodeCaller: zapcore.ShortCallerEncoder,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDevelopmentConfig is a reasonable development logging configuration.
|
||||||
|
// Logging is enabled at DebugLevel and above.
|
||||||
|
//
|
||||||
|
// It enables development mode (which makes DPanicLevel logs panic), uses a
|
||||||
|
// console encoder, writes to standard error, and disables sampling.
|
||||||
|
// Stacktraces are automatically included on logs of WarnLevel and above.
|
||||||
|
func NewDevelopmentConfig() Config {
|
||||||
|
return Config{
|
||||||
|
Level: NewAtomicLevelAt(DebugLevel),
|
||||||
|
Development: true,
|
||||||
|
Encoding: "console",
|
||||||
|
EncoderConfig: NewDevelopmentEncoderConfig(),
|
||||||
|
OutputPaths: []string{"stderr"},
|
||||||
|
ErrorOutputPaths: []string{"stderr"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build constructs a logger from the Config and Options.
|
||||||
|
func (cfg Config) Build(opts ...Option) (*Logger, error) {
|
||||||
|
enc, err := cfg.buildEncoder()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sink, errSink, err := cfg.openSinks()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log := New(
|
||||||
|
zapcore.NewCore(enc, sink, cfg.Level),
|
||||||
|
cfg.buildOptions(errSink)...,
|
||||||
|
)
|
||||||
|
if len(opts) > 0 {
|
||||||
|
log = log.WithOptions(opts...)
|
||||||
|
}
|
||||||
|
return log, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg Config) buildOptions(errSink zapcore.WriteSyncer) []Option {
|
||||||
|
opts := []Option{ErrorOutput(errSink)}
|
||||||
|
|
||||||
|
if cfg.Development {
|
||||||
|
opts = append(opts, Development())
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cfg.DisableCaller {
|
||||||
|
opts = append(opts, AddCaller())
|
||||||
|
}
|
||||||
|
|
||||||
|
stackLevel := ErrorLevel
|
||||||
|
if cfg.Development {
|
||||||
|
stackLevel = WarnLevel
|
||||||
|
}
|
||||||
|
if !cfg.DisableStacktrace {
|
||||||
|
opts = append(opts, AddStacktrace(stackLevel))
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.Sampling != nil {
|
||||||
|
opts = append(opts, WrapCore(func(core zapcore.Core) zapcore.Core {
|
||||||
|
return zapcore.NewSampler(core, time.Second, int(cfg.Sampling.Initial), int(cfg.Sampling.Thereafter))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cfg.InitialFields) > 0 {
|
||||||
|
fs := make([]Field, 0, len(cfg.InitialFields))
|
||||||
|
keys := make([]string, 0, len(cfg.InitialFields))
|
||||||
|
for k := range cfg.InitialFields {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
for _, k := range keys {
|
||||||
|
fs = append(fs, Any(k, cfg.InitialFields[k]))
|
||||||
|
}
|
||||||
|
opts = append(opts, Fields(fs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg Config) openSinks() (zapcore.WriteSyncer, zapcore.WriteSyncer, error) {
|
||||||
|
sink, closeOut, err := Open(cfg.OutputPaths...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
errSink, _, err := Open(cfg.ErrorOutputPaths...)
|
||||||
|
if err != nil {
|
||||||
|
closeOut()
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return sink, errSink, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg Config) buildEncoder() (zapcore.Encoder, error) {
|
||||||
|
return newEncoder(cfg.Encoding, cfg.EncoderConfig)
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
// Copyright (c) 2016 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in
|
||||||
|
// all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
// THE SOFTWARE.
|
||||||
|
|
||||||
|
// Package zap provides fast, structured, leveled logging.
|
||||||
|
//
|
||||||
|
// For applications that log in the hot path, reflection-based serialization
|
||||||
|
// and string formatting are prohibitively expensive - they're CPU-intensive
|
||||||
|
// and make many small allocations. Put differently, using json.Marshal and
|
||||||
|
// fmt.Fprintf to log tons of interface{} makes your application slow.
|
||||||
|
//
|
||||||
|
// Zap takes a different approach. It includes a reflection-free,
|
||||||
|
// zero-allocation JSON encoder, and the base Logger strives to avoid
|
||||||
|
// serialization overhead and allocations wherever possible. By building the
|
||||||
|
// high-level SugaredLogger on that foundation, zap lets users choose when
|
||||||
|
// they need to count every allocation and when they'd prefer a more familiar,
|
||||||
|
// loosely typed API.
|
||||||
|
//
|
||||||
|
// Choosing a Logger
|
||||||
|
//
|
||||||
|
// In contexts where performance is nice, but not critical, use the
|
||||||
|
// SugaredLogger. It's 4-10x faster than other structured logging packages and
|
||||||
|
// supports both structured and printf-style logging. Like log15 and go-kit,
|
||||||
|
// the SugaredLogger's structured logging APIs are loosely typed and accept a
|
||||||
|
// variadic number of key-value pairs. (For more advanced use cases, they also
|
||||||
|
// accept strongly typed fields - see the SugaredLogger.With documentation for
|
||||||
|
// details.)
|
||||||
|
// sugar := zap.NewExample().Sugar()
|
||||||
|
// defer sugar.Sync()
|
||||||
|
// sugar.Infow("failed to fetch URL",
|
||||||
|
// "url", "http://example.com",
|
||||||
|
// "attempt", 3,
|
||||||
|
// "backoff", time.Second,
|
||||||
|
// )
|
||||||
|
// sugar.Infof("failed to fetch URL: %s", "http://example.com")
|
||||||
|
//
|
||||||
|
// By default, loggers are unbuffered. However, since zap's low-level APIs
|
||||||
|
// allow buffering, calling Sync before letting your process exit is a good
|
||||||
|
// habit.
|
||||||
|
//
|
||||||
|
// In the rare contexts where every microsecond and every allocation matter,
|
||||||
|
// use the Logger. It's even faster than the SugaredLogger and allocates far
|
||||||
|
// less, but it only supports strongly-typed, structured logging.
|
||||||
|
// logger := zap.NewExample()
|
||||||
|
// defer logger.Sync()
|
||||||
|
// logger.Info("failed to fetch URL",
|
||||||
|
// zap.String("url", "http://example.com"),
|
||||||
|
// zap.Int("attempt", 3),
|
||||||
|
// zap.Duration("backoff", time.Second),
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// Choosing between the Logger and SugaredLogger doesn't need to be an
|
||||||
|
// application-wide decision: converting between the two is simple and
|
||||||
|
// inexpensive.
|
||||||
|
// logger := zap.NewExample()
|
||||||
|
// defer logger.Sync()
|
||||||
|
// sugar := logger.Sugar()
|
||||||
|
// plain := sugar.Desugar()
|
||||||
|
//
|
||||||
|
// Configuring Zap
|
||||||
|
//
|
||||||
|
// The simplest way to build a Logger is to use zap's opinionated presets:
|
||||||
|
// NewExample, NewProduction, and NewDevelopment. These presets build a logger
|
||||||
|
// with a single function call:
|
||||||
|
// logger, err := zap.NewProduction()
|
||||||
|
// if err != nil {
|
||||||
|
// log.Fatalf("can't initialize zap logger: %v", err)
|
||||||
|
// }
|
||||||
|
// defer logger.Sync()
|
||||||
|
//
|
||||||
|
// Presets are fine for small projects, but larger projects and organizations
|
||||||
|
// naturally require a bit more customization. For most users, zap's Config
|
||||||
|
// struct strikes the right balance between flexibility and convenience. See
|
||||||
|
// the package-level BasicConfiguration example for sample code.
|
||||||
|
//
|
||||||
|
// More unusual configurations (splitting output between files, sending logs
|
||||||
|
// to a message queue, etc.) are possible, but require direct use of
|
||||||
|
// go.uber.org/zap/zapcore. See the package-level AdvancedConfiguration
|
||||||
|
// example for sample code.
|
||||||
|
//
|
||||||
|
// Extending Zap
|
||||||
|
//
|
||||||
|
// The zap package itself is a relatively thin wrapper around the interfaces
|
||||||
|
// in go.uber.org/zap/zapcore. Extending zap to support a new encoding (e.g.,
|
||||||
|
// BSON), a new log sink (e.g., Kafka), or something more exotic (perhaps an
|
||||||
|
// exception aggregation service, like Sentry or Rollbar) typically requires
|
||||||
|
// implementing the zapcore.Encoder, zapcore.WriteSyncer, or zapcore.Core
|
||||||
|
// interfaces. See the zapcore documentation for details.
|
||||||
|
//
|
||||||
|
// Similarly, package authors can use the high-performance Encoder and Core
|
||||||
|
// implementations in the zapcore package to build their own loggers.
|
||||||
|
//
|
||||||
|
// Frequently Asked Questions
|
||||||
|
//
|
||||||
|
// An FAQ covering everything from installation errors to design decisions is
|
||||||
|
// available at https://github.com/uber-go/zap/blob/master/FAQ.md.
|
||||||
|
package zap // import "go.uber.org/zap"
|
|
@ -0,0 +1,75 @@
|
||||||
|
// Copyright (c) 2016 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in
|
||||||
|
// all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
// THE SOFTWARE.
|
||||||
|
|
||||||
|
package zap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errNoEncoderNameSpecified = errors.New("no encoder name specified")
|
||||||
|
|
||||||
|
_encoderNameToConstructor = map[string]func(zapcore.EncoderConfig) (zapcore.Encoder, error){
|
||||||
|
"console": func(encoderConfig zapcore.EncoderConfig) (zapcore.Encoder, error) {
|
||||||
|
return zapcore.NewConsoleEncoder(encoderConfig), nil
|
||||||
|
},
|
||||||
|
"json": func(encoderConfig zapcore.EncoderConfig) (zapcore.Encoder, error) {
|
||||||
|
return zapcore.NewJSONEncoder(encoderConfig), nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_encoderMutex sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterEncoder registers an encoder constructor, which the Config struct
|
||||||
|
// can then reference. By default, the "json" and "console" encoders are
|
||||||
|
// registered.
|
||||||
|
//
|
||||||
|
// Attempting to register an encoder whose name is already taken returns an
|
||||||
|
// error.
|
||||||
|
func RegisterEncoder(name string, constructor func(zapcore.EncoderConfig) (zapcore.Encoder, error)) error {
|
||||||
|
_encoderMutex.Lock()
|
||||||
|
defer _encoderMutex.Unlock()
|
||||||
|
if name == "" {
|
||||||
|
return errNoEncoderNameSpecified
|
||||||
|
}
|
||||||
|
if _, ok := _encoderNameToConstructor[name]; ok {
|
||||||
|
return fmt.Errorf("encoder already registered for name %q", name)
|
||||||
|
}
|
||||||
|
_encoderNameToConstructor[name] = constructor
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newEncoder(name string, encoderConfig zapcore.EncoderConfig) (zapcore.Encoder, error) {
|
||||||
|
_encoderMutex.RLock()
|
||||||
|
defer _encoderMutex.RUnlock()
|
||||||
|
if name == "" {
|
||||||
|
return nil, errNoEncoderNameSpecified
|
||||||
|
}
|
||||||
|
constructor, ok := _encoderNameToConstructor[name]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("no encoder registered for name %q", name)
|
||||||
|
}
|
||||||
|
return constructor(encoderConfig)
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
// Copyright (c) 2017 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in
|
||||||
|
// all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
// THE SOFTWARE.
|
||||||
|
|
||||||
|
package zap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _errArrayElemPool = sync.Pool{New: func() interface{} {
|
||||||
|
return &errArrayElem{}
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Error is shorthand for the common idiom NamedError("error", err).
|
||||||
|
func Error(err error) Field {
|
||||||
|
return NamedError("error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NamedError constructs a field that lazily stores err.Error() under the
|
||||||
|
// provided key. Errors which also implement fmt.Formatter (like those produced
|
||||||
|
// by github.com/pkg/errors) will also have their verbose representation stored
|
||||||
|
// under key+"Verbose". If passed a nil error, the field is a no-op.
|
||||||
|
//
|
||||||
|
// For the common case in which the key is simply "error", the Error function
|
||||||
|
// is shorter and less repetitive.
|
||||||
|
func NamedError(key string, err error) Field {
|
||||||
|
if err == nil {
|
||||||
|
return Skip()
|
||||||
|
}
|
||||||
|
return Field{Key: key, Type: zapcore.ErrorType, Interface: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
type errArray []error
|
||||||
|
|
||||||
|
func (errs errArray) MarshalLogArray(arr zapcore.ArrayEncoder) error {
|
||||||
|
for i := range errs {
|
||||||
|
if errs[i] == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// To represent each error as an object with an "error" attribute and
|
||||||
|
// potentially an "errorVerbose" attribute, we need to wrap it in a
|
||||||
|
// type that implements LogObjectMarshaler. To prevent this from
|
||||||
|
// allocating, pool the wrapper type.
|
||||||
|
elem := _errArrayElemPool.Get().(*errArrayElem)
|
||||||
|
elem.error = errs[i]
|
||||||
|
arr.AppendObject(elem)
|
||||||
|
elem.error = nil
|
||||||
|
_errArrayElemPool.Put(elem)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type errArrayElem struct {
|
||||||
|
error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *errArrayElem) MarshalLogObject(enc zapcore.ObjectEncoder) error {
|
||||||
|
// Re-use the error field's logic, which supports non-standard error types.
|
||||||
|
Error(e.error).AddTo(enc)
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,310 @@
|
||||||
|
// Copyright (c) 2016 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in
|
||||||
|
// all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
// THE SOFTWARE.
|
||||||
|
|
||||||
|
package zap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Field is an alias for Field. Aliasing this type dramatically
|
||||||
|
// improves the navigability of this package's API documentation.
|
||||||
|
type Field = zapcore.Field
|
||||||
|
|
||||||
|
// Skip constructs a no-op field, which is often useful when handling invalid
|
||||||
|
// inputs in other Field constructors.
|
||||||
|
func Skip() Field {
|
||||||
|
return Field{Type: zapcore.SkipType}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Binary constructs a field that carries an opaque binary blob.
|
||||||
|
//
|
||||||
|
// Binary data is serialized in an encoding-appropriate format. For example,
|
||||||
|
// zap's JSON encoder base64-encodes binary blobs. To log UTF-8 encoded text,
|
||||||
|
// use ByteString.
|
||||||
|
func Binary(key string, val []byte) Field {
|
||||||
|
return Field{Key: key, Type: zapcore.BinaryType, Interface: val}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bool constructs a field that carries a bool.
|
||||||
|
func Bool(key string, val bool) Field {
|
||||||
|
var ival int64
|
||||||
|
if val {
|
||||||
|
ival = 1
|
||||||
|
}
|
||||||
|
return Field{Key: key, Type: zapcore.BoolType, Integer: ival}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByteString constructs a field that carries UTF-8 encoded text as a []byte.
|
||||||
|
// To log opaque binary blobs (which aren't necessarily valid UTF-8), use
|
||||||
|
// Binary.
|
||||||
|
func ByteString(key string, val []byte) Field {
|
||||||
|
return Field{Key: key, Type: zapcore.ByteStringType, Interface: val}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complex128 constructs a field that carries a complex number. Unlike most
|
||||||
|
// numeric fields, this costs an allocation (to convert the complex128 to
|
||||||
|
// interface{}).
|
||||||
|
func Complex128(key string, val complex128) Field {
|
||||||
|
return Field{Key: key, Type: zapcore.Complex128Type, Interface: val}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complex64 constructs a field that carries a complex number. Unlike most
|
||||||
|
// numeric fields, this costs an allocation (to convert the complex64 to
|
||||||
|
// interface{}).
|
||||||
|
func Complex64(key string, val complex64) Field {
|
||||||
|
return Field{Key: key, Type: zapcore.Complex64Type, Interface: val}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float64 constructs a field that carries a float64. The way the
|
||||||
|
// floating-point value is represented is encoder-dependent, so marshaling is
|
||||||
|
// necessarily lazy.
|
||||||
|
func Float64(key string, val float64) Field {
|
||||||
|
return Field{Key: key, Type: zapcore.Float64Type, Integer: int64(math.Float64bits(val))}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float32 constructs a field that carries a float32. The way the
|
||||||
|
// floating-point value is represented is encoder-dependent, so marshaling is
|
||||||
|
// necessarily lazy.
|
||||||
|
func Float32(key string, val float32) Field {
|
||||||
|
return Field{Key: key, Type: zapcore.Float32Type, Integer: int64(math.Float32bits(val))}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int constructs a field with the given key and value.
|
||||||
|
func Int(key string, val int) Field {
|
||||||
|
return Int64(key, int64(val))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int64 constructs a field with the given key and value.
|
||||||
|
func Int64(key string, val int64) Field {
|
||||||
|
return Field{Key: key, Type: zapcore.Int64Type, Integer: val}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int32 constructs a field with the given key and value.
|
||||||
|
func Int32(key string, val int32) Field {
|
||||||
|
return Field{Key: key, Type: zapcore.Int32Type, Integer: int64(val)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int16 constructs a field with the given key and value.
|
||||||
|
func Int16(key string, val int16) Field {
|
||||||
|
return Field{Key: key, Type: zapcore.Int16Type, Integer: int64(val)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int8 constructs a field with the given key and value.
|
||||||
|
func Int8(key string, val int8) Field {
|
||||||
|
return Field{Key: key, Type: zapcore.Int8Type, Integer: int64(val)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String constructs a field with the given key and value.
|
||||||
|
func String(key string, val string) Field {
|
||||||
|
return Field{Key: key, Type: zapcore.StringType, String: val}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uint constructs a field with the given key and value.
|
||||||
|
func Uint(key string, val uint) Field {
|
||||||
|
return Uint64(key, uint64(val))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uint64 constructs a field with the given key and value.
|
||||||
|
func Uint64(key string, val uint64) Field {
|
||||||
|
return Field{Key: key, Type: zapcore.Uint64Type, Integer: int64(val)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uint32 constructs a field with the given key and value.
|
||||||
|
func Uint32(key string, val uint32) Field {
|
||||||
|
return Field{Key: key, Type: zapcore.Uint32Type, Integer: int64(val)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uint16 constructs a field with the given key and value.
|
||||||
|
func Uint16(key string, val uint16) Field {
|
||||||
|
return Field{Key: key, Type: zapcore.Uint16Type, Integer: int64(val)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uint8 constructs a field with the given key and value.
|
||||||
|
func Uint8(key string, val uint8) Field {
|
||||||
|
return Field{Key: key, Type: zapcore.Uint8Type, Integer: int64(val)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uintptr constructs a field with the given key and value.
|
||||||
|
func Uintptr(key string, val uintptr) Field {
|
||||||
|
return Field{Key: key, Type: zapcore.UintptrType, Integer: int64(val)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reflect constructs a field with the given key and an arbitrary object. It uses
|
||||||
|
// an encoding-appropriate, reflection-based function to lazily serialize nearly
|
||||||
|
// any object into the logging context, but it's relatively slow and
|
||||||
|
// allocation-heavy. Outside tests, Any is always a better choice.
|
||||||
|
//
|
||||||
|
// If encoding fails (e.g., trying to serialize a map[int]string to JSON), Reflect
|
||||||
|
// includes the error message in the final log output.
|
||||||
|
func Reflect(key string, val interface{}) Field {
|
||||||
|
return Field{Key: key, Type: zapcore.ReflectType, Interface: val}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Namespace creates a named, isolated scope within the logger's context. All
|
||||||
|
// subsequent fields will be added to the new namespace.
|
||||||
|
//
|
||||||
|
// This helps prevent key collisions when injecting loggers into sub-components
|
||||||
|
// or third-party libraries.
|
||||||
|
func Namespace(key string) Field {
|
||||||
|
return Field{Key: key, Type: zapcore.NamespaceType}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stringer constructs a field with the given key and the output of the value's
|
||||||
|
// String method. The Stringer's String method is called lazily.
|
||||||
|
func Stringer(key string, val fmt.Stringer) Field {
|
||||||
|
return Field{Key: key, Type: zapcore.StringerType, Interface: val}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time constructs a Field with the given key and value. The encoder
|
||||||
|
// controls how the time is serialized.
|
||||||
|
func Time(key string, val time.Time) Field {
|
||||||
|
return Field{Key: key, Type: zapcore.TimeType, Integer: val.UnixNano(), Interface: val.Location()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stack constructs a field that stores a stacktrace of the current goroutine
|
||||||
|
// under provided key. Keep in mind that taking a stacktrace is eager and
|
||||||
|
// expensive (relatively speaking); this function both makes an allocation and
|
||||||
|
// takes about two microseconds.
|
||||||
|
func Stack(key string) Field {
|
||||||
|
// Returning the stacktrace as a string costs an allocation, but saves us
|
||||||
|
// from expanding the zapcore.Field union struct to include a byte slice. Since
|
||||||
|
// taking a stacktrace is already so expensive (~10us), the extra allocation
|
||||||
|
// is okay.
|
||||||
|
return String(key, takeStacktrace())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duration constructs a field with the given key and value. The encoder
|
||||||
|
// controls how the duration is serialized.
|
||||||
|
func Duration(key string, val time.Duration) Field {
|
||||||
|
return Field{Key: key, Type: zapcore.DurationType, Integer: int64(val)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Object constructs a field with the given key and ObjectMarshaler. It
|
||||||
|
// provides a flexible, but still type-safe and efficient, way to add map- or
|
||||||
|
// struct-like user-defined types to the logging context. The struct's
|
||||||
|
// MarshalLogObject method is called lazily.
|
||||||
|
func Object(key string, val zapcore.ObjectMarshaler) Field {
|
||||||
|
return Field{Key: key, Type: zapcore.ObjectMarshalerType, Interface: val}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any takes a key and an arbitrary value and chooses the best way to represent
|
||||||
|
// them as a field, falling back to a reflection-based approach only if
|
||||||
|
// necessary.
|
||||||
|
//
|
||||||
|
// Since byte/uint8 and rune/int32 are aliases, Any can't differentiate between
|
||||||
|
// them. To minimize surprises, []byte values are treated as binary blobs, byte
|
||||||
|
// values are treated as uint8, and runes are always treated as integers.
|
||||||
|
func Any(key string, value interface{}) Field {
|
||||||
|
switch val := value.(type) {
|
||||||
|
case zapcore.ObjectMarshaler:
|
||||||
|
return Object(key, val)
|
||||||
|
case zapcore.ArrayMarshaler:
|
||||||
|
return Array(key, val)
|
||||||
|
case bool:
|
||||||
|
return Bool(key, val)
|
||||||
|
case []bool:
|
||||||
|
return Bools(key, val)
|
||||||
|
case complex128:
|
||||||
|
return Complex128(key, val)
|
||||||
|
case []complex128:
|
||||||
|
return Complex128s(key, val)
|
||||||
|
case complex64:
|
||||||
|
return Complex64(key, val)
|
||||||
|
case []complex64:
|
||||||
|
return Complex64s(key, val)
|
||||||
|
case float64:
|
||||||
|
return Float64(key, val)
|
||||||
|
case []float64:
|
||||||
|
return Float64s(key, val)
|
||||||
|
case float32:
|
||||||
|
return Float32(key, val)
|
||||||
|
case []float32:
|
||||||
|
return Float32s(key, val)
|
||||||
|
case int:
|
||||||
|
return Int(key, val)
|
||||||
|
case []int:
|
||||||
|
return Ints(key, val)
|
||||||
|
case int64:
|
||||||
|
return Int64(key, val)
|
||||||
|
case []int64:
|
||||||
|
return Int64s(key, val)
|
||||||
|
case int32:
|
||||||
|
return Int32(key, val)
|
||||||
|
case []int32:
|
||||||
|
return Int32s(key, val)
|
||||||
|
case int16:
|
||||||
|
return Int16(key, val)
|
||||||
|
case []int16:
|
||||||
|
return Int16s(key, val)
|
||||||
|
case int8:
|
||||||
|
return Int8(key, val)
|
||||||
|
case []int8:
|
||||||
|
return Int8s(key, val)
|
||||||
|
case string:
|
||||||
|
return String(key, val)
|
||||||
|
case []string:
|
||||||
|
return Strings(key, val)
|
||||||
|
case uint:
|
||||||
|
return Uint(key, val)
|
||||||
|
case []uint:
|
||||||
|
return Uints(key, val)
|
||||||
|
case uint64:
|
||||||
|
return Uint64(key, val)
|
||||||
|
case []uint64:
|
||||||
|
return Uint64s(key, val)
|
||||||
|
case uint32:
|
||||||
|
return Uint32(key, val)
|
||||||
|
case []uint32:
|
||||||
|
return Uint32s(key, val)
|
||||||
|
case uint16:
|
||||||
|
return Uint16(key, val)
|
||||||
|
case []uint16:
|
||||||
|
return Uint16s(key, val)
|
||||||
|
case uint8:
|
||||||
|
return Uint8(key, val)
|
||||||
|
case []byte:
|
||||||
|
return Binary(key, val)
|
||||||
|
case uintptr:
|
||||||
|
return Uintptr(key, val)
|
||||||
|
case []uintptr:
|
||||||
|
return Uintptrs(key, val)
|
||||||
|
case time.Time:
|
||||||
|
return Time(key, val)
|
||||||
|
case []time.Time:
|
||||||
|
return Times(key, val)
|
||||||
|
case time.Duration:
|
||||||
|
return Duration(key, val)
|
||||||
|
case []time.Duration:
|
||||||
|
return Durations(key, val)
|
||||||
|
case error:
|
||||||
|
return NamedError(key, val)
|
||||||
|
case []error:
|
||||||
|
return Errors(key, val)
|
||||||
|
case fmt.Stringer:
|
||||||
|
return Stringer(key, val)
|
||||||
|
default:
|
||||||
|
return Reflect(key, val)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
// Copyright (c) 2016 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in
|
||||||
|
// all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
// THE SOFTWARE.
|
||||||
|
|
||||||
|
package zap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LevelFlag uses the standard library's flag.Var to declare a global flag
|
||||||
|
// with the specified name, default, and usage guidance. The returned value is
|
||||||
|
// a pointer to the value of the flag.
|
||||||
|
//
|
||||||
|
// If you don't want to use the flag package's global state, you can use any
|
||||||
|
// non-nil *Level as a flag.Value with your own *flag.FlagSet.
|
||||||
|
func LevelFlag(name string, defaultLevel zapcore.Level, usage string) *zapcore.Level {
|
||||||
|
lvl := defaultLevel
|
||||||
|
flag.Var(&lvl, name, usage)
|
||||||
|
return &lvl
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
hash: f073ba522c06c88ea3075bde32a8aaf0969a840a66cab6318a0897d141ffee92
|
||||||
|
updated: 2017-07-22T18:06:49.598185334-07:00
|
||||||
|
imports:
|
||||||
|
- name: go.uber.org/atomic
|
||||||
|
version: 4e336646b2ef9fc6e47be8e21594178f98e5ebcf
|
||||||
|
- name: go.uber.org/multierr
|
||||||
|
version: 3c4937480c32f4c13a875a1829af76c98ca3d40a
|
||||||
|
testImports:
|
||||||
|
- name: github.com/apex/log
|
||||||
|
version: d9b960447bfa720077b2da653cc79e533455b499
|
||||||
|
subpackages:
|
||||||
|
- handlers/json
|
||||||
|
- name: github.com/axw/gocov
|
||||||
|
version: 3a69a0d2a4ef1f263e2d92b041a69593d6964fe8
|
||||||
|
subpackages:
|
||||||
|
- gocov
|
||||||
|
- name: github.com/davecgh/go-spew
|
||||||
|
version: 04cdfd42973bb9c8589fd6a731800cf222fde1a9
|
||||||
|
subpackages:
|
||||||
|
- spew
|
||||||
|
- name: github.com/fatih/color
|
||||||
|
version: 62e9147c64a1ed519147b62a56a14e83e2be02c1
|
||||||
|
- name: github.com/go-kit/kit
|
||||||
|
version: e10f5bf035be9af21fd5b2fb4469d5716c6ab07d
|
||||||
|
subpackages:
|
||||||
|
- log
|
||||||
|
- name: github.com/go-logfmt/logfmt
|
||||||
|
version: 390ab7935ee28ec6b286364bba9b4dd6410cb3d5
|
||||||
|
- name: github.com/go-stack/stack
|
||||||
|
version: 54be5f394ed2c3e19dac9134a40a95ba5a017f7b
|
||||||
|
- name: github.com/golang/lint
|
||||||
|
version: c5fb716d6688a859aae56d26d3e6070808df29f7
|
||||||
|
subpackages:
|
||||||
|
- golint
|
||||||
|
- name: github.com/kr/logfmt
|
||||||
|
version: b84e30acd515aadc4b783ad4ff83aff3299bdfe0
|
||||||
|
- name: github.com/mattn/go-colorable
|
||||||
|
version: 3fa8c76f9daed4067e4a806fb7e4dc86455c6d6a
|
||||||
|
- name: github.com/mattn/go-isatty
|
||||||
|
version: fc9e8d8ef48496124e79ae0df75490096eccf6fe
|
||||||
|
- name: github.com/mattn/goveralls
|
||||||
|
version: 6efce81852ad1b7567c17ad71b03aeccc9dd9ae0
|
||||||
|
- name: github.com/pborman/uuid
|
||||||
|
version: e790cca94e6cc75c7064b1332e63811d4aae1a53
|
||||||
|
- name: github.com/pkg/errors
|
||||||
|
version: 645ef00459ed84a119197bfb8d8205042c6df63d
|
||||||
|
- name: github.com/pmezard/go-difflib
|
||||||
|
version: d8ed2627bdf02c080bf22230dbb337003b7aba2d
|
||||||
|
subpackages:
|
||||||
|
- difflib
|
||||||
|
- name: github.com/rs/zerolog
|
||||||
|
version: eed4c2b94d945e0b2456ad6aa518a443986b5f22
|
||||||
|
- name: github.com/satori/go.uuid
|
||||||
|
version: 5bf94b69c6b68ee1b541973bb8e1144db23a194b
|
||||||
|
- name: github.com/sirupsen/logrus
|
||||||
|
version: 7dd06bf38e1e13df288d471a57d5adbac106be9e
|
||||||
|
- name: github.com/stretchr/testify
|
||||||
|
version: f6abca593680b2315d2075e0f5e2a9751e3f431a
|
||||||
|
subpackages:
|
||||||
|
- assert
|
||||||
|
- require
|
||||||
|
- name: go.pedge.io/lion
|
||||||
|
version: 87958e8713f1fa138d993087133b97e976642159
|
||||||
|
- name: golang.org/x/sys
|
||||||
|
version: c4489faa6e5ab84c0ef40d6ee878f7a030281f0f
|
||||||
|
subpackages:
|
||||||
|
- unix
|
||||||
|
- name: golang.org/x/tools
|
||||||
|
version: 496819729719f9d07692195e0a94d6edd2251389
|
||||||
|
subpackages:
|
||||||
|
- cover
|
||||||
|
- name: gopkg.in/inconshreveable/log15.v2
|
||||||
|
version: b105bd37f74e5d9dc7b6ad7806715c7a2b83fd3f
|
||||||
|
subpackages:
|
||||||
|
- stack
|
||||||
|
- term
|
|
@ -0,0 +1,35 @@
|
||||||
|
package: go.uber.org/zap
|
||||||
|
license: MIT
|
||||||
|
import:
|
||||||
|
- package: go.uber.org/atomic
|
||||||
|
version: ^1
|
||||||
|
- package: go.uber.org/multierr
|
||||||
|
version: ^1
|
||||||
|
testImport:
|
||||||
|
- package: github.com/satori/go.uuid
|
||||||
|
- package: github.com/sirupsen/logrus
|
||||||
|
- package: github.com/apex/log
|
||||||
|
subpackages:
|
||||||
|
- handlers/json
|
||||||
|
- package: github.com/go-kit/kit
|
||||||
|
subpackages:
|
||||||
|
- log
|
||||||
|
- package: github.com/stretchr/testify
|
||||||
|
subpackages:
|
||||||
|
- assert
|
||||||
|
- require
|
||||||
|
- package: gopkg.in/inconshreveable/log15.v2
|
||||||
|
- package: github.com/mattn/goveralls
|
||||||
|
- package: github.com/pborman/uuid
|
||||||
|
- package: github.com/pkg/errors
|
||||||
|
- package: go.pedge.io/lion
|
||||||
|
- package: github.com/rs/zerolog
|
||||||
|
- package: golang.org/x/tools
|
||||||
|
subpackages:
|
||||||
|
- cover
|
||||||
|
- package: github.com/golang/lint
|
||||||
|
subpackages:
|
||||||
|
- golint
|
||||||
|
- package: github.com/axw/gocov
|
||||||
|
subpackages:
|
||||||
|
- gocov
|
|
@ -0,0 +1,168 @@
|
||||||
|
// Copyright (c) 2016 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in
|
||||||
|
// all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
// THE SOFTWARE.
|
||||||
|
|
||||||
|
package zap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
_loggerWriterDepth = 2
|
||||||
|
_programmerErrorTemplate = "You've found a bug in zap! Please file a bug at " +
|
||||||
|
"https://github.com/uber-go/zap/issues/new and reference this error: %v"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_globalMu sync.RWMutex
|
||||||
|
_globalL = NewNop()
|
||||||
|
_globalS = _globalL.Sugar()
|
||||||
|
)
|
||||||
|
|
||||||
|
// L returns the global Logger, which can be reconfigured with ReplaceGlobals.
|
||||||
|
// It's safe for concurrent use.
|
||||||
|
func L() *Logger {
|
||||||
|
_globalMu.RLock()
|
||||||
|
l := _globalL
|
||||||
|
_globalMu.RUnlock()
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// S returns the global SugaredLogger, which can be reconfigured with
|
||||||
|
// ReplaceGlobals. It's safe for concurrent use.
|
||||||
|
func S() *SugaredLogger {
|
||||||
|
_globalMu.RLock()
|
||||||
|
s := _globalS
|
||||||
|
_globalMu.RUnlock()
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceGlobals replaces the global Logger and SugaredLogger, and returns a
|
||||||
|
// function to restore the original values. It's safe for concurrent use.
|
||||||
|
func ReplaceGlobals(logger *Logger) func() {
|
||||||
|
_globalMu.Lock()
|
||||||
|
prev := _globalL
|
||||||
|
_globalL = logger
|
||||||
|
_globalS = logger.Sugar()
|
||||||
|
_globalMu.Unlock()
|
||||||
|
return func() { ReplaceGlobals(prev) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStdLog returns a *log.Logger which writes to the supplied zap Logger at
|
||||||
|
// InfoLevel. To redirect the standard library's package-global logging
|
||||||
|
// functions, use RedirectStdLog instead.
|
||||||
|
func NewStdLog(l *Logger) *log.Logger {
|
||||||
|
logger := l.WithOptions(AddCallerSkip(_stdLogDefaultDepth + _loggerWriterDepth))
|
||||||
|
f := logger.Info
|
||||||
|
return log.New(&loggerWriter{f}, "" /* prefix */, 0 /* flags */)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStdLogAt returns *log.Logger which writes to supplied zap logger at
|
||||||
|
// required level.
|
||||||
|
func NewStdLogAt(l *Logger, level zapcore.Level) (*log.Logger, error) {
|
||||||
|
logger := l.WithOptions(AddCallerSkip(_stdLogDefaultDepth + _loggerWriterDepth))
|
||||||
|
logFunc, err := levelToFunc(logger, level)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return log.New(&loggerWriter{logFunc}, "" /* prefix */, 0 /* flags */), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RedirectStdLog redirects output from the standard library's package-global
|
||||||
|
// logger to the supplied logger at InfoLevel. Since zap already handles caller
|
||||||
|
// annotations, timestamps, etc., it automatically disables the standard
|
||||||
|
// library's annotations and prefixing.
|
||||||
|
//
|
||||||
|
// It returns a function to restore the original prefix and flags and reset the
|
||||||
|
// standard library's output to os.Stderr.
|
||||||
|
func RedirectStdLog(l *Logger) func() {
|
||||||
|
f, err := redirectStdLogAt(l, InfoLevel)
|
||||||
|
if err != nil {
|
||||||
|
// Can't get here, since passing InfoLevel to redirectStdLogAt always
|
||||||
|
// works.
|
||||||
|
panic(fmt.Sprintf(_programmerErrorTemplate, err))
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// RedirectStdLogAt redirects output from the standard library's package-global
|
||||||
|
// logger to the supplied logger at the specified level. Since zap already
|
||||||
|
// handles caller annotations, timestamps, etc., it automatically disables the
|
||||||
|
// standard library's annotations and prefixing.
|
||||||
|
//
|
||||||
|
// It returns a function to restore the original prefix and flags and reset the
|
||||||
|
// standard library's output to os.Stderr.
|
||||||
|
func RedirectStdLogAt(l *Logger, level zapcore.Level) (func(), error) {
|
||||||
|
return redirectStdLogAt(l, level)
|
||||||
|
}
|
||||||
|
|
||||||
|
func redirectStdLogAt(l *Logger, level zapcore.Level) (func(), error) {
|
||||||
|
flags := log.Flags()
|
||||||
|
prefix := log.Prefix()
|
||||||
|
log.SetFlags(0)
|
||||||
|
log.SetPrefix("")
|
||||||
|
logger := l.WithOptions(AddCallerSkip(_stdLogDefaultDepth + _loggerWriterDepth))
|
||||||
|
logFunc, err := levelToFunc(logger, level)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
log.SetOutput(&loggerWriter{logFunc})
|
||||||
|
return func() {
|
||||||
|
log.SetFlags(flags)
|
||||||
|
log.SetPrefix(prefix)
|
||||||
|
log.SetOutput(os.Stderr)
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func levelToFunc(logger *Logger, lvl zapcore.Level) (func(string, ...Field), error) {
|
||||||
|
switch lvl {
|
||||||
|
case DebugLevel:
|
||||||
|
return logger.Debug, nil
|
||||||
|
case InfoLevel:
|
||||||
|
return logger.Info, nil
|
||||||
|
case WarnLevel:
|
||||||
|
return logger.Warn, nil
|
||||||
|
case ErrorLevel:
|
||||||
|
return logger.Error, nil
|
||||||
|
case DPanicLevel:
|
||||||
|
return logger.DPanic, nil
|
||||||
|
case PanicLevel:
|
||||||
|
return logger.Panic, nil
|
||||||
|
case FatalLevel:
|
||||||
|
return logger.Fatal, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unrecognized level: %q", lvl)
|
||||||
|
}
|
||||||
|
|
||||||
|
type loggerWriter struct {
|
||||||
|
logFunc func(msg string, fields ...Field)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *loggerWriter) Write(p []byte) (int, error) {
|
||||||
|
p = bytes.TrimSpace(p)
|
||||||
|
l.logFunc(string(p))
|
||||||
|
return len(p), nil
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
// Copyright (c) 2019 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in
|
||||||
|
// all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
// THE SOFTWARE.
|
||||||
|
|
||||||
|
// See #682 for more information.
|
||||||
|
// +build go1.12
|
||||||
|
|
||||||
|
package zap
|
||||||
|
|
||||||
|
const _stdLogDefaultDepth = 1
|
|
@ -0,0 +1,26 @@
|
||||||
|
// Copyright (c) 2019 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in
|
||||||
|
// all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
// THE SOFTWARE.
|
||||||
|
|
||||||
|
// See #682 for more information.
|
||||||
|
// +build !go1.12
|
||||||
|
|
||||||
|
package zap
|
||||||
|
|
||||||
|
const _stdLogDefaultDepth = 2
|
|
@ -0,0 +1,81 @@
|
||||||
|
// Copyright (c) 2016 Uber Technologies, Inc.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in
|
||||||
|
// all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
// THE SOFTWARE.
|
||||||
|
|
||||||
|
package zap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServeHTTP is a simple JSON endpoint that can report on or change the current
|
||||||
|
// logging level.
|
||||||
|
//
|
||||||
|
// GET requests return a JSON description of the current logging level. PUT
|
||||||
|
// requests change the logging level and expect a payload like:
|
||||||
|
// {"level":"info"}
|
||||||
|
//
|
||||||
|
// It's perfectly safe to change the logging level while a program is running.
|
||||||
|
func (lvl AtomicLevel) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
type errorResponse struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
type payload struct {
|
||||||
|
Level *zapcore.Level `json:"level"`
|
||||||
|
}
|
||||||
|
|
||||||
|
enc := json.NewEncoder(w)
|
||||||
|
|
||||||
|
switch r.Method {
|
||||||
|
|
||||||
|
case http.MethodGet:
|
||||||
|
current := lvl.Level()
|
||||||
|
enc.Encode(payload{Level: ¤t})
|
||||||
|
|
||||||
|
case http.MethodPut:
|
||||||
|
var req payload
|
||||||
|
|
||||||
|
if errmess := func() string {
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
return fmt.Sprintf("Request body must be well-formed JSON: %v", err)
|
||||||
|
}
|
||||||
|
if req.Level == nil {
|
||||||
|
return "Must specify a logging level."
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}(); errmess != "" {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
enc.Encode(errorResponse{Error: errmess})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lvl.SetLevel(*req.Level)
|
||||||
|
enc.Encode(req)
|
||||||
|
|
||||||
|
default:
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
enc.Encode(errorResponse{
|
||||||
|
Error: "Only GET and PUT are supported.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue