nntp-client 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
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: