For the XMPP interface for the Stack Overflow chat I am parsing the JSON feed from chat and generating Ruby objects for every chat events, such as messages sent, edits sent, users logging in or out, etc. I also generate events for "slash-commands" sent to the XMPP server, like "/help" or "/auth" in order to allow the XMPP user to authenticate with their Stack Overflow chat account.
I have set up these classes in a hierarchy I feel makes good logical sense:
class SOChatEvent # base class
|
|--- class SOXMPPEvent # base for all events that are initiated via XMPP
| |
| |--- class SOXMPPMessage # messages sent to the XMPP bridge via XMPP
| | |
| | |--- class SOXMPPMessageToRoom # messages sent from an XMPP user to an XMPP MUC
| | |
| | |--- class SOXMPPUserCommand # class for "slash commands", that is, messages starting
| | | | # with /, used for sending commands to the bridge
| | | |
| | | |--- class SOXMPPUserHelpCommand
| | | |--- class SOXMPPUserLoginCommand
| | | |--- class SOXMPPUserBroadcastCommand
|
|--- class SOChatRoomEvent # base class for all events that originate from an SO chat room
| |
| |--- class SOChatMessage # messages sent to an SO chat room via the SO chat system
| | |
| | |--- class SOChatMessageEdit # edits made to a prior SOChatMessage
| |
| |--- class SOChatUserEvent # events related to SO chat users
| | |
| | |--- class SOChatUserJoinRoom #Event for when a So user joins a room
| | |--- class SOChatUserLeaveRoom #Event for when a So user leaves a room
(etc)
You can see the full hierarchy and source in Trac or via SVN.
My question is twofold: First, what is the best way to instantiate these events? What I'm currently doing is parsing the JSON events using a giant switch
statement --well, it's ruby so it's a case
statement -- and, it's not giant yet, but it will be if I continue this way:
rooms.each do |room|
rid = "r"+"#{room.room_id}"
if !data[rid].nil?
@last_update = data[rid]['t'] if data[rid]['t']
if data[rid]["e"]
data[rid]["e"].each do |e|
puts "DEBUG: found an event: #{e.inspect}"
case e["event_type"]
when 1
event = SOChatMessage.new(room,e['user_name'])
event.encoded_body = e['content']
event.server = @server
events.push event
when 2
event = SOChatMessageEdit.new(room,e['user_name'])
event.encoded_body = e['content']
event.server = @server
events.push event
when 3
user = SOChatUser.new(e['user_id'], e['user_name'])
event = SOChatUserJoinRoom.new(room,user)
event.server = @server
events.push event
when 4
user = SOChatUser.new(e['user_id'], e['user_name'])
event = SOChatUserLeaveRoom.new(room,user)
event.server = @server
events.push event
end
end
end
end
end
But I imagine there has to be a better way to handle that! Something like SOChatEvent.createFromJSON( json_data )
... But, what's the best way to structure my code so that objects of the proper subclass are created in response to a given event_type
?
Second, I'm not actually using ant subclasses of SOXMPPUserCommand
yet. Right now all commands are just instances of SOXMPPUserCommand
itself, and that class has a single execute
method which switches based upon regex of the command. Much the same problem -- I know there's a better way, I just am not sure what the best way is:
def handle_message(msg)
puts "Room \"#{@name}\" handling message: #{msg}"
puts "message: from #{msg.from} type #{msg.type} to #{msg.to}: #{msg.body.inspect}"
event = nil
if msg.body =~ /\/.*/
#puts "DEBUG: Creating a new SOXMPPUserCommand"
event = SOXMPPUserCommand.new(msg)
else
#puts "DEBUG: Creating a new SOXMPPMessageToRoom"
event = SOXMPPMessageToRoom.new(msg)
end
if !event.nil?
event.user = get_soxmpp_user_by_jid event.from
handle_event event
end
end
and:
class SOXMPPUserCommand < SOXMPPMessage
def execute
case @body
when "/help"
"Available topics are: help auth /fkey /cookie\n\nFor information on a topic, send: /help <topic>"
when "/help auth"
"To use this system, you must send your StackOverflow chat cookie and fkey to the system. To do this, use the /fkey and /cookie commands"
when "/help /fkey"
"Usage: /fkey <fkey>. Displays or sets your fkey, used for authentication. Send '/fkey' alone to display your current fkey, send '/fkey <something>' to set your fkey to <something>. You can obtain your fkey via the URL: javascript:alert(fkey().fkey)"
when "/help /cookie"
"Usage: /cookie <cookie>. Displays or sets your cookie, used for authentication. Send '/cookie' alone to display your current fkey, send '/cookie <something>' to set your cookie to <something>"
when /\/fkey( .*)?/
if $1.nil?
"Your fkey is \"#{@user.fkey}\""
else
@user.fkey = $1.strip
if @user.authenticated?
"fkey set to \"#{@user.fkey}\". You are now logged in and can send messages to the chat"
else
"fkey set to \"#{@user.fkey}\". You must also send your cookie with /cookie before you can chat"
end
end
when /\/cookie( .*)?/
if $1.nil?
"Your cookie is: \"#{@user.cookie}\""
else
if $1 == " chocolate chip"
"You get a chocolate chip cookie!"
else
@user.cookie = $1.strip
if @user.authenticated?
"cookie set to \"#{@user.cookie}\". You are now logged in and can send messages to the chat"
else
"cookie set to \"#{@user.cookie}\". You must also send your fkey with /fkey before you can chat"
end
end
end
else
"Unknown Command \"#{@body}\""
end
end
end
I know there's a better way to do this, just not sure what specifically it is. Should the responsibility of creating subclasses of SOXMPPUserCommand
fall on SOXMPPUserCommand
itself? Should all subclasses register with the parent? Do I need a new class?
What's the best way to instantiate objects of subclasses in such a hierarchal structure?