Linebot 2.0 with Django Complete Tutorial — Echo Bot, Saving Userprofile, Two-Page Richmenu

GoatWang
14 min readFeb 12, 2020

Introduction

There are four parts in this tutorial. The Github repo is at https://github.com/GoatWang/LinebotTutorial.

  1. Create Line Official Account: Line official account is different from general line account. Line official account will be shown as an independent username & picture in line.
  2. Build Django Backend Website (Echo Bot): In this part, the communication structure between user, line server and the website will be explained. Also, this tutorial will go through every step to build this website and deploy on to the heroku.
  3. Write Logic of the Bot (Saving Userprofile): The concept of Event, LineBotApi, Message and Action will be explained. Dealing with different types of Events, Sending different types of Messages, Embedding different types of Actions in the Message is the main point of this section. In the implementation tutorial, we are going to save the user profile into the database when the user add the account as a friend.
  4. RichMenu and RichMessage (Two-Page Richmenu): It is common to use self-defined images as button in Line nowadays. This part will describe how to produce this kind of image and the location of buttons. Also, we are going to make a two-page RichMenu in the implementation tutorial.

Create Line Official Account

Intro: Linebot Management Official Websites

There are two main websites to manage your bot: Line Business ID and Line Developer. There are two things to be noticed:

  1. Even most of the settings can be set in both sites, some settings can be managed only in one site. For example, developer “Roles” can only be managed in Line Developer, while “Rich menus” can only be designed in Line Business ID.
  2. Line Business ID is for Linebot 2.0, while Line@ Manager is for Linebot 1.0. If you don’t know what is Line@ Manager, just forget it. you can not log in to Line@ Manager if you don’t have any bot in Linebot 1.0. Do not confuse any of them with each other.

Step1: Allow your line account to log through other platforms.

Step2: Log in Line Developer

Step3: Create a new Provider

Step4: Select Channel Type: Messaging API Channel

Step5: Create Messaging API channel

Step6: other operations

  1. disable the Auto-reply messages
  2. issue a secret token

Build Django Backend Website

Intro: the communication structure between user, line server and the website.

There are two kinds of interaction mode in Line Messaging API: reply and push. In reply mode, the reply message will be sent only when the user questioned. In push mode, however, the push message will be sent automatically whenever you want. To be noticed, there is no limitation on the number of reply messages but only 500 user * message can be sent at line’s free-tier price plan. That is to say, once you have more than 500 friends, you cannot use push message function at line’s free-tier price plan.

The image below shows the interaction chain of user client, line server and your server. In reply mode, message will first from sent from user client to line server, then your server (left-to-right). This is followed by the backward response (right-to-left). In push mode, only the backward direction interaction (right-to-left) will happen. Anyway, we are going to build a django server as an echo bot in the section.

Step1: Install prerequisite

  1. Install Python3.6 ( https://www.python.org/downloads/release/python-365/)
  2. packages: django, virtualenv
>>> pip install django virtualenv

Step2: Create a project

Once Django is successfully installed in your python, you can run django-admin startproject <project_name> to create a new project.

>>> django-admin startproject LinebotTutorial

This command will create a directory with a structure like this.

│   manage.py

└───LinebotTutorial
│ settings.py
│ urls.py
│ wsgi.py
└ __init__.py

Step2: Create an app

An application can be imagined as an independent module of the whole website. If we only want to create linebot server, one application is enough. Be sure to change directory to the project directory. Then run python manage.py startapp <app_name> to create a new application. To be mentioned, its recommended not to set app_name as names that will appear in the packages used by the project such as “linebot” or “app”.

>>> cd LinebotTutorial
>>> python manage.py startapp tutorialbot

This command will create a directory called tutorialbot. The whole structure will be like this.

│   manage.py

├───LinebotTutorial
│ │ settings.py
│ │ urls.py
│ │ wsgi.py
│ └ __init__.py
└───tutorialbot
│ admin.py
│ apps.py
│ models.py
│ tests.py
│ views.py
│ __init__.py

└───migrations
__init__.py

Step3: Build a virtual environment

Virtual environment is generally used in python to build an independent environment that can help to avoid the conflicts between packages. Virtual environment is also helpful for coping an environment from PC to server.

>>> virtualenv venv # create the env
>>> venv\Scripts\activate #activate the env
>>> pip install django line-bot-sdk #install packages

Step4: write the response function and set the route

  • in tutorialbot\views.py
from django.http import HttpResponsedef index(request):
return HttpResponse("test!!")
  • create tutorialbot\urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
]
  • in LinebotTutorial\urls.py
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path('tutorialbot/', include('tutorialbot.urls')),
path('admin/', admin.site.urls),
]

Step5: test runserver locally

Run the server locally using the following script, and go to http://127.0.0.1:8000/tutorialbot/. If you can see “test!!”, you successfully run the server locally.

>>> python manage.py runserver

Steps6: Deployment of the website

Be sure that the url of the website should be https which is limited by line official. Once you don’t have https server, it is recommended to use heroku. Heroku is a free resource to deploy your website. The only drawback is that the server will sleep when it receives no web traffic in a 30-minute. It will wake up when receiving a new request, but it takes about 30 seconds to wake up. The Django deploy tutorial is issued by heroku.

# prepare requirements.txt in the project root directory
>>> pip install gunicorn
>>> pip install django-heroku
>>> pip freeze > requirements.txt
# in the LinebotTutorial\settings.py
# at the top of settings.py
import django_heroku
# at the bottom of settings.py
django_heroku.settings(locals()) #Activate Django-Heroku.
# create Procfile (no extension) in the project root directory
web: gunicorn LinebotTutorial.wsgi
  • prepare the project as a git repo

Write .gitignore in the project root to avoid venv directory to be uploaded since it is quite fat. And we are going to use requirements.txt to copy the venv in the server.

#just write these lines in .gitignore
venv
staticfiles
__pycache__

initialize a git repo

>>> git init 
>>> git add .
>>> git commit -m "my first commit"
  • login to heroku & create the app & deploy the website
>>> heroku login 
>>> heroku create # create the heroku app
>>> git push heroku master # deploy the code on to the heroku server
>>> heroku ps:scale web=1 # call the server to run the Procfile
>>> heroku open # open the website url. you can add "/tutorialbot/" at the end of the url, then you will see the test!! again

Step7: Write CHANNEL_ACCESS_TOKEN and CHANNEL_SECRET

write LINE_CHANNEL_ACCESS_TOKEN and LINE_CHANNEL_SECRET, which is from Line Business ID website, at the bottom of LinebotTutorial\settings.py. If you are going to push this to the cloud server such as Github, it is recommended to store the token and the secret in the environment variables and call them through os.environ[‘<env_name>’]

LINE_CHANNEL_ACCESS_TOKEN = "<your LINE_CHANNEL_ACCESS_TOKEN>"
LINE_CHANNEL_SECRET = "<your LINE_CHANNEL_SECRET>"

Step8: Write response function

This code is modified from line-bot-sdk git repository. It is used to receive the requests from line server and give the same text response back to line.

in tutorialbot/views.py

from django.conf import settings # calls the object written in settings.py
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from linebot import LineBotApi, WebhookHandler
from linebot.exceptions import InvalidSignatureError
from linebot.models import MessageEvent, TextMessage, TextSendMessage
line_bot_api = LineBotApi(settings.LINE_CHANNEL_ACCESS_TOKEN)
handler = WebhookHandler(settings.LINE_CHANNEL_SECRET)
# this is for testing
def index(request):
return HttpResponse("test!!")
# this is code is modeified from https://github.com/line/line-bot-sdk-python
@csrf_exempt # this is used for avoid csrf request from line server
def callback(request):
if request.method == "POST":
# get X-Line-Signature header value
signature = request.META['HTTP_X_LINE_SIGNATURE']
global domain
domain = request.META['HTTP_HOST']

# get request body as text
body = request.body.decode('utf-8')
# handle webhook body
try:
handler.handle(body, signature)
except InvalidSignatureError:
return HttpResponseBadRequest()
return HttpResponse()
else:
return HttpResponseBadRequest()
# this function is used for process TextMessage from users
@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
line_bot_api.reply_message(
event.reply_token,
TextSendMessage(text=event.message.text))

in tutorialbot/urls.py

urlpatterns = [
path('', views.index, name='index'),
path('callback/', views.callback, name='callback'), # add this line
]

in the INSTALLED_APP part in SLinebotTutorial/settings.py

INSTALLED_APPS = [
'tutorialbot.apps.TutorialbotConfig', # add this line
...
'django.contrib.messages',
'django.contrib.staticfiles',
]

deploy the app

>>> git add .
>>> git commit -m "callback function"
>>> git push heroku master

Step9: Set Webhook url on Line Developers

Fill in your heroku url , which can get from “heroku open”. Remember to add “/LinebotTutorial/callback/” at the tail of the url. After editing the url, be careful to check the “Use webhook” button.

Step10: add your bot as a friend and check its status

Write Logic of the Bot

Intro: Event, LineBotApi, Message and Action

The basic concept of line-bot-sdk is composed of Event, Message, LineBotApi, SendMessage and Action objects. Once the bot receives an Event (e.g. MessageEvent) with a Message (TextMessage), the bot server should use LineBotApi to reply_message. Then SendMessage type (e.g. TextSendMessage) should be selected. If some buttons is embedded in the SendMessage (e.g. TemplateSendMessage), the Action (e.g. UriAction)should be defined when the button is clicked. All types of Event, Message , SendMessage and Action and all functions of LineBotApi are described in detail in the repo of line-bot-sdk. Some generally used objects of line-bot-sdk are listed below.

  • Event Types: MessageEvent, FollowEvent, UnfollowEvent, JoinEvent, LeaveEvent, PostbackEvent…
  • Message Types: TextMessage, ImageMessage, LocationMessage, FileMessage…
  • LineBotApi Functions: reply_message, push_message, get_profile…
  • SendMessage Types: TextSendMessage, ImageSendMessage, LocationSendMessage, StickerSendMessage, ImagemapSendMessage, TemplateSendMessage…
  • Action Types: URIAction, PostbackAction, MessageAction…

Intro: Deal with the FollowEvent

In this section, I am going to use FollowEvent as example to illustrate the method to use Event, LineBotApi, Message and Action objects in Django. As a result, we are going to experience the following steps:

  1. the user adds the bot as a friend, and the bot receives the FollowEvent without Message.
  2. the bot use get_profile to get the user profile and save it to the database.
  3. the bot send a TemplateSendMessage with a button to ask the user if he/she wants to receive sales promotion messages.
  4. When the button is clicked, the PostbackAction will be triggered and bot will receive a PostbackEvent and update the database.

Step1: Disable Greeting Message from line

Step2: Open Userprofile schema in Database

Although Django have built a User model for us, the schema is not matched with the information provided by line. As a result, we should design a new schema to save information provided by get_profile function. To be mentioned, sqlite3 is used as the default database by Django, which can be changed by editing settings.py.

  • in tutorialbot/models.py
from django.db import models
from django.contrib.auth.models import User
# Create your models here.
class UserProfile(models.Model):
# get from line
user = models.OneToOneField(User, on_delete=models.CASCADE) #OneToOneField is used to extend the original User object provided from Django.
line_id = models.CharField(max_length=50, primary_key=True, verbose_name="LineID")
line_name = models.CharField(max_length=100, verbose_name="Line名稱")
line_picture_url = models.URLField(verbose_name="Line照片網址")
line_status_message = models.CharField(max_length=100, blank=True, null=True, verbose_name="Line狀態訊息")
# generated by system
unfollow = models.BooleanField(default=False, verbose_name="封鎖")
create_time = models.DateTimeField(auto_now=True, verbose_name="創建時間")
# other
promotable = models.BooleanField(default=False, verbose_name="promotable")
  • update the database
>>> python manage.py makemigrations # generate the sql code 
>>> python manage.py migrate # apply the sql code to local DB

Step3: Handle FollowEvent Function

  • at the top of tutorialbot/views.py
from django.contrib.auth.models import User
from tutorialbot.models import UserProfile
from linebot.models import (
MessageEvent, FollowEvent,
TextMessage,
PostbackAction,
TextSendMessage, TemplateSendMessage,
ButtonsTemplate
)
  • at the bottom of tutorialbot/views.py
@handler.add(FollowEvent)
def handle_follow(event):
line_id = event.source.user_id
profile = line_bot_api.get_profile(line_id)
profile_exists = User.objects.filter(username=line_id).count() != 0
if profile_exists:
user = User.objects.get(username = line_id)
user_profile = UserProfile.objects.get(user = user)
user_profile.line_name = profile.display_name
user_profile.line_picture_url = profile.picture_url
user_profile.line_status_message = profile.status_message
user_profile.unfollow = False
user_profile.save()
else:
user = User(username = line_id)
user.save()
user_profile = UserProfile(
line_id = line_id,
line_name = profile.display_name,
line_picture_url = profile.picture_url,
line_status_message=profile.status_message,
user = user
)
user_profile.save()
buttons_template_message = TemplateSendMessage(
alt_text='Product Promotion',
template=ButtonsTemplate(
title="Product Promotion",
text='Do you want to receive the promotion messages?',
actions=[
PostbackAction(
label='yes',
display_text='yes',
data='promotion=true'
),
]
)
)
line_bot_api.reply_message(
event.reply_token,
[
TextSendMessage(text="Hello\U0010007A"),
buttons_template_message,
]
)
  • deploy to heroku
>>> git add .
>>> git commit -m "handle FollowEvent"
>>> git push heroku master
>>> heroku run python manage.py migrate # apply the sql code to heroku DB

Step4: Test in line

Unfollow the account first, then follow it again.

Step5: Test in Heroku

  • ssh to heroku server & run Django shell
>>> heroku run python manage.py shell
  • show all UserProfile
>>> from tutorialbot.models import UserProfile
>>> user_profiles = UserProfile.objects.all()
>>> for u in user_profiles:
... print(u.line_name, u.create_time)

Then, “<user_line_name> <create_time>” will be printed.

Step6: Handle PostbackEvent Function

  • at the top of tutorialbot/views.py
from linebot.models import (
MessageEvent, FollowEvent, PostbackEvent,
TextMessage,
PostbackAction,
TextSendMessage, TemplateSendMessage,
ButtonsTemplate
)
  • at the bottom of tutorialbot/views.py
@handler.add(PostbackEvent)
def handle_postback(event):
if event.postback.data == "promotion=true":
line_id = event.source.user_id
user_profile = User.objects.get(username=line_id)
user_profile.promotable= True # set promotable to be True
user_profile.save()
  • push to heroku server
>>> git add .
>>> git commit -m "handle PostbackEvent"
>>> git push heroku master

Step7: Test in Heroku

  • ssh to heroku server & run Django shell
>>> heroku run python manage.py shell
  • show all UserProfile
>>> from tutorialbot.models import UserProfile
>>> user_profiles = UserProfile.objects.all()
>>> for u in user_profiles:
... print(u.line_name, u.promotable, u.create_time)

Then, “<user_line_name> <promotable> <create_time>” will be printed.

Rich Menu and Message

Intro: we are going to implement…

In the section, I draw two images. There is a button to go to the next page in the first image, while there is a button to go to the previous page in the second image. The icon of my company is just an url to go to the home website of Thinktron.

step1: prepare the images

There is a limitation on the images’ size. The size should be in 2500x1686, 2500x843, 1200x810, 1200x405, 800x540 or 800x270, which is from here. To make this kind of image, I use an online tool called pixlr. It is possible to create any size of canvas and draw any image on to the canvas, which can help to generate the required images.

step2: write the script to define the location of buttons

There are two ways to define the location and area of buttons. First is Line Business ID. Its drawback is you can design the RichMenu only by templates provided by line. Also, the postback action cannot be set on the website.

where to design richmenu
templates provided by line
No postback action

The second way to design RichMenu is sand a post request to line server. It is more complicated but more flexible. You have to define the absolute start location (x and y) and the area (height and width) for each button. It is possible that you want to put many buttons on your RichMenu and they are all in the arbitrary location and area. It’s recommended to use Bot Designer to design it, the tool provides a convenient GUI.

What we are going to is just split the image into two parts (left and right). As a result, we can just divide the width of the image by 2 to get the width of the button.

  • create a new directory called RichMenu in the project root
  • create a new directory called images in the RichMenu directory
  • put the image in the images directory (if you want to use the same image with this project, download by here. The size of the image is 800 * 540)
  • create two .py files called create_firstpage.py & create_secondpage.py
  • (optional) create a .py files called list_richmenus.py
  • remember to add RichMenu directory in .gitignore file
venv
.vscode
staticfiles
__pycache__
RichMenu # add this line
  • in create_firstpage.py

Notice the RichMenuBounds of the first area, the start location is (0, 0), width is 400(half of the image width) and height is 540 (same with the image). The RichMenu id will be printed, which should be kept and used in the postback event handler in views.py.

