Merge remote-tracking branch 'woozle/master'

This commit is contained in:
Neale Pickett 2014-10-24 21:20:04 -06:00
commit 3bbee55de2
18 changed files with 2087 additions and 0 deletions

2
Makefile Normal file
View File

@ -0,0 +1,2 @@
all:
GOPATH=$(CURDIR) go build -v all

64
PROTOCOL Normal file
View File

@ -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

29
README Normal file
View File

@ -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.

View File

@ -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"
}
}

5
app/background.js Normal file
View File

@ -0,0 +1,5 @@
chrome.app.runtime.onLaunched.addListener(function() {
chrome.app.window.create('wirc.html', {
'state': 'normal'
})
})

361
app/example.html Normal file
View File

@ -0,0 +1,361 @@
<html><head>
<meta charset="utf-8">
<!-- generic -->
<script src="util.js"></script>
<script src="keycodes.js"></script>
<script src="message_handler.js"></script>
<script src="event_emitter.js"></script>
<script src="chat/notification.js"></script>
<script src="chat/notification_group.js"></script>
<script src="net/abstract_tcp_socket.js"></script>
<script src="event.js"></script>
<script src="timer.js"></script>
<script src="context.js"></script>
<!-- net -->
<script src="net/chrome_socket.js"></script>
<script src="net/ssl_socket.js"></script>
<script src="net/remote_socket.js"></script>
<!-- irc protocol -->
<script src="irc/irc.js"></script>
<script src="irc/irc_util.js"></script>
<script src="irc/ctcp_handler.js"></script>
<script src="irc/server_response_handler.js"></script>
<!-- scripting -->
<script src="script/script.js"></script>
<script src="script/prepackaged/source_array.js"></script>
<script src="script/script_handler.js"></script>
<script src="script/script_loader.js"></script>
<!-- one identity -->
<script src="remote_connection.js"></script>
<script src="chat/remote_connection_handler.js"></script>
<script src="remote_device.js"></script>
<!-- user input -->
<script src="chat/custom_command_parser.js"></script>
<script src="input_stack.js"></script>
<script src="completion_finder.js"></script>
<script src="auto_complete.js"></script>
<script src="keyboard_shortcut_map.js"></script>
<script src="user_input_handler.js"></script>
<script src="chat/user_command_handler.js"></script>
<script src="chat/user_command.js"></script>
<script src="chat/developer_commands.js"></script>
<!-- client and UI -->
<script src="chat/chat.js"></script>
<script src="chat/walkthrough.js"></script>
<script src="chat/storage.js"></script>
<script src="chat/chat_log.js"></script>
<script src="chat/notice.js"></script>
<script src="chat/message_formatter.js"></script>
<script src="chat/html_list.js"></script>
<script src="chat/nick_mentioned_notification.js"></script>
<script src="chat/irc_message_handler.js"></script>
<script src="chat/window.js"></script>
<script src="chat/window_message_renderer.js"></script>
<script src="chat/scrollable.js"></script>
<script src="chat/help_message_renderer.js"></script>
<script src="chat/nick_list.js"></script>
<script src="chat/channel_list.js"></script>
<script src="chat/window_list.js"></script>
<!-- third party -->
<script src="third_party/jquery.js"></script>
<script src="third_party/md5.js"></script>
<!-- forge SSL library -->
<script type="text/javascript" src="third_party/forge/debug.js"></script>
<script type="text/javascript" src="third_party/forge/util.js"></script>
<script type="text/javascript" src="third_party/forge/log.js"></script>
<script type="text/javascript" src="third_party/forge/socket.js"></script>
<script type="text/javascript" src="third_party/forge/md5.js"></script>
<script type="text/javascript" src="third_party/forge/sha1.js"></script>
<script type="text/javascript" src="third_party/forge/sha256.js"></script>
<script type="text/javascript" src="third_party/forge/hmac.js"></script>
<script type="text/javascript" src="third_party/forge/aes.js"></script>
<script type="text/javascript" src="third_party/forge/pem.js"></script>
<script type="text/javascript" src="third_party/forge/asn1.js"></script>
<script type="text/javascript" src="third_party/forge/jsbn.js"></script>
<script type="text/javascript" src="third_party/forge/prng.js"></script>
<script type="text/javascript" src="third_party/forge/random.js"></script>
<script type="text/javascript" src="third_party/forge/oids.js"></script>
<script type="text/javascript" src="third_party/forge/rsa.js"></script>
<script type="text/javascript" src="third_party/forge/pbe.js"></script>
<script type="text/javascript" src="third_party/forge/x509.js"></script>
<script type="text/javascript" src="third_party/forge/pki.js"></script>
<script type="text/javascript" src="third_party/forge/tls.js"></script>
<script type="text/javascript" src="third_party/forge/aesCipherSuites.js"></script>
<script type="text/javascript" src="third_party/forge/tlssocket.js"></script>
<script type="text/javascript" src="third_party/forge/http.js"></script>
<link id="main-style" rel="stylesheet" href="style.css">
<link rel="stylesheet" href="message_style.css">
<link rel="stylesheet" href="topbar.css">
<title>CIRC undefined</title></head>
<body>
<!-- templates -->
<div id="templates">
<div class="server-channels">
<div class="server room">
<div class="content-item"></div>
<div class="remove-button">-</div>
</div>
<ul class="channels"></ul>
</div>
<li class="channel room">
<div class="content-item"></div>
<div class="remove-button">-</div>
</li>
<ul class="nicks"></ul>
<li class="nick">
<div class="content-item"></div>
</li>
<ul class="messages"></ul>
<li class="message">
<div class="timestamp"></div>
<div class="source-content-container">
<div class="source"></div>
<div class="content"></div>
</div>
</li>
</div>
<!-- main -->
<div id="main">
<div id="main-top-border"></div>
<div id="rooms-and-nicks" class="">
<div id="rooms-container">
<h1>rooms</h1>
<div class="rooms"><div class="server-channels">
<div class="server room current-server">
<div class="content-item">us.slashnet.org</div>
<div class="remove-button">-</div>
</div>
<ul class="channels"><li class="channel room selected">
<div class="content-item">#tron</div>
<div class="remove-button">-</div>
</li><li class="channel room footer">
<div class="content-item">+ add channel</div>
<div class="remove-button">-</div>
</li></ul>
</div><div class="server-channels">
<div class="server room footer">
<div class="content-item">+ add server</div>
<div class="remove-button">-</div>
</div>
<ul class="channels"></ul>
</div></div>
</div>
<div id="nicks-container">
<h1>members</h1>
<ul class="nicks"><li class="nick">
<div class="content-item">also</div>
</li><li class="nick">
<div class="content-item">atob</div>
</li><li class="nick">
<div class="content-item">Bermuda</div>
</li><li class="nick">
<div class="content-item">bit</div>
</li><li class="nick">
<div class="content-item">bk</div>
</li><li class="nick">
<div class="content-item">bz2</div>
</li><li class="nick">
<div class="content-item">caycos</div>
</li><li class="nick">
<div class="content-item">ckape</div>
</li><li class="nick">
<div class="content-item">clavicle</div>
</li><li class="nick">
<div class="content-item">CrackMonkey</div>
</li><li class="nick">
<div class="content-item">Dumont</div>
</li><li class="nick">
<div class="content-item">dzho</div>
</li><li class="nick">
<div class="content-item">emad</div>
</li><li class="nick">
<div class="content-item">eythian</div>
</li><li class="nick">
<div class="content-item">felixc</div>
</li><li class="nick">
<div class="content-item">fo0bar</div>
</li><li class="nick">
<div class="content-item">fuzzie</div>
</li><li class="nick">
<div class="content-item">GodEater</div>
</li><li class="nick">
<div class="content-item">hollow</div>
</li><li class="nick">
<div class="content-item">hoylemd</div>
</li><li class="nick">
<div class="content-item">jhewl</div>
</li><li class="nick">
<div class="content-item">jv</div>
</li><li class="nick">
<div class="content-item">kees</div>
</li><li class="nick">
<div class="content-item">khmer</div>
</li><li class="nick">
<div class="content-item">lamont</div>
</li><li class="nick">
<div class="content-item">lexicondal</div>
</li><li class="nick">
<div class="content-item">neale</div>
</li><li class="nick">
<div class="content-item">nemo</div>
</li><li class="nick">
<div class="content-item">nerdtron3000</div>
</li><li class="nick">
<div class="content-item">nornagon</div>
</li><li class="nick">
<div class="content-item">Octal</div>
</li><li class="nick">
<div class="content-item">pdx6</div>
</li><li class="nick">
<div class="content-item">pedro</div>
</li><li class="nick">
<div class="content-item">Randall</div>
</li><li class="nick">
<div class="content-item">sarah</div>
</li><li class="nick">
<div class="content-item">Sciri</div>
</li><li class="nick">
<div class="content-item">scorche</div>
</li><li class="nick">
<div class="content-item">scorche|sh</div>
</li><li class="nick">
<div class="content-item">Screwtape</div>
</li><li class="nick">
<div class="content-item">sneakums</div>
</li><li class="nick">
<div class="content-item">squinky</div>
</li><li class="nick">
<div class="content-item">stat</div>
</li><li class="nick">
<div class="content-item">teferi</div>
</li><li class="nick">
<div class="content-item">tiaz</div>
</li><li class="nick">
<div class="content-item">watson</div>
</li><li class="nick">
<div class="content-item">wcarss</div>
</li><li class="nick">
<div class="content-item">wombat</div>
</li><li class="nick">
<div class="content-item">X11R5</div>
</li><li class="nick">
<div class="content-item">Zen</div>
</li><li class="nick footer">
<div class="content-item">footer</div>
</li></ul></div>
</div>
<div class="dragbar"></div>
<div id="messages-and-input">
<div id="topic-container">
<button id="hide-channels" class="topbar-button">=</button>
<div id="status" class="content-item"><span title="Spel werkt, vertel het door! | hype mismatch error | ERG DRUK | <ginnie> everything is awesome! | Save The Date: 20150212 is Dumont's 15th Birthday | sparrows form Voltron | Deksels! | sysadmin establishment &quot;hanged&quot; by lunatic devop | Levis dehydrates unisexual cleavage. | A fanfare for locked traffic --" class="topic">Spel werkt, vertel het door! | hype mismatch error | ERG DRUK | &lt;ginnie&gt; everything is awesome! | Save The Date: 20150212 is Dumont's 15th Birthday | sparrows form Voltron | Deksels! | sysadmin establishment "hanged" by lunatic devop | Levis dehydrates unisexual cleavage. | A fanfare for locked traffic --</span></div>
</div>
<div id="notice" class="hide">
<div class="content"></div>
<button class="close">X</button>
<button class="option2 hidden"></button>
<button class="option1 hidden"></button>
</div>
<div id="messages-container"><ul class="messages"><li class="message system">
<div class="timestamp">3:46:12 PM</div>
<div class="source-content-container">
<div class="source empty"></div>
<div class="content"><span>Awesome, you've connected to #tron.</span></div>
</div>
</li><li class="message system">
<div class="timestamp">3:46:12 PM</div>
<div class="source-content-container">
<div class="source empty"></div>
<div class="content"><span>If you're ever stuck, type /help to see a list of all commands.</span></div>
</div>
</li><li class="message system">
<div class="timestamp">3:46:12 PM</div>
<div class="source-content-container">
<div class="source empty"></div>
<div class="content"><span>You can switch windows with alt+[0-9] or click in the channel list on the left.</span></div>
</div>
</li><li class="message update join self">
<div class="timestamp">3:46:12 PM</div>
<div class="source-content-container">
<div class="source empty"></div>
<div class="content"><span>(You joined the channel)</span></div>
</div>
</li><li class="message notice topic">
<div class="timestamp">3:46:12 PM</div>
<div class="source-content-container">
<div class="source empty"></div>
<div class="content"><span>The topic is: Spel werkt, vertel het door! | hype mismatch error | ERG DRUK | &lt;ginnie&gt; everything is awesome! | Save The Date: 20150212 is Dumont's 15th Birthday | sparrows form Voltron | Deksels! | sysadmin establishment "hanged" by lunatic devop | Levis dehydrates unisexual cleavage. | A fanfare for locked traffic --</span></div>
</div>
</li><li class="message notice topic_info">
<div class="timestamp">3:46:12 PM</div>
<div class="source-content-container">
<div class="source empty"></div>
<div class="content"><span>Topic set by sneakums on Fri Oct 17 2014 16:05:00 GMT-0600 (MDT).</span></div>
</div>
</li><li class="message notice-group notice">
<div class="timestamp">3:46:12 PM</div>
<div class="source-content-container">
<div class="source empty"></div>
<div class="content"><span>Received a CTCP VERSION from squinky.</span></div>
</div>
</li><li class="message update privmsg self">
<div class="timestamp">3:47:13 PM</div>
<div class="source-content-container">
<div class="source" colornumber="22"><span>nerdtron3000</span></div>
<div class="content"><span>merf</span></div>
</div>
</li><li class="message update privmsg self">
<div class="timestamp">3:47:53 PM</div>
<div class="source-content-container">
<div class="source" colornumber="22"><span>nerdtron3000</span></div>
<div class="content"><span>X11R5: say something to me</span></div>
</div>
</li><li class="message update mention privmsg">
<div class="timestamp">3:47:55 PM</div>
<div class="source-content-container">
<div class="source" colornumber="22"><span>X11R5</span></div>
<div class="content"><span>nerdtron3000: Get me some of those things where 99 times out on to something.</span></div>
</div>
</li><li class="message update privmsg self">
<div class="timestamp">3:48:01 PM</div>
<div class="source-content-container">
<div class="source" colornumber="22"><span>nerdtron3000</span></div>
<div class="content"><span>...</span></div>
</div>
</li><li class="message update privmsg">
<div class="timestamp">3:48:02 PM</div>
<div class="source-content-container">
<div class="source" colornumber="12"><span>Dumont</span></div>
<div class="content"><span>[You have a sad feeling for a moment, then it passes.]</span></div>
</div>
</li></ul></div>
<div id="nick-and-input">
<div id="nick"><span class="name">nerdtron3000</span></div><input id="input" class="" style="-webkit-transition: 300ms; transition: 300ms;">
</div>
</div>
</div>
<script src="main.js"></script>
<iframe src="script_frame.html" style="display: none;"></iframe></body></html>

