20201212のNode.jsに関する記事は11件です。

メモリリークは滅びぬ!何度でも蘇るさ!

今年もたくさんクソコードを書きました。障害対応もたくさんしました。
その中でも特にしんどかった障害について回想したいと思います。

環境

  • Amazon ECS
  • node:alpine

嵐の前の静けさ

この環境で動いているのは、私が前任者から引き継いだサブシステムの1つでした。

プロジェクトの中でもほとんど改修されることもなければ、特に改善の話も出てこなかったこのシステムに、突如として新APIを追加するタスクが私に降ってきました。

好きに作ってくれて良いと言われたNode.js素人の私は、ソースコードにごm、、APIを水を得た魚のように追加していくタスクに明け暮れました。

Expressを使っていくうちに感じる開発のスピード感や、バリバリ使われているPromiseをawait/asyncにリファクタリングしていく爽快感。。

「なるほど、、完全に理解した」

初心者特有の勘違いをしたままの私は、その自信満々のソースコードを引っ提げてレビューを依頼するのでした。

そして散々に指摘されまくった私の改修したソースコードは晴れて日の目を見ることになります。

障害は突然に

それはリリースされて数ヶ月後の出来事でした。

お客さんから報告された内容としては、

「未登録データが多いのでサーバで何か問題が起きてるのではないか」

といったざっくりとした内容でした。

この時、一次調査をメンバーが担当していたので私はそんなことつゆ知らずといった感じで別タスクに精を出していました。

そして、一次調査を行ったメンバーから次のような報告を受けました。

メンバー「ログを追ってみたところ、〇〇(私)さんが以前担当していたサブシステムでメモリリークが起きてコンテが再起動してて、それが尾を引いて未登録データが増えてるみたいっす」

わたし「、、ほーん、、なるほどですね(わかってない)」

、、と、正気に戻って詳しく内容をみてみると、確かにここ数ヶ月でユーザー数が増えたことにより、当初想定していたコンテナ負荷を軽々と超えていたということが分かりました。
(「なぜアラート通知やモニタリングをしていないんだ?」という正論も飛んできそうですが、、)

最初の過ち

とりあえずエラーメッセージを手がかりに、具体的な対策を打つ方法を調べてみましたが、

  • Node.jsが使用するメモリ容量(ヒープ)の上限値を上げる(デフォルトは1.4GB)

という情報しか出てきませんでした。

そのほかにもリファクタリングしたところがまずかったのではないかと、ソースコードを確認しましたが、リファクタリングした箇所でもメモリリークと関連つけて報告されてるような記事を見つけられませんでした。

とにかく最初に出てきた情報頼りで、メンバーに打診しようとした直前、

メンバー「とりあえずECSのスケールアップ(メモリ増やす)が先ですかね〜」

と回答が来ていて、「なるほど、まずはスケールアップが先決か、、」とすっかり思い込んだ私は、検証環境でスケールアップしたシステムのテストを行い、実際のアクセス数と同様の負荷に耐えられることを確認しました。

そして障害報告のあった翌日、本番環境のスケールアップを行い、この障害はこれで解決、、かのように見えました。

悪夢再び

それはスケールアップを行った翌日のことでした。

翌日は、私に代わってメンバーがスケールアップ後の本番環境のログを追っていました。
そして、当日昼にメンバーから以下のような報告を受けました。

メンバー「メモリリーク、起きちゃってました?」

メモリリークってそんなに頻繁に起きるものなのか。。

上の空状態の私は、また同じトレースログを追って同じ記事を見つけ、

「よし、、もうこれしかない、、これしかないよな、、」

とNode.jsが使用するメモリ容量を上げることを前提にした報告を考えていました。

救いの手が差し伸べられる

ここで、別チームのエンジニアから1つの画像とリンクが送られてきました。

memory_leak.png
https://github.com/nodejs/node/issues/29038

別チームエンジニア「ここで報告されてる通り、対象のDockerイメージだとメモリリークするみたいですね。」

本番のメトリクスを確認すると、確かに同じようなグラフの形をしていました。

報告されているのはnodeのベースイメージで対象のalpine版だとメモリリークが起きるとのことで、まさにピンポイントのベースイメージを使用していました。

試しに、ベースイメージを現状のalpine版とalpineではないイメージとで比較して負荷テストを行った結果、確かにalpineではないイメージは上記のようなグラフにはならずにメモリが解放されて波打つようなグラフになりました。

そして、対応としてはalpineではないイメージをベースイメージとして使用するという結論に至りました。

(結局、私が打診しようとしていたNode.jsのメモリ容量を上げる対策だけを行ったところで延命措置にしかならなかったと気づくこともできました?)

まとめ

経験が浅いからとかそういうのではなく、反省の多い障害対応(トラブルシューティング)だったと思います。。
(エラーメッセージに固執してしまったり、負荷テストが十分でなかったり、モニタリングしてなかったり、そもそもなんでメトリクスに気づけなかったんだとか思ったり、、)

と、思うことに留めずに反省をバネにして来年もトラブルシューティングしていきます?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

npm でインストールした tiddlywiki にプラグインをインストールする

ひじょうに困ったのと、時間を要したので同じようなめにあうことがないように記しておきます。

少ないと思いますけど。

tiddlywiki a non-linear personal web notebook (2004-2020) version 5.1.22

とはどういうものかというと、情報カードみたいにセルを取り出せるブログみたいな wiki みたいな Zettelkasten ていうらしいですが、そういうやつだとおもいます。なぜかキャラとして猫がどーんと推されています。tiddly って猫のことかな ?と思いましたが、たぶん違うでしょう。いつかわかるかもしれないですね、使っていたら。
html ファイル 1 つだけで完結しているものも出来上がるのでこのように github pages でおいておくこともできます。

この道しるべは、Tiddlywiki を android で nodejs の web サーバーを使っている場合を想定しています。

Android タブレットの場合は、npm からサーバーバージョンをインストールするように公式の tiddlywiki のページにありますから、素直にそうしたという初めての npm インストールから、初めての tiddlywiki なあなたです。

そうそれは、5 日以上前のわたし。
そしてついさっきまでかかってようやく、そこに マークダウンのプラグインをインストールできた今のわたしが、ここにいて、やり方が全然わからなかったし、途中で何回か無理かなとあきらめかけたなと思っているわけです。

そう、 tiddlywiki はマークダウンで書けるわけですが、それには javascript のプラグインをインストールする必要があります。

Nodejs を使わない単体の tiddlywiki であればそのインストールは、なにも知る必要がなくインターネットにつながっていれば、 atom エディターのプラグイン追加のように、例のようにできますし、プラグインのリンクがあれば開いている tiddlywiki の上にドラッグアンドドロップでインストール可能です。それでことたります。

しかし、 npm でインストールした nodejs でコマンドでサーバーによって動いている tiddlywiki の場合はもうちょっとてがかかります。
わかってしまえばそれまでなんですが、わかるまでに tiddlywiki の特性でもある情報カードみたいに細切れにいくつあるのかもしれない公式ドキュメントの断片や github ページをぐるぐる見回ることになります。

という前置き。

みるべきところは 2 つだけ、でした。
そして、それも片方だけです。
これ、
Installing custom plugins on Node.js

これ。( こっちダ

Installing a plugin from the plugin library

tiddlywiki.info を探して書き加える。

何を、
"tiddlywiki/markdown"
を。

前提として、プラグインの名称を前もって知っている必要があります。

どうやって ?

サーバーなしの tiddlywiki を開いてプラグインのライブラリーにアクセスして調べます。

npm からインストールした tiddlywiki ではプラグインのライブラリーになーんにも表示されませんので不可能です。ここで android だけで ローカルホストからでしか tiddlywiki を見たことないで何とかしようとするとな、謎が深まるのです。
謎がなぞをよびます。
いや、見えないんだからおかしいやろ、と思わないともうそこでつんでしまいです。

https://tiddlywiki.com/ 
ここにアクセスして tools のタブをクリックし、controll
panel
のなかの plugins のなかの Get more plugins でプラグインを検索できます。
このように。
Screenshot_20201213-120453.png

Nodejs によってローカルホストでたちあがっている tiddlywiki だとアクセスできませんから、プラグインの検索画面でもなーんにも表示されませんから注意してください。

前者のリンクには

Note that including a plugin as an ordinary tiddler (e.g. by dragging and dropping a plugin into the browser) will result in the plugin only being active in the browser, and not available under Node.js.

とあります。

これは、
「プラグインを通常の tiddler として含めると(たとえば、プラグインをブラウザーにドラッグアンドドロップすることにより)、プラグインはブラウザーでのみアクティブになり、 Node.js では使用できないことに注意してください。」
ということですが、じゃあ、だからどうするのよ、とよく読んでもわからないのです。

なので、わかんないわー、どこに実例あるの? youtube でチュートリアルは ?

とならないために実例で、

プラグインの名前は markdown という実在のプラグインを追加する事だけを説明します。

markdown プラグインは先程見たプラグインライブラリーに登録されているものです。カスタムプラグインはプラグインライブラリーに登録されていないものを含むので、登録されていないものは別に探しだしてダウンロードする必要があります。

Termux のコマンドライン
ctrl + c
で サーバーを止めます。( codeboardhackerskeyboard ) などのアプリを使ってコマンドのキーを使えるようにしておく必要があります。)

Termux のコマンドラインで cd を駆使して( cdls )探しだした tiddlywiki.info ファイルに nano とか vi や vim 等のテキストエディターを使ってファイルを開いて、

$ vim tiddlywiki.info

Plugins の 項目のなかに

"plugins": [

        "tiddlywiki/markdown"
    ],

というように一行を書き足して、保存。
tidlywiki.info の中はこのように。
Screenshot_20201213-125149.png

Plugins のディレクトリを mkdir でつくる。(カスタムプラグインファイルをダウンロード可能な場合はここにプラグインファイルを配置するが、 markdown プラグインに関してはここにインストールされるわけではないので必要ない。)

├── plugins
│ 
├── tiddlers
└── tiddlywiki.info

そうして

Tiddlywiki mynewwiki --init server

でサーバーを再稼働させて、127.0.0.1:8080
をブラウザーで開くとプラグインが読み込まれている。
Screenshot_20201213-000132.png

以上です。

もうひとつ。

npm でインストールした tiddlywiki を 127.0.0.1:8080 で起動しようとして port 8080 が address alredy in use となっている場面。

IMG_20201207_141856_230.jpg

これは、rootじゃない android で nodejs のサーバーが動いている状態で、バッテリー切れになり、再度電源を入れてみると、8080番ポートが何かに占有されています、いました。
2日経ってもおなじです。おなじでした。
リブートしてもおなじです。

root ではないのでプロセスを探そうにもプロセス番号を知ることができませんでした。
これは root 権限のない android だと困る問題です。たぶん。

Screenshot_20201207-142359.png

プロセス番号のわからない、ローカルホストの 8080 番ポートの何かを解放する方法。

$fuser -k -n tcp 8080

これで見えない、なぞの8080ポートのなにかを殺害できました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

はじめての Raspberry Pi Zero W に Homebridgeインストール まで

目次

はじめに

はじめての Raspberry Pi Zero W に Homebridgeインストール までの手順を簡単にまとめました。

1. Raspberry Pi OSインストール

まず、SDカードにOSを書き込むツールをオフィシャルからダウンロードしてくる。
Install Raspberry Pi OS using Raspberry Pi Imager
image.png

「CHOOSE OS」でインストールしたい「Raspberry Pi OS」を選択し、「CHOOSE SD CARD」で、書き込み先のSDカードを選択します。

  • SDカードは、FAT32フォーマットで、32GBまで使えるので、「microSDHC 32GB class10」があれば十分だと思います。

  • 「CHOOSE OS -> USE CUSTOM」を選択すると、「Operating system images」でダウンロードした「zip」ファイルなどをSDカードに書き込むことができます。

2. SSH, VNCを使えるようにする

Raspberry Piの電源を入れて、言語環境、WiFiなどの初期セットアップ後、SSHとVNC(Virtual Network Computing)を有効化しておくと、WindowsやMac、iOSの「VNC Viewer」から、Raspberry Piのデスクトップを操作できます。

  • Raspberry Piの接続ポートに、HDMIを接続して、USBハブを接続して、キーボードつないで、マウスつないで、といった煩わしい作業が不要になります。
  • iPadなどのタブレットやスマホをRaspberryPiのモニターとして使えます。
    ipad_raspberry.png

  • <参考リンク> Raspberry Pi 4にSSHとVNCで接続してみた

3. node.js, npmのインストール

Pi ZeroはCPUのアーキテクチャがARMv6なので、通常のnodeインストールはエラーになるっぽいです。
なので「node-pi-zero.git」で、node.jsをインストールするのが簡単な様です。

