⛳
GASでTwitterAPIを使ったら快適だった件
APIが有料化する前はTweepyを使いAPIを使ってあれこれをしていたのですが
APIが有料化(しかも1万5000円と高い)になってからはツイートのみしかできない
そのためあまり使っていなかったのですが、ツイートだけでも価値あるかな?と思い
もう一度チャレンジすることにした。しかしタスクスケジューラを使うにはPCを常時起動しなくてはいけないので、GCPでも使うか?と思っていたらGASというものがあることを知った。
もともとExcelと連動させていたのでGASと連携させるのは非常に使い勝手よく使うことができた。
ただAPIが有料化して1年経過したのに未だに不便なこともあった。
画像付きのツイートするには1.1認証で画像をアップロードして
2.0認証でツイートしなければならないという手間のかかるところ
※まだこれ治らないの??イーロンってまじ何しているんですかね?
結構頑張って調べてようやく作成することに成功したぜ
初投稿ということで色々書いてみます。
function main() {
const service = getService();
if (service.hasAccess()) {
Logger.log("Already authorized");
} else {
const authorizationUrl = service.getAuthorizationUrl();
Logger.log('Open the following URL and re-run the script: %s', authorizationUrl);
}
}
function getService() {
pkceChallengeVerifier();
const userProps = PropertiesService.getUserProperties();
const scriptProps = PropertiesService.getScriptProperties();
return OAuth2.createService('twitter')
.setAuthorizationBaseUrl('https://twitter.com/i/oauth2/authorize')
.setTokenUrl('https://api.twitter.com/2/oauth2/token?code_verifier=' + userProps.getProperty("code_verifier"))
.setClientId(CLIENT_ID)
.setClientSecret(CLIENT_SECRET)
.setCallbackFunction('authCallback')
.setPropertyStore(userProps)
.setScope('users.read tweet.read tweet.write offline.access')
.setParam('response_type', 'code')
.setParam('code_challenge_method', 'S256')
.setParam('code_challenge', userProps.getProperty("code_challenge"))
.setTokenHeaders({
'Authorization': 'Basic ' + Utilities.base64Encode(CLIENT_ID + ':' + CLIENT_SECRET),
'Content-Type': 'application/x-www-form-urlencoded'
})
}
function authCallback(request) {
const service = getService();
const authorized = service.handleCallback(request);
if (authorized) {
return HtmlService.createHtmlOutput('Success!');
} else {
return HtmlService.createHtmlOutput('Denied.');
}
}
function pkceChallengeVerifier() {
var userProps = PropertiesService.getUserProperties();
if (!userProps.getProperty("code_verifier")) {
var verifier = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
for (var i = 0; i < 128; i++) {
verifier += possible.charAt(Math.floor(Math.random() * possible.length));
}
var sha256Hash = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, verifier)
var challenge = Utilities.base64Encode(sha256Hash)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '')
userProps.setProperty("code_verifier", verifier)
userProps.setProperty("code_challenge", challenge)
}
}
function logRedirectUri() {
var service = getService();
Logger.log(service.getRedirectUri());
}
// OAuth1認証
function getServiceOld() {
return OAuth1.createService( "Twitter" )
.setAccessTokenUrl( "https://api.twitter.com/oauth/access_token" )
.setRequestTokenUrl( "https://api.twitter.com/oauth/request_token" )
.setAuthorizationUrl( "https://api.twitter.com/oauth/authorize" )
.setConsumerKey( CONSUMER_API_KEY )
.setConsumerSecret( CONSUMER_API_SECRET )
.setAccessToken( ACCESS_TOKEN, ACCESS_TOKEN_SECRET )
.setCallbackFunction('authCallback'); // コールバック関数名
}
// OAuthコールバック
function authCallback(request) {
const service = getTwitterService();
const authorized = service.handleCallback(request);
if (authorized) {
return HtmlService.createHtmlOutput('Success!');
} else {
return HtmlService.createHtmlOutput('Denied.');
}
}
function uploadMediaFromDrive(folderId) {
var folder = DriveApp.getFolderById(folderId);
var files = folder.getFiles();
var fileList = [];
var mediaIds = [];
var service = getServiceOld();
// ファイルをリストに追加
while (files.hasNext()) {
var file = files.next();
fileList.push(file);
}
// ファイル名でソート
fileList.sort(function(a, b) {
return a.getName().localeCompare(b.getName());
});
// ソートされたファイルを処理
for (var i = 0; i < fileList.length; i++) {
var file = fileList[i];
var blob = file.getBlob();
var mimeType = blob.getContentType();
// 画像ファイルの場合
if (mimeType.startsWith("image/")) {
var encodedImage = Utilities.base64Encode(blob.getBytes());
var img_option = {
'method': "POST",
'payload': {
'media_data': encodedImage
}
};
var endPointMedia = 'https://upload.twitter.com/1.1/media/upload.json';
var image_upload = JSON.parse(service.fetch(endPointMedia, img_option));
mediaIds.push(image_upload['media_id_string']);
}
// 画像ファイルでない場合はログに記録して次へ
else {
Logger.log("Not an image file: " + file.getName());
continue; // 次のファイルへ
}
}
console.log(mediaIds);
return mediaIds;
}
// URLからフォルダIDを抽出する関数
function extractFolderIdFromUrl(url) {
var match = url.match(/folders\/([\w-]+)/);
if (match) {
return match[1];
}
return null;
}
こちらが画像をアップする関数
function sendTweet() {
result = getContentText()
tweet = result.tweet
media = result.media
var payload = {
text: tweet
}
if (media != ""){
folderId = extractFolderIdFromUrl(media)
mediaIds = uploadMediaFromDrive(folderId);
if (mediaIds.length > 0) {
payload["media"] = {"media_ids": mediaIds};
}
}
var service = getService();
if (service.hasAccess()) {
var url = `https://api.twitter.com/2/tweets`;
var response = UrlFetchApp.fetch(url, {
method: 'POST',
'contentType': 'application/json',
headers: {
Authorization: 'Bearer ' + service.getAccessToken()
},
muteHttpExceptions: true,
payload: JSON.stringify(payload)
});
var result = JSON.parse(response.getContentText());
Logger.log(JSON.stringify(result, null, 2));
} else {
var authorizationUrl = service.getAuthorizationUrl();
Logger.log('Open the following URL and re-run the script: %s',authorizationUrl);
}
}
これがツイートする関数です。
getContentTextはスプシの情報を取得する関数です。
あとはGASのスケジューラを設定すると毎日スプシのツイートが画像付きでツイートされます。
これで完全自動化ツイート完成ですね。
Discussion