├── .gitignore ├── config.ru ├── images ├── apple.jpg └── qrcode.png ├── line_chatbot_v3.pdf ├── Gemfile ├── README.ja.md ├── tokyo_events_api.rb ├── imagga.rb ├── weather_api.rb ├── Gemfile.lock ├── app.rb └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | test.rb 3 | .env -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | require './app' 2 | run Sinatra::Application 3 | -------------------------------------------------------------------------------- /images/apple.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lewagonjapan/bob-the-bot/HEAD/images/apple.jpg -------------------------------------------------------------------------------- /images/qrcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lewagonjapan/bob-the-bot/HEAD/images/qrcode.png -------------------------------------------------------------------------------- /line_chatbot_v3.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lewagonjapan/bob-the-bot/HEAD/line_chatbot_v3.pdf -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'sinatra' 3 | gem 'line-bot-api', '~> 1.18.0 ' 4 | gem 'geocoder' 5 | 6 | gem 'puma' 7 | gem 'thin' 8 | gem 'reel' 9 | gem 'http' 10 | gem 'webrick' 11 | gem 'rest-client' -------------------------------------------------------------------------------- /README.ja.md: -------------------------------------------------------------------------------- 1 | # LINE Bot 101 2 | 下記のリンク先ドキュメントに従って進めるとIBM Watsonを使った画像認識BOTが作れます 3 | 4 | ## Heroku 5 | https://scrapbox.io/xhack-ttl-workshops/Heroku%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9 6 | 7 | ## Codenvy 8 | https://www.notion.so/Codenvy-363a45f837c241b58af5cc56bec024ba 9 | 10 | ## LINE Developers 11 | https://scrapbox.io/xhack-ttl-workshops/LINE_Developers_%E3%82%A2%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E4%BD%9C%E6%88%90%E3%81%A8bot%E3%81%AE%E6%BA%96%E5%82%99 12 | 13 | ## Docs of OpenWeather API 14 | https://openweathermap.org/api 15 | https://news.mynavi.jp/techplus/article/nadeshiko-22/ 16 | 17 | ## Docs of Imagga image recognition 18 | https://docs.imagga.com/?ruby#getting-started 19 | -------------------------------------------------------------------------------- /tokyo_events_api.rb: -------------------------------------------------------------------------------- 1 | require 'date' 2 | require 'json' 3 | require 'open-uri' 4 | 5 | def fetch_tokyo_events 6 | url = 'https://tokyo-events.herokuapp.com/api' 7 | begin 8 | data_serialized = URI.open(url).read 9 | rescue OpenURI::HTTPError => e 10 | return 'No events found in Tokyo...' 11 | end 12 | data = JSON.parse(data_serialized) 13 | # Only keep the events of the week and sort'em 14 | week_events = data.reject { |event| Date.parse(event['date']) - Date.today > 7 }.sort_by { |event| event['date'] } 15 | week_events_hash = Hash.new('') 16 | week_events.each do |event| 17 | date = Date.parse(event['date']).strftime('%a, %b %e') 18 | week_events_hash[date] += "#{event['name']}\n" 19 | end 20 | 21 | answer = "There are some cool events this week:\n\n" 22 | week_events_hash.each do |key, value| 23 | answer += "- #{key}\n#{value}\n" 24 | end 25 | answer 26 | end 27 | -------------------------------------------------------------------------------- /imagga.rb: -------------------------------------------------------------------------------- 1 | require 'rest-client' 2 | require 'base64' 3 | 4 | def fetch_imagga(response_image) 5 | tf = Tempfile.open 6 | tf.write(response_image.body) 7 | 8 | image_results = "" 9 | File.open(tf.path) do |images_file| 10 | classes = get_classes(images_file) 11 | yield(classes) 12 | end 13 | # Do something with the image results 14 | tf.unlink 15 | end 16 | 17 | def get_classes(images_file) 18 | api_key = ENV["IMAGGA_KEY"] 19 | api_secret = ENV["IMAGGA_SECRET"] 20 | 21 | auth = 'Basic ' + Base64.strict_encode64( "#{api_key}:#{api_secret}" ).chomp 22 | response = RestClient.post "https://api.imagga.com/v2/uploads", { :image => images_file }, { :Authorization => auth } 23 | response = JSON.parse(response) 24 | p "Image uploda ID:", response["result"]["upload_id"] 25 | image_upload_id = response["result"]["upload_id"] 26 | 27 | response = RestClient.get "https://api.imagga.com/v2/tags?image_upload_id=#{image_upload_id}", { :Authorization => auth } 28 | response = JSON.parse(response) 29 | classes = response["result"]["tags"].map { |tag| tag["tag"]["en"]} 30 | p "Recognition result:", classes 31 | classes 32 | end -------------------------------------------------------------------------------- /weather_api.rb: -------------------------------------------------------------------------------- 1 | require 'geocoder' 2 | require 'date' 3 | require 'json' 4 | require 'open-uri' 5 | 6 | def fetch_weather(message) 7 | # Accepted message: 8 | # ~~~~~ weather in XXXXX 9 | # ^anything ^will become the location 10 | location = message.match(/.*eather in (\w+).*/)[1] 11 | 12 | # Coordinates from keyword 13 | coord = Geocoder.search(location).first.coordinates 14 | api_key = ENV["WEATHER_API"] 15 | url = "https://api.openweathermap.org/data/2.5/onecall?lat=#{coord[0]}&lon=#{coord[1]}&exclude=current,minutely,hourly&appid=#{api_key}" 16 | begin 17 | data_serialized = URI.open(url).read 18 | rescue OpenURI::HTTPError => e 19 | return { mostly: '', temps: '', report: 'No weather forecast for this city...' } 20 | end 21 | data = JSON.parse(data_serialized)['daily'][0..3] 22 | 23 | days = ['today', 'tomorrow', (Date.today + 2).strftime('%A'), (Date.today + 3).strftime('%A')] 24 | weather_forcast = data.map.with_index { |day, index| [days[index], day['weather'][0]['main'], day['temp']['day'] - 272.15] } 25 | freq = weather_forcast.map { |day| day[1] }.inject(Hash.new(0)) { |h, v| h[v] += 1; h } 26 | most_freq_weather = freq.max_by { |_k, v| v }[0] 27 | 28 | # Report creation 29 | report = "The weather is mostly #{most_freq_weather.upcase} in #{location} for the next 4 days.\n" 30 | # If there are particular weather days 31 | other_weathers = weather_forcast.reject { |day| day[1] == most_freq_weather} 32 | report += "Except on #{other_weathers.map { |day| "#{day[0]}(#{day[1]})" }.join(", ")}.\n" if other_weathers.any? 33 | # tempreatures 34 | report += "\nThe temperature will be:\n#{weather_forcast.map { |day| " #{day[2].round}˚C for #{day[0]}" }.join("\n")}" 35 | # Return the string from fore_cast data 36 | return report 37 | end 38 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.8.0) 5 | public_suffix (>= 2.0.2, < 5.0) 6 | celluloid (0.18.0) 7 | timers (~> 4) 8 | celluloid-io (0.17.3) 9 | celluloid (>= 0.17.2) 10 | nio4r (>= 1.1) 11 | timers (>= 4.1.1) 12 | daemons (1.4.1) 13 | domain_name (0.5.20190701) 14 | unf (>= 0.0.5, < 1.0.0) 15 | eventmachine (1.2.7) 16 | ffi (1.15.5) 17 | ffi-compiler (1.0.1) 18 | ffi (>= 1.0.0) 19 | rake 20 | geocoder (1.8.0) 21 | http (5.1.0) 22 | addressable (~> 2.8) 23 | http-cookie (~> 1.0) 24 | http-form_data (~> 2.2) 25 | llhttp-ffi (~> 0.4.0) 26 | http-accept (1.7.0) 27 | http-cookie (1.0.5) 28 | domain_name (~> 0.5) 29 | http-form_data (2.3.0) 30 | http_parser.rb (0.8.0) 31 | line-bot-api (1.18.0) 32 | llhttp-ffi (0.4.0) 33 | ffi-compiler (~> 1.0) 34 | rake (~> 13.0) 35 | mime-types (3.4.1) 36 | mime-types-data (~> 3.2015) 37 | mime-types-data (3.2022.0105) 38 | mustermann (2.0.2) 39 | ruby2_keywords (~> 0.0.1) 40 | netrc (0.11.0) 41 | nio4r (2.5.8) 42 | public_suffix (4.0.7) 43 | puma (5.6.4) 44 | nio4r (~> 2.0) 45 | rack (2.2.4) 46 | rack-protection (2.2.2) 47 | rack 48 | rake (13.0.6) 49 | reel (0.6.1) 50 | celluloid (>= 0.15.1) 51 | celluloid-io (>= 0.15.0) 52 | http (>= 0.6.0.pre) 53 | http_parser.rb (>= 0.6.0) 54 | websocket-driver (>= 0.5.1) 55 | rest-client (2.1.0) 56 | http-accept (>= 1.7.0, < 2.0) 57 | http-cookie (>= 1.0.2, < 2.0) 58 | mime-types (>= 1.16, < 4.0) 59 | netrc (~> 0.8) 60 | ruby2_keywords (0.0.5) 61 | sinatra (2.2.2) 62 | mustermann (~> 2.0) 63 | rack (~> 2.2) 64 | rack-protection (= 2.2.2) 65 | tilt (~> 2.0) 66 | thin (1.8.1) 67 | daemons (~> 1.0, >= 1.0.9) 68 | eventmachine (~> 1.0, >= 1.0.4) 69 | rack (>= 1, < 3) 70 | tilt (2.0.11) 71 | timers (4.3.3) 72 | unf (0.1.4) 73 | unf_ext 74 | unf_ext (0.0.8.2) 75 | webrick (1.7.0) 76 | websocket-driver (0.7.5) 77 | websocket-extensions (>= 0.1.0) 78 | websocket-extensions (0.1.5) 79 | 80 | PLATFORMS 81 | ruby 82 | 83 | DEPENDENCIES 84 | geocoder 85 | http 86 | line-bot-api (~> 1.18.0) 87 | puma 88 | reel 89 | rest-client 90 | sinatra 91 | thin 92 | webrick 93 | 94 | BUNDLED WITH 95 | 2.3.7 96 | -------------------------------------------------------------------------------- /app.rb: -------------------------------------------------------------------------------- 1 | # app.rb 2 | require 'sinatra' 3 | require 'json' 4 | require 'net/http' 5 | require 'uri' 6 | require 'tempfile' 7 | require 'line/bot' 8 | 9 | require_relative 'imagga' 10 | require_relative 'weather_api' 11 | require_relative 'tokyo_events_api' 12 | 13 | def client 14 | @client ||= Line::Bot::Client.new do |config| 15 | config.channel_secret = ENV['LINE_CHANNEL_SECRET'] 16 | config.channel_token = ENV['LINE_ACCESS_TOKEN'] 17 | end 18 | end 19 | 20 | def bot_answer_to(message, user_name) 21 | # If you want to add Bob to group chat, uncomment the next line 22 | # return '' unless message.downcase.include?('bob') # Only answer to messages with 'bob' 23 | 24 | if message.downcase.include?('hello') 25 | # respond if a user says hello 26 | "Hello #{user_name}, how are you doing today?" 27 | elsif message.downcase.include?('weather in') 28 | # call weather API in weather_api.rb 29 | fetch_weather(message) 30 | elsif message.downcase.include?('eat') 31 | ['sushi', 'tacos', 'curry', 'pad thai', 'kebab', 'spaghetti', 'burger'].sample 32 | elsif message.downcase.include?('events') 33 | # call events API in tokyo_events.rb 34 | fetch_tokyo_events 35 | elsif message.match?(/([\p{Hiragana}\p{Katakana}\p{Han}]+)/) 36 | # respond in japanese! 37 | bot_jp_answer_to(message, user_name) 38 | elsif message.end_with?('?') 39 | # respond if a user asks a question 40 | "Good question, #{user_name}!" 41 | else 42 | ["I couldn't agree more.", 'Great to hear that.', 'Interesting.'].sample 43 | end 44 | end 45 | 46 | def bot_jp_answer_to(message, user_name) 47 | if message.match?(/(おはよう|こんにちは|こんばんは|ヤッホー|ハロー).*/) 48 | "こんにちは#{user_name}さん!お元気ですか?" 49 | elsif message.match?(/.*元気.*(?|\?|か)/) 50 | "私は元気です、#{user_name}さん" 51 | elsif message.match?(/.*(le wagon|ワゴン|バゴン).*/i) 52 | "#{user_name}さん... もしかして京都のLE WAGONプログラミング学校の話ですかね? 素敵な画っこと思います!" 53 | elsif message.end_with?('?','?') 54 | "いい質問ですね、#{user_name}さん!" 55 | else 56 | ['そうですね!', '確かに!', '間違い無いですね!'].sample 57 | end 58 | end 59 | 60 | def send_bot_message(message, client, event) 61 | # Log prints for debugging 62 | p 'Bot message sent!' 63 | p event['replyToken'] 64 | p client 65 | 66 | message = { type: 'text', text: message } 67 | p message 68 | 69 | client.reply_message(event['replyToken'], message) 70 | 'OK' 71 | end 72 | 73 | get '/' do 74 | "Up and running!" 75 | end 76 | 77 | post '/callback' do 78 | body = request.body.read 79 | 80 | signature = request.env['HTTP_X_LINE_SIGNATURE'] 81 | unless client.validate_signature(body, signature) 82 | error 400 do 'Bad Request' end 83 | end 84 | 85 | events = client.parse_events_from(body) 86 | events.each do |event| 87 | p event 88 | # Focus on the message events (including text, image, emoji, vocal.. messages) 89 | next if event.class != Line::Bot::Event::Message 90 | 91 | case event.type 92 | # when receive a text message 93 | when Line::Bot::Event::MessageType::Text 94 | user_name = '' 95 | user_id = event['source']['userId'] 96 | response = client.get_profile(user_id) 97 | if response.class == Net::HTTPOK 98 | contact = JSON.parse(response.body) 99 | p contact 100 | user_name = contact['displayName'] 101 | else 102 | # Can't retrieve the contact info 103 | p "#{response.code} #{response.body}" 104 | end 105 | 106 | if event.message['text'].downcase == 'hello, world' 107 | # Sending a message when LINE tries to verify the webhook 108 | send_bot_message( 109 | 'Everything is working!', 110 | client, 111 | event 112 | ) 113 | else 114 | # The answer mechanism is here! 115 | send_bot_message( 116 | bot_answer_to(event.message['text'], user_name), 117 | client, 118 | event 119 | ) 120 | end 121 | # when receive an image message 122 | when Line::Bot::Event::MessageType::Image 123 | response_image = client.get_message_content(event.message['id']) 124 | fetch_imagga(response_image) do |image_results| 125 | # Sending the image results 126 | send_bot_message( 127 | "Looking at that picture, the first words that come to me are #{image_results[0..1].join(', ')} and #{image_results[2]}. Pretty good, eh?", 128 | client, 129 | event 130 | ) 131 | end 132 | end 133 | end 134 | 'OK' 135 | end 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Built on the shoulder of giants, especially https://github.com/hidehiro98/ 2 | 3 | # LINE Bot 101 4 | 5 | [日本語ドキュメント](README.ja.md) 6 | 7 | ## Line Bot example (scan the QR code with your Line app) 8 | ![qr code](https://github.com/YannKlein/bob-the-bot/blob/master/images/qrcode.png?raw=true) 9 | 10 | ## What we use 11 | - [LINE Messaging API](https://developers.line.me/en/docs/messaging-api/) 12 | - [Heroku](https://www.heroku.com) 13 | 14 | ## Slides 15 | - This covers the Line configuration, step by step. 16 | - [Slides Link](line_chatbot_v3.pdf) 17 | 18 | ## CAUTION! 19 | DO NOT INCLUDE 'line' in the name of provider and channel. 20 | If you do so, you cannnot create the provider nor the channel. 21 | 22 | ## Setup 23 | 24 | ### MacOS 25 | #### Install Heroku 26 | If you don't have brew, do this first 27 | ``` 28 | ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 29 | ``` 30 | Then do, 31 | ``` 32 | brew install heroku/brew/heroku 33 | ``` 34 | _Alternative option:_ install by dowloading [the Heroku Installer](https://devcenter.heroku.com/articles/heroku-cli#download-and-install). 35 | #### Install Git 36 | Download Git [here](https://git-scm.com/download/mac) and install it 37 | ### Ubuntu 38 | #### Install Heroku 39 | ``` 40 | sudo snap install --classic heroku 41 | ``` 42 | #### Install Git 43 | ``` 44 | apt-get install git 45 | ``` 46 | ### Windows 47 | #### Install Heroku 48 | Download [the Heroku Installer](https://devcenter.heroku.com/articles/heroku-cli#download-and-install) and install it. 49 | 50 | #### Install Git 51 | Download Git [here](https://git-scm.com/download/win) and install it 52 | 53 | ## Get the code up and running 54 | Open a terminal (search for "Terminal" (Mac, Ubuntu) or "Command Prompt" (Windows) in your OS search bar). 55 | 56 | *ANYTHING INSIDE OF [ ] NEEDS TO BE CHANGED* 57 | 58 | We will dowload the bot code from Github, then we will push it to Heroku: 59 | ``` 60 | git clone https://github.com/lewagonjapan/bob-the-bot.git [CHANGE_THIS_TO_YOUR_BOT_NAME] 61 | cd [CHANGE_THIS_TO_YOUR_BOT_NAME] 62 | heroku login 63 | heroku create [CHANGE_THIS_TO_YOUR_BOT_NAME] 64 | git push heroku master 65 | ``` 66 | We will configure the keys to access the LINE API service: 67 | 68 | Replace `[CHANGE_THIS_TO_YOUR_LINE_CHANNEL_SECRET]` and `[CHANGE_THIS_TO_YOUR_LINE_ACCESS_TOKEN]` with your own keys. 69 | ``` 70 | heroku config:set LINE_CHANNEL_SECRET=[CHANGE_THIS_TO_YOUR_LINE_CHANNEL_SECRET] 71 | heroku config:set LINE_ACCESS_TOKEN=[CHANGE_THIS_TO_YOUR_LINE_ACCESS_TOKEN] 72 | ``` 73 | 74 | _Example (do not copy/paste in your terminal):_ 75 | ``` 76 | heroku config:set LINE_CHANNEL_SECRET=f73d5df3fagu3g301856e1dc4cfcf3e1 77 | heroku config:set LINE_ACCESS_TOKEN=FbKBF7cB1HReh9lIc6M3bDz8Rd6D+0f1kvBaJF93QadC7SsGpHP9K1EOOYkbwRThXHdVSSupJ4TgKMEtE/LbnE2heif2GZci+ntGdP89cGfrbLiofFFBlrFygi58f/B5UsvqkvlfNM7BHddRZhhV2RgdB04t89/1O/w1cDnyilFU= 78 | ``` 79 | 80 | Optional: we will set the key for weather forecast and image recognition: 81 | 82 | Register and get the keys: 83 | - Weather forecast: https://home.openweathermap.org/api_keys 84 | - Image recognition: https://imagga.com/profile/dashboard 85 | 86 | ``` 87 | heroku config:set WEATHER_API=[CHANGE_THIS_TO_YOUR_WEATHER_API_KEY] 88 | heroku config:set IMAGGA_KEY=[CHANGE_THIS_TO_YOUR_IMAGGA_API_KEY] 89 | heroku config:set IMAGGA_SECRET=[CHANGE_THIS_TO_YOUR_IMAGGA_API_SECRET] 90 | ``` 91 | ## Ready to Upgrade? Making Changes to your Bot 92 | - Make your changes in your text editor 93 | - You can download [ Sublime Text](https://www.sublimetext.com/) or [VS code](https://code.visualstudio.com/) if you don't have one. 94 | - Commit your changes and send them to Heroku: 95 | ``` 96 | git add . 97 | git commit -m "DESCRIBE WHAT CHANGES YOU MADE" 98 | git push heroku master 99 | ``` 100 | - When it's finished pushing, message you bot to test it out! 101 | 102 | ## Have Errors? 103 | - In Terminal, you can run 104 | ``` 105 | heroku logs 106 | ``` 107 | - This will give you the server log. Big challenge to find that bug! 🐛 108 | 109 | ## Docs 110 | ### Docs of LINE Messagin API 111 | - https://developers.line.me/en/docs/messaging-api/building-sample-bot-with-heroku/ 112 | - https://github.com/line/line-bot-sdk-ruby 113 | 114 | ### Docs of Sinatra 115 | - https://devcenter.heroku.com/articles/rack#sinatra 116 | 117 | ### Docs of OpenWeather API 118 | - https://openweathermap.org/api 119 | 120 | ### Docs of Imagga image recognition 121 | - https://docs.imagga.com/?ruby#getting-started 122 | 123 | ## Contributors 124 | - [hidehiro98](https://github.com/hidehiro98/) 125 | - [yannklein](https://github.com/yannklein/) 126 | - [dmbf29](https://github.com/dmbf29/) 127 | --------------------------------------------------------------------------------