macOS標準搭載のAppleScriptにおいてもWeb APIの利用はもはや一般的になっています。日常的に「じゃあこの処理はWeb APIでやろう」という話はザラです。
写真.appで選択中の写真をMicrosoft Cognitive APIで画像認識してタグをつけるとか、タグが英語で書かれていて整理しにくいからGoogle APIを呼び出して日本語に翻訳してタグづけしようとか、Finder上で選択しているファイルをDropboxにアップロードして共有リンクを作成してクリップボードに設定するなど枚挙にいとまがありません。
Xcode上でアプリケーションを開発しているときにも、画面上に出す文言をWeb APIで数か国語ぶん翻訳して使うといったこともしています。そのまま使わず、きちんと検証は行っていますが、それでも見たことも聞いたこともない言語への翻訳に、こうした「道具」が使えることの意義は大きいです。
▲Mac App Storeユーティリティ部門で最高33位を獲得した「Uni Detector」もすべてAppleScriptで書いたアプリケーション
話を戻しますが……さまざまなローカルのアプリケーション、ローカルのデータ、Web APIのサービスをまぜて利用する(プライバシーへの配慮を行いつつ)というのが、2021年的なAppleScriptの利用の仕方になっています。
今回、Qiitaエンジニアフェスタのお知らせを見かけ、冒頭のようにREST APIなら割と国内外のさまざまなサービスを呼び出して処理を行っていたので、DirectCloud-BOXという初めてお目にかかるサービスへの扉を開いたのです。
#まずは、ユーザー認証
どのWeb APIでもユーザー認証を行うことが前提です。ユーザー認証せずに使い始められるWeb APIは(ほとんど)ないので、これをクリアできなければ他の機能を試すことはほとんどできませんし、これができたからといって他のAPIの機能を使いこなせるというものでもありません。
ここでつまづくパターンが割と多いので、このあたりは重点的にサンプルコードが掲載されているといいですよね。本命の機能ではPOSTメソッドを使っていても、ユーザー認証機能のあたりはGETメソッドで実装されているWebサービスがほとんどです。
これは、サービスの設計がひとつのチュートリアルになっていて、「まずはご挨拶」ということで技術的な難易度の低いGETメソッドで呼んでみることができて「ああ、このサービスは動いているんだな」「自分の環境から確実に呼べて、応答してもらっているんだな」という信頼関係が、サービス運営側と呼び出し側との間で築かれるように感じます。
アクションゲームの最初のステージが、とても簡単な内容になっていて、その最初のステージをクリアすることでプレイヤーがゲーム全体の仕組みを理解できるようになっている、という設計に近い話だと思います。とくに、このあたりは任天堂さんのゲームにそうした姿勢が見られますよね。
API利用ドキュメントにcurlで呼び出すサンプルが掲載されていたりすると、まずは呼び出して確認ができます。curlコマンド実行サンプルは「命綱」のように感じています。
#ユーザー認証であきらめて「次」へ
肝心のDirectCloud-BOXへのアクセスについては、ドキュメントを見てもさっぱりわかりません。
ユーザーサポートに問い合わせていろいろ聞いてみたものの、問い合わせないとわからない重要情報(なぜかドキュメントに書かれていない、エンドポイントURL)、仕様どおりに実装されていない各種機能(使い始めた時点ではメッセージの言語指定が無視されていた)、後出しジャンケンのように判明する技術仕様のかずかずに目を回してしまいました。
--ご注意:アカウント関連情報を入れてもエラーが返ってきます
use AppleScript version "2.7"
use scripting additions
use framework "Foundation"
property NSString : a reference to current application's NSString
property NSUTF8StringEncoding : a reference to current application's NSUTF8StringEncoding
set reqURLStr to "https://api.directcloud.jp/openapi/jauth/token"
set aPostData to {service:"service_account", service_key:"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", code:"xxxxxxxx", |id|:"xxxxxxxx", |password|:"xxxxxxxxxxx"}
set aUUID to (current application's NSUUID's UUID's UUIDString())
set aBodyDat to recToMultipartFormDat(aPostData, aUUID) of me
set aRes to callRestPOSTAPIAndParseResults(reqURLStr, aBodyDat, aUUID) of me
return aRes
set aRESCode to (responseCode of aRes) as integer
if aRESCode is not equal to 200 then return false
set aRESHeader to responseHeader of aRes
set aRESTres to json of aRes
--POST methodのREST APIを呼ぶ
on callRestPOSTAPIAndParseResults(aURL, aPostData, aUUID)
--set dataJson to convRecToJson(aPostData) of me
set aRequest to current application's NSMutableURLRequest's requestWithURL:(current application's |NSURL|'s URLWithString:aURL)
aRequest's setHTTPMethod:"POST"
--aRequest's setCachePolicy:(current application's NSURLRequestReloadIgnoringLocalCacheData)
aRequest's setHTTPShouldHandleCookies:false
aRequest's setTimeoutInterval:60
set aBoundary to generateBundaryTop(aUUID) of me
set contentType to NSString's stringWithFormat_("multipart/form-data; boundary=%@", aBoundary)
aRequest's setValue:contentType forHTTPHeaderField:"Content-Type"
aRequest's setHTTPBody:aPostData
set aRes to current application's NSURLConnection's sendSynchronousRequest:aRequest returningResponse:(reference) |error|:(missing value)
set resList to aRes as list
set bRes to contents of (first item of resList)
set resStr to current application's NSString's alloc()'s initWithData:bRes encoding:(current application's NSUTF8StringEncoding)
set jsonString to current application's NSString's stringWithString:resStr
set jsonData to jsonString's dataUsingEncoding:(current application's NSUTF8StringEncoding)
set aJsonDict to current application's NSJSONSerialization's JSONObjectWithData:jsonData options:0 |error|:(missing value)
--Get Response Code & Header
set dRes to contents of second item of resList
if dRes is not equal to missing value then
set resCode to (dRes's statusCode()) as number
set resHeaders to (dRes's allHeaderFields()) as record
else
set resCode to 0
set resHeaders to {}
end if
return {json:aJsonDict, responseCode:resCode, responseHeader:resHeaders}
end callRestPOSTAPIAndParseResults
on convRecToJson(aRec)
set aDict to current application's NSDictionary's dictionaryWithDictionary:aRec
set jsonData to current application's NSJSONSerialization's dataWithJSONObject:aDict options:(0 as integer) |error|:(missing value) --0 is NSJSONWritingPrettyPrinted
set resString to current application's NSString's alloc()'s initWithData:jsonData encoding:(current application's NSUTF8StringEncoding)
return resString
end convRecToJson
--multipart/form-data データを作成する
on recToMultipartFormDat(aPostData, aUUID)
set crlf to NSString's stringWithString:(string id 13 & string id 10)
set crlfcrlf to NSString's stringWithString:(string id 13 & string id 10 & string id 13 & string id 10)
set bRes to generateBundaryTop(aUUID) of me
set bbRes to generateBundary(aUUID) of me
set aDict to current application's NSDictionary's dictionaryWithDictionary:aPostData
set kList to aDict's allKeys() as list
set aBody to current application's NSMutableData's |data|()
--set tmpFormStr01 to NSString's stringWithFormat_("Content-Type: multipart/form-data;boundary=\"%@\"%@", bRes, crlfcrlf)
--(aBody's appendData:(tmpFormStr01's dataUsingEncoding:NSUTF8StringEncoding))
repeat with i in kList
set aKey to (contents of i)
set aDat to (NSString's stringWithString:(aDict's valueForKey:aKey))
--Boundary From
(aBody's appendData:(bbRes's dataUsingEncoding:NSUTF8StringEncoding))
(aBody's appendData:(crlf's dataUsingEncoding:NSUTF8StringEncoding))
--Form Name field
set tmpFormStr1 to NSString's stringWithFormat_("Content-Disposition: form-data; name=\"%@\"%@", aKey, crlfcrlf)
(aBody's appendData:(tmpFormStr1's dataUsingEncoding:NSUTF8StringEncoding))
--Form Value field
(aBody's appendData:(aDat's dataUsingEncoding:NSUTF8StringEncoding))
(aBody's appendData:(crlf's dataUsingEncoding:NSUTF8StringEncoding))
end repeat
--Boundary To
set tmpFormStr2 to NSString's stringWithFormat_("%@--%@", bbRes, crlfcrlf)
(aBody's appendData:(tmpFormStr2's dataUsingEncoding:NSUTF8StringEncoding))
return aBody
end recToMultipartFormDat
--macOS 11.0〜11.4ではmacOS側のバグのために実行エラーになるので注意!!
on generateBundaryTop(aKey) --最初の部分のためだけに作成
return NSString's stringWithFormat_("Boundary-%@", (aKey))
end generateBundaryTop
on generateBundary(aKey)
return NSString's stringWithFormat_("--Boundary-%@", (aKey))
end generateBundary
結果として、サポートの方にはいろいろお返事をいただいたものの、自分はあきらめてしまいました。
せっかくなので、実際に試そうと思っていた各種活用方法についてまとめることといたします。
#DirectCloud-BOX APIの特徴
ユーザー管理とグループ管理、このあたりの機能がAPIで実装されていることが特徴と思われました。
あとは、DirectCloud-BOX API呼び出し時のユーザー認証で「入力された情報が正しくありません。」エラーにハネられまくっていた間に感じたことですが、応答速度がかなり高速。
また、無料コースで試せる容量が5GB。ストレージとして活用しまくるにはすこーし心もとない容量です。
#ストレージではなく、ファイルベースのメッセージ伝達システムとして活用
大きなファイルを置くのは(無料コースでは)無理なので、小さなファイルを頻繁に書き込みするような使い方が合っているのだろうと想像しました(実際にそこまで行けていないのですが)。
そこで、複数のクライアント間でクラウドを通じて連携動作を行うための、伝送経路として使えると役立ちそうだと目星をつけました(実際にやってみないと確証は持てないのですが)。
さまざまな情報を示す極小サイズのファイルを書き込み、そのファイルを介して複数クライアント間でRemote Procedure Callをやってみようというわけです。
#試作プログラム案1 ローカルじゃんけんプログラム
このファイルベースのメッセージ伝達システムの有効性を検証するために試作を予定していたのが「じゃんけんプログラム」です。まずは、このレベルの簡単なものを作って確認が必要でしょう。
まず最初に、同じLAN上にいるマシン同士でじゃんけんを行えるプログラムを組みます。このさいにも、LAN上にあるファイルサーバーに対して「どの手を出したか」「どのタイミングで出したか(タイムスタンプ)」などの情報をペアにして(たぶんJSONファイル)特定のフォルダに送ります(書き込みます)。
一定時間ごとに(0.5秒ぐらい?)ファイルサーバー上の特定のフォルダの内容を走査し、相手が何か新しい「手」を出していないかを確認します。
#試作プログラム案2 リモート多人数じゃんけんプログラム
ローカルじゃんけんプログラムで問題点のあぶり出しができたら、DirectCloud-BOXを利用した「リモートじゃんけん」に移行するつもりでした。おそらく、処理速度の問題や、「ファイルを書き込んだのに(すぐには)検出できない」といった問題を解決する必要があったことでしょう。
この「リモートじゃんけん」プログラムの段階で、DirectCloud-BOXの機能上の問題が出てくることがわかっていました。ファイルベースでメッセージを書き込むため、「古くなったメッセージファイル」はいい感じのタイミングで削除しなくてはなりません。掃除を行う必要があるわけです。この「掃除」をどのタイミングで行うべきなのか、逆に履歴として残しておくべきなのか。そうした検討が行えるものと期待していました。
じゃんけんの「試合」をマッチングした段階で新規フォルダを作成し、そのフォルダへのアクセス権限を参加クライアントに付与。じゃんけんに参加するのは2台のクライアントとは限りません。設定上限まで複数のクライアントが参加できるように試すべきでしょう。
じゃんけんの「試合」が終わったら試合に使ったフォルダごとまるごと削除することで「お掃除」ができるのではないか、という見当をつけていました。
ここまでひととおり作る途中で、おそらく今回のコンテストの応募時間は終わっていたことでしょう。実際には、ユーザー認証のAPI呼び出しの段階で時間が終わってしまったので、「次」に行けなかったのは本当に残念です。
#試作プログラム案3 アイデア立案プログラムの複数メンバーによる同時実行
おそらく時間的に「無理」ということになって、ここまで作れないことは最初からわかっていました。でも、最終的に作りたいものや目標もなく、その前段階の試作を行うことは無理です。
そのため、試作プログラム案1&2もこの試作プログラム案3を作るための前哨戦として計画しました。
自分がAppleScriptで開発してMac App Storeで販売しているアイデアプロセッサー「Kamenoko」(→ Mac App Store)。
画面上に6角形のセルを書き込み、色分けしたセルとその中に書き込んだ短文によってアイデアをまとめるアプリケーションです。マウスカーソルを移動させると、画面上に6角形のカーソルが描画されます。
選択したセルの内容をWikipediaのREST APIを呼び出して意味的に分解する機能なども持っています(WikipediaのMedia APIは使いやすくてわかりやすく、とてもいいAPIだと思います)。
▲「クラウドストレージ」の単語をWikipedia REST API呼び出し機能により、意味的に分解したところ
アウトラインプロセッサやマインドマップなどひととおり試した上で、自分に合うようなプログラムを作ったらこのようなものになっていました。Kamenokoは自分で作って毎日使っており、Twitter上で「#Kamenoko」のタグで検索すると、いろいろアイデアやオンラインミーティングの内容をまとめている様子が見られるようになっています。
1人でこの「Kamenoko」を使ってアイデアをまとめるのもよいのですが、この新型コロナウィルス禍の中、遠隔地にいる相手とこのKamenokoを使ってブレインストーミングができるとよさそうだ、というアイデアを温めていました。
自分のカーソルとは別の色で、画面上に相手のカーソルを描画したいですし、自分が書き込んだ内容に対してコメントが書き込まれるといった味付けも期待したいところです。
ただし、遠隔地間をつなぐのに、どのような方法が実際に使えるか手探り状態でした。
オンラインゲームでREST APIを介して対戦情報をやりとりしている、という話を聞きつけてゲーム系のサービスもいろいろ調べてみたものの、「小さく産んで大きく育てる」ようなことができにくい雰囲気でした。自分にとってはおおげさすぎるように感じられたためです(主に費用面とか導入の手間とか)。
そこに、DirectCloud-BOXでファイルベースのメッセージをやりとりするというプランを思いつき、DirectCloud-BOXの機能を用いてグループ管理やユーザー管理を行い、サービスにログインしている相手と一緒にKamenokoを使ってブレーンストーミングを行うのは、かなりの生産性向上に寄与してくれることになるだろう! と期待しています。
単なるビデオ会議とか音声通話では、会議の時間がどんどん伸びてしまう傾向があるように感じていますが、こうしたツールを介してアイデアや意図を共有できると、時間の節約や生産性の向上をより多くの組織で実現できるに違いありません。