mrDoctorWho/vk4xmpp

View on GitHub
gateway.py

Summary

Maintainability
F
6 days
Test Coverage
#!/usr/bin/env python2
# coding: utf-8

# vk4xmpp gateway, v3.6
# © simpleApps, 2013 — 2018.
# Program published under the MIT license.

# Disclamer: be aware that this program's code may hurt your eyes or feelings.
# You were warned.

__author__ = "mrDoctorWho <mrdoctorwho@gmail.com>"
__license__ = "MIT"
__version__ = "3.6"

import hashlib
import logging
import os
import re
import signal
import sys
import threading
import time
from argparse import ArgumentParser

try:
    import ujson as json
except ImportError:
    import json

core = getattr(sys.modules["__main__"], "__file__", None)
root = "."
if core:
    core = os.path.abspath(core)
    root = os.path.dirname(core)
    if root:
        os.chdir(root)

sys.path.insert(0, "library")
sys.path.insert(1, "modules")
reload(sys)
sys.setdefaultencoding("utf-8")

# Now we can import our own modules
import xmpp
from itypes import Database
from stext import setVars, _
from defaults import *
from printer import *
from webtools import *

Users = {}
Semaphore = threading.Semaphore()

# command line arguments
argParser = ArgumentParser()
argParser.add_argument("-c", "--config",
    help="set the general config file destination", default="Config.txt")
argParser.add_argument("-d", "--daemon",
    help="run in daemon mode (no auto-restart)", action="store_true")
args = argParser.parse_args()
Daemon = args.daemon
Config = args.config

startTime = int(time.time())


DatabaseFile = None
TransportID = None
Host = None
Server = None
Port = None
Password = None
LAST_REPORT = None


execfile(Config)
Print("#-# Config loaded successfully.")

# logger
logger = logging.getLogger("vk4xmpp")
logger.setLevel(LOG_LEVEL)
loggerHandler = logging.FileHandler(logFile)
formatter = logging.Formatter("%(asctime)s %(levelname)s:"
    "%(name)s: %(message)s", "%d.%m.%Y %H:%M:%S")
loggerHandler.setFormatter(formatter)
logger.addHandler(loggerHandler)

# now we can import the last modules
from writer import *
from settings import *
import vkapi as api
import utils

# Setting variables
# DefLang for language id, root for the translations directory
setVars(DefLang, root)

if THREAD_STACK_SIZE:
    threading.stack_size(THREAD_STACK_SIZE)
del formatter, loggerHandler

if os.name == "posix":
    OS = "{0} {2:.16} [{4}]".format(*os.uname())
else:
    import platform
    OS = "Windows {0}".format(*platform.win32_ver())

PYTHON_VERSION = "{0} {1}.{2}.{3}".format(sys.subversion[0], *sys.version_info)

# See extensions/.example.py for more information about handlers
Handlers = {"msg01": [], "msg02": [],
            "msg03": [], "evt01": [],
            "evt02": [], "evt03": [],
            "evt04": [], "evt05": [],
            "evt06": [], "evt07": [],
            "prs01": [], "prs02": [],
            "evt08": [], "evt09": []}

Stats = {"msgin": 0,  # from vk
        "msgout": 0,  # to vk
        "method": 0}


MAX_MESSAGES_PER_REQUEST = 20

# Status code that indicates what to do with returning body from plugins
MSG_SKIP = -1
MSG_PREPEND = 0
MSG_APPEND = 1

# Timeout after which user is considered paused typing
TYPING_TIMEOUT = 5

# Timeout for friends updating
FRIENDS_UPDATE_TIMEOUT = 300


def runDatabaseQuery(query, args=(), set=False, many=True):
    """
    Executes the given sql to the database
    Args:
        query: the sql query to execute
        args: the query argument
        set: whether to commit after the execution
        many: whether to return more than one result
    Returns:
        The query execution result
    """
    semph = None
    if threading.currentThread() != "MainThread":
        semph = Semaphore
    with Database(DatabaseFile, semph) as db:
        db(query, args)
        if set:
            db.commit()
            result = None
        elif many:
            result = db.fetchall()
        else:
            result = db.fetchone()
    return result


def initDatabase(filename):
    """
    Initializes database if it doesn't exist
    Args:
        filename: the database filename
    """
    runDatabaseQuery("create table if not exists users"
        "(jid text, username text, token text, "
            "lastMsgID integer, rosterSet bool)", set=True)
    return True


def executeHandlers(type, list=()):
    """
    Executes all handlers with the given type
    Args:
        type: the handlers type
        list: the arguments to pass to handlers
    """
    handlers = Handlers[type]
    for handler in handlers:
        utils.execute(handler, list)


def registerHandler(type, func):
    """
    Register a handler
    Args:
        type: the handler type
        func: the function to register
    """
    logger.info("main: add \"%s\" to handle type %s", func.func_name, type)
    for handler in Handlers[type]:
        if handler.func_name == func.func_name:
            Handlers[type].remove(handler)
    Handlers[type].append(func)


def getGatewayRev():
    """
    Gets gateway revision using git or custom revision number
    """
    number, hash = 480, 0
    shell = os.popen("git describe --always &"
        "& git log --pretty=format:''").readlines()
    if shell:
        number, hash = len(shell), shell[0]
    return "#%s-%s" % (number, hash)


def vk2xmpp(id):
    """
    Converts a numeric VK ID to a Jabber ID and vice versa
    Args:
        id: a Jabber or VK id
    Returns:
        id@TransportID if parameter id is a number
        id if parameter "id" is id@TransportID
        TransportID if the given id is equal to TransportID
    """
    if not utils.isNumber(id) and "@" in id:
        id = id.split("@")[0]
        if utils.isNumber(id):
            id = int(id)
    elif id != TransportID:
        id = u"%s@%s" % (id, TransportID)
    return id


REVISION = getGatewayRev()

# Escape xmpp non-allowed chars
badChars = [x for x in xrange(32) if x not in (9, 10, 13)] + [57003, 65535]
escape = re.compile("|".join(unichr(x) for x in badChars),
    re.IGNORECASE | re.UNICODE | re.DOTALL).sub

sortMsg = lambda first, second: first.get("id", 0) - second.get("id", 0)
require = lambda name: os.path.exists("extensions/%s.py" % name)
isdef = lambda var: var in globals()
findUserInDB = lambda source: runDatabaseQuery("select * from users where jid=?", (source,), many=False)


class Transport(object):
    """
    A dummy class to store settings (ATM)
    """
    def __init__(self):
        self.settings = Settings(TransportID, user=False)


class VK(object):
    """
    Contains methods to handle the VK stuff
    """
    def __init__(self, token=None, source=None):
        self.token = token
        self.source = source
        self.pollConfig = {"mode": 66, "wait": 30, "act": "a_check", "version": 3}
        self.pollServer = ""
        self.pollInitialized = False
        self.engine = None
        self.online = False
        self.userID = 0
        self.methods = 0
        self.lists = []
        self.friends_fields = {"screen_name", "online"}
        self.engine = None
        self.cache = {}
        self.permissions = 0
        self.timezone = 0
        logger.debug("VK initialized (jid: %s)", source)

    def __str__(self):
        return ("user id: %s; timezone: %s; online: %s; token: %s" %
            (self.userID, self.timezone, self.online, self.token))

    def init(self):
        self.getUserPreferences()
        self.getPermissions()

    getToken = lambda self: self.engine.token

    def checkToken(self):
        """
        Checks the token
        """
        try:
            data = self.engine.method("users.get")
            if not data:
                raise RuntimeError("Unable to get data for user!")
        except api.VkApiError:
            logger.error("unable to check user's token, error: %s (user: %s)",
                traceback.format_exc(), self.source)
            return False
        return True

    def auth(self):
        """
        Initializes the APIBinding object
        Returns:
            True if everything went fine
        """
        logger.debug("VK going to authenticate (jid: %s)", self.source)
        self.engine = api.APIBinding(self.token, DEBUG_API, self.source)
        if not self.checkToken():
            raise api.TokenError("The token is invalid (jid: %s, token: %s)"
                % (self.source, self.token))
        self.online = True
        return True

    def initPoll(self):
        """
        Initializes longpoll
        Returns:
            False: if any error occurred
            True: if everything went just fine
        """
        self.pollInitialized = False
        logger.debug("longpoll: requesting server address (jid: %s)", self.source)
        try:
            response = self.method("messages.getLongPollServer", {"use_ssl": 1, "need_pts": 0})
        except Exception:
            response = None
        if not response:
            logger.warning("longpoll: no response!")
            return False
        self.pollServer = "https://%s" % response.pop("server")
        self.pollConfig.update(response)
        logger.debug("longpoll: server: %s (jid: %s)",
            self.pollServer, self.source)
        self.pollInitialized = True
        return True

    # TODO: move it the hell outta here
    # wtf it's still doing here?
    def makePoll(self):
        """
        Returns:
            socket connected to the poll server
        Raises api.LongPollError if poll not yet initialized (self.pollInitialized)
        """
        if not self.pollInitialized:
            raise api.LongPollError("The Poll wasn't initialized yet")
        return api.AsyncHTTPRequest.getOpener(self.pollServer, self.pollConfig)

    def method(self, method, args=None, force=False, notoken=False):
        """
        This is a duplicate function of self.engine.method
        Needed to handle errors properly exactly in __main__
        Args:
            method: a VK API method
            args: method arguments
            force: whether to force execution (ignore self.online and captcha)
            notoken: whether to cut the token from the request
        Returns:
            The method execution result
        See library/vkapi.py for more information about exceptions
        """
        args = args or {}
        result = {}
        self.methods += 1
        Stats["method"] += 1
        if not self.engine.captcha and (self.online or force):
            try:
                result = self.engine.method(method, args, notoken=notoken)
            except (api.InternalServerError, api.AccessDenied) as e:
                if force:
                    raise

            except api.CaptchaNeeded as e:
                executeHandlers("evt04", (self.source, self.engine.captcha))
                self.online = False

            except api.NetworkNotFound as e:
                self.online = False

            except api.NotAllowed as e:
                if self.engine.lastMethod[0] == "messages.send":
                    sendMessage(self.source,
                        vk2xmpp(args.get("user_id", TransportID)),
                        _("You're not allowed to perform this action."))

            except api.VkApiError as e:
                # There are several types of VkApiError
                # But the user definitely must be removed.
                # The question is: how?
                # Should we completely exterminate them or just remove?
                remove = False
                m = e.message
                # TODO: Make new exceptions for each of the conditions below
                if m == "User authorization failed: user revoke access for this token.":
                    remove = True
                elif m == "User authorization failed: invalid access_token.":
                    sendMessage(self.source, TransportID, m + " Please, register again")
                if remove:
                    utils.runThread(removeUser, (self.source, remove))
                    self.online = False
                logger.error("VK: apiError %s (jid: %s)", m, self.source)
            else:
                return result
            logger.error("VK: error %s occurred while executing"
                " method(%s) (%s) (jid: %s)",
                e.__class__.__name__, method, e.message, self.source)
            return result

    @utils.threaded
    def disconnect(self):
        """
        Stops all user handlers and removes the user from Poll
        """
        self.online = False
        logger.debug("VK: user %s has left", self.source)
        executeHandlers("evt06", (self,))
        self.setOffline()

    def setOffline(self):
        """
        Sets the user status to offline
        """
        self.method("account.setOffline")

    def setOnline(self):
        """
        Sets the user status to online
        """
        self.method("account.setOnline")

    @staticmethod
    def formatName(data):
        """
        Extracts a string name from a user object
        Args:
            user: a VK user object which is a dict with the first_name and last_name keys
        Returns:
            User's first and last name
        """
        name = escape("", "%(first_name)s %(last_name)s" % data)
        del data["first_name"]
        del data["last_name"]
        return name

    def getFriends(self, fields=None, limit=MAX_FRIENDS):
        """
        Executes the friends.get method and formats it in the key-value style
        Example:
            {1: {"name": "Pavel Durov", "online": False}
        Args:
            fields: a list of advanced fields to receive
        Which will be added in the result values
        """
        fields = fields or self.friends_fields
        raw = self.method("friends.get", {"fields": str.join(",", fields), "count": limit}) or {}
        raw = raw.get("items", {})
        friends = {}
        for friend in raw:
            uid = friend["id"]
            online = friend.get("online")
            if online is None:
                logger.warning("No online for friend: %d (jid: %s)", friend, self.source)
                online = False
            name = self.formatName(friend)
            friends[uid] = {"name": name, "online": online, "lists": friend.get("lists")}
            for key in fields:
                friends[uid][key] = friend.get(key)
        return friends

    @staticmethod
    def getPeerIds(conversations, source=None):
        """
        Returns a list of peer ids that exist in the given conversations
        Args:
            conversations: list of Conversations objects
        Returns:
            A list of peer id strings
        """
        peers = []
        if not conversations:
            logger.warning("no conversations for (jid: %s)", source)
            return peers
        for conversation in conversations:
            if isinstance(conversation, dict):
                innerConversation = conversation.get("conversation")
                if innerConversation:
                    peers.append(str(innerConversation["peer"]["id"]))
        return peers

    def getMessagesBulk(self, peers, count=20, mid=0):
        """
        Receives messages for all the conversations' peers
        25 is the maximum number of conversations we can receive in a single request
        The sky is the limit!
        Args:
            peers: a list of peer ids (strings)
            messages: a list of messages (used internally)
            count: the number of messages to receive
            uid: the last message id
        Returns:
            A list of VK Message objects
        """
        step = 20
        messages = []
        if peers:
            cursor = 0
            for i in xrange(step, len(peers) + step, step):
                tempPeers = peers[cursor:i]
                users = ",".join(tempPeers)
                response = self.method("execute.getMessagesBulk",
                    {"users": users,
                    "start_message_id": mid,
                    "count": count})
                if response:
                    for item in response:
                        item = item.get("items")
                        if not item:
                            continue
                        messages.extend(item)
                else:
                    # not sure if that's okay
                    # VK is totally unpredictable now
                    logger.warning("No response for execute.getMessagesBulk!"
                        +" Users: %s, mid: %s (jid: %s)", users, mid, self.source)
                cursor += step
        return messages

    def getMessages(self, count=20, mid=0, uid=0, filter_="unread"):
        """
        Gets the last messages list
        Args:
            count: the number of messages to receive
            mid: the last message id
        Returns:
            A list of VK Message objects
        """
        if uid == 0:
            conversations = self.method("messages.getConversations", {"count": count, "filter": filter_}) or {}
            conversations = conversations.get("items")
        else:
            conversations = [{"conversation": {"peer": {"id": uid}}}]
        peers = VK.getPeerIds(conversations, self.source)
        return self.getMessagesBulk(peers, count=count, mid=mid)

    # TODO: put this in the DB
    def getUserPreferences(self):
        """
        Receives the user's id and timezone
        Returns:
            The current user id
        """
        if not self.userID or not self.timezone:
            data = self.method("users.get", {"fields": "timezone"})
            if data:
                data = data.pop()
                self.timezone = data.get("timezone")
                self.userID = data.get("id")
        return (self.userID, self.timezone)

    def getPermissions(self):
        """
        Update the app permissions
        Returns:
            The current permission mask
        """
        if not self.permissions:
            self.permissions = self.method("account.getAppPermissions")
        return self.permissions

    def getLists(self):
        """
        Receive the list of the user friends' groups
        Returns:
            a list of user friends groups
        """
        if not self.lists:
            self.lists = self.method("friends.getLists")
        return self.lists

    @utils.cache
    def getGroupData(self, gid, fields=None):
        """
        Gets group data (only name so far)
        Args:
            gid: the group id
            fields: a list of advanced fields to receive
        Returns:
            The group information
        """
        fields = fields or ["name"]
        data = self.method("groups.getById", {"group_id": abs(gid), "fields": str.join(",", fields)})
        if data:
            data = data[0]
            return data
        raise RuntimeError("Unable to get group data for %d" % gid)

    @utils.cache
    def getUserData(self, uid, fields=None):
        """
        Gets user data. Such as name, photo, etc
        Args:
            uid: the user id (list or str)
            fields: a list of advanced fields to receive
        Returns:
            The user information
        """
        if uid < 0:
            raise RuntimeError("Unable to get user name. User ids can't be negative: %d" % uid)
        if not fields:
            user = Users.get(self.source)
            if user and uid in user.friends:
                return user.friends[uid]
            fields = ["screen_name"]
        if isinstance(uid, (list, tuple)):
            uid = str.join(",", uid)
        data = self.method("users.get", {"user_ids": uid, "fields": str.join(",", fields)})
        if data:
            data = data[0]
            data["name"] = self.formatName(data)
            return data
        raise RuntimeError("Unable to get the user's name for %d"  % uid)

    def getName(self, id_):
        return self.getData(id_).get("name", "Unknown group (id: %s)" % id_)

    def getData(self, id_, fields=None):
        if id_ > 0:
            data = self.getUserData(id_, fields)
        else:
            data = self.getGroupData(id_, fields) 
        return data

    def sendMessage(self, body, id, mType="user_id", more={}):
        """
        Sends message to a VK user (or a chat)
        Args:
            body: the message body
            id: the user id
            mType: the message type (user_id is for dialogs, chat_id is for chats)
            more: for advanced fields such as attachments
        Returns:
            The result of sending the message
        """
        Stats["msgout"] += 1
        values = {mType: id, "message": body, "type": 0, "random_id": int(time.time())}
        values.update(more)
        try:
            result = self.method("messages.send", values)
        except api.VkApiError:
            crashLog("messages.send")  # this actually never happens
            result = False
        return result


