diff --git a/config.yml.example b/config.yml.example index 825083d..956498f 100644 --- a/config.yml.example +++ b/config.yml.example @@ -4,7 +4,7 @@ telegram: api_hash: '344583e45741c457fe1862106095a5eb' verbosity: 2 useragent: 'Zhabogram XMPP Gateway' - version: '0.10' + version: '0.4' use_test_dc: false loglevel: 0 content_path: '/var/www/tg_media' diff --git a/inc/telegramclient.rb b/inc/telegramclient.rb index 79df45a..7d42c4f 100644 --- a/inc/telegramclient.rb +++ b/inc/telegramclient.rb @@ -1,5 +1,6 @@ require 'tdlib-ruby' require 'digest' +require 'base64' class TelegramClient @@ -11,12 +12,16 @@ class TelegramClient @@content_upload_prefix = params["content_upload_prefix"] || 'https://localhost/upload/' TD.configure do |config| config.lib_path = params['path'] || 'lib/' # we hope it's here - config.client.api_id = params['api_id'] || '17349' # desktop telegram app - config.client.api_hash = params['api_hash'] || '344583e45741c457fe1862106095a5eb' # desktop telegram app - config.client.device_model = params['useragent'] || 'Zhabogram XMPP Gateway' - config.client.application_version = params['version'] || '-1.0' # hmm... + config.client.api_id = params['api_id'] || '50322' # telegram app. from debian repositories + config.client.api_hash = params['api_hash'] || '9ff1a639196c0779c86dd661af8522ba' # telegram app. from debian repositories + config.client.device_model = params['useragent'] || 'Zhabogram' + config.client.application_version = params['version'] || '1.0' # hmm... config.client.use_test_dc = params['use_test_dc'] || false config.client.system_version = '42' # I think I have permission to hardcode The Ultimate Question of Life, the Universe, and Everything?.. + config.client.use_file_database = false # wow + config.client.use_message_database = false # such library + config.client.use_chat_info_database = false # much options + config.client.enable_storage_optimizer = false # ... end TD::Api.set_log_verbosity_level(params['verbosity'] || 1) end @@ -28,7 +33,7 @@ class TelegramClient @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: {}, unread_msg: {} } # we will store our cache here + @cache = {chats: {}, users: {}, users_fi: {}, unread_msg: {} } # we will store our cache here @files_dir = File.dirname(__FILE__) + '/../sessions/' + @xmpp.user_jid + '/files/' # spawn telegram client and specify callback handlers @@ -42,21 +47,8 @@ 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 # + @client.connect - # we will check for outgoing messages in a queue and/or auth data from XMPP thread while XMPP indicates that service is online # - begin - while not @xmpp.online? === false do - self.process_outgoing_msg(@xmpp.message_queue.pop) unless @xmpp.message_queue.empty? # found something in message queue - self.process_auth(@xmpp.auth_data.shift) unless @xmpp.auth_data.empty? # found something in auth queue - sleep 0.1 - end - rescue Exception => e - @logger.error 'Unexcepted exception! %s' % e.to_s - ensure - @logger.info 'Exitting gracefully...' - @client.dispose - end end ########################################### @@ -75,22 +67,26 @@ class TelegramClient # auth stage 1: wait for authorization code # when TD::Types::AuthorizationState::WaitCode @logger.info 'Waiting for authorization code..' - @xmpp.send_message(nil, 'Please, enter authorization code via /code 12345') + @xmpp.incoming_message(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.send_message(nil, 'Please, enter 2FA passphrase via /password 12345') + @xmpp.incoming_message(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!' - @xmpp.online! @client.get_me().then { |user| @me = user }.wait - @client.get_chats(limit=9999).wait + @client.get_chats(limit=9999) @logger.info "Contact list updating finished" - self.sync_roster() + @xmpp.online! + # closing session: sent offline presences to XMPP user # + when TD::Types::AuthorizationState::Closing + @logger.info 'Closing session..' + self.disconnect() + # session closed gracefully when TD::Types::AuthorizationState::Closed @logger.info 'Session closed.' - @xmpp.offline! + self.disconnect() end end @@ -156,7 +152,7 @@ class TelegramClient # send and add message id to unreads @cache[:unread_msg][update.message.chat_id] = update.message.id - @xmpp.send_message(update.message.chat_id.to_s, text) + @xmpp.incoming_message(update.message.chat_id.to_s, text) end # new chat update -- when tg client discovers new chat # @@ -180,7 +176,7 @@ class TelegramClient # formatting text = "[MSG %s EDIT] %s" % [update.message_id.to_s, update.new_content.text.text.to_s] - @xmpp.send_message(update.chat_id.to_s, text) + @xmpp.incoming_message(update.chat_id.to_s, text) end # deleted msg # @@ -189,7 +185,7 @@ class TelegramClient @logger.debug update.to_json return if not update.is_permanent text = "[MSG ID %s DELETE]" % update.message_ids.join(',') - @xmpp.send_message(update.chat_id.to_s, text) + @xmpp.incoming_message(update.chat_id.to_s, text) end # file msg -- symlink to download path # @@ -198,7 +194,7 @@ class TelegramClient @logger.debug update.to_json if update.file.local.is_downloading_completed then fname = update.file.local.path.to_s - target = "%s/%s%s" % [@@content_path, Digest::SHA256.hexdigest("Current user = %s, File ID = %s" % [@tg_login.to_s, update.file.remote.id]), File.extname(fname)] + target = "%s/%s%s" % [@@content_path, Digest::SHA256.hexdigest(update.file.remote.id), File.extname(fname)] @logger.debug 'Downloading of <%s> completed! Link to <%s>' % [fname, target] File.symlink(fname, target) end @@ -218,10 +214,10 @@ class TelegramClient ########################################### # processing authorization # - def process_auth(auth_data) - @logger.debug 'check_authorization :%s..' % auth_data[0] - @client.check_authentication_code(auth_data[1]) if auth_data[0] == :code - @client.check_authentication_password(auth_data[1]) if auth_data[0] == :password + def process_auth(typ, auth_data) + @logger.debug 'check_authorization with %s..' % typ + @client.check_authentication_code(auth_data) if typ == '/code' + @client.check_authentication_password(auth_data) if typ == '/password' end # /command # @@ -234,8 +230,6 @@ class TelegramClient @client.search_public_chat(splitted[1][1..-1]).then {|chat| resolved = chat}.wait if splitted[1] and splitted[1][0] == '@' case splitted[0] - when '/info' # retrieve some information by link, @username or username - response = "Contact id: %s\nContact name: %s\nContact type: %s" % [resolved.id.to_s, resolved.title.to_s, resolved.type.class.to_s] if resolved when '/add' # open new private chat by its id chat = (resolved) ? resolved.id : splitted[1].to_i @client.create_private_chat(chat).wait @@ -258,8 +252,8 @@ class TelegramClient @client.close_chat(chat_id).wait @client.leave_chat(chat_id).wait @client.delete_chat_history(chat_id, true).wait - @xmpp.presence_update(chat_id, :unsubscribed) - @xmpp.presence_update(chat_id, :unavailable) + @xmpp.presence(chat_id, :unsubscribed) + @xmpp.presence(chat_id, :unavailable) @cache[:chats].delete(chat_id) when '/sed' # sed-like edit sed = splitted[1].split('/') @@ -278,7 +272,6 @@ class TelegramClient /s/mitsake/mistake/ — Edit last message /d — Delete last message - /info @username — Search public chat or user /add @username or id — Creates conversation with specified user /join chat_link or id — Joins chat by its link or id /invite @username — Invites @username to current chat @@ -291,13 +284,12 @@ class TelegramClient ' end - @xmpp.send_message(chat_id, response) if response + @xmpp.incoming_message(chat_id, response) if response end # processing outgoing message from queue # - def process_outgoing_msg(msg) - @logger.debug 'Sending message to user/chat <%s> within Telegram network..' % msg[:to] - chat_id, text, reply_to = msg[:to].to_i, msg[:text], 0 + def process_outgoing_msg(chat_id, text) + @logger.debug 'Sending message to user/chat <%s> within Telegram network..' % chat_id.to_s # processing /commands # return self.process_command(chat_id, text) if text[0] == '/' @@ -307,6 +299,8 @@ class TelegramClient splitted = text.split("\n") reply_to = splitted[0].scan(/\d/).join('') || 0 text = splitted.drop(1).join("\n") if reply_to != 0 + else + reply_to = 0 end # handling files received from xmpp # @@ -328,15 +322,11 @@ class TelegramClient # fullfil cache.. pasha durov, privet. # @client.get_chat(chat_id).then { |chat| @cache[:chats][chat_id] = chat # cache chat - self.process_user_info(chat.type.user_id) if chat.type.instance_of? TD::Types::ChatType::Private # cache user if it is private chat + @client.download_file(chat.photo.small.id) if chat.photo # download userpic + @xmpp.presence(chat_id.to_s, :subscribe, nil, nil, @cache[:chats][chat_id].title.to_s) # send subscription request + @xmpp.presence(chat_id.to_s, nil, :chat, nil, @cache[:chats][chat_id].title.to_s) if chat.type.instance_of? TD::Types::ChatType::BasicGroup orchat.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 - - # send to roster # - if @cache[:chats].key? chat_id - @logger.debug "Sending presence to roster.." - @xmpp.presence_update(chat_id.to_s, :subscribe, nil, nil, @cache[:chats][chat_id].title.to_s) # send subscription request - @xmpp.presence_update(chat_id.to_s, nil, :chat, nil, @cache[:chats][chat_id].title.to_s) if chat_id < 0 # send :chat status if its group/supergroup - end end # update user info in cache and sync status to roster if needed # @@ -346,6 +336,9 @@ class TelegramClient @cache[:users][user_id] = user # add to cache self.process_status_update(user_id, user.status) # status update }.wait + @client.get_user_full_info(user_id).then{ |user_info| + @cache[:users_fi][user_id] = user_info # here is user "bio" + }.wait end # convert telegram status to XMPP one @@ -369,7 +362,41 @@ class TelegramClient xmpp_show = :unavailable xmpp_status = "Last seen last month" end - @xmpp.presence_update(user_id.to_s, nil, xmpp_show, xmpp_status) + @xmpp.presence(user_id.to_s, nil, xmpp_show, xmpp_status) + end + + # get contact information (for vcard). + def get_contact_info(chat_id) + return if not @cache[:chats].key? chat_id # no such chat # + + username, firstname, lastname, phone, bio, userpic = nil + title = @cache[:chats][chat_id].title # + + # user information + if @cache[:users].key? chat_id then # its an user + firstname = @cache[:users][chat_id].first_name # + lastname = @cache[:users][chat_id].last_name # + username = @cache[:users][chat_id].username # + phone = @cache[:users][chat_id].phone_number # + bio = @cache[:users_fi][chat_id].bio if @cache[:users_fi].key? chat_id # + end + + # userpic # + if @cache[:chats][chat_id].photo then # we have userpic + userpic = self.format_content_link(@cache[:chats][chat_id].photo.small.remote.id, 'image.jpg', true) + userpic = Base64.encode64(IO.binread(userpic)) if File.exist? userpic + end + + # .. + return title, username, firstname, lastname, phone, bio, userpic + 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 ########################################### @@ -386,8 +413,9 @@ class TelegramClient end # format content link # - def format_content_link(file_id, fname) - path = "%s/%s%s" % [@@content_link, Digest::SHA256.hexdigest("Current user = %s, File ID = %s" % [@tg_login.to_s, file_id.to_s]).to_s, File.extname(fname)] + def format_content_link(file_id, fname, local = false) + prefix = (local) ? @@content_path : @@content_link + path = "%s/%s%s" % [prefix, Digest::SHA256.hexdigest(file_id), File.extname(fname)] return path end end diff --git a/inc/xmppcomponent.rb b/inc/xmppcomponent.rb index 6791dcb..c5776ce 100644 --- a/inc/xmppcomponent.rb +++ b/inc/xmppcomponent.rb @@ -1,5 +1,4 @@ require 'sqlite3' -require 'fileutils' require 'xmpp4r' ############################# @@ -9,6 +8,7 @@ require 'xmpp4r' /login — Connect to Telegram network /code 12345 — Enter confirmation code /password secret — Enter 2FA password + /connect ­— Connect to Telegram network if have active session /disconnect ­— Disconnect from Telegram network /logout — Disconnect from Telegram network and forget session ' @@ -59,7 +59,7 @@ class XMPPComponent @@transport.auth( @config[:secret] ) @@transport.add_message_callback do |msg| msg.first_element_text('body') ? self.message_handler(msg) : nil end @@transport.add_presence_callback do |presence| self.presence_handler(presence) end - #@@transport.add_iq_callback do |iq| self.iq_handler(iq) end + @@transport.add_iq_callback do |iq| self.iq_handler(iq) end @logger.info "Connection established" self.load_db() @logger.info 'Found %s sessions in database.' % @sessions.count @@ -88,20 +88,31 @@ class XMPPComponent def message_handler(msg) @logger.info 'New message from [%s] to [%s]' % [msg.from, msg.to] 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 - return @sessions[msg.from.bare.to_s].queue_message(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 + 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 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].offline!; return; end # go offline when received offline presence from jabber user + 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].connect(); return; end # connect if we have session end def iq_handler(iq) @logger.debug "New iq received" - @logger.debug(iq) + @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) end ############################# @@ -116,16 +127,15 @@ class XMPPComponent @sessions[jfrom].connect() self.update_db(jfrom) when '/code', '/password' # pass auth data if we have session - typ = body.split[0][1..8] - data = body.split[1] - @sessions[jfrom].enter_auth_data(typ, data) if @sessions.key? jfrom + @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 when '/disconnect' # going offline without destroying a session - @sessions[jfrom].offline! if @sessions.key? jfrom + @sessions[jfrom].disconnect() if @sessions.key? jfrom when '/logout' # destroying session - @sessions[jfrom].offline! if @sessions.key? jfrom + @sessions[jfrom].disconnect(true) if @sessions.key? jfrom self.update_db(jfrom, true) @sessions.delete(jfrom) - FileUtils.remove_dir('sessions/' + jfrom, true) else # unknown command -- display help # msg = Jabber::Message.new msg.from = @@transport.jid @@ -143,13 +153,13 @@ end ############################# class XMPPSession < XMPPComponent attr_reader :user_jid, :tg_login - attr_accessor :online, :message_queue, :auth_data + 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, @auth_data, @message_queue = jid, tg_login, Hash.new(), Queue.new() # init class variables + @user_jid, @tg_login = jid, tg_login end # connect to tg # @@ -157,14 +167,20 @@ class XMPPSession < XMPPComponent return if self.online? @logger.info "Spawning Telegram client.." @online = nil - Thread.kill(@telegram_thr) if defined? @telegram_thr # kill old thread if it exists - @telegram_thr = Thread.new{ TelegramClient.new(self, @tg_login) } # init tg instance in new thread + @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 send_message(from = nil, body = '') + def incoming_message(from = nil, body = '') @logger.info "Received new message from Telegram peer %s" % from || "[self]" reply = Jabber::Message.new reply.type = :chat @@ -176,7 +192,7 @@ class XMPPSession < XMPPComponent end # presence update # - def presence_update(from, type = nil, show = nil, status = nil, nickname = nil) + def presence(from, type = nil, show = nil, status = nil, nickname = 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 @@ -192,21 +208,46 @@ class XMPPSession < XMPPComponent ########################################### # queue message (we will share this queue within :message_queue to Telegram client thread) # - def queue_message(to, text = '') - @logger.debug "Queuing message to be sent to Telegram network user -> " % to - @message_queue << {to: to.split('@')[0], text: text} + 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) end # enter auth data (we will share this data within :auth_data {} to Telegram client thread ) # - def enter_auth_data(typ, data) + def tg_auth(typ, data) @logger.info "Authenticating in Telegram network with :%s" % typ - @auth_data[typ.to_sym] = data + @telegram.process_auth(typ, data) end + + # 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 + ########################################### # session status # def online?() @online end - def online!() @logger.info "Connection established"; @online = true; self.presence_update(nil, :subscribe); self.presence_update(nil, nil, nil, "Logged in as " + @tg_login.to_s) end - def offline!() @online = false; self.presence_update(nil, :unavailable, nil, "Logged out"); 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 end diff --git a/zhabogram.rb b/zhabogram.rb index bc21a9b..40bd40f 100644 --- a/zhabogram.rb +++ b/zhabogram.rb @@ -3,7 +3,7 @@ require 'yaml' require 'logger' require 'xmpp4r' require 'digest' -require 'fileutils' +require 'base64' require 'sqlite3' require 'tdlib-ruby' require_relative 'inc/telegramclient'