#!/usr/bin/env python """ noetbot - Blogging + IRC + Python + Pwyky + Noets License: GPL 2; share and enjoy! Author: Sean B. Palmer, inamidst.com """ # # # # # # # # # # # # # # # # # # # Main config stuff nickname = 'noetbot' bloguri = 'http://example.org/notes/' blogname = 'My Notes' base = '/notes' # End of main config stuff # # # # # # # # # # # # # # # # # # import sys, os, re, time, socket, asyncore, asynchat class IRCBot(asynchat.async_chat): def __init__(self, nick, channels=None): asynchat.async_chat.__init__(self) self.buffer = '' self.set_terminator('\r\n') self.nick = nick self.uid = 'nobody' self.name = 'ircasl user' self.channels = channels or ['#test'] self.rules = {} def write(self, args, text=None): if text is not None: self.push(' '.join(args) + ' :' + text + '\r\n') else: self.push(' '.join(args) + '\r\n') def handle_connect(self): print >> sys.stderr, "connected!" self.write(('NICK', self.nick)) self.write(('USER', self.uid, '+iw', self.nick), self.name) for channel in self.channels: self.write(('JOIN', channel)) def collect_incoming_data(self, data): self.buffer += data def found_terminator(self): line = self.buffer self.buffer = '' if line[0] == ':': origin, line = line[1:].split(' ', 1) if ('!' in origin) and ('@' in origin): nick, uhost = origin.split('!', 1) user, host = uhost.split('@', 1) origin = (nick, user, host) else: origin = (origin, None, None) else: origin = (None, None, None) if ' :' in line: args, text = line.split(' :', 1) else: args, text = line, '' args = args.split() self.dispatch(args, text, origin) def rule(self, thunk, pattern, command=None): command = command or 'PRIVMSG' args = (thunk, re.compile(pattern)) if self.rules.has_key(command): self.rules[command].append(args) else: self.rules[command] = [args] def dispatch(self, args, text, origin): if self.rules.has_key(args[0]): for (thunk, pattern) in self.rules[args[0]]: m = pattern.search(text) if m: thunk(m, origin, args, text) if args[0] == 'PING': self.write(('PONG', text)) def msg(self, dest, text): self.write(('PRIVMSG', dest), text) def notice(self, dest, text): self.write(('NOTICE', dest), text) def run(self, host, port): self.create_socket(socket.AF_INET, socket.SOCK_STREAM) print >> sys.stderr, "Connecting to %s:%s..." % (host, port), self.connect((host, port)) asyncore.loop() def parseOrigin(origin): nickid, host = tuple(origin.split('@')) nick, userid = tuple(nickid.split('!')) return (nick, userid, host) def runIRC(hostName, port, chans): c = IRCBot(nick=nickname, channels=chans) c.userid = nickname c.bloggers = {} # Put any new functions here def help(m, origin, (cmd, channel), text, c=c): doc = ( "Hi. To start a blog entry, type e.g. '<<< Entry Title'", "Everything you type from then on goes in the entry", "The link syntax is '{http://example.uri/ Link Title}'", "Use '..' on a single line to separate paragraphs", "To post the entry, use '>>>' on a single line", "I won't post any line starting with a colon, ':'", "To quit an entry whilst editing, use ':quit'." ) for line in doc: c.msg(channel, line) time.sleep(1.5) c.rule(help, r"(?i)^help,? %s[?!]?$" % nickname) c.rule(help, r"(?i)^%s[:,]? help[?!]?$" % nickname) def startEntry(m, (nick, userid, host), args, text, c=c): channel = args[1] if nick not in ('sbp', 'd8uv'): return title = m.group(1) or None if title: title = title.lstrip() c.notice(nick, "%s: set title" % nick) else: title = None c.bloggers[nick] = {'title': title, 'content': '', 'nick': nick, 'channel': channel} c.rule(startEntry, r"^<<<( .+)?$") def titleEntry(m, (nick, userid, host), args, text, c=c): channel = args[1] if c.bloggers.has_key(nick): title = m.group(1) if c.bloggers[nick]['title'] is None: c.bloggers[nick]['title'] = title c.notice(nick, "%s: set title" % nick) else: c.bloggers[nick]['title'] = title c.notice(nick, "%s: reset title" % nick) # else: just do nothing. @@ display a message? c.rule(titleEntry, r"^:title (.+)$") def entryText(m, (nick, userid, host), args, text, c=c): channel = args[1] if c.bloggers.has_key(nick): if (channel == c.bloggers[nick].get('channel') and text not in ('<<<', '>>>') and not text.startswith('<<< ') and not text.startswith(':')): c.bloggers[nick]['content'] += text + '\n' c.rule(entryText, r"(.+)") def editEntry(m, (nick, userid, host), args, text, c=c): channel = args[1] stype, find, repl, flags = m.groups() find = find.replace('\\%s' % stype, '%s' % stype) repl = repl.replace('\\%s' % stype, '%s' % stype) if c.bloggers.has_key(nick): if channel == c.bloggers[nick].get('channel'): text = c.bloggers[nick]['content'] addNewline = '' if text.endswith('\n'): text = text[:-1] addNewline = '\n' i = text.rfind('\n') + 1 line = text[i:] text = text[:i] if 'g' in flags: line = line.replace(find, repl) else: line = line.replace(find, repl, 1) msg = 'New line is: %r' % line[:300] c.notice(nick, msg) c.bloggers[nick]['content'] = text + line + addNewline r_sol = r'[^/\\]*(?:\\.[^/\\]*)*' r_exc = r'[^!\\]*(?:\\.[^!\\]*)*' c.rule(editEntry, r"^:s(/)(%s)/(%s)/(g?)$" % (r_sol, r_sol)) c.rule(editEntry, r"^:s(!)(%s)!(%s)!(g?)$" % (r_exc, r_exc)) def finishEntry(m, (nick, userid, host), args, text, c=c): channel = args[1] if c.bloggers.has_key(nick): if not c.bloggers[nick]['title']: msg = "%s: you need to title the entry" % nick c.notice(nick, msg) return if not c.bloggers[nick]['content'].strip(): msg = "%s: you must supply some text" % nick c.notice(nick, msg) return form = c.bloggers[nick] try: i = blogit(form) except Exception, e: exp = "%s: %s" % (e.__class__, e) msg = "%s: publishing error: %s. try again!" % (nick, exp) c.msg(channel, msg) else: msg = "%s: published entry to %s%s" % (nick, bloguri, i) c.msg(channel, msg) del c.bloggers[nick] else: msg = "%s: you've not started an entry" % nick c.notice(nick, msg) c.rule(finishEntry, r"^>>>$") def quitEntry(m, (nick, userid, host), args, text, c=c): channel = args[1] if c.bloggers.has_key(nick): del c.bloggers[nick] c.notice(nick, "%s: abandoned current entry" % nick) c.rule(quitEntry, r"^:quit$") c.run(hostName, port) navigation = """ """.lstrip() address = '
\n' address += 'Sean B. Palmer, \n' address += 'and the irc.freenode.net/#sbp folk.\n' address += '
\n' def quoteHTML(s): s = s.replace('&', '&') s = s.replace('<', '<') return s t_wlink = r'(?}\s]+) ([^}]+)(?"]+$') def wikiLinkify(s): def htmlify(m): uri, title = m.group(1), m.group(2) return '%s' % (uri, title) s = r_wlink.sub(htmlify, s) return s def unicodeify(s): i = int(s, 16) if i in (0x9, 0xA, 0xD) + tuple(xrange(0x20, 0x7E)): return chr(i) elif i > 0x10FFFF: raise "UnicodeError", "Codepoint exceeds U+10FFFF" return '&#x%s;' % s def wikiFormat(s): # @@ reserve \[A-Za-z]+{...} for future extensions # @@ use a proper parser, or catch the matches using a function result = '' pos = 0 while pos < len(s): m = r_wlink.match(s[pos:]) if m: span = m.span() result += wikiLinkify(s[pos:pos+span[1]]) pos += len(m.group(0)) else: m = re.compile(r'[A-Za-z0-9]--[A-Za-z0-9]').match(s[pos:]) if m: result += s[pos] + '—' + s[pos+3] pos += 4 else: m = r_uniquot.match(s[pos:]) if m: result += unicodeify(m.group(1)) pos += len(m.group(0)) elif s[pos:pos+4] == '\n..\n': result += '

\n\n

' pos += 4 elif s[pos] == '&': result += '&' pos += 1 elif s[pos] == '<': result += '<' pos += 1 else: result += s[pos] pos += 1 return result def html(title, body): s = '\n' s += '\n' s += '%s\n' % title s += '\n' s += '\n' s += '\n' s += body s += '\n' s += '' return s def getEntryIDs(): # Each entry's content will be stored as (digit).html # So we can use that as a kind of entry-index filenames = filter(lambda s: (s.endswith('.html') and s[:-len('.html')].isdigit()), os.listdir('.')) filenames = [int(fn[:-len('.html')]) for fn in filenames] filenames.sort() return filenames def postEntry(form): # Compute the next available entry ID filenames = getEntryIDs() if filenames: nextid = filenames[-1] + 1 else: nextid = 1 t = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) title = str(form.get('title')).strip() content = str(form.get('content')).strip() nick = str(form.get('nick')).strip() data = open('./%s.html' % nextid, 'w') print >> data, '' % t print >> data, '%s' % quoteHTML(title) print >> data, ('') print >> data, '

%s

' % quoteHTML(title) print >> data, '

%s

' % wikiFormat(content) print >> data, '

* Posted by %s at > data, 'href="./%s">%s.

' % (nextid, t.replace('T', ' ').replace('Z', ' UTC')) data.close() # Return the ID as an int return int(nextid) class Entry(object): def __init__(self, i): self.data = open('./%s.html' % i, 'r').read() self.lines = self.data.splitlines() self.date = self.lines[0][5:-4] self.title = self.lines[1][7:-8] def __str__(self): return '\n'.join(self.lines[3:]) def buildIndex(n=None): if n is None: n = 10 result = navigation result += '

%s \n' % blogname result += '(by sbp & ' result += 'friends)

\n' filenames = getEntryIDs()[-n:] # get the last ten or so entries if filenames: filenames.reverse() for fn in filenames: e = Entry(fn) result += '
\n' + str(e) + '
\n\n' else: result += '

No entries yet.

\n' result += address return html("%s - the noets of #sbp &c." % blogname, result) def buildArchives(): filenames = getEntryIDs() filenames.reverse() result = navigation result += '
\n\n\n

%s (', ']]>]]>\n' % (bloguri, filename) rss += '\t%s\n' % title rss += '\t%s%s\n' % (bloguri, filename) rss += '\t\n' % data rss += '\t%s' % date rss += '\t\n' % data rss += '\n' rss += '\n' return rss def blogit(form): i = postEntry(form) print >> open('index.html', 'w'), buildIndex() print >> open('archives.html', 'w'), buildArchives() print >> open('rss.xml', 'w'), buildRSS() return i def main(argv=None): if argv is None: argv = sys.argv if len(argv) > 2: server, chans = argv[1], argv[2:] chans = [('#%s' % c, c)[c.startswith('#')] for c in chans] if ':' in server: server, port = server.split(':') else: port = '6667' runIRC(server, int(port), chans) else: print __doc__ if __name__=='__main__': main()