nntp_scrape 0.0.1

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.
@@ -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: