├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cmd └── api │ └── main.go ├── docker-compose.yml ├── filter ├── service.go └── service_test.go ├── go.mod ├── go.sum ├── killfile ├── rules ├── github_repository.go ├── local_repository.go └── rules.go ├── run_develop.sh └── vendor ├── github.com ├── go-chi │ └── chi │ │ ├── .gitignore │ │ ├── .travis.yml │ │ ├── CHANGELOG.md │ │ ├── CONTRIBUTING.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── chain.go │ │ ├── chi.go │ │ ├── context.go │ │ ├── mux.go │ │ └── tree.go ├── go-kit │ └── kit │ │ ├── LICENSE │ │ └── log │ │ ├── README.md │ │ ├── doc.go │ │ ├── json_logger.go │ │ ├── level │ │ ├── doc.go │ │ └── level.go │ │ ├── log.go │ │ ├── logfmt_logger.go │ │ ├── nop_logger.go │ │ ├── stdlib.go │ │ ├── sync.go │ │ └── value.go ├── go-logfmt │ └── logfmt │ │ ├── .gitignore │ │ ├── .travis.yml │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── decode.go │ │ ├── doc.go │ │ ├── encode.go │ │ ├── fuzz.go │ │ ├── go.mod │ │ ├── go.sum │ │ └── jsonstring.go ├── kr │ └── logfmt │ │ ├── .gitignore │ │ ├── Readme │ │ ├── decode.go │ │ ├── scanner.go │ │ └── unquote.go ├── peterbourgon │ └── ff │ │ ├── .build.yml │ │ ├── .gitignore │ │ ├── LICENSE │ │ ├── README.md │ │ ├── go.mod │ │ ├── go.sum │ │ ├── json.go │ │ └── parse.go └── robfig │ └── cron │ └── v3 │ ├── .gitignore │ ├── .travis.yml │ ├── LICENSE │ ├── README.md │ ├── chain.go │ ├── constantdelay.go │ ├── cron.go │ ├── doc.go │ ├── go.mod │ ├── logger.go │ ├── option.go │ ├── parser.go │ └── spec.go ├── miniflux.app ├── LICENSE └── client │ ├── README.md │ ├── client.go │ ├── core.go │ ├── doc.go │ └── request.go └── modules.txt /.gitignore: -------------------------------------------------------------------------------- 1 | /develop.env 2 | /miniflux-sidekick 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.12-alpine as builder 2 | 3 | RUN apk add git bash 4 | 5 | ENV GO111MODULE=on 6 | 7 | # Add our code 8 | ADD ./ $GOPATH/src/github.com/dewey/miniflux-sidekick 9 | 10 | # build 11 | WORKDIR $GOPATH/src/github.com/dewey/miniflux-sidekick 12 | RUN cd $GOPATH/src/github.com/dewey/miniflux-sidekick && \ 13 | GO111MODULE=on GOGC=off go build -mod=vendor -v -o /miniflux-sidekick ./cmd/api/ 14 | 15 | # multistage 16 | FROM alpine:latest 17 | 18 | # https://stackoverflow.com/questions/33353532/does-alpine-linux-handle-certs-differently-than-busybox#33353762 19 | RUN apk --update upgrade && \ 20 | apk add curl ca-certificates && \ 21 | update-ca-certificates && \ 22 | rm -rf /var/cache/apk/* 23 | 24 | COPY --from=builder /miniflux-sidekick /usr/bin/miniflux-sidekick 25 | 26 | # Run the image as a non-root user 27 | RUN adduser -D mfs 28 | RUN chmod 0755 /usr/bin/miniflux-sidekick 29 | 30 | USER mfs 31 | 32 | # Run the app. CMD is required to run on Heroku 33 | CMD miniflux-sidekick -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Philipp Defner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | IMAGE_NAME := dewey-miniflux-sidekick 2 | VERSION_DOCKER := $(shell git describe --abbrev=0 --tags | sed 's/^v\(.*\)/\1/') 3 | 4 | all: install 5 | 6 | install: 7 | go install -v 8 | 9 | test: 10 | go test ./... -v 11 | 12 | image-push-staging: 13 | docker build -t docker.pkg.github.com/dewey/miniflux-sidekick/$(IMAGE_NAME):staging . 14 | docker push docker.pkg.github.com/dewey/miniflux-sidekick/$(IMAGE_NAME):staging 15 | 16 | image-push: 17 | docker build -t docker.pkg.github.com/dewey/miniflux-sidekick/$(IMAGE_NAME):latest . 18 | docker tag docker.pkg.github.com/dewey/miniflux-sidekick/$(IMAGE_NAME):latest docker.pkg.github.com/dewey/miniflux-sidekick/$(IMAGE_NAME):$(VERSION_DOCKER) 19 | docker push docker.pkg.github.com/dewey/miniflux-sidekick/$(IMAGE_NAME):latest 20 | docker push docker.pkg.github.com/dewey/miniflux-sidekick/$(IMAGE_NAME):$(VERSION_DOCKER) 21 | 22 | release: 23 | git tag -a $(VERSION) -m "Release $(VERSION)" || true 24 | git push origin $(VERSION) 25 | 26 | .PHONY: install test 27 | 28 | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Miniflux Sidekick 2 | 3 | This is a sidekick container that runs alongside [Miniflux](https://miniflux.app) which is an Open Source RSS feed reader written in Go. You can check out the source code on [GitHub](https://github.com/miniflux/miniflux). 4 | 5 | The goal is to support so called [Killfiles](https://en.wikipedia.org/wiki/Kill_file) to filter out items you don't want to see. You can think of it as an ad-blocker for your feed reader. The items are not deleted, they are just marked as *read* so you can still find all items in your feed reader if you have to. 6 | 7 | ## Features 8 | 9 | - Supports a subset of so called UseNet killfiles rules 10 | - Supports remote killfile (Share one killfile with other people, similar to ad-blocking lists) 11 | - Supports local killfiles on disk 12 | 13 | ## Supported Rules 14 | 15 | The general format of the `killfile` is: 16 | 17 | ``` 18 | ignore-article "" "" 19 | ``` 20 | 21 | ### `` 22 | 23 | This contains the URL of the feed that should be matched. It fuzzy matches the URL so if you only have one feed just use the base URL of the site. Example: `https://example.com` if the feed is on `https://example.com/rss/atom.xml`. A wildcard selector of `*` is also supported instead of the URL. 24 | 25 | ### `` Filter Expressions 26 | 27 | From the [available rule set](https://newsboat.org/releases/2.15/docs/newsboat.html#_filter_language) and attributes (`Table 5. Available Attributes`) only a small subset are supported right now. These should cover most use cases already though. 28 | 29 | **Attributes** 30 | 31 | - `title` 32 | - `content` 33 | 34 | **Comparison Operators** 35 | 36 | - `=~`: test whether regular expression matches 37 | - `!~`: logical negation of the `=~` operator 38 | - `#`: contains; this operator matches if a word is contained in a list of space-separated words (useful for matching tags, see below) 39 | - `!#`: contains not; the negation of the `#` operator 40 | 41 | 42 | 43 | ### Example 44 | 45 | Here's an example of what a `killfile` could look like with these rules. 46 | 47 | This one marks all feed items as read that have a `[Sponsor]` string in the title. 48 | ``` 49 | ignore-article "https://www.example.com" "title =~ \[Sponsor\]" 50 | ``` 51 | 52 | This one filters out all feed items that have the word `Lunar` OR `moon` in there. 53 | ``` 54 | ignore-article "https://xkcd.com/atom.xml" "title # Lunar,Moon" 55 | ``` 56 | 57 | This one filters out all feed items that have the word `lunar` OR `moon` in there and is case insensitive. 58 | ``` 59 | ignore-article "https://xkcd.com/atom.xml" "title =~ (?i)(lunAR|MOON)" 60 | ``` 61 | 62 | ### Testing rules 63 | 64 | There are tests in `filter/` that can be used to easily test rules or add new comparison operators. 65 | 66 | ## Deploy 67 | 68 | There are the environment variables that can be set. If you want to use a local file you can set `MF_KILLFILE_PATH="~/path/to/killfile"`. A local killfile always overwrites a remote one, even if the remote killfile URL is set (`MF_KILLFILE_URL`). `MF_USERNAME`, `MF_PASSWORD` and `MF_API_ENDPOINT` are your Miniflux credentials. If `MF_REFRESH_INTERVAL` isn't set it's running on every 30 minutes of every hour (`0 30 * * * *`). 69 | 70 | ``` 71 | export MF_ENVIRONMENT=development 72 | export MF_PORT=8181 73 | export MF_USERNAME=dewey 74 | export MF_PASSWORD="changeme" 75 | export MF_API_ENDPOINT=https://rss.notmyhostna.me 76 | export MF_KILLFILE_URL=https://raw.githubusercontent.com/dewey/miniflux-sidekick/master/killfile 77 | export MF_REFRESH_INTERVAL="0 30 * * * *" 78 | export MF_KILLFILE_REFRESH_HOURS=2 79 | ``` 80 | 81 | There's also a Dockerfile and Docker Compose file included so you can easily run it via `docker-compose -f docker-compose.yml up -d`. 82 | 83 | ## See Also 84 | 85 | Miniflux v2.0.25 added built-in support for [filtering rules](https://miniflux.app/docs/rules.html#filtering-rules). 86 | -------------------------------------------------------------------------------- /cmd/api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "net" 8 | "net/http" 9 | "os" 10 | "strings" 11 | "text/template" 12 | "time" 13 | 14 | "github.com/dewey/miniflux-sidekick/filter" 15 | "github.com/dewey/miniflux-sidekick/rules" 16 | "github.com/go-chi/chi" 17 | "github.com/go-kit/kit/log" 18 | "github.com/go-kit/kit/log/level" 19 | "github.com/peterbourgon/ff" 20 | "github.com/robfig/cron/v3" 21 | miniflux "miniflux.app/client" 22 | ) 23 | 24 | func main() { 25 | fs := flag.NewFlagSet("mf", flag.ExitOnError) 26 | var ( 27 | environment = fs.String("environment", "develop", "the environment we are running in") 28 | minifluxUsername = fs.String("username", "", "the username used to log into miniflux") 29 | minifluxPassword = fs.String("password", "", "the password used to log into miniflux") 30 | minifluxAPIKey = fs.String("api-key", "", "api key used for authentication") 31 | minifluxAPIEndpoint = fs.String("api-endpoint", "https://rss.notmyhostna.me", "the api of your miniflux instance") 32 | killfilePath = fs.String("killfile-path", "", "the path to the local killfile") 33 | killfileURL = fs.String("killfile-url", "", "the url to the remote killfile eg. Github gist") 34 | killfileRefreshHours = fs.Int("killfile-refresh-hours", 1, "how often the rules should be updated from local or remote config (in hours)") 35 | refreshInterval = fs.String("refresh-interval", "", "interval defining how often we check for new entries in miniflux") 36 | port = fs.String("port", "8080", "the port the miniflux sidekick is running on") 37 | logLevel = fs.String("log-level", "", "the level to filter logs at eg. debug, info, warn, error") 38 | ) 39 | 40 | ff.Parse(fs, os.Args[1:], 41 | ff.WithConfigFileFlag("config"), 42 | ff.WithConfigFileParser(ff.PlainParser), 43 | ff.WithEnvVarPrefix("MF"), 44 | ) 45 | 46 | if *environment == "" { 47 | panic("environment can't be empty") 48 | } 49 | 50 | l := log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr)) 51 | switch strings.ToLower(*logLevel) { 52 | case "debug": 53 | l = level.NewFilter(l, level.AllowDebug()) 54 | case "info": 55 | l = level.NewFilter(l, level.AllowInfo()) 56 | case "warn": 57 | l = level.NewFilter(l, level.AllowWarn()) 58 | case "error": 59 | l = level.NewFilter(l, level.AllowError()) 60 | default: 61 | switch strings.ToLower(*environment) { 62 | case "development": 63 | l = level.NewFilter(l, level.AllowDebug()) 64 | case "prod": 65 | l = level.NewFilter(l, level.AllowError()) 66 | } 67 | } 68 | l = log.With(l, "ts", log.DefaultTimestampUTC, "caller", log.DefaultCaller) 69 | 70 | var client *miniflux.Client 71 | if *minifluxUsername != "" && *minifluxPassword != "" { 72 | client = miniflux.New(*minifluxAPIEndpoint, *minifluxUsername, *minifluxPassword) 73 | } else if *minifluxAPIKey != "" { 74 | client = miniflux.New(*minifluxAPIEndpoint, *minifluxAPIKey) 75 | } else { 76 | level.Error(l).Log("err", errors.New("api endpoint, username and password or api key need to be provided")) 77 | return 78 | } 79 | u, err := client.Me() 80 | if err != nil { 81 | level.Error(l).Log("err", err) 82 | return 83 | } 84 | level.Info(l).Log("msg", "user successfully logged in", "username", u.Username, "user_id", u.ID, "is_admin", u.IsAdmin) 85 | 86 | var t = &http.Transport{ 87 | Dial: (&net.Dialer{ 88 | Timeout: 5 * time.Second, 89 | }).Dial, 90 | TLSHandshakeTimeout: 5 * time.Second, 91 | } 92 | var c = &http.Client{ 93 | Timeout: time.Second * 10, 94 | Transport: t, 95 | } 96 | 97 | // We parse our rules from disk or from an provided endpoint 98 | var rr rules.Repository 99 | if *killfilePath != "" { 100 | level.Info(l).Log("msg", "using a local killfile", "path", *killfilePath) 101 | localRepo, err := rules.NewLocalRepository() 102 | if err != nil { 103 | level.Error(l).Log("err", err) 104 | return 105 | } 106 | parsedRules, err := localRepo.FetchRules(*killfilePath) 107 | if err != nil { 108 | level.Error(l).Log("err", err) 109 | return 110 | } 111 | localRepo.SetCachedRules(parsedRules) 112 | rr = localRepo 113 | } 114 | // A local rule set always trumps a remote one 115 | if *killfileURL != "" && *killfilePath == "" { 116 | level.Info(l).Log("msg", "using a remote killfile") 117 | githubRepo, err := rules.NewGithubRepository(c) 118 | if err != nil { 119 | level.Error(l).Log("err", err) 120 | return 121 | } 122 | parsedRules, err := githubRepo.FetchRules(*killfileURL) 123 | if err != nil { 124 | level.Error(l).Log("err", err) 125 | return 126 | } 127 | // Fill cache when fetched first 128 | githubRepo.SetCachedRules(parsedRules) 129 | rr = githubRepo 130 | 131 | if *killfileRefreshHours != 0 { 132 | dur, err := time.ParseDuration(fmt.Sprintf("%dh", *killfileRefreshHours)) 133 | if err != nil { 134 | level.Error(l).Log("err", err) 135 | return 136 | } 137 | ticker := time.NewTicker(dur) 138 | go func() { 139 | for { 140 | select { 141 | case <-ticker.C: 142 | if err := githubRepo.RefreshRules(*killfileURL); err != nil { 143 | level.Error(l).Log("err", err) 144 | } 145 | } 146 | } 147 | }() 148 | } 149 | } 150 | 151 | filterService := filter.NewService(l, client, rr) 152 | 153 | cron := cron.New() 154 | // Set a fallback, documented in README 155 | if *refreshInterval == "" { 156 | *refreshInterval = "*/5 * * * *" 157 | level.Info(l).Log("msg", "set fallback interval as non provided", "env", *environment, "interval_cron", *refreshInterval) 158 | } 159 | switch strings.ToLower(*environment) { 160 | case "development": 161 | level.Info(l).Log("msg", "running filter job in simulation mode", "env", *environment, "interval_cron", *refreshInterval) 162 | filterService.RunFilterJob(true) 163 | case "prod": 164 | level.Info(l).Log("msg", "running filter job in destructive mode", "env", *environment, "interval_cron", *refreshInterval) 165 | _, err := cron.AddJob(*refreshInterval, filterService) 166 | if err != nil { 167 | level.Error(l).Log("msg", "error adding cron job to scheduler", "err", err) 168 | } 169 | cron.Start() 170 | for _, e := range cron.Entries() { 171 | level.Info(l).Log("msg", "cron job entry scheduled", "id", e.ID, "next_execution", e.Next) 172 | } 173 | } 174 | 175 | // Set up HTTP API 176 | r := chi.NewRouter() 177 | 178 | tmpl, err := template.New("rules").Parse(` 179 | 180 | miniflux-sidekick 181 | 182 | 183 |

