RocketChatでChatbotによりJenkinsJOBを起動したいのだが（前編）

# RocketChatからチャットボットでJenkinsJOBを起動したい
開発環境にさまざまツールが入ってくると
その使い方を覚えるのに拒否をしてくる方々が一定数発生します。

といってその方がに理解を求めるために対面で時間をとって話をしても状況は改善することはあまりないです。そしてその流れに便乗して学ぶことを止める方々が増幅する状況になります。よくないですね。

## チャットボットでいいんじゃないの
RocketChat導入スミなので学びたくないんだったらチャットボットでいいんでないの？ということでチャットボット利用に向けて対応します。ま、RocketChat自体も使いたくないという人が一定数居るのですが、もはや道にもならんので切り捨てます。チャットツール利用ですら抵抗受けるってのは予想してなかったけど。

## RocketChatなのでHubotを使いたいが
チャットボットもいろいろあったり、自前で作ったりとさまざまな模様。RocketChatだとどうやらHubotが事実上のスタンダードの模様。んじゃ、利用したいんだけどDevOps環境に既に入っているしそれいい？
→ 不安だからダメ〜（管理者）
→ 別インスタンスで作るならいいけど（管理者）

--- 

は？別インスタンスってそれは管理者が巻き込まれたくないって理由から？
全体観点からかんがえてもらえないものなのか／／／
が、そんな話をやっていると永遠に事が進まない。

## んじゃ、別インスタンスでHubotを立てるには
「どうぞそちらの予算で」（管理者）
「ーーーオイ」

## じゃ、いくらなのさ
「見積もり出してください」

そして何度も催促するが1か月経過しても見積もりは出てこない。
「結局、理由をつけて拒否したかっただけかい？」


## ラチ明かないのでChatbotを自前で作ることにした
もういいや、自分で作ることにした。

Webの先人さまたちに教えを請うとどうやら何とかなりそうな予感

- RocketChatのwebhool outgoingとのインターフェースを利用
- サーバサイドのAPIアプリを作れば良い（GUIはいらない→RocketChat様が担う）
- サーバサイドのAPIアプリからJenkinsを起動する処理を呼び出す（実は既にPython部品として作ってある）

なるほど、3番目で作った部品をそのまま使えそうなのでサーバサイドAPIアプリはPythonってことで。スクラッチで書くのも大変そうなのでライブラリを使用する。なにがいいか。

- Flask ：よく使われているようだが
- FastAPI：こちらの方が早くて、Swaggerも持っているのでお勧めが多い

というこで`FastAPI`を使って書くことにします（あまり考えずぎず、とっとと決断）

## まずはRocketChatからのJenkinsJOB起動だけにしぼる
RocketChatからは`jenkins?jobname=aaaa&param1=p1&param2=p2`でチャットを飛ばすと指定したJenkinsJOB(=aaaa)をデフォルトでもっているパラメータ（param:p1,param:p2、、、、可変）で上書きして実行するようになればOK。

RocketChatインストール／設定やFastAPIやuvicornインストール／起動などは先人たちが解説してますので省略します。

## リクエストボディから欲しい情報
### rid
チャットボットからJOB起動リクエストを投げたときにそのリクエストが投入されたチャンネルに結果を返したいので`rid`が必要になります。作ったJenkinnsJOB側でも結果を投入するチャンネルを指定するパラメータを持っている実装をしていますので。

### リクエストパラメータ
`jenkins?jobname=aaaaa&param1=p1&param2=p2`の部分が必要になります（当然）。


## 実験してみて
リクエストパラメータを取得するコードは容易に書けます。だけど本来持っている`rid`が何故か取れない。たぶん実装の方法が良くないのだろう。もう一度FastAPIのガイドを見て再考する。

これではだめだ〜

## ちゃんとFastAPIの良いところ使って再度実装し直す
適当に実装すれば適当な結果にしかならない。。。ので
FastAPIが提供している機能を使って再実装します。

FastAPIのBaseModelを継承して受け取ったリクエストボディをきっちり型にはめて処理します。
BaseModelを継承して実装するところも事例がたくさんありますので先人たちに聞いてみてください。

さて実行すると422エラー（型が一致していないよー）と。

```
INFO:     192.168.10.110:43184 - "POST /jenkins HTTP/1.1" 422 Unprocessable Entity
INFO:     192.168.10.110:43186 - "POST /jenkins HTTP/1.1" 422 Unprocessable Entity
INFO:     192.168.10.110:43196 - "POST /jenkins HTTP/1.1" 422 Unprocessable Entity
INFO:     192.168.10.110:43278 - "POST /jenkins HTTP/1.1" 422 Unprocessable Entity
INFO:     192.168.10.110:43304 - "POST /jenkins HTTP/1.1" 422 Unprocessable Entity
```

リクエストBodyを1行の文字列とみなして受け取りClassを定義してみたけどどうやらNG。じゃ、リクエストボディの構造ってどうなってるのか。。。わからん（RocketChatのガイドには当然のうように書いてない）。

う〜ん？

## F12でブラウザ側のNetworkタブから中身を推察してみるが
はい、飛ばしているのはおぼろげに見えますね。
これが渡ってくることを前提に受け取るClassを定義してみますが状況変わらず422連呼。

## デバッグコードを書いてみる
FastAPIはこの辺スマートにやれるはずだが。。。

仕方ないのでrequestを使ってベタでリクエストの中身を除くデバッグコードを使います（このコードのまま運用では絶対使わないことを誓って）

```python
  1 #########################################################
  0 # 調査用コード
  1 #########################################################
  2 @app.post("/jenkins")
  3 async def read_requestbody(request: Request):
  4     pprint(f'request body test')
  5     print('==== request 構造 ====')
  6     pprint(f'{list(request)}')
  7     print('==== request headers ====')
  8     pprint(f'{request.headers}')
  9     byte_1 = await request.body()
 10     print('==== request body ====')
 11     print(type(byte_1))
 12     print(byte_1)
 13     str_my_json = byte_1.decode('utf-8')
 14     print('==== request body json ====')
 15     print(type(str_my_json))
 16     print(str_my_json)
 17     print('==== request body json（整形) ====')
 18     data = json.loads(str_my_json)
 19     pprint(data)
 20     print('-'*80)
```

そして結果を眺めてみる。

```python
==== request body json（整形) ====
{'bot': False,
 'channel_id': 'GENERAL',
 'channel_name': 'general',
 'message_id': 'yj6WFMPNnBP39bJkA',
 'siteUrl': 'http://localhost:3000',
 'text': 'jenkins?jobname=test&param1=p1&param2=p2',
 'timestamp': '2021-03-05T03:29:50.829Z',
 'token': 'e9q968t9jyw',
 'trigger_word': 'jenkins',
 'user_id': 'fr8prGxt3YakXtZDz',
 'user_name': 'admin'}
```
おう。。。ブラウザF12で見えている情報と全然違う


## BaseModelによる定義を再考
得られたデバッグコードからリクエストボディを受け取る入れ物定義を再度記述。

```python
  0 class Item(BaseModel):
  1     bot: str
  2     channel_id: str
  3     channel_name: str
  4     siteUrl: str
  5     text: str
  6     timestamp: str
  7     token: str
  8     trigger_word: str
  9     user_id: str
 10     user_name: str
```
## 再実行
```python
  0 #########################################################
  1 # 振る舞い定義 
  2 #########################################################
  3 @app.post("/jenkins")
  4 async def read_requestbody(item: Item):
  5     pprint(item)
  6     print('-'*80)
  7     pprint(f'channel_id: {item.channel_id}')
  8     pprint(f'channel_name: {item.channel_name}')
  9     pprint(f'token: {item.token}')
 10     pprint(f'param_string: {item.text}')
 11     return {}
```

結果は上々

