Quip について
一言で言えば、"文書管理アプリ"です。
が、他のアプリケーションと異なるポイントとしては、"コミュニケーション"と"文書を一緒に作り上げていく"を同時に実現することを目指したもの...だと思います。
また、以下のような特徴を持っています。
- 全てのドキュメントにチャット機能があり、チーム内でコミュニケーションをとりながら文書を作り上げていける
- モバイルからでも文書を編集したり、チャットに参加してコミュニケーションとれる(モバイルファースト!)
- 文書の特定の箇所でコメントを追記できる(チームメンバへのメンションも可)
- 文章の他に、スプレッドシート、チャートをつけられる
さて、そんなQuipですが、今年になってSalesforceが買収したことを発表してます。Dreamforce2016でも大きく取り上げられており、今後Salesforceとの連携が強化されていくことでしょう。
なお、Dreamforce2016におけるQuipに関するセッションは、以下で公開されています。
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"というボタンでアクセストークンを発行するようです。
![quip_get_personalaccesstoken.png](https://qiita-image-store.s3.amazonaws.com/0/1840/351d2c48-0246-47d5-b6ba-165f8e28b584.png)
取得したトークンは...一旦どこかに保持しておく必要があるので、例えばカスタム設定とかに置いておきます。
※ここは非常に残念で、今後要改善ですね。。
![customsetting_quip_token.png](https://qiita-image-store.s3.amazonaws.com/0/1840/ae265206-e229-2d7d-1087-1c65ef8256ab.png)
### 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);
}
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) {
//
}
}
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 {}
}
おためし
取引先を作成してみます。
無事にフォルダとドキュメントが作成されました。
その他
12/13-14に開催された"Salesforce World Tour Tokyo 2016"でもQuipのブースはもちろんありました。
そこでSalesforceとのインテグレーションについても少し話を聞くことができました。
※予めお伝えしますと、まだSalesforce傘下に入って間もないこともあり、インテグレーションに関しては発展途上...という話でした。
- SalesforceのLightningComponentとして組み込む
- 関連リストとしてQuipへのドキュメントリンク用レコードを保持する
- 関連リストにボタンを用意して、Quipのドキュメントを作成し、そのリンクをレコードとして保持する
といったことが今後追加されるようです。
おわりに
リアルタイムでコラボレーションしながらドキュメントを作成orメンテしていくという点では、Quipはなかなか使い勝手が良さそうですし、様々なところで応用ができそうかな、と思います。
開発視点からすれば、APIが公開されているので、Salesforceもしくは他のクライアントアプリからQuipを色々と操作することもできるようです。
今後、Salesforceとの連携がますます強化されていくとは思いますが、HttpCalloutを使って色々と操作することもできるかと思います。(どんな連携機能が今後実装されていくかは....分かりませんが。。。)
Salesforceにはない文書機能について、その可能性を探ってみるのも面白いかもしれません。