<file_sep>class TelegramResponder
  def initialize(bot, database)
    @bot = bot
    @database = database
  end

  def filter_keyboard
    types = ['C1-C4', 'Interlude', 'Epilogue', 'High five', 'Classic', I18n.t('other')]
    types.delete('C1-C4')         if @user.filter_chronicle.include?('c1')
    types.delete('Interlude')     if @user.filter_chronicle.include?('interlude')
    types.delete('Epilogue')      if @user.filter_chronicle.include?('epilogue')
    types.delete('High five')     if @user.filter_chronicle.include?('high five')
    types.delete('Classic')       if @user.filter_chronicle.include?('classic')
    types.delete(I18n.t('other')) if @user.filter_chronicle.include?('freya')
    pairs = []
    types.each_with_index do |t, i|
      if i.even? then
        pairs << [t]
      else
        pairs.last << t
      end
    end
    pairs << Telegram::Bot::Types::KeyboardButton.new(text: I18n.t('enough'))
  end

  def respond_to(message)
    @all      = @database.exec("SELECT * from servers").values
    @user     = User.new(@database, message.from.id)
    @user.chat_id = message.chat.id
    I18n.locale = (@user.lang || :en).to_sym
    case message
    when Telegram::Bot::Types::CallbackQuery
      respond_to_callback_query(message)
    when Telegram::Bot::Types::Message
      respond_to_text(message)
    end
  end

  private

  def respond_to_text(message)
    case message.text
    when '/start'
      if @user.lang then
        help(message)
      else
        ask_lang(message)
      end
    when '/lang'
      ask_lang(message)
    when '/help'
      help(message)
    when '/soon'
      send_servers soon(@all, 14), message
      add_help_to_end(message)
    when '/recent'
      send_servers recent(@all, 14), message
      add_help_to_end(message)
    when '/info'
      info(message)
      add_help_to_end(message)
    when '/filter'
      clear_filter
      ask_filter_chronicle(message)
    when '/lowrate'
      send_servers(from_to(apply_rates_filter(@all, 0, 20), -21, 30), message)
      add_help_to_end(message)
    when '/multicraft'
      send_servers(from_to(apply_rates_filter(@all, 20, 200), -21, 30), message)
      add_help_to_end(message)
    when '/pvprate'
      send_servers(from_to(apply_rates_filter(@all, 200, 99999999), -21, 30), message)
      add_help_to_end(message)
    when '/notify'
      clear_filter
      ask_filter_chronicle(message)
    when 'C1-C4'
      @user.filter_chronicle = @user.filter_chronicle + ['c1', 'c2', 'c3', 'c4']
      send_chronicle_filter(message)
      ask_filter_chronicle(message)
    when 'Interlude'
      @user.filter_chronicle = @user.filter_chronicle + ['interlude', 'interlude+']
      send_chronicle_filter(message)
      ask_filter_chronicle(message)
    when 'High five'
      @user.filter_chronicle = @user.filter_chronicle + ['high five']
      send_chronicle_filter(message)
      ask_filter_chronicle(message)
    when 'Epilogue'
      @user.filter_chronicle = @user.filter_chronicle + ['epilogue']
      send_chronicle_filter(message)
      ask_filter_chronicle(message)
    when 'Classic'
      @user.filter_chronicle = @user.filter_chronicle + ['classic']
      send_chronicle_filter(message)
      ask_filter_chronicle(message)
    when I18n.t('.other')
      @user.filter_chronicle = @user.filter_chronicle + ['final', 'lindvior', 'freya', 'ertheria', 'odyssey']
      send_chronicle_filter(message)
      ask_filter_chronicle(message)
    when I18n.t('enough')
      ask_filter_rates(message)
    when 'Low-Rate'
      @user.filter_rates_from = 0
      @user.filter_rates_to = 15
      ask_for_notifies(message)
    when 'Multi-Craft'
      @user.filter_rates_from = 15
      @user.filter_rates_to = 250
      ask_for_notifies(message)
    when 'PVP-Rate'
      @user.filter_rates_from = 250
      @user.filter_rates_to = 99999999
      ask_for_notifies(message)
    when 'RU'
      @user.lang = 'ru'
      I18n.locale = :ru
      add_help_to_end(message)
    when 'ENG'
      @user.lang = 'en'
      I18n.locale = :en
      add_help_to_end(message)
    when '/reset'
      @user.last_notified = Time.now.iso8601
      filter(message)
      @user.notify_period = nil
    when I18n.t('notify_everyday')
      @user.last_notified = Time.now.iso8601
      filter(message)
      @user.notify_period = 1
    when I18n.t('notify_everyweek')
      @user.last_notified = Time.now.iso8601
      filter(message)
      @user.notify_period = 7
    when I18n.t('notify_everymonth')
      @user.last_notified = Time.now.iso8601
      filter(message)
      @user.notify_period = 30
    else
      @bot.api.send_message(chat_id: message.chat.id, text: message.text)
    end
  end

  def respond_to_callback_query(message)
    # case message.data
    # end
  end

  def send_servers(servers, message)
    servers.each do |s|
      day_diff = date_diff(Time.now, s[3])
      day_part =  if (day_diff > 0) then
                    I18n.t('opens_in_x_days', x: day_diff)
                  elsif (day_diff < 0) then
                    I18n.t('opened_x_days_ago', x: -day_diff)
                  else
                    I18n.t('opens_today')
                  end
      text = "#{s[0]}\n#{s[1]} x#{s[2]}\n#{s[3]} #{day_part}"
      @bot.api.send_message(chat_id: message.chat.id, text: text)
    end
  end

  def ask_lang(message)
    kb = [
      [Telegram::Bot::Types::KeyboardButton.new(text: 'RU'),
      Telegram::Bot::Types::KeyboardButton.new(text: 'ENG')]
    ]
    markup = Telegram::Bot::Types::ReplyKeyboardMarkup.new(keyboard: kb, one_time_keyboard: true)
    @bot.api.send_message(chat_id: message.chat.id, text: "LANGUAGE", reply_markup: markup)
  end

  def ask_filter_chronicle(message)
    markup = Telegram::Bot::Types::ReplyKeyboardMarkup.new(keyboard: filter_keyboard, one_time_keyboard: true)
    @bot.api.send_message(chat_id: message.chat.id, text: I18n.t('select_chronicle'), reply_markup: markup)
  end

  def ask_filter_rates(message)
    kb = [
      Telegram::Bot::Types::KeyboardButton.new(text: 'Low-Rate'),
      Telegram::Bot::Types::KeyboardButton.new(text: 'Multi-Craft'),
      Telegram::Bot::Types::KeyboardButton.new(text: 'PVP-Rate'),
    ]
    markup = Telegram::Bot::Types::ReplyKeyboardMarkup.new(keyboard: kb, one_time_keyboard: true)
    @bot.api.send_message(chat_id: message.chat.id, text: I18n.t('choose_rates'), reply_markup: markup)
  end

  def ask_for_notifies(message)
    kb = [
      Telegram::Bot::Types::KeyboardButton.new(text: I18n.t('notify_everyday')),
      Telegram::Bot::Types::KeyboardButton.new(text: I18n.t('notify_everyweek')),
      Telegram::Bot::Types::KeyboardButton.new(text: I18n.t('notify_everymonth')),
    ]
    markup = Telegram::Bot::Types::ReplyKeyboardMarkup.new(keyboard: kb, one_time_keyboard: true)
    @bot.api.send_message(chat_id: message.chat.id, text: I18n.t('should_we_notify'), reply_markup: markup)
  end

  def send_chronicle_filter(message)
    @bot.api.send_message(chat_id: message.chat.id, text: I18n.t('selected_chronicles', x: @user.filter_chronicle.join(", ")))
  end

  def help(message)
    @bot.api.send_message(chat_id: message.chat.id, text: "#{I18n.t('command_list')}\n/soon\n/recent\n/lowrate\n/multicraft\n/pvprate\n/notify\n/help")
  end

  def info(message)
    @bot.api.send_message(chat_id: message.chat.id, text: "#{I18n.t('total_servers')} #{@all.length}")
  end

  def add_help_to_end(message)
    @bot.api.send_message(chat_id: message.chat.id, text: I18n.t('need_help'))
  end

  def soon(servers, days)
    servers.select do |server|
      date_diff(Time.now, server[3]) > 0 && date_diff(Time.now, server[3]) <= days
    end.sort do |s1, s2|
      date_diff(Time.now, s2[3]) <=> date_diff(Time.now, s1[3])
    end
  end

  def recent(servers, days)
    servers.select do |server|
      date_diff(Time.now, server[3]) < 0 && date_diff(Time.now, server[3]) >= -days
    end.sort do |s1, s2|
      date_diff(Time.now, s1[3]) <=> date_diff(Time.now, s2[3])
    end
  end

  def from_to(servers, from, to)
    servers.select do |server|
      date_diff(Time.now, server[3]) >= from && date_diff(Time.now, server[3]) <= to
    end.sort do |s1, s2|
      date_diff(Time.now, s1[3]) <=> date_diff(Time.now, s2[3])
    end
  end

  def apply_chonicle_filter(servers, chronicles)
    servers.select do |server|
      chronicles.include? server[1]
    end
  end

  def apply_rates_filter(servers, from, to)
    servers.select do |server|
      server[2].to_i >= from.to_i && server[2].to_i <= to.to_i
    end
  end

  def clear_filter
    @user.filter_chronicle = []
  end

  def filter(message)
    send_servers(
      from_to(
        apply_chonicle_filter(
          apply_rates_filter(
            @all,
            @user.filter_rates_from,
            @user.filter_rates_to
          ),
          @user.filter_chronicle
        ),
        -14,
        14
      ),
      message
    )
    add_help_to_end(message)
  end

  def date_diff(*dates)
    days = 0
    dates.each_with_index do |date, i|
      if date.is_a? Time then
        d = date.day + date.month*30 + date.year*365
      else
        parsed = date.split('.').map! { |x| x.to_i }
        d = parsed[0] + parsed[1]*30 + (parsed[2]+2000)*365
      end
      days += (i.even? ? -d : d)
    end
    days
  end
