├── .gitignore ├── LICENSE ├── README ├── bot ├── MySQL-python-1.2.2.tar.gz ├── config.py ├── irc_config.txt.example ├── ircbot.py ├── irclib.py ├── mysql_config.txt.example ├── pierc.py └── pierc_db.py └── web ├── config.php.example ├── index.php ├── json.php ├── pierc.js ├── pierc_db.php └── theme ├── rnarian ├── ajax-loader.gif └── style.css └── standard ├── ajax-loader.gif └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | config.php 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright (C) 2008-2012 Curtis Lassam 3 | 4 | This library is free software; you can redistribute it and/or 5 | modify it under the terms of the GNU Lesser General Public 6 | License as published by the Free Software Foundation; either 7 | version 2.1 of the License, or (at your option) any later version. 8 | 9 | This library is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | Lesser General Public License for more details. 13 | 14 | You should have received a copy of the GNU Lesser General Public 15 | License along with this library; if not, write to the Free Software 16 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 17 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | 2 | Pierc is a python bot that logs IRC channels, and a PHP/JS interface for browsing said logs. 3 | 4 | Installation instructions can be found at http://classam.github.com/pierc/ . 5 | 6 | It depends on python-mysql and Joel Rosdahl's IRC Bot library, both of which are ~GPLv2 - so 7 | Pierc, too, is GPLv2. 8 | 9 | 10 | -------------------------------------------------------------------------------- /bot/MySQL-python-1.2.2.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cube-drone/pierc/9cdcb19bb41878782a9da1d548f5c8d5d9245127/bot/MySQL-python-1.2.2.tar.gz -------------------------------------------------------------------------------- /bot/config.py: -------------------------------------------------------------------------------- 1 | 2 | def config(filename): 3 | """ Process a file (located at filename) 4 | containing a series of key-value pairs 5 | this: awesome 6 | numkids: 10 7 | funk: soulbrother 8 | Into a dictionary. (dict[this] = "awesome") 9 | """ 10 | try: 11 | config_data = dict() 12 | db_file = open(filename, "r") 13 | for line in db_file: 14 | if line.startswith("#"): 15 | continue 16 | temp = line.replace(":", "").split() 17 | key = temp[0] 18 | value = temp[1] 19 | config_data[key] = value 20 | db_file.close() 21 | return config_data 22 | except: 23 | print filename, "missing." 24 | exit(); 25 | 26 | if __name__ == "__main__": 27 | irc_config = config("mysql_config.txt") 28 | for key,value in irc_config.iteritems(): 29 | print key, value 30 | -------------------------------------------------------------------------------- /bot/irc_config.txt.example: -------------------------------------------------------------------------------- 1 | server: irc.freenode.net 2 | port: 6667 3 | channel: #logbot_test 4 | nick: lumberjack 5 | reconnect: 300 6 | -------------------------------------------------------------------------------- /bot/ircbot.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 1999--2002 Joel Rosdahl 2 | # 3 | # This library is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU Lesser General Public 5 | # License as published by the Free Software Foundation; either 6 | # version 2.1 of the License, or (at your option) any later version. 7 | # 8 | # This library is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | # Lesser General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Lesser General Public 14 | # License along with this library; if not, write to the Free Software 15 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 16 | # 17 | # Joel Rosdahl 18 | # 19 | # $Id: ircbot.py,v 1.23 2008/09/11 07:38:30 keltus Exp $ 20 | 21 | """ircbot -- Simple IRC bot library. 22 | 23 | This module contains a single-server IRC bot class that can be used to 24 | write simpler bots. 25 | """ 26 | 27 | import sys 28 | from UserDict import UserDict 29 | 30 | from irclib import SimpleIRCClient 31 | from irclib import nm_to_n, irc_lower, all_events 32 | from irclib import parse_channel_modes, is_channel 33 | from irclib import ServerConnectionError 34 | 35 | class SingleServerIRCBot(SimpleIRCClient): 36 | """A single-server IRC bot class. 37 | 38 | The bot tries to reconnect if it is disconnected. 39 | 40 | The bot keeps track of the channels it has joined, the other 41 | clients that are present in the channels and which of those that 42 | have operator or voice modes. The "database" is kept in the 43 | self.channels attribute, which is an IRCDict of Channels. 44 | """ 45 | def __init__(self, server_list, nickname, realname, reconnection_interval=60): 46 | """Constructor for SingleServerIRCBot objects. 47 | 48 | Arguments: 49 | 50 | server_list -- A list of tuples (server, port) that 51 | defines which servers the bot should try to 52 | connect to. 53 | 54 | nickname -- The bot's nickname. 55 | 56 | realname -- The bot's realname. 57 | 58 | reconnection_interval -- How long the bot should wait 59 | before trying to reconnect. 60 | 61 | dcc_connections -- A list of initiated/accepted DCC 62 | connections. 63 | """ 64 | 65 | SimpleIRCClient.__init__(self) 66 | self.channels = IRCDict() 67 | self.server_list = server_list 68 | if not reconnection_interval or reconnection_interval < 0: 69 | reconnection_interval = 2**31 70 | self.reconnection_interval = reconnection_interval 71 | 72 | self._nickname = nickname 73 | self._realname = realname 74 | for i in ["disconnect", "join", "kick", "mode", 75 | "namreply", "nick", "part", "quit"]: 76 | self.connection.add_global_handler(i, 77 | getattr(self, "_on_" + i), 78 | -10) 79 | def _connected_checker(self): 80 | """[Internal]""" 81 | if not self.connection.is_connected(): 82 | self.connection.execute_delayed(self.reconnection_interval, 83 | self._connected_checker) 84 | self.jump_server() 85 | 86 | def _connect(self): 87 | """[Internal]""" 88 | password = None 89 | if len(self.server_list[0]) > 2: 90 | password = self.server_list[0][2] 91 | try: 92 | self.connect(self.server_list[0][0], 93 | self.server_list[0][1], 94 | self._nickname, 95 | password, 96 | ircname=self._realname) 97 | except ServerConnectionError: 98 | pass 99 | 100 | def _on_disconnect(self, c, e): 101 | """[Internal]""" 102 | self.channels = IRCDict() 103 | self.connection.execute_delayed(self.reconnection_interval, 104 | self._connected_checker) 105 | 106 | def _on_join(self, c, e): 107 | """[Internal]""" 108 | ch = e.target() 109 | nick = nm_to_n(e.source()) 110 | if nick == c.get_nickname(): 111 | self.channels[ch] = Channel() 112 | self.channels[ch].add_user(nick) 113 | 114 | def _on_kick(self, c, e): 115 | """[Internal]""" 116 | nick = e.arguments()[0] 117 | channel = e.target() 118 | 119 | if nick == c.get_nickname(): 120 | del self.channels[channel] 121 | else: 122 | self.channels[channel].remove_user(nick) 123 | 124 | def _on_mode(self, c, e): 125 | """[Internal]""" 126 | modes = parse_channel_modes(" ".join(e.arguments())) 127 | t = e.target() 128 | if is_channel(t): 129 | ch = self.channels[t] 130 | for mode in modes: 131 | if mode[0] == "+": 132 | f = ch.set_mode 133 | else: 134 | f = ch.clear_mode 135 | f(mode[1], mode[2]) 136 | else: 137 | # Mode on self... XXX 138 | pass 139 | 140 | def _on_namreply(self, c, e): 141 | """[Internal]""" 142 | 143 | # e.arguments()[0] == "@" for secret channels, 144 | # "*" for private channels, 145 | # "=" for others (public channels) 146 | # e.arguments()[1] == channel 147 | # e.arguments()[2] == nick list 148 | 149 | ch = e.arguments()[1] 150 | for nick in e.arguments()[2].split(): 151 | if nick[0] == "@": 152 | nick = nick[1:] 153 | self.channels[ch].set_mode("o", nick) 154 | elif nick[0] == "+": 155 | nick = nick[1:] 156 | self.channels[ch].set_mode("v", nick) 157 | self.channels[ch].add_user(nick) 158 | 159 | def _on_nick(self, c, e): 160 | """[Internal]""" 161 | before = nm_to_n(e.source()) 162 | after = e.target() 163 | for ch in self.channels.values(): 164 | if ch.has_user(before): 165 | ch.change_nick(before, after) 166 | 167 | def _on_part(self, c, e): 168 | """[Internal]""" 169 | nick = nm_to_n(e.source()) 170 | channel = e.target() 171 | 172 | if nick == c.get_nickname(): 173 | del self.channels[channel] 174 | else: 175 | self.channels[channel].remove_user(nick) 176 | 177 | def _on_quit(self, c, e): 178 | """[Internal]""" 179 | nick = nm_to_n(e.source()) 180 | for ch in self.channels.values(): 181 | if ch.has_user(nick): 182 | ch.remove_user(nick) 183 | 184 | def die(self, msg="Bye, cruel world!"): 185 | """Let the bot die. 186 | 187 | Arguments: 188 | 189 | msg -- Quit message. 190 | """ 191 | 192 | self.connection.disconnect(msg) 193 | sys.exit(0) 194 | 195 | def disconnect(self, msg="I'll be back!"): 196 | """Disconnect the bot. 197 | 198 | The bot will try to reconnect after a while. 199 | 200 | Arguments: 201 | 202 | msg -- Quit message. 203 | """ 204 | self.connection.disconnect(msg) 205 | 206 | def get_version(self): 207 | """Returns the bot version. 208 | 209 | Used when answering a CTCP VERSION request. 210 | """ 211 | return "ircbot.py by Joel Rosdahl " 212 | 213 | def jump_server(self, msg="Changing servers"): 214 | """Connect to a new server, possibly disconnecting from the current. 215 | 216 | The bot will skip to next server in the server_list each time 217 | jump_server is called. 218 | """ 219 | if self.connection.is_connected(): 220 | self.connection.disconnect(msg) 221 | 222 | self.server_list.append(self.server_list.pop(0)) 223 | self._connect() 224 | 225 | def on_ctcp(self, c, e): 226 | """Default handler for ctcp events. 227 | 228 | Replies to VERSION and PING requests and relays DCC requests 229 | to the on_dccchat method. 230 | """ 231 | if e.arguments()[0] == "VERSION": 232 | c.ctcp_reply(nm_to_n(e.source()), 233 | "VERSION " + self.get_version()) 234 | elif e.arguments()[0] == "PING": 235 | if len(e.arguments()) > 1: 236 | c.ctcp_reply(nm_to_n(e.source()), 237 | "PING " + e.arguments()[1]) 238 | elif e.arguments()[0] == "DCC" and e.arguments()[1].split(" ", 1)[0] == "CHAT": 239 | self.on_dccchat(c, e) 240 | 241 | def on_dccchat(self, c, e): 242 | pass 243 | 244 | def start(self): 245 | """Start the bot.""" 246 | self._connect() 247 | SimpleIRCClient.start(self) 248 | 249 | 250 | class IRCDict: 251 | """A dictionary suitable for storing IRC-related things. 252 | 253 | Dictionary keys a and b are considered equal if and only if 254 | irc_lower(a) == irc_lower(b) 255 | 256 | Otherwise, it should behave exactly as a normal dictionary. 257 | """ 258 | 259 | def __init__(self, dict=None): 260 | self.data = {} 261 | self.canon_keys = {} # Canonical keys 262 | if dict is not None: 263 | self.update(dict) 264 | def __repr__(self): 265 | return repr(self.data) 266 | def __cmp__(self, dict): 267 | if isinstance(dict, IRCDict): 268 | return cmp(self.data, dict.data) 269 | else: 270 | return cmp(self.data, dict) 271 | def __len__(self): 272 | return len(self.data) 273 | def __getitem__(self, key): 274 | return self.data[self.canon_keys[irc_lower(key)]] 275 | def __setitem__(self, key, item): 276 | if key in self: 277 | del self[key] 278 | self.data[key] = item 279 | self.canon_keys[irc_lower(key)] = key 280 | def __delitem__(self, key): 281 | ck = irc_lower(key) 282 | del self.data[self.canon_keys[ck]] 283 | del self.canon_keys[ck] 284 | def __iter__(self): 285 | return iter(self.data) 286 | def __contains__(self, key): 287 | return self.has_key(key) 288 | def clear(self): 289 | self.data.clear() 290 | self.canon_keys.clear() 291 | def copy(self): 292 | if self.__class__ is UserDict: 293 | return UserDict(self.data) 294 | import copy 295 | return copy.copy(self) 296 | def keys(self): 297 | return self.data.keys() 298 | def items(self): 299 | return self.data.items() 300 | def values(self): 301 | return self.data.values() 302 | def has_key(self, key): 303 | return irc_lower(key) in self.canon_keys 304 | def update(self, dict): 305 | for k, v in dict.items(): 306 | self.data[k] = v 307 | def get(self, key, failobj=None): 308 | return self.data.get(key, failobj) 309 | 310 | 311 | class Channel: 312 | """A class for keeping information about an IRC channel. 313 | 314 | This class can be improved a lot. 315 | """ 316 | 317 | def __init__(self): 318 | self.userdict = IRCDict() 319 | self.operdict = IRCDict() 320 | self.voiceddict = IRCDict() 321 | self.modes = {} 322 | 323 | def users(self): 324 | """Returns an unsorted list of the channel's users.""" 325 | return self.userdict.keys() 326 | 327 | def opers(self): 328 | """Returns an unsorted list of the channel's operators.""" 329 | return self.operdict.keys() 330 | 331 | def voiced(self): 332 | """Returns an unsorted list of the persons that have voice 333 | mode set in the channel.""" 334 | return self.voiceddict.keys() 335 | 336 | def has_user(self, nick): 337 | """Check whether the channel has a user.""" 338 | return nick in self.userdict 339 | 340 | def is_oper(self, nick): 341 | """Check whether a user has operator status in the channel.""" 342 | return nick in self.operdict 343 | 344 | def is_voiced(self, nick): 345 | """Check whether a user has voice mode set in the channel.""" 346 | return nick in self.voiceddict 347 | 348 | def add_user(self, nick): 349 | self.userdict[nick] = 1 350 | 351 | def remove_user(self, nick): 352 | for d in self.userdict, self.operdict, self.voiceddict: 353 | if nick in d: 354 | del d[nick] 355 | 356 | def change_nick(self, before, after): 357 | self.userdict[after] = 1 358 | del self.userdict[before] 359 | if before in self.operdict: 360 | self.operdict[after] = 1 361 | del self.operdict[before] 362 | if before in self.voiceddict: 363 | self.voiceddict[after] = 1 364 | del self.voiceddict[before] 365 | 366 | def set_mode(self, mode, value=None): 367 | """Set mode on the channel. 368 | 369 | Arguments: 370 | 371 | mode -- The mode (a single-character string). 372 | 373 | value -- Value 374 | """ 375 | if mode == "o": 376 | self.operdict[value] = 1 377 | elif mode == "v": 378 | self.voiceddict[value] = 1 379 | else: 380 | self.modes[mode] = value 381 | 382 | def clear_mode(self, mode, value=None): 383 | """Clear mode on the channel. 384 | 385 | Arguments: 386 | 387 | mode -- The mode (a single-character string). 388 | 389 | value -- Value 390 | """ 391 | try: 392 | if mode == "o": 393 | del self.operdict[value] 394 | elif mode == "v": 395 | del self.voiceddict[value] 396 | else: 397 | del self.modes[mode] 398 | except KeyError: 399 | pass 400 | 401 | def has_mode(self, mode): 402 | return mode in self.modes 403 | 404 | def is_moderated(self): 405 | return self.has_mode("m") 406 | 407 | def is_secret(self): 408 | return self.has_mode("s") 409 | 410 | def is_protected(self): 411 | return self.has_mode("p") 412 | 413 | def has_topic_lock(self): 414 | return self.has_mode("t") 415 | 416 | def is_invite_only(self): 417 | return self.has_mode("i") 418 | 419 | def has_allow_external_messages(self): 420 | return self.has_mode("n") 421 | 422 | def has_limit(self): 423 | return self.has_mode("l") 424 | 425 | def limit(self): 426 | if self.has_limit(): 427 | return self.modes[l] 428 | else: 429 | return None 430 | 431 | def has_key(self): 432 | return self.has_mode("k") 433 | 434 | def key(self): 435 | if self.has_key(): 436 | return self.modes["k"] 437 | else: 438 | return None 439 | -------------------------------------------------------------------------------- /bot/irclib.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 1999--2002 Joel Rosdahl 2 | # 3 | # This library is free software; you can redistribute it and/or 4 | # modify it under the terms of the GNU Lesser General Public 5 | # License as published by the Free Software Foundation; either 6 | # version 2.1 of the License, or (at your option) any later version. 7 | # 8 | # This library is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | # Lesser General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Lesser General Public 14 | # License along with this library; if not, write to the Free Software 15 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 16 | # 17 | # keltus 18 | # 19 | # $Id: irclib.py,v 1.47 2008/09/25 22:00:59 keltus Exp $ 20 | 21 | """irclib -- Internet Relay Chat (IRC) protocol client library. 22 | 23 | This library is intended to encapsulate the IRC protocol at a quite 24 | low level. It provides an event-driven IRC client framework. It has 25 | a fairly thorough support for the basic IRC protocol, CTCP, DCC chat, 26 | but DCC file transfers is not yet supported. 27 | 28 | In order to understand how to make an IRC client, I'm afraid you more 29 | or less must understand the IRC specifications. They are available 30 | here: [IRC specifications]. 31 | 32 | The main features of the IRC client framework are: 33 | 34 | * Abstraction of the IRC protocol. 35 | * Handles multiple simultaneous IRC server connections. 36 | * Handles server PONGing transparently. 37 | * Messages to the IRC server are done by calling methods on an IRC 38 | connection object. 39 | * Messages from an IRC server triggers events, which can be caught 40 | by event handlers. 41 | * Reading from and writing to IRC server sockets are normally done 42 | by an internal select() loop, but the select()ing may be done by 43 | an external main loop. 44 | * Functions can be registered to execute at specified times by the 45 | event-loop. 46 | * Decodes CTCP tagging correctly (hopefully); I haven't seen any 47 | other IRC client implementation that handles the CTCP 48 | specification subtilties. 49 | * A kind of simple, single-server, object-oriented IRC client class 50 | that dispatches events to instance methods is included. 51 | 52 | Current limitations: 53 | 54 | * The IRC protocol shines through the abstraction a bit too much. 55 | * Data is not written asynchronously to the server, i.e. the write() 56 | may block if the TCP buffers are stuffed. 57 | * There are no support for DCC file transfers. 58 | * The author haven't even read RFC 2810, 2811, 2812 and 2813. 59 | * Like most projects, documentation is lacking... 60 | 61 | .. [IRC specifications] http://www.irchelp.org/irchelp/rfc/ 62 | """ 63 | 64 | import bisect 65 | import re 66 | import select 67 | import socket 68 | import string 69 | import sys 70 | import time 71 | import types 72 | 73 | VERSION = 0, 4, 8 74 | DEBUG = 0 75 | 76 | # TODO 77 | # ---- 78 | # (maybe) thread safety 79 | # (maybe) color parser convenience functions 80 | # documentation (including all event types) 81 | # (maybe) add awareness of different types of ircds 82 | # send data asynchronously to the server (and DCC connections) 83 | # (maybe) automatically close unused, passive DCC connections after a while 84 | 85 | # NOTES 86 | # ----- 87 | # connection.quit() only sends QUIT to the server. 88 | # ERROR from the server triggers the error event and the disconnect event. 89 | # dropping of the connection triggers the disconnect event. 90 | 91 | class IRCError(Exception): 92 | """Represents an IRC exception.""" 93 | pass 94 | 95 | 96 | class IRC: 97 | """Class that handles one or several IRC server connections. 98 | 99 | When an IRC object has been instantiated, it can be used to create 100 | Connection objects that represent the IRC connections. The 101 | responsibility of the IRC object is to provide an event-driven 102 | framework for the connections and to keep the connections alive. 103 | It runs a select loop to poll each connection's TCP socket and 104 | hands over the sockets with incoming data for processing by the 105 | corresponding connection. 106 | 107 | The methods of most interest for an IRC client writer are server, 108 | add_global_handler, remove_global_handler, execute_at, 109 | execute_delayed, process_once and process_forever. 110 | 111 | Here is an example: 112 | 113 | irc = irclib.IRC() 114 | server = irc.server() 115 | server.connect(\"irc.some.where\", 6667, \"my_nickname\") 116 | server.privmsg(\"a_nickname\", \"Hi there!\") 117 | irc.process_forever() 118 | 119 | This will connect to the IRC server irc.some.where on port 6667 120 | using the nickname my_nickname and send the message \"Hi there!\" 121 | to the nickname a_nickname. 122 | """ 123 | 124 | def __init__(self, fn_to_add_socket=None, 125 | fn_to_remove_socket=None, 126 | fn_to_add_timeout=None): 127 | """Constructor for IRC objects. 128 | 129 | Optional arguments are fn_to_add_socket, fn_to_remove_socket 130 | and fn_to_add_timeout. The first two specify functions that 131 | will be called with a socket object as argument when the IRC 132 | object wants to be notified (or stop being notified) of data 133 | coming on a new socket. When new data arrives, the method 134 | process_data should be called. Similarly, fn_to_add_timeout 135 | is called with a number of seconds (a floating point number) 136 | as first argument when the IRC object wants to receive a 137 | notification (by calling the process_timeout method). So, if 138 | e.g. the argument is 42.17, the object wants the 139 | process_timeout method to be called after 42 seconds and 170 140 | milliseconds. 141 | 142 | The three arguments mainly exist to be able to use an external 143 | main loop (for example Tkinter's or PyGTK's main app loop) 144 | instead of calling the process_forever method. 145 | 146 | An alternative is to just call ServerConnection.process_once() 147 | once in a while. 148 | """ 149 | 150 | if fn_to_add_socket and fn_to_remove_socket: 151 | self.fn_to_add_socket = fn_to_add_socket 152 | self.fn_to_remove_socket = fn_to_remove_socket 153 | else: 154 | self.fn_to_add_socket = None 155 | self.fn_to_remove_socket = None 156 | 157 | self.fn_to_add_timeout = fn_to_add_timeout 158 | self.connections = [] 159 | self.handlers = {} 160 | self.delayed_commands = [] # list of tuples in the format (time, function, arguments) 161 | 162 | self.add_global_handler("ping", _ping_ponger, -42) 163 | 164 | def server(self): 165 | """Creates and returns a ServerConnection object.""" 166 | 167 | c = ServerConnection(self) 168 | self.connections.append(c) 169 | return c 170 | 171 | def process_data(self, sockets): 172 | """Called when there is more data to read on connection sockets. 173 | 174 | Arguments: 175 | 176 | sockets -- A list of socket objects. 177 | 178 | See documentation for IRC.__init__. 179 | """ 180 | for s in sockets: 181 | for c in self.connections: 182 | if s == c._get_socket(): 183 | c.process_data() 184 | 185 | def process_timeout(self): 186 | """Called when a timeout notification is due. 187 | 188 | See documentation for IRC.__init__. 189 | """ 190 | t = time.time() 191 | while self.delayed_commands: 192 | if t >= self.delayed_commands[0][0]: 193 | self.delayed_commands[0][1](*self.delayed_commands[0][2]) 194 | del self.delayed_commands[0] 195 | else: 196 | break 197 | 198 | def process_once(self, timeout=0): 199 | """Process data from connections once. 200 | 201 | Arguments: 202 | 203 | timeout -- How long the select() call should wait if no 204 | data is available. 205 | 206 | This method should be called periodically to check and process 207 | incoming data, if there are any. If that seems boring, look 208 | at the process_forever method. 209 | """ 210 | sockets = map(lambda x: x._get_socket(), self.connections) 211 | sockets = filter(lambda x: x != None, sockets) 212 | if sockets: 213 | (i, o, e) = select.select(sockets, [], [], timeout) 214 | self.process_data(i) 215 | else: 216 | time.sleep(timeout) 217 | self.process_timeout() 218 | 219 | def process_forever(self, timeout=0.2): 220 | """Run an infinite loop, processing data from connections. 221 | 222 | This method repeatedly calls process_once. 223 | 224 | Arguments: 225 | 226 | timeout -- Parameter to pass to process_once. 227 | """ 228 | while 1: 229 | self.process_once(timeout) 230 | 231 | def disconnect_all(self, message=""): 232 | """Disconnects all connections.""" 233 | for c in self.connections: 234 | c.disconnect(message) 235 | 236 | def add_global_handler(self, event, handler, priority=0): 237 | """Adds a global handler function for a specific event type. 238 | 239 | Arguments: 240 | 241 | event -- Event type (a string). Check the values of the 242 | numeric_events dictionary in irclib.py for possible event 243 | types. 244 | 245 | handler -- Callback function. 246 | 247 | priority -- A number (the lower number, the higher priority). 248 | 249 | The handler function is called whenever the specified event is 250 | triggered in any of the connections. See documentation for 251 | the Event class. 252 | 253 | The handler functions are called in priority order (lowest 254 | number is highest priority). If a handler function returns 255 | \"NO MORE\", no more handlers will be called. 256 | """ 257 | if not event in self.handlers: 258 | self.handlers[event] = [] 259 | bisect.insort(self.handlers[event], ((priority, handler))) 260 | 261 | def remove_global_handler(self, event, handler): 262 | """Removes a global handler function. 263 | 264 | Arguments: 265 | 266 | event -- Event type (a string). 267 | 268 | handler -- Callback function. 269 | 270 | Returns 1 on success, otherwise 0. 271 | """ 272 | if not event in self.handlers: 273 | return 0 274 | for h in self.handlers[event]: 275 | if handler == h[1]: 276 | self.handlers[event].remove(h) 277 | return 1 278 | 279 | def execute_at(self, at, function, arguments=()): 280 | """Execute a function at a specified time. 281 | 282 | Arguments: 283 | 284 | at -- Execute at this time (standard \"time_t\" time). 285 | 286 | function -- Function to call. 287 | 288 | arguments -- Arguments to give the function. 289 | """ 290 | self.execute_delayed(at-time.time(), function, arguments) 291 | 292 | def execute_delayed(self, delay, function, arguments=()): 293 | """Execute a function after a specified time. 294 | 295 | Arguments: 296 | 297 | delay -- How many seconds to wait. 298 | 299 | function -- Function to call. 300 | 301 | arguments -- Arguments to give the function. 302 | """ 303 | bisect.insort(self.delayed_commands, (delay+time.time(), function, arguments)) 304 | if self.fn_to_add_timeout: 305 | self.fn_to_add_timeout(delay) 306 | 307 | def dcc(self, dcctype="chat"): 308 | """Creates and returns a DCCConnection object. 309 | 310 | Arguments: 311 | 312 | dcctype -- "chat" for DCC CHAT connections or "raw" for 313 | DCC SEND (or other DCC types). If "chat", 314 | incoming data will be split in newline-separated 315 | chunks. If "raw", incoming data is not touched. 316 | """ 317 | c = DCCConnection(self, dcctype) 318 | self.connections.append(c) 319 | return c 320 | 321 | def _handle_event(self, connection, event): 322 | """[Internal]""" 323 | h = self.handlers 324 | for handler in h.get("all_events", []) + h.get(event.eventtype(), []): 325 | if handler[1](connection, event) == "NO MORE": 326 | return 327 | 328 | def _remove_connection(self, connection): 329 | """[Internal]""" 330 | self.connections.remove(connection) 331 | if self.fn_to_remove_socket: 332 | self.fn_to_remove_socket(connection._get_socket()) 333 | 334 | _rfc_1459_command_regexp = re.compile("^(:(?P[^ ]+) +)?(?P[^ ]+)( *(?P .+))?") 335 | 336 | class Connection: 337 | """Base class for IRC connections. 338 | 339 | Must be overridden. 340 | """ 341 | def __init__(self, irclibobj): 342 | self.irclibobj = irclibobj 343 | 344 | def _get_socket(): 345 | raise IRCError, "Not overridden" 346 | 347 | ############################## 348 | ### Convenience wrappers. 349 | 350 | def execute_at(self, at, function, arguments=()): 351 | self.irclibobj.execute_at(at, function, arguments) 352 | 353 | def execute_delayed(self, delay, function, arguments=()): 354 | self.irclibobj.execute_delayed(delay, function, arguments) 355 | 356 | 357 | class ServerConnectionError(IRCError): 358 | pass 359 | 360 | class ServerNotConnectedError(ServerConnectionError): 361 | pass 362 | 363 | 364 | # Huh!? Crrrrazy EFNet doesn't follow the RFC: their ircd seems to 365 | # use \n as message separator! :P 366 | _linesep_regexp = re.compile("\r?\n") 367 | 368 | class ServerConnection(Connection): 369 | """This class represents an IRC server connection. 370 | 371 | ServerConnection objects are instantiated by calling the server 372 | method on an IRC object. 373 | """ 374 | 375 | def __init__(self, irclibobj): 376 | Connection.__init__(self, irclibobj) 377 | self.connected = 0 # Not connected yet. 378 | self.socket = None 379 | self.ssl = None 380 | 381 | def connect(self, server, port, nickname, password=None, username=None, 382 | ircname=None, localaddress="", localport=0, ssl=False, ipv6=False): 383 | """Connect/reconnect to a server. 384 | 385 | Arguments: 386 | 387 | server -- Server name. 388 | 389 | port -- Port number. 390 | 391 | nickname -- The nickname. 392 | 393 | password -- Password (if any). 394 | 395 | username -- The username. 396 | 397 | ircname -- The IRC name ("realname"). 398 | 399 | localaddress -- Bind the connection to a specific local IP address. 400 | 401 | localport -- Bind the connection to a specific local port. 402 | 403 | ssl -- Enable support for ssl. 404 | 405 | ipv6 -- Enable support for ipv6. 406 | 407 | This function can be called to reconnect a closed connection. 408 | 409 | Returns the ServerConnection object. 410 | """ 411 | if self.connected: 412 | self.disconnect("Changing servers") 413 | 414 | self.previous_buffer = "" 415 | self.handlers = {} 416 | self.real_server_name = "" 417 | self.real_nickname = nickname 418 | self.server = server 419 | self.port = port 420 | self.nickname = nickname 421 | self.username = username or nickname 422 | self.ircname = ircname or nickname 423 | self.password = password 424 | self.localaddress = localaddress 425 | self.localport = localport 426 | self.localhost = socket.gethostname() 427 | if ipv6: 428 | self.socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) 429 | else: 430 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 431 | try: 432 | self.socket.bind((self.localaddress, self.localport)) 433 | self.socket.connect((self.server, self.port)) 434 | if ssl: 435 | self.ssl = socket.ssl(self.socket) 436 | except socket.error, x: 437 | self.socket.close() 438 | self.socket = None 439 | raise ServerConnectionError, "Couldn't connect to socket: %s" % x 440 | self.connected = 1 441 | if self.irclibobj.fn_to_add_socket: 442 | self.irclibobj.fn_to_add_socket(self.socket) 443 | 444 | # Log on... 445 | if self.password: 446 | self.pass_(self.password) 447 | self.nick(self.nickname) 448 | self.user(self.username, self.ircname) 449 | return self 450 | 451 | def close(self): 452 | """Close the connection. 453 | 454 | This method closes the connection permanently; after it has 455 | been called, the object is unusable. 456 | """ 457 | 458 | self.disconnect("Closing object") 459 | self.irclibobj._remove_connection(self) 460 | 461 | def _get_socket(self): 462 | """[Internal]""" 463 | return self.socket 464 | 465 | def get_server_name(self): 466 | """Get the (real) server name. 467 | 468 | This method returns the (real) server name, or, more 469 | specifically, what the server calls itself. 470 | """ 471 | 472 | if self.real_server_name: 473 | return self.real_server_name 474 | else: 475 | return "" 476 | 477 | def get_nickname(self): 478 | """Get the (real) nick name. 479 | 480 | This method returns the (real) nickname. The library keeps 481 | track of nick changes, so it might not be the nick name that 482 | was passed to the connect() method. """ 483 | 484 | return self.real_nickname 485 | 486 | def process_data(self): 487 | """[Internal]""" 488 | 489 | try: 490 | if self.ssl: 491 | new_data = self.ssl.read(2**14) 492 | else: 493 | new_data = self.socket.recv(2**14) 494 | except socket.error, x: 495 | # The server hung up. 496 | self.disconnect("Connection reset by peer") 497 | return 498 | if not new_data: 499 | # Read nothing: connection must be down. 500 | self.disconnect("Connection reset by peer") 501 | return 502 | 503 | lines = _linesep_regexp.split(self.previous_buffer + new_data) 504 | 505 | # Save the last, unfinished line. 506 | self.previous_buffer = lines.pop() 507 | 508 | for line in lines: 509 | if DEBUG: 510 | print "FROM SERVER:", line 511 | 512 | if not line: 513 | continue 514 | 515 | prefix = None 516 | command = None 517 | arguments = None 518 | self._handle_event(Event("all_raw_messages", 519 | self.get_server_name(), 520 | None, 521 | [line])) 522 | 523 | m = _rfc_1459_command_regexp.match(line) 524 | if m.group("prefix"): 525 | prefix = m.group("prefix") 526 | if not self.real_server_name: 527 | self.real_server_name = prefix 528 | 529 | if m.group("command"): 530 | command = m.group("command").lower() 531 | 532 | if m.group("argument"): 533 | a = m.group("argument").split(" :", 1) 534 | arguments = a[0].split() 535 | if len(a) == 2: 536 | arguments.append(a[1]) 537 | 538 | # Translate numerics into more readable strings. 539 | if command in numeric_events: 540 | command = numeric_events[command] 541 | 542 | if command == "nick": 543 | if nm_to_n(prefix) == self.real_nickname: 544 | self.real_nickname = arguments[0] 545 | elif command == "welcome": 546 | # Record the nickname in case the client changed nick 547 | # in a nicknameinuse callback. 548 | self.real_nickname = arguments[0] 549 | 550 | if command in ["privmsg", "notice"]: 551 | target, message = arguments[0], arguments[1] 552 | messages = _ctcp_dequote(message) 553 | 554 | if command == "privmsg": 555 | if is_channel(target): 556 | command = "pubmsg" 557 | else: 558 | if is_channel(target): 559 | command = "pubnotice" 560 | else: 561 | command = "privnotice" 562 | 563 | for m in messages: 564 | if type(m) is types.TupleType: 565 | if command in ["privmsg", "pubmsg"]: 566 | command = "ctcp" 567 | else: 568 | command = "ctcpreply" 569 | 570 | m = list(m) 571 | if DEBUG: 572 | print "command: %s, source: %s, target: %s, arguments: %s" % ( 573 | command, prefix, target, m) 574 | self._handle_event(Event(command, prefix, target, m)) 575 | if command == "ctcp" and m[0] == "ACTION": 576 | self._handle_event(Event("action", prefix, target, m[1:])) 577 | else: 578 | if DEBUG: 579 | print "command: %s, source: %s, target: %s, arguments: %s" % ( 580 | command, prefix, target, [m]) 581 | self._handle_event(Event(command, prefix, target, [m])) 582 | else: 583 | target = None 584 | 585 | if command == "quit": 586 | arguments = [arguments[0]] 587 | elif command == "ping": 588 | target = arguments[0] 589 | else: 590 | target = arguments[0] 591 | arguments = arguments[1:] 592 | 593 | if command == "mode": 594 | if not is_channel(target): 595 | command = "umode" 596 | 597 | if DEBUG: 598 | print "command: %s, source: %s, target: %s, arguments: %s" % ( 599 | command, prefix, target, arguments) 600 | self._handle_event(Event(command, prefix, target, arguments)) 601 | 602 | def _handle_event(self, event): 603 | """[Internal]""" 604 | self.irclibobj._handle_event(self, event) 605 | if event.eventtype() in self.handlers: 606 | for fn in self.handlers[event.eventtype()]: 607 | fn(self, event) 608 | 609 | def is_connected(self): 610 | """Return connection status. 611 | 612 | Returns true if connected, otherwise false. 613 | """ 614 | return self.connected 615 | 616 | def add_global_handler(self, *args): 617 | """Add global handler. 618 | 619 | See documentation for IRC.add_global_handler. 620 | """ 621 | self.irclibobj.add_global_handler(*args) 622 | 623 | def remove_global_handler(self, *args): 624 | """Remove global handler. 625 | 626 | See documentation for IRC.remove_global_handler. 627 | """ 628 | self.irclibobj.remove_global_handler(*args) 629 | 630 | def action(self, target, action): 631 | """Send a CTCP ACTION command.""" 632 | self.ctcp("ACTION", target, action) 633 | 634 | def admin(self, server=""): 635 | """Send an ADMIN command.""" 636 | self.send_raw(" ".join(["ADMIN", server]).strip()) 637 | 638 | def ctcp(self, ctcptype, target, parameter=""): 639 | """Send a CTCP command.""" 640 | ctcptype = ctcptype.upper() 641 | self.privmsg(target, "\001%s%s\001" % (ctcptype, parameter and (" " + parameter) or "")) 642 | 643 | def ctcp_reply(self, target, parameter): 644 | """Send a CTCP REPLY command.""" 645 | self.notice(target, "\001%s\001" % parameter) 646 | 647 | def disconnect(self, message=""): 648 | """Hang up the connection. 649 | 650 | Arguments: 651 | 652 | message -- Quit message. 653 | """ 654 | if not self.connected: 655 | return 656 | 657 | self.connected = 0 658 | 659 | self.quit(message) 660 | 661 | try: 662 | self.socket.close() 663 | except socket.error, x: 664 | pass 665 | self.socket = None 666 | self._handle_event(Event("disconnect", self.server, "", [message])) 667 | 668 | def globops(self, text): 669 | """Send a GLOBOPS command.""" 670 | self.send_raw("GLOBOPS :" + text) 671 | 672 | def info(self, server=""): 673 | """Send an INFO command.""" 674 | self.send_raw(" ".join(["INFO", server]).strip()) 675 | 676 | def invite(self, nick, channel): 677 | """Send an INVITE command.""" 678 | self.send_raw(" ".join(["INVITE", nick, channel]).strip()) 679 | 680 | def ison(self, nicks): 681 | """Send an ISON command. 682 | 683 | Arguments: 684 | 685 | nicks -- List of nicks. 686 | """ 687 | self.send_raw("ISON " + " ".join(nicks)) 688 | 689 | def join(self, channel, key=""): 690 | """Send a JOIN command.""" 691 | self.send_raw("JOIN %s%s" % (channel, (key and (" " + key)))) 692 | 693 | def kick(self, channel, nick, comment=""): 694 | """Send a KICK command.""" 695 | self.send_raw("KICK %s %s%s" % (channel, nick, (comment and (" :" + comment)))) 696 | 697 | def links(self, remote_server="", server_mask=""): 698 | """Send a LINKS command.""" 699 | command = "LINKS" 700 | if remote_server: 701 | command = command + " " + remote_server 702 | if server_mask: 703 | command = command + " " + server_mask 704 | self.send_raw(command) 705 | 706 | def list(self, channels=None, server=""): 707 | """Send a LIST command.""" 708 | command = "LIST" 709 | if channels: 710 | command = command + " " + ",".join(channels) 711 | if server: 712 | command = command + " " + server 713 | self.send_raw(command) 714 | 715 | def lusers(self, server=""): 716 | """Send a LUSERS command.""" 717 | self.send_raw("LUSERS" + (server and (" " + server))) 718 | 719 | def mode(self, target, command): 720 | """Send a MODE command.""" 721 | self.send_raw("MODE %s %s" % (target, command)) 722 | 723 | def motd(self, server=""): 724 | """Send an MOTD command.""" 725 | self.send_raw("MOTD" + (server and (" " + server))) 726 | 727 | def names(self, channels=None): 728 | """Send a NAMES command.""" 729 | self.send_raw("NAMES" + (channels and (" " + ",".join(channels)) or "")) 730 | 731 | def nick(self, newnick): 732 | """Send a NICK command.""" 733 | self.send_raw("NICK " + newnick) 734 | 735 | def notice(self, target, text): 736 | """Send a NOTICE command.""" 737 | # Should limit len(text) here! 738 | self.send_raw("NOTICE %s :%s" % (target, text)) 739 | 740 | def oper(self, nick, password): 741 | """Send an OPER command.""" 742 | self.send_raw("OPER %s %s" % (nick, password)) 743 | 744 | def part(self, channels, message=""): 745 | """Send a PART command.""" 746 | if type(channels) == types.StringType: 747 | self.send_raw("PART " + channels + (message and (" " + message))) 748 | else: 749 | self.send_raw("PART " + ",".join(channels) + (message and (" " + message))) 750 | 751 | def pass_(self, password): 752 | """Send a PASS command.""" 753 | self.send_raw("PASS " + password) 754 | 755 | def ping(self, target, target2=""): 756 | """Send a PING command.""" 757 | self.send_raw("PING %s%s" % (target, target2 and (" " + target2))) 758 | 759 | def pong(self, target, target2=""): 760 | """Send a PONG command.""" 761 | self.send_raw("PONG %s%s" % (target, target2 and (" " + target2))) 762 | 763 | def privmsg(self, target, text): 764 | """Send a PRIVMSG command.""" 765 | # Should limit len(text) here! 766 | self.send_raw("PRIVMSG %s :%s" % (target, text)) 767 | 768 | def privmsg_many(self, targets, text): 769 | """Send a PRIVMSG command to multiple targets.""" 770 | # Should limit len(text) here! 771 | self.send_raw("PRIVMSG %s :%s" % (",".join(targets), text)) 772 | 773 | def quit(self, message=""): 774 | """Send a QUIT command.""" 775 | # Note that many IRC servers don't use your QUIT message 776 | # unless you've been connected for at least 5 minutes! 777 | self.send_raw("QUIT" + (message and (" :" + message))) 778 | 779 | def send_raw(self, string): 780 | """Send raw string to the server. 781 | 782 | The string will be padded with appropriate CR LF. 783 | """ 784 | if self.socket is None: 785 | raise ServerNotConnectedError, "Not connected." 786 | try: 787 | if self.ssl: 788 | self.ssl.write(string + "\r\n") 789 | else: 790 | self.socket.send(string + "\r\n") 791 | if DEBUG: 792 | print "TO SERVER:", string 793 | except socket.error, x: 794 | # Ouch! 795 | self.disconnect("Connection reset by peer.") 796 | 797 | def squit(self, server, comment=""): 798 | """Send an SQUIT command.""" 799 | self.send_raw("SQUIT %s%s" % (server, comment and (" :" + comment))) 800 | 801 | def stats(self, statstype, server=""): 802 | """Send a STATS command.""" 803 | self.send_raw("STATS %s%s" % (statstype, server and (" " + server))) 804 | 805 | def time(self, server=""): 806 | """Send a TIME command.""" 807 | self.send_raw("TIME" + (server and (" " + server))) 808 | 809 | def topic(self, channel, new_topic=None): 810 | """Send a TOPIC command.""" 811 | if new_topic is None: 812 | self.send_raw("TOPIC " + channel) 813 | else: 814 | self.send_raw("TOPIC %s :%s" % (channel, new_topic)) 815 | 816 | def trace(self, target=""): 817 | """Send a TRACE command.""" 818 | self.send_raw("TRACE" + (target and (" " + target))) 819 | 820 | def user(self, username, realname): 821 | """Send a USER command.""" 822 | self.send_raw("USER %s 0 * :%s" % (username, realname)) 823 | 824 | def userhost(self, nicks): 825 | """Send a USERHOST command.""" 826 | self.send_raw("USERHOST " + ",".join(nicks)) 827 | 828 | def users(self, server=""): 829 | """Send a USERS command.""" 830 | self.send_raw("USERS" + (server and (" " + server))) 831 | 832 | def version(self, server=""): 833 | """Send a VERSION command.""" 834 | self.send_raw("VERSION" + (server and (" " + server))) 835 | 836 | def wallops(self, text): 837 | """Send a WALLOPS command.""" 838 | self.send_raw("WALLOPS :" + text) 839 | 840 | def who(self, target="", op=""): 841 | """Send a WHO command.""" 842 | self.send_raw("WHO%s%s" % (target and (" " + target), op and (" o"))) 843 | 844 | def whois(self, targets): 845 | """Send a WHOIS command.""" 846 | self.send_raw("WHOIS " + ",".join(targets)) 847 | 848 | def whowas(self, nick, max="", server=""): 849 | """Send a WHOWAS command.""" 850 | self.send_raw("WHOWAS %s%s%s" % (nick, 851 | max and (" " + max), 852 | server and (" " + server))) 853 | 854 | class DCCConnectionError(IRCError): 855 | pass 856 | 857 | 858 | class DCCConnection(Connection): 859 | """This class represents a DCC connection. 860 | 861 | DCCConnection objects are instantiated by calling the dcc 862 | method on an IRC object. 863 | """ 864 | def __init__(self, irclibobj, dcctype): 865 | Connection.__init__(self, irclibobj) 866 | self.connected = 0 867 | self.passive = 0 868 | self.dcctype = dcctype 869 | self.peeraddress = None 870 | self.peerport = None 871 | 872 | def connect(self, address, port): 873 | """Connect/reconnect to a DCC peer. 874 | 875 | Arguments: 876 | address -- Host/IP address of the peer. 877 | 878 | port -- The port number to connect to. 879 | 880 | Returns the DCCConnection object. 881 | """ 882 | self.peeraddress = socket.gethostbyname(address) 883 | self.peerport = port 884 | self.socket = None 885 | self.previous_buffer = "" 886 | self.handlers = {} 887 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 888 | self.passive = 0 889 | try: 890 | self.socket.connect((self.peeraddress, self.peerport)) 891 | except socket.error, x: 892 | raise DCCConnectionError, "Couldn't connect to socket: %s" % x 893 | self.connected = 1 894 | if self.irclibobj.fn_to_add_socket: 895 | self.irclibobj.fn_to_add_socket(self.socket) 896 | return self 897 | 898 | def listen(self): 899 | """Wait for a connection/reconnection from a DCC peer. 900 | 901 | Returns the DCCConnection object. 902 | 903 | The local IP address and port are available as 904 | self.localaddress and self.localport. After connection from a 905 | peer, the peer address and port are available as 906 | self.peeraddress and self.peerport. 907 | """ 908 | self.previous_buffer = "" 909 | self.handlers = {} 910 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 911 | self.passive = 1 912 | try: 913 | self.socket.bind((socket.gethostbyname(socket.gethostname()), 0)) 914 | self.localaddress, self.localport = self.socket.getsockname() 915 | self.socket.listen(10) 916 | except socket.error, x: 917 | raise DCCConnectionError, "Couldn't bind socket: %s" % x 918 | return self 919 | 920 | def disconnect(self, message=""): 921 | """Hang up the connection and close the object. 922 | 923 | Arguments: 924 | 925 | message -- Quit message. 926 | """ 927 | if not self.connected: 928 | return 929 | 930 | self.connected = 0 931 | try: 932 | self.socket.close() 933 | except socket.error, x: 934 | pass 935 | self.socket = None 936 | self.irclibobj._handle_event( 937 | self, 938 | Event("dcc_disconnect", self.peeraddress, "", [message])) 939 | self.irclibobj._remove_connection(self) 940 | 941 | def process_data(self): 942 | """[Internal]""" 943 | 944 | if self.passive and not self.connected: 945 | conn, (self.peeraddress, self.peerport) = self.socket.accept() 946 | self.socket.close() 947 | self.socket = conn 948 | self.connected = 1 949 | if DEBUG: 950 | print "DCC connection from %s:%d" % ( 951 | self.peeraddress, self.peerport) 952 | self.irclibobj._handle_event( 953 | self, 954 | Event("dcc_connect", self.peeraddress, None, None)) 955 | return 956 | 957 | try: 958 | new_data = self.socket.recv(2**14) 959 | except socket.error, x: 960 | # The server hung up. 961 | self.disconnect("Connection reset by peer") 962 | return 963 | if not new_data: 964 | # Read nothing: connection must be down. 965 | self.disconnect("Connection reset by peer") 966 | return 967 | 968 | if self.dcctype == "chat": 969 | # The specification says lines are terminated with LF, but 970 | # it seems safer to handle CR LF terminations too. 971 | chunks = _linesep_regexp.split(self.previous_buffer + new_data) 972 | 973 | # Save the last, unfinished line. 974 | self.previous_buffer = chunks[-1] 975 | if len(self.previous_buffer) > 2**14: 976 | # Bad peer! Naughty peer! 977 | self.disconnect() 978 | return 979 | chunks = chunks[:-1] 980 | else: 981 | chunks = [new_data] 982 | 983 | command = "dccmsg" 984 | prefix = self.peeraddress 985 | target = None 986 | for chunk in chunks: 987 | if DEBUG: 988 | print "FROM PEER:", chunk 989 | arguments = [chunk] 990 | if DEBUG: 991 | print "command: %s, source: %s, target: %s, arguments: %s" % ( 992 | command, prefix, target, arguments) 993 | self.irclibobj._handle_event( 994 | self, 995 | Event(command, prefix, target, arguments)) 996 | 997 | def _get_socket(self): 998 | """[Internal]""" 999 | return self.socket 1000 | 1001 | def privmsg(self, string): 1002 | """Send data to DCC peer. 1003 | 1004 | The string will be padded with appropriate LF if it's a DCC 1005 | CHAT session. 1006 | """ 1007 | try: 1008 | self.socket.send(string) 1009 | if self.dcctype == "chat": 1010 | self.socket.send("\n") 1011 | if DEBUG: 1012 | print "TO PEER: %s\n" % string 1013 | except socket.error, x: 1014 | # Ouch! 1015 | self.disconnect("Connection reset by peer.") 1016 | 1017 | class SimpleIRCClient: 1018 | """A simple single-server IRC client class. 1019 | 1020 | This is an example of an object-oriented wrapper of the IRC 1021 | framework. A real IRC client can be made by subclassing this 1022 | class and adding appropriate methods. 1023 | 1024 | The method on_join will be called when a "join" event is created 1025 | (which is done when the server sends a JOIN messsage/command), 1026 | on_privmsg will be called for "privmsg" events, and so on. The 1027 | handler methods get two arguments: the connection object (same as 1028 | self.connection) and the event object. 1029 | 1030 | Instance attributes that can be used by sub classes: 1031 | 1032 | ircobj -- The IRC instance. 1033 | 1034 | connection -- The ServerConnection instance. 1035 | 1036 | dcc_connections -- A list of DCCConnection instances. 1037 | """ 1038 | def __init__(self): 1039 | self.ircobj = IRC() 1040 | self.connection = self.ircobj.server() 1041 | self.dcc_connections = [] 1042 | self.ircobj.add_global_handler("all_events", self._dispatcher, -10) 1043 | self.ircobj.add_global_handler("dcc_disconnect", self._dcc_disconnect, -10) 1044 | 1045 | def _dispatcher(self, c, e): 1046 | """[Internal]""" 1047 | m = "on_" + e.eventtype() 1048 | if hasattr(self, m): 1049 | getattr(self, m)(c, e) 1050 | 1051 | def _dcc_disconnect(self, c, e): 1052 | self.dcc_connections.remove(c) 1053 | 1054 | def connect(self, server, port, nickname, password=None, username=None, 1055 | ircname=None, localaddress="", localport=0, ssl=False, ipv6=False): 1056 | """Connect/reconnect to a server. 1057 | 1058 | Arguments: 1059 | 1060 | server -- Server name. 1061 | 1062 | port -- Port number. 1063 | 1064 | nickname -- The nickname. 1065 | 1066 | password -- Password (if any). 1067 | 1068 | username -- The username. 1069 | 1070 | ircname -- The IRC name. 1071 | 1072 | localaddress -- Bind the connection to a specific local IP address. 1073 | 1074 | localport -- Bind the connection to a specific local port. 1075 | 1076 | ssl -- Enable support for ssl. 1077 | 1078 | ipv6 -- Enable support for ipv6. 1079 | 1080 | This function can be called to reconnect a closed connection. 1081 | """ 1082 | self.connection.connect(server, port, nickname, 1083 | password, username, ircname, 1084 | localaddress, localport, ssl, ipv6) 1085 | 1086 | def dcc_connect(self, address, port, dcctype="chat"): 1087 | """Connect to a DCC peer. 1088 | 1089 | Arguments: 1090 | 1091 | address -- IP address of the peer. 1092 | 1093 | port -- Port to connect to. 1094 | 1095 | Returns a DCCConnection instance. 1096 | """ 1097 | dcc = self.ircobj.dcc(dcctype) 1098 | self.dcc_connections.append(dcc) 1099 | dcc.connect(address, port) 1100 | return dcc 1101 | 1102 | def dcc_listen(self, dcctype="chat"): 1103 | """Listen for connections from a DCC peer. 1104 | 1105 | Returns a DCCConnection instance. 1106 | """ 1107 | dcc = self.ircobj.dcc(dcctype) 1108 | self.dcc_connections.append(dcc) 1109 | dcc.listen() 1110 | return dcc 1111 | 1112 | def start(self): 1113 | """Start the IRC client.""" 1114 | self.ircobj.process_forever() 1115 | 1116 | 1117 | class Event: 1118 | """Class representing an IRC event.""" 1119 | def __init__(self, eventtype, source, target, arguments=None): 1120 | """Constructor of Event objects. 1121 | 1122 | Arguments: 1123 | 1124 | eventtype -- A string describing the event. 1125 | 1126 | source -- The originator of the event (a nick mask or a server). 1127 | 1128 | target -- The target of the event (a nick or a channel). 1129 | 1130 | arguments -- Any event specific arguments. 1131 | """ 1132 | self._eventtype = eventtype 1133 | self._source = source 1134 | self._target = target 1135 | if arguments: 1136 | self._arguments = arguments 1137 | else: 1138 | self._arguments = [] 1139 | 1140 | def eventtype(self): 1141 | """Get the event type.""" 1142 | return self._eventtype 1143 | 1144 | def source(self): 1145 | """Get the event source.""" 1146 | return self._source 1147 | 1148 | def target(self): 1149 | """Get the event target.""" 1150 | return self._target 1151 | 1152 | def arguments(self): 1153 | """Get the event arguments.""" 1154 | return self._arguments 1155 | 1156 | _LOW_LEVEL_QUOTE = "\020" 1157 | _CTCP_LEVEL_QUOTE = "\134" 1158 | _CTCP_DELIMITER = "\001" 1159 | 1160 | _low_level_mapping = { 1161 | "0": "\000", 1162 | "n": "\n", 1163 | "r": "\r", 1164 | _LOW_LEVEL_QUOTE: _LOW_LEVEL_QUOTE 1165 | } 1166 | 1167 | _low_level_regexp = re.compile(_LOW_LEVEL_QUOTE + "(.)") 1168 | 1169 | def mask_matches(nick, mask): 1170 | """Check if a nick matches a mask. 1171 | 1172 | Returns true if the nick matches, otherwise false. 1173 | """ 1174 | nick = irc_lower(nick) 1175 | mask = irc_lower(mask) 1176 | mask = mask.replace("\\", "\\\\") 1177 | for ch in ".$|[](){}+": 1178 | mask = mask.replace(ch, "\\" + ch) 1179 | mask = mask.replace("?", ".") 1180 | mask = mask.replace("*", ".*") 1181 | r = re.compile(mask, re.IGNORECASE) 1182 | return r.match(nick) 1183 | 1184 | _special = "-[]\\`^{}" 1185 | nick_characters = string.ascii_letters + string.digits + _special 1186 | _ircstring_translation = string.maketrans(string.ascii_uppercase + "[]\\^", 1187 | string.ascii_lowercase + "{}|~") 1188 | 1189 | def irc_lower(s): 1190 | """Returns a lowercased string. 1191 | 1192 | The definition of lowercased comes from the IRC specification (RFC 1193 | 1459). 1194 | """ 1195 | return s.translate(_ircstring_translation) 1196 | 1197 | def _ctcp_dequote(message): 1198 | """[Internal] Dequote a message according to CTCP specifications. 1199 | 1200 | The function returns a list where each element can be either a 1201 | string (normal message) or a tuple of one or two strings (tagged 1202 | messages). If a tuple has only one element (ie is a singleton), 1203 | that element is the tag; otherwise the tuple has two elements: the 1204 | tag and the data. 1205 | 1206 | Arguments: 1207 | 1208 | message -- The message to be decoded. 1209 | """ 1210 | 1211 | def _low_level_replace(match_obj): 1212 | ch = match_obj.group(1) 1213 | 1214 | # If low_level_mapping doesn't have the character as key, we 1215 | # should just return the character. 1216 | return _low_level_mapping.get(ch, ch) 1217 | 1218 | if _LOW_LEVEL_QUOTE in message: 1219 | # Yup, there was a quote. Release the dequoter, man! 1220 | message = _low_level_regexp.sub(_low_level_replace, message) 1221 | 1222 | if _CTCP_DELIMITER not in message: 1223 | return [message] 1224 | else: 1225 | # Split it into parts. (Does any IRC client actually *use* 1226 | # CTCP stacking like this?) 1227 | chunks = message.split(_CTCP_DELIMITER) 1228 | 1229 | messages = [] 1230 | i = 0 1231 | while i < len(chunks)-1: 1232 | # Add message if it's non-empty. 1233 | if len(chunks[i]) > 0: 1234 | messages.append(chunks[i]) 1235 | 1236 | if i < len(chunks)-2: 1237 | # Aye! CTCP tagged data ahead! 1238 | messages.append(tuple(chunks[i+1].split(" ", 1))) 1239 | 1240 | i = i + 2 1241 | 1242 | if len(chunks) % 2 == 0: 1243 | # Hey, a lonely _CTCP_DELIMITER at the end! This means 1244 | # that the last chunk, including the delimiter, is a 1245 | # normal message! (This is according to the CTCP 1246 | # specification.) 1247 | messages.append(_CTCP_DELIMITER + chunks[-1]) 1248 | 1249 | return messages 1250 | 1251 | def is_channel(string): 1252 | """Check if a string is a channel name. 1253 | 1254 | Returns true if the argument is a channel name, otherwise false. 1255 | """ 1256 | return string and string[0] in "#&+!" 1257 | 1258 | def ip_numstr_to_quad(num): 1259 | """Convert an IP number as an integer given in ASCII 1260 | representation (e.g. '3232235521') to an IP address string 1261 | (e.g. '192.168.0.1').""" 1262 | n = long(num) 1263 | p = map(str, map(int, [n >> 24 & 0xFF, n >> 16 & 0xFF, 1264 | n >> 8 & 0xFF, n & 0xFF])) 1265 | return ".".join(p) 1266 | 1267 | def ip_quad_to_numstr(quad): 1268 | """Convert an IP address string (e.g. '192.168.0.1') to an IP 1269 | number as an integer given in ASCII representation 1270 | (e.g. '3232235521').""" 1271 | p = map(long, quad.split(".")) 1272 | s = str((p[0] << 24) | (p[1] << 16) | (p[2] << 8) | p[3]) 1273 | if s[-1] == "L": 1274 | s = s[:-1] 1275 | return s 1276 | 1277 | def nm_to_n(s): 1278 | """Get the nick part of a nickmask. 1279 | 1280 | (The source of an Event is a nickmask.) 1281 | """ 1282 | return s.split("!")[0] 1283 | 1284 | def nm_to_uh(s): 1285 | """Get the userhost part of a nickmask. 1286 | 1287 | (The source of an Event is a nickmask.) 1288 | """ 1289 | return s.split("!")[1] 1290 | 1291 | def nm_to_h(s): 1292 | """Get the host part of a nickmask. 1293 | 1294 | (The source of an Event is a nickmask.) 1295 | """ 1296 | return s.split("@")[1] 1297 | 1298 | def nm_to_u(s): 1299 | """Get the user part of a nickmask. 1300 | 1301 | (The source of an Event is a nickmask.) 1302 | """ 1303 | s = s.split("!")[1] 1304 | return s.split("@")[0] 1305 | 1306 | def parse_nick_modes(mode_string): 1307 | """Parse a nick mode string. 1308 | 1309 | The function returns a list of lists with three members: sign, 1310 | mode and argument. The sign is \"+\" or \"-\". The argument is 1311 | always None. 1312 | 1313 | Example: 1314 | 1315 | >>> irclib.parse_nick_modes(\"+ab-c\") 1316 | [['+', 'a', None], ['+', 'b', None], ['-', 'c', None]] 1317 | """ 1318 | 1319 | return _parse_modes(mode_string, "") 1320 | 1321 | def parse_channel_modes(mode_string): 1322 | """Parse a channel mode string. 1323 | 1324 | The function returns a list of lists with three members: sign, 1325 | mode and argument. The sign is \"+\" or \"-\". The argument is 1326 | None if mode isn't one of \"b\", \"k\", \"l\", \"v\" or \"o\". 1327 | 1328 | Example: 1329 | 1330 | >>> irclib.parse_channel_modes(\"+ab-c foo\") 1331 | [['+', 'a', None], ['+', 'b', 'foo'], ['-', 'c', None]] 1332 | """ 1333 | 1334 | return _parse_modes(mode_string, "bklvo") 1335 | 1336 | def _parse_modes(mode_string, unary_modes=""): 1337 | """[Internal]""" 1338 | modes = [] 1339 | arg_count = 0 1340 | 1341 | # State variable. 1342 | sign = "" 1343 | 1344 | a = mode_string.split() 1345 | if len(a) == 0: 1346 | return [] 1347 | else: 1348 | mode_part, args = a[0], a[1:] 1349 | 1350 | if mode_part[0] not in "+-": 1351 | return [] 1352 | for ch in mode_part: 1353 | if ch in "+-": 1354 | sign = ch 1355 | elif ch == " ": 1356 | collecting_arguments = 1 1357 | elif ch in unary_modes: 1358 | if len(args) >= arg_count + 1: 1359 | modes.append([sign, ch, args[arg_count]]) 1360 | arg_count = arg_count + 1 1361 | else: 1362 | modes.append([sign, ch, None]) 1363 | else: 1364 | modes.append([sign, ch, None]) 1365 | return modes 1366 | 1367 | def _ping_ponger(connection, event): 1368 | """[Internal]""" 1369 | connection.pong(event.target()) 1370 | 1371 | # Numeric table mostly stolen from the Perl IRC module (Net::IRC). 1372 | numeric_events = { 1373 | "001": "welcome", 1374 | "002": "yourhost", 1375 | "003": "created", 1376 | "004": "myinfo", 1377 | "005": "featurelist", # XXX 1378 | "200": "tracelink", 1379 | "201": "traceconnecting", 1380 | "202": "tracehandshake", 1381 | "203": "traceunknown", 1382 | "204": "traceoperator", 1383 | "205": "traceuser", 1384 | "206": "traceserver", 1385 | "207": "traceservice", 1386 | "208": "tracenewtype", 1387 | "209": "traceclass", 1388 | "210": "tracereconnect", 1389 | "211": "statslinkinfo", 1390 | "212": "statscommands", 1391 | "213": "statscline", 1392 | "214": "statsnline", 1393 | "215": "statsiline", 1394 | "216": "statskline", 1395 | "217": "statsqline", 1396 | "218": "statsyline", 1397 | "219": "endofstats", 1398 | "221": "umodeis", 1399 | "231": "serviceinfo", 1400 | "232": "endofservices", 1401 | "233": "service", 1402 | "234": "servlist", 1403 | "235": "servlistend", 1404 | "241": "statslline", 1405 | "242": "statsuptime", 1406 | "243": "statsoline", 1407 | "244": "statshline", 1408 | "250": "luserconns", 1409 | "251": "luserclient", 1410 | "252": "luserop", 1411 | "253": "luserunknown", 1412 | "254": "luserchannels", 1413 | "255": "luserme", 1414 | "256": "adminme", 1415 | "257": "adminloc1", 1416 | "258": "adminloc2", 1417 | "259": "adminemail", 1418 | "261": "tracelog", 1419 | "262": "endoftrace", 1420 | "263": "tryagain", 1421 | "265": "n_local", 1422 | "266": "n_global", 1423 | "300": "none", 1424 | "301": "away", 1425 | "302": "userhost", 1426 | "303": "ison", 1427 | "305": "unaway", 1428 | "306": "nowaway", 1429 | "311": "whoisuser", 1430 | "312": "whoisserver", 1431 | "313": "whoisoperator", 1432 | "314": "whowasuser", 1433 | "315": "endofwho", 1434 | "316": "whoischanop", 1435 | "317": "whoisidle", 1436 | "318": "endofwhois", 1437 | "319": "whoischannels", 1438 | "321": "liststart", 1439 | "322": "list", 1440 | "323": "listend", 1441 | "324": "channelmodeis", 1442 | "329": "channelcreate", 1443 | "331": "notopic", 1444 | "332": "currenttopic", 1445 | "333": "topicinfo", 1446 | "341": "inviting", 1447 | "342": "summoning", 1448 | "346": "invitelist", 1449 | "347": "endofinvitelist", 1450 | "348": "exceptlist", 1451 | "349": "endofexceptlist", 1452 | "351": "version", 1453 | "352": "whoreply", 1454 | "353": "namreply", 1455 | "361": "killdone", 1456 | "362": "closing", 1457 | "363": "closeend", 1458 | "364": "links", 1459 | "365": "endoflinks", 1460 | "366": "endofnames", 1461 | "367": "banlist", 1462 | "368": "endofbanlist", 1463 | "369": "endofwhowas", 1464 | "371": "info", 1465 | "372": "motd", 1466 | "373": "infostart", 1467 | "374": "endofinfo", 1468 | "375": "motdstart", 1469 | "376": "endofmotd", 1470 | "377": "motd2", # 1997-10-16 -- tkil 1471 | "381": "youreoper", 1472 | "382": "rehashing", 1473 | "384": "myportis", 1474 | "391": "time", 1475 | "392": "usersstart", 1476 | "393": "users", 1477 | "394": "endofusers", 1478 | "395": "nousers", 1479 | "401": "nosuchnick", 1480 | "402": "nosuchserver", 1481 | "403": "nosuchchannel", 1482 | "404": "cannotsendtochan", 1483 | "405": "toomanychannels", 1484 | "406": "wasnosuchnick", 1485 | "407": "toomanytargets", 1486 | "409": "noorigin", 1487 | "411": "norecipient", 1488 | "412": "notexttosend", 1489 | "413": "notoplevel", 1490 | "414": "wildtoplevel", 1491 | "421": "unknowncommand", 1492 | "422": "nomotd", 1493 | "423": "noadmininfo", 1494 | "424": "fileerror", 1495 | "431": "nonicknamegiven", 1496 | "432": "erroneusnickname", # Thiss iz how its speld in thee RFC. 1497 | "433": "nicknameinuse", 1498 | "436": "nickcollision", 1499 | "437": "unavailresource", # "Nick temporally unavailable" 1500 | "441": "usernotinchannel", 1501 | "442": "notonchannel", 1502 | "443": "useronchannel", 1503 | "444": "nologin", 1504 | "445": "summondisabled", 1505 | "446": "usersdisabled", 1506 | "451": "notregistered", 1507 | "461": "needmoreparams", 1508 | "462": "alreadyregistered", 1509 | "463": "nopermforhost", 1510 | "464": "passwdmismatch", 1511 | "465": "yourebannedcreep", # I love this one... 1512 | "466": "youwillbebanned", 1513 | "467": "keyset", 1514 | "471": "channelisfull", 1515 | "472": "unknownmode", 1516 | "473": "inviteonlychan", 1517 | "474": "bannedfromchan", 1518 | "475": "badchannelkey", 1519 | "476": "badchanmask", 1520 | "477": "nochanmodes", # "Channel doesn't support modes" 1521 | "478": "banlistfull", 1522 | "481": "noprivileges", 1523 | "482": "chanoprivsneeded", 1524 | "483": "cantkillserver", 1525 | "484": "restricted", # Connection is restricted 1526 | "485": "uniqopprivsneeded", 1527 | "491": "nooperhost", 1528 | "492": "noservicehost", 1529 | "501": "umodeunknownflag", 1530 | "502": "usersdontmatch", 1531 | } 1532 | 1533 | generated_events = [ 1534 | # Generated events 1535 | "dcc_connect", 1536 | "dcc_disconnect", 1537 | "dccmsg", 1538 | "disconnect", 1539 | "ctcp", 1540 | "ctcpreply", 1541 | ] 1542 | 1543 | protocol_events = [ 1544 | # IRC protocol events 1545 | "error", 1546 | "join", 1547 | "kick", 1548 | "mode", 1549 | "part", 1550 | "ping", 1551 | "privmsg", 1552 | "privnotice", 1553 | "pubmsg", 1554 | "pubnotice", 1555 | "quit", 1556 | "invite", 1557 | "pong", 1558 | ] 1559 | 1560 | all_events = generated_events + protocol_events + numeric_events.values() 1561 | -------------------------------------------------------------------------------- /bot/mysql_config.txt.example: -------------------------------------------------------------------------------- 1 | server: mysql.sample.net 2 | port: 3306 3 | database: pierc 4 | user: pierc_user 5 | password: pierc_password 6 | -------------------------------------------------------------------------------- /bot/pierc.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # 3 | 4 | #libs 5 | from ircbot import SingleServerIRCBot 6 | from irclib import nm_to_n, nm_to_h, irc_lower, ip_numstr_to_quad, ip_quad_to_numstr 7 | import irclib 8 | import sys 9 | import re 10 | import time 11 | import datetime 12 | 13 | #mine 14 | import pierc_db 15 | import config 16 | 17 | 18 | # Configuration 19 | 20 | class Logger(irclib.SimpleIRCClient): 21 | 22 | def __init__(self, server, port, channel, nick, password, username, ircname, localaddress, localport, ssl, ipv6, 23 | mysql_server, mysql_port, mysql_database, mysql_user, mysql_password): 24 | 25 | 26 | irclib.SimpleIRCClient.__init__(self) 27 | 28 | #IRC details 29 | self.server = server 30 | self.port = port 31 | self.target = channel 32 | self.channel = channel 33 | self.nick = nick 34 | self.password = password 35 | self.username = username 36 | self.ircname = ircname 37 | self.localaddress = localaddress 38 | self.localport = localport 39 | self.ssl = ssl 40 | self.ipv6 = ipv6 41 | 42 | #MySQL details 43 | self.mysql_server = mysql_server 44 | self.mysql_port = mysql_port 45 | self.mysql_database = mysql_database 46 | self.mysql_user = mysql_user 47 | self.mysql_password = mysql_password 48 | 49 | #Regexes 50 | self.nick_reg = re.compile("^" + nick + "[:,](?iu)") 51 | 52 | #Message Cache 53 | self.message_cache = [] #messages are stored here before getting pushed to the db 54 | 55 | #Disconnect Countdown 56 | self.disconnect_countdown = 5 57 | 58 | self.last_ping = 0 59 | self.ircobj.delayed_commands.append( (time.time()+5, self._no_ping, [] ) ) 60 | 61 | self.connect(self.server, self.port, self.nick, self.password, self.username, self.ircname, self.localaddress, self.localport, self.ssl, self.ipv6) 62 | 63 | def _no_ping(self): 64 | if self.last_ping >= 1200: 65 | raise irclib.ServerNotConnectedError 66 | else: 67 | self.last_ping += 10 68 | self.ircobj.delayed_commands.append( (time.time()+10, self._no_ping, [] ) ) 69 | 70 | 71 | def _dispatcher(self, c, e): 72 | # This determines how a new event is handled. 73 | if(e.eventtype() == "topic" or 74 | e.eventtype() == "part" or 75 | e.eventtype() == "join" or 76 | e.eventtype() == "action" or 77 | e.eventtype() == "quit" or 78 | e.eventtype() == "nick" or 79 | e.eventtype() == "pubmsg"): 80 | try: 81 | source = e.source().split("!")[0] 82 | 83 | # Try to parse the channel name 84 | try: 85 | channel = e.target()[1:] 86 | except TypeError: 87 | channel = "undefined" 88 | 89 | except IndexError: 90 | source = "" 91 | try: 92 | text = e.arguments()[0] 93 | except IndexError: 94 | text = "" 95 | 96 | # Print message to stdout 97 | print str(datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + " ("+ e.eventtype() +") [#" + channel + "] <" + source + "> " + text 98 | 99 | # Prepare a message for the buffer 100 | message_dict = {"channel":channel, 101 | "name": source, 102 | "message": text, 103 | "type": e.eventtype(), 104 | "time": str(datetime.datetime.utcnow()) } 105 | 106 | if e.eventtype() == "nick": 107 | message_dict["message"] = e.target() 108 | 109 | # Most of the events are pushed to the buffer. 110 | self.message_cache.append( message_dict ) 111 | 112 | m = "on_" + e.eventtype() 113 | if hasattr(self, m): 114 | getattr(self, m)(c, e) 115 | 116 | def on_nicknameinuse(self, c, e): 117 | c.nick(c.get_nickname() + "_") 118 | 119 | def on_welcome(self, connection, event): 120 | if irclib.is_channel(self.target): 121 | connection.join(self.target) 122 | 123 | def on_disconnect(self, connection, event): 124 | self.on_ping(connection, event) 125 | connection.disconnect() 126 | raise irclib.ServerNotConnectedError 127 | 128 | def on_ping(self, connection, event): 129 | self.last_ping = 0 130 | try: 131 | db = pierc_db.Pierc_DB( self.mysql_server, 132 | self.mysql_port, 133 | self.mysql_database, 134 | self.mysql_user, 135 | self.mysql_password) 136 | for message in self.message_cache: 137 | db.insert_line(message["channel"], message["name"], message["time"], message["message"], message["type"] ) 138 | 139 | db.commit() 140 | if self.disconnect_countdown < 5: 141 | self.disconnect_countdown = self.disconnect_countdown + 1 142 | 143 | del db 144 | # clear the cache 145 | self.message_cache = [] 146 | 147 | except Exception, e: 148 | print "Database Commit Failed! Let's wait a bit!" 149 | print e 150 | if self.disconnect_countdown <= 0: 151 | sys.exit( 0 ) 152 | connection.privmsg(self.channel, "Database connection lost! " + str(self.disconnect_countdown) + " retries until I give up entirely!" ) 153 | self.disconnect_countdown = self.disconnect_countdown - 1 154 | 155 | 156 | def on_pubmsg(self, connection, event): 157 | text = event.arguments()[0] 158 | 159 | # If you talk to the bot, this is how he responds. 160 | if self.nick_reg.search(text): 161 | if text.split(" ")[1] and text.split(" ")[1] == "quit": 162 | connection.privmsg(self.channel, "Goodbye.") 163 | self.on_ping( connection, event ) 164 | sys.exit( 0 ) 165 | 166 | if text.split(" ")[1] and text.split(" ")[1] == "ping": 167 | self.on_ping(connection, event) 168 | return 169 | 170 | def main(): 171 | mysql_settings = config.config("mysql_config.txt") 172 | irc_settings = config.config("irc_config.txt") 173 | c = Logger( 174 | irc_settings["server"], 175 | int(irc_settings["port"]), 176 | irc_settings["channel"], 177 | irc_settings["nick"], 178 | irc_settings.get("password",None), 179 | irc_settings.get("username",None), 180 | irc_settings.get("ircname",None), 181 | irc_settings.get("localaddress",""), 182 | int(irc_settings.get("localport",0)), 183 | bool(irc_settings.get("ssl",False)), 184 | bool(irc_settings.get("ipv6",False)), 185 | 186 | mysql_settings["server"], 187 | int(mysql_settings["port"]), 188 | mysql_settings["database"], 189 | mysql_settings["user"], 190 | mysql_settings["password"] ) 191 | c.start() 192 | 193 | if __name__ == "__main__": 194 | irc_settings = config.config("irc_config.txt") 195 | reconnect_interval = irc_settings["reconnect"] 196 | while True: 197 | try: 198 | main() 199 | except irclib.ServerNotConnectedError: 200 | print "Server Not Connected! Let's try again!" 201 | time.sleep(float(reconnect_interval)) 202 | 203 | -------------------------------------------------------------------------------- /bot/pierc_db.py: -------------------------------------------------------------------------------- 1 | import MySQLdb 2 | import config 3 | import datetime 4 | 5 | class Pierc_DB: 6 | 7 | def __init__(self, server, port, database, user, password): 8 | self.conn = MySQLdb.connect ( host = server, 9 | port = port, 10 | user = user, 11 | passwd = password, 12 | db = database ) 13 | self.cursor = self.conn.cursor() 14 | 15 | 16 | def __del__(self): 17 | try: 18 | self.conn.close() 19 | except: 20 | return 21 | 22 | def create_table(self): 23 | self.cursor.execute( 24 | """ 25 | CREATE TABLE IF NOT EXISTS main 26 | ( 27 | id INT(12) NOT NULL AUTO_INCREMENT PRIMARY KEY, 28 | channel VARCHAR(16), 29 | name VARCHAR(16), 30 | time DATETIME, 31 | message TEXT, 32 | type VARCHAR(10), 33 | hidden CHAR(1) 34 | ) engine = InnoDB; 35 | 36 | """) 37 | 38 | def insert_line(self, channel, name, time, message, msgtype, hidden = "F"): 39 | 40 | """ 41 | Sample line: "sfucsss, danly, 12:33-09/11/2009, I love hats, normal, 0" 42 | """ 43 | query = "INSERT INTO main (channel, name, time, message, type, hidden) VALUES" + \ 44 | "(\""+self.conn.escape_string(channel)+ "\"," + \ 45 | "\""+self.conn.escape_string(name)+"\"," + \ 46 | "\""+time+"\"," + \ 47 | "\""+self.conn.escape_string(message)+"\"," + \ 48 | "\""+self.conn.escape_string(msgtype)+"\"," + \ 49 | "\""+self.conn.escape_string(hidden)+"\")" 50 | 51 | self.cursor.execute(query) 52 | 53 | def commit(self): 54 | self.conn.commit() 55 | 56 | if __name__ == "__main__": 57 | mysql_config = config.config("mysql_config.txt") 58 | db = Pierc_DB( mysql_config["server"], 59 | int(mysql_config["port"]), 60 | mysql_config["database"], 61 | mysql_config["user"], 62 | mysql_config["password"]) 63 | db.create_table() 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /web/config.php.example: -------------------------------------------------------------------------------- 1 | 34 | -------------------------------------------------------------------------------- /web/index.php: -------------------------------------------------------------------------------- 1 | get_channels() ; 11 | ?> 12 | 13 | 15 | 16 | 17 | 18 | IRC Archive 19 | " type="text/css" /> 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |
31 | 32 |
33 | 40 | 41 | 45 |
46 | Home 47 | 48 | More 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | "/> 61 |
62 | 63 |
64 | 65 | 78 | 79 |
80 | 81 |
    82 |
83 |
84 | 85 |
86 | 87 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /web/json.php: -------------------------------------------------------------------------------- 1 | get_search_results( $search, $n, $offset ); 55 | print json_encode( $lines ); 56 | return; 57 | } 58 | 59 | if ( $_GET['type'] == 'list_users' ) 60 | { 61 | $lines = $pdb->get_users( $channel ); 62 | print json_encode( $lines ); 63 | return; 64 | } 65 | 66 | # USER 67 | if ( $_GET['type'] == 'user' ) 68 | { 69 | $lines = $pdb->get_user( $channel, $_GET['user'], $n); 70 | print json_encode( $lines ); 71 | return; 72 | } 73 | 74 | 75 | # CONTEXT - results centered about an ID value 76 | if( $_GET['type'] == 'context' ) 77 | { 78 | // context type (before, middle, after) 79 | if( isset( $_GET['context']) ) 80 | { 81 | $context = $_GET['context']; 82 | } 83 | else 84 | { 85 | $context = "middle"; 86 | } 87 | 88 | // Used to retrieve a page before the existing page 89 | if( $context == "before" ) 90 | { 91 | $lines = $pdb->get_before( $channel, $id, $n ); 92 | } 93 | // Used to retrieve a page centered about an ID value 94 | if( $context == "middle" ) 95 | { 96 | $lines = $pdb->get_context( $id, $n ); 97 | } 98 | // Used to retrieve a page after the existing page 99 | if( $context == "after" ) 100 | { 101 | $lines = $pdb->get_after( $channel, $id, $n ); 102 | } 103 | print json_encode( $lines ); 104 | return; 105 | } 106 | 107 | // UPDATE - get all results that occur after $id 108 | if ( $_GET['type'] == 'update' ) 109 | { 110 | if( !isset( $id )) 111 | { 112 | print "Cannot return results without provided id parameter."; 113 | return; 114 | } 115 | $lines = $pdb->get_lines_between_now_and_id( $channel, $id ) ; 116 | print json_encode( $lines ); 117 | return; 118 | } 119 | 120 | // TAG- get all results that match a blah: blahblah tag. 121 | if ( $_GET['type'] == 'tag' && isset( $_GET['tag']) ) 122 | { 123 | $lines = $pdb->get_tag( $channel, $_GET['tag'], $n ) ; 124 | print json_encode( $lines ); 125 | return; 126 | } 127 | 128 | // LAST SEEN - when was the last time name n posted? 129 | if( $_GET['type'] == 'lastseen' && isset( $_GET['user'] ) ) 130 | { 131 | $lines = $pdb->get_lastseen( $channel, $_GET['user'] ); 132 | print json_encode( $lines ); 133 | return; 134 | } 135 | 136 | // DEFAULT - get the last $n results 137 | 138 | $lines = $pdb->get_last_n_lines( $channel, $n ); 139 | print json_encode( $lines ); 140 | return; 141 | 142 | 143 | -------------------------------------------------------------------------------- /web/pierc.js: -------------------------------------------------------------------------------- 1 | // The main site is powered, rather unnecessarily, entirely by AJAX calls. 2 | 3 | // Constants 4 | var irc_refresh_in_seconds = 60; // How often we refresh the page 5 | var page_hash_check_in_seconds = 1; // How often we check the page hash for changes. 6 | 7 | // Globals (the horror) 8 | var last_id = 0; // The ID of the comment at the very bottom of the page 9 | var first_id = 0; // The ID of the comment at the very top of the page 10 | var refresh_on = true; // Whether or not the 'refresh' action is currently operating 11 | var hash = "#"; // The most recent hash value in the URL ("#search-poop") 12 | var channelselect = $('#channellist').val(); // Selected channel in dropdown 13 | 14 | var current_offset = 50; // The current search offset; 15 | var most_recent_search = ""; //The last thing searched for. 16 | 17 | function everything_has_failed( xhr ){ 18 | clear(); 19 | $("#horrible_error").show(); 20 | $("#error").html( xhr.responseText ); 21 | } 22 | 23 | // On Load 24 | $(function() { 25 | $("#channellist").change(function(){ 26 | clear(); 27 | refresh_on = true; 28 | home(); 29 | }); 30 | 31 | $("#join-quit-toggle").click(function(){ 32 | hideJoinQuit(); 33 | }); 34 | 35 | $("#inline-media-toggle").click(function(){ 36 | toggleInlineMedia(); 37 | }); 38 | 39 | $("#searchoptions").hide(); 40 | 41 | // check for new content every N seconds 42 | setInterval("refresh()", irc_refresh_in_seconds * 1000); 43 | setInterval("hashnav_check()", page_hash_check_in_seconds * 1000); 44 | 45 | if( ! hashnav() ) { home(); } 46 | 47 | //Toolbar setup 48 | $("#load_more").click( load_more_search_results ); 49 | $("#search").submit( search ); 50 | $("#home").click( home ); 51 | $("#prev").click( page_up ); 52 | $("#next").click( page_down ); 53 | $("#events").click( events ); 54 | $("#important").click( important ); 55 | 56 | // Live Search 57 | // This should prevent the sending of too many queries (i.e. one per letter typed). 58 | 59 | // Reference to the pending search query. 60 | var liveSearchTimer; 61 | // Delay after typing a key, before the search request is sent. 62 | var liveSearchTimerInterval = 200; 63 | 64 | // When a key is typed 65 | $("#searchbox").keyup(function(){ 66 | // Cancel the prending query 67 | clearTimeout(liveSearchTimer); 68 | // Create a new delayed one 69 | liveSearchTimer = setTimeout(liveSearchSubmit, liveSearchTimerInterval); 70 | }); 71 | 72 | function liveSearchSubmit () { 73 | if ($('#searchbox').val()=="") 74 | home(); 75 | else 76 | search(); 77 | } 78 | 79 | }); 80 | 81 | // Hide all join/quit messages 82 | function hideJoinQuit() { 83 | if ($('#join-quit-toggle').is(':checked')) { 84 | $('#irc .join, #irc .quit').hide(); 85 | } else { 86 | $('#irc .join, #irc .quit').show(); 87 | } 88 | } 89 | 90 | function toggleInlineMedia() { 91 | if ($('#inline-media-toggle').is(':checked')) { 92 | $('#irc .inline-image').show(); 93 | } else { 94 | $('#irc .inline-image').hide(); 95 | } 96 | } 97 | 98 | // Display embedable media inline 99 | // Only images (jpg,gif,png) and youtube links right now 100 | function displayInlineMedia() { 101 | $('.message a:not(.inline-image-link)').each(function() { 102 | link = $(this); 103 | url = link.attr('href'); 104 | var testRegex = /^https?:\/\/(?:[a-z\-]+\.)+[a-z]{2,6}(?:\/[^\/#?]+)+\.(?:jpe?g|gif|png)$/; 105 | if (testRegex.test(url)) { 106 | link.append('').addClass('inline-image-link').attr('target','_blank'); 107 | // Re-hide media if necessary 108 | toggleInlineMedia(); 109 | } 110 | }); 111 | 112 | 113 | var vidWidth = 425; 114 | var vidHeight = 344; 115 | 116 | var obj = '' + 117 | '' + 118 | ' '; 124 | 125 | $('.message a:not(.inline-youtube-link):contains("youtube.com/watch")').each(function(){ 126 | var that = $(this); 127 | var vid = that.html().match(/(?:v=)([\w\-]+)/g); 128 | if (vid.length) { 129 | $.each(vid, function(){ 130 | that.append( obj.replace(/\[vid\]/g, this.replace('v=','')) ); 131 | }); 132 | } 133 | }); 134 | 135 | } 136 | 137 | //Direct channel links via ?channel=mychan 138 | $(document).ready(function() { 139 | var channel = getUrlVars()["channel"]; 140 | 141 | $('#channellist option').each(function(){ 142 | if (this.value === channel){ 143 | $('#channellist').val(channel); 144 | } 145 | }); 146 | }); 147 | 148 | function getUrlVars() { 149 | var vars = {}; 150 | var parts = window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m,key,value) { 151 | vars[key] = value; 152 | }); 153 | return vars; 154 | } 155 | 156 | // Navigate around the site based on the site hash. 157 | // This allows for use of the "Back" button, as well as reusable URL structure. 158 | function hashnav() 159 | { 160 | hash = window.location.hash 161 | if( hash.substring(1, 7) == "search") 162 | { 163 | var searchterm = hash.substring( 8, hash.length ); 164 | $("#searchbox").attr({"value":decodeURIComponent(searchterm)}); 165 | search(); 166 | return true; 167 | } 168 | if( hash.substring(1, 4) == "tag") 169 | { 170 | var tagname = hash.substring( 5, hash.length ); 171 | tag( tagname ); 172 | return true; 173 | } 174 | else if (hash.substring(1, 3) == "id") 175 | { 176 | var id = hash.substring( 4, hash.length ); 177 | context(id); 178 | $("#toolbar_inner").prepend("« Back to channellist."); 179 | $('.backbutton').click(function(){ 180 | $(this).hide(); 181 | home(); 182 | }); 183 | return true; 184 | } 185 | else if (hash.substring(1, 5) == "home") 186 | { 187 | home(); 188 | return true; 189 | } 190 | else if (hash.substring(1, 8) == "loading") 191 | { 192 | return true; 193 | } 194 | return false; 195 | } 196 | 197 | // Check the current hash against the hash in the url. If they're different, perform hashnav. 198 | // Note: this happens frequently 199 | function hashnav_check() 200 | { 201 | if( hash == window.location.hash ) 202 | { 203 | return false; 204 | } 205 | else 206 | { 207 | return hashnav(); 208 | } 209 | } 210 | 211 | 212 | // Populate the page with the last 50 things said 213 | // This is the default 'home' activity for the page. 214 | function home() 215 | { 216 | var channelselect = $('#channellist').val(); 217 | 218 | clear(); 219 | refresh_on = true; 220 | $('#irc').removeClass("searchresult"); 221 | $("#options").show(); 222 | $("#searchoptions").hide(); 223 | // Ajax call to populate table 224 | loading() 225 | 226 | //alert($('#channellist').val()); 227 | 228 | $.ajax({ 229 | url: "json.php", 230 | data: { channel: channelselect}, 231 | dataType: "json", 232 | success: function(data){ 233 | first_id = data[0].id; 234 | $(data).each( function(i, item) { 235 | $(irc_render(item)).appendTo("#irc"); 236 | last_id = item.id; 237 | }); 238 | done_loading(); 239 | window.location.hash = "home"; 240 | hash = window.location.hash; 241 | hideJoinQuit(); 242 | displayInlineMedia(); 243 | scroll_to_bottom(); 244 | }, 245 | error: everything_has_failed 246 | }); 247 | 248 | 249 | } 250 | 251 | // Check if anything 'new' has been said in the past minute or so. 252 | function refresh() 253 | { 254 | // Selected value in dropdown 255 | var channelselect = $('#channellist').val(); 256 | 257 | if( !refresh_on ) { return; } 258 | loading(); 259 | $.getJSON("json.php?channel=" + channelselect, { 'type':'update', 'id': last_id }, 260 | function(data){ 261 | $(data).each( function(i, item) { 262 | try 263 | { 264 | $(irc_render(item)).appendTo("#irc"); last_id = item.id; 265 | } 266 | catch(err) 267 | { 268 | // do nuffins 269 | } 270 | }); 271 | done_loading(); 272 | }).error(everything_has_failed); 273 | } 274 | 275 | // Perform a search for the given search value. Populate the page with the results. 276 | function search_for( searchvalue ) 277 | { 278 | // If the previous search was for the same term, don't search again 279 | if(searchvalue == most_recent_search) 280 | { 281 | return; 282 | } 283 | 284 | current_offset = 50; 285 | most_recent_search = searchvalue; 286 | window.location.hash = "search-"+searchvalue; 287 | hash = window.location.hash; 288 | 289 | //Before 290 | refresh_on = false; 291 | $("#options").hide(); 292 | $("#searchoptions").show(); 293 | 294 | clear(); 295 | loading(); 296 | 297 | // Ajax call to get search results 298 | $.getJSON("json.php", {'search':searchvalue}, 299 | function(data){ 300 | if( data.length < 50 ) { $("#searchoptions").hide(); } 301 | $(data).each( function(i, item) { 302 | try{ 303 | $(irc_render(item)).appendTo("#irc"); 304 | } 305 | catch(err){ 306 | // do nuffins 307 | } 308 | } ); 309 | $("#irc").addClass("searchresult"); 310 | done_loading(); 311 | 312 | highlight( searchvalue ); 313 | hideJoinQuit(); 314 | displayInlineMedia(); 315 | scroll_to_bottom(); 316 | }).error(everything_has_failed); 317 | } 318 | 319 | // Perform a search for the search value in the #searchbox element. 320 | function search() 321 | { 322 | var searchvalue = escape($("#searchbox").attr("value")); 323 | search_for( searchvalue ); 324 | return false; // This should prevent the search form from submitting 325 | } 326 | 327 | // Switch to a specific IRC message, centered about its ID. 328 | function context(id) 329 | { 330 | // Before 331 | clear(); 332 | refresh_on = false; 333 | $("#options").show(); 334 | $("#searchoptions").hide(); 335 | 336 | $('#irc').removeClass("searchresult"); 337 | loading(); 338 | 339 | // Ajax call to get 'context' (find the comment at id 'id' and 'n' spaces around it). 340 | $.getJSON("json.php", {'type':'context', 'id':id }, 341 | function(data){ 342 | first_id = data[0].id; 343 | $(data).each( function(i, item) { 344 | $(irc_render(item)).appendTo("#irc"); 345 | last_id = item.id; 346 | }); 347 | 348 | // After 349 | 350 | $('#irc-'+id).addClass('highlighted' ) 351 | done_loading(); 352 | window.location.hash = "id-"+id; 353 | hash = window.location.hash; 354 | hideJoinQuit(); 355 | displayInlineMedia(); 356 | scroll_to_id( id ); 357 | }).error(everything_has_failed); 358 | 359 | } 360 | 361 | // Add n more search results 362 | function load_more_search_results() 363 | { 364 | if( current_offset < 40 ){ current_offset = 40 }; 365 | 366 | // Ajax call 367 | loading(); 368 | $.getJSON("json.php", {'type':'search', 'n':40, 'offset':current_offset, 'search':most_recent_search }, 369 | function(data){ 370 | $("#irc li:first-child").addClass("pagebreak"); 371 | var id = 0; 372 | if( data.length < 40 ) { $("#searchoptions").hide(); } 373 | else{ $("#searchoptions").show(); } 374 | data.reverse(); 375 | $(data).each( function( i, item) { 376 | $(irc_render(item)).prependTo("#irc"); 377 | id = item.id; 378 | }); 379 | scroll_to_id( id ); 380 | done_loading(); 381 | current_offset += 40; 382 | highlight( most_recent_search ); 383 | }).error(everything_has_failed); 384 | return false; 385 | } 386 | 387 | // Add a page of IRC chat _before_ the current page of IRC chat 388 | function page_up() 389 | { 390 | // Selected value in dropdown 391 | var channelselect = $('#channellist').val(); 392 | 393 | // Ajax call to populate table 394 | loading(); 395 | $.getJSON("json.php?channel=" + channelselect, {'type':'context', 'id':first_id, 'n':40, 'context':'before' }, 396 | function(data){ 397 | $("#irc li:first-child").addClass("pagebreak"); 398 | $(data).each( function(i, item) { 399 | $(irc_render(item)).prependTo("#irc"); 400 | first_id = item.id; 401 | }); 402 | hideJoinQuit(); 403 | displayInlineMedia(); 404 | scroll_to_id( first_id ); 405 | done_loading(); 406 | }).error(everything_has_failed); 407 | return false; 408 | } 409 | 410 | // Add a page of IRC chat _after_ the current page of IRC chat 411 | function page_down() 412 | { 413 | // Selected value in dropdown 414 | var channelselect = $('#channellist').val(); 415 | 416 | loading(); 417 | 418 | $.getJSON("json.php?channel=" + channelselect, {'type':'context', 'id':last_id, 'n':40, 'context':'after' }, 419 | function(data){ 420 | $("#irc li:last-child").addClass("pagebreak"); 421 | $(data).each( function(i, item) { 422 | $(irc_render(item)).appendTo("#irc"); 423 | last_id = item.id; 424 | }); 425 | hideJoinQuit(); 426 | displayInlineMedia(); 427 | scroll_to_bottom(); 428 | done_loading(); 429 | }).error(everything_has_failed); 430 | return false; 431 | } 432 | 433 | function events ( ) 434 | { 435 | tag( "event" ); 436 | return false; 437 | } 438 | 439 | function important( ) 440 | { 441 | tag( "important" ); 442 | return false; 443 | } 444 | 445 | // Load a tag 446 | function tag( tagname ) 447 | { 448 | window.location.hash = "tag-"+tagname; 449 | hash = window.location.hash; 450 | 451 | clear(); 452 | refresh_on = false; 453 | $("#options").hide(); 454 | $("#searchoptions").hide(); 455 | $('#irc').removeClass("searchresult"); 456 | 457 | loading(); 458 | $.getJSON("json.php", {'type':'tag', 'tag':tagname, 'n':15 }, 459 | function(data){ 460 | $(data).each( function(i, item) { 461 | $(irc_render(item)).appendTo("#irc"); 462 | }); 463 | 464 | done_loading(); 465 | hideJoinQuit(); 466 | displayInlineMedia(); 467 | scroll_to_bottom(); 468 | }).error(everything_has_failed); 469 | return false; 470 | } 471 | 472 | 473 | //----------------------------------------------- 474 | 475 | // Convert a single IRC message into a table row 476 | function irc_render( item ) 477 | { 478 | if ( item.hidden != "F" ) { return "";} 479 | 480 | var message_tag = /^\s*([A-Za-z]*):/.exec(item.message); 481 | var tag_tag = ""; 482 | if (message_tag) 483 | { 484 | message_tag = message_tag[1].toLowerCase(); 485 | tag_tag = "tag"; 486 | } 487 | else 488 | { 489 | message_tag = ""; 490 | } 491 | 492 | var construct_string = "
  • "; 493 | construct_string += "" + html_escape(item.name) + " "; 494 | 495 | if (item.type == "join") { construct_string += "has joined #" + html_escape(item.channel); } 496 | else if (item.type == "part") { construct_string += "has left #" + html_escape(item.channel) + " -- "; } 497 | else if (item.type == "quit") { construct_string += "has quit -- "; } 498 | else if (item.type == "topic") { construct_string += "has changed the topic:
    "; } 499 | else if (item.type == "nick") { construct_string += " is now known as ";} 500 | else if (item.type == "action") { } 501 | 502 | construct_string += link_replace(spanify(html_escape(item.message))) + "
    "; 503 | var message_date = datetimeify(item.time); 504 | var pretty_date = human_date(message_date); 505 | construct_string += "" + pretty_date + ""; 506 | return $(construct_string); 507 | } 508 | 509 | // Make EVERY WORD A SPAN TAG moo hoo ha ha ha 510 | function spanify( string ) 511 | { 512 | if (!string){ return string; } 513 | var split = $(string.split(" ")); 514 | var join = [] 515 | split.each( function(i, thing) 516 | { 517 | if( thing[0] == 'h' && thing[1] == 't' ){ join.push( thing ); } 518 | else{ 519 | join.push( ""+thing+"" ); 520 | } 521 | }); 522 | return join.join(" "); 523 | } 524 | 525 | function highlight( words ) 526 | { 527 | var split = $(words.split(/[ (%2520)]/)); 528 | split.each( function( i, word) 529 | { 530 | var random = Math.floor((Math.random()*10)+1) 531 | if( word.length > 3 ){ 532 | $("span[class*=spanify-"+word.toLowerCase().replace(/\W/g, '')+"]").addClass("search-highlight"); 533 | $("span[class*=spanify-"+word.toLowerCase().replace(/\W/g, '')+"]").addClass("highlight-"+random); 534 | } 535 | }); 536 | 537 | } 538 | 539 | // Make links clickable, and images images 540 | function link_replace( string ) 541 | { 542 | if(!string){ return string; } 543 | var links = string.match( /(https*://\S*)/g ); 544 | if (links) 545 | { 546 | for( var i = 0; i < links.length; i++ ) 547 | { 548 | var replacement = links[i] 549 | if (replacement.length > 100) 550 | { 551 | replacement = links[i].substring(0,100) + "..."; 552 | } 553 | 554 | string = string.replace( links[i], ""+replacement+""); 555 | } 556 | } 557 | return string; 558 | } 559 | 560 | // Show the 'loading' widget. 561 | function loading() 562 | { 563 | $("#loading").fadeIn('fast'); 564 | document.body.style.cursor = 'wait'; 565 | } 566 | 567 | function done_loading() 568 | { 569 | $('#loading').fadeOut('slow'); 570 | document.body.style.cursor = 'default'; 571 | } 572 | 573 | // Clears the IRC area. 574 | function clear() 575 | { 576 | $("#irc").html(""); 577 | } 578 | 579 | // Scroll to the bottom of the page 580 | function scroll_to_bottom() 581 | { 582 | setTimeout( function() { 583 | $('html, body').animate({scrollTop: $(document).height()}, 200); 584 | }, 100) 585 | } 586 | 587 | // Attempt to scroll to the id of the item specified. 588 | function scroll_to_id(id) 589 | { 590 | setTimeout( function() { 591 | $target = $("#irc-"+id); 592 | var targetOffset = $target.offset().top - 100; 593 | $('html,body').animate({scrollTop: targetOffset}, 200); 594 | }, 100) 595 | } 596 | 597 | // MySQL date string (2009-06-13 18:10:59 / yyyy-mm-dd hh:mm:ss ) 598 | function datetimeify( mysql_date_string ) 599 | { 600 | var dt = new Date(); 601 | var space_split = mysql_date_string.split(" "); 602 | var d = space_split[0]; 603 | var t = space_split[1]; 604 | var date_split = d.split("-"); 605 | dt.setFullYear( date_split[0] ); 606 | dt.setMonth( date_split[1]-1 ); 607 | dt.setDate( date_split[2] ); 608 | var time_split = t.split(":"); 609 | dt.setHours( time_split[0] ); 610 | dt.setMinutes( time_split[1] ); 611 | dt.setSeconds( time_split[2] ); 612 | return dt; 613 | } 614 | 615 | // human_date - tries to construct a human-readable date 616 | function human_date( date ) 617 | { 618 | var td = new Date(); 619 | var dt = date.toDateString() 620 | if( date.getDate() == td.getDate() && 621 | date.getMonth() == td.getMonth() && 622 | date.getYear() == td.getYear() ) { dt = "Today"; } 623 | 624 | var yesterday = new Date(); 625 | yesterday.setDate( td.getDate() - 1 ); 626 | 627 | if( date.getDate() == yesterday.getDate() && 628 | date.getMonth() == yesterday.getMonth() && 629 | date.getYear() == yesterday.getYear() ) { dt = "Yesterday";} 630 | 631 | if( hours == 0 && minutes == 0 ) { return dt + " - Midnight"; } 632 | else if( hours == 12 && minutes == 0 ){ return dt + " - Noon"; } 633 | else 634 | { 635 | var ampm = "AM"; 636 | var hours = date.getHours(); 637 | if(hours > 11){ hours = hours - 12; ampm = "PM"; } 638 | 639 | var minutes = date.getMinutes(); 640 | if( minutes < 10 ){ minutes = "0" + minutes; } 641 | 642 | // I find it strange, but in a 12-hour clock, '12' acts as 0. 643 | if( hours == 0 ) { hours = 12; } 644 | 645 | return dt + " - " + hours + ":" + minutes + " " + ampm; 646 | } 647 | } 648 | 649 | // Shouldn't this be part of javascript somewhere? 650 | // Nevetheless, escapes HTML control characters. 651 | function html_escape( string ) 652 | { 653 | if( !string ){ return string; } 654 | string = string.replace(/&/g, '&'); 655 | string = string.replace(//g, '>'); 657 | string = string.replace(/\"/g, '"' ); 658 | string = string.replace(/'/g, ''' ); 659 | string = string.replace(/\//g, '/'); 660 | return string; 661 | } 662 | -------------------------------------------------------------------------------- /web/pierc_db.php: -------------------------------------------------------------------------------- 1 | _conn = mysqli_connect( $server.$port, $user, $password ); 12 | $this->_conn = new mysqli( $server.$port, $user, $password , $database); 13 | if (!$this->_conn){ die ("Could not connect: " + mysqli_error() ); } 14 | 15 | // Verify that we received a proper time zone, otherwise fall back to default 16 | $allZones = DateTimeZone::listIdentifiers(); 17 | if(!in_array($timezone, $allZones)) { 18 | $timezone = "America/Vancouver"; 19 | } 20 | $this->timezone = new DateTimeZone($timezone); 21 | } 22 | 23 | public function __destruct( ) 24 | { 25 | mysqli_close( $this->_conn ); 26 | } 27 | 28 | } 29 | 30 | class pierc_db extends db_class 31 | { 32 | 33 | protected function hashinate( $result ) 34 | { 35 | $lines = array(); 36 | $counter = 0; 37 | while( $row = mysqli_fetch_assoc($result) ) 38 | { 39 | if( isset( $row['time'] ) ) 40 | { 41 | date_default_timezone_set('UTC'); 42 | $dt = date_create( $row['time']); 43 | $dt->setTimezone( $this->timezone ); 44 | $row['time'] = $dt->format("Y-m-d H:i:s"); 45 | } 46 | $lines[$counter] = $row; 47 | $counter++; 48 | } 49 | return $lines; 50 | } 51 | 52 | public function get_last_n_lines( $channel, $n ) 53 | { 54 | $channel = mysqli_real_escape_string( $this->_conn, $channel ); 55 | $n = (int)$n; 56 | $query = " 57 | SELECT id, channel, name, time, message, type, hidden FROM main WHERE channel = '$channel' ORDER BY id DESC LIMIT $n;"; 58 | 59 | $results = mysqli_query( $this->_conn, $query ); 60 | if (!$results){ print mysqli_error(); return false; } 61 | if( mysqli_num_rows($results) == 0 ) { return false; } 62 | 63 | return array_reverse($this->hashinate($results)); 64 | } 65 | 66 | public function get_before( $channel, $id, $n ) 67 | { 68 | $channel = mysqli_real_escape_string( $this->_conn, $channel ); 69 | $n = (int)$n; 70 | $id = (int)$id; 71 | $query = " 72 | SELECT id, channel, name, time, message, type, hidden FROM main WHERE channel = '$channel' AND id < $id ORDER BY id DESC LIMIT $n;"; 73 | 74 | $results = mysqli_query( $this->_conn, $query); 75 | if (!$results){ print mysqli_error(); return false; } 76 | if( mysqli_num_rows($results) == 0 ) { return false; } 77 | 78 | return $this->hashinate($results); 79 | } 80 | 81 | public function get_after( $channel, $id, $n ) 82 | { 83 | $channel = mysqli_real_escape_string( $this->_conn, $channel ); 84 | $n = (int)$n; 85 | $id = (int)$id; 86 | $query = " 87 | SELECT id, channel, name, time, message, type, hidden FROM main WHERE channel = '$channel' AND id > $id ORDER BY time ASC, id DESC LIMIT $n;"; 88 | 89 | $results = mysqli_query( $this->_conn, $query); 90 | if (!$results){ print mysqli_error(); return false; } 91 | if( mysqli_num_rows($results) == 0 ) { return false; } 92 | 93 | return $this->hashinate($results); 94 | } 95 | 96 | public function get_lines_between_now_and_id( $channel, $id) 97 | { 98 | $channel = mysqli_real_escape_string( $this->_conn, $channel ); 99 | $id = (int)$id; 100 | $query = " 101 | SELECT id, channel, name, time, message, type, hidden FROM main WHERE channel = '$channel' AND id > $id ORDER BY id DESC LIMIT 500"; 102 | 103 | $results = mysqli_query( $this->_conn, $query ); 104 | if (!$results){ print mysqli_error(); return false; } 105 | if( mysqli_num_rows($results) == 0 ) { return false; } 106 | 107 | return array_reverse($this->hashinate($results)); 108 | } 109 | 110 | // Returns the number of records in 'channel' with an ID below $id 111 | public function get_count( $channel, $id) 112 | { 113 | $channel = mysqli_real_escape_string( $this->_conn, $channel ); 114 | $id = (int)$id; 115 | $query = " 116 | SELECT COUNT(*) as count FROM main 117 | WHERE channel = '$channel' 118 | AND id < $id;"; 119 | 120 | $results = mysqli_query( $this->_conn, $query); 121 | if (!$results){ print mysqli_error(); return false; } 122 | if( mysqli_num_rows($results) == 0 ) { return false; } 123 | 124 | $res = $this->hashinate($results); 125 | $count = $res[0]["count"]; 126 | if ( $count < 0 ) 127 | { 128 | return 0; 129 | } 130 | return $count; 131 | } 132 | 133 | public function get_context( $id, $n) 134 | { 135 | // Let's imagine that we have 800,000 records, divided 136 | // between two different channels, #hurf and #durf. 137 | // we want to select the $n (50) records surrounding 138 | // id-678809 in #durf. So, first we count the number 139 | // of records in # durf that are below id-678809. 140 | // 141 | // Remember: OFFSET is the number of records that MySQL 142 | // will skip when you do a SELECT statement - 143 | // So "SELECT * FROM main LIMIT 50 OFFSET 150 will select 144 | // rows 150-200. 145 | // 146 | // If we used the $count as an $offset, we'd have a conversation 147 | // _starting_ with id-678809 - but we want to capture the 148 | // conversation _surrounding_ id-678809, so we subtract 149 | // $n (50)/2, or 25. 150 | 151 | 152 | $id = (int)$id; 153 | $n = (int)$n; 154 | 155 | $query = " 156 | SELECT channel 157 | FROM main 158 | WHERE id = $id ; 159 | "; 160 | 161 | $results = mysqli_query( $this->_conn, $query ); 162 | if (!$results){ print mysqli_error(); return false; } 163 | if( mysqli_num_rows($results) == 0 ) { return false; } 164 | 165 | while ($row = mysqli_fetch_assoc($results)) { 166 | $channel = $row['channel']; 167 | } 168 | 169 | $channel = mysqli_real_escape_string( $this->_conn, $channel ); 170 | 171 | $count = $this->get_count( $channel, $id ); 172 | 173 | $offset = $count - (int)($n/2); 174 | 175 | if( $offset < 0) 176 | { 177 | $offset = 0; 178 | } 179 | 180 | $query = " 181 | SELECT * 182 | FROM (SELECT * FROM main 183 | WHERE channel = '$channel' 184 | LIMIT $n OFFSET $offset) channel_table 185 | ORDER BY id DESC ; 186 | "; 187 | 188 | $results = mysqli_query( $this->_conn, $query ); 189 | if (!$results){ print mysqli_error(); return false; } 190 | if( mysqli_num_rows($results) == 0 ) { return false; } 191 | 192 | return array_reverse($this->hashinate($results)); 193 | } 194 | 195 | public function get_search_results( $search, $n, $offset=0 ) 196 | { 197 | $search = urldecode($search); 198 | $n = (int) $n; 199 | $offset = (int) $offset; 200 | 201 | $searchquery = " WHERE "; 202 | $searcharray = preg_split("/[ |]/", $search); 203 | foreach($searcharray as $searchterm ) 204 | { 205 | $searchquery .= "(message LIKE '%". 206 | mysqli_real_escape_string($this->_conn, $searchterm)."%' OR name LIKE '%". 207 | mysqli_real_escape_string($this->_conn, $searchterm)."%' ) AND"; 208 | } 209 | 210 | $n = (int)$n; 211 | $query = " 212 | SELECT id, channel, name, time, message, type, hidden 213 | FROM main 214 | $searchquery true ORDER BY id DESC LIMIT $n OFFSET $offset;"; 215 | 216 | $results = mysqli_query( $this->_conn, $query ); 217 | if (!$results){ print mysqli_error(); return false; } 218 | if( mysqli_num_rows($results) == 0 ) { return false; } 219 | 220 | $results = array_reverse($this->hashinate($results)); 221 | return $results; 222 | } 223 | 224 | public function get_tag( $channel, $tag, $n ) 225 | { 226 | $tag = mysqli_real_escape_string($this->_conn, $tag); 227 | $channel = mysqli_real_escape_string($this->_conn, $channel); 228 | $n = (int)$n; 229 | 230 | $query = " 231 | SELECT id, channel, name, time, message, type, hidden 232 | FROM main 233 | WHERE message LIKE '".$tag.":%' ORDER BY id DESC LIMIT $n;"; 234 | 235 | $results = mysqli_query( $this->_conn, $query ); 236 | if (!$results){ print mysqli_error(); return false; } 237 | if( mysqli_num_rows($results) == 0 ) { return false; } 238 | 239 | return array_reverse($this->hashinate($results)); 240 | } 241 | 242 | public function get_lastseen( $channel, $user ) 243 | { 244 | $user = mysqli_real_escape_string($this->_conn, $user); 245 | $channel = mysqli_real_escape_string($this->_conn, $channel); 246 | 247 | $query = " 248 | SELECT time 249 | FROM main 250 | WHERE name = '".$user."' ORDER BY id DESC LIMIT 1;"; 251 | 252 | $results = mysqli_query( $this->_conn, $query ); 253 | if (!$results){ print mysqli_error(); return false; } 254 | if( mysqli_num_rows($results) == 0 ) { return false; } 255 | 256 | return $this->hashinate($results); 257 | } 258 | 259 | public function get_user( $channel, $user, $n ) 260 | { 261 | $user = mysqli_real_escape_string($this->_conn, $user); 262 | $channel = mysqli_real_escape_string($this->_conn, $channel); 263 | $n = (int) $n; 264 | 265 | $query = " 266 | SELECT id, channel, name, time, message, type 267 | FROM main 268 | WHERE name = '".$user."' ORDER BY id DESC LIMIT ".$n.";"; 269 | 270 | $results = mysqli_query( $this->_conn, $query); 271 | if (!$results){ print mysqli_error(); return false; } 272 | if( mysqli_num_rows($results) == 0 ) { return false; } 273 | 274 | return $this->hashinate($results); 275 | } 276 | 277 | public function get_users( $channel ) 278 | { 279 | $query = " SELECT DISTINCT name FROM main; "; 280 | $results = mysqli_query( $this->_conn, $query ); 281 | 282 | if (!$results){ print mysqli_error(); return false; } 283 | if( mysqli_num_rows($results) == 0 ) { return false; } 284 | 285 | $lines = $this->hashinate($results); 286 | $users = array(); 287 | foreach($lines as $line) 288 | { 289 | $users[] = $line['name']; 290 | } 291 | return $users; 292 | } 293 | 294 | public function get_channels() 295 | { 296 | $query = " SELECT DISTINCT channel FROM main WHERE type <> \"nick\" AND channel <> \"undefined\";"; 297 | $results = mysqli_query( $this->_conn, $query ); 298 | 299 | if (!$results){ print mysqli_error(); return false; } 300 | if( mysqli_num_rows($results) == 0 ) { return false; } 301 | 302 | $lines = $this->hashinate($results); 303 | $channels = array(); 304 | foreach($lines as $line) 305 | { 306 | $channels[] = $line['channel']; 307 | } 308 | return $channels; 309 | } 310 | } 311 | ?> 312 | 313 | -------------------------------------------------------------------------------- /web/theme/rnarian/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cube-drone/pierc/9cdcb19bb41878782a9da1d548f5c8d5d9245127/web/theme/rnarian/ajax-loader.gif -------------------------------------------------------------------------------- /web/theme/rnarian/style.css: -------------------------------------------------------------------------------- 1 | body{ margin: 0; padding: 0; font-family: "Helvetica Neue", sans-serif; } 2 | 3 | 4 | /* important: this is dumb. */ 5 | .search-highlight{ 6 | font-family: "Helvetica Neue", sans-serif; 7 | background-color: rgba(255, 255, 0, .8); 8 | color: rgba(0,0,0,.6); 9 | border-radius: 3px; 10 | font-size: 13px; 11 | padding: 2px 5px;} 12 | .highlight-1{ background-color: rgba(255, 255, 0, 0.4); } 13 | .highlight-2{ background-color: rgba(255, 255, 0, 0.45); } 14 | .highlight-3{ background-color: rgba(255, 255, 0, 0.5); } 15 | .highlight-4{ background-color: rgba(255, 255, 0, 0.55); } 16 | .highlight-5{ background-color: rgba(255, 255, 0, 0.6); } 17 | .highlight-6{ background-color: rgba(255, 255, 0, 0.65); } 18 | .highlight-7{ background-color: rgba(255, 255, 0, 0.7); } 19 | .highlight-8{ background-color: rgba(255, 255, 0, 0.75); } 20 | .highlight-9{ background-color: rgba(255, 255, 0, 0.8); } 21 | .highlight-10{ background-color: rgba(255, 255, 0, 0.85); } 22 | 23 | #events { display: none; } 24 | #important{ display: none; } 25 | 26 | a { 27 | color: #3366FF; 28 | } 29 | 30 | #toolbar { 31 | position: fixed; 32 | z-index: 9999; 33 | top: 0; 34 | width: 100%; 35 | background-color: rgba(255,255,255,.9); 36 | box-shadow: 0 1px 1px rgba(0,0,0,.3), 0 -20px 5px 20px rgba(0,0,0,.3); 37 | } 38 | 39 | .toolbar-break { 40 | display: block; 41 | } 42 | 43 | @media (min-width: 500px) { 44 | .toolbar-break { 45 | display: inline; 46 | } 47 | } 48 | 49 | #toolbar_inner { 50 | padding: 5px 8px 6px; 51 | text-align: center; 52 | } 53 | 54 | #toolbar_inner h3 { 55 | margin: 0; 56 | padding: 3px 0 ; 57 | font-size: 13px; 58 | } 59 | 60 | #toolbar_inner h3:before { 61 | content: "\00ab "; 62 | margin-right: 5px; 63 | } 64 | 65 | #toolbar #searchbox { 66 | width: 150px; 67 | border: 1px solid rgba(0,0,0,.2); 68 | border-top-color: rgba(0,0,0,.3); 69 | box-shadow: 0 1px 1px rgba(0,0,0,.1) inset; 70 | padding: 1px 5px 2px; 71 | margin-left: 2px; 72 | -moz-border-radius: 3px; 73 | -webkit-border-radius: 3px; 74 | border-radius: 3px; 75 | } 76 | 77 | #toolbar #searchbutton { 78 | display:none; 79 | } 80 | 81 | #toolbar .toolbutton { 82 | border: 1px solid rgba(0,0,0,.3); 83 | border-top-color: rgba(0,0,0,.2); 84 | border-left-color: rgba(0,0,0,.25); 85 | border-right-color: rgba(0,0,0,.25); 86 | padding: 1px 5px 1px; 87 | margin-left: 2px; 88 | -moz-border-radius: 3px; 89 | -webkit-border-radius: 3px; 90 | border-radius: 3px; 91 | text-decoration: none; 92 | background-color: #fff; 93 | box-shadow: 0 1px 1px rgba(0,0,0,.1), 0 -1px 1px rgba(0,0,0,.03) inset; 94 | font-size: 12px; 95 | color: #000; 96 | } 97 | 98 | #toolbar .toolbutton:hover { 99 | border: 1px solid rgba(0,0,0,.4); 100 | } 101 | 102 | .backbutton { 103 | position: absolute; 104 | left: 0; 105 | right: 0; 106 | font-size: 12px; 107 | line-height: 25px; 108 | background-color: #fff 109 | } 110 | 111 | #hide-join-quit label { 112 | font-size: 12px; 113 | } 114 | 115 | #loading { 116 | position: absolute; 117 | left: 8px; 118 | top: 8px; 119 | } 120 | 121 | #content { 122 | margin: 32px 0px; 123 | color: #333; 124 | font-size: 13px; 125 | background-color: #fefefe; 126 | } 127 | 128 | .searchresult { 129 | padding-top: 20px 130 | } 131 | 132 | #irc, 133 | #irc li { 134 | margin: 0; 135 | padding: 0; 136 | } 137 | 138 | #irc li { 139 | position: relative; 140 | line-height: 20px; 141 | border-bottom: 1px solid #f1f1f1; 142 | vertical-align: top; 143 | overflow: hidden; 144 | } 145 | #irc li:hover { 146 | background-color: #f9f9f9; 147 | } 148 | 149 | #irc .pagebreak { 150 | border-bottom: 1px solid #333; 151 | } 152 | 153 | #irc li:first-child.pagebreak { 154 | border-bottom: 1px solid #f1f1f1; 155 | } 156 | 157 | #irc li:last-child.pagebreak { 158 | border-bottom: 1px solid #f1f1f1; 159 | } 160 | 161 | .name { 162 | display: block; 163 | padding: 5px 5px 0 5px; 164 | } 165 | 166 | .name a { 167 | font-weight: bold; 168 | text-decoration: none; 169 | color: #333; 170 | } 171 | 172 | .message { 173 | margin-left: 0%; 174 | display: block; 175 | padding: 0 5px 5px 5px; 176 | } 177 | 178 | @media (min-width: 500px) { 179 | .name { 180 | position: absolute; 181 | left:0; 182 | top: -1px; 183 | text-align: right; 184 | width: 20%; 185 | padding: 0; 186 | } 187 | 188 | .message { 189 | margin-left: 20%; 190 | padding: 0; 191 | } 192 | } 193 | 194 | @media (min-width: 900px) { 195 | .name { 196 | width: 10%; 197 | } 198 | 199 | .message { 200 | margin-left: 10%; 201 | } 202 | } 203 | 204 | .date { 205 | position: absolute; 206 | right: 5px; 207 | top: 0; 208 | display: none; 209 | font-size: x-small; 210 | color: #999; 211 | background-color: #f9f9f9; 212 | box-shadow: -10px 0 5px 0px #f9f9f9; 213 | } 214 | li:hover .date { 215 | display: block; 216 | } 217 | 218 | .inline-image-link { 219 | color: #ccc; 220 | } 221 | .inline-image { 222 | display: block; 223 | border:1px solid #ccc; 224 | margin: 0 0 5px 0; 225 | padding: 10px; 226 | background-color: #fff; 227 | max-width: 92%; 228 | } 229 | 230 | .highlighted { 231 | background-color: rgba(0, 255, 0, .2) 232 | } 233 | 234 | #irc .join, #irc .join a { 235 | color: #999; 236 | } 237 | 238 | #irc .action, #irc .action a { 239 | color: purple; 240 | } 241 | 242 | #irc .topic, #irc .topic a { 243 | color: blue; 244 | } 245 | 246 | #irc .nick, #irc .nick a { 247 | color: darkblue; 248 | } 249 | 250 | #irc .quit, #irc .quit a { 251 | font-size: small; 252 | color: #666; 253 | } 254 | 255 | #footer { 256 | font-size: xx-small; 257 | text-align: right; 258 | margin: 10px; 259 | } 260 | 261 | 262 | 263 | 264 | #horrible_error { 265 | position: absolute; 266 | background-color: #F5F57F; 267 | top: 30px; 268 | left: 50%; 269 | margin-left: -25%; 270 | z-index: 500; 271 | width: 50%; 272 | padding: 10px; 273 | border: 2px solid black; 274 | } 275 | 276 | #horrible_error h2 { 277 | margin-top: 0; 278 | } 279 | 280 | #error { 281 | background-color: white; 282 | border-radius: 5px; 283 | padding: 10px; 284 | width: 95%; 285 | margin: auto; 286 | } -------------------------------------------------------------------------------- /web/theme/standard/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cube-drone/pierc/9cdcb19bb41878782a9da1d548f5c8d5d9245127/web/theme/standard/ajax-loader.gif -------------------------------------------------------------------------------- /web/theme/standard/style.css: -------------------------------------------------------------------------------- 1 | body{ margin: 0; padding: 0; font-family: "Helvetica Neue", sans-serif; } 2 | 3 | 4 | /* important: this is dumb. */ 5 | .search-highlight{ 6 | font-family: "Helvetica Neue", sans-serif; 7 | background-color: rgba(255, 255, 0, .8); 8 | color: rgba(0,0,0,.6); 9 | border-radius: 3px; 10 | font-size: 13px; 11 | padding: 2px 5px;} 12 | .highlight-1{ background-color: rgba(255, 255, 0, 0.4); } 13 | .highlight-2{ background-color: rgba(255, 255, 0, 0.45); } 14 | .highlight-3{ background-color: rgba(255, 255, 0, 0.5); } 15 | .highlight-4{ background-color: rgba(255, 255, 0, 0.55); } 16 | .highlight-5{ background-color: rgba(255, 255, 0, 0.6); } 17 | .highlight-6{ background-color: rgba(255, 255, 0, 0.65); } 18 | .highlight-7{ background-color: rgba(255, 255, 0, 0.7); } 19 | .highlight-8{ background-color: rgba(255, 255, 0, 0.75); } 20 | .highlight-9{ background-color: rgba(255, 255, 0, 0.8); } 21 | .highlight-10{ background-color: rgba(255, 255, 0, 0.85); } 22 | 23 | #events { display: none; } 24 | #important{ display: none; } 25 | 26 | a { 27 | color: #3366FF; 28 | } 29 | 30 | #toolbar { 31 | position: fixed; 32 | z-index: 9999; 33 | top: 0; 34 | width: 100%; 35 | background-color: rgba(210,230,255,.9); 36 | box-shadow: 0 1px 1px rgba(0,0,0,.3), 0 -20px 5px 20px rgba(0,0,0,.3); 37 | } 38 | 39 | .toolbar-break { 40 | display: block; 41 | } 42 | 43 | @media (min-width: 500px) { 44 | .toolbar-break { 45 | display: inline; 46 | } 47 | } 48 | 49 | #toolbar_inner { 50 | padding: 5px 8px 6px; 51 | text-align: center; 52 | } 53 | 54 | #toolbar_inner h3 { 55 | margin: 0; 56 | padding: 3px 0 ; 57 | font-size: 13px; 58 | } 59 | 60 | #toolbar_inner h3:before { 61 | content: "\00ab "; 62 | margin-right: 5px; 63 | } 64 | 65 | #toolbar #searchbox { 66 | width: 150px; 67 | border: 1px solid rgba(0,0,0,.2); 68 | border-top-color: rgba(0,0,0,.3); 69 | box-shadow: 0 1px 1px rgba(0,0,0,.1) inset; 70 | padding: 1px 5px 2px; 71 | margin-left: 2px; 72 | -moz-border-radius: 3px; 73 | -webkit-border-radius: 3px; 74 | border-radius: 3px; 75 | } 76 | 77 | #toolbar #searchbutton { 78 | display:none; 79 | } 80 | 81 | #toolbar .toolbutton { 82 | border: 1px solid rgba(0,0,0,.3); 83 | border-top-color: rgba(0,0,0,.2); 84 | border-left-color: rgba(0,0,0,.25); 85 | border-right-color: rgba(0,0,0,.25); 86 | padding: 1px 5px 1px; 87 | margin-left: 2px; 88 | -moz-border-radius: 3px; 89 | -webkit-border-radius: 3px; 90 | border-radius: 3px; 91 | text-decoration: none; 92 | background-color: #fff; 93 | box-shadow: 0 1px 1px rgba(0,0,0,.1), 0 -1px 1px rgba(0,0,0,.03) inset; 94 | font-size: 12px; 95 | color: #000; 96 | } 97 | 98 | #toolbar .toolbutton:hover { 99 | border: 1px solid rgba(0,0,0,.4); 100 | } 101 | 102 | .backbutton { 103 | position: absolute; 104 | left: 0; 105 | right: 0; 106 | font-size: 12px; 107 | line-height: 25px; 108 | background-color: #fff 109 | } 110 | 111 | #hide-join-quit label, #inline-media-menu label { 112 | font-size: 12px; 113 | } 114 | 115 | #loading { 116 | position: absolute; 117 | top: 100px; 118 | left: 50px; 119 | width: 50px; 120 | } 121 | 122 | #content { 123 | margin: 32px 0px; 124 | color: #333; 125 | font-size: 13px; 126 | background-color: #fefefe; 127 | } 128 | 129 | .searchresult { 130 | padding-top: 20px 131 | } 132 | 133 | #irc, 134 | #irc li { 135 | margin: 0; 136 | padding: 0; 137 | } 138 | 139 | #irc li { 140 | position: relative; 141 | line-height: 20px; 142 | border-bottom: 1px solid #f1f1f1; 143 | vertical-align: top; 144 | overflow: hidden; 145 | } 146 | #irc li:hover { 147 | background-color: #f9f9f9; 148 | } 149 | 150 | #irc .pagebreak { 151 | border-bottom: 1px solid #333; 152 | } 153 | 154 | #irc li:first-child.pagebreak { 155 | border-bottom: 1px solid #f1f1f1; 156 | } 157 | 158 | #irc li:last-child.pagebreak { 159 | border-bottom: 1px solid #f1f1f1; 160 | } 161 | 162 | .name { 163 | display: block; 164 | padding: 5px 5px 0 5px; 165 | } 166 | 167 | .name a { 168 | font-weight: bold; 169 | text-decoration: none; 170 | color: #333; 171 | } 172 | 173 | .message { 174 | margin-left: 0%; 175 | display: block; 176 | padding: 0 5px 5px 5px; 177 | } 178 | 179 | @media (min-width: 500px) { 180 | .name { 181 | position: absolute; 182 | left:0; 183 | top: -1px; 184 | text-align: right; 185 | width: 20%; 186 | padding: 0; 187 | } 188 | 189 | .message { 190 | margin-left: 20%; 191 | padding: 0; 192 | } 193 | } 194 | 195 | @media (min-width: 900px) { 196 | .name { 197 | width: 10%; 198 | } 199 | 200 | .message { 201 | margin-left: 10%; 202 | } 203 | } 204 | 205 | .date { 206 | position: absolute; 207 | right: 5px; 208 | top: 0; 209 | display: none; 210 | font-size: x-small; 211 | color: #999; 212 | background-color: #f9f9f9; 213 | box-shadow: -10px 0 5px 0px #f9f9f9; 214 | } 215 | li:hover .date { 216 | display: block; 217 | } 218 | 219 | .inline-image-link { 220 | color: #ccc; 221 | } 222 | .inline-image { 223 | display: block; 224 | border:1px solid #ccc; 225 | margin: 0 0 5px 0; 226 | padding: 10px; 227 | background-color: #fff; 228 | max-width: 92%; 229 | } 230 | 231 | .highlighted { 232 | background-color: rgba(0, 255, 0, .2) 233 | } 234 | 235 | #irc .join, #irc .join a { 236 | color: #999; 237 | } 238 | 239 | #irc .action, #irc .action a { 240 | color: purple; 241 | } 242 | 243 | #irc .topic, #irc .topic a { 244 | color: blue; 245 | } 246 | 247 | #irc .nick, #irc .nick a { 248 | color: darkblue; 249 | } 250 | 251 | #irc .quit, #irc .quit a { 252 | font-size: small; 253 | color: #666; 254 | } 255 | 256 | #footer { 257 | font-size: xx-small; 258 | text-align: right; 259 | margin: 10px; 260 | } 261 | 262 | 263 | 264 | 265 | #horrible_error { 266 | position: absolute; 267 | background-color: #F5F57F; 268 | top: 30px; 269 | left: 50%; 270 | margin-left: -25%; 271 | z-index: 500; 272 | width: 50%; 273 | padding: 10px; 274 | border: 2px solid black; 275 | } 276 | 277 | #horrible_error h2 { 278 | margin-top: 0; 279 | } 280 | 281 | #error { 282 | background-color: white; 283 | border-radius: 5px; 284 | padding: 10px; 285 | width: 95%; 286 | margin: auto; 287 | } 288 | --------------------------------------------------------------------------------