simの通信をon/offするLINE Botを作って料金を節約して使えるiPhoneにしてみた。

こちらにも同じ内容の記事を書きました。
https://www.pnkts.net/2020/04/26/soracom-sim-bot


先日、職場の人にsimフリーのiPhone6sを頂いたので、どこのSIMを使うか悩んでました。
格安SIMのものがいいなと思って調べたり、周りの人に聞いていたら、[SORACOM](https://soracom.jp/)というものを教えてもらいました。
ということで、SORACOMのSIMを購入して使うことにしました。
> SORACOMは、IoT/M2M向けワイヤレス通信を提供するプラットフォームです。セルラー、LPWA（LoRaWAN、Sigfox）を、1回線からリーズナブルにご利用いただけます。それらの通信は、ウェブコンソールやAPIを通じて一括操作・管理することができます。さらにプラットフォームには、IoTシステム構築に必要となる、セキュリティやデバイス管理、クラウド連携などのサービスが用意されており、これらを必要に応じて組み合わせて利用することで、少ないリソースでスピーディにIoTシステムを構築し、ビジネス活用を始めることができます。

![IMG_0012.JPG](https://qiita-image-store.s3.amazonaws.com/0/76916/d563e3ec-8d3d-4653-c2b6-d65b8817b873.jpeg)

SORACOMのいいところは、APIが使えるところです。
APIリファレンスは[こちら](https://dev.soracom.io/jp/docs/api/)にあります。
APIで何ができるかというと、一番大きいと思うのは通信のON/OFFができるということ。
使わないときはOFFにして、使うときだけONにするということができるようになって、費用も最低限に抑えられます。（ちなみに従量課金制になってますので、通信した分だけお金がかかるSIMです。）
基本料金は10円/日+使った分だけ、なので、月300円から使えることになります。


# LINEのBotとGASを用いたシステム構成
今回のシステム構成としては、LINEのMessaging APIを使います。
サーバサイドは無料で使えるGoogle Apps Script(GAS)を使います。

# GASのスクリプト
まずはLINEのwebhookを受け取るところの処理です。
自分はいつもこの形を使っていますので参考になれば使って下さい。

```js:gas.gs
// pushしたいときに送る先のuser_idで、自分の値はスプレッドシートに残るログを見ればわかる
var log_user_id = '********************';
// postされたログを残すスプレッドシートのid
var spreadsheet_id = '**********************';


/**
 * postされたときの処理
 */
function doPost(e) {
  try {
    var json = JSON.parse(e.postData.contents);
    SpreadsheetApp.openById(spreadsheet_id).getSheetByName('log1').getRange(1, 1).setValue(json.toSource());
//    SpreadsheetApp.openById(spreadsheet_id).getSheetByName('log1').getRange(2, 1).setValue(json.events[0].source.groupId.toSource());
    
    switch(json.events[0].type) {
      case 'join':
        push(log_user_id, 'join');
        break;
      case 'message':
        getMessage(json);
        break;
      case 'postback':
        getPostback(json)
        break;;
      default:
        push(log_user_id, 'default');
        push(log_user_id, e.postData.contents);
        break;
    }
  } catch (ex) {
    push(log_user_id, 'exception\n' + json.events[0].toSource());
    push(log_user_id, ex.message);
  }
}
```


次にどこのグループ(orユーザ)から来たデータなのかを判別するところです

```js:getMessage.gs
/**
 * メッセージを受け取ったときの処理
 * userかgroupかを分けるだけ
 */
function getMessage(json) {
  switch(json.events[0].source.type) {
      // 1対1メッセージ
    case 'user':
      getUserMessage(json);
      break;
      // グループチャット
    case 'group':
      getGroupMessage(json);
      break;
  }
}

/**
 * userチャットを受け取ったときの処理
 */
function getUserMessage(json) {
  var userId = json.events[0].source.userId;
  var text = json.events[0].message.text;
  var replyToken = json.events[0].replyToken;
  switch(userId) {
    default:
      push(log_user_id, '見知らぬ人チャット');
      break;
  }
}

/**
 * groupチャットを受け取ったときの処理
 */
function getGroupMessage(json) {
  var groupId = json.events[0].source.groupId;
  var text = json.events[0].message.text;
  var replyToken = json.events[0].replyToken;
  switch(groupId) {
      // あいほん
    case '*******************':
      getSoracomMessage(replyToken, groupId, json.events[0].message);
      break;
    default:
      push(log_user_id, 'その他グループ:' + groupId);
      break;
  }
}
```

Postbackも同じような処理を書きます

```js:getPostback.gs
/**
 * メッセージを受け取ったときの処理
 * userかgroupかを分けるだけ
 */
function getPostback(json) {
  switch(json.events[0].source.type) {
      // 1対1メッセージ
    case 'user':
      getUserMessage(json);
      break;
      // グループチャット
    case 'group':
      getGroupPostback(json);
      break;
  }
}
function getUserPostback(json) {}
function getGroupPostback(json) {
  var groupId = json.events[0].source.groupId;
  var postback = json.events[0].postback;
  var replyToken = json.events[0].replyToken;
  
  var data = {};
  var pair=postback.data.split('&');
  for(var i=0;pair[i];i++) {
    var kv = pair[i].split('=');
    data[kv[0]] = kv[1];
  }
  
  switch(groupId) {
      ///// あいほん
    case '********************':
      getSoracomPostback(replyToken, groupId, postback);
      break;
      
      ///// 何もしない
    default:
      push(log_user_id, 'その他グループ postback:' + groupId + '\n' + postback.toSource());
      if (postback.data && 'message' in data) {
        push(groupId, data.message)
      }
      break;
  }
}
```

次に今回のメインの処理です。
SORACOMのAPIを叩いたりするところです。

```js:soracom.gs
// 管理画面から取得するキーなど
var soracomAuthKeyId = 'keyId-*******************';
var soracomAuthKey = 'secret-*******************';
var soracomLineGroupId = '********************';
var soracomName = 'iPhone6s'; // tagの名前を入力する

var soracomImsi = null;
var soracomApiKey = null;
var soracomToken = null;


/**
 * LINEに投稿されたら呼ばれる
 * 接続状況に応じてpostbackを投稿する
 */
function getSoracomMessage(replyToken,groupId, message) {
  var subscriberData = getSoracomsubscribers();
  if (subscriberData.status == 'active') {
    push(soracomLineGroupId, '現在は接続されています');
    pushSoracomInactiveButtons();
  } else {
    push(soracomLineGroupId, '現在は切断されています');
    pushSoracomActiveButtons();
  }
}

/**
 * LINEからpostbackのボタンを押されたら呼ばれる
 * 選択されたボタンによって挙動を変える
 */
function getSoracomPostback(replyToken, groupId, postback) {
  soracomInitialize();
  switch(postback.data) {
    case 'offline':
      soracomDeactivate();
      push(groupId, '切断したお');
      break;
    case 'change':
      pushSoracomActiveButtons();
      break;
    default:
      soracomActivate();
      soracomUpdateSpeedClass(postback.data);
      push(groupId, postback.data + 'にしたお');
      break;
  }
}

/**
 * soracomのapiを使う前に呼ぶ
 * これを呼べばapiが使えるようになる
 */
function soracomInitialize() {
  var data = getSoracomsubscribersByName(soracomName);
  if (!data) throw new Exception('指定した端末が見つかりませんでした');
  soracomImsi = data.imsi;
  soracomAuthorize();
}

/**
 * soracomのapiを叩くためのトークンを取得する
 */
function soracomAuthorize() {
var url = 'https://api.soracom.io/v1/auth';
  var headers = {
  };
  var postData = {
  'authKeyId': soracomAuthKeyId,
  'authKey': soracomAuthKey,
  }
  var options = {
    "method" : "post",
    'contentType': 'application/json',
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };
  var result = JSON.parse(UrlFetchApp.fetch(url, options).getContentText());
  soracomToken = result.token
  soracomApiKey = result.apiKey;
}

/**
 * simをアクティブにする(接続)
 */
function soracomActivate() {
  var url = 'https://api.soracom.io/v1/subscribers/' + soracomImsi + '/activate';
  var headers = {
    'X-Soracom-API-Key': soracomApiKey,
    'X-Soracom-Token': soracomToken,
  };

  var postData = {
  };

  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  return UrlFetchApp.fetch(url, options);  
}

/**
 * simを非アクティブにする(切断)
 */
function soracomDeactivate() {
  var url = 'https://api.soracom.io/v1/subscribers/' + soracomImsi + '/deactivate';
  var headers = {
    'X-Soracom-API-Key': soracomApiKey,
    'X-Soracom-Token': soracomToken,
  };

  var postData = {
  };

  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  return UrlFetchApp.fetch(url, options);  
}

/**
 * スピードのクラスを変更する
 */
function soracomUpdateSpeedClass(speedClass) {
  var url = 'https://api.soracom.io/v1/subscribers/' + soracomImsi + '/update_speed_class';
  var headers = {
    'X-Soracom-API-Key': soracomApiKey,
    'X-Soracom-Token': soracomToken,
  };
  var postData = {
    "speedClass": "s1." + speedClass
  }
  var options = {
    "method" : "post",
    'contentType': 'application/json',
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  return UrlFetchApp.fetch(url, options);  
}

/**
 * 指定したtag名のsubscriberを取得する
 */
function getSoracomsubscribersByName(name) {
  soracomAuthorize();
  var url = 'https://api.soracom.io/v1/subscribers'
  var headers = {
    'X-Soracom-API-Key': soracomApiKey,
    'X-Soracom-Token': soracomToken,
  };
  var options = {
    "method" : "get",
    'contentType': 'application/json',
    "headers" : headers,
  };

  var result = JSON.parse(UrlFetchApp.fetch(url, options).getContentText());
  for (var i = 0; i < result.length; i++) {
    if (result[i].tags.name == name) {
      return result[i];
    }
  }
  Logger.log('getSoracomsubscribersByName:Not Found.');
  return null
}

var _soracomSubscriberData = null;
/**
 * 使用するsimのsubscriberを取得する
 */
function getSoracomsubscribers() {
  if (_soracomSubscriberData) return _soracomSubscriberData;
  soracomInitialize();
  var url = 'https://api.soracom.io/v1/subscribers/' + soracomImsi;
  var headers = {
    'X-Soracom-API-Key': soracomApiKey,
    'X-Soracom-Token': soracomToken,
  };
  var postData = {

  }
  var options = {
    "method" : "get",
    'contentType': 'application/json',
    "headers" : headers,
  };
  
  var result = JSON.parse(UrlFetchApp.fetch(url, options).getContentText());
  _soracomSubscriberData = result;
  
  return result;
}

/**
 * 非アクティブ化するボタンを送信する
 */
function pushSoracomInactiveButtons() {
  var groupId = soracomLineGroupId;
  var subscriberData = getSoracomsubscribers();
  pushButtons(
    groupId,
    'https://corporate-tech-blog-wp.s3.amazonaws.com/tech/wp-content/uploads/2015/10/fig_home_02.png',
    'iPhone6s[' + subscriberData.speedClass + ']',
    '通信を切断しますか？',
    [
      {
        "type": "postback",
        "label": "切断する",
        "data": "offline"
      },
      {
        "type": "postback",
        "label": "classを変更する",
        "data": "change"
      },
    ]);
}

/**
 * アクティブ化するボタンを送信する
 */
function pushSoracomActiveButtons() {
  var groupId = soracomLineGroupId;
  var subscriberData = getSoracomsubscribers();
  pushButtons(
    groupId,
    'https://corporate-tech-blog-wp.s3.amazonaws.com/tech/wp-content/uploads/2015/10/fig_home_02.png',
    'iPhone6s[' + subscriberData.speedClass + ']',
    'どのクラスで接続しますか？',
    [
    
    {
    "type": "postback",
    "label": "s1.minimum",
    "data": "minimum"
    },
    {
    "type": "postback",
    "label": "s1.slow",
    "data": "slow"
    },
    {
    "type": "postback",
    "label": "s1.standard",
    "data": "standard"
    },
    {
    "type": "postback",
    "label": "s1.fast",
    "data": "fast"
    },
    ]);
}


/**
 * 毎日切断する。
 * このメソッドを未明(4時~5時)に呼ぶように設定する。
 */
function soracomAutoOffline() {
  soracomInitialize();
  var subscriberData = getSoracomsubscribers();
  if (subscriberData.status == 'active') {
    soracomDeactivate();
    push(soracomLineGroupId, '夜中なので切断するお');
  }
}

/**
 * 料金を報告する
 * このメソッドを毎日呼ぶように設定する
 */
function soracomGetBillsLatest() {
  soracomInitialize();
  var url = 'https://api.soracom.io/v1/bills/latest';
  var headers = {
    'X-Soracom-API-Key': soracomApiKey,
    'X-Soracom-Token': soracomToken,
  };

  var postData = {
  };

  var options = {
    "method" : "get",
    "headers" : headers,
  };

  var result = JSON.parse(UrlFetchApp.fetch(url, options).getContentText()).amount;
  push(soracomLineGroupId, '最新の料金は' + result + '円だお');
}
```

LINEの送信する周りの処理をまとめたもの。
今回使ってないメソッドもたくさんこちらに書いてあるので、ぜひ使ってみて下さい。

```js:line.gs
// line developersに書いてあるChannel Access Token
var access_token = '**********************';

/**
 * 指定のuser_idにpushをする
 */
function push(to, text) {
  Logger.log('push:' + to + ':' + text)
  var url = "https://api.line.me/v2/bot/message/push";
  var headers = {
    "Content-Type" : "application/json; charset=UTF-8",
    'Authorization': 'Bearer ' + access_token,
  };

  var postData = {
    "to" : to,
    "messages" : [
      {
        'type':'text',
        'text':text,
      }
    ]
  };

  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  return UrlFetchApp.fetch(url, options);  
}


/**
 * 指定のuser_idにpushをする
 */
function pushImage(to, src, srcPreview) {
  var url = "https://api.line.me/v2/bot/message/push";
  var headers = {
    "Content-Type" : "application/json; charset=UTF-8",
    'Authorization': 'Bearer ' + access_token,
  };

  var postData = {
    "to" : to,
    "messages" : [
      {
        'type':'image',
        'originalContentUrl':src,
        'previewImageUrl':srcPreview,
      }
    ]
  };

  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  return UrlFetchApp.fetch(url, options);
}

function pushButtons(to, thumbnailImageUrl, title, text, actions) {
  var url = "https://api.line.me/v2/bot/message/push";
  var headers = {
    "Content-Type" : "application/json; charset=UTF-8",
    'Authorization': 'Bearer ' + access_token,
  };

  var postData = {
    "to" : to,
    'messages': [{
      "type" : 'template',
      "altText": "'buttons template'に非対応のため別の端末で確認してください。",
      "template": {
        "type": "buttons",
        "thumbnailImageUrl": thumbnailImageUrl,
        "title": title,
        "text": text,
        "actions":actions,
      },
    }]
  };
  Logger.log(postData)

  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  return UrlFetchApp.fetch(url, options);
}

function pushSticker(to, packageId, stickerId) {
  var url = "https://api.line.me/v2/bot/message/push";
  var headers = {
    "Content-Type" : "application/json; charset=UTF-8",
    'Authorization': 'Bearer ' + access_token,
  };

  var postData = {
    "to" : to,
    "messages" : [
      {
        'type':'sticker',
        'packageId':packageId,
        'stickerId': stickerId,
      }
    ]
  };

  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  return UrlFetchApp.fetch(url, options);  
}

function pushLocation(to, title, address, latitude, longitude) {
  var url = "https://api.line.me/v2/bot/message/push";
  var headers = {
    "Content-Type" : "application/json; charset=UTF-8",
    'Authorization': 'Bearer ' + access_token,
  };

  var postData = {
    "to" : to,
    "messages" : [
      {
        'type':'location',
        'title':title,
        'address':address,
        'latitude':latitude,
        'longitude':longitude,
      }
    ]
  };

  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  return UrlFetchApp.fetch(url, options);  
}



/**
 * reply_tokenを使ってreplyする
 */
function reply(replyToken, text) {
  var url = "https://api.line.me/v2/bot/message/reply";
  var headers = {
    "Content-Type" : "application/json; charset=UTF-8",
    'Authorization': 'Bearer ' + access_token,
  };

  var postData = {
    "replyToken" : replyToken,
    "messages" : [
      {
        'type':'text',
        'text':text,
      }
    ]
  };

  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  return UrlFetchApp.fetch(url, options);  
}

function replySticker(replyToken, packageId, stickerId) {
  var url = "https://api.line.me/v2/bot/message/reply";
  var headers = {
    "Content-Type" : "application/json; charset=UTF-8",
    'Authorization': 'Bearer ' + access_token,
  };

  var postData = {
    "replyToken" : replyToken,
    "messages" : [
      {
        'type':'sticker',
        'packageId':packageId,
        'stickerId': stickerId,
      }
    ]
  };

  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  return UrlFetchApp.fetch(url, options);   
}

function replyImage(replyToken, src) {
  var url = "https://api.line.me/v2/bot/message/reply";
  var headers = {
    "Content-Type" : "application/json; charset=UTF-8",
    'Authorization': 'Bearer ' + access_token,
  };

  var postData = {
    "replyToken" : replyToken,
    "messages" : [
      {
        'type':'image',
        'originalContentUrl':src,
        'previewImageUrl':src,
      }
    ]
  };

  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  return UrlFetchApp.fetch(url, options);    
}
```

# GASプロジェクトを公開する
LINEのwebhookからデータを遅れるように、作成したGASプロジェクトを公開してあげます。
GASの上のメニューの「公開」→「ウェブアプリケーションとして導入」を選択。
以下の項目は間違えやすいので気をつけましょう。
次のユーザとしてアプリケーションを実行：自分(****@gmail.com)
アプリケーションにアクセスできるユーザ：全員(匿名ユーザを含む)

以上の設定で公開した時にURLが表示されるので、しっかりとコピペしてメモしておきましょう。

# LINEのwebhookに登録する
今回はLINEのMessaging APIを使用します。
登録手順はここでは省きますが、作成したボットのwebhookに先程メモしたURLを貼っておきましょう。


# 実際に動かしてみる
テキトーに文字を送ってみて下さい。
今回は文字ならなんでもおっけーなプログラムになっています。
![Screenshot_20171106-020036.png](https://qiita-image-store.s3.amazonaws.com/0/76916/8de73088-6f56-f085-76ee-1dfd0d99d009.png)
もしも接続されていない場合はこのような表示がされます。
スピードクラスを選択することで、指定したクラスで通信をONにしてくれます。

![Screenshot_20171106-020048.png](https://qiita-image-store.s3.amazonaws.com/0/76916/1d87cf5f-40e9-13fe-6741-ef7006cfe15e.png)
もし、ONの状態だった場合はこのような表示になります。
切断を選択すると切断することができます。
![Screenshot_20171106-020143.png](https://qiita-image-store.s3.amazonaws.com/0/76916/dfc419a8-2e6e-0909-e294-c319db8f407f.png)
また、クラスを変更することも可能です。

# おまけ機能
## 自動OFF機能
このようにできても接続をOFFにし忘れたら意味がありません。
そこで、soracomAutoOffline()というメソッドを毎日呼ぶようにGASの設定をすることで、毎日OFFにすることができます。
自分は4時〜5時に実行するように設定しています。
![Screenshot_20171106-020247.png](https://qiita-image-store.s3.amazonaws.com/0/76916/f9240476-f447-0385-dea0-7b3fde119a64.png)
今回は素材として手動で実行したので時間がおかしいですが。

## 料金案内機能
毎日の料金が気になりますね！
そこで、料金も送るように設定してみます。
soracomGetBillsLatest()を毎日送るように設定することで、使用している料金を毎日送ることができます。
![Screenshot_20171106-020259.png](https://qiita-image-store.s3.amazonaws.com/0/76916/c9051a47-44d5-1ee1-e61f-8be78b6d04e2.png)

# 最後に
このようにして頂いたiPhone6sを節約しながら使っています。
通信速度も今のところは気になっていません。（ほぼWi-Fi環境で使用しているので。）
格安SIMの選択肢にSORACOMを使ってみてはいかがですか！