aboutsummaryrefslogtreecommitdiff
path: root/client.go
diff options
context:
space:
mode:
authorShulhan <m.shulhan@gmail.com>2020-02-28 01:07:17 +0700
committerShulhan <m.shulhan@gmail.com>2020-03-02 23:42:00 +0700
commitbf9240a2b91ec855a772c5fdc98ddd89ab28b6e5 (patch)
tree6a11e15f88463b8d82c2a4ca6e6d609fb1d78676 /client.go
downloadkamusku-bf9240a2b91ec855a772c5fdc98ddd89ab28b6e5.tar.xz
kbbi: The command line interface and Go library for kbbi.kemdikbud.go.id
Diffstat (limited to 'client.go')
-rw-r--r--client.go304
1 files changed, 304 insertions, 0 deletions
diff --git a/client.go b/client.go
new file mode 100644
index 0000000..69401cd
--- /dev/null
+++ b/client.go
@@ -0,0 +1,304 @@
+// Copyright 2020, Shulhan <m.shulhan@gmail.com>. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package kbbi
+
+import (
+ "bytes"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "net/http/cookiejar"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+
+ "golang.org/x/net/html"
+ "golang.org/x/net/publicsuffix"
+)
+
+const (
+ maxPageNumber = 501
+ defTimeout = 20 * time.Second
+)
+
+//
+// Client for KBBI web using HTTP.
+//
+type Client struct {
+ cookieURL *url.URL
+ httpc *http.Client
+}
+
+//
+// New create and initialize new client for KBBI web.
+//
+func New(cookies []*http.Cookie) (cl *Client, err error) {
+ cookieURL, err := url.Parse(baseURL)
+ if err != nil {
+ return nil, fmt.Errorf("New: %w", err)
+ }
+
+ jarOpt := &cookiejar.Options{
+ PublicSuffixList: publicsuffix.List,
+ }
+
+ jar, err := cookiejar.New(jarOpt)
+ if err != nil {
+ return nil, err
+ }
+
+ if cookies != nil {
+ jar.SetCookies(cookieURL, cookies)
+ }
+
+ cl = &Client{
+ cookieURL: cookieURL,
+ httpc: &http.Client{
+ Jar: jar,
+ Timeout: defTimeout,
+ },
+ }
+
+ return cl, nil
+}
+
+//
+// ListKataDasar get list of kata dasar
+//
+func (cl Client) ListKataDasar() (kataDasar daftarKata, err error) {
+ params := url.Values{
+ paramNameMasukan: []string{paramValueDasar},
+ paramNameMasukanLengkap: []string{paramValueDasar},
+ }
+
+ urlPage := baseURL + "/Cari/Jenis?"
+
+ kataDasar = make(daftarKata)
+
+ for pageNumber := 1; pageNumber <= maxPageNumber; pageNumber++ {
+ params.Set(paramNamePage, strconv.Itoa(pageNumber))
+
+ req, err := http.NewRequest(http.MethodGet, urlPage+params.Encode(), nil)
+ if err != nil {
+ return kataDasar, err
+ }
+
+ res, err := cl.httpc.Do(req)
+ if err != nil {
+ return kataDasar, fmt.Errorf("ListKataDasar: page %d: %w",
+ pageNumber, err)
+ }
+
+ defer res.Body.Close()
+
+ body, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ return kataDasar, fmt.Errorf("ListKataDasar: page %d: %w",
+ pageNumber, err)
+ }
+
+ got, err := cl.parseHTMLKataDasar(body)
+ if err != nil {
+ return kataDasar, fmt.Errorf("ListKataDasar: page %d: %w",
+ pageNumber, err)
+ }
+ if len(got) == 0 {
+ break
+ }
+
+ kataDasar.merge(got)
+
+ log.Printf("ListKataDasar: halaman %d, jumlah kata %d, total kata %d",
+ pageNumber, len(got), len(kataDasar))
+ }
+
+ return kataDasar, nil
+}
+
+//
+// Login authenticate the client using username and password.
+//
+func (cl *Client) Login(user, pass string) (cookies []*http.Cookie, err error) {
+ tokenLogin, err := cl.preLogin()
+ if err != nil {
+ return nil, fmt.Errorf("Login: %w", err)
+ }
+
+ params := url.Values{
+ paramNameRequestVerificationToken: []string{tokenLogin},
+ paramNamePosel: []string{user},
+ paramNameKataSandi: []string{pass},
+ paramNameIngatSaya: []string{paramValueFalse},
+ }
+
+ reqBody := strings.NewReader(params.Encode())
+
+ req, err := http.NewRequest(http.MethodPost, loginURL, reqBody)
+ if err != nil {
+ return nil, fmt.Errorf("Login: %w", err)
+ }
+
+ req.Header.Set(headerNameContentType, headerValueContentType)
+
+ res, err := cl.httpc.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("Login: %w", err)
+ }
+
+ defer res.Body.Close()
+
+ resBody, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ return nil, fmt.Errorf("Login: %w", err)
+ }
+
+ if res.StatusCode >= http.StatusBadRequest {
+ return nil, fmt.Errorf("Login: %d %s", res.StatusCode, resBody)
+ }
+
+ cookies = cl.httpc.Jar.Cookies(cl.cookieURL)
+
+ return cookies, nil
+}
+
+//
+// SetCookies for HTTP request that need an authentication.
+//
+func (cl *Client) SetCookies(cookies []*http.Cookie) {
+ if len(cookies) > 0 {
+ cl.httpc.Jar.SetCookies(cl.cookieURL, cookies)
+ }
+}
+
+func (cl Client) parseHTMLKataDasar(htmlBody []byte) (kataDasar daftarKata, err error) {
+ node, err := html.Parse(bytes.NewReader(htmlBody))
+ if err != nil {
+ return nil, err
+ }
+
+ kataDasar = make(daftarKata)
+
+ var prev *html.Node
+
+ for {
+ switch {
+ case node.FirstChild != nil && node.FirstChild != prev && node.LastChild != prev:
+ node = node.FirstChild
+ case node.NextSibling != nil:
+ node = node.NextSibling
+ default:
+ prev = node
+ node = node.Parent
+ }
+ if node == nil {
+ break
+ }
+
+ if node.Type != html.ElementNode {
+ continue
+ }
+ if node.Data != elementTypeAnchor {
+ continue
+ }
+ for _, attr := range node.Attr {
+ if attr.Key != attrNameHref {
+ continue
+ }
+ if !strings.HasPrefix(attr.Val, entriURL) {
+ continue
+ }
+ k := strings.TrimSpace(node.FirstChild.Data)
+ kataDasar[k] = struct{}{}
+ }
+ }
+
+ return kataDasar, nil
+}
+
+//
+// parseHTMLLogin get the token at the form login.
+//
+func (cl Client) parseHTMLLogin(htmlBody []byte) (token string, err error) {
+ node, err := html.Parse(bytes.NewReader(htmlBody))
+ if err != nil {
+ return "", err
+ }
+
+ var prev *html.Node
+
+ for {
+ switch {
+ case node.FirstChild != nil && node.FirstChild != prev && node.LastChild != prev:
+ node = node.FirstChild
+ case node.NextSibling != nil:
+ node = node.NextSibling
+ default:
+ prev = node
+ node = node.Parent
+ }
+ if node == nil {
+ break
+ }
+
+ if node.Type != html.ElementNode {
+ continue
+ }
+ if node.Data != elementTypeInput {
+ continue
+ }
+ for _, attr := range node.Attr {
+ if attr.Key != attrNameName {
+ continue
+ }
+
+ token = getAttrValue(node.Attr)
+ if len(token) > 0 {
+ return token, nil
+ }
+ }
+ }
+
+ return "", fmt.Errorf("token login not found")
+}
+
+//
+// preLogin initialize the client to get the first cookie.
+//
+func (cl *Client) preLogin() (token string, err error) {
+ req, err := http.NewRequest(http.MethodGet, loginURL, nil)
+ if err != nil {
+ return "", err
+ }
+
+ res, err := cl.httpc.Do(req)
+ if err != nil {
+ return "", err
+ }
+
+ defer res.Body.Close()
+
+ body, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ return "", err
+ }
+
+ token, err = cl.parseHTMLLogin(body)
+ if err != nil {
+ return "", err
+ }
+
+ return token, nil
+}
+
+func getAttrValue(attrs []html.Attribute) string {
+ for _, attr := range attrs {
+ if attr.Key == attrNameValue {
+ return attr.Val
+ }
+ }
+ return ""
+}