2019-04-07 11:58:07 +00:00
require 'sqlite3'
Release v0.3
[UPD] more correct way to handle user/chat/auth updates
[UPD] removed successful login message, using presence instead
[NEW] config option `content_upload_prefix` that says which url prefixes will be treated as media and uploaded as file to telegram
[NEW] processing self outgoing messages that sent via another telegram instance
[NEW] added gif animations, locations, video, chat events support
[NEW] control commands implemented:
/s/old/new (edit), /d (delete), /info @username, /add @username or uid, /join invite.link or id, /invite @username, /kick @username, /ban @username [hours], /block, /unblock, /leave, /delete
2019-04-12 03:11:56 +00:00
require 'xmpp4r'
2019-04-06 06:12:54 +00:00
2019-04-06 08:23:14 +00:00
#############################
2019-04-06 07:29:48 +00:00
### Some constants #########
2019-04-09 06:42:42 +00:00
:: HELP_MESSAGE = ' Unknown command .
/ login <telegram_login> — Connect to Telegram network
/ code 12345 — Enter confirmation code
/ password secret — Enter 2FA password
2019-04-13 14:42:56 +00:00
/ connect — Connect to Telegram network if have active session
2019-04-09 06:42:42 +00:00
/ disconnect — Disconnect from Telegram network
/ logout — Disconnect from Telegram network and forget session
'
2019-04-06 08:23:14 +00:00
#############################
2019-04-06 07:29:48 +00:00
2019-04-06 06:12:54 +00:00
#############################
## XMPP Transport Class #####
#############################
class XMPPComponent
2019-04-07 11:58:07 +00:00
2019-04-06 08:23:14 +00:00
# init class and set logger #
2019-04-07 11:58:07 +00:00
def initialize ( params )
2019-04-09 06:42:42 +00:00
@@loglevel = params [ 'loglevel' ] || Logger :: DEBUG
@logger = Logger . new ( STDOUT ) ; @logger . level = @@loglevel ; @logger . progname = '[XMPPComponent]'
2019-04-07 11:58:07 +00:00
@config = { host : params [ " host " ] || 'localhost' , port : params [ " port " ] || 8899 , jid : params [ " jid " ] || 'tlgrm.rxtx.us' , secret : params [ 'secret' ] || '' } # default config
@sessions = { }
@db = SQLite3 :: Database . new ( params [ 'db_path' ] || 'users.db' )
@db . execute ( " CREATE TABLE IF NOT EXISTS users(jid varchar(256), tg_login varchar(256), PRIMARY KEY(jid) ); " )
@db . results_as_hash = true
end
# database #
def load_db ( jid = nil ) # load
2019-04-09 06:42:42 +00:00
@logger . info " Initializing database.. "
2019-04-07 11:58:07 +00:00
query = ( jid . nil? ) ? " SELECT * FROM users " : " SELECT * FROM users where jid = '%s'; " % jid
@logger . debug ( query )
@db . execute ( query ) do | user |
2019-04-09 06:42:42 +00:00
@logger . info " Found session for JID %s and Telegram login %s " % [ user [ " jid " ] . to_s , user [ " tg_login " ] ]
2019-04-07 11:58:07 +00:00
@sessions [ user [ " jid " ] ] = XMPPSession . new ( user [ " jid " ] , user [ " tg_login " ] )
end
2019-04-06 07:29:48 +00:00
end
2019-04-07 11:58:07 +00:00
def update_db ( jid , delete = false ) # write
return if not @sessions . key? jid
2019-04-09 06:42:42 +00:00
@logger . info " Writing database [%s].. " % jid . to_s
2019-04-07 11:58:07 +00:00
query = ( delete ) ? " DELETE FROM users where jid = '%s'; " % jid . to_s : " INSERT OR REPLACE INTO users(jid, tg_login) VALUES('%s', '%s'); " % [ jid . to_s , @sessions [ jid ] . tg_login . to_s ]
@logger . debug query
@db . execute ( query )
end
2019-04-06 06:12:54 +00:00
# transport initialization & connecting to XMPP server #
2019-04-07 11:58:07 +00:00
def connect ( ) # :jid => transport_jid, :host => xmpp_server, :port => xmpp_component_port, :secret => xmpp_component_secret
2019-04-06 07:29:48 +00:00
@logger . info " Connecting.. "
2019-04-06 06:12:54 +00:00
begin
2019-04-07 11:58:07 +00:00
@@transport = Jabber :: Component . new ( @config [ :jid ] )
@@transport . connect ( @config [ :host ] , @config [ :port ] )
@@transport . auth ( @config [ :secret ] )
2019-04-06 06:12:54 +00:00
@@transport . add_message_callback do | msg | msg . first_element_text ( 'body' ) ? self . message_handler ( msg ) : nil end
2019-04-07 11:58:07 +00:00
@@transport . add_presence_callback do | presence | self . presence_handler ( presence ) end
2019-04-13 14:42:56 +00:00
@@transport . add_iq_callback do | iq | self . iq_handler ( iq ) end
2019-04-06 07:29:48 +00:00
@logger . info " Connection established "
2019-04-07 11:58:07 +00:00
self . load_db ( )
@logger . info 'Found %s sessions in database.' % @sessions . count
@sessions . each do | jid , session |
2019-04-09 06:42:42 +00:00
@logger . debug " Sending presence to %s " % jid
2019-04-07 11:58:07 +00:00
p = Jabber :: Presence . new ( )
p . to = jid
p . from = @@transport . jid
p . type = :subscribe
@logger . debug p
@@transport . send ( p )
end
2019-04-06 06:12:54 +00:00
Thread . stop ( )
rescue Exception = > e
2019-04-09 06:42:42 +00:00
@logger . error 'Connection failed: %s' % e
2019-04-07 11:58:07 +00:00
@db . close
2019-04-06 06:12:54 +00:00
exit 1
end
end
2019-04-06 08:23:14 +00:00
#############################
#### Callback handlers #####
#############################
2019-04-06 06:12:54 +00:00
# new message to XMPP component #
def message_handler ( msg )
2019-04-06 07:29:48 +00:00
@logger . info 'New message from [%s] to [%s]' % [ msg . from , msg . to ]
2019-04-07 11:58:07 +00:00
return self . process_internal_command ( msg . from . bare . to_s , msg . first_element_text ( 'body' ) ) if msg . to == @@transport . jid # treat message as internal command if received as transport jid
2019-04-13 14:42:56 +00:00
return @sessions [ msg . from . bare . to_s ] . tg_outgoing ( msg . to . to_s , msg . first_element_text ( 'body' ) ) #if @sessions.key? msg.from.bare.to_s and @sessions[msg.from.bare.to_s].online? # queue message for processing session is active for jid from
2019-04-07 11:58:07 +00:00
end
def presence_handler ( presence )
2019-04-09 06:42:42 +00:00
@logger . debug " New presence iq received "
2019-04-07 11:58:07 +00:00
@logger . debug ( presence )
if presence . type == :subscribe then reply = presence . answer ( false ) ; reply . type = :subscribed ; @@transport . send ( reply ) ; end # send "subscribed" reply to "subscribe" presence
2019-04-13 14:42:56 +00:00
if presence . to == @@transport . jid and @sessions . key? presence . from . bare . to_s and presence . type == :unavailable then @sessions [ presence . from . bare . to_s ] . disconnect ( ) ; return ; end # go offline when received offline presence from jabber user
2019-04-07 11:58:07 +00:00
if presence . to == @@transport . jid and @sessions . key? presence . from . bare . to_s then @sessions [ presence . from . bare . to_s ] . connect ( ) ; return ; end # connect if we have session
end
def iq_handler ( iq )
2019-04-09 06:42:42 +00:00
@logger . debug " New iq received "
2019-04-13 14:42:56 +00:00
@logger . debug ( iq . to_s )
reply = iq . answer
if iq . vcard and @sessions . key? iq . from . bare . to_s then
vcard = @sessions [ iq . from . bare . to_s ] . make_vcard ( iq . to . to_s )
reply . type = :result
reply . elements [ " vCard " ] = vcard
@@transport . send ( reply )
else
reply . type = :error
end
@@transport . send ( reply )
2019-04-06 06:12:54 +00:00
end
2019-04-06 08:23:14 +00:00
#############################
#### Command handlers #####
#############################
2019-04-06 06:12:54 +00:00
# process internal /command #
def process_internal_command ( jfrom , body )
case body . split [ 0 ] # /command argument = [command, argument]
2019-04-07 11:58:07 +00:00
when '/login' # creating new session if not exists and connect if user already has session
@sessions [ jfrom ] = XMPPSession . new ( jfrom , body . split [ 1 ] ) if not @sessions . key? jfrom
@sessions [ jfrom ] . connect ( )
self . update_db ( jfrom )
when '/code' , '/password' # pass auth data if we have session
2019-04-13 14:42:56 +00:00
@sessions [ jfrom ] . tg_auth ( body . split [ 0 ] , body . split [ 1 ] ) if @sessions . key? jfrom
when '/connect' # going online
@sessions [ jfrom ] . connect ( ) if @sessions . key? jfrom
2019-04-07 11:58:07 +00:00
when '/disconnect' # going offline without destroying a session
2019-04-13 14:42:56 +00:00
@sessions [ jfrom ] . disconnect ( ) if @sessions . key? jfrom
2019-04-07 11:58:07 +00:00
when '/logout' # destroying session
2019-04-13 14:42:56 +00:00
@sessions [ jfrom ] . disconnect ( true ) if @sessions . key? jfrom
2019-04-07 11:58:07 +00:00
self . update_db ( jfrom , true )
@sessions . delete ( jfrom )
else # unknown command -- display help #
msg = Jabber :: Message . new
msg . from = @@transport . jid
msg . to = jfrom
msg . body = :: HELP_MESSAGE
msg . type = :chat
@@transport . send ( msg )
2019-04-06 06:12:54 +00:00
end
end
2019-04-06 08:23:14 +00:00
2019-04-06 06:12:54 +00:00
end
#############################
## XMPP Session Class #######
#############################
class XMPPSession < XMPPComponent
Release v0.3
[UPD] more correct way to handle user/chat/auth updates
[UPD] removed successful login message, using presence instead
[NEW] config option `content_upload_prefix` that says which url prefixes will be treated as media and uploaded as file to telegram
[NEW] processing self outgoing messages that sent via another telegram instance
[NEW] added gif animations, locations, video, chat events support
[NEW] control commands implemented:
/s/old/new (edit), /d (delete), /info @username, /add @username or uid, /join invite.link or id, /invite @username, /kick @username, /ban @username [hours], /block, /unblock, /leave, /delete
2019-04-12 03:11:56 +00:00
attr_reader :user_jid , :tg_login
2019-04-13 14:42:56 +00:00
attr_accessor :online
2019-04-06 06:12:54 +00:00
# start XMPP user session and Telegram client instance #
def initialize ( jid , tg_login )
2019-04-09 06:42:42 +00:00
@logger = Logger . new ( STDOUT ) ; @logger . level = @@loglevel ; @logger . progname = '[XMPPSession: %s/%s]' % [ jid , tg_login ] # init logger
@logger . info " Initializing new session.. "
2019-04-13 14:42:56 +00:00
@user_jid , @tg_login = jid , tg_login
2019-04-07 11:58:07 +00:00
end
# connect to tg #
def connect ( )
return if self . online?
2019-04-09 06:42:42 +00:00
@logger . info " Spawning Telegram client.. "
2019-04-07 11:58:07 +00:00
@online = nil
2019-04-13 14:42:56 +00:00
@telegram = TelegramClient . new ( self , @tg_login ) # init tg instance in new thread
2019-04-06 06:12:54 +00:00
end
2019-04-13 14:42:56 +00:00
# disconnect from tg#
def disconnect ( logout = false )
return if not self . online? or not @telegram
@logger . info " Disconnecting Telegram client.. "
@telegram . disconnect ( logout )
end
2019-04-06 08:23:14 +00:00
###########################################
# send message to current user via XMPP #
2019-04-13 14:42:56 +00:00
def incoming_message ( from = nil , body = '' )
2019-04-09 06:42:42 +00:00
@logger . info " Received new message from Telegram peer %s " % from || " [self] "
2019-04-07 11:58:07 +00:00
reply = Jabber :: Message . new
reply . type = :chat
reply . from = from . nil? ? @@transport . jid : from . to_s + '@' + @@transport . jid . to_s
reply . to = @user_jid
reply . body = body
@logger . debug reply
2019-04-06 06:12:54 +00:00
@@transport . send ( reply )
end
2019-04-07 11:58:07 +00:00
# presence update #
2019-04-13 14:42:56 +00:00
def presence ( from , type = nil , show = nil , status = nil , nickname = nil )
2019-04-09 06:42:42 +00:00
@logger . debug " Presence update request from %s.. " % from . to_s
2019-04-07 11:58:07 +00:00
req = Jabber :: Presence . new ( )
req . from = from . nil? ? @@transport . jid : from . to_s + '@' + @@transport . jid . to_s # presence <from>
req . to = @user_jid # presence <to>
req . type = type unless type . nil? # pres. type
Release v0.3
[UPD] more correct way to handle user/chat/auth updates
[UPD] removed successful login message, using presence instead
[NEW] config option `content_upload_prefix` that says which url prefixes will be treated as media and uploaded as file to telegram
[NEW] processing self outgoing messages that sent via another telegram instance
[NEW] added gif animations, locations, video, chat events support
[NEW] control commands implemented:
/s/old/new (edit), /d (delete), /info @username, /add @username or uid, /join invite.link or id, /invite @username, /kick @username, /ban @username [hours], /block, /unblock, /leave, /delete
2019-04-12 03:11:56 +00:00
req . show = show unless show . nil? # presence <show>
req . status = status unless status . nil? # presence message
req . add_element ( 'nick' , { 'xmlns' = > 'http://jabber.org/protocol/nick' } ) . add_text ( nickname ) unless nickname . nil? # nickname
2019-04-07 11:58:07 +00:00
@logger . debug req
@@transport . send ( req )
end
Release v0.3
[UPD] more correct way to handle user/chat/auth updates
[UPD] removed successful login message, using presence instead
[NEW] config option `content_upload_prefix` that says which url prefixes will be treated as media and uploaded as file to telegram
[NEW] processing self outgoing messages that sent via another telegram instance
[NEW] added gif animations, locations, video, chat events support
[NEW] control commands implemented:
/s/old/new (edit), /d (delete), /info @username, /add @username or uid, /join invite.link or id, /invite @username, /kick @username, /ban @username [hours], /block, /unblock, /leave, /delete
2019-04-12 03:11:56 +00:00
2019-04-07 11:58:07 +00:00
###########################################
2019-04-06 06:12:54 +00:00
# queue message (we will share this queue within :message_queue to Telegram client thread) #
2019-04-13 14:42:56 +00:00
def tg_outgoing ( to , text = '' )
@logger . debug " Sending message to be sent to Telegram network user -> " % to
@telegram . process_outgoing_msg ( to . split ( '@' ) [ 0 ] . to_i , text )
2019-04-06 06:12:54 +00:00
end
Release v0.3
[UPD] more correct way to handle user/chat/auth updates
[UPD] removed successful login message, using presence instead
[NEW] config option `content_upload_prefix` that says which url prefixes will be treated as media and uploaded as file to telegram
[NEW] processing self outgoing messages that sent via another telegram instance
[NEW] added gif animations, locations, video, chat events support
[NEW] control commands implemented:
/s/old/new (edit), /d (delete), /info @username, /add @username or uid, /join invite.link or id, /invite @username, /kick @username, /ban @username [hours], /block, /unblock, /leave, /delete
2019-04-12 03:11:56 +00:00
# enter auth data (we will share this data within :auth_data {} to Telegram client thread ) #
2019-04-13 14:42:56 +00:00
def tg_auth ( typ , data )
2019-04-09 06:42:42 +00:00
@logger . info " Authenticating in Telegram network with :%s " % typ
2019-04-13 14:42:56 +00:00
@telegram . process_auth ( typ , data )
2019-04-06 06:12:54 +00:00
end
2019-04-13 14:42:56 +00:00
# make vcard from telegram contact #
def make_vcard ( to )
@logger . debug " Requesting information to make a VCard for Telegram contact... " # title, username, firstname, lastname, phone, bio, userpic
fn , nickname , given , family , phone , desc , photo = @telegram . get_contact_info ( to . split ( '@' ) [ 0 ] . to_i )
vcard = Jabber :: Vcard :: IqVcard . new ( )
vcard [ " FN " ] = fn
vcard [ " NICKNAME " ] = nickname if nickname
vcard [ " URL " ] = " https://t.me/%s " % nickname if nickname
vcard [ " N/GIVEN " ] = given if given
vcard [ " N/FAMILY " ] = family if family
vcard [ " DESC " ] = desc if desc
vcard [ " PHOTO/TYPE " ] = 'image/jpeg' if photo
vcard [ " PHOTO/BINVAL " ] = photo if photo
if phone then
ph = vcard . add_element ( " TEL " )
ph . add_element ( " HOME " )
ph . add_element ( " VOICE " )
ph . add_element ( " NUMBER " )
ph . elements [ " NUMBER " ] . text = phone
end
@logger . debug vcard . to_s
return vcard
end
2019-04-06 07:46:29 +00:00
2019-04-06 08:23:14 +00:00
###########################################
2019-04-06 07:46:29 +00:00
# session status #
2019-04-06 08:23:14 +00:00
def online? ( ) @online end
2019-04-13 14:42:56 +00:00
def online! ( ) @logger . info " Connection established " ; @online = true ; self . presence ( nil , :subscribe ) ; self . presence ( nil , nil , nil , " Logged in as " + @tg_login . to_s ) end
def offline! ( ) @online = false ; self . presence ( nil , :unavailable , nil , " Logged out " ) ; @telegram = nil ; end
2019-04-06 06:12:54 +00:00
end