🧫

ゆめかわ日蚘

MisskeyやSpotifyのAPIで投皿を消したり投げたりする凊理をGoで実装する際に詰たった郚分のメモ

2023幎12月1日にQiitaで公開しおいた蚘事のアヌカむブです。

冬ですね。非垞に寒くお足の指先が倉な色しおいたす。 最近APIを䜿っお䜕らかの凊理をしおいくのにハマっおいたす。

僕は圓初PythonでTwitterのAPIを䜿っお時報や投皿の䞀括消去を行っおみたりしお楜しんでいたんですけど、残念なこずに2022幎の12月にすべおのアカりントが蚀われもない閉鎖凊理に遭っおしたい、アカりントを䜜り盎したのにもかかわらずAPI有料化隒動が出おしたい、流石にもげたした。

しかし、APIの快楜を味わっおしたったのでどこかで摂取したいずいう動機は収たらず犁断症状同じこずをMisskeyでも出来ないかず思っお挑戊しおみたした。

色々悩んだ結果11月の間に3぀出来たんですけど、その䞭の䞀぀である「Misskeyで投皿したコンテンツを抹消するAPI」をテストしたら圓たり前のように党郚投皿が消えちゃったので、色々苊戊した蚘録が消し飛んでしたいたした苊笑

今回はただ蚘憶が残っおるうちにMisskeyやSpotifyのAPIに関する認蚌郚分、投皿郚分で詰たったこずや悩んだこずを敎理しおおいお、今埌たた䜜るずきに確認するチヌトシヌトにしおおきたいず思いたす。

今回も䟋の劂くGoで曞きたした。

認蚌

Misskey

https://misskey-hub.net/docs/api/

MisskeyでAPIを䜿うためには察応するサヌバヌのアクセストヌクンの取埗が必須です。 アクセストヌクンの発行手段はMisskeyの自アカりントの蚭定欄から手動で取埗する方法ずOauth2.0を䜿っおアクセスする手法の二皮類が存圚したす。 今回は自分で利甚するだけずいう点もあり比范的楜な手動発行方匏を採甚したした。

初期の実装は毎回入力させるタむプで曞いおみたした。

func oauth() string {
    var token, host string

    fmt.Println("トヌクンを蚭定しおください。")
    token = readInput("Token: ")

    fmt.Println("サヌバヌのホストを蚭定しおください。")
    host = readInput("Host: ")

 endpoint = "https://" + host + "/api/"

    return token
}

①タヌミナル䞊に入力を促す衚瀺を出し、ナヌザヌに入力させる。 ②入力させた情報を゚ンドポむントの圢にしお成圢する ③POST凊理に枡しお実行する

ずいう圢です。

基本的にはこれで問題ないんですけど、毎回凊理を走らせるたびにトヌクンずホスト名を入力しおいるず非垞に面倒だなず思ったので、もうファむルの䞭に保持しおしたおうかずいう考えに至りたした。

しかしながらファむル内に盎接トヌクンのような機密情報を保持させおしたうず、チラリズムどころの話ではなく電子露出狂になっおしたう懞念があったので、コヌド本䜓に埋め蟌むのではなく、環境倉数を䜿うように方向転換したした。

.envファむルを䜿甚した堎合、git等のリポゞトリに公開する際に.gitignoreで該圓するファむルを陀倖できる点や、今埌DevブランチやTestブランチを䜜っお異なる環境倉数が登堎した際に䞀括しお管理するこずが出来るので、テストやデバッグにも最適です。 䜕よりも毎回入力する必芁が無いのが非垞に助かりたすね。

今回はGo向けの倖郚パッケヌゞで.env ファむルから環境倉数を読み蟌むためのツヌルずしお怜玢に匕っかかったgodotenvを䜿甚させおいただきたした。

godotenvをむンポヌトするずこんな感じでファむル経由で環境倉数が読み取れるようになるので非垞に䟿利です。

package main

import (
    "log"
    "os"

    "github.com/joho/godotenv"
)

func main() {
  err := godotenv.Load()
  if err != nil {
    log.Fatal(".envファむルが芋぀からないよ")
  }

  loginID := os.Getenv("YAH_YAH_YAH")
  logginSECRET := os.Getenv("DO_YA_DO")

}

Misskeyの堎合はホスト名ずトヌクンを入力するこずになるので、このように改造したした。

func oauth() string {
    err := godotenv.Load()
    if err != nil {
        fmt.Println(".envファむルが芋぀からないか読み蟌めたせん")
        return ""
    }

    token := os.Getenv("TOKEN")
    host := os.Getenv("HOST")

    if token == "" || host == "" {
        fmt.Println("トヌクンずホストを正しく蚭定しおください。")
        return ""
    }

    endpoint = "https://" + host + "/api/"

    fmt.Println("Endpoint:", endpoint)
    return token
}

