summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRandomChars <random@chars.jp>2021-08-31 17:04:27 +0900
committerRandomChars <random@chars.jp>2021-08-31 17:04:27 +0900
commit05a03d33a8ffba4db2d098c987a9a2a71d7adb1b (patch)
tree810c8385c584d4a55ca54fab205b2406f05861d0
parent2def60e4bf1a5cc315861120238c741bd6c2f63f (diff)
initial implementation of API wrapperv0.8.8
-rw-r--r--client/image.go140
-rw-r--r--client/misc.go7
-rw-r--r--client/remote.go56
-rw-r--r--client/request.go101
-rw-r--r--client/tag.go81
-rw-r--r--client/user.go107
6 files changed, 492 insertions, 0 deletions
diff --git a/client/image.go b/client/image.go
new file mode 100644
index 0000000..08c1b7b
--- /dev/null
+++ b/client/image.go
@@ -0,0 +1,140 @@
+package client
+
+import (
+ "bytes"
+ "mime/multipart"
+ "net/http"
+ "random.chars.jp/git/image-board/api"
+ "random.chars.jp/git/image-board/store"
+ "strconv"
+)
+
+// Images returns a slice of snowflakes of all images. Only available to privileged users.
+func (r *Remote) Images() ([]string, error) {
+ var flakes []string
+ err := r.fetch(http.MethodGet, api.Image, &flakes, nil)
+ return flakes, err
+}
+
+// ImageAdd adds an image to Remote and returns a store.Image.
+func (r *Remote) ImageAdd(data []byte) (store.Image, error) {
+ buf := &bytes.Buffer{}
+ w := multipart.NewWriter(buf)
+ if f, err := w.CreateFormFile("image", ""); err != nil {
+ return store.Image{}, err
+ } else {
+ if _, err = f.Write(data); err != nil {
+ return store.Image{}, err
+ }
+ }
+
+ if err := w.Close(); err != nil {
+ return store.Image{}, err
+ }
+
+ if resp, err := r.request(http.MethodPost, api.Image, buf); err != nil {
+ return store.Image{}, err
+ } else {
+ var image store.Image
+ err = unmarshal(resp.Body, &image)
+ return image, err
+ }
+}
+
+// Image returns store.Image with given snowflake.
+func (r *Remote) Image(flake string) (store.Image, error) {
+ var image store.Image
+ err := r.fetch(http.MethodGet, populateField(api.ImageField, "flake", flake), &image, nil)
+ return image, err
+}
+
+// ImageUpdate updates metadata of store.Image with given snowflake. To persist original value in a field set \000.
+func (r *Remote) ImageUpdate(flake, source, commentary, commentaryTranslation string) error {
+ payload := api.ImageUpdatePayload{
+ Source: source,
+ Commentary: commentary,
+ CommentaryTranslation: commentaryTranslation,
+ }
+ return r.requestJSONnoResp(http.MethodPatch, populateField(api.ImageField, "flake", flake), payload)
+}
+
+// ImageDestroy destroys a store.Image with given snowflake.
+func (r *Remote) ImageDestroy(flake string) error {
+ return r.requestNoResp(http.MethodDelete, populateField(api.ImageField, "flake", flake), nil)
+}
+
+// ImageFile returns a slice of bytes of the image with given snowflake.
+func (r *Remote) ImageFile(flake string, preview bool) ([]byte, error) {
+ switch preview {
+ case true:
+ return r.fetchAll(http.MethodGet, populateField(api.ImagePreview, "flake", flake), nil)
+ case false:
+ return r.fetchAll(http.MethodGet, populateField(api.ImageFile, "flake", flake), nil)
+ default:
+ return nil, nil
+ }
+}
+
+// ImageTag returns a slice of tags of an image with given snowflake.
+func (r *Remote) ImageTag(flake string) ([]string, error) {
+ var tags []string
+ err := r.fetch(http.MethodGet, populateField(api.ImageTag, "flake", flake), &tags, nil)
+ return tags, err
+}
+
+// ImageTagAdd adds a tag to an image with given snowflake.
+func (r *Remote) ImageTagAdd(flake, tag string) error {
+ return r.requestNoResp(http.MethodPut,
+ populateField(populateField(api.ImageTagField,
+ "flake", flake),
+ "tag", tag),
+ nil)
+}
+
+// ImageTagRemove removes a tag from an image with given snowflake.
+func (r *Remote) ImageTagRemove(flake, tag string) error {
+ return r.requestNoResp(http.MethodDelete,
+ populateField(populateField(api.ImageTagField,
+ "flake", flake),
+ "tag", tag),
+ nil)
+}
+
+// ImagePages returns total amount of image pages.
+func (r *Remote) ImagePages() (int, error) {
+ if payload, err := r.fetchAll(http.MethodGet, api.ImagePage, nil); err != nil {
+ return 0, err
+ } else {
+ var pages int
+ if pages, err = strconv.Atoi(string(payload)); err != nil {
+ return 0, err
+ } else {
+ return pages, nil
+ }
+ }
+}
+
+// ImagePage returns a slice of snowflakes of images in a page.
+func (r *Remote) ImagePage(entry int) ([]string, error) {
+ var flakes []string
+ err := r.fetch(http.MethodGet, populateField(api.ImagePageField, "entry", strconv.Itoa(entry)), &flakes, nil)
+ return flakes, err
+}
+
+// ImagePageImage returns a slice of store.Image in a page.
+func (r *Remote) ImagePageImage(entry int) ([]store.Image, error) {
+ var images []store.Image
+ err := r.fetch(http.MethodGet, populateField(api.ImagePageImage, "entry", strconv.Itoa(entry)), &images, nil)
+ return images, err
+}
+
+// ImageSearch searches for images that contains all specified tags.
+func (r *Remote) ImageSearch(tags []string) ([]string, error) {
+ t := tags[0]
+ for i := 1; i < len(tags); i++ {
+ t += "!" + tags[i]
+ }
+ var flakes []string
+ err := r.fetch(http.MethodGet, populateField(api.SearchField, "tags", t), &flakes, nil)
+ return flakes, err
+}
diff --git a/client/misc.go b/client/misc.go
new file mode 100644
index 0000000..b37f568
--- /dev/null
+++ b/client/misc.go
@@ -0,0 +1,7 @@
+package client
+
+import "strings"
+
+func populateField(path, field, content string) string {
+ return strings.Replace(path, ":"+field, content, 1)
+}
diff --git a/client/remote.go b/client/remote.go
new file mode 100644
index 0000000..3391cc9
--- /dev/null
+++ b/client/remote.go
@@ -0,0 +1,56 @@
+package client
+
+import (
+ "fmt"
+ "net/http"
+ "random.chars.jp/git/image-board/api"
+ "random.chars.jp/git/image-board/store"
+)
+
+// Remote represents a remote image board server.
+type Remote struct {
+ url string
+ single bool
+ secret string
+ client *http.Client
+ user *api.UserPayload
+ store.Info
+}
+
+// New returns a pointer to a new Remote.
+func New(url string) (*Remote, error) {
+ remote := &Remote{url: url, client: &http.Client{}}
+ return remote, remote.Handshake()
+}
+
+// URL returns the URL of Remote.
+func (r *Remote) URL(path string) string {
+ return r.url + path
+}
+
+// SingleUser returns whether the Remote is running in single user mode.
+func (r *Remote) SingleUser() bool {
+ return r.single
+}
+
+// Handshake checks if the server is still online and updates Remote.
+func (r *Remote) Handshake() error {
+ if err := r.fetch(http.MethodGet, api.SingleUser, r.single, nil); err != nil {
+ return err
+ }
+ return r.fetch(http.MethodGet, api.Base, r, nil)
+}
+
+// Secret authenticates and sets secret.
+func (r *Remote) Secret(secret string) (api.UserPayload, bool) {
+ prev := r.secret
+ r.secret = secret
+ if user, err := r.This(); err != nil {
+ fmt.Printf("Error getting user post secret change, %s", err)
+ r.secret = prev
+ return api.UserPayload{}, false
+ } else {
+ r.user = &user
+ return user, true
+ }
+}
diff --git a/client/request.go b/client/request.go
new file mode 100644
index 0000000..ecb3569
--- /dev/null
+++ b/client/request.go
@@ -0,0 +1,101 @@
+package client
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+)
+
+func (r *Remote) send(req *http.Request) (*http.Response, error) {
+ if req == nil {
+ return nil, nil
+ }
+
+ if r.secret != "" {
+ req.Header.Add("secret", r.secret)
+ }
+
+ return r.client.Do(req)
+}
+
+func (r *Remote) request(method, path string, body io.Reader) (*http.Response, error) {
+ if req, err := http.NewRequest(method, r.URL(path), body); err != nil {
+ return nil, err
+ } else {
+ return r.send(req)
+ }
+}
+
+func (r *Remote) requestNoResp(method, path string, body io.Reader) error {
+ if resp, err := r.request(method, path, body); err != nil {
+ return err
+ } else {
+ return resp.Body.Close()
+ }
+}
+
+func (r *Remote) requestJSON(method, path string, v interface{}) (*http.Response, error) {
+ var reader *bytes.Reader
+ if v != nil {
+ if payload, err := json.Marshal(v); err != nil {
+ return nil, err
+ } else {
+ reader = bytes.NewReader(payload)
+ }
+ }
+ return r.request(method, path, reader)
+}
+
+func (r *Remote) requestJSONnoResp(method, path string, v interface{}) error {
+ if resp, err := r.requestJSON(method, path, v); err != nil {
+ return err
+ } else {
+ return resp.Body.Close()
+ }
+}
+
+func (r *Remote) fetch(method, path string, v interface{}, body io.Reader) error {
+ if resp, err := r.request(method, path, body); err != nil {
+ return err
+ } else {
+ if v == nil {
+ return nil
+ }
+ if err = unmarshal(resp.Body, v); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (r *Remote) fetchAll(method, path string, body io.Reader) ([]byte, error) {
+ if resp, err := r.request(method, path, body); err != nil {
+ return nil, err
+ } else {
+ defer func() {
+ if err = resp.Body.Close(); err != nil {
+ fmt.Printf("Error closing body after reading all, %s\n", err)
+ }
+ }()
+ var content []byte
+ if content, err = io.ReadAll(resp.Body); err != nil {
+ return nil, err
+ } else {
+ return content, nil
+ }
+ }
+}
+
+func unmarshal(reader io.ReadCloser, v interface{}) error {
+ defer func() {
+ if err := reader.Close(); err != nil {
+ fmt.Printf("Error closing reader after unmarshalling, %s\n", err)
+ }
+ }()
+ if err := json.NewDecoder(reader).Decode(v); err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/client/tag.go b/client/tag.go
new file mode 100644
index 0000000..37b8745
--- /dev/null
+++ b/client/tag.go
@@ -0,0 +1,81 @@
+package client
+
+import (
+ "net/http"
+ "random.chars.jp/git/image-board/api"
+ "random.chars.jp/git/image-board/store"
+ "strconv"
+)
+
+// Tags returns a slice of tag names on Remote.
+func (r *Remote) Tags() ([]string, error) {
+ var tags []string
+ err := r.fetch(http.MethodGet, api.Tag, &tags, nil)
+ return tags, err
+}
+
+// Tag returns a slice of snowflakes in a given tag.
+func (r *Remote) Tag(tag string) ([]string, error) {
+ var flakes []string
+ err := r.fetch(http.MethodGet, populateField(api.TagField, "tag", tag), &flakes, nil)
+ return flakes, err
+}
+
+// TagPage returns a slice of snowflakes in a page of a given tag.
+func (r *Remote) TagPage(tag string, entry int) ([]string, error) {
+ var flakes []string
+ err := r.fetch(http.MethodGet,
+ populateField(populateField(api.TagPageField,
+ "tag", tag),
+ "entry", strconv.Itoa(entry)),
+ &flakes, nil)
+ return flakes, err
+}
+
+// TagPageImages returns a slice of store.Image in a page of a given tag.
+func (r *Remote) TagPageImages(tag string, entry int) ([]store.Image, error) {
+ var images []store.Image
+ err := r.fetch(http.MethodGet,
+ populateField(populateField(api.TagPageImage,
+ "tag", tag),
+ "entry", strconv.Itoa(entry)),
+ &images, nil)
+ return images, err
+}
+
+// TagPages returns total amount of pages of a tag.
+func (r *Remote) TagPages(tag string) (int, error) {
+ if payload, err := r.fetchAll(http.MethodGet,
+ populateField(api.TagPage, "tag", tag), nil); err != nil {
+ return 0, err
+ } else {
+ var pages int
+ if pages, err = strconv.Atoi(string(payload)); err != nil {
+ return 0, err
+ } else {
+ return pages, nil
+ }
+ }
+}
+
+// TagCreate creates a tag on Remote.
+func (r *Remote) TagCreate(tag string) error {
+ return r.requestNoResp(http.MethodPut, populateField(api.TagField, "tag", tag), nil)
+}
+
+// TagDestroy destroys a tag on Remote.
+func (r *Remote) TagDestroy(tag string) error {
+ return r.requestNoResp(http.MethodDelete, populateField(api.TagField, "tag", tag), nil)
+}
+
+// TagInfo returns store.Tag of given tag.
+func (r *Remote) TagInfo(tag string) (store.Tag, error) {
+ var t store.Tag
+ err := r.fetch(http.MethodGet, populateField(api.TagInfo, "tag", tag), &t, nil)
+ return t, err
+}
+
+// TagType sets type of given tag.
+func (r *Remote) TagType(tag, t string) error {
+ return r.requestJSONnoResp(http.MethodPatch, populateField(api.TagInfo, "tag", tag), api.TagUpdatePayload{Type: t})
+}
diff --git a/client/user.go b/client/user.go
new file mode 100644
index 0000000..97985e6
--- /dev/null
+++ b/client/user.go
@@ -0,0 +1,107 @@
+package client
+
+import (
+ "net/http"
+ "random.chars.jp/git/image-board/api"
+ "random.chars.jp/git/image-board/store"
+ "strings"
+)
+
+// User returns api.UserPayload of a snowflake.
+func (r *Remote) User(flake string) (api.UserPayload, error) {
+ var user api.UserPayload
+ err := r.fetch(http.MethodGet, api.User+"/"+flake, &user, nil)
+ return user, err
+}
+
+// This returns api.UserPayload currently authenticated as.
+func (r *Remote) This() (api.UserPayload, error) {
+ return r.User("this")
+}
+
+// Username returns api.UserPayload of username.
+func (r *Remote) Username(name string) (api.UserPayload, error) {
+ var user api.UserPayload
+ err := r.fetch(http.MethodGet, api.Username+"/"+name, &user, nil)
+ return user, err
+}
+
+// Auth authenticates using a username and its corresponding password.
+func (r *Remote) Auth(username, password string) (string, error) {
+ var secret api.UserSecretPayload
+ err := r.fetch(http.MethodPost,
+ populateField(api.UsernameAuth, "name", username),
+ &secret,
+ strings.NewReader(password))
+ return secret.Secret, err
+}
+
+// UserAdd adds a new user.
+func (r *Remote) UserAdd(username string, password string, privileged bool) (store.User, error) {
+ if resp, err := r.requestJSON(http.MethodPut, api.User, api.UserCreatePayload{
+ Username: username,
+ Password: password,
+ Privileged: privileged,
+ }); err != nil {
+ return store.User{}, err
+ } else {
+ var user store.User
+ err = unmarshal(resp.Body, &user)
+ return user, err
+ }
+}
+
+// UserUpdate updates a user.
+func (r *Remote) UserUpdate(flake, newname string) error {
+ return r.requestJSONnoResp(http.MethodPatch,
+ populateField(api.UserField, "flake", flake),
+ api.UserUpdatePayload{Username: newname})
+}
+
+// UserDestroy destroys a user.
+func (r *Remote) UserDestroy(flake string) error {
+ return r.requestJSONnoResp(http.MethodDelete,
+ populateField(api.UserField, "flake", flake),
+ nil)
+}
+
+// UserDisable disables a user.
+func (r *Remote) UserDisable(flake string) error {
+ return r.requestJSONnoResp(http.MethodDelete,
+ populateField(api.UserPassword, "flake", flake),
+ nil)
+}
+
+// UserPassword sets a user's password.
+func (r *Remote) UserPassword(flake, password string) (string, error) {
+ if resp, err := r.request(http.MethodPut,
+ populateField(api.UserPassword, "flake", flake),
+ strings.NewReader(password)); err != nil {
+ return "", err
+ } else {
+ var payload api.UserSecretPayload
+ err = unmarshal(resp.Body, &payload)
+ return payload.Secret, err
+ }
+}
+
+// UserSecret returns a user's secret.
+func (r *Remote) UserSecret(flake string) (string, error) {
+ var secret api.UserSecretPayload
+ err := r.fetch(http.MethodGet, populateField(api.UserSecret, "flake", flake), &secret, nil)
+ return secret.Secret, err
+}
+
+// UserSecretRegen regenerates a user's secret.
+func (r *Remote) UserSecretRegen(flake string) (string, error) {
+ var secret api.UserSecretPayload
+ err := r.fetch(http.MethodPut, populateField(api.UserSecret, "flake", flake), &secret, nil)
+ return secret.Secret, err
+}
+
+// UserImages returns a slice of snowflakes of a user's images.
+func (r *Remote) UserImages(flake string) ([]string, error) {
+ var flakes []string
+ err := r.fetch(http.MethodGet, populateField(api.UserImage, "flake", flake), &flakes, nil)
+ return flakes, err
+}