0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GASでGoogleCalendarに登録した予定を取得してX APIでツイートする

Last updated at Posted at 2024-08-20

自分用メモ書き。適宜追記していきます。指摘等あればコメントください。

やりたいこと

準備

GASプロジェクトの準備

  1. GASのプロジェクトを作成する
    image.png
  2. プロジェクトの設定を開き、スクリプトIDをメモする
    image.png

X(旧Twitter)開発者アカウントの準備

  1. Developer Platformへアクセス
    FreeのGet Startをクリック
  2. 使用したいTwitterアカウントでログイン
  3. 以下のような画面が出るのでSign up for Free Accountをクリック
    image.png
  4. アプリケーションの名前、説明、ウェブサイトなどを設定し、Submitをクリック
    image.png

ClientID/ClientSecretの取得

  1. Project&Appsから作成したプロジェクトを選択し、画面上部のSettingsをクリック
    User authentication settingsSet upをクリック
    (※添付の画像は設定済みのためEditになっています)
    image.png
  2. 以下のような画面が出るので次のように入力
    • App permissionsをRead and writeに設定
    • Type of AppをWeb App, Automated App or Botに設定
    • App infoのCallback URI/Redirect URLとWebsite URLを下記のように設定
      MY_SCRIPT_IDGASプロジェクトの準備でメモしたスクリプトIDに置き換えてください
      Callback URI/Redirect URL
      https://script.google.com/macros/d/MY_SCRIPT_ID/usercallback
      
      Website URL
      https://script.google.com/macros/d/MY_SCRIPT_ID/
      
      image.png
  3. SaveをクリックするとClient IDとClient Secretが表示されるのでメモしておく
    image.png

GASプロジェクトのスクリプトプロパティの設定

  1. GASのプロジェクトの設定を開き、スクリプトプロパティを編集スクリプトプロパティを追加をクリック
  2. プロパティにMY_CLIENT_IDを追加し、値にClientID/ClientSecretの取得で取得したX(旧Twitter)APIのClient IDをペーストする
  3. プロパティにMY_CLIENT_SECRETを追加し、値にClientID/ClientSecretの取得で取得したX(旧Twitter)APIのClient Secretをペーストする
  4. スクリプトプロパティを保存をクリックして保存する
    image.png

カレンダーIDの取得

こちらの記事を参考に使用したいカレンダーのカレンダーIDを取得する

設定ファイルの作成

基本変更することがない値を別ファイルに分けて定義します。
スクリプトプロパティに保存するでも良いかも。スプレッドシートでも良いかも。
どこに保存するのが良いかは要検討。
文言はスプレッドシート、URL等はスクリプトプロパティなのかなぁ…。
定型文はお好きな文言に変更してください。
MY_CALENDAR_IDカレンダーIDの取得で取得したIDに置き換えてください。

Const.gs
/**
 * 定型文
 * 改行を入れたい場合は\nと入力してください
 */
const remindTommorowEvent = "明日の予定\n"
const startEventAnnounce = "まもなく開始\n"

/**
 * カレンダーID
 * eventCalendarID:カレンダー登録時と前日と当日開始時刻にツイート。
 */
const eventCalendarID = "MY_CALENDAR_ID"

// 以下は変更不要 ////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * OAuth2.0用ライブラリgithub
 * https://github.com/googleworkspace/apps-script-oauth2
 * サンプル
 * https://github.com/googleworkspace/apps-script-oauth2/blob/main/samples/Twitter.gs
 */
const authorizationBaseUrl = 'https://twitter.com/i/oauth2/authorize';
const tokenUrl = 'https://api.twitter.com/2/oauth2/token';

/**
 * ツイート投稿用エンドポイント
 */
const postUrl = "https://api.twitter.com/2/tweets";

/**
 * 日時計算用
 */
const one_minute = 60 * 1000
const one_hour = 60 * one_minute
const one_day = 24 * one_hour

認証処理の実装

使用ライブラリ

