Permalink
Browse files

initial commit

  • Loading branch information...
0 parents commit 32da17faf683e5bb2f426eb669992cb3e362ece2 @fictorial committed May 26, 2011
Showing with 477 additions and 0 deletions.
  1. +3 −0 .gitignore
  2. +5 −0 .npmignore
  3. +11 −0 Makefile
  4. +75 −0 README.md
  5. +16 −0 package.json
  6. +55 −0 src/chat-client-cli.coffee
  7. +145 −0 src/chat-client.coffee
  8. +167 −0 src/chat-server.coffee
@@ -0,0 +1,3 @@
+lib
+bin
+node_modules
@@ -0,0 +1,5 @@
+.DS_Store
+.git*
+*.md
+Makefile
+src/
@@ -0,0 +1,11 @@
+compile:
+ coffee -c -o lib src/chat-client.coffee
+ coffee -c -o bin src/chat-server.coffee
+
+npm: compile
+ npm publish
+
+clean:
+ rm -rf lib bin
+
+.PHONY: compile npm clean
@@ -0,0 +1,75 @@
+# chat-server
+
+A multi-user chat server.
+
+## Installation
+
+ npm i chat-server
+
+## Running
+
+ chat-server [OPTIONS]
+
+ Options:
+ -h ip ip to bind to default: 127.0.0.1
+ -p port port to bind to default: 11746
+ -m bytes max input size per client default: 1MB
+ -l level log level default: error
+
+## Client Usage
+
+````javascript
+var client = require('chat-client').connect();
+client.join('jokes');
+client.say('jokes', 'what did the hat say to the hat rack?');
+client.on('say', function (channel, who, message) {
+ console.log(new Date(), channel, who, message);
+});
+````
+
+## Protocol
+
+The client/server protocol is based on CRLF-delimited JSON objects.
+
+### Client Requests
+
+ { "nick": "unique-nickname" }
+ { "list": "channel-prefix" }
+ { "join": "channel-name" }
+ { "members": "channel-name" }
+ { "say": "channel-name", "what": "message" }
+ { "part": "channel-name", "why": "reason" }
+ { "info": true }
+
+### Server Responses
+
+A subset of client requests elicit a response.
+
+ { "error": "error-message" }
+ { "list": ["channel-name", ...] }
+ { "members": "channel-name", "nicks": ["nick", ...] }
+ { "info": { "channels": int, "users": int, "uptime": int } }
+
+### Server Notifications
+
+Something happened server-side that the client might be interested in.
+
+ { "join": "channel-name", "who": "nick" }
+ { "part": "channel-name", "who": "nick", "why": "reason" }
+ { "say": "channel-name", "who": "nick", "what": "message" }
+ { "nick": "channel-name", "old_nick": "nick", "new_nick": "new-nick" }
+
+## Notes
+
+Any client wrongdoing results in an immediate disconnection.
+
+This server does not connect to other servers like a IRC network.
+
+## Author
+
+Brian Hammond <brian@fictorial.com> (http://fictorial.com)
+
+## License
+
+MIT
+
@@ -0,0 +1,16 @@
+{
+ "name": "chat-server",
+ "version": "0.0.1",
+ "description": "Everyone has a chat server and this one is mine",
+ "keywords": [ "chat", "server", "irc" ],
+ "author": "Brian Hammond <brian@fictorial.com> (http://fictorial.com)",
+ "main": "./lib/chat-server.js",
+ "bin": { "chat-server": "./bin/chat-server.js" },
+ "repository": { "type": "git", "url": "https://github.com/fictorial/chat-server.git" },
+ "dependencies": {
+ "json-line-protocol": "0.x.x",
+ "optimist": "0.x.x",
+ "log": "1.x.x"
+ },
+ "engines": { "node": ">= 0.4.7" }
+}
@@ -0,0 +1,55 @@
+#!/usr/bin/env coffee
+
+# chat client command-line interface.
+
+client = (require './chat-client').connect()
+
+fatal_error_handler = (error) ->
+ console.error 'fatal:', error.toString()
+ client.close()
+ process.exit 1
+
+show_error_handler = (error) ->
+ console.error 'error:', error.toString()
+
+client.on 'error', show_error_handler
+client.on 'server-error', show_error_handler
+client.on 'protocol-error', fatal_error_handler
+client.on 'client-error', fatal_error_handler
+
+client.on 'join', (channel, who) -> console.log "[#{channel}] * #{who} joined"
+client.on 'part', (channel, who, why) -> console.log "[#{channel}] * #{who} parted (#{why})"
+client.on 'say', (channel, who, what) -> console.log "[#{channel}] <#{who}> #{what}"
+client.on 'nick', (channel, old_nick, new_nick) ->
+ console.log "[#{channel}] * #{old_nick} is now known as #{new_nick}"
+
+process.stdin.setEncoding 'utf8'
+process.stdin.resume()
+process.stdin.on 'data', (chunk) ->
+ lines = chunk.split '\n'
+ for line in lines
+ line = line.trim()
+ continue if line.length is 0
+ if line.match /^\/list\s*/i
+ client.list (list) -> console.log list
+ else if match = line.match /^\/nick\s+(\S+)/i
+ client.nick match[1]
+ else if match = line.match /^\/join\s+(\S+)/i
+ client.join match[1]
+ else if match = line.match /^\/part\s+(\S+)/i
+ client.part match[1]
+ else if match = line.match /^\/members\s+(\S+)/i
+ channel = match[1]
+ client.members match[1], (nicks) -> console.log "#{channel}: #{nicks}"
+ else if match = line.match /^\/say\s+(\S+)\s+(.+)/i
+ [channel, what] = [match[1], match[2]]
+ client.say channel, what
+ else if match = line.match /^\/info\s*/i
+ client.info (info) -> console.log info
+ else
+ console.log "unknown command"
+
+process.stdin.on 'end', ->
+ console.debug 'QUITTING'
+ client.close()
+
@@ -0,0 +1,145 @@
+net = require 'net'
+util = require 'util'
+EventEmitter = (require 'events').EventEmitter
+JsonLineProtocol = (require 'json-line-protocol').JsonLineProtocol
+
+CRLF = '\r\n'
+
+# Chat server client.
+# - Emits 'protocol-error' (error, line) on protocol error
+# - Emits 'server-error' (error) on server-side runtime error
+# - Emits 'client-error' (error) when the client encounters some error
+# - Emits 'close' when a connection to the server is closed
+# - Emits 'join' (channel_name, nick) when someone joins a channel
+# that the client has joined
+# - Emits 'part' (channel_name, nick, reason) when someone parts/leaves a channel
+# that the client has joined
+# - reason is a string or null
+# - Emits 'say' (channel_name, nick, message) when someone says something in
+# a channel that the client has joined
+# - Emits 'nick' (channel_name, old_nick, new_nick) when someone changes their
+# nickname and has joined to a channel that the client has joined
+
+class ChatClient extends EventEmitter
+ constructor: (port, host) ->
+ @list_callback = null
+ @info_callback = null
+ @members_callbacks = {}
+
+ @conn = net.createConnection port, host
+
+ @conn.on 'connect', =>
+ @conn.setEncoding 'utf8'
+ @conn.setKeepAlive true
+ @conn.setNoDelay true
+
+ @protocol = new JsonLineProtocol
+
+ @conn.on 'data', (data) =>
+ @protocol.feed data
+
+ @conn.on 'error', (error) =>
+ @emit 'client-error', error.toString()
+
+ @conn.on 'end', =>
+ @emit 'client-error', 'connection-closed'
+
+ @protocol.on 'error', (error, line) =>
+ @emit 'protocol-error', error, line
+
+ @protocol.on 'value', (msg) =>
+ if msg.error?
+ @emit 'server-error', msg.error
+ return
+
+ if msg.list?
+ @list_callback? msg.list
+ @list_callback = null
+ return
+
+ if msg.members?
+ channel_name = msg.members
+ @members_callbacks[channel_name]? msg.nicks
+ delete @members_callbacks[channel_name]
+ return
+
+ if msg.info?
+ @info_callback? msg.info
+ @info_callback = null
+ return
+
+ if msg.join?
+ @emit 'join', msg.join, msg.who
+ return
+
+ if msg.part?
+ @emit 'part', msg.part, msg.who, msg.why
+ return
+
+ if msg.say?
+ @emit 'say', msg.say, msg.who, msg.what
+ return
+
+ if msg.nick?
+ @emit 'nick', msg.nick, msg.old_nick, msg.new_nick
+ return
+
+ throw new Error "unexpected message #{util.inspect msg}"
+ return
+
+ close: ->
+ @conn.end()
+
+ _write: (obj) ->
+ json = JSON.stringify obj
+ @conn.write json + CRLF if @conn.writable
+
+ # Set or change your nickname to 'nick'.
+ # Must be unique server-wide.
+
+ nick: (nick) -> @_write {nick}
+
+ # List all active channels that start with 'prefix'.
+ # 'callback' is called back with an array of channel names.
+
+ list_filter: (prefix, callback) ->
+ @list_callback = callback
+ @_write list:prefix
+
+ # List all active channels.
+ # 'callback' is called back with an array of channel names.
+
+ list: (callback) ->
+ @list_callback = callback
+ @_write list:true
+
+ # Join a channel with name 'channel'.
+
+ join: (channel) -> @_write join:channel
+
+ # List the members joined to channel with name 'channel'.
+ # 'callback' is called back with an array of nicknames.
+
+ members: (channel, callback) ->
+ @members_callbacks[channel] = callback
+ @_write members:channel
+
+ # Say something ('what') in a channel with name 'channel'
+ # already joined.
+
+ say: (channel, what) -> @_write say:channel, what:what
+
+ # Part or leave a joined channel with name 'channel' for reason 'why'.
+
+ part: (channel, why='') -> @_write part:channel, why:why
+
+ # Get server information.
+ # 'callback' is passed an object of the form {channels:int, users:int, uptime:int}
+ # where 'uptime' is in seconds.
+
+ info: (callback) ->
+ @info_callback = callback
+ @_write info:true
+
+exports.connect = (port=11746, host='127.0.0.1') ->
+ new ChatClient port, host
Oops, something went wrong.

0 comments on commit 32da17f

Please sign in to comment.