Edited at

Quip API をためしてみる

More than 1 year has passed since last update.


Quip について

https://quip.com/

一言で言えば、"文書管理アプリ"です。

が、他のアプリケーションと異なるポイントとしては、"コミュニケーション"と"文書を一緒に作り上げていく"を同時に実現することを目指したもの...だと思います。

また、以下のような特徴を持っています。


  1. 全てのドキュメントにチャット機能があり、チーム内でコミュニケーションをとりながら文書を作り上げていける

  2. モバイルからでも文書を編集したり、チャットに参加してコミュニケーションとれる(モバイルファースト!)

  3. 文書の特定の箇所でコメントを追記できる(チームメンバへのメンションも可)

  4. 文章の他に、スプレッドシート、チャートをつけられる

さて、そんなQuipですが、今年になってSalesforceが買収したことを発表してます。Dreamforce2016でも大きく取り上げられており、今後Salesforceとの連携が強化されていくことでしょう。

なお、Dreamforce2016におけるQuipに関するセッションは、以下で公開されています。

* https://www.youtube.com/watch?v=DduJWrkCpsY

* 製品デモ -> 19:42〜


Quip API

そして、QuipにもAPIリファレンスが公開されています。

-> https://quip.com/api/reference

Quip API のクライアントとしてPython, node.jsも実装されているようです。

-> https://github.com/quip/quip-api

APIリファレンスは以下の構成になっています。


  • Authentication

  • Threads

  • Messages

  • Folders

  • Users

Quipでは、"Thread"の中にドキュメントとメッセージ(チャットの履歴)をもっていて、これがメインのリソースとなっています。

Quipの"Folder"は、ファイルシステムにおけるディレクトリというよりも、タグ付けの役割を担っているようです。1つのTreadは複数の"Folder"と関連づけることができるようです。


Salesforceから使ってみる

さて、APIが公開されている...ということで、SalesforceからもHttpCalloutを使えば色々と操作できることでしょう、というわけで(わざわざ)試してみました。


認証

認証では "Personal Authentication" と "Domain Authentication" の2種類が用意されています。"Domain Authentication" はエンタープライズ版で使えるそうなので、 "Personal Authnecation" を使います。

"Personal Authencation" では、https://quip.com/api/personal-token

にアクセスして、トークンを取得します。

ただ、curlでアクセスして...というより、ここだけはブラウザでアクセスして、"Get Personal Access Token"というボタンでアクセストークンを発行するようです。

取得したトークンは...一旦どこかに保持しておく必要があるので、例えばカスタム設定とかに置いておきます。

※ここは非常に残念で、今後要改善ですね。。


APIを利用する

ここまでくればHttpCalloutを使ってQuipのAPIを利用する感じになります。

今回は以下の流れに沿ったプログラムを考えてみます。


  1. 取引先を作成したら、その名前でフォルダを作成する

  2. フォルダ作成と同時に、ドキュメントも作成する

※以下は、取引先作成後に、Quipフォルダ/ドキュメント作成のためのバッチをスケジューラーに登録してみる、という動きになります。

別にバッチでなくてもfutureアノテーションのついたメソッドを用意しても良いです。


AccountToQuipTrigger.trigger

trigger AccountToQuipTrigger on Account (after insert) {

List<Id> objIds = new List<Id>(Trigger.newMap.keySet());

BatchCreateQuip batch = new BatchCreateQuip(objIds);
System.scheduleBatch(batch, 'create_quip_docs', 1, 1);

}



BatchCreateQuip.cls

global class BatchCreateQuip implements Database.Batchable<sObject>,Database.AllowsCallouts,Database.Stateful {

private List<Id> objIds;
private String objApi;

global BatchCreateQuip(List<Id> pIds){
objIds = pIds;
objApi = objIds[0].getSObjectType().getDescribe().getName();
}

global Database.QueryLocator start(Database.BatchableContext BC) {
String soql = 'Select Name From ' + objApi + ' Where Id in :objIds Order By Name';
return Database.getQueryLocator(soql);
}

global void execute(Database.BatchableContext BC, List<sObject> scope) {

List<SObject> upds = new List<SObject>();
for(SObject obj : scope){
obj = ServiceCreateQuip.createFolder(obj);
obj = ServiceCreateQuip.createDocument(obj);

upds.add(obj);
}
update upds;

}

global void finish(Database.BatchableContext BC) {
//
}

}



ServiceCreateQuip.cls

global with sharing class ServiceCreateQuip {

global ServiceCreateQuip(){}

/**
*
*/
global static SObject createFolder(SObject obj){
String url = 'https://platform.quip.com/1/folders/new';
String method = 'POST';

Map<String, String> param = new Map<String, String>{
'title' => String.valueOf(obj.get('Name'))
};

String body = convertParamToQueryString(param);

HttpResponse res = sendRequest(method, url, body);

if(res.getStatusCode() != 200){
throw new ServiceCreateQuipException(res.getBody());
}

Map<String, Object> m = (Map<String, Object>)JSON.deserializeUntyped(res.getBody());
Map<String, Object> f = (Map<String, Object>)m.get('folder');

String quipId = String.valueOf(f.get('id'));

obj.put('QuipFolderId__c', quipId);
return obj;
}

/**
*
*/
global static SObject createDocument(SObject obj){
String url = 'https://platform.quip.com/1/threads/new-document';
String method = 'POST';

Map<String, String> param = new Map<String, String>{
'content' => String.format(
'<h1>{0}</h1>',
new List<String>{
String.valueOf(obj.get('Name'))
}
),
'title' => String.valueOf(obj.get('Name'))
};
if(obj.get('QuipFolderId__c') != null){
param.put('member_ids', String.valueOf(obj.get('QuipFolderId__c')));
}

String body = convertParamToQueryString(param);

HttpResponse res = sendRequest(method, url, body);

if(res.getStatusCode() != 200){
throw new ServiceCreateQuipException(res.getBody());
}

Map<String, Object> m = (Map<String, Object>)JSON.deserializeUntyped(res.getBody());
Map<String, Object> f = (Map<String, Object>)m.get('thread');

String quipId = String.valueOf(f.get('id'));

obj.put('QuipThreadId__c', quipId);
return obj;
}

/**
*
*/
private static String convertParamToQueryString(Map<String, String> p){

List<String> wks = new List<String>();
for(String key : p.keySet()){
String wk = String.format(
'{0}={1}',
new List<String>{
key,
EncodingUtil.urlEncode(p.get(key), 'UTF-8')
}
);
wks.add(wk);
}

return String.join(wks, '&');
}

/**
*
*/
global static HttpResponse sendRequest(String method, String url, String body){
HttpRequest req = new HttpRequest();

//
String token = CustomSetting.getQuipToken();

req.setEndpoint(url);
req.setHeader('Authorization', 'Bearer ' + token);
req.setMethod(method);
req.setBody(body);

Http h = new Http();
HttpResponse res = h.send(req);

return res;
}

global class ServiceCreateQuipException extends Exception {}
}



おためし

取引先を作成してみます。

(作成後)

(Quipのリンク起動)

無事にフォルダとドキュメントが作成されました。


その他

12/13-14に開催された"Salesforce World Tour Tokyo 2016"でもQuipのブースはもちろんありました。

そこでSalesforceとのインテグレーションについても少し話を聞くことができました。

※予めお伝えしますと、まだSalesforce傘下に入って間もないこともあり、インテグレーションに関しては発展途上...という話でした。


  • SalesforceのLightningComponentとして組み込む

  • 関連リストとしてQuipへのドキュメントリンク用レコードを保持する


    • 関連リストにボタンを用意して、Quipのドキュメントを作成し、そのリンクをレコードとして保持する



といったことが今後追加されるようです。


おわりに

リアルタイムでコラボレーションしながらドキュメントを作成orメンテしていくという点では、Quipはなかなか使い勝手が良さそうですし、様々なところで応用ができそうかな、と思います。

開発視点からすれば、APIが公開されているので、Salesforceもしくは他のクライアントアプリからQuipを色々と操作することもできるようです。

今後、Salesforceとの連携がますます強化されていくとは思いますが、HttpCalloutを使って色々と操作することもできるかと思います。(どんな連携機能が今後実装されていくかは....分かりませんが。。。)

Salesforceにはない文書機能について、その可能性を探ってみるのも面白いかもしれません。