nntp_scrape 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in nntp_scrape.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Scott Fleckenstein
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.
@@ -0,0 +1,56 @@
1
+ # NntpScrape
2
+
3
+ Various usenet related utilities
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'nntp_scrape'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install nntp_scrape
18
+
19
+ ## Usage
20
+
21
+ First, write a config file at ~/.nntp_scrape
22
+
23
+ ```yaml
24
+ host: unlimited.newshosting.com
25
+ port: 563 #default
26
+ ssl: true #default
27
+ user: foobar
28
+ pass: bazbak
29
+
30
+ ```
31
+
32
+ Then you can:
33
+
34
+ ```bash
35
+ #/usr/bin/env bash
36
+ # watches the provided group, outputting a line of json everytime a new article is posted
37
+ nntp_scrape headers alt.binaries.teevee
38
+
39
+ # starts a repl to interact with the usenet server directly
40
+ nntp_scrape repl
41
+ >> run Capabilities
42
+ => #<NntpScrape::Commands::Capabilities:0x007ffc56914e20
43
+ @caps=
44
+ ["VERSION 1",
45
+ "MODE-READER",
46
+ "READER",
47
+ ...
48
+ ```
49
+
50
+ ## Contributing
51
+
52
+ 1. Fork it
53
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
54
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
55
+ 4. Push to the branch (`git push origin my-new-feature`)
56
+ 5. Create new Pull Request
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ require 'nntp_scrape'
4
+
5
+ NntpScrape::Cli.start(ARGV)
@@ -0,0 +1,24 @@
1
+ require "nntp_scrape/version"
2
+ require 'active_support/core_ext/string'
3
+
4
+ module NntpScrape
5
+ autoload :Cli, "nntp_scrape/cli"
6
+ autoload :Config, "nntp_scrape/config"
7
+ autoload :NNTP, "nntp_scrape/nntp"
8
+ autoload :Repl, "nntp_scrape/repl"
9
+
10
+ module Commands
11
+ autoload :Base, "nntp_scrape/commands/base"
12
+ autoload :AuthInfo, "nntp_scrape/commands/auth_info"
13
+ autoload :Group, "nntp_scrape/commands/group"
14
+ autoload :Next, "nntp_scrape/commands/next"
15
+ autoload :Head, "nntp_scrape/commands/head"
16
+ autoload :Xhdr, "nntp_scrape/commands/xhdr"
17
+ autoload :Xover, "nntp_scrape/commands/xover"
18
+ autoload :Capabilities, "nntp_scrape/commands/capabilities"
19
+
20
+ end
21
+
22
+
23
+
24
+ end
@@ -0,0 +1,47 @@
1
+ require 'thor'
2
+ require 'active_support'
3
+
4
+ module NntpScrape
5
+
6
+ class Cli < Thor
7
+ class_option :config,
8
+ :desc => "Configuration file location",
9
+ :type => :string,
10
+ :default => ENV["HOME"] + "/.nntp_scrape"
11
+
12
+ desc "headers GROUP", "watches GROUP, outputting lines of json as new headers are found"
13
+ def headers(group)
14
+ setup
15
+
16
+ @client.watch_new(group) do |result|
17
+ p result
18
+ end
19
+
20
+ end
21
+
22
+ desc "repl", "Starts an interactive repl"
23
+ def repl
24
+ setup
25
+ r = Repl.new(@client)
26
+ r.start
27
+ end
28
+
29
+
30
+ private
31
+ def setup
32
+ config, status = *Config.load(options[:config])
33
+
34
+ unless status == true
35
+ puts status
36
+ exit 1
37
+ end
38
+
39
+ @client = NNTP.new(config.host, config.port, config.ssl, config.user, config.pass)
40
+
41
+ unless @client.logged_in?
42
+ puts "login failed"
43
+ exit 1
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,16 @@
1
+ module NntpScrape
2
+ module Commands
3
+ class AuthInfo < Base
4
+
5
+ def initialize(type, value)
6
+ @type = type
7
+ @value = value
8
+ end
9
+
10
+ def execute(client)
11
+ run_short client, "AUTHINFO", @type, @value
12
+ end
13
+
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,46 @@
1
+ module NntpScrape
2
+ module Commands
3
+ class Base
4
+ attr_reader :response, :status_line, :lines
5
+ attr_accessor :timeout
6
+
7
+ def initialize
8
+ @timeout = 5
9
+ end
10
+
11
+ def self.supported?(client)
12
+ true
13
+ end
14
+
15
+ def ran?
16
+ status_line.present?
17
+ end
18
+
19
+ def success?
20
+ return false unless ran?
21
+ status_code[0] == "1" || status_code[0] == "2"
22
+ end
23
+
24
+ def continue?
25
+ success? || status_code[0] == "3"
26
+ end
27
+
28
+ def status_code
29
+ @status_code ||= status_line.split.first
30
+ end
31
+
32
+ def execute(client)
33
+ raise "implement in subclass"
34
+ end
35
+
36
+ def run_short(client, cmd, *params)
37
+ @status_line = client.run_short(cmd, *params).strip
38
+
39
+ end
40
+
41
+ def run_long(client, cmd, *params)
42
+ @status_line, @lines = *client.run_long(cmd, *params)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,18 @@
1
+ module NntpScrape
2
+ module Commands
3
+ class Capabilities < Base
4
+ attr_reader :caps
5
+
6
+ def initialize
7
+ @caps = []
8
+ end
9
+
10
+ def execute(client)
11
+ run_long client, "CAPABILITIES"
12
+ return unless success?
13
+
14
+ @caps = lines
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,27 @@
1
+ module NntpScrape
2
+ module Commands
3
+ class Group < Base
4
+ attr_reader :article_count
5
+ attr_reader :low_id
6
+ attr_reader :high_id
7
+
8
+ PARSER = /^(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/
9
+
10
+ def initialize(group)
11
+ @group = group
12
+ end
13
+
14
+ def execute(client)
15
+ run_short client, "GROUP", @group
16
+
17
+ if match = PARSER.match(status_line)
18
+ @article_count = match[2]
19
+ @low_id = match[3]
20
+ @high_id = match[4]
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,28 @@
1
+ module NntpScrape
2
+ module Commands
3
+ class Head < Base
4
+ attr_reader :message_id
5
+ attr_reader :data
6
+
7
+ def initialize(number=nil)
8
+ @number = number
9
+ end
10
+
11
+ def execute(client)
12
+ if @number.present?
13
+ run_long client, "HEAD", @number
14
+ else
15
+ run_long client, "HEAD"
16
+ end
17
+
18
+
19
+ @message_id = status_line.split.last
20
+ @data = {}
21
+ lines.each do |line|
22
+ key, value = *line.split(": ", 2)
23
+ @data[key] = value
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,9 @@
1
+ module NntpScrape
2
+ module Commands
3
+ class Next < Base
4
+ def execute(client)
5
+ run_short client, "NEXT"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,26 @@
1
+ module NntpScrape
2
+ module Commands
3
+ class Xhdr < Base
4
+ attr_reader :results
5
+
6
+ def self.supported?(client)
7
+ client.caps.include? "XHDR"
8
+ end
9
+
10
+ def initialize(field, range)
11
+ @field = field
12
+ @range = range
13
+ end
14
+
15
+ def execute(client)
16
+ if @range.is_a? Range
17
+ run_long client, "XHDR", @field, "#{@range.begin}-#{@range.end}"
18
+ else
19
+ run_long client, "XHDR", @field, @range
20
+ end
21
+
22
+ @results = @lines.map{|l| l.split(" ")}
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,25 @@
1
+ module NntpScrape
2
+ module Commands
3
+ class Xover < Base
4
+ attr_reader :results
5
+
6
+ def self.supported?(client)
7
+ client.caps.include? "XOVER"
8
+ end
9
+
10
+ def initialize(range)
11
+ @range = range
12
+ end
13
+
14
+ def execute(client)
15
+ if @range.is_a? Range
16
+ run_long client, "XOVER", "#{@range.begin}-#{@range.end}"
17
+ else
18
+ run_long client, "XOVER", @range
19
+ end
20
+
21
+ @results = @lines.map{|l| l.split("\t")}
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ require 'yaml'
2
+ require 'hashie'
3
+
4
+ module NntpScrape
5
+ class Config < Hashie::Trash
6
+ property :host, :required => true
7
+ property :ssl, :default => true
8
+ property :port, :default => 563
9
+ property :user, :required => true
10
+ property :pass, :required => true
11
+
12
+ def self.load(config_path)
13
+ return [nil, :path_doesnt_exist] unless File.exist?(config_path)
14
+
15
+ yaml = YAML.load_file(config_path)
16
+ [new(yaml), true]
17
+ rescue Psych::SyntaxError
18
+ [nil, :invalid_yaml]
19
+ rescue ArgumentError => e
20
+ [nil, e]
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,163 @@
1
+ require 'socket'
2
+ require 'openssl'
3
+ require 'timeout'
4
+
5
+ module NntpScrape
6
+ class NNTP
7
+ attr_reader :socket
8
+ attr_reader :caps
9
+
10
+ def initialize(host, port, ssl, user, pass)
11
+ @host = host
12
+ @port = port
13
+ @ssl = ssl
14
+ @user = user
15
+ @pass = pass
16
+ @caps = []
17
+ open
18
+ end
19
+
20
+ def logged_in?
21
+ @logged_in
22
+ end
23
+
24
+ def debug?
25
+ @debug
26
+ end
27
+
28
+ def debug=(val)
29
+ @debug = val
30
+ end
31
+
32
+ def open
33
+ @socket = make_socket
34
+ login
35
+ end
36
+
37
+ def watch(group, start_id=nil)
38
+ watch = Commands::Group.new(group)
39
+ run watch
40
+ return false unless watch.success?
41
+
42
+ puts watch.high_id
43
+ head = Commands::Head.new(start_id || watch.high_id)
44
+ run head
45
+ return false unless head.success?
46
+
47
+ loop do
48
+ commands = [Commands::Next.new, Commands::Head.new]
49
+
50
+ unless run *commands
51
+ puts "pausing..."
52
+ sleep 1
53
+ next
54
+ end
55
+ puts commands[1].message_id
56
+ end
57
+ end
58
+
59
+ def watch_new(group, start_id=nil)
60
+ if start_id.nil?
61
+ watch = Commands::Group.new(group)
62
+ run watch
63
+ return false unless watch.success?
64
+ start_id = watch.high_id
65
+ end
66
+
67
+ loop do
68
+ watch = Commands::Group.new(group)
69
+ run watch
70
+ next unless watch.success?
71
+
72
+ end_id = watch.high_id
73
+
74
+ if start_id == end_id
75
+ sleep 1
76
+ next
77
+ end
78
+
79
+ xhdr = Commands::Xhdr.new "Message-ID", start_id...end_id
80
+ run xhdr
81
+ next unless xhdr.success?
82
+
83
+ xhdr.results.each{|v| yield v}
84
+ start_id = end_id
85
+ end
86
+
87
+ end
88
+
89
+ ##
90
+ # Executes an array of commands until one fails
91
+ #
92
+ # @return [Boolean] true if all commands succeeded
93
+ def run(*commands)
94
+ commands.each do |cmd|
95
+ Timeout::timeout(cmd.timeout) do
96
+ cmd.execute(self)
97
+ end
98
+ return false unless cmd.continue?
99
+ end
100
+ true
101
+ rescue Errno::EPIPE, Errno::ECONNRESET, OpenSSL::SSL::SSLError, Timeout::Error
102
+ open
103
+ retry
104
+ end
105
+
106
+
107
+ def run_short(cmd, *params)
108
+ command = "#{cmd} #{params.join " "}"
109
+
110
+ puts command if debug?
111
+
112
+ socket.print "#{command}\r\n"
113
+ status_line = socket.gets
114
+
115
+ puts status_line if debug?
116
+
117
+ status_line
118
+ end
119
+
120
+ def run_long(cmd, *params)
121
+ status_line = run_short(cmd, *params)
122
+ lines = []
123
+
124
+ return [status_line, lines] unless ["2", "1"].include? status_line[0] #i.e. success
125
+
126
+ loop do
127
+ next_line = socket.gets
128
+ break if next_line.strip == "."
129
+ lines << next_line.strip
130
+ end
131
+ [status_line, lines]
132
+ end
133
+
134
+ private
135
+ def make_socket
136
+ socket = TCPSocket.open @host, @port
137
+
138
+ if @ssl
139
+ cert_store = OpenSSL::X509::Store.new
140
+ cert_store.set_default_paths
141
+ ssl_context = OpenSSL::SSL::SSLContext.new
142
+ ssl_context.cert_store = cert_store
143
+ socket = OpenSSL::SSL::SSLSocket.new socket, ssl_context
144
+ socket.connect
145
+ end
146
+
147
+ socket.gets
148
+ socket
149
+ end
150
+
151
+ def login
152
+ login_commands = [
153
+ Commands::AuthInfo.new("USER", @user),
154
+ Commands::AuthInfo.new("PASS", @pass),
155
+ Commands::Capabilities.new,
156
+ ]
157
+ @logged_in = run *login_commands
158
+
159
+ @caps = login_commands.last.caps
160
+
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,40 @@
1
+ module NntpScrape
2
+ class NNTPCommand
3
+ attr_reader :response, :status_line
4
+
5
+ def success?
6
+ status_code[0] == "2"
7
+ end
8
+
9
+ def status_code
10
+ @status_code ||= status_line.split.first
11
+ end
12
+
13
+ def execute(client)
14
+ raise "implement in subclass"
15
+ end
16
+ end
17
+
18
+ class ShortCommand < NNTPCommand
19
+ def execute(client)
20
+ socket = client.socket
21
+ socket.print "#{cmd} #{params.join " "}\r\n"
22
+ @status_line = socket.gets
23
+ end
24
+ end
25
+
26
+ class LongCommand < ShortCommand
27
+ attr_reader :lines
28
+
29
+ def execute(client)
30
+ super client
31
+
32
+ @lines = []
33
+ loop do
34
+ next_line = socket.gets
35
+ break if next_line.strip == "."
36
+ @lines << next_line
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,41 @@
1
+ require 'pry'
2
+
3
+ module NntpScrape
4
+ class Repl
5
+ include Commands
6
+
7
+ attr_reader :client
8
+
9
+ def initialize(client)
10
+ @client = client
11
+ end
12
+
13
+ def start
14
+ my_prompt = [
15
+ proc { |obj, *| ">> " },
16
+ proc { |obj, *| "*> "}
17
+ ]
18
+ binding.pry :quiet => true, :cli => true, :prompt => my_prompt
19
+ end
20
+
21
+ def run(klass, *params)
22
+ cmd = klass.new(*params)
23
+ @client.run(cmd)
24
+ cmd
25
+ end
26
+
27
+ def short(cmd, *params)
28
+ @client.run_short(cmd, *params)
29
+ rescue Errno::EPIPE, Errno::ECONNRESET, OpenSSL::SSL::SSLError
30
+ @client.open
31
+ retry
32
+ end
33
+
34
+ def long(cmd, *params)
35
+ @client.run_long(cmd, *params)
36
+ rescue Errno::EPIPE, Errno::ECONNRESET, OpenSSL::SSL::SSLError
37
+ @client.open
38
+ retry
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,3 @@
1
+ module NntpScrape
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'nntp_scrape/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "nntp_scrape"
8
+ gem.version = NntpScrape::VERSION
9
+ gem.authors = ["Scott Fleckenstein"]
10
+ gem.email = ["nullstyle@gmail.com"]
11
+ gem.description = %q{A series of command line tools to interact with usenet}
12
+ gem.summary = %q{A series of command line tools to interact with usenet}
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency "thor", ">= 0.16.0"
21
+ gem.add_dependency "activesupport", ">= 3.2.9"
22
+ gem.add_dependency "hashie", ">= 1.2.0"
23
+ gem.add_dependency "pry", ">= 0.9.10"
24
+ end
metadata ADDED
@@ -0,0 +1,133 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nntp_scrape
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Scott Fleckenstein
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-12-11 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: thor
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 0.16.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 0.16.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: activesupport
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: 3.2.9
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: 3.2.9
46
+ - !ruby/object:Gem::Dependency
47
+ name: hashie
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: 1.2.0
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: 1.2.0
62
+ - !ruby/object:Gem::Dependency
63
+ name: pry
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: 0.9.10
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: 0.9.10
78
+ description: A series of command line tools to interact with usenet
79
+ email:
80
+ - nullstyle@gmail.com
81
+ executables:
82
+ - nntp_scrape
83
+ extensions: []
84
+ extra_rdoc_files: []
85
+ files:
86
+ - .gitignore
87
+ - Gemfile
88
+ - LICENSE.txt
89
+ - README.md
90
+ - Rakefile
91
+ - bin/nntp_scrape
92
+ - lib/nntp_scrape.rb
93
+ - lib/nntp_scrape/cli.rb
94
+ - lib/nntp_scrape/commands/auth_info.rb
95
+ - lib/nntp_scrape/commands/base.rb
96
+ - lib/nntp_scrape/commands/capabilities.rb
97
+ - lib/nntp_scrape/commands/group.rb
98
+ - lib/nntp_scrape/commands/head.rb
99
+ - lib/nntp_scrape/commands/next.rb
100
+ - lib/nntp_scrape/commands/xhdr.rb
101
+ - lib/nntp_scrape/commands/xover.rb
102
+ - lib/nntp_scrape/config.rb
103
+ - lib/nntp_scrape/nntp.rb
104
+ - lib/nntp_scrape/nntp_command.rb
105
+ - lib/nntp_scrape/repl.rb
106
+ - lib/nntp_scrape/version.rb
107
+ - nntp_scrape.gemspec
108
+ homepage: ''
109
+ licenses: []
110
+ post_install_message:
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ none: false
116
+ requirements:
117
+ - - ! '>='
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ requirements: []
127
+ rubyforge_project:
128
+ rubygems_version: 1.8.23
129
+ signing_key:
130
+ specification_version: 3
131
+ summary: A series of command line tools to interact with usenet
132
+ test_files: []
133
+ has_rdoc: