3
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

GAS + slack で褒めあえる仕組みを作る

Posted at

TL;DR:

実装要件

  • メンバー同士で定期的にお互いを褒め合える(トロフィーを送る)
  • メンバーみんなが褒められた人となぜ褒められたかを認知できる

仕様

  • slack で対象者を指定してトロフィー贈与(現トロフィー所有者からしか贈れない)

send toPerson reason

fromPerson(メッセージ投稿者) から toPerson に reason によりトロフィーが授与されました!

  • slack でトロフィー履歴が確認できる

history

受賞日 誰から 誰に 理由
YYYY/MM/DD 織田信長 豊臣秀吉 仇を討ってくれた
YYYY/MM/DD 豊臣秀吉 徳川家康 家臣になってくれた
  • トロフィー履歴はAPIで管理
  • 一定周期毎にトロフィー保持者に次のトロフィー贈与を促す

実装

トロフィー履歴は GoogleSpeadSheet を用いる

まずは GoogleSperedSheet を用意しましょう

GAS1.jpg

ここに受賞履歴を格納していく形にします

GASのプロジェクトを作成する

そして、GASのプロジェクトを作成します。

image.png

GAS上で指定のスプレッドシートを利用するには、スプレッドシートを開いた際のURLのキーを指定すれば、利用できます(https://docs.google.com/spreadsheets/d/XXXXXXXXX/~)

example.gs
var spreadsheet = SpreadsheetApp.openById('XXXXXXXXX')
var sheet_name = 'hoge'
var sheet = spreadsheet.getSheetByName(sheet_name);

sheet.getRange(1, 3).getDisplayValue()

slackにメッセージを送りつけるWebhookを用意する

Slackにて用意してくれているAPI Incoming Webhook を用います。GAS -(Incoming Webhook)-> slack の部分です。

image.png

設定で投稿するチャネルなどを指定します。WebHook URL は後でGASで設定で使用します。
GAS6.jpg

slackとメッセージをやり取りするメソッドを作成する

メッセージを受取り、ごにょごにょした後にメッセージを返すメソッドをGASに用意します。
下記では、受け取ったメッセージのパラメータ(ユーザー名、入力文字列)をslackにメッセージとして返す部分だけをまず作っています。
最後、メッセージを送る際のURLに上記で用意した Incoming Webhook URL を指定しています。

example.gs
function doPost(e) {
  // 送信
  var options =
      {
        "method" : "post",
        "contentType" : "application/json",
        "payload" : JSON.stringify(
          {
            "text" : e.parameter.user_name + 'から [' + e.parameter.text + ']というメッセージを受け取ったよ'
          }
        )
      }
  UrlFetchApp.fetch('https://hooks.slack.com/services/XXXX(incoming webhook URL)', options);
}
    

Webアプリケーションとして公開する

GASをWebアプリケーションとして公開しましょう。GASのメニューから公開処理を行います。

image.png

GAS7.jpg

表示されるURLが、受け取り側のURLとなります。次で使います。

slackからメッセージを受取るWebhookを用意する

こちらもSlackにて用意してくれているAPI Outgoing Webhook を用います。slack -(Outgoing Webhook)-> GAS の部分です。

image.png

設定で、引き金となる言葉や、対象のチャネルの他、キックするURLとして、GASで設定したURLを指定します。

GAS4.jpg

ここでは、引き金となる言葉として、fumiを設定しました。上記の仕様と合わせると、送るメッセージは、
fumi send toPerson reason
という様なイメージになります

起きた課題

slackでトロフィーを授与する際に、UXを考慮してメンション形式で指定させることにしようとしましたが・・・

image.png

メンションだと、パラメータとしてGAS側で渡されるユーザー名はslackのuser_idになってしまいます。

image.png

@trello で送って "trello" を期待していたら、trelloのuser_idの<@XXXXXX>で届いてしまう。

しょうがないのでuser.infoを取得し名前をひき直すことにしました。

slack-apiを作成

image.png

Create New App から AppName等を設定をして Permissions を選択

image.png

Scpoesでユーザー情報を取得する users:read を指定

image.png

許可します

GAS2.jpg

OAuthのトークンが取得できるのでそちらをコピーしておきます

GAS3.jpg

GAS側の処理

GAS側はこんな感じで引き渡されたslack_uidからusers.infoを呼び出してuserinfo.user.profileを取得し名前を取得します
cf) users.info

example.gs
function getUserName(slack_uid){
  try{
    // slack_uidからidのみを取得する
    slack_uid = slack_uid.replace('<','').replace('>', '').replace('@', '')
    
    var geturl = 'https://slack.com/api/users.info?token=' + OAuthのトークン + '&user=' + slack_uid + '&pretty=1'
    var ret = UrlFetchApp.fetch(geturl);
    var userinfo = JSON.parse(ret.getContentText());
    if(!userinfo.ok){
      throw new Error("Invalid User!!")
    }
    return userinfo.user.profile.display_name
  } catch(e){
    throw e
  }
}

最終的にはこんな感じにGAS側でコードを用意しました。

example.gs
// spreadsheet 関係
var spreadsheet = SpreadsheetApp.openById('XXXXXXX')
var sheet_name = 'trophies'
var sheet = spreadsheet.getSheetByName(sheet_name);
// slack - outgoing関係
var ogtoken = 'YYYYYYYY'
var ogurl = 'https://hooks.slack.com/services/ZZZZZZZZ'
// slack - userget関係
var ugtoken = 'xoxp-111111111111111111111'

function doPost(e) {
  try{
    var sendmessage = ''
    var title = ''
    
    var text = e.parameter.text.split(' ')
    if(text[1] == 'history'){
      title = '**** トロフィーヒストリー ****\n'
      sendmessage = title + getHistory()
      
    } else if(text[1] == 'send'){
      title = '**** トロフィー贈与!! ****\n'
      
      if(text.length != 4) {
        sendmessage = 'パラメタがおかしいよ!\nfumi send [to] [reason]'
      } else {
        // 贈り手チェック
        if(e.parameter.user_name != getHolder()){
          sendmessage = '今のトロフィー所有者以外は贈れないよ\n現所有者 = ' + getHolder()
        } else {
          // 記録
          setHistory(e.parameter.user_name, text[2], text[3])
          sendmessage =  title + e.parameter.user_name + 'から <' + text[2] + '> に 『' + text[3] + '』という理由でトロフィーが送られました!おめでとう!!\n' 
        }
      }
    } else if(text[1] == 'usage' || text.length <= 1){
      title = '**** usage ****\n'
      
      sendmessage = title + 'fumi history            : トロフィーヒストリーを表示するよ\nfumi send [to] [reason] : トロフィーを[reason]の理由で[to]に送るよ'
    }
  } catch(err){
    sendmessage = sendmessage + '\n' + err.message
  } finally {    
    // 送信
    var options =
        {
          "method" : "post",
          "contentType" : "application/json",
          "payload" : JSON.stringify(
            {
              "text" : sendmessage
            }
          )
        };
    
    UrlFetchApp.fetch(ogurl, options);
  }
}

function getHistory(){
  try{
    var startrow = 2
    var rangerow = 5
    if (sheet.getLastRow() > rangerow){
      startrow = sheet.getLastRow() - rangerow
    }
    var bufarr = sheet.getRange(startrow, 1, rangerow, 4).getDisplayValues()
    var mem = new Array()
    
    for(var i = 0; i < bufarr.length; i++){
      mem.push('|' + bufarr[i][0] + '| ' + bufarr[i][1] + ' ==> ' + bufarr[i][2] + '' + bufarr[i][3])
    }
    return(mem.join('\n'))
  } catch(err){
    throw err
  }
}

function setHistory(fromn, ton, reason){
  try{
    var row = sheet.getLastRow()+1
    var uname = getUserName(ton)
    
    sheet.getRange(row, 1).setValue(Utilities.formatDate(new Date(), "JST", "yyyy/MM/dd"))
    sheet.getRange(row, 2).setValue(fromn)
    sheet.getRange(row, 3).setValue(uname)
    sheet.getRange(row, 4).setValue(reason)
  } catch(err){
    throw err
  }
}

function getHolder(){
  try{
    return sheet.getRange(sheet.getLastRow(), 3).getDisplayValue()
  } catch(err){
    throw err
  }
}

function getUserName(slack_uid){
  try{
    slack_uid = slack_uid.replace('<','').replace('>', '').replace('@', '')
    
    var geturl = 'https://slack.com/api/users.info?token=' + ugtoken + '&user=' + slack_uid + '&pretty=1'
    var ret = UrlFetchApp.fetch(geturl);
    var userinfo = JSON.parse(ret.getContentText());
    if(!userinfo.ok){
      throw new Error("指定ユーザーが不正です")
    }
    return userinfo.user.profile.display_name
  } catch(err){
    throw err
  }
}

動かしてみます。

fumi history
GAS8.jpg

fumi send toPerson reason
GAS9.jpg

トロフィー所有者じゃない人がトロフィーを送った場合
GAS10.jpg

とりあえず、Slack <> GAS間でメッセージをやり取りし、データを GoogleSpreadSheet に登録するところは動くようになりました。

一定周期毎にトロフィー保持者に次のトロフィー贈与を促す

トロフィーは1週間位で定期的にメンバーを移動するようにしたいです。そのため、トロフィー所有者にslackから通知が行くような機能を用意したいです。この実装にはGASのトリガー機能を使えば簡単にできそうです。

先にトリガーでキックされるメソッドを用意しておきます。ここではトロフィーが贈られてから1週間経ったら通知がいくようにします。

example.gs
function noticeHolderChange(){
var termdate = 7 
  try{
    var sendmessage = ""
    var presenddate = new Date(sheet.getRange(sheet.getLastRow(), 1).getDisplayValue())
    var nowdate = new Date()
    if((nowdate - presenddate)/86400000 > termdate){
      var presendmember = sheet.getRange(sheet.getLastRow(), 3).getDisplayValue()
      sendmessage = "<@" + presendmember + "> そろそろ次の人にトロフィーを送ろう!!\nfumi send [to] [reason]"
    }
  }catch(err){
    sendmessage = err.message
  }finally{
    if(sendmessage != ""){
      var options =
          {
            "method" : "post",
            "contentType" : "application/json",
            "payload" : JSON.stringify(
              {
                "text" : sendmessage
              }
            )
          };
      
      UrlFetchApp.fetch(ogurl, options);
    }
  }
}

次にGASのトリガーを登録します
image.png

上記で用意したメソッドを指定し、1日1回起動されるようなトリガーの設定で用意しました。
GAS11.jpg

試してみます。
GoogleSpreadSheet の中身はこんな状態
image.png

今日は2019/10/3です。トリガーを仕掛けておいて出力を確認しました。
image.png

まとめ

  • おおよそやりたかったことは簡単にできた。
  • GAS凄く便利。Excelとかが未だに使われているけど、共有やAPI連携で柔軟なのでもっと使っていきたい。
  • herokuとかAWSとかでアプリ用意するより気軽にできる。でもセキュリティ系はやや注意。仕事で使う場合はしっかり設定すべし。
3
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?