Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions Konstantin_Krasilov/3/Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

source 'https://rubygems.org'

gem 'telegramAPI'
gem 'sinatra'

gem 'sinatra-activerecord'
gem 'activerecord'
gem 'pg'

gem 'dotenv-rails'
gem 'json'
gem 'progress_bar'

gem 'whenever', require: false
gem 'rake'

gem 'rspec'
gem 'factory_bot'
12 changes: 12 additions & 0 deletions Konstantin_Krasilov/3/Rakefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

require 'sinatra/activerecord/rake'
require './app'

namespace :db do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Не очень понятно, если честно, что ты хотел тут сказать. Выглядит как ненужный код.

Copy link
Contributor Author

@kkrasilov kkrasilov May 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Я его добавил чтобы была возможность работать с таксками гема sinatra/activerecord/rake - rake db:migrate, rake db:seed.
https://github.com/sinatra-activerecord/sinatra-activerecord#setup

task :load_config do
require './app'
end
end

Dir.glob('lib/tasks/*.rake').each { |r| load r }
63 changes: 63 additions & 0 deletions Konstantin_Krasilov/3/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Telegram-Bot "English lessons"

## Описание

Приложение, которое помогает учить английский слова!
После регистрации каждому пользователю с некоторой периодичностью отправляются новые английские слова для обучения.
Пользователь может выбрать сколько слов в день он хочет учить!

## Пример взаимодействия

> /start
>
> Привет! Я бот, который помогает учить новые английские слова каждый день. Давай сперва определимся,' \
'сколько слов в день (от 1 до 6 ) ты хочешь узнавать?
>
> 7
>
> Сорри, только 6 слов. Давай еще раз?
>
> 4
>
> Принято!

Через какое-то время от бота должно прийти сообщение похожего смысла:

> **Opinion** - 1 unproven belief. 2 view held as probable. 3 what one thinks about something. 4 piece of professional advice (a second opinion). 5 estimation (low opinion of). [latin: related to *opine]

Если человек не отреагировал на слово, то бот напомнит ему об этом:

> Кажется, ты был слишком занят и пропустил слово выше? Дай мне знать, что у тебя все хорошо!
>
> 😘
>
> Вижу, что ты заметил слово! Продолжаем учиться дальше!

## Установка