## aptのアップデート(インストール可能なパッケージの一覧更新)
sudo apt-get update
## aptのアップグレード(インストール済みパッケージ更新)
sudo apt-get upgrade

## gitのインストール
sudo apt-get install -y git

## node-pi-zero.gitのクローン
git clone https://github.com/sdesalas/node-pi-zero.git

## node v11.5.0 のインストール
cd node-pi-zero
sudo chmod 755 ./install-node-v11.5.0.sh
./install-node-v11.5.0.sh

## バージョンの確認
node -v
v11.5.0

npm -v
6.4.1

後から気がついたのですが、Gitでcloneしなくても、直接以下のコマンドで、node-pi-zeroの最新版をインストールできるみたい。

wget -O - https://raw.githubusercontent.com/sdesalas/node-pi-zero/master/install-node-v.last.sh | bash

4. Homebridgeのインストール

Homebridge
image.png

Homebridgeをインストールすることで、RaspberryPiのシステム状態をダッシュボード上で見ることができたり、
iPhoneのSiriから家電をコントロールできたりと、とっても便利です。

# npmでHomebridgeインストール
sudo npm install -g --unsafe-perm homebridge homebridge-config-ui-x

# hb-serviceでWebサービスとして設定
cd /opt/nodejs/lib/node_modules/homebridge-config-ui-x/dist/bin
sudo ./hb-service.js install --user homebridge

↓ install結果

ℹ Installing Homebridge Service
ℹ Created service user: homebridge
ℹ Creating Homebridge directory: /var/lib/homebridge
ℹ Creating default config.json: /var/lib/homebridge/config.json
ℹ Starting Homebridge Service...
✔ Homebridge Started

Manage Homebridge by going to one of the following in your browser:

* http://localhost:8581
* http://192.168.1.255:8581
* http://[ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff]:8581

Default Username: admin
Default Password: admin

✔ Homebridge Setup Complete

おわりに

今回は、Raspberry Pi OSに、node.js, npm → Homebridgeをインストールしていきましたが、
Raspberry Pi (Zero W) に、直接「homebridge-raspbian-image」を稼働させたい場合は、以下のGitを参考にすると良いと思います。
* 基本的には、ダウンロードしたzipファイルをSDカードに書き込んで、初回セットアップ手順に従ってWiFiを設定するだけです。
* 「based on Raspbian Lite.」なので、Desktop版のようなGUI(Graphical User Interface)のないバージョンになります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptでHTTPSなしで暗号化したデータを送りたい。

目的

image.png

使用するライブラリ

https://github.com/kjur/jsrsasign

jsrsasignはJavaScriptのライブラリでブラウザサイド、node.js側のサーバーサイドで以下のような暗号化を行えます。

RSA/RSAPSS/ECDSA/DSA signing/validation, ASN.1, PKCS#1/5/8 private/public key, X.509 certificate, CRL, OCSP, CMS SignedData, TimeStamp, CAdES JSON Web Signature/Token/Key

事前準備

opensslで公開鍵と秘密鍵を作成します。

openssl genrsa 2024 > secret.key 
openssl rsa -pubout < secret.key > public.key

サンプル

以下の例ではブラウザ側で公開鍵で暗号化したデータをnode.js側で秘密鍵で復号化しています。

server.js
const express = require("express");
const app = express();
const fs = require('fs');
const bodyParser = require('body-parser')

const port = 3000; // 1024以下にした場合は管理者権限が必要になります.

const http = require("http");
const server = http.createServer(app);

const rs = require('jsrsasign');
const rsu = require('jsrsasign-util')
/**
openssl genrsa 2024 > secret.key 
openssl rsa -pubout < secret.key > public.key
 */
const secretKey = rs.KEYUTIL.getKey(rsu.readFile('secret.key'))

app.use(express.static(__dirname + "/public"));
app.use(bodyParser.urlencoded({ extended: true }))
app.use(bodyParser.json())

app.post('/login', async (req, res) => {
  console.log(req.body.data)
  const data = JSON.parse(rs.KJUR.crypto.Cipher.decrypt(req.body.data,secretKey,'RSA'))
  console.log(data.time, (new Date).getTime(), (new Date).getTime()-data.time)
  if ((new Date).getTime()-data.time > 1000 * 60) {
    res.status(401).send('時間切れ')
    return
  }
  const result = {
    message: '応答:' + data.message,
  }

  res.status(200).send(result)
})

server.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});
public/index.html
<!DOCTYPE html>
<html>
  <head>
    <title>シンプルチャット</title>
    <meta charset="UTF-8" />
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jsrsasign/8.0.20/jsrsasign-all-min.js"></script>
  </head>
  <body>
    <div id="app">
      <div>
        <input v-model="sendName"></input>
        <button v-on:click="send">Send</button>
      </div>
    </div>
<script>
  console.log('app')
const app = new Vue({
  el: '#app',
  data: {
    sendName : "abcde"
  },
  computed: {
  },
  created : function() {
  },
  methods: {
    send : function() {
      console.log('send')
      // 以下のコマンドで作った公開キー
      // openssl genrsa 2024 > secret.key 
      // openssl rsa -pubout < secret.key > public.key
      const publicKeyStr = `
-----BEGIN PUBLIC KEY-----
MIIBHjANBgkqhkiG9w0BAQEFAAOCAQsAMIIBBgKB/gC1UZYJkhTJpfitHn0Jv6Ms
b15tHhsYO1DHICrRwNMkePCm1hWUbK3aG+Q173SrO1yR1qadPsz3heMbDwwqaU3t
0CMsdaLzPdCLiTT7HFXHkc1TI/ltwg0NAo4YrHN89WFk7/zGquy8ekeZFX21b2Xf
sqtiQCkHf6W2XIOgSo5AbH8V6wPgzCPBn1hu2lL5btF10Rbt9KkW/3WiRt/U06wD
5QgwJZ4A140dzea3mBSH6r0bje9h3nHmzqpwA5a9QxSL1HYH4E9VEV8FDwIAI3Qw
O2kxvpTKG0qst3i6nxcvRHmeWFakPnCqnyuqV31FJVf8cebbMlVePUb2IfCbAgMB
AAE=
-----END PUBLIC KEY-----
`
      console.log(this.sendName)
      const data = {
        message: this.sendName,
        time: new Date().getTime()
      }
      const publicKey = KEYUTIL.getKey(publicKeyStr)
      const encyptData = KJUR.crypto.Cipher.encrypt(JSON.stringify(data), publicKey, 'RSA')

      axios.post('/login', {data: encyptData})
      .then(function (response) {
        alert(JSON.stringify(response.data))
      })
      .catch(function (error) {
        alert(error)
      })
    },

  }
})

</script>
  </body>
</html>

まとめ

jsrsasignを利用することでブラウザ側でも暗号化できます。
でも、素直にHTTPS化した方がいいと思うので使うことはないでしょう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HOYAのVoiceTextを使ってDiscordのテキストを読み上げるBotを作ってみた

はじめに

このBotを作った経緯としては、夜遅くなどどうしてもしゃべれない時間帯があるため、そうした時間帯でもVCで参加する方法が必要になり、このBotを作ってみました。
どうやら、既に喋太郎というBotがあるらしいですが、このBotを最初に作った時(2018年2月ぐらい)にはなかったのでご容赦ください。
似たようなことをしたい人の参考になれば良いなと思います。

ソースコード

GitHubにあります。
noriokun4649/Discord-TTS-Voice-channel-Bot
利用してるライブラリなどの依存関係はpackage.jsonに書いてありますが下記の通りです。

  • Node.js v12.19.0
  • @discordjs/opus : 0.3.2
  • config : 3.3.2
  • config-reloadable : 1.0.8
  • discord.js : 12.4.0
  • ffmpeg-static : 4.2.7
  • voice-text : 0.1.2

開発してみての感想

VoiceTextのライブラリや、Discordのライブラリが用意されていたため、簡単に開発することができました。
躓いた点としては、VoiceTextのライブラリから返ってくるbufferをDiscord.jsのstreamに渡す方法に躓きました。
このBotを作った当初(2018年2月ごろ)の時点では、一度wavファイルに保存して、保存後wavファイルを読み取りDiscord.jsに渡すという糞みたいな処理をしてましたが、現在は直接bufferからstreamに変換して渡しています。

また、DiscordのAPI自体の仕様で、Discordのテキストチャネルで送信されるサーバ独自の絵文字やメンションなどが変わった形で送られてくるため、こうした絵文字やメンションの処理も少しだけ戸惑いましたが、最終的には正規表現で対応しました。

使い方

Node.jsで動くので通常どおりnpmのコマンドで使えます。

機能や設定、コマンドなど

機能はそんなに多くありませんが下記のような機能があります。

  • コマンドの接頭語の変更機能
  • エラー時の自動再起動する機能
  • 読み上げ音声の変更機能
    • 音声の種類変更
    • 読み上げ音声の速度の変更
    • 読み上げ音声の高さの変更
  • ブラックリスト
    • コマンドの接頭語別に読み上げ禁止にする機能
    • ユーザー別に読み上げ禁止にする機能
    • すべてのBotを読み上げ禁止にする機能

読み上げ音声の変更機能以外は、Configファイルで設定可能です。

Configファイルについて

configフォルダ内のdefault.jsonがコンフィグファイルです。

{
  "Api": {
    "discordToken": "",
    "voiceTextApiKey": ""
  },
  "Prefix": "/",
  "AutoRestart": true,
  "ReadMe": false,
  "Defalut": {
    "apiType": 1,
    "voiceType": "hikari"
  },
  "BlackLists": {
    "memberIds": [
      "381054450451742720"
    ],
    "prefixes": [
      "!",
      "/"
    ],
    "bots": true
  }
}
項目 内容・説明
discordToken Discordのトークンを記入
voiceTextApiKey VoiceTextのAPIキーを記入
Prefix コマンドの接頭語を決めます
AutoRestart 予期せぬエラー時に自動でボイスチャンネルへ再接続すかどうか
ReadMe このBotが送るメッセージを読み上げるかどうか
apiType デフォルトのAPIを指定 (利用できるAPIが1になってしまったので無意味)
voiceType デフォルトのボイスを指定
memberIds 読み上げから除外するユーザーのユーザーID
prefixes 読み上げから除外する接頭語
bots Botを読み上げから除外する

Botのコマンドについて

接頭語+α 内容・説明
join ボイスチャンネルにBotを呼びます
reconnect ボイスチャンネルへ再接続します
kill ボイスチャンネルから切断します
mode 読み上げに利用するTTSのAPIを変更します
type APIで利用可能な音声タイプを一覧表示します
voice 音声タイプを変更します
speed 音声の速度を変更します(0~200の数値)
pitch 音声の高さを変更します(0~200の数値)
reload コンフィグを再読み込みします

接頭語は、Configファイルで指定した接頭語を使います。

ToDo

一応まだ、開発のモチベーションがあるので今後の予定としては、ユーザーごとに読み上げ音声の種類を設定できるようにしたいと考えています。
聞き専の人が複数居ても、読み上げ音声の種類で人を区別できるようになるので利便性が上がると思います。

最後に

2018年2月頃に書いたプログラムをリファクタリングや機能の改善、バグ修正などしましたが、最終的には実用に足りる実装ができたと思います。
まだまだ、至らぬ点がありそうですが今後も開発出来たら良いなと思います。
最後まで見て頂きありがとうございます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

node.jsのエラー解決方法 "gyp ERR! find VS You need to install the latest version of Visual Studio"

node.jsを使用するときのエラーの対処法

普段、Vue.JsのフレームワークであるNuxt.jsを使用して主に開発業務を行っているのですが、
node.jsでのエラーにはかなり悩まされました。
ちなみに私のOSはwindowsです。macユーザーLinuxの方はご参考程度に。

yarn devでプロジェクトは起動できるからといっかと無視できない下記のエラー

Nuxt
gyp ERR! find VS You need to install the latest version of Visual Studio
gyp ERR! find VS including the "Desktop development with C++" workload.
gyp ERR! find VS For more information consult the documentation at:
gyp ERR! find VS https://github.com/nodejs/node-gyp#on-windows

このエラーが出ている状態では、ライブラリが正常に入らなかったり、
yarnをしてもyarn.lockが入らなかったり、非常に厄介。

解決方法はググってみるといろいろ出ています。
オーソドックスなのが

PowerShellの管理権限から"widows-build-tools"を入れる。

PoweShell
npm install --global --production windows-build-tools

node.js自体がそもそも、C言語やPythonで作られているようなので、python2.7やらCをインストールして
パスを通す必要があるようです。
上述のコマンドを入れるとすべて自動で入れてくれるという優れもの!

しかし、今回はこれではエラーが出たままでした(´;ω;`)

Visual StudioのC++ Build Toolsを入れてパスを通す。

参考にしたのはこちらのサイト。↓
参考サイト(1)

Visual StudioをインストールしてC++ Build Toolsというものをいれます。
VS Codeとは別ものですので注意!(私も知りませんでした、、、、)
windowsならデフォルトでVisual Studio Installer というものが入っていると思いますので、
それを使ってインストールすると下記のスクショのようになります。インストールされている状態です。
インストールするときは、C++ Build Toolsも一緒にインストールしてあげてください。(あとから変更もできるので忘れても大丈夫!)

sample1.png

変更ボタンを押して、C++ Build Toolsがインストールされているかどうか確認してみましょう。
sample2.png

問題なくインストールできています。
これでC++がインストールできたのか、、、、
もう少しです笑

今度は"C:\Users\your user name\"のディレクトリ配下にbinding.gypという
ファイルを作成し下記のコードを入れます。

Python
{
    "targets": [{
        "target_name": "binding",
        "sources": [ "binding.vcxproj" ]
    }]
}

参考サイトの"sources"と若干違うのは、VS C++ Build Toolsにパスを通してあげる形にするのがミソ!!

そのあとはコマンドプロンプトを起動し下記のコマンドを打っていきます。

$ node-gyp configure
↓
$ node-gyp configure --msvs_version=2019※すでに2019年版をインストールしていたため、2019指定です。
↓
$ node-gyp build

エラーは頑張って対処するしかない。

エンジニアになって半年近くたちますが、いまだに辛いのが今回のようなエラーが起こった時。
でも最初は、俺嫌われてんのかと思いました。センスないんかなって(笑)
でも実は、みんな困ってる!(笑)
しっかりエラーを読み解いていくと私のような素人に毛が生えたようなエンジニアでもなんとか対処できました。
こういうチュートリアルが意外となかったので、
誰かの役に立てばと思い載せさせていただきました。
駆け出しエンジニアの皆さん!あきらめず一緒に頑張りましょう★

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

npmパッケージ検索サイトからの使い方を初心者が解説してみた!

node.jsのパッケージが無料で使えるサイト

npmという無料で使えるnode.jsのパッケージまとめサイト
npm build amazing things
image.png

このサイトから必要な情報を取り出し実装するまでを、node初心者が解説します!

初心者なのでスムーズには扱えなかった

node.jsのパッケージを無料で提供してくれているとは言っても、使い方がわからなかったので
覚書として記録しておこうと思います。

使ってみたい!と思ったのはvue.jsのパッケージであるdraggbale

デモサイトは
https://david-desmaisons.github.io/draggable-example/

vue.jsを利用して、ドラッグ&ドラッグが簡単に実装できるみたいです。
これを使ってタスク管理をしてみたい!と思ったので使ってみる事にしました。

ラッキー!サンプルコードがgithubに上がってる

image.png

image.png

image.png

Hello.vue
というフォルダに入っていました。

<template>
  <div class="fluid container">
    <div class="form-group form-group-lg panel panel-default">
      <div class="panel-heading">
        <h3 class="panel-title">Sortable control</h3>
      </div>
      <div class="panel-body">
        <div class="checkbox">
          <label><input type="checkbox" v-model="editable">Enable drag and drop</label>
        </div>
        <button type="button" class="btn btn-default" @click="orderList">Sort by original order</button>
      </div>
    </div>

    <div class="col-md-3">
      <draggable class="list-group" tag="ul" v-model="list" v-bind="dragOptions" :move="onMove" @start="isDragging=true" @end="isDragging=false">
        <transition-group type="transition" :name="'flip-list'">
          <li class="list-group-item" v-for="element in list" :key="element.order">
            <i :class="element.fixed? 'fa fa-anchor' : 'glyphicon glyphicon-pushpin'" @click=" element.fixed=! element.fixed" aria-hidden="true"></i>
            {{element.name}}
            <span class="badge">{{element.order}}</span>
          </li>
        </transition-group>
      </draggable>
    </div>

    <div class="col-md-3">
      <draggable element="span" v-model="list2" v-bind="dragOptions" :move="onMove">
        <transition-group name="no" class="list-group" tag="ul">
          <li class="list-group-item" v-for="element in list2" :key="element.order">
            <i :class="element.fixed? 'fa fa-anchor' : 'glyphicon glyphicon-pushpin'" @click=" element.fixed=! element.fixed" aria-hidden="true"></i>
            {{element.name}}
            <span class="badge">{{element.order}}</span>
          </li>
        </transition-group>
      </draggable>
    </div>

    <div class="list-group col-md-3">
      <pre>{{listString}}</pre>
    </div>
    <div class="list-group col-md-3">
      <pre>{{list2String}}</pre>
    </div>
  </div>
</template>

<script>
import draggable from "vuedraggable";
const message = [
  "vue.draggable",
  "draggable",
  "component",
  "for",
  "vue.js 2.0",
  "based",
  "on",
  "Sortablejs"
];

export default {
  name: "hello",
  components: {
    draggable
  },
  data() {
    return {
      list: message.map((name, index) => {
        return { name, order: index + 1, fixed: false };
      }),
      list2: [],
      editable: true,
      isDragging: false,
      delayedDragging: false
    };
  },
  methods: {
    orderList() {
      this.list = this.list.sort((one, two) => {
        return one.order - two.order;
      });
    },
    onMove({ relatedContext, draggedContext }) {
      const relatedElement = relatedContext.element;
      const draggedElement = draggedContext.element;
      return (
        (!relatedElement || !relatedElement.fixed) && !draggedElement.fixed
      );
    }
  },
  computed: {
    dragOptions() {
      return {
        animation: 0,
        group: "description",
        disabled: !this.editable,
        ghostClass: "ghost"
      };
    },
    listString() {
      return JSON.stringify(this.list, null, 2);
    },
    list2String() {
      return JSON.stringify(this.list2, null, 2);
    }
  },
  watch: {
    isDragging(newValue) {
      if (newValue) {
        this.delayedDragging = true;
        return;
      }
      this.$nextTick(() => {
        this.delayedDragging = false;
      });
    }
  }
};
</script>

<style>
.flip-list-move {
  transition: transform 0.5s;
}

.no-move {
  transition: transform 0s;
}

.ghost {
  opacity: 0.5;
  background: #c8ebfb;
}

.list-group {
  min-height: 20px;
}

.list-group-item {
  cursor: move;
}

.list-group-item i {
  cursor: pointer;
}
</style>

これだけでは動きませんでした。
当たり前ですよね。

githubからファイルをインストール

githubから、まずはURLをコピーします。
image.png

使用するツールはVisual Studio Code
Visuak Studio Codeのターミナルを立ち上げ
image.png

以下のコマンドを打ちました。

git clone https://github.com/David-Desmaisons/draggable-example.git

cloneが完了するとまるっと一式追加されていました。

image.png

必要npmインストール / run serv / build

が必要でした。最後まできちんと見ないといけないですね。
また、githubからcloneしたファイルの【README.md】にもコマンドの手順が記載されていました。

https://github.com/David-Desmaisons/draggable-exampleの最下部にコマンドが書かれています。

image.png

順番にコマンドを打っていきます

draggableを動かすために必要な、他のnpmを一括インストールします。

npm install

結果。【found 0】間違えてそうな。。。
image.png

cd draggable-example

draggable-exampleのフォルダに移動してからinstallをしないといけなかったです。
沢山インストールされたようです。
image.png

node_moduleフォルダが生成され、大量のnmpがインストールされました。
image.png

次はブラウザで動きが見れるようにサーバーを立ち上げます。

npm run serve

ローカルサーバー立ち上がりました!

image.png

localhost:8080でデモサイトとまったく同じものが出来上がりました。

image.png

一端【Ctrl + C】コマンドでサーバーを終了します。
最後にビルドをして、webサーバーへアップロードしても動くようにします。

npm run build

index.htmlを始めとする必要ファイルがdocsフォルダの中に入っていました。
docsフォルダの中身を全てサーバーへアップロードするとサーバーで稼働します。

image.png

カスタマイズする時

src > components
の中にあるHello.vueを修正
image.png

修正をしたら

npm run serve

でブラウザで確認。
本番へアップする時だけ最後に

npm run build

をすれば良いです。毎回buildまでする必要はありません!

まとめ

・githubの説明は最後まできちんと読む事
・ダウンロードしたファイルにあるREADME.mdを見に行く
・ファイルの階層に注意してnpmのコマンドをうつ

自分で調べたnpmを初めて使いました。
もっとnpmを自由に操れるように修業が必要だなと感じました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Puppeteerを使って動作確認のための作業を効率化したい

CAPMFIREコミュニティでエンジニアをしている阿部です.
今日は機能追加や修正などの後の動作確認するときやデータ収集するときに,トラックパッドでクリックをしすぎて指がつりそうになるのを防ぐため(完全私欲),Puppeteerにこちらの作業をを全部投げたい!!!という気持ちでやっていきます!!!
とは言っても,かなり膨大な量になるので,今日はちょっと振り返りをしていこうと思います.

Puppeteerって?

PuppeteerはChrome,Chromiumを制御することができるNode.jsのAPIです.Puppeteerではデフォルトでヘッドレスモード(ブラウザ表示をさせず,バックグラウンドで制御が可能なモード)が設定されており,高速かつメモリ使用を押さええながらwebテストなどを行うとこができます.ヘッドフルモード(ブラウザ表示をしながらのモード)もオプションを選択することで使用できます.

私たちが日常ブラウザを使う上でできること(クリックや文字入力など)はほとんど可能で,SPA,SSRのWebページの制御,Chromeの拡張機能などもテストすることができます.
もっと詳細を知りたい方はこちら

使用してる環境

Jest + Puppeteerで作ったスクレイピング専用プロジェクトを使用
環境構築に関しては後日追記

バージョンなど

使用してるもの バージョン
Node.js
Puppeteer
Npm
csv-witer
date-fns 2.16.1
dotenv 8.2.0
jest 26.6.3
jest-puppeteer 4.4.0
mkdirp 1.0.4

ディレクトリ構成

.
├── __test__
│   ├── ... フローごとに分類,一つのフローに1ファイル   
│
├── config
│   ├── jest-setup.js             ... Timeoutの時間を設定
│   ├── puppeteer-environment.js  ... setupとteardownの呼び出し
│   ├── setup.js                  ... ブラウザ起動時の設定(Headfullなど)
│   └── teardown.js         ... ブラウザクローズ時の処理をメイン
├── global
|   ├── ... 部品(チェックボックスなど)や全般的に使う動作(クリックやフォーム絵のタイプなど)をまとめる
| 
├── project
│   ├── ... ページ(またはサービス)ごとに使う動作をまとめる
│   
├── node_module /
├── output
│   ├── ... スクリーンショットやcsvなどのexportしたもののまとめ
│   
├── .env ... ログインパスなどPublicにしたくないもの
├── package.json
└── package-lock.json

コーディング一部

__test__/sample.js
const variables = require("../global/variables")
const functions = require("../global/functions")
const top = require("../project/example/top")
const fs = require('fs')
require('dotenv').config()
const {createObjectCsvWriter} = require('csv-writer')
const OUTPUT_PATH = "output"

const VIEWPORT = {
  width : 1280,
  height: 1024
}

describe('Index page', () => {

  let page
  let testIndex = 0

  let topPage


  it("トップページ表示", async () => {
    await page.goto("https://example.com/", { waitUntil: "networkidle2" })
  })

  it("力技で取得", async () => {
    await topPage.getArticlesTrend()
  })

  async function csvWrite(data) {
    if (!fs.existsSync(OUTPUT_PATH)) {
      fs.mkdirSync(OUTPUT_PATH)
    }
    var exec = require('child_process').exec
    exec(`touch ${OUTPUT_PATH}/page.csv`, function(err, stdout, stderr) {
      if (err) { console.log(err) }
    })
    const csvfilepath = `${OUTPUT_PATH}/page.csv`
    const csvWriter = createObjectCsvWriter({
      path: csvfilepath,
      header: [
        {id: 'id', title: 'No.'},
        {id: 'title', title: 'タイトル'},
        {id: 'published_at', title: '公開日'},
        {id: 'url', title: 'URL'}
      ],
      encoding:'utf8',
      append :false,
    })
    csvWriter.writeRecords(data)
        .then(() => {
          console.log('...Done')
        })
  }

  beforeAll(async () => {
    context = await global.__BROWSER__.createIncognitoBrowserContext()
    page = await context.newPage()
    await page.setViewport({
      width : VIEWPORT.width,
      height: VIEWPORT.height
    })

    topPage = new top.topPage(page)
  })

  afterAll(async () => {
    await page.close()
  })

  beforeEach(() => {
    console.log(`[START]:\t${testIndex}`)
  })

  afterEach(() => {
    console.log(`[END]:\t${testIndex++}`)
  })
});
projects/example/top.js
const basicAction = require("../../global/basic-action")

const selector = {
    articles: 'div',
    article_title: 'h2 > a',
    article_lgtm: 'footer > div > div > div',
    article_published_at: 'headler > time'
}

module.exports = {
    topPage: class {
        constructor(page) {
            this.page = page
        }

        async moveArticleTrend() {
            await basicAction.click(this.page, xpath.sideMenu.article.trend)
        }

        async getArticlesTrend() {
            await this.page.waitForSelector(selector.articles)
            const lists =  await this.page.$$(selector.articles)

            let datas = []

            for(let i = 0; i < lists.length; ++i) {
                let title = await lists[i].$(selector.article_title)
                let lgtm = await lists[i].$(selector.article_lgtm)
                let published_at = await lists[i].$(selector.article_published_at)
                let href = await lists[i].$$eval(selector.article_title, tep => tep.map(item => item.href));
                const dataArray = await Promise.all([
                    i + 1,
                    basicAction.getTextBySelector(title),
                    basicAction.getTextBySelector(lgtm)
                    basicAction.getTextBySelector(published_at),
                    href.join('')
                ])

                datas.push({id: dataArray[0], title: dataArray[1], lgtm: dataArray[2], published_at: dataArray[3], url: dataArray[4]})
                console.log(datas[i])
            }
            return datas
        }


    }
}

global/basic-action.js
module.exports = {
    async type(page, xpath, text) {
        await page.waitForXPath(xpath)
        const elementHandleList = await page.$x(xpath)
        await elementHandleList[0].type(text)
    },
    async click(page, xpath) {
        const elementHandle = await page.waitForXPath(xpath)
        await Promise.all([
            page.waitForNavigation({waitUntil: "networkidle2"}),
            elementHandle.click()
        ])
    },
    async pressEnter(page, xpath) {
        await page.waitForXPath(xpath)
        const elementHandleList = await page.$x(xpath)
        await elementHandleList[0].press('Enter')
    },
    async clickNoWaitting(page, xpath) {
        const elementHandle = await page.waitForXPath(xpath)
        await elementHandle.click()
    },
    async getText(page, xpath) {
        await page.waitForXPath(xpath)
        const elementHandleList = await page.$x(xpath)
        const textContent = await elementHandleList.getProperty("textContent")
        const text = (await textContent.jsonValue()).replace(/[\s ]/g, "")
        return text
    },
    async getTextBySelector(elementHandleList) {
        const textContent = await elementHandleList.getProperty("textContent")
        const text = (await textContent.jsonValue()).replace(/[\s ]/g, "")
        return text
    },
    async selectOption(page, selector, optionText) {
        const selectElement = await page.$(selector)
        await page.evaluate(
            (selectElem, text) => {
                let hasChanged = false
                for (let i = 0; i < selectElem.options.length; ++i) {
                    if (selectElem.options[i].innerText == text) {
                        selectElem.selectedIndex = i
                        hasChanged = true
                        break
                    }
                }
                if (hasChanged) {
                    const event = new Event("change")
                    selectElem.dispatchEvent(event)
                } else {
                    console.log(`${text} not found.`)
                }
            },
            selectElement,
            optionText
        )
    }
}

個人的に面白かったもの

dialog

今回は触れてないのですが,ダイアログの回答などを行うClass
つまり,ダイアログはClickの対象ではないということ

なので,事前にダイアログの回答を入力しておかないといけない.
さらに,ダイアログが複数回出現し,回答パターンが違う場合は下記リンクの記事のようにonceの中にonceを配置して,1度目と2度目の回答を変えるなどの回避策が必要のようです.
ダイアログに表示される文章を取ってくることもできるので,それによって回答を変えるなどの回避策もあります.
https://qiita.com/khsk/items/0b7ef6d012f0167ed2bb

API
puppeteer/api.md at v5.5.0 · puppeteer/puppeteer · GitHub

getProperty

上記のコードではタグ内にあるテキストを取って来ているが,hrefやtypeなどのプロパティの値も指定できるみたいです.
自分はまだ使ったことがないので使ってみたい.
API
https://github.com/puppeteer/puppeteer/blob/v5.5.0/docs/api.md#elementhandlegetpropertypropertyname

まとめ

今回は試しにサンプルプログラムを作ってみました.
汎用性高く書こうとするとXPathやSelectorの書き方を工夫する必要がある部分はかなり工数かかるなと改めて感じました.
Headless Recorderなども出てきてるし,小ちゃい修正や機能追加などはこれを使う方がいいのかもと思いました.
ただ,Headless Recorderだけだと,matcherでの確認とかは記録されないから,そこは自力で頑張るしかなさそうです.
逆に定期的に行うケース,データ収集とかで使う場合は大きなUI変更とかがない限りはちゃんとclass名とかをみて判断するので使い勝手良さそうです.

そして,サービスによってはスクレイピングを禁止しているものもあるので,利用規約などを確認し,スクレイピングでのデータの扱いには十分な注意をお願いします.

参考資料

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

npm install した時に,Maximum call stack size exceeded エラーが発生した時の対処法

TL; DR

npm rebuild

これで解決した.

解説?

sharpやjsdomなど,nodejsのNative moduleを使うときにこのエラーが発生する.
ビルドのやり直しで解決する事もあるようだ.

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

M1 MacBookにnvmをインストールする方法

nvmのインストール

ターミナルを以下の記事を参考に起動する(下記のコマンドを実行する)

https://qiita.com/tanaka-lapis-jp/items/2039570c8d05824665da

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.0/install.sh | bash

この際、Profile not found.と怒られた場合は以下のコマンドでProfileを作成してあげれば解決できる。

source ~/.nvm/nvm.sh

Node.jsのインストール

nvm i v14.15.1

上記は2020/12/12時点のLTS。nvmのバージョンは特に指定がなければ最新版のLTSを使用すると良い。(LTSとはLong Time Supportの略。長期的なサポートを保証するという意味。)

npm install
% npm -v
6.14.8
% node -v
v14.15.1

これでNodeが使えるようになりました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SORACOMひげボタンを使った追跡装置の実装方法

はじめに

こちらはSORACOM Advent Calendar 2020 12日目の記事です。
初のアドベントカレンダー挑戦です。
超ハイレベルな記事が多い中、稚拙な内容で大変恐縮ではありますが、よろしくお願いいたします。

本記事は SORACOM UG Online #2 のLTで発表した装置の実装方法について説明しています。
イベントの様子、内容については以下をご覧ください。
- イベントの動画(YouTube)
- 発表資料(Speaker Deck)

概要

2019年4月にSORACOMの LTE-M Button Plus (通称ひげボタン)が発売され、同年8月から 簡易位置測位機能 が提供されています。
これを使って盗難対策(追跡装置)を作ってみました。

うごき

装置が移動を検知すると位置情報をスマホ(Slack)に通知します。
function.png

構成図

システム構成は以下となります。
system.png
1. ひげボタンが移動を検知するとSORACOM Funkに通知
2. SORACOM FunkからAWS Lambdaを呼び出し
3. AWS LambdaはSORACOM APIを使い、ボタンに割り当てられた名前を取得し、SlackのIncoming Webhooksを叩く
4. 携帯のSlackに通知される

移動の検知

移動の検知は Omronの振動(転倒)センサー を使います。
取り付けはこんな感じです。
assemble.png
屋外使用のため、タカチの防水プラボックス に入れました。

また、振動(転倒)センサーはB接点(通常がON、倒すとOFF)なので、通常ボックスを立てた(センサーは横にした)状態にしておき、動かすとONになるようにしておきます。
setting.png

デモ

簡単ですが、こんな感じで動きます。
demo.png
1. 装置を持ち上げる
2. スマホのSlackに通知が表示される
3. メッセージのリンクをクリックするとGoogleマップに表示される

あくまで”簡易”位置情報なので、数十メートルから数百メートルのズレが出てしまいますが、大まかな追跡はできると思います。

実装方法

前置きが長くなりましたが、実装方法です。

以下の順で設定します。
1. 事前準備
2. SORACOM FunkからSlack通知
3. SORACOM追加設定
4. Lambdaの設定

1. 事前準備

今回、ボタンに設定された名前を取得する機能を実装するため、SORACOM APIを使えるようにします。
こちら(拙稿) を参考にSORACOM APIを使えるようにしてください。

2. SORACOM FunkからSlack通知

続いてSORACOM FunkからSlackに通知する仕組みを作ります。
ソラコムさんのサンプル に沿ってしてみてください。

3. SORACOM追加設定

LambdaからAPIを呼ぶため、以下の手順でSAMの設定を追加します。

(1) SORACOMコンソール画面で右上のIDボタン、「セキュリティ」をクリックする。
image.png

(2) 「ユーザーの追加」をクリックする。
(3) 任意の「名前」、「概要」を入力し、「作成」をクリックする。
image.png
(4) 「権限設定」タブで以下のJSONを指定し、保存する。

{
  "statements": [
    {
      "api": ["Subscriber:getSubscriber"],
      "effect": "allow"
    }
  ]
}

(5) 「認証設定」タブで「認証キーを生成」をクリックする。
ここで取得する認証キーID認証キーシークレットをメモしておいてください。
image.png

4. Lambdaの設定

2.で設定したLambdaの環境変数に先ほど取得したSAMの認証キーIDを「SORACOM_KEY」、認証キーシークレットを「SORACOM_KEY_ID」という名前で追加します。
image.png
環境変数が既存の「SLACK_URL」とあわせて3つとなります。

続いて、コードを以下に書き換えます。

const https = require('https');
const url = require('url');
const slackUrl = process.env.SLACK_URL;
const soracomKey = process.env.SORACOM_KEY;
const soracomKeyId = process.env.SORACOM_KEY_ID;

const initialize = (event, context) => {
    return new Promise((resolve) => {
        const stash = {
            bat: 0,
            imsi: '',
            lat: 0,
            lon: 0,
            btn: '不明'
        };

        stash.bat = event.batteryLevel;
        stash.imsi = context.clientContext.custom.imsi;
        stash.lat = context.clientContext.custom.location.lat;
        stash.lon = context.clientContext.custom.location.lon;

        console.log(stash);
        resolve(stash);
    });
};

const getButtonName = (stash) => {
    return new Promise((resolve, reject) => {
        var soracomApi = require('soracom_api');
        var soracom = new soracomApi({authKeyId: soracomKey,authKey: soracomKeyId});

        soracom.get('/subscribers/' + stash.imsi,function(err, res){
            console.log({err:err,res:res});
            if (err) {
                console.log('Gadgets API Error: ' + err);
                reject(err);
            } else {
                stash.btn =  res.tags.name;
                resolve(stash);
            }
        });
    });
};

const postSlack = (stash) => {
    return new Promise((resolve, reject) => {
        var slackReqOptions = url.parse(slackUrl);

        slackReqOptions.method = 'POST';
        slackReqOptions.headers = { 'Content-Type': 'application/json' };
        var payload = {
            "text": stash.btn + "が移動されました。\n" +
            'バッテリー残量:' + stash.bat,
            "attachments": [
                {
                    "title": "簡易位置情報",
                    "color": "#34cdd7",
                    "text": `<https://www.google.com/maps?q=${stash.lat},${stash.lon}>`,
                    "mrkdwn_in": ["text"]
                }
            ]
        };

        var body = JSON.stringify(payload);
        slackReqOptions.headers = {
            'Content-Type': 'application/json',
            'Content-Length': Buffer.byteLength(body),
        };

        var req = https.request(slackReqOptions, function(res) {
            if (res.statusCode === 200) {
                console.log('Posted to slack');
                resolve(stash);
            } else {
                console.log('Slack API Error: ' + res.statusCode);
                const err = {
                    "statusCode": res.statusCode
                }
                reject(err)
            }
        });
        req.write(body);
        req.end();
    });
}

exports.handler = (event, context, callback) => {
    console.log('event: %j', event);
    console.log('context: %j', context);

    initialize(event, context)
        .then(getButtonName)
        .then(postSlack)
        .then(callback.bind(null, null))
        .catch(callback);
};

以上で完了となります。
ボタンを押してみて、通知が届くかを確認してください。

さいごに

初のアドベントカレンダー、めちゃくちゃ大変でした。
私は元々プログラマーだったのですが、かなりブランクがある"なんちゃってエンジニア”です。
自分用の試作は適当にコード書くのですが、今回はさすがに緊張感持って書き直しました。
至らぬところがあるかと思いますが、お気づきの点がございましたらご指摘いただけると幸いです。

また、今回の装置開発、プログラム(特にSIMに割り当てた名前の取得・非同期処理の対応)はKenichiro Wada さんの記事を参考にさせていただきました。
この場をお借りして御礼申し上げます。

最後になりますが、このような機会を与えていただき、ありがとうございました。
大変でしたが、良い経験になりました。
中年になって初めてのことができるのは非常にありがたい限りです。

では引き続きアドベントカレンダーをお楽しみください!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む