Line Bot+チャット機能に画像の送受信をつける

Python Django Line

概要

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という振り分けるための専用の処理があるようです。興味がある方は、こちらも利用してみてください。そして、私のリポジトリにプルリクとかをください。

Relation Posts

Django、Lineで更新を通知

Djangoで、Webサイトの更新情報を通知するシリーズの一つです。Lineで、Webサイトの更新を伝えていきます。

Python Django Line

Comment

記事にコメントする

まだコメントはありません。