Quip API をためしてみる

# Quip について

https://quip.com/

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

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

1. 全てのドキュメントにチャット機能があり、チーム内でコミュニケーションをとりながら文書を作り上げていける
2. モバイルからでも文書を編集したり、チャットに参加してコミュニケーションとれる（モバイルファースト！）
3. 文書の特定の箇所でコメントを追記できる（チームメンバへのメンションも可）
4. 文章の他に、スプレッドシート、チャートをつけられる


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

なお、Dreamforce2016におけるQuipに関するセッションは、以下で公開されています。
* https://www.youtube.com/watch?v=DduJWrkCpsY
* 製品デモ -> [19:42〜](https://www.youtube.com/watch?v=DduJWrkCpsY&feature=youtu.be&t=1182)

# 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" の２種類が用意されています。"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);

}
```

```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 {}
}
```

### おためし

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

![create_account.png](https://qiita-image-store.s3.amazonaws.com/0/1840/a986f9f7-3562-f2b1-f593-4da1e8ebf79c.png)

(作成後)
![sfdc_account.png](https://qiita-image-store.s3.amazonaws.com/0/1840/fb946c9b-afec-3cab-ba85-0c4c4821ae07.png)

(Quipのリンク起動)
![quip_doc.png](https://qiita-image-store.s3.amazonaws.com/0/1840/a35e2d4b-be96-bc42-fc03-e71d68fd5784.png)

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


# その他

12/13-14に開催された"Salesforce World Tour Tokyo 2016"でもQuipのブースはもちろんありました。
そこでSalesforceとのインテグレーションについても少し話を聞くことができました。

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

* SalesforceのLightningComponentとして組み込む
* 関連リストとしてQuipへのドキュメントリンク用レコードを保持する
    * 関連リストにボタンを用意して、Quipのドキュメントを作成し、そのリンクをレコードとして保持する

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


# おわりに

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

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

今後、Salesforceとの連携がますます強化されていくとは思いますが、HttpCalloutを使って色々と操作することもできるかと思います。（どんな連携機能が今後実装されていくかは....分かりませんが。。。）
Salesforceにはない文書機能について、その可能性を探ってみるのも面白いかもしれません。