MENU

HHKB Professional HYBRID Type-Sを購入して1年使っての感想とキーマッピングのカスタマイズ

結論

最高でした。あまりキーボードにこだわりがある方ではないですしキーボードを買うこと自体始めてですが本当に買ってよかったと思える品でした。

HHKB Professional HYBRID Type-Sとは

2019年12月10日に発売された高級キーボードHHKBシリーズの最新作!です。
自分も仕事柄メールやプログラミングでタイピングすることも多く、趣味でプログラミングもしていたので、HHKBはずっと気になっていたキーボードでした。
ただ家と会社両方で使うことも考えると、ただでさえPCが入って重いリュックにキーボードを入れて持ち歩くのも面倒だし、かといって持ち歩かずに家でPCを使うのと会社でPCを使うのとでキーボードの使用感が変わるのも嫌だったので少し悩んでいました。
そんな折にコロナ禍で仕事も大部分在宅になったので、思い切って買ってみました。
HHKB Professional HYBRID Type-Sにもいろいろな色、種類がありますが、やはり使用頻度が高い配列ということでJIS配列の黒を購入しました。

打キー感が最高

やはりこれが何より一番の理由ですね。 これは本当に凄いです。もともとPM的な業務が多かった(昨年は。今は実装がメインですが)ので社内外のメンバとメール、チャットでやり取りをすることも多く、ドキュメント作成も数多くしていたので、一日中タイピングしていたのですが、キーボードのタッチ感は本当に気持ちよくてずーっと文字を打っていたいと思うぐらいです。
具体的にどんな感覚なのかというと自分はあまり他のキーボードを触ったことがなかったのであまり比較は出来ないのでわかりません。。 が、以前バイト先でREALFORCEのキーボードでプログラミングしていたのですが、打キー感はそれと近いなーぐらいの感覚です。 まぁどちらも「静電容量無接点方式」とかいう仕組みのキーボードなので近いのでしょうが。
もちろんこの文章もHHKBを使ってタイピングしていますがいつまでも文字を打っていたいと思えるのですごく無駄な文章になっている気が。。

キーマッピングを自在に変更可能

Happy Hacking Keyboard キーマップ変更ツールというのが公式サイトからダウンロード出来るのですが、このアプリを使ってキーマッピングを好みのものに変更出来ます。
このキーマッピングの変更というのは、↑のソフトウェアで変更を管理するのではなく、HHKB本体のメモリに変更を書き込むようなので、PCに依存せず、接続するすべてのPCで変更したキーマッピングでキーボードを使用することが出来ます。

私の設定内容

私は以下の感じで設定しています。

変更前

通常状態(デフォルト状態)
通常状態(デフォルト状態)

Fnを押した際のキーマッピング(デフォルト状態)

変更後

通常状態
通常状態

Fnを押した際のキーマッピング
Fnを押した際のキーマッピング

  • 右下の矢印キーをすべて無効化(プログラミングしているときに _とか/を小指で押したいときにめちゃめちゃ押し間違って意図せずカーソル移動させちゃうので、、)
  • Vimのキーバインドが好きなので以下を設定(少し違いますが)
    • Fn + j ->
    • Fn + k ->
    • Fn + h ->
    • Fn + l ->
    • Fn + f -> PgDn
    • Fn + b -> PgUp
  • Fn押下時の矢印キー、+-PScScrLPusを無効化(普通に@とか:とか出るようにした。使わないしタイポの原因)
  • Fn押下時のInsCapsを無効化(何も反応しないようにした)
  • Fn + u -> BackSpaceFn + u -> Deleteを設定

上記を設定したら、本当にホームポジションから手を動かすことが少なくなり本当に快適にタイピングが出来るようになりました。

KarabinerでMacとWindowsでの文字種変更を共通化

現在、仕事でもプライベートでも、MacとWindowsどちらも使うという状況だったので、文字種変更だけでも共通化したいと思いました。
何も設定をしない状態だと、HHKBを接続した時の文字入力の変更は以下の通りとなっております。

  • Mac
    • ctrl + spaceで文字入力変更
  • Windows
    • Kanaでかな入力に変更
    • 左下のキー(画像の場所)で半角/全角キーと同等 f:id:ti_taka:20210507205102p:plain

通常のMacのJIS配列キーボードでもかなを押したら日本語入力になり、英数を押すと英語入力になるので、Windowsと使用感をあわせて、以下の感じにしました。

  • Mac
    • Kanaでかな入力に変更
    • 左下のキー(画像の場所)で英数入力に変更 f:id:ti_taka:20210507205102p:plain

Macでのキーマッピングの変更にはKarabiner-Elementsを使います。