Для корректной работы программы на вашем компьютере должен быть установлены [Ruby](https://www.ruby-lang.org/en/),
база данных [Postgres](https://www.postgresql.org/) и [Ngrok](https://ngrok.com/).
Запустите сервер `ngrok`:
```
$ ./ngrok http 4567
```
Создайте своего [telegram бота](https://core.telegram.org/bots).
Пропишите переменные окружения в .env файле:
```
TELEGRAM_TOKEN=Ваш токен telegram
DATABASE_USER=Ваши_данные
DATABASE_PASSWORD=Ваши_данные
DATABASE_NAME=Ваши_данные
URL=https url который выдал ngrok
```
Запустите комманды:
```
$ bundle
$ bunlde exec rake db:migrate db:seed
$ whenever --update-crontab
```
## Запуск
Запуск осуществляется командой:
```
$ bundle exec ruby app.rb
```
38 changes: 38 additions & 0 deletions Konstantin_Krasilov/3/app.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# frozen_string_literal: true

require 'telegramAPI'
require 'sinatra'
require 'dotenv'
require 'sinatra/activerecord'
require_relative 'config/connection'
require_relative 'app/models/user'
require_relative 'app/services/telegram/conversation'

Dotenv.load
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Лайк за использование ENV переменных 👍


api = TelegramAPI.new ENV['TELEGRAM_TOKEN']

post '/telegram' do
status 200

request.body.rewind
data = JSON.parse(request.body.read)

chat_id = data['message']['chat']['id']
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Чисто в теории у тебя тут очень легко может случиться ошибка, которую ты не обработаешь. Например chat ключа не будет в сообщении, оно вернет nil попробует взять ['id'] и получится беда.

Чтобы от такого застраховаться - пойди узнать про метод dig у хеша. И не забывай про обработку ошибок.

Copy link
Contributor Author

@kkrasilov kkrasilov May 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Как лучше обрабатывать ошибки?

Просто добавить

chat_id = data.dig('message', 'chat', 'id')
raise StandardError, 'Sorry, chat id cannot be blank' if chat_id.nil?

или обернуть все в begin -

begin
  chat_id = data.dig('message', 'chat', 'id')
  raise StandardError if chat_id.nil?
rescue
  puts 'Error - Sorry, chat id cannot be blank'
end

чтобы программа продолжала работать.

И с ошибками ActiveRecord, сделать -

def set_user
  user = User.find_or_create_by!(telegram_id: chat_id)
rescue ActiveRecord::NotNullViolation => e
  puts "Error - #{e}"
end

или просто добавить метод find_or_create_by! без rescue?

message = data['message']['text']

user = User.find_or_create_by(telegram_id: chat_id)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

find_or_create_by если не создаст пользователя, вернет тебе false и будет беда. Лучше всего если не обрабатываешь ошибки в ActiveRecord использовать методы с восклицательным знаком find_or_create_by!


case message
when '/start'
user.send_max_word!
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Понятно что ты тут пытался сделать какую-то машину состояний. Так как ты тоже можно, но лучше посмотреть в сторону https://github.com/aasm/aasm

api.sendMessage(chat_id, Telegram::Conversation::RESPONSE[:welcome])
else
api.sendMessage(chat_id, Telegram::Conversation.new(user, message).call) unless user.conversation_break?
end

# Return an empty json, to say "ok" to Telegram
'{}'
end

api.setWebhook(ENV['URL'])
12 changes: 12 additions & 0 deletions Konstantin_Krasilov/3/app/models/user.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

class User < ActiveRecord::Base
enum conversation_status: {
send_max_word: 0,
send_smiley: 1,
conversation_break: 2
}

has_many :user_words, dependent: :destroy
has_many :words, through: :user_words
end
6 changes: 6 additions & 0 deletions Konstantin_Krasilov/3/app/models/user_word.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true

class UserWord < ActiveRecord::Base
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rails/ApplicationRecord: Models should subclass ApplicationRecord.

belongs_to :user
belongs_to :word
end
10 changes: 10 additions & 0 deletions Konstantin_Krasilov/3/app/models/word.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

class Word < ActiveRecord::Base
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rails/ApplicationRecord: Models should subclass ApplicationRecord.

has_many :user_words, dependent: :destroy
has_many :users, through: :user_words

def to_s
"#{value} - #{meaning}"
end
end
40 changes: 40 additions & 0 deletions Konstantin_Krasilov/3/app/services/telegram/conversation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true

module Telegram
# Класс который общаеться с пользователем - реагирует на ответы в зависимости от статуса беседы.
class Conversation
RESPONSE = {
welcome: 'Привет! Я бот, который помогает учить новые английские слова каждый день. Давай сперва определимся,' \
'сколько слов в день (от 1 до 6 ) ты хочешь узнавать?',
max_word_success: 'Принято!',
max_word_error: 'Сорри, только 6 слов. Давай еще раз?',
smiley_success: 'Вижу, что ты заметил слово! Продолжаем учиться дальше!',
smiley_error: 'Отправь смайл 😉'
}.freeze

def initialize(user, message)
@user = user
@message = message
end

def call
send(@user.conversation_status)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

У тебя сейчас в приложении появилась очень странная неприятность. Получается что работа твоего класса Telegram::Coversation зависит от названия статусов в модели. Так делать нельзя, хоть и может казаться что это красиво. Завтра кто-нибудь решит переименовать все статусы и будет большая беда.

Copy link
Contributor Author

@kkrasilov kkrasilov May 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Можно разбить на отдельные сервисы - Telegram::Start, Telegram::SendMaxWord, Telegram::SendSmiley?

end

private

def send_max_word
return RESPONSE[:max_word_error] unless (1..6).include?(@message.to_i)

@user.update(max_words: @message, conversation_status: 'conversation_break')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@user.update!

RESPONSE[:max_word_success]
end

def send_smiley
return RESPONSE[:smiley_error] unless @message.unpack('U*').any? { |e| e.between?(0x1F600, 0x1F6FF) }

@user.conversation_break!
RESPONSE[:smiley_success]
end
end
end
48 changes: 48 additions & 0 deletions Konstantin_Krasilov/3/app/services/telegram/lesson.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# frozen_string_literal: true

require 'telegramAPI'
require 'dotenv'
require_relative '../../models/user'
require_relative '../../models/word'
require_relative '../../models/user_word'

Dotenv.load

module Telegram
# Класс который учит пользователя новым словам. Ищет подходящее слово, отправляет пользователю и ждет реакции.
class Lesson
USER_SLEEP = 'Кажется, ты был слишком занят и пропустил слово выше? Дай мне знать, что у тебя все хорошо!'

def initialize(user)
@user = user
@user.send_smiley!
@api = TelegramAPI.new ENV['TELEGRAM_TOKEN']
end

def call
find_and_added_word_to_user
send_word
waiting_answer
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Тут надо было перключать пользователя в статус "ждет ответа" и просто выходить.

end

private

def find_and_added_word_to_user
@word = Word.where.not(id: @user.words).order('RANDOM()').first
@user.words << @word
end

def send_word
@api.sendMessage(@user.telegram_id, @word.to_s)
end

def waiting_answer
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А вот это очень опасная архитектура этой фишки. Проблема заключается в том что ты с помощью крона запускаешь каждые N минут процесс. И если пользователь тебе не ответил - тупо его держишь. Это будет работать пока у тебя парочку пользователей, но когда у тебя их станет хотя бы пару десятков - твой сервис упадет.

loop do
sleep(600)
@user.reload

@user.conversation_break? ? break : @api.sendMessage(@user.telegram_id, USER_SLEEP)
end
end
end
end
15 changes: 15 additions & 0 deletions Konstantin_Krasilov/3/config/connection.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

require 'sinatra/activerecord'
require 'dotenv'

Dotenv.load

set :database, {
adapter: 'postgresql',
encoding: 'unicode',
pool: 5,
username: ENV['DATABASE_USER'],
password: ENV['DATABASE_PASSWORD'],
database: ENV['DATABASE_NAME']
}
27 changes: 27 additions & 0 deletions Konstantin_Krasilov/3/config/schedule.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true

set :output, 'log/cron.log'

every '0 10 * * 1-7' do
rake 'telegram:start_lesson[6]'
end

every '0 12 * * 1-7' do
rake 'telegram:start_lesson[5]'
end

every '0 14 * * 1-7' do
rake 'telegram:start_lesson[4]'
end

every '0 16 * * 1-7' do
rake 'telegram:start_lesson[3]'
end

every '0 18 * * 1-7' do
rake 'telegram:start_lesson[2]'
end

every '0 20 * * 1-7' do
rake 'telegram:start_lesson[1]'
end
15 changes: 15 additions & 0 deletions Konstantin_Krasilov/3/db/migrate/20210430082533_create_users.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class CreateUsers < ActiveRecord::Migration[6.1]
def up
create_table :users do |t|
t.integer :telegram_id, null: false, index: { unique: true }
t.integer :max_words, null: false, default: 1
t.integer :conversation_status, null: false, default: 0

t.timestamps
end
end

def down
drop_table :users
end
end
14 changes: 14 additions & 0 deletions Konstantin_Krasilov/3/db/migrate/20210430084025_create_words.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class CreateWords < ActiveRecord::Migration[6.1]
def up
create_table :words do |t|
t.string :value
t.text :meaning

t.timestamps
end
end

def down
drop_table :words
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class CreateUserWords < ActiveRecord::Migration[6.1]
def up
create_table :user_words do |t|
t.references :user, null: false, foreign_key: true
t.references :word, null: false, foreign_key: true

t.timestamps
end
end

def down
drop_table :user_words
end
end
13 changes: 13 additions & 0 deletions Konstantin_Krasilov/3/db/seeds.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

require_relative '../app/models/word'
require 'progress_bar'
require 'json'

words = JSON.parse(File.read(File.join(__dir__, 'words.json')))
bar = ProgressBar.new(words.count, :bar, :percentage, :elapsed)

words.each do |word|
Word.create(value: word[0], meaning: word[1])
bar.increment!
end
Loading