class User(object):
    """
    Handles XMPP and VK stuff
    """
    def __init__(self, source=""):
        self.friends = {}
        self.typing = {}
        self.msgCacheByUser = {}  # the cache of associations vk mid: xmpp mid
        self.lastMsgByUser = {}  # the cache of last messages sent to user (user id: msg id)
        self.source = source  # TODO: rename to jid

        self.lastMsgID = 0
        self.rosterSet = None
        self.username = None

        self.resources = set([])
        self.settings = Settings(source)
        self.last_udate = time.time()
        self.sync = threading.Lock()
        logger.debug("User initialized (jid: %s)", self.source)

    def sendMessage(self, body, id, mType="user_id", more={}, mid=0):
        result = self.vk.sendMessage(body, id, mType, more)
        if mid:
            self.msgCacheByUser[int(id)] = {"xmpp": mid, "vk": result}
        return result

    def connect(self, username=None, password=None, token=None):
        """
        Initializes a VK() object and tries to make an authorization if no token provided
        Args:
            username: the user's phone number or e-mail for password authentication
            password: the user's account password
            token: the user's token
        Returns:
            True if everything went fine
        """
        logger.debug("User connecting (jid: %s)", self.source)
        exists = False
        user = findUserInDB(self.source)  # check if user registered
        if user:
            exists = True
            logger.debug("User was found in the database... (jid: %s)", self.source)
            if not token:
                logger.debug("... but no token was given. Using the one from the database (jid: %s)", self.source)
                _, _, token, self.lastMsgID, self.rosterSet = user

        if not (token or password):
            logger.warning("User wasn't found in the database and no token or password was given!")
            raise RuntimeError("Either no token or password was given!")

        if password:
            logger.debug("Going to authenticate via password (jid: %s)", self.source)
            pwd = api.PasswordLogin(username, password).login()
            token = pwd.confirm()

        self.vk = vk = VK(token, self.source)
        try:
            vk.auth()
        except api.CaptchaNeeded:
            self.sendSubPresence()
            logger.error("User: running captcha challenge (jid: %s)", self.source)
            executeHandlers("evt04", (self.source, self.vk.engine.captcha))
            return True
        else:
            logger.debug("User seems to be authenticated (jid: %s)", self.source)
            if exists:
                # Anyways, it won't hurt anyone
                runDatabaseQuery("update users set token=? where jid=?",
                    (vk.getToken(), self.source), True)
            else:
                runDatabaseQuery("insert into users (jid, token, lastMsgID, rosterSet) values (?,?,?,?)",
                    (self.source, vk.getToken(),
                        self.lastMsgID, self.rosterSet), True)
            executeHandlers("evt07", (self,))
            vk.init()
            # TODO: move friends to VK() and check last update timeout?
            # Currently, every time we request friends a new object is being created
            # As we request it very frequently, it might be better to move
            # getFriends() to vk.init() and every time check if the list is due for the update
            self.friends = vk.getFriends()
        return vk.online

    def markRosterSet(self):
        """
        Marks the user's roster as already set, so the gateway won't need to send it again
        """
        self.rosterSet = True
        runDatabaseQuery("update users set rosterSet=? where jid=?",
            (self.rosterSet, self.source), True)

    def initialize(self, force=False, send=True, resource=None, first=False):
        """
        Initializes user after the connection has been completed
        Args:
            force: force sending subscription presence
            send: whether to send the init presence
            resource: add resource in self.resources to prevent unneeded stanza sending
            first: whether to initialize the user for the first time (during registration)
        """
        logger.debug("User: beginning user initialization (jid: %s)", self.source)
        Users[self.source] = self
        if not self.friends:
            self.friends = self.vk.getFriends()
        if force or not self.rosterSet:
            logger.debug("User: sending subscription presence with force:%s (jid: %s)",
                force, self.source)
            import rostermanager
            rostermanager.Roster.checkRosterx(self, resource)
        if send:
            self.sendInitPresence()
        if resource:
            self.resources.add(resource)
        utils.runThread(self.vk.getUserPreferences)
        if first:
            self.sendMessages(True, filter_="unread")
        else:
            self.sendMessages(True)
        Poll.add(self)
        utils.runThread(executeHandlers, ("evt05", (self,)))

    def sendInitPresence(self):
        """
        Sends available presence to the user from all online friends
        """
        if not self.vk.engine.captcha:
            if not self.friends:
                self.friends = self.vk.getFriends()
            logger.debug("User: sending init presence (friends count: %s) (jid %s)",
                len(self.friends), self.source)
            for uid, value in self.friends.iteritems():
                if value["online"]:
                    sendPresence(self.source, vk2xmpp(uid), hash=USER_CAPS_HASH)
            sendPresence(self.source, TransportID, hash=TRANSPORT_CAPS_HASH)

    def sendOutPresence(self, destination, reason=None, all=False):
        """
        Sends the unavailable presence to destination. Defines a reason if set.
        Args:
            destination: to whom send the stanzas
            reason: the reason why going offline
            all: send the unavailable presence from all the friends or only the ones who's online
        """
        logger.debug("User: sending out presence to %s", self.source)
        friends = self.friends.keys()
        if not all and friends:
            friends = filter(lambda key: self.friends[key]["online"], friends)

        for uid in friends + [TransportID]:
            sendPresence(destination, vk2xmpp(uid), "unavailable", reason=reason)

    def sendSubPresence(self, dist=None):
        """
        Sends the subscribe presence to self.source
        Args:
            dist: friends list
        """
        dist = dist or {}
        for uid, value in dist.iteritems():
            sendPresence(self.source, vk2xmpp(uid), "subscribe", value["name"])
        sendPresence(self.source, TransportID, "subscribe", IDENTIFIER["name"])
        # TODO: Only mark roster set when we received authorized/subscribed event from the user
        if dist:
            self.markRosterSet()

    def sendMessages(self, init=False, messages=None, mid=0, uid=0, filter_="unread"):
        """
        Sends messages from vk to xmpp and call message01 handlers
        Args:
            init: needed to know if function called at init (add time or not)
            messages: a list of pre-defined messages that would be handled and sent (w/o requesting new ones)
            mid: last message id
            uid: user id
            filter_: what filter to use (all/unread)
        """
        with self.sync:
            date = 0
            if not messages:
                messages = self.vk.getMessages(MAX_MESSAGES_PER_REQUEST, mid or self.lastMsgID, uid, filter_)

            messages = sorted(messages, sortMsg)
            for message in messages:
                # check if message wasn't sent by our user
                if not message["out"]:
                    if self.lastMsgID >= message["id"]:
                        continue
                    Stats["msgin"] += 1
                    frm = message["from_id"]
                    mid = message["id"]
                    self.removeTyping(frm)
                    fromjid = vk2xmpp(frm)
                    body = uhtml(message["text"])
                    iter_ = Handlers["msg01"].__iter__()
                    for func in iter_:
                        try:
                            status, data = func(self, message)
                        except Exception:
                            status, data = MSG_APPEND, ""
                            crashLog("handle.%s" % func.__name__)

                        # got ignore status, continuing execution
                        if status == MSG_SKIP:
                            for func in iter_:
                                utils.execute(func, (self, message))
                            break
                        elif status == MSG_APPEND:
                            body += data
                        elif status == MSG_PREPEND:
                            body = data + body
                    else:
                        if self.settings.force_vk_date or init:
                            date = message["date"]
                        self.lastMsgByUser[frm] = mid
                        sendMessage(self.source, fromjid, escape("", body), date, mid=mid)
        if messages:
            newLastMsgID = messages[-1]["id"]
            if newLastMsgID > self.lastMsgID:
                self.lastMsgID = newLastMsgID
                runDatabaseQuery("update users set lastMsgID=? where jid=?",
                    (newLastMsgID, self.source), True)

    def removeTyping(self, frm):
        if frm in self.typing:
            del self.typing[frm]

    def updateTypingUsers(self, cTime):
        """
        Sends "paused" message event to stop user from composing a message
        Sends only if last typing activity in VK was more than 7 seconds ago
        Args:
            cTime: current time
        """
        with self.sync:
            for user, last in self.typing.items():
                if cTime - last > TYPING_TIMEOUT:
                    del self.typing[user]
                    sendMessage(self.source, vk2xmpp(user), typ="paused")

    def updateFriends(self, cTime):
        """
        Updates friends list.
        Compares the current friends list to the new list
        Takes a corresponding action if any difference found
        """
        if (cTime - self.last_udate) > FRIENDS_UPDATE_TIMEOUT and not self.vk.engine.captcha:
            if self.settings.keep_online:
                self.vk.setOnline()
            else:
                self.vk.setOffline()
            self.last_udate = cTime
            friends = self.vk.getFriends()
            if not friends:
                logger.error("updateFriends: no friends received (jid: %s).",
                    self.source)
                return None

            for uid in friends:
                if uid not in self.friends:
                    self.sendSubPresence({uid: friends[uid]})
            for uid in self.friends:
                if uid not in friends:
                    sendPresence(self.source, vk2xmpp(uid), "unsubscribe")
            self.friends = friends

    def reauth(self):
        """
        Tries to execute self.initialize() again and connect() if needed
        Usually needed after captcha challenge is done
        """
        logger.debug("calling reauth for user (jid: %s)", self.source)
        if not self.vk.online:
            self.connect()
        self.initialize()

    def captchaChallenge(self, key):
        """
        Sets the captcha key and sends it to VK
        Args:
            key: the captcha text
        """
        engine = self.vk.engine
        engine.captcha["key"] = key
        logger.debug("retrying for user (jid: %s)", self.source)
        if engine.retry():
            self.reauth()

    def __str__(self):
        return "User(token=%s, source=%s, friends_count=%s)" % (self.vk.getToken(), self.source, len(self.friends))


