mirror of
https://github.com/Mikaela/Limnoria.git
synced 2024-11-30 06:49:24 +01:00
Initial checkin.
This commit is contained in:
parent
234d4d8ef9
commit
e5e98fdc2f
145
plugins/Note.py
145
plugins/Note.py
@ -43,8 +43,11 @@ import sets
|
||||
import time
|
||||
import getopt
|
||||
import os.path
|
||||
import operator
|
||||
from itertools import imap
|
||||
|
||||
import supybot.dbi as dbi
|
||||
import supybot.log as log
|
||||
import supybot.conf as conf
|
||||
import supybot.utils as utils
|
||||
import supybot.ircdb as ircdb
|
||||
@ -76,84 +79,50 @@ class Ignores(registry.SpaceSeparatedListOfStrings):
|
||||
|
||||
conf.registerUserValue(conf.users.plugins.Note, 'ignores', Ignores([], ''))
|
||||
|
||||
class FlatfileNoteDB(plugins.FlatfileDB):
|
||||
class Note(object):
|
||||
def __init__(self, L=None, to=None, frm=None, text=None):
|
||||
if L is not None:
|
||||
self.frm = int(L[0])
|
||||
self.to = int(L[1])
|
||||
self.at = float(L[2])
|
||||
self.notified = bool(int(L[3]))
|
||||
self.read = bool(int(L[4]))
|
||||
self.public = bool(int(L[5]))
|
||||
self.text = L[6]
|
||||
else:
|
||||
self.to = to
|
||||
self.frm = frm
|
||||
self.text = text
|
||||
self.read = False
|
||||
self.public = True
|
||||
self.at = time.time()
|
||||
self.notified = False
|
||||
|
||||
def __str__(self):
|
||||
return csv.join(map(str, [self.frm, self.to, self.at,
|
||||
int(self.notified), int(self.read),
|
||||
int(self.public), self.text]))
|
||||
|
||||
def serialize(self, n):
|
||||
return str(n)
|
||||
|
||||
def deserialize(self, s):
|
||||
return self.Note(csv.split(s))
|
||||
class DbiNoteDB(dbi.DB):
|
||||
Mapping = 'flat'
|
||||
class Record(object):
|
||||
__metaclass__ = dbi.Record
|
||||
__fields__ = [
|
||||
'frm',
|
||||
'to',
|
||||
'at',
|
||||
'notified',
|
||||
'read',
|
||||
'public',
|
||||
('text', str)
|
||||
]
|
||||
|
||||
def setRead(self, id):
|
||||
n = self.getRecord(id)
|
||||
n = self.get(id)
|
||||
n.read = True
|
||||
n.notified = True
|
||||
self.setRecord(id, n)
|
||||
self.set(id, n)
|
||||
|
||||
def setNotified(self, id):
|
||||
n = self.getRecord(id)
|
||||
n = self.get(id)
|
||||
n.notified = True
|
||||
self.setRecord(id, n)
|
||||
self.set(id, n)
|
||||
|
||||
def getUnnotifiedIds(self, to):
|
||||
L = []
|
||||
for (id, note) in self.records():
|
||||
if note.to == to and not note.notified:
|
||||
L.append(id)
|
||||
return L
|
||||
def p(note):
|
||||
return not note.notified and note.to == to
|
||||
return [note.id for note in self.select(p)]
|
||||
|
||||
def getUnreadIds(self, to):
|
||||
L = []
|
||||
for (id, note) in self.records():
|
||||
if note.to == to and not note.read:
|
||||
L.append(id)
|
||||
return L
|
||||
def p(note):
|
||||
return not note.read and note.to == to
|
||||
return [note.id for note in self.select(p)]
|
||||
|
||||
def send(self, frm, to, text):
|
||||
n = self.Note(frm=frm, to=to, text=text)
|
||||
return self.addRecord(n)
|
||||
|
||||
def get(self, id):
|
||||
return self.getRecord(id)
|
||||
|
||||
def remove(self, id):
|
||||
n = self.getRecord(id)
|
||||
assert not n.read
|
||||
self.delRecord(id)
|
||||
|
||||
def notes(self, p):
|
||||
L = []
|
||||
for (id, note) in self.records():
|
||||
if p(note):
|
||||
L.append((id, note))
|
||||
return L
|
||||
def send(self, frm, to, public, text):
|
||||
n = self.Record(frm=frm, to=to, text=text,
|
||||
at=time.time(), public=public)
|
||||
return self.add(n)
|
||||
|
||||
|
||||
def NoteDB():
|
||||
return FlatfileNoteDB(conf.supybot.directories.data.dirize('Note.db'))
|
||||
# XXX This should eventually be smarter.
|
||||
return DbiNoteDB(conf.supybot.directories.data.dirize('Note.db'))
|
||||
|
||||
|
||||
class Note(callbacks.Privmsg):
|
||||
@ -249,7 +218,7 @@ class Note(callbacks.Privmsg):
|
||||
return
|
||||
sent = []
|
||||
for toId in ids:
|
||||
id = self.db.send(fromId, toId, text)
|
||||
id = self.db.send(fromId, toId, public, text)
|
||||
name = ircdb.users.getUser(toId).name
|
||||
s = 'note #%s sent to %s' % (id, name)
|
||||
sent.append(s)
|
||||
@ -362,15 +331,15 @@ class Note(callbacks.Privmsg):
|
||||
except KeyError:
|
||||
irc.errorNoUser()
|
||||
|
||||
def _formatNoteId(self, msg, id, frm, public, sent=False):
|
||||
if public or not ircutils.isChannel(msg.args[0]):
|
||||
sender = ircdb.users.getUser(frm).name
|
||||
def _formatNoteId(self, msg, note, sent=False):
|
||||
if note.public or not ircutils.isChannel(msg.args[0]):
|
||||
sender = ircdb.users.getUser(note.frm).name
|
||||
if sent:
|
||||
return '#%s to %s' % (id, sender)
|
||||
return '#%s to %s' % (note.id, sender)
|
||||
else:
|
||||
return '#%s from %s' % (id, sender)
|
||||
return '#%s from %s' % (note.id, sender)
|
||||
else:
|
||||
return '#%s (private)' % id
|
||||
return '#%s (private)' % note.id
|
||||
|
||||
def list(self, irc, msg, args):
|
||||
"""[--{old,sent}] [--{from,to} <user>]
|
||||
@ -414,16 +383,14 @@ class Note(callbacks.Privmsg):
|
||||
except KeyError:
|
||||
irc.errorNoUser()
|
||||
return
|
||||
notesAndIds = self.db.notes(p)
|
||||
notesAndIds.sort()
|
||||
#notesAndIds.reverse() # Newer notes, higher ids.
|
||||
if not notesAndIds:
|
||||
notes = list(self.db.select(p))
|
||||
if not notes:
|
||||
irc.reply('You have no unread notes.')
|
||||
else:
|
||||
L = [self._formatNoteId(msg, id, n.frm, n.public)
|
||||
for (id, n) in notesAndIds]
|
||||
L = self._condense(L)
|
||||
irc.reply(utils.commaAndify(L))
|
||||
utils.sortBy(operator.attrgetter('id'), notes)
|
||||
ids = [self._formatNoteId(msg, note) for note in notes]
|
||||
ids = self._condense(ids)
|
||||
irc.reply(utils.commaAndify(ids))
|
||||
|
||||
def _condense(self, notes):
|
||||
temp = {}
|
||||
@ -455,14 +422,13 @@ class Note(callbacks.Privmsg):
|
||||
originalP = p
|
||||
def p(note):
|
||||
return originalP(note) and note.to == receiver
|
||||
notesAndIds = self.db.notes(p)
|
||||
notesAndIds.sort()
|
||||
notesAndIds.reverse()
|
||||
if not notesAndIds:
|
||||
notes = list(self.db.select(p))
|
||||
if not notes:
|
||||
irc.error('I couldn\'t find any sent notes for your user.')
|
||||
else:
|
||||
ids = [self._formatNoteId(msg, id, note.to, note.public,sent=True)
|
||||
for (id, note) in notesAndIds]
|
||||
utils.sortBy(operator.attrgetter('id'), notes)
|
||||
notes.reverse() # Most recently sent first.
|
||||
ids = [self._formatNoteId(msg, note, sent=True) for note in notes]
|
||||
ids = self._condense(ids)
|
||||
irc.reply(utils.commaAndify(ids))
|
||||
|
||||
@ -483,14 +449,13 @@ class Note(callbacks.Privmsg):
|
||||
originalP = p
|
||||
def p(note):
|
||||
return originalP(note) and note.frm == sender
|
||||
notesAndIds = self.db.notes(p)
|
||||
notesAndIds.sort()
|
||||
notesAndIds.reverse()
|
||||
if not notesAndIds:
|
||||
notes = list(self.db.select(p))
|
||||
if not notes:
|
||||
irc.reply('I couldn\'t find any matching read notes for your user.')
|
||||
else:
|
||||
ids = [self._formatNoteId(msg, id, note.frm, note.public)
|
||||
for (id, note) in notesAndIds]
|
||||
utils.sortBy(operator.attrgetter('id'), notes)
|
||||
notes.reverse()
|
||||
ids = [self._formatNoteId(msg, note) for note in notes]
|
||||
ids = self._condense(ids)
|
||||
irc.reply(utils.commaAndify(ids))
|
||||
|
||||
|
367
src/dbi.py
Normal file
367
src/dbi.py
Normal file
@ -0,0 +1,367 @@
|
||||
#!/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.
|
||||
###
|
||||
|
||||
"""
|
||||
Module for some slight database-independence for simple databases.
|
||||
"""
|
||||
|
||||
__revision__ = "$Id$"
|
||||
|
||||
import supybot.fix as fix
|
||||
|
||||
import csv
|
||||
import math
|
||||
import sets
|
||||
|
||||
import supybot.cdb as cdb
|
||||
import supybot.utils as utils
|
||||
|
||||
class Error(Exception):
|
||||
"""General error for this module."""
|
||||
|
||||
class MappingInterface(object):
|
||||
"""This is a class to represent the underlying representation of a map
|
||||
from integer keys to strings."""
|
||||
def __init__(self, filename, **kwargs):
|
||||
"""Feel free to ignore the filename."""
|
||||
raise NotImplementedError
|
||||
|
||||
def get(id):
|
||||
"""Gets the record matching id. Raises KeyError otherwise."""
|
||||
raise NotImplementedError
|
||||
|
||||
def set(id, s):
|
||||
"""Sets the record matching id to s."""
|
||||
raise NotImplementedError
|
||||
|
||||
def add(self, s):
|
||||
"""Adds a new record, returning a new id for it."""
|
||||
raise NotImplementedError
|
||||
|
||||
def remove(self, id):
|
||||
"Returns and removes the record with the given id from the database."
|
||||
raise NotImplementedError
|
||||
|
||||
def __iter__(self):
|
||||
"Return an iterator over (id, s) pairs. Not required to be ordered."
|
||||
raise NotImplementedError
|
||||
|
||||
def flush(self):
|
||||
"""Flushes current state to disk."""
|
||||
raise NotImplementedError
|
||||
|
||||
def close(self):
|
||||
"""Flushes current state to disk and invalidates the Mapping."""
|
||||
raise NotImplementedError
|
||||
|
||||
def vacuum(self):
|
||||
"Cleans up in the database, if possible. Not required to do anything."
|
||||
pass
|
||||
|
||||
class FlatfileMapping(MappingInterface):
|
||||
def __init__(self, filename, maxSize=10**6):
|
||||
self.filename = filename
|
||||
try:
|
||||
fd = file(self.filename)
|
||||
strId = fd.readline().rstrip()
|
||||
self.maxSize = len(strId)
|
||||
try:
|
||||
self.currentId = int(strId)
|
||||
except ValueError:
|
||||
raise Error, 'Invalid file for FlatfileMapping: %s' % filename
|
||||
except EnvironmentError, e:
|
||||
# File couldn't be opened.
|
||||
self.maxSize = int(math.log10(maxSize))
|
||||
self.currentId = 0
|
||||
self._incrementCurrentId()
|
||||
|
||||
def _canonicalId(self, id):
|
||||
if id is not None:
|
||||
return str(id).zfill(self.maxSize)
|
||||
else:
|
||||
return '-'*self.maxSize
|
||||
|
||||
def _incrementCurrentId(self, fd=None):
|
||||
fdWasNone = fd is None
|
||||
if fdWasNone:
|
||||
fd = file(self.filename, 'a')
|
||||
fd.seek(0)
|
||||
self.currentId += 1
|
||||
fd.write(self._canonicalId(self.currentId))
|
||||
fd.write('\n')
|
||||
if fdWasNone:
|
||||
fd.close()
|
||||
|
||||
def _splitLine(self, line):
|
||||
line = line.rstrip('\r\n')
|
||||
(id, s) = line.split(':', 1)
|
||||
return (id, s)
|
||||
|
||||
def _joinLine(self, id, s):
|
||||
return '%s:%s\n' % (self._canonicalId(id), s)
|
||||
|
||||
def add(self, s):
|
||||
line = self._joinLine(self.currentId, s)
|
||||
try:
|
||||
fd = file(self.filename, 'r+')
|
||||
fd.seek(0, 2) # End.
|
||||
fd.write(line)
|
||||
return self.currentId
|
||||
finally:
|
||||
self._incrementCurrentId(fd)
|
||||
fd.close()
|
||||
|
||||
def get(self, id):
|
||||
strId = self._canonicalId(id)
|
||||
try:
|
||||
fd = file(self.filename)
|
||||
fd.readline() # First line, nextId.
|
||||
for line in fd:
|
||||
(lineId, s) = self._splitLine(line)
|
||||
if lineId == strId:
|
||||
return s
|
||||
raise KeyError, id
|
||||
finally:
|
||||
fd.close()
|
||||
|
||||
def set(self, id, s):
|
||||
strLine = self._joinLine(id, s)
|
||||
try:
|
||||
fd = file(self.filename, 'r+')
|
||||
self.remove(id, fd)
|
||||
fd.seek(0, 2) # End.
|
||||
fd.write(strLine)
|
||||
finally:
|
||||
fd.close()
|
||||
|
||||
def remove(self, id, fd=None):
|
||||
fdWasNone = fd is None
|
||||
strId = self._canonicalId(id)
|
||||
try:
|
||||
if fdWasNone:
|
||||
fd = file(self.filename, 'r+')
|
||||
fd.seek(0)
|
||||
fd.readline() # First line, nextId
|
||||
pos = fd.tell()
|
||||
line = fd.readline()
|
||||
while line:
|
||||
(lineId, _) = self._splitLine(line)
|
||||
if lineId == strId:
|
||||
fd.seek(pos)
|
||||
fd.write(self._canonicalId(None))
|
||||
fd.seek(pos)
|
||||
fd.readline() # Same line we just rewrote the id for.
|
||||
pos = fd.tell()
|
||||
line = fd.readline()
|
||||
# We should be at the end.
|
||||
finally:
|
||||
if fdWasNone:
|
||||
fd.close()
|
||||
|
||||
def __iter__(self):
|
||||
fd = file(self.filename)
|
||||
fd.readline() # First line, nextId.
|
||||
for line in fd:
|
||||
(id, s) = self._splitLine(line)
|
||||
if not id.startswith('-'):
|
||||
yield (int(id), s)
|
||||
fd.close()
|
||||
|
||||
def vacuum(self):
|
||||
infd = file(self.filename)
|
||||
outfd = utils.transactionalFile(self.filename)
|
||||
outfd.write(infd.readline()) # First line, nextId.
|
||||
for line in infd:
|
||||
if not line.startswith('-'):
|
||||
outfd.write(line)
|
||||
infd.close()
|
||||
outfd.close()
|
||||
|
||||
def flush(self):
|
||||
pass # No-op, we maintain no open files.
|
||||
|
||||
def close(self):
|
||||
self.vacuum() # Should we do this? It should be fine.
|
||||
|
||||
|
||||
class CdbMapping(MappingInterface):
|
||||
def __init__(self, filename, **kwargs):
|
||||
self.filename = filename
|
||||
self.db = cdb.open(filename, 'c', **kwargs)
|
||||
if 'nextId' not in self.db:
|
||||
self.db['nextId'] = '1'
|
||||
|
||||
def _getNextId(self):
|
||||
i = int(self.db['nextId'])
|
||||
self.db['nextId'] = str(i+1)
|
||||
return i
|
||||
|
||||
def get(self, id):
|
||||
return self.db[str(id)]
|
||||
|
||||
def set(self, id, s):
|
||||
self.db[str(id)] = s
|
||||
|
||||
def add(self, s):
|
||||
id = self._getNextId()
|
||||
self.set(id, s)
|
||||
return id
|
||||
|
||||
def remove(self, id):
|
||||
del self.db[str(id)]
|
||||
|
||||
def __iter__(self):
|
||||
for (id, s) in self.db.iteritems():
|
||||
if id != 'nextId':
|
||||
yield (int(id), s)
|
||||
|
||||
def flush(self):
|
||||
self.db.flush()
|
||||
|
||||
def close(self):
|
||||
self.db.close()
|
||||
|
||||
|
||||
class DB(object):
|
||||
Mapping = None
|
||||
Record = None
|
||||
def __init__(self, filename, Mapping=None, Record=None):
|
||||
if Record is not None:
|
||||
self.Record = Record
|
||||
if Mapping is not None:
|
||||
self.Mapping = Mapping
|
||||
if isinstance(self.Mapping, basestring):
|
||||
self.Mapping = Mappings[self.Mapping]
|
||||
self.map = self.Mapping(filename)
|
||||
|
||||
def _newRecord(self, id, s):
|
||||
record = self.Record(id=id)
|
||||
record.deserialize(s)
|
||||
return record
|
||||
|
||||
def get(self, id):
|
||||
s = self.map.get(id)
|
||||
return self._newRecord(id, s)
|
||||
|
||||
def set(self, id, record):
|
||||
s = record.serialize()
|
||||
self.map.set(id, s)
|
||||
|
||||
def add(self, record):
|
||||
s = record.serialize()
|
||||
return self.map.add(s)
|
||||
|
||||
def remove(self, id):
|
||||
s = self.map.remove(id)
|
||||
return self._newRecord(id, s)
|
||||
|
||||
def __iter__(self):
|
||||
for (id, s) in self.map:
|
||||
# We don't need to yield the id because it's in the record.
|
||||
yield self._newRecord(id, s)
|
||||
|
||||
def select(self, p):
|
||||
for record in self:
|
||||
if p(record):
|
||||
yield record
|
||||
|
||||
def flush(self):
|
||||
self.map.flush()
|
||||
|
||||
def close(self):
|
||||
self.map.close()
|
||||
|
||||
Mappings = {
|
||||
'cdb': CdbMapping,
|
||||
'flat': FlatfileMapping,
|
||||
}
|
||||
|
||||
|
||||
class Record(type):
|
||||
"""__fields should be a list of two-tuples, (name, converter) or
|
||||
(name, (converter, default))."""
|
||||
def __new__(cls, clsname, bases, dict):
|
||||
defaults = {}
|
||||
converters = {}
|
||||
fields = []
|
||||
for name in dict['__fields__']:
|
||||
if isinstance(name, tuple):
|
||||
(name, spec) = name
|
||||
else:
|
||||
spec = utils.safeEval
|
||||
assert name != 'convert' and name != 'id'
|
||||
fields.append(name)
|
||||
if isinstance(spec, tuple):
|
||||
(converter, default) = spec
|
||||
else:
|
||||
converter = spec
|
||||
default = None
|
||||
defaults[name] = default
|
||||
converters[name] = converter
|
||||
del dict['__fields__']
|
||||
|
||||
def __init__(self, id=None, convert=False, **kwargs):
|
||||
if id is not None:
|
||||
assert isinstance(id, int), 'id must be an integer.'
|
||||
self.id = id
|
||||
set = sets.Set()
|
||||
for (name, value) in kwargs.iteritems():
|
||||
assert name in fields, 'name must be a record value.'
|
||||
set.add(name)
|
||||
if convert:
|
||||
setattr(self, name, converters[name](value))
|
||||
else:
|
||||
setattr(self, name, value)
|
||||
for name in fields:
|
||||
if name not in set:
|
||||
setattr(self, name, defaults[name])
|
||||
|
||||
def serialize(self):
|
||||
return csv.join([str(getattr(self, name)) for name in fields])
|
||||
|
||||
def deserialize(self, s):
|
||||
unseenRecords = sets.Set(fields)
|
||||
for (name, strValue) in zip(fields, csv.split(s)):
|
||||
setattr(self, name, converters[name](strValue))
|
||||
unseenRecords.remove(name)
|
||||
for name in unseenRecords:
|
||||
setattr(self, record, defaults[record])
|
||||
|
||||
dict['__init__'] = __init__
|
||||
dict['serialize'] = serialize
|
||||
dict['deserialize'] = deserialize
|
||||
return type.__new__(cls, clsname, bases, dict)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:
|
Loading…
Reference in New Issue
Block a user