From 5c74f774fff54ef18f2264cd9b2d43a5ae0f21b6 Mon Sep 17 00:00:00 2001 From: Wim Brand Date: Tue, 9 Mar 2021 18:11:23 +0100 Subject: [PATCH] Transfer code from API repo to own repo --- .gitignore | 1 + README.md | 3 +- go.mod | 3 + pixelapi/admin.go | 26 ++++++ pixelapi/file.go | 24 +++++ pixelapi/filesystem.go | 18 ++++ pixelapi/list.go | 8 ++ pixelapi/misc.go | 27 ++++++ pixelapi/patreon.go | 16 ++++ pixelapi/pixelapi.go | 194 +++++++++++++++++++++++++++++++++++++++ pixelapi/subscription.go | 19 ++++ pixelapi/user.go | 135 +++++++++++++++++++++++++++ 12 files changed, 473 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 go.mod create mode 100644 pixelapi/admin.go create mode 100644 pixelapi/file.go create mode 100644 pixelapi/filesystem.go create mode 100644 pixelapi/list.go create mode 100644 pixelapi/misc.go create mode 100644 pixelapi/patreon.go create mode 100644 pixelapi/pixelapi.go create mode 100644 pixelapi/subscription.go create mode 100644 pixelapi/user.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..08cb523 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +go.sum diff --git a/README.md b/README.md index 5f3ed7d..c0a5318 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ # pixeldrain_api_client -Client for the pixeldrain API. Used by pixeldrain itself for tranferring data between the web UI and API server. And for rendering JSON responses \ No newline at end of file +Client for the pixeldrain API. Used by pixeldrain itself for tranferring data +between the web UI and API server. And for rendering JSON responses diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..c0fe126 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module fornaxian.tech/pixeldrain_api_client + +go 1.16 diff --git a/pixelapi/admin.go b/pixelapi/admin.go new file mode 100644 index 0000000..c382b19 --- /dev/null +++ b/pixelapi/admin.go @@ -0,0 +1,26 @@ +package pixelapi + +import ( + "net/url" + + "fornaxian.tech/pixeldrain_server/api/restapi/apitype" +) + +// AdminGetGlobals returns the global API settings +func (p *PixelAPI) AdminGetGlobals() (resp []apitype.AdminGlobal, err error) { + return resp, p.jsonRequest("GET", "admin/globals", &resp) +} + +// AdminSetGlobals sets a global API setting +func (p *PixelAPI) AdminSetGlobals(key, value string) (err error) { + return p.form("POST", "admin/globals", url.Values{"key": {key}, "value": {value}}, nil) +} + +// AdminBlockFiles blocks files from being downloaded +func (p *PixelAPI) AdminBlockFiles(text, abuseType, reporter string) (bl apitype.AdminBlockFiles, err error) { + return bl, p.form( + "POST", "admin/block_files", + url.Values{"text": {text}, "type": {abuseType}, "reporter": {reporter}}, + &bl, + ) +} diff --git a/pixelapi/file.go b/pixelapi/file.go new file mode 100644 index 0000000..b0bd1ad --- /dev/null +++ b/pixelapi/file.go @@ -0,0 +1,24 @@ +package pixelapi + +import ( + "io" + "net/url" + + "fornaxian.tech/pixeldrain_server/api/restapi/apitype" +) + +// GetFile makes a file download request and returns a readcloser. Don't forget +// to close it! +func (p *PixelAPI) GetFile(id string) (io.ReadCloser, error) { + return p.getRaw("file/" + id) +} + +// GetFileInfo gets the FileInfo from the pixeldrain API +func (p *PixelAPI) GetFileInfo(id string) (resp apitype.FileInfo, err error) { + return resp, p.jsonRequest("GET", "file/"+id+"/info", &resp) +} + +// PostFileView adds a view to a file +func (p *PixelAPI) PostFileView(id, viewtoken string) (err error) { + return p.form("POST", "file/"+id+"/view", url.Values{"token": {viewtoken}}, nil) +} diff --git a/pixelapi/filesystem.go b/pixelapi/filesystem.go new file mode 100644 index 0000000..8a34602 --- /dev/null +++ b/pixelapi/filesystem.go @@ -0,0 +1,18 @@ +package pixelapi + +import ( + "net/url" + + "fornaxian.tech/pixeldrain_server/api/restapi/apitype" +) + +// GetFilesystemBuckets returns a list of buckets for the user. You need to be +// authenticated +func (p *PixelAPI) GetFilesystemBuckets() (resp []apitype.Bucket, err error) { + return resp, p.jsonRequest("GET", "filesystem", &resp) +} + +// GetFilesystemPath opens a filesystem path +func (p *PixelAPI) GetFilesystemPath(path string) (resp apitype.FilesystemPath, err error) { + return resp, p.jsonRequest("GET", "filesystem/"+url.PathEscape(path)+"?stat", &resp) +} diff --git a/pixelapi/list.go b/pixelapi/list.go new file mode 100644 index 0000000..7568c6c --- /dev/null +++ b/pixelapi/list.go @@ -0,0 +1,8 @@ +package pixelapi + +import "fornaxian.tech/pixeldrain_server/api/restapi/apitype" + +// GetListID get a List from the pixeldrain API +func (p *PixelAPI) GetListID(id string) (resp apitype.ListInfo, err error) { + return resp, p.jsonRequest("GET", "list/"+id, &resp) +} diff --git a/pixelapi/misc.go b/pixelapi/misc.go new file mode 100644 index 0000000..1ee61ee --- /dev/null +++ b/pixelapi/misc.go @@ -0,0 +1,27 @@ +package pixelapi + +import "fornaxian.tech/pixeldrain_server/api/restapi/apitype" + +// Recaptcha stores the reCaptcha site key +type Recaptcha struct { + SiteKey string `json:"site_key"` +} + +// GetMiscRecaptcha gets the reCaptcha site key from the pixelapi server. If +// reCaptcha is disabled the key will be empty +func (p *PixelAPI) GetMiscRecaptcha() (resp Recaptcha, err error) { + return resp, p.jsonRequest("GET", "misc/recaptcha", &resp) +} + +// GetMiscViewToken requests a viewtoken from the server. The viewtoken is valid +// for a limited amount of time and can be used to add views to a file. +// Viewtokens can only be requested from localhost +func (p *PixelAPI) GetMiscViewToken() (resp string, err error) { + return resp, p.jsonRequest("GET", "misc/viewtoken", &resp) +} + +// GetSiaPrice gets the price of one siacoin +func (p *PixelAPI) GetSiaPrice() (resp float64, err error) { + var sp apitype.SiaPrice + return sp.Price, p.jsonRequest("GET", "misc/sia_price", &sp) +} diff --git a/pixelapi/patreon.go b/pixelapi/patreon.go new file mode 100644 index 0000000..e7838d1 --- /dev/null +++ b/pixelapi/patreon.go @@ -0,0 +1,16 @@ +package pixelapi + +import ( + "fornaxian.tech/pixeldrain_server/api/restapi/apitype" +) + +// GetPatreonByID returns information about a patron by the ID +func (p *PixelAPI) GetPatreonByID(id string) (resp apitype.Patron, err error) { + return resp, p.jsonRequest("GET", "patreon/"+id, &resp) +} + +// PostPatreonLink links a patreon subscription to the pixeldrain account which +// is logged into this API client +func (p *PixelAPI) PostPatreonLink(id string) (err error) { + return p.jsonRequest("POST", "patreon/"+id+"/link_subscription", nil) +} diff --git a/pixelapi/pixelapi.go b/pixelapi/pixelapi.go new file mode 100644 index 0000000..4c664d9 --- /dev/null +++ b/pixelapi/pixelapi.go @@ -0,0 +1,194 @@ +package pixelapi + +import ( + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "net/url" + "strings" + "time" +) + +// PixelAPI is the Pixeldrain API client +type PixelAPI struct { + client *http.Client + apiEndpoint string + key string + realIP string +} + +// New creates a new Pixeldrain API client to query the Pixeldrain API with +func New(apiEndpoint string) (api PixelAPI) { + api.client = &http.Client{Timeout: time.Minute * 5} + api.apiEndpoint = apiEndpoint + + // Pixeldrain uses unix domain sockets on its servers to minimize latency + // between the web interface daemon and API daemon. Golang does not + // understand that it needs to dial a unix socket on this case so we create + // a custom HTTP transport which uses the unix socket instead of TCP + if strings.HasPrefix(apiEndpoint, "http://unix:") { + // Get the socket path from the API endpoint + var sockPath = strings.TrimPrefix(apiEndpoint, "http://unix:") + + // Fake the dialer to use a unix socket instead of TCP + api.client.Transport = &http.Transport{ + DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { + return net.Dial("unix", sockPath) + }, + } + + // Fake a domain name to stop Go's HTTP client from complaining about + // the domain name. This string will be completely ignored during + // requests + api.apiEndpoint = "http://api.sock" + } else { + api.client.Transport = http.DefaultTransport + } + + return api +} + +// Login logs a user into the pixeldrain API. The original PixelAPI does not get +// logged in, only the returned PixelAPI +func (p PixelAPI) Login(apiKey string) PixelAPI { + p.key = apiKey + return p +} + +// RealIP sets the real IP address to use when making API requests +func (p PixelAPI) RealIP(ip string) PixelAPI { + p.realIP = ip + return p +} + +// Standard response types + +// Error is an error returned by the pixeldrain API. If the request failed +// before it could reach the API the error will be on a different type +type Error struct { + Status int `json:"-"` // One of the http.Status types + Success bool `json:"success"` + StatusCode string `json:"value"` + Message string `json:"message"` + + // In case of the multiple_errors code this array will be populated with + // more errors + Errors []Error `json:"errors,omitempty"` + + // Metadata regarding the error + Extra map[string]interface{} `json:"extra,omitempty"` +} + +func (e Error) Error() string { return e.StatusCode } + +// ErrIsServerError returns true if the error is a server-side error +func ErrIsServerError(err error) bool { + if apierr, ok := err.(Error); ok && apierr.Status >= 500 { + return true + } + return false +} + +// ErrIsClientError returns true if the error is a client-side error +func ErrIsClientError(err error) bool { + if apierr, ok := err.(Error); ok && apierr.Status >= 400 && apierr.Status < 500 { + return true + } + return false +} + +func (p *PixelAPI) do(r *http.Request) (*http.Response, error) { + if p.key != "" { + r.SetBasicAuth("", p.key) + } + if p.realIP != "" { + r.Header.Set("X-Real-IP", p.realIP) + } + + return p.client.Do(r) +} + +func (p *PixelAPI) getString(path string) (string, error) { + req, err := http.NewRequest("GET", p.apiEndpoint+"/"+path, nil) + if err != nil { + return "", err + } + resp, err := p.do(req) + if err != nil { + return "", err + } + + defer resp.Body.Close() + + bodyBytes, err := ioutil.ReadAll(resp.Body) + + return string(bodyBytes), err +} + +func (p *PixelAPI) getRaw(path string) (io.ReadCloser, error) { + req, err := http.NewRequest("GET", p.apiEndpoint+"/"+path, nil) + if err != nil { + return nil, err + } + resp, err := p.do(req) + if err != nil { + return nil, err + } + + return resp.Body, err +} + +func (p *PixelAPI) jsonRequest(method, path string, target interface{}) error { + req, err := http.NewRequest(method, p.apiEndpoint+"/"+path, nil) + if err != nil { + return err + } + resp, err := p.do(req) + if err != nil { + return err + } + + defer resp.Body.Close() + return parseJSONResponse(resp, target) +} + +func (p *PixelAPI) form(method, url string, vals url.Values, target interface{}) error { + req, err := http.NewRequest(method, url, strings.NewReader(vals.Encode())) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := p.do(req) + if err != nil { + return err + } + + defer resp.Body.Close() + return parseJSONResponse(resp, target) +} + +func parseJSONResponse(resp *http.Response, target interface{}) (err error) { + // Test for client side and server side errors + if resp.StatusCode >= 400 { + errResp := Error{Status: resp.StatusCode} + if err = json.NewDecoder(resp.Body).Decode(&errResp); err != nil { + return err + } + return errResp + } + + if target == nil { + return nil + } + + if err = json.NewDecoder(resp.Body).Decode(target); err != nil { + return fmt.Errorf("failed to decode json response: %w", err) + } + + return nil +} diff --git a/pixelapi/subscription.go b/pixelapi/subscription.go new file mode 100644 index 0000000..213f052 --- /dev/null +++ b/pixelapi/subscription.go @@ -0,0 +1,19 @@ +package pixelapi + +import ( + "net/url" + + "fornaxian.tech/pixeldrain_server/api/restapi/apitype" +) + +// GetSubscriptionID returns the subscription object identified by the given ID +func (p *PixelAPI) GetSubscriptionID(id string) (resp apitype.Subscription, err error) { + return resp, p.jsonRequest("GET", p.apiEndpoint+"/subscription/"+url.PathEscape(id), &resp) +} + +// PostSubscriptionLink links a subscription to the logged in user account. Use +// Login() before calling this function to select the account to use. This +// action cannot be undone. +func (p *PixelAPI) PostSubscriptionLink(id string) (err error) { + return p.jsonRequest("POST", p.apiEndpoint+"/subscription/"+url.PathEscape(id)+"/link", nil) +} diff --git a/pixelapi/user.go b/pixelapi/user.go new file mode 100644 index 0000000..267ba5c --- /dev/null +++ b/pixelapi/user.go @@ -0,0 +1,135 @@ +package pixelapi + +import ( + "net/url" + "strconv" + + "fornaxian.tech/pixeldrain_server/api/restapi/apitype" +) + +// UserRegister registers a new user on the Pixeldrain server. username and +// password are always required. email is optional, but without it you will not +// be able to reset your password in case you forget it. captcha depends on +// whether reCaptcha is enabled on the Pixeldrain server, this can be checked +// through the GetRecaptcha function. +// +// The register API can return multiple errors, which will be stored in the +// Errors array. Check for len(Errors) == 0 to see if an error occurred. If err +// != nil it means a connection error occurred +func (p *PixelAPI) UserRegister(username, email, password, captcha string) (err error) { + return p.form( + "POST", "user/register", + url.Values{ + "username": {username}, + "email": {email}, + "password": {password}, + "recaptcha_response": {captcha}, + }, + nil, + ) +} + +// PostUserLogin logs a user in with the provided credentials. The response will +// contain the returned API key. If saveKey is true the API key will also be +// saved in the client and following requests with this client will be +// autenticated +func (p *PixelAPI) PostUserLogin(username, password string) (resp apitype.UserSession, err error) { + return resp, p.form( + "POST", "user/login", + url.Values{"username": {username}, "password": {password}}, + &resp, + ) +} + +// GetUser returns information about the logged in user. Requires an API key +func (p *PixelAPI) GetUser() (resp apitype.UserInfo, err error) { + return resp, p.jsonRequest("GET", "user", &resp) +} + +// PostUserSession creates a new user sessions +func (p *PixelAPI) PostUserSession() (resp apitype.UserSession, err error) { + return resp, p.jsonRequest("POST", "user/session", &resp) +} + +// GetUserSession lists all active user sessions +func (p *PixelAPI) GetUserSession() (resp []apitype.UserSession, err error) { + return resp, p.jsonRequest("GET", "user/session", &resp) +} + +// DeleteUserSession destroys an API key so it can no longer be used to perform +// actions +func (p *PixelAPI) DeleteUserSession(key string) (err error) { + return p.jsonRequest("DELETE", "user/session", nil) +} + +// GetUserFiles gets files uploaded by a user +func (p *PixelAPI) GetUserFiles() (resp apitype.FileInfoSlice, err error) { + return resp, p.jsonRequest("GET", "user/files", &resp) +} + +// GetUserLists gets lists created by a user +func (p *PixelAPI) GetUserLists() (resp apitype.ListInfoSlice, err error) { + return resp, p.jsonRequest("GET", "user/lists", &resp) +} + +// PutUserPassword changes the user's password +func (p *PixelAPI) PutUserPassword(oldPW, newPW string) (err error) { + return p.form( + "PUT", "user/password", + url.Values{"old_password": {oldPW}, "new_password": {newPW}}, + nil, + ) +} + +// PutUserEmailReset starts the e-mail change process. An email will be sent to +// the new address to verify that it's real. Once the link in the e-mail is +// clicked the key it contains can be sent to the API with UserEmailResetConfirm +// and the change will be applied +func (p *PixelAPI) PutUserEmailReset(email string, delete bool) (err error) { + return p.form( + "PUT", "user/email_reset", + url.Values{"new_email": {email}, "delete": {strconv.FormatBool(delete)}}, + nil, + ) +} + +// PutUserEmailResetConfirm finishes process of changing a user's e-mail address +func (p *PixelAPI) PutUserEmailResetConfirm(key string) (err error) { + return p.form( + "PUT", "user/email_reset_confirm", + url.Values{"key": {key}}, + nil, + ) +} + +// PutUserPasswordReset starts the password reset process. An email will be sent +// the user to verify that it really wanted to reset the password. Once the link +// in the e-mail is clicked the key it contains can be sent to the API with +// UserPasswordResetConfirm and a new password can be set +func (p *PixelAPI) PutUserPasswordReset(email string, recaptchaResponse string) (err error) { + return p.form( + "PUT", "user/password_reset", + url.Values{"email": {email}, "recaptcha_response": {recaptchaResponse}}, + nil, + ) +} + +// PutUserPasswordResetConfirm finishes process of resetting a user's password. +// If the key is valid the new_password parameter will be saved as the new +// password +func (p *PixelAPI) PutUserPasswordResetConfirm(key string, newPassword string) (err error) { + return p.form( + "PUT", "user/password_reset_confirm", + url.Values{"key": {key}, "new_password": {newPassword}}, + nil, + ) +} + +// PutUserUsername changes the user's username. +func (p *PixelAPI) PutUserUsername(username string) (err error) { + return p.form( + "PUT", "user/username", + url.Values{"new_username": {username}}, + nil, + ) +}