예제 #1
0
    def __init__(self, manager, identity, config):
        self._abortTime = None
        self._timeout = None
        self._xmlAddresses = None
        self._finishInitialization = threading.Event()
        self._initialized = False
        self._monsters = defaultdict(list)
        self._requestQueue = deque()
        self._faxReply = None
        self._delayMode = threading.Event()
        self._delayStart = 0
        self._faxState = None
        self._faxCommands = []
        self._success = None
        self._lastXmlUpdate = 0
        self._lastFaxCheck = 0

        # last request the bot made to FaxBot
        self._lastRequest, self._lastRequestTime = None, None
        # last monster in the fax log
        self._lastFax, self._lastFaxTime = None, None
        # username of last faxer / last time bot got a message from faxbot
        self._lastFaxUname, self._lastFaxBotTime = None, None
        self._lastFaxCheck = 0

        super(FaxModule2, self).__init__(manager, identity, config)
예제 #2
0
 def __init__(self, hbSys=None, **kwargs):
     self.__hb = None
     self.__registered = threading.Lock()
     self.__lock = threading.RLock()
     if hbSys is not None:
         self.heartbeatRegister(hbSys)
     super(HeartbeatSubsystem.HeartbeatCapable, self).__init__(**kwargs)
예제 #3
0
 def __init__(self, name, identity, evSys=None, **kwargs):
     self.__lock = threading.RLock()
     self.__type = name
     self.__id = "__" + identity.lower().strip() + "__"
     self.__ev = None
     self.__registered = threading.Lock()
     if evSys is not None:
         self.eventRegister(evSys)
     super(EventSubsystem.EventCapable, self).__init__(**kwargs)
예제 #4
0
 def __init__(self, queue):
     self._log = logging.getLogger("heartbeat")
     self.queue = queue
     self._stopEvent = threading.Event()
     self.id = str(uuid.uuid4())
     super(HeartbeatSubsystem._HeartbeatTaskThread,
           self).__init__(name="Heartbeat-Task")
예제 #5
0
    def __init__(self, parent, identity, iData, config):
        """ Initialize the BaseClanDungeonChannelManager """
        self.__eventLock = threading.RLock()  # lock for reading events
        # LOCK THIS BEFORE
        # LOCKING self._syncLock
        self.__initialized = False
        self.__lastEvents = None
        self._lastEventCheck = 0
        self._logEntryDb = []
        printDbLoad = False
        if self._csvFile is not None:
            self._logEntryDb = database.csvDatabase(self._csvFile)
            printDbLoad = True

        with self.__raidlogDownloadLock:
            if self.__initialRaidlog is None:
                rl = ClanRaidLogRequest(iData.session)
                result = tryRequest(rl,
                                    numTries=5,
                                    initialDelay=0.5,
                                    scaleFactor=2)
                self.__initialRaidlog = result
        self.__lastEvents = self.__initialRaidlog

        super(BaseClanDungeonChannelManager,
              self).__init__(parent, identity, iData, config)
        if printDbLoad:
            self._log.info("Loaded {} entries of data from {}.".format(
                len(self._logEntryDb), self._csvFile))
        self.__initialized = True
예제 #6
0
 def __init__(self, numThreads, period, stopEvent):
     self._log = logging.getLogger("heartbeat")
     self._n = numThreads
     self._t = period
     self._stopEvent = stopEvent
     self.queue = Queue.Queue()
     self._objs = []
     self._lock = threading.RLock()
     self._threads = []
     super(HeartbeatSubsystem._HeartbeatMainThread,
           self).__init__(name="Heartbeat-Main")
예제 #7
0
 def __init__(self, session, props, invMan, db):
     self._lastOnlineCheck = 0
     self._lastCanReceiveItemsCheck = 0
     self._lastCanReceiveItems = False
     self._db = db
     self._receivedMessages = defaultdict(list)
     self._clearedItems = dict((iid, False) for iid in _doNotSendItems)
     logConfig.setFileHandler("mail-handler", 'log/mailhandler.log')
     self._log = logging.getLogger("mail-handler")
     self._log.info("---- Mail Handler startup ----")
     if self._db.version > 1:
         raise Exception("MailHandler cannot use database version {}"
                         .format(self._db.version))
     self._s = session
     self._m = MailboxManager(session)
     self._props = props 
     self._invMan = invMan
     self.__lock = threading.RLock()
     self._event = threading.Event() # set when thread should do something
     self._stopEvent = threading.Event()
     self._name = db.createMailTransactionTable()
     self._initialize()
     self._event.set()
     super(MailHandler, self).__init__(name="MailHandler")
예제 #8
0
class EventSubsystem(object):
    """ The class that handles the event subsystem.
    
    To use the event subsystem in your class, derive from 
    EventSubsystem.EventCapable. See full documentation in that class.
    """
    
    _lock = threading.RLock()
    class EventException(Exception):
        pass
    
    
    class DuplicateObjectException(Exception):
        pass

    
    class __EventObject(object):
        def __init__(self, obj, typeString, identityString, callback):
            self.obj = weakref.ref(obj)
            self.type = typeString.lower().strip()
            self.id = identityString.lower().strip()
            self.callback = callback

    
    class EventCapable(EmptyObject):
        """An event-capable class has two "names": a name, which 
        describes its class, and an identity, which is a string unique to each
        instance. Event-capable classes can be registered to one 
        EventSubsystem instance at maximum. Registration can either be done 
        automatically in __init__() or using the eventRegister() method. The 
        eventUnregister() method removes the instance from its EventSubsystem. 
        
        To raise an event, use the _raiseEvent() method. Each event has a 
        subject, which should be a string. A receiver can be specified, 
        meaning only a matching object will be notified of the event. To match
        by type name, simply specify the type name as the receiver. To match 
        by identity, use __identity__, where identity is the identity name of 
        the object (for example, "__system__"). If the receiver is None, the 
        event will be broadcast to all objects registered to the 
        EventSubsystem. Events also have a data attribute, which is a dict
        that may hold additional information.
            
        When an event is raised and received by an event-capable class, the
        _eventCallback() method is called, with an EventData struct as the 
        argument. The programmer should check the subject of the event to see 
        if it should be processed. Optionally, the object can send a reply back
        to the object that raised the event using the _eventReply() method. 
        This does not trigger an event; instead, the EventSubsystem collects 
        all replies into a list, which is returned when _raiseEvent() has 
        completed. Each _eventReply() call will create an EventData object in
        the returned list. _eventReply() should ONLY be called inside
        the _eventCallback() function.
        
        It is possible to raise an event inside another event. Events are 
        tracked in a stack. Event behavior is single-threaded.
        
        Be careful raising events inside threads. The Event Subsystem uses
        internal locking, so be careful not to cause any deadlocks.
        """

        def __init__(self, name, identity, evSys=None, **kwargs):
            self.__lock = threading.RLock()
            self.__type = name
            self.__id = "__" + identity.lower().strip() + "__"
            self.__ev = None
            self.__registered = threading.Lock()
            if evSys is not None:
                self.eventRegister(evSys)
            super(EventSubsystem.EventCapable, self).__init__(**kwargs)
                
        
        def __del__(self):
            if self.__ev is not None:
                self.eventUnregister()
                
                
        def __eventCallback(self, eData):
            self._eventCallback(eData)

        
        @property
        def eventSubsystem(self):
            return self.__ev
                
            
        def eventRegister(self, evSubsystem):
            if not self.__registered.acquire(False):
                raise EventSubsystem.EventException(
                        "Object {} ({}) is already assigned"
                        " to an event subsystem."
                        .format(self.__id, self.__type))
            with self.__lock:
                evSubsystem.registerObject(
                        self, self.__type, self.__id, self.__eventCallback)
                self.__ev = evSubsystem
            
            
        def eventUnregister(self):
            with self.__lock:
                if self.__ev is not None:
                    self.__ev.unregisterObject(self)
                    self.__ev = None
                    try:
                        self.__registered.release()
                    except threading.ThreadError:
                        pass
            
            
        def _raiseEvent(self, subject, receiver=None, data=None, **kwargs):
            if data is None:
                data = {}
            if self.__ev is None:
                raise EventSubsystem.EventException(
                    "Object {} ({}) has no event subsystem."
                    .format(self.__id, self.__type))
            return self.__ev.raiseEvent(
                    self.__type, self.__id, receiver, subject, data)
        
        
        def _eventReply(self, data=None, **kwargs):
            if data is None:
                data = {}
            if self.__ev is None:
                raise EventSubsystem.EventException(
                        "Object {} ({}) has no event subsystem."
                        .format(self.__id, self.__type))
            self.__ev.eventReply(self.__type, self.__id, data)
            
            
        def _eventCallback(self, eData):
            pass
        
        
    def __init__(self):
        self._eventObjects = []
        self._replyStack = []
        self._eventStack = []
        self._log = logging.getLogger("events")
        
        
    def _clearDead(self):
        with self._lock:
            self._eventObjects = [eo for eo in self._eventObjects 
                                  if eo.obj() is not None]
        
    
    def registerObject(self, obj, typeString, identityString, callback):
        with self._lock:
            newEO = self.__EventObject(
                    obj, typeString, identityString, callback)
            if any(True for eo in self._eventObjects if eo.id == newEO.id):
                raise EventSubsystem.DuplicateObjectException(
                        "An object with identity {} is already registered."
                        .format(newEO.id))
            if any(True for eo in self._eventObjects if eo.obj() is obj):
                raise EventSubsystem.DuplicateObjectException(
                        "Duplicate object {} ({}) registered."
                        .format(newEO.id, newEO.type))
            self._eventObjects.append(newEO)
#            print("Registered object {!s} with event subsystem."
#                  .format(obj))

        
    def unregisterObject(self, obj):
        with self._lock:
            self._clearDead()
            if obj is not None:
                oldSize = len(self._eventObjects)
                self._eventObjects = [
                        eo for eo in self._eventObjects 
                        if eo.obj() is not obj
                        and obj is not None]
                sizeDiff = oldSize - len(self._eventObjects)
                if sizeDiff == 0:
                    raise ValueError("Object {!s} is not registered."
                                     .format(obj))
                elif sizeDiff > 1:
                    raise Exception(
                          "Internal error: duplicate objects {!s} "
                          "detected in event registry.".format(obj))


    def raiseEvent(self, senderType, senderId, receiver, 
                   subject, data):
        with self._lock:
            if subject is None:
                raise self.EventException("Null event not allowed.")
            e = EventData(senderType, senderId, receiver, subject, data)
            eventDepth = len(self._eventStack)
            self._log.debug("{}Event raised: {}"
                            .format("  " * eventDepth,e))
            self._clearDead()
            self._eventStack.append(senderId)
            self._replyStack.append([])
            if receiver is not None:
                receiver = receiver.lower().strip()
            for eventObj in self._eventObjects:
                if (receiver is None or 
                        receiver == eventObj.type or receiver == eventObj.id):
                    o = eventObj.obj()
                    if o is not None:
                        # may modify self._replies
                        reply = eventObj.callback(e)
                        if reply is not None:
                            self._log.warning("Object {} returned a value "
                                              "from its event callback. The "
                                              "_eventReply method should be "
                                              "used to communicate with the "
                                              "calling object."
                                              .format(o))
            replies = self._replyStack.pop()
            self._eventStack.pop()
            return replies
        
    
    def eventReply(self, rType, rId, data):
        with self._lock:
            if not self._eventStack:
                raise EventSubsystem.EventException(
                        "Can't reply outside of event")
            eventDepth = len(self._eventStack)
            self._log.debug("{}Received reply: {} ({}): {}"
                            .format("  " * eventDepth, rId, rType, data))
            self._replyStack[-1].append(EventData(
                        rType, rId, self._eventStack[-1], "reply", data))
예제 #9
0
class BaseClanDungeonChannelManager(MultiChannelManager):
    """ Subclass of MultiChannelManager that incorporates Dungeon chat 
    and Raid Logs. Subclasses of this manager should specialize these functions
    for individual channel. Modules under this manager are initialized with 
    event data from the clan raid log and also receive periodic updates.
    Dungeon chat is separated and passed to the process_dungeon extended call,
    and log data is passed there as well. Periodic log updates are processed
    as well. 
    """

    capabilities = set(['chat', 'inventory', 'admin', 'hobopolis', 'dread'])

    _csvFile = None  # override this in child classes

    __initialRaidlog = None
    __raidlogDownloadLock = threading.RLock()
    _lastChatNum = None
    delay = 300

    def __init__(self, parent, identity, iData, config):
        """ Initialize the BaseClanDungeonChannelManager """
        self.__eventLock = threading.RLock()  # lock for reading events
        # LOCK THIS BEFORE
        # LOCKING self._syncLock
        self.__initialized = False
        self.__lastEvents = None
        self._lastEventCheck = 0
        self._logEntryDb = []
        printDbLoad = False
        if self._csvFile is not None:
            self._logEntryDb = database.csvDatabase(self._csvFile)
            printDbLoad = True

        with self.__raidlogDownloadLock:
            if self.__initialRaidlog is None:
                rl = ClanRaidLogRequest(iData.session)
                result = tryRequest(rl,
                                    numTries=5,
                                    initialDelay=0.5,
                                    scaleFactor=2)
                self.__initialRaidlog = result
        self.__lastEvents = self.__initialRaidlog

        super(BaseClanDungeonChannelManager,
              self).__init__(parent, identity, iData, config)
        if printDbLoad:
            self._log.info("Loaded {} entries of data from {}.".format(
                len(self._logEntryDb), self._csvFile))
        self.__initialized = True

    def _configure(self, config):
        """ Additional configuration for the log_check_interval option """
        super(BaseClanDungeonChannelManager, self)._configure(config)
        try:

            self.delay = min(self.delay,
                             int(config.setdefault('log_check_interval', 15)))
        except ValueError:
            raise Exception("Error in module config: "
                            "(BaseClanDungeonChannelManager) "
                            "log_check_interval must be integral")

    def _moduleInitData(self):
        """ The initData here is the last read raid log events. """
        d = self._filterEvents(self.lastEvents)
        d['event-db'] = self._logEntryDb
        return d

    @property
    def lastEvents(self):
        """ get the last-read events """
        with self.__eventLock:
            return copy.deepcopy(self.__lastEvents)

    @lastEvents.setter
    def lastEvents(self, val):
        with self.__eventLock:
            self.__lastEvents = copy.deepcopy(val)

    @lastEvents.deleter
    def lastEvents(self):
        with self.__eventLock:
            del self.__lastEvents

    def _getRaidLog(self, noThrow=True, force=False):
        """ Access the raid log and store it locally """
        with self.__raidlogDownloadLock:
            if not self.__initialized and not force:
                return self.lastEvents
            self._log.debug("Reading clan raid logs...")
            rl = ClanRaidLogRequest(self.session)
            result = tryRequest(rl,
                                nothrow=noThrow,
                                numTries=5,
                                initialDelay=0.5,
                                scaleFactor=2)
            if result is None:
                self._log.warning("Could not read clan raid logs.")
                return self.lastEvents
            with self._syncLock:
                self._raiseEvent("new_raid_log", None, LogDict(result))
            return result

    def _updateLogs(self, force=False):
        """ Read new logs and parse them if it is time. Then call the
        process_log extended call of each module. """
        with self.__raidlogDownloadLock:
            if time.time() - self._lastEventCheck >= self.delay or force:
                result = self._getRaidLog()
                return result
            return self.lastEvents

    def _processDungeonChat(self, msg, checkNum):
        """
        This function is called when messages are received from Dungeon. 
        Like parseChat, the value checkNum is identical for chats received 
        at the same time.
        """
        replies = []
        with self.__raidlogDownloadLock:
            if self._lastChatNum != checkNum:
                # get new events
                self._lastChatNum = checkNum
                evts = self._updateLogs(force=True)
            else:
                evts = self.lastEvents
            with self.__eventLock:
                raidlog = self._filterEvents(evts)
                with self._syncLock:
                    txt = msg['text']
                    for m in self._modules:
                        mod = m.module
                        printStr = mod.extendedCall('process_dungeon', txt,
                                                    raidlog)
                        if printStr is not None:
                            replies.extend(printStr.split("\n"))
                    self._syncState()
        return replies

    def parseChat(self, msg, checkNum):
        """ Override of parseChat to split dungeon chat off from "regular"
        chat """
        if self._chatApplies(msg, checkNum):
            if msg['userName'].lower() == 'dungeon' and msg['userId'] == -2:
                return self._processDungeonChat(msg, checkNum)
            else:
                return self._processChat(msg, checkNum)
        return []

    def cleanup(self):
        with self.__eventLock:
            self.__initialized = False
        MultiChannelManager.cleanup(self)

    def _heartbeat(self):
        """ Update logs in heartbeat """
        if self.__initialized:
            self._updateLogs()
            super(BaseClanDungeonChannelManager, self)._heartbeat()

    def _eventCallback(self, eData):
        MultiChannelManager._eventCallback(self, eData)
        if eData.subject == "new_raid_log":
            raidlog = dict((k, v) for k, v in eData.data.items())
            self.lastEvents = raidlog
            if not self.__initialized:
                return
            self._notifyModulesOfNewRaidLog(raidlog)
        elif eData.subject == "request_raid_log":
            if self.lastEvents is not None:
                self._eventReply(LogDict(self.lastEvents))

    def _notifyModulesOfNewRaidLog(self, raidlog):
        # it's important not to process the log while responding
        # to chat, so we need a lock here.
        if not self.__initialized:
            return

        # important to keep this ordering for the locks
        with self.__eventLock:
            with self._syncLock:
                self._log.debug("{} received new log".format(self.identity))
                self._lastEventCheck = time.time()
                filteredLog = self._filterEvents(raidlog)
                self._handleNewRaidlog(filteredLog)
                for m in self._modules:
                    mod = m.module
                    mod.extendedCall('process_log', filteredLog)
                self._syncState()

    def _dbMatchRaidLog(self, raidlog):
        try:
            eventList = []
            for e in raidlog['events']:
                e['db-match'] = {}
                for dbEntry in self._logEntryDb:
                    if dbEntry['category'].strip() == e['category']:
                        if re.search(dbEntry['regex'], e['event']):
                            if not e['db-match']:
                                e['db-match'] = dbEntry
                            else:
                                raise KeyError("Duplicate match in database: "
                                               "event {} matches '{}' and "
                                               "'{}'".format(
                                                   e['event'],
                                                   e['db-match']['regex'],
                                                   dbEntry['regex']))
                eventList.append(e)
            raidlog['events'] = eventList
            return raidlog
        except Exception:
            print(raidlog)
            raise

############# Override the following:

    def _filterEvents(self, raidlog):
        """ This function is used by subclasses to remove unrelated event
        information. """
        return self._dbMatchRaidLog(raidlog)

    def _handleNewRaidlog(self, raidlog):
        """ This function is called when new raidlogs are downloaded. """
        pass

    @abc.abstractmethod
    def active(self):
        pass
예제 #10
0
파일: BotSystem.py 프로젝트: zeryl/RUcwbot
    def __init__(self, s, c, props, inv, configFile, db, exitEvent):
        """ Initialize the BotSystem """

        self._exitEvent = exitEvent

        self._initialized = False

        # trigger to stop heartbeat subsystem
        self._hbStop = threading.Event()

        # start subsystems
        try:
            oldTxt = None
            hbSys = HeartbeatSubsystem(numThreads=6,
                                       period=5,
                                       stopEvent=self._hbStop)
            evSys = EventSubsystem()

            # initialize subsystems
            super(BotSystem, self).__init__(name="sys.system",
                                            identity="system",
                                            evSys=evSys,
                                            hbSys=hbSys)

            if exitEvent.is_set():
                sys.exit()

            # copy arguments
            self._s = s
            self._c = c
            self._props = props
            self._inv = inv
            self._db = db
            self._log = logging.getLogger()

            # initialize some RunProperties data now that we are logged on
            self._log.debug("Getting my userId...")
            r1 = StatusRequest(self._s)
            d1 = tryRequest(r1)
            self._props.userId = int(d1['playerid'])
            self._log.info("Getting my clan...")
            r2 = UserProfileRequest(self._s, self._props.userId)
            d2 = tryRequest(r2)
            self._props.clan = d2.get('clanId', -1)
            self._log.info("I am a member of clan #{} [{}]!".format(
                self._props.clan, d2.get('clanName', "Not in any clan")))

            # config file stuff
            self._config = None
            self._overWriteConfig = False
            oldTxt = self._loadConfig(configFile)

            # listen to channels
            self._initializeChatChannels(self._config)
            c.getNewChatMessages()  # discard old PM's and whatnot

            # initialize directors
            self._dir = None
            iData = InitData(s, c, props, inv, db)
            self._log.info("Starting communication system...")
            self._dir = CommunicationDirector(self, iData,
                                              self._config['director'])
            self._lastCheckedChat = 0
            self._initialized = True
        except:
            self._hbStop.set()
            raise
        finally:
            # rewrite config file (values may be modified by modules/managers)
            if oldTxt is not None:
                self._saveConfig(configFile, oldTxt)
예제 #11
0
def main(curFolder=None, connection=None):
    import __main__
    if curFolder == None:
        curFolder = os.path.dirname(os.path.abspath(inspect.getfile(__main__)))
    props = processArgv(sys.argv, curFolder)  # set current folder
    props.connection = connection
    crashWait = 60
    myDb = Database(databaseName)
    loginWait = 0
    log = logging.getLogger()
    logging.getLogger("requests").setLevel(logging.INFO)
    logging.getLogger("urllib3").setLevel(logging.INFO)

    # register signals
    signal.signal(signal.SIGTERM, signalHandler)
    signal.signal(signal.SIGINT, signalHandler)
    try:
        # windows signals
        signal.signal(signal.CTRL_C_EVENT, signalHandler)
        signal.signal(signal.CTRL_BREAK_EVENT, signalHandler)
    except:
        pass

    try:
        while loginWait >= 0 and not exitEvent.is_set():
            ### LOGIN LOOP ###
            if loginWait > 0:
                log.info("Sleeping for {} seconds.".format(loginWait))
                time.sleep(loginWait)
                props.refresh()

            # main section of login loop
            if exitEvent.is_set():
                break
            (loginWait, fastCrash) = loginLoop(myDb, props)
            if fastCrash:
                # fast crash: perform exponential back-off
                crashWait = min(2 * 60 * 60, crashWait * 2)
                loginWait = crashWait
                log.info("New crash wait: {}".format(crashWait))
            else:
                # reset exponential back-off
                crashWait = 60
            _reset_traceback()
    except:
        raise
    finally:
        # close a bunch of stuff
        log.info("Main thread done.")

        # wait for other threads to close
        threads = [t for t in threading.enumerate() if t.daemon == False]
        numThreads = len(threads)
        delayTime = 0
        log.info("Waiting for threads to close: {}".format(threads))
        while numThreads > 1:
            try:
                time.sleep(delayTime)
                delayTime = 0.25
                if props is not None:
                    try:
                        props.close()
                    except:
                        pass
                threads = [
                    t for t in threading.enumerate() if t.daemon == False
                ]
                numThreadsNew = len(threads)
                if numThreadsNew != numThreads:
                    log.debug(
                        "Waiting for threads to close: {}".format(threads))
                numThreads = numThreadsNew
            except:
                pass

        log.info("-------- System Shutdown --------\n")
        if loginWait == -2:
            if props.connection is not None:
                props.connection.send("restart")
            else:
                # restart program
                log.info("Invoking manual restart.")
                python = sys.executable
                os.execl(python, python, *sys.argv)
        else:
            if props.connection is not None:
                props.connection.send("stop")

        try:
            props.connection.close()
        except Exception:
            pass
예제 #12
0
import cwbot.util.DebugThreading as threading
from cwbot.util.DebugThreading import _reset_traceback
from cwbot.processArgv import processArgv
from cwbot.sys.BotSystem import BotSystem
from cwbot.common.exceptions import ManualException, ManualRestartException, \
                                    FatalError
from cwbot.kolextra.manager.ChatManager import ChatManager
from cwbot.kolextra.request.SendMessageRequest import SendMessageRequest
from cwbot.kolextra.manager.InventoryManager import InventoryManager
from cwbot.util.tryRequest import tryRequest
from cwbot.database import database
from kol.Session import Session
import kol.Error
from cwbot.sys.database import Database

exitEvent = threading.Event()
databaseName = 'data/cwbot.db'


def openSession(props):
    """ Log in to the KoL servers. """
    log = logging.getLogger()
    s = Session()
    s.login(props.userName, props.password)
    log.info("Logged in.")
    return s


def createChatManager(s):
    """ Open a new chat manager. """
    log = logging.getLogger()
예제 #13
0
class BaseManager(EventSubsystem.EventCapable,
                  HeartbeatSubsystem.HeartbeatCapable):
    """
    Base class for all manager objects. Every subclass MUST impliment a
    capabilities attribute that is a list of strings.
    
    Managers are the middle tier of processing. The CommunicationDirector
    holds many managers, and each manager holds many modules.
    
    The job of a manager is to filter information. The CommunicationDirector
    passes every Kmail and Chat to every manager. Each manager filters this
    information, passing applicable Kmails/Chats to each of its
    modules. Manager filtering should be "all-or-nothing": Managers should
    decide if a Kmail/Chat is applicable, and if so, pass it to each of
    its modules. It is not the job of a manager to determine which of its
    modules should process which chat/kmail. 
    
    It is also the manager's job to handle permissions by checking if a user
    has the required permission before passing chats/kmails to modules. The
    same applies to checking in-clan status.
    
    A manager may also pass supplementary information to its modules,
    by both supplying information via the _moduleInitData method and
    possibly through other methods.
    
    Managers are also in charge of syncing the state of their constituent
    modules by periodically calling _syncState(), which utilizes the sqlite3
    database. 
    """

    __metaclass__ = ManagerMetaClass
    capabilities = ['inventory', 'chat']

    __clanMembers = set([])
    __clanNonMembers = {}

    _syncLock = threading.RLock()  # lock for syncing state

    def __init__(self, parent, identity, iData, config):
        """ Initialize the BaseManager. When you call this from a
        derived class, the following occurs:
        
        1. The manager is linked to the Heartbeat and Event subsystems.
        2. Various variables are established.
        3. The _configure() method is called.
        4. The modules in the config map are added to self._modules.
        5. The _initialize() method is called.
        """

        self._initialized = False
        super(BaseManager, self).__init__(name="sys.{}".format(identity),
                                          identity=identity,
                                          evSys=parent.eventSubsystem,
                                          hbSys=parent.heartbeatSubsystem)
        self.__configureOnline = False
        self.__initializeOnline = False
        self._s = iData.session
        self._c = iData.chatManager
        logConfig.setFileHandler(identity, "log/{}.log".format(identity))
        self._log = logging.getLogger(identity)
        self._log.info("----- Manager {} startup -----".format(identity))
        self._invMan = iData.inventoryManager
        self._props = iData.properties
        self._db = iData.database
        self.identity = identity
        self.syncTime = 300
        self._lastSync = time.time()

        self._db.createStateTable()
        self._persist = self._db.loadStateTable(self.identity)

        self._modules = []
        self.__parent = weakref.ref(parent)

        self._configure(config)
        self._addModules(config)
        self._initialize()
        self._initialized = True

    def _configure(self, config):
        """ 
        Perform configuration of the Manager. This should be overridden in
        derived classes. But be sure to call its parent's _configure() method
        too. Otherwise, self.syncTime will be set to 300. """
        try:
            self.syncTime = config['sync_interval']
        except ValueError:
            raise Exception("sync_interval must be integral")

    def _addModules(self, config):
        """ Dynamically import the modules specified in modules.ini. This
        should not be overridden. """
        base = config['base']

        # loop through modules
        for k, v in config.items():
            if isinstance(v, dict):
                cfg = v
                perm = toTypeOrNone(v['permission'], str)
                priority = v['priority']
                clanOnly = v['clan_only']

                # import class
                try:
                    ModuleClass = easyImportClass(base, v['type'])
                except ImportError:
                    raise FatalError("Error importing module/class {0} "
                                     "from base {1}. Either the module does "
                                     "not exist, or there was an error. To "
                                     "check for errors, use the command line "
                                     "'python -m {1}.{0}'; the actual path "
                                     "may vary.".format(v['type'], base))

                self._modules.append(
                    ModuleEntry(ModuleClass, priority, perm, clanOnly, self, k,
                                cfg))

        # sort by decreasing priority
        self._modules.sort(key=lambda x: -x.priority)
        self._log.info("---- {} creating module instances... ----".format(
            self.identity))
        for m in self._modules:
            self._log.info(
                "Creating {0.className} with priority "
                "{0.priority}, permission {0.permission}.".format(m))
            try:
                m.createInstance()
            except TypeError as e:
                self._log.exception("Error!")
                raise FatalError("Error instantiating class {}: {}".format(
                    m.className, e.args[0]))

        self._log.info("---- All modules created. ----")

    def _initialize(self):
        """ Runs after _addModules. If there is additional initialization
        to do, you should override this, but be sure to call the parent's
        _initialize() method to properly initialize the modules. """
        self._log.debug("Initializing...")
        d = self._moduleInitData()
        self._log.debug("Loaded initialization data.")
        with self._syncLock:
            self._log.debug("Checking persistent state...")
            try:
                if len(self._persist) == 0:
                    self._persist['__init__'] = ""
            except ValueError:
                self._clearPersist()
            self._log.debug("Preparing to initialize modules...")
            self._initializeModules(d)
            self._log.debug("Performing initial state sync...")
            self._syncState(force=True)

    def _moduleInitData(self):
        """ This is the initialization data that is passed when initializing
        each module. """
        return {}

    def _initializeModules(self, initData):
        """(Re)initialize processors. If persistent state is present, it is
        loaded and passed to the module's initialize() method; if absent, the
        module's initialState property is used instead. If an error occurs,
        the initialState is used as well and the old state is deleted.
        """
        hbSys = self.heartbeatSubsystem
        with self._syncLock:
            for m in self._modules:
                mod = m.module
                self._log.info("Initializing {} ({}).".format(
                    mod.id, mod.__class__.__name__))
                try:
                    state = self._persist.get(mod.id, None)
                    if state is None:
                        self._log.info("Null state for module {}, using "
                                       "default...".format(mod.id))
                        state = mod.initialState

                    if len(str(state)) > 500:
                        self._log.debug("Initializing module {} ({}) with "
                                        "state {{TOO LONG TO FIT}}".format(
                                            mod.id, mod.__class__.__name__))
                    else:
                        self._log.debug("Initializing module {} ({}) with "
                                        "state {}".format(
                                            mod.id, mod.__class__.__name__,
                                            state))
                    mod.initialize(state, initData)
                except (KeyboardInterrupt, SystemExit, SyntaxError, FatalError,
                        NameError, ImportError, MemoryError,
                        NotImplementedError, SystemError, TypeError):
                    raise
                except Exception as e:
                    self._log.exception("ERROR initializing module "
                                        "with persistent state")
                    mod.initializationFailed(state, initData, e)
                mod.heartbeatRegister(hbSys)
            self._log.info("---- Finished initializing modules ----")

    def _clearPersist(self):
        """ Remove all persistent state data. Note that states are
        periodically synced, so if you don't also reset each module, this will
        essentially do nothing. """
        with self._syncLock:
            self._db.updateStateTable(self.identity, {}, purge=True)
            self._persist = self._db.loadStateTable(self.identity)

    def _syncState(self, force=False):
        ''' Store persistent data for Modules in the database. '''
        with self._syncLock:
            if self._persist is None:
                return
            for m in self._modules:
                mod = m.module
                self._persist[mod.id] = mod.state
            if time.time() - self._lastSync > self.syncTime or force:
                self._log.debug("Syncing state for {}".format(self.identity))
                try:
                    self._db.updateStateTable(self.identity, self._persist)
                except Exception as e:
                    for k, v in self._persist.items():
                        try:
                            encode([k, v])  # check if JSON error
                        except:
                            raise ValueError("Error encoding state {} for "
                                             "module {}: {}".format(
                                                 v, k, e.args))
                    raise
                self._lastSync = time.time()

    def checkClan(self, uid):
        """ Check if a user is in the same clan as the bot or if they are on
        the whitelist. Returns {} if user is not in clan. Otherwise returns
        the user record, a dict with keys 'userId', 'userName', 'karma', 
        'rankName', 'whitelist', and 'inClan'. Note that 'karma' will be zero
        if the user is whitelisted and outside the clan (which will be
        indicated by the 'inClan' field equal to False). """
        if uid <= 0:
            return {
                'inClan': True,
                'userId': uid,
                'rankName': 'SPECIAL',
                'userName': str(uid),
                'karma': 1,
                'whitelist': False
            }
        info = self.director.clanMemberInfo(uid)
        return info

    def cleanup(self):
        """ run cleanup operations before bot shutdown. This MUST be called
        before shutting down by the CommunicationDirector. """
        with self._syncLock:
            self._log.info("Cleaning up manager {}...".format(self.identity))
            self._log.debug("Cleanup: syncing states...")
            self._syncState(force=True)
            self._initialized = False
            self._persist = None
        for m in reversed(self._modules):
            mod = m.module
            self._log.debug(
                "Cleanup: unregistering heartbeat for {}...".format(mod.id))
            mod.heartbeatUnregister()
            self._log.debug("Cleanup: unregistering events for {}...".format(
                mod.id))
            mod.eventUnregister()
            self._log.debug("Cleanup: cleaning up module {}...".format(mod.id))
            mod.cleanup()
        self._log.debug("Unregistering heartbeat...")
        self.heartbeatUnregister()
        self._log.debug("Unregistering events...")
        self.eventUnregister()
        self._log.info("Done cleaning up manager {}".format(self.identity))
        self._modules = None
        self._log.info("----- Manager shut down. -----\n")

    @property
    def director(self):
        """ Get a reference to the CommunicationDirector. """
        parent = self.__parent()
        if parent is not None:
            return parent
        return None

    @property
    def session(self):
        """ Get the current session (for pyKol requests). """
        return self._s

    @property
    def properties(self):
        """ Get the current RunProperties (load various information) """
        return self._props

    @property
    def inventoryManager(self):
        """ Get the current InventoryManager """
        return self._invMan

    @property
    def chatManager(self):
        return self._c

    def defaultChannel(self):
        """ Get the default chat channel for this manager. May be overridden
        in derived classes. If no channel is specified in sendChatMessage(),
        self.defaultChannel is used. By default, this uses the current
        chat channel (i.e., not the "listened" channels, the main one). """
        return self.chatManager.currentChannel

    def sendChatMessage(self,
                        text,
                        channel=None,
                        waitForReply=False,
                        raw=False):
        """ Send a chat message with specified text. If no channel is
        specified, self.defaultChannel is used. If waitForReply is true, the
        chatManager will block until response data is loaded; otherwise, the
        chat is sent asynchronously and no response is available. If raw is
        true, the chat is sent undecorated; if false, the chat is sanitized
        to avoid /command injections and is decorated in emote format. """
        if channel is None:
            channel = self.defaultChannel()
        if channel is None or channel == "DEFAULT":
            channel = self.chatManager.currentChannel

        useEmote = not raw
        return self.director.sendChat(channel, text, waitForReply, useEmote)

    def whisper(self, uid, text, waitForReply=False):
        """ Send a private message to the specified user. """
        return self.director.whisper(uid, text, waitForReply)

    def sendKmail(self, message):
        """ Send a Kmail that is not a reply. message should be a Kmail object
        from the common.kmailContainer package. """
        self.director.sendKmail(message)

    def parseChat(self, msg, checkNum):
        """ This function is called by the CommunicationDirector every time
        a new chat is received. The manager can choose to ignore the chat or
        to process it. To ignore the chat, just return []. To process it, pass
        the chat to each module and return a LIST of all the replies that 
        are not None. """
        return []

    def parseKmail(self, msg):
        """ Parse Kmail and return any replies in a LIST of KmailResponses
        in the same fashion as the parseChat method. """
        return []

    def kmailFailed(self, module, message, exception):
        """ This is called by the CommunicationDirector if a kmail fails 
        to send for some reason. """
        if module is not None:
            module.extendedCall('message_send_failed', message, exception)

    def _heartbeat(self):
        """ By default, the heartbeat calls syncState(), so in derived classes
        be sure to do that too or call the parent _heartbeat(). """
        if self._initialized:
            self._syncState()
예제 #14
0
파일: main.py 프로젝트: ijzer/cwbot-ndy
def main(curFolder=None, connection=None):
    import __main__
    if curFolder == None:
        curFolder = os.path.dirname(os.path.abspath(inspect.getfile(__main__)))
    props = processArgv(sys.argv, curFolder) # set current folder
    props.connection = connection
    crashWait = 60
    myDb = Database(databaseName) 
    loginWait = 0
    log = logging.getLogger()
    logging.getLogger("requests").setLevel(logging.INFO)
    logging.getLogger("urllib3").setLevel(logging.INFO)

    # register signals
    signal.signal(signal.SIGTERM, signalHandler)
    signal.signal(signal.SIGINT, signalHandler)
    try:
        # windows signals
        signal.signal(signal.CTRL_C_EVENT, signalHandler)
        signal.signal(signal.CTRL_BREAK_EVENT, signalHandler)
    except:
        pass
    
    try:
        while loginWait >= 0 and not exitEvent.is_set():
            ### LOGIN LOOP ###
            if loginWait > 0:
                log.info("Sleeping for {} seconds.".format(loginWait))
                time.sleep(loginWait)
                props.refresh()
            
            # main section of login loop
            if exitEvent.is_set():
                break
            (loginWait, fastCrash) = loginLoop(myDb, props)
            if fastCrash:
                # fast crash: perform exponential back-off
                crashWait = min(2*60*60, crashWait*2)
                loginWait = crashWait
                log.info("New crash wait: {}".format(crashWait))
            else:
                # reset exponential back-off
                crashWait = 60
            _reset_traceback()
    except:
        raise
    finally:
        # close a bunch of stuff
        log.info("Main thread done.")
        
        # wait for other threads to close
        threads = [t for t in threading.enumerate() if t.daemon == False]
        numThreads = len(threads)
        delayTime = 0
        log.info("Waiting for threads to close: {}".format(threads))
        while numThreads > 1:
            try:
                time.sleep(delayTime)
                delayTime = 0.25
                if props is not None:
                    try:
                        props.close()
                    except:
                        pass
                threads = [t for t in threading.enumerate() 
                           if t.daemon == False]
                numThreadsNew = len(threads)
                if numThreadsNew != numThreads:
                    log.debug("Waiting for threads to close: {}"
                             .format(threads))
                numThreads = numThreadsNew
            except:
                pass

        log.info("-------- System Shutdown --------\n")            
        if loginWait == -2:
            if props.connection is not None:
                props.connection.send("restart")
            else:
                # restart program
                log.info("Invoking manual restart.")
                python = sys.executable
                os.execl(python, python, *sys.argv)
        else:
            if props.connection is not None:
                props.connection.send("stop")

        try:
            props.connection.close()
        except Exception:
            pass
