[無料][2024年版] LINE Messaging API v3 + Python(Flask) でボットを作る [その8 - Flex Message 編]

## Flex Message とは

まずは公式ドキュメントのリンクからどうぞ。

https://developers.line.biz/ja/docs/messaging-api/using-flex-messages/

https://developers.line.biz/ja/reference/messaging-api/#flex-message

> Flex Messageは、通常のLINEメッセージに比べ、より豊かでインタラクティブなレイアウトが可能なメッセージです。通常のLINEメッセージでは、テキスト、画像、動画など1種類のソースしか配信されません。しかし、Flex Messageでは、CSS Flexible Box（CSS Flexbox） (opens new window)の仕様に基づいて、レイアウトを自由にカスタマイズできます。

> Flex Messageの構成要素は、コンテナ、ブロック、コンポーネントです。各Flex Messageは、メッセージバブルを含むコンテナという単一の最上位構造を持ちます。コンテナには複数のメッセージバブルを含めることができます。バブルはブロックで構成され、ブロックはコンポーネントで構成されています。

これだけでは何を言っているか全然わかりません。公式のサンプル動画を見てみましょう。

![bubbleSamples-Update1.96cf1f73.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/727634/dbe701fd-ba27-04fe-9eed-7513d517ea65.png)

このように、レイアウトを自分でカスタマイズできるのが Flex Message です。Flex Message は、**Bubble** という角丸長方形が一つの単位になります。この Bubble はクイックリプライ同様に横に複数並べることができ、**FlexCarousel** と呼ばれます（画像右下のサンプル）。

Flex Message のレイアウトは、原則 JSON によって書きます。[Flex Message Simulator](https://developers.line.biz/flex-simulator/) という公式のGUIで出来上がりがどうなるかの確認が可能ですが、このサンプルの JSON がどうなっているか、**\</> View as JSON** のボタンを押して確認してみて下さい。かなり長い量の文字列が出てくると思います。

JSON の手書きは本当に骨が折れます。何か要素を追加しようと思った時に、ネストのどの深さまで行けばいいのか、親要素は何なのか、パッと見ではわかりにくことが多いからです。

ですが、`line-bot-sdk` の v3 には、Flex Message を構成する各要素（上記の説明ではコンポーネント）がクラスとして用意されています。これらをオブジェクトとして積み重ねていくことで、**JSON を一切書くことなしに Flex Message が作成可能**です。というか、この LINE Messaging API 自体も本来は JSON でやり取りしているんですが、SDK を使ってプログラムを書いているおかげで今まで JSON に触れずに済んでいると言えますね。

## Flex Message の構成要素

これに関しては他に良記事がたくさんあるかと思いますが、JSON そのものではなく `line-bot-sdk` のクラスと関連して概説します。以下のクラスは、全て `linebot.v3.messaging` 内にあります。

- `FlexBubble` : 
    - 最上位の要素、一つのメッセージ
    - 複数を横に並べる場合は、`FlexCarousel` 内に配置する
    - `header`, `hero` (画像配置), `body`, `footer` といった領域がある
    - https://developers.line.biz/ja/reference/messaging-api/#bubble
- `FlexBox` :
    - 子要素を配置していくための箱。
    - 子要素を水平方向 `horizontal`・垂直方向 `vertical` のどちらに配置するかを指定できる
    - 子要素は `contents` プロパティに、リストとして格納する
    - `FlexBubble` の `header` や `body` などは、`FlexBox` を必ず指定しなければならない
    - https://developers.line.biz/ja/reference/messaging-api/#box
- `FlexText` :
    - `FlexBox` 内に配置するテキスト要素
    - `text` プロパティに文字列を指定する
    - または、`contents` プロパティに `FlexSpan` をリストとして格納する
    - https://developers.line.biz/ja/reference/messaging-api/#f-text
- `FlexSeparator` : 
    - `FlexBox` 内に配置する仕切り線要素
    - https://developers.line.biz/ja/reference/messaging-api/#separator
- `FlexSpan`
    - `FlexText` 内に配置する区切られたテキスト要素
    - 各 span ごとに、異なるフォントサイズや色を指定できる
    - HTML の \<span> のようなもの
    - https://developers.line.biz/ja/reference/messaging-api/#span

とりあえずこれだけ知っておけば、ある程度は作れると思います。VScode などの IDE で開発しているならプロパティ名がキーワード変数として表示されますし、APIドキュメントを読み込まずとも、細かい調整は実際にコーディングしながら学んでいけると思います。Flex Message Simulator でもプロパティ名は表示されますので、それを参考にするのも良いと思います。

## とりあえず作る

既に歌詞を表示する機能を作りましたが、そのままメッセージとして送信するのでは文字も小さいし、見栄えも良くありません。ということで、Flex Message を使ってかっちょいいメッセージにしたいと思います。完成形はこれです。

![IMG_6007.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/727634/07928355-4fcf-5beb-fcee-803cb8187dff.png)


曲名と、アルバム名、リリース年をヘッダに表示するために、[こちらのレポジトリ](https://github.com/nozomiyamada/linebot/tree/main/data)にある `album_year.csv` も併用します。まずは、これを辞書（または `pd.Series`）として読み込んでおきます。

さらに、前回コードの冗長化を減らすために調子に乗ってクラス化してしまったので、その修正も必要になってきます。曲名が一つに確定した時に `TextMessage` オブジェクトとして返していたものを、型確認して文字列ならば `TextMessage` として、そうでないならば `FlexMessage` オブジェクトとして返すように修正します。`FlexMessage` の場合は `altText` というチャット一覧画面などで代わりに表示される代わりにテキストを指定してあげる必要があります（ここでは曲名にしています）。以下のコードの真ん中のあたりです。

~~~python:botfuncs.py
## CLASS FOR GET INFORMATION BY SONG TITLE
class _GetBySongTitle:
	def __init__(self, col_name, mode_prefix='', decorate=lambda x: x):
		self.data = DATA[col_name] ## pd.Series : index - song title
		self.prefix = mode_prefix
		self.decorate = decorate  ## function to add sth to reply message

	def get(self, query:str) -> list:
		songtitles = search_song_title(query)
		if len(songtitles) == 0:  ## no candidate
			reply = 'NOT FOUND:\nPlease try again with different words.'
			messages = [TextMessage(text=reply)]
		elif len(songtitles) == 1:  ## only one candidate -> return URL
			songtitle = songtitles[0]
			reterieved = self.data[songtitle]  ## retrieve data 
			reply = self.decorate(reterieved, songtitle)
			if type(reply) == str:
				messages = [TextMessage(text=reply)]
			else:
				messages = [FlexMessage(altText=songtitle, contents=reply)]
		else:
			quickreply = QuickReply(items=[])  ## instantiation 
			for song in songtitles:
				if len(song) > 20:  ## max characters : 20
					label = song[:19] + '…'
				else:
					label = song
				item = QuickReplyItem(action=MessageAction(label=label, text=self.prefix+song))
				quickreply.items.append(item)
			messages = [TextMessage(text='candidate songs:', quickReply=quickreply)]
		return messages
~~~

肝心の Flex Message 作成関数はこちらになります。

~~~python:app.py
ALBUM_YEAR = pd.read_csv('data/album_year.csv', index_col='album')['year'] ## pd.Series

def create_flex_lyrics(lyrics, songtitle):
	## GET SONG INFO
	album = DATA.loc[songtitle, 'album']
	year = ALBUM_YEAR[album]

	## PREPARE BUBBLE CONTAINER
	bubble_container = FlexBubble(size='giga')

	## ADD HEADER WITH SONG TITLE, ALBUM, YEAR
	song_title_text = FlexText(text=songtitle, color='#FFFFFF', size='xl', weight='bold')
	song_album_text = FlexText(text=f'{album} ({year})', color='#FFFFFF66', size='lg') 
	header_box = FlexBox(
		layout='vertical',
		contents=[song_title_text, song_album_text],
		spacing='sm',
		backgroundColor='#0367D3',
		paddingAll='xxl'
	)
	bubble_container.header = header_box

	## ADD EMPTY BOX TO BODY
	bubble_container.body = FlexBox(
		layout='vertical',
		spacing='xxl', ## space between each paragraph 
		contents=[]
	)

	## INSERT EACH LINE TO PARAGRAPH BOX
    ## paragraphs = list of list of lines e.g [[Jojo was..., But he...], [Get Back, ...]]
	paragraphs = [paragraph.split('\n') for paragraph in lyrics.split('\n\n')]
	for paragraph in paragraphs:
		para_box = FlexBox(layout="vertical", contents=[], spacing='sm')
		for line in paragraph:
			para_box.contents.append(FlexText(text=line, size='lg', wrap=True))
		bubble_container.body.contents.append(para_box)

	return bubble_container
~~~

これだけでは何をやっているかサッパリだと思いますので、階層構造を書きますとこのようになっています。こういった階層構造関係は Web を作ったことがある方は DOM でお馴染みだと思います。

~~~
FlexBubble
　├ header
　|　└ FlexBox
　|　　├ FlexText (曲名)
　|　　└ FlexText (アルバム名、リリース年)
　└ body
　 　└ FlexBox
　　　　├ FlexBox (パラグラフ)
　　　　|　├ FlexText (一行)
　　　　|　└ FlexText (一行)
　　　　├ FlexBox (パラグラフ)
　　　　|　├ FlexText (一行)
　　　　|　└ FlexText (一行)
　　　...
~~~

上述したように、`FlexBubble` の `header` および `body` は入れ物である `FlexBox` でなければなりません。その `FlexBox` の `contents` の中に、子要素をさらに入れていく形になります。歌詞はパラグラフごとに分かれているので、その一かたまりをさらに `FlexBox` とし、その中に歌詞を一行ずつ入れていきます。なんでこんなことをしているかというと、折り返しの行間と改行に微妙な差が付くからです。折り返しは `wrap=True` を設定することでできます。

## 各プロパティの解説

階層構造がわかったところで、細かいプロパティ設定の説明に入ります。

- `FlexBubble`
    - https://developers.line.biz/ja/reference/messaging-api/#bubble
    - `size` : 大きさを指定。[SI接頭語](https://ja.wikipedia.org/wiki/SI接頭語#一覧)が利用されている。ここでは `giga` という一番大きいものを使用。
    - `header` : ヘッダー部分。`FlexBox` を指定する
    - `body` : ボディ部分。`FlexBox` を指定する
- `FlexBox`
    - https://developers.line.biz/ja/reference/messaging-api/#box
    - `contents` : 必須要素。とりあえずインスタンス化して後から追加する際は、空リストにしておく
    - `layout` : 必須要素。`vertical`、`horizontal`、`baseline` の3つから選ぶ
    - `spacing` : 子要素同士の間隔を決める。`xs`, `sm`, `md`, `lg`, `xl`, `xxl` から選ぶか、px で指定
    - `backgroundColor` : 背景色。hexカラーで指定
- `FlexText`
    - https://developers.line.biz/ja/reference/messaging-api/#f-text
    - `text` : 表示テキスト
    - `size` : フォントサイズ。`xxs`, `xs`, `sm`, `md`, `lg`, `xl`, `xxl`, `3xl`, `4xl`, `5xl` から選ぶか、px で指定
    - `weight` : 太字は `bold` を指定
    - `color` : 文字色。hexカラーで指定
    - `wrap` : テキストの折り返しをするかどうか

これ以外にも設定できる項目はたくさんあります。何をどういじるとどう変わるかは、[Flex Message Simulator](https://developers.line.biz/flex-simulator/) で遊んでみると一目瞭然です。

## レイアウト確認の注意点

パラメータを変えるたびにいちいちボットで送信してレイアウトを確認するのも面倒ですから、基本的には GUI である [Flex Message Simulator](https://developers.line.biz/flex-simulator/) を使うことになります。そのためには JSON 形式で出力する必要があります。

`Flex...` クラスには全て `.to_json()` というメソッドがあり、これで JSON 形式にして出力できますので、それを Flex Message Simulator の **View as JSON** の部分にコピペして確認が可能です。以下は `FlexBubble` オブジェクトがどう出力されるかを notebook 上で試した結果です。

![Screenshot 2024-01-08 at 04.57.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/727634/c42bb594-2a5c-978b-25f9-703c2797809b.png)

この `to_json()` の結果の、最初と最後のクォーテーションマーク `'` を除いた部分をそのままコピペし、Flex Message Simulator に貼り付けてもいいのですが、上記の例のようにテキストに日本語が入っていると、以下のようになってしまいます。

![Screenshot 2024-01-08 at 05.02.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/727634/128b7d02-8b25-a72d-c282-3282a9e37999.png)

これ、最初は「Flex Message Simulator が Unicode エスケープされた文字を扱えないという問題があるのでは？」と思っていたのですが、完全に私の間違いでした。`\\u3042` のようにバックスラッシュ `\` が2つ繋がってしまっているせいで、JSON が正しく認識できていないのです。LINEさん疑って本当にごめんなさい。

正しい方法は、`print(flex.to_json())` のように JSON文字列をプリントして、バックスラッシュエスケープのための最初の `\` を表示させないことです。プリントすれば文字列であることを示す最初と最後の `'` も表示されませんし、コピーも楽です。**JSONは必ず print せよ**、というのが私にとっても教訓となりました。

![Screenshot 2024-01-08 at 08.15.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/727634/bb009296-4032-ddad-375e-c87d41840100.png)

ちなみに日本語だけでなく、`don't` のようにシングルクォーテーションマークが入ってしまう場合でもエスケープのためのバックスラッシュが入ってしまうため、同様の問題が起きます。めんどくさがらずに、**必ず print しましょう**。

また、Flex Message Simulator に貼り付けるのはあくまで本体部分の JSON のみです。メッセージオブジェクトではない `FlexBubble` の JSON 出力であればそのまま貼り付けていいですが、 `FlexMessage` でメッセージオブジェクト化された物の JSON は注意が必要です。具体的には以下のようになっていますが、この場合は "contents" 以下の `{"type": "bubble" ...}` だけコピーすれば良いです。

~~~json
{
  "type": "flex",
  "altText": "alt",
  "contents": {
    "type": "bubble",
    "body": {
      "type": "box",
      "layout": "vertical",
      "contents": [
        {
          "type": "text",
          "text": "あいうえお"
        }
      ]
    }
  }
}
~~~

逆に、Postman などで API を使って送信テストをする際には、`FlexMessage` でメッセージオブジェクト化したものを JSON出力する必要があります。

<h2>次回予告<br><br>ポ<br>ストバック</h2>

Flex Message はさらにもっと複雑なこともできますが、SDK v3 仕様での Flex Message の触りとしては今回で終了し、次回はポストバックについて扱おうと思います。これを使うことで、ボットの可能性がさらに広がります。

## 目次 : [無料][2024年版] LINE Messaging API v3 + Python(Flask) でボットを作る

[GitHub レポジトリ](https://github.com/nozomiyamada/linebot/tree/main)
`older_version` 内に、各回時点での `app.py` と `requirements.txt` があります。

- [その0 - v3 での変更点](https://qiita.com/nozomiyamada/items/dff5f240e8c936d731fd)
- [その1 - 準備編](https://qiita.com/nozomiyamada/items/f6c6816e02359e08a74e)
- [その2 - テストアプリ編](https://qiita.com/nozomiyamada/items/87389aacb151b8418501)
- [その3 - 情報検索理論(前編)](https://qiita.com/nozomiyamada/items/d1bcedcc885c43794ff7)
- [その4 - 情報検索理論(後編)](https://qiita.com/nozomiyamada/items/6b0b30ebc36eeddc73d2)
- [その5 - tips 編](https://qiita.com/nozomiyamada/items/58918cd4024987ad1979)
- [その6 - モジュール化編](https://qiita.com/nozomiyamada/items/b80eaf5aef0c294c4acf)
- [その7 - クラス化編](https://qiita.com/nozomiyamada/items/c8315c8a0f0f7d19129b)
- その8 - Flex Message 編
- [その9 - ポストバック編](https://qiita.com/nozomiyamada/items/8a236a2d38f33827b4ce)
- [その10 - リッチメニュー & LIFF 編](https://qiita.com/nozomiyamada/items/ffbeeafbadfa6e98c711)