關於 web service, unity, blogger 等軟體工程筆記

使用 Golang 串接 Google Blogger API,張貼文章與上傳圖片

Edit icon 沒有留言
Golang Goolge APIs - Blogger & PicasaWeb

個人專案需求,建置較容易發佈文章到部落格的流程服務,使用 golang 串接 Google Blogger API,將文章文字以及其圖片上傳到 Blogger 平台。Google 已經在 Github 上發布專為 golang 所準備的 SDK 實作,其中包含管理部落格的 Blogger API SDK 實作,但關於 Blogger 平台的圖床 (image hosting) 的圖片上傳管理,Picasa API 卻沒有提供其 SDK 實作……。紀錄當初完成流程實作的開發筆記。

Google Blogger API (張貼文章)

了解 Blogger API 可參考以下兩個來源:

而 Blogger API SDK for Golang,則可以在 Github 上找的到,上頭包含部分 API 範例、說明文件以及其他相關資源連結,或是透過 Go 指令直接安裝 Blogger SDK:

go get google.golang.org/api/blogger/v3

因此取得帳號部落格列表 (為了取得 blogId, blogger.Blog.Id):

import (
   "net/http"
   "google.golang.org/api/blogger/v3"
)

func BlogList(client *http.Client) ([]*blogger.Blog, error) {
   service, err := blogger.New(client)
   if err != nil {
      return nil, err
   }

   user, err := service.Users.Get("self").Do()
   if err != nil {
      return nil, err
   }

   list, err := service.Blogs.ListByUser(user.Id).Do()
   if err != nil {
      return nil, err
   }

   return list.Items, nil
}
  • Line6: client 為取得認證的 HTTP Client,後續章節會使用 OAuth2 取得該 client
  • Line7: 建立 service,後續 API 呼叫都需要靠它

透過 SDK 新增文章:

func PostBloggerPost(client *http.Client, blogId, title, content string, lables []string, draft bool) (*blogger.Post, error) {
   service, err := blogger.New(client)
   if err != nil {
      return nil, err
   }

   post := &blogger.Post{
      Content: content,
      Labels:  lables,
      Title:   title,
   }

   rpost, err := service.Posts.Insert(blogId, post).IsDraft(draft).Do()
   if err != nil {
       return nil, err
   }
   return rpost, nil
}
  • Line6: blogId 上述取得的 BlogId,draft 表示建立草稿而不是直接發佈
  • Line7: 建立文章能放入的參數,對比使用 Blogger 系統似乎是少了許多,不能設定文章連結,不能設定搜尋說明…

Picasa Web API (上傳圖片)

Blogger 原先內建的圖床 (image hosting) 是 Picasa,但之前 Picasa 因為推出 Google Photos 而被放棄 (see this post),那麼如何上傳圖片到預設的 Blogger 圖床呢?

從一些討論以及文章,目前還可以從 Album archive 取得之前上傳到 Picasa 的相片,也可從找到來自於 Blogger 的相片。另外在 Picasa Web API 說明頁中,發現還是可以有限度的調用 API,上傳圖片到 Picasa 這一個 Blogger 預設圖床中。

但…Google 並沒有提供 Picasa Web API for Golang SDK,當初從 Github 上僅僅找到 picago,裡頭有實作部分 Picasa Web API 串接,並提供從 Picasa 下載相片的實作與範例,但卻沒有提供上傳的功能。

因為沒有,所以只好按照 Picasa Web API 文件: Posting a new photo 的說明,土砲作了一個上傳功能,並且送出 pull requests 給 picago,最後意外變成該 library 的 contributors 之一。

取得 picago:

go get github.com/tgulacsi/picago

其主要上傳函數說明:

package picago

func UploadPhoto(client *http.Client, userID, albumID, fileName, summary, MIME string, photoRaw []byte) (*Photo, error) {
   ...
}
  • client: 跟 Blogger 上傳文章一樣,為取得認證的 HTTP Client
  • userID: 使用者 ID,空字串表示自己 (self)
  • albumID: 相簿 ID,在這個專案中,應該是 Blogger 相簿 ID
  • fileName: 檔案名稱,慎選檔明,SEO 很重要
  • summary: 說明,只有在 Album archive 才看得到
  • MIME: 相片類型,只有這四種 “image/bmp”, “image/gif”, “image/jpeg”, or “image/png”

