Node.js (Typescript) で Bluesky に画像を自動投稿する Bot を作る (その2)

この記事は、[Node.js (Typescript) で Bluesky に画像を自動投稿する Bot を作る (その1)](https://qiita.com/eXpresser/items/fce2066f442bd37c8a36) の続きになります。
前の記事では、Node.js から Bluesky へ API 経由で投稿する部分まで実装しました。
今回の記事では、 Azure Function のタイマートリガーを使用して定期的に画像を投稿する Bot にしていきたいと思います。

<details><summary>Twitter での完成イメージ</summary>

![Twitterでの完成イメージ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171480/2cdabdbc-2a39-0bd0-8b54-6fa42c87d489.png)

</details>

## 完成像
![完成像](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171480/afc15b57-4c45-cb04-19dc-f4739384a3c8.png)

やりたいことは上記の図のようなイメージです。
1. **Azure Functions のタイマートリガーで定期的に起動する**
1. 自分の写真展示 Web サイトから、今日の写真とタイトルなどの情報をダウンロード
1. Bluesky へ画像のアップロード、投稿

今回の記事は、1 について説明します。
2, 3 については、前の記事 ([Node.js (Typescript) で Bluesky に画像を自動投稿する Bot を作る (その1)](https://qiita.com/eXpresser/items/fce2066f442bd37c8a36)) をご参照ください。

# この記事のターゲット

この記事は以下のお悩みをお持ちの方に、お役立てできると思います。

* Bluesky へ定期的に自動投稿できる Bot を作りたい！

また、今後の記事で、以下の点についても言及する予定です。

* 画像をそのまま投稿するのと、Webサイトカードで投稿するのとでどんな違いがあるか知りたい！

# Azure Functions で自動投稿を行う
## Azure Functions の実行環境を構築する

下記リンクを参考に Azure Functions の実行環境を構築します。

[クイックスタート: Visual Studio Code と TypeScript を使用して Azure に関数を作成する](https://learn.microsoft.com/ja-jp/azure/azure-functions/create-first-function-vs-code-typescript?pivots=nodejs-model-v4)

必要な準備は以下の通りです。
* 有効な Azure サブスクリプションを準備します。 (Azure Function を動かすためのクラウド環境を準備します)
* Node.js 18 以降をインストールします。
* [Visual Studio Code](https://code.visualstudio.com/) をインストールします。
* Visual Studio Code の拡張機能として、 [Azure Functions 拡張機能 v1.10.4](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurefunctions) 以降をインストールします。
* 同様に、[Azurite 拡張機能](https://marketplace.visualstudio.com/items?itemName=azurite.azurite) をインストールします。
* ローカル実行のために、 [Azure Functions Core Tools v4](https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-run-local?tabs=windows%2Cisolated-process%2Cnode-v4%2Cpython-v2%2Chttp-trigger%2Ccontainer-apps&pivots=programming-language-javascript#install-the-azure-functions-core-tools) をインストールします。 

## タイマートリガーによって、画像投稿を自動実行する Function を作成する

ソース全体は以下のリポジトリにコミットしています。
上記環境整備が完了したら、ローカル環境で実行できるようになると思いますので、気軽にお試しください。
[upload-photo-to-bluesky-nodejs | GitHub](https://github.com/eXpresser-UXM/upload-photo-to-bluesky-nodejs/tree/main/02_upload-photo-continuously)

``` Typescript: 
import { app, InvocationContext, Timer } from "@azure/functions";
import postPhotoToBlueskyMain from "../lib/post-photo-to-bluesky";

export async function postPhotoDaily(myTimer: Timer, context: InvocationContext): Promise<void> {
  try {
    // 環境変数から設定情報を取得します。
    const myWebsiteHost = process.env["MyWebsite:Host"];
    const imagePath = process.env["MyWebsite:Image"];
    const photoTitle = process.env["MyWebsite:Title"];
    const bskyService = process.env["Bluesky:Service"];
    const bskyIdentifier = process.env["Bluesky:Identifier"];
    const bskyAppPassword = process.env["Bluesky:AppPassword"];

    if (!myWebsiteHost || !imagePath || !photoTitle || !bskyService || !bskyIdentifier || !bskyAppPassword) {
      throw new Error('Not enough environment variables. "MyWebsite:Host", "MyWebsite:Image", "MyWebsite:Title", "Bluesky:Service", "Bluesky:Identifier", "Bluesky:AppPassword" are required.');
    }

    // 前回記事で紹介した Bluesky 投稿ロジックを実行します。
    await postPhotoToBlueskyMain(myWebsiteHost, imagePath, photoTitle, bskyService, bskyIdentifier, bskyAppPassword);

  } catch (e) {
    console.error(e);
  }
}

// 1日1回、19時に実行
app.timer('postPhotoDaily', {
  // 実行は世界標準時基準 (UTC) で行われます
  schedule: '0 0 10 * * *',
  handler: postPhotoDaily
});

```

コード自体はあまり難しくありません。
`app.timer()` 関数で、どのタイミングに実行するかを [NCrontab 式](https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-bindings-timer?tabs=python-v2%2Cisolated-process%2Cnodejs-v4&pivots=programming-language-javascript#ncrontab-expressions) で定義し、前回紹介したプログラムを実行するだけです。

環境変数から設定情報を取得していますが、ローカル環境であれば、local.settings.json に以下のように記載、クラウド環境であれば、デプロイした関数の「アプリ設定」より設定可能です。

``` json: local.settings.json
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "AzureWebJobsSecretStorageType": "files",
    "AzureWebJobsFeatureFlags": "EnableWorkerIndexing",
    "MyWebsite:Host": "<自分のWebサイト 例: https://example.com>",
    "MyWebsite:Image": "<取得したい画像のパス 例: /photos/001.jpg>",
    "MyWebsite:Title": "<写真のタイトル>",
    "Bluesky:Service": "https://bsky.social",
    "Bluesky:Identifier": "<Bluesky ID 例: myaccount.bsky.social>",
    "Bluesky:AppPassword": "<Bluesky アプリパスワード>"
  }
}
```
ローカルで実行する際は、F5キーを押下して、実行

## クラウド側の環境整備
Azure Portal から関数アプリのリソースを作成します。
Visual Studio Code からリソース作成できるので、以下のページを参考にしてください。
ランタイムスタックは **Node.js 18 LTS** にするのをお忘れなく。
* [Azure へのサインイン](https://learn.microsoft.com/ja-jp/azure/azure-functions/create-first-function-vs-code-typescript?pivots=nodejs-model-v4#sign-in-to-azure)
* [Azure に関数アプリを作成する](https://learn.microsoft.com/ja-jp/azure/azure-functions/create-first-function-vs-code-typescript?pivots=nodejs-model-v4#create-the-function-app-in-azure)

関数アプリのデプロイの際は以下の手順を参考いただければと思います。
* [Azure にプロジェクトをデプロイする](https://learn.microsoft.com/ja-jp/azure/azure-functions/create-first-function-vs-code-typescript?pivots=nodejs-model-v4#deploy-the-project-to-azure)

以下の画像の通り、作成した関数アプリを右クリックして、「Deploy to Function App」を選択するとOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171480/40634eea-2092-2597-209b-3d513739c038.png)

環境変数の設定が必要なため、作成した関数をさらに開き、「Application Settings」を右クリックして、「Upload Local Settings」を選択します。
これで、local.settings.json の設定がクラウドの方にも展開されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171480/80430378-e3d0-883d-8fae-4db61b4d2d66.png)

実行する場合は、「Functions」を開き、「postPhotoImmediately」を右クリックして、「Execute Function Now」を選択します。
リクエストボディを指定されますが、特にボディを拾っているわけではないので、なんでもOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171480/be0db428-1a45-b378-9698-db287a2c6381.png)

実行に成功すると、ご自身の Bluesky アカウントに写真がポストされるはずです。

なお、もう一つの方の「postPhotoDaily」で、毎日19時に定期ポストされると思いますので、しばらく置くか時刻を変更して再デプロイしてみてください。


# おわりに

前回の記事と今回の記事とで、 Azure Functions のタイマートリガーで、Bluesky へ定期的に画像投稿する Bot を作成できました。
あとは、投稿する写真をランダムに変えたり、本文をいじったり、投稿タイミングを調整すれば、良い感じのBotになると思います。

私は以下のように、毎日19時に投稿するように設定しています。ちょくちょく「いいね」もついており、ありがたい限りです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/171480/5527256a-1150-4db3-38e0-d18f237fd2c3.png)

Bot化することで、忙しい時でも、自分のコンテンツを定期的に OUTPUT していくことができると思いますので、是非皆さんもお試しください。

次は、Bluesky の仕様側の話になります。単に写真をアップロードして Post する場合と、 Webサイトカードで投稿する場合とでどのような違いがあるのか、説明したく思います。
最後までお読みいただき、ありがとうございました。

# 宣伝

* 鉄道系のWebサイト・Webアプリを公開する個人サークル「Studio UXM」を運営しています
    * [Studio UXM](https://uxm.jp)
    * Webアプリ: [配線略図エディタ](https://raildiag.uxm.jp)
    * Webアプリ: [路線図エディタ(仮)](https://routemap.uxm.jp)
    * 鉄道写真展示 Web サイト: [Urban eXpress Museum](https://photos.uxm.jp/photos)
* Bluesky のアカウントやってます
    * [Studio UXM 公式](https://bsky.app/profile/studio.uxm.jp)
    * [eXpresser](https://bsky.app/profile/expresser.uxm.jp)
* X のアカウントも是非フォローお願いします
    * [Studio UXM 公式](https://twitter.com/Studio_UXM)
    * [配線略図エディタ公式](https://twitter.com/raildiag)
    * [eXpresser](https://twitter.com/expresser_uxm)