↑の画像のキーはMacだと `として入力されるので、karabinerの設定で以下のように設定してあげます。

karabiner設定
karabiner設定

こうすることで、MacでもWindowsでもKanaを入力すれば日本語入力となり、上記画像のキーを押せば英数入力になるという感じで、かなり使いやすくなりました。
普段MacもWindowsもどちらも使うので、どちらに接続したとしても同じ使用感でタイピング出来るのは本当に気持ちいいです。

まとめ

1年使うまでもなく、2020年のベストバイの一品でした。
自分は色々とカスタマイズして使っていますが、それをしなくても打キー感の気持ちよさだけでも購入する価値のある品だと思います。
今回紹介した以外にも、HybridTypeなのでUSB給電でも電池駆動でもどちらでもいけますし、USB接続でBluetooth接続でもどちらも可能というのもなかなかありがたいです。
自分は基本的には有線接続で使っておりますが、家の中でちょっと移動して普段と違う場所でくつろぎながらーという場合でも気軽に持ち運んで使用出来るのも便利です。
ちなみに今では出社することは殆ど無いですが、前の部署だとコロナ禍でもそれなりに出社していたので、出社の際にキーボードを裸でバッグにしまうのは嫌だったので以下を購入して通勤してました。(今は全く使っていない。。)

Ruby Silverに合格しました

昨年社内異動があり、Ruby on Railsで開発する部署に配属となりました。
これまでも趣味レベルでプログラミングはやっていたけど、ガチの仕事でプロダクトコード書くのは学生時代のバイト以来で実に5年ぶりなので、ちゃんと体系立てて勉強せねばと思い、取り敢えずRubyの資格をとってみることにしました。
結果的に82点(75点合格なので割とギリ)で合格できたので、勉強内容を備忘として書きます。

色々自身の情報

進め方

正直、ざっと情報を調べた限りでは実務でのRails実装の経験は役に立たなそうだったので、0から暗記・勉強し始める気持ちではじめました。
他の経験談でも書かれていることですが、ざっくりとは以下の流れで進めました。

  1. 経験談など見て進め方ざっくり調べた
    • 見て「ふーん」程度の感じ
  2. 問題解きまくった

だけです。

勉強量

大体どの経験談の記事を見ても、一日○時間、合計○時間程度、って記載でしたが、自分にはあまりイメージが沸かず。。
正直自分自身も、子供面倒見ながら問題を解いたり、Youtube見ながら参考書読んだりダラダラとながら勉強をしていたことも多かったので、どの程度勉強したのか覚えておりません。
なので情報までに「どの程度問題を解いたか」で記載します。

  • 公式模擬問題集
    • 2回実施
      • 1回目44点。最初に現時点の実力試しとして実施。なんとなくでは解かず説明出来るものだけ回答したにしてもひどい点数。
      • 2回目点。色々勉強後。なんか行けそうかなーと思った。
  • RubyExamination
    • 5回実施
      • 50点、56点、86点、86点、90点
  • [改訂2版]Ruby技術者認定試験合格教本(Silver/Gold対応)Ruby公式資格教科書
    • 買って解いてみました。(お金をかけないとやる気にならないタイプなので)
    • 基礎力確認問題、模擬問題それぞれ2回ずつ
      • 基礎力確認問題:28点→44点(60点満点)
      • 模擬問題:60点→80点
    • 一応模擬問題だけでなく前段階の教科書部分の方も読みましたが、Silver試験の範囲に限って言えば初見の情報も少なくあまり得るものは無いなという印象でした。Goldの出題範囲のほうがObjectやModuleの継承、include時の挙動などが非常に詳細に解説されていてすごく興味深く、実務でも役に立つなーと思いました。

↑で解きまくったと書きましたが、実際に解いた回数としては、それぞれ問題数に差異はあれど合計11回ということになります。
もちろん解いた後は、問題、回答(選択肢も含めて)に出てきたメソッドはすべて公式リファレンスで確認するというのを繰り返し、よく出てくるメソッドは大体覚えました。
ファイル操作関係のメソッドは、正直実務でもあまり使っていなかったこともあり興味が沸かず、捨て問だと思ってあまり勉強しませんでした(笑)。

所感

あまりRubyの言語としての作り的なところは多くなく、メソッドの暗記が多いなーという印象でした。
もちろん変数定義のルール、スコープや、オブジェクト継承ルールなども出題されましたが、Rubyに限った話ではないのでプログラミング経験がある人であれば簡単に解けるかと。
特に配列、ハッシュ、文字列に関するメソッド(破壊的、非破壊的、似ているメソッドなど)を一通り覚えれば大丈夫だと思います。
(もちろん個人差やその時折々の出題によって変わってくると思いますが。自分のときはそこまで癖のある問題は出ませんでした。)

という感じで続いてGoldの勉強もして行こう〜!と思っていたのですが所属する開発チームでの今年度担当するプロダクトが色々変更があり、あまりRuby on Rails触らないかも。。むしろPythonのほうが触るのでは・・?となっているのでGoldまで取るか悩み中です。。

スティード400を復活 〜スプロケ・チェーン交換編(→結局フロントスプロケだけ)〜

先日プラグ交換を実施した際にスプロケ・チェーンの交換もやってみました。
(と思っていたのですが結局今回はフロントスプロケだけ交換しました。)

kittagon.hateblo.jp

参考にしたサイト

事前にこのあたりのブログを参考にさせていただき、予習しておきました。

seilife.blog.jp

ameblo.jp

ざっくりと以下の手順で実施できそうです。

  1. ジャッキアップ  → 2.の後でもOKですが高さ的にやりづらかったので最初に上げました。
  2. チェーンカット → 工具必要
  3. フロントスプロケカバー外す
  4. フロントスプロケ交換
  5. リアタイヤ外す
  6. リアスプロケ交換
  7. リアタイヤ戻す
  8. チェーンを取り付ける
  9. チェーンを締める
  10. フロントスプロケカバーを戻す

スプロケ・チェーンの適合

この辺りを参考に調査、確認してみました。

https://mtc.greeco-channel.com/honda/steed400_nc26_gear/

www.sunstar-kc.jp

検索結果

純正だとフロント:16丁、リア:45丁、チェーンリンク数:120、チェーンサイズ:525という感じみたいです。

今回は純正と同じセッティングということでこちらを購入することにしました。

あとは、今回は↑のブログでも紹介されていたチェーンカッターを購入して作業することにしました。

チェーンのカット

↑で購入したチェーンカッターはこんな感じで、チェーンにはめてボルトを回せば切れる仕組みみたいです。

チェーンカッター
チェーンカッター

チェーンカッター設置
チェーンカッター設置

この状態でボルトをどんどん回していくと↓のように少しずつボルトの先端がチェーンの結合部を押し込んで行って、穴があきます。(カシメ?っていうんですかね、細かい部品名称を知らないからうまく説明できない)
このときギアを入れておくとチェーンが回らなくてやりやすいです。(そもそもジャッキアップの前にやるべきだった。。)

チェーンカッター途中
チェーンカッター途中

穴があいた後は、ボルトをより長いものに交換して、チェーンの接合部を貫通させて切断します。

ボルト交換
ボルト交換

するとこんな感じで接合部のピンが外れてチェーンが切れます。

チェーンピン
チェーンピン

チェーン切断後
チェーン切断後

これで無事にチェーンが外せました。 チェーンがカットできて外すときはギアをニュートラルに入れておくと外しやすいです。

カット後チェーン
カット後チェーン

フロントスプロケ交換

次にフロントスプロケを交換します。
フロントスプロケを交換するにはエンジンの横のスプロケカバーを外して作業します。
赤枠のところのボルトとピンを外してカバーを手前に引っ張ると外せます。

スプロケカバー
スプロケカバー

カバー上部のピン拡大図

カバー上部のピン
カバー上部のピン

こんな感じで外せました。 スプロケを固定しているボルトを外します。

スプロケカバー外した後
スプロケカバー外した後

ボルトを外すとこんな感じ。
スプロケを固定している金具があるので、これを外します。
めちゃくちゃ錆びついてしまっていたのでCRC556なんかを吹きかけてペンチでゆっくり引っ張ってようやく取れました。

ボルト外し後
ボルト外し後

スプロケ固定金具外した後
スプロケ固定金具外した後

スプロケも引っ張れば外せます。
新旧スプロケ比較↓

新旧スプロケ比較
新旧スプロケ比較

結構厚みに差があります。が、古い方のスプロケは円盤中央部にくぼみがあるので実際に設置したときの厚みは同じです。(のはず)

新旧スプロケ比較厚み
新旧スプロケ比較厚み

新しいスプロケに歯車の山が合わない・・?

ついに新しいフロントスプロケを取り付けようとしたところ問題が発覚しました。
スプロケと、留める金具を重ねてみると、ボルトの位置を合わせると内側の歯車の山が合わなくなり、内側の歯車の山を合わせてしまうとボルトの位置が合わない感じになってました。

スプロケの歯車とボルトの位置
スプロケの歯車とボルトの位置

どう考えてもやり方がわからなくなってしまったので一旦今回は作業終了として、色々と確認することにしました。

以前も↓の記事のキャブ洗浄のやり方を教えてくれた友人に相談しました。
kittagon.hateblo.jp

どうやらこの留め具の金具は、スプロケの軸のところにある溝のところに位置するそう(なので歯車の山は関係なく回転出来る)で、ボルトの位置だけ合わせて留めるそうです。(というかもともと取り付けられてた写真確認したら確かにそうなってたわ。。)

スプロケ軸 溝
スプロケ軸 溝

なので下の感じ(赤がスプロケ、黄色が留め具の位置)

スプロケ軸
スプロケ軸
(ちなみにこれの元画像、友人から説明として送られてきたものなので転載元不明です。。すみません、、なんかまずかったらご連絡ください。。)

というわけで再トライ

後日↑のやり方で再度取り付けを試してみました。
↓の写真の赤枠の溝に留め具をつける感じですね。

スプロケ軸
スプロケ軸

こんな感じで取り付けます。

留め具取り付け
留め具取り付け

ボルトの位置もOKですね。

留め具取り付け正面
留め具取り付け正面

ボルトも取り付けちゃいます。

ボルト取り付け後
ボルト取り付け後

ボルト取り付け後
ボルト取り付け後

こんな感じで無事フロントスプロケ交換ができました。

続いてリアスプロケの交換とチェーンの取り付けも実施しようとしたのですが、後からタイヤの交換もしたいし、ブレーキシューの交換もしたいと思っていたので、今このタイミングでリアタイヤを外してスプロケ交換、戻してチェーンを取り付けてしまうと、また後日外さなくてはいけなくなってしまうので二度手間かと思い、今回のタイミングでは実施しないことにしました。
また後日タイヤ交換、ブレーキシュー交換のタイミングでリアスプロケ交換、チェーンの取り付けは行おうと思います。

まとめ

今回は当初スプロケとチェーンとを一気に交換してしまおうと思っていたのですが、リアのタイヤを外すのが大変そうなのと後からの作業を考えると二度手間になりそうだったので取り敢えずフロントスプロケだけの作業にしました。
とはいえフロントスプロケだけでもいろいろと詰まるポイントがあって苦労しましたが、なんとか完了させられてよかったです。

【Pythyon】Google Photos API を実行するとUnknownApiNameOrVersionのエラー

google-api-python-clientに関しての内容です。

github.com

一年前に実装したGoogle Photosから写真を取得するプログラムをそのまま放置してしまっており、久しぶりに別環境で動かそうと思ったらエラーが出てしまい動かなくなってしまっておりました。

kittagon.hateblo.jp

正しいやり方ではないのかもしれませんが取り急ぎ解決出来たのでメモとして残しておきます。
(Qiita等でも「間違った情報の記事をアップすると全体の質が下がる。初心者は記事を投稿するな」という意見がありますがこちらは個人用の備忘録ですのであしからず。。)

エラーメッセージ

Traceback (most recent call last):
  File "*******/Develop/google-photos/google_photos.py", line 177, in <module>
    main()
  File "******/google-photos/google_photos.py", line 156, in main
    service = discovery.build(
  File "*******/.pyenv/versions/google-photos/lib/python3.9/site-packages/googleapiclient/_helpers.py", line 134, in positional_wrapper
    return wrapped(*args, **kwargs)
  File "*******/.pyenv/versions/google-photos/lib/python3.9/site-packages/googleapiclient/discovery.py", line 273, in build
    content = _retrieve_discovery_doc(
  File "*******/.pyenv/versions/google-photos/lib/python3.9/site-packages/googleapiclient/discovery.py", line 387, in _retrieve_discovery_doc
    raise UnknownApiNameOrVersion("name: %s  version: %s" % (serviceName, version))
googleapiclient.errors.UnknownApiNameOrVersion: name: photoslibrary  version: v1

上記のようなエラーが出てしまい、APIが存在しないと言われてしまっております。
取り敢えず、エラーが出ているところを見てみます。

当該ソースコード

ライブラリのパスはお使いの環境によって変わると思いますが、おおよそ以下のような箇所でエラーが出ているようです。

# .pyenv/versions/google-photos/lib/python3.9/site-packages/googleapiclient/_helpers.py
    def positional_decorator(wrapped):
        @functools.wraps(wrapped)
        def positional_wrapper(*args, **kwargs):
            if len(args) > max_positional_args:
                plural_s = ""
                if max_positional_args != 1:
                    plural_s = "s"
                message = (
                    "{function}() takes at most {args_max} positional "
                    "argument{plural} ({args_given} given)".format(
                        function=wrapped.__name__,
                        args_max=max_positional_args,
                        args_given=len(args),
                        plural=plural_s,
                    )
                )
                if positional_parameters_enforcement == POSITIONAL_EXCEPTION:
                    raise TypeError(message)
                elif positional_parameters_enforcement == POSITIONAL_WARNING:
                    logger.warning(message)
            return wrapped(*args, **kwargs)

        return positional_wrapper

    if isinstance(max_positional_args, six.integer_types):
        return positional_decorator
    else:
        args, _, _, defaults = inspect.getargspec(max_positional_args)
        return positional(len(args) - len(defaults))(max_positional_args)
# .pyenv/versions/google-photos/lib/python3.9/site-packages/googleapiclient/discovery.py", line 273, in build
    params = {"api": serviceName, "apiVersion": version}

    if http is None:
        discovery_http = build_http()
    else:
        discovery_http = http

    service = None

    for discovery_url in _discovery_service_uri_options(discoveryServiceUrl, version):
        requested_url = uritemplate.expand(discovery_url, params)

        try:
            content = _retrieve_discovery_doc(
                requested_url,
                discovery_http,
                cache_discovery,
                serviceName,
                version,
                cache,
                developerKey,
                num_retries=num_retries,
                static_discovery=static_discovery,
            )
            service = build_from_document(
                content,
                base=discovery_url,
                http=http,
                developerKey=developerKey,
                model=model,
                requestBuilder=requestBuilder,
                credentials=credentials,
                client_options=client_options,
                adc_cert_path=adc_cert_path,
                adc_key_path=adc_key_path,
            )
            break  # exit if a service was created
        except HttpError as e:
            if e.resp.status == http_client.NOT_FOUND:
                continue
            else:
                raise e

    # If discovery_http was created by this function, we are done with it
    # and can safely close it
    if http is None:
        discovery_http.close()

    if service is None:
        raise UnknownApiNameOrVersion("name: %s  version: %s" % (serviceName, version))
    else:
        return service
# .pyenv/versions/google-photos/lib/python3.9/site-packages/googleapiclient/discovery.py", line 387, in _retrieve_discovery_doc
    if cache_discovery:
        if cache is None:
            cache = discovery_cache.autodetect()
        if cache:
            content = cache.get(url)
            if content:
                return content

    # When `static_discovery=True`, use static discovery artifacts included
    # with the library
    if static_discovery:
        content = discovery_cache.get_static_doc(serviceName, version)
        if content:
            return content
        else:
            raise UnknownApiNameOrVersion("name: %s  version: %s" % (serviceName, version))

どうやらGoogle APIに接続するためのServiceを生成しようと、discovery.pyの中のbuildメソッドを実行し、その中で更に、discovery_cache/__init__ .py_retrieve_discovery_docを呼び出しているが、contentnullのためraise UnknownApiNameOrVersion("name: %s version: %s" % (serviceName, version))しているようです。
ではなぜcontentnullなのかというと、本来discovery_cache.get_static_doc(serviceName, version)で取得できるはずなのですが、取得出来ていないようです。
そのメソッドを追ってみると以下のような実装になっておりました。

# .pyenv/versions/google-photos/lib/python3.9/site-packages/googleapiclient/discovery_cache/__init__ .py
def get_static_doc(serviceName, version):
    """Retrieves the discovery document from the directory defined in
    DISCOVERY_DOC_DIR corresponding to the serviceName and version provided.

    Args:
        serviceName: string, name of the service.
        version: string, the version of the service.

    Returns:
        A string containing the contents of the JSON discovery document,
        otherwise None if the JSON discovery document was not found.
    """

    content = None
    doc_name = "{}.{}.json".format(serviceName, version)

    try:
        with open(os.path.join(DISCOVERY_DOC_DIR, doc_name), 'r') as f:
            content = f.read()
    except FileNotFoundError:
        # File does not exist. Nothing to do here.
        pass

    return content

DISCOVERY_DOC_DIR配下のdoc_name = "{}.{}.json".format(serviceName, version)ファイル(今回はphotoslibrary.v1.json)を開きに行っているようですが、ファイルが見つからないようです。
当該ディレクトリを見てみると、以下のように様々なAPI向けのJSONファイルが配置してあるのですが、確かにphotoslibrary.v1.jsonが無いようです。

document配下 document配下

google-api-python-clientリポジトリを見てみるとこのdocumentディレクトリにstatic filesが配置されそこからserviceオブジェクトを生成する方式になったのは昨年2020年の11月のようなので、そこから上記のようなエラーが出るようになってしまっているものと思われます。

github.com

他の方法で取得するやり方も不明なので、取り敢えずphotoslibrary.v1.jsonを無理くりで作っちゃうことにしました。
他のサービスのjsongmail.v1.json)などを見てみると、oauth用のためのscopeやエンドポイントなどそのサービスのAPIに関する情報がすべてまとめてあるJSONファイルだったので、その情報を集めていたとこと、GoogleCloudPlatformのリポジトリにほぼ求めていたまんまのjsonがありました。

github.com

中身をざーっと見た感じでも、フォーマットも合っており必要な情報も揃ってそうだったので、乱暴にもそれをそのままリネームして当該ディレクトリに配置してみたところうまく動くようになりました。
↓こんな感じのjsonです。

// *******/.pyenv/versions/3.9.0/envs/google-photos/lib/python3.9/site-packages/googleapiclient/discovery_cache/documents/photoslibrary.v1.json
{
    "auth": {
      "oauth2": {
        "scopes": {
          "https://www.googleapis.com/auth/drive.photos.readonly": {
            "description": "View the photos, videos and albums in your Google Photos"
          },
          "https://www.googleapis.com/auth/photoslibrary": {
            "description": "View and manage your Google Photos library"
          },
          "https://www.googleapis.com/auth/photoslibrary.appendonly": {
            "description": "Add to your Google Photos library"
          },
          "https://www.googleapis.com/auth/photoslibrary.readonly": {
            "description": "View your Google Photos library"
          },
          "https://www.googleapis.com/auth/photoslibrary.readonly.appcreateddata": {
            "description": "Manage photos added by this app"
          },
          "https://www.googleapis.com/auth/photoslibrary.sharing": {
            "description": "Manage and add to shared albums on your behalf"
          }
        }
      }
    },
    "basePath": "",
    "baseUrl": "https://photoslibrary.googleapis.com/",
    "batchPath": "batch",
    "canonicalName": "Photos Library",
    "description": "Manage photos, videos, and albums in Google Photos\n",
    "discoveryVersion": "v1",
    "documentationLink": "https://developers.google.com/photos/",

  ~中略~~

    "servicePath": "",
    "title": "Photos Library API",
    "version": "v1",
    "version_module": true
}

その後も色々と調査してみたのですが、結局正式にはどのようにgoogle photos用のサービスオブジェクトを生成すればよいかわからなかったのですが、go言語のAPIクライアントの方にもgoogle photosに関する定義ファイルは存在していないようなので、何か意図があってのことなのかもしれません。
取り敢えず今回はこれで動くようになったのでしばらくはこのまま運用しようと思います。

【Golang】Google API 複数scopeのクレデンシャルを取得

先日1年ほど前に書いたGoのソースを眺めていたら、Google APIのクレデンシャル取得のところをどうしてこういう実装にしたのか忘れてしまい、戸惑ったので備忘として残しておきます。

Gmail API Go Quickstart に掲載されている実装の

        // If modifying these scopes, delete your previously saved token.json.
        config, err := google.ConfigFromJSON(b, gmail.GmailReadonlyScope)

の箇所が、ここではGmailのReadOnlyのScopeでconfigを取得していますが、これが複数サービス(たとえはGoogle Calender、Google Tasks)への権限も含めて取得したいとなった場合、このConfigFromJSONは使えず、またscopeを複数配列で渡せるようなメソッドもなかった(見つけられなかっただけかもしれません。。) ので、自分で実装を変更してみた、という内容です。

方法

↑の実装ではConfigFromJSONで取得するはずだった、Credentials構造体のscopeの箇所だけ自分で代入した、というだけです。

type Credentials struct {
    Installed struct {
        ClientId                string   `json:"client_id"`
        ProjectId               string   `json:"project_id"`
        AuthUrl                 string   `json:"auth_url"`
        TokenUrl                string   `json:"token_url"`
        AuthProviderX509CertUrl string   `json:"auth_provider_x509_cert_url"`
        ClientSecret            string   `json:"client_secret"`
        RdirectUris             []string `json:"redirect_uris"`
    } `json:"installed"`
}

func getConfig() *oauth2.Config {
    b, err := ioutil.ReadFile("credentials.json")
    if err != nil {
        log.Fatalf("Unable to read client secret file: %v", err)
    }

    var cred Credentials
    json.Unmarshal(b, &cred)
    // tasks.TasksReadonlyScope → tasks.TasksScope
    scopes := []string{gmail.GmailReadonlyScope, tasks.TasksScope, calendar.CalendarReadonlyScope}
    return &oauth2.Config{
        ClientID:     cred.Installed.ClientId,
        ClientSecret: cred.Installed.ClientSecret,
        Endpoint:     google.Endpoint,
        Scopes:       scopes,
        RedirectURL:  cred.Installed.RdirectUris[0],
    }
}

それ以外のToken取得などの処理はチュートリアルのままでOKです。
全体としては以下のような実装になります。

type Credentials struct {
    Installed struct {
        ClientId                string   `json:"client_id"`
        ProjectId               string   `json:"project_id"`
        AuthUrl                 string   `json:"auth_url"`
        TokenUrl                string   `json:"token_url"`
        AuthProviderX509CertUrl string   `json:"auth_provider_x509_cert_url"`
        ClientSecret            string   `json:"client_secret"`
        RdirectUris             []string `json:"redirect_uris"`
    } `json:"installed"`
}

// Retrieve a token, saves the token, then returns the generated client.
func getClient(config *oauth2.Config) *http.Client {
    // The file token.json stores the user's access and refresh tokens, and is
    // created automatically when the authorization flow completes for the first
    // time.
    tokFile := "token.json"
    tok, err := tokenFromFile(tokFile)
    if err != nil {
        tok = getTokenFromWeb(config)
        saveToken(tokFile, tok)
    }
    return config.Client(context.Background(), tok)
}

// Request a token from the web, then returns the retrieved token.
func getTokenFromWeb(config *oauth2.Config) *oauth2.Token {
    authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
    fmt.Printf("Go to the following link in your browser then type the "+
        "authorization code: \n%v\n", authURL)

    var authCode string
    if _, err := fmt.Scan(&authCode); err != nil {
        log.Fatalf("Unable to read authorization code: %v", err)
    }

    tok, err := config.Exchange(context.TODO(), authCode)
    if err != nil {
        log.Fatalf("Unable to retrieve token from web: %v", err)
    }
    return tok
}

// Retrieves a token from a local file.
func tokenFromFile(file string) (*oauth2.Token, error) {
    f, err := os.Open(file)
    if err != nil {
        return nil, err
    }
    defer f.Close()
    tok := &oauth2.Token{}
    err = json.NewDecoder(f).Decode(tok)
    return tok, err
}

// Saves a token to a file path.
func saveToken(path string, token *oauth2.Token) {
    fmt.Printf("Saving credential file to: %s\n", path)
    f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
    if err != nil {
        log.Fatalf("Unable to cache oauth token: %v", err)
    }
    defer f.Close()
    json.NewEncoder(f).Encode(token)
}

func getConfig() *oauth2.Config {
    b, err := ioutil.ReadFile("credentials.json")
    if err != nil {
        log.Fatalf("Unable to read client secret file: %v", err)
    }

    var cred Credentials
    json.Unmarshal(b, &cred)
    // tasks.TasksReadonlyScope → tasks.TasksScope
    scopes := []string{gmail.GmailReadonlyScope, tasks.TasksScope, calendar.CalendarReadonlyScope}
    return &oauth2.Config{
        ClientID:     cred.Installed.ClientId,
        ClientSecret: cred.Installed.ClientSecret,
        Endpoint:     google.Endpoint,
        Scopes:       scopes,
        RedirectURL:  cred.Installed.RdirectUris[0],
    }
}

func main() {
    config := getConfig()
    client := getClient(config)
    srv, err := gmail.New(client)
        //
        // gmail取得処理
        //
    srv, err := calendar.New(client)
        //
        // calendar取得処理
        //
    srv, err :=tasks.New(client)
        //
        //task取得処理
        //
}

こんな感じで一つのトークン、Configで複数サービスへの接続ができるようになります。
もちろん一つのクレデンシャルに権限を与えすぎるのもリスクはありますが自分で管理するアプリケーションの中で接続サービスごとにトークンを分けるのも大変なので、自分はこうしてしまっています。

【Golang】Gmail API メール本文のデコード

以前できなくて悩んでいた所が最近再度見てみたらサクッと解決できたのでメモ。

結論

base64パッケージのStdEncodingではなくURLEncodingを使えばOK。
以前はStdEncodingを使って、うまくできない、途中で途切れてしまう〜と悩んでいました。
以下の感じで取れます。

message, err := srv.Users.Messages.Get(user, m.Id).Format("full").Do()
message, err := srv.Users.Messages.Get(user, m.Id).Format("full").Do()
if err != nil {
    log.Fatalf("Unable to retrieve messages: %v", err)
}
body := message.Payload.Body
data := body.Data
decodedMessage, _ := b64.URLEncoding.DecodeString(data)

GmailAPIの仕様

については今回は割愛しますが、自分は主にusers.messages.listメソッドにクエリーを投げてmessageIdを取得し、取得したmessageIdusers.messages.listへのリクエストに投げてメール本文を取得、という感じでつかってます。

developers.google.com

developers.google.com

取得したメールのオブジェクトの構造は以下URLの通りなので、この通りbody.dataまでを辿って上記の通りデコードすれば本文が取得出来ます。

developers.google.com

スティード400を復活 〜プラグ交換編〜

これまでにキャブの洗浄とサスペンション交換、オイル、エレメント交換を実施しました。
コロナ禍でなかなか地元に帰ることができず時間が立ってしまいましたが、先日久しぶりに作業できたのでまとめておきます。

オイル交換してからすでに一年が経過してしまっているので結局乗り出す前には再度交換しないといけませんが。。。
作業する順序を少し考えないと。。

kittagon.hateblo.jp

kittagon.hateblo.jp

前回エンジンがまたかからなくなってしまっておりプラグがダメになっている可能性があったので、今回交換してみました。

プラグの場所

ティードにはVツインエンジンの前後それぞれ左右に一つづつ、計4つのプラグがあります。

左前
左前

左後ろ
左後ろ

右前
右前

右後ろ
右後ろ

見ての通り、左前と右後ろの位置のプラグはなかなかエグい位置にプラグが刺さっておりかなり作業しづらいので、少し工夫が必要です。

右前と左後ろの位置

この位置は簡単です。
こんな感じでプラグカバーを外せばプラグがむき出しになるので、プラグレンチで外して交換するだけです。

右前プラグカバー外したところ
右前プラグカバー外したところ

適合するプラグは Webike とかで検索してみるとわかります。

今回はこれを購入しました。

また、プラグの対辺寸法は18mmなのでプラグレンチはこちらを購入しました。
純正の車載工具があるとプラグ交換がとてもやりやすいらしいのですが、自分は持っていなかったのでこうした社外品で頑張りました。

スティードのプラグは深い位置にあるのでこういった長めのものがあると回しやすいです。
でも左前と右後ろはそれでもやりづらいですが。。

このプラグをプラグレンチで外すと簡単に取れます。

右前プラグ外すところ
右前プラグ外すところ

新しいものと比較すると汚れが一目瞭然。。。

新旧プラグ比較
新旧プラグ比較

中に埃が入らないよう素早く新しいものを取り付けます。

右前新プラグ
右前新プラグ

プラグカバーをもとに戻したら完了です。

右前プラグカバー取り付け
右前プラグカバー取り付け

左後ろの位置も比較的やりやすい場所にあるので同様に外して新しいものを取り付ければ簡単に交換できます。

左前と右後ろの位置

右後ろのプラグカバーは、繋がっているケーブルがプラスチックの留め具で固定されているので、それを外してから外す感じになります。

右後ろプラスチック留め具
右後ろプラスチック留め具

マイナスドライバーなどで簡単に外れます。

右後ろプラスチック留め具外したところ
右後ろプラスチック留め具外したところ

左前と右後ろの位置かなり作業がしずらいところにあるので、プラグレンチだけでは回せません。
このようにプラグレンチを挿しても穴にすっぽり収まってしまい長さが足りず回すことができません。

左前プラグレンチ指したところ
左前プラグレンチ挿したところ

そこで今回は六角レンチをジョイントして回すことにしました。
使っているプラグレンチの片側が16mmのため、16mmの六角レンチを購入しました。

これをプラグレンチに挿せば狭い場所でも回すことができます。

六角レンチ挿したところ
六角レンチ挿したところ

左前六角レンチ挿したところ
左前六角レンチ挿したところ

右後ろ六角レンチ挿したところ
右後ろ六角レンチ挿したところ

これでレンチで回すことはできるようになるのですが、穴が深いので緩めたあとの回収と取り付けも気をつけないと大変です。

緩めたあとは↓のようにラジオペンチを突っ込んで挟めれば取れます。
(素人なのでわからないのですがこういうのを回収できるような磁石がついた工具とかあるもんなんですかね・・・?)

右後ろラジペンでプラグ回収
右後ろラジペンでプラグ回収

ただ、ここでラジペンでしっかり挟めていない状態でプラグが外れてしまうと下のように空洞?になっているところにプラグが落ちてしまい取るのが大変になってしまいます。

右後ろプラグ落ちてしまったところ
右後ろプラグ落ちてしまったところ

なので、プラグレンチで緩めきったあとは下に紙を挟み込んでおいて、落ちないようにしておいてからプラグレンチを引き抜き、プラグ回収をしたほうがやりやすいです。

左前プラグレンチの下に紙
左前プラグレンチの下に紙

右後ろプラグの下に紙
右後ろプラグの下に紙
(写真の場所がそれぞれ違うところを写したものになってますが)

こうするとうまくプラグが回収できます。

プラグを取り付ける際も、穴が深いので、プラグを取り付けてからレンチで締めるということができないので、プラグをレンチにはめた状態で取り付けるとうまくできます。

左前プラグ取り付け
左前プラグ取り付け

こうして4箇所プラグ交換をすれば完了です。

終わりに

今回始めてプラグ交換を実施してみました。
かなり難しい位置にあるプラグなので苦労しましたが、ちょうどよい道具が見つけられたのでなんとか交換できました。
肝心なエンジンがかかるかどうかの確認はできていないのでまたの機会に確認しようと思います。
また、このときスプロケ、チェーン交換もやってみた(結局できなかった。。)のでそちらも後日まとめたいと思います。