Commits

firefly committed ffbab6c

Initial commit. Bot currently consists of four source files and one module.

  • Participants

Comments (0)

Files changed (8)

deps/JSONSelect/JSONSelect.md

+**WARNING**: This document is a work in progress, just like JSONSelect itself.
+View or contribute to the latest version [on github](http://github.com/lloyd/JSONSelect/blob/master/JSONSelect.md)
+
+# JSONSelect
+
+  1. [introduction](#introduction)
+  1. [levels](#levels)
+  1. [language overview](#overview)
+  1. [grouping](#grouping)
+  1. [selectors](#selectors)
+  1. [pseudo classes](#pseudo)
+  1. [expressions](#expressions)
+  1. [combinators](#combinators)
+  1. [grammar](#grammar)
+  1. [conformance tests](#tests)
+  1. [references](#references)
+
+## Introduction<a name="introduction"></a>
+
+JSONSelect defines a language very similar in syntax and structure to
+[CSS3 Selectors](http://www.w3.org/TR/css3-selectors/).  JSONSelect
+expressions are patterns which can be matched against JSON documents.
+
+Potential applications of JSONSelect include:
+
+  * Simplified programmatic matching of nodes within JSON documents.
+  * Stream filtering, allowing efficient and incremental matching of documents.
+  * As a query language for a document database.
+
+## Levels<a name="levels"></a>
+
+The specification of JSONSelect is broken into three levels.  Higher
+levels include more powerful constructs, and are likewise more
+complicated to implement and use.
+
+**JSONSelect Level 1** is a small subset of CSS3.  Every feature is
+derived from a CSS construct that directly maps to JSON.  A level 1
+implementation is not particularly complicated while providing basic
+querying features.
+
+**JSONSelect Level 2** builds upon Level 1 adapting more complex CSS
+constructs which allow expressions to include constraints such as
+patterns that match against values, and those which consider a node's
+siblings.  Level 2 is still a direct adaptation of CSS, but includes
+constructs whose semantic meaning is significantly changed.
+
+**JSONSelect Level 3** adds constructs which do not necessarily have a
+direct analog in CSS, and are added to increase the power and convenience
+of the selector language.  These include aliases, wholly new pseudo
+class functions, and more blue sky dreaming.
+
+## Language Overview<a name="overview"></a>
+
+<table>
+<tr><th>pattern</th><th>meaning</th><th>level</th></tr>
+<tr><td>*</td><td>Any node</td><td>1</td></tr>
+<tr><td>T</td><td>A node of type T, where T is one string, number, object, array, boolean, or null</td><td>1</td></tr>
+<tr><td>T.key</td><td>A node of type T which is the child of an object and is the value its parents key property</td><td>1</td></tr>
+<tr><td>T."complex key"</td><td>Same as previous, but with property name specified as a JSON string</td><td>1</td></tr>
+<tr><td>T:root</td><td>A node of type T which is the root of the JSON document</td><td>1</td></tr>
+<tr><td>T:nth-child(n)</td><td>A node of type T which is the nth child of an array parent</td><td>1</td></tr>
+<tr><td>T:nth-last-child(n)</td><td>A node of type T which is the nth child of an array parent counting from the end</td><td>2</td></tr>
+<tr><td>T:first-child</td><td>A node of type T which is the first child of an array parent (equivalent to T:nth-child(1)</td><td>1</td></tr>
+<tr><td>T:last-child</td><td>A node of type T which is the last child of an array parent (equivalent to T:nth-last-child(1)</td><td>2</td></tr>
+<tr><td>T:only-child</td><td>A node of type T which is the only child of an array parent</td><td>2</td></tr>
+<tr><td>T:empty</td><td>A node of type T which is an array or object with no child</td><td>2</td></tr>
+<tr><td>T U</td><td>A node of type U with an ancestor of type T</td><td>1</td></tr>
+<tr><td>T > U</td><td>A node of type U with a parent of type T</td><td>1</td></tr>
+<tr><td>T ~ U</td><td>A node of type U with a sibling of type T</td><td>2</td></tr>
+<tr><td>S1, S2</td><td>Any node which matches either selector S1 or S2</td><td>1</td></tr>
+<tr><td>T:has(S)</td><td>A node of type T which has a child node satisfying the selector S</td><td>3</td></tr>
+<tr><td>T:expr(E)</td><td>A node of type T with a value that satisfies the expression E</td><td>3</td></tr>
+<tr><td>T:val(V)</td><td>A node of type T with a value that is equal to V</td><td>3</td></tr>
+<tr><td>T:contains(S)</td><td>A node of type T with a string value contains the substring S</td><td>3</td></tr>
+</table>
+
+## Grouping<a name="grouping"></a>
+
+## Selectors<a name="selectors"></a>
+
+## Pseudo Classes<a name="pseudo"></a>
+
+## Expressions<a name="expressions"></a>
+
+## Combinators<a name="combinators"></a>
+
+## Grammar<a name="grammar"></a>
+
+(Adapted from [CSS3](http://www.w3.org/TR/css3-selectors/#descendant-combinators) and
+ [json.org](http://json.org/))
+
+    selectors_group
+      : selector [ `,` selector ]*
+      ;
+
+    selector
+      : simple_selector_sequence [ combinator simple_selector_sequence ]*
+      ;
+
+    combinator
+      : `>` | \s+
+      ;
+
+    simple_selector_sequence
+      /* why allow multiple HASH entities in the grammar? */
+      : [ type_selector | universal ]
+        [ class | pseudo ]*
+      | [ class | pseudo ]+
+      ;
+
+    type_selector
+      : `object` | `array` | `number` | `string` | `boolean` | `null`
+      ;
+
+    universal
+      : '*'
+      ;
+
+    class
+      : `.` name
+      | `.` json_string
+      ;
+
+    pseudo
+      /* Note that pseudo-elements are restricted to one per selector and */
+      /* occur only in the last simple_selector_sequence. */
+      : `:` pseudo_class_name
+      | `:` nth_function_name `(` nth_expression `)`
+      | `:has` `(`  selectors_group `)`
+      | `:expr` `(`  expr `)`
+      | `:contains` `(`  json_string `)`
+      | `:val` `(` val `)`
+      ;
+
+    pseudo_class_name
+      : `root` | `first-child` | `last-child` | `only-child`
+
+    nth_function_name
+      : `nth-child` | `nth-last-child`
+
+    nth_expression
+      /* expression is and of the form "an+b" */
+      : TODO
+      ;
+
+    expr
+      : expr binop expr
+      | '(' expr ')'
+      | val
+      ;
+
+    binop
+      : '*' | '/' | '%' | '+' | '-' | '<=' | '>=' | '$='
+      | '^=' | '*=' | '>' | '<' | '=' | '!=' | '&&' | '||'
+      ;
+
+    val
+      : json_number | json_string | 'true' | 'false' | 'null' | 'x'
+      ;
+
+    json_string
+      : `"` json_chars* `"`
+      ;
+
+    json_chars
+      : any-Unicode-character-except-"-or-\-or-control-character
+      |  `\"`
+      |  `\\`
+      |  `\/`
+      |  `\b`
+      |  `\f`
+      |  `\n`
+      |  `\r`
+      |  `\t`
+      |   \u four-hex-digits
+      ;
+
+    name
+      : nmstart nmchar*
+      ;
+
+    nmstart
+      : escape | [_a-zA-Z] | nonascii
+      ;
+
+    nmchar
+      : [_a-zA-Z0-9-]
+      | escape
+      | nonascii
+      ;
+
+    escape
+      : \\[^\r\n\f0-9a-fA-F]
+      ;
+
+    nonascii
+      : [^\0-0177]
+      ;
+
+## Conformance Tests<a name="tests"></a>
+
+See [https://github.com/lloyd/JSONSelectTests](https://github.com/lloyd/JSONSelectTests)
+
+## References<a name="references"></a>
+
+In no particular order.
+
+  * [http://json.org/](http://json.org/)
+  * [http://www.w3.org/TR/css3-selectors/](http://www.w3.org/TR/css3-selectors/)
+  * [http://ejohn.org/blog/selectors-that-people-actually-use/](http://ejohn.org/blog/selectors-that-people-actually-use/)
+  * [http://shauninman.com/archive/2008/05/05/css\_qualified\_selectors](  * http://shauninman.com/archive/2008/05/05/css_qualified_selectors)
+  * [http://snook.ca/archives/html\_and\_css/css-parent-selectors](http://snook.ca/archives/html_and_css/css-parent-selectors)
+  * [http://remysharp.com/2010/10/11/css-parent-selector/](http://remysharp.com/2010/10/11/css-parent-selector/)
+  * [https://github.com/jquery/sizzle/wiki/Sizzle-Home](https://github.com/jquery/sizzle/wiki/Sizzle-Home)

deps/JSONSelect/LICENSE

+Copyright (c) 2011, Lloyd Hilaiel <lloyd@hilaiel.com>
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

deps/JSONSelect/package.json

+{
+  "author": "Lloyd Hilaiel <lloyd@hilaiel.com> (http://lloyd.io)",
+  "name": "JSONSelect",
+  "description": "CSS-like selectors for JSON",
+  "version": "0.2.2",
+  "homepage": "http://jsonselect.org",
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/lloyd/JSONSelect.git"
+  },
+  "main": "src/jsonselect",
+  "engines": {
+    "node": ">=0.4.7"
+  },
+  "dependencies": {},
+  "devDependencies": {},
+  "files": [
+    "src/jsonselect.js",
+    "src/test/run.js",
+    "src/test/tests",
+    "tests",
+    "README.md",
+    "JSONSelect.md",
+    "package.json",
+    "LICENSE"
+  ],
+  "scripts": {
+    "test": "node src/test/run.js"
+  }
+}

modules/core.coffee

+@commands =
+	"echo": ->
+		@output.apply @, arguments
+
+	"cat": ->
+		@output.apply @, @input
+
+	"tac": ->
+		@output.apply @, @input.reverse()
+
+	"sel": (pattern) ->
+		results = require('../deps/JSONSelect').match pattern, @input[0]
+		@output result for result in results
+
+	"grep": (pattern) ->
+		regex = new RegExp pattern
+
+		for line in @input
+			if line.match regex
+				@output line
+	
+	"sed": (pattern) ->
+		match = pattern.match ///
+			s (.)
+			( [^\1]+ ) \1
+			( [^\1]* ) \1
+			([a-z]*)
+		///
+
+		if not match
+			throw new Error "Invalid regex pattern."
+
+		[_, _, matcher, replacement, flags] = match
+		@output @input[0].replace RegExp(matcher, flags), replacement
+
+	"eval": (expr) ->
+		@output eval expr
+
+	"addCommand": (name, argNames...) ->
+		functionBody = require('coffee-script').compile @input.join "\n"
+		bot.addCommand name, Function.apply null, argNames.concat [functionBody]
+		@output "Done!"
+	
+	"reload": ->
+		bot.reloadModule module for module in bot.modules
+		@output "Done!"
+
+	"g": @async ->
+		query = encodeURIComponent Array::join.call arguments, " "
+
+		require('http').get {
+			host : 'ajax.googleapis.com'
+			port : 80
+			path : '/ajax/services/search/web?v=1.0&q=' + query
+		}, (res) =>
+			buf = []
+			res.on 'data', (chunk) ->
+				buf.push chunk
+
+			res.on 'end', =>
+				console.log buf.join ""
+				response = JSON.parse buf.join ""
+				hits = response["responseData"]["results"]
+
+				if hits.length < 1
+					console.log "No results!"
+					@output "No result. :("
+					@done()
+					return
+
+				{titleNoFormatting, url, content} = hits[0]
+				title = titleNoFormatting
+				cont  = content[0..180] + (if content.length > 180 then " ..." else "")
+				cont  = cont.replace /<[^>]+>/g, ''
+
+				@output "[#{title}]: #{cont} <#{url}>"
+				@done()
+
+fs      = require 'fs'
+vm      = require 'vm'
+
+irc     = require './irc'
+{Shell} = require './shell'
+
+server = new irc.Server { hostname : "chat.freenode.net" }
+shell  = new Shell
+
+server.connection.on 'connect', ->
+	server.connection.authenticate "Eldis", "eldis", "Eldflugan"
+
+server.connection.on '001', ->
+	server.connection.write "JOIN", "##FireFly"
+
+server.connection.on 'PRIVMSG', (sender, target, message) ->
+	if message.match /^!/
+		try
+			shell.exec message[1..], (err, res) ->
+				server.connection.write "PRIVMSG", target, res.join(" ")
+
+		catch err
+			server.connection.write "PRIVMSG", target, "Error: #{err.message}"
+
+bot =
+	modules: []
+
+	addCommand: (name, func) ->
+		shell.commands[name] = func
+
+	reloadModule: (name) ->
+		fs.readFile "modules/#{name}.coffee", (err, res) ->
+			throw err if err
+			code = require('coffee-script').compile res.toString()
+
+			ctx =
+				require : require
+				console : console
+				bot     : bot
+
+				#### Command 'decorators' ###############################
+				async : (fn) ->
+					fn.asynchronous = true
+					fn
+			
+			try
+				vm.runInNewContext code, ctx, name
+				bot.addCommand key, func for key,func of ctx.commands ? {}
+
+			catch err
+				console.warn "--- Error while loading module '#{name}'!"
+				throw err
+
+	loadModule: (name) ->
+		@modules.push name
+		@reloadModule name
+
+bot.loadModule 'core'
+

src/buffered-socket.coffee

+{EventEmitter} = require 'events'
+net  = require 'net'
+util = require 'util'
+
+#### BufferedSocket #################################################
+BufferedSocket = (opts) ->
+	if not opts.hostname or not opts.port
+		throw new TypeError "Missing required option property: hostname or port."
+	
+	opts.separator ?= "\r\n"
+	
+	@options = opts
+	@
+
+exports.BufferedSocket = BufferedSocket
+util.inherits BufferedSocket, EventEmitter
+
+BufferedSocket::connect = ->
+	conn = this
+
+	socket = @socket = net.createConnection @options.port, @options.hostname
+	cache = ""
+
+	socket.on 'data', (chunk) =>
+		linesRaw = "" + cache + chunk
+
+		lines = linesRaw.split '\r\n'
+		cache = lines.pop()
+		
+		@emit 'data', line for line in lines
+
+	socket.on 'connect', =>
+		@emit 'connect'
+
+	socket.on 'end', =>
+		@emit 'end'
+
+BufferedSocket::write = (str) ->
+	@socket.write str
+
+exports.BufferedSocket = BufferedSocket
+
+{EventEmitter} = require 'events'
+util = require 'util'
+net  = require 'net'
+
+{BufferedSocket} = require './buffered-socket'
+
+#### Helper functions ###############################################
+validateChannelName = (name) ->
+	if not name.match /^[#&][^,]*$/
+		throw new Error "Invalid channel name: '#{name}'"
+
+rawExpression = ///
+	^
+	(?: :([^\x20]+) \x20 |)         # : (user!ident@host)
+	([^:] [^\x20]*)                 # command
+	((?: \x20 [^\x20:] [^\x20]* )*)   # parameters except last
+	\x20
+	(?: : (.*)                    # last parameter: either colon + rest of string
+		| ([^\x20]*))               # ...or multiple non-space characters (like other params)
+	$
+///
+
+userExpression = ///
+	(       [^!]+ )       #   (nick )
+	(?: ! ( [^@]+ ) )?    # ! (ident)
+	(?: @ (    .+ ) )?    # @ (host )
+///
+
+parseLine = (line) ->
+	matches = line.match rawExpression
+	throw new Error "Invalid line: '#{line}'." if not matches
+
+	[_, userData, command, argsData, lastArg, lastArg2] = matches
+	lastArg ?= lastArg2
+
+	user = if userData?
+			[_, nick, ident, host] = userData.match userExpression
+			{nick, ident, host}
+		else
+			{ nick : "<server>" }
+	
+	user.toString = -> @nick
+
+	args = [lastArg]
+	if argsData != ""
+		args = argsData[1..].split(" ").concat args
+	
+	{ user, command, arguments: args }
+
+#### Connection #####################################################
+Connection = (opts) ->
+	opts.port ?= 6667
+
+	@options = opts
+	@
+
+util.inherits Connection, EventEmitter
+
+Connection::connect = ->
+	EventEmitter.call this
+
+	conn = this
+	socket = @socket = new BufferedSocket
+		hostname  : @options.hostname
+		port      : @options.port
+		separator : "\r\n"
+	
+	socket.connect()
+
+	socket.on 'data', (lineRaw) ->
+		line = parseLine lineRaw.trimLeft()
+		#conn.emit 'raw', line
+
+		args = [line.command, line.user].concat line.arguments
+		EventEmitter::emit.apply conn, args
+	
+	socket.on 'connect', =>
+		@emit 'connect'
+	
+	socket.on 'end', =>
+		@emit 'disconnect'
+
+Connection::authenticate = (nick, ident, realname) ->
+	@write "NICK", nick
+	@write "USER", ident, "-", "-", realname
+
+isArrayLike = (arr) ->
+	typeof arr == 'object' and arr.length?
+
+Connection::write = (cmdAndArgs..., lastArg) ->
+	@sendRaw raw = cmdAndArgs.concat([":" + lastArg]).join(" ")
+
+Connection::sendRaw = (cmd, args) ->
+	if cmd.match /[\r\n]/
+		throw new Error "Trying to send more than one raw command at the same time!"
+	else
+		console.log "> #{cmd}"
+		@socket.write "#{cmd}\r\n"
+
+#### Server #########################################################
+Server = (opts) ->
+	@connection = conn = new Connection opts
+	conn.connect()
+
+	conn.on 'PING', (sender, message) ->
+		conn.write "PONG", message
+	
+	conn.on 'PRIVMSG', (sender, target, message) ->
+		console.log "<#{sender} to #{target}> #{message}"
+
+	conn.on 'NOTICE', (sender, target, message) ->
+		console.log "[NOTICE] <#{sender} to #{target}> #{message}"
+
+	conn.on 'JOIN', (sender, channel) ->
+		console.log "#{sender} joined channel #{channel}"
+
+	conn.on 'PART', (sender, channel, message = "") ->
+		console.log "#{sender} parted from channel #{channel} (#{message})"
+
+	conn.on 'KICK', (sender, channel, kickee, message = "") ->
+		console.log "#{sender} kicked #{kickee} from channel #{channel} (#{message})"
+
+	conn.on 'QUIT', (sender, message = "") ->
+		console.log "User #{sender} has quit IRC (#{message})"
+
+	conn.on 'NICK', (sender, newNick) ->
+		console.log "User #{sender} changed nick to #{newNick}"
+
+	conn.on '001', (sender, user, message) ->
+		console.log "[Welcome] #{message}"
+
+	conn.on '002', (sender, user, message) ->
+		console.log "[Host] #{message}"
+
+	conn.on '003', (sender, user, message) ->
+		console.log "[Created] #{message}"
+
+	conn.on '004', (sender, user, host, version, usermodes, chanmodes, supported...) ->
+		console.log "[My Info] I am #{host} running #{version}. " +
+				"User modes: #{usermodes}. Channel modes: #{chanmodes}"
+
+	conn.on '005', (sender, user, message...) ->
+		console.log "[Supported] #{message}"
+
+	conn.on '250', (sender, user, message) ->
+		console.log "[Statistics] #{message}"
+	
+	@
+
+#### exports ########################################################
+exports.Connection = Connection
+exports.Server     = Server
+# FireFly shell thing
+
+exports.Shell = Shell = (opts = {}) ->
+	@defaults =
+		input  : opts.input or []
+		output : opts.output or =>
+			Array::push.apply @outputBuffer, arguments
+
+	@outputBuffer = []
+	
+	@variables = opts.variables or {}
+	@commands  = opts.commands  or {}
+	
+	@commands["echo"] ?= -> @output.apply @, arguments
+	@commands["cat"]  ?= -> @output.apply @, @input
+	
+	@
+
+specialModesMap = {
+	"'" : 'str-single'
+	'"' : 'str-double'
+	"`" : 'str-eval'
+}
+
+lex = (str) ->
+	res       = []
+	mode      = 'none'
+	modeStart = 'none'
+	last      = 0
+
+	push = (type, value) ->
+		return if type == 'literal' and value == "" # Disallow empty literals
+
+		res.push
+			type  : type
+			value : value
+	
+	idx = 0
+	while idx < str.length
+		chr = str[idx]
+
+		if mode == 'none'
+			pushed = true
+
+			switch chr
+				when " "
+					push 'literal', str[last..idx-1]
+
+				when "="
+					push 'varname', str[last..idx-1]
+
+				when "|", ";"
+					push 'literal', str[last..idx-1]
+					push 'internal', str[idx]
+
+				when '"', "'", "`"
+					mode = specialModesMap[chr]
+
+					if str[idx-1] != " " and idx > 0
+						push 'unknown', str[last..idx-1]
+
+					#modeStart = mode
+
+				else
+					pushed = false
+
+			last = idx + 1 if pushed
+
+		else
+			if chr in "\"'`" and mode == specialModesMap[chr]
+				if mode == 'str-eval'
+					push 'eval', str[last..idx+1]
+					--idx
+				else
+					push 'quoted', str[last..idx-1]
+
+				mode = 'none'
+				last = idx + 2
+
+		++idx
+	
+	line = str[last..]
+	#line = line[0..-2] if mode != 'none'
+	#	line = line[1..-2]
+	
+	push 'literal', line if line.length > 0
+
+	res
+
+Shell::createExecution = ->
+	{
+		input  : @defaults.input[0..] # We want a copy
+		output : @defaults.output
+	}
+
+Shell::process = (tokens) ->
+	res = []
+	curr = @createExecution()
+	mode = 'none'
+	
+	for {type,value} in tokens
+		if mode == 'none' and type == 'literal'
+			curr.command = value
+			curr.args    = []
+			mode = 'args'
+
+		else if mode == 'args'
+			if type == 'literal' or type == 'quoted'
+				curr.args.push value
+
+			else if type == 'internal'
+				if value == ";"
+					res.push curr
+					curr = @createExecution()
+					mode = 'none'
+
+				else if value == "|"
+					newExecution = @createExecution()
+					curr.output = ->
+						Array::push.apply newExecution.input, arguments
+
+					res.push curr
+					curr = newExecution
+					mode = 'none'
+
+			else
+				throw new Error "Syntax error: near #{value}"
+
+		else
+			throw new Error "Syntax error: near #{value}"
+
+	
+	res.push curr
+	res
+
+Shell::exec = (str, callback) ->
+	[first, rest...] = @process lex str
+
+	@outputBuffer = []
+
+	execute = (ex, rest) =>
+		func = @commands[ex.command]
+
+		if not func
+			throw new Error "Command not found: #{ex.command}"
+		
+		ex.stdout ?= console.log
+		ex.done = =>
+			if rest.length > 0
+				execute rest[0], rest[1..]
+			else
+				callback null, @outputBuffer # FIXME: Collect output here and pass to callback!
+
+		func.apply ex, ex.args
+		ex.done() if not func.asynchronous
+
+	callback null, @outputBuffer if not first
+
+	execute first, rest
+
+#console.log lex 'echo "hello, \'foo\' world!"'
+#(new Shell).exec 'echo Hello world'
+