Google Calendar API v3でMeetのURLを発行する。

Right-Click-to-Calendar v3.9.2をリリースしました。
Right-Click to Calendar – Chrome ウェブストア
カレンダーに登録したときにMeetsのURLも発行してほしいと要望がきたので対応しました。カレンダーのAPIでMeetのURLが発行できるのを今回初めて知りました。Meet無料でも使えるのはありがたいですね。
Events: insert  |  Calendar API  |  Google Developers

const body = {
  "start": {
    "dateTime": "2020-09-12T02:05:00.000+09:00"
  },
  "end": {
    "dateTime": "2020-09-12T02:05:00.000+09:00"
  },
  "conferenceData": {
    "createRequest": {
      "requestId": Math.random().toString(32).substring(2), // ランダムな文字列
      "conferenceSolutionKey": {
        "type": "hangoutsMeet"
      }
    }
  }
};

const xhr = new XMLHttpRequest();
xhr.onloadend = () => {
  if (xhr.status === 200) {
    const data = JSON.parse(xhr.responseText);
    if (data.conferenceData.createRequest.status.statusCode === "success") { // 発行されたMeetのURL取得
      console.log(data.conferenceData.entryPoints[0].uri)
    }
  }
};
xhr.open('POST',
  "https://www.googleapis.com/calendar/v3/calendars/" + カレンダーのID + "/events?conferenceDataVersion=1", // conferenceDataを送信するためのフラグを有効にする
  true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Authorization', 'Bearer ' + accessToken);
xhr.send(JSON.stringify(body));

カレンダー共有すれば人を招待する必要はないし、URLを発行する機能だけつけました。

Chromeの拡張、リリース時の審査に1ヶ月くらいかかるときいていたのですが、今回は1営業日で完了しました。よかったです。
次のリリースあるときはNuxt.jsで実装しなおす予定です。

あと、今WordPress捨てようと思っていて、Gatsbyに移行するために記事をmdに落とし込むところまではやったのですが、今使っているテンプレートをGatsbyに変換するところで詰まったので、時間をかけて頑張ろうと思います。Reactもやっていこう。

この記事コナンのエピソードワン見ながら書いてたのですが、やっぱり新一のどこがいいのかわかんねえなと思いました。主要人物の紹介違和感なく全部ぶち込んでて構成上手でしたね。すごい。

Vue全然知らないけどNuxt.js v2.14.4 + JavaScriptでTodoアプリを作る。

はじめに

フロントエンドは素のJavaScript(ES6)とjQueryしか使ったことがないのと、雨後の筍みたいにフレームワークが乱立していて、どれから手を付けていいのかわからず困っていたのですが、Nuxt.jsが結論のようなのでNuxt.jsを使うことにしました。
チュートリアルといえばTodoアプリということで、「Nuxt.js Todo」で検索するとたくさんサンプルが出てくるのですが、どれもまともに動かないか、Deprecatedになっている方法を使っていたり、動作確認環境すら書いていないものが多いので、やはり信じられるのは公式ドキュメントだけという結論に達しました。今回は公式ドキュメントのサンプルを参考にした上で、多少機能を追加したものを作成しました。
似たような記事は山ほどあるのですが、どの記事を読んでもだめだった人向けのメモとして残しておきます。
なお、使用する言語はJavaScriptです。

今回作成するアプリのイメージ

file

ほぼ公式ドキュメントのサンプルを写経していますが、日付と検索機能を追加しています。
file

動作確認環境

  • Ubuntu 20.04.1 LTS (GNU/Linux 4.19.104-microsoft-standard x86_64)
    • Windows 10 Pro ver 2004のWSL2上で動いてるUbuntu
    • Windows向けのNode.jsもありますが、Linux環境があるならそれを使うのが一番わかりやすいと思います。
  • Node.js v12.18.3
  • Nuxt.js v2.14.4
  • yarn 1.22.5

WSL2を使用する場合の注意事項

WSL2を使用する場合、動作させるアプリケーションを/mnt/*以下に作成すると、ビルドが終わらない・ホットリロードが効かなくなる問題が発生します。私はこれで半日つぶしました。Windowsくんさぁ…。
WSL2の実行環境の中 /home/* にアプリを作れば問題を回避できます。私はVSCodeを使いたいので、Remote Development機能を使いました。
以下参考記事です。

アプリ作成

以下のコマンドを実行してアプリを作成します。Todoアプリは全部初期設定でも動作に影響ないので、全部初期設定にしました。

$ npx create-nuxt-app sample-app

参考:Nuxt.jsを使うときに、SPA・SSR・静的化のどれがいいか迷ったら – Qiita

ディレクトリ構造の参考サイト

アプリ動作確認

$ cd sample-app
$ yarn dev

http://localhost:3000 にアクセスして、デフォルト画面が出てくればOKです。
file

また、今後の開発速度に1億倍くらい影響が出てくるため、ホットリロードが効くかどうかをこの段階で検証しておいたほうがいいです。http://localhost:3000 を開いた状態で、pages/index.vue内の適当なテキストを修正して、ブラウザの更新無しで修正した内容が反映されているかを確認してください。
反映されていなければエラー内容で検索してください。

ストアの追加

Todoの状態を管理するためのストアをモジュールモードで使用します。クラシックモードはDeprecatedなのですが、大体の解説記事がクラシックモードでコードを書いていて、Nuxt.js v2で使うと警告 Classic mode for store/ is deprecated and will be removed in Nuxt 3 が出てきます。
Vuex ストア – NuxtJS
公式ドキュメントのサンプルほぼそのままですが、サンプルが間違っているので、一部修正します。また、idは採用せずに、todoの作成日時のDateを保持するよう変更しました。

// store/todos.js
export const state = () => ({
    list: []
})

export const mutations = {
    add(state, text) {
        state.list.push({
            text,
            done: false,
            created: new Date(),
        })
    },
    remove(state, todo) { // 公式ドキュメントではtodoをオブジェクトで受け取っているが、呼び出し元がオブジェクトで渡していないので正しく動作しない。
        state.list.splice(state.list.indexOf(todo), 1)
    },
    toggle(state, todo) {
        todo.done = !todo.done
    },
}

templateの修正

作成時の日付を文字列に変換する関数と、絞り込み用の関数を追加しました。検索用のテキスト入力欄に文字が入力されると、その文字列を含むリストを返却しています。

<template>
  <section class="container">
    <ul>
      <li v-for="(todo, index) in todos" :key="index">
        <input :checked="todo.done" @change="toggle(todo)" type="checkbox" />
        <span :class="{ done: todo.done }">{{ todo.text }} {{ getStringFromDate(todo.created) }}</span>
        <button @click="removeTodo(todo)">remove</button>
      </li>
    </ul>
    <p>
      <input
        :value="todoText"
        @input="todoText = $event.target.value"
        placeholder="✨What needs to be done?"
      />
      <button @click="addTodo">add</button>
    </p>
    <p>
      <input
        :value="searchText"
        @input="searchText = $event.target.value"
        placeholder="🔍Search your todo."
      />
    </p>
  </section>
</template>

<script>
import { mapMutations } from "vuex";

export default {
  data() {
    return {
      todoText: "",
      searchText: "",
    };
  },
  computed: {
    todos() {
      if (this.searchText.length > 0) {
        return this.$store.state.todos.list.filter((item) =>
          item.text.toLowerCase().includes(this.searchText.toLowerCase())
        );
      }
      return this.$store.state.todos.list;
    },
  },
  methods: {
    addTodo() {
      this.$store.commit("todos/add", this.todoText);
      this.todoText = "";
    },
    ...mapMutations({
      toggle: "todos/toggle",
    }),
    removeTodo(todo) {
      this.$store.commit("todos/remove", todo);
    },
    getStringFromDate(date) {
      return (
        date.getFullYear() +
        "-" +
        ("00" + (date.getMonth() + 1)).slice(-2) +
        "-" +
        ("00" + date.getDate()).slice(-2) +
        " " +
        ("00" + date.getHours()).slice(-2) +
        ":" +
        ("00" + date.getMinutes()).slice(-2) +
        ":" +
        ("00" + date.getSeconds()).slice(-2)
      );
    },
  },
};
</script>

<style>
</style>

所感

最初はわけがわからずかなり戸惑ったのですが、一回書き始めてみるとかなり直感的にDOMの内容を操作できますし、素のJavaScriptで同じことをやろうとすると絶対こんなにきれいに書けないので、これは流行るのもわかるフレームワークだと思いました。久々に書いていて楽しかったです。
ソースコードはGitHubにも上げておきました。
https://github.com/retrorocket/nuxtjs-2-sample-todo-app/

Windows 10 Home (ver 2004, build 19041)でKubernetesとNginx Ingress Controllerを使う。

五飛、教えてくれ……俺たちはあと何回Windowsをクリーンインストールすればいい?

May 2020 Updateの上書きアップデートに失敗して、ログイン直後にハングアップするようになったのでクリーンインストールしました。セーフモードだと起動するのでサービスかドライバのせいだと思うのですが、調査するよりクリーンインストールしたほうが早いです。サインイン画面からShiftキー押しっぱなし -> 電源 -> 再起動 でセーフモードが使えることを今回初めて知りました。
Windows 10 のセーフ モードおよびその他のスタートアップ設定

Windows 10 HomeでもDocker Desktopが動作するようになったのでKubernetesでNginx Ingress Controllerの動作確認をしました。

使用したバージョン

  • Docker Desktop community: 2.3.0.4(46911) stable
  • Docker Engine: 19.03.12
  • Kubernetes: v1.16.5

WSL2を有効にする

  1. コントロールパネルの「Windowsの機能の有効化または無効化」から「仮想マシンプラットフォーム」を有効化
  2. WSL 2 Linux カーネルの更新 | Microsoft Docs の手順に従いカーネルを更新する
  3. 管理者のPowerShellで wsl --set-default-version 2 を実行

Docker Desktopのインストール

Docker Desktop for Mac and Windows | Docker
インストーラーからインストールします。インストール時に「WSL2を使用するか?」みたいなチェックボックスがあるのでオンにします。
インストール後にチュートリアルとしてテスト用のコンテナ作成が実行できます。コンテナが立ち上がるか確認して、問題なければ停止します。

Kubernetesの有効化

  1. Settings->Kubernetes->Enable Kubernetesを選択
  2. kubectl get nodes でnodeの状態が取得できるか確認。シングルノードなのでmasterしかない。
    PS C:\Users\mail\Documents\pods> kubectl get nodes
    NAME             STATUS   ROLES    AGE    VERSION
    docker-desktop   Ready    master   154m   v1.16.6-beta.0

Nginx Ingress Controllerのインストール

Installation Guide – NGINX Ingress Controllerの手順を実行

kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v0.35.0/deploy/static/provider/cloud/deploy.yaml

ingress controllerの起動確認

PS C:\Users\mail\Documents\pods> kubectl -n ingress-nginx get services,deployments,pods
NAME                                         TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)                      AGE
service/ingress-nginx-controller             LoadBalancer   10.102.109.2   localhost     80:31301/TCP,443:31691/TCP   111m
service/ingress-nginx-controller-admission   ClusterIP      10.99.131.11   <none>        443/TCP                      111m

NAME                                       READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/ingress-nginx-controller   1/1     1            1           111m

NAME                                            READY   STATUS      RESTARTS   AGE
pod/ingress-nginx-admission-create-4rz86        0/1     Completed   0          111m
pod/ingress-nginx-admission-patch-zwgdm         0/1     Completed   0          111m
pod/ingress-nginx-controller-5947756d78-jp7q4   1/1     Running     0          111m

動作確認用のDeploymentとServiceとIngressのapply

動作確認用のDeploymentとServiceとIngressをapplyします。今回は「Docker/Kubernetes 実践コンテナ開発入門」という本でチュートリアル用に公開されているgihyodocker/echo:latestのイメージを使用しました。8080ポートにアクセスすると「Hello Docker!!」というテキストが返ってくるアプリケーションです。Podの数は3つにしておきました。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: echo-deployment
  labels:
    app: echo
spec:
  replicas: 3
  selector:
    matchLabels:
      app: echo
  template:
    metadata:
      labels:
        app: echo
    spec:
      containers:
      - name: echo
        image: gihyodocker/echo:latest
        ports:
        - containerPort: 8080

動作確認用にClusterIPでServiceを作って80番ポートで待ち受け。

kind: Service
apiVersion: v1
metadata:
  name: echo-service
spec:
  type: ClusterIP
  ports:
  - name: http
    port: 80
    targetPort: 8080
    protocol: TCP
  selector:
    app: echo

/echo へのアクセスを振り分けるように設定。

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: test-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
  - http:
      paths:
      - path: /echo
        backend:
          serviceName: echo-service
          servicePort: 80

動作確認

http://localhost/echo にアクセスすると「Hello Docker!!」が返ってきました。成功です。

所感

Windows Homeでも手軽にDockerやK8sが試せるようになって本当に素晴らしいなと思いました。
ちなみにWindows 7以降のProライセンスを所有しているとWindows 10もProにできるそうで、8年前に購入した8のProライセンスで試してみたら普通にアップグレードできました。この記事何のために書いたんでしょうね。

QiitaのTシャツが届きました。

Qiita夏祭り2020オンライン プレゼント企画 結果発表! – Qiita Blog
Qiita賞で優秀賞をいただきました。ありがとうございます。

昔こういう記事を書いたのですが
だから私はQiitaに投稿しない。 – return $lock;
Qiitaに対するスタンスは変わってないです。というか、ガイドラインが曖昧すぎてプログラミングに関係ある記事とない記事の線引がわからないので投稿できないです。思いつきでメモを書くこともありますが、メインは個人ブログでやっていこうと思います。

Python3でポケモン剣盾のバトルデータをurllibで取得し、SQLite3に書き込む。

シンタックスハイライターをSyntaxHighlighter EvolvedからPrism.jsに移行したので、テストを兼ねた記事です。
過去記事を移行するかは検討中です。 すべての記事をPrism.jsに移行しました。

ポケモンのバトルデータのJSONをurllibで取得->SQLite3に書き込むスクリプト。テーブル作ってる箇所とか適当ですね。

2020/08/17追記

せいかくが取得できるようになったので、対応しました。

import json
import urllib.request
import sqlite3
from contextlib import closing

### 設定
season = 9
battle_rule = 1 # シングルなら1 ダブルなら2
dbname = 'single.db'

### ランクマの情報を取得
season_id = 10000 + season * 10 + battle_rule
user_agent = 'Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Mobile Safari/537.36'

url = 'https://api.battle.pokemon-home.com/cbd/competition/rankmatch/list'
headers = {
    'User-Agent': user_agent,
    'countrycode': '304',
    'authorization': 'Bearer',
    'langcode': '1',
    'accept': 'application/json, text/javascript, */*; q=0.01',
    'content-type': 'application/json',
}
data = {
    'soft': 'Sw',
}

req = urllib.request.Request(url, json.dumps(data).encode(), headers)
body = ''
with urllib.request.urlopen(req) as res:
    body = json.load(res)

detail = body['list'][str(season)][str(season_id)]
ts2 = detail['ts2']
rst = detail['rst']

### 図鑑情報の読み込み
pokedex = ''
#  https://resource.pokemon-home.com/battledata/js/bundle.js の図鑑と持ち物情報を抜き出してjsonにしたものを読み込む
with open('./bundle.json', 'r') as json_open:
    pokedex = json.load(json_open)

### ポケモン情報の取得(今回はポケモンのわざともちものととくせいとせいかくの採用率を取得する)
headers = {
    'User-Agent': user_agent,
    'countrycode': '304',
    'authorization': 'Bearer',
    'langcode': '1',
    'accept': 'application/json, text/javascript, */*; q=0.01',
}

with closing(sqlite3.connect(dbname)) as conn:
    c = conn.cursor()

    ### テーブル作成
    c.execute( '''create table waza (name text, waza text, adoption_rate real)''')
    c.execute( '''create table item (name text, item text, adoption_rate real)''')
    c.execute('''create table tokusei (name text, tokusei text, adoption_rate real)''')
    c.execute('''create table seikaku (name text, seikaku text, adoption_rate real)''')

    for i in range(1, 6):
        url = f'https://resource.pokemon-home.com/battledata/ranking/{season_id}/{rst}/{ts2}/pdetail-{i}'
        req = urllib.request.Request(url, headers=headers)
        pdetail = ''
        with urllib.request.urlopen(req) as res:
            pdetail = json.load(res)

        for pokenum in pdetail.keys():
            for p_detail_id in pdetail[pokenum].keys():
                name = pokedex['poke'][int(pokenum) -1]
                if p_detail_id != '0': # 0以外はフォルム・性別・リージョンetc違いなので分けて扱う
                    name = name + p_detail_id
                for pokewaza in pdetail[pokenum][p_detail_id]['temoti']['waza']:
                    sql = 'insert into waza (name, waza, adoption_rate) values (?,?,?)'
                    waza = (name, pokedex['waza'][pokewaza['id']], pokewaza['val'])
                    c.execute(sql, waza)
                for pokeitem in pdetail[pokenum][p_detail_id]["temoti"]["motimono"]:
                    sql = 'insert into item (name, item, adoption_rate) values (?,?,?)'
                    item = (name, pokedex['item'][pokeitem['id']], pokeitem['val'])
                    c.execute(sql, item)
                for poketokusei in pdetail[pokenum][p_detail_id]['temoti']['tokusei']:
                    sql = 'insert into tokusei (name, tokusei, adoption_rate) values (?,?,?)'
                    tokusei = (name, pokedex["tokusei"][poketokusei["id"]],  poketokusei["val"])
                    c.execute(sql, tokusei)
                for pokeseikaku in pdetail[pokenum][p_detail_id]['temoti']['seikaku']:
                    sql = 'insert into seikaku (name, seikaku, adoption_rate) values (?,?,?)'
                    seikaku = (name, pokedex["seikaku"][pokeseikaku["id"]],  pokeseikaku["val"])
                    c.execute(sql, seikaku)

    conn.commit()