2021年5月27日 (木)

#LINEWORKS にBot投稿させるCLIアプリを作ってみた

0. 自動処理に組み込みたかったので

cron実行している処理の結果は、基本的に/var/logの下に書かれていくわけだが、異常な場合だけは早く知りたい、というのがある。

弊社ではLINE WORKSを導入しているので、これでBotにしゃべらせれば要件を満たせるのでは?ということで、Goでちゃちゃっと書いてみた。

ソースコードはGitHubに上げてある。

1. 使い方

さらっと書くとこんな感じ。

  LineWorksBotMessenger [options] messages
    -c configFilePath
          configuration file path
    -d userId
          Destination username to speak
    -k authorizationKeyPath
          Authorization Key file path
    messages
          messages to make LINE WORKS Bot speak

messagesの部分は、標準入力にも対応させた。そのおかげでパイプライン処理をそのまま受け付けることができるので、

cat logfile.txt | LineWorksBotMessenger [options...]

みたいな処理も可能。

使用に際しては、LINE WORKSのDeveloper Consoleと管理者画面の両方で、Botを使えるようにしておくことをお忘れなく。

なお、Bot APIは無料枠でも使えるので、システム管理のお供にどうぞ。

2. ひとりごと

最初はPythonで書こうかと思ったんだが、以下2つがネックになったのでGoで書いてみた。

  • 依存モジュールをシステムにインストールしないといけない
  • かといって、virtualenvだと実行ファイルを置くディレクトリを動かしにくくなる

2020年10月10日 (土)

Webショップ運営を(ほぼ)自動化した件

0. 弊社、Web販売もやってます

弊社はYahoo!ショッピングに出店しているんだが、問屋とのやり取り含め、その運営がまるごと手作業で結構手間だったりする。

一部の問屋は発送の代行もしてくれる、いわゆるドロップシッピング業者なのだが、そこを使った場合でも大まかな流れはこんな感じ。

1

人が監視する手前、受注はYahoo!ショッピングから送られてくるメールがトリガーになるので、担当者が不在の場合はどうしても遅れが生じるし、問屋への発注締め切り時刻を超過してしまうとその分エンドユーザーへの発送も遅延する。というのが悩みの種だった。

1. なので自動化に挑んだ

幸いYahoo!ショッピングにはストア運営を自動化するためのAPIが用意されているので、これを利用して受発注と発送通知の自動化に挑んだ。

で、こんな感じに。

2

1-1. 受注をfetchして発送依頼データを作るまで

件のストア運営を自動化するためのAPIを使ってストア運営する流れについては、公式の注文APIガイドに沿って順番にAPIコールしていけばいいので、これ自体は特に難しいことはなかった。
ただ、ここのAPIはXMLを投げ合う類だったりする。

肝心のシステムにはSpring Batchを使った。言語はJava。理由は単純、自分がJavaに比較的慣れていたためだ。

