1
# Twisted, the Framework of Your Internet
2
# Copyright (C) 2001 Matthew W. Lefkowitz
4
# This library is free software; you can redistribute it and/or
5
# modify it under the terms of version 2.1 of the GNU Lesser General Public
6
# License as published by the Free Software Foundation.
8
# This library is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11
# Lesser General Public License for more details.
13
# You should have received a copy of the GNU Lesser General Public
14
# License along with this library; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
NNTP protocol support.
20
Stability: semi-stable
22
Maintainer: U{Jp Calderone<mailto:exarkun@twistedmatrix.com>}
24
The following protocol commands are currently understood::
26
LIST LISTGROUP XOVER XHDR
27
POST GROUP ARTICLE STAT HEAD
28
BODY NEXT MODE STREAM MODE READER SLAVE
29
LAST QUIT HELP IHAVE XPATH
30
XINDEX XROVER TAKETHIS CHECK
32
The following protocol commands require implementation::
36
XTHREAD AUTHINFO NEWGROUPS
39
Other desired features:
42
- More robust client input handling
46
from twisted.internet import protocol
47
from twisted.protocols import basic
48
from twisted.python import log
49
from twisted.python import failure
55
import cStringIO as StringIO
60
articles = text.split('-')
61
if len(articles) == 1:
67
elif len(articles) == 2:
82
def extractCode(line):
83
line = line.split(' ', 1)
87
return int(line[0]), line[1]
92
class NNTPError(Exception):
93
def __init__(self, string):
97
return 'NNTPError: %s' % self.string
100
class NNTPClient(basic.LineReceiver):
101
MAX_COMMAND_LENGTH = 510
104
self.currentGroup = None
108
self._inputBuffers = []
109
self._responseCodes = []
110
self._responseHandlers = []
114
self._newState(self._statePassive, None, self._headerInitial)
117
def connectionMade(self):
118
self.ip = self.transport.getPeer()[1:]
120
def gotAllGroups(self, groups):
121
"Override for notification when fetchGroups() action is completed"
124
def getAllGroupsFailed(self, error):
125
"Override for notification when fetchGroups() action fails"
128
def gotOverview(self, overview):
129
"Override for notification when fetchOverview() action is completed"
132
def getOverviewFailed(self, error):
133
"Override for notification when fetchOverview() action fails"
136
def gotSubscriptions(self, subscriptions):
137
"Override for notification when fetchSubscriptions() action is completed"
140
def getSubscriptionsFailed(self, error):
141
"Override for notification when fetchSubscriptions() action fails"
144
def gotGroup(self, group):
145
"Override for notification when fetchGroup() action is completed"
148
def getGroupFailed(self, error):
149
"Override for notification when fetchGroup() action fails"
152
def gotArticle(self, article):
153
"Override for notification when fetchArticle() action is completed"
156
def getArticleFailed(self, error):
157
"Override for notification when fetchArticle() action fails"
160
def gotHead(self, head):
161
"Override for notification when fetchHead() action is completed"
164
def getHeadFailed(self, error):
165
"Override for notification when fetchHead() action fails"
168
def gotBody(self, info):
169
"Override for notification when fetchBody() action is completed"
172
def getBodyFailed(self, body):
173
"Override for notification when fetchBody() action fails"
177
"Override for notification when postArticle() action is successful"
180
def postFailed(self, error):
181
"Override for notification when postArticle() action fails"
184
def gotXHeader(self, headers):
185
"Override for notification when getXHeader() action is successful"
188
def getXHeaderFailed(self, error):
189
"Override for notification when getXHeader() action fails"
192
def gotNewNews(self, news):
193
"Override for notification when getNewNews() action is successful"
196
def getNewNewsFailed(self, error):
197
"Override for notification when getNewNews() action fails"
200
def gotNewGroups(self, groups):
201
"Override for notification when getNewGroups() action is successful"
204
def getNewGroupsFailed(self, error):
205
"Override for notification when getNewGroups() action fails"
208
def setStreamSuccess(self):
209
"Override for notification when setStream() action is successful"
212
def setStreamFailed(self, error):
213
"Override for notification when setStream() action fails"
216
def fetchGroups(self):
218
Request a list of all news groups from the server. gotAllGroups()
219
is called on success, getGroupsFailed() on failure
221
self.sendLine('LIST')
222
self._newState(self._stateList, self.getAllGroupsFailed)
225
def fetchOverview(self):
227
Request the overview format from the server. gotOverview() is called
228
on success, getOverviewFailed() on failure
230
self.sendLine('LIST OVERVIEW.FMT')
231
self._newState(self._stateOverview, self.getOverviewFailed)
234
def fetchSubscriptions(self):
236
Request a list of the groups it is recommended a new user subscribe to.
237
gotSubscriptions() is called on success, getSubscriptionsFailed() on
240
self.sendLine('LIST SUBSCRIPTIONS')
241
self._newState(self._stateSubscriptions, self.getSubscriptionsFailed)
244
def fetchGroup(self, group):
246
Get group information for the specified group from the server. gotGroup()
247
is called on success, getGroupFailed() on failure.
249
self.sendLine('GROUP %s' % (group,))
250
self._newState(None, self.getGroupFailed, self._headerGroup)
253
def fetchHead(self, index = ''):
255
Get the header for the specified article (or the currently selected
256
article if index is '') from the server. gotHead() is called on
257
success, getHeadFailed() on failure
259
self.sendLine('HEAD %s' % (index,))
260
self._newState(self._stateHead, self.getHeadFailed)
263
def fetchBody(self, index = ''):
265
Get the body for the specified article (or the currently selected
266
article if index is '') from the server. gotBody() is called on
267
success, getBodyFailed() on failure
269
self.sendLine('BODY %s' % (index,))
270
self._newState(self._stateBody, self.getBodyFailed)
273
def fetchArticle(self, index = ''):
275
Get the complete article with the specified index (or the currently
276
selected article if index is '') or Message-ID from the server.
277
gotArticle() is called on success, getArticleFailed() on failure.
279
self.sendLine('ARTICLE %s' % (index,))
280
self._newState(self._stateArticle, self.getArticleFailed)
283
def postArticle(self, text):
285
Attempt to post an article with the specified text to the server. 'text'
286
must consist of both head and body data, as specified by RFC 850. If the
287
article is posted successfully, postedOk() is called, otherwise postFailed()
290
self.sendLine('POST')
291
self._newState(None, self.postFailed, self._headerPost)
292
self._postText.append(text)
295
def fetchNewNews(self, groups, date, distributions = ''):
297
Get the Message-IDs for all new news posted to any of the given
298
groups since the specified date - in seconds since the epoch, GMT -
299
optionally restricted to the given distributions. gotNewNews() is
300
called on success, getNewNewsFailed() on failure.
302
One invocation of this function may result in multiple invocations
303
of gotNewNews()/getNewNewsFailed().
305
date, timeStr = time.strftime('%y%m%d %H%M%S', time.gmtime(date)).split()
306
line = 'NEWNEWS %%s %s %s %s' % (date, timeStr, distributions)
308
while len(groups) and len(line) + len(groupPart) + len(groups[-1]) + 1 < NNTPClient.MAX_COMMAND_LENGTH:
310
groupPart = groupPart + ',' + group
312
self.sendLine(line % (groupPart,))
313
self._newState(self._stateNewNews, self.getNewNewsFailed)
316
self.fetchNewNews(groups, date, distributions)
319
def fetchNewGroups(self, date, distributions):
321
Get the names of all new groups created/added to the server since
322
the specified date - in seconds since the ecpoh, GMT - optionally
323
restricted to the given distributions. gotNewGroups() is called
324
on success, getNewGroupsFailed() on failure.
326
date, timeStr = time.strftime('%y%m%d %H%M%S', time.gmtime(date)).split()
327
self.sendLine('NEWGROUPS %s %s %s' % (date, timeStr, distributions))
328
self._newState(self._stateNewGroups, self.getNewGroupsFailed)
331
def fetchXHeader(self, header, low = None, high = None, id = None):
333
Request a specific header from the server for an article or range
334
of articles. If 'id' is not None, a header for only the article
335
with that Message-ID will be requested. If both low and high are
336
None, a header for the currently selected article will be selected;
337
If both low and high are zero-length strings, headers for all articles
338
in the currently selected group will be requested; Otherwise, high
339
and low will be used as bounds - if one is None the first or last
340
article index will be substituted, as appropriate.
343
r = header + ' <%s>' % (id,)
344
elif low is high is None:
347
r = header + ' %d-' % (low,)
349
r = header + ' -%d' % (high,)
351
r = header + ' %d-%d' % (low, high)
352
self.sendLine('XHDR ' + r)
353
self._newState(self._stateXHDR, self.getXHeaderFailed)
358
Set the mode to STREAM, suspending the normal "lock-step" mode of
359
communications. setStreamSuccess() is called on success,
360
setStreamFailed() on failure.
362
self.sendLine('MODE STREAM')
363
self._newState(None, self.setStreamFailed, self._headerMode)
367
self.sendLine('QUIT')
368
self.transport.loseConnection()
371
def _newState(self, method, error, responseHandler = None):
372
self._inputBuffers.append([])
373
self._responseCodes.append(None)
374
self._state.append(method)
375
self._error.append(error)
376
self._responseHandlers.append(responseHandler)
380
buf = self._inputBuffers[0]
381
del self._responseCodes[0]
382
del self._inputBuffers[0]
385
del self._responseHandlers[0]
389
def _newLine(self, line, check = 1):
390
if check and line and line[0] == '.':
392
self._inputBuffers[0].append(line)
395
def _setResponseCode(self, code):
396
self._responseCodes[0] = code
399
def _getResponseCode(self):
400
return self._responseCodes[0]
403
def lineReceived(self, line):
404
if not len(self._state):
405
self._statePassive(line)
406
elif self._getResponseCode() is None:
407
code = extractCode(line)
408
if code is None or not (200 <= code[0] < 400): # An error!
412
self._setResponseCode(code)
413
if self._responseHandlers[0]:
414
self._responseHandlers[0](code)
419
def _statePassive(self, line):
420
log.msg('Server said: %s' % line)
423
def _passiveError(self, error):
424
log.err('Passive Error: %s' % (error,))
427
def _headerInitial(self, (code, message)):
435
def _stateList(self, line):
437
data = filter(None, line.strip().split())
438
self._newLine((data[0], int(data[1]), int(data[2]), data[3]), 0)
440
self.gotAllGroups(self._endState())
443
def _stateOverview(self, line):
445
self._newLine(filter(None, line.strip().split()), 0)
447
self.gotOverview(self._endState())
450
def _stateSubscriptions(self, line):
452
self._newLine(line.strip(), 0)
454
self.gotSubscriptions(self._endState())
457
def _headerGroup(self, (code, line)):
458
self.gotGroup(tuple(line.split()))
462
def _stateArticle(self, line):
464
self._newLine(line, 0)
466
self.gotArticle('\n'.join(self._endState()))
469
def _stateHead(self, line):
471
self._newLine(line, 0)
473
self.gotHead('\n'.join(self._endState()))
476
def _stateBody(self, line):
478
self._newLine(line, 0)
480
self.gotBody('\n'.join(self._endState()))
483
def _headerPost(self, (code, message)):
485
self.transport.write(self._postText[0])
486
if self._postText[-2:] != '\r\n':
487
self.sendLine('\r\n')
489
del self._postText[0]
490
self._newState(None, self.postFailed, self._headerPosted)
492
self.postFailed('%d %s' % (code, message))
496
def _headerPosted(self, (code, message)):
500
self.postFailed('%d %s' % (code, message))
504
def _stateXHDR(self, line):
506
self._newLine(line.split(), 0)
508
self._gotXHeader(self._endState())
511
def _stateNewNews(self, line):
513
self._newLine(line, 0)
515
self.gotNewNews(self._endState())
518
def _stateNewGroups(self, line):
520
self._newLine(line, 0)
522
self.gotNewGroups(self._endState())
525
def _headerMode(self, (code, message)):
527
self.setStreamSuccess()
529
self.setStreamFailed((code, message))
533
class NNTPServer(basic.LineReceiver):
535
'LIST', 'GROUP', 'ARTICLE', 'STAT', 'MODE', 'LISTGROUP', 'XOVER',
536
'XHDR', 'HEAD', 'BODY', 'NEXT', 'LAST', 'POST', 'QUIT', 'IHAVE',
537
'HELP', 'SLAVE', 'XPATH', 'XINDEX', 'XROVER', 'TAKETHIS', 'CHECK'
541
self.servingSlave = 0
544
def connectionMade(self):
545
self.ip = self.transport.getPeer()[1:]
546
self.inputHandler = None
547
self.currentGroup = None
548
self.currentIndex = None
549
self.sendLine('200 server ready - posting allowed')
551
def lineReceived(self, line):
552
if self.inputHandler is not None:
553
self.inputHandler(line)
555
parts = line.strip().split()
557
cmd, parts = parts[0].upper(), parts[1:]
558
if cmd in NNTPServer.COMMANDS:
559
func = getattr(self, 'do_%s' % cmd)
563
self.sendLine('501 command syntax error')
564
log.msg("501 command syntax error")
565
log.msg("command was", line)
568
self.sendLine('503 program fault - command not performed')
569
log.msg("503 program fault")
570
log.msg("command was", line)
573
self.sendLine('500 command not recognized')
576
def do_LIST(self, subcmd = '', *dummy):
577
subcmd = subcmd.strip().lower()
578
if subcmd == 'newsgroups':
579
# XXX - this could use a real implementation, eh?
580
self.sendLine('215 Descriptions in form "group description"')
582
elif subcmd == 'overview.fmt':
583
defer = self.factory.backend.overviewRequest()
584
defer.addCallbacks(self._gotOverview, self._errOverview)
586
elif subcmd == 'subscriptions':
587
defer = self.factory.backend.subscriptionRequest()
588
defer.addCallbacks(self._gotSubscription, self._errSubscription)
589
log.msg('subscriptions')
591
defer = self.factory.backend.listRequest()
592
defer.addCallbacks(self._gotList, self._errList)
594
self.sendLine('500 command not recognized')
597
def _gotList(self, list):
598
self.sendLine('215 newsgroups in form "group high low flags"')
600
self.sendLine('%s %d %d %s' % tuple(i))
604
def _errList(self, failure):
605
print 'LIST failed: ', failure
606
self.sendLine('503 program fault - command not performed')
609
def _gotSubscription(self, parts):
610
self.sendLine('215 information follows')
616
def _errSubscription(self, failure):
617
print 'SUBSCRIPTIONS failed: ', failure
618
self.sendLine('503 program fault - comand not performed')
621
def _gotOverview(self, parts):
622
self.sendLine('215 Order of fields in overview database.')
624
self.sendLine(i + ':')
628
def _errOverview(self, failure):
629
print 'LIST OVERVIEW.FMT failed: ', failure
630
self.sendLine('503 program fault - command not performed')
633
def do_LISTGROUP(self, group = None):
634
group = group or self.currentGroup
636
self.sendLine('412 Not currently in newsgroup')
638
defer = self.factory.backend.listGroupRequest(group)
639
defer.addCallbacks(self._gotListGroup, self._errListGroup)
642
def _gotListGroup(self, (group, articles)):
643
self.currentGroup = group
645
self.currentIndex = int(articles[0])
647
self.currentIndex = None
649
self.sendLine('211 list of article numbers follow')
651
self.sendLine(str(i))
655
def _errListGroup(self, failure):
656
print 'LISTGROUP failed: ', failure
657
self.sendLine('502 no permission')
660
def do_XOVER(self, range):
661
if self.currentGroup is None:
662
self.sendLine('412 No news group currently selected')
664
l, h = parseRange(range)
665
defer = self.factory.backend.xoverRequest(self.currentGroup, l, h)
666
defer.addCallbacks(self._gotXOver, self._errXOver)
669
def _gotXOver(self, parts):
670
self.sendLine('224 Overview information follows')
672
self.sendLine('\t'.join(map(str, i)))
676
def _errXOver(self, failure):
677
print 'XOVER failed: ', failure
678
self.sendLine('420 No article(s) selected')
681
def xhdrWork(self, header, range):
682
if self.currentGroup is None:
683
self.sendLine('412 No news group currently selected')
686
if self.currentIndex is None:
687
self.sendLine('420 No current article selected')
690
l = h = self.currentIndex
692
# FIXME: articles may be a message-id
693
l, h = parseRange(range)
696
self.sendLine('430 no such article')
698
return self.factory.backend.xhdrRequest(self.currentGroup, l, h, header)
701
def do_XHDR(self, header, range = None):
702
d = self.xhdrWork(header, range)
704
d.addCallbacks(self._gotXHDR, self._errXHDR)
707
def _gotXHDR(self, parts):
708
self.sendLine('221 Header follows')
710
self.sendLine('%d %s' % i)
713
def _errXHDR(self, failure):
714
print 'XHDR failed: ', failure
715
self.sendLine('502 no permission')
718
def do_XROVER(self, header, range = None):
719
d = self.xhdrWork(header, range)
721
d.addCallbacks(self._gotXROVER, self._errXROVER)
724
def _gotXROVER(self, parts):
725
self.sendLine('224 Overview information follows')
727
self.sendLine('%d %s' % i)
731
def _errXROVER(self, failure):
732
print 'XROVER failed: ',
733
self._errXHDR(failure)
737
self.inputHandler = self._doingPost
739
self.sendLine('340 send article to be posted. End with <CR-LF>.<CR-LF>')
742
def _doingPost(self, line):
744
self.inputHandler = None
745
group, article = self.currentGroup, self.message
748
defer = self.factory.backend.postRequest(article)
749
defer.addCallbacks(self._gotPost, self._errPost)
751
if line and line[0] == '.':
753
self.message = self.message + line + '\r\n'
756
def _gotPost(self, parts):
757
self.sendLine('240 article posted ok')
760
def _errPost(self, failure):
761
print 'POST failed: ', failure
762
self.sendLine('441 posting failed')
765
def do_CHECK(self, id):
766
d = self.factory.backend.articleExistsRequest(id)
767
d.addCallbacks(self._gotCheck, self._errCheck)
770
def _gotCheck(self, result):
772
self.sendLine("438 already have it, please don't send it to me")
774
self.sendLine('238 no such article found, please send it to me')
777
def _errCheck(self, failure):
778
print 'CHECK failed: ', failure
779
self.sendLine('431 try sending it again later')
782
def do_TAKETHIS(self, id):
783
self.inputHandler = self._doingTakeThis
787
def _doingTakeThis(self, line):
789
self.inputHandler = None
790
article = self.message
792
d = self.factory.backend.postRequest(article)
793
d.addCallbacks(self._didTakeThis, self._errTakeThis)
795
if line and line[0] == '.':
797
self.message = self.message + line + '\r\n'
800
def _didTakeThis(self, result):
801
self.sendLine('239 article transferred ok')
804
def _errTakeThis(self, failure):
805
print 'TAKETHIS failed: ', failure
806
self.sendLine('439 article transfer failed')
809
def do_GROUP(self, group):
810
defer = self.factory.backend.groupRequest(group)
811
defer.addCallbacks(self._gotGroup, self._errGroup)
814
def _gotGroup(self, (name, num, high, low, flags)):
815
self.currentGroup = name
816
self.currentIndex = low
817
self.sendLine('211 %d %d %d %s group selected' % (num, low, high, name))
820
def _errGroup(self, failure):
821
print 'GROUP failed: ', failure
822
self.sendLine('411 no such group')
825
def articleWork(self, article, cmd, func):
826
if self.currentGroup is None:
827
self.sendLine('412 no newsgroup has been selected')
830
if self.currentIndex is None:
831
self.sendLine('420 no current article has been selected')
833
article = self.currentIndex
835
if article[0] == '<':
836
return func(self.currentGroup, index = None, id = article)
839
article = int(article)
840
return func(self.currentGroup, article)
841
except ValueError, e:
842
self.sendLine('501 command syntax error')
845
def do_ARTICLE(self, article = None):
846
defer = self.articleWork(article, 'ARTICLE', self.factory.backend.articleRequest)
848
defer.addCallbacks(self._gotArticle, self._errArticle)
851
def _gotArticle(self, (index, id, article)):
852
if isinstance(article, types.StringType):
855
"Returning the article as a string from `articleRequest' "
856
"is deprecated. Return a file-like object instead."
858
article = StringIO.StringIO(article)
859
self.currentIndex = index
860
self.sendLine('220 %d %s article' % (index, id))
861
s = basic.FileSender()
862
d = s.beginFileTransfer(article, self.transport, self.transformChunk)
863
d.addCallback(self.finishedFileTransfer)
866
## Helpers for FileSender
868
def transformChunk(self, chunk):
869
return chunk.replace('\n', '\r\n').replace('\r\n.', '\r\n..')
871
def finishedFileTransfer(self, lastsent):
879
def _errArticle(self, failure):
880
print 'ARTICLE failed: ', failure
881
self.sendLine('423 bad article number')
884
def do_STAT(self, article = None):
885
defer = self.articleWork(article, 'STAT', self.factory.backend.articleRequest)
887
defer.addCallbacks(self._gotStat, self._errStat)
890
def _gotStat(self, (index, id, article)):
891
self.currentIndex = index
892
self.sendLine('223 %d %s article retreived - request text separately' % (index, id))
895
def _errStat(self, failure):
896
print 'STAT failed: ', failure
897
self.sendLine('423 bad article number')
900
def do_HEAD(self, article = None):
901
defer = self.articleWork(article, 'HEAD', self.factory.backend.headRequest)
903
defer.addCallbacks(self._gotHead, self._errHead)
906
def _gotHead(self, (index, id, head)):
907
self.currentIndex = index
908
self.sendLine('221 %d %s article retrieved' % (index, id))
909
self.transport.write(head + '\r\n')
913
def _errHead(self, failure):
914
print 'HEAD failed: ', failure
915
self.sendLine('423 no such article number in this group')
918
def do_BODY(self, article):
919
defer = self.articleWork(article, 'BODY', self.factory.backend.bodyRequest)
921
defer.addCallbacks(self._gotBody, self._errBody)
924
def _gotBody(self, (index, id, body)):
925
if isinstance(body, types.StringType):
928
"Returning the article as a string from `articleRequest' "
929
"is deprecated. Return a file-like object instead."
931
body = StringIO.StringIO(body)
932
self.currentIndex = index
933
self.sendLine('221 %d %s article retrieved' % (index, id))
934
s = basic.FileSender()
935
d = s.beginFileTransfer(body, self.transport, self.transformChunk)
936
d.addCallback(self.finishedFileTransfer)
938
def _errBody(self, failure):
939
print 'BODY failed: ', failure
940
self.sendLine('423 no such article number in this group')
943
# NEXT and LAST are just STATs that increment currentIndex first.
944
# Accordingly, use the STAT callbacks.
946
i = self.currentIndex + 1
947
defer = self.factory.backend.articleRequest(self.currentGroup, i)
948
defer.addCallbacks(self._gotStat, self._errStat)
952
i = self.currentIndex - 1
953
defer = self.factory.backend.articleRequest(self.currentGroup, i)
954
defer.addCallbacks(self._gotStat, self._errStat)
957
def do_MODE(self, cmd):
958
cmd = cmd.strip().upper()
960
self.servingSlave = 0
961
self.sendLine('200 Hello, you can post')
962
elif cmd == 'STREAM':
963
self.sendLine('500 Command not understood')
965
# This is not a mistake
966
self.sendLine('500 Command not understood')
970
self.sendLine('205 goodbye')
971
self.transport.loseConnection()
975
self.sendLine('100 help text follows')
976
self.sendLine('Read the RFC.')
981
self.sendLine('202 slave status noted')
982
self.servingeSlave = 1
985
def do_XPATH(self, article):
986
# XPATH is a silly thing to have. No client has the right to ask
987
# for this piece of information from me, and so that is what I'll
989
self.sendLine('502 access restriction or permission denied')
992
def do_XINDEX(self, article):
993
# XINDEX is another silly command. The RFC suggests it be relegated
994
# to the history books, and who am I to disagree?
995
self.sendLine('502 access restriction or permission denied')
998
def do_XROVER(self, range = None):
999
self.do_XHDR(self, 'References', range)
1002
def do_IHAVE(self, id):
1003
self.factory.backend.articleExistsRequest(id).addCallback(self._foundArticle)
1006
def _foundArticle(self, result):
1008
self.sendLine('437 article rejected - do not try again')
1010
self.sendLine('335 send article to be transferred. End with <CR-LF>.<CR-LF>')
1011
self.inputHandler = self._handleIHAVE
1015
def _handleIHAVE(self, line):
1017
self.inputHandler = None
1018
self.factory.backend.postRequest(
1020
).addCallbacks(self._gotIHAVE, self._errIHAVE)
1024
if line.startswith('.'):
1026
self.message = self.message + line + '\r\n'
1029
def _gotIHAVE(self, result):
1030
self.sendLine('235 article transferred ok')
1033
def _errIHAVE(self, failure):
1034
print 'IHAVE failed: ', failure
1035
self.sendLine('436 transfer failed - try again later')
1038
class UsenetClientProtocol(NNTPClient):
1040
A client that connects to an NNTP server and asks for articles new
1041
since a certain time.
1044
def __init__(self, groups, date, storage):
1046
Fetch all new articles from the given groups since the
1047
given date and dump them into the given storage. groups
1048
is a list of group names. date is an integer or floating
1049
point representing seconds since the epoch (GMT). storage is
1050
any object that implements the NewsStorage interface.
1052
NNTPClient.__init__(self)
1053
self.groups, self.date, self.storage = groups, date, storage
1056
def connectionMade(self):
1057
NNTPClient.connectionMade(self)
1058
log.msg("Initiating update with remote host: " + str(self.transport.getPeer()))
1060
self.fetchNewNews(self.groups, self.date, '')
1063
def articleExists(self, exists, article):
1065
self.fetchArticle(article)
1067
self.count = self.count - 1
1068
self.disregard = self.disregard + 1
1071
def gotNewNews(self, news):
1073
self.count = len(news)
1074
log.msg("Transfering " + str(self.count) + " articles from remote host: " + str(self.transport.getPeer()))
1076
self.storage.articleExistsRequest(i).addCallback(self.articleExists, i)
1079
def getNewNewsFailed(self, reason):
1080
log.msg("Updated failed (" + reason + ") with remote host: " + str(self.transport.getPeer()))
1084
def gotArticle(self, article):
1085
self.storage.postRequest(article)
1086
self.count = self.count - 1
1088
log.msg("Completed update with remote host: " + str(self.transport.getPeer()))
1090
log.msg("Disregarded %d articles." % (self.disregard,))
1091
self.factory.updateChecks(self.transport.getPeer())