import os
import requests
from linebot import LineBotApi
from linebot.models import (
RichMenu, RichMenuSize, RichMenuArea, RichMenuBounds,
URIAction, PostbackAction
)
LINE_CHANNEL_ACCESS_TOKEN = "<your LINE_CHANNEL_ACCESS_TOKEN>"
line_bot_api = LineBotApi(LINE_CHANNEL_ACCESS_TOKEN)
# create rich menu
# from https://developers.line.biz/en/reference/messaging-api/#create-rich-menu
rich_menu_to_create = RichMenu(
size=RichMenuSize(width=800, height=540), #2500x1686, 2500x843, 1200x810, 1200x405, 800x540, 800x270
selected=True,
name="NextPage",
chat_bar_text="See Menu",
areas=[
RichMenuArea(
bounds=RichMenuBounds(x=0, y=0, width=400, height=540),
action=URIAction(label='Thinktron', uri='https://www.thinktronltd.com/')),
RichMenuArea(
bounds=RichMenuBounds(x=400, y=0, width=400, height=540),
action=PostbackAction(label='Next Page', data='action=nextpage')),
]
)
rich_menu_id = line_bot_api.create_rich_menu(rich_menu=rich_menu_to_create)
print("rich_menu_id", rich_menu_id)
# upload image and link it to richmenu
# from https://developers.line.biz/en/reference/messaging-api/#upload-rich-menu-image
with open(os.path.join('images', 'firstpage.jpg'), 'rb') as f:
line_bot_api.set_rich_menu_image(rich_menu_id, 'image/jpeg', f)
# set as default image
url = "https://api.line.me/v2/bot/user/all/richmenu/"+rich_menu_id
requests.post(url, headers={"Authorization": "Bearer "+LINE_CHANNEL_ACCESS_TOKEN})
  • in create_secondpage.py
import os
import requests
from linebot import LineBotApi
from linebot.models import (
RichMenu, RichMenuSize, RichMenuArea, RichMenuBounds,
URIAction, PostbackAction
)
LINE_CHANNEL_ACCESS_TOKEN = "<your LINE_CHANNEL_ACCESS_TOKEN>"
line_bot_api = LineBotApi(LINE_CHANNEL_ACCESS_TOKEN)
# create rich menu
# from https://developers.line.biz/en/reference/messaging-api/#create-rich-menu
rich_menu_to_create = RichMenu(
size=RichMenuSize(width=800, height=540), #2500x1686, 2500x843, 1200x810, 1200x405, 800x540, 800x270
selected=True,
name="PreviousPage",
chat_bar_text="See Menu",
areas=[
RichMenuArea(
bounds=RichMenuBounds(x=0, y=0, width=400, height=540),
action=PostbackAction(label='Previous Page', data='action=previouspage')),
RichMenuArea(
bounds=RichMenuBounds(x=400, y=0, width=400, height=540),
action=URIAction(label='Thinktron', uri='https://www.thinktronltd.com/')),
]
)
rich_menu_id = line_bot_api.create_rich_menu(rich_menu=rich_menu_to_create)
print("rich_menu_id", rich_menu_id)
# upload image and link it to richmenu
# from https://developers.line.biz/en/reference/messaging-api/#upload-rich-menu-image
with open(os.path.join('images', 'secondpage.jpg'), 'rb') as f:
line_bot_api.set_rich_menu_image(rich_menu_id, 'image/jpeg', f)
  • (optional) in list_richmenus.py
from pprint import pprint
from linebot import LineBotApi
LINE_CHANNEL_ACCESS_TOKEN = "<your LINE_CHANNEL_ACCESS_TOKEN>"
line_bot_api = LineBotApi(LINE_CHANNEL_ACCESS_TOKEN)
rich_menu_list = line_bot_api.get_rich_menu_list()
pprint(rich_menu_list)
# delete all richmenus
# for rm in rich_menu_list:
# line_bot_api.delete_rich_menu(rm.rich_menu_id)
  • run create_firstpage.py & create_secondpage.py to create rich menu
>>> cd RichMenu
>>> python create_firstpage.py
>>> python create_secondpage.py
  • at the bottom of tutorialbot\views.py

Modify your handle_postback function. You should fill in the id for each RichMenu. Once you forget it you can use list_richmenus.py to find it.

@handler.add(PostbackEvent)
def handle_postback(event):
line_id = event.source.user_id
if event.postback.data == "promotion=true":
user_profile = UserProfile.objects.get(line_id=line_id)
user_profile.promotable = True
user_profile.save()
line_bot_api.reply_message(
event.reply_token,
[
TextSendMessage(text="Thanks\U0010007A"),
]
)
elif event.postback.data == "action=nextpage":
line_bot_api.link_rich_menu_to_user(line_id, "<firstpage richmenu id>")
elif event.postback.data == "action=previouspage":
line_bot_api.link_rich_menu_to_user(line_id, "<secondpage richmenu id>")
  • evaluate the result

--

--