├── .gitignore ├── Gemfile ├── LICENSE ├── README.md ├── example.rb ├── lib ├── ext │ └── string.rb ├── telegram.rb └── telegram │ ├── api.rb │ ├── auth_properties.rb │ ├── authorization.rb │ ├── callback.rb │ ├── cli_arguments.rb │ ├── client.rb │ ├── config.rb │ ├── connection.rb │ ├── connection_pool.rb │ ├── events.rb │ ├── logger.rb │ ├── models.rb │ └── version.rb └── telegram-rb.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | *.gem 3 | *.rbc 4 | /.config 5 | /coverage/ 6 | /InstalledFiles 7 | /pkg/ 8 | /spec/reports/ 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | 13 | ## Specific to RubyMotion: 14 | .dat* 15 | .repl_history 16 | build/ 17 | 18 | ## Documentation cache and generated files: 19 | /.yardoc/ 20 | /_yardoc/ 21 | /doc/ 22 | /rdoc/ 23 | 24 | ## Environment normalisation: 25 | /.bundle/ 26 | /vendor/bundle 27 | /lib/bundler/man/ 28 | 29 | # for a library or gem, you might want to ignore these files since the code is 30 | # intended to run in multiple environments; otherwise, check them in: 31 | # Gemfile.lock 32 | # .ruby-version 33 | # .ruby-gemset 34 | 35 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 36 | .rvmrc 37 | 38 | # OS X 39 | .DS_Store 40 | 41 | # Telegram 42 | telegram-cli 43 | tg-server.pub 44 | .idea/ 45 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 SuHun Han 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Telegram API for Ruby! 2 | 3 | [![DUB](https://img.shields.io/dub/l/vibe-d.svg)](http://opensource.org/licenses/MIT) 4 | [![Gem Version](https://badge.fury.io/rb/telegram-rb.svg)](http://badge.fury.io/rb/telegram-rb) 5 | [![Code Climate](https://codeclimate.com/github/ssut/telegram-rb/badges/gpa.svg)](https://codeclimate.com/github/ssut/telegram-rb) 6 | [![Inline docs](http://inch-ci.org/github/ssut/telegram-rb.svg?branch=master)](http://inch-ci.org/github/ssut/telegram-rb) 7 | 8 | [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/ssut/telegram-rb?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 9 | 10 | A Ruby wrapper that communicates with the [Telegram-CLI](https://github.com/vysheng/tg). 11 | 12 | ## Installation 13 | 14 | ### Requirements 15 | 16 | * You need to install the [Telegram-CLI](https://github.com/vysheng/tg) version 1.3.0 or higher first. 17 | 18 | ### RubyGems 19 | 20 | In order to use the telegram-rb you will need to install the gem (either manually or using Bundler), and require the library in your Ruby application: 21 | 22 | ```bash 23 | $ gem install telegram-rb 24 | ``` 25 | 26 | or in your `Gemfile`: 27 | 28 | ```ruby 29 | # latest stable 30 | gem 'telegram-rb', require: 'telegram' 31 | 32 | # or track master repo 33 | gem 'telegram-rb', github: 'ssut/telegram-rb', require: 'telegram' 34 | ``` 35 | 36 | ## Usage 37 | 38 | The library uses EventMachine, so the logic is wrapped in `EM.run`. 39 | 40 | ```ruby 41 | # When using Bundler, let it load all libraries 42 | require 'bundler' 43 | Bundler.require(:default) 44 | 45 | # Otherwise, require 'telegram', which will load its dependencies 46 | # require 'telegram' 47 | 48 | EM.run do 49 | telegram = Telegram::Client.new do |cfg, auth| 50 | cfg.daemon = '/path/to/tg/bin/telegram-cli' 51 | cfg.key = '/path/to/tg/tg-server.pub' 52 | cfg.config_file = '/path/to/config' # optional, default file will be used if not set 53 | cfg.profile = 'user2' # optional, the profiles must be configured in ~/.telegram-cli/config 54 | cfg.logger = Logger.new(STDOUT) # optional, default logger will be created if not set 55 | 56 | # optional properties, could be used for authorization/registration telegram accounts 57 | auth.phone_number = '' 58 | auth.confirmation_code = -> { '' } 59 | auth.register = { # required in case with registration new telegram account 60 | first_name: '', 61 | last_name: '' 62 | } 63 | end 64 | 65 | telegram.connect do 66 | # This block will be executed when initialized. 67 | 68 | # See your telegram profile 69 | puts telegram.profile 70 | 71 | telegram.contacts.each do |contact| 72 | puts contact 73 | end 74 | 75 | telegram.chats.each do |chat| 76 | puts chat 77 | end 78 | 79 | # Event listeners 80 | # When you've received a message: 81 | telegram.on[Telegram::EventType::RECEIVE_MESSAGE] = Proc.new do |event| 82 | # `tgmessage` is TelegramMessage instance 83 | puts event.tgmessage 84 | end 85 | 86 | # When you've sent a message: 87 | telegram.on[Telegram::EventType::SEND_MESSAGE] = Proc.new do |event| 88 | puts event 89 | end 90 | 91 | # When connection is closed: 92 | telegram.on_disconnect = Proc.new do 93 | puts 'Connection with telegram-cli is closed' 94 | end 95 | end 96 | end 97 | ``` 98 | 99 | ### Documentation 100 | 101 | **You can check documentation from [here](http://www.rubydoc.info/github/ssut/telegram-rb)!** 102 | 103 | My goal is to have the code fully documentated for the project, so developers can use this library easily! 104 | 105 | ## Coverage (TODO) 106 | 107 | - [ ] Messaging/Multimedia 108 | - [x] Send typing signal to specific user or chat 109 | - [x] Send a message to specific user or chat 110 | - [x] Send an image to specific user or chat 111 | - [x] Send a video to specific user or chat 112 | - [x] Mark as read all received messages 113 | - [ ] Download an image of someone sent 114 | - [ ] Forward a message to specific user 115 | - [ ] Set profile picture 116 | - [ ] Group chat options 117 | - [x] Add a user to the group 118 | - [x] Remove a user from the group 119 | - [x] Leave from the group 120 | - [x] Create a new group chat 121 | - [ ] Set group chat photo 122 | - [ ] Rename group chat title 123 | - [ ] Search 124 | - [ ] Search for specific message from a conversation 125 | - [ ] Search for specific message from all conversions 126 | - [ ] Secret chat 127 | - [x] Reply message in secret chat 128 | - [ ] Visualize of encryption key of the secret chat 129 | - [ ] Create a new secret chat 130 | - [ ] Stats and various info 131 | - [x] Get user profile 132 | - [x] Get chat list 133 | - [x] Get contact list 134 | - [ ] Get history and mark it as read 135 | - [ ] Card 136 | - [ ] export card 137 | - [ ] import card 138 | 139 | ## Contributing 140 | 141 | If there are bugs or things you would like to improve, fork the project, and implement your awesome feature or patch in its own branch, then send me a pull request here! 142 | 143 | ## License 144 | 145 | **telegram-rb** is licensed under the MIT License. 146 | 147 | ``` 148 | The MIT License (MIT) 149 | 150 | Copyright (c) 2015 SuHun Han 151 | 152 | Permission is hereby granted, free of charge, to any person obtaining a copy 153 | of this software and associated documentation files (the "Software"), to deal 154 | in the Software without restriction, including without limitation the rights 155 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 156 | copies of the Software, and to permit persons to whom the Software is 157 | furnished to do so, subject to the following conditions: 158 | 159 | The above copyright notice and this permission notice shall be included in all 160 | copies or substantial portions of the Software. 161 | 162 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 163 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 164 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 165 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 166 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 167 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 168 | SOFTWARE. 169 | ``` 170 | -------------------------------------------------------------------------------- /example.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | lib = File.join(File.dirname(__FILE__), 'lib') 3 | $:.unshift lib unless $:.include?(lib) 4 | 5 | require 'eventmachine' 6 | require 'telegram' 7 | 8 | EM.run do 9 | telegram = Telegram::Client.new do |cfg| 10 | cfg.daemon = './telegram-cli' 11 | cfg.key = '/Users/ssut/tmp/tg/tg-server.pub' 12 | end 13 | 14 | telegram.connect do 15 | puts telegram.profile 16 | telegram.contacts.each do |contact| 17 | puts contact 18 | end 19 | telegram.chats.each do |chat| 20 | puts chat 21 | end 22 | 23 | telegram.on[Telegram::EventType::RECEIVE_MESSAGE] = Proc.new { |ev| 24 | 25 | } 26 | 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/ext/string.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | class String 4 | def escape 5 | newstr = gsub("\n", "\\n") 6 | newstr.gsub!('"', '\"') 7 | newstr 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/telegram.rb: -------------------------------------------------------------------------------- 1 | require 'telegram/version' 2 | require 'telegram/client' 3 | 4 | # Telegram Module 5 | # 6 | # @see Client 7 | # @version 0.1.1 8 | module Telegram 9 | end 10 | -------------------------------------------------------------------------------- /lib/telegram/api.rb: -------------------------------------------------------------------------------- 1 | module Telegram 2 | # Telegram API Implementation 3 | # 4 | # @note You must avoid doing direct calls or initializes 5 | # @see Client 6 | # @version 0.1.0 7 | class API 8 | # Update user profile, contacts and chats 9 | # 10 | # @api private 11 | def update!(&cb) 12 | done = false 13 | EM.synchrony do 14 | multi = EM::Synchrony::Multi.new 15 | multi.add :profile, update_profile! 16 | multi.add :contacts, update_contacts! 17 | multi.add :chats, update_chats! 18 | multi.perform 19 | done = true 20 | end 21 | 22 | check_done = Proc.new { 23 | if done 24 | @starts_at = Time.now 25 | cb.call unless cb.nil? 26 | logger.info("Successfully loaded all information") 27 | else 28 | EM.next_tick(&check_done) 29 | end 30 | } 31 | EM.add_timer(0, &check_done) 32 | end 33 | 34 | # Update user profile 35 | # 36 | # @api private 37 | def update_profile! 38 | assert! 39 | callback = Callback.new 40 | @profile = nil 41 | @connection.communicate('get_self') do |success, data| 42 | if success 43 | callback.trigger(:success) 44 | contact = TelegramContact.pick_or_new(self, data) 45 | @contacts << contact unless self.contacts.include?(contact) 46 | @profile = contact 47 | else 48 | raise "Couldn't fetch the user profile." 49 | end 50 | end 51 | callback 52 | end 53 | 54 | # Update user contacts 55 | # 56 | # @api private 57 | def update_contacts! 58 | assert! 59 | callback = Callback.new 60 | @contacts = [] 61 | @connection.communicate('contact_list') do |success, data| 62 | if success and data.class == Array 63 | callback.trigger(:success) 64 | data.each { |contact| 65 | contact = TelegramContact.pick_or_new(self, contact) 66 | @contacts << contact unless self.contacts.include?(contact) 67 | } 68 | else 69 | raise "Couldn't fetch the contact list." 70 | end 71 | end 72 | callback 73 | end 74 | 75 | # Update user chats 76 | # 77 | # @api private 78 | def update_chats! 79 | assert! 80 | callback = Callback.new 81 | 82 | collected = 0 83 | collect_done = Proc.new do |id, data, count| 84 | collected += 1 85 | @chats << TelegramChat.new(self, data) 86 | callback.trigger(:success) if collected == count 87 | end 88 | collect = Proc.new do |id, count| 89 | @connection.communicate(['chat_info', "chat\##{id}"]) do |success, data| 90 | collect_done.call(id, data, count) if success 91 | end 92 | end 93 | 94 | @chats = [] 95 | @connection.communicate('dialog_list') do |success, data| 96 | if success and data.class == Array 97 | chatsize = data.count { |chat| chat['peer_type'] == 'chat' } 98 | data.each do |chat| 99 | if chat['peer_type'] == 'chat' 100 | collect.call(chat['peer_id'], chatsize) 101 | elsif chat['peer_type'] == 'user' 102 | @chats << TelegramChat.new(self, chat) 103 | end 104 | end 105 | callback.trigger(:success) if chatsize == 0 106 | else 107 | raise "Couldn't fetch the dialog(chat) list." 108 | end 109 | end 110 | callback 111 | end 112 | 113 | # Send a message to specific user or chat 114 | # 115 | # @param [String] target Target to send a message 116 | # @param [String] text Message content to be sent 117 | # @yieldparam [Bool] success The result of the request (true or false) 118 | # @yieldparam [Hash] data The data of the request 119 | # @since [0.1.0] 120 | # @example 121 | # telegram.msg('user#1234567', 'hello!') do |success, data| 122 | # puts success # => true 123 | # puts data # => {"event": "message", "out": true, ...} 124 | # end 125 | def msg(target, text, &callback) 126 | assert! 127 | @connection.communicate(['msg', target, text], &callback) 128 | end 129 | 130 | # Mark as read all received messages with specific user 131 | # 132 | # @param [String] target Target to mark read messages 133 | # @example 134 | # telegram.mark_read('user#1234567') 135 | def mark_read(target) 136 | assert! 137 | @connection.communicate(['mark_read', target]) 138 | end 139 | 140 | # Add a user to the chat group 141 | # 142 | # @param [String] chat Target chat group to add a user 143 | # @param [String] user User who would be added 144 | # @param [Block] callback Callback block that will be called when finished 145 | # @yieldparam [Bool] success The result of the request (true or false) 146 | # @yieldparam [Hash] data The raw data of the request 147 | # @since [0.1.0] 148 | # @example 149 | # telegram.chat_add_user('chat#1234567', 'user#1234567') do |success, data| 150 | # puts success # => true 151 | # puts data # => {"event": "service", ...} 152 | # end 153 | def chat_add_user(chat, user, &callback) 154 | assert! 155 | @connection.communicate(['chat_add_user', chat, user], &callback) 156 | end 157 | 158 | # Remove a user from the chat group 159 | # You can leave a group by this method (Set a user identifier to your identifier) 160 | # 161 | # @param [String] chat Target chat group to remove a user 162 | # @param [String] user User who would be removed from the chat 163 | # @param [Block] callback Callback block that will be called when finished 164 | # @yieldparam [Bool] success The result of the request (true or false) 165 | # @yieldparam [Hash] data The raw data of the request 166 | # @since [0.1.0] 167 | # @example 168 | # telegram.chat_del_user('chat#1234567', 'user#1234567') do |success, data| 169 | # puts success # => true 170 | # puts data # => {"event": "service", ...} 171 | # end 172 | def chat_del_user(chat, user, &callback) 173 | assert! 174 | @connection.communicate(['chat_del_user', chat, user], &callback) 175 | end 176 | 177 | # Send typing signal to the chat 178 | # 179 | # @param [String] chat Target chat group to send typing signal 180 | # @param [Block] callback Callback block that will be called when finished 181 | # @yieldparam [Bool] success The result of the request (true or false) 182 | # @yieldparam [Hash] data The raw data of the request 183 | # @since [0.1.1] 184 | # @example 185 | # telegram.send_typing('chat#1234567') do |success, data| 186 | # puts success # => true 187 | # puts data # => {"result": "SUCCESS"} 188 | # end 189 | def send_typing(chat, &callback) 190 | assert! 191 | @connection.communicate(['send_typing', chat], &callback) 192 | end 193 | 194 | # Send contact to peer chat 195 | # 196 | # @param [String] peer Target chat to which contact will be send 197 | # @param [String] contact phone number 198 | # @param [String] contact first name 199 | # @param [String] contact last name 200 | # @example 201 | # telegram.send_contact('chat#1234567', '9329232332', 'Foo', 'Bar') 202 | def send_contact(peer, phone, first_name, last_name) 203 | assert! 204 | @connection.communicate(['send_contact', peer, phone, first_name, last_name]) 205 | end 206 | 207 | # Abort sendign typing signal 208 | # 209 | # @param [String] chat Target chat group to stop sending typing signal 210 | # @param [Block] callback Callback block that will be called when finished 211 | # @yieldparam [Bool] success The result of the request (true or false) 212 | # @yieldparam [Hash] data The raw data of the request 213 | # @since [0.1.1] 214 | # @example 215 | # telegram.send_typing_abort('chat#1234567') do |success, data| 216 | # puts success # => true 217 | # puts data # => {"result": "SUCCESS"} 218 | # end 219 | def send_typing_abort(chat, &callback) 220 | assert! 221 | @connection.communicate(['send_typing_abort', chat], &callback) 222 | end 223 | 224 | # Send a photo to the chat 225 | # 226 | # @param [String] chat Target chat group to send a photo 227 | # @param [String] path The path of the image you want to send 228 | # @param [Block] callback Callback block that will be called when finished 229 | # @yieldparam [Bool] success The result of the request (true or false) 230 | # @yieldparam [Hash] data The raw data of the request 231 | # @since [0.1.1] 232 | # @example 233 | # telegram.send_photo('chat#1234567') do |success, data| 234 | # puts "there was a problem during the sending" unless success 235 | # puts success # => true 236 | # puts data # => {"event": "message", "media": {"type": "photo", ...}, ...} 237 | # end 238 | def send_photo(chat, path, &callback) 239 | assert! 240 | @connection.communicate(['send_photo', chat, path], &callback) 241 | end 242 | 243 | # Send a video to the chat 244 | # 245 | # @param [String] chat Target chat group to send a video 246 | # @param [String] path The path of the video you want to send 247 | # @param [Block] callback Callback block that will be called when finished 248 | # @yieldparam [Bool] success The result of the request (true or false) 249 | # @yieldparam [Hash] data The raw data of the request 250 | # @since [0.1.1] 251 | # @example 252 | # telegram.send_photo('chat#1234567') do |success, data| 253 | # puts "there was a problem during the sending" unless success 254 | # puts success # => true 255 | # puts data # => {"event": "message", "media": {"type": "video", ...}, ...} 256 | # end 257 | def send_video(chat, path, &callback) 258 | assert! 259 | @connection.communicate(['send_video', chat, path], &callback) 260 | end 261 | 262 | # Send a file to the chat 263 | # 264 | # @param [String] chat Target chat group to send a file 265 | # @param [String] path The path of the file you want to send 266 | # @param [Block] callback Callback block that will be called when finished 267 | # @yieldparam [Bool] success The result of the request (true or false) 268 | # @yieldparam [Hash] data The raw data of the request 269 | # @example 270 | # telegram.send_file('chat#1234567', file_path) do |success, data| 271 | # puts "there was a problem during the sending" unless success 272 | # puts success # => true 273 | # puts data # => {"event": "message", "media": {"type": "document", ...}, ...} 274 | # end 275 | def send_file(chat, path, &callback) 276 | assert! 277 | @connection.communicate(['send_file', chat, path], &callback) 278 | end 279 | 280 | def create_group_chat(chat_topic, *users, &callback) 281 | assert! 282 | members = users.join(" ") 283 | @connection.communicate(['create_group_chat', chat_topic.escape, members], &callback) 284 | end 285 | 286 | def add_contact(phone_number, first_name, last_name, &callback) 287 | assert! 288 | @connection.communicate(['add_contact', phone_number, first_name.escape, last_name.escape], &callback) 289 | end 290 | # Closes the telegram CLI app (used in case of app shutdown to kill the child process) 291 | # 292 | def disconnect(&callback) 293 | assert! 294 | @connection.communicate(['quit'], &callback) 295 | end 296 | 297 | # Download an attachment from a message 298 | # 299 | # @param [type] type The type of an attachment (:photo, :video, :audio) 300 | # @param [String] seq Message sequence number 301 | # @param [Block] callback Callback block that will be called when finished 302 | # @yieldparam [Bool] success The result of the request (true or false) 303 | # @yieldparam [Hash] data The raw data of the request 304 | # @since [0.1.1] 305 | def download_attachment(type, seq, &callback) 306 | assert! 307 | raise "Type mismatch" unless %w(photo video audio).include?(type) 308 | @connection.communicate(["load_#{type.to_s}", seq], &callback) 309 | end 310 | 311 | protected 312 | # Check the availability of the telegram-cli daemon 313 | # 314 | # @since [0.1.0] 315 | # @api private 316 | def assert! 317 | raise "It appears that the connection to the telegram-cli is disconnected." unless connected? 318 | end 319 | end 320 | end 321 | -------------------------------------------------------------------------------- /lib/telegram/auth_properties.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | 3 | module Telegram 4 | # Telegram client authorization properties 5 | # 6 | # Available options: 7 | # 8 | # * phone_number: phone number which will be authorized 9 | # * confirmation: proc which should return confirmation code received via 10 | # text message, or call 11 | # * register(optional): hash with two options 12 | # - :first_name: user first name 13 | # - :last_name: user last name 14 | class AuthProperties 15 | extend Forwardable 16 | 17 | DEFAULT_OPTIONS = { 18 | confirmation: -> {}, 19 | registration: {}, 20 | }.freeze 21 | 22 | def_delegators :@options, :phone_number, :phone_number=, :confirmation, 23 | :confirmation=, :registration= , :registration 24 | 25 | def initialize(options = {}) 26 | @options = OpenStruct.new(DEFAULT_OPTIONS.merge(options)) 27 | end 28 | 29 | def register? 30 | registration.include?(:first_name) && 31 | registration.include?(:last_name) 32 | end 33 | 34 | def present? 35 | !phone_number.nil? 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/telegram/authorization.rb: -------------------------------------------------------------------------------- 1 | module Telegram 2 | class Authorization 3 | def initialize(std_in_out, properties, logger) 4 | @std_in_out = std_in_out 5 | @properties = properties 6 | @logger = logger 7 | end 8 | 9 | def perform 10 | stdout_log = '' 11 | 12 | # there is no success auth message from telegram-cli to stdou as result 13 | # no way detect finish of authorization or registration process (after 14 | # success auth message will be added to telegram-cli, watcher should be removed) 15 | watcher = Watcher.new do 16 | std_in_out.puts('get_self') 17 | logger.info 'Authorization: watcher fired get_self' 18 | end 19 | 20 | # low-level read of stdout which mean that we receive data by chuncks 21 | while (data = std_in_out.sysread(1024)) 22 | watcher.stop 23 | stdout_log << data 24 | 25 | case stdout_log 26 | when /phone number:/ 27 | stdout_log = '' 28 | handle_phone_number 29 | when /code \('CALL' for phone code\):/ 30 | stdout_log = '' 31 | handle_confirmation_code 32 | when /register \(Y\/n\):/ 33 | stdout_log = '' 34 | handle_registration 35 | when /"phone": "#{properties.phone_number}"/ 36 | logger.info 'Authorization: successfully completed' 37 | return true 38 | else 39 | # ping telegram-cli after stdout is inactive 2 sec (expects that 40 | # there is no bigger delay between stdout and next stdin request) 41 | watcher.call_after(2) 42 | end 43 | end 44 | end 45 | 46 | private 47 | 48 | attr_accessor :std_in_out, :properties, :logger 49 | 50 | def handle_phone_number 51 | raise 'Incorrect phone number' if @_phone_number_triggered 52 | @_phone_number_triggered = true 53 | 54 | std_in_out.puts(properties.phone_number) 55 | logger.info "Authorization: sent phone number (#{properties.phone_number})" 56 | end 57 | 58 | def handle_confirmation_code 59 | raise 'Incorrect confirmation code' if @_confirmation_triggered 60 | @_confirmation_triggered = true 61 | 62 | logger.info 'Authorization: retrieving confirmation code' 63 | confirmation_code = properties.confirmation.call 64 | std_in_out.puts(confirmation_code) 65 | logger.info "Authorization: sent confirmation code (#{confirmation_code})" 66 | end 67 | 68 | def handle_registration 69 | raise 'Registration required' unless properties.register? 70 | raise 'Incorrect first or last name' if @_registration_triggered 71 | @_registration_triggered = true 72 | 73 | std_in_out.puts('Y') 74 | std_in_out.puts(properties.registration[:first_name]) 75 | std_in_out.puts(properties.registration[:last_name]) 76 | logger.info "Authorization: sent registration data (#{properties.registration.inspect})" 77 | end 78 | end 79 | 80 | class Watcher 81 | def initialize(&block) 82 | @thread = nil 83 | @block = block 84 | end 85 | 86 | def call_after(sec) 87 | stop 88 | @thread = Thread.new { sleep(sec); @block.call } 89 | end 90 | 91 | def stop 92 | @thread.exit if @thread && @thread.alive? 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /lib/telegram/callback.rb: -------------------------------------------------------------------------------- 1 | module Telegram 2 | # Callback class for em-synchrony 3 | # 4 | # @note You don't need to make this callback object without when it needed 5 | # @see API 6 | # @version 0.1.0 7 | class Callback 8 | # @return [Object] Data 9 | attr_reader :data 10 | 11 | def initialize 12 | @success = nil 13 | @fail = nil 14 | @data = nil 15 | end 16 | 17 | # Set a callback to be called when succeed 18 | # 19 | # @param [Block] cb 20 | def callback(&cb) 21 | @success = cb 22 | end 23 | 24 | # Set a callback to be called when failed 25 | # 26 | # @param [Block] cb 27 | def errback(&cb) 28 | @fail = cb 29 | end 30 | 31 | # Trigger either success or error actions with data 32 | # 33 | # @param [Symbol] type :success or :fail 34 | # @param [Object] data 35 | def trigger(type = :success, data = nil) 36 | @data = data 37 | case type 38 | when :success 39 | @success.call 40 | when :fail 41 | @fail.call 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/telegram/cli_arguments.rb: -------------------------------------------------------------------------------- 1 | module Telegram 2 | # Command line arguments for telegram-cli 3 | class CLIArguments 4 | def initialize(config) 5 | @config = config 6 | end 7 | 8 | def to_s 9 | [ 10 | disable_colors, 11 | rsa_key, 12 | disable_names, 13 | wait_dialog_list, 14 | udp_socket, 15 | json, 16 | disable_readline, 17 | profile, 18 | config_file 19 | ].compact.join(' ') 20 | end 21 | 22 | private 23 | 24 | def disable_colors 25 | '-C' 26 | end 27 | 28 | def rsa_key 29 | "-k '#{@config.key}'" 30 | end 31 | 32 | def disable_names 33 | '-I' 34 | end 35 | 36 | def wait_dialog_list 37 | '-W' 38 | end 39 | 40 | def udp_socket 41 | "-S '#{@config.sock}'" 42 | end 43 | 44 | def json 45 | '--json' 46 | end 47 | 48 | def disable_readline 49 | '-R' 50 | end 51 | 52 | def profile 53 | "-p #{@config.profile}" if @config.profile 54 | end 55 | 56 | def config_file 57 | "-c #{@config.config_file}" if @config.config_file 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/telegram/client.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'eventmachine' 3 | require 'em-synchrony' 4 | require 'em-synchrony/fiber_iterator' 5 | require 'em-http-request' 6 | require 'oj' 7 | require 'date' 8 | require 'tempfile' 9 | require 'fastimage' 10 | 11 | require 'telegram/config' 12 | require 'telegram/auth_properties' 13 | require 'telegram/authorization' 14 | require 'telegram/cli_arguments' 15 | require 'telegram/logger' 16 | require 'telegram/connection' 17 | require 'telegram/connection_pool' 18 | require 'telegram/callback' 19 | require 'telegram/api' 20 | require 'telegram/models' 21 | require 'telegram/events' 22 | require 'ext/string' 23 | 24 | module Telegram 25 | # Telegram Client 26 | # 27 | # @see API 28 | # @version 0.1.1 29 | class Client < API 30 | include Logging 31 | 32 | # @return [ConnectionPool] Socket connection pool, includes {Connection} 33 | # @since [0.1.0] 34 | attr_reader :connection 35 | 36 | # @return [TelegramContact] Current user's profile 37 | # @since [0.1.0] 38 | attr_reader :profile 39 | 40 | # @return [Array] Current user's contact list 41 | # @since [0.1.0] 42 | attr_reader :contacts 43 | 44 | # @return [Array] Chats that current user joined 45 | # @since [0.1.0] 46 | attr_reader :chats 47 | 48 | attr_reader :stdout 49 | 50 | # Event listeners that can respond to the event arrives 51 | # 52 | # @see EventType 53 | # @since [0.1.0] 54 | attr_accessor :on, :auth_properties 55 | 56 | # Initialize Telegram Client 57 | # 58 | # @yieldparam [Block] block 59 | # @yield [config] Given configuration struct to the block 60 | def initialize(&block) 61 | @config = Telegram::Config.new 62 | @auth_properties = Telegram::AuthProperties.new 63 | yield @config, @auth_properties 64 | @logger = @config.logger if @config.logger 65 | @connected = 0 66 | @stdout = nil 67 | @connect_callback = nil 68 | @on = {} 69 | 70 | @profile = nil 71 | @contacts = [] 72 | @chats = [] 73 | @starts_at = nil 74 | @events = EM::Queue.new 75 | 76 | logger.info("Initialized") 77 | end 78 | 79 | # Execute telegram-cli daemon and wait for the response 80 | # 81 | # @api private 82 | def execute 83 | cli_arguments = Telegram::CLIArguments.new(@config) 84 | command = "'#{@config.daemon}' #{cli_arguments.to_s}" 85 | @stdout = IO.popen(command, 'a+') 86 | initialize_stdout_reading 87 | end 88 | 89 | # Do the long-polling from stdout of the telegram-cli 90 | # 91 | # @api private 92 | def poll 93 | logger.info("Start polling for events") 94 | while (data = @stdout.gets) 95 | begin 96 | brace = data.index('{') 97 | data = data[brace..-2] 98 | data = Oj.load(data, mode: :compat) 99 | @events << data 100 | rescue 101 | end 102 | end 103 | end 104 | 105 | # Process given data to make {Event} instance 106 | # 107 | # @api private 108 | def process_data 109 | process = Proc.new { |data| 110 | begin 111 | type = case data['event'] 112 | when 'message' 113 | if data['from']['peer_id'] != @profile.id 114 | EventType::RECEIVE_MESSAGE 115 | else 116 | EventType::SEND_MESSAGE 117 | end 118 | end 119 | 120 | action = data.has_key?('action') ? case data['action'] 121 | when 'chat_add_user' 122 | ActionType::CHAT_ADD_USER 123 | when 'create_group_chat' 124 | ActionType::CREATE_GROUP_CHAT 125 | when 'add_contact' 126 | ActionType::ADD_CONTACT 127 | else 128 | ActionType::UNKNOWN_ACTION 129 | end : ActionType::NO_ACTION 130 | 131 | event = Event.new(self, type, action, data) 132 | @on[type].call(event) if @on.has_key?(type) 133 | rescue Exception => e 134 | logger.error("Error occurred during the processing: #{data}\n #{e.inspect} #{e.backtrace}") 135 | end 136 | @events.pop(&process) 137 | } 138 | @events.pop(&process) 139 | end 140 | 141 | # Start telegram-cli daemon 142 | # 143 | # @yield This block will be executed when all connections have responded 144 | def connect(&block) 145 | logger.info("Trying to start telegram-cli and then connect") 146 | @connect_callback = block 147 | process_data 148 | EM.defer(method(:execute), method(:create_pool), method(:execution_failed)) 149 | end 150 | 151 | # Create a connection pool based on the {Connection} and given configuration 152 | # 153 | # @api private 154 | def create_pool(*) 155 | @connection = ConnectionPool.new(@config.size) do 156 | client = EM.connect_unix_domain(@config.sock, Connection) 157 | client.on_connect = self.method(:on_connect) 158 | client.on_disconnect = self.method(:on_disconnect) 159 | client 160 | end 161 | end 162 | 163 | # A event listener that will be called if the {Connection} successes on either of {ConnectionPool} 164 | # 165 | # @api private 166 | def on_connect 167 | @connected += 1 168 | if connected? 169 | logger.info("Successfully connected to the Telegram CLI") 170 | EM.defer(&method(:poll)) 171 | update!(&@connect_callback) 172 | end 173 | end 174 | 175 | # A event listener that will be called if the {Connection} closes on either of {ConnectionPool} 176 | # 177 | # @api private 178 | def on_disconnect 179 | @connected -= 1 180 | if @connected == 0 181 | logger.info("Disconnected from Telegram CLI") 182 | close_stdout 183 | @disconnect_callback.call if @disconnect_callback 184 | end 185 | end 186 | 187 | def on_disconnect=(callback) 188 | @disconnect_callback = callback 189 | end 190 | 191 | # @return [bool] Connection pool status 192 | # @since [0.1.0] 193 | def connected? 194 | @connected == @config.size 195 | end 196 | 197 | private 198 | 199 | def execution_failed(e) 200 | logger.error("Failed execution of telegram-cli: #{e}") 201 | close_stdout 202 | end 203 | 204 | def close_stdout 205 | Process.kill('INT', stdout.pid) 206 | end 207 | 208 | def initialize_stdout_reading 209 | return stdout.readline unless auth_properties.present? 210 | Authorization.new(stdout, auth_properties, logger).perform 211 | end 212 | end 213 | end 214 | -------------------------------------------------------------------------------- /lib/telegram/config.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | require 'forwardable' 3 | 4 | module Telegram 5 | # Telegram client config 6 | # 7 | # Available options: 8 | # 9 | # * daemon: path to telegram-cli binary 10 | # * key: path to telegram-cli public key 11 | # * sock: path to Unix domain socket that telegram-cli will listen to 12 | # * size: connection pool size 13 | class Config 14 | extend Forwardable 15 | 16 | DEFAULT_OPTIONS = { 17 | daemon: 'bin/telegram', 18 | key: 'tg-server.pub', 19 | sock: 'tg.sock', 20 | size: 5 21 | }.freeze 22 | 23 | def_delegators :@options, :daemon, :daemon=, :key, :key=, :sock, :sock=, 24 | :size, :size=, :profile, :profile=, :logger, :logger=, 25 | :config_file, :config_file= 26 | 27 | def initialize(options = {}) 28 | @options = OpenStruct.new(DEFAULT_OPTIONS.merge(options)) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/telegram/connection.rb: -------------------------------------------------------------------------------- 1 | module Telegram 2 | # Telegram-CLI Connection 3 | # 4 | # @note Don't make a connection directly to the telegram-cli 5 | # @see Client 6 | # @see ConnectionPool 7 | # @version 0.1.0 8 | class Connection < EM::Connection 9 | # Initialize connection 10 | # 11 | # @version 0.1.0 12 | def initialize 13 | super 14 | @connected = false 15 | @on_connect = nil 16 | @on_disconnect = nil 17 | @callback = nil 18 | @available = true 19 | @data = '' 20 | end 21 | 22 | # @return [Bool] the availiability of current connection 23 | def available? 24 | @available 25 | end 26 | 27 | # Communicate telegram-rb with telegram-cli connection 28 | # 29 | # @param [Array] messages Messages that will be sent 30 | # @yieldparam [Block] callback Callback block that will be called when finished 31 | def communicate(*messages, &callback) 32 | @available = false 33 | @data = '' 34 | @callback = callback 35 | messages = messages.first if messages.size == 1 and messages.first.is_a?(Array) 36 | messages = messages.join(' ') << "\n" 37 | send_data(messages) 38 | end 39 | 40 | # Set a block that will be called when connected 41 | # 42 | # @param [Block] block 43 | def on_connect=(block) 44 | @on_connect = block 45 | end 46 | 47 | 48 | # Set a block that will be called when disconnected 49 | # 50 | # @param [Block] block 51 | def on_disconnect=(block) 52 | @on_disconnect = block 53 | end 54 | 55 | # This method will be called by EventMachine when connection completed 56 | # 57 | # @api private 58 | def connection_completed 59 | @connected = true 60 | @on_connect.call unless @on_connect.nil? 61 | end 62 | 63 | # This method will be called by EventMachine when connection unbinded 64 | # 65 | # @api private 66 | def unbind 67 | @connected = false 68 | @on_disconnect.call unless @on_disconnect.nil? 69 | end 70 | 71 | # @return [Bool] the availiability of current connection 72 | def connected? 73 | @connected 74 | end 75 | 76 | # This method will be called by EventMachine when data arrived 77 | # then parse given data and execute callback method if exists 78 | # 79 | # @api private 80 | def receive_data(data) 81 | @data << data 82 | 83 | return unless data.index("\n\n") 84 | begin 85 | result = _receive_data(@data) 86 | rescue 87 | raise 88 | result = nil 89 | end 90 | @callback.call(!result.nil?, result) unless @callback.nil? 91 | @callback = nil 92 | @available = true 93 | end 94 | 95 | protected 96 | # Parse received data to correct json format and then convert to Ruby {Hash} 97 | # 98 | # @api private 99 | def _receive_data(data) 100 | if data[0..6] == 'ANSWER ' 101 | lf = data.index("\n") + 1 102 | lflf = data.index("\n\n", lf) - 1 103 | data = data[lf..lflf] 104 | data = Oj.load(data, :mode => :compat) 105 | end 106 | data 107 | end 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /lib/telegram/connection_pool.rb: -------------------------------------------------------------------------------- 1 | module Telegram 2 | # Connection Pool 3 | # 4 | # @see Client 5 | # @version 0.1.0 6 | class ConnectionPool < Array 7 | include Logging 8 | 9 | # @return [Integer] Connection pool size, will be set when initialized 10 | attr_reader :size 11 | 12 | # Initialize ConnectionPool 13 | # 14 | # @param [Integer] size Connection pool size 15 | # @param [Block] block Create a connection in this block, have to pass a {Connection} object 16 | def initialize(size=10, &block) 17 | size.times do 18 | self << block.call if block_given? 19 | end 20 | end 21 | 22 | # Communicate with acquired connection 23 | # 24 | # @see Connection 25 | # @param [Array] messages Messages that will be sent 26 | # @param [Block] block Callback block that will be called when finished 27 | def communicate(*messages, &block) 28 | begin 29 | acquire do |conn| 30 | conn.communicate(*messages, &block) 31 | end 32 | rescue Exception => e 33 | logger.error("Error occurred during the communicating: #{e.inspect} #{e.backtrace}") 34 | end 35 | 36 | end 37 | 38 | # Acquire available connection 39 | # 40 | # @see Connection 41 | # @param [Block] callback This block will be called when successfully acquired a connection 42 | # @yieldparam [Connection] connection acquired connection 43 | def acquire(&callback) 44 | acq = Proc.new { 45 | conn = self.find { |conn| conn.available? } 46 | if not conn.nil? and conn.connected? 47 | callback.call(conn) 48 | else 49 | logger.warn("Failed to acquire available connection, retry after 0.1 second") 50 | EM.add_timer(0.1, &acq) 51 | end 52 | } 53 | EM.add_timer(0, &acq) 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/telegram/events.rb: -------------------------------------------------------------------------------- 1 | module Telegram 2 | # Define Event Types 3 | # 4 | # @see Event 5 | # @since [0.1.0] 6 | module EventType 7 | # Unknown Event 8 | # @since [0.1.0] 9 | UNKNOWN_EVENT = -1 10 | # Service 11 | # @since [0.1.0] 12 | SERVICE = 0 13 | # Receive Message 14 | # @since [0.1.0] 15 | RECEIVE_MESSAGE = 1 16 | # Send Message 17 | # @since [0.1.0] 18 | SEND_MESSAGE = 2 19 | # Online Status Changes 20 | # @since [0.1.0] 21 | ONLINE_STATUS = 3 22 | end 23 | 24 | # Define Action Types 25 | # 26 | # @see Event 27 | # @since [0.1.0] 28 | module ActionType 29 | # Unknown Action 30 | # @since [0.1.0] 31 | UNKNOWN_ACTION = -1 32 | # No Action 33 | # @since [0.1.0] 34 | NO_ACTION = 0 35 | # Adde a user to the chat 36 | # @since [0.1.0] 37 | CHAT_ADD_USER = 1 38 | # Remove a user from the chat 39 | # @since [0.1.0] 40 | CHAT_DEL_USER = 2 41 | # Rename title of the chat 42 | # @since [0.1.0] 43 | CHAT_RENAME = 3 44 | # Create group chat 45 | # @since [0.1.0] 46 | CREATE_GROUP_CHAT = 4 47 | 48 | ADD_CONTACT = 5 49 | end 50 | 51 | # Message object belong to {Event} class instance 52 | # 53 | # @see Event 54 | # @since [0.1.0] 55 | class Message < Struct.new(:id, :text, :type, :from, :from_type, :raw_from, :to, :to_type, :raw_to) 56 | # @!attribute id 57 | # @return [Number] Message Identifier 58 | attr_accessor :id 59 | 60 | # @!attribute text 61 | # @return [String] The text of the message 62 | attr_accessor :text 63 | 64 | # @!attribute type 65 | # @return [String] The type of the message (either of text, photo, video or etc) 66 | attr_accessor :type 67 | 68 | # @!attribute from 69 | # @return [TelegramContact] The sender of the message 70 | attr_accessor :from 71 | 72 | # @!attribute from_type 73 | # @return [String] The type of the sender 74 | attr_accessor :from_type 75 | 76 | # @!attribute raw_from 77 | # @return [String] Raw identifier string of the sender 78 | attr_accessor :raw_from 79 | 80 | # @!attribute to 81 | # @return [TelegramChat] If you receive a message in the chat group 82 | # @return [TelegramContact] If you receive a message in the chat with one contact 83 | attr_accessor :to 84 | 85 | # @!attribute to_type 86 | # @return [String] The type of the receiver 87 | attr_accessor :to_type 88 | 89 | # @!attribute raw_to 90 | # @return [String] Raw identifier string of the receiver 91 | attr_accessor :raw_to 92 | end 93 | 94 | # Event object, will be created in the process part of {Client} 95 | # 96 | # @see Client 97 | # @since [0.1.0] 98 | class Event 99 | # @return [Number] Event identifier 100 | attr_reader :id 101 | 102 | # @return [EventType] Event type, created from given data 103 | attr_reader :event 104 | 105 | # @return [ActionType] Action type, created from given data 106 | attr_reader :action 107 | 108 | # @return [Time] Time event received 109 | attr_reader :time 110 | 111 | # @return [Message] Message object, created from given data 112 | attr_reader :message 113 | 114 | # @return [TelegramMessage] Telegram message object, created from {Message} 115 | attr_reader :tgmessage 116 | 117 | # Create a new {Event} instance 118 | # 119 | # @param [Client] client Root client instance 120 | # @param [EventType] event Event type 121 | # @param [ActionType] action Action type 122 | # @param [Hash] data Raw data 123 | # @since [0.1.0] 124 | def initialize(client, event = EventType::UNKNOWN_EVENT, action = ActionType::NO_ACTION, data = {}) 125 | @client = client 126 | @id = data.respond_to?(:[]) ? data['id'] : '' 127 | @message = nil 128 | @tgmessage = nil 129 | @raw_data = data 130 | @time = nil 131 | 132 | @event = event 133 | @action = action 134 | 135 | @time = Time.at(data['date'].to_i) if data.has_key?('date') 136 | @time = DateTime.strptime(data['when'], "%Y-%m-%d %H:%M:%S") if @time.nil? and data.has_key?('when') 137 | 138 | case event 139 | when EventType::SERVICE 140 | foramt_service 141 | when EventType::RECEIVE_MESSAGE, EventType::SEND_MESSAGE 142 | format_message 143 | @tgmessage = TelegramMessage.new(@client, self) 144 | when EventType::ONLINE_STATUS 145 | foramt_status 146 | end 147 | end 148 | 149 | # Process raw data in which event type is service given. 150 | # 151 | # @return [void] 152 | # @api private 153 | def format_service 154 | 155 | end 156 | 157 | # Process raw data in which event type is message given. 158 | # 159 | # @return [void] 160 | # @api private 161 | def format_message 162 | message = Message.new 163 | 164 | message.id = @id 165 | message.text = @raw_data['text'] ||= '' 166 | media = @raw_data['media'] 167 | message.type = media ? media['type'] : 'text' 168 | message.raw_from = @raw_data['from']['peer_id'] 169 | message.from_type = @raw_data['from']['peer_type'] 170 | message.raw_to = @raw_data['to']['peer_id'] 171 | message.to_type = @raw_data['to']['peer_type'] 172 | 173 | from = @client.contacts.find { |c| c.id == message.raw_from } 174 | to = @client.contacts.find { |c| c.id == message.raw_to } 175 | to = @client.chats.find { |c| c.id == message.raw_to } if to.nil? 176 | 177 | message.from = from 178 | message.to = to 179 | 180 | @message = message 181 | 182 | if @message.from.nil? 183 | user = @raw_data['from'] 184 | user = TelegramContact.pick_or_new(@client, user) 185 | @client.contacts << user unless @client.contacts.include?(user) 186 | @message.from = user 187 | end 188 | 189 | if @message.to.nil? 190 | type = @raw_data['to']['peer_type'] 191 | case type 192 | when 'chat', 'encr_chat' 193 | chat = TelegramChat.pick_or_new(@client, @raw_data['to']) 194 | @client.chats << chat unless @client.chats.include?(chat) 195 | if type == 'encr_chat' then 196 | @message.to = chat 197 | else 198 | @message.from = chat 199 | end 200 | when 'user' 201 | user = TelegramContact.pick_or_new(@client, @raw_data['to']) 202 | @client.contacts << user unless @client.contacts.include?(user) 203 | @message.to = user 204 | end 205 | end 206 | end 207 | 208 | # Convert {Event} instance to the string format 209 | # 210 | # @return [String] 211 | def to_s 212 | "" 213 | end 214 | end 215 | end 216 | -------------------------------------------------------------------------------- /lib/telegram/logger.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | 3 | module Telegram 4 | # Module for logging: 5 | # You can use logger using this module 6 | # @example 7 | # class Klass 8 | # include Telegram::Logging 9 | # def initialize 10 | # logger.info("Initialized!") # => [1970-01-01 00:00:00] INFO (Klass): Initialized! 11 | # end 12 | # end 13 | # 14 | # @since [0.1.0] 15 | module Logging 16 | # Get logger, will be initialized within a class 17 | # 18 | # @return [Logger] logger object 19 | def logger 20 | @logger ||= Logging.logger_for(self.class.name) 21 | end 22 | 23 | @loggers = {} 24 | class << self 25 | # Logger pool, acquire logger from the logger table or not, create a new logger 26 | # 27 | # @param [String] klass Class name 28 | # @return [Logger] Logger instance 29 | # @api private 30 | def logger_for(klass) 31 | @loggers[klass] ||= configure_logger_for(klass) 32 | end 33 | 34 | # Create a new logger 35 | # 36 | # @param [String] klass Class name 37 | # @return [Logger] Logger instance 38 | # @api private 39 | def configure_logger_for(klass) 40 | logger = Logger.new(STDOUT) 41 | logger.progname = klass 42 | logger.level = Logger::DEBUG 43 | logger.formatter = proc do |severity, datetime, progname, msg| 44 | date_format = datetime.strftime('%Y-%m-%d %H:%M:%S') 45 | blanks = severity.size == 4 ? ' ' : ' ' 46 | "[#{date_format}] #{severity}#{blanks}(#{progname}): #{msg}\n" 47 | end 48 | 49 | logger 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/telegram/models.rb: -------------------------------------------------------------------------------- 1 | module Telegram 2 | # Base class for Telegram models 3 | # 4 | # @see TelegramChat 5 | # @see TelegramRoom 6 | # @since [0.1.0] 7 | class TelegramBase 8 | include Logging 9 | 10 | # @return [Client] Root client instance 11 | attr_reader :client 12 | 13 | # @return [Integer] Identifier 14 | attr_reader :id 15 | 16 | # Return an instance if exists, or else create a new instance and return 17 | # 18 | # @param [Client] client Root client instance 19 | # @param [Integer] raw Raw data on either of telegram objects 20 | # @since [0.1.0] 21 | def self.pick_or_new(client, raw) 22 | # where to search for 23 | where = if self == TelegramChat 24 | client.chats 25 | elsif self == TelegramContact 26 | client.contacts 27 | end 28 | 29 | # pick a first item if exists, or else create 30 | where.find { |obj| obj.id == raw['peer_id'] } or self.new(client, raw) 31 | end 32 | 33 | # Convert to telegram-cli target format from {TelegramChat} or {TelegramContact} 34 | # 35 | # @since [0.1.1] 36 | def targetize 37 | @type == 'encr_chat' ? @title : to_tg 38 | end 39 | 40 | # Execute a callback block with failure result 41 | # 42 | # @since [0.1.1] 43 | # @api private 44 | def fail_back(&callback) 45 | callback.call(false, {}) unless callback.nil? 46 | end 47 | 48 | # Send typing signal 49 | # 50 | # @param [Block] callback Callback block that will be called when finished 51 | # @since [0.1.1] 52 | def send_typing(&callback) 53 | if @type == 'encr_chat' 54 | logger.warn("Currently telegram-cli has a bug with send_typing, then prevent this for safety") 55 | return 56 | end 57 | @client.send_typing(targetize, &callback) 58 | end 59 | 60 | # Abort sending typing signal 61 | # 62 | # @param [Block] callback Callback block that will be called when finished 63 | # @since [0.1.1] 64 | def send_typing_abort(&callback) 65 | if @type == 'encr_chat' 66 | logger.warn("Currently telegram-cli has a bug with send_typing, then prevent this for safety") 67 | return 68 | end 69 | @client.send_typing_abort(targetize, &callback) 70 | end 71 | 72 | # Send a message with given text 73 | # 74 | # @param [String] text text you want to send for 75 | # @param [TelegramMessage] refer referrer of the method call 76 | # @param [Block] callback Callback block that will be called when finished 77 | # @since [0.1.0] 78 | def send_message(text, refer, &callback) 79 | @client.msg(targetize, text, &callback) 80 | end 81 | 82 | # @abstract Send a sticker 83 | def send_sticker() 84 | 85 | end 86 | 87 | # Send an image 88 | # 89 | # @param [String] path The absoulte path of the image you want to send 90 | # @param [TelegramMessage] refer referral of the method call 91 | # @param [Block] callback Callback block that will be called when finished 92 | # @since [0.1.1] 93 | def send_image(path, refer, &callback) 94 | if @type == 'encr_chat' 95 | logger.warn("Currently telegram-cli has a bug with send_typing, then prevent this for safety") 96 | return 97 | end 98 | fail_back(&callback) if not File.exist?(path) 99 | @client.send_photo(targetize, path, &callback) 100 | end 101 | 102 | # Send an image with given url, not implemen 103 | # 104 | # @param [String] url The URL of the image you want to send 105 | # @param [TelegramMessage] refer referral of the method call 106 | # @param [Block] callback Callback block that will be called when finished 107 | # @since [0.1.1] 108 | def send_image_url(url, opt, refer, &callback) 109 | begin 110 | opt = {} if opt.nil? 111 | http = EM::HttpRequest.new(url, :connect_timeout => 2, :inactivity_timeout => 5).get opt 112 | file = Tempfile.new(['image', 'jpg']) 113 | http.stream { |chunk| 114 | file.write(chunk) 115 | } 116 | http.callback { 117 | file.close 118 | type = FastImage.type(file.path) 119 | if %i(jpeg png gif).include?(type) 120 | send_image(file.path, refer, &callback) 121 | else 122 | fail_back(&callback) 123 | end 124 | } 125 | rescue Exception => e 126 | logger.error("An error occurred during the image downloading: #{e.inspect} #{e.backtrace}") 127 | fail_back(&callback) 128 | end 129 | end 130 | 131 | # Send a video 132 | # 133 | # @param [String] path The absoulte path of the video you want to send 134 | # @param [TelegramMessage] refer referral of the method call 135 | # @param [Block] callback Callback block that will be called when finished 136 | # @since [0.1.1] 137 | def send_video(path, refer, &callback) 138 | fail_back(&callback) if not File.exist?(path) 139 | @client.send_video(targetize, path, &callback) 140 | end 141 | end 142 | 143 | # Telegram Chat Model 144 | # 145 | # @see TelegramBase 146 | # @since [0.1.0] 147 | class TelegramChat < TelegramBase 148 | # @return [String] The name of the chat 149 | attr_reader :name 150 | 151 | # @return [Array] The members of the chat 152 | attr_reader :members 153 | 154 | # @return [String] The type of the chat (chat, encr_chat, user and etc) 155 | attr_reader :type 156 | 157 | # Create a new chat instance 158 | # 159 | # @param [Client] client Root client instance 160 | # @param [Integer] chat Raw chat data 161 | # @since [0.1.0] 162 | def initialize(client, chat) 163 | @client = client 164 | @chat = chat 165 | 166 | @id = chat['peer_id'] 167 | @name = @title = chat.has_key?('title') ? chat['title'] : chat['print_name'] 168 | @type = chat['peer_type'] 169 | 170 | @members = [] 171 | if chat.has_key?('members') 172 | chat['members'].each { |user| 173 | @members << TelegramContact.pick_or_new(client, user) 174 | } 175 | elsif @type == 'user' and chat['user'] 176 | @members << TelegramContact.pick_or_new(client, chat) 177 | end 178 | end 179 | 180 | # Leave from current chat 181 | # 182 | # @since [0.1.0] 183 | def leave! 184 | @client.chat_del_user(self, @client.profile.to_tg) 185 | end 186 | 187 | # @return [String] A chat identifier formatted with type 188 | def to_tg 189 | "#{@type}\##{@id}" 190 | end 191 | 192 | # Convert {Event} instance to the string format 193 | # 194 | # @return [String] 195 | def to_s 196 | "" 197 | end 198 | end 199 | 200 | # Telegram Contact Model 201 | # 202 | # @see TelegramBase 203 | # @since [0.1.0] 204 | class TelegramContact < TelegramBase 205 | # @return [String] The name of the contact 206 | attr_reader :name 207 | 208 | # @return [String] The username of the contact 209 | attr_reader :username 210 | 211 | # @return [Array] The phone number of the contact 212 | attr_reader :phone 213 | 214 | # @return [String] The type of the contact # => "user" 215 | attr_reader :type 216 | 217 | # Create a new contact instance 218 | # 219 | # @param [Client] client Root client instance 220 | # @param [Integer] contact Raw chat contact 221 | # @since [0.1.0] 222 | def initialize(client, contact) 223 | @client = client 224 | @contact = contact 225 | 226 | @id = contact['peer_id'] 227 | @type = 'user' 228 | @username = contact.has_key?('username') ? contact['username'] : '' 229 | @name = contact['print_name'] 230 | @phone = contact.has_key?('phone') ? contact['phone'] : '' 231 | 232 | @client.contacts << self unless @client.contacts.include?(self) 233 | end 234 | 235 | # @return [Array] Chats that contact participates 236 | # @since [0.1.0] 237 | def chats 238 | @client.chats.select { |c| c.member.include?(self) } 239 | end 240 | 241 | # @return [String] A chat identifier formatted with type 242 | def to_tg 243 | "#{@type}\##{@id}" 244 | end 245 | 246 | # Convert {Event} instance to the string format 247 | # 248 | # @return [String] 249 | def to_s 250 | "" 251 | end 252 | end 253 | 254 | # Telegram Message Model 255 | # 256 | # @see Event 257 | # @since [0.1.0] 258 | class TelegramMessage 259 | # @return [Client] Root client instance 260 | attr_reader :client 261 | 262 | # @return [String] Raw string of the text 263 | attr_reader :raw 264 | 265 | # @return [Integer] Message identifier 266 | attr_reader :id 267 | 268 | # @return [Time] Time message received 269 | attr_reader :time 270 | 271 | # @return [TelegramContact] The contact who sent this message 272 | attr_reader :user 273 | 274 | # @return [String] 275 | attr_reader :raw_target 276 | 277 | # @return [String] Content type 278 | attr_reader :content_type 279 | 280 | # @return [TelegramChat] if you were talking in a chat group 281 | # @return [TelegramContact] if you were talking with contact 282 | attr_reader :target 283 | 284 | # Create a new tgmessage instance 285 | # 286 | # @param [Client] client Root client instance 287 | # @param [Event] event Root event instance 288 | # @see Event 289 | # @since [0.1.0] 290 | def initialize(client, event) 291 | @event = event 292 | 293 | @id = event.id 294 | @raw = event.message.text 295 | @time = event.time 296 | @content_type = event.message.type 297 | 298 | @raw_sender = event.message.raw_from 299 | @raw_receiver = event.message.raw_to 300 | 301 | @user = @sender = event.message.from 302 | @receiver = event.message.to 303 | 304 | @target = 305 | begin 306 | case @receiver.type 307 | when 'user' 308 | @sender 309 | when 'chat', 'encr_chat' 310 | @receiver 311 | end 312 | rescue NoMethodError 313 | @sender 314 | end 315 | end 316 | 317 | # @abstract Reply a message to the sender (peer to peer) 318 | # 319 | # @param [Symbol] type Type of the message (either of :text, :sticker, :image) 320 | # @param [String] content Content to send a message 321 | # @param [Block] callback Callback block that will be called when finished 322 | def reply_user(type, content, &callback) 323 | 324 | end 325 | 326 | # Reply a message to the chat 327 | # 328 | # @param [Symbol] type Type of the message (either of :text, :sticker, :image) 329 | # @param [String] content Content to send a message 330 | # @param [TelegramChat] target Specify a TelegramChat to send 331 | # @param [TelegramContact] target Specify a TelegramContact to send 332 | # @param [Block] callback Callback block that will be called when finished 333 | # @since [0.1.0] 334 | def reply(type, content, target=nil, &callback) 335 | target = @target if target.nil? 336 | 337 | case type 338 | when :text 339 | target.send_message(content, self, &callback) 340 | when :image 341 | option = nil 342 | content, option = content if content.class == Array 343 | if content.include?('http') 344 | target.method(:send_image_url).call(content, option, self, &callback) 345 | else 346 | target.method(:send_image).call(content, self, &callback) 347 | end 348 | when :video 349 | target.send_video(content, self, &callback) 350 | end 351 | end 352 | 353 | def members 354 | contact_list = [] 355 | if @target.class == TelegramContact 356 | contact_list << @target 357 | else 358 | contact_list = @target.members 359 | end 360 | 361 | contact_list 362 | end 363 | 364 | # Convert {TelegramMessage} instance to the string format 365 | # 366 | # @return [String] 367 | def to_s 368 | "" 369 | end 370 | end 371 | end 372 | -------------------------------------------------------------------------------- /lib/telegram/version.rb: -------------------------------------------------------------------------------- 1 | module Telegram 2 | # version of the library 3 | VERSION = '0.1.0' 4 | end 5 | -------------------------------------------------------------------------------- /telegram-rb.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.join(File.dirname(__FILE__), 'lib') 2 | $:.unshift lib unless $:.include?(lib) 3 | 4 | require 'telegram/version' 5 | 6 | Gem::Specification.new do |s| 7 | s.date = Date.today.to_s 8 | s.name = 'telegram-rb' 9 | s.version = Telegram::VERSION 10 | s.licenses = ['MIT'] 11 | s.summary = 'A Ruby wrapper that communicates with the telegram-cli.' 12 | s.description = "A Ruby wrapper that communicates with the telegram-cli." 13 | s.authors = ["SuHun Han (ssut)"] 14 | s.email = 'ssut@ssut.me' 15 | s.files = `git ls-files`.split("\n") 16 | s.homepage = 'https://github.com/ssut/telegram-rb' 17 | 18 | s.add_dependency('eventmachine') 19 | s.add_dependency('em-synchrony') 20 | s.add_dependency('em-http-request') 21 | s.add_dependency('fastimage') 22 | s.add_dependency('oj') 23 | 24 | s.add_development_dependency('rubysl-irb') 25 | s.add_development_dependency('yard') 26 | s.add_development_dependency('ruby-prof') 27 | end 28 | --------------------------------------------------------------------------------