20200619のNode.jsに関する記事は8件です。

socket.ioでビデオ通話+ユーザー名表示

Zoomにあるようなビデオ映像に名前を入れて欲しい…

以前に作成したsocket.io にセッション情報を渡すExpress+MySQLでログインするの続編です。

改めて、このプログラムを書いた経緯です。

  1. 自社ネットワーク内でビデオ通話をしたいという相談をうける
  2. hidden.inを見つける
  3. これは便利だと感動し、セットアップする
  4. ユーザー名(支店名)を表示して欲しいと相談をうける
  5. Node.jsを勉強する

このような経緯で今に至っております。

冒頭に紹介した記事で、MySQLにユーザー情報を保存しておき、passportモジュールを読み込み、ログイン判定をする。というところまでが出来ている想定で話を進めます。

では、実装方法です!

編集するファイルは

  • index.js
  • static/js/index.js
  • static/css/app.css
  • views/screen.ejs(前回説明不足でしたが、元からあるstatic/screen.htmlをejsファイルに変更しています。ディレクトリもviewsフォルダを作成しています。)

プログラムの流れ(イメージ)

  1. ログインしているユーザーがemit('join')する
  2. サーバーサイドでon('join')された時emit('joined')でユーザー名とIDを渡す
  3. サーバーサイドでemit('otherjoined')する
  4. 2に対してクライアントでユーザー名を保持する
  5. on('otherjoined')した時にmyNameSend関数を呼び出し、自分の名前とIDをemitする
  6. サーバーサイドでon('mynamesend')された時にemit('othernamesend')で値を送る
  7. クライアントサイドでon('othernamesend')を受け取った時にユーザー名を表示させる
index.js
//中略
//サーバーサイド
//プログラムの流れ2,3

socket.on('join', function (req) {
    io.to(socket.id).emit("joined", { id: socket.id,name:socket.request.session.passport.user });
    socket.broadcast.to(socket.room).json.emit("otherJoined", { id: socket.id});

}

ここでは、joinしたユーザーに対して、joinedを送る+ルームにいるメンバーに対して、他のユーザーがルームに入ったことを知らせています。

static/js/index.js
//中略
//クライアントサイド
//プログラムの流れ4,5
socket.on('joined', function (event) {
        joinedToRoom = true;
        // socket.idが入っている
        selfId = event.id;
        //usernameが入っている
        myName = event.name;
      }).on('otherJoined', function (event) {
        sendOffer(event.id);
        myNameSend(selfId,myName);
    })

//myNameSend関数
function myNameSend(myId,myName){
   socket.emit('mynamesend',{id:myId,name:myName});   
}

見ての通りです

index.js
//中略
//サーバーサイド
//プログラムの流れ6
socket.on('mynamesend',function(id,name){
    socket.broadcast.to(socket.room).emit('othernamesend',{id,name});
  });

myNameSend関数で送られてきた値をブロードキャストしてルームに所属するユーザーに送っています。

static/js/index.js
//中略
//クライアントサイド
//プログラムの流れ7
.on('othernamesend',function(event){
        otherNameCreate(event);        
    })

function otherNameCreate(otherconf){
     if(otherconf.id.name != myName){
    var otherId = 'area_' + otherconf.id.id;
    var otherArea = document.getElementById(otherId);
    if(!otherArea){
        otherArea = document.createElement('div');
        otherArea.className = 'remoteArea';
        otherArea.id = otherId;
        document.getElementById('video_wrap').appendChild(otherArea);
        remoteName = document.createElement('span');
        remoteName.className = 'remoteName';
        remoteName.textContent = otherconf.id.name;
        otherArea.appendChild(remoteName);
    }
    }else{
    console.log('名前が一緒');
    return;
    }
}

名前表示の制限として自分と違う名前なら表示させるようにしています。

area_ + socket.idというように一意なエリアを作成し、そこに名前を表示させるHTMLを生成しているプログラムです。

この流れでログインしたユーザー名の表示が出来るようになりました!

全コードGitHubに上げています。

今後の課題

  • 対応ブラウザを増やす(現在はGoogle Chromeのみ)
  • FullScreenにした時にもユーザー名を表示させる、全画面時の画面編集機能を追加する

などです。
まだまだ、稚拙なコードの書き方なので精進したいと思います。

間違い、改善などがありましたら、よろしくお願いします。

読んでくださりありがとうございました。
以上です

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

socket ioでビデオ通話+ユーザー名表示

Zoomにあるようなビデオ映像に名前を入れて欲しい…

以前に作成したsocket.io にセッション情報を渡すExpress+MySQLでログインするの続編です。

改めて、このプログラムを書いた経緯です。

  1. 自社ネットワーク内でビデオ通話をしたいという相談をうける
  2. hidden.inを見つける
  3. これは便利だと感動し、セットアップする
  4. ユーザー名(支店名)を表示して欲しいと相談をうける
  5. Node.jsを勉強する

このような経緯で今に至っております。

冒頭に紹介した記事で、MySQLにユーザー情報を保存しておき、passportモジュールを読み込み、ログイン判定をする。というところまでが出来ている想定で話を進めます。

では、実装方法です!

編集するファイルは

  • index.js
  • static/js/index.js
  • static/css/app.css
  • views/screen.ejs(前回説明不足でしたが、元からあるstatic/screen.htmlをejsファイルに変更しています。ディレクトリもviewsフォルダを作成しています。)

プログラムの流れ(イメージ)

  1. ログインしているユーザーがemit('join')する
  2. サーバーサイドでon('join')された時emit('joined')でユーザー名とIDを渡す
  3. サーバーサイドでemit('otherjoined')する
  4. 2に対してクライアントでユーザー名を保持する
  5. on('otherjoined')した時にmyNameSend関数を呼び出し、自分の名前とIDをemitする
  6. サーバーサイドでon('mynamesend')された時にemit('othernamesend')で値を送る
  7. クライアントサイドでon('othernamesend')を受け取った時にユーザー名を表示させる
index.js
//中略
//サーバーサイド
//プログラムの流れ2,3

socket.on('join', function (req) {
    io.to(socket.id).emit("joined", { id: socket.id,name:socket.request.session.passport.user });
    socket.broadcast.to(socket.room).json.emit("otherJoined", { id: socket.id});

}

ここでは、joinしたユーザーに対して、joinedを送る+ルームにいるメンバーに対して、他のユーザーがルームに入ったことを知らせています。

static/js/index.js
//中略
//クライアントサイド
//プログラムの流れ4,5
socket.on('joined', function (event) {
        joinedToRoom = true;
        // socket.idが入っている
        selfId = event.id;
        //usernameが入っている
        myName = event.name;
      }).on('otherJoined', function (event) {
        sendOffer(event.id);
        myNameSend(selfId,myName);
    })

//myNameSend関数
function myNameSend(myId,myName){
   socket.emit('mynamesend',{id:myId,name:myName});   
}

見ての通りです

index.js
//中略
//サーバーサイド
//プログラムの流れ6
socket.on('mynamesend',function(id,name){
    socket.broadcast.to(socket.room).emit('othernamesend',{id,name});
  });

myNameSend関数で送られてきた値をブロードキャストしてルームに所属するユーザーに送っています。

static/js/index.js
//中略
//クライアントサイド
//プログラムの流れ7
.on('othernamesend',function(event){
        otherNameCreate(event);        
    })

function otherNameCreate(otherconf){
     if(otherconf.id.name != myName){
    var otherId = 'area_' + otherconf.id.id;
    var otherArea = document.getElementById(otherId);
    if(!otherArea){
        otherArea = document.createElement('div');
        otherArea.className = 'remoteArea';
        otherArea.id = otherId;
        document.getElementById('video_wrap').appendChild(otherArea);
        remoteName = document.createElement('span');
        remoteName.className = 'remoteName';
        remoteName.textContent = otherconf.id.name;
        otherArea.appendChild(remoteName);
    }
    }else{
    console.log('名前が一緒');
    return;
    }
}

名前表示の制限として自分と違う名前なら表示させるようにしています。

area_ + socket.idというように一意なエリアを作成し、そこに名前を表示させるHTMLを生成しているプログラムです。

この流れでログインしたユーザー名の表示が出来るようになりました!

全コードGitHubに上げています。

今後の課題

  • 対応ブラウザを増やす(現在はGoogle Chromeのみ)
  • FullScreenにした時にもユーザー名を表示させる、全画面時の画面編集機能を追加する

などです。
まだまだ、稚拙なコードの書き方なので精進したいと思います。

間違い、改善などがありましたら、よろしくお願いします。

読んでくださりありがとうございました。
以上です

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

コスパ最強IoT家電!TPLink製品をRaspberryPiから操作

TPLinkとは?

ルータを主力とする中国・深圳のネットワーク機器メーカーです。
近年はスマート電球、スマートプラグ等のIoT家電に力を入れており、コスパの良さからAmazonで独自の地位を築いています。

plug.jpg
             スマートプラグのHS105

bulb.jpg
             スマート電球のKL110

今回は、APIを使用して、
・機器のON-OFF操作
・ON-OFF、電球の明るさ等の情報取得
を、PythonおよびNode.jsで実行してみました

IoT家電として思いつく用途の多くを上記でカバーできるので
応用の可能性を感じる結果となりました!

必要なもの

・PC
・RaspberryPi
・TPLink製スマートプラグあるいは電球
今回は下記3製品を試しました
HS105:スマートプラグ
KL110:ホワイト電球
KL130:カラー電球

①データ取得の確認

まずは、TPLinkからデータが取得できるかターミナル上でテストします。

※参考にさせて頂いた記事
https://lmjs7.net/blog/tag/tp-link/
https://qiita.com/tmisuoka0423/items/582ff0c303abe8570ee5

IPを調べる

tplink-smarthome-api(参考)をインストール

sudo npm install -g tplink-smarthome-api

下記コマンドで、接続しているTPLinkデバイス一覧を取得

tplink-smarthome-api search
HS105(JP) plug IOT.SMARTPLUGSWITCH 192.168.0.101 9999 B0BE76‥ スマートプラグ
KL110(JP) bulb IOT.SMARTBULB 192.168.0.102 9999 98DAC4‥ ホワイト電球
KL130(JP) bulb IOT.SMARTBULB 192.168.0.103 9999 0C8063‥ カラー電球

3つのデバイス全てが検出できていることが分かります

デバイス動作情報の取得確認

下記コマンドで、デバイスの設定やOnOffが取得できる

tplink-smarthome-api getSysInfo [デバイスのIPアドレス]:9999

・KL130(カラー電球)の例

  :
  ctrl_protocols: { name: 'Linkie', version: '1.0' },
  ↓ここからがデバイスの設定
  light_state: {
    on_off: 1,
    mode: 'normal',
    hue: 0,
    saturation: 0,
    color_temp: 2700,
    brightness: 100
  },
  ↑ここまでがデバイスの設定
  is_dimmable: 1,
  is_color: 1,
  :

on_off:0なら電源OFF、1なら電源ON
hue:色?(白色モードのとき0)
color_temp:色温度(白色モード以外のとき0)
brightness:明るさ(%単位)
と思われます

・KL110(ホワイト電球)の例

  :
  ctrl_protocols: { name: 'Linkie', version: '1.0' },
  ↓ここからがデバイスの設定
  light_state: {
    on_off: 1,
    mode: 'normal',
    hue: 0,
    saturation: 0,
    color_temp: 2700,
    brightness: 100
  },
  ↑ここまでがデバイスの設定
  is_dimmable: 1,
  is_color: 0,
  :

on_off:0なら電源OFF、1なら電源ON
hue:色相(白色モードのとき0)
saturation:彩度
color_temp:色温度(白色モード以外のとき0)
brightness:明るさ(%単位)
と思われます。
KL130とほぼ同じですが、カラーではないのでis_color: 0となっていると思われます。

・KL105(スマートプラグ)の例

  alias: '',
↓ここからがデバイスの設定
  relay_state: 1,
  on_time: 288,
  active_mode: 'none',
  feature: 'TIM',
  updating: 0,
  icon_hash: '',
  rssi: -52,
  led_off: 0,
  longitude_i: 1356352,
  latitude_i: 348422,
↑ここまでがデバイスの設定
  hwId: '047D‥',

relay_state:0なら電源OFF、1なら電源ON
on_time:連続電源ON時間
rssi: WiFiの信号強度
と思われます。
経度(logitude)と緯度(latitude)も表示されていますが、実際の場所と5キロくらいずれていて謎が深まります。

上記で、コマンドで欲しい情報が取得できることが確認できました!
次章以降で、プログラム(Node.js&Python)から取得・操作する方法を記載します。

②Node.jsで状態取得

※「Pythonを使うからNode.jsの説明はいらん!」という方は、この章を飛ばして③に移動してください

こちらを参考に、Node.jsを

npmにパスを通す(Windowの場合)

Windowsだとnpmのグローバルインストール先にパスが通っておらず、Node.jsでモジュールが読み込めないので、下記を参考にパスを通してください
https://qiita.com/shiftsphere/items/5610f692899796b03f99

npmにパスを通す(RaspberryPiの場合)

下記コマンドで、グローバルでのnpmモジュールインストール先を調べます
(なぜかWindowsのときのコマンド"npm bin -g"で見つかるフォルダとは違うようです)

npm ls -g 

下記コマンドで.profileを編集します。
※SSH環境では.profileの代わりに、.bash_profileを編集してください

nano /home/[ユーザ名]/.profile

.profileの最後に下記の1行を追加してrebootしてください

export NODE_PATH=[上で調べたパス]/node_modules

下記コマンドで指定したパスが表示されれば成功です

printenv NODE_PATH

node.jsスクリプトの作成

下記スクリプトを作成します

tplink_test.js
const { Client } = require('tplink-smarthome-api');
const client = new Client();
client.getDevice({ host: '192.168.0.102' }).then(device => {
  device.getSysInfo().then(console.log);
});

下記コマンドでスクリプトを実行すると、①と同様に各種情報が取得できます

node tplink_test.js

※上記をcsvロギングするスクリプト(③のPythonスクリプトと同機能)も作成しましたが、私のJavaScriptスキルが低くうまく動作しないときがある(非同期部分の処理順が逆転する)ので、コードはここには貼らないこととします
下記GitHubにアップロードしたので、自己責任で改造して使用していただければと思います。
(願わくば無知な私に処理順が逆転する理由もコメント…頂けると嬉しいです笑)
https://github.com/c60evaporator/TPLink_Info_Nodejs

③Pythonで状態取得

私のJavaScriptスキル不足でNode.jsでのロギングが上手くいかなかったので、
気を取り直してPythonで操作・ロギングするスクリプトを作りました。

PythonはNode.jsほど丁寧なドキュメントが見当たらず苦戦しましたが、こちらこちらのコードを解読して、スクリプトを作成しました。

TPLink操作クラスの作成

上記コードを参考に、下記の4つのクラスを作成しました
TPLink_Common():プラグ、電球共通機能のクラス
TPLink_Plug():プラグ専用機能のクラス(TPLink_Common()を継承)
TPLink_Bulb():電球専用機能のクラス(TPLink_Common()を継承)
GetTPLinkData():上記クラスを利用して、データを取得するクラス

tplink.py
import socket
from struct import pack
import json

#TPLinkデータ取得用クラス
class GetTPLinkData():
    #プラグデータ取得用メソッド
    def get_plug_data(self, ip):
        #プラグ操作用クラス作成
        plg = TPLink_Plug(ip)
        #データを取得し、dictに変換
        rjson = plg.info()
        rdict = json.loads(rjson)
        return rdict

    #電球データ取得用メソッド
    def get_bulb_data(self, ip):
        #電球操作用クラス作成
        blb = TPLink_Bulb(ip)
        #データを取得し、dictに変換
        rjson = blb.info()
        rdict = json.loads(rjson)
        return rdict

#TPLink電球&プラグ共通クラス
class TPLink_Common():
    def __init__(self, ip, port=9999):
        """Default constructor
        """
        self.__ip = ip
        self.__port = port

    def info(self):
        cmd = '{"system":{"get_sysinfo":{}}}'
        receive = self.send_command(cmd)
        return receive

    def send_command(self, cmd, timeout=10):
        try:
            sock_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock_tcp.settimeout(timeout)
            sock_tcp.connect((self.__ip, self.__port))
            sock_tcp.settimeout(None)
            sock_tcp.send(self.encrypt(cmd))
            data = sock_tcp.recv(2048)
            sock_tcp.close()

            decrypted = self.decrypt(data[4:])
            print("Sent:     ", cmd)
            print("Received: ", decrypted)
            return decrypted

        except socket.error:
            quit("Could not connect to host " + self.__ip + ":" + str(self.__port))
            return None

    def encrypt(self, string):
        key = 171
        result = pack('>I', len(string))
        for i in string:
            a = key ^ ord(i)
            key = a
            result += bytes([a])
        return result

    def decrypt(self, string):
        key = 171
        result = ""
        for i in string:
            a = key ^ i
            key = i
            result += chr(a)
        return result

#TPLinkプラグ操作用クラス
class TPLink_Plug(TPLink_Common):
    def on(self):
        cmd = '{"system":{"set_relay_state":{"state":1}}}'
        receive = self.send_command(cmd)

    def off(self):
        cmd = '{"system":{"set_relay_state":{"state":0}}}'
        receive = self.send_command(cmd)

    def ledon(self):
        cmd = '{"system":{"set_led_off":{"off":0}}}'
        receive = self.send_command(cmd)

    def ledoff(self):
        cmd = '{"system":{"set_led_off":{"off":1}}}'
        receive = self.send_command(cmd)

    def set_countdown_on(self, delay):
        cmd = '{"count_down":{"add_rule":{"enable":1,"delay":' + str(delay) +',"act":1,"name":"turn on"}}}'
        receive = self.send_command(cmd)

    def set_countdown_off(self, delay):
        cmd = '{"count_down":{"add_rule":{"enable":1,"delay":' + str(delay) +',"act":0,"name":"turn off"}}}'
        receive = self.send_command(cmd)

    def delete_countdown_table(self):
        cmd = '{"count_down":{"delete_all_rules":null}}'
        receive = self.send_command(cmd)

    def energy(self):
        cmd = '{"emeter":{"get_realtime":{}}}'
        receive = self.send_command(cmd)
        return receive

#TPLink電球操作用クラス
class TPLink_Bulb(TPLink_Common):
    def on(self):
        cmd = '{"smartlife.iot.smartbulb.lightingservice":{"transition_light_state":{"on_off":1}}}'
        receive = self.send_command(cmd)

    def off(self):
        cmd = '{"smartlife.iot.smartbulb.lightingservice":{"transition_light_state":{"on_off":0}}}'
        receive = self.send_command(cmd)

    def transition_light_state(self, hue: int = None, saturation: int = None, brightness: int = None,
                               color_temp: int = None, on_off: bool = None, transition_period: int = None,
                               mode: str = None, ignore_default: bool = None):
        # copy all given argument name-value pairs as a dict
        d = {k: v for k, v in locals().items() if k is not 'self' and v is not None}
        r = {
            'smartlife.iot.smartbulb.lightingservice': {
                'transition_light_state': d
            }
        }
        cmd = json.dumps(r)
        receive = self.send_command(cmd)
        print(receive)

    def brightness(self, brightness):
        self.transition_light_state(brightness=brightness)

    def purple(self, brightness = None, transition_period = None):
        self.transition_light_state(hue=277, saturation=86, color_temp=0, brightness=brightness, transition_period=transition_period)

    def blue(self, brightness = None, transition_period = None):
        self.transition_light_state(hue=240, saturation=100, color_temp=0, brightness=brightness, transition_period=transition_period)

    def cyan(self, brightness = None, transition_period = None):
        self.transition_light_state(hue=180, saturation=100, color_temp=0, brightness=brightness, transition_period=transition_period)

    def green(self, brightness = None, transition_period = None):
        self.transition_light_state(hue=120, saturation=100, color_temp=0, brightness=brightness, transition_period=transition_period)

    def yellow(self, brightness = None, transition_period = None):
        self.transition_light_state(hue=60, saturation=100, color_temp=0, brightness=brightness, transition_period=transition_period)

    def orange(self, brightness = None, transition_period = None):
        self.transition_light_state(hue=39, saturation=100, color_temp=0, brightness=brightness, transition_period=transition_period)

    def red(self, brightness = None, transition_period = None):
        self.transition_light_state(hue=0, saturation=100, color_temp=0, brightness=brightness, transition_period=transition_period)

    def lamp_color(self, brightness = None):
        self.transition_light_state(color_temp=2700, brightness=brightness)

TPLink操作クラスの実行方法

上記クラスは、Pythonコード上で下記のように実行できます

・電球の電源をONにしたいとき

TPLink_Bulb(電球のIPアドレス).on()

・プラグの電源をOFFにしたいとき

TPLink_Plug(プラグのIPアドレス).off()

・10秒後にプラグをONにしたいとき

TPLink_Plug(プラグのIPアドレス).set_countdown_on(10)

・電球の明るさを10%にしたいとき

TPLink_Bulb(電球のIPアドレス).brightness(10)

・電球を赤色にしたいとき(カラー電球のみ)

TPLink_Bulb(電球のIPアドレス).red()

・電球のOn-Off等の情報を取得

info = GetTPLinkData().get_plug_data(プラグのIPアドレス)

※上記メソッドは、取得したjson情報をdict形式に変換して出力されます。
出力される電球情報は①を参照ください

④ロギング用Pythonスクリプトの作成

前章最後の方法を利用して、電球やプラグの情報をロギングするスクリプトを作成しました。
スクリプトの構造はこちらと同じなので、リンク先をご一読いただければと思います。

設定ファイル

こちらの記事同様、管理をしやすくするため下記2種類の設定ファイルを作成しました
・DeviceList.csv:センサごとに必要情報を記載

DeviceList.csv
ApplianceName,ApplianceType,IP,Retry
TPLink_KL130_ColorBulb_1,TPLink_ColorBulb,192.168.0.103,2
TPLink_KL110_WhiteBulb_1,TPLink_WhiteBulb,192.168.0.102,2
TPLink_HS105_Plug_1,TPLink_Plug,192.168.0.101,2

カラムの意味は下記となります
ApplianceName:デバイス名を管理、同種類のデバイスが複数あるときの識別用
ApplianceType:デバイスの種類。
    TPLink_ColorBulb:カラー電球(KL130等)
    TPLink_WhiteBulb:白色電球(KL110等)
    TPLink_Plug:スマートプラグ(HS105等)
IP:デバイスのIPアドレス
Retry:最大再実行回数詳細(取得失敗時の再実行回数、詳しくはこちら

・config.ini:CSVおよびログ出力ディレクトリを指定

config.ini
[Path]
CSVOutput = /share/Data/Appliance
LogOutput = /share/Log/Appliance
どちらもsambaで作成した共有フォルダ内に出力すると、RaspberryPi外からアクセスできて便利です。

実際のスクリプト

appliance_data_logger.py
from tplink import GetTPLinkData
import logging
from datetime import datetime, timedelta
import os
import csv
import configparser
import pandas as pd

#グローバル変数
global masterdate

######TPLinkのデータ取得######
def getdata_tplink(appliance):
    #データ値が得られないとき、最大appliance.Retry回スキャンを繰り返す
    for i in range(appliance.Retry):
        try:
            #プラグのとき
            if appliance.ApplianceType == 'TPLink_Plug':
                applianceValue = GetTPLinkData().get_plug_data(appliance.IP)
            #電球のとき
            elif appliance.ApplianceType == 'TPLink_ColorBulb' or appliance.ApplianceType == 'TPLink_WhiteBulb':
                applianceValue = GetTPLinkData().get_bulb_data(appliance.IP)
            else:
                applianceValue = None
        #エラー出たらログ出力
        except:
            logging.warning(f'retry to get data [loop{str(i)}, date{str(masterdate)}, appliance{appliance.ApplianceName}]')
            applianceValue = None
            continue
        else:
            break

    #値取得できていたら、POSTするデータをdictに格納
    if applianceValue is not None:
        #プラグのとき
        if appliance.ApplianceType == 'TPLink_Plug':
            data = {        
                'ApplianceName': appliance.ApplianceName,        
                'Date_Master': str(masterdate),
                'Date': str(datetime.today()),
                'IsOn': str(applianceValue['system']['get_sysinfo']['relay_state']),
                'OnTime': str(applianceValue['system']['get_sysinfo']['on_time'])
            }
        #電球のとき
        else:
            data = {        
                'ApplianceName': appliance.ApplianceName,        
                'Date_Master': str(masterdate),
                'Date': str(datetime.today()),
                'IsOn': str(applianceValue['system']['get_sysinfo']['light_state']['on_off']),
                'Color': str(applianceValue['system']['get_sysinfo']['light_state']['hue']),
                'ColorTemp': str(applianceValue['system']['get_sysinfo']['light_state']['color_temp']),
                'Brightness': str(applianceValue['system']['get_sysinfo']['light_state']['brightness'])
            }
        return data

    #取得できていなかったら、ログ出力
    else:
        logging.error(f'cannot get data [loop{str(appliance.Retry)}, date{str(masterdate)}, appliance{appliance.ApplianceName}]')
        return None

######データのCSV出力######
def output_csv(data, csvpath):
    appliancename = data['ApplianceName']
    monthstr = masterdate.strftime('%Y%m')
    #出力先フォルダ名
    outdir = f'{csvpath}/{appliancename}/{masterdate.year}'
    #出力先フォルダが存在しないとき、新規作成
    os.makedirs(outdir, exist_ok=True)
    #出力ファイルのパス
    outpath = f'{outdir}/{appliancename}_{monthstr}.csv'

    #出力ファイル存在しないとき、新たに作成
    if not os.path.exists(outpath):        
        with open(outpath, 'w', newline="") as f:
            writer = csv.DictWriter(f, data.keys())
            writer.writeheader()
            writer.writerow(data)
    #出力ファイル存在するとき、1行追加
    else:
        with open(outpath, 'a', newline="") as f:
            writer = csv.DictWriter(f, data.keys())
            writer.writerow(data)

######メイン######
if __name__ == '__main__':    
    #開始時刻を取得
    startdate = datetime.today()
    #開始時刻を分単位で丸める
    masterdate = startdate.replace(second=0, microsecond=0)   
    if startdate.second >= 30:
        masterdate += timedelta(minutes=1)

    #設定ファイルとデバイスリスト読込
    cfg = configparser.ConfigParser()
    cfg.read('./config.ini', encoding='utf-8')
    df_appliancelist = pd.read_csv('./ApplianceList.csv')
    #全センサ数とデータ取得成功数
    appliance_num = len(df_appliancelist)
    success_num = 0

    #ログの初期化
    logname = f"/appliancelog_{str(masterdate.strftime('%y%m%d'))}.log"
    logging.basicConfig(filename=cfg['Path']['LogOutput'] + logname, level=logging.INFO)

    #取得した全データ保持用dict
    all_values_dict = None

    ######デバイスごとにデータ取得######
    for appliance in df_appliancelist.itertuples():
        #ApplianceTypeがTPLinkeであることを確認
        if appliance.ApplianceType in ['TPLink_Plug','TPLink_ColorBulb','TPLink_WhiteBulb']:
            data = getdata_tplink(appliance)
        #上記以外
        else:
            data = None        

        #データが存在するとき、全データ保持用Dictに追加し、CSV出力
        if data is not None:
            #all_values_dictがNoneのとき、新たに辞書を作成
            if all_values_dict is None:
                all_values_dict = {data['ApplianceName']: data}
            #all_values_dictがNoneでないとき、既存の辞書に追加
            else:
                all_values_dict[data['ApplianceName']] = data

            #CSV出力
            output_csv(data, cfg['Path']['CSVOutput'])
            #成功数プラス
            success_num+=1

    #処理終了をログ出力
    logging.info(f'[masterdate{str(masterdate)} startdate{str(startdate)} enddate{str(datetime.today())} success{str(success_num)}/{str(appliance_num)}]')

上記を実行すれば、設定ファイル"CSVOutput"で指定したフォルダに、取得データがデバイス名と日時の名称でCSV出力されます
tplinkcsv.png

以上で、情報取得が完了です

おわりに

RaspberrypPiで24時間稼働、かつPythonはIFTTTよりも自由度が高いので、色々なアイデアを具現化可能です
・人感センサと組み合わせて、人が入ったら電気が点くようにする
・30分以上人がいなければ、電気を消す
・人によって電球の明るさを自動で切り替える
などなどです。

いくつか作りたいものがあるので、製作が完了したらまた記事にしようと思います

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

node.js Firebase Functionで作ったGoogle Chat BotでChatからのリクエストであるかの検証を行う

皆さんこんにちわ。Chat Bot作ってますか?まだApps Scriptで作ってますか?
このごろは外部のG SuiteテナントユーザーともやりとりできるようになったGoogle Chat。
Apps Scriptのままじゃテナントまたげません1
やっぱりFirebase Functionsでしょう! (Cloud Functionsもほぼ同じ)

…ってことでCloud Functionsをnode.jsで作り始めると、開発ドキュメントの「Verifying bot authenticity」でGoogle Chatからのリクエストかどうかを検証するためのサンプルコードが書かれていることに気づきます。
この対策をしないと、Bot Function URLが知らんやつに知られたら、Bot messageのsender情報を偽装されちゃうかもしれません。Botでユーザー情報を保持していてメッセージよって返すようにしていたら、その情報が抜かれちゃいます。そりゃまずいってことでサンプルのとおりに実装しようとしたところ…

node.jsでBot Function作ってるのに、JavaとPythonのサンプルしか書かれていない!2

まじありえんと…そこから調べるのいろいろ苦労したんですがシンプルな方法で解決したので記録します。
以下ざっくりとした手順です。

  1. まずありがたい、google-id-token を使わせてもらう。
  2. node_modules/google-id-token/Readme.md をざっと読む。
  3. Readme.mdに記載のサンプルコードを、まるっとfunctionにする。
  4. 好みの問題もあるけど required('request') は使いづらいので、node-fetch で代替しちゃう。
  5. Firebaseプロジェクトに紐付くGCPのプロジェクト番号を、firebase funsionsコンフィグにセット。
  6. BOT URLとなるエンドポイント関数のド頭で、まるっと作ったfunctionに、req.headers.authorizationを渡して評価させてNGなら「botから呼べ!」と怒るコードを書く。

では手順の詳細。

firebase initしたプロジェクトディレクトリ内には、functionsディレクトリが作成されて、その中にindex.jsやpackage.jsonが作られてます。プロジェクトディレクトリ内はfirebaseコマンドを叩く場所。functionsディレクトリ内はFirebase Functionsのパッケージ構成する場所。…としっかり分けて考えましょう。たまに私もnpmコマンドをプロジェクトディレクトリ内で叩いて「うわーまたやっちまったー」と泣いてます。注意しましょう。
functionsディレクトリ内で、手順 1.と手順 4.をいっぺんにやっちゃいます。

# npm i google-id-token
# npm i node-fetch

まるっとfunctionにしたものは、verifyChatIdToken.jsとでもしておきます。

google-id-tokenのReadme.mdでは、記載されているgetGoogleCerts関数でトークンの検証に使用する署名のURLが'https://www.googleapis.com/oauth2/v1/certs' となっています。しかしGoogle Chatの開発ドキュメントのJava/Pythonのサンプルを読むと、'https://www.googleapis.com/service_accounts/v1/metadata/x509/chat@system.gserviceaccount.com'が使われていることがわかりますので差し替える必要があります。
これをしないとJWTのデコード自体はできるものの署名による検証結果(isAuthentic)はtrueになりません。

まるっとコピーしてfunctionにしたのち、getGoogleCerts関数はgetChatCertsに名前を変更して、requestはfetchに差し替えてます。またJWTのBearerトークンからデコードした結果から、署名の検証が成功した場合のみtrueを返すようにしたものがこちら。

verifyChatIdToken.js
'use strict'

const functions = require('firebase-functions')
const fetch = require('node-fetch')
const googleIdToken = require('google-id-token')
const PROJECT_NUMBER = functions.config().project.number
const CHAT_SERVICE_ISS = "chat@system.gserviceaccount.com"
const CHAT_SERVICE_CERTS_URI =
  `https://www.googleapis.com/service_accounts/v1/metadata/x509/${CHAT_SERVICE_ISS}`


function getChatCerts(kid, callback) {
  fetch(CHAT_SERVICE_CERTS_URI)
    .then(res => res.json())
    .then(certs => {
      callback(null, certs[kid])
    }).catch(err => {
      callback(err, {})
    })
}

module.exports = async function verifyChatIdToken(authorization) {
  const BEARER_STARTS = "Bearer "

  if (!authorization) {
    console.log("authorization not found")
    return false
  }
  if (!authorization.startsWith(BEARER_STARTS)) {
    console.log(`authorization is not a ${BEARER_STARTS}idtoken`)
    return false
  }
  const idToken = authorization.substring(BEARER_STARTS.length)

  const parser = new googleIdToken({ getKeys: getChatCerts })

  return new Promise((resolve) => {
    parser.decode(idToken, function (err, token) {
      let result = false
      if (err || !token) {
        console.log("error while parsing the google token: " + err)
      } else {
        console.log("parsed id_token is:\n" + JSON.stringify(token))
        if (!token.isAuthentic) {
          console.log("failed verify by signature\n")
        } else {
          result = token.header && token.data &&
            PROJECT_NUMBER == token.data.aud &&
            CHAT_SERVICE_ISS == token.data.iss
        }
      }
      resolve(result)
    })
  })
}

それと、PROJECT_NUMBERという定数で、Bot URLとなるFirebase Functionが属するGCPプロジェクトのプロジェクト番号をFirebase Function CONFIGから参照するようにしていますので、手順 5.をやっちゃいます。GCPのプロジェクトダッシュボードでプロジェクトIDに並んでプロジェクト番号が記載されていますのでコピってきて、firebaseプロジェクトディレクトリで設定します。

# firebase functions:config:set project.number="{コピってきたプロジェクト番号}"

audienceがPROJECT_NUMBER、issuerがCHAT_SERVICE_ISSと一致するかも併せてチェックして、これでChatからの要求確認が十分できました。

これであとは手順 6.のみ。ド頭でチェックしてGoogle Chatから呼ばれていない場合は怒ってあげるだけです。開発ドキュメントにあるように、User-Agentも固有の名前があるので、ついでにチェックします。

index.js
const verifyChatIdToken = require('./verifyChatIdToken')

exports.{BOT名} = functions.https.onRequest(async (req, res)=>{
  if (req.method !== 'POST' || !req.body || !req.body.message
        || req.headers["user-agent"] !== "Google-Dynamite"
        || !(await verifyChatIdToken(req.headers.authorization))) {
    res.status(400).send('Hello! This function is meant to be used in a Google Chat Room or DM.\n')
    return
  }
  :
  // ボットの主処理
  :
})

以上でございます。
これだけで安心・安全なボットにグレードアップしますよ! お試しください。


  1. 厳密に言えばMarketplaceに公開できればApps Scriptでもいけますが、公開審査までが大変だし、そもそも開発中にマルチテナントテストができないのがApps ScriptのChat Botの難点だったりします。 

  2. 同ページからリクエスト送りましたが音沙汰なしです…。 

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

ESLint7系を使っていると「TypeError: createRequire is not a function」が出る

概要

ESLint 7 系とNode.js 12未満をつかっていると以下のエラーがでる場合がある

ESLint 7 がサポートしている Node.js バージョンは ^10.12.0 || >=12.0.0 となっています

↑だそうです。(コメントありがとうございました)

TypeError: createRequire is not a function
Referenced from: /something/.eslintrc
    at Object.resolve (/something/node_modules/eslint/lib/shared/relative-module-resolver.js:28:20)
    at ConfigArrayFactory._loadExtendedShareableConfig (/something/node_modules/eslint/lib/cli-engine/config-array-factory.js:854:39)
    at ConfigArrayFactory._loadExtends (/something/node_modules/eslint/lib/cli-engine/config-array-factory.js:763:25)
    at ConfigArrayFactory._normalizeObjectConfigDataBody (/something/node_modules/eslint/lib/cli-engine/config-array-factory.js:702:25)
    at _normalizeObjectConfigDataBody.next (<anonymous>)
    at ConfigArrayFactory._normalizeObjectConfigData (/something/node_modules/eslint/lib/cli-engine/config-array-factory.js:647:20)
    at _normalizeObjectConfigData.next (<anonymous>)
    at ConfigArrayFactory.loadInDirectory (/something/node_modules/eslint/lib/cli-engine/config-array-factory.js:495:28)
    at CascadingConfigArrayFactory._loadConfigInAncestors (/something/node_modules/eslint/lib/cli-engine/cascading-config-array-factory.js:355:46)
    at CascadingConfigArrayFactory._loadConfigInAncestors (/something/node_modules/eslint/lib/cli-engine/cascading-config-array-factory.js:374:20)

対策

その1

Node.jsを最新にする

その2

ESLintをダウングレードする

package.json (ESLintを6系にダウングレード)
"eslint": "^6.8.0",
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

virtual背景のロジックをnpm パッケージ化した件

はじめに

先日、自分史上初めてnpmのパッケージを作成したので、記録として作業の流れを残しておくものです。
新しい情報は少ないかもしれません。
作成したパッケージは以前紹介したvirtual背景のコンポーネントです。

https://qiita.com/wok/items/962929e63bc98e4033b9

movie2.gif

もしよかったら使ってやってください。

概要

Typescriptを用いた環境でnpmパッケージを作成してpublishするまでの流れです。

前提

  • npmのアカウントは作っておく。
  • githubのアカウントとリポジトリは作っておく。
  • 作成したgitリポジトリをクローンして、そのディレクトリに移動している状態。

作業の流れ

まずはじめに、npmの環境設定とログインをしておきましょう。

$ npm set init.author.name "xxxxxx"
$ npm set init.author.email "xxxx@xxxx.co.jp"
$ npm set init.author.url "https://qiita.com/wok"
$ npm adduser

それでは、プロジェクトの初期化を行いpackage.jsonを作成します。

$ npm init -y

次に必要なモジュールをインストールします。
ここではtypescriptを用いてパッケージを作成するので、typescript用の環境を準備します。
npx tsc --initによりtsconfig.jsonが生成されます。

$ npm install -D typescript @types/node
$ npx tsc --init

パッケージを利用する人が型推論を使えるように、宣言ファイルを生成するようにtsconfig.jsonを変更します。
また、私はデバッグが楽になるようにsource mapも生成しておきます。これは好みです。
出力先は色んな場所で解説されているものに合わせて./distにしておきます。
moduleは用途により変える必要があるようですが、あまり理解できていません。今回はesnextにしています。
esnextにした場合は、moduleResolutionをnodeにしておかないとコンパイルが通らないようです。
(この辺がjsをいまいち好きになれないところなんだよな。そんなに覚えられないよ。)

"declaration": true, 
"sourceMap": true,
"outDir": "./dist",
"module": "esnext",
"moduleResolution": "node", 

この段階で、ソースコードを書き始めます。(実際はもともとのソースコードをコピペした。)
そしてコンパイル。エラーがなければ下記のようなファイルが出力されていると思います。

$ npx tsc
$ ls dist/
index.d.ts  index.js  index.js.map

次に、package.jsonを変更してエントリポイントを設定してあげます。
今回の場合は次のような記述になります。

  "main": "dist/index.js",

併せて、package.jsonにパッケージの情報を記載します。
nameはリポジトリ名が自動で設定されますが、すでに(他の人に)使われている名前はpublish時にエラーとなるので、かぶらないように設定します。なお、かぶったときに表示されるエラーメッセージは意味不明なものなので、ハマると辛いです。たしか、セキュリティが問題、という感じのメッセージだったと思います。

  "name": "local-video-effector",
  "author": "xxxxxx <xxxx@xxx.co.jp> (https://qiita.com/wok)",
  "license": "Apache-2.0",

ここまでできたら、あとはpublishするまであと少し。
バージョン情報追加します。gitリポジトリと同期させておくと、問題が発生したときに解析が楽になりそうです。

npm version patch
git push origin <tag>

最後にpublishして完了です。

npm publish

成果物

https://www.npmjs.com/package/local-video-effector

お疲れ様でした。>自分

次回はここで作成したバーチャル背景のパッケージの使ったデモの開発を説明する予定です。

では、また。

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

Nodejs用のモデルライブラリを作ってみた【Express】

リポジトリ

https://github.com/kbc18a11/oreoreExpress
ライブラリ本体はoreoreExpress/database/AbstractModel.jsです。

解説用の構成や注意点

  • FW:Express
  • ディレクトリ構成:WebStormの新規Expressプロジェクトの状態にプロジェクト名/databaseプロジェクト名/modelというディレクトリを生成
  • 本記事はチーム開発のメンバーのマニュアルとしても書いていますので、ライブラリには直接関係ないことも書いています。

今回利用しているtestsテーブルは以下の構成になっています

id text created_at updated_at

使い方

リポジトリのものを利用するのであれば、以下のコマンドを実行してください。

npm install mysql2

ライブラリだけを使用するのであれば、mysql2date-utlisをインストールします

npm install mysql2
npm install date-utlis

そして以下のとおり、mysql2の設定ファイルを作ります。

プロジェクト名/database/mysqlConnection
const mysql = require('mysql2');
//MySQLの接続設定
const connection = mysql.createConnection({
    host: 'localhost',
    user: '',
    password: '',
    database: ''
}).promise();

module.exports = connection;

下記の通り、作りたいモデルクラスに設定します。
また以下のメソッドをオーバーライド、オーバーロードします。
abstractTABLE_NAME
abstractVALIDATIONRULES
update

メソッドabstractTABLE_NAMEは、連携しているテーブル名のです。
メソッドabstractVALIDATIONRULESは、バリエーションルールを記述する所です。
バリエーションのルールはvalidatorjsのものになります。
https://www.npmjs.com/package/validatorjs

プロジェクト名/model/Tests
const AbstractModel = require('./AbstractModel');
const connection = require('../database/mysqlConnection');
require('date-utils');

class Tests extends AbstractModel {
    constructor() {
        super();
    }

    /**
     * テーブル名
     * @override
     * @returns {string}
     */
    static get abstractTABLE_NAME() {
        return 'tests';
    }

    /**
     * バリデーションルール
     * @override
     * @return {Object}
     */
    static get abstractVALIDATIONRULES() {
        return {
            get:{
                rule: {
                    id: 'required|integer'
                },
                errorMessage: {
                    required: '必須項目です。',
                    integer: '数値で入力してください'
                }
            },
            //POSTリクエスト用
            post: {
                rule: {
                    text: 'required'
                },
                errorMessage: {
                    required: '必須項目です。',
                }
            },
            //PUTリクエスト用
            put: {
                rule: {
                    id: 'required|integer',
                    text: 'required'
                },
                errorMessage: {
                    required: '必須項目です。',
                    integer: '数値で入力してください'
                }
            },
            //DELETEリクエスト用
            delete: {
                rule: {
                    id: 'required|integer'
                },
                errorMessage: {
                    required: '必須項目です。',
                    integer: '数値で入力してください'
                }
            }
        };
    }

    /**
     * UPDATE文の準備を行って、親クラスのupdate()に実行をさせる
     * @param {Object} insertParam
     */
    static async update(insertParam) {
        //UPDATE文
        const sql = `UPDATE ${this.abstractTABLE_NAME} SET text = ?,updated_at = ? WHERE id = ?`;

        //create_at用の日付時間取得
        insertParam.updated_at = new Date().toFormat('YYYY-MM-DD HH:MI:SS');

        //SQLの実行
        await super.update(insertParam, sql);
    }
}

module.exports = Tests;

機能

基本的にライブラリの機能を使う場合は、Expressのルーティングファイルの無名関数にasyncを付与します。

プロジェクト名/routes/ルーティングファイル
router.get('/tests', async (req, res, next) => {

そして、ルーティングファイルで以下のライブラリを取り組みます。

プロジェクト名/routes/ルーティングファイル
const express = require('express');
const router = express.Router();
const validator = require('validatorjs');
const Tests = require('../model/Tests');

ライブラリでデータベースを操作するメソッドは、Promiseオブジェクトがかかわっているため、呼び出しの際には、awaitを付与します。

プロジェクト名/routes/ルーティングファイル
//レコードをすべて取得
await Test.all()
//引数idのカラム取得
await Test.find(id)
//引数idの存在確認を行う
await Test.existId(id)
//引数paramの値で新規登録を行う
await Test.insert(param)
//引数paramの値で更新を行う
await Test.update(param)
//引数idのレコードを削除する
await Test.delete(id)

all() レコードをすべて取得

プロジェクト名/routes/test.js
/**
 * @GET
 * testsのレコードをすべて取得
 */
router.get('/tests', async (req, res, next) => {

    try {
        //レコードをすべて取得
        const allRows = await Tests.all();

        //レコードを返す
        return res.send(allRows);
    } catch (error) {
        //レコードの取得失敗時
        console.log(error);
        res.status(500);
        return res.send({'error': 'サーバー側でエラーが発生しました'});
    }

});

http://localhost:3000/testsGETでアクセスします。

http
[
    {
        "id": 1,
        "text": "これはテストです",
        "created_at": "2020-06-18T16:50:45.000Z",
        "updated_at": null
    },
    {
        "id": 2,
        "text": "これはテストです",
        "created_at": "2020-06-18T16:50:50.000Z",
        "updated_at": null
    },
    {
        "id": 3,
        "text": "これはテストです",
        "created_at": "2020-06-18T16:50:51.000Z",
        "updated_at": null
    }
]

find(id) 引数idのカラム取得

プロジェクト名/routes/test.js
/**
 * @GET
 * 指定されたidのカラムを取得
 */
router.get('/tests/:id', async (req, res, next) => {
    //バリデーションの検証を受ける値
    const verificationValue = {
        id: req.params.id
    }
    //バリデーションの結果にエラーがあるかのチェック
    const validation = new validator(
        verificationValue,
        Tests.abstractVALIDATIONRULES.get.rule,
        Tests.abstractVALIDATIONRULES.get.errorMessage
    );
    if (validation.fails()) {
        //エラーを422で返す
        return res.status(422).send({errors: validation.errors.all()});
    }

    try {
        //レコードを取得
        const row = await Tests.find(verificationValue.id);
        //レコードを返す
        return res.send(row);
    } catch (error) {
        //レコードの取得失敗時
        console.log(error);
        res.status(500);
        return res.send({'error': 'サーバー側でエラーが発生しました'});
    }
})

http://localhost:3000/(testのidを指定)GETでアクセスします。

http
[
    {
        "id": 1,
        "text": "これはテストです",
        "created_at": "2020-06-18T16:50:45.000Z",
        "updated_at": null
    }
]

また、URIのidを指定する所が以下のとおり数値以外の場合は、エラーメッセージを返します。
http://localhost:3000/aaaa77

http
{
    "errors": {
        "id": [
            "数値で入力してください"
        ]
    }
}

insert() 引数paramの値で新規登録を行う

プロジェクト名/routes/test.js
/**
 * @POST
 * testsに新しいレコードを挿入
 */
router.post('/test', async (req, res, next) => {
    //バリデーションの検証を受ける値
    const verificationValue = {
        text: req.query.text
    }
    //バリデーションの結果にエラーがあるかのチェック
    const validation = new validator(
        verificationValue,
        Tests.abstractVALIDATIONRULES.post.rule,
        Tests.abstractVALIDATIONRULES.post.errorMessage
    );
    if (validation.fails()) {
        //エラーを422で返す
        return res.status(422).send({errors: validation.errors.all()});
    }

    try {
        //レコードの挿入開始
        await Tests.insert({text: req.query.text});
        return res.send({'insertResult': true});
    } catch (error) {
        //レコードの挿入失敗時
        console.log(error);
        return res.status(500).send({'insertResult': false});
    }
});

http://localhost:3000/test?text=テストやりたいPOSTでアクセスします。

http
[
    {
        "id": 1,
        "text": "これはテストです",
        "created_at": "2020-06-18T16:50:45.000Z",
        "updated_at": null
    }
]

リクエストのボディにtextがない場合は、エラーメッセージを返します。

http
{
    "errors": {
        "text": [
            "必須項目です。"
        ]
    }
}

update(param) 引数paramの値で更新を行う existId(id) 引数idの存在確認を行う

プロジェクト名/routes/test.js
/**
 * @PUT
 * レコードの更新
 */
router.put('/test/:id', async (req, res, next) => {
    //バリデーションの検証を受ける値
    const verificationValue = {
        id: req.params.id,
        text: req.query.text
    }
    //バリデーションの結果にエラーがあるかのチェック
    const validation = new validator(
        verificationValue,
        Tests.abstractVALIDATIONRULES.put.rule,
        Tests.abstractVALIDATIONRULES.put.errorMessage
    );
    if (validation.fails()) {
        //エラーを422で返す
        return res.status(422).send({errors: validation.errors.all()});
    }

    //idは存在しないか?
    if (!await Tests.existId(verificationValue.id)) {
        //エラーを422で返す
        return res.status(422).send({
            errors: {
                id: ['idが存在しません']
            }
        });
    }

    try {
        //レコードの更新開始
        await Tests.update(verificationValue);
        return res.send({'updateResult': true});
    } catch (error) {
        //レコードの更新失敗時
        console.log(error);
        return res.status(500).send({'updateResult': false});
    }

});

http://localhost:3000/test?text=テストやりたいPUTでアクセスします。

http
{
    "updateResult": true
}

URIのidを指定する所が以下のとおり数値以外やリクエストのボディにtextがない場合は、エラーメッセージを返します。
http://localhost:3000/test/aaa

http
{
    "errors": {
        "id": [
            "数値で入力してください"
        ],
        "text": [
            "必須項目です。"
        ]
    }
}

そして、find(id)の引数のidがレコードに存在しない場合、このようなエラーを返します

http
{
    "errors": {
        "id": [
            "idが存在しません"
        ]
    }
}

delete(id) 引数idのレコードを削除する

プロジェクト名/routes/test.js
/**
 * @DELETE
 * レコードの削除
 */
router.delete('/test/:id', async (req, res, next) => {
    //バリデーションの検証を受ける値
    const verificationValue = {
        id: req.params.id,
    }
    //バリデーションの結果にエラーがあるかのチェック
    const validation = new validator(
        verificationValue,
        Tests.abstractVALIDATIONRULES.delete.rule,
        Tests.abstractVALIDATIONRULES.delete.errorMessage
    );
    if (validation.fails()) {
        //エラーを422で返す
        return res.status(422).send({errors: validation.errors.all()});
    }

    //idは存在しないか?
    if (!await Tests.existId(verificationValue.id)) {
        //エラーを422で返す
        return res.status(422).send({
            errors: {
                id: ['idが存在しません']
            }
        });
    }

    try {
        //レコードの削除開始
        await Tests.delete(verificationValue.id);
        return res.send({'deleteResult': true});
    } catch (error) {
        //レコードの削除失敗時
        console.log(error);
        return res.status(500).send({'deleteResult': false});
    }


})

http://localhost:3000/(testのidを指定)DELETEでアクセスします。

http
[
    {
        "id": 1,
        "text": "これはテストです",
        "created_at": "2020-06-18T16:50:45.000Z",
        "updated_at": null
    }
]

URIのidを指定する所が以下のとおり数値以外の場合は、エラーメッセージを返します。
http://localhost:3000/aaaa77

http
{
    "errors": {
        "id": [
            "数値で入力してください"
        ]
    }
}

そして、find(id)の引数のidがレコードに存在しない場合、このようなエラーを返します

http
{
    "errors": {
        "id": [
            "idが存在しません"
        ]
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptワンライナー #1 任意長のランダムな文字列を作る

シェル芸ならぬワンライナー芸@JavaScriptで色々やってみようという試みです。


今回はテストデータを作るときに役に立つかもしれない「任意長のランダムな文字列を作る」というのをやってみます。
「ランダムな文字列を作る」というのをざっと調べるとよく見つかるコードにこんなのがあります。

Math.random().toString(36).slice(-8);
  • 乱数を発生させます。
  • それを36進数としてパースすれば0~9a~zで値が表現されます。
  • そのままでは先頭に0.というのが付いてしまうのでお尻から使えるところを拾っています。

都合の良いワンライナーですので、ここから発展させてみたいと思います。

0.を捨てる

Math.random().toString(36).split('.')[1];
  • ピリオドで分割します。
  • 後ろを拾います。

もっと長くしたい

このままでは得られる文字列の長さに限界があるので適当に繋げてみます。
先ずは4つぐらいかな...

[0,1,2,3].map(() => Math.random().toString(36).split('.')[1]).join('');
  • 先程の文字列を返すアロー関数を用意します。
  • 4つの要素を持つ適当な配列からmapメソッドで文字列を配列化します。
  • joinで連結して1つの文字列にします。

任意長にしたい

この方法で任意長にするためには配列の要素数を適切に設定する必要があります。
そのためには1回で得られる文字列の長さを把握しておきたいものです。
試しにこんな事をしてみますと...

for (let i = 0; i < 8; i++) console.log(Math.random().toString(36));

結果が

0.tss9ygi8yk7
0.cbl7c75unf
0.oi3ntqpk9f
0.3u3sulrw5ig
0.ph9gzmq637c
0.2pn7h79139
0.eqdmmpp0kyg
0.q1zz93b8csc

少なくとも10桁は使えそうです。
念の為、9桁使うという想定で進めます。(小心者)

欲しい長さを変数lenで与えたとして必要な要素数の配列を得るには...

[...Array(Math.ceil(len/9))]
  • 欲しい長さを一度に得られる文字数で割ります。
  • 切り上げで整数化します。
  • 便利なオマジナイで配列を生成します。

これを予め作っていたコードに適用してみます。

[...Array(Math.ceil(len/9))].map(() => Math.random().toString(36).split('.')[1]).join('').slice(-len);
  • 必要な長さを十分に満たす文字列を作ります。
  • 与えられた長さを切り出します。

長さ0以下を指定された場合の対策

このままでは長さに0を指定されると都合が悪いので、その場合は長さ0の文字列を返すことにします。

const randomString = (len) => len > 0 ? [...Array(Math.ceil(len/9))].map(() => Math.random().toString(36).split('.')[1]).join('').slice(-len) : '';

結果

出来上がりました。
動かしてみましょう。

const randomString = (len) => len > 0 ? [...Array(Math.ceil(len/9))].map(() => Math.random().toString(36).split('.')[1]).join('').slice(-len) : '';

for (let i = -1; i < 20; i++) console.log(randomString(i))

結果は


f
in
bso
2qz6
6ywe9
1hdq1r
a7nxjoa
yaamsm56
0mu9x06ft
5vogkds05g
e49k52cmq1m
l9lqg4npbdag
mtlygyh0dntn5
zkvbccv93eqbcg
0ywlv1k5wq9fj6q
k8amki0fo5de4r4h
vn05f4lic4ls3z3jd
knmcn6l5nex6xflnvk
c0h63fzghj9rfkq97qo

狙い通りです。
めでたしめでたし

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