diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..98d0a4c
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,2 @@
+all:
+ GOPATH=$(CURDIR) go build -v all
diff --git a/PROTOCOL b/PROTOCOL
new file mode 100644
index 0000000..5710812
--- /dev/null
+++ b/PROTOCOL
@@ -0,0 +1,64 @@
+wirc protocol
+===========
+
+This document attempts to describe the wirc protocol.
+The source code will always be authoritative,
+but this protocol has been around for a while now so should be changed often, if ever.
+
+
+Out Queue
+---------
+
+
+Any files that appear in the directory outq/ are written verbatim to the IRC server.
+
+You may put multiple lines in a single file.
+
+Filenames beginning with "." are ignored.
+
+You are advised to create files beginning with ".",
+then rename them on completion of the write,
+to avoid race conditions.
+
+
+Log
+---
+
+### Log Filenames
+
+TBD
+
+
+### Log Messages
+
+IRC messages are written to the log, one message per line.
+Messages are translated to an easier-to-parse format:
+
+ timestamp fullname command sender forum [args...] :text
+
+Where:
+
+* `timestamp` is in Unix epoch time.
+* `fullname` is the full name of the message origin (typically `nick!user@host.name`)
+* `command` is the IRC command
+* `sender` is the IRC name of the entity that sent the message
+* `forum` is the IRC name of the audience of the message
+* `args` are any additional arguments not otherwise covered
+* `text` is the text of the message
+
+`sender` and `forum` are provided in every message, for the convenience of the client.
+A PRIVMSG to `sender` will make it back to whomever sent the message,
+a PRIVMSG to `forum` will be seen by everyone in the audience.
+
+For example, a "private message" will have `sender` equal to `forum`.
+But a "channel message" will have `forum` set to the channel.
+
+See `wirc.go` for details of each message type.
+
+
+### Initial Messages
+
+Each log file will contain the following initial messages,
+to facilitate stateful clients:
+
+TBD
diff --git a/README b/README
new file mode 100644
index 0000000..2fa3013
--- /dev/null
+++ b/README
@@ -0,0 +1,29 @@
+Woozle IRC
+=========
+
+This is a sort of bouncer for clients with transient network connections,
+like cell phones and laptops.
+It's a lot like [tapchat](https://github.com/tapchat/tapchat) but is a whole lot simpler
+while being (at time of writing) much more feature-complete.
+
+It supports (currently) an JavaScript browser-based client,
+and can also be worked from the command-line using Unix tools like "tail" and "echo".
+
+Ironically, it doesn't currently work with any existing IRC clients,
+although we are kicking around ideas for such a thing.
+Honestly, though, if you want a bouncer for a traditional IRC client,
+you are better off using something like znc.
+
+We have an [architectural diagram](https://docs.google.com/drawings/d/1am_RTUh89kul-318GoYK73AbjOE_jMYi4vI4NyEgKrY/edit?usp=sharing) if you care about such things.
+
+
+Features
+--------
+
+* Gracefully handles clients with transient networking, such as cell phones and laptops.
+
+
+Todo
+-----
+
+I need to make this document suck less.
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
new file mode 100644
index 0000000..c0c2c24
--- /dev/null
+++ b/app/_locales/en/messages.json
@@ -0,0 +1,15 @@
+{
+ "appName": {
+ "message": "wIRC Chat Client",
+ "description": "Application name"
+ },
+ "appShortName": {
+ "message": "wIRC",
+ "description": "Short application name"
+ },
+ "appDesc": {
+ "message": "Chat client for the wIRC bouncer thingamajiggy",
+ "description": "Application description for app store listing"
+ }
+}
+
diff --git a/app/background.js b/app/background.js
new file mode 100644
index 0000000..31c2421
--- /dev/null
+++ b/app/background.js
@@ -0,0 +1,5 @@
+chrome.app.runtime.onLaunched.addListener(function() {
+ chrome.app.window.create('wirc.html', {
+ 'state': 'normal'
+ })
+})
diff --git a/app/example.html b/app/example.html
new file mode 100644
index 0000000..0c07b8a
--- /dev/null
+++ b/app/example.html
@@ -0,0 +1,361 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+CIRC undefined
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
members
+
+ also
+
+ atob
+
+ Bermuda
+
+ bit
+
+ bk
+
+ bz2
+
+ caycos
+
+ ckape
+
+ clavicle
+
+ CrackMonkey
+
+ Dumont
+
+ dzho
+
+ emad
+
+ eythian
+
+ felixc
+
+ fo0bar
+
+ fuzzie
+
+ GodEater
+
+ hollow
+
+ hoylemd
+
+ jhewl
+
+ jv
+
+ kees
+
+ khmer
+
+ lamont
+
+ lexicondal
+
+ neale
+
+ nemo
+
+ nerdtron3000
+
+ nornagon
+
+ Octal
+
+ pdx6
+
+ pedro
+
+ Randall
+
+ sarah
+
+ Sciri
+
+ scorche
+
+ scorche|sh
+
+ Screwtape
+
+ sneakums
+
+ squinky
+
+ stat
+
+ teferi
+
+ tiaz
+
+ watson
+
+ wcarss
+
+ wombat
+
+ X11R5
+
+ Zen
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/manifest.json b/app/manifest.json
new file mode 100644
index 0000000..227be33
--- /dev/null
+++ b/app/manifest.json
@@ -0,0 +1,21 @@
+{
+ "manifest_version": 2,
+ "version": "1.0",
+
+ "name": "__MSG_appName__",
+ "short_name": "__MSG_appShortName__",
+ "description": "__MSG_appDesc__",
+ "author": "Neale Pickett ",
+ "icons": {"128": "wirc.png"},
+ "app": {
+ "background": {
+ "scripts": ["background.js"]
+ }
+ },
+ "permissions": [
+ "storage",
+ "fileSystem",
+ "https://woozle.org/"
+ ],
+ "default_locale": "en"
+}
diff --git a/app/message_style.css b/app/message_style.css
new file mode 100644
index 0000000..5da3f16
--- /dev/null
+++ b/app/message_style.css
@@ -0,0 +1,172 @@
+.message.system {
+ color: #505053;
+}
+
+.message.notice {
+ color: #97008B;
+}
+
+.message.error {
+ color: #B33B2D;
+}
+
+.message.update {
+ color: #00068F;
+}
+
+.message.update.self {
+ color: #005816;
+}
+
+.message.update.privmsg {
+ color: #505053;
+}
+
+.message.update.privmsg.self {
+ color: gray;
+}
+
+.message.update.privmsg.mention {
+ color: darkred;
+}
+
+.message.update.privmsg.mention .source {
+ font-weight: bold;
+}
+
+.message.update.privmsg.direct .source {
+ color: #488AA8;
+}
+
+.message.update.privmsg.notice .source {
+ color: #97008B;
+}
+
+/* Nickname Colors. Taken from Textual */
+
+.message.update.privmsg.self .source-content-container .source {
+ color: #ea0d68;
+}
+
+.message.update.privmsg:not(.self) .source-content-container .source[colornumber='0'] {
+ color: #0080ff;
+}
+
+.message.update.privmsg:not(.self) .source-content-container .source[colornumber='1'] {
+ color: #059005;
+}
+
+.message.update.privmsg:not(.self) .source-content-container .source[colornumber='2'] {
+ color: #a80054;
+}
+
+.message.update.privmsg:not(.self) .source-content-container .source[colornumber='3'] {
+ color: #9b0db1;
+}
+
+.message.update.privmsg:not(.self) .source-content-container .source[colornumber='4'] {
+ color: #108860;
+}
+
+.message.update.privmsg:not(.self) .source-content-container .source[colornumber='5'] {
+ color: #7F4FFF;
+}
+
+.message.update.privmsg:not(.self) .source-content-container .source[colornumber='6'] {
+ color: #58701a;
+}
+
+.message.update.privmsg:not(.self) .source-content-container .source[colornumber='7'] {
+ color: #620a8e;
+}
+
+.message.update.privmsg:not(.self) .source-content-container .source[colornumber='8'] {
+ color: #BB0008;
+}
+
+.message.update.privmsg:not(.self) .source-content-container .source[colornumber='9'] {
+ color: #44345f;
+}
+
+.message.update.privmsg:not(.self) .source-content-container .source[colornumber='10'] {
+ color: #2f5353;
+}
+
+.message.update.privmsg:not(.self) .source-content-container .source[colornumber='11'] {
+ color: #904000;
+}
+
+.message.update.privmsg:not(.self) .source-content-container .source[colornumber='12'] {
+ color: #808000;
+}
+
+.message.update.privmsg:not(.self) .source-content-container .source[colornumber='13'] {
+ color: #57797e;
+}
+
+.message.update.privmsg:not(.self) .source-content-container .source[colornumber='14'] {
+ color: #3333dd;
+}
+
+.message.update.privmsg:not(.self) .source-content-container .source[colornumber='15'] {
+ color: #5f4d22;
+}
+
+.message.update.privmsg:not(.self) .source-content-container .source[colornumber='16'] {
+ color: #706616;
+}
+
+.message.update.privmsg:not(.self) .source-content-container .source[colornumber='17'] {
+ color: #46799c;
+}
+
+.message.update.privmsg:not(.self) .source-content-container .source[colornumber='18'] {
+ color: #80372e;
+}
+
+.message.update.privmsg:not(.self) .source-content-container .source[colornumber='19'] {
+ color: #8F478E;
+}
+
+.message.update.privmsg:not(.self) .source-content-container .source[colornumber='20'] {
+ color: #5b9e4c;
+}
+
+.message.update.privmsg:not(.self) .source-content-container .source[colornumber='21'] {
+ color: #13826c;
+}
+
+.message.update.privmsg:not(.self) .source-content-container .source[colornumber='22'] {
+ color: #b13637;
+}
+
+.message.update.privmsg:not(.self) .source-content-container .source[colornumber='23'] {
+ color: #e45d59;
+}
+
+.message.update.privmsg:not(.self) .source-content-container .source[colornumber='24'] {
+ color: #1b51ae;
+}
+
+.message.update.privmsg:not(.self) .source-content-container .source[colornumber='25'] {
+ color: #4855ac;
+}
+
+.message.update.privmsg:not(.self) .source-content-container .source[colornumber='26'] {
+ color: #7f1d86;
+}
+.message.update.privmsg:not(.self) .source-content-container .source[colornumber='27'] {
+ color: #73643f;
+}
+
+.message.update.privmsg:not(.self) .source-content-container .source[colornumber='28'] {
+ color: #0b9578;
+}
+
+.message.update.privmsg:not(.self) .source-content-container .source[colornumber='29'] {
+ color: #569c96;
+}
+
+.message.update.privmsg:not(.self) .source-content-container .source[colornumber='30'] {
+ color: #08465f;
+}
diff --git a/app/server.js b/app/server.js
new file mode 100644
index 0000000..4749e41
--- /dev/null
+++ b/app/server.js
@@ -0,0 +1,38 @@
+// Functionality dealing with server-level things
+
+var maxScrollback = 500;
+
+function Server(baseURL, network, authtok, messageHandler) {
+ function handleEventSourceLine(line) {
+ var lhs = line.split(" :", 1)[0]
+ var parts = lhs.split(' ')
+ var timestamp = new Date(parts[0] * 1000);
+ var fullSender = parts[1];
+ var command = parts[2];
+ var sender = parts[3];
+ var forum = parts[4];
+ var args = parts.slice(5);
+ var txt = line.substr(lhs.length + 2);
+
+ messageHandler(timestamp, fullSender, command, sender, forum, args, txt);
+ }
+
+ function handleEventSourceMessage(oEvent) {
+ msgs = oEvent.data.split("\n");
+
+ var first = Math.min(0, msgs.length - maxScrollback);
+ for (var i = first; i < msgs.length; i += 1) {
+ handleEeventSourceLine(msgs[i]);
+ }
+ }
+
+ function handleEventSourceError(oEvent) {
+ timestamp = new Date();
+ messageHandler(timestamp, null, "ERROR", null, null, [], null);
+ }
+
+ var pullURL = baseURL + "?network=" + encodeURIComponent(network) + "&authtok=" + encodeURIComponent(authtok);
+ this.eventSource = new EventSource(baseURL);
+ this.eventSource.addEventListener("message", handleEventSourceMessage);
+ this.eventSource.addEventListener("error", handleEventSourceError);
+}
diff --git a/app/style.css b/app/style.css
new file mode 100644
index 0000000..7ece3ca
--- /dev/null
+++ b/app/style.css
@@ -0,0 +1,510 @@
+html, body {
+ margin: 0;
+ padding: 0
+}
+
+*, *:before, *:after {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+body {
+ color: #505053;
+ font-family: sans-serif;
+ font-size: 14px;
+}
+
+.hidden {
+ display: none;
+}
+
+#templates {
+ display: none;
+}
+
+ul {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.footer {
+ display: none;
+ font-style: italic;
+ color: #9493A2;
+}
+
+.help-command {
+ display: inline-block;
+ width: 7em;
+}
+
+.content-item {
+ white-space: nowrap;
+ overflow-x: hidden;
+ text-overflow: ellipsis;
+}
+
+#main {
+ display: -webkit-box;
+ -webkit-box-orient: horizontal;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ border-top: 1px solid rgba(0,0,0,0.15);
+}
+
+#main-top-border {
+ position: absolute;
+ top: 0;
+ z-index: 100;
+ width: 100%;
+ height: 1px;
+ background-color: rgba(0,0,0,0.05);
+ pointer-events: none;
+}
+
+#rooms-and-nicks {
+ background-color: #F7F5E4;
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-box-flex: 0;
+ width: 150px;
+ height: inherit;
+ padding: 4px 0px;
+ border-right: 1px solid rgba(0, 0, 0, .15);
+ overflow-y: auto;
+ -webkit-overflow-scrolling: touch;
+}
+
+#rooms-and-nicks h1 {
+ color: #406698;
+ text-transform: uppercase;
+ font-size: 12px;
+ padding: 8px 0px 2px 8px;
+ margin: 0px;
+}
+
+#rooms-and-nicks .nick,
+#rooms-and-nicks .room {
+ padding: 0 8px;
+ line-height: 26px;
+}
+
+#rooms-and-nicks .room {
+ cursor: pointer;
+}
+
+#rooms-and-nicks .server.footer {
+ display: block;
+}
+
+.dragbar:hover {
+ width: 6px;
+ transition: width .1s;
+}
+
+.dragbar {
+ height: 100%;
+ width: 1px;
+ cursor: col-resize;
+}
+
+#ghostbar{
+ width:3px;
+ background-color:#000;
+ opacity:0.8;
+ position:absolute;
+ cursor: col-resize;
+ z-index:999
+}
+
+#rooms-container {
+ -webkit-box-flex: 0;
+ border-bottom: 1px solid #CCC;
+ padding-bottom: 10px;
+}
+
+.no-nicks #rooms-container {
+ border-bottom-style: none;
+ padding-bottom: 0px;
+}
+
+#rooms-and-nicks.hidden {
+ display: none;
+}
+
+#rooms-and-nicks .room.server {
+ background-color: #F2EFD3;
+ position: relative;
+}
+
+#rooms-and-nicks:hover .room.server .content-item {
+ width: 130px;
+}
+
+#rooms-and-nicks .add-channel {
+ position: absolute;
+ right: 2px;
+ top: -1px;
+ cursor: pointer;
+ font-size: 18px;
+ display: none;
+}
+
+#rooms-and-nicks:hover .add-channel {
+ display: block;
+}
+
+#rooms-and-nicks .add-channel:hover {
+ color: #888;
+}
+
+#rooms-and-nicks .room.channel:first-child {
+ padding-top: 3px;
+}
+
+#rooms-and-nicks .room.channel:first-child .content-item {
+ padding-bottom: 3px;
+ line-height: 20px;
+}
+
+#rooms-and-nicks not(.current-server) + .channels .room.channel:nth-last-child(2) {
+ padding-bottom: 3px;
+}
+#rooms-and-nicks .room.channel:last-child {
+ padding-bottom: 3px;
+}
+
+#rooms-and-nicks not(.current-server) + .channels .room.channel:nth-last-child(2) .content-item {
+ padding-top: 3px;
+ line-height: 20px;
+}
+#rooms-and-nicks .room.channel:last-child .content-item {
+ padding-top: 3px;
+ line-height: 20px;
+}
+
+#rooms-and-nicks not(.current-server) + .channels .room.channel:nth-last-child(2):first-child .content-item {
+ line-height: 14px;
+}
+#rooms-and-nicks .room.channel:last-child:first-child .content-item {
+ line-height: 14px;
+}
+
+#rooms-and-nicks .current-server:not(.always-empty) + .channels .footer {
+ display: block;
+}
+
+#rooms-and-nicks .room.channel .content-item {
+ border-left: 1px solid rgba(0, 0, 0, .5);
+ padding-left: 7px;
+}
+
+#rooms-and-nicks .room.activity {
+ font-weight: bold;
+}
+
+#rooms-and-nicks .room.mention {
+ color: darkred;
+}
+
+#rooms-and-nicks .room.disconnected .content-item {
+ color: #9493A2;
+ font-style: italic;
+}
+
+#rooms-and-nicks .room.channel.disconnected .content-item {
+ border-left: 1px solid #9493A2;
+}
+
+#rooms-and-nicks .room.selected {
+ background-color: #F0E798;
+}
+
+#rooms-and-nicks .room {
+ position: relative;
+}
+
+#rooms-and-nicks .room .remove-button {
+ position: absolute;
+ right: 6px;
+ top: -1px;
+ cursor: pointer;
+ font-size: 30px;
+ color: rgba(0, 0, 0, .25);
+ display: none;
+}
+
+#rooms-and-nicks .room .remove-button:hover {
+ color: rgba(0, 0, 0, .5);
+}
+
+#rooms-and-nicks .room.selected:not(.footer):not(.always-empty) .content-item {
+ width: 123px;
+}
+#rooms-and-nicks .room:hover:not(.footer):not(.always-empty) .content-item {
+ width: 123px;
+}
+
+#rooms-and-nicks .room.selected:not(.footer):not(.always-empty) .remove-button {
+ display: inline-block;
+}
+#rooms-and-nicks .room:hover:not(.footer):not(.always-empty) .remove-button {
+ display: inline-block;
+}
+
+#nicks-container {
+ border-top: 1px solid #FFF;
+ -webkit-box-flex: 1;
+}
+
+.no-nicks #nicks-container {
+ display: none;
+}
+
+.rooms,
+.nicks {
+ padding-top: 5px;
+}
+
+.nicks li:nth-child(odd) {
+ background-color: #F2EFD3;
+}
+
+#notice {
+ background-color: #406698;
+ box-shadow: 0px 1px 4px #888;
+ color: #FFF;
+ position: absolute;
+ width: 100%;
+ -webkit-transition: 150ms;
+ top: 0;
+ padding: 2px 0;
+}
+
+#notice.hide {
+ top: -38px;
+}
+
+#notice .content {
+ display: inline;
+ padding-left: 14px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ overflow-x: hidden;
+ position: absolute;
+ top: 50%;
+ margin-top: -.5em;
+}
+
+#notice button {
+ float: right;
+ height: 22px;
+ font-size: 14px;
+ padding: 0px 4px;
+ margin: 4px;
+ border: none;
+ background-color: #ECECEC;
+ color: #505053;
+ cursor: pointer;
+ outline: none;
+}
+
+#notice button.close {
+ margin-right: 8px;
+ font-size: 12px;
+ border-radius: 42px;
+ width: 21px;
+ height: 21px;
+}
+
+#messages-and-input {
+ -webkit-box-flex: 1;
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ box-shadow: 0px 0px 8px #CCC;
+ position: relative;
+}
+
+#messages-container {
+ overflow-y: auto;
+ -webkit-overflow-scrolling: touch;
+ -webkit-box-flex: 1;
+ border-bottom: 1px solid #CCC;
+}
+
+.messages {
+ display: table;
+}
+
+.message {
+ display: table-row;
+ -webkit-user-select: initial;
+}
+
+.message.activity-marker .source-content-container {
+ border-top: 1px solid rgb(224, 179, 179);
+}
+
+.message .timestamp {
+ color: #6060C0;
+ font-style: italic;
+ font-size: smaller;
+ white-space: nowrap;
+ display: table-cell;
+ text-align: right;
+ padding: 0px 10px 0px 10px;
+ border-right: 1px solid rgba(0,0,0,0.15);
+ cursor: text;
+}
+
+.source-content-container {
+ display: table-cell;
+ padding: 4px 10px 4px 15px;
+ width: 100%;
+}
+
+.message .sender {
+ font-weight: bold;
+ padding-right: 5px;
+ margin-left: -5px;
+ white-space: nowrap;
+ display: inline;
+ text-align: right;
+ cursor: text;
+}
+
+.message .sender.empty {
+ padding-right: 0;
+}
+
+.message .text {
+ display: inline;
+ white-space: pre-wrap;
+ cursor: text;
+}
+
+#messages-container .messages .message:first-child .source-content-container {
+ padding-top: 10px;
+}
+
+#messages-container .messages .message:last-child .source-content-container {
+ padding-bottom: 8px;
+}
+
+.message:not(.privmsg) + .message.privmsg .source-content-container {
+ padding-top: 4px;
+ padding-bottom: 1px;
+}
+.message.privmsg + .message.privmsg .source-content-container {
+ padding-top: 1px;
+ padding-bottom: 1px;
+}
+
+.message:not(.group) + .message.group .source-content-container {
+ padding-top: 4px;
+ padding-bottom: 0px;
+}
+.message.group + .message.group .source-content-container {
+ padding-top: 0px;
+ padding-bottom: 0px;
+}
+
+.message:not(.notice-group) + .message.notice-group .source-content-container {
+ padding-top: 4px;
+ padding-bottom: 0px;
+}
+.message.notice-group + .message.notice-group .source-content-container {
+ padding-top: 0px;
+ padding-bottom: 0px;
+}
+
+.message.list {
+ /* empty style for now */
+}
+
+.longword { word-break: break-all; }
+
+#nick-and-input {
+ border-top: 1px solid #F9F9F9;
+ -webkit-box-flex: 0;
+ display: -webkit-box;
+ background-color: #ECECEC;
+ padding: 5px 10px 5px 10px;
+}
+
+#nick {
+ padding-top: 5px;
+}
+
+#nick > span {
+ padding-right: 10px;
+}
+
+#nick > .name:before {
+ font-weight: bold;
+ content: "[ "
+}
+#nick > .name:after {
+ font-weight: bold;
+ content: " ]"
+}
+
+#nick .away:before {
+ content: "("
+}
+#nick .away:after {
+ content: ")"
+}
+
+#input-bar {
+ -webkit-box-flex: 1;
+ display: -webkit-box;
+}
+
+#input {
+ background-color: #F9F9F9;
+ display: block;
+ -webkit-box-flex: 1;
+ width: 100%;
+ height: 30px;
+ border: 1px;
+ border-radius: 5px;
+ -webkit-box-shadow: 0px 0px 3px #888;
+ color: #505053;
+ font-size: 14px;
+ padding: 0px 8px;
+ outline: 0;
+}
+
+#input.blink {
+ -webkit-box-shadow: 0px 0px 6px #406698;
+}
+
+::-webkit-scrollbar {
+ width: 9px;
+ height: 9px;
+}
+::-webkit-scrollbar-button:start:decrement,
+::-webkit-scrollbar-button:end:increment {
+ display: block;
+ height: 0;
+}
+::-webkit-scrollbar-track-piece {
+ background-color: rgba(0,0,0,0.1);
+}
+::-webkit-scrollbar-thumb:vertical {
+ height: 50px;
+ background-color: #999;
+ border-radius: 8px;
+}
+::-webkit-scrollbar-thumb:vertical:hover {
+ background-color: #888;
+}
+::-webkit-scrollbar-thumb:horizontal {
+ width: 50px;
+ background-color: #999;
+ border-radius: 8px;
+}
diff --git a/app/topbar.css b/app/topbar.css
new file mode 100644
index 0000000..8db1003
--- /dev/null
+++ b/app/topbar.css
@@ -0,0 +1,43 @@
+#hide-channels {
+ display: inline-block;
+ font-size: 12px;
+}
+
+#topic-container {
+ border-bottom: 1px solid #CCC;
+ -webkit-box-flex: 0;
+ display: -webkit-box;
+ background-color: #ECECEC;
+ padding: 4px 6px;
+}
+
+#status {
+ -webkit-box-flex: 1;
+ padding: 4px 5px;
+}
+
+#status .topic {
+ font-style: italic;
+}
+
+.topbar-button {
+ background: rgba(0, 0, 0, .08);
+ border: 0 transparent;
+ text-decoration: none;
+ cursor: pointer;
+ border-radius: 2px;
+ -webkit-transition: .1s linear -webkit-box-shadow;
+ margin-right: 4px;
+}
+
+.topbar-button:active {
+ box-shadow: 0 0 0 1px rgba(0 ,0 ,0 ,.15) inset, 0 0 6px rgba(0, 0, 0, .2) inset;
+}
+
+.topbar-button:hover {
+ background: rgba(0, 0, 0, .13);
+}
+
+.topbar-button:focus {
+ outline: none;
+}
diff --git a/app/wirc.html b/app/wirc.html
new file mode 100644
index 0000000..27dd1db
--- /dev/null
+++ b/app/wirc.html
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/wirc.js b/app/wirc.js
new file mode 100644
index 0000000..349e824
--- /dev/null
+++ b/app/wirc.js
@@ -0,0 +1,237 @@
+var msgRe = /([^ ]+) (<[^>]+>) (.*)/;
+var kibozeRe = /[Nn]eal/;
+var urlRe = /[a-z]+:\/\/[^ ]*/;
+
+var nick = "Mme. M";
+
+var scrollbackLength = 500;
+
+if (String.prototype.startsWith == null) {
+ String.prototype.startsWith = function(needle) {
+ return this.lastIndexOf(needle, 0) == 0;
+ }
+}
+
+function getTemplate(className) {
+ return document.templates.getElementsByClassName(className)[0].cloneNode(true);
+}
+
+function isinView(oObject) {
+ return (oObject.offsetParent.clientHeight <= oObject.offsetTop);
+}
+
+function selectForum(room) {
+ var kids = document.rooms_list.childNodes;
+
+ for (i = 0; i < kids.length; i += 1) {
+ e = kids[i];
+ if (e == room) {
+ e.className = "room selected";
+ e.messages.display = "block";
+ } else {
+ e.className = "room";
+ e.messages.display = "none";
+ }
+ }
+
+ if (room.lastChild) {
+ room.lastChild.scrollIntoView(false);
+ }
+}
+
+fora = {}
+function getForumElement(forum) {
+ var fe = fora[forum];
+
+ if (! fe) {
+ var room = getTemplate("channel room");
+ room.textContent = forum;
+ document.rooms_list.appendChild(room);
+
+ fe = getTemplate("messages");
+ fe.room = room;
+
+ room.messages = fe;
+ // XXX: split out into non-anon function
+ room.addEventListener("click", function() {selectForum(fe)});
+
+ fora[forum] = fe;
+ document.getElementById("messages-container").appendChild(fe);
+ }
+
+ return fe;
+}
+
+function addMessagePart(p, className, text) {
+ var e = document.createElement("span");
+ e.className = className;
+ e.appendChild(document.createTextNode(text));
+ p.appendChild(e);
+ p.appendChild(document.createTextNode(" "));
+}
+
+function addText(p, text, kiboze) {
+ // Look for a URL
+ var txtElement = document.createElement("span");
+ txtElement.className = "text";
+ var rhs = text;
+ var match;
+
+ while ((match = urlRe.exec(rhs)) != null) {
+ var before = rhs.substr(0, match.index);
+ var a = document.createElement("a");
+ var href = match[0];
+
+ if (href.indexOf("hxx") == 0) {
+ href = "htt" + href.substr(3);
+ }
+ a.href = href
+ a.target = "_blank";
+ a.appendChild(document.createTextNode(match[0]));
+ txtElement.appendChild(document.createTextNode(before));
+ txtElement.appendChild(a);
+ rhs = rhs.substr(match.index + match[0].length);
+ }
+ txtElement.appendChild(document.createTextNode(rhs));
+ p.appendChild(txtElement);
+
+ if ((kiboze) || (-1 != text.search(kibozeRe))) {
+ var k = document.getElementById("kiboze");
+ var p2 = p.cloneNode(true);
+
+ if (k) {
+ k.insertBefore(p2, k.firstChild);
+ p2.onclick = function() { focus(p); }
+
+ // Setting title makes the tab flash sorta
+ document.title = document.title;
+ }
+ }
+}
+
+function focus(e) {
+ var pct = 1;
+ var timeout;
+
+ selectForum(e.parentNode);
+ e.scrollIntoView(false);
+ e.style.backgroundColor = "yellow";
+
+ timeout = setInterval(function() {
+ pct = pct - 0.1;
+ e.style.backgroundColor = "rgba(255, 255, 0, " + pct + ")";
+ if (pct <= 0) {
+ e.style.backgroundColor = "inherit";
+ clearInterval(timeout);
+ }
+ }, 50)
+}
+
+function addMessage(txt) {
+ var lhs = txt.split(" :", 1)[0]
+ var parts = lhs.split(' ')
+ var ts = new Date(parts[0] * 1000);
+ var fullSender = parts[1];
+ var command = parts[2];
+ var sender = parts[3];
+ var forum = parts[4];
+ var args = parts.slice(5);
+ var msg = txt.substr(lhs.length + 2)
+
+ var forumElement = getForumElement(forum);
+ var p = getTemplate("message");
+
+ addMessagePart(p, "timestamp", ts.toLocaleTimeString());
+
+ switch (command) {
+ case "PING":
+ case "PONG":
+ return;
+ break;
+ case "PRIVMSG":
+ addMessagePart(p, "forum", forum);
+ addMessagePart(p, "sender", sender);
+ addText(p, msg, (sender == forum));
+ break;
+ case "NOTICE":
+ addMessagePart(p, "forum", forum);
+ addMessagePart(p, "sender notice", sender);
+ addText(p, msg, (sender == forum));
+ break;
+ default:
+ addMessagePart(p, "forum", forum);
+ addMessagePart(p, "sender", sender);
+ addMessagePart(p, "raw", command + " " + args + " " + msg);
+ break;
+ }
+ while (forumElement.childNodes.length > scrollbackLength) {
+ forumElement.removeChild(forumElement.firstChild)
+ }
+ forumElement.appendChild(p);
+ p.scrollIntoView(false);
+}
+
+function newmsg(oEvent) {
+ msgs = oEvent.data.split("\n");
+
+ var first = Math.max(0, msgs.length - scrollbackLength);
+ for (var i = first; i < msgs.length; i += 1) {
+ addMessage(msgs[i]);
+ }
+}
+
+function handleInput(oEvent) {
+ console.log(oEvent);
+ var oReq = new XMLHttpRequest();
+ function reqListener() {
+ }
+
+ var txt = oEvent.target.value;
+ if (txt.startsWith("/connect ")) {
+ // XXX: should allow tokens with spaces
+ var parts = txt.split(" ");
+
+ connect(parts[1], parts[2], parts[3]);
+ } else {
+ oReq.onload = reqListener;
+ oReq.open("POST", window.postURL, true);
+ oReq.send(new FormData(event.target));
+ }
+
+ oEvent.target.value = "";
+
+ return false;
+}
+
+function connect(url, server, authtok) {
+ document.postURL = url;
+ var pullURL = url + "?server=" + server + "&auth=" + authtok
+
+ if (document.source != null) {
+ document.source.close();
+ }
+ document.source = new EventSource(pullURL);
+ document.source.onmessage = newmsg;
+
+ chrome.storage.sync.set({"connections": [[url, server, authtok]]});
+}
+
+function restore(items) {
+ var connections = items["connections"];
+
+ for (var k = 0; k < connections.length; k += 1) {
+ var conn = connections[k];
+
+ connect(conn[0], conn[1], conn[2]);
+ }
+}
+
+function init() {
+ chrome.storage.sync.get("connections", restore);
+ document.getElementById("input").addEventListener("change", handleInput);
+
+ document.templates = document.getElementById("templates");
+ document.rooms_list = document.getElementById("rooms-container").getElementsByClassName("rooms")[0];
+}
+
+window.addEventListener("load", init);
diff --git a/app/wirc.png b/app/wirc.png
new file mode 100644
index 0000000..372b05c
Binary files /dev/null and b/app/wirc.png differ
diff --git a/chat.svg b/chat.svg
new file mode 100644
index 0000000..f8841d1
--- /dev/null
+++ b/chat.svg
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+ Openclipart
+
+
+
+ 2008-02-19T10:10:56
+
+ https://openclipart.org/detail/14475/callout-chat-by-ericlemerdy
+
+
+ ericlemerdy
+
+
+
+
+ balloon
+ bubble
+ callout
+ speech
+
+
+
+
+
+
+
+
+
+
+
diff --git a/rotate b/rotate
new file mode 100755
index 0000000..f88beaa
--- /dev/null
+++ b/rotate
@@ -0,0 +1,7 @@
+#! /bin/sh
+
+nl=$(date +log.%s)
+cp log $nl
+tail -n 40 $nl > log
+
+echo "Don't forget to reload the clients"
diff --git a/src/wirc.cgi/wirc.cgi.go b/src/wirc.cgi/wirc.cgi.go
new file mode 100644
index 0000000..1bc04a4
--- /dev/null
+++ b/src/wirc.cgi/wirc.cgi.go
@@ -0,0 +1,141 @@
+package main
+
+import (
+ "fmt"
+ "github.com/go-fsnotify/fsnotify"
+ "io/ioutil"
+ "log"
+ "os"
+ "bufio"
+ "strconv"
+ "strings"
+ "net/http"
+ "net/http/cgi"
+ "time"
+ "path"
+)
+
+type Handler struct {
+ cgi.Handler
+}
+
+var ServerDir string
+
+func ReadString(fn string) string {
+ octets, err := ioutil.ReadFile(fn)
+ if err != nil {
+ log.Fatal(err)
+ }
+ return strings.TrimSpace(string(octets))
+}
+
+func tail(w http.ResponseWriter, pos int64) {
+ logfn := path.Join(ServerDir, "log")
+
+ f, err := os.Open(logfn)
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer f.Close()
+
+ watcher, err := fsnotify.NewWatcher()
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer watcher.Close()
+ watcher.Add(logfn)
+
+ for {
+ printid := false
+
+ newpos, err := f.Seek(pos, 0)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ if newpos < pos {
+ // File has been truncated!
+ pos = 0
+ f.Seek(0, 0)
+ }
+
+ bf := bufio.NewScanner(f)
+ for bf.Scan() {
+ t := bf.Text()
+ pos += int64(len(t)) + 1 // XXX: this breaks if we ever see \r\n
+ fmt.Fprintf(w, "data: %s\n", t)
+ printid = true
+ }
+ if printid {
+ _, err = fmt.Fprintf(w, "id: %d\n\n", pos)
+ }
+ if err != nil {
+ break
+ }
+ w.(http.Flusher).Flush()
+
+ select {
+ case _ = <-watcher.Events:
+ // Somethin' happened!
+ case err := <-watcher.Errors:
+ log.Fatal(err)
+ }
+ }
+}
+
+func handleCommand(w http.ResponseWriter, text string, target string) {
+ fn := path.Join(ServerDir, fmt.Sprintf("outq/cgi.%d", time.Now().Unix()))
+ f, err := os.Create(fn)
+ if err != nil {
+ fmt.Fprintln(w, "NO")
+ fmt.Fprintln(w, err)
+ return
+ }
+ defer f.Close()
+
+ switch {
+ case strings.HasPrefix(text, "/quote "):
+ fmt.Fprintln(f, text[7:])
+ case strings.HasPrefix(text, "/me "):
+ fmt.Fprintf(f, "PRIVMSG %s :\001ACTION %s\001\n", target, text[4:])
+ default:
+ fmt.Fprintf(f, "PRIVMSG %s :%s\n", target, text)
+ }
+
+ fmt.Fprintln(w, "OK")
+}
+
+
+func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ BaseDir := "servers"
+ DefaultDir := path.Join(BaseDir, "default")
+ ServerDir = path.Join(BaseDir, r.FormValue("server"))
+
+ if path.Dir(DefaultDir) != path.Dir(ServerDir) {
+ ServerDir = DefaultDir
+ }
+
+ authtok := ReadString(path.Join(ServerDir, "authtok"))
+ if r.FormValue("auth") != authtok {
+ w.Header().Set("Content-Type", "text/plain")
+ fmt.Fprintln(w, "NO")
+ return
+ }
+ switch r.FormValue("type") {
+ case "command":
+ w.Header().Set("Content-Type", "text/plain")
+ handleCommand(w, r.Form.Get("text"), r.FormValue("target"))
+ default:
+ w.Header().Set("Content-Type", "text/event-stream")
+ id, _ := strconv.ParseInt(os.Getenv("HTTP_LAST_EVENT_ID"), 0, 64)
+ tail(w, id)
+ }
+}
+
+func main() {
+ h := Handler{}
+ if err := cgi.Serve(h); err != nil {
+ log.Fatal(err)
+ }
+}
+
diff --git a/src/wirc/wirc.go b/src/wirc/wirc.go
new file mode 100644
index 0000000..8ae72ff
--- /dev/null
+++ b/src/wirc/wirc.go
@@ -0,0 +1,267 @@
+package main
+
+import (
+ "bufio"
+ "crypto/tls"
+ "flag"
+ "fmt"
+ "log"
+ "net"
+ "os"
+ "strings"
+ "time"
+)
+
+type Message struct {
+ Command string
+ FullSender string
+ Sender string
+ Forum string
+ Args []string
+ Text string
+}
+
+var running bool = true
+var nick string
+var gecos string
+var logq chan Message
+
+func isChannel(s string) bool {
+ if (s == "") {
+ return false
+ }
+
+ switch s[0] {
+ case '#', '&', '!', '+', '.', '-':
+ return true
+ default:
+ return false
+ }
+}
+
+func (m Message) String() string {
+ args := strings.Join(m.Args, " ")
+ return fmt.Sprintf("%s %s %s %s %s :%s", m.FullSender, m.Command, m.Sender, m.Forum, args, m.Text)
+}
+
+func logLoop() {
+ logf, err := os.OpenFile("log", os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0666)
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer logf.Close()
+ for m := range logq {
+ fmt.Fprintf(logf, "%d %s\n", time.Now().Unix(), m.String())
+ }
+}
+
+func nuhost(s string) (string, string, string) {
+ var parts []string
+
+ parts = strings.SplitN(s, "!", 2)
+ if len(parts) == 1 {
+ return s, "", ""
+ }
+ n := parts[0]
+ parts = strings.SplitN(parts[1], "@", 2)
+ if len(parts) == 1 {
+ return s, "", ""
+ }
+ return n, parts[0], parts[1]
+}
+
+func connect(host string, dotls bool) (net.Conn, error) {
+ if dotls {
+ config := &tls.Config{
+ InsecureSkipVerify: true,
+ }
+ return tls.Dial("tcp", host, config)
+ } else {
+ return net.Dial("tcp", host)
+ }
+}
+
+func readLoop(conn net.Conn, inq chan<- string) {
+ scanner := bufio.NewScanner(conn)
+ for scanner.Scan() {
+ inq <- scanner.Text()
+ }
+ close(inq)
+}
+
+func writeLoop(conn net.Conn, outq <-chan string) {
+ for v := range outq {
+ m, _ := parse(v)
+ logq <- m
+ fmt.Fprintln(conn, v)
+ }
+}
+
+func parse(v string) (Message, error) {
+ var m Message
+ var parts []string
+ var lhs string
+
+ parts = strings.SplitN(v, " :", 2)
+ if len(parts) == 2 {
+ lhs = parts[0]
+ m.Text = parts[1]
+ } else {
+ lhs = v
+ m.Text = ""
+ }
+
+ m.FullSender = "."
+ m.Forum = "."
+ m.Sender = "."
+
+ parts = strings.Split(lhs, " ")
+ if parts[0][0] == ':' {
+ m.FullSender = parts[0][1:]
+ parts = parts[1:]
+
+ n, u, _ := nuhost(m.FullSender)
+ if u != "" {
+ m.Sender = n
+ }
+ }
+
+ m.Command = strings.ToUpper(parts[0])
+ switch m.Command {
+ case "PRIVMSG", "NOTICE":
+ switch {
+ case isChannel(parts[1]):
+ m.Forum = parts[1]
+ case m.FullSender == ".":
+ m.Forum = parts[1]
+ default:
+ m.Forum = m.Sender
+ }
+ case "PART", "MODE", "TOPIC", "KICK":
+ m.Forum = parts[1]
+ case "JOIN":
+ if len(parts) == 1 {
+ m.Forum = m.Text
+ m.Text = ""
+ } else {
+ m.Forum = parts[1]
+ }
+ case "INVITE":
+ if m.Text != "" {
+ m.Forum = m.Text
+ m.Text = ""
+ } else {
+ m.Forum = parts[2]
+ }
+ case "NICK":
+ if len(parts) > 1 {
+ m.Sender = parts[1]
+ } else {
+ m.Sender = m.Text
+ m.Text = ""
+ }
+ m.Forum = m.Sender
+ }
+
+ return m, nil
+}
+
+func dispatch(outq chan<- string, m Message) {
+ logq <- m
+ switch m.Command {
+ case "PING":
+ outq <- "PONG :" + m.Text
+ case "433":
+ nick = nick + "_"
+ outq <- fmt.Sprintf("NICK %s", nick)
+ }
+}
+
+func handleInfile(path string, outq chan<- string) {
+ f, err := os.Open(path)
+ if err != nil {
+ return
+ }
+ defer f.Close()
+ os.Remove(path)
+ inf := bufio.NewScanner(f)
+ for inf.Scan() {
+ txt := inf.Text()
+ outq <- txt
+ }
+}
+
+func monitorDirectory(dirname string, dir *os.File, outq chan<- string) {
+ latest := time.Unix(0, 0)
+ for running {
+ fi, err := dir.Stat()
+ if err != nil {
+ break
+ }
+ current := fi.ModTime()
+ if current.After(latest) {
+ latest = current
+ dn, _ := dir.Readdirnames(0)
+ for _, fn := range dn {
+ path := dirname + string(os.PathSeparator) + fn
+ handleInfile(path, outq)
+ }
+ _, _ = dir.Seek(0, 0)
+ }
+ time.Sleep(500 * time.Millisecond)
+ }
+}
+
+func usage() {
+ fmt.Fprintf(os.Stderr, "Usage: %s [OPTIONS] HOST:PORT\n", os.Args[0])
+ flag.PrintDefaults()
+}
+
+func main() {
+ dotls := flag.Bool("notls", true, "Disable TLS security")
+ outqdir := flag.String("outq", "outq", "Output queue directory")
+ flag.StringVar(&gecos, "gecos", "Bob The Merry Slug", "Gecos entry (full name)")
+
+ flag.Parse()
+ if flag.NArg() != 2 {
+ fmt.Fprintln(os.Stderr, "Error: must specify nickname and host")
+ os.Exit(69)
+ }
+
+ dir, err := os.Open(*outqdir)
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer dir.Close()
+
+ nick := flag.Arg(0)
+ host := flag.Arg(1)
+
+ conn, err := connect(host, *dotls)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ inq := make(chan string)
+ outq := make(chan string)
+ logq = make(chan Message)
+ go logLoop()
+ go readLoop(conn, inq)
+ go writeLoop(conn, outq)
+ go monitorDirectory(*outqdir, dir, outq)
+
+ outq <- fmt.Sprintf("NICK %s", nick)
+ outq <- fmt.Sprintf("USER %s %s %s: %s", nick, nick, nick, gecos)
+ for v := range inq {
+ p, err := parse(v)
+ if err != nil {
+ continue
+ }
+ dispatch(outq, p)
+ }
+
+ running = false
+
+ close(outq)
+ close(logq)
+}