From a6629b6a1f40616dbf3e5f78c76d601f3b89127a Mon Sep 17 00:00:00 2001 From: Shulhan Date: Wed, 17 Apr 2024 23:31:39 +0700 Subject: all: rename the module and package to "kbbi" Since this module is connecting to KBBI server directly, the module and package name now reflect that, hence we rename them to kbbi. Another reason is we have an internal module named kamusku, in another repository. --- Makefile | 2 +- README.md | 14 +- client.go | 389 ++++++++++++++++++++++++++++++++++++++++++++++++++++ client_test.go | 28 ++++ cmd/kamusku/main.go | 113 --------------- cmd/kbbi/main.go | 113 +++++++++++++++ go.mod | 2 +- kamusku.go | 9 -- kbbi.go | 9 ++ kbbi_client.go | 389 ---------------------------------------------------- kbbi_client_test.go | 28 ---- lookup_response.go | 2 +- word.go | 2 +- word_definition.go | 2 +- word_test.go | 2 +- words.go | 2 +- 16 files changed, 553 insertions(+), 553 deletions(-) create mode 100644 client.go create mode 100644 client_test.go delete mode 100644 cmd/kamusku/main.go create mode 100644 cmd/kbbi/main.go delete mode 100644 kamusku.go create mode 100644 kbbi.go delete mode 100644 kbbi_client.go delete mode 100644 kbbi_client_test.go diff --git a/Makefile b/Makefile index 4090794..c033f6f 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ lint: .PHONY: install install: - go install ./cmd/kamusku + go install ./cmd/kbbi .PHONY: serve-docs serve-docs: diff --git a/README.md b/README.md index d9f8f19..4d7f595 100644 --- a/README.md +++ b/README.md @@ -3,22 +3,22 @@ SPDX-FileCopyrightText: 2024 M. Shulhan SPDX-License-Identifier: GPL-3.0-or-later --> -# kamusku +# kbbi -kamusku adalah Go module yang berisi pustaka dan program untuk mencari +kbbi adalah Go module yang berisi pustaka dan program untuk mencari definisi kata Bahasa Indonesia dari situs resmi KBBI. -## Program kamusku +## Program kbbi -Program kamusku yaitu antar-muka untuk mencari definisi dari kata lewat baris +Program kbbi yaitu antar-muka untuk mencari definisi dari kata lewat baris perintah. Program ini sangat sederhana, caranya yaitu dengan memberikan kata yang dicari setelah nama program, misalnya, ``` -$ kamusku kamus,bahasa +$ kbbi kamus,bahasa ``` Maka akan mencetak definisi dari kata "kamus" dan "bahasa" ke layar, @@ -56,7 +56,7 @@ Maka akan mencetak definisi dari kata "kamus" dan "bahasa" ke layar, Bot untuk aplikasi Telegram: https://t.me/KamuskuBot Untuk saat ini, KamuskuBot hanya punya satu perintah yaitu "/definisi". Cara -menggunakan perintah ini hampir sama dengan program kamusku yaitu dengan +menggunakan perintah ini hampir sama dengan program kbbi yaitu dengan memberikan kata yang dicari, contohnya, ``` @@ -66,7 +66,7 @@ memberikan kata yang dicari, contohnya, ## LISENSI ``` -kamusku - The Go module library and program as interface to KBBI server. +kbbi - The Go module library and program as interface to KBBI server. Copyright (C) 2020-2024 M. Shulhan This program is free software: you can redistribute it and/or modify it diff --git a/client.go b/client.go new file mode 100644 index 0000000..39dd031 --- /dev/null +++ b/client.go @@ -0,0 +1,389 @@ +// SPDX-FileCopyrightText: 2020 M. Shulhan +// SPDX-License-Identifier: GPL-3.0-or-later + +package kbbi + +import ( + "bytes" + "encoding/gob" + "errors" + "fmt" + "io" + "log" + "net/http" + "net/http/cookiejar" + "net/url" + "os" + "path/filepath" + "strconv" + "strings" + "time" + + "git.sr.ht/~shulhan/pakakeh.go/lib/html" + libhttp "git.sr.ht/~shulhan/pakakeh.go/lib/http" + "golang.org/x/net/publicsuffix" +) + +const ( + kbbiUrlBase = "https://kbbi.kemdikbud.go.id" + kbbiUrlLogin = kbbiUrlBase + "/Account/Login" + kbbiPathEntri = "/entri/" + + attrNameClass = "class" + attrNameHref = "href" + attrNameTitle = "title" + attrNameValue = "value" + + attrValueRootWord = "rootword" + + paramNameMasukan = "masukan" + paramNameMasukanLengkap = "masukanLengkap" + paramNameIngatSaya = "IngatSaya" + paramNameKataSandi = "KataSandi" + paramNamePage = "page" + paramNamePosel = "Posel" + paramNameRequestVerificationToken = "__RequestVerificationToken" //nolint: gosec + + paramValueDasar = "dasar" + paramValueFalse = "false" + + tagNameAnchor = "a" + tagNameFont = "font" + tagNameHeader2 = "h2" + tagNameInput = "input" + tagNameItalic = "i" + tagNameOrderedList = "ol" + tagNameSpan = "span" + tagNameUnorderedList = "ul" + + cookieFile = "cookie" + configDir = `kbbi` + defTimeout = 20 * time.Second + maxPageNumber = 501 +) + +// Client for official KBBI web using HTTP. +type Client struct { + baseDir string + cookieURL *url.URL + cookies []*http.Cookie + httpc *http.Client +} + +// NewClient create and initialize new client that connect directly to +// KBBI official website. +func NewClient() (cl *Client, err error) { + cookieURL, err := url.Parse(kbbiUrlBase) + 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, fmt.Errorf("New: %w", err) + } + + cl = &Client{ + cookieURL: cookieURL, + httpc: &http.Client{ + Jar: jar, + Timeout: defTimeout, + }, + } + + err = cl.loadCookies() + if err != nil { + return nil, fmt.Errorf("New: %w", err) + } + + if cl.cookies != nil { + jar.SetCookies(cookieURL, cl.cookies) + } + + return cl, nil +} + +// Lookup lookup definition of one or more words. +func (cl *Client) Lookup(ins []string) (res LookupResponse, err error) { + res = make(LookupResponse, len(ins)) + + for _, in := range ins { + _, ok := res[in] + if ok { + continue + } + + kata := &Word{} + res[in] = kata + + entriURL := kbbiUrlBase + kbbiPathEntri + in + httpRes, err := cl.httpc.Get(entriURL) + if err != nil { + kata.err = err + continue + } + + defer httpRes.Body.Close() + + body, err := io.ReadAll(httpRes.Body) + if err != nil { + kata.err = err + continue + } + + err = kata.parseHTMLEntri(in, body) + if err != nil { + kata.err = err + } + + if len(kata.Definition) == 0 && len(kata.Message) == 0 { + kata.Message = "Entri tidak ditemukan" + } + } + + return res, nil +} + +// ListRootWords list all of the root words in dictionary. +func (cl *Client) ListRootWords() (rootWords Words, err error) { + params := url.Values{ + paramNameMasukan: []string{paramValueDasar}, + paramNameMasukanLengkap: []string{paramValueDasar}, + } + + urlPage := kbbiUrlBase + "/Cari/Jenis?" + + rootWords = make(Words) + + 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 rootWords, err + } + + res, err := cl.httpc.Do(req) + if err != nil { + return rootWords, fmt.Errorf("ListRootWords: page %d: %w", + pageNumber, err) + } + + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + return rootWords, fmt.Errorf("ListRootWords: page %d: %w", + pageNumber, err) + } + + got, err := cl.parseHTMLRootWords(body) + if err != nil { + return rootWords, fmt.Errorf("ListRootWords: page %d: %w", + pageNumber, err) + } + if len(got) == 0 { + break + } + + rootWords.merge(got) + + log.Printf("ListRootWords: halaman %d, jumlah kata %d, total kata %d", + pageNumber, len(got), len(rootWords)) + } + + return rootWords, nil +} + +// IsAuthenticated will return true if the client already login; otherwise it +// will return false. +func (cl *Client) IsAuthenticated() bool { + return len(cl.cookies) > 0 +} + +// Login authenticate the client using user email and password. +func (cl *Client) Login(email, pass string) (err error) { + tokenLogin, err := cl.preLogin() + if err != nil { + return fmt.Errorf("Login: %w", err) + } + + params := url.Values{ + paramNameRequestVerificationToken: []string{tokenLogin}, + paramNamePosel: []string{email}, + paramNameKataSandi: []string{pass}, + paramNameIngatSaya: []string{paramValueFalse}, + } + + reqBody := strings.NewReader(params.Encode()) + + req, err := http.NewRequest(http.MethodPost, kbbiUrlLogin, reqBody) + if err != nil { + return fmt.Errorf("Login: %w", err) + } + + req.Header.Set(libhttp.HeaderContentType, libhttp.ContentTypeForm) + + res, err := cl.httpc.Do(req) + if err != nil { + return fmt.Errorf("Login: %w", err) + } + + defer res.Body.Close() + + resBody, err := io.ReadAll(res.Body) + if err != nil { + return fmt.Errorf("Login: %w", err) + } + + if res.StatusCode >= http.StatusBadRequest { + return fmt.Errorf("login: %d %s", res.StatusCode, resBody) + } + + cl.cookies = cl.httpc.Jar.Cookies(cl.cookieURL) + cl.setCookies() + cl.saveCookies() + + return nil +} + +// setCookies for HTTP request that need an authentication. +func (cl *Client) setCookies() { + cl.httpc.Jar.SetCookies(cl.cookieURL, cl.cookies) +} + +func (cl *Client) parseHTMLRootWords(htmlBody []byte) ( + rootWords Words, err error, +) { + iter, err := html.Parse(bytes.NewReader(htmlBody)) + if err != nil { + return nil, err + } + + rootWords = make(Words) + + for node := iter.Next(); node != nil; node = iter.Next() { + if !node.IsElement() { + continue + } + if node.Data != tagNameAnchor { + continue + } + hrefValue := node.GetAttrValue(attrNameHref) + if !strings.HasPrefix(hrefValue, kbbiPathEntri) { + continue + } + k := strings.TrimSpace(node.FirstChild.Data) + rootWords[k] = struct{}{} + } + + return rootWords, nil +} + +// parseHTMLLogin get the token at the form login. +func (cl *Client) parseHTMLLogin(htmlBody []byte) ( + token string, err error, +) { + iter, err := html.Parse(bytes.NewReader(htmlBody)) + if err != nil { + return "", err + } + + for node := iter.Next(); node != nil; node = iter.Next() { + if !node.IsElement() { + continue + } + if node.Data != tagNameInput { + continue + } + + token := node.GetAttrValue(attrNameValue) + 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, kbbiUrlLogin, nil) + if err != nil { + return "", err + } + + res, err := cl.httpc.Do(req) + if err != nil { + return "", err + } + + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + return "", err + } + + token, err = cl.parseHTMLLogin(body) + if err != nil { + return "", err + } + + return token, nil +} + +// loadCookies load the KBBI cookies from file. +func (cl *Client) loadCookies() (err error) { + cl.baseDir, err = os.UserConfigDir() + if err != nil { + return nil + } + + f := filepath.Join(cl.baseDir, configDir, cookieFile) + + _, err = os.Stat(f) + if errors.Is(err, os.ErrNotExist) { + return nil + } + + body, err := os.ReadFile(f) + if err != nil { + return fmt.Errorf("loadCookies: %w", err) + } + + dec := gob.NewDecoder(bytes.NewReader(body)) + + err = dec.Decode(&cl.cookies) + if err != nil { + return fmt.Errorf("loadCookies: %w", err) + } + + return nil +} + +// saveCookies store the client cookies to the file for future use. +func (cl *Client) saveCookies() { + err := os.MkdirAll(filepath.Join(cl.baseDir, configDir), 0700) + if err != nil { + log.Println("saveCookies:", err) + } + + f := filepath.Join(cl.baseDir, configDir, cookieFile) + + var buf bytes.Buffer + enc := gob.NewEncoder(&buf) + err = enc.Encode(cl.cookies) + if err != nil { + log.Println("saveCookies: ", err) + } + + err = os.WriteFile(f, buf.Bytes(), 0600) + if err != nil { + log.Println("saveCookies: ", err) + } +} diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..fa9e3c7 --- /dev/null +++ b/client_test.go @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2020 M. Shulhan +// SPDX-License-Identifier: GPL-3.0-or-later + +package kbbi + +import ( + "os" + "testing" +) + +func TestClient_parseHTMLKataDasar(t *testing.T) { + htmlBody, err := os.ReadFile(`testdata/kbbi_dasar.html`) + if err != nil { + t.Fatal(err) + } + + cl, err := NewClient() + if err != nil { + t.Fatal(err) + } + + got, err := cl.parseHTMLRootWords(htmlBody) + if err != nil { + t.Fatal(err) + } + + t.Logf("Root words: %v", got) +} diff --git a/cmd/kamusku/main.go b/cmd/kamusku/main.go deleted file mode 100644 index 796a31e..0000000 --- a/cmd/kamusku/main.go +++ /dev/null @@ -1,113 +0,0 @@ -// SPDX-FileCopyrightText: 2020 M. Shulhan -// SPDX-License-Identifier: GPL-3.0-or-later - -// Program kamusku is the command-line interface to Kamus Besar Bahasa -// Indonesia (KBBI). -package main - -import ( - "flag" - "fmt" - "log" - "sort" - - "git.sr.ht/~shulhan/kamusku" -) - -const ( - cmdNameSurel = "surel" - cmdNameSandi = "sandi" - cmdNameDaftarKataDasar = "daftar-kata-dasar" -) - -func main() { - var ( - isListRootWords bool - email string - pass string - ) - - log.SetFlags(0) - log.SetPrefix("kamusku: ") - - flag.StringVar(&email, cmdNameSurel, "", "Nama pengguna") - flag.StringVar(&pass, cmdNameSandi, "", "Sandi pengguna") - flag.BoolVar(&isListRootWords, cmdNameDaftarKataDasar, false, - "Ambil dan cetak semua kata dasar") - - flag.Parse() - - cl, err := kamusku.NewKbbiClient() - if err != nil { - log.Fatal(err) - } - - if len(email) > 0 && len(pass) > 0 { - err = cl.Login(email, pass) - if err != nil { - log.Fatal(err) - } - } - - if isListRootWords { - if !cl.IsAuthenticated() { - log.Fatalf("opsi %s membutuhkan opsi %s dan %s", - cmdNameDaftarKataDasar, cmdNameSurel, - cmdNameSandi) - } - listRootWords(cl) - return - } - - resDefinition, err := cl.Lookup(flag.Args()) - if err != nil { - log.Fatal(err) - } - - for word, wordDef := range resDefinition { - err = wordDef.Err() - if err != nil { - fmt.Printf("!!! %s: %s\n", word, err) - continue - } - - fmt.Println("===", word) - if len(wordDef.Message) != 0 { - fmt.Println(" " + wordDef.Message) - continue - } - if len(wordDef.Root) > 0 { - fmt.Printf(" Kata dasar: %s\n", wordDef.Root) - } - for x, def := range wordDef.Definition { - fmt.Printf(" Definisi #%d: %s\n", x+1, def.Value) - - for y, nomina := range def.Classes { - fmt.Printf(" Kelas #%d: %s\n", y+1, nomina) - } - for z, contoh := range def.Examples { - fmt.Printf(" Contoh #%d: %s\n", z+1, contoh) - } - fmt.Println() - } - } -} - -func listRootWords(cl *kamusku.KbbiClient) { - words, err := cl.ListRootWords() - if err != nil { - log.Println(err) - } - - list := make([]string, 0, len(words)) - - for k := range words { - list = append(list, k) - } - - sort.Strings(list) - - for _, word := range list { - fmt.Println(word) - } -} diff --git a/cmd/kbbi/main.go b/cmd/kbbi/main.go new file mode 100644 index 0000000..c605428 --- /dev/null +++ b/cmd/kbbi/main.go @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: 2020 M. Shulhan +// SPDX-License-Identifier: GPL-3.0-or-later + +// Program kbbi is the command-line interface to Kamus Besar Bahasa +// Indonesia (KBBI) at https://kbbi.kemdikbud.go.id/. +package main + +import ( + "flag" + "fmt" + "log" + "sort" + + kbbi "git.sr.ht/~shulhan/kbbi" +) + +const ( + cmdNameSurel = "surel" + cmdNameSandi = "sandi" + cmdNameDaftarKataDasar = "daftar-kata-dasar" +) + +func main() { + var ( + isListRootWords bool + email string + pass string + ) + + log.SetFlags(0) + log.SetPrefix(`kbbi: `) + + flag.StringVar(&email, cmdNameSurel, "", "Nama pengguna") + flag.StringVar(&pass, cmdNameSandi, "", "Sandi pengguna") + flag.BoolVar(&isListRootWords, cmdNameDaftarKataDasar, false, + "Ambil dan cetak semua kata dasar") + + flag.Parse() + + cl, err := kbbi.NewClient() + if err != nil { + log.Fatal(err) + } + + if len(email) > 0 && len(pass) > 0 { + err = cl.Login(email, pass) + if err != nil { + log.Fatal(err) + } + } + + if isListRootWords { + if !cl.IsAuthenticated() { + log.Fatalf("opsi %s membutuhkan opsi %s dan %s", + cmdNameDaftarKataDasar, cmdNameSurel, + cmdNameSandi) + } + listRootWords(cl) + return + } + + resDefinition, err := cl.Lookup(flag.Args()) + if err != nil { + log.Fatal(err) + } + + for word, wordDef := range resDefinition { + err = wordDef.Err() + if err != nil { + fmt.Printf("!!! %s: %s\n", word, err) + continue + } + + fmt.Println("===", word) + if len(wordDef.Message) != 0 { + fmt.Println(" " + wordDef.Message) + continue + } + if len(wordDef.Root) > 0 { + fmt.Printf(" Kata dasar: %s\n", wordDef.Root) + } + for x, def := range wordDef.Definition { + fmt.Printf(" Definisi #%d: %s\n", x+1, def.Value) + + for y, nomina := range def.Classes { + fmt.Printf(" Kelas #%d: %s\n", y+1, nomina) + } + for z, contoh := range def.Examples { + fmt.Printf(" Contoh #%d: %s\n", z+1, contoh) + } + fmt.Println() + } + } +} + +func listRootWords(cl *kbbi.Client) { + words, err := cl.ListRootWords() + if err != nil { + log.Println(err) + } + + list := make([]string, 0, len(words)) + + for k := range words { + list = append(list, k) + } + + sort.Strings(list) + + for _, word := range list { + fmt.Println(word) + } +} diff --git a/go.mod b/go.mod index ea3b4e8..f294c9d 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2024 M. Shulhan // SPDX-License-Identifier: GPL-3.0-or-later -module git.sr.ht/~shulhan/kamusku +module git.sr.ht/~shulhan/kbbi go 1.21 diff --git a/kamusku.go b/kamusku.go deleted file mode 100644 index 6cc0d3f..0000000 --- a/kamusku.go +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-FileCopyrightText: 2020 M. Shulhan -// SPDX-License-Identifier: GPL-3.0-or-later - -// Package kamusku is the Go library to access the Bahasa Indonesia dictionary -// from https://kbbi.kemdikbud.go.id. -package kamusku - -// Version of this module. -var Version = `0.1.0` diff --git a/kbbi.go b/kbbi.go new file mode 100644 index 0000000..5243238 --- /dev/null +++ b/kbbi.go @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2020 M. Shulhan +// SPDX-License-Identifier: GPL-3.0-or-later + +// Package kbbi is the Go library to access the Bahasa Indonesia dictionary +// from https://kbbi.kemdikbud.go.id. +package kbbi + +// Version of this module. +var Version = `0.1.0` diff --git a/kbbi_client.go b/kbbi_client.go deleted file mode 100644 index 2ec4eab..0000000 --- a/kbbi_client.go +++ /dev/null @@ -1,389 +0,0 @@ -// SPDX-FileCopyrightText: 2020 M. Shulhan -// SPDX-License-Identifier: GPL-3.0-or-later - -package kamusku - -import ( - "bytes" - "encoding/gob" - "errors" - "fmt" - "io" - "log" - "net/http" - "net/http/cookiejar" - "net/url" - "os" - "path/filepath" - "strconv" - "strings" - "time" - - "git.sr.ht/~shulhan/pakakeh.go/lib/html" - libhttp "git.sr.ht/~shulhan/pakakeh.go/lib/http" - "golang.org/x/net/publicsuffix" -) - -const ( - kbbiUrlBase = "https://kbbi.kemdikbud.go.id" - kbbiUrlLogin = kbbiUrlBase + "/Account/Login" - kbbiPathEntri = "/entri/" - - attrNameClass = "class" - attrNameHref = "href" - attrNameTitle = "title" - attrNameValue = "value" - - attrValueRootWord = "rootword" - - paramNameMasukan = "masukan" - paramNameMasukanLengkap = "masukanLengkap" - paramNameIngatSaya = "IngatSaya" - paramNameKataSandi = "KataSandi" - paramNamePage = "page" - paramNamePosel = "Posel" - paramNameRequestVerificationToken = "__RequestVerificationToken" //nolint: gosec - - paramValueDasar = "dasar" - paramValueFalse = "false" - - tagNameAnchor = "a" - tagNameFont = "font" - tagNameHeader2 = "h2" - tagNameInput = "input" - tagNameItalic = "i" - tagNameOrderedList = "ol" - tagNameSpan = "span" - tagNameUnorderedList = "ul" - - cookieFile = "cookie" - configDir = "kamusku" - defTimeout = 20 * time.Second - maxPageNumber = 501 -) - -// KbbiClient client for official KBBI web using HTTP. -type KbbiClient struct { - baseDir string - cookieURL *url.URL - cookies []*http.Cookie - httpc *http.Client -} - -// NewKbbiClient create and initialize new client that connect directly to -// KBBI official website. -func NewKbbiClient() (cl *KbbiClient, err error) { - cookieURL, err := url.Parse(kbbiUrlBase) - 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, fmt.Errorf("New: %w", err) - } - - cl = &KbbiClient{ - cookieURL: cookieURL, - httpc: &http.Client{ - Jar: jar, - Timeout: defTimeout, - }, - } - - err = cl.loadCookies() - if err != nil { - return nil, fmt.Errorf("New: %w", err) - } - - if cl.cookies != nil { - jar.SetCookies(cookieURL, cl.cookies) - } - - return cl, nil -} - -// Lookup lookup definition of one or more words. -func (cl *KbbiClient) Lookup(ins []string) (res LookupResponse, err error) { - res = make(LookupResponse, len(ins)) - - for _, in := range ins { - _, ok := res[in] - if ok { - continue - } - - kata := &Word{} - res[in] = kata - - entriURL := kbbiUrlBase + kbbiPathEntri + in - httpRes, err := cl.httpc.Get(entriURL) - if err != nil { - kata.err = err - continue - } - - defer httpRes.Body.Close() - - body, err := io.ReadAll(httpRes.Body) - if err != nil { - kata.err = err - continue - } - - err = kata.parseHTMLEntri(in, body) - if err != nil { - kata.err = err - } - - if len(kata.Definition) == 0 && len(kata.Message) == 0 { - kata.Message = "Entri tidak ditemukan" - } - } - - return res, nil -} - -// ListRootWords list all of the root words in dictionary. -func (cl *KbbiClient) ListRootWords() (rootWords Words, err error) { - params := url.Values{ - paramNameMasukan: []string{paramValueDasar}, - paramNameMasukanLengkap: []string{paramValueDasar}, - } - - urlPage := kbbiUrlBase + "/Cari/Jenis?" - - rootWords = make(Words) - - 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 rootWords, err - } - - res, err := cl.httpc.Do(req) - if err != nil { - return rootWords, fmt.Errorf("ListRootWords: page %d: %w", - pageNumber, err) - } - - defer res.Body.Close() - - body, err := io.ReadAll(res.Body) - if err != nil { - return rootWords, fmt.Errorf("ListRootWords: page %d: %w", - pageNumber, err) - } - - got, err := cl.parseHTMLRootWords(body) - if err != nil { - return rootWords, fmt.Errorf("ListRootWords: page %d: %w", - pageNumber, err) - } - if len(got) == 0 { - break - } - - rootWords.merge(got) - - log.Printf("ListRootWords: halaman %d, jumlah kata %d, total kata %d", - pageNumber, len(got), len(rootWords)) - } - - return rootWords, nil -} - -// IsAuthenticated will return true if the client already login; otherwise it -// will return false. -func (cl *KbbiClient) IsAuthenticated() bool { - return len(cl.cookies) > 0 -} - -// Login authenticate the client using user email and password. -func (cl *KbbiClient) Login(email, pass string) (err error) { - tokenLogin, err := cl.preLogin() - if err != nil { - return fmt.Errorf("Login: %w", err) - } - - params := url.Values{ - paramNameRequestVerificationToken: []string{tokenLogin}, - paramNamePosel: []string{email}, - paramNameKataSandi: []string{pass}, - paramNameIngatSaya: []string{paramValueFalse}, - } - - reqBody := strings.NewReader(params.Encode()) - - req, err := http.NewRequest(http.MethodPost, kbbiUrlLogin, reqBody) - if err != nil { - return fmt.Errorf("Login: %w", err) - } - - req.Header.Set(libhttp.HeaderContentType, libhttp.ContentTypeForm) - - res, err := cl.httpc.Do(req) - if err != nil { - return fmt.Errorf("Login: %w", err) - } - - defer res.Body.Close() - - resBody, err := io.ReadAll(res.Body) - if err != nil { - return fmt.Errorf("Login: %w", err) - } - - if res.StatusCode >= http.StatusBadRequest { - return fmt.Errorf("login: %d %s", res.StatusCode, resBody) - } - - cl.cookies = cl.httpc.Jar.Cookies(cl.cookieURL) - cl.setCookies() - cl.saveCookies() - - return nil -} - -// setCookies for HTTP request that need an authentication. -func (cl *KbbiClient) setCookies() { - cl.httpc.Jar.SetCookies(cl.cookieURL, cl.cookies) -} - -func (cl *KbbiClient) parseHTMLRootWords(htmlBody []byte) ( - rootWords Words, err error, -) { - iter, err := html.Parse(bytes.NewReader(htmlBody)) - if err != nil { - return nil, err - } - - rootWords = make(Words) - - for node := iter.Next(); node != nil; node = iter.Next() { - if !node.IsElement() { - continue - } - if node.Data != tagNameAnchor { - continue - } - hrefValue := node.GetAttrValue(attrNameHref) - if !strings.HasPrefix(hrefValue, kbbiPathEntri) { - continue - } - k := strings.TrimSpace(node.FirstChild.Data) - rootWords[k] = struct{}{} - } - - return rootWords, nil -} - -// parseHTMLLogin get the token at the form login. -func (cl *KbbiClient) parseHTMLLogin(htmlBody []byte) ( - token string, err error, -) { - iter, err := html.Parse(bytes.NewReader(htmlBody)) - if err != nil { - return "", err - } - - for node := iter.Next(); node != nil; node = iter.Next() { - if !node.IsElement() { - continue - } - if node.Data != tagNameInput { - continue - } - - token := node.GetAttrValue(attrNameValue) - 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 *KbbiClient) preLogin() (token string, err error) { - req, err := http.NewRequest(http.MethodGet, kbbiUrlLogin, nil) - if err != nil { - return "", err - } - - res, err := cl.httpc.Do(req) - if err != nil { - return "", err - } - - defer res.Body.Close() - - body, err := io.ReadAll(res.Body) - if err != nil { - return "", err - } - - token, err = cl.parseHTMLLogin(body) - if err != nil { - return "", err - } - - return token, nil -} - -// loadCookies load the KBBI cookies from file. -func (cl *KbbiClient) loadCookies() (err error) { - cl.baseDir, err = os.UserConfigDir() - if err != nil { - return fmt.Errorf("loadCookies: %w", err) - } - - f := filepath.Join(cl.baseDir, configDir, cookieFile) - - _, err = os.Stat(f) - if errors.Is(err, os.ErrNotExist) { - return nil - } - - body, err := os.ReadFile(f) - if err != nil { - return fmt.Errorf("loadCookies: %w", err) - } - - dec := gob.NewDecoder(bytes.NewReader(body)) - - err = dec.Decode(&cl.cookies) - if err != nil { - return fmt.Errorf("loadCookies: %w", err) - } - - return nil -} - -// saveCookies store the client cookies to the file for future use. -func (cl *KbbiClient) saveCookies() { - err := os.MkdirAll(filepath.Join(cl.baseDir, configDir), 0700) - if err != nil { - log.Println("saveCookies:", err) - } - - f := filepath.Join(cl.baseDir, configDir, cookieFile) - - var buf bytes.Buffer - enc := gob.NewEncoder(&buf) - err = enc.Encode(cl.cookies) - if err != nil { - log.Println("saveCookies: ", err) - } - - err = os.WriteFile(f, buf.Bytes(), 0600) - if err != nil { - log.Println("saveCookies: ", err) - } -} diff --git a/kbbi_client_test.go b/kbbi_client_test.go deleted file mode 100644 index ba46de1..0000000 --- a/kbbi_client_test.go +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-FileCopyrightText: 2020 M. Shulhan -// SPDX-License-Identifier: GPL-3.0-or-later - -package kamusku - -import ( - "os" - "testing" -) - -func TestClient_parseHTMLKataDasar(t *testing.T) { - htmlBody, err := os.ReadFile(`testdata/kbbi_dasar.html`) - if err != nil { - t.Fatal(err) - } - - cl, err := NewKbbiClient() - if err != nil { - t.Fatal(err) - } - - got, err := cl.parseHTMLRootWords(htmlBody) - if err != nil { - t.Fatal(err) - } - - t.Logf("Root words: %v", got) -} diff --git a/lookup_response.go b/lookup_response.go index 389a8e2..4a6ed07 100644 --- a/lookup_response.go +++ b/lookup_response.go @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2020 M. Shulhan // SPDX-License-Identifier: GPL-3.0-or-later -package kamusku +package kbbi // LookupResponse contains mapping of word and its definition. type LookupResponse map[string]*Word diff --git a/word.go b/word.go index f999e5a..bad564e 100644 --- a/word.go +++ b/word.go @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2020 M. Shulhan // SPDX-License-Identifier: GPL-3.0-or-later -package kamusku +package kbbi import ( "bytes" diff --git a/word_definition.go b/word_definition.go index 0f4820e..385ba9d 100644 --- a/word_definition.go +++ b/word_definition.go @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2020 M. Shulhan // SPDX-License-Identifier: GPL-3.0-or-later -package kamusku +package kbbi import ( "fmt" diff --git a/word_test.go b/word_test.go index a38550d..7e0a69b 100644 --- a/word_test.go +++ b/word_test.go @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2020 M. Shulhan // SPDX-License-Identifier: GPL-3.0-or-later -package kamusku +package kbbi import ( "os" diff --git a/words.go b/words.go index 13fa8cc..e5af469 100644 --- a/words.go +++ b/words.go @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2020 M. Shulhan // SPDX-License-Identifier: GPL-3.0-or-later -package kamusku +package kbbi // Words contains list of words. type Words map[string]struct{} -- cgit v1.3