Release 0.4

[NEW] vCard support
[UPD] improved overall stability, no longer use threads
[UPD] no longer using tdlib messages and chats database — now correctly /leaving chats
[UPD] removed /info command because of vCards support
This commit is contained in:
annelin 2019-04-13 17:42:56 +03:00
parent ef144a3b83
commit 12191222b0
4 changed files with 148 additions and 79 deletions

View file

@ -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'

View file

@ -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 # <FN>
# user information
if @cache[:users].key? chat_id then # its an user
firstname = @cache[:users][chat_id].first_name # <N/GIVEN>
lastname = @cache[:users][chat_id].last_name # <N/FAMILY>
username = @cache[:users][chat_id].username # <NICKNAME>
phone = @cache[:users][chat_id].phone_number # <TEL>
bio = @cache[:users_fi][chat_id].bio if @cache[:users_fi].key? chat_id # <DESC>
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

View file

@ -1,5 +1,4 @@
require 'sqlite3'
require 'fileutils'
require 'xmpp4r'
#############################
@ -9,6 +8,7 @@ require 'xmpp4r'
/login <telegram_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 <from>
@ -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

View file

@ -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'