diff options
| author | Christian Cleberg <[email protected]> | 2026-03-01 19:32:52 -0600 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-03-01 19:32:52 -0600 |
| commit | a98c2a4542e19ce4fb632bc32fc47c18273161cc (patch) | |
| tree | 99d016c1c9f12d98d57cfec34262e7c228a1d920 /tumblr.go | |
| parent | cd8c38db601355003510e92a0936cde6b71cf849 (diff) | |
| download | michelangelo-a98c2a4542e19ce4fb632bc32fc47c18273161cc.tar.gz michelangelo-a98c2a4542e19ce4fb632bc32fc47c18273161cc.tar.bz2 michelangelo-a98c2a4542e19ce4fb632bc32fc47c18273161cc.zip | |
convert from PHP to Go
Diffstat (limited to 'tumblr.go')
| -rw-r--r-- | tumblr.go | 285 |
1 files changed, 285 insertions, 0 deletions
diff --git a/tumblr.go b/tumblr.go new file mode 100644 index 0000000..b76d18f --- /dev/null +++ b/tumblr.go @@ -0,0 +1,285 @@ +package main + +import ( + "crypto/hmac" + "crypto/sha1" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "math/rand" + "net/http" + "net/url" + "sort" + "strconv" + "strings" + "time" +) + +const apiBase = "https://api.tumblr.com/v2" +const oauthBase = "https://www.tumblr.com" + +type TumblrClient struct { + ConsumerKey string + ConsumerSecret string + Token string + TokenSecret string +} + +func NewClient(ck, cs, token, secret string) *TumblrClient { + return &TumblrClient{ck, cs, token, secret} +} + +// --- OAuth 1.0a --- + +func oauthNonce() string { + b := make([]byte, 16) + rand.Read(b) + return base64.StdEncoding.EncodeToString(b) +} + +func oauthTimestamp() string { + return strconv.FormatInt(time.Now().Unix(), 10) +} + +func hmacSha1(key, data string) string { + mac := hmac.New(sha1.New, []byte(key)) + mac.Write([]byte(data)) + return base64.StdEncoding.EncodeToString(mac.Sum(nil)) +} + +func (c *TumblrClient) oauthHeader(method, rawURL string, extraParams map[string]string) string { + params := map[string]string{ + "oauth_consumer_key": c.ConsumerKey, + "oauth_nonce": oauthNonce(), + "oauth_signature_method": "HMAC-SHA1", + "oauth_timestamp": oauthTimestamp(), + "oauth_token": c.Token, + "oauth_version": "1.0", + } + for k, v := range extraParams { + params[k] = v + } + // Build base string + keys := make([]string, 0, len(params)) + for k := range params { + keys = append(keys, k) + } + sort.Strings(keys) + parts := make([]string, 0, len(keys)) + for _, k := range keys { + parts = append(parts, url.QueryEscape(k)+"="+url.QueryEscape(params[k])) + } + paramStr := strings.Join(parts, "&") + baseStr := method + "&" + url.QueryEscape(rawURL) + "&" + url.QueryEscape(paramStr) + sigKey := url.QueryEscape(c.ConsumerSecret) + "&" + url.QueryEscape(c.TokenSecret) + params["oauth_signature"] = hmacSha1(sigKey, baseStr) + + // Build header + headerParts := []string{} + for k, v := range params { + if strings.HasPrefix(k, "oauth_") { + headerParts = append(headerParts, k+`="`+url.QueryEscape(v)+`"`) + } + } + sort.Strings(headerParts) + return "OAuth " + strings.Join(headerParts, ", ") +} + +func (c *TumblrClient) get(endpoint string, params map[string]string) ([]byte, error) { + u, _ := url.Parse(apiBase + endpoint) + q := u.Query() + for k, v := range params { + q.Set(k, v) + } + u.RawQuery = q.Encode() + rawURL := apiBase + endpoint + req, _ := http.NewRequest("GET", u.String(), nil) + req.Header.Set("Authorization", c.oauthHeader("GET", rawURL, params)) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + return io.ReadAll(resp.Body) +} + +func (c *TumblrClient) post(endpoint string, params map[string]string) ([]byte, error) { + form := url.Values{} + for k, v := range params { + form.Set(k, v) + } + rawURL := apiBase + endpoint + req, _ := http.NewRequest("POST", rawURL, strings.NewReader(form.Encode())) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Authorization", c.oauthHeader("POST", rawURL, params)) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + return io.ReadAll(resp.Body) +} + +// --- OAuth token exchange (no client tokens yet) --- + +func oauthRequest(method, rawURL string, consumerKey, consumerSecret, token, tokenSecret string, params map[string]string) ([]byte, error) { + c := &TumblrClient{consumerKey, consumerSecret, token, tokenSecret} + if method == "POST" { + return c.post(strings.TrimPrefix(rawURL, apiBase), params) + } + return c.get(strings.TrimPrefix(rawURL, apiBase), params) +} + +func GetRequestToken(consumerKey, consumerSecret string) (string, string, error) { + c := &TumblrClient{ConsumerKey: consumerKey, ConsumerSecret: consumerSecret} + rawURL := oauthBase + "/oauth/request_token" + req, _ := http.NewRequest("POST", rawURL, nil) + req.Header.Set("Authorization", c.oauthHeader("POST", rawURL, nil)) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", "", err + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + vals, _ := url.ParseQuery(string(body)) + return vals.Get("oauth_token"), vals.Get("oauth_token_secret"), nil +} + +func GetAccessToken(consumerKey, consumerSecret, tmpToken, tmpSecret, verifier string) (string, string, error) { + c := &TumblrClient{ConsumerKey: consumerKey, ConsumerSecret: consumerSecret, Token: tmpToken, TokenSecret: tmpSecret} + rawURL := oauthBase + "/oauth/access_token" + req, _ := http.NewRequest("POST", rawURL, strings.NewReader("oauth_verifier="+url.QueryEscape(verifier))) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Authorization", c.oauthHeader("POST", rawURL, map[string]string{"oauth_verifier": verifier})) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", "", err + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + vals, _ := url.ParseQuery(string(body)) + return vals.Get("oauth_token"), vals.Get("oauth_token_secret"), nil +} + +// --- API response types --- + +type Post struct { + ID json.Number `json:"id"` + IDString string `json:"id_string"` + Type string `json:"type"` + BlogName string `json:"blog_name"` + PostURL string `json:"post_url"` + ReblogKey string `json:"reblog_key"` + NoteCount int `json:"note_count"` + Liked bool `json:"liked"` + Photos []struct { + OriginalSize struct { + URL string `json:"url"` + } `json:"original_size"` + } `json:"photos"` + VideoURL string `json:"video_url"` + ThumbnailURL string `json:"thumbnail_url"` + Caption string `json:"caption"` +} + +type UserInfo struct { + Response struct { + User struct { + Blogs []struct { + Name string `json:"name"` + } `json:"blogs"` + } `json:"user"` + } +} + +type PostsResponse struct { + Response struct { + Posts []Post `json:"posts"` + } +} + +type TaggedResponse struct { + Response []Post +} + +// --- API methods --- + +func (c *TumblrClient) GetUserInfo() (*UserInfo, error) { + body, err := c.get("/user/info", nil) + if err != nil { + return nil, err + } + var result UserInfo + if err := json.Unmarshal(body, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (c *TumblrClient) GetDashboard(offset, limit int, postType string) ([]Post, error) { + params := map[string]string{ + "offset": strconv.Itoa(offset), + "limit": strconv.Itoa(limit), + "type": postType, + } + body, err := c.get("/user/dashboard", params) + if err != nil { + return nil, err + } + var result PostsResponse + if err := json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("parse error: %w — body: %s", err, string(body)) + } + return result.Response.Posts, nil +} + +func (c *TumblrClient) GetBlogPosts(blogName string, offset, limit int, postType string) ([]Post, error) { + params := map[string]string{ + "offset": strconv.Itoa(offset), + "limit": strconv.Itoa(limit), + "type": postType, + } + body, err := c.get("/blog/"+blogName+".tumblr.com/posts", params) + if err != nil { + return nil, err + } + var result PostsResponse + if err := json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("parse error: %w — body: %s", err, string(body)) + } + return result.Response.Posts, nil +} + +func (c *TumblrClient) GetTagged(tag string) ([]Post, error) { + body, err := c.get("/tagged", map[string]string{"tag": tag}) + if err != nil { + return nil, err + } + // Tagged endpoint returns array directly under response + var raw struct { + Response []Post `json:"response"` + } + if err := json.Unmarshal(body, &raw); err != nil { + return nil, err + } + return raw.Response, nil +} + +func (c *TumblrClient) LikePost(id, reblogKey string) error { + _, err := c.post("/user/like", map[string]string{"id": id, "reblog_key": reblogKey}) + return err +} + +func (c *TumblrClient) UnlikePost(id, reblogKey string) error { + _, err := c.post("/user/unlike", map[string]string{"id": id, "reblog_key": reblogKey}) + return err +} + +func (c *TumblrClient) ReblogPost(nativeBlog, id, reblogKey, sourceBlog string) error { + _, err := c.post("/blog/"+nativeBlog+".tumblr.com/post/reblog", map[string]string{ + "id": id, + "reblog_key": reblogKey, + }) + return err +} |
