CodiMDで社内簡易Qiitaっぽいものが作れるので、練習がてら作ってみた話

まえおき

アドベントカレンダーでCW社のイケイケエンジニアが、なんかおもしろそうな記事を発信していた。

qiita.com

HackMD自体は使ってたことはあるんだけど、CW社ではQiita Teamが自由に使えていたので、自分用のメモにわざわざHackMDを使うことはしていなかった。

ただ、今の会社に転職してから、Qiita Teamは使えないし、社内のドキュメント共有で使っているAtlassian Confluenceは使い勝手が最低だし、HackMDいいんじゃね?と思って自分用インスタンスを立ててみることにした。

セットアップで少しだけハマった

config.json.example というものを config.jsonにコピーして使うんだけど、config.json.exampleにあるもので必要のないSAML設定などは消さないと動かなかったり、逆に protocolUseSSL などは当然ほとんどの人は使うはずなのにexampleにはなく自分で書き足さないといけなかったり、... あとは、データベースの設定が db っていうのだと動かなくて、 dbURLっていうのを指定しないといけなかったり...w

ともかく、exampleがほとんど信用にならなくて、 CodiMD Documentation - HackMD ここにある設定値を、冒頭で書いたアドベントカレンダー記事を参考にしつつ書き足していく必要があった。

最終的な運用で使うことになったconfig.jsonは以下の通り、ほぼexampleの跡形はないものであるw

{
    "production": {
        "domain": "ひみつ.japaneast.cloudapp.azure.com",
        "protocolUseSSL": true,
        "useSSL": false,
        "sessionSecret": "ひみつ",
        "loglevel": "info",
        "host": "localhost",
        "port": 8080,
        "allowAnonymous": false,
        "allowAnonymousEdits": false,
        "allowAnonymousViews": false,
        "defaultPermission": "private",
        "email": false,
        "allowEmailRegister": false,
        "allowGravatar": true,

        "dbURL": "mysql://hackmd:ひみつ@127.0.0.1:3306/hackmd",
        "bitbucket": {
            "clientID": "ひみつ",
            "clientSecret": "ひみつ",
            "emailDomain": "ひみつ"
        },

        "imageUploadType": "azure",
        "azure":
        {
          "connectionString": "ひみつ",
          "container": "ひみつ"
        },
        "plantuml":
        {
          "server": "https://www.plantuml.com/plantuml"
        },
        "linkifyHeaderStyle": "gfm"
    }
}

ちなみに、今回は勉強がてら自前でAzure VMDebian上でいろいろインストールしてCodiMDを入れたので、config.jsonでハマったわけだが、普通のユースケースではHerokuを使えば十分なはずなので、こんなハマり方はきっとしないだろう。

Bitbucketログインは無かった...

今回は、Atlassian Cloudの会社用アカウントでCodiMDを使えるのがいい。なぜかというと、「コンフルの糞め!」と思った人をいずれ取り込みたいから。

しかしながら,世の中的にBitbucketアカウントを使う人は少ないのか、CodiMDは(GitHubやGitLabはサポートしていたが)Bitbucketアカウントでのログインはサポートされていなかった。

とりあえず練習がてらGitLabのソースコードをコピペして、Bitbucketログイン機能を作った。

github.com

(いい感じでレビューしてもらえて、たぶん次のバージョンでは使えるようになるらしい。CodiMDのスピード感すごいな...)

ソースコードが役割ごとによく整理されていて、grepもしやすかったので、Node.jsはコピペプログラマな自分でも機能追加するのはかなり簡単だった。良いソースは人を育てるとはこういうことなのか。

社外の人を見れなくするには

たとえばoauth2_proxyを使うと、特定のOrganizationに属してる人じゃなければ403エラーにすることができる。

でもCodiMDには現時点でそんな機能はない。GitHubGoogleログインでもそういう機能はないらしく

github.com

github.com

それぞれにissueがあがっている状態である。

ただ、流石に社内メモを社外に漏らすのはダメだ。汚くてもいいから作らないといけない。

// bitbucket auth callback
bitbucketAuth.get('/auth/bitbucket/callback',
  passport.authenticate('bitbucket', {
    successReturnToOrRedirect: config.serverURL + '/',
    failureRedirect: config.serverURL + '/'
  })
)

ここのところで、特定のEメールアドレスじゃない人はfailureRedirectのほうに行くようにすると、とりあえずは良さそう。

で、早速passportのソースコードを見てみる。

github.com

これまた最高に読みやすい。 req.logIn() ってところで失敗ルートに流せば良さそうである。しかしながらそのlogInっていうメソッド定義は

passport/request.js at 42ff63c60ae55f466d21332306e9112295c0535e · jaredhanson/passport · GitHub

本当にログイン処理しかしていない。となると、選択肢は2つしかない

  • 自前でログイン後の処理を(successReturnToOrRedirectに頼らず)実装する
  • 汚くてもいいから passport-bitbucket-oauth2 を改造して、特定メールアドレス以外はエラーにしてしまう

今回の場合(Bitbucketログイン)だと、そもそもpassport-bitbucket-oauth2がユーザのemailを取得しに行ってくれていなかったという問題もあり、短手番でやるためにpassport-bitbucket-oauth2をいじることにした。

Strategy.prototype.userProfile = function(accessToken, done) {
  var emailDomain = this._emailDomain;
  var oauth2 = this._oauth2;
  oauth2.get(this._userProfileURL, accessToken, function (err, body, res) {
    var json;

    if (err) {
      return done(new InternalOAuthError('Failed to fetch user profile', err));
    }

    try {
      json = JSON.parse(body);
    } catch (ex) {
      return done(new Error('Failed to parse user profile'));
    }

    var profile = Profile.parse(json);
    profile.provider  = 'bitbucket';
    profile._raw = body;
    profile._json = json;

    if (!emailDomain) {
      return done(null, profile);
    }

    oauth2.get("https://api.bitbucket.org/2.0/user/emails", accessToken, function (err, body, res) {
      try {
        json = JSON.parse(body);
      } catch (ex) {
        return done(new Error('Failed to parse user emails'));
      }

      /* 以下のような構造のJSONが返ってくる。
      [
        {
          "is_primary": true,
          "is_confirmed": true,
          "type": "email",
          "email": "iwaki@yusuke-iwaki.com",
          "links": {
            "self": {
              "href": "https://api.bitbucket.org/2.0/user/emails/iwaki@yusuke-iwaki.com"
            }
          }
        }
      ]
      */
      var primaryEmail = json.values.find(function(email) { return email.is_primary && email.is_confirmed });
      console.log("primaryEmail:", primaryEmail)
      if (!primaryEmail || !primaryEmail.email) {
        return done(new Error('Primary email is not found.'));
      }
      if (primaryEmail.email.split("@")[1] != emailDomain) {
        return done(new Error('Primary Email address does not belongs to @' + emailDomain));
      }

      done(null, profile);
    });
  });
};

いやー、死ぬほど汚い。けど、とりあえず目的は果たせた。

f:id:YusukeIwaki:20191230153328p:plain

社外の人がBitbucketログインしても、500エラーになる。

500エラー・・・。まぁダメだとはわかってるんだけど、とりあえず社外の人を弾ければ何でもいいんだ。まずは自分しか使わないし。

そんなわけで、使ってみるとまるでQiita/Kobito

kobitoがサービス終了して久しいが、それに近い使用感でメモが取れるのは本当に良い。

Confluenceは使い勝手が最悪なので、自分の作業メモは今後CodiMD使うようになると思う。

まとめ

無理してConfluenceで消耗しているくらいなら、CodiMDを適当に建てて使うほうが絶対に良い。

日常から「ちゃんと」というワードを取り除くと、なんとなく余裕が感じられる

まえおき

自分は年をとってきたのか、以前にましてマイペースになってきている。

なんとなくではあるけど、なぜか半年前くらいから「ちゃんと○○して!」みたいな言葉を人から言われることに、ものすごく苛立ちを感じはじめた。