幸いJavaにはJava Architecture for XML Binding(JAXB)という公式の自動マッピング仕様があるため、XMLによるリクエスト/レスポンスデータとオブジェクトとの相互変換には、これを使うことで比較的容易に対応できた。
とはいえ、最初は「XMLかよ~」と泣きながらJAXBの解説記事を漁っていたのだが(あせ

で、取得した受注データから、発送依頼用ファイルを作るわけだが、相手方がシフトJISなCSVでのみ受け付けるため、色々見て回った結果opencsvを使うことにした。オブジェクトからCSVを生成するだけなので、特段苦労はなかった。

1-2. 発送依頼データをアップロードする

相手方の卸業者は受注や発送状況に関するAPIを持っていないとの話(こればっかりは仕方ない)なんだが、先方に確認したところ先方に確認したところ幸いにもRPA等の機械的なアクセスまでは禁じているわけではなかったので、Seleniumによるオートパイロットで発送依頼処理を代行させることにした。

こちらも最初はすべてSpring Batchで書いていたんだが、以下のような問題が出てきたのでSpringで書くのをやめ、処理を分離することにした。

  • WebDriverをSpringでBeanにしている場合、個々のオートパイロット処理の終了時にWebDriverをクローズすると、その「クローズされた状態」を次の処理で使いまわすため、WebDriverが開始できなくて倒れる
    まぁ、Seleniumはもともとテストツールで、JUnitなんかと一緒に動かすことが前提のものであって、インスタンスが使いまわされるような使用法は想定していないんだろう
  • AWSに受発注処理用サーバを置いて動かしていたら、相手方から攻撃とみなされてしまったらしく、IPベースで接続を遮断されてしまったため、AWS上に置けなくなってしまった
    「パブリックIP変えればええやん」とお思いになるかと思うが、Yahoo!ショッピングの本番系にAPIアクセスするには固定のパブリックPが必要で、これを変えるにはまた申請が必要だったりするので面倒

後半のやつについては「やりすぎました、ほんとごめんなさい」というだけの話で、先方にも当然のことながら謝りを入れた。けれども、一度遮断したIPアドレスを開けてくれるわけもなく、引っ越しを余儀なくされることに。

どういうわけか弊社には、社内システム用のプライベートクラウド的な仮想化基盤があり、まだ余裕がありそうだったのでそこに発送依頼データのアップロードを行うVMインスタンスを作ることにした。

引っ越し先は決まったとして、Spring Batchで書いていた処理だけを切り出してやるのではなく、せっかくなのでGoで一から書いてみることにした。
要件的にDBアクセスが必要な個所はなかったことと、幸いにもほぼSeleniumなagoutiという受け入れテスト用ライブラリがあったので、agoutiでオートパイロットする処理を実装。AWS側で発送依頼データのCSVを作ったらscpでアップロード用VMに送り込み、頃合いを見計らってGoで作ったオートパイロット処理でファイルをアップロードする、という算段だ。

1-3. 発送状況の追跡と更新

こちらは、前述とは逆の流れ。
先方からは発送状況をCSVでダウンロードできるので、Goで作った「発送状況CSVをダウンロードする処理」でCSVをダウンロードし、こいつをscpでAWS上の受発注処理用サーバに送り込んで、Spring BatchでCSVのパースと発送ステータスの変更を行う、という流れになる。

この段階でも、CSVがシフトJISという以外は特段苦労なくパースできた。

Yahoo!ショッピングのAPIで該当の注文に対してパース結果の運送業者と配送伝票番号を埋めたあと、Springのメール送信機能で注文者に発送状況を通知している。本文については、Thymeleafのテキストテンプレート機能を使った。

Springのメール送信機能については、Guide to Spring Emailという記事が大いに参考になった。

2. で、どうなったか

ひとまず、Webでの受発注はひととおり自動化したのだが、そうもいかない場合も存在する。例えばこんな感じ。

  • 「領収書の要否をカートに表示する」設定にしていて、注文ボタンが出ている画面に「領収書をどうするか」という選択が出ているのに、領収書不要でお客様要望欄に「領収書をください」と書いている場合がある
  • 同じく、領収書必要にはなっているが、お客様要望欄に宛名や但書に関する指定がある
  • 期日指定配送は承っていないのだが、お客様要望欄に「○○月××日必着で願います」と書かれている場合がある

こういう場合はどうしても人による確認が必要になるので、どんな対応が必要なのかを社内のMLに流す処理を追加することで対応した。一応、人間様は結果だけ受け取るという状況にはできたといえるが、今後は

  • 領収書の自動作成(※額面5万円以上の場合は印紙が必要になるのでテンプレートはそこで分ける必要があるが)
  • 受注結果をチャットのタイムラインに流す

あたりをやっていきたいと思う。

2019年12月15日 (日)

俺氏、ついにWindowsでgo-oci8を使うことに成功する

0. 結果的にいろいろ理解不足だった

前回記事では進捗ダメです状態で終わっていたのだけど、あれからいろいろいじり倒してどうにか形にすることができた。

まず前回記事の最後にある

  > go get github.com/mattn/go-oci8
  # github.com/mattn/go-oci8
  In file included from GOPATH\src\github.com\mattn\go-oci8\cHelpers.go:3:0:
  ./oci8.go.h:1:17: fatal error: oci.h: No such file or directory
  compilation terminated.

について。これはoci8.pcのインクルードパスの書き方に問題があったようで、InstantClient側の階層を一つ下げるとともに、WindowsのパスセパレータであるバックスラッシュをUnix系OSで使われるスラッシュに置き換えることで事なきを得た。

  orasdk=C:/bin/instantclient_19_3/sdk
  gcc=C:/bin/TDM-GCC-64

  oralib=${orasdk}/lib/msvc
  orainclude=${orasdk}/include

  gcclib=${gcc}/lib
  gccinclude=${gcc}/include

  glib_genmarshal=glib-genmarshal
  gobject_query=gobject-query
  glib_mkenums=glib-mkenums

  Name: oci8
  Description: oci8 library
  Libs: -L${gcclib} -L${oralib} -loci
  Libs.private:
  Cflags: -I${orainclude} -I${gccinclude}
  Version: 19.3.0

1. 最終的にどうしたのか

どうやら、最初から以下のようにしておけばよかった、ということらしい。

  1. GCCをMinGW GCCの最新版にする
  2. MinGWと同じツールチェインで作られているGTK+由来のpkg-configを追加する
  3. oci8.pcをUnix形式のパスセパレータ表記で作成する

1-1. GCCをMinGW GCCの最新版にする

推奨品という触れ込みのTDM-GCCだが、上記のoci8.pcgo buildしてみると、

  C:\bin\instantclient_19_3\sdk\lib\msvc\oci.lib
  error adding symbols: File in wrong format
  collect2.exe: error: ld returned 1 exit status

と出る。

ググると同じところで躓いていた人がおり、スレッドを追っていくことで原因が「GCCのバージョンが古いこと」だということにようやく気が付いた。

なので、SourceForgeのMinGW-w64 - for 32 and 64 bit WindowsにあるMinGW-w64のインストーラを使ってMinGWのツールチェインをインストールした。
今回はversion 8.1.0を使い、フレーバーはインストーラのデフォルトと思しきPOSIX/sehにした。

1-2. MinGWと同じツールチェインで作られているGTK+由来のpkg-configを追加する

MinGWにはokg-configは含まれないので、前回記事で入れたgettext、glib、pkg-configをMinGWのインストール先に上書きで統合するだけ。

1-3. oci8.pcをUnix形式のパスセパレータ表記で作成する

パスセパレータについては前述のとおり変更済みなので、GCCを変更したことに対応させる。
最終的にoci8.pcはこうなった。

  orasdk=C:/bin/instantclient_19_3/sdk
  gcc="C:/Program Files/mingw-w64/x86_64-8.1.0-posix-seh-rt_v6-rev0/mingw64"

  oralib=${orasdk}/lib/msvc
  orainclude=${orasdk}/include

  gcclib=${gcc}/lib
  gccinclude=${gcc}/include

  glib_genmarshal=glib-genmarshal
  gobject_query=gobject-query
  glib_mkenums=glib-mkenums

  Name: oci8
  Description: oci8 library
  Libs: -L${gcclib} -L${oralib} -loci
  Libs.private:
  Cflags: -I${orainclude} -I${gccinclude}
  Version: 19.3.0

この状態でgo installしてみると、特にエラーアウトプットはなし。どうやらいけたらしい。
Elwhjakuyaaahoa

で、動作確認をしてみる。
_example/dbms_outputは、発行したSQLの戻りに入っている文字列「hello」を表示するものなので、最終的なアウトプットに「hello」と出ていればいいはず。

  C:\Users\f97one\GOPATH\src\github.com\mattn\go-oci8\_example\dbms_output>set GO_OCI8_CONNECT_STRING=system/Orcl19cAdmin@//localhost:1521/ORCLCDB
  C:\Users\f97one\GOPATH\src\github.com\mattn\go-oci8\_example\dbms_output>go run -x main.go
  WORK=C:\Users\f97one\AppData\Local\Temp\go-build013517802
  mkdir -p $WORK\b001\
  cat >$WORK\b001\importcfg.link << 'EOF' # internal
  packagefile command-line-arguments=C:\Users\f97one\AppData\Local\go-build\58\5863c3505d965e6d20b9f8f592c519e220e5eed4b41183b13033d2e2374a1571-d
  packagefile database/sql=C:\Users\f97one\AppData\Local\go-build\62\628feb6160d48c5ea359bb61135be38aa5057fe7edbe4e99838bfb2c6089bbde-d
  packagefile fmt=c:\go\pkg\windows_amd64\fmt.a
  packagefile github.com/mattn/go-oci8=C:\Users\f97one\GOPATH\pkg\windows_amd64\github.com\mattn\go-oci8.a
  packagefile log=c:\go\pkg\windows_amd64\log.a
  packagefile os=c:\go\pkg\windows_amd64\os.a
  packagefile runtime=c:\go\pkg\windows_amd64\runtime.a
  packagefile context=c:\go\pkg\windows_amd64\context.a
  packagefile database/sql/driver=C:\Users\f97one\AppData\Local\go-build\bb\bbc0505de003812bb8e208b6f9c39c7ee40a4e3ade95c0d9d2db378042112a86-d
  packagefile errors=c:\go\pkg\windows_amd64\errors.a
  packagefile io=c:\go\pkg\windows_amd64\io.a
  packagefile reflect=c:\go\pkg\windows_amd64\reflect.a
  packagefile sort=c:\go\pkg\windows_amd64\sort.a
  packagefile strconv=c:\go\pkg\windows_amd64\strconv.a
  packagefile sync=c:\go\pkg\windows_amd64\sync.a
  packagefile sync/atomic=c:\go\pkg\windows_amd64\sync\atomic.a
  packagefile time=c:\go\pkg\windows_amd64\time.a
  packagefile unicode=c:\go\pkg\windows_amd64\unicode.a
  packagefile unicode/utf8=c:\go\pkg\windows_amd64\unicode\utf8.a
  packagefile internal/fmtsort=c:\go\pkg\windows_amd64\internal\fmtsort.a
  packagefile math=c:\go\pkg\windows_amd64\math.a
  packagefile bytes=c:\go\pkg\windows_amd64\bytes.a
  packagefile encoding/binary=c:\go\pkg\windows_amd64\encoding\binary.a
  packagefile io/ioutil=c:\go\pkg\windows_amd64\io\ioutil.a
  packagefile regexp=C:\Users\f97one\AppData\Local\go-build\d2\d2c97385147b6b2df852f8314ea0bc365ea5fd428c22cc28a7751caff46481e2-d
  packagefile strings=c:\go\pkg\windows_amd64\strings.a
  packagefile runtime/cgo=C:\Users\f97one\AppData\Local\go-build\05\050b0c52184a06b9e406b4908e746f8690cbeab9c2d8c629ec5358b2648f5d6f-d
  packagefile syscall=c:\go\pkg\windows_amd64\syscall.a
  packagefile internal/oserror=c:\go\pkg\windows_amd64\internal\oserror.a
  packagefile internal/poll=c:\go\pkg\windows_amd64\internal\poll.a
  packagefile internal/syscall/windows=c:\go\pkg\windows_amd64\internal\syscall\windows.a
  packagefile internal/testlog=c:\go\pkg\windows_amd64\internal\testlog.a
  packagefile unicode/utf16=c:\go\pkg\windows_amd64\unicode\utf16.a
  packagefile internal/bytealg=c:\go\pkg\windows_amd64\internal\bytealg.a
  packagefile internal/cpu=c:\go\pkg\windows_amd64\internal\cpu.a
  packagefile runtime/internal/atomic=c:\go\pkg\windows_amd64\runtime\internal\atomic.a
  packagefile runtime/internal/math=c:\go\pkg\windows_amd64\runtime\internal\math.a
  packagefile runtime/internal/sys=c:\go\pkg\windows_amd64\runtime\internal\sys.a
  packagefile internal/reflectlite=c:\go\pkg\windows_amd64\internal\reflectlite.a
  packagefile math/bits=c:\go\pkg\windows_amd64\math\bits.a
  packagefile internal/race=c:\go\pkg\windows_amd64\internal\race.a
  packagefile internal/syscall/windows/registry=c:\go\pkg\windows_amd64\internal\syscall\windows\registry.a
  packagefile path/filepath=c:\go\pkg\windows_amd64\path\filepath.a
  packagefile regexp/syntax=C:\Users\f97one\AppData\Local\go-build\09\09ed04a320a2025b1e0a177d0508fba26180bcc7e9a93f5b6b9a0215a3250c5a-d
  packagefile internal/syscall/windows/sysdll=c:\go\pkg\windows_amd64\internal\syscall\windows\sysdll.a
  EOF
  mkdir -p $WORK\b001\exe\
  cd .
  "c:\\go\\pkg\\tool\\windows_amd64\\link.exe" -o "C:\\Users\\f97one\\AppData\\Local\\Temp\\go-build013517802\\b001\\exe\\main.exe" -importcfg "C:\\Users\\f97one\\AppData\\Local\\Temp\\go-build013517802\\b001\\importcfg.link" -s -w -buildmode=exe -buildid=ZWzZXxPk1jGh5zBAmzMZ/JNrpGUoogY-8JjSab9MF/eSW2eCfLhSZIeYr8x4zF/ZWzZXxPk1jGh5zBAmzMZ -extld=gcc "C:\\Users\\f97one\\AppData\\Local\\go-build\\58\\5863c3505d965e6d20b9f8f592c519e220e5eed4b41183b13033d2e2374a1571-d"
  $WORK\b001\exe\main.exe
  hello

  C:\Users\f97one\GOPATH\src\github.com\mattn\go-oci8\_example\dbms_output>

今度は大丈夫そうである。

2. 終わりに

蓋を開けてみれば、GCCさえちゃんとしていれば何の苦労もなかった、ということなんだが、MSYS2のpacmanが挙動不審になるのを皮切りに、いろいろ大周りをする羽目になった。

2019年12月14日 (土)

Windowsでmatn/go-oci8を使おうとしてとても苦労している話

0. 唐突にOracle Databaseを使おうと思い立った

Oracle Databaseを使ったシステムなど、お仕事くらいでしか使うことはなかったんだけど、Oracle Cloud Free Tierとして使えるDBのPaaSがOracle Databaseだけ、というので、かなり仕方なく使う、というネガティブな話だったりする。

Javaや.NETには公式のデータベースドライバが出ているので、それらを使えば簡単にDBを利用できるけど、せっかくなので今回はGoでやってみることにした。

なお、結論から書くとこの話、実現できてないorz

1. 環境

ビルドには以下が必要。

  • C/C++コンパイラ
  • pkg-config
  • プラットフォームとバージョンに応じたOracle InstantClient

このほか、当然のことながらGitやGoが必要。

1-1. MSYS2

僕のWindows PC(Windows 10 pro version 1909)にはSphinxでPDFをビルドするため、MSYS由来のGNU Makeとshが入っていたので、GCCを入れるためpacmanを起動。
しかしながら、なぜか何もフィードバックを出さずpacmanが途中で終了してしまい、パッケージのインストールもアップデートもできずハマる。

おかしい、version 1809のころはふつうにpacmanが動いていたはずなのに。

これでは何もできないので、MSYS2はあきらめることに。

1-2. Cygwin

ならば、ということでMSYS2をアンインストールしてCygwin x64を入れてみた。

Baseパッケージグループのほか、makeやGCCをCygwin Installerで一とおりインストールし、C:\cygwin64\binなどをWindowsのPath環境変数に追加していった。

Cygwin shellではなくDOS窓からgo get github.com/mattn/go-oci8とやったところ、

  # runtime/cgo
  gcc_libinit_windows.c: 関数 ‘x_cgo_sys_thread_create’ 内:
  gcc_libinit_windows.c:57:12: エラー: implicit declaration of function ‘_beginthread’; did you mean ‘OpenThread’? [-Werror=implicit-function-declaration]
    thandle = _beginthread(func, 0, arg);
              ^~~~~~~~~~~~
              OpenThread
  cc1: all warnings being treated as errors
  go: failed to remove work dir: GetFileInformationByHandle C:\cygwin64\tmp\go-build601027150\NUL: Incorrect function.

と出てビルドできず。後でググると本体のGitHub WikiInstallFromSource

Go does not support the Cygwin toolchain.

などど書かれていた。なん....だと....?

1-3. WSL

最近のエディションにはWSL(Windows Subsystem for Linux)という、読んで字のごとくLinux互換環境を提供するサブシステムが標準搭載されている。デーモンを常駐させられない以外はほぼLinuxそのままなので、こいつで頑張ってみることに。

僕は普段UbuntuのWSLを使っているけど、GCCもpkg-configもapt installで導入できるのでお手軽だ。

ということでapt installでGCCやらpkg-configやらをインストールしてgo getしてみる。
結果、エラー表示は出ず。

いけた、のか? ということで付属のexampleで動作確認。

  $ cd $GOPATH/src/github.com/mattn/go-oci8/_example/nls
  $ GO_OCI8_CONNECT_STRING=system/Orcl19cAdmin@//localhost:1521/ORCLCDB go run main.go
  ORA-12571: TNS:packet writer failure

パケット到達不能、だと....? まぁ、Oracle Databaseは親環境のDockerで動かしているとはいえ、WSLからDockerを制御できているので、親環境とは通信できているはずだが。

てなわけで、この方法もダメということに。

1-4. TDM-GCC

Goの公式としてはTDM-GCCを推奨しているようなので、Cygwin GCCをアンインストールしてこちらに切り替えてみた。

パッケージマネージャは付属していないので、pkg-configを自力でセットアップしなければならない。やり方自体はStackOverflowの記事を参考にやってみた。

一応、直リンクを置いておく。

準備自体はGistを残してくれている方がいたので、それを使わせてもらうことに。

  > go get github.com/mattn/go-oci8
  # pkg-config --cflags  -- oci8
  pkg-config: exit status 3221225595

3221225595をWindowsの電卓に通すと0xC000007Bになるのだが、pkg-configが正常起動できていない、ということのようだ。どうやらABIが違うようなので、win64ではなくwin32に変えることで正常起動するようになった。

で、再挑戦。

  > go get github.com/mattn/go-oci8
  # github.com/mattn/go-oci8
  In file included from GOPATH\src\github.com\mattn\go-oci8\cHelpers.go:3:0:
  ./oci8.go.h:1:17: fatal error: oci.h: No such file or directory
  compilation terminated.

....pkg-configがインクルードパスを正常に処理できていないようだ。もちろん、oci.hの場所は-Iオプションの行で指定しているんだが。

おわりに

最後のTDM-GCCパターンではあと一歩なような気もする。

そんな気はするんだが、MySQLやPostgreSQLのドライバだったり、GoではなくJavaを選択していた場合だと、もっと簡単にデータアクセスを実装できていたであろう、と考えると、「なぜ僕はこんな苦行を....?」などと考えてしまう。

2023年12月
          1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31            

最近のトラックバック

無料ブログはココログ