├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── gate │ └── main.go ├── gatecli │ └── main.go └── internal │ └── internal.go ├── config.go ├── config_test.go ├── gate.go ├── gate_test.go ├── go.mod ├── go.sum ├── line.go ├── payload.go ├── pixela.go ├── slack.go └── slack_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | artifacts 2 | 3 | # Created by https://www.gitignore.io/api/go 4 | 5 | ### Go ### 6 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 7 | *.o 8 | *.a 9 | *.so 10 | 11 | # Folders 12 | _obj 13 | _test 14 | 15 | # Architecture specific extensions/prefixes 16 | *.[568vq] 17 | [568vq].out 18 | 19 | *.cgo1.go 20 | *.cgo2.c 21 | _cgo_defun.c 22 | _cgo_gotypes.go 23 | _cgo_export.* 24 | 25 | _testmain.go 26 | 27 | *.exe 28 | *.test 29 | *.prof 30 | 31 | # Output of the go coverage tool, specifically when used with LiteIDE 32 | *.out 33 | 34 | # External packages folder 35 | # vendor/ 36 | 37 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 38 | .glide/ 39 | 40 | # End of https://www.gitignore.io/api/go 41 | # Created by https://www.gitignore.io/api/vim 42 | 43 | ### Vim ### 44 | # swap 45 | [._]*.s[a-v][a-z] 46 | [._]*.sw[a-p] 47 | [._]s[a-v][a-z] 48 | [._]sw[a-p] 49 | # session 50 | Session.vim 51 | # temporary 52 | .netrwhist 53 | *~ 54 | # auto-generated tag files 55 | tags 56 | 57 | # End of https://www.gitignore.io/api/vim 58 | # Created by https://www.gitignore.io/api/linux 59 | 60 | ### Linux ### 61 | *~ 62 | 63 | # temporary files which can be created if a process still has a handle open of a deleted file 64 | .fuse_hidden* 65 | 66 | # KDE directory preferences 67 | .directory 68 | 69 | # Linux trash folder which might appear on any partition or disk 70 | .Trash-* 71 | 72 | # .nfs files are created when an open file is removed but is still being accessed 73 | .nfs* 74 | 75 | # End of https://www.gitignore.io/api/linux 76 | # Created by https://www.gitignore.io/api/osx 77 | 78 | ### OSX ### 79 | *.DS_Store 80 | .AppleDouble 81 | .LSOverride 82 | 83 | # Icon must end with two \r 84 | Icon 85 | 86 | 87 | # Thumbnails 88 | ._* 89 | 90 | # Files that might appear in the root of a volume 91 | .DocumentRevisions-V100 92 | .fseventsd 93 | .Spotlight-V100 94 | .TemporaryItems 95 | .Trashes 96 | .VolumeIcon.icns 97 | .com.apple.timemachine.donotpresent 98 | 99 | # Directories potentially created on remote AFP share 100 | .AppleDB 101 | .AppleDesktop 102 | Network Trash Folder 103 | Temporary Items 104 | .apdisk 105 | 106 | # End of https://www.gitignore.io/api/osx 107 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Shintaro Kaneko 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOVERSION=$(shell go version) 2 | GOOS=$(word 1,$(subst /, ,$(lastword $(GOVERSION)))) 3 | GOARCH=$(word 2,$(subst /, ,$(lastword $(GOVERSION)))) 4 | TARGET_ONLY_PKGS=$(shell go list ./... 2> /dev/null | grep -v "/vendor/") 5 | IGNORE_DEPS_GOLINT='vendor/.+\.go' 6 | IGNORE_DEPS_GOCYCLO='vendor/.+\.go' 7 | HAVE_GOLINT:=$(shell which golint) 8 | HAVE_GOCYCLO:=$(shell which gocyclo) 9 | HAVE_GHR:=$(shell which ghr) 10 | HAVE_GOX:=$(shell which gox) 11 | PROJECT_REPONAME=$(notdir $(abspath ./)) 12 | PROJECT_USERNAME=$(notdir $(abspath ../)) 13 | OBJS=$(notdir $(TARGETS)) 14 | LDFLAGS=-ldflags="-s -w" 15 | COMMITISH=$(shell git rev-parse HEAD) 16 | ARTIFACTS_DIR=artifacts 17 | TARGETS=$(addprefix github.com/$(PROJECT_USERNAME)/$(PROJECT_REPONAME)/cmd/,gate gatecli) 18 | VERSION=$(patsubst "%",%,$(lastword $(shell grep 'const Version' gate.go))) 19 | 20 | all: $(TARGETS) 21 | 22 | $(TARGETS): 23 | @go install $(LDFLAGS) -v $@ 24 | 25 | .PHONY: build release clean 26 | build: gox 27 | @mkdir -p $(ARTIFACTS_DIR)/$(VERSION) && cd $(ARTIFACTS_DIR)/$(VERSION); \ 28 | gox $(LDFLAGS) $(TARGETS) 29 | 30 | release: ghr verify-github-token build 31 | @ghr -c $(COMMITISH) -u $(PROJECT_USERNAME) -r $(PROJECT_REPONAME) -t $$GITHUB_TOKEN \ 32 | --replace $(VERSION) $(ARTIFACTS_DIR)/$(VERSION) 33 | 34 | clean: 35 | $(RM) -r $(ARTIFACTS_DIR) 36 | 37 | .PHONY: unit lint cyclo test 38 | unit: lint cyclo test 39 | 40 | lint: golint 41 | @echo "go lint" 42 | @lint=`golint ./...`; \ 43 | lint=`echo "$$lint" | grep -E -v -e ${IGNORE_DEPS_GOLINT}`; \ 44 | echo "$$lint"; if [ "$$lint" != "" ]; then exit 1; fi 45 | 46 | cyclo: gocyclo 47 | @echo "gocyclo -over 30" 48 | @cyclo=`gocyclo -over 30 . 2>&1`; \ 49 | cyclo=`echo "$$cyclo" | grep -E -v -e ${IGNORE_DEPS_GOCYCLO}`; \ 50 | echo "$$cyclo"; if [ "$$cyclo" != "" ]; then exit 1; fi 51 | 52 | test: 53 | @go test $(TARGET_ONLY_PKGS) 54 | 55 | .PHONY: verify-github-token 56 | verify-github-token: 57 | @if [ -z "$$GITHUB_TOKEN" ]; then echo '$$GITHUB_TOKEN is required'; exit 1; fi 58 | 59 | .PHONY: golint gocyclo ghr gox 60 | golint: 61 | ifndef HAVE_GOLINT 62 | @echo "Installing linter" 63 | @go get -u golang.org/x/lint/golint 64 | endif 65 | 66 | gocyclo: 67 | ifndef HAVE_GOCYCLO 68 | @echo "Installing gocyclo" 69 | @go get -u github.com/fzipp/gocyclo 70 | endif 71 | 72 | ghr: 73 | ifndef HAVE_GHR 74 | @echo "Installing ghr to upload binaries for release page" 75 | @go get -u github.com/tcnksm/ghr 76 | endif 77 | 78 | gox: 79 | ifndef HAVE_GOX 80 | @echo "Installing gox to build binaries for Go cross compilation" 81 | @go get -u github.com/mitchellh/gox 82 | endif 83 | 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gate 2 | 3 | Gate posts a message in Slack and LINE. 4 | 5 | ## Installation 6 | 7 | To compile the Gate binaries from source, clone the Gate repository. Then, navigate to the new directory. 8 | 9 | ```shell 10 | $ git clone https://github.com/kaneshin/gate.git 11 | $ cd gate 12 | ``` 13 | 14 | Compile the `gate` and `gatecli` which will be stored it in $GOPATH/bin. 15 | 16 | ```shell 17 | $ go install 18 | ``` 19 | 20 | Finally, make sure that the gate and gatecli binaries are available on your PATH. 21 | 22 | ## Setup 23 | 24 | Gate loads its configuration in ~/.config/gate/config.json as a default. You need to create it then setup your channels' configuration: 25 | 26 | ```json 27 | { 28 | "gate": { 29 | "scheme": "http", 30 | "host": "0.0.0.0", 31 | "port": 5731, 32 | "client": { 33 | "default": "slack.channel-1" 34 | } 35 | }, 36 | "platforms": { 37 | "slack": { 38 | "channel-1": "[YOUR-INCOMING-URL]", 39 | "channel-2": "[YOUR-INCOMING-URL]" 40 | }, 41 | "line": { 42 | "service-1": "[YOUR-ACCESS-TOKEN]" 43 | }, 44 | "pixela": { 45 | "username/graph-id": "[YOUR-TOKEN]" 46 | } 47 | } 48 | } 49 | ``` 50 | 51 | ## Usage 52 | 53 | ### gate 54 | 55 | Run the gate server. If neeeded, run the command with argument of configuration path. 56 | 57 | ```shell 58 | $ gate -config=/path/to/config.json 59 | ``` 60 | 61 | ### gatecli 62 | 63 | First, you need to install the configuration of gate to run `gatecli` via network. You will get the partial configuration of config.json. 64 | 65 | ```shell 66 | $ curl -sL http://0.0.0.0:5731/config/cli.json > ~/.config/gate/cli.json 67 | ``` 68 | 69 | Then, run `gatecli` to post the platforms. 70 | 71 | ```shell 72 | $ echo "foobar" | gatecli slack.channel-1 73 | $ echo "foobar" | gatecli # You can send a message without argument for the default target in cli.json 74 | ``` 75 | 76 | If you'd like to parse another cli.json. Please run the command with arguments like the below. 77 | 78 | ``` 79 | $ echo "foobar" | gatecli -config=/path/to/cli.json slack.channel-2 80 | ``` 81 | 82 | #### Slack 83 | 84 | You need to get an incoming webhook url what you want to post message before run it. 85 | 86 | ```shell 87 | $ echo "Hello world!" | gatecli slack.channel-1 88 | ``` 89 | 90 | #### LINE 91 | 92 | You need to create a service what you want to post message before run it. 93 | 94 | ```shell 95 | $ echo "Hello world!" | gatecli line.service-1 96 | ``` 97 | 98 | #### Pixela 99 | 100 | You need to create a graph what you want to update before run it. 101 | 102 | Input a quantity from STDIN, then pass it to `gatecli`. 103 | 104 | ```shell 105 | # i, inc, or increment 106 | $ echo i | gatecli pixela.username/graph-id # to increment of the day 107 | 108 | # d, dec, or decrement 109 | $ echo d | gatecli pixela.username/graph-id # to decrement of the day 110 | 111 | $ echo 5 | gatecli pixela.username/graph-id # to assign the quantity of the day 112 | ``` 113 | 114 | ## License 115 | 116 | [The MIT License (MIT)](http://kaneshin.mit-license.org/) 117 | 118 | ## Author 119 | 120 | Shintaro Kaneko 121 | -------------------------------------------------------------------------------- /cmd/gate/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "flag" 8 | "fmt" 9 | "log" 10 | "net" 11 | "net/http" 12 | "net/http/httputil" 13 | "os" 14 | "regexp" 15 | "strings" 16 | "time" 17 | 18 | "github.com/kaneshin/gate" 19 | "github.com/kaneshin/gate/cmd/internal" 20 | ) 21 | 22 | var errNotSupportedPlatform = errors.New("not supported platform") 23 | var errNotDefinedTarget = errors.New("not defined target") 24 | 25 | func postToSlackIncoming(url, text string) error { 26 | config := gate.NewConfig().WithHTTPClient(http.DefaultClient) 27 | svc := gate.NewSlackIncomingService(config).WithBaseURL(url) 28 | _, err := svc.PostTextPayload(gate.TextPayload{ 29 | Text: text, 30 | }) 31 | if err != nil { 32 | return err 33 | } 34 | return nil 35 | } 36 | 37 | func postToLINENotify(token, text string) error { 38 | config := gate.NewConfig().WithHTTPClient(http.DefaultClient) 39 | config.WithAccessToken(token) 40 | svc := gate.NewLINENotifyService(config) 41 | _, err := svc.PostMessagePayload(gate.MessagePayload{ 42 | Message: text, 43 | }) 44 | if err != nil { 45 | return err 46 | } 47 | return nil 48 | } 49 | 50 | func postToPixela(target, token, text string) error { 51 | el := strings.Split(target, "/") 52 | if len(el) != 2 { 53 | return errNotDefinedTarget 54 | } 55 | 56 | config := gate.NewConfig().WithHTTPClient(http.DefaultClient) 57 | config.WithAccessToken(token) 58 | config.WithID(el[0]) 59 | svc := gate.NewPixelaService(config) 60 | 61 | payload := gate.GraphPayload{ 62 | ID: el[1], 63 | } 64 | var err error 65 | switch text { 66 | case "i", "inc", "increment": 67 | _, err = svc.Increment(payload) 68 | case "d", "dec", "decrement": 69 | _, err = svc.Decrement(payload) 70 | default: 71 | payload.Date = time.Now().Format("20060102") 72 | payload.Quantity = text 73 | _, err = svc.PostGraphPayload(payload) 74 | } 75 | if err != nil { 76 | return err 77 | } 78 | return nil 79 | } 80 | 81 | func post(name, text string) error { 82 | var err error 83 | var buf bytes.Buffer 84 | var v map[string]map[string]string 85 | 86 | err = json.NewEncoder(&buf).Encode(config.Platforms) 87 | if err != nil { 88 | return err 89 | } 90 | err = json.NewDecoder(&buf).Decode(&v) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | el := strings.Split(name, ".") 96 | if len(el) != 2 { 97 | return errNotDefinedTarget 98 | } 99 | platform, ok := v[el[0]] 100 | if !ok { 101 | return errNotDefinedTarget 102 | } 103 | target := el[1] 104 | token, ok := platform[target] 105 | if !ok { 106 | return errNotDefinedTarget 107 | } 108 | 109 | switch el[0] { 110 | case "slack": 111 | err = postToSlackIncoming(token, text) 112 | case "line": 113 | err = postToLINENotify(token, text) 114 | case "pixela": 115 | err = postToPixela(target, token, text) 116 | default: 117 | err = errNotSupportedPlatform 118 | } 119 | if err != nil { 120 | return err 121 | } 122 | return nil 123 | } 124 | 125 | func recovery() { 126 | if r := recover(); r != nil { 127 | log.Printf("[Fatal] Recover: %v", r) 128 | } 129 | } 130 | 131 | func notFoundHandler(w http.ResponseWriter, r *http.Request) { 132 | b, err := httputil.DumpRequest(r, false) 133 | if err != nil { 134 | log.Printf("[Request] 404 Not Found: %s", err) 135 | } else { 136 | log.Printf("[Request] 404 Not Found: %s", string(b)) 137 | } 138 | http.Error(w, "404 Not Found", http.StatusNotFound) 139 | } 140 | 141 | func methodNotAllowdHandler(w http.ResponseWriter, r *http.Request) { 142 | b, err := httputil.DumpRequest(r, false) 143 | if err != nil { 144 | log.Printf("[Request] 405 Method Not Allowed: %s", err) 145 | } else { 146 | log.Printf("[Request] 405 Method Not Allowed: %s", string(b)) 147 | } 148 | http.Error(w, "405 Method Not Allowed", http.StatusMethodNotAllowed) 149 | } 150 | 151 | func internalServerErrorHandler(w http.ResponseWriter, r *http.Request) { 152 | b, err := httputil.DumpRequest(r, false) 153 | if err != nil { 154 | log.Printf("[Request] 500 Internal Server Error: %s", err) 155 | } else { 156 | log.Printf("[Request] 500 Internal Server Error: %s", string(b)) 157 | } 158 | http.Error(w, "500 Internal Server Error", http.StatusInternalServerError) 159 | } 160 | 161 | var postReg = regexp.MustCompile(`/post/(|.+\..+)$`) 162 | 163 | func postHandler(w http.ResponseWriter, r *http.Request) { 164 | defer recovery() 165 | 166 | if !postReg.MatchString(r.URL.Path) { 167 | notFoundHandler(w, r) 168 | return 169 | } 170 | if r.Method != http.MethodPost { 171 | methodNotAllowdHandler(w, r) 172 | return 173 | } 174 | 175 | var text string 176 | var target string 177 | matches := postReg.FindStringSubmatch(r.URL.Path) 178 | if matches[1] == "" { 179 | // POST /notify/ 180 | err := r.ParseForm() 181 | if err != nil { 182 | internalServerErrorHandler(w, r) 183 | log.Printf("Error: %v", err) 184 | return 185 | } 186 | target = r.FormValue("target") 187 | text = r.FormValue("text") 188 | } else { 189 | // POST /notify/[platform].[target]?t=[text] 190 | target = matches[1] 191 | text = r.URL.Query().Get("t") 192 | } 193 | 194 | var msg string 195 | err := post(target, text) 196 | if err != nil { 197 | msg = fmt.Sprintf("✘ %s: failed %s\n", target, err) 198 | } else { 199 | msg = fmt.Sprintf("✔ %s: success\n", target) 200 | } 201 | log.Printf("[Post] %s", msg) 202 | w.Write([]byte(msg)) 203 | } 204 | 205 | func configHandler(w http.ResponseWriter, r *http.Request) { 206 | defer recovery() 207 | 208 | switch r.URL.Path { 209 | case internal.ConfigCLIJSON: 210 | if r.Method != http.MethodGet { 211 | methodNotAllowdHandler(w, r) 212 | return 213 | } 214 | 215 | gate := config.Gate 216 | gate.Host = privateIP() 217 | 218 | c := map[string]interface{}{ 219 | "gate": gate, 220 | } 221 | var buf bytes.Buffer 222 | err := json.NewEncoder(&buf).Encode(c) 223 | if err != nil { 224 | internalServerErrorHandler(w, r) 225 | log.Printf("Error: %v", err) 226 | return 227 | } 228 | w.Write(buf.Bytes()) 229 | } 230 | } 231 | 232 | var privateIPBlocks []*net.IPNet 233 | 234 | func init() { 235 | for _, cidr := range []string{ 236 | "10.0.0.0/8", // RFC1918 237 | "172.16.0.0/12", // RFC1918 238 | "192.168.0.0/16", // RFC1918 239 | "169.254.0.0/16", // RFC3927 link-local 240 | } { 241 | _, block, err := net.ParseCIDR(cidr) 242 | if err != nil { 243 | log.Panic(fmt.Errorf("parse error on %q: %v", cidr, err)) 244 | } 245 | privateIPBlocks = append(privateIPBlocks, block) 246 | } 247 | } 248 | 249 | func isPrivateIP(ip net.IP) bool { 250 | for _, block := range privateIPBlocks { 251 | if block.Contains(ip) { 252 | return true 253 | } 254 | } 255 | return false 256 | } 257 | 258 | func privateIP() string { 259 | net.InterfaceAddrs() 260 | addrs, err := net.InterfaceAddrs() 261 | if err != nil { 262 | log.Printf("Error: %v", err) 263 | return "0.0.0.0" 264 | } 265 | 266 | for _, addr := range addrs { 267 | ipnet, ok := addr.(*net.IPNet) 268 | if ok && isPrivateIP(ipnet.IP) { 269 | return ipnet.IP.String() 270 | } 271 | } 272 | return "0.0.0.0" 273 | } 274 | 275 | type Config struct { 276 | Gate struct { 277 | Scheme string `json:"scheme"` 278 | Host string `json:"host"` 279 | Port int `json:"port"` 280 | Client struct { 281 | Default string `json:"default"` 282 | } `json:"client"` 283 | } `json:"gate"` 284 | Platforms struct { 285 | Slack map[string]string `json:"slack"` 286 | Line map[string]string `json:"line"` 287 | Pixela map[string]string `json:"pixela"` 288 | } `json:"platforms"` 289 | } 290 | 291 | var configPath = flag.String("config", "$HOME/.config/gate/config.json", "") 292 | var config Config 293 | 294 | func main() { 295 | flag.Usage = func() { 296 | fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) 297 | flag.PrintDefaults() 298 | } 299 | flag.Parse() 300 | 301 | f, err := os.Open(os.ExpandEnv(*configPath)) 302 | if err != nil { 303 | log.Fatal(err) 304 | } 305 | err = json.NewDecoder(f).Decode(&config) 306 | if err != nil { 307 | log.Fatal(err) 308 | } 309 | 310 | http.HandleFunc("/post/", postHandler) 311 | http.HandleFunc("/config/", configHandler) 312 | 313 | scheme := config.Gate.Scheme 314 | if scheme == "" { 315 | scheme = "http" // default value 316 | } 317 | port := fmt.Sprintf(":%d", config.Gate.Port) 318 | if port == ":" { 319 | scheme = ":5731" // default value 320 | } 321 | fmt.Fprintf(os.Stdout, "Listening for HTTP on %s://%s%s\n\n", scheme, config.Gate.Host, port) 322 | fmt.Fprintf(os.Stdout, 323 | "Please run the command to fetch cli.json\n\n curl -sL %s://%s%s%s > ~/.config/gate/cli.json\n\n", 324 | scheme, privateIP(), port, internal.ConfigCLIJSON) 325 | log.Fatal(http.ListenAndServe(port, http.DefaultServeMux)) 326 | } 327 | -------------------------------------------------------------------------------- /cmd/gatecli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "flag" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "net/http" 11 | "net/url" 12 | "os" 13 | "strings" 14 | ) 15 | 16 | func run() error { 17 | b, err := ioutil.ReadAll(os.Stdin) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | text := strings.TrimSpace(string(b)) 23 | if text == "" { 24 | return nil 25 | } 26 | 27 | if *code { 28 | text = fmt.Sprintf("```\n%s\n```", text) 29 | } else if *quote { 30 | reader := strings.NewReader(text) 31 | scanner := bufio.NewScanner(reader) 32 | scanner.Split(bufio.ScanLines) 33 | text = "" 34 | for scanner.Scan() { 35 | text += fmt.Sprintf("> %s\n", scanner.Text()) 36 | } 37 | } 38 | 39 | target := flag.Arg(0) 40 | if target == "" { 41 | target = config.Gate.Client.Default 42 | } 43 | 44 | val := url.Values{ 45 | "target": []string{target}, 46 | "text": []string{text}, 47 | } 48 | 49 | scheme := config.Gate.Scheme 50 | if scheme == "" { 51 | scheme = "http" // default value 52 | } 53 | port := fmt.Sprintf(":%d", config.Gate.Port) 54 | if port == ":" { 55 | scheme = ":5731" // default value 56 | } 57 | 58 | url := fmt.Sprintf("%s://%s%s/post/", scheme, config.Gate.Host, port) 59 | resp, err := http.PostForm(url, val) 60 | if err != nil { 61 | return err 62 | } 63 | defer resp.Body.Close() 64 | b, err = ioutil.ReadAll(resp.Body) 65 | if err != nil { 66 | return err 67 | } 68 | fmt.Print(string(b)) 69 | return nil 70 | } 71 | 72 | type Config struct { 73 | Gate struct { 74 | Scheme string `json:"scheme"` 75 | Host string `json:"host"` 76 | Port int `json:"port"` 77 | Client struct { 78 | Default string `json:"default"` 79 | } `json:"client"` 80 | } `json:"gate"` 81 | } 82 | 83 | var code = flag.Bool("code", false, "Be inline-code") 84 | var quote = flag.Bool("quote", false, "Be quote-text") 85 | var configPath = flag.String("config", "$HOME/.config/gate/cli.json", "") 86 | var config Config 87 | 88 | func main() { 89 | flag.Usage = func() { 90 | fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) 91 | flag.PrintDefaults() 92 | } 93 | flag.Parse() 94 | 95 | f, err := os.Open(os.ExpandEnv(*configPath)) 96 | if err != nil { 97 | log.Fatal(err) 98 | } 99 | err = json.NewDecoder(f).Decode(&config) 100 | if err != nil { 101 | log.Fatal(err) 102 | } 103 | 104 | err = run() 105 | if err != nil { 106 | log.Fatal(err) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /cmd/internal/internal.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | const ConfigCLIJSON = "/config/cli.json" 4 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package gate 2 | 3 | import "net/http" 4 | 5 | type ( 6 | // Config provides service configuration for service. 7 | Config struct { 8 | // The HTTP client to use when sending requests. 9 | HTTPClient *http.Client 10 | 11 | ID string 12 | AccessToken string 13 | } 14 | ) 15 | 16 | var ( 17 | defaultConfig = *(NewConfig().WithHTTPClient(http.DefaultClient)) 18 | ) 19 | 20 | // NewConfig returns a pointer of new Config objects. 21 | func NewConfig() *Config { 22 | return &Config{} 23 | } 24 | 25 | // WithHTTPClient sets a config HTTPClient value returning a Config pointer 26 | // for chaining. 27 | func (c *Config) WithHTTPClient(client *http.Client) *Config { 28 | c.HTTPClient = client 29 | return c 30 | } 31 | 32 | // WithID sets an id value to verify service returning 33 | // a Config pointer for chaining. 34 | func (c *Config) WithID(id string) *Config { 35 | c.ID = id 36 | return c 37 | } 38 | 39 | // WithAccessToken sets a access token value to verify service returning 40 | // a Config pointer for chaining. 41 | func (c *Config) WithAccessToken(token string) *Config { 42 | c.AccessToken = token 43 | return c 44 | } 45 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package gate 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestConfig(t *testing.T) { 11 | assert := assert.New(t) 12 | 13 | conf := NewConfig() 14 | assert.NotNil(conf) 15 | assert.Nil(conf.HTTPClient) 16 | assert.Empty(conf.AccessToken) 17 | 18 | conf.WithHTTPClient(http.DefaultClient). 19 | WithAccessToken("access-token"). 20 | WithID("id") 21 | assert.Equal(http.DefaultClient, conf.HTTPClient) 22 | assert.Equal("id", conf.ID) 23 | } 24 | -------------------------------------------------------------------------------- /gate.go: -------------------------------------------------------------------------------- 1 | package gate 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | ) 7 | 8 | // Version represents gate's semantic version. 9 | const Version = "v1.2.1" 10 | 11 | const ( 12 | bodyTypeURLEncoded = "application/x-www-form-urlencoded" 13 | bodyTypeJSON = "application/json; charset=utf-8" 14 | ) 15 | 16 | type ( 17 | service struct { 18 | config *Config 19 | baseURL *url.URL 20 | } 21 | ) 22 | 23 | // newService returns a new service. If a nil Config is 24 | // provided, DefaultConfig will be used. 25 | func newService(config *Config) *service { 26 | if config == nil { 27 | c := defaultConfig 28 | config = &c 29 | } 30 | 31 | if config.HTTPClient == nil { 32 | config.HTTPClient = http.DefaultClient 33 | } 34 | 35 | return &service{ 36 | config: config, 37 | } 38 | } 39 | 40 | // withBaseURL sets a base url value returning a service pointer 41 | // for chaining. 42 | func (s *service) withBaseURL(baseURL string) *service { 43 | s.baseURL, _ = url.Parse(baseURL) 44 | return s 45 | } 46 | -------------------------------------------------------------------------------- /gate_test.go: -------------------------------------------------------------------------------- 1 | package gate 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestService(t *testing.T) { 10 | 11 | t.Run("Service with config", func(t *testing.T) { 12 | t.Parallel() 13 | 14 | svc := newService(NewConfig()) 15 | assert.NotNil(t, svc) 16 | assert.NotNil(t, svc.config) 17 | assert.NotNil(t, svc.config.HTTPClient) 18 | 19 | svc.withBaseURL("http://example.com?v=1") 20 | assert.Equal(t, "http://example.com?v=1", svc.baseURL.String()) 21 | }) 22 | 23 | t.Run("Service with nil config", func(t *testing.T) { 24 | t.Parallel() 25 | 26 | svc := newService(nil) 27 | assert.NotNil(t, svc) 28 | assert.NotNil(t, svc.config) 29 | assert.NotNil(t, svc.config.HTTPClient) 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kaneshin/gate 2 | 3 | go 1.15 4 | 5 | require github.com/stretchr/testify v1.4.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 7 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 10 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 11 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 12 | -------------------------------------------------------------------------------- /line.go: -------------------------------------------------------------------------------- 1 | package gate 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net/http" 7 | ) 8 | 9 | // lineNotifyAPIURL is Notify API URL for LINE. 10 | const lineNotifyAPIURL = "https://notify-api.line.me/api/notify" 11 | 12 | type ( 13 | // LINENotifyService is a slack incoming webhook service. 14 | LINENotifyService struct { 15 | *service 16 | } 17 | ) 18 | 19 | // NewLINENotifyService returns a new LINENotifyService. 20 | func NewLINENotifyService(config *Config) *LINENotifyService { 21 | svc := &LINENotifyService{ 22 | service: newService(config).withBaseURL(lineNotifyAPIURL), 23 | } 24 | return svc 25 | } 26 | 27 | // Post posts a payload to LINE. 28 | func (s LINENotifyService) Post(contentType string, body io.Reader) (*http.Response, error) { 29 | req, err := http.NewRequest("POST", s.baseURL.String(), body) 30 | if err != nil { 31 | return nil, err 32 | } 33 | req.Header.Add("Content-Type", contentType) 34 | req.Header.Add("Authorization", "Bearer "+s.service.config.AccessToken) 35 | return s.config.HTTPClient.Do(req) 36 | } 37 | 38 | // PostMessagePayload posts a message payload to LINE. 39 | func (s LINENotifyService) PostMessagePayload(payload MessagePayload) (*http.Response, error) { 40 | buf := bytes.NewBufferString("message=" + payload.Message) 41 | return s.Post(bodyTypeURLEncoded, buf) 42 | } 43 | -------------------------------------------------------------------------------- /payload.go: -------------------------------------------------------------------------------- 1 | package gate 2 | 3 | type ( 4 | // TextPayload represents a text payload. 5 | TextPayload struct { 6 | Text string `json:"text"` 7 | } 8 | 9 | // MessagePayload represents a message payload. 10 | MessagePayload struct { 11 | Message string `json:"message"` 12 | } 13 | 14 | // GraphPayload represents a graph payload. 15 | GraphPayload struct { 16 | ID string `json:"id"` 17 | Date string `json:"date"` 18 | Quantity string `json:"quantity"` 19 | } 20 | ) 21 | -------------------------------------------------------------------------------- /pixela.go: -------------------------------------------------------------------------------- 1 | package gate 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "net/http" 8 | "path" 9 | ) 10 | 11 | // pixelaAPIURL is an API URL for Pixela. 12 | const pixelaAPIURL = "https://pixe.la/v1" 13 | 14 | type ( 15 | // PixelaService is a slack incoming webhook service. 16 | PixelaService struct { 17 | *service 18 | } 19 | ) 20 | 21 | // NewPixelaService returns a new PixelaService. 22 | func NewPixelaService(config *Config) *PixelaService { 23 | return &PixelaService{ 24 | service: newService(config).withBaseURL(pixelaAPIURL), 25 | } 26 | } 27 | 28 | // Post posts a payload to pixela. 29 | func (s PixelaService) Post(id string, body io.Reader) (*http.Response, error) { 30 | u := *s.baseURL 31 | u.Path = path.Join(u.Path, "users", s.config.ID, "graphs", id) 32 | req, err := http.NewRequest("POST", u.String(), body) 33 | if err != nil { 34 | return nil, err 35 | } 36 | req.Header.Add("Content-Type", bodyTypeJSON) 37 | req.Header.Add("X-USER-TOKEN", s.service.config.AccessToken) 38 | return s.config.HTTPClient.Do(req) 39 | } 40 | 41 | // PostGraphPayload posts a graph payload to pixela. 42 | func (s PixelaService) PostGraphPayload(payload GraphPayload) (*http.Response, error) { 43 | b, err := json.Marshal(payload) 44 | if err != nil { 45 | return nil, err 46 | } 47 | buf := bytes.NewBuffer(b) 48 | return s.Post(payload.ID, buf) 49 | } 50 | 51 | // Put updates a graph. 52 | func (s PixelaService) Put(id, suffix string) (*http.Response, error) { 53 | u := *s.baseURL 54 | u.Path = path.Join(u.Path, "users", s.config.ID, "graphs", id, suffix) 55 | req, err := http.NewRequest("PUT", u.String(), nil) 56 | if err != nil { 57 | return nil, err 58 | } 59 | req.Header.Add("Content-Length", "0") 60 | req.Header.Add("X-USER-TOKEN", s.service.config.AccessToken) 61 | return s.config.HTTPClient.Do(req) 62 | } 63 | 64 | // Increment increments quantity of a graph. 65 | func (s PixelaService) Increment(payload GraphPayload) (*http.Response, error) { 66 | return s.Put(payload.ID, "increment") 67 | } 68 | 69 | // Decrement decrements quantity of a graph. 70 | func (s PixelaService) Decrement(payload GraphPayload) (*http.Response, error) { 71 | return s.Put(payload.ID, "decrement") 72 | } 73 | -------------------------------------------------------------------------------- /slack.go: -------------------------------------------------------------------------------- 1 | package gate 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "net/http" 8 | ) 9 | 10 | type ( 11 | // SlackIncomingService is a slack incoming webhook service. 12 | SlackIncomingService struct { 13 | *service 14 | } 15 | ) 16 | 17 | // NewSlackIncomingService returns a new SlackIncomingService. 18 | func NewSlackIncomingService(config *Config) *SlackIncomingService { 19 | return &SlackIncomingService{ 20 | service: newService(config), 21 | } 22 | } 23 | 24 | // WithBaseURL sets a base url value returning a service pointer 25 | // for chaining. 26 | func (s *SlackIncomingService) WithBaseURL(baseURL string) *SlackIncomingService { 27 | s.service.withBaseURL(baseURL) 28 | return s 29 | } 30 | 31 | // Post posts a payload to slack. 32 | func (s SlackIncomingService) Post(contentType string, body io.Reader) (*http.Response, error) { 33 | req, err := http.NewRequest("POST", s.baseURL.String(), body) 34 | if err != nil { 35 | return nil, err 36 | } 37 | req.Header.Add("Content-Type", contentType) 38 | return s.config.HTTPClient.Do(req) 39 | } 40 | 41 | // PostTextPayload posts a text payload to slack. 42 | func (s SlackIncomingService) PostTextPayload(payload TextPayload) (*http.Response, error) { 43 | b, err := json.Marshal(payload) 44 | if err != nil { 45 | return nil, err 46 | } 47 | buf := bytes.NewBuffer(b) 48 | return s.Post(bodyTypeJSON, buf) 49 | } 50 | -------------------------------------------------------------------------------- /slack_test.go: -------------------------------------------------------------------------------- 1 | package gate 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSlack(t *testing.T) { 10 | 11 | t.Run("SlackIncomingService", func(t *testing.T) { 12 | t.Parallel() 13 | 14 | svc := NewSlackIncomingService(NewConfig()) 15 | assert.NotNil(t, svc) 16 | assert.Nil(t, svc.baseURL) 17 | }) 18 | } 19 | --------------------------------------------------------------------------------