nntp-client 0.0.5
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/.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:
|