つれづれなるままに日々の色々なことを綴ります

【GAS】基本的な操作メモ

GASの勉強をしています。スプレッドシートの操作をする中で出てきたメソッドや関数などを記載しています。随時更新

取得

スプレッドシートを取得にする

const ss = SpreadsheetApp.getActiveSpreadsheet();
  • スクリプトエディタがスプレッドシートに紐づいている場合はこちらを使用する
  • 紐づいていない場合は下を使う

今開いているのとは別のスプレッドシートを取得する

const ss2 = SpreadsheetApp.openById('xxxxxxxxxxxxxxxxxxxxxxx');

スプレッドシートのシートを取得する

const sheet = ss.getSheetByName('シート1');

シートのセルの記載内容を取得する

\\ A1記法
const values = sheet.getRange('A1:E3').getValues();
\\ 番地記法
const values = sheet.getRange(1, 1, 3, 5).getValues();
  • セルの指定方法にはいくつか種類がある
  • A1記法
    • 見たまんま
  • 番地記法
    • 上のコードでは、1行目1列目から3行5列分を取得している

シート上にデータがある範囲を取得する

const values = sheet.getDataRange().getValues()

getDataRange()で取得できる。 ヘッダーを取り除きたいなどはこの関数は向かない。

セル内が空白になっている最終行番号を取得

const lastRow = sheet.getLastRow();

書き込み

スプレッドシートの最終行のセルに書き込む

sheet.appendRow(dataArray);

appendRowで空白ではない最終行を取得してセルに値を記入する

まあでも困ったときはドキュメントだよねっ

developers.google.com

【GAS】特定のChに特定のメッセージが送信されたらお返事を返す

GASを勉強しています。忘れないように学習メモを作っています。 そのままコピペして動くみたいな記事ではないのでお気をつけを。

やったこと

  • 特定のChに特定のメッセージが送信されたら、お返事を返す

コード全文

if ( json.event.type === 'message') {
    handleEvent(json.event);
    } 

function handleEvent(event) {
  const targetChannelID = "チャンネルID"
  // メッセージが特定のチャンネルで送信されたかどうかを確認
  if (event.channel === targetChannelID && event.text.includes('ワッショイ!')) {
    // メッセージが特定の条件を満たしている場合に返信
    sendReply(event.channel, createMessage());
  }
}
function createMessage() {
  return 'ご用件をどうぞ!';
}
function sendReply(channelID, message) {
  let token = "xoxb-HOGEHOGE"
  let url = "https://slack.com/api/chat.postMessage";
  let payload = {
    "channel" : channelID,
    "text" : message,
     };
  let options = {
    "method" : "post",
    "contentType" : "application/json",
    "headers": {"Authorization": "Bearer " + token},
    "payload" : JSON.stringify(payload)
  };
  UrlFetchApp.fetch(url, options);
}
  • 送られてきたJsonのeventのtypeがmessageだったらhandleEventを呼び出しています。
  • messageイベントを受け取るためには、Event Subscriptionsでmessage.channelsを登録してあげる必要があります。
  • messageイベントで送られてくるリクエストには本文テキストが含まれているので、指定の文字が含まれているときにお返事を返すようにしてあげます。

つまづいたこと

  • 当初、Event Subscriptionsに頼らない形の実装を試みていました。
  • GASのトリガーにて、1分おきにチャンネルヒストリーを読み込み、指定の文字列があれば返事を出すというものです。しかし下記の点でつまづき、取りやめました。
    • 最大1分の遅延が発生する
    • 過去送信されたすべての指定の文字列に対して返事をしてしまう
      • こっちは対処法も考えてみましたが、うまく作動せず…

api.slack.com

まとめ

指定のメッセージが送信されたというのをどう感知するかが悩ましいポイントでした。

【GAS】Slackのスラッシュコマンドでユーザーを選択し、ユーザーIDを返事させる

GASを勉強しています。忘れないように学習メモを作っています。 そのままコピペして動くみたいな記事ではないのでお気をつけを。

やったこと

  • Slack上でスラッシュコマンド入力後、モーダルが開き、ユーザーを選択し送信すると選択したユーザーのユーザーIDが送信されてくる。
  • ユーザーは複数選択が可能

コード全文

const doPost = (e) => {
  const parameter = e.parameter
  try {
    if(parameter.payload) {
      const payload = JSON.parse(decodeURIComponent(parameter.payload))
      if (payload.type === "view_submission") {
      slackID(payload);
      }
    } else {
      if (SLACK_TOKEN != parameter.token) throw new Error(parameter.token);
      if (parameter.channel_id !== ALLOWED_CHANNEL_ID) {
      return ContentService.createTextOutput("このテストコマンドは指定のチャンネルでのみ作動するようになっています。すみません!") }
      // モーダルを開くようにSlackへリクエスト
      return openModal(parameter);
    }
  } catch(error) {
    return ContentService.createTextOutput(403)
  }
  return ContentService.createTextOutput();
}

const openModal = payload => {
  const modalView = generateModalView()
  const viewData = {
    token: SLACK_ACCESS_TOKEN,
    trigger_id: payload.trigger_id,
    view: JSON.stringify(modalView)
  }
  const postUrl = 'https://slack.com/api/views.open'
  const viewDataPayload = JSON.stringify(viewData)
  const options = {
    method: "post",
    contentType: "application/json",
    headers: { "Authorization": `Bearer ${SLACK_ACCESS_TOKEN}` },
    payload: viewDataPayload
  }

  UrlFetchApp.fetch(postUrl, options)
  return ContentService.createTextOutput()
}

//モーダル入力サブミットしたときの処理
function slackID(payload) {
  const selectedUsers = payload.view.state.values.lvIf8.selectusers.selected_users;

  const channel_id = 'チャンネルID';
  const token = "xoxb-HOGEHOGE";
  const message = `選択したユーザーIDをお返しします。 ${selectedUsers.join(", ")}`;
  sendMessageTochannel(token, channel_id, message);
}

function sendMessageTochannel(token, channel_id, message){
  const url = 'https://slack.com/api/chat.postMessage';
  const options = {
    'method': 'post',
    'contentType': 'application/json',
    'headers': {
      'Authorization': 'Bearer ' + token
    },
    'payload': JSON.stringify ({
      'channel': channel_id,  // 返答を送信するチャンネルのID
      'text': message,         // 送信するメッセージ内容
    })
  };

  // Slack APIへリクエストを送信
  UrlFetchApp.fetch(url, options);
}



/**
 * モーダルBlocks
 */
const generateModalView = () => {
  return {
    "type": "modal",
    "title": {
        "type": "plain_text",
        "text": "My App",
        "emoji": true
    },
    "submit": {
        "type": "plain_text",
        "text": "Submit",
        "emoji": true
    },
    "close": {
        "type": "plain_text",
        "text": "Cancel",
        "emoji": true
    },
    "blocks": [
        {
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": "ユーザーを選択してください (複数選択可能)"
            },
            "accessory": {
        "action_id": "selectusers",
                "type": "multi_users_select",
                "placeholder": {
                    "type": "plain_text",
                    "text": "Select options",
                    "emoji": true
                },
        }
      }
    ]
  }
}

事前準備

  • Slack上でスラッシュコマンドを使用するには、Interactivity & ShortcutsにデプロイしたアプリのURL登録とSlash Commandsに使用するコマンドの登録が必要です。
    • コマンドの登録の際にもRequest URLを求められますがInteractivity & Shortcutsに登録したものと同じものを使います

doPostで受け取る

  • 自分が作成したSlackアプリかつ指定のチャンネルだったときにモーダルを開く用にSlackへリクエストを投げます。

モーダルを開きます。

const openModal = payload => {
  const modalView = generateModalView()
  const viewData = {
    token: SLACK_ACCESS_TOKEN,
    trigger_id: payload.trigger_id,
    view: JSON.stringify(modalView)
  }
  const postUrl = 'https://slack.com/api/views.open'
  const viewDataPayload = JSON.stringify(viewData)
  const options = {
    method: "post",
    contentType: "application/json",
    headers: { "Authorization": `Bearer ${SLACK_ACCESS_TOKEN}` },
    payload: viewDataPayload
  }

  UrlFetchApp.fetch(postUrl, options)
  return ContentService.createTextOutput()
}
  • views.openを使用するにはtokneのほかにtrigger_idとviewが必要です。ここでは、generateModalViewで記載したViewの中身をJSON.stringifyで文字列としています。

api.slack.com

モーダルから送られてきた内容を受け取る。

  • モーダルから送られてきた中身には、type : view_submissionが含まれています。それが含まれているかを判定して、含まれていたらSlackIDという関数に受け渡しています。
  • 送られてきたjsonのselected_usersに選択したユーザーのIDが含まれているので、これを返事の中に含ませます。
const message = `選択したユーザーIDをお返しします。 ${selectedUsers.join(", ")}`;
  • この一文に地味に苦しんだ…。 (初心者)${ }を使うことで、変数の値を出力できるようになります。が、バッククオートで閉じなければならないというところになかなか気付けず、数敗。

Block Kit Builder… なんて甘美な調べ

  • モーダルのデザインはSlack側が用意してくれているBuilderを使うことで簡単に組み立てが出来ます。
  • 使いたい要素をポチポチ足していくだけ。
  • 入力欄にユーザーの候補を出すようにするためには"type"を"multi_users_select"にする必要があります。
  • 実際に触ってみたほうが多分楽しい。

app.slack.com

まとめ

楽しいです。

参考記事

zenn.dev

【GAS】Slackの特定のChで発言に特定のスタンプがついたときに、その発言をスプレッドシートに書き出す

GASの勉強中です。自分用に学んだことをメモする用の記事です。 とりあえず動けばいいの精神なのでコードのきれいさには目を瞑ってもらえると…。

自分用なので、そのままコピペしても動かないよ、多分。

やったこと

特定のChで特定のスタンプが付いたときに、その発言をスプレッドシートに書き出す

コード全文

function doPost(e) {
  // Event API Verification 時のコード
  try {
    const json = JSON.parse(e.postData.getDataAsString());

    if (json.type == "url_verification") {
      return ContentService.createTextOutput(json.challenge);        
    }
  }
  catch (ex) {
    Logger.log(ex);
  } 

    const json = JSON.parse(e.postData.getDataAsString());
    
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    const sheet = ss.getSheetByName("シート1");
    sheet.getRange("F2").setValue(json);

    
    // 特定のチャンネルIDと絵文字
    const targetChannel = "チャンネルID"; // 対象のチャンネルIDを設定
    const targetReaction = "balance"; // 対象の絵文字を設定

    sheet.getRange("F3").setValue(json.event.type);

    // targetChannelとtargetReactionが一致する場合にのみ、getMessage関数を呼び出す
    if (json.event.type === 'reaction_added'){
      const channel = json.event.item.channel; // チャンネルIDを格納
      const ts = json.event.item.ts; // タイムスタンプを格納
      const reaction = json.event.reaction; // リアクションの絵文字を格納
        if (channel === targetChannel && reaction === targetReaction){
          getMessage(ts, channel);
        }
    }
  
    if ( json.event.type === 'message') {
    sheet.getRange("F4").setValue(json);
    handleEvent(json.event);
    } 

    sheet.getRange("F7").setValue("処理終了");
  }




function getMessage(ts, channel) {
  const url = "https://slack.com/api/conversations.replies";
  const slack_app_token = "xoxb-HOGEHOGE"; // トークンを設定
  const limit = 10;
  const options = {
    "method": "get",
    "headers": {
      "Authorization": `Bearer ${slack_app_token}`
    },
    "payload": { 
      "channel": channel,
      "ts": ts
    }
  };
  

  const response = UrlFetchApp.fetch(url, options);
  const json = JSON.parse(response.getContentText());
  const text = json.messages[0].text;
  const baseUrl = "https://HOGEHOGE.slack.com/archives/";
  const messageLink = `${baseUrl}${channel}/p${ts.replace('.', '')}`;
  const date = new Date();
  const channelname = "#CHのお名前"
  
  
  SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().appendRow([date, text, messageLink, channelname]);
}

分解して詳しく見ていく

それでは下記でコードを分解して考えてみましょうね。

Event SubscriptionsとdoPost関数での疎通確認

  // Event API Verification 時のコード
  try {
    const json = JSON.parse(e.postData.getDataAsString());

    if (json.type == "url_verification") {
      return ContentService.createTextOutput(json.challenge);        
    }
  }
  catch (ex) {
    Logger.log(ex);
  } 
  • GAS上でデプロイすると、WebアプリURLが発行されます。それをSlack apiのEvent SubscriptionsのRequest URLに登録する必要があります。登録する際にSlackから検証リクエストが送信されます。
  • 新しくデプロイするとURLが変わってしまうので登録のし直しが必要。
  • ただし、デプロイの管理を選ぶことでURLを変えずにデプロイすることが可能です。
  • 上記の検証リクエストやイベントが起こったときは上記で登録したURLにリクエストが送られます。
  • doPostはWebアプリにPOSTリクエストが送られたときに実行される関数。リクエストは下記が含まれた形で送られてくる。
{
"token": "example-verification-token",
"challenge": "random-string-challenge",
"type": "url_verification"
}
  • if (json.type == "url_verification")で、受け取った上記のJSONのtypeがurl_verificationだったらjson.challengeの内容を返すことで検証が完了する。

reactionをSlack上で付けたよというリクエストをJSON.parseで解析する。

const json = JSON.parse(e.postData.getDataAsString());
        
    // 特定のチャンネルIDと絵文字
    const targetChannel = "チャンネルID"; // 対象のチャンネルIDを設定
    const targetReaction = "balance"; // 対象の絵文字を設定

    // targetChannelとtargetReactionが一致する場合にのみ、getMessage関数を呼び出す
    if (json.event.type === 'reaction_added'){
      const channel = json.event.item.channel; // チャンネルIDを格納
      const ts = json.event.item.ts; // タイムスタンプを格納
      const reaction = json.event.reaction; // リアクションの絵文字を格納
        if (channel === targetChannel && reaction === targetReaction){
          getMessage(ts, channel);
        }
    }
  • 届いたリクエストの内容はe.postData.getDataAsString()にJSON.parse()をかけることでObject形式で得ることができる。
  • reaction_addedをゲッチュするため、忘れずにSlack appのSubscribe to bot eventsでイベントを追加しておきます。
  • 取り出したデータをjsonで保持し、そこから更にChannleやts、reactionで分解して情報を保持します。
  • targetChannelとtargetReactionが一致する場合にのみ、getMessage関数を呼び出します。
  • json.event.item.channelは下記画像の赤丸を取って来いってことですね。
  • 詳しくは公式のドキュメントを見てください

reaction_addedで送られてくるイベントの例

api.slack.com

getMessage関数でmessage内容を取得する

 const url = "https://slack.com/api/conversations.replies";
  const slack_app_token = "xoxb-HOGEHOGE"; // トークンを設定
  const limit = 10;
  const options = {
    "method": "get",
    "headers": {
      "Authorization": `Bearer ${slack_app_token}`
    },
    "payload": { 
      "channel": channel,
      "ts": ts
    }
  };


  const response = UrlFetchApp.fetch(url, options);
  • 受け取ったイベントデータのChannelIDとtsを使用してSlackAPIのconversations.repliesにリクエストを送信し、メッセージ内容を取得します。
  • conversations.repliesを使用するにはtoken,channel,tsが必要です。そのため、PayloadにChannleとtsを持たせてあげます。
  • UrlFetchAppは外部のAPIやサイトにHTTPリクエストを送るためのGASのメソッドです。

api.slack.com

さらにJSON.parseで解体。スプレッドシートに書き出す。

const json = JSON.parse(response.getContentText());
  const text = json.messages[0].text;
  const baseUrl = "https://HOGEHOGE.slack.com/archives/";
  const messageLink = `${baseUrl}${channel}/p${ts.replace('.', '')}`;
  const date = new Date();
  const channelname = "#CHのお名前"
  
  
  SpreadsheetApp.getActiveSpreadsheet().getActiveSheet().appendRow([date, text, messageLink, channelname]);
}
  • const responseで帰ってきたものをJSON.parseで解体します。
  • Slackのメッセージのリンクは"https://HOGEHOGE.slack.com/archives/"というベースとなるURLにチャンネルIDとタイムスタンプが合体したものになります。(HOGEHOGEにはワークスペースのお名前がはいります。)
  • 最後にappendRowでシート最終行へ書き出しています。

まとめ

動かないなと思ったら、こまめにJSONがどこまで来てるかなど吐き出すのが大事かなと思いました。

HHKB Studio買ったよ。

買ったよ。

いつか欲しかったHHKB

社会人になるとき、キーボードを新調しようということで、メカニカルキーボードを手放し、静電容量無接点に手を出しました。その際HHKBかREALFORCEかで悩み、あんまり特殊配列すぎるのもな…ということで、REALFORCE R3を購入した、という経緯があります。HHKBに関してはいつか欲しいな、どうせなら雪typeみたいな真っ白いやつがいいなあと思いを馳せていたぐらいです。

そんなんで、REALFORCEを使っていましたが、今の職が想像以上にタイピング量が多く、そして利き手でない右手でのマウス操作に右手首が死亡。色々とキーボードだけで完結させたい欲が増加しました。 そんな折に見てしまったわけです、HHKB Studioの情報を。

というわけで使って思ったことをつらつらと書いていこうと思います。

項目としては大体以下を予定しています。

前提

  • HHKBはHHKB Studioがはじめて
  • ここ1年半ぐらいはずっとREALFORCE
  • ポチボタン式もはじめて

打鍵感

普通、HHKBといえば静電容量無接点式ですが、今回Studioではメカニカルキーボードになりました。事前レビューや界隈の反応を見ている限り、静電容量無接点じゃなくなったことに対するネガが多かったように感じます。

実際の打鍵感はかなり好みでした。REALFORCEを30gとだいぶ軽いのを買っていたということもあり、久々にある意味押しごたえがある、それでいて静かというスコスコ感がかなり良い。

旧来のHHKBとの比較はちょっと難しいですが、個人的には全然アリの押し心地です。良きかな。

ポインティングスティック

ThinkPad的なポチ初挑戦...! ということで、まずスティックを倒すとカーソルが移動する感動から味合わせていただきました。合掌。これがマウスいらずってことかあ〜!!

ポチはちょっと自分には重たいかなという印象です。でもあまり感度良すぎても誤爆が発生するだけですし、これがちょうどいいのかも。 傾ける速度?によってカーソルの速度がかわります。ちょっとした微調整が難しい。たとえば小さいセルのプルダウンを選ぼうとするときとか。ショートカットをはやく身体に叩き込みたい。

サイドバー

ぶっちゃけあんまり使っていない。というより、サイドバーに出張するのすら億劫という有り様。 活用法を考えていますがあんまり思いつかず…

持ち運びやすさ

HHKB Studioの重さは、従来のHHKBより300gほど重いらしい800g程度です。重くなったことを憂いてる方もいらっしゃいましたが、個人的には、REALFORCE R3の1.3kgより軽くなったので、あまり気になりません。

専用ケースがあまりまだ充実していないのが辛いところです。お高めでしたが、専用キャリングケース買っちゃった…

総評

個人的には全体的に満足しています。カスタマイズ性の高さや、ポインティング・スティックなど、他のキーボードには見られない楽しさが詰まっているかなと!

2024.1 ゲーム感想書き散らし

ゲームの感想を書き散らして1年が経ちました。今年もつらつらと書き綴ればと思います 2024年1月は、PS5で遊ぶことが多かったです。

Power Wash Simulator

前々から気になっていたこのゲーム・・・。やっっっっっと買いました。

ゲームとしてはただただ汚いものをひたすら高圧洗浄機できれいにしていくというだけのゲームです。 何も考えたくないときにはピッタリですが、ある意味めちゃくちゃ単調なので、そこそこ眠気もやってきます。

CUPHEAD

秒で挫折しました。昔、友人とやったことがあり、もう一度挑戦してみましたがやはりだめですね・・・。こういう即死系だったり初見殺しみたいなゲームは全くと言っていいほどできません。

音楽とかは好きなんですけどね・・・。

THE CREW MOTORFEST

オープンワールド車ゲームです。フォルツァホライズンと系統としては一緒だなという感じでした。 ハワイが舞台となっており、好きな車でオープンワールドを走り回れる楽しさは文句なしです。

日本をテーマにしたレースがあるんですが、モチーフが龍で、それはどちらかというと中国では・・・?と思いましたが、まあそれはご愛嬌です。 レース名が湾岸スピリッツとか峠スピリッツとかで笑いました。

ヒューマンフォールフラット

(クリアしたのは12月ですが、12月にブログを書き損じたので・・・。)

PC版を持っているのですが、改めてPS5版を買い、ノーマルステージは全部制覇しました。

一人ではなく、二人で遊んだのですが、二人だと一部ギミックを「パワー!」で乗り越えることもできてしまいますが、それはそれでおもしろいですね。

It Takes Two

これもまたクリアしたのは12月・・・。 2回めクリアです。久々にやりましたが、やはり面白いですねこれ。途中どうしても長いステージや既視感のあるギミックで中だるみもしますが、ストーリーや演出よく10時間ほどでクリアまでたどり着きました。

アクションとしてはそこそこ難しい気がするので、ある程度プレイスキルが一緒ぐらいの人だと楽しめるかもしれない。

おわりに

最近、どうしても車のゲームがしたくってPSストアをずっと眺めています。ただ、レースは疲れるので、のんびりと走るやつがいいなあと・・・。 3月ぐらいにタクシーのゲームが出るので、気になっています。

【仕事納め2023】下っ端PMもどきの1年間の振り返り

振り返り大事だって。

導入

とあるプロジェクトのしがない下っ端PMもどきをやっています。新卒として昨年11月末に配属されてからちょうど1年経ちました。 感じたことや来年に活かせる振り返りを行っていきたいと思います。

振り返りには、KPTと呼ばれる手法から、見出しだけを拝借してきました

KEEP : 良かったこと、継続すべきこと

  • 重たい仕事も任せてもらえることが多く、いろいろな経験が積めた
  • 会議のファシリや大勢の前での発言への抵抗が配属当時より薄まった
    • (が、無くなったわけではない…)
  • 社内に公開している個人メモにPMに関するまとめページを作れたこと
  • 仕事を通して得た知見をまとめるメモを作った
    • 暗黙知、秘伝タレ知識を文書化してまとめておきたい
  • 仕事の指針を立てられた
    • 2023の指針は以下の通り
      • 思想のない仕事をしない
      • 自分がコストを払うのを躊躇わない
      • 柔軟である
      • 誰かにとって価値のないアウトプットをしない

PROBLEM : 改善すること

  • 一人で悩みすぎて、次のアクションに出るまでが時間がかかりすぎる
    • 先輩からも一人で悩み過ぎという指摘をいただいた
  • 先輩方に話しかけることにハードルを感じてしまい、気楽に相談という流れをなかなか踏めなかった
  • タスクが渋滞し、常時タスクリストがいっぱい
    • 職種柄会議に出ることが多いが、会議に出るたびに差し込みタスクが増え、当初の予定タスクをなかなか捌ききれない
    • 上述の通り、先輩たちとのコミュニケーションにそこそこのコストを感じており、話しかけるのに時間がかかり、手元でボールを温めすぎてしまう
  • チーム内からのお気持ちに過剰に反応しすぎてしまい、精神を疲弊することが多かった
  • 日々の流動的なタスクに忙殺されてしまい、腰を据えて取り組みたいと思っていた長期的な改善タスクに取り組めなかった
    • 日々のタスクの時間配分を見直したほうが良い
  • 抽象的で曖昧な表現や発話に逃げてしまうことがあった
    • 自分の発言の重みに耐えられなかった
  • 自分がコストを払うのは躊躇わないという指針を立てていたが、これに少々苦しめられた
    • モチベが高いときは問題ないように感じるが、俯瞰してみたときに自分の仕事を無闇矢鱈に増やす結果になった
    • 自分がコストを払うべきタイミングの見極めが甘く、結果としてモチベの低下に繋がった
    • モチベに依存する仕事の仕方をしてしまった
      • (モチベという人間に左右されるものに依存して仕事をしているのがそもそも問題という思考)

TRY : 次のアクション

  • 悩みすぎる前に他者にアドバイスを求める
  • コミュニケーションのハードルを感じてしまっている理由を分析し、ハードルを下げる方法を模索する
  • タスクリストの活用
    • 長期的/短期的タスクをロストしないように
  • 長期的な改善タスクに関して進め方を検討する

締めくくり

仕事としても大きな節目を年であり、いろいろと思い悩む日々も多かった1年でした。 TRYに関してはもう少し熟考して、解像度を上げていきたいと思っています。 本年もお疲れ様でした。