でも人から言われることに苛立っているということは、裏を返すと、自分が「ちゃんと○○して!」を無意識に発しているとすれば是正する必要があるということ。

そんなわけで、ここ半年は意識的に日常から「ちゃんと」っていう言葉を取り除くようにしていた。

「ちゃんと」はもともと多義語すぎる言葉

「ちゃんと」とか「きちんと」とか、それっぽい類義語も含め、結局その意味は文脈に大きく依存する。そして意味だけでなく程度も人によって大きく違う。

今こうしてブログに文章を書いているところに「もうちょっとちゃんと説明してもらわないとわからない」とコメントが付いたとしよう。私としては何をどの程度までしたら"ちゃんと"説明したことになるのだろうか?

といった具合に、(ブログにちゃんと説明しろって言うコメントは付ける人は流石に居ないと思うがw)「ちゃんと」という言葉を発する背景には、要求事項が整理されていないまま漠然と要求していることがある。

自分に対する「ちゃんと」と、相手に対する「ちゃんと」

「ちゃんと」はとても主観的で整理されていない状態なので、自身に対して「ちゃんとしなきゃ」という自戒をすることはあっても良いと思う。

しかしながら、人に対して「ちゃんと○○しなさい」「きちんと○○してよ」と要求するのはもっと冷静に考えないといけない。「なんでもいいから○○しなさい」「いいかんじに○○してよ」と同程度の要求をしているんだと意識すれば、おのずと具体的な指示内容に置き換わる。

  • 「ちゃんとソースコメントを書いてください」の代わりに「社内の誰が読んでも意味が分かる程度にコメント書いてください」なのか「コーディングルールなのでコメントは忘れずに書いてください」なのか。
  • 「きちんと設計して」の代わりに「3年後に自分が見て困らない設計にして」なのか「新入社員が見てすぐ理解できる粒度に設計して」なのか。

ようするに、依頼する側も「ちゃんと」することはどういう意味があってやらせていることなのかを言語化することになる。

数カ月間「ちゃんと」を封印してみると・・・

自分がここ半年くらい、いざ「ちゃんと」「きちんと」という言葉を意識的に封印してみると、最初は代わりとなる言葉が見つからなくて発言で詰まることも多かった。代わりの言葉が見つからず、無意識に「ちゃんと」って言ってしまったことも多々あった。

しかし最近は、自分のなかで「いまは○○が一番やりたいことなんだ」って思考を整理するのに少しずつ慣れてきて、「ちゃんと」してほしいって要求することは減ってきた。

  • 思考がまとまってない状態で思考回路もdumpしないまま、「ちゃんと」要求するのをやめる。
    • なにが一番やりたいことなのかを今一度考える
    • やりたいことがわからないときは、そういう思考回路をたどってきたのかを絵/図で書く

「ちゃんと」を意識的に封印することで、自分の思考回路や判断を他人がトレースできる形にすることが根本的に重要なことだということにも気づき始めた。

 

「ちゃんと○○して」の裏返しには「あなたがちゃんとしてくれないと、私が○○しないといけない...」みたいな意図が少なからず無意識にでも存在している。ようするにこれは判断が属人化しているし、精神的にも余裕がない状態だ。

いっぽう、「ちゃんと」を封印して、思考回路や判断・意図をトレースできるようにした状態というのは、「あなたが私の意図を汲み取ってくれることを期待しています、信頼しています」のように、属人化から少し開放され、精神的にやや余裕がある状態である。

 

そう、「ちゃんと」を封印すると、なんとなく余裕が生まれるのである(とても短絡的な結論www)

まとめ

「ちゃんと」って発言する背景には、自分の思考が整理されていないことがある。

「ちゃんと」を要求するんじゃなくて、思考回路や判断・意図を誰にでもトレースできる状態にして、今後も気楽にいこう。

f:id:YusukeIwaki:20191112090801p:plain

 

 

そういえば・・・

書き終えたところで、konifar氏が昔、なにか記事を書いてたなーって思い出した。

konifar-zatsu.hatenadiary.jp

