Class: Meimei::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/meimei/client.rb

Instance Method Summary collapse

Constructor Details

#initialize(nick, options = {}) ⇒ Client

Parameters

  • nick: The nickname your bot will use.

  • options: The username of your bot (defaults to nick).

  • options: The real name of your bot (defaults to nick).

  • options: The password for your bot’s account.

  • options: A path to the directory where Meimei plugins will be stored (defaults to plugins).

  • options: A path to where log files will be stored (defaults to .).

  • options: The log level. Can be: :error, :info, :debug. Defaults to :info.



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/meimei/client.rb', line 16

def initialize(nick, options = {})
  @servers = []
  @running = false
  @command_plugins = []
  @timer_plugins = []
  @nick = nick
  @username = options[:username] || @nick
  @realname = options[:realname] || @nick
  @password = options[:password]
  @plugin_dir = options[:plugin_dir] || "plugins"
  @log_dir = options[:log_dir] || "."
  @log_level = options[:log_level] || :info
  @logger = Logger.new(open("#{@log_dir}/#{@nick}-#{Time.now.strftime('%Y%m%d-%H%M')}.log", "w"))
  @logger.datetime_format = "%Y-%m-%d %H:%M:%S"
  @last_saw_traffic_at = Time.now
end

Instance Method Details

#add_server(hostname, port, autojoin) ⇒ Object

Registers a server with the client. This method will not open a connection.



34
35
36
# File 'lib/meimei/client.rb', line 34

def add_server(hostname, port, autojoin)
  @servers << Server.new(hostname, port, autojoin, :log_dir => @log_dir, :log_level => @log_level)
end

#autojoin(server) ⇒ Object



218
219
220
221
222
# File 'lib/meimei/client.rb', line 218

def autojoin(server)
  server.autojoin.each do |channel|
    server.write("JOIN #{channel}")
  end
end

#autojoin_on_recognize(server, msg) ⇒ Object



212
213
214
215
216
# File 'lib/meimei/client.rb', line 212

def autojoin_on_recognize(server, msg)
  if msg =~ /^:\S+ MODE #{@nick} :\+r/
    autojoin(server)
  end
end

#autopingObject

Autopings any server that hasn’t seen traffic in over 5 minutes



50
51
52
53
54
55
56
57
58
59
# File 'lib/meimei/client.rb', line 50

def autoping
  @servers.each do |server|
    if server.last_saw_traffic_at + 300 < Time.now
      server.write("PING #{server.hostname}")
      
      # Artificially set the last_saw_traffic_at value to now so that we don't flood the server
      server.last_saw_traffic_at = Time.now
    end
  end
end

#check_command_plugins(msg) ⇒ Object

Run all command plugins.



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/meimei/client.rb', line 118

def check_command_plugins(msg)
  @command_plugins.each do |name, block, options|
    begin
      if name =~ /^!/
        if msg =~ /^#{name}\b/
          msg = msg.gsub(/!\S+\s*/, "")
          block.call(msg)
        end
      else
        block.call(msg)
      end
    rescue Exception => e
      reply "Error: #{e.class}"
      e.dump(@current_server.logger)
    end
  end
end

#check_timer_pluginsObject

Run all timer plugins.



97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/meimei/client.rb', line 97

def check_timer_plugins
  @timer_plugins.each do |plugin|
    begin
      # Need to refer directly to the array since we'll be changing its values
      # plugin[0]: name
      # plugin[1]: interval
      # plugin[2]: next_run_at
      # plugin[3]: block
      # plugin[4]: options
      
      if plugin[2] < Time.now
        plugin[3].call()
        plugin[2] = Time.now + plugin[1]
      end
    rescue Exception => e
      e.dump(@logger)
    end
  end
end

#define_plugin(name, options = {}, &block) ⇒ Object

Creates a new plugin.

Parameters

  • 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.

  • options: Insert plugin at position n (lower number positions will be run first).



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/meimei/client.rb', line 66