예제 #15
0
class FaxModule2(BaseChatModule):
    """ 
    A module that handles faxing, including fax lookup for unknown monster
    codes, reporting on incoming faxes, and reporting what's in the fax
    machine.
    
    Configuration options:
    faxbot_timeout - time to wait until giving up on a fax request [def. = 90]
    url_timeout - time to try to load XML page before timing out [def. = 15]
    [[[[xml]]]]
        BOTNAME = URL_TO_XML
    [[[[alias]]]]
        ALIASNAME = ALIAS (monster alias name)
        
    Configuration example:
    [[[[xml]]]]
        1 = http://hogsofdestiny.com/faxbot/faxbot.xml
        2 = http://faust.kolbots.com/faustbot.xml
        3 = https://sourceforge.net/p/easyfax/code/HEAD/tree/Easyfax.xml?format=raw
    [[[[success]]]]
        FaxBot = has copied
        faustbot = has been delivered
        Easyfax = fax is ready
    [[[[alias]]]]
        lobsterfrogman = lfm    # now you can type '!fax lfm' 
    """

    requiredCapabilities = ['chat']
    _name = "fax"

    __lock = threading.RLock()
    _faxWait = 60
    _xmlMins = 30
    _checkFrequency = 15

    _defaultXml = {
        '1':
        "http://faust.kolbots.com/faustbot.xml",
        '2':
        "https://sourceforge.net/p/easyfax/"
        "code/HEAD/tree/Easyfax.xml?format=raw"
    }
    _defaultSuccess = {
        'faustbot': "has been delivered",
        'Easyfax': "fax is ready"
    }

    def __init__(self, manager, identity, config):
        self._abortTime = None
        self._timeout = None
        self._xmlAddresses = None
        self._finishInitialization = threading.Event()
        self._initialized = False
        self._monsters = defaultdict(list)
        self._requestQueue = deque()
        self._faxReply = None
        self._delayMode = threading.Event()
        self._delayStart = 0
        self._faxState = None
        self._faxCommands = []
        self._success = None
        self._lastXmlUpdate = 0
        self._lastFaxCheck = 0

        # last request the bot made to FaxBot
        self._lastRequest, self._lastRequestTime = None, None
        # last monster in the fax log
        self._lastFax, self._lastFaxTime = None, None
        # username of last faxer / last time bot got a message from faxbot
        self._lastFaxUname, self._lastFaxBotTime = None, None
        self._lastFaxCheck = 0

        super(FaxModule2, self).__init__(manager, identity, config)

    def _configure(self, config):
        try:
            self._abortTime = int(config.setdefault('faxbot_timeout', 90))
            self._timeout = int(config.setdefault('url_timeout', 15))
            self._xmlAddresses = config.setdefault('xml', self._defaultXml)
            success = config.setdefault('success', self._defaultSuccess)
            self._success = {''.join(k.lower()): v for k, v in success.items()}
        except ValueError:
            raise Exception("Fax Module config error: "
                            "faxbot_timeout, url_timeout must be integral")
        self._alias = config.setdefault('alias', {'lobsterfrogman': 'lfm'})

    def initialize(self, state, initData):
        self._finishInitialization.set()

    @property
    def state(self):
        return {}

    @property
    def initialState(self):
        return {}

    def getFaxMatch(self, args):
        '''Look up the monster in the list using fuzzy matching. '''
        splitArgs = args.split()

        # did we force?
        if any(s for s in splitArgs if s.strip().lower() == "force"):
            # make a new monster
            return _FaxMatch(splitArgs[0], True, 'forcing')

        # make list of all possible names/codes/aliases
        nameList = {}
        for k, v in self._monsters.items():
            names = set(chain.from_iterable(val.nameList for val in v))
            nameList[k] = names

        simplify = (lambda x: x.replace("'", "").replace("_", " ").replace(
            "-", " ").lower())
        sArgs = simplify(args)

        # first, check for exact code/name/alias matches
        matches = []
        for k, names in nameList.items():
            if any(True for name in names if sArgs == simplify(name)):
                matches.append(k)
        if len(matches) == 1:
            return _FaxMatch(matches[0], False, 'exact match')

        # next, check for "close" matches
        scoreDiff = 15
        scores = {}
        for k, names in nameList.items():
            score1 = max(
                fuzz.partial_token_set_ratio(simplify(name), sArgs)
                for name in names)
            scores[k] = score1
        maxScore = max(scores.values())
        fuzzyMatchKeys = set(k for k, v in scores.items()
                             if v >= maxScore - scoreDiff)

        # also check for args as a subset of string or code
        detokenize = lambda x: ''.join(re.split(r"'|_|-| ", x)).lower()
        dArgs = detokenize(args)

        subsetMatchKeys = set()
        for k, names in nameList.items():
            if any(True for name in names if dArgs in detokenize(name)):
                subsetMatchKeys.add(k)

        ls = len(subsetMatchKeys)
        lf = len(fuzzyMatchKeys)
        matchKeys = subsetMatchKeys | fuzzyMatchKeys
        lm = len(matchKeys)

        if ls == 0 and lf == 1:
            m = matchKeys.pop()
            return _FaxMatch(m, False, "fuzzy match")
        elif lm == 1:
            m = matchKeys.pop()
            return _FaxMatch(m, False, "subset match")
        elif lm > 1 and lm < 6:
            possibleMatchStr = ", ".join(
                (self._monsters[k][0].name for k in matchKeys))
            return _FaxMatch(
                None, False,
                ("Did you mean one of: {}?".format(possibleMatchStr)))
        elif lm > 1:
            return _FaxMatch(None, False,
                             ("Matched {} monster names/codes; "
                              "please be more specific. Send \"!fax list\" for"
                              " monster list.".format(ls + lf)))

        return _FaxMatch(
            None, False, "No known monster with name/code "
            "matching '{0}'. "
            "Use '!fax {0} force' to force, "
            "or send \"!fax list\" for a list.".format(args))

    def checkForNewFax(self, announceInChat=True):
        """ See if a new fax has arrived, and possibly announce it if it has. 
        """
        with self.__lock:
            self._lastFaxCheck = utcTime()
            replyStr = None
            lastFaxTime = self._lastFaxTime
            lastFaxUname = self._lastFaxUname
            lastFaxMonster = self._lastFax
            event = self.updateLastFax()
            if (self._lastFax is not None
                    and (lastFaxTime != self._lastFaxTime
                         or lastFaxUname != self._lastFaxUname
                         or lastFaxMonster != self._lastFax)):
                self.log("Received new fax {}".format(event))
            return replyStr

    def updateLastFax(self):
        """ Update what's in the Fax machine """
        with self.__lock:

            # suppress annoying output from pyKol
            kol.util.Report.removeOutputSection("*")
            try:
                r = ClanLogPartialRequest(self.session)
                log = self.tryRequest(r,
                                      numTries=5,
                                      initialDelay=0.25,
                                      scaleFactor=1.5)
            finally:
                kol.util.Report.addOutputSection("*")
            faxEvents = [
                event for event in log['entries']
                if event['type'] == CLAN_LOG_FAX
            ]
            lastEvent = None if len(faxEvents) == 0 else faxEvents[0]
            if lastEvent is None:
                self._lastFax = None
                self._lastFaxTime = None
                self._lastFaxUname = None
            else:
                self._lastFax = lastEvent['monster']
                lastFaxTimeAz = tz.localize(lastEvent['date'])
                lastFaxTimeUtc = lastFaxTimeAz.astimezone(utc)
                self._lastFaxTime = calendar.timegm(lastFaxTimeUtc.timetuple())
                self._lastFaxUname = lastEvent['userName']
            return lastEvent

    def printLastFax(self):
        """ Get the chat text to represent what's in the Fax machine. """
        if utcTime() - self._lastFaxCheck >= self._checkFrequency:
            self.checkForNewFax(False)
        if self._lastFax is None:
            return "I can't tell what's in the fax machine."
        elapsed = utcTime() - self._lastFaxTime
        timeStr = "{} minutes".format(int((elapsed + 59) // 60))
        return ("The fax has been holding a(n) {} for the last {}. "
                "(Send \"!fax list\" for a list of monsters.)".format(
                    self._lastFax, timeStr))

    def faxMonster(self, args, isPM):
        """Send a request, if not waiting on another request."""
        with self.__lock:
            (monster, force, message) = self.getFaxMatch(args)
            matches = self._monsters.get(monster, [])

            if monster is None or not matches:
                return message

            if isPM:
                str1 = "Matched {} ({})\n".format(matches[0].name, message)
                return str1 + "\n".join(
                    "/w {} {}".format(m.faxbot.name, m.code) for m in matches)

            if self._delayMode.is_set():
                return ("Please wait {} more seconds to request a fax.".format(
                    int(self._faxWait - time.time() + self._delayStart)))
            if self._requestQueue:
                return "I am still waiting on my last request."

            self._requestQueue.extend(matches)
            return "Requested {} ({})...".format(matches[0].name, message)

    def _processCommand(self, message, cmd, args):
        if cmd == "fax":
            if args.lower() == "list":
                return self._sendMonsterList(message['userId'])
            if args != "":
                isPM = (message['type'] == "private")
                return self.faxMonster(args, isPM)
            else:
                return self.printLastFax()
        with self.__lock:
            if self._faxState:
                if message.get('userId', 0) == self._faxState.requestId:
                    self.log("Received {} PM: {}".format(
                        self._faxState.requestId, message['text']))
                    self._faxReply = message['text']
            return None

    def _sendMonsterList(self, uid):
        text = ("Available monsters:\n\n" +
                "\n".join(sorted(self._monsters.keys())))
        self.sendKmail(Kmail(uid, text))
        return "Monster list sent."

    def _refreshMonsterList(self):
        genLen = lambda gen: sum(1 for _ in gen)
        entryCount = genLen(chain.from_iterable(self._monsters.values()))
        self.log("Updating xml... ({} entries)".format(entryCount))
        for _, v in self._monsters.items():
            v = [
                entry for entry in v
                if entry.faxbot.xml in self._xmlAddresses.values()
            ]

        # clear empty entries
        monsters = defaultdict(list)
        monsters.update({k: v for k, v in self._monsters.items() if v})
        self._monsters = monsters

        entryCount2 = genLen(chain.from_iterable(self._monsters.values()))
        if entryCount != entryCount2:
            self._log("Removed {} entries due to config file mismatch.".format(
                entryCount - entryCount2))

        numTries = 3
        for key in sorted(self._xmlAddresses.keys()):
            address = self._xmlAddresses[key]
            txt = None
            for _ in range(numTries):
                try:
                    txt = urlopen(address, timeout=self._timeout).read()
                    d = xmltodict.parse(txt)
                except (HTTPError, URLError, socket.timeout, socket.error,
                        ExpatError) as e:
                    self.log("Error loading webpage "
                             "for fax list: {}: {}".format(
                                 e.__class__.__name__, e.args))
                else:
                    entryCount = genLen(
                        chain.from_iterable(self._monsters.values()))
                    d1 = d[d.keys()[0]]
                    try:
                        faxbot = _Faxbot(d1['botdata']['name'].encode('ascii'),
                                         int(d1['botdata']['playerid']),
                                         address)
                    except KeyError:
                        continue

                    monsters = d1['monsterlist']['monsterdata']
                    newMonsters = {}
                    for monster in monsters:
                        mname = unidecode(monster['actual_name']).lower()
                        code = unidecode(monster['command']).lower()
                        name = unidecode(monster['name'])
                        newMonsters[mname] = FaxMonsterEntry(
                            name, code, faxbot)
                        for n, alias in self._alias.items():
                            if n.lower().strip() in [
                                    mname, code,
                                    name.lower().strip()
                            ]:
                                newMonsters[mname].addAlias(alias)

                    for k, v in self._monsters.items():
                        self._monsters[k] = [
                            entry for entry in v if entry.faxbot.xml != address
                        ]
                    for mname, monster in newMonsters.items():
                        self._monsters[mname].append(monster)
                    entryCount2 = genLen(
                        chain.from_iterable(self._monsters.values()))

                    # clear empty entries
                    monsters = defaultdict(list)
                    monsters.update(
                        {k: v
                         for k, v in self._monsters.items() if v})
                    self._monsters = monsters
                    self.log("Net change of {} entries from {} xml ({} -> {})".
                             format(entryCount2 - entryCount, faxbot.name,
                                    entryCount, entryCount2))
                    break

        self._lastXmlUpdate = time.time()

    def _heartbeat(self):
        if self._finishInitialization.is_set():
            self._finishInitialization.clear()
            self._refreshMonsterList()
            self._initialized = True
        if self._initialized:
            with self.__lock:
                # are we waiting for a request?
                if self._faxState:
                    # check if we received a reply
                    request = self._requestQueue[0]
                    if self._faxReply:
                        # check if it matches
                        regex = self._success[''.join(
                            request.faxbot.name.lower())]

                        if re.search(regex, self._faxReply):
                            # matched!
                            self.chat("{} has delivered a(n) {}.".format(
                                request.faxbot.name, request.name))
                            self._requestQueue.clear()
                            self._delayMode.set()
                            self._delayStart = time.time()
                            self._faxCommands = []
                        else:
                            # not a match.
                            self.chat("{} reply: {}".format(
                                request.faxbot.name, self._faxReply))
                            self._requestQueue.popleft()
                            if not self._requestQueue:
                                self.chat("Could not receive fax. "
                                          "Try one of: {}".format(", ".join(
                                              self._faxCommands)))
                                self._faxCommands = []
                        self._faxReply = None
                        self._faxState = None
                    else:
                        # no fax reply yet
                        if (time.time() - self._faxState.requestTime >
                                self._abortTime):
                            self.chat("{} did not reply.".format(
                                request.faxbot.name))
                            self._requestQueue.popleft()
                            self._faxState = None
                            self._faxReply = None
                            if not self._requestQueue:
                                self.chat("Could not receive fax. "
                                          "Try one of: {}".format(", ".join(
                                              self._faxCommands)))
                                self._faxCommands = []

                elif self._delayMode.is_set():
                    if time.time() - self._delayStart > self._faxWait:
                        self._delayMode.clear()
                        self._delayStart = 0

                elif self._requestQueue:
                    request = self._requestQueue[0]
                    self.chat("Requesting {} from {}...".format(
                        request.name, request.faxbot.name))
                    self._faxState = _FaxState(requestTime=time.time(),
                                               requestId=request.faxbot.id)
                    self.whisper(request.faxbot.id, request.code)
                    self._faxCommands.append("/w {} {}".format(
                        request.faxbot.name, request.code))
                elif time.time() - self._lastXmlUpdate > 60 * self._xmlMins:
                    self._refreshMonsterList()

    def _eventCallback(self, eData):
        s = eData.subject
        if s == "state":
            if eData.to is None:
                self._eventReply(
                    {'warning': '(omitted for general state inquiry)'})
            else:
                self._eventReply(self.state)

    def _availableCommands(self):
        return {
            'fax':
            "!fax: check the contents of the fax machine. "
            "'!fax MONSTERNAME' requests a fax from FaxBot."
        }
예제 #16
0
 def __init__(self, obj, callback):
     self.obj = weakref.ref(obj)
     self.callback = callback
     self.done = threading.Event()
     self.stop = threading.Event()
     self.lock = threading.RLock()
예제 #17
0
 def __init__(self, numThreads, period, stopEvent=threading.Event()):
     self._log = logging.getLogger("heartbeat")
     self._thread = self._HeartbeatMainThread(numThreads, period, stopEvent)
     self._thread.start()
예제 #18
0
class HeartbeatSubsystem(object):
    """ The class that handles the heartbeat (simple threading) subsystem.
    
    To use the heartbeat subsystem in your class, derive from 
    HeartbeatSubsystem.HeartbeatCapable. See full documentation in that class.
    """

    _lock = threading.RLock()

    class DuplicateObjectException(Exception):
        pass

    class HeartbeatException(Exception):
        pass

    class _HeartbeatObject(object):
        def __init__(self, obj, callback):
            self.obj = weakref.ref(obj)
            self.callback = callback
            self.done = threading.Event()
            self.stop = threading.Event()
            self.lock = threading.RLock()

    class HeartbeatCapable(EmptyObject):
        """An heartbeat-capable class has a _heartbeat() method, which is 
        called periodically in a separate thread. The frequency of this method
        call is configurable when constructing the HeartbeatSubsystem. 
        
        To enable the heartbeat, use the heartbeatRegister() method with
        the HeartbeatSubsystem object to which the object is bound. Each
        object may be registered to only one HeartbeatSubsystem. To stop
        the heartbeat, use the heartbeatUnregister() method.


        """
        def __init__(self, hbSys=None, **kwargs):
            self.__hb = None
            self.__registered = threading.Lock()
            self.__lock = threading.RLock()
            if hbSys is not None:
                self.heartbeatRegister(hbSys)
            super(HeartbeatSubsystem.HeartbeatCapable, self).__init__(**kwargs)

        def __del__(self):
            self.heartbeatUnregister()

        def __heartbeat(self):
            # a lock is unnecessary here, since the task thread has a lock
            # already
            self._heartbeat()

        @property
        def heartbeatSubsystem(self):
            return self.__hb

        def heartbeatRegister(self, hbSubsystem):
            if not self.__registered.acquire(False):
                raise HeartbeatSubsystem.HeartbeatException(
                    "Object {} ({}) is already assigned"
                    " to an event subsystem.".format(self.__id, self.__type))
            with self.__lock:
                hbSubsystem.registerObject(self, self.__heartbeat)
                self.__hb = hbSubsystem

        def heartbeatUnregister(self):
            with self.__lock:
                if self.__hb is not None:
                    self.__hb.unregisterObject(self)
                    self.__hb = None
                    try:
                        self.__registered.release()
                    except threading.ThreadError:
                        pass

        def _heartbeat(self):
            pass

    class _HeartbeatTaskThread(ExceptionThread):
        def __init__(self, queue):
            self._log = logging.getLogger("heartbeat")
            self.queue = queue
            self._stopEvent = threading.Event()
            self.id = str(uuid.uuid4())
            super(HeartbeatSubsystem._HeartbeatTaskThread,
                  self).__init__(name="Heartbeat-Task")

        def stop(self):
            self._stopEvent.set()

        def _run(self):
            self._log.debug("Heartbeat task thread {} started.".format(
                self.id))
            while not self._stopEvent.is_set():
                task = None
                try:
                    task = self.queue.get(True, 0.1)
                except Queue.Empty:
                    pass
                if task is not None:
                    with task.lock:
                        if not task.stop.is_set():
                            obj = task.obj()
                            if obj is not None:
                                task.callback()
                                task.done.set()
                            self.queue.task_done()

    class _HeartbeatMainThread(ExceptionThread):
        def __init__(self, numThreads, period, stopEvent):
            self._log = logging.getLogger("heartbeat")
            self._n = numThreads
            self._t = period
            self._stopEvent = stopEvent
            self.queue = Queue.Queue()
            self._objs = []
            self._lock = threading.RLock()
            self._threads = []
            super(HeartbeatSubsystem._HeartbeatMainThread,
                  self).__init__(name="Heartbeat-Main")

        def _run(self):
            self._initialize()
            reAddList = deque()
            try:
                while not self._stopEvent.is_set():
                    #time.sleep(0.001)
                    time.sleep(1)
                    self._checkThreadExceptions()
                    self._clearDead()
                    with self._lock:
                        curTime = time.time()
                        for obj in self._objs:
                            o = obj.obj()
                            if o is not None:
                                if obj.done.is_set():
                                    obj.done.clear()
                                    reAddList.append((curTime, obj))
                        while (reAddList
                               and reAddList[0][0] + self._t < curTime):
                            self._enqueue(reAddList[0][1])
                            reAddList.popleft()
            finally:
                for th in self._threads:
                    th.stop()
                for th in self._threads:
                    self._log.debug("Joining thread {}...".format(th.id))
                    th.join()

        def registerObject(self, obj, callback):
            with self._lock:
                self._clearDead()
                newHO = HeartbeatSubsystem._HeartbeatObject(obj, callback)
                if any(True for ho in self._objs
                       if obj is ho.obj() and obj is not None):
                    raise HeartbeatSubsystem.DuplicateObjectException(
                        "Object {!s} is already registered.".format(obj))
                self._objs.append(newHO)
                self._enqueue(newHO)

        def unregisterObject(self, obj):
            with self._lock:
                self._clearDead()
                oldSize = len(self._objs)
                matches = [
                    ho for ho in self._objs
                    if ho.obj() is obj and obj is not None
                ]
                self._objs = [
                    ho for ho in self._objs
                    if ho.obj() is not obj and obj is not None
                ]
                sizeDiff = oldSize - len(self._objs)
                if sizeDiff == 0:
                    raise ValueError(
                        "Object {!s} is not registered.".format(obj))
                elif sizeDiff > 1:
                    raise Exception("Internal error: duplicate objects {!s} "
                                    "detected in event registry.".format(obj))
                elif len(matches) != 1:
                    raise Exception("Internal error: More than one match "
                                    "for object {!s}.".format(obj))
                o = matches[0]
                with o.lock:
                    o.stop.set()

        def _initialize(self):
            for _i in range(self._n):
                newThread = HeartbeatSubsystem._HeartbeatTaskThread(self.queue)
                newThread.start()
                self._threads.append(newThread)

        def _checkThreadExceptions(self):
            for thread_ in self._threads:
                if thread_.exception.is_set():
                    # this will cause an exception
                    thread_.join()

        def _clearDead(self):
            with self._lock:
                self._objs = [o for o in self._objs if o.obj() is not None]

        def _enqueue(self, obj):
            self.queue.put_nowait(obj)

    def __init__(self, numThreads, period, stopEvent=threading.Event()):
        self._log = logging.getLogger("heartbeat")
        self._thread = self._HeartbeatMainThread(numThreads, period, stopEvent)
        self._thread.start()

    @property
    def exception(self):
        return self._thread.exception.is_set()

    def registerObject(self, obj, callback):
        self._thread.registerObject(obj, callback)

    def unregisterObject(self, obj):
        self._thread.unregisterObject(obj)

    def raiseException(self):
        if self.exception:
            self._thread.join()
        else:
            raise Exception("Tried to get heartbeat exception, but there "
                            "is none.")

    def join(self):
        self._log.info("Joining heartbeat threads...")
        self._thread.join()
        self._log.info("All threads joined.")
예제 #19
0
class KmailLock(object):
    lock = threading.RLock()
예제 #20
0
class InventoryLock(object):
    lock = threading.RLock()
예제 #21
0
class FaxModule(BaseChatModule):
    """ 
    A module that handles faxing, including fax lookup for unknown monster
    codes, reporting on incoming faxes, and reporting what's in the fax
    machine.
    
    Configuration options:
    announce - set to true to use announcements (uses lots of bandwidth!)
    allow_requests - allows !fax MONSTERNAME
    fax_check_interval - frequency of checking if a new fax arrived [def. = 15]
    faxbot_timeout - time to wait until giving up on a fax request [def. = 90]
    url_timeout - time to try to load forum page before timing out [def. = 15]
    faxbot_id_number - player id number of faxbot [default = 2194132]
    fax_list_url - url of kolspading faxbot list [def. = http://goo.gl/Q352Q]
    [[[[alias]]]]
        ALIASNAME = ALIAS (monster alias name)
        
    Configuration example:
    [[[[alias]]]]
        lobsterfrogman = lfm    # now you can type '!fax lfm' 
    """
        
    requiredCapabilities = ['chat']
    _name = "fax"
    
    __lock = threading.RLock()
    _faxWait = 60
    
    _checkFrequency = None
    _abortTime = None
    timeout = None
    faxbot_uid = None
    fax_list_url = None


    def __init__(self, manager, identity, config):
        self._initialized = False
        self._announce = True
        self._allowRequests = True
        self._downloadedFaxList = False
        self._faxList = {}
        self._alias = None
        super(FaxModule, self).__init__(manager, identity, config)
        # last request the bot made to FaxBot
        self._lastRequest, self._lastRequestTime = None, None
        # last monster in the fax log
        self._lastFax, self._lastFaxTime = None, None
        # username of last faxer / last time bot got a message from faxbot 
        self._lastFaxUname, self._lastFaxBotTime = None, None
        self._noMoreFaxesTime = None
        self._lastFaxCheck = 0
        self.updateLastFax()
    
    
    def _configure(self, config):
        try:
            self._checkFrequency = int(
                    config.setdefault('fax_check_interval', 15))
            self._abortTime = int(config.setdefault('faxbot_timeout', 90))
            self.timeout = int(config.setdefault('url_timeout', 15))
            self.faxbot_uid = int(
                    config.setdefault('faxbot_id_number',"2194132))
            self.fax_lisw_url = config.setdefault('fax_list_url', 
                                                  "http://goo.gl/Q352Q")
            self._announce = stringToBool(config.setdefault('announce', 
                                                            'false'))
            self._lite = stringToBool(config.setdefault('allow_requests',
                                                        'true'))
        except ValueError:
            raise Exception("Fax Module config error: "
                            "fax_check_interval, faxbot_timeout, "
                            "url_timeout, faxbot_id_number must be integral")
        self._alias = config.setdefault('alias', {'lobsterfrogman': 'lfm'})


    def initialize(self, state, initData):
        newFaxList = map(FaxMonsterEntry.fromDict, state['faxes'])
        self._faxList = dict((e.code, e) for e in newFaxList)
        self._noMoreFaxesTime = None


    @property
    def state(self):
        return {'faxes': map(FaxMonsterEntry.toDict, self._faxList.values())}

    
    @property
    def initialState(self):
        return {'faxes': []}
            
    
    def initializeFaxList(self):
        """ download and parse the list of fax monsters from the thread
        on kolspading.com """
        self.log("Initializing fax list...")
        numTries = 3
        success = False
        for i in range(numTries):
            try:
                # download and interpret the page
                txt = urllib2.urlopen(self.fax_list_url, 
                                      timeout=self.timeout).read()
                for c in range(32,128):
                    htmlCode = "&#{};".format(c)
                    txt = txt.replace(htmlCode, chr(c))
                matches = re.findall(r'>([^>/]+): /w FaxBot ([^<]+)<', txt)
                self._faxList = dict((b.strip().lower(), FaxMonsterEntry(a,b)) 
                                     for a,b in matches)
                self.log("Found {} available faxes."
                         .format(len(self._faxList)))
                success = True
                break
            except (HTTPError, URLError, socket.timeout, socket.error) as e:
                self.log("Error loading webpage for fax list: {}: {}"
                         .format(e.__class__.__name__, e.args[0]))
                if i + 1 != numTries:
                    time.sleep(1)
        if not success:
            self.log("Failed to initialize fax list; using backup ({} entries)"
                     .format(len(self._faxList)))
        for code,alias in self._alias.items():
            code = code.strip().lower()
            if code in self._faxList:
                self._faxList[code].addAlias(alias)
            elif len(self._faxList) > 0:
                raise Exception("Invalid fax alias (no such fax code): "
                                "{} -> {}".format(alias, code))
            
        
    def checkAbort(self):
        """ Check if a request is too old and should be aborted """
        with self.__lock:
            if (self._lastRequest is not None 
                    and self._noMoreFaxesTime is None):
                timeDiff = utcTime() - self._lastRequestTime
                if timeDiff > self._abortTime:
                    self.chat("FaxBot has not replied to my request. "
                              "Please try again.")
                    self.log("Aborting fax request for '{}'"
                             .format(self._lastRequest))
                    self._lastRequest = None
                    self._lastRequestTime = None
    
        
    def checkForNewFax(self, announceInChat=True):
        """ See if a new fax has arrived, and possibly announce it if it has. 
        """
        with self.__lock:
            self._lastFaxCheck = utcTime()
            replyStr = None
            lastFaxTime = self._lastFaxTime
            lastFaxUname = self._lastFaxUname
            lastFaxMonster = self._lastFax
            event = self.updateLastFax()
            if (self._lastFax is not None and 
                    (lastFaxTime != self._lastFaxTime or 
                     lastFaxUname != self._lastFaxUname or 
                     lastFaxMonster != self._lastFax)):
                self.log("Received new fax {}".format(event))
                replyStr = ("{} has copied a {} into the fax machine."
                            .format(self._lastFaxUname, self._lastFax))
                if announceInChat:
                    self.chat(replyStr) 
                if event['userId'] == self.faxbot_uid:
                    self._lastRequest = None
                    self._lastRequestTime = None
            if self._noMoreFaxesTime is not None:
                if utcTime() - self._noMoreFaxesTime > 3600 * 3:
                    self._noMoreFaxesTime = False
                    self._lastRequestTime = None
            self.checkAbort()
            return replyStr

        
    def faxMonster(self, args, isPM):
        """Send a request to FaxBot, if not waiting on another request.
        This function is a wrapper for fax() and handles the fax lookup
        and faxbot delay. """
        with self.__lock:
            self.checkAbort()
            last = self._lastFaxBotTime
            if last is None:
                last = utcTime() - self._faxWait - 1
            if self._lastRequest is not None:
                return ("I am still waiting for FaxBot to reply "
                        "to my last request.")
            timeSinceLast = utcTime() - last
            if timeSinceLast >= self._faxWait or isPM:
                if len(self._faxList) > 0:
                    return self.faxFromList(args, isPM)
                monstername = args.split()[0]
                return self.fax(monstername, monstername, 
                                "(fax lookup unavailable) ", isPM)
            else:
                return ("Please wait {} more seconds to request a fax."
                        .format(int(self._faxWait - timeSinceLast)))
        

    def fax(self, monstercode, monstername, prependText="", 
            isPM=False, force=False):
        ''' This function actually performs the fax request '''
        stripname = re.search(r'(?:Some )?(.*)', monstername).group(1)
        if isPM:
            return ("After asking in chat, you can manually request a {} "
                    "with /w FaxBot {}".format(monstername, monstercode))
        elif not self._allowRequests:
            return ("Code to request a {}: "
                    "/w FaxBot {}".format(monstername, monstercode))
        with self.__lock:
            self.log("{}Requesting {}...".format(prependText, monstername))
            self.checkForNewFax(self._announce)
            if (self._lastFax.lower().strip() == stripname.lower().strip() 
                    and not force):
                self.log("{} already in fax.".format(monstercode))
                return ("{} There is already a(n) {} in the fax machine. "
                        "Use '!fax {} force' to fax one anyway."
                        .format(prependText, monstername, monstercode))
            if self._noMoreFaxesTime is None:
                self.whisper(self.faxbot_uid, monstercode)
                self._lastRequest = monstercode
                self._lastRequestTime = utcTime()
                return ("{}Requested {}, waiting for reply..."
                        .format(prependText, monstername))
            return ("You can manually request a {} with /w FaxBot {}"
                    .format(monstername, monstercode))

    
    def faxFromList(self, args, isPM):
        '''Look up the monster code in the list using fuzzy matching, 
        then fax it. (Or, if in quiet mode, display its code)
        '''
        splitArgs = args.split()
        if any(s for s in splitArgs if s.strip().lower() == "force"):
            return self.fax(splitArgs[0], splitArgs[0], "(forcing) ", 
                            isPM, force=True)
        
        # first, check for exact code/name/alias matches
        matches = [entry.code for entry in self._faxList.values() 
                   if entry.contains(args)]
        if len(matches) == 1:
            return self.fax(matches[0], self._faxList[matches[0]].name, "", 
                            isPM)
        
        # next, check for "close" matches
        simplify = (lambda x: x.replace("'", "")
                              .replace("_", " ")
                              .replace("-", " ").lower())
        sArgs = simplify(args)
        scoreDiff = 15
        scores = defaultdict(list)
        
        # make list of all possible names/codes/aliases
        allNames = [name for entry in self._faxList.values() 
                    for name in entry.nameList] 
        for s in allNames:
            score1 = fuzz.partial_token_set_ratio(simplify(s), sArgs)
            scores[score1].append(s)
        allScores = scores.keys()
        maxScore = max(allScores)
        for score in allScores:
            if score < maxScore - scoreDiff:
                del scores[score]
        matches = []
        for match in scores.values():
            matches.extend(match)
        fuzzyMatchKeys = set(entry.code for entry in self._faxList.values() 
                             for match in matches if entry.contains(match))
        

        # also check for args as a subset of string or code
        detokenize = lambda x: ''.join(re.split(r"'|_|-| ", x)).lower()
        dArgs = detokenize(args)
        matches = [name for name in allNames if dArgs in detokenize(name)]
        subsetMatchKeys = set(entry.code for entry in self._faxList.values() 
                              for match in matches if entry.contains(match))
        
        ls = len(subsetMatchKeys)
        lf = len(fuzzyMatchKeys)
        matchKeys = subsetMatchKeys | fuzzyMatchKeys
        lm = len(matchKeys)
        
        if ls == 0 and lf == 1:
            m = matchKeys.pop()
            return self.fax(m, self._faxList[m].name, "(fuzzy match) ", isPM)
        elif lm == 1:
            m = matchKeys.pop()
            return self.fax(m, self._faxList[m].name, "(subset match) ", isPM)
        elif lm > 1 and lm < 6:
            possibleMatchStr = ", ".join(
                    ("{} ({})".format(self._faxList[k].name,k))
                     for k in matchKeys)
            return "Did you mean one of: {}?".format(possibleMatchStr)
                    
        elif lm > 1:
            return ("Matched {} monster names/codes; please be more specific."
                    .format(ls + lf))

        return ("No known monster with name/code matching '{0}'. "
                "Use '!fax {0} force' to force, or check the monster list "
                "at {1} .".format(args, self.fax_list_url))
        
        
    def updateLastFax(self):
        """ Update what's in the Fax machine """
        with self.__lock:
            
            # suppress annoying output from pyKol
            kol.util.Report.removeOutputSection("*")
            try:
                r = ClanLogPartialRequest(self.session)
                log = self.tryRequest(r, numTries=5, initialDelay=0.25, 
                                      scaleFactor=1.5)
            finally:
                kol.util.Report.addOutputSection("*")
            faxEvents = [event for event in log['entries'] 
                         if event['type'] == CLAN_LOG_FAX]
            lastEvent = None if len(faxEvents) == 0 else faxEvents[0]
            if lastEvent is None:
                self._lastFax = None
                self._lastFaxTime = None
                self._lastFaxUname = None
            else:
                self._lastFax = lastEvent['monster']
                lastFaxTimeAz = tz.localize(lastEvent['date'])
                lastFaxTimeUtc = lastFaxTimeAz.astimezone(utc)
                self._lastFaxTime = calendar.timegm(lastFaxTimeUtc.timetuple())
                self._lastFaxUname = lastEvent['userName']
            return lastEvent 
                
    
    def processFaxbotMessage(self, txt):
        """ Process a PM from FaxBot """
        with self.__lock:
            if "I do not understand your request" in txt:
                replyTxt = ("FaxBot does not have the requested monster '{}'. "
                            "(Check the list at {} )"
                            .format(self._lastRequest, self.fax_list_url)) 
                self._lastRequest = None
                self._lastRequestTime = None
                return replyTxt
            if "just delivered a fax" in txt:
                self._lastRequest = None
                self._lastRequestTime = None
                return ("FaxBot received the request too early. "
                        "Please try again.")
            if "try again tomorrow" in txt:
                self._noMoreFaxesTime = utcTime()
                txt = ("I'm not allowed to request any more faxes today. "
                       "Request manually with /w FaxBot {}"
                       .format(self._lastRequest))
                self._lastRequest = None
                self._lastRequestTime = utcTime()
                return txt
            m = re.search(r'has copied', txt)
            if m is not None:
                self._lastRequest = None
                self._lastRequestTime = None
                self._lastFaxBotTime = utcTime()
                # suppress output from checkForNewFax since we are returning
                # the text, to be output later
                return self.checkForNewFax(False)
            self._lastRequest = None
            self._lastRequestTime = None
            return "Received message from FaxBot: {}".format(txt)

        
    def printLastFax(self):
        """ Get the chat text to represent what's in the Fax machine. """
        if self._lite:
            if utcTime() - self._lastFaxCheck >= self._checkFrequency:
                self.checkForNewFax(False)
        if self._lastFax is None:
            return "I can't tell what's in the fax machine."
        elapsed = utcTime() - self._lastFaxTime
        timeStr = "{} minutes".format(int((elapsed+59) // 60))
        return ("The fax has held a(n) {} for the last {}. "
                "(List of monsters {} )"
                .format(self._lastFax, timeStr, self.fax_list_url))
        
        
    def _processCommand(self, message, cmd, args):
        if cmd == "fax":
            if args != "":
                isPM = (message['type'] == "private")
                return self.faxMonster(args, isPM)
            else:
                return self.printLastFax()
        elif message.get('userId', 0) == self.faxbot_uid:
            self.log("Received FaxBot PM: {}".format(message['text']))
            msg = self.processFaxbotMessage(message['text'])
            if msg is not None:
                self.chat(msg)
            return None
        

    def _heartbeat(self):
        if self._initialized and not self._downloadedFaxList:
            with self.__lock:
                self.initializeFaxList()
                self._downloadedFaxList = True
        if utcTime() - self._lastFaxCheck >= self._checkFrequency:
            if self._announce:
                self.checkForNewFax(True)
        

    def _eventCallback(self, eData):
        s = eData.subject
        if s == "state":
            if eData.to is None:
                self._eventReply({
                        'warning': '(omitted for general state inquiry)'})
            else:
                self._eventReply(self.state)
        elif s == "startup" and eData.fromIdentity == "__system__":
            self._initialized = True
            
    
    def _availableCommands(self):
        if self._allowRequests:
            return {'fax': "!fax: check the contents of the fax machine. "
                           "'!fax MONSTERNAME' requests a fax from FaxBot."}
        else:
            return {'fax': "!fax: check the contents of the fax machine. "
                           "'!fax MONSTBRNAME' shows the code to request "
                           "MONSTERNAME from FaxBot."}
예제 #22
0
 def __init__(self, *args, **kwargs):
     self._exc = None
     self._exc_info = sys.exc_info
     self.exception = threading.Event()
     super(ExceptionThread, self).__init__(*args, **kwargs)