ruby-net-nntp 0.2.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +145 -128
- data/CHANGELOG.debian +5 -0
- data/Manifest.txt +37 -0
- data/README.txt +69 -0
- data/Rakefile +28 -0
- data/build-stamp +0 -0
- data/lib/net/nntp.rb +261 -327
- data/lib/net/nntp/request.rb +840 -0
- data/lib/net/nntp/response.rb +731 -0
- data/lib/net/nntp/version.rb +2 -1
- data/script/console +12 -0
- data/spec/net/nntp/request_spec.rb +771 -0
- data/spec/net/nntp/response_spec.rb +994 -0
- data/spec/net/nntp_spec.rb +634 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +28 -0
- data/spec/stories/all.rb +2 -0
- data/spec/stories/helper.rb +17 -0
- data/spec/stories/net/nntp.rb +7 -0
- data/spec/stories/net/nntp.story +12 -0
- data/spec/stories/steps/nntp.rb +18 -0
- data/tasks/ann.rake +81 -0
- data/tasks/bones.rake +21 -0
- data/tasks/gem.rake +126 -0
- data/tasks/git.rake +41 -0
- data/tasks/manifest.rake +49 -0
- data/tasks/mercurial.rake +6 -0
- data/tasks/notes.rake +28 -0
- data/tasks/pallet.rake +17 -0
- data/tasks/post_load.rake +39 -0
- data/tasks/rdoc.rake +51 -0
- data/tasks/rubyforge.rake +57 -0
- data/tasks/setup.rb +268 -0
- data/tasks/spec.rake +55 -0
- data/tasks/svn.rake +48 -0
- data/tasks/test.rake +38 -0
- metadata +57 -33
- data/README +0 -46
- data/lib/net/nntp/article.rb +0 -188
- data/lib/net/nntp/group.rb +0 -83
- data/test/functional/test_nntp.rb +0 -288
- data/test/mock/mock_socket.rb +0 -359
- data/test/unit/test_nntp_article.rb +0 -98
- data/test/unit/test_nntp_group.rb +0 -60
@@ -0,0 +1,731 @@
|
|
1
|
+
module Net
|
2
|
+
class NNTP
|
3
|
+
|
4
|
+
class GenericError < StandardError; end
|
5
|
+
class ProtocolError < RuntimeError; end
|
6
|
+
# Simple header class to parse headers received from LIST OVERVIEW.FMT
|
7
|
+
#
|
8
|
+
# See Net::NNTP::ListInformationFollows
|
9
|
+
class FormatHeader
|
10
|
+
attr_reader :name, :full
|
11
|
+
def initialize(line)
|
12
|
+
if match = line.match(/^(\S+):(full)?/m)
|
13
|
+
@name = match[1].downcase
|
14
|
+
@full = ('full' == match[2])
|
15
|
+
elsif match = line.match(/^:(\S+)$/m)
|
16
|
+
@name = match[1].downcase
|
17
|
+
@full = false
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Prepares the basic functionality for responses that have a body (directly following the status line).
|
23
|
+
#
|
24
|
+
# Every class that includes this can use strings or objects responding to +readline+ to retrieve the body.
|
25
|
+
module BodyBaseResponse
|
26
|
+
# Sets the body attribute.
|
27
|
+
#
|
28
|
+
# The parameter +body+ can be a string or an object responding to +readline+. Reading will be halted at a line
|
29
|
+
# with a single dot followed by <CRLF> or <LF>, or when an EOFError occurs. The result will be stored in the
|
30
|
+
# internal attribute @raw. Subsequently, parse_body will be called.
|
31
|
+
def body=(body)
|
32
|
+
if body.respond_to? :readline
|
33
|
+
begin
|
34
|
+
@raw = ''
|
35
|
+
loop do
|
36
|
+
line = body.readline
|
37
|
+
@raw << line
|
38
|
+
break if line =~/\A\.\r?\n\z/m
|
39
|
+
end
|
40
|
+
rescue EOFError => e
|
41
|
+
end
|
42
|
+
else
|
43
|
+
@raw = body
|
44
|
+
end
|
45
|
+
parse_body
|
46
|
+
end
|
47
|
+
# Returns the body attribute.
|
48
|
+
#
|
49
|
+
# If a block is given, the results will be sent to body= before returning the attribute.
|
50
|
+
def body
|
51
|
+
self.body = yield if block_given?
|
52
|
+
@body
|
53
|
+
end
|
54
|
+
# Is called from body= and should be used to set the @body attribute transforming the contents of the @raw
|
55
|
+
# attribute.
|
56
|
+
def parse_body
|
57
|
+
@body = @raw.dup
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Base response class. Parent of all responses. DO NOT instantiate directly unless you're ready to handle the nitty
|
62
|
+
# gritty yourself.
|
63
|
+
#
|
64
|
+
# Class Tree:
|
65
|
+
# * Net::NNTP::Response
|
66
|
+
# * InformationResponse
|
67
|
+
# * HelpResponse
|
68
|
+
# * CapabilityList
|
69
|
+
# * DateResponse
|
70
|
+
# * OKResponse
|
71
|
+
# * PostingAllowed
|
72
|
+
# * PostingProhibited
|
73
|
+
# * ConnectionClosing
|
74
|
+
# * GroupSelected
|
75
|
+
# * ListInformationFollows
|
76
|
+
# * ArticleResponse
|
77
|
+
# * HeaderResponse
|
78
|
+
# * BodyResponse
|
79
|
+
# * ArticleSelected
|
80
|
+
# * OverviewInformation
|
81
|
+
# * HdrResponse
|
82
|
+
# * NewnewsResponse
|
83
|
+
# * NewgroupsResponse
|
84
|
+
# * TransferOK
|
85
|
+
# * ArticleReceived
|
86
|
+
# * AuthenticationAccepted
|
87
|
+
class Response
|
88
|
+
attr_reader :code, :message
|
89
|
+
# * Parameter +code+ is the code returned from the server, should be a string representing the first three bytes
|
90
|
+
# (digits).
|
91
|
+
# * Parameter +message+ is the message following the code on the status line without the separating whitespace.
|
92
|
+
# * Parameter +generic+ gives the subclasses the opportunity to denote if the response is a generic response
|
93
|
+
# according to the RFC, and therefor valid following any request, or if the response is a response that MUST
|
94
|
+
# follow a certain request. See Request for validating responses.
|
95
|
+
# * Parameter +multiline+ gives the subclasses the opportunity to denote if the response is a multiline response
|
96
|
+
# according to RFC, and if a body is to be expected.
|
97
|
+
def initialize(request, code, message, generic=false, multiline=false)
|
98
|
+
@code = code
|
99
|
+
@message = message
|
100
|
+
@generic=generic
|
101
|
+
@multiline = multiline
|
102
|
+
@request = request
|
103
|
+
end
|
104
|
+
|
105
|
+
# Returns the value of the *generic* attribute, set by the generic parameter to new.
|
106
|
+
def generic?
|
107
|
+
@generic
|
108
|
+
end
|
109
|
+
|
110
|
+
# Returns the value of the *multiline* attribute, set by the multiline parameter to new.
|
111
|
+
def multiline?
|
112
|
+
@multiline
|
113
|
+
end
|
114
|
+
|
115
|
+
# Returns *true* if the response is a multiline response and has a body set.
|
116
|
+
def has_body?
|
117
|
+
multiline? && body
|
118
|
+
end
|
119
|
+
|
120
|
+
# Returns the body in subclasses; returns nil in Response.
|
121
|
+
def body
|
122
|
+
nil
|
123
|
+
end
|
124
|
+
|
125
|
+
def ==(other)
|
126
|
+
if other.is_a? Response
|
127
|
+
self.class == other.class && self.code == other.code && self.message == other.message && self.multiline? == other.multiline? && self.generic? == other.generic? && self.body == other.body
|
128
|
+
else
|
129
|
+
return false
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def needs_article?
|
134
|
+
false
|
135
|
+
end
|
136
|
+
|
137
|
+
def force_close?
|
138
|
+
false
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Categorizes the responses starting with a code of 1xx as information responses.
|
143
|
+
#
|
144
|
+
# All InformationResponse subclasses do have a body, so BodyBaseResponse is included here.
|
145
|
+
#
|
146
|
+
# == Subclasses
|
147
|
+
# * HelpResponse
|
148
|
+
# * CapabilityList
|
149
|
+
# * DateResponse
|
150
|
+
class InformationResponse < Response
|
151
|
+
include BodyBaseResponse
|
152
|
+
end
|
153
|
+
|
154
|
+
# Code: 100
|
155
|
+
#
|
156
|
+
# Response to a Help request.
|
157
|
+
class HelpResponse < InformationResponse
|
158
|
+
attr_reader :raw
|
159
|
+
def initialize(request, code, message)
|
160
|
+
super request, code, message, false, true
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Code: 101
|
165
|
+
#
|
166
|
+
# Response to a Capabilities request.
|
167
|
+
#
|
168
|
+
class CapabilityList < InformationResponse
|
169
|
+
attr_reader :raw
|
170
|
+
|
171
|
+
def initialize(request, code, message)
|
172
|
+
@capabilities = {}
|
173
|
+
super request, code, message, false, true
|
174
|
+
end
|
175
|
+
|
176
|
+
# Returns the capabilities as a hash. Key is the first word (capability) downcased, value either a single string
|
177
|
+
# or a array of strings, or the value 'true'.
|
178
|
+
#
|
179
|
+
def capabilities
|
180
|
+
@capabilities.dup
|
181
|
+
end
|
182
|
+
def parse_body # :nodoc: internal use only
|
183
|
+
@raw.split(/\n/).each do |line|
|
184
|
+
break if line == '.'
|
185
|
+
key, *value = line.split(/\s+/)
|
186
|
+
value = (value.size <= 1 ? value[0]: value)
|
187
|
+
@capabilities[key.downcase]= value || true
|
188
|
+
end unless @raw.nil?
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
# Code: 111
|
193
|
+
#
|
194
|
+
# DATE response.
|
195
|
+
class DateResponse < InformationResponse
|
196
|
+
# Returns the date given as message from server.
|
197
|
+
def date
|
198
|
+
DateTime.parse(@message)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# Categorizes responses starting with a code of 2xx as OKResponses
|
203
|
+
# Codes: 2xx
|
204
|
+
#
|
205
|
+
# DO NOT instantiate, use the following
|
206
|
+
#
|
207
|
+
# == Subclasses
|
208
|
+
# * PostingAllowed
|
209
|
+
# * PostingProhibited
|
210
|
+
# * ConnectionClosing
|
211
|
+
# * GroupSelected
|
212
|
+
# * ListInformationFollows
|
213
|
+
# * ArticleResponse
|
214
|
+
# * HeaderResponse
|
215
|
+
# * BodyResponse
|
216
|
+
# * ArticleSelected
|
217
|
+
# * OverviewInformation
|
218
|
+
# * HdrResponse
|
219
|
+
# * NewnewsResponse
|
220
|
+
# * NewgroupsResponse
|
221
|
+
# * TransferOK
|
222
|
+
# * ArticleReceived
|
223
|
+
# * AuthenticationAccepted
|
224
|
+
class OKResponse < Response
|
225
|
+
end
|
226
|
+
|
227
|
+
# Code: 200
|
228
|
+
# Posting allowed response. See NNTP#connect, Modereader.
|
229
|
+
class PostingAllowed < OKResponse
|
230
|
+
end
|
231
|
+
|
232
|
+
# Code: 201
|
233
|
+
# Posting prohibited response. See NNTP#connect, Modereader, Post.
|
234
|
+
class PostingProhibited < OKResponse
|
235
|
+
end
|
236
|
+
|
237
|
+
# Code: 205
|
238
|
+
# Connection closing response. See NNTP#quit.
|
239
|
+
class ConnectionClosing < OKResponse
|
240
|
+
def force_close?
|
241
|
+
true
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
# Code: 211
|
246
|
+
#
|
247
|
+
# GroupSelected response.
|
248
|
+
#
|
249
|
+
# See Group, Listgroup.
|
250
|
+
class GroupSelected < OKResponse
|
251
|
+
attr_reader :group, :list
|
252
|
+
attr_reader :raw
|
253
|
+
include BodyBaseResponse
|
254
|
+
def initialize(request, code, message)
|
255
|
+
@group = OpenStruct.new
|
256
|
+
tokens = message.split
|
257
|
+
raise ProtocolError, "Malformed group message: #{message}" unless tokens.size >= 4
|
258
|
+
@group.number, @group.low, @group.high, @group.name = tokens[0].to_i, tokens[1].to_i, tokens[2].to_i, tokens[3]
|
259
|
+
@body = nil
|
260
|
+
super request, code, message, false, Listgroup === request
|
261
|
+
end
|
262
|
+
def parse_body
|
263
|
+
@body=@raw
|
264
|
+
@list = @raw.gsub(/\r?\n\.\r?\n/, '').split(/\r?\n/)
|
265
|
+
end
|
266
|
+
|
267
|
+
end
|
268
|
+
|
269
|
+
class GroupListResponse < OKResponse
|
270
|
+
include BodyBaseResponse
|
271
|
+
attr_reader :list, :raw
|
272
|
+
|
273
|
+
def initialize(request, code, message)
|
274
|
+
@list = []
|
275
|
+
super request, code, message, false, true
|
276
|
+
end
|
277
|
+
|
278
|
+
end
|
279
|
+
|
280
|
+
# Code: 215
|
281
|
+
class ListInformationFollows < GroupListResponse
|
282
|
+
def parse_body
|
283
|
+
case @request.keyword
|
284
|
+
when nil, 'ACTIVE'
|
285
|
+
@raw.split(/\r?\n/).each do |line|
|
286
|
+
break if line == '.'
|
287
|
+
name, low, high, status = line.split(/\s+/)
|
288
|
+
@list << OpenStruct.new(:name => name, :low => low.to_i, :high => high.to_i, :status => status.strip)
|
289
|
+
end unless @raw.nil?
|
290
|
+
when 'ACTIVE.TIMES'
|
291
|
+
@raw.split(/\r?\n/).each do |line|
|
292
|
+
break if line == '.'
|
293
|
+
name, epoch, creator = line.split(/\s+/)
|
294
|
+
@list << OpenStruct.new(:name => name, :epoch => epoch, :creator => creator)
|
295
|
+
end unless @raw.nil?
|
296
|
+
when 'DISTRIB.PATS'
|
297
|
+
@raw.split(/\r?\n/).each do |line|
|
298
|
+
break if line == '.'
|
299
|
+
priority, pattern, distribution = line.split(/:/)
|
300
|
+
@list << OpenStruct.new(:priority => priority.to_i, :pattern => pattern, :distribution => distribution)
|
301
|
+
end unless @raw.nil?
|
302
|
+
when 'HEADERS'
|
303
|
+
@list = @raw.gsub(/\r?\n\.\r?\n/, '').split(/\r?\n/) unless @raw.nil?
|
304
|
+
when 'NEWSGROUPS'
|
305
|
+
@raw.split(/\r?\n/).each do |line|
|
306
|
+
break if line == '.'
|
307
|
+
if match = line.match(/\A(\S+)\s+(.*)\z/)
|
308
|
+
name, desc = match.captures
|
309
|
+
@list << OpenStruct.new(:name => name, :desc => desc)
|
310
|
+
else
|
311
|
+
raise ProtocolError,'List format mismatch'
|
312
|
+
end
|
313
|
+
end unless @raw.nil?
|
314
|
+
when 'OVERVIEW.FMT'
|
315
|
+
@raw.split(/\r?\n/).each do |line|
|
316
|
+
break if line == '.'
|
317
|
+
@list << FormatHeader.new(line)
|
318
|
+
end unless @raw.nil?
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
end
|
323
|
+
|
324
|
+
class ArticleBaseResponse < OKResponse
|
325
|
+
attr_reader :number, :message_id, :raw
|
326
|
+
def initialize(request, code, message, generic=false, multiline=true)
|
327
|
+
number, id = message.split(/\s+/)
|
328
|
+
@number = number.to_i
|
329
|
+
@message_id = id
|
330
|
+
super request, code, message, generic, multiline
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
# Code: 220
|
335
|
+
class ArticleResponse < ArticleBaseResponse
|
336
|
+
include BodyBaseResponse
|
337
|
+
def parse_body
|
338
|
+
unless @article
|
339
|
+
r = @raw.dup
|
340
|
+
r.gsub!(/^\./,"")
|
341
|
+
@article = TMail::Mail.parse(r)
|
342
|
+
end
|
343
|
+
end
|
344
|
+
def article
|
345
|
+
@article.dup
|
346
|
+
end
|
347
|
+
def [](header)
|
348
|
+
@article[header].to_s
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
# Code: 221
|
353
|
+
#
|
354
|
+
# CAVEAT:: according to RFC 2980, an XHDR request will trigger a 221 response, but with a TOTALLY different format.
|
355
|
+
# THANKS, IDIOTS (fucking on a heap of barbed wire ...)
|
356
|
+
class HeaderResponse < ArticleBaseResponse
|
357
|
+
include BodyBaseResponse
|
358
|
+
attr_reader :list
|
359
|
+
def parse_body
|
360
|
+
unless @article
|
361
|
+
if Xhdr === @request
|
362
|
+
@xhdr = true
|
363
|
+
@list = {}
|
364
|
+
@raw.split(/\r?\n/).each do |line|
|
365
|
+
number, value = line.split(/\t/)
|
366
|
+
@list[number.to_i] = value
|
367
|
+
end
|
368
|
+
elsif Head === @request
|
369
|
+
@xhdr = false
|
370
|
+
begin
|
371
|
+
@article = TMail::Mail.new
|
372
|
+
@article.__send__(:parse_header, StringIO.new(@raw))
|
373
|
+
rescue TMail::SyntaxError
|
374
|
+
end
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
378
|
+
|
379
|
+
def [](header)
|
380
|
+
return @article[header].to_s if @article
|
381
|
+
nil
|
382
|
+
end
|
383
|
+
def header
|
384
|
+
return @article.header if @article
|
385
|
+
nil
|
386
|
+
end
|
387
|
+
|
388
|
+
end
|
389
|
+
|
390
|
+
|
391
|
+
# Code: 222
|
392
|
+
class BodyResponse < ArticleBaseResponse
|
393
|
+
include BodyBaseResponse
|
394
|
+
end
|
395
|
+
|
396
|
+
# Code: 223
|
397
|
+
class ArticleSelected < ArticleBaseResponse
|
398
|
+
def initialize(request, code, message)
|
399
|
+
super request, code, message, false, false
|
400
|
+
end
|
401
|
+
end
|
402
|
+
|
403
|
+
# Code: 224
|
404
|
+
class OverviewInformation < ArticleBaseResponse
|
405
|
+
include BodyBaseResponse
|
406
|
+
attr_reader :subject, :from, :date, :references, :bytes, :lines, :optional_headers
|
407
|
+
def parse_body
|
408
|
+
raw = @raw.gsub(/\r\n\.\r\n/, '')
|
409
|
+
number, subject, from, date, message_id, references, bytes, lines, *rest =
|
410
|
+
raw.strip.split(/\t/)
|
411
|
+
@number = number.to_i
|
412
|
+
@subject = subject
|
413
|
+
@from = from
|
414
|
+
@date = DateTime.parse(date)
|
415
|
+
@message_id = message_id
|
416
|
+
@references = references.split(/\s/)
|
417
|
+
@bytes = bytes.to_i
|
418
|
+
@lines = lines.to_i
|
419
|
+
@optional_headers = {}
|
420
|
+
if rest
|
421
|
+
rest.each do |line|
|
422
|
+
header, value = line.split(/:\s+/)
|
423
|
+
optional_headers[header.downcase] = value
|
424
|
+
end
|
425
|
+
end
|
426
|
+
end
|
427
|
+
end
|
428
|
+
|
429
|
+
# Code : 225
|
430
|
+
class HdrResponse < ArticleBaseResponse
|
431
|
+
include BodyBaseResponse
|
432
|
+
attr_reader :list
|
433
|
+
def parse_body
|
434
|
+
@list = {}
|
435
|
+
@raw.split(/\r?\n/).each do |line|
|
436
|
+
number, value = line.split(/\t/)
|
437
|
+
@list[number.to_i] = value
|
438
|
+
end
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|
442
|
+
# Code: 230
|
443
|
+
class NewnewsResponse < ArticleBaseResponse
|
444
|
+
include BodyBaseResponse
|
445
|
+
attr_reader :list
|
446
|
+
def parse_body
|
447
|
+
@list = @raw.split(/\r?\n/)
|
448
|
+
end
|
449
|
+
end
|
450
|
+
|
451
|
+
# Code: 231
|
452
|
+
class NewgroupsResponse < GroupListResponse
|
453
|
+
def parse_body
|
454
|
+
@raw.split(/\r?\n/).each do |line|
|
455
|
+
break if line == '.'
|
456
|
+
name, low, high, status = line.split(/\s+/)
|
457
|
+
@list << OpenStruct.new(:name => name, :low => low.to_i, :high => high.to_i, :status => status.strip)
|
458
|
+
end unless @raw.nil?
|
459
|
+
end
|
460
|
+
|
461
|
+
end
|
462
|
+
|
463
|
+
# Code: 235
|
464
|
+
class TransferOK < OKResponse
|
465
|
+
end
|
466
|
+
|
467
|
+
# Code: 240
|
468
|
+
class ArticleReceived < OKResponse
|
469
|
+
end
|
470
|
+
|
471
|
+
# Code: 281
|
472
|
+
class AuthenticationAccepted < OKResponse
|
473
|
+
end
|
474
|
+
|
475
|
+
class ContinueResponse < Response
|
476
|
+
end
|
477
|
+
|
478
|
+
# Code: 335
|
479
|
+
class TransferArticle < ContinueResponse
|
480
|
+
def needs_article?
|
481
|
+
true
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
485
|
+
# Code: 340
|
486
|
+
class PostArticle < ContinueResponse
|
487
|
+
def needs_article?
|
488
|
+
true
|
489
|
+
end
|
490
|
+
end
|
491
|
+
|
492
|
+
# Code: 381
|
493
|
+
class PasswordRequired < ContinueResponse
|
494
|
+
end
|
495
|
+
|
496
|
+
|
497
|
+
# Codes: 4xx
|
498
|
+
# == Subclasses:
|
499
|
+
# * TemporarilyUnavailable (400)
|
500
|
+
# * InternalFault (403)
|
501
|
+
# * GroupUnknown (411)
|
502
|
+
# * NoGroupSelected (412)
|
503
|
+
# * InvalidArticle (420)
|
504
|
+
# * NoNextArticle (421)
|
505
|
+
# * NoPreviousArticle (422)
|
506
|
+
# * InvalidNumberOrRange (423)
|
507
|
+
# * NoSuchMessageid (430)
|
508
|
+
# * ArticleNotWanted (435)
|
509
|
+
# * TransferNotPossible (436)
|
510
|
+
# * TransferRejected (437)
|
511
|
+
# * PostingNotPermitted (440)
|
512
|
+
# * PostingFailed (441)
|
513
|
+
# * AuthenticationRequired (480)
|
514
|
+
# * AuthenticationFailed (481)
|
515
|
+
# * AuthenticationOutOfSequence (482)
|
516
|
+
# * PrivacyRequired (483)
|
517
|
+
class RetryResponse < Response
|
518
|
+
end
|
519
|
+
|
520
|
+
# Code: 400
|
521
|
+
class TemporarilyUnavailable < RetryResponse
|
522
|
+
def initialize(request, code, message)
|
523
|
+
super request, code, message, true
|
524
|
+
end
|
525
|
+
end
|
526
|
+
|
527
|
+
# Code: 403
|
528
|
+
class InternalFault < RetryResponse
|
529
|
+
def initialize(request, code, message)
|
530
|
+
super request, code, message, true
|
531
|
+
end
|
532
|
+
end
|
533
|
+
|
534
|
+
# Code: 411
|
535
|
+
class GroupUnknown < RetryResponse
|
536
|
+
end
|
537
|
+
|
538
|
+
# Code: 412
|
539
|
+
class NoGroupSelected < RetryResponse
|
540
|
+
end
|
541
|
+
|
542
|
+
# Code: 420
|
543
|
+
class InvalidArticle < RetryResponse
|
544
|
+
end
|
545
|
+
|
546
|
+
# Code: 421
|
547
|
+
class NoPreviousArticle < RetryResponse
|
548
|
+
end
|
549
|
+
|
550
|
+
# Code: 422
|
551
|
+
class NoNextArticle < RetryResponse
|
552
|
+
end
|
553
|
+
|
554
|
+
# Code: 423
|
555
|
+
class InvalidNumberOrRange < RetryResponse
|
556
|
+
end
|
557
|
+
|
558
|
+
# Code: 430
|
559
|
+
class NoSuchMessageid < RetryResponse
|
560
|
+
end
|
561
|
+
|
562
|
+
# Code: 435
|
563
|
+
class ArticleNotWanted < RetryResponse
|
564
|
+
end
|
565
|
+
|
566
|
+
# Code: 436
|
567
|
+
class TransferNotPossible < RetryResponse
|
568
|
+
end
|
569
|
+
|
570
|
+
# Code: 437
|
571
|
+
class TransferRejected < RetryResponse
|
572
|
+
end
|
573
|
+
|
574
|
+
# Code: 440
|
575
|
+
class PostingNotPermitted < RetryResponse
|
576
|
+
end
|
577
|
+
|
578
|
+
# Code: 441
|
579
|
+
class PostingFailed < RetryResponse
|
580
|
+
end
|
581
|
+
|
582
|
+
# Code: 480
|
583
|
+
class AuthenticationRequired < RetryResponse
|
584
|
+
def initialize(request, code, message)
|
585
|
+
super request, code, message, true
|
586
|
+
end
|
587
|
+
end
|
588
|
+
|
589
|
+
# Code: 481
|
590
|
+
class AuthenticationFailed < RetryResponse
|
591
|
+
end
|
592
|
+
|
593
|
+
# Code: 482
|
594
|
+
class AuthenticationOutOfSequence < RetryResponse
|
595
|
+
end
|
596
|
+
|
597
|
+
# Code: 483
|
598
|
+
class PrivacyRequired < RetryResponse
|
599
|
+
def initialize(request, code, message)
|
600
|
+
super request, code, message, true
|
601
|
+
end
|
602
|
+
end
|
603
|
+
|
604
|
+
# Codes: 5xx
|
605
|
+
# == Subclasses:
|
606
|
+
# * NotImplemented
|
607
|
+
# * SyntaxError
|
608
|
+
# * PermanentlyUnavailable
|
609
|
+
# * FeatureNotProvided
|
610
|
+
# * EncodingError
|
611
|
+
class FailResponse < Response
|
612
|
+
end
|
613
|
+
|
614
|
+
# Code: 500
|
615
|
+
class NotImplemented < FailResponse
|
616
|
+
def initialize(request, code, message)
|
617
|
+
super request, code, message, true
|
618
|
+
end
|
619
|
+
end
|
620
|
+
|
621
|
+
# Code: 501
|
622
|
+
class SyntaxError < FailResponse
|
623
|
+
def initialize(request, code, message)
|
624
|
+
super request, code, message, true
|
625
|
+
end
|
626
|
+
end
|
627
|
+
|
628
|
+
# Code: 502
|
629
|
+
class PermanentlyUnavailable < FailResponse
|
630
|
+
def initialize(request, code, message)
|
631
|
+
super request, code, message, true
|
632
|
+
end
|
633
|
+
end
|
634
|
+
|
635
|
+
# Code: 503
|
636
|
+
class FeatureNotProvided < FailResponse
|
637
|
+
def initialize(request, code, message)
|
638
|
+
super request, code, message, true
|
639
|
+
end
|
640
|
+
end
|
641
|
+
|
642
|
+
# Code: 500
|
643
|
+
class EncodingError < FailResponse
|
644
|
+
def initialize(request, code, message)
|
645
|
+
super request, code, message, true
|
646
|
+
end
|
647
|
+
end
|
648
|
+
|
649
|
+
class UnknownResponse < Response
|
650
|
+
end
|
651
|
+
RESPONSES = {
|
652
|
+
'100' => HelpResponse,
|
653
|
+
'101' => CapabilityList,
|
654
|
+
'111' => DateResponse,
|
655
|
+
|
656
|
+
'200' => PostingAllowed,
|
657
|
+
'201' => PostingProhibited,
|
658
|
+
'205' => ConnectionClosing,
|
659
|
+
'211' => GroupSelected,
|
660
|
+
'215' => ListInformationFollows,
|
661
|
+
'220' => ArticleResponse,
|
662
|
+
'221' => HeaderResponse,
|
663
|
+
'222' => BodyResponse,
|
664
|
+
'223' => ArticleSelected,
|
665
|
+
'224' => OverviewInformation,
|
666
|
+
'225' => HdrResponse,
|
667
|
+
'230' => NewnewsResponse,
|
668
|
+
'231' => NewgroupsResponse,
|
669
|
+
'235' => TransferOK,
|
670
|
+
'240' => ArticleReceived,
|
671
|
+
'281' => AuthenticationAccepted,
|
672
|
+
|
673
|
+
'335' => TransferArticle,
|
674
|
+
'340' => PostArticle,
|
675
|
+
'381' => PasswordRequired,
|
676
|
+
|
677
|
+
'400' => TemporarilyUnavailable,
|
678
|
+
'403' => InternalFault,
|
679
|
+
'411' => GroupUnknown,
|
680
|
+
'412' => NoGroupSelected,
|
681
|
+
'420' => InvalidArticle,
|
682
|
+
'421' => NoNextArticle,
|
683
|
+
'422' => NoPreviousArticle,
|
684
|
+
'423' => InvalidNumberOrRange,
|
685
|
+
'430' => NoSuchMessageid,
|
686
|
+
'435' => ArticleNotWanted,
|
687
|
+
'436' => TransferNotPossible,
|
688
|
+
'437' => TransferRejected,
|
689
|
+
'440' => PostingNotPermitted,
|
690
|
+
'441' => PostingFailed,
|
691
|
+
'480' => AuthenticationRequired,
|
692
|
+
'481' => AuthenticationFailed,
|
693
|
+
'482' => AuthenticationOutOfSequence,
|
694
|
+
'483' => PrivacyRequired,
|
695
|
+
|
696
|
+
'500' => NotImplemented,
|
697
|
+
'501' => SyntaxError,
|
698
|
+
'502' => PermanentlyUnavailable,
|
699
|
+
'503' => FeatureNotProvided,
|
700
|
+
'504' => EncodingError
|
701
|
+
}
|
702
|
+
CLASSES = {
|
703
|
+
'1' => InformationResponse,
|
704
|
+
'2' => OKResponse,
|
705
|
+
'3' => ContinueResponse,
|
706
|
+
'4' => RetryResponse,
|
707
|
+
'5' => FailResponse
|
708
|
+
}
|
709
|
+
|
710
|
+
class Response
|
711
|
+
attr_accessor :request
|
712
|
+
class << self
|
713
|
+
def create(request, statusline)
|
714
|
+
match = statusline.strip.match(/\A(\d{3})\s?(.*)\z/) or raise ProtocolError, "Unknown Response"
|
715
|
+
Net::NNTP.logger.debug("Response#create: request = #{request}")
|
716
|
+
klass = class_from_code(match[1])
|
717
|
+
Net::NNTP.logger.debug("Response#create: class = #{klass}")
|
718
|
+
this = klass.new(request, *match.captures)
|
719
|
+
end
|
720
|
+
def class_from_code(code)
|
721
|
+
Net::NNTP::RESPONSES[code] or
|
722
|
+
Net::NNTP::CLASSES[code[0,1]] or
|
723
|
+
Net::NNTP::UnknownResponse
|
724
|
+
end
|
725
|
+
|
726
|
+
end
|
727
|
+
end
|
728
|
+
|
729
|
+
end
|
730
|
+
|
731
|
+
end
|