21
app/manifest.json Normal file
View File

@ -0,0 +1,21 @@
{
"manifest_version": 2,
"version": "1.0",
"name": "__MSG_appName__",
"short_name": "__MSG_appShortName__",
"description": "__MSG_appDesc__",
"author": "Neale Pickett <neale@woozle.org>",
"icons": {"128": "wirc.png"},
"app": {
"background": {
"scripts": ["background.js"]
}
},
"permissions": [
"storage",
"fileSystem",
"https://woozle.org/"
],
"default_locale": "en"
}

172
app/message_style.css Normal file
View File

@ -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;
}

38
app/server.js Normal file
View File

@ -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);
}

510
app/style.css Normal file
View File

@ -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;
}

43
app/topbar.css Normal file
View File

@ -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;
}

79
app/wirc.html Normal file
View File

@ -0,0 +1,79 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="message_style.css">
<link rel="stylesheet" href="topbar.css">
<script src="wirc.js"></script>
</head>
<body>
<!-- templates -->
<div id="templates">
<div class="server-channels">
<div class="server room">
<div class="content-item"></div>
<div class="remove-button">-</div>
</div>
<ul class="channels"></ul>
</div>
<li class="channel room">
<div class="content-item"></div>
<div class="remove-button">-</div>
</li>
<ul class="nicks"></ul>
<li class="nick">
<div class="content-item"></div>
</li>
<ul class="messages"></ul>
<li class="message">
<div class="timestamp"></div>
<div class="source-content-container">
<div class="source"></div>
<div class="content"></div>
</div>
</li>
</div>
<!-- main -->
<div id="main">
<div id="main-top-border"></div>
<div id="rooms-and-nicks" class="no-nicks">
<div id="rooms-container">
<h1>rooms</h1>
<div class="rooms"></div>
</div>
<div id="nicks-container">
<h1>members</h1>
</div>
</div>
<div class="dragbar"></div>
<div id="messages-and-input">
<div id="topic-container">
<button id="hide-channels" class="topbar-button">=</button>
<div id="status" class="content-item"></div>
</div>
<div id="notice" class="hide">
<div class="content"></div>
<button class="close">X</button>
<button class="option2 hidden"></button>
<button class="option1 hidden"></button>
</div>
<div id="messages-container"></div>
<div id="nick-and-input">
<div id="nick"></div><input id="input">
</div>
</div>
</div>
</body>
</html>

237
app/wirc.js Normal file
View File

@ -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);

BIN
app/wirc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

96
chat.svg Normal file
View File

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
id="svg548"
inkscape:version="0.48.5 r10040"
inkscape:export-filename="/home/neale/tmp/chat.png"
viewBox="0 0 500 500"
sodipodi:version="0.32"
inkscape:export-xdpi="11.52"
version="1.0"
inkscape:output_extension="org.inkscape.output.svg.inkscape"
inkscape:export-ydpi="11.52"
sodipodi:docname="chat.svg"
width="100%"
height="100%">
<defs
id="defs550" />
<sodipodi:namedview
id="base"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:zoom="1.26"
inkscape:current-layer="g3182"
inkscape:cx="258.38604"
inkscape:cy="234.12698"
inkscape:window-width="1280"
showgrid="false"
inkscape:window-height="738"
inkscape:window-maximized="1" />
<g
id="g3182"
transform="translate(14.44,59.9282)">
<path
id="path779"
style="fill:#660080;fill-rule:evenodd;stroke:#dd55ff;stroke-width:15;stroke-linecap:round;stroke-linejoin:round"
d="m 132.35508,161.83857 c -64.776001,1.05 -121.910001,37.14 -134.0400006,88.35 -13.8580004,58.5 36.2389996,114.67 111.8400006,125.43 0.01,0.09 -0.01,0.22 -0.02,0.35 l -0.43,0.02 -58.912001,54.78 121.030001,-56.86 -1.19,0.04 0.78,-0.6 1.73,-0.05 -1.32,0.61 1.66,-0.06 v -0.57 h 0.02 c 52.37,-11.54 89.09,-42.8 98.91,-84.23 13.86,-58.52 -36.3,-114.72 -111.95,-125.44 -9.46,-1.34 -18.86,-1.91 -28.11,-1.77 z"
inkscape:connector-curvature="0" />
<path
id="path2391"
style="fill:#b3ff80;fill-rule:evenodd;stroke:#338000;stroke-width:22.78469849000000025;stroke-linecap:round;stroke-linejoin:round"
d="m 276.28812,-45.717333 c 92.80208,1.491258 174.6675,53.2102445 192.04083,126.580393 19.85687,83.81352 -51.91493,164.28908 -160.23339,179.70292 -0.0228,0.12532 0.0114,0.31899 0.0228,0.50127 l 0.62657,0.0342 84.39453,78.49328 -173.40295,-81.47808 1.69746,0.057 -1.10506,-0.85442 -2.48353,-0.0798 1.89113,0.87721 -2.381,-0.0798 v -0.82025 h -0.0342 C 142.29131,240.68634 89.68144,195.90301 75.623281,136.53748 55.755024,52.688641 127.61796,-27.831345 236.01617,-43.199624 c 13.5455,-1.916193 27.01126,-2.731885 40.27195,-2.518848 z"
inkscape:connector-curvature="0" />
</g>
<metadata
id="metadata3045">
<rdf:RDF>
<cc:Work>
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<cc:license
rdf:resource="http://creativecommons.org/licenses/publicdomain/" />
<dc:publisher>
<cc:Agent
rdf:about="http://openclipart.org/">
<dc:title>Openclipart</dc:title>
</cc:Agent>
</dc:publisher>
<dc:title></dc:title>
<dc:date>2008-02-19T10:10:56</dc:date>
<dc:description />
<dc:source>https://openclipart.org/detail/14475/callout-chat-by-ericlemerdy</dc:source>
<dc:creator>
<cc:Agent>
<dc:title>ericlemerdy</dc:title>
</cc:Agent>
</dc:creator>
<dc:subject>
<rdf:Bag>
<rdf:li>balloon</rdf:li>
<rdf:li>bubble</rdf:li>
<rdf:li>callout</rdf:li>
<rdf:li>speech</rdf:li>
</rdf:Bag>
</dc:subject>
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/publicdomain/">
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
</cc:License>
</rdf:RDF>
</metadata>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

7
rotate Executable file
View File

@ -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"

141
src/wirc.cgi/wirc.cgi.go Normal file
View File

@ -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)
}
}

267
src/wirc/wirc.go Normal file
View File

@ -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)
}