nntp-client 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +51 -0
- data/Rakefile +1 -0
- data/lib/NNTPClient.rb +120 -0
- data/lib/article.rb +7 -0
- data/lib/nntp.rb +49 -0
- data/lib/nntp/connection.rb +126 -0
- data/lib/nntp/group.rb +3 -0
- data/lib/nntp/message.rb +3 -0
- data/lib/nntp/session.rb +139 -0
- data/lib/nntp/ssl_connection.rb +22 -0
- data/lib/nntp/status.rb +7 -0
- data/lib/nntp/version.rb +3 -0
- data/nntp-client.gemspec +22 -0
- data/spec/Status_spec.rb +13 -0
- data/spec/connection_spec.rb +26 -0
- data/spec/nntp_spec.rb +73 -0
- data/spec/session_spec.rb +103 -0
- data/spec/spec_helper.rb +16 -0
- metadata +90 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Michael Westbom
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# NNTPClient
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'NNTPClient'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install NNTPClient
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
### Connection
|
22
|
+
Two different methods can be used to connect to a usenet server:
|
23
|
+
|
24
|
+
1. First, by supplying a URL and a port number as hash values:
|
25
|
+
```ruby
|
26
|
+
nntp = NNTPClient.new(:url => 'nntp.example.org', :port => 119)
|
27
|
+
```
|
28
|
+
An optional `:socket_factory` value can be included if you'd with for something other than TCPSocket to be used.
|
29
|
+
Please note that the signature of `::new` must match `TCPSocket::new`'s signature.
|
30
|
+
|
31
|
+
2. By supplying an existing socket:
|
32
|
+
```ruby
|
33
|
+
my_socket = TCPSocket.new('nntp.example.org', 119)
|
34
|
+
nntp = NNTPClient.new(:socket => my_socket)
|
35
|
+
```
|
36
|
+
|
37
|
+
### Listing Newsgroups
|
38
|
+
Upon connecting to a server, a list of valid newsgroups may be retrieved as such:
|
39
|
+
```ruby
|
40
|
+
groups = nntp.groups
|
41
|
+
```
|
42
|
+
|
43
|
+
The first time `#groups` is called, it retrieves the list of groups from the server. Subsequent calls return an instance variable.
|
44
|
+
|
45
|
+
## Contributing
|
46
|
+
|
47
|
+
1. Fork it
|
48
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
49
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
50
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
51
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/lib/NNTPClient.rb
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require_relative 'group'
|
3
|
+
require_relative 'article'
|
4
|
+
require "NNTPClient/version"
|
5
|
+
|
6
|
+
class NNTPClient
|
7
|
+
attr_reader :socket, :status, :current_group
|
8
|
+
|
9
|
+
def initialize(options = {})
|
10
|
+
@socket = open_socket(options)
|
11
|
+
init_attributes
|
12
|
+
end
|
13
|
+
|
14
|
+
def groups
|
15
|
+
@groups ||= list_groups
|
16
|
+
end
|
17
|
+
|
18
|
+
def group(group)
|
19
|
+
send_message "GROUP #{group}"
|
20
|
+
self.status = get_status
|
21
|
+
if status[:code] == 211
|
22
|
+
self.current_group = create_group(status)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def articles
|
27
|
+
@articles ||= fetch_articles
|
28
|
+
end
|
29
|
+
|
30
|
+
def auth(options = {})
|
31
|
+
send_message "AUTHINFO USER #{options[:user]}"
|
32
|
+
self.status = get_status
|
33
|
+
send_message "AUTHINFO PASS #{options[:pass]}"
|
34
|
+
self.status = get_status
|
35
|
+
if status[:code] == 281
|
36
|
+
true
|
37
|
+
else
|
38
|
+
false
|
39
|
+
end
|
40
|
+
end
|
41
|
+
private
|
42
|
+
def fetch_articles
|
43
|
+
send_message "XHDR Subject #{current_group.first}-"
|
44
|
+
self.status = get_status
|
45
|
+
return nil unless status[:code] == 221
|
46
|
+
get_data_block.map do |line|
|
47
|
+
article_id, article_subject = line.split(' ', 2)
|
48
|
+
NNTP::Article.new(article_id, article_subject)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def init_attributes
|
53
|
+
@current_group = nil
|
54
|
+
@status = nil
|
55
|
+
@groups = nil
|
56
|
+
@articles = nil
|
57
|
+
end
|
58
|
+
|
59
|
+
def create_group(status)
|
60
|
+
params = status[:params]
|
61
|
+
# TODO: This is ugly
|
62
|
+
NNTP::Group.new(*params[1..-1])
|
63
|
+
end
|
64
|
+
|
65
|
+
def open_socket(options)
|
66
|
+
options.fetch(:socket) {
|
67
|
+
url = options.fetch(:url) { raise ArgumentError, ':url is required' }
|
68
|
+
port = options.fetch(:port, 119)
|
69
|
+
socket_factory = options.fetch(:socket_factory) { TCPSocket }
|
70
|
+
socket_factory.new(url, port)
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
def list_groups
|
75
|
+
send_message "LIST"
|
76
|
+
status = get_status
|
77
|
+
return nil unless status[:code] == 215
|
78
|
+
|
79
|
+
get_data_block
|
80
|
+
end
|
81
|
+
|
82
|
+
def groups=(list=[])
|
83
|
+
@groups = list
|
84
|
+
end
|
85
|
+
|
86
|
+
def status=(status)
|
87
|
+
@status = status
|
88
|
+
end
|
89
|
+
|
90
|
+
def current_group=(group)
|
91
|
+
@current_group = group
|
92
|
+
end
|
93
|
+
|
94
|
+
def get_status
|
95
|
+
line = get_line
|
96
|
+
{
|
97
|
+
:code => line[0..2].to_i,
|
98
|
+
:message => line[3..-1].lstrip,
|
99
|
+
:params => line.split
|
100
|
+
}
|
101
|
+
end
|
102
|
+
|
103
|
+
def get_data_block
|
104
|
+
lines = []
|
105
|
+
current_line = get_line
|
106
|
+
until current_line == '.'
|
107
|
+
lines << current_line
|
108
|
+
current_line = get_line
|
109
|
+
end
|
110
|
+
lines
|
111
|
+
end
|
112
|
+
|
113
|
+
def get_line
|
114
|
+
socket.gets().chomp
|
115
|
+
end
|
116
|
+
|
117
|
+
def send_message(msg)
|
118
|
+
socket.print "#{msg}\r\n"
|
119
|
+
end
|
120
|
+
end
|
data/lib/article.rb
ADDED
data/lib/nntp.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
2
|
+
require "nntp/version"
|
3
|
+
require "nntp/session"
|
4
|
+
require "nntp/connection"
|
5
|
+
require 'nntp/ssl_connection'
|
6
|
+
|
7
|
+
# The main entry point for this module is the open method.
|
8
|
+
#
|
9
|
+
# ::open returns an object that is an active NNTP session.
|
10
|
+
# If a block is passed to it, the session object is made available
|
11
|
+
# therein.
|
12
|
+
module NNTP
|
13
|
+
# The main entrypoint to the module.
|
14
|
+
#
|
15
|
+
# @option options :url The URL of the NNTP server.
|
16
|
+
# @option options :port (119/563) The port number of the server.
|
17
|
+
# @option options [Boolean] :ssl (false) Connect via SSL?
|
18
|
+
# @option options [Hash] :auth Authentication credentials and style.
|
19
|
+
# @example Basic connection
|
20
|
+
# nntp = NNTP.open(
|
21
|
+
# :url => 'nntp.example.org',
|
22
|
+
# :auth => {:user => 'mike', :pass => 'soopersecret'}
|
23
|
+
# )
|
24
|
+
# nntp.quit
|
25
|
+
# @example Pass a block
|
26
|
+
# # Automatically closes connection
|
27
|
+
# NNTP.open(:url => 'nntp.example.org') do |nntp|
|
28
|
+
# nntp.group = 'alt.bin.foo'
|
29
|
+
# end
|
30
|
+
# @return [NNTP::Session] the active NNTP session
|
31
|
+
def self.open(options)
|
32
|
+
if options[:ssl]
|
33
|
+
connection = SSLConnection.new(options)
|
34
|
+
else
|
35
|
+
connection = Connection.new(options)
|
36
|
+
end
|
37
|
+
|
38
|
+
session = Session.new(:connection => connection)
|
39
|
+
|
40
|
+
session.auth(options[:auth]) if options[:auth]
|
41
|
+
|
42
|
+
if block_given?
|
43
|
+
yield session
|
44
|
+
session.quit
|
45
|
+
else
|
46
|
+
session
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'nntp/status'
|
3
|
+
|
4
|
+
module NNTP
|
5
|
+
# Handles communication with the NNTP server.
|
6
|
+
#
|
7
|
+
# Most communication with an NNTP server happens in a back-and-forth
|
8
|
+
# style.
|
9
|
+
#
|
10
|
+
# The client sends a message to the server. The server will respond with a status response, and sometimes with extra data. See {https://tools.ietf.org/html/rfc3977 RFC 3977} for more details.
|
11
|
+
#
|
12
|
+
# This class handles this communication by providing two methods,
|
13
|
+
# one for use when additional data is expected, and one for when it is not.
|
14
|
+
class Connection
|
15
|
+
# @!attribute [r] socket
|
16
|
+
# The object upon which all IO takes place.
|
17
|
+
#
|
18
|
+
# @!attribute [r] status
|
19
|
+
# The most status of the most recent command.
|
20
|
+
# @return [NNTP::Status]
|
21
|
+
|
22
|
+
attr_reader :socket, :status
|
23
|
+
|
24
|
+
# (see #build_socket)
|
25
|
+
def initialize(options)
|
26
|
+
@socket = build_socket(options)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Sends a message to the server, collects the the status and additional data, if successful.
|
30
|
+
# A Hash is returned containing two keys: :status and :data.
|
31
|
+
# :status is an {NNTP::Status}, and :data is an array containing
|
32
|
+
# the lines from the response See example for details.
|
33
|
+
# @example
|
34
|
+
# nntp.query(:list) do |status, data|
|
35
|
+
# $stdout.puts status
|
36
|
+
# data.each? do |line|
|
37
|
+
# $stdout.puts line
|
38
|
+
# end
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# => 215 Information follows
|
42
|
+
# => alt.bin.foo
|
43
|
+
# => alt.bin.bar
|
44
|
+
# @param query [Symbol, String] The command to send
|
45
|
+
# @param args Any additional parameters are passed along with the command.
|
46
|
+
# @yield The status and data from the server.
|
47
|
+
# @yieldparam status [NNTP::Status]
|
48
|
+
# @yieldparam data [Array<String>, nil] An array with the lines from the server. Nil if the query failed.
|
49
|
+
# @return [Hash] The status and requested data.
|
50
|
+
def query(query, *args)
|
51
|
+
command = form_message(query, args)
|
52
|
+
send_message(command)
|
53
|
+
status = get_status
|
54
|
+
data = if (400..599).include? status.code
|
55
|
+
nil
|
56
|
+
else
|
57
|
+
get_block_data
|
58
|
+
end
|
59
|
+
yield status, data if block_given?
|
60
|
+
{:status => status, :data => data}
|
61
|
+
end
|
62
|
+
|
63
|
+
# Sends a message to the server and collects the status response.
|
64
|
+
# @param (see #query)
|
65
|
+
# @return [NNTP::Status] The server's response.
|
66
|
+
def command(command, *args)
|
67
|
+
command = form_message(command, args)
|
68
|
+
send_message(command)
|
69
|
+
get_status
|
70
|
+
end
|
71
|
+
|
72
|
+
# Fetch a status line from the server.
|
73
|
+
# @return [NNTP::Status]
|
74
|
+
def get_status
|
75
|
+
code, message = get_line.split(' ', 2)
|
76
|
+
@status = status_factory(code.to_i, message)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Sends "QUIT\\r\\n" to the server, disconnects the socket.
|
80
|
+
# @return [void]
|
81
|
+
def quit
|
82
|
+
command(:quit)
|
83
|
+
socket.close
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
def form_message(command, *args)
|
88
|
+
message = "#{command.to_s.upcase}"
|
89
|
+
message += " #{args.join(' ')}" unless args.empty?
|
90
|
+
end
|
91
|
+
|
92
|
+
def build_socket(options)
|
93
|
+
options.fetch(:socket) do
|
94
|
+
url = options.fetch(:url) do
|
95
|
+
raise ArgumentError "Must have :url or :socket"
|
96
|
+
end
|
97
|
+
port = options.fetch(:port, 119)
|
98
|
+
socket_factory = options.fetch(:socket_factory) { TCPSocket }
|
99
|
+
socket_factory.new(url, port)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def send_message(message)
|
104
|
+
socket.print "#{message}\r\n"
|
105
|
+
end
|
106
|
+
|
107
|
+
def get_line
|
108
|
+
line = socket.gets
|
109
|
+
line.chomp
|
110
|
+
end
|
111
|
+
|
112
|
+
def get_block_data
|
113
|
+
data = []
|
114
|
+
line = get_line
|
115
|
+
until line == '.'
|
116
|
+
data << line
|
117
|
+
line = get_line
|
118
|
+
end
|
119
|
+
data
|
120
|
+
end
|
121
|
+
|
122
|
+
def status_factory(*args)
|
123
|
+
NNTP::Status.new(*args)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
data/lib/nntp/group.rb
ADDED
data/lib/nntp/message.rb
ADDED
data/lib/nntp/session.rb
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
require "nntp/group"
|
2
|
+
require 'nntp/message'
|
3
|
+
|
4
|
+
module NNTP
|
5
|
+
# Most of the action happens here. This class describes the
|
6
|
+
# object the user interacts with the most.
|
7
|
+
# It is constructed by NNTP::open, but you can build your own
|
8
|
+
# if you like.
|
9
|
+
class Session
|
10
|
+
attr_reader :connection, :group
|
11
|
+
# @option options [NNTP::Connection, NNTP::SSLConnection] :connection
|
12
|
+
# The connection object.
|
13
|
+
def initialize(options)
|
14
|
+
@group = nil
|
15
|
+
@connection = options.fetch(:connection) do
|
16
|
+
raise ArgumentError ":connection missing"
|
17
|
+
end
|
18
|
+
check_initial_status
|
19
|
+
end
|
20
|
+
|
21
|
+
# Authenticate to the server.
|
22
|
+
#
|
23
|
+
# @option args :user Username
|
24
|
+
# @option args :pass Password
|
25
|
+
# @option args :type (:standard)
|
26
|
+
# Which authentication type to use. Currently only
|
27
|
+
# standard is supported.
|
28
|
+
# @return [NNTP::Status]
|
29
|
+
def auth(args)
|
30
|
+
auth_method = args.fetch(:type, :standard)
|
31
|
+
standard_auth(args) if auth_method == :standard
|
32
|
+
end
|
33
|
+
|
34
|
+
# Fetches and returns the list of groups from the server or, if it
|
35
|
+
# has already been fetched, returns the saved list.
|
36
|
+
# @return [Array<NNTP::Group>]
|
37
|
+
def groups
|
38
|
+
@groups ||= fetch_groups
|
39
|
+
end
|
40
|
+
|
41
|
+
# @!attribute [rw] group
|
42
|
+
# Retrieves current group, or
|
43
|
+
# sets current group, server-side, to assigned value.
|
44
|
+
# @param [String] group_name The name of the group to be selected.
|
45
|
+
# @return [NNTP::Group] The current group.
|
46
|
+
def group=(group_name)
|
47
|
+
connection.command(:group, group_name)
|
48
|
+
if status[:code] == 211
|
49
|
+
num, low, high, name = status[:msg].split
|
50
|
+
@group = group_factory(name, low.to_i, high.to_i, num.to_i)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Fetch list of message numbers from a given group.
|
55
|
+
# @param group The name of the group to list defaults to {#group #group.name}
|
56
|
+
# @param range (nil) If given, specifies the range of messages to retrieve
|
57
|
+
# @return [Array<NNTP::Message>] The list of messages
|
58
|
+
# (only the message numbers will be populated).
|
59
|
+
# @see https://tools.ietf.org/html/rfc3977#section-6.1.2
|
60
|
+
def listgroup(*args)
|
61
|
+
messages = []
|
62
|
+
connection.query(:listgroup, *args) do |status, data|
|
63
|
+
if status[:code] == 211
|
64
|
+
data.each do |line|
|
65
|
+
message = Message.new
|
66
|
+
message.num = line.to_i
|
67
|
+
messages << message
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
messages
|
72
|
+
end
|
73
|
+
|
74
|
+
# Fetch list of messages from current group.
|
75
|
+
# @return [Array<NNTP::Message>] The list of messages
|
76
|
+
# (The numbers AND the subjects will be populated).
|
77
|
+
def subjects
|
78
|
+
subjects = []
|
79
|
+
range ="#{group[:first_message]}-"
|
80
|
+
connection.query(:xhdr, "Subject", range) do |status, data|
|
81
|
+
if status[:code] == 221
|
82
|
+
data.each do |line|
|
83
|
+
message = Message.new
|
84
|
+
message.num, message.subject = line.split(' ', 2)
|
85
|
+
subjects << message
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
subjects
|
90
|
+
end
|
91
|
+
|
92
|
+
# The most recent status from the server.
|
93
|
+
def status
|
94
|
+
connection.status
|
95
|
+
end
|
96
|
+
|
97
|
+
# (see NNTP::Connection#quit)
|
98
|
+
def quit
|
99
|
+
connection.quit
|
100
|
+
end
|
101
|
+
private
|
102
|
+
def standard_auth(args)
|
103
|
+
connection.command(:authinfo, "USER #{args[:user]}")
|
104
|
+
if status[:code] == 381
|
105
|
+
connection.command(:authinfo, "PASS #{args[:pass]}")
|
106
|
+
elsif [281, 482, 502].include? status[:code]
|
107
|
+
status
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def check_initial_status
|
112
|
+
raise "#{status}" if [400, 502].include? connection.get_status.code
|
113
|
+
end
|
114
|
+
|
115
|
+
def group_from_list(group_string)
|
116
|
+
params = group_string.split
|
117
|
+
name = params[0]
|
118
|
+
high_water_mark = params[1].to_i
|
119
|
+
low_water_mark = params[2].to_i
|
120
|
+
group_factory(name, low_water_mark, high_water_mark)
|
121
|
+
end
|
122
|
+
|
123
|
+
def group_factory(*args)
|
124
|
+
name = args[0]
|
125
|
+
low, high, num = args[1..-1].map { |arg| arg.to_i }
|
126
|
+
NNTP::Group.new(name, low, high, num)
|
127
|
+
end
|
128
|
+
|
129
|
+
def fetch_groups
|
130
|
+
group_list = []
|
131
|
+
connection.query :list do |status, list|
|
132
|
+
list.each do |group|
|
133
|
+
group_list << group_from_list(group)
|
134
|
+
end if status[:code] == 215
|
135
|
+
end
|
136
|
+
group_list
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require_relative 'connection'
|
2
|
+
require 'openssl'
|
3
|
+
require 'socket'
|
4
|
+
|
5
|
+
module NNTP
|
6
|
+
class SSLConnection < Connection
|
7
|
+
private
|
8
|
+
def build_socket(args)
|
9
|
+
url = args.fetch(:url) do
|
10
|
+
raise ArgumentError ":url missing"
|
11
|
+
end
|
12
|
+
port = args.fetch(:port, 563)
|
13
|
+
socket = ssl_class.new(TCPSocket.new(url, port))
|
14
|
+
socket.connect
|
15
|
+
socket
|
16
|
+
end
|
17
|
+
|
18
|
+
def ssl_class
|
19
|
+
OpenSSL::SSL::SSLSocket
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/nntp/status.rb
ADDED
data/lib/nntp/version.rb
ADDED
data/nntp-client.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'nntp/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "nntp-client"
|
8
|
+
gem.version = NNTP::VERSION
|
9
|
+
gem.authors = ["Michael Westbom"]
|
10
|
+
gem.email = %w(totallymike@gmail.com)
|
11
|
+
gem.description = %q{Gem to handle basic NNTP usage}
|
12
|
+
gem.summary = %q{NNTP Client}
|
13
|
+
gem.homepage = ""
|
14
|
+
gem.license = "MIT"
|
15
|
+
|
16
|
+
gem.files = `git ls-files`.split($/)
|
17
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
18
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
19
|
+
gem.require_paths = %w(lib)
|
20
|
+
|
21
|
+
gem.add_development_dependency "rake"
|
22
|
+
end
|
data/spec/Status_spec.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
require "nntp/status"
|
3
|
+
|
4
|
+
describe NNTP::Status do
|
5
|
+
describe "#to_s" do
|
6
|
+
it "knows how to render the server response it represents" do
|
7
|
+
foo = NNTP::Status.new(100, "foo")
|
8
|
+
foo.to_s.should eq "100 foo"
|
9
|
+
bar = NNTP::Status.new(200, "bar")
|
10
|
+
bar.to_s.should eq "200 bar"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require_relative "spec_helper"
|
2
|
+
require 'nntp/connection'
|
3
|
+
require 'nntp/status'
|
4
|
+
|
5
|
+
describe NNTP::Connection do
|
6
|
+
let(:sock) { double }
|
7
|
+
let(:conn) { NNTP::Connection.new(:socket => sock)}
|
8
|
+
|
9
|
+
describe "#query" do
|
10
|
+
it "should properly break up multi-line data" do
|
11
|
+
sock.stub(:gets).and_return("123 status thing\r\n", "foo\r\n", "bar\r\n", ".\r\n")
|
12
|
+
sock.stub(:print)
|
13
|
+
response = conn.query(:foo)
|
14
|
+
response[:data].should eq %w(foo bar)
|
15
|
+
response[:status][:code].should eq 123
|
16
|
+
response[:status][:msg].should eq "status thing"
|
17
|
+
end
|
18
|
+
it "won't ask for data if an invalid status comes through" do
|
19
|
+
sock.should_receive(:gets).exactly(:once).and_return "400 error"
|
20
|
+
sock.stub(:print)
|
21
|
+
response = conn.query :foo
|
22
|
+
response[:status].should eq NNTP::Status.new(400, "error")
|
23
|
+
response[:data].should be nil
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/spec/nntp_spec.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
require "nntp"
|
3
|
+
require 'openssl'
|
4
|
+
|
5
|
+
describe "NNTP" do
|
6
|
+
|
7
|
+
describe "::open" do
|
8
|
+
let(:sock) { double() }
|
9
|
+
|
10
|
+
describe "parameters" do
|
11
|
+
it "accepts an open socket in the parameter hash" do
|
12
|
+
conn = NNTP.open(:socket => sock)
|
13
|
+
conn.should_not be nil
|
14
|
+
end
|
15
|
+
|
16
|
+
it "will open its own socket if given a url and a port number" do
|
17
|
+
TCPSocket.should_receive(:new).with("nntp.example.org", 120)
|
18
|
+
conn = NNTP.open(:url => 'nntp.example.org', :port => 120)
|
19
|
+
conn.should_not be nil
|
20
|
+
end
|
21
|
+
|
22
|
+
it "uses port number 119 as a default if no port is specified" do
|
23
|
+
TCPSocket.should_receive(:new).with("nntp.example.org", 119)
|
24
|
+
NNTP.open(:url => 'nntp.example.org')
|
25
|
+
end
|
26
|
+
|
27
|
+
it "can take a class as :socket_factory alongside the url and port" do
|
28
|
+
foo = double()
|
29
|
+
foo.should_receive(:new).with('nntp.example.org', 119)
|
30
|
+
NNTP.open(:url => 'nntp.example.org', :socket_factory => foo)
|
31
|
+
end
|
32
|
+
|
33
|
+
describe ":ssl => true" do
|
34
|
+
let(:ssl_double) do
|
35
|
+
socket = double()
|
36
|
+
socket.stub(:connect)
|
37
|
+
socket
|
38
|
+
end
|
39
|
+
before(:each) do
|
40
|
+
OpenSSL::SSL::SSLSocket.should_receive(:new).and_return { ssl_double }
|
41
|
+
end
|
42
|
+
|
43
|
+
it "builds an SSL connection if :ssl => true" do
|
44
|
+
TCPSocket.stub(:new) { double() }
|
45
|
+
NNTP.open(:url => 'ssl-nntp.example.org', :ssl => true)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'uses port 563 as default' do
|
49
|
+
TCPSocket.should_receive(:new).with('ssl-nntp.example.org', 563)
|
50
|
+
|
51
|
+
NNTP.open(:url => 'ssl-nntp.example.org', :ssl => true)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "yield behavior" do
|
57
|
+
it "yields the session object if a block is given" do
|
58
|
+
sock.stub(:print)
|
59
|
+
sock.stub(:gets).and_return("205 closing connection\r\n")
|
60
|
+
sock.stub(:close)
|
61
|
+
expect { |b| NNTP.open( {:socket => sock}, &b ) }.to yield_control
|
62
|
+
end
|
63
|
+
|
64
|
+
it "automatically closes the connection if a block is given" do
|
65
|
+
conn = double()
|
66
|
+
conn.should_receive(:quit)
|
67
|
+
NNTP.open(:socket => sock) do |nntp|
|
68
|
+
nntp.stub(:connection) { conn }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
require 'nntp'
|
3
|
+
|
4
|
+
describe NNTP::Session do
|
5
|
+
let(:sock) { double() }
|
6
|
+
let(:nntp) { NNTP.open(:socket => sock) }
|
7
|
+
|
8
|
+
describe "#initialize" do
|
9
|
+
it "will raise ArgumentError if no connection is found" do
|
10
|
+
expect { NNTP::Session.new() }.to raise_error(ArgumentError)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "raises an error if the server presents an error message on connection" do
|
14
|
+
NNTP::Session.any_instance.unstub(:check_initial_status)
|
15
|
+
sock.stub(:gets) { "502 Permanently unavailable\r\n" }
|
16
|
+
expect { NNTP.open(:socket => sock) }.to raise_error
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "authorization" do
|
21
|
+
it "will use authinfo style authentication if a user and pass are provided" do
|
22
|
+
NNTP::Session.any_instance.should_receive(:auth).
|
23
|
+
with({:user => 'foo', :pass => 'bar'}).and_call_original
|
24
|
+
NNTP::Session.any_instance.should_receive(:standard_auth)
|
25
|
+
NNTP.open(
|
26
|
+
:socket => sock,
|
27
|
+
:auth => {:user => 'foo', :pass => 'bar'}
|
28
|
+
)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe "#groups" do
|
33
|
+
before(:each) do
|
34
|
+
@connection = double()
|
35
|
+
nntp.stub(:connection) { @connection }
|
36
|
+
end
|
37
|
+
it "should return a list of groups" do
|
38
|
+
@connection.stub(:query).and_yield({:code=>215}, ["alt.bin.foo 2 1 y", "alt.bin.bar 2 1 n"])
|
39
|
+
groups = [ NNTP::Group.new('alt.bin.foo', 1, 2), NNTP::Group.new('alt.bin.bar', 1, 2)]
|
40
|
+
nntp.groups.should eq groups
|
41
|
+
end
|
42
|
+
|
43
|
+
it "will return an empty list if there are no groups" do
|
44
|
+
@connection.stub(:query).and_yield({:code => 215}, [])
|
45
|
+
nntp.groups.should eq []
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "#group=" do
|
50
|
+
before(:each) do
|
51
|
+
sock.stub(:print) {}
|
52
|
+
sock.stub(:gets) { "211 2 1 2 alt.bin.foo\r\n"}
|
53
|
+
end
|
54
|
+
|
55
|
+
it "can change current newsgroup with the assignment operator and a string" do
|
56
|
+
nntp.group = "alt.bin.foo"
|
57
|
+
nntp.group.should eq NNTP::Group.new("alt.bin.foo", 1, 2, 2)
|
58
|
+
end
|
59
|
+
it "does not change the current group if the new one is not found" do
|
60
|
+
sock.stub(:gets) { "411 group not found\r\n" }
|
61
|
+
nntp.group.should eq nil
|
62
|
+
nntp.group = "alt.does.not.exist"
|
63
|
+
nntp.group.should eq nil
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe "#messages" do
|
68
|
+
it "retrieves the list of messages in the current group" do
|
69
|
+
sock.stub(:print)
|
70
|
+
NNTP::Connection.any_instance.stub(:get_status) do
|
71
|
+
NNTP::Status.new(211, "2 1 2 alt.bin.foo list follows")
|
72
|
+
end
|
73
|
+
NNTP::Connection.any_instance.stub(:get_block_data) { %w(1 2) }
|
74
|
+
nntp.stub(:group) { NNTP::Group.new("alt.bin.foo", 1, 2, 2) }
|
75
|
+
|
76
|
+
message_numbers(nntp.listgroup).should eq [1, 2]
|
77
|
+
end
|
78
|
+
it "retrieves the list of messages from a given group" do
|
79
|
+
sock.stub(:print)
|
80
|
+
NNTP::Connection.any_instance.should_receive(:query).with(:listgroup, "alt.bin.bar").
|
81
|
+
and_call_original
|
82
|
+
NNTP::Connection.any_instance.stub(:get_status) do
|
83
|
+
NNTP::Status.new(211, "3 1 3 alt.bin.bar list follows")
|
84
|
+
end
|
85
|
+
NNTP::Connection.any_instance.stub(:get_block_data) { %w(1 2 3) }
|
86
|
+
|
87
|
+
message_numbers(nntp.listgroup("alt.bin.bar")).should eq [1, 2, 3]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
describe "subjects" do
|
91
|
+
let(:subjects) { ["1 Foo bar", "2 Baz bang"] }
|
92
|
+
it "retrieves a list of subjects from the current group" do
|
93
|
+
sock.stub(:print)
|
94
|
+
NNTP::Connection.any_instance.stub(:get_status) do
|
95
|
+
NNTP::Status.new(221, "Header follows")
|
96
|
+
end
|
97
|
+
NNTP::Connection.any_instance.stub(:get_block_data) { subjects }
|
98
|
+
nntp.stub(:group) { NNTP::Group.new("alt.bin.foo", 1, 2, 2) }
|
99
|
+
|
100
|
+
message_subjects(nntp.subjects).should eq ['Foo bar', 'Baz bang']
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require "rspec"
|
2
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
3
|
+
|
4
|
+
RSpec.configure do |config|
|
5
|
+
config.before(:each) do
|
6
|
+
NNTP::Session.any_instance.stub(:check_initial_status) { nil }
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def message_numbers(list)
|
11
|
+
list.map { |msg| msg.num }
|
12
|
+
end
|
13
|
+
|
14
|
+
def message_subjects(list)
|
15
|
+
list.map {|msg| msg.subject }
|
16
|
+
end
|
metadata
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: nntp-client
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.5
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Michael Westbom
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-01-11 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rake
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
description: Gem to handle basic NNTP usage
|
31
|
+
email:
|
32
|
+
- totallymike@gmail.com
|
33
|
+
executables: []
|
34
|
+
extensions: []
|
35
|
+
extra_rdoc_files: []
|
36
|
+
files:
|
37
|
+
- .gitignore
|
38
|
+
- .rspec
|
39
|
+
- Gemfile
|
40
|
+
- LICENSE.txt
|
41
|
+
- README.md
|
42
|
+
- Rakefile
|
43
|
+
- lib/NNTPClient.rb
|
44
|
+
- lib/article.rb
|
45
|
+
- lib/nntp.rb
|
46
|
+
- lib/nntp/connection.rb
|
47
|
+
- lib/nntp/group.rb
|
48
|
+
- lib/nntp/message.rb
|
49
|
+
- lib/nntp/session.rb
|
50
|
+
- lib/nntp/ssl_connection.rb
|
51
|
+
- lib/nntp/status.rb
|
52
|
+
- lib/nntp/version.rb
|
53
|
+
- nntp-client.gemspec
|
54
|
+
- spec/Status_spec.rb
|
55
|
+
- spec/connection_spec.rb
|
56
|
+
- spec/nntp_spec.rb
|
57
|
+
- spec/session_spec.rb
|
58
|
+
- spec/spec_helper.rb
|
59
|
+
homepage: ''
|
60
|
+
licenses:
|
61
|
+
- MIT
|
62
|
+
post_install_message:
|
63
|
+
rdoc_options: []
|
64
|
+
require_paths:
|
65
|
+
- lib
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
68
|
+
requirements:
|
69
|
+
- - ! '>='
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: '0'
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
requirements: []
|
79
|
+
rubyforge_project:
|
80
|
+
rubygems_version: 1.8.24
|
81
|
+
signing_key:
|
82
|
+
specification_version: 3
|
83
|
+
summary: NNTP Client
|
84
|
+
test_files:
|
85
|
+
- spec/Status_spec.rb
|
86
|
+
- spec/connection_spec.rb
|
87
|
+
- spec/nntp_spec.rb
|
88
|
+
- spec/session_spec.rb
|
89
|
+
- spec/spec_helper.rb
|
90
|
+
has_rdoc:
|