RM-BLOG

IT系技術職のおっさんがIT技術とかライブとか日常とか雑多に語るブログです。* 本ブログに書かれている内容は個人の意見・感想であり、特定の組織に属するものではありません。/All opinions are my own.*

InstagramからTwitterに投稿を自動連携する個人ツールを改造した話

はじめに

Instagramにあげた投稿をTwitetrに自動連携する個人ツール(以下i2tと略す、オリジナルの発想はここ、まあ興味があれば)をつくって回してたんだが、 日本時間で2023年5月23日の夜間からTwitter Appがsuspendされて、Twitter API[{"message":"Could not authenticate you","code":32}] ってエラーを吐くようになった。 Twitter検索してたら同時期に同じ現象に遭遇していた人たちがいっぱいいたので、まあ同じタイミングで大量suspendされたんだろう。 明確な理由はわからないし別に最早あまり興味もないが、おそらくTwitter API 1.1が止まったorこれから1.1止めるので未だに使い続けてるやつをsuspend、って感じなんじゃないかと予想する。 そんなわけでこの個人ツールは死亡し、i2tは終わりを迎えた。のだが…

諸々検討

Twitter API v2を使えば投稿(POST)できなくもなさそうだな、というのはちょっと調べてわかっていた。 1.1に比べると大分面倒くさそうだし、仮にできても画像のアップロードが出来ないんじゃそこまで本格的に頑張りたい気持ちもわかなかったのだが、どうせならちょっと調べてみるかと、suspendされた後からちょこちょことやり方を模索していた。 で、もともと1.1使って動かしていたi2tを、2.0を使う形で改造しようと思うに至った。

改造ポイント

node-twitter -> axios

i2tのTwitter APInode-twitterというOSSを使ってたんだけど、これは1.1を使っているのと、今回使うエンドポイントは限られているのもあるので、axios使って自分でリクエストする実装に変えた。 まあというかそんなこと言い出したらもともと別にいちいちOSS使うまでもないんだけどね、そんな複雑なことしてるわけでもなかったので。。 なお、今探したらnode-twitter-api-v2とかいうそれっぽいOSSがもう出来てるようだが、後の祭りなので見なかったことにする。

DynamoDB

OAuth2 Tokenを使ったAPI経由でのTweetは、1.1時代に比べると面倒くさくなっていて、適切なパーミッションに基づきOAuth2トークンを発行して、必要に応じてrefreshして使う、という前準備が必要だった。 このテのやつは環境変数に持ちたくなるが、i2tの動作基盤であるLambdaに関しては、環境変数更新するたびにdeployが必要という話を聴いて、なんか面倒くさそうなのでやめた。 TwitterのOauth2トークンは2時間で有効期限が切れるので、最大でも24時間で12回、コードの変更は一切なくてもdeployをして新バージョンを作らなければいけなくなる。 これはちょっと本質的ではないと感じた。 そういうわけで、あまり使いたくなかったんだがこの際もういいやと思って、トークンの値をDynamoDBに保持して、refreshするたびにDynamoDBを更新する形で使うことに決めた。

DynamoDBは今まで使ってなかったので(別の個人開発では使ったことあったが)今回新たにこの処理部分を追加実装する必要があった。 知らなかったのだが、従来のaws-sdk(v2系)はどうやらもうEOLに近づいてるらしく、何も考えずにyarn add aws-sdkして使ってたら「v3使えよ」と警告された。 aws-sdkはそれ単品で色々なサービスのリクエストを行うクラスを生成できるライブラリだったが、v3系はサービスごとにライブラリが分離していて、例えばDynamoDBだと "@aws-sdk/client-dynamodb" のような形でいちいち個別に指定しないといけなくなっている。 GetItemとかの実装も少し変わっていて、v2系だと AWS.DynamoDB.DocumentClient.get.promiseのような形でコールしていたのが、事前にGetItemCommandというパラメータを用意して、DynamoDBClient.sendというメソッドにそれを渡すという構造になっている。 わかりづらかったのがUpdateで、"@aws-sdk/client-dynamodb"UpdateItemCommand というクラスがある一方で、"@aws-sdk/lib-dynamodb"という別のライブラリに UpdateCommand というクラスがあって、後者の方が使い勝手がいいのだが、そのことがあまり解説されておらず困った。 なんでわざわざ2つ別の場所に使い勝手の違うクラスを用意したんだかわからん… 以下のブログ記事が参考になった。
https://maku.blog/p/5mv5dkt/#%E3%82%A2%E3%82%A4%E3%83%86%E3%83%A0%E3%81%AE%E5%B1%9E%E6%80%A7%E5%80%A4%E3%82%92%E9%83%A8%E5%88%86%E7%9A%84%E3%81%AB%E6%9B%B4%E6%96%B0%E3%81%99%E3%82%8B-updateitemcommand

