diff options
author | RandomChars <random@chars.jp> | 2021-08-31 17:04:27 +0900 |
---|---|---|
committer | RandomChars <random@chars.jp> | 2021-08-31 17:04:27 +0900 |
commit | 05a03d33a8ffba4db2d098c987a9a2a71d7adb1b (patch) | |
tree | 810c8385c584d4a55ca54fab205b2406f05861d0 | |
parent | 2def60e4bf1a5cc315861120238c741bd6c2f63f (diff) |
initial implementation of API wrapperv0.8.8
-rw-r--r-- | client/image.go | 140 | ||||
-rw-r--r-- | client/misc.go | 7 | ||||
-rw-r--r-- | client/remote.go | 56 | ||||
-rw-r--r-- | client/request.go | 101 | ||||
-rw-r--r-- | client/tag.go | 81 | ||||
-rw-r--r-- | client/user.go | 107 |
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 +} |