JS.36 GASでLINE BOTを作成し、各種IDを取得する

2021-06-21(Mon) | tags: GAS

LINE Official Account に友達登録している人の user ID リストを作成したかったのですが、Messaging API referenceを見る限り、verified or premium アカウントでしかこの機能を使えないようなので、メッセージが送信された際に取得できる情報を利用することにしました。

※ 認証済みアカウントで上記のAPIを用いる時のコードは認証済みアカウントでuserIdを収集するに記載しました。(ページ下部)

LINE BOTの作成

LINE developers Consoleで必要な登録と設定を済ませ、LINE BOT(Channel)を作成します。それに関しては、LINEのBot開発 超入門(前編) ゼロから応答ができるまでが参考になるかと思います。

Channelが作成できたら、[LINE developer] -> [Messaging API] から、Channel access token を取得してください。また、以下で作成するGAS(web application)のWebhook URLの設定箇所も同じ場所です。

※ LINE アカウントを作成したい場合は LINE Official Account Manager から作成してください。

GASの設定

以下のように doPost を定義したスクリプトを用意し、web applicationとして公開、そのURLを先ほどのwebhook URLに設定すれば、(例えば以下では "message" のみの) event に反応して特殊な反応(func_xxx_event)をさせることができます。

const prop = PropertiesService.getScriptProperties();
const ss = SpreadsheetApp.getActiveSpreadsheet();

function doPost(e) {
  var contents = e.postData.contents;
  var obj = JSON.parse(contents)
  var events = obj["events"];
  for(var i = 0; i < events.length; i++){
    if(events[i].type == "message"){
      func_message_event(events[i]);
    }else if (events[i].type == "follow"){
      func_follow_event(events[i])
    }
    :
  }
}

get_profile

userId だけを収集してしまうとわからなくなってしまうので、userId からプロフィール情報を入手する関数を用意する。なお、この関数を使いながらデータをためても良いし、ある程度のデータがたまった後にまとめてこの関数でプロフィールに紐づけても良い。

/** Get the profile information of users with ``userId``.
 * @ref <https://developers.line.biz/en/reference/messaging-api/#get-profile>
 * @param {string} userId User ID that is returned in a webhook event object. Do not use the LINE ID found on LINE.
 * @param {string} acces_token Channel's Access Token.
 * @return {Object} User profile Information.
*/
function get_profile(userId, access_token){
  try{
    var options = {
      "method" : "GET",
      "headers" : {
        "Content-Type" : "application/json",
        "Authorization" : `Bearer ${access_token}`,
      },
    };
    var response = UrlFetchApp.fetch(`https://api.line.me/v2/bot/profile/${userId}`, options);
    return JSON.parse(response.getContentText());
  } catch(ex) {
    return {
      "displayName": "",
      "userId": "",
      "language": "",
      "pictureUrl": "",
      "statusMessage": ""
    }
  }
}

store_message_info

例えば func_message_event に以下の関数を用いれば、user ID などを記録することができ、user ID の収集に非常に役立ちます。

※ Property の CHANNEL_ACCESS_TOKEN に上記で取得した Channel access token の値を設定してください。

var CHAT_LOG_SHEET = ss.getSheetByName("LINE_chat_logs");
var CHAT_LOG_COUNTER_CELL = CHAT_LOG_SHEET.getRange("B1");
var CHAT_LOG_TABLE_A1     = CHAT_LOG_SHEET.getRange("A3");
var CHAT_LOG_TABLE_OFFSET_ROW = CHAT_LOG_TABLE_A1.getRow();
var CHAT_LOG_TABLE_OFFSET_COL = CHAT_LOG_TABLE_A1.getColumn();
/** Store chat logs.
 * @ref <https://developers.line.biz/en/reference/messaging-api/#message-event>
 * @param {Object} e Message event.
*/
function store_message_info(e){
  var userId = e.source.userId;
  var profile = get_profile(userId, prop.getProperty("CHANNEL_ACCESS_TOKEN"))
  var data = [[e.timestamp, profile.displayName, userId, e.source.groupId, e.source.roomId, e.message.text]]
  var num_stored_messages = parseInt(CHAT_LOG_COUNTER_CELL.getValue());
  CHAT_LOG_SHEET.getRange(
    CHAT_LOG_TABLE_OFFSET_ROW+num_stored_messages,
    CHAT_LOG_TABLE_OFFSET_COL,
    data.length,
    data[0].length
  ).setValues(data);
  CHAT_LOG_COUNTER_CELL.setValue(num_stored_messages+1);
}

demo image

reply_message_echolalia

同様にして、 func_message_event に以下の関数を用いれば、送信されたメッセージをそのまま返すLINE BOTを作成することもできます。

※ Property の CHANNEL_ACCESS_TOKEN に上記で取得した Channel access token の値を設定してください。

function reply_message_echolalia(e) {
  var postData = {
    "replyToken" : e.replyToken,
    "messages" : [
      {
        "type" : "text",
        "text" : e.message.text,
      }
    ]
  };
  var options = {
    "method" : "post",
    "headers" : {
      "Content-Type" : "application/json",
      "Authorization" : "Bearer " + prop.getProperty("CHANNEL_ACCESS_TOKEN")
    },
    "payload" : JSON.stringify(postData)
  };
  UrlFetchApp.fetch("https://api.line.me/v2/bot/message/reply", options);
}

store_new_followers_info

func_follow_event に以下の関数を用いれば、新規followerの情報を逐次収集できる。

var USER_ID_SHEET = ss.getSheetByName("LINE_user_Ids");
var USER_ID_COUNTER_CELL = USER_ID_SHEET.getRange("B2");
var USER_ID_TABLE_A1     = USER_ID_SHEET.getRange("A4");
var USER_ID_TABLE_OFFSET_ROW = USER_ID_TABLE_A1.getRow();
var USER_ID_TABLE_OFFSET_COL = USER_ID_TABLE_A1.getColumn();
/** Store new followers info
 * @ref <https://developers.line.biz/en/reference/messaging-api/#follow-event>
 * @param {Object} e Follow event.
*/
function store_new_followers_info(e){
  var userId = e.source.userId;
  var profile = get_profile(userId, prop.getProperty("CHANNEL_ACCESS_TOKEN"));
  var data = [[profile.displayName, userId, ""]];
  var num_stored_userIds = parseInt(USER_ID_COUNTER_CELL.getValue());
  USER_ID_SHEET.getRange(
    USER_ID_TABLE_OFFSET_ROW+num_stored_userIds,
    USER_ID_TABLE_OFFSET_COL,
    data.length,
    data[0].length,
  ).setValues(data);
  USER_ID_COUNTER_CELL.setValue(num_stored_userIds+1);
}

demo image

Python で LINE BOT を操作する

line-bot-sdk-pythonという便利なSDKがあったので、これを用いて操作してみます。

$ pip install line-bot-sdk

以下のコードでメッセージの送信が可能です。なお、サンプルコードには、flaskと連携し、LINE BOTをカスタマイズする方法が記載されています。

シンプルなテキストを送信する

from linebot import LineBotApi
from linebot.models import TextSendMessage

line_bot_api = LineBotApi(channel_access_token=channel_access_token)
line_bot_api.push_message(to=user_ID, messages=TextSendMessage(text='Hello World!'))

テンプレートテキストを送信する

from linebot import LineBotApi
from linebot.models import ButtonsTemplate, PostbackAction, MessageAction, URIAction, TemplateSendMessage

line_bot_api = LineBotApi(channel_access_token=channel_access_token)
line_bot_api.push_message(to=user_ID, messages=TemplateSendMessage(
    alt_text='Buttons template',
    template=ButtonsTemplate(
        thumbnail_image_url='https://iwasakishuto.github.io/images/profile/twitter.png',
        title='Menu',
        text='Please select',
        actions=[
            PostbackAction(
                label='postback',
                display_text='postback text',
                data='action=buy&itemid=1'
            ),
            MessageAction(
                label='message',
                text='message text'
            ),
            URIAction(
                label='portfolio',
                uri='https://iwasakishuto.github.io/'
            )
        ]

    )
))

これらによって、以下のようなメッセージを送信できます。

demo image

認証済みアカウントでuserIdを収集する

認証済みアカウントであれば https://api.line.me/v2/bot/followers/ids のAPIが使えるので、以下のコードで一度に全てのuserIdリストを作成することができます。

get_follower_ids

(認証済)アカウントの ACCESS_TOKEN を用いて get_follower-idsというAPIを叩きます。このとき、一度に300人分しか取れないため、それ以降のユーザーの情報を取得するためには continuationToken という引数に、この関数の返り値の next というパラメータを入れる必要があります。