def sendPresence(destination, source, pType=None, nick=None,
    reason=None, hash=None, show=None):
    """
    Sends a presence to destination from source
    Args:
        destination: whom send the presence to
        source: who send the presence from
        pType: the presence type
        nick: add <nick> tag
        reason: set status message
        hash: add caps hash
        show: add status show
    """
    presence = xmpp.Presence(destination, pType,
        frm=source, status=reason, show=show)
    if nick:
        presence.setTag("nick", namespace=xmpp.NS_NICK)
        presence.setTagData("nick", nick)
    if hash:
        presence.setTag("c", {"node": CAPS_NODE, "ver": hash, "hash": "sha-1"}, xmpp.NS_CAPS)
    executeHandlers("prs02", (presence, destination, source))
    sender(Component, presence)


def sendMessage(destination, source, body=None, timestamp=0, typ="active", mtype="chat", mid=0):
    """
    Sends message to destination from source
    Args:
        destination: to whom send the message
        source: from who send the message
        body: message body
        timestamp: message timestamp (XEP-0091)
        typ: xmpp chatstates type (XEP-0085)
        mtype: the message type
        mid: message id
    """
    msg = xmpp.Message(destination, body, mtype, frm=source)
    msg.setTag(typ, namespace=xmpp.NS_CHATSTATES)
    if timestamp:
        timestamp = time.gmtime(timestamp)
        msg.setTimestamp(time.strftime("%Y%m%dT%H:%M:%S", timestamp))
    if mid:
        msg.setID(mid)
    executeHandlers("msg03", (msg, destination, source))
    sender(Component, msg)


def sendChatMarker(destination, source, mid, typ="displayed"):
    """
    Sends a chat marker as per XEP-0333
    Args:
        destination: to whom send the marker
        source: from who send the marker
        mid: which message id should be marked as read
        typ: marker type (displayed by default)
    """
    msg = xmpp.Message(destination, typ="chat",frm=source)
    msg.setTag(typ, {"id": mid}, xmpp.NS_CHAT_MARKERS)
    sender(Component, msg)


def report(message):
    """
    Critical error reporter
    """
    global LAST_REPORT
    if Transport.settings.send_reports and message != LAST_REPORT:
        LAST_REPORT = message
        message = "Critical failure:\n%s" % message
        for admin in ADMIN_JIDS:
            sendMessage(admin, TransportID, message)


def computeCapsHash(features=TransportFeatures):
    """
    Computes a hash which will be placed in all presence stanzas
    Args:
        features: the list of features to compute hash from
    """
    result = "%(category)s/%(type)s//%(name)s<" % IDENTIFIER
    features = sorted(features)
    result += str.join("<", features) + "<"
    return hashlib.sha1(result).digest().encode("base64")


# TODO: rename me
def sender(cl, stanza, cb=None, args={}):
    """
    Sends stanza. Writes a crashlog on error
    Parameters:
        cl: the xmpp.Client object
        stanza: the xmpp.Node object
        cb: callback function
        args: callback function arguments
    """
    if cb:
        cl.SendAndCallForResponse(stanza, cb, args)
    else:
        try:
            cl.send(stanza)
        except Exception:
            disconnectHandler()


