type holyshared = Engineer<mixed>

技術的なことなど色々

2020年の靴の着用回数を集計した

2019年から引き続き履いているもの、買い足したもの、手放したものを集計対象にしています。

2020年は900件を投稿していたようです。
一つの靴について、だいたい年間で20回ぐらいの着用、サンダルのみ使用頻度が高いので、倍ぐらいの着用回数になっています。

靴は1年間で16足増えて、9足手放し、合計で36足になりました。

2020年に関しては、スニーカーが増えています。
BROTHER BRIDGEのBERLINを買ってから、スニーカーもいいなと思ったので、日本のメーカーのものをいくつか買ってみました。

また、2020年はコロナ禍の影響もあり自宅で仕事することが増え他ことにより運動することが減ったので散歩をすることが増えました。
大体毎日6〜7km位歩いているようです。

そのおかげが全体的な靴の着用回数が増えたようです。
2021年は30足位に抑えようと思います。

No. ブランド モデル 2020年までの着用回数 2020年の着用回数
1 Tokyo Sandals DOBULE MONK SANDAL 182 92
2 Tokyo Sandals Enginier Slip on 124 88
3 BROTHER BRIDGE JAMES 57 28
4 Rolling dub trio ROOTS 60 27
5 BROTHER BRIDGE BERLIN 27 27
6 RAMSEY 442 MILITARY CAP TOE OXFORD 57 26
7 Redwing Irish Setter 56 26
8 Crockett&Jones CONISTON 29 24
9 BROTHER BRIDGE McCLOUD 53 24
10 BROTHER BRIDGE Morgan 28 23
11 Redwing 1930s Sport Boot 30 23
12 Sanders Military Apron Derby Boots 33 22
13 SLOW WEAR LION OB-8593GT 栃木レザープレーンミッドブーツ 50 22
14 SLOW WEAR LION OB-8593G オイルドレザープレーンMIDブーツ 48 22
15 Redwing Irish Setter 51 22
16 Nicks 6inch HotShot 50 21
17 JOHN LOFGREN M-43 SERVICE SHOES 21 21
18 Wesco Custom Jobmaster 38LTT 28 20
19 Pistorelo Norwogian Split 43 20
20 Chippewa 6inch Service Boots 49 20
21 Dry Bones CYPRESS 20 20
22 NPS shoes CAMERON ESSENTIAL 20 20
23 Scotch Grain Spider 28 19
24 BROTHER BRIDGE Harry 19 19
25 Berwick Chukka boots 20 18
26 Berwick Wing tip 34 18
27 SLOW WEAR LION OB-8208G オイルドレザーサイドゴアブーツ 47 18
28 MoonStar HI BASKET W 17 17
29 Chippewa Suburban 35 16
30 Chippewa Bridgemen 40 15
31 ASAHI M022 14 14
32 Chippewa 6inch Boots Reverse Black Odessa 14 14
33 KATSUYA TOKUNAGA BOYS 14 14
34 Timberland GARRISON TRAIL GORE-TEX HIKING SHOES 14 14
35 Ballband Jackie 12 12
36 Onitsuka Tiger MEXICO 66 12 12
37 New Balance Postal 706V2 11 11
38 NPS shoes LAW 7 7
39 ASAHI M020 6 6
40 SOLOVAIR Black Grain 6 Eye Derby Boot 5 5
41 Chippewa 6inch Service Boots 33 5
42 Rolling dub trio HunterⅡ 4 4
43 Berwick Chukka boots 30 3
44 Chippewa Bridgemen 27 1
45 Hunter Chelsea Boots 30 0

Oura Ring買って見た

Oura Ring

ずっと気になっていたので買って見ました。
サイズキット付きで購入し、サイズを確認してオンラインでサイズ指定するだけでリングを届けてくれます。

届いたあとは充電して、つけてるだけでメトリクスが取れます。
気にしているのは睡眠のスコアと消費カロリーです。

自宅で仕事をすることが多くなったので、運動不足解消の為にウォーキングをしているのですが、毎日目標の距離と消費カロリーを提示してくれるので、どのくらい歩けばいいのか目安になっていいですね。

また睡眠スコアがよくて、熟睡している時間とか細かく見れます。
スコアの内訳をみて、熟睡時間が長かったから目覚めがいいのかなとか、寝返りが多かったからスコア低いんだなと色々わかります。

体調のコンディションがわかることで、寝る時間をコントロールしやすくなったので満足度はすごい高いです。
まだ使ったことない機能もあるので、色々試して行きたいです。

