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 ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format specdoc
2
+ --color
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in nntp.gemspec
4
+ gem 'rake'
5
+ gem "rspec"
6
+ gem "rspec-core"
7
+ gem "rspec-expectations"
8
+ gem "rspec-mocks"
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
@@ -0,0 +1,7 @@
1
+ module NNTP
2
+ Article = Struct.new(:number, :subject, :article_id) do
3
+ def to_s
4
+ subject
5
+ end
6
+ end
7
+ end
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
@@ -0,0 +1,3 @@
1
+ module NNTP
2
+ Group = Struct.new(:name, :first_message, :last_message, :num_messages)
3
+ end
@@ -0,0 +1,3 @@
1
+ module NNTP
2
+ Message = Struct.new(:id, :subject, :group, :num)
3
+ end
@@ -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
@@ -0,0 +1,7 @@
1
+ module NNTP
2
+ Status = Struct.new(:code, :msg) do
3
+ def to_s
4
+ "#{self[:code]} #{self[:msg]}"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ module NNTP
2
+ VERSION = "0.0.5"
3
+ end
@@ -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
@@ -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
@@ -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: