class Mircruby
Constants
- BRIDGES_KEY
- BRIDGE_NAME_PATTERN
- INBOX_KEY
- IRC_RESET
- MIRC_FILENAME
- PLUGIN
- RUBY_FILENAME
- STYLE_NICK
- STYLE_TITLE
- UserReply
- VERSION
Public Class Methods
Source
# File plugins/mircruby.rb, line 295 def self.bridge_mirc_source template = <<~'MIRC' ; Rabbot mIRC Ruby bridge v__VERSION__ ; /mircruby register <name> — register local bridge ; /mircruby update — fetch latest bridge files from Rabbot alias mircruby { if (!$1) { echo -a Usage: /mircruby <command> | return } var %cmd $+(!mircruby , $1-) msg $mircruby_chan %cmd } alias mircruby_chan { return #yourchannel } alias mircruby_update { msg $mircruby_chan !mircruby update msg $mircruby_chan !mircruby update ruby msg $mircruby_chan !mircruby update mirc } MIRC template.gsub("__VERSION__", VERSION) end
Source
# File plugins/mircruby.rb, line 165 def self.bridge_owner?(bridge, owner) Normalize.user_key(bridge["owner"]) == Normalize.user_key(owner) end
Source
# File plugins/mircruby.rb, line 260 def self.bridge_ruby_source template = <<~'RUBY' # frozen_string_literal: true # Rabbot mIRC Ruby bridge v__VERSION__ # Save as mircruby_bridge.rb and run: ruby mircruby_bridge.rb <server> <port> <nick> <channel> <bridge> require "socket" module RabbotMircrubyBridge VERSION = "__VERSION__" def self.send_command(server, port, nick, channel, command) socket = TCPSocket.new(server, port.to_i) socket.puts "NICK #{nick}" socket.puts "USER mircruby 0 * :mircruby bridge" socket.puts "JOIN #{channel}" sleep 1 socket.puts "PRIVMSG #{channel} :!mircruby #{command}" sleep 1 socket.close end end if $PROGRAM_NAME == __FILE__ argv = ARGV raise "usage: ruby mircruby_bridge.rb <server> <port> <nick> <channel> <bridge> [command]" if argv.length < 5 server, port, nick, channel, bridge = argv[0, 5] command = argv[5..]&.join(" ") || "ping #{bridge}" RabbotMircrubyBridge.send_command(server, port, nick, channel, command) end RUBY template.gsub("__VERSION__", VERSION) end
Source
# File plugins/mircruby.rb, line 30 def self.command_pattern /mircruby(?: (.+))?$/i end
Source
# File plugins/mircruby.rb, line 535 def self.dispatch(args, from:, network:, channel:, store: RabbotDb) parsed = parse_command(args) return { reply: format_error_reply(parsed[:error]) } if parsed[:error] case parsed[:action] when :help { reply: format_help_reply } when :update handle_update(parsed, from: from) when :info_download { reply: format_info_download_reply } else return { reply: format_channel_required_reply } if channel.nil? || channel.to_s.strip.empty? handle_channel_action(parsed, from: from, network: network, channel: channel, store: store) end end
Source
# File plugins/mircruby.rb, line 161 def self.find_bridge(bridges, name) bridges[name.to_s.downcase] end
Source
# File plugins/mircruby.rb, line 392 def self.format_channel_required_reply format_error_reply("Use !mircruby in a channel.") end
Source
# File plugins/mircruby.rb, line 385 def self.format_error_reply(message) [ IrcFormat.tag("MIRCRUBY"), message.to_s ].join("\n") end
Source
# File plugins/mircruby.rb, line 376 def self.format_help_reply [ IrcFormat.report_header(tag: "MIRCRUBY", title: "mIRC Ruby bridge", style: STYLE_TITLE), usage_message, "Local Ruby class + mIRC script relay games and system info through Rabbot.", IrcFormat.report_footer("update", "register", "relay") ].join("\n") end
Source
# File plugins/mircruby.rb, line 475 def self.format_info_download_reply [ IrcFormat.report_header(tag: "MIRCRUBY", title: "bridge download", style: STYLE_TITLE), "mircruby_bridge.rb — !mircruby update ruby", "mIRC script — !mircruby update mirc", "Checksums — !mircruby update", IrcFormat.report_footer("info only", "save files locally from IRC") ].join("\n") end
Source
# File plugins/mircruby.rb, line 485 def self.format_info_reply(network:, channel:, name:, store: RabbotDb) bridges = load_bridges(network: network, channel: channel, store: store) bridge = find_bridge(bridges, name) return format_error_reply("Bridge #{name} is not registered.") unless bridge info = bridge.fetch("info", {}) if info.empty? return [ IrcFormat.report_header(tag: "MIRCRUBY", title: "no info yet", style: STYLE_TITLE), "Bridge #{name} has not reported system info.", IrcFormat.report_footer("owner: !mircruby info #{name} host=... os=...") ].join("\n") end lines = info.sort.map { |key, value| "#{key} — #{value}" } [ IrcFormat.report_header(tag: "MIRCRUBY", title: name, style: STYLE_TITLE), *lines, IrcFormat.report_footer(IrcFormat.nick(bridge["owner"])) ].join("\n") end
Source
# File plugins/mircruby.rb, line 507 def self.format_info_updated_reply(name:, info:) lines = info.sort.map { |key, value| "#{key} — #{value}" } [ IrcFormat.report_header(tag: "MIRCRUBY", title: "info updated", style: STYLE_TITLE), "Stored for bridge #{name}.", *lines, IrcFormat.report_footer("system info bridge") ].join("\n") end
Source
# File plugins/mircruby.rb, line 413 def self.format_list_reply(bridges:) names = bridges.keys.sort if names.empty? return [ IrcFormat.report_header(tag: "MIRCRUBY", title: "no bridges", style: STYLE_TITLE), "No bridges registered in this channel.", IrcFormat.report_footer("!mircruby register <name>") ].join("\n") end lines = names.map do |name| bridge = bridges[name] owner = bridge["owner"] ping = bridge["last_ping"] meta = [IrcFormat.nick(owner)] meta << "ping #{ping}" if ping "#{name} — #{meta.join(' · ')}" end [ IrcFormat.report_header(tag: "MIRCRUBY", title: "#{names.length} bridge#{'s' if names.length != 1}", style: STYLE_TITLE), *lines, IrcFormat.report_footer("relay", "info") ].join("\n") end
Source
# File plugins/mircruby.rb, line 439 def self.format_ping_reply(name:, at:) [ IrcFormat.report_header(tag: "MIRCRUBY", title: "bridge alive", style: STYLE_TITLE), "#{name} pinged at #{at}.", IrcFormat.report_footer("poll for queued messages") ].join("\n") end
Source
# File plugins/mircruby.rb, line 455 def self.format_poll_reply(name:, messages:) if messages.empty? return [ IrcFormat.report_header(tag: "MIRCRUBY", title: "inbox empty", style: STYLE_TITLE), "No relay messages waiting for #{name}.", IrcFormat.report_footer("poll again later") ].join("\n") end body = messages.map do |entry| "#{IrcFormat.nick(entry['from'])}: #{entry['message']}" end [ IrcFormat.report_header(tag: "MIRCRUBY", title: "#{messages.length} message#{'s' if messages.length != 1}", style: STYLE_TITLE), *body, IrcFormat.report_footer(name) ].join("\n") end
Source
# File plugins/mircruby.rb, line 396 def self.format_register_reply(name:, owner:) [ IrcFormat.report_header(tag: "MIRCRUBY", title: "bridge registered", style: STYLE_TITLE), "Bridge #{name} owned by #{IrcFormat.nick(owner)}.", "Run !mircruby update to fetch the local Ruby bridge and mIRC script.", IrcFormat.report_footer("relay", "poll", "info") ].join("\n") end
Source
# File plugins/mircruby.rb, line 447 def self.format_relay_queued_reply(name:) [ IrcFormat.report_header(tag: "MIRCRUBY", title: "message queued", style: STYLE_TITLE), "Relay queued for bridge #{name}.", IrcFormat.report_footer("owner polls with !mircruby poll") ].join("\n") end
Source
# File plugins/mircruby.rb, line 405 def self.format_unregister_reply(name:) [ IrcFormat.report_header(tag: "MIRCRUBY", title: "bridge removed", style: STYLE_TITLE), "Bridge #{name} unregistered.", IrcFormat.report_footer("register again anytime") ].join("\n") end
Source
# File plugins/mircruby.rb, line 352 def self.format_update_delivery_reply(label:, filename:, checksum:) [ IrcFormat.report_header(tag: "MIRCRUBY", title: "#{label} v#{VERSION}", style: STYLE_TITLE), "Sending #{filename} via DCC · checksum #{checksum}", IrcFormat.report_footer("save locally", "mircruby bridge") ].join("\n") end
Source
# File plugins/mircruby.rb, line 527 def self.format_update_source(label:, source:) [ IrcFormat.report_header(tag: "MIRCRUBY", title: "#{label} v#{VERSION}", style: STYLE_TITLE), source, IrcFormat.report_footer("#{label} · checksum #{source_checksum(source)}") ].join("\n") end
Source
# File plugins/mircruby.rb, line 517 def self.format_update_summary [ IrcFormat.report_header(tag: "MIRCRUBY", title: "bridge update v#{VERSION}", style: STYLE_TITLE), "ruby — #{ruby_checksum}", "mirc — #{mirc_checksum}", "Fetch: !mircruby update ruby | !mircruby update mirc", IrcFormat.report_footer("local bridge communicates with Rabbot on IRC") ].join("\n") end
Source
# File plugins/mircruby.rb, line 573 def self.handle_channel_action(parsed, from:, network:, channel:, store:) case parsed[:action] when :register result = register_bridge( network: network, channel: channel, name: parsed[:name], owner: from, store: store ) return { reply: format_error_reply(result[:error]) } if result[:error] { reply: format_register_reply(name: result[:name], owner: from) } when :unregister result = unregister_bridge( network: network, channel: channel, name: parsed[:name], owner: from, store: store ) return { reply: format_error_reply(result[:error]) } if result[:error] { reply: format_unregister_reply(name: result[:name]) } when :list bridges = load_bridges(network: network, channel: channel, store: store) { reply: format_list_reply(bridges: bridges) } when :ping result = ping_bridge( network: network, channel: channel, name: parsed[:name], owner: from, store: store ) return { reply: format_error_reply(result[:error]) } if result[:error] { reply: format_ping_reply(name: result[:name], at: result[:at]) } when :poll result = poll_relay_messages( network: network, channel: channel, name: parsed[:name], owner: from, store: store ) return { reply: format_error_reply(result[:error]) } if result[:error] { reply: format_poll_reply(name: result[:name], messages: result[:messages]) } when :relay result = queue_relay_message( network: network, channel: channel, name: parsed[:name], from: from, message: parsed[:message], store: store ) return { reply: format_error_reply(result[:error]) } if result[:error] { reply: format_relay_queued_reply(name: result[:name]) } when :info if parsed[:fields] result = store_bridge_info( network: network, channel: channel, name: parsed[:name], owner: from, fields: parsed[:fields], store: store ) return { reply: format_error_reply(result[:error]) } if result[:error] { reply: format_info_updated_reply(name: result[:name], info: result[:info]) } else { reply: format_info_reply(network: network, channel: channel, name: parsed[:name], store: store) } end else { reply: format_error_reply(usage_message) } end end
Source
# File plugins/mircruby.rb, line 553 def self.handle_update(parsed, from:) case parsed[:target] when :summary { reply: format_update_summary } when :ruby, :mirc delivery = prepare_update_delivery(target: parsed[:target], nick: from) { reply: delivery[:reply], dcc: { target: parsed[:target], filename: delivery[:filename], source: delivery[:source] }, private_message: delivery[:private_message] } else { reply: format_error_reply(usage_message) } end end
Source
# File plugins/mircruby.rb, line 117 def self.invalid_name_message "Bridge name must be 2-16 letters, digits, or underscores." end
Source
# File plugins/mircruby.rb, line 121 def self.load_bridges(network:, channel:, store: RabbotDb) store.get_plugin_json( plugin: PLUGIN, key: BRIDGES_KEY, network: network, channel: channel, default: {} ) end
Source
# File plugins/mircruby.rb, line 141 def self.load_inbox(network:, channel:, store: RabbotDb) store.get_plugin_json( plugin: PLUGIN, key: INBOX_KEY, network: network, channel: channel, default: {} ) end
Source
# File plugins/mircruby.rb, line 321 def self.mirc_checksum @mirc_checksum ||= source_checksum(bridge_mirc_source) end
Source
# File plugins/mircruby.rb, line 52 def self.parse_command(args) text = args.to_s.strip return { action: :help } if text.empty? || text.casecmp?("help") if text.casecmp?("update") return { action: :update, target: :summary } end if (match = text.match(/\Aupdate\s+(ruby|mirc)\z/i)) return { action: :update, target: match[1].downcase.to_sym } end return { action: :list } if text.casecmp?("list") if (match = text.match(/\Aregister\s+(\S+)\z/i)) name = match[1].downcase return { error: invalid_name_message } unless valid_bridge_name?(name) return { action: :register, name: name } end if (match = text.match(/\Aunregister\s+(\S+)\z/i)) name = match[1].downcase return { error: invalid_name_message } unless valid_bridge_name?(name) return { action: :unregister, name: name } end if (match = text.match(/\Aping\s+(\S+)\z/i)) name = match[1].downcase return { error: invalid_name_message } unless valid_bridge_name?(name) return { action: :ping, name: name } end if (match = text.match(/\Apoll\s+(\S+)\z/i)) name = match[1].downcase return { error: invalid_name_message } unless valid_bridge_name?(name) return { action: :poll, name: name } end if (match = text.match(/\Arelay\s+(\S+)\s+(.+)\z/i)) name = match[1].downcase return { error: invalid_name_message } unless valid_bridge_name?(name) message = match[2].to_s.strip return { error: "Relay message cannot be empty." } if message.empty? return { action: :relay, name: name, message: message } end return { action: :info_download } if text.casecmp?("info") if (match = text.match(/\Ainfo\s+(\S+)(?:\s+(.+))?\z/i)) name = match[1].downcase return { error: invalid_name_message } unless valid_bridge_name?(name) fields = parse_info_fields(match[2]) return { action: :info, name: name, fields: fields.empty? ? nil : fields } end { error: usage_message } end
Source
# File plugins/mircruby.rb, line 44 def self.parse_info_fields(text) fields = {} text.to_s.scan(/([a-z][a-z0-9_]*)=([^\s]+)/i) do |key, value| fields[key.downcase] = value end fields end
Source
# File plugins/mircruby.rb, line 203 def self.ping_bridge(network:, channel:, name:, owner:, store: RabbotDb) bridges = load_bridges(network: network, channel: channel, store: store) key = name.to_s.downcase bridge = find_bridge(bridges, key) return { error: "Bridge #{key} is not registered." } unless bridge return { error: "Only the bridge owner can ping it." } unless bridge_owner?(bridge, owner) bridge["last_ping"] = RabbotDb.iso_time save_bridges(network: network, channel: channel, bridges: bridges, store: store) { ok: true, name: key, at: bridge["last_ping"] } end
Source
# File plugins/mircruby.rb, line 243 def self.poll_relay_messages(network:, channel:, name:, owner:, store: RabbotDb) bridges = load_bridges(network: network, channel: channel, store: store) key = name.to_s.downcase bridge = find_bridge(bridges, key) return { error: "Bridge #{key} is not registered." } unless bridge return { error: "Only the bridge owner can poll messages." } unless bridge_owner?(bridge, owner) inbox = load_inbox(network: network, channel: channel, store: store) messages = Array(inbox.delete(key)) save_inbox(network: network, channel: channel, inbox: inbox, store: store) { ok: true, name: key, messages: messages } end
Source
# File plugins/mircruby.rb, line 360 def self.prepare_update_delivery(target:, nick:) label = target.to_s source = update_source(target) filename = update_filename(target) checksum = source_checksum(source) { label: label, filename: filename, source: source, checksum: checksum, nick: Normalize.nick(nick), reply: format_update_delivery_reply(label: label, filename: filename, checksum: checksum), private_message: format_update_source(label: label, source: source) } end
Source
# File plugins/mircruby.rb, line 227 def self.queue_relay_message(network:, channel:, name:, from:, message:, store: RabbotDb) bridges = load_bridges(network: network, channel: channel, store: store) key = name.to_s.downcase return { error: "Bridge #{key} is not registered." } unless find_bridge(bridges, key) inbox = load_inbox(network: network, channel: channel, store: store) inbox[key] ||= [] inbox[key] << { "from" => Normalize.nick(from), "message" => message.to_s, "at" => RabbotDb.iso_time } save_inbox(network: network, channel: channel, inbox: inbox, store: store) { ok: true, name: key } end
Source
# File plugins/mircruby.rb, line 169 def self.register_bridge(network:, channel:, name:, owner:, store: RabbotDb) bridges = load_bridges(network: network, channel: channel, store: store) key = name.to_s.downcase if bridges.key?(key) return { error: "Bridge #{key} is already registered." } end bridges[key] = { "owner" => Normalize.nick(owner), "registered_at" => RabbotDb.iso_time, "last_ping" => nil, "info" => {} } save_bridges(network: network, channel: channel, bridges: bridges, store: store) { ok: true, name: key } end
Source
# File plugins/mircruby.rb, line 317 def self.ruby_checksum @ruby_checksum ||= source_checksum(bridge_ruby_source) end
Source
# File plugins/mircruby.rb, line 131 def self.save_bridges(network:, channel:, bridges:, store: RabbotDb) store.set_plugin_json( plugin: PLUGIN, key: BRIDGES_KEY, network: network, channel: channel, value: bridges ) end
Source
# File plugins/mircruby.rb, line 151 def self.save_inbox(network:, channel:, inbox:, store: RabbotDb) store.set_plugin_json( plugin: PLUGIN, key: INBOX_KEY, network: network, channel: channel, value: inbox ) end
Source
# File plugins/mircruby.rb, line 256 def self.source_checksum(text) Digest::SHA256.hexdigest(text.to_s)[0, 12] end
Source
# File plugins/mircruby.rb, line 215 def self.store_bridge_info(network:, channel:, name:, owner:, fields:, store: RabbotDb) bridges = load_bridges(network: network, channel: channel, store: store) key = name.to_s.downcase bridge = find_bridge(bridges, key) return { error: "Bridge #{key} is not registered." } unless bridge return { error: "Only the bridge owner can set info." } unless bridge_owner?(bridge, owner) bridge["info"] = bridge.fetch("info", {}).merge(fields.transform_keys(&:downcase)) save_bridges(network: network, channel: channel, bridges: bridges, store: store) { ok: true, name: key, info: bridge["info"] } end
Source
# File plugins/mircruby.rb, line 186 def self.unregister_bridge(network:, channel:, name:, owner:, store: RabbotDb) bridges = load_bridges(network: network, channel: channel, store: store) key = name.to_s.downcase bridge = find_bridge(bridges, key) return { error: "Bridge #{key} is not registered." } unless bridge return { error: "Only the bridge owner can unregister it." } unless bridge_owner?(bridge, owner) bridges.delete(key) save_bridges(network: network, channel: channel, bridges: bridges, store: store) inbox = load_inbox(network: network, channel: channel, store: store) inbox.delete(key) save_inbox(network: network, channel: channel, inbox: inbox, store: store) { ok: true, name: key } end
Source
# File plugins/mircruby.rb, line 325 def self.update_filename(target) case target.to_sym when :ruby then RUBY_FILENAME when :mirc then MIRC_FILENAME else raise ArgumentError, "unknown update target: #{target}" end end
Source
# File plugins/mircruby.rb, line 334 def self.update_source(target) case target.to_sym when :ruby then bridge_ruby_source when :mirc then bridge_mirc_source else raise ArgumentError, "unknown update target: #{target}" end end
Source
# File plugins/mircruby.rb, line 36 def self.usage_message "Usage: !mircruby [update [ruby|mirc]|register <name>|unregister <name>|list|ping <name>|poll <name>|relay <name> <msg>|info [download|<name> [key=val ...]]]" end
Source
# File plugins/mircruby.rb, line 40 def self.valid_bridge_name?(name) name.to_s.match?(BRIDGE_NAME_PATTERN) end
Source
# File plugins/mircruby.rb, line 343 def self.write_update_file(target:, dir:) filename = update_filename(target) source = update_source(target) FileUtils.mkdir_p(dir) path = File.join(dir, filename) File.write(path, source) { path: path, filename: filename, source: source } end
Public Instance Methods
Source
# File plugins/mircruby.rb, line 675 def deliver_private_update(m, text) return if text.nil? || text.to_s.strip.empty? themed_flood_safe_reply(UserReply.new(m.user), text) end
Source
# File plugins/mircruby.rb, line 661 def deliver_update_file(m, result) dcc = result[:dcc] return unless dcc dir = Dir.mktmpdir("mircruby-update") written = self.class.write_update_file(target: dcc[:target], dir: dir) m.user.dcc_send(File.open(written[:path], "rb"), written[:filename]) rescue StandardError => e bot.loggers.debug "mircruby DCC failed: #{e.message}" deliver_private_update(m, result[:private_message]) ensure FileUtils.remove_entry(dir) if defined?(dir) && dir && File.directory?(dir) end
Source
# File plugins/mircruby.rb, line 681 def execute(m, args) result = self.class.dispatch( args, from: m.user.nick, network: bot.config.server, channel: m.channel&.name ) themed_flood_safe_reply(m, result[:reply]) deliver_update_file(m, result) if result[:dcc] end