meimei 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGES ADDED
@@ -0,0 +1,7 @@
1
+ = meimei Changelog
2
+
3
+ === Version 0.1.1
4
+ * Fixed bug with plugins
5
+
6
+ === Version 0.1.0
7
+ * Initial public release
data/LICENSE ADDED
@@ -0,0 +1,31 @@
1
+ Copyright (c) 2008, Albert Yi
2
+
3
+ All rights reserved.
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ * Redistributions of source code must retain the above copyright notice,
9
+ this list of conditions and the following disclaimer.
10
+
11
+ * Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the
13
+ documentation and/or other materials provided with the
14
+ distribution.
15
+
16
+ * Neither the name of Albert Yi nor the
17
+ names of its contributors may be used to endorse or promote
18
+ products derived from this software without specific prior written
19
+ permission.
20
+
21
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
22
+ IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
23
+ TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
24
+ PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
25
+ OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
26
+ EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
27
+ PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
28
+ PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
29
+ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
30
+ NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
31
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README ADDED
@@ -0,0 +1,28 @@
1
+ = meimei
2
+
3
+ == DESCRIPTION
4
+ Meimei is a simple IRC bot framework. It is designed to be easily extensible. In the interest of simplicity it only implements a subset RFC 1459, mainly those dealing with PRIVMSGs.
5
+
6
+ === Usage
7
+
8
+ require 'meimei'
9
+ client = Meimei::Client.new("meimei")
10
+ client.add_server("irc.synirc.net", 6667, "#meimei, #bonklers")
11
+ client.start()
12
+
13
+ === Plugins
14
+
15
+ # In plugins/reload.rb
16
+ define_plugin("!reload") do |msg|
17
+ load_plugins()
18
+ reply("Plugins reloaded.")
19
+ end
20
+
21
+ # In plugins/clock.rb
22
+ define_plugin("@1h") do
23
+ say("localhost", "#meimei", "One hour has passed.")
24
+ end
25
+
26
+ === Thread Safety
27
+
28
+ Meimei does not use any global or class variables, so a single client instance should be thread safe. Obviously Meimei does make use of instance variables, so using threads from within a single client may cause issues.
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ # make sure our project's ./lib directory is added to the ruby search path
2
+ $: << File.join(File.dirname(__FILE__),"lib")
3
+
4
+ require 'rubygems'
5
+ require 'rake/gempackagetask'
6
+ require 'rake/clean'
7
+ require 'rake/rdoctask'
8
+ require 'rake/contrib/sshpublisher'
9
+
10
+ require 'meimei'
11
+
12
+ load 'tasks/setup.rb'
13
+
14
+
@@ -0,0 +1,7 @@
1
+ module ArrayExtensions
2
+ def pick_random
3
+ return self[rand(size)]
4
+ end
5
+ end
6
+
7
+ Array.__send__(:include, ArrayExtensions)
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'logger'
4
+ require 'socket'
5
+
6
+ module Meimei
7
+ class Client
8
+ # === Parameters
9
+ # - nick: The nickname your bot will use.
10
+ # - options[:username]: The username of your bot (will default to nick).
11
+ # - options[:realname]: The real name of your bot (will default to nick).
12
+ # - options[:password]: The password for your bot's account.
13
+ # - options[:plugin_dir]: An absolute path to the directory where Meimei plugins will be stored.
14
+ # - options[:log_dir]: An absolute path to where log files will be stored.
15
+ # - options[:log_level]: The log level. Can be: :error, :info, :debug.
16
+ def initialize(nick, options = {})
17
+ @servers = []
18
+ @running = false
19
+ @command_plugins = []
20
+ @timer_plugins = []
21
+ @nick = nick
22
+ @username = options[:username] || @nick
23
+ @realname = options[:realname] || @nick
24
+ @password = options[:password]
25
+ @plugin_dir = options[:plugin_dir] || "plugins"
26
+ @log_dir = options[:log_dir] || "."
27
+ @log_level = options[:log_level] || :info
28
+ @logger = Logger.new(open("#{@log_dir}/#{@nick}-#{Time.now.strftime('%Y%m%d-%H%M')}.log", "w"))
29
+ @logger.datetime_format = "%Y-%m-%d %H:%M:%S"
30
+ end
31
+
32
+ # Registers a server with the client. This method will not open a connection.
33
+ def add_server(hostname, port, autojoin)
34
+ @servers << Server.new(hostname, port, autojoin, :log_dir => @log_dir, :log_level => @log_level)
35
+ end
36
+
37
+ # Reloads all plugins.
38
+ def load_plugins
39
+ @command_plugins.clear()
40
+ @timer_plugins.clear()
41
+
42
+ # TODO: This is hackish
43
+ Dir["#{@plugin_dir}/*.rb"].sort.each do |file|
44
+ eval(File.open(file, "r").read, binding, file)
45
+ end
46
+ end
47
+
48
+ # Creates a new plugin.
49
+ #
50
+ # === Parameters
51
+ # - name: The name of the plugin. There are two special cases: (1) If the name begins with "!", then it is considered a command. For example, a plugin named "!wiki" would only be invoked if a message began with "!wiki". The message passed to the plugin will have the command stripped out. (2) If the plugin begins with "@", then it is considered a timer event. For example, a plugin named "@60" will run every 60 seconds. "@10m" will run every 10 minutes, and "@4h" will run every 4 hours.
52
+ # - options[:position]: Insert plugin at position n (lower number positions will be run first).
53
+ def define_plugin(name, options = {}, &block)
54
+ # TODO: Is there a memory leak here?
55
+
56
+ case name
57
+ when /^@(\d+)([mh])?/
58
+ interval = $1.to_i
59
+
60
+ if $2 == "m"
61
+ interval = interval * 60
62
+ elsif $2 == "h"
63
+ interval = interval * 60 * 60
64
+ end
65
+
66
+ plugins = @timer_plugins
67
+ plugin = [name, interval, Time.now, block, options]
68
+
69
+ else
70
+ plugins = @command_plugins
71
+ plugin = [name, block, options]
72
+ end
73
+
74
+ if options[:position]
75
+ plugins.insert(options[:position], plugin)
76
+ else
77
+ plugins << plugin
78
+ end
79
+
80
+ plugins.compact!
81
+ end
82
+
83
+ # Run all timer plugins.
84
+ def check_timer_plugins
85
+ @timer_plugins.each do |plugin|
86
+ begin
87
+ # Need to refer directly to the array since we'll be changing its values
88
+ # plugin[0]: name
89
+ # plugin[1]: interval
90
+ # plugin[2]: next_run_at
91
+ # plugin[3]: block
92
+ # plugin[4]: options
93
+
94
+ if plugin[2] < Time.now
95
+ plugin[3].call()
96
+ plugin[2] = Time.now + plugin[1]
97
+ end
98
+ rescue Exception => e
99
+ e.dump(@logger)
100
+ end
101
+ end
102
+ end
103
+
104
+ # Run all command plugins.
105
+ def check_command_plugins(msg)
106
+ @command_plugins.each do |name, block, options|
107
+ begin
108
+ if name.index("!") == 0
109
+ if msg =~ /^#{name}\b/
110
+ msg = msg.gsub(/!\S+\s*/, "")
111
+ block.call(msg)
112
+ end
113
+ else
114
+ block.call(msg)
115
+ end
116
+ rescue Exception => e
117
+ reply "Error: #{e.class}"
118
+ e.dump(@current_server.logger)
119
+ end
120
+ end
121
+ end
122
+
123
+ # Returns the first server that has data to be read.
124
+ def select
125
+ sockets = @servers.map {|x| x.socket}.compact
126
+ socket = Kernel.select(sockets, nil, nil, 1)
127
+
128
+ if socket == nil
129
+ return nil
130
+ else
131
+ socket = socket[0][0]
132
+ return @servers.find {|x| x.socket.__id__ == socket.__id__}
133
+ end
134
+ end
135
+
136
+ # Starts the main event loop.
137
+ def start
138
+ self.load_plugins()
139
+ @running = true
140
+
141
+ while @running
142
+ @servers.each do |server|
143
+ unless server.is_connected
144
+ if server.reconnect_delay_passed? && server.connect()
145
+ server.write("PASS #{@password}") if @password
146
+ server.write("NICK #{@nick}")
147
+ server.write("USER #{@username} hostname servername :#{@username}")
148
+ server.autojoin.each do |channel|
149
+ server.write("JOIN #{channel}")
150
+ end
151
+ end
152
+ end
153
+
154
+ self.check_timer_plugins()
155
+
156
+ @current_server = self.select()
157
+
158
+ if @current_server
159
+ msg = @current_server.read
160
+
161
+ if msg != nil
162
+ self.process_message(msg)
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
168
+
169
+ # Quits all clients and ends the event loop.
170
+ def quit(msg = nil)
171
+ @servers.each do |server|
172
+ if msg
173
+ server.write("QUIT :#{msg}")
174
+ else
175
+ server.write("QUIT")
176
+ end
177
+ end
178
+
179
+ @running = false
180
+ end
181
+
182
+ # Respond to the current server/channel/user. If {with_nick} is true, then the messager's sender's nick will be prepended to the response.
183
+ def reply(msg, with_nick = true)
184
+ if with_nick
185
+ @current_server.write("PRIVMSG #{@current_to} :#{@current_from}: #{msg}")
186
+ else
187
+ @current_server.write("PRIVMSG #{@current_to} :#{msg}")
188
+ end
189
+ end
190
+
191
+ # Send {msg} to {to} on {server}. {to} can either be a channel or someone's nick.
192
+ def say(server, to, msg)
193
+ if server.is_a?(String)
194
+ server = @servers.find {|x| x.hostname == server}
195
+ end
196
+
197
+ server.write("PRIVMSG #{to} :#{msg}")
198
+ end
199
+
200
+ # Parses a messages and dispatches as necessary.
201
+ def process_message(msg)
202
+ @current_from = nil
203
+ @current_to = nil
204
+ @current_msg = nil
205
+
206
+ case msg
207
+ when /^:(.+?)!(.+?)@(\S+) PRIVMSG (\S+) :(.+)$/
208
+ @current_from = $1
209
+ @current_to = $4
210
+ @current_msg = $5.strip
211
+
212
+ self.check_command_plugins(@current_msg)
213
+
214
+ when /^PING (.+)$/
215
+ @current_server.write("PONG #{$1}")
216
+ end
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,14 @@
1
+ module Meimei
2
+ module ExceptionDumpMethods
3
+ def dump(logger)
4
+ logger.info "* #{self.class} error thrown"
5
+ logger.info "* Message: #{self.message}"
6
+ logger.info "* Backtrace:"
7
+ self.backtrace.each do |line|
8
+ logger.info "** #{line}"
9
+ end
10
+ end
11
+ end
12
+
13
+ Exception.__send__(:include, ExceptionDumpMethods)
14
+ end
@@ -0,0 +1,45 @@
1
+ require 'rubygems'
2
+ require 'meimei/specification'
3
+ require 'meimei/version'
4
+ require 'rake'
5
+
6
+ # The Gem Specification plus some extras for meimei.
7
+ module Meimei
8
+ SPEC = Meimei::Specification.new do |spec|
9
+ spec.name = "meimei"
10
+ spec.version = Meimei::VERSION
11
+ spec.rubyforge_project = "meimei"
12
+ spec.author = "Albert Yi"
13
+ spec.email = "r888888888@gmail.com"
14
+ spec.homepage = "http://meimei.rubyforge.org/"
15
+
16
+ spec.summary = "A simple IRC bot framework."
17
+ spec.description = <<-DESC
18
+ Meimei is a simple IRC bot framework. It is designed to be easily extensible. In the interest of simplicity it only implements a subset RFC 1459, mainly those dealing with PRIVMSGs.
19
+ DESC
20
+
21
+ spec.extra_rdoc_files = FileList["[A-Z]*"]
22
+ spec.has_rdoc = true
23
+ spec.rdoc_main = "README"
24
+ spec.rdoc_options = [ "--line-numbers" , "--inline-source" ]
25
+
26
+ spec.test_files = FileList["spec/**/*.rb", "test/**/*.rb"]
27
+ spec.files = spec.test_files + spec.extra_rdoc_files +
28
+ FileList["lib/**/*.rb", "resources/**/*"]
29
+
30
+
31
+ # add dependencies
32
+ # spec.add_dependency("somegem", ">= 0.4.2")
33
+
34
+ spec.platform = Gem::Platform::RUBY
35
+
36
+ spec.local_rdoc_dir = "doc/rdoc"
37
+ spec.remote_rdoc_dir = "#{spec.name}/rdoc"
38
+ spec.local_coverage_dir = "doc/coverage"
39
+ spec.remote_coverage_dir= "#{spec.name}/coverage"
40
+
41
+ spec.remote_site_dir = "#{spec.name}/"
42
+ end
43
+ end
44
+
45
+
@@ -0,0 +1,43 @@
1
+ class PersistentHash
2
+ def initialize(file_path, commit_interval = 1)
3
+ @commit_interval = commit_interval
4
+ @commit_count = 0
5
+ @file_path = file_path
6
+ restore!
7
+ unless @hash.is_a?(Hash)
8
+ @hash = {}
9
+ commit!
10
+ end
11
+ end
12
+
13
+ def restore!
14
+ if File.exist?(@file_path)
15
+ mode = File::RDONLY
16
+ else
17
+ return
18
+ end
19
+
20
+ File.open(@file_path, mode) do |f|
21
+ @hash = Marshal.restore(f)
22
+ end
23
+ end
24
+
25
+ def commit!
26
+ File.open(@file_path, File::WRONLY | File::CREAT) do |f|
27
+ Marshal.dump(@hash, f)
28
+ end
29
+ end
30
+
31
+ def []=(key, value)
32
+ @hash[key] = value
33
+ @commit_count += 1
34
+ if @commit_count >= @commit_interval
35
+ @commit_count = 0
36
+ commit!
37
+ end
38
+ end
39
+
40
+ def method_missing(name, *params, &block)
41
+ @hash.send(name, *params, &block)
42
+ end
43
+ end
@@ -0,0 +1,107 @@
1
+ module Meimei
2
+ class Server
3
+ attr_accessor :socket, :hostname, :port, :is_connected, :logger, :autojoin
4
+
5
+ def initialize(hostname, port, autojoin, options = {})
6
+ @hostname, @port, @is_connected = hostname, port, false
7
+ @log_dir = options[:log_dir] || "."
8
+ @logger = Logger.new(open("#{@log_dir}/#{@hostname}-#{Time.now.strftime('%Y%m%d-%H%M')}.log", "w"))
9
+ @logger.datetime_format = "%Y-%m-%d %H:%M:%S"
10
+
11
+ case options[:log_level]
12
+ when :fatal
13
+ @logger.level = Logger::FATAL
14
+
15
+ when :error
16
+ @logger.level = Logger::ERROR
17
+
18
+ when :warn
19
+ @logger.level = Logger::WARN
20
+
21
+ when :info
22
+ @logger.level = Logger::INFO
23
+
24
+ when :debug
25
+ @logger.level = Logger::DEBUG
26
+
27
+ else
28
+ @logger.level = Logger::INFO
29
+ end
30
+
31
+ @autojoin = autojoin.split(/,\s*/)
32
+ end
33
+
34
+ def close
35
+ begin
36
+ @socket.close if @socket
37
+ rescue Exception => x
38
+ @is_connected = false
39
+ @logger.info "* Could not close socket"
40
+ x.dump(@logger)
41
+ end
42
+ end
43
+
44
+ def reconnect_delay_passed?
45
+ if @last_connected_at == nil
46
+ @last_connected_at = Time.now
47
+ return true
48
+ end
49
+
50
+ if Time.now - @last_connected_at > 10
51
+ @last_connected_at = Time.now
52
+ return true
53
+ else
54
+ return false
55
+ end
56
+ end
57
+
58
+ def connect
59
+ begin
60
+ self.close()
61
+ @socket = TCPSocket.open(@hostname, @port)
62
+ @is_connected = true
63
+ @logger.info "* Connected (resolved to #{@socket.peeraddr[3]})"
64
+ rescue Exception => x
65
+ @is_connected = false
66
+ @logger.info "* Connection failed"
67
+ x.dump(@logger)
68
+ end
69
+
70
+ return @is_connected
71
+ end
72
+
73
+ def read
74
+ begin
75
+ msg = @socket.gets
76
+ @logger.debug "> #{msg}"
77
+ return msg
78
+ rescue Errno::ECONNRESET => x
79
+ @logger.info "* Connection reset"
80
+ @is_connected = false
81
+ x.dump(@logger)
82
+ return nil
83
+ rescue Exception => x
84
+ @logger.info "* Read failed"
85
+ @is_connected = false
86
+ x.dump(@logger)
87
+ return nil
88
+ end
89
+ end
90
+
91
+ def write(msg)
92
+ @logger.debug "< #{msg}"
93
+
94
+ begin
95
+ @socket.write(msg + "\n")
96
+ rescue Errno::ECONNRESET => x
97
+ @logger.info "* Connection reset"
98
+ @is_connected = false
99
+ x.dump(@logger)
100
+ rescue Exception => x
101
+ @logger.info "* Write failed"
102
+ @is_connected = false
103
+ x.dump(@logger)
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,128 @@
1
+ require 'rubygems'
2
+ require 'rubygems/specification'
3
+ require 'rake'
4
+
5
+ module Meimei
6
+ # Add some additional items to Gem::Specification
7
+ # A Meimei::Specification adds additional pieces of information the
8
+ # typical gem specification
9
+ class Specification
10
+
11
+ RUBYFORGE_ROOT = "/var/www/gforge-projects/"
12
+
13
+ # user that accesses remote site
14
+ attr_accessor :remote_user
15
+
16
+ # remote host, default 'rubyforge.org'
17
+ attr_accessor :remote_host
18
+
19
+ # name the rdoc main
20
+ attr_accessor :rdoc_main
21
+
22
+ # local directory in development holding the generated rdoc
23
+ # default 'doc'
24
+ attr_accessor :local_rdoc_dir
25
+
26
+ # remote directory for storing rdoc, default 'doc'
27
+ attr_accessor :remote_rdoc_dir
28
+
29
+ # local directory for coverage report
30
+ attr_accessor :local_coverage_dir
31
+
32
+ # remote directory for storing coverage reports
33
+ # This defaults to 'coverage'
34
+ attr_accessor :remote_coverage_dir
35
+
36
+ # local directory for generated website, default +site/public+
37
+ attr_accessor :local_site_dir
38
+
39
+ # remote directory relative to +remote_root+ for the website.
40
+ # website.
41
+ attr_accessor :remote_site_dir
42
+
43
+ # is a .tgz to be created?, default 'true'
44
+ attr_accessor :need_tar
45
+
46
+ # is a .zip to be created, default 'true'
47
+ attr_accessor :need_zip
48
+
49
+
50
+ def initialize
51
+ @remote_user = nil
52
+ @remote_host = "rubyforge.org"
53
+
54
+ @rdoc_main = "README"
55
+ @local_rdoc_dir = "doc"
56
+ @remote_rdoc_dir = "doc"
57
+ @local_coverage_dir = "coverage"
58
+ @remote_coverage_dir = "coverage"
59
+ @local_site_dir = "site/public"
60
+ @remote_site_dir = "."
61
+
62
+ @need_tar = true
63
+ @need_zip = true
64
+
65
+ @spec = Gem::Specification.new
66
+
67
+ yield self if block_given?
68
+
69
+ # update rdoc options to take care of the rdoc_main if it is
70
+ # there, and add a default title if one is not given
71
+ if not @spec.rdoc_options.include?("--main") then
72
+ @spec.rdoc_options.concat(["--main", rdoc_main])
73
+ end
74
+
75
+ if not @spec.rdoc_options.include?("--title") then
76
+ @spec.rdoc_options.concat(["--title","'#{name} -- #{summary}'"])
77
+ end
78
+ end
79
+
80
+ # if this gets set then it overwrites what would be the
81
+ # rubyforge default. If rubyforge project is not set then use
82
+ # name. If rubyforge project and name are set, but they are
83
+ # different then assume that name is a subproject of the
84
+ # rubyforge project
85
+ def remote_root
86
+ if rubyforge_project.nil? or
87
+ rubyforge_project == name then
88
+ return RUBYFORGE_ROOT + "#{name}/"
89
+ else
90
+ return RUBYFORGE_ROOT + "#{rubyforge_project}/#{name}/"
91
+ end
92
+ end
93
+
94
+ # rdoc files is the same as what would be generated during gem
95
+ # installation. That is, everything in the require paths plus
96
+ # the rdoc_extra_files
97
+ #
98
+ def rdoc_files
99
+ flist = extra_rdoc_files.dup
100
+ @spec.require_paths.each do |rp|
101
+ flist << FileList["#{rp}/**/*.rb"]
102
+ end
103
+ flist.flatten.uniq
104
+ end
105
+
106
+ # calculate the remote directories
107
+ def remote_root_location
108
+ "#{remote_user}@#{remote_host}:#{remote_root}"
109
+ end
110
+
111
+ def remote_rdoc_location
112
+ remote_root_location + @remote_rdoc_dir
113
+ end
114
+
115
+ def remote_coverage_location
116
+ remote_root_loation + @remote_coverage_dir
117
+ end
118
+
119
+ def remote_site_location
120
+ remote_root_location + @remote_site_dir
121
+ end
122
+
123
+ # we delegate any other calls to spec
124
+ def method_missing(method_id,*params,&block)
125
+ @spec.send method_id, *params, &block
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,18 @@
1
+ module Meimei
2
+ class Version
3
+ MAJOR = 0
4
+ MINOR = 1
5
+ BUILD = 1
6
+
7
+ class << self
8
+ def to_a
9
+ [MAJOR, MINOR, BUILD]
10
+ end
11
+
12
+ def to_s
13
+ to_a.join(".")
14
+ end
15
+ end
16
+ end
17
+ VERSION = Version.to_s
18
+ end
data/lib/meimei.rb ADDED
@@ -0,0 +1,25 @@
1
+ module Meimei
2
+
3
+ ROOT_DIR = File.expand_path(File.join(File.dirname(__FILE__),".."))
4
+ LIB_DIR = File.join(ROOT_DIR,"lib").freeze
5
+ RESOURCE_DIR = File.join(ROOT_DIR,"resources").freeze
6
+
7
+ #
8
+ # Utility method to require all files ending in .rb in the directory
9
+ # with the same name as this file minus .rb
10
+ #
11
+ def require_all_libs_relative_to(fname)
12
+ prepend = File.basename(fname,".rb")
13
+ search_me = File.join(File.dirname(fname),prepend)
14
+
15
+ Dir.entries(search_me).each do |rb|
16
+ if File.extname(rb) == ".rb" then
17
+ require "#{prepend}/#{File.basename(rb,".rb")}"
18
+ end
19
+ end
20
+ end
21
+ module_function :require_all_libs_relative_to
22
+
23
+ end
24
+
25
+ Meimei.require_all_libs_relative_to(__FILE__)
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.2
3
+ specification_version: 1
4
+ name: meimei
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.1.1
7
+ date: 2008-02-06 00:00:00 -05:00
8
+ summary: A simple IRC bot framework.
9
+ require_paths:
10
+ - lib
11
+ email: r888888888@gmail.com
12
+ homepage: http://meimei.rubyforge.org/
13
+ rubyforge_project: meimei
14
+ description: Meimei is a simple IRC bot framework. It is designed to be easily extensible. In the interest of simplicity it only implements a subset RFC 1459, mainly those dealing with PRIVMSGs.
15
+ autorequire:
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - Albert Yi
31
+ files:
32
+ - CHANGES
33
+ - LICENSE
34
+ - Rakefile
35
+ - README
36
+ - lib/meimei/array_extensions.rb
37
+ - lib/meimei/client.rb
38
+ - lib/meimei/exception_dump.rb
39
+ - lib/meimei/gemspec.rb
40
+ - lib/meimei/persistent_hash.rb
41
+ - lib/meimei/server.rb
42
+ - lib/meimei/specification.rb
43
+ - lib/meimei/version.rb
44
+ - lib/meimei.rb
45
+ test_files: []
46
+
47
+ rdoc_options:
48
+ - --line-numbers
49
+ - --inline-source
50
+ - --main
51
+ - README
52
+ - --title
53
+ - "'meimei -- A simple IRC bot framework.'"
54
+ extra_rdoc_files:
55
+ - CHANGES
56
+ - LICENSE
57
+ - Rakefile
58
+ - README
59
+ executables: []
60
+
61
+ extensions: []
62
+
63
+ requirements: []
64
+
65
+ dependencies: []
66
+