[RAILS] How to make LINE messaging function made with Ruby

Introduction

I had the opportunity to develop a function for exchanging messages between a system written in Ruby and LINE, a general user, so I will share the knowledge I gained at that time.

I also made a demo of a working application using Rails, so see here for detailed implementation details. I think that the content of the article will come in smoothly if you ask.

What to make

A system administrator (hereinafter referred to as "administrator") and a general user (hereinafter referred to as "user") create a function for exchanging text messages via LINE. Since the system needs to know the user's LINE information in order to actually communicate, we will also implement LINE login on the browser.

After linking the user's LINE information, when the user sends a message to the LINE official account linked to the administrator, the message will be sent to the system by Webhook and saved in the database from there. The image is roughly as shown in the figure below. Untitled Diagram (1).png

Administrators can also send LINE messages to users through the system. When you send a text from the system, you can send a message to the target user from the administrator's LINE official account through the program. Once sent, the message will be saved in the DB. The image is as follows. Untitled Diagram (2).png

Work required in advance

Create a LINE API channel

If the administrator does not have a LINE API channel, it will not be possible to communicate with users on LINE, so create it. Let's make it from LINE Developer console. The channel you need to create this time

There are two.

The Messaging API acts as an official LINE account. As a caveat, let's set the ** response mode to "Bot" **. In chat mode, I can communicate from the LINE official account manager, but I can't receive webhooks. Therefore, it is necessary to use it in Bot mode because there is a problem that the system cannot keep a log of the interaction. For details on how to create it, click here [https://developers.line.biz/ja/docs/messaging-api/getting-started/#%E3%83%81%E3%83%A3%E3%83%8D% E3% 83% AB% E3% 81% AE% E4% BD% 9C% E6% 88% 90) Please. LINE Login is literally a channel for using LINE Login.

After creating, create the data in the admins table of DB. Originally, I think that it is good to register from the screen, but it is made to wear it sideways and poke it directly from the console (when putting it in the DB with the production code, it is recommended to encrypt it in consideration of security To do)

  create_table "admins", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", force: :cascade do |t|
    t.integer "line_messaging_id", null: false #Messaging API "Channel ID"
    t.string "line_messaging_secret", null: false #Messaging API "Channel Secret"
    t.string "line_messaging_token", null: false #Messaging API "Channel Access Token"
    t.integer "line_login_id", null: false #"Channel ID" of LINE Login
    t.string "line_login_secret", null: false #LINE Login "Channel Secret"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

Set various URLs

Next time, I need to set some URLs on the console side of LINE, so I will register. Specifically, it is as follows.

--Messaging API Webhook URL (URL received by the system when the user sends a message) --LINE login callback URL (redirect URL after login)

There are some things to be aware of when registering this URL. It has a URL

--https only --localhost not available

That is the point. At this rate, local development seems difficult. However, you can temporarily publish the local URL to the outside by using ngrok, so you can comfortably develop locally by registering the URL created by it. You can do it. It's very convenient.

After creating the POST request path for the Webhook URL and setting it on the console, verify the request through the "Verify" button. Make sure you get 200 responses. For more information [here](https://developers.line.biz/ja/docs/messaging-api/building-bot/#%E3%82%B3%E3%83%B3%E3%82%BD%E3% 83% BC% E3% 83% AB% E3% 81% A7% E3% 83% 9C% E3% 83% 83% E3% 83% 88% E3% 82% 92% E8% A8% AD% E5% AE% 9A%E3%81%99%E3%82%8B) Please.

Now that the preparations for development are complete, I would like to continue developing.

Link the user's LINE account to the system

First, I would like to create a LINE login function to put the user's LINE information necessary for sending and receiving messages into the DB. This LINE information is the "user ID". Please note that it is not the LINE ID used for ID search.

The login flow is [here](https://developers.line.biz/ja/docs/line-login/integrate-line-login/#%E3%83%AD%E3%82%B0%E3%82% A4% E3% 83% B3% E3% 81% AE% E3% 83% 95% E3% 83% AD% E3% 83% BC), but if I write it again

  1. Direct the user from the system to the authorization URL for LINE login using the required query parameters
  2. The LINE login screen opens in your browser, and the user logs in and is authenticated.
  3. Redirect the user from the LINE platform to the system via redirect_uri. At this time, the query string including the authorization code and state is also sent to the system.
  4. After validating the state, the system uses the authorization code to request an access token at the endpoint URL https://api.line.me/oauth2/v2.1/token.
  5. The LINE platform validates the request from the system and returns an access token.
  6. Call the Social API based on the access token to get the user ID and store it in the DB.

Let's look at the code that creates the authorization URL for step 1.

app/controllers/auth_controller.rb


class AuthController < ApplicationController
  def index
    admin = Admin.find(params[:admin_id])

    state = SecureRandom.hex(32)
    session[:state] = state
    redirect_to Line::Api::Oauth.new(admin).auth_uri(state)
  end
end

Use / admin /: admin_id / auth to redirect to the authorization URL based on the account information of the target administrator's LINE login. Since state is necessary for CSRF measures, a random number is generated on the application side. The value is stored in session (used in post-authorization processing) and passed to Line :: Api :: Oauth # auth_uri. Let's take a look at the code in it.

app/models/line/api/oauth.rb


AUTH_URI = 'https://access.line.me/oauth2/v2.1/authorize'

def auth_uri(state)
  params = {
    response_type: 'code',
    client_id: @admin.line_login_id,
    redirect_uri: callback_uri,
    state: state,
    scope: 'openid',
    prompt: 'consent', #Option to make sure to allow LINE authentication
    bot_prompt: 'aggressive' #After logging in, a screen will appear asking if you want to make friends with the official account you linked with.
  }

  # NOTE: https://developers.line.biz/ja/docs/line-login/integrate-line-login/#making-an-authorization-request
  "#{AUTH_URI}?#{params.to_query}"
end

See here for the detailed meaning of the parameters. Personally, the bot_prompt parameter is convenient, and after logging in to LINE, the screen for adding friends to the corresponding official account is also displayed, so I thought it would be convenient to take messages with me.

When you hit the URL, you will be redirected to the login screen as shown in the image below.

スクリーンショット 2020-06-16 1.52.14.png

When login is complete and authorization is complete, you will be redirected to the LINE login callback URL you set earlier. Now that we're heading to / admin /: admin_id / callback, let's look at the corresponding code.

app/controllers/callback_controller.rb


class CallbackController < ApplicationController
  def index
    admin = Admin.find(params[:admin_id])

    #Throw an exception if the states are different
    raise Line::InvalidState unless params[:state] == session[:state]

    line_user_id = Line::Api::Oauth.new(admin).line_user_id(params[:code])
    User.create!(line_user_id: line_user_id)

    render plain: 'LINE cooperation completed!', status: :ok
  end
end

CSRF measures are taken by comparing the state value that comes after authorization with the state value of the session that was previously entered. Then get your LINE user ID. Once obtained, store it in the DB. Let's take a look at the user ID acquisition code.

app/models/line/api/oauth.rb


def line_user_id(code)
  verify_id_token(access_token_data(code))['sub']
end

def access_token_data(code)
  req_body = {
    grant_type: 'authorization_code',
    code: code,
    redirect_uri: callback_uri, # NOTE:It is compared with the "callback URL" set in the console of the LINE login channel.
    client_id: @admin.line_login_id,
    client_secret: @admin.line_login_secret
  }

  # NOTE: https://developers.line.biz/ja/docs/line-login/integrate-line-login/#get-access-token
  res = conn.post do |req|
    req.url '/oauth2/v2.1/token'
    req.headers['Content-Type'] = 'application/x-www-form-urlencoded'
    req.body = req_body
  end
  JSON.parse(handle_error(res).body)
end

def verify_id_token(access_token_data)
  req_body = {
    id_token: access_token_data['id_token'],
    client_id: @admin.line_login_id
  }
  # NOTE: https://developers.line.biz/ja/reference/social-api/#verify-id-token
  res = conn.post do |req|
    req.url '/oauth2/v2.1/verify'
    req.body = req_body
  end
  JSON.parse(handle_error(res).body)
end

ʻAccess_token_data` hits Access Token Acquisition API to return information, and it To the Social API ID Token Verification API and succeed, you will get your user ID.

One thing to keep in mind is that the access token acquisition API passes redirect_uri, which should be exactly the same as the redirect_uri set in the authorization URI. You may not do much, but for various reasons, if you want to pass something as a query parameter to redirect_uri when creating an authorization URL and get it with a callback, the access token acquisition API including that query parameter If you do not pass it to redirect_uri, you will get an error.

Receive user's message on the system

Now that the user has been able to link with the administrator's LINE official account, I would like to receive the LINE message sent by the user in the system next. Since the URL of the path of / admin /: admin_id / webhook is specified in the Webhook URL of the Messaging API, the request will be sent every time some event (add friend, block message, etc.) occurs in the official account. Become. So, if the event where the message was sent from that request && the person who sent it is the LINE user ID ** saved in the DB, if you save the message in the DB, the LINE messages from the user will be collected. You can. The code for the webhook controller is below.

app/controllers/webhook_controller.rb


class WebhookController < ApplicationController
  protect_from_forgery with: :null_session

  def create
    admin = Admin.find(params[:admin_id])

    bot = Line::Api::Bot.new(admin)
    body = request.body.read
    raise Line::InvalidSignatureError unless bot.validate_signature?(body, request.env['HTTP_X_LINE_SIGNATURE'])

    events = bot.parse_events_from(body)
    events.each do |event|
      case event
      when Line::Bot::Event::Message
        Line::SaveReceivedMessage.new(admin).call(event)
      end
    end

    render plain: 'success!', status: :ok
  end
end

Since the request comes from the outside, invalidate the CSRF token. You will also need to do a Signature Validation to determine if the request is from the LINE platform. It's a little complicated, but let's use it because it will be done with a gem called line-bot-sdk-ruby.

Once verified, determine what the event looks like. I will use it because the gem will also parse the event information from the request body. I think it's simple to branch the event with a case statement. Since Line :: Bot :: Event :: Message is the event when the message arrives, the process of saving the message is inserted at that time. The code is below.

app/models/line/save_received_message.rb


module Line
  class SaveReceivedMessage
    def initialize(admin)
      @admin = admin
    end

    def call(event)
      user = User.find_by(line_user_id: event['source']['userId'])

      resource = MessageText.new(content: event.message['text'])
      Message.create!(sendable: user, receivable: @admin, resource: resource) if user.present?
    end
  end
end

See here for the contents of the event object.

Finally, check the operation. iOS_の画像.png When you send a message

スクリーンショット_2020-06-16_13_02_20.png You can see that the request is skipped and the record is saved. The system can now receive the user's LINE message and put it in the DB.

Send a message from the system to the user

Next, we will look at System-> User's LINE message transmission, which is the opposite of the previous one. You don't need to use Webhook, just hit API to send push message and you're done.

app/models/line/save_sent_message.rb


module Line
  class SaveSentMessage
    def initialize(admin)
      @admin = admin
    end

    def call_with_text(user:, text:)
      user = User.find_by(line_user_id: user.line_user_id)

      if user.present?
        Line::Api::Push.new(@admin).call_with_text(user: user, text: text)
        resource = MessageText.new(content: text)
        Message.create!(sendable: @admin, receivable: user, resource: resource)
      end
    end
  end
end

app/models/line/api/push.rb


module Line::Api
  class Push < Base
    def call_with_text(user:, text:)
      call(user: user, resource: Message::Text.new([text]))
    end

    private

    def call(user:, resource:)
      req_body = {to: user.line_user_id}.merge(resource.request_body)
      # NOTE: https://developers.line.biz/ja/reference/messaging-api/#send-push-message
      res = conn.post do |req|
        req.url '/v2/bot/message/push'
        req.headers['Content-Type'] = 'application/json'
        req.headers['Authorization'] = "Bearer #{@admin.line_messaging_token}"
        req.body = req_body.to_json
      end

      handle_error(res)
    end
  end
end

It is an operation check. Wear it sideways and hit the chord from the console.

スクリーンショット_2020-06-18_8_13_23.png A record has been added.

iOS_の画像__1_.png

A message has been sent to LINE!

at the end

With the above, it is possible to communicate with each other between the user and the system and leave the contents. I think it will be more complicated in a real system, but I hope this will help you implement something: pray:

Recommended Posts

How to make LINE messaging function made with Ruby
Make Ruby Kurosawa with Ruby (Ruby + LINE Messaging API)
Learning Ruby with AtCoder 13 How to make a two-dimensional array
How to implement TextInputLayout with validation function
[Swift] How to implement the LINE login function
How to make a follow function in Rails
Easy to make LINE BOT with Java Servlet
How to make shaded-jar
How to make batch processing with Rails + Heroku configuration
How to make a factory with a model with polymorphic association
How to make an almost static page with rails
Let's make a LINE Bot with Ruby + Sinatra --Part 2
How to make Laravel faster with Docker for Mac
Let's make a LINE Bot with Ruby + Sinatra --Part 1
[Java] How to start a new line with StringBuilder
[Ruby On Rails] How to use simple_format to display the entered text with line breaks
Java to play with Function
Java --How to make JTable
How to add ActionText function
How to use Ruby return
[Ruby] How to comment out
How to number (number) with html.erb
How to update with activerecord-import
Ruby: How to use cookies
[Ruby] How to write blocks
[Rails] How to make seed
Comparison of how to write Callback function (Java, JavaScript, Ruby)
[Easy] How to automatically format Ruby erb file with vsCode
I want to make a function with kotlin and java!
How to make an app using Tensorflow with Android Studio
How to deal with different versions of rbenv and Ruby
Make electronic music with randomness with Ruby
I want to make a button with a line break with link_to [Note]
How to get started with slim
How to iterate infinitely in Ruby
I want to add a browsing function with ruby on rails
How to make a jar file with no dependencies in Maven
How to make a Java container
How to install ruby through rbenv
How to make an application with ruby on rails (assuming that the environment has been built)
How to enclose any character with "~"
How to use Ruby on Rails
How to make a JDBC driver
How to install Bootstrap in Ruby
How to write ruby if in one line Summary by beginner
[Ruby] How to use any? Method
How to use mssql-tools with alpine
How to make a splash screen
How to make a Jenkins plugin
How to make a Maven project
How to get along with Rails
How to add the delete function
[For beginners] How to get the Ruby delayed railway line name
Learning Ruby with AtCoder 11 How to receive frequently used standard input
How to use Ruby inject method
How to make a Java array
How to use MinIO with the same function as S3 Use docker-compose
[Android] How to make Dialog Fragment
How to execute Ruby irb (interactive ruby)
How to start Camunda with Docker
I tried to make a group function (bulletin board) with Rails