2019-04-06 06:12:54 +00:00
require 'tdlib-ruby'
2019-04-07 11:58:07 +00:00
require 'digest'
2019-04-06 07:29:48 +00:00
2019-04-06 06:12:54 +00:00
class TelegramClient
# tdlib configuration, shared within all instances #
def self . configure ( params )
2019-04-07 11:58:07 +00:00
@@loglevel = params [ 'loglevel' ] || Logger :: DEBUG
@@content_path = params [ 'content_path' ] || '/tmp'
@@content_link = params [ 'content_link' ] || 'https://localhost/tg_media'
@@content_size_limit = params [ " content_size_limit " ] || 100 * 1024 * 1024
2019-04-06 06:12:54 +00:00
TD . configure do | config |
2019-04-07 11:58:07 +00:00
config . lib_path = params [ 'path' ] || 'lib/' # we hope it's here
config . client . api_id = params [ 'api_id' ] || '17349' # desktop telegram app
config . client . api_hash = params [ 'api_hash' ] || '344583e45741c457fe1862106095a5eb' # desktop telegram app
config . client . device_model = params [ 'useragent' ] || 'Zhabogram XMPP Gateway'
config . client . application_version = params [ 'version' ] || '-1.0' # hmm...
config . client . use_test_dc = params [ 'use_test_dc' ] || false
config . client . system_version = '42' # I think I have permission to hardcode The Ultimate Question of Life, the Universe, and Everything?..
2019-04-06 06:12:54 +00:00
end
2019-04-07 11:58:07 +00:00
TD :: Api . set_log_verbosity_level ( params [ 'verbosity' ] || 1 )
2019-04-06 06:12:54 +00:00
end
# instance initialization #
def initialize ( xmpp , login )
2019-04-07 11:58:07 +00:00
return if not @@loglevel # call .configure() first
2019-04-06 06:12:54 +00:00
2019-04-07 11:58:07 +00:00
@logger = Logger . new ( STDOUT ) ; @logger . level = @@loglevel ; @logger . progname = '[TelegramClient: %s/%s]' % [ xmpp . user_jid , login ] # create logger
2019-04-06 08:23:14 +00:00
@xmpp = xmpp # our XMPP user session. we will send messages back to Jabber through this instance.
@login = login # store tg login
2019-04-08 22:28:26 +00:00
@cache = { chats : { } , users : { } , unread_msg : { } } # we will store our cache here
2019-04-07 11:58:07 +00:00
@files_dir = File . dirname ( __FILE__ ) + '/../sessions/' + @xmpp . user_jid + '/files/'
2019-04-06 06:12:54 +00:00
2019-04-06 08:23:14 +00:00
# spawn telegram client and specify callback handlers
2019-04-09 06:42:42 +00:00
@logger . info 'Connecting to Telegram network..'
2019-04-07 11:58:07 +00:00
@client = TD :: Client . new ( database_directory : 'sessions/' + @xmpp . user_jid , files_directory : 'sessions/' + @xmpp . user_jid + '/files/' ) # create telegram client instance
2019-04-06 06:12:54 +00:00
@client . on ( TD :: Types :: Update :: AuthorizationState ) do | update | self . auth_handler ( update ) end # register auth update handler
@client . on ( TD :: Types :: Update :: NewMessage ) do | update | self . message_handler ( update ) end # register new message update handler
2019-04-07 11:58:07 +00:00
@client . on ( TD :: Types :: Update :: MessageContent ) do | update | self . message_edited_handler ( update ) end # register msg edited handler
@client . on ( TD :: Types :: Update :: DeleteMessages ) do | update | self . message_deleted_handler ( update ) end # register msg del handler
@client . on ( TD :: Types :: Update :: File ) do | update | self . file_handler ( update ) end # register file handler
@client . on ( TD :: Types :: Update :: NewChat ) do | update | self . new_chat_handler ( update ) end # register new chat handler
@client . on ( TD :: Types :: Update :: UserStatus ) do | update | self . status_update_handler ( update ) end # register status handler
2019-04-06 06:12:54 +00:00
@client . connect #
2019-04-06 08:23:14 +00:00
# we will check for outgoing messages in a queue and/or auth data from XMPP thread while XMPP indicates that service is online #
2019-04-06 07:29:48 +00:00
begin
2019-04-06 07:46:29 +00:00
while not @xmpp . online? === false do
2019-04-06 06:12:54 +00:00
self . process_outgoing_msg ( @xmpp . message_queue . pop ) unless @xmpp . message_queue . empty? # found something in message queue
self . process_auth ( :code , @xmpp . tg_auth_data [ :code ] ) unless @xmpp . tg_auth_data [ :code ] . nil? # found code in auth queue
self . process_auth ( :password , @xmpp . tg_auth_data [ :password ] ) unless @xmpp . tg_auth_data [ :password ] . nil? # found 2fa password in auth queue
2019-04-09 06:42:42 +00:00
sleep 0 . 1
2019-04-06 06:12:54 +00:00
end
2019-04-07 11:58:07 +00:00
rescue Exception = > e
@logger . error 'Unexcepted exception! %s' % e . to_s
2019-04-06 07:29:48 +00:00
ensure
@logger . info 'Exitting gracefully...'
@client . dispose
end
2019-04-06 06:12:54 +00:00
end
2019-04-06 07:29:48 +00:00
###########################################
## Callback handlers #####################
###########################################
2019-04-06 06:12:54 +00:00
# authorization handler #
def auth_handler ( update )
2019-04-06 07:29:48 +00:00
@logger . debug 'Authorization state changed: %s' % update . authorization_state
2019-04-06 06:12:54 +00:00
2019-04-06 07:29:48 +00:00
case update . authorization_state
2019-04-06 06:12:54 +00:00
# auth stage 0: specify login #
when TD :: Types :: AuthorizationState :: WaitPhoneNumber
2019-04-06 07:29:48 +00:00
@logger . info 'Logging in..'
2019-04-06 06:12:54 +00:00
@client . set_authentication_phone_number ( @login )
# auth stage 1: wait for authorization code #
when TD :: Types :: AuthorizationState :: WaitCode
2019-04-06 07:29:48 +00:00
@logger . info 'Waiting for authorization code..'
2019-04-06 06:12:54 +00:00
@xmpp . send_message ( nil , 'Please, enter authorization code via /code 12345' )
# auth stage 2: wait for 2fa passphrase #
when TD :: Types :: AuthorizationState :: WaitPassword
2019-04-06 07:29:48 +00:00
@logger . info 'Waiting for 2FA password..'
2019-04-06 06:12:54 +00:00
@xmpp . send_message ( nil , 'Please, enter 2FA passphrase via /password 12345' )
2019-04-07 11:58:07 +00:00
# authorization successful -- indicate that client is online and retrieve contact list #
2019-04-06 06:12:54 +00:00
when TD :: Types :: AuthorizationState :: Ready
2019-04-06 07:29:48 +00:00
@logger . info 'Authorization successful!'
2019-04-06 07:46:29 +00:00
@xmpp . online!
2019-04-07 11:58:07 +00:00
@client . get_chats ( limit = 9999 ) . then { | chats | chats . chat_ids . each do | chat_id | self . process_chat_info ( chat_id ) end } . wait
@logger . info " Contact list updating finished "
self . sync_roster ( )
when TD :: Types :: AuthorizationState :: Closed
@logger . info 'Session closed.'
@xmpp . offline!
2019-04-06 06:12:54 +00:00
end
end
# message from telegram network handler #
def message_handler ( update )
2019-04-09 06:42:42 +00:00
@logger . debug 'Got NewMessage update'
2019-04-07 11:58:07 +00:00
@logger . debug update . message . to_json
return if update . message . is_outgoing # ignore outgoing
return if not @cache [ :chats ] . key? update . message . chat_id
# media? #
content = nil
@logger . debug update . message . content . to_json
case update . message . content # content = [content, name, mime]
when TD :: Types :: MessageContent :: Photo then content = [ update . message . content . photo . sizes [ - 1 ] . photo , update . message . content . photo . id . to_s + '.jpg' , 'image/jpeg' ]
when TD :: Types :: MessageContent :: Sticker then content = [ update . message . content . sticker . sticker , update . message . content . sticker . emoji . to_s + '.webp' , 'image/webp' ]
when TD :: Types :: MessageContent :: Audio then content = [ update . message . content . audio . audio , update . message . content . audio . file_name . to_s , update . message . content . audio . mime_type . to_s ]
when TD :: Types :: MessageContent :: Document then content = [ update . message . content . document . document , update . message . content . document . file_name . to_s , update . message . content . document . mime_type . to_s ]
end
@client . download_file ( content [ 0 ] . id ) if content # download it if already not
# formatting...
2019-04-08 20:46:16 +00:00
text = ( content . nil? ) ? update . message . content . text . text . to_s : update . message . content . caption . text . to_s
text = " [%s (%s), %d bytes] | %s | %s " % [ content [ 1 ] , content [ 2 ] , content [ 0 ] . size . to_i , self . format_content_link ( content [ 0 ] . remote . id , content [ 1 ] ) , text ] if content # content format
2019-04-07 11:58:07 +00:00
text = " [FWD From %s] %s " % [ self . format_username ( update . message . forward_info . sender_user_id ) , text ] if update . message . forward_info . instance_of? TD :: Types :: MessageForwardInfo :: MessageForwardedFromUser # fwd
text = " [Reply to MSG %s] %s " % [ update . message . reply_to_message_id . to_s , text ] if update . message . reply_to_message_id . to_i != 0 # reply
text = " [MSG %s] [%s] %s " % [ update . message . id . to_s , self . format_username ( update . message . sender_user_id ) , text ] # username/id
2019-04-08 22:28:26 +00:00
# send and add message id to unreads
@cache [ :unread_msg ] [ update . message . chat_id ] = update . message . id
2019-04-07 11:58:07 +00:00
@xmpp . send_message ( update . message . chat_id . to_s , text )
end
# new chat update -- when tg client discovers new chat #
def new_chat_handler ( update )
2019-04-09 06:42:42 +00:00
@logger . debug 'Got NewChat update'
2019-04-07 11:58:07 +00:00
@logger . debug update . to_json
self . process_chat_info ( update . chat . id )
end
# edited msg #
def message_edited_handler ( update )
2019-04-09 06:42:42 +00:00
@logger . debug 'Got MessageEdited update'
2019-04-07 11:58:07 +00:00
@logger . debug update . to_json
# formatting
text = " [MSG %s EDIT] %s " % [ update . message_id . to_s , update . new_content . text . text . to_s ]
@xmpp . send_message ( update . chat_id . to_s , text )
2019-04-06 06:12:54 +00:00
end
2019-04-07 11:58:07 +00:00
# deleted msg #
def message_deleted_handler ( update )
2019-04-09 06:42:42 +00:00
@logger . debug 'Got MessageDeleted update'
2019-04-07 11:58:07 +00:00
@logger . debug update . to_json
return if not update . is_permanent
text = " [MSG ID %s DELETE] " % update . message_ids . join ( ',' )
@xmpp . send_message ( update . chat_id . to_s , text )
end
# file msg -- symlink to download path #
def file_handler ( update )
2019-04-09 06:42:42 +00:00
@logger . debug 'Got File update'
2019-04-07 11:58:07 +00:00
@logger . debug update . to_json
if update . file . local . is_downloading_completed then
fname = update . file . local . path . to_s
2019-04-08 20:46:16 +00:00
target = " %s/%s%s " % [ @@content_path , Digest :: SHA256 . hexdigest ( " Current user = %s, File ID = %s " % [ @tg_login . to_s , update . file . remote . id ] ) , File . extname ( fname ) ]
2019-04-07 11:58:07 +00:00
@logger . debug 'Downloading of <%s> completed! Link to <%s>' % [ fname , target ]
File . symlink ( fname , target )
end
end
# status update handler #
def status_update_handler ( update )
2019-04-09 06:42:42 +00:00
@logger . debug 'Got new StatusUpdate'
2019-04-07 11:58:07 +00:00
@logger . debug update . to_json
presence , message = self . format_status ( update . status )
@xmpp . presence_update ( update . user_id . to_s , presence , message )
end
2019-04-06 06:12:54 +00:00
2019-04-06 07:29:48 +00:00
###########################################
## LooP handlers #########################
###########################################
2019-04-06 06:12:54 +00:00
# processing authorization #
def process_auth ( typ , data )
2019-04-09 06:42:42 +00:00
@logger . debug 'check_authorization :%s..' % typ . to_s
2019-04-06 06:12:54 +00:00
@client . check_authentication_code ( data ) if typ == :code
@client . check_authentication_password ( data ) if typ == :password
2019-04-09 06:42:42 +00:00
@xmpp . tg_auth_data = { }
2019-04-06 06:12:54 +00:00
end
# processing outgoing message from queue #
def process_outgoing_msg ( msg )
2019-04-09 06:42:42 +00:00
@logger . debug 'Sending message to user/chat <%s> within Telegram network..' % msg [ :to ]
2019-04-08 22:28:26 +00:00
chat_id , text , reply_to = msg [ :to ] . to_i , msg [ :text ] , 0
# handling replies #
if msg [ :text ] [ 0 ] == '>' then
splitted = msg [ :text ] . split ( " \n " )
reply_to , reply_text = splitted [ 0 ] . scan ( / \ d / ) [ 0 ] || 0
text = splitted . drop ( 1 ) . join ( " \n " ) if reply_to != 0
end
# handle commands... (todo) #
#
# mark all messages within this chat as read #
@client . view_messages ( chat_id , [ @cache [ :unread_msg ] [ chat_id ] ] , force_read : true ) if @cache [ :unread_msg ] [ chat_id ]
@cache [ :unread_msg ] [ chat_id ] = nil
# send message #
message = TD :: Types :: InputMessageContent :: Text . new ( :text = > { :text = > text , :entities = > [ ] } , :disable_web_page_preview = > true , :clear_draft = > false )
@client . send_message ( chat_id , message , reply_to_message_id : reply_to )
2019-04-06 06:12:54 +00:00
end
2019-04-07 11:58:07 +00:00
# update users information and save it to cache #
def process_chat_info ( chat_id )
2019-04-09 06:42:42 +00:00
@logger . debug 'Updating chat id %s..' % chat_id . to_s
2019-04-07 11:58:07 +00:00
# fullfil cache.. pasha durov, privet. #
@client . get_chat ( chat_id ) . then { | chat |
@cache [ :chats ] [ chat_id ] = chat # cache chat
self . process_user_info ( chat . type . user_id ) if chat . type . instance_of? TD :: Types :: ChatType :: Private # cache user if it is private chat
} . wait
# send to roster #
if @cache [ :chats ] . key? chat_id
2019-04-09 06:42:42 +00:00
@logger . debug " Sending presence to roster.. "
2019-04-07 11:58:07 +00:00
@xmpp . subscription_req ( chat_id . to_s , @cache [ :chats ] [ chat_id ] . title . to_s ) # send subscription request
case @cache [ :chats ] [ chat_id ] . type # determine status / presence
when TD :: Types :: ChatType :: BasicGroup , TD :: Types :: ChatType :: Supergroup then presence , status = :chat , @cache [ :chats ] [ chat_id ] . title . to_s
when TD :: Types :: ChatType :: Private then presence , status = self . format_status ( @cache [ :users ] [ chat_id ] . status )
end
@xmpp . presence_update ( chat_id . to_s , presence , status ) # send presence
end
end
# update user info #
def process_user_info ( user_id )
2019-04-09 06:42:42 +00:00
@logger . debug 'Updating user id %s..' % user_id . to_s
2019-04-07 11:58:07 +00:00
@client . get_user ( user_id ) . then { | user | @cache [ :users ] [ user_id ] = user } . wait
end
2019-04-06 07:29:48 +00:00
2019-04-07 11:58:07 +00:00
###########################################
## Format functions #######################
###########################################
# convert telegram status to XMPP one
def format_status ( status )
presence , message = nil , ''
case status
when TD :: Types :: UserStatus :: Online
presence = nil
message = " Online "
when TD :: Types :: UserStatus :: Offline
presence = ( Time . now . getutc . to_i - status . was_online . to_i < 3600 ) ? :away : :xa
message = DateTime . strptime ( status . was_online . to_s , '%s' ) . strftime ( " Last seen at %H:%M %d/%m/%Y " )
when TD :: Types :: UserStatus :: Recently
presence = :dnd
message = " Last seen recently "
when TD :: Types :: UserStatus :: LastWeek
presence = :unavailable
message = " Last seen last week "
when TD :: Types :: UserStatus :: LastMonth
presence = :unavailable
message = " Last seen last month "
end
return presence , message
end
# format tg user name #
def format_username ( user_id )
if not @cache [ :users ] . key? user_id then self . process_user_info ( user_id ) end
2019-04-08 22:28:26 +00:00
id = ( @cache [ :users ] [ user_id ] . username == '' ) ? user_id : @cache [ :users ] [ user_id ] . username
2019-04-09 06:42:42 +00:00
name = '%s %s (@%s)' % [ @cache [ :users ] [ user_id ] . first_name , @cache [ :users ] [ user_id ] . last_name , id ]
2019-04-07 11:58:07 +00:00
name . sub! ' ]' , ']'
return name
end
# format content link #
def format_content_link ( file_id , fname )
path = " %s/%s%s " % [ @@content_link , Digest :: SHA256 . hexdigest ( " Current user = %s, File ID = %s " % [ @tg_login . to_s , file_id . to_s ] ) . to_s , File . extname ( fname ) ]
return path
end
2019-04-06 06:12:54 +00:00
end