bpeldi2oerkd8の開発日誌

とある大学院生の成長の記録。

Herokuへのデプロイ - 自作サービスづくり11

今回もWeb開発に関する記事です。
前回はAPIにJWT認証を追加しました。
developer-bpeldi2oerkd8.hatenablog.com

今回は今までの変更内容を本番環境にデプロイしたいと思います。
本番環境としては前のデプロイでも用いたHerokuを使います。
developer-bpeldi2oerkd8.hatenablog.com

Herokuへのデプロイ

手順については、Heroku CLIのインストール後、以下のページに従ってデプロイするのみです。
devcenter.heroku.com
これを2つのシステムについて行いました。
どちらも問題なく動作しました。

完成したもの

Lattendance (Webサイト)

Lattendance-bot (ボットシステム(Slack用))

READMEの編集

実行手順をわかりやすくするため、それぞれのシステムのREADMEを編集しました。
実行手順のみでなく、使用した技術・機能・デモ動画についても掲載しています。

github.com

github.com

感想

今回でN予備校の教材をベースにしたシステムに、独自機能であるJWT認証付きのAPIを用いたbotによる出欠更新機能を追加することができました。
これでひとまず自己学習を終え、インターンへの参加を目指したいと思います。

今回までで以下のことが学べ、Web開発の基本となる知識が吸収できました。

今後はさらに次のような技術についても勉強していきたいと思っています。

JWT認証を導入したAPI実装 - 自作サービスづくり10

今回もWeb開発に関する記事です。
前回はデザイン改善とSlackとの連携機能の実装を行いました。
developer-bpeldi2oerkd8.hatenablog.com

ここでは、ロゴの作成とSlack連携登録・編集機能を作ったため、
今回はいよいよJWT認証を導入したAPIの実装に移ります。

構成

構成は以下のようになっています。
f:id:bpeldi2oerkd8:20210710205316j:plain

手順としては、

  1. /api/v1/login にアクセスし、lattendanceにJWTをリクエス
  2. lattendance上でJWTを発行し、JSONでJWTを返す
  3. /api/v1/schedulesにアクセス時(以前実装したAPIの利用時)にヘッダーに発行したJWTを追加しリクエス
  4. 送られてきたJWTをもとに認証し、OKの場合のみ結果を返す(NGの時はエラー内容を返す)

順番に実装していきます。

JWTの発行

JWTの発行・認証時に jsonwebtoken を用います。
ドキュメントを見ると、JWTを発行するために jwt.sign() 、JWTを検証する ためにjwt.verify()を用いることが書いてあります。

これに従って、まずJWTの発行を実装します。
まず、JWTを使用できるようにするため、以下のようにapp.jsを変更します。

app.use(express.urlencoded({ extended: true }));

また、APIのログイン部分は以下の通りです。
事前に発行したチャンネルトークンとチャンネルIDをPOSTし、JWTを返します。

'use strict';
const express = require('express');
const router = express.Router();
const jwt = require('jsonwebtoken');
const Room = require('../../models/room');
//APIのシークレットキー
const serverAPISecret = process.env.SERVER_API_SECRET || require('../../secret_info/server_api_info');

router.post('/', (req, res) => {
  //roomIDとroomTokenを取得
  const roomId = req.body.roomId;
  const roomToken = req.body.roomToken;

  Room.findByPk(roomId)
  .then((room) => {
    if(room && room.roomToken === roomToken) {
      //JWTを生成
      const token = jwt.sign({roomId: roomId}, serverAPISecret, {expiresIn: '1h'});
      res.json({
        status: 'OK',
        data: {
          token: token
        }
      });
    }
    else {
      res.json({
        status: 'NG',
        error: {
          messages: ['認証エラー']
        }
      });
    }
  });
});

module.exports = router;

JWTの認証

今度はJWTの認証を入れます。
認証を行う関数をverifyToken()とし、ミドルウェアとして引数に入れます。
verifyTokenの実装はこちらです。

'use strict';
const jwt = require('jsonwebtoken');

//APIのシークレットキー
const serverAPISecret = process.env.SERVER_API_SECRET || require('../../secret_info/server_api_info');

function verifyToken(req, res, next) {
  const authHeader = req.headers["authorization"];

  //HeaderにAuthorizationがあるかチェック
  if (authHeader) {
    //Bearerのチェック
    if (authHeader.split(" ")[0] === "Bearer") {
      try {
        const decoded = jwt.verify(authHeader.split(" ")[1], serverAPISecret);
        const roomId = req.params.roomId;

        //roomIdの検証
        if (decoded.roomId === roomId) {
          next();
        }
        else {
          res.json({
            status: 'NG',
            error: {
              messages: ['検証エラー']
            }
          });
        }
      } catch (e) {
        res.json({
          status: 'NG',
          error: {
            messages: ['Tokenエラー']
          }
        });
      }
    }
    else {
      res.json({
        status: 'NG',
        error: {
          messages: ['ヘッダー形式エラー']
        }
      });
    }
  }
  else {
    res.json({
      status: 'NG',
      error: {
        messages: ['ヘッダーエラー']
      }
    });
  }
}

module.exports = verifyToken;

これを以下のようにミドルウェアとして挟むことで認証を入れています。

router.post('/:roomId/users/:slackId/dates/:dateString',
  verifyToken,
  (req, res, next) => {

APIのテスト

APIが正しく実装されているかテストします。
今回はJestではなく、VS Code上で手軽にREST APIのテストができるREST Clientを使います。
使い方はこちらのページが参考になります。
qiita.com

では、実際にリクエストを書いていきます。

# JWT発行テスト
POST http://localhost:8000/api/v1/login
Content-Type: application/json

{
  "roomId": "TEST0001",
  "roomToken": "事前に発行したチャンネルトークン"
}

###
# 認証がない場合の出欠確認テスト(認証エラー)
GET http://localhost:8000/api/v1/schedules/TEST0001/users/{ユーザーID}/dates/2021-07-02

###
# 認証がある場合の出欠確認テスト
GET http://localhost:8000/api/v1/schedules/TEST0001/users/{ユーザーID}/dates/2021-07-02
Authorization: Bearer {先ほど返ってきたJWT}

###
# 認証がある場合の出欠確認テスト
GET http://localhost:8000/api/v1/schedules/TEST0001/users/{ユーザーID}/dates/2021-07-03
Authorization: Bearer {先ほど返ってきたJWT}

###
# 認証がある場合の出欠更新テスト
POST http://localhost:8000/api/v1/schedules/TEST0001/users/{ユーザーID}/dates/2021-07-02
Authorization: Bearer {先ほど返ってきたJWT}
Content-Type: application/json

{
  "availability": 2
}

正しいJSONが返ってきました。

botの実装の変更と動作確認

APIにJWT認証が追加されたため、botの実装を変更しました。
まず、/api/v1/loginにアクセスし、JWTが返ってきてからヘッダーに追加しリクエストしています。
github.com

動作確認をします。
正しく結果が返ってくることが確認できました。
f:id:bpeldi2oerkd8:20210712234141j:plain


以上でJWT認証付きのAPIの実装が完了しました。
次回は、Herokuへのデプロイをしたいと思います。

デザイン改善とSlackとの連携機能の実装 - 自作サービスづくり9

今回もWeb開発に関する記事です。
前回は認証付きAPIを作る前準備を行いました。
developer-bpeldi2oerkd8.hatenablog.com

ここでは設計を行ったため、今回はAPIの実装の前にSlackとの連携に必要な情報を登録する機能の実装をします。

デザイン改善

その前にデザインの改善をします。
今のままでは味気ないので、トップページやマイページのデザインの改善に取り組みます。

まず、このサービスのロゴを考えました。
そのあと、自分の中で良いと思ったデザインを画像にしました。
画像にするために使ったものは、Inkscapeです。
点を結んでいくことで簡単に作成できるためおすすめです。

完成したロゴはこちらです。
f:id:bpeldi2oerkd8:20210711220023p:plain
サービス名であるLattendanceの頭文字Lと出席を意味するチェックマークを組み合わせたものです。

加えて、トップページはスライド式でサービスの名前と特徴が交互に入れ替わるものにしました。
これには、Bootstrapの「Carousel」を用いています。
f:id:bpeldi2oerkd8:20210711220517j:plain
f:id:bpeldi2oerkd8:20210711220539j:plain

つまづいた点は、文字を好きな位置に配置する方法です。
最初は「Carousel」のcaptionを用いる方法を考えたのですがうまくいかず、
「Carousel」に「Card」を入れることができることに気付いたため、最終的にはCardのImage overlaysを利用しました。

Slackとの連携登録機能の実装

Slackとの連携に必要なチャンネルIDとチャンネルのトークンを予定に紐づけます。
Roomテーブルを作成し、そこにroomIdカラムとroomTokenカラムを追加します。
さらに、ScheduleテーブルにroomIdカラムを追加し、roomIdを外部キーを設定します。

チャンネルIDを入力し、トークンを発行するように実装します。

router.post('/:scheduleId/new2', authenticationEnsurer, csrfProtection, (req, res, next) => {
  const scheduleId = req.body.scheduleId;
  const roomId = req.body.roomId.trim();

  //scheduleIdに紐づくscheduleにroomIdが登録されていないかチェック
  const noRoomId = (scheduleId) => {
    return new Promise((resolve, reject) => {
      Schedule.findByPk(scheduleId)
      .then((schedule) => {
        if(schedule)
          console.log('exist');
        else
          console.log('not exist');
        if(schedule && schedule.roomId === null) {
          resolve(schedule);
        }
        else {
          reject('This schedule is already linked to room ID.');
        }
      });
    });
  };

  //同じRoomIDが登録されていないかチェック
  const checkRoomId = (roomId) => {
    return new Promise((resolve, reject) => {
      Room.findByPk(roomId)
      .then((room) => {
        if(room) {
          reject('Room ID is registered.');
        }
        else {
          resolve();
        }
      });
    });
  };

  Promise.all([noRoomId(scheduleId), checkRoomId(roomId)])
  .then((result) => {
    let schedule = result[0];
    if(isMine(req, schedule)) {
      //roomTokenの生成
      const N = 40;
      const roomToken = crypto.randomBytes(N).toString('base64');

      //チャンネル情報の登録
      Room.create({
        roomId: roomId,
        roomToken: roomToken
      })
      .then(() => {
        //roomIdの登録
        schedule.roomId = roomId;
        schedule.save();

        res.render('slack-channel-linker-new2', { 
          schedule: schedule,
          roomToken: roomToken, 
          csrfToken: req.csrfToken()
        });
      });
    }
    else {
      const err = new Error('指定された予定がない、または、予定する権限がありません');
      err.status = 404;
      next(err);
    }
  })
  .catch((err) => {
    console.log(err);
    res.redirect(`/schedules/slack-channel-linker/${scheduleId}/new?alert=1`);
  });
});

細かい実装は以下の通りです。
github.com

実装後はこのようになります。
f:id:bpeldi2oerkd8:20210711224841j:plain
f:id:bpeldi2oerkd8:20210711224916j:plain

つまづいた点は、トークンを表示するページを無関係の方に表示させない方法です。
最終的には、isMineという自身が作った予定かを確認する関数に加えて、
1ページ目からpostした情報をもとに処理し、そのページのままレンダリングをすることで解決しました。
フォームにはCSRF対策も施してあるため、無関係のユーザーがトークンを発行するページにアクセスできないと思われます。

Slackとの連携変更機能の実装

次に、Slackとの連携設定の変更機能を実装します。
必要な機能としては、以下の3つです。

  • Slack連携の解除
  • チャンネルIDの変更
  • トークンの再発行

この機能それぞれについて実装しました。
細かい実装については以下の通りです。
実装は登録の際に使ったものを再利用してます。
github.com

また、予定の削除機能についてもRoomテーブルを加えたことで削除するデータが増えたため、変更しています。
github.com

実装後はこのような画面になります。
f:id:bpeldi2oerkd8:20210711230857j:plain
ここで変更したい項目のボタンを押すと、変更されます。


以上で、Lattendance上にSlack連携に必要な情報を登録することができるようになりました。
次回は、JWT認証を導入したAPI実装について取り組みたいと思います。

認証付きAPIを作る前準備 - 自作サービスづくり8

今回もWeb開発に関する記事です。
前回はbotを使った出欠確認・更新機能の実装を行いました。
developer-bpeldi2oerkd8.hatenablog.com

しかし、作成したAPIには認証がありません。
このため、URLさえわかってしまえば、APIを無関係の方が叩けてしまいます。
これは問題であるため、APIに認証機能を付け、
ユーザーが許可したシステムからしAPIを叩けないように変更します。

APIの認証方法

APIの認証について全く知らなかったため、調べてみました。
調べると、いくつか種類があります。
Basic認証やDigest認証、OAuthなど有名なものも多いですが、
今回はこちらのページにもあるように、
より一般的な手法であるJWT認証を取り入れることにしました。

JWTとは

JWTはJSON Web Tokenの略で、JSON形式で書かれたデータをBase64urlでエンコードした認証用のトークンです。
署名があるため、改ざん防止ができます。
詳しい説明はこちらのサイトが非常に参考になりました。
scgajge12.hatenablog.com

構成の考案

現在のAPI周りの構成は以下のようになっています。
f:id:bpeldi2oerkd8:20210610162127j:plain

ここに認証を付けます。
今回は jsonwebtoken というパッケージを使います。
このドキュメントを見ると、JWTを発行する jwt.sign() と、JWTを検証する jwt.verify() が必要なことがわかりました。

このため、まずJWTを発行する /api/v1/login にアクセスし、JWTを発行した後そのJWTを返し、
そのJWTをヘッダーに追加してAPIを利用するときに検証するという流れを考えました。
これを図にしたのが以下の図です。
f:id:bpeldi2oerkd8:20210710205316j:plain

ここで、JWTを発行する際に通常はユーザー名とパスワードを使うそうですが、
今回はSlack上のチャンネルが対応しているので、
チャンネルIDとそのチャンネルのトークンを利用することにしました。

チャンネルのトークンはSlackの連携設定時に発行するものとし、
セキュリティ上の問題が発生した際に再発行できるようにします。

Slackとの連携機能の実装の際のDBと機能の整理

Slackとの連携機能を実装するうえで、DBと機能の整理をします。

データベースは以下のような構成にします。
(sequelizeというORMを利用)

scheduleテーブル

const Schedule = loader.database.define(
  'schedules',
  {
    scheduleId: {
      type: Sequelize.UUID,
      primaryKey: true,
      allowNull: false
    },
    scheduleName: {
      type: Sequelize.STRING,
      allowNull: false
    },
    description: {
      type: Sequelize.TEXT,
      allowNull: false
    },
    createdBy: {
      type: Sequelize.INTEGER,
      allowNull: false
    },
    updatedAt: {
      type: Sequelize.DATE,
      allowNull: false
    },
    roomId: {
      type: Sequelize.STRING
    }
  },
  {
    freezeTableName: true,
    timestamps: false,
    indexes: [
      {
        fields: ['createdBy', 'roomId']
      }
    ]
  }
);

roomテーブル

const Room = loader.database.define(
  'rooms',
  {
    roomId: {
      type: Sequelize.STRING,
      primaryKey: true,
      allowNull: true
    },
    roomToken: {
      type: Sequelize.STRING,
      allowNull: true
    }
  },
  {
    freezeTableName: true,
    timestamps: false
  }
);

次に、追加する機能を整理します。
追加したい機能は以下の通りです。

  • Slackとの連携がされていない場合、slackとの連携ボタンを表示
  • Slackとの連携がされている場合、slackとの連携設定変更ボタンの表示
  • チャンネルIDの登録とトークン発行機能
  • slackとの連係解除機能
  • チャンネルIDの変更機能
  • トークンの再発行機能


これらの機能をまず実装していきます。
ページの関係で次回に続きます。

Botを使った出欠確認機能の実装 - 自作サービスづくり7

今回の記事もWeb開発に関する記事です。
前回はslack上のbotから前々回実装した出欠更新APIを叩きました。
developer-bpeldi2oerkd8.hatenablog.com

このままでは、現在の出欠情報を確認する際にその都度出欠を更新する必要があります。
このため、出欠情報の確認のみをする機能も必要だと考えました。
そこで、今回は出欠確認APIを実装し、botからこのAPIを叩いてみたいと思います。

出欠確認APIの実装

出欠更新APIと同じように実装していきます。
基本的には同じで、変更部分はAvailabilityの更新部分をfindOneに変えるだけです。

    getData()
    .then(([schedule, user, date]) => {
      Availability.findOne({
        where: {
          scheduleId: schedule.scheduleId,
          userId: user.userId,
          dateId: date.dateId
        }
      })
      .then((a) => {
        if(a) {
          res.json({
            status: 'OK',
            data: {
              slackId: user.slackId,
              date: date.date,
              availability: a.availability
            }
          });
        }
        //出欠登録情報がない場合は欠席
        else {
          res.json({
            status: 'OK',
            data: {
              slackId: user.slackId,
              date: date.date,
              availability: 0
            }
          });
        }
      });
    })
    .catch(([scheduleMessage, userMessage, dateMessage]) => {
      res.json({
        status: 'NG',
        error: {
          messages: [scheduleMessage, userMessage, dateMessage]
        } 
      });
    });

データベースに出欠情報が登録されていない場合は欠席として扱っているため、その処理が入っています。

そのほかのコードについてはこちらです。
github.com

エラーメッセージが正しく出力されるように修正

テストコードを書き、正常時は登録が可能になったことが確認できたのですが、予定に存在しない日付を入れるとエラーメッセージが正常に返ってこない問題がありました。

私はエラーが出た場合Promiseのrejectでエラーメッセージを返していたのですが、Javascriptでは複数のPromiseを非同期処理する場合、1つrejectでエラーを返されると、そこでcatch内の処理に入ってしまうそうです。
今回はエラーが発生した場合、そのすべてのエラーメッセージを出力したかったため、エラーが起きた場合でもresolveでエラー状態であることを返し、その後エラーメッセージを配列にしてJSONで返す情報に入れました。

具体的には、以下のようにエラーの場合'error'という文字列を返し、

  if(d) {
    resolve(d);
  }
  else {
    resolve('error');
  }

エラーの場合は、そのエラーメッセージをmessagesという配列に入れるようにしました。

  schedule = schedule === 'error' ? 'このルームIDはシステムに登録されていません' : '';
  user = user === 'error' ? 'このSlackIDはシステムに登録されていません' : '';
  date = date === 'error' ? '入力した日付はこの予定に存在しません' : '';
  const messages = [];
  if(schedule) {
    messages.push(schedule);
  }
  if(user) {
    messages.push(user);
  }
  if(date) {
    messages.push(date);
  }

もう少しきれいに書けるような気もしますが、ひとまずこちらで行きます。

コードの詳細については以下のようになっています。
github.com

Jestでテストコードを書き、問題がないか確認しました。
結果がこちらです。
f:id:bpeldi2oerkd8:20210621205255p:plain
問題なくテストが通っています。

BotからAPIを叩く

APIが完成したので、今度はbotのほうを実装していきます。
出欠更新APIの時と同じように実装します。
違いは、今回はPOSTではなく、GETを用いる点です。

function confirmAvailability(msg, roomId, slackId, dateString){
  const confirm_url = api_url + '/' + roomId + '/users/' + slackId + '/dates/' + dateString;

  msg.http(confirm_url)
  .get() ((err, res, body) => {
    if(err) {
      msg.send(err);
      return;
    }

    const data = JSON.parse(body);
    const availabilityStatus = ['欠席', '不明', '出席'];
    if(data.status === 'OK'){
      msg.send('出欠確認成功:' + '<@' + data.data.slackId + '> さんの' 
        + data.data.date + 'の予定は ' + availabilityStatus[data.data.availability] + ' です');
    } 
    else {
      msg.send('出欠確認失敗:' + '\n' + data.error.messages.join('\n'));
    }
  });
} 

そのほかのコードについてはこちらです。
github.com

動作確認

どちらも完成したため、動作確認をします。
Slack上のbotから決められた形式でbotにメンションすると、結果を返してくれます。
出欠を確認する場合は、

@bot名 確認 日付(月/日の形式)

でメッセージを送ります。

実際に動かした結果がこちらです。
f:id:bpeldi2oerkd8:20210621211353j:plain
エラーの場合はエラーメッセージを返しています。
はじめ6/15の予定が欠席だったのが、出欠更新後出席に変わっていることがわかります。

Webサイト(Lattendance)の表示も出席に更新されています。
f:id:bpeldi2oerkd8:20210621211755j:plain


今回は、出欠確認機能の実装に取り組みました。
次回こそは、APIの認証に取り組みたいと思います。

Hubotから出欠更新APIを叩く-自作サービス作り6

今回の記事もWeb開発の勉強に関する記事です。
前回は出欠更新APIの実装をしました。
developer-bpeldi2oerkd8.hatenablog.com

今回はbotからAPIを叩くことでbot経由で出欠更新ができるようにします。

構成

今回は2つのシステムを開発しています。
図にすると以下の通りです。
f:id:bpeldi2oerkd8:20210610162127j:plain

1つは予定の作成・削除・編集と自らの出欠が更新できるLattendanceです。
ここには、APIとDBが存在しており、外部からAPIを叩くことでも出欠を更新できます。

もう1つは外部から出欠登録を行うbotであるLattendance Botです。
今回はHubotを用いてSlack用のbotを作成しています。
LattendanceのAPIを叩くことで出欠の更新が行えるため、
これをbotを入れたいツールに応じて作り変えることで、
別のコミュニケーションツール(Twitter・ChatWorkなど)に対応することが可能です。

RoomIdの追加

今回は1つの予定につき1つのSlackのチャンネルを作成し、
そこに1つの出欠更新用のbotを入れることを想定しています。
DBのScheduleテーブルにroomIdというカラムを追加します。
RoomIdはチャンネルを右クリックし、「リンクをコピー」でコピーしたリンクの

https://***.slack.com/archives/〇〇〇〇〇〇〇

の〇〇〇〇〇〇〇の部分です。

まずは、RoomIdを登録できるように実装を変更しました。
下の図のようにし、roomIdが重複した場合にはアラートを出すようにしました。
f:id:bpeldi2oerkd8:20210610164804j:plain

f:id:bpeldi2oerkd8:20210610164817j:plain

コードについては以下のリンクを確認してください。
github.com

HubotからAPIを叩く

このようにSlackのチャンネルとLattendance上の予定を1:1で結びつけることができたら、
APIを叩くbotのほうを作成していきます。

Slack上のbot上でわかる情報は以下のようになっています。

情報 意味
roomId SlackのチャンネルID
(Lattendance上の予定と1:1で紐づいている)
SlackId SlackでのユーザーID
(Lattendance上のユーザーと1:1で紐づいている)
dateString 予定の日付
(フォーマットはYYYY-MM-DD)
availability 出欠情報
(欠席:0, 不明:1, 出席:2でLattendance上のと同じ)
(この情報のみPOSTデータとして送る)

この情報をもとにAPI側でDBの出欠情報を更新し、
以下のようにJSONとしてstatus(OK: 成功, NG: 失敗)と関連データが返ってきます。

成功した場合

res.json({
  status: 'OK',
  data: {
    slackId: user.slackId,
    date: date.date,
    availability: availability
  }
});

失敗した場合

res.json({
  status: 'NG',
  error: {
    messages: [scheduleMessage, userMessage, dateMessage]
  } 
});

このJSONをパースし、返ってきた結果に応じてメッセージを返します。
その部分を実装したコードは以下の通りです。

function updateAvailability(msg, roomId, slackId, dateString, availability){
  const update_url = api_url + '/' + roomId + '/users/' + slackId + '/dates/' + dateString;
  const param = JSON.stringify({
    availability: availability
  });
    
  msg.http(update_url)
  .header('Content-Type', 'application/json')
  .post(param) ((err, res, body) => {
    if(err) {
      msg.send("Error :( " + err);
      return;
    }

    const data = JSON.parse(body);
    const availabilityStatus = ['欠席', '不明', '出席'];
    if(data.status === 'OK'){
      msg.send('出欠更新完了:' + '<@' + data.data.slackId + '> さんの' 
        + data.data.date + 'の予定は ' + availabilityStatus[data.data.availability] + ' です');
    } 
    else {
      msg.send('出欠更新失敗:' + '\n' + data.error.messages.join('\n'));
    }
  });
}

動作確認

ここまで作ったものが実際に動作するか確認します。
今まで作った2つのシステムを起動し、bot経由で出欠が更新できるか確認します。

変更前はこのようになっています。
f:id:bpeldi2oerkd8:20210612132902p:plain

ここで、Slackで出欠情報を更新します。
f:id:bpeldi2oerkd8:20210612132933p:plain

すると、出欠情報が更新されているのが確認できます。
f:id:bpeldi2oerkd8:20210612132943p:plain


今回はHubotからAPIを叩き、出欠情報を更新できるようにしました。
次回は、APIに認証をつけることでセキュリティを高めたいと思います。

Web開発の勉強12- 自作サービス作り5

今回の記事もWeb開発の勉強の記事です。
前回の記事では、GitHubアカウントでログインして予定の作成・削除・編集と出欠登録ができるシステムを完成させました。
developer-bpeldi2oerkd8.hatenablog.com

とはいっても前回までの内容はN予備校の教材の内容に近かったため、オリジナルの機能である「Slackのbotを使って出欠登録をする」ことを目指したいと思います。

準備

どのように機能を実現するかを考えます。
本体の出欠登録システムであるlattendance (1)と、slackのbotとしてのlattendance-bot (2)の2つを用意しています。
アプローチとしては、次の2つを思いつきました。

  • 2つのシステムでデータベースを共有し、(2)で直接データベース内の出欠情報を更新する。
  • (2)から(1)の出欠更新APIを叩き、出欠情報を更新する。(データベースは(1)に存在する。)

今後、botをSlack以外にも拡張するかもしれないことを考えると、外部からAPIを叩けばよいだけの2つ目を採用することにしました。

出欠更新APIの実装

早速実装に移ります。
今回は、APIにアクセス制限はつけず、まずは正しく動作するAPIを実装することに重きを置きます。

routesディレクトリ下にv1というディレクトリを作り、そこにAPIに関するファイルを入れることにします。
APIのURLは /api/v1/schedules/:roomId/users/:slackId/dates/:dateString としました。
dateStringのフォーマットは、YYYY-MM-DDとします。

完成したものがこちらです。
github.com

Promiseやasync/awaitを使い、実装しました。
日付の先頭が0のとき、うまく動かなかったため修正しました。

Jestを使ってテストコードも書きました。
テストもしっかり通り、今のところは問題ないようです。
github.com
f:id:bpeldi2oerkd8:20210609001313p:plain


今回はここで終わります。
次回はbotの実装と、それに伴うデータベースの変更に取り組みます。

Web開発の勉強11- 自作サービス作り4

およそ半年ぶりとなってしまいました。
今回はWeb開発の勉強の記事です。
前回は出席登録機能以外を完成させました。
developer-bpeldi2oerkd8.hatenablog.com

今回は出席登録機能とURL共有機能の追加、Herokuへのデプロイまで完成させました。

完成したもの

GitHubにも載せましたが、念のためデモ動画を載せます。

経過

経過の詳細は以下のコミット履歴を確認してください。
github.com

まずは概要を説明します。
前回からの変更点は以下の3つです。

  • 出欠登録機能の追加
  • Herokuへデプロイするためにパッケージ等のアップグレード

それぞれ簡単に説明します。

出欠登録機能の追加

前回完成していなかった出欠登録機能を完成させました。
前回はbotを使ってAPIを叩いて出欠登録ができるようにしようと思っていたのですが、時間の関係でいったん諦めました。
その代わりにN予備校の教材にあったAjaxを使った登録を実装しました。
この部分については、今後時間があればbotを使って出欠登録ができるように修正したいと思います。

URL共有機能の追加

予定ページにそのページのURLを表示し、Copyボタンを押すとクリップボードにコピーされるように変更しました。
実装はjQueryを用いて実装しています。
jQueryのclickメソッドとselectメソッドは非推奨らしいので、onメソッドとtriggerメソッドを代わりに使いました。
Bootstrap4がjQueryを必要とするため、jQueryを使いましたが、今のトレンドはReactですかね...

Herokuへのデプロイ

基本的には今までやったようにデプロイするだけでした。
Node.jsのバージョンを上げるとなぜか動かなかったのですが、pgモジュールのバージョンを上げることで解決しました。

感想

事情があって時間がかなり空きましたが、ひとまずサービスとして動くように完成できたと思います。
botとの連携については時間がかかりそうなので今回はパスしましたが、時間があるときに追加したいと思っています。
あと、今年からN予備校の入門コースの教材がDocker対応になったのでそれも時間があれば試したいと思います。

【メモ】Windows10で画面録画をするとき「録画が動作していません エラー:0x82323007」とエラーが出て録画できない場合の対処法

Windows10で画面録画をしようと思ったのですが、「録画が動作していません エラー: 0x82323007」というエラーメッセージが出て、録画できなかったので対処法を探しました。

対処法

Windows10のバージョンを1909から20H2にバージョンアップすることで解決。

原因

おそらくDirectX 12 UltimateをサポートしているバージョンでないとXbox Game Barが録画できないようになったため。
なので、バージョン2004以上に上げれば動作するようになるはず。

Web開発の勉強10- 自作サービス作り3

今回もWeb開発に関する記事です。
前回は出欠登録に使うbotを側だけ作りました。
developer-bpeldi2oerkd8.hatenablog.com

今回は予定の作成・編集・削除ページとその処理を書き、デザイン面も決めました。
後はメインのbotを使って出席登録をできるようにするだけです。
(おそらくここが大変だと思いますが)

作ったもの

削除確認ダイアログはBootstrapのModalを使って実装しました。

slackIDはユーザー一人につき1回のみslackのIDを登録することになっています。
ここのあたりの仕様はまだ詰め切れてないので、残りの部分を実装しながら修正します。

各フォームはcsurfモジュールを用いてCSRF対策を実装済みです。

  • 予定一覧ページ

f:id:bpeldi2oerkd8:20201110232303p:plain

  • 予定確認ページ

f:id:bpeldi2oerkd8:20201110232354p:plain

  • 予定作成ページ

f:id:bpeldi2oerkd8:20201110232609p:plain

  • 予定編集ページ

f:id:bpeldi2oerkd8:20201110232501p:plain

  • 予定削除確認ダイアログ

f:id:bpeldi2oerkd8:20201110232543p:plain

  • slackID登録ページ

f:id:bpeldi2oerkd8:20201110232744p:plain

経過

細かな経過はこちらのコミット履歴をご覧ください。
github.com

工夫した点は削除ボタンを押した際、確認ダイアログを作った点です。
また、苦労した点はHerokuにデプロイする際、ローカルで出なかったエラーが出て苦戦した点です。

原因はいくつかありました。
1つ目は、bootstrapとjQueryをバージョンアップさせた際、yarn installをし直してyarn.lockファイルを更新していなかったことです。
2つ目は、不要なrequireやrequireの位置がおかしかったなどrequire関係のエラーです。

ひとまず出席登録以外の機能は完成したと思います。

まだまだ改善の余地はあると思いますが(日程の追加・編集方法など)、今後はbotを使った出欠登録を完成させることを優先させたいと思います。

感想

時間はかかりましたが、出席登録機能以外を完成させました。

残りはbotを使った出席登録ですが、以前作ったlattendance-botからlattenndanceのAPIを叩き、出欠情報をデータベースに登録させたいと考えています。

うまくいくかはわかりませんが、何とか完成させたいと思います。

Web開発の勉強9- 自作サービス作り2

今回もWeb開発に関する記事です。
前回は自作サービスづくりで側だけ作りました。
developer-bpeldi2oerkd8.hatenablog.com

今回はbotの部分を作りました。

作ったもの

新たにhubotのひな型を作成し、出席登録のbotを作成しました。
今回は決まったフォーマットでメッセージを飛ばすと登録結果が返ってくるようにしました。
f:id:bpeldi2oerkd8:20201027164724p:plain

誤登録を防ぐため、メンションをつけないと反応しないようになっています。

反応する形式は、以下の3つです。

  • 出席(空白いくつでも)(月)/(日)
  • 欠席(空白いくつでも)(月)/(日)
  • 不明(空白いくつでも)(月)/(日)

経過

hubotのひな型をyoで作り、決まった形式ではないと反応しないようにbotJavascriptで実装しました。

個人的にはまったのが、herokuで動くようにするための方法です。
ひな形のままでは、「/usr/bin/env: 'coffee': No such file or directory」というエラーが出てしまい、動きません。

これを解決するためには、binフォルダ直下のhubotとhubot.cmdを以下のように編集します。

#!/bin/sh

set -e

yarn install #--no-bin-linksを削除
export PATH="node_modules/.bin:node_modules/hubot/node_modules/.bin:$PATH"

exec node_modules/hubot/bin/hubot --name "lattendance-bot" "$@"
@echo off

rem --no-bin-linksを削除
call yarn install  
SETLOCAL
SET PATH=node_modules\.bin;node_modules\hubot\node_modules\.bin;%PATH%

node_modules\hubot\bin\hubot.cmd --name "lattendance-bot" %*

感想

botはすぐ完成させて次に行く予定だったのですが、heroku上で動かないという問題に悩まされて思ったより時間がかかりました。

今後は、登録のメッセージが飛ばされた後、APIを呼び、出欠情報をデータベース上に登録できるようにしたいと思います。

次回こそ、フォーム・データモデルの実装を行っていきたいと思います。

Web開発の勉強8- 自作サービス作り1

今回もWeb開発に関する記事です。
前回の記事でようやくWeb開発の入門コースが終わりました。
developer-bpeldi2oerkd8.hatenablog.com

今回から自分でサービスを1つ作ろうと思います。

何を作るか

せっかく「予定調整君」を作ったので、これを活かして自ら使うサービスを作れないかと考えました。

身近な問題として「研究室の出欠登録と確認が大変だ」という課題がありました。

このため、習った知識を生かし、Slackのbotを用いて出欠を簡単に登録できるようにし、それをサイト上で確認できるようにするというサービスを考案しました。

このサービスをLab(研究室)+Attendance(出席)の造語として「Lattendance」と名付けました。

今回からはこのサービスを作っていこうと思います。

設計

ページ構成は以下の通りです。

  • トップページ/マイページ(自ら作った予定の一覧)ログアウトはボタンを押すとできるようにする
  • ログインページ(Githubのログインへのリンク)
  • 予定表示/出欠一覧ページ(Slackのbotから登録した出欠情報を表形式で表示)
  • 予定作成ページ

注意点として、出欠の登録はSlackのbot経由でのみ可能です。

これによって、無関係者が出欠の登録をすることを防ぎます。

使う技術

開発言語:Javascript, HTML, CSS, SQL
フレームワーク:Node.js, Express, Bootstrap, Jest
環境:Linux, VS Code, heroku
データベース:PostgreSQL
プロジェクト管理:GitHub, Git

雛型の作成

今回はログイン機能の実装とBootstrapを使ったデザインの実装を行いました。

完成した画像はこちらです。

  • トップページ

f:id:bpeldi2oerkd8:20201010235558p:plain

  • ログインページ

f:id:bpeldi2oerkd8:20201010235644p:plain

  • ログインボタン後の遷移画面

f:id:bpeldi2oerkd8:20201010235749p:plain

  • マイページ

f:id:bpeldi2oerkd8:20201010235812p:plain

感想

今回はログイン機能とデザインを主にやりましたが、思った以上に苦戦しました。

特にログインしたときにユーザー名が表示できるようにし、プルダウンメニューでログアウトボタンを実装するのはかなり時間がかかりました。

また、GitHubでログインボタンのデザインにも悩み苦戦しました。

ただ、自分で試行錯誤して実装すると、今まで理解が浅かった部分がわかり、力になっていることを実感できました。

次回は、フォーム・データモデルの実装を行っていきたいと思います。

Web開発の勉強7- 実践サーバーサイドプログラミング2

今回もWeb開発に関する記事です。
ようやく夏のインターンシップが終わり、一息したところで残っていたWeb開発入門のコースを終わらせました。

このシリーズの前回の記事はこちらです。
developer-bpeldi2oerkd8.hatenablog.com

内容

教材としては「N予備校」の「 プログラミング入門Webアプリコース 」を使ってます。
www.nnn.ed.nico

内容のほうは、4章をすべて終わらせました。

具体的な内容としては以下のような感じです。

  • 「予定調整くん」というサービスを今までの知識を用いて制作

具体的には設計の部分からデザイン・セキュリティ対策まで実装しました。

感想

ひとまずこれで入門コースが終わり、Web開発に関する基本的な知識がついたと思います。
今回得た知識を使って、簡単なサービスを設計から一度やってみて完成させようと思います。
その経過をこのブログに載せたいと思います。


というわけで、Web開発の勉強の記事でした。
この入門コースはボリュームがあり、大変でしたが大変実のあるものになったと思います。
実際にExpressを用いて自分でサービスを作ることで、知識をより強固なものにしたいと思っています。

Web開発の勉強6- 実践サーバーサイドプログラミング1

お久しぶりです。
今回はWeb開発に関する記事です。

インターンシップ・研究などと忙しく、なかなかAtCoder、Web開発に手がつかない時期が続きました。
ここ1か月は時間が取れるようになってきたので、再開したいと思います。

このシリーズの前回の記事はこちらです。
developer-bpeldi2oerkd8.hatenablog.com

内容

教材としては「N予備校」の「 プログラミング入門Webアプリコース 」を使ってます。
www.nnn.ed.nico

無料登録期間中に登録したので、1年間は無料です。
ありがとうございます。

内容のほうは、4章の 15.テーブルの集計 まで終わりました。

具体的な内容としては以下のような感じです。

  • Webフレームワーク(Express)を使った開発の基礎

いよいよ本格的なWeb開発という感じですね。
Web APIの開発までカバーしています。

感想

これでWeb開発の基本はほとんど終わり、あとは実際のサービス開発の練習のみとなりました。
無料でこの教材が使えるのは大きく、とても勉強になります。
(有償でも受ける価値はあると思います。これはステマでもなんでもなく。)
このコースが終わった後は、実際に勉強した内容を使って1つサービスを作る予定です。


というわけで、Web開発の勉強の記事でした。
残りは予定調整くんという架空のサービスを作る練習です。
夏休み中には自作のサービスを1つ完成させたいなと思っています。

エイシング プログラミング コンテスト 2020 に参加しました (AtCoder)

エイシング2020
お久しぶりとなります。
エイシング プログラミング コンテスト 2020 に参加しました。
久しぶりのコンテストだったので、感覚を忘れていて、リハビリ感覚です。

ちなみに、前回の記事はこちらです。(だいぶ前となってしまいますが・・・)
developer-bpeldi2oerkd8.hatenablog.com

準備

ほとんどできていないです。
これから少しずつでも復帰できたらなと思います。

経過

前回からしばらく空いていたので、とにかく感覚を取り戻すのに精一杯でした。
A・B問題をさくっと解き、C問題に移りました。

C問題はぱっと見わからず、式変形を試みたりしましたが、
x^x+y^y+z^z+x*y+y*z+z*x >= 1となる関係から、
x=1, y=1と考えてもz=100くらいだと思ったので、全探索で行きました。
余裕をもってx, y, z <= 200で3重ループを回すと問題なく通りました。
(ここまで40分ほど)

import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Scanner;

public class Main {
	//java11

	public static void main(String[] args) {

		Scanner sc = new Scanner(System.in);

		int N = sc.nextInt();
		int[] count = new int[N+1];
		Arrays.fill(count, 0);
		for(int x=1; x<=200; x++) {
			for(int y=1; y<=200; y++) {
				for(int z=1; z<=200; z++) {
					int n = x*x+y*y+z*z+x*y+y*z+z*x;
					if(n <=N) {
						count[n]++;
					}
				}
			}
		}

		PrintWriter pw = new PrintWriter(System.out);
		for(int i=1; i<=N; i++) {
			pw.println(count[i]);
		}
		pw.flush();

	}

}

D問題は再帰と進数の変換を用いて実装しましたが、N=2*10^5と大きいので、
途中計算の記憶をMapで実装しました。
しかし、サンプルは通したのですが、ほとんどREとなってしまいました。
原因については、調べます。

import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;

public class Main {
	//java11

//	static int[] fn;
	static Map<Integer, Integer> fn;
	public static void main(String[] args) {

		Scanner sc = new Scanner(System.in);

		int N = sc.nextInt();
		String X = sc.next();

//		fn = new int[(int)Math.pow(2, N)];
//		fn = new int[10000000];
		fn = new HashMap<>();
//		Arrays.fill(fn, -1);

		PrintWriter pw = new PrintWriter(System.out);
		for(int i=0; i<N; i++) {
			StringBuilder sb = new StringBuilder(X);
			//bit反転
			if(X.charAt(i) == '0') {
				sb.setCharAt(i, '1');
			}else {
				sb.setCharAt(i, '0');
			}

			int Xi = Integer.parseInt(sb.toString(), 2);

			pw.println(fn(Xi));

		}

		pw.flush();

	}

	public static int fn(int n) {
		if(fn.get(n) != null) {
			return fn.get(n);
		}

		if(n == 0) {
//			fn[n] = 0;
			fn.put(n, 0);
			return fn.get(n);
		}

		if(popcount(n) == 0) {
			fn.put(n, fn(0) + 1);
		}else {
			fn.put(n, fn(n % popcount(n)) + 1);
		}
		return fn.get(n);
	}

	public static int popcount(int n) {
		return Integer.bitCount(n);
	}

}

結果

エイシング2020_result
久しぶりにしてはという感じですが、やはり問題を解かないとレベルは上がっていきませんね。
他のことでも忙しいのですが、なんとか時間を作って頑張りたいと思います。


というわけで、エイシング プログラミング コンテスト 2020の結果でした。
次回も参加したいと思います。