因此上傳圖片到 Blogger 圖床的 SDK 問題已解決,比較大的問題會是怎麼知道哪些是 Blogger 專屬相簿?從 API 文件找到可根據 AlbumType 以及其相簿標題來尋找:

import (
   "errors"
   "net/http"
   "github.com/tgulacsi/picago"
)

func FindAlbumId(client *http.Client, blogName string) (string, error){
   albums, err := picago.GetAlbums(client, "")
   if err != nil {
      return "", err
   }
   for _, album := range albums {
      if album.AlbumType == "Blogger" && album.Title == blog.Name {
         return album.ID, nil
      }
   }

   return "", errors.New("Can't not find Blogger album")
}

在上傳圖片成功後,如何取得 Blogger 圖片 URL?可以使用 UploadPhoto 的回傳值,取得圖片 URL,然後將其 URL 調整轉換成在 Blogger 平台插入圖片的格式,其中 BlogspotHosts 定義目前可用的 Hosts:

var (
   BlogspotHosts = [...]string{
      "1.bp.blogspot.com",
      "2.bp.blogspot.com",
      "3.bp.blogspot.com",
      "4.bp.blogspot.com",
   }
)

photo, err := picago.UploadPhoto(...)
if err != nil {
    return nil, err
}

u, _ := url.Parse(photo.URL)
lastSlash := strings.LastIndex(u.Path, "/")
u.Scheme = "https"
u.Host = BlogspotHosts[rand.Int31n(int32(len(BlogspotHosts)))]
u.Path = u.Path[0:lastSlash+1] + "s1600" + u.Path[lastSlash:]

// TODO: photo URL, u.String()

例如以下範例,Original 為原先的相片路徑 photo.URL,Modified 為調整後的路徑 u.String()

Original: https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEip2fHeV89Czuol52fsHuNr_Go2Di4dRuHuNYkr8idVu2BPrOA9NhZrmH0FGa-Fp-ZnzVFQsFz6s66WbEuDnpx76YbW2mUnZoKrVDyGwSTWW_oGL27OMDzQHGOzAfs41I7qNB1Y-xM1LPwQ/

Modified: https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEip2fHeV89Czuol52fsHuNr_Go2Di4dRuHuNYkr8idVu2BPrOA9NhZrmH0FGa-Fp-ZnzVFQsFz6s66WbEuDnpx76YbW2mUnZoKrVDyGwSTWW_oGL27OMDzQHGOzAfs41I7qNB1Y-xM1LPwQ/s1600/gopher.jpg

使用 OAuth2 Playground 取得 Access token,建立測試用的 HttpClient

上述兩節都需要擁有授權資料的 HttpClient 才可以正常運作,否則都會出現 Authorization error。因此需要使用 OAuth2 流程來建立取得 Access token,取得擁有授權資料 HttpClient。

在開發階段可以使用 OAuth2 Playground 來取得 Access token。

OAuth2 Playground 介面

OAuth2 Playground 介面

取得 Access token,利用 OAuth2 建立取得 HttpClient:

import (
   "context"
   "net/http"

   "golang.org/x/oauth2"
   "golang.org/x/oauth2/google"
)

func testClient() *http.Client {
   config := &oauth2.Config{
      Endpoint: google.Endpoint,
      Scopes: []string{
         "https://www.googleapis.com/auth/blogger",
         "https://picasaweb.google.com/data/",
      },
   }

   token := oauth2.Token{
      AccessToken: "YOUR_ACCESS_TOKEN",
      TokenType:   "Bearer",
   }

   return config.Client(context.Background(), &token)
}

*http.Client 提供給上幾節範例使用,便可完成 API 串接,但總不能每次都靠 OAuth2 Playground 來取得吧,因此下節要實作整個授權流程。

建立 OAuth2 授權流程,取得授權的 HttpClient

為了完成 OAuth2 授權流程,讓使用者授權取得管理其帳號的 Blogger 以及 Picasa 管理圖片影片的權限,得先申請專用的 ClientID 以及 ClientSecret。

  • Google APIs 管理介面建立新的專案
  • 啟用適當的 API ,在這個專案中最主要是啟用 Blogger,Picasa 則不用 (猜測是舊的 API 所以沒有被納入管理)
    • Blogger API v3
  • 建立 OAuth ClientID,其應用程式為「網路應用程式」
  • 記住其用戶端 ID (Client_ID) 以及其密鑰 (Client_Secret)
