tag:crieit.net,2005:https://crieit.net/users/kin-mi/feed きんみの投稿 - Crieit Crieitでユーザーきんみによる最近の投稿 2020-09-24T10:25:23+09:00 https://crieit.net/users/kin-mi/feed tag:crieit.net,2005:PublicArticle/16070 2020-09-24T10:25:23+09:00 2020-09-24T10:25:23+09:00 https://crieit.net/posts/unlock-bank ハッカー専用パズルゲームを作ったので全てネタバレする <blockquote> <p>当記事は <a target="_blank" rel="nofollow noopener" href="https://zenn.dev/kinmi/articles/b6646b4902dbda585c0b">ハッカー専用パズルゲームを作ったので全てネタバレする</a> のクロス投稿です</p> </blockquote> <h1 id="💰作ったもの💰"><a href="#%F0%9F%92%B0%E4%BD%9C%E3%81%A3%E3%81%9F%E3%82%82%E3%81%AE%F0%9F%92%B0">💰作ったもの💰</a></h1> <p><a href="https://crieit.now.sh/upload_images/aa565ccbf6a415ffff3d41f0f20fe93f5f6bf02787aff.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/aa565ccbf6a415ffff3d41f0f20fe93f5f6bf02787aff.png?mw=700" alt="UNLOCK BANK" /></a><br /> <a target="_blank" rel="nofollow noopener" href="https://unlock-bank.vercel.app/">UNLOCK BANK</a></p> <blockquote class="twitter-tweet"><p lang="ja" dir="ltr">@銀行口座をHackしたくてウズウズしている皆さん「違法行為はしたくないけどハッキングはしたい…」そんな貴方の悩みを解決するゲームをご用意しました。このゲームでは何をやっても合法です。是非、不正ログインしてみてください💰<a target="_blank" rel="nofollow noopener" href="https://t.co/LyIpYLCRrP">https://t.co/LyIpYLCRrP</a> <a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/UNLOCK_BANK?src=hash&ref_src=twsrc%5Etfw">#UNLOCK_BANK</a></p>— きんみ | ツイッター大喜利サイト🎍ついぎり🎍作りました🙄 (@_kinmi) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/_kinmi/status/1307988548591611904?ref_src=twsrc%5Etfw">September 21, 2020</a></blockquote> <h2 id="仕様"><a href="#%E4%BB%95%E6%A7%98">仕様</a></h2> <ul> <li>Account Number(数字8桁) と Password(数字4桁) によるログイン認証</li> <li>Account Number は全て存在する(8桁なので全口座数は1億口)</li> <li>Password を3回間違えるとロック(再試行不可)される</li> <li><em>[裏設定]</em> 銀行口座を模している為、もちろん<strong>パスワードが流出したら他者もログイン可能</strong></li> </ul> <p>ユーザーはどんな攻撃手法を用いても良い。<br /> 最近話題の1段階認証を突破してみようというパズルゲーム。</p> <h2 id="システム構成"><a href="#%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E6%A7%8B%E6%88%90">システム構成</a></h2> <ul> <li>JSフレームワーク: Nuxt.js 2.14.5(Vue.js 2.x)</li> <li>トランスパイル: TypeScript 4.0</li> <li>CSSフレームワーク: TailwindCSS</li> <li>ホスティング: Vercel</li> </ul> <p>お馴染みの構成を選択したが、Vue.jsの使用については少し後悔している(後述)</p> <h1 id="以下ネタバレ"><a href="#%E4%BB%A5%E4%B8%8B%E3%83%8D%E3%82%BF%E3%83%90%E3%83%AC">以下ネタバレ</a></h1> <h2 id="想定する解法"><a href="#%E6%83%B3%E5%AE%9A%E3%81%99%E3%82%8B%E8%A7%A3%E6%B3%95">想定する解法</a></h2> <p>基本的には以下の2つと考えている。<br /> - <strong>☠️リバースブルートフォース攻撃☠️</strong><br /> 件の金融事件で脚光を浴びた攻撃手法。<br /> 暗証番号の総当たり(ブルートフォース攻撃)は一般的なシステムだと試行回数に制限を設けているが、ユーザーIDの様な公開情報については秘匿性が低いため無制限に試行できるシステムが多い。そこを突いてパスワードは固定のままIDの方を総当たりするという攻撃。<br /> 特に当ゲームのような、認証情報に数値しか許容していないシステムだと少ない試行回数で突破できるため致命的な脆弱性となる。</p> <ul> <li><strong>😈リバースエンジニアリング😈</strong><br /> JavaScriptのみで構成している為、コードを解析してパスワードの取得が可能。<br /> 当初はNode.jsサーバー等を用意して認証処理を隠蔽するつもりだったがガチ攻撃された場合、サーバーか私の財布が死ぬと思い断念。<br /> しかし「ハッカー専用」と銘打っている以上、コードを覗くだけで突破されては面白くないので悪足掻きとして難読化を施している。</li> </ul> <h2 id="仕組みについて"><a href="#%E4%BB%95%E7%B5%84%E3%81%BF%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">仕組みについて</a></h2> <h3 id="🔐パスワード生成"><a href="#%F0%9F%94%90%E3%83%91%E3%82%B9%E3%83%AF%E3%83%BC%E3%83%89%E7%94%9F%E6%88%90">🔐パスワード生成</a></h3> <p>仕様として、口座番号とパスワードの紐付きを作らないといけない(パスワードが流出したら誰でもログインできるように)<br /> 最初は愚直に1億口座分のパスワードマッピングを定数で用意しようと考えた。<br /> しかし上述のコードを隠蔽できない事情によりリテラル値で持つのは避けたかった為、下記のライブラリを採用した。<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/davidbau/seedrandom">seedrandom.js</a><br /> これを用いてシード値から再現性のある乱数を生成(※1)する。</p> <p>下記のように口座番号をシード値とした乱数を生成した後、整形してパスワードとしている。</p> <pre><code class="ts">const seedrandom = require('seedrandom') function authenticated(account: string, password: string): boolean { const rng = seedrandom(account) const _pass = String(Math.round(rng() * 10000)).padStart(4, '0') return _pass === password } </code></pre> <p>これで口座番号とパスワードの紐付きを再現している。</p> <h4 id="seedrandom.jsの使用感"><a href="#seedrandom.js%E3%81%AE%E4%BD%BF%E7%94%A8%E6%84%9F">seedrandom.jsの使用感</a></h4> <ul> <li>🙆‍♂️ <strong>良かった所</strong> <ul> <li>直感的に使える</li> <li>複数の乱数アルゴリズムに対応している</li> <li><code>Math.random()</code>をラップできる為、テストにも使えそう</li> </ul></li> <li>🙅‍♂️ <strong>頑張って〜</strong> <ul> <li>型定義がない(2020/09現在、<a target="_blank" rel="nofollow noopener" href="https://github.com/davidbau/seedrandom/pull/70">issue</a>にて対応中)</li> </ul></li> </ul> <p>個人的には「再現性のある乱数を生成する処理」というのは<a target="_blank" rel="nofollow noopener" href="https://qiita.com/kinmi/items/ddd213bf7a09f67f68ee">バーコードバトラーライクのゲーム</a>を作る際に重宝するので、有難いライブラリ。</p> <h3 id="状態管理, 認証処理"><a href="#%E7%8A%B6%E6%85%8B%E7%AE%A1%E7%90%86%2C+%E8%AA%8D%E8%A8%BC%E5%87%A6%E7%90%86">状態管理, 認証処理</a></h3> <p>本筋と外れるが、クライアントで認証処理を行う為に口座番号と暗証番号をグローバル管理する必要があった。<br /> Vuexはオーバースペックなので状態を<a target="_blank" rel="nofollow noopener" href="https://ja.nuxtjs.org/guide/plugins/#%E7%B5%B1%E5%90%88%E3%81%95%E3%82%8C%E3%81%9F%E6%B3%A8%E5%85%A5">Inject</a>するプラグインを自作してstoreとして使用した。</p> <blockquote> <p>~/plugins/auth.ts</p> </blockquote> <pre><code class="ts">import Vue from 'vue' import { Plugin } from '@nuxt/types' import { Seedrandom } from '../types/seedrandom' const seedrandom = require('seedrandom') as Seedrandom type InjectTypeAuth = { /** * 口座番号 */ accountNumber: string /** * 暗証番号 */ password: string /** * 認証処理 * seedから4桁の乱数(先頭0埋め)を生成し、passwordとの比較結果を返却する * @param {string} seed シード値となる値 * @param {string} password パスワード */ authenticated(seed: string, password: string): boolean } declare module '@nuxt/types' { interface Context { $auth: InjectTypeAuth } interface NuxtAppOptions { $auth: InjectTypeAuth } } declare module 'vue/types/vue' { interface Vue { $auth: InjectTypeAuth } } type State = { accountNumber: string password: string } /********************************************** * 認証情報プラグイン * @param {Context} ctx * @param {(key: string, value: any) => void} inject */ const AuthPlugin: Plugin = (_ctx, inject) => { /** * Observable properties */ const state = Vue.observable({ accountNumber: '', password: '', } as State) function authenticated(seed: string, password: string): boolean { const rng = seedrandom(seed) const _pass = String(Math.round(rng() * 10000)).padStart(4, '0') if (_pass === password) state.password = _pass return _pass === password } /** * Injection */ inject('auth', { get accountNumber() { return state.accountNumber }, set accountNumber(accountNumber: string) { state.accountNumber = accountNumber }, get password() { return state.password }, authenticated, }) } export default AuthPlugin </code></pre> <p>※ 実際の乱数生成処理はもう少しノイズを入れてるので、このまま動かしても同じパスワードは生成されません</p> <p>この<code>authenticated</code>をEnterボタン押下時と、直アクセスを防ぐ目的でログイン後ページ内の<code>validate()</code>hookでも呼び出して認証処理としている。</p> <p>⚠️注意<br /> <em>当然ですが、フロントエンドでパスワードの一致チェック等の認証処理を行ってはいけません。</em></p> <p>Vue3のRCが外れて正式リリースとなったが、<a target="_blank" rel="nofollow noopener" href="https://v3.vuejs.org/api/basic-reactivity.html">Reactivity API</a>等を活用したVuex(ver 5)のリリースはまだ先の様なので暫く状態管理はこの手法に落ち着きそう。</p> <h2 id="攻撃方法の紹介"><a href="#%E6%94%BB%E6%92%83%E6%96%B9%E6%B3%95%E3%81%AE%E7%B4%B9%E4%BB%8B">攻撃方法の紹介</a></h2> <p>以上を踏まえ、解法の一つであるリバースブルートフォース攻撃の実施方法を紹介していく。<br /> 尚、当ゲームでは外部からブラウザ操作しやすいように主要な要素に対してid属性を付与している。<br /> <a href="https://crieit.now.sh/upload_images/8e2ef469b048d0e22a0b6e053a8a9f7d5f6bf3fec95cd.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/8e2ef469b048d0e22a0b6e053a8a9f7d5f6bf3fec95cd.png?mw=700" alt="振っているID一覧" /></a></p> <h3 id="前提知識: JavaScript の場合"><a href="#%E5%89%8D%E6%8F%90%E7%9F%A5%E8%AD%98%3A+JavaScript+%E3%81%AE%E5%A0%B4%E5%90%88">前提知識: JavaScript の場合</a></h3> <p>「<strong>システム構成</strong>」の節でも触れたが、Vue.jsの採用を後悔したのはここ。</p> <p>システムがHTML+PureJSの構成であれば、ブラウザの開発者コンソールから下記の様に実行して入力値を動的に与える事が可能。</p> <pre><code class="js">const digit1 = document.getElementById('digit1') digit1.value = "1" </code></pre> <p>これは一般的な方法だし、私も当初このやり方を想定していた。<br /> しかしVue.jsの場合、inputイベントを検知してVueインスタンス内に保有するデータを更新する。<br /> 直接input要素のvalue値を書き換えてもイベントは発火せず、データは更新されない。<br /> この挙動を想定しておらず、ユーザーに無駄なハードルを与えることとなってしまった。</p> <p>ではどうするのか。<br /> 当ゲームには数値をカウントアップ/カウントダウンするボタンを実装している。<br /> <a href="https://crieit.now.sh/upload_images/ffc2f4e46d2b2c361e83cb2dd2287bde5f6bf42460c65.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/ffc2f4e46d2b2c361e83cb2dd2287bde5f6bf42460c65.png?mw=700" alt="これ" /></a></p> <p><strong>金融システムって誰得UI多いよな</strong> という遊び心で実装したボタンだが、Vue内のデータを書き換えるイベントに直結している。<br /> このボタンをプログラムからクリックすることが出来れば口座番号入力の自動化が可能となる。</p> <p>ここから先の解説は、既にUNLOCK成功された<a target="_blank" rel="nofollow noopener" href="https://twitter.com/yoneapp">@yoneapp</a>さんが記事を書かれている為、そちらに任せることとする。<br /> <a target="_blank" rel="nofollow noopener" href="https://zenn.dev/yoneapp/articles/ec2892c7e2e5c499684d">リバースブルートフォース攻撃を使ってUNLOCK BANKの口座に不正ログインして優勝する</a>(Zenn)<br /> (改めて解説記事の執筆ありがとうございます🙇‍♂️)</p> <h3 id="前提知識: Vue.js の場合"><a href="#%E5%89%8D%E6%8F%90%E7%9F%A5%E8%AD%98%3A+Vue.js+%E3%81%AE%E5%A0%B4%E5%90%88">前提知識: Vue.js の場合</a></h3> <p>Vue.jsには <a target="_blank" rel="nofollow noopener" href="https://github.com/vuejs/vue-devtools">vue-devtools</a> というデバッグ用のブラウザ拡張が存在する。<br /> これを使えばコンポーネント構成を覗きつつ、各種データの書き換えやイベント発火が可能となるが通常はProduction環境で開けない。<br /> しかし、開発者ツールを使いこれをこじ開ける方法がある。</p> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/POPOPON/items/60010faae1eb4e4e67ac">本番公開されているサイトで Vue devtools を使う裏技</a>(Qiita)</p> <p>厳密にはソースの難読化を施してるのため上記記事と全く同じではないが、ソースを検索すれば<code>devtool</code>というキーワードは見つかるはず。<br /> あとはその後ろにブレークポイントを張り、<code>_0x1bd719.devtools = true</code> を実行すれば開発者ツールを開けることが可能。</p> <p><img src="https://storage.googleapis.com/zenn-user-upload/y04xwot067stonsyjc0dxwtwrbj6" alt="ProductionでDevtoolsを開いた図" /></p> <p>図の状態(Devtoolsでコンポーネントを選択した状態)だと、開発者コンソール上で<code>$vm0</code>というオブジェクトが使用出来る。これは選択したコンポーネントのVueインスタンスであり、コンポーネント内に存在するデータやメソッドが全て内包されている。<br /> あとはそこからアタリを付けて、自動化プログラムを組めば良い。<br /> 実際には下記をイジれば入力操作の自動化が可能となる。</p> <pre><code class="js">$vm0.accountNumbers // 口座番号(1桁ずつ格納した配列) $vm0.password // パスワード $vm0.enter() // Enterボタン押下時の処理を実行 </code></pre> <p>⚠️注意<br /> <em>初めてこれ(Productionでdevtoolsが使えること)を知った方はセキュリティ面に不安を覚えるかもしれませんが、それはお門違いです。</em><br /> <em>フロントエンドで保持するデータはユーザーから自由に改ざんされる前提であるべきです。</em></p> <h3 id="前提知識: Nuxt.js の場合"><a href="#%E5%89%8D%E6%8F%90%E7%9F%A5%E8%AD%98%3A+Nuxt.js+%E3%81%AE%E5%A0%B4%E5%90%88">前提知識: Nuxt.js の場合</a></h3> <p>Nuxt.jsで作られたアプリケーションは<code>window</code>直下に<code>$nuxt</code>というオブジェクトが作られる。<br /> そこには全ての情報が含まれており、上記のようにDevtoolsを開かずとも開発者コンソールからデータの書き換えやメソッドの実行が可能。<br /> しかし内包する情報量が膨大な為、開発者ではない第三者が操作したい対象を探すのは一苦労かもしれない。<br /> ちなみに当ゲームでは下記が自動化に必要な対象となる。</p> <pre><code class="js">window.$nuxt.$children[1].$children[0].$children[0].accountNumbers window.$nuxt.$children[1].$children[0].$children[0].password window.$nuxt.$children[1].$children[0].$children[0].enter() </code></pre> <h3 id="その他の手法"><a href="#%E3%81%9D%E3%81%AE%E4%BB%96%E3%81%AE%E6%89%8B%E6%B3%95">その他の手法</a></h3> <p><a target="_blank" rel="nofollow noopener" href="https://www.selenium.dev/documentation/ja/">Selenium</a> や <a target="_blank" rel="nofollow noopener" href="https://github.com/puppeteer/puppeteer">Puppeteer</a> 等を用いたブラウザ操作の自動化があげられる。<br /> 今のところ、これらを使ってUNLOCKしたという報告は観測していない。ブラウザの開発者ツールが万能すぎる。</p> <p>⚠️注意<br /> <em>悪意が無くともサーバーに高負荷をかける行為は法律により罰せられる可能性があります。</em><br /> <em>しかし、当ゲームで行う分には問題ありません。存分に攻撃してください。</em></p> <h3 id="更にネタバレ"><a href="#%E6%9B%B4%E3%81%AB%E3%83%8D%E3%82%BF%E3%83%90%E3%83%AC">更にネタバレ</a></h3> <p>もしproduction環境でログイン後のページを確認する必要が出た場合を考慮して、<br /> - <strong>Account Number</strong>: 1145-1419<br /> - <strong>Password</strong>: 1919</p> <p>と入力したら開発コンソールに上記口座のパスワードが出力されるようになっている。<br /> 攻撃するのは面倒くさいけどログインしてみたい、という方はどうぞ。</p> <h2 id="確率について"><a href="#%E7%A2%BA%E7%8E%87%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">確率について</a></h2> <p>数値4桁のパスワードというのは 0000 ~ 9999 の1万パターンしかない。<br /> パスワードを固定にして、口座番号を総当たりした場合の確率は下記で求められる。</p> <p><em>1 - ( 9999 / 10000 )^x</em><br /> x=試行回数</p> <p>(間違ってたらご指摘ください)<br /> 確率を表にするとこうなる。<br /> <div class="table-responsive"><table> <thead> <tr> <th>試行回数</th> <th>HITする確率</th> </tr> </thead> <tbody> <tr> <td>3回</td> <td>0.03%未満</td> </tr> <tr> <td>10回</td> <td>約0.1%</td> </tr> <tr> <td>100回</td> <td>約1%</td> </tr> <tr> <td>1,000回</td> <td>約10%</td> </tr> <tr> <td>10,000回</td> <td>約63%</td> </tr> <tr> <td>100,000回</td> <td>約99%</td> </tr> </tbody> </table></div></p> <p>つまり、口座番号8桁(1億パターン)全て試行せずとも、10万回程度で1口座はUNLOCKできてしまう。<br /> 下5桁総当たりすればいい計算で、実際にTwitterでUNLOCKされた方の反応も「思ったより早かった」という声が多かった。<br /> 当ゲームはパスワードを乱数によって生成しているが、これが本当の銀行口座の場合、パスワードの偏りが生じるはずなので更にUNLOCKは容易となるだろう。</p> <h1 id="おわり"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A">おわり</a></h1> <p>ネタのつもりで作ったんですが思いのほか反響があって、解説記事を書いて頂いたり、難読化をデコードしてリバースエンジニアリングまでやって下さってる方もいて、個人開発冥利に尽きるなと思いました。<br /> 他にも「こんなやり方あるよ」という方がいましたらご連絡ください。</p> <h1 id="注釈"><a href="#%E6%B3%A8%E9%87%88">注釈</a></h1> <p>※1: JavaScriptにはシード値から乱数を生成する機能が備わっていない</p> きんみ tag:crieit.net,2005:PublicArticle/15776 2020-03-22T17:10:52+09:00 2020-03-22T20:38:57+09:00 https://crieit.net/posts/web1week 敗者のweb1week <p>先週行われた <a href="https://crieit.net/boards/web1week-202003">web1week</a>。<br /> 初開催にも関わらず50を超えるサービスが登場し、大盛況のうちに幕を閉じた。</p> <p>経験者ならば理解を得られると思うが、個人開発という舞台には魔物が棲んでいる。<br /> 着手時点で思い描いた予定などペロリと喰われてしまう。</p> <p>不意に削られる作業時間。<br /> 理想と妥協の狭間で揺れる要件。<br /> 技術仕様の落とし穴。<br /> サービスの意義を自問自答し続ける日々。<br /> リリース直後に訪れる無慈悲な障害。</p> <p>これらを乗り越えローンチされた個人サービスの奥には、語られることのない物語が潜んでいる。<br /> そんなサービスが50以上も並ぶweb1weekのボードは圧巻で、たとえHello Worldの様なサービスでも私の心を踊らせてくれる。<br /> とても甲乙など付けられない。</p> <p>しかし「敗者」は存在する。<br /> 「web1week楽しみ!」と方方で意気込みを語り、「やってみませんか?」と様々な人を勧誘した挙句、あろうことか最小限の機能でさえ1weekに間に合わず、最終的には要件すら満たせなかった。<br /> 紛うことなき「敗者」だ。<br /> 個人開発界隈ではよく「リリースすることが大事」と言われている。その通りだ。リリースさえすれば勝ちなのだ。</p> <p>これは、そんな勝負に敗北を喫した男の記録である。</p> <h1 id="立案"><a href="#%E7%AB%8B%E6%A1%88">立案</a></h1> <p>月曜の午前0時、お題「Home」が発表された。<br /> こじつけでも何でも「Home」を絡ませたWebサービスならクリアとのこと。<br /> 初学者でも参加し易いうえに、CGMやIoTを絡めたサービスも構想でき、かつ疫病の脅威に怯える時勢に寄り添った素晴らしいお題だと思った。<br /> それと同時に頭を抱えた。<br /> 1週間で作り上げるとなると出来る事は限られている。ピンと来るアイディアが何も思い浮かばない。</p> <p>「自分が実装してみたい機能」というアプローチで考えてみる。<br /> 我が家には<a target="_blank" rel="nofollow noopener" href="https://store.google.com/jp/product/google_nest_hub">Google Nest Hub</a>というガジェットが存在する。ディスプレイ付きスマートスピーカーというイロモノだ。<br /> スマート家電のような贅沢品は存在しない我が家だが、昨年の私の誕生日に妻から贈ってもらっていた。しかし、動画を見るにはタブレットやテレビに転送で事足りるし、BGMをかける風習も無い。なかなか使う場面が訪れなかった。<br /> これを何とか活用できないか。</p> <p>玄関や冷蔵庫に貼ってるホワイトボード。これをNest Hubで代用できないかと考えた。<br /> 「いってらっしゃい」「おやつにわらび餅を冷蔵庫に入れてます」の様な、家族間のメッセージは手書きの方が嬉しかったりする。<br /> しかし、ホワイトボードのデメリットはホワイトボードの場所へ行かないと書けないことだ。<br /> 仕事中に「冷蔵庫にわらび餅入れてるの忘れてた!」となった場合、LINEに頼るしかない。<br /> スマホから手書きでメッセージを書き、玄関やリビングに置かれたNest Hubに表示されたらエモいのでは。</p> <p>Nest Hubで表示するWebアプリについて調べてみる。<br /> どうやら<a target="_blank" rel="nofollow noopener" href="https://developers.google.com/assistant/interactivecanvas">Interactive Canvas</a>というフレームワークで実装するらしい。<br /> しかし、基本的にVUI(音声ユーザーインターフェース)が前提となる作りで、ストアへ並べるにはGoogleの承認も必要となる。<br /> 今回の要件・納期では厳しい、、</p> <p>方針を変える。<br /> Nest HubにはGoogle Photosにあるアルバムを選択して、フォトフレームとして表示する機能がある。<br /> Nest Hub側でメッセージ画像を入れたアルバムを選択して、Webサービス側ではそのメッセージ画像を更新すれば良いのはないだろうか。<br /> 調べてみた。さすがGoogle様。ちゃんと<a target="_blank" rel="nofollow noopener" href="https://developers.google.com/photos">Google PhotosのAPI</a>も存在する。</p> <p>これでいこう。</p> <ol> <li>WebサービスにGoogleアカウントでログイン</li> <li>手書きUIのCanvasでメッセージを書く。</li> <li>Nest Hub表示用のアルバムをGoogle Photosに作成</li> <li>Canvasを画像化して3.のアルバムにアップロード</li> <li>既にメッセージ画像が存在する場合は削除(アルバム内の画像は1枚のみとする)</li> <li>Nest Hubで表示</li> </ol> <p>OAuth2認証をクライアントで完結できれば、DBすら不要。<br /> この程度なら1週間でいける。</p> <p>市場調査もしてみた。</p> <blockquote class="twitter-tweet"><p lang="ja" dir="ltr">スマートディスプレイ(ディスプレイ付きスマートスピーカー)がどれぐらい普及してるのか気になるのでアンケお願いします!持ってるか持ってないか。押し入れで眠っててもOKです!</p>— きんみ | ツイッター大喜利サイト🎍ついぎり🎍作りました🙄 (@_kinmi) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/_kinmi/status/1236827457027592192?ref_src=twsrc%5Etfw">March 9, 2020</a></blockquote> <p>うん、Nest Hub専用サービスは良い感じのスキマ産業のようだ(強がり)</p> <p>Nuxt.jsでプロジェクトを作成して今日の作業は終了。<br /> この時点で月曜の夜。順調だ。</p> <h1 id="裏の目的"><a href="#%E8%A3%8F%E3%81%AE%E7%9B%AE%E7%9A%84">裏の目的</a></h1> <p>常々思っていたのが「もっと手軽に画像アップロード機能を作れないか」という事だった。<br /> 画像アップロードというのは、ストレージを用意して、アップされる画像の最適化処理を組み、著作権の侵害等を考慮して規約や同意UIの設計をしなければならない。<br /> リリース後は不適切な画像が上がってないかウォッチする義務が発生するし、ストレージの残容量も気にしなければならない。<br /> 更に、増え続ける画像の管理方法(一定期間で削除するか、お金で解決するか)も検討しなければならない。<br /> 個人開発において、割とコストが高めの機能だ。</p> <p>この課題はGoogle Photos等のユーザーが保有するクラウドストレージに公開状態で保存すれば解決するのではないかと考えた。<br /> このご時世、Googleアカウントは誰でも持っている。アカウント登録時に自動で付与されるGoogle Photosの容量は高画質モードなら無制限だし、あくまで「画像を公開しているのはユーザー」という体裁を繕えるので著作権問題もグレーゾーンとなる(漫画村方式)<br /> サービス側で保有するのは画像の静的URLのみだ。</p> <p>今回、Google PhotosのAPIを使いこなせる様になれば今後の個人開発に幅を持たせることが出来るかもしれない。</p> <h1 id="ハマる"><a href="#%E3%83%8F%E3%83%9E%E3%82%8B">ハマる</a></h1> <p>コロナ自粛の影響により勤務時間が削減され、個人開発の時間は比較的多めに取れた。<br /> それでも間に合わなかったのは全てにハマったからだ。<br /> 全てだ。<br /> 時系列でハマりポイントを紹介していく。</p> <h2 id="JS ClientSDKが使いづらい"><a href="#JS+ClientSDK%E3%81%8C%E4%BD%BF%E3%81%84%E3%81%A5%E3%82%89%E3%81%84">JS ClientSDKが使いづらい</a></h2> <p>Google APIをWebで利用する場合、<a target="_blank" rel="nofollow noopener" href="https://github.com/google/google-api-javascript-client">Google API Client Library for JavaScript</a>(GAPI)というライブラリを利用する。<br /> Initial Commitは2011年。それなりに年季の入ったライブラリだ。</p> <h3 id="NPMには登録されていない。"><a href="#NPM%E3%81%AB%E3%81%AF%E7%99%BB%E9%8C%B2%E3%81%95%E3%82%8C%E3%81%A6%E3%81%84%E3%81%AA%E3%81%84%E3%80%82">NPMには登録されていない。</a></h3> <p>scriptタグで読み込む必要がある。<br /> Nuxt.jsで外部スクリプトを使用する場合、<code>$nextTicks()</code>を用いてもスクリプトのロードが完了していない場合がある。<br /> ロード状態を監視する必要があるので少し手間だ。</p> <h3 id="DiscoveryDocs"><a href="#DiscoveryDocs">DiscoveryDocs</a></h3> <p>GAPIのスクリプトを読み込んだ時点では<code>init()</code>等、最低限の機能しか保有していない。<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/google/google-api-javascript-client/blob/master/docs/start.md">Getting Started</a>に記載されているが、API Key等を設定して初期化する際に<code>DiscoveryDocs</code>というURLを設定する。<br /> DiscoveryDocsはAPI単位で用意されており、URLの先にあるJSONをGAPIでロードすれば機能として利用できる。<br /> Google Photos APIのDiscoveryDocsは下記となる。<br /> <a target="_blank" rel="nofollow noopener" href="https://content.googleapis.com/discovery/v1/apis/photoslibrary/v1/rest"><code>https://content.googleapis.com/discovery/v1/apis/photoslibrary/v1/rest</code></a><br /> スクリプトの肥大化を防ぐための機構だろう。<br /> ここまではまだ良い。手間を感じるがまだ許せる。</p> <h3 id="同期処理"><a href="#%E5%90%8C%E6%9C%9F%E5%87%A6%E7%90%86">同期処理</a></h3> <p>ドキュメントのサンプルはthenチェーンで記載されている。<br /> じゃあ・・・と思って<code>async / await</code>で書き直してみる。動かない。<br /> そう、返却値は<strong>ES6 Promiseじゃない</strong>。<br /> <a target="_blank" rel="nofollow noopener" href="https://google.github.io/closure-library/api/goog.Thenable.html">goog.Thenable</a>という独自インターフェースを継承したオブジェクトだ。<br /> <code>init().then(()=>{ })</code> の中でしかGAPIは動かない。同期的にいくつものAPIを使いたければthenのネストを深め続けるしかない。F*ck。</p> <p>ES6が標準化されたのが2015年。このライブラリの開発当初ならスマートな仕様だったのだろう。<br /> しかし、こちとら去年からJSを学び始めた身。<br /> 終始、この仕様に慣れず生産性が低下した。</p> <h2 id="認証で躓く"><a href="#%E8%AA%8D%E8%A8%BC%E3%81%A7%E8%BA%93%E3%81%8F">認証で躓く</a></h2> <p>ユーザー管理には<a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/auth?hl=ja">Firebase Authentication</a>を採用した。<br /> 手軽に導入でき、「特定のユーザーには使わせない」といったセキュリティ対策も出来る。<br /> Twitter API等、他社のAPIと連携する場合はトークンの保管といった一手間が必要になってくるが、今回はGoogle Platform内で連携するだけ。<br /> 簡単だろうと思った。</p> <h3 id="🙅 Firebase → Google API"><a href="#%F0%9F%99%85+Firebase+%E2%86%92+Google+API">🙅 Firebase → Google API</a></h3> <p>以下の記事を参考に、Firebase UIでログイン→GAPIのイニシャライズを試みる。<br /> <a target="_blank" rel="nofollow noopener" href="https://medium.com/google-cloud/using-google-apis-with-firebase-auth-and-firebase-ui-on-the-web-46e6189cf571">Using Google APIs with Firebase Auth and Firebase UI on the Web</a></p> <p><strong>できない。</strong><br /> この記事ではFirebaseでログインした時点で <em>GAPIの認証状態</em> : <code>gapi.auth2.getAuthInstance().isSignedIn.get()</code>が <code>true</code> になる想定だが <code>false</code> だ。<br /> GAPIでもログインする必要があるのか?と思って試したがFirebaseとGAPIで二重ログインされてしまった。</p> <p>記事のコメント欄に<a target="_blank" rel="nofollow noopener" href="https://medium.com/@slashdrew/this-appears-to-be-no-longer-functional-cc2ca49db640">記載がされている</a>が、現在はFirebaseでログインしてもGAPIでログイン状態を検知しない。<br /> 下記のissueにもその事が記載されており、ステータスはopenのままだ。<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/google/google-api-javascript-client/issues/561">https://github.com/google/google-api-javascript-client/issues/561</a></p> <h3 id="🙆 Google API → Firebase"><a href="#%F0%9F%99%86+Google+API+%E2%86%92+Firebase">🙆 Google API → Firebase</a></h3> <p>つまり現状、FirebaseとGAPIを連携するには認証順を逆にしないといけない。<br /> <em>GoogleAPIのサインイン</em> : <code>gapi.auth2.signIn()</code>でログインした後、認証情報を取得して <em>Firebaseの認証情報を使用したサインイン</em> : <code>firebase.auth().signInWithCredential()</code>でもログインするフローとなる。<br /> 下記の記事を参考に実装した。<br /> <a target="_blank" rel="nofollow noopener" href="https://fireship.io/snippets/how-to-use-google-apis-or-gapi-with-firebase-auth/">How to Use Google APIs on the Web</a></p> <h3 id="進捗:だめです"><a href="#%E9%80%B2%E6%8D%97%EF%BC%9A%E3%81%A0%E3%82%81%E3%81%A7%E3%81%99">進捗:だめです</a></h3> <p>この時点で既に平日は終わっていた。残すところは土日のみ。<br /> 対応が長期化した要因は「出来ると書いてあったから」。<br /> Firebase→GAPIの認証は不可能という可能性を考慮していなかった。<br /> この考慮漏れが発生するとプログラマーの思考はどうなるか。<br /> <strong>ひたすらタイポを探し続ける</strong>のである。</p> <h2 id="画像アップロードが用意されてない"><a href="#%E7%94%BB%E5%83%8F%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%E3%81%8C%E7%94%A8%E6%84%8F%E3%81%95%E3%82%8C%E3%81%A6%E3%81%AA%E3%81%84">画像アップロードが用意されてない</a></h2> <p>まだ間に合う。<br /> 土曜にアップロード機能を実装。<br /> 日曜にデザインを微修正&実機で動作確認。<br /> ここまで盛大に躓いて尚、根拠なく「出来る」と盲信する。<br /> 進捗が遅れてるプログラマーにありがちな<strong>逆算スケジュール</strong>である。</p> <h3 id="🙅 GAPIで画像アップロード"><a href="#%F0%9F%99%85+GAPI%E3%81%A7%E7%94%BB%E5%83%8F%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89">🙅 GAPIで画像アップロード</a></h3> <p>ガイドラインを読んでみる。<br /> <a target="_blank" rel="nofollow noopener" href="https://developers.google.com/photos/library/guides/upload-media">https://developers.google.com/photos/library/guides/upload-media</a><br /> どうやら画像をアップロードするには、二段階の手順を踏まないといけないようだ。</p> <ol> <li>画像のバイナリデータをGoogleにアップロードする</li> <li>上記返却値に含まれるアップロードトークンを用いてアルバムに追加する。</li> </ol> <p>Google Photosの<a target="_blank" rel="nofollow noopener" href="https://content.googleapis.com/discovery/v1/apis/photoslibrary/v1/rest">DiscoveryDocs</a>から該当のメソッドを探してみる。<br /> <strong>無い。</strong><br /> 何度検索しても「uploads」という処理は無い。<br /> 「なぜ無いのか」なんて考える余裕も無い。<br /> (現在、不安になって再度探してみたがやっぱり無い)</p> <p>GAPIじゃGoogleに画像をアップロード出来ない・・・?</p> <h3 id="🙅 axiosでリクエスト"><a href="#%F0%9F%99%85+axios%E3%81%A7%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88">🙅 axiosでリクエスト</a></h3> <p>悩んでいる暇は無い。もうクライアントSDKは捨てる。<br /> ドキュメントにはエンドポイントが記載されているので、そこに対して<code>axios</code>でPOSTリクエストを投げてみる。</p> <p><strong>Network Error.</strong></p> <p>・・・久し振りに見たぜ。<br /> <code>has been blocked by CORS policy: No 'Access-Control-Allow-Origin'</code><br /> <a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/HTTP/CORS/Errors">CORSエラー</a>だ。<br /> おいおい、嘘だろ。CORSだと?<br /> 浅学だが、基本的にはAPI側での対応が必須だったと記憶している。詰んだか?</p> <h3 id="🙅 XMLHttpRequestでリクエスト"><a href="#%F0%9F%99%85+XMLHttpRequest%E3%81%A7%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88">🙅 XMLHttpRequestでリクエスト</a></h3> <p>ドキュメントには「CORSもサポートしているよ」と書かれている。<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/google/google-api-javascript-client/blob/master/docs/cors.md">How to use CORS to access Google APIs</a><br /> クライアントSDKを用いれば回避できるよ。と。<br /> <strong>使えねぇんだよチクショウが。</strong><br /> 記事の最後に<a target="_blank" rel="nofollow noopener" href="https://developer.mozilla.org/ja/docs/Web/API/XMLHttpRequest"><code>XMLHttpRequest</code></a>を用いた例も記載されている。<br /> 存在は知っていたが初めて使うなこれ。<br /> 愚直に書いてある通り組んでみる。</p> <p><code>has been blocked by CORS policy: No 'Access-Control-Allow-Origin'</code></p> <p>泣く。</p> <h3 id="🙅 gapi.client.request()"><a href="#%F0%9F%99%85+gapi.client.request%28%29">🙅 gapi.client.request()</a></h3> <p>まだだ。諦めない。<br /> 俺は長男だから我慢できたけど次男だったら我慢できなかった。</p> <p>ドキュメントを漁り、もう1つの可能性を見つける。<br /> クライアントSDKには標準で <em>APIを叩く為のメソッド</em> : <a target="_blank" rel="nofollow noopener" href="https://github.com/google/google-api-javascript-client/blob/master/docs/reference.md#api-requests">gapi.client.request()</a>が備わっている。<br /> GoogleAPIのエンドポイントをベタ書きして引数に渡せば叩けるらしい。<br /> クライアントSDKならCORSを回避できるんだろ?信じるぞ?<br /> 叩いてみる。</p> <p><strong>404</strong></p> <p>???<br /> タイポ探しフェーズ(数時間)に入るも、やっぱり問題は無い。<br /> リクエスト内容を確認した。POST先のURLが変わっている。<br /> 画像アップロードのエンドポイントは<code>https://photoslibrary.googleapis.com/v1/uploads</code>だが、ドメイン部が<code>content.googleapis.com</code>に変わってる。<br /> なんだこれは。<br /> 恐らく、クライアントSDKが書き換えていると推測。M*ther F*cker。</p> <p>諦めた。</p> <p>クライアントで完結することを。</p> <h3 id="🙆 Netlify Functions"><a href="#%F0%9F%99%86+Netlify+Functions">🙆 Netlify Functions</a></h3> <p>ホスト先にNetlifyを利用している。<br /> そこでクラウド関数を作れるNetlify Functionsを使ってみようと思い立った。<br /> 使った経験が無い機能だ。下記の記事を参考にHello Worldから始めてみる。<br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/Sr_Bangs/items/7867853f5e71bd4ada56">【入門】Netlify Functionsコトハジメ</a><br /> そして「Googleに画像をアップロードしてアップロードトークン返すクラウド関数」を立ててみた。<br /> うまくいった。<br /> 初めて使う機能なのに、ハマらず動く。ローカル環境もすぐ出来た。<br /> 感動した。モダン最高。</p> <h3 id="進捗:納期は昨日なので大丈夫です"><a href="#%E9%80%B2%E6%8D%97%EF%BC%9A%E7%B4%8D%E6%9C%9F%E3%81%AF%E6%98%A8%E6%97%A5%E3%81%AA%E3%81%AE%E3%81%A7%E5%A4%A7%E4%B8%88%E5%A4%AB%E3%81%A7%E3%81%99">進捗:納期は昨日なので大丈夫です</a></h3> <p>この時点で日曜の夜。<br /> 軸となる機能は一通り完成したものの、「同名アルバムが存在していた場合」などのシチュエーションに応じた分岐は作れてないし、要件として「既にメッセージ画像が存在していた場合は更新する」といった機能も存在するが未実装だ。実機確認もやれてない。</p> <p>「ごめんなさい」しつつ、月曜に出そう。<br /> そう決めて床についた。</p> <h2 id="画像の削除/更新はできない"><a href="#%E7%94%BB%E5%83%8F%E3%81%AE%E5%89%8A%E9%99%A4%2F%E6%9B%B4%E6%96%B0%E3%81%AF%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84">画像の削除/更新はできない</a></h2> <p>ベットに入った直後、悪寒がした。</p> <p><strong>見覚えがない。</strong></p> <p>散々GoogleAPIのドキュメントと格闘し続けたが、Google Photos内の写真を<strong>削除/更新するエンドポイントは見た記憶がない</strong>。<br /> スマホを取り出し確認してみるも、やはり無い。</p> <p>冗談だろ?クラウドストレージを操作するAPIのくせに削除/更新は出来ない?そんな馬鹿な話があるか。<br /> 調べてみる。<br /> <a target="_blank" rel="nofollow noopener" href="https://issuetracker.google.com/issues/109759781">https://issuetracker.google.com/issues/109759781</a><br /> 結論:<strong>出来ないらしい</strong></p> <p>詰みです。お疲れ様でした。</p> <p>過去画像を含めてランダム表示されるなんてホワイトボードじゃない。</p> <p>「お手数ですがGoogle Photosを開いて、ご自身で過去の画像を削除してください。」<br /> しょーもな。<br /> サービスとして成立していない。</p> <p>・・・いや、まだだ。</p> <p>よし、譲ろう。</p> <p>百歩譲ろう。</p> <p>Google Driveなら削除/更新もAPIで可能じゃなかろうか。<br /> Photosなんかより歴史あるクラウドストレージだ。<br /> Google Driveへアップした写真をNest Hubで表示できれば問題無い。</p> <p><a target="_blank" rel="nofollow noopener" href="https://support.google.com/googlenest/thread/671055">Can I connect my Google Drive to my Google home hub? </a><br /> 結論:<strong>出来ないらしい</strong></p> <p>こうして私は、web1weekの敗者となった。</p> <h2 id="Safari対応(おまけ)"><a href="#Safari%E5%AF%BE%E5%BF%9C%EF%BC%88%E3%81%8A%E3%81%BE%E3%81%91%EF%BC%89">Safari対応(おまけ)</a></h2> <p>負け犬でも遠吠えぐらいは出来る。<br /> 「手書き画像をGoogle Photosへアップするサービス」としてローンチしよう。<br /> そう決めて実機確認に入った。</p> <h3 id="CORSぞ"><a href="#CORS%E3%81%9E">CORSぞ</a></h3> <p>MacのChrome、Android Chromeは問題なく動作完了。<br /> 妻に「iPhoneでこれ使えるか確認してほしい」と依頼する。<br /> 返信は「画面が驚きの白さ」。<br /> ここで初めてMacのSafariでも動作確認してみる。</p> <p><code>has been blocked by CORS policy: No 'Access-Control-Allow-Origin'</code></p> <p><strong>CORSぞ</strong>。</p> <p>何度立ちはだかれば気が済むのだCORSよ。<br /> まぁ思い当たる節はあった。<br /> GAPIが使い辛いので、下記のようにクラアントSDKを動的に読み込んでplugin化していたのだ。</p> <pre><code class="javascript">export default async ({ $axios, store }, inject) => { // GAPIの読み込み const gapiScript = document.createElement('script') const src = await $axios.$get('https://apis.google.com/js/api.js') gapiScript.appendChild(document.createTextNode(src)) document.head.appendChild(gapiScript) const gapi = window.gapi // ロード処理(Promise化) const clientLoad = new Promise((resolve, reject) => { gapi.load('client:auth2', () => { resolve() }) }) // GAPI初期化処理 const init = () => { return clientLoad.then(() => { return gapi.client.init({ apiKey: authConfig.Google.apiKey, clientId: authConfig.Google.clientId, discoveryDocs: authConfig.Google.discoveryDocs, scope: authConfig.Google.scopes.join(' ') }) }) } inject('gapi', gapi) inject('gapiInit', init) } </code></pre> <p>不安はあったが、動いたのでそのままにしていた。<br /> 外部スクリプトを動的に読み込む場合、普通はCORSの問題が発生する。<br /> 素直にscriptタグで読み込むように変更した。</p> <p>というか、なぜChromeでは読み込めたのか分からない。<br /> 同じGoogle製品だから?<br /> プラットフォーマー恐るべし。</p> <h3 id="クロスサイトトラッキング"><a href="#%E3%82%AF%E3%83%AD%E3%82%B9%E3%82%B5%E3%82%A4%E3%83%88%E3%83%88%E3%83%A9%E3%83%83%E3%82%AD%E3%83%B3%E3%82%B0">クロスサイトトラッキング</a></h3> <p>さて、初期表示までは問題なくSafariで動作した。<br /> しかしGoogleへログインしても認証状態を検知しない。<br /> プログラムが「ログイン中」と判断しないのだ。<br /> こればっかりは思い当たる節も無い。</p> <p>issueが上がっていた。<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/google/google-api-javascript-client/issues/503">gapi auth2 issue on safari</a><br /> 同様の事象、かつオープンのままだ。<br /> Safariの場合、Cookieを全削除するか、プライベートブラウズなら問題無いらしい。<br /> マジかよ・・・<br /> 頑張れば何か対策できるのかもしれないが、負け犬にそんな根性は無い。<br /> UserAgentから「Safari、もしくはiPhone/iPad」を判定して、該当する場合は長ったらしい注意文言を表示するようにした。</p> <h1 id="敗因"><a href="#%E6%95%97%E5%9B%A0">敗因</a></h1> <p>次回以降のweb1weekで勝つための敗因分析を行う。</p> <h2 id="PoC(概念実証)不足"><a href="#PoC%EF%BC%88%E6%A6%82%E5%BF%B5%E5%AE%9F%E8%A8%BC%EF%BC%89%E4%B8%8D%E8%B6%B3">PoC(概念実証)不足</a></h2> <p>一番の原因はこれ。<br /> 事前に「画像の削除/更新は出来ない」と知っていれば別案を採用していた。<br /> とはいえ、1週間という過密スケジュールで検証工程を取れるかは疑問が残る。<br /> 少なくとも、</p> <ol> <li>機能の洗い出し</li> <li>実装に必要な外部インターフェースのドキュメントはちゃんと読む</li> </ol> <p>まぁ、、当たり前のことはちゃんとしよう。ということ。</p> <h2 id="「出来ない」の判断が遅い"><a href="#%E3%80%8C%E5%87%BA%E6%9D%A5%E3%81%AA%E3%81%84%E3%80%8D%E3%81%AE%E5%88%A4%E6%96%AD%E3%81%8C%E9%81%85%E3%81%84">「出来ない」の判断が遅い</a></h2> <p>認証や画像アップロード等、ハマった際に「出来ない」という判断が出来ていない。<br /> 昨今の潤沢な開発環境に甘え、「出来ない事は無い」と思い込んでいる節がある。<br /> 確かに大抵の場合、ハマる原因は外部インターフェースの仕様変更、もしくはタイポが多い。<br /> 塩梅が難しいが、もっと早いタイミングでissueを探そう。</p> <h1 id="Google Photosを利用した画像アップローダーは作れるのか"><a href="#Google+Photos%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%9F%E7%94%BB%E5%83%8F%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%80%E3%83%BC%E3%81%AF%E4%BD%9C%E3%82%8C%E3%82%8B%E3%81%AE%E3%81%8B">Google Photosを利用した画像アップローダーは作れるのか</a></h1> <p>冒頭に記した「裏の目的」について。<br /> そもそもGoogle Photos APIの<a target="_blank" rel="nofollow noopener" href="https://developers.google.com/photos/library/guides/acceptable-use">利用規約</a>には「ホスティングサービスとして使うならCloud Storageを使え」と明記してある。<br /> 開発するサービスを「Google Photosへアップした写真の共有ギャラリー」という位置付けにするならグレーかな?と思ったが、<br /> 結論から言うと「多分、不可」だ。<br /> 少なくとも、そう言うサービスを作れるだけのAPIが提供されていない。</p> <p>まず前述の通り、API経由での画像の削除/更新は出来ない。<br /> あくまで画像のアップロードのみで、アルバム間の移動すら出来ない。</p> <p>そして、更に致命的なことに<strong>静的なURLは取得できない</strong>。<br /> 写真のURLを取得するAPIは存在するが、一定期間が過ぎると無効化されるらしい。<br /> これはGoogle Photosの「URLで共有する」と同じURLなのだろう。<br /> URL先には静的な画像が埋め込まれているらしいが、それを取得するAPIは存在しない。<br /> スクレイピングを駆使すれば取得できるかもしれないが、実用的では無いだろう。</p> <p>「ユーザーのプライベートストレージを用いた画像アップローダー」は別サービスを検討した方が良さそうだ。</p> <h1 id="ゴミを公開する"><a href="#%E3%82%B4%E3%83%9F%E3%82%92%E5%85%AC%E9%96%8B%E3%81%99%E3%82%8B">ゴミを公開する</a></h1> <p>これはweb1weekじゃない。<br /> 私が作りたかったものでもない。<br /> ゴミだ。</p> <p><a target="_blank" rel="nofollow noopener" href="https://nest-board.netlify.com/">Nest Board</a><br /> <a target="_blank" rel="nofollow noopener" href="https://nest-board.netlify.com/">https://nest-board.netlify.com/</a></p> <p>Google APIは申請無しで使用できるが、Googleから承認されていない場合、ログイン時に警告画面が出る。<br /> 利用する際は注意文言を読んで頂きたい。</p> <p>需要あるか分からないが、Nest Hub側で表示する際の手順も明記しておく。</p> <blockquote> <p><strong>1. スマホ(iPhone/Android)でGoogle Homeアプリを開く</strong><br /> Nest Hubの初期設定に必要なアプリなのでインストールされているはず。<br /> <br /> <strong>2. 表示したいNest Hub > フォトフレームを編集 > Google フォト の順で選択</strong></p> <p><strong>3. Nest Boardで作成したアルバムを選択する</strong><br /> 初期値は「Nest Board」<br /> <br /> <strong>4. 「フォトフレーム」画面の下部にある「個人的な写真の整理」を「リアルタイム共有アルバムのみ」にする</strong><br /> これを設定しないとNest Hubがフォトフレームに不向きな写真と判定して表示されない。<br /> ハマりポイント。</p> </blockquote> <p>以上の手順を踏めば、Nest Hubに手書きのメッセージ画像が表示される。</p> <p><a href="https://crieit.now.sh/upload_images/e5d58ba13a25c14fac70e13548eff8d95e770af16b1cc.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/e5d58ba13a25c14fac70e13548eff8d95e770af16b1cc.jpg?mw=700" alt="敗北者じゃけぇ" /></a></p> <p>普段はソースコードを公開したりしないのだが、今回はリポジトリをpublicにした。<br /> なぜなら敗者だからだ。<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/kin-mi/nest-board">https://github.com/kin-mi/nest-board</a></p> <h1 id="おわり"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A">おわり</a></h1> <p>次は勝つ。</p> きんみ tag:crieit.net,2005:PublicArticle/15640 2019-12-25T17:06:57+09:00 2019-12-25T17:06:57+09:00 https://crieit.net/posts/b65b1da3aaaca6b4260d9e9065dd6aaf 私の経歴書 <p><img src="https://assets.st-note.com/production/uploads/images/17159577/rectangle_large_type_2_468591c3888f966f722d844566528754.jpeg?fit=bounds&quality=60&width=1280" alt="Eye Catch" /></p> <blockquote> <p>この記事はcrieitのなんでも <a href="https://crieit.net/advent-calendars/2019/crieit">Advent Calendar 2019</a>の20日目の記事です。</p> </blockquote> <p>私がプログラミングを始めたのは中学生の時だった。</p> <p>それほど裕福な家庭ではなかったが、作曲が趣味の母親が給与数ヶ月分を叩いてVAIOのデスクトップを買ってきた。<br /> 当時、私はパソコンのことはよく分からなかったが「色んなことが出来る機械」と漠然なイメージを持っていたのを覚えている。<br /> なので当然、ゲームも出来るはず、と思い”Internet”と書かれてあるアイコンをクリックしてYahoo!を開き「ゲーム 無料」で検索しては手当たり次第遊んでいた。<br /> 今思えば、身を削って購入した高級な端末を子供に触らせるなんて、母親は怖くなかったのかと思うが、特に何も言わず自由に使わせてくれた(ダイヤルアップ接続だった時期は「夜だけにしろ」とだけ言われていた)</p> <h1 id="創作欲と出会った話"><a href="#%E5%89%B5%E4%BD%9C%E6%AC%B2%E3%81%A8%E5%87%BA%E4%BC%9A%E3%81%A3%E3%81%9F%E8%A9%B1">創作欲と出会った話</a></h1> <p>中学校には私と同じようにパソコンゲームに興じる友人が2人いた。<br /> 俗に言うWeb1.0時代、無料で遊べるゲームは沢山あったが、私たちはブラウザだけで遊べるゲームにハマっていた。有名所であれば「箱庭諸島」や「バトルロワイヤル」といったゲームである。<br /> ページ遷移でゲームが進行する形式で、ユーザー同士の対戦やチャット等、プレイヤー間でコミュニケーションを取れる機能が組み込まれていることが多かった。</p> <p>そんな時代、私たちはVagoo(ヴァグー)というゲームに夢中になっていた。「うぇぶなげぇむ」という個人で運営されてるサイトにあったRPGだ(現在は閉鎖中)<br /> 上記に並べたゲームに比べるとマイナーで、モンスターと戦ってストーリーを進めるというシンプルな作りだが、キャラメイク機能やモンスターをペットに出来る機能など、クオリティの高いゲームだった。<br /> 索敵をするには一定時間待つ必要があり、その間ユーザーはゲーム内のチャットに集まる。ゲーム自体も面白かったが、そこで顔も知らない人達と夜通し会話をする体験が思春期の私にはとても刺激的で楽しかった。</p> <p>そのゲームはコピーレフト(オープンソース)として配布されていた。<br /> 当時、GitHub等のリポジトリサービスは無く、zipファイルが直リンクで置かれているだけだった。<br /> これを動かせば自分だけのゲームが作れるかもしれない。<br /> 動かし方を必死に調べた。</p> <p>CGI/Perlで組まれているプログラムだが、それを動かせる無料レンタルサーバーというのは中々無い時代だった。<br /> 月数百円で運用出来るサーバーはあったが、中学生にはそれでもキツい。生まれて初めて「早く大人になりたい」という感情を抱いた。<br /> 調べていく内に、自分のPCをWebサーバーに出来ることを知った。幸い、母親が買ってきたこの端末は性能が良かったので問題無くApache HTTP Serverを導入出来た。僕は無敵になった。</p> <p>自分のPCで動いているVagooを見て、とても感動した。<br /> ソースコードの中身はよく分からないが"Vagoo"と書かれていた文字列を書き換えるとゲームタイトルが変わる。<br /> 時間を設定していそうな数値を書き換えると索敵の待機時間が変わる。<br /> 予測して、書き換えて、結果を確認する。<br /> この循環がとても楽しかった。<br /> この過程で条件分岐や反復処理、HTML/CSSなど基礎的な知識は身についた。</p> <p>冒頭に「プログラミングを始めたのは中学生の時」と書いたが、当時の私には「始めた」という感覚は無く、無意識でノートの端に描く落書きと似たような感覚だった。<br /> 良く描けた落書きは人に見せたくなる。<br /> また必死に調べながら、ポートを開放して、DNSを設定して、Webサーバとして稼働させて、友人に「ぼくのつくったVagoo」を見せて遊んでいた。</p> <p>遊んでいる最中、管理者画面へ入るパスワードがテキストファイルに記載されていて、ファイル名さえ知っていればアクセス出来る状態だということに気が付いた。<br /> ファイルのパーミッションを設定すれば良いだけだが「本家はどうなっているんだろう」と思って覗いてみるとアクセス出来てしまった。ゲーム内にはユーザー間のDMみたいな機能もあり、管理者画面に入るとそれも含め全ユーザーの情報を見ることが出来る。倫理観なんて無かった僕は友人に「あの人とDMしてたでしょw」と得意げに話してしまった。怒られた。謝った後、運営者に脆弱性の報告をした。<br /> 注)この行為は不正アクセス禁止法により処罰の対象となります</p> <p>そういった経緯もあり「Vagoo2」の開発フェーズでテストプレイヤーに選ばれるぐらい僕は主要メンバーになっていた。<br /> 年齢関係無く大人達の仲間に入れる。そんな、インターネット上で遊んだり創作したりする時間が、僕にとっての快楽となっていた。</p> <h1 id="創作欲を封印した話"><a href="#%E5%89%B5%E4%BD%9C%E6%AC%B2%E3%82%92%E5%B0%81%E5%8D%B0%E3%81%97%E3%81%9F%E8%A9%B1">創作欲を封印した話</a></h1> <p>高校は偏差値の低い地元の工業高校へ進学した。<br /> どこでだったか忘れたが「ホームページ制作は云十万かかる」というのを見て、既にHTML/CSSぐらい書けるようになっていた僕は「ITは楽に稼げる」と思った。他教科の勉強は時間の無駄、ひたすらプログラミングの勉強をしたいと考え、受験勉強せずとも入れる、情報系の学科を選択した。</p> <p>プログラミングを学びたくて入学した高校に、プログラミングが出来る生徒はいなかった。それどころか、殆どの人はパソコンすら触ったことがなかった。地方の工業高校なんてそんなものだ。<br /> 周りの友人達は夜通し遊びに興じていた。入学当初は時間を見つけては自宅でプログラミングをしていたが、次第にその行為を「恥ずかしい」と思うようになってきた。中学生の頃は「ゲーム」という共通の話題の中でプログラミングを行う事が出来たが、今はそれすら無い。何か作って見せても「遊びの誘いを断ってこんなもの作ってるなんて、気持ち悪い」と思われるかもしれない。そういった恐怖心から僕は創作意欲を心の奥に閉じ込めるようになっていった。</p> <h1 id="卒業研究"><a href="#%E5%8D%92%E6%A5%AD%E7%A0%94%E7%A9%B6">卒業研究</a></h1> <p>高校のプログラミングの授業は面白かった。ビット演算や論理演算などの基本的な内容だが、独学では中々踏み込まなかった所だったからだ。同級生からは「何故かプログラミングの授業だけ点数が良いやつ」と思われていた。<br /> もっと勉強したいし、やっぱり何か作りたいという欲が常にあった高校生活だったので大学へ進学した。基本教科の勉強はろくにしていなかったのと、附属高校だった為、受験勉強せずとも入学できる同系列の大学へ行った。</p> <p>高校へ進学した時と同じ志望動機で大学を選択すれば、当然ながら周囲の環境も変わらない。僕は浅はかだった。<br /> 案の定、何も出来ない(やらない)時間だけが過ぎるキャンパスライフだった。</p> <p>中学生時代に遊んだブラウザゲームは個人でも改造出来るレベルのものだったが今はどうなっているのだろう、と流行っているパソコンゲームを調べてみた。MMORPGと出会ってしまった。3Dで描画された世界を好きなだけ駆け巡り、他者と協力したり対戦できたりする。<br /> クリエイターとしての私は絶望した。クオリティが高過ぎて、とても一個人が足を踏み入れられる世界じゃない。<br /> ゲーマーとしての興奮は凄かった。私は夢中になり、ゲームをしながらご飯を食べ、パソコンの前で寝て、起きて、また世界を駆け回る。所謂、廃人になった。</p> <p>こう書くと私は進路を後悔しているように見えるかもしれないが、私は高校・大学ともに行って良かったと今でも思っている。遊び呆ける体験も思春期には必要な時間だ。<br /> ずっと心の奥底に何かが引っかかっている。その程度の違和感があり続けた、というだけの話だ。</p> <p>この違和感を解消できるチャンスは「卒業研究」だけだと思っていた。<br /> 私は地元よりも就職活動の時期が早い東京で、早々に職を決めた。プログラミングの仕事なら何でも良い。それより卒業研究に励みたいと思っていた。「研究」より「制作」に近い研究室を意識的に選んだ。<br /> 研究内容は「ゲームを作る」今の私にピッタリだった。<br /> 研究室には僕の他にも数人の同期がいたが、プログラミングが出来るのは僕だけだった。もう、この事象に違和感は持たなくなっていた。「企画やゲームの素材画像、論文等は全てやってくれ。代わりにプログラムは全部俺が書く」という役割分担にした。ひたすらプログラミングをしたかった私にとっても、これが一番良い分担だった。<br /> 半年間ぐらいだったと思う。朝から大学が施錠される23時まで、ひたすら研究室に籠ってプログラムを書いた。ずっと一人作業ではあったが、それは幸福な時間だった。<br /> 世の中はiPhoneの登場に沸き立っている時期だったが、私たちの研究内容はガラケーのゲームだった。<br /> 出来上がったものはとてもチープなゲームで、ダウンロードURLも公開したが、恐らく遊んだのは研究室のメンバーだけだろう。それも数分程度。<br /> しかし、私は大学から推薦され電子情報通信学会の成績優秀賞を頂いた。推薦されれば必ず授与される「頑張ったで賞」みたいなものだが、そういった経験がなかった私には自信を与えてくれた。</p> <h1 id="新社会人"><a href="#%E6%96%B0%E7%A4%BE%E4%BC%9A%E4%BA%BA">新社会人</a></h1> <p>私は上京して社会人になった。<br /> 就職活動を早く切り上げたい一心で選んだ企業は、中小のシステムエンジニアリングサービス会社で、大企業の開発プロジェクトへ派遣されて常駐するタイプの仕事だ。<br /> 変わらず「何か作りたい」という漠然とした欲は持ち続けており、初任給でデスクトップ端末を買って開発環境を構築したが、何を作ればいいのか分からなくなっていた。<br /> 絵を描くスキルの無い私がゲームを作ったところで、卒業研究以上にチープなものが出来上がるだけだろう。それを自分だけ遊ぶ?そんな虚しい趣味には興じれない。絵を描ける友人に「ゲーム開発しないか」と話を持ち掛けたことは何度もある。しかし、その度になぁなぁで終わっていた。ゲームの素材作成というのもまた、熱量のいる作業だし、社会人になって数年程度の若手だと私生活に割ける時間も少ない時代だった。</p> <p>仕事の方は割と順調だった。<br /> 中小だが一次請けの案件が主で、二次請け三次請けのパートナー会社を管理する仕事だった。若手の頃にプログラマーとしての経験を積み、次第にチームやプロジェクトのリーダーとして管理職の経験を積む。<br /> 私は保守案件のチームリーダーや、受託開発プロジェクトのNo2ポジション、オフショアという海外へ開発を委託する手法が流行った時期は中国の子会社へ赴任するなど、低学歴の私からすると派手な経歴が連なってきていた。<br /> 私の中にあった「何かを作りたい」という欲求は、仕事を進める上での自動化ツール作成に置き換わっていた。これなら私が作ったものを誰かに使ってもらえる。</p> <p>代わりに、私生活は堕落しきっていた。<br /> 創作欲は仕事で満たされてしまい、今あるスキルだけで仕事が成立するので独習などする必要も無い。<br /> スマホでソーシャルゲームをしたり、ギャンブルしたり、毎晩の様に酒を飲みに行ったりしていた。大抵の娯楽は身を削るレベルでハマった。</p> <h1 id="転職"><a href="#%E8%BB%A2%E8%81%B7">転職</a></h1> <p>仕事は順調だったが、違和感を感じていた。<br /> プログラミングをしたくて就いた会社で、プログラミングをさせてもらえなくなっていたからだ。「開発をしたい」と意を唱えたこともある。しかし、なし崩し的に管理職になっていく未来が見えていた。自分の生産性を上げることより、大勢を管理出来るスキルを磨く方が売り上げ的にも有利なことは明白だった。<br /> 堕落していた私生活を矯正する為にも、私は転職して地元へ帰る決断をした。</p> <p>それが現職の会社となるのだが、30歳を超えた今でもプログラマーとして働けている。<br /> 業種や会社の形態は前職と変わらない為、管理職になるべきという圧は感じるが、まぁ自由に働けるようになったと思う。</p> <h1 id="2018年某日"><a href="#2018%E5%B9%B4%E6%9F%90%E6%97%A5">2018年某日</a></h1> <p>中学生の頃からの夢だった「プログラミングで飯を食う」は達成された。<br /> しかし、それでも私の心にはモヤがかかっていた。<br /> キャリアパスを見据え、やりたい事だけやる生活を求め、年収を100万下げてまで転職した。しかし、やりたくない事を拒絶し続けた結果、そこには出来る事しか残っていなかった。もちろん、技術の移り変わりがあるが、そこのキャッチアップも含め私には「作業」となっていた。中学生時代に抱いていた知的好奇心の煌めきを感じられなくなっていた。<br /> 余談だがこの年、私は結婚した。人生のターニングポイントを迎えたことで今後の人生を具体的にイメージし始め、上記の様なことを考えていた気もする。</p> <p>東京にいた頃とは打って変わり、業務中にも暇が出来るぐらい余裕のある生活となっていた。<br /> 私はよく技術記事投稿サイト<a target="_blank" rel="nofollow noopener" href="https://qiita.com/">Qiita</a>を眺めるようになっていた。とくに何かを調べるわけでもなく暇潰しだったのだが、徐々に引き込まれていった。<br /> Qiitaはトラブルシューティングや技術の紹介記事ばかりと思っていたが、この年は「個人でサービス立ち上げてみた/アプリを作ってみた」系の記事が多かったのだ。</p> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/jabba/items/1a49e860a09a613b09d4">開設後3週間で収益10万円を得た個人開発サイトでやったことの全部を公開する</a><br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/dala00/items/2861b4496c532c136256">ポエムでも何でも書けるQiitaの様なサービスを作った</a><br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/jabba/items/edefda09121877b79760">技術書ランキングサイトをQiita記事の集計から作ったら、約4000冊の技術本がいい感じに並んだ</a><br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/cdms/items/95a561d163d26bbfe274">もっと気軽にアウトプットできる技術ブログサービス「Qrunch(クランチ)」をリリースした【個人開発】</a><br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/kazu1496/items/7ce5043f9128c9ab55f4">Qiita版TweetDeck!?カラム形式でQiita記事をウォッチできるクライアントサービス「QiitaDeck」をリリースしました!</a><br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/rubys8arks/items/8123c2797d8cb6bd9dee">Web未経験から100日でリリース!初心者が「お笑い情報のアグリゲーションサイト」をつくりました【個人開発】</a></p> <p>上記の記事もほんの一部で、この個人開発ブームを分析するような記事も出ていた。</p> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/Yametaro/items/e9e401eee5171332c639">ワイの選ぶ2018年個人開発サイトBEST5</a><br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/nh321/items/0373a740e56fad6a49d8">【比較してみた】みんなが使っている個人開発(Webサービス)向けPaaS・ホスティングサービス</a></p> <p>またQiitaでは、年末に各テーマに沿った技術記事を毎日誰かが投稿するアドベントカレンダーという企画がある。<br /> 前年までの私は気にも止めてなかったのだが、<a target="_blank" rel="nofollow noopener" href="https://qiita.com/advent-calendar/2018/kuso-app">クソアプリ Advent Calender</a>という、無駄に技術で遊ぶカレンダーの存在にも気が付いた。</p> <p>創作欲を閉じ込め続けていた私は、どの記事を読んでも心が躍った。世の中には個人で開発を楽しんでる人が沢山いる。私は製作者達のファンとなり、日々の活動も追いたいと思ってTwitterアカウントを作った。<br /> 最初に交流を計ったのはQiitaの<a target="_blank" rel="nofollow noopener" href="https://qiita.com/jabba/items/85ca69f6f690c2151316#comment-57131540fa496bb43701">コメント欄</a>だった。送信後は柄にもなく、相当舞い上がっていた。「失礼にあたらないか」「的外れな事言ってないか」などを気にして仕事に手が付かない。恋する乙女のような心模様だった。31歳のおっさんが。恐らく相手もおっさんだ。<br /> 返信で「とても参考になる」と返ってきて更に興奮した。その日の晩、帰宅して妻に意気揚々とこの話をしたことを覚えている。</p> <p>ここまで来たら何もせずにはいられない。妻に「来年は個人開発を頑張りたい」と申し出た。仕事を辞めるわけではないが、その分家事が出来なくなる、というお願いだ。快諾してくれた。</p> <h1 id="今年の話"><a href="#%E4%BB%8A%E5%B9%B4%E3%81%AE%E8%A9%B1">今年の話</a></h1> <p>ここから先の話はQiitaやCrieitに記事を残しているのでそちらに任せようと思う。</p> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/kinmi/items/c66aa98718acad84621b">フロント明るくないおじさんがWebアプリ作った話【悟空語ジェネレーター】</a><br /> <a href="https://crieit.net/posts/19b75796b5c032c728ebab48ae064af4">なぜ大喜利サービスを作ったのか</a><br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/kinmi/items/ce39b52ff712098431c4">Nuxt.js+Firebase+GAEで作った個人サービスが半月で2万PV超えたので実績値を全て公開する</a><br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/kinmi/items/ddd213bf7a09f67f68ee">ギャザをドローするクソアプリを作りました</a></p> <p>この一年間、とにかく、ひたすら、開発をしていた。<br /> 昼休憩や仕事終わりはもちろん、休日や長期休暇は出来るだけ予定を入れずに朝から晩まで開発していた。<br /> 流石にもう歳なので学生時代よりは疲弊する。しかし苦痛を感じたこと一度もなかった。楽しかった。これまでの人生で抱えていたストレスを発散するかのような一年だった。</p> <p>中盤でも少し触れたが、私は私の人生を肯定している。個人開発には全ての経験が生きるからだ。出遅れた私は他の開発者の方々と、スキルや経験値の差を感じてばかりだが、それでも私にしか作れないものが必ずある。<br /> 今更、学歴に見合わない派手な経歴を増やそうとは考えていない。しかし、糧に出来ればそれが私の強みになる。過去も今年の経験も含めて発展させる、そんな経歴書を今後の人生で作っていきたい。</p> きんみ tag:crieit.net,2005:PublicArticle/15451 2019-10-04T17:10:28+09:00 2019-10-04T17:13:27+09:00 https://crieit.net/posts/Nuxt-js-Firebase-GAE-2-PV Nuxt.js+Firebase+GAEで作った個人サービスが半月で2万PV超えたので実績値を全て公開する <blockquote> <p>Qiitaへ<a target="_blank" rel="nofollow noopener" href="https://qiita.com/kinmi/items/ce39b52ff712098431c4">投稿した記事</a>のクロス投稿になります。</p> </blockquote> <h1 id="こんなサービス作りました"><a href="#%E3%81%93%E3%82%93%E3%81%AA%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E4%BD%9C%E3%82%8A%E3%81%BE%E3%81%97%E3%81%9F">こんなサービス作りました</a></h1> <blockquote class="twitter-tweet"><p lang="ja" dir="ltr">【<a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/%E6%8B%A1%E6%95%A3%E5%B8%8C%E6%9C%9B?src=hash&ref_src=twsrc%5Etfw">#拡散希望</a>】🙌🎉🎊サービス開始🎊🎉🙌ボケをツイートして「いいね❤️」「リツイート🔁」の数でランキング!Twitter連動型 大喜利サイト「ついぎり」サービス開始しました‼️<a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/%E3%81%A4%E3%81%84%E3%81%8E%E3%82%8A?src=hash&ref_src=twsrc%5Etfw">#ついぎり</a><a target="_blank" rel="nofollow noopener" href="https://t.co/bkXfzHyVSs">https://t.co/bkXfzHyVSs</a></p>— ついぎり@公式アカウント (@twigiri_app) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/twigiri_app/status/1161589000223641600?ref_src=twsrc%5Etfw">August 14, 2019</a></blockquote> <p>Twitterで大喜利するサービスです。<br /> 8月中頃にローンチしたのですが、有難いことに半月で約2.5万PVいきました。</p> <p>開発に至ったポエム記事はCrieitに投稿しています。<br /> <a href="https://crieit.net/posts/19b75796b5c032c728ebab48ae064af4">なぜ大喜利サービスを作ったのか</a></p> <h1 id="この記事について"><a href="#%E3%81%93%E3%81%AE%E8%A8%98%E4%BA%8B%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">この記事について</a></h1> <p>やっぱり公開直後は怖かったです。</p> <p>そう、<strong>クラウド破産</strong>。<br /> <a target="_blank" rel="nofollow noopener" href="https://gigazine.net/news/20180803-spent-much-money-in-firebase/">Firebaseの設定を間違えて72時間で300万円以上請求されてしまったウェブサービス</a><br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/itkr/items/745d54c781badc148bb9">BigQueryで150万円溶かした人の顔</a></p> <p>上記以外にもネット上にはインパクトのある高額請求の記事がたくさんあり、<br /> 内容を確認すれば「なんだそんな事か」となるのですが、<br /> 実績値というのはセキュリティやNDAの関係からか、なかなか出回ってません。</p> <p><strong>個人開発ならそんなの関係ねぇ</strong></p> <p>って事で全て公開します。<br /> 使い方によって大きく変わる所かと思いますが、何かの参考(個人開発の後押し)になれば幸いです。</p> <h1 id="環境構成"><a href="#%E7%92%B0%E5%A2%83%E6%A7%8B%E6%88%90">環境構成</a></h1> <ul> <li>Nuxt v2.8.1 <ul> <li>SSRモード</li> </ul></li> <li>GAE <ul> <li>standard</li> <li>runtime: nodejs10</li> <li>instance_class: F1</li> </ul></li> <li>Firestore <ul> <li>改めて別記事書こうと思いますが一般的な構成だと思います</li> </ul></li> <li>Cloud Functions for Firebase <ul> <li>engine: node8</li> <li>ユーザー登録/脱会/ツイート/いいねRT集計/バックアップ で使用</li> </ul></li> <li>Cloud Storage for Firebase <ul> <li>OGP画像の保存に使用</li> </ul></li> <li>Regionは全てasia-northeast1</li> </ul> <h1 id="使用量"><a href="#%E4%BD%BF%E7%94%A8%E9%87%8F">使用量</a></h1> <h2 id="PV数"><a href="#PV%E6%95%B0">PV数</a></h2> <p><img src="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F127547%2F24881933-651c-4c96-824c-39736e729605.png?ixlib=rb-1.2.2&auto=compress%2Cformat&gif-q=60&s=6fb0cd97a92b355d9c652e4ab302b671" alt="PV数" /><br /> トータルで約2.5万PV。<br /> デイリーだとローンチ直後の約5,500PVがMAXです。</p> <h2 id="割り当て"><a href="#%E5%89%B2%E3%82%8A%E5%BD%93%E3%81%A6">割り当て</a></h2> <p><img src="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F127547%2Fb76116ed-9fff-e787-4c78-bee5045b0550.png?ixlib=rb-1.2.2&auto=compress%2Cformat&gif-q=60&s=aa7c74d1f29cd701cbaaf4fd11b3492f" alt="割り当て" /><br /> アクセス数に比例した増加は見られませんでした。</p> <p><code>GCP Storage egress between NA and APAC</code><br /> 使用量: 3.59GB<br /> 一番費用が発生してるこれ、よく分かってません・・・<br /> GAEのデプロイにかかる項目なのかな?と思ってます。</p> <p><code>Cloud Firestore Entity Writes Japan</code><br /> <code>Cloud Firestore Read Ops Japan</code><br /> Write: 480,077<br /> Read: 606,253<br /> Firestoreは<a target="_blank" rel="nofollow noopener" href="https://firebase.google.com/docs/firestore/quotas?hl=ja#free-quota">1日単位で無料枠がリセットされる</a>のですが、リリース直後はちょっと無料枠から足出た程度ですね。<br /> 21日辺りは障害等でデータいじってた・・・気がします。</p> <h1 id="諸経費"><a href="#%E8%AB%B8%E7%B5%8C%E8%B2%BB">諸経費</a></h1> <h2 id="サーバー代"><a href="#%E3%82%B5%E3%83%BC%E3%83%90%E3%83%BC%E4%BB%A3">サーバー代</a></h2> <p><img src="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F127547%2F79b2afae-5344-fea9-34f5-2f1b7e7e7b7f.png?ixlib=rb-1.2.2&auto=compress%2Cformat&gif-q=60&s=5a86fdff390d11dcb2a1529bde970ce9" alt="サーバー代" /><br /> 0円でした。<br /> 使用量として57円かかってるのですが、<br /> GCPには1年間有効な$300のトライアルクレジットがあるので、そこに収まってます。<br /> クレジットを$0.5消費しました。使い切れる自信がありません。</p> <h2 id="ドメイン代"><a href="#%E3%83%89%E3%83%A1%E3%82%A4%E3%83%B3%E4%BB%A3">ドメイン代</a></h2> <p><img src="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F127547%2F47080e38-4045-f28d-c4d8-e55661e5af2d.png?ixlib=rb-1.2.2&auto=compress%2Cformat&gif-q=60&s=4106c6c479ee293fc185f0b086063bc9" alt="ドメイン代" /><br /> 1,600円です。<br /> <a target="_blank" rel="nofollow noopener" href="https://domains.google.com/">Google Domains</a>でappドメインを取りました。<br /> appドメインはSSL証明書無しでは使えないという安全なドメインです。<br /> GAEの場合、SSL証明は<a target="_blank" rel="nofollow noopener" href="https://cloud.google.com/appengine/docs/standard/nodejs/securing-custom-domains-with-ssl?hl=ja">デフォルトで有効&無料</a>です。</p> <h1 id="収益"><a href="#%E5%8F%8E%E7%9B%8A">収益</a></h1> <p>AdSense広告を導入しています。<br /> キャプチャは載せて良いのか分からなかったので・・・<br /> 半月で約1,300円でした。</p> <h1 id="所感"><a href="#%E6%89%80%E6%84%9F">所感</a></h1> <h2 id="環境構成について"><a href="#%E7%92%B0%E5%A2%83%E6%A7%8B%E6%88%90%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">環境構成について</a></h2> <p>一番びっくりしたのはNuxt.jsがF1インスタンスで落ちずに稼働していることです。<br /> 公式ドキュメントでも<a target="_blank" rel="nofollow noopener" href="https://ja.nuxtjs.org/faq/appengine-deployment/">F2が推奨されてる</a>のですが、F2では無料枠に収めることが難しいです。<br /> バックエンド処理をFirebaseに任せてるおかげで負荷が少ない構成になってるのかなーと思います。</p> <h2 id="費用について"><a href="#%E8%B2%BB%E7%94%A8%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">費用について</a></h2> <p>正直「バズった」と言える数値ではないと思ってるので「バズっても大丈夫」とは言えないです・・・<br /> 座布団数(ボケツイートのいいね/RT総数)も、現在のランキング1位は3桁です。<br /> 「バズっても大丈夫」と分かったら改めて記事書くので、誰かバズってください。</p> <p>しかし、まぁバズが発生してない個人開発としてはそれなりのアクセス数だと思ってます。<br /> それで数十円なので「そんなに怖くないよ!」とは言えるかと。</p> <h2 id="安心して使うために"><a href="#%E5%AE%89%E5%BF%83%E3%81%97%E3%81%A6%E4%BD%BF%E3%81%86%E3%81%9F%E3%82%81%E3%81%AB">安心して使うために</a></h2> <ul> <li>GCPの課金アラートを設定する</li> <li>Rulesはちゃんと書く</li> </ul> <p>は最低限やっとくべきです。<br /> ただ、Rulesはちゃんと書くと一般的なバックエンド処理と同レベルの規模になると思います。<br /> 最低限の尺度が難しいですが、認証/取得件数/タイムスタンプ比較辺りは最低でもやっておくべきです。</p> <h2 id="マネタイズについて"><a href="#%E3%83%9E%E3%83%8D%E3%82%BF%E3%82%A4%E3%82%BA%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">マネタイズについて</a></h2> <p>AdSenseに頼ったサービスの運営は凄く難しいという体験をしました。<br /> PV落ちたら施策を打たないといけないので放置出来ないし、<br /> 収益率はAdSenseの匙加減1つで大きくブレそうです。</p> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/kinmi/items/c66aa98718acad84621b">前作</a>ではマネタイズしてなく、正直「もしバズったらお金かかるよな・・・」とアクセス数に対してネガティブになってました。<br /> その経験を踏まえ、広告を貼りました。<br /> 結果、素直に「バズれ!」と思えるようになったので、それだけでも導入した価値はあったかと思います。</p> <h1 id="おわり"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A">おわり</a></h1> <p>今後、この個人開発で得た知見で記事書こうと思ってます。</p> <ul> <li>TwitterAPIがBANされた話</li> <li>Firestoreの構成について</li> <li>文字入れ画像を作るソースコード</li> <li>オレオレアトミックデザイン</li> </ul> <p>他に「ここ知りたい」等あれば公開しますし、ボリュームある内容であれば記事書きます!</p> <p>ただ、一番書きたいのは「バズっても大丈夫だった件」です。</p> <p><a target="_blank" rel="nofollow noopener" href="https://twigiri.app/">Twitter連動型 大喜利サイト「ついぎり」</a><br /> <img src="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F127547%2F61391e21-cd2f-0ca3-734e-f96beee4a3b5.png?ixlib=rb-1.2.2&auto=compress%2Cformat&gif-q=60&s=1d4d37fe422a9c228abb233de8b07382" alt="ついぎり" /><br /> 面白い投稿お待ちしてます😋</p> きんみ tag:crieit.net,2005:PublicArticle/15444 2019-10-01T17:53:19+09:00 2019-10-01T17:53:19+09:00 https://crieit.net/posts/19b75796b5c032c728ebab48ae064af4 なぜ大喜利サービスを作ったのか <p><a href="https://crieit.now.sh/upload_images/4f4ceff8a6a31085413a2d5b587f4b9b5d91f2292a55a.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/4f4ceff8a6a31085413a2d5b587f4b9b5d91f2292a55a.png?mw=700" alt="Twitter連動型 大喜利サイト「ついぎり」" /></a><br /> お笑いは人並み程度の「好き」です。<br /> 劇場へ足を運んだことは無いし、常にチェックしているお笑いコンテンツがあるわけでもありません。<br /> タイムラインで目に留まれば見ますし、笑います。</p> <p>「ボケる」文化はどちらかと言えば苦手です。<br /> 「ここはボケるところやろ!なんで!?」<br /> 「スベってもえーからボケたろゆう姿勢は見せてーや!」<br /> とか言う人、ちょっと苦手です。<br /> マナーの強要というか。<br /> 「会話転がしてこ!?」とか知ったこっちゃねーです。<br /> キャッチボールで勝手にカーブ投げてくるなって話です。</p> <p>すみません、脱線しました。</p> <p>こんな僕ですが、個人開発でこんなサービス作りました。</p> <blockquote class="twitter-tweet"><p lang="ja" dir="ltr">【<a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/%E6%8B%A1%E6%95%A3%E5%B8%8C%E6%9C%9B?src=hash&ref_src=twsrc%5Etfw">#拡散希望</a>】🙌🎉🎊サービス開始🎊🎉🙌ボケをツイートして「いいね❤️」「リツイート🔁」の数でランキング!Twitter連動型 大喜利サイト「ついぎり」サービス開始しました‼️<a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/%E3%81%A4%E3%81%84%E3%81%8E%E3%82%8A?src=hash&ref_src=twsrc%5Etfw">#ついぎり</a><a target="_blank" rel="nofollow noopener" href="https://t.co/bkXfzHyVSs">https://t.co/bkXfzHyVSs</a></p>— ついぎり@公式アカウント (@twigiri_app) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/twigiri_app/status/1161589000223641600?ref_src=twsrc%5Etfw">August 14, 2019</a></blockquote> <p>大喜利サービスです。</p> <p>つまり何が言いたかったかと言うと、<br /> 開発の動機に「お笑いコンテンツの課題を解決したい」とか<br /> 「こんなサービス欲しい」といったものは無かったのです。</p> <p>そんな個人開発の動機やモチベーションを振り返ろうと思います。</p> <h1 id="個人開発はじめました"><a href="#%E5%80%8B%E4%BA%BA%E9%96%8B%E7%99%BA%E3%81%AF%E3%81%98%E3%82%81%E3%81%BE%E3%81%97%E3%81%9F">個人開発はじめました</a></h1> <p>僕は約10年間、SESでサラリーマンプログラマーとして働いています。<br /> その間、プライベートでプログラミングの勉強等はやってこなかったです。<br /> しなくてもなんとかなる世界だったので。</p> <p>そんなある日、<del>業務が暇で暇で</del>自己研鑽したくてQiitaを眺めてると下記の記事を見つけました。<br /> <a href="https://crieit.net/posts/262838a0014117e9a88a0ec98df3ba84">開設後3週間で収益10万円を得た個人開発サイトでやったことの全部を公開する</a><br /> 神記事として名高いので、一度は目にしたことあると思います。</p> <p>捉え方は人それぞれだと思いますが、僕がこの記事を読んだ感想は<br /> <strong>「ちょー気持ち良さそう」</strong><br /> でした。</p> <p>僕がこれまでの人生で得た収入は全て「既存の商流に便乗する」ことで発生したものです。<br /> まぁ普通な事だと思います。一般的なサラリーマンなら。<br /> 絵は描けない。作曲もできない。起業?独立?そんな勇気ない。<br /> 僕はPCに向かって文字打つ事しかできない。<br /> 他の選択肢なんて考えた事も無かったです。</p> <p>そんな人生だからこそ「自分の中から生まれたもので収益を得る」ということに凄く魅力を感じました。<br /> 自己承認欲求だと思います。<br /> 月10万なんて収益を得られた日には<strong>半端無い快楽を得られるのではないか</strong> と思いました。</p> <p>そんな気持ちで、まずは勉強がてら小さいアプリを作った話がこちらになります。<br /> <a href="https://crieit.net/posts/Web-5d9016d040fdf">フロント明るくないおじさんがWebアプリ作った話【悟空語ジェネレーター】</a><br /> これでなんとなく、Web開発の雰囲気は掴めました。</p> <h1 id="個人開発としてのPoC"><a href="#%E5%80%8B%E4%BA%BA%E9%96%8B%E7%99%BA%E3%81%A8%E3%81%97%E3%81%A6%E3%81%AEPoC">個人開発としてのPoC</a></h1> <p>上記アプリを作る過程でTwitterを始めたり、いつも以上にQiitaを眺めたりしました。<br /> そこで個人開発としてのベストプラクティスやアンチパターンを沢山耳にしました。</p> <ul> <li>Nuxt.js(Vue.js)+Firebaseなら爆速でサービスを量産できる</li> <li>GCP(Firebase)は基本無料の代わりにバズると怖い</li> <li>個人で広告型マネタイズは厳しい</li> <li>PV稼いで収益出すタイプのサービスは難しい、ニッチを狙うべき</li> </ul> <p>僕は要領悪いので、体験を得ないことには納得できないタイプでして。<br /> どれも言ってる事は分かるけど、ピンと来なかったです。</p> <p>少なくとも、これを真に受けて次に進むと<br /> 「AdSense?wやめときなって。個人じゃ稼げないよ」<br /> なんて知った風な顔で講釈を垂れる、ダサい男になる気がしました。<br /> そんな親父にはなりたくない。</p> <p>次にやる事が決まりました。<br /> <strong>「今の自分が本気出したら、どれぐらいのPV(広告収益)を稼げるのか」</strong><br /> とりあえずバズっとけば大変さや運用費を体感できるでしょう。</p> <p>芸能人でもインフルエンサーでも起業家でもない俺が、<br /> それどころか個人開発トーシローの俺が、<br /> バズるサービス作ってやる!</p> <h1 id="拡散力全振りサービスのアイデア"><a href="#%E6%8B%A1%E6%95%A3%E5%8A%9B%E5%85%A8%E6%8C%AF%E3%82%8A%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%81%AE%E3%82%A2%E3%82%A4%E3%83%87%E3%82%A2">拡散力全振りサービスのアイデア</a></h1> <p>とはいえ、まだWeb開発駆け出しの身。<br /> 小難しいサービスは作れない自信があります。<br /> WebサービスのFizzBuzz的な。ミニマムサイズで拡散力あるもの。</p> <p>Twitterをやっていれば自然とここへ行き着きます。</p> <blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Nuxt.js+Firebaseで質問箱クローン作ってみようかな。…トランザクション的にFirebaseだとキツいかな。まぁやってみよう。</p>— きんみ / 🎍ついぎり🎍リリースしました🎉 (@_kinmi) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/_kinmi/status/1106385106569064448?ref_src=twsrc%5Etfw">March 15, 2019</a></blockquote> <p>ベースは<a target="_blank" rel="nofollow noopener" href="https://peing.net/ja/">質問箱</a>でいこう。<br /> まるでお手本の様なミニマムバズサービス。</p> <p>クローン作るだけでもWeb開発の勉強にはなります。<br /> しかし、何の検証にもなりません。<br /> 使う側からしたら「質問箱でええやん」終わり。です。<br /> バズりません。<br /> 他にも類似サービスは沢山あります。<br /> 後出しのこっちは、バズる為のナニカが欲しいところ。</p> <p>質問箱は「誰でも気軽に質問できる」を目指して作ったのでしょう。<br /> しかし、こちらの目指すところは「めっちゃ拡散されて、バズ契機を増やす!!」<br /> 解決したい課題なんて無い。拡散ファースト。<br /> いっそ <strong>拡散されたか</strong> をシステムの機能として組み込めば・・・?<br /> こんな感じで<br /> 「シェアツイートのいいね/RT数を使って何かする」<br /> が決まります。</p> <p>それから質問箱の回答ツイートを意識して見てると、割と大喜利やってるユーザーが多いことに気付きます。<br /> 僕自身も質問箱を始めたんですが、ネタ振りと捉えて回答することも多く<br /> 「ならいっそ、大喜利特化型の質問箱にしちゃおう」<br /> こんな感じでアイデアは固まりました。</p> <p>その他、検討段階で出てた案をここに供養しておきます。</p> <ul> <li><p><strong>ブログパーツ版質問箱</strong><br /> 質問箱ベースで出たアイデア。<br /> ブログに埋め込んでコメント欄として使ったら面白いんじゃないかなーと。<br /> オーナーからの回答はツイートされる。<br /> 何かしらでランキングにしちゃえばブログの宣伝にも繋がる。<br /> 今でも割とアリだと思ってます。</p></li> <li><p><strong>Twitterで遊ぶRPG</strong><br /> 「ツイートのいいね/RT数を使って何かする」ベースで出たアイデア。<br /> 一定時間経つと敵とエンカウント(自動ツイート)<br /> ユーザーが気付けばそのまま戦える。<br /> 気付かなくてもフォロワーがツイートを「いいね」「RT」することで援護できる。<br /> これはナシかなーと思ってます。<br /> いいね/RTをする意味が変わるのでフォロワー的にウザそう。<br /> (RTキャンペーンに近いネガティブイメージ持たれそう)</p></li> </ul> <h1 id="これ匿名投稿サービスなの?"><a href="#%E3%81%93%E3%82%8C%E5%8C%BF%E5%90%8D%E6%8A%95%E7%A8%BF%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%81%AA%E3%81%AE%EF%BC%9F">これ匿名投稿サービスなの?</a></h1> <p>既についぎりで遊んで頂いた方はそう思うのではないでしょうか。<br /> 実はこっそりマイページに「ユーザーに匿名でお題を出す」という機能はあります。<br /> 最初はこれとランキングだけでした。</p> <p>しかし「誰からもお題が来ない」って状況が想像出来たので、誰でもお題を投稿&取得できる「お題タンク」を作りました。<br /> 今では、ほとんどの投稿がお題タンク起因でスタートしてます。<br /> 当初の想定とは違う形になってますが、ユーザーの使い方やサービスの色に合わせて進化してる感じで運営者としても楽しいです。</p> <h1 id="モチベーション管理"><a href="#%E3%83%A2%E3%83%81%E3%83%99%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E7%AE%A1%E7%90%86">モチベーション管理</a></h1> <p>構想からローンチまで半年近くかかりました。<br /> ほぼ使ったことない技術(Nuxt.js、Firesotre(NoSQL)、GCP、Node.js、TwitterAPI、Bootstrap)だったので、<br /> キャッチアップ含めの工期です。<br /> 平日・休日関係無しで時間があればやりました。<br /> GWも夏季休暇も9連休でしたが、全て費やしました。</p> <p>しかし、モチベが落ちることは無かったです。<br /> <strong>イケると思ってたから。</strong></p> <p>「個人開発はモチベ維持が難しい」と言ってる人の大半は「結果イメージに自信が持てなくなる 」に尽きるのかなーと思います。<br /> それは数々の失敗を重ねてきたが故の、経験則から来るものだと思ってます。<br /> 再現性の無さを何度も肌で感じていて、いつでも足を止められる状況。<br /> そこを走り続けるのだからモチベ管理は重要なことなのでしょう。</p> <p>しかし、僕にはその経験がありません。<br /> (悟空語ジェネレーターは一発ネタで、PV狙ってないので)<br /> 経験が浅く、失敗体験の少ないうちは「これはイケる」と、根拠無き自信が持てるモチベのボーナスタイムだと思います。<br /> そして、その自覚がありました。なので、あえて妄想を膨らませてました。<br /> 「希望売却額は1億です。ただしSNSに金額を公表して良いなら5,000万でいいです」程度のことは妄想してました。<br /> どうせ何も分からないのだから、不安よりポジティブになる事考えた方が得という判断です。</p> <p>これから個人開発やってみたいなと思ってる方は<br /> 「自信なんて無いけど勉強目的だし・・・」で取り組むの、危険だと思います。<br /> 僕の観測範囲だと続かない人多いです(規模や工期によりますが)<br /> もし僕が言語習得目的で個人開発していたら「質問箱クローン」を作っていたでしょう。<br /> そしてローンチ出来たかは微妙です。途中で飽きそうです。<br /> 目標が言語習得ではなく「バズるサービスを作ること」だったのでクローンの先へ行けたし、ローンチまで漕ぎ着けられました。</p> <p>知識なんて後からいくらでも付いてくるので、まずは一攫千金狙いましょう。<br /> そのお金で何するかまでは具体的に想像しましょう。<br /> それが「勉強」に繋がると思います。</p> <h1 id="検証結果"><a href="#%E6%A4%9C%E8%A8%BC%E7%B5%90%E6%9E%9C">検証結果</a></h1> <p>この記事では「なぜ作ったか」に留めておいて、<br /> リリース直後の実績値については改めてQiitaに晒そうと思ってます。</p> <p>ただ、まだ検証途中という意識が強いです。<br /> 開発してるうちに、もっと色々試したいという思いが強くなってきたので。<br /> 技術記事による集客効果だったり、<br /> 一般層に対するリーチ方法だったり、<br /> <a target="_blank" rel="nofollow noopener" href="https://bokete.jp/">bokete</a>のような画像ボケ機能も導入して「個人開発における画像アップロード機能」も模索したいです。</p> <h1 id="おわり"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A">おわり</a></h1> <p>「世の中の課題を解決したい」なんて大義名分は無い個人開発ですが、楽しいです。<br /> 本気で喜んだり、悩んだりしてます。<br /> 承認欲求だけでもモチベになるし、この経験を通して今までは遠目で見てた凄い方達と会話することが出来てます。<br /> これほどコスパの良い取り組みは無いと思います。</p> <p>リリース直後AdSenseコンソール見たら9円入ってました。<br /> これ、普段の給与所得の比じゃないぐらい嬉しかったです。</p> きんみ tag:crieit.net,2005:PublicArticle/15434 2019-09-29T11:28:32+09:00 2019-09-29T11:29:48+09:00 https://crieit.net/posts/Web-5d9016d040fdf フロント明るくないおじさんがWebアプリ作った話【悟空語ジェネレーター】 <blockquote> <p>2019年2月に<a target="_blank" rel="nofollow noopener" href="https://qiita.com/kinmi/items/c66aa98718acad84621b">Qiitaへ投稿</a>した記事のクロス投稿です。</p> </blockquote> <h1 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h1> <p>近年、個人開発がとても盛んになってきました。<br /> クラウドサービスのリッチ化に伴い、個人でも簡単にWebサービスを運営できる時代となり、<br /> 個人開発者の熱意が更なる個人開発者の熱意を生む、素晴らしい連鎖が発生しています。</p> <p>特に去年、2018年は爆発的な個人開発ブームでした。</p> <p><em>「企業の一兵隊だった自分でも、何者かになれるんじゃないか」</em></p> <p><em>「世界は俺のプルリクを待っているのではないか」</em></p> <p>そう思わせてくれる1年でした。</p> <p>かく言う私も、その熱意にあてられ去年1年間、<br /> ひたすら<strong>Qiitaを傍観</strong>していました。</p> <p>私の業務はずっとサーバーサイドのJava開発。<br /> フロント明るくないおじさんです。<br /> はじめまして。きんみと申します。以後よろしくお願いします。</p> <h1 id="作ったもの"><a href="#%E4%BD%9C%E3%81%A3%E3%81%9F%E3%82%82%E3%81%AE">作ったもの</a></h1> <p>とは言え、私も兵隊の端くれ。<br /> 血湧き肉躍る開発は大好物なわけでして。<br /> Web開発の真似事のような事はやっていました。<strong>年末頃</strong>から。<br /> その集大成がこちらになります。</p> <p><strong>悟空語ジェネレーター</strong><br /> <a target="_blank" rel="nofollow noopener" href="https://goku-lang.netlify.com/">https://goku-lang.netlify.com/</a><br /> <img src="https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.amazonaws.com%2F0%2F127547%2F0fb19b09-cfc8-e408-bfe9-48c66a21397d.gif?ixlib=rb-1.2.2&auto=compress%2Cformat&gif-q=60&w=1400&fit=max&s=d00e45fecad196cc77e243f27355def9" alt="悟空語ジェネレーター" /><br /> 日本語を入力すると、悟空語に変換します。<br /> 初回はブラックボックスなサービスとして遊んで頂きたいので、<br /> 変換ルールについては最後に書きます。</p> <h1 id="使った技術"><a href="#%E4%BD%BF%E3%81%A3%E3%81%9F%E6%8A%80%E8%A1%93">使った技術</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://jp.vuejs.org/index.html">Vue.js</a><br /> JavaScript三大フレームワークの1つ。<br /> 公式ドキュメントの日本語がとても丁寧で、初学者の私でもとっつきやすいと感じたため使ってます。</p> <p><a target="_blank" rel="nofollow noopener" href="https://www.npmjs.com/package/kuromoji">kuromoji.js</a><br /> JavaScript製 形態素解析ライブラリ。<br /> 形態素とは文節の最小単位で、日本語文字列をこの単位で分解して、読み仮名や品詞情報を付与してくれます。<br /> <a target="_blank" rel="nofollow noopener" href="https://takuyaa.github.io/kuromoji.js/demo/tokenize.html">こちら</a>のデモサイトが分かりやすいです。</p> <p><a target="_blank" rel="nofollow noopener" href="https://www.netlify.com/">Netlify</a><br /> 静的ホスティングサービス。<br /> ちょー簡単にWEBサイト作れます。</p> <p>ということで、このサービスは静的なサイトです。<br /> 冒頭に話した「リッチなクラウドサービス」は、ほぼ<strong>使ってません</strong>。<br /> 流行りのNetlifyを使ってますが、別にどこだろう動きます。<br /> <strong>故・Yahoo!ジオシティーズ</strong>でも多分動きます。</p> <p>紆余曲折を経ての結果ですが、ちょっと後悔しています。<br /> 後述します。</p> <h1 id="仕組み"><a href="#%E4%BB%95%E7%B5%84%E3%81%BF">仕組み</a></h1> <ol> <li>入力値をkuromoji.jsで形態素解析</li> <li>各形態素の読み仮名情報をローマ字に変換</li> <li>変換ルールに従い、正規表現でゴリゴリ変換</li> <li>変換された形態素はひらがなへ戻す</li> <li>変換されなかった形態素は元キーワードへ戻す</li> <li>全形態素を文字列結合</li> </ol> <p>たったこれだけです。</p> <h1 id="ハマったところ"><a href="#%E3%83%8F%E3%83%9E%E3%81%A3%E3%81%9F%E3%81%A8%E3%81%93%E3%82%8D">ハマったところ</a></h1> <h2 id="1. フロントゼンゼンワカラナイ"><a href="#1.+%E3%83%95%E3%83%AD%E3%83%B3%E3%83%88%E3%82%BC%E3%83%B3%E3%82%BC%E3%83%B3%E3%83%AF%E3%82%AB%E3%83%A9%E3%83%8A%E3%82%A4">1. フロントゼンゼンワカラナイ</a></h2> <p>学生時代にHP作った事ありましたし、少しですが業務でjQuery等をイジる事もあります。<br /> しかし、いざ勉強をしてみるとそのカオスっぷりに腰が抜けました。<br /> <strong>フロントなんて刺身にたんぽぽ乗せてるだけ</strong>と思ってた自分を殴りたくなりました。</p> <p>まずは基本となるHTML5、というより<strong>脱TABLEタグ</strong>から入り、<br /> ES6、Vue.js、React等の公式ドキュメントや入門記事をサーフィンして<br /> Hello Worldな日々を4〜5日程続けました。<br /> 広く浅く現代の技術に触れて、無知の知を得る事に徹しました。</p> <h2 id="2. 形態素解析"><a href="#2.+%E5%BD%A2%E6%85%8B%E7%B4%A0%E8%A7%A3%E6%9E%90">2. 形態素解析</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/torao@github/items/45ad9640cf94d3169cae">React + Kuromoji を使ってブラウザ上で形態素解析</a><br /> こちらを参考に、Vue.js上でkuromoji.jsを動かせるようにしました。</p> <p>まずは<code>npm install kuromoji</code>した後、辞書を公開ディレクトリへコピー。</p> <pre><code>$ cd project-directory $ cp -a node_modules/kuromoji/dict public/ </code></pre> <p>下記のようにしてVue.js上でkuromoji.jsを動かしました。</p> <pre><code><template> <div id="app"> <input id="inputText" v-model="inputText"> <button v-on:click="conv">変換</button> <p>outputToken is: <span>{</span><span>{</span> outputToken <span>}</span><span>}</span></p> </div> </template> <script> import kuromoji from "kuromoji" export default { name: 'App', data() { return { inputText: "形態素解析される文字列", outputToken: [], builder: kuromoji.builder({ dicPath: "/dict" }) } }, methods: { conv: async function(event) { var vm = this this.builder.build(await function(err, tokenizer){ if(err){ throw err } else { var token = tokenizer.tokenize(vm.inputText) vm.outputToken = token } }); } } } </script> </code></pre> <p>ポイントは下記です。</p> <p><code>var vm = this</code></p> <p>通常、単一コンポーネント内ではthisでプロパティを参照するが、</p> <p>builder内でも参照できるようにグローバル変数に格納する。</p> <p><code>async / await</code></p> <p>builder.build は非同期処理の為、async / awaitを使う。</p> <p><code>vm.outputToken = token</code></p> <p>形態素解析後のトークンをプロパティへ格納する。</p> <p>この勘所が全く分からずハマり、Vue.js上でkuromoji.jsを動かす事は一度諦めて、下記を検討しました。</p> <ul> <li>代替品として<a target="_blank" rel="nofollow noopener" href="https://github.com/rakuten-nlp/rakutenma/blob/master/README-ja.md">Rakuten MA</a>の使用</li> <li>GoogleCloudFunctions(GCF)にkuromoji.jsを置いてAPI化</li> <li>GCFに<a target="_blank" rel="nofollow noopener" href="https://github.com/miurahr/pykakasi">Pykakasi</a>(Python製 形態素解析ライブラリ)を置いてAPI化</li> <li><a target="_blank" rel="nofollow noopener" href="https://dev.classmethod.jp/server-side/mecab-using-python3-ja/">MecabをPythonで叩く</a>API作成</li> <li>外部APIサービス(<a target="_blank" rel="nofollow noopener" href="https://developer.yahoo.co.jp/webapi/jlp/ma/v1/parse.html">Yahoo!JAPAN テキスト解析WebAPI</a>等)の使用</li> </ul> <p>箇条書きで書きましたがそれぞれ、調査して、コードを書いて、ハマって、諦めてを繰り返してます。<br /> 1行につき1〜2日は費やしています。<br /> もっと頑張れば、いずれも実現可能だったかもしれませんが、最終的に「ブラウザ上でkuromoji.jsを実行」に回帰しました。<br /> 選定した訳ではなく、現段階の私の技術力ではこれしか出来なかったのです。</p> <h2 id="3. ローマ字変換"><a href="#3.+%E3%83%AD%E3%83%BC%E3%83%9E%E5%AD%97%E5%A4%89%E6%8F%9B">3. ローマ字変換</a></h2> <p>今回の処理は、<br /> 入力値をローマ字に変換 → 解析 → ひらがなへ戻す<br /> というフローになります。<br /> 便利なJS製のカナ/ローマ字変換ライブラリがいくつかありましたが、<br /> どれもヘボン式で不可逆でした(とうきょう → tokyo → ときょ)<br /> また、解析は正規表現で行うため[sa,<strong>shi</strong>,su,se,so]のような不規則な変化は避けたく<br /> 可逆なローマ字変換処理を自前で作成しました(とうきょう → toukyou)</p> <h2 id="4. CSSフレームワーク"><a href="#4.+CSS%E3%83%95%E3%83%AC%E3%83%BC%E3%83%A0%E3%83%AF%E3%83%BC%E3%82%AF">4. CSSフレームワーク</a></h2> <p>このアプリはドラゴンボールのネタアプリです。<br /> もちろん、CSSフレームワークは<strong>Bulma</strong>(ブルマ)を使ってます(ドヤ</p> <p>言いたかっただけです。特にハマってません。</p> <p>BootstrapからjQuery依存を省いた軽量なCSSフレームワークです。<br /> <a target="_blank" rel="nofollow noopener" href="https://bulma.io/documentation/">公式ドキュメント</a>や、下記記事を参考にサクっと導入出来ました。素晴らしい。<br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/ochiochi/items/de1afd2d3fc8f6d3ea55">CSSフレームワーク BULMA チュートリアル①</a></p> <h1 id="マーケティング"><a href="#%E3%83%9E%E3%83%BC%E3%82%B1%E3%83%86%E3%82%A3%E3%83%B3%E3%82%B0">マーケティング</a></h1> <p>フロント技術の学習が目的だった為、開発以外の何かを行うつもりはなかったです。<br /> しかし、せっかくWebアプリを公開するならば、使ってもらえるよう努力したい。<br /> これもまたWebの学習。「やらない」はただの機会損失、と思い</p> <p><strong>悟空なりきりアカウントをフォローしてみました。</strong></p> <blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">やったぜ!!!よろしくな!! <a target="_blank" rel="nofollow noopener" href="https://t.co/kVgRtneAMp">https://t.co/kVgRtneAMp</a></p>— きんみ / 🎍ついぎり🎍リリースしました🎉 (@_kinmi) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/_kinmi/status/1087275299161616384?ref_src=twsrc%5Etfw">2019年1月21日</a></blockquote> <p>今のところ、<strong>使って頂いた形跡はありません。</strong></p> <p><em>2019/4追記<br /> いつの間にかこちらのなりきりさん、アカウント削除されてました。。</em></p> <p>しかし、Twitterで公開したことで、多くのフォロワーさんに使って頂き、バグ報告も多数頂戴しました。<br /> テスト不足が露呈した結果ですが、ローカルで動かしていただけじゃ気付かず終わっていたと思います。<br /> 公開して得られる学びの多さを痛感しました。</p> <h1 id="公開後のバグ対応"><a href="#%E5%85%AC%E9%96%8B%E5%BE%8C%E3%81%AE%E3%83%90%E3%82%B0%E5%AF%BE%E5%BF%9C">公開後のバグ対応</a></h1> <h2 id="① iPhoneでは表示されない問題"><a href="#%E2%91%A0+iPhone%E3%81%A7%E3%81%AF%E8%A1%A8%E7%A4%BA%E3%81%95%E3%82%8C%E3%81%AA%E3%81%84%E5%95%8F%E9%A1%8C">① iPhoneでは表示されない問題</a></h2> <p>Twitterで公開した直後の出来事でした。</p> <blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">見れないです・・・iPhoneだからですかね(´・ω・`)</p>— 無職やめ太郎(本名) (@Yametaro1983) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/Yametaro1983/status/1091833791142715393?ref_src=twsrc%5Etfw">2019年2月2日</a></blockquote> <p>はい、Chromeでしかテストしてません。。。<br /> Safariで開いてみたところ、ブランクページが表示されました。</p> <p>変換処理で多用していた正規表現の「<a target="_blank" rel="nofollow noopener" href="http://js-next.hatenablog.com/entry/2015/11/20/083622">後読み</a>」が原因でした。<br /> 2019/2現在、対応しているブラウザはChromeだけのようです。<br /> ここでJSが落ち、レンダリング自体されない事態に陥ってました。</p> <p>先読みは使えるとの事だったので下記を参考に、文字列を反転させてマッチさせるという荒技を使いました。<br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/yumarule/items/a37520974e39b25b7a6f">Javascriptでの正規表現の後読みの代替</a></p> <h2 id="② ぱい◯いでか美さん対応"><a href="#%E2%91%A1+%E3%81%B1%E3%81%84%E2%97%AF%E3%81%84%E3%81%A7%E3%81%8B%E7%BE%8E%E3%81%95%E3%82%93%E5%AF%BE%E5%BF%9C">② ぱい◯いでか美さん対応</a></h2> <blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">ぱいぱいでか美 <a target="_blank" rel="nofollow noopener" href="https://t.co/ESyx59epVx">https://t.co/ESyx59epVx</a> <a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/%E6%82%9F%E7%A9%BA%E8%AA%9E%E3%82%B8%E3%82%A7%E3%83%8D%E3%83%AC%E3%83%BC%E3%82%BF%E3%83%BC?src=hash&ref_src=twsrc%5Etfw">#悟空語ジェネレーター</a>これは「ぺぇぺぇでか美」にならへんのかいw</p>— 無職やめ太郎(本名) (@Yametaro1983) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/Yametaro1983/status/1091859171635412992?ref_src=twsrc%5Etfw">2019年2月3日</a></blockquote> <p>想定外です。ワケが分からない。<br /> ぱい系ワードは沢山テストしたのに、何故でか美さんだけ変換されないのか、、、</p> <p>原因は「ぱいぱいでか」を識別不可なワードとして解析され、<br /> 本来カタカナである読み仮名情報がひらがなになっていました。<br /> 私の仕様把握不足です。<br /> 前提としていた仕様が崩れ、<strong>デスマーチ</strong>が始まります。</p> <blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">やべぇ…でか美さん手強い…原因っぽいの直したら「ぺぇぱいでか美」ってなった、、どんな処理通ったらこうなるんや… <a target="_blank" rel="nofollow noopener" href="https://t.co/gwF0SffzKS">pic.twitter.com/gwF0SffzKS</a></p>— きんみ / 🎍ついぎり🎍リリースしました🎉 (@_kinmi) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/_kinmi/status/1091997573915656199?ref_src=twsrc%5Etfw">2019年2月3日</a></blockquote> <blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">ぐあぁぁあ!!!ちくしょーーーー!!! <a target="_blank" rel="nofollow noopener" href="https://t.co/oLCeRtNyD9">pic.twitter.com/oLCeRtNyD9</a></p>— きんみ / 🎍ついぎり🎍リリースしました🎉 (@_kinmi) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/_kinmi/status/1092001619837743105?ref_src=twsrc%5Etfw">2019年2月3日</a></blockquote> <p>現在は正常に「ぺぇぺぇでか美」となるのでご安心ください。</p> <h1 id="やらなかった事/やれなかった事"><a href="#%E3%82%84%E3%82%89%E3%81%AA%E3%81%8B%E3%81%A3%E3%81%9F%E4%BA%8B%2F%E3%82%84%E3%82%8C%E3%81%AA%E3%81%8B%E3%81%A3%E3%81%9F%E4%BA%8B">やらなかった事/やれなかった事</a></h1> <h2 id="1. バックエンド開発"><a href="#1.+%E3%83%90%E3%83%83%E3%82%AF%E3%82%A8%E3%83%B3%E3%83%89%E9%96%8B%E7%99%BA">1. バックエンド開発</a></h2> <p>当初の学習目標は「Firebase Hosting + Cloud Functionsを使ったサーバレス開発」でした。<br /> しかし、結果としてはバックエンド処理の無い、<strong>文字通りのサーバレス</strong>となってしまいました。</p> <p>辞書ファイルをキャッシュするため、初回の変換は超絶重いうえに、挙動のクライアント環境依存が強すぎます。。<br /> 友人はスマホの速度制限が来ていたのか、<strong>5分</strong>かかったと言っていました。<br /> 別の友人は<strong>Android版Edgeなるもの</strong>を使っていて「動かないよ」と言ってました。<br /> やはり、コアな処理は裏に持たせるべきですね。</p> <p>また、ユーザが変換したワードの収集が出来ません。<br /> リリース後のメンテナンス作業を見越した場合、生きたデータは貴重です。<br /> こんな小さなネタアプリでも、バックエンド処理は作るべきでした。</p> <h2 id="2. OGP作成"><a href="#2.+OGP%E4%BD%9C%E6%88%90">2. OGP作成</a></h2> <p>ここは捨てました。ついでにファビコンもVueデフォルトのままです。<br /> 作り出すと拘ってしまい、リリース延期に繋がると思ったからです。<br /> バズを狙う場合は必須ですが、、、</p> <p>ただ、こちらの記事を拝見して<br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/serinuntius/items/3017fb6ef51cd47352f6">Vue.jsとFirebaseでOGP画像生成系のサービスを爆速で作ろう</a><br /> OGPとしてSNSに共有するのは技術的にも面白そうと思ったので、近いうち導入するかもしれません。</p> <h1 id="モチベーション管理"><a href="#%E3%83%A2%E3%83%81%E3%83%99%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E7%AE%A1%E7%90%86">モチベーション管理</a></h1> <p>私のモチベ傾向として、完成までの道筋が見えたら熱が冷めます。<br /> 手探りでやっている時が一番楽しいんですよね。<br /> 過去にはリリースまで行かず、中途半端に終わったスマホアプリが幾つかあります。<br /> 今回も二の舞になるのではないか、という懸念があったので、<br /> 完成が見えた段階で<a target="_blank" rel="nofollow noopener" href="https://qiita.com/jabba">@jabba</a>さん作の<a target="_blank" rel="nofollow noopener" href="https://www.mobet.gq/">mobet</a> を使いました。</p> <blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">週末振休含めて4連休なので٩(๑òωó๑)۶今お遊びで作ってる「悟空語ジェネレーター」を公開する。 <a target="_blank" rel="nofollow noopener" href="https://t.co/Nuhjm7PCtp">https://t.co/Nuhjm7PCtp</a> <a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/%E3%83%A2%E3%83%99%E3%83%83%E3%83%88?src=hash&ref_src=twsrc%5Etfw">#モベット</a> <a target="_blank" rel="nofollow noopener" href="https://twitter.com/_kinmi?ref_src=twsrc%5Etfw">@_kinmi</a>さんから</p>— きんみ / 🎍ついぎり🎍リリースしました🎉 (@_kinmi) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/_kinmi/status/1087988495120023552?ref_src=twsrc%5Etfw">2019年1月23日</a></blockquote> <p>しかし、残念ながらこの4連休に<strong>インフルエンザにかかるという痛恨のミス</strong>を犯してしまい未達成です。<br /> 「未達成と判定するまで(期日から判定まで五日間の猶予があります)には完成させる」をモチベーションにリリースまで漕ぎ着けました。</p> <h1 id="変換ルール"><a href="#%E5%A4%89%E6%8F%9B%E3%83%AB%E3%83%BC%E3%83%AB">変換ルール</a></h1> <p>最後に変換ルールです。<br /> 当アプリの発想は下記ブログエントリーからです。<br /> <a target="_blank" rel="nofollow noopener" href="http://junk.hatenablog.jp/entry/2018/02/15/034507">野沢雅子語の活用けぇ(活用形)</a><br /> 変換ルールはこちらを肉付けした形になります。</p> <p><strong>1. 母音[a, i] + [i, e] => 母音[e] + 'ぇ'</strong><br /> e.g.) 最初 → さいしょ → saisyo → selesyo → せぇしょ</p> <p><strong>2. 品詞:動詞 に含まれる 'る' => 'っ' 文末の場合は 'っぞ'</strong><br /> 文末かつ、動詞+助動詞の場合は後続の助動詞を削除しています。<br /> kuromoji.jsでは単語の原型(サ行変格活用前、等)のワードも取得でき、そちらもチェックしています。<br /> e.g.) するから → surukara → sultukara → すっから<br /> します(する + ます) → suru → sultuzo → すっぞ<br /> 帰ります(かえる + ます) → kaeru → keleltuzo → けぇっぞ(ルール1と2の併用)</p> <p><strong>3. 一人称 => 「おら」、二人称 => 「おめぇ」</strong><br /> Wikipediaから一人称と二人称の一覧を作り、固定値で変換かけてます。<br /> 「貴様」が代名詞として判断されなかったので、「品詞=代名詞」の条件はつけてません。<br /> ベジータの台詞を悟空語に変換したかったので。</p> <p>上記がデフォルト挙動であり、なまりレベル:普通です。<br /> なまりレベル:強い/超 は上記に加えて固有の変換ルールが加わります。</p> <p><strong>強い:母音[o] + [i, e] => 母音[e] + 'ぇ'</strong><br /> e.g.) におい → nioi → niele → にえぇ</p> <p><strong>超:3文字のローマ字(ltu 等)以外の母音全て => 母音[e]</strong><br /> e.g.) オッス!おら孫悟空!→ oltusu!orasongokuu! → eltuse!eresengekee! → えっせ!えれせんげけぇ!<br /> (※1)</p> <p>なまりレベル:超 は文字だとよく分からないですが、<strong>声に出す</strong>と意外と読めます。<br /> 他のルール発見された方は教えてください!</p> <h1 id="おわり"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A">おわり</a></h1> <p>次は、すげぇWEBせぇとつくっぞ。<br /> (次は、凄いWEBサイト作る。)(※2)</p> <h1 id="その他参考サイト"><a href="#%E3%81%9D%E3%81%AE%E4%BB%96%E5%8F%82%E8%80%83%E3%82%B5%E3%82%A4%E3%83%88">その他参考サイト</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/hrdaya/items/291276a5a20971592216">JavaScriptで正規表現(文字列置換え編)</a></p> <p>※1 :「孫悟空」だと大丈夫ですが、「悟空」は「さとるそら」と読んでしまいます。<br /> ※2 : なまりレベル:強</p> きんみ