#!/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 " __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 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!