Release 1.0-beta1. Some more tests and here we go..
Warning! It is beta software and it has not been tested thoroughly. [UPD] Code restructurization. Removed unneccessary XMPPSession class. [UPD] Recuded memory consumption. [UPD] Seems like memory leaks fixed too, need test a bit longer [UPD] Now running in forever-loop, use double Ctrl+C to quit [ADD] Admin commands /sessions, /debug and /restart [ADD] New config parameters: xmpp->debug to print XML stream and xmpp->admins[] to give access to above mentioned /commands [ADD] We can now use memory profiler to get detailed memory usage information: run application with --profiler key (and also gem install memprof2) (yes, fucking leaks...) [FIX] Fixed secret chat closing (I hope so...)
This commit is contained in:
parent
20d1e68781
commit
88c7eb09da
|
@ -4,6 +4,7 @@
|
|||
* ruby-sqlite3 >= 1.3
|
||||
* xmpp4r == 0.5.6
|
||||
* tdlib-ruby == 2.0 with pre-compiled _libtdjson.so_
|
||||
* memprof2 == 0.1.2 (optional, if running with --profiler key to detailed memory usage information)
|
||||
|
||||
There is pre-compiled _libtdjson.so_ for Debian Stretch x64 in repository.
|
||||
For any other distro you need to manually compile [**tdlib**](https://github.com/tdlib/td) and place _libtdjson.so_ to relative **lib/** directory (or **LD_LIBRARY_PATH**).
|
||||
|
|
|
@ -4,7 +4,7 @@ telegram:
|
|||
api_hash: '27fe5224bc822bf3a45e015b4f9dfdb7' # telegram API HASH (my.telegram.org) #
|
||||
verbosity: 2 # 1 = no verbosity, 2 = moderate verbosity, 3 = network requests debug
|
||||
useragent: 'Zhabogram XMPP Gateway' # client name
|
||||
version: '0.8' # client version
|
||||
version: '1.0' # client version
|
||||
use_test_dc: false # always use false
|
||||
loglevel: 0 # 0 = debug, 1 = info, 2 = warn, 3 = err, 4 = fatal, 5 = unknown (ruby logger class)
|
||||
content_path: '/var/zhabogram/content' # we will move (symlink) downloaded content here — you must setup web server that serve this directry
|
||||
|
@ -13,6 +13,9 @@ telegram:
|
|||
|
||||
xmpp:
|
||||
|
||||
debug: false
|
||||
admins:
|
||||
- 'root@localhost'
|
||||
db_path: 'users.db' # sqlite3 users (JID:Telegram Login) database
|
||||
jid: 'tlgrm.localhost' # component JID
|
||||
host: 'localhost' # XMPP server
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
require 'tdlib-ruby'
|
||||
require 'digest'
|
||||
require 'base64'
|
||||
|
||||
class TelegramClient
|
||||
|
||||
attr_reader :jid, :login, :online, :auth_state, :me
|
||||
attr_accessor :timezone
|
||||
|
||||
# tdlib configuration, shared within all instances #
|
||||
def self.configure(params)
|
||||
@@loglevel = params['loglevel'] || Logger::DEBUG
|
||||
|
@ -27,18 +26,23 @@ class TelegramClient
|
|||
end
|
||||
|
||||
# instance initialization #
|
||||
def initialize(xmpp, login)
|
||||
def initialize(xmpp, jid, login)
|
||||
return if not @@loglevel # call .configure() first
|
||||
|
||||
@logger = Logger.new(STDOUT); @logger.level = @@loglevel; @logger.progname = '[TelegramClient: %s/%s]' % [xmpp.user_jid, login] # create logger
|
||||
@xmpp = xmpp # our XMPP user session. we will send messages back to Jabber through this instance.
|
||||
@login = login # store tg login
|
||||
@cache = {chats: {}, users: {}, users_fullinfo: {}, userpics: {}, unread_msg: {} } # we will store our cache here
|
||||
@files_dir = File.dirname(__FILE__) + '/../sessions/' + @xmpp.user_jid + '/files/'
|
||||
@logger = Logger.new(STDOUT); @logger.level = @@loglevel; @logger.progname = '[TelegramClient: %s/%s]' % [jid, login] # create logger
|
||||
@xmpp = xmpp # XMPP stream
|
||||
@jid = jid # user JID
|
||||
@timezone = '-00:00' # default timezone is UTC
|
||||
@login = login # telegram login
|
||||
@me = nil # self telegram profile
|
||||
@online = nil # we do not know
|
||||
@auth_state = 'nil' # too.
|
||||
@cache = {chats: {}, users: {}, users_fullinfo: {}, userpics: {}, unread_msg: {} } # cache storage
|
||||
@files_dir = File.dirname(__FILE__) + '/../sessions/' + @jid + '/files/'
|
||||
|
||||
# spawn telegram client and specify callback handlers
|
||||
@logger.info 'Connecting to Telegram network..'
|
||||
@client = TD::Client.new(database_directory: 'sessions/' + @xmpp.user_jid, files_directory: 'sessions/' + @xmpp.user_jid + '/files/') # create telegram client instance
|
||||
@logger.info 'Starting Telegram client..'
|
||||
@client = TD::Client.new(database_directory: 'sessions/' + @jid, files_directory: 'sessions/' + @jid + '/files/') # create telegram client instance
|
||||
@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
|
||||
@client.on(TD::Types::Update::MessageContent) do |update| self.message_edited_handler(update) end # register msg edited handler
|
||||
|
@ -47,10 +51,22 @@ class TelegramClient
|
|||
@client.on(TD::Types::Update::NewChat) do |update| self.new_chat_handler(update) end # register new chat handler
|
||||
@client.on(TD::Types::Update::User) do |update| self.user_handler(update) end # new user update?
|
||||
@client.on(TD::Types::Update::UserStatus) do |update| self.status_update_handler(update) end # register status handler
|
||||
@client.connect
|
||||
|
||||
end
|
||||
|
||||
# connect/disconnect #
|
||||
def connect()
|
||||
@logger.info 'Connecting to Telegram network..' if not @client.ready?
|
||||
@client.connect() if not @client.ready?
|
||||
end
|
||||
|
||||
def disconnect(logout = false)
|
||||
@logger.info 'Disconnecting..'
|
||||
@cache[:chats].each_key do |chat_id| @xmpp.presence(@jid, chat_id.to_s, :unavailable) end # send offline presences
|
||||
(logout) ? @client.log_out : @client.dispose # logout if needed
|
||||
@online = false
|
||||
end
|
||||
|
||||
|
||||
###########################################
|
||||
## Callback handlers #####################
|
||||
###########################################
|
||||
|
@ -58,6 +74,7 @@ class TelegramClient
|
|||
# authorization handler #
|
||||
def auth_handler(update)
|
||||
@logger.debug 'Authorization state changed: %s' % update.authorization_state
|
||||
@auth_state = update.authorization_state.class.name
|
||||
|
||||
case update.authorization_state
|
||||
# auth stage 0: specify login #
|
||||
|
@ -67,18 +84,20 @@ class TelegramClient
|
|||
# auth stage 1: wait for authorization code #
|
||||
when TD::Types::AuthorizationState::WaitCode
|
||||
@logger.info 'Waiting for authorization code..'
|
||||
@xmpp.incoming_message(nil, 'Please, enter authorization code via /code 12345')
|
||||
@xmpp.message(@jid, nil, 'Please, enter authorization code via /code 12345')
|
||||
# auth stage 2: wait for 2fa passphrase #
|
||||
when TD::Types::AuthorizationState::WaitPassword
|
||||
@logger.info 'Waiting for 2FA password..'
|
||||
@xmpp.incoming_message(nil, 'Please, enter 2FA passphrase via /password 12345')
|
||||
@xmpp.message(@jid, nil, 'Please, enter 2FA passphrase via /password 12345')
|
||||
# authorization successful -- indicate that client is online and retrieve contact list #
|
||||
when TD::Types::AuthorizationState::Ready
|
||||
@logger.info 'Authorization successful!'
|
||||
@client.get_me().then { |user| @me = user }.wait
|
||||
@client.get_chats(limit=9999)
|
||||
@logger.info "Contact list updating finished"
|
||||
@xmpp.online!
|
||||
@online = true
|
||||
@xmpp.presence(@jid, nil, :subscribe)
|
||||
@xmpp.presence(@jid, nil, nil, "Logged in as %s" % @login)
|
||||
# closing session: sent offline presences to XMPP user #
|
||||
when TD::Types::AuthorizationState::Closing
|
||||
@logger.info 'Closing session..'
|
||||
|
@ -147,7 +166,7 @@ class TelegramClient
|
|||
@client.download_file(file.id) if file # download it if already not
|
||||
|
||||
# forwards, replies and message id..
|
||||
prefix += "[%s]" % DateTime.strptime((update.message.date+Time.now.getlocal(@xmpp.timezone).utc_offset).to_s,'%s').strftime("[%d %b %Y %H:%M:%S]") if show_date
|
||||
prefix += "[%s]" % DateTime.strptime((update.message.date+Time.now.getlocal(@timezone).utc_offset).to_s,'%s').strftime("[%d %b %Y %H:%M:%S]") if show_date
|
||||
prefix += "fwd from %s | " % self.format_username(update.message.forward_info.sender_user_id) if update.message.forward_info.instance_of? TD::Types::MessageForwardInfo::MessageForwardedFromUser # fwd from user
|
||||
prefix += "fwd from %s | " % self.format_chatname(update.message.forward_info.chat_id) if update.message.forward_info.instance_of? TD::Types::MessageForwardInfo::MessageForwardedPost # fwd from chat
|
||||
prefix += "reply to %s | " % self.format_reply(update.message.chat_id, update.message.reply_to_message_id) if update.message.reply_to_message_id.to_i != 0 # reply to
|
||||
|
@ -158,7 +177,7 @@ class TelegramClient
|
|||
|
||||
# send and add message id to unreads
|
||||
@cache[:unread_msg][update.message.chat_id] = update.message.id
|
||||
@xmpp.incoming_message(update.message.chat_id.to_s, text)
|
||||
@xmpp.message(@jid, update.message.chat_id.to_s, text)
|
||||
end
|
||||
|
||||
# new chat update -- when tg client discovers new chat #
|
||||
|
@ -182,7 +201,7 @@ class TelegramClient
|
|||
|
||||
# formatting
|
||||
text = "✎ %s | %s" % [update.message_id.to_s, update.new_content.text.text.to_s]
|
||||
@xmpp.incoming_message(update.chat_id.to_s, text)
|
||||
@xmpp.message(@jid, update.chat_id.to_s, text)
|
||||
end
|
||||
|
||||
# deleted msg #
|
||||
|
@ -191,7 +210,7 @@ class TelegramClient
|
|||
@logger.debug update.to_json
|
||||
return if not update.is_permanent
|
||||
text = "✗ %s |" % update.message_ids.join(',')
|
||||
@xmpp.incoming_message(update.chat_id.to_s, text)
|
||||
@xmpp.message(@jid, update.chat_id.to_s, text)
|
||||
end
|
||||
|
||||
# file msg -- symlink to download path #
|
||||
|
@ -284,10 +303,10 @@ class TelegramClient
|
|||
when '/leave', '/delete' # delete / leave chat
|
||||
@client.close_chat(chat_id).wait
|
||||
@client.leave_chat(chat_id).wait
|
||||
@client.close_secret_chat(chat_id).wait if @cache[:chats][chat_id].type.instance_of? TD::Types::ChatType::Secret
|
||||
@client.close_secret_chat(@cache[:chats][chat_id].secret_chat_id).wait if @cache[:chats][chat_id].type.instance_of? TD::Types::ChatType::Secret
|
||||
@client.delete_chat_history(chat_id, true).wait
|
||||
@xmpp.presence(chat_id, :unsubscribed)
|
||||
@xmpp.presence(chat_id, :unavailable)
|
||||
@xmpp.presence(@jid, chat_id, :unsubscribed)
|
||||
@xmpp.presence(@jid, chat_id, :unavailable)
|
||||
@cache[:chats].delete(chat_id)
|
||||
when '/sed' # sed-like edit
|
||||
sed = splitted[1].split('/')
|
||||
|
@ -359,7 +378,7 @@ class TelegramClient
|
|||
'
|
||||
end
|
||||
|
||||
@xmpp.incoming_message(chat_id, response) if response
|
||||
@xmpp.message(@jid, chat_id, response) if response
|
||||
end
|
||||
|
||||
# processing outgoing message from queue #
|
||||
|
@ -401,8 +420,8 @@ class TelegramClient
|
|||
@cache[:chats][chat_id] = chat # cache chat
|
||||
@client.download_file(chat.photo.small.id).wait if chat.photo # download userpic
|
||||
@cache[:userpics][chat_id] = Digest::SHA1.hexdigest(IO.binread(self.format_content_link(chat.photo.small.remote.id, 'image.jpg', true))) if chat.photo and File.exist? self.format_content_link(chat.photo.small.remote.id, 'image.jpg', true) # cache userpic
|
||||
@xmpp.presence(chat_id.to_s, :subscribe, nil, nil, @cache[:chats][chat_id].title.to_s) if subscription # send subscription request
|
||||
@xmpp.presence(chat_id.to_s, nil, :chat, @cache[:chats][chat_id].title.to_s, nil, @cache[:userpics][chat_id]) if chat.type.instance_of? TD::Types::ChatType::BasicGroup or chat.type.instance_of? TD::Types::ChatType::Supergroup # send :chat status if its group/supergroup
|
||||
@xmpp.presence(@jid, chat_id.to_s, :subscribe, nil, nil, @cache[:chats][chat_id].title.to_s) if subscription # send subscription request
|
||||
@xmpp.presence(@jid, chat_id.to_s, nil, :chat, @cache[:chats][chat_id].title.to_s, nil, @cache[:userpics][chat_id]) if chat.type.instance_of? TD::Types::ChatType::BasicGroup or chat.type.instance_of? TD::Types::ChatType::Supergroup # send :chat status if its group/supergroup
|
||||
# self.process_user_info(chat.type.user_id) if chat.type.instance_of? TD::Types::ChatType::Private # process user if its a private chat
|
||||
}.wait
|
||||
end
|
||||
|
@ -429,7 +448,7 @@ class TelegramClient
|
|||
xmpp_status = "Online"
|
||||
when TD::Types::UserStatus::Offline
|
||||
xmpp_show = (Time.now.getutc.to_i - status.was_online.to_i < 3600) ? :away : :xa
|
||||
xmpp_status = DateTime.strptime((status.was_online+Time.now.getlocal(@xmpp.timezone).utc_offset).to_s,'%s').strftime("Last seen at %H:%M %d/%m/%Y")
|
||||
xmpp_status = DateTime.strptime((status.was_online+Time.now.getlocal(@timezone).utc_offset).to_s,'%s').strftime("Last seen at %H:%M %d/%m/%Y")
|
||||
when TD::Types::UserStatus::Recently
|
||||
xmpp_show = :dnd
|
||||
xmpp_status = "Last seen recently"
|
||||
|
@ -441,7 +460,7 @@ class TelegramClient
|
|||
xmpp_status = "Last seen last month"
|
||||
end
|
||||
xmpp_photo = @cache[:userpics][user_id] if @cache[:userpics].key? user_id
|
||||
@xmpp.presence(user_id.to_s, nil, xmpp_show, xmpp_status, nil, xmpp_photo)
|
||||
@xmpp.presence(@jid, user_id.to_s, nil, xmpp_show, xmpp_status, nil, xmpp_photo)
|
||||
end
|
||||
|
||||
# get contact information (for vcard).
|
||||
|
@ -470,21 +489,6 @@ class TelegramClient
|
|||
return title, username, firstname, lastname, phone, bio, userpic
|
||||
end
|
||||
|
||||
# roster status sync #
|
||||
def sync_status(user_id = nil)
|
||||
@logger.debug "Syncing statuses.."
|
||||
if user_id and @cache[:users].key? user_id then return process_status_update(@cache[:users][user_id].id, @cache[:users][user_id].status) end # sync single contact #
|
||||
@cache[:users].each_value do |user| process_status_update(user.id, user.status) end # sync everyone #
|
||||
end
|
||||
|
||||
# graceful disconnect
|
||||
def disconnect(logout)
|
||||
@logger.info 'Disconnect request received..'
|
||||
@cache[:chats].each_key do |chat_id| @xmpp.presence(chat_id.to_s, :unavailable) end # send offline presences
|
||||
(logout) ? @client.log_out : @client.dispose # logout if needed
|
||||
@xmpp.offline!
|
||||
end
|
||||
|
||||
###########################################
|
||||
## Format functions #######################
|
||||
###########################################
|
||||
|
@ -524,4 +528,7 @@ class TelegramClient
|
|||
return path
|
||||
end
|
||||
|
||||
###########################################
|
||||
def online?() @online end
|
||||
def tz_set?() return @timezone != '-00:00' end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
require 'sqlite3'
|
||||
require 'xmpp4r'
|
||||
require 'digest'
|
||||
|
||||
#############################
|
||||
### Some constants #########
|
||||
::HELP_MESSAGE = 'Unknown command.
|
||||
|
@ -12,6 +8,10 @@ require 'digest'
|
|||
/connect — Connect to Telegram network if have active session
|
||||
/disconnect — Disconnect from Telegram network
|
||||
/logout — Disconnect from Telegram network and forget session
|
||||
|
||||
/sessions — Shows current active sessions (available for admins)
|
||||
/debug — Shows some debug information (available for admins)
|
||||
/restart — Reset Zhabogram (available for admins)
|
||||
'
|
||||
|
||||
#############################
|
||||
|
@ -25,36 +25,35 @@ class XMPPComponent
|
|||
def initialize(params)
|
||||
@@loglevel = params['loglevel'] || Logger::DEBUG
|
||||
@logger = Logger.new(STDOUT); @logger.level = @@loglevel; @logger.progname = '[XMPPComponent]'
|
||||
@config = { host: params["host"] || 'localhost', port: params["port"] || 8899, jid: params["jid"] || 'tlgrm.rxtx.us', secret: params['password'] || '' } # default config
|
||||
@config = { host: params["host"] || 'localhost', port: params["port"] || 8899, jid: params["jid"] || 'tlgrm.localhost', secret: params['password'] || '', admins: params['admins'] || [], debug: params['debug'] || false } # 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.execute("CREATE TABLE IF NOT EXISTS users(jid varchar(256), login varchar(256), PRIMARY KEY(jid) );")
|
||||
@db.results_as_hash = true
|
||||
end
|
||||
|
||||
# database #
|
||||
# load sessions from db #
|
||||
def load_db(jid = nil) # load
|
||||
@logger.info "Initializing database.."
|
||||
query = (jid.nil?) ? "SELECT * FROM users" : "SELECT * FROM users where jid = '%s';" % jid
|
||||
@logger.debug(query)
|
||||
@db.execute(query) do |user|
|
||||
@logger.info "Found session for JID %s and Telegram login %s" % [ user["jid"].to_s, user["tg_login"] ]
|
||||
@sessions[user["jid"]] = XMPPSession.new(user["jid"], user["tg_login"])
|
||||
end
|
||||
@db.execute(query) do |session| @sessions[session['jid']] = TelegramClient.new(self, session['jid'], session['login']) end
|
||||
end
|
||||
|
||||
# store session to db #
|
||||
def update_db(jid, delete = false) # write
|
||||
return if not @sessions.key? jid
|
||||
@logger.info "Writing database [%s].." % jid.to_s
|
||||
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]
|
||||
query = (delete) ? "DELETE FROM users where jid = '%s';" % jid.to_s : "INSERT OR REPLACE INTO users(jid, login) VALUES('%s', '%s');" % [jid.to_s, @sessions[jid].login.to_s]
|
||||
@logger.debug query
|
||||
@db.execute(query)
|
||||
end
|
||||
|
||||
|
||||
# transport initialization & connecting to XMPP server #
|
||||
# connecting to XMPP server #
|
||||
def connect() # :jid => transport_jid, :host => xmpp_server, :port => xmpp_component_port, :secret => xmpp_component_secret
|
||||
@logger.info "Connecting.."
|
||||
begin
|
||||
Jabber::debug = @config[:debug]
|
||||
@@transport = Jabber::Component.new( @config[:jid] )
|
||||
@@transport.connect( @config[:host], @config[:port] )
|
||||
@@transport.auth( @config[:secret] )
|
||||
|
@ -65,15 +64,7 @@ class XMPPComponent
|
|||
@logger.info "Connection established"
|
||||
self.load_db()
|
||||
@logger.info 'Found %s sessions in database.' % @sessions.count
|
||||
@sessions.each do |jid, session|
|
||||
@logger.debug "Sending presence to %s" % jid
|
||||
p = Jabber::Presence.new()
|
||||
p.to = jid
|
||||
p.from = @@transport.jid
|
||||
p.type = :subscribe
|
||||
@logger.debug p
|
||||
@@transport.send(p)
|
||||
end
|
||||
@sessions.each do |jid, session| self.presence(jid, nil, :subscribe) end
|
||||
Thread.stop()
|
||||
rescue Interrupt
|
||||
@logger.error 'Interrupted!'
|
||||
|
@ -83,7 +74,7 @@ class XMPPComponent
|
|||
rescue Exception => e
|
||||
@logger.error 'Connection failed: %s' % e
|
||||
@db.close
|
||||
exit 1
|
||||
exit -8
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -94,197 +85,41 @@ class XMPPComponent
|
|||
@@transport.close()
|
||||
end
|
||||
|
||||
# vse umrut a ya ostanus'... #
|
||||
def survive(exception, stream, state)
|
||||
@logger.error "Stream error on :%s (%s)" % [state.to_s, exception.to_s]
|
||||
@logger.info "Trying to revive session..."
|
||||
@logger.info "Trying to ressurect XMPP stream.."
|
||||
self.connect()
|
||||
end
|
||||
|
||||
#############################
|
||||
#### Callback handlers #####
|
||||
#############################
|
||||
|
||||
# new message to XMPP component #
|
||||
def message_handler(msg)
|
||||
return if msg.type == :error
|
||||
@logger.info 'New message from [%s] to [%s]' % [msg.from, msg.to]
|
||||
return self.process_internal_command(msg.from, msg.first_element_text('body') ) if msg.to == @@transport.jid # treat message as internal command if received as transport jid
|
||||
return @sessions[msg.from.bare.to_s].tg_outgoing(msg.from, 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
|
||||
end
|
||||
|
||||
def presence_handler(presence)
|
||||
@logger.debug "New presence iq received"
|
||||
@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
|
||||
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
|
||||
if presence.to == @@transport.jid and @sessions.key? presence.from.bare.to_s then @sessions[presence.from.bare.to_s].request_tz(presence.from); @sessions[presence.from.bare.to_s].connect(); return; end # connect if we have session
|
||||
end
|
||||
|
||||
def iq_handler(iq)
|
||||
@logger.debug "New iq received"
|
||||
@logger.debug(iq.to_s)
|
||||
|
||||
# vcard request #
|
||||
if iq.type == :get and iq.vcard and @sessions.key? iq.from.bare.to_s then
|
||||
@logger.debug "Got VCard request"
|
||||
vcard = @sessions[iq.from.bare.to_s].tg_contact_vcard(iq.to.to_s)
|
||||
reply = iq.answer
|
||||
reply.type = :result
|
||||
reply.elements["vCard"] = vcard
|
||||
@@transport.send(reply)
|
||||
# time response #
|
||||
elsif iq.type == :result and iq.elements["time"] and @sessions.key? iq.from.bare.to_s then
|
||||
@logger.debug "Got Timezone response"
|
||||
timezone = iq.elements["time"].elements["tzo"].text
|
||||
@sessions[iq.from.bare.to_s].set_tz(timezone)
|
||||
elsif iq.type == :get then
|
||||
reply = iq.answer
|
||||
reply.type = :error
|
||||
end
|
||||
@@transport.send(reply)
|
||||
end
|
||||
|
||||
#############################
|
||||
#### Command handlers #####
|
||||
#############################
|
||||
|
||||
# process internal /command #
|
||||
def process_internal_command(from, body)
|
||||
case body.split[0] # /command argument = [command, argument]
|
||||
when '/login' # creating new session if not exists and connect if user already has session
|
||||
@sessions[from.bare.to_s] = XMPPSession.new(from.bare.to_s, body.split[1]) if not @sessions.key? from.bare.to_s
|
||||
@sessions[from.bare.to_s].request_tz(from)
|
||||
@sessions[from.bare.to_s].connect()
|
||||
self.update_db(from.bare.to_s)
|
||||
when '/code', '/password' # pass auth data if we have session
|
||||
@sessions[from.bare.to_s].tg_auth(body.split[0], body.split[1]) if @sessions.key? from.bare.to_s
|
||||
when '/connect' # going online
|
||||
@sessions[from.bare.to_s].connect() if @sessions.key? from.bare.to_s
|
||||
when '/disconnect' # going offline without destroying a session
|
||||
@sessions[from.bare.to_s].disconnect() if @sessions.key? from.bare.to_s
|
||||
when '/logout' # destroying session
|
||||
@sessions[from.bare.to_s].disconnect(true) if @sessions.key? from.bare.to_s
|
||||
self.update_db(from.bare.to_s, true)
|
||||
@sessions.delete(from.bare.to_s)
|
||||
else # unknown command -- display help #
|
||||
msg = Jabber::Message.new
|
||||
msg.from = @@transport.jid
|
||||
msg.to = from.bare.to_s
|
||||
msg.body = ::HELP_MESSAGE
|
||||
msg.type = :chat
|
||||
@@transport.send(msg)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
#############################
|
||||
## XMPP Session Class #######
|
||||
#############################
|
||||
class XMPPSession < XMPPComponent
|
||||
attr_reader :user_jid, :tg_login, :timezone
|
||||
attr_accessor :online
|
||||
|
||||
# start XMPP user session and Telegram client instance #
|
||||
def initialize(jid, tg_login)
|
||||
@logger = Logger.new(STDOUT); @logger.level = @@loglevel; @logger.progname = '[XMPPSession: %s/%s]' % [jid, tg_login] # init logger
|
||||
@logger.info "Initializing new session.."
|
||||
@user_jid, @tg_login = jid, tg_login
|
||||
@timezone = '-00:00'
|
||||
end
|
||||
|
||||
# connect to tg #
|
||||
def connect()
|
||||
return if self.online?
|
||||
@logger.info "Spawning Telegram client.."
|
||||
@online = nil
|
||||
@telegram = TelegramClient.new(self, @tg_login) # init tg instance in new thread
|
||||
end
|
||||
|
||||
# disconnect from tg#
|
||||
def disconnect(logout = false)
|
||||
return if not self.online? or not @telegram
|
||||
@logger.info "Disconnecting Telegram client.."
|
||||
@telegram.disconnect(logout)
|
||||
end
|
||||
|
||||
###########################################
|
||||
|
||||
# send message to current user via XMPP #
|
||||
def incoming_message(from = nil, body = '')
|
||||
@logger.info "Received new message from Telegram peer %s" % from || "[self]"
|
||||
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
|
||||
@@transport.send(reply)
|
||||
# message to users #
|
||||
def message(to, from = nil, body = '')
|
||||
@logger.info "Sending message from <%s> to <%s>" % [from || @@transport.jid, to]
|
||||
msg = Jabber::Message.new
|
||||
msg.from = (from) ? "%s@%s" % [from, @@transport.jid.to_s] : @@transport.jid
|
||||
msg.to = to
|
||||
msg.body = body
|
||||
msg.type = :chat
|
||||
@logger.debug msg.to_s
|
||||
@@transport.send(msg)
|
||||
end
|
||||
|
||||
# presence update #
|
||||
def presence(from, type = nil, show = nil, status = nil, nickname = nil, photo = nil)
|
||||
@logger.debug "Presence update request from %s.." %from.to_s
|
||||
def presence(to, from = nil, type = nil, show = nil, status = nil, nickname = nil, photo = nil)
|
||||
@logger.debug "Presence update request from %s.." % from.to_s
|
||||
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.from = from.nil? ? @@transport.jid : "%s@%s" % [from, @@transport.jid] # presence <from>
|
||||
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
|
||||
@logger.debug req.to_s
|
||||
@@transport.send(req)
|
||||
end
|
||||
|
||||
###########################################
|
||||
|
||||
# queue message (we will share this queue within :message_queue to Telegram client thread) #
|
||||
def tg_outgoing(from, to, text = '')
|
||||
@logger.debug "Sending message to be sent to Telegram network user -> " % to
|
||||
self.request_tz(from) if not self.tz_set?
|
||||
@telegram.process_outgoing_msg(to.split('@')[0].to_i, text)
|
||||
end
|
||||
|
||||
# enter auth data (we will share this data within :auth_data {} to Telegram client thread ) #
|
||||
def tg_auth(typ, data)
|
||||
@logger.info "Authenticating in Telegram network with :%s" % typ
|
||||
@telegram.process_auth(typ, data)
|
||||
end
|
||||
|
||||
# sync roster #
|
||||
def tg_sync_status(to = nil)
|
||||
@logger.debug "Sync Telegram contact status with roster.. %s" % to.to_s
|
||||
to = (to) ? to.split('@')[0].to_i : nil
|
||||
@telegram.sync_status(to)
|
||||
end
|
||||
|
||||
# make vcard from telegram contact #
|
||||
def tg_contact_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
|
||||
|
||||
###########################################
|
||||
## timezones ##
|
||||
# request timezone information #
|
||||
def request_tz(jid)
|
||||
@logger.debug "Request timezone from JID %s" % jid.to_s
|
||||
iq = Jabber::Iq.new
|
||||
|
@ -297,18 +132,112 @@ class XMPPSession < XMPPComponent
|
|||
@@transport.send(iq)
|
||||
end
|
||||
|
||||
def set_tz(timezone)
|
||||
@logger.debug "Set TZ to %s" % timezone
|
||||
@timezone = timezone
|
||||
# @logger.debug "Resyncing contact list.."
|
||||
# self.tg_sync_status()
|
||||
#############################
|
||||
#### Callback handlers #####
|
||||
#############################
|
||||
|
||||
# new message to XMPP component #
|
||||
def message_handler(msg)
|
||||
return if msg.type == :error
|
||||
@logger.info 'Received message from <%s> to <%s>' % [msg.from, msg.to]
|
||||
if msg.to == @@transport.jid then self.process_command(msg.from, msg.first_element_text('body') ); return; end # treat message as internal command if received as transport jid
|
||||
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
|
||||
end
|
||||
|
||||
###########################################
|
||||
# new presence to XMPP component #
|
||||
def presence_handler(prsnc)
|
||||
@logger.info "New presence received"
|
||||
@logger.debug(prsnc)
|
||||
if prsnc.type == :subscribe then reply = prsnc.answer(false); reply.type = :subscribed; @@transport.send(reply); end # send "subscribed" reply to "subscribe" presence
|
||||
if prsnc.to == @@transport.jid and @sessions.key? prsnc.from.bare.to_s and prsnc.type == :unavailable then @sessions[prsnc.from.bare.to_s].disconnect(); return; end # go offline when received offline presence from jabber user
|
||||
if prsnc.to == @@transport.jid and @sessions.key? prsnc.from.bare.to_s then self.request_tz(prsnc.from); @sessions[prsnc.from.bare.to_s].connect(); return; end # connect if we have session
|
||||
end
|
||||
|
||||
# new iq (vcard/tz) request to XMPP component #
|
||||
def iq_handler(iq)
|
||||
@logger.info "New iq received"
|
||||
@logger.debug(iq.to_s)
|
||||
|
||||
# vcard request #
|
||||
if iq.type == :get and iq.vcard and @sessions.key? iq.from.bare.to_s then
|
||||
@logger.info "Got VCard request"
|
||||
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
|
||||
reply = iq.answer
|
||||
reply.type = :result
|
||||
reply.elements["vCard"] = vcard
|
||||
@logger.debug reply.to_s
|
||||
@@transport.send(reply)
|
||||
# time response #
|
||||
elsif iq.type == :result and iq.elements["time"] and @sessions.key? iq.from.bare.to_s then
|
||||
@logger.info "Got Timezone response"
|
||||
timezone = iq.elements["time"].elements["tzo"].text
|
||||
@sessions[iq.from.bare.to_s].timezone = timezone
|
||||
elsif iq.type == :get then
|
||||
reply = iq.answer
|
||||
reply.type = :error
|
||||
end
|
||||
@@transport.send(reply)
|
||||
end
|
||||
|
||||
#############################
|
||||
#### Command handlers #####
|
||||
#############################
|
||||
|
||||
# process internal /command #
|
||||
def process_command(from, body)
|
||||
case body.split[0] # /command argument = [command, argument]
|
||||
when '/login' # create new session
|
||||
@sessions[from.bare.to_s] = TelegramClient.new(self, from.bare.to_s, body.split[1]) if not @sessions.key? from.bare.to_s
|
||||
@sessions[from.bare.to_s].connect()
|
||||
self.request_tz(from)
|
||||
self.update_db(from.bare.to_s)
|
||||
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
|
||||
@sessions[from.bare.to_s].client.connect() if @sessions.key? from.bare.to_s
|
||||
when '/disconnect' # go offline (without destroying a session)
|
||||
@sessions[from.bare.to_s].disconnect() if @sessions.key? from.bare.to_s
|
||||
when '/logout' # go offline and destroy session
|
||||
@sessions[from.bare.to_s].disconnect(true) if @sessions.key? from.bare.to_s
|
||||
self.update_db(from.bare.to_s, true)
|
||||
@sessions.delete(from.bare.to_s)
|
||||
when '/debug' # show some debug information
|
||||
return if not @config[:admins].include? from.bare.to_s
|
||||
GC.start
|
||||
dump = (defined? Memprof2) ? "/tmp/zhabogram.%s.dump" % Time.now.to_i : nil
|
||||
Memprof2.report(out: dump) if dump
|
||||
response = "Debug information: \n\n"
|
||||
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
|
||||
response += "Objects memory allocated: %d bytes \n" % `cut -d' ' -f1 #{dump}`.lines.map(&:to_i).reduce(0, :+) if dump
|
||||
response += "\nDetailed memory info saved to %s\n" % dump if dump
|
||||
response += "\nRun this transport with --profiler (depends on gem memprof2) to get detailed memory infnormation.\n" if not dump
|
||||
self.message(from.bare, nil, response)
|
||||
when '/sessions' # show active sessions
|
||||
return if not @config[:admins].include? from.bare.to_s
|
||||
response = "Active sessions list: \n\n"
|
||||
@sessions.each do |jid, session| response += "JID: %s | Login: %s | Status: %s (%s) | Telegram profile: %s\n" % [jid, session.login, (session.online == true) ? 'Online' : 'Offline', session.auth_state, (session.me) ? session.format_username(session.me.id) : 'Unknown' ] end
|
||||
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..')
|
||||
sleep(0.5)
|
||||
Process.kill("INT", Process.pid)
|
||||
else # unknown command -- display help #
|
||||
self.message(from.bare, nil, ::HELP_MESSAGE)
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
# session status #
|
||||
def online?() @online end
|
||||
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
|
||||
def tz_set?() return @timezone != '-00:00' end
|
||||
end
|
||||
|
|
10
zhabogram.rb
10
zhabogram.rb
|
@ -6,13 +6,15 @@ require 'digest'
|
|||
require 'base64'
|
||||
require 'sqlite3'
|
||||
require 'tdlib-ruby'
|
||||
require 'memprof2' if ARGV.include? '--profiler'
|
||||
require_relative 'inc/telegramclient'
|
||||
require_relative 'inc/xmppcomponent'
|
||||
|
||||
# profiler #
|
||||
Memprof2.start if defined? Memprof2
|
||||
|
||||
# configuration
|
||||
Config = YAML.load_file(File.dirname(__FILE__) + '/config.yml')
|
||||
TelegramClient.configure(Config['telegram']) # configure tdlib
|
||||
|
||||
# run
|
||||
Zhabogram = XMPPComponent.new(Config['xmpp'])
|
||||
Zhabogram.connect()
|
||||
Zhabogram = XMPPComponent.new(Config['xmpp']) # spawn zhabogram
|
||||
loop do Zhabogram.connect(); sleep(1); end # forever loop jk till double ctrl+c
|
||||
|
|
Reference in a new issue