White Box技術部

WEB開発のあれこれ(と何か)

【C100対応サークルチェッカー】Circle Checker2を作りました

以前にも紹介した、Twitterのユーザ名からイベント参加情報を抽出するツール「Circle Checker」ですが、ややあってRustで作り直しました。

以前の記事はこちらです

なので今回は新しく作ったCircle Checker2の使い方と、前バージョンとの違い、QAなどを書いていこうと思います。

続きを読む

マザボが壊れたのでWindows8.1からWindows10に乗り換えた(その後Win11になりました

これはWindows PCのマザーボード変更日記です。

だいぶ記事を寝かしてしまったので、今が2022年1月の気分で読んで下さい

「何もしてないのに壊れた」

年が明けてから、毎朝液タブでのクロッキーを習慣にしているのですが、ある日、電源ボタンを押してもBIOSが起動しない自体が発生しました。

この日は「あ、マザボリチウム電池が切れたな」と人生二度目のマザーボード電池交換をすることで復旧させることができたのですが、 明けて次の日、二度とBIOSが起動してくることはありませんでした。。。

2013年に組んだので、寿命は9年でした。

実は

そろそろWin8.1じゃ駄目だろうと、Win10を入れた起動ドライブを作ったり、大枚叩いてM1MAXなMacを買ったりしていたのですが、 肝心の絵を描く環境の移行を全然していなかったのです。

Win10環境作ったら、「お前のPC、Win11駄目だから!」ってMSに言われたり、 「Macへのクリスタ設定の引き継ぎ、よくわからん」ってなったせいなのですが。

思い返せば初代自作PCマザーボードの電池交換のあと、HDDがぶっとんで使えなくなったので、 マザーボード付属のリチウム電池が切れた場合は、新しいPCの準備を始めるのが良いのかもしれません。

「とりあえず、マザボだけ買うか」

というわけにはもちろんいかず、CPUとメモリは最低限買い替える必要があります。会社辞めるというのにこの出費よ。。。

気を取り直して、マザーボード選びからしました。
使っていたマザーボードASUSのだったので、今回もASUSから探していったのですが、いいなと思ったProArt B660-CREATOR D4はまだ出てなかったので、

「じゃあなんかそれなりのでいいか」

とこれらをツクモで買ってきたわけです。

それなりとか言いながら、ちょっとだけ奮発しました

2.5インチSSDじゃ駄目だった

後はWin10のOS入っているSSD繋いでおしまいかと思っていたのですが、最新のマザーボードは起動ディスクとしてSATAスロットのディスクを認識しませんでした。。。

いろいろ調べていると、新し目のマザーボードはM2スロットだけが起動ディスクになるようで、SATAのディスクでも起動するにはCMS機能を有効にする必要があるようでした。

この機能を有効にするには、グラフィックボードがマザーボードに挿されてなければならず、そうするとCPUについているグラフィック機能は使えなくなり、 intelのF無し型番を買った意味は消失しました・・・

ともあれしょうがないので、前のマザーボードにも挿していたグラフィックボードをはめ込み、CMSを有効にしました。

新しいマザーボードで過去環境を起動するのは難しい

とりあえず前も使っていたWin8.1のOSで起動し、クリスタ環境を移行しようとしたのですが、 そもそもマザーボードWindows 8.1に対応していないようで、ドライバの類がインストールできず、Win8.1環境ではネットに繋ぐことができなくなってしまいました。

まあそれも致し方なしかと思い、設定のコピー作業を数分していたところ、CPUが100%で張り付きました。 タスクマネージャで見る限り、特に何かがパワーを食っているようでもなかったので、とりあえず一旦PCを終了させ、再度立ち上げましたが、 2分くらいするとやはり100%になるようだったので、これも旧OSとマザーボードとの相性問題なのかもしれません。

更にいうと今回買った今のマザーボードSATAが4ポートで、他がM2とかいう固定するタイプのスロットになってたので、 過去環境と同じにするにはSATAポートが足りませんでした。

なので過去環境をそのまま移行するような場合は、最新のマザーボードを選んでもあまり利点はないのかもしれないですね。

大人しく全部買い換えましょう!

クリスタ環境の移行

公式の説明どおりに実行すれば移行はできました。

ただキャンバス設定は移行されなかったので、CPUが張り付く前に画面キャプチャを撮っておき、手動で設定し直しました。

XP-PENの液晶タブレット移行で詰まったところ

前の環境だとできなかった4K表示ができたので嬉しかったのですが、描画遅延が発生していました。
色々見てみたのですが、Win10だとディスプレイのリフレッシュレートも変更できるようで、そこが30Hzになっていました。

これは60Hz近くが設定できるまで解像度を落とすことで解消できました。

その後の話

数日使っていると、Windows11のアップデートが配信されたので、面倒くさいことは早めに済ましたい思いから、そのままアップデートを実行しました。

新しいOSなので不具合出るかなとは思ったのですが、意外と問題なく環境が引き継がれていました。

ともあれWindows11に全く問題がないわけでもなく、Windows Updateのあとにサウンド設定が初期化されてしまったり、画面設定がおかしくなったりはします。 設定し直せば元に戻すことはできるので、苦汁をなめさせられてきたWindows Update問題の中ではマシな方だとは思います。

それでもWindows11は今までのWindowsで一番しっくり来ている感じなので、アップデートおすすめです。

Firebase AuthenticationとFirestoreへのTypeScriptでのアクセス方法

TypeScriptで書いているNext.jsアプリのユーザ情報管理を、 Firebase AuthenticationとFirestoreで行ったのですが、 調べたときに見かけたコードサンプルは、呼び出し方がJavaScriptのままだったので、 TypeScriptっぽいサンプルもここに残して置きます。

というかWeb v9の書き方じゃない・・・と後から気付きました・・・
https://firebase.google.com/docs/firestore/query-data/get-data?hl=ja#web-v9

Firebase SDKの設定

TSの書き方では(もといWeb v9の書き方では)firebase.authのような呼び出し方をすることはできないので、 それぞれgetAuthやgetFirestoreを使ってインスタンスを取得する必要があります。

  • libs/firebase.ts
import * as firebase from 'firebase/app';
import { Auth, getAuth } from 'firebase/auth';
import { Firestore, getFirestore } from 'firebase/firestore';

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
  measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
};

if (typeof window !== 'undefined' && !firebase.getApps().length) {
  firebase.initializeApp(firebaseConfig);
}

export default firebase;
export const getFirebaseAuth = (): Auth => getAuth();
export const getFirebaseDb = (): Firestore => getFirestore();

firebaseをexportしていますが、今のところこれを触ることはなかったので、いらないかもしれません。

Firebaseアクセス

次はAuthenticationとFirestoreへの接続です。

①Authenticationへのサインイン

signInWithPopupの呼び出しは、パラメータにAuthインスタンスと対象のプロバイダを渡すことで行なえます。
サインインが成功するとUserCredentialが返却されるので、ユーザ情報が必要な場合は、そのuser属性を取得してください。

import { GoogleAuthProvider, User, UserCredential, signInWithPopup } from 'firebase/auth';
import { getFirebaseAuth } from 'libs/firebase';
〜〜〜
const auth = getFirebaseAuth();
const provider = new GoogleAuthProvider();
const signInResult: UserCredential = await signInWithPopup(auth, provider);
const crrentUser: User = signInResult.user;

Auth.currentUserに関する注意

AuthインターフェイスにはcurrentUserがあるのですが、これはonAuthStateChangedの処理後でなければ値がnullになっているので、利用する場合は気を付けてください。

②Firestoreへのアクセス

FirestoreもAuthenticationと同じように、getFirestore(ここではgetFirebaseDb)を使って処理を行います。 対象のコレクションとドキュメントを指定し、collection関数とdoc関数を使って、入れ子にして呼び出します。

doc(collection(Firestoreインスタンス, コレクション名), ドキュメントID)

これで得られるDocumentDataオブジェクトに、getDocで取得、setDocで追加の操作をします。

データ取得

getDocで取得したオブジェクトのdata()を呼び出すと、コレクションの値が取得できます。
フィールドの値はDocumentData["フィールド名"]の形で取得します。

import { DocumentData, collection, doc, getDoc } from 'firebase/firestore';
〜〜〜
const db = getFirebaseDb();
const uid = "対象のドキュメントID";
const document = await getDoc(doc(collection(db, 'users'), uid));

if (document.exists()) {
  const documentId = document.id;  // この場合、uidと同じ値
  const documentData: DocumentData = document.data();
  const user: UserType = {
    id: documentId,
    name: documentData['name'],
    email: documentData['email'],
    picture: documentData['picture'],
    token: documentData['token'],
  };

データ追加

ドキュメントをコレクションに追加するには、DocumentDataにsetDocを行います。
登録するデータはオブジェクトで定義してください。

const data = {
  name: "名前",
  email: "メールアドレス",
  picture: "アバターURL",
  token: "アクセストークン",
};

await setDoc(対象のDocumentData, data);

③コード例

実際のコードだと以下のようになりました。

  • functions/account.ts
import { GoogleAuthProvider, UserCredential, signInWithPopup } from 'firebase/auth';
import { Firestore, collection, doc, getDoc, setDoc } from 'firebase/firestore';
import Router from 'next/router';

import { convertToUser } from './user';

import { getFirebaseAuth, getFirebaseDb } from 'libs/firebase';
import { UserType } from 'application';

export const googleSignin = () => {
  (async () => {
    const auth = getFirebaseAuth();
    const provider = new GoogleAuthProvider();
    try {
      const signInResult: UserCredential = await signInWithPopup(auth, provider);
      const db = getFirebaseDb();
      const exist = await existFirebaseUser(db, signInResult.user?.uid);
      if (!exist) {
        // firebaseへのユーザ登録処理
        const user: UserType = await convertToUser(signInResult.user);
        const isRegistered = await registerUser(db, user);
        if (isRegistered) {
          console.log('登録OK uid: ' + user.id);
        } else {
          console.log('登録NG uid: ' + user.id);
          Router.push('/error');
        }
      }
      Router.push('/mypage');
    } catch (error) {
      console.log(error);
      console.log(error.code);
      console.log(error.message);
    }
  })();
};

const existFirebaseUser = async (db: Firestore, uid: string): Promise<boolean> => {
  const result = (await getDoc(doc(collection(db, 'users'), uid))).exists();
  return result;
};

const registerUser = async (db: Firestore, user: UserType): Promise<boolean> => {
  const userData = {
    name: user.name,
    email: user.email,
    picture: user.picture,
    token: user.token,
  };
  try {
    await setDoc(doc(collection(db, 'users'), user.id), userData);
    console.log('登録成功');
    return true;
  } catch (error) {
    console.log('登録失敗:' + error);
    return false;
  }
};

export const signout = () => {
  getFirebaseAuth()
    .signOut()
    .then(() => {
      window.location.reload();
    });
};
  • functions/user.ts
import { User } from 'firebase/auth';

import { UserType } from 'application';

export const convertToUser = async (firebaseUser: User): Promise<UserType> => {
  if (firebaseUser) {
    const user: UserType = {
      id: firebaseUser.uid,
      name: firebaseUser.displayName,
      email: firebaseUser.email,
      picture: firebaseUser.photoURL,
      token: await firebaseUser.getIdToken(),
    };
    return user;
  }
  return null;
};

Google Apps Script(GAS)からBox APIを叩くためのすべて

四苦八苦してしまって本当に辛かったので、他の人が同じような苦労をしないように・・・

個人アカウントのOAuth認証で良い場合はこのサイトの手順が参考になります。
How to Use the Box API with Google Apps Script - Digital Inspiration

ただ今回は、AppSheetからの呼び出しで使いたかったので、これだと用途に合わないんですよね。

必要なこと

ざっくりですが、以下の作業が必要です。

  • Boxの開発アプリをCustom AppのServer Authenticationで作成
  • Box Adminでアプリを承認
  • BoxアプリのService Account IDを操作したいBoxフォルダの共有に追加
  • GASの実行権限設定
  • Box APIからアクセストークンの取得

Box側

1. 開発アプリの作成

Boxの開発アプリをCustom AppのServer Authenticationで作成します。

※途中で設定を日本語にしたので、図ではサーバー認証(クライアント資格情報許可)です

f:id:seri_wb:20210810010958p:plain:w400

構成での作業

  • クライアントシークレットの発行
  • アプリケーションスコープを設定
  • CROSドメインにGASのドメインを追加
    • https://*.googleusercontent.com

2. アプリのEnterprise承認をする

アプリの設定が終わったら承認リクエストを飛ばします。
そうするとBox Adminの権限があるメンバーにアプリ承認のリクエストが飛ぶので、承認してください。

GAS側

GASのコードはclaspを使って作成しています。
ここは特に真似る必要はありませんが、TypeScriptでのdoPost関数の実装例として見て貰えればと。

結局nodeライブラリは使わなかったのですが、claspの使い方はこのサイトを参考にさせて貰いました。

appsscript.jsonの中身

{
  "timeZone": "Asia/Tokyo",
  "dependencies": {},
  "exceptionLogging": "STACKDRIVER",
  "oauthScopes": [
    "https://www.googleapis.com/auth/drive.readonly",
    "https://www.googleapis.com/auth/drive",
    "https://www.googleapis.com/auth/script.external_request"
  ],
  "runtimeVersion": "V8"
}

main.tsの全量

重要なのはアクセストークンを取得しているgetAccessTokenで、他は各自の処理に合わせて貰えればOKです。

const BOX_CLIENT_ID = 'クライアントID';
const BOX_CLIENT_SECRET = 'クライアントシークレット';
const BOX_ENTERPRISE_ID = '組織ID';
const BOX_TEMPLATE_FOLDER_ID = 'コピー元フォルダのID';
const BOX_PARENT_FOLDER_ID = 'コピー先(親)フォルダのID';

// https://developers.google.com/apps-script/guides/web#request_parameters
export function doPost(e: GoogleAppsScript.Events.DoPost): GoogleAppsScript.Content.TextOutput {
  const contents = JSON.parse(e.postData.contents);
  const orderDate = contents["orderDate"];
  const customerName = contents["customerName"];

  const createFolderId = copyFolder(orderDate, customerName);

  const resultParams = {folderId: createFolderId};
  const result = JSON.stringify(resultParams);
  return ContentService.createTextOutput(result);
}

const getAccessToken = (): string => {
  const endpoint = 'https://api.box.com/oauth2/token';
  const payload = {
    client_id: BOX_CLIENT_ID,
    client_secret: BOX_CLIENT_SECRET,
    grant_type: 'client_credentials',
    box_subject_type: 'enterprise',
    box_subject_id: BOX_ENTERPRISE_ID
  };
  const headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
  const response = UrlFetchApp.fetch(endpoint, {
    headers: headers,
    method: 'post',
    payload: payload
  });
  const json = JSON.parse(response.getContentText());

  return json.access_token;
};


const copyFolder = (orderDate: string, customerName: string): string => {
  const endpoint = `https://api.box.com/2.0/folders/${BOX_TEMPLATE_FOLDER_ID}/copy`;

  const access_token = getAccessToken();
  const headers = {
    'Authorization': 'Bearer ' + access_token,
    "Content-type": "application/json",
  };
  const payload = {
    name: `${orderDate}_${customerName}`,
    parent: {
      id: BOX_PARENT_FOLDER_ID
    }
  };
  const response = UrlFetchApp.fetch(endpoint, {
    headers: headers,
    method: 'post',
    payload: JSON.stringify(payload)
  });
  const json = JSON.parse(response.getContentText());
  Logger.log(json.id);
  return json.id;
};

以上です。


落ち穂拾い

Client Credentials Grantの日本語ページがない

今回のやり方のベースになっている、以下のページに該当する日本語のページがありません。

Boxの開発ドキュメントは日本語のものが用意されており、 そちらが最初に表示されたので、別段気にしていなかったのですが、 クライアントクレデンシャルでのアクセストークン取得方法のページはなぜかガッツリ抜け落ちていました。

英語

f:id:seri_wb:20210810012220p:plain:w500

日本語

f:id:seri_wb:20210810012251p:plain:w500

Boxで最初に企業ドメインアカウントで開発者登録をすると、その人が管理者になる

ADMINは、他のアカウントへのスイッチもできるので、適当なタイミングで適切な人に権限を渡しましょう。

https://support.box.com/hc/ja/articles/360044194953/

AppSheetはPOSTのレスポンスを使えない

本記事の例ではdoPostでレスポンスを返していますが、これを呼び出すAppSheetは現状レスポンスの中身を利用できないので、 あの部分はなくても問題ありません。

GASでデプロイ後はGCP連携していないとログが表示されない

GASをデプロイし、外部からGASのスクリプトcurlなどで叩いた場合、GCP連携していないとコード中に組み込んだログは表示されません。