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
2019-05-31 22:35:03 +00:00
/ reconnect — Reconnect to Telegram network
2019-04-09 06:42:42 +00:00
/ logout — Disconnect from Telegram network and forget session
2019-05-04 20:17:29 +00:00
2019-05-07 19:22:26 +00:00
/ info — Show information and usage statistics of this instance (only for JIDs specified as administrators)
/ restart — Restart this instance (only for JIDs specified as administrators)
2019-04-09 06:42:42 +00:00
'
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 #####
#############################
2019-05-07 19:22:26 +00:00
include Jabber :: Discovery
include Jabber :: Dataforms
2019-04-06 06:12:54 +00:00
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-05-07 19:22:26 +00:00
@config = { host : params [ " host " ] || 'localhost' , port : params [ " port " ] || 8899 , jid : params [ " jid " ] || 'tlgrm.localhost' , secret : params [ 'password' ] || '' , admins : params [ 'admins' ] || [ ] , debug : params [ 'debug' ] } # default config
2019-04-07 11:58:07 +00:00
@sessions = { }
2019-05-07 19:22:26 +00:00
@presence_que = { }
2019-06-11 13:24:44 +00:00
@db = params [ 'db_path' ] || 'users.dat'
2019-05-07 19:22:26 +00:00
self . load_db ( )
2019-04-07 11:58:07 +00:00
end
2019-05-04 20:17:29 +00:00
# load sessions from db #
2019-06-11 13:24:44 +00:00
def load_db ( )
@logger . info " Loading sessions... "
File . open ( @db , 'r' ) { | f | YAML . load ( f ) . each do | jid , login | @sessions [ jid ] = TelegramClient . new ( self , jid , login ) end }
2019-04-06 07:29:48 +00:00
end
2019-05-04 20:17:29 +00:00
# store session to db #
2019-06-11 13:24:44 +00:00
def save_db ( )
@logger . info " Saving sessions... "
sessions_store = [ ]
@sessions . each do | jid , session | store << { jid : jid , login : session . login } end
File . open ( @db , 'w' ) { | f | f . write ( YAML . dump ( sessions_store ) ) }
2019-04-07 11:58:07 +00:00
end
2019-05-04 20:17:29 +00:00
# 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 06:12:54 +00:00
begin
2019-05-04 20:17:29 +00:00
Jabber :: debug = @config [ :debug ]
2019-05-07 19:22:26 +00:00
# component
@component = Jabber :: Component . new ( @config [ :jid ] )
@component . connect ( @config [ :host ] , @config [ :port ] )
@component . auth ( @config [ :secret ] )
@component . add_message_callback do | msg | msg . first_element_text ( 'body' ) ? self . message_handler ( msg ) : nil end
@component . add_presence_callback do | presence | self . presence_handler ( presence ) end
@component . add_iq_callback do | iq | self . iq_handler ( iq ) end
@component . on_exception do | exception , stream , state | self . survive ( exception , stream , state ) end
@logger . info " Connection to XMPP server established! "
# disco
@disco = Jabber :: Discovery :: Responder . new ( @component )
@disco . identities = [ Identity . new ( 'gateway' , 'Telegram Gateway' , 'telegram' ) ]
@disco . add_features ( [ 'http://jabber.org/protocol/disco' , 'jabber:iq:register' ] )
# janbber::iq::register
@iq_register = Jabber :: Register :: Responder . new ( @component )
@iq_register . instructions = 'Please enter your Telegram login'
@iq_register . add_field ( :login , true ) do | jid , login | self . process_command ( jid , '/login %s' % login ) end
# jabber::iq::gateway
@iq_gateway = Jabber :: Gateway :: Responder . new ( @component ) do | iq , query | ( @sessions . key? iq . from . bare . to_s and @sessions [ iq . from . bare . to_s ] . online? ) ? @sessions [ iq . from . bare . to_s ] . resolve_username ( query ) . to_s + '@' + @component . jid . to_s : '' end
@iq_gateway . description = " Specify @username / ID / https://t.me/link "
@iq_gateway . prompt = " Telegram contact "
@logger . info 'Loaded %s sessions from database.' % @sessions . count
2019-05-04 20:17:29 +00:00
@sessions . each do | jid , session | self . presence ( jid , nil , :subscribe ) end
2019-05-07 19:22:26 +00:00
Thread . new { while @component . is_connected? do @presence_que . each_value { | p | @component . send ( p ) } ; @presence_que . clear ; sleep ( 60 ) ; end } # presence updater thread
Thread . stop ( )
2019-06-13 10:12:59 +00:00
rescue Interrupt , SignalException
2019-05-04 02:40:02 +00:00
@logger . error 'Interrupted!'
2019-05-07 19:22:26 +00:00
@component . on_exception do | exception , | end
2019-05-04 02:40:02 +00:00
self . disconnect ( )
return - 11
2019-04-06 06:12:54 +00:00
rescue Exception = > e
2019-04-09 06:42:42 +00:00
@logger . error 'Connection failed: %s' % e
2019-06-11 13:24:44 +00:00
self . save_db ( )
2019-05-04 20:17:29 +00:00
exit - 8
2019-04-06 06:12:54 +00:00
end
end
2019-04-06 08:23:14 +00:00
2019-05-04 02:40:02 +00:00
# transport shutdown #
def disconnect ( )
2019-05-07 19:22:26 +00:00
@logger . info " Closing connections... "
2019-05-31 11:30:25 +00:00
@sessions . each do | jid , session | @sessions [ jid ] . disconnect ( ) ; self . presence ( jid , nil , :unavailable ) end
2019-05-07 19:22:26 +00:00
@component . close ( )
2019-05-04 02:40:02 +00:00
end
2019-05-04 20:17:29 +00:00
# vse umrut a ya ostanus'... #
2019-04-16 03:45:56 +00:00
def survive ( exception , stream , state )
@logger . error " Stream error on :%s (%s) " % [ state . to_s , exception . to_s ]
2019-05-07 19:22:26 +00:00
@logger . info " Trying to revive stream.. "
2019-04-16 03:45:56 +00:00
self . connect ( )
end
2019-05-04 20:17:29 +00:00
# message to users #
def message ( to , from = nil , body = '' )
2019-05-07 19:22:26 +00:00
@logger . info " Sending message from <%s> to <%s> " % [ from || @component . jid , to ]
2019-05-04 20:17:29 +00:00
msg = Jabber :: Message . new
2019-05-07 19:22:26 +00:00
msg . from = ( from ) ? " %s@%s " % [ from , @component . jid . to_s ] : @component . jid
2019-05-04 20:17:29 +00:00
msg . to = to
msg . body = body
msg . type = :chat
@logger . debug msg . to_s
2019-05-07 19:22:26 +00:00
@component . send ( msg )
2019-05-04 20:17:29 +00:00
end
# presence update #
2019-05-07 19:22:26 +00:00
def presence ( to , from = nil , type = nil , show = nil , status = nil , nickname = nil , photo = nil , immediately = true )
@logger . debug " Presence update request from %s (immed = %s).. " % [ from . to_s , immediately ]
2019-05-04 20:17:29 +00:00
req = Jabber :: Presence . new ( )
2019-05-07 19:22:26 +00:00
req . from = from . nil? ? @component . jid : " %s@%s " % [ from , @component . jid ] # presence <from>
2019-05-04 20:17:29 +00:00
req . to = to # presence <to>
req . type = type unless type . nil? # pres. type
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
req . add_element ( 'x' , { 'xmlns' = > 'vcard-temp:x:update' } ) . add_element ( " photo " ) . add_text ( photo ) unless photo . nil? # nickname
@logger . debug req . to_s
2019-05-31 11:30:25 +00:00
( immediately ) ? @component . send ( req ) : @presence_que . store ( req . from . to_s + req . to . to_s , req )
2019-05-04 20:17:29 +00:00
end
# request timezone information #
def request_tz ( jid )
@logger . debug " Request timezone from JID %s " % jid . to_s
iq = Jabber :: Iq . new
iq . type = :get
iq . to = jid
2019-05-07 19:22:26 +00:00
iq . from = @component . jid
2019-05-04 20:17:29 +00:00
iq . id = 'time_req_1'
iq . add_element ( " time " , { " xmlns " = > " urn:xmpp:time " } )
@logger . debug iq . to_s
2019-05-07 19:22:26 +00:00
@component . send ( iq )
2019-05-04 20:17:29 +00:00
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-15 05:03:40 +00:00
return if msg . type == :error
2019-05-05 01:09:08 +00:00
@logger . info 'Received message from <%s> to <%s>' % [ msg . from . to_s , msg . to . to_s ]
@logger . debug msg . to_s
2019-05-07 19:22:26 +00:00
if msg . to == @component . jid then self . process_command ( msg . from , msg . first_element_text ( 'body' ) ) ; return ; end # treat message as internal command if received as transport jid
2019-05-04 20:17:29 +00:00
if @sessions . key? msg . from . bare . to_s then self . request_tz ( msg . from ) if not @sessions [ msg . from . bare . to_s ] . tz_set? ; @sessions [ msg . from . bare . to_s ] . process_outgoing_msg ( msg . to . to_s . split ( '@' ) [ 0 ] . to_i , msg . first_element_text ( 'body' ) ) ; return ; end #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
2019-05-04 20:17:29 +00:00
# new presence to XMPP component #
def presence_handler ( prsnc )
2019-05-05 01:09:08 +00:00
@logger . debug " Received presence :%s from <%s> to <%s> " % [ prsnc . type . to_s , prsnc . from . to_s , prsnc . to . to_s ]
@logger . debug ( prsnc . to_s )
2019-05-07 19:22:26 +00:00
if prsnc . type == :subscribe then reply = prsnc . answer ( false ) ; reply . type = :subscribed ; @component . send ( reply ) ; end # send "subscribed" reply to "subscribe" presence
2019-06-11 13:24:44 +00:00
if prsnc . to == @component . jid and @sessions . key? prsnc . from . bare . to_s and prsnc . type == :unavailable then @sessions [ prsnc . from . bare . to_s ] . disconnect ( ) ; self . presence ( prsnc . from , nil , :subscribe ) ; return ; end # go offline when received offline presence from jabber user
if prsnc . to == @component . jid and @sessions . key? prsnc . from . bare . to_s then self . request_tz ( prsnc . from ) ; @sessions [ prsnc . from . bare . to_s ] . connect ( ) || @sessions [ prsnc . from . bare . to_s ] . sync_status ( ) ; return ; end # connect if we have session
2019-05-04 20:17:29 +00:00
end
# new iq (vcard/tz) request to XMPP component #
2019-04-07 11:58:07 +00:00
def iq_handler ( iq )
2019-05-05 01:09:08 +00:00
@logger . debug " Received iq :%s from <%s> to <%s> " % [ iq . type . to_s , iq . from . to_s , iq . to . to_s ]
2019-04-13 14:42:56 +00:00
@logger . debug ( iq . to_s )
2019-04-14 02:56:29 +00:00
# vcard request #
if iq . type == :get and iq . vcard and @sessions . key? iq . from . bare . to_s then
2019-05-05 01:09:08 +00:00
@logger . debug " VCard request for <%s> " % iq . to . to_s
2019-05-04 20:17:29 +00:00
fn , nickname , given , family , phone , desc , photo = @sessions [ iq . from . bare . to_s ] . get_contact_info ( iq . to . to_s . 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
2019-04-13 15:39:20 +00:00
reply = iq . answer
2019-04-13 14:42:56 +00:00
reply . type = :result
reply . elements [ " vCard " ] = vcard
2019-05-04 20:17:29 +00:00
@logger . debug reply . to_s
2019-05-07 19:22:26 +00:00
@component . send ( reply )
2019-04-14 02:56:29 +00:00
# time response #
elsif iq . type == :result and iq . elements [ " time " ] and @sessions . key? iq . from . bare . to_s then
2019-05-05 01:09:08 +00:00
@logger . debug " Timezone response from <%s> " % iq . from . to_s
2019-04-14 02:56:29 +00:00
timezone = iq . elements [ " time " ] . elements [ " tzo " ] . text
2019-05-04 20:17:29 +00:00
@sessions [ iq . from . bare . to_s ] . timezone = timezone
2019-04-14 02:56:29 +00:00
elsif iq . type == :get then
2019-05-05 01:09:08 +00:00
@logger . debug " Unknown iq type <%s> " % iq . from . to_s
2019-04-13 15:39:20 +00:00
reply = iq . answer
2019-04-13 14:42:56 +00:00
reply . type = :error
end
2019-05-07 19:22:26 +00:00
@component . 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 #
2019-05-04 20:17:29 +00:00
def process_command ( from , body )
2019-04-06 06:12:54 +00:00
case body . split [ 0 ] # /command argument = [command, argument]
2019-05-04 20:17:29 +00:00
when '/login' # create new session
2019-05-04 22:42:59 +00:00
@sessions [ from . bare . to_s ] = TelegramClient . new ( self , from . bare . to_s , body . split [ 1 ] ) if not ( @sessions . key? from . bare . to_s and @sessions [ from . bare . to_s ] . online? )
2019-05-04 20:17:29 +00:00
@sessions [ from . bare . to_s ] . connect ( )
self . request_tz ( from )
2019-06-11 13:24:44 +00:00
self . save_db ( )
2019-05-04 20:17:29 +00:00
when '/code' , '/password' # pass auth data to telegram
@sessions [ from . bare . to_s ] . process_auth ( body . split [ 0 ] , body . split [ 1 ] ) if @sessions . key? from . bare . to_s
when '/connect' # go online
2019-05-04 21:21:59 +00:00
@sessions [ from . bare . to_s ] . connect ( ) if @sessions . key? from . bare . to_s
2019-05-04 20:17:29 +00:00
when '/disconnect' # go offline (without destroying a session)
2019-04-14 02:56:29 +00:00
@sessions [ from . bare . to_s ] . disconnect ( ) if @sessions . key? from . bare . to_s
2019-05-31 22:35:03 +00:00
when '/reconnect' # reconnect
@sessions [ from . bare . to_s ] . disconnect ( ) if @sessions . key? from . bare . to_s
sleep ( 0 . 1 )
@sessions [ from . bare . to_s ] . connect ( ) if @sessions . key? from . bare . to_s
2019-05-04 20:17:29 +00:00
when '/logout' # go offline and destroy session
2019-04-14 02:56:29 +00:00
@sessions [ from . bare . to_s ] . disconnect ( true ) if @sessions . key? from . bare . to_s
2019-06-11 13:24:44 +00:00
self . save_db ( )
2019-04-14 02:56:29 +00:00
@sessions . delete ( from . bare . to_s )
2019-05-07 19:22:26 +00:00
when '/info' # show some debug information
2019-05-04 20:17:29 +00:00
return if not @config [ :admins ] . include? from . bare . to_s
2019-05-07 19:22:26 +00:00
response = " Information about this instance: \n \n "
2019-05-04 20:17:29 +00:00
response += " Running from: %s \n " % ` ps -p #{ $$ } -o lstart ` . lines . last . strip
response += " System memory used: %d KB \n " % ` ps -o rss -p #{ $$ } ` . lines . last . strip . to_i
2019-05-07 19:22:26 +00:00
response += " \n \n Sessions: %d online | %d total \n " % [ @sessions . inject ( 0 ) { | cnt , ( jid , sess ) | cnt = ( sess . online? ) ? cnt + 1 : cnt } , @sessions . count ]
2019-05-31 22:35:03 +00:00
@sessions . each do | jid , session | response += " JID: %s | Login: %s | Status: %s (%s) | %s \n " % [ jid , session . login , ( session . online == true ) ? 'Online' : 'Offline' , session . auth_state , ( session . me ) ? session . format_contact ( session . me . id ) : 'Unknown' ] end
2019-05-04 20:17:29 +00:00
self . message ( from . bare , nil , response )
when '/restart' # reset transport
return if not @config [ :admins ] . include? from . bare . to_s
self . message ( from . bare , nil , 'Trying to restart all active sessions and reconnect to XMPP server..' )
2019-05-07 19:22:26 +00:00
sleep ( 1 )
2019-05-04 20:17:29 +00:00
Process . kill ( " INT " , Process . pid )
2019-04-07 11:58:07 +00:00
else # unknown command -- display help #
2019-05-04 20:17:29 +00:00
self . message ( from . bare , nil , :: HELP_MESSAGE )
2019-04-06 06:12:54 +00:00
end
2019-05-07 19:22:26 +00:00
return true
2019-04-06 06:12:54 +00:00
end
2019-04-06 08:23:14 +00:00
2019-05-04 20:17:29 +00:00
end