Building customized bootstrap theme

なんかタイトルの英語が間違ってたらごめんなさい。
備忘録です。

今職場でAccess + SQLServer2005 で作られている業務用のデスクストップアプリケーションをウェブシステム化する仕事を、本来の業務と並行で行っているのですが、僕が一番苦手なUIデザイン、モバイル携帯向けの含めて、どうしようか・・・と考えたとき、やっぱりネットでの情報が比較的多いbootstrapで・・・となりますよね💦

そこで問題になるのはやっぱりブートストラップ臭が・・・ということでしょうか。。。僕は古いタイプの人間なんで愚直に一個一個CSS、セレクタ、プロパティをガシガシ上書きしてました。これが実に苦痛で。。。

そこで、bootstrap4に移行するついでにSASSでカスタマイズするフローを学ぼう、というわけです(^▽^)/

bootstrap4のテーマの変更(Themingって日本語でなんていうんだ?)の仕方って、ググればググるほど、一体何が正解なんだ?という気になります。僕はGulpとか、gruntとか、webpack とか・・・ググらないと分からない・・・というレベルのオッサンでなんで、今時のトレンデーな、マックブック片手にバリバリ仕事をこなしている、最先端イってるイケてる人たちから見ると、アホかいな?というレベルの備忘録ですので、あらかじめご了承のほど。

余談です。フロントエンド開発が何を意味しているのか僕は正直わかんないです。最近は特についてけないです。Gulpとかgruntとかwebpackとか無縁の職場で、ナニソレ、おいしいの? いや普通に bootstrapの色を変えたり、角丸なくしたり、したいだけなんですぅ。未だに git じゃなくて svn だしぃ。

さて、まずはともかく、本家本元の手順に従いましょう。
ビルドツールのインストールです。https://getbootstrap.com/docs/4.3/getting-started/build-tools/
僕は Windowsユーザーなので、WSL(ubuntu16.04)を使用します。

(1) Node.js のインストール・・・これは適当に入れる。僕は以下の手順でいれます。
一旦aptでインストールし、nパッケージを入れ最新のものに入れ替えた後、apt removeします。

$ sudo apt install -y nodejs npm
$ sudo npm cache clean
$ sudo npm install n -g
$ sudo n stable
$ sudo ln -sf /usr/local/bin/node /usr/bin/node

$ sudo apt purge -v nodejs npm

(2) 上記サイトでは ruby のインストールも書かれていますが、テーマの変更だけしたい場合には必要ないと思います。まぁ、今時の開発者なら ruby は初めからインストールしているでしょうし・・・割愛(^^;

(3) 本家からbootstrapのソースをダウンロードし、適当なディレクトリに展開し、そのディレクトリで npm install で依存しているパッケージをインストールし、npm run css とすると、dist ディレクトリに css がコンパイル、ミニファイされたcssができあがります。

さて・・・ここからが本題です。

本家サイトのTheming(https://getbootstrap.com/docs/4.3/getting-started/theming/)を見るとカスタマイズ方法が書かれていますが・・・おそらくNode.jsに詳しくない、あるいは初めて触る人にとっては、そのページは何の参考にもなりません。。。custom.scssを作って・・・とか書いてますが・・・それで?って感じです。
scssファイルからcssをコンパイルおよびミニファイする具体的な手順とかほとんど書かれていません。

このあたりをググってみても、上記のgulpだの、gruntだの webpackだの、の手順とかがヒットして、オッサンが一番知りたい事の情報がでてきません。単に探し方が悪いのかも。。。
そこで自分なりに調べた結果、以下の手順でカスタマイズするフローに辿り着きました。

以下のフローでは、オリジナルのbootstrapには一切手を付けません。バージョンが上がるといろいろ困りますからね。
具体的なテーマをカスタマイズする方法は、本家サイトの Theming ページを参考にしてもらい、ここでは自分が用意したカスタマイズ用の.scssファイルを、どのように配置して、どのようにビルドするのかを書き留めておきます。

(0) 前準備として・・・

git がインストールされていなければインストールしときます。bootstrapのソースは git clone で取るようにしますんで。

(1) 作業用に適当なディレクトリを作り・・・

以下のようにディレクトリ・ファイルを配置します。。。各ファイル(a,b,c1~3)については後述します。

$ tree -n
.
├── babel.config.js  (a)
├── bootstrap (bootstrapのソースを置くディレクトリ)
├── dist (コンパイルされた css/js の置き場)
│   ├── css
│   └── js
├── package.json (b)
└── scss
    ├── custom-bootstrap-grid.scss (c1)
    ├── custom-bootstrap-reboot.scss (c2)
    └── custom-bootstrap.scss (c3)

(a) babel.config.js

bootstrapのソースにある、.babelrc.js をコピーしてこの名前にリネームします。JSファイルをコンパイルする際に単純にコピーして配置しただけだとエラーになってしまいました。理屈は知りません。すみません。npm-babelのサイトを斜め読みしたら、このファイル名に行きつきました💦

(b) package.json

これもbootstrapのソースにある、package.json をそのままコピーし、カスタマイズ用に修正しました。僕にとっては要らない機能もあるので逐一調べて不要なものを削除しまくり、以下のコードに行きつきました。内容については・・・よく分かりません。調べてください。すみません。

package.jsonは、npm(node package manager)が管理するファイルで、scriptsっていうキーにコマンド名、値に実行するコマンドラインを記述していくみたいです。
npm run コマンド名 で、そのコマンド名の値のコマンドラインが実行される、みたいな。makeみたいなもんか?

長くなりましたが、コピーした package.jsonファイル内のパス指定されている部分を修正したり、単体テストとかlintとか、サーバー機能、ドキュメントの構築などのビルドツールは基本必要ないので、片っ端から削除しました。テーマのカスタマイズ用にcss/jsのビルドさえできればいいという事にフォーカスしています。

通常はファイルを修正したら即反映されるような監視タスクを入れるのが開発のセオリーみたいなのですが・・・あれもこれもとやってると、結局、開発環境構築オタクになってしまうんで、やめときます。開発環境を作りたいんじゃなくて・・・コードを書くのが目的!

{
  "name": "custom-bootstrap",
  "description": "custom bootstrap4",
  "scripts": {
    "clone-bootstrap": "git clone https://github.com/twbs/bootstrap.git -b v4.3.1 --depth 1 bootstrap",
    "css": "npm-run-all css-compile css-prefix css-minify",
    "css-main": "npm-run-all css-compile-main css-prefix-main css-minify-main css-copy",
    "css-compile": "npm-run-all --parallel css-compile-*",
    "css-compile-main": "node-sass --output-style expanded --source-map true --source-map-contents true --precision 6 scss/custom-bootstrap.scss dist/css/bootstrap.css && node-sass --output-style expanded --source-map true --source-map-contents true --precision 6 scss/custom-bootstrap-grid.scss dist/css/bootstrap-grid.css && node-sass --output-style expanded --source-map true --source-map-contents true --precision 6 scss/custom-bootstrap-reboot.scss dist/css/bootstrap-reboot.css",
    "css-minify": "npm-run-all --parallel css-minify-*",
    "css-minify-main": "cleancss --level 1 --format breaksWith=lf --source-map --source-map-inline-sources --output dist/css/bootstrap.min.css dist/css/bootstrap.css && cleancss --level 1 --format breaksWith=lf --source-map --source-map-inline-sources --output dist/css/bootstrap-grid.min.css dist/css/bootstrap-grid.css && cleancss --level 1 --format breaksWith=lf --source-map --source-map-inline-sources --output dist/css/bootstrap-reboot.min.css dist/css/bootstrap-reboot.css",
    "css-prefix": "npm-run-all --parallel css-prefix-*",
    "css-prefix-main": "postcss --config bootstrap/build/postcss.config.js --replace \"dist/css/*.css\" \"!dist/css/*.min.css\"",
    "js": "npm-run-all js-compile js-minify js-move",
    "js-main": "npm-run-all js-lint js-compile js-minify-main",
    "js-compile": "npm-run-all --parallel js-compile-*",
    "js-compile-standalone": "rollup --environment BUNDLE:false --config bootstrap/build/rollup.config.js --sourcemap",
    "js-compile-bundle": "rollup --environment BUNDLE:true --config bootstrap/build/rollup.config.js --sourcemap",
    "js-compile-plugins": "node bootstrap/build/build-plugins.js",
    "js-minify": "npm-run-all --parallel js-minify-main",
    "js-minify-main": "npm-run-all js-minify-standalone js-minify-bundle",
    "js-minify-standalone": "uglifyjs --compress typeofs=false --mangle --comments \"/^!/\" --source-map \"content=bootstrap/dist/js/bootstrap.js.map,includeSources,url=bootstrap.min.js.map\" --output bootstrap/dist/js/bootstrap.min.js bootstrap/dist/js/bootstrap.js",
    "js-minify-bundle": "uglifyjs --compress typeofs=false --mangle --comments \"/^!/\" --source-map \"content=bootstrap/dist/js/bootstrap.bundle.js.map,includeSources,url=bootstrap.bundle.min.js.map\" --output bootstrap/dist/js/bootstrap.bundle.min.js bootstrap/dist/js/bootstrap.bundle.js",
    "js-move": "cross-env-shell shx mkdir -p dist/js && cross-env-shell shx cp -r bootstrap/dist/js dist/",
    "dist": "npm-run-all --parallel css js",
    "dist-clean": "cross-env-shell shx rm -f dist/{js,css}/*"
  },
  "devDependencies": {
    "@babel/cli": "^7.2.3",
    "@babel/core": "^7.2.2",
    "@babel/plugin-proposal-object-rest-spread": "^7.3.2",
    "@babel/preset-env": "^7.3.1",
    "autoprefixer": "^9.4.7",
    "babel-eslint": "^10.0.1",
    "babel-plugin-istanbul": "^5.1.0",
    "bundlesize": "0.15.3",
    "clean-css-cli": "^4.2.1",
    "cross-env": "^5.2.0",
    "find-unused-sass-variables": "^0.3.2",
    "glob": "^7.1.3",
    "node-sass": "^4.11.0",
    "npm-run-all": "^4.1.5",
    "postcss-cli": "^6.1.1",
    "rollup": "^1.1.2",
    "rollup-plugin-babel": "^4.3.2",
    "rollup-plugin-commonjs": "^9.2.0",
    "rollup-plugin-node-resolve": "^4.0.0",
    "shelljs": "^0.8.3",
    "shx": "^0.3.2",
    "uglify-js": "^3.4.9"
  },
  "engines": {
    "node": ">=6"
  }
}

上記 scripts を見てもらえれば、だいたい何をやってるのか分かると思います。
devDependencyは・・・必要なnodeプログラムの一覧?で、npm install すると、これらのプログラムが node_modules ディレクトリにインスコされるみたいですぅ。知らんけど。知らんけど。

(c1 ~ c3)bootstrapの下記ファイルをそれぞれインポートしてカスタマイズするファイルです。

  • bootstrap/scss/bootstrap.scss
  • bootstrap/scss/bootstrap-grid.scss
  • bootstrap/scss/bootstrap-reboot.scss

これは Theming でも説明されています。各ファイルの内容は・・・

/*******************************************************************************
 ファイル名: ./scss/custum-bootstrap.scss
ここに bootstrap本体の変数SASS を記述(上書き)していく。
その方法は、Theming を参照のこと。
*******************************************************************************/
$primary:  orange; 
.
.
.

/* ソース本体をインポート */
@import "../bootstrap/scss/bootstrap";

./scss/custom-bootstrap-grid.scss、./scss/custom-bootstrap-reboot.scss も同様です。

これらの./scss/custum-bootstrap*.scssを自分用にカスタマイズを施したあと、最後はビルドします。
カスタマイズは、主にbootstrap/scss/_variables.scss の中で定義されている変数を変更したりするのがカスタマイズの中心になってくると思います。もちろん、独自のスタイルを定義したければ、ここに書きます。

(2) コンパイル処理

./scss/custom-bootstrap*.scss の編集が終わったら、ビルドします。
順番としては、bootstrapソースを取得してから、npm run dist します。
distは、js,cssを一緒にやる感じです。

# まず、bootstrapのソースをbootstrapディレクトリに取得し、格納します。
# package.json を見ればわかりますが、git clone しているだけです。
$ npm run clone-bootstrap

# もし css/js すべてをビルドしたい場合は、dist
$ npm run dist

# css だけ欲しかったら css
$ npm run css

bootstrapをcloneするとき、v4.3.1のブランチを取得していますが、最新のものが必要であれば、”-b v4.3.1″の部分を消す。また”–depth 1″を消すと過去の履歴全部取得するので異常に時間がかかりますので消さない方がいいと思います。

なお、自分でソースをダウンロードする場合は、clone-bootstrapは実行する必要ありません。

lintとかテストとか一切省いているのでエラーが出たら・・・自力で何とかする!

CentOS から Windows上のSQLServerへの道のり

副題: Road to SQLServer on Windows. (^^;

【追記 2019年3月12日】
CentOS7 と PHP7系については、こちらにまとめてます。
https://ptsv.jp/repository/rhel7-php71-sqlserver/


備忘録です。

今まで、CentOS6からSQLServerへのアクセスは、FreeTDS を使っていました。PHPからはPDO_DBLIB 経由。これは結構簡単にセットアップできました。yum で FreeTDSとか、もっと簡単に、php-sybaseなりphp-mssqlなり入れれば依存関係で必要なライブラリは勝手にインストールされますしね。設定自体も、/etc/freetds.conf と /etc/locales.conf とかいじればあとは、PDOからアクセスできます。

で、非常に疎かったので知らなかったんですが、Microsoftから Linux向けのSQLServer ODBCドライバが供給されている、とのこと。

今更何言ってんの??? って感じですが、知らなかったものはしょうがない。説明通りにやってみます(^^;

# curl https://packages.microsoft.com/config/rhel/6/prod.repo > /etc/yum.repos.d/mssql-release.repo
exit root

$ sudo ACCEPT_EULA=Y yum install msodbcsql
$ sudo ACCEPT_EULA=Y yum install mssql-tools
$ sudo yum install unixODBC-devel
$ sudo ln -sfn /opt/mssql-tools/bin/sqlcmd /usr/bin/sqlcmd
$ sudo ln -sfn /opt/mssql-tools/bin/bcp /usr/bin/bcp

エラーもなく、インストール完了。
ここまでは説明書通り。

さて、ここから設定。

/etc/odbcinst.ini を開くと、以下のセクションが追加されているはず。

[ODBC Driver 13 for SQL Server]
Description=Microsoft ODBC Driver 13 for SQL Server
Driver=/opt/microsoft/msodbcsql/lib64/libmsodbcsql-13.1.so.6.0
UsageCount=1

そして、/etc/odbc.ini を編集します。僕の環境ではodbc.ini は空のファイルでした。
データソースを記述していきます。

[Development]
Driver = ODBC Driver 13 for SQL Server #odbcinst.ini に追加されたセクション名
Description = for development databse #適当
Trace = Yes #わからん。 
Server = 10.1.1.1 #稼働中のSQLServerのIPアドレスもしくはホスト名 ファイヤーウォールでアクセス許可を出すこと。
Port = 1433 #SQLServer Configuration Manager で TCP/IP を有効に。
Database = Sample  #データベース名

この状態で、まず、isql コマンドで接続してみます。書式は、isql [データソース名] [ユーザー名] [パスワード]ですね。

$ isql Development dbuser dbpass
+---------------------------------------+
| Connected!                            |
|                                       |
| sql-statement                         |
| help [tablename]                      |
| quit                                  |
|                                       |
+---------------------------------------+
SQL> select count(*) from hogehoge
+------------+
|            |
+------------+
| 5500       |
+------------+
SQLRowCount returns 0
1 rows fetched
SQL>

無事接続できました!

今度は、マイクロソフトが提供しているツールを使用してみましょう。

$ sqlcmd -S 10.1.1.1  -U dbuser
Password: xxxxxx
1> use Sample
2> go
データベース コンテキストが 'Sample' に変更されました。
1> select count(*) from hogehoge
2> go

-----------
       5500

(1 rows affected)
1>

接続できました。

次に、PHP で PDO_ODBC経由で接続してみます。
DSNには、odbc.ini で設定した、データソース名ではなく、ドライバ名・サーバー名・データベース名をそれぞれ直接指定してみました。

$ php -a
Interactive shell

php > $pdo = new PDO('odbc:Driver={ODBC Driver 13 for SQL Server};Server=10.1.1.1;Database=Sample','dbuser','dbpass');
php > $sth = $pdo->query('select count(*) from hogehoge');
php > echo $sth->fetchColumn();
5500
php >

接続できました。

日本語(UTF-8)が正常にCRUDできるか、まだテストしていませんが、とりあえずは、正常に接続できることを確認できたことでヨシとしましょう。

:continue 続きはこちら

フリーのSSL証明書をお試し中

追記 2017/3/11
最新のChromeおよびFirefoxでは、現在 startssl の無料版のSSLサーバー証明書は拒否されます。将来のバージョンでは復活する可能性もなくはないと思いますが、現在のところ、無料で発行してくれるSSL証明書は let’s encrypt のみとなっているみたい。

このサイトも、startsslのものを使っていましたが、lets encrypt に変更しました。
OSのバージョンが理由で、公式ツールは使えませんでしたが、dehydratedというシェルスクリプトで運用することにしました。


ずっとこのドメインは オレオレ認証でSSL通信してたわけですが、なんか世の中的にはSSL(HTTPS)がデフォルトに、という方向を向いてるようなので、とりあえず、無料のSSL証明書をググってみたら、非商用個人向けに限ると「StartSSL」というのが主流だそうです。というか、無料だとこれしかないみたいです(^^;;;
詳しいことはよく分かりませんが、 非商用個人だと 有効期限1年のClass1のServer証明書(DV)を5ドメインまで発行してくれるみたい。

あと、つい最近正式にサービスが開始された、Let’s Encrypt というのがあって、Let’s Encrypt 総合ポータルによると・・・

無料で利用できる自動化されていてオープンな認証局(CA)です。公共の利益を図る目的で Internet Security Research Group (ISRG) が運営しています。

だそうです。特筆すべきは、商用利用も可能だということ。まぁ、でも、あくまでDV(Domain Validation)だけですので商用利用・・・っていうのはどうなんでしょうかねぇ???という感じです。ただ、有効期限が3ヶ月なので、SSL証明書の更新を自動化して運用するのが前提のサービスですね。


startssl  lets_encrypt

最初、Let’s Ecrypt を使ってみよう・・・と思ったんですが・・・僕が利用しているVPSのOSがCentOS5のままなので、yumでインストールできるソフトが古すぎて、Let’s Encryptで使用するCertbot クライアントを導入できませんでした(笑) OSのバージョンアップとかメンドクサイので諦め。

ってことで、StartSSL で SSL証明書を発行してもらってApacheに設定して、オレオレ認証局から卒業です(^^)

CSSで文字回転

HTMLで表組みを組んでいくと、カラム数が多くなるとどうしても↓の左端のように不格好になりますよねぇ。。。。
damedame
イラレとかで文字を縦書きにして描画してSVGで書き出して配置すれば一応解決しますが・・・できるならそんなメンドッチーこと抜きに、画像を使わずCSSだけで済ませたい・・・。

はじめは、writing-mode プロパティを使えば縦書きにできるやろ???? と思ってth要素にwriting-mode: vertical-lrと設定してみたけど、だめだった。それならば・・・と思って、<th>タグ内のテキストを<span>タグで囲ってやり、そのspan要素に writing-mode: vertical-lr;を設定すれば一応縦書きになってくれた。
でも、望んでいるものと違う。縦書きだとアルファベットが90度回転してしまうので、縦書きではなく、横書きのまま90度回転した状態になってほしいんだな。。。

で、CSS3ならtransformプロパティでテキスト回転できる!ってのをグーグル先生に教えてもらって早速やってみたんですが・・・これがなかなか、圧倒的な知識不足でスマートな指定が分からない。試行錯誤して下記のような感じにできました。
kaiten
しかし・・・やり方がスマートじゃない・・・・。

<!-- HTML 抜粋 -->
<tr>
  <th><span style="margin-top:3em;">ほにゃらら</span></th>
  <td>
     ナンチャラ<br>
     かんちゃら
  </td>
</tr>

で、CSSは

/* ベンダープレフィックスは省略 */
th {
  vertical-align: middle;
  text-align: left;
  width: 1em !important;
}
th > span {
  transform: rotate(-90deg);
  transform-origin: left top;
  display: inline-block;
  white-space: nowrap;
  position: absolute;
  line-height: 1;
}

ようするに、セル内のspan要素を反時計回りに90度回転。デフォルトでspan要素の中心で回転するので不揃いになるので、回転軸は 左端上に設定。これだけではダメで、左端で回転するのでせっかくth要素でvertical-align:middleにして縦中央表示させているのにずれてしまう。これを補正するため、span要素にわざわざstyle属性で、span要素内の文字数の半分の長さだけマージンを設定。

span要素を補正するのはスクリプトを使って、文字数を割り出して・・・って自動でやればいいかもしれない。

/* サンプル */
(function($)
{
  $('th > span').each( function(){ $(this).css('margin-top', $(this).width() / 2); } );
}(jQuery);

一応画像は使わずできたけど、正直やり方が強引過ぎて納得できない。たぶんもっとCSS3のプロパティとか探せばパシッとスマートな設定(プロパティ・値)があるのかもしれないけど・・・ザラッと調べてみた感じ見つけられなかった。

グーグル先生にどういうワードで検索すればヒットするんだろ・・・T(;_;)T 教えてエロい人!

twitterの「いいね」画像をダウンロード 修正版

追記 2017/07/15
C#に書き直した修正版 → https://ptsv.jp/2016/04/20/tweet-image-download-agent/


2015年11月27日・29日 コード修正
コード修正 画像ファイルの正規表現の間違い修正・ちょこっと追加。
コード修正 URL を favorites => likes に変更

2015年11月23日 コード修正
2回目以降のtimelineのURLの決定方法が間違っておりましたので修正しました。ごめんなさい(m_m)

2015年11月17日 コード修正
ログインを行うコードを追加。これにより、ブラウザからクッキーファイルをエクスポートする手間を省くようにした。ログインコードはこれでいいのかどうかわからん。twitterってrails使ってんだっっけ? よくわからんが、適当。
本来はTwitter APIを使わないといけないと思うけど・・・API経由は正直メンドクサイ。勉強する気なし。(m_m)

C#で書き直したプロトタイプをさっき書いたので、これもデバッグが終わり次第また書こっと。
『perlなんかインストールしてねーよ、ボケ!』って方は、C#で書き直した方に ビルドしたやつを置いてます。


今年の2月ぐらいに書いたtwitter画像ダウンロードスクリプトが動かなくなったので修正・修正(^_^;;

2回目以降のタイムライン取得のURLが変更になったみたい?。昼休みにブラウザでアクセスしてデベロッパーツールでちょこっと解析したんですが・・・どのパラメーターで読めばいいのか、いまいち分かんないです。適当です。あくまで、自分用の備忘録なんで、すみません。取れればいいんです(m_m)

twitterにログインするところまで書きなおそうと一瞬思いましたが、また今度にします(・・;
ブラウザでログインしてクッキーエクスポート、twitter.comドメインだけ抜き出して、カレントディレクトリへcookie.txtというファイル名で保存。テキトーですまない。

使い方は下記参考。ログインしたcookie.txtが必要なのは、いいね(旧称:お気に入り)の取得のみ。他人のIDの「いいね」も同様にログインが必要です。この辺よくわからん。自分もしくは他人のIDのタイムラインにログインクッキーは必要ない。です。

#!/usr/bin/perl
=pod

=head1 ツイッターでのタイムラインから、画像のみダウンロードするスクリプト

使い方 targetに画像を取得したいアカウント名を指定する。

(いいね 画像の場合は type に favo をセット(デフォルト) 
(※ いいね 取得は自分以外のアカウントでも必ずログインが必要みたいです。)

> twitter.pl --username=xxxxx --password=yyyyy --type=favo --target=zzzz


(単に任意のアカウントのタイムラインの画像が欲しい場合は type に profileをセット)
(TLに流れているリツイートも含めた画像を取得したい場合は、timelineをセット)
(※通常公開されているアカウントのタイムラインではusename,passwordは必要ありません。
(※非公開アカウントではフォローを許可されたアカウントのusername/passwordが必要です。)

> twitter.pl --type=profile --target=公開アカウント名


ディレクトリ<tmp>に画像が吐き出されます。

=cut

use strict;
use warnings;
use LWP::UserAgent;
use Getopt::Long qw/:config no_ignore_case/;

my %CONFIG = (username => '', password => '', dir => './tmp', type => 'favo', target => '');
my %TIMELINE = (favo    => 'https://twitter.com/%s/likes/timeline',
                profile => 'https://twitter.com/i/profiles/show/%s/media_timeline',
                timeline => 'https://twitter.com/i/profiles/show/%s/timeline');

#エージェント
my $UserAgent = LWP::UserAgent->new(cookie_jar => {});

&{sub
{
  my @argv = @_;
  my $result = Getopt::Long::GetOptionsFromArray(\@argv,
                                                 'username=s' => \$CONFIG{username},
                                                 'password=s' => \$CONFIG{password},
                                                 'type=s'     => \$CONFIG{type},
                                                 'dir=s'      => \$CONFIG{dir},
                                                 'target=s'   => \$CONFIG{target});

  exists $TIMELINE{$CONFIG{type}} or die "invalid type....\n";
  $CONFIG{dir} || die "specified output directory...\n";
  $CONFIG{target} || die "specified target account name...\n";

  mkdir $CONFIG{dir} unless(-e $CONFIG{dir});

  &use_lwp_agent;

}}(@ARGV);

sub use_lwp_agent
{
  my ($param,$result) = ('?include_available_features=1&include_entities=1','');

  if($CONFIG{username} && $CONFIG{password})
    {
      my $response = $UserAgent->get('https://twitter.com/login');
      $result = $response->decoded_content;

      $result =~ /<input type="hidden" value="([\d\w]+?)" name="authenticity_token"(?:\s*\/)?>/ or die "can not detect authenticity_token...\n";

      my $auth = $1;
      $response = $UserAgent->post('https://twitter.com/sessions',
                                   {'session[username_or_email]' => $CONFIG{username},
                                    'session[password]'          => $CONFIG{password},
                                    'authenticity_token'         => $auth,
                                    'remember_me'                => '1',
                                    'redirect_after_login'       => '/' }) or die "can not access login page... \n";

      $result = $response->decoded_content;
      die "failed to login...\n" if($result =~ /error/);
    }

  my $url = sprintf($TIMELINE{$CONFIG{type}},$CONFIG{target});
  do
    {
      my $timeline = $url.$param;

      print "---- getting and parsing\n$timeline ...\n----\n";
      $result = &lwp_agent($timeline,'-') || die "can not get timeline. may be wrong url.\n";

      $param = &get_images($result,\&lwp_agent);

    } while( $param );

  print "done!\n";
}

#タイムラインのJSONデータから画像を取得して、次のタイムラインのパラメータを返す。
sub get_images
{
  my ($json,$agent) = @_;

  $json =~ s/\\\//\//g;
  foreach my $url_($json =~ m!https://pbs\.twimg\.com/media/[\w\-]+\.\w{3,4}(?::large)?!g)
    {
      if($url_ =~ m/([\w\-]+\.\w{3,4})(:large)?$/)
        {
          my $basename = $1;
          my $filename = "$CONFIG{dir}/$basename";
          if($2)
            {
              $url_ =~ s/:large/:orig/;
            }
          else
            {
              $url_ .= ':orig';
            }

          unless(-e $filename)
            {
              print "fetching: $url_\n";
              &$agent($url_,$filename);
              print "saved $basename\n";
            }
        }
    }

  my @ids = $json =~ m/data-tweet-id=\\\"([0-9]+)/g;
  my $max_id = @ids > 0 ? pop @ids : '';

  return $max_id ? "?max_position=${max_id}&include_available_features=1&include_entities=1" : undef;
}

#URLを取得して返す。
sub lwp_agent
{
  my ($url,$ofile) = @_;
  my %options = ();

  if($ofile ne '-')
    {
      if($ofile eq ':src')
        {
          if($url =~ /([\w\-\.%]+?\.\w)$/)
            {
              $ofile = "$CONFIG{dir}/$1";
            }
          else
            {
              goto cleanup;
            }
        }

      return $UserAgent->get($url,':content_file' => $ofile);
    }

cleanup:
  if(my $response = $UserAgent->get($url))
    {
      return $response->decoded_content;
    }

  0;
}

__END__