Line Bot+チャット機能に画像の送受信をつける
概要
Djangoで、Line Botを使いつつ1対1のチャット機能で、LINE BOTとチャット機能を両立させました。
LINEではテキストのほか、画像だったりスタンプだったりの送受信も頻繁に行われます。LINEの機能を再現しようとするとキリがないのですが、画像の送受信に関してはあったほうが楽しそうです。それに、LINE BOTでの画像の送受信ができれば、作れるものの幅も広がります。画像を受け取って、Django側でそれを機械学習だとかさせる、とかも現実的ですね。
以前と同様に、Djangoで作ったサイト上でラインのやりとりができます。画像も送信することができますし、受け取れます。
もちろん、スマホのラインも同じ内容です。スマホ側でも、画像を送信でき、受け取れています。
Githubにソースコードがあるので、欲しい方はクローンしてください。
モデル
前回のモデルから、少し変更があります。
class LineMessage(models.Model):
"""Lineの各メッセージを表現する"""
push = models.ForeignKey(LinePush, verbose_name='プッシュ先', on_delete=models.SET_NULL, blank=True, null=True)
text = models.TextField('テキスト', blank=True)
image = models.ImageField('画像', blank=True, null=True)
is_admin = models.BooleanField('このメッセージは管理者側の発言か', default=True)
created_at = models.DateTimeField('作成日', default=timezone.now)
def __str__(self):
return f'{self.push} - {self.is_admin}'
imageフィールドが増えました。LINEメッセージは画像のみ(テキストなし)とテキストのみ(画像なし)のケースが考えられるので、画像とテキストはそれぞれ空欄を許可するようにしています。
画像の受信処理
callback
関数が次のようになりました。
from django.core.files.base import ContentFile
...
...
@csrf_exempt
def callback(request):
"""ラインの友達追加時・メッセージ受信時に呼ばれる"""
if request.method == 'POST':
request_json = json.loads(request.body.decode('utf-8'))
events = request_json['events']
line_user_id = events[0]['source']['userId']
# チャネル設定のWeb hook接続確認時にはここ。このIDで見に来る。
if line_user_id == 'Udeadbeefdeadbeefdeadbeefdeadbeef':
pass
# 友達追加時
elif events[0]['type'] == 'follow':
profile = line_bot_api.get_profile(line_user_id)
LinePush.objects.create(user_id=line_user_id, display_name=profile.display_name)
# アカウントがブロックされたとき
elif events[0]['type'] == 'unfollow':
LinePush.objects.filter(user_id=line_user_id).delete()
# メッセージ受信時
elif events[0]['type'] == 'message':
line_push = get_object_or_404(LinePush, user_id=line_user_id)
# テキストメッセージの場合
if events[0]['message']['type'] == 'text':
text = events[0]['message']['text']
LineMessage.objects.create(push=line_push, text=text, is_admin=False)
# 画像メッセージの場合
elif events[0]['message']['type'] == 'image':
message_id = events[0]['message']['id']
result = line_bot_api.get_message_content(message_id)
content_type = result.content_type
extension = content_type.split('/')[-1]
file_name = 'line.{}'.format(extension)
line = LineMessage.objects.create(push=line_push, is_admin=False)
line.image.save(file_name, ContentFile(result.content))
return HttpResponse()
変わったのは、メッセージ受信時の処理です。
# メッセージ受信時
elif events[0]['type'] == 'message':
line_push = get_object_or_404(LinePush, user_id=line_user_id)
# テキストメッセージの場合
if events[0]['message']['type'] == 'text':
text = events[0]['message']['text']
LineMessage.objects.create(push=line_push, text=text, is_admin=False)
# 画像メッセージの場合
elif events[0]['message']['type'] == 'image':
message_id = events[0]['message']['id']
result = line_bot_api.get_message_content(message_id)
content_type = result.content_type
extension = content_type.split('/')[-1]
file_name = 'line.{}'.format(extension)a
line = LineMessage.objects.create(push=line_push, is_admin=False)
line.image.save(file_name, ContentFile(result.content)) # result.contentが、画像のバイナリデータ
メッセージがテキストなのか画像なのかは、events[0]['message']['type']
の値で判断できます。テキストの場合は、今まで通りの処理ですね。
画像だった場合の処理ですが、このコールバックには画像データそのものが含まれていなくて、画像データ本体は別の場所から取り出す必要があります。それをしているのが、result = line_bot_api.get_message_content(message_id)
ですね。これで、画像データ本体を取得しています。
画像のcontent_typeから拡張子を取得し、それを使ってline.png
とかline.jpeg
といったファイル名を作ります。固定のファイル名だと保存のときに重複にならない?と不安に思うかもしれませんが、DjangoのImageFieldを使ってファイルを保存する際に、この辺のファイル名はうまいこと重複しないようにしてくれます。
画像データ本体は外の世界から持ってきつつ、それをImageFieldに設定したいって場合があります。具体例を出すと今回のように、よそのWeb APIを叩いて、それが画像のバイナリデータを返してくれて、それをモデルのImageFieldに設定したい場合です。それをしているのが、line.image.save(file_name, ContentFile(result.content))
です。
画像の表示
line_message_list.html
を、次のようにします。
{% extends 'app/base.html' %}
{% block content %}
<h1>{{ push.display_name }} チャット</h1>
{% for message in message_list %}
{% if message.text %}
<div class="card {% if message.is_admin %}right{% else %}left{% endif %}">
{{ message.text }} - {{ message.created_at }}
</div>
{% endif %}
{% if message.image %}
<div class="card {% if message.is_admin %}right{% else %}left{% endif %}">
<img src="{{ message.image.url }}" style="max-width: 100%; height: auto">
</div>
{% endif %}
{% endfor %}
<form action="" method="POST" enctype="multipart/form-data">
{{ form.as_p }}
<button type="submit">送信</button>
{% csrf_token %}
</form>
{% endblock %}
前との違いは、{% if message.text %}
でテキストがあればテキスト表示を、{% if message.image %}
として画像があればそれを表示するようにしました。
また、form要素にenctype="multipart/form-data"
もつけました。ファイルアップロードを行うフォームでは、これが必要です。
画像の送信処理
チャットページとなる、LineMessageListビューを次のようにします。
class LineMessageList(generic.CreateView):
model = LineMessage
fields = ('text', 'image')
template_name = 'app/line_message_list.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
line_push = get_object_or_404(LinePush, pk=self.kwargs['pk'])
context['message_list'] = LineMessage.objects.filter(push=line_push)
context['push'] = line_push
return context
def form_valid(self, form):
line_push = get_object_or_404(LinePush, pk=self.kwargs['pk'])
message = form.save(commit=False)
# テキストか画像が送信されていれば
if message.text or message.image:
message.push = line_push
message.is_admin = True
message.save()
if message.text:
line_bot_api.push_message(line_push.user_id, messages=TextSendMessage(text=message.text))
if message.image:
url = '{0}://{1}{2}'.format(
self.request.scheme,
self.request.get_host(),
message.image.url,
)
image_message = ImageSendMessage(
original_content_url=url,
preview_image_url=url,
)
line_bot_api.push_message(line_push.user_id, messages=image_message)
return redirect('app:line_message_list', pk=line_push.pk)
このサイト上から画像送信をできるようにしたいので、fields = ('text', 'image')
としておきます。これで、チャットページに画像アップロードのための欄が表示されます。チャットぺージのテンプレートファイル(line_message_list.html)では、{{ form.as_p }}
のようにしているので、textとimageフィールドのための欄が自動的に作られています。
form_validメソッドも変更がありました。if message.text or message.image:
は、テキストか画像のどちらかがあれば、という意味です。テキストも画像もなしで送信ボタンを押されたら、当然送るものもメッセージログも必要ないので、何もしないでリダイレクトさせます。
テキストがあれば、line_bot_api.push_message(line_push.user_id, messages=TextSendMessage(text=message.text))
でメッセージを送信します。
画像があれば、画像の完全なURLを作成し、ImageSendMessageで組み立て、line_bot_api.push_message(line_push.user_id, messages=image_message)
で送信します。URLはhttpsなものでないといけません。
WebhookHandler
callback関数でやっている振り分け処理ですが、line-bot-sdk-pythonを見ると、WebhookHandler
という振り分けるための専用の処理があるようです。興味がある方は、こちらも利用してみてください。そして、私のリポジトリにプルリクとかをください。