ファむルが芋぀からないこずに加え、どちらかを欠萜させおしたう可胜性があるかもしれないので色々譊告文を出しおいたすが、゚ンドポむントに匕き枡す凊理は前回ず同様のたたで問題ありたせん。

あずはmain関数の郚分でMisskeyが指定するiのパラメヌタヌにoauth()で入力されたトヌクンを枡すず、認蚌が通るようになりたす。

func main() {
    token := oauth()
    me, err := FetchUser("i", map[string]interface{}{"i": token})
    if err != nil {
        fmt.Println("照䌚䞭に゚ラヌが発生したした:", err)
        return
    }

Spotify

SpotifyのAPIを䜿甚する堎合、OAuth 2.0認蚌プロセスを通じおアクセストヌクンaccess_tokenずリフレッシュトヌクンrefresh_tokenを取埗したす。(これがなかなかにキツかった...)

Spotifyのアクセストヌクンは䞀時間で死んでしたうため、リフレッシュトヌクンを䜿っお定期的に蘇生するこずになりたす。リフレッシュトヌクンがあるずいちいちナヌザヌに再床蚱可を求めなくおもアクセストヌクンが発行できるので非垞に䟿利です。

しかし、SpotifyのDevダッシュボヌドで確認できるのはClientIDずClientSecretだけです。 そのため、 ClientIDずClientSecretを䜿甚しお→リフレッシュトヌクン発行コヌドを取埗→リフレッシュトヌクンを取埗→アクセストヌクンの取埗ずいうプロセスを螏むこずになりたす。

https://github.com/yude/np2mast

ここの凊理に関しおはYudeさんずいう開発者の方が1幎ほど前にMastodonで再生䞭の曲を送信する凊理を曞かれおいるのを倧いに参考にしたした。

初期の段階ではむンポヌトしお必芁な情報を曞き蟌んだのに䜕故か401゚ラヌが出おしたったので悩んでいたずころ、リフレッシュトヌクン発行コヌドを取埗するプロセスでスコヌプを指定する郚分が取れおしたっおいたので、こちらを远加しおMisskey向けに改造したした。

values.Add("scope", "user-read-playback-state user-read-currently-playing")
//envファむルから読み取る郚分
values.Add("client_id", os.Getenv("SPOTIFY_CLIENT_ID"))
        values.Add("response_type", "code")
        values.Add("redirect_uri", "http://localhost:3000/callback")
        values.Add("scope", "user-read-playback-state user-read-currently-playing")
        fmt.Println("https://accounts.spotify.com/authorize?" + values.Encode())
//URLにアクセスしおコヌドを取埗する郚分
func spotify_login(w http.ResponseWriter, req *http.Request) {
    values := url.Values{}
    values.Add("client_id", os.Getenv("SPOTIFY_CLIENT_ID"))
    values.Add("response_type", "code")
    values.Add("redirect_uri", "http://localhost:3000/callback")
    values.Add("scope", "user-read-playback-state user-read-currently-playing")

    http.Redirect(w, req, "https://accounts.spotify.com/authorize?"+values.Encode(), http.StatusFound)
}

送信、消去の凊理

Misskeyで投皿を送信、消去する堎合は、゚ンドポむントに適切なパラメヌタでjson圢匏のデヌタを送信したす。 ゚ンドポむントは各操䜜ごずに明確に分かれおいお、リク゚スト凊理がPOSTかGETかずいう点に関しおも匷く意識する必芁がありたす。

https://misskey-hub.net/docs/api/endpoints/notes/create.html

詳现な点ぱンドポむント䞀芧をご参照ください。

䟋えば、送信凊理の堎合は

func postToMisskey(message string) error {
    misskeyURL := os.Getenv("MISSKEY_ENDPOINT_URL") + "/api/notes/create"
    //ここで゚ンドポむントを指定。"/api/notes/create"が珟行の投皿䜜成の゚ンドポむント

    requestData := map[string]string{
        "i":          os.Getenv("MISSKEY_ACCESS_TOKEN"),
        "text":       message,
        "visibility": "home",
    }
    //ここで゚ンドポむントが芁求する適切なパラメヌタを蚭定しおいなければUnauthorized゚ラヌ(401)が出おしたいたす。

    jsonData, err := json.Marshal(requestData)
    if err != nil {
        return fmt.Errorf("リク゚ストデヌタのJSON゚ンコヌドに倱敗したした。: %v", err)
    }

    resp, err := http.Post(misskeyURL, "application/json", bytes.NewBuffer(jsonData))
    if err != nil {
        return fmt.Errorf("ノヌトに倱敗したした。: %v", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        body, _ := io.ReadAll(resp.Body)
        log.Printf("Failed to post to Misskey. Status: %d. Response: %s", resp.StatusCode, string(body))
        return errors.New("ノヌトに倱敗したした")
    }

    return nil
}

僕の堎合「トヌクンがあっおるのに認蚌されないっ...䜕故」ずいう点で非垞に詰たっおしたっおしたい、適切なパラメヌタが蚭定されおいないこずに時間がかかっおしたいたした。 芁するにここの実装が抜けおいお、盎接゚ンドポむントに文字列を送ろうずしたわけですね。

    requestData := map[string]string{
        "i":          os.Getenv("MISSKEY_ACCESS_TOKEN"),
        "text":       message,
        "visibility": "public",
    }
    jsonData, err := json.Marshal(requestData)
    if err != nil {
        return fmt.Errorf("リク゚ストデヌタのJSON゚ンコヌドに倱敗したした。: %v", err)
    }

    resp, err := http.Post(misskeyURL, "application/json", bytes.NewBuffer(jsonData))
    if err != nil {
        return fmt.Errorf("ノヌトに倱敗したした。: %v", err)
    }
    defer resp.Body.Close()

送信先のMisskeyは厳栌にデヌタの扱いを決めおいるため、単玔な文字列の挿入では送信できないようになっおいたす。逆にこれがあるから倉なデヌタが送り付けられないずいう点があり非垞に助かりたすね。

投皿の取埗やノヌトの消去も同様です。

func GetUsersNotes(userId, untilId, token string) ([]Note, error) {
    args := map[string]interface{}{}
    if untilId == "" {
        args = map[string]interface{}{
            "userId": userId,
            "limit":  100,
            "i":      token,
            "withChannelNotes": true,
        }
    } else {
        args = map[string]interface{}{
            "userId":  userId,
            "untilId": untilId,
            "limit":   100,
            "i":       token,
            "withChannelNotes": true,
        }
    }

    notes, err := FetchNotes("users/notes", args)
    return notes, err
}
func DeleteNote(noteId, token string) error {
    args := map[string]interface{}{
        "noteId": noteId,
        "i":      token,
    }

    return Post("notes/delete", args, nil)
}

尚、いちいちJson圢匏に盎すのは非垞に億劫だず思うので、Goではナヌザヌ、ノヌト、゚ラヌ毎に構造䜓を持たせお䞀元管理する方法が䟿利です。

type User struct {
    Name        string `json:"name"`
    Username    string `json:"username"`
    NotesCount  int    `json:"notesCount"`
    Id          string `json:"id"`
    PinnedNotes []Note `json:"pinnedNotes"`
}

type Note struct {
    Id        string    `json:"id"`
    CreatedAt time.Time `json:"createdAt"`
}

type Error struct {
    Message string `json:"message"`
    Code    string `json:"code"`
    Id      string `json:"id"`
    Kind    string `json:"kind"`
}

このように管理しおおけば、同様の凊理を䜕床も蚘述する必芁がありたせん。

補足

API䜿甚時の䜜法ずしお、MisskeyやMastodonは個人で運営しおいるサヌバヌが倚いこずから、APIを䞀床に倧量にリク゚ストしおしたうず運営者のサヌバヌに負荷をかけおしたいたす。

䞀応どちらにもリク゚スト制限が蚭けられおいお、平均的には300リク゚ストぐらいするず1時間のむンタヌバルを蚭けるように蚭定しおあるのですが、䜕床も同じ堎所からリク゚ストされるずお行儀が悪いのず、コストの無駄が考えられたす。

ぶっちゃけ同䞀の送信元から䜕床もリク゚ストしおるのを芋られるず「こい぀DOSんちゅだな...」ず解釈しおIPBANされかねたせん。

そのため、リク゚スト制限がかかったら䜕床もリク゚ストしないようにブランクタむムを蚭けおおくのが有効です。

䟋えばGoの暙準ラむブラリにあるtimeをむンポヌトしおおいお

time.Sleep(30 * time.Minute)

のように蚭定しおおけば30分間送信凊理を䞭断するこずが出来たす。

感想

今たではロヌカル環境で凊理を行うものだけを曞いおきたのもあっお、送信先の事を配慮した実装に関しおは意識できおいなかったのですが、1か月を通しおAPIの実装を孊んだこずでお行儀のよい通信ずは䜕かを意識するようになりたした。

今埌もチャンネルのノヌトを消す実装だったり、ゎルヌチンがこけた時に埩垰させる凊理を远加する等、やりたいこずがいっぱいあるので、日々粟進しお参りたす。

よくGoで曞くず゚ラヌハンドリングがどうしおも面倒くさいずいう意芋を頂くのですが、僕は諞孊者なのでどういった゚ラヌなのかを蚀葉で説明できるアりトプットの堎になるのですごく助かっおいたす。

今埌もゎヌファヌ君を愛で぀぀2024幎を迎えおいきたいず思いたす。よいお幎を早い

ここに曞かれおいる内容に関しおは根拠や出兞の存圚を䞀切保蚌したせん。

たた、実際の人物や団䜓に圱響が出ないようにある皋床抜象化したり脚色したりするこずもありたす。

ぶび