<commit_before>#!/usr/bin/env python

###
# Copyright (c) 2002, Jeremiah Fincher
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#   * Redistributions of source code must retain the above copyright notice,
#     this list of conditions, and the following disclaimer.
#   * Redistributions in binary form must reproduce the above copyright notice,
#     this list of conditions, and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
#   * Neither the name of the author of this software nor the name of
#     contributors to this software may be used to endorse or promote products
#     derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###

"""
Provides several commands that go out to websites and get things.
"""

__revision__ = "$Id$"

import plugins

import re
import sets
import getopt
import socket
import urllib2
import xml.dom.minidom
from itertools import imap, ifilter

import conf
import utils
import webutils
import privmsgs
import callbacks

class FreshmeatException(Exception):
    pass

class Http(callbacks.Privmsg):
    threaded = True
    _titleRe = re.compile(r'<title>(.*?)</title>', re.I | re.S)
    def callCommand(self, method, irc, msg, *L):
        try:
            callbacks.Privmsg.callCommand(self, method, irc, msg, *L)
        except webutils.WebException, e:
            irc.error(msg, str(e))

    def title(self, irc, msg, args):
        """<url>

        Returns the HTML <title>...</title> of a URL.
        """
        url = privmsgs.getArgs(args)
        if '://' not in url:
            url = 'http://%s' % url
        try:
            text = webutils.getUrl(url, size=4096)
            m = self._titleRe.search(text)
            if m is not None:
                irc.reply(msg, utils.htmlToText(m.group(1).strip()))
            else:
                irc.reply(msg, 'That URL appears to have no HTML title.')
        except ValueError, e:
            irc.error(msg, str(e))

    def freshmeat(self, irc, msg, args):
        """<project name>

        Returns Freshmeat data about a given project.
        """
        project = privmsgs.getArgs(args)
        project = ''.join(project.split())
        url = 'http://www.freshmeat.net/projects-xml/%s' % project
        try:
            text = webutils.getUrl(url)
            if text.startswith('Error'):
                raise FreshmeatException, text
            dom = xml.dom.minidom.parseString(text)
            def getNode(name):
                node = dom.getElementsByTagName(name)[0]
                return str(node.childNodes[0].data)
            project = getNode('projectname_full')
            version = getNode('latest_release_version')
            vitality = getNode('vitality_percent')
            popularity = getNode('popularity_percent')
            lastupdated = getNode('date_updated')
            irc.reply(msg,
                      '%s, last updated %s, with a vitality percent of %s '\
                      'and a popularity of %s, is in version %s.' % \
                      (project, lastupdated, vitality, popularity, version))
        except FreshmeatException, e:
            irc.error(msg, utils.exnToString(e))

    def stockquote(self, irc, msg, args):
        """<company symbol>

        Gets the information about the current price and change from the
        previous day of a given compny (represented by a stock symbol).
        """
        symbol = privmsgs.getArgs(args)
        url = 'http://finance.yahoo.com/d/quotes.csv?s=%s'\
              '&f=sl1d1t1c1ohgv&e=.csv' % symbol
        quote = webutils.getUrl(url)
        data = quote.split(',')
        if data[1] != '0.00':
            irc.reply(msg,
                       'The current price of %s is %s, as of %s EST.  '\
                       'A change of %s from the last business day.' %\
                       (data[0][1:-1], data[1], data[3][1:-1], data[4]))
        else:
            m = 'I couldn\'t find a listing for %s' % symbol
            irc.error(msg, m)

    _cityregex = re.compile(
        r''\
        r'(.*?), (.*?),(.*?)	', re.IGNORECASE)
    _interregex = re.compile(
        r''\
        r'(.*?), (.*?)	', re.IGNORECASE)
    _condregex = re.compile(
        r''\
        r'(.*?)	', re.IGNORECASE)
    _tempregex = re.compile(
        r''\
        r'(.*?)	', re.IGNORECASE)
    # States
    _realStates = sets.Set(['ak', 'al', 'ar', 'az', 'ca', 'co', 'ct',
    			    'dc', 'de', 'fl', 'ga', 'hi', 'ia', 'id',
			    'il', 'in', 'ks', 'ky', 'la', 'ma', 'md',
			    'me', 'mi', 'mn', 'mo', 'ms', 'mt', 'nc',
			    'nd', 'ne', 'nh', 'nj', 'nm', 'nv', 'ny',
			    'oh', 'ok', 'or', 'pa', 'ri', 'sc', 'sd',
			    'tn', 'tx', 'ut', 'va', 'vt', 'wa', 'wi',
			    'wv', 'wy'])
    # Provinces.  (Province being a metric state measurement mind you. :D)
    _fakeStates = sets.Set(['ab', 'bc', 'mb', 'nb', 'nf', 'ns', 'nt',
                           'nu', 'on', 'pe', 'qc', 'sk', 'yk'])
    # Certain countries are expected to use a standard abbreviation
    # The weather we pull uses weird codes.  Map obvious ones here.
    _countryMap = {'uk': 'gb'}
    def weather(self, irc, msg, args):
        """<US zip code> <US/Canada city, state> <Foreign city, country>

        Returns the approximate weather conditions for a given city.
        """

        #If we received more than one argument, then we have received
        #a city and state argument that we need to process.
        if len(args) > 1:
            #If we received more than 1 argument, then we got a city with a
            #multi-word name.  ie ['Garden', 'City', 'KS'] instead of
            #['Liberal', 'KS'].  We join it together with a + to pass
            #to our query
            state = args.pop()
            state = state.lower()
            city = '+'.join(args)
            city = city.rstrip(',')
            city = city.lower()
            #We must break the States up into two sections.  The US and
            #Canada are the only countries that require a State argument.

            if state in self._realStates:
                country = 'us'
            elif state in self._fakeStates:
                country = 'ca'
            else:
                country = state
                state = ''
	    if country in self._countryMap.keys():
	        country = self._countryMap[country]
            url = 'http://www.hamweather.net/cgi-bin/hw3/hw3.cgi?'\
                  'pass=&dpp=&forecast=zandh&config=&'\
                  'place=%s&state=%s&country=%s' % \
                  (city, state, country)
	    html = webutils.getUrl(url)
	    if 'was not found' in html:
	        url = 'http://www.hamweather.net/cgi-bin/hw3/hw3.cgi?'\
		      'pass=&dpp=&forecast=zandh&config=&'\
		      'place=%s&state=&country=%s' % \
		      (city, state)
		html = webutils.getUrl(url)
		if 'was not found' in html:
		    irc.error(msg, 'No such location could be found.')
		    return

        #We received a single argument.  Zipcode or station id.
        else:
            zip = privmsgs.getArgs(args)
            zip = zip.replace(',','')
            zip = zip.lower().split()
            url = 'http://www.hamweather.net/cgi-bin/hw3/hw3.cgi?'\
                  'config=&forecast=zandh&pands=%s&Submit=GO' % args[0]
            html = webutils.getUrl(url)
	    if 'was not found' in html:
	        irc.error(msg, 'No such location could be found.')
		return

        headData = self._cityregex.search(html)
        if headData:
            (city, state, country) = headData.groups()
        else:
            headData = self._interregex.search(html)
	    if headData:
	        (city, state) = headData.groups()
	    else:
	        irc.error(msg, 'No such location could be found.')
		return

        temp = self._tempregex.search(html).group(1)
        conds = self._condregex.search(html).group(1)

        if temp and conds and city and state:
            conds = conds.replace('Tsra', 'Thunder Storms')
            s = 'The current temperature in %s, %s is %s.  ' \
                'Conditions: %s.' % \
                (city.strip(), state.strip(), temp, conds)
            irc.reply(msg, s)
        else:
            irc.error(msg, 'The format of the page was odd.')

    _mlgeekquotere = re.compile('
(.*?)
', re.M | re.DOTALL)
    def geekquote(self, irc, msg, args):
        """[--id=<value>]

        Returns a random geek quote from bash.org; the optional argument
        --id specifies which quote to retrieve.
        """
        (optlist, rest) = getopt.getopt(args, '', ['id='])
        id = 'random1'
        for (option, arg) in optlist:
            if option == '--id':
                try:
                    id = int(arg)
                except ValueError, e:
                    irc.error(msg, 'Invalid id: %s' % e)
                    return

        html = webutils.getUrl('http://bash.org/?%s' % id)
        m = self._mlgeekquotere.search(html)
        if m is None:
            irc.error(msg, 'No quote found.')
            return
        quote = utils.htmlToText(m.group(1))
        quote = ' // '.join(quote.splitlines())
        irc.reply(msg, quote)

    _acronymre = re.compile(r'valign="middle" width="7\d%" bgcolor="[^"]+">'
                            r'(?:)?([^<]+)')
    def acronym(self, irc, msg, args):
        """<acronym>

        Displays acronym matches from acronymfinder.com
        """
        acronym = privmsgs.getArgs(args)
        url = 'http://www.acronymfinder.com/' \
              'af-query.asp?String=exact&Acronym=%s' % acronym
        request = urllib2.Request(url, headers={'User-agent':
          'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 4.0)'})
        html = webutils.getUrl(request)
        # The following definitions are stripped and empties are removed.
        defs = filter(None, imap(str.strip, self._acronymre.findall(html)))
        utils.sortBy(lambda s: not s.startswith('[not an acronym]'), defs)
        for (i, s) in enumerate(defs):
            if s.startswith('[not an acronym]'):
                defs[i] = s.split('is ', 1)[1]
        if len(defs) == 0:
            irc.reply(msg,'No definitions found.  (%s)'%conf.replyPossibleBug)
        else:
            s = ', or '.join(defs)
            irc.reply(msg, '%s could be %s' % (acronym, s))

    _netcraftre = re.compile(r'whatos text -->(.*?)<a href="/up/acc', re.S)
    def netcraft(self, irc, msg, args):
        """<hostname|ip>

        Returns Netcraft.com's determination of what operating system and
        webserver is running on the host given.
        """
        hostname = privmsgs.getArgs(args)
        url = 'http://uptime.netcraft.com/up/graph/?host=%s' % hostname
        html = webutils.getUrl(url)
        m = self._netcraftre.search(html)
        if m:
            html = m.group(1)
            s = utils.htmlToText(html, tagReplace='').strip('\xa0 ')
            irc.reply(msg, s[9:]) # Snip off "the site"
        elif 'We could not get any results' in html:
            irc.reply(msg, 'No results found for %s.' % hostname)
        else:
            irc.error(msg, 'The format of page the was odd.')

    def kernel(self, irc, msg, args):
        """takes no arguments

        Returns information about the current version of the Linux kernel.
        """
        try:
            fd = webutils.getUrlFd('http://www.kernel.org/kdist/finger_banner')
            for line in fd:
                (name, version) = line.split(':')
                if 'latest stable' in name:
                    stable = version.strip()
                elif 'latest beta' in name:
                    beta = version.strip()
        finally:
            fd.close()
        irc.reply(msg, 'The latest stable kernel is %s; ' \
                       'the latest beta kernel is %s.' % (stable, beta))

    _pgpkeyre = re.compile(r'pub\s+\d{4}\w/([^<]+)[^>]+>([^<]+)')
    def pgpkey(self, irc, msg, args):
        """<search words>

        Returns the results of querying pgp.mit.edu for keys that match
        the <search words>.
        """
        search = privmsgs.getArgs(args)
        urlClean = search.replace(' ', '+')
        host = 'http://pgp.mit.edu:11371'
        url = '%s/pks/lookup?op=index&search=%s' % (host, urlClean)
        try:
            L = []
            fd = urllib2.urlopen(url)
            for line in iter(fd.next, ''):
                info = self._pgpkeyre.search(line)
                if info:
                    L.append('%s <%s%s>' % (info.group(3),host,info.group(1)))
            if len(L) == 0:
                irc.reply(msg, 'No results found for %s.' % search)
            else:
                s = 'Matches found for %s: %s' % (search, ' :: '.join(L))
                irc.reply(msg, s)
        finally:
            fd.close()

Class = Http

# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:
<commit_msg>Handle errors in getUrlFd properly in Http.kernel.<commit_after>#!/usr/bin/env python

###
# Copyright (c) 2002, Jeremiah Fincher
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#   * Redistributions of source code must retain the above copyright notice,
#     this list of conditions, and the following disclaimer.
#   * Redistributions in binary form must reproduce the above copyright notice,
#     this list of conditions, and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
#   * Neither the name of the author of this software nor the name of
#     contributors to this software may be used to endorse or promote products
#     derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###

"""
Provides several commands that go out to websites and get things.
"""

__revision__ = "$Id$"

import plugins

import re
import sets
import getopt
import socket
import urllib2
import xml.dom.minidom
from itertools import imap, ifilter

import conf
import utils
import webutils
import privmsgs
import callbacks

class FreshmeatException(Exception):
    pass

class Http(callbacks.Privmsg):
    threaded = True
    _titleRe = re.compile(r'<title>(.*?)</title>', re.I | re.S)
    def callCommand(self, method, irc, msg, *L):
        try:
            callbacks.Privmsg.callCommand(self, method, irc, msg, *L)
        except webutils.WebException, e:
            irc.error(msg, str(e))

    def title(self, irc, msg, args):
        """<url>

        Returns the HTML <title>...</title> of a URL.
        """
        url = privmsgs.getArgs(args)
        if '://' not in url:
            url = 'http://%s' % url
        try:
            text = webutils.getUrl(url, size=4096)
            m = self._titleRe.search(text)
            if m is not None:
                irc.reply(msg, utils.htmlToText(m.group(1).strip()))
            else:
                irc.reply(msg, 'That URL appears to have no HTML title.')
        except ValueError, e:
            irc.error(msg, str(e))

    def freshmeat(self, irc, msg, args):
        """<project name>

        Returns Freshmeat data about a given project.
        """
        project = privmsgs.getArgs(args)
        project = ''.join(project.split())
        url = 'http://www.freshmeat.net/projects-xml/%s' % project
        try:
            text = webutils.getUrl(url)
            if text.startswith('Error'):
                raise FreshmeatException, text
            dom = xml.dom.minidom.parseString(text)
            def getNode(name):
                node = dom.getElementsByTagName(name)[0]
                return str(node.childNodes[0].data)
            project = getNode('projectname_full')
            version = getNode('latest_release_version')
            vitality = getNode('vitality_percent')
            popularity = getNode('popularity_percent')
            lastupdated = getNode('date_updated')
            irc.reply(msg,
                      '%s, last updated %s, with a vitality percent of %s '\
                      'and a popularity of %s, is in version %s.' % \
                      (project, lastupdated, vitality, popularity, version))
        except FreshmeatException, e:
            irc.error(msg, utils.exnToString(e))

    def stockquote(self, irc, msg, args):
        """<company symbol>

        Gets the information about the current price and change from the
        previous day of a given compny (represented by a stock symbol).
        """
        symbol = privmsgs.getArgs(args)
        url = 'http://finance.yahoo.com/d/quotes.csv?s=%s'\
              '&f=sl1d1t1c1ohgv&e=.csv' % symbol
        quote = webutils.getUrl(url)
        data = quote.split(',')
        if data[1] != '0.00':
            irc.reply(msg,
                       'The current price of %s is %s, as of %s EST.  '\
                       'A change of %s from the last business day.' %\
                       (data[0][1:-1], data[1], data[3][1:-1], data[4]))
        else:
            m = 'I couldn\'t find a listing for %s' % symbol
            irc.error(msg, m)

    _cityregex = re.compile(
        r''\
        r'(.*?), (.*?),(.*?)	', re.IGNORECASE)
    _interregex = re.compile(
        r''\
        r'(.*?), (.*?)	', re.IGNORECASE)
    _condregex = re.compile(
        r''\
        r'(.*?)	', re.IGNORECASE)
    _tempregex = re.compile(
        r''\
        r'(.*?)	', re.IGNORECASE)
    # States
    _realStates = sets.Set(['ak', 'al', 'ar', 'az', 'ca', 'co', 'ct',
    			    'dc', 'de', 'fl', 'ga', 'hi', 'ia', 'id',
			    'il', 'in', 'ks', 'ky', 'la', 'ma', 'md',
			    'me', 'mi', 'mn', 'mo', 'ms', 'mt', 'nc',
			    'nd', 'ne', 'nh', 'nj', 'nm', 'nv', 'ny',
			    'oh', 'ok', 'or', 'pa', 'ri', 'sc', 'sd',
			    'tn', 'tx', 'ut', 'va', 'vt', 'wa', 'wi',
			    'wv', 'wy'])
    # Provinces.  (Province being a metric state measurement mind you. :D)
    _fakeStates = sets.Set(['ab', 'bc', 'mb', 'nb', 'nf', 'ns', 'nt',
                           'nu', 'on', 'pe', 'qc', 'sk', 'yk'])
    # Certain countries are expected to use a standard abbreviation
    # The weather we pull uses weird codes.  Map obvious ones here.
    _countryMap = {'uk': 'gb'}
    def weather(self, irc, msg, args):
        """<US zip code> <US/Canada city, state> <Foreign city, country>

        Returns the approximate weather conditions for a given city.
        """

        #If we received more than one argument, then we have received
        #a city and state argument that we need to process.
        if len(args) > 1:
            #If we received more than 1 argument, then we got a city with a
            #multi-word name.  ie ['Garden', 'City', 'KS'] instead of
            #['Liberal', 'KS'].  We join it together with a + to pass
            #to our query
            state = args.pop()
            state = state.lower()
            city = '+'.join(args)
            city = city.rstrip(',')
            city = city.lower()
            #We must break the States up into two sections.  The US and
            #Canada are the only countries that require a State argument.

            if state in self._realStates:
                country = 'us'
            elif state in self._fakeStates:
                country = 'ca'
            else:
                country = state
                state = ''
	    if country in self._countryMap.keys():
	        country = self._countryMap[country]
            url = 'http://www.hamweather.net/cgi-bin/hw3/hw3.cgi?'\
                  'pass=&dpp=&forecast=zandh&config=&'\
                  'place=%s&state=%s&country=%s' % \
                  (city, state, country)
	    html = webutils.getUrl(url)
	    if 'was not found' in html:
	        url = 'http://www.hamweather.net/cgi-bin/hw3/hw3.cgi?'\
		      'pass=&dpp=&forecast=zandh&config=&'\
		      'place=%s&state=&country=%s' % \
		      (city, state)
		html = webutils.getUrl(url)
		if 'was not found' in html:
		    irc.error(msg, 'No such location could be found.')
		    return

        #We received a single argument.  Zipcode or station id.
        else:
            zip = privmsgs.getArgs(args)
            zip = zip.replace(',','')
            zip = zip.lower().split()
            url = 'http://www.hamweather.net/cgi-bin/hw3/hw3.cgi?'\
                  'config=&forecast=zandh&pands=%s&Submit=GO' % args[0]
            html = webutils.getUrl(url)
	    if 'was not found' in html:
	        irc.error(msg, 'No such location could be found.')
		return

        headData = self._cityregex.search(html)
        if headData:
            (city, state, country) = headData.groups()
        else:
            headData = self._interregex.search(html)
	    if headData:
	        (city, state) = headData.groups()
	    else:
	        irc.error(msg, 'No such location could be found.')
		return

        temp = self._tempregex.search(html).group(1)
        conds = self._condregex.search(html).group(1)

        if temp and conds and city and state:
            conds = conds.replace('Tsra', 'Thunder Storms')
            s = 'The current temperature in %s, %s is %s.  ' \
                'Conditions: %s.' % \
                (city.strip(), state.strip(), temp, conds)
            irc.reply(msg, s)
        else:
            irc.error(msg, 'The format of the page was odd.')

    _mlgeekquotere = re.compile('
(.*?)
', re.M | re.DOTALL)
    def geekquote(self, irc, msg, args):
        """[--id=<value>]

        Returns a random geek quote from bash.org; the optional argument
        --id specifies which quote to retrieve.
        """
        (optlist, rest) = getopt.getopt(args, '', ['id='])
        id = 'random1'
        for (option, arg) in optlist:
            if option == '--id':
                try:
                    id = int(arg)
                except ValueError, e:
                    irc.error(msg, 'Invalid id: %s' % e)
                    return

        html = webutils.getUrl('http://bash.org/?%s' % id)
        m = self._mlgeekquotere.search(html)
        if m is None:
            irc.error(msg, 'No quote found.')
            return
        quote = utils.htmlToText(m.group(1))
        quote = ' // '.join(quote.splitlines())
        irc.reply(msg, quote)

    _acronymre = re.compile(r'valign="middle" width="7\d%" bgcolor="[^"]+">'
                            r'(?:)?([^<]+)')
    def acronym(self, irc, msg, args):
        """<acronym>

        Displays acronym matches from acronymfinder.com
        """
        acronym = privmsgs.getArgs(args)
        url = 'http://www.acronymfinder.com/' \
              'af-query.asp?String=exact&Acronym=%s' % acronym
        request = urllib2.Request(url, headers={'User-agent':
          'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 4.0)'})
        html = webutils.getUrl(request)
        # The following definitions are stripped and empties are removed.
        defs = filter(None, imap(str.strip, self._acronymre.findall(html)))
        utils.sortBy(lambda s: not s.startswith('[not an acronym]'), defs)
        for (i, s) in enumerate(defs):
            if s.startswith('[not an acronym]'):
                defs[i] = s.split('is ', 1)[1]
        if len(defs) == 0:
            irc.reply(msg,'No definitions found.  (%s)'%conf.replyPossibleBug)
        else:
            s = ', or '.join(defs)
            irc.reply(msg, '%s could be %s' % (acronym, s))

    _netcraftre = re.compile(r'whatos text -->(.*?)<a href="/up/acc', re.S)
    def netcraft(self, irc, msg, args):
        """<hostname|ip>

        Returns Netcraft.com's determination of what operating system and
        webserver is running on the host given.
        """
        hostname = privmsgs.getArgs(args)
        url = 'http://uptime.netcraft.com/up/graph/?host=%s' % hostname
        html = webutils.getUrl(url)
        m = self._netcraftre.search(html)
        if m:
            html = m.group(1)
            s = utils.htmlToText(html, tagReplace='').strip('\xa0 ')
            irc.reply(msg, s[9:]) # Snip off "the site"
        elif 'We could not get any results' in html:
            irc.reply(msg, 'No results found for %s.' % hostname)
        else:
            irc.error(msg, 'The format of page the was odd.')

    def kernel(self, irc, msg, args):
        """takes no arguments

        Returns information about the current version of the Linux kernel.
        """
        try:
            try:
                fd = webutils.getUrlFd('http://kernel.org/kdist/finger_banner')
            except webutils.WebException, e:
                irc.error(msg, str(e))
                return
            for line in fd:
                (name, version) = line.split(':')
                if 'latest stable' in name:
                    stable = version.strip()
                elif 'latest beta' in name:
                    beta = version.strip()
        finally:
            fd.close()
        irc.reply(msg, 'The latest stable kernel is %s; ' \
                       'the latest beta kernel is %s.' % (stable, beta))

    _pgpkeyre = re.compile(r'pub\s+\d{4}\w/([^<]+)[^>]+>([^<]+)')
    def pgpkey(self, irc, msg, args):
        """<search words>

        Returns the results of querying pgp.mit.edu for keys that match
        the <search words>.
        """
        search = privmsgs.getArgs(args)
        urlClean = search.replace(' ', '+')
        host = 'http://pgp.mit.edu:11371'
        url = '%s/pks/lookup?op=index&search=%s' % (host, urlClean)
        try:
            L = []
            fd = urllib2.urlopen(url)
            for line in iter(fd.next, ''):
                info = self._pgpkeyre.search(line)
                if info:
                    L.append('%s <%s%s>' % (info.group(3),host,info.group(1)))
            if len(L) == 0:
                irc.reply(msg, 'No results found for %s.' % search)
            else:
                s = 'Matches found for %s: %s' % (search, ' :: '.join(L))
                irc.reply(msg, s)
        finally:
            fd.close()

Class = Http

# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:
<|endoftext|>


<commit_before>#!/usr/bin/env python

###
# Copyright (c) 2003, Brett Kelly
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#   * Redistributions of source code must retain the above copyright notice,
#     this list of conditions, and the following disclaimer.
#   * Redistributions in binary form must reproduce the above copyright notice,
#     this list of conditions, and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
#   * Neither the name of the author of this software nor the name of
#     contributors to this software may be used to endorse or promote products
#     derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###

"""
A complete messaging system that allows users to leave 'notes' for other
users that can be retrieved later.
"""

__revision__ = "$Id$"

import plugins

import sets
import time
import getopt
import os.path
from itertools import imap

import conf
import utils
import ircdb
import ircmsgs
import plugins
import privmsgs
import registry
import ircutils
import callbacks

try:
    import sqlite
except ImportError:
    raise callbacks.Error, 'You need to have PySQLite installed to use this ' \
                           'plugin.  Download it at <http://pysqlite.sf.net/>'

conf.registerPlugin('Note')
conf.registerGlobalValue(conf.supybot.plugins.Note, 'notifyOnJoin',
    registry.Boolean(False, """Determines whether the bot will notify people of
    their new messages when they join the channel.  Normally it will notify
    them when they send a message to the channel, since oftentimes joins are
    the result of netsplits and not the actual presence of the user."""))
conf.registerGlobalValue(conf.supybot.plugins.Note, 'notifyOnJoinRepeatedly',
    registry.Boolean(False, """Determines whether the bot will repeatedly
    notify people of their new messages when they join the channel.  That means
    when they join the channel, the bot will tell them they have unread
    messages, even if it's told them before."""))

class Ignores(registry.SpaceSeparatedListOfStrings):
    List = ircutils.IrcSet

conf.registerUserValue(conf.users.plugins.Note, 'ignores', Ignores([], ''))

class NoteDb(plugins.DBHandler):
    def makeDb(self, filename):
        "create Notes database and tables"
        if os.path.exists(filename):
            db = sqlite.connect(filename)
        else:
            db = sqlite.connect(filename, converters={'bool': bool})
            cursor = db.cursor()
            cursor.execute("""CREATE TABLE notes (
                              id INTEGER PRIMARY KEY,
                              from_id INTEGER,
                              to_id INTEGER,
                              added_at TIMESTAMP,
                              notified BOOLEAN,
                              read BOOLEAN,
                              public BOOLEAN,
                              note TEXT
                              )""")
            db.commit()
        return db

class Note(callbacks.Privmsg):
    def __init__(self):
        callbacks.Privmsg.__init__(self)
        dataDir = conf.supybot.directories.data()
        self.dbHandler = NoteDb(name=os.path.join(dataDir, 'Notes'))

    def setAsRead(self, id):
        db = self.dbHandler.getDb()
        cursor = db.cursor()
        cursor.execute("""UPDATE notes
                          SET read=1, notified=1
                          WHERE id=%s""", id)
        db.commit()

    def die(self):
        self.dbHandler.die()

    def doPrivmsg(self, irc, msg):
        self._notify(irc, msg)

    def doJoin(self, irc, msg):
        if self.registryValue('notifyOnJoin'):
            repeatedly = self.registryValue('notifyOnJoinRepeatedly')
            self._notify(irc, msg, repeatedly)

    def _notify(self, irc, msg, repeatedly=False):
        try:
            id = ircdb.users.getUserId(msg.prefix)
        except KeyError:
            return
        db = self.dbHandler.getDb()
        cursor = db.cursor()
        cursor.execute("""SELECT COUNT(*) FROM notes
                          WHERE notes.to_id=%s AND notified=0""", id)
        unnotified = int(cursor.fetchone()[0])
        if unnotified != 0 or repeatedly:
            cursor.execute("""SELECT COUNT(*) FROM notes
                              WHERE notes.to_id=%s AND read=0""", id)
            unread = int(cursor.fetchone()[0])
            s = 'You have %s; ' \
                '%s that I haven\'t told you about before now..' % \
                (utils.nItems('note', unread, 'unread'), unnotified)
            irc.queueMsg(ircmsgs.privmsg(msg.nick, s))
            cursor.execute("""UPDATE notes SET notified=1
                              WHERE notes.to_id=%s""", id)
            db.commit()

    def send(self, irc, msg, args):
        """<recipient> <text>

        Sends a new note to the user specified.
        """
        (name, note) = privmsgs.getArgs(args, required=2)
        if ircdb.users.hasUser(name):
            toId = ircdb.users.getUserId(name)
        else:
            # name must be a nick, we'll try that.
            try:
                hostmask = irc.state.nickToHostmask(name)
                toId = ircdb.users.getUserId(hostmask)
            except KeyError:
                irc.errorNoUser()
                return
        try:
            fromId = ircdb.users.getUserId(msg.prefix)
            senderName = ircdb.users.getUser(fromId).name
        except KeyError:
            irc.errorNotRegistered()
            return
        if senderName in self.userValue('ignores', name):
            irc.error('That user has ignored messages from you.')
            return
        if ircutils.isChannel(msg.args[0]):
            public = 1
        else:
            public = 0
        db = self.dbHandler.getDb()
        cursor = db.cursor()
        now = int(time.time())
        cursor.execute("""INSERT INTO notes VALUES
                          (NULL, %s, %s, %s, 0, 0, %s, %s)""",
                       fromId, toId, now, public, note)
        db.commit()
        cursor.execute("""SELECT id FROM notes WHERE
                          from_id=%s AND to_id=%s AND added_at=%s""",
                       fromId, toId, now)
        id = cursor.fetchone()[0]
        irc.reply('Note #%s sent to %s.' % (id, name))

    def unsend(self, irc, msg, args):
        """<id>

        Unsends the note with the id given.  You must be the
        author of the note, and it must be unread.
        """
        id = privmsgs.getArgs(args)
        try:
            userid = ircdb.users.getUserId(msg.prefix)
        except KeyError:
            irc.errorNotRegistered()
            return
        db = self.dbHandler.getDb()
        cursor = db.cursor()
        cursor.execute("""SELECT from_id, read FROM notes WHERE id=%s""", id)
        if cursor.rowcount < 1:
            irc.error('That\'s not a valid note id.')
            return
        (from_id, read) = map(int, cursor.fetchone())
        if from_id == userid:
            if not read:
                cursor.execute("""DELETE FROM notes WHERE id=%s""", id)
                db.commit()
                irc.replySuccess()
            else:
                irc.error('That note has been read already.')
        else:
            irc.error('That note wasn\'t sent by you.')

    def note(self, irc, msg, args):
        """<note id>

        Retrieves a single note by its unique note id.  Use the 'note list'
        command to see what unread notes you have.
        """
        noteid = privmsgs.getArgs(args)
        if noteid.startswith('get'):
            irc.error('The Note.get command has changed to be simply "note".')
            return
        try:
            id = ircdb.users.getUserId(msg.prefix)
        except KeyError:
            irc.errorNotRegistered()
            return
        try:
            noteid = int(noteid)
        except ValueError:
            irc.error('%r is not a valid note id.' % noteid)
            return
        db = self.dbHandler.getDb()
        cursor = db.cursor()
        cursor.execute("""SELECT note, to_id, from_id, added_at, public
                          FROM notes
                          WHERE (to_id=%s OR from_id=%s) AND id=%s""",
                       id, id, noteid)
        if cursor.rowcount < 1:
            s = 'You may only retrieve notes you\'ve sent or received.'
            irc.error(s)
            return
        (note, toId, fromId, addedAt, public) = cursor.fetchone()
        (toId,fromId,addedAt,public) = imap(int, (toId,fromId,addedAt,public))
        elapsed = utils.timeElapsed(time.time() - addedAt)
        if toId == id:
            author = ircdb.users.getUser(fromId).name
            newnote = '%s (Sent by %s %s ago)' % (note, author, elapsed)
        elif fromId == id:
            recipient = ircdb.users.getUser(toId).name
            newnote = '%s (Sent to %s %s ago)' % (note, recipient, elapsed)
        irc.reply(newnote, private=(not public))
        self.setAsRead(noteid)

    def _formatNoteData(self, msg, id, fromId, public, sent=False):
        (id, fromId, public) = imap(int, (id, fromId, public))
        if public or not ircutils.isChannel(msg.args[0]):
            sender = ircdb.users.getUser(fromId).name
            if sent:
                return '#%s to %s' % (id, sender)
            else:
                return '#%s from %s' % (id, sender)
        else:
            return '#%s (private)' % id

    def ignore(self, irc, msg, args):
        """[--remove] <user>

        Ignores all messages from <user>.  If --remove is listed, remove <user>
        from the list of users being ignored.
        """
        remove = False
        while '--remove' in args:
            remove = True
            args.remove('--remove')
        user = privmsgs.getArgs(args)
        try:
            L = self.userValue('ignores', msg.prefix)
            if remove:
                try:
                    L.remove(user)
                except (KeyError, ValueError):
                    irc.error('%r was not in your list of ignores.' % user)
                    return
            else:
                L.add(user)
            self.setUserValue(msg.prefix, 'ignores', L, setValue=True)
            irc.replySuccess()
        except KeyError:
            irc.errorNoUser()

    def list(self, irc, msg, args):
        """[--{old,sent}] [--{from,to} <user>]

        Retrieves the ids of all your unread notes.  If --old is given, list
        read notes.  If --sent is given, list notes that you have sent.  If
        --from is specified, only lists notes sent to you from <user>.  If
        --to is specified, only lists notes sent by you to <user>.
        """
        options = ['old', 'sent', 'from=', 'to=']
        (optlist, rest) = getopt.getopt(args, '', options)
        sender, receiver, old, sent = ('', '', False, False)
        for (option, arg) in optlist:
            option = option.lstrip('-')
            if option == 'old':
                old = True
            if option == 'sent':
                sent = True
            if option == 'from':
                sender = arg
            if option == 'to':
                receiver = arg
                sent = True
        if old:
            return self._oldnotes(irc, msg, sender)
        if sent:
            return self._sentnotes(irc, msg, receiver)
        try:
            id = ircdb.users.getUserId(msg.prefix)
        except KeyError:
            irc.errorNotRegistered()
            return
        sql = """SELECT id, from_id, public
                 FROM notes
                 WHERE notes.to_id=%r AND notes.read=0""" % id
        if sender:
            try:
                sender = ircdb.users.getUserId(sender)
            except KeyError:
                irc.error('That user is not in my user database.')
                return
            sql = '%s %s' % (sql, 'AND notes.from_id=%r' % sender)
        db = self.dbHandler.getDb()
        cursor = db.cursor()
        cursor.execute(sql)
        count = cursor.rowcount
        if count < 1:
            irc.reply('You have no unread notes.')
        else:
            L = [self._formatNoteData(msg, *t) for t in cursor.fetchall()]
            L = self._condense(L)
            irc.reply(utils.commaAndify(L))

    def _condense(self, notes):
        temp = {}
        for note in notes:
            note = note.split(' ', 1)
            if note[1] in temp:
                temp[note[1]].append(note[0])
            else:
                temp[note[1]] = [note[0]]
        notes = []
        for (k,v) in temp.iteritems():
            notes.append('%s %s' % (utils.commaAndify(v), k))
        return notes

    def _sentnotes(self, irc, msg, receiver):
        """takes no arguments

        Returns a list of your most recent old notes.
        """
        try:
            id = ircdb.users.getUserId(msg.prefix)
        except KeyError:
            irc.errorNotRegistered()
            return
        sql = """SELECT id, to_id, public
                 FROM notes
                 WHERE notes.from_id=%r""" % id
        if receiver:
            try:
                receiver = ircdb.users.getUserId(receiver)
            except KeyError:
                irc.error('That user is not in my user database.')
                return
            sql = '%s %s' % (sql, 'AND notes.to_id=%r' % receiver)
        sql = '%s ORDER BY id DESC' % sql
        db = self.dbHandler.getDb()
        cursor = db.cursor()
        cursor.execute(sql)
        if cursor.rowcount < 1:
            irc.reply('I couldn\'t find any sent notes for your user.')
        else:
            ids = [self._formatNoteData(msg, sent=True, *t) for t in
                   cursor.fetchall()]
            ids = self._condense(ids)
            irc.reply(utils.commaAndify(ids))

    def _oldnotes(self, irc, msg, sender):
        """takes no arguments

        Returns a list of your most recent old notes.
        """
        try:
            id = ircdb.users.getUserId(msg.prefix)
        except KeyError:
            irc.errorNotRegistered()
            return
        sql = """SELECT id, from_id, public
                 FROM notes
                 WHERE notes.to_id=%r AND notes.read=1""" % id
        #self.log.warning(sender)
        if sender:
            try:
                sender = ircdb.users.getUserId(sender)
            except KeyError:
                irc.error('That user is not in my user database.')
                return
            sql = '%s %s' % (sql, 'AND notes.from_id=%r' % sender)
        sql = '%s ORDER BY id DESC' % sql
        db = self.dbHandler.getDb()
        cursor = db.cursor()
        cursor.execute(sql)
        #self.log.warning(cursor.rowcount)
        if cursor.rowcount < 1:
            irc.reply('I couldn\'t find any read notes for your user.')
        else:
            ids = [self._formatNoteData(msg, *t) for t in cursor.fetchall()]
            #self.log.warning(ids)
            ids = self._condense(ids)
            irc.reply(utils.commaAndify(ids))

Class = Note

# vim: shiftwidth=4 tabstop=8 expandtab textwidth=78:
<commit_msg>Added multiple recipient support.<commit_after>#!/usr/bin/env python

###
# Copyright (c) 2003, Brett Kelly
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#   * Redistributions of source code must retain the above copyright notice,
#     this list of conditions, and the following disclaimer.
#   * Redistributions in binary form must reproduce the above copyright notice,
#     this list of conditions, and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
#   * Neither the name of the author of this software nor the name of
#     contributors to this software may be used to endorse or promote products
#     derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###

"""
A complete messaging system that allows users to leave 'notes' for other
users that can be retrieved later.
"""

__revision__ = "$Id$"

import plugins

import sets
import time
import getopt
import os.path
from itertools import imap

import conf
import utils
import ircdb
import ircmsgs
import plugins
import privmsgs
import registry
import ircutils
import callbacks

try:
    import sqlite
except ImportError:
    raise callbacks.Error, 'You need to have PySQLite installed to use this ' \
                           'plugin.  Download it at <http://pysqlite.sf.net/>'

conf.registerPlugin('Note')
conf.registerGlobalValue(conf.supybot.plugins.Note, 'notifyOnJoin',
    registry.Boolean(False, """Determines whether the bot will notify people of
    their new messages when they join the channel.  Normally it will notify
    them when they send a message to the channel, since oftentimes joins are
    the result of netsplits and not the actual presence of the user."""))
conf.registerGlobalValue(conf.supybot.plugins.Note, 'notifyOnJoinRepeatedly',
    registry.Boolean(False, """Determines whether the bot will repeatedly
    notify people of their new messages when they join the channel.  That means
    when they join the channel, the bot will tell them they have unread
    messages, even if it's told them before."""))
conf.registerUserValue(conf.users.plugins.Note, 'notifyWithNotice',
    registry.Boolean(False, """Determines whether the bot will notify users of
    new messages with a NOTICE, rather than a PRIVMSG, so their clients won't
    open a new query window."""))

class Ignores(registry.SpaceSeparatedListOfStrings):
    List = ircutils.IrcSet

conf.registerUserValue(conf.users.plugins.Note, 'ignores', Ignores([], ''))

class NoteDb(plugins.DBHandler):
    def makeDb(self, filename):
        "create Notes database and tables"
        if os.path.exists(filename):
            db = sqlite.connect(filename)
        else:
            db = sqlite.connect(filename, converters={'bool': bool})
            cursor = db.cursor()
            cursor.execute("""CREATE TABLE notes (
                              id INTEGER PRIMARY KEY,
                              from_id INTEGER,
                              to_id INTEGER,
                              added_at TIMESTAMP,
                              notified BOOLEAN,
                              read BOOLEAN,
                              public BOOLEAN,
                              note TEXT
                              )""")
            db.commit()
        return db

class Note(callbacks.Privmsg):
    def __init__(self):
        callbacks.Privmsg.__init__(self)
        dataDir = conf.supybot.directories.data()
        self.dbHandler = NoteDb(name=os.path.join(dataDir, 'Notes'))

    def setAsRead(self, id):
        db = self.dbHandler.getDb()
        cursor = db.cursor()
        cursor.execute("""UPDATE notes
                          SET read=1, notified=1
                          WHERE id=%s""", id)
        db.commit()

    def die(self):
        self.dbHandler.die()

    def doPrivmsg(self, irc, msg):
        self._notify(irc, msg)

    def doJoin(self, irc, msg):
        if self.registryValue('notifyOnJoin'):
            repeatedly = self.registryValue('notifyOnJoinRepeatedly')
            self._notify(irc, msg, repeatedly)

    def _notify(self, irc, msg, repeatedly=False):
        try:
            id = ircdb.users.getUserId(msg.prefix)
        except KeyError:
            return
        db = self.dbHandler.getDb()
        cursor = db.cursor()
        cursor.execute("""SELECT COUNT(*) FROM notes
                          WHERE notes.to_id=%s AND notified=0""", id)
        unnotified = int(cursor.fetchone()[0])
        if unnotified != 0 or repeatedly:
            cursor.execute("""SELECT COUNT(*) FROM notes
                              WHERE notes.to_id=%s AND read=0""", id)
            unread = int(cursor.fetchone()[0])
            s = 'You have %s; ' \
                '%s that I haven\'t told you about before now..' % \
                (utils.nItems('note', unread, 'unread'), unnotified)
            maker = ircmsgs.privmsg
            if self.userValue('notifyWithNotice', msg.prefix):
                maker = ircmsgs.notice
            irc.queueMsg(maker(msg.nick, s))
            cursor.execute("""UPDATE notes SET notified=1
                              WHERE notes.to_id=%s""", id)
            db.commit()

    def getUserId(self, irc, name):
        if ircdb.users.hasUser(name):
            return ircdb.users.getUserId(name)
        else:
            try:
                hostmask = irc.state.nickToHosmtask(name)
                return ircdb.users.getUserId(hostmask)
            except KeyError:
                return None

    def send(self, irc, msg, args):
        """<recipient>,[<recipient>,[...]] <text>

        Sends a new note to the user specified.  Multiple recipients may be
        specified by separating their names by commas, with *no* spaces
        between.
        """
        (names, note) = privmsgs.getArgs(args, required=2)
        # Let's get the from user.
        try:
            fromId = ircdb.users.getUserId(msg.prefix)
            senderName = ircdb.users.getUser(fromId).name
        except KeyError:
            irc.errorNotRegistered()
            return
        # Let's get the publicitousness.
        if ircutils.isChannel(msg.args[0]):
            public = 1
        else:
            public = 0

        names = names.split(',')
        ids = [self.getUserId(irc, name) for name in names]
        badnames = []
        if None in ids:
            for (id, name) in zip(ids, names):
                if id is None:
                    badnames.append(name)
            irc.errorNoUser(name=utils.commaAndify(badnames, And='or'))
            return

        for name in names:
            if senderName in self.userValue('ignores', name):
                badnames.append(name)
        if badnames:
            irc.error('%s %s ignoring notes from you.' % \
                      (utils.commaAndify(badnames), utils.be(len(badnames))))
            return
        db = self.dbHandler.getDb()
        cursor = db.cursor()
        now = int(time.time())
        sent = []
        for (name, toId) in zip(names, ids):
            cursor.execute("""INSERT INTO notes VALUES
                              (NULL, %s, %s, %s, 0, 0, %s, %s)""",
                           fromId, toId, now, public, note)
            cursor.execute("""SELECT id FROM notes WHERE
                              from_id=%s AND to_id=%s AND added_at=%s""",
                           fromId, toId, now)
            s = 'note %s sent to %s' % (cursor.fetchone()[0], name)
            sent.append(s)
        db.commit()
        irc.reply(utils.commaAndify(sent).capitalize() + '.')

    def unsend(self, irc, msg, args):
        """<id>

        Unsends the note with the id given.  You must be the
        author of the note, and it must be unread.
        """
        id = privmsgs.getArgs(args)
        try:
            userid = ircdb.users.getUserId(msg.prefix)
        except KeyError:
            irc.errorNotRegistered()
            return
        db = self.dbHandler.getDb()
        cursor = db.cursor()
        cursor.execute("""SELECT from_id, read FROM notes WHERE id=%s""", id)
        if cursor.rowcount < 1:
            irc.error('That\'s not a valid note id.')
            return
        (from_id, read) = map(int, cursor.fetchone())
        if from_id == userid:
            if not read:
                cursor.execute("""DELETE FROM notes WHERE id=%s""", id)
                db.commit()
                irc.replySuccess()
            else:
                irc.error('That note has been read already.')
        else:
            irc.error('That note wasn\'t sent by you.')

    def note(self, irc, msg, args):
        """<note id>

        Retrieves a single note by its unique note id.  Use the 'note list'
        command to see what unread notes you have.
        """
        noteid = privmsgs.getArgs(args)
        if noteid.startswith('get'):
            irc.error('The Note.get command has changed to be simply "note".')
            return
        try:
            id = ircdb.users.getUserId(msg.prefix)
        except KeyError:
            irc.errorNotRegistered()
            return
        try:
            noteid = int(noteid)
        except ValueError:
            irc.error('%r is not a valid note id.' % noteid)
            return
        db = self.dbHandler.getDb()
        cursor = db.cursor()
        cursor.execute("""SELECT note, to_id, from_id, added_at, public
                          FROM notes
                          WHERE (to_id=%s OR from_id=%s) AND id=%s""",
                       id, id, noteid)
        if cursor.rowcount < 1:
            s = 'You may only retrieve notes you\'ve sent or received.'
            irc.error(s)
            return
        (note, toId, fromId, addedAt, public) = cursor.fetchone()
        (toId,fromId,addedAt,public) = imap(int, (toId,fromId,addedAt,public))
        elapsed = utils.timeElapsed(time.time() - addedAt)
        if toId == id:
            author = ircdb.users.getUser(fromId).name
            newnote = '%s (Sent by %s %s ago)' % (note, author, elapsed)
        elif fromId == id:
            recipient = ircdb.users.getUser(toId).name
            newnote = '%s (Sent to %s %s ago)' % (note, recipient, elapsed)
        irc.reply(newnote, private=(not public))
        self.setAsRead(noteid)

    def _formatNoteData(self, msg, id, fromId, public, sent=False):
        (id, fromId, public) = imap(int, (id, fromId, public))
        if public or not ircutils.isChannel(msg.args[0]):
            sender = ircdb.users.getUser(fromId).name
            if sent:
                return '#%s to %s' % (id, sender)
            else:
                return '#%s from %s' % (id, sender)
        else:
            return '#%s (private)' % id

    def ignore(self, irc, msg, args):
        """[--remove] <user>

        Ignores all messages from <user>.  If --remove is listed, remove <user>
        from the list of users being ignored.
        """
        remove = False
        while '--remove' in args:
            remove = True
            args.remove('--remove')
        user = privmsgs.getArgs(args)
        try:
            L = self.userValue('ignores', msg.prefix)
            if remove:
                try:
                    L.remove(user)
                except (KeyError, ValueError):
                    irc.error('%r was not in your list of ignores.' % user)
                    return
            else:
                L.add(user)
            self.setUserValue(msg.prefix, 'ignores', L, setValue=True)
            irc.replySuccess()
        except KeyError:
            irc.errorNoUser()

    def list(self, irc, msg, args):
        """[--{old,sent}] [--{from,to} <user>]

        Retrieves the ids of all your unread notes.  If --old is given, list
        read notes.  If --sent is given, list notes that you have sent.  If
        --from is specified, only lists notes sent to you from <user>.  If
        --to is specified, only lists notes sent by you to <user>.
        """
        options = ['old', 'sent', 'from=', 'to=']
        (optlist, rest) = getopt.getopt(args, '', options)
        sender, receiver, old, sent = ('', '', False, False)
        for (option, arg) in optlist:
            option = option.lstrip('-')
            if option == 'old':
                old = True
            if option == 'sent':
                sent = True
            if option == 'from':
                sender = arg
            if option == 'to':
                receiver = arg
                sent = True
        if old:
            return self._oldnotes(irc, msg, sender)
        if sent:
            return self._sentnotes(irc, msg, receiver)
        try:
            id = ircdb.users.getUserId(msg.prefix)
        except KeyError:
            irc.errorNotRegistered()
            return
        sql = """SELECT id, from_id, public
                 FROM notes
                 WHERE notes.to_id=%r AND notes.read=0""" % id
        if sender:
            try:
                sender = ircdb.users.getUserId(sender)
            except KeyError:
                irc.error('That user is not in my user database.')
                return
            sql = '%s %s' % (sql, 'AND notes.from_id=%r' % sender)
        db = self.dbHandler.getDb()
        cursor = db.cursor()
        cursor.execute(sql)
        count = cursor.rowcount
        if count < 1:
            irc.reply('You have no unread notes.')
        else:
            L = [self._formatNoteData(msg, *t) for t in cursor.fetchall()]
            L = self._condense(L)
            irc.reply(utils.commaAndify(L))

    def _condense(self, notes):
        temp = {}
        for note in notes:
            note = note.split(' ', 1)
            if note[1] in temp:
                temp[note[1]].append(note[0])
            else:
                temp[note[1]] = [note[0]]
        notes = []
        for (k,v) in temp.iteritems():
            notes.append('%s %s' % (utils.commaAndify(v), k))
        return notes

    def _sentnotes(self, irc, msg, receiver):
        """takes no arguments

        Returns a list of your most recent old notes.
        """
        try:
            id = ircdb.users.getUserId(msg.prefix)
        except KeyError:
            irc.errorNotRegistered()
            return
        sql = """SELECT id, to_id, public
                 FROM notes
                 WHERE notes.from_id=%r""" % id
        if receiver:
            try:
                receiver = ircdb.users.getUserId(receiver)
            except KeyError:
                irc.error('That user is not in my user database.')
                return
            sql = '%s %s' % (sql, 'AND notes.to_id=%r' % receiver)
        sql = '%s ORDER BY id DESC' % sql
        db = self.dbHandler.getDb()
        cursor = db.cursor()
        cursor.execute(sql)
        if cursor.rowcount < 1:
            irc.reply('I couldn\'t find any sent notes for your user.')
        else:
            ids = [self._formatNoteData(msg, sent=True, *t) for t in
                   cursor.fetchall()]
            ids = self._condense(ids)
            irc.reply(utils.commaAndify(ids))

    def _oldnotes(self, irc, msg, sender):
        """takes no arguments

        Returns a list of your most recent old notes.
        """
        try:
            id = ircdb.users.getUserId(msg.prefix)
        except KeyError:
            irc.errorNotRegistered()
            return
        sql = """SELECT id, from_id, public
                 FROM notes
                 WHERE notes.to_id=%r AND notes.read=1""" % id
        #self.log.warning(sender)
        if sender:
            try:
                sender = ircdb.users.getUserId(sender)
            except KeyError:
                irc.error('That user is not in my user database.')
                return
            sql = '%s %s' % (sql, 'AND notes.from_id=%r' % sender)
        sql = '%s ORDER BY id DESC' % sql
        db = self.dbHandler.getDb()
        cursor = db.cursor()
        cursor.execute(sql)
        #self.log.warning(cursor.rowcount)
        if cursor.rowcount < 1:
            irc.reply('I couldn\'t find any read notes for your user.')
        else:
            ids = [self._formatNoteData(msg, *t) for t in cursor.fetchall()]
            #self.log.warning(ids)
            ids = self._condense(ids)
            irc.reply(utils.commaAndify(ids))

Class = Note

# vim: shiftwidth=4 tabstop=8 expandtab textwidth=78:
<|endoftext|>


<commit_before>import plugin
import logging
import exceptions

from lurklib.exceptions import _Exceptions

class Plugin(plugin.baseplugin):
    def __init__(self, bot):
        bot.register_callback("load", self.load)
        bot.register_callback("quit", self.quit)
        bot.register_callback("part", self.part)
        bot.register_callback("join", self.join)

    def is_admin(self, bot, user):
        if user in bot._admins:
            logging.debug("%s is an admin" % user)
            return True
        else:
            logging.debug("%s is not an admin" % user)
            return False

    def load(self, bot, user, channel, text):
        if self.is_admin(bot, user):
            try:
                bot.plugger.load_plugin(text.split()[1])
            except exceptions.NoSuchPlugin:
                bot.privmsg(channel, "%s could not be found" % text[1])

    def part(self, bot, user, channel, text):
        if self.is_admin(bot, user):
            try:
                logging.debug("Parting %s" % text.split()[1])
                bot.part(text.split()[1])
            except _Exceptions.NotInChannel, e:
                bot.privmsg(channel, "Sorry, %s, I'm not in %s" % (user,
                    text.split()[1]))

    def join(self, bot, user, channel, text):
        if self.is_admin(bot, user):
            chan = text.split()[1]
            try:
                logging.debug("Joining %s" % chan)
                bot.join(chan)
            except _Exceptions.ChannelIsFull, e:
                bot.privmsg(channel, "Sorry, %s is full." % chan)
            except _Exceptions.NoSuchChannel, e:
                bot.privmsg(channel, "Sorry, %s doesn't exist." % chan)
            except _Exceptions.TooManyChannels, e:
                bot.privmsg(channel, "Sorry, I'm already in enough channels.")
            except _Exceptions.InviteOnlyChan, e:
                bot.privmsg(channel, "Sorry, invite only.")
            except _Exceptions.AlreadyInChannel, e:
                bot.privmsg(channel, "I'm already there!")

    def quit(self, bot, user, channel, text):
        if self.is_admin(bot, user):
            logging.debug("Quitting")
            bot.quit("leaving")
<commit_msg>Add a !server command<commit_after>import plugin
import logging
import exceptions

from lurklib.exceptions import _Exceptions

class Plugin(plugin.baseplugin):
    def __init__(self, bot):
        bot.register_callback("load", self.load)
        bot.register_callback("quit", self.quit)
        bot.register_callback("part", self.part)
        bot.register_callback("join", self.join)
        bot.register_callback("server", self.server)

    def is_admin(self, bot, user):
        if user in bot._admins:
            logging.debug("%s is an admin" % user)
            return True
        else:
            logging.debug("%s is not an admin" % user)
            return False

    def load(self, bot, user, channel, text):
        if self.is_admin(bot, user):
            try:
                bot.plugger.load_plugin(text.split()[1])
            except exceptions.NoSuchPlugin:
                bot.privmsg(channel, "%s could not be found" % text[1])

    def part(self, bot, user, channel, text):
        if self.is_admin(bot, user):
            try:
                logging.debug("Parting %s" % text.split()[1])
                bot.part(text.split()[1])
            except _Exceptions.NotInChannel, e:
                bot.privmsg(channel, "Sorry, %s, I'm not in %s" % (user,
                    text.split()[1]))

    def join(self, bot, user, channel, text):
        if self.is_admin(bot, user):
            chan = text.split()[1]
            try:
                logging.debug("Joining %s" % chan)
                bot.join(chan)
            except _Exceptions.ChannelIsFull, e:
                bot.privmsg(channel, "Sorry, %s is full." % chan)
            except _Exceptions.NoSuchChannel, e:
                bot.privmsg(channel, "Sorry, %s doesn't exist." % chan)
            except _Exceptions.TooManyChannels, e:
                bot.privmsg(channel, "Sorry, I'm already in enough channels.")
            except _Exceptions.InviteOnlyChan, e:
                bot.privmsg(channel, "Sorry, invite only.")
            except _Exceptions.AlreadyInChannel, e:
                bot.privmsg(channel, "I'm already there!")

    def quit(self, bot, user, channel, text):
        if self.is_admin(bot, user):
            logging.debug("Quitting")
            bot.quit("leaving")

    def server(self, bot, user, channel, text):
        """Shows the server the bot is connected to"""
        bot.privmsg(user, bot.server)
<|endoftext|>


<commit_before>from modules.basemodule import BaseModule
import mapper.libmapper
import collections
import json
import os
import pprint
import re
import shutil
import time

class Mapper(BaseModule):
    def help(self, args):
        strs = ["Commands:"]
        for cmd in self.commands.keys():
            strs.append(cmd)
        self.log('\n'.join(strs))

    def current(self):
        return self.gmcp['room']['info']['num']

    def here(self, args):
        if args:
            this = int(args[0])
        else:
            this = self.current()

        just_exits = self.m.getRoomExits(this)
        exits = {}
        for dir, tgt in just_exits.items():
            data = self.m.getExitData(this, tgt)
            data = json.loads(data) if data else {}
            exits[dir] = {'dst': tgt, 'data': data}

        self.log('\n' + pprint.pformat({
            'num': this,
            'name': self.m.getRoomName(this),
            'data': self.m.getRoomData(this),
            'coords': self.m.getRoomCoords(this),
            'exits': exits,
            }))

    def path(self, there):
        if there in self.data['bookmarks']:
            there = self.data['bookmarks'][there]
        else:
            try:
                there = int(there)
            except ValueError:
                self.log("No such bookmark")
                return

        this = self.current()
        if this == there:
            self.log("Already there!")
            return ''
        then = time.time()
        path = self.m.findPath(this, there)
        self.log("{} (found in {} seconds)".format(path, time.time() - then))
        return path

    def go(self, room):
        path = self.path(room)
        if path:
            self.send(path.replace(';', '\n'))

    def bookmarks(self, args):
        self.log('Bookmarks:\n' + pprint.pformat(self.data['bookmarks']))

    def bookmark(self, args):
        arg = ' '.join(args)
        self.data['bookmarks'][arg] = self.current()
        self.bookmarks([])

    def draw(self, args, sizeX=None, sizeY=None):
        # Draw room at x,y,z. Enumerate exits. For each exit target, breadth-first, figure out its new dimensions, rinse, repeat.
        # █▓▒░
        oneArea = len(args) == 1 and args[0] == 'all'
        if sizeX and sizeY:
            columns, lines = sizeX, sizeY
        else:
            columns, lines = shutil.get_terminal_size((21, 22))

        def adjustExit(x, y, d, prev):
            if d == 'n':
                return x, y-1, '│', '↑', '║'
            if d == 'w':
                return x-1, y, '─', '←', '═'
            if d == 's':
                return x, y+1, '│', '↓', '║'
            if d == 'e':
                return x+1, y, '─', '→', '═'
            if d == 'd':
                if prev == '▲':
                    return x, y, '◆', '◆', '◆'
                else:
                    return x, y, '▼', '▼', '▼'
            if d == 'u':
                if prev == '▼':
                    return x, y, '◆', '◆', '◆'
                else:
                    return x, y, '▲', '▲', '▲'
            # TODO: test these in SneezyMUD
            if d == 'nw':
                return x-1, y-1, '/', '/', '/'
            if d == 'sw':
                return x-1, y+1, '\\', '\\', '\\'
            if d == 'se':
                return x+1, y+1, '/', '/', '/'
            if d == 'ne':
                return x+1, y-1, '\\', '\\', '\\'

        out = []  # NB! indices are out[y][x] because the greater chunks are whole lines
        for _ in range(lines - 1):  # -1 for the next prompt
            out.append([' '] * columns)

        # The only room coordinates that matter are the start room's -- the rest get calculated by tracing paths.
        startX, startY, startZ = self.m.getRoomCoords(self.current())
        centerX, centerY = (columns-1)//2, (lines-1)//2
        data = self.m.getRoomData(self.current())
        area = json.loads(data)['zone']

        roomq = collections.deque()
        roomq.append((centerX, centerY, self.current()))

        visited = set()

        def getExitLen(source, target):
            exitDataS = self.m.getExitData(source, target)
            if not exitDataS:
                return 1
            exitData = json.loads(exitDataS)
            if not exitData or 'len' not in exitData:
                return 1
            return int(exitData['len'])

        def fits(x, y):
            return 0 <= x and x < columns and 0 <= y and y < lines-1

        # TODO: one-way exits
        # TODO: draw doors
        coordCache = {}  # Remember where we drew each room, to search for broken-looking exits
        while roomq:
            drawX, drawY, room = roomq.popleft()
            if room not in visited:  # A given room might end up in the queue through different paths
                mapX, mapY, mapZ = self.m.getRoomCoords(room)
                visited.add(room)
                # It's possible to keep walking through z layers and end up back on z=initial, which might produce nicer maps -- but we'll have to walk the _whole_ map, or bound by some range.
                out[drawY][drawX] = '█'
                coordCache[room] = (drawX, drawY)
                # out[drawY][drawX] = str(count % 10)
                # count += 1
                exits = self.m.getRoomExits(room)
                for d, tgt in exits.items():
                    if d in ['n', 'e', 's', 'w', 'u', 'd', 'ne', 'se', 'sw', 'nw']:
                        dataS = self.m.getRoomData(tgt)
                        exists = dataS != ''
                        dataD = json.loads(dataS) if exists else {}
                        nextArea = dataD['zone'] if 'zone' in dataD else None
                        sameAreas = oneArea or nextArea == area

                        if not exists or not sameAreas:
                            exitLen = 1
                        else:
                            exitLen = getExitLen(room, tgt)

                        exX = drawX
                        exY = drawY

                        roomX, roomY = exX, exY
                        # Figure out the coordinates of the target room
                        for _ in range(exitLen + 1):  # exitlen for the exit, +1 for the target room
                            roomX, roomY, _, _, _ = adjustExit(roomX, roomY, d, ' ')

                        # Mark exits that break map (if the target room is already drawn, but not adjacent to this one)
                        mark = False
                        if tgt in visited:
                            tgtX, tgtY = coordCache[tgt]
                            if tgtX != roomX or tgtY != roomY:
                                print("Offset detected:", roomX - tgtX, roomY-tgtX)
                                mark = True

                        # draw a long exit for beautification
                        for _ in range(exitLen):
                            exX, exY, regularExit, hiddenExit, markedExit = adjustExit(exX, exY, d, out[drawY][drawX])
                            if fits(exX, exY):
                                # If the map grid element we'd occupy is already occupied, don't go there
                                nextX, nextY, _, _, _ = adjustExit(exX, exY, d, ' ')  # Adjust again, ie. go one step further in the same direction for the target room
                                # Don't overwrite already drawn areas
                                free = fits(exX, exY) and (not fits(nextX, nextY) or out[nextY][nextX] == ' ') or tgt in visited

                                if mark:
                                    out[exY][exX] = markedExit
                                elif free and exists and sameAreas:
                                    out[exY][exX] = regularExit
                                else:
                                    out[exY][exX] = hiddenExit

                        visit = (exists
                                and tgt not in visited
                                and sameAreas
                                and d not in ['u', 'd']
                                and fits(roomX, roomY)
                                and out[roomY][roomX] == ' '
                                )
                        if visit:
                            roomq.append((roomX, roomY, tgt))

        # Special marking for start room:
        if out[centerY][centerX] == '▼':
            out[centerY][centerX] = '▿'
        elif out[centerY][centerX] == '▲':
            out[centerY][centerX] = '▵'
        elif out[centerY][centerX] == '◆':
            out[centerY][centerX] = '◇'
        else:
            out[centerY][centerX] = '░'

        outlines = [''.join(char) for char in out]
        outstr = ""
        for l in outlines:
            if l.strip(' '):
                outstr += l + '\n'
        return outstr

    def quit(self, args=None):
        self.save([self.mapfname])

    def save(self, args):
        if len(args) == 1:
            self.mapfname = args[0]
        self.m.setMapData(json.dumps(self.data))
        with open(self.mapfname, 'w') as f:
            f.write(self.m.serialize())
        self.log("Serialized map to", self.mapfname)

    def startExit(self, args):
        self.exitKw = ' '.join(args)
        room = self.gmcp['room']['info']
        self.exitFrom = {}
        self.exitFrom['exits'] = {}
        self.exitFrom['id'] = room['num']
        self.exitFrom['name'] = room['name']
        self.exitFrom['data'] = dict(zone=room['zone'], terrain = room['terrain'])
        exits = {}
        for k, v in room['exits'].items():
            self.exitFrom['exits'][k.lower()] = v
        self.log("Type '#map endexit' when you're in the right room, or #map endexit abort")
        self.send(self.exitKw.replace(';', '\n'))

    def endExit(self, args):
        if len(args) == 1:
            self.log("Aborted.")
            return
        self.exitFrom['exits'][self.exitKw] = self.current()
        self.m.addRoom(
                self.exitFrom['id'],
                self.exitFrom['name'],
                json.dumps(self.exitFrom['data']),
                self.exitFrom['exits'])
        self.exitKw = None

    def getRoomByDirection(self, direction):
        here = self.current()
        exits = self.m.getRoomExits(here)
        if direction.lower() not in exits:
            self.log("No such direction")
            return None
        return exits[direction.lower()]

    def exitLen(self, direction, increment):
        here = self.current()
        there = self.getRoomByDirection(direction)

        def do(here, there):
            data = self.m.getExitData(here, there)
            if data:
                data = json.loads(data)
            else:
                data = {}
            # Oops, must have stored it as string :( TODO: correct in files and remove this code

            data['len'] = int(data['len']) if 'len' in data else 1
            data['len'] += increment
            if data['len'] <= 1:
                data['len'] = 1
            self.m.setExitData(here, there, json.dumps(data))

        do(here, there)
        do(there, here)
        print(self.draw([]))

    def inc(self, args):
        self.exitLen(args[0], 2)

    def dec(self, args):
        self.exitLen(args[0], -2)

    def load(self, args):
        # TODO: memory usage and map size can be reduced by storing
        # terrains/zones in mapdata, and referencing them by index in rooms
        if len(args) == 1:
            self.mapfname = args[0]
        try:
            with open(self.mapfname, 'r') as f:
                ser = f.read()
            self.m = mapper.libmapper.Map(ser)
            print("Loaded map from", self.mapfname)
        except FileNotFoundError:
            self.m = mapper.libmapper.Map()
            print("Created a new map")

        md = self.m.getMapData()
        if md:
            self.data = json.loads(self.m.getMapData())
        else:
            self.data = {
                    'bookmarks': {},
                    }

    def find(self, args):
        self.log('\n' + pprint.pformat(self.m.findRoomByName(args[0])))

    def unmapped(self, args):
        out = set()
        visited = set()
        roomq = collections.deque()
        roomq.append(self.current())
        visited.add(self.current())
        startArea = json.loads(self.m.getRoomData(self.current()))['zone']
        while roomq:
            room = roomq.popleft()
            visited.add(room)
            exits = self.m.getRoomExits(room)
            for d, tgt in exits.items():
                dataS = self.m.getRoomData(tgt)
                if not dataS:
                    out.add(tgt)
                else:
                    if  tgt not in visited and json.loads(dataS)['zone'] == startArea:
                        roomq.append(tgt)
        return out

    def __init__(self, mud, mapfname='default.map'):
        self.load([mapfname])

        self.commands = {
                'unmapped': lambda args: self.log('\n' + '\n'.join([str(i) for i in self.unmapped(args)])),
                'gounmapped': lambda args: self.go(self.unmapped(args).pop()),
                'find': self.find,
                'load': self.load,
                'read': self.load,
                'help': self.help,
                'here': self.here,
                'draw': lambda args: print(self.draw(args)),
                'bookmark': self.bookmark,
                'name': self.bookmark,
                'bookmarks': self.bookmarks,
                'path': lambda args: self.path(args[0]),
                'go': lambda args: self.go(args[0]),
                'save': self.save,
                'write': self.save,
                'startexit': self.startExit,
                'endexit': self.endExit,
                'inc': self.inc,
                'dec': self.dec,
                }

        # for creating custom exits
        self.exitKw = None
        self.exitFrom = None

        super().__init__(mud)

    def alias(self, line):
        words = line.split(' ')

        if words[0] != '#map':
            return

        if len(words) == 1:
            print(self.draw([]))
            return True

        cmd = words[1]
        if cmd in self.commands:
            self.commands[cmd](words[2:])
        else:
            self.help(words[2:])
        return True

    def handleGmcp(self, cmd, value):
        # room.info
        # {'coord': {'cont': 0, 'id': 0, 'x': -1, 'y': -1},
        #   'desc': '',
        #   'details': '',
        #   'exits': {'N': -565511209},
        #   'id': 'Homes#1226',
        #   'name': 'An empty room',
        #   'num': -565511180,
        #   'terrain': 'cave',
        #   'zone': 'Homes'}
        if cmd == 'room.info':
            id = value['num']
            name = value['name']
            data = dict(zone=value['zone'], terrain = value['terrain'])
            exits = self.m.getRoomExits(id)  # retain custom exits
            for k, v in value['exits'].items():
                exits[k.lower()] = v
            self.m.addRoom(id, name, json.dumps(data), exits)

            with open('mapdraw', 'w') as f:
                f.write(self.draw([], 35, 35) + '\n')
<commit_msg>fix exit inc/dec against doors<commit_after>from modules.basemodule import BaseModule
import mapper.libmapper
import collections
import json
import os
import pprint
import re
import shutil
import time

class Mapper(BaseModule):
    def help(self, args):
        strs = ["Commands:"]
        for cmd in self.commands.keys():
            strs.append(cmd)
        self.log('\n'.join(strs))

    def current(self):
        return self.gmcp['room']['info']['num']

    def here(self, args):
        if args:
            this = int(args[0])
        else:
            this = self.current()

        just_exits = self.m.getRoomExits(this)
        exits = {}
        for dir, tgt in just_exits.items():
            data = self.m.getExitData(this, tgt)
            data = json.loads(data) if data else {}
            exits[dir] = {'dst': tgt, 'data': data}

        self.log('\n' + pprint.pformat({
            'num': this,
            'name': self.m.getRoomName(this),
            'data': self.m.getRoomData(this),
            'coords': self.m.getRoomCoords(this),
            'exits': exits,
            }))

    def path(self, there):
        if there in self.data['bookmarks']:
            there = self.data['bookmarks'][there]
        else:
            try:
                there = int(there)
            except ValueError:
                self.log("No such bookmark")
                return

        this = self.current()
        if this == there:
            self.log("Already there!")
            return ''
        then = time.time()
        path = self.m.findPath(this, there)
        self.log("{} (found in {} seconds)".format(path, time.time() - then))
        return path

    def go(self, room):
        path = self.path(room)
        if path:
            self.send(path.replace(';', '\n'))

    def bookmarks(self, args):
        self.log('Bookmarks:\n' + pprint.pformat(self.data['bookmarks']))

    def bookmark(self, args):
        arg = ' '.join(args)
        self.data['bookmarks'][arg] = self.current()
        self.bookmarks([])

    def draw(self, args, sizeX=None, sizeY=None):
        # Draw room at x,y,z. Enumerate exits. For each exit target, breadth-first, figure out its new dimensions, rinse, repeat.
        # █▓▒░
        oneArea = len(args) == 1 and args[0] == 'all'
        if sizeX and sizeY:
            columns, lines = sizeX, sizeY
        else:
            columns, lines = shutil.get_terminal_size((21, 22))

        def adjustExit(x, y, d, prev):
            m = re.match(r'open (.);.', d)
            if m:
                return adjustExit(x, y, m.group(1), prev)
            if d == 'n':
                return x, y-1, '│', '↑', '║'
            if d == 'w':
                return x-1, y, '─', '←', '═'
            if d == 's':
                return x, y+1, '│', '↓', '║'
            if d == 'e':
                return x+1, y, '─', '→', '═'
            if d == 'd':
                if prev == '▲':
                    return x, y, '◆', '◆', '◆'
                else:
                    return x, y, '▼', '▼', '▼'
            if d == 'u':
                if prev == '▼':
                    return x, y, '◆', '◆', '◆'
                else:
                    return x, y, '▲', '▲', '▲'
            # TODO: test these in SneezyMUD
            if d == 'nw':
                return x-1, y-1, '/', '/', '/'
            if d == 'sw':
                return x-1, y+1, '\\', '\\', '\\'
            if d == 'se':
                return x+1, y+1, '/', '/', '/'
            if d == 'ne':
                return x+1, y-1, '\\', '\\', '\\'

        out = []  # NB! indices are out[y][x] because the greater chunks are whole lines
        for _ in range(lines - 1):  # -1 for the next prompt
            out.append([' '] * columns)

        # The only room coordinates that matter are the start room's -- the rest get calculated by tracing paths.
        startX, startY, startZ = self.m.getRoomCoords(self.current())
        centerX, centerY = (columns-1)//2, (lines-1)//2
        data = self.m.getRoomData(self.current())
        area = json.loads(data)['zone']

        roomq = collections.deque()
        roomq.append((centerX, centerY, self.current()))

        visited = set()

        def getExitLen(source, target):
            exitDataS = self.m.getExitData(source, target)
            if not exitDataS:
                return 1
            exitData = json.loads(exitDataS)
            if not exitData or 'len' not in exitData:
                return 1
            return int(exitData['len'])

        def fits(x, y):
            return 0 <= x and x < columns and 0 <= y and y < lines-1

        # TODO: one-way exits
        # TODO: draw doors
        coordCache = {}  # Remember where we drew each room, to search for broken-looking exits
        while roomq:
            drawX, drawY, room = roomq.popleft()
            if room not in visited:  # A given room might end up in the queue through different paths
                mapX, mapY, mapZ = self.m.getRoomCoords(room)
                visited.add(room)
                # It's possible to keep walking through z layers and end up back on z=initial, which might produce nicer maps -- but we'll have to walk the _whole_ map, or bound by some range.
                out[drawY][drawX] = '█'
                coordCache[room] = (drawX, drawY)
                # out[drawY][drawX] = str(count % 10)
                # count += 1
                exits = self.m.getRoomExits(room)
                for d, tgt in exits.items():
                    if d in ['n', 'e', 's', 'w', 'u', 'd', 'ne', 'se', 'sw', 'nw']:
                        dataS = self.m.getRoomData(tgt)
                        exists = dataS != ''
                        dataD = json.loads(dataS) if exists else {}
                        nextArea = dataD['zone'] if 'zone' in dataD else None
                        sameAreas = oneArea or nextArea == area

                        if not exists or not sameAreas:
                            exitLen = 1
                        else:
                            exitLen = getExitLen(room, tgt)

                        exX = drawX
                        exY = drawY

                        roomX, roomY = exX, exY
                        # Figure out the coordinates of the target room
                        for _ in range(exitLen + 1):  # exitlen for the exit, +1 for the target room
                            roomX, roomY, _, _, _ = adjustExit(roomX, roomY, d, ' ')

                        # Mark exits that break map (if the target room is already drawn, but not adjacent to this one)
                        mark = False
                        if tgt in visited:
                            tgtX, tgtY = coordCache[tgt]
                            if tgtX != roomX or tgtY != roomY:
                                print("Offset detected:", roomX - tgtX, roomY-tgtX)
                                mark = True

                        # draw a long exit for beautification
                        for _ in range(exitLen):
                            exX, exY, regularExit, hiddenExit, markedExit = adjustExit(exX, exY, d, out[drawY][drawX])
                            if fits(exX, exY):
                                # If the map grid element we'd occupy is already occupied, don't go there
                                nextX, nextY, _, _, _ = adjustExit(exX, exY, d, ' ')  # Adjust again, ie. go one step further in the same direction for the target room
                                # Don't overwrite already drawn areas
                                free = fits(exX, exY) and (not fits(nextX, nextY) or out[nextY][nextX] == ' ') or tgt in visited

                                if mark:
                                    out[exY][exX] = markedExit
                                elif free and exists and sameAreas:
                                    out[exY][exX] = regularExit
                                else:
                                    out[exY][exX] = hiddenExit

                        visit = (exists
                                and tgt not in visited
                                and sameAreas
                                and d not in ['u', 'd']
                                and fits(roomX, roomY)
                                and out[roomY][roomX] == ' '
                                )
                        if visit:
                            roomq.append((roomX, roomY, tgt))

        # Special marking for start room:
        if out[centerY][centerX] == '▼':
            out[centerY][centerX] = '▿'
        elif out[centerY][centerX] == '▲':
            out[centerY][centerX] = '▵'
        elif out[centerY][centerX] == '◆':
            out[centerY][centerX] = '◇'
        else:
            out[centerY][centerX] = '░'

        outlines = [''.join(char) for char in out]
        outstr = ""
        for l in outlines:
            if l.strip(' '):
                outstr += l + '\n'
        return outstr

    def quit(self, args=None):
        self.save([self.mapfname])

    def save(self, args):
        if len(args) == 1:
            self.mapfname = args[0]
        self.m.setMapData(json.dumps(self.data))
        with open(self.mapfname, 'w') as f:
            f.write(self.m.serialize())
        self.log("Serialized map to", self.mapfname)

    def startExit(self, args):
        self.exitKw = ' '.join(args)
        room = self.gmcp['room']['info']
        self.exitFrom = {}
        self.exitFrom['exits'] = {}
        self.exitFrom['id'] = room['num']
        self.exitFrom['name'] = room['name']
        self.exitFrom['data'] = dict(zone=room['zone'], terrain = room['terrain'])
        exits = {}
        for k, v in room['exits'].items():
            self.exitFrom['exits'][k.lower()] = v
        self.log("Type '#map endexit' when you're in the right room, or #map endexit abort")
        self.send(self.exitKw.replace(';', '\n'))

    def endExit(self, args):
        if len(args) == 1:
            self.log("Aborted.")
            return
        self.exitFrom['exits'][self.exitKw] = self.current()
        self.m.addRoom(
                self.exitFrom['id'],
                self.exitFrom['name'],
                json.dumps(self.exitFrom['data']),
                self.exitFrom['exits'])
        self.exitKw = None

    def getRoomByDirection(self, direction):
        here = self.current()
        exits = self.m.getRoomExits(here)
        if direction.lower() not in exits:
            self.log("No such direction")
            return None
        return exits[direction.lower()]

    def exitLen(self, direction, increment):
        here = self.current()
        there = self.getRoomByDirection(direction)

        def do(here, there):
            data = self.m.getExitData(here, there)
            if data:
                data = json.loads(data)
            else:
                data = {}
            # Oops, must have stored it as string :( TODO: correct in files and remove this code

            data['len'] = int(data['len']) if 'len' in data else 1
            data['len'] += increment
            if data['len'] <= 1:
                data['len'] = 1
            self.m.setExitData(here, there, json.dumps(data))

        do(here, there)
        do(there, here)
        print(self.draw([]))

    def inc(self, args):
        self.exitLen(args[0], 2)

    def dec(self, args):
        self.exitLen(args[0], -2)

    def load(self, args):
        # TODO: memory usage and map size can be reduced by storing
        # terrains/zones in mapdata, and referencing them by index in rooms
        if len(args) == 1:
            self.mapfname = args[0]
        try:
            with open(self.mapfname, 'r') as f:
                ser = f.read()
            self.m = mapper.libmapper.Map(ser)
            print("Loaded map from", self.mapfname)
        except FileNotFoundError:
            self.m = mapper.libmapper.Map()
            print("Created a new map")

        md = self.m.getMapData()
        if md:
            self.data = json.loads(self.m.getMapData())
        else:
            self.data = {
                    'bookmarks': {},
                    }

    def find(self, args):
        self.log('\n' + pprint.pformat(self.m.findRoomByName(args[0])))

    def unmapped(self, args):
        out = set()
        visited = set()
        roomq = collections.deque()
        roomq.append(self.current())
        visited.add(self.current())
        startArea = json.loads(self.m.getRoomData(self.current()))['zone']
        while roomq:
            room = roomq.popleft()
            visited.add(room)
            exits = self.m.getRoomExits(room)
            for d, tgt in exits.items():
                dataS = self.m.getRoomData(tgt)
                if not dataS:
                    out.add(tgt)
                else:
                    if  tgt not in visited and json.loads(dataS)['zone'] == startArea:
                        roomq.append(tgt)
        return out

    def __init__(self, mud, mapfname='default.map'):
        self.load([mapfname])

        self.commands = {
                'unmapped': lambda args: self.log('\n' + '\n'.join([str(i) for i in self.unmapped(args)])),
                'gounmapped': lambda args: self.go(self.unmapped(args).pop()),
                'find': self.find,
                'load': self.load,
                'read': self.load,
                'help': self.help,
                'here': self.here,
                'draw': lambda args: print(self.draw(args)),
                'bookmark': self.bookmark,
                'name': self.bookmark,
                'bookmarks': self.bookmarks,
                'path': lambda args: self.path(args[0]),
                'go': lambda args: self.go(args[0]),
                'save': self.save,
                'write': self.save,
                'startexit': self.startExit,
                'endexit': self.endExit,
                'inc': self.inc,
                'dec': self.dec,
                }

        # for creating custom exits
        self.exitKw = None
        self.exitFrom = None

        super().__init__(mud)

    def alias(self, line):
        words = line.split(' ')

        if words[0] != '#map':
            return

        if len(words) == 1:
            print(self.draw([]))
            return True

        cmd = words[1]
        if cmd in self.commands:
            self.commands[cmd](words[2:])
        else:
            self.help(words[2:])
        return True

    def handleGmcp(self, cmd, value):
        # room.info
        # {'coord': {'cont': 0, 'id': 0, 'x': -1, 'y': -1},
        #   'desc': '',
        #   'details': '',
        #   'exits': {'N': -565511209},
        #   'id': 'Homes#1226',
        #   'name': 'An empty room',
        #   'num': -565511180,
        #   'terrain': 'cave',
        #   'zone': 'Homes'}
        if cmd == 'room.info':
            id = value['num']
            name = value['name']
            data = dict(zone=value['zone'], terrain = value['terrain'])
            exits = self.m.getRoomExits(id)  # retain custom exits
            for k, v in value['exits'].items():
                exits[k.lower()] = v
            self.m.addRoom(id, name, json.dumps(data), exits)

            with open('mapdraw', 'w') as f:
                f.write(self.draw([], 35, 35) + '\n')
<|endoftext|>