20200516のNode.jsに関する記事は9件です。

Expressでcssとjavascriptを読み込む方法

1. ディレクトリ構成

Expressでプロジェクトを作成するとディレクトリは以下のような構成になります。

スクリーンショット 2020-05-16 22.33.17.png

詳細は今後に書くとして、cssとjavascriptをテンプレートエンジンへ適用させたい時は以下の手順を行ってください。

2. css作成

publicフォルダ直下のstylesheetsフォルダにcssファイルを作成してください。

3. javascript作成

publicフォルダ直下のjavascriptsフォルダにjsファイルを作成してください。
javascriptの場合はAPIの作成だったりルーティングの兼ね合いがありますので、
その点を考慮した実装としておいてください。

4.適用

viewsフォルダに作成した(プロジェクト作成時は1ファイルのみ作成されている)テンプレートエンジンのファイルに読み込みをさせていきます。

4-1. cssの場合

おなじみ、linkタグを使いましょう
e.g.
<link rel='stylesheet' href='/stylesheets/style.css' />
publicフォルダ直下のstylesheetsフォルダのstyle.cssファイルなので良いですね?

4-2. javascriptの場合

こちらもおなじみ、scriptタグを使いましょう
<script type="text/javascript" src='/javascripts/test.js'></script>
こちらも問題ないですね?

5. 最後に

まだ終わりません。
生htmlと違い、タグで読み込むだけでは表示ができません。
どうしたら良いかと言うと、ディレクトリ構成の中にある、app.jsファイルに1行書き込みます。
app.jsはルーティングを定義したりしていますので、全体のコントローラと思ってください。
そこで以下を追加します。
app.use(express.static('public'));
若しくは
app.use(express.static(path.join(__dirname, 'public')));
__dirnameは現在実行されているファイルの絶対パスが入っています。
これで表示ができると思います。

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

GitHubでindex.jsだけのライブラリーを使った学習法

はじめに

ある程度プログラミングできるようになったけど、レベルを上げるためにどうやって勉強したらいいのかわからない人向けの勉強法です。

何か困った時、プログラマーならまずライブラリーを探すかと思います。

探したライブラリーのGitHubにコードを見に行った時に⭐️がゼロだったらスルーしてしまうかもしれないですが それは非常にもったいないです。

具体的にどういう風にもったいないか説明します。

ある日のこと

.svgなファイルをrequireしたらどうなるのかなって思ってreuqireしたらエラーになりました。

$ node
Welcome to Node.js v12.7.0.
Type ".help" for more information.
> var sprites = require('./src/assets/svg-sprites.svg')
Thrown:
/Users/yukihirop/JavaScriptProjects/sample/src/assets/svg-sprites.svg:1
<svg width="0" height="0" class="hidden" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
^

SyntaxError: Unexpected token <
>

なんか読み込めるようにするライブラリーないか探したところ、 inline-svg-registerというものを見つけたのですが、?がゼロでした。(今は一個あります。色々勉強になったので私がつけました。)

image.png

「星ないのかー...」って思ったのですが、使い方はシンプルなので使ってみました。

$ yarn -D add inline-svg-register
$ node
Welcome to Node.js v12.7.0.
Type ".help" for more information.
> var unhook = require("inline-svg-register")
undefined
> var sprites = require("./src/assets/svg-sprites.svg")
undefined
> sprites
'<svg width="0" height="0" class="hidden" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <defs> <!-- yukihirop -->

エラーも起きずに文字列で出力されるようになりました。

知識の獲得

すぐに仕組みが気になりました。 100行程度の index.js しかないしコード見てみるかと思って見たら、ラッキーな事に babel-register のコードを参考にして作ったとあったので読んで見ました。

そこで以下の知識を得ました。

  • nodeの requirerequire.extensions という関数を使って、拡張子で判定してコンパイルできる。
    • だが、require.extensions非推奨
    • 非推奨どうしたらいいのかstackoverflowを見たのですが、ピシャッとくる回答がない。

するとすぐに次のような興味が湧きました。

現在のbabel-register では当然非推奨の対応をしてあるだろう。もしかしたら、babel-register での非推奨対応のPRを見つけたら勉強になるかもしれないぞと思って、babel-registerのコードのコードを見に行きました。

?実際にbabel-registerのコードが差し替えられた時のPRはこちら

すると現在では、require.extensions が使われておらず、pirates というものが使われている事を知りました。

pirates って何だろうって思ってコードを見に行ったらこれまたラッキーな事に index.js しかなく100行程度だったので読んでみました。

そこで以下の知識を得ました。

  • nodeのビルトインModuleのModule._extensions をうまく使って拡張子で判定してコンパイルできるようにしている事を知りました。
    • ビルトインモジュールに機能拡張しているライブラリーを見たのは初めてでした。新鮮でした。

使い方が簡単だったので、すぐに inline-svg-register のコードに還元してやろうと思いました。

知識の還元

得た知識はすぐに使ったほうが定着がいいと思ったのですぐに知識の還元を行いました。

  • inline-svg-registerpirates を使うようにしてPRを送りました。
  • stackoverflowに回答を書きました。こちら

まとめ

svgファイルをrequireで読み込んで見たい」って思っただけで思わぬ収穫がありました。

知識獲得フェーズ

  • babel-register というコードが何をするのか想像ができた。
  • require.extensions という関数で拡張子毎にコンパイルを変えれるが非推奨である。
  • require.extensions の代わりに pirates というライブラリーが使われているようである。

知識還元フェーズ

  • 知見を得るきっかけになった inline-svg-registerPRを送った。
  • require.extensions の代わりに何を使えばいいか困っている人に回答を書いた。
  • この記事を書いた。

いつもこんなラッキーが続くとは思いませんが、たまには index.js だけのライブラリーを読んでみるもの面白いかなって思います。

以上です。

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

初めての Azure Bot Service -multi-turn-prompt と Azure への Deploy -

はじめに

この記事はこちらの記事の続きとなってます。
(諸事情によりかなり時間が空きました)

流れ

  • index.js の中身を見てみる
  • 会話の分岐の実装(multi-turn-prompt の編集)
    • sample のまま Deploy する場合は skip!
  • Azure でのリソース作成
  • Azure への Deploy
  • Teams との連携

index.js の中身を見てみる

まずはindex.jsを見てみます。今回関係する箇所だけを抜粋してます。

index.js
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

const restify = require('restify');
const path = require('path');

--- 中略 ---

// Import our custom bot class that provides a turn handling function.
const { DialogBot } = require('./bots/dialogBot');
const { UserProfileDialog } = require('./dialogs/userProfileDialog');

--- 中略 ---

// Create the main dialog.
const dialog = new UserProfileDialog(userState);
const bot = new DialogBot(conversationState, userState, dialog);

--- 中略 ---

// Listen for incoming requests.
server.post('/api/messages', (req, res) => {
    adapter.processActivity(req, res, async (context) => {
        // Route the message to the bot's main handler.
        await bot.run(context);
    });
});

「あ、restify っていうフレームワーク?を使ってるんだなー」と思いながら、Import,Create, そして Listen の部分を見ていましょう。
ざっくり言うと Listen 部分の bot.run というところで dialog 起動してます。bot というインスタンスを起動することで会話が開始されるんですね。
そして、その bot は DialogBot というものの引数に userProfileDialogというものを入れることで生成しています。
ややこしいですが、結論としてはuserProfileDialog を編集したら、Dialog が編集できます!
DialogBot の方は特にいじらなくて大丈夫です!

ちなみに、もしも push 通知を実装したい場合にはこの Listen部分に別のエンドポイント(現在の/api/message の部分)を用意してあげることになります。詳細はまた機会があれば別記事でお話しますね。

会話の分岐

この章は Azure への Deploy に関しては一切関係ないので、飛ばしても問題ありません。

現状の sample のままだと少し困った(?)点があります。
それは、メッセージのやり取り数が一定になってしまっていることです。

例えば、Bot への入力を"時間" だけの1要素にしたい場合と、"時間"と"場所"のように2要素にしたい場合では、会話のやりとりが変わってくると思います。(もちろん、一度に2要素を入力するように実装することもできますが)

少し前回のおさらいにもなるかもしれませんが、sample の ./dialogs/userProfileDialogs.js を開くと、constructor で以下のように記述されています。

userProfileDialogs.js(25~45line)
 constructor(userState) {
        super('userProfileDialog');

        this.userProfile = userState.createProperty(USER_PROFILE);

        this.addDialog(new TextPrompt(NAME_PROMPT));
        this.addDialog(new ChoicePrompt(CHOICE_PROMPT));
        this.addDialog(new ConfirmPrompt(CONFIRM_PROMPT));
        this.addDialog(new NumberPrompt(NUMBER_PROMPT, this.agePromptValidator));

        this.addDialog(new WaterfallDialog(WATERFALL_DIALOG, [
            this.transportStep.bind(this),
            this.nameStep.bind(this),
            this.nameConfirmStep.bind(this),
            this.ageStep.bind(this),
            this.confirmStep.bind(this),
            this.summaryStep.bind(this)
        ]));

        this.initialDialogId = WATERFALL_DIALOG;
    }

addDialog というfunction が多々使われてますが、これはその名の通り、この Dialogに機能を追加しています。この js ファイルでは TextPromptやChoicePrmpt を使用するので、コンストラクタ内で書いてやる必要があります。

これは WATERFALL_DIALOG と呼ばれる形式で addDialog で登録した分だけ会話のやりとりを続けるというものです。add されているstep は Bot が送るメッセージだと思ってください(ただし、各 step で複数のメッセージを送ることができます。)

では、step の中身をいくつか見てみましょう。
confirmStep と summaryStep を見てみます。

userProfileDialogs.js(101~130line)
async confirmStep(step) {
        step.values.age = step.result;
        const msg = step.values.age === -1 ? 'No age given.' : `I have your age as ${ step.values.age }.`;

        // We can send messages to the user at any point in the WaterfallStep.
        await step.context.sendActivity(msg);

        // WaterfallStep always finishes with the end of the Waterfall or with another dialog, here it is a Prompt Dialog.
        return await step.prompt(CONFIRM_PROMPT, { prompt: 'Is this okay?' });
    }
async summaryStep(step) {
        if (step.result) {
            // Get the current profile object from user state.
            const userProfile = await this.userProfile.get(step.context, new UserProfile());

            userProfile.transport = step.values.transport;
            userProfile.name = step.values.name;
            userProfile.age = step.values.age;

            let msg = `I have your mode of transport as ${ userProfile.transport } and your name as ${ userProfile.name }.`;
            if (userProfile.age !== -1) {
                msg += ` And age as ${ userProfile.age }.`;
            }

            await step.context.sendActivity(msg);
        } else {
            await step.context.sendActivity('Thanks. Your profile will not be kept.');
        }

        // WaterfallStep always finishes with the end of the Waterfall or with another dialog, here it is the end.
        return await step.endDialog();
    }

色々と違いはありますが、return の部分に注目してみてみださい。
confirmStep では最後にchoicePrompt のメッセージをreturn しています。
一方で、summaryStep では step.endDialog() となっていますね。

これがWATERFALL_Dialogの終わり、つまりその一連の会話の流れの終了を意味しています。

ここまでの内容から、会話の長さを条件によって変えるには、この WATERFALL_DIALOG を条件ごとに用意してあげて、step.endDialog() までの step 数を調整してあげたら良いことが分かります。

これらを基に、実際に簡単な sample を用意しました。
こちらからコードは見れます
(nodeのmodule は外してます。)

最初に呼び出されるstartDialog.jsだけ少しみてみましょう。

startDialog.js
// 自作 Dialog の読み込み
const { haveDialog, HAVE_DIALOG } = require('./haveDialog');
const { nothingDialog, NOTHING_DIALOG } = require('./nothingDialog');

--- 中略 ---

class startDialog extends ComponentDialog {
    constructor(userState) {
        super('START_DIALOG');

        this.userProfile = userState.createProperty(USER_PROFILE);
        // 自作 Dialog の追加
        this.addDialog(new haveDialog());
        this.addDialog(new nothingDialog());

        this.addDialog(new ChoicePrompt(CHOICE_PROMPT));

        this.addDialog(new WaterfallDialog(WATERFALL_DIALOG, [
            this.transportStep.bind(this),
            this.summaryStep.bind(this)
        ]));

        this.initialDialogId = WATERFALL_DIALOG;
    }

    async transportStep(step) {
        // WaterfallStep always finishes with the end of the Waterfall or with another dialog; here it is a Prompt Dialog.
        // Running a prompt here means the next WaterfallStep will be run when the users response is received.
        return await step.prompt(CHOICE_PROMPT, {
            prompt: 'Do you have Nintendo Switch?',
            choices: ChoiceFactory.toChoices(['Yes!', 'No...'])
        });
    }


    // step.beginDialog で外部のDialog を呼び出している
    async summaryStep(step) {
        if(step.result.value.includes("Yes")){
            return await step.beginDialog(HAVE_DIALOG);
        }else if(step.result.value.includes("No")){
            return await step.beginDialog(NOTHING_DIALOG);
        }
    }

}

module.exports.startDialog = startDialog;
module.exports.START_DIALOG = START_DIALOG;


注目するとしたら、自作の Dialog をコンストラクタで追加していることや、step.DialogでDialog を開始しているところですかね。
なお、index.js なんかも変更されているので、気になる方は確認してみてください。

image.png

このように、長さがそれぞれ変わることが確認できました!

Azure でのリソース作成

プログラムの編集は一回ここで止めといて、実際に Azure でリソースを作成してみましょう。
Azure の portal 画面で検索バーに「bot」と入力してAzure Bot Service を選択してください。
*Azure のアカウント作成などに関しては割愛します。

image.png

Bot service を選んだのち、「追加」ボタンを押すと2種類の選択肢が現れます。

image.png

これらの違いは、Bot App をAzure App Service で作成するか、別のものを使用するかです。

image.png

今回は、App Service を使用する Web App Bot を使用します。
すると、色んなことを設定する画面になります。

image.png

いっぱいなんかありますね!
簡単に解説します!

用語 ざっくりした意味 備考
ボットハンドル Bot Service のリソース名 一意にすること(Azure のサービスだいたいはそう)
サブスクリプション 請求をどのサブスクにするか指定
リソースグループ どのリソースグループに入れるか リソースグループはAzure のリソースを管理するグループ
場所 Bot Service を置く場所 とりあえずは Japan East とかで良いかと
価格レベル Bot Service の料金プラン お試しであれば F0 (無料プラン)
Bot テンプレート 作成する際のランタイムなどを指定 今回は Node.js で Echo Bot を選ぶ
App Service プラン App Service の料金プラン お試しであれば F1: Free で作成する
Application Insight Azure のサービスの挙動を確認・監視するもの とりあえず ON にしてみると良いと思う

最悪、なにかあれば後で削除してしまいましょう。

リソースの作成が終わったら、リソースグループのページへ行ってみましょう。
Azure Portal の上部の検索で「リソースグループ」を検索してください。

以下の Bot Service, App Service , Application Insights のリソースがあるかと思います。
(私は既存の App Service プランを使用していたのですが、新規のプランを使用していたらここに App Service のプランもあるかもしれません。)

image.png

これで、リソースの作成は成功ですね。

Azure への Deploy

さて、これでプログラムの準備、Deploy するリソースが揃ったので、あとは Azure に Deploy するだけです。
Deploy する方法はいくつかあるのですが、今回は VS Code の拡張機能を使ってみます。
サイドバーの拡張機能を選び、「Azure」と検索すると出てくる Azure の 拡張機能を install してください。

image.png

これらの全部を install する必要はなさそうですが、App Service は install してください。
(もしかしたら、「Azure Account」も必要かもです。)

これで、拡張機能がサイドバーに追加されたと思います。
クリックしてみると、自分のサブスクリプションで生成している App Service のリソース一覧が出てきます。
(初回ログインが必要です)

その中から、Deploy したいリソースを選んで右クリック → Deploy Web App...
これでAzure に Deploy できます。

image.png

実際に Deploy できたか確認してみましょう。

Bot Service のリソースを選択して、Web チャットでテストを選んでください。

image.png

無事に動作しているようですね!

Teams との連携

最後に、Microsoft Teams でやりとり方法を示します。
先ほどテストした画面のサイドバーに「チャンネル」という項目があります。

image.png

ここから Teams を選び、画面の指示に従いボタンを押していくと、
Teams でのログインを要求されます。
ログインすると、Teams の画面に自分の作成した Bot が追加されているはずです。
(気が向いたらこの辺も何か画像とか追加するかもです。)

最後に

2回にわたり Azure Bot Service での Bot の作成を行いました。
今後、またの機会に Push 通知のやり方なんかも書こうかなと思います。
リモートワークが流行っている現状、Teams などの自分の使用しているコミュニケーションツール上で色んなことを完結させられる Bot はよいぞ!
それでは良い Bot Life を。

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

【AWS】Lambda+Amazon SNSでSMSを送信する

やりたいこと

AWS LambdakからAmazon SNSを利用して、任意の電話番号あてにSMSを送信します。

Amazon SNSって?

Amazon Simple Notification Service
任意の通知をいろんなプロトコルで送信できるよ。
事前にトピックっていう通知カテゴリみたいなの作って、そいつに紐づくサブスクリプションを作成しておくと、
トピックが叩かれたときに紐づいているサブスクリプションにメッセージが送信されるよ。
で認識合ってる?

プロトコル/トランスポート
HTTP/HTTPS
Email
Email-JSON
Amazon SQS
AWS Lambda
Platform application endpoint
SMS

公式の概要
Black Beltのスライド

コードとかより注意しておきたいこと

送信制限について

SNSでのSMS送信は送信制限が厳しめに設定されています。
利用料金の上限が1USD/月に設定されていて、東京リージョンにおいては12,3通/月しか送れません。
テスト送信だけで上限に達する可能性もあるので、必要があれば上限緩和申請を出しましょう。

宛先指定について

SNSでSMSを飛ばす分にはトピックを作成しなくても送信が可能です。

SMSじゃなくてE-Mailを送信したいという場合、
SNSでは事前にE-Mailの検証(実際にメールを送って、受信者が認証する)が必要です。
Lambdaでメールアドレスを指定してメール送信したいときにはAmazon SESを使いましょう。

構成

AWS LambdaとAmazon SNSのみです。
tempsnip.png

Lambda関数

事前にLabmda実行ロールにSNS実行ポリシーをアタッチしましょう。
とりあえずテストなのでAmazonSNSFullAccessをアタッチしておきます。

コード

ランタイムはNode.js 12.xです。

TestSendSMS.js
var AWS = require("aws-sdk");
var SNS = new AWS.SNS();

exports.handler = function(event, context, callback){

    // メッセージ情報を設定
    var param = {
        PhoneNumber: 'E.164形式の電話番号',
        Message: 'This is test message.',
        MessageAttributes: {
            'AWS.SNS.SMS.SMSType': {
                DataType: 'String',
                StringValue: 'Transactional' // 信頼性を重視する場合はこちらを
              //StringValue: 'Promotional'   // コストを重視する場合はこちらを
            },
            'AWS.SNS.SMS.SenderID': {
                DataType: 'String',
                StringValue: 'TEST'  // 端末に表示される送信元名
            }
        }
    };

    // メッセージを送信する
    SNS.publish(param, function(err, data){
        if(err){
            context.done(null, err);
        }
        context.done(null, data);
    });
};

多分これでいけます―

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

Box UI ElementsのContent OpenWithでG Suiteを開いてみた

概要

この記事は、Box UI ElementsのContent OpenWithでBox Editをつかってみた の続編です。

前の記事ではBoxEditを使うだけでしたが、GSuiteもつかってみました。

ここを参考にしています。
https://developer.box.com/guides/embed/ui-elements/open-with/

Githubはこちらをごらんください。

変えた部分

前回のコードをベースに、以下のsetup_update.jsをつくりました。
利用可能な統合をすべて登録しました。
G Suiteを利用するのに必要なexternal_app_user_idをApp Userにつけて更新しています。
以下のコードを実行すると、前回作成したAppUserでG Suiteのオンライン編集が利用できるようになります。

setup_update.js
const boxSDK = require("box-node-sdk");
const config = require("./config.json");
const axios = require("axios");

const USER_ID = "12771965844";

const main = async () => {
  const sdk = boxSDK.getPreconfiguredInstance(config);
  const saClient = sdk.getAppAuthClient("enterprise");

  // 前回作成したAppUserを更新
  // G Suiteを使いたい場合は、external_app_user_idに、G SuiteのE-mailアドレスを登録しておく必要がある。\
  const appUser = await saClient.users.update(USER_ID, {
    external_app_user_id: "yourname@example.com",
  });
  console.log(appUser); //戻り値ではexternal_app_user_idが見えない。

  // 現在利用可能なWebApp統合を一覧する
  const appIntegs = await saClient.get("/app_integrations");
  console.log("利用可能なWebApp統合一覧", appIntegs.body);

  /*
    {
      next_marker: null,
      entries: [
        { type: "app_integration", id: "10897" }, // Edit with G Suite <= 今回、これをつかいたい
        { type: "app_integration", id: "1338" }, // Edit with desktop apps <= これなんだろ・・
        { type: "app_integration", id: "13418" }, // Edit with desktop apps (SFC)  <= 13418がBox Editの統合
        { type: "app_integration", id: "3282" }, // Sign with Adobe Sign
      ],
      limit: 100,
    };
  */



  // clientオブジェクトから何故かpostの実行(client.post)がうまく機能しなかったので、axiosで実行する
  // Authorizationにつけるアクセストークンは、ServiceAccountのものを利用する必要がある。
  const saTokenInfo = await sdk.getEnterpriseAppAuthTokens();
  const saAxios = axios.create({
    baseURL: "https://api.box.com/2.0",
    headers: {
      Authorization: `Bearer ${saTokenInfo.accessToken}`,
    },
  });

  // 今回は、登録可能な統合は、全部登録する。
  for (const ai of appIntegs.body.entries) {

    // おまけ: 各統合の詳細情報を見たい場合
    // const info = await saClient.get(`/app_integrations/${ai.id}`);
    // console.log(info.body);

    await saAxios.post("/app_integration_assignments", {
      assignee: {
        type: "user",
        id: appUser.id,
      },
      app_integration: {
        type: "app_integration",
        id: ai.id,
      },
    });
  }
};

main();

試してみる

Google Docが選択できるようになりました。

google docs.png

Google Docsを選択すると、Google Docsがオンラインで開きます。

google docs2.png

同じように、別途Excel、Powerpointのファイルを登録してみると、Google Sheets、Google Slidesも利用できました。

googlesheets.png

GoogleSlides.png

ちなみに:統合の個別情報

ちなみに、統合の情報を表示してみると、こんな内容がでてきます。
一緒に登録してみたものの、1338は、なんなんだろうか・・・

コンソール
// 利用可能なWebApp統合一覧 
{
  next_marker: null,
  entries: [
    { type: 'app_integration', id: '10897' },
    { type: 'app_integration', id: '1338' },
    { type: 'app_integration', id: '13418' },
    { type: 'app_integration', id: '3282' }
  ],
  limit: 100
}

// /app_integrations/10897
{
  type: 'app_integration',
  id: '10897',
  app: { type: 'app', id: '336417' },
  name: 'Edit with G Suite',
  description: 'Securely manage your Google Docs, Sheets and Slides in Box',
  executable_item_types: [ 'FILE' ],
  restricted_extensions: [
    'docx',   'gdoc',
    'xlsx',   'gsheet',
    'pptx',   'gslides',
    'gslide'
  ],
  scoped_to: 'parent'
}

// /app_integrations/1338
{
  type: 'app_integration',
  id: '1338',
  app: { type: 'app', id: '8636' },
  name: 'Edit with desktop apps',
  description: 'Open and edit file with desktop applications',
  executable_item_types: [ 'FILE' ],
  restricted_extensions: [],
  scoped_to: 'parent'
}

// /app_integrations/13418
{
  type: 'app_integration',
  id: '13418',
  app: { type: 'app', id: '8636' },
  name: 'Edit with desktop apps (SFC)',
  description: 'Open and edit document in your desktop',
  executable_item_types: [ 'FILE' ],
  restricted_extensions: [],
  scoped_to: 'self'
}

// /app_integrations/3282
{
  type: 'app_integration',
  id: '3282',
  app: { type: 'app', id: '81713' },
  name: 'Sign with Adobe Sign',
  description: 'Send your document for signature to Adobe Sign',
  executable_item_types: [ 'FILE' ],
  restricted_extensions: [
    'pdf',  'doc',
    'docx', 'xls',
    'xlsx', 'ppt',
    'pptx'
  ],
  scoped_to: 'root'
}

よくわからなかったところ

G Suiteが動くのは確認できたのですが、以下の点がよくわからなかったです・・・

  • Adobe Signはどうやって使えばいいのか不明
  • 統合の一覧に出てくる、1338は、Appユーザーに登録してみたものの、特にメニューにも出てこないのでなににつかうものなのか不明
  • 13418は前回からつかっていて、BoxEditなことは間違いないんだけど、SFCってなんのことなんだろ。。。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Box UI ElementsのContent OpenWithでファイルの更新に反応してみた

この記事は、Box UI ElementsのContent OpenWithでBox Editをつかってみた の続編です。
ファイルの更新を検知して、画面をリフレッシュします。

Githubは、こちらをごらんください。

前回の内容と課題

前回、Box UI ElementsのContent OpenWithでBox Editをつかってみた という記事の中で、Box UI ElementsのOpenWithを使って、カスタム画面から変更するというのを試したのですが、課題として変更した内容が標準のBOXのように検知できていませんでした。

@daichiiiiiii さんから、コメントをいただいて、BoxのLong Polling APIを利用すれば良いとおしえていただきました。ありがとうございます!

更新ボタン表示させるには、BoxのLong Polling APIを使用して更新情報を監視する必要があります。
https://developer.box.com/guides/events/polling/

イベントを検知したら、User Event APIからいま表示しているファイルの更新イベントが見つかったらPopupを出す。みたいなロジックを組み込む必要があります。
残念ながらUI Elementsには上記ロジック内包されていません。。。残念

今回は、このロングポーリングを利用して、変更を検知する操作を試したので、内容を残します。

試したこと概要

以下のような実装を追加しました。

  • Herokuのアプリケーションから、Boxのロングポーリングにつなぎ、AppUserのイベントを購読するようにする。

  • Herokuアプリとブラウザを、Websocketでつなぎ、Boxのイベントに更新を見つけたらブラウザに通知する。

  • ブラウザは更新の通知を受けたら、Confirmのダイアログを開き、更新するか確認しYesの場合画面をリロードし、Noの場合は二度と更新を確認しない。

なお、app.jsはシンプル版と、Heorku対応版を作りました。
利用しているHerokuの特性として、レスポンスが55秒かえってこないと自動的に接続を殺し、エラーにするという機能に対処する必要があるためです。
変更の通知が要件として大切な場合、Herokuのようなインフラを利用しない方がいいと思います。

改良の余地は多分に残っていますが、基本的な動きは確認できたのでコードを共有します。

コードは前回の記事のものをベースに使っています。

具体的な変更

サーバー側の改造

wsモジュールを追加します。

yarn add ws

シンプル版

Boxのロングポーリングイベントを利用し、クライアント側ともWebsocketでつなぎます。
シンプル版です。Herokuだと55秒で動かなくなります。

app_ws.js
const express = require("express");
const http = require("http");
const boxSDK = require("box-node-sdk");
const config = require("./config.json");
const WebSocket = require("ws");

const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });

app.set("views", ".");
app.set("view engine", "ejs");

const USER_ID = "12771965844";
const FILE_ID = "665319803554";

const sdk = boxSDK.getPreconfiguredInstance(config);
const auClient = sdk.getAppAuthClient("user", USER_ID);

app.get("/", async (req, res) => {
  // トークンをダウンスコープする。
  // ここでは、OpenWithで必要なものと、Previewで必要なものを両方スコープにいれてトークンをダウンスコープする
  const downToken = await auClient.exchangeToken(
    [
      "item_execute_integration",
      "item_readwrite",
      "item_preview",
      "root_readwrite",
    ],
    `https://api.box.com/2.0/folders/0`
  );

  // テンプレートにパラメータを渡して、HTMLを返す
  res.render("index", {
    fileId: FILE_ID,
    token: downToken.accessToken,
  });
});

// Boxで、ファイルが変更されたことを、ロングポーリングを使って検知し、フロントエンドに通知する。
// 必ずしもそうする必要は無いが、ここではブラウザとHeroku間をWebsocketでつないでいる。
// Websocketの中で、Heroku ⇔ Box APIを、Long Pollingでつなぐ。
// ブラウザ ⇔ (Websocket)⇔ Heroku App ⇔ (Long Polling) ⇔ BOX API
wss.on("connection", async (ws) => {
  // ブラウザとHerokuの間のWebsocketのハンドリング

  // ロングポーリングはAppUserのトークンで行う必要がある
  const stream = await auClient.events.getEventStream();

  // ロングポーリングからデータを受け取ったときの処理
  stream.on("data", (event) => {
    // 更新されたことを、event_typeで判定(プレビューの場合などもイベントが来る)
    if (event.event_type && event.event_type === "ITEM_UPLOAD") {
      // クライアントに更新を通知。ここでは簡易的にupdatedという文字列を返している。
      wss.clients.forEach((client) => {
        client.send("updated");
      });
    }
  });
});

server.listen(process.env.PORT || 3000, () => {
  console.log(`express started on port ${server.address().port}`);
});

Heroku対応版

こちらは、Herokuで利用する場合のため、45秒でつなぎ直しとブラウザへのPing打ちをしています。

app_ws_heroku.js
const express = require("express");
const http = require("http");
const boxSDK = require("box-node-sdk");
const config = require("./config.json");
const WebSocket = require("ws");

const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });

app.set("views", ".");
app.set("view engine", "ejs");

const USER_ID = "12771965844";
const FILE_ID = "665319803554";

const sdk = boxSDK.getPreconfiguredInstance(config);
const auClient = sdk.getAppAuthClient("user", USER_ID);

app.get("/", async (req, res) => {
  // トークンをダウンスコープする。
  // ここでは、OpenWithで必要なものと、Previewで必要なものを両方スコープにいれてトークンをダウンスコープする
  const downToken = await auClient.exchangeToken(
    [
      "item_execute_integration",
      "item_readwrite",
      "item_preview",
      "root_readwrite",
    ],
    `https://api.box.com/2.0/folders/0`
  );

  // テンプレートにパラメータを渡して、HTMLを返す
  res.render("index", {
    fileId: FILE_ID,
    token: downToken.accessToken,
  });
});

// Boxで、ファイルが変更されたことを、ロングポーリングを使って検知し、フロントエンドに通知する。
// 必ずしもそうする必要は無いが、ブラウザとHeroku間をWebsocketでつなぐ。
// Websocketの中で、Heroku ⇔ Box APIを、Long Pollingでつなぐ。
// 関係は、以下のようなイメージ
// ブラウザ <= (Websocket) => Heroku App <=(Long Polling)=> BOX API
wss.on("connection", async (ws) => {
  // ブラウザとHerokuの間のWebsocketのハンドリング
  // Herokuではロングポーリングは55秒で強制的に止められるので、setTimeoutをつかって45秒毎につなぎ直している。
  // 単純なポーティングよりマシだが、本当に更新検知が必要な場合は、インフラにHeorkuを使わないほうがいいと思う。
  let pollingTimer;
  async function longPolling() {
    // Boxのロングポーリングイベント監視
    // ロングポーリングはAppUserのトークンで行う必要がある
    const stream = await auClient.events.getEventStream();

    // ロングポーリングからデータを受け取ったときの処理
    stream.on("data", (event) => {
      // 更新されたことを、event_typeで判定(プレビューの場合などもイベントが来る)
      if (event.event_type && event.event_type === "ITEM_UPLOAD") {
        // ここで本当ならfileId等もチェックしたほうがいいかも
        // クライアントに更新を通知。ここでは簡易的にupdatedという文字列を返している。
        wss.clients.forEach((client) => {
          client.send("updated");
        });
      }
    });

    // 45秒たったら、もう一度ロングポーリングをつなぎ直す
    pollingTimer = setTimeout(longPolling, 45000);
  }
  // setTimeoutは初回は即座に実行されないので、初回分だけ実行しておく。
  longPolling();

  // ブラウザとHerokuの間のWSも、Herokuは55秒でシャットダウンしてしまうので、pingだけ飛ばしておく。
  // Heorkuのようなインフラを使わないのであれば不要
  const pingTimer = setInterval(() => {
    wss.clients.forEach((client) => {
      client.send("ping");
    });
  }, 45000);

  // ブラウザが閉じられたとき、無駄な再接続を止める
  ws.on("close", () => {
    // ブラウザへのpingを止める
    clearInterval(pingTimer);
    // Boxへのロングポーリングを止める
    clearTimeout(pollingTimer);
  });
});

server.listen(process.env.PORT || 3000, () => {
  console.log(`express started on port ${server.address().port}`);
});

クライアント側の改造

Heroku側とWebsocketでつなぎ、更新があったとき反応するようにします。
変更があった場合、簡易的にConfirmでリフレッシュの要否確認し、previewオブジェクトを作り直してpreview部分のみ再描画しています。

index_ws.ejs
<!DOCTYPE html>
<html lang="en-US">
<head>
    <meta charset="utf-8" />
    <title>Sample</title>

    <link href="https://cdn01.boxcdn.net/platform/elements/11.0.2/ja-JP/openwith.css" rel="stylesheet" type="text/css"></link>
    <link href="https://cdn01.boxcdn.net/platform/preview/2.34.0/ja-JP/preview.css" rel="stylesheet" type="text/css"></link>

    <script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=es6,Intl"></script>
    <script src="https://cdn01.boxcdn.net/polyfills/core-js/2.5.3/core.min.js"></script>

    <script src="https://cdn01.boxcdn.net/platform/elements/11.0.2/ja-JP/openwith.js"></script>
    <script src="https://cdn01.boxcdn.net/platform/preview/2.34.0/ja-JP/preview.js"></script>

    <style>
        .openwith-container {
            margin-left: 250px;
        }
        .preview-container {
            height: 800px;
            width: 100%;
        }
    </style>

</head>
<body>
    <h3>File Id: <%= fileId %></h3>

    <div id="container">
        <div class="openwith-container"></div>
        <div class="preview-container"></div>
    </div>

    <script>
        // app.jsから渡されたパラメータ
        const fileId = "<%= fileId %>"
        const token = "<%= token %>"

        const openWith = new Box.ContentOpenWith();
        openWith.show(fileId, token,  { container: ".openwith-container"})

        let preview = new Box.Preview();
        preview.show(fileId, token, { container: ".preview-container", autoFocus: false });

        // ファイルの更新に反応するため、Websocketを利用する
        const host = location.origin.replace(/^http/, "ws"); // -> wss://xxx.herokuapp.com/
        const ws = new WebSocket(host);

        let confirmed = null
        ws.onmessage = event => {
            // Herokuからメッセージが来たとき。
            // updatedであれば、簡易的にconfirmウィンドウを出す。
            // OKを押したら、簡易的にリロードして再読み込みし、変更を反映。
            // OK, Cancelの確認は一度だけ聞く。
            if(event.data === "updated" && confirmed !== false) {
                if(confirmed === null) {
                    confirmed = confirm("refresh?")
                }
                if(confirmed) {
                    // previewだけを描画し直す。
                    // preview.hide() // hide()はしてもしなくてもすぐには見た目変わらず。
                    // preview.show(..) // 単純にshowを再度呼んだだけでは画面が更新されない。
                    preview = new Box.Preview(); // preivewオブジェクトは再利用できないっぽい。再度newする必要があるみたい。
                    // 毎回プレビューの位置までスクロールされたくないのでautoFocus:false
                    preview.show(fileId, token, { container: ".preview-container", autoFocus: false });
                }
            }
        }
    </script>
</body>
</html>


試してみる

「開く」ボタンを押して、ローカルでWordを立ち上げ、保存をして数秒でConfirmが表示されるのが確認できました。Yesを押すとリロードしてPreviewに最新のデータが反映されました。

スクリーンショット 2020-05-16 13.33.51.png

まとめ

@daichiiiiiii さんから教えていただいた方法でうまく行きました。

Herokuの制限のため再接続している点が微妙です。
本気で更新の通知を実装する場合、この点で制限が無いインフラを選定すべきだと思います。

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

Denoのマニュアルで気になった所を試してみる

Deno

公式ページ

最近よく目にするので、何はともあれ触ってみようかという事で
マニュアルを元にザッと気になる所を試して見たいと思います。

:computer:環境構築

Denoの公式Docker(?)を見つけたのでそちらで環境を構築して見たいと思います。
https://hub.docker.com/r/hayd/deno

ワンランナーでさくっと試してみます。

$ docker run --rm -it hayd/alpine-deno run https://deno.land/std/examples/welcome.ts
Welcome to Deno ?

:thumbsup:

:pencil: 色々試してみる

簡単なサンプル

公式にも載っていた以下サンプルをローカルに server.ts として作成し、起動させてみます。

import { serve } from "https://deno.land/std@0.50.0/http/server.ts";
const s = serve({ port: 8000 });
console.log("http://localhost:8000/");
for await (const req of s) {
  req.respond({ body: "Hello World\n" });
}
$ docker run --rm -it --init -p 8000:8000 -v $PWD:/app hayd/alpine-deno run /app/server.ts
...
error: Uncaught PermissionDenied: network access to "0.0.0.0:8000", run again with the --allow-net flag

Premissionが無いと怒られました。Denoはデフォルトでファイル、ネットワーク、環境変数等のアクセスが
許可されてないので、--allow-net を付けて再度実行してみます。

$ docker run --rm -it --init -p 8000:8000 -v $PWD:/app hayd/alpine-deno run --allow-net /app/server.ts

今度は実行でき http://localhost:8000/ にアクセスするとちゃんと Hello World が返ってきました :sparkles:

ファイル読み込み

こちら を試して見たいと思います。
cat.ts として以下を作成します。

const filenames = Deno.args;
for (const filename of filenames) {
  const file = await Deno.open(filename);
  await Deno.copy(file, Deno.stdout);
  file.close();
}

読み込み用のファイル sample.txt を以下の内容で作成。

Deno is a simple, modern and secure runtime for JavaScript and TypeScript that uses V8 and is built in Rust.

デフォルトだとファイル読み込みもPremissionが無いので --allow-read を付けて実行します。

$ docker run --rm -it --init -v $PWD:/app hayd/alpine-deno run --allow-read /app/cat.ts /app/sample.txt
> Deno is a simple, modern and secure runtime for JavaScript and TypeScript that uses V8 and is built in Rust.

ちゃんと読み込めてそうです :sparkles:

dependency inspectorformatter

Denoには dependency inspectorformatter がビルドインされているらしいので試してみます。

  • dependency inspector

    $ docker run --rm -it --init hayd/alpine-deno info
    DENO_DIR location: "/deno-dir/"
    Remote modules cache: "/deno-dir/deps"
    TypeScript compiler cache: "/deno-dir/gen"
    

    remoteから取得したmoduleとTypescriptコンパイル時のキャッシュ場所を教えてくれてます :sparkles:

  • formatter

    $ docker run --rm -it --init -v $PWD:/app hayd/alpine-deno fmt
    

テスト

こちらもビルドインでtest runnerが入っているらしいです。至れり尽くせり :sparkles:
https://deno.land/manual/testing

simple.test.ts
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";
Deno.test("simple", () => {
  const x = 1 + 2;
  assertEquals(x, 3);
});

実行!

$ docker run --rm -it --init -v $PWD:/app hayd/alpine-deno test /app/simple.test.ts
running 1 tests
test simple ... ok (3ms)

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (4ms)

Debugger

https://deno.land/manual/tools/debugger
V8 Inspector Protocolをサポートしている

先ほどのファイル読み込みのサンプルをDebugして見たいと思います。
--inspect-brk を付けて最初の1行目でブレークする様にします。

$ docker run --rm -it --init -p 9229:9229 -v $PWD:/app hayd/alpine-deno run --inspect-brk=0.0.0.0:9229 --allow-read /app/cat.ts /app/sample.txt
Debugger listening on ws://127.0.0.1:9229/ws/xxxxx

Chromeで chrome://inspect にアクセスして

↑のinspectをクリックすると

デバッグできました :sparkles:

また、試せては無いですが、VSCode上でもデバッグ可能らしいです。
https://deno.land/manual/tools/debugger#vscode

VSCodeの拡張機能

Deno - Visual Studio Marketplace

:bomb: バッドノウハウ

  • Debugger使用時にchromeでinspectが表示されない :warning:
    • docker使用する場合だと --inspect-brk だけではダメで --inspect-brk=0.0.0.0:9229 と指定しないといけない

:sparkles: 感想

  • ビルドインで色々入っているので便利
  • 外部パッケージをURLでインポートできるのはサクッと試せるので便利
  • いきなりNodeのプロジェクトをDenoに置き換えるのは厳しそうだが、新規のプロジェクトや単独の小さなモジュールとかで使ってみたい

:link: 参考になったURL

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

Firebase Local Emulator SuiteのFirestoreに外部のスクリプトからアクセスしてモックデータを突っ込む

こちらに非常に良い記事があります。まだ読んでおられない方は是非!

環境変数のFIRESTORE_EMULATOR_HOSTに適切な値を設定することで外部のスクリプトからエミュレーターにアクセスできます。(他言語のSDKでも同様だそうです)

import { Firestore } from '@google-cloud/firestore';

Object.assign(process.env, {
  GCLOUD_PROJECT: '[プロジェクトID]', 
  FIRESTORE_EMULATOR_HOST: '0.0.0.0:[Firestoreのポート番号]'
});

const firestore = new Firestore();

(async () => {
  // admin権限が必要な操作も可能
  console.log(await firestore.listCollections());
})();

なお、GCLOUD_PROJECTに関しては環境変数を使わなくても適当なメソッドで代替できます。実際に存在するプロジェクトIDである必要はないことに注意です!Firestoreのエミュレータは並列実行しても1インスタンスなので、プロジェクトIDが被ると共通の領域が使用されるようになります。

これに気づくまでに数時間を要しましたが、冒頭にも書いた通り、既に偉大な先人の方が記事を書かれていたようです。こちらの記事は十分に読み込んだつもりになっていましたがまだまだ甘かった・・・。

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

SQL(データの追加、更新、削除)とNode.js基本

○データの追加

qiita.js
insert into テーブル名 カラム名
values (カラム名)

↑テーブルにレコードを挿入したいときはinsertを使う。

○データの更新

qiita.js
update テーブル名
set カラム名='新しい値', カラム名='新しい値'
where 条件

↑テーブルのレコードを更新したいときはupdateを使う。
※whereで更新するレコードを指定しないとカラム内の全データが更新される。
※update実行後はデータを戻すことができない

○データの削除

qiita.js
delete from テーブル名
where 条件

↑テーブルからレコードを削除したいときはupdateを使う。
※whereで削除するレコードを指定しないとテーブル内の全レコードが削除されてしまう。

○Node.jsとは?

・Node.jsは本来クライアント側の言語であるJavaScriptをサーバーサイドで動かすための仕組み。

expressとは?

・ExpressはNode.jsでWebアプリの開発をするためのフレームワーク。

ページの表示の仕組み

qiita.js
app.get ('URL', (req, res) => {
  res.render('ファイル名')
});

↑URLにリクエストが来た時に、ファイル名を表示することができる。

EJSとは?

・EJSとは、HTMLとJavaScriptのコード両方を記述できるNode.jsのパッケージ

・JavaScriptのコードを記述するには、<% %>または<%= %>で囲む。
<% %>で囲んだ場合はブラウザに何も表示されないので、変数の定義などに用いる。変数の値などをブラウザに表示したい場合は<%= %>を用いる。

MySQLとは?

・MySQLとは、データベースを管理するツールの一つ。

qiita.js
const mysql = require('mysql')

↑上記のようなコードでmysqlパッケージを読み込むことができる。

クエリの実行

qiita.js
connection.query('クエリ', クエリ実行後の処理)

↑Node.jsからデータベースに対してクエリを実行することができる。
・クエリ実行後の処理は2つの引数を取ることができる。
第1引数のerrorにはクエリが失敗したときのエラー情報が、第2引数のresultsにはクエリの実行結果が入る。

getとpost

・画面を表示したいときはget、データベースを変更したいときはpost

リダイレクトとは

qiita.js
res.redirect('URL')

↑サーバーは「次はこのURLにリクエストしてね」というレスポンスを返すことができる。このレスポンスを受け取ったブラウザは指定されたURLに自動的にリクエストする。このような別のURLに再度リクエストさせる仕組みを「リダイレクト」と言う。

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