Release 2.0.0
! Backward compatibility with old sessions file (sessions.db) is broken due to planned config variables in future. New format is: :sessions jid@server :login: '1234567890' — Huge code refactoring and readability improvement — Removed XMPP features jabber:iq:register (will now try to log in after subscription request) and jabber:iq:gateway (since it does not working on foreign servers?) — Reworked application logic — now code in `xmppcomponent.rb` only used to XMPP jobs and `telegramclient.rb` aims Telegram. Because of that, commands like /login /code /password are moved out to `telegramclient.rb` and now are parts of internal command to gateway (chat id 0) — Reworked /chat and /gateway command list — please, just send /help to gateway or any active chat. — Improved stability (I hope so...) — Any other small (and not only) unlisted and forgotten changes..
This commit is contained in:
parent
eca1bfcb8f
commit
4dfd278a39
|
@ -1,10 +1,8 @@
|
||||||
### Dependencies ###
|
### Dependencies ###
|
||||||
|
|
||||||
* Ruby >= 1.9
|
* Ruby >= 1.9
|
||||||
* ruby-sqlite3 >= 1.3
|
|
||||||
* xmpp4r == 0.5.6
|
* xmpp4r == 0.5.6
|
||||||
* tdlib-ruby == 2.0 with pre-compiled _libtdjson.so_
|
* 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.
|
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**).
|
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**).
|
||||||
|
|
|
@ -1,25 +1,23 @@
|
||||||
telegram:
|
:telegram:
|
||||||
|
:lib_path: 'lib/'
|
||||||
api_id: '845316' # telegram API ID (my.telegram.org) #
|
:verbosity: 1
|
||||||
api_hash: '27fe5224bc822bf3a45e015b4f9dfdb7' # telegram API HASH (my.telegram.org) #
|
:loglevel: :warn
|
||||||
verbosity: 2 # 1 = no verbosity, 2 = moderate verbosity, 3 = network requests debug
|
:client:
|
||||||
useragent: 'Zhabogram' # client name
|
:api_id: '17349'
|
||||||
version: '1.2' # client version
|
:api_hash: '344583e45741c457fe1862106095a5eb'
|
||||||
use_test_dc: false # always use false
|
:device_model: 'zhabogram'
|
||||||
loglevel: 0 # 0 = debug, 1 = info, 2 = warn, 3 = err, 4 = fatal, 5 = unknown (ruby logger class)
|
:application_version: '2.0'
|
||||||
content_path: '/var/zhabogram/content' # we will move (symlink) downloaded content here — you must setup web server that serve this directry
|
:use_chat_info_database: false
|
||||||
content_link: 'https://tlgrm.localhost/content' # web server that serve `content_path` internet address
|
:content:
|
||||||
content_upload_prefix: 'https://xmppfiles.localhost/upload' # we will send files with URL starting with this string as document to Telegram (mod_http_upload get_url, see readme)
|
:path: '/var/www/zhabogram/media' # webserver workdir
|
||||||
|
:link: 'http://localhost/zhabogram/media' # webserver public address
|
||||||
xmpp:
|
:upload: 'https://localhost/upload' # xmpp http upload address
|
||||||
|
|
||||||
debug: false
|
|
||||||
admins:
|
|
||||||
- 'root@localhost'
|
|
||||||
db_path: 'users.dat' # users store (JID:Telegram Login)
|
|
||||||
jid: 'tlgrm.localhost' # component JID
|
|
||||||
host: 'localhost' # XMPP server
|
|
||||||
port: 8888 # component port
|
|
||||||
password: 'secret' # component auth secret
|
|
||||||
loglevel: 0 # 0 = debug, 1 = info, 2 = warn, 3 = err, 4 = fatal, 5 = unknown (ruby logger class)
|
|
||||||
|
|
||||||
|
:xmpp:
|
||||||
|
:debug: false
|
||||||
|
:loglevel: :warn
|
||||||
|
:host: '127.0.0.1'
|
||||||
|
:port: 8899
|
||||||
|
:jid: 'tlgrm.localhost'
|
||||||
|
:password: 'password'
|
||||||
|
:db: 'sessions.dat'
|
||||||
|
|
|
@ -1,483 +1,318 @@
|
||||||
|
::HELP_GATE_CMD= "/login phone — sign in\n" \
|
||||||
|
"/logout — sign out\n" \
|
||||||
|
"/code — check one-time code\n" \
|
||||||
|
"/password — check 2fa password\n" \
|
||||||
|
"/setusername username — update @username\n" \
|
||||||
|
"/setname first last — update name\n" \
|
||||||
|
"/setbio — update about\n" \
|
||||||
|
"/setpassword [old] [new] — set or remove password"
|
||||||
|
::HELP_CHAT_CMD= "/d [n] — delete your last message(s)\n" \
|
||||||
|
"/s regexp replace — edit your last message\n" \
|
||||||
|
"/search string [limit] — search <string> in current chat\n" \
|
||||||
|
"/history [limit] — get last [limit] messages from current chat\n" \
|
||||||
|
"/add @username — add @username to your chat list\n" \
|
||||||
|
"/join https://t.me/invite_link — join to chat via invite link\n" \
|
||||||
|
"/supergroup title description — create new supergroup «title» with «description»\n" \
|
||||||
|
"/channel title description — create new channel «title» with «description»\n" \
|
||||||
|
"/secret — create secretchat with current user\n" \
|
||||||
|
"/group title — create groupchat «title» with current user\n" \
|
||||||
|
"/block — blacklist current user\n" \
|
||||||
|
"/unblock — unblacklist current user\n" \
|
||||||
|
"/invite id or @username — add user to current chat\n" \
|
||||||
|
"/kick id or @username — remove user from current chat\n" \
|
||||||
|
"/ban id or @username [hours] — restrict @username from current chat for [hours] or forever\n" \
|
||||||
|
"/leave — leave current chat\n" \
|
||||||
|
"/close — close current secret chat\n" \
|
||||||
|
"/delete — delete current chat from chat list"
|
||||||
|
|
||||||
class TelegramClient
|
class TelegramClient
|
||||||
|
|
||||||
attr_reader :jid, :login, :online, :auth_state, :me
|
attr_reader :session, :state
|
||||||
attr_accessor :timezone
|
@@config = {loglevel: :debug, verbosity: 2, lib_path: 'lib/', client: {api_id: 50322, api_hash: '9ff1a639196c0779c86dd661af8522ba', use_chat_info_database: false}, content: {path:'',link:'',upload:''}} # defaults
|
||||||
|
|
||||||
# tdlib configuration, shared within all instances #
|
## configure tdlib (when valid tdlib params specified) or zhabogram
|
||||||
def self.configure(params)
|
def self.configure(**config)
|
||||||
@@loglevel = params['loglevel'] || Logger::DEBUG
|
@@config = @@config.merge(config)
|
||||||
@@content_path = params['content_path'] || '/tmp'
|
TD.config[:lib_path] = @@config[:lib_path]
|
||||||
@@content_link = params['content_link'] || 'https://localhost/tg_media'
|
TD.configure do |config| @@config[:client].each do |key,value| config.client[key] = value if config.client.key? key end; end
|
||||||
@@content_upload_prefix = params["content_upload_prefix"] || 'https://localhost/upload/'
|
TD::Api.set_log_verbosity_level(@@config[:verbosity])
|
||||||
TD.configure do |config|
|
|
||||||
config.lib_path = params['path'] || 'lib/' # we hope it's here
|
|
||||||
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 = true # 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
|
end
|
||||||
|
|
||||||
# instance initialization #
|
## initialize telegram client instance (xmpp = XMPP stream, jid = user's jid , login = user's telegram login (for now, it is phone number)
|
||||||
def initialize(xmpp, jid, login)
|
def initialize(xmpp, jid, **session)
|
||||||
return if not @@loglevel # call .configure() first
|
@logger = Logger.new(STDOUT, level: @@config[:loglevel], progname: 'TelegramClient: %s | %s' % [jid, session[:login]] )
|
||||||
@logger = Logger.new(STDOUT); @logger.level = @@loglevel; @logger.progname = '[TelegramClient: %s/%s]' % [jid, login] # create logger
|
@xmpp = xmpp
|
||||||
@logger.info 'Starting Telegram client..'
|
@jid = jid
|
||||||
@xmpp = xmpp # XMPP stream
|
@session = session
|
||||||
@jid = jid # user JID
|
@cache = {chats: {nil => []}, users: {}}
|
||||||
@timezone = '-00:00' # default timezone is UTC
|
@xmpp.send_presence(@jid, nil, :subscribe)
|
||||||
@login = login # telegram login
|
@xmpp.send_presence(@jid, nil, :probe)
|
||||||
@me = nil # self telegram profile
|
|
||||||
@online = nil # we do not know
|
|
||||||
@auth_state = 'nil' # too.
|
|
||||||
@cache = {chats: {}, users: {}, photos: {}, subscribed: []} # cache
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# initialize and connect telegram client #
|
## connect telegram client
|
||||||
def connect()
|
def connect()
|
||||||
return if @client and @client.ready?
|
return if self.online? # already connected.
|
||||||
@logger.info 'Connecting to Telegram network..'
|
@logger.warn 'Connecting to Telegram network..'
|
||||||
@client = TD::Client.new(database_directory: 'sessions/' + @jid, files_directory: 'sessions/' + @jid + '/files/') # create telegram client instance
|
@telegram = TD::Client.new(database_directory: 'sessions/' + @jid, files_directory: 'sessions/' + @jid + '/files/')
|
||||||
@client.on(TD::Types::Update::AuthorizationState) do |update| self.auth_handler(update) end # register auth update handler
|
@telegram.on(TD::Types::Update::AuthorizationState) do |u| @logger.debug(u); self.update_authorizationstate(u) end
|
||||||
@client.on(TD::Types::Update::File) do |update| self.file_handler(update); end # register file handler
|
@telegram.on(TD::Types::Update::User) do |u| @logger.debug(u); self.update_user(u) end
|
||||||
@client.on(TD::Types::Update::NewMessage) do |update| self.message_handler(update); end # register new message update handler
|
@telegram.on(TD::Types::Update::UserStatus) do |u| @logger.debug(u); self.update_userstatus(u) end
|
||||||
@client.on(TD::Types::Update::MessageContent) do |update| self.message_edited_handler(update) end # register msg edited handler
|
@telegram.on(TD::Types::Update::NewChat) do |u| @logger.debug(u); self.update_newchat(u) end
|
||||||
@client.on(TD::Types::Update::DeleteMessages) do |update| self.message_deleted_handler(update) end # register msg del handler
|
@telegram.on(TD::Types::Update::NewMessage) do |u| @logger.debug(u); self.update_newmessage(u) end
|
||||||
@client.on(TD::Types::Update::NewChat) do |update| self.new_chat_handler(update) end # register new chat handler
|
@telegram.on(TD::Types::Update::MessageContent) do |u| @logger.debug(u); self.update_messagecontent(u) end
|
||||||
@client.on(TD::Types::Update::User) do |update| self.user_handler(update) end # new user update?
|
@telegram.on(TD::Types::Update::DeleteMessages) do |u| @logger.debug(u); self.update_deletemessages(u) end
|
||||||
@client.on(TD::Types::Update::UserStatus) do |update| self.status_update_handler(update) end # register status handler
|
@telegram.on(TD::Types::Update::File) do |u| @logger.debug(u); self.update_file(u) end
|
||||||
@client.connect()
|
@telegram.connect()
|
||||||
return true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# disconnect and destroy telegram client #
|
## disconnect telegram client
|
||||||
def disconnect(logout = false)
|
def disconnect()
|
||||||
return if not @client
|
return unless self.online? # already disconnected
|
||||||
@logger.info 'Disconnecting..'
|
@logger.warn 'Disconnecting from Telegram network..'
|
||||||
@cache[:chats].each_key do |chat_id| @xmpp.presence(@jid, chat_id.to_s, :unavailable) end # send offline presences
|
@cache[:chats].each_key do |chat| @xmpp.send_presence(@jid, chat, :unavailable) end # we're offline (unsubscribe if logout)
|
||||||
(logout) ? @client.log_out : @client.dispose # logout if needed
|
@telegram.dispose()
|
||||||
@client = nil
|
@telegram = nil
|
||||||
@online = false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
## online?
|
||||||
|
def online?
|
||||||
|
@telegram and @telegram.alive?
|
||||||
|
end
|
||||||
|
|
||||||
###########################################
|
#########################################################################
|
||||||
## Callback handlers #####################
|
# telegram updates handlers #############################################
|
||||||
###########################################
|
#########################################################################
|
||||||
|
|
||||||
# authorization handler #
|
|
||||||
def auth_handler(update)
|
|
||||||
@logger.debug 'Authorization state changed: %s' % update.authorization_state
|
|
||||||
@auth_state = update.authorization_state.class.name
|
|
||||||
|
|
||||||
|
## authorization state change
|
||||||
|
def update_authorizationstate(update)
|
||||||
|
@state = update.authorization_state.class.name
|
||||||
case update.authorization_state
|
case update.authorization_state
|
||||||
# auth stage 0: specify login #
|
when TD::Types::AuthorizationState::WaitPhoneNumber # stage 0: set login
|
||||||
when TD::Types::AuthorizationState::WaitPhoneNumber
|
@logger.warn 'Logging in..'
|
||||||
@logger.info 'Logging in..'
|
@telegram.set_authentication_phone_number(@session[:login]) if @session[:login]
|
||||||
@client.set_authentication_phone_number(@login)
|
@xmpp.send_message(@jid, nil, 'Please, enter your Telegram login via /login 12345') if not @session[:login]
|
||||||
# auth stage 1: wait for authorization code #
|
when TD::Types::AuthorizationState::WaitCode # stage 1: wait for auth code
|
||||||
when TD::Types::AuthorizationState::WaitCode
|
@logger.warn 'Waiting for authorization code..'
|
||||||
@logger.info 'Waiting for authorization code..'
|
@xmpp.send_message(@jid, nil, 'Please, enter authorization code via /code 12345')
|
||||||
@xmpp.message(@jid, nil, 'Please, enter authorization code via /code 12345')
|
when TD::Types::AuthorizationState::WaitPassword # stage 2: wait for 2fa
|
||||||
# auth stage 2: wait for 2fa passphrase #
|
@logger.warn 'Waiting for 2FA password..'
|
||||||
when TD::Types::AuthorizationState::WaitPassword
|
@xmpp.send_message(@jid, nil, 'Please, enter 2FA passphrase via /password 12345')
|
||||||
@logger.info 'Waiting for 2FA password..'
|
when TD::Types::AuthorizationState::Ready # stage 3: auth completed
|
||||||
@xmpp.message(@jid, nil, 'Please, enter 2FA passphrase via /password 12345')
|
@logger.warn 'Authorization successful!'
|
||||||
# authorization successful -- indicate that client is online and retrieve contact list #
|
@telegram.get_me.then{|me| @me = me}.wait
|
||||||
when TD::Types::AuthorizationState::Ready
|
@telegram.get_chats(limit=999).wait
|
||||||
@logger.info 'Authorization successful!'
|
@session[:login] ||= @me.phone_number
|
||||||
@client.get_me().then { |user| @me = user }.wait
|
@xmpp.send_presence(@jid, nil, nil, nil, "Logged in %s" % @session[:login])
|
||||||
@client.get_chats(limit=9999)
|
when TD::Types::AuthorizationState::Closing, TD::Types::AuthorizationState::Closed # disconnecting
|
||||||
@logger.info "Contact list updating finished"
|
@logger.warn 'Closing session..'
|
||||||
@xmpp.presence(@jid, nil, :subscribe)
|
|
||||||
@xmpp.presence(@jid, nil, nil, nil, "Logged in as %s" % @login)
|
|
||||||
@online = true
|
|
||||||
# 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.'
|
|
||||||
self.disconnect()
|
self.disconnect()
|
||||||
|
when TD::Types::AuthorizationState::LoggingOut # logout
|
||||||
|
@logger.warn 'Logging out..'
|
||||||
|
@session[:login] = nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# message from telegram network handler #
|
## message received
|
||||||
def message_handler(update, show_date = false)
|
def update_newmessage(update, show_date = false)
|
||||||
return if update.message.is_outgoing and update.message.sending_state.instance_of? TD::Types::MessageSendingState::Pending # ignore self outgoing messages
|
return if update.message.is_outgoing and update.message.sending_state.instance_of? TD::Types::MessageSendingState::Pending # ignore self outgoing messages
|
||||||
|
@logger.warn 'New message from chat %s' % update.message.chat_id
|
||||||
|
|
||||||
@logger.debug 'Got NewMessage update'
|
content, prefix = update.message.content, []
|
||||||
@logger.debug update.message.to_json
|
text = case content # text
|
||||||
@logger.info 'New message from Telegram chat %s' % update.message.chat_id
|
|
||||||
|
|
||||||
# add to contact list
|
|
||||||
self.process_chat_info(update.message.chat_id, true) if not @cache[:subscribed].include? update.message.chat_id
|
|
||||||
|
|
||||||
# message content
|
|
||||||
prefix = []
|
|
||||||
content = update.message.content
|
|
||||||
|
|
||||||
# file handling
|
|
||||||
file = case content
|
|
||||||
when TD::Types::MessageContent::Sticker then [content.sticker.sticker, 'sticker.webp']
|
|
||||||
when TD::Types::MessageContent::VoiceNote then [content.voice_note.voice, 'voicenote (%i s.).oga' % content.voice_note.duration]
|
|
||||||
when TD::Types::MessageContent::VideoNote then [content.video_note.video, 'videonote (%i s.).mp4' % content.video_note.duration]
|
|
||||||
when TD::Types::MessageContent::Animation then [content.animation.animation, 'animation.mp4' ]
|
|
||||||
when TD::Types::MessageContent::Photo then [content.photo.sizes[-1].photo, 'photo' + content.photo.id + '.jpg']
|
|
||||||
when TD::Types::MessageContent::Audio then [content.audio.audio, 'audio' + content.audio.file_name]
|
|
||||||
when TD::Types::MessageContent::Video then [content.video.video, 'video' + content.video.file_name]
|
|
||||||
when TD::Types::MessageContent::Document then [content.document.document, 'doc' + content.document.file_name]
|
|
||||||
end
|
|
||||||
|
|
||||||
# text handling
|
|
||||||
text = case content
|
|
||||||
when TD::Types::MessageContent::Sticker then content.sticker.emoji
|
when TD::Types::MessageContent::Sticker then content.sticker.emoji
|
||||||
when TD::Types::MessageContent::BasicGroupChatCreate, TD::Types::MessageContent::SupergroupChatCreate then "has created chat"
|
when TD::Types::MessageContent::BasicGroupChatCreate, TD::Types::MessageContent::SupergroupChatCreate then "has created chat"
|
||||||
when TD::Types::MessageContent::ChatJoinByLink then "joined chat via invite link"
|
when TD::Types::MessageContent::ChatJoinByLink then "joined chat via invite link"
|
||||||
when TD::Types::MessageContent::ChatAddMembers then "invited %s" % self.format_contact(message.content.member_user_ids.first)
|
when TD::Types::MessageContent::ChatAddMembers then "invited %s" % self.format_contact(message.content.member_user_ids.first)
|
||||||
when TD::Types::MessageContent::ChatDeleteMember then "kicked %s" % self.format_contact(update.message.content.user_id)
|
when TD::Types::MessageContent::ChatDeleteMember then "kicked %s" % self.format_contact(update.message.content.user_id)
|
||||||
when TD::Types::MessageContent::PinMessage then "pinned message: %s" % self.format_message(update.message.chat_id, content.message_id)
|
when TD::Types::MessageContent::PinMessage then "pinned message: %s" % self.format_message(update.message.chat_id, content.message_id)
|
||||||
when TD::Types::MessageContent::ChatChangeTitle then "chat title set to: %s" % update.message.content.title.to_s
|
when TD::Types::MessageContent::ChatChangeTitle then "chat title set to: %s" % update.message.content.title
|
||||||
when TD::Types::MessageContent::Location then "coordinates: %s | https://www.google.com/maps/search/%s,%s/" % [content.location.latitude, content.location.longitude]
|
when TD::Types::MessageContent::Location then "coordinates: %s | https://www.google.com/maps/search/%s,%s/" % [content.location.latitude, content.location.longitude]
|
||||||
when TD::Types::MessageContent::Photo, TD::Types::MessageContent::Audio, TD::Types::MessageContent::Video, TD::Types::MessageContent::Document then content.caption.text
|
when TD::Types::MessageContent::Photo, TD::Types::MessageContent::Audio, TD::Types::MessageContent::Video, TD::Types::MessageContent::Document then content.caption.text
|
||||||
when TD::Types::MessageContent::Text then content.text.text
|
when TD::Types::MessageContent::Text then content.text.text
|
||||||
when TD::Types::MessageContent::VoiceNote then content.caption.text
|
when TD::Types::MessageContent::VoiceNote then content.caption.text
|
||||||
when TD::Types::MessageContent::VideoNote then ''
|
when TD::Types::MessageContent::VideoNote then ''
|
||||||
when TD::Types::MessageContent::Animation then ''
|
when TD::Types::MessageContent::Animation then ''
|
||||||
else "unknown message type %s" % update.message.content.class
|
else "unknown message (%s)" % update.message.content.class
|
||||||
end
|
end
|
||||||
|
file = case content # file(s)
|
||||||
# download file if needed
|
when TD::Types::MessageContent::Sticker then [content.sticker.sticker, 'sticker.webp']
|
||||||
@client.download_file(file[0].id) if file and not file[0].local.is_downloading_completed
|
when TD::Types::MessageContent::VoiceNote then [content.voice_note.voice, 'voice note (%i s.).oga' % content.voice_note.duration]
|
||||||
|
when TD::Types::MessageContent::VideoNote then [content.video_note.video, 'video note (%i s.).mp4' % content.video_note.duration]
|
||||||
# forwards, replies and message id..
|
when TD::Types::MessageContent::Animation then [content.animation.animation, 'animation.mp4' ]
|
||||||
prefix << DateTime.strptime((update.message.date+Time.now.getlocal(@timezone).utc_offset).to_s,'%s').strftime("%d %b %Y %H:%M:%S") if show_date # show date if its
|
when TD::Types::MessageContent::Photo then [content.photo.sizes[-1].photo, content.photo.id + '.jpg']
|
||||||
|
when TD::Types::MessageContent::Audio then [content.audio.audio, content.audio.file_name]
|
||||||
|
when TD::Types::MessageContent::Video then [content.video.video, 'video' + content.video.file_name + '.mp4']
|
||||||
|
when TD::Types::MessageContent::Document then [content.document.document, content.document.file_name]
|
||||||
|
end
|
||||||
|
@telegram.download_file(file[0].id) if file and not file[0].local.is_downloading_completed # download file(s)
|
||||||
|
prefix << DateTime.strptime((update.message.date+Time.now.getlocal(@session[:timezone]).utc_offset).to_s,'%s').strftime("%d %b %Y %H:%M:%S") if show_date # show date if its
|
||||||
prefix << (update.message.is_outgoing ? '➡ ' : '⬅ ') + update.message.id.to_s # message direction
|
prefix << (update.message.is_outgoing ? '➡ ' : '⬅ ') + update.message.id.to_s # message direction
|
||||||
prefix << "%s" % self.format_contact(update.message.sender_user_id) if update.message.chat_id < 0 # show sender in group chats
|
prefix << "%s" % self.format_contact(update.message.sender_user_id) if update.message.chat_id < 0 and update.message.sender_user_id # show sender in group chats
|
||||||
prefix << "fwd: %s" % self.format_contact(update.message.forward_info.sender_user_id) if update.message.forward_info.instance_of? TD::Types::MessageForwardInfo::MessageForwardedFromUser # fwd from user
|
prefix << "fwd: %s" % self.format_contact(update.message.forward_info.sender_user_id) if update.message.forward_info.instance_of? TD::Types::MessageForwardInfo::MessageForwardedFromUser # fwd from user
|
||||||
prefix << "fwd: %s%s" % [self.format_contact(update.message.forward_info.chat_id), (update.message.forward_info.author_signature != '') ? " (%s)"%update.message.forward_info.author_signature : ''] if update.message.forward_info.instance_of? TD::Types::MessageForwardInfo::MessageForwardedPost # fwd from chat
|
prefix << "fwd: %s%s" % [self.format_contact(update.message.forward_info.chat_id), (update.message.forward_info.author_signature != '') ? " (%s)"%update.message.forward_info.author_signature : ''] if update.message.forward_info.instance_of? TD::Types::MessageForwardInfo::MessageForwardedPost # fwd from chat
|
||||||
prefix << "reply: %s" % self.format_message(update.message.chat_id, update.message.reply_to_message_id, false) if update.message.reply_to_message_id.to_i != 0 # reply to
|
prefix << "reply: %s" % self.format_message(update.message.chat_id, update.message.reply_to_message_id, true) if update.message.reply_to_message_id.to_i != 0 # reply to
|
||||||
prefix << "file: %s" % self.format_file(file[0], file[1]) if file
|
prefix << "file: %s" % self.format_content(file[0], file[1]) if file
|
||||||
prefix = prefix.join(' | ')
|
prefix = prefix.join(' | ')
|
||||||
prefix += (update.message.chat_id < 0 and text and text != "") ? "\n" : '' # \n if it is groupchat and message is not empty
|
prefix += (update.message.chat_id < 0 and text and text != "") ? "\n" : '' # \n if it is groupchat and message is not empty
|
||||||
prefix += (update.message.chat_id > 0 and text and text != "") ? " | " : ''
|
prefix += (update.message.chat_id > 0 and text and text != "") ? " | " : ''
|
||||||
|
|
||||||
# OTR support
|
text = prefix + text unless text.start_with? '?OTR' # OTR support (I do not know why would you need it, seriously)
|
||||||
text = prefix + text unless text.start_with? '?OTR'
|
@telegram.view_messages(update.message.chat_id, [update.message.id], force_read: true) # mark message as read
|
||||||
|
@xmpp.send_message(@jid, update.message.chat_id, text) # forward message to XMPP
|
||||||
# read message & send it to xmpp
|
|
||||||
@client.view_messages(update.message.chat_id, [update.message.id], force_read: true)
|
|
||||||
@xmpp.message(@jid, update.message.chat_id.to_s, text)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# new chat update -- when tg client discovers new chat #
|
## message content updated
|
||||||
def new_chat_handler(update)
|
def update_messagecontent(update)
|
||||||
@logger.debug 'Got NewChat update'
|
text = "✎ %s | %s" % [update.message_id, update.new_content.text.text]
|
||||||
@logger.debug update.to_json
|
@xmpp.send_message(@jid, update.chat_id, text)
|
||||||
self.process_chat_info(update.chat.id)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# user -- something changed in user data #
|
## message(s) deleted
|
||||||
def user_handler(update)
|
def update_deletemessages(update)
|
||||||
@logger.debug 'Got User update'
|
text = "✗ %s" % update.message_ids.join(',')
|
||||||
@logger.debug update.to_json
|
@xmpp.send_message(@jid, update.chat_id, text) if update.is_permanent
|
||||||
self.process_user_info(update.user.id)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# edited msg #
|
## new chat discovered
|
||||||
def message_edited_handler(update)
|
def update_newchat(update)
|
||||||
@logger.debug 'Got MessageEdited update'
|
@telegram.download_file(update.chat.photo.small.id).wait if update.chat.photo
|
||||||
@logger.debug update.to_json
|
@cache[:chats][update.chat.id] = update.chat
|
||||||
|
@xmpp.send_presence(@jid, update.chat.id, :subscribe, nil, nil, update.chat.title.to_s) unless (update.chat.type.instance_of? TD::Types::ChatType::Supergroup and update.chat.type.is_channel and update.chat.last_read_inbox_message_id == 0)
|
||||||
# formatting
|
self.process_status_update(update.chat.id, update.chat.title, :chat) if update.chat.id < 0
|
||||||
text = "✎ %s | %s" % [update.message_id.to_s, update.new_content.text.text.to_s]
|
|
||||||
@xmpp.message(@jid, update.chat_id.to_s, text)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# deleted msg #
|
## new user discovered
|
||||||
def message_deleted_handler(update)
|
def update_user(update)
|
||||||
@logger.debug 'Got MessageDeleted update'
|
@cache[:users][update.user.id] = update.user
|
||||||
@logger.debug update.to_json
|
self.process_status_update(update.user.id, update.user.status)
|
||||||
return if not update.is_permanent
|
|
||||||
text = "✗ %s |" % update.message_ids.join(',')
|
|
||||||
@xmpp.message(@jid, update.chat_id.to_s, text)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# status update handler #
|
## user status changed
|
||||||
def status_update_handler(update)
|
def update_userstatus(update)
|
||||||
@logger.debug 'Got new StatusUpdate'
|
self.process_status_update(update.user_id, update.status, nil, false)
|
||||||
@logger.debug update.to_json
|
|
||||||
return if update.user_id == @me.id # ignore self statuses
|
|
||||||
self.process_status_update(update.user_id, update.status, false)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
## file downloaded
|
||||||
# file msg -- symlink to download path #
|
def update_file(update)
|
||||||
def file_handler(update)
|
return unless update.file.local.is_downloading_completed # not really
|
||||||
@logger.debug 'Got File update'
|
File.symlink(update.file.local.path, "%s/%s%s" % [@@config[:content][:path], Digest::SHA256.hexdigest(update.file.remote.id), File.extname(update.file.local.path)])
|
||||||
@logger.debug update.to_json
|
|
||||||
if update.file.local.is_downloading_completed then
|
|
||||||
source = update.file.local.path.to_s
|
|
||||||
target = self.format_file(update.file, update.file.local.path, true)
|
|
||||||
@logger.debug 'Downloading of <%s> completed! Created link to <%s>' % [source, target]
|
|
||||||
File.symlink(source, target)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
###########################################
|
#########################################################################
|
||||||
## LooP handlers #########################
|
# xmpp to telegram gateway functions ####################################
|
||||||
###########################################
|
#########################################################################
|
||||||
|
|
||||||
# processing authorization #
|
## get user and chat information from cache (or try to retrieve it, if missing)
|
||||||
def process_auth(typ, auth_data)
|
def get_contact(id)
|
||||||
@logger.info "Authorizing with :%s.." % typ
|
return unless self.online? # we're offline.
|
||||||
@client.check_authentication_code(auth_data) if typ == '/code'
|
@telegram.search_public_chat(id).then{|chat| id = chat.id }.wait if id[0] == '@'
|
||||||
@client.check_authentication_password(auth_data) if typ == '/password'
|
@telegram.get_user(id).wait if not @cache[:users][id] and (id>0)
|
||||||
|
@telegram.get_chat(id).wait if not @cache[:chats][id]
|
||||||
|
return @cache[:chats][id], @cache[:users][id]
|
||||||
end
|
end
|
||||||
|
|
||||||
# /command #
|
## set contact status
|
||||||
def process_command(chat_id, text)
|
def process_status_update(chat_id, status=nil, show=nil, immed=true)
|
||||||
arg = text[0..2] == '/s/' ? ['/sed', text[3..-1]] : text.split
|
return unless self.online? # we're offline.
|
||||||
|
@logger.info "Status update for %s" % chat_id
|
||||||
# ..
|
chat, user = self.get_contact(chat_id)
|
||||||
if arg[1] and arg[1][0] == '@' then @client.search_public_chat(arg[1][1..-1]).then {|c| resolve = c}.wait end # try to resolve @username from second arg #
|
photo = Digest::SHA1.hexdigest(IO.binread(chat.photo.small.local.path)) if chat and chat.photo and File.exist? chat.photo.small.local.path
|
||||||
if arg[1].to_i < 0 then resolve = self.process_chat_info(arg[1].to_i, false) end # try to resolve chat_id/user_id from second arg
|
status ||= user.status if user and user.status
|
||||||
if arg[1].to_i > 0 then resolve = self.process_user_info(arg[1].to_i) end # try to resolve user_id from second arg
|
|
||||||
|
|
||||||
# command...
|
|
||||||
response = nil
|
|
||||||
current = @cache[:chats][chat_id] # current chat
|
|
||||||
resolve = resolve || nil # resolved chat or nil
|
|
||||||
chat = resolve || current # resolved chat or current
|
|
||||||
case arg[0]
|
|
||||||
when '/info' then response = self.format_contact(chat.id) # print information
|
|
||||||
when '/add' then (chat.id > 0) ? self.process_chat_info(chat.id, true) : @client.join_chat(chat.id).wait # add contact
|
|
||||||
when '/join' then @client.join_chat_by_invite_link(arg[1]).wait if arg[1][0..3] == 'http' # join chat by link
|
|
||||||
when '/secret' then @client.create_new_secret_chat(chat.id).wait if chat.id > 0 # new secret chat
|
|
||||||
when '/group' then @client.create_new_basic_group_chat(resolve.id, arg[2]).it if resolve and arg[2]
|
|
||||||
when '/supergroup' then @client.create_new_supergroup_chat(arg[1], arg[2]).wait if arg[2]
|
|
||||||
when '/channel' then @client.create_new_supergroup_chat(arg[1], arg[2], is_channel: true).wait if arg[2]
|
|
||||||
when '/invite' then @client.add_chat_member(current.id, resolve.id).wait if resolve
|
|
||||||
when '/kick' then @client.set_chat_member_status(current, resolve.id, TD::Types::ChatMemberStatus::Left.new()).wait if resolve
|
|
||||||
when '/ban' then @client.set_chat_member_status(current.id, resolve.id, TD::Types::ChatMemberStatus::Banned.new(banned_until_date: (arg[1]) ? Time.now.getutc.to_i + arg[1].to_i * 3600 : 0)).wait if resolve
|
|
||||||
when '/block' then @client.block_user(current.id).wait
|
|
||||||
when '/unblock' then @client.unblock_user(current.id).wait
|
|
||||||
when '/members' then members = []
|
|
||||||
response = "- Members of chat %s -\n\n" % current.title
|
|
||||||
@client.search_chat_members(current.id,filter:TD::Types::ChatMembersFilter::Members.new).then{ |m| members+=m.members }.wait if current.type.instance_of? TD::Types::ChatType::BasicGroup # basic
|
|
||||||
@client.get_supergroup_members(current.type.supergroup_id).then{|m| members+=m.members }.wait if current.type.instance_of? TD::Types::ChatType::Supergroup # super
|
|
||||||
members.each do |user| response += "%s | Role: %s \n" % [self.format_contact(user.user_id, true, false), user.status.class] end
|
|
||||||
when '/leave','/delete' then @client.close_chat(current.id).wait
|
|
||||||
@client.leave_chat(current.id) if current.type.instance_of? TD::Types::ChatType::BasicGroup or current.type.instance_of? TD::Types::ChatType::Supergroup
|
|
||||||
@client.close_secret_chat(current.type.secret_chat_id).wait if current.type.instance_of? TD::Types::ChatType::Secret
|
|
||||||
@client.delete_chat_history(current.id, true).wait
|
|
||||||
@xmpp.presence(@jid, current.id.to_s, :unsubscribed)
|
|
||||||
@xmpp.presence(@jid, current.id.to_s, :unavailable)
|
|
||||||
@cache[:chats].delete(current.id) if @cache[:chats].key? current.id
|
|
||||||
@cache[:users].delete(current.id) if @cache[:users].key? current.id
|
|
||||||
when '/sed' then id, edited = nil, nil
|
|
||||||
sed = arg[1].split('/')
|
|
||||||
@client.search_chat_messages(current.id, 0, 1, sender_user_id: @me.id, filter: TD::Types::SearchMessagesFilter::Empty.new).then{|m| id,edited = m.messages[0].id,m.messages[0].content.text.text.to_s}.wait
|
|
||||||
@client.edit_message_text(current.id,id,TD::Types::InputMessageContent::Text.new(text: {text: edited.gsub(Regexp.new(sed[0]),sed[1]), entities: []},disable_web_page_preview: false, clear_draft: true)).wait if id
|
|
||||||
when '/d' then id = arg[1].to_i
|
|
||||||
@client.search_chat_messages(current.id, 0, 1, sender_user_id: @me.id, filter: TD::Types::SearchMessagesFilter::Empty.new).then {|m| id = m.messages[0].id }.wait if id == 0
|
|
||||||
@client.delete_messages(current.id, [id], true)
|
|
||||||
when '/search' then count = arg[1] || 10
|
|
||||||
query = arg[2] || nil
|
|
||||||
@client.search_chat_messages(current.id, 0, count, query: query, filter: TD::Types::SearchMessagesFilter::Empty.new).then {|msgs|
|
|
||||||
msgs.messages.reverse.each do |msg| self.message_handler(TD::Types::Update::NewMessage.new(message: msg, disable_notification: false, contains_mention: false), true) end
|
|
||||||
}.wait
|
|
||||||
when '/setusername' then @client.set_username(arg[1] || '')
|
|
||||||
when '/setname' then @client.set_name(arg[1] || '', arg[2] || '')
|
|
||||||
when '/setbio' then @client.set_bio(arg[1..99].join(' '))
|
|
||||||
when '/setpassword' then old_password, new_password = arg[1], arg[2]
|
|
||||||
old_password = '' if old_password == 'nil'
|
|
||||||
new_password = nil if new_password == 'nil'
|
|
||||||
@client.set_password(old_password, new_password: new_password)
|
|
||||||
when '/dump' then response = current.to_json
|
|
||||||
else response = 'Unknown command.
|
|
||||||
|
|
||||||
/s/mitsake/mistake/ — Edit last message
|
|
||||||
/d — Delete last message
|
|
||||||
|
|
||||||
/info id — Information about user/chat by its id
|
|
||||||
/add @username or id — Create conversation with specified user or chat id
|
|
||||||
/join chat_link or id — Join chat by its link or id
|
|
||||||
|
|
||||||
/secret @username — Create "secret chat" with specified user
|
|
||||||
/group @username groupname — Create group chat named groupname with @username
|
|
||||||
/supergroup name description — Create supergroup chat
|
|
||||||
/channel name description — Create channel
|
|
||||||
|
|
||||||
/members — Supergroup members
|
|
||||||
/search count query — Search in chat history
|
|
||||||
|
|
||||||
/invite @username — Invite @username to current chat
|
|
||||||
/kick @username — Remove @username from current chat
|
|
||||||
/ban @username [hours] — Ban @username in current chat for [hours] hrs or forever if [hours] not specified
|
|
||||||
/block — Blacklistscurrent user
|
|
||||||
/unblock — Remove current user from blacklist
|
|
||||||
/delete — Delete current chat
|
|
||||||
/leave — Leave current chat
|
|
||||||
|
|
||||||
/setusername username — Set username
|
|
||||||
/setname First Last — Set name
|
|
||||||
/setbio Bio — Set bio
|
|
||||||
/setpassword old new — Set 2FA password (use "nil" for no password")
|
|
||||||
'
|
|
||||||
end
|
|
||||||
|
|
||||||
@xmpp.message(@jid, chat_id.to_s, response) if response
|
|
||||||
end
|
|
||||||
|
|
||||||
# processing outgoing message from queue #
|
|
||||||
def process_outgoing_msg(chat_id, text)
|
|
||||||
@logger.info 'Sending message to Telegram chat %s...' % chat_id
|
|
||||||
|
|
||||||
# processing /commands #
|
|
||||||
return if not @cache[:chats].key? chat_id # null chat
|
|
||||||
return self.process_command(chat_id, text) if text[0] == '/'
|
|
||||||
|
|
||||||
# handling replies #
|
|
||||||
reply_to = 0
|
|
||||||
if text[0] == '>' and text.match(Regexp.new /^>( )?[0-9]{10,20}/) then
|
|
||||||
text = text.split("\n")
|
|
||||||
reply_to = text[0].scan(/\d+/).first.to_i
|
|
||||||
text = text.drop(1).join("\n")
|
|
||||||
end
|
|
||||||
|
|
||||||
# handling files received from xmpp #
|
|
||||||
message = TD::Types::InputMessageContent::Text.new(:text => { :text => text, :entities => []}, :disable_web_page_preview => false, :clear_draft => true )
|
|
||||||
message = TD::Types::InputMessageContent::Document.new(document: TD::Types::InputFile::Remote.new(id: text), caption: { :text => '', :entities => []}) if text.start_with? @@content_upload_prefix
|
|
||||||
|
|
||||||
# send message and mark chat as read #
|
|
||||||
@client.send_message(chat_id, message, reply_to_message_id: reply_to)
|
|
||||||
end
|
|
||||||
|
|
||||||
# update users information and save it to cache #
|
|
||||||
def process_chat_info(chat_id, subscription = false)
|
|
||||||
@logger.debug 'Updating chat id %s..' % chat_id.to_s
|
|
||||||
@client.get_chat(chat_id).then { |chat|
|
|
||||||
@cache[:chats][chat_id] = chat # cache chat
|
|
||||||
@client.download_file(chat.photo.small.id).then{|f| @cache[:photos][chat_id] = f}.wait if chat.photo # download userpic
|
|
||||||
if subscription then # send subscription request
|
|
||||||
@xmpp.presence(@jid, chat_id.to_s, :subscribe, nil, nil, chat.title.to_s)
|
|
||||||
@cache[:subscribed] << chat_id
|
|
||||||
end
|
|
||||||
self.process_status_update(chat_id, chat.title.to_s, true) if chat.id < 0 # groups presence
|
|
||||||
}.wait
|
|
||||||
return @cache[:chats][chat_id] if @cache[:chats].key? chat_id
|
|
||||||
end
|
|
||||||
|
|
||||||
# update user info in cache and sync status to roster if needed #
|
|
||||||
def process_user_info(user_id)
|
|
||||||
@logger.debug 'Updating user id %s..' % user_id
|
|
||||||
@client.get_user(user_id).then { |user|
|
|
||||||
@cache[:users][user_id] = user # add to cache
|
|
||||||
@client.get_user_full_info(user_id).then{ |bio| @cache[:chats][user_id].attributes[:client_data] = bio.bio }.wait
|
|
||||||
self.process_status_update(user_id, user.status, true) # status update
|
|
||||||
}.wait
|
|
||||||
return @cache[:users][user_id] if @cache[:users].key? user_id
|
|
||||||
end
|
|
||||||
|
|
||||||
# sync statuses with XMPP roster
|
|
||||||
def sync_status()
|
|
||||||
@logger.debug "Syncing statuses with roster.."
|
|
||||||
@cache[:chats].each_value do |chat| self.process_status_update(chat.id, (chat.id > 0 and @cache[:users].include? chat.id) ? @cache[:users][chat.id].status : chat.title.to_s, true) end
|
|
||||||
end
|
|
||||||
|
|
||||||
# convert telegram status to XMPP one
|
|
||||||
def process_status_update(user_id, status, immed = true)
|
|
||||||
@logger.debug "Processing status update for user id %s.." % user_id.to_s
|
|
||||||
xmpp_show, xmpp_status, xmpp_photo = nil
|
|
||||||
case status
|
case status
|
||||||
when TD::Types::UserStatus::Online then xmpp_show, xmpp_status = nil, "Online"
|
when nil then show, status = :chat, chat ? chat.title : nil
|
||||||
when TD::Types::UserStatus::Offline then xmpp_show, xmpp_status = (Time.now.getutc.to_i - status.was_online.to_i < 3600) ? :away : :xa, 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::Online then show, status = nil, "Online"
|
||||||
when TD::Types::UserStatus::Recently then xmpp_show, xmpp_status = :dnd, "Last seen recently"
|
when TD::Types::UserStatus::Recently then show, status = :dnd, "Last seen recently"
|
||||||
when TD::Types::UserStatus::LastWeek then xmpp_show, xmpp_status = :unavailable, "Last seen last week"
|
when TD::Types::UserStatus::LastWeek then show, status = :unavailable, "Last seen last week"
|
||||||
when TD::Types::UserStatus::LastMonth then xmpp_show, xmpp_status = :unavailable, "Last seen last month"
|
when TD::Types::UserStatus::LastMonth then show, status = :unavailable, "Last seen last month"
|
||||||
else xmpp_show, xmpp_status = :chat, status
|
when TD::Types::UserStatus::Empty then show, status = :unavailable, "Last seen a long time ago"
|
||||||
|
when TD::Types::UserStatus::Offline then show, status = (Time.now.getutc.to_i-status.was_online.to_i<3600) ? :away : :xa,
|
||||||
|
DateTime.strptime((status.was_online+Time.now.getlocal(@session[:timezone]).utc_offset).to_s,'%s').strftime("Last seen at %H:%M %d/%m/%Y")
|
||||||
|
end
|
||||||
|
@xmpp.send_presence(@jid, chat_id, nil, show, status, nil, photo, immed)
|
||||||
end
|
end
|
||||||
|
|
||||||
xmpp_photo = self.format_file(@cache[:photos][user_id], 'image.jpg', true) if @cache[:photos].include? user_id
|
## send outgoing message to telegram user
|
||||||
xmpp_photo = (File.exist? xmpp_photo.to_s) ? Digest::SHA1.hexdigest(IO.binread(xmpp_photo)) : nil
|
def process_outgoing_message(chat_id, text, message_id = nil)
|
||||||
# ...
|
return if self.process_command(chat_id, text.split.first, text.split[1..-1]) # try to execute a command (and do not send on success)
|
||||||
return @xmpp.presence(@jid, user_id.to_s, nil, xmpp_show, xmpp_status, nil, xmpp_photo, immed)
|
return unless self.online? # we're offline.
|
||||||
|
@logger.warn 'Send message to chat %s' % chat_id
|
||||||
|
reply = text.lines[0].scan(/\d+/).first.to_i if text.lines[0] =~ /^> ?[0-9]{10}/ # quotations
|
||||||
|
file = TD::Types::InputFile::Remote.new(id: text) if text.start_with? @@config[:content][:upload] # attach a file
|
||||||
|
text = TD::Types::FormattedText.new(text: (reply or file) ? text.lines[1..-1].join : text, entities: []) # remove first line from text
|
||||||
|
message = TD::Types::InputMessageContent::Text.new(text: text, disable_web_page_preview: false, clear_draft: false) # compile our message
|
||||||
|
document = TD::Types::InputMessageContent::Document.new(document: file, caption: text) if file # we can try to send a document
|
||||||
|
message_id ? @telegram.edit_message_text(chat_id, message_id, message) : @telegram.send_message(chat_id, document || message, reply_to_message_id: reply || 0).rescue{@telegram.send_message(chat_id, message, 0)}
|
||||||
end
|
end
|
||||||
|
|
||||||
# get contact information (for vcard).
|
## /commands (some telegram actions)
|
||||||
def get_contact_info(chat_id)
|
def process_command(chat_id, cmd, args)
|
||||||
return if not @cache[:chats].key? chat_id # no such chat #
|
chat, user = self.get_contact(chat_id) unless chat_id == 0 # get chat information
|
||||||
|
if chat_id == 0 then # transport commands
|
||||||
username, firstname, lastname, phone, bio, userpic = nil
|
case cmd
|
||||||
title = @cache[:chats][chat_id].title # <FN>
|
when '/login' then @telegram.set_authentication_phone_number(args[0]).then{|_| @session[:login] = args[0]} unless @session[:login] # sign in
|
||||||
|
when '/logout' then @telegram.log_out().then{|_| @cache[:chats].each_key do |chat| @xmpp.send_presence(@jid, chat, :unsubscribed); @session[:login] = nil end } # sign out
|
||||||
# user information
|
when '/code' then @telegram.check_authentication_code(args[0]) # check auth code
|
||||||
if @cache[:users].key? chat_id then # its an user
|
when '/password' then @telegram.check_authentication_password(args[0]) # chech auth password
|
||||||
firstname = @cache[:users][chat_id].first_name # <N/GIVEN>
|
when '/setusername' then @telegram.set_username(args[0] || '') # set @username
|
||||||
lastname = @cache[:users][chat_id].last_name # <N/FAMILY>
|
when '/setname' then @telegram.set_name(args[0] || '', args[1] || '') # set My Name
|
||||||
username = @cache[:users][chat_id].username # <NICKNAME>
|
when '/setbio' then @telegram.set_bio(args[0] || '') # set About
|
||||||
phone = @cache[:users][chat_id].phone_number # <TEL>
|
when '/setpassword' then @telegram.set_password((args[1] ? args[0] : ''), args[1]) # set password
|
||||||
bio = @cache[:chats][chat_id].client_data # <DESC>
|
else @xmpp.send_message(@jid, nil, ::HELP_GATE_CMD)
|
||||||
userpic = self.format_file(@cache[:photos][chat_id], 'image.jpg', true) if @cache[:photos].include? chat_id
|
end
|
||||||
userpic = (File.exist? userpic.to_s) ? Base64.encode64(IO.binread(userpic)) : nil
|
return true # stop executing
|
||||||
|
else # chat commands
|
||||||
|
case cmd
|
||||||
|
when '/d' then @telegram.delete_messages(chat.id, @telegram.search_chat_messages(chat.id, 0, args[0]||1, sender_user_id: @me.id, filter: TD::Types::SearchMessagesFilter::Empty.new).value.messages.map(&:id), true) # delete last message(s)
|
||||||
|
when '/s' then @telegram.search_chat_messages(chat.id, 0, 1, sender_user_id: @me.id, filter: TD::Types::SearchMessagesFilter::Empty.new).value.messages.each do |msg| self.process_outgoing_message(chat.id, msg.content.text.text.to_s.gsub(Regexp.new(args[0]),args[1..-1].join(' ')), msg.id) end # edit last message
|
||||||
|
when '/add' then @telegram.search_public_chat(args[0]).then{|chat| @xmpp.send_presence(@jid, chat.id, :subscribe)}.wait # add @contact
|
||||||
|
when '/join' then @telegram.join_chat_by_invite_link(args[0]) # join https://t.me/publichat
|
||||||
|
when '/supergroup' then @telegram.create_new_supergroup_chat(args[0], args[1..-1].join(' '), is_channel: false) # create new supergroup
|
||||||
|
when '/channel' then @telegram.create_new_supergroup_chat(args[0], args[1..-1].join(' '), is_channel: true) # create new channel
|
||||||
|
when '/secret' then @telegram.create_new_secret_chat(chat.id) if user # create secret chat with current user
|
||||||
|
when '/group' then @telegram.create_new_basic_group_chat(chat.id, args[0]) if user # create group chat with current user
|
||||||
|
when '/block' then @telegram.block_user(chat.id) if user # blacklists current user
|
||||||
|
when '/unblock' then @telegram.unblock_user(chat.id) if user # unblacklists current user
|
||||||
|
when '/invite' then @telegram.add_chat_member(chat.id, (args[0].to_i == 0 ? @telegram.search_public_chat(args[0]).value.id : args[0].to_i)) if chat.id < 0 # invite @username to current groupchat
|
||||||
|
when '/kick' then @telegram.set_chat_member_status(chat.id, (args[0].to_i == 0 ? @telegram.search_public_chat(args[0]).value.id : args[0].to_i)) if chat.id < 0 # kick @username from current group chat
|
||||||
|
when '/ban' then @telegram.set_chat_member_status(chat.id, (args[0].to_i == 0 ? @telegram.search_public_chat(args[0]).value.id : args[0].to_i), TD::Types::ChatMemberStatus::Banned(banned_until_date: (args[1].to_i > 0 ? Time.now.getutc.to_i+(args[1].to_i*3600) : 0))) if chat.id < 0 # ban @username from current chat [for N hours]
|
||||||
|
when '/leave' then @telegram.leave_chat(chat.id).then{|result| @xmpp.send_presence(@jid, chat_id, :unsubscribed)} if chat.type.instance_of? TD::Types::ChatType::Supergroup or chat.type.instance_of? TD::Types::ChatType::BasicGroup # leave current chat
|
||||||
|
when '/close' then @telegram.close_secret_chat(chat.type.secret_chat_id).then{|result| @xmpp.send_presence(@jid, chat_id, :unsubscribed)} if chat.type.instance_of? TD::Types::ChatType::Secret # close secret chat
|
||||||
|
when '/delete' then @telegram.delete_chat_history(chat.id, true).then{|result| @xmpp.send_presence(@jid, chat_id, :unsubscribed)} # delete current chat
|
||||||
|
when '/search' then @telegram.search_chat_messages(chat.id, 0, args[1]||10, query: args[0]||nil, filter: TD::Types::SearchMessagesFilter::Empty.new).value.messages.reverse.each do |msg| @xmpp.send_message(@jid, chat_id, self.format_message(nil,nil,nil,msg)) end # search messages within current chat
|
||||||
|
when '/history' then @telegram.get_chat_history(chat.id, 0, 0, args[0]||10).value.messages.reverse.each do |msg| @xmpp.send_message(@jid, chat_id, self.format_message(nil,nil,msg)) end # get latest entries from history
|
||||||
|
when '/help' then @xmpp.send_message(@jid, chat_id, ::HELP_CHAT_CMD)
|
||||||
|
else return # continue executing
|
||||||
|
end
|
||||||
|
return true # stop executing
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# ..
|
#########################################################################
|
||||||
return title, username, firstname, lastname, phone, bio, userpic
|
# formatting functions #################################################
|
||||||
|
#########################################################################
|
||||||
|
|
||||||
|
def format_contact(chat_id)
|
||||||
|
return if not chat_id or chat_id == 0
|
||||||
|
chat, user = self.get_contact(chat_id)
|
||||||
|
str = chat_id
|
||||||
|
str = "%s (%s)" % [chat.title, chat.id] if chat
|
||||||
|
str = "%s %s (%s)" % [user.first_name, user.last_name, (user.username.empty?) ? user.id : user.username] if user
|
||||||
|
str = str.gsub(' ', ' ')
|
||||||
|
return str
|
||||||
end
|
end
|
||||||
|
|
||||||
# resolve id by @username (or just return id)
|
def format_content(file, fname)
|
||||||
def resolve_username(username)
|
str = "%s (%d kbytes) | %s/%s%s" % [fname, file.size/1024, @@config[:content][:link], Digest::SHA256.hexdigest(file.remote.id), File.extname(fname).to_s]
|
||||||
resolved = username
|
return str
|
||||||
if username[0] == '@' then @client.search_public_chat(username[1..-1]).then {|chat| resolved = '@' + chat.id.to_s}.wait end
|
|
||||||
if username[0..3] == 'http' or username[0..3] == 't.me' then @client.join_chat_by_invite_link(username) end
|
|
||||||
return resolved
|
|
||||||
end
|
end
|
||||||
|
|
||||||
###########################################
|
def format_message(chat_id, message_id, preview=false, message=nil)
|
||||||
## Format functions #######################
|
message ||= @telegram.get_message(chat_id, message_id).value
|
||||||
###########################################
|
return unless message
|
||||||
# format tg user name #
|
str = "%s | %s | " % [message.id, self.format_contact(message.sender_user_id)]
|
||||||
def format_contact(id, show_id = false, resolve = true)
|
str += DateTime.strptime((message.date+Time.now.getlocal(@session[:timezone]).utc_offset).to_s,'%s').strftime("%d %b %Y %H:%M:%S | ") unless preview
|
||||||
fmt = ''
|
str += (not preview or message.content.text.text.lines.count <= 1) ? message.content.text.text : message.content.text.text.lines.first
|
||||||
if id < 0 then # its chat
|
return str
|
||||||
fmt = (@cache[:chats].key? id) ? "%s" % @cache[:chats][id].title : "%s" % id
|
|
||||||
elsif id > 0 then # its user
|
|
||||||
self.process_user_info(id) if not @cache[:users].key? id and resolve
|
|
||||||
user = @cache[:users][id] if @cache[:users].key? id
|
|
||||||
fmt += user.first_name if user and user.first_name != ''
|
|
||||||
fmt += " " + user.last_name if user and user.last_name != ''
|
|
||||||
fmt += " (@%s)" % user.username if user and user.username != ''
|
|
||||||
fmt += " (%s)" % id if (user and user.username == '') or show_id
|
|
||||||
else
|
|
||||||
fmt = "unknown (%s)" % id
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return fmt
|
|
||||||
end
|
|
||||||
|
|
||||||
# format reply#
|
|
||||||
def format_message(chat_id, message_id, full = true)
|
|
||||||
text = ''
|
|
||||||
@client.get_message(chat_id, message_id).then { |message| text = message.content.text.text }.wait
|
|
||||||
return (not full) ? "%s >> %s.." % [message_id, text.split("\n")[0]] : "%s | %s " % [message_id, text]
|
|
||||||
end
|
|
||||||
|
|
||||||
def format_file(file, filename, local = false)
|
|
||||||
if local then return "%s/%s%s" % [@@content_path, Digest::SHA256.hexdigest(file.remote.id), File.extname(filename)] end
|
|
||||||
return "%s (%d kbytes) | %s/%s%s" % [filename, file.size/1024, @@content_link, Digest::SHA256.hexdigest(file.remote.id), File.extname(filename).to_s]
|
|
||||||
end
|
|
||||||
|
|
||||||
# state functions #
|
|
||||||
def online?() @online end
|
|
||||||
def tz_set?() return @timezone != '-00:00' end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,266 +1,107 @@
|
||||||
#############################
|
|
||||||
### Some constants #########
|
|
||||||
::HELP_MESSAGE = 'Unknown command.
|
|
||||||
|
|
||||||
/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
|
|
||||||
/reconnect — Reconnect to Telegram network
|
|
||||||
/logout — Disconnect from Telegram network and forget session
|
|
||||||
|
|
||||||
/info — Show information and usage statistics of this instance (only for JIDs specified as administrators)
|
|
||||||
/restart — Restart this instance (only for JIDs specified as administrators)
|
|
||||||
'
|
|
||||||
|
|
||||||
#############################
|
|
||||||
|
|
||||||
#############################
|
|
||||||
## XMPP Transport Class #####
|
|
||||||
#############################
|
|
||||||
|
|
||||||
include Jabber::Discovery
|
|
||||||
include Jabber::Dataforms
|
|
||||||
|
|
||||||
class XMPPComponent
|
class XMPPComponent
|
||||||
|
|
||||||
# init class and set logger #
|
## initialize zhabogram
|
||||||
def initialize(params)
|
def initialize(**config)
|
||||||
@@loglevel = params['loglevel'] || Logger::DEBUG
|
@sessions = {} # sessions list
|
||||||
@logger = Logger.new(STDOUT); @logger.level = @@loglevel; @logger.progname = '[XMPPComponent]'
|
@queue = {} # presence queue
|
||||||
@config = { host: params["host"] || 'localhost', port: params["port"] || 8899, jid: params["jid"] || 'tlgrm.localhost', secret: params['password'] || '', admins: params['admins'] || [], debug: params['debug'] } # default config
|
@logger = Logger.new(STDOUT, level: config[:loglevel], progname: 'XMPPComponent')
|
||||||
@sessions = {}
|
@config = {host: config[:host], port: config[:port], jid: config[:jid], password: config[:password], debug: config[:debug]}
|
||||||
@presence_que = {}
|
@db = YAML::Store.new(config[:db])
|
||||||
@db = params['db_path'] || 'users.dat'
|
@db.transaction do @db[:sessions] ||= {} end
|
||||||
self.load_db()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# load sessions from db #
|
## connect to XMPP server
|
||||||
def load_db()
|
def connect()
|
||||||
@logger.info "Loading sessions..."
|
|
||||||
File.open( @db, 'r' ) {|f| YAML.load(f).each do |jid,login| @sessions[jid] = TelegramClient.new(self, jid, login) end }
|
|
||||||
end
|
|
||||||
|
|
||||||
# store session to db #
|
|
||||||
def save_db()
|
|
||||||
@logger.info "Saving sessions..."
|
|
||||||
sessions_store = []
|
|
||||||
@sessions.each do |jid,session| store << {jid: jid, login: session.login} end
|
|
||||||
File.open( @db, 'w' ) {|f| f.write(YAML.dump(sessions_store)) }
|
|
||||||
end
|
|
||||||
|
|
||||||
# connecting to XMPP server #
|
|
||||||
def connect() # :jid => transport_jid, :host => xmpp_server, :port => xmpp_component_port, :secret => xmpp_component_secret
|
|
||||||
begin
|
|
||||||
Jabber::debug = @config[:debug]
|
Jabber::debug = @config[:debug]
|
||||||
|
begin
|
||||||
# component
|
@component = Jabber::Component.new(@config[:jid]) # init XMPP component
|
||||||
@component = Jabber::Component.new( @config[:jid] )
|
@component.connect(@config[:host], @config[:port]) # connect to XMPP server
|
||||||
@component.connect( @config[:host], @config[:port] )
|
@component.auth(@config[:password]) # authorize
|
||||||
@component.auth( @config[:secret] )
|
@component.on_exception do |error,| @logger.error(error) and self.connect() end # exception handler
|
||||||
@component.add_message_callback do |msg| msg.first_element_text('body') ? self.message_handler(msg) : nil end
|
@component.add_presence_callback do |stanza| self.handle_subscription(stanza) if stanza.type == :subscribe end # presence handler
|
||||||
@component.add_presence_callback do |presence| self.presence_handler(presence) end
|
@component.add_presence_callback do |stanza| self.handle_presence(stanza) if stanza.to == @component.jid end # presence handler
|
||||||
@component.add_iq_callback do |iq| self.iq_handler(iq) end
|
@component.add_message_callback do |stanza| self.handle_message(stanza) if stanza.type != :error and stanza.first_element_text('body') end # messages handler
|
||||||
@component.on_exception do |exception, stream, state| self.survive(exception, stream, state) end
|
@component.add_iq_callback do |stanza| self.handle_vcard_iq(stanza) if stanza.type == :get and stanza.vcard end # vcards handler
|
||||||
@logger.info "Connection to XMPP server established!"
|
@logger.warn 'Connected to XMPP server'
|
||||||
|
@db.transaction do @db[:sessions].each do |jid, session| @sessions[jid] = TelegramClient.new(self, jid, session) end end # probe all known sessions
|
||||||
# disco
|
Thread.new { while @component.is_connected? do sleep 60; @queue.delete_if {|_, presence| @component.send(presence) || true } end } # status updater thread
|
||||||
@disco = Jabber::Discovery::Responder.new(@component)
|
Thread.stop() # stop main thread loop
|
||||||
@disco.identities = [ Identity.new('gateway', 'Telegram Gateway', 'telegram') ]
|
rescue Exception => error
|
||||||
@disco.add_features(['http://jabber.org/protocol/disco','jabber:iq:register'])
|
@logger.error 'Disconnecting.. %s' % error.to_s
|
||||||
|
@sessions.each_value do |session| session.disconnect() end # close all sessions
|
||||||
# janbber::iq::register
|
@db.transaction do @sessions.each do |jid, session| @db[:sessions][jid] = session.session end end # save sessions
|
||||||
@iq_register = Jabber::Register::Responder.new(@component)
|
@component.on_exception do |exception,| end # disable exception handling
|
||||||
@iq_register.instructions = 'Please enter your Telegram login'
|
@component.close() # close stream
|
||||||
@iq_register.add_field(:login, true) do |jid, login| self.process_command(jid, '/login %s' % login) end
|
exit -1 # bye
|
||||||
|
|
||||||
# jabber::iq::gateway
|
|
||||||
@iq_gateway = Jabber::Gateway::Responder.new(@component) do |iq, query| (@sessions.key? iq.from.bare.to_s and @sessions[iq.from.bare.to_s].online?) ? @sessions[iq.from.bare.to_s].resolve_username(query).to_s + '@' + @component.jid.to_s : '' end
|
|
||||||
@iq_gateway.description = "Specify @username / ID / https://t.me/link"
|
|
||||||
@iq_gateway.prompt = "Telegram contact"
|
|
||||||
|
|
||||||
@logger.info 'Loaded %s sessions from database.' % @sessions.count
|
|
||||||
@sessions.each do |jid, session| self.presence(jid, nil, :subscribe) end
|
|
||||||
Thread.new { while @component.is_connected? do @presence_que.each_value { |p| @component.send(p) }; @presence_que.clear; sleep(60); end } # presence updater thread
|
|
||||||
Thread.stop()
|
|
||||||
rescue Interrupt, SignalException
|
|
||||||
@logger.error 'Interrupted!'
|
|
||||||
@component.on_exception do |exception,| end
|
|
||||||
self.disconnect()
|
|
||||||
return -11
|
|
||||||
rescue Exception => e
|
|
||||||
@logger.error 'Connection failed: %s' % e
|
|
||||||
self.save_db()
|
|
||||||
exit -8
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# transport shutdown #
|
############################################################
|
||||||
def disconnect()
|
#### Callback handlers (from XMPP) #########################
|
||||||
@logger.info "Closing connections..."
|
|
||||||
@sessions.each do |jid, session| @sessions[jid].disconnect(); self.presence(jid, nil, :unavailable) end
|
def handle_subscription(presence)
|
||||||
@component.close()
|
@logger.warn 'Subscription request from %s to %s' % [presence.from, presence.to]
|
||||||
|
@logger.debug presence.to_s
|
||||||
|
answer = presence.answer(false)
|
||||||
|
answer.type = :subscribed
|
||||||
|
@component.send(answer)
|
||||||
end
|
end
|
||||||
|
|
||||||
# vse umrut a ya ostanus'... #
|
def handle_presence(presence)
|
||||||
def survive(exception, stream, state)
|
@logger.warn 'Presence (%s) from %s to %s' % [presence.type || 'online', presence.from, presence.to]
|
||||||
@logger.error "Stream error on :%s (%s)" % [state.to_s, exception.to_s]
|
@logger.debug presence.to_s
|
||||||
@logger.info "Trying to revive stream.."
|
@sessions[presence.from.bare.to_s] = TelegramClient.new(self, presence.from.bare.to_s) unless @sessions.key? presence.from.bare.to_s # create session
|
||||||
self.connect()
|
@sessions[presence.from.bare.to_s] = nil if presence.type == :unsubscribed # destroy session
|
||||||
|
@sessions[presence.from.bare.to_s].disconnect() if presence.type == :unavailable or presence.type == :error # go offline
|
||||||
|
@sessions[presence.from.bare.to_s].connect() if not presence.type # go online
|
||||||
end
|
end
|
||||||
|
|
||||||
# message to users #
|
def handle_message(message)
|
||||||
def message(to, from = nil, body = '')
|
@logger.warn 'Message from %s to %s' % [message.from, message.to]
|
||||||
@logger.info "Sending message from <%s> to <%s>" % [from || @component.jid, to]
|
@logger.debug message.to_s
|
||||||
msg = Jabber::Message.new
|
@sessions[message.from.bare.to_s].process_outgoing_message(message.to.to_s.split('@').first.to_i, message.first_element_text('body')) if @sessions.key? message.from.bare.to_s
|
||||||
msg.from = (from) ? "%s@%s" % [from, @component.jid.to_s] : @component.jid
|
|
||||||
msg.to = to
|
|
||||||
msg.body = body
|
|
||||||
msg.type = :chat
|
|
||||||
@logger.debug msg.to_s
|
|
||||||
@component.send(msg)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# presence update #
|
def handle_vcard_iq(iq)
|
||||||
def presence(to, from = nil, type = nil, show = nil, status = nil, nickname = nil, photo = nil, immediately = true)
|
@logger.warn 'VCard request from %s for %s' % [iq.from, iq.to]
|
||||||
@logger.debug "Presence update request from %s (immed = %s).." % [from.to_s, immediately]
|
chat, user = @sessions[iq.from.bare.to_s].get_contact(iq.to.to_s.split('@').first.to_i) if @sessions.key? iq.from.bare.to_s
|
||||||
req = Jabber::Presence.new()
|
|
||||||
req.from = from.nil? ? @component.jid : "%s@%s" % [from, @component.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.to_s
|
|
||||||
(immediately) ? @component.send(req) : @presence_que.store(req.from.to_s+req.to.to_s, req)
|
|
||||||
# @component.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 = @component.jid
|
|
||||||
#iq.id = 'time_req_1'
|
|
||||||
#iq.add_element("time", {"xmlns" => "urn:xmpp:time"})
|
|
||||||
#@logger.debug iq.to_s
|
|
||||||
#@component.send(iq)
|
|
||||||
#end
|
|
||||||
|
|
||||||
#############################
|
|
||||||
#### 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.to_s, msg.to.to_s]
|
|
||||||
@logger.debug msg.to_s
|
|
||||||
if msg.to == @component.jid then self.process_command(msg.from, msg.first_element_text('body') ); return; end # treat message as internal command if received as transport jid
|
|
||||||
if @sessions.key? msg.from.bare.to_s then
|
|
||||||
# self.request_tz(msg.from) if not @sessions[msg.from.bare.to_s].tz_set?
|
|
||||||
return @sessions[msg.from.bare.to_s].process_outgoing_msg(msg.to.to_s.split('@')[0].to_i, msg.first_element_text('body'))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# new presence to XMPP component #
|
|
||||||
def presence_handler(prsnc)
|
|
||||||
@logger.debug "Received presence :%s from <%s> to <%s>" % [prsnc.type.to_s, prsnc.from.to_s, prsnc.to.to_s]
|
|
||||||
@logger.debug(prsnc.to_s)
|
|
||||||
if prsnc.type == :subscribe then reply = prsnc.answer(false); reply.type = :subscribed; @component.send(reply); end # send "subscribed" reply to "subscribe" presence
|
|
||||||
if prsnc.to == @component.jid and @sessions.key? prsnc.from.bare.to_s and prsnc.type == :unavailable then @sessions[prsnc.from.bare.to_s].disconnect(); self.presence(prsnc.from, nil, :subscribe) ; return; end # go offline when received offline presence from jabber user
|
|
||||||
if prsnc.to == @component.jid and @sessions.key? prsnc.from.bare.to_s then
|
|
||||||
# self.request_tz(prsnc.from);
|
|
||||||
@sessions[prsnc.from.bare.to_s].connect() || @sessions[prsnc.from.bare.to_s].sync_status()
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# new iq (vcard/tz) request to XMPP component #
|
|
||||||
def iq_handler(iq)
|
|
||||||
@logger.debug "Received iq :%s from <%s> to <%s>" % [iq.type.to_s, iq.from.to_s, iq.to.to_s]
|
|
||||||
@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 "VCard request for <%s>" % iq.to.to_s
|
|
||||||
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 = Jabber::Vcard::IqVcard.new()
|
||||||
vcard["FN"] = fn
|
vcard["FN"] = chat.title if chat
|
||||||
vcard["NICKNAME"] = nickname if nickname
|
vcard["NICKNAME"], vcard["N/GIVEN"], vcard["N/FAMILY"], vcard["TEL/NUMBER"] = user.username, user.first_name, user.last_name, user.phone_number if user
|
||||||
vcard["URL"] = "https://t.me/%s" % nickname if nickname
|
vcard["PHOTO/TYPE"], vcard["PHOTO/BINVAL"] = 'image/jpeg', Base64.encode64(IO.binread(chat.photo.small.local.path)) if chat and chat.photo and File.exist? chat.photo.small.local.path
|
||||||
vcard["N/GIVEN"] = given if given
|
answer = iq.answer
|
||||||
vcard["N/FAMILY"] = family if family
|
answer.type = :result
|
||||||
vcard["DESC"] = desc if desc
|
answer.elements['vCard'] = vcard
|
||||||
vcard["PHOTO/TYPE"] = 'image/jpeg' if photo
|
@logger.debug answer.to_s
|
||||||
vcard["PHOTO/BINVAL"] = photo if photo
|
@component.send(answer)
|
||||||
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
|
|
||||||
@component.send(reply)
|
|
||||||
# time response #
|
|
||||||
elsif iq.type == :result and iq.elements["time"] and @sessions.key? iq.from.bare.to_s then
|
|
||||||
@logger.debug "Timezone response from <%s>" % iq.from.to_s
|
|
||||||
timezone = iq.elements["time"].elements["tzo"].text
|
|
||||||
@sessions[iq.from.bare.to_s].timezone = timezone
|
|
||||||
elsif iq.type == :get then
|
|
||||||
@logger.debug "Unknown iq type <%s>" % iq.from.to_s
|
|
||||||
reply = iq.answer
|
|
||||||
reply.type = :error
|
|
||||||
end
|
|
||||||
@component.send(reply)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
#############################
|
############################################################
|
||||||
#### Command handlers #####
|
#### XMPP gateway functions (to XMPP) #####################
|
||||||
#############################
|
|
||||||
|
|
||||||
# process internal /command #
|
def send_message(to, from=nil, body='')
|
||||||
def process_command(from, body)
|
@logger.warn "Got message from %s to %s" % [from||@component.jid, to]
|
||||||
case body.split[0] # /command argument = [command, argument]
|
message = Jabber::Message.new
|
||||||
when '/login' # create new session
|
message.from = (from) ? "%s@%s" % [from.to_s, @component.jid.to_s] : @component.jid
|
||||||
@sessions[from.bare.to_s] = TelegramClient.new(self, from.bare.to_s, body.split[1]) if not (@sessions.key? from.bare.to_s and @sessions[from.bare.to_s].online?)
|
message.to = to
|
||||||
@sessions[from.bare.to_s].connect()
|
message.body = body
|
||||||
# self.request_tz(from)
|
message.type = :chat
|
||||||
self.save_db()
|
@logger.debug message.to_s
|
||||||
when '/code', '/password' # pass auth data to telegram
|
@component.send(message)
|
||||||
@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].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 '/reconnect' # reconnect
|
|
||||||
@sessions[from.bare.to_s].disconnect() if @sessions.key? from.bare.to_s
|
|
||||||
sleep(0.1)
|
|
||||||
@sessions[from.bare.to_s].connect() if @sessions.key? from.bare.to_s
|
|
||||||
when '/logout' # go offline and destroy session
|
|
||||||
@sessions[from.bare.to_s].disconnect(true) if @sessions.key? from.bare.to_s
|
|
||||||
self.save_db()
|
|
||||||
@sessions.delete(from.bare.to_s)
|
|
||||||
when '/info' # show some debug information
|
|
||||||
return if not @config[:admins].include? from.bare.to_s
|
|
||||||
response = "Information about this instance: \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 += "\n\nSessions: %d online | %d total \n" % [ @sessions.inject(0){ |cnt, (jid, sess)| cnt = (sess.online?) ? cnt + 1 : cnt }, @sessions.count]
|
|
||||||
@sessions.each do |jid, session| response += "JID: %s | Login: %s | Status: %s (%s) | %s\n" % [jid, session.login, (session.online == true) ? 'Online' : 'Offline', session.auth_state, (session.me) ? session.format_contact(session.me.id) : 'Unknown' ] end
|
|
||||||
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(1)
|
|
||||||
Process.kill("INT", Process.pid)
|
|
||||||
else # unknown command -- display help #
|
|
||||||
self.message(from.bare, nil, ::HELP_MESSAGE)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return true
|
def send_presence(to, from=nil, type=nil, show=nil, status=nil, nickname=nil, photo=nil, immed=true)
|
||||||
|
@logger.info "Got presence :%s from %s to %s" % [type, from||@component.jid, to]
|
||||||
|
presence = Jabber::Presence.new()
|
||||||
|
presence.from = from.nil? ? @component.jid : "%s@%s" % [from.to_s, @component.jid.to_s] # presence <from>
|
||||||
|
presence.to = to # presence <to>
|
||||||
|
presence.type = type unless type.nil? # pres. type
|
||||||
|
presence.show = show unless show.nil? # presence <show>
|
||||||
|
presence.status = status unless status.nil? # presence message
|
||||||
|
presence.add_element('nick', {'xmlns' => 'http://jabber.org/protocol/nick'} ).add_text(nickname) unless nickname.nil? # nickname
|
||||||
|
presence.add_element('x', {'xmlns' => 'vcard-temp:x:update'} ).add_element("photo").add_text(photo) unless photo.nil? # nickname
|
||||||
|
@logger.debug presence.to_s
|
||||||
|
(immed) ? @component.send(presence) : @queue.store(presence.from.to_s+presence.to.to_s, presence)
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,68 +0,0 @@
|
||||||
module Jabber
|
|
||||||
module Gateway
|
|
||||||
|
|
||||||
NS_GATEWAY = 'jabber:iq:gateway'
|
|
||||||
|
|
||||||
class Responder
|
|
||||||
attr_accessor :description
|
|
||||||
attr_accessor :prompt
|
|
||||||
|
|
||||||
def initialize(stream, &func)
|
|
||||||
@stream = stream
|
|
||||||
@func = func
|
|
||||||
|
|
||||||
@stream.add_iq_callback() do |iq|
|
|
||||||
if iq.query.kind_of?(IqQueryGateway)
|
|
||||||
if iq.type == :get
|
|
||||||
# Client is requesting fields
|
|
||||||
answer = iq.answer(false)
|
|
||||||
answer.type = :result
|
|
||||||
query = answer.add(IqQueryGateway.new)
|
|
||||||
query.desc = @description || ''
|
|
||||||
query.prompt = @prompt || ''
|
|
||||||
@stream.send(answer)
|
|
||||||
elsif iq.type == :set
|
|
||||||
# Client is requesting full JID
|
|
||||||
query = iq.query.prompt
|
|
||||||
jid = @func.call(iq, query)
|
|
||||||
answer = iq.answer(false)
|
|
||||||
answer.type = :result
|
|
||||||
query = answer.add(IqQueryGateway.new)
|
|
||||||
query.jid = jid
|
|
||||||
@stream.send(answer)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class IqQueryGateway < IqQuery
|
|
||||||
name_xmlns 'query', Jabber::Gateway::NS_GATEWAY
|
|
||||||
|
|
||||||
def desc
|
|
||||||
first_element_text('desc')
|
|
||||||
end
|
|
||||||
|
|
||||||
def desc=(new_desc)
|
|
||||||
replace_element_text('desc', new_desc)
|
|
||||||
end
|
|
||||||
|
|
||||||
def prompt
|
|
||||||
first_element_text('prompt')
|
|
||||||
end
|
|
||||||
|
|
||||||
def prompt=(new_prompt)
|
|
||||||
replace_element_text('prompt', new_prompt)
|
|
||||||
end
|
|
||||||
|
|
||||||
def jid
|
|
||||||
first_element_text('jid')
|
|
||||||
end
|
|
||||||
|
|
||||||
def jid=(new_prompt)
|
|
||||||
replace_element_text('jid', new_prompt)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,70 +0,0 @@
|
||||||
module Jabber
|
|
||||||
|
|
||||||
module Register
|
|
||||||
|
|
||||||
NS_REGISTER = 'jabber:iq:register'
|
|
||||||
|
|
||||||
class Responder
|
|
||||||
attr_accessor :instructions
|
|
||||||
|
|
||||||
def initialize(stream)
|
|
||||||
@stream = stream
|
|
||||||
@fields = []
|
|
||||||
@registered_callbacks = []
|
|
||||||
|
|
||||||
@stream.add_iq_callback() do |iq|
|
|
||||||
if iq.query.kind_of?(IqQueryRegister) then
|
|
||||||
if iq.type == :get then # Registration request
|
|
||||||
answer = iq.answer(false)
|
|
||||||
answer.type = :result
|
|
||||||
query = answer.add(IqQueryRegister.new)
|
|
||||||
query.add(Field.new(:instructions, @instructions)) unless instructions.nil?
|
|
||||||
@fields.each do |field| query.add(Field.new(field[0])) end
|
|
||||||
@stream.send(answer)
|
|
||||||
elsif iq.type == :set then # Registration response
|
|
||||||
iq.query.each do |field|
|
|
||||||
validator = @fields.assoc(field.name.to_sym)[2]
|
|
||||||
if !validator.call(iq.from, field.text) then
|
|
||||||
puts "- - n0t acceptable here - -"
|
|
||||||
answer = iq.answer(true)
|
|
||||||
answer.type = :error
|
|
||||||
answer.add(Jabber::ErrorResponse.new('not-acceptable'))
|
|
||||||
@stream.send(answer)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# let them know that all looks good!
|
|
||||||
answer = iq.answer(false)
|
|
||||||
answer.type = :result
|
|
||||||
@stream.send(answer)
|
|
||||||
|
|
||||||
# Fire off callbacks
|
|
||||||
@registered_callbacks.each do |cb|
|
|
||||||
cb.call(iq.from)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_field(name, required, &validator)
|
|
||||||
@fields << [ name, required, validator ]
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_registered_callback(&cb)
|
|
||||||
@registered_callbacks << cb
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class IqQueryRegister < IqQuery
|
|
||||||
name_xmlns 'query', Jabber::Register::NS_REGISTER
|
|
||||||
end
|
|
||||||
|
|
||||||
class Field < REXML::Element
|
|
||||||
def initialize(name, value=nil)
|
|
||||||
super(name.to_s)
|
|
||||||
self.text = value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
11
zhabogram.rb
11
zhabogram.rb
|
@ -1,19 +1,16 @@
|
||||||
# Some very important libraries'
|
require 'set'
|
||||||
require 'yaml'
|
require 'yaml'
|
||||||
|
require 'yaml/store'
|
||||||
require 'logger'
|
require 'logger'
|
||||||
require 'xmpp4r'
|
require 'xmpp4r'
|
||||||
require 'xmpp4r/discovery'
|
|
||||||
require 'digest'
|
require 'digest'
|
||||||
require 'base64'
|
require 'base64'
|
||||||
require 'fileutils'
|
require 'fileutils'
|
||||||
require 'tdlib-ruby'
|
require 'tdlib-ruby'
|
||||||
require_relative 'inc/telegramclient'
|
require_relative 'inc/telegramclient'
|
||||||
require_relative 'inc/xmppregister'
|
|
||||||
require_relative 'inc/xmppgateway'
|
|
||||||
require_relative 'inc/xmppcomponent'
|
require_relative 'inc/xmppcomponent'
|
||||||
|
|
||||||
# configuration
|
|
||||||
Config = YAML.load_file(File.dirname(__FILE__) + '/config.yml')
|
Config = YAML.load_file(File.dirname(__FILE__) + '/config.yml')
|
||||||
TelegramClient.configure(Config['telegram']) # configure tdlib
|
TelegramClient.configure Config[:telegram]
|
||||||
Zhabogram = XMPPComponent.new(Config['xmpp']) # spawn zhabogram
|
Zhabogram = XMPPComponent.new Config[:xmpp]
|
||||||
Zhabogram.connect()
|
Zhabogram.connect()
|
||||||
|
|
Reference in a new issue