使用ライブラリを簡単に覗く方法~Windows編~

こちらは Unityゲーム開発者ギルド Advent Calendar 2022 14日目の記事です。

概要

実はUnity製のプロダクトでは逆アセンブルや逆コンパイルなどのリバースエンジニアリングを用いずに内部の構造を覗き見れてしまいます。 今回は一番簡単なWindowsでの確認方法を紹介します。 使用しているOSSを参考にしたい場合や、プロジェクトの構成を参考にしたい場合に有用ですが、開発者にとっては簡単に覗き見られるリスクが存在します。 適当な名前でプロジェクトやアセットを作成していたり、不要なファイルを追加しているだけならかわいいものですが、 違法な仮アセットをそのまま残していたり、OSSやライブラリのライセンス表記をせずに使用している場合はバレやすく問題となります。

Unityビルドの構造

Windowsでビルドすると以下のようなフォルダ構成で出力されます。主なリソースは_Dataのフォルダ内に出力されます。

_Dataは以下のようになっています。

この時点で分かることもあります。例えばLevel〇ファイルはシーンファイルなので画面数に比べてファイルが少ないならシーン切り替えではなくプレハブを切り替える運用であるとか、resourcesはResourcesフォルダに依存しているのでサイズが大きいとResouresフォルダを使った運用をしているとかが推測できます。

使用ライブラリの確認

ScriptingAssemblies.json

_DataにあるScriptingAssemblies.jsonはプロジェクトに含まれるAssembly Definition FilesがDLLとして確認できます。最近のOSSやプロジェクトではAssembly Definitionを使用しているものも多いため、ScriptingAssemblies.jsonを見ることで使用しているライブラリ情報が分かってしまうのです。 例えば以下は実際のScriptingAssemblies.jsonの一部ですが、UnityEngineの機能だけでなく、自分で定義したもの(Sample.dll)やUniTaskなどのOSSまで確認できます。

{
    "names": [
        "UnityEngine.dll",
        "UnityEngine.AIModule.dll",
        "UnityEngine.ARModule.dll",
        "UnityEngine.AccessibilityModule.dll",
......
        "Sample.dll", ※自分のプロジェクトのDLL
......
        "UniTask.dll", 
......

非公開情報や非公開のコードネームで名前付けしてしまうと、その情報が漏れてしまう可能性がありますので気をつけましょう。 (非公開になっているはずの協力会社A社のライブラリをACompany.Libraryなどとして入れてしまうとバレてしまう)

Addressables

Addressables(Addressable Asset System)を使用してLocal向けにビルドを行うとStreamingAssets内にaaというフォルダが出力されます。その中のcatalog.jsonにはAddressablesを使用して出力したアセット情報が入っています。 以下はTextMeshProといくつかの画像・プレハブを追加したプロジェクトのcatalog.jsonの一部ですが、このように含まれているアセットがパス名付きで分かってしまいます。

    "m_InternalIds": [
        "{UnityEngine.AddressableAssets.Addressables.RuntimePath}\\StandaloneWindows64\\sampleassets_assets_all_3ee2f45a2df087cb8a1cc977ff507570.bundle",
        "Assets/SampleAssets/Image1.png", ※オリジナル画像1
        "Assets/SampleAssets/Image2.png", ※オリジナル画像2
        "Assets/SampleAssets/SamplePrefab.prefab", ※オリジナルプレハブ
        "Assets/SampleAssets/SecretImge.png", ※オリジナル画像、本当は隠しておきたいアセット
        "DebugUICanvas",
        "DebugUIPersistentCanvas",
        "Fonts & Materials/LiberationSans SDF",
        "Fonts & Materials/LiberationSans SDF - Drop Shadow",
        "Fonts & Materials/LiberationSans SDF - Fallback",
        "Fonts & Materials/LiberationSans SDF - Outline",
        "LineBreaking Following Characters",
        "LineBreaking Leading Characters",
        "Scenes/SampleScene", ※シーンファイル
        "Sprite Assets/EmojiOne",
        "Style Sheets/Default Style Sheet",
        "TMP Settings"
    ],

配布ビルドには含めず追加のリソースとして管理することでStreamingAssetsから除くことはできますが、結局ダウンロードフォルダにも同じ状況で保存されてしまうため隠すことはできません。 ファイル名の情報だけではあり、大きな問題にはなりにくいものの、問題のある仮ファイルを残したままにするのは避けるべきです。(ネットミームの画像を仮素材として使っていた場合など、企業としてはふさわしくないこともある)

また、アセット情報はm_resourceTypesにも追加されます。以下のようにアセットにDOTweenを使っていることが分かります。

{
    "m_AssemblyName": "DOTween, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
    "m_ClassName": "DG.Tweening.Core.DOTweenSettings"
},

今回はテキストファイルのような簡単に見られるファイルからのアプローチでしたが、もちろんリバースエンジニアリングをするとより細かい情報があらわになってしまいます。対策も可能ですが、開発者の心構えとしては、プロジェクトが外から見られても恥ずかしくない状態にしておきたいですね。

余談

これらの情報を調べてみたきっかけは、有名企業がリリースしたWindows向けアプリなのにも関わらずOSS情報が一切載っていないことへの疑いでした。調べてみると案の定有名なOSSやライブラリ・アセットが数多く使われているようでした。UniTaskやVContainerといったよく使われるOSSだけでなく、UnityScreenNavigatorNativeWebSocketといったプロダクトでの利用はそこまで聞いたことがなかったOSSまで使われていました。 更にはアセットストアで販売しているアセット群の名前もあり、プロダクトでの実用事例として知ることもできました。アプリの出来はよく、使用ライブラリもまともでアピールできるのにOSSのライセンス表記がない状況で、同じ開発者として寂しく思っています。

正直なところOSSの表記をわざわざ実装したり管理したりには面倒なところがあります。一方で開発者として恩恵を受けている感謝の気持ちはせめてライセンス表記の形で残したいですね。

※たまたま同じ名前で開発した自前ライブラリだったり、裏でOSSの管理者と密約を交わしている可能性もあるため、この情報だけで違反だなんだと決めつけることはできません。

CRI ADX2 + Addressables + 宴のマシマシセットを動かす

こちらはUnity Advent Calendar 2022 12/7の記事です

CRI ADX2(以下ADX)のAddressables対応およびUnity用ビジュアルノベルツール「宴」への導入について紹介します。まずADXとAddressablesの連携について紹介し、次に宴への導入について解説します。

ADXのAddressables対応

これまでのADXとAddressablesの状況

ADXのACBファイルなどはバイナリであるため、これまではUnityアセットとして取り込めませんでした。

AddressablesはUnityアセットを読み込む仕組みであるため、ADXのファイルはAddressablesに非対応であり、一時的に拡張子を.bytesや.txtなどのTextAssetとして認識させる方法を取っていました。

以前の方法はこちらにまとめています。

speakerdeck.com

一方でこの方法はメモリ効率が悪かったり、拡張子を書き換えるため編集がしにくかったりと課題が存在していました。

公式によるサポート

これらの課題を解決するため、CRIWAREから公式のサポートプラグインがリリースされました。

game.criware.jp

ADXのファイルがUnityアセットとして認識されることで、Addressablesに対応できるだけでなく、Unity上でCUEや音の確認までできるようになりました。また、これまでは必ずStreamingAssetsフォルダに配置していましたが、Addressablesと連携することでファイルの配置場所も自由に変更できるようになります。

また、プラグインは無償版のCRI ADX LEでも使えるようになっています。ありがたや。

プラグインの詳細

サポートプラグインには以下の2つのプラグインが存在します。

  • CRI Assets 公式ドキュメント
    • ADXのファイルをUnityアセットとして認識させる機能
    • ファイルパスを指定したロードではなく、アセットの参照(インスペクタでのアタッチ)が可能になっている
    • 単体で使用可能で、Addressablesを使っていない場合でもオススメ
  • CRI Addressables 公式ドキュメント
    • CRI Assetsで認識したADXファイルをAddressablesでロード可能にする機能
    • CRI Assetsと併用する前提で使用

プラグインの使用方法

公式ドキュメントを参考にプラグインをプロジェクトに導入します。 導入自体はunitypackagesをプロジェクトに入れるだけです。

注意点としてはプラグインではAssembly Definitionの対応版を使用するため、プロジェクトにもAssembly Definitionの導入が必要かもしれません。

実はプラグインを導入したとしても既存の仕組みであるファイルパスでのロードもそのまま併用して使えます。音源ファイルをUnityに取り込まず、独自でダウンロードして使う場合など、ファイルパスを指定してロードしたほうが都合がよいケースもあります。プロジェクトの運用に応じて方法を変えられます。

ADXファイルのロード

具体的なAddressablesを利用したADXファイルのロード手順です。

  1. ADXファイルのDeployTypeを設定する
    1. アプリに含める場合はAddressables(Local)
    2. 外部リソースはAddressables(Remote)にする
  2. ADXファイルをAddressablesに登録する
  3. 以下のスクリプトで読み込む(UniTaskを使用)
// keyでロードするアセットを指定
var handle = Addressables.LoadAssetAsync<CriAtomAcbAsset>(key);
var acbAsset = await handle.Task;

// 必要に応じてロードを実行
if (!acbAsset.Loaded && !acbAsset.LoadRequested)
{
    acbAsset.LoadAsync();
}

// ロード完了まで待機
await UniTask.WaitUntil(() => acbAsset.Loaded);

return acbAsset.Handle;

上記はADXのファイルをCriAtomExAcbとして返すため、CriAtomExPlayerのSetCueに渡して再生します。

Addressablesでアセットのロードが完了した後にACBのロードを行います。そのためLoadAsyncでロードを行い、UniTask.WaitUntilでロードが終わるまで待つといった処理を行います。

CRI Assetsを利用したインスペクタでの音源指定は一連の処理を自動で行ってくれるため、動的に音源が変わらない場合はそちらの使用をオススメします。

宴との連携

宴について

はUnity用ビジュアルノベルツールで、エクセルでシナリオを作成することで簡単にノベルゲームを実現できます。ノベルゲームとしての基本の機能が揃っているだけでなく、必要に応じて改変しやすい作りになっています。また、国内で開発しているためサポートが厚いこともメリットです。ありがたや。

宴のAddressables対応

宴の標準ではResourcesフォルダや独自のAssetBundle管理の仕組みを用いてアセットのロード・管理を行いますが、開発者のサポートによりAddressablesでのロードにも切り替えられるようになっています。 公式ドキュメントに従いセットアップすることで、宴内部のアセットローダーが差し代わり、Addressablesでのロードが可能になります。

宴でのADXファイルロード

標準の仕組みからAddressablesへの移行について、画像ファイルはそのままスムーズに移行できますが、ADXファイルはひと手間必要です。 ADX2の使用方法を参考にサウンド再生にADX2を使うことはできますが、こちらはAddressablesに対応していません。そのため一部改変を行います。

  1. 上記ドキュメントを参考に宴の拡張を行う
    1. サウンドシートへのCueSheet列の追加が忘れがち
    2. 拡張コードの説明は解説なので対応は不要です
  2. 宴のスクリプトを以下のように変更する

UtageForAddressableCustomFileManagerのFindAsset

void FindAsset(AssetFileManager mangager, AssetFileInfo fileInfo, IAssetFileSettingData settingData, ref AssetFileBase asset)
{
    // サウンドデータならAddressablesCustomFileManagerからのロードはスキップする
    if (fileInfo.Setting.FileType == AssetFileType.Sound)
    {
        return;
    }
    asset = new UtageForAddressableCustomFile(mangager, fileInfo, settingData);
}

これによりSound指定のファイルはUtageForAddressableCustomFileManager経由ではなく、Adx2LeForUtage経由でAssetFileBaseが更新されます。 つまり、ADXのファイルはUtageForAddressableCustomFileではなく、Adx2AssetFileとして読み込みましょうということです。

Adx2AssetFileのLoadAsync

public override IEnumerator LoadAsync(Action onComplete, Action onFailed)
{
    if (CriAtom.GetCueSheet(this.CueSheet) == null)
    {
        // Addressableからの取得に変更
        var key = CueSheet;
        var handle = Addressables.LoadAssetAsync<CriAtomAcbAsset>(key);
        AcbAsset = handle.WaitForCompletion();
        CriAtomAssetsLoader.AddCueSheet(AcbAsset);

        // 元のコード
        // string acbPath = this.CueSheet + ".acb";
        // string awbPath = this.CueSheet + ".awb";
        // CriAtom.AddCueSheet(this.CueSheet, acbPath, awbPath);
    }
    IsLoadEnd = true;
    if (onComplete != null) onComplete();
    yield break;
}
// 取得したAcbAssetは外から利用するのでプロパティを定義しておく
public CriAtomAcbAsset AcbAsset { get; private set; }

ファイルパスを指定しての読み込みからCriAtomAcbAssetとAddressablesを利用した読み込みに変更します。 読み込んだAcbAssetはロードのためCriAtomAssetsLoaderに追加します。 外部公開したAcbAssetは次のAdx2Audioで使用します。

Adx2Audioの変更

// プロパティのSourceをForAssetのほうに変更する
public CriAtomSourceForAsset Source { get { return source ? source : (source = this.gameObject.AddComponent<CriAtomSourceForAsset>()); } }
CriAtomSourceForAsset source;
// 元コード
// public CriAtomSource Source { get { return source ?? (source = this.gameObject.AddComponent<CriAtomSource>()); } }
// CriAtomSource source;

// CoWaitDelayの内部を以下に変更
IEnumerator CoWaitDelay(float fadeTime, float delay)
{
    isFadeOuting = false;
    Adx2AssetFile adx2File = Data.File as Adx2AssetFile;
    // Source.cueNameからadx2File.CueNameに変更
    if (Adx2LeForUtage.DebugLog) Debug.Log(string.Format("Play {0} FadeIn{1}", adx2File.CueName, fadeTime));
    Source.volume = 0;
    Source.loop = Data.IsLoop;
    
    // CueSheetとCueNameでの指定をやめ、取得したAcbAssetを元にCriAtomCueReferenceを作成し指定する
    Source.Cue = new CriAtomCueReference(adx2File.AcbAsset, 0);
    // 元コード
    // Source.cueSheet = adx2File.CueSheet;
    // Source.cueName = adx2File.CueName;

CriAtomSourceではファイルパスからのロードのためCueSheetとCueNameを指定し再生を行っていましたが、Addressablesを利用するCriAtomSourceForAssetではアセットを直接指定しているため、AcbAssetのインスタンスを元にCueを構築します。 また、Source.cueNameのプロパティは取得できなくなるため、デバッグなどで表示する場合は代わりにadx2File.CueNameやSource.Cue.AcbAsset.nameで取得します。

以上の更新によりCRI ADX2+Addressables+宴の構成でも音源を鳴らすことができ、各ミドルウェアのいいとこどりを実現しています。

Easy Mobile Proを使った簡単アプリ内課金実装

Easy Moblie Proを使ってアプリ内課金実装を行ってみました。 ローカルでのレシート検証ではありますがセットアップすればすぐに使えて便利でした。

Easy Mobile Proとは

Easy Mobile Pro はモバイルゲーム開発における多機能プラグインで、アプリ内課金だけでなく広告サービスやゲームサービスなどをサポートしています。 今回は課金機能だけを使いましたがリファレンス に沿って実装することで詰まることなく実装できました。 内容としてはUnityIAPのラッパーになっており、初期化処理の自動化やリストアがシンプルに行えます。

開発環境

Easy Mobile Proを使った実装

In-App Purchaseの項目に従いセットアップを進めます。 英語ですがGoogle Chromeの日本語翻訳を通すだけでだいぶ読みやすくなります。 リファレンス通りに進めることでほとんどは問題なく実装できるため、つまりどころとポイントのみ記載します。

Easy Mobile Proの設定画面が出ない

Easy Mobile ProのImport後はUnityを再起動しましょう。

Assembly Definitionの設定

Easy Mobile ProはAssembly Definitionがないため自分で作成する必要があります。 特にUnityIAP周りのAssembly Definitionを設定する必要があるため注意が必要です。 今回のサンプル実装では以下となりました。

f:id:tetsujp84:20210623200954p:plain
UnityGifDecoderはEasy Mobile Proに含まれるライブラリです。

EasyMoblie/EditorフォルダにもEditor用のAssetmbly Definitionを作成します。 f:id:tetsujp84:20210623201522p:plain
こちらは設定が面倒だったため、関連していそうなものを追加しています。そのため不要なReferenceも含んでいるかもしれません。

Productの設定

課金アイテムにはIDを振ります。App Storeでは製品ID、Google Play StoreではアイテムIDと呼ばれます。 Google Play StoreではアイテムIDに小文字しか使えないため、特別な理由がなければIDは小文字で統一したほうがよいです。 Easy Moblie ProではProductsの項目からストアごとに個別に設定することもできます。参考

ローカルでのレシート検証

Androidではアプリのストアに紐づくRSA 公開鍵が必要です。 Google Play Consoleの収益化のセットアップの項目から公開鍵を取得します。 収益化のセットアップの項目は以下のようなURLになっています。
- https://play.google.com/console/u/1/developers/○○/app/○○/monetization-setup

UnityではWindow->UnityIAP->Receipt Validation Obfuscatorからローカル検証用の難解化を行いセットアップします。 参考

ストアとの連携

課金のテストでは事前に以下を行っておきます。

App Store Connect

  • 銀行口座や納税フォーム、連絡先の登録
  • App内課金の項目で課金アイテムの登録
    • App Store 情報やスクリーンショットは仮でも良いので全部埋める必要があります。
    • 「配信可能」のチェックボックスは有効にします。
    • ステータスが「送信準備完了」になっていればテストできます。
    • ※「一番最初の App 内課金は、App の新しいバージョンと一緒に提出する必要があります。」と表示されますが、テストではストアアプリの更新は不要です。
  • 課金テスターの登録
    • 課金テスターは専用のAppleIdを登録する必要があります。
    • 開発者と同じAppleIdは使えないため、専用のGmailアカウントを取得し別で使用することが多いです。

Google Play Console

  • IAPを有効にしたAPKかAABをアップロード
    • アプリをアップロードすることでストアのセットアップができるようになります。
  • アプリ内アイテムの項目で課金アイテムの登録
    • アイテムのステータスは有効にします。
  • 課金テスターの登録
    • テスターはメールアドレスで登録します。
    • ライセンステストの項目で同一のメールアドレスをライセンス テスターとして追加します。
      • ライセンステスターとして追加していないと課金テストであっても支払いが発生してしまいます。

実機テストでUnavailable product(NoProductsAvailable)が出る場合

  • ストアと同じBundleIdになっているか確認します。BundleIdをストアビルドと開発ビルドで分けていると発生しやすいです。
  • iOSでは「配信可能」にチェックが入っていること、Androidではステータスが「有効」になっていることを確認します。
  • iOSのプロビジョニングプロファイルはAdHocを使います。課金テストのためにTestFlightを経由する必要はありません。
  • AndroidではAPKをGoogle Play Consoleにアップロードし、クローズドテストなどのテストの機能を用いてインストールする必要があります。Unityから直接端末にインストールしても課金のテストが行えません。

サンプルスクリプト

課金用スクリプトのサンプルを記載しました。Easy Moblieの設定画面でAutoInitializationを有効にしておけば初期化処理はアプリ起動時に自動で行われます。OnEnable/OnDisableでイベント管理をしているのでMonoBehaviourにしています。

// 課金プロセスの実行クラス
public class PurchasingProcessController : MonoBehaviour
{
    // 課金成功/失敗の通知
    public IObservable<bool> OnComplete => _onComplete;
    private Subject<bool> _onComplete = new Subject<bool>();

    // すでに購入済みか(非消費型アイテム向け)
    public bool IsOwned(string productionName)
    {
        return InAppPurchasing.IsProductOwned(productionName);
    }

    // 購入実行
    // productNameは製品ID/アイテムID
    public void DoPurchase(string productName)
    {
        InAppPurchasing.Purchase(productName);
    }

    // リストア処理
    public void TryRestore()
    {
        InAppPurchasing.RestorePurchases();
    }

    private void OnEnable()
    {
        InAppPurchasing.PurchaseCompleted += PurchaseCompletedHandler;
        InAppPurchasing.PurchaseFailed += PurchaseFailedHandler;

        InAppPurchasing.RestoreCompleted += RestoreCompletedHandler;
        InAppPurchasing.RestoreFailed += RestoreFailedHandler;
    }

    private void OnDisable()
    {
        InAppPurchasing.PurchaseCompleted -= PurchaseCompletedHandler;
        InAppPurchasing.PurchaseFailed -= PurchaseFailedHandler;

        InAppPurchasing.RestoreCompleted -= RestoreCompletedHandler;
        InAppPurchasing.RestoreFailed -= RestoreFailedHandler;
    }

    // 購入成功処理
    private void PurchaseCompletedHandler(IAPProduct product)
    {
        Debug.Log(product.Name);
        _onComplete.OnNext(true);
    }

    // 購入失敗処理
    private void PurchaseFailedHandler(IAPProduct product, string failureReason)
    {
        Debug.Log("The purchase of product " + product.Name + " has failed with reason: " + failureReason);
        _onComplete.OnNext(false);
    }

    // リストア成功通知
    // 通知のみ
    private void RestoreCompletedHandler()
    {
        Debug.Log("All purchases have been restored successfully.");
    }

    // リストア成功通知
    private void RestoreFailedHandler()
    {
        Debug.Log("The purchase restoration has failed.");
    }
}

使い方例

[SerializeField] private PurchasingProcessController _purchasingProcessController; 

// Viewで購入する商品を選択
_view.OnSelect.Subscribe(productName =>
{
    _purchasingProcessController.DoPurchase(producName);
});

// 購入の成功/失敗に応じて処理を分岐
_purchasingProcessController.OnComplete.Subscribe(isSuccess =>
{
    Debug.Log(isSuccess ? "購入に成功しました" : "購入に失敗しました。\n再度お試しください。");
});

まとめ

課金機能は初期化処理周りのハンドリングが面倒ですが、Easy Mobile Proは初期化も自動で行ってくれます。 Easy Mobile Proは約$100とお高めなアセットですが今回紹介した課金機能以外にも様々な機能が含まれています。 また、アップデートも行われておりUnity2020にも対応していました。 導入の参考になれば幸いです。

ProjectSettingsからみるUnity2020

こちらはUnityゲーム開発者ギルド Advent Calendar 2020 12/17の記事です

アドカレのネタを探していたところUnity2020.2がいつの間にか正式リリースされていたのでこれまでのUnityとProjectSettingsの項目で比較してみます
これまでUnity2019を使っていたので主にUnity2020とUnity2019との比較になります(Unity2020.2とは...)

前置き

2020年も終わりに差し掛かっていますがUnity2020.2正式版が出ました
Unity2020.1正式版は7月公開だったので約6か月ぶりのバージョンアップになります
(Unity2020.3 LTSは2021年春に出るらしい、2020年とは...。春...?夏には出たらいいな)

Unity2020.2は特にエディタ周りでの大幅な改善が行われていますが、そもそもUnity2020に満足に触れていなかったので今回はProjectSettingsからUnity2020への変化を見てみたいと思います

ProjectSettingsの項目

f:id:tetsujp84:20201217030620p:plain

まずは大項目から
増えたもの

なくなったもの

Editor

Graphics

Build-in Shader SettingsにVideoの項目が追加
デフォルトではAlways IncludeなのでVideoが不要なら設定を変える

Package Manager

Advanced Settingsの項目が追加
Package ManagerでPreview版を表示するか選べるようになった
デフォルトはOffなのでPreview版を利用している人は要設定
f:id:tetsujp84:20201217041418p:plain

Physics

Default Max Depenetration Velocityが追加
https://docs.unity.cn/2020.2/Documentation/ScriptReference/Physics-defaultMaxDepenetrationVelocity.html

Physics2D

Simulation Modeが追加
f:id:tetsujp84:20201217040812p:plain
https://forum.unity.com/threads/thank-you-for-the-physics2d-update-simulation-mode.955167/
物理をUpdateタイミングで動かす、ができるのか?

Player

OhterSettings

  • Lightmap Streaming Enabled → Lightmap Streamingに名前が変わった(ただの表記ブレ修正か)
  • Enable Frame Timing Stats → Frame Timing Stats(これも表記ブレ修正か)

f:id:tetsujp84:20201217042612p:plain

f:id:tetsujp84:20201217043804p:plain

  • Mac App Store Options
    Bundle Identifierがoverrideにチェック→変更の形式になった

f:id:tetsujp84:20201217044924p:plain

Script Execution Order

f:id:tetsujp84:20201217050037p:plain
ToggleGroupがOrder10として追加

Tags and Layers

f:id:tetsujp84:20201217050152p:plain
Buildin Layerの間にUser Layer3が追加

終わり

Scene TemplateやNumbering Scheme、Scripting Define Symbolsの形式変更がうれしい

Slackのアイコンを毎日違う嫁にする

2022/01/12 追記

WaifuLabsのAPIが非公開になり形式が変わったので非対応になりました。悲しい。

概要

Go言語+AWS LambdaでSlackのアイコンを定期的に変えます。

アイコンはWaifuLabsから自動で生成します。

WaifuLabsを利用することで毎日違うアイコンになります。新鮮。

嫁サンプル
f:id:tetsujp84:20200409024045p:plain:w100 f:id:tetsujp84:20200409024247p:plain:w100 f:id:tetsujp84:20200409024250p:plain:w100 f:id:tetsujp84:20200409024413p:plain:w100

プロジェクト

コード全体はGitHubで公開しています。
auto-waifu-lab

アイコン変更のコード

get_image.go

WaifuLabs から嫁画像を取得します。

取得APIhttps://api.waifulabs.com/generate_bigを使用しました。

https://api.waifulabs.com/generateはパラメータ不要で取得できますが、
1回で無駄に16枚分の画像を返してしまうのでこちらは使っていません。

func GetImage() {
    // WaifuLabsはいつくかのAPIをよんでおり、そのうちの一つがgenerate_big
    URL := "https://api.waifulabs.com/generate_big"

    rand.Seed(time.Now().UnixNano())

    // JSON形式でパラメータ生成
    // currentGirlのあとに16個のパラメータ値を指定する
    values := `{"currentGirl" : [`
    param := ""
    val := strconv.Itoa(rand.Intn(999999))
    for i := 0; i < 16; i++ {
        param += val + ","
    }
    param += "0"
    values += param
    values += "]}"

    req, err := http.NewRequest("POST", URL, bytes.NewBufferString(values))
    if err != nil {
        fmt.Println(err)
        return
    }
    req.Header.Add("Content-Type", "application/json")

    // API実行
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        log.Println(err)
        return
    }

    // Res解析
    b, err := ioutil.ReadAll(resp.Body)
    if err == nil {
        // Base64デコード
        base64Data := new(Base64Data)
        if err := json.Unmarshal(b, &base64Data); err != nil {
            fmt.Println(err)
            return
        }
        data, err := base64.StdEncoding.DecodeString(base64Data.girl)
        if err != nil {
            fmt.Println(err)
        }
         // レスポンスデータを使用してpng生成
         // send_to_slackでtoday.pngの名前を指定してSlackで送っているため
        file, err := os.Create("/tmp/today" + ".png")
        if err != nil {
            fmt.Println(err)
            defer file.Close()
            return
        }
        file.Write(data)
        defer file.Close()
    }
    defer resp.Body.Close()
}

send_to_slack.go

Slackのアイコン変更APIを実行します。

Slack用ライブラリはnlopes/slackを使用しました。

func trySend(trycount int) {
    // Lambda側でTOKENを設定する
    token := os.Getenv("SLACKTOKEN")
    api := slack.New(token)

    err := api.SetUserPhoto("/tmp/today.png", slack.NewUserSetPhotoParams())
    if err != nil {
        fmt.Println(err)
        // 何度が失敗することがあったので再度送っている、不要かもれしない
        if trycount == 1 {
            trycount++
            trySend(trycount)
        }
        return
    }
    return
}

Slack Tokenの取得

リンクを参考にSlackのAppを作成します。

APIとしてusers.profile.setを使用するのでPermissionsにusers.profile:writeを追加します。

このとき生成されるOAuth Access Token(xoxp-で始まる)を環境変数として後で使用します。

AWS Lambdaでの実行

こちらを参考に設定を進めます。

entry.goがlambdaのエントリーポイントになります。

entry.go

package main

import (
    "fmt"

    "./mywaifulab"
    "github.com/aws/aws-lambda-go/lambda"
)
// mainは必須
func main() {
    // lambda側で呼ぶ
    lambda.Start(entry)
}

func entry() {
    // 画像を取得→Slackに投げる
    mywaifulab.GetImage()
    mywaifulab.SendToSlack()
}

windowsでパッケージをZIPにする際はAWSから提供されているビルドツールを使用します。

こちらでZIP化しないとメソッドの呼び出しが正しく行われませんでした。

Lambdaで関数を作成し、関数コードとしてZIPファイルをアップロードします。

環境変数にSLACKTOKENを追加し、ValueはSlackのAccess Tokenにします。

定期実行にはLambdaのCloudWatch Events/EventBridgeを使用します。参考

スケジュール式はrate式かcron式で記述します。参考

保存すると定期実行されます。

結果

アイコンを毎日変えると混乱します。

【Go言語】goqueryとゴルーチンで簡単高速Webスクレイピング

WebスクレイピングといえばPythonのイメージがありますが、

Go言語でのWebスクレイピングが簡単かつ高速だったので紹介します。

今回注目するのはgoqueryとゴルーチンです。

以下の記事を組み合わせたような構成です。

【毎秒1万リクエスト!?】Go言語で始める爆速Webスクレイピング【Golang】
Goとgoqueryでスクレイピング

goqueryとは

Go言語でjQueryのようにWebスクレイピングができるライブラリです。

GitHub
https://github.com/PuerkitoBio/goquery

使う前にgo getで事前に導入しておき、importで"github.com/PuerkitoBio/goquery"を追加しておきます。

使い方の例

// Documentの形式で取得
doc, err := goquery.NewDocument(url)

// 要素をFindとTextを使ってstringとして取得
// idで取得は # をつける
tex := doc.Find("#some_id").Text()
// classで取得は . をつける
tex := doc.Find(".some_class").Text()

// あとはよしなに

NewDocumentの処理としてはhttp.GetでWebページを取ってきて、html.Parseで解析しツリー構造化、最後に扱いやすいDocumentに変換します。

Findでツリー構造からの検索を行いよしなな結果を取得します。

goqueryはツリー構造からの要素検索が簡単に行える点が良いですね。

ゴルーチンとは

Go言語において簡単に並列処理化する仕組みのことで、スレッドなどのリソースを意識せずに使えるようになっています。

使い方も簡単で、並列化したい関数にgoを付けるだけです。gogo

注意点としては戻り値を返すような処理には使えないので、go func() {}()としてクロージャにしてあげるととよいでしょう。

また、並列化した処理がすべて終わるまで次の処理を待ちたい場合が多いため、WaitGroupを使ってすべての処理が終わるまで待ちます。

// 通信のような重い処理が存在する場合
func connect() {
    // 重い処理を行う
    // 通信は重い
    res, e := http.Get("通信先")
    // 重い処理終わり
}

// 何もせずforで回すと通信結果が返ってくるまで次の通信が走らない=直列処理
func main() {
    for i := 0; i < 10; i++ {
        connect()
    } 
}

// ゴルーチンを使うと前の通信結果を待たずに次の通信を走らせる=並列処理
func main() {
    for i := 0; i < 10; i++ {
        // 並列化したい関数にgoをつけるだけ
        go connect()
        // 並列化したい関数が戻り値を持つならクロージャにする
        go func () {
            v := connectWithReturn()
        } ()
    } 
}

// 実際はWaitGroupとともに使ってすべての処理が終わるまで待つ必要がある
func main() {
    // 並列処理のカウンタとして働く
    var wg sync.WaitGroup
    // 10カウンタを追加
    wg.Add(10)
    for i := 0; i < 10; i++ {
        go func () {
            connect()
            // カウンタ消費
            wg.Done()
        }()
    }
    // 10カウンタすべて消費されるまで処理を待つ
    wg.Wait()
}

Webスクレイピングをしてみる

例として当ブログのタイトルを取得してみます。

package main

import (
    "fmt"
    "sync"

    "github.com/PuerkitoBio/goquery"
)

// 通信してスクレイピング、ブログタイトル(or はてなID)を取得
func connect(count int) {
    doc, _ := goquery.NewDocument("https://tetsujp84.hatenablog.com/")
    tex := doc.Find("#title").Text()
    fmt.Printf(tex+":%d回目\n", count)
}

func main() {
    var wg sync.WaitGroup
    wg.Add(10)
    for i := 0; i < 10; i++ {
        // クロージャーでループ変数を使う場合はキャプチャが必要
        count := i
        go func() {
            connect(count)
            wg.Done()
        }()
    }
    wg.Wait()
    fmt.Println("終わり")
}
実行結果
鉄ブロ:3回目
鉄ブロ:7回目
鉄ブロ:5回目
鉄ブロ:0回目
鉄ブロ:4回目
鉄ブロ:2回目
鉄ブロ:6回目
鉄ブロ:8回目
鉄ブロ:1回目
鉄ブロ:9回目
終わり

並列化により実行順はバラバラになります。

go funcを除くだけで直列処理になります。楽ちん。

終わり

スクレイピングはサイトへのアクセスを繰り返すことになるのでほどほどにしましょう。
規約により禁止されていたらごめんなさい。

参考

Go言語でクロージャと goroutine を組み合わせた時に起こること

【iOS】GoogleAppScriptを使ってAppStoreへのアプリ更新を検知する

概要

AppStoreのアプリ更新を行ってもストアの影響により反映遅延が起こります。

ストア反映遅延の例

配信作業は完了しているのにストアに反映されない...。

反映まで30分かからない日もあれば6時間経っても反映されない日もあります。

Appleガイドラインでは最大24時間かかるらしいです。

Apple Store Reviewガイドライン

反映時間は日によって変わってしまうのですが原因は不明です。(Apple様のご機嫌次第)

まだかまだかと反映を待って何度も確認するのは面倒&心臓に悪いので、

GoogleAppScript(GAS)でSlackに通知してくれる仕組みを作りました。

手順

Slackのセットアップ

通知するSlackに通知用チャンネルを作りIncoming Webhookを設定しておきます(参考)

GoogleAppScriptの作成

以下のGoogleAppSctiptを作成します。

// iOSのストア反映を検知する
// トリガーで毎分実行する
// 更新が見つかるとSlackに通知する

// ・事前準備
// SlackのIncoming Webhookを作る
// ・事後準備
// 毎分起動するトリガーを設定する

// ストアのURLは https://apps.apple.com/jp/app/{アプリ名}/id{アプリID} の形式になっている
var appId = "アプリID";

// iTunes Search APIを用いてストア情報を取得する
// APIの結果はキャッシュが有効なのでパラメータを更新して取得する
var unixtime = new Date().getTime();
var iosUrl = "https://itunes.apple.com/lookup?id=" + appId + "&country=JP" + "&" + unixtime;

// プロパティKey
var iosKey = "iOSVersion";

// 新しいバージョンを検知
function checkNewVersion()
{
  // スクリプトプロパティから最終確認バージョン取得
  var userProperties = PropertiesService.getUserProperties();
  var currentVersion = userProperties.getProperty(iosKey);
  
  var storeVersion = getStoreVersion();
  
  // 新しいバージョンがあれば最終確認バージョンを更新しSlackに通知
  if (currentVersion != storeVersion) {
    userProperties.setProperty(iosKey, storeVersion);
    sendToSlack(storeVersion)
  }
}

// ストアバージョンの取得
function getStoreVersion() {
  var response = UrlFetchApp.fetch(iosUrl);
  var parsedData = JSON.parse(response);
  var version = parsedData["results"][0]["version"];
  return version;
}

// Slackに通知
function sendToSlack(body) {
  // SlackのWebhookURL
  var url = "SlackのWebhookURL(https://hooks.slack.com/services/XXXXXXXXXX)";
  var data = { "channel" : "{チャンネル名}", "text" : body};
  var payload = JSON.stringify(data);
  var options = {
    "method" : "POST",
    "contentType" : "application/json",
    "payload" : payload
  };
  var response = UrlFetchApp.fetch(url, options);
}

実行トリガーの設定

GoogleAppScriptエディタの 「編集」 > 「現在のプロジェクトのトリガー」からトリガーを追加します。

f:id:tetsujp84:20191212013246p:plain

今回の規模ならGoogleAppScriptは個人ユーザーの無料プランでも1分おきに実行できます。 (GoogleAppScript利用制限)

バージョンに更新があったらSlackに新しいバージョンが通知されます。

終わり

そもそも反映遅延が起こる原因と対策が知りたいところ。

参考

[iOS] iTunes Search API を利用してアプリの情報を取得する