From 30e64631627168f14dfa1c98139e951178524946 Mon Sep 17 00:00:00 2001 From: Snezhnyy Date: Mon, 16 Apr 2018 23:21:44 +0300 Subject: [PATCH] Add homework 2, telegram bot. --- 2018/AnatoliDobshikov/2/.gitignore | 2 + 2018/AnatoliDobshikov/2/Gemfile | 12 +++ 2018/AnatoliDobshikov/2/Gemfile.lock | 68 +++++++++++++++ .../2/lib/CommandController.rb | 44 ++++++++++ 2018/AnatoliDobshikov/2/lib/HelpCommand.rb | 14 ++++ 2018/AnatoliDobshikov/2/lib/HistoryCommand.rb | 27 ++++++ .../2/lib/RepoCommandExtension.rb | 18 ++++ 2018/AnatoliDobshikov/2/lib/SearchCommand.rb | 83 +++++++++++++++++++ 2018/AnatoliDobshikov/2/lib/SetRepoCommand.rb | 27 ++++++ .../AnatoliDobshikov/2/lib/ShowRepoCommand.rb | 9 ++ 2018/AnatoliDobshikov/2/lib/StartCommand.rb | 9 ++ 2018/AnatoliDobshikov/2/lib/TeleBotCommand.rb | 14 ++++ 2018/AnatoliDobshikov/2/lib/model/Request.rb | 7 ++ 2018/AnatoliDobshikov/2/lib/model/User.rb | 7 ++ 2018/AnatoliDobshikov/2/migrations/Request.rb | 38 +++++++++ 2018/AnatoliDobshikov/2/migrations/User.rb | 33 ++++++++ 2018/AnatoliDobshikov/2/run_bot.rb | 33 ++++++++ 17 files changed, 445 insertions(+) create mode 100644 2018/AnatoliDobshikov/2/.gitignore create mode 100644 2018/AnatoliDobshikov/2/Gemfile create mode 100644 2018/AnatoliDobshikov/2/Gemfile.lock create mode 100644 2018/AnatoliDobshikov/2/lib/CommandController.rb create mode 100644 2018/AnatoliDobshikov/2/lib/HelpCommand.rb create mode 100644 2018/AnatoliDobshikov/2/lib/HistoryCommand.rb create mode 100644 2018/AnatoliDobshikov/2/lib/RepoCommandExtension.rb create mode 100644 2018/AnatoliDobshikov/2/lib/SearchCommand.rb create mode 100644 2018/AnatoliDobshikov/2/lib/SetRepoCommand.rb create mode 100644 2018/AnatoliDobshikov/2/lib/ShowRepoCommand.rb create mode 100644 2018/AnatoliDobshikov/2/lib/StartCommand.rb create mode 100644 2018/AnatoliDobshikov/2/lib/TeleBotCommand.rb create mode 100644 2018/AnatoliDobshikov/2/lib/model/Request.rb create mode 100644 2018/AnatoliDobshikov/2/lib/model/User.rb create mode 100644 2018/AnatoliDobshikov/2/migrations/Request.rb create mode 100644 2018/AnatoliDobshikov/2/migrations/User.rb create mode 100644 2018/AnatoliDobshikov/2/run_bot.rb diff --git a/2018/AnatoliDobshikov/2/.gitignore b/2018/AnatoliDobshikov/2/.gitignore new file mode 100644 index 000000000..a65b379f3 --- /dev/null +++ b/2018/AnatoliDobshikov/2/.gitignore @@ -0,0 +1,2 @@ +passport.yml +octotest.rb diff --git a/2018/AnatoliDobshikov/2/Gemfile b/2018/AnatoliDobshikov/2/Gemfile new file mode 100644 index 000000000..02086e459 --- /dev/null +++ b/2018/AnatoliDobshikov/2/Gemfile @@ -0,0 +1,12 @@ +source "https://rubygems.org" + +# bot api +gem 'telegram-bot-ruby' +# for easier classes requiring +gem 'require_all' +# GitHub api +gem 'octokit' +# postgresql gem +gem 'pg' +# ORM adapter +gem 'activerecord' diff --git a/2018/AnatoliDobshikov/2/Gemfile.lock b/2018/AnatoliDobshikov/2/Gemfile.lock new file mode 100644 index 000000000..a27e6d37d --- /dev/null +++ b/2018/AnatoliDobshikov/2/Gemfile.lock @@ -0,0 +1,68 @@ +GEM + remote: https://rubygems.org/ + specs: + activemodel (5.2.0) + activesupport (= 5.2.0) + activerecord (5.2.0) + activemodel (= 5.2.0) + activesupport (= 5.2.0) + arel (>= 9.0) + activesupport (5.2.0) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 0.7, < 2) + minitest (~> 5.1) + tzinfo (~> 1.1) + addressable (2.5.2) + public_suffix (>= 2.0.2, < 4.0) + arel (9.0.0) + axiom-types (0.1.1) + descendants_tracker (~> 0.0.4) + ice_nine (~> 0.11.0) + thread_safe (~> 0.3, >= 0.3.1) + coercible (1.0.0) + descendants_tracker (~> 0.0.1) + concurrent-ruby (1.0.5) + descendants_tracker (0.0.4) + thread_safe (~> 0.3, >= 0.3.1) + equalizer (0.0.11) + faraday (0.14.0) + multipart-post (>= 1.2, < 3) + i18n (1.0.0) + concurrent-ruby (~> 1.0) + ice_nine (0.11.2) + inflecto (0.0.2) + minitest (5.11.3) + multipart-post (2.0.0) + octokit (4.8.0) + sawyer (~> 0.8.0, >= 0.5.3) + pg (1.0.0) + public_suffix (3.0.2) + require_all (2.0.0) + sawyer (0.8.1) + addressable (>= 2.3.5, < 2.6) + faraday (~> 0.8, < 1.0) + telegram-bot-ruby (0.8.6.1) + faraday + inflecto + virtus + thread_safe (0.3.6) + tzinfo (1.2.5) + thread_safe (~> 0.1) + virtus (1.0.5) + axiom-types (~> 0.1) + coercible (~> 1.0) + descendants_tracker (~> 0.0, >= 0.0.3) + equalizer (~> 0.0, >= 0.0.9) + +PLATFORMS + ruby + +DEPENDENCIES + activerecord + octokit + pg + require_all + telegram-bot-ruby + +BUNDLED WITH + 1.16.1 diff --git a/2018/AnatoliDobshikov/2/lib/CommandController.rb b/2018/AnatoliDobshikov/2/lib/CommandController.rb new file mode 100644 index 000000000..0a5a83edb --- /dev/null +++ b/2018/AnatoliDobshikov/2/lib/CommandController.rb @@ -0,0 +1,44 @@ +# here is the main logic +# here we decide what to response +require 'telegram/bot' +require 'pg' +require 'active_record' +require_relative 'model/User.rb' + +class CommandController + # switch message and find exact answer + def self.handler(message = Telegram::Bot::Types::Message.new) + # trying to find a user in database + user = User.find_by(id: message.from.id) + # add user to database if he is not already in it + if user.nil? + user = User.create(id: message.from.id, current_repo: 'none', last_command: 'none') + end + # use the bot's memory to make complicated requests + text = user.last_command == 'none' ? message.text : user.last_command + # chose the suitable answer to the /question + case text + # first contact with the user))) + when '/start' + StartCommand.new(message) + # show help + when '/help' + HelpCommand.new(message) + # set new repo adress + when '/set_repo' + SetRepoCommand.new(message, user) + # show user's new repository + when '/show_repo' + ShowRepoCommand.new(message, user) + # search in commits + when '/search' + SearchCommand.new(message, user) + # view search history + when '/history' + HistoryCommand.new(message) + # default answer for unknown commands + else + TeleBotCommand.new() + end + end +end diff --git a/2018/AnatoliDobshikov/2/lib/HelpCommand.rb b/2018/AnatoliDobshikov/2/lib/HelpCommand.rb new file mode 100644 index 000000000..560e22f23 --- /dev/null +++ b/2018/AnatoliDobshikov/2/lib/HelpCommand.rb @@ -0,0 +1,14 @@ +require 'telegram/bot' +require_relative 'TeleBotCommand' +# It shows info about the bot +class HelpCommand < TeleBotCommand + def reply + "Available commands list: + /help - view help; + /set_repo - set the repository to search; + /show_repo - show current repository address; + /search - search in the current repository, while searching type /1 to get the first page or /3 to get the third page, type /ok to end searching; + /history - print your search queries. + !Warning - search works only with the first 30 results! Sorry for this((((" + end +end diff --git a/2018/AnatoliDobshikov/2/lib/HistoryCommand.rb b/2018/AnatoliDobshikov/2/lib/HistoryCommand.rb new file mode 100644 index 000000000..767af0612 --- /dev/null +++ b/2018/AnatoliDobshikov/2/lib/HistoryCommand.rb @@ -0,0 +1,27 @@ +# query history +require 'pg' +require 'active_record' +require 'telegram/bot' +require_relative 'TeleBotCommand' +require_relative 'model/Request' + +class HistoryCommand < TeleBotCommand + def reply + # load all requests for current user + requests = Request.where(user_id: @message.from.id) + # if user haven't got queries YET + if requests.length == 0 + return "You haven't got any queries. \nType /help to see the commands list." + end + # form history + history = "History of #{@message.from.first_name} #{@message.from.last_name}.\n" + requests.length.times do |index| + history += "#{requests[index].updated_at}: searching for #{requests[index].query} in #{requests[index].repository} repository.\n" + end + # cut VERY long history + if history.length > 4090 + history = "..." + history.slice((history.length - 4090)..(history.length - 1)) + end + history + end +end diff --git a/2018/AnatoliDobshikov/2/lib/RepoCommandExtension.rb b/2018/AnatoliDobshikov/2/lib/RepoCommandExtension.rb new file mode 100644 index 000000000..2796a7afd --- /dev/null +++ b/2018/AnatoliDobshikov/2/lib/RepoCommandExtension.rb @@ -0,0 +1,18 @@ +require 'octokit' +require_relative 'TeleBotCommand' +# safe repository setter +class RepoCommandExtension < TeleBotCommand + def initialize_repo(address = String.new) + begin + @repo = Octokit.repo(address) + rescue Octokit::NotFound + 'Cannot find this repository.' + rescue Octokit::RepositoryUnavailable + 'Access denied.' + rescue Octokit::InvalidRepository + 'Invalid repository.' + rescue + 'Unknown error.' + end + end +end diff --git a/2018/AnatoliDobshikov/2/lib/SearchCommand.rb b/2018/AnatoliDobshikov/2/lib/SearchCommand.rb new file mode 100644 index 000000000..449a3f609 --- /dev/null +++ b/2018/AnatoliDobshikov/2/lib/SearchCommand.rb @@ -0,0 +1,83 @@ +# Here we will search for something in somewhere and we would hope that we can find something really cool +require 'pg' +require 'active_record' +require 'json' +require_relative 'model/Request' +require_relative 'RepoCommandExtension' + +class SearchCommand < RepoCommandExtension + def reply + # check if repo is available + error_message = initialize_repo(@user.current_repo) + if @repo.nil? + return "#{error_message}\nPlease, set the available repository." + end + # ask to input search query + unless @user.last_command == '/search' + @user.update(last_command: '/search') + return 'Input search query:' + end + # listing the results + @request = Request.find_by(user_id: @user.id, active: true) + # if user has active requests + unless @request.nil? + # end dialog + if @message.text == '/ok' + @user.update(last_command: 'none') + @request.update(active: false) + return 'I hope you found something useful.' + end + # page listing + @search_results = search(@request[:query]) + page_number = @message.text.delete('/').to_i + case page_number + when 1..(@search_results['items'].length / 3) + return print_page(page_number) + else + return "Cannot find this page. Type '/ok' to end search. Type '/{page_number}' to view page(/5 for example)." + end + end + # process new query + @search_results = search(@message.text) + # add request to DB + @request = Request.create(query: @message.text, repository: @user.current_repo, user_id: @user.id, active: true) + return "I have found #{@search_results['total_count']} result(s).\n#{print_page(1)}" + end + + private + # create query for http GET + def create_query(query_text = String.new) + "repo:#{@user.current_repo}+#{query_text.split(' ').join('+')}" + end + # search in github commits + def search(query_text = String.new) + query = create_query(query_text) + # search via github api + search_text = %x`curl -H 'Accept: application/vnd.github.cloak-preview+json' \ + -i https://api.github.com/search/commits?q=#{query}` + # select JSON from response + search_text = search_text.slice(search_text.index('{')..(search_text.length - 1)) + JSON.parse(search_text) + end + # list the search results + def print_page(page_number = 1) + # header + page = "Searching for #{@request.query} in #{@request.repository}.\n" + page += "Page /#{page_number} of /#{@search_results['items'].length / 3} (three items per page):\n#{'=' * 30}\n" + item_number = (page_number - 1) * 3 + 1 + # body XD + 3.times do + if item_number > @search_results['items'].length + return "#{page}\nThis is the last page. Type /ok to end search dialog, if you want.\n#{'=' * 30}\n" + end + commit_message = (@search_results['items'][item_number - 1]['commit']['message']).split("\n").join('. ') + # cut long messages + if commit_message.length > 100 + commit_message = commit_message.slice(0..96) + '...' + end + page += "#{item_number}. Link: #{@search_results['items'][item_number - 1]['html_url']}\nMessage: #{commit_message}\n#{'=' * 30}\n" + item_number += 1 + end + page + end +end diff --git a/2018/AnatoliDobshikov/2/lib/SetRepoCommand.rb b/2018/AnatoliDobshikov/2/lib/SetRepoCommand.rb new file mode 100644 index 000000000..9700c1fce --- /dev/null +++ b/2018/AnatoliDobshikov/2/lib/SetRepoCommand.rb @@ -0,0 +1,27 @@ +require 'telegram/bot' +require_relative 'RepoCommandExtension' +require 'pg' +require 'active_record' +require_relative 'model/User.rb' +require 'octokit' +# set the repository to work with it +class SetRepoCommand < RepoCommandExtension + def reply + # if user is asked to set new adress of the repository + if @user.last_command != '/set_repo' + @user.update(last_command: '/set_repo') + "Please, input repository address ('rubyroidlabs/bsuir-courses' for example):" + # if user send his adress of the repository + else + # rescue errors + error_message = initialize_repo(@message.text) + unless @repo.nil? + @user.update(current_repo: @message.text, last_command: 'none') + return "#{@message.text} repository have set." + else + @user.update(last_command: 'none') + return error_message + end + end + end +end diff --git a/2018/AnatoliDobshikov/2/lib/ShowRepoCommand.rb b/2018/AnatoliDobshikov/2/lib/ShowRepoCommand.rb new file mode 100644 index 000000000..cdfcbc0a3 --- /dev/null +++ b/2018/AnatoliDobshikov/2/lib/ShowRepoCommand.rb @@ -0,0 +1,9 @@ +# here we will show to User his current repository +require 'telegram/bot' +require_relative 'TeleBotCommand' +# command class +class ShowRepoCommand < TeleBotCommand + def reply + "Current repository is #{@user.current_repo}." + end +end diff --git a/2018/AnatoliDobshikov/2/lib/StartCommand.rb b/2018/AnatoliDobshikov/2/lib/StartCommand.rb new file mode 100644 index 000000000..2c0f5a34f --- /dev/null +++ b/2018/AnatoliDobshikov/2/lib/StartCommand.rb @@ -0,0 +1,9 @@ +require 'telegram/bot' +require_relative 'TeleBotCommand' +# Cindy introduce itself in this class +class StartCommand < TeleBotCommand + # introducing and giving the additional information about Cindy + def reply + return "Hello, #{@message.from.first_name}.\nMy name is Cindy. I can help you to work with the GitHub.\nYou can type /help to see what I exactly can do." + end +end diff --git a/2018/AnatoliDobshikov/2/lib/TeleBotCommand.rb b/2018/AnatoliDobshikov/2/lib/TeleBotCommand.rb new file mode 100644 index 000000000..f4b3d3a2a --- /dev/null +++ b/2018/AnatoliDobshikov/2/lib/TeleBotCommand.rb @@ -0,0 +1,14 @@ +require 'telegram/bot' +require_relative 'model/User.rb' +# super class for all commands +class TeleBotCommand + # constructor + def initialize(message = Telegram::Bot::Types::Message.new(), user = User.new) + @message = message + @user = user + end + # here is the answer to wrong request + def reply + "I'm sorry I do not understand you.\nTry to type /help to see what I can understand." + end +end diff --git a/2018/AnatoliDobshikov/2/lib/model/Request.rb b/2018/AnatoliDobshikov/2/lib/model/Request.rb new file mode 100644 index 000000000..c46ac2df0 --- /dev/null +++ b/2018/AnatoliDobshikov/2/lib/model/Request.rb @@ -0,0 +1,7 @@ +require 'pg' +require 'active_record' + +class Request < ActiveRecord::Base + # owner of the requests history + belongs_to :user +end diff --git a/2018/AnatoliDobshikov/2/lib/model/User.rb b/2018/AnatoliDobshikov/2/lib/model/User.rb new file mode 100644 index 000000000..fa4e3cbb5 --- /dev/null +++ b/2018/AnatoliDobshikov/2/lib/model/User.rb @@ -0,0 +1,7 @@ +require 'pg' +require 'active_record' + +class User < ActiveRecord::Base + # requests history + has_many :requests, dependent: :destroy +end diff --git a/2018/AnatoliDobshikov/2/migrations/Request.rb b/2018/AnatoliDobshikov/2/migrations/Request.rb new file mode 100644 index 000000000..f7d3881b9 --- /dev/null +++ b/2018/AnatoliDobshikov/2/migrations/Request.rb @@ -0,0 +1,38 @@ +# here is the requests tablesheet migration +require 'pg' +require 'active_record' +require 'pry' + +PASSPORT = YAML.load(File.read('../passport.yml')) +ActiveRecord::Base.logger = Logger.new(STDOUT) +ActiveRecord::Base::establish_connection( + adapter: 'postgresql', + host: '', + user: PASSPORT['user'], + database: PASSPORT['database'] +) + +class CreateRequests < ActiveRecord::Migration[5.0] + def up + create_table :requests do |t| + # owner of the request + t.references :user, foreign_key: true, index: true + # text of the request + t.string :query + # repository address + t.string :repository + # is active + t.boolean :active + # when we had do it + t.timestamps + end + end + + def down + drop_table :requests + end +end + +create = CreateRequests.new + +binding.pry diff --git a/2018/AnatoliDobshikov/2/migrations/User.rb b/2018/AnatoliDobshikov/2/migrations/User.rb new file mode 100644 index 000000000..0fdf63b6b --- /dev/null +++ b/2018/AnatoliDobshikov/2/migrations/User.rb @@ -0,0 +1,33 @@ +# here is the user tablesheet migration +require 'pg' +require 'active_record' +require 'pry' + +PASSPORT = YAML.load(File.read('../passport.yml')) +ActiveRecord::Base.logger = Logger.new(STDOUT) +ActiveRecord::Base::establish_connection( + adapter: 'postgresql', + host: '', + user: PASSPORT['user'], + database: PASSPORT['database'] +) + +# this table contain information about User +class CreateUsers < ActiveRecord::Migration[5.0] + def up + create_table :users do |t| + # adress of the current_repo + t.string :current_repo + # last command + t.string :last_command + end + end + + def down + drop_table :users + end +end + +create = CreateUsers.new + +binding.pry diff --git a/2018/AnatoliDobshikov/2/run_bot.rb b/2018/AnatoliDobshikov/2/run_bot.rb new file mode 100644 index 000000000..70637c00b --- /dev/null +++ b/2018/AnatoliDobshikov/2/run_bot.rb @@ -0,0 +1,33 @@ +require 'telegram/bot' +require 'require_all' +require 'pg' +require 'active_record' +require 'yaml' +require_all 'lib' +# load passport with secret and personal information +PASSPORT = YAML.load(File.read('passport.yml')) +# ORM setup +ActiveRecord::Base::establish_connection( + adapter: 'postgresql', + host: '', + user: PASSPORT['user'], + database: PASSPORT['database'] +) +# log all database actions +ActiveRecord::Base.logger = Logger.new(STDOUT) +# bot token setup +TOKEN = PASSPORT['bot_token'] +# run bot +Telegram::Bot::Client.run(TOKEN, logger: Logger.new($stderr)) do |bot| + # run messages listener + bot.listen do |message| + # set the request handler + command = CommandController.handler(message) + # sending the answer to the request + begin + bot.api.send_message(chat_id: message.chat.id, text: command.reply) + rescue Telegram::Bot::Exceptions::ResponseError + bot.api.send_message(chat_id: message.chat.id, text: "Something goes wrong. It is very strange. Maybe it will work later. Maybe not.\nError 400: Response Error.") + end + end +end