def updateCron():
    """
    Calls the functions to update friends and typing users list
    """
    while ALIVE:
        for user in Users.values():
            cTime = time.time()
            user.updateTypingUsers(cTime)
            user.updateFriends(cTime)
        time.sleep(2)


def calcStats():
    """
    Returns count(*) from users database
    """
    countOnline = len(Users)
    countTotal = runDatabaseQuery("select count(*) from users", many=False)[0]
    return [countTotal, countOnline]


def removeUser(user, roster=False, notify=True):
    """
    Removes user from the database
    Args:
        user: User class object or jid without resource
        roster: remove vk contacts from user's roster
            (only if User class object was in the first param)
        notify: whether to let the user know that they're being exterminated
    """
    if isinstance(user, (str, unicode)):  # unicode is the default, but... who knows
        source = user
    elif user:
        source = user.source
    else:
        raise RuntimeError("Invalid user argument: %s" % str(user))
    if notify:
        sendMessage(source, TransportID,
            _("Your record was EXTERMINATED from the database."
                " Let us know if you feel exploited."), -1)
    logger.debug("User: removing user from db (jid: %s)" % source)
    runDatabaseQuery("delete from users where jid=?", (source,), True)
    logger.debug("User: deleted (jid: %s)", source)

    user = Users.get(source)
    if user:
        del Users[source]
        if roster:
            friends = user.friends
            user.exists = False  # Make the Daleks happy
            if friends:
                logger.debug("User: removing myself from roster (jid: %s)", source)
                for id in friends.keys() + [TransportID]:
                    jid = vk2xmpp(id)
                    sendPresence(source, jid, "unsubscribe")
                    sendPresence(source, jid, "unsubscribed")
                user.settings.exterminate()
                executeHandlers("evt03", (user,))
            user.vk.online = False