ついでといってはなんだが、初回起動でLambdaが「権限もらってねーからDynamoDBみれねえよ」といって死んだ。 そういわれてみればそうだった、君わざわざ権限与えないと動けないんだったね。。 以下を見てアクセス権を付与。
https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/reference_policies_examples_lambda-access-dynamodb.html

OAuth2 Tokenの発行

一度発行してしまえば、あとは定期的にrefreshするのはi2tが自動的にやってくれるが、最初の発行だけはどうしても手動にならざるを得ない。 ローカルに超簡単なExpressのWEBサーバーを立ち上げて、Twitterのコールバック先をそこに指定し(http://localhos:4000t/callback、ポートはなんでもいいけど)、発行した。 詰まったのは、認可エンドポイント https://twitter.com/i/oauth2/authorize にリクエストしてcode取得してから、 OAuth2 Tokenを取得するためのエンドポイント https://api.twitter.com/2/oauth2/token にリクエストするまでに30秒以上経過してしまうことが多く、 OAuth2 Tokenのリクエストで毎回 {"error":"invalid_request","error_description":"Value passed for the authorization code was invalid."} のエラーが返ってくることだった。 どうも認可コードは30秒という超短期間でexpireするらしく、これが原因だったようだ。以下該当箇所抜粋。 This allows an application to hit APIs on behalf of users. Known as the auth_code. The auth_code has a time limit of 30 seconds once the App owner receives an approved auth_code from the user. You will have to exchange it with an access token within 30 seconds, or the auth_code will expire. つまり認可コード取得→OAuth2 Token発行までを手動でやってると恐らく間に合わなそうだということがわかった。 そんなわけで、コールバックのルーター処理の中で認可コード捕まえて、速攻でaxiosするように実装した。 参考までに。以下のようなコードです。

const express = require('express');
const app = express();
const port = process.env['PORT'] || 4000;
const axios = require('axios');

app.get('/callback' , async (req,res)=>{
    const msg = `hello /callback;req=${JSON.stringify(req.query)}`;
    try {
        if (req.query.code) {
            const authorizationCode = req.query.code;
            console.log('call /2/oauth2/token; code=' + authorizationCode);
            const r = await axios.post('https://api.twitter.com/2/oauth2/token', {
                code: authorizationCode,
                grant_type: 'authorization_code',
                client_id: '<cliend id>',
                redirect_uri: 'http://localhost:4000/callback',
                code_verifier: '<code verifier>',
            },{
                headers: {
                    'Authorization' : 'Basic <cliend id:client secret - base64>',
                    'Content-Type' : 'application/x-www-form-urlencoded',
                }
            }
            );

            console.log(new Date());
            console.log(`r=${JSON.stringify(r.data)}`);

        }
    } catch(error) {
        console.log(`error happened; error=${JSON.stringify(error)}`);
    }
    res.send(msg);
});

app.listen(port, (err)=>{
    if(err) {
        console.log(`error happened during app#listen`);
        throw err;
    }
    console.log(`server listened by port ${port} ...`);
});

リリース

6/12夜間にリリースし、6/13 0:50JSTの回が成功したのを確認して一旦動作確認終了。 以後通常運用している。 Twitterの気まぐれで●される可能性もあると思われるので、まあこれもいつまで生きてられるか…って感じではあるが。 しばらく様子見。

余談

  • 正直言って、Twitter APIが色々と終わった方向に進んでいるのを見て、この件では最初、個人ツールの改造をしようという気力はもう湧かなくなっており、「きっぱりあきらめよう」という気持ちの方が最初は強かった。 今回個人ツールの改修に舵を切ったのは、技術的な好奇心が半分と、もう半分は惰性である。 このやり方も、色々調べた限りでは多分OKのはずなんだが、昨今色々と目まぐるしく変わっていっている事情を見るに、知らない部分でポリシー違反してる可能性もなくはないし、仮に今は違反してなくても突然の方針変更でまたすぐにSUSPENDされる可能性もある。 そういう意味では、「個人ツールをとりあえず動かない状態から復旧した」というだけの暫定対処に過ぎないという認識でおり、いつまた即死するかはわからないという危険な状態は続いていると思っている。 そういう状態に振り回されるに正直疲れたというか、面倒くさくなったというか、 自分の手が及ばないところで自分の趣味の生殺与奪が握られている現状が、他と違って現実的に目の見える範囲に迫った脅威として存在していることが認知できており、それに居心地の悪さを感じている。 (これにあまり体力をかけても無駄になるんじゃないかというのが薄っすら目に見えているのを考えると馬鹿らしくなる) 次にSUSPENDされたら、それが自分の手の及ぶ範囲ではないと認識した時点で、この個人ツールの開発運用からは撤退しようと思う。
  • そもそもの話で言うと、最初の思想からして、もともと個人ツールなんか開発するつもりはなかったのだ。 「TwitterInstagramの投稿を載せたい」という単純な思いを実現できれば、ありものでもなんでも良かったし、実際最初はありものを使ってみたのだ。 ただIFTTTはキャプション長すぎると即死するし、Instagramのアプリ連携はpermlinkとOGP画像しかまわってこないし(そして現時点でこのアプリ連携機能自体何故かもう消滅している)、 どうも自分が求めるようなものがないなと思って、最終的にツールの開発にたどり着いたのだ。 結果的に開発は面白かったからいいんだけど、それはもうちょっと自由な開発の体験が提供されていたからで、今はもうそれがないのだから、 「ツールを開発して思いを実現する」という考え方自体が夢想と化しているのだ。 これは悲しいことだが、現実的に今はそうなんだから仕方ない。
  • 基本的に1日1件、多い時でも3件くらいしか使う予定がないので、今の使い方だとありえないが、仮に毎日3件投稿したとしても、「月に1500件のwriteのみが許可」というFreeの条件を超えることはない想定である。 まぁ個人利用の範疇でしかないのでそんなもんだ。 逆に言えばここに払拭しない範囲では自由にさせてほしいと願うばかりである。
  • Instagramの投稿をTwitterに自動連携するアプリ連携の機能って、記憶にある限り3~4年くらい前まではあったはずんだけど、今は探してもFacebookしか見つからなく、設定とかすれば追加できそうな余地も、調べた限りではなさそうだった。 Twitterの広告ポリシーの変更騒ぎがあったときにTwitterが連携先アプリから外れたのか? 現時点の決定仕様なのかどうかは知らないが、ちょっと調べた感じでは、これは(今は)出来ないという結論に個人的に至った。 これが出来ればこれが一番わかりやすくて良かったんだけどね。。
  • Twitterの広告ポリシーに、「他SNSのリンク貼るのはNG」とかいう、馬鹿げた話が出たことがあったが、あれが今どうなってんのかわからない。 今はポリシーから削除されたらしく貼ってもいいってことになってそうだが(実際貼れる)、 明確に「OK」と記載もされてないので、あの騒ぎの後でどういう取り扱いになってるのか謎。 正直どこかの結論に「落ち着いた」とも思っていなくて、また「他SNSへのリンク禁止」とかいきなり言ってくるんじゃないかと不安がある。 そうなったら終わるのだが。。