From 797df5c289b4c82142389e7582d49e49fccb33ad Mon Sep 17 00:00:00 2001 From: Guilherme Carreiro Date: Wed, 26 Sep 2018 00:23:23 -0300 Subject: [PATCH] Create Bot ;-). --- Gemfile | 6 +++- Gemfile.lock | 76 ++++++++++++++++++++++++++++++++++++++++++ LICENSE.txt | 20 +++++++++++ README.md | 46 +++++++++++++++++++++++++ adalberto.rb | 60 +++++++++++++++++++++++++++++++++ app.rb | 21 ++++++++++-- category_engine.rb | 45 +++++++++++++++++++++++++ config.ru | 2 +- finance_entry.rb | 38 +++++++++++++++++++++ finance_spreadsheet.rb | 25 ++++++++++++++ google_sheets_api.rb | 63 ++++++++++++++++++++++++++++++++++ message.rb | 51 ++++++++++++++++++++++++++++ telegram.rb | 12 +++++++ 13 files changed, 461 insertions(+), 4 deletions(-) create mode 100644 Gemfile.lock create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 adalberto.rb create mode 100644 category_engine.rb create mode 100644 finance_entry.rb create mode 100644 finance_spreadsheet.rb create mode 100644 google_sheets_api.rb create mode 100644 message.rb create mode 100644 telegram.rb diff --git a/Gemfile b/Gemfile index c6fbb05..80f7aae 100644 --- a/Gemfile +++ b/Gemfile @@ -1,2 +1,6 @@ +ruby '2.4.4' + source 'https://rubygems.org' -gem 'sinatra' +gem 'sinatra', '2.0.2' +gem 'httparty' +gem 'google-api-client' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..de67f03 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,76 @@ +GEM + remote: https://rubygems.org/ + specs: + addressable (2.5.2) + public_suffix (>= 2.0.2, < 4.0) + declarative (0.0.10) + declarative-option (0.1.0) + faraday (0.13.1) + multipart-post (>= 1.2, < 3) + google-api-client (0.19.6) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.5, < 0.7.0) + httpclient (>= 2.8.1, < 3.0) + mime-types (~> 3.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.0) + googleauth (0.6.2) + faraday (~> 0.12) + jwt (>= 1.4, < 3.0) + logging (~> 2.0) + memoist (~> 0.12) + multi_json (~> 1.11) + os (~> 0.9) + signet (~> 0.7) + httparty (0.15.7) + multi_xml (>= 0.5.2) + httpclient (2.8.3) + jwt (2.1.0) + little-plugger (1.1.4) + logging (2.2.2) + little-plugger (~> 1.1) + multi_json (~> 1.10) + memoist (0.16.0) + mime-types (3.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2016.0521) + multi_json (1.12.1) + multi_xml (0.6.0) + multipart-post (2.0.0) + mustermann (1.0.3) + os (0.9.6) + public_suffix (2.0.5) + rack (2.0.5) + rack-protection (2.0.2) + rack + representable (3.0.4) + declarative (< 0.1.0) + declarative-option (< 0.2.0) + uber (< 0.2.0) + retriable (3.1.1) + signet (0.8.1) + addressable (~> 2.3) + faraday (~> 0.9) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + sinatra (2.0.2) + mustermann (~> 1.0) + rack (~> 2.0) + rack-protection (= 2.0.2) + tilt (~> 2.0) + tilt (2.0.8) + uber (0.1.0) + +PLATFORMS + ruby + +DEPENDENCIES + google-api-client + httparty + sinatra (= 2.0.2) + +RUBY VERSION + ruby 2.4.4p296 + +BUNDLED WITH + 1.16.5 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..e3dbacb --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,20 @@ +Copyright JS Foundation and other contributors, https://js.foundation/ + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e67d7d8 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# Arnaldo Bot + +A simple bot for helping you with your financial life, read more [here](http://karreiro.com/2018/09/25/financial-bot.html). + + +## Demo +[![Demo](http://karreiro.com/assets/demo-bot.gif "Demo")](http://karreiro.com/assets/demo-bot.gif) + + +## Configuring + +Configure an instance of the bot by following the next 3 simple steps: + +### 1) Telegram environment variables + +First of all, configure the **TELEGRAM_TOKEN** and the **TELEGRAM_ID** environment variables. + +You'll need to create a bot on Telegram, by adding the *BotFather* and following the instructions there. He will help you with everything and, in the end, you will get the bot token. + +Considering that the token is something like `"111111111:AAAAaa11aaaaAA1aAAaAA1AAaAaaaAaaaAA"`, the **TELEGRAM_TOKEN** value must be `"bot111111111:AAAAaa11aaaaAA1aAAaAA1AAaAaaaAaaaAA"` (noticed that it's necessary to add the word _bot_ in the beginning). + +For the **TELEGRAM_ID**, variable, which your user id, you can ask for it to the @jsondumpbot ;-) + + +### 2) Google API environment variables + +The variables configured in this steps are: **GOOGLE_ACCOUNT_TYPE**, **GOOGLE_CLIENT_EMAIL** and **GOOGLE_PRIVATE_KEY**. + +First, go to [Google Developers Console](https://console.developers.google.com/project), click the Library section, enable Google Sheets API, and finally, in the Service account section, create a service account without roles, with the "G Suite Domain-wide Delegation" enabled. + +Finally, click on the "Create key" button and Download the JSON file with the three variables that we need. + + +### 3) The spreadsheet + +Create a spreadsheet on Google Drive. The URL is something like this `https://docs.google.com/spreadsheets/d/AAAAAA/edit`, and the `AAAAAA` is the **SPREADSHEET_ID** that we're looking for. + +Finally, share the spreadsheet with the e-mail from the **GOOGLE_CLIENT_EMAIL** variable, and boom! It's done :-) + + +## Contributing + 1. Fork it + 2. Create your feature branch (`git checkout -b my-new-feature`) + 3. Commit your changes (`git commit -am 'Add some feature'`) + 4. Push to the branch (`git push origin my-new-feature`) + 5. Create a new Pull Request diff --git a/adalberto.rb b/adalberto.rb new file mode 100644 index 0000000..59a6f27 --- /dev/null +++ b/adalberto.rb @@ -0,0 +1,60 @@ +class Adalberto + + attr_reader :message + + def initialize(message) + @message = message + end + + def execute! + return unless message.valid? + return unless message.valid_sender? + + if message.expense? + say append_expense(message) + else + say excuse(message) + end + end + + private + + def append_expense(message) + finance_spreadsheet.append_expense finance_entry(message) + success + rescue + 'Oops... Something went wrong! ☚ī¸' + end + + def success + [ + 'Ok... done! 😉', + 'Done! 😁', + 'Ok, ok... done 😒', + ].sample + end + + def finance_entry(message) + FinanceEntry.new(message) + end + + def excuse(message) + [ + 'I\'m not sure if I can help you right now.. 🧐', + 'IDK.. 🤔', + 'Hmmmmmmmmmm.. đŸ˜ļ', + ].sample + end + + def say(message) + telegram.send(message) + end + + def finance_spreadsheet + @finance_spreadsheet ||= FinanceSpreadsheet.new + end + + def telegram + @telegram = Telegram.new + end +end diff --git a/app.rb b/app.rb index f01822f..c49ff55 100644 --- a/app.rb +++ b/app.rb @@ -1,5 +1,22 @@ require 'sinatra' +require 'google/apis/sheets_v4' +require 'httparty' +require 'cgi' +require './adalberto' +require './category_engine' +require './finance_entry' +require './finance_spreadsheet' +require './google_sheets_api' +require './message' +require './telegram' -get '/' do - "Hello World!" +post '/' do + + json = JSON.parse(request.body.read) + message = Message.new(json) + adalberto = Adalberto.new(message) + + adalberto.execute! + + status 200 end diff --git a/category_engine.rb b/category_engine.rb new file mode 100644 index 0000000..4332102 --- /dev/null +++ b/category_engine.rb @@ -0,0 +1,45 @@ +class CategoryEngine + + attr_reader :message + + def initialize(message) + @message = message + end + + def category + return 'entertainment' if entertainment? + return 'market' if market? + return 'dinner' if dinner? + 'other' + end + + private + + def entertainment? + keywords = %w(beer party) + + keywords.any? do |keyword| + raw_message.include?(keyword) + end + end + + def market? + keywords = %w(market shopping) + + keywords.any? do |keyword| + raw_message.include?(keyword) + end + end + + def dinner? + keywords = %w(food dinner) + + keywords.any? do |keyword| + raw_message.include?(keyword) + end + end + + def raw_message + message.text.downcase + end +end diff --git a/config.ru b/config.ru index c4b5f84..76a6edf 100644 --- a/config.ru +++ b/config.ru @@ -1,2 +1,2 @@ -require './hello' +require './app' run Sinatra::Application diff --git a/finance_entry.rb b/finance_entry.rb new file mode 100644 index 0000000..b4fcbd0 --- /dev/null +++ b/finance_entry.rb @@ -0,0 +1,38 @@ +class FinanceEntry + + attr_reader :message + + def initialize(message) + @message = message + end + + def raw + [[id, date, value, category, raw_message]] + end + + private + + def id + message.id + end + + def date + message.date + end + + def value + message.expense_value + end + + def category + category_engine.category + end + + def category_engine + CategoryEngine.new(message) + end + + def raw_message + message.text + end +end diff --git a/finance_spreadsheet.rb b/finance_spreadsheet.rb new file mode 100644 index 0000000..a230c2d --- /dev/null +++ b/finance_spreadsheet.rb @@ -0,0 +1,25 @@ +class FinanceSpreadsheet + + attr_reader :google_sheets_api + + def initialize + @google_sheets_api = GoogleSheetsApi.new(ENV['SPREADSHEET_ID']) + end + + def append_expense(finance_entry) + append_data(finance_entry) + rescue + create_sheet(finance_entry) + end + + private + + def append_data(finance_entry) + google_sheets_api.append_data(finance_entry.raw) + end + + def create_sheet(finance_entry) + google_sheets_api.create_sheet + google_sheets_api.append_data(finance_entry.raw) + end +end diff --git a/google_sheets_api.rb b/google_sheets_api.rb new file mode 100644 index 0000000..4c2891a --- /dev/null +++ b/google_sheets_api.rb @@ -0,0 +1,63 @@ +class GoogleSheetsApi + + attr_reader :spreadsheet_id + + def initialize(spreadsheet_id) + @spreadsheet_id = spreadsheet_id + end + + def append_data(values) + + range = "#{sheet_name}!A1" + range_value = Google::Apis::SheetsV4::ValueRange.new(values: values) + + service.append_spreadsheet_value(spreadsheet_id, + range, + range_value, + value_input_option: 'USER_ENTERED') + end + + def create_sheet + service.batch_update_spreadsheet(spreadsheet_id, batch_update_request, {}) + end + + private + + def batch_update_request + + gray = { "red": 0.8, "green": 0.8, "blue": 0.8 } + three_columns = { "row_count": 1, "column_count": 5 } + + { + requests: [ + { + add_sheet: { + properties: { + "title": sheet_name, + "grid_properties": three_columns, + "tab_color": gray + } + } + } + ] + } + end + + def sheet_name + Date.today.strftime("%m%Y") + end + + def service + @service ||= Google::Apis::SheetsV4::SheetsService.new.tap do |sheets_service| + sheets_service.authorization = authorization + end + end + + def authorization + Google::Auth.get_application_default(auth_spreadsheets) + end + + def auth_spreadsheets + Google::Apis::SheetsV4::AUTH_SPREADSHEETS + end +end diff --git a/message.rb b/message.rb new file mode 100644 index 0000000..096992f --- /dev/null +++ b/message.rb @@ -0,0 +1,51 @@ +class Message + + attr_reader :json + + def initialize(json) + @json = json + end + + def valid_sender? + sender_id.to_i == ENV['TELEGRAM_ID'].to_i + end + + def valid? + !!message + end + + def expense? + /.*([Ss]pent).*\d.*/ =~ text + end + + def id + message['message_id'] + end + + def date + Time.at(message['date']).strftime("%d/%m/%Y") + end + + def expense_value + matches = text.match /(?\d{1,}.\d{0,2})/ + matches[:value].to_f if matches + end + + def sender_id + from['id'] if from + end + + def text + message['text'] if message + end + + private + + def from + message['from'] if message + end + + def message + json['message'] if json + end +end diff --git a/telegram.rb b/telegram.rb new file mode 100644 index 0000000..a5385ff --- /dev/null +++ b/telegram.rb @@ -0,0 +1,12 @@ +class Telegram + def send(message) + HTTParty.get url(message) + end + + private + + def url(message) + escaped_message = CGI::escape(message.to_s) + "https://api.telegram.org/#{ENV['TELEGRAM_TOKEN']}/sendMessage?chat_id=#{ENV['TELEGRAM_ID']}&text=#{escaped_message}" + end +end