Google Apps Script で6分以上の処理をする

複数のスプレッドシートを集計したりするのにGoogle Apps Script(GAS)を使うことはよくあります。
ですが、GASは6分という制限時間があり、度々それに悩まされます。
6分を超えると「起動時間の最大値を超えました」とエラーが吐かれ、途中で処理が終了してしまいます。
これを解決する方法として、処理を途中で止め、どこまで処理したかを保存し、1分後に再度実行するようにトリガーを発行するという方法があります。
どこまで処理したかを保存できるような作りでないといけないという制約はあります。
例えば、行毎に処理しているならば、何行まで処理したかを保存しておけば良いということです。
どこに保存するかというと、PropertiesServiceというところにKey-Valueで保存できます。
トリガーは発行すると以下の図のようにずっとトリガー一覧に残ってしまいます。残っていても害はないのですが、邪魔なので削除する処理も入れます。

トリガー

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
function func() {
var startTime = new Date();
//タイマーで起動するのでgetActiveSheet()などは使えない
var sheet = SpreadsheetApp.openById("1W3lfUaF_9msFJ2oRETpKMTvbF_xxxxxxxxxxxxx").getSheetByName("シート1");
var properties = PropertiesService.getScriptProperties(); //途中経過保存用
var startRowKey = "startRow"; //何行目まで処理したかを保存するときに使用するkey
var triggerKey = "trigger"; //トリガーIDを保存するときに使用するkey
//途中から実行した場合、ここに何行目まで実行したかが入る
var startRow = parseInt(properties.getProperty(startRowKey));
if(!startRow){
//初めて実行する場合はこっち
startRow = 1;
}
var rows = sheet.getDataRange().getValues();
for(var i = startRow; i < rows.length; i++){
var diff = parseInt((new Date() - startTime) / (1000 * 60));
if(diff >= 5){
//5分経過していたら処理を中断
properties.setProperty(startRowKey, i); //何行まで処理したかを保存
setTrigger(triggerKey, "func"); //トリガーを発行
return;
}
//なんか重い処理
}
//全て実行終えたらトリガーと何行目まで実行したかを削除する
deleteTrigger(triggerKey);
properties.deleteProperty(startRowKey);
}
//指定したkeyに保存されているトリガーIDを使って、トリガーを削除する
function deleteTrigger(triggerKey) {
var triggerId = PropertiesService.getScriptProperties().getProperty(triggerKey);
if(!triggerId) return;
ScriptApp.getProjectTriggers().filter(function(trigger){
return trigger.getUniqueId() == triggerId;
})
.forEach(function(trigger) {
ScriptApp.deleteTrigger(trigger);
});
PropertiesService.getScriptProperties().deleteProperty(triggerKey);
}
//トリガーを発行
function setTrigger(triggerKey, funcName){
deleteTrigger(triggerKey); //保存しているトリガーがあったら削除
var dt = new Date();
dt.setMinutes(dt.getMinutes() + 1); //1分後に再実行
var triggerId = ScriptApp.newTrigger(funcName).timeBased().at(dt).create().getUniqueId();
//あとでトリガーを削除するためにトリガーIDを保存しておく
PropertiesService.getScriptProperties().setProperty(triggerKey, triggerId);
}

ちなみにですが、https://developers.google.com/apps-script/guides/services/quotasによると、Early Accessだと実行時間は30分になります。
条件満たしていたので会社で申請したら、2週間ぐらい経って、通って30分になりました。

Go言語でSansanのAPIを呼ぶ

準備

名刺管理のSansanはAPIを公開しており、自分の所有する名刺を検索したりすることができる。
今回はそのAPIを使って名刺を検索してみる。
まず、Sansanにログインして「管理者設定」から「外部サービス連携」をクリック。
APIタブをクリックしたページで、APIキーを発行できる。ちなみにAPI利用にも権限があり、権限がないユーザでは発行できない。

Go言語で実装

発行したAPIキーを使ってAPIを呼ぶが、方法は至ってシンプル。
X-Sansan-Api-KeyヘッダにこのAPIキーを入れるだけで良い。
というわけで、会社名で検索するときの、ソースが以下である。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package main
import (
"encoding/json"
"log"
"net/http"
"net/url"
"github.com/davecgh/go-spew/spew"
)
const (
apiKey = "xxxxxxxxxxxxxxxxxxxxx"
searchURL = "https://api.sansan.com/v1.1/bizCards/search"
)
type SansanRes struct {
HasMore bool `json:"hasMore"`
Data []Card `json:"data"`
}
//本当はもっといろいろあるけど、ざっくりと
type Card struct {
LastName string `json:"lastName"`
FirstName string `json:"firstName"`
Email string `json:"email"`
Mobile string `json:"mobile"`
Tel string `json:"tel"`
DepartmentName string `json:"DepartmentName"`
Title string `json:"title"`
CompanyName string `json:"companyName"`
PostalCode string `json:"postalCode"`
Address string `json:"address"`
PersonId string `json:"personId"`
}
func main() {
values := url.Values{}
values.Add("companyName", "test") //会社名をtestで検索
values.Add("range", "all") //会社全体で持ってる名刺を検索
req, err := http.NewRequest("GET", searchURL+"?"+values.Encode(), nil)
if err != nil {
log.Fatal("http request error", err.Error())
}
req.Header.Set("X-Sansan-Api-Key", apiKey)
client := new(http.Client)
resp, err := client.Do(req)
if err != nil {
log.Fatal(err.Error())
}
defer resp.Body.Close()
dec := json.NewDecoder(resp.Body)
var res SansanRes
dec.Decode(&res)
spew.Dump(res) //表示
}