def checkPID():
    """
    Gets a new PID and kills the previous PID
    by signal 15 and then by 9
    """
    pid = os.getpid()
    if os.path.exists(pidFile):
        old = rFile(pidFile)
        if old:
            old = int(old)
            if pid != old:
                Print("#-# Killing the previous instance: ", False)
                try:
                    os.kill(old, signal.SIGTERM)
                    time.sleep(3)
                    os.kill(old, signal.SIGKILL)
                except OSError as e:
                    if e.errno != 3:
                        Print("%d %s.\n" % (old, e.message), False)
                else:
                    Print("%d killed.\n" % old, False)
    wFile(pidFile, str(pid))


def loadExtensions(dir):
    """
    Loads extensions
    """
    for file in os.listdir(dir):
        if not file.startswith(".") and file.endswith(".py"):
            execfile("%s/%s" % (dir, file), globals())


def connect():
    """
    Makes a connection to the jabber server
    Returns:
        False if failed
        True if completed
    """
    global Component
    Component = xmpp.Component(Host, debug=DEBUG_XMPPPY)
    Print("\n#-# Connecting: ", False)
    if not Component.connect((Server, Port)):
        Print("failed.\n", False)
        return False
    else:
        Print("ok.\n", False)
        Print("#-# Auth: ", False)
        if not Component.auth(TransportID, Password):
            Print("failed (%s/%s)!\n"
                % (Component.lastErr, Component.lastErrCode), True)
            return False
        else:
            Print("ok.\n", False)
            Component.RegisterDisconnectHandler(disconnectHandler)
            Component.set_send_interval(STANZA_SEND_INTERVAL)
    return True


def initializeUsers():
    """
    Initializes users by sending them "probe" presence
    """
    Print("#-# Initializing users", False)
    users = runDatabaseQuery("select jid from users")
    for user in users:
        Print(".", False)
        sendPresence(user[0], TransportID, "probe")
    Print("\n#-# Yay! Component %s initialized well." % TransportID)


def runMainActions():
    """
    Runs the actions for the gateway to work well
    Initializes extensions, longpoll and modules
    Computes required hashes
    """
    for num, event in enumerate(Handlers["evt01"]):
        utils.runThread(event, name=("extension-%d" % num))
    utils.runThread(Poll.process, name="longPoll")
    utils.runThread(updateCron)
    import modulemanager
    Manager = modulemanager.ModuleManager
    Manager.load(Manager.list())
    global USER_CAPS_HASH, TRANSPORT_CAPS_HASH
    USER_CAPS_HASH = computeCapsHash(UserFeatures)
    TRANSPORT_CAPS_HASH = computeCapsHash(TransportFeatures)


def main():
    """
    Runs the init actions
    Checks if any other copy running and kills it
    """
    logger.info("gateway started")
    if RUN_AS:
        import pwd
        uid = pwd.getpwnam(RUN_AS).pw_uid
        logger.warning("switching to user %s:%s", RUN_AS, uid)
        os.setuid(uid)
    checkPID()
    initDatabase(DatabaseFile)
    if connect():
        initializeUsers()
        runMainActions()
        logger.info("gateway initialized at %s", TransportID)
    else:
        disconnectHandler(False)


def disconnectHandler(crash=True):
    """
    Handles disconnect
    Writes a crash log if the crash parameter is True
    """
    executeHandlers("evt02")
    if crash:
        crashLog("main.disconnect")
    logger.critical("disconnecting from the server")
    try:
        Component.disconnect()
    except AttributeError:
        pass
    global ALIVE
    ALIVE = False
    if not Daemon:
        logger.warning("the gateway is going to be restarted!")
        Print("Restarting...")
        time.sleep(5)
        os.execl(sys.executable, sys.executable, *sys.argv)
    else:
        logger.info("the gateway is shutting down!")
        os._exit(-1)


def exit(sig=None, frame=None):
    """
    Just stops the gateway and sends unavailable presence
    """
    status = "Shutting down by %s" % ("SIGTERM" if sig == signal.SIGTERM else "SIGINT")
    Print("#! %s" % status, False)
    for user in Users.itervalues():
        user.sendOutPresence(user.source, status, all=True)
        Print("." * len(user.friends), False)
    Print("\n")
    executeHandlers("evt02")
    try:
        os.remove(pidFile)
    except OSError:
        pass
    os._exit(0)


def loop():
    """
    The main loop which is used to call the stanza parser
    """
    while ALIVE:
        try:
            Component.iter(1)
        except Exception:
            logger.critical("disconnected")
            crashLog("component.iter")
            disconnectHandler(True)


if __name__ == "__main__":
    signal.signal(signal.SIGTERM, exit)
    signal.signal(signal.SIGINT, exit)
    loadExtensions("extensions")
    Transport = Transport()
    from longpoll import *
    try:
        main()
        Poll.init()
    except Exception:
        crashLog("main")
        os._exit(1)
    loop()

# This is the end!