見出し画像

LINE公式アカウントで大会リピーターを増やそう

ノブナガと申します。
見た目も大体それを想像していただけたら50%程度は似ていると思います。
(地毛で丁髷を結っています)

画像16

昨年に引き続き今年もAdvent Calenderにて記事を書かせていただくことになりました。

他の執筆者の皆様がイベントの裏事情であったり、在り方についてのアプローチをしてくださっているので、私は大会を趣味として開催している軽めの層として口当たりの良い短文での記事とさせていただこうかと思います。


ところで…

スクリーンショット 2021-12-07 17.55.58

あの、ハードル上げないでください…(ありがとうございます)!

上記の記事は此方から!

Tonamel(大会開催プラットフォーム)

Tonamelについては知名度も上がり大分認知されてきたと思う為、今回は敢えて課題となりそうなところに対するアプローチを紹介しようと思います。

まだ使ったことのない方は是非Tonamelを利用してみてください!

本題ですが現在のTonamelには主催団体のフォロー、その団体の新規大会が開催された際に通知してくれる方法がありません。
(APIとか実装してもらえないかな…チラッ)

そこでLINE公式アカウントを利用しての大会運営を考えました。

LINE公式アカウント

通常のLINEアカウントと別に作成することが出来、なおかつ基本無料での運用が可能です。

スクリーンショット 2021-11-10 18.03.27

ざっくばらんに説明すると1ヶ月辺り1,000通までは無料です。
この1,000通は友達登録しているユーザーそれぞれに1通としてメッセージを送っての1,000通の為、単純に1000を友達数で割れば月当たりで何回メッセージを無料で送信出来るかの目安になります。

手っ取り早く運用してみたい方は此方のサイトが簡潔にまとめられているので参考までに(公式ではない外部サイトへのリンクになります)。

1.一斉送信が便利

公式アカウントを友達に追加してくれているユーザーに対して一斉にメッセージを送ることが出来ます。
即時送ることも出来ますし、予約時間での送信も可能です。

大会開催が決まった段階で即時メッセージを送信し、募集開始のタイミングで再度その旨をお知らせするメッセージなどを送るのも効果的です。

画像にリンクをつけたバナーの形で告知することも出来るのでセンスさえあればイカした告知も。

2.リッチメニューが機能的

画像2

画像は私の運営している大会アカウントのものです。

メニューのレイアウトは様々で、それぞれにリンクなどを設定可能です。
上記の例では寂しいボタンにも画像を設定することも可能なので見栄えもある程度自由に装飾可能です。

大会参加・練習会はそれぞれ設定しているTonamelの大会ページへリンクしています(余談ですが練習会作成以前はLINEの機能であるスタンプカードを実装していましたが未使用のまま終わりました…)。

HN登録・デッキ登録はbotに動作をさせる定型文のコマンドに割り当てています(画像内、「デッキ登録をします。」という文言がそれです)。
※需要があればbot作成についてもそのうち解説しようかと思います。

アンケートフォームやスポンサー機能へのリンクなども大会のファンに向けてという意味ではアリだな、と思い実装検討しているところだったりします。

メッセージに動画を添付した場合は再生数(1秒,3秒)、
リンクを添付した場合はクリック数、
インプレッションなどを閲覧可能な為どれくらいリピーターが利用してくれているかわかるのも良いところです。

3.SNSでのアプローチと比較して

Twitterを例にSNSでの情報発信のメリットを挙げると、

Twitter
・TLを流し見しながら情報が自然に目に入る
・RTなど拡散力が見込める


といった初見の方へのアプローチでは大きな優位点がある一方で、

LINE公式アカウント
・プッシュ通知の設定が不要
・2項で挙げた多様なアプローチが可能


といった部分ではLINEアカウントに軍配が上がります。

新規集客に関しては不向きな一方で、リピーターに向けた強いアプローチをかけられるのが長所となっております。

まとめ(+雑談)

オフラインイベントも徐々に復活の兆しを見せており、大会も様々なジャンルで活性化してる中、如何にリピーターを増やすかというところに焦点を当てて今年は取り組んで参りました。

ポケモンカードは大会にデッキコードの提出を求めることも多く、Botを始めとしたユーザビリティの担保を行うこと、エントリー開始前に通知を送れることなどリピーターの獲得に適しているツールだったかと思います。

募集開始の通知が送れるようになった影響か、1日経たず定員が埋まることも増えてきており、嬉しい限りです。
今後もユーザーが使いやすい窓口になるよう手を加えて参りたいと思います。

関西圏のポケカプレイヤーの方は是非野望杯へのエントリーを、そしてLINE公式アカウントで友達になってください。

これAdvent Calendarでやることだったのか…?

明日はオチャを?さんです!オフ大会で感じたことについて書かれる予定のようで、これまた勉強になりそうですな!


付録(サンプルBotプログラム)

※やや技術者向けの内容になりますが、コピペして動作させるだけならば出来るかと思いますので興味がある方はLet's Try!
相談には乗ります…笑

私の主催しているポケモンカードの大会である野望杯の為に作成したBotをサンプルとして置いておきます。

現在の最大の欠点が同一ハンドルネームの見分けがつかないところです。
(デッキコードの提出間口をGoogleフォームとLINEの2箇所にしておりそれをそれを一纏めにしている為)
それでも宜しければ是非参考までにどうぞ。


1.LINE Developerアカウントを作成する

アカウントを作成したらプロバイダーを作ります。

スクリーンショット 2021-12-07 17.13.13

スクリーンショット 2021-12-07 17.14.45

スクリーンショット 2021-12-07 17.16.39

Messaging APIの下にChannel access tokenというところがあるので確認。


2.スプレッドシートを作成する

サンプルはこちら

スプレッドシートのシート名などもサンプル同様に作成してください。

(任意:Googleフォームを追加しても、LINEからの提出と一纏めに出来ます。)

スクリーンショット 2021-12-07 16.59.55


3.Apps Scriptを編集

スクリーンショット 2021-12-07 17.05.53

サンプル内にソースコードがあるのでそのままコピペします。

(12/10 追記:どうやら権限関係でアクセス出来ていなかったようです…。
Apps Scriptの中身は以下の通りなのでコピペしてください)


// 利用しているシート
const SHEET_ID = '1775UPHrbIl1nIUdCt89qfdREDMj0qsuvgVbmMLebmE4';//スプレッドシートの/d/から/editまでの間の文字列をコピペ
// 利用しているSSのシート名(※変えるとみえなくなる)
const DB_NAME = 'userDB';
const FORM_NAME = 'deckcode';

const LINEID_FIELD = 1;
const USERNAME_FIELD = 2;
const DECK_FIELD = 3;
const DATE_FIELD = 4;

const FORM_DATE_FIELD = 1;
const FORM_NAME_FIELD = 2;
const FORM_DECK_FIELD = 3;
const FORM_LINEUSER_FIELD = 4;

const DECK_REGEXP = /^[a-zA-Z0-9]{6}-[a-zA-Z0-9]{6}-[a-zA-Z0-9]{6}$/g;

// LINE Message API アクセストークン
const ACCESS_TOKEN = '○○○';//LINE Message APIをコピペ
// 通知URL
const PUSH = "https://api.line.me/v2/bot/message/push";
// リプライ時URL
const REPLY = "https://api.line.me/v2/bot/message/reply";
// プロフィール取得URL
const PROFILE = "https://api.line.me/v2/profile";

/**
* doPOST
* POSTリクエストのハンドリング
*/
function doPost(e) {
 let json = JSON.parse(e.postData.contents);
 reply(json);
}

/** 
* doGet
* GETリクエストのハンドリング
*/
function doGet(e) {
   return ContentService.createTextOutput("SUCCESS");
}

/** 
* reply
* ユーザからのアクションに返信する
*/
function reply(data) {
 // POST情報から必要データを抽出
 let lineUserId = data.events[0].source.userId;
 let postMsg    = data.events[0].message.text;
 let replyToken = data.events[0].replyToken;
 let action    = data.events[0].message.action;
 // 記録用に検索語とuserIdを記録
 //  debug(postMsg, lineUserId);
 //debug(data.events[0], lineUserId);
 let replyText = "メッセージありがとうございます。申し訳ありませんが直接お問い合わせください。";

 let cache = CacheService.getScriptCache();
 let type = cache.get("type");

 switch (postMsg) {
   case "HN登録をします。":
     cache.put("type", 1);
     replyText = "大会名で使用するハンドルネームを入力してください。"
     break;
   case "デッキ登録をします。":
     cache.put("type", 10);
     replyText = "公式のデッキメーカーで保存したデッキコードをペーストしてください。";
     break;
   //隠しモード
   // case "殿":

   //   break;
   default:
     switch (type) {
       case null:
         break;
       case "1":
         replyText = registPlayer(lineUserId,postMsg);
         cache.put("type",null);
         break;
       case "10":
         replyText = registDeck(lineUserId,postMsg);
         cache.put("type",null);
         break;            
       default:
         cache.put("type", null);
         replyText = "申し訳ございません。最初からやり直してください。Type(" + type +")";
     }
 }
 sendMessage(replyToken, replyText);
}

function registPlayer(userId,name) {
 let sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName(DB_NAME);
 let fields = sheet.getRange('B2:B');
 fields.activate();
 let finder = fields.createTextFinder(name).matchCase(true);

 if(finder.findAll().length != 0){
   return "そのハンドルネームは既に使用されています。\n最初からやり直してください。"; 
 }
 else{
   for(let i=2; i<=sheet.getDataRange().getLastRow(); i++){
       if(sheet.getRange(i,LINEID_FIELD).getValue() == userId){
         sheet.getRange(i,USERNAME_FIELD).setValue(name);
         return name + "に変更しました。";
       }
   }
   sheet.appendRow([userId,name]);
   return name + "で登録しました。";
 }
}

function registDeck(userId,deckCode) {
 let sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName(DB_NAME);
 let nowTime = new Date();
 let formatNowTime = Utilities.formatDate( nowTime, 'Asia/Tokyo', 'yyyy/MM/dd HH:mm:ss');
 for(let i=2; i<=sheet.getDataRange().getLastRow(); i++){
   if(sheet.getRange(i,LINEID_FIELD).getValue() == userId){
     if(deckCode.match(DECK_REGEXP) != null){
       sheet.getRange(i,DECK_FIELD).setValue(deckCode);
       sheet.getRange(i,DATE_FIELD).setValue(formatNowTime);

       //ここから下を関数化したいが何故か出来なかった
       let formSheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName(FORM_NAME);
       //debug(formSheet,userId);
       let setLow = formSheet.getLastRow()+1;
       //debug (setLow,userId);
       formSheet.getRange(setLow, FORM_DATE_FIELD).setValue(formatNowTime);
       formSheet.getRange(setLow, FORM_NAME_FIELD).setValue(sheet.getRange(i,USERNAME_FIELD).getValue());
       formSheet.getRange(setLow, FORM_DECK_FIELD).setValue(deckCode);
       formSheet.getRange(setLow, FORM_LINEUSER_FIELD).setValue('LINE');

       //databaseToForm(formatNowTime,sheet.getRange(i,USERNAME_FIELD).getValue(),deckCode);
       return "デッキコードを登録しました。";
     } else {
       return "登録出来ませんでした。\n正しいデッキコードがペーストされているか確認してください。"
     }
   }
 }
 return "ハンドルネームが登録されていません。\n先にハンドルネーム登録を行ってください。" + userId;
}

function databaseToForm(time,name,deck){
 debug(deck,name);
 let sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName(FORM_NAME);

 let setLow = sheet.getLastRow+1;
 sheet.getRange(setLow, FORM_DATE_FIELD).setValue(time);
 sheet.getRange(setLow, FORM_NAME_FIELD).setValue(name);
 sheet.getRange(setLow, FORM_DECK_FIELD).setValue(deck);
 sheet.getRange(setLow, FORM_LINEUSER_FIELD).setValue('LINE');
}

function checkFormDuplicate(){
 let sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName(FORM_NAME);
 sheet.getRange("A:Z").sort({column: 1, ascending: true}).removeDuplicates([2]);
 deckCodeToURL();
}

function deckCodeToURL(){
 let sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName(FORM_NAME);
 const URL = 'https://www.pokemon-card.com/deck/thumbs.html/deckID/';
 for(let i=2; i<=sheet.getDataRange().getLastRow(); i++){
   let deckcode = sheet.getRange(i,FORM_DECK_FIELD).getValue();
   if(deckcode.indexOf(URL) == -1 ){
     sheet.getRange(i,FORM_DECK_FIELD).setValue(URL + deckcode);
   }
 }
}

function setData(data,userId) {
 let sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName(DB_NAME);
 sheet.appendRow([data,userId]);
}

// SSからデータを取得
function getData() {
 let sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName(DB_NAME);
 let data = sheet.getDataRange().getValues();

 return data.map(function(row) { return {key: row[0], value: row[1], type: row[2]}; });
}

// 画像形式でAPI送信
function sendMessageImage(replyToken, imageUrl) {
 // replyするメッセージの定義
 let postData = {
   "replyToken" : replyToken,
   "messages" : [
     {
       "type": "image",
       "originalContentUrl": imageUrl
     }
   ]
 };
 return postMessage(postData);
}

// LINE messaging apiにJSON形式でデータをPOST
function sendMessage(replyToken, replyText) {  
 // replyするメッセージの定義
 let postData = {
   "replyToken" : replyToken,
   "messages" : [
     {
       "type" : "text",
       "text" : replyText
     }
   ]
 };
 return postMessage(postData);
}

function sendConfirm(replyToken) {  
 // replyするメッセージの定義
 let postData = {
   "replyToken" : replyToken,
   "messages": [{          
           "type": "template",
           "altText": "this is a confirm template",
           "template": {
             "type": "confirm",
             "actions": [
               {
                 "type": "message",
                 "label": "新規登録",
                 "text": "新規登録"
               },
               {
                 "type": "message",
                 "label": "HN変更",
                 "text": "HN変更"
               }
             ],
           "text": "野望杯で使用するHNを登録します。"
           }
   }]
 }
 return postMessage(postData);
}

// LINE messaging apiにJSON形式でデータをPOST
function postMessage(postData) {  
 // リクエストヘッダ
 let headers = {
   "Content-Type" : "application/json; charset=UTF-8",
   "Authorization" : "Bearer " + ACCESS_TOKEN
 };
 // POSTオプション作成
 let options = {
   "method" : "POST",
   "headers" : headers,
   "payload" : JSON.stringify(postData)
 };
 return UrlFetchApp.fetch(REPLY, options);      
}

/** ユーザーのアカウント名を取得
*/
function getUserDisplayName(userId) {
 let url = 'https://api.line.me/v2/bot/profile/' + userId;
 let userProfile = UrlFetchApp.fetch(url,{
   'headers': {
     'Authorization' :  'Bearer ' + ACCESS_TOKEN,
   },
 })
 return JSON.parse(userProfile).displayName;
}

// debugシートに値を記載
function debug(text, userId) {
 let sheet = SpreadsheetApp.openById(SHEET_ID).getSheetByName('debug');
 let date = new Date();
 let userName = getUserDisplayName(userId);
 sheet.appendRow([userId, userName, text, Utilities.formatDate( date, 'Asia/Tokyo', 'yyyy-MM-dd HH:mm:ss')]);
}


スクリーンショット 2021-12-07 17.20.03

この時、SHEET_IDとACCESS_TOKENの変更が必要です。

SHEET_IDは作成したスプレッドシートのURLの/d/~~~/editの~~~をコピペ
ACCESS_TOKENは1項で確認したChannel access tokenをコピペ

スクリーンショット 2021-12-07 17.22.48


トリガーを選択し、右下の追加ボタンからトリガーを追加します。

この関数は同名ユーザーの重複提出を削除する関数です。
不要であればトリガーは作成する必要がありません。
時間の間隔は適当で大丈夫です。

スクリーンショット 2021-12-07 16.41.18


変更が終わったらデプロイします。

スクリーンショット 2021-12-07 17.27.15

スクリーンショット 2021-12-07 17.33.35

このURLをLINE Messaging APIのWebhook URLにコピペします。
Use Webhookも忘れずに。

スクリーンショット_2021-12-07_17_36_03

4.LINE公式アカウント

作成した自分の公式アカウントと友達になります。

スクリーンショット 2021-12-07 17.37.58

あとは、何か適当なメッセージを送ってみてください。
「申し訳ございません。最初からやり直してください。」
と返ってきていたら成功です。

HN登録、デッキ登録でbotが進むようになっています。
是非野望杯情報を友達登録して動作確認をば。


投げ銭置いておきますのでお気持ちだけでも…!


ここから先は

0字

¥ 100

この記事が気に入ったらサポートをしてみませんか?