2019 年ふりかえり

2019 年も終わりなので、毎年恒例 1 年の振り返りをしようと思う。 元日に 1 年の抱負として OKR を設定していたので、それを 1 つずつ振り返って評価していく。

tnakamura.hatenablog.com

OKR の Objectives は 2 つ設定していて、1つが個人開発を頑張る。もう1つは英語を頑張る。

  • 個人開発を頑張る
    • Write Code Every Day
    • プロダクトを 2 つリリースする
  • 英語を頑張る
    • TOEIC を受ける
    • コミットコメントやドキュメントを英語で書く
    • 英語のポッドキャストを聴きまくる

個人開発の Key Results には Write code every day と、 プロダクトを 2 つリリースすることを挙げていた。 コードを書く方は、毎日とまでは行かないけど結構書いたんじゃないかなと思う。 GitHub の草を見た感じ、7 割くらいは埋まってるんじゃないか。 いや、言い過ぎか。6 割ぐらいかな。 プロダクトをリリースする方は 1 個もリリースできていないので、これは果たせていない。 実際のところ、公開直前までいってたアプリが 1 つあったんだけど、ちょっと事情が変わって OSS にしようとして、 その作業が間に合わなかった。 1 月の間に公開したいと思う。

次に英語の方だけど、 TOEIC は受けてない。 コミットやドキュメントに関しては、 public にしているリポジトリでは英語で書くよう頑張っているが、 そんなに長文を書いているわけじゃないので、これはやったと言えるものじゃない。 英語の Podcast は、日本語の Podcast のストックが切れた時に、 The Changelog Podcast をちょこちょこ聴いていた。

設定した OKR はほとんど達成できなかったが、 2019 年はデータベーススペシャリストを取ったり、 MCSD をゼロから取ったりと、 資格の勉強を頑張った 1 年間だった。 就職してからおそらく一番勉強した気がする。 それを加味して 2019 年を採点するなら、まぁ 50 点ぐらいはつけておこう。 それでも優良可不可では不可なんだけどね。 満足行く 1 年ではなかった。 来年はもうちょっと英語とか何とかしたい。

それでは良いお年を。

波よ聞いてくれ(7)

引きこもりの更生に自身のラジオ番組の企画として挑むミナレ達なんだけど、最後の手段と思われていた暴力沙汰に開始1分でなってしまうのは、観てる分には痛快で面白いが、当事者だったらたまったもんじゃないだろうな。まぁ、引きこもりの男とその家族みんなワケありで、個性的なキャラクターばかりだから、それでも成り立っちゃうんだが。中原はミナレのどこに惚れたんかね。

そんな風なマンガなので、作中で北海道の地震を扱ったのは驚いた。北海道が舞台で、かつラジオの社会的役割を考えれば、扱わない方がむしろ不自然か。リアルな地震の描写は、当時のニュースでは伝わってこなかった臨場感というか、現場感が伝わってきた。

波よ聞いてくれ(7) (アフタヌーンコミックス)

波よ聞いてくれ(7) (アフタヌーンコミックス)

とり田 薬院店

とり田の薬院店に念願の水炊きを食べに行ってきた。 最初は博多本店を考えていたけど、博多本店は当日予約できなかったのと、 当日キャンセルすると 100% 取られるペナルティがあるので、 二の足を踏んでいた。 ドタキャンするつもりはないが、 子供が当日熱を出すとかありえるので、100% 取られるのは怖いなと。 その点、薬院店は当日電話して予約がとれたので、意外と穴場だったのかもしれない。

コースは豪華な方から順に松・竹・梅の 3 コースがあり、 最初はケチって梅を予約していたけど、 やっぱ胡麻鯖とか食べたいなと思って、竹にアップグレードした。 竹の方がオススメみたいだし。

席に案内されて、まずはドリンクを注文。 最近ハマっているのジンジャエールにしたら、ウィルキンソンだった。 生姜の辛さが後からガツンとくる大人向けの味でクセになりそう。

前菜は名物のとり田たまご。 半熟の煮卵を真ん中で割って、肉味噌をつけていただく。 絶妙の半熟具合で、煮卵だけで十分旨い。 下の肉味噌をつけて食べるとさらに絶品。 とても食べ足りない。 もう1個ぐらい食べたいくらいだった。

その下の器には、きのこの和え物が入っていた。 きのこは嫌いじゃないけど得意でもないんだけど、 上品で美味しく仕上がっていた。 子供も最初嫌がっていたけど、一口食べて美味しかったと言っていた。

前菜を食べ終わること、主役のお鍋がグツグツ言わせながら運ばれてきた。 白濁というよりこれは黄金色だな。 黄金スープに鶏肉が入っている。

鍋を眺めていると胡麻鯖が運ばれてきた。 胡麻鯖もとり田の名物らしい。 胡麻ダレがすごく濃厚かつ鯖がプリッと新鮮で、 かなりレベルの高い胡麻鯖だったた。 胡麻鯖にうるさい奥さんも満足したようで、 これもまた食べたいくらい、かなり気に入った。

鯖を食べた後は、水炊きの黄金スープ試飲。 鶏だけを長時間煮込んで取ったという説明があり、 それでこれだけ濃厚なスープが出来上がるなんて、 という驚きを隠せないとともに、 続く鍋に期待が膨らむ一杯だった。

店員さんが鶏肉とネギしゃぶをよそってくれた。 とり田特性ポン酢をかけて食べると、 濃厚スープと柑橘のさわやかなポン酢が合わさって、 期待以上に美味しかった。 特にこのポン酢は買って帰りたいくらい。 鶏肉は柔らかく煮込まれていて、 この時点ですでに満足度高い。

店員さんが、野菜と豆腐、あとつくねを入れてくれた。 つくねは火が通っても赤い色が残るらしい。 このつくねも肉肉しくて旨かった。 子供もパクパク食べていた。 野菜はお代わりができるので、つい欲張ってお代わりしたら、 予想以上に量があってお腹はかなりパンパンに。 野菜のお代わりは要注意だな。

コースにはとり天もついていた。 写真撮るの忘れていて、取り分けたあとに写真撮ったんだけど。 失敗。 このとり天も、非常に肉がプリッとしていて旨かった。 唐揚げもいいけど、とり天でランチとかやってもウケるんじゃないだろうか。

鶏肉に野菜につくねと食べて満足してきたころに、ようやく締め。 雑炊とラーメンが選べた。 スープを堪能するならやっぱり雑炊だろう、 ということで雑炊を選択。

黄金スープで炊き上げた雑炊は、今まで締めで食べてきた雑炊の中で一番だな。 鶏や野菜の旨味が凝縮されていて至高。 これも単品メニューで出てきていいレベルだ。

最後は、デザートのほうじ茶プリンとお茶。 デザートまでクオリティ高かった。

かなり満腹になった。そして大満足。 ずっと、とり田の水炊きを食べに行きたくて、 タスクにも登録していたくらいだったので、 ようやく念願が叶った。 薬院店の方は隠れ家みたいで雰囲気が良いのに対し、 博多本店の方は観光客が多そうなので、 薬院店にして結果オーライだったかもな。 コースも竹ではなく、水炊きランチや梅を注文していたら、 この満足度は味わえなかっただろう。 頻繁に食に行けるものでもないし、ケチって良いことはない。 オススメの竹にして正解だった。

とり田 薬院店
〒810-0022 福岡県福岡市中央区薬院2-3-30 CASE BLDG1F
6,000円(平均)1,000円(ランチ平均)

MangaRank のソースコードを公開

MangaRank という Web サービスを個人開発していたが、今回そのソースコードを公開することに決めた。

github.com

MangaRank は、はてなブログのマンガカテゴリの記事をクロールし集計して、レビューの多いマンガをランキングにした Web サービス。

tnakamura.hatenablog.com

なぜソースコードを公開する決断をしたかというと、 はてなブログの記事をクロールして良い感じのランキングは出来上がったものの、 運用を続けてもほとんどランキングに変動がなかったから、というのが一つ。

もう一つの、そして主たる理由が、商品情報の取得に使っている AmazonPA-API の利用ポリシーが下記のように変わったから。

  1. PA-APIを継続してご利用いただくためには過去30日以内にPA-APIから取得したリンク経由の売上が少なくとも1件必要となります。
  2. アカウントのPA-APIのリクエスト上限数は過去30日以内の売上によって計算されます。

クローラーを動かし続けたところで、新しいマンガの商品情報を Amazon から取得できないなら、 動かすだけ無駄だなってことでクローラーは止めた。

フロントエンドは Netlify で公開していたけど、そっちはまだ残してある。 ランキング自体は自分にとって有用なので、しばらくはそのままにしておこうかなと。 他に Netlify で公開したい Web サービスができたら、 Netlify のリソースを明け渡そうと考えている。 それまで延命。

肝心のソースコードだけど、サーバーの方は .NET Core を使ったクローラーで、 技術的に大したことはやっていない。 GCPAmazonAPIC# で使うときの参考程度にはなると思う。 フロントエンドは Gatsby を使い、 Lighthouse のスコアを重視してチューニングしたので、 Blazing fast なサクサク感に仕上げてある。 こちらも、JAMStack の一例として参考になったら嬉しい。 なお、使った技術に関しては、下記の記事に詳しく書いてある。

tnakamura.hatenablog.com

ソースコードの公開が、個人開発した Web サービスの、せめてもの供養になれば良いと思う。

さて、次は何を作ろうかな。

からかい上手の高木さん(12)

2 年生に進級して、西片くんに対する高木さんのからかいの破壊力が増してきているな。二人の勝負は、当然のように西片くんが負けてしまうわけだけど、知らないうちに一矢報いている場面が出てきた。高木さんはキスや恋愛を想起させるような、攻めのからかいが多くなっているので、そのぶん反撃されるリスクを背負っているわけだ。無自覚な西片くんは、試合に負けて勝負に勝ったとでも言うべきか。案外、勝利は近いのかもしれないな。

からかい上手の高木さん (12) (ゲッサン少年サンデーコミックス)

からかい上手の高木さん (12) (ゲッサン少年サンデーコミックス)

豚そば 月や 大名店

支那そば月やの新ブランド『豚そば月や』が中洲にあり、行ってみたかったけど夜営業のみでなかなか行けずにいたら、11月に大名店がオープンしたらしい。しかも大名店は昼営業している。場所が中央区役所の裏だから会社からは遠いけど、走ればなんとか昼休みに行けそうだったので、ひとっ走りして食べに行った。

豚そばの他にしゅうまいも名物らしかったので、豚そばとしゅうまいとご飯のセットを注文。しゅうまいは自分が今まで食べてきたものと比べて数倍大きく、そしてフワフワの食感だった。このフワフワ食感は予想外。

お目当ての豚そばは透き通った豚の清湯スープで、しっかりと旨みだけ抽出したような癖のない仕上がり。スープだけずっと飲んでいられる。メニューに追いスープなんてものがあるくらいだから、自信があるんだろうとは思っていたが、飲んで納得した。これはもう豚のコンソメスープだな。

走って片道 10 分超かけて来たぶん期待のハードルは高かったが、見事に超えてくれた。むしろ衝撃的ですらあったな。あとは、場所がもうちょい天神寄りだったら、もっと良かったんだけど。毎回走るのはさすがにシンドイので。

関連ランキング:ラーメン | 赤坂駅天神駅西鉄福岡駅(天神)

Xamarin.Auth を使って Xamarin.Forms 製アプリを OAuth2 に対応させる

はじめに

Web サービスが提供する API を使ったクライアントアプリを開発する場合、 API の認証には OAuth を使うことが多い。

アプリで OAuth 対応を行う場合、肝心の認証部分は WebView を使った埋め込みブラウザを使うよりは、 SFSafariViewController や Chrome Custom Tabs を使うほうが望ましい。

それは Xamarin 製アプリでも同じ。

Xamarin.Auth

Xamarin.Auth という、Xamarin 製アプリに OAuth を使う認証機能を組み込むためのライブラリがある。 開発は活発ではないものの、メンテは続いてそう。 Deprecate にはなってないし。

github.com

Xamarin.Auth は認証時にネイティブの UI を使えるようになっていて、iOS なら SFSafariViewController、Android なら Chrome Custom Tabs を使ってくれる。

この Xamarin.Auth を使って Xamarin.Forms 製アプリを OAuth に対応させてみた。 だいぶ前に Xamarin.iOS プロジェクトに組み込もうとして上手くいかず断念した過去があり、 今回はそのリベンジ。

事前準備

GoogleFacebookGitHub などの OAuth サービスプロバイダにアプリ情報を登録し、 クライアント ID とクライアントシークレットを取得しておく。

アプリ情報登録の際に指定するリダイレクト URL は、カスタム URL スキーマのものを指定しておく。

Xamarin.Auth のインストール

NuGet で Xamarin.Auth をインストールする。

www.nuget.org

Xamarin.Forms プロジェクト側の実装

状態を共有するためのクラスを用意する

Xamarin.iOS 側と Xamarin.Android 側で、リダイレクト URL が呼ばれた後に処理を続行できるように、状態を共有するためのクラスを用意する。

using Xamarin.Auth;

public static class AuthenticationState
{
    public static OAuth2Authenticator Authenticator { get; set; }
}
OAuth2 での認証を開始するメソッドを実装する

Xamarin.Auth の OAuth2Authenticator を使用。

using Xamarin.Auth;
using Xamarin.Auth.Presenters;