あとでちゃんと読んで、ちゃんと理解しよう・・・(あっ!

Kotlinの ?.let とか run とかをDartで使うためのパッケージを作ってみた

まえおき

always DRINKのパス表示アプリをFlutterで作ってみた - YusukeIwakiのブログ をやってたときに、

「Kotlinだったら ?.let でサクッと書けるのになー・・・」ってのを何度となく感じていた。

f:id:YusukeIwaki:20191106101230p:plain

でも言語仕様的に拡張はできないしな〜。と。そんなときにTwitterを見てたら

おおお。まじで??メソッド生やせるの???ええやん?

ということで、速攻やってみることにした。

パッケージを作るのはとても簡単

npm init てきなものはDartにはない。(参考: Dartのパッケージマネージャーのpubにはinitコマンドがない - Qiita )一応、stagehand っていうパッケージ雛形はあるんだけど、勉強がてら手で作ってみることにした。

pubspec.yamllib/ example/ test/ あたりを既存のパッケージの真似をして作ればいいだけで、かなり簡単だ。

Dart 2.6の static extension methods を使うところは

github.com

とかをかなり参考にさせてもらった。今回は ?.let を使いたいのが要望の9割だったので

extension ScopeFunctionsForObject<T extends Object> on T {
  ReturnType let<ReturnType>(ReturnType operation_for(T self)) {
    return operation_for(this);
  }
}

こんな感じで。ユニットテストtestパッケージ だけで

import 'package:test/test.dart';
import 'package:kotlin_flavor/scope_functions.dart';

void main() {
  group('.let', () {
    test('for simple integer', () {
      var result = 3.let((x) => x * 2);
      expect(result, equals(6));
    });

    test('for simple string', () {
      var result = "hoge".let((String x) => x.toUpperCase());
      expect(result, equals("HOGE"));
    });

    test('type conversion', () {
      var result = "123".let((String x) => int.parse(x));
      expect(result, equals(123));
    });

    test('with Null-Aware operators', () {
      String target;
      var result = target?.let((String s) => 'target is ${s}');
      expect(result, equals(null));

      target = "hogehoge";
      result = target?.let((String s) => 'target is ${s}');
      expect(result, equals('target is hogehoge'));
    });
  });
}

ものすごい雑に書いただけだが、動いた。

CIもメッチャ簡単に設定できる

GitHub ActionsにDart用の雛形がある。(pubspec.yaml が置いてあって、そこそこDartのコードをコミットしたらGitHubが自動判別してサジェストしてくれる!)

f:id:YusukeIwaki:20191106103142p:plain

DartのDockerも信頼と実績のGoogle製のものが提供されている。

hub.docker.com

ということで、セットアップ自体はまじで簡単だった。

github.com

個人的に、dartfmt忘れが何度もあったので、そこだけ

f:id:YusukeIwaki:20191106103351p:plain

こんな感じでチェックを追加した。

後追いでLinterを追加したりもした。

github.com

いずれにしても、全くハマるところはなく、快適に設定ができた。

pub publish するまで

これがちょっとだけ厄介だった。

LICENSEがないとpublishできない

まぁ当たり前といえば当たり前。

f:id:YusukeIwaki:20191106102119p:plain

GitHubが自動的にMITライセンスのひな形とか作ってくれるので、今回はそれに頼った。

独自ドメインを持っていないと、メールアドレス公開の刑に処される

pub publishGoogleアカウントがあれば誰でもできるんだけど、Verified Publisherではない人がアップロードしたパッケージはメールアドレスが思いっきりパッケージページに載せられてしまう。 dartxパッケージ は2019/11/05現在、まだVerified Publisherにされてないので、

f:id:YusukeIwaki:20191106102401p:plain

メールアドレスが公開されている。

Verified Publisherになるには、独自ドメインDNSレコードを管理(正確には、TXTレコードに任意の文字列を設定できる状態に)している必要があって、サブドメインでもいいから自前でドメインを取得する必要がある。

それをやるとようやく、メールアドレス公開の刑から解放される。

そんなわけで

Dart 2.6公開 の2日前くらいに、パッケージを無事に公開しましたとさ。

pub.dev