OAuth2.0で認証を行う。
apps-script-oauth2を使用する。

手順

  1. ライブラリを導入する
    GASの画面でエディタライブラリライブラリを追加をクリック
    スクリプトIDに以下を入力して検索をクリック
    スクリプトID
    1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF
    
    最新のバージョンを選択して追加をクリック
    image.png
  2. 使用ライブラリに記載のリンクのGitHubのサンプルを参考に以下のコードを作成
    Auth.gs
    /**
     * アクセスが許可されている場合はアクセストークンを返却
     * アクセスが未許可の場合は認証用URLを表示
     */
    const getXAPIToken = () => {
      const service = getOAuthService();
      if (!service.hasAccess()) {
        const authorizationUrl = service.getAuthorizationUrl();
        Logger.log('次のURLを開き、再度スクリプトを実行してください: %s', authorizationUrl);
      } else {
        const accessToken = service.getAccessToken();
        return accessToken;
      }
    }
    /**
     * OAuth2Serviceオブジェクトの作成
     */
    const getOAuthService = () => {
      const serviceName = "xapi";
      const properties = PropertiesService.getScriptProperties();
      const clientId = properties.getProperty("MY_CLIENT_ID");
      const clientSecret = properties.getProperty("MY_CLIENT_SECRET");
      const scope = "tweet.read tweet.write users.read offline.access"
      return OAuth2.createService(serviceName)
        .setAuthorizationBaseUrl(authorizationBaseUrl)
        .setTokenUrl(tokenUrl)
        .setClientId(clientId)
        .setClientSecret(clientSecret)
        .setCallbackFunction('authCallback')
        .setPropertyStore(PropertiesService.getUserProperties())
        .setScope(scope)
        .generateCodeVerifier()
        .setTokenHeaders({
          'Authorization': 'Basic ' + Utilities.base64Encode(clientId + ':' + clientSecret),
          'Content-Type': 'application/x-www-form-urlencoded'
        })
    }
    
    /** 
     * 認証終了時に呼び出される関数
    */
    const authCallback = (request) => {
      var service = getOAuthService();
      var authorized = service.handleCallback(request);
      if (authorized) {
        return HtmlService.createHtmlOutput('認証が完了しました。このウィンドウを閉じてください。');
      } else {
        return HtmlService.createHtmlOutput('認証に失敗しました。');
      }
    }
    
    ここでscopeにoffline.accessを入れ忘れてリフレッシュトークンが発行されず、アクセストークンが期限切れになっては認証用URLにアクセスして許可して…を繰り返していました。
    なんで再取得してくれないの~?!と思ったら抜けてた。(アホすぎる)
    同じ悩みを持つ人に届け…。
  3. getXAPITokenを実行すると「次のURLを開き、再度スクリプトを実行してください:…」と出るので、表示されたURLにアクセス
  4. 認証を求められるのでAuthorize.appをクリック
    image.png
  5. 認証が完了しました。このウィンドウを閉じてください。と表示されたらOAuth2を使った認証は完了です◎

ツイート投稿処理の実装

ツイートの文章整形

ツイートする文章からHTMLタグを除去する処理を実装します。

TextUtil.gs
/**
 * 文章からHTMLタグを除去します
 * 
 * @param {string} text - 対象の文章
 * @return {string} - HTMLタグを除去した文章
 */
const removeHTMLTag = (text) => {
  try {
    text = text.replace(/<br>/g, "\n");
    return text.replace(/<("[^"]*"|'[^']*'|[^'">])*>/g, '');
  } catch (e) {
    Logger.log("Error at removeHTMLTag:")
    Logger.log(e)
  }
}

X(旧Twitter)APIでツイートを投稿

ツイート内容を引数に受け取って、認証処理の実装で取得したアクセストークンを使用してツイートを行う処理を実装します。

Tweet.gs
/**
 * postリクエストを行う関数
 * @param {string} url - リクエストを行うURL
 * @param header - リクエストヘッダ
 * @param message - ポストを行うメッセージ
 */
const fetchPost = (url, headers, message) => {
  const options = {
    method: "post",
    headers,
    muteHttpExceptions: true,
    payload: JSON.stringify(message),
    contentType: "application/json"
  };

  try {
    const response = UrlFetchApp.fetch(url, options);
    const result = JSON.parse(response.getContentText());
    Logger.log(JSON.stringify(result, null, 2));
  } catch (e) {
    Logger.log("Error:")
    Logger.log(e)
  }
}

/**
 * ツイートをポストする
 * @param {string} tweetText - ツイート内容
 */
const postTweet = (tweetText) => {
  try {
    const message = {
      text: removeHTMLTag(tweetText)
    }
    const headers = {
      Authorization: 'Bearer ' + getXAPIToken()
    }
    fetchPost(postUrl, headers, message);
  } catch (e) {
    Logger.log("Error at postTweet:")
    Logger.log(e)
  }
}

GoogleカレンダーAPIの有効化

Google Apps ScriptからGoogleカレンダーを読み書きするためにGoogle Calendar APIを有効化します。

  1. サービスを追加をクリック
    image.png
  2. Google Calendar APIを選択し、追加をクリック
    image.png

カレンダー編集時に予定の「説明」をツイートする

予定編集時に実行する関数の用意

カレンダーの変更を取得のあったイベントを取得し、非公開の予定でなければ説明欄に記入されている内容をpostTweetでツイートします

Calendar.gs
/**
 * カレンダー新規登録・編集時に非公開の予定でなければポストする
 * トリガー「カレンダーから」で設定
 */
const onCalendarEventAdded = (e) => {
  try {
    const calendarId = e.calendarId
    Logger.log("onCalendarEventAdded calendarId:%s", calendarId)
    const properties = PropertiesService.getScriptProperties();
    // スクリプトプロパティにSYNC_TOKEN_CALENDAR_ID=XXXXというカレンダーIDに紐づいた名前でnextSyncTokenが保存される
    const syncTokenProperty = "SYNC_TOKEN_CALENDAR_ID=" + calendarId
    const nextSyncToken = properties.getProperty(syncTokenProperty);
    const optionalArgs = (!nextSyncToken) ? {} : { syncToken: nextSyncToken }
    // 変更のあったイベントを取得
    const events = Calendar.Events.list(calendarId, optionalArgs);
    Logger.log("onCalendarEventAdded events.items.length:%d", events.items.length)
    for (event of events.items) {
      Logger.log("onCalendarEventAdded event:%s", JSON.stringify(event, null, 2))
      // カレンダー新規登録/編集時にstatus=confirmedになる
      if (event.status == 'confirmed') {
        // 非公開の予定の場合はツイートしない
        if (event.visibility == "private") continue;
        // カレンダーに登録されている予定の「説明」をツイートする
        if (event.description) postTweet(event.description);
      }
    }
    // スクリプトプロパティのnextSyncTokenを更新する
    properties.setProperty(syncTokenProperty, events["nextSyncToken"]);
  } catch (e) {
    Logger.log("Error at onCalendarEventAdded:")
    Logger.log(e)
  }
}

トリガーの作成

  1. GASでトリガートリガーを作成をクリック

  2. イベントのソースをカレンダーからに設定する

    イベントのソースをカレンダーからに設定すると、指定したカレンダーが編集された際に自動で指定した関数を実行してくれる。

  3. 実行する関数に予定編集時に実行する関数の用意で作成した関数onCalendarEventAddedを指定する。

  4. カレンダーのオーナーのメールアドレスに予定の変更を検知したいカレンダーのカレンダーIDを入力する。
    image.png

毎日決まった時間帯に次の日の予定一覧を取得し、予定の開始時刻にツイートする

トリガーでイベントのソースを時間主導型にすると、日付ベースや時間ベースのトリガーをセットすることができる。

処理の流れ

  1. 日付ベースのトリガーで、毎日0~1時の間に「当日の20:00に指定の関数を実行する」トリガーをセットする
  2. 20:00になったら実行される関数で、翌日のイベントを取得する
  3. 取得したイベントの「説明」をツイートする
  4. 取得したイベントの「開始時刻」を取得し、「開始時刻」に「説明」をツイートするトリガーをセットする

トリガーの削除処理の実装

実行済みのトリガーを削除するため、指定されたトリガーIDのトリガーを削除する関数を用意する

Trigger.gs
const deleteTrigger = (triggerId) => {
  try {
    const triggers = ScriptApp.getProjectTriggers();
    for (trigger of triggers) {
      if (trigger.getUniqueId() === triggerId) {
        ScriptApp.deleteTrigger(trigger);
      }
    }
  } catch (e) {
    Logger.log("Error at deleteTrigger:")
    Logger.log(e)
  }
}

指定された時刻に指定された内容を呟くための実装

以下の2つの関数を実装する

  • postByTrigger
    • セットされた時間になったら実行される
    • スクリプトプロパティにトリガーIDに紐づいて保存されているツイート内容を取得する
    • 取得したツイート内容を投稿する
    • スクリプトプロパティからツイート内容を削除する
    • 実行済みのトリガーを削除する
  • setTriggerPostDesignatedTime
    • 時間とツイート内容を受け取り、指定された時間にpostByTrigger関数を実行するトリガーをセットする
    • セットしたトリガーのIDを取得し、IDに紐づけてスクリプトプロパティにツイート内容を保存する
Tweet.gs
/**
 * セットされたトリガーの時刻になったらトリガーにセットされたツイート内容をポストする関数
 */
const postByTrigger = (e) => {
  try {
    const triggerId = e.triggerUid;
    const property = PropertiesService.getScriptProperties();
    const body = property.getProperty(triggerId);
    postTweet(body);
    // 後処理
    property.deleteProperty(triggerId);
    const triggers = ScriptApp.getProjectTriggers();
    for (let i = 0; i < triggers.length; i++) {
      if (triggers[i].getUniqueId() === triggerId) {
        ScriptApp.deleteTrigger(triggers[i]);
      }
    }
  } catch (e) {
    Logger.log("Error at postByTrigger:")
    Logger.log(e)
  }
}
/**
 * 指定された時刻に指定された内容を呟くためのトリガーをセットする
 */
const setTriggerPostDesignatedTime = (designatedTime, tweetDescription) => {
  try {
    const trigger = ScriptApp.newTrigger('postByTrigger').timeBased().at(designatedTime).create();
    const triggerId = trigger.getUniqueId();
    const properties = PropertiesService.getScriptProperties();
    properties.setProperty(triggerId, tweetDescription);
  } catch (e) {
    Logger.log("Error at setTriggerPostDesignatedTime:")
    Logger.log(e)
  }
}

任意のカレンダーから任意の期間の予定を取得する関数の実装

指定されたカレンダーの指定された区間の予定を取得して返す関数を実装する。

Calendar.gs
/**
 * 任意のカレンダーIDのカレンダーから指定区間の予定を取得して返す
 * @param {string} calendarId - カレンダーID(空文字が渡された場合はデフォルトのカレンダー)
 * @param {date} startTime - 検索する期間の開始日時
 * @param {date} endTime - 検索する期間の終了日時
 * @return {event[]} - イベントのリスト
 */
const getCalendarEvents = (calendarId, startTime, endTime) => {
  try {
    if (!calendarId) {
      calendarId = 'primary';
    }
    const calendar = CalendarApp.getCalendarById(calendarId);
    const events = calendar.getEvents(startTime, endTime);
    return events
  } catch (e) {
    Logger.log("Error at getCalendarEvents:")
    Logger.log(e)
  }
}

翌日の予定の取得&ツイートと予定開始時刻に呟くトリガーをセットする関数の実装

以下のような処理を行う

  1. 実行済みトリガーの削除(本関数がトリガーによって実行されるため)
  2. 任意のカレンダーから任意の期間の予定を取得する関数の実装で作成した関数を用いて次の日の予定を取得
  3. 取得した予定の「説明」に文言を付加してツイート
  4. イベント開始時刻を取得
  5. 取得した予定の「説明」に文言を付加してイベント開始時刻にツイートしたい内容を作成
  6. 指定された時刻に指定された内容を呟くための実装で作成した関数を用いてイベント開始時刻にツイートを行うトリガーをセット
Tweet.gs
/**
 * イベントカレンダーに登録された明日の予定をsendTweetに渡し、予定開始時刻に走るトリガーを作成する
 */
const postTommorowEvent = (e) => {
  try {
    const triggerId = e.triggerUid;
    deleteTrigger(triggerId);
    // 現在の日時を取得
    const now = new Date();
    // 現在の日付を基に翌日の 0:00 を計算
    const nextDayStart = new Date(now);
    nextDayStart.setDate(now.getDate() + 1);
    nextDayStart.setHours(0, 0, 0, 0); // 時間、分、秒、ミリ秒を 0 に設定
    // 翌日の 23:59 を計算
    const nextDayEnd = new Date(nextDayStart);
    nextDayEnd.setHours(23, 59, 59, 999); // 時間、分、秒、ミリ秒を 23:59:59.999 に設定
    const events = getCalendarEvents(eventCalendarID, nextDayStart, nextDayEnd)
    if (!events.length) return
    for (event of events) {
      const description = event.getDescription();
      if (!description) continue;
      const tommorowEventPost = remindTommorowEvent + `\n` + description;
      postTweet(tommorowEventPost);
      // イベント開始時刻に呟くトリガーをセット
      const eventStartTime = event.getStartTime();
      const startEventPost = startEventAnnounce + `\n` + description;
      setTriggerPostDesignatedTime(eventStartTime, startEventPost);
    }
  } catch (e) {
    Logger.log("Error at postTommorowEvent:")
    Logger.log(e)
  }
}

日付ベースのトリガーにより実行される関数の実装

以下の2つの関数を実装する。

  • setTodaysTrigger
    • 日付ベースのトリガーにより、本関数を毎日0~1時の時間帯で実行する。
    • 20:00になったらpostTommorowEventを実行するトリガーをセットする。
  • setTrigger
    • 指定された日時に指定された関数を実行するトリガーを作成する。
Trigger.gs
/**
 * 本日実行するトリガーを作成する
 * この関数を毎日0-1時の間に呼ぶようトリガーの設定画面から設定する
 */
const setTodaysTrigger = () => {
  try {
    setTrigger('postTommorowEvent', 0, 20, 00);
  } catch (e) {
    Logger.log("Error at setTodaysTrigger:")
    Logger.log(e)
  }
}

const setTrigger = (func, day, hour, min) => {
  try {
    //Dateオブジェクトで実行した時間を取得
    const time = new Date();
    time.setHours(hour);
    time.setMinutes(min);
    ScriptApp.newTrigger(func).timeBased().at(time).create();
  } catch (e) {
    Logger.log("Error at setTrigger:")
    Logger.log(e)
  }
}

日付ベースのトリガーをセットする

日付ベースのトリガーにより実行される関数の実装で作成した関数を毎日0~1時の間で実行されるようトリガーを以下のようにセットする

image.png

まとめ

Googleカレンダーに登録された予定から自動でツイートを行えるようにしました。
具体的には行ったことは以下です。

  • カレンダー編集時に予定の「説明」を呟く
  • 毎日20:00に翌日0:00-23:59の予定一覧を取得し、予定の「説明」に文言を付加して呟く
  • 毎日20:00に翌日0:00-23:59の予定一覧を取得し、予定の開始時刻に予定の「説明」に文言を付加して呟くトリガーをセットする

参考にさせていただいたリンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?