Mi To Do その後3

テトリスかアートロジック系のToDoビューにしたいのだがアイデアが浮かばないので手を少し入れただけ。ChromeのURLアクセス履歴取得処理を調整中。

Mi To Do さらにその後

どちらかというとパズル系にしたいのだが。それとは別に音声認識部を追加改良中。

assets

https://opengameart.org/content/war-on-water-gfx
https://opengameart.org/content/the-battle-for-wesnoth-water-animation

Reactive

父: コンピュータはすごいぞ。論理を処理する機械だ。2+3と入れれば、5と出る。

子: すごいね! じゃ5と2を入れれば、3が出るね。

父: いや出ないよ。足せるだけだ。

子: え? 何で? 論理的機械なんでしょ?

Reactiveについての私の理解はそんなものである。逆から操作しても整合した結果が出るフレームワーク。

既視感があると思い浮かべてみると、物理世界をエミュレーションするゲーム物理エンジンとかに似ている気がする。

元々Vector演算をがっつりコンピュータに持ってきたのもシミュレーション分野→CGやゲームへみたいなところだし。

コンピュータ技術がこのあたりの整合性が現実的速度で出来るようになった結果、コンピューターは手続きやフローを幾何的に理解出来る空間感覚がある人が有利だったとこから、集合を変換演算していく数学的感覚が高い人が有利になってきたような気がする。

MS To Do クローンその後

まだバグは少々残っているが、いざとなればGoogle CalendarのTaskで操作すればよいのでまぁ困らぬ。

ネック端末制御用サーバーアプリのフロントに置く形でPWAアプリ可能にしているが、サーバーアプリそのものがまだ開発用serveに載せているので、今PWA化しても面倒かなぁ。

こっちを作るのがメインの目的ではないので、ネック端末側の調整に戻ろう。

最近ごそごそ作っていた個人情報端末

最近夜中にごそごそ作っていたもの。

スケジュールやニューストレンドをボタン一つで読み上げるデバイス。

案そのものは考えていて最近無料系Webサービスのアクセス処理を作っていたのだが、TwitterでわりとよさげなSharpのネックスピーカーが出てたので、それと組み合わせたもの。

動画ではスケジュールなしと言っているがその後にTwitterのトレンド語とニュースのヘッドラインを読み上げてます(長く見てもしかたないので少し縮めていますが)

そのほかにはメール件数読み上げ(送り先とそこからの件数)と時報読み上げもできます。ToDoは作り直し中。

音声認識での操作も考えて少しやったのだが、現実的には誤認識でかえって面倒になるので、Bluetoothのボタン操作を情報が欲しいときに押したほうがよさげ。細かい操作を指示するより、単純に押す時間帯に合わせて、読み上げる情報を最適に変えるほうが役立つ感じ。

はやりの腕時計PDAより、こっちを個人の好みにカスタマイズしたほうが面白そうである。

最終的には光速エスパーのチカみたいになったらいいなぁ。

[node]Gmailリマインダーを作ろうとして悩んだ話

最近自分のはやりで、日常PC作業をできるだけ自動化しようとしている。朝のデイリー作業をPowerShellでまとめたりとか。

その中でGmailの通知を個人アプリサーバーあたりに集約して適切に振り分けようと思ったのだが、思ったよりGmailへのプログラム操作が面倒だった。という話。

自分にとっての結論項

  • gmailアクセスはgmailAPIが無難
  • gmail API認証は個人利用なら問題ない
  • 参考としてわかりやすかったのは npm gmail-tester

要件としては、

  • nodejsで軽量にやりたい。
  • 宛先、送り先、サブジェクト、日時くらいが取得できれば十分
  • ネットショップ用とか複数のアカウントがある
  • 無料サービスの範囲でなんとかしたい

有料でよければ、ネットにあるIOT系の通知制御サービス(zapierとか)でまとめるのが悩まなくて済む。でも有料にひっかかった後とかにいろいろ悩むので、勉強かねて作ってみることに(これビジネス的に考えると、金がかかっても即立ち上げ、即フィードバック、即リリースが出来ないとダメな訳で、それだけ考えても自分が起業家やマネージャーに向かないと思い知る訳です)

Gmailへのプログラムからのアクセスにはいくつか方法があり、

  • Gmail APIを叩く
  • GmailにIMAPあたりの設定をしてIMAPクライアントでアクセスする
  • Gmail API+Cloud Pub/SubでGmailの通知だけプッシュさせる

上に上げたzapierとかの外部サービスを使うのも手だし、無理筋ならSeleniumでブラウザ自動制御(ただしGmailとかはおそらくセキュリティがっちりだろうし)とか。でもGmail APIかIMAPあたりならさくさく出来るのではないかと思ったのです。

Nodeで使える部品は、gmail APIを叩くなら、gmail-tester あたり、IMAPを叩くなら imap-simple あたり。ネットで調べると GCP のPub/SubサービスでGmailへの通知を取得する方法もあるとか。

https://developers.google.com/identity/protocols/oauth2?csw=1

でも、Gmailそのものが長くから使われてレガシー的になってきた+結構セキュリティが重要なGoogleの基幹サービスでいろいろガチガチ+仕様や手法が古くからあったのでネットの情報も新旧混じって今は使えない方法も結構ある → どれが今よい方法なのかがよくわからない。。。

GCPからGmail APIを有効へ
認証情報ウィザードから認証情報を作ろうとしたら、
認証には6週間かかるとか、審査料もかかるとか画面に表示が出た。。。


gmail-tester がそのまま用途に合致してたので触ってみたのだが、gmail APIの認証情報を作る段階で、有料とか何が月もかかるとか出てきて「gmailの認証設定結構面倒?」と考えて一旦imap-simpleでIMAP接続を作る方向にしました。

でもこっちはこっちで今時IMAPでGmailをアクセスしようとするとThunderbirdなどのきちんとサポートされているメーラーなら問題はないが、取り急ぎIMAP的なツールだとGmailの設定でセキュリティレベルを下げる設定が必要になるのがわかった。。。

動くはずの呼びだしでIMAPの処理が失敗するので結構悩み込みセキュリティレベル設定の問題にやっとで気づいて、取りあえずつながったけど、今度はgcpのサーバーにデプロイすると「海外からのアクセスでブロックしました」とか結構いやらしいセキュリティ通知が出る。。。

これIMAPままだと後々までこういう通知が来まくってどんどん面倒臭くなる話だなと推察。あきらめてgmail APIで再検討をしなおしました。

一旦諦めた認証情報をいろいろ試してみると規約上は個人利用する分には問題ないとの記述でなんとかこの方向で進めることにする。
https://support.google.com/cloud/answer/9110914#restricted-scopes

Google 翻訳画面

ただし認証操作時に警告画面が出るがこれを消すにはやはり面倒な審査とお金がいるらしいと解釈。でもなんとか読み込み権限までのoauthの認証ファイルは手に入るな。

最近のGoogle系APIは自前アカウント上でgcpのappサービスに上げる分には自動認証されてあまり考え込まずに済むはずだが、gmailAPIは古いため最近のGoogle系APIと比べてちょっと癖があり、自動認証は通らないっぽい。複数アカウントも操作したいので、結局きちんとoauthを通さないとダメそう。oauth系認証は最近twitterおもちゃを作った関係で大枠は分かる。じゃやっぱり認証情報とアクセストークンという2つの情報をブラウザ経由で必要になるはず。

改めて具体的にoauthのトークン情報の方はどうやって取得するのがよいのか? サーバーサイドで処理したいのに認証操作をどうするかな、と思った時、gmail-tester と辻褄があった。

( from https://www.npmjs.com/package/gmail-tester )
1. Save the Google Cloud Platform OAuth2 Authentication file named credentials.json inside an accessible directory (see instructions below).
2. In terminal, run the following command:
node <node_modules>/gmail-tester/init.js <path-to-credentials.json> <path-to-token.json> <target-email>
<path-to-credentials.json> Is the path to OAuth2 Authentication file.
<path-to-token.json> Is the path to OAuth2 token. If it doesn't exist, the script will create it.

gmail-tester はローカルのコマンドでトークン取得を行うユーティリティが付いている。これでトークン情報をブラウザ経由でoatuh認証して取得してくれって話。ああやっぱり考えてあるなと納得。

import {Injectable} from "@tsed/di";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const gmail_tester = require("gmail-tester");

export interface MailTitle {
  from: string,
  subject: string,
  receiver: string,
  date: string
}


@Injectable()
export class GmailApiService {
  public async getMessage(): Promise<MailTitle[]> {

    return await gmail_tester.get_messages(
        "認証ファイル.json",
        "トークンファイル.json",
        {
          after: new Date(2020, 8, 6),
        }
    )
  }
}

無事gmail-testerで見出しの取得ができたら、ついででpub/subも通したくなった。gmail-testerがどんなふうに認証処理を通しているかを参考にする。認証ファイルと取得したトークンファイルを使ったoauthというのはわかったので、悪さをしない程度の注意しつつ処理を整理。

import {Injectable} from "@tsed/di";
import {FirestoreService} from "./FirestoreService";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const {google} = require("googleapis");

export interface MailTitle {
  To: string | undefined;
  From: string | undefined;
  FromAddress: string | undefined;
  Subject: string | undefined;
  Date: string | undefined;
}

@Injectable()
export class GmailApiService {
  constructor(private firestoreService: FirestoreService) {
  }  
/**
   * Gmail API watchを発行して、google のpub/subに送信する
   */
  public async watch(account: string) {
    const gmail_client = await this.getGmailClient(account);
    if (gmail_client) {
      const res = await gmail_client.users.watch({
        userId: `${account}`, //'me',
        requestBody: {
          // Replace with `projects/${PROJECT_ID}/topics/${TOPIC_NAME}`
          topicName: 'projects/xxxxxxxxx/topics/yyyyyyy',
        },
      });
      console.log(`watch res:${JSON.stringify(res)}`)
    }
  }

  /**
   * 内部的にGmailアドレスからAPIインスタンスを取得する
   * @param account
   */
  async getGmailClient(account: string): Promise<any> {
    const setting = await this.firestoreService.getAccount(account);
    if (!setting) {
      return undefined;
    }
    const client = JSON.parse(setting.get("client")).installed;
    const token = JSON.parse(setting.get("token"));
    const oAuth2Client = new google.auth.OAuth2(
        client.client_id,
        client.client_secret,
        client.redirect_uris[0]
    );
    oAuth2Client.setCredentials(token);
    return google.gmail({version: "v1", auth: oAuth2Client});
  }
// 別途コントローラーを付けて、pub/subはそこで受け付けて、historyIdを取得する
  /**
   * Controllerからpubsub通知を受けたら、historyIdから始まる一覧で、見出し,From,Toなどのリストを取得する
   * @param account
   * @param historyId
   */
  public async getMailTitles(account: string, historyId: string): Promise<MailTitle[]> {
    try {
      const gmail_client = await this.getGmailClient(account);
      if (gmail_client) {
        const setting = await this.firestoreService.getAccount(account);
        if (!setting) {
          return [];
        }
        const recentHistoryId = setting.get("recentHistoryId") || (parseInt(historyId) - 200).toString();  //  保存がないなら200くらい引いた値で取得する
        await this.firestoreService.saveAccount(account, "recentHistoryId", historyId)
        const ret = (await this.getHistory(account, gmail_client, recentHistoryId) as any)
        if (ret.history) {
          const history = ret.history;
          await this.firestoreService.saveTestLog(`history${recentHistoryId}`,{a:JSON.stringify(history)} )
          const idList: string[] =
              history.map((h: any) => h.messages.map((k: any) => k.id)).reduce((a: [], c: []) => a.concat(c));
          const titleList = idList.map(async id => {
            const mes = await this.getMes(account, gmail_client, id);
            if (mes) {
              const partsList: { name: string, value: string; }[] = mes.payload.headers;
              const f = partsList.find(value => value.name == "From")
              const f1 = f.value.slice(f.value.lastIndexOf("<")+1,f.value.lastIndexOf(">"));
              await this.firestoreService.saveTestLog(`mailBody${id}`,{ b: JSON.stringify(mes.payload.headers)});
              return {
                Subject: partsList.find(value => value.name == "Subject")?.value,
                To: partsList.find(value => value.name == "To")?.value,
                From: f?.value,
                FromAddress: f1,
                Date: partsList.find(value => value.name == "Date")?.value
              } as MailTitle
            } else return {} as MailTitle
          })
          const ans = await Promise.all(titleList);
          return ans.reduce((previousValue, currentValue) => Object.keys(currentValue).length === 0 ? previousValue : previousValue.concat(currentValue), [] as MailTitle[])
        }
      }
    } catch (e) {
      console.log("getMailTitles:", e)
    }
    return [];
  }
}

なんとか複数のgmail個人アカウントからpub/subを受けてgmailの見出し類を抽出。別途作ってたslackとの連動と組み合わせてslackにサマリーを通知。

ここまでだとgmailとslack連動をするツールを契約すれば全然時間がかからなかった訳だが、勉強にはなったし、ここからの次への再加工にも全然応用がきくはずなのだ。でも面倒だった。。。

繰り返しではあるが、ビジネスだったらここは有料サービスを使って半日で終わらせるべきところである。道楽だからアリな話だね。