1 # -*- test-case-name: twisted.news.test.test_nntp -*-
2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
9 The following protocol commands are currently understood::
11 LIST LISTGROUP XOVER XHDR
12 POST GROUP ARTICLE STAT HEAD
13 BODY NEXT MODE STREAM MODE READER SLAVE
14 LAST QUIT HELP IHAVE XPATH
15 XINDEX XROVER TAKETHIS CHECK
17 The following protocol commands require implementation::
21 XTHREAD AUTHINFO NEWGROUPS
24 Other desired features:
27 - More robust client input handling
33 from twisted.protocols import basic
34 from twisted.python import log
37 articles = text.split('-')
38 if len(articles) == 1:
44 elif len(articles) == 2:
59 def extractCode(line):
60 line = line.split(' ', 1)
64 return int(line[0]), line[1]
69 class NNTPError(Exception):
70 def __init__(self, string):
74 return 'NNTPError: %s' % self.string
77 class NNTPClient(basic.LineReceiver):
78 MAX_COMMAND_LENGTH = 510
81 self.currentGroup = None
85 self._inputBuffers = []
86 self._responseCodes = []
87 self._responseHandlers = []
91 self._newState(self._statePassive, None, self._headerInitial)
94 def gotAllGroups(self, groups):
95 "Override for notification when fetchGroups() action is completed"
98 def getAllGroupsFailed(self, error):
99 "Override for notification when fetchGroups() action fails"
102 def gotOverview(self, overview):
103 "Override for notification when fetchOverview() action is completed"
106 def getOverviewFailed(self, error):
107 "Override for notification when fetchOverview() action fails"
110 def gotSubscriptions(self, subscriptions):
111 "Override for notification when fetchSubscriptions() action is completed"
114 def getSubscriptionsFailed(self, error):
115 "Override for notification when fetchSubscriptions() action fails"
118 def gotGroup(self, group):
119 "Override for notification when fetchGroup() action is completed"
122 def getGroupFailed(self, error):
123 "Override for notification when fetchGroup() action fails"
126 def gotArticle(self, article):
127 "Override for notification when fetchArticle() action is completed"
130 def getArticleFailed(self, error):
131 "Override for notification when fetchArticle() action fails"
134 def gotHead(self, head):
135 "Override for notification when fetchHead() action is completed"
138 def getHeadFailed(self, error):
139 "Override for notification when fetchHead() action fails"
142 def gotBody(self, info):
143 "Override for notification when fetchBody() action is completed"
146 def getBodyFailed(self, body):
147 "Override for notification when fetchBody() action fails"
151 "Override for notification when postArticle() action is successful"
154 def postFailed(self, error):
155 "Override for notification when postArticle() action fails"
158 def gotXHeader(self, headers):
159 "Override for notification when getXHeader() action is successful"
162 def getXHeaderFailed(self, error):
163 "Override for notification when getXHeader() action fails"
166 def gotNewNews(self, news):
167 "Override for notification when getNewNews() action is successful"
170 def getNewNewsFailed(self, error):
171 "Override for notification when getNewNews() action fails"
174 def gotNewGroups(self, groups):
175 "Override for notification when getNewGroups() action is successful"
178 def getNewGroupsFailed(self, error):
179 "Override for notification when getNewGroups() action fails"
182 def setStreamSuccess(self):
183 "Override for notification when setStream() action is successful"
186 def setStreamFailed(self, error):
187 "Override for notification when setStream() action fails"
190 def fetchGroups(self):
192 Request a list of all news groups from the server. gotAllGroups()
193 is called on success, getGroupsFailed() on failure
195 self.sendLine('LIST')
196 self._newState(self._stateList, self.getAllGroupsFailed)
199 def fetchOverview(self):
201 Request the overview format from the server. gotOverview() is called
202 on success, getOverviewFailed() on failure
204 self.sendLine('LIST OVERVIEW.FMT')
205 self._newState(self._stateOverview, self.getOverviewFailed)
208 def fetchSubscriptions(self):
210 Request a list of the groups it is recommended a new user subscribe to.
211 gotSubscriptions() is called on success, getSubscriptionsFailed() on
214 self.sendLine('LIST SUBSCRIPTIONS')
215 self._newState(self._stateSubscriptions, self.getSubscriptionsFailed)
218 def fetchGroup(self, group):
220 Get group information for the specified group from the server. gotGroup()
221 is called on success, getGroupFailed() on failure.
223 self.sendLine('GROUP %s' % (group,))
224 self._newState(None, self.getGroupFailed, self._headerGroup)
227 def fetchHead(self, index = ''):
229 Get the header for the specified article (or the currently selected
230 article if index is '') from the server. gotHead() is called on
231 success, getHeadFailed() on failure
233 self.sendLine('HEAD %s' % (index,))
234 self._newState(self._stateHead, self.getHeadFailed)
237 def fetchBody(self, index = ''):
239 Get the body for the specified article (or the currently selected
240 article if index is '') from the server. gotBody() is called on
241 success, getBodyFailed() on failure
243 self.sendLine('BODY %s' % (index,))
244 self._newState(self._stateBody, self.getBodyFailed)
247 def fetchArticle(self, index = ''):
249 Get the complete article with the specified index (or the currently
250 selected article if index is '') or Message-ID from the server.
251 gotArticle() is called on success, getArticleFailed() on failure.
253 self.sendLine('ARTICLE %s' % (index,))
254 self._newState(self._stateArticle, self.getArticleFailed)
257 def postArticle(self, text):
259 Attempt to post an article with the specified text to the server. 'text'
260 must consist of both head and body data, as specified by RFC 850. If the
261 article is posted successfully, postedOk() is called, otherwise postFailed()
264 self.sendLine('POST')
265 self._newState(None, self.postFailed, self._headerPost)
266 self._postText.append(text)
269 def fetchNewNews(self, groups, date, distributions = ''):
271 Get the Message-IDs for all new news posted to any of the given
272 groups since the specified date - in seconds since the epoch, GMT -
273 optionally restricted to the given distributions. gotNewNews() is
274 called on success, getNewNewsFailed() on failure.
276 One invocation of this function may result in multiple invocations
277 of gotNewNews()/getNewNewsFailed().
279 date, timeStr = time.strftime('%y%m%d %H%M%S', time.gmtime(date)).split()
280 line = 'NEWNEWS %%s %s %s %s' % (date, timeStr, distributions)
282 while len(groups) and len(line) + len(groupPart) + len(groups[-1]) + 1 < NNTPClient.MAX_COMMAND_LENGTH:
284 groupPart = groupPart + ',' + group
286 self.sendLine(line % (groupPart,))
287 self._newState(self._stateNewNews, self.getNewNewsFailed)
290 self.fetchNewNews(groups, date, distributions)
293 def fetchNewGroups(self, date, distributions):
295 Get the names of all new groups created/added to the server since
296 the specified date - in seconds since the ecpoh, GMT - optionally
297 restricted to the given distributions. gotNewGroups() is called
298 on success, getNewGroupsFailed() on failure.
300 date, timeStr = time.strftime('%y%m%d %H%M%S', time.gmtime(date)).split()
301 self.sendLine('NEWGROUPS %s %s %s' % (date, timeStr, distributions))
302 self._newState(self._stateNewGroups, self.getNewGroupsFailed)
305 def fetchXHeader(self, header, low = None, high = None, id = None):
307 Request a specific header from the server for an article or range
308 of articles. If 'id' is not None, a header for only the article
309 with that Message-ID will be requested. If both low and high are
310 None, a header for the currently selected article will be selected;
311 If both low and high are zero-length strings, headers for all articles
312 in the currently selected group will be requested; Otherwise, high
313 and low will be used as bounds - if one is None the first or last
314 article index will be substituted, as appropriate.
317 r = header + ' <%s>' % (id,)
318 elif low is high is None:
321 r = header + ' %d-' % (low,)
323 r = header + ' -%d' % (high,)
325 r = header + ' %d-%d' % (low, high)
326 self.sendLine('XHDR ' + r)
327 self._newState(self._stateXHDR, self.getXHeaderFailed)
332 Set the mode to STREAM, suspending the normal "lock-step" mode of
333 communications. setStreamSuccess() is called on success,
334 setStreamFailed() on failure.
336 self.sendLine('MODE STREAM')
337 self._newState(None, self.setStreamFailed, self._headerMode)
341 self.sendLine('QUIT')
342 self.transport.loseConnection()
345 def _newState(self, method, error, responseHandler = None):
346 self._inputBuffers.append([])
347 self._responseCodes.append(None)
348 self._state.append(method)
349 self._error.append(error)
350 self._responseHandlers.append(responseHandler)
354 buf = self._inputBuffers[0]
355 del self._responseCodes[0]
356 del self._inputBuffers[0]
359 del self._responseHandlers[0]
363 def _newLine(self, line, check = 1):
364 if check and line and line[0] == '.':
366 self._inputBuffers[0].append(line)
369 def _setResponseCode(self, code):
370 self._responseCodes[0] = code
373 def _getResponseCode(self):
374 return self._responseCodes[0]
377 def lineReceived(self, line):
378 if not len(self._state):
379 self._statePassive(line)
380 elif self._getResponseCode() is None:
381 code = extractCode(line)
382 if code is None or not (200 <= code[0] < 400): # An error!
386 self._setResponseCode(code)
387 if self._responseHandlers[0]:
388 self._responseHandlers[0](code)
393 def _statePassive(self, line):
394 log.msg('Server said: %s' % line)
397 def _passiveError(self, error):
398 log.err('Passive Error: %s' % (error,))
401 def _headerInitial(self, (code, message)):
409 def _stateList(self, line):
411 data = filter(None, line.strip().split())
412 self._newLine((data[0], int(data[1]), int(data[2]), data[3]), 0)
414 self.gotAllGroups(self._endState())
417 def _stateOverview(self, line):
419 self._newLine(filter(None, line.strip().split()), 0)
421 self.gotOverview(self._endState())
424 def _stateSubscriptions(self, line):
426 self._newLine(line.strip(), 0)
428 self.gotSubscriptions(self._endState())
431 def _headerGroup(self, (code, line)):
432 self.gotGroup(tuple(line.split()))
436 def _stateArticle(self, line):
438 if line.startswith('.'):
440 self._newLine(line, 0)
442 self.gotArticle('\n'.join(self._endState())+'\n')
445 def _stateHead(self, line):
447 self._newLine(line, 0)
449 self.gotHead('\n'.join(self._endState()))
452 def _stateBody(self, line):
454 if line.startswith('.'):
456 self._newLine(line, 0)
458 self.gotBody('\n'.join(self._endState())+'\n')
461 def _headerPost(self, (code, message)):
463 self.transport.write(self._postText[0].replace('\n', '\r\n').replace('\r\n.', '\r\n..'))
464 if self._postText[0][-1:] != '\n':
467 del self._postText[0]
468 self._newState(None, self.postFailed, self._headerPosted)
470 self.postFailed('%d %s' % (code, message))
474 def _headerPosted(self, (code, message)):
478 self.postFailed('%d %s' % (code, message))
482 def _stateXHDR(self, line):
484 self._newLine(line.split(), 0)
486 self._gotXHeader(self._endState())
489 def _stateNewNews(self, line):
491 self._newLine(line, 0)
493 self.gotNewNews(self._endState())
496 def _stateNewGroups(self, line):
498 self._newLine(line, 0)
500 self.gotNewGroups(self._endState())
503 def _headerMode(self, (code, message)):
505 self.setStreamSuccess()
507 self.setStreamFailed((code, message))
511 class NNTPServer(basic.LineReceiver):
513 'LIST', 'GROUP', 'ARTICLE', 'STAT', 'MODE', 'LISTGROUP', 'XOVER',
514 'XHDR', 'HEAD', 'BODY', 'NEXT', 'LAST', 'POST', 'QUIT', 'IHAVE',
515 'HELP', 'SLAVE', 'XPATH', 'XINDEX', 'XROVER', 'TAKETHIS', 'CHECK'
519 self.servingSlave = 0
522 def connectionMade(self):
523 self.inputHandler = None
524 self.currentGroup = None
525 self.currentIndex = None
526 self.sendLine('200 server ready - posting allowed')
528 def lineReceived(self, line):
529 if self.inputHandler is not None:
530 self.inputHandler(line)
532 parts = line.strip().split()
534 cmd, parts = parts[0].upper(), parts[1:]
535 if cmd in NNTPServer.COMMANDS:
536 func = getattr(self, 'do_%s' % cmd)
540 self.sendLine('501 command syntax error')
541 log.msg("501 command syntax error")
542 log.msg("command was", line)
545 self.sendLine('503 program fault - command not performed')
546 log.msg("503 program fault")
547 log.msg("command was", line)
550 self.sendLine('500 command not recognized')
553 def do_LIST(self, subcmd = '', *dummy):
554 subcmd = subcmd.strip().lower()
555 if subcmd == 'newsgroups':
556 # XXX - this could use a real implementation, eh?
557 self.sendLine('215 Descriptions in form "group description"')
559 elif subcmd == 'overview.fmt':
560 defer = self.factory.backend.overviewRequest()
561 defer.addCallbacks(self._gotOverview, self._errOverview)
563 elif subcmd == 'subscriptions':
564 defer = self.factory.backend.subscriptionRequest()
565 defer.addCallbacks(self._gotSubscription, self._errSubscription)
566 log.msg('subscriptions')
568 defer = self.factory.backend.listRequest()
569 defer.addCallbacks(self._gotList, self._errList)
571 self.sendLine('500 command not recognized')
574 def _gotList(self, list):
575 self.sendLine('215 newsgroups in form "group high low flags"')
577 self.sendLine('%s %d %d %s' % tuple(i))
581 def _errList(self, failure):
582 print 'LIST failed: ', failure
583 self.sendLine('503 program fault - command not performed')
586 def _gotSubscription(self, parts):
587 self.sendLine('215 information follows')
593 def _errSubscription(self, failure):
594 print 'SUBSCRIPTIONS failed: ', failure
595 self.sendLine('503 program fault - comand not performed')
598 def _gotOverview(self, parts):
599 self.sendLine('215 Order of fields in overview database.')
601 self.sendLine(i + ':')
605 def _errOverview(self, failure):
606 print 'LIST OVERVIEW.FMT failed: ', failure
607 self.sendLine('503 program fault - command not performed')
610 def do_LISTGROUP(self, group = None):
611 group = group or self.currentGroup
613 self.sendLine('412 Not currently in newsgroup')
615 defer = self.factory.backend.listGroupRequest(group)
616 defer.addCallbacks(self._gotListGroup, self._errListGroup)
619 def _gotListGroup(self, (group, articles)):
620 self.currentGroup = group
622 self.currentIndex = int(articles[0])
624 self.currentIndex = None
626 self.sendLine('211 list of article numbers follow')
628 self.sendLine(str(i))
632 def _errListGroup(self, failure):
633 print 'LISTGROUP failed: ', failure
634 self.sendLine('502 no permission')
637 def do_XOVER(self, range):
638 if self.currentGroup is None:
639 self.sendLine('412 No news group currently selected')
641 l, h = parseRange(range)
642 defer = self.factory.backend.xoverRequest(self.currentGroup, l, h)
643 defer.addCallbacks(self._gotXOver, self._errXOver)
646 def _gotXOver(self, parts):
647 self.sendLine('224 Overview information follows')
649 self.sendLine('\t'.join(map(str, i)))
653 def _errXOver(self, failure):
654 print 'XOVER failed: ', failure
655 self.sendLine('420 No article(s) selected')
658 def xhdrWork(self, header, range):
659 if self.currentGroup is None:
660 self.sendLine('412 No news group currently selected')
663 if self.currentIndex is None:
664 self.sendLine('420 No current article selected')
667 l = h = self.currentIndex
669 # FIXME: articles may be a message-id
670 l, h = parseRange(range)
673 self.sendLine('430 no such article')
675 return self.factory.backend.xhdrRequest(self.currentGroup, l, h, header)
678 def do_XHDR(self, header, range = None):
679 d = self.xhdrWork(header, range)
681 d.addCallbacks(self._gotXHDR, self._errXHDR)
684 def _gotXHDR(self, parts):
685 self.sendLine('221 Header follows')
687 self.sendLine('%d %s' % i)
690 def _errXHDR(self, failure):
691 print 'XHDR failed: ', failure
692 self.sendLine('502 no permission')
696 self.inputHandler = self._doingPost
698 self.sendLine('340 send article to be posted. End with <CR-LF>.<CR-LF>')
701 def _doingPost(self, line):
703 self.inputHandler = None
704 group, article = self.currentGroup, self.message
707 defer = self.factory.backend.postRequest(article)
708 defer.addCallbacks(self._gotPost, self._errPost)
710 self.message = self.message + line + '\r\n'
713 def _gotPost(self, parts):
714 self.sendLine('240 article posted ok')
717 def _errPost(self, failure):
718 print 'POST failed: ', failure
719 self.sendLine('441 posting failed')
722 def do_CHECK(self, id):
723 d = self.factory.backend.articleExistsRequest(id)
724 d.addCallbacks(self._gotCheck, self._errCheck)
727 def _gotCheck(self, result):
729 self.sendLine("438 already have it, please don't send it to me")
731 self.sendLine('238 no such article found, please send it to me')
734 def _errCheck(self, failure):
735 print 'CHECK failed: ', failure
736 self.sendLine('431 try sending it again later')
739 def do_TAKETHIS(self, id):
740 self.inputHandler = self._doingTakeThis
744 def _doingTakeThis(self, line):
746 self.inputHandler = None
747 article = self.message
749 d = self.factory.backend.postRequest(article)
750 d.addCallbacks(self._didTakeThis, self._errTakeThis)
752 self.message = self.message + line + '\r\n'
755 def _didTakeThis(self, result):
756 self.sendLine('239 article transferred ok')
759 def _errTakeThis(self, failure):
760 print 'TAKETHIS failed: ', failure
761 self.sendLine('439 article transfer failed')
764 def do_GROUP(self, group):
765 defer = self.factory.backend.groupRequest(group)
766 defer.addCallbacks(self._gotGroup, self._errGroup)
769 def _gotGroup(self, (name, num, high, low, flags)):
770 self.currentGroup = name
771 self.currentIndex = low
772 self.sendLine('211 %d %d %d %s group selected' % (num, low, high, name))
775 def _errGroup(self, failure):
776 print 'GROUP failed: ', failure
777 self.sendLine('411 no such group')
780 def articleWork(self, article, cmd, func):
781 if self.currentGroup is None:
782 self.sendLine('412 no newsgroup has been selected')
785 if self.currentIndex is None:
786 self.sendLine('420 no current article has been selected')
788 article = self.currentIndex
790 if article[0] == '<':
791 return func(self.currentGroup, index = None, id = article)
794 article = int(article)
795 return func(self.currentGroup, article)
797 self.sendLine('501 command syntax error')
800 def do_ARTICLE(self, article = None):
801 defer = self.articleWork(article, 'ARTICLE', self.factory.backend.articleRequest)
803 defer.addCallbacks(self._gotArticle, self._errArticle)
806 def _gotArticle(self, (index, id, article)):
807 self.currentIndex = index
808 self.sendLine('220 %d %s article' % (index, id))
809 s = basic.FileSender()
810 d = s.beginFileTransfer(article, self.transport)
811 d.addCallback(self.finishedFileTransfer)
814 ## Helper for FileSender
816 def finishedFileTransfer(self, lastsent):
824 def _errArticle(self, failure):
825 print 'ARTICLE failed: ', failure
826 self.sendLine('423 bad article number')
829 def do_STAT(self, article = None):
830 defer = self.articleWork(article, 'STAT', self.factory.backend.articleRequest)
832 defer.addCallbacks(self._gotStat, self._errStat)
835 def _gotStat(self, (index, id, article)):
836 self.currentIndex = index
837 self.sendLine('223 %d %s article retreived - request text separately' % (index, id))
840 def _errStat(self, failure):
841 print 'STAT failed: ', failure
842 self.sendLine('423 bad article number')
845 def do_HEAD(self, article = None):
846 defer = self.articleWork(article, 'HEAD', self.factory.backend.headRequest)
848 defer.addCallbacks(self._gotHead, self._errHead)
851 def _gotHead(self, (index, id, head)):
852 self.currentIndex = index
853 self.sendLine('221 %d %s article retrieved' % (index, id))
854 self.transport.write(head + '\r\n')
858 def _errHead(self, failure):
859 print 'HEAD failed: ', failure
860 self.sendLine('423 no such article number in this group')
863 def do_BODY(self, article):
864 defer = self.articleWork(article, 'BODY', self.factory.backend.bodyRequest)
866 defer.addCallbacks(self._gotBody, self._errBody)
869 def _gotBody(self, (index, id, body)):
870 self.currentIndex = index
871 self.sendLine('221 %d %s article retrieved' % (index, id))
873 s = basic.FileSender()
874 d = s.beginFileTransfer(body, self.transport)
875 d.addCallback(self.finishedFileTransfer)
877 def _errBody(self, failure):
878 print 'BODY failed: ', failure
879 self.sendLine('423 no such article number in this group')
882 # NEXT and LAST are just STATs that increment currentIndex first.
883 # Accordingly, use the STAT callbacks.
885 i = self.currentIndex + 1
886 defer = self.factory.backend.articleRequest(self.currentGroup, i)
887 defer.addCallbacks(self._gotStat, self._errStat)
891 i = self.currentIndex - 1
892 defer = self.factory.backend.articleRequest(self.currentGroup, i)
893 defer.addCallbacks(self._gotStat, self._errStat)
896 def do_MODE(self, cmd):
897 cmd = cmd.strip().upper()
899 self.servingSlave = 0
900 self.sendLine('200 Hello, you can post')
901 elif cmd == 'STREAM':
902 self.sendLine('500 Command not understood')
904 # This is not a mistake
905 self.sendLine('500 Command not understood')
909 self.sendLine('205 goodbye')
910 self.transport.loseConnection()
914 self.sendLine('100 help text follows')
915 self.sendLine('Read the RFC.')
920 self.sendLine('202 slave status noted')
921 self.servingeSlave = 1
924 def do_XPATH(self, article):
925 # XPATH is a silly thing to have. No client has the right to ask
926 # for this piece of information from me, and so that is what I'll
928 self.sendLine('502 access restriction or permission denied')
931 def do_XINDEX(self, article):
932 # XINDEX is another silly command. The RFC suggests it be relegated
933 # to the history books, and who am I to disagree?
934 self.sendLine('502 access restriction or permission denied')
937 def do_XROVER(self, range=None):
939 Handle a request for references of all messages in the currently
942 This generates the same response a I{XHDR References} request would
945 self.do_XHDR('References', range)
948 def do_IHAVE(self, id):
949 self.factory.backend.articleExistsRequest(id).addCallback(self._foundArticle)
952 def _foundArticle(self, result):
954 self.sendLine('437 article rejected - do not try again')
956 self.sendLine('335 send article to be transferred. End with <CR-LF>.<CR-LF>')
957 self.inputHandler = self._handleIHAVE
961 def _handleIHAVE(self, line):
963 self.inputHandler = None
964 self.factory.backend.postRequest(
966 ).addCallbacks(self._gotIHAVE, self._errIHAVE)
970 self.message = self.message + line + '\r\n'
973 def _gotIHAVE(self, result):
974 self.sendLine('235 article transferred ok')
977 def _errIHAVE(self, failure):
978 print 'IHAVE failed: ', failure
979 self.sendLine('436 transfer failed - try again later')
982 class UsenetClientProtocol(NNTPClient):
984 A client that connects to an NNTP server and asks for articles new
985 since a certain time.
988 def __init__(self, groups, date, storage):
990 Fetch all new articles from the given groups since the
991 given date and dump them into the given storage. groups
992 is a list of group names. date is an integer or floating
993 point representing seconds since the epoch (GMT). storage is
994 any object that implements the NewsStorage interface.
996 NNTPClient.__init__(self)
997 self.groups, self.date, self.storage = groups, date, storage
1000 def connectionMade(self):
1001 NNTPClient.connectionMade(self)
1002 log.msg("Initiating update with remote host: " + str(self.transport.getPeer()))
1004 self.fetchNewNews(self.groups, self.date, '')
1007 def articleExists(self, exists, article):
1009 self.fetchArticle(article)
1011 self.count = self.count - 1
1012 self.disregard = self.disregard + 1
1015 def gotNewNews(self, news):
1017 self.count = len(news)
1018 log.msg("Transfering " + str(self.count) + " articles from remote host: " + str(self.transport.getPeer()))
1020 self.storage.articleExistsRequest(i).addCallback(self.articleExists, i)
1023 def getNewNewsFailed(self, reason):
1024 log.msg("Updated failed (" + reason + ") with remote host: " + str(self.transport.getPeer()))
1028 def gotArticle(self, article):
1029 self.storage.postRequest(article)
1030 self.count = self.count - 1
1032 log.msg("Completed update with remote host: " + str(self.transport.getPeer()))
1034 log.msg("Disregarded %d articles." % (self.disregard,))
1035 self.factory.updateChecks(self.transport.getPeer())