前置き
${\Huge\text iPadください!!!!!!!!!!}$
前置き2
欲望に包まれてこんにちは。この記事は見るからにQiita Engineer Festa 2022のうち1テーマ、Claris Connect を使った SaaS 連携ユースケースを紹介しよう!の賞品であるiPad Pro 256GBに釣られて書かれたものですし、実際そうです。iPadください。
今回何で記事を書くかは色々悩んだんですが、Claris Connectの公式ページによれば
「App が多すぎて、カオス!」な状態はもうおしまい。
だそうなので、逆に言えばなるべくカオスなAppの開発に使うのが効果的かな、という考えに至りました。
そういうわけで今回は、「何もしてないのに機能が増えた」をはじめとする恐怖現象の温床……Discord botの開発に、このツールを使用していこうと思います。
方向性を考えておく
とりあえず、作っていくDiscord botの方向性を決めておきましょう。とはいえClaris Connectはそれ自体がGUIなので、ちょうどフローチャートのように「組みながらどうするか決めていく」ことも可能だと思います。なので今回は、簡単に3つくらいの指針を決めておきます。
実装
1. APIを叩く部分
1-1. 定期的に実行する
まず、巡回のための定期実行にあたる部分を作ります。
この画像における左下のSchedules
を追加して進むと、遷移先の画面で発火頻度を聞かれます。今回はとりあえず2分に1度の頻度で実行するので、Custom (advanced)
を選択します。
そこから進むとCron Expressionで頻度を入力するよう促されるので、「毎時偶数分目」を意味する
0/2 * * * *
を突っ込みます。
タイムゾーンには普通にAsia/Tokyo
を入力して、あとはSave Triggerでトリガーの追加が完了します。
1-2. ストレージを読む
1-2-1. 前置き
とりあえず実行上のデータを保持するためにストレージを作ります2。
連携アプリケーションリストで「Database」「Cloud Storage」あたりのカテゴリを見るとAWS S3だのBoxだのFileMaker Cloudだの色々なサービスがヒットします。Claris Connectを使ってることを考えるとFileMaker Cloudが王道かなと思うんですが、今回はそこまで大規模なデータを扱うわけでもないので、Google Sheetsで運用しようと思います。
1-2-2. ファイルを作っておく
一度Google Driveを開き、NekochanRealSaikyou
などの適当な名前でGoogle Sheetsファイルを作成します。コンテキストメニューから共有用URLを取得すると、
`https://drive.google.com/file/d/${ID}/view?usp=sharing`
みたいな文字列がクリップボードに来るので、このID
にあたる文字列を適当な手段で保管しておきます。
NekochanRealSaikyou
には、とりあえず
-
config
シート:実行に関わる設定を保管するシート -
novels
シート:監視対象の小説の情報を保管するシート
の二つを作成しておきます。
novels
シートについては、何も書いておかないのもわかりにくいので、小説家になろうのトップページにある月間ランキングの上位5作品(執筆当時)のncodeを入力しておきました。
1-2-3. 取得の準備
これは罠なんですが、Claris ConnectのGoogle Sheetsモジュールには、ファイルを編集するコマンドはあっても取得するコマンドはありません。Google Driveモジュールも同様です3。
ではどうやってSheetsのデータを持ってくるかというと、Google Cloud Platform(GCP)のAPIを叩くことになります。
詳しくはこの記事に譲るとして、今回やるべきは
- GCPで適当なプロジェクトを作る
- プロジェクト内でGoogle Sheets APIを有効化する
- APIキーを作成する
-
NekochanRealSaikyou
をリンクを知っている全員を対象として共有する
という工程です。
この状態で、例えば
`https://sheets.googleapis.com/v4/spreadsheets/${ID}/values/novels?key=${KEY}`
にアクセスすれば、novels
シートの内容がjson形式で返ってきます。
1-2-4. 取得
そういうわけで、実際に取得してみましょう。
これも罠なんですが、Claris ConnectのWebhook
モジュールは実はWebhookではありません。なぜそう言えるかというと、クリックした瞬間に
これが出てくるからです。
いや、HTTP GET requestではないだろ!4という感が相当強いですが……まあいいでしょう。ここからGET requestを選択した状態でContinueして、前述のAPIアクセス用URLを入力する作業をconfig
とnovels
で1回ずつ行います。
返ってきたデータをConvert to JSON format
する必要があるかと思ったんですが、どうもデフォルトでjson形式として認識されているようなので割愛します。
1-3. APIを叩く
1-3-1. 理屈
小説家になろうの更新を感知する方法はいろいろあります。具体的に言うとスクレイピング・Atomフィード・なろう小説APIなどですが、今回はそのうちAPIを使用します。
まず、今回取得すべきデータについて考えると
- 事前入力されたncodeを持つ作品が対象(=検索は必要ない)
- 念のため出力上限はできるだけ大きく
- 作品そのものに付随するデータとして必要なのは、
- 最終更新日時(更新されたことを確認するため)
- 話数
- 文字数(前回更新からの差分を表示するため)
- ncode
- タイトル
- 作者名
といったことが言えます。
公式リファレンスをもとにこれを実装することを考えると、
https://api.syosetu.com/novelapi/api/
のクエリを
-
out
:json
-
ncode
:対象となるncodeを-
で連結した文字列 -
of
:gl-ga-l-n-t
-
lim
:500
に設定してGETすればいいでしょう。
1-3-2. 実装
まず、前述の対象となるncodeを-
で連結する工程をやります。
先ほどの仮入力例でいくと、Google Sheets APIのnovels
はこういうjsonを返してきます。
{
"range": "novels!A1:Z1000",
"majorDimension": "ROWS",
"values": [
[
"n8703hn"
],
[
"n5082hp"
],
[
"n6634hp"
],
[
"n4481hp"
],
[
"n8088hp"
]
]
}
基本はvalues
内に入っている二次元リストから、ncode列(0列)に入っている文字列を抽出し、-
で連結すればいいでしょう。
Claris Connectにも多分通常のプログラミング言語で言うところのmap
文がありそうな気がするんですが、今回は見つけられなかったので気合いでループ実装します。
ここからはダイジェストでお送りします。
結合用変数を作る
Claris ConnectのVariable
を追加、名前はlinked_ncodes
にしておきます。
この時どうやら空文字列が使えないっぽいので、先ほどAPIから取得したnovels
シートのうち最初の行のncodeを突っ込んでおきます。
ループを設定する
Claris Connectでは追加メニューからRepeat
を選択することでループを設定することが可能です。
novels
シートのすべての行に対してforEachをかけるイメージですね。
index==0
を弾く
linked_ncodes
の宣言時に既に最初の行のncodeは追加済みなので、index==0
の場合も処理をすると同じncodeが二重登録されてしまいます。それを防ぐため、Claris Connectのif-Then
で条件を付けています。
やっぱりこういうのはあんまりスマートじゃない感じがあるので、もうちょっといい方法がある気がしますね……
linked_ncodes
に追記する
先ほどのif-Then
の中に追記処理を入れます。
外見的には単純明快、linked_ncodes
の中身と-
とRepeatから渡された行内容のncode列を結合した結果を、linked_ncodes
そのものに代入しています。
Claris Connectのこういうところは良いですね、文字列結合が外見的にわかりやすく行えます。
APIを叩く
一度ifからもループからも抜けまして、HTTP GET request
を追加します。
基本的には『理屈』のセクションに書いてある内容をそのまま実装している感じですね。
こういうめんどくさくなりがちなクエリ処理をGUIでできるのは結構楽しいと思います。
1-3-3. 確認
ここまでで理論上は小説家になろうAPIを叩く処理が完成したはずなので、本当にできているか確認してみましょう。
Flows
メニューを開いて今まで編集していたフローの横のラジオボタンをオン、フローに飛んで右上のRun Now
を押します。
するとHistory
に飛ばされますので、実行を待った後履歴の個別リンクを押します。8番のHTTP GET Request
命令のレスポンスを確認すると……
うまいことAPIが叩けているようです!良さそうですね。
1-4. 更新確認
1-4-1. データ構造
とりあえずGoogle Sheetsに保管するデータについて、もう少し厳密に構造を考えます。
まあそうはいっても、基本的に更新確認は比較によって行われるので、APIを叩いた結果出てきた諸々をそのまま横に並べればいいでしょう。
この形状を一行として、作品数だけ下に伸ばしていく感じで行きましょう。
["ncode","title","general_lastup","general_all_no","length"]
1-4-2. 実装
問題
基本的に先ほどAPIを叩いて出てきたデータをRepeat
で回すだけ……ではありません。
あのですね……これは良くないところだと思うんですけど、Claris Connectは持ってきたjsonデータの加工にあんまり向いていないように見えます。FileMakerとの併用が前提だからだとは思うんですが、しかしもう少し何とかしてほしいところがあります。
何が問題かというと、返ってくるjson内の最初の要素です。
[
{"allcount":887041},
{
"title":...,
"ncode":...
},
...
]
このallcount
を持っているオブジェクトを、Claris Connectは配列における典型と認識します。つまり、この状態でRepeat
を設定しようとすると……
ご覧の通り、item
のキーには title
もncode
もありません。
典型というのはこういうことです。Repeat
は回される要素の持つキーについて、『最初の要素』を参照して推測する……そのため、最初の一件だけallcount
以外のキーを持たない今回の場合相性が悪いのです。
これをどうにか解決しましょう。
Remove item from list
について
この課題の解決にあたり最初に思いつくのは、Lists
モジュールのRemove item from list
コマンドを使うことです。
このコマンドは「任意のリストの任意番目のアイテムを削除してくれる」という非常にゴキゲンな存在です。これを使って取得したデータのうち0番目のみを削除すれば、特殊なキー内容を持つ要素が消えて良さそうです。実際にやってみましょう!
使えません。
はい。
細かい理屈はわからないんですが、たぶんリスト内に存在する連想配列をこのコマンドでは解釈できないため、文字列化して扱ってしまった……みたいな感じだと思います。さすがにこれはもうちょっと何とかならなかったのか、という思いは割とありますね……。
ではどうするのか
こうします。
ご覧の通り、Documents
モジュールのコマンド二つを使って、jsonを一度csvに変換し、そのcsvをさらにリストに変換する処理をしています。
9 Convert JSON to CSV
のオプションは、
-
JSON
: step8のdata
-
delimiter
:,
-
Column title
:false
-
Flatten
:false
10 Convert text to list
のオプションは、
-
JSON
: step9のcsv
-
Separator
:\n
をそれぞれ指定しています。
この処理を挟むことで、最終的に
これが
[
{
"allcount": 5
},{
"title": "真の聖女らしい義妹をいじめたという罪で婚約破棄されて辺境の地に追放された騎士好き聖女は、憧れだった騎士団の寮で働けて今日も幸せ。【書籍化・コミカライズ決定】",
"ncode": "N6634HP",
"general_lastup": "2022-06-08 17:04:30",
"general_all_no": 48,
"length": 100518
},{
"title": "婚約者が浮気相手と駆け落ちしました。色々とありましたが幸せなので、今さら戻りたいと言われても困ります。【外伝更新中】",
"ncode": "N5082HP",
"general_lastup": "2022-06-07 20:00:00",
"general_all_no": 45,
"length": 125187
},{
"title": "天才魔術師を弟に持つと人生はこうなる",
"ncode": "N8703HN",
"general_lastup": "2022-06-06 11:00:00",
"general_all_no": 57,
"length": 136290
},{
"title": "「お前が代わりに死ね」と言われた私。妹の身代わりに冷酷な辺境伯のもとへ嫁ぎ、幸せを手に入れる",
"ncode": "N4481HP",
"general_lastup": "2022-05-08 13:51:59",
"general_all_no": 16,
"length": 61510
},{
"title": "前世魔術師団長だった私、「貴女を愛することはない」と言った夫が、かつての部下",
"ncode": "N8088HP",
"general_lastup": "2022-05-06 21:25:01",
"general_all_no": 1,
"length": 6319
}
]
こうなります。
[
"5,,,,,",
",'真の聖女らしい義妹をいじめたという罪で婚約破棄されて辺境の地に追放された騎士好き聖女は、憧れだった騎士団の寮で働けて今日も幸せ。【書籍化・コミカライズ決定】','N6634HP','2022-06-08 17:04:30',48,100518",
",'婚約者が浮気相手と駆け落ちしました。色々とありましたが幸せなので、今さら戻りたいと言われても困ります。【外伝更新中】','N5082HP','2022-06-07 20:00:00',45,125187",
",'天才魔術師を弟に持つと人生はこうなる','N8703HN','2022-06-06 11:00:00',57,136290",
",'「お前が代わりに死ね」と言われた私。妹の身代わりに冷酷な辺境伯のもとへ嫁ぎ、幸せを手に入れる','N4481HP','2022-05-08 13:51:59',16,61510",
",'前世魔術師団長だった私、「貴女を愛することはない」と言った夫が、かつての部下','N8088HP','2022-05-06 21:25:01',1,6319"
]
これをRepeatでループして、適宜リスト化していく感じで行きましょう。
ループ基盤
さて、回していきましょう。
まず先ほどncodeを結合した時と同じようにRepeat
を追加、index==0
をIf
で弾きます。
表の整形
先ほど、Google Sheetsのnovels
が持つデータはこういう内容だと書きました。
["ncode","title","general_lastup","general_all_no","length"]
ですが、現時点で存在するデータはこういう並びです。
["空要素","title","ncode","general_lastup","general_all_no","length"]
下を上に整形するために手っ取り早い工程は、
- 0番目の要素(空要素)を2番目の要素(
ncode
)で置換する - 2番目の要素を削除する
という流れでしょう。
それをやっているのがこの部分です。
13番でcsv状態のテキストをリストに変換し、14番と15番で前述の工程をやっています。
ここまでの処理を走らせてみたところ、History
のログでは
こういうオブジェクトが帰ってきているようなので、たぶん大丈夫だと思います。
対応するncodeのローをテーブルから探す
忘れがちなんですが、Google Sheets APIから返ってきたのはあくまでも二次元配列にすぎません。つまり、連想配列のようにキーを渡せばバリューが帰ってくるわけではなく……特定のncodeを持つ行を取得するためにはループを回すしかない、ということになります5。
ということでstep3で取得したデータのうちvalues
をRepeat
し、If
でEqual to Case-Insensitive
(大文字と小文字を区別しないで比較)を使います。
これにより、step11のループとstep16のループで回されているncode
が一致した場合のみの処理を行うようにできます。
更新されているかどうかを確認する
ついにこのフェーズですよ!
まず、flag
という名前でvariable
を作っておきます。判定処理を経て、これが-1
なら更新されていない、1
なら更新されている……という寸法でいきます。
更新確認は、
- 最終更新時刻
general_lastup
が前回の取得から変化している - そもそも
general_lastup
が設定されていない
のうちどちらかの条件を満たすことで行われます。
まず、後者から実装していきましょう。
いちいちスクショを貼るのも面倒なので、3つの処理をまとめて解説します。ここでは、
-
19:
novels
シートのうち処理中の行の長さを取得 -
20: 取得した長さが3未満、つまり
general_lastup
の列を含まないなら、条件分岐 -
21: フラグを
1
に設定
という処理をしています。
-
22:
novels
シートのうち処理中の行のgeneral_lastup
列に位置する要素を取得 - 23: 取得した要素がAPIから得られる内容と違うなら、条件分岐
-
21: フラグを
1
に設定
という処理です。
1-5. 更新時処理
1-5-1. 実装
シートを書き換える処理
いよいよ、更新された場合の処理です!
まずflag
が立てられているかで条件分岐します。
分岐内でA,B,C,D,E
を内容としたリストを作ります。
これはClaris ConnectのGoogle Sheetsモジュール用のリストです。作っている途中に気づいたんですが、どうもUpdate spreadsheet row
は行番号を変数で指定できないという謎仕様があるみたいなんです!
Update cell
では普通に変数指定が可能なのでこちらを使うことになりますが、それにあたりA1
とかC5
みたいな感じの指定方法をとる必要があって、それに使うためのアルファベットリストなわけですね。
ここからはダイジェストです。
-
27: API側の処理中の行が持つ
ncode,title,general_lastup,general_all_no,length
の5項目をRepeat
-
28: 先ほどの
A,B,C,D,E
のリストから、step27で処理中の項目に対応するものを抜き出す -
29: Google Sheetsの行数は
1
から始まるので、行数カウンタに1
を足しておく - 30: step28とstep29を組み合わせたセルに書き込む
これで、いよいよDiscordに投稿する部分だけです!
1-5-2. 残念なお知らせ
APIのリクエスト制限に達しました。
はい。
………………まあ、テストランができなくなっただけだと考えましょう。
1-5-3. Discord側のWebhook設定
まあ、気を取り直して……取り直せるかこれ?……取り直して、やっていきましょう。
Discordの適当な自分に管理権限があるサーバーで、適当にチャンネルを作ります。
いかにも精神状態が反映されたチャンネル名ですね。
ここからチャンネル設定を開いて、『連携サービス』を押し……
『ウェブフックを作成』を押すとWebhookが作れます。
『ウェブフックURLをコピー』を押し、出てきたやつをNekochanRealSaikyou
のconfig
シートの1行目にでも置いておきます6。
1-5-4. 投稿用の準備
いろいろ準備をします。
文字数差分
「何文字増えました」みたいなのを知らせる機能が欲しいので、増えた文字数を取得します。
-
31:
length
列を取得- どうも前述の「典型」現象が発生した状態でAPIが使えなくなったせいで、アイテムからインデックス指定で取り出すにはいちいちこれをやる必要があるっぽいです。なんてこった!
-
32: APIで取得した
length
と比較する
日付のフォーマット
最終更新日時をフォーマットします。
Add date
とはいうものの、内部では1秒も足していません。Format date
がなぜか失敗することに対する苦肉の策です。
添えるjson
{"content":"小説が更新されました!","embeds":[{"color":2340239,"timestamp":"{{kmmX.add_date.date}}","title":"{{OuoH.list.list.[1]}}","url":"https://ncode.syosetu.com/{{OuoH.list.list.[0]}}/{{OuoH.list.list.[3]}}/","fields":[{"name":"話数","value":"第{{OuoH.list.list.[3]}}話","inline":true},{"name":"文字数","value":"{{OuoH.list.list.[4]}}文字(+{{VSIh.subtract.subtract}})","inline":true}],"author":{"name":"{{OuoH.list.list.[1]}}","url":"https://ncode.syosetu.com/{{OuoH.list.list.[0]}}/"},"image":{"url":"https://sbo.syosetu.com/{{OuoH.list.list.[0]}}/twitter.png"}}]}
詳細は省く……というか説明しようがありません。この二重波括弧はClaris Connectの変数なんかを使用した場合に出てくるやつのようで、これらを踏まえた解説は極めて難しいです。多分出力された結果を見たほうが分かりやすいと思うし、説明はしません。
Discordのembedsを活用しているため結構長いです。
Webhookにリクエストを投げる
ついにこの時です。
APIが制限されたとはいえ、どうもTest Actionについては普段通り使えるようです。なのでそれを踏まえ、生成したjsonをconfig
から読んだURLにPOSTリクエストで投げてみます。
これをテストとして実行すると……
(知らない小説ですが)更新通知が届きました!!!!!!!
本当にありがとうございました。
今回のフローの最終的な全体像
感想
記事自体の感想
- API利用は計画的に!
- 本来はもうちょっとカオスにしたかったんですが、API枯渇が先でしたね……
Claris Connectの感想
- ところどころ罠が目立つが、それを熟知したうえで使えば楽しいツールかもしれない
- 罠の例:Webhookモジュール内にGETリクエストが置かれている
- 変数の中身が空だと宣言できないのってどうなんだろう
- データのフォーマッティングが雑にできるのは非常によかった
感想2
${\Huge\text iPadください!!!!!!!!!!}$
参考
- https://www.claris.com/ja/blog/2021/claris-connect-google-form
- https://taiki-t.hatenablog.com/entry/2016/10/14/031124
- https://qiita.com/tyuma/items/46cbb9d4e3d078e661dd
- https://qiita.com/Eai/items/1165d08dce9f183eac74
脚注
-
小説家になろうの作品におけるユニークIDみたいなもの ↩
-
本来であればこの『ストレージを読む』セクションはもっと前に配置したかったんですが、Claris Connectではトリガーが必須となるため定期実行の後に置かざるを得ませんでした ↩
-
Download Google Doc, Slide, or Sheet
という名前のコマンドはあるんですが、URLしか返ってきてないように見える ↩ -
一般的にWebhookはPOSTリクエストを利用するものなので ↩
-
長期的設計ではマネしちゃだめだぞ!Claris Connectの連想配列周りが面倒というのはわかるけどね! ↩
-
今回使っているサーバーはWebhookを盗まれたところで特に実害がないような場所だからやっていますが、セキュリティ的には相当カスです。場合によりもっと堅牢にするのがいいと思います。 ↩