| 1 | # dramabot |
|---|
| 2 | # a text generation IRC technology |
|---|
| 3 | # by rolloffle, 2006 |
|---|
| 4 | |
|---|
| 5 | from httplib import HTTPConnection |
|---|
| 6 | import Queue |
|---|
| 7 | from random import choice, randrange |
|---|
| 8 | from re import compile |
|---|
| 9 | from socks import socks4a # if using Tor on 127.0.0.1:9050 |
|---|
| 10 | import threading |
|---|
| 11 | from time import sleep |
|---|
| 12 | from urllib import urlencode |
|---|
| 13 | from socket import AF_INET, SOCK_STREAM, socket |
|---|
| 14 | |
|---|
| 15 | DENOUEMENTS = ('oops', 'oh hi', 'oh... hi', 'eurgh, shit', 'oh shit', 'oh fuck', 'speak of the devil', 'good timing', 'well this is akward, heh', 'this is awkward...') |
|---|
| 16 | INSULTS = ( |
|---|
| 17 | '%s is such a fat faggot', |
|---|
| 18 | '%s is such a faggot', |
|---|
| 19 | '%s\'s a fucking tool', |
|---|
| 20 | '%s is a judenspy', |
|---|
| 21 | 'good thing %s isn\'t here to see that', |
|---|
| 22 | 'im gonna tell %s you said that', |
|---|
| 23 | 'tell %s to fuck off already', |
|---|
| 24 | '%s is a complete attention whore', |
|---|
| 25 | '%s is a worthless jew, why do we even keep him around?', |
|---|
| 26 | 'who the fuck invited %s here', |
|---|
| 27 | '%s is a useless goblinkike', |
|---|
| 28 | 'nobody wants %s around but don\'t mention it', |
|---|
| 29 | '%s is a complete cocksucking aspie', |
|---|
| 30 | 'i know exactly what you mean about %s', |
|---|
| 31 | 'did you hear the rumours about %s?', |
|---|
| 32 | '%s is a loser too', |
|---|
| 33 | 'but I won\'t tell %s you said that', |
|---|
| 34 | '%s is a fucking spastic', |
|---|
| 35 | '%s is a cunt', |
|---|
| 36 | ) |
|---|
| 37 | OPENINGS = ( |
|---|
| 38 | 'yeah, %s,', |
|---|
| 39 | 'yeah %s', |
|---|
| 40 | 'totally %s,', |
|---|
| 41 | 'srsly %s,', |
|---|
| 42 | 'agree 100%%, %s,', |
|---|
| 43 | 'agreed %s,', |
|---|
| 44 | '%s backs me up on this,', |
|---|
| 45 | 'yah, %s told me,', |
|---|
| 46 | 'i agree %s,' |
|---|
| 47 | ) |
|---|
| 48 | |
|---|
| 49 | def rand_char(): |
|---|
| 50 | return chr(randrange(ord('a'), ord('z'))) |
|---|
| 51 | |
|---|
| 52 | def gen_rand_username(): |
|---|
| 53 | #return 'wqat' |
|---|
| 54 | return ''.join([rand_char() for i in range(8)]) |
|---|
| 55 | |
|---|
| 56 | class channel: |
|---|
| 57 | |
|---|
| 58 | def __init__(self, con, chan): |
|---|
| 59 | """Initialise, join the channel.""" |
|---|
| 60 | |
|---|
| 61 | self.con = con |
|---|
| 62 | self.chan = chan |
|---|
| 63 | self.nicks = [ ] |
|---|
| 64 | self.con.lsend('JOIN ' + chan) |
|---|
| 65 | |
|---|
| 66 | def rand_nick(self): |
|---|
| 67 | """Returns a random nick present in this channel.""" |
|---|
| 68 | return choice(self.nicks) |
|---|
| 69 | |
|---|
| 70 | def strip_nick_status_symbol(self, nick): |
|---|
| 71 | """If present, removes the status character from a nick.""" |
|---|
| 72 | if nick[0] in '@%+': |
|---|
| 73 | nick = nick[1:] |
|---|
| 74 | return nick |
|---|
| 75 | |
|---|
| 76 | def add_nicks(self, nicks): |
|---|
| 77 | """Add a list of nicks to this channel's nick list.""" |
|---|
| 78 | for nick in nicks: |
|---|
| 79 | nick = self.strip_nick_status_symbol(nick) |
|---|
| 80 | if (nick != self.con.nick) and (nick not in self.nicks): |
|---|
| 81 | self.nicks += [ nick ] |
|---|
| 82 | |
|---|
| 83 | def del_nick(self, nick): |
|---|
| 84 | """Delete a nick from this channel's nick list.""" |
|---|
| 85 | nick = self.strip_nick_status_symbol(nick) |
|---|
| 86 | try: |
|---|
| 87 | self.nicks.remove(nick) |
|---|
| 88 | except: |
|---|
| 89 | print 'Error: failed to remove \'%s\' from nick list for %s' % (nick, self.chan) |
|---|
| 90 | |
|---|
| 91 | def drama_bomb(self, target): |
|---|
| 92 | """Say something in the channel to a target nick to provoke them.""" |
|---|
| 93 | global DENOUEMENTS, INSULTS, OPENINGS |
|---|
| 94 | |
|---|
| 95 | template = 'PRIVMSG %s :' + choice(OPENINGS) + ' ' + choice(INSULTS) |
|---|
| 96 | print "TEMPLATE: " + template |
|---|
| 97 | bomb = template % (self.chan, self.rand_nick(), target) |
|---|
| 98 | |
|---|
| 99 | print '%s: drama bomb "%s"' % (self.chan, bomb) |
|---|
| 100 | self.con.lsend(bomb) |
|---|
| 101 | |
|---|
| 102 | if randrange(1, 2) != 1: |
|---|
| 103 | # chance of saying a denouement |
|---|
| 104 | sleep(1) |
|---|
| 105 | self.con.lsend('PRIVMSG %s :%s' % (self.chan, choice(DENOUEMENTS))) |
|---|
| 106 | |
|---|
| 107 | def display_status(self): |
|---|
| 108 | """Write a status report for this channel on stdout.""" |
|---|
| 109 | print ' %s: new nick list length %u' % (self.chan, len(self.nicks)) |
|---|
| 110 | |
|---|
| 111 | class con: |
|---|
| 112 | |
|---|
| 113 | def __init__(self, server, port, channels, nick, socks4a_proxy = None): |
|---|
| 114 | |
|---|
| 115 | self.server, self.port, self.nick = server, port, nick |
|---|
| 116 | |
|---|
| 117 | self.r = { } |
|---|
| 118 | self.r['001'] = compile(':[^ ]+ 001 ') |
|---|
| 119 | self.r['353'] = compile(':[^ ]+ 353 [^ ]+ . ([^ ]+) :(.+)') |
|---|
| 120 | self.r['433'] = compile(':[^ ]+ 433 . [^ ]+ :.+') |
|---|
| 121 | self.r['join'] = compile(':([^!~:]+)![^ ]+ JOIN :([^ ]+)') |
|---|
| 122 | self.r['part'] = compile(':([^!~:]+)![^ ]+ PART ([^ ]+)( :(.+))?') |
|---|
| 123 | self.r['kick'] = compile(':([^!~:]+)![^ ]+ KICK ([^ ]+) ([^ ]+)( :(.+))?') |
|---|
| 124 | self.r['privmsg'] = compile(':([^!~:]+)![^ ]+ PRIVMSG ([^ ]+) :(.+)') |
|---|
| 125 | self.r['http'] = compile(':([^!~:]+)![^ ]+ PRIVMSG ([^ ]+) :.*http://.*') |
|---|
| 126 | self.r['die'] = compile(':([^!~:]+)![^ ]+ PRIVMSG ([^ ]+) :' + nick + ': die') |
|---|
| 127 | |
|---|
| 128 | self.connect(socks4a_proxy) |
|---|
| 129 | |
|---|
| 130 | self.channels = { } |
|---|
| 131 | for chan in channels: |
|---|
| 132 | self.channels[chan] = channel(self, chan) |
|---|
| 133 | |
|---|
| 134 | def lsend(self, s): |
|---|
| 135 | self.sock.send(s + '\r\n') |
|---|
| 136 | |
|---|
| 137 | def lrecv(self): |
|---|
| 138 | c, s = '', '' |
|---|
| 139 | while c != '\n': |
|---|
| 140 | c = self.sock.recv(1) |
|---|
| 141 | if c == '': # connection closed |
|---|
| 142 | break |
|---|
| 143 | s += c |
|---|
| 144 | return s.strip('\r\n') |
|---|
| 145 | |
|---|
| 146 | def connect(self, socks4a_proxy = None): |
|---|
| 147 | """Make the initial connection to the server and try to register.""" |
|---|
| 148 | |
|---|
| 149 | self.sock = socks4a() |
|---|
| 150 | |
|---|
| 151 | print 'Connecting to %s:%u' % (self.server, self.port) |
|---|
| 152 | if socks4a_proxy == None: |
|---|
| 153 | self.sock.connect((self.server, self.port)) |
|---|
| 154 | else: |
|---|
| 155 | self.sock.proxy_connect(socks4a_proxy, (self.server, self.port)) |
|---|
| 156 | |
|---|
| 157 | username = gen_rand_username() |
|---|
| 158 | print ' username = ' + username |
|---|
| 159 | self.lsend('USER ' + username + ' 0 0 :Unknown') |
|---|
| 160 | self.lsend('NICK ' + self.nick + ' 0') |
|---|
| 161 | |
|---|
| 162 | # Wait for the 001 status reply. |
|---|
| 163 | while 1: |
|---|
| 164 | line = self.lrecv() |
|---|
| 165 | if self.r['001'].match(line): |
|---|
| 166 | # We got the 001, break out of the loop and proceed. |
|---|
| 167 | break |
|---|
| 168 | elif self.r['433'].match(line): |
|---|
| 169 | # This nick is taken. Change the last 2 chars and retry. |
|---|
| 170 | self.nick = self.nick[:-2] + rand_char() + rand_char() |
|---|
| 171 | self.lsend('NICK ' + self.nick + ' 0') |
|---|
| 172 | elif line == '': |
|---|
| 173 | raise 'ConnectError', (self.server, self.port, 'EOFBefore001') |
|---|
| 174 | |
|---|
| 175 | print ' got 001' |
|---|
| 176 | |
|---|
| 177 | def go(self): |
|---|
| 178 | |
|---|
| 179 | global rand_char |
|---|
| 180 | |
|---|
| 181 | # Join the channels. |
|---|
| 182 | print ' joining channels' |
|---|
| 183 | for channel in self.channels: |
|---|
| 184 | self.lsend('JOIN ' + channel) |
|---|
| 185 | print ' joined all channels' |
|---|
| 186 | |
|---|
| 187 | # Sit and watch at the socket for incoming data and act on it. |
|---|
| 188 | |
|---|
| 189 | while 1: |
|---|
| 190 | |
|---|
| 191 | line = self.lrecv() |
|---|
| 192 | |
|---|
| 193 | if line == '': |
|---|
| 194 | raise 'ConnectionClose', (self.server, self.port) |
|---|
| 195 | |
|---|
| 196 | elif line[:6] == 'PING :': |
|---|
| 197 | print ' PONG :' + line[6:] |
|---|
| 198 | self.lsend('PONG :' + line[6:]) |
|---|
| 199 | continue |
|---|
| 200 | |
|---|
| 201 | m = self.r['join'].match(line) |
|---|
| 202 | if m != None: |
|---|
| 203 | # Somebody joined. If it's not us, start the drama and add |
|---|
| 204 | # their nick to the master list of nicks in this channel. |
|---|
| 205 | print 'Received: JOIN' |
|---|
| 206 | |
|---|
| 207 | if m.group(1) == self.nick: |
|---|
| 208 | continue |
|---|
| 209 | |
|---|
| 210 | chan = self.channels[m.group(2)] |
|---|
| 211 | |
|---|
| 212 | chan.add_nicks([ m.group(1) ]) |
|---|
| 213 | chan.display_status() |
|---|
| 214 | chan.drama_bomb(m.group(1)) |
|---|
| 215 | |
|---|
| 216 | continue |
|---|
| 217 | |
|---|
| 218 | m = self.r['part'].match(line) |
|---|
| 219 | if m != None: |
|---|
| 220 | # Somebody parted, remove their nick from the master list of |
|---|
| 221 | # this channel's nicks. |
|---|
| 222 | print 'Received: PART' |
|---|
| 223 | chan = self.channels[m.group(2)] |
|---|
| 224 | chan.del_nick(m.group(1)) |
|---|
| 225 | chan.display_status() |
|---|
| 226 | |
|---|
| 227 | m = self.r['353'].match(line) |
|---|
| 228 | if m != None: |
|---|
| 229 | # 353 lists the nicks of the user in a channel we just |
|---|
| 230 | # joined. Add the list of nicks given to the master list of |
|---|
| 231 | # nicks present in the channel. |
|---|
| 232 | print 'Received: 353' |
|---|
| 233 | chan = self.channels[m.group(1)] |
|---|
| 234 | chan.add_nicks(m.group(2).split()) |
|---|
| 235 | chan.display_status() |
|---|
| 236 | continue |
|---|
| 237 | |
|---|
| 238 | m = self.r['kick'].match(line) |
|---|
| 239 | if m != None: |
|---|
| 240 | # Someone was kicked. |
|---|
| 241 | if m.group(3) == self.nick: |
|---|
| 242 | # It was us! Rejoin. |
|---|
| 243 | print 'Kicked! Attempting to rejoin ' + m.group(2) |
|---|
| 244 | self.lsend('JOIN ' + m.group(2)) |
|---|
| 245 | line = self.lrecv() |
|---|
| 246 | |
|---|
| 247 | if line == '': |
|---|
| 248 | raise 'ConnectionClose', (self.server, self.port) |
|---|
| 249 | |
|---|
| 250 | elif line[:6] == 'PING :': |
|---|
| 251 | print ' PONG :' + line[6:] |
|---|
| 252 | self.lsend('PONG :' + line[6:]) |
|---|
| 253 | continue |
|---|
| 254 | |
|---|
| 255 | print line |
|---|
| 256 | |
|---|
| 257 | m = self.r['die'].match(line) |
|---|
| 258 | if m != None: |
|---|
| 259 | print 'Request: die' |
|---|
| 260 | nick = m.group(1).lower().replace('m', '/\\/\\') |
|---|
| 261 | out_queue.put( [m.group(2), 'f u' + nick] ) |
|---|
| 262 | print 'response sent' |
|---|
| 263 | |
|---|
| 264 | m = self.r['http'].match(line) |
|---|
| 265 | if m != None: |
|---|
| 266 | # Someone said a URL. |
|---|
| 267 | reply = [ |
|---|
| 268 | '/!\\ FUCKIN\' OLD /!\\', |
|---|
| 269 | '404', |
|---|
| 270 | 'broken link', |
|---|
| 271 | 'busted link', |
|---|
| 272 | 'link fails', |
|---|
| 273 | 'LM link dont click!', |
|---|
| 274 | 'LastMeasure link detected', |
|---|
| 275 | 'OLD', |
|---|
| 276 | 'CP link', |
|---|
| 277 | 'LM link', |
|---|
| 278 | 'CP', |
|---|
| 279 | 'LM', |
|---|
| 280 | 'OLD i posted that yesterday' |
|---|
| 281 | ] |
|---|
| 282 | out_queue.put( [ m.group(2), choice(reply) ] ) |
|---|
| 283 | continue |
|---|
| 284 | |
|---|
| 285 | class con_thread(threading.Thread): |
|---|
| 286 | |
|---|
| 287 | def __init__(self, server, port, channels, nick): |
|---|
| 288 | self.co = con(server, port, channels, nick) |
|---|
| 289 | threading.Thread.__init__(self) |
|---|
| 290 | |
|---|
| 291 | def run(self): |
|---|
| 292 | self.co.go() |
|---|
| 293 | #self.co.go(('127.0.0.1', 9050)) # connect via Tor |
|---|
| 294 | |
|---|
| 295 | # Create the queue to store events to be announced. |
|---|
| 296 | out_queue = Queue.Queue(999) |
|---|
| 297 | |
|---|
| 298 | threads = [ |
|---|
| 299 | con_thread('irc.ww88.org', 6667, ['#gnaa'], 'dramabot'), |
|---|
| 300 | #con_thread('irc.chir.pn', 6667, ['#chirp'], 'dramabot') |
|---|
| 301 | |
|---|
| 302 | # The wacky .onion hostname is because it's Freenode's hidden Tor service |
|---|
| 303 | #con_thread('mejokbp2brhw4omd.onion', 6667, ['#wikipedia'], 'fade') |
|---|
| 304 | ] |
|---|
| 305 | |
|---|
| 306 | for thread in threads: |
|---|
| 307 | thread.setDaemon(1) |
|---|
| 308 | thread.start() |
|---|
| 309 | |
|---|
| 310 | while 1: |
|---|
| 311 | try: |
|---|
| 312 | event = out_queue.get(True) |
|---|
| 313 | except KeyboardInterrupt: |
|---|
| 314 | break |
|---|
| 315 | for thread in threads: |
|---|
| 316 | thread.co.lsend('PRIVMSG ' + event[0] + ' :' + event[1]) |
|---|