end
<file_sep>#!/usr/bin/env ruby
require 'telegram/bot'
require 'pg'
require 'json'
require 'time'
require_relative '../app/telegram-responder'
# Models
require_relative '../models/user'
# I18n
require 'i18n'
I18n.load_path = ["#{__dir__}/../locale/en.yml", "#{__dir__}/../locale/ru.yml"]

token = ENV["L2WATCHBOT_TOKEN"]
database  = PG.connect( dbname: "l2watchbot", user: "postgres", password: "<PASSWORD>" )

def date_diff(*dates)
  days = 0
  dates.each_with_index do |date, i|
    if date.is_a? Time then
      d = date.day + date.month*30 + date.year*365
    else
      parsed = date.split('.').map! { |x| x.to_i }
      d = parsed[0] + parsed[1]*30 + (parsed[2]+2000)*365
    end
    days += (i.even? ? -d : d)
  end
  days
end

def send_servers(api, servers, user)
  servers.each do |s|
    day_diff = date_diff(Time.now, s[3])
    day_part =  if (day_diff > 0) then
                  I18n.t('opens_in_x_days', x: day_diff)
                elsif (day_diff < 0) then
                  I18n.t('opened_x_days_ago', x: -day_diff)
                else
                  I18n.t('opens_today')
                end
    text = "#{s[0]}\n#{s[1]} x#{s[2]}\n#{s[3]} #{day_part}"
    api.send_message(chat_id: user[9], text: text)
  end
end

def from_to(servers, from, to)
  servers.select do |server|
    date_diff(Time.now, server[3]) >= from && date_diff(Time.now, server[3]) <= to
  end.sort do |s1, s2|
    date_diff(Time.now, s1[3]) <=> date_diff(Time.now, s2[3])
  end
end

def apply_chonicle_filter(servers, chronicles)
  servers.select do |server|
    chronicles.include? server[1]
  end
end

def apply_rates_filter(servers, from, to)
  servers.select do |server|
    server[2].to_i >= from.to_i && server[2].to_i <= to.to_i
  end
end


def add_help_to_end(api, user)
  api.send_message(chat_id: user[9], text: I18n.t('need_help'))
end

def filter(api, servers, user)
  send_servers(
    api,
    from_to(
      apply_chonicle_filter(
        apply_rates_filter(
          servers,
          user[4],
          user[5]
        ),
        JSON.parse(user[3])
      ),
      -14,
      14
    ),
    user
  )
  api.send_message(chat_id: user[9], text: I18n.t('your_feed'))
  add_help_to_end(api, user)
end

SECONDS_IN_MIN  = 60
MIN_IN_HOUR     = 60
SECONDS_IN_HOUR = SECONDS_IN_MIN * MIN_IN_HOUR

logger = Logger.new("#{__dir__}/../log/notifier.log", 'daily')

loop do
  begin
    api = Telegram::Bot::Api.new(token)
    servers = database.exec("SELECT * from servers").values
    users = database.exec("SELECT * from users").values
    users.each do |user|
      if (user[7].nil? && !user[8].nil?) || date_diff(Time.parse(user[7]), Time.now) >= user[8].to_i then
        filter(api, servers, user)
        database.exec "UPDATE users SET last_notified = $1 WHERE user_id = $2;", [Time.now.to_s, user[0]]
      end
    end
    p users
  rescue Exception => e
    logger.fatal e
    logger.fatal e.backtrace
  end
  sleep(SECONDS_IN_HOUR * 1)
end
<file_sep>source "https://rubygems.org"

# bot and crawler
gem 'telegram-bot-ruby'
gem 'mechanize'
gem 'i18n'
gem 'mail'

# database
gem 'pg'
gem 'json'

# fancy stuff
gem 'logger'
gem 'pry'
gem 'awesome_print'
<file_sep>#!/usr/bin/env ruby

require 'logger'
require_relative '../app/crawler'

SECONDS_IN_MIN  = 60
MIN_IN_HOUR     = 60
SECONDS_IN_HOUR = SECONDS_IN_MIN * MIN_IN_HOUR

logger = Logger.new("#{__dir__}/../log/crawler.log", 'daily')

loop do
  begin
    Crawler.new(logger: logger).run
  rescue Exception => e
    logger.fatal e
    logger.fatal e.backtrace
  end
  sleep(SECONDS_IN_HOUR * 3)
end
<file_sep>require 'json'

class User
  attr_reader :user_id, :filter_from, :filter_to, :filter_rates_from, :filter_rates_to, :lang, :last_notified, :notify_period, :chat_id

  def initialize(db, id)
    @db       = db
    values    = @db.exec("SELECT * from users WHERE user_id = $1", [id.to_s]).values[0]

    @user_id            = values[0].to_s
    @filter_from        = values[1].to_s
    @filter_to          = values[2].to_s
    @filter_chronicle   = values[3].to_s
    @filter_rates_from  = values[4].to_s
    @filter_rates_to    = values[5].to_s
    @lang               = values[6]
    @last_notified      = values[7].to_s
    @notify_period      = values[8].to_i
    @chat_id            = values[9].to_s
    puts values
  end

  def filter_from=(val)
    @db.exec "UPDATE users SET filter_from = $1 WHERE user_id = $2;", [val.to_s, @user_id.to_s]
    @filter_from = val
  end

  def filter_to=(val)
    @db.exec "UPDATE users SET filter_to = $1 WHERE user_id = $2;", [val.to_s, @user_id.to_s]
    @filter_to = val
  end

  def filter_chronicle
    @filter_chronicle.empty? ? [] : JSON.parse(@filter_chronicle)
  end

  def filter_chronicle=(val)
    val = val.uniq.to_json
    @db.exec "UPDATE users SET filter_chronicle = $1 WHERE user_id = $2;", [val.to_s, @user_id.to_s]
    @filter_chronicle = val
  end

  def filter_rates_from=(val)
    @db.exec "UPDATE users SET filter_rates_from = $1 WHERE user_id = $2;", [val.to_s, @user_id.to_s]
    @filter_rates_from = val
  end

  def filter_rates_to=(val)
    @db.exec "UPDATE users SET filter_rates_to = $1 WHERE user_id = $2;", [val.to_s, @user_id.to_s]
    @filter_rates_to = val
  end

  def lang=(val)
    @db.exec "UPDATE users SET lang = $1 WHERE user_id = $2;", [val.to_s, @user_id.to_s]
    @lang = val
  end

  def last_notified=(val)
    @db.exec "UPDATE users SET last_notified = $1 WHERE user_id = $2;", [val.to_s, @user_id.to_s]
    @last_notified = val
  end

  def notify_period=(val)
    @db.exec "UPDATE users SET notify_period = $1 WHERE user_id = $2;", [val.to_i, @user_id.to_s]
    @notify_period = val.to_i
  end

  def chat_id=(val)
    @db.exec "UPDATE users SET chat_id = $1 WHERE user_id = $2;", [val.to_s, @user_id.to_s]
    @chat_id = val.to_i
  end
end
<file_sep>#!/usr/bin/env ruby

require 'pg'

Dir.mkdir("#{__dir__}/../log") rescue puts 'Folder "log" already exists.'

database = PG.connect( dbname: "l2watchbot", user: "postgres", password: "<PASSWORD>" )