SalesforceのAPIでリードが作れない

前記事でAPIを使ってAccount(取引先)のデータを作ることができた。
次にLead(リード)のデータを作ろうと思った。
試しに /services/data/v38.0/sobjects/Lead/ に以下のデータをPOSTしてみた。

1
2
3
4
{
"Name" : "John",
"Company" : "test"
}

すると、以下のようなレスポンスが返ってきた。

1
2
3
4
5
6
7
8
9
[
{
"message": "Unable to create/update fields: Name. Please check the security settings of this field and verify that it is read/write for your profile or permission set.",
"errorCode": "INVALID_FIELD_FOR_INSERT_UPDATE",
"fields": [
"Name"
]
}
]

Nameフィールドに対して、権限がないようなエラーである。しかし、権限も何も特別なことをしている記憶はない。
これにすごい時間を取られたが、どうやらリードのNameは人名であり、LastNameとFirstNameに分ける必要があるらしい。
というわけで、以下のデータを送ったところ、無事作成できた。こんなのはまるに決まってる。

1
2
3
4
5
{
"FirstName" : "john",
"LastName" : "Smith",
"Company" : "test"
}

SalesforceのRest API を呼ぶ

REST API

SalesforceはOAuth2の認証を使ってAPIを呼ぶことができる。
今回はそれを試したいと思う。

参考にしたのはこれ。
Force.com REST API 開発者ガイド

準備

Salesforce 開発者 からサインアップすると、無料の開発エディションが使用できる。
ログインしたら、設定からアプリケーションを選択し、最下部にある接続アプリケーションの新規ボタンをクリック。

接続アプリケーションの作成

接続アプリケーション名などは適当に入力。
OAuth設定の有効化にチェック。コールバックURLは環境に合わせて設定。今回はhttp://localhost:8080/とした。
選択したOAuth範囲は、「フルアクセス」と「ユーザに代わっていつでも要求を実行」の2つ。後者がないとRefresh Tokenを取得できない。

scope

これで保存。
保存後、コンシューマ鍵とコンシューマの秘密の2つをコピーしておく。

Go言語で実装

Go言語で実装していく。
まず、認証のEndpointだが、上記pdfによると、以下の2つのようだ。
ただし、sandbox環境の場合は、loginの箇所をtestにする。

そして、APIを呼ぶ際のEndpointは以下である。
xxxの箇所には各自ログインしたときのSalesforceのアドレスと同じものが入る。例えばap4など。

手順としては認証用のURLを作り、コールバック用に指定したlocalhost:8080でサーバを起動しておく。
URLにアクセスすると、ユーザに確認が求められ、承認するとリダイレクトされる。
リダイレクト時にcodeが付与されているので、これを使用し、トークンを手に入れられる。
トークンを手に入れたら、APIを呼ぶことができるが、今回はAccountに新しいレコードを追加してみる。
以上の手順のソースが以下である。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package main
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"net/http"
"golang.org/x/oauth2"
)
var conf *oauth2.Config
const (
ClientId = "xxxxxxxxxxx" //コンシューマ鍵
ClientSecret = "xxxxxxxxxxx" //コンシューマの秘密
AuthURL = "https://login.salesforce.com/services/oauth2/authorize"
TokenURL = "https://login.salesforce.com/services/oauth2/token"
RedirectURL = "http://localhost:8080/"
ApiURL = "https://xxx.salesforce.com/services/data/v38.0/"
)
func main() {
conf = &oauth2.Config{
ClientID: ClientId,
ClientSecret: ClientSecret,
Endpoint: oauth2.Endpoint{
AuthURL: AuthURL,
TokenURL: TokenURL,
},
RedirectURL: RedirectURL,
}
url := conf.AuthCodeURL("")
fmt.Println(url)
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
func handler(w http.ResponseWriter, r *http.Request) {
code := r.FormValue("code")
fmt.Println(code)
token, err := conf.Exchange(oauth2.NoContext, code)
if err != nil {
log.Fatal("exchange error", err.Error())
return
}
client := conf.Client(oauth2.NoContext, token)
jsonStr := `{"Name" : "John"}`
req, err := http.NewRequest("POST", ApiURL+"sobjects/Account", bytes.NewBuffer([]byte(jsonStr)))
if err != nil {
log.Fatal(err.Error())
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
log.Fatal(err.Error())
}
body, _ := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
println(string(body))
}

上記実行し、Account(取引先)に名前がJohnの新しいレコードが作られるのを確認できた。

SalesforceのOAuth2でリフレッシュトークンが取得できない

最近、仕事でSalesforceを触っている。
REST APIを使っていろいろしようとしたのだが、OAuth2のトークンを交換するところで、なぜかRefresh Tokenが空で取得できなくてはまった。
Access Tokenは取得できるのに、Refresh Tokenが空。

答えはここで判明。
oauth authorization no longer returning refresh token (HELP!)

Salesforce側の接続アプリケーションの設定でスコープを定義するところがあるのだが、
そこに「ユーザに代わっていつでも要求を実行(refresh_token,offline_access)」というスコープがあり、これが必要なようだ。
フルアクセスのスコープを入れてたのでこれで全て入っているとばかり思っていたが、そうではないらしい。

scope

これで無事Refresh Tokenが取れた。