def define_plugin(name, options = {}, &block)
  # TODO: Is there a memory leak here?
  
  case name
  when /^@(\d+)([mh])?/
    interval = $1.to_i
            
    if $2 == "m"
      interval = interval * 60
    elsif $2 == "h"
      interval = interval * 60 * 60
    end
    
    plugins = @timer_plugins
    plugin = [name, interval, Time.now, block, options]
    
  else
    plugins = @command_plugins
    plugin = [name, block, options]
  end

  if options[:position]
    plugins.insert(options[:position], plugin)
  else
    plugins << plugin
  end
  
  plugins.compact!
end

#load_pluginsObject

Reloads all plugins.



39
40
41
42
43
44
45
46
47
# File 'lib/meimei/client.rb', line 39

def load_plugins
  @command_plugins.clear()
  @timer_plugins.clear()

  # TODO: This is hackish
  Dir["#{@plugin_dir}/*.rb"].sort.each do |file|
    eval(File.open(file, "r").read, binding, file)
  end
end

#process_message(msg) ⇒ Object

Parses a messages and dispatches as necessary.



225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/meimei/client.rb', line 225

def process_message(msg)
  @current_from = nil
  @current_to = nil
  @current_msg = nil
  
  case msg
  when /^:(.+?)!(.+?)@(\S+) PRIVMSG (\S+) :(.+)$/
    @current_from = $1
    @current_to = $4
    @current_msg = $5.strip

    self.check_command_plugins(@current_msg)
    
  when /^PING (.+)$/
    @current_server.write("PONG #{$1}")
  end
end

#quit(msg = nil) ⇒ Object

Quits all clients and ends the event loop.



182
183
184
185
186
187
188
189
190
191
192
# File 'lib/meimei/client.rb', line 182

def quit(msg = nil)
  @servers.each do |server|
    if msg
      server.write("QUIT :#{msg}")
    else
      server.write("QUIT")
    end
  end
  
  @running = false
end

#reply(msg, with_nick = true) ⇒ Object

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.



195
196
197
198
199
200
201
# File 'lib/meimei/client.rb', line 195

def reply(msg, with_nick = true)
  if with_nick
    @current_server.write("PRIVMSG #{@current_to} :#{@current_from}: #{msg}")
  else
    @current_server.write("PRIVMSG #{@current_to} :#{msg}")
  end
end

#say(server, to, msg) ⇒ Object

Send msg to to on server. to can either be a channel or someone’s nick.



204
205
206
207
208
209
210
# File 'lib/meimei/client.rb', line 204

def say(server, to, msg)
  if server.is_a?(String)
    server = @servers.find {|x| x.hostname == server}
  end
  
  server.write("PRIVMSG #{to} :#{msg}")
end

#selectObject

Returns the first server that has data to be read.



137
138
139
140
141
142
143
144
145
146
147
# File 'lib/meimei/client.rb', line 137

def select
  sockets = @servers.map {|x| x.socket}.compact
  socket = Kernel.select(sockets, nil, nil, 1)
  
  if socket == nil
    return nil
  else
    socket = socket[0][0]
    return @servers.find {|x| x.socket.__id__ == socket.__id__}
  end
end

#startObject

Starts the main event loop.



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/meimei/client.rb', line 150

def start
  self.load_plugins()
  @running = true
  
  while @running
    @servers.each do |server|
      unless server.is_connected
        if server.reconnect_delay_passed? && server.connect
          server.write("PASS #{@password}") if @password
          server.write("NICK #{@nick}")
          server.write("USER #{@username} hostname servername :#{@username}")
        end
      end

      autoping()
      check_timer_plugins()
      
      @current_server = self.select()

      if @current_server
        msg = @current_server.read
        
        if msg != nil
          autojoin_on_recognize(server, msg)
          process_message(msg)
        end
      end
    end        
  end
end