begin
  database.exec <<-SQL
    create table servers (
      title text,
      chronicles text,
      rates int,
      date text,
      created_at text,
      CONSTRAINT uniq UNIQUE(title, chronicles, rates, date)
    );
  SQL
rescue Exception => e
  puts e
end

begin
  database.exec <<-SQL
    create table results (
      date text,
      total_servers int
    )
  SQL
rescue Exception => e
  puts e
end

begin
  database.exec <<-SQL
    create table users (
      user_id text,
      filter_from int,
      filter_to int,
      filter_chronicle text,
      filter_rates_from int,
      filter_rates_to int,
      CONSTRAINT uniq2 UNIQUE(user_id)
    )
  SQL
rescue Exception => e
  puts e
end

begin
  database.exec <<-SQL
    ALTER TABLE users ADD COLUMN lang varchar(30);
  SQL
rescue Exception => e
  puts e
end

begin
  database.exec <<-SQL
    ALTER TABLE users ADD COLUMN last_notified text;
    ALTER TABLE users ADD COLUMN notify_period int;
  SQL
rescue Exception => e
  puts e
end

begin
  database.exec <<-SQL
    ALTER TABLE users ADD COLUMN chat_id text;
  SQL
rescue Exception => e
  puts e
end
<file_sep>#!/usr/bin/env ruby
require 'telegram/bot'
require 'pg'
require_relative '../app/telegram-responder'
# Models
require_relative '../models/user'
# I18n
require 'i18n'
I18n.load_path = ["#{__dir__}/../locale/en.yml", "#{__dir__}/../locale/ru.yml"]
I18n.enforce_available_locales = true
I18n.default_locale = :en

token = ENV["L2WATCHBOT_TOKEN"]
database  = PG.connect( dbname: "l2watchbot", user: "postgres", password: "<PASSWORD>" )

logger = Logger.new("#{__dir__}/../log/bot.log", 'daily')

Telegram::Bot::Client.run(token) do |bot|
  responder = TelegramResponder.new(bot, database)
  bot.listen do |message|
    begin
      database.exec "INSERT INTO users (user_id, chat_id) values ($1, $2)", [message.from.id.to_s, message.chat.id] rescue nil
      responder.respond_to message
    rescue Exception => e
      logger.fatal e
      logger.fatal e.backtrace
    end
  end
end
<file_sep>require 'mechanize'
require 'pg'
require 'logger'
require 'mail'

require 'awesome_print'
require 'pry'


class Crawler
  SOURCE_WEBSITE = "http://l2tops.ru/"

  def initialize(logger:)
    @logger    = logger
    @database  = PG.connect( dbname: "l2watchbot", user: "postgres", password: "<PASSWORD>" )
    @agent     = Mechanize.new
  end

  def run
    connect
    extract_nodes_from_page.each do |sever_node|
      begin
        @database.exec(
          "insert into servers (title, chronicles, rates, date, created_at) values ($1, $2, $3, $4, $5)",
          extract_data_from_node(sever_node)
        )
      rescue Exception => e
        @logger.warn "#{e} for page:\n #{sever_node}"
      end
    end
    save_results
  end

  private

  def connect
    @page = @agent.get(SOURCE_WEBSITE)
  end

  def extract_nodes_from_page
    @page.search('.server')
  end

  def extract_data_from_node(node)
    title       = node.at('.name').text
    chronicles  = node.at('.chronicle').text
    rates       = node.at('.rates').text[1..-1].to_i
    date        = node.at('.date').text
    [title, chronicles, rates, date, Time.now.iso8601]
  end

  def save_results
    server_count = @database.exec("select COUNT(*) from servers").values.first.first.to_i
    begin
      @database.exec(
        "insert into results (date, total_servers) values ($1, $2)",
        [Time.now.iso8601, server_count]
      )
      @logger.info "Total servers: #{server_count}"
      send_warn_email(server_count)
    rescue Exception => e
      @logger.error "When saving results: #{e}"
    end
  end

  def send_warn_email(server_count)
    return if server_count > 50     # meh
    mail = Mail.new do
      from    '<EMAIL>'
      to      '<EMAIL>'
      subject 'L2Watchbot Crawler warning!'
      body    "While scanning #{SOURCE_WEBSITE} at #{Time.now.to_s} found only #{server_count} servers."
    end
    mail.delivery_method :smtp, {openssl_verify_mode: "none"}
    mail.deliver!
    @logger.warn mail.to_s
  end
end
