diff --git a/README.md b/README.md index 3184b58..b3ec249 100644 --- a/README.md +++ b/README.md @@ -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**). diff --git a/config.yml.example b/config.yml.example index 7440ddd..a1db7ea 100644 --- a/config.yml.example +++ b/config.yml.example @@ -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 diff --git a/inc/telegramclient.rb b/inc/telegramclient.rb index 30a1519..1e0bbc7 100644 --- a/inc/telegramclient.rb +++ b/inc/telegramclient.rb @@ -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 diff --git a/inc/xmppcomponent.rb b/inc/xmppcomponent.rb index a5cb6bd..aace9f6 100644 --- a/inc/xmppcomponent.rb +++ b/inc/xmppcomponent.rb @@ -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 + @logger.debug(query) + @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,12 +85,53 @@ 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 + # 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(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 : "%s@%s" % [from, @@transport.jid] # presence + req.to = to # presence + req.type = type unless type.nil? # pres. type + req.show = show unless show.nil? # presence + 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 + @@transport.send(req) + 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 + iq.from = @@transport.jid + iq.id = 'time_req_1' + iq.add_element("time", {"xmlns" => "urn:xmpp:time"}) + @logger.debug iq.to_s + @@transport.send(iq) + end + ############################# #### Callback handlers ##### ############################# @@ -107,36 +139,49 @@ class XMPPComponent # 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 + @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.debug "New iq received" + @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.debug "Got VCard request" - vcard = @sessions[iq.from.bare.to_s].tg_contact_vcard(iq.to.to_s) + @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.debug "Got Timezone response" + @logger.info "Got Timezone response" timezone = iq.elements["time"].elements["tzo"].text - @sessions[iq.from.bare.to_s].set_tz(timezone) + @sessions[iq.from.bare.to_s].timezone = timezone elsif iq.type == :get then reply = iq.answer reply.type = :error @@ -149,166 +194,50 @@ class XMPPComponent ############################# # process internal /command # - def process_internal_command(from, body) + def process_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() + 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 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 + 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' # destroying session + 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 # - msg = Jabber::Message.new - msg.from = @@transport.jid - msg.to = from.bare.to_s - msg.body = ::HELP_MESSAGE - msg.type = :chat - @@transport.send(msg) + self.message(from.bare, nil, ::HELP_MESSAGE) 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) + return 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 - req = Jabber::Presence.new() - req.from = from.nil? ? @@transport.jid : from.to_s+'@'+@@transport.jid.to_s # presence - req.to = @user_jid # presence - req.type = type unless type.nil? # pres. type - req.show = show unless show.nil? # presence - 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 - @@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 ## - def request_tz(jid) - @logger.debug "Request timezone from JID %s" % jid.to_s - iq = Jabber::Iq.new - iq.type = :get - iq.to = jid - iq.from = @@transport.jid - iq.id = 'time_req_1' - iq.add_element("time", {"xmlns" => "urn:xmpp:time"}) - @logger.debug iq.to_s - @@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() - 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 +end diff --git a/zhabogram.rb b/zhabogram.rb index d670b4e..e1cdedd 100644 --- a/zhabogram.rb +++ b/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