- 投稿日:2021-12-05T21:33:05+09:00
WebSocket通信のクライアント/サーバの環境構築
要約 WebSocket通信のクライアント/サーバの環境を構築する。お仕事でWebSocket通信のツール、環境が必要となった際にまとめたものです。 wsのインストール ~/develop/wscatTest $ yarn init yarn init v1.22.5 question name (wscatTest): question version (1.0.0): question description: question entry point (index.js): question repository url: question author: question license (MIT): question private: success Saved package.json ✨ Done in 2.88s. ~/develop/wscatTest $ yarn add ws yarn add v1.22.5 info No lockfile found. [1/4] ? Resolving packages... [2/4] ? Fetching packages... [3/4] ? Linking dependencies... [4/4] ? Building fresh packages... success Saved lockfile. success Saved 1 new dependency. info Direct dependencies └─ ws@8.3.0 info All dependencies └─ ws@8.3.0 ✨ Done in 1.40s. ~/develop/wscatTest $ サーバ環境構築 ~/develop/wscatTest $ npx wscat -l 3000 Listening on port 3000 (press CTRL+C to quit) クライアント接続 ~/develop/wscatTest $ npx wscat -c ws://localhost:3000 Connected (press CTRL+C to quit) あとは文字を打てば動画の通りに動きます。
- 投稿日:2021-12-05T20:35:05+09:00
Serverless Frameworkでニュースサイトの新着記事をLINEで通知するアプリを作る
アプリケーション構築を学習する一環として、RSSフィードで配信された記事をLINE Messaging API経由で通知するアプリをServerless Frameworkで作りました。 友達追加すると、 新着記事を定期配信したり、手動で取得したりすることができるアプリです。 成果物はGithubにアップロードしていますが、せっかくなのでアプリの概要と、ハマったポイントをこちらにも投稿しておこうと思います。(以下、Github上のREADME.mdのほぼ抜粋です) 概要 RSSFeedで配信された記事を、LINE Messaging API経由で購読する 購読対象の記事は、設定用サイトで追加・削除・有効化・無効化が可能 定期購読のほかに、LINEでメッセージを送ることで記事を手動購読することも可能 システム構成は以下の通り。AWS上にソースをデプロイし、LINE Messaging API・LIFFと連携させる。 環境 種類 バージョン OS Windows10 version 2004 Node 14.17.5 Serverless Framework 2.66.1 開発環境 VSCode & Powershell ソース Githubにアップしています。 使ったサービス・言語・フレームワーク サービス・言語等 詳細 言語・環境 バックエンドのロジックはすべてNode.js v14.17.5で構築。 フレームワーク フレームワークはServerless Framework v2.66.2を用いて、AWS上サービスをデプロイした。 また、いくつかプラグインを導入した(※1)。 LINE Messaging API LINE上でのメッセージを受信し、適切な応答をするためにLINE Messaging APIを導入。無料枠で利用するので、月1,000件のメッセージ送信数制限には要注意・・・。応答はFlex Message(カルーセル型)もしくはテキスト形式で実施。 LIFF LINE Front-end Framework (LIFF)を使って、設定用のWebサイトで、LINEログインを利用しユーザID・名前・プロフィール画像を取得。 データベース AWS DynamoDBにユーザごとの情報(※2)を格納。 UI React + React-Bootstrap + Fontawesomeで設定画面のUIを構築。 ※1 利用したServerless Frameworkのプラグインは以下の通り。 server/serverless.yml抜粋 plugins: - serverless-dynamodb-local #DynamoDB接続部分の開発・テスト用 - serverless-vpc-plugin #Lambda用のVPC作成 - serverless-offline #ローカルでの開発・テスト - serverless-layers #Lambda Layersを利用するためのプラグイン client/serverless.yml抜粋 plugins: - serverless-s3-sync #ビルドしたフロントエンドのS3アップロード - serverless-cloudfront-invalidate #CloudFrontのキャッシュ削除自動化 ※2 DynamoDBレコードの構成は以下の通り。 userId:ユーザID lastSubscribe:RSSフィードの最終配信日時 subscribeFeeds:購読するRSSフィード情報(ここではPublicKeyとDevelopersIOの2つ) name:ユーザ名 DynamoDBのレコード { "userId": "U0000000000000000000000000000000", "lastSubscribe": "2021-11-25T01:00:19.091Z", "subscribeFeeds": [ { "feedUrl": "https://www.publickey1.jp/atom.xml", "addedAt": "2021-11-22T12:23:51.240Z", "siteUrl": "https://www.publickey1.jp/", "lastModifiedAt": "2021-11-21T23:23:51.240Z", "lastAction": "added", "feedId": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "title": "Publickey", "enabled": true }, { "feedUrl": "https://dev.classmethod.jp/feed/", "addedAt": "2021-11-22T05:24:35.168Z", "siteUrl": "https://dev.classmethod.jp", "lastModifiedAt": "2021-11-21T23:24:35.168Z", "lastAction": "added", "feedId": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "title": "DevelopersIO", "enabled": true } ], "name": "XXX" } ハマったところ Missing Authentication Tokenエラー 開発中、sls offline startでローカル開発環境では問題なくバックエンドAPIが動作するのに、AWSにデプロイするとMissing Authentication Tokenエラーが返されるようになった。 特に認証トークンを求めるような仕様にはしてないはず・・・ということで調べてみると、公式回答曰く認証トークンがない時だけでなく存在しないリソースやメソッドにリクエストを投げた時もこのレスポンスになるとのこと。(だったら、いかにも認証トークンエラーっぽい文言にしないでほしいですが・・・) ただ、メソッドやリソースが存在しないといってもローカル開発環境では問題なく動作するし・・・と色々調べたところ、実は当時serverless.ymlに設定していた下記が誤っていることが判明。 functions: webhook: handler: functions/webhook.handler events: - http: method: ANY path: /webhook/{proxy+} #<=この部分 この設定を書いたとき、https://xxxxxxx/webhook/xxx-xxx-1234{uuid}のように、パスパラメータ付きでリクエストすることも想定していたのですが、どうも{proxy+}は空白はNGのようでした。ただ、serverless-offlineを使った開発環境では空白でも動作してしまうので、混乱してしまいました・・・。 改めて、下記のようにしたら動作することを確認。 functions: webhook: handler: functions/webhook.handler events: - http: method: ANY path: /webhook/{proxy+} - http: #追加 method: ANY #追加 path: /webhook #追加 その後、アプリ構築を進める中でパスパラメータを使うことがないと分かったので、現在の形で決着。 functions: webhook: handler: functions/webhook.handler events: - http: method: ANY path: "/webhook" https化 LIFFが連携するURL(Endpoint URL)が、httpsサイトでないと設定できないことが途中で判明。 本番環境については、S3にホスティングしているURLを直接指定するのではなく、Cloudfrontを追加でデプロイすることで、CloudfrontのURLはhttps化しているためそちらを指定することで解決した。 開発環境については、yarn startではどうしてもhttp://localhost:3000になってしまうと思っていたが、こちらのサイトを参照し.env.development.localでHTTPS = "true"を定義することで簡単にhttps化できることを知り、それで解決できた。 自分でオレオレ証明書を発行して・・・とそれなりに面倒な手順を踏もうかと思っていたので、助かりました。 outbound80の手動穴あけ RSSフィード配信サイトに情報を取りに行く際、LambdaがインターネットアクセスするためにVPC・NAT Gateway等の定義をする必要があったが、それらを簡単に実施するためにserverless-vpc-pluginを使って開発していた。 その後、Lambda上でテストをする中でRSSフィードをうまく取得できるサイトと、取得できないサイトがあることがわかった。 原因を調べていくとserverless-vpc-pluginで構築した中でLambdaに割り当てられるセキュリティグループは、インターネットへのアウトバウンドアクセスが443(https)しか許可されておらず、"http://~"の配信サイトにアクセスできていないことが判明(特に、画像データだけはhttps→httpにリダイレクトして配信しているようなサイトもあり、なかなか検知しづらかったです・・・)。 serverless.yml上に自分でVPC等のリソースを定義しようかとも考えたが、結局deploy.ps1上でLambda実行用のセキュリティグループにOutbound 80ポートの穴あけを追加で定義することで解決しました。 #deploy.ps1抜粋 $TEMP_SG = aws cloudformation describe-stacks --output text --stack-name feed-notify-dev --query 'Stacks[].Outputs[?OutputKey==`AppSecurityGroupId`].[OutputValue]' aws ec2 authorize-security-group-egress --group-id $TEMP_SG --ip-permissions IpProtocol=tcp, FromPort=80, ToPort=80, IpRanges='[{CidrIp=0.0.0.0/0,Description="HTTP ACCESS for RSS Feed Subscribe"}]' Write-Output "Outbound HTTP Access added to Security Group" TypeError: Cannot read property 'pipe' of undefined 開発している途中で、sls deployするとタイトルの通りのエラーが表示されるようになった。 調査した結果、私の場合はserverless-layersとserverless-s3-syncの両方を同一ディレクトリにインストールしていたことが原因と判明。(もともとserver/serverless.ymlでクライアント側のソースも含めて一括デプロイしていました) serverless-layersはserverディレクトリ、serverless-s3-syncはclientディレクトリにインストールし、それぞれのディレクトリに分割してserverless.ymlを定義たうえでdeploy.ps1を用いて一括実行する形に見直すことで解決した。 yarn build時の環境変数読み込み deploy.ps1でバックエンド側のデプロイとクライアント側のソースビルド・デプロイを一括実行するようスクリプトを組む中で、タイトル部分の挙動が手動実行時と違うことを検知した。 もともとスクリプトを組む前はこちらのサイトを参考にproduction環境での環境変数をenv.productionに定義していたが、スクリプト上で.env.productionファイルを作成&yarn buildを実行するとうまくenv.productionファイルを読み込まないことが判明。 以下のように.env.productionファイルを使わず、直接環境変数に設定することで解決しました。おそらく権限周りの問題なのだと思われるが、よくわからず・・・。 #deploy.ps1抜粋 $env:REACT_APP_API_URL_PROD = aws cloudformation describe-stacks --output text --stack-name feed-notify-dev --query 'Stacks[].Outputs[?OutputKey==`ServiceEndpoint`].[OutputValue]' yarn build Write-Output "Client App Built" 終わりに 今回、初めてServerless Frameworkで本格的にアプリ構築をしてみましたが、いろいろとハマってしまって難しいと感じた面もありつつ、Serverless Frameworkそのものやプラグインがとても便利で、非常に勉強になりつつ楽しかったです。 ReactによるUI構築も勉強になったので、今回の経験を踏まえて、また新しいアプリ構築にチャレンジしてみたいと思います。
- 投稿日:2021-12-05T20:02:09+09:00
Discord.jsv13でwebhookを使ってグローバルチャットを作る
初めに 結構簡単だった 30分で組み立ててやったから細かいバグあるかもあったらBURI#9515まで連絡ください Operation webhookv13.findOne() buffering timed out after 10000ms というエラーはMONGOに関するエラーです 必要なもの aurora-mongo discord.js npm i discord.js aurora-mongo Mongoのaccesskey Mongoのaccesskeyの取得に関してはこちらを参照してください click me コード const {Client,Intents,WebhookClient} = require('discord.js'); const client = new Client({ intents: [Intents.FLAGS.GUILDS,Intents.FLAGS.GUILD_MESSAGES,Intents.FLAGS.GUILD_WEBHOOKS] }); const mongo = require("aurora-mongo"); /* arora-mongoを読み込む すずねーうさんが作成したモジュールです 詳しい使い方はこちら https://www.npmjs.com/package/aurora-mongo */ mongo.connect("YOUMONGOKEY") /* mongoとの接続 接続にはKeyが必要です 取得方法の動画はこちら https://www.youtube.com/watch?v=18hjrZCvxxk&ab_channel=BURI */ const chathook = new mongo.Database("webhookv13") //テーブル作る const list = []; //オブジェクト作る(DBとのやり取りを極力抑えるため)) client.on('ready', async() => { //クライアント(BOT)が起動したときのイベント console.log("起動"); //コンソールに起動と表示 list["chat"] = await chathook.keys(); //listオブジェクトにchatプロパティを作って保存してある(DBにある)チャンネルIDすべてを取得 }) client.on("messageCreate", async message => { //メッセージイベント発火 if (!message.guild || message.author.bot) return; //打たれたところがギルド以外そしてBOTだったら処理しない if (message.content === "!作成") { //打たれたメッセージが!作成だったら実行 if (!message.member.permissions.has("ADMINISTRATOR")) return message.channel.send('NOADOMIN'); //権限確認 if (!message.channel.permissionsFor(message.guild.me).has("MANAGE_WEBHOOKS")) return message.channel.send("NOWEBHOOKcreate"); //BOT自身の権限がwebhook作れるか確認 message.channel.createWebhook('BOTwebhook') //BOTwebhookという名前でwebhookを作る .then(async webhooks => { //成功した場合 await chathook.set(message.channel.id, `${webhooks.id},${webhooks.token}`) //DBにチャンネルIDをkeyにしてwebhookidとwebhooktokenを保存する list["chat"] = await chathook.keys(); //listオブジェクトのchatプロパティを更新する(新しく入ったチャンネルIDも併せて代入) message.channel.send(`作成成功`); //処理が終わったら作成成功と出す }).catch(e => message.channel.send(`エラー1:${e.message}`)); //webhookを作るのに失敗した場合エラーを出す await Promise.all(list["chat"].map(async hook => { //複数のPromise関数を実行し、全ての結果を得る(foreachの非同期処理版と考えてもらっていい) const webhook = await chathook.get(hook); //webhookにDBからチャンネルIDでとったwebhookidとwebhooktokenを代入する([webhookid,webhooktoken]となっている) const webhookid = webhook.split(",")[0]; //代入された値から,で区切って配列にして1番目(プログラミングでは0番目)の値をwebhookidに代入する const webhooktoken = webhook.split(",")[1]; //代入された値から,で区切って配列にして2番目(プログラミングでは1番目)の値をwebhooktokenに代入する const webhooks = new WebhookClient({id:webhookid, token:webhooktoken}) //webhooksにwebhook情報を代入 webhooks.send("グローバルチャットに参加しました。") //取得したwebhookにメッセージを送る .catch(e => message.channel.send(`エラー2${e.message}`)) //送れなかった場合エラーを出す })) } if (!list["chat"]?.includes(message.channel.id)) return; //listオブジェクトのchatプロパティに入ってるチャンネルIDとメッセージが打たれたチャンネルIDを比較なかったら処理停止 await Promise.all(list["chat"].map(async hook => { //複数のPromise関数を実行し、全ての結果を得る(foreachの非同期処理版と考えてもらっていい) if (hook === message.channel.id) return; //メッセージが打たれたチャンネルは処理しない const webhook = await chathook.get(hook); //webhookにDBからチャンネルIDでとったwebhookidとwebhooktokenを代入する([webhookid,webhooktoken]となっている) const webhookid = webhook.split(",")[0]; //代入された値から,で区切って配列にして1番目(プログラミングでは0番目)の値をwebhookidに代入する const webhooktoken = webhook.split(",")[1]; //代入された値から,で区切って配列にして2番目(プログラミングでは1番目)の値をwebhooktokenに代入する const webhooks = new WebhookClient({id:webhookid, token:webhooktoken}) //webhooksにwebhook情報を代入 const avater = await message.author.displayAvatarURL({ format: 'png' }) //メッセージを打った人のアバターをpngにフォーマットして取得 webhooks.send({ content:message.content, //打たれたメッセージを送る username: message.author.tag, //オプションのユーザーネームを打った人にする avatarURL: avater, //オプションのさっき取得したアバターをアイコンにする disableMentions: "all" //メンションを無効にする }).catch(e => message.channel.send(`エラー3${e.message}`)) //エラーが起きた場合は知らせる })) }); client.login("YOUTOKEN").catch(e => console.log(`ログイン時にエラーが起きました${e.message}`)) 解説 すべてコメントアウトに残した そこまで難しいことはやってない 最後に webhook苦手意識があるだけでやってみたら案外簡単だったことに驚いた 昔は苦戦した記憶が,,, バグあったらBURI#9515まで 尚今回はaurora-mongo特有のプロパティを使っているためkeyvにはできません
- 投稿日:2021-12-05T19:34:59+09:00
Discord.jsv12でwebhookを使ってグローバルチャットを作る
初めに 結構簡単だった 30分で組み立ててやったから細かいバグあるかもあったらBURI#9515まで連絡ください Operation webhook.findOne() buffering timed out after 10000ms というエラーはMONGOに関するエラーです 必要なもの aurora-mongo discord.js@12 npm i discord.js@12 aurora-mongo Mongoのaccesskey Mongoのaccesskeyの取得に関してはこちらを参照してください click me コード const Discord = require("discord.js") const client = new Discord.Client(); const mongo = require("aurora-mongo"); /* arora-mongoを読み込む すずねーうさんが作成したモジュールです 詳しい使い方はこちら https://www.npmjs.com/package/aurora-mongo */ mongo.connect("YOURMONGOKEY") /* mongoとの接続 接続にはKeyが必要です 取得方法の動画はこちら https://www.youtube.com/watch?v=18hjrZCvxxk&ab_channel=BURI */ const chathook = new mongo.Database("webhook") //テーブル作る const list = []; //オブジェクト作る(DBとのやり取りを極力抑えるため)) client.on('ready', async() => { //クライアント(BOT)が起動したときのイベント console.log("起動"); //コンソールに起動と表示 list["chat"] = await chathook.keys(); //listオブジェクトにchatプロパティを作って保存してある(DBにある)チャンネルIDすべてを取得 }) client.on("message", async message => { //メッセージイベント発火 if (!message.guild || message.author.bot) return; //打たれたところがギルド以外そしてBOTだったら処理しない if (message.content === "!作成") { //打たれたメッセージが!作成だったら実行 if (!message.member.permissions.has("ADMINISTRATOR")) return message.channel.send('NOADOMIN'); //権限確認 if (!message.channel.permissionsFor(message.guild.me).has("MANAGE_WEBHOOKS")) return message.channel.send("NOWEBHOOKcreate"); //BOT自身の権限がwebhook作れるか確認 message.channel.createWebhook('BOTwebhook') //BOTwebhookという名前でwebhookを作る .then(async webhooks => { //成功した場合 await chathook.set(message.channel.id, `${webhooks.id},${webhooks.token}`) //DBにチャンネルIDをkeyにしてwebhookidとwebhooktokenを保存する list["chat"] = await chathook.keys(); //listオブジェクトのchatプロパティを更新する(新しく入ったチャンネルIDも併せて代入) message.channel.send(`作成成功`); //処理が終わったら作成成功と出す }).catch(e => message.channel.send(`エラー1:${e.message}`)); //webhookを作るのに失敗した場合エラーを出す await Promise.all(list["chat"].map(async hook => { //複数のPromise関数を実行し、全ての結果を得る(foreachの非同期処理版と考えてもらっていい) const webhook = await chathook.get(hook); //webhookにDBからチャンネルIDでとったwebhookidとwebhooktokenを代入する([webhookid,webhooktoken]となっている) const webhookid = webhook.split(",")[0]; //代入された値から,で区切って配列にして1番目(プログラミングでは0番目)の値をwebhookidに代入する const webhooktoken = webhook.split(",")[1]; //代入された値から,で区切って配列にして2番目(プログラミングでは1番目)の値をwebhooktokenに代入する const webhooks = new Discord.WebhookClient(webhookid, webhooktoken) //webhooksにwebhook情報を代入 webhooks.send("グローバルチャットに参加しました。") //取得したwebhookにメッセージを送る .catch(e => message.channel.send(`エラー2${e.message}`)) //送れなかった場合エラーを出す })) } if (!list["chat"].includes(message.channel.id)) return; //listオブジェクトのchatプロパティに入ってるチャンネルIDとメッセージが打たれたチャンネルIDを比較なかったら処理停止 await Promise.all(list["chat"].map(async hook => { //複数のPromise関数を実行し、全ての結果を得る(foreachの非同期処理版と考えてもらっていい) if (hook === message.channel.id) return; //メッセージが打たれたチャンネルは処理しない const webhook = await chathook.get(hook); //webhookにDBからチャンネルIDでとったwebhookidとwebhooktokenを代入する([webhookid,webhooktoken]となっている) const webhookid = webhook.split(",")[0]; //代入された値から,で区切って配列にして1番目(プログラミングでは0番目)の値をwebhookidに代入する const webhooktoken = webhook.split(",")[1]; //代入された値から,で区切って配列にして2番目(プログラミングでは1番目)の値をwebhooktokenに代入する const webhooks = new Discord.WebhookClient(webhookid, webhooktoken) //webhooksにwebhook情報を代入 const avater = await message.author.displayAvatarURL({ format: 'png' }) //メッセージを打った人のアバターをpngにフォーマットして取得 webhooks.send(message.content, { //打たれたメッセージを送る username: message.author.tag, //オプションのユーザーネームを打った人にする avatarURL: avater, //オプションのさっき取得したアバターをアイコンにする disableMentions: "all" //メンションを無効にする }).catch(e => message.channel.send(`エラー3${e.message}`)) //エラーが起きた場合は知らせる })) }); client.login("YOUTOKEN").catch(e => console.log(`ログイン時にエラーが起きました${e.message}`)) 解説 すべてコメントアウトに残した そこまで難しいことはやってない 最後に webhook苦手意識があるだけでやってみたら案外簡単だったことに驚いた 昔は苦戦した記憶が,,, バグあったらBURI#9515まで 尚今回はaurora-mongo特有のプロパティを使っているためkeyvにはできません
- 投稿日:2021-12-05T18:26:20+09:00
文字列をVRCに投げる
1. この記事は 日経平均株価等の情報を外部から取得し、VRchat内のワールドに流し込む方法を解説します。 2. システム構成 外部からネットワーク経由でVRCに情報を取りこむには、動画をインターフェースにするしか現状手段がないようです。よって、文字列をUnicode(UTF-16BE)に変換、さらに色情報に変換を行い、動画を生成し、その動画をVRCに読み込ませるという方法が現在実現されている方法のようです。ここでは、まずWebサイトより日経平均株価を定期的に取得するアプリをRaspberry pi4に実装します。日経平均株価のテキスト情報を動画に変換し、Web上にアップロードします。動画情報をVRchat上で読み込ませ、動画情報をテキストに戻し、最終的にVRChat上のワールドに表示しております。Publicに公開するWebサーバーを自前で準備することは費用と手間がかかりますが、Herokuサービスを使うと個人目的であれば無料でWebサービスを実装することができます。 3 Raspberry Pi4上での作業 3-1 Raspberry pi4上にDocker & Ubuntuをインストールする。 Raspberry Pi4上にDockerをインストールする。 WindwosにDocker(Docker Desktop for windows)をインストールする場合は下記を見てください。 Docker上でUbuntu環境を立ち上げる。下記のコードを入力する。 (Windowsの場合、Powershellを立ち上げて、下記のコードを入力する。) /home/pi docker pull ubuntu ubuntu環境をdocker上に立ち上げる。 Powershell docker run -v /home/pi/dcshare:/home -it --privileged --publish 500:500 ubuntu:latest bash 上記を実施するとコンテナが生成される。 C:/Users/fdfpy/OneDrive/Documents/vrctest 3-2 Ubuntu上に必要なモジュールをインストールする。 先ほど作成したコンテナに入り下記のコマンドを実施し必要なライブラリをインストールする。 Ubuntu apt-get update cd home mkdir vrctest cd vrctest apt install npm ffmpeg npm install node #### npm install node でエラーが出る場合下記を実行すること ####### curl -fsSL https://deb.nodesource.com/setup_16.x apt-get install -y nodejs ################################################################ npm install -g npm yarn apt-get install wget npm install -g n n lts apt-get install git 3-3 必要なモジュールのインストール Ubuntu上にてサーバーにある文字列を動画に変換しVRCに流し込み、VRCの中で動画を文字列に変換するライブラリを先に引き続き、Ubuntu上にインストールします。 ・VRChatで文字列を動画から読み出すためのツールです home/vrctest ## (1)ライブラリをダウンロードし、buildを実施します。 git clone https://github.com/m-hayabusa/send_text_to_vrc.git git clone https://github.com/m-hayabusa/send_text_to_vrc_example.git git clone https://github.com/m-hayabusa/VRC_get_text_from_video.git cd send_text_to_vrc_example yarn yarn build 3-4 コードの変更 send_text_to_vrc_example/build/main.jsを下記のとおり変更する。 send_text_to_vrc_example/build/main.js import * as send_text_to_vrc from "send_text_to_vrc"; import fs from 'fs' const CSVPATH="./dat.json" const files = []; const file = new send_text_to_vrc.File("株価", ["ticker", "val"]); //ファイルの読み込みを行っている。 //fs.readFileSync 第1引数:読み取りたいファイルが配置されているパス、第2引数:文字コード設定 let json_obj=fs.readFileSync(CSVPATH,'utf-8') //ファイルの読み込みを行っている。 let json_dat=JSON.parse(json_obj) //文字列 -> 配列に変換 json_dat=JSON.parse(json_dat) //配列 -> 配列 let json_dat_today=JSON.stringify(json_dat.today) // 配列 -> JSONに変換 さらに変更 json_dat_today=JSON.parse(json_dat_today) //本日の株価を抽出する。 let json_dat_dif=JSON.stringify(json_dat.dif) // 配列 -> JSONに変換 さらに変更 json_dat_dif=JSON.parse(json_dat_dif) //前日との株価の差を出力する const NOW=json_dat_today['time'] const NOWDIF=json_dat_dif['time'] const N225=json_dat_today['9999'] const N225DIF=json_dat_dif['9999'] const DJI=json_dat_today['^DJI'] const DJIDIF=json_dat_dif['^DJI'] const GSPC=json_dat_today['^GSPC'] const GSPCDIF=json_dat_dif['^GSPC'] const JPYX=json_dat_today['^JPYX'] const JPYXDIF=json_dat_dif['^JPYX'] const VIX=json_dat_today['^VIX'] const VIXDIF=json_dat_dif['^VIX'] const CLF=json_dat_today['^CLF'] const CLFDIF=json_dat_dif['^CLF'] file.push(["time", NOW]); file.push(["time", NOWDIF]); file.push(["N225", N225]); file.push(["N225DIF", N225DIF]); file.push(["DJI", DJI]); file.push(["DJIDIF", DJIDIF]); file.push(["GSPC", GSPC]); file.push(["GSPCDIF", GSPCDIF]); file.push(["JPYX", JPYX]); file.push(["JPYXDIF", JPYXDIF]); file.push(["VIX", VIX]); file.push(["VIXDIF", VIXDIF]); file.push(["CLF", CLF]); file.push(["CLFDIF", CLFDIF]); files.push(file); send_text_to_vrc.publish(files, "/home/vrctest/node-js-getting-started/public/kaimonolist.webm"); 上記のコードで読み取りを行うdat.jsonの記載例を記述します。 send_text_to_vrc_example/build/dat.json "{\"today\":{\"time\":\"2022-01-02 11:52\",\"9999\":\"28791\",\"^CLF\":\"75\",\"^DJI\":\"36338\",\"^GSPC\":\"4766\",\"^JPYX\":\"115\",\"^VIX\":\"17\"},\"dif\":{\"time\":0,\"9999\":\"-115\",\"^CLF\":\"-1\",\"^DJI\":\"-59\",\"^GSPC\":\"-12\",\"^JPYX\":\"0\",\"^VIX\":\"0\"}}" 下記のコマンドを実施し、文字列をwebmファイルに変更します send_text_to_vrc_example #実行後、kaimonolist.webmファイルが生成される。 yarn start 3-5 Herokuをインストール & ログインする 上記で作成したUbuntuのコンテナにてHeroku CLIをインストールします。 home/vrctest apt-get install snapd npm install -g heroku home/vrctest ## (0)Herokuにログインする heroku login -i Email: xxxxxxxxxx Password: (API key) PasswordはAPI Keyを入力すること。API KeyはWebブラウザにてアカウントログインした後、Manage Accountで確認することができる。 3-6 Heroku上にWebアプリを生成する。 上記で作成したUbuntuのコンテナの/home/vrctest上にて下記の作業を実施する。 home/vrctest ## (1)node.jsを取得する。 git clone https://github.com/heroku/node-js-getting-started.git ## (2)Webサイトを生成する。 cd node-js-getting-started heroku create Creating app... done, ⬢ vast-oasis-59351 https://vast-oasis-59351.herokuapp.com/ | https://git.heroku.com/vast-oasis-59351.git ## https://vast-oasis-59351.herokuapp.com/上にWebアプリが作成される。 ## (3)アプリのファイルをHerokuにデプロイ git push heroku HEAD:master ## (4)ファイルを修正してデプロイしてみる git add . git commit -m "testx" git push heroku HEAD:master https://vast-oasis-59351.herokuapp.com/ を確認するとWebページが閲覧できる。 次に###2-3で生成したwebm動画をhome/VRCTEST/node-js-getting-started/publicにコピーします(ここではkaimonolist.webmと名前の動画ファイルとします)。次に動画のみを表示されるようにコードを下記のとおり変更します。 home/VRCTEST/node-js-getting-started/views/index.ejs <html> <body> <video controls autoplay name="media"> <source src="https://vast-oasis-59351.herokuapp.com/kaimonolist.webm", type="video/webm"> </video> Webサイトhttps://vast-oasis-59351.herokuapp.com/ にアクセスすると下記の動画が表示されます。 4 Unity上での作業 4-1 Assetのインポート 下記「VRC_get_text_from_video」の「Video2StrCore.prefab」をUnity上のAssets-> Import Package よりImportします。 HierarchyのVideo2StrCoreを選択し、InspectorのUrl欄に動画が表示されるWebサイト(ここでの例:https://vast-oasis-59351.herokuapp.com/kaimonolist.webm )を記載します。その他のアタッチメントも下記に画像のように設定を行います。 4-2 Camvasの作成 下記のサイトに従い、文字を表示させるCamvasを作成します。 4-3 データを表示させるコードを作成する。 下記のC#(Udonsharp)コードを作成します。 TestCanvas.cs using UnityEngine; using UdonSharp; using nekomimiStudio.video2String; using VRC.SDKBase; using UnityEngine.UI; public class Testlist : UdonSharpBehaviour { public Video2Str video2Str; // Video2StrCore についている Video2Str をここに割りあてる [UdonSynced(UdonSyncMode.None)] string[] _content = new string[20]; // [UdonSynced(UdonSyncMode.None)] 後から来た人にも同様な値を見せる private bool done = false; private Parser parser; [SerializeField] Text _time; [SerializeField] Text _n225Text; [SerializeField] Text _n225difText; [SerializeField] Text _sp500Text; [SerializeField] Text _sp500difText; [SerializeField] Text _vixText; [SerializeField] Text _vixdifText; [SerializeField] Text _clfText; [SerializeField] Text _clfdifText; [SerializeField] Text _jpydText; [SerializeField] Text _jpyddifText; void Start() { Debug.Log("###########121##############"); parser = video2Str.getParser(); } void Press1() { Debug.Log("###########1212##############"); parser = video2Str.getParser(); } //操作対象とするオブジェクトの所有者が別のユーザーだった場合、所有者をオブジェクト操作者に移動させる。 public void ChangeOwner() { if (!Networking.IsOwner(Networking.LocalPlayer, this.gameObject)) Networking.SetOwner(Networking.LocalPlayer, this.gameObject); } // Updateメソッドは毎フレーム呼ばれるため上方向に進み続けるサンプルとなっています。 void Update() { if (!done) { //Debug.Log($"progress: {video2Str.getDecodeProgress()}"); if (parser.isDone()) { for (int i = 0; i < parser.getLength("株価"); i++) { Debug.Log($"{i}: {parser.getString("株価", i, "ticker")}, {parser.getString("株価", i, "val")}"); _content[i] = parser.getString("株価", i, "val"); //_n225Text.text = _content[0].ToString(); } done = true; _time.text = _content[0].ToString(); _n225Text.text = _content[2].ToString(); _n225difText.text = _content[3].ToString(); _sp500Text.text = _content[6].ToString(); _sp500difText.text = _content[7].ToString(); _vixText.text = _content[10].ToString(); _vixdifText.text = _content[11].ToString(); _clfText.text = _content[12].ToString(); _clfdifText.text = _content[13].ToString(); _jpydText.text = _content[8].ToString(); _jpyddifText.text = _content[9].ToString(); } } } public override void Interact() { video2Str.reload(); } } 4-4 アタッチメント行う 下記の図を参考にし、Canvasオブジェクト(TestCanvas)のInspectorに「TestCanvas.cs」と「Video2StrCore」とその他表示文字のアタッチメントを行う
- 投稿日:2021-12-05T14:51:56+09:00
Discord.js v13で画像認証BOTを作ろう
初めに バグがあったら至急BURI#9515に連絡!! 今回は画像認証BOTを作ってみました replitなどでは使えませんので了承してください 必要なもの Discord.js canvas aurora-mongo npm i discord.js canvas aurora-mongo mongodbのkeyの取得方法はこちら click me Operation certification.findOne() buffering timed out after 10000ms と言うエラーはロール権限ではなくMONGOに問題がありますもう一度動画を見ながらやってください MONGODBが使えないよって方はこちらをクリック click me const mongo = require("aurora-mongo"); const { setTimeout } = require('timers/promises'); /* arora-mongoを読み込む すずねーうさんが作成したモジュールです 詳しい使い方はこちら https://www.npmjs.com/package/aurora-mongo */ const { Client, Intents, MessageAttachment } = require('discord.js'); const client = new Client({ intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES]}); //client関数 mongo.connect("YOUMONGOKey!!") /* mongoとの接続 接続にはKeyが必要です 取得方法の動画はこちら https://www.youtube.com/watch?v=18hjrZCvxxk&ab_channel=BURI */ const ser = new mongo.Database("certification"); //DBのテーブル作成 const deltime = 10; //メッセージを消す時間(秒) client.on('messageCreate', async message => { //messageイベント発火 if (!message.channel.type == "text" || message.author.bot) return; //textチャンネル以外とBOTは処理しない if (message.content.startsWith("!登録")) { //メッセージが!登録だった場合 if (!message.member.permissions.has("ADMINISTRATOR")) return message.channel.send('NOADOMIN'); //権限確認 const args = message.content.split(" ").slice(1); //メッセージのコマンドを抜いた部分を配列にしてargsに代入 const role = message.mentions.roles.first(); //メンションされた情報をroleに代入 if (!args[0]) return message.channel.send("引用がないよ"); //もしコマンドだけなら if (role) { //メンションされた場合の処理 message.member.roles.add(role) //メッセージを打った人にロールを与える .then(async() => { //成功した場合 await ser.set(message.guild.id, `${message.channel.id},${role.id}`) //DBにギルドID(key)でチャンネルIDとロールIDを書き出す message.channel.send(`登録が完了しました!!\nロール:${role}\nchannel:${message.channel}`); //完了のメッセージを送る }) .catch(e => message.channel.send(`エラー1:${e.message}`)); //失敗した場合 } else { //メンション以外 message.member.roles.add(args) //打たれた文字列をメッセージを打った人につける .then(async() => { //成功した場合 await ser.set(message.guild.id, `${message.channel.id},${args}`) //DBにギルドID(key)でチャンネルIDとロールIDを書き出す message.channel.send(`登録が完了しました!!\nロール:${message.guild.roles.cache.find(role => role.id === args)||"エラー"}\nchannel:${message.channel||"エラー"}`); //完了のメッセージを送るその際ロールIDをギルド内で検索してロールメンションの状態にする }) .catch(e => message.channel.send(`エラー2:${e.message}`)); //失敗した場合 } } if (message.content === "!削除") { //打たれたメッセージが!削除の場合 if (!message.member.permissions.has("ADMINISTRATOR")) return message.channel.send('NOADOMIN'); //権限確認 const content = await ser.get(message.guild.id); //contentにDBから取得した値を代入 if (!content) return message.channel.send("登録がされていません"); //もしも何も代入されていなかったら処理しない const check = content.split(","); //,で区切って配列にする await ser.delete(message.guild.id); //keyからそのサーバーの情報を消す message.channel.send(`情報を削除しました!!\n詳細\nチャンネル:${message.guild.channels.cache.find(channels => channels.id === check[0])}\nロール${message.guild.roles.cache.find(role => role.id === check[1])}`); //チャンネルIDとロールIDをギルド内で検索して表示している } if (message.content === "!確認") { //打たれたメッセージが!確認の場合 const content = await ser.get(message.guild.id); //contentにDBから取得した値を代入 if (!content) return message.channel.send("登録がされていません"); //もしも何も代入されていなかったら処理しない const check = content.split(","); //,で区切って配列にする message.channel.send(`登録詳細\nチャンネル:${message.guild.channels.cache.find(channels => channels.id === check[0])}\nロール:${message.guild.roles.cache.find(role => role.id === check[1])}`); //チャンネルIDとロールIDをギルド内で検索して表示している } if (message.content === "!認証") { //打たれたメッセージが!認証だったら message.delete() //!認証のメッセージを消す const content = await ser.get(message.guild.id); //DBからギルドIDを使ってチャンネルIDとロールIDを取得 if (!content) return message.channel.send("登録がされていません"); //取得できなかったら処理しない const check = content.split(","); //取得した値を,で区切って配列にする if (check[0] !== message.channel.id) return; //DBからとったチャンネルIDと現在のチャンネルIDがあってるか確かめるあっていなかったら処理しない const Canvas = require('canvas'); //canvas読み込み var S = "abcdefghjkmnpqrstuvwxyz23456789" //出したい文字列 var N = 6 //桁数 var A = Array.from(Array(N)).map(() => S[Math.floor(Math.random() * S.length)]).join(''); //ランダムな英数字を作成 //ここから下はdocs見て let fontSize = 50; const applyText = (canvas, text) => { const context = canvas.getContext('2d'); do { context.font = `${fontSize / 2}px serif`; } while (context.measureText(text).width > canvas.width - 300); return context.font; }; const canvas = Canvas.createCanvas(700, 250); const context = canvas.getContext('2d'); const background = await Canvas.loadImage('https://discordjs.guide/assets/canvas-preview.30c4fe9e.png'); //背景 context.drawImage(background, 0, 0, canvas.width, canvas.height); context.strokeStyle = '#0099ff'; context.strokeRect(0, 0, canvas.width, canvas.height); context.font = `${fontSize}px serif`; context.fillStyle = '#ffffff'; context.fillText(A, canvas.width / 2.5, canvas.height / 1.8); context.font = applyText(canvas, message.member.displayName); context.fillStyle = '#ffffff'; context.fillText(`${message.member.displayName}さんの認証コードは`, canvas.width / 2.5, canvas.height / 3.5); context.beginPath(); context.arc(125, 125, 100, 0, Math.PI * 2, true); context.closePath(); context.clip(); const avatar = await Canvas.loadImage(message.author.displayAvatarURL({ format: 'jpg' })); context.drawImage(avatar, 25, 25, 200, 200); context.drawImage(avatar, 25, 0, 200, canvas.height); const attachment = new MessageAttachment(canvas.toBuffer(), 'unko.png'); const main = await message.channel.send({ files: [attachment] }); //作った画像を送る const fel = m => m.author.id == message.author.id message.channel.awaitMessages({ fel, max: 1, time: deltime*1000, errors: ['time'] }) //メッセージをdeltime秒待つ .then(collected => { //成功した場合 if (!collected.first()) return message.channel.send("timeout"); //何も取得されていなかった場合処理しない collected.first().delete(); //取得したメッセージを消す if (collected.first().content == A) { //取得したメッセージがランダムな英数字と一緒だったら message.member.roles.add(check[1]) //メッセージを打った人にロールを付ける .then(() => { //成功した場合 message.channel.send("認証成功") //メッセージを送る .then(async d => { await setTimeout(deltime * 1000); d.delete() }); //メッセージを送るのに成功した場合メッセージをdeltime秒後に消す }) .catch(e => message.channel.send(`エラー4:${e.message}`)); //ロールを付けるのに失敗した場合 } else { //一緒ではなかったら message.channel.send("認証失敗") //失敗のメッセージを送る .then(async d => { await setTimeout(deltime * 1000); d.delete() }); //失敗メッセージをdeltime秒後に消す } }).catch(() => message.channel.send(`timeout`)); //メッセージを待ってる間にエラーが起きた場合 await setTimeout(deltime * 1000); main.delete() } }) client.login("YOUTOKEN!!") 機能説明 !登録 @ロールメンション又はロールID ロールを登録できます !削除 ロール情報を消します !確認 現在の情報を確認します !認証 認証コードを生成して認証します 以上です 実行結果 解説 解説はコメントアウトで残しときました 最後に バグがあったらBURI#9515まで おまけ MONGODBが使えないといいう方が少しいるのでkeyvを使ったやり方も書いときます なんとaurora-mongoはkeyvとほぼ書き方が同じなので移行がチョー楽!! keyvで書いたのをaurora-mongoでも使えます 必要なもの discord.js keyv @keyv/sqlite canvas npm i discord.js keyv @keyv/sqlite canvas コード(MONGOができない人用) const Keyv = require('keyv') const { setTimeout } = require('timers/promises'); const { Client, Intents, MessageAttachment } = require('discord.js'); const client = new Client({ intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES]}); //client関数 const ser = new Keyv('sqlite://db.sqlite', { table: 'certification' }) //DBのテーブル作成 const deltime = 10; //メッセージを消す時間(秒) client.on('messageCreate', async message => { //messageイベント発火 if (!message.channel.type == "text" || message.author.bot) return; //textチャンネル以外とBOTは処理しない if (message.content.startsWith("!登録")) { //メッセージが!登録だった場合 if (!message.member.permissions.has("ADMINISTRATOR")) return message.channel.send('NOADOMIN'); //権限確認 const args = message.content.split(" ").slice(1); //メッセージのコマンドを抜いた部分を配列にしてargsに代入 const role = message.mentions.roles.first(); //メンションされた情報をroleに代入 if (!args[0]) return message.channel.send("引用がないよ"); //もしコマンドだけなら if (role) { //メンションされた場合の処理 message.member.roles.add(role) //メッセージを打った人にロールを与える .then(async() => { //成功した場合 await ser.set(message.guild.id, `${message.channel.id},${role.id}`) //DBにギルドID(key)でチャンネルIDとロールIDを書き出す message.channel.send(`登録が完了しました!!\nロール:${role}\nchannel:${message.channel}`); //完了のメッセージを送る }) .catch(e => message.channel.send(`エラー1:${e.message}`)); //失敗した場合 } else { //メンション以外 message.member.roles.add(args) //打たれた文字列をメッセージを打った人につける .then(async() => { //成功した場合 await ser.set(message.guild.id, `${message.channel.id},${args}`) //DBにギルドID(key)でチャンネルIDとロールIDを書き出す message.channel.send(`登録が完了しました!!\nロール:${message.guild.roles.cache.find(role => role.id === args)||"エラー"}\nchannel:${message.channel||"エラー"}`); //完了のメッセージを送るその際ロールIDをギルド内で検索してロールメンションの状態にする }) .catch(e => message.channel.send(`エラー2:${e.message}`)); //失敗した場合 } } if (message.content === "!削除") { //打たれたメッセージが!削除の場合 if (!message.member.permissions.has("ADMINISTRATOR")) return message.channel.send('NOADOMIN'); //権限確認 const content = await ser.get(message.guild.id); //contentにDBから取得した値を代入 if (!content) return message.channel.send("登録がされていません"); //もしも何も代入されていなかったら処理しない const check = content.split(","); //,で区切って配列にする await ser.delete(message.guild.id); //keyからそのサーバーの情報を消す message.channel.send(`情報を削除しました!!\n詳細\nチャンネル:${message.guild.channels.cache.find(channels => channels.id === check[0])}\nロール${message.guild.roles.cache.find(role => role.id === check[1])}`); //チャンネルIDとロールIDをギルド内で検索して表示している } if (message.content === "!確認") { //打たれたメッセージが!確認の場合 const content = await ser.get(message.guild.id); //contentにDBから取得した値を代入 if (!content) return message.channel.send("登録がされていません"); //もしも何も代入されていなかったら処理しない const check = content.split(","); //,で区切って配列にする message.channel.send(`登録詳細\nチャンネル:${message.guild.channels.cache.find(channels => channels.id === check[0])}\nロール:${message.guild.roles.cache.find(role => role.id === check[1])}`); //チャンネルIDとロールIDをギルド内で検索して表示している } if (message.content === "!認証") { //打たれたメッセージが!認証だったら message.delete() //!認証のメッセージを消す const content = await ser.get(message.guild.id); //DBからギルドIDを使ってチャンネルIDとロールIDを取得 if (!content) return message.channel.send("登録がされていません"); //取得できなかったら処理しない const check = content.split(","); //取得した値を,で区切って配列にする if (check[0] !== message.channel.id) return; //DBからとったチャンネルIDと現在のチャンネルIDがあってるか確かめるあっていなかったら処理しない const Canvas = require('canvas'); //canvas読み込み var S = "abcdefghjkmnpqrstuvwxyz23456789" //出したい文字列 var N = 6 //桁数 var A = Array.from(Array(N)).map(() => S[Math.floor(Math.random() * S.length)]).join(''); //ランダムな英数字を作成 //ここから下はdocs見て let fontSize = 50; const applyText = (canvas, text) => { const context = canvas.getContext('2d'); do { context.font = `${fontSize / 2}px serif`; } while (context.measureText(text).width > canvas.width - 300); return context.font; }; const canvas = Canvas.createCanvas(700, 250); const context = canvas.getContext('2d'); const background = await Canvas.loadImage('https://discordjs.guide/assets/canvas-preview.30c4fe9e.png'); //背景 context.drawImage(background, 0, 0, canvas.width, canvas.height); context.strokeStyle = '#0099ff'; context.strokeRect(0, 0, canvas.width, canvas.height); context.font = `${fontSize}px serif`; context.fillStyle = '#ffffff'; context.fillText(A, canvas.width / 2.5, canvas.height / 1.8); context.font = applyText(canvas, message.member.displayName); context.fillStyle = '#ffffff'; context.fillText(`${message.member.displayName}さんの認証コードは`, canvas.width / 2.5, canvas.height / 3.5); context.beginPath(); context.arc(125, 125, 100, 0, Math.PI * 2, true); context.closePath(); context.clip(); const avatar = await Canvas.loadImage(message.author.displayAvatarURL({ format: 'jpg' })); context.drawImage(avatar, 25, 25, 200, 200); context.drawImage(avatar, 25, 0, 200, canvas.height); const attachment = new MessageAttachment(canvas.toBuffer(), 'unko.png'); const main = await message.channel.send({ files: [attachment] }); //作った画像を送る const fel = m => m.author.id == message.author.id message.channel.awaitMessages({ fel, max: 1, time: deltime*1000, errors: ['time'] }) //メッセージをdeltime秒待つ .then(collected => { //成功した場合 if (!collected.first()) return message.channel.send("timeout"); //何も取得されていなかった場合処理しない collected.first().delete(); //取得したメッセージを消す if (collected.first().content == A) { //取得したメッセージがランダムな英数字と一緒だったら message.member.roles.add(check[1]) //メッセージを打った人にロールを付ける .then(() => { //成功した場合 message.channel.send("認証成功") //メッセージを送る .then(async d => { await setTimeout(deltime * 1000); d.delete() }); //メッセージを送るのに成功した場合メッセージをdeltime秒後に消す }) .catch(e => message.channel.send(`エラー4:${e.message}`)); //ロールを付けるのに失敗した場合 } else { //一緒ではなかったら message.channel.send("認証失敗") //失敗のメッセージを送る .then(async d => { await setTimeout(deltime * 1000); d.delete() }); //失敗メッセージをdeltime秒後に消す } }).catch(() => message.channel.send(`timeout`)); //メッセージを待ってる間にエラーが起きた場合 await setTimeout(deltime * 1000); main.delete() } }) client.login("YOUTOKEN!!")
- 投稿日:2021-12-05T10:15:06+09:00
【初心者向け】npmについてよく知らない人のための教科書 ~npm installは何をおこなっているのか~
はじめに 対象はnpmを学習し始めた人、npmへの理解が浅い人となります。 本記事は、npm(node package manager)に苦手意識があった筆者が克服するために勉強した備忘録です。 開発環境 Apple M1 Big Sur 11.6 Node.js 16.13.0 npm 8.1.0 npmとは こちらについては、【初心者向け】NPMとpackage.jsonを概念的に理解するをお読みください。 「パッケージとは何か」や「package.jsonとpackage-lock.jsonの違い」など、とても理解がしやすいようにまとめられております。 いつかこんな素敵な記事を書いてみたいです。 npm installとは パッケージをインストールするコマンドです。 package.jsonのdependenciesとdevDependenciesに書かれているパッケージをインストールする npm installのエイリアスは、 npm i または npm add です。 まれにエイリアスを使って、解説している記事があり、「このコマンド初めて見たかも?」と思ったものはエイリアスの可能性が高いです。 また、公式を見てみると、他のコマンドにもエイリアスがたくさんあることがわかります。 dependenciesとは 本番環境で必要なパッケージが書かれている dependenciesにパッケージを追加したいときは、 npm install <package-name> --save-prod --save-prodを、-Pとしても可能 であるが、「--save-prod」はデフォルトなため npm install <package-name> と、「--save-prod」を省略できます。 devDependenciesとは 開発環境やテストに必要なパッケージが書かれている devDependenciesにパッケージを追加したいときは npm install <package-name> --save-dev --save-devを、-Dとしても可能 さいごに わからないことがあった時に、Qiitaやドキュメント以外の記事を漁るクセなおしたい。 まず最初に目を通すべきは、やはりドキュメントですね! 参考・おすすめ記事 「『package.json』って何?って時に少し調べた時のノート」 https://overworker.hatenablog.jp/entry/2020/09/27/232236 「package-lock.jsonってなに?」 https://qiita.com/sugurutakahashi12345/items/1f6bb7a372b8263500e5 「package.json のチルダ(~) とキャレット(^)」 https://qiita.com/sotarok/items/4ebd4cfedab186355867 「【いまさらですが】package.jsonのdependenciesとdevDependencies」 https://qiita.com/chihiro/items/ca1529f9b3d016af53ec 「npmパッケージのvulnerability対応フロー」 https://qiita.com/riversun/items/7f1679509f38b1ae8adb
- 投稿日:2021-12-05T04:20:34+09:00
【Node.js 2021(2つ目)】 Node.js での UDP・TCP通信をシンプルに試す(2021年12月)
この記事は、2021年の Node.js のアドベントカレンダー の 10日目の記事です。 内容は、以下の記事を書く中で使った「zigsim-ws」の中で利用されている「dgram」が気になって調べたり試したりしたことを、記事として書いた形です。 ●【IoTLT 2021】 ZIG SIM から送られるデータを p5.js Web Editor上で活用してみる - Qiita https://qiita.com/youtoy/items/caca41a68ab3bff6ffa6 zigsim-ws のプログラムと UDP通信 zigsim-ws は、プロトタイピングに役立つスマホアプリの「ZIG SIM」を、ブラウザ(HTML+JavaScript のプログラム)との間で通信させる時に利用するものです。 軽く補足をすると、ZIG SIM がデータを送る時に用いる通信が UDP・TCP で、ブラウザ上の JavaScript のプログラムでは直接は扱えないものになるので、それらの間で通信できるようにするためには仲介役が必要です。zigsim-ws は、その仲介役として UDP 通信をブラウザ上で動く JavaScript が扱える WebSocket の通信に変換する役割を担います。 ●zigsim-ws/index.js at master · acrylicode/zigsim-ws https://github.com/acrylicode/zigsim-ws/blob/master/index.js そのプログラムは、以下のような実装になっています。 const dgram = require('dgram'); const WebSocket = require('ws'); function init(updPort = 50000, wsServerPort = 8080) { const updServer = dgram.createSocket('udp4'); const wss = new WebSocket.Server({ port: wsServerPort }); console.log(`websocket server listening on port: ${wsServerPort}`) wss.on('connection', (ws) => { console.log(`New client connected to the websocket. Number of clients: ${wss.clients.size}`) }); updServer.on('error', (err) => { console.log(`upd server error:\n${err.stack}`); updServer.close(); }); updServer.on('message', (msg) => { wss.clients.forEach(client => { if (client.readyState === WebSocket.OPEN) { client.send(msg.toString()) } }); }); updServer.on('listening', () => { const address = updServer.address(); console.log(`udp server listening on port: ${address.port}`); }); updServer.bind(updPort); } module.exports.init = init; 冒頭の 2行目の部分は、WebSocketサーバーを動かすためのものを読み込んでいますが、1行目で「dgram」を読み込んでいます。 dgram について見てみる npm のページを見て、さらに情報をチェックしようと思って以下にアクセスしました。 ●dgram - npm https://www.npmjs.com/package/dgram そうすると、以下をのような記載が...(deprecated になってる...) そして、さらに調査をしていきました。 dgram についてさらにチェック 先ほどの npm のページの記載をあらためて見てみると 「As it's a core Node module, we will not transfer it ...」 と書かれていました。 さらに、Node.js の公式情報を合わせて見てみると、Node.js自体の機能の話で「UDP/datagram sockets」というものが出てきました。 ●UDP/datagram sockets | Node.js v17.2.0 Documentation https://nodejs.org/api/dgram.html 結論を書くと、先ほどの zigsim-ws は、こちらの Node.js内のものを呼んでいたようです。 (※ 自分が zigsim-ws のプログラムを実行していたフォルダ等を見ると、node_modulesフォルダ以下に dgram のフォルダがなかった) そして、Node.js での UDP通信の話を見ていくと、以下の内容も見つかりました。 こちらの記事でも、Node.js に元から入っていそうな dgram の話をされています。 ●Node.jsでUDP通信する - Re: note https://hikoleaf.hatenablog.jp/entry/2019/06/08/235753 Node.js で UDP通信を実行する それでは、実際に Node.js での UDP通信を、自分の環境で実行してみます。 2者間の単純な通信 先ほど、調べて出てきたと書いていたサイトを見てみると、2者間でのシンプルな通信の例が掲載されていました。 そこで、まずはこの例をそのまま利用します。 1点、元の記事で $ npm install dgram を実行する手順が書いてありますが、これは不要だと思われます。 (これをやってしまうと、上記で deprecated となってたやつを読み込んでしまいそうな予感...) 以下の2つのプログラムを用意し、2つのターミナルを開いて、それぞれで 1つずつのプログラムを実行すると、定期的にデータのやりとりが行われている様子が確認できました。 const dgram = require('dgram'); const PORT_A = 3002; const HOST_A = '127.0.0.1'; const PORT_B = 3003; const HOST_B = '127.0.0.1'; const socket = dgram.createSocket('udp4'); socket.on('listening', () => { const address = socket.address(); console.log('UDP socket listening on ' + address.address + ":" + address.port); }); socket.on('message', (message, remote) => { console.log(remote.address + ':' + remote.port +' - ' + message); socket.send(message, 0, message.length, PORT_B, HOST_B, (err, bytes) => { if (err) throw err; }); }); socket.bind(PORT_A, HOST_A); const dgram = require('dgram'); const PORT_A = 3002; const HOST_A ='127.0.0.1'; const PORT_B = 3003; const HOST_B ='127.0.0.1'; const socket = dgram.createSocket('udp4'); var count = 0; setInterval(() => { count++; const data = Buffer.from(String(count)); socket.send(data, 0, data.length, PORT_A, HOST_A, (err, bytes) => { if (err) throw err; }); }, 500); socket.on('message', (message, remote) => { console.log(remote.address + ':' + remote.port +' - ' + message); }); socket.bind(PORT_B, HOST_B); これらを実行すると、一方から一定間隔で他方へカウントアップする文字列を送り、それを受信した側はエコーバックするという動作が確認できました。 2つのプログラム間での UDP通信! https://t.co/PHpipZ0fZs pic.twitter.com/2evFUu7Um8— you (@youtoy) December 5, 2021 そういえば、●● over UDP という形のものを使うことはよくあったけど、UDP を直接扱うようなことはこれまでなかったかも。 Node.js で TCP通信を実行する この後は、UDP通信を行うプログラムに手を入れて何かやろうかとも思ったのですが、ふと「TCP はどんな感じなんだろう?」という考えが頭をよぎりました。 そして、記事を検索したところ、以下のものが出てきました。 非常にシンプルなプログラムが掲載されていて、試してみるのにちょうど良さそうです。 ●Node.jsでTCP通信する - Re: note https://hikoleaf.hatenablog.jp/entry/2019/06/09/131620 UDP に関する記事と同様に $ npm install net を実行する手順が書いてありますが、これは不要な感じが。 const net = require('net'); const server = net.createServer(socket => { socket.on('data', data => { console.log(data + ' from ' + socket.remoteAddress + ':' + socket.remotePort); socket.write('server -> Repeating: ' + data); }); socket.on('close', () => { console.log('client closed connection'); }); }).listen(3000); console.log('listening on port 3000'); const net = require('net'); const client = net.connect('3000', 'localhost', () => { console.log('connected to server'); client.write('Hello World!'); }); client.on('data', data => { console.log('client-> ' + data); client.destroy(); }); client.on('close', () => { console.log('client-> connection is closed'); }); そのまま動かしてみたら、以下のように通信が行えました。 1度データを送信すると、送信側は終了する挙動のようだったので、何度か送信をやってみています。 先ほどの例をそのまま動かしただけ、なのだけど、Node.js で TCP! pic.twitter.com/osz9wifMfQ— you (@youtoy) December 5, 2021 そういえば、●● over TCP という形のものを...(以降、省略) おわりに 既存のサンプルを動かしただけではありますが、プログラムをざっくり見てみると、Node.js ではかなりシンプルな実装で UDP や TCP の通信を扱えるんだな、というのを感じました。 たしか、「M5Stack系の UIFlow で、UDP を扱うブロックがあったな」とか思い浮かんだので、それとつないでみるとか、何かこれを使ったデバイス/アプリ/サービス間通信を試せたら、とか思っています。 たしかあったよな...、と思って確認したら、やはり!#UIFlow でソケット通信カテゴリと、その中の UDP。#M5Stack pic.twitter.com/c2fYvyB70D— you (@youtoy) December 5, 2021
- 投稿日:2021-12-05T01:35:20+09:00
【IoTLT 2021】 ZIG SIM から送られるデータを p5.js Web Editor上で活用してみる
この記事は、2021年の IoTLT のアドベントカレンダー の 5日目の記事です。 内容は、以下の記事を書いた時にも使った ZIG SIM の話がメインです。 ●ZIG SIM と Node.js のプログラム(osc.js を利用)との間で UDP による OSC通信を軽く試す(OSC Data Monitor の話も) - Qiita https://qiita.com/youtoy/items/fddc750759f4ecef7ca7 これをブラウザ上で動く p5.js を使ったプログラムで活用します。 また、開発環境として p5.js Web Editor を利用します。 ZIG SIM について ZIG SIM はプロトタイピングに便利なアプリで、2〜3年前くらいに知ったものの、あまり活用できていませんでした。 それを、ちょこちょこ試し始めて、冒頭に掲載した記事を書いたりしていました。 公式の紹介動画 以下は、公式の紹介動画です。 利用事例 Twitter で #ZIGSIM のハッシュタグで検索をすると、活用された方の事例がを見ることができます。 ざっくり見ると、TouchDesigner と合わせて使っている例が多い気がします。 ZIG SIM をブラウザ上で動く JavaScript のプログラムで扱う ZIG SIM で使われる通信 ZIG SIM のアプリの設定では、通信プロトコルで「UDP」か「TCP」を選ぶことができ、メッセージのフォーマットは「JSON」か「OSC」のどちらかを選ぶことができます。 このような通信方法になるため、ブラウザ上で動く JavaScript のプログラムでは、直接データを受けとることができません(ブラウザが、UDP・TCP を直接扱えないため)。 冒頭に記載した記事で、osc.js を使った ZIG SIM との通信を試していましたが、その理由の1つが「osc.js で OSC over UDP を OSC over WebSocket に変換」して、ブラウザ上で動くプログラムと ZIG SIM との間の通信を実現する、というものでした。 zigsim-ws そんな中、以下のリポジトリを見つけました。 ●acrylicode/zigsim-ws https://github.com/acrylicode/zigsim-ws こちらの「How does it work ?」の部分やソースコードを見てみると、仕組みは ZIG SIM から UDP で送られてくるデータを受信し、それを WebSocket で送り出す、という仕組みのようです。 使い方は非常に簡単で、以下を実行するだけです。 npm install zigsim-ws を実行 以下の内容のプログラムを作成する プログラムを実行する zigsim-ws を使うためのプログラムは、以下の通りです。 const zigsimWs = require("zigsim-ws") zigsimWs.init(); プログラムを実行すると、以下のようなメッセージが表示されます。 ZIG SIM からのデータを 50000番ポートで待ち受け、それを WebSocket でクライアントへと送ります。 ここで書いたクライアントは、8080版ポートで待ち受けをしている WebSocketサーバーに接続に来たクライアントを意味します。 ZIG SIM と p5.js を使ったプログラムとの通信1 これで、ZIG SIM のデータを WebSocketサーバーに接続したクライアントへ送ることができるようになりました。 ZIG SIMアプリで、「プロトコル: UDP」・「メッセージフォーマット: JSON」に設定してデータの送信を行い、 p5.js Web Editor上では、WebSocketサーバーからメッセージを受信するプログラムを動かします。 p5.js Web Editor で扱うプログラムは、以下の記事を書いた時に作ったものを利用します。 ●p5.js Web Editor で WebSocket を扱ってみる(wscat でローカル環境にサーバーを準備して試したり、OBS連携を実行) - Qiita https://qiita.com/youtoy/items/40220a09b98a89013d0d 少しだけ手を加え、以下のような処理にしました。 let socket; function setup() { createCanvas(400, 400); background(220); noLoop(); socket = new WebSocket("ws://localhost:8080"); socket.addEventListener("open", function (event) { console.log("OK!"); }); socket.addEventListener("message", function (event) { const receivedData = event.data; console.log("Message from server ", receivedData); }); } function draw() {} #ZIGSIM アプリのデータを、 #p5js Web Editor上で受けとれた!とりあえず、受けとった JSON をログで出力しただけ、という感じのものだけれど。仲介役として、zigsim-ws を使っています。 https://t.co/uHPDelnALV pic.twitter.com/rHc0KeNMQk— you (@youtoy) December 4, 2021 ZIG SIM と p5.js を使ったプログラムとの通信2 次はこちらです。 先ほどの p5.js を使ったプログラムに、少し手を加えてみました。 #ZIGSIM アプリの 2Dタッチの座標データを、 #p5js Web Editor上で受けとって、キャンバス上に描画してみた! pic.twitter.com/Q85FgPfXZl— you (@youtoy) December 4, 2021 プログラムは、このような感じです。 let socket, x = [0, 0, 0, 0, 0], y = [0, 0, 0, 0, 0]; const colors = ["#6E3CBC", "#7267CB", "#98BAE7", "#B8E4F0"]; function setup() { createCanvas(400, 400); socket = new WebSocket("ws://localhost:8080"); socket.addEventListener("open", function (event) { console.log("OK!"); }); socket.addEventListener("message", function (event) { const receivedData = JSON.parse(event.data); // console.log(receivedData); const touchData = receivedData.sensordata.touch; // console.log(touchData); for (let i = 0; i < 5; i++) { if (i < touchData.length) { x[i] = map(touchData[i].x, -1, 1, 0, width); y[i] = map(touchData[i].y, -1, 1, 0, height); } else { x[i] = undefined; y[i] = undefined; } } }); } function draw() { background(220); for (let i = 0; i < 4; i++) { fill(colors[i]); ellipse(x[i], y[i], 50); } } おわりに ZIG SIM から送られるデータを、zigsim-ws を介して p5.js Web Editor上で受信し利用することができるようになりました。 今回使った zigsim-ws のソースを見てみると、わりとシンプルな実装になっていたため、このプログラムに直接手を入れて活用するようなこともできればと思っています。 ●zigsim-ws/index.js at master · acrylicode/zigsim-ws https://github.com/acrylicode/zigsim-ws/blob/master/index.js
- 投稿日:2021-12-05T00:02:00+09:00
【Node.js】Puppeteerで発生したUnhandledPromiseRejectionWarningエラーを解決する
はじめに 普段はVue.js, Nuxt.jsを使った開発をしているのですが、興味本位でExpressにも触れている者です。見よう見まねでpuppeteerのコードを書いていたのですが、使いやすいように改造した際、エラーが発生。解決までに時間がかかったのでメモを残します。 # エラー内容 UnhandledPromiseRejectionWarning: Error: Evaluation failed: ReferenceError: shop is not defined 原因 $$eval内の関数はchrome上で実行されるもので、この中で事前に定義した変数とか使おうとするとエラる。 とのことでevaluate内で変数を使ったのが原因っぽいです。実際リファクタ前の文字列ベタ打ちの時はエラーが起きていなかったので。 エラーが起きた時のコード const postPrices = await page.evaluate(() => { const prices = document.querySelectorAll(shopPrice); let getPrices = []; for (let price of prices) { getPrices.push({ price: price.textContent }); } return getPrices; }); 解決した時のコード evaluateを使わずに下記コードで実装。変数を使っても問題なかった。ついでにオブジェクト化させて配列に入れる処理も変更。 const priceSelector = await page.$$(shop.pricePath); const nameSelector = await page.$$(shop.namePath); const resultArray = []; for (let i = 0; i < priceSelector.length; i++) { const data = { price: await ( await priceSelector[i].getProperty("textContent") ).jsonValue(), name: await ( await nameSelector[i].getProperty("textContent") ).jsonValue(), }; resultArray.push(data); } 全体のコード const puppeteer = require("puppeteer-core"); const executablePath = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"; const scraping = async (shop) => { const browser = await puppeteer.launch({ executablePath: executablePath, slowMo: 500, headless: false, }); const page = await browser.newPage(); await page.setDefaultNavigationTimeout(0); await page.goto(shop.url, { waitUntil: "networkidle2", }); // scraping const priceSelector = await page.$$(shop.pricePath); const nameSelector = await page.$$(shop.namePath); const resultArray = []; for (let i = 0; i < priceSelector.length; i++) { const data = { price: await ( await priceSelector[i].getProperty("textContent") ).jsonValue(), name: await ( await nameSelector[i].getProperty("textContent") ).jsonValue(), }; resultArray.push(data); } await browser.close(); return resultArray; }; module.exports = { scraping }; おわりに evaluateに変数を入れるとエラーが出るという問題に対して深掘りができず解決ではなく回避という結果になってしまったので時間がある時にevaluateの挙動について調べていきたいです。 突貫で作ったので参考にする際はリファクタした方が良いかと…とりあえずPuppeteerで発生したUnhandledPromiseRejectionWarningエラーで躓く人がいなくなれば幸いです。 参考URL