├── .gitignore ├── README.md ├── ajax ├── GetConversation.php ├── ListChannels.php ├── ListServers.php └── Search.php ├── css └── main.css ├── images └── ajax-loader.gif ├── index.php ├── js └── main.js └── lib ├── GetConversation.class.php ├── ListChannels.class.php ├── ListServers.class.php ├── PathValidator.class.php ├── Search.class.php ├── SearchResult.class.php └── config.ini-dist /.gitignore: -------------------------------------------------------------------------------- 1 | lib/config.ini 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | IRC Log Viewer 2 | ============= 3 | 4 | This was an IRC log parser written to make it possible to search IRC log files 5 | for the #firebreath channel on irc.freenode.net. 6 | 7 | The goal was to make it easier to find pervious discussions based on keyword 8 | matching (partial or complete, including searching for special characters). 9 | 10 | It attempts to group discussions by date/time and rank results by relevance, 11 | noting keyword frequency and who mentioned them. 12 | 13 | It's currently a functional work in progress. 14 | 15 | You can find a live example running at http://logs.firebreath.org/ 16 | 17 | 18 | Requirements 19 | ------- 20 | 21 | ### Server Requirements 22 | - PHP 5 23 | - No Apache or database configuration required. 24 | 25 | 26 | ### Client Requirements 27 | - Internet Explorer 7+, Firefox 3.5+ or recent version of Safari or Chrome. 28 | - JavaScript must be enabled. 29 | 30 | 31 | If it doesn't work in your browser, do let me know. Feel free to send me a 32 | screenshot. I'm aware there are currently usability issues on mobile browsers. 33 | 34 | 35 | Configuration 36 | ------- 37 | 38 | Configure lib/config.ini to point to your log file directory. You will need 39 | to use a directory structure as described below. 40 | 41 | TIP: If you don't want to change how you currently save your IRC logs, you can 42 | use symlinks to create a structure with directories that point to wherever 43 | you currently save your log files to. 44 | 45 | Example (if you currently save your logs in ~/irclogs/firebreath/): 46 | 47 | mkdir -p "/var/irclogs/irc.freenode.net/#firebreath" 48 | ln -s ~/irclogs/firebreath/ "/var/irclogs/irc.freenode.net/#firebreath" 49 | 50 | ### Directory Structure 51 | 52 | Directory structure should be in the form: 53 | 54 | {servername}/{channelname}/{logfiles} 55 | 56 | Log files must have the date in the filename in YYYYDDMM or YYYY-DD-MM format. 57 | 58 | Examples: 59 | 60 | /var/irclogs/irc.freenode.net/#firebreath/#firebreath_20100131.log 61 | /var/irclogs/irc.freenode.net/%23firebreath/2010-01-31.txt 62 | 63 | 64 | ### Log File Format 65 | 66 | Actual log lines must be in the form "{time} {user} {message}" (or 67 | "{time} {message}" for system messages). 68 | 69 | Examples: 70 | 71 | [08:01:04] Hello everyone! 72 | [11:23] Hey 73 | 17:22 Hi 74 | 18:50:17 *** Quits: Alex (~alex@host) (Remote host closed the connection) 75 | 76 | 77 | Scalability/Performance 78 | ------- 79 | 80 | It's been designed to support a range of log file formats, and specifically to 81 | need as little configuration as possible (hence not requiring a database). 82 | 83 | It works by parsing raw log files and recording keyword hits, using strpos() 84 | for fast matching. In practice, this seems to be fast enough to allow parsing 85 | through ~6 years/(30MB+) worth of logs in less than 10 seconds. 86 | 87 | With a log directory structure of ./servername/channel/ adding additional 88 | servers / channels doesn't impact performance. 89 | 90 | 91 | Contributing 92 | ------- 93 | 94 | I'd love it if you'd like to help with bug reports, feature requests or code! 95 | I suggest joining us on irc://irc.freenode.net/%23firebreath or email 96 | . 97 | -------------------------------------------------------------------------------- /ajax/GetConversation.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ajax/ListChannels.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ajax/ListServers.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ajax/Search.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /css/main.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | margin: 0; 4 | padding: 0px; 5 | font-size: 10pt; 6 | font-family: 'Lucida Grande', 'Lucida Sans Unicode', Arial, Verdana, sans-serif; 7 | color: #444; 8 | font-size: 10pt; 9 | } 10 | 11 | #header { 12 | padding: 8px; 13 | background: #c44700; 14 | background: -moz-linear-gradient(top, #F17432 0%, #BC4B05 54%, #BC4B05 96%); /* firefox */ 15 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#F17432), color-stop(54%,#BC4B05), color-stop(96%,#BC4B05)); /* webkit */ 16 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#F17432', endColorstr='#BC4B05',GradientType=0 ); /* ie */ 17 | border-bottom: 2px solid #7b2d00; 18 | } 19 | 20 | #header h1 { 21 | margin: 0; 22 | padding: 0; 23 | font-size: 16pt; 24 | color: #ffb77f; 25 | text-shadow: #7b2d00 0px 2px 0px; 26 | font-family: Arial, Helvetica; 27 | } 28 | 29 | #header .version { 30 | position: absolute; 31 | top: 12px; 32 | right: 15px; 33 | font-weight: bold; 34 | color: #7b2d00; 35 | color: #7b2d00; 36 | text-shadow: #ffb77f 0px 1px 0px; 37 | } 38 | 39 | #footer { 40 | display: none; 41 | text-align: center; 42 | clear: both; 43 | } 44 | 45 | #ircLogSearchContainer { 46 | padding: 8px; 47 | } 48 | 49 | #ircLogSearchFormContainer { 50 | margin-top: 5px; 51 | margin-bottom: 30px; 52 | clear: both; 53 | margin-left: auto; 54 | margin-right: auto; 55 | width: 660px; 56 | } 57 | 58 | #ircLogSearchForm { 59 | margin: 0; 60 | padding: 10px; 61 | float: left; 62 | border: 1px solid #ddd; 63 | -border-radius: 8pt; 64 | -moz-border-radius: 8pt; 65 | -webkit-border-radius: 8pt; 66 | -khtml-border-radius: 8pt; 67 | background-color: #f9f9f9; 68 | } 69 | 70 | #ircLogSearchForm select { 71 | float: left; 72 | margin-right: 15px; 73 | margin-top: 5px; 74 | } 75 | 76 | #ircLogSearchForm label { 77 | float: left; 78 | font-weight: bold; 79 | background: transparent; 80 | z-index: 0; 81 | color: #666; 82 | margin-right: 5px; 83 | font-family: 'Lucida Grande', 'Lucida Sans Unicode', Arial, Verdana, sans-serif; 84 | color: #444; 85 | font-size: 8pt; 86 | font-weight: bold; 87 | margin-top: 6px; 88 | } 89 | 90 | #ircLogSearchForm button { 91 | float: left; 92 | margin: 0; 93 | color: #444; 94 | position: relative; 95 | left: -1px; 96 | display: inline-block; 97 | outline: none; 98 | cursor: pointer; 99 | text-align: center; 100 | text-decoration: none; 101 | font-size: 12px; 102 | font-weight: bold; 103 | text-shadow: #fff 0px 1px 0px; 104 | -border-top-right-radius: .5em; 105 | -moz-border-radius-topright: .5em; 106 | -webkit-border-top-right-radius: .5em; 107 | -border-bottom-right-radius: .5em; 108 | -moz-border-radius-bottomright: .5em; 109 | -webkit-border-bottom-right-radius: .5em; 110 | border-width: 1px; 111 | color: #606060; 112 | border: solid 1px #b7b7b7; 113 | background: #fff; 114 | background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#ededed)); 115 | background: -moz-linear-gradient(top, #fff, #ededed); 116 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#ededed'); 117 | dheight: 26px; 118 | padding: 5px; 119 | dtop: 2px; 120 | padding-left: 10px; 121 | padding-right: 10px; 122 | } 123 | 124 | #ircLogSearchForm button:hover { 125 | text-decoration: none; 126 | -webkit-box-shadow: 0 0px 0px rgba(0,0,0,.2); 127 | -moz-box-shadow: 0 0px 0px rgba(0,0,0,.2); 128 | box-shadow: 0 0px 0px rgba(0,0,0,.2); 129 | background: #ededed; 130 | background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#dcdcdc)); 131 | background: -moz-linear-gradient(top, #fff, #dcdcdc); 132 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#dcdcdc'); 133 | } 134 | 135 | #ircLogSearchForm button:active { 136 | position: relative; 137 | color: #999; 138 | background: -webkit-gradient(linear, left top, left bottom, from(#ededed), to(#fff)); 139 | background: -moz-linear-gradient(top, #ededed, #fff); 140 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ededed', endColorstr='#ffffff'); 141 | } 142 | 143 | /* Webkit specific rendering tweak (for Safari and Chrome) */ 144 | @media screen and (-webkit-min-device-pixel-ratio:0) { 145 | #ircLogSearchForm button { 146 | top: 2px; 147 | } 148 | } 149 | 150 | #ircLogSearchForm input { 151 | float: left; 152 | width: 150px; 153 | color: #666; 154 | font-size: 10pt; 155 | border: 1px solid #ccc; 156 | background: #fff; 157 | background: -moz-linear-gradient(top, #eeeeee 0%, #FFFFFF 32%); /* firefox */ 158 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#eeeeee), color-stop(32%,#FFFFFF)); /* webkit */ 159 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#FFFFFF',GradientType=0 ); /* ie */ 160 | -border-top-left-radius: .5em; 161 | -moz-border-radius-topleft: .5em; 162 | -webkit-border-top-left-radius: .5em; 163 | -border-bottom-left-radius: .5em; 164 | -moz-border-radius-bottomleft: .5em; 165 | -webkit-border-bottom-left-radius: .5em; 166 | position: relative; 167 | outline-width: thin; 168 | padding-top: 4px; 169 | padding-bottom: 5px; 170 | padding-left: 6px; 171 | dmax-height: 20px; 172 | } 173 | 174 | /* Gecko specific rendering tweak */ 175 | @-moz-document url-prefix() { 176 | #ircLogSearchForm input { 177 | padding-top: 6px; 178 | } 179 | } 180 | 181 | #ircLogSearchResults { 182 | display: none; 183 | clear: left; 184 | position: relative; 185 | padding-top: 10px; 186 | } 187 | 188 | #ircLogSearchResultsConversations { 189 | font-size: 9pt; 190 | position: relative; 191 | display: block; 192 | width: 240px; 193 | height: auto; 194 | float: left; 195 | border: 1px solid #ddd; 196 | background-color: #fff; 197 | -border-top-left-radius: 12px; 198 | -border-bottom-left-radius: 12px; 199 | -moz-border-radius-topleft: 12px; 200 | -moz-border-radius-bottomleft: 12px; 201 | -webkit-border-top-left-radius: 12px; 202 | -webkit-border-bottom-left-radius: 12px; 203 | padding-bottom: 20px; 204 | background: #f9f9f9; 205 | font-size: 8pt; 206 | } 207 | 208 | #uniform-ircServer, 209 | #uniform-ircChannel { 210 | cursor: default; 211 | text-align: left; 212 | z-index: 1; 213 | position: relative; 214 | top: 6px; 215 | padding-top: 12px; 216 | display: inherit; 217 | padding-right: 190px; 218 | } 219 | 220 | #uniform-ircServer span, 221 | #uniform-ircChannel span { 222 | position: absolute; 223 | top: 0px; 224 | left: 10px; 225 | } 226 | 227 | #ircLogSearchResultsConversations .heading { 228 | font-size: 11pt; 229 | text-shadow: #fff 0px 1px 0; 230 | color: #777; 231 | padding: 6px; 232 | text-align: center; 233 | -border-top-left-radius: 12px; 234 | -moz-border-radius-topleft: 12px; 235 | -webkit-border-top-left-radius: 12px; 236 | background: #ddd; 237 | background: -moz-linear-gradient(top, #F2F2F2 0%, #D6D6D6 100%); /* firefox */ 238 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#F2F2F2), color-stop(100%,#D6D6D6)); /* webkit */ 239 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#F2F2F2', endColorstr='#D6D6D6',GradientType=0 ); /* ie */ 240 | font-family: 'Lucida Grande', 'Lucida Sans Unicode', Arial, Verdana, sans-serif; 241 | color: #444; 242 | font-size: 9pt; 243 | font-weight: bold; 244 | } 245 | 246 | #ircLogSearchResultsConversations .conversation, 247 | #ircLogSearchResultsConversations .conversationSelected { 248 | position: relative; 249 | color: #777; 250 | padding: 8px; 251 | border-bottom: 1px solid #ddd; 252 | border-top: 1px solid #fff; 253 | background: #f9f9f9; 254 | } 255 | 256 | 257 | #ircLogSearchResultsConversations .conversation:hover { 258 | cursor: pointer; 259 | color: #555; 260 | background: #fff; 261 | background: -moz-linear-gradient(top, #ffffff 0%, #f3f3f3 50%, #ededed 51%, #ffffff 100%); /* firefox */ 262 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(50%,#f3f3f3), color-stop(51%,#ededed), color-stop(100%,#ffffff)); /* webkit */ 263 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#ffffff',GradientType=0 ); /* ie */ 264 | } 265 | 266 | #ircLogSearchResultsConversations .conversation:hover .ircConversationLabel { 267 | color: #888; 268 | 269 | } 270 | 271 | #ircLogSearchResultsConversationsWrapper { 272 | overflow: auto; 273 | border-bottom: 1px solid #ddd; 274 | border-top: 1px solid #ddd; 275 | background: #f9f9f9; 276 | } 277 | 278 | #ircLogSearchResultsConversationsWrapper .loadingConversationsPlaceholder { 279 | position: absolute; 280 | width: 100%; 281 | text-align: center; 282 | font-weight: bold; 283 | top: 50%; 284 | } 285 | 286 | #ircLogSearchResultsConversations .conversation .selectedArrow { 287 | display: none; 288 | } 289 | 290 | #ircLogSearchResultsConversations .conversationSelected .selectedArrow { 291 | display: block; 292 | position: absolute; 293 | top: 5px; 294 | right: 5px; 295 | font-size: 14pt; 296 | color: #ffb77f; 297 | text-shadow: #BC4B05 1px 1px 0px; 298 | font-weight: bold; 299 | } 300 | 301 | #ircLogSearchResultsConversations .conversationSelected, 302 | #ircLogSearchResultsConversations .conversationSelected:hover { 303 | border-top: 1px solid #ffb28a; 304 | border-bottom: 1px solid #BC4B05; 305 | background: #c44700; 306 | background: -moz-linear-gradient(top, #f17432 0%, #BC4B05 100%); /* firefox */ 307 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f17432), color-stop(100%,#BC4B05)); /* webkit */ 308 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f17432', endColorstr='#BC4B05',GradientType=0 ); /* ie */ 309 | color: #ffdecb; 310 | } 311 | 312 | #ircLogSearchResultsConversations .conversationSelected .ircConversationLabel, 313 | #ircLogSearchResultsConversations .conversationSelected:hover .ircConversationLabel { 314 | color: #ffb77f; 315 | } 316 | 317 | .ircConversationLabel { 318 | float: left; 319 | width: 60px; 320 | text-align: right; 321 | padding-right: 5px; 322 | clear: left; 323 | } 324 | 325 | .ircConversationValues { 326 | padding-left: 65px; 327 | } 328 | 329 | #ircLogSearchResultsLogView { 330 | position: relative; 331 | display: block; 332 | left: 4px; 333 | margin-right: 5px; 334 | margin-left: 0px; 335 | padding-bottom: 10px; 336 | overflow: auto; 337 | -border-top-right-radius: 12px; 338 | -border-bottom-right-radius: 12px; 339 | -moz-border-radius-topright: 12px; 340 | -moz-border-radius-bottomright: 12px; 341 | -webkit-border-top-right-radius: 12px; 342 | -webkit-border-bottom-right-radius: 12px; 343 | background: #f9f9f9; 344 | border: 1px solid #ddd; 345 | } 346 | 347 | #ircLogSearchResultsLogView .heading { 348 | text-shadow: #fff 0px 1px 0; 349 | font-size: 11pt; 350 | color: #777; 351 | padding: 6px; 352 | text-align: center; 353 | -border-top-right-radius: 12px; 354 | -moz-border-radius-topright: 12px; 355 | -webkit-border-top-right-radius: 12px; 356 | border-bottom: 1px solid #ddd; 357 | background: #ddd; 358 | background: -moz-linear-gradient(top, #F2F2F2 0%, #D6D6D6 100%); /* firefox */ 359 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#F2F2F2), color-stop(100%,#D6D6D6)); /* webkit */ 360 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#F2F2F2', endColorstr='#D6D6D6',GradientType=0 ); /* ie */ 361 | font-family: 'Lucida Grande', 'Lucida Sans Unicode', Arial, Verdana, sans-serif; 362 | color: #444; 363 | font-size: 9pt; 364 | font-weight: bold; 365 | } 366 | 367 | 368 | #ircLogSearchResultsLogView div.oddRow, 369 | #ircLogSearchResultsLogView div.evenRow { 370 | font-family: Andale Mono, monospace; 371 | font-size: 8pt; 372 | width: auto; 373 | border-bottom: 1px solid #444; 374 | color: #fff; 375 | padding-left: 4px; 376 | padding-top: 2px; 377 | padding-bottom: 2px; 378 | overflow: auto; 379 | } 380 | 381 | /* Webkit specific rendering tweak (for Safari and Chrome) */ 382 | @media screen and (-webkit-min-device-pixel-ratio:0) { 383 | #ircLogSearchResultsLogView div.oddRow, 384 | #ircLogSearchResultsLogView div.evenRow { 385 | font-size: 8pt; 386 | } 387 | } 388 | 389 | 390 | #ircLogSearchResultsLogView div.evenRow { 391 | background: #1a1a1a; 392 | } 393 | 394 | #ircLogSearchResultsLogView div.oddRow div, 395 | #ircLogSearchResultsLogView div.evenRow div { 396 | display: inline; 397 | float: left; 398 | } 399 | 400 | #ircLogSearchResultsLogView .keyword { 401 | background: #ffb500; 402 | color: #6d3d00; 403 | font-weight: bold; 404 | } 405 | 406 | #ircLogSearchResultsLogView .time { 407 | display: block; 408 | width: 50pt; 409 | color: #888; 410 | } 411 | 412 | #ircLogSearchResultsLogView .user { 413 | display: block; 414 | width: 110px; 415 | padding-right: 10px; 416 | overflow: hidden; 417 | text-align: right; 418 | color: #bbb; 419 | } 420 | 421 | #ircLogSearchResultsLogView .systemMsg { 422 | color: #888; 423 | font-style: italic; 424 | } 425 | 426 | .ircConversationLabel { 427 | font-weight: bold; 428 | color: #aaa; 429 | } 430 | 431 | .ircConversationParticipant { 432 | display: inline; 433 | padding-left: 2px; 434 | padding-right: 2px; 435 | color: #C84B17 ; 436 | font-weight: bold; 437 | font-size: 8pt; 438 | } 439 | 440 | .ircConversationKeyword { 441 | display: inline; 442 | padding-left: 2px; 443 | padding-right: 2px; 444 | color: #59912B; 445 | font-weight: bold; 446 | font-size: 8pt; 447 | } 448 | 449 | #ircLogSearchResultsConversations .conversationSelected .ircConversationParticipant, 450 | #ircLogSearchResultsConversations .conversationSelected .ircConversationKeyword { 451 | color: white; 452 | } 453 | 454 | #ircLogSearchResultsLogViewWrapper { 455 | overflow: auto; 456 | margin-bottom: 9px; 457 | border-bottom: 1px solid #ddd; 458 | border-top: 1px solid #ddd; 459 | background: #000; 460 | } -------------------------------------------------------------------------------- /images/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/firebreath/irc-logviewer/5e7a3b9b56550d3b8507def6db5cbdd089fe3911/images/ajax-loader.gif -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | FireBreath IRC Log Search 10 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 23 | 24 |
25 | 26 | 27 |
28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 |
36 |
37 | 38 | 39 |
40 |
41 |
42 |
43 | 44 |
45 | 46 | 47 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | 2 | jQuery(document).ready(function() { 3 | jQuery(window).resize(function() { 4 | ircLogSearch.redrawWindow(); 5 | }); 6 | 7 | // Set layout correctly on page load 8 | ircLogSearch.redrawWindow(); 9 | 10 | // Get list of IRC servers on page load 11 | ircLogSearch.populateIrcServerList(); 12 | }); 13 | 14 | var ircLogSearch = {}; 15 | 16 | ircLogSearch.populateIrcServerList = function() { 17 | 18 | jQuery.ajax({ 19 | url: "ajax/ListServers.php?timestamp=" + (new Date().getTime().toString()), 20 | type: "GET", 21 | dataType: "json", 22 | success: function(result) { 23 | 24 | $("#ircServer").html(""); 25 | $("#ircChannel").html(""); 26 | 27 | for (var i = 0; i < result.length; i++) { 28 | var options = $('#ircServer').attr('options'); 29 | options[options.length] = new Option(result[i], result[i]); 30 | } 31 | 32 | // Calling this method sucessfully always triggers the IRC Channel List to be re-populated too. 33 | ircLogSearch.populateIrcChannelList(); 34 | } 35 | 36 | }); 37 | } 38 | 39 | ircLogSearch.populateIrcChannelList = function() { 40 | 41 | var server = $('#ircServer option:selected').val(); 42 | 43 | jQuery.ajax({ 44 | url: "ajax/ListChannels.php?timestamp=" + (new Date().getTime().toString()) + "&server=" + encodeURIComponent(server), 45 | type: "GET", 46 | dataType: "json", 47 | success: function(result) { 48 | 49 | $("#ircChannel").html(""); 50 | 51 | for (var i = 0; i < result.length; i++) { 52 | var options = $('#ircChannel').attr('options'); 53 | options[options.length] = new Option(result[i], result[i]); 54 | } 55 | } 56 | 57 | }); 58 | } 59 | 60 | 61 | ircLogSearch.selectConversation = function(element, server, channel, startTime, endTime, keywords) { 62 | $(".conversation").removeClass("conversationSelected"); 63 | $(element).addClass("conversationSelected"); 64 | 65 | ircLogSearch.getConversation(server, channel, startTime, endTime, keywords); 66 | } 67 | 68 | 69 | ircLogSearch.getConversation = function(server, channel, startTime, endTime, keywords) { 70 | 71 | jQuery('#ircLogSearchResultsLogViewWrapper').html('
Chat Log
'); 72 | 73 | $.ajax({ 74 | url: "ajax/GetConversation.php?timestamp=" + (new Date().getTime().toString()) + "&server=" + encodeURIComponent(server) + "&channel=" + encodeURIComponent(channel) + "&startTime=" + encodeURIComponent(startTime)+ "&endTime=" + encodeURIComponent(endTime)+"&keywords="+keywords, 75 | type: "GET", 76 | dataType: "json", 77 | success: function(json) { 78 | 79 | jQuery('#ircLogSearchResultsLogView').html('
Chat Log: ' + channel +'
' 80 | +'
'); 81 | ircLogSearch.redrawWindow(); 82 | 83 | var rowClass = "oddRow"; 84 | for (var i = 0; i < json['conversation'].length; i++) { 85 | 86 | if (json['conversation'][i].user) { 87 | jQuery('#ircLogSearchResultsLogViewWrapper').append( '
' 88 | +'
' + json['conversation'][i].time + '
' 89 | +'
<' + json['conversation'][i].user + '>
' 90 | +'' + json['conversation'][i].msg + '' 91 | +'
' 92 | ); 93 | } else { 94 | jQuery('#ircLogSearchResultsLogViewWrapper').append( '
' 95 | +'
' + json['conversation'][i].time + '
' 96 | +'' + json['conversation'][i].msg + '' 97 | +'
' 98 | ); 99 | } 100 | 101 | (rowClass == "oddRow") ? rowClass = "evenRow" : rowClass = "oddRow"; 102 | 103 | } 104 | } 105 | 106 | }); 107 | 108 | ircLogSearch.redrawWindow(); 109 | 110 | return; 111 | } 112 | 113 | ircLogSearch.search = function() { 114 | 115 | 116 | var server = document.getElementById('ircServer').value; 117 | var channel = document.getElementById('ircChannel').value; 118 | var keywords = document.getElementById('keywords').value; 119 | 120 | // Todo: Impliment href query string, so URL's of results can be copy/pasted 121 | //window.location.href = "#q=search&server="+encodeURIComponent(server) + "&channel=" + encodeURIComponent(channel) + "&keywords=" + encodeURIComponent(keywords); 122 | 123 | if (keywords == "") 124 | return; 125 | 126 | // Reset results view 127 | jQuery('#ircLogSearchResultsConversations').html('
Searching for Conversations...
Loading
'); 128 | jQuery('#ircLogSearchResultsLogView').html('
Chat Log
'); 129 | ircLogSearch.redrawWindow(); 130 | jQuery('#ircLogSearchResults').show(); 131 | 132 | $.ajax({ 133 | url: "ajax/Search.php?timestamp=" + (new Date().getTime().toString()) + "&server=" + encodeURIComponent(server) + "&channel=" + encodeURIComponent(channel) + "&keywords=" + encodeURIComponent(keywords), 134 | type: "GET", 135 | dataType: "json", 136 | success: function(json) { 137 | 138 | // If we get a response with any matches, immedaitely start getting the first entry in the background 139 | if (json['searchResults'].length > 0) 140 | ircLogSearch.getConversation(server, channel, json['searchResults'][0].startTime, json['searchResults'][0].endTime, keywords); 141 | 142 | 143 | jQuery('#ircLogSearchResultsConversations').html('
Conversations (' + json['searchResults'].length + ')
'); 144 | 145 | 146 | for (var i = 0; i < json['searchResults'].length; i++) { 147 | 148 | var conversationClass = "conversation"; 149 | if (i == 0) 150 | conversationClass = "conversation conversationSelected"; 151 | 152 | var usersHtml = ''; 153 | for (user in json['searchResults'][i].users) { 154 | if (usersHtml != '') 155 | usersHtml += ', '; 156 | 157 | usersHtml += '
' + user + '
(' + json['searchResults'][i].users[user] + ')'; 158 | 159 | } 160 | 161 | var keywordsHtml = ''; 162 | for (keyword in json['searchResults'][i].keywords) { 163 | if (keywordsHtml != '') 164 | keywordsHtml += ', '; 165 | 166 | keywordsHtml += '
' + keyword + '
(' + json['searchResults'][i].keywords[keyword] + ')'; 167 | 168 | } 169 | 170 | var duration = Math.floor(json['searchResults'][i].duration / 60); 171 | if (duration < 6) { 172 | if (json['searchResults'][i].startTime == json['searchResults'][i].endTime) { 173 | duration = "Mentioned once"; 174 | } else { 175 | duration = "Less than 5 minutes"; 176 | } 177 | } else if (duration < 60) { 178 | duration += " minutes"; 179 | } else { 180 | duration = Math.floor(duration / 60); 181 | if (duration == 1) { 182 | duration += " hour"; 183 | } else { 184 | duration += " hours"; 185 | } 186 | } 187 | 188 | jQuery('#ircLogSearchResultsConversationsWrapper').append( '
' 190 | 191 | +'
Start:
' + json['searchResults'][i].startTime + '
' 192 | +'
End:
' + json['searchResults'][i].endTime + '
' 193 | +'
Duration:
' + duration + '
' 194 | +'
Keywords:
' + keywordsHtml + '
' 195 | +'
Users:
' + usersHtml + '
' 196 | +'
>
' 197 | +'
'); 198 | } 199 | 200 | ircLogSearch.redrawWindow(); 201 | 202 | } 203 | }); 204 | 205 | return false; 206 | }; 207 | 208 | ircLogSearch.redrawWindow = function() { 209 | var windowHeight = 0; 210 | if( typeof( window.innerWidth ) == 'number' ) { 211 | //Non-IE 212 | windowHeight = window.innerHeight; 213 | } else if( document.documentElement && ( document.documentElement.clientWidth || document.documentElement.clientHeight ) ) { 214 | //IE 6+ in 'standards compliant mode' 215 | windowHeight = document.documentElement.clientHeight; 216 | } else if( document.body && ( document.body.clientWidth || document.body.clientHeight ) ) { 217 | //IE 4 compatible 218 | windowHeight = document.body.clientHeight; 219 | } 220 | 221 | var newHeight = windowHeight - 180; 222 | if (newHeight < 200) 223 | newHeight = 200; 224 | 225 | if (document.getElementById('ircLogSearchResultsConversationsWrapper')) 226 | document.getElementById('ircLogSearchResultsConversationsWrapper').style.height = newHeight + "px"; 227 | if (document.getElementById('ircLogSearchResultsLogViewWrapper')) 228 | document.getElementById('ircLogSearchResultsLogViewWrapper').style.height = newHeight + "px"; 229 | 230 | } 231 | -------------------------------------------------------------------------------- /lib/GetConversation.class.php: -------------------------------------------------------------------------------- 1 | searchResults 38 | $logDir = $baseLogDir."/".addslashes($server)."/".addslashes($channel); 39 | 40 | $pathToFile = ""; 41 | $dirHandle = opendir($logDir); 42 | $i = 0; 43 | while(($filename = readdir($dirHandle)) !== false) { 44 | if(substr($filename, 0, 1) != ".") { // Don't include hidden directories (i.e. beginning with a ".") 45 | if (is_file($logDir."/".$filename)) { // Only open files 46 | 47 | // Get the day from the filename (this is why filenames must have the date 48 | // in them, in the e.g. "mylog_YYYY-MM-DD.log" or "mylogYYYYMMDD.txt", etc.. 49 | $dateFromFilename = preg_replace('/^(.*)(\d{4})(.*?)(\d{2})(.*?)(\d{2}?)(.*?)$/', "$2-$4-$6", $filename); 50 | $dateRangeStart = strtotime($dateFromFilename." 00:00:00"); 51 | $dateRangeEnd = strtotime($dateFromFilename." 23:59:59"); 52 | 53 | if ($startTimeStamp >= $dateRangeStart && $startTimeStamp <= $dateRangeEnd) { 54 | $pathToFile = $logDir."/".$filename; 55 | break; 56 | } 57 | 58 | } 59 | } 60 | $i++; 61 | } 62 | closedir($dirHandle); 63 | 64 | // TODO: Make these config options 65 | $startTimeStamp = $startTimeStamp - (60 * 5); // Show leading 5 minutes 66 | $endTimeStamp = $endTimeStamp + (60 * 60); // Show trailing 60 minutes 67 | 68 | // Min / Max lines (so that even if a channel is really quiet or really busy, responses are useful) 69 | $minLines = 25; // Good when people ask questions during the night that are not answered for hours 70 | $maxLines = 250; // TODO: Create UI option so users can load more of a convo if they really want 71 | 72 | $lineCount = 0; 73 | $matchingLineCount = 0; 74 | $fileHandle = fopen($pathToFile, "r") or die("Unable to open IRC log file for reading."); 75 | while(!feof($fileHandle)) { 76 | $line = fgets($fileHandle); 77 | 78 | $lineCount++; 79 | 80 | // Get timestamp (based on filename + time on line where match was found) 81 | @list($time, $username, $msg) = explode(' ', $line, 3); 82 | $username = preg_replace("/[^A-z0-9:_()\\|-]/", "", $username); 83 | $time = preg_replace("/[^0-9:]/", "", $time); 84 | $timestamp = strtotime($date." ".$time); 85 | 86 | $logLine = null; 87 | if ($time && $username) { 88 | $msg = preg_replace("/\n$/", "", $msg); 89 | $msg = htmlentities($msg); 90 | $msg = $this->highliteKeywords($msg, $keywords); 91 | 92 | if ($timestamp >= $startTimeStamp) { 93 | $logLine = array('line' => $lineCount, 'time' => $time, 'user' => $username, 'msg' => $msg); 94 | $matchingLineCount++; 95 | } 96 | 97 | } else { 98 | // Handle system messages & emotes (i.e. lines without a username) 99 | @list($time, $msg) = explode(' ', $line, 2); 100 | $time = preg_replace("/[^0-9:]/", "", $time); 101 | $timestamp = strtotime($date." ".$time); 102 | 103 | $msg = preg_replace("/\n$/", "", $msg); 104 | $msg = htmlentities($msg); 105 | $msg = $this->highliteKeywords($msg, $keywords); 106 | 107 | if ($timestamp >= $startTimeStamp) { 108 | $logLine = array('line' => $lineCount, 'time' => $time, 'msg' => $msg); 109 | $matchingLineCount++; 110 | } 111 | } 112 | 113 | if ($logLine !== null) 114 | array_push($this->conversation,$logLine); 115 | 116 | // Only attempt to exit if we have got at least number of lines in $minLines 117 | if ($matchingLineCount >= $minLines) 118 | if ($timestamp > $endTimeStamp) 119 | break; 120 | 121 | // Exit early if we have hit $maxLines 122 | if ($matchingLineCount >= $maxLines) 123 | break; 124 | 125 | } 126 | fclose($fileHandle); 127 | 128 | return $this->conversation; 129 | } 130 | 131 | private function highliteUrls($line) { 132 | // FIXME: 1) is borked regex 2) Conflicts with highliteKeywords() :-( Not sure how to resolve the latter problem. 133 | $pattern = "@\b(https?://)?(([0-9a-zA-Z_!~*'().&=+$%-]+:)?[0-9a-zA-Z_!~*'().&=+$%-]+\@)?(([0-9]{1,3}\.){3}[0-9]{1,3}|([0-9a-zA-Z_!~*'()-]+\.)*([0-9a-zA-Z][0-9a-zA-Z-]{0,61})?[0-9a-zA-Z]\.[a-zA-Z]{2,6})(:[0-9]{1,4})?((/[0-9a-zA-Z_!~*'().;?:\@&=+$,%#-]+)*/?)@"; 134 | $line = preg_replace($pattern, '\0', $line); 135 | return $line; 136 | } 137 | 138 | private function highliteKeywords($line, $keywords) { 139 | 140 | // Allow mo more than 10 keywords (to avoid easily overloading server) 141 | $keywords = explode(" ", $keywords, 10); 142 | 143 | // Remove any duplicates (for efficency) 144 | $keywords = array_unique($keywords); 145 | 146 | foreach ($keywords as $keyword) { 147 | $keyword_escaped = htmlentities(preg_quote($keyword)); 148 | $newLine = @preg_replace("/$keyword_escaped/i", "".htmlentities("$0")."", $line); 149 | 150 | if ($newLine) 151 | $line = $newLine; 152 | } 153 | 154 | return $line; 155 | } 156 | 157 | } 158 | 159 | 160 | 161 | ?> -------------------------------------------------------------------------------- /lib/ListChannels.class.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/ListServers.class.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/PathValidator.class.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/Search.class.php: -------------------------------------------------------------------------------- 1 | searchResults 35 | $logDir = $baseLogDir."/".addslashes($server)."/".addslashes($channel); 36 | $dirHandle = opendir($logDir); 37 | $i = 0; 38 | while(($filename = readdir($dirHandle)) !== false) { 39 | if(substr($filename, 0, 1) != ".") { // Don't include hidden directories (i.e. beginning with a ".") 40 | if (is_file($logDir."/".$filename)) { // Only open files 41 | $searchResult = $this->searchInFile($logDir, $filename, $keywords); 42 | 43 | // Push search result into search results if any keywords match 44 | if (count($searchResult->keywords) > 0) 45 | array_push($this->searchResults, $searchResult); 46 | } 47 | } 48 | $i++; 49 | } 50 | closedir($dirHandle); 51 | 52 | // Sort all conversations by score (those with the highest score first) 53 | // TODO: Support additional sort methods and formward/reverse sorting. 54 | function sortByScore($a, $b) { 55 | if ($a->keywordScore == $b->keywordScore) 56 | return 0; 57 | 58 | return ($a->keywordScore < $b->keywordScore) ? -1 : 1; 59 | } 60 | usort($this->searchResults, "sortByScore"); 61 | $this->searchResults = array_reverse($this->searchResults); 62 | 63 | return $this->searchResults; 64 | } 65 | 66 | 67 | private function searchInFile($dir, $filename, $keywords) { 68 | 69 | // Get date from path to file (requires log file name in format along the lines of "channelname_YYYYMMDD.log" or "#channelYYYY-MM-DD.txt") 70 | $date = preg_replace('/^(.*)(\d{4})(.*?)(\d{2})(.*?)(\d{2}?)(.*?)$/', "$2-$4-$6", $filename); 71 | 72 | $searchResult = new SearchResult(); 73 | $searchResult->filename = $filename; 74 | $searchResult->startTime = ""; 75 | $searchResult->endTime = ""; 76 | 77 | // Top the keyword limit at 50 78 | $keywords = explode(" ", $keywords, 50); 79 | $keywords = array_unique($keywords); 80 | 81 | $fileHandle = fopen($dir."/".$filename, "r") or die("Unable to open IRC log file for searching."); 82 | while(!feof($fileHandle)) { 83 | $line = fgets($fileHandle); 84 | 85 | @list($time, $restOfLine) = explode(' ', $line, 2); 86 | $time = preg_replace("/[^0-9:]/", "", $time); 87 | 88 | // Completely skip over malformed lines (e.g. not HH:MM or HH:MM:SS) 89 | if (!$time) 90 | continue; 91 | if (strlen($time) < 5) 92 | continue; 93 | 94 | // Create timestamp for comparison 95 | $timestamp = strtotime($date." ".$time); 96 | 97 | // Look for matching keywords anywhere on the line (after the time) 98 | foreach ($keywords as $keyword) { 99 | 100 | $pos = @strpos(strtolower($restOfLine),strtolower($keyword)); 101 | if ($pos !== false) { 102 | // Record there was a match (adding to the keywords array if it's not already found) 103 | if (array_key_exists($keyword, $searchResult->keywords)) { 104 | $searchResult->keywords[$keyword]++; 105 | } else { 106 | $searchResult->keywords[$keyword] = 1; 107 | } 108 | 109 | // If there is no recorded timestamp, the conversation starts here! 110 | if ($searchResult->startTime == "") 111 | $searchResult->startTime = $timestamp; 112 | 113 | // Always update last timestamp when we find a keyword 114 | $searchResult->endTime = $timestamp; 115 | 116 | // Increasing the keywordScore by one for every positive match 117 | $searchResult->keywordScore++; 118 | 119 | // Check for users (not all lines will have usernames in them - e.g. some will be system messages) 120 | @list($username, $junk) = explode(' ', $restOfLine, 2); 121 | $username = preg_replace("/[^A-z0-9:_()\\|-]/", "", $username); 122 | if ($username) { 123 | // Count mentions of user 124 | if ($username != "") { 125 | if (array_key_exists($username, $searchResult->users)) { 126 | $searchResult->users[$username]++; 127 | } else { 128 | $searchResult->users[$username] = 1; 129 | } 130 | } 131 | } 132 | 133 | } 134 | } 135 | 136 | } 137 | fclose($fileHandle); 138 | 139 | if (count($searchResult->keywords) > 0) { 140 | // Duration (in seconds) 141 | $searchResult->duration = $searchResult->endTime - $searchResult->startTime; 142 | 143 | // Convert from UNIX timestamps to a human readable timestamp 144 | $searchResult->startTime = date('Y-m-d H:i', $searchResult->startTime); 145 | $searchResult->endTime = date('Y-m-d H:i', $searchResult->endTime); 146 | 147 | // Sort keyword and user arrays by frequency of mentions 148 | arsort($searchResult->keywords, SORT_NUMERIC); 149 | arsort($searchResult->users, SORT_NUMERIC); 150 | } 151 | 152 | return $searchResult; 153 | } 154 | 155 | } 156 | 157 | 158 | 159 | ?> -------------------------------------------------------------------------------- /lib/SearchResult.class.php: -------------------------------------------------------------------------------- 1 | 3; // Where 3 is the of lines they keyword "example" occured on 13 | public $keywordScore = 0; // For sorting. Assign higher rankings to better matches. 14 | 15 | public $users = array(); // e.g. $users['joe'] => 5; // Where 5 is the number of lines a user contributed 16 | 17 | } 18 | 19 | ?> -------------------------------------------------------------------------------- /lib/config.ini-dist: -------------------------------------------------------------------------------- 1 | [irc-logviewer] 2 | ; Leave value for irc_logs blank to use the default directory (logs/) 3 | ; or specify an absolute path. 4 | irc_log_dir = "../../irc-logs" 5 | --------------------------------------------------------------------------------