Skip to content

Commit

Permalink
init commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Largitdata authored and Largitdata committed Oct 15, 2021
0 parents commit 14f878a
Show file tree
Hide file tree
Showing 5 changed files with 294 additions and 0 deletions.
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/HoMuChen/linenotify

go 1.17
160 changes: 160 additions & 0 deletions linenotify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package linenotify

import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os/exec"
"strings"
"runtime"
)

const (
LINE_AUTH_PATH = "https://notify-bot.line.me/oauth/authorize"
LINE_TOKEN_PATH = "https://notify-bot.line.me/oauth/token"
NOTIFY_HOST = "https://notify-api.line.me/api/notify"
)

type client struct {
id string
secret string
callback *url.URL
token string
done chan error
}

func New(id, secret, callback string) (*client, error) {
if id == "" || secret == "" {
return nil, fmt.Errorf("client id: %s and secret: %s can not be empty", id, secret)
}

cb, err := url.Parse(callback)
if err != nil {
return nil, fmt.Errorf("fail to pars callback url with err: %v", err)
}

c := &client{
id: id,
secret: secret,
callback: cb,
token: "",
done: make(chan error),
}

return c, nil
}

func (c *client) Login() (string, error) {
if err := openbrowser(c.AuthUrl()); err != nil {
return "", fmt.Errorf("fail to open browser, %v", err)
}

if err := c.waitForCallback(); err != nil {
return "", fmt.Errorf("fail to start server waiting for callback code to exchange access toekn: %v", err)
}

return c.token, nil
}

func (c *client) AuthUrl() string {
values := url.Values{}
values.Add("client_id", c.id)
values.Add("response_type", "code")
values.Add("scope", "notify")
values.Add("redirect_uri", c.callback.String())
values.Add("state", "123")
query := values.Encode()

url := LINE_AUTH_PATH + "?" + query

return url
}

func (c *client) makeTokenBody(code string) string {
values := url.Values{}
values.Add("client_id", c.id)
values.Add("client_secret", c.secret)
values.Add("redirect_uri", c.callback.String())
values.Add("code", code)
values.Add("grant_type", "authorization_code")
body := values.Encode()

return body
}

func (c *client) GetToken(code string) (string, error) {
body := c.makeTokenBody(code)
req, _ := http.NewRequest("POST", LINE_TOKEN_PATH, strings.NewReader(body))
req.Header.Add("Content-type", "application/x-www-form-urlencoded")

res, err := http.DefaultClient.Do(req)
if err != nil {
return "", fmt.Errorf("fail to get token with err: %v", err)
}
defer res.Body.Close()

data, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", fmt.Errorf("fail to read response body with err %v", err)
}

var buf struct {
Access_token string `json:access_token`
Status int `json:status`
Message string `json:message`
}
if err := json.Unmarshal(data, &buf); err != nil {
return "", fmt.Errorf("fail to json parse body with err %v", err)
}

if buf.Status >= 400 {
return "", fmt.Errorf("fail to get token with status: %v and message: %s", buf.Status, buf.Message)
}

return buf.Access_token, nil
}

func (c *client) handler(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
token, err := c.GetToken(code)
if err != nil {
c.done <- err
return
}

w.Write([]byte(token))

c.token = token
c.done <- nil
}

func (c *client) waitForCallback() error {
server := &http.Server{
Addr: c.callback.Host,
Handler: http.HandlerFunc(c.handler),
}

go server.ListenAndServe()
defer server.Close()

return <-c.done
}

func openbrowser(url string) error {
var err error

switch runtime.GOOS {
case "linux":
err = exec.Command("xdg-open", url).Start()
case "windows":
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case "darwin":
err = exec.Command("open", url).Start()
default:
err = fmt.Errorf("unsupported platform")
}

return err
}
49 changes: 49 additions & 0 deletions linenotify_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package linenotify

import (
"testing"
)

const (
ID = "id"
SECRET = "secret"
CB = "http://localhost:5000/callback"
)

func TestNewWithoutSecret(t *testing.T) {
_, err := New(ID, "", "")

if err == nil {
t.Errorf("error should not nil")
}
}

func TestNew(t *testing.T) {
_, err := New(ID, SECRET, CB)

if err != nil {
t.Errorf("error should be nil, but got: %v", err)
}
}

func TestAuthUrl(t *testing.T) {
c, _ := New(ID, SECRET, CB)

url := c.AuthUrl()
expected := "https://notify-bot.line.me/oauth/authorize?client_id=id&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2Fcallback&response_type=code&scope=notify&state=123"

if url != expected {
t.Errorf("expected: %s, but got: %s", expected, url)
}
}

func TestMakeTokenBody(t *testing.T) {
c, _ := New(ID, SECRET, CB)

body := c.makeTokenBody("abctoken")
expected := "client_id=id&client_secret=secret&code=abctoken&grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A5000%2Fcallback"

if body != expected {
t.Errorf("expected: %s, but got: %s", expected, body)
}
}
50 changes: 50 additions & 0 deletions send.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package linenotify

import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
)


func (c *client) Send(token, message string) error {
req := c.makeNotifyRequest(token, message)
res, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("fail to post to notify api with err: %v", err)
}
defer res.Body.Close()

data, err := ioutil.ReadAll(res.Body)
if err != nil {
return fmt.Errorf("fail to read response body with err %v", err)
}

var buf struct {
Status int `json:status`
Message string `json:message`
}
if err := json.Unmarshal(data, &buf); err != nil {
return fmt.Errorf("fail to json parse body with err %v", err)
}
if buf.Status >= 400 {
return fmt.Errorf("fail to send message with status: %v and res message: %s", buf.Status, buf.Message)
}

return nil
}

func (c *client) makeNotifyRequest(token, message string) *http.Request {
values := url.Values{}
values.Add("message", message)
body := values.Encode()

req, _ := http.NewRequest("POST", NOTIFY_HOST, strings.NewReader(body))
req.Header.Add("Content-type", "application/x-www-form-urlencoded")
req.Header.Add("Authorization", "Bearer " + token)

return req
}
32 changes: 32 additions & 0 deletions send_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package linenotify

import (
"testing"
"net/http"
"strings"
)

func TestSend(t *testing.T) {
c, _ := New(ID, SECRET, CB)

message := "this is a message for test"
req := c.makeNotifyRequest("token", message)

if req.Method != http.MethodPost {
t.Error()
}
if req.Header.Get("Content-Type") != "application/x-www-form-urlencoded" {
t.Error()
}
if !strings.Contains(req.Header.Get("Authorization"), "token") {
t.Error()
}

err := req.ParseForm()
if err != nil {
t.Error(err)
}
if req.Form.Get("message") != message {
t.Errorf("form data with key message should be %s, but got: %s", message, req.Form.Get("message"))
}
}

0 comments on commit 14f878a

Please sign in to comment.