- 投稿日:2020-09-19T22:52:31+09:00
Node.jsでByteDance製ビジネスアプリLarkのBotを作る
Larkってなに
Larkとは、シンガポールに本社を置くLark Technologiesが開発するアプリでチャット、ビデオ、カレンダー、スプレッドシート、ドキュメントなど全部入りのビジネスソフトと言っていいでしょう
こちらの企業はTikTokでおなじみのバイトダンス(字節跳動)の子会社ということで、同社は海外向けやエンタープライズ向けの製品に力を入れたい意向があるそうです
G SuiteやOffice、Slackなどの競合に位置する気概のようですが、そういうバックグラウンドはいちエンジニアの端くれたる僕には関係なく、ともかく僕のチームで採用されていています
日本語公式サイトよりできることが多彩で正直使い切れてないのですが、LarkではAppという形で外部と連携した機能を使えるようです。
Appにはチーム内で使用するCustom Appと、Larkユーザー全体に公開できるPublic Appとがあり、それぞれにまたGadget、Web page、Botという区分があります。よくわからない。
Lark Developer - Open Platform Overview今回つくるもの
その中で今回は、特定のチャットにメッセージを送る機能を用いたいと思うのでCustom AppのBotにあるメッセージ機能を使います
どこかの環境にBotサーバーを設置し、それを呼び出すことでLarkのチャットにメッセージが送られるという単純な仕組みにしたいと思います。
メッセージを受け取って返答したり、そのほかLark内のイベントに応じたアクションなどBotの機能は数多くありそうですが、今回は単純な送信のみに対応したいと思います。
実行環境はGoogleのCloud Functions上ですが、Node.jsを用いるのでGCEやAWSのEC2, Lambdaなど他の環境でも参考にできると思います。
サーバー自体のリクエスト処理などは今回の範疇外とさせていただきます。上記の通り環境にもよりますし、ただでさえ冗長なのと、何より初めてQiitaを書く僕が限界です。
※一応こちらのページにPythonでの完全なサンプルがありますが説明等はなく、コメントも中国語ですので自信のある方はお使いください。ほんとは解説したい。Lark botの設定
公式ドキュメントにしたがって進めます。サンプルやチュートリアルではないのでサーバーなどの話は載っていません。
4つのプロセスに分かれていますが初めの2つだけで今回は実装できます
AppとBotの設定
Create a Bot in LarkではLarkの開発者向けサイトからAppを追加し、Botを設定する手順が記載されています。直接コーディングなどはしませんが大まかに解説します。
See Create a custom app to Create an app for your business.
とのことなのでそこの解説です。公式のガイドに従えば特に支障なくすすめます。
Lark Developerの各ページから右上のMy Appsに移動し、Create an Appから必要な項目を入力してCustom Appを追加します。
それからApp Capabilityを有効化する必要があるので先ほど追加したAppの詳細を表示し、FeaturesからBotを有効化します。Permissionなどの指示もありますが不要です。
最後に、自分が使っているLarkからBotを見つけて使用するためにReleaseしてVersionを1度追加する必要があります。Releaseを行っても使用しているチームの外には見えないのでVersion Management & Releaseから適当に項目を入力してください。(チームの権限者から承認されるとチャットに追加できるようになります。)
メッセージ送信の要件
Bot development processに戻りましょう。Appの設定ができたので次のSend messagesに進みます。
Step 1: Obtain the API access token
See Obtain access token.Step 2: Invoke the message sending API
Send messages to users/group chats as an app. For the message API, see Send a message.
Send a private message: Send a message to a user within the app visibility range. A user must have an access visibility to that app.
Send group messages: Send messages within a group. The app needs to be in the corresponding group, see Using bots in groups.
といった形で2ステップ書かれています。Botからメッセージを送るにはアクセストークンが必要で、Step1ではその手法が書いてあります。Step2ではテキストメッセージの送信とその際に必要な情報が載っています。
先ほどのガイドに従ってもいいのですが、リンクのみの箇所が多いため直接リファレンスを参照することをお勧めします
まとめると
- Appの詳細から
app ID
とapp secret
を取得する(初回のみ)- IDとsecretでAPIを呼び出しAPIから
chat_id
を取得する(初回のみ)- 同様に、
tenant_access_token
を取得するchat_id
とtenant_access_token
と一緒にメッセージをリクエストといった流れで実装できます
1.と2.は変更されないのでbotサーバーの処理として行う必要はありません。ただし2.を呼び出すためには3.が必要なので前準備でもサーバーでも実行します。ややこしいですが2.と3.を一度ターミナルのコマンドなどで済ませ、3.と4.をサーバーの処理として実装しましょう
実装
コマンドから認証情報を取得 (1.と3.の部分)
APIを呼び出すので任意のHTTPリクエストができるツールで構いませんがここでは
curl
を用いますリクエストを送信するのに必要な1.の情報は先ほどのMy Appsの該当アプリ設定ページから取得します
Credentials & Basic Infoの一番上にApp ID
とApp Secret
が載っているので、下記コマンドの****に置き換えてください(この情報は後のサーバーコードでも使用します)参考 : Reference > Getting Started > Authorization > Obtain tenant_access_token (internal apps)
$curl -XPOST -H 'Content-Type:application/json' -d '{"app_id":"*********","app_secret":"*********"}' https://open.larksuite.com/open-apis/auth/v3/tenant_access_token/internal/これで
tenant_access_token
が入手できるので、次のコマンドにて使用しますコマンドから
chat_id
を取得 (2.の部分)同様に
tenant_access_token
を用いることでchat_id
が入手できます参考 : Reference > Bot > Group (Bot) > Obtain group list
$curl -H 'Authorization:Bearer *********' https://open.larksuite.com/open-apis/chat/v4/list整形しないと大変ですが以下の画像のようなレスポンスが得られます。
この場合2つのチャットに参加しており、それぞれの情報が取得されています。チャット名は
name
の部分に書かれていますが日本語の場合はコードに変換されているので探しにくいかもしれません。(背景は実際のLarkの画面です)もし参加しているチャットが多い場合
has_more
の部分がtrue
になり、表示されませんここで取得した、メッセージを送りたい目的のチャットの
chat_id
を後ほど使用しますNodeでメッセージを送信
やっと必要な情報が揃ったので実際のコードに移ります。メッセージ送信の要件にあった3.4.を書きましょう。
axiosを用いていますがこちらも好きなHTTPクライアントで構いません
まずコマンドでも行った3.の認証情報の取得を関数にまとめます。同様に、
app_id
とapp_secret
を用いてリクエストを送る処理です。単純なトークンを返すようにしましょう。index.jsasync function getTenantToken(app_id, app_secret) { const reqConf = { "conf": { "headers": { "Content-Type": "application/json" } }, "data": { "app_id": app_id, "app_secret": app_secret, } } return await axios.post('https://open.larksuite.com/open-apis/auth/v3/tenant_access_token/internal/', reqConf.data, reqConf.conf) .then(function (response) { return response.data.tenant_access_token }) .catch(function (error) { console.log(error); }); }2.は先ほど用意した
chat_id
で良いので4.の処理です。こちらも情報さえあればPOSTリクエストを送るだけで可能なので簡単ですね。index.jsasync function sendMessageToLark(tenant_access_token, chat_id, message) { const reqConf = { "conf": { "headers": { "Content-Type": "application/json", "Authorization": "Bearer " + tenant_access_token, } }, "data": { "chat_id": chat_id, "msg_type": "text", "content": { "text": message } } } await axios.post('https://open.larksuite.com/open-apis/message/v4/send/', reqConf.data, reqConf.conf) .then(function (response) { return }) .catch(function (error) { console.log(error); }); }上記の関数を実行すれば実際にLarkでメッセージを受け取れるでしょう。当然ですがIDやトークンなどセキュリティに関わる情報は直に記入せず環境変数などで秘匿化してください。
index.js// さっきのchat_id const chat_id = ************ const app_id = ************ const app_secret = ************ const message = "Lark Botからメッセージです" getTenantToken(app_id, app_secret).then((r) => { sendMessageToLark(r, chat_id, message) }).catch(e => console.log(e))まとめ
以上でLark Botから単純なメッセージの送信が実装できました。送信の部分は既存のチャットBotとあまり違いがなく、ほとんどがLarkの設定など固有の要素の理解が重要でしょうか。
実際ぼくも情報がない中で苦労したので少しでも参考になれば、と思います。ドキュメントを見て分かる通り他にもLarkには多くの機能が存在します。チャットBotにしてもそもそもメッセージを受信するところから作れますし、Larkはそれだけでアプリ一つが成り立つようなかなり大規模なプラグインもできるようです。
まだまだ日本では途上にあるサービスなので、今後の動向を注視したいですね。
- 投稿日:2020-09-19T19:15:46+09:00
Node.jsの文字列・オブジェクト⇔クエリ文字列への変換
利用するライブラリ
var qs = require("qs"); var querystring = require('querystring'); console.log(qs) ⇒{ stringify: [Function], parse: [Function] } console.log(querystring ) ⇒{ unescapeBuffer: [Function: unescapeBuffer], unescape: [Function: qsUnescape], escape: [Function: qsEscape], stringify: [Function: stringify], encode: [Function: stringify], parse: [Function: parse], decode: [Function: parse] }文字列・オブジェクト⇒クエリ文字列
qs.stringify(object, [options]);
第1引数にオブジェクトを渡す。
const form_data_qs = qs.stringify({ grant_type: 'authorization_code', code: 'code', redirect_uri: 'redirect_uri' }); console.log(form_data_qs) ⇒grant_type=authorization_code&code=code&redirect_uri=redirect_uriquerystring.stringify(obj[, sep[, eq[, options]]]);
第1引数にオブジェクトを渡す。
const form_data_querystring = querystring.stringify({ grant_type: 'authorization_code', code: 'code', redirect_uri: 'redirect_uri' }); console.log(form_data_querystring) ⇒grant_type=authorization_code&code=code&redirect_uri=redirect_uriquerystring.escape(str)
第1引数に文字列を渡す。
const str = querystring.escape('a:b?c$d*1') console.log(str) ⇒a%3Ab%3Fc%24d*1クエリ文字列⇒文字列・オブジェクト
qs.parse(string, [options]);
第1引数に文字列を渡す。
const form_data_qs = qs.parse('grant_type=authorization_code&code=code&redirect_uri=redirect_uri') console.log(form_data_qs) ⇒{ grant_type: 'authorization_code', code: 'code', redirect_uri: 'redirect_uri' }querystring.parse(str[, sep[, eq[, options]]])
第1引数に文字列を渡す。
const form_data_querystring = 'grant_type=authorization_code&code=code&redirect_uri=redirect_uri' console.log(form_data_querystring) ⇒{ grant_type: 'authorization_code', code: 'code', redirect_uri: 'redirect_uri' }
- 投稿日:2020-09-19T15:00:46+09:00
友達作りに敬語なんて必要なし!敬語禁止Discord Bot「Breako」リリース
敬語を判定する簡単なBotを作成しました!
概要
こんな感じで簡単な敬語を感知して返信するBotです。
「河童様」は反応、様は付くけど無礼な言葉とされる「貴様」には無反応なことを例に
誤検知を防ぐよう設計しています。
コマンド「@Breako」を入力すると設定されたNGワードを確認できます。作った理由
Twitterのつぶやきは基本ため口ですが、そこまで親しくない人からリプライが飛んできた場合は敬語になりませんか?
親しくなりたいけどついよそよそしくなってしまいがちです。
なのでDiscord内でサーバー側から敬語を禁止したらたくさん友達ができるんじゃないかという魂胆で作成しました。密かに人見知りや繊細な人にも枷を外してインターネットを楽しめる世の中にしたいという野望があります。
参考にした記事
使用した技術
- Glitch
- discord.js
v11.6.412.3.1- Google Apps Script
コード
基本、「誰でも作れる!Discord Bot(基礎編)」の記事の順序に従って作成しました。
server.jsconst ng = require('./ng.json') const ok = require('./ok.json')ng.json[ "です", "ます", "ました", "でした", ...ok.json[ "かます", "ますます", "さます", "覚ます", "冷ます", ...管理しやすいようNGワード、除外ワードを配列でjsonファイルに保存して呼び出しています。
server.js// 禁止ワード設定 var ng_reg = ng.map(v => { return new RegExp(v, "g"); }); var ok_reg = ok.map(v => { return new RegExp(v, "g"); }); var ok_under = ok.map(v => { for (let i = 0; i < ng_reg.length; i++) { v = v.replace(ng_reg[i], " __" + ng[i] + "__ "); } return v; }); var ok_under_reg = ok_under.map(v => { return new RegExp(v, "g"); }); const ng_list = ng.join(", ");NG、除外ワードの配列を正規表現化しています。
ok_underは除外ワードをDiscord用に下線で装飾するようreplaceで置換されたワードを上書きするために下線で置き換えしています。
(例: 覚ます → 覚 _ます_)server.js// 禁止ワード判定 // 禁止ワードがあればカウントプラス、OKワードがあればマイナス var match_count = 0; ng_reg.forEach((v) => { match_count = match_count + ( message.content.match( v ) || [] ).length; }); ok.forEach((v) => { match_count = match_count - ( message.content.match( v ) || [] ).length; }); if (match_count >= 1) { let text = message.content; for (let i = 0; i < ng_reg.length; i++) { text = text.replace(ng_reg[i], " __" + ng[i] + "__ "); } for (let i = 0; i < ok_under_reg.length; i++) { text = text.replace(ok_under_reg[i], ok[i]); } let Breako = "敬語が含まれているよ!: " + text; sendReply(message, Breako); return; }NGワード数をmatchでカウントして、除外ワードでマイナスしています。1以上ならば敬語が含まれていると感知して返信をする仕組みです。
「天狗様、目を覚ます。」であれば
「様」、「ます」で+2、「覚ます」で-1で合計1になり敬語が含まれている判定になっています。そして前半の
.replace(ng_reg, ' __' + ng + '__ ')
で「天狗_様_ 、目を覚 _ます_ 。」となり
後半の.replace(ok_under_reg,ok)
で「覚_ます_」を「覚ます」置き換えて「天狗_様_ 、目を覚ます 。」となります。server.jsconst ng_list = ng.join(', ') client.on('message', message =>{ if (message.author.id == client.user.id || message.author.bot){ return; } if(message.isMemberMentioned(client.user)){ sendReply(message, "禁止された敬語: " + ng_list); return; }@Breakoの部分です。NGワードの配列を文字列化して返信します。
振り返り
replaceを複数条件で置き換えするためにメソッドチェーンを利用したんですがかなり長いコードになってしまいました。
他によい方法が見つからずこのような形になっています。メンテナンスもしづらいのでよい方法があったら是非教えていただきたいです。
コメントのご指摘通り修正させていただきました!ありがとうございます!さいごに
NGワード、除外ワードについてはかなり杜撰なのでご意見あればTwitterなどにお願いします!
- 投稿日:2020-09-19T14:56:10+09:00
GCPのすゝめ【Cloud Run編】
概要
Cloud Runを使う際の手順とまとめです。
公式ドキュメント
Cloud Run
Cloud Buildサンプルレポジトリー
https://github.com/koffe0522/cloudrun-express準備
Docker for MAC
Docker for Windows
Google Cloud SDKGoogle Cloud Platform での設定
※既存のprojectプロジェクトを使用する場合は以下コマンドのみ
$ gcloud config set project <PROJECT_ID>projectの作成
projectの作成方法は、以下の2種類
- Cloud Consoleから作成
- gcloudコマンドライン ツールで作成公式ドキュメント
プロジェクトの作成と管理ディレクトリー構成
. ├── .env ├── .dockerignore ├── Dockerfile ├── README.md ├── cloud-build.yml ├── docker-compose.yml ├── index.js ├── package.json └── yarn.lock
アプリケーションの作成
※nodeプロジェクトが存在している場合は不要
$ mkdir <project name> $ cd <project name> $ yarn init -y $ yarn add express # 開発用 $ yarn add -D nodemonindex.jsconst express = require('express'); const app = express(); app.get('/', (req, res) => { res.send(`Hello World!`); }); const port = process.env.PORT || 8080; app.listen(port, () => { console.log('Hello world listening on port', port); });以下を追加
package.json{ ... "scripts": { "start": "node index.js", "dev": "PORT=3000 nodemon index.js" }, ... }ローカル開発環境設定
docker-compose.ymlversion: "3.6" services: app: image: node:10 env_file: .env tty: true ports: - "3000:3000" volumes: - .:/app working_dir: /app command: yarn run dev起動してみる
$ docker-compose up -d停止する
$ docker-compose down
Buildの準備
DockerfileFROM node:10 WORKDIR /src COPY package*.json ./ RUN yarn install \ --prefer-offline \ --frozen-lockfile\ --non-interractive \ --production=false COPY . . CMD ["yarn", "run", "start"].dockerignorenode_modules npm-debug.logcloud-build.ymlsteps: - name: gcr.io/cloud-builders/docker args: [ "build", "-f", "Dockerfile", "-t", "gcr.io/<PROJECT_ID>/<image name 好きな名前>", # ① ".", ] images: # ①と同じ - gcr.io/<PROJECT_ID>/<image name 好きな名前>Cloud Build & Deploy
Cloud Buildの実行
$ gcloud builds submit --project <PROJECT_ID> --config=cloud-build.yml ... Would you like to enable and retry (this will take a few minutes)? (y/N)? y$ gcloud beta run deploy <cloud-run-name> --region us-central1 --project <PROJECT_ID> --image <cloud-build.ymlのimages名> --platform managed ... you like to enable and retry (this will take a few minutes)? (y/N)? y ... Allow unauthenticated invocations to [cloud-run-name] (y/N)? y Deploying container to Cloud Run service [cloud-run-name] in project [PROJECT_ID] region [us-central1] ✓ Deploying new service... Done. ✓ Creating Revision... Deploying Revision. ✓ Routing traffic... ✓ Setting IAM Policy... Done. Service [cloud-run-name] revision [cloud-run-name-00000-xxx] has been deployed and is serving 100 percent of traffic at <URL> Routing traffic...表示されたURLにアクセスする
備考
- Cloud Run では8080ポートでないとエラーとなる
- 投稿日:2020-09-19T00:08:31+09:00
webpackをtsで構成してみる
はじめに
使用バージョン
node: v10.15.3 npm: 6.4.1package.json"typescript": "3.7.2" "webpack": "4.41.2"前提条件
- node.jsがインストール済み
- webpackを使用してTypescriptのバンドルができる状態
なぜwebpackをTypescriptで構成するか
typescriptを使用している時にどうせなら
webpack
もtsにしてみたいなぁと、まあなんとなくです構成する前に
ちなみにフォルダ構成はこうなっています
. ├── build │ └── src │ └── (bundleされたものが入る) ├── node_modules │ └── ...(多いので省略) ├── src │ └── app.ts ├── index.html ├── package.json ├── package-lock.json ├── tsconfig.json └── webpack.config.js
app.ts
とwebpack.config.js
のソースコードapp.tsclass Baz { name: string; constructor() { this.name = Baz.name; } } const baz = new Baz(); console.log(baz.name);webpack.config.jsconst path = require("path"); module.exports = { mode: 'development', // エントリーポイントの設定 entry: './src/app.ts', watchOptions: { // ディレクトリを監視対象から除外する ignored: /node_modules/ }, // 出力先の設定 output: { // 出力するファイル名 filename: 'app.js', // 出力先のパス(絶対パス) path: path.join(__dirname, './build/src'), }, module: { rules: [ { // 利用するローダー loader: 'ts-loader', // ローダーの処理対象ファイル test: /\.ts$/, } ] } };webpackを実行すると
build/src/
配下にapp.js
がバンドルされるようになっています。
そして、バンドルされたjsファイルをhtml側で読み込めば、コンソールにBaz
が表示されるようになっています。とまあこんな感じになっています。
本題
ここからwebpackの設定ファイルをtsで構成していこうと思います。
型定義ファイル
まず型定義ファイルが必要になってくるので
@types/webpack
をnpmインストールします。npm install --save-dev @types/webpackwebpackの設定
終わったらwebpackの設定ファイルを変更していきます。
まず、ファイル名を
webpack.config.ts
に変更します。ソースコードの大まかな変更点としては3つあり、
- ライブラリを
require
ではなくimport
で読み込む (importとrequireの違いはこちらの記事等で確認してください)- 型定義を行う
- exportする
です。
全てを行った状態がこちらになります。
webpack.config.tsimport * as path from 'path'; import * as webpack from 'webpack'; const config: webpack.Configuration = { mode: 'development', // エントリーポイントの設定 entry: './src/app.ts', watchOptions: { // ディレクトリを監視対象から除外する ignored: /node_modules/ }, // 出力先の設定 output: { // 出力するファイル名 filename: 'app.js', // 出力先のパス(絶対パス) path: path.join(__dirname, './build/src'), }, module: { rules: [ { // 利用するローダー loader: 'ts-loader', // ローダーの処理対象ファイル test: /\.ts$/, } ] } }; export default config;これで設定完了!とはいかず、このまま
webpack
を実行してもエラーになります。(function (exports, require, module, __filename, __dirname) { import * as path from 'path'; ^ SyntaxError: Unexpected token *原因としては、
tsconfig.json
の"module"
にCommonJS
以外のもの指定されていた為でした。
これは、ts-nodeがcommonjs以外のモジュール構文をサポートしていないために発生するそうです。(webpack公式より)なので、
tsconfig.json
の"module"
をCommonJS
にすれば、解決しwebpack
を実行することができるようになります。moduleにCommonJS以外を指定したい場合
タイトル通り、
CommonJS
ではなくES2015
を指定したいとなった場合、別のアプローチをする必要があります。
別のアプローチの詳細としては、webpack用のtsconfigファイルを作るというアプローチになります。最後にこちらだけ書いてこの記事はおしまいにしたいと思います。
必要なものをインストール
npm install --save-dev cross-env tsconfig-paths次に、webpack用のtsconfigファイルを作成します
touch tsconfig.webpack.json (ファイル名は任意)中のソースについては公式のものをそのままコピペしてます
{ "compilerOptions": { "module": "commonjs", "target": "es5", "esModuleInterop": true } }そして最後に実行のためのnpmスクリプトを
package.json
に追加します。"build": "cross-env TS_NODE_PROJECT=\"tsconfig.webpack.json\" webpack"
TS_NODE_PROJECT
というのはtsconfig-pathsによって提供される環境変数で、こちらを利用することでtsconfig.jsonファイルの問題を解決することができます。webpackを実行したいときは
npm run build
を行えばビルドができます。最後に
最後の方ちょっと雑かもしれないですがこれで終わります。
参考資料
https://webpack.js.org/configuration/configuration-languages/