Currently active rules

184 | 185 | 186 | 187 | 188 | 189 | 190 | {{range .}} 191 | 192 | 193 | 194 | 195 | {{end}} 196 |
CommandURLFilter Expression
{{ .Command }}{{ .URL }}{{ .FilterExpression }}
197 | 198 | `) 199 | if err != nil { 200 | level.Error(l).Log("err", err) 201 | return 202 | } 203 | 204 | r.Get("/", func(w http.ResponseWriter, r *http.Request) { 205 | tmpl.Execute(w, rr.Rules()) 206 | }) 207 | 208 | level.Info(l).Log("msg", fmt.Sprintf("miniflux-sidekick api is running on :%s", *port), "environment", *environment) 209 | 210 | // Set up webserver and and set max file limit to 50MB 211 | err = http.ListenAndServe(fmt.Sprintf(":%s", *port), r) 212 | if err != nil { 213 | level.Error(l).Log("err", err) 214 | return 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | app: 4 | image: ghcr.io/dewey/miniflux-sidekick/dewey-miniflux-sidekick:latest 5 | ports: 6 | - "8080:8080" 7 | environment: 8 | - MF_ENVIRONMENT=development 9 | - MF_PORT=8080 10 | - MF_USERNAME=dewey 11 | - MF_PASSWORD="changeme" 12 | - MF_API_ENDPOINT=https://rss.notmyhostna.me 13 | - MF_KILLFILE_URL=https://raw.githubusercontent.com/dewey/miniflux-sidekick/master/killfile 14 | - MF_KILLFILE_REFRESH_HOURS=5 15 | - MF_REFRESH_INTERVAL=*/5 * * * * 16 | -------------------------------------------------------------------------------- /filter/service.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | 7 | "github.com/dewey/miniflux-sidekick/rules" 8 | "github.com/go-kit/kit/log" 9 | "github.com/go-kit/kit/log/level" 10 | miniflux "miniflux.app/client" 11 | ) 12 | 13 | // Service is an interface for a filter service 14 | type Service interface { 15 | RunFilterJob(simulation bool) 16 | Run() 17 | } 18 | 19 | type service struct { 20 | rulesRepository rules.Repository 21 | client *miniflux.Client 22 | l log.Logger 23 | } 24 | 25 | // NewService initializes a new filter service 26 | func NewService(l log.Logger, c *miniflux.Client, rr rules.Repository) Service { 27 | return &service{ 28 | rulesRepository: rr, 29 | client: c, 30 | l: l, 31 | } 32 | } 33 | 34 | func (s *service) Run() { 35 | s.RunFilterJob(false) 36 | } 37 | 38 | var filterEntryRegex = regexp.MustCompile(`(\w+?) (\S+?) (.+)`) 39 | 40 | func (s *service) RunFilterJob(simulation bool) { 41 | // Fetch all feeds. 42 | f, err := s.client.Feeds() 43 | if err != nil { 44 | level.Error(s.l).Log("err", err) 45 | return 46 | } 47 | for _, feed := range f { 48 | // Check if the feed matches one of our rules 49 | var found bool 50 | for _, rule := range s.rulesRepository.Rules() { 51 | // Also support the wildcard selector 52 | if rule.URL == "*" { 53 | found = true 54 | } 55 | if strings.Contains(feed.FeedURL, rule.URL) { 56 | found = true 57 | } 58 | } 59 | if !found { 60 | continue 61 | } 62 | 63 | // We then get all the unread entries of the feed that matches our rule 64 | entries, err := s.client.FeedEntries(feed.ID, &miniflux.Filter{ 65 | Status: miniflux.EntryStatusUnread, 66 | }) 67 | if err != nil { 68 | level.Error(s.l).Log("err", err) 69 | continue 70 | } 71 | 72 | // We then check if the entry title matches a rule, if it matches we set it to "read" so we don't see it any more 73 | var matchedEntries []int64 74 | for _, entry := range entries.Entries { 75 | if s.evaluateRules(entry) { 76 | level.Info(s.l).Log("msg", "entry matches rules in the killfile", "entry_id", entry.ID, "feed_id", feed.ID) 77 | matchedEntries = append(matchedEntries, entry.ID) 78 | } 79 | } 80 | if simulation { 81 | for _, me := range matchedEntries { 82 | e, err := s.client.Entry(me) 83 | if err != nil { 84 | level.Error(s.l).Log("err", err) 85 | return 86 | } 87 | level.Info(s.l).Log("msg", "would set status to read", "entry_id", me, "entry_title", e.Title) 88 | } 89 | } else { 90 | for _, me := range matchedEntries { 91 | level.Info(s.l).Log("msg", "set status to read", "entry_id", me) 92 | if err := s.client.UpdateEntries([]int64{me}, miniflux.EntryStatusRead); err != nil { 93 | level.Error(s.l).Log("msg", "error on updating the feed entries", "ids", me, "err", err) 94 | return 95 | } 96 | } 97 | } 98 | if len(matchedEntries) > 0 { 99 | level.Info(s.l).Log("msg", "marked all matched feed items as read", "affected", len(matchedEntries)) 100 | } 101 | } 102 | } 103 | 104 | // evaluateRules checks a feed items against the available rules. It returns wheater this entry should be killed or not. 105 | func (s service) evaluateRules(entry *miniflux.Entry) bool { 106 | var shouldKill bool 107 | for _, rule := range s.rulesRepository.Rules() { 108 | tokens := filterEntryRegex.FindStringSubmatch(rule.FilterExpression) 109 | if tokens == nil || len(tokens) != 4 { 110 | level.Error(s.l).Log("err", "invalid filter expression", "expression", rule.FilterExpression) 111 | continue 112 | } 113 | // We set the string we want to compare against (https://newsboat.org/releases/2.15/docs/newsboat.html#_filter_language are supported in the killfile format) 114 | var entryTarget string 115 | switch tokens[1] { 116 | case "title": 117 | entryTarget = entry.Title 118 | case "description": 119 | entryTarget = entry.Content 120 | } 121 | 122 | // We check what kind of comparator was given 123 | switch tokens[2] { 124 | case "=~", "!~": 125 | invertFilter := tokens[2][0] == '!' 126 | 127 | matched, err := regexp.MatchString(tokens[3], entryTarget) 128 | if err != nil { 129 | level.Error(s.l).Log("err", err) 130 | } 131 | 132 | if matched && !invertFilter || !matched && invertFilter { 133 | shouldKill = true 134 | } 135 | case "#", "!#": 136 | invertFilter := tokens[2][0] == '!' 137 | 138 | var containsTerm bool 139 | blacklistTokens := strings.Split(tokens[3], ",") 140 | for _, t := range blacklistTokens { 141 | if strings.Contains(entryTarget, t) { 142 | containsTerm = true 143 | break 144 | } 145 | } 146 | if containsTerm && !invertFilter || !containsTerm && invertFilter { 147 | shouldKill = true 148 | } 149 | } 150 | } 151 | return shouldKill 152 | } 153 | -------------------------------------------------------------------------------- /filter/service_test.go: -------------------------------------------------------------------------------- 1 | package filter 2 | 3 | import ( 4 | "github.com/dewey/miniflux-sidekick/rules" 5 | "github.com/go-kit/kit/log" 6 | miniflux "miniflux.app/client" 7 | "testing" 8 | ) 9 | 10 | func TestEvaluateRules(t *testing.T) { 11 | type mockService struct { 12 | rules []rules.Rule 13 | l log.Logger 14 | } 15 | 16 | tests := []struct { 17 | name string 18 | rules []rules.Rule 19 | args *miniflux.Entry 20 | want bool 21 | }{ 22 | { 23 | name: "Entry contains string", 24 | rules: []rules.Rule{ 25 | { 26 | Command: "ignore-article", 27 | URL: "http://example.com/feed.xml", 28 | FilterExpression: "title # Moon", 29 | }, 30 | }, 31 | args: &miniflux.Entry{ 32 | Title: "Moon entry", 33 | }, 34 | want: true, 35 | }, 36 | { 37 | name: "Entry contains string", 38 | rules: []rules.Rule{ 39 | { 40 | Command: "ignore-article", 41 | URL: "http://example.com/feed.xml", 42 | FilterExpression: "title # Moon", 43 | }, 44 | }, 45 | args: &miniflux.Entry{ 46 | Title: "Sun entry", 47 | }, 48 | want: false, 49 | }, 50 | { 51 | name: "Entry contains string, matched with Regexp", 52 | rules: []rules.Rule{ 53 | { 54 | Command: "ignore-article", 55 | URL: "http://example.com/feed.xml", 56 | FilterExpression: "title =~ [Sponsor]", 57 | }, 58 | }, 59 | args: &miniflux.Entry{ 60 | Title: "[Sponsor] Sun entry", 61 | }, 62 | want: true, 63 | }, 64 | { 65 | name: "Entry doesn't string, matched with Regexp", 66 | rules: []rules.Rule{ 67 | { 68 | Command: "ignore-article", 69 | URL: "http://example.com/feed.xml", 70 | FilterExpression: `title =~ \[Sponsor\]`, 71 | }, 72 | }, 73 | args: &miniflux.Entry{ 74 | Title: "[SponSomethingElsesor] Sun entry", 75 | }, 76 | want: false, 77 | }, 78 | { 79 | name: "Entry doesn't string, matched with Regexp, ignore case", 80 | rules: []rules.Rule{ 81 | { 82 | Command: "ignore-article", 83 | URL: "http://example.com/feed.xml", 84 | FilterExpression: "title =~ (?i)(Podcast|scooter)", 85 | }, 86 | }, 87 | args: &miniflux.Entry{ 88 | Title: "podcast", 89 | }, 90 | want: true, 91 | }, 92 | { 93 | name: "Entry doesn't string, matched with Regexp, ignore case", 94 | rules: []rules.Rule{ 95 | { 96 | Command: "ignore-article", 97 | URL: "http://example.com/feed.xml", 98 | FilterExpression: "title =~ (?i)(Podcast|scooter)", 99 | }, 100 | }, 101 | args: &miniflux.Entry{ 102 | Title: "SCOOTER", 103 | }, 104 | want: true, 105 | }, 106 | { 107 | name: "Entry doesn't string, matched with Regexp, respect case", 108 | rules: []rules.Rule{ 109 | { 110 | Command: "ignore-article", 111 | URL: "http://example.com/feed.xml", 112 | FilterExpression: "title =~ (Podcast)", 113 | }, 114 | }, 115 | args: &miniflux.Entry{ 116 | Title: "podcast", 117 | }, 118 | want: false, 119 | }, 120 | } 121 | for _, tt := range tests { 122 | t.Run(tt.name, func(t *testing.T) { 123 | localMockRepository, err := rules.NewLocalRepository() 124 | if err != nil { 125 | t.Fatal(err) 126 | } 127 | 128 | s := service{ 129 | rulesRepository: localMockRepository, 130 | } 131 | s.rulesRepository.SetCachedRules(tt.rules) 132 | if got := s.evaluateRules(tt.args); got != tt.want { 133 | t.Errorf("evaluateRules() = %v, want %v", got, tt.want) 134 | } 135 | }) 136 | } 137 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dewey/miniflux-sidekick 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/go-chi/chi v4.0.2+incompatible 7 | github.com/go-kit/kit v0.9.0 8 | github.com/go-logfmt/logfmt v0.4.0 // indirect 9 | github.com/go-stack/stack v1.8.0 // indirect 10 | github.com/peterbourgon/ff v1.6.0 11 | github.com/robfig/cron/v3 v3.0.0 12 | miniflux.app v0.0.0-20200718020908-1b5f217e9c4e 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= 4 | github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= 5 | github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U= 6 | github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= 7 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 8 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 9 | github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs= 10 | github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= 11 | github.com/go-kit/kit v0.9.0 h1:wDJmvq38kDhkVxi50ni9ykkdUr1PKgqKOoi01fa0Mdk= 12 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 13 | github.com/go-logfmt/logfmt v0.4.0 h1:MP4Eh7ZCb31lleYCFuwm0oe4/YGak+5l1vA2NOE80nA= 14 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 15 | github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= 16 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 17 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 18 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 19 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 20 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 21 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 22 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 23 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 24 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 25 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 26 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 27 | github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 28 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= 29 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 30 | github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 31 | github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs= 32 | github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= 33 | github.com/peterbourgon/ff v1.6.0 h1:DNnSOwtqmHfQ/yLgdOvtN4eFzP4ps+IjNhUEW9/ZkIg= 34 | github.com/peterbourgon/ff v1.6.0/go.mod h1:8rO4i98n/oYmyP28qiK6V4jGB85nMNVr+qwSErTwFrs= 35 | github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= 36 | github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E= 37 | github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= 38 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 39 | github.com/tdewolff/minify/v2 v2.7.4/go.mod h1:BkDSm8aMMT0ALGmpt7j3Ra7nLUgZL0qhyrAHXwxcy5w= 40 | github.com/tdewolff/parse/v2 v2.4.2/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho= 41 | github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= 42 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 43 | golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 44 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 45 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 46 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 47 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 48 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 49 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 50 | golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 51 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 52 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 53 | golang.org/x/sys v0.0.0-20181031143558-9b800f95dbbc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 54 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 55 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 56 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 57 | golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 58 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 59 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 60 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 61 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= 62 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 63 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 64 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 65 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 66 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 67 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 68 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 69 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 70 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 71 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 72 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 73 | gopkg.in/square/go-jose.v2 v2.5.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= 74 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 75 | miniflux.app v0.0.0-20200718020908-1b5f217e9c4e h1:vWxPMi3dPNITQBgMMUFbrzVlPgz6bSsNIleMfbWi7Pw= 76 | miniflux.app v0.0.0-20200718020908-1b5f217e9c4e/go.mod h1:XLf2SN52eyy+LErMeUt1GIlnD2GoKbo1HCRusT6ZVCg= 77 | -------------------------------------------------------------------------------- /killfile: -------------------------------------------------------------------------------- 1 | ignore-article "https://www.example.com" "title =~ \[Weekly\sSponsor\]" 2 | ignore-article "https://xkcd.com/atom.xml" "title # Lunar,Moon" 3 | ignore-article * "title =~ \[Sponsor\]" 4 | -------------------------------------------------------------------------------- /rules/github_repository.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "bufio" 5 | "net/http" 6 | "sync" 7 | ) 8 | 9 | type githubRepository struct { 10 | c *http.Client 11 | mutex sync.RWMutex 12 | cachedRules []Rule 13 | } 14 | 15 | // NewGithubRepository returns a newly initialized Github.com repository 16 | func NewGithubRepository(c *http.Client) (Repository, error) { 17 | return &githubRepository{ 18 | c: c, 19 | }, nil 20 | } 21 | 22 | func (r *githubRepository) Rules() []Rule { 23 | if r.cachedRules != nil { 24 | return r.cachedRules 25 | } else { 26 | return []Rule{} 27 | } 28 | } 29 | 30 | // FetchRules parses a remote killfile to get all rules 31 | func (r *githubRepository) FetchRules(location string) ([]Rule, error) { 32 | resp, err := r.c.Get(location) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | scanner := bufio.NewScanner(resp.Body) 38 | var rules []Rule 39 | for scanner.Scan() { 40 | matches := reRuleSplitter.FindStringSubmatch(scanner.Text()) 41 | if len(matches) == 4 { 42 | rules = append(rules, Rule{ 43 | Command: matches[1], 44 | URL: matches[2], 45 | FilterExpression: matches[3], 46 | }) 47 | } 48 | } 49 | 50 | return rules, scanner.Err() 51 | } 52 | 53 | // RefreshRules fetches the new rules and updates the local cache 54 | func (r *githubRepository) RefreshRules(location string) error { 55 | rules, err := r.FetchRules(location) 56 | if err != nil { 57 | return err 58 | } 59 | r.SetCachedRules(rules) 60 | return nil 61 | } 62 | 63 | // SetCachedRules sets the in-memory cache 64 | func (r *githubRepository) SetCachedRules(rules []Rule) { 65 | r.mutex.Lock() 66 | r.cachedRules = rules 67 | r.mutex.Unlock() 68 | } 69 | -------------------------------------------------------------------------------- /rules/local_repository.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | "sync" 7 | ) 8 | 9 | type localRepository struct { 10 | mutex sync.RWMutex 11 | cachedRules []Rule 12 | } 13 | 14 | // NewLocalRepository returns a newly initialized rules repository 15 | func NewLocalRepository() (Repository, error) { 16 | return &localRepository{}, nil 17 | } 18 | 19 | func (r *localRepository) Rules() []Rule { 20 | if r.cachedRules != nil { 21 | return r.cachedRules 22 | } else { 23 | return []Rule{} 24 | } 25 | } 26 | 27 | // FetchRules parses a local killfile to get all rules 28 | func (r *localRepository) FetchRules(location string) ([]Rule, error) { 29 | file, err := os.Open(location) 30 | if err != nil { 31 | return nil, err 32 | } 33 | defer file.Close() 34 | 35 | var rules []Rule 36 | scanner := bufio.NewScanner(file) 37 | for scanner.Scan() { 38 | matches := reRuleSplitter.FindStringSubmatch(scanner.Text()) 39 | if len(matches) == 4 { 40 | rules = append(rules, Rule{ 41 | Command: matches[1], 42 | URL: matches[2], 43 | FilterExpression: matches[3], 44 | }) 45 | } 46 | } 47 | return rules, scanner.Err() 48 | } 49 | 50 | // RefreshRules for local repositories isn't implemented yet. 51 | func (r *localRepository) RefreshRules(location string) error { 52 | return nil 53 | } 54 | 55 | // SetCachedRules sets the in-memory cache 56 | func (r *localRepository) SetCachedRules(rules []Rule) { 57 | r.mutex.Lock() 58 | r.cachedRules = rules 59 | r.mutex.Unlock() 60 | } 61 | -------------------------------------------------------------------------------- /rules/rules.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "regexp" 5 | ) 6 | 7 | var ( 8 | reRuleSplitter = regexp.MustCompile(`(.+?)\s\"?(.+?)\"?\s\"(.+)\"`) 9 | ) 10 | 11 | // Repository defines the interface for the rules repository 12 | type Repository interface { 13 | // FetchRules fetches the list of rules from a file or remote location 14 | FetchRules(location string) ([]Rule, error) 15 | 16 | // RefreshRules refreshes the in-memory cached rules 17 | RefreshRules(location string) error 18 | 19 | // SetCachedRules([]Rule) 20 | SetCachedRules(rules []Rule) 21 | 22 | // Rules returns rules from cache 23 | Rules() []Rule 24 | } 25 | 26 | // Rule contains a killfile rule. There's no official standard so we implement these rules https://newsboat.org/releases/2.15/docs/newsboat.html#_killfiles 27 | type Rule struct { 28 | Command string 29 | URL string 30 | FilterExpression string 31 | } 32 | -------------------------------------------------------------------------------- /run_develop.sh: -------------------------------------------------------------------------------- 1 | source develop.env 2 | 3 | function cleanup() { 4 | rm -f miniflux-sidekick 5 | } 6 | trap cleanup EXIT 7 | 8 | # Compile Go 9 | GO111MODULE=on GOGC=off go build -mod=vendor -v -o miniflux-sidekick ./cmd/api/ 10 | ./miniflux-sidekick 11 | -------------------------------------------------------------------------------- /vendor/github.com/go-chi/chi/.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.sw? 3 | .vscode 4 | -------------------------------------------------------------------------------- /vendor/github.com/go-chi/chi/.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.10.x 5 | - 1.11.x 6 | - 1.12.x 7 | 8 | script: 9 | - go get -d -t ./... 10 | - go vet ./... 11 | - go test ./... 12 | - > 13 | go_version=$(go version); 14 | if [ ${go_version:13:4} = "1.12" ]; then 15 | go get -u golang.org/x/tools/cmd/goimports; 16 | goimports -d -e ./ | grep '.*' && { echo; echo "Aborting due to non-empty goimports output."; exit 1; } || :; 17 | fi 18 | 19 | -------------------------------------------------------------------------------- /vendor/github.com/go-chi/chi/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v4.0.0 (2019-01-10) 4 | 5 | - chi v4 requires Go 1.10.3+ (or Go 1.9.7+) - we have deprecated support for Go 1.7 and 1.8 6 | - router: respond with 404 on router with no routes (#362) 7 | - router: additional check to ensure wildcard is at the end of a url pattern (#333) 8 | - middleware: deprecate use of http.CloseNotifier (#347) 9 | - middleware: fix RedirectSlashes to include query params on redirect (#334) 10 | - History of changes: see https://github.com/go-chi/chi/compare/v3.3.4...v4.0.0 11 | 12 | 13 | ## v3.3.4 (2019-01-07) 14 | 15 | - Minor middleware improvements. No changes to core library/router. Moving v3 into its 16 | - own branch as a version of chi for Go 1.7, 1.8, 1.9, 1.10, 1.11 17 | - History of changes: see https://github.com/go-chi/chi/compare/v3.3.3...v3.3.4 18 | 19 | 20 | ## v3.3.3 (2018-08-27) 21 | 22 | - Minor release 23 | - See https://github.com/go-chi/chi/compare/v3.3.2...v3.3.3 24 | 25 | 26 | ## v3.3.2 (2017-12-22) 27 | 28 | - Support to route trailing slashes on mounted sub-routers (#281) 29 | - middleware: new `ContentCharset` to check matching charsets. Thank you 30 | @csucu for your community contribution! 31 | 32 | 33 | ## v3.3.1 (2017-11-20) 34 | 35 | - middleware: new `AllowContentType` handler for explicit whitelist of accepted request Content-Types 36 | - middleware: new `SetHeader` handler for short-hand middleware to set a response header key/value 37 | - Minor bug fixes 38 | 39 | 40 | ## v3.3.0 (2017-10-10) 41 | 42 | - New chi.RegisterMethod(method) to add support for custom HTTP methods, see _examples/custom-method for usage 43 | - Deprecated LINK and UNLINK methods from the default list, please use `chi.RegisterMethod("LINK")` and `chi.RegisterMethod("UNLINK")` in an `init()` function 44 | 45 | 46 | ## v3.2.1 (2017-08-31) 47 | 48 | - Add new `Match(rctx *Context, method, path string) bool` method to `Routes` interface 49 | and `Mux`. Match searches the mux's routing tree for a handler that matches the method/path 50 | - Add new `RouteMethod` to `*Context` 51 | - Add new `Routes` pointer to `*Context` 52 | - Add new `middleware.GetHead` to route missing HEAD requests to GET handler 53 | - Updated benchmarks (see README) 54 | 55 | 56 | ## v3.1.5 (2017-08-02) 57 | 58 | - Setup golint and go vet for the project 59 | - As per golint, we've redefined `func ServerBaseContext(h http.Handler, baseCtx context.Context) http.Handler` 60 | to `func ServerBaseContext(baseCtx context.Context, h http.Handler) http.Handler` 61 | 62 | 63 | ## v3.1.0 (2017-07-10) 64 | 65 | - Fix a few minor issues after v3 release 66 | - Move `docgen` sub-pkg to https://github.com/go-chi/docgen 67 | - Move `render` sub-pkg to https://github.com/go-chi/render 68 | - Add new `URLFormat` handler to chi/middleware sub-pkg to make working with url mime 69 | suffixes easier, ie. parsing `/articles/1.json` and `/articles/1.xml`. See comments in 70 | https://github.com/go-chi/chi/blob/master/middleware/url_format.go for example usage. 71 | 72 | 73 | ## v3.0.0 (2017-06-21) 74 | 75 | - Major update to chi library with many exciting updates, but also some *breaking changes* 76 | - URL parameter syntax changed from `/:id` to `/{id}` for even more flexible routing, such as 77 | `/articles/{month}-{day}-{year}-{slug}`, `/articles/{id}`, and `/articles/{id}.{ext}` on the 78 | same router 79 | - Support for regexp for routing patterns, in the form of `/{paramKey:regExp}` for example: 80 | `r.Get("/articles/{name:[a-z]+}", h)` and `chi.URLParam(r, "name")` 81 | - Add `Method` and `MethodFunc` to `chi.Router` to allow routing definitions such as 82 | `r.Method("GET", "/", h)` which provides a cleaner interface for custom handlers like 83 | in `_examples/custom-handler` 84 | - Deprecating `mux#FileServer` helper function. Instead, we encourage users to create their 85 | own using file handler with the stdlib, see `_examples/fileserver` for an example 86 | - Add support for LINK/UNLINK http methods via `r.Method()` and `r.MethodFunc()` 87 | - Moved the chi project to its own organization, to allow chi-related community packages to 88 | be easily discovered and supported, at: https://github.com/go-chi 89 | - *NOTE:* please update your import paths to `"github.com/go-chi/chi"` 90 | - *NOTE:* chi v2 is still available at https://github.com/go-chi/chi/tree/v2 91 | 92 | 93 | ## v2.1.0 (2017-03-30) 94 | 95 | - Minor improvements and update to the chi core library 96 | - Introduced a brand new `chi/render` sub-package to complete the story of building 97 | APIs to offer a pattern for managing well-defined request / response payloads. Please 98 | check out the updated `_examples/rest` example for how it works. 99 | - Added `MethodNotAllowed(h http.HandlerFunc)` to chi.Router interface 100 | 101 | 102 | ## v2.0.0 (2017-01-06) 103 | 104 | - After many months of v2 being in an RC state with many companies and users running it in 105 | production, the inclusion of some improvements to the middlewares, we are very pleased to 106 | announce v2.0.0 of chi. 107 | 108 | 109 | ## v2.0.0-rc1 (2016-07-26) 110 | 111 | - Huge update! chi v2 is a large refactor targetting Go 1.7+. As of Go 1.7, the popular 112 | community `"net/context"` package has been included in the standard library as `"context"` and 113 | utilized by `"net/http"` and `http.Request` to managing deadlines, cancelation signals and other 114 | request-scoped values. We're very excited about the new context addition and are proud to 115 | introduce chi v2, a minimal and powerful routing package for building large HTTP services, 116 | with zero external dependencies. Chi focuses on idiomatic design and encourages the use of 117 | stdlib HTTP handlers and middlwares. 118 | - chi v2 deprecates its `chi.Handler` interface and requires `http.Handler` or `http.HandlerFunc` 119 | - chi v2 stores URL routing parameters and patterns in the standard request context: `r.Context()` 120 | - chi v2 lower-level routing context is accessible by `chi.RouteContext(r.Context()) *chi.Context`, 121 | which provides direct access to URL routing parameters, the routing path and the matching 122 | routing patterns. 123 | - Users upgrading from chi v1 to v2, need to: 124 | 1. Update the old chi.Handler signature, `func(ctx context.Context, w http.ResponseWriter, r *http.Request)` to 125 | the standard http.Handler: `func(w http.ResponseWriter, r *http.Request)` 126 | 2. Use `chi.URLParam(r *http.Request, paramKey string) string` 127 | or `URLParamFromCtx(ctx context.Context, paramKey string) string` to access a url parameter value 128 | 129 | 130 | ## v1.0.0 (2016-07-01) 131 | 132 | - Released chi v1 stable https://github.com/go-chi/chi/tree/v1.0.0 for Go 1.6 and older. 133 | 134 | 135 | ## v0.9.0 (2016-03-31) 136 | 137 | - Reuse context objects via sync.Pool for zero-allocation routing [#33](https://github.com/go-chi/chi/pull/33) 138 | - BREAKING NOTE: due to subtle API changes, previously `chi.URLParams(ctx)["id"]` used to access url parameters 139 | has changed to: `chi.URLParam(ctx, "id")` 140 | -------------------------------------------------------------------------------- /vendor/github.com/go-chi/chi/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Prerequisites 4 | 5 | 1. [Install Go][go-install]. 6 | 2. Download the sources and switch the working directory: 7 | 8 | ```bash 9 | go get -u -d github.com/go-chi/chi 10 | cd $GOPATH/src/github.com/go-chi/chi 11 | ``` 12 | 13 | ## Submitting a Pull Request 14 | 15 | A typical workflow is: 16 | 17 | 1. [Fork the repository.][fork] [This tip maybe also helpful.][go-fork-tip] 18 | 2. [Create a topic branch.][branch] 19 | 3. Add tests for your change. 20 | 4. Run `go test`. If your tests pass, return to the step 3. 21 | 5. Implement the change and ensure the steps from the previous step pass. 22 | 6. Run `goimports -w .`, to ensure the new code conforms to Go formatting guideline. 23 | 7. [Add, commit and push your changes.][git-help] 24 | 8. [Submit a pull request.][pull-req] 25 | 26 | [go-install]: https://golang.org/doc/install 27 | [go-fork-tip]: http://blog.campoy.cat/2014/03/github-and-go-forking-pull-requests-and.html 28 | [fork]: https://help.github.com/articles/fork-a-repo 29 | [branch]: http://learn.github.com/p/branching.html 30 | [git-help]: https://guides.github.com 31 | [pull-req]: https://help.github.com/articles/using-pull-requests 32 | -------------------------------------------------------------------------------- /vendor/github.com/go-chi/chi/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-present Peter Kieltyka (https://github.com/pkieltyka), Google Inc. 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /vendor/github.com/go-chi/chi/chain.go: -------------------------------------------------------------------------------- 1 | package chi 2 | 3 | import "net/http" 4 | 5 | // Chain returns a Middlewares type from a slice of middleware handlers. 6 | func Chain(middlewares ...func(http.Handler) http.Handler) Middlewares { 7 | return Middlewares(middlewares) 8 | } 9 | 10 | // Handler builds and returns a http.Handler from the chain of middlewares, 11 | // with `h http.Handler` as the final handler. 12 | func (mws Middlewares) Handler(h http.Handler) http.Handler { 13 | return &ChainHandler{mws, h, chain(mws, h)} 14 | } 15 | 16 | // HandlerFunc builds and returns a http.Handler from the chain of middlewares, 17 | // with `h http.Handler` as the final handler. 18 | func (mws Middlewares) HandlerFunc(h http.HandlerFunc) http.Handler { 19 | return &ChainHandler{mws, h, chain(mws, h)} 20 | } 21 | 22 | // ChainHandler is a http.Handler with support for handler composition and 23 | // execution. 24 | type ChainHandler struct { 25 | Middlewares Middlewares 26 | Endpoint http.Handler 27 | chain http.Handler 28 | } 29 | 30 | func (c *ChainHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 31 | c.chain.ServeHTTP(w, r) 32 | } 33 | 34 | // chain builds a http.Handler composed of an inline middleware stack and endpoint 35 | // handler in the order they are passed. 36 | func chain(middlewares []func(http.Handler) http.Handler, endpoint http.Handler) http.Handler { 37 | // Return ahead of time if there aren't any middlewares for the chain 38 | if len(middlewares) == 0 { 39 | return endpoint 40 | } 41 | 42 | // Wrap the end handler with the middleware chain 43 | h := middlewares[len(middlewares)-1](endpoint) 44 | for i := len(middlewares) - 2; i >= 0; i-- { 45 | h = middlewares[i](h) 46 | } 47 | 48 | return h 49 | } 50 | -------------------------------------------------------------------------------- /vendor/github.com/go-chi/chi/chi.go: -------------------------------------------------------------------------------- 1 | // 2 | // Package chi is a small, idiomatic and composable router for building HTTP services. 3 | // 4 | // chi requires Go 1.7 or newer. 5 | // 6 | // Example: 7 | // package main 8 | // 9 | // import ( 10 | // "net/http" 11 | // 12 | // "github.com/go-chi/chi" 13 | // "github.com/go-chi/chi/middleware" 14 | // ) 15 | // 16 | // func main() { 17 | // r := chi.NewRouter() 18 | // r.Use(middleware.Logger) 19 | // r.Use(middleware.Recoverer) 20 | // 21 | // r.Get("/", func(w http.ResponseWriter, r *http.Request) { 22 | // w.Write([]byte("root.")) 23 | // }) 24 | // 25 | // http.ListenAndServe(":3333", r) 26 | // } 27 | // 28 | // See github.com/go-chi/chi/_examples/ for more in-depth examples. 29 | // 30 | // URL patterns allow for easy matching of path components in HTTP 31 | // requests. The matching components can then be accessed using 32 | // chi.URLParam(). All patterns must begin with a slash. 33 | // 34 | // A simple named placeholder {name} matches any sequence of characters 35 | // up to the next / or the end of the URL. Trailing slashes on paths must 36 | // be handled explicitly. 37 | // 38 | // A placeholder with a name followed by a colon allows a regular 39 | // expression match, for example {number:\\d+}. The regular expression 40 | // syntax is Go's normal regexp RE2 syntax, except that regular expressions 41 | // including { or } are not supported, and / will never be 42 | // matched. An anonymous regexp pattern is allowed, using an empty string 43 | // before the colon in the placeholder, such as {:\\d+} 44 | // 45 | // The special placeholder of asterisk matches the rest of the requested 46 | // URL. Any trailing characters in the pattern are ignored. This is the only 47 | // placeholder which will match / characters. 48 | // 49 | // Examples: 50 | // "/user/{name}" matches "/user/jsmith" but not "/user/jsmith/info" or "/user/jsmith/" 51 | // "/user/{name}/info" matches "/user/jsmith/info" 52 | // "/page/*" matches "/page/intro/latest" 53 | // "/page/*/index" also matches "/page/intro/latest" 54 | // "/date/{yyyy:\\d\\d\\d\\d}/{mm:\\d\\d}/{dd:\\d\\d}" matches "/date/2017/04/01" 55 | // 56 | package chi 57 | 58 | import "net/http" 59 | 60 | // NewRouter returns a new Mux object that implements the Router interface. 61 | func NewRouter() *Mux { 62 | return NewMux() 63 | } 64 | 65 | // Router consisting of the core routing methods used by chi's Mux, 66 | // using only the standard net/http. 67 | type Router interface { 68 | http.Handler 69 | Routes 70 | 71 | // Use appends one of more middlewares onto the Router stack. 72 | Use(middlewares ...func(http.Handler) http.Handler) 73 | 74 | // With adds inline middlewares for an endpoint handler. 75 | With(middlewares ...func(http.Handler) http.Handler) Router 76 | 77 | // Group adds a new inline-Router along the current routing 78 | // path, with a fresh middleware stack for the inline-Router. 79 | Group(fn func(r Router)) Router 80 | 81 | // Route mounts a sub-Router along a `pattern`` string. 82 | Route(pattern string, fn func(r Router)) Router 83 | 84 | // Mount attaches another http.Handler along ./pattern/* 85 | Mount(pattern string, h http.Handler) 86 | 87 | // Handle and HandleFunc adds routes for `pattern` that matches 88 | // all HTTP methods. 89 | Handle(pattern string, h http.Handler) 90 | HandleFunc(pattern string, h http.HandlerFunc) 91 | 92 | // Method and MethodFunc adds routes for `pattern` that matches 93 | // the `method` HTTP method. 94 | Method(method, pattern string, h http.Handler) 95 | MethodFunc(method, pattern string, h http.HandlerFunc) 96 | 97 | // HTTP-method routing along `pattern` 98 | Connect(pattern string, h http.HandlerFunc) 99 | Delete(pattern string, h http.HandlerFunc) 100 | Get(pattern string, h http.HandlerFunc) 101 | Head(pattern string, h http.HandlerFunc) 102 | Options(pattern string, h http.HandlerFunc) 103 | Patch(pattern string, h http.HandlerFunc) 104 | Post(pattern string, h http.HandlerFunc) 105 | Put(pattern string, h http.HandlerFunc) 106 | Trace(pattern string, h http.HandlerFunc) 107 | 108 | // NotFound defines a handler to respond whenever a route could 109 | // not be found. 110 | NotFound(h http.HandlerFunc) 111 | 112 | // MethodNotAllowed defines a handler to respond whenever a method is 113 | // not allowed. 114 | MethodNotAllowed(h http.HandlerFunc) 115 | } 116 | 117 | // Routes interface adds two methods for router traversal, which is also 118 | // used by the `docgen` subpackage to generation documentation for Routers. 119 | type Routes interface { 120 | // Routes returns the routing tree in an easily traversable structure. 121 | Routes() []Route 122 | 123 | // Middlewares returns the list of middlewares in use by the router. 124 | Middlewares() Middlewares 125 | 126 | // Match searches the routing tree for a handler that matches 127 | // the method/path - similar to routing a http request, but without 128 | // executing the handler thereafter. 129 | Match(rctx *Context, method, path string) bool 130 | } 131 | 132 | // Middlewares type is a slice of standard middleware handlers with methods 133 | // to compose middleware chains and http.Handler's. 134 | type Middlewares []func(http.Handler) http.Handler 135 | -------------------------------------------------------------------------------- /vendor/github.com/go-chi/chi/context.go: -------------------------------------------------------------------------------- 1 | package chi 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "net/http" 7 | "strings" 8 | ) 9 | 10 | var ( 11 | // RouteCtxKey is the context.Context key to store the request context. 12 | RouteCtxKey = &contextKey{"RouteContext"} 13 | ) 14 | 15 | // Context is the default routing context set on the root node of a 16 | // request context to track route patterns, URL parameters and 17 | // an optional routing path. 18 | type Context struct { 19 | Routes Routes 20 | 21 | // Routing path/method override used during the route search. 22 | // See Mux#routeHTTP method. 23 | RoutePath string 24 | RouteMethod string 25 | 26 | // Routing pattern stack throughout the lifecycle of the request, 27 | // across all connected routers. It is a record of all matching 28 | // patterns across a stack of sub-routers. 29 | RoutePatterns []string 30 | 31 | // URLParams are the stack of routeParams captured during the 32 | // routing lifecycle across a stack of sub-routers. 33 | URLParams RouteParams 34 | 35 | // The endpoint routing pattern that matched the request URI path 36 | // or `RoutePath` of the current sub-router. This value will update 37 | // during the lifecycle of a request passing through a stack of 38 | // sub-routers. 39 | routePattern string 40 | 41 | // Route parameters matched for the current sub-router. It is 42 | // intentionally unexported so it cant be tampered. 43 | routeParams RouteParams 44 | 45 | // methodNotAllowed hint 46 | methodNotAllowed bool 47 | } 48 | 49 | // NewRouteContext returns a new routing Context object. 50 | func NewRouteContext() *Context { 51 | return &Context{} 52 | } 53 | 54 | // Reset a routing context to its initial state. 55 | func (x *Context) Reset() { 56 | x.Routes = nil 57 | x.RoutePath = "" 58 | x.RouteMethod = "" 59 | x.RoutePatterns = x.RoutePatterns[:0] 60 | x.URLParams.Keys = x.URLParams.Keys[:0] 61 | x.URLParams.Values = x.URLParams.Values[:0] 62 | 63 | x.routePattern = "" 64 | x.routeParams.Keys = x.routeParams.Keys[:0] 65 | x.routeParams.Values = x.routeParams.Values[:0] 66 | x.methodNotAllowed = false 67 | } 68 | 69 | // URLParam returns the corresponding URL parameter value from the request 70 | // routing context. 71 | func (x *Context) URLParam(key string) string { 72 | for k := len(x.URLParams.Keys) - 1; k >= 0; k-- { 73 | if x.URLParams.Keys[k] == key { 74 | return x.URLParams.Values[k] 75 | } 76 | } 77 | return "" 78 | } 79 | 80 | // RoutePattern builds the routing pattern string for the particular 81 | // request, at the particular point during routing. This means, the value 82 | // will change throughout the execution of a request in a router. That is 83 | // why its advised to only use this value after calling the next handler. 84 | // 85 | // For example, 86 | // 87 | // func Instrument(next http.Handler) http.Handler { 88 | // return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 89 | // next.ServeHTTP(w, r) 90 | // routePattern := chi.RouteContext(r.Context()).RoutePattern() 91 | // measure(w, r, routePattern) 92 | // }) 93 | // } 94 | func (x *Context) RoutePattern() string { 95 | routePattern := strings.Join(x.RoutePatterns, "") 96 | return strings.Replace(routePattern, "/*/", "/", -1) 97 | } 98 | 99 | // RouteContext returns chi's routing Context object from a 100 | // http.Request Context. 101 | func RouteContext(ctx context.Context) *Context { 102 | return ctx.Value(RouteCtxKey).(*Context) 103 | } 104 | 105 | // URLParam returns the url parameter from a http.Request object. 106 | func URLParam(r *http.Request, key string) string { 107 | if rctx := RouteContext(r.Context()); rctx != nil { 108 | return rctx.URLParam(key) 109 | } 110 | return "" 111 | } 112 | 113 | // URLParamFromCtx returns the url parameter from a http.Request Context. 114 | func URLParamFromCtx(ctx context.Context, key string) string { 115 | if rctx := RouteContext(ctx); rctx != nil { 116 | return rctx.URLParam(key) 117 | } 118 | return "" 119 | } 120 | 121 | // RouteParams is a structure to track URL routing parameters efficiently. 122 | type RouteParams struct { 123 | Keys, Values []string 124 | } 125 | 126 | // Add will append a URL parameter to the end of the route param 127 | func (s *RouteParams) Add(key, value string) { 128 | (*s).Keys = append((*s).Keys, key) 129 | (*s).Values = append((*s).Values, value) 130 | } 131 | 132 | // ServerBaseContext wraps an http.Handler to set the request context to the 133 | // `baseCtx`. 134 | func ServerBaseContext(baseCtx context.Context, h http.Handler) http.Handler { 135 | fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 136 | ctx := r.Context() 137 | baseCtx := baseCtx 138 | 139 | // Copy over default net/http server context keys 140 | if v, ok := ctx.Value(http.ServerContextKey).(*http.Server); ok { 141 | baseCtx = context.WithValue(baseCtx, http.ServerContextKey, v) 142 | } 143 | if v, ok := ctx.Value(http.LocalAddrContextKey).(net.Addr); ok { 144 | baseCtx = context.WithValue(baseCtx, http.LocalAddrContextKey, v) 145 | } 146 | 147 | h.ServeHTTP(w, r.WithContext(baseCtx)) 148 | }) 149 | return fn 150 | } 151 | 152 | // contextKey is a value for use with context.WithValue. It's used as 153 | // a pointer so it fits in an interface{} without allocation. This technique 154 | // for defining context keys was copied from Go 1.7's new use of context in net/http. 155 | type contextKey struct { 156 | name string 157 | } 158 | 159 | func (k *contextKey) String() string { 160 | return "chi context value " + k.name 161 | } 162 | -------------------------------------------------------------------------------- /vendor/github.com/go-kit/kit/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Peter Bourgon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /vendor/github.com/go-kit/kit/log/README.md: -------------------------------------------------------------------------------- 1 | # package log 2 | 3 | `package log` provides a minimal interface for structured logging in services. 4 | It may be wrapped to encode conventions, enforce type-safety, provide leveled 5 | logging, and so on. It can be used for both typical application log events, 6 | and log-structured data streams. 7 | 8 | ## Structured logging 9 | 10 | Structured logging is, basically, conceding to the reality that logs are 11 | _data_, and warrant some level of schematic rigor. Using a stricter, 12 | key/value-oriented message format for our logs, containing contextual and 13 | semantic information, makes it much easier to get insight into the 14 | operational activity of the systems we build. Consequently, `package log` is 15 | of the strong belief that "[the benefits of structured logging outweigh the 16 | minimal effort involved](https://www.thoughtworks.com/radar/techniques/structured-logging)". 17 | 18 | Migrating from unstructured to structured logging is probably a lot easier 19 | than you'd expect. 20 | 21 | ```go 22 | // Unstructured 23 | log.Printf("HTTP server listening on %s", addr) 24 | 25 | // Structured 26 | logger.Log("transport", "HTTP", "addr", addr, "msg", "listening") 27 | ``` 28 | 29 | ## Usage 30 | 31 | ### Typical application logging 32 | 33 | ```go 34 | w := log.NewSyncWriter(os.Stderr) 35 | logger := log.NewLogfmtLogger(w) 36 | logger.Log("question", "what is the meaning of life?", "answer", 42) 37 | 38 | // Output: 39 | // question="what is the meaning of life?" answer=42 40 | ``` 41 | 42 | ### Contextual Loggers 43 | 44 | ```go 45 | func main() { 46 | var logger log.Logger 47 | logger = log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr)) 48 | logger = log.With(logger, "instance_id", 123) 49 | 50 | logger.Log("msg", "starting") 51 | NewWorker(log.With(logger, "component", "worker")).Run() 52 | NewSlacker(log.With(logger, "component", "slacker")).Run() 53 | } 54 | 55 | // Output: 56 | // instance_id=123 msg=starting 57 | // instance_id=123 component=worker msg=running 58 | // instance_id=123 component=slacker msg=running 59 | ``` 60 | 61 | ### Interact with stdlib logger 62 | 63 | Redirect stdlib logger to Go kit logger. 64 | 65 | ```go 66 | import ( 67 | "os" 68 | stdlog "log" 69 | kitlog "github.com/go-kit/kit/log" 70 | ) 71 | 72 | func main() { 73 | logger := kitlog.NewJSONLogger(kitlog.NewSyncWriter(os.Stdout)) 74 | stdlog.SetOutput(kitlog.NewStdlibAdapter(logger)) 75 | stdlog.Print("I sure like pie") 76 | } 77 | 78 | // Output: 79 | // {"msg":"I sure like pie","ts":"2016/01/01 12:34:56"} 80 | ``` 81 | 82 | Or, if, for legacy reasons, you need to pipe all of your logging through the 83 | stdlib log package, you can redirect Go kit logger to the stdlib logger. 84 | 85 | ```go 86 | logger := kitlog.NewLogfmtLogger(kitlog.StdlibWriter{}) 87 | logger.Log("legacy", true, "msg", "at least it's something") 88 | 89 | // Output: 90 | // 2016/01/01 12:34:56 legacy=true msg="at least it's something" 91 | ``` 92 | 93 | ### Timestamps and callers 94 | 95 | ```go 96 | var logger log.Logger 97 | logger = log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr)) 98 | logger = log.With(logger, "ts", log.DefaultTimestampUTC, "caller", log.DefaultCaller) 99 | 100 | logger.Log("msg", "hello") 101 | 102 | // Output: 103 | // ts=2016-01-01T12:34:56Z caller=main.go:15 msg=hello 104 | ``` 105 | 106 | ## Levels 107 | 108 | Log levels are supported via the [level package](https://godoc.org/github.com/go-kit/kit/log/level). 109 | 110 | ## Supported output formats 111 | 112 | - [Logfmt](https://brandur.org/logfmt) ([see also](https://blog.codeship.com/logfmt-a-log-format-thats-easy-to-read-and-write)) 113 | - JSON 114 | 115 | ## Enhancements 116 | 117 | `package log` is centered on the one-method Logger interface. 118 | 119 | ```go 120 | type Logger interface { 121 | Log(keyvals ...interface{}) error 122 | } 123 | ``` 124 | 125 | This interface, and its supporting code like is the product of much iteration 126 | and evaluation. For more details on the evolution of the Logger interface, 127 | see [The Hunt for a Logger Interface](http://go-talks.appspot.com/github.com/ChrisHines/talks/structured-logging/structured-logging.slide#1), 128 | a talk by [Chris Hines](https://github.com/ChrisHines). 129 | Also, please see 130 | [#63](https://github.com/go-kit/kit/issues/63), 131 | [#76](https://github.com/go-kit/kit/pull/76), 132 | [#131](https://github.com/go-kit/kit/issues/131), 133 | [#157](https://github.com/go-kit/kit/pull/157), 134 | [#164](https://github.com/go-kit/kit/issues/164), and 135 | [#252](https://github.com/go-kit/kit/pull/252) 136 | to review historical conversations about package log and the Logger interface. 137 | 138 | Value-add packages and suggestions, 139 | like improvements to [the leveled logger](https://godoc.org/github.com/go-kit/kit/log/level), 140 | are of course welcome. Good proposals should 141 | 142 | - Be composable with [contextual loggers](https://godoc.org/github.com/go-kit/kit/log#With), 143 | - Not break the behavior of [log.Caller](https://godoc.org/github.com/go-kit/kit/log#Caller) in any wrapped contextual loggers, and 144 | - Be friendly to packages that accept only an unadorned log.Logger. 145 | 146 | ## Benchmarks & comparisons 147 | 148 | There are a few Go logging benchmarks and comparisons that include Go kit's package log. 149 | 150 | - [imkira/go-loggers-bench](https://github.com/imkira/go-loggers-bench) includes kit/log 151 | - [uber-common/zap](https://github.com/uber-common/zap), a zero-alloc logging library, includes a comparison with kit/log 152 | -------------------------------------------------------------------------------- /vendor/github.com/go-kit/kit/log/doc.go: -------------------------------------------------------------------------------- 1 | // Package log provides a structured logger. 2 | // 3 | // Structured logging produces logs easily consumed later by humans or 4 | // machines. Humans might be interested in debugging errors, or tracing 5 | // specific requests. Machines might be interested in counting interesting 6 | // events, or aggregating information for off-line processing. In both cases, 7 | // it is important that the log messages are structured and actionable. 8 | // Package log is designed to encourage both of these best practices. 9 | // 10 | // Basic Usage 11 | // 12 | // The fundamental interface is Logger. Loggers create log events from 13 | // key/value data. The Logger interface has a single method, Log, which 14 | // accepts a sequence of alternating key/value pairs, which this package names 15 | // keyvals. 16 | // 17 | // type Logger interface { 18 | // Log(keyvals ...interface{}) error 19 | // } 20 | // 21 | // Here is an example of a function using a Logger to create log events. 22 | // 23 | // func RunTask(task Task, logger log.Logger) string { 24 | // logger.Log("taskID", task.ID, "event", "starting task") 25 | // ... 26 | // logger.Log("taskID", task.ID, "event", "task complete") 27 | // } 28 | // 29 | // The keys in the above example are "taskID" and "event". The values are 30 | // task.ID, "starting task", and "task complete". Every key is followed 31 | // immediately by its value. 32 | // 33 | // Keys are usually plain strings. Values may be any type that has a sensible 34 | // encoding in the chosen log format. With structured logging it is a good 35 | // idea to log simple values without formatting them. This practice allows 36 | // the chosen logger to encode values in the most appropriate way. 37 | // 38 | // Contextual Loggers 39 | // 40 | // A contextual logger stores keyvals that it includes in all log events. 41 | // Building appropriate contextual loggers reduces repetition and aids 42 | // consistency in the resulting log output. With and WithPrefix add context to 43 | // a logger. We can use With to improve the RunTask example. 44 | // 45 | // func RunTask(task Task, logger log.Logger) string { 46 | // logger = log.With(logger, "taskID", task.ID) 47 | // logger.Log("event", "starting task") 48 | // ... 49 | // taskHelper(task.Cmd, logger) 50 | // ... 51 | // logger.Log("event", "task complete") 52 | // } 53 | // 54 | // The improved version emits the same log events as the original for the 55 | // first and last calls to Log. Passing the contextual logger to taskHelper 56 | // enables each log event created by taskHelper to include the task.ID even 57 | // though taskHelper does not have access to that value. Using contextual 58 | // loggers this way simplifies producing log output that enables tracing the 59 | // life cycle of individual tasks. (See the Contextual example for the full 60 | // code of the above snippet.) 61 | // 62 | // Dynamic Contextual Values 63 | // 64 | // A Valuer function stored in a contextual logger generates a new value each 65 | // time an event is logged. The Valuer example demonstrates how this feature 66 | // works. 67 | // 68 | // Valuers provide the basis for consistently logging timestamps and source 69 | // code location. The log package defines several valuers for that purpose. 70 | // See Timestamp, DefaultTimestamp, DefaultTimestampUTC, Caller, and 71 | // DefaultCaller. A common logger initialization sequence that ensures all log 72 | // entries contain a timestamp and source location looks like this: 73 | // 74 | // logger := log.NewLogfmtLogger(log.NewSyncWriter(os.Stdout)) 75 | // logger = log.With(logger, "ts", log.DefaultTimestampUTC, "caller", log.DefaultCaller) 76 | // 77 | // Concurrent Safety 78 | // 79 | // Applications with multiple goroutines want each log event written to the 80 | // same logger to remain separate from other log events. Package log provides 81 | // two simple solutions for concurrent safe logging. 82 | // 83 | // NewSyncWriter wraps an io.Writer and serializes each call to its Write 84 | // method. Using a SyncWriter has the benefit that the smallest practical 85 | // portion of the logging logic is performed within a mutex, but it requires 86 | // the formatting Logger to make only one call to Write per log event. 87 | // 88 | // NewSyncLogger wraps any Logger and serializes each call to its Log method. 89 | // Using a SyncLogger has the benefit that it guarantees each log event is 90 | // handled atomically within the wrapped logger, but it typically serializes 91 | // both the formatting and output logic. Use a SyncLogger if the formatting 92 | // logger may perform multiple writes per log event. 93 | // 94 | // Error Handling 95 | // 96 | // This package relies on the practice of wrapping or decorating loggers with 97 | // other loggers to provide composable pieces of functionality. It also means 98 | // that Logger.Log must return an error because some 99 | // implementations—especially those that output log data to an io.Writer—may 100 | // encounter errors that cannot be handled locally. This in turn means that 101 | // Loggers that wrap other loggers should return errors from the wrapped 102 | // logger up the stack. 103 | // 104 | // Fortunately, the decorator pattern also provides a way to avoid the 105 | // necessity to check for errors every time an application calls Logger.Log. 106 | // An application required to panic whenever its Logger encounters 107 | // an error could initialize its logger as follows. 108 | // 109 | // fmtlogger := log.NewLogfmtLogger(log.NewSyncWriter(os.Stdout)) 110 | // logger := log.LoggerFunc(func(keyvals ...interface{}) error { 111 | // if err := fmtlogger.Log(keyvals...); err != nil { 112 | // panic(err) 113 | // } 114 | // return nil 115 | // }) 116 | package log 117 | -------------------------------------------------------------------------------- /vendor/github.com/go-kit/kit/log/json_logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "encoding" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "reflect" 9 | ) 10 | 11 | type jsonLogger struct { 12 | io.Writer 13 | } 14 | 15 | // NewJSONLogger returns a Logger that encodes keyvals to the Writer as a 16 | // single JSON object. Each log event produces no more than one call to 17 | // w.Write. The passed Writer must be safe for concurrent use by multiple 18 | // goroutines if the returned Logger will be used concurrently. 19 | func NewJSONLogger(w io.Writer) Logger { 20 | return &jsonLogger{w} 21 | } 22 | 23 | func (l *jsonLogger) Log(keyvals ...interface{}) error { 24 | n := (len(keyvals) + 1) / 2 // +1 to handle case when len is odd 25 | m := make(map[string]interface{}, n) 26 | for i := 0; i < len(keyvals); i += 2 { 27 | k := keyvals[i] 28 | var v interface{} = ErrMissingValue 29 | if i+1 < len(keyvals) { 30 | v = keyvals[i+1] 31 | } 32 | merge(m, k, v) 33 | } 34 | return json.NewEncoder(l.Writer).Encode(m) 35 | } 36 | 37 | func merge(dst map[string]interface{}, k, v interface{}) { 38 | var key string 39 | switch x := k.(type) { 40 | case string: 41 | key = x 42 | case fmt.Stringer: 43 | key = safeString(x) 44 | default: 45 | key = fmt.Sprint(x) 46 | } 47 | 48 | // We want json.Marshaler and encoding.TextMarshaller to take priority over 49 | // err.Error() and v.String(). But json.Marshall (called later) does that by 50 | // default so we force a no-op if it's one of those 2 case. 51 | switch x := v.(type) { 52 | case json.Marshaler: 53 | case encoding.TextMarshaler: 54 | case error: 55 | v = safeError(x) 56 | case fmt.Stringer: 57 | v = safeString(x) 58 | } 59 | 60 | dst[key] = v 61 | } 62 | 63 | func safeString(str fmt.Stringer) (s string) { 64 | defer func() { 65 | if panicVal := recover(); panicVal != nil { 66 | if v := reflect.ValueOf(str); v.Kind() == reflect.Ptr && v.IsNil() { 67 | s = "NULL" 68 | } else { 69 | panic(panicVal) 70 | } 71 | } 72 | }() 73 | s = str.String() 74 | return 75 | } 76 | 77 | func safeError(err error) (s interface{}) { 78 | defer func() { 79 | if panicVal := recover(); panicVal != nil { 80 | if v := reflect.ValueOf(err); v.Kind() == reflect.Ptr && v.IsNil() { 81 | s = nil 82 | } else { 83 | panic(panicVal) 84 | } 85 | } 86 | }() 87 | s = err.Error() 88 | return 89 | } 90 | -------------------------------------------------------------------------------- /vendor/github.com/go-kit/kit/log/level/doc.go: -------------------------------------------------------------------------------- 1 | // Package level implements leveled logging on top of Go kit's log package. To 2 | // use the level package, create a logger as per normal in your func main, and 3 | // wrap it with level.NewFilter. 4 | // 5 | // var logger log.Logger 6 | // logger = log.NewLogfmtLogger(os.Stderr) 7 | // logger = level.NewFilter(logger, level.AllowInfo()) // <-- 8 | // logger = log.With(logger, "ts", log.DefaultTimestampUTC) 9 | // 10 | // Then, at the callsites, use one of the level.Debug, Info, Warn, or Error 11 | // helper methods to emit leveled log events. 12 | // 13 | // logger.Log("foo", "bar") // as normal, no level 14 | // level.Debug(logger).Log("request_id", reqID, "trace_data", trace.Get()) 15 | // if value > 100 { 16 | // level.Error(logger).Log("value", value) 17 | // } 18 | // 19 | // NewFilter allows precise control over what happens when a log event is 20 | // emitted without a level key, or if a squelched level is used. Check the 21 | // Option functions for details. 22 | package level 23 | -------------------------------------------------------------------------------- /vendor/github.com/go-kit/kit/log/level/level.go: -------------------------------------------------------------------------------- 1 | package level 2 | 3 | import "github.com/go-kit/kit/log" 4 | 5 | // Error returns a logger that includes a Key/ErrorValue pair. 6 | func Error(logger log.Logger) log.Logger { 7 | return log.WithPrefix(logger, Key(), ErrorValue()) 8 | } 9 | 10 | // Warn returns a logger that includes a Key/WarnValue pair. 11 | func Warn(logger log.Logger) log.Logger { 12 | return log.WithPrefix(logger, Key(), WarnValue()) 13 | } 14 | 15 | // Info returns a logger that includes a Key/InfoValue pair. 16 | func Info(logger log.Logger) log.Logger { 17 | return log.WithPrefix(logger, Key(), InfoValue()) 18 | } 19 | 20 | // Debug returns a logger that includes a Key/DebugValue pair. 21 | func Debug(logger log.Logger) log.Logger { 22 | return log.WithPrefix(logger, Key(), DebugValue()) 23 | } 24 | 25 | // NewFilter wraps next and implements level filtering. See the commentary on 26 | // the Option functions for a detailed description of how to configure levels. 27 | // If no options are provided, all leveled log events created with Debug, 28 | // Info, Warn or Error helper methods are squelched and non-leveled log 29 | // events are passed to next unmodified. 30 | func NewFilter(next log.Logger, options ...Option) log.Logger { 31 | l := &logger{ 32 | next: next, 33 | } 34 | for _, option := range options { 35 | option(l) 36 | } 37 | return l 38 | } 39 | 40 | type logger struct { 41 | next log.Logger 42 | allowed level 43 | squelchNoLevel bool 44 | errNotAllowed error 45 | errNoLevel error 46 | } 47 | 48 | func (l *logger) Log(keyvals ...interface{}) error { 49 | var hasLevel, levelAllowed bool 50 | for i := 1; i < len(keyvals); i += 2 { 51 | if v, ok := keyvals[i].(*levelValue); ok { 52 | hasLevel = true 53 | levelAllowed = l.allowed&v.level != 0 54 | break 55 | } 56 | } 57 | if !hasLevel && l.squelchNoLevel { 58 | return l.errNoLevel 59 | } 60 | if hasLevel && !levelAllowed { 61 | return l.errNotAllowed 62 | } 63 | return l.next.Log(keyvals...) 64 | } 65 | 66 | // Option sets a parameter for the leveled logger. 67 | type Option func(*logger) 68 | 69 | // AllowAll is an alias for AllowDebug. 70 | func AllowAll() Option { 71 | return AllowDebug() 72 | } 73 | 74 | // AllowDebug allows error, warn, info and debug level log events to pass. 75 | func AllowDebug() Option { 76 | return allowed(levelError | levelWarn | levelInfo | levelDebug) 77 | } 78 | 79 | // AllowInfo allows error, warn and info level log events to pass. 80 | func AllowInfo() Option { 81 | return allowed(levelError | levelWarn | levelInfo) 82 | } 83 | 84 | // AllowWarn allows error and warn level log events to pass. 85 | func AllowWarn() Option { 86 | return allowed(levelError | levelWarn) 87 | } 88 | 89 | // AllowError allows only error level log events to pass. 90 | func AllowError() Option { 91 | return allowed(levelError) 92 | } 93 | 94 | // AllowNone allows no leveled log events to pass. 95 | func AllowNone() Option { 96 | return allowed(0) 97 | } 98 | 99 | func allowed(allowed level) Option { 100 | return func(l *logger) { l.allowed = allowed } 101 | } 102 | 103 | // ErrNotAllowed sets the error to return from Log when it squelches a log 104 | // event disallowed by the configured Allow[Level] option. By default, 105 | // ErrNotAllowed is nil; in this case the log event is squelched with no 106 | // error. 107 | func ErrNotAllowed(err error) Option { 108 | return func(l *logger) { l.errNotAllowed = err } 109 | } 110 | 111 | // SquelchNoLevel instructs Log to squelch log events with no level, so that 112 | // they don't proceed through to the wrapped logger. If SquelchNoLevel is set 113 | // to true and a log event is squelched in this way, the error value 114 | // configured with ErrNoLevel is returned to the caller. 115 | func SquelchNoLevel(squelch bool) Option { 116 | return func(l *logger) { l.squelchNoLevel = squelch } 117 | } 118 | 119 | // ErrNoLevel sets the error to return from Log when it squelches a log event 120 | // with no level. By default, ErrNoLevel is nil; in this case the log event is 121 | // squelched with no error. 122 | func ErrNoLevel(err error) Option { 123 | return func(l *logger) { l.errNoLevel = err } 124 | } 125 | 126 | // NewInjector wraps next and returns a logger that adds a Key/level pair to 127 | // the beginning of log events that don't already contain a level. In effect, 128 | // this gives a default level to logs without a level. 129 | func NewInjector(next log.Logger, level Value) log.Logger { 130 | return &injector{ 131 | next: next, 132 | level: level, 133 | } 134 | } 135 | 136 | type injector struct { 137 | next log.Logger 138 | level interface{} 139 | } 140 | 141 | func (l *injector) Log(keyvals ...interface{}) error { 142 | for i := 1; i < len(keyvals); i += 2 { 143 | if _, ok := keyvals[i].(*levelValue); ok { 144 | return l.next.Log(keyvals...) 145 | } 146 | } 147 | kvs := make([]interface{}, len(keyvals)+2) 148 | kvs[0], kvs[1] = key, l.level 149 | copy(kvs[2:], keyvals) 150 | return l.next.Log(kvs...) 151 | } 152 | 153 | // Value is the interface that each of the canonical level values implement. 154 | // It contains unexported methods that prevent types from other packages from 155 | // implementing it and guaranteeing that NewFilter can distinguish the levels 156 | // defined in this package from all other values. 157 | type Value interface { 158 | String() string 159 | levelVal() 160 | } 161 | 162 | // Key returns the unique key added to log events by the loggers in this 163 | // package. 164 | func Key() interface{} { return key } 165 | 166 | // ErrorValue returns the unique value added to log events by Error. 167 | func ErrorValue() Value { return errorValue } 168 | 169 | // WarnValue returns the unique value added to log events by Warn. 170 | func WarnValue() Value { return warnValue } 171 | 172 | // InfoValue returns the unique value added to log events by Info. 173 | func InfoValue() Value { return infoValue } 174 | 175 | // DebugValue returns the unique value added to log events by Warn. 176 | func DebugValue() Value { return debugValue } 177 | 178 | var ( 179 | // key is of type interface{} so that it allocates once during package 180 | // initialization and avoids allocating every time the value is added to a 181 | // []interface{} later. 182 | key interface{} = "level" 183 | 184 | errorValue = &levelValue{level: levelError, name: "error"} 185 | warnValue = &levelValue{level: levelWarn, name: "warn"} 186 | infoValue = &levelValue{level: levelInfo, name: "info"} 187 | debugValue = &levelValue{level: levelDebug, name: "debug"} 188 | ) 189 | 190 | type level byte 191 | 192 | const ( 193 | levelDebug level = 1 << iota 194 | levelInfo 195 | levelWarn 196 | levelError 197 | ) 198 | 199 | type levelValue struct { 200 | name string 201 | level 202 | } 203 | 204 | func (v *levelValue) String() string { return v.name } 205 | func (v *levelValue) levelVal() {} 206 | -------------------------------------------------------------------------------- /vendor/github.com/go-kit/kit/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import "errors" 4 | 5 | // Logger is the fundamental interface for all log operations. Log creates a 6 | // log event from keyvals, a variadic sequence of alternating keys and values. 7 | // Implementations must be safe for concurrent use by multiple goroutines. In 8 | // particular, any implementation of Logger that appends to keyvals or 9 | // modifies or retains any of its elements must make a copy first. 10 | type Logger interface { 11 | Log(keyvals ...interface{}) error 12 | } 13 | 14 | // ErrMissingValue is appended to keyvals slices with odd length to substitute 15 | // the missing value. 16 | var ErrMissingValue = errors.New("(MISSING)") 17 | 18 | // With returns a new contextual logger with keyvals prepended to those passed 19 | // to calls to Log. If logger is also a contextual logger created by With or 20 | // WithPrefix, keyvals is appended to the existing context. 21 | // 22 | // The returned Logger replaces all value elements (odd indexes) containing a 23 | // Valuer with their generated value for each call to its Log method. 24 | func With(logger Logger, keyvals ...interface{}) Logger { 25 | if len(keyvals) == 0 { 26 | return logger 27 | } 28 | l := newContext(logger) 29 | kvs := append(l.keyvals, keyvals...) 30 | if len(kvs)%2 != 0 { 31 | kvs = append(kvs, ErrMissingValue) 32 | } 33 | return &context{ 34 | logger: l.logger, 35 | // Limiting the capacity of the stored keyvals ensures that a new 36 | // backing array is created if the slice must grow in Log or With. 37 | // Using the extra capacity without copying risks a data race that 38 | // would violate the Logger interface contract. 39 | keyvals: kvs[:len(kvs):len(kvs)], 40 | hasValuer: l.hasValuer || containsValuer(keyvals), 41 | } 42 | } 43 | 44 | // WithPrefix returns a new contextual logger with keyvals prepended to those 45 | // passed to calls to Log. If logger is also a contextual logger created by 46 | // With or WithPrefix, keyvals is prepended to the existing context. 47 | // 48 | // The returned Logger replaces all value elements (odd indexes) containing a 49 | // Valuer with their generated value for each call to its Log method. 50 | func WithPrefix(logger Logger, keyvals ...interface{}) Logger { 51 | if len(keyvals) == 0 { 52 | return logger 53 | } 54 | l := newContext(logger) 55 | // Limiting the capacity of the stored keyvals ensures that a new 56 | // backing array is created if the slice must grow in Log or With. 57 | // Using the extra capacity without copying risks a data race that 58 | // would violate the Logger interface contract. 59 | n := len(l.keyvals) + len(keyvals) 60 | if len(keyvals)%2 != 0 { 61 | n++ 62 | } 63 | kvs := make([]interface{}, 0, n) 64 | kvs = append(kvs, keyvals...) 65 | if len(kvs)%2 != 0 { 66 | kvs = append(kvs, ErrMissingValue) 67 | } 68 | kvs = append(kvs, l.keyvals...) 69 | return &context{ 70 | logger: l.logger, 71 | keyvals: kvs, 72 | hasValuer: l.hasValuer || containsValuer(keyvals), 73 | } 74 | } 75 | 76 | // context is the Logger implementation returned by With and WithPrefix. It 77 | // wraps a Logger and holds keyvals that it includes in all log events. Its 78 | // Log method calls bindValues to generate values for each Valuer in the 79 | // context keyvals. 80 | // 81 | // A context must always have the same number of stack frames between calls to 82 | // its Log method and the eventual binding of Valuers to their value. This 83 | // requirement comes from the functional requirement to allow a context to 84 | // resolve application call site information for a Caller stored in the 85 | // context. To do this we must be able to predict the number of logging 86 | // functions on the stack when bindValues is called. 87 | // 88 | // Two implementation details provide the needed stack depth consistency. 89 | // 90 | // 1. newContext avoids introducing an additional layer when asked to 91 | // wrap another context. 92 | // 2. With and WithPrefix avoid introducing an additional layer by 93 | // returning a newly constructed context with a merged keyvals rather 94 | // than simply wrapping the existing context. 95 | type context struct { 96 | logger Logger 97 | keyvals []interface{} 98 | hasValuer bool 99 | } 100 | 101 | func newContext(logger Logger) *context { 102 | if c, ok := logger.(*context); ok { 103 | return c 104 | } 105 | return &context{logger: logger} 106 | } 107 | 108 | // Log replaces all value elements (odd indexes) containing a Valuer in the 109 | // stored context with their generated value, appends keyvals, and passes the 110 | // result to the wrapped Logger. 111 | func (l *context) Log(keyvals ...interface{}) error { 112 | kvs := append(l.keyvals, keyvals...) 113 | if len(kvs)%2 != 0 { 114 | kvs = append(kvs, ErrMissingValue) 115 | } 116 | if l.hasValuer { 117 | // If no keyvals were appended above then we must copy l.keyvals so 118 | // that future log events will reevaluate the stored Valuers. 119 | if len(keyvals) == 0 { 120 | kvs = append([]interface{}{}, l.keyvals...) 121 | } 122 | bindValues(kvs[:len(l.keyvals)]) 123 | } 124 | return l.logger.Log(kvs...) 125 | } 126 | 127 | // LoggerFunc is an adapter to allow use of ordinary functions as Loggers. If 128 | // f is a function with the appropriate signature, LoggerFunc(f) is a Logger 129 | // object that calls f. 130 | type LoggerFunc func(...interface{}) error 131 | 132 | // Log implements Logger by calling f(keyvals...). 133 | func (f LoggerFunc) Log(keyvals ...interface{}) error { 134 | return f(keyvals...) 135 | } 136 | -------------------------------------------------------------------------------- /vendor/github.com/go-kit/kit/log/logfmt_logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "sync" 7 | 8 | "github.com/go-logfmt/logfmt" 9 | ) 10 | 11 | type logfmtEncoder struct { 12 | *logfmt.Encoder 13 | buf bytes.Buffer 14 | } 15 | 16 | func (l *logfmtEncoder) Reset() { 17 | l.Encoder.Reset() 18 | l.buf.Reset() 19 | } 20 | 21 | var logfmtEncoderPool = sync.Pool{ 22 | New: func() interface{} { 23 | var enc logfmtEncoder 24 | enc.Encoder = logfmt.NewEncoder(&enc.buf) 25 | return &enc 26 | }, 27 | } 28 | 29 | type logfmtLogger struct { 30 | w io.Writer 31 | } 32 | 33 | // NewLogfmtLogger returns a logger that encodes keyvals to the Writer in 34 | // logfmt format. Each log event produces no more than one call to w.Write. 35 | // The passed Writer must be safe for concurrent use by multiple goroutines if 36 | // the returned Logger will be used concurrently. 37 | func NewLogfmtLogger(w io.Writer) Logger { 38 | return &logfmtLogger{w} 39 | } 40 | 41 | func (l logfmtLogger) Log(keyvals ...interface{}) error { 42 | enc := logfmtEncoderPool.Get().(*logfmtEncoder) 43 | enc.Reset() 44 | defer logfmtEncoderPool.Put(enc) 45 | 46 | if err := enc.EncodeKeyvals(keyvals...); err != nil { 47 | return err 48 | } 49 | 50 | // Add newline to the end of the buffer 51 | if err := enc.EndRecord(); err != nil { 52 | return err 53 | } 54 | 55 | // The Logger interface requires implementations to be safe for concurrent 56 | // use by multiple goroutines. For this implementation that means making 57 | // only one call to l.w.Write() for each call to Log. 58 | if _, err := l.w.Write(enc.buf.Bytes()); err != nil { 59 | return err 60 | } 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /vendor/github.com/go-kit/kit/log/nop_logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | type nopLogger struct{} 4 | 5 | // NewNopLogger returns a logger that doesn't do anything. 6 | func NewNopLogger() Logger { return nopLogger{} } 7 | 8 | func (nopLogger) Log(...interface{}) error { return nil } 9 | -------------------------------------------------------------------------------- /vendor/github.com/go-kit/kit/log/stdlib.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "regexp" 7 | "strings" 8 | ) 9 | 10 | // StdlibWriter implements io.Writer by invoking the stdlib log.Print. It's 11 | // designed to be passed to a Go kit logger as the writer, for cases where 12 | // it's necessary to redirect all Go kit log output to the stdlib logger. 13 | // 14 | // If you have any choice in the matter, you shouldn't use this. Prefer to 15 | // redirect the stdlib log to the Go kit logger via NewStdlibAdapter. 16 | type StdlibWriter struct{} 17 | 18 | // Write implements io.Writer. 19 | func (w StdlibWriter) Write(p []byte) (int, error) { 20 | log.Print(strings.TrimSpace(string(p))) 21 | return len(p), nil 22 | } 23 | 24 | // StdlibAdapter wraps a Logger and allows it to be passed to the stdlib 25 | // logger's SetOutput. It will extract date/timestamps, filenames, and 26 | // messages, and place them under relevant keys. 27 | type StdlibAdapter struct { 28 | Logger 29 | timestampKey string 30 | fileKey string 31 | messageKey string 32 | } 33 | 34 | // StdlibAdapterOption sets a parameter for the StdlibAdapter. 35 | type StdlibAdapterOption func(*StdlibAdapter) 36 | 37 | // TimestampKey sets the key for the timestamp field. By default, it's "ts". 38 | func TimestampKey(key string) StdlibAdapterOption { 39 | return func(a *StdlibAdapter) { a.timestampKey = key } 40 | } 41 | 42 | // FileKey sets the key for the file and line field. By default, it's "caller". 43 | func FileKey(key string) StdlibAdapterOption { 44 | return func(a *StdlibAdapter) { a.fileKey = key } 45 | } 46 | 47 | // MessageKey sets the key for the actual log message. By default, it's "msg". 48 | func MessageKey(key string) StdlibAdapterOption { 49 | return func(a *StdlibAdapter) { a.messageKey = key } 50 | } 51 | 52 | // NewStdlibAdapter returns a new StdlibAdapter wrapper around the passed 53 | // logger. It's designed to be passed to log.SetOutput. 54 | func NewStdlibAdapter(logger Logger, options ...StdlibAdapterOption) io.Writer { 55 | a := StdlibAdapter{ 56 | Logger: logger, 57 | timestampKey: "ts", 58 | fileKey: "caller", 59 | messageKey: "msg", 60 | } 61 | for _, option := range options { 62 | option(&a) 63 | } 64 | return a 65 | } 66 | 67 | func (a StdlibAdapter) Write(p []byte) (int, error) { 68 | result := subexps(p) 69 | keyvals := []interface{}{} 70 | var timestamp string 71 | if date, ok := result["date"]; ok && date != "" { 72 | timestamp = date 73 | } 74 | if time, ok := result["time"]; ok && time != "" { 75 | if timestamp != "" { 76 | timestamp += " " 77 | } 78 | timestamp += time 79 | } 80 | if timestamp != "" { 81 | keyvals = append(keyvals, a.timestampKey, timestamp) 82 | } 83 | if file, ok := result["file"]; ok && file != "" { 84 | keyvals = append(keyvals, a.fileKey, file) 85 | } 86 | if msg, ok := result["msg"]; ok { 87 | keyvals = append(keyvals, a.messageKey, msg) 88 | } 89 | if err := a.Logger.Log(keyvals...); err != nil { 90 | return 0, err 91 | } 92 | return len(p), nil 93 | } 94 | 95 | const ( 96 | logRegexpDate = `(?P[0-9]{4}/[0-9]{2}/[0-9]{2})?[ ]?` 97 | logRegexpTime = `(?P