Coding: Ruby

 

RUBY: установка через rbenv
apt update
apt install -y git curl build-essential libssl-dev \
libreadline-dev zlib1g-dev libyaml-dev libffi-dev

git clone https://github.com/rbenv/rbenv.git ~/.rbenv
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(rbenv init - bash)"' >> ~/.bashrc
export PATH="$HOME/.rbenv/bin:$PATH"
eval "$(rbenv init - bash)"

git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)/plugins/ruby-build"

rbenv install -l # можно посмотреть другие версии

rbenv install 3.3.12
rbenv global 3.3.12
rbenv rehash

ruby -v
Ruby: установка и управление gem в Ruby

www.rubygems.org – основной сайт с гемами

Ubuntu/Centos/Gentoo

Установка ruby/gems:

sudo apt-get/yum/emerge install ruby
sudo apt-get/yum/emerge install rubygems

Гемы обычно ставятся через утилиту gem – она заведует поиском/установкой/удалениями гемов и имеет другие полезные команды. Краткий ман. Утилита поддерживает мультиверсионность гемов – если последняя вышедшая версия используемого вами gem не совместима с той, которую вы используете сейчас, то можно использовать обе версии одновременно.  Так же установить gem можно указав URL (часто в репозитории гемы не актуальны), но для этого нужно сначала скачать gem specific_install.

sudo gem install snmp/net-ping/builder/spreadsheet/mechanize # установка самой последней версии (RubyGems по умолчанию выберет последнюю)
sudo gem install builder -v 1.1.0 # установка конкретной версии
gem environment – куча инфы (версия руби и гемов, где лежит руби и гемы и прочее)
sudo gem install specific_install # устанавливаем gem для возможности прямой установки по URL
sudo gem specific_install https://github.com/telegram-bot-rb/telegram-bot.git # устанавливаем gem telegram_bot

Так же для установки могут использоваться собранные пакеты с гемами в стандартных репозиториях ОС. Способ хуже. Пригодно для установки только для популярных гемов, например в Gentoo такие гемы ставятся через emerge (-vp проверяем отсутствие конфликтов с опцией pretend, -v – устанавливаем). Точные названия можно посмотреть на gentoo.org

sudo emerge -vp dev-ruby/mechanize
sudo emerge -vp dev-ruby/ruby-oci8
sudo emerge -vp phantomjs

После установки gem, достаточно написать команду require<имя gem>, чтобы подключить его. Т.е. работа с гемами не отличается от работы с обычными библиотеками.

require 'net/ping'

Если после этого пишет “no such file to load — <gem_name>”, а gem при этом установлен локально – может помочь строчка require ‘rubygems’.