Microsoft Graphでユーザーのアバター画像を取得する

Microsoft Graphにはv1.0とbetaのエンドポイントがあります。
どちらにもユーザーのアバターを取得するAPIがあります。  

しかし、v1.0とbetaで取得できる場合とそうでない場合があるようです。

https://docs.microsoft.com/ja-jp/graph/api/profilephoto-get?view=graph-rest-1.0 https://docs.microsoft.com/ja-jp/graph/api/profilephoto-get?view=graph-rest-beta

v1.0とbetaで同じユーザーのアバターを取得してみる

アバターはサイズを指定して取得をできるので、必要なサイズのものを取得します。
データはバイナリで受け取るので、適当にBufferなどに変えてファイルに書き出すかData URI schemeに置き換えて、DBに格納したりすることができます。

ここではData URI schemeに変えるコードを載せておきます。

import fetch from 'node-fetch';

const v1Endpoint = 'https://graph.microsoft.com/v1.0';
const betaEndpoint = 'https://graph.microsoft.com/beta';

type Endpoint = "v1.0" | "beta";
type AvatarSize = "48x48" | "64x64" | "96x96";

type Avatar = {
  mimeType: string;
  url: string;
};

const userAvatar = (endpointType: Endpoint, size: AvatarSize) => async (userId: string): Promise<Avatar | undefined> => {
  const options = {
    method: "get",
    headers: {
      Authorization: `Bearer ${process.env.ACCESS_TOKEN}`
    }
  };

  const endpoint = endpointType === 'v1.0' ? v1Endpoint : betaEndpoint;
  const response = await fetch(`${endpoint}/users/${userId}/photos/${size}/$value`, options);

  if (!response.ok) {
    const failedBody = await response.json();
    console.log(failedBody);
    return;
  }

  const mimeType = response.headers.get('content-type') as string;

  const buffer = await response.buffer();
  const data = buffer.toString('base64');

  const url = `data:${mimeType};base64,${data}`;

  return {
    mimeType,
    url
  };
};

const v1Avatar = userAvatar("v1.0", "48x48");
const betaAvatar = userAvatar("beta", "48x48");

const main = async (userId: string) => {
  const v1 = await v1Avatar(userId);
  const beta = await betaAvatar(userId);
  console.log(v1);
  console.log(beta);
};

const userId = process.argv[2];

main(userId).then(() => {
  console.log('done');
}).catch(err => {
  console.log(err.stack);
});

実行するにはアクセストークンとユーザーIDを指定します。

ACCESS_TOKEN=[アクセストークン] \
node avatar.js [ユーザーID]

v1.0では一部のユーザーのアバターが取得できない場合がある

一部のユーザーではアバターを設定しているのにもかかわらず、アバターが取得できないユーザーがいました。
そのユーザーの場合はMailboxNotEnabledForRESTAPIがエラー本文に含まれていました。  

{
  error: {
    code: 'MailboxNotEnabledForRESTAPI',
    message: 'REST API is not yet supported for this mailbox.',
    innerError: {
      date: '2020-08-25T10:53:21',
      'request-id': xxxxxxxxxxxx
    }
  }
}

betaで問題が発生しないのは一部のAPIの制限がなくなったものと思われます。
OfficeなどのAPIがMicrosoft Graphに統合されているはずなのですが、まだ完全には終わっていないのかもしれないです。

Azure Bot ServiceのBot Framework SDKのkoa対応

最近はBot Framework v4を利用して、Botアプリケーションを作成しています。
Botアプリケーションを作成するには、SDKを利用するのですが、expressrestifyくらいしかWebアプリケーションのフレームワークが対応してないような気がします。

koaの場合

例えば、BotFrameworkAdapter.processActivity の要求にレスポンスオブジェクトを渡す必要があるのですが、 koacontext.response を渡すと status、send、end あたりのメソッドがない為に、ランタイムエラーが発生します。

これは、expressrestifyのレスポンスオブジェクトのインターフェースをBotFrameworkAdapterが期待しているからです。

また、koaの実装の場合、ミドルウェアを一通り処理した後に最終的な応答を返すので、サンプルでよくある実装をすると応答を正常に返せません。

// よくみるサンプルコード
// express/restifyの場合
app.post('/api/messages', (req, res) => {
  adapter.processActivity(req, res, async (context) => {
    await bot.run(context);
  });
});

Bot Emulatorを使用してデバッグすると、404を返した後に200を返したりします。

f:id:holyshared:20200802185533p:plain
Emulatorのログ

// 素直にkoaでサンプルのコードを実装した例
// routerはkoa-routerのインスタンス
router.post('/api/messages', async (ctx: RouterContext<{}, {}>) => {
  const { request: req, response: res } = ctx;
  adaptor.processActivity(req, res, async (turnContext: TurnContext): Promise<void> => {
    await bot.run(turnContext);
  });
});

この現象の理由は、processActivityをawaitしてない為、コールバック関数を抜けた後に、404を直ぐに返し、その後にBotのメッセージ応答が正常に終了し、200などの応答を返す為だと思います。

express/restifyの場合はawaitしなくてもレスポンスオブジェクトのendを実行したタイミングで応答が完了するので問題ないのですが、koaの場合はそうではなく、最終的な応答を返すのがフレームワーク側 なのでうまく行きません。

サンプルのコードとかだと、ただのコールバック関数で処理しているだけに見えたので若干困りました。

最終的な対応

原因がわかったので、適当なexpress/restifyのレスポンスオブジェクトを渡しつつ、processActivityをawaitして対応することにしました。

スマートな解決方法ではないですが、テキストメッセージをユーザーに送るくらいだったらこれで良さそうです。
また、WebResponseのインターフェース仕様を読む限り、end、send、statusだけ実装されてれば良いぽいのでこれであってそうではあります。

import Koa from 'koa';
import Router, { RouterContext } from 'koa-router';
import { BotFrameworkAdapter, TurnContext, WebResponse } from 'botbuilder';
import { Bot } from './bot';

const app = new Koa();
const router = new Router();

const adaptor = new BotFrameworkAdapter({
  appId: process.env.BOT_APP_CLIENT_ID,
  appPassword: process.env.BOT_APP_CLIENT_PASSWORD
});

const bot = new Bot();

const createWebResponse = (ctx: RouterContext<{}, {}>): WebResponse => ({
  status(code: number): void {
    ctx.status = code;
  },
  send(body: string): void {
    ctx.body = body;
  },
  end(): void {
  }
});

router.post('/api/messages', async (ctx: RouterContext<{}, {}>) => {
  const res = createWebResponse(ctx);
  await adaptor.processActivity(ctx.req, res, async (turnContext: TurnContext): Promise<void> => {
    await bot.run(turnContext);
  });
});

app.use(router.routes())
  .use(router.allowedMethods())
  .listen(3000);

HerokuのmLabからMongoDB Atlasへ

自分が運用している365shoes.styleはHerokuを使用しています。 そして、mLabアドオンを使用していました。

mLabアドオンはアナウンスがあったように利用できなくなります。
なので、何かしらの方法で移行する必要がありました。

移行先は一番移行が楽そうな、MongoDB Atlasを選びました。
MongoDB AtlasはmLabからの移行をサポートしています。

https://docs.mlab.com/how-to-migrate-to-atlas/#complete-migration-wizard-for-specific-deployment

ステージング環境の移行

ステージング環境のMongoDBをMongoDB Atlas側でリンクさせて、手順通りに進めました。
クラスタをタイプを指定し、マイグレーションを実行するとmLabのデータをまるっとMongoDB Atlasの環境に移してくれます。

データの内容を確認した後に、アプリケーションの環境変数を変えて、新しい方のMongoDBのデータベースをアクセスしているか確認します。
接続できるかはマイグレーションのステップで、接続確認をするステップがあるので、そこで接続できれば特に問題ないような気がします。

本番環境の移行

ステージング環境の移行が済んだので、次は本番環境なのですが、ちょっと手順が代わります。
MongoDB Atlasからは一つのDBしかリンクできないようで、一旦ステージングとのリンクを切って、本番用の方にリンクし直す必要があります。

「Disconnect from mLab」というのがあるので、それをクリックすればOKです。

f:id:holyshared:20200727005431p:plain
MongoDB Atlasの管理画面

そのあとは、ステージング環境と同じように移行するだけなのですが、下記の2つの問題がありました。

  1. ステージング環境と同じプロジェクトにしたら、ユーザー名がステージング環境のユーザー名になって、データが正常にインポートできなかった。
  2. 移行後のユーザーの権限がreadだけになってしまった。

1に関してはユーザー名が本番環境のやつと違うけど、大丈夫なのかなと思いつつ、データインポートの処理を実行したら、データがインポートされないので、試しに別の新しいプロジェクトを作成して、試したところ正常に完了しました。

2に関しては、マイグレーションのステップを途中までやっていて、最後まで完了させなかったのが問題のようです。 データ移行まで終わって、安心したのか最後らへんの1、2ステップを見落としていたような気がします。
途中でタブ閉じちゃったのがよくなかったと思います。

ステップとしては移行元の書き込みを停止させるためにユーザーの権限を読み取り権限だけにするようなプロセスになっているようです。
https://docs.mlab.com/how-to-migrate-to-atlas/#importing-from-source-to-target

移行作業のまとめ

ウィザード形式で移行が進むので手順は楽でとてもよかったです。
その代わり、現状どういう状態なのかを確認して、各ステップでどうならなければならないといけないかを意識して作業しないと見落としがでそうです。

通常では移行作業自体は、手順を整理してこういう細かいことをやるのですが、あらかじめ手順が誰かに用意されている場合、意識することは少ないので気をつけたいです。

画像の変換にimgixを使う

アップロードした画像を変換するのに最近は、SaaSを使用しています。
普段はcloudinaryを使用していたのですが、imgixを試してみたら導入が簡単でした。

設定そのものは、imgixの設定画面でAWSのS3のバケットを指定して、アクセスキー、シークレットキーを指定する感じです。
後はimgixのライブラリを利用して、URLのパスを生成だけです。

import ImgixClient from 'imgix-core-js';

const client = new ImgixClient({
  domain: "example.imgix.net",
});

const key = "/wxyz_user_assets/original.jpg"; // S3のオブジェクトキー
const url = client.buildURL(key, { w: 400, h: 300 });

console.log("resize image: %s", url);

URLのセキュリティトークン

管理画面で Secure URLs を有効にすると、URLにセキュリティ用のトークンを追加できます。
クライアントのオプションで、トークンを設定するとURL生成時に、トークン + パス + クエリパラメータ をMD5でハッシュ化したパラメータをURLのクエリに追加してくれます。

このトークンがない状態で画像のURLにアクセスすると、403 のステータスコードを返してくれます。
この仕組みは特定のユーザーにしか閲覧を許可したくない場合に有効です。

URLが外部に漏れてしまっても、トークンを変えるだけで、アクセスできなくすることができます。
また、第三者にアプリケーションで使用していないサイズの画像をURLを変えることで生成されることも防げます。

可能な限り有効にしておいた方がいいと思います。

import ImgixClient from 'imgix-core-js';

const client = new ImgixClient({
  domain: "example.imgix.net",
  secureURLToken: process.env.IMGIX_SECURE_TOKEN
});

const key = "/wxyz_user_assets/original.jpg"; // S3のオブジェクトキー
const url = client.buildURL(key, { w: 400, h: 300 });

console.log("resize image: %s", url);

画像の読み込み

クライアントで画像のexifを読み出すのに普段はblueimp-load-imageを使用しています。
画像をアップロードをするフォームの投稿日のデフォルト値を指定したりする時や、写真をトリミングするコンポーネントなどで利用しています。  

自分がメンテナンスしているアプリケーションで古いバージョン2系を使用していたので、新しいバージョンの5系にアップグレードしました。

const options = {
  maxWidth: 600,
  orientation: true,
  meta: true,
  canvas: true // 1. オプションの追加
};
const photoEnableOrientation = document.getElementById('photoEnableOrientation');
photoEnableOrientation.addEventListener('change', (evt) => {
  loadImage(evt.target.files[0], options).then((result, meta) => {

    const exif = result.exif.get("Exif"); // 2. Exif IFDの読み出し
    const dateTimeOriginal = exif.get("DateTimeOriginal");
    console.log(dateTimeOriginal);

    document.body.appendChild(result.image);
  }).catch(err => {
    console.log(err);
  });
});

主な変更点は下記の通りでした。

  1. オプションの修正

    古いバージョンだと、コールバック関数の引数がHTMLCanvasElementだったのが、オプションでcanvas: trueを指定しないと、デフォルトではHTMLImageElementになる。

    また、Promiseだと結果オブジェクトになっていて、imageプロパティで参照できるようだ。

  2. exifオブジェクトのインターフェース変更の修正

    読み込み結果のオブジェクトにexifオブジェクトがあったが、参照キーが変わったようで撮影日時が取れなくなっていた。
    DateTimeOriginal指定で参照できていたが、Exifキーを指定して、Exif IFD の結果に対して、DateTimeOriginalを指定する必要があった。

修正自体はそんなに大したことなかったです。