```python
Item(bot='False', channel_id='GENERAL', channel_name='general', siteUrl='http://localhost:3000', text='jenkins?jobname=test&param1=p1&param2=p2', timestamp='2021-03-05T03:34:05.242Z', token='e9q968t9jyw', trigger_word='jenkins', user_id='fr8prGxt3YakXtZDz', user_name='admin')
--------------------------------------------------------------------------------
'channel_id: GENERAL'
'channel_name: general'
'token: e9q968t9jyw'
'param_string: jenkins?jobname=test&param1=p1&param2=p2'
INFO:     192.168.10.110:55934 - "POST /jenkins HTTP/1.1" 200 OK
```

## まとめ

- FastAPI、簡単にアプリAPIを書くにはとてもやりやすい。
- データの型規約もきっちりできるので運用目的のコードも品質を保つことが出来そう
- 今後も使おう！（GUIのないサーバアプリ）

ということで、ざくっと断片知識を持ち寄って作ってみたのですがちょっと罠があったものの効率良く実装が出来そう。

ちゃんと体系的に理解するためにも`FastAPIチュートリアル`を一通りやっておくことにする。


### お試しコード全量

エラーハンドリングはまだ全く入ってないけど。

```python
from fastapi import FastAPI
from fastapi import Query
from fastapi import Path
from fastapi import Request

from typing import Optional
from typing import List

from pydantic import BaseModel
from pydantic import Field

from pprint import pprint
import json

#########################################################
# リクエストボディインターフェース定義 
#########################################################
class Item(BaseModel):
    bot: str
    channel_id: str
    channel_name: str
    siteUrl: str
    text: str
    timestamp: str
    token: str
    trigger_word: str
    user_id: str
    user_name: str

 
#########################################################
# インスタンス生成
#########################################################
app = FastAPI()

#########################################################
# 振る舞い定義 
#########################################################
@app.post("/jenkins")
async def read_requestbody(item: Item):
    pprint(item)
    print('-'*80)
    pprint(f'channel_id: {item.channel_id}')
    pprint(f'channel_name: {item.channel_name}')
    pprint(f'token: {item.token}')
    pprint(f'param_string: {item.text}')
    return {}

#########################################################
# 調査用コード
#########################################################
@app.post("/jenkins")
async def read_requestbody(request: Request):
    pprint(f'request body test')
    print('==== request 構造 ====')
    pprint(f'{list(request)}')
    print('==== request headers ====')
    pprint(f'{request.headers}')
    byte_1 = await request.body()
    print('==== request body ====')
    print(type(byte_1))
    print(byte_1)
    str_my_json = byte_1.decode('utf-8')
    print('==== request body json ====')
    print(type(str_my_json))
    print(str_my_json)
    print('==== request body json（整形) ====')
    data = json.loads(str_my_json)
    pprint(data)
    print('-'*80)
```

## 追記：
FastAPIのガイドをよく読み解いてみる（英語）と、どうやら
`pydantic を使わず、Body を使ってリクエストを受け取り、json として処理する方法`が紹介されているようだ。ガイドをそのまま読むとそうとは受け止められないのだけれども試行錯誤するうちにそういうことか。。。と。

```python
#########################################################
# 調査コード（Bodyを使ってみる） 
#########################################################
@app.post("/jenkins")
async def read_requestbody(body=Body(...)):
    pprint(body)
    print('-'*80)
    return {'Body': body}
```
結果

```python
{'bot': False,
 'channel_id': 'GENERAL',
 'channel_name': 'general',
 'message_id': 's3DCRrgeazxndYQpr',
 'siteUrl': 'http://localhost:3000',
 'text': 'jenkins?jobname=test&param1=p1&param2=p2',
 'timestamp': '2021-03-06T04:55:43.175Z',
 'token': 'e9q968t9jyw',
 'trigger_word': 'jenkins',
 'user_id': 'fr8prGxt3YakXtZDz',
 'user_name': 'admin'}
```

お〜素晴らしい。
リクエストボディに何が入っているか分からない初期段階での解析コードが簡単に書ける！
pydantic を使ったクラスで処理するのはこのあたりがくりあになってからでよいですね。

Body 解析に捗る！
書きかたがClassやRequestを使う時とちょっと違うので注意が必要ですが。