/home/redkin_p/bin/test.rb:6:in `require': no such file to load -- snmp (LoadError)
 from /home/redkin_p/bin/test.rb:6

Другие команды

gem list |snmp/builder| # список гемов 
gem list --remote # показывает все хдоступые гемах в репозитории, а не локально (как без опции) 
gem list --remote --name-matches net-ping # ищем конкретный гем 
sudo gem uninstall net-ping # удаление GEM
sudo gem cleanup rjb # удаление старых версий
sudo gem uninstall telegram-bot-ruby --version 0.8.6.1 # удаление определенной версии
gem environment # тут можно посмотреть много разной инфы. К примеру очень полезен путь до гемов, исполняемый ruby

Пример разбора ошибок установки gem из-за системных библиотек и других зависимых gem есть в статье про mechanize.

Если Ruby установлен через RVM:

rvm 2.4.0 # переходим в RVM
gem install unicode # ставим gem

 

 

 

Ruby простая отправка писем
 
Простая отправка писем в ruby без вложений:
#!/usr/bin/env ruby
#coding: utf-8
 
require 'net/smtp'
 
from = 'login@exmpl.com'
to = [ "mail_to@exmpl.com", "mail_to2@exmpl.com" ]
theme = 'Subj!'
text= "Msg"
message = ""
message<<"From: admin <#{from}>\n"
message<<"To: #{to}\n"
message<<"Subject: #{theme}\n"
message<<text
Net::SMTP.start('smtp.exmpl.com', 25, 'exmpl.ru',
 'login', 'pass', :plain) do |smtp|
smtp.send_message message, from, to
end
gem mail поддерживает отправку с вложениями. Нужно учитывать что gem требует минимум второй версии ruby, а для gentoo, если ставить system wide, в стандартных репозиториях нету stable релиза.
 
 
 
Ruby список операторов (присвоения, сравнения, проч)

см. тут

 
 
Ruby: работа с текстом

 String Class

Индексы

Строка в ruby является объектом класса String.  По индексу строки, по аналогии с индексом массива, можно извлекать, изменять и удалять данные из строки. Причем обращения по индексу идет через те же квадратные скобки, что и для массива. Обращаясь к строке можно извлечь например первые 6 элементов или последний символ на основе индекса строки, по типу того, как мы извлекаем элементы array:

str[start, lenght]
str[0,6] - извлекаем первые 6 элементов, где 0 - первый символ, 6 - сколько символов от первого
str[7..10] - извлекаем с 7го по 10ый символы используя range
str[-1] - извлекаем последний символ из строки
str[-6,6] - извлекаем с конца строки шесть последний символов
str[-6..-1] - извлекаем с конца строки шесть последний символов используя range

Кроме получения данных по индексу можно эти данные и менять/удалять/добавлять:

str[0,6] = "new_first_6_char" - заменяем первые 6 элементов на новые
str[0,6] = "" - удаляем
str[16,0] = "the end" - добавляем в конец строки данные (16 символ последний)
str[-1,0] = "the end" - добавляем в конец строки данные (более корректный способ)
Методы

Почти все в ruby является объектом. Каждая строка является объектом класса string и к ней могут быть применены методы этого класса.

ПРОСМОТР

.dump – показываем все символы, включая специальные .count(“e”) – считаем сколько раз встречается буква “e” в строке .length – количество символов. Может быть использовано для базовых проверок полученных переменных (MAC, кредитка)

irb(main):017:0* "My name is Petr".length

=> 15

Выводим элементы массива, длина которых меньше определенного количества символов.

puts res.select {|x, l| x.length < 10 }
Поиск

.index – поиск начала позиции (индекса первой буквы) по строке в строке.

irb(main):001:0> str = "Fdffdfd lolkf fdsfdsf"
=> "Fdffdfd lolkf fdsfdsf"
irb(main):002:0> str.index("lol") 
=> 8
irb(main):003:0> puts str[8,3]
lol
=> nil

.include? (str) – ищет подпадания указанной строки в указанной переменной строки. В результате выдает true/false.

We can do that using Ruby's .include? method, which evaluates to true if it finds what it's looking for and false otherwise.

(As a general rule, Ruby methods that end with ? evaluate to the boolean values true or false.)
irb(main):027:0* puts str.include?("test")
false
=> nil
irb(main):028:0> puts str.include?("lol") 
true
=> nil
Замещение

.sub(“from”, “to”)  – для замен первого подпадения (substitution) на заданное (аналог sed без g). Для замены в той же переменной нужно использовать sub!

.sub(/!|'/,"") # подменяет первый ! на ничего, остальные остаются

@fw = snmp_get(@OID_FW).sub(/Build /, "")

.gsub(“from”, “to”)  – для замен всех подпадений  (global substitution) на заданное (аналог sed с g). Для замены в той же переменной нужно использовать gsub!

.gsub(/!|'/,"") # подменяет все ! на ничего

feed = args.join(" ").sub(/f |F /,"").gsub(/!|'|"/,"")
Модификация

.capitalize – делает первый символ слова upcase, остальные downcase .upcase/.downcase – понятно. Может быть использовано для приведения переменных к единому формату для облегчения проверок/стандартизации вывода.

We want to make sure we capture both "S" and "s" in the user's input. We could write separate if / else statements to handle this, but we can also use .downcase! to convert the user's input to all lower case. That way, we only have to search for "s".

.reverse – экранирование текста задом-наперед. Можно использовать в том числе и для экранирования массива.

irb(main):018:0> "My name is Petr".reverse
=> "rteP si eman yM"

colors = {"blue" => 3, "green" => 1, "red" => 2}
colors = colors.sort_by do |color, count|
  count
end
colors.reverse!

>
blue
3
red
2
green
1
Удаление

.chomp – удаление спец. символов с конца строки (напр. \n, \r, \r\n)

\r\n
\r - carriage return, курсор должен быть перемещен в начало строки
\n - newline separator, курсор должен быть перемещен на новую строку

.chomp(“end.”) – удаляем символы “end.” с конца строки .chop – удаляем последний символ в строке, который не относится к служебному .strip – офигенный метод класса String. Удаляет whitespace characters с начала и конца строки в ruby. Может например помочь любимый для Windows .txt символ ^M с конца строки (обнаружить просто через cat -v <file>).

.strip will remove all extra whitespace from the front and back, leaving innards alone.

  Разное Если выдает UTF-8 regexp with ASCII-8BIT string, то или строку нужно привести в UTF-8 (правильно) или regexp в ASCII.

str.force_encoding("utf-8")

 

Ruby: работа с массивами

Сортировка hash reverse по значению в ruby используя sort_by.

hash_sorted = Hash[hash.sort_by {|key, value| value}.reverse!]
Iteration
 
each_with_index – из названия понятно, что полезно когда нужен элемент массива и его индекс)
 
Удаление из массива элементов
 
.drop(1) – удалить первый элемент (если указать 10 – первые 10 элементов)
a.delete(‘test’) – удалить по значению
arr[0..-3] – удалить последние два элемента
 
 
Элемент по ID
 
В Ruby, как, в принципе, и в PHP,  можно натравить метод какой-то на массив и элемент массива получить в одной строке.
number = array_slice(arr, -1, 1)[0];
Иногда это помогает значительно уменьшить количество строк кода:
date = res.split("<th>Текст<\/th>")[1].split("\n")[3].sub("</td>","").sub(/<(.*)>/,"").strip.split(" ")[0]
Первый, второй, последний, предпоследний элементы
.first(1) # Первый элемент
.first(2).first(1) # Второй элемент
.last(1) # Последний элемент 
.last(2).first(1) # Предпоследний элемент (можно так же запускать на массив из одного элемента)
intersection Крутая штука intersection – смотрим без итераций есть ли (и если есть, то какие) перекликания в элементах массива.
# простой пример
([2, 6, 13, 99, 27] & [2, 6]).any?

# пример пересечения массива MAC-адресов из таблицы коммутации с массивом заданных MAC
intersect = (marr & macs) 
if !intersect.empty?
 mac = intersect.join(",")
 return [ "success", "#{ip}", "#{p}", "#{mac}" ] if marr.include? "#{mac}"
end

Hash.new(0) h = Hash.new(0) – позволяет указать дефолтное значение для всех ключей в виде 0. К примеру, это полезно если ключ определяет что-то, а значение инкрементируется – таким образом не нужно определять значение каждого нового ключа. В результатме мы можем сразу использовать конструкцию для подсчета типа:

h = Hash.new(0)
h["admin"] += 1

SELECT/FIND/FIND_ALL .find и .find_all – методы поиска в массиве не ограничиваются .select, есть так же .find и .find_all.

perl = a.find { |a| a[0] =~ /^Test/ } # выведет первый массив, который соответствует pattern
perl = a.find_all { |a| a[0] =~ /^Test/ } # выведет все массивы, которые соответствуют pattern

.map – позволяет проделать операцию с каждым элементом массива без итерации. Очень удобно. К примеру одна строка:

 IPAddr.new("#{ip}/#{mask}").to_range.map { |ip_obj| ip_object.to_s }

Замещает 4 строки:

ip_range = []
IPAddr.new("#{ip}/#{mask}").to_range.each do |ip_obj| 
  ip_range.push(ip_obj.to_s)
end

.select – .select позволяет делать условия не только с сравнением string, regexp, но и любые кастомные, получая из array новый array – к примеру без итераций .each можно проверить доступность ip-адресов и сформировать массив из только доступных. В результате как выше с .map: Одна строка:

ip_range.select { |ip| Net::Ping::Extrenal.new(ip).ping? }

Замещает 7 строк:

active_ip = []
ip_range.each do |ip_addr|
  if Net::Ping::Extrenal.new(ip).ping?
    active_ip.push(ip_addr)
  end
end
active_ip
Ruby HTTP get/post

Simple get Простая функция для отправки http get.

def get(request)
request = CGI::escape(request)
uri = URI("http://#{$server}/#{request}")
Net::HTTP.get(uri)
end

Get with header Пример запроса с header. Обращаю внимание, что в старой версии ruby 1.9.3 обязательно нужно к uri применить метод .request_uri, в новых не обязательно.

uri = URI("http://test-api.net/login=#{login}")
req = Net::HTTP::Get.new(uri.request_uri)
req['Auth-Token'] = "my_token"
res = Net::HTTP.start(uri.hostname, uri.port) {|http|
  http.request(req).body
}

Get with timeout Если при http_get выдает ошибку Timeout rescue in rbuf_fill – пробуем увеличить таймаут. Так же в каких-то кейсах имеет смысл выставить max_retries в 2-3 (по умолчанию 1). Код сложнее первого и не так очевиден.

net/protocol.rb:141:in read_nonblock
net/protocol.rb:141:in rbuf_fill
net/protocol.rb:122:in readuntil
net/protocol.rb:132:in readline
net/http.rb:2563:in read_status_line
net/http.rb:2552:in read_new
net/http.rb:1320:in block in transport_request
net/http.rb:1317:in catch
net/http.rb:1317:in transport_request
net/http.rb:1294:in request

def get_new(request)
request = CGI::escape(request)
uri = URI("http://#{$server}/#{request}")
request = Net::HTTP::Get.new(uri.path)
res = Net::HTTP.start(uri.hostname, uri.port, :read_timeout => 500) do |http|
http.request(request)
end
res.body
end

 

Ruby: парсинг и модификация WEB страниц с Nokogiri и Mechanize

Парсить страницу через curl + regexp можно, но только в простых сценариях. В противном случае проще и надежнее использовать библиотеки.

Mechanize и Nokogiri похожи. Насколько я понимаю Mechanize использует Nokogiri для своей работы. Оба Gem могут использовать html объекты для парсинга/манипуляции (nokogiri так же может работать с xml). Основной косяк их обоих – отсутствие поддержки js. Для этого нужно использовать phantomjs или что-то новое вроде headless chrome.

Пример вывода title для страницы с nokogiri.

require 'nokogiri'
require 'open-uri'

html_page = Nokogiri::HTML(open("http://www.weril.me/"))
puts html_page.title

Можно искать в тегах.

puts html_page.search("p")
Cookies
Использование cookie файла для mechanize очень удобная штука. Особенно когда авторизация по JS с куки файлом и механайз просто ее пройти не сможет.
 
Генерируем куки
`/usr/bin/phantomjs --cookies-file=cookies.txt --ignore-ssl-errors=true --load-images=false --ssl-protocol=any login.js <login> <pass>`
Используем
cookie_file = 'cookies.txt'
a = Mechanize.new
cookie_val = `grep cookies #{cookie_file} | sed 's/.*dmn=//' | sed 's/;.*//'`.strip
cookie = Mechanize::Cookie.new :domain => 'mydomain.net', :name => 'domain_id', :value => "#{cookie_val}", :path => '/', :expires => (Date.today + 1).to_s
a.cookie_jar << cookie
Ошибки Mechanize

Ошибка “SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed ” или “`post_connection_check’: hostname does not match the server certificate (OpenSSL::SSL::SSLError)” при парсинге

Возникала на старых версиях ruby. Причем возникала не постоянно. Для лечения обновить версию или отключить проверку SSL.

a.verify_mode = OpenSSL::SSL::VERIFY_NONE 

Ошибка “Failed to build gem native extension” при установке Ruby Gem’а Mechanize (mkmf.rb can’t find header files for ruby at /usr/lib/ruby/ruby.h или другие причины)

Mechanize требует огромное количество зависимостей для корректной установки, без них установка будет завершаться ошибкой “Failed to build gem native extension” с различными причинами в описании.

Fix на ubuntu/centos:

sudo apt-get install ruby-dev gcc g++ make
sudo apt-get install build-essential libstdc++6
sudo apt-get install zlib1g-dev
sudo yum install zlib-devel gcc gcc-c++ ruby-devel rubygems

Может так же не ставится из-за того, что другие гемы требуют ruby большей версии, чем та, которая установлена в системе. Тогда обновляем ruby (лучше) или ставим те версии, которые поддерживают наш ruby.

ubuntu@ip-172-31-21-155:~$ sudo gem install mechanize
Fetching: net-http-digest_auth-1.4.1.gem (100%)
Fetching: mime-types-data-3.2018.0812.gem (100%)
ERROR: Error installing mechanize:
mime-types-data requires Ruby version >= 2.0.

ubuntu@ip-172-31-21-155:~$ sudo gem install mechanize -v 2.7.3
Building native extensions. This could take a while...
Fetching: unf-0.1.4.gem (100%)
Fetching: domain_name-0.5.20180417.gem (100%)
Fetching: http-cookie-1.0.3.gem (100%)
Fetching: mini_portile2-2.4.0.gem (100%)
Fetching: nokogiri-1.9.1.gem (100%)
ERROR: Error installing mechanize:
nokogiri requires Ruby version >= 2.1.0.

ubuntu@ip-172-31-21-155:~$ sudo gem install nokogiri -v 1.6.6.2
ubuntu@ip-172-31-21-155:~$ sudo gem install mechanize -v 2.7.3

 

Ruby: простой Web Server на базе WEBrick

В Ruby есть встроенный сервер WEBrick. У него есть довольно богатый функционал, но юзать его не рекомендуют (как минимум из-за отсутствия документации и однопоточности). Для целей production могут использоваться другие ruby-сервера: puma, unicorn, rainbows; но по факту лучше использовать nginx :).

WEBrick::HTTPAuth
WEBrick::HTTPAuth::Authenticator
WEBrick::HTTPAuth::BasicAuth
WEBrick::HTTPAuth::DigestAuth
WEBrick::HTTPAuth::Htdigest
WEBrick::HTTPAuth::Htgroup
WEBrick::HTTPAuth::Htpasswd
WEBrick::HTTPAuth::ProxyAuthenticator
WEBrick::HTTPAuth::ProxyBasicAuth
WEBrick::HTTPAuth::ProxyDigestAuth
WEBrick::HTTPAuth::UserDB
WEBrick::Cookie

Поднимаем простой http сервер. Взято прямо из wiki и rubydoc.

#Include WEBrick class with require
require 'webrick'

#FileHandler servlet provides the option to choose which files from user to serve
#The following code shows how to serve them from the folder '~/'
root = File.expand_path '~/'

#Instantiating a new server with HTTPServer.new on port 1234 serving the documents from root folder
server = WEBrick::HTTPServer.new :Port => 1234, :DocumentRoot => root

#The following proc is used to customize the server operations
server.mount_proc '/' do |request, response|
 response.body = 'Hello, world!'
end

#The following command will provide a hook to shutdown the server (often done with Ctrl+C)
trap('INT') {server.shutdown}

#Start the server
server.start

Поднимаем https сервер с заданным сертификатом.

#!/usr/bin/env ruby
#coding: utf-8

# Created by Petr V. Redkin

#Include WEBrick class with require
require 'webrick'
require 'webrick/https'
require 'openssl'

#FileHandler servlet provides the option to choose which files from user to serve
#The following code shows how to serve them from the folder '~/'
root = File.expand_path '~/'

#Instantiating a new server with HTTPServer.new on port 1234 serving the documents from root folder
cert = OpenSSL::X509::Certificate.new File.read '/etc/letsencrypt/live/my_site.net/cert.pem'
pkey = OpenSSL::PKey::RSA.new File.read '/etc/letsencrypt/live/my_site.net/privkey.pem'
server = WEBrick::HTTPServer.new(:Port => 1234,
 :SSLEnable => true,
 :SSLCertificate => cert,
 :SSLPrivateKey => pkey)

#The following proc is used to customize the server operations
server.mount_proc '/' do |request, response|
 response.body = 'Hello, world!'
end

#The following command will provide a hook to shutdown the server (often done with Ctrl+C)
trap('INT') {server.shutdown}

#Start the server
server.start

 

Ruby json

Ruby json

require 'json'

encode
'hello'.to_json

decode
JSON.parse(some_json)
Ruby: генератор списка выходных на год

Простейший генератор списка выходных на год. Что можно кастомизировать (жирным в коде):

  • В переменной cur_date указываем дату отчета
  • В счетчике можно подкрутить количество дней с 365 до любого количества
  • Строку вывода можно заменить генерацией INSERT в какую то SQL таблицу (закоменчено)
#bin/generate_vacation_list.rb

require 'time'

cur_date = Date.new(2019,1,1)
counter = 0
while counter < 365
 puts "#{cur_date}" if cur_date.saturday? == true || cur_date.sunday? ==  true
 #puts "INSERT INTO <tb> (vac_date) VALUES (to_date('#{cur_date}','YYYY-MM-DD'));" if cur_date.saturday? == true || cur_date.sunday? == true
 cur_date+=1
 counter+=1
end

Далее на основе производственного календаря к этим данным можно добавить праздники и исключить часть “ложных” (в 2019 таких нет) выходных.        

Ruby: разные методы по работе с сетью, работа с сетью (библиотеки/гемы IPAddr, net-ping, net-snmp, MAC)
Ruby ip addr class
Используя стандартную библиотеку Ruby IPaddr можно работать с IP (как IPv4, так и IPv6) адресами – проверка формата, генерация array на основе Ip/mask, проверка относится ли хост к сети. Очень удобно.
 
Метод для проверки формата полагается на rescue создания объекта в классе IPaddr. Используя !! происходит конвертация значения (успешно созданного объекта класса IPaddr) в true.
require "ipaddr"

def ip?(ip)
 !!IPAddr.new(ip) rescue false
end

puts ip?("192.168.0.1")
puts ip?("192.168.1111.1")
.to_range –  используя .to_range в связке с .map (вместо .map можно сделать простой .to_a, но тогда в array будет объекты с ip/mask) можно сгенерировать array из ip на основе ip/mask.
require "ipaddr"

def calc_net_range(ip, mask)
 IPAddr.new("#{ip}/#{mask}").to_range.map { |ip_obj| ip_obj.to_s }
end

print calc_net_range("192.168.1.0", "255.255.255.0").inspect

.include? – можно использовать для проверки относится ли определенный хост к сети.

require 'ipaddr'
net1 = IPAddr.new("192.168.2.0/24")
net2 = IPAddr.new("192.168.2.100")
net3 = IPAddr.new("192.168.3.0")
p net1.include?(net2) #=> true
p net1.include?(net3) #=> false

 

net-ping

Популярная и поддерживаемая (что очень важно) библиотека для проверки по ICMP хостов. ВЗЯТЬ ИЗ NET_QUERY.RB  

MAC
Приведение к единому формату
def mac_normalize(mac)
  mac.gsub(/!|’|”|-|:|;|\./,””).downcase
end
 
Проверка
def check_mac(mac)
  return “MAC \”#{mac}\” имеет длину не равную 12 символам” if mac.length != 12
  return “MAC \”#{mac}\” включает русские символы” if mac =~ /[а-я]/i
  return “MAC \”#{mac}\” включает невозможные в MAC латинские символы” if mac =~ /[g-z]/i
  return “MAC \”#{mac}\” является адресом из примера” if mac == “a0b3cceb0fed” || mac == “d4dccda0b283”
  “OK”
end
 
 
 
Ruby: регулярные выражения
Основная статья по регулярным выражениям тут. .match – очень прокаченный метод в ruby для работы с регулярными выражениями.
/[0-9]/.match("2")
Например, к результату match можно применять методы .pre_match, .post_,match – получая данные ДО и ПОСЛЕ match – очень полезно, не городя костылей с заменой вроде <regexp>.*.
regex = /some_regexp/
puts regex.match("#{some_text}").pre_match
.scan(/<regexp>/) – итерация на основе regexp каждого символа в строке
"Ruby".scan(/./) do |letter|
  puts letter
end
.split – может принимать regexp как аргумент для разбиения строки в массив
octets = "192.168.1.1".split(/\./)
 
Ruby: работа в командной строке с ruby -e
Ruby можно вызывать из shell используя опцию -e, не указывая файл с кодом, а прямо из командной строки. По сути вызов irb из консоли. Очень крутая штука. Если строк несколько – то делить их нужно через “;”.
~$ ruby -e 'puts 1+1'
2
~$ ruby -e 'puts 1+1;puts 1+2'
2
3
Так же Ruby в командной режиме может использоваться по аналогии с pipeline с опцией -n – берем STDIN как входящий поток для кода и применяем его для каждой строки STDIN, пока их не останется (аналог bash while read | do). Каждая строка хранится в специальной переменной $_.
~$ cat sw
apple
orange
melon
tomato
~$ cat sw | ruby -ne 'puts $_ if $_.include?("tomat")' 
tomato
~$ cat sw | ruby -ne 'puts $_ if $_.include?("e")' 
apple
orange
melon
 
Ruby: тестирование
Про exceptions в отдельной статье. .inspect – показывает данные. Очень удобная вещь при debug вместо использования puts/print на переменную.

 

Ruby: специальные символы

#{} – string enterpolation ! – методы заканчивающиеся на ! замещают результат оперируемой переменной (gsub!) !! – конвертация значения в boolean (true/false)

def is_ip?(ip)
!!IPAddr.new(ip) rescue false
end

? – методы заканчивающиеся на ? генерируют (обычно) boolean результат (include?)

 

 

Ruby: работа с ФС (файлами и папками)

ФАЙЛЫ

дока

Прежде всего нужно обозначить, что закрытие файла крайне необходимый процесс:
1) открытый файл может быть заблокирован файловой системой для других скриптов
2) при открытии создается file descriptor, их количество ограничено файловой системой, хотя и обычно значение ограничения довольно большое
3) могут произойти race conditions, когда несколько процессов пытаются прочитать/записать один и тот же ресурс (в данном случае файл) в один момент времени

Чтение и вывод

# вывод контента (первой строки)
File.open("test.txt") do |file| # открытие файла
 puts file.gets # вывод контента (первой строки)
end # автоматическое закрытие по завершению метода Open, про необходимость закрытия теория выше

# Вывод всех строк
File.open("test.txt").each do |line|
 puts line
end

# Кладем строки файла в array, где каждая новая строка является отдельным элементом массива. Этот метод не рекомендуется использовать если файл большой и нам не нужно сохранять все строки для последующей обработки, а нужно сохранить/изменить только ряд - в таком случае лучше использовать последовательное чтение без сохранения в массив, чтобы не выделять лишнюю память на большое количество данных.
array_of_lines = File.readlines("test.txt")

Аналогичный по функции пример, с созданием объекта в File class (проще использовать предыдущий метод):

file = File.new("test.txt") # открытие файла
puts file.gets # вывод контента (первой строки)
file.close # закрытие, про необходимость закрытия теория выше

Запись

Запись данных с перезаписью контента (w - write)
File.open("test.txt", "w") do |file|
  file.puts "I'm writing line"
end

Разбор строк:
1) File.open("test.txt", "w") do |file| # открытие файла в режиме записи ("w"), по умолчанию r, список всех возможностей тут (r, r+, w, w+, a, a+)
2) file.puts "I'm writing line" # ввод контента в файл (не последней строкой, а ПОЛНЫМ замещением текущих данных в файле), если файла не существует - он будет создан
3) end # автоматическое закрытие по завершению метода Open, про закрытие подробнее выше

Запись данных добивкой, без перезаписи контента (a - append)
File.open("test.txt", "a") do |file| 
  file.puts "I'm writing line" 
end 

Можно сократить до одной строки (полезно для debug)
File.open($log_file, 'a') { |f| f.puts "#{$cur_dt} --- EXCEPTION #{Time.now} mess #{mess} err #{err}" } if $debug == 1

Другие

полезные методы для работы с файлами

# Удаление файла
File.delete("file_name_to_delete")

# Удаление ряда файлов
files_to_delete = ["test1.txt", "test2.txt", "test3.txt"]
files_to_delete.each do |file|
 File.delete(file)
end

# Rename файла
File.rename("file_name_to_rename", "file_name_new")

# Проверка на существование файла
if File.exists?("test.txt")
 puts "File test.txt exist!"
end

# Получаем полный путь файла (по умолчанию директории из которой происходит запуск скрипта + имени файла) на основе относительного
puts File.expand_path("test.txt") 
/var/www/test.txt

# Получение имени без расширения и пути
puts File.basename("/var/www/test.txt", ".txt")
test

# Получение директории без имени файла
puts File.dirname("/var/www/test.txt") 
/var/www

# Получение атрибутов файла
File.size("test.txt") # размер файла в байтах
File.mtime("test.txt") # dt последней модификации

ПАПКИ

дока

# Показать текущий относительный путь, по умолчанию равен директории из которой происходит запуск скрипта
puts Dir.pwd
/home/user

# Изменение относительного пути - можно указать полный путь или будет изменение на текущий относительный + новый
puts Dir.pwd
dir.chdir("new_dir")
/home/user/new_dir

# Создаем новую директорию
Dir.mkdir("new_dir")

# Удаляем директорию, директория должна быть пустой (не иметь файлов или других директорий в ней)
Dir.delete("new_dir")

# Проверяем пустая ли директория
Dir.empty?("new_dir")

# Итерация по папке с получением имен файлов и директорий в заданной папке (., .. - в элементах это норма, это указатели текущей и корневой директорий). Во вложенных директориях файлы показаны не будут (не рекурсивно).
Dir.foreach("test_dir") do |entry|
 puts entry 
end

# Тоже самое что выше, но умеет работать рекурсивно и фильтровать выборку.
contents = Dir.glob("**/*") # ** - рекурсия, * - условия выборки aka wildcard (* - никаких, *.txt - только .txt)
contents.each do |entry|
puts entry 
end

# Получаем массив из имен файлов и директорий в заданной папке (., .. - в элементах это норма, это указатели текущей и корневой директорий)
Dir.entries("test_dir")

 

Ruby: вызов внешних команд

Дока

Правило: в общем случае лучше не использовать вызов внешних команд, а попробовать положиться на standart libraries/gem/modules native ruby.

Недостаток в подходе вызова внешних команд очевиден – мы полагаемся на внешние пути, переменные в ОС и скрипты, будь то системные или написанные на других языках, а ошибки из-за этих проблем зачастую сложно обнаружить.

Исключения возможны, например:

  • если в ruby нет достойной библиотеки или она по какой-то причине хуже того, что можно вызывать
  • если задачу нужно решить быстро

Процесс вызова внешней команды делается используя подпроцесс (subprocess, child process) – ruby создает подпроцесс по отношению к основному (parent process) при вызове внешней команды. В этом процессе создается второй shell, который исполняет команду. Причем при использовании большинства методов вызова внешних процессов, пока внешний подпроцесс не отработает, основной процесс находится в режиме блокировки (blocked) – т.е. он не делает ничего, пока процесс не закончится. Только после того как child процесс закончится, flow control вернется к процессу parent и скрипт продолжит стандартное исполнение. Исключением из этого правила является вызов внешнего процесса через Fork – тогда Ruby интерпретатор не будет ждать отработки подпроцесса.

Для вызова внешний команд можно использовать:

System метод – берет имя команды как аргумент, возвраещает true если команда успешна (команда вернула 0 в exit status) и false если нет (exit status не равен 0). Отлично подходит если нужно знать только успешна команда или нет.

 result = System("chmod", "777", "file.txt") # аналог chmod 777 file.txt

 

`<command>` или %x(<command>) – запись команды в обратных ковычках. Отлично подходит на случай если нужен только STDOUT от child процесса и падение основного скрипта не критично. В отличии от System метода возвращает весь STDOUT после отработки child процесса, а true/false на основе exit status. Узнать exit статус в таком случае мы можем используя переменную $?, которая сохраняет значение exit status последнего child процесса. Применив к этой переменной метод .success можно получить информацию о успешности последней команды в виде true/false, по аналогии с System методом.

output = `ls` # аналог output = %x(ls)
puts output
puts "success" if $?.success?

При использовании подпроцессов через ковычки и %x, нужно учитывать что ошибка в подпроцессе (например, вызов утилиты, которая не установлена) по умолчанию приводит к exception не только в подпроцессе, но и в основном коде. Workaround есть – перенаправить STDERR в тот же поток что и STDOUT.

output = `not_existed_process 2>&1`
 puts output

При использовании этого workaround  нужно обязательно проверять exit status подпроцесса, иначе мы можем не знать о каких-то ошибках в скриптах (silent failure). silent failure нужно избегать всегда. Поэтому если нам не нужны ошибки основного процесса из-за падения подпроцесса лучше не использовать костыли, а использовать Open3.

Open3 (дока) – самый богатый способ вызова внешних команд т.к. он имеет метод capture3, который возвращает STDOUT, STDERR, EXIT code после завершения child process без каких то костылей в виде redirection, как в случае ковычек. Самый лучший вариант если нам нужны все три вывода или даже два из трех. Open3 является стандартной библиотекой, не требует установки, но требует require т.к. он не включен в код Ruby core. Еще одним преймуществом Open3 является то, что он не передает exception от subprocess в корневой, что делается при использовании ковычек. Для приема данных удобнее всего использовать multiple assignment – определение нескольких переменных одной командой.

require 'open3'

stdout, stderr, status = Open3.capture3("ls", "-l")
 puts stdout
 puts stderr
 puts status.success?

 

Ruby: работа с датой и временем
TIME

Класс Time

Получить hours:minutes (h:m) из Time.now можно по разному

1) лучший и самый универсальный способ через strftime

h_m = Time.now.strftime("%H:%M") # можно выцепить все что угодно, не только h:m, формат %d/%m/%Y %H:%M

2) хуже

h_m = "#{Time.now.hour}:#{Time.now.min}"

3) плохой

time_and_date = "#{Time.now}".split(" ")
h_m = "#{time_and_date[1]}".split(":")
h_m = "#{query_time[0]}:#{query_time[1]}"
 

Парсим date_time из string

require 'time'

dt_string = "20.03.2018 20:15:36"
dt_object = Time.parse(dt_string)

Добавляем часы в date_time object

hours = 14
dt_result = dt_object + hours*60*60
puts dt_result

Находим SEC и MIN между двумя объектами времени (между now и dt_object)

sec = Time.now - dt_object
min = Time.at(sec).min

Сравниваем время

a = Time.parse("10:00")
b = Time.parse("09:00")
puts a < b
DATE

Класс Date

Сегодня

Date.today

Сегодня – 1

Date.today - 1

Парсим дату

Date.parse("2018-10-01")

День недели на объект даты (частный случай today)

Date.today.friday?
Date.today.saturday?
Date.today.monday?

Номер дня в неделе (понедельник -1, воскресенье -7)

Date.cwday

Номер недели в году

Date.cweek
Скрипты

Простейший скрипт по генерации дат всех выходных (суббот и воскресений) на год без исключений переносов (нужно удаление/добавление дат в соответствии с произ. календарем).

require 'time'

cur_date = Date.new(2018,1,1)
counter = 0
while counter < 365
 puts "#{cur_date}" if cur_date.saturday? == true || cur_date.sunday? ==  true
 cur_date+=1
 counter+=1
end

 

Ruby: работаем с базой mysql

Как показала недолгая практика использования gem mysql, лучше его не использовать вообще, а сразу использовать gem mysql2.

gem mysql не развивается (последний апдейт в 2013) и имеет косяки, например, с кодировкой (кириллица кладется в cp1251 и изменить, насколько я понял, никак).

Установка mysql2

sudo gem install mysql2

Использование простое. При Insert крайне полезно почистить данные перед insert от мусора, во избежания exception, универсальный метод для этого можно взять тут.

[code language=”ruby”]
require ‘mysql2’

def db_mysql_con
client = Mysql2::Client.new(:host => “localhost”, :username => “root”, :password => “somepass”, :database => “db”)
end

def db_select_log(conn)
res = []
conn.query(“SELECT * FROM log WHERE date=CURDATE() AND user_int=’TELEGRAM'”).each do |r| res << r; end
return res
end

def db_insert_log(conn,id,text)
res = []
conn.query(“INSERT INTO log (ID, TEXT) VALUES (\”#{id}\”, \”#{text}\”)”).each do |r| res << r; end
end

conn_mysql = db_mysql_con # Подключаемся
puts db_select_log(conn_mysql) # Получаем результат в виде array в array
db_insert_log(conn_mysql, id, text) # Добавляем какие-то данные
conn_mysql.close # Отключаемся
[/code]

 

Ruby: удаляем спец. символы перед insert в базу

Метод на ruby по удалению мусора в виде разных скобок, exclamation и других символов, которые гарантированно приведут к ошибке при insert в базу. При необходимости можно добавить символы в переменную pattern.

You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ....

[code language=”ruby”]
def del_garbage_for_bd(data)
pattern = /!|’|”|\\/
if data.is_a? Array
res = []
data.each do |r|
if r.is_a? Array
data2 = []
r.each do |r2|
r2 = r2.gsub(pattern,””) if r2.is_a? String
data2 << r2
end
r = data2
end
if r.is_a? String
r = r.gsub(pattern,””)
end
res << r
end
return res
end
data = data.gsub(pattern,””) if data.is_a? String
data
end
[/code]

Примеры отработки.

str = "test1!!\""
print del_garbage_for_bd(str)
> test1

num = 4
print del_garbage_for_bd(num)
> 4

arr = [ "test1!!\"", "test5", "test6", "test7", 1, 2 ]
print del_garbage_for_bd(arr)
> ["test1", "test5", "test6", "test7", 1, 2]

arr2 = [ ["test1!!\"", "test5"], ["test5!!\\\""], ["test6"], ["test7"], [1], [2] ]
print del_garbage_for_bd(arr2)
> [["test1", "test5"], ["test5"], ["test6"], ["test7"], [1], [2]]

 

 

 

Ruby: gem oci8 для работы с БД Oracle
INSTALL

Gem oci8 как в Ubuntu, так и в Gentoo использует oracle instantclient для работы, поэтому сначала нужно поставить его. Полностью процесс установки gem описан тут.

После установки instantclient устанавливаем уже расширение:

  • Установка Ubuntu требует обязательное определение переменной LD_LIBRARY_PATH.
~$ sudo env LD_LIBRARY_PATH=/usr/lib/oracle/12.1/client64/lib gem install ruby-oci8
Building native extensions.  This could take a while...
Successfully installed ruby-oci8-2.2.1
1 gem installed
Installing ri documentation for ruby-oci8-2.2.1...
Installing RDoc documentation for ruby-oci8-2.2.1...
  • Установка Gentoo проходила без проблем простым emerge.

Если при использовании выдает:

ocidatetime.c:119:in oci8lib_220.so: ORA-01805: possible error in date/time operation (OCIError)

Помогает (добавить до cursor.exec):

OCI8::BindType::Mapping[Time] = OCI8::BindType::LocalTime
OCI8::BindType::Mapping[:date] = OCI8::BindType::LocalTime
USAGE

[code language=”ruby”]
require ‘oci8’

def db_ora_con
OCI8.new(‘<user>’,'<pass>’,'<SID>’)
end

def get_addr(conn)
res = []
conn.exec(“select U_NAME
,U_CITY
,U_STREET
,U_HOUSE
from ABTB do |r| res << r; end
return res
end

conn_ora = db_ora_con # Подключаемся
addr_info = get_addr(conn_ora) # Получаем результат в виде array в array
conn_ora.logoff # Отключаемся
[/code]

JOIN в SELECT

В примере по выгрузке данным из Oracle используют join по “;” для объединения элементов одной строки в результате select чтобы получить массив, в котором каждый новый элемент представлял собой строку из результата.

Как по мне так делать не нужно по двум причинам:

1. будет беда если в данных окажется ваш разделитель “;” который вы использовали для join. В результате вы можете получить кучу боли.

do |r| res << r.join(';'); end

2. Если вам понадобится, а вам наверняка понадобится, не просто же так вы данные выгружали, работать с этими данными – вам придется делать split, что само по себе костыль, хоть и в ruby это делается простой точкой.

Поэтому, как по мне, намного надежнее и проще работать с двумерным массивом (массив и в нем каждый новый массив – новая строка из результата select).

do |r| res << r; end
Разное

0.178766998E9  – при аналогичных запросах в PHP и ruby в выводе php нет конвертации числа в форму 0.<число>E9 (количество разрядов), а в ruby есть. Если это ненужно – решается простой конвертацией в char при запросе. Вместо select t.id пишем select to_char(t.id).

 

Ruby: работа с Excel (xls) и CSV с гемами spreadsheet, axlsx

В целом про csv/excel тут.

Для работы с CSV достаточно системной библиотеки CSV.

 

CSV

Работа с CSV

# открываем файл, каждая строка является массивом, каждая ячейкам – элементом массива

csv.foreach("csv.csv") do |line|
puts line.inspect
end

# если нам нужно сохранить весь файл в двумерный массив (array, в нем строки в виде отдельных array) – просто вызываем read. В последующем с ним можно работать как с обычным array – делать поиск в нем/модифицировать и т.д.

arr = CSV.read("csv.csv")

# Добавляем данные в конец array.

CSV.open("csv.csv", "a"| do |arr|
add = [ "1", "2" ]
arr.add_row(add)
end

Функция по генерации CSV

def create_csv_from_simple_arr(filename,arr)
	CSV.open("#{filename}", "w", {:col_sep => ";"}) do |csv|
		arr.each do |row|
			csv << row
		end
	end
end

Для работы с xls/xlsx первоначально использовал gem Spreadsheet. Гем самый популярный, но не сказать что функциональный – нет поддержки xlsx, нельзя включить autofilter поля, нет создания caviat table (сводные отчеты). Первые два есть в PHPExcel, поэтому гему “не зачет”. Если все это (и другое) необходимо – есть gem Axlsx, который явно больше прокачан по функционалу. Его и детально рассмотрим.

 

Axlsx

Дока

Примеры на git

Мои примеры

Создание простейшего xlsx
p = Axlsx::Package.new
p.workbook.add_worksheet(:name => 'result') do |sheet|
sheet.add_row ['row1']
sheet.add_row ['row2']
sheet.add_hyperlink :location => 'https://hlpdesk.fttb.corbina.net/shedule_crash_tt.pl?area=123', :ref => sheet.rows.first.cells.first
#sheet['A1'].color = "0000FF"
end
p.serialize('Test Excel.xlsx')
Создание xlsx с разными плюшками
arr_data = [["link", "1", "2"], ["link", "2", "8"], ["link", "3", "9"]]
p = Axlsx::Package.new
ws = p.workbook.add_worksheet(:name => 'result')
title = ws.styles.add_style(:bg_color => "FFE8E5E5", :sz=>14, :border=> {:style => :thin, :color => "000000"})
blue_link = ws.styles.add_style(:fg_color => '0000FF')
# header
ws.auto_filter = "A1:C1"
ws.sheet_view.pane do |pane|
pane.top_left_cell = "A2"
pane.state = :frozen_split
pane.y_split = 1
pane.x_split = 0
pane.active_pane = :bottom_right
end
ws.add_row [ "", "row1", "row2"]
# data
i=1
arr_data.each do |row|
i+=1
ws.add_row row
ws.add_hyperlink :location => "https://url.ru/#{row[1]}", :ref => "A#{i}"
ws["A#{i}"].style = blue_link
end
p.serialize('Test Excel.xlsx')

Разбор

Создание
p = Axlsx::Package.new # instance
ws = p.workbook.add_worksheet(:name => 'result')  # книги и листа одновременно (можно разделять)
Данные
ws.add_row [ "", "row1", "row2"]
ws.add_row [ "", "row1", "row2"] + arr + [some_calc_value]
sheet.add_image(:image_src => image_path)  # картинки
alg_left = wb.styles.add_style({:alignment => {:horizontal => :left}})
title = ws.styles.add_style(:bg_color => "FFE8E5E5", :sz=>14, :border=> {:style => :thin, :color => "000000"}) # header
blue_link = ws.styles.add_style(:fg_color => '0000FF') # hyperlink
ws["A#{i}"].style = blue_link # применение стиля на ячейку sheet.add_row arr, :style => header # применение стиля на строку ячеек
Ссылки
ws.add_hyperlink :location => 'https://hlpdesk.fttb.corbina.net/shedule_crash_tt.pl?area=123', :ref => sheet.rows.first.cells.first
ws.add_hyperlink :location => "https://hlpdesk.fttb.corbina.net/shedule_crash_tt.pl?area=#{row[1]}", :ref => "A#{i}"
Header автофильтр + заморозка
# header
ws.auto_filter = "A1:C1"
ws.sheet_view.pane do |pane|
pane.top_left_cell = "A2"
pane.state = :frozen_split
pane.y_split = 1
pane.x_split = 0
pane.active_pane = :bottom_right
end
Генерация
p.serialize('Test Excel.xlsx')

 

Spreadsheet

Установка

sudo gem install spreadsheet

Код конвертации с CSV в XLS

Сделал консольный скрипт csv_to_xls.rb по конвертации CSV в XLS с настройками форматирования столбцов.

Usage:

csv_to_xls.rb test.csv test.xls

Код, основа отсюда:

#!/usr/bin/env ruby 
#coding: utf-8

require 'spreadsheet' 
require 'csv' # Встроенная библиотека, установка не обязательна

csv_path = ARGV[0] 
xls_path = ARGV[1] 
book = Spreadsheet::Workbook.new 
sheet1 = book.create_worksheet(:name => 'Sheet1') # Можно имя и не указывать

# Построчно ковертируем CSV в XLS 
CSV.open('/home/user/test.csv', 'r', {:col_sep => ";"}) do |csv| 
 csv.each_with_index do |row, i| 
  sheet1.row(i).replace(row) 
 end 
end

# sheet2[1,4] = Spreadsheet::Link.new 'www.google.com', '2017-03-13' # <чтобы не потерять> Можно сделать линк, указав [строку, ряд] ячейки

# Делаем шаблон для форматирования header
header_format = Spreadsheet::Format.new( 
 :weight => :bold, 
 :horizontal_align => :center, 
 :locked => true, 
 :text_wrap => true # Перенос по словам
 :pattern_fg_color => :silver, :pattern => 1 # Серый цвет
 #:number_format => 'YYYY.MM.DD' # Формат даты
)

# Для форматирования остальных столбцов
other_format = Spreadsheet::Format.new( 
 :text_wrap => true # perenos po slovam!
)

# Применяем формат к столбцам
sheet1.row(0).default_format = header_format 
sheet1.column(0).default_format = other_format 
sheet1.column(1).default_format = other_format 
sheet1.column(2).default_format = other_format 
sheet1.column(3).default_format = other_format 
sheet1.column(4).default_format = other_format 
sheet1.column(5).default_format = other_format 
sheet1.column(6).default_format = other_format 
sheet1.column(7).default_format = other_format 
sheet1.column(8).default_format = other_format

# Выставляем ширину столбцов
 sheet1.column(0).width = 20 
 sheet1.column(1).width = 40 
 sheet1.column(2).width = 15 
 sheet1.column(3).width = 5 
 sheet1.column(4).width = 10 
 sheet1.column(5).width = 10 
 sheet1.column(6).width = 40 

# Создаем файл
book.write('/home/user/test.xls')

 

 

Telegram bot на Ruby c gem telegram-bot-ruby
 
РЕСУРСЫ, переменные И МЕТОДЫ
  • https://core.telegram.org/bots/api – основной необходимый MAN. Очень крутой, но не хватает примеров
  • https://github.com/atipugin/telegram-bot-ruby – ruby gem telegram, есть пример кода для разных кейсов
  • https://github.com/mustafababil/Telegram-Weather-Bot/blob/master/responseController.py – пример бота в телеграм на python
METHOD
bot.api.send_message - основной метод, отправка сообщений
bot.api.answer_callback_query- ответ на CallBackQuery, отвечать нужно после обработки сообщения, чтобы ошибки ответа на callback не привели к проблемам с самим ответом
bot.api.deleteMessage - удаление сообщений (своих, исходящих от бота)

ID
rqst.from.id - user info id. Если ботом отвечать на него, то даже если бот добавлен в группу ответ пользователю будет в личный чат, а не в групповой.
rqst.chat.id - id чата из которого сделан запрос (с пользователем, групповой). Если ботом отвечать на него, то если бот добавлен в группу ответ пользователю будет в групповой чат. rqst.message.message_id - id сообщения внутри чата rqst.message.id - id сообщения глобальный
TEXT
rqst.text - текст сообщения в случае типа Telegram::Bot::Types
rqst.data - текст "за кнопкой" InlineKeyBoardButton в случае типа Telegram::Bot::Types
rqst.message.text - текст сообщения, на которое мы отвечаем, в случае типа Telegram::Bot::Types::CallbackQuery

Разное
rqst.from.first_name - имя
rqst.from.last_name - фамилия
rqst.chat.type - тип сообщения в чате
ЗАПУСКАЕМ Простой БОТ
1) BotFather – создаем bot, получаем token
Done! Congratulations on your new bot. You will find it at t.me/xxxxxx_Bot. You can now add a description, about section and profile picture for your bot, see /help for a list of commands. By the way, when you've finished creating your cool bot, ping our Bot Support if you want a better username for it. Just make sure the bot is fully operational before you do this.

Use this token to access the HTTP API:
xxxxxxx

For a description of the Bot API, see this page: https://core.telegram.org/bots/api
2) Ставим gem telegram-bot-ruby
sudo gem install telegram-bot-ruby
3) Проверяем работу токена (код из Usage)
require 'telegram/bot'

token = 'YOUR_TELEGRAM_BOT_API_TOKEN'

Telegram::Bot::Client.run(token) do |bot|
  bot.listen do |message|
  case message.text
  when '/start'
  bot.api.send_message(chat_id: message.chat.id, text: "Hello, #{message.from.first_name}")
  when '/stop'
  bot.api.send_message(chat_id: message.chat.id, text: "Bye, #{message.from.first_name}")
  end
  end
end
Добавляем плюшки

THREAD

Бот надо делать многопоточным, по умолчанию это не так – запросы разных пользователей в одной очереди и один сложный запрос может всю очередь на минуту затормозить. Для фикса заворачиваем обработку в Thread. Чтобы не путаться с конструкциями типа “.message.message”, message выше заменено на rqst (в telegram есть тип сообщения message).

Telegram::Bot::Client.run(token) do |bot|
  bot.listen do |rqst|
    Thread.start(rqst) do |rqst|
      <ОСНОВНОЙ КОД БОТА>
    end
  end
end
RESCUE
Необходимо завернуть основной код бота в конструкцию loop-do и begin-rescue. Пример уже с многопоточностью.
loop do
  begin
    Telegram::Bot::Client.run(token) do |bot|
      bot.listen do |rqst|
        Thread.start(rqst) do |rqst|
          begin
            <ОСНОВНОЙ КОД БОТА>
          rescue
            <RESCUE, можно сделать лог в файл типа "RESCUE PRCSNG">
          end
        end
      end
    end
  rescue
    <RESCUE, можно сделать лог в файл типа "RESCUE API">
  end
end
Это самое важное. Без него бот будет падать как из-за ошибок при обработке ваших запросов (в том числе это может быть и нормой – например при работе с callback или message edit, об этом ниже), так и из-за работ на платформе telegram, которые проводятся регулярно.
CALLBACK
2017-10-02 11:01:45 +0300;out: Telegram API has returned the error. (ok: false, error_code: 400, description: Bad Request: QUERY_ID_INVALID);mess: /var/lib/gems/1.9.1/gems/telegram-bot-ruby-0.8.3/lib/telegram/bot/api.rb:74:in call 
/var/lib/gems/1.9.1/gems/telegram-bot-ruby-0.8.3/lib/telegram/bot/api.rb:58:in method_missing 
/home/redkin_p/bin/telegram_bot.rb:72:in block (4 levels) in <main>

API
2017-10-02 06:39:23 +0300;out: Telegram API has returned the error. (error_code: 502, uri: https://api.telegram.org/botxxxxxxx/getUpdates);mess: /var/lib/gems/1.9.1/gems/telegram-bot-ruby-0.8.3/lib/telegram/bot/api.rb:74:in call
/var/lib/gems/1.9.1/gems/telegram-bot-ruby-0.8.3/lib/telegram/bot/api.rb:58:in method_missing
/var/lib/gems/1.9.1/gems/telegram-bot-ruby-0.8.3/lib/telegram/bot/client.rb:30:in fetch_updates
/var/lib/gems/1.9.1/gems/telegram-bot-ruby-0.8.3/lib/telegram/bot/client.rb:25:in listen
/home/redkin_p/bin/telegram_bot.rb:16:in block (2 levels) in <main>
/var/lib/gems/1.9.1/gems/telegram-bot-ruby-0.8.3/lib/telegram/bot/client.rb:18:in run
/var/lib/gems/1.9.1/gems/telegram-bot-ruby-0.8.3/lib/telegram/bot/client.rb:8:in run
/home/redkin_p/bin/telegram_bot.rb:14:in block in <main>
/home/redkin_p/bin/telegram_bot.rb:12:in loop
/home/redkin_p/bin/telegram_bot.rb:12:in <main>;
Отправка файлов

Чтобы расшарить файл скидываем в чате с ботом необходимый файл боту и узнаем ботом id файла – в ответе от бота получаем id (типо BQ1DAsSDF@14FSFDSFFDAJ-3diLAg). Осторожно применение метода .file_id не на тот тип данных приведет к ошибке: фотографии windows desktop приложение отсылает как document, а не фото, а в мобильном приложении по умолчанию используется тип именно photo.

Узнаем ID файла
  fid = rqst.document.file_id
  bot.api.send_message(chat_id: rqst.chat.id, text: "#{fid}")

Отсылка файла требует id пользователя и file_id сообщения, остальные параметры опциональные.

Отсылаем файл
 bot.api.send_document(chat_id: rqst.from.id, document: "BQ1DAsSDF@14FSFDSFFDAJ-3diLAg")

Простой пример – отсылаем на основе запросов в inline keyboard файлы.

Создаем кнопки по вызову файлов. 
kb = [ 
Telegram::Bot::Types::InlineKeyboardButton.new(text: 'some-text-test1', callback_data: "test1"), 
Telegram::Bot::Types::InlineKeyboardButton.new(text: 'some-text-test2', callback_data: "test2"), 
Telegram::Bot::Types::InlineKeyboardButton.new(text: 'some-text-test3', callback_data: "test3") 
] 
markup_retry = Telegram::Bot::Types::InlineKeyboardMarkup.new(inline_keyboard: kb) 

Делаем соответствующие кнопкам ответы со ссылками на файлы (вместо send_message используем send_document).
if rqst.data == "test1" 
 bot.api.send_document(chat_id: rqst.from.id, document: "BQ1DAsSDF@14FSFDSFFDAJ-3diLAg1") 
elsif rqst.data == "test2" 
 bot.api.send_document(chat_id: rqst.from.id, document: "BQ1DAsSDF@14FSFDSFFDAJ-3diLAg2") 
elsif rqst.data == "test3" 
 bot.api.send_document(chat_id: rqst.from.id, document: "BQ1DAsSDF@14FSFDSFFDAJ-3diLAg3") 
else
 "тут что то еще"
end

Не забываем отвечать на Callback. Отвечать нужно после обработки сообщения, чтобы ошибки ответа на callback не привели к проблемам с самим ответом.
 bot.api.answer_callback_query(callback_query_id: rqst.id)
Запрос phone_number

Авторизация по номеру очень клевая штука. Тут есть особенность – нужно проверять чтобы ID пользователя совпадал с ID контакта, потому что свой номер передается как контакт и легко передать контакт “левого”, авторизованного, человека.

Для начала запрашиваем контакт (например, если чела с таким ID telegram еще нет в нашей телеграм-базе) через кастом-клавиатуру с одной кнопкой (https://github.com/atipugin/telegram-bot-ruby#Usage раздел Custom keyboards):

kb = Telegram::Bot::Types::KeyboardButton.new(text: 'Отправить номер', request_contact: true)
markup = Telegram::Bot::Types::ReplyKeyboardMarkup.new(keyboard: kb)
bot.api.send_message(chat_id: rqst.chat.id, text: 'Для работы с ботом вам нужно пройти идентификацию посредством отправки вашего номера телефона.', reply_markup: markup)

Далее при приеме контакта (например, через проверку rqst.contact != nil) проверяем что пользователь отправивший контакт имеет такой же ID как указанный в полученном контакте, если это не так – ругаемся.

bot.api.send_message(chat_id: rqst.chat.id, text: 'Нет, так не получится.') if rqst.from.id != rqst.contact.user_id

Потом проверяем уже наличие номера в своих системах. Если есть – добавляем чела с таким ID telegram в нашу телеграм-базу. Если нет – просим не беспокоить. В любом случае удаляем кастом клавиатуру с запросом номера.

number = rqst.contact.phone_number.sub(/\+/,"")
check = check_number(number)
if check == "ok"
  add_to_telega(number,rqst.contact.user_id)
  text = "Спасибо, идентификация пройдена."
else
  text = "Вас нет в базе данных."
end
kb = Telegram::Bot::Types::ReplyKeyboardRemove.new(remove_keyboard: true)
bot.api.send_message(chat_id: rqst.chat.id, text: "#{text}", reply_markup: kb)
Inline keyboards

Пример простого статического InlineKeyBoard.

kb = [ 
Telegram::Bot::Types::InlineKeyboardButton.new(text: 'some-text-test1', callback_data: "test1"), 
Telegram::Bot::Types::InlineKeyboardButton.new(text: 'some-text-test2', callback_data: "test2"), 
Telegram::Bot::Types::InlineKeyboardButton.new(text: 'some-text-test3', callback_data: "test3") 
] 
markup_retry = Telegram::Bot::Types::InlineKeyboardMarkup.new(inline_keyboard: kb)

Пример статического InlineKB с кнопками на разных рядах.

kb = 
[ 
  [ 
  Telegram::Bot::Types::InlineKeyboardButton.new(text: 'Text1', callback_data: "1"), 
  Telegram::Bot::Types::InlineKeyboardButton.new(text: 'Text2', callback_data: "2"), 
  ], 
  [ 
  Telegram::Bot::Types::InlineKeyboardButton.new(text: 'Text3', callback_data: "3"), 
  Telegram::Bot::Types::InlineKeyboardButton.new(text: 'Text4', callback_data: "4"), 
  ], 
Telegram::Bot::Types::InlineKeyboardButton.new(text: 'Text5', callback_data: "5") 
]
markup_retry = Telegram::Bot::Types::InlineKeyboardMarkup.new(inline_keyboard: kb)

Далее отсылаем ответ с созданной клавой.

bot.api.send_message(chat_id: user_id, text: "#{res}", reply_markup: markup_retry)
bot.api.answer_callback_query(callback_query_id: rqst.id)
Inline keyboards on-the-fly

Очень крутая штука, с помощью нее можно создавать многоуровневые меню, я так сделал трехуровневое меню с кнопками туда-сюда. Очень удобно.

Inline keyboards and on-the-fly updating

There are times when you'd prefer to do things without sending any messages to the chat. For example, when your user is changing settings or flipping through search results. In such cases you can use Inline Keyboards that are integrated directly into the messages they belong to.

Unlike with custom reply keyboards, pressing buttons on inline keyboards doesn't result in messages sent to the chat. Instead, inline keyboards support buttons that work behind the scenes: callback buttons, URL buttons and switch to inline buttons.

В интернете кто-то почему то думает, что чтобы поменять свое сообщение нужно сохранить результат операции отправки (в виде ID) в файле. По факту это не нужно. При получении CallBackQuery можно извлечь ID и даже текст сообщения, на которое пришел ответ.

last_bot_message_id_to_user = rqst.message.message_id
last_bot_message_text_to_user = rqst.message.text

В целом для ответа нужен только id и новая клавиатура. Если клавиатура будет такая же как раньше – telegram ругнется (тут нам поможет exception выше) и менять ничего не будет.

bot.api.editMessageReplyMarkup(chat_id: rqst.from.id, message_id: last_bot_message_id_to_user, reply_markup: markup_retry)
Emoji
Отсылаются просто по unicode коду внутри сообщения. Unicode можно взять отсюда. Буква \u обязательно должна быть маленькая. Например, используем в инлайн-кнопках.
hammer = "\u{1F528}"
Telegram::Bot::Types::InlineKeyboardButton.new(text: "#{hammer} Сделать то-то", callback_data: "test_operations")
Разметка
Отправка форматированных сообщений. Можно размечать через Markdown, можно через HTML. HTML базовый, без каких либо таблиц и прочего. В одном сообщении не должно быть больше 100 URL ссылок (последующие обрезаются) вне зависимости от типа форматирования.
bot.api.send_message(chat_id: "#{id}", text: "[test](https://t.me/TESTBot?start=command-TEST)", parse_mode: "Markdown")

bot.api.send_message(chat_id: "#{id}", text: "<a href=\"http://www.example.com/\">inline URL</a>", parse_mode: "HTML")

Markdown симпатичнее HTML, но на практике очень капризный – из-за него могут сыпаться ошибки и не доставляться сообщения пользователю если markdown увидит в сообщении спец. символы типа _, причем даже не в самой ссылке, что можно было бы “понять и простить”, а дальше в тексте.

bot.api.send_message(chat_id: "#{id}", text: "[Test](https://t.me/TestBot?start) My_Test", parse_mode: "Markdown")

Telegram API has returned the error. (ok: "false", error_code: "400", description: "Bad Request: can't parse entities: Can't find end of the entity starting at byte offset 37") (Telegram::Bot::Exceptions::ResponseError)
Редирект на свой же бот, для исполнения команды  можно сделать двумя способами:
1) Используя встроенные команды, поставив перед числом или неразрывной латиницей знак \
\start
2) Используя Deep Linking (1,2,3). Единственный найденный способ для редиректа IP. Недостаток – пользователю придется нажать кнопку Start (не во всех реализациях клиента), даже если он уже общался с ботом и запрос от пользователя в чате для самого пользователя будет выглядеть как /start, хотя по факту за ним будут скрываться какие-то данные. В случае не первого общения с ботом выглядит немного странно, но работает. В целом, Deep Linking нужен именно для старта общения – авторизации, связи между ботом и другими системами, реферальных ссылок и прочего через передачу ID при старте общения с ботом.
bot.api.send_message(chat_id: "#{id}", text: "[test](https://t.me/TESTBot?start=command-TEST)", parse_mode: "Markdown")

УВЕДОМЛЕНИЯ

По умолчанию бот отправляет сообщения только в ответ на сообщение пользователя. Чтобы отправлять уведомления нужно вместо Listen использовать New. Можно отправлять в ответ и отправлять уведомления одновременно.

# initialize bot client and store it in constant
# that can be done in any file which is loaded at your application's boot process
BOT = Telegram::Bot::Client.new(TOKEN)

# anywhere in another file
BOT.send_message(chat_id: id, text: "hello world")
КОМАНДЫ

Как установить команды которые видит клиент в боте:

  1. Открыть чат с BotFather
  2. Отправить ему команду /setcommands
  3. Выбрать бота для установки
  4. Составить лист команд
    /help Помощь
    /settings Настройки
  5. enjoy

РАБОТА ЧЕРЕЗ PROXY

Gem использует Faraday для подключения. В Faraday встроена возможность работать через HTTP proxy – он читает глобальные переменные http_proxy, ftp_proxy и берет из них адреса серверов. Можно так же задать напрямую в коде.

Faraday will try to automatically infer the proxy settings from your system using URI#find_proxy. This will retrieve them from environment variables such as http_proxy, ftp_proxy, no_proxy, etc. If for any reason you want to disable this behaviour, you can do so by setting the global varibale ignore_env_proxy:

Faraday.ignore_env_proxy = true
You can also specify a custom proxy when initializing the connection

Faraday.new('http://www.example.com', :proxy => 'http://proxy.com')
export http_proxy=http://your.proxy.server:port/ # включаем
unset http_proxy # отключаем
 
Ruby: exceptions

raise “tratatttata OSHIBKA” – вызов ошибки в коде скрипта. Может быть полезно:

1)      Для проверки отработки кода на exception (Begin – Rescue – End)
2)      Когда в результате какой-то внутренней проверки выяснено, что произошло что-то плохое и лучше не продолжать отработку, а завершить ее.
 

Leave a Reply