Skip to content

Instantly share code, notes, and snippets.

@a2ikm
Created March 19, 2023 07:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save a2ikm/ffc6058660db6226193058e0b92b4c9d to your computer and use it in GitHub Desktop.
Save a2ikm/ffc6058660db6226193058e0b92b4c9d to your computer and use it in GitHub Desktop.
package main
import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"os/exec"
"strings"
"time"
)
const state = "this-is-state"
const (
authorizationSuccess = iota
authorizationFailure
)
type authorizationResult struct {
status int
token string
}
func newAuthorizationResultSuccess(token string) authorizationResult {
return authorizationResult{
status: authorizationSuccess,
token: token,
}
}
func newAuthorizationResultFailure() authorizationResult {
return authorizationResult{
status: authorizationFailure,
token: "",
}
}
type tokenResponse struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
TokenType string `json:"token_type"`
Scope string `json:"scope"`
RefreshToken string `json:"refresh_token"`
}
var consumerKey string
var consumerSecret string
var accessToken string
var blogIdentifier string
var beforeTimestamp int64
var authorized chan authorizationResult
func main() {
consumerKey = readConsumerKey()
consumerSecret = readConsumerSecret()
authorized = make(chan authorizationResult)
server := bootServer(authorized)
requestAuthorization()
result := <-authorized
server.Shutdown(context.Background())
switch result.status {
case authorizationSuccess:
log.Println("authorized")
accessToken = result.token
deleteOldPosts()
case authorizationFailure:
log.Fatalln("authorization failed")
}
}
func deleteOldPosts() {
blogIdentifier = readBlogIdentifier()
beforeTimestamp = readBeforeTimestamp()
for {
ids := getOldPostIds()
if len(ids) == 0 {
break
}
time.Sleep(1 * time.Second)
for _, id := range ids {
deletePost(id)
time.Sleep(1 * time.Second)
}
}
}
type getPostsResponse struct {
Response struct {
Posts []struct {
Id int64 `json:"id"`
} `json:"posts"`
TotalPosts int `json:"total_posts"`
} `json:"response"`
}
func getOldPostIds() []int64 {
u := fmt.Sprintf("https://api.tumblr.com/v2/blog/%s/posts?before=%d", blogIdentifier, beforeTimestamp)
req, err := http.NewRequest(http.MethodGet, u, nil)
if err != nil {
log.Fatalf("failed to create request to get ids: %v", err)
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", accessToken))
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatalf("failed to send request to get ids: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
msg, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatalf("failed to read response of get ids: %v", err)
}
log.Fatalf("failed to get ids: %s", msg)
}
var res getPostsResponse
err = json.NewDecoder(resp.Body).Decode(&res)
if err != nil {
log.Fatalf("failed to decode get posts response: %v", err)
}
log.Printf("total posts is %d\n", res.Response.TotalPosts)
ids := make([]int64, 0, 10)
for _, post := range res.Response.Posts {
ids = append(ids, post.Id)
}
return ids
}
func deletePost(id int64) {
u := fmt.Sprintf("https://api.tumblr.com/v2/blog/%s/post/delete", blogIdentifier)
body := bytes.NewBufferString(fmt.Sprintf("id=%d", id))
req, err := http.NewRequest(http.MethodPost, u, body)
if err != nil {
log.Fatalf("failed to create request to delete post: %v", err)
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", accessToken))
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatalf("failed to send request to delete post: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
msg, err := io.ReadAll(resp.Body)
if err != nil {
log.Fatalf("failed to read response of delete post: %v", err)
}
log.Fatalf("failed to delete post: %s", msg)
}
}
func readUserInput(label string) string {
reader := bufio.NewReader(os.Stdin)
fmt.Printf("Please input %s: ", label)
input, _ := reader.ReadString('\n')
return strings.TrimSpace(input)
}
func readConsumerKey() string {
return readUserInput("your consumer key")
}
func readConsumerSecret() string {
return readUserInput("your consumer secret")
}
func readBlogIdentifier() string {
return readUserInput("your blog identifier like your-blog-name.tumblr.com")
}
func readBeforeTimestamp() int64 {
input := readUserInput("before timestamp in yyyy-mm-dd form")
t, err := time.Parse("2006-01-02", input)
if err != nil {
log.Fatalf("failed to parse before timestamp: %s err: %v", input, err)
}
return t.Unix()
}
func requestAuthorization() {
params := url.Values{}
params.Set("client_id", consumerKey)
params.Set("response_type", "code")
params.Set("scope", "basic write")
params.Set("state", state)
aurl := "https://www.tumblr.com/oauth2/authorize?" + params.Encode()
exec.Command("open", aurl).Run()
}
func bootServer(authorized chan authorizationResult) *http.Server {
mux := http.NewServeMux()
mux.HandleFunc("/ping", pingHandler)
mux.HandleFunc("/callback", callbackHandler)
s := &http.Server{
Addr: "0.0.0.0:8000",
Handler: mux,
}
go func() {
log.Println("booting server...")
err := s.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
log.Fatalf("server error: %v", err)
}
}()
client := &http.Client{
Timeout: 1 * time.Second,
}
for {
_, err := client.Get("http://localhost:8000/ping")
if err != nil {
log.Println("wait 1 second...")
time.Sleep(1 * time.Second)
continue
}
break
}
log.Println("booted")
return s
}
func pingHandler(resp http.ResponseWriter, req *http.Request) {
resp.WriteHeader(http.StatusOK)
}
func callbackHandler(resp http.ResponseWriter, req *http.Request) {
defer req.Body.Close()
u, err := url.ParseRequestURI(req.RequestURI)
if err != nil {
resp.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(resp, "failed to parse request url: %v", err)
authorized <- newAuthorizationResultFailure()
return
}
authres, err := url.ParseQuery(u.RawQuery)
if err != nil {
resp.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(resp, "failed to parse query: %v", err)
authorized <- newAuthorizationResultFailure()
return
}
if authres.Has("error") {
resp.WriteHeader(http.StatusUnauthorized)
fmt.Fprintf(resp, "authorization request error: %v", authres)
authorized <- newAuthorizationResultFailure()
return
}
params := url.Values{}
params.Set("grant_type", "authorization_code")
params.Set("code", authres.Get("code"))
params.Set("client_id", consumerKey)
params.Set("client_secret", consumerSecret)
res, err := http.PostForm("https://api.tumblr.com/v2/oauth2/token", params)
if err != nil {
resp.WriteHeader(http.StatusUnauthorized)
fmt.Fprintf(resp, "token request error: %v", err)
authorized <- newAuthorizationResultFailure()
return
}
tokres := tokenResponse{}
err = json.NewDecoder(res.Body).Decode(&tokres)
if err != nil {
resp.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(resp, "token response parse error: %v", err)
authorized <- newAuthorizationResultFailure()
return
}
res.Body.Close()
resp.WriteHeader(http.StatusOK)
fmt.Fprintf(resp, "Authorized :)\n")
authorized <- newAuthorizationResultSuccess(tokres.AccessToken)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment