Briswell Tech Blog

ブリスウェルのテックブログです

ChatGPTでslack自動投稿botを作ってみた

昨年末に2回ほど、GAS & slackの記事を書かせていただきました。

GAS & SlackでMeetのリンク自動生成Botを作ってみた - Briswell Tech Blog GAS & Slackで地震発生時の安否確認Botを作ってみた - Briswell Tech Blog

私自身プログラマではないため、記事を書くにあたってスクリプトの作成が肝だったりします。
というよりスクリプト組めなくて諦めた記事がいくつかあったりします。。。

そこで、今回は最近流行りのChatGPTを使うことで、この悩みが解消されるのでは?と思い実際にやってみた次第です。
プログラムとかよくわからないけど、ChatGPTを使って色々自動化したいなんて人の参考になれば幸いです。

今回実装したのは、定期投稿botで投稿条件は以下となります。
* 毎週木曜日にslackの特定のチャンネルに投稿する
* 木曜日が祝日だった場合は水曜日に投稿する
* 年末年始(12/28 ~ 1/3)は投稿しない
※今回の目的はChatGPTでどこまでできるかなので簡易的な要件を選んでいます。

ChatGPTでコード生成(失敗例)

今回はさっそくスクリプトの生成から行なっていきます。 ChatGPT ChatGPTにスクリプトを組んでもらうため、仕様(やりたいこと)を伝える必要があります。
実際にChatGPTに投げた内容は以下になります。

以下の要件を網羅したgoogle apps scriptのScriptを作成してください
なお、VERIFICATION_TOKENとBOT_USER_OAUTH_TOKENは既に取得済みであるものとする
本処理はgoogle apps scriptの日次トリガーで実行されるものとする
祝日の取得には以下のカレンダーIDを使用すること
ja.japanese#holiday@group.v.calendar.google.com

分岐1: 実行日が年末年始(12月28日 ~ 1月3日)ではない
 yes → 分岐2へ
 no → 処理終了

分岐2: 実行日の曜日を確認する
 実行日が水曜日である → 分岐3へ
 実行日が木曜日である → 分岐4へ
 上記以外の曜日である → 処理終了

分岐3: 実行日翌日が祝日である
 yes → 分岐4へ
 no → 処理終了

分岐4: 実行日が祝日である
 yes → 処理終了
 no → slackの特定のチャンネルへ通知を出す

通知内容は以下とする
「<!channel>\nおはようございます。\n退社までに本日分までの工数入力をお願いします。」

そして実際に作成されたスクリプトがこちらです。

function myFunction() {
  const VERIFICATION_TOKEN = "XXXXXXXXXX";
  const BOT_USER_OAUTH_TOKEN = "xoxb-XXXXXXXXXX";
  const CHANNEL_ID = "XXXXXXXXXX";
  const CALENDAR_ID = "ja.japanese#holiday@group.v.calendar.google.com";
  
  const now = new Date();
  const yearEndStart = new Date(now.getFullYear(), 11, 28); // 12月28日
  const newYearEnd = new Date(now.getFullYear() + 1, 0, 3); // 1月3日
  const isYearEnd = now >= yearEndStart && now <= newYearEnd;
  const dayOfWeek = now.getDay();
  
  if (!isYearEnd) {
    if (dayOfWeek === 3) { // 水曜日
      const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
      const calendar = CalendarApp.getCalendarById(CALENDAR_ID);
      const events = calendar.getEventsForDay(tomorrow);
      const isTomorrowHoliday = events.length > 0;
      
      if (isTomorrowHoliday) {
        sendSlackMessage("<!channel>\nおはようございます。\n退社までに本日分までの工数入力をお願いします。", VERIFICATION_TOKEN, BOT_USER_OAUTH_TOKEN, CHANNEL_ID);
      }
    } else if (dayOfWeek === 4) { // 木曜日
      sendSlackMessage("<!channel>\nおはようございます。\n退社までに本日分までの工数入力をお願いします。", VERIFICATION_TOKEN, BOT_USER_OAUTH_TOKEN, CHANNEL_ID);
      }
    }
  }
}

function sendSlackMessage(message, verificationToken, botUserOauthToken, channelId) {
  const url = "https://slack.com/api/chat.postMessage";
  const headers = {
    "Content-type": "application/json",
    "Authorization": "Bearer " + botUserOauthToken,
  };
  const payload = {
    "text": message,
    "channel": channelId,
  };
  const options = {
    "method": "post",
    "headers": headers,
    "payload": JSON.stringify(payload),
  };
  
  UrlFetchApp.fetch(url, options);
}
}

なかなか良さそうなのですが、木曜日の場合に祝日判断が抜けている。。。

ChatGPTが生成したコードに追記して完成させる

ということで、木曜日が祝日の場合にskipする処理を追記します。
追記したものがこちらになります。
※当方がプログラマではないのでコーディングに無駄があるかもしれませんがご容赦ください。

function myFunction() {
  const VERIFICATION_TOKEN = "XXXXXXXXXX";
  const BOT_USER_OAUTH_TOKEN = "xoxb-XXXXXXXXXX";
  const CHANNEL_ID = "XXXXXXXXXX";
  const CALENDAR_ID = "ja.japanese#holiday@group.v.calendar.google.com";
  
  const now = new Date();
  const yearEndStart = new Date(now.getFullYear(), 11, 28); // 12月28日
  const newYearEnd = new Date(now.getFullYear() + 1, 0, 3); // 1月3日
  const isYearEnd = now >= yearEndStart && now <= newYearEnd;
  const dayOfWeek = now.getDay();
  
  if (!isYearEnd) {
    if (dayOfWeek === 3) { // 水曜日
      const tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
      const calendar = CalendarApp.getCalendarById(CALENDAR_ID);
      const events = calendar.getEventsForDay(tomorrow);
      const isTomorrowHoliday = events.length > 0;
      
      if (isTomorrowHoliday) {
        sendSlackMessage("<!channel>\nおはようございます。\n退社までに本日分までの工数入力をお願いします。", VERIFICATION_TOKEN, BOT_USER_OAUTH_TOKEN, CHANNEL_ID);
      }
    } else if (dayOfWeek === 4) { // 木曜日
      //水曜日同様祝日判定を行う
      const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
      const calendar = CalendarApp.getCalendarById(CALENDAR_ID);
      const events = calendar.getEventsForDay(today);
      const isTodayHoliday = events.length = 0;

      if (isTodayHoliday) {
      //木曜日が祝日の場合は投稿しない
      }else{
        sendSlackMessage("<!channel>\nおはようございます。\n退社までに本日分までの工数入力をお願いします。", VERIFICATION_TOKEN, BOT_USER_OAUTH_TOKEN, CHANNEL_ID);
      }
    }
  }
}

function sendSlackMessage(message, verificationToken, botUserOauthToken, channelId) {
  const url = "https://slack.com/api/chat.postMessage";
  const headers = {
    "Content-type": "application/json",
    "Authorization": "Bearer " + botUserOauthToken,
  };
  const payload = {
    "text": message,
    "channel": channelId,
  };
  const options = {
    "method": "post",
    "headers": headers,
    "payload": JSON.stringify(payload),
  };
  
  UrlFetchApp.fetch(url, options);
}
}

スクリプトを実行してみる

実際に動作確認をしてみます。
const VERIFICATION_TOKEN = "XXXXXXXXXX";
const BOT_USER_OAUTH_TOKEN = "xoxb-XXXXXXXXXX";
const CHANNEL_ID = "XXXXXXXXXX";
上記3つのパラメータ値の設定を忘れずに行いましょう(設定方法は過去の記事を参照してください)。
GAS & SlackでMeetのリンク自動生成Botを作ってみた - Briswell Tech Blog

検証は以下の日付を指定することで水曜日や木曜日を待たずに検証できます。

「実施日」
const now = new Date(); の括弧内に日付をセットすることで該当日で検証可能です。
const now = new Date(2023, 2, 21); // 2023年3月21日で検証する場合の書き方
※月は0始まり(0 = 1月、11 = 12月)でセットします

「曜日」
if (dayOfWeek === 3) の数値を変更することで曜日を変えて検証可能です。
if (dayOfWeek === 2) // 火曜日で検証したい場合の書き方
※曜日は0始まり(0 = 日曜日、6 = 土曜日)でセットします

検証してみて、投稿ができていることを確認できました。
また、祝前日投稿や祝日に投稿しない条件もバッチリでした。

苦労した点

ここからは余談になりますが、ChatGPTでスクリプト生成で苦労した点を残しておきます。

  • 生成するたびに異なるスクリプトを組んでくる
  • 回答の文字数に制限があるのでスクリプト途中で回答が終了する(有料プラン入れってことですね)
  • 完璧に仕様を網羅できていない

ちなみに、最終版のスクリプトになるまで何度も生成を繰り返してました。
また、仕様を網羅できていない点については、ChatGPTへの渡し方(伝え方)で解決できるのかもしれません。

最後に

当初は依頼を投げたらそれっぽいスクリプトを返してくれるのですごく便利なのでは?と思っていたのですが、
実際取り組んでみると、多少のコツであったり知見(生成されたスクリプトが正しいかどうかの判断)がないと厳しいかもしれません。
とはいえ、このレベルであればわざわざプログラマの方を捕まえなくてもよくなったのは非常に良いことだと思います。
ChatGPTへの橋渡しをする職業(コミュニケーター)が今後は出てきたり資格ができたりするんでしょうか。。。

なにはともあれ、ブログの記事が捗りそうですね!

流体シミュレーション

2023年が始まったと思ったら、あっという間にもう三月。年度末は何かと忙しくなりがちですね。

コーヒーを注ぐ瞬間ってなんかほっとします。お湯をかけるときの香りや、ポタポタと落ちる音、しばし待ってからの一口... 至福です。

その至福のコーヒーの再現まではできませんでしたが、今回は、Blenderの物理演算を利用して、流体シミュレーションをしてみました。

1. 障害物:立方体

障害物の立方体を配置して、流体の受け皿とします。

・物理演算プロパティ
流体 → タイプ:エフェクターエフェクタータイプ:コリジョン

2. 障害物:円柱

障害物の円柱を配置して、蛇口のように流体の流出を調整します。

・物理演算プロパティ
流体 → タイプ:エフェクターエフェクタータイプ:コリジョン

3. フロー:球

流体の発生源のオブジェクトになります。

・物理演算プロパティ
流体 → タイプ:フロー → フロータイプ:液体 → フローの挙動:流入

4. ドメイン

流体シミュレーションを行う領域を指定します。

・物理演算プロパティ
流体 → タイプ:ドメインドメインタイプ:液体

5. アニメーション再生結果

モデルを変えることで、色々なシミュレーションができそうです。

Zoom Meeting SDKでWebシステムにZoomを埋め込む話

はじめに

コストコで売っているどデカくて甘いココアがamazonでも買えることを知り、勢いでポチってしまったid:rosoneです。60食分でした。

前回はYoutubeライブ配信をWebシステムに埋め込む記事を書きましたが、今回はその続きでZoomをWebシステムに埋め込む話になります。 背景などは前回の記事を御覧ください。

Zoom SDKの種類

2023/03現在、Webシステムで利用できるSDKは2種類存在しています。 https://marketplace.zoom.us/docs/guides/

  • Zoom Meeting SDK
    • 標準のUI/UXからカスタマイズはほとんどできないが、その分簡単に組み込むことが可能
    • 手間はかからないが、自由度が低いイメージ
  • Zoom Video SDK
    • 標準のUI/UXが準備されていないので、画面レイアウトなどイチからの実装が必要
    • 手間はかかるが、自由度が高いイメージ

今回はZoom Meeting SDKの方を使用します。

Zoom Japanの方がまとめた記事がわかりやすいので、詳細は下記を御覧ください。

qiita.com

Zoomウェビナーの補足

Zoomウェビナー用のSDKも存在するのですが、Zoomウェビナーを使うには有料ライセンスが必要なので、今回は試せていないです。 ただし、ZoomウェビナーはYoutubeライブ配信連携ができるそうなので、前回の記事と組み合わせればWebシステム上に配信を埋め込むことはできそうです。

  1. Zoom上でウェビナー開始
  2. Youtubeライブでも連携配信
  3. Webシステム上にYoutubeライブ配信の埋め込みリンク設置
  4. ユーザーはWebシステム上でウェビナーを閲覧

準備

Zoom公式のGithubからAPI/フロントのサンプルリポジトリが公開されており、今回はフロントはVue, APIはnodeのリポジトリを使用してお試しします。

他にもiOS/Android, react, angularなどあり、充実しています。

Zoomアカウント

APIサイドでZoomのID(key)/Secretを使用するのですが、これらの取得にはZoomアカウントが必要になります。

アカウント作成は以下のSignUpから行います。

https://marketplace.zoom.us/

SDK Key, Secretの発行

https://marketplace.zoom.us/

Zoomアカウントを作成したら、以下のキャプチャにそって進めます。

4は作成したappをマーケットプレイスに公開しますか?というチェックなので、今回はチェックOFFにします。

5では「会社名」「氏名」「メールアドレス」が必須となっているので、入力して進めます。

6の「App Credentials」まで進むとClient ID(key)/Client Secretを取得できますので、コピーして手元に保管します。 (Github等でパブリックに公開などしないよう、お取り扱いにはご注意下さい)

API準備

https://github.com/zoom/meetingsdk-sample-signature-node.js

上記のリポジトリからcloneします。

$ git clone https://github.com/zoom/meetingsdk-sample-signature-node.js.git

リポジトリのREADMEにSetup方法が書いてあるので、内容にそって進めていきます。

1.In terminal, cd into the cloned repo:
$ cd meetingsdk-sample-signature-node.js

ターミナルでcloneしたディレクトリに移動します。

2.Then install the dependencies:
$ npm install

リポジトリ直下でnpm installの実行します。

3.Create an environment file to store your SDK Key and Secret:
$ touch .env

.envファイルを作成します。

4.Add the following code to the .env file, and insert your Zoom SDK App's Key and Secret found on the App Credentials page in the Zoom App Marketplace:
ZOOM_SDK_KEY=SDK_KEY_HERE
ZOOM_SDK_SECRET=SDK_SECRET_HERE
5.Save and close .env.

先程取得したClient ID(key)/Client Secretを3.で作成したファイルにセットするというもので、流れ的にターミナル上でviで編集すると楽かと思います。viに慣れていなければお気に入りのエディタで編集しても問題ありません。

ZOOM_SDK_KEY=「SDK Key, Secretの発行」で取得したSDK Key
ZOOM_SDK_SECRET=「SDK Key, Secretの発行」で取得したSDK Secret

フロント準備

https://github.com/zoom/meetingsdk-vuejs-sample

上記のリポジトリからcloneします。

$ git clone https://github.com/zoom/meetingsdk-sample-vuejs.git

Yarnが必要になるので、入っていない場合は下記などを参考にYarnのインストールを行って下さい。(下記はmacのやり方なので、windowsの方は別途お調べ下さい) https://qiita.com/niwa1903/items/fb1d37c180d6cbc696c8

こちらもリポジトリのREADMEにSetup方法が書いてあるので、内容にそって進めていきます。

1.Once cloned, navigate to the meetingsdk-sample-vuejs directory:
$ cd meetingsdk-sample-vuejs

ターミナルでcloneしたディレクトリに移動します。

2.Then install the dependencies:

$ yarn install

リポジトリ直下でyarn installの実行します。

3.Open the meetingsdk-sample-vuejs directory in your code editor.

cloneした meetingsdk-sample-vuejsディレクトリをお気に入りのエディタで開きます。

4.Open the src/components/HelloWorld.vue file, and enter values for the variables:

src/components/HelloWorld.vue ファイルを開き変数値を編集、と記載されていますが、HelloWorld.vueは埋め込みではなくZoomアプリと同様でウィンドウ全体を使うUIのため、今回はsrc/components/HelloWorldNew.vueを編集して使用します。

src/App.vueを開き、import HelloWorld from ... のファイル指定を HelloWorld.vue から HelloWorldNew.vue に変更します。

②変数値セット

今回はお試しのためハードコードしますが、本来は画面やDBから取得してセットするなど、何かしらロジックは必要になってくるかと思います。

src/components/HelloWorldNew.vueのexport default内 data()のreturnを以下のように変更します。

sdkKey: "「SDK Key, Secretの発行」で取得したSDK Key",
meetingNumber: "ミーティングのID",
passWord: "ミーティングのPW",
signatureEndpoint: "http://localhost:4000",
userEmail: "参加者のメールアドレス",
userName: "参加者のユーザ名",

ミーティングのID/PWはzoomアプリから取得できます。

③UIの日本語表示化 src/components/HelloWorldNew.vueのstartMeeting()内 this.client.initI()の引数に以下を追加します

language: 'jp-JP',

動作確認

APIをローカルで起動します。

$ npm run start

フロントをローカルで起動します。

$ yarn serve

ローカルのフロントにアクセスします。

http://localhost:8080/

サンプルページ感がすごいので、前回の記事でも使わせていただいたおしゃれなテンプレートページを以下のサイト様からお借りして埋め込みしてみます。(レイアウトなどのソース調整部分は割愛します)

ちなみにフルスクリーン版はこんな感じで、いつも使っているようなUIです。

おわりに

Zoom周りのロジックはほぼサンプルリポジトリのまま使える形なので、想像よりも簡単に実現することができたと感じでいます。

しかし、SDKを埋め込みモードで使うやり方や日本語UI設定が検索でなかなかHITせず、ソースをじっくり読み込んだため調査としては時間が結構かかってしまいました。有識者や感の良い方であれば爆速でたどり着ける部分だとは思います。

Webシステム+Zoom埋め込みはアイデアしだいで可能性が広がるので、ワクワクしながらお試しできました!

AWS CodePipelineを使用してAWS Beanstalkにデプロイする方法

こんにちは。ブリスウェルのSonです。
今回は、GitHubからAWS Beanstalkにアプリケーションをデプロイする方法を紹介します。

AWS Beanstalkは、アプリケーションを簡単で早くデプロイおよび管理するためのサービスです。 AWS CodePipelineは、コードを継続的に配信およびデプロイするためのサービスです。

前提条件


1. GitHubトークンの作成

まず、AWS CodePipelineでGitHubリポジトリを使用するために、GitHubからトークンを作成する必要があります。このトークンがbuildspec.ymlファイルで使用されます。トークンを持っている場合は、このステップをスキップができます。

GitHubのアカウントでログインし、Settings > Developer settings > Personal access tokensに移動します。「Generate new token」をクリックして、アクセス許可を与えるためのトークンを作成します。このトークンを取得しておいてください。


2. AWS CodePipelineのセットアップ

AWSコンソールにログインし、「AWS CodePipeline」を選択して、「パイプラインを作成する」ボタンをクリックします。


3. ソースステージの設定

次の画面で、パイプラインに名前を付けます。「次に」のボタンをクリックします。


「ソースステージを追加する」の画面でソースプロバイダーで「GitHub バージョン 2」を選択します。GitHub アプリ接続を選択します。作成していない場合は作成するための「GitHub に接続する」ボタンをクリックします。



次に、デプロイするためのリポジトリとブランチ名を選択します。ここでは、「feature/pipeline」を使用します。ご要望に応じて、「master」や「develop」等のブランチを使用ができます。


4. CodeBuildプロジェクトの設定

CodePipelineは、ソースステージからソースを取得した後、CodeBuildプロジェクトを実行します。

「ビルドステージを追加する」の画面で「AWS CodeBuild」を選択します。 次に、プロジェクト名を選択します。プロジェクトがない場合はプロジェクトを作成するために、「プロジェクトを作成する」ボタンをクリックします。
当画面でデプロイ用の環境変数を設定ができます。


5. デプロイの設定

「デプロイステージを追加する」の画面でデプロイプロバイダーでAWS Elastic Beanstalkを選択して、アプリケーション名、環境名を入力します。


6. レビュー

レビューの画面で設定した情報を確認します。全部の情報はOKだったら、「パイプラインを作成する」のボタンをクリックします。


7. buildspec.ymlの設定

ソースコードのRootフォルダーでbuildspec.ymlファイルを作成します。

version: 0.2

phases: install: runtime-versions: nodejs: 16 commands: - npm -v - yarn -v - npm config set //npm.pkg.github.com/:_authToken=YOUR_PRIVATE_TOKEN
pre_build: commands: - yarn - yarn run check
build: commands: - yarn build

artifacts: files: - '*/'

cache: paths: - 'node_modules/*/**'

このbuildspec.ymlファイルは、AWS CodeBuildのビルドプロジェクトで使用するために使用されます。ビルドプロセスの3つの段階が定義されており、それぞれにコマンドが含まれています。

最初の段階は、「install」です。この段階では、Node.jsランタイムバージョン16を使用し、npmとyarnのバージョンを表示し、作成した「YOUR_PRIVATE_TOKEN」を設定するnpmコマンドを実行します。

次の段階は、「pre_build」です。この段階では、yarnコマンドを使用して必須なパッケージをインストールし、「yarn run check」コマンド等で解析ツールを実行してコード品質を確認します。

最後の段階は、「build」です。この段階では、「yarn build」コマンドを使用してアプリケーションをビルドします。

「artifacts」セクションでは、すべてのファイルがアップロードされるように、ワイルドカードを使用しています。

最後に、「cache」セクションでは、キャッシュされるファイルのパスが指定されています。ここでは、node_modulesディレクトリ以下のすべてのファイルがキャッシュされます。

* 詳しく設定については下記リンクを参照してください。
CodeBuild のビルド仕様に関するリファレンス - AWS CodeBuild


8. 終わりに

テストするために、ちょっとソースを修正して、GitHubへ「feature/pipeline」をプッシュします。実行が成功すると、下記の表示となります。

Pipelineの基本的な機能紹介は以上となります。間違い等がありましたら、コメントお願いします。

ご覧いただきありがとうございました。

Youtubeライブ配信の埋め込みリンクを設置する話

はじめに

凄まじい寒波によりキッチンの水道管が凍りました。水が出ない半日を過ごしたid:rosoneです。 (更に近所では水道管が破裂していました🙄)

さて、Webシステムの開発をしている中で、ライブ配信機能を追加してみるのはどうか?というアイディアが出ました。 少し掘り下げで考えてみます。

  • ライブ配信機能をイチから実装していくと、開発コストだけでなくインフラ面でのコストも高くつく
  • 何かしらの方法でライブ配信を行い、自前のWebシステム上でユーザーが配信を見れる形であれば良さそう
    • ライブ配信のプラットフォームを立ち上げたいわけではない

上記により、例えばYoutube等の既存のプラットフォームを使って配信をし、自前のWebページに配信動画のプレイヤーを配置してあげれば、低コストでスモールスタートすることができそうです。

以上の背景から、オープン/クローズドは問わず1:nの形で配信できるプラットフォームの中から実現できそうなサービスを調べたところ、YoutubeとZoomが候補に上がりました。

Youtubeは非常に簡単にお試しすることができたので、今回はYoutubeライブ配信のページ埋め込みについてまとめます。

Zoomの方は公式のSDKが公開されており実現はできそうなのですが、ある程度しっかりとした実装が必要になりそうなため、お試しがうまく行ったら別の機会でブログにまとめたいと思います。

Youtubeライブから埋め込み用リンクを取得

ゆったりとしたjazzのライブ配信をお借りします。

埋め込みリンクは以下の手順で取得できます。

以下の埋め込みリンクを取得できました。

 <iframe width="560" height="315" src="https://www.youtube.com/embed/YSAodnpu1OA" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

HTMLの準備

Webページのテンプレートを以下のサイト様からお借りします。

テンプレートのHTMLをブラウザで開くと、おしゃれなページが表示されました。

この写真部分をYoutubeライブに差し替えていきます。

埋め込みリンクを設置

今回はChromeの検証機能を使用し、ソース上で差し替え対象の写真を指定している箇所を特定します。

エディタでHTMLを開き、該当箇所に埋め込みリンクを差し替えていきます。

コード差し替え後に画面を表示すると、Youtubeライブ配信のプレイヤーが表示されました。

レイアウトが崩れているので、表示位置とサイズを調整します。

いい感じに配置できました。

コメントやいいねなどの付属機能は付いていないですが、シークバーの追っかけ再生、再生速度や画質変更などの基本的な再生機能は本家Youtubeと同等で利用できるようです。

最後に

レイアウトなど気にしなければ、モノの数分で自前のページにYoutubeライブのプレイヤーを設置することができました。

今回はソースを直接編集していますが、実際の運用では同じように毎回ソースを修正してデプロイしてというのは現実的ではないですね。

自前のWebシステム上で配信動画のURLを入力して更新するような仕組みだけ作ってあげれば、「はじめに」で記載したアイディアを実現できそうです。

  1. Youtubeライブ配信を開始
  2. Youtube上から埋め込みリンクを取得
  3. 自前のWebシステム上でURLを入力

ちなみに、はてなブログに埋め込みリンクをそのまま設置すると、下記のようになりました。

寒い日が続きますが、水道管の凍結対策も忘れずにお過ごし下さい。

バタフライ・エフェクト

どんな小さな出来事でも連鎖反応で未来に大きな影響を及ぼす「バタフライ・エフェクト」。ゲームや映画で良く使われる演出ですね。

今回は、以下のローレンツ方程式により、バタフライ・エフェクトの現象を確かめてみます。

x, y, zの変数とp, r, bの定数の常微分方程式です。定数値は、p=10, r=28, b=8/3 をローレンツさんは用いています。

1. ローレンツ方程式を定義

def lorenz(x, y, z, p=10, r=28, b=8/3):
    dot_x = -p * x + p * y
    dot_y = -x * z + r * x - y
    dot_z = x * y - b * z
    return dot_x, dot_y, dot_z

2. 時間間隔とステップ数を設定

dt = 0.01
num_steps = 15000

3. 空の配列を作成

xs = np.empty(num_steps + 1)
ys = np.empty(num_steps + 1)
zs = np.empty(num_steps + 1)

4. 初期値を設定

初期値①

xs[0], ys[0], zs[0] = (1., 1., 1.)

初期値②(0.001だけ増やす)

xs[0], ys[0], zs[0] = (1., 1., 1.001)

5. 座標生成

for i in range (n_steps):
    dot_x, dot_y, dot_z = lorenz(xs[i], ys[i], zs[i] )
    xs[i + 1] = xs[i] +  (dot_x * dt)
    ys[i + 1] = ys[i] +  (dot_y * dt)
    zs[i + 1] = zs[i] +  (dot_z * dt)

6. 描画

初期値の違いがどのように結果に影響してくるのか。グラフ描画(ローレンツ・アトラクタ)により確認してみます。

初期値①(1.000, 1.000, 1.000)

初期値②(1.000, 1.000, 1.001)

ほんの僅かな初期値の違いにより、周回する軌道が異なってくることが確認できました。まさにカオスです。

予測には精度の高い初期値(観測値)が大事なのですね。

芝大門から眺める愛宕山

Pythonで3D地形図化 - Briswell Tech Blog

年の瀬も間近になってまいりました。ラストスパートですね!

弊社のオフィスがある芝大門の近くにある愛宕山
天然の山としては東京23区内最高峰(25.7m)で、かつては江戸の見晴らしの名所と言われていました。

愛宕山 | 錦絵でたのしむ江戸の名所

こちら(出典:国立国会図書館)のように愛宕山は錦絵にも描かれています。
現在は、周辺が高層ビルに囲まれているため、かつてのように見晴らすことはできなくなりましたが、急な石段(出世の石段)も現存し、存在感を残しています。

ふと感じたのが...
愛宕山から見る江戸(愛宕山→江戸)は、上記のような錦絵や江戸後期の写真にも残っているので、どのような眺めであったのか確認できるのですが、
自分が今いる芝大門から見る愛宕山(江戸→愛宕山)はどのような眺めだったのか。町屋や大名屋敷も数メートルほどの高さだったので、愛宕山を眺めることができたはず。

現在の3D地形図をもとにそれを確認してみようと思います。

地理院地図 / GSI Maps|国土地理院

こちらの地理院地図の3D表示機能を使い

  • ズームレベル:13
  • タイルのX座標:7276
  • タイルのY座標:3226

芝大門愛宕山が含まれる上記のタイルのobjファイル(3Dモデルフォーマット)をダウンロードします。

そのobjファイルをパワポで手軽に3Dビューしてみます。

当時はこのような眺めだったのですね。感動です。

良いお年をお迎えください。

GAS & Slackで地震発生時の安否確認Botを作ってみた

弊社のようにリモートワークが多い会社では、地震発生時に安否確認を「迅速かつ正確に」行う必要があります。
ただ、地震の発生はいつどこで起きるかわからないため、担当者の割り振りが難しいのが現状です。
今回はGASとSlackを組み合わせて、地震発生時にSlackに安否確認の投稿をする仕組みを実装(自動化)してみました。
 

投稿のイメージはこんな感じです。
   

地震の情報源は気象庁の以下電文を利用します。
気象庁防災情報XMLフォーマット形式電文の公開(PULL型)
今回は10分に一度、情報を取得し地震情報のチェックを行うため、「高頻度フィード(地震火山)」のxmlを使用します。

「高頻度フィード(地震火山)」には直近に発生した地震火山関連情報を一覧で取得できます。
今回は、この中から震度3以上に適用される「震度速報」を安否確認のトリガーとします。
震度速報の[entry]タグには、詳細情報が記載されたURLが[link]タグ内にセットされています。
 

[link]タグ内のURLにアクセスすると、このような詳細情報を確認することでができます。
「震度」や「震源」といった情報を取得できますね。
 

そのため、手順としては以下の流れとなりそうです。

  • 「高頻度フィード(地震火山)」を取得

  • 「震度速報」の[entry]タグ内リンクを順に確認していく

  • 詳細情報リンクにて、震度5以上の情報があれば、Slackにメッセージを投稿  
     

では、実際に実装していきます。 
 

GASでスクリプトの作成

Google Apps Script
上記リンクより、GASの新規プロジェクトを作成します。
プロジェクトの作成は「新しいプロジェクト」より行えます。

コードの作成

プロジェクトの作成ができたらスクリプトを書いていきます。
※[token]と[channel]は後ほど取得するので仮置きしておきます。

/**
* 気象庁の地震情報(高頻度フィード)を取得しSlackに安否確認を飛ばす
* http://xml.kishou.go.jp/xmlpull.html
*/
function getJisinkun() {
  var now = new Date();
  var Before_1_Minute = new Date();
  Before_1_Minute.setMinutes(now.getMinutes() - 11); //11分前の時刻を取得(10分ごとに実行するため)
  var preDate = Utilities.formatDate(Before_1_Minute,"JST","yyyy-MM-dd HH:mm");
  console.log(preDate);

  var feedUrl   = 'https://www.data.jma.go.jp/developer/xml/feed/eqvol_l.xml';
  var feedHtml  = UrlFetchApp.fetch(feedUrl).getContentText();
  var feedDoc   = XmlService.parse(feedHtml);
  var feedXml   = feedDoc.getRootElement();
  var feedentry = getElementsByTagName(feedXml, 'entry'); //xmlのentry要素を配列で取得
  var atom      = XmlService.getNamespace('http://www.w3.org/2005/Atom');

  //初期値設定
  var slackFlg  = false; //slack送信フラグ
  var quakedt   = ""; //地震発生時刻

  //気象庁のxmlデータ(feed)より、entry要素を繰り返し探索
  var quakeInfo = ""; //地震情報
  feedentry.forEach(function(value, i) {
    var titleText = value.getChild('title', atom).getText(); //title
    var linkText  = value.getChild('link', atom).getAttribute('href').getValue(); //link
    
    //titleが震度速報の場合(震度3以上が震度速報に該当)
    if('震度速報' == titleText){
      //気象庁のxmlデータ(data)の情報を取得
      var dataUrl   = linkText;
      var dataHtml  = UrlFetchApp.fetch(dataUrl).getContentText();
      var dataDoc   = XmlService.parse(dataHtml);
      var dataXml   = dataDoc.getRootElement();
      var titleText = value.getChild('title', atom).getText();  //title

      var dataItem            = getElementsByTagName(dataXml, 'Pref'); // xmlに含まれるPref要素を配列で取得する
      var dataMaxInt          = getElementsByTagName(dataXml, 'Kind')[0].getValue(); // 震度
      var dataArea            = getElementsByTagName(dataXml, 'Area')[0].getValue(); // 地域
      var dataReportDateTime  = getElementsByTagName(dataXml, 'TargetDateTime')[0].getValue(); // 地震発生時刻
      dataReportDateTime      = dataReportDateTime.replace('T', ' ');
      dataReportDateTime      = dataReportDateTime.replace('+09:00', '');

      //気象庁のxmlデータ(data)より、Pref要素を繰り返し探索
      dataItem.forEach(function(value, i) {
        //Pref要素の文字列を取得(震度と地域)
        var strPref = dataArea;
        // 都道府県指定(東京近辺を対象)
        if(dataArea.match(/福島県|東京都|神奈川県|埼玉県|千葉県|茨城県|静岡県|栃木県|群馬県|愛知県|長野県/)){
          //「地震発生時刻 > 実行時刻の5分前」かつ「震度速報相当」の場合
          if(dataReportDateTime > preDate && dataMaxInt.match(/[震度5-|震度5+|震度6-|震度6+|震度7]/)){
              slackFlg = true;  //送信フラグをtrue
              quakedt     = dataReportDateTime;  //地震発生時刻
              quakeArea   = dataArea;  //地震発生地域
              quakeMaxInt = dataMaxInt;  //震度
              Logger.log("地震発生時刻:" + dataReportDateTime);
              Logger.log("地震発生地域:" + dataArea);  //地震発生地域
              Logger.log(dataMaxInt);  //震度
          }
        }
      });
    }
  });
  
  //送信フラグがtrueの場合
  if(slackFlg == true){
    var url = "https://slack.com/api/chat.postMessage";
    var payload = {
    "token" : "xoxb-XXXXXXXXXXXXX-XXXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXX",
    "channel" : "XXXXXXXXXX",
    "text" : "<!channel>\n【安否確認】\n震度5以上検知しました。\n安否確認のため、各自リアクションを行ってください。\n\n【地震情報】\n地震発生時刻:" + quakedt +"\n地震発生地域:"+ quakeArea +"\n"+ quakeMaxInt,
    };
    var params = {
      "method" : "post",
      "payload" : payload
    };
    // Slackに投稿する
    UrlFetchApp.fetch(url, params); 
    Logger.log("送信完了");
  }else{
    Logger.log("送信なし");
  }
}

/**
* @param {string} element 検索要素
* @param {string} tagName タグ
* @return {string} data 要素
*/
function getElementsByTagName(element, tagName) {
  var data = [], descendants = element.getDescendants();
  for(var i in descendants) {
    var elem = descendants[i].asElement();
    if ( elem != null && elem.getName() == tagName) data.push(elem);
  }
  return data;
}

スクリプトの記載は以上です。

このように丸々コピペでOKです。

スクリプトについては下記サイトを参考にさせていただきました。
参考文献: 気象庁のサイトから地震情報を取得してメール通知する

 

ライブラリ(SlackAPI)の追加

Slackへの投稿を行うために、SlackAPIライブラリを追加します。
ライブラリの追加はサイドメニューのライブラリ[ + ]より追加できます。
追加に際して、スクリプトIDが必要になるのですが、SlackAPIの場合は下記となります。

SlackAPIスクリプトID:
  1on93YOYfSmV92R5q59NpKmsyWIQD8qnoLYk-gkQBI92C58SPyA2x1-bq

ライブラリを追加したことで、GAS上でSlackAPIの利用が可能となります。
 


続いて、Slackアプリの作成とtoken情報の取得を行います。


 

Slackアプリ(bot)作成

SlackAPI
上記リンクより、Slackアプリを作成します。
※既に作成済みの場合はBOT_USER_OAUTH_TOKENの取得までスキップしてください。

アプリの作成は「Create New App」ボタンより行えます。
 
アプリの作成には[From scratch]と[From an app manifest]のどちらかを選べます。
今回は[From scratch]を選択します
   

Botの権限設定

サイドメニューの[OAuth & Permissions]よりOAuth & Permissions画面に遷移します。

OAuth & Permissionsのページ中腹にある[Scopes]より[Bot Token Scopes]を設定します。
 
[Add an OAuth Scope]より以下の権限を追加
今回はメッセージの書き込みを行うので、[chat:write]と[chat:write.public]を追加します。
 

ワークスペースへのインストール

OAuth & Permissionsのページ上部にある[Install to Workspace]をクリックし、ワークスペースにインストールを行います。
 
アクセス権限のリクエストを許可します。
 

BOT_USER_OAUTH_TOKENの取得

ワークスペースへのインストールが完了すると、OAuth & Permissionsのページに[Bot User OAuth Token]が表示されます。
 
取得した[Bot User OAuth Token]を、GASの[token]にセットします。
 


次に、投稿したいSlackチャンネルのIDを取得します


 

SlackのチャンネルIDを取得

Slackのワークスペースにて、投稿したいチャンネルのURLをコピーします。
投稿したいチャンネルを[右クリック]→[コピー]→[リンクをコピー]
https://[slackのワークスペース名].slack.com/archives/XXXXXXXXXXX
上記のようなリンクをコピーできるので、チャンネルID(URL末尾のXXXXXXXXXXX)を取得します。

 
取得した[SlackチャンネルID]を、GASの[channel]にセットします。
 


仮置きの値をセットできたので、スクリプトのデプロイ作業を行います。


 

作成したスクリプトをデプロイ

GASのデプロイボタンをクリックし、作成したScriptのデプロイを行います。
[新しいデプロイ][デプロイを管理][デプロイをテスト]の3種選択可能ですが、[新しいデプロイ]を選択します。

[種類の選択]ではウェブアプリを選択し、[設定]の説明文に概要を記載します。
[アクセスできるユーザー]は[全員]としておきます。
入力完了後、[デプロイ]ボタンよりデプロイ作業が開始されます。

アクセス許可を求められるので[アクセスを承認]より承認します。

デプロイが完了するとURLが発行されるのですが、特に利用しないので[完了]ボタンより戻ります。
 


最後にGASのトリガーを設定し、スクリプトを定期実行させます。


 

GASのトリガー設定

GASサイドメニューの[時計アイコン]をクリックし、トリガー画面に遷移します。

右下の「トリガーを追加」から新規トリガーの作成を行います。

[時間ベースのトリガーのタイプを選択]にて、[分ベースのタイマー]を選択します。
[時間の間隔を選択(分)]にて、[10分おき]を選択します。
上記以外はデフォルト値のままでOKです。
選択できたら[保存]ボタンを押下し登録します。

登録できました。
 


これで設定完了です!お疲れ様でした🙌


 

実行結果を確認してみる

震度5以上の地震は直近で発生していないため、対象を震度3まで引き下げて検証してみます。
実行はGASのヘッダーメニューの[実行]ボタンより行えます。 実行後はこのようにログが出力されます。 ※直近で震度3の発生がなかったため[Before_1_Minute]の値を30000にまで引き上げています。

Slackに投稿されました!

これで地震発生時の取り急ぎでの安否確認はできますね👏
実運用では投稿メッセージにgoogleフォームのリンクをつけて細かい安否確認を行った方が良いかと思います😇。

GAS & SlackでMeetのリンク自動生成Botを作ってみた

弊社ではSlackの文面では伝わらない時などに、Google Meet(以下Meet)を使用しています。
MeetのURL発行には以下の手順を踏まねばならず、若干の手間だったりします。

  • Meetのページを開く
  • 会議を新規で作成
  • 作成されたMeetURLをコピー
  • Slackに貼り付け

今回は、この手順をSlackのコマンドで実装(半自動化)してみました。
参考になれば幸いです。

GASでスクリプトの作成

Google Apps Script
上記リンクより、GASの新規プロジェクトを作成します。
プロジェクトの作成は「新しいプロジェクト」より行えます。

コードの作成

プロジェクトの作成ができたらスクリプトを書いていきます。
まずは、定数の定義からです。

const SLACK_POST_URL = 'https://slack.com/api/chat.postMessage';
const VERIFICATION_TOKEN = 'xxxxxxxxxx';
const BOT_USER_OAUTH_TOKEN = 'xxxxxxxxxx'

※[VERIFICATION_TOKEN]と[BOT_USER_OAUTH_TOKEN]は後ほどSlackのアプリから取得するので仮置きしておきます。

続いてMeetURLの取得処理を書いていきます。

/* Google MeetのURLを作成 */
function getMeetUrl() {
  const calendarId = 'primary'; // 一時的にイベントを作成するカレンダーID
  const dt = new Date();
  const date = dt.getFullYear() + '-' + (dt.getMonth() + 1) + '-' + dt.getDate();
  const requestId = Math.random().toString(32).substring(2); // 適当な文字列を作る
  const events = Calendar.Events.insert({
    summary: 'tmp_event',
    singleEvents: true,
    allDayEvent: true,
    start: { date },
    end: { date },
    conferenceData: {
      createRequest: {
        requestId,
        conferenceSolutionKey: {
          type: 'hangoutsMeet'
        },
      }
    }
  }, calendarId, { conferenceDataVersion: 1 })

  // MeetURLだけあれば良いので、作成後に予定そのものは削除する
  Calendar.Events.remove(calendarId, events.id);

  if (events.conferenceData.createRequest.status.statusCode === 'success') {
    const meetUrl = events.conferenceData.entryPoints[0].uri;
    return meetUrl;
  }
}

最後に、Slackへのレスポンス処理を作成します。

/* Slackに投稿 */
function postMessage(event, message) {
  const thread_ts = event.thread_ts ?? event.ts;
  const params = {
    method: 'post',
    payload: {
      token: BOT_USER_OAUTH_TOKEN,
      channel: event.channel,
      thread_ts: thread_ts,
      text: message,
    },
  };
  UrlFetchApp.fetch(SLACK_POST_URL, params);
}


/* Slackにメッセージを送信 */
function doPost(e) {
  const meetUrl = getMeetUrl();
  let message = meetUrl !== undefined ? `Meetのリンクを発行しました\n${meetUrl}` : 'Meetのリンクを作成できませんでした';
  let response = {
    response_type: 'in_channel',
    text: message,
  };

  if (e.parameter.command) {
    if (e.parameter.token !== VERIFICATION_TOKEN) {
      return null;
    }
    // Slash command
    return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON);

  } else if (e.postData) {
    const contents = JSON.parse(e.postData.contents);

    if (contents.token !== VERIFICATION_TOKEN) {
      return null;
    }

    if (contents.type === 'url_verification') {
      // Event SubscriptionsのPost先URL検証のため
      return ContentService.createTextOutput(contents.challenge);
    } else if (contents.type === 'event_callback') {

      // botがbotの投稿に反応しないようにする
      if (contents.event.subtype && contents.event.subtype === 'bot_message') {
        return null;
      }

      // アプリメンションで起動させる
      if (contents.event.type === 'app_mention') {
        postMessage(contents.event, message);
      }
    }
  }
}

スクリプトの記載は以上です。

このようにつなげて記載できていればOKです。

スクリプトについては本職ではないため、偉大な先人の御知恵を賜りました。
参考文献: 【GAS】SlackでいつでもどこでもGoogle MeetのURLを発行できるBotを作ってみた

 

ライブラリ(SlackAPI)の追加

Slackへの投稿を行うために、SlackAPIライブラリを追加します。
ライブラリの追加はサイドメニューのライブラリ[ + ]より追加できます。
追加に際して、スクリプトIDが必要になるのですが、SlackAPIの場合は下記となります。

SlackAPIスクリプトID:
  1on93YOYfSmV92R5q59NpKmsyWIQD8qnoLYk-gkQBI92C58SPyA2x1-bq

※現状ライブラリのスクリプトIDはググってみるしかないようです...

ライブラリを追加したことで、GAS上でSlackAPIの利用が可能となります。

 

サービス(Google Calendar API)の追加

続いてMeetURL取得のため、Google Calendar APIを追加します。
サービスの追加はサイドメニューのサービス[ + ]より追加できます。
サービス一覧より[Google Calendar API]を選択。
サービスを追加したことで、GAS上でGoogle Calendar APIの利用が可能となります。
 


ここまでできたら一旦保存し、Slackアプリの作成を行います。


 

Slackアプリ(bot)作成

SlackAPI
上記リンクより、Slackアプリを作成します。

アプリの作成は「Create New App」ボタンより行えます。
アプリの作成には[From scratch]と[From an app manifest]のどちらかを選べます。
今回は[From scratch]を選択します。
 

VERIFICATION_TOKENの取得

Basic Informationのページ中腹にある[App Credentials]から[Verification Token]を取得します。

取得した[Verification Token]をGASの[VERIFICATION_TOKEN]にセットします。    

Botの権限設定

サイドメニューの[OAuth & Permissions]よりOAuth & Permissions画面に遷移します。

OAuth & Permissionsのページ中腹にある[Scopes]より[Bot Token Scopes]を設定します。

[Add an OAuth Scope]より以下の権限を追加
今回はメッセージの書き込みを行うので、[chat:write]と[chat:write.public]を追加します。
 

ワークスペースへのインストール(1回目)

OAuth & Permissionsのページ上部にある[Install to Workspace]をクリックし、ワークスペースにインストールを行います。

アクセス権限のリクエストを許可します。

BOT_USER_OAUTH_TOKENの取得

ワークスペースへのインストールが完了すると、OAuth & Permissionsのページに[Bot User OAuth Token]が表示されます。

取得した[Bot User OAuth Token]を、GASの[BOT_USER_OAUTH_TOKEN]にセットします。
 


仮置きした値の設定が完了したので、GASのデプロイを行います。


 

作成したスクリプトをデプロイ

GASのデプロイボタンをクリックし、作成したScriptのデプロイを行います。
[新しいデプロイ][デプロイを管理][デプロイをテスト]の3種選択可能ですが、[新しいデプロイ]を選択します。

[種類の選択]ではウェブアプリを選択し、[設定]の説明文に概要を記載します。
[アクセスできるユーザー]は[全員]としておきます。
入力完了後、[デプロイ]ボタンよりデプロイ作業が開始されます。

アクセス許可を求められるので[アクセスを承認]より承認します。

デプロイが完了するとURLが発行されるのでコピーします。
 


最後はSlackアプリにてコマンドの作成です。


 

Slackコマンドの設定

Slackサイドメニューの[Slash Commands]を選択しSlash Commands画面に遷移します。
[Create New Command]をクリックいただくことでコマンドの作成が行えます。

各項目をセットします。
[Request URL]にはGASでデプロイした際に作成されたURLをセットしてください。

設定できたら[save]ボタンで登録します。

登録できました。
 

ワークスペースへの再インストール(2回目)

Slackサイドメニューの[Installed App]を選択しInstalled App Setting画面に遷移します。

[Reinstall to Workspace]よりWorkspaceへの再インストールを行います。

アクセス権限のリクエストを許可します。  


これで設定完了です!お疲れ様でした!🙌


 

コマンドを試してみる

MeetのURLが発行されました!

これで多少は手間が省けそうですね👏

大門・浜松町を3D化

PLATEAUの3D都市データについて、弊社のオフィスがある大門・浜松町(三次メッシュコード:53393680)がLOD2(屋根形状があるモデル)にて公開されているということで確認してみます。

ピンク部分がLOD2整備範囲

3D都市モデル(Project PLATEAU)東京都23区 - データセット

東京都23区の3D都市データ(商用利用可能)はこちらよりダウンロードできます。

さて、CityGML、3D Tiles、GeoJson、MVT、Shape、FBX、OBJ、GeoTIFF...
と色々ファイル形式がありますが、どれを利用するか。
地理空間や3D関連のファイルは種類が多いですね。

今回はBlenderオープンソースの3DCGソフト)でインポートできる以下のFBXファイルを利用します。

  1. 建物モデル:13100_tokyo23-ku_2020_fbx_3_op > bldg > lod2 > 53393680_bldg_6677.fbx

  2. 橋梁モデル:13100_tokyo23-ku_2020_fbx_3_op > brid > 53393680_brid_6677.fbx

【blender】リアルな都市の作り方!(~PLATEAUを用いる方法~)(ボイスピーク 解説) - YouTube

こちらの動画を参考にさせていただき、Blenderで大門・浜松町の3D都市を作成しました。地面、背景、光源の設定や、建物のテクスチャ(色や質感)変更等、大変勉強になりました。ありがとうございます。

MacBook Pro(M1 Proチップ)で作業していたのですが、なかなか画像が出力(レンダリング)されず、調べていたところ...
なんとBlenderでM1 Macレンダリングが最適化される設定がありました。

この設定により、無事にレンダリングすることができました。

結果(赤の矢印が弊社のオフィスがあるビルです)

すごいですね。このようなオープンデータから価値を生み出していきたいです。