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 простая отправка писем
#!/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
Ruby: работа с текстом
Индексы
Строка в 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!]
a.delete(‘test’) – удалить по значению
arr[0..-3] – удалить последние два элемента
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
`/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
require "ipaddr"
def ip?(ip)
!!IPAddr.new(ip) rescue false
end
puts ip?("192.168.0.1")
puts ip?("192.168.1111.1")
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
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
Получить 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.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 достаточно системной библиотеки 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
Мои примеры
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')
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
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 - тип сообщения в чате
ЗАПУСКАЕМ Простой БОТ
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
sudo gem install telegram-bot-ruby
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
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 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
hammer = "\u{1F528}"
Telegram::Bot::Types::InlineKeyboardButton.new(text: "#{hammer} Сделать то-то", callback_data: "test_operations")
Разметка
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)
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")
КОМАНДЫ
Как установить команды которые видит клиент в боте:
- Открыть чат с BotFather
- Отправить ему команду /setcommands
- Выбрать бота для установки
- Составить лист команд
/help Помощь
/settings Настройки - 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” – вызов ошибки в коде скрипта. Может быть полезно:
