class TelegramClient

    attr_reader :jid, :login, :online, :auth_state, :me
    attr_accessor :timezone

    # tdlib configuration, shared within all instances #
    def self.configure(params) 
        @@loglevel = params['loglevel'] || Logger::DEBUG
        @@content_path = params['content_path'] || '/tmp'
        @@content_link = params['content_link'] || 'https://localhost/tg_media'
        @@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'] || '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
    
    # instance initialization #
    def initialize(xmpp, jid, login)
        return if not @@loglevel # call .configure() first    
        @logger = Logger.new(STDOUT); @logger.level = @@loglevel; @logger.progname = '[TelegramClient: %s/%s]' % [jid, login] # create logger
        @logger.info 'Starting Telegram client..'
        @xmpp = xmpp # XMPP stream 
        @jid = jid # user JID 
        @timezone = '-00:00' # default timezone is UTC
        @login = login # telegram login 
        @me = nil # self telegram profile
        @online = nil # we do not know
        @auth_state = 'nil' # too.
        @cache = {chats: {}, users: {}, photos: {}} # cache 
    end
    
    # initialize and connect telegram client #
    def connect()
        return if @client and @client.ready? 
        @logger.info 'Connecting to Telegram network..' 
        @client = TD::Client.new(database_directory: 'sessions/' + @jid, files_directory: 'sessions/' + @jid + '/files/') # create telegram client instance
        @client.on(TD::Types::Update::AuthorizationState) do |update|  self.auth_handler(update) end # register auth update handler 
        @client.on(TD::Types::Update::File) do |update| self.file_handler(update) end # register file handler 
        @client.on(TD::Types::Update::NewMessage) do |update|  self.message_handler(update) end # register new message update handler 
        @client.on(TD::Types::Update::MessageContent) do |update|  self.message_edited_handler(update) end # register msg edited handler
        @client.on(TD::Types::Update::DeleteMessages) do |update|  self.message_deleted_handler(update) end # register msg del handler
        @client.on(TD::Types::Update::NewChat) do |update|  self.new_chat_handler(update) end # register new chat handler 
        @client.on(TD::Types::Update::User) do |update|  self.user_handler(update) end # new user update? 
        @client.on(TD::Types::Update::UserStatus) do |update|  self.status_update_handler(update) end # register status handler 
        @client.connect() 
    end

    # disconnect and destroy telegram client #
    def disconnect(logout = false)
        return if not @client 
        @logger.info 'Disconnecting..'
        @cache[:chats].each_key do |chat_id| @xmpp.presence(@jid, chat_id.to_s, :unavailable) end # send offline presences
        (logout) ? @client.log_out : @client.dispose # logout if needed  
        @client = nil
        @online = false
    end
    
    
    ###########################################
    ## Callback handlers #####################
    ###########################################

    # authorization handler #
    def auth_handler(update)
        @logger.debug 'Authorization state changed: %s' % update.authorization_state
        @auth_state = update.authorization_state.class.name

        case update.authorization_state
        # auth stage 0: specify login #
        when TD::Types::AuthorizationState::WaitPhoneNumber
            @logger.info 'Logging in..'
            @client.set_authentication_phone_number(@login)
         # auth stage 1: wait for authorization code #    
        when TD::Types::AuthorizationState::WaitCode
            @logger.info 'Waiting for authorization code..'
            @xmpp.message(@jid, nil, 'Please, enter authorization code via /code 12345')
        # auth stage 2: wait for 2fa passphrase #
        when TD::Types::AuthorizationState::WaitPassword
            @logger.info 'Waiting for 2FA password..'
            @xmpp.message(@jid, nil, 'Please, enter 2FA passphrase via /password 12345')
        # authorization successful -- indicate that client is online and retrieve contact list  #
        when TD::Types::AuthorizationState::Ready 
            @logger.info 'Authorization successful!'
            @client.get_me().then { |user| @me = user }.wait 
            @client.get_chats(limit=9999) 
            @logger.info "Contact list updating finished"
            @xmpp.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()
        end        
    end

    # message from telegram network handler # 
    def message_handler(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

        @logger.debug 'Got NewMessage update'
        @logger.debug update.message.to_json
        @logger.info 'New message from Telegram chat %s' % 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, content.sticker.emoji + '.webp']
            when TD::Types::MessageContent::VoiceNote then [content.voice_note.voice, 'voice message %i s.' % content.voice_note.duration + '.oga']
            when TD::Types::MessageContent::Animation then [content.animation.animation, content.animation.file_name + '.mp4' ]
            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, content.video.file_name] 
            when TD::Types::MessageContent::Document then [content.document.document, content.document.file_name]
        end
        
        # text handling
        text = case content
            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::ChatAddMembers then "invited %s" % self.format_username(message.content.member_user_ids.first)
            when TD::Types::MessageContent::ChatDeleteMember then "kicked %s" % self.format_username(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::ChatChangeTitle then "chat title set to: %s" % update.message.content.title.to_s
            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::Text then content.text.text
            else "unknown message type %s" % update.message.content.class
        end
        
        # download file if needed
        @client.download_file(file[0].id) if file and not file[0].local.is_downloading_completed

        # forwards, replies and message id..
        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 
        prefix << (update.message.is_outgoing ? '➡ ' : '⬅ ') + update.message.id.to_s  # message direction
        prefix << "fwd: %s" % self.format_username(update.message.forward_info.sender_user_id) if update.message.forward_info.instance_of? TD::Types::MessageForwardInfo::MessageForwardedFromUser  # fwd  from user 
        prefix << "fwd: %s" % self.format_chatname(update.message.forward_info.chat_id) if update.message.forward_info.instance_of? TD::Types::MessageForwardInfo::MessageForwardedPost  # fwd from chat 
        prefix << "reply: %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 << "file: %s" % self.format_file(file[0], file[1]) if file 
        prefix << "user: %s" % self.format_username(update.message.sender_user_id) if update.message.chat_id < 0 # show sender in group chats 

        text = (prefix << text).join(' | ') 

        # 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
    
    # new chat update -- when tg client discovers new chat #
    def new_chat_handler(update)   
        @logger.debug 'Got NewChat update'
        @logger.debug update.to_json
        self.process_chat_info(update.chat.id)
    end

    # user -- something changed in user data #
    def user_handler(update)   
        @logger.debug 'Got User update'
        @logger.debug update.to_json
        self.process_user_info(update.user.id)
    end

    # edited msg #
    def message_edited_handler(update)
        @logger.debug 'Got MessageEdited update'
        @logger.debug update.to_json
        
        # formatting
        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

    # deleted msg #
    def message_deleted_handler(update)
        @logger.debug 'Got MessageDeleted update'
        @logger.debug update.to_json
        return if not update.is_permanent
        text = "✗ %s |" % update.message_ids.join(',')
        @xmpp.message(@jid, update.chat_id.to_s, text)                
    end
    
    # status update handler #
    def status_update_handler(update)
        @logger.debug 'Got new StatusUpdate'
        @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


    # file msg -- symlink to download path #
    def file_handler(update)
        @logger.debug 'Got File update'
        @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
    
    ###########################################
    ## LooP handlers #########################
    ###########################################
    
    # processing authorization #
    def process_auth(typ, auth_data)
        @logger.info "Authorizing with :%s.." % typ
        @client.check_authentication_code(auth_data) if typ == '/code'
        @client.check_authentication_password(auth_data) if typ == '/password'
    end

    # /command #
    def process_command(chat_id, text)
        splitted = text.split # splitted[0] = command, splitted[1] = argument
        splitted = ['/sed', text[3..-1]] if text[0..2] == '/s/' # sed-like edit 
        resolved = nil; response = nil

        # if second argument starts with @, try to resolve it
        @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' # information about user / chat
            response = ''
            id = (resolved) ? resolved.id : splitted[1]
            id ||= chat_id
            id = id.to_i
            self.process_user_info(id) if id and id > 0 and not @cache[:users].key? id
            self.process_chat_info(id, false) if id and id < 0 and not @cache[:cache].key? id
            response = self.format_chatname(id)  if @cache[:chats].key? id
            response = self.format_username(id, true) if @cache[:users].key? id
        when '/add' # open new private chat by its id
            chat = (resolved) ? resolved.id : splitted[1].to_i
            self.process_chat_info(chat) if chat != 0
        when '/join' # join group/supergroup by invite link or by id 
            chat = (resolved) ? resolved.id : splitted[1]
            chat ||= chat_id
            chat.to_s[0..3] == "http" ? @client.join_chat_by_invite_link(chat).wait : @client.join_chat(chat.to_i).wait
        when '/secret' # create new secret chat 
            uid = (resolved) ? resolved.id : chat_id 
            @client.create_new_secret_chat(uid) if uid > 0 
        when '/group' # create new group with @user_id
            @client.create_new_basic_group_chat([resolved.id], splitted[2]) if resolved and splitted[2]
        when '/supergroup' # create new supergroup
            @client.create_new_supergroup_chat(splitted[1], splitted[2]) if splitted[2]
        when '/channel' # create new channel
            @client.create_new_supergroup_chat(splitted[1], splitted[2], is_channel: true) if splitted[2]
        when '/members' # view members of a group
            response = "- Members of chat %s -\n\n" % @cache[:chats][chat_id].title
            # supergroup 
            if @cache[:chats][chat_id].type.instance_of? TD::Types::ChatType::Supergroup then 
                @client.get_supergroup_members(@cache[:chats][chat_id].type.supergroup_id, TD::Types::SupergroupMembersFilter::Recent.new(), 0, 200).then { |members| members.members.each do |member|
                    self.process_user_info(member.user_id) if not @cache[:users].key? member.user_id # fetch userdata if noinfo
                    response += (@cache[:users].key? member.user_id) ? self.format_username(member.user_id, true) : "ID %s" % member.user_id
                    response += " | %s\n" % member.status.class.to_s
                end }.wait
            # normal group 
            elsif @cache[:chats][chat_id].type.instance_of? TD::Types::ChatType::BasicGroup then
                @cache[:chats][chat_id].last_message.content.member_user_ids.each do |member| response += (@cache[:users].key? member) ? self.format_username(member, true) : "ID %s" % member; response += "\n" end
            end
        when '/invite' # invite user to chat
            @client.add_chat_member(chat_id, resolved.id).wait if resolved
        when '/kick' #  removes user from chat
            @client.set_chat_member_status(chat_id, resolved.id, TD::Types::ChatMemberStatus::Left.new()).wait if resolved
        when '/ban' #  removes user from chat. argument = hours to ban.
            until_date = (splitted[1]) ? Time.now.getutc.to_i + splitted[1].to_i * 3600 : 0
            @client.set_chat_member_status(chat_id, resolved.id, TD::Types::ChatMemberStatus::Banned.new(banned_until_date: until_date)).wait if resolved
        when '/block' # add user to blacklist 
            @client.block_user(chat_id) 
        when '/unblock' # add user to blacklist 
            @client.unblock_user(chat_id) 
        when '/leave', '/delete' #  delete / leave chat
            @client.close_chat(chat_id).wait
            @client.leave_chat(chat_id).wait
            @client.close_secret_chat(@cache[:chats][chat_id].type.secret_chat_id).wait if @cache[:chats][chat_id].type.instance_of? TD::Types::ChatType::Secret
            @client.delete_chat_history(chat_id, true).wait
            @xmpp.presence(@jid, chat_id, :unsubscribed) 
            @xmpp.presence(@jid, chat_id, :unavailable)
            @cache[:chats].delete(chat_id)
        when '/sed' # sed-like edit
            sed = splitted[1].split('/')
            @client.search_chat_messages(chat_id, 0, 1, sender_user_id: @me.id, filter: TD::Types::SearchMessagesFilter::Empty.new).then {|msgs| 
                original = msgs.messages[0].content.text.text.to_s
                edited = (sed[0] != '' ) ? original.gsub(Regexp.new(sed[0]), sed[1]) : sed[1]
                @client.edit_message_text(chat_id, msgs.messages[0].id, TD::Types::InputMessageContent::Text.new(:text => { :text => edited, :entities => []}, :disable_web_page_preview => false, :clear_draft => true )) if edited != original
            }.wait
        when '/d' # delete last message
            id = splitted[1].to_i
            @client.search_chat_messages(chat_id, 0, 1, sender_user_id: @me.id, filter: TD::Types::SearchMessagesFilter::Empty.new).then {|msgs| id = msgs.messages[0].id }.wait if not id or id == 0
            @client.delete_messages(chat_id, [id], true)
        when '/dump'
            response = @cache[:chats][chat_id].to_json
        when '/search'
            count = (splitted[1]) ? splitted[1].to_i : 10
            query = (splitted[2]) ? splitted[2] : nil
            @client.search_chat_messages(chat_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 '/history'
            count = (splitted[1]) ? splitted[1].to_i : 10
            @client.get_chat_history(chat_id, 0, 0, count).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'
            @client.set_username(splitted[1]) if splitted[1]
        when '/setname'
            @client.set_name(splitted[1], splitted[2]) if splitted[1]
        when '/setbio'
            @client.set_bio(splitted[1]) if splitted[1]
        when '/setpassword'
            old_password = splitted[1]
            new_password = splitted[2]
            old_password = '' if old_password == 'nil'
            new_password = nil if new_password == 'nil'
            @client.set_password(old_password, new_password: new_password)
        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
            /history count — Retrieve chat history
            /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 #
        if text[0] == '>' then 
            splitted = text.split("\n")
            reply_to = splitted[0].scan(/\d/).join('').to_i
            reply_to = 0 if reply_to < 10000 # o_O
            text = splitted.drop(1).join("\n") if reply_to != 0 
        else
            reply_to = 0
        end
        
        # handling files received from xmpp #
        if text.start_with? @@content_upload_prefix  then
            message = TD::Types::InputMessageContent::Document.new(document: TD::Types::InputFile::Remote.new(id: text), caption: { :text => '', :entities => []})
        else
            message = TD::Types::InputMessageContent::Text.new(:text => { :text => text, :entities => []}, :disable_web_page_preview => false, :clear_draft => true )
        end
        
        # 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 = true)
        @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
            @xmpp.presence(@jid, chat_id.to_s, :subscribe, nil, nil, chat.title.to_s) if subscription # send subscription request
            self.process_status_update(chat_id, chat.title.to_s, true) if chat.id < 0 # groups presence 
        }.wait
    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
    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 
        when TD::Types::UserStatus::Online 
            xmpp_show = nil
            xmpp_status = "Online"
        when TD::Types::UserStatus::Offline 
            xmpp_show = (Time.now.getutc.to_i - status.was_online.to_i < 3600) ? :away : :xa
            xmpp_status = DateTime.strptime((status.was_online+Time.now.getlocal(@timezone).utc_offset).to_s,'%s').strftime("Last seen at %H:%M %d/%m/%Y")
        when TD::Types::UserStatus::Recently 
            xmpp_show = :dnd
            xmpp_status = "Last seen recently"
        when TD::Types::UserStatus::LastWeek 
            xmpp_show = :unavailable
            xmpp_status = "Last seen last week"
        when TD::Types::UserStatus::LastMonth 
            xmpp_show = :unavailable
            xmpp_status = "Last seen last month"
        else
            xmpp_show = :chat
            xmpp_status = status
        end

        xmpp_photo = self.format_file(@cache[:photos][user_id], 'image.jpg', true) if @cache[:photos].include? user_id
        xmpp_photo = (File.exist?  xmpp_photo.to_s) ? Base64.encode64(IO.binread(xmpp_photo)) : nil
        @xmpp.presence(@jid, user_id.to_s, nil, xmpp_show, xmpp_status, nil, xmpp_photo, immed) 
    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[:chats][chat_id].client_data # <DESC>
            userpic = self.format_file(@cache[:photos][chat_id], 'image.jpg', true) if @cache[:photos].include? chat_id
            userpic = (File.exist? userpic.to_s) ? Base64.encode64(IO.binread(userpic)) : nil
        end
        
        # ..
        return title, username, firstname, lastname, phone, bio, userpic
    end
    
    # resolve id by @username (or just return id)
    def resolve_username(username)
        resolved = nil
        if username[0] == '@' then  # @username
            @client.search_public_chat(username[1..-1]).then {|chat| resolved = '@' + chat.id.to_s}.wait
        elsif username[0..3] == 'http' or username[0..3] == 't.me' then # chat link 
            @client.join_chat_by_invite_link(username)
        elsif username.to_i != 0 then # user id
            resolved = username
        end
        
        return resolved || '' 
    end
    
    ###########################################
    ## Format functions #######################
    ###########################################
    # format tg user name #
    def format_username(user_id, show_id = false)
        return if user_id == 0 # @me
        if not @cache[:users].key? user_id then self.process_user_info(user_id) end # update cache 
        if not @cache[:users].key? user_id then return user_id end # return id if not found anything about this user 
        id = (@cache[:users][user_id].username == '') ? user_id : @cache[:users][user_id].username # username or user id
        name = @cache[:users][user_id].first_name # firstname
        name = name + ' ' + @cache[:users][user_id].last_name if @cache[:users][user_id].last_name != '' # lastname
        id = "%s ID %s" % [id, user_id] if show_id
        return "%s (@%s)" % [name, id]
    end

    # format tg chat name #
    def format_chatname(chat_id)
        if not @cache[:chats].key? chat_id then self.process_chat_info(chat_id, false) end
        if not @cache[:chats].key? chat_id then return chat_id end
        name = '%s (%s)' % [@cache[:chats][chat_id].title, chat_id]
        return name 
    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 
        text = (text.lines.count > 1 and not full) ? "%s.." % text.split("\n")[0] : text
        return "msg %s [%s]" % [message_id.to_s, 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