[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class MainPage
{
    // 省略

    void ShowLoginPage()
    {
        var authenticator = new OAuth2Authenticator(
            clientId: "{OAuth サービスプロバイダから取得したクライアント ID}",
            clientSecret: "{OAuth サービスプロバイダから取得したクライアントシークレット}",
            scope: "{必要なスコープ}",
            authorizeUrl: new Uri("{OAuth サービスプロバイダの認証 URL}"),
            redirectUrl: new Uri("{OAuth サービスプロバイダに登録したリダイレクト URL}"),
            accessTokenUrl: new Uri("{OAuth サービスプロバイダに登録したアクセストークン取得 URL}"),
            isUsingNativeUI: true); // iOS では SFSafariViewController、Android では Chrome Custom Tabs を使う

        // AllowCancel が true だと Android の WebAuthenticatorNativeBrowserActivity.OnResume
        // で Authenticator の OnCancelled を呼び出してしまい、Completed イベントの
        // e.IsAuthenticated が false になってしまっていた。
        authenticator.AllowCancel = false;

        authenticator.ClearCookiesBeforeLogin = true;
        authenticator.Completed += Authenticator_Completed;
        authenticator.Error += Authenticator_Error;
        AuthenticationState.Authenticator = authenticator;

        var presenter = new OAuthLoginPresenter();
        presenter.Login(authenticator);
    }

    async void Authenticator_Completed(object sender, AuthenticatorCompletedEventArgs e)
    {
        if (sender is OAuth2Authenticator authenticator)
        {
            authenticator.Completed -= Authenticator_Completed;
            authenticator.Error -= Authenticator_Error;
        }

        // アクセストークは Account.Properties から取り出せる。
        if (e.IsAuthenticated &&
            e.Account.Properties.TryGetValue("access_token", out var accessToken))
        {
            // TODO: アクセストークンを保存する
        }
    }

    void Authenticator_Error(object sender, AuthenticatorErrorEventArgs e)
    {
        if (sender is OAuth2Authenticator authenticator)
        {
            authenticator.Completed -= Authenticator_Completed;
            authenticator.Error -= Authenticator_Error;
        }
    }
}

このメソッドを、認証を行いたいタイミングで呼び出すようにする。

Xamarin.iOS プロジェクト側の実装

Custom URL Schema を設定する

Info.plist にカスタム URL スキーマを追加する。 OAuth サービスプロバイダに登録したリダイレクト URL の URL スキーマを追加すること。

AppDelegate.OpenUrl メソッドを実装する

SFSafariViewController での認証後にリダイレクトされると、 AppDelete の OpenUrl が呼び出される。 コールバックに指定した URL が呼び出されたのかどうかをチェックし、 もしそうだった場合は OAuth2Authenticator の処理の続きを実行する。

using System;
using Foundation;
using UIKit;

[Register("AppDelegate")]
public class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
{
    public override bool FinishedLaunching(UIApplication uiApplication, NSDictionary launchOptions)
    {
        global::Xamarin.Forms.Forms.SetFlags("CollectionView_Experimental");

        global::Xamarin.Forms.Forms.Init();

        global::Xamarin.Auth.Presenters.XamarinIOS.AuthenticationConfiguration.Init();

        LoadApplication(new App());

        return base.FinishedLaunching(uiApplication, launchOptions);
    }

    public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
    {
        if (url.AbsoluteString.StartsWith("{OAuth サービスプロバイダに登録したコールバック URL}", StringComparison.OrdinalIgnoreCase))
        {
            AuthenticationState.Authenticator?.OnPageLoading(new Uri(url.AbsoluteString));
        }
        return true;
    }
}

Xamarin.Android プロジェクト側の実装

Chrome Custom Tabs での認証後にリダイレクトされると呼び出される Activity を実装する。 この Activity は OAuth2Authenticator の処理を呼び出したら即終了する。

using System;
using System.Text;
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.OS;

[Activity(Label = "CustomUrlSchemeInterceptorActivity", NoHistory = true, LaunchMode = LaunchMode.SingleTop)]
[IntentFilter(
    actions: new[] { Intent.ActionView },
    Categories = new[]
    {
        Intent.CategoryDefault,
        Intent.CategoryBrowsable
    },
    DataSchemes = new[]
    {
        "{OAuth サービスプロバイダに登録したコールバック URL の URL スキーマ}",
    },
    DataPaths = new[]
    {
        "{OAuth サービスプロバイダに登録したコールバック URL のパス部分}",
    }
)]
public class CustomUrlSchemeInterceptorActivity : Activity
{
    protected override void OnCreate(Bundle savedInstanceState)
    {
        base.OnCreate(savedInstanceState);

        var uri = new Uri(Intent.Data.ToString());

        AuthenticationState.Authenticator?.OnPageLoading(uri);

        Finish();
    }
}

おわりに

最初、Android だと認証後のリダイレクトが上手くいかず、リベンジ失敗の文字が頭をよぎった。 そこで諦めず、Xamarin.Auth のソースコードを追っていって、なんとかとか解決。 リベンジ成功。

ただ、Xamarin.Auth 自体が結構重厚なライブラリなので、 SDK の更新で容易に壊れてしまいそうな恐怖感がある。 対応するサービスプロバイダが 1 つだけなら自前で実装したほうがいいのかも。 そう思って、Xamarin.Essentials を使って実装を試みたけど、 リダイレクトしてから Xamarin.Forms 側のページに戻ったら、 ナビゲーションスタックが変になってしまった。断念。 このアプローチは Xamarin.Forms の実装に詳しくならないと厳しそうだ。