diff --git a/Aleksandrova_Vilena/3/bot/.gitignore b/Aleksandrova_Vilena/3/bot/.gitignore new file mode 100644 index 00000000..edb71371 --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/.gitignore @@ -0,0 +1,3 @@ +.idea/ +/.env +lib/ruby/ \ No newline at end of file diff --git a/Aleksandrova_Vilena/3/bot/.rspec b/Aleksandrova_Vilena/3/bot/.rspec new file mode 100644 index 00000000..c99d2e73 --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/Aleksandrova_Vilena/3/bot/Gemfile b/Aleksandrova_Vilena/3/bot/Gemfile new file mode 100644 index 00000000..f6afab75 --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/Gemfile @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } + +# gem "rails" +gem 'activerecord' +gem 'dotenv' # for using env. variables +gem 'multi_json' +gem 'nanoc', '~> 4.12' +gem 'pry', '~> 0.14.1' +gem 'puma' +gem 'rake', '~> 13.0.3' +gem 'recursive-open-struct', '~> 1.1.3' +gem 'robocop' +gem 'rspec' # for running tests +gem 'sinatra' +gem 'sinatra-activerecord', '~> 2.0' +gem 'singleton' +gem 'sqlite3' # db for devtesting +gem 'telegram-bot-ruby' +gem 'whenever', require: false +gem 'whenever-test' diff --git a/Aleksandrova_Vilena/3/bot/Gemfile.lock b/Aleksandrova_Vilena/3/bot/Gemfile.lock new file mode 100644 index 00000000..2259d9b1 --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/Gemfile.lock @@ -0,0 +1,182 @@ +GEM + remote: https://rubygems.org/ + specs: + activemodel (6.1.3.2) + activesupport (= 6.1.3.2) + activerecord (6.1.3.2) + activemodel (= 6.1.3.2) + activesupport (= 6.1.3.2) + activesupport (6.1.3.2) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 1.6, < 2) + minitest (>= 5.1) + tzinfo (~> 2.0) + zeitwerk (~> 2.3) + addressable (2.7.0) + public_suffix (>= 2.0.2, < 5.0) + axiom-types (0.1.1) + descendants_tracker (~> 0.0.4) + ice_nine (~> 0.11.0) + thread_safe (~> 0.3, >= 0.3.1) + chronic (0.10.2) + coderay (1.1.3) + coercible (1.0.0) + descendants_tracker (~> 0.0.1) + colored (1.2) + concurrent-ruby (1.1.8) + cri (2.15.11) + ddmemoize (1.0.0) + ddmetrics (~> 1.0) + ref (~> 2.0) + ddmetrics (1.0.1) + ddplugin (1.0.3) + descendants_tracker (0.0.4) + thread_safe (~> 0.3, >= 0.3.1) + diff-lcs (1.4.4) + dotenv (2.7.6) + equalizer (0.0.11) + faraday (1.4.1) + faraday-excon (~> 1.1) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.1) + multipart-post (>= 1.2, < 3) + ruby2_keywords (>= 0.0.4) + faraday-excon (1.1.0) + faraday-net_http (1.0.1) + faraday-net_http_persistent (1.1.0) + hamster (3.0.0) + concurrent-ruby (~> 1.0) + i18n (1.8.10) + concurrent-ruby (~> 1.0) + ice_nine (0.11.2) + inflecto (0.0.2) + json_schema (0.21.0) + method_source (1.0.0) + minitest (5.14.4) + multi_json (1.15.0) + multipart-post (2.1.1) + mustermann (1.1.1) + ruby2_keywords (~> 0.0.1) + nanoc (4.12.1) + addressable (~> 2.5) + colored (~> 1.2) + nanoc-checking (~> 1.0) + nanoc-cli (= 4.12.1) + nanoc-core (= 4.12.1) + nanoc-deploying (~> 1.0) + parallel (~> 1.12) + tty-command (~> 0.8) + tty-which (~> 0.4) + nanoc-checking (1.0.1) + nanoc-cli (~> 4.11, >= 4.11.15) + nanoc-core (~> 4.11, >= 4.11.15) + nanoc-cli (4.12.1) + cri (~> 2.15) + diff-lcs (~> 1.3) + nanoc-core (= 4.12.1) + zeitwerk (~> 2.1) + nanoc-core (4.12.1) + concurrent-ruby (~> 1.1) + ddmemoize (~> 1.0) + ddmetrics (~> 1.0) + ddplugin (~> 1.0) + hamster (~> 3.0) + json_schema (~> 0.19) + slow_enumerator_tools (~> 1.0) + tty-platform (~> 0.2) + zeitwerk (~> 2.1) + nanoc-deploying (1.0.1) + nanoc-checking (~> 1.0) + nanoc-cli (~> 4.11, >= 4.11.15) + nanoc-core (~> 4.11, >= 4.11.15) + nio4r (2.5.7) + parallel (1.20.1) + pastel (0.8.0) + tty-color (~> 0.5) + pry (0.14.1) + coderay (~> 1.1) + method_source (~> 1.0) + public_suffix (4.0.6) + puma (5.2.2) + nio4r (~> 2.0) + rack (2.2.3) + rack-protection (2.1.0) + rack + rake (13.0.3) + recursive-open-struct (1.1.3) + ref (2.0.0) + robocop (0.1.1) + rack + rspec (3.10.0) + rspec-core (~> 3.10.0) + rspec-expectations (~> 3.10.0) + rspec-mocks (~> 3.10.0) + rspec-core (3.10.1) + rspec-support (~> 3.10.0) + rspec-expectations (3.10.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.10.0) + rspec-mocks (3.10.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.10.0) + rspec-support (3.10.2) + ruby2_keywords (0.0.4) + sinatra (2.1.0) + mustermann (~> 1.0) + rack (~> 2.2) + rack-protection (= 2.1.0) + tilt (~> 2.0) + sinatra-activerecord (2.0.22) + activerecord (>= 4.1) + sinatra (>= 1.0) + singleton (0.1.1) + slow_enumerator_tools (1.1.0) + sqlite3 (1.4.2) + telegram-bot-ruby (0.15.0) + faraday + inflecto + virtus + thread_safe (0.3.6) + tilt (2.0.10) + tty-color (0.6.0) + tty-command (0.10.1) + pastel (~> 0.8) + tty-platform (0.3.0) + tty-which (0.4.2) + tzinfo (2.0.4) + concurrent-ruby (~> 1.0) + virtus (1.0.5) + axiom-types (~> 0.1) + coercible (~> 1.0) + descendants_tracker (~> 0.0, >= 0.0.3) + equalizer (~> 0.0, >= 0.0.9) + whenever (1.0.0) + chronic (>= 0.6.3) + whenever-test (1.0.1) + whenever + zeitwerk (2.4.2) + +PLATFORMS + x86_64-linux + +DEPENDENCIES + activerecord + dotenv + multi_json + nanoc (~> 4.12) + pry (~> 0.14.1) + puma + rake (~> 13.0.3) + recursive-open-struct (~> 1.1.3) + robocop + rspec + sinatra + sinatra-activerecord (~> 2.0) + singleton + sqlite3 + telegram-bot-ruby + whenever + whenever-test + +BUNDLED WITH + 2.2.17 diff --git a/Aleksandrova_Vilena/3/bot/README.md b/Aleksandrova_Vilena/3/bot/README.md new file mode 100644 index 00000000..2aee3628 --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/README.md @@ -0,0 +1,54 @@ +# Telegram Bot + +A simple telegram bot that allows to study endlish words. + +## Description + +Bot has the following functionality: +- A user can start/stop the studying process. + +## Getting Started + +### Gems +``` +gem 'activerecord' +gem 'dotenv' # for using env. variables +gem 'multi_json' +gem 'nanoc', '~> 4.12' +gem 'pry', '~> 0.14.1' +gem 'puma' +gem 'rake', '~> 13.0.3' +gem 'robocop' +gem 'rspec' # for running tests +gem 'sinatra' +gem 'sinatra-activerecord', '~> 2.0' +gem 'singleton' +gem 'sqlite3' # db for testing +gem 'telegram-bot-ruby' +gem 'whenever', require: false +gem 'whenever-test' +``` +### Before executing +``` +1. Run tunels using ngrok tool for server emulating: + ./ngrok authtoken {ngrok_token} + ./ngrok http {port} +2. Send post request to telegram API that allows to redirect + curl --location --request POST 'https://api.telegram.org/bot{token}/setWebhook' --header 'Content-Type: application/json' --data-raw '{"url": {url}}' +``` + +### Executing program + +* Run ``` bundle install ``` +* Run ``` bundle exec rake db:seed ``` +* Run ``` bundle exec rake db:migrate ``` +* Run ``` bundle exec rake db:seed ``` +* Run ``` rackup -p {port} ``` + +``` +P.S. In this case it's enough to run: 'bundle install', 'rackup -p {port}' +``` + +### Task for cron +* Start ``` whenever --update-crontab --set environment='development' ``` +* Stop ``` whenever --clear-crontab ``` \ No newline at end of file diff --git a/Aleksandrova_Vilena/3/bot/Rakefile b/Aleksandrova_Vilena/3/bot/Rakefile new file mode 100644 index 00000000..7c902cf9 --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/Rakefile @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require_relative 'config/environment' +require 'sinatra/activerecord/rake' +require_relative 'lib/cron_worker' + +namespace :payload do + desc 'Send out lesson messages' + task :send_messages do + CronWorker.perform + end +end diff --git a/Aleksandrova_Vilena/3/bot/bot.iml b/Aleksandrova_Vilena/3/bot/bot.iml new file mode 100644 index 00000000..be1312d2 --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/bot.iml @@ -0,0 +1,1476 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Aleksandrova_Vilena/3/bot/bot_api.rb b/Aleksandrova_Vilena/3/bot/bot_api.rb new file mode 100644 index 00000000..038bb45f --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/bot_api.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'sinatra/base' +require 'telegram/bot' +require 'dotenv/load' +require_relative 'lib/loader' +require_relative 'lib/bot_handler' + +class BotApi < Sinatra::Base + post '/' do + if Bot.instance.api + BotHandler.new(request.body).run + status 200 + else + status 400 + end + end + + # start the server if ruby file executed directly + run! if app_file == $PROGRAM_NAME +end diff --git a/Aleksandrova_Vilena/3/bot/config.ru b/Aleksandrova_Vilena/3/bot/config.ru new file mode 100644 index 00000000..2c5f420a --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/config.ru @@ -0,0 +1,3 @@ +require './bot_api' + +run BotApi \ No newline at end of file diff --git a/Aleksandrova_Vilena/3/bot/config/database.yml b/Aleksandrova_Vilena/3/bot/config/database.yml new file mode 100644 index 00000000..d127b14a --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/config/database.yml @@ -0,0 +1,21 @@ +development: + adapter: 'sqlite3' + database: 'db/learning.db' + +#default: &default +# adapter: postgresql +# encoding: unicode +# pool: 5 +# host: localhost + +#development: +# <<: *default +# database: learning_development +# username: admin +# password: 1 + +#production: +# <<: *default +# database: learning_production +# username: admin +# password: 1 \ No newline at end of file diff --git a/Aleksandrova_Vilena/3/bot/config/environment.rb b/Aleksandrova_Vilena/3/bot/config/environment.rb new file mode 100644 index 00000000..e58eb774 --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/config/environment.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'sinatra/activerecord' + +ActiveRecord::Base.logger = Logger.new($stdout) + +ActiveRecord::Base.establish_connection( + adapter: 'sqlite3', + database: 'db/learning.db' + # adapter: 'postgresql', + # host: 'localhost', + # username: 'admin', + # database: 'learning_development', + # password: 1 +) diff --git a/Aleksandrova_Vilena/3/bot/config/schedule.rb b/Aleksandrova_Vilena/3/bot/config/schedule.rb new file mode 100644 index 00000000..23adc379 --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/config/schedule.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require_relative '../lib/cron_worker' + +set :environment, 'development' +set :output, 'log/cron.log' + +# prod +# every '0 10-19/1 * * 1-5' do +# rake 'payload:send_messages' +# end + +# dev testing +every '*/3 * * * 1-5' do + rake 'payload:send_messages' +end diff --git a/Aleksandrova_Vilena/3/bot/db/learning.db b/Aleksandrova_Vilena/3/bot/db/learning.db new file mode 100644 index 00000000..295a0d92 Binary files /dev/null and b/Aleksandrova_Vilena/3/bot/db/learning.db differ diff --git a/Aleksandrova_Vilena/3/bot/db/migrate/20210426082010_create_definitions_table.rb b/Aleksandrova_Vilena/3/bot/db/migrate/20210426082010_create_definitions_table.rb new file mode 100644 index 00000000..189abe7c --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/db/migrate/20210426082010_create_definitions_table.rb @@ -0,0 +1,12 @@ +class CreateDefinitionsTable < ActiveRecord::Migration[6.1] + def change + create_table :definitions do |t| + t.string :letter, null: false + t.string :word, null: false + t.text :description, null: false + + t.timestamps + end + add_index :definitions, :word + end +end diff --git a/Aleksandrova_Vilena/3/bot/db/migrate/20210426082522_create_users_table.rb b/Aleksandrova_Vilena/3/bot/db/migrate/20210426082522_create_users_table.rb new file mode 100644 index 00000000..0740396a --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/db/migrate/20210426082522_create_users_table.rb @@ -0,0 +1,12 @@ +class CreateUsersTable < ActiveRecord::Migration[6.1] + def change + create_table :users do |t| + t.integer :telegram_id, null: false + t.integer :status, null: true, default: 0 + t.integer :repeat_qty, null: true, default: 0 + + t.timestamps + end + add_index :users, :telegram_id, unique: true + end +end diff --git a/Aleksandrova_Vilena/3/bot/db/migrate/20210502112213_create_learned_definitions_table.rb b/Aleksandrova_Vilena/3/bot/db/migrate/20210502112213_create_learned_definitions_table.rb new file mode 100644 index 00000000..360793ba --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/db/migrate/20210502112213_create_learned_definitions_table.rb @@ -0,0 +1,13 @@ +class CreateLearnedDefinitionsTable < ActiveRecord::Migration[6.1] + def change + create_table :learned_definitions do |t| + t.belongs_to :user, null: false, foreign_key: true + t.belongs_to :definition, null: false, foreign_key: true + t.integer :sent_qty, null: true, default: 0 + t.integer :received_qty, null: true, default: 0 + t.boolean :missed_notification, null: true, default: false + + t.timestamps + end + end +end diff --git a/Aleksandrova_Vilena/3/bot/db/schema.rb b/Aleksandrova_Vilena/3/bot/db/schema.rb new file mode 100644 index 00000000..d0c739bd --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/db/schema.rb @@ -0,0 +1,47 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 2021_05_02_112213) do + + create_table "definitions", force: :cascade do |t| + t.string "letter", null: false + t.string "word", null: false + t.text "description", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["word"], name: "index_definitions_on_word" + end + + create_table "learned_definitions", force: :cascade do |t| + t.integer "user_id", null: false + t.integer "definition_id", null: false + t.integer "sent_qty", default: 0 + t.integer "received_qty", default: 0 + t.boolean "missed_notification", default: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["definition_id"], name: "index_learned_definitions_on_definition_id" + t.index ["user_id"], name: "index_learned_definitions_on_user_id" + end + + create_table "users", force: :cascade do |t| + t.integer "telegram_id", null: false + t.integer "status", default: 0 + t.integer "repeat_qty", default: 0 + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["telegram_id"], name: "index_users_on_telegram_id", unique: true + end + + add_foreign_key "learned_definitions", "definitions" + add_foreign_key "learned_definitions", "users" +end diff --git a/Aleksandrova_Vilena/3/bot/db/seeds.rb b/Aleksandrova_Vilena/3/bot/db/seeds.rb new file mode 100644 index 00000000..46e8670f --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/db/seeds.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require_relative '../lib/models/definition' +Definition.destroy_all + +ActiveRecord::Base.record_timestamps = true + +Definition.create(letter: 'A', word: 'A-', description: ['prefix (also an- before a vowel sound) not.']) +Definition.create(letter: 'A', word: 'Aa', description: ['abbr. 1 automobile association. 2 alcoholics anonymous.']) +# and so on diff --git a/Aleksandrova_Vilena/3/bot/lib/answers.rb b/Aleksandrova_Vilena/3/bot/lib/answers.rb new file mode 100644 index 00000000..4f1a7abc --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/lib/answers.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Answers + def load_answers + { welcome: 'Привет. Я бот, который помогает учить новые английские слова каждый день. ' \ + 'Давай сперва определимся сколько слов в день ( от 1 до 6 ) ты хочешь узнавать?', + max_qty: 'Я не умею учить больше чем 6 словам. Давай еще раз?', + registered: 'Принято.', + new: 'Лови новое слово 🤓', + warning: 'Кажется ты был слишком занят, и пропустил слово выше? Дай мне знать что у тебя все хорошо. 😘', + accepted: 'Вижу, что ты заметил слово! Продолжаем учиться дальше!', + spam: 'Мне кажется или ты шлешь какую-то фигню 😉', + pause: 'Иногда нужно отдыхать 🥳', + bye: 'Пока 😪' }.freeze + end +end diff --git a/Aleksandrova_Vilena/3/bot/lib/bot_handler.rb b/Aleksandrova_Vilena/3/bot/lib/bot_handler.rb new file mode 100644 index 00000000..459bd595 --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/lib/bot_handler.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'telegram/bot' +require 'json' +require 'ostruct' +require_relative 'logging' +require_relative 'json_parser' +require_relative 'loader' +require_relative 'sender' +require 'byebug' + +class BotHandler + include Logging + include JsonParser + + def initialize(request_body) + @msg = hash_to_struct(load(request_body)) + end + + def run + Talking.send(@msg) + end +end diff --git a/Aleksandrova_Vilena/3/bot/lib/cron_worker.rb b/Aleksandrova_Vilena/3/bot/lib/cron_worker.rb new file mode 100644 index 00000000..928b5336 --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/lib/cron_worker.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require_relative 'logging' +require_relative 'reply_factory' +require_relative 'answers' +require_relative 'generator' +require_relative 'models/user' +require_relative 'models/learned_definition' +require_relative 'models/definition' +require 'telegram/bot' + +class CronWorker + include Logging + def self.perform + logger.info 'starting cron notifications' + Job.new.notify_users + end +end + +class Job + include Logging + include Answers + include Generator + + attr_reader :telegram_id, :msg + + def initialize + @api = Telegram::Bot::Api.new(ENV['BOT_API_TOKEN']) + @answers = load_answers + @telegram_id = 0 + @msg = nil + end + + def notify_users + User.where(status: User.statuses[:active]).each do |user| + @telegram_id = user.telegram_id + studied_word = LearnedDefinition.by_user_id(user.id).order(created_at: :desc).first + if new?(user.repeat_qty, studied_word.received_qty, studied_word.sent_qty) + send_new_word + next + end + next if word_missed?(studied_word.received_qty, studied_word.sent_qty) + + send_word(studied_word) + studied_word.update(sent_qty: studied_word.sent_qty + 1) + end + end + + def generate_definition(telegram_id) + @msg = generate_word(telegram_id) + end + + private + + def send_new_word + @api.sendMessage(chat_id: @telegram_id, text: @answers[:new]) + sleep 1 + generate_definition(@telegram_id) + @api.sendMessage(chat_id: @telegram_id, text: @msg) + end + + def new?(qty, sent_qty, received_qty) + return true if qty == sent_qty && qty == received_qty + + false + end + + def send_word(studied_word) + voc_word = Definition.where(id: studied_word.definition_id).first + @msg = "#{voc_word.word.upcase} - #{voc_word.description}" + @api.sendMessage(chat_id: @telegram_id, text: @msg) + end + + def word_missed?(received_qty, sent_qty) + @api.sendMessage(chat_id: @telegram_id, text: @answers[:warning]) if received_qty != sent_qty + received_qty != sent_qty + end +end diff --git a/Aleksandrova_Vilena/3/bot/lib/generator.rb b/Aleksandrova_Vilena/3/bot/lib/generator.rb new file mode 100644 index 00000000..288aefe2 --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/lib/generator.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Generator + def generate_word(telegram_id) + arr = ('A'..'Z').to_a + letter = arr[rand(arr.count)] + ids = Definition.where(letter: letter).select(:id) + def_id = ids[rand(ids.count)].id + definition = Definition.where(id: def_id).first + user_id = User.by_id(telegram_id).first.id + LearnedDefinition.create(user_id: user_id, definition_id: def_id, sent_qty: 1) + "#{definition.word.upcase} - #{definition.description}" + end +end diff --git a/Aleksandrova_Vilena/3/bot/lib/json_parser.rb b/Aleksandrova_Vilena/3/bot/lib/json_parser.rb new file mode 100644 index 00000000..c7097cd9 --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/lib/json_parser.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'multi_json' + +# Json parser +module JsonParser + def load(request_body) + request_body.rewind + body = request_body.read + + MultiJson.load(body) + end + + def hash_to_struct(hash) + OpenStruct.new(hash.transform_values do |val| + val.is_a?(Hash) ? hash_to_struct(val) : val + end) + end +end diff --git a/Aleksandrova_Vilena/3/bot/lib/loader.rb b/Aleksandrova_Vilena/3/bot/lib/loader.rb new file mode 100644 index 00000000..76c15232 --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/lib/loader.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'singleton' +require 'dotenv/load' +require_relative 'logging' +require_relative 'answers' + +class Bot + include Singleton + include Logging + include Answers + + attr_accessor :api, :answer + + def initialize + if ENV['BOT_API_TOKEN'].nil? + logger.error 'environmental variable BOT_API_KEY not set' + return + end + @api = Telegram::Bot::Api.new(ENV['BOT_API_TOKEN']) + @answer = load_answers + end +end diff --git a/Aleksandrova_Vilena/3/bot/lib/logging.rb b/Aleksandrova_Vilena/3/bot/lib/logging.rb new file mode 100644 index 00000000..d5c8a6f0 --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/lib/logging.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'logger' + +# Logger +module Logging + class << self + def logger + @logger ||= Logger.new($stdout) + end + + attr_writer :logger + end + + def self.included(base) + class << base + def logger + Logging.logger + end + end + end + + def logger + Logging.logger + end +end diff --git a/Aleksandrova_Vilena/3/bot/lib/models/definition.rb b/Aleksandrova_Vilena/3/bot/lib/models/definition.rb new file mode 100644 index 00000000..95bdd008 --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/lib/models/definition.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class Definition < ActiveRecord::Base + has_many :learned_definitions, dependent: :destroy + has_many :users, through: :learned_definitions +end diff --git a/Aleksandrova_Vilena/3/bot/lib/models/learned_definition.rb b/Aleksandrova_Vilena/3/bot/lib/models/learned_definition.rb new file mode 100644 index 00000000..776155c8 --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/lib/models/learned_definition.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class LearnedDefinition < ActiveRecord::Base + belongs_to :user + belongs_to :definition + + scope :by_user_id, ->(id) { where('user_id = ?', id) } +end diff --git a/Aleksandrova_Vilena/3/bot/lib/models/user.rb b/Aleksandrova_Vilena/3/bot/lib/models/user.rb new file mode 100644 index 00000000..16645d6f --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/lib/models/user.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class User < ActiveRecord::Base + enum status: %i[registered active pending stop] + has_many :learned_definitions, dependent: :destroy + has_many :definitions, through: :learned_definitions + + scope :by_id, ->(telegram_id) { where('telegram_id = ?', telegram_id) } +end diff --git a/Aleksandrova_Vilena/3/bot/lib/reply_factory.rb b/Aleksandrova_Vilena/3/bot/lib/reply_factory.rb new file mode 100644 index 00000000..e70a76c2 --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/lib/reply_factory.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +require 'sinatra/activerecord' +require_relative 'loader' +require_relative 'generator' +require_relative 'models/user' +require_relative 'models/learned_definition' +require_relative 'models/definition' +require 'pry' + +class Response + attr_reader :telegram_id, :msg + + def initialize(req, msg = '') + @telegram_id = req.message.from.id + @msg = msg + end + + def send + Bot.instance.api.sendMessage(chat_id: @telegram_id, text: @msg) + end +end + +class FeedbackResponse < Response + include Generator + def initialize(req) + super(req) + @req = req + @telegram_id = req.message.from.id + @msg = Bot.instance.answer[:accepted] + end + + def send + return unless status_active? + + user = User.by_id(@telegram_id).first + cur_word = LearnedDefinition.by_user_id(user.id).order(created_at: :desc).first + return if answered?(user, cur_word) + + if new?(user.repeat_qty, cur_word.received_qty, cur_word.sent_qty) + @msg = Bot.instance.answer[:new] + super + sleep 1 + send_new_word + super + return + end + cur_word.update(received_qty: cur_word.received_qty + 1) + super + end + + private + + def answered?(user, cur_word) + cur_word.sent_qty == cur_word.received_qty && user.repeat_qty > cur_word.sent_qty + end + + def status_active? + user = User.by_id(@telegram_id).where(status: User.statuses[:active]) + !user.empty? + end + + def new?(qty, sent_qty, received_qty) + return true if qty == sent_qty && qty == received_qty + + false + end + + def send_new_word + @msg = generate_word(@telegram_id) + end +end + +class RegisterResponse < Response + include Generator + def initialize(req) + super(req) + @req = req + @telegram_id = req.message.from.id + @msg = Bot.instance.answer[:registered] + end + + def send + return unless status_registered? + + # accept the qty + super + sleep 1 + send_new_word + # send the new word + super + # change the status at the end to avoid concurrent conflict with scheduler + register + end + + private + + def register + qty = @req.message.text.to_i + user = User.where(telegram_id: @telegram_id).first + User.update(user.id, status: User.statuses[:active], repeat_qty: qty) + end + + def send_new_word + @msg = generate_word(@telegram_id) + end + + def status_registered? + user = User.by_id(@telegram_id).where(status: User.statuses[:registered]) + !user.empty? + end +end diff --git a/Aleksandrova_Vilena/3/bot/lib/sender.rb b/Aleksandrova_Vilena/3/bot/lib/sender.rb new file mode 100644 index 00000000..2d797458 --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/lib/sender.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require_relative 'loader' +require_relative 'reply_factory' +require 'pry' + +class Talking + def self.send(req) + Sender.for(req).send + end +end + +class Sender + def self.for(req) + case req.message.text + when '/start' + User.find_or_create_by(telegram_id: req.message.from.id) + Response.new(req, Bot.instance.answer[:welcome]) + when '/pause' + Response.new(req, Bot.instance.answer[:pause]) + when '/stop' + Response.new(req, Bot.instance.answer[:bye]) + when /[1-6]/ + RegisterResponse.new(req) + when '🙂' + FeedbackResponse.new(req) + else + Response.new(req, Bot.instance.answer[:spam]) + end + end +end diff --git a/Aleksandrova_Vilena/3/bot/lib/tasks/payload.rake b/Aleksandrova_Vilena/3/bot/lib/tasks/payload.rake new file mode 100644 index 00000000..60535cdc --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/lib/tasks/payload.rake @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require_relative '../cron_worker' + +namespace :payload do + desc 'Send out lesson messages' + task send_messages: :environment do + CronWorker.perform + end +end diff --git a/Aleksandrova_Vilena/3/bot/log/cron.log b/Aleksandrova_Vilena/3/bot/log/cron.log new file mode 100644 index 00000000..77600d55 --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/log/cron.log @@ -0,0 +1,7 @@ + +I, [2021-05-07T11:42:02.597468 #174069] INFO -- : starting cron notifications +I, [2021-05-07T11:45:03.039877 #174595] INFO -- : starting cron notifications +I, [2021-05-07T11:48:02.470802 #175135] INFO -- : starting cron notifications +I, [2021-05-07T11:51:02.299669 #175692] INFO -- : starting cron notifications +I, [2021-05-07T11:54:02.728681 #176134] INFO -- : starting cron notifications +I, [2021-05-07T11:57:02.169705 #176645] INFO -- : starting cron notifications diff --git a/Aleksandrova_Vilena/3/bot/spec/generator_spec.rb b/Aleksandrova_Vilena/3/bot/spec/generator_spec.rb new file mode 100644 index 00000000..abd5b956 --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/spec/generator_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'singleton' +require 'telegram/bot' +require_relative '../lib/generator' + +RSpec.describe Generator do + before do + allow(Generator).to receive(:generate_word).and_return(:definition) + end + + it 'stubbed when calling Generator' do + expect(Generator.generate_word).to eq :definition + end +end diff --git a/Aleksandrova_Vilena/3/bot/spec/job_spec.rb b/Aleksandrova_Vilena/3/bot/spec/job_spec.rb new file mode 100644 index 00000000..da0f3be4 --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/spec/job_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'singleton' +require 'telegram/bot' +require_relative '../lib/cron_worker' +require_relative '../lib/loader' +require 'recursive-open-struct' + +RSpec.describe Job do + before { Singleton.__init__(Bot) } + subject { described_class.new } + + describe '#generate_word' do + context 'checks the telegranm_id' do + it 'right telegram_id' do + expect(subject.telegram_id).to_not eq(11_111) + end + end + context 'checks a new word was generated' do + it 'word generation' do + subject.generate_definition(11_111) + expect(subject.msg).to_not eq('') + end + end + context 'checks if the word was sent' do + it 'sent word' do + subject.notify_users + user_id = User.by_id(11_111).first.id + word = LearnedDefinition.where(user_id: user_id).first + expect(word.sent_qty).to eq(1) + end + end + end +end diff --git a/Aleksandrova_Vilena/3/bot/spec/json_parser_spec.rb b/Aleksandrova_Vilena/3/bot/spec/json_parser_spec.rb new file mode 100644 index 00000000..05ea61f8 --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/spec/json_parser_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'singleton' +require 'telegram/bot' +require_relative '../lib/json_parser' + +RSpec.describe JsonParser do + before do + allow(JsonParser).to receive(:load).and_return('{ + "update_id": 210195582, + "message": { + "from": { + "id": 11111 + } + }}') + end + + it '#load' do + expect(JsonParser.send(:load)).to eq '{ + "update_id": 210195582, + "message": { + "from": { + "id": 11111 + } + }}' + end +end diff --git a/Aleksandrova_Vilena/3/bot/spec/loader_spec.rb b/Aleksandrova_Vilena/3/bot/spec/loader_spec.rb new file mode 100644 index 00000000..3764834b --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/spec/loader_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'singleton' +require 'telegram/bot' +require_relative '../lib/loader' + +RSpec.describe Bot do + before { Singleton.__init__(Bot) } + context 'telegram api' do + it 'returns telegram bot api' do + expect(Bot.instance.api).not_to eq nil + end + end + + context 'bot answers' do + it 'returns bot answers' do + expect(Bot.instance.answer).not_to eq nil + end + end +end diff --git a/Aleksandrova_Vilena/3/bot/spec/sender_spec.rb b/Aleksandrova_Vilena/3/bot/spec/sender_spec.rb new file mode 100644 index 00000000..2668854a --- /dev/null +++ b/Aleksandrova_Vilena/3/bot/spec/sender_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'singleton' +require 'telegram/bot' +require_relative '../lib/sender' +require_relative '../lib/loader' +require 'recursive-open-struct' + +RSpec.describe Sender do + before { Singleton.__init__(Bot) } + describe '#for' do + let(:req) { RecursiveOpenStruct.new({ message: { text: '/start', from: { id: 11_111 } } }) } + context 'checks the start message' do + it 'greeting response' do + expect(Sender.for(req).msg).to eq(Bot.instance.answer[:welcome]) + end + end + context 'checks the telegram_id' do + it 'right telegram_id' do + expect(Sender.for(req).telegram_id).to eq(11_111) + end + end + context 'checks the db user' do + it 'db user' do + Sender.for(req) + user = User.by_id(11_111) + expect(!user.empty?).to be_truthy + end + end + context 'checks the stop message' do + let(:req) { RecursiveOpenStruct.new({ message: { text: '/stop', from: { id: 11_111 } } }) } + it 'bye response' do + expect(Sender.for(req).msg).to eq(Bot.instance.answer[:bye]) + end + end + context 'checks the pause message' do + let(:req) { RecursiveOpenStruct.new({ message: { text: '/pause', from: { id: 11_111 } } }) } + it 'pause response' do + expect(Sender.for(req).msg).to eq(Bot.instance.answer[:pause]) + end + end + context 'checks the repeat_qty' do + let(:req) { RecursiveOpenStruct.new({ message: { text: '3', from: { id: 11_111 } } }) } + it 'qty response' do + expect(Sender.for(req).msg).to eq(Bot.instance.answer[:registered]) + end + end + context 'checks db repeat_qty' do + it 'db quantity' do + user = User.by_id(11_111).first + expect(user.repeat_qty).to eq(0) + end + end + context 'checks the accept response' do + let(:req) { RecursiveOpenStruct.new({ message: { text: '🙂', from: { id: 11_111 } } }) } + it 'accept response' do + expect(Sender.for(req).msg).to eq(Bot.instance.answer[:accepted]) + end + end + context 'checks the spam response' do + let(:req) { RecursiveOpenStruct.new({ message: { text: 'Hello world', from: { id: 11_111 } } }) } + it 'spam response' do + expect(Sender.for(req).msg).to eq(Bot.instance.answer[:spam]) + end + end + end +end diff --git a/Aleksandrova_Vilena/3/bot/tools/ngrok b/Aleksandrova_Vilena/3/bot/tools/ngrok new file mode 100755 index 00000000..e84270ff Binary files /dev/null and b/Aleksandrova_Vilena/3/bot/tools/ngrok differ