Skip to content

Instantly share code, notes, and snippets.

@kennethlakin
Created April 14, 2013 03:15
Show Gist options
  • Save kennethlakin/5381258 to your computer and use it in GitHub Desktop.
Save kennethlakin/5381258 to your computer and use it in GitHub Desktop.
TypeScript conversion, typechecking, or the lack thereof.
declare class SocketId {};
declare var chrome : {
socket: {
create(protocol:string, options:Object, cb:(s:{socketId:SocketId;})=>void);
connect(sock:SocketId, host:string, port:number, cb:Function);
write(sock:SocketId, ab:ArrayBuffer, cb:Function);
read(sock:SocketId, opt:Object, cb:(ab:ArrayBuffer)=>void);
disconnect(sock:SocketId, cb?:()=>void);
};
storage: {
local: {
set(mapping:Object, cb?:()=>void);
};
};
};
declare interface Node {
remove();
}
declare interface Window {
require(mod:string);
chrome();
exports;
}
//Easy way to package up a message from the server.
class IrcCommand {
prefix : string = "";
command : string = "";
username : string = "";
args : string[] = [];
}
//Defined extension points:
//onMessages(serverMessages[x]IrcCommands
//onDisconnect(resultCode)
//onConnect()
//onRead(readInfo)
//onWritten(writeInfo, optional_function)
//onWrite(data)
class IrcClient {
public nick : string;
public socketId : SocketId;
constructor(public serverName: string,
public serverPort: number,
defaultNick: string,
public channel: string) {
this.retrieveUserName(defaultNick);
//We're probably running as a Chrome Extension.
//FIXME: Make this check square with the runningInChrome check,
// and play nicely with the various mocks that we're working with.
if(typeof window !== 'undefined' && chrome && chrome.socket) {
this._connect = function(serverName, port, cb) {
chrome.socket.create('tcp', {}, function (createInfo)
{
this.socketId = createInfo.socketId;
chrome.socket.connect(this.socketId, serverName, serverPort, cb);
}.bind(this)); // end socket.create
}
this._write = function(str, func) {
var ab = IrcClient.str2ab(str);
chrome.socket.write(this.socketId, ab, func);
}
this._read = function(cb) {
chrome.socket.read(this.socketId, null, cb);
}
this._disconnect = function() {
chrome.socket.disconnect(this.socketId);
}
}
//We should be running under node.js.
else if(typeof window === 'undefined' && window.require) {
var net = window.require("net");
var client;
this._connect = function(serverName, port, cb) {
client = net.connect({port: port, host: serverName}, cb);
client.on('data', this._callReadForever);
//FIXME: Need to pass result code to onDisconnected.
client.on('error', this.onDisconnected);
client.on('close', this.onDisconnected);
client.on('end', this.onDisconnected);
}.bind(this);
this._write = function(str, func) {
client.write(str, func);
}
this._read = function(data) {
//...we don't get to manually schedule reads on our own. Grr.
//So, we just do nothing and let the node.js event handling
//schedule our eternal reads...
}
this._disconnect = function() {
client.end();
}
this._callReadForever = function(data) {
//If we've been called, we have data, without error,
//so setting resultCode to >0 is okay.
var readInfo = { resultCode: 1, data: data};
this.readForever(readInfo);
}.bind(this);
}
}
//Extension points.
public onMessages(serverMessages: IrcCommand[]) {};
public onDisconnect(resultCode : any) {};
public onConnect() {};
public onRead(readInfo: ArrayBuffer) {};
public onWritten(one: any, two: Function) {};
public onWrite(data: any) {};
private _connect(serverName: string, port: number, cb: Function) : void {};
private _write(str: string, func: Function) : void {};
private _read(cb: (ab:ArrayBuffer)=>void) : void {};
private _disconnect() : void {};
private _callReadForever(data : any) : void {};
public static str2ab(str : string) : ArrayBuffer {
var buf = new ArrayBuffer(str.length*1); // 1 byte for each char
var bufView = new Uint8Array(buf);
for (var i=0, strLen=str.length; i<strLen; i++)
{
bufView[i] = str.charCodeAt(i);
}
return buf;
}
public static ab2str (buf : ArrayBuffer) : string {
return String.fromCharCode.apply(null, new Uint8Array(buf));
}
//Converts a single message from an IRC server into an IrcCommand object.
public static crackMessage (serverLine : string) : IrcCommand {
if(serverLine.length === 0)
{
return undefined;
}
var r = new IrcCommand();
var parts = serverLine.split(" ");
var offset = 0;
//If our message had a prefix, store it.
if(parts[0].charAt(0) == ":" )
{
r.prefix = parts[0];
offset = 1;
}
r.command = parts[0+offset];
r.username = parts[1+offset];
r.args = parts.slice(2+offset);
return r;
}
//@returns true if we're both running in an browser, and running under Google
//Chrome, and have access to Chrome Storage.
public static runningInChrome() : bool {
if(typeof window === "undefined") return false;
return (window.chrome && chrome && !!chrome.storage);
}
public write (s : string, f? : Function) : void {
var w;
if(this.onWrite) {
this.onWrite(s);
}
if(this.onWritten) {
w = this.onWritten.bind(this, f);
}
s+="\r\n";
this._write(s, w);
}
public connect() : void {
this._connect(this.serverName, this.serverPort, this.onConnected.bind(this));
}
public onConnected() : void {
if(this.onConnect) {
this.onConnect();
}
this.readForever();
this.write('PASS none');
this.write('NICK ' + this.nick);
this.write('USER USER 0 * :Real Name');
}
public pong (serverMessage : IrcCommand) : void {
if(serverMessage) {
this.write("PONG :"+ serverMessage.username.substring(1));
}
else {
throw new Error("Error: No message passed to pong.");
}
}
public joinChannel (channelName : string) : void {
if(channelName) {
this.write('JOIN ' + channelName);
}
else {
if(this.channel) {
this.write('JOIN ' + this.channel);
}
else {
throw new Error("joinChannel: No channelName passed in and no default channel defined!");
}
}
}
public sendPrivmsg (reciever : string, message : string) : void {
if(reciever && message) {
this.write("PRIVMSG " + reciever + " :" + message);
}
else {
var except = "sendPrivmsg: ";
if(!reciever) {
except += "reciever unspecified. ";
}
if(!message) {
except += "message unspecified. ";
}
throw new Error(except);
}
}
//Main message processing loop.
public readForever (one? : any, two? : any) : void
{
var readInfo;
var prevLine;
if(arguments.length == 2) {
readInfo = two;
prevLine = one;
}
else {
readInfo = one;
}
if(readInfo!==undefined && readInfo.resultCode < 0)
{
this.onDisconnected(readInfo.resultCode);
return;
}
if (readInfo !== undefined && readInfo.resultCode > 0)
{
if(this.onRead) {
this.onRead(readInfo);
}
var serverStr = IrcClient.ab2str(readInfo.data);
var serverMsg =
prevLine !== undefined ?
prevLine + serverStr : serverStr;
var serverLines = [];
var serverMessages = [];
serverLines = serverMsg.split("\n");
//Split the server messages into single lines.
for(var i = 0; i < serverLines.length; i++) {
var line = serverLines[i];
//This Chrome Sockets API sometimes gives us incomplete lines.
//We assume that there will be only one of these, and save it for later.
if(line.length > 0 && line.slice(-1) != "\r") {
prevLine = line;
break;
}
//If the line wasn't empty, save the message.
var msg = IrcClient.crackMessage(line);
if(msg !== undefined) {
serverMessages.push(msg);
}
}
if(this.onMessages) {
this.onMessages(serverMessages);
}
}
this._read(this.readForever.bind(this, prevLine));
}//end readForever
public onDisconnected (resultCode : any) : void
{
if(this.onDisconnect) {
// we've been disconnected, dang.
this.onDisconnect(resultCode);
}
this._disconnect();
} // end onDisconnected
public setUserName (newUserName : string, optionalCallback? : ()=>void) : void
{
if(IrcClient.runningInChrome()) {
chrome.storage.local.set({userName: newUserName}, optionalCallback);
}
else {
//Don't do anything for now, as we don't have Local Storage.
}
} // end setUserName
public retrieveUserName (defaultUsername : string) : void {
this.nick = defaultUsername;
}
}
//Node.js exports.
if(typeof this.exports !== 'undefined') {
this.exports.IrcClient = IrcClient;
}
declare interface Event {
keyCode;
};
declare interface HTMLElement {
value;
};
class OptimistBot {
private client: IrcClient;
private timeOfLastChanMsg: Date;
private silentTimeInMin: number;
private goodVibes: string[];
constructor(
private serverName: string,
private serverPort: number,
private userName: string,
private channelName: string) {
this.timeOfLastChanMsg = new Date();
this.silentTimeInMin = 0.5;
//OptimistBot Sayings
this.goodVibes = [
"Great job team!","Wow! I can't believe how much headway we're making!",
"That's a great point! Let's explore this perspective with bit more dicussion. ",
"Keep up the great work team! This discussion is fascinating!",
"This is very encouraging. We are reaching our goals by talking things out.",
"All of these are great ideas! Let's keep going and get everyone's contribution.",
"Congratulations team! Great work so far!",
"Thanks for mentioning that. That's a perspective I've never thought about before.",
"All right! Fantastic point!",
"Just wanted to throw in my two cents- you're all doing a dynamite job here!",
"That's one thing I love about this channel- the truly diverse ideas being discussed. Great job!",
"I like that. Let's brainstorm some more on this idea."
]
}
public main () {
this.client = new IrcClient(
this.serverName, this.serverPort,
this.userName, this.channelName)
this.timeOfLastChanMsg.setTime(1); //initialize the time to 1.
this.client.onConnect = OptimistBot.onConnected;
this.client.onDisconnect = OptimistBot.onDisconnected;
this.client.onMessages = this.handleOptimistMessages.bind(this);
this.client.onRead = OptimistBot.read;
this.client.onWritten = OptimistBot.onWritten;
this.client.onWrite = OptimistBot.onWrite;
this.client.connect();
var inputElement = document.getElementById('typing');
if(inputElement) {
inputElement.addEventListener('keydown', function (event)
{
// if the user pushed the enter key while typing a message (13 is enter):
if (event.keyCode === 13)
{
var message = inputElement.value;
inputElement.value = "";
this.client.write("PRIVMSG " + this.channelName + " :" + message);
}
});
}
};
private static onConnected ()
{
document.getElementById('connectionStatus').textContent = "connected!";
} // end onConnected
private static onDisconnected ()
{
document.getElementById('connectionStatus').textContent = "disconnected :(";
} // end onDisconnected
private static onWrite (s)
{
console.log(s);
displayLineToScreen("[sending] " + s);
}//end write
private static read (readInfo)
{
console.log(new Date() + IrcClient.ab2str(readInfo.data));
}//end read
private static onWritten (one: any, two: Function)
{
if(two) {
console.log('write was ', two);
if(one) {
one();
}
}
else {
console.log('write was ', one);
}
}
private handleOptimistMessages (serverMessages : IrcCommand[]) {
for(var i = 0; i < serverMessages.length; ++i)
{
var m = serverMessages[i];
console.log(m.command, m);
switch(m.command)
{
//Welcome message!
case "001":
this.client.joinChannel(this.channelName);
break;
case "PING":
this.client.pong(m);
OptimistBot.displayLineToScreen('[SERVER PONG]');
break;
case "PRIVMSG":
this.handlePrivmsg(m);
break;
default:
//All this spew is a bit annoying.
//console.log("WARN: Unhandled message: ", m);
break;
}
}
}
private handlePrivmsg (message) {
var text;
//This is a message to the channel:
if(message.username === this.channelName)
{
for(var i = 0; i < message.args.length; ++i)
{
var arg = message.args[i];
//Slice off the colon from the first arg.
//FIXME: We should do this fixup elsewhere.
if(i === 0)
{
arg = arg.substring(1);
}
//If someone has mentioned us, speak back.
if(arg.search(this.userName) != -1)
{
text = "I LIKE RAINBOWS?";
this.sendPrivmsg(this.channelName, text);
}
}
}
//If not, it must be a message to me.
else
{
var messagingUser = message.prefix.slice(1, message.prefix.search("!"));
text = "I LIKE RAINBOWS!?";
this.sendPrivmsg(messagingUser, text);
}
}
private sendPrivmsg (reciever, message) {
var dateObj = new Date();
if(reciever != this.channelName) {
this.client.sendPrivmsg(reciever, message);
}
else {
var currTime = dateObj.getTime();
var silentTime = this.silentTimeInMin*60000;
var lastSentTime = this.timeOfLastChanMsg.getTime();
if (currTime - lastSentTime > silentTime) {
this.timeOfLastChanMsg.setTime(currTime);
this.client.sendPrivmsg(reciever, message);
}
else {
console.log("You don't get to write because you messaged the channel already. dateObj.getTime: ")
console.log(currTime);
console.log("Time of timeOfLastChanMsg")
console.log(lastSentTime);
console.log(currTime - lastSentTime)
console.log(currTime - lastSentTime < silentTime)
}
}
}
private static displayLineToScreen (text: string)
{
var p = document.createElement('pre');
p.textContent = text;
var container = document.getElementById('recent-chat-display');
container.appendChild(p);
while (container.childNodes.length > 15)
{
container.childNodes[0].remove();
}
}
}
//Node.js imports!
if(typeof window.require !== 'undefined') {
var IrcClient = window.require("./irclib").IrcClient;
}
if(IrcClient.runningInChrome()) {
var ob = new OptimistBot("chat.freenode.net",
6667, "LakinBot", "#realtestchannel");
ob.main();
}
//Node.js main!
else if(typeof window.exports !== 'undefined') {
//Mocks to make the UI "work" under node.
var document = {
createElement : function() {
return { textContent : "" }
},
getElementById : function() {
return {
textContent : "",
addEventListener: function() {},
appendChild: function() {},
childNodes: { length: 0}
}
}
};
var ob = new OptimistBot("chat.freenode.net",
6667, "LakinBot", "#realtestchannel");
ob.main();
}
@kennethlakin
Copy link
Author

It's a pity that the github Typescript syntax highlighter doesn't work right now. :(

@rictic
Copy link

rictic commented Apr 14, 2013

declare interface Event {
  keyCode;
};

declare interface HTMLElement {
  value;
};

Advise against this. keyCode should only be a field on KeyboardEvent, and value is a field on InputElement. If you're getting warnings there the right thing is probably to cast the general type you've got into the more specific type. (Typescript 0.9.0 is adding a feature that will make this much less necessary).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment