Node.js(TypeScript)でmixi APIを使って画像付きでツイートするメモ

## はじめに

ここまで来たら、mixi APIも調べてみようと思いましたが、意外と大変でした。何度も心がくじけそうになりました。

## 手順
### 1.ディベロッパー登録
まず大変だったのがクレジットカードの登録。クレジットカードの登録をしないと先に進めません。バーチャルカードは使用できませんでした。さらに、一日一回しか登録できないので、失敗すると次の日まで待たないといけないため、心がくじけそうになります。

### 2.アプリケーションの登録
アプリケーションを登録する際には、リダイレクトURLも記入してください。特にリダイレクト用のURLを用意しなくても、例えば「`http://www.mztm.jp/redirect`」のようなURLで大丈夫です。

consumer keyとconsumer secretを環境変数にでも入れておきましょう。
``` .env
MIXI_CONSUMER_KEY=aaaaaaaa
MIXI_CONSUMER_SECRET=bbbbbbbbbb
```


3.server stateの取得
ここからは時間との勝負です。

ここからは時間との勝負です。まずはServer Stateを取得しましょう。Server Stateとは...あー、このコードの有効時間は30分です。のんきに調べていたらコードが無効になってしまいますので急ぎましょう。

``` typescript
private getServerState = async (): Promise<any> => {
        const param = new URLSearchParams({
            grant_type: "server_state",
            client_id: process.env.MIXI_CONSUMER_KEY,
        });
        const headers = {
            'Content-Type': 'application/x-www-form-urlencoded'
        };

        try {
            const response = await axios.post("https://secure.mixi-platform.com/2/token", param, {headers})
            console.log(response.data.server_state)
        } catch (e) {
            console.log("error")
        }
    }
```
ここで出力されるのがServer Stateです。仮に`ccccccc`と出力されたとしましょう。

4.OAuth Codeの取得

以下のようにURLを生成して、ブラウザに入力します。

``` typescript
const param = "client_id=" + process.env.MIXI_CONSUMER_KEY +
            "&response_type=code" +
            "&scope=w_voice" +
            "&display=pc" +
            "&server_state=ccccccc"
const url = "https://mixi.jp/connect_authorize.pl?" + param;
console.log(url)
```
許可をすると、上のリダイレクトURLにページ遷移します。
そのURLのアドレスから`code`の変数を取得します。
例：http://www.mztm.jp/redirect?code=ddddddddd
このdddddddddがOAuth Codeです。
OAuth Codeとは・・・
あー！OAuth Codeの有効時間は3分です。
ささ、急ぎましょう。

### 5.Tokenの取得

Server StateとOAuth Codeを使って、Tokenを取得します。


```typescript
public getToken = async (): Promise<any> => {
        const server_state:string = "ccccccc";
        const code:string = "ddddddddd";
        const param = new URLSearchParams({
            grant_type: "authorization_code",
            client_id: process.env.MIXI_CONSUMER_KEY,
            client_secret: process.env.MIXI_CONSUMER_SECRET,
            code: code,
            server_state: server_state
        });

        const headers = {
            'Content-Type': 'application/x-www-form-urlencoded'
        };
        try {
            const response = await axios.post("https://secure.mixi-platform.com/2/token", param, {headers})
            console.log(response.data);
        } catch (e) {
            console.log("error")
        }
    }
```

以上を実行すると、出力されるはずです。

``` console
refresh_token: 'eeeeeeee',
expires_in: 900,
access_token: 'ffffffffffff',
token_type: 'Bearer',
scope: 'w_voice'
```

このaccess_tokenは900秒です。
ただ、今回は900秒を超えてしまっても、refresh_tokenを使って再度access_tokenを取得することができます。
refresh_tokenを環境変数に入れておきます。

``` .env
MIXI_REFRESH_TOKEN=eeeeeeee 
```

### 6.投稿

下準備はおわりました。
このrefresh tokenを使って投稿ごとにtokenを取得して、メッセージを投稿しましょう。
とりあえず画像付きの関数と画像なしの関数を用意しています。
それぞれ、まずはrefresh tokenを使い、access tokenを取得して、POSTにくっつけて送ります。

``` typescript
    private refreshToken = async () => {
        const date = {
            grant_type: "refresh_token",
            client_id: process.env.MIXI_CONSUMER_KEY,
            client_secret: process.env.MIXI_CONSUMER_SECRET,
            refresh_token: process.env.MIXI_REFRESH_TOKEN
        }
        const param = new URLSearchParams(date);
        const headers = {
            'Content-Type': 'application/x-www-form-urlencoded'
        };
        try {
            const response = await axios.post("https://secure.mixi-platform.com/2/token", param, {headers})

            return {
                data: {
                    success: true,
                    access_token: response.data.access_token
                }
            }
        } catch (e) {
            console.log("error " + e)
            return {
                data: {
                    success: false,
                }
            }
        }
    }

    
/**
 * メッセージをMixi APIを使用して送信します
 *
 * @param {string} message - 送信するメッセージ
 * @returns {Promise<any>} - 成功ステータスを含むオブジェクトを返すPromise
 * @throws {Error} - 送信プロセス中に失敗した場合、エラーを投げます
 */
    public send = async (message: string):Promise<any> => {
        const response: any = await this.refreshToken();
        if (response.data.success) {
            const param = new URLSearchParams({
                status: message,
                oauth_token: response.data.access_token
            });
            const headers = {
                'Content-Type': 'application/x-www-form-urlencoded'
            };
            try {
                await axios.post("https://api.mixi-platform.com/2/voice/statuses/update", param, {headers})
                return {
                    data:{
                        success: true
                    }
                }
            } catch (e) {
                console.log("error send!")
                return {
                    data:{
                        success: false,
                        error:{
                            message:e
                        }
                    }
                }
            }
        }
    }

/**
 * 画像をメッセージと一緒にMixiプラットフォームに送信します
 *
 * @param {string} message - 画像と一緒に送信するメッセージ
 * @param {string} imagePath - 画像ファイルへのパス
 * @returns {Promise<any>} - 成功ステータスとオプションのエラーメッセージを含むオブジェクトを返すPromise
 * @throws {Error} - 画像の読み取りまたは送信中にエラーが発生した場合、エラーを投げます
 */
    public sendEmbedImage = async (message: string, imagePath: string): Promise<any> => {
        const response: any = await this.refreshToken();
        if (response.data.success) {
            let photoData: Buffer;
            let contentType: string;
            if (imagePath.endsWith('.jpg') || imagePath.endsWith('.jpeg')) {
                contentType = 'image/jpeg';
            } else if (imagePath.endsWith('.png')) {
                contentType = 'image/png';
            } else {
                console.log("Unsupported image format");
                return;
            }

            try {
                const filePath = path.join(__dirname, imagePath);
                photoData = await fs.readFile(filePath);
                const headers = {
                    'Content-Type': contentType
                };
                const url: string = `https://api.mixi-platform.com/2/voice/statuses/update?status=${encodeURIComponent(message)}&oauth_token=${response.data.access_token}`;
                await axios.post(url, photoData, {headers});
                return {
                    data:{
                        success:true
                    }
                }
            } catch (e) {
                console.log("error " + e);
                return {
                    data:{
                        success: false,
                        error:{
                            message:e
                        }
                    }
                }
            }
        }
    }


```


## まとめ
あー、疲れました。
もう少しきれいな方法があるかもしれませんが、とりあえず送信ができました。

mixiのドキュメントによると、mixi側が勝手にリフレッシュトークンを使えないことがあるかもしれないとのことです。OAuthコードを再度取得して処理を行う必要があるかもしれません。
完全自動化は難しいかもしれませんね。
しかし、3ヶ月ほど動かしてみたところ、とりあえず動いています。ここまで動けば大丈夫かな。


## 参考
mixi api 認証認可手順（新方式）
https://developer.mixi.co.jp/connect/mixi_graph_api/api_auth/

Voice API
https://developer.mixi.co.jp/connect/mixi_graph_api/mixi_io_spec_top/voice-api/