/** Get followers userId lists
 * @param {string} access_token Channels Access token. (Must be verfied.)
 * @param {string} continuationToken
 * @ref <https://developers.line.biz/en/reference/messaging-api/#get-follower-ids>
*/
function get_follower_ids(access_token, continuationToken=""){
  start_param = (continuationToken.length == 0) ? "" : `?start=${continuationToken}`;
  var options = {
    "method" : "GET",
    "headers" : {
      "Content-Type" : "application/json",
      "Authorization" : `Bearer ${access_token}`,
    },
  };
  var response = UrlFetchApp.fetch(`https://api.line.me/v2/bot/followers/ids${start_param}`, options);
  return JSON.parse(response.getContentText());
}

get_all_follower_ids

上述の通り、一度に取得できるユーザーIDの数に限りがあるので、while で全てのユーザーIdを取得するまで get_follower_ids を実行します。

/** Get all followers' userId list.
 * @param {string} access_token Channels Access token.
 * @ref https://developers.line.biz/en/reference/messaging-api/#get-follower-ids>
*/
function get_all_follower_ids(){
  var userIds = [];
  var continuationToken="";
  while (true){
    response = get_follower_ids(continuationToken)
    userIds = userIds.concat(response.userIds)
    continuationToken = response.next;
    if (continuationToken == undefined) break
  }
  return userIds;
}

update_followers_profile

もし、これらの関数を定期的に実行してスプレッドシートにまとめる、といったオペレーションがある場合、以下の関数を使うと便利だと思います。以下の関数では、

  • 取得していないuserIdがあった場合、それを表に追加する
  • 取得していたが、displayName に変更があった場合、更新する

ことができます。

/** Get user profile and organize it in a table while updating user's displayName.
 * @param {string} access_token Channels Access token.
 * @param {string} counter_cell The name of the cell which holds the number of acquired data.
 * @param {string} table_A1 Upper left part (worth like a A1 cell) of the table to save data.
 * @param {string} sheet_name Target Sheet Name.
*/
function update_followers_profile(access_token, counter_cell, table_A1, sheet_name){
  var FOLLOWERS_SHEET = ss.getSheetByName(sheet_name);
  var FOLLOWERS_COUNTER_CELL = FOLLOWERS_SHEET.getRange(counter_cell);
  var FOLLOWERS_TABLE_A1     = FOLLOWERS_SHEET.getRange(table_A1);
  var FOLLOWERS_TABLE_OFFSET_ROW = FOLLOWERS_TABLE_A1.getRow();
  var FOLLOWERS_TABLE_OFFSET_COL = FOLLOWERS_TABLE_A1.getColumn();

  var num_stored_users = parseInt(FOLLOWERS_COUNTER_CELL.getValue());
  var follower_userIds = get_all_followers_profile(access_token);
  var stored_userIds = [];
  var stored_displayNames = [];
  var stored_Names = [];
  FOLLOWERS_SHEET.getRange(
    FOLLOWERS_TABLE_OFFSET_ROW,
    FOLLOWERS_TABLE_OFFSET_COL,
    num_stored_users,
    3
  ).getValues().forEach(function(e){
    stored_userIds.push(e[0]);
    stored_displayNames.push(e[1]);
    stored_Names.push(e[2]);
  })
  for (var i=0; i<follower_userIds.length; i++){
    var userId = follower_userIds[i];
    var idx = stored_userIds.indexOf(userId);
    var profile = get_profile(userId, access_token);
    Logger.log([i, idx, profile.displayName]);
    if (idx==-1){
      // Register a new user.
      var data = [[userId, profile.displayName, ""]];
      FOLLOWERS_SHEET.getRange(
        FOLLOWERS_TABLE_OFFSET_ROW+num_stored_users,
        FOLLOWERS_TABLE_OFFSET_COL,
        data.length,
        data[0].length,
      ).setValues(data);
      num_stored_users++;
    }else if (stored_displayNames[idx] != profile.displayName){
      // Update a stored user info.
      Logger.log(`${stored_displayNames[idx]} != ${profile.displayName}`);
      var data = [[userId, profile.displayName, stored_Names[idx]]];
      FOLLOWERS_SHEET.getRange(
        FOLLOWERS_TABLE_OFFSET_ROW+idx,
        FOLLOWERS_TABLE_OFFSET_COL,
        data.length,
        data[0].length,
      ).setValues(data);
    }
  }
  FOLLOWERS_COUNTER_CELL.setValue(num_stored_users);
}
other contents
social