API 管理頁,注意必須啟用 Blogger API

API 管理頁,注意必須啟用 Blogger API

一張圖解釋整個 OAuth2 的授權流程,需要實作 /blogger/login 重新導向到 Google 授權頁面以及 /blogger/login/callback 取得 Authorization code 後,再向 Google 交換正式的 Access token:

OAuth2 取得 Access Token 的流程

OAuth2 取得 Access Token 的流程

參考開發指南,按照範例 建立 OAuth2 登入流程:

package main

import (
   "context"
   "fmt"
   "math/rand"
   "net/http"

   "golang.org/x/oauth2"
   "golang.org/x/oauth2/google"

   "github.com/tgulacsi/picago"
   "google.golang.org/api/blogger/v3"

   gcontext "github.com/gorilla/context"
   "github.com/gorilla/sessions"
)

const (
   GoogleClientID     = "YOUR_CLIENT_ID"
   GoogleClientSecret = "YOUR_CLIENT_SECRET"

   sessionName = "TWSIYAN-LOGIN-EXAMPLE"
)

var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")

func randStringRunes(n int) string {
   b := make([]rune, n)
   for i := range b {
      b[i] = letterRunes[rand.Intn(len(letterRunes))]
   }
   return string(b)
}

var store = sessions.NewCookieStore([]byte("something-very-secret"))

func main() {

   config := &oauth2.Config{
      ClientID:     GoogleClientID,
      ClientSecret: GoogleClientSecret,
      Endpoint:     google.Endpoint,
      RedirectURL:  "http://127.0.0.1:8080/blogger/login/callback",
      Scopes: []string{
         blogger.BloggerScope,
         picago.PicasaScope,
      },
   }

   m := http.NewServeMux()
   s := &http.Server{
      Addr:    ":8080",
      Handler: gcontext.ClearHandler(m),
   }

   m.HandleFunc("/blogger/login", func(w http.ResponseWriter, req *http.Request) {

      session, err := store.Get(req, sessionName)
      if err != nil {
         http.Error(w, err.Error(), http.StatusInternalServerError)
         return
      }

      // Avoid CSRF (Cross-site request forgery)
      state := randStringRunes(20)
      session.Values["oauth_state"] = state
      session.Save(req, w)

      // Require refresh_token, so set oauth2.AccessTypeOffline
      authCodeUrl := config.AuthCodeURL(state, oauth2.ApprovalForce, oauth2.AccessTypeOffline)
      http.Redirect(w, req, authCodeUrl, http.StatusTemporaryRedirect)
   })

   m.HandleFunc("/blogger/login/callback", func(w http.ResponseWriter, req *http.Request) {

      session, err := store.Get(req, sessionName)
      if err != nil {
         http.Error(w, err.Error(), http.StatusInternalServerError)
         return
      }

      state, ok := session.Values["oauth_state"].(string)
      if !ok {
         http.Error(w, "Bad request, no oauth_state", http.StatusBadRequest)
         return
      }

      values := req.URL.Query()
      if values["state"] == nil || values["code"] == nil || values["state"][0] != state {
         http.Error(w, "Bad request, arguments is not valid", http.StatusBadRequest)
         return
      }

      code := values["code"][0]
      token, err := config.Exchange(context.Background(), code)
      if err != nil {
         http.Error(w, err.Error(), http.StatusInternalServerError)
         return
      }

      // TODO: Do API using this client
      client := config.Client(context.Background(), token)

      // TODO: Save token for further use
   })

   println("Go to the url below to login: http://127.0.0.1:8080/blogger/login")

   if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
      panic(err)
   }
}

使用者開啟 http://127.0.0.1:8080/blogger/login 頁面,導入 Google 登入頁面進行授權,然後將登入授權碼代入重新轉向頁 http://127.0.0.1:8080/blogger/login/callback,之後 OAuth2 內部實作取得 Token 並建立帶有授權資料的 HttpClient,提供給上幾節範例使用,便可完成 API 串接,透過 Golang 在 Blogger 張貼文章以及上傳圖片到其圖床。

授權確認視窗

授權確認視窗

沒有留言: