aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/Twisted/py2/twisted/news/database.py
diff options
context:
space:
mode:
authorshmel1k <shmel1k@ydb.tech>2023-11-26 18:16:14 +0300
committershmel1k <shmel1k@ydb.tech>2023-11-26 18:43:30 +0300
commitb8cf9e88f4c5c64d9406af533d8948deb050d695 (patch)
tree218eb61fb3c3b96ec08b4d8cdfef383104a87d63 /contrib/python/Twisted/py2/twisted/news/database.py
parent523f645a83a0ec97a0332dbc3863bb354c92a328 (diff)
downloadydb-b8cf9e88f4c5c64d9406af533d8948deb050d695.tar.gz
add kikimr_configure
Diffstat (limited to 'contrib/python/Twisted/py2/twisted/news/database.py')
-rw-r--r--contrib/python/Twisted/py2/twisted/news/database.py1046
1 files changed, 1046 insertions, 0 deletions
diff --git a/contrib/python/Twisted/py2/twisted/news/database.py b/contrib/python/Twisted/py2/twisted/news/database.py
new file mode 100644
index 0000000000..5a25dd37b8
--- /dev/null
+++ b/contrib/python/Twisted/py2/twisted/news/database.py
@@ -0,0 +1,1046 @@
+# -*- test-case-name: twisted.news.test -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+News server backend implementations.
+"""
+
+import getpass, pickle, time, socket
+import os
+import StringIO
+from hashlib import md5
+from email.Message import Message
+from email.Generator import Generator
+from zope.interface import implementer, Interface
+
+from twisted.news.nntp import NNTPError
+from twisted.mail import smtp
+from twisted.internet import defer
+from twisted.enterprise import adbapi
+from twisted.persisted import dirdbm
+
+
+
+ERR_NOGROUP, ERR_NOARTICLE = range(2, 4) # XXX - put NNTP values here (I guess?)
+
+OVERVIEW_FMT = [
+ 'Subject', 'From', 'Date', 'Message-ID', 'References',
+ 'Bytes', 'Lines', 'Xref'
+]
+
+def hexdigest(md5): #XXX: argh. 1.5.2 doesn't have this.
+ return ''.join(map(lambda x: hex(ord(x))[2:], md5.digest()))
+
+class Article:
+ def __init__(self, head, body):
+ self.body = body
+ self.headers = {}
+ header = None
+ for line in head.split('\r\n'):
+ if line[0] in ' \t':
+ i = list(self.headers[header])
+ i[1] += '\r\n' + line
+ else:
+ i = line.split(': ', 1)
+ header = i[0].lower()
+ self.headers[header] = tuple(i)
+
+ if not self.getHeader('Message-ID'):
+ s = str(time.time()) + self.body
+ id = hexdigest(md5(s)) + '@' + socket.gethostname()
+ self.putHeader('Message-ID', '<%s>' % id)
+
+ if not self.getHeader('Bytes'):
+ self.putHeader('Bytes', str(len(self.body)))
+
+ if not self.getHeader('Lines'):
+ self.putHeader('Lines', str(self.body.count('\n')))
+
+ if not self.getHeader('Date'):
+ self.putHeader('Date', time.ctime(time.time()))
+
+
+ def getHeader(self, header):
+ h = header.lower()
+ if h in self.headers:
+ return self.headers[h][1]
+ else:
+ return ''
+
+
+ def putHeader(self, header, value):
+ self.headers[header.lower()] = (header, value)
+
+
+ def textHeaders(self):
+ headers = []
+ for i in self.headers.values():
+ headers.append('%s: %s' % i)
+ return '\r\n'.join(headers) + '\r\n'
+
+ def overview(self):
+ xover = []
+ for i in OVERVIEW_FMT:
+ xover.append(self.getHeader(i))
+ return xover
+
+
+class NewsServerError(Exception):
+ pass
+
+
+class INewsStorage(Interface):
+ """
+ An interface for storing and requesting news articles
+ """
+
+ def listRequest():
+ """
+ Returns a deferred whose callback will be passed a list of 4-tuples
+ containing (name, max index, min index, flags) for each news group
+ """
+
+
+ def subscriptionRequest():
+ """
+ Returns a deferred whose callback will be passed the list of
+ recommended subscription groups for new server users
+ """
+
+
+ def postRequest(message):
+ """
+ Returns a deferred whose callback will be invoked if 'message'
+ is successfully posted to one or more specified groups and
+ whose errback will be invoked otherwise.
+ """
+
+
+ def overviewRequest():
+ """
+ Returns a deferred whose callback will be passed the a list of
+ headers describing this server's overview format.
+ """
+
+
+ def xoverRequest(group, low, high):
+ """
+ Returns a deferred whose callback will be passed a list of xover
+ headers for the given group over the given range. If low is None,
+ the range starts at the first article. If high is None, the range
+ ends at the last article.
+ """
+
+
+ def xhdrRequest(group, low, high, header):
+ """
+ Returns a deferred whose callback will be passed a list of XHDR data
+ for the given group over the given range. If low is None,
+ the range starts at the first article. If high is None, the range
+ ends at the last article.
+ """
+
+
+ def listGroupRequest(group):
+ """
+ Returns a deferred whose callback will be passed a two-tuple of
+ (group name, [article indices])
+ """
+
+
+ def groupRequest(group):
+ """
+ Returns a deferred whose callback will be passed a five-tuple of
+ (group name, article count, highest index, lowest index, group flags)
+ """
+
+
+ def articleExistsRequest(id):
+ """
+ Returns a deferred whose callback will be passed with a true value
+ if a message with the specified Message-ID exists in the database
+ and with a false value otherwise.
+ """
+
+
+ def articleRequest(group, index, id = None):
+ """
+ Returns a deferred whose callback will be passed a file-like object
+ containing the full article text (headers and body) for the article
+ of the specified index in the specified group, and whose errback
+ will be invoked if the article or group does not exist. If id is
+ not None, index is ignored and the article with the given Message-ID
+ will be returned instead, along with its index in the specified
+ group.
+ """
+
+
+ def headRequest(group, index):
+ """
+ Returns a deferred whose callback will be passed the header for
+ the article of the specified index in the specified group, and
+ whose errback will be invoked if the article or group does not
+ exist.
+ """
+
+
+ def bodyRequest(group, index):
+ """
+ Returns a deferred whose callback will be passed the body for
+ the article of the specified index in the specified group, and
+ whose errback will be invoked if the article or group does not
+ exist.
+ """
+
+class NewsStorage:
+ """
+ Backwards compatibility class -- There is no reason to inherit from this,
+ just implement INewsStorage instead.
+ """
+ def listRequest(self):
+ raise NotImplementedError()
+ def subscriptionRequest(self):
+ raise NotImplementedError()
+ def postRequest(self, message):
+ raise NotImplementedError()
+ def overviewRequest(self):
+ return defer.succeed(OVERVIEW_FMT)
+ def xoverRequest(self, group, low, high):
+ raise NotImplementedError()
+ def xhdrRequest(self, group, low, high, header):
+ raise NotImplementedError()
+ def listGroupRequest(self, group):
+ raise NotImplementedError()
+ def groupRequest(self, group):
+ raise NotImplementedError()
+ def articleExistsRequest(self, id):
+ raise NotImplementedError()
+ def articleRequest(self, group, index, id = None):
+ raise NotImplementedError()
+ def headRequest(self, group, index):
+ raise NotImplementedError()
+ def bodyRequest(self, group, index):
+ raise NotImplementedError()
+
+
+
+class _ModerationMixin:
+ """
+ Storage implementations can inherit from this class to get the easy-to-use
+ C{notifyModerators} method which will take care of sending messages which
+ require moderation to a list of moderators.
+ """
+ sendmail = staticmethod(smtp.sendmail)
+
+ def notifyModerators(self, moderators, article):
+ """
+ Send an article to a list of group moderators to be moderated.
+
+ @param moderators: A C{list} of C{str} giving RFC 2821 addresses of
+ group moderators to notify.
+
+ @param article: The article requiring moderation.
+ @type article: L{Article}
+
+ @return: A L{Deferred} which fires with the result of sending the email.
+ """
+ # Moderated postings go through as long as they have an Approved
+ # header, regardless of what the value is
+ group = article.getHeader('Newsgroups')
+ subject = article.getHeader('Subject')
+
+ if self._sender is None:
+ # This case should really go away. This isn't a good default.
+ sender = 'twisted-news@' + socket.gethostname()
+ else:
+ sender = self._sender
+
+ msg = Message()
+ msg['Message-ID'] = smtp.messageid()
+ msg['From'] = sender
+ msg['To'] = ', '.join(moderators)
+ msg['Subject'] = 'Moderate new %s message: %s' % (group, subject)
+ msg['Content-Type'] = 'message/rfc822'
+
+ payload = Message()
+ for header, value in article.headers.values():
+ payload.add_header(header, value)
+ payload.set_payload(article.body)
+
+ msg.attach(payload)
+
+ out = StringIO.StringIO()
+ gen = Generator(out, False)
+ gen.flatten(msg)
+ msg = out.getvalue()
+
+ return self.sendmail(self._mailhost, sender, moderators, msg)
+
+
+
+@implementer(INewsStorage)
+class PickleStorage(_ModerationMixin):
+ """
+ A trivial NewsStorage implementation using pickles
+
+ Contains numerous flaws and is generally unsuitable for any
+ real applications. Consider yourself warned!
+ """
+ sharedDBs = {}
+
+ def __init__(self, filename, groups=None, moderators=(),
+ mailhost=None, sender=None):
+ """
+ @param mailhost: A C{str} giving the mail exchange host which will
+ accept moderation emails from this server. Must accept emails
+ destined for any address specified as a moderator.
+
+ @param sender: A C{str} giving the address which will be used as the
+ sender of any moderation email generated by this server.
+ """
+ self.datafile = filename
+ self.load(filename, groups, moderators)
+ self._mailhost = mailhost
+ self._sender = sender
+
+
+ def getModerators(self, groups):
+ # first see if any groups are moderated. if so, nothing gets posted,
+ # but the whole messages gets forwarded to the moderator address
+ moderators = []
+ for group in groups:
+ moderators.extend(self.db['moderators'].get(group, None))
+ return filter(None, moderators)
+
+
+ def listRequest(self):
+ "Returns a list of 4-tuples: (name, max index, min index, flags)"
+ l = self.db['groups']
+ r = []
+ for i in l:
+ if len(self.db[i].keys()):
+ low = min(self.db[i].keys())
+ high = max(self.db[i].keys()) + 1
+ else:
+ low = high = 0
+ if i in self.db['moderators']:
+ flags = 'm'
+ else:
+ flags = 'y'
+ r.append((i, high, low, flags))
+ return defer.succeed(r)
+
+ def subscriptionRequest(self):
+ return defer.succeed(['alt.test'])
+
+ def postRequest(self, message):
+ cleave = message.find('\r\n\r\n')
+ headers, article = message[:cleave], message[cleave + 4:]
+
+ a = Article(headers, article)
+ groups = a.getHeader('Newsgroups').split()
+ xref = []
+
+ # Check moderated status
+ moderators = self.getModerators(groups)
+ if moderators and not a.getHeader('Approved'):
+ return self.notifyModerators(moderators, a)
+
+ for group in groups:
+ if group in self.db:
+ if len(self.db[group].keys()):
+ index = max(self.db[group].keys()) + 1
+ else:
+ index = 1
+ xref.append((group, str(index)))
+ self.db[group][index] = a
+
+ if len(xref) == 0:
+ return defer.fail(None)
+
+ a.putHeader('Xref', '%s %s' % (
+ socket.gethostname().split()[0],
+ ''.join(map(lambda x: ':'.join(x), xref))
+ ))
+
+ self.flush()
+ return defer.succeed(None)
+
+
+ def overviewRequest(self):
+ return defer.succeed(OVERVIEW_FMT)
+
+
+ def xoverRequest(self, group, low, high):
+ if group not in self.db:
+ return defer.succeed([])
+ r = []
+ for i in self.db[group].keys():
+ if (low is None or i >= low) and (high is None or i <= high):
+ r.append([str(i)] + self.db[group][i].overview())
+ return defer.succeed(r)
+
+
+ def xhdrRequest(self, group, low, high, header):
+ if group not in self.db:
+ return defer.succeed([])
+ r = []
+ for i in self.db[group].keys():
+ if low is None or i >= low and high is None or i <= high:
+ r.append((i, self.db[group][i].getHeader(header)))
+ return defer.succeed(r)
+
+
+ def listGroupRequest(self, group):
+ if group in self.db:
+ return defer.succeed((group, self.db[group].keys()))
+ else:
+ return defer.fail(None)
+
+ def groupRequest(self, group):
+ if group in self.db:
+ if len(self.db[group].keys()):
+ num = len(self.db[group].keys())
+ low = min(self.db[group].keys())
+ high = max(self.db[group].keys())
+ else:
+ num = low = high = 0
+ flags = 'y'
+ return defer.succeed((group, num, high, low, flags))
+ else:
+ return defer.fail(ERR_NOGROUP)
+
+
+ def articleExistsRequest(self, id):
+ for group in self.db['groups']:
+ for a in self.db[group].values():
+ if a.getHeader('Message-ID') == id:
+ return defer.succeed(1)
+ return defer.succeed(0)
+
+
+ def articleRequest(self, group, index, id = None):
+ if id is not None:
+ raise NotImplementedError
+
+ if group in self.db:
+ if index in self.db[group]:
+ a = self.db[group][index]
+ return defer.succeed((
+ index,
+ a.getHeader('Message-ID'),
+ StringIO.StringIO(a.textHeaders() + '\r\n' + a.body)
+ ))
+ else:
+ return defer.fail(ERR_NOARTICLE)
+ else:
+ return defer.fail(ERR_NOGROUP)
+
+
+ def headRequest(self, group, index):
+ if group in self.db:
+ if index in self.db[group]:
+ a = self.db[group][index]
+ return defer.succeed((index, a.getHeader('Message-ID'), a.textHeaders()))
+ else:
+ return defer.fail(ERR_NOARTICLE)
+ else:
+ return defer.fail(ERR_NOGROUP)
+
+
+ def bodyRequest(self, group, index):
+ if group in self.db:
+ if index in self.db[group]:
+ a = self.db[group][index]
+ return defer.succeed((index, a.getHeader('Message-ID'), StringIO.StringIO(a.body)))
+ else:
+ return defer.fail(ERR_NOARTICLE)
+ else:
+ return defer.fail(ERR_NOGROUP)
+
+
+ def flush(self):
+ with open(self.datafile, 'w') as f:
+ pickle.dump(self.db, f)
+
+
+ def load(self, filename, groups = None, moderators = ()):
+ if filename in PickleStorage.sharedDBs:
+ self.db = PickleStorage.sharedDBs[filename]
+ else:
+ try:
+ with open(filename) as f:
+ self.db = pickle.load(f)
+ PickleStorage.sharedDBs[filename] = self.db
+ except IOError:
+ self.db = PickleStorage.sharedDBs[filename] = {}
+ self.db['groups'] = groups
+ if groups is not None:
+ for i in groups:
+ self.db[i] = {}
+ self.db['moderators'] = dict(moderators)
+ self.flush()
+
+
+class Group:
+ name = None
+ flags = ''
+ minArticle = 1
+ maxArticle = 0
+ articles = None
+
+ def __init__(self, name, flags = 'y'):
+ self.name = name
+ self.flags = flags
+ self.articles = {}
+
+
+@implementer(INewsStorage)
+class NewsShelf(_ModerationMixin):
+ """
+ A NewStorage implementation using Twisted's dirdbm persistence module.
+ """
+ def __init__(self, mailhost, path, sender=None):
+ """
+ @param mailhost: A C{str} giving the mail exchange host which will
+ accept moderation emails from this server. Must accept emails
+ destined for any address specified as a moderator.
+
+ @param sender: A C{str} giving the address which will be used as the
+ sender of any moderation email generated by this server.
+ """
+ self.path = path
+ self._mailhost = self.mailhost = mailhost
+ self._sender = sender
+
+ if not os.path.exists(path):
+ os.mkdir(path)
+
+ self.dbm = dirdbm.Shelf(os.path.join(path, "newsshelf"))
+ if not len(self.dbm.keys()):
+ self.initialize()
+
+
+ def initialize(self):
+ # A dictionary of group name/Group instance items
+ self.dbm['groups'] = dirdbm.Shelf(os.path.join(self.path, 'groups'))
+
+ # A dictionary of group name/email address
+ self.dbm['moderators'] = dirdbm.Shelf(os.path.join(self.path, 'moderators'))
+
+ # A list of group names
+ self.dbm['subscriptions'] = []
+
+ # A dictionary of MessageID strings/xref lists
+ self.dbm['Message-IDs'] = dirdbm.Shelf(os.path.join(self.path, 'Message-IDs'))
+
+
+ def addGroup(self, name, flags):
+ self.dbm['groups'][name] = Group(name, flags)
+
+
+ def addSubscription(self, name):
+ self.dbm['subscriptions'] = self.dbm['subscriptions'] + [name]
+
+
+ def addModerator(self, group, email):
+ self.dbm['moderators'][group] = email
+
+
+ def listRequest(self):
+ result = []
+ for g in self.dbm['groups'].values():
+ result.append((g.name, g.maxArticle, g.minArticle, g.flags))
+ return defer.succeed(result)
+
+
+ def subscriptionRequest(self):
+ return defer.succeed(self.dbm['subscriptions'])
+
+
+ def getModerator(self, groups):
+ # first see if any groups are moderated. if so, nothing gets posted,
+ # but the whole messages gets forwarded to the moderator address
+ for group in groups:
+ try:
+ return self.dbm['moderators'][group]
+ except KeyError:
+ pass
+ return None
+
+
+ def notifyModerator(self, moderator, article):
+ """
+ Notify a single moderator about an article requiring moderation.
+
+ C{notifyModerators} should be preferred.
+ """
+ return self.notifyModerators([moderator], article)
+
+
+ def postRequest(self, message):
+ cleave = message.find('\r\n\r\n')
+ headers, article = message[:cleave], message[cleave + 4:]
+
+ article = Article(headers, article)
+ groups = article.getHeader('Newsgroups').split()
+ xref = []
+
+ # Check for moderated status
+ moderator = self.getModerator(groups)
+ if moderator and not article.getHeader('Approved'):
+ return self.notifyModerators([moderator], article)
+
+
+ for group in groups:
+ try:
+ g = self.dbm['groups'][group]
+ except KeyError:
+ pass
+ else:
+ index = g.maxArticle + 1
+ g.maxArticle += 1
+ g.articles[index] = article
+ xref.append((group, str(index)))
+ self.dbm['groups'][group] = g
+
+ if not xref:
+ return defer.fail(NewsServerError("No groups carried: " + ' '.join(groups)))
+
+ article.putHeader('Xref', '%s %s' % (socket.gethostname().split()[0], ' '.join(map(lambda x: ':'.join(x), xref))))
+ self.dbm['Message-IDs'][article.getHeader('Message-ID')] = xref
+ return defer.succeed(None)
+
+
+ def overviewRequest(self):
+ return defer.succeed(OVERVIEW_FMT)
+
+
+ def xoverRequest(self, group, low, high):
+ if group not in self.dbm['groups']:
+ return defer.succeed([])
+
+ if low is None:
+ low = 0
+ if high is None:
+ high = self.dbm['groups'][group].maxArticle
+ r = []
+ for i in range(low, high + 1):
+ if i in self.dbm['groups'][group].articles:
+ r.append([str(i)] + self.dbm['groups'][group].articles[i].overview())
+ return defer.succeed(r)
+
+
+ def xhdrRequest(self, group, low, high, header):
+ if group not in self.dbm['groups']:
+ return defer.succeed([])
+
+ if low is None:
+ low = 0
+ if high is None:
+ high = self.dbm['groups'][group].maxArticle
+ r = []
+ for i in range(low, high + 1):
+ if i in self.dbm['groups'][group].articles:
+ r.append((i, self.dbm['groups'][group].articles[i].getHeader(header)))
+ return defer.succeed(r)
+
+
+ def listGroupRequest(self, group):
+ if group in self.dbm['groups']:
+ return defer.succeed((group, self.dbm['groups'][group].articles.keys()))
+ return defer.fail(NewsServerError("No such group: " + group))
+
+
+ def groupRequest(self, group):
+ try:
+ g = self.dbm['groups'][group]
+ except KeyError:
+ return defer.fail(NewsServerError("No such group: " + group))
+ else:
+ flags = g.flags
+ low = g.minArticle
+ high = g.maxArticle
+ num = high - low + 1
+ return defer.succeed((group, num, high, low, flags))
+
+
+ def articleExistsRequest(self, id):
+ return defer.succeed(id in self.dbm['Message-IDs'])
+
+
+ def articleRequest(self, group, index, id = None):
+ if id is not None:
+ try:
+ xref = self.dbm['Message-IDs'][id]
+ except KeyError:
+ return defer.fail(NewsServerError("No such article: " + id))
+ else:
+ group, index = xref[0]
+ index = int(index)
+
+ try:
+ a = self.dbm['groups'][group].articles[index]
+ except KeyError:
+ return defer.fail(NewsServerError("No such group: " + group))
+ else:
+ return defer.succeed((
+ index,
+ a.getHeader('Message-ID'),
+ StringIO.StringIO(a.textHeaders() + '\r\n' + a.body)
+ ))
+
+
+ def headRequest(self, group, index, id = None):
+ if id is not None:
+ try:
+ xref = self.dbm['Message-IDs'][id]
+ except KeyError:
+ return defer.fail(NewsServerError("No such article: " + id))
+ else:
+ group, index = xref[0]
+ index = int(index)
+
+ try:
+ a = self.dbm['groups'][group].articles[index]
+ except KeyError:
+ return defer.fail(NewsServerError("No such group: " + group))
+ else:
+ return defer.succeed((index, a.getHeader('Message-ID'), a.textHeaders()))
+
+
+ def bodyRequest(self, group, index, id = None):
+ if id is not None:
+ try:
+ xref = self.dbm['Message-IDs'][id]
+ except KeyError:
+ return defer.fail(NewsServerError("No such article: " + id))
+ else:
+ group, index = xref[0]
+ index = int(index)
+
+ try:
+ a = self.dbm['groups'][group].articles[index]
+ except KeyError:
+ return defer.fail(NewsServerError("No such group: " + group))
+ else:
+ return defer.succeed((index, a.getHeader('Message-ID'), StringIO.StringIO(a.body)))
+
+
+@implementer(INewsStorage)
+class NewsStorageAugmentation:
+ """
+ A NewsStorage implementation using Twisted's asynchronous DB-API
+ """
+ schema = """
+
+ CREATE TABLE groups (
+ group_id SERIAL,
+ name VARCHAR(80) NOT NULL,
+
+ flags INTEGER DEFAULT 0 NOT NULL
+ );
+
+ CREATE UNIQUE INDEX group_id_index ON groups (group_id);
+ CREATE UNIQUE INDEX name_id_index ON groups (name);
+
+ CREATE TABLE articles (
+ article_id SERIAL,
+ message_id TEXT,
+
+ header TEXT,
+ body TEXT
+ );
+
+ CREATE UNIQUE INDEX article_id_index ON articles (article_id);
+ CREATE UNIQUE INDEX article_message_index ON articles (message_id);
+
+ CREATE TABLE postings (
+ group_id INTEGER,
+ article_id INTEGER,
+ article_index INTEGER NOT NULL
+ );
+
+ CREATE UNIQUE INDEX posting_article_index ON postings (article_id);
+
+ CREATE TABLE subscriptions (
+ group_id INTEGER
+ );
+
+ CREATE TABLE overview (
+ header TEXT
+ );
+ """
+
+ def __init__(self, info):
+ self.info = info
+ self.dbpool = adbapi.ConnectionPool(**self.info)
+
+
+ def __setstate__(self, state):
+ self.__dict__ = state
+ self.info['password'] = getpass.getpass('Database password for %s: ' % (self.info['user'],))
+ self.dbpool = adbapi.ConnectionPool(**self.info)
+ del self.info['password']
+
+
+ def listRequest(self):
+ # COALESCE may not be totally portable
+ # it is shorthand for
+ # CASE WHEN (first parameter) IS NOT NULL then (first parameter) ELSE (second parameter) END
+ sql = """
+ SELECT groups.name,
+ COALESCE(MAX(postings.article_index), 0),
+ COALESCE(MIN(postings.article_index), 0),
+ groups.flags
+ FROM groups LEFT OUTER JOIN postings
+ ON postings.group_id = groups.group_id
+ GROUP BY groups.name, groups.flags
+ ORDER BY groups.name
+ """
+ return self.dbpool.runQuery(sql)
+
+
+ def subscriptionRequest(self):
+ sql = """
+ SELECT groups.name FROM groups,subscriptions WHERE groups.group_id = subscriptions.group_id
+ """
+ return self.dbpool.runQuery(sql)
+
+
+ def postRequest(self, message):
+ cleave = message.find('\r\n\r\n')
+ headers, article = message[:cleave], message[cleave + 4:]
+ article = Article(headers, article)
+ return self.dbpool.runInteraction(self._doPost, article)
+
+
+ def _doPost(self, transaction, article):
+ # Get the group ids
+ groups = article.getHeader('Newsgroups').split()
+ if not len(groups):
+ raise NNTPError('Missing Newsgroups header')
+
+ sql = """
+ SELECT name, group_id FROM groups
+ WHERE name IN (%s)
+ """ % (', '.join([("'%s'" % (adbapi.safe(group),)) for group in groups]),)
+
+ transaction.execute(sql)
+ result = transaction.fetchall()
+
+ # No relevant groups, bye bye!
+ if not len(result):
+ raise NNTPError('None of groups in Newsgroup header carried')
+
+ # Got some groups, now find the indices this article will have in each
+ sql = """
+ SELECT groups.group_id, COALESCE(MAX(postings.article_index), 0) + 1
+ FROM groups LEFT OUTER JOIN postings
+ ON postings.group_id = groups.group_id
+ WHERE groups.group_id IN (%s)
+ GROUP BY groups.group_id
+ """ % (', '.join([("%d" % (id,)) for (group, id) in result]),)
+
+ transaction.execute(sql)
+ indices = transaction.fetchall()
+
+ if not len(indices):
+ raise NNTPError('Internal server error - no indices found')
+
+ # Associate indices with group names
+ gidToName = dict([(b, a) for (a, b) in result])
+ gidToIndex = dict(indices)
+
+ nameIndex = []
+ for i in gidToName:
+ nameIndex.append((gidToName[i], gidToIndex[i]))
+
+ # Build xrefs
+ xrefs = socket.gethostname().split()[0]
+ xrefs = xrefs + ' ' + ' '.join([('%s:%d' % (group, id)) for (group, id) in nameIndex])
+ article.putHeader('Xref', xrefs)
+
+ # Hey! The article is ready to be posted! God damn f'in finally.
+ sql = """
+ INSERT INTO articles (message_id, header, body)
+ VALUES ('%s', '%s', '%s')
+ """ % (
+ adbapi.safe(article.getHeader('Message-ID')),
+ adbapi.safe(article.textHeaders()),
+ adbapi.safe(article.body)
+ )
+
+ transaction.execute(sql)
+
+ # Now update the posting to reflect the groups to which this belongs
+ for gid in gidToName:
+ sql = """
+ INSERT INTO postings (group_id, article_id, article_index)
+ VALUES (%d, (SELECT last_value FROM articles_article_id_seq), %d)
+ """ % (gid, gidToIndex[gid])
+ transaction.execute(sql)
+
+ return len(nameIndex)
+
+
+ def overviewRequest(self):
+ sql = """
+ SELECT header FROM overview
+ """
+ return self.dbpool.runQuery(sql).addCallback(lambda result: [header[0] for header in result])
+
+
+ def xoverRequest(self, group, low, high):
+ sql = """
+ SELECT postings.article_index, articles.header
+ FROM articles,postings,groups
+ WHERE postings.group_id = groups.group_id
+ AND groups.name = '%s'
+ AND postings.article_id = articles.article_id
+ %s
+ %s
+ """ % (
+ adbapi.safe(group),
+ low is not None and "AND postings.article_index >= %d" % (low,) or "",
+ high is not None and "AND postings.article_index <= %d" % (high,) or ""
+ )
+
+ return self.dbpool.runQuery(sql).addCallback(
+ lambda results: [
+ [id] + Article(header, None).overview() for (id, header) in results
+ ]
+ )
+
+
+ def xhdrRequest(self, group, low, high, header):
+ sql = """
+ SELECT articles.header
+ FROM groups,postings,articles
+ WHERE groups.name = '%s' AND postings.group_id = groups.group_id
+ AND postings.article_index >= %d
+ AND postings.article_index <= %d
+ """ % (adbapi.safe(group), low, high)
+
+ return self.dbpool.runQuery(sql).addCallback(
+ lambda results: [
+ (i, Article(h, None).getHeader(h)) for (i, h) in results
+ ]
+ )
+
+
+ def listGroupRequest(self, group):
+ sql = """
+ SELECT postings.article_index FROM postings,groups
+ WHERE postings.group_id = groups.group_id
+ AND groups.name = '%s'
+ """ % (adbapi.safe(group),)
+
+ return self.dbpool.runQuery(sql).addCallback(
+ lambda results, group = group: (group, [res[0] for res in results])
+ )
+
+
+ def groupRequest(self, group):
+ sql = """
+ SELECT groups.name,
+ COUNT(postings.article_index),
+ COALESCE(MAX(postings.article_index), 0),
+ COALESCE(MIN(postings.article_index), 0),
+ groups.flags
+ FROM groups LEFT OUTER JOIN postings
+ ON postings.group_id = groups.group_id
+ WHERE groups.name = '%s'
+ GROUP BY groups.name, groups.flags
+ """ % (adbapi.safe(group),)
+
+ return self.dbpool.runQuery(sql).addCallback(
+ lambda results: tuple(results[0])
+ )
+
+
+ def articleExistsRequest(self, id):
+ sql = """
+ SELECT COUNT(message_id) FROM articles
+ WHERE message_id = '%s'
+ """ % (adbapi.safe(id),)
+
+ return self.dbpool.runQuery(sql).addCallback(
+ lambda result: bool(result[0][0])
+ )
+
+
+ def articleRequest(self, group, index, id = None):
+ if id is not None:
+ sql = """
+ SELECT postings.article_index, articles.message_id, articles.header, articles.body
+ FROM groups,postings LEFT OUTER JOIN articles
+ ON articles.message_id = '%s'
+ WHERE groups.name = '%s'
+ AND groups.group_id = postings.group_id
+ """ % (adbapi.safe(id), adbapi.safe(group))
+ else:
+ sql = """
+ SELECT postings.article_index, articles.message_id, articles.header, articles.body
+ FROM groups,articles LEFT OUTER JOIN postings
+ ON postings.article_id = articles.article_id
+ WHERE postings.article_index = %d
+ AND postings.group_id = groups.group_id
+ AND groups.name = '%s'
+ """ % (index, adbapi.safe(group))
+
+ return self.dbpool.runQuery(sql).addCallback(
+ lambda result: (
+ result[0][0],
+ result[0][1],
+ StringIO.StringIO(result[0][2] + '\r\n' + result[0][3])
+ )
+ )
+
+
+ def headRequest(self, group, index):
+ sql = """
+ SELECT postings.article_index, articles.message_id, articles.header
+ FROM groups,articles LEFT OUTER JOIN postings
+ ON postings.article_id = articles.article_id
+ WHERE postings.article_index = %d
+ AND postings.group_id = groups.group_id
+ AND groups.name = '%s'
+ """ % (index, adbapi.safe(group))
+
+ return self.dbpool.runQuery(sql).addCallback(lambda result: result[0])
+
+
+ def bodyRequest(self, group, index):
+ sql = """
+ SELECT postings.article_index, articles.message_id, articles.body
+ FROM groups,articles LEFT OUTER JOIN postings
+ ON postings.article_id = articles.article_id
+ WHERE postings.article_index = %d
+ AND postings.group_id = groups.group_id
+ AND groups.name = '%s'
+ """ % (index, adbapi.safe(group))
+
+ return self.dbpool.runQuery(sql).addCallback(
+ lambda result: result[0]
+ ).addCallback(
+ # result is a tuple of (index, id, body)
+ lambda result: (result[0], result[1], StringIO.StringIO(result[2]))
+ )
+
+####
+#### XXX - make these static methods some day
+####
+def makeGroupSQL(groups):
+ res = ''
+ for g in groups:
+ res = res + """\n INSERT INTO groups (name) VALUES ('%s');\n""" % (adbapi.safe(g),)
+ return res
+
+
+def makeOverviewSQL():
+ res = ''
+ for o in OVERVIEW_FMT:
+ res = res + """\n INSERT INTO overview (header) VALUES ('%s');\n""" % (adbapi.safe(o),)
+ return res