├── html
├── img
│ ├── favicon.ico
│ ├── glitch.png
│ ├── patreon.png
│ ├── twitterlogo.png
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── mstile-310x310.png
│ ├── patreon_white.png
│ ├── apple-touch-icon.png
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ ├── circle.svg
│ ├── menu.svg
│ ├── keyboard_arrow_up.svg
│ ├── keyboard_arrow_down.svg
│ ├── delete.svg
│ ├── error.svg
│ ├── close.svg
│ ├── messages.svg
│ ├── browserconfig.xml
│ ├── star.svg
│ ├── date.svg
│ ├── vod.svg
│ ├── edit.svg
│ ├── timeout.svg
│ ├── loop.svg
│ ├── ban.svg
│ ├── loadmore.svg
│ ├── search.svg
│ ├── manifest.json
│ ├── help.svg
│ └── safari-pinned-tab.svg
├── css
│ ├── leaderboard.css
│ ├── list.css
│ ├── material-icons.css
│ ├── settings.css
│ ├── dark.css
│ ├── animations.css
│ ├── logviewer.css
│ ├── base.css
│ └── roboto-1-3-4-5-7.css
├── contact.html
├── bower.json
├── footer.html
├── moddinghelp.html
├── connect.html
├── js
│ ├── list.js
│ ├── leaderboard.js
│ ├── modlogs.js
│ ├── index.js
│ ├── messageparser.js
│ └── settings.js
├── list.html
├── leaderboard.html
├── modlogs.html
├── tos.html
├── about.html
├── index.html
├── settings.html
└── channel.html
├── discord.js
├── LICENSE
├── appflow.txt
├── README.md
├── .gitignore
├── settings.default.json
├── package.json
├── ircbot.js
├── collections.js
├── pubsub.js
├── messagecompressor.js
├── api.js
├── db
└── mysql.js
└── bot.js
/html/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CBenni/logviewer/HEAD/html/img/favicon.ico
--------------------------------------------------------------------------------
/html/img/glitch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CBenni/logviewer/HEAD/html/img/glitch.png
--------------------------------------------------------------------------------
/html/img/patreon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CBenni/logviewer/HEAD/html/img/patreon.png
--------------------------------------------------------------------------------
/html/img/twitterlogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CBenni/logviewer/HEAD/html/img/twitterlogo.png
--------------------------------------------------------------------------------
/html/img/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CBenni/logviewer/HEAD/html/img/favicon-16x16.png
--------------------------------------------------------------------------------
/html/img/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CBenni/logviewer/HEAD/html/img/favicon-32x32.png
--------------------------------------------------------------------------------
/html/img/mstile-310x310.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CBenni/logviewer/HEAD/html/img/mstile-310x310.png
--------------------------------------------------------------------------------
/html/img/patreon_white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CBenni/logviewer/HEAD/html/img/patreon_white.png
--------------------------------------------------------------------------------
/discord.js:
--------------------------------------------------------------------------------
1 | var Discord = require('discord.js');
2 |
3 | module.exports = function(settings) {
4 |
5 | }
6 |
--------------------------------------------------------------------------------
/html/img/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CBenni/logviewer/HEAD/html/img/apple-touch-icon.png
--------------------------------------------------------------------------------
/html/img/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CBenni/logviewer/HEAD/html/img/android-chrome-192x192.png
--------------------------------------------------------------------------------
/html/img/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CBenni/logviewer/HEAD/html/img/android-chrome-512x512.png
--------------------------------------------------------------------------------
/html/img/circle.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/html/img/menu.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/html/img/keyboard_arrow_up.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/html/img/keyboard_arrow_down.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/html/css/leaderboard.css:
--------------------------------------------------------------------------------
1 | .logviewer-user-row {
2 | height: 51px;
3 | }
4 |
5 | .logviewer-user-row.even {
6 | background-color: #E3E3E6;
7 | }
8 |
9 | .leaderboardbox {
10 | max-width: 800px;
11 | }
12 |
13 | .table-header {
14 | font-weight: bold;
15 | }
--------------------------------------------------------------------------------
/html/img/delete.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | This work is licensed under the Creative Commons Attribution 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by/4.0/
2 |
3 | Permissions beyond the scope of this license may be available at business@cbenni.com
4 |
--------------------------------------------------------------------------------
/html/img/error.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/html/img/close.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/html/img/messages.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/html/img/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #da532c
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/html/img/star.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/html/img/date.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/html/img/vod.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/html/img/edit.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/html/img/timeout.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/html/img/loop.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/html/img/ban.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/html/img/loadmore.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/html/img/search.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/html/img/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Logviewer",
3 | "icons": [
4 | {
5 | "src": "\/html\/img\/android-chrome-192x192.png?v=xQoAW3RK4w",
6 | "sizes": "192x192",
7 | "type": "image\/png"
8 | },
9 | {
10 | "src": "\/html\/img\/android-chrome-512x512.png?v=xQoAW3RK4w",
11 | "sizes": "512x512",
12 | "type": "image\/png"
13 | }
14 | ],
15 | "theme_color": "#ffffff",
16 | "display": "standalone"
17 | }
18 |
--------------------------------------------------------------------------------
/html/img/help.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/html/contact.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Contact
5 |
6 | @cbenni_o
7 | cbenni
8 |
9 |
10 | Let's Kappa!
11 |
--------------------------------------------------------------------------------
/html/css/list.css:
--------------------------------------------------------------------------------
1 | .content.index-page-content {
2 | width: 75%;
3 | }
4 |
5 |
6 | .channel-list-item {
7 | display: inline-block;
8 | }
9 |
10 | .channel-live-indicator.live md-icon svg {
11 | fill: red;
12 | }
13 |
14 | h1.bigtitle {
15 | font-weight: 100;
16 | font-size: 72px;
17 | margin-bottom: 0;
18 | }
19 |
20 | h5.bigtitle {
21 | margin-top: 0;
22 | }
23 |
24 | @media (max-width:599px) {
25 | h1.bigtitle {
26 | font-size: 50px;
27 | }
28 | }
29 |
30 | a.greylink {
31 | color: #999;
32 | }
--------------------------------------------------------------------------------
/appflow.txt:
--------------------------------------------------------------------------------
1 | DISCORD
2 | grant flow
3 | auth bot -> failed attempt -> return connection link http://cbenni.com/:channel/connect/?type=discord&id=SERVERID&descr=SERVER_NAME -> click connect -> add connection object
4 |
5 | consume flow
6 | bot gets request -> gets channel -> gets connection for SERVERID -> returns logs
7 |
8 |
9 | SLACK
10 | grant flow
11 | add /command -> failed attempt -> return connection link http://cbenni.com/:channel/connect/?type=slack&descr=TEAM_DOMAIN -> enter token -> click connect -> add connection object
12 |
13 | consume flow
14 | /command is executed -> webhook fires ->
--------------------------------------------------------------------------------
/html/css/material-icons.css:
--------------------------------------------------------------------------------
1 | /* fallback */
2 | @font-face {
3 | font-family: 'Material Icons';
4 | font-style: normal;
5 | font-weight: 400;
6 | src: url(https://fonts.gstatic.com/s/materialicons/v16/2fcrYFNaTjcS6g4U3t-Y5ZjZjT5FdEJ140U2DJYC3mY.woff2) format('woff2');
7 | }
8 |
9 | .material-icons {
10 | font-family: 'Material Icons';
11 | font-weight: normal;
12 | font-style: normal;
13 | font-size: 24px;
14 | line-height: 1;
15 | letter-spacing: normal;
16 | text-transform: none;
17 | display: inline-block;
18 | white-space: nowrap;
19 | word-wrap: normal;
20 | direction: ltr;
21 | -webkit-font-feature-settings: 'liga';
22 | -webkit-font-smoothing: antialiased;
23 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # logviewer
2 | View chat logs of individuals in twitch chat
3 |
4 | ## Set up
5 | I generally discourage setting this up on your own, as you will not receive any support by me. This is an end-user product meant to run on my server, so this guide is meant for possible collaborators.
6 | 1) Install a MySQL server (or write a custom database connector)
7 | 2) Copy settings.default.json to settings.json and fill in the gaps
8 | 3) run `npm install`
9 | 4) run `node index.js`
10 |
11 | Optional:
12 | Run TMoohI and use that to proxy your connections for quicker restarts. You will have to modify [settings.json/bot/server](https://github.com/CBenni/logviewer/blob/master/settings.default.json#L16) for this to work.
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Directory for instrumented libs generated by jscoverage/JSCover
11 | lib-cov
12 |
13 | # Coverage directory used by tools like istanbul
14 | coverage
15 |
16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
17 | .grunt
18 |
19 | # node-waf configuration
20 | .lock-wscript
21 |
22 | # Compiled binary addons (http://nodejs.org/api/addons.html)
23 | build/Release
24 |
25 | # Dependency directory
26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
27 | node_modules
28 |
29 | html/bower_components/
30 | html/bower_modules/
31 | tools/
32 | settings.json
33 | run_forever
34 | mods.json
35 | *.concat.js
36 | *.concat.html
37 | *.concat.css
38 |
--------------------------------------------------------------------------------
/html/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "logviewer-website",
3 | "homepage": "https://github.com/CBenni/logviewer",
4 | "authors": [
5 | "CBenni "
6 | ],
7 | "description": "the website portion of the logviewer",
8 | "main": "index.html",
9 | "moduleType": [],
10 | "license": "MIT",
11 | "private": true,
12 | "ignore": [
13 | "**/.*",
14 | "node_modules",
15 | "bower_components",
16 | "test",
17 | "tests"
18 | ],
19 | "dependencies": {
20 | "angular-ui-router": "ui-router#^0.2.18",
21 | "angular": "^1.5.5",
22 | "angular-sanitize": "^1.5.5",
23 | "angular-animate": "^1.5.5",
24 | "angular-material": "^1.0.7",
25 | "angular-cookies": "^1.5.5",
26 | "angular-socket-io": "^0.7.0",
27 | "socket.io-client": "^1.4.6",
28 | "angular-linkify": "^1.2.0"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/settings.default.json:
--------------------------------------------------------------------------------
1 | {
2 | "database": {
3 | "type": "mysql",
4 | "host": "localhost",
5 | "user": "root",
6 | "database": "logviewer",
7 | "password": ""
8 | },
9 | "auth": {
10 | "client_id": "",
11 | "client_secret": "",
12 | "baseurl": "http://127.0.0.1:8080/",
13 | "secure_cookies": true
14 | },
15 | "bot": {
16 | "server": "irc.chat.twitch.tv:80",
17 | "nick": "",
18 | "id": 123456,
19 | "oauth": "",
20 | "modcheckinterval": 2
21 | },
22 | "index": {
23 | "html": "/html/index.html"
24 | },
25 | "logging": {
26 | "file": {
27 | "filename": "logviewer.log",
28 | "level": "info"
29 | },
30 | "console": {
31 | "level": "debug"
32 | }
33 | },
34 | "discord": {
35 | "client_id": "",
36 | "token": ""
37 | },
38 | "pubsub": {
39 | "server": "wss://pubsub-edge.twitch.tv/v1",
40 | "maxtopics": 49
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/html/footer.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "logviewer",
3 | "version": "1.0.0",
4 | "description": "Tool for keeping and retrieving logs of individuals in twitch chat",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/CBenni/logviewer.git"
12 | },
13 | "keywords": [
14 | "twitch",
15 | "chat"
16 | ],
17 | "author": "CBenni",
18 | "license": "MIT",
19 | "bugs": {
20 | "url": "https://github.com/CBenni/logviewer/issues"
21 | },
22 | "homepage": "https://github.com/CBenni/logviewer#readme",
23 | "dependencies": {
24 | "body-parser": "^1.15.0",
25 | "compression": "^1.6.0",
26 | "cookie-parser": "^1.4.1",
27 | "express": "^4.13.3",
28 | "http": "0.0.0",
29 | "lodash": "^4.13.1",
30 | "mysql": "^2.10.0",
31 | "request": "^2.72.0",
32 | "socket.io": "^1.4.4",
33 | "strftime": "^0.9.2",
34 | "tls": "0.0.1",
35 | "winston": "^2.2.0"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/html/css/settings.css:
--------------------------------------------------------------------------------
1 | md-select.inlineselect {
2 | margin: 0;
3 | display: inline-block;
4 | }
5 |
6 | .event-log {
7 | height: 30vh;
8 | max-height: 30vh;
9 | min-height: 100px;
10 | line-height: 1.5em;
11 | overflow: auto;
12 | border: 1px rgba(194,194,194, 0.4) solid;
13 | box-shadow: 0 1px 10px 0px rgba(0, 0, 0, 0.14);
14 | margin-bottom: 10px;
15 | }
16 |
17 | .event-item {
18 | padding: 0;
19 | }
20 |
21 | .event-time {
22 | font-size: small;
23 | }
24 |
25 | .event-icon {
26 | border-radius: 3px;
27 | padding: 3px;
28 | color: white;
29 | font-size: small;
30 | }
31 |
32 | .event-setting {
33 | background-color: #B94A48;
34 | }
35 |
36 | .event-level {
37 | background-color: #3A87AD;
38 | }
39 |
40 | .event-comment {
41 | background-color: #230346;
42 | }
43 |
44 | .event-system {
45 | background-color: #F5970A;
46 | }
47 |
48 | .event-channel {
49 | background-color: #468847;
50 | }
51 |
52 | .event-command {
53 | background-color: #0B3311;
54 | }
55 |
56 | .betatag {
57 | color: #FF4444;
58 | font-variant: small-caps;
59 | line-height: 0;
60 | }
61 |
62 | .settings-column {
63 | min-width: 300px;
64 | }
65 |
66 | .hint-text {
67 | font-style: italic;
68 | }
--------------------------------------------------------------------------------
/html/moddinghelp.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Why can't I enable mod logs?
4 | In order to use the mod logs feature, you need to make the user {{globalSettings.botname}} a moderator of your channel.
5 | To do that, type /mod {{globalSettings.botname}} in your channel's chat or click here .
6 | Keep in mind that it can take up to several minutes to detect moderator status. check now
7 |
8 | Why that?
9 | Moderator logs are restricted to moderators and higher, so to receive these events, the logviewer has to be a mod in your channel.
10 | IT WILL NEVER POST OR USE COMMANDS IN YOUR CHANNEL, EVER - it is just there to listen for mod logs.
11 |
12 | But what if the logviewer account gets hacked?
13 | I do my best to secure the account by all means necessary. It is Two-Factor-Authenticated and has a strong password, and the server is protected as good as I can.
14 |
15 | Let's Kappa!
16 |
17 |
--------------------------------------------------------------------------------
/html/css/dark.css:
--------------------------------------------------------------------------------
1 | html, body, a, input,
2 | .footer, md-dialog.md-default-theme, md-dialog,
3 | .footer a, .footer a:visited, .footer a:hover, .footer a:active,
4 | .contacts a, .contacts a:visited, .contacts a:hover, .contacts a:active,
5 | md-input-container .md-placeholder, md-input-container label:not(.md-no-float):not(.md-container-ignore),
6 | md-input-container.md-default-theme .md-input, md-input-container .md-input {
7 | color: white;
8 | }
9 |
10 | html, body, #main, md-dialog.md-default-theme, md-dialog {
11 | background-color: #363636;
12 | }
13 |
14 | .header, .footer {
15 | background-color: #1E1E1E;
16 | }
17 |
18 | md-input-container.md-default-theme .md-input, md-input-container .md-input,
19 | md-select.md-default-theme .md-select-value, md-select .md-select-value {
20 | border-color: rgba(255,255,255,0.12);
21 | }
22 |
23 | .logviewer-chat-text.status-msg {
24 | color: #ccc;
25 | }
26 |
27 | .logviewer-comment {
28 | background-color: #666;
29 | }
30 |
31 | .logviewer-comment:before {
32 | border-right-color: #666;
33 | }
34 |
35 | md-icon svg {
36 | fill: white;
37 | }
38 |
39 | .md-button.videobutton[disabled] md-icon svg {
40 | fill: rgba(255,255,255,0.46);
41 | }
42 |
43 | .topbanner {
44 | padding: 8px;
45 | background-color: #258920;
46 | }
47 |
48 | md-tabs .md-tab {
49 | color: rgba(255,255,255,0.54);
50 | }
51 |
52 | md-tabs .md-tab.md-active {
53 | color: white;
54 | }
55 |
56 | md-datepicker {
57 | background: none;
58 | }
59 |
60 | .md-datepicker-input {
61 | color: white;
62 | }
63 |
64 | .logviewer-user-row.even {
65 | background-color: #1E1E1E;
66 | }
--------------------------------------------------------------------------------
/html/connect.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | loading...
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | You currently do not have access to change app connections of the channel {{channel}}.
12 | If you have {{10 | aAnAccountType }} account, then you can Login with twitch
13 |
14 |
15 |
16 |
Allow {{app.name}} to access data from the channel {{channel}}?
17 | Apps have a level (just like normal users) governing what they have access to. Most apps will require access to logs and comments, so make sure to provide the app with a level that allows it to perform the features it provides.
18 |
21 |
22 | SAVE
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/html/js/list.js:
--------------------------------------------------------------------------------
1 | var logviewerApp = angular.module("logviewerApp");
2 |
3 |
4 | logviewerApp.controller("ChannelListController", function($rootScope, $scope, $http, $stateParams, $window){
5 | $scope.channels = [];
6 | $scope.channellimit = 80;
7 | $scope.channelSearch = {};
8 |
9 |
10 | // todo: dynamic channellimit based on window width+height. Average item width is ~ 130px, height 51px
11 | var calcChannellimit = function () {
12 | var availableWidth = 0.7 * window.innerWidth;
13 | var availableHeight = window.innerHeight - 450; // 450px get used on the header, footer, title, show more and search
14 |
15 | $scope.channellimit = Math.max(5, Math.floor(availableHeight * availableWidth / (130 * 51)));
16 | }
17 |
18 | angular.element($window).bind('resize', function(){
19 | $scope.$apply(function(){
20 | if(isFinite($scope.channellimit)) calcChannellimit();
21 | });
22 | });
23 | calcChannellimit();
24 |
25 | var updateChannels = function(){
26 | $http.get("/api/channels/").then(function(response) {
27 | $scope.channels = response.data;
28 | });
29 | }
30 |
31 | // update every 2 minutes
32 | var interval = setInterval(updateChannels,2*60000);
33 |
34 | $scope.$on('$destroy', function() {
35 | clearInterval(interval);
36 | });
37 | updateChannels();
38 | });
39 |
40 | logviewerApp.filter("orderChannels", function() {
41 | return function(channels) {
42 | return channels.slice().sort(function(a,b){
43 | if(a.live < b.live) return 1;
44 | if(a.live > b.live) return -1;
45 | if(a.ispremium < b.ispremium) return 1;
46 | if(a.ispremium > b.ispremium) return -1;
47 | if(a.name > b.name) return 1;
48 | if(a.name < b.name) return -1;
49 | });
50 | }
51 | });
--------------------------------------------------------------------------------
/ircbot.js:
--------------------------------------------------------------------------------
1 | var net = require('net');
2 | var events = require('events');
3 | var winston = require('winston');
4 | var parseIRCMessage = require("./messagecompressor").parseIRCMessage;
5 |
6 | var rx = /^(?:@([^ ]+) )?(?:[:](\S+) )?(\S+)(?: (?!:)(.+?))?(?: [:](.+))?$/;
7 | var rx2 = /([^=;]+)=([^;]*)/g;
8 | var rx3 = /\r\n|\r|\n/;
9 | var STATE_V3 = 1
10 | var STATE_PREFIX = 2
11 | var STATE_COMMAND = 3
12 | var STATE_PARAM = 4
13 | var STATE_TRAILING = 5
14 |
15 |
16 | function IRCBot(host, port) {
17 | var self = this;
18 | self.client = new net.Socket();
19 |
20 | self.send = function(data) {
21 | if(data.indexOf("\n") >= 0) {
22 | winston.warn("Tried to send newline character!");
23 | return;
24 | }
25 | winston.debug("--> "+data);
26 | self.client.write(data+'\n');
27 | }
28 |
29 | self._buffer = "";
30 | self.connect = function() {
31 | self.client.connect(port, host, function() {
32 | self.emit('connect');
33 | });
34 | }
35 |
36 | var buffer = new Buffer('');
37 |
38 | self.client.on('data', function(chunk) {
39 | if (typeof (chunk) === 'string') {
40 | buffer += chunk;
41 | } else {
42 | buffer = Buffer.concat([buffer, chunk]);
43 | }
44 |
45 | var lines = buffer.toString().split(rx3);
46 |
47 | if (lines.pop()) {
48 | return;
49 | } else {
50 | buffer = new Buffer('');
51 | }
52 |
53 | lines.forEach(function(line) {
54 | if(line.length>0) {
55 | var parsed = parseIRCMessage(line);
56 | parsed[STATE_COMMAND] == "PING" && self.send("PONG");
57 | try {
58 | self.emit('raw', parsed);
59 | self.emit(parsed[STATE_COMMAND], parsed);
60 | } catch (error) {
61 | winston.error(error);
62 | }
63 | }
64 | });
65 | });
66 |
67 | self.client.on('close', function() {
68 | self.emit('disconnect');
69 | });
70 |
71 |
72 | // client.destroy();
73 | }
74 | IRCBot.prototype = new events.EventEmitter;
75 | exports = module.exports = IRCBot;
--------------------------------------------------------------------------------
/html/list.html:
--------------------------------------------------------------------------------
1 |
2 |
Spoon me on GitHub
3 |
4 |
Logviewer
5 |
Denying people unbans since 2014
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | Premium channel
16 | {{channelObj.name}}
17 |
18 |
19 |
20 |
No channels found. If you have access and want to add {{channelSearch.name}} to the logviewer, click here
21 |
22 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/html/css/animations.css:
--------------------------------------------------------------------------------
1 | /* "Load more..." spinner */
2 |
3 | @-moz-keyframes logviewer-spin {
4 | from { -moz-transform: rotate(0deg); }
5 | to { -moz-transform: rotate(360deg); }
6 | }
7 | @-webkit-keyframes logviewer-spin {
8 | from { -webkit-transform: rotate(0deg); }
9 | to { -webkit-transform: rotate(360deg); }
10 | }
11 | @keyframes logviewer-spin {
12 | from {transform:rotate(0deg);}
13 | to {transform:rotate(360deg);}
14 | }
15 |
16 | .logviewer-spin-icon md-icon {
17 | transition: 2s;
18 | }
19 |
20 | .logviewer-spin-icon:hover md-icon {
21 | transform: rotate(360deg);
22 | }
23 |
24 | .logviewer-loading .logviewer-spin-icon md-icon {
25 | -moz-transition: none;
26 | -webkit-transition: none;
27 | -o-transition: all 0 none;
28 | transition: none;
29 | animation-duration: 0.1s;
30 | animation-name: logviewer-spin;
31 | animation-iteration-count: infinite;
32 | }
33 |
34 |
35 |
36 | /* "More messages above/below" infinite arrows */
37 | @keyframes logviewer-infinitearrows-up {
38 | from {transform:translateY(2em);}
39 | to {transform:translateY(-2em);}
40 | }
41 | @keyframes logviewer-infinitearrows-down {
42 | from {transform:translateY(-2em);}
43 | to {transform:translateY(2em);}
44 | }
45 |
46 | .logviewer-arrows-up:hover md-icon {
47 | animation-name: logviewer-infinitearrows-up;
48 | animation-iteration-count: infinite;
49 | animation-timing-function: linear;
50 | animation-fill-mode: forwards;
51 | animation-duration: 800ms;
52 | animation-delay: -400ms;
53 | }
54 |
55 | .logviewer-arrows-down:hover md-icon {
56 | animation-name: logviewer-infinitearrows-down;
57 | animation-iteration-count: infinite;
58 | animation-timing-function: linear;
59 | animation-fill-mode: forwards;
60 | animation-duration: 800ms;
61 | animation-delay: -400ms;
62 | }
63 |
64 | .logviewer-loading .logviewer-arrows-up md-icon {
65 | animation-name: logviewer-infinitearrows-up;
66 | animation-iteration-count: infinite;
67 | animation-timing-function: linear;
68 | animation-fill-mode: forwards;
69 | animation-duration: 100ms;
70 | animation-delay: -50ms;
71 | }
72 |
73 | .logviewer-loading .logviewer-arrows-down md-icon {
74 | animation-name: logviewer-infinitearrows-down;
75 | animation-iteration-count: infinite;
76 | animation-timing-function: linear;
77 | animation-fill-mode: forwards;
78 | animation-duration: 100ms;
79 | animation-delay: -50ms;
80 | }
--------------------------------------------------------------------------------
/html/leaderboard.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | loading...
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | You currently do not have access to change settings of the channel {{channel}}.
12 | If you have {{channelsettings.viewlogs | aAnAccountType }} account, then you can Login with twitch
13 |
14 |
15 |
16 |
17 |
18 |
Leaderboards
19 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/html/js/leaderboard.js:
--------------------------------------------------------------------------------
1 | if (!String.prototype.startsWith) {
2 | String.prototype.startsWith = function(searchString, position){
3 | position = position || 0;
4 | return this.substr(position, searchString.length) === searchString;
5 | };
6 | }
7 |
8 | logviewerApp.controller("LeaderboardController", function($scope, $http, $stateParams, $rootScope, $sce, logviewerSocket, $mdDialog, $timeout, $q){
9 | $scope.channel = $stateParams.channel.toLowerCase();
10 | $scope.channelsettings = null;
11 | $scope.userObject = null;
12 | $scope.newcomments = {};
13 | $scope.editingComment = {id:-1};
14 | $scope.loadStatus = 0;
15 | $scope.videos = [];
16 | $scope.highlights = [];
17 |
18 |
19 | $http.get("/api/channel/"+$scope.channel+"/?token="+$rootScope.auth.token).then(function(response){
20 | $scope.channelsettings = response.data.channel;
21 | $scope.userObject = response.data.me;
22 | $scope.loadStatus = response.data.channel==null?-1:-1+2*response.data.channel.active;
23 | if($stateParams.user) {
24 | $scope.addUser($stateParams.user);
25 | }
26 | }, function(response){
27 | $scope.loadStatus = -1;
28 | });
29 |
30 | // virtualRepeat model
31 | var infiniteScroller = function () {
32 | this.PAGE_SIZE = 250;
33 | this.totallength = 0;
34 | this.endofusers = null;
35 | this.topindex = 0;
36 | this.error = "";
37 | this.pages = [];
38 | }
39 |
40 | infiniteScroller.prototype.getItemAtIndex = function(offset) {
41 | var self = this;
42 | var pageindex = Math.floor(offset/this.PAGE_SIZE);
43 | // always make sure we have the next page loaded
44 | if(self.pages[pageindex+1] === undefined) {
45 | this.loadPage(pageindex+1);
46 | }
47 | var page = self.pages[pageindex];
48 | if (page) {
49 | return page[offset%this.PAGE_SIZE];
50 | } else if(page === undefined) {
51 | this.loadPage(pageindex);
52 | return null;
53 | }
54 | }
55 |
56 | infiniteScroller.prototype.getLength = function() {
57 | if(this.endofusers) return this.totallength;
58 | else return this.totallength+10;
59 | };
60 |
61 | infiniteScroller.prototype.loadPage = function(pageindex) {
62 | var self = this;
63 | console.log("Getting users page "+pageindex);
64 | self.pages[pageindex] = null;
65 | $http.get("/api/leaderboard/" + $scope.channel,{
66 | params: {
67 | offset: pageindex*this.PAGE_SIZE,
68 | limit: this.PAGE_SIZE,
69 | token: $rootScope.auth.token
70 | }
71 | }).then(function(response){
72 | console.log("Got users page "+pageindex);
73 | var page = self.pages[pageindex] = new Array(response.data.length);
74 | for(var i=0;i 0) {
78 | self.totallength = Math.max(self.totallength, pageindex*self.PAGE_SIZE+response.data.length);
79 | }
80 | if(response.data.length < self.PAGE_SIZE) {
81 | self.endofusers = true;
82 | console.log("end of users reached. Row count: "+self.totallength);
83 | }
84 | },function(response){
85 | console.log(response);
86 | });
87 | }
88 |
89 | $scope.allUsers = new infiniteScroller();
90 | });
--------------------------------------------------------------------------------
/html/js/modlogs.js:
--------------------------------------------------------------------------------
1 | logviewerApp.controller("ModLogController", function($scope, $http, $stateParams, $rootScope, $sce, logviewerSocket, $mdDialog, $timeout, $q){
2 | $scope.channel = $stateParams.channel.toLowerCase();
3 | $scope.channelsettings = null;
4 | $scope.userObject = null;
5 | $scope.newcomments = {};
6 | $scope.editingComment = {id:-1};
7 | $scope.loadStatus = 0;
8 | $scope.filters = {commands: {}, include: "", exclude: ""};
9 | $scope.commands = ["timeout","ban","unban","untimeout","slow","slowoff","subscribers","subscribersoff","followers","followersoff","emoteonly","emoteonlyoff","clear","host","unhost","commercial","mod","unmod"];
10 | for(var i=0;i<$scope.commands.length;++i) {
11 | $scope.filters.commands[$scope.commands[i]] = "1";
12 | }
13 | $scope.filters.commands.timeout = undefined;
14 |
15 |
16 | $http.get("/api/channel/"+$scope.channel+"/?token="+$rootScope.auth.token).then(function(response){
17 | $scope.channelsettings = response.data.channel;
18 | $scope.userObject = response.data.me;
19 | $scope.loadStatus = response.data.channel==null?-1:-1+2*response.data.channel.active;
20 | if($stateParams.user) {
21 | $scope.addUser($stateParams.user);
22 | }
23 | }, function(response){
24 | $scope.loadStatus = -1;
25 | });
26 |
27 |
28 | // virtualRepeat model
29 | var infiniteScroller = function (filters) {
30 | this.filters = angular.copy(filters);
31 | this.PAGE_SIZE = 250;
32 | this.totallength = 0;
33 | this.endofusers = null;
34 | this.topindex = 0;
35 | this.error = "";
36 | this.pages = [];
37 | }
38 |
39 | infiniteScroller.prototype.getItemAtIndex = function(offset) {
40 | var self = this;
41 | var pageindex = Math.floor(offset/this.PAGE_SIZE);
42 | // always make sure we have the next page loaded
43 | if(self.pages[pageindex+1] === undefined) {
44 | this.loadPage(pageindex+1);
45 | }
46 | var page = self.pages[pageindex];
47 | if (page) {
48 | return page[offset%this.PAGE_SIZE];
49 | } else if(page === undefined) {
50 | this.loadPage(pageindex);
51 | return null;
52 | }
53 | }
54 |
55 | infiniteScroller.prototype.getLength = function() {
56 | if(this.endofusers) return this.totallength;
57 | else return this.totallength+10;
58 | };
59 |
60 | infiniteScroller.prototype.loadPage = function(pageindex) {
61 | var self = this;
62 | console.log("Getting users page "+pageindex);
63 | self.pages[pageindex] = null;
64 | $http.get("/api/leaderboard/" + $scope.channel,{
65 | params: {
66 | offset: pageindex*this.PAGE_SIZE,
67 | limit: this.PAGE_SIZE,
68 | token: $rootScope.auth.token
69 | }
70 | }).then(function(response){
71 | console.log("Got users page "+pageindex);
72 | var page = self.pages[pageindex] = new Array(response.data.length);
73 | for(var i=0;i 0) {
77 | self.totallength = Math.max(self.totallength, pageindex*self.PAGE_SIZE+response.data.length);
78 | }
79 | if(response.data.length < self.PAGE_SIZE) {
80 | self.endofusers = true;
81 | console.log("end of users reached. Row count: "+self.totallength);
82 | }
83 | },function(response){
84 | console.log(response);
85 | });
86 | }
87 |
88 | $scope.allUsers = new infiniteScroller();
89 |
90 | $scope.setFilters() {
91 | $scope.allUsers = new infiniteScroller($scope.filters);
92 | }
93 | });
--------------------------------------------------------------------------------
/collections.js:
--------------------------------------------------------------------------------
1 | /*
2 | Holds an indexed collection of objects
3 | @param indexes A list of properties to be indexed. Omit to autoindex.
4 | */
5 |
6 | function IndexedCollection(indexes) {
7 | let self = this;
8 | if(indexes && !indexes.length) throw "Bad value for indexes";
9 | self.indexes = indexes;
10 | self.index = {}; // self.index[prop][value] is an array containing all elements in this IndexedCollection that have the property `prop` set to `value`
11 | self.items = [];
12 | if(indexes) for(let i=0;i item"
14 | }
15 | }
16 |
17 | /*
18 | Adds an item to the IndexedCollection
19 | @param item the object to be added
20 | */
21 | const hasOwn = Object.prototype.hasOwnProperty;
22 | IndexedCollection.prototype.add = function(item) {
23 | let self = this;
24 | if(self.indexes === undefined) {
25 | let props = Object.keys(item);
26 | for(let i=0;i
2 |
3 | loading...
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | You currently do not have access to change settings of the channel {{channel}}.
12 | If you have {{channelsettings.viewmodlogs | aAnAccountType }} account, then you can Login with twitch
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | Moderator activity log
22 |
23 |
24 |
25 |
Commands
26 |
27 | {{command}}
28 |
29 |
User
30 | Include:
42 | {{item}}
43 |
44 | Exclude:
45 |
46 |
47 |
63 |
64 |
65 |
66 |
67 | Performance analysis Coming soon
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/pubsub.js:
--------------------------------------------------------------------------------
1 | var winston = require('winston');
2 | var WebSocket = require('ws');
3 | var request = require("request");
4 | var events = require('events');
5 |
6 | function pubsub(settings, db, io) {
7 | this.settings = settings;
8 | this.db = db;
9 | this.io = io;
10 | this.connections = [];
11 | this.currenthead = 0;
12 | this.topics2conn = {};
13 | }
14 |
15 | pubsub.prototype = new events.EventEmitter;
16 | var connID = 0;
17 | function pubsubConnection(ps) {
18 | var self = this;
19 | self.ws = new WebSocket(ps.settings.pubsub.server);
20 | self.open = false;
21 | self.topics = [];
22 | self.buffer = [];
23 | self.id = connID++;
24 |
25 | self.ws.on("open", function() {
26 | winston.info("PubSub connected");
27 | self.open = true;
28 | for(var i=0;i_<
83 | connection = conn;
84 | break;
85 | }
86 | }
87 | // no connection found, try to create a new one
88 | if(!connection) {
89 | // add a new connection
90 | var connection = self.addConnection();
91 | }
92 |
93 | if(connection) {
94 | // add the topic
95 | self.topics2conn[topics[i]] = connection;
96 | connection.topics.push(topics[i]);
97 | connection.send(JSON.stringify({"type":"LISTEN","data":{"topics":[topics[i]], "auth_token": oauth}}));
98 | } else {
99 | winston.error("Pubsub limit exceeded!");
100 | }
101 | }
102 | }
103 |
104 | pubsub.prototype.unlisten = function(topics) {
105 | var self = this;
106 | for(var i=0;i{
43 | $rootScope.emote = globalEmotes[Math.floor(Math.random()*globalEmotes.length)];
44 | });
45 | });
46 | });
47 |
48 | app.controller("mainctrl", function($rootScope,$scope,$http,$location,$cookies,$stateParams,$mdDialog){
49 | $rootScope.auth = { name: $cookies.get("login")||"", token: $cookies.get("token")||"" };
50 | $scope.$stateParams = $stateParams;
51 | $rootScope.$stateParams = $stateParams;
52 | $rootScope.globalSettings = settings;
53 | $scope.login = function() {
54 | window.location.href = "https://api.twitch.tv/kraken/oauth2/authorize"
55 | +"?response_type=code"
56 | +"&client_id="+settings.auth.client_id
57 | +"&redirect_uri="+settings.auth.baseurl+"/api/login"
58 | +"&scope="
59 | +"&state="+window.location.pathname;
60 | }
61 |
62 | $scope.logout = function() {
63 | $http.get("/api/logout/?token="+$rootScope.auth.token).then(function(result) {
64 | window.location.reload();
65 | });
66 | }
67 |
68 |
69 | $scope.showDialog = function(ev, tpl) {
70 | console.log($rootScope.globalSettings);
71 | $mdDialog.show({
72 | controller: DialogController,
73 | templateUrl: tpl,
74 | parent: angular.element(document.body),
75 | targetEvent: ev,
76 | clickOutsideToClose: true
77 | });
78 | }
79 |
80 | $scope.userSettings = JSON.parse(localStorage.logviewerUserSettings ||
81 | '{"dark": false, "chat": true}'
82 | );
83 | $scope.saveMode = function() {
84 | localStorage.logviewerUserSettings = JSON.stringify($scope.userSettings);
85 | }
86 |
87 | // preload the global emotes
88 | $rootScope.globalEmotes = new Promise((r,j)=>{
89 | $http.get("https://api.twitch.tv/kraken/chat/emoticon_images?emotesets=0,33,457&client_id="+settings.auth.client_id, {cache: true}).then(function(result) {
90 | var allemotes = [];
91 | var emotesets = Object.keys(result.data.emoticon_sets);
92 | // flatten response
93 | for(var i=0;i");
97 | emoteset[j].url = "//static-cdn.jtvnw.net/emoticons/v1/" + emoteset[j].id + "/3.0"
98 | allemotes.push(emoteset[j]);
99 | }
100 | }
101 | r(allemotes);
102 | });
103 | });
104 | });
105 |
106 |
107 | function DialogController($scope, $mdDialog, $location, $http, $stateParams) {
108 | $scope.location = $location;
109 | $scope.globalSettings = settings;
110 |
111 | $scope.hide = function() {
112 | $mdDialog.hide();
113 | };
114 | $scope.cancel = function() {
115 | $mdDialog.cancel();
116 | };
117 | $scope.answer = function(answer) {
118 | $mdDialog.hide(answer);
119 | };
120 |
121 | // other functionality
122 | $scope.checkModded = function() {
123 | $http.get("/api/checkmodded/"+$stateParams.channel);
124 | }
125 | }
126 |
127 | app.directive('clickOutside', function ($document) {
128 | return {
129 | restrict: 'A',
130 | scope: {
131 | clickOutside: '&'
132 | },
133 | link: function (scope, el, attr) {
134 |
135 | $document.on('click', function (e) {
136 | if (el !== e.target && !el[0].contains(e.target)) {
137 | scope.$apply(function () {
138 | scope.$eval(scope.clickOutside);
139 | });
140 | }
141 | });
142 | }
143 | }
144 |
145 | });
--------------------------------------------------------------------------------
/html/tos.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Logviewer Terms and Conditions of Use
5 |
6 |
7 |
8 | 1. Terms
9 |
10 |
11 |
12 | By accessing this web application, you are agreeing to be bound by these
13 | web application Terms and Conditions of Use, all applicable laws and regulations,
14 | and agree that you are responsible for compliance with any applicable local
15 | laws. If you do not agree with any of these terms, you are prohibited from
16 | using or accessing this application. The materials contained in this web application are
17 | protected by applicable copyright and trade mark law.
18 |
19 |
20 |
21 | 2. Use License
22 |
23 |
24 | Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License
25 | Copyright (c) 2016 CBenni
26 |
27 | Logviewer by CBenni is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License . Based on a work at https://github.com/CBenni/logviewer . Permissions beyond the scope of this license may be available at business@cbenni.com .
28 |
29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
30 |
Third party licenses
31 |
32 | angular.js, angular-material and related libraries are released under the MIT license, stated above.
33 | Robots lovingly delivered by Robohash.org
34 |
35 | The logviewer is not affiliated with twitch.tv or any other company that provides services, APIs or software used to create this product, unless stated otherwise.
36 |
37 | Privacy Policy
38 |
39 | I use Google Analytics to track the usage of the application, purely to improve the service where possible. All information this third-party service collects is therefore anonymous.
40 | The information I gather is exclusively used to provide the service and is not used, given or sold to any third-party for any other purpose and is securely stored on my server.
41 |
42 | SLA
43 |
44 | No service license agreement is provided with this application, although I attempt to have the highest uptime I can achieve with the means I have access to.
45 |
46 | Donation Policy
47 |
48 | The logviewer is currently free for all users. I however reserve the right to sell additional features or options in a freemium model in the future, although no such system is currently planned. All monetary contributions are therefore considered donations and do not grant you any service license, uptime or liability agreement.
49 |
50 | By donating to my PayPal, you are claiming that the money being donated is your own money and you are willingly and voluntarily donating this money. You acknowledge that you are receiving no items in return for this donation, and that the money donated is non-refundable.
51 |
52 |
53 | API Policy
54 |
55 |
56 | The API is free and open for everyone to use and supports CORS and JSONP. If you use the API to integrate the logviewer into your custom software, websites, apps or services, please adhere to the rules stated below:
57 |
58 | The API is provided under a "fair-use" license. This means no api scraping, polling, DoSing, spamming, ... - try to keep each request to my API triggered by a user action. It is preferred to cache requests client-side
59 | Access to the API is restricted to purely non-commercial services and products, unless an extended license has been explicitely obtained. A user has to have access to whatever service you provide without payment, subscription or credit card information
60 | When displaying logs or stats of users, please always link back to cbenni.com - preferably with deep links like https://cbenni.com/esl_csgo/?user=cbenni
61 | The API is subject to change, and while breaking changes are rare, they might occur at any point in time - I take no responsibility or liability for changes that break your applications.
62 |
63 | If an application intentionally violates these terms, I take the liberty to permanently block it from access to my API in the manner I decide. Any future attempts to access or abuse my page, API or services will be prohibited and ultimately prosecuted.
64 |
65 |
66 |
67 |
68 | Let's Kappa!
69 |
70 |
--------------------------------------------------------------------------------
/html/css/logviewer.css:
--------------------------------------------------------------------------------
1 | /* layout */
2 | div { outline-style:none; }
3 |
4 | #chat {
5 | width: 20%;
6 | min-width: 250px;
7 | height: calc(100% - 50px);
8 | position: fixed;
9 | right: 0;
10 | padding: 0;
11 | box-sizing: border-box;
12 | overflow: hidden;
13 | }
14 |
15 | #chat iframe {
16 | height: 100%;
17 | width: 100%;
18 | }
19 |
20 | @media (max-width:960px) {
21 | #chat {
22 | display: none;
23 | }
24 | }
25 | @media (min-width:960px) {
26 | #main.largecolumn {
27 | max-width: calc(100% - 250px);
28 | }
29 | }
30 |
31 | #main.rhs {
32 | float: right;
33 | }
34 |
35 | #chat.lhs {
36 | left: 0;
37 | right: inherit;
38 | }
39 |
40 | .topbanner {
41 | padding: 8px;
42 | background-color: #53d910;
43 | text-align: center;
44 | width: 500px;
45 | margin: 0px auto;
46 | }
47 |
48 | /* user view */
49 |
50 | .userlogsview
51 | {
52 | margin: 15px 0;
53 | }
54 |
55 | .logview .userheader {
56 | max-width: 500px;
57 | }
58 |
59 | .logview .username
60 | {
61 | font-size: large;
62 | font-weight: bold;
63 | }
64 |
65 | .logview
66 | {
67 | border: 1px rgba(194,194,194, 0.4) solid;
68 | box-shadow: 0 1px 10px 0px rgba(0, 0, 0, 0.14);
69 | margin-bottom: 10px;
70 | }
71 |
72 | .btn-close:hover {
73 | color: #999;
74 | }
75 |
76 | .panel-heading {
77 | position: relative;
78 | }
79 |
80 | .userstats {
81 | text-align: center;
82 | margin: 0 5px;
83 | }
84 |
85 | .logviewer-comment .text {
86 | white-space: pre-wrap;
87 | }
88 |
89 | .writecomment {
90 | margin-top: 20px;
91 | font-size: 14px;
92 | }
93 |
94 | .writecomment md-input-container {
95 | width: 50%;
96 | min-width: 200px;
97 | }
98 |
99 | .comment-editing-buttons .editing-button {
100 | margin: 0;
101 | min-height: 0;
102 | height: 20px;
103 | min-width: 0;
104 | width: 20px;
105 | padding: 0;
106 | }
107 |
108 | .comment-editing-buttons .editing-button md-icon {
109 | font-size: 16px;
110 | height: 20px;
111 | width: 20px;
112 | }
113 |
114 | .comment-editing md-input-container {
115 | width: 100%;
116 | }
117 |
118 | .comment-editing .md-button {
119 | float: right;
120 | }
121 |
122 | /* Add animation */
123 | .logview.ng-enter.ng-enter-active,
124 | .logview.ng-leave {
125 | opacity: 1;
126 | -webkit-transition: opacity 100ms linear;
127 | -moz-transition: opacity 100ms linear;
128 | transition: opacity 100ms linear;
129 | }
130 |
131 | /* Remove animation */
132 | .logview.ng-leave.ng-leave-active,
133 | .logview.ng-enter {
134 | opacity: 0;
135 | -webkit-transition: all 100ms linear;
136 | -moz-transition: all 100ms linear;
137 | transition: all 100ms linear;
138 | }
139 |
140 | /* chat messages */
141 |
142 | /* more messages buttons */
143 | .logviewer-round-button {
144 | padding: 0px 20px;
145 | border-radius: 20px;
146 | }
147 |
148 | div.logviewer-more-messages md-button {
149 | margin-top: 0;
150 | }
151 |
152 | div.logviewer-more-messages .md-primary:hover, div.logviewer-more-messages .md-primary:focus {
153 | background-color: #2674F2 !important;
154 | }
155 |
156 | /* "More above/below" */
157 | /* chat messages */
158 |
159 | .logview .logviewer-message {
160 | font-size: 15px;
161 | }
162 |
163 | span.logviewer-time {
164 | font-size: 0.9em;
165 | color: #666;
166 | margin-right: 2px;
167 | }
168 |
169 | span.logviewer-chat-action {
170 | font-style: italic;
171 | }
172 |
173 | span.logviewer-colon {
174 | padding-right: 5px;
175 | }
176 |
177 | img.logviewer-badge {
178 | margin-right: 2px;
179 | height: 1em;
180 | }
181 |
182 | .logviewer-message {
183 | word-break: break-word;
184 | }
185 |
186 | img.emote {
187 | height: 1.5em;
188 | margin-top: -0.25em;
189 | margin-bottom: -0.25em;
190 | }
191 |
192 | .logviewer-chat-message {
193 | min-height: 20px;
194 | }
195 |
196 | .logviewer-disabled-message {
197 | opacity: 0.3;
198 | }
199 |
200 | .logviewer-selected-message {
201 | background-color: rgba(105,105,105,0.25);
202 | }
203 |
204 | .logviewer-clickable-message {
205 | cursor: pointer;
206 | }
207 |
208 | .logviewer-chat-text.status-msg {
209 | color: #333;
210 | }
211 |
212 | /* comments */
213 | .logviewer-comment {
214 | background-color: #eee;
215 | border-radius:5px;
216 | margin: 20px;
217 | padding: 5px;
218 | max-width:500px;
219 | position: relative;
220 | }
221 |
222 | .logviewer-comment img {
223 | height: 34px;
224 | width: 34px;
225 | border-radius: 25px;
226 | margin: 0 5px 0 0;
227 | }
228 |
229 | .logviewer-comment:before {
230 | content: "";
231 | position: absolute;
232 | top: 15px;
233 | left: -20px;
234 | border: 10px solid transparent;
235 | border-right-color: #eee;
236 | display: block;
237 | }
238 |
239 | .logviewer-comment .author {
240 | font-weight: bold;
241 | }
242 |
243 | .logviewer-comment .age {
244 | font-size: 12px;
245 | }
246 |
247 | a.videobutton.md-button.md-icon-button {
248 | height: 20px;
249 | margin: -3px 0;
250 | padding: 0;
251 | line-height: 0;
252 | min-height: 0;
253 | border-radius: 0;
254 | }
255 |
256 | .vod-tooltip {
257 | background-color: #333;
258 | padding: 5px;
259 | font-size: 10pt;
260 | }
261 |
262 | .md-button.videobutton md-icon svg {
263 | fill: rgb(63,81,181);
264 | }
265 |
266 | .md-button.videobutton md-icon svg {
267 | fill: rgb(63,81,181);
268 | }
269 |
270 | .md-button.videobutton.highlight-button md-icon svg {
271 | fill: #e3b338;
272 | }
273 |
274 | .md-button.videobutton[disabled] md-icon svg {
275 | fill: rgba(0,0,0,0.54);
276 | }
277 |
278 | md-tooltip .md-content.md-show, md-tooltip .md-content.md-show-add-active {
279 | height: auto;
280 | font-size: 12px;
281 | }
282 |
283 | span.twitchbot, span.logviewer-modlog-text {
284 | font-style: italic;
285 | }
--------------------------------------------------------------------------------
/html/about.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | About
5 |
6 | The logviewer is a tool, created by CBenni, that allows users to check the chat logs of individuals in twitch chat of certain channels.
7 |
8 | How to use it
9 | Simply go to the channel page you wish to check logs for, enter a name in the username box and press the "ADD" button, which adds a log view for this user.
10 |
11 | Alternatively, you can whisper the logviewer account from any twitch chat or the whisper menu to check for logs, with the following:
12 | /w logviewer #channel username
13 | where #channel is the channel name (for example, #nl_kripp) and username is the username you want to check. It will respond with the newest logs of this users and a direct link
14 |
15 |
Slack integration
16 | You can add a slash command to look up chat logs in your Slack team:
17 |
18 | Add Slacks custom slash commands app to your server
19 | Add a new configuration and call it something like lv
20 | Set the URL to {{location.protocol()+"://"+location.host()+(location.port()=="80"?"":":"+location.port())}}/api/slack/?default_channel={{$parent.$stateParams.channel || "YOUR_CHANNEL_NAME"}} and change the method to GET
21 | If the channel doesn't have logs readable by "everyone" (see the settings settings ), then add a token to the URL. Contact me for details.
22 | Customize the command as you like
23 | Save
24 |
25 | Usage
26 | To check a users logs, you can now use the slash command you just created, for example:
27 | /lv username - gets the last 10 lines of log for the user username in the default channel
28 | /lv username 3 - gets the last 3 lines of log for the user username in the default channel
29 | /lv username channel - gets the last 10 lines of log for the user username in the channel channel
30 | /lv username channel 3 - gets the last 3 lines of log for the user username in the channel channel
31 | Keep in mind only you can see the response!
32 | Discord integration
33 | Coming soon™
34 |
35 | FAQ
36 |
37 | I want my channel to be logged, can you add me?
38 | You can add your channel yourself:
39 | Go to your settings and enable it!
40 | Log in, go to your settings and enable it!
41 |
42 | I want another channel to be logged, can you add it?
43 | I do not log channels without explicit permission by the broadcaster or someone entitled to give permission (this includes official channel managers, group members for shared channels, but usually excludes moderators)
44 | If you fulfill this condition but do not have access to the channel account (in which case you can add it yourself), you can contact me.
45 |
46 | How long are logs saved?
47 | As long as my server costs are covered by the voluntary donations. Contact me if you want to make an agreement on permanent log storage.
48 |
49 | A certain users logs don't show up, what happened?
50 | There are multiple things that can result in this:
51 |
52 | The user hasn't talked in chat while the logviewer was enabled in the channel.
53 | The user hasn't talked for too long and his logs have been deleted.
54 | A downtime has caused certain messages not to be recorded.
55 | Twitch chat has been known to "drop" messages, albeit this has become very rare in recent times.
56 |
57 |
58 | Can you add/improve a feature?
59 | I am always happy to listen to feedback, requests and suggestions, so please contact me!
60 |
61 | Can you remove a feature?
62 | The feature set of the logviewer has been carefully designed and chosen, and in most cases, features are built-in for good. If you feel strongly about something, contact me and I might add a toggle for it.
63 |
64 | Can you import my old logs (from another page)?
65 | I have an importer from overrustlelogs, so if you have logs to be imported from there, contact me about it. If it is some other kind of log, talk to me, but don't expect me to write a parser for all kinds of logs.
66 |
67 | Why is the user "logviewer" in my channel? Do I need to mod it?
68 | The chat logger connects via twitch chat using the account "logviewer" (and no other username or variation of it). It currently only needs moderator permissions if you want to use the "mod logs" feature and will never talk in chat. It is there to collect logs and receive and store your moderator list as well as notify you of its presence in the user list. DO NOT BAN IT , as this will break the bot for you.
69 | It is protected by Two-Factor-Authentication, meaning it is essentially impossible that it will ever get hacked. If you notice suspicious activity, please notify me immediately.
70 |
71 | That icon is great! Who made it?
72 | The icon was made by whsky - I love it!
73 |
74 |
75 |
76 |
77 |
78 | Let's Kappa!
79 |
--------------------------------------------------------------------------------
/html/css/base.css:
--------------------------------------------------------------------------------
1 | #main {
2 | float: left;
3 | width: 100%;
4 | min-height: calc(100% - 50px);
5 | }
6 |
7 | #main.largecolumn {
8 | width: 80%;
9 | }
10 |
11 | @media (max-width:960px) {
12 | #main.largecolumn {
13 | width: 100%;
14 | }
15 | }
16 |
17 | .content {
18 | width: 85%;
19 | margin-bottom: 20px;
20 | }
21 |
22 | @media (max-width:1200px) {
23 | .content {
24 | float: none;
25 | width: 90%;
26 | }
27 | }
28 |
29 | .panel {
30 | border: 0;
31 | }
32 |
33 |
34 | ::-webkit-scrollbar {
35 | max-width: 10px;
36 | min-width: 4px;
37 | width: 1%;
38 | height: 4px;
39 | transition: all 0.1s linear;
40 | border-radius: 50%;
41 | }
42 |
43 |
44 | ::-webkit-scrollbar-track {
45 | background: rgba(0, 0, 0, 0);
46 | }
47 |
48 |
49 | ::-webkit-scrollbar-track:hover {
50 | background: rgba(0, 0, 0, .2);
51 | }
52 |
53 |
54 | ::-webkit-scrollbar-thumb {
55 | background: #a5abb1;
56 | }
57 |
58 |
59 | [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
60 | display: none !important;
61 | }
62 |
63 |
64 |
65 | .md-button.md-raised.twitch-button {
66 | background-color: #6441a5;
67 | color: white;
68 | }
69 |
70 | .md-button.md-raised:not([disabled]):hover.twitch-button {
71 | background-color: #B9A3E3;
72 | }
73 |
74 | .header {
75 | height: 50px;
76 | width: 100%;
77 | background-color: #19191F;
78 | }
79 |
80 | .header a, .header a:hover, .header a:visited, .header a:active {
81 | text-decoration: none;
82 | margin: 0 10px;
83 | font-size: 20px;
84 | opacity: 0.7;
85 | color: white;
86 | }
87 |
88 | .header a.active {
89 | opacity: 1;
90 | }
91 |
92 | .usermenu {
93 | background-color: #19191F;
94 | position: absolute;
95 | top: 50px;
96 | left: 0;
97 | padding: 8px 15px;;
98 | width: 200px;
99 | z-index: 999;
100 | color: white;
101 | }
102 |
103 | .fab-nobackground {
104 | background-color: transparent !important;
105 | }
106 |
107 |
108 | .nobottom .md-errors-spacer, md-autocomplete[md-floating-label] md-input-container .md-errors-spacer {
109 | display: none;
110 | }
111 |
112 | md-input-container.nobottom {
113 | margin-bottom: 0;
114 | padding-bottom: 0;
115 | }
116 |
117 | md-input-container.notop {
118 | margin-top: 0;
119 | }
120 |
121 | .md-button.md-small {
122 | line-height: 20px;
123 | min-height: 0;
124 | min-width: 0;
125 | font-size: 12px;
126 | padding: 5px 10px;
127 | }
128 |
129 | md-icon svg {
130 | fill: rgba(0,0,0,0.54);
131 | }
132 |
133 | .md-button.md-primary.md-raised md-icon svg {
134 | fill: white;
135 | }
136 |
137 | .menu-button md-icon svg {
138 | fill: white;
139 | }
140 |
141 | md-checkbox.darkbg .md-icon {
142 | border-color: rgba(255,255,255,0.54);
143 | }
144 |
145 | .hide-gt-s {
146 | display: none;
147 | }
148 | @media (max-width:700px) {
149 | .md-button.md-small-xs {
150 | line-height: 20px;
151 | min-height: 0;
152 | min-width: 0;
153 | font-size: 12px;
154 | padding: 5px 10px;
155 | }
156 |
157 | .header a, .header a:hover, .header a:visited, .header a:active {
158 | margin: 0 7px;
159 | font-size: 15px;
160 | }
161 |
162 | .hide-s {
163 | display: none;
164 | }
165 |
166 | .hide-gt-s {
167 | display: inherit;
168 | }
169 | }
170 |
171 | div.big-error-icon md-icon {
172 | font-size: 60px;
173 | }
174 |
175 | div.big-error-icon md-icon svg {
176 | fill: #c00;
177 | }
178 |
179 | div.big-error-icon {
180 | width: 60px;
181 | height: 60px;
182 | margin: 10px;
183 | }
184 |
185 | div.big-error-icon md-icon svg {
186 | width: 60px;
187 | height: 60px;
188 | }
189 |
190 | /* footer */
191 | div[ng-include] {
192 | width: 100%;
193 | }
194 |
195 | .footer {
196 | width: 100%;
197 | background-color: white;
198 | height: 36px;
199 | }
200 |
201 | .footer a, .footer a:visited, .footer a:hover, .footer a:active {
202 | text-decoration: none;
203 | color: #66757f;
204 | font-weight: bold;
205 | font-family: 'Open Sans', sans-serif;
206 | }
207 |
208 | .donate-btn {
209 | height: 100%;
210 | padding: 0 6px;
211 | }
212 |
213 | .donate-btn a {
214 | height: 21px;
215 | }
216 |
217 | .donate-btn a img {
218 | height: 100%;
219 | }
220 |
221 | .pixel {
222 | display: none;
223 | }
224 |
225 | .contacts a, .contacts a:visited, .contacts a:hover, .contacts a:active {
226 | text-decoration: none;
227 | color: #66757f;
228 | font-weight: bold;
229 | font-family: 'Open Sans', sans-serif;
230 | }
231 |
232 | .rightbutton {
233 | float: right;
234 | margin: 0;
235 | padding 0;
236 | }
237 |
238 | .closedialog {
239 | display: block;
240 | order: 2;
241 | }
242 |
243 | div.inline {
244 | display: inline-block;
245 | }
246 |
247 | img.logo {
248 | padding: 0;
249 | width: 20px;
250 | vertical-align: middle;
251 | }
252 |
253 | #forkongithub a {
254 | background: #000;
255 | color: #fff;
256 | text-decoration: none;
257 | text-align: center;
258 | font-weight: bold;
259 | padding: 5px 40px;
260 | font-size: 1rem;
261 | line-height: 2rem;
262 | position: relative;
263 | transition: 0.5s;
264 | }
265 |
266 | #forkongithub a:hover {
267 | background: #c11;
268 | color: #fff;
269 | }
270 |
271 | #forkongithub a::before, #forkongithub a::after {
272 | content: "";
273 | width: 100%;
274 | display: block;
275 | position: absolute;
276 | top: 1px;
277 | left: 0;
278 | height: 1px;
279 | background: #fff;
280 | }
281 |
282 | #forkongithub a::after {
283 | bottom: 1px;
284 | top: auto;
285 | }
286 |
287 | @media screen and (min-width: 700px) {
288 | #forkongithub {
289 | position: absolute;
290 | display: block;
291 | left: 0;
292 | width: 200px;
293 | overflow: hidden;
294 | height: 200px;
295 | }
296 |
297 | #forkongithub a {
298 | width: 200px;
299 | position: absolute;
300 | top: 60px;
301 | left: -60px;
302 | transform: rotate(-45deg);
303 | -webkit-transform: rotate(-45deg);
304 | -ms-transform: rotate(-45deg);
305 | -moz-transform: rotate(-45deg);
306 | -o-transform: rotate(-45deg);
307 | box-shadow: 4px 4px 10px rgba(0,0,0,0.8);
308 | }
309 | }
310 |
311 | .leftpad {
312 | padding-left: 8px;
313 | }
--------------------------------------------------------------------------------
/html/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Logviewer
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
46 |
47 |
48 |
49 |
50 |
76 |
77 |
78 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
--------------------------------------------------------------------------------
/html/settings.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | loading...
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | You currently do not have access to change settings of the channel {{channel}}.
12 | If you have {{10 | aAnAccountType }} account, then you can Login with twitch
13 |
14 |
15 |
16 |
17 |
Event log
18 |
19 |
20 | {{event.time | secondsTimestamp | date : 'yyyy/MM/dd hh:mm a'}} {{event.action}} {{event.user}} {{event.desc}}
21 |
22 |
23 | No items found.
24 |
25 |
26 |
27 |
28 |
29 |
Settings
30 | Enable the logviewer
31 | Enable moderator logs beta
32 |
33 |
34 |
35 | Permissions
36 |
37 | everyone
38 | logged in users
39 | regulars
40 | moderators
41 | super-moderators
42 | managers
43 | can read logs
44 |
45 | everyone
46 | logged in users
47 | regulars
48 | moderators
49 | super-moderators
50 | managers
51 | can view mod logs
52 |
53 | everyone
54 | logged in users
55 | regulars
56 | moderators
57 | super-moderators
58 | managers
59 | can read comments
60 |
61 | logged in users
62 | regulars
63 | moderators
64 | super-moderators
65 | managers
66 | can write comments
67 |
68 | logged in users
69 | regulars
70 | moderators
71 | super-moderators
72 | managers
73 | can delete comments
74 |
75 |
76 |
107 |
108 |
109 |
110 | SAVE
111 |
112 |
113 |
114 |
115 |
--------------------------------------------------------------------------------
/html/js/messageparser.js:
--------------------------------------------------------------------------------
1 | var rx = /^(?:@([^ ]+) )?(?:[:](\S+) )?(\S+)(?: (?!:)(.+?))?(?: [:](.+))?$/;
2 | var rx2 = /([^=;]+)=([^;]*)/g;
3 | var STATE_V3 = 1;
4 | var STATE_PREFIX = 2;
5 | var STATE_COMMAND = 3;
6 | var STATE_PARAM = 4;
7 | var STATE_TRAILING = 5;
8 | function parseIRCMessage(message) {
9 | var data = rx.exec(message);
10 | var tagdata = data[STATE_V3];
11 | if (tagdata) {
12 | var tags = {};
13 | do {
14 | m = rx2.exec(tagdata);
15 | if (m) {
16 | tags[m[1]] = m[2];
17 | }
18 | } while (m);
19 | data[STATE_V3] = tags;
20 | }
21 | return data;
22 | }
23 |
24 | function splitWithTail(str,delim,count){
25 | var parts = str.split(delim);
26 | var tail = parts.slice(count).join(delim);
27 | var result = parts.slice(0,count);
28 | result.push(tail);
29 | return result;
30 | }
31 |
32 | function getPrivmsgInfo(parsedmessage) {
33 | var tags = parsedmessage[STATE_V3] || {};
34 | var nick = parsedmessage[STATE_PREFIX].match(/(\w+)/)[1];
35 | var channel = parsedmessage[STATE_PARAM];
36 | var badges = [];
37 | if(tags.badges) badges = tags.badges.split(",");
38 |
39 | var text = parsedmessage[STATE_TRAILING];
40 | var isaction = false;
41 | var actionmatch = /^\u0001ACTION (.*)\u0001$/.exec(text);
42 | if(actionmatch != null) {
43 | isaction = true;
44 | text = actionmatch[1];
45 | }
46 | var emoteparsertext = text+"";
47 | var surrogates = [];
48 |
49 | for (var i = 0; i < emoteparsertext.length; ++i) {
50 | var charcode = emoteparsertext.charCodeAt(i);
51 | if (charcode <= 0xDBFF && charcode >= 0xD800) {
52 | surrogates.push([charcode, emoteparsertext.charCodeAt(i + 1)]);
53 | ++i;
54 | }
55 | }
56 | // Replace surrogates while calculating emotes
57 | for (var i = 0; i < surrogates.length; ++i) {
58 | emoteparsertext = emoteparsertext.replace(String.fromCharCode(surrogates[i][0], surrogates[i][1]), String.fromCharCode(0xE000 + i));
59 | }
60 |
61 |
62 | var emotes = [];
63 | if(tags["emotes"]) {
64 | var emotelists = tags["emotes"].split("/");
65 | for(var i=0;i": ">",
106 | '"': '"',
107 | "'": ''',
108 | "/": '/'
109 | };
110 |
111 | function escapeHtml(string) {
112 | return String(string).replace(/[&<>"'\/]/g, function (s) {
113 | return entityMap[s];
114 | });
115 | }
116 |
117 | DEFAULTCOLORS = ['#e391b8', '#e091ce', '#da91de', '#c291db', '#ab91d9', '#9691d6', '#91a0d4', '#91b2d1', '#91c2cf', '#91ccc7', '#91c9b4', '#90c7a2', '#90c492', '#9dc290', '#aabf8f', '#b5bd8f', '#bab58f', '#b8a68e', '#b5998e', '#b38d8d']
118 | function renderMessage(messageinfo, badges) {
119 | var result = ""
120 | for(var i=0;i'
126 | }
127 | }
128 | }
129 |
130 | var color = messageinfo.tags["color"];
131 | if(color == "") {
132 | color = DEFAULTCOLORS[sdbmCode(messageinfo.nick)%(DEFAULTCOLORS.length)];
133 | }
134 | var display_name = messageinfo.tags["display-name"];
135 | if(display_name == "") {
136 | display_name = messageinfo.nick;
137 | }
138 |
139 |
140 | var message = messageinfo.textWithSurrogatesInPUAs;
141 | if(display_name == "jtv" || display_name == "twitchnotify" || display_name == "Twitchnotify") {
142 | result += ''
143 | }
144 | else if (display_name == "twitchbot") {
145 | result += 'Message was flagged for moderator attention: '
146 | }
147 | else if(messageinfo.isaction) {
148 | result += ''+display_name+' '
149 | }
150 | else {
151 | result += ''+display_name+' : ';
152 | }
153 |
154 |
155 | // replace emotes
156 |
157 | // sort in descending order
158 | var emotes = messageinfo.emotes.sort(function(a,b){ return a.start-b.start; });
159 | var position = 0;
160 | for(var i=0;i';
165 | }
166 | result += escapeHtml(message.substring(position));
167 | // close span tag
168 | result += ' ';
169 |
170 |
171 | // Put surrogate pairs back in
172 | for (var i = 0; i < messageinfo.surrogates.length; ++i) {
173 | result = result.replace(String.fromCharCode(0xE000 + i), String.fromCharCode(messageinfo.surrogates[i][0], messageinfo.surrogates[i][1]));
174 | }
175 | result = result.replace(/[\uE000-\uF8FF]/g, function (x) {
176 | return String.fromCharCode(0xD800 + (x.charCodeAt(0) - 0xE000));
177 | });
178 |
179 | return result;
180 | }
181 |
--------------------------------------------------------------------------------
/messagecompressor.js:
--------------------------------------------------------------------------------
1 | var winston = require('winston');
2 |
3 | var TAGS = 1
4 | var PREFIX = 2
5 | var COMMAND = 3
6 | var PARAM = 4
7 | var TRAILING = 5
8 |
9 | function toTitleCase(str)
10 | {
11 | return str.replace(/([^a-z0-9]|\b)[a-z]/g,function(x){return x.toUpperCase()});
12 | }
13 |
14 | function compressEmotes(emotes) {
15 | /*
16 | * Compresses the emotes tag
17 | * 2:0-1,3-4,12-13,18-19,21-22
18 | * is compressed to
19 | * 2:1,0,3,12,18,21
20 | * so the format is
21 | * emote1id:length1,start1,start2,start3:length2,start4,start5/emote2id:length3,start6
22 | */
23 | var res = "";
24 | var emotesplit = emotes.split("/");
25 | for(var i=0;i
97 | * where ircv3info is
98 | *
99 | * with emotes compressed
100 | */
101 | var res = "";
102 | if(data[TAGS]) {
103 | var defaults = {"color":"","emotes":"","display-name":toTitleCase(user),"badges":""};
104 | for(var i=0;i 0) {
63 | return true;
64 | }
65 | var changedlevels = getListChanges(oldlevels,$scope.levels);
66 | return changedlevels.length!=0;
67 | }
68 |
69 | var errorToast = function(reason) {
70 | var reasonString = {
71 | 403: "Error: Access denied.",
72 | 404: "Error: Channel not found."
73 | }[reason.status] || "An unknown error occurred. Please try again later.";
74 | $mdToast.show($mdToast.simple({
75 | parent: "#main",
76 | textContent: reasonString,
77 | position: "top right",
78 | hideDelay: 3000
79 | }));
80 | };
81 |
82 | var saveSettings = function() {
83 | var changedsettings = getChanges(oldsettings,$scope.settings);
84 | var changed = [];
85 | if(Object.keys(changedsettings).length > 0) {
86 | changed.push("settings");
87 | return new Promise(function(r,j) {
88 | $http.post("/api/settings/"+$stateParams.channel, {token: $rootScope.auth.token, settings: changedsettings}).then(function(){r(changed);},j);
89 | });
90 | } else {
91 | return Promise.resolve(changed);
92 | }
93 | }
94 |
95 | var saveLevels = function(changed) {
96 | var changedlevels = getListChanges(oldlevels,$scope.levels);
97 | if(changedlevels.length!=0) {
98 | changed.push("levels");
99 | return new Promise(function(r,j) {
100 | $http.post("/api/levels/"+$stateParams.channel, {token: $rootScope.auth.token, levels: changedlevels}).then(function(){r(changed);},j);
101 | });
102 | } else {
103 | return Promise.resolve(changed);
104 | }
105 | }
106 |
107 | $scope.save = function() {
108 | saveSettings()
109 | .then(saveLevels)
110 | .then(function(changed){
111 | oldsettings = angular.copy($scope.settings);
112 | oldlevels = angular.copy($scope.levels);
113 | $mdToast.show($mdToast.simple({
114 | parent: "#main",
115 | textContent: changed.join(" and ") + " saved",
116 | position: "top right",
117 | hideDelay: 3000
118 | }));
119 | }, errorToast);
120 | }
121 |
122 | $scope.addEmptyRow = function() {
123 | if($scope.levels.length > 0) {
124 | var lastrow = $scope.levels[$scope.levels.length - 1];
125 | if(lastrow.nick != "") {
126 | $scope.levels.push({nick: "", level: 0});
127 | }
128 | }
129 | else {
130 | $scope.levels.push({nick: "", level: 0});
131 | }
132 | }
133 |
134 | $scope.events = [];
135 |
136 | var prettyToggle = {
137 | "active": "the logviewer",
138 | "modlogs": "mod logs"
139 | };
140 |
141 | var prettyPerm = {
142 | "viewlogs": "view logs",
143 | "viewmodlogs": "view mod logs",
144 | "viewcomments": "view comments",
145 | "writecomments": "write comments",
146 | "deletecomments": "delete comments"
147 | };
148 |
149 | var userGroup = {
150 | "-10": "banned",
151 | 0: "everyone",
152 | 1: "logged in user",
153 | 2: "regular",
154 | 5: "moderator",
155 | 7: "super-moderator",
156 | 10: "manager",
157 | 50: "admin",
158 | 1337: "developer"
159 | };
160 | var commentAction = {
161 | "add": "added",
162 | "edit": "edited",
163 | "delete": "deleted"
164 | };
165 | var eventParser = {
166 | "channel": function(event) {
167 | if(event.name == "add") return "added the logviewer to the channel.";
168 | },
169 | "setting": function(event) {
170 | if(prettyToggle[event.name]) {
171 | if(event.data == "1") {
172 | return "enabled "+prettyToggle[event.name]+"."
173 | } else {
174 | return "disabled "+prettyToggle[event.name]+"."
175 | }
176 | } else if(prettyPerm[event.name]) {
177 | return "set "+prettyPerm[event.name]+" to "+(userGroup[event.data] || event.data)+(event.data > 0?"s and higher only":"");
178 | }
179 | },
180 | "level": function(event) {
181 | return "set the user level of "+event.name+" to "+userGroup[event.data];
182 | },
183 | "comment": function(event) {
184 | var comment = JSON.parse(event.data);
185 | if(comment.author != event.user) {
186 | return commentAction[event.name]+" a comment by "+comment.author;
187 | }
188 | },
189 | "system": function(event) {
190 | return event.data;
191 | },
192 | "command": function(event) {
193 | return "used command "+event.data;
194 | }
195 | }
196 |
197 | var addEvent = function(ev) {
198 | if(eventParser[ev.action]) {
199 | var desc = eventParser[ev.action](ev);
200 | if(desc) {
201 | $scope.events.push({action: ev.action, user: ev.user, desc: desc, time: ev.time});
202 | }
203 | }
204 | }
205 |
206 | $http.jsonp("/api/events/"+$stateParams.channel+"/?token="+$rootScope.auth.token+"&callback=JSON_CALLBACK").then(function(result) {
207 | $scope.events = [];
208 | for(var i=0;i
2 |
4 |
7 |
8 | Created by potrace 1.11, written by Peter Selinger 2001-2013
9 |
10 |
12 |
129 |
130 |
132 |
133 |
135 |
137 |
138 |
140 |
141 |
142 |
143 |
144 |
--------------------------------------------------------------------------------
/html/css/roboto-1-3-4-5-7.css:
--------------------------------------------------------------------------------
1 | /* cyrillic-ext */
2 | @font-face {
3 | font-family: 'Roboto';
4 | font-style: normal;
5 | font-weight: 100;
6 | src: local('Roboto Thin'), local('Roboto-Thin'), url(https://fonts.gstatic.com/s/roboto/v15/ty9dfvLAziwdqQ2dHoyjphTbgVql8nDJpwnrE27mub0.woff2) format('woff2');
7 | unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
8 | }
9 | /* cyrillic */
10 | @font-face {
11 | font-family: 'Roboto';
12 | font-style: normal;
13 | font-weight: 100;
14 | src: local('Roboto Thin'), local('Roboto-Thin'), url(https://fonts.gstatic.com/s/roboto/v15/frNV30OaYdlFRtH2VnZZdhTbgVql8nDJpwnrE27mub0.woff2) format('woff2');
15 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
16 | }
17 | /* greek-ext */
18 | @font-face {
19 | font-family: 'Roboto';
20 | font-style: normal;
21 | font-weight: 100;
22 | src: local('Roboto Thin'), local('Roboto-Thin'), url(https://fonts.gstatic.com/s/roboto/v15/gwVJDERN2Amz39wrSoZ7FxTbgVql8nDJpwnrE27mub0.woff2) format('woff2');
23 | unicode-range: U+1F00-1FFF;
24 | }
25 | /* greek */
26 | @font-face {
27 | font-family: 'Roboto';
28 | font-style: normal;
29 | font-weight: 100;
30 | src: local('Roboto Thin'), local('Roboto-Thin'), url(https://fonts.gstatic.com/s/roboto/v15/aZMswpodYeVhtRvuABJWvBTbgVql8nDJpwnrE27mub0.woff2) format('woff2');
31 | unicode-range: U+0370-03FF;
32 | }
33 | /* vietnamese */
34 | @font-face {
35 | font-family: 'Roboto';
36 | font-style: normal;
37 | font-weight: 100;
38 | src: local('Roboto Thin'), local('Roboto-Thin'), url(https://fonts.gstatic.com/s/roboto/v15/VvXUGKZXbHtX_S_VCTLpGhTbgVql8nDJpwnrE27mub0.woff2) format('woff2');
39 | unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB;
40 | }
41 | /* latin-ext */
42 | @font-face {
43 | font-family: 'Roboto';
44 | font-style: normal;
45 | font-weight: 100;
46 | src: local('Roboto Thin'), local('Roboto-Thin'), url(https://fonts.gstatic.com/s/roboto/v15/e7MeVAyvogMqFwwl61PKhBTbgVql8nDJpwnrE27mub0.woff2) format('woff2');
47 | unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
48 | }
49 | /* latin */
50 | @font-face {
51 | font-family: 'Roboto';
52 | font-style: normal;
53 | font-weight: 100;
54 | src: local('Roboto Thin'), local('Roboto-Thin'), url(https://fonts.gstatic.com/s/roboto/v15/2tsd397wLxj96qwHyNIkxPesZW2xOQ-xsNqO47m55DA.woff2) format('woff2');
55 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
56 | }
57 | /* cyrillic-ext */
58 | @font-face {
59 | font-family: 'Roboto';
60 | font-style: normal;
61 | font-weight: 300;
62 | src: local('Roboto Light'), local('Roboto-Light'), url(https://fonts.gstatic.com/s/roboto/v15/0eC6fl06luXEYWpBSJvXCBJtnKITppOI_IvcXXDNrsc.woff2) format('woff2');
63 | unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
64 | }
65 | /* cyrillic */
66 | @font-face {
67 | font-family: 'Roboto';
68 | font-style: normal;
69 | font-weight: 300;
70 | src: local('Roboto Light'), local('Roboto-Light'), url(https://fonts.gstatic.com/s/roboto/v15/Fl4y0QdOxyyTHEGMXX8kcRJtnKITppOI_IvcXXDNrsc.woff2) format('woff2');
71 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
72 | }
73 | /* greek-ext */
74 | @font-face {
75 | font-family: 'Roboto';
76 | font-style: normal;
77 | font-weight: 300;
78 | src: local('Roboto Light'), local('Roboto-Light'), url(https://fonts.gstatic.com/s/roboto/v15/-L14Jk06m6pUHB-5mXQQnRJtnKITppOI_IvcXXDNrsc.woff2) format('woff2');
79 | unicode-range: U+1F00-1FFF;
80 | }
81 | /* greek */
82 | @font-face {
83 | font-family: 'Roboto';
84 | font-style: normal;
85 | font-weight: 300;
86 | src: local('Roboto Light'), local('Roboto-Light'), url(https://fonts.gstatic.com/s/roboto/v15/I3S1wsgSg9YCurV6PUkTORJtnKITppOI_IvcXXDNrsc.woff2) format('woff2');
87 | unicode-range: U+0370-03FF;
88 | }
89 | /* vietnamese */
90 | @font-face {
91 | font-family: 'Roboto';
92 | font-style: normal;
93 | font-weight: 300;
94 | src: local('Roboto Light'), local('Roboto-Light'), url(https://fonts.gstatic.com/s/roboto/v15/NYDWBdD4gIq26G5XYbHsFBJtnKITppOI_IvcXXDNrsc.woff2) format('woff2');
95 | unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB;
96 | }
97 | /* latin-ext */
98 | @font-face {
99 | font-family: 'Roboto';
100 | font-style: normal;
101 | font-weight: 300;
102 | src: local('Roboto Light'), local('Roboto-Light'), url(https://fonts.gstatic.com/s/roboto/v15/Pru33qjShpZSmG3z6VYwnRJtnKITppOI_IvcXXDNrsc.woff2) format('woff2');
103 | unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
104 | }
105 | /* latin */
106 | @font-face {
107 | font-family: 'Roboto';
108 | font-style: normal;
109 | font-weight: 300;
110 | src: local('Roboto Light'), local('Roboto-Light'), url(https://fonts.gstatic.com/s/roboto/v15/Hgo13k-tfSpn0qi1SFdUfVtXRa8TVwTICgirnJhmVJw.woff2) format('woff2');
111 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
112 | }
113 | /* cyrillic-ext */
114 | @font-face {
115 | font-family: 'Roboto';
116 | font-style: normal;
117 | font-weight: 400;
118 | src: local('Roboto'), local('Roboto-Regular'), url(https://fonts.gstatic.com/s/roboto/v15/ek4gzZ-GeXAPcSbHtCeQI_esZW2xOQ-xsNqO47m55DA.woff2) format('woff2');
119 | unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
120 | }
121 | /* cyrillic */
122 | @font-face {
123 | font-family: 'Roboto';
124 | font-style: normal;
125 | font-weight: 400;
126 | src: local('Roboto'), local('Roboto-Regular'), url(https://fonts.gstatic.com/s/roboto/v15/mErvLBYg_cXG3rLvUsKT_fesZW2xOQ-xsNqO47m55DA.woff2) format('woff2');
127 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
128 | }
129 | /* greek-ext */
130 | @font-face {
131 | font-family: 'Roboto';
132 | font-style: normal;
133 | font-weight: 400;
134 | src: local('Roboto'), local('Roboto-Regular'), url(https://fonts.gstatic.com/s/roboto/v15/-2n2p-_Y08sg57CNWQfKNvesZW2xOQ-xsNqO47m55DA.woff2) format('woff2');
135 | unicode-range: U+1F00-1FFF;
136 | }
137 | /* greek */
138 | @font-face {
139 | font-family: 'Roboto';
140 | font-style: normal;
141 | font-weight: 400;
142 | src: local('Roboto'), local('Roboto-Regular'), url(https://fonts.gstatic.com/s/roboto/v15/u0TOpm082MNkS5K0Q4rhqvesZW2xOQ-xsNqO47m55DA.woff2) format('woff2');
143 | unicode-range: U+0370-03FF;
144 | }
145 | /* vietnamese */
146 | @font-face {
147 | font-family: 'Roboto';
148 | font-style: normal;
149 | font-weight: 400;
150 | src: local('Roboto'), local('Roboto-Regular'), url(https://fonts.gstatic.com/s/roboto/v15/NdF9MtnOpLzo-noMoG0miPesZW2xOQ-xsNqO47m55DA.woff2) format('woff2');
151 | unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB;
152 | }
153 | /* latin-ext */
154 | @font-face {
155 | font-family: 'Roboto';
156 | font-style: normal;
157 | font-weight: 400;
158 | src: local('Roboto'), local('Roboto-Regular'), url(https://fonts.gstatic.com/s/roboto/v15/Fcx7Wwv8OzT71A3E1XOAjvesZW2xOQ-xsNqO47m55DA.woff2) format('woff2');
159 | unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
160 | }
161 | /* latin */
162 | @font-face {
163 | font-family: 'Roboto';
164 | font-style: normal;
165 | font-weight: 400;
166 | src: local('Roboto'), local('Roboto-Regular'), url(https://fonts.gstatic.com/s/roboto/v15/CWB0XYA8bzo0kSThX0UTuA.woff2) format('woff2');
167 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
168 | }
169 | /* cyrillic-ext */
170 | @font-face {
171 | font-family: 'Roboto';
172 | font-style: normal;
173 | font-weight: 500;
174 | src: local('Roboto Medium'), local('Roboto-Medium'), url(https://fonts.gstatic.com/s/roboto/v15/ZLqKeelYbATG60EpZBSDyxJtnKITppOI_IvcXXDNrsc.woff2) format('woff2');
175 | unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
176 | }
177 | /* cyrillic */
178 | @font-face {
179 | font-family: 'Roboto';
180 | font-style: normal;
181 | font-weight: 500;
182 | src: local('Roboto Medium'), local('Roboto-Medium'), url(https://fonts.gstatic.com/s/roboto/v15/oHi30kwQWvpCWqAhzHcCSBJtnKITppOI_IvcXXDNrsc.woff2) format('woff2');
183 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
184 | }
185 | /* greek-ext */
186 | @font-face {
187 | font-family: 'Roboto';
188 | font-style: normal;
189 | font-weight: 500;
190 | src: local('Roboto Medium'), local('Roboto-Medium'), url(https://fonts.gstatic.com/s/roboto/v15/rGvHdJnr2l75qb0YND9NyBJtnKITppOI_IvcXXDNrsc.woff2) format('woff2');
191 | unicode-range: U+1F00-1FFF;
192 | }
193 | /* greek */
194 | @font-face {
195 | font-family: 'Roboto';
196 | font-style: normal;
197 | font-weight: 500;
198 | src: local('Roboto Medium'), local('Roboto-Medium'), url(https://fonts.gstatic.com/s/roboto/v15/mx9Uck6uB63VIKFYnEMXrRJtnKITppOI_IvcXXDNrsc.woff2) format('woff2');
199 | unicode-range: U+0370-03FF;
200 | }
201 | /* vietnamese */
202 | @font-face {
203 | font-family: 'Roboto';
204 | font-style: normal;
205 | font-weight: 500;
206 | src: local('Roboto Medium'), local('Roboto-Medium'), url(https://fonts.gstatic.com/s/roboto/v15/mbmhprMH69Zi6eEPBYVFhRJtnKITppOI_IvcXXDNrsc.woff2) format('woff2');
207 | unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB;
208 | }
209 | /* latin-ext */
210 | @font-face {
211 | font-family: 'Roboto';
212 | font-style: normal;
213 | font-weight: 500;
214 | src: local('Roboto Medium'), local('Roboto-Medium'), url(https://fonts.gstatic.com/s/roboto/v15/oOeFwZNlrTefzLYmlVV1UBJtnKITppOI_IvcXXDNrsc.woff2) format('woff2');
215 | unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
216 | }
217 | /* latin */
218 | @font-face {
219 | font-family: 'Roboto';
220 | font-style: normal;
221 | font-weight: 500;
222 | src: local('Roboto Medium'), local('Roboto-Medium'), url(https://fonts.gstatic.com/s/roboto/v15/RxZJdnzeo3R5zSexge8UUVtXRa8TVwTICgirnJhmVJw.woff2) format('woff2');
223 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
224 | }
225 | /* cyrillic-ext */
226 | @font-face {
227 | font-family: 'Roboto';
228 | font-style: normal;
229 | font-weight: 700;
230 | src: local('Roboto Bold'), local('Roboto-Bold'), url(https://fonts.gstatic.com/s/roboto/v15/77FXFjRbGzN4aCrSFhlh3hJtnKITppOI_IvcXXDNrsc.woff2) format('woff2');
231 | unicode-range: U+0460-052F, U+20B4, U+2DE0-2DFF, U+A640-A69F;
232 | }
233 | /* cyrillic */
234 | @font-face {
235 | font-family: 'Roboto';
236 | font-style: normal;
237 | font-weight: 700;
238 | src: local('Roboto Bold'), local('Roboto-Bold'), url(https://fonts.gstatic.com/s/roboto/v15/isZ-wbCXNKAbnjo6_TwHThJtnKITppOI_IvcXXDNrsc.woff2) format('woff2');
239 | unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
240 | }
241 | /* greek-ext */
242 | @font-face {
243 | font-family: 'Roboto';
244 | font-style: normal;
245 | font-weight: 700;
246 | src: local('Roboto Bold'), local('Roboto-Bold'), url(https://fonts.gstatic.com/s/roboto/v15/UX6i4JxQDm3fVTc1CPuwqhJtnKITppOI_IvcXXDNrsc.woff2) format('woff2');
247 | unicode-range: U+1F00-1FFF;
248 | }
249 | /* greek */
250 | @font-face {
251 | font-family: 'Roboto';
252 | font-style: normal;
253 | font-weight: 700;
254 | src: local('Roboto Bold'), local('Roboto-Bold'), url(https://fonts.gstatic.com/s/roboto/v15/jSN2CGVDbcVyCnfJfjSdfBJtnKITppOI_IvcXXDNrsc.woff2) format('woff2');
255 | unicode-range: U+0370-03FF;
256 | }
257 | /* vietnamese */
258 | @font-face {
259 | font-family: 'Roboto';
260 | font-style: normal;
261 | font-weight: 700;
262 | src: local('Roboto Bold'), local('Roboto-Bold'), url(https://fonts.gstatic.com/s/roboto/v15/PwZc-YbIL414wB9rB1IAPRJtnKITppOI_IvcXXDNrsc.woff2) format('woff2');
263 | unicode-range: U+0102-0103, U+1EA0-1EF9, U+20AB;
264 | }
265 | /* latin-ext */
266 | @font-face {
267 | font-family: 'Roboto';
268 | font-style: normal;
269 | font-weight: 700;
270 | src: local('Roboto Bold'), local('Roboto-Bold'), url(https://fonts.gstatic.com/s/roboto/v15/97uahxiqZRoncBaCEI3aWxJtnKITppOI_IvcXXDNrsc.woff2) format('woff2');
271 | unicode-range: U+0100-024F, U+1E00-1EFF, U+20A0-20AB, U+20AD-20CF, U+2C60-2C7F, U+A720-A7FF;
272 | }
273 | /* latin */
274 | @font-face {
275 | font-family: 'Roboto';
276 | font-style: normal;
277 | font-weight: 700;
278 | src: local('Roboto Bold'), local('Roboto-Bold'), url(https://fonts.gstatic.com/s/roboto/v15/d-6IYplOFocCacKzxwXSOFtXRa8TVwTICgirnJhmVJw.woff2) format('woff2');
279 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
280 | }
--------------------------------------------------------------------------------
/api.js:
--------------------------------------------------------------------------------
1 | var winston = require('winston');
2 | var request = require('request');
3 | var messagecompressor = require('./messagecompressor');
4 |
5 | function API(settings, db, bot, io) {
6 | this.settings = settings;
7 | this.db = db;
8 | this.bot = bot;
9 | this.io = io;
10 | this.streaming = {};
11 | this.checkStreams();
12 | setInterval(API.prototype.checkStreams.bind(this), 60*1000);
13 | }
14 |
15 | // generic helpers
16 |
17 | // returns the smallest absolute maximum of the inputs
18 | // for example, absMinMax(5,10,-10) would return -10
19 | function absMinMax(){
20 | var best=0;
21 | for(var i=0;i{
38 | request.get({url: url, headers: headers}, function (error, response, body) {
39 | if(error) j(error, response);
40 | else {
41 | try {
42 | r(JSON.parse(body), response);
43 | } catch(e) {
44 | j(e, response);
45 | }
46 | }
47 | });
48 | });
49 | }
50 |
51 | API.prototype.adminLog = function(channel, user, action, key, data) {
52 | var t = Math.floor(Date.now()/1000);
53 | this.io.to("events-"+channel).emit("adminlog", {channel: channel, user: user, action: action, name: key, data: data, time: t});
54 | this.db.adminLog(channel, user, action, key, data);
55 | winston.debug("Emitting adminlog: "+action+" on channel "+channel);
56 | }
57 |
58 | API.prototype.getChannelID = function(channelname) {
59 | var self = this;
60 | return self.twitchGet("https://api.twitch.tv/kraken/users?login="+channelname)
61 | }
62 |
63 | // helper function: gets both the channel object and the user level of the specified token
64 | API.prototype.getChannelObjAndLevel = function(channelname, token, callback) {
65 | var self = this;
66 | channelname = channelname.toLowerCase();
67 | self.db.getChannel({name: channelname}, function(channelObj) {
68 | if(channelObj) {
69 | self.getLevel(channelObj, token, function(level, username){
70 | callback(null, channelObj, level, username);
71 | });
72 | } else {
73 | self.getLevel({name: channelname, id: }, token, function(level, username){
74 | callback({status: 404, message: "Channel "+channelname+" not found."}, null, level, username);
75 | });
76 |
77 | }
78 | });
79 | }
80 |
81 |
82 | // gets the level of a user by name
83 | API.prototype.getUserLevel = function(channelObj,id,callback) {
84 | var self = this;
85 | var reslvl = null;
86 | var templvl = 0;
87 | if(name) {
88 | if(channelObj) {
89 | if(self.bot.userlevels[channelObj.id] && self.bot.userlevels[channelObj.id][name]) templvl = self.bot.userlevels[channelObj.id][name];
90 | if(channelObj.id === id) templvl = 10;
91 | self.db.getUserLevel(channelObj, id, function(lv){
92 | if(reslvl === null) {
93 | reslvl = lv;
94 | } else {
95 | callback(absMinMax(1,reslvl,lv,templvl));
96 | }
97 | });
98 | } else {
99 | reslvl = 0;
100 | }
101 |
102 | self.db.getUserLevel({id: self.settings.bot.id, name: self.settings.bot.nick}, id, function(lv){
103 | if(reslvl === null) {
104 | reslvl = lv;
105 | } else {
106 | callback(absMinMax(1,reslvl,lv,templvl));
107 | }
108 | });
109 | } else callback(0);
110 | }
111 |
112 | // gets the level of a user by token
113 | API.prototype.getLevel = function(channelObj, token, callback) {
114 | var self = this;
115 | if(!token || token === "") callback(0);
116 | else {
117 | self.db.getAuthUser(token, function(name, id){
118 | self.getUserLevel(channelObj,id,function(level){
119 | callback(level,name,id);
120 | });
121 | });
122 | }
123 | }
124 |
125 | // updates a channel settings
126 | var allowedsettings = ["active","modlogs","viewlogs","viewmodlogs","viewcomments","writecomments","deletecomments"];
127 | API.prototype.updateSettings = function(channelObj, user, settings, callback) {
128 | var self = this;
129 | var error;
130 | var async = false; // TODO: promises
131 | console.log(settings);
132 |
133 | for(var i=0;i= channelObj.editcomments || comment.author == username) {
289 | var time = Math.floor(Date.now()/1000);
290 | comment.edited = time;
291 | comment.text = newcomment.text;
292 | self.adminLog(channelObj.name, username, "comment", "edit", JSON.stringify(comment));
293 | self.db.updateComment(channelObj.name, comment.id, comment.text);
294 | // only send back stuff needed for identification and changes
295 | self.io.to("comments-"+channelObj.name+"-"+comment.topic).emit("comment-update", comment);
296 | callback();
297 | } else {
298 | callback({"status": 403, "message":"Can only edit own comments"});
299 | return;
300 | }
301 | } else {
302 | callback({"status": 404, "message":"Comment not found"});
303 | }
304 | });
305 | } else {
306 | if(newcomment.topic === undefined) {
307 | callback({"status":400, "message":"Missing parameter topic."});
308 | } else if(newcomment.text === undefined) {
309 | callback({"status":400, "message":"Missing parameter text."});
310 | } else if(level >= channelObj.writecomments) {
311 | var time = Math.floor(Date.now()/1000);
312 | self.db.addComment(channelObj.name, username, newcomment.topic, newcomment.text, function(id){
313 | var comment = {id: id, added: time, edited: time, channel: channelObj.name, author: username, topic: newcomment.topic, text: newcomment.text};
314 | self.adminLog(channelObj.name, username, "comment", "add", JSON.stringify(comment));
315 | self.io.to("comments-"+channelObj.name+"-"+newcomment.topic).emit("comment-add", comment);
316 | });
317 | callback();
318 | } else {
319 | callback({"status":403, "message":"Cannot write comments for this channel"});
320 | return;
321 | }
322 | }
323 | }
324 |
325 | API.prototype.getChannels = function(callback){
326 | var self = this;
327 | self.db.getChannelList(function(channels) {
328 | for(var i=0;ix.name);
345 | self.twitchGet("https://api.twitch.tv/kraken/streams?limit=100&channel="+channelChunk.join(",")).then(function(data){
346 | // reset streams
347 | for(let j=0;j
2 |
3 | loading...
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Channel {{::channel}} not found.
12 |
If you want to add the logviewer to the channel, then head over to the
13 | settings .
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | You currently do not have access to view logs of the channel {{::channel}}.
23 | If you have {{::channelsettings.viewlogs | aAnAccountType}} account, then you can Login with twitch
24 |
25 |
26 |
27 |
28 |
29 |
30 | User logs for channel {{::channelsettings.name}}
31 | Logs for channel {{::channelsettings.name}}
32 |
59 |
60 |
61 |
62 |
63 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | Load more
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | Load more
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | {{messageBefore.time | secondsTimestamp | date : 'yyyy/MM/dd hh:mm a'}}
104 |
105 |
106 |
(By: {{modLogList(messageBefore.modlog)}})
107 |
108 |
109 |
110 | {{user}} {{modLogDisplay(time)}}
111 |
112 |
113 |
114 |
115 |
116 |
117 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | {{message.time | secondsTimestamp | date : 'yyyy/MM/dd hh:mm a'}}
133 |
134 |
135 |
(By: {{modLogList(message.modlog)}})
136 |
137 |
138 |
139 | {{user}} {{modLogDisplay(time)}}
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 | {{messageAfter.time | secondsTimestamp | date : 'yyyy/MM/dd hh:mm a'}}
156 |
157 |
158 |
(By: {{modLogList(messageAfter.modlog)}})
159 |
160 |
161 |
162 | {{user}} {{modLogDisplay(time)}}
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 | Load more
173 |
174 |
175 |
176 |
177 |
178 |
179 | Load newer
180 |
181 |
182 |
183 |
No recorded chat messages found.
184 |
Loading...
185 |
213 |
220 |
224 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 | Full log
236 |
237 |
238 | Full logs for channel {{::channelsettings.name}}
239 | Logs for channel {{::channelsettings.name}}
240 | Jump to date:
241 |
242 |
243 |
267 |
268 | End of logs reached.
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
--------------------------------------------------------------------------------
/db/mysql.js:
--------------------------------------------------------------------------------
1 | var mysql = require('mysql');
2 | var winston = require('winston');
3 |
4 | module.exports = function MySQLDatabaseConnector(settings) {
5 | var self = this;
6 |
7 | self.pool = mysql.createPool({
8 | connectionLimit: 100,
9 | host: settings.host,
10 | port: settings.port || 3306,
11 | user: settings.user,
12 | database: settings.database,
13 | password: settings.password,
14 | charset: "utf8mb4_unicode_ci"
15 | });
16 | self.pool.getConnection(function(err, connection) {
17 | if(err) {
18 | winston.error('Error connecting to MySQL database: ' + err.stack);
19 | return;
20 | }
21 | // create the channels table if it doesnt exist
22 | connection.query("CREATE TABLE IF NOT EXISTS channels ("
23 | +"id int(10) UNSIGNED PRIMARY KEY,"
24 | +"name varchar(32),"
25 | +"active tinyint(4) UNSIGNED NOT NULL DEFAULT '0',"
26 | +"modlogs tinyint(4) UNSIGNED NOT NULL DEFAULT '0',"
27 | +"viewlogs tinyint(4) UNSIGNED NOT NULL DEFAULT '0',"
28 | +"viewmodlogs tinyint(4) UNSIGNED NOT NULL DEFAULT '5',"
29 | +"viewcomments tinyint(4) UNSIGNED NOT NULL DEFAULT '5',"
30 | +"writecomments tinyint(4) UNSIGNED NOT NULL DEFAULT '5',"
31 | +"deletecomments tinyint(4) UNSIGNED NOT NULL DEFAULT '10',"
32 | +"color varchar(32) NULL,"
33 | +"premium BIGINT UNSIGNED NOT NULL," // time of expiration of premium features
34 | +"`max-age` int(10) UNSIGNED NOT NULL DEFAULT '2678400'," // currently unused
35 | +"INDEX channels_by_name (name ASC)"
36 | +")");
37 | // create the auth table if it doesnt exist
38 | connection.query("CREATE TABLE IF NOT EXISTS auth ("
39 | +"token varchar(64) PRIMARY KEY,"
40 | +"userid INT UNSIGNED NULL,"
41 | +"name varchar(32),"
42 | +"expires BIGINT UNSIGNED"
43 | +")");
44 | // create the comment table if it doesnt exist
45 | connection.query("CREATE TABLE IF NOT EXISTS comments ("
46 | +"id INT NOT NULL AUTO_INCREMENT,"
47 | +"added BIGINT UNSIGNED NOT NULL,"
48 | +"edited BIGINT UNSIGNED NOT NULL,"
49 | +"channel VARCHAR(32) NULL,"
50 | +"author VARCHAR(32) NULL,"
51 | +"topic VARCHAR(64) NULL,"
52 | +"text TEXT NULL COLLATE utf8mb4_unicode_ci,"
53 | +"PRIMARY KEY (id),"
54 | +"INDEX comments_by_channel_and_topic (channel ASC, topic ASC)"
55 | +")");
56 | // create the alias table if it doesnt exist
57 | connection.query("CREATE TABLE IF NOT EXISTS aliases ("
58 | +"alias varchar(32) PRIMARY KEY,"
59 | +"id INT UNSIGNED"
60 | +")");
61 |
62 | // create the logviewer tables if they dont exist
63 | self.ensureTablesExist({name: "logviewer"});
64 |
65 | /* create the integrations table if it doesnt exist */
66 | connection.query("CREATE TABLE IF NOT EXISTS connections ("
67 | +"id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,"
68 | +"channel VARCHAR(32) NULL,"
69 | +"level INT DEFAULT '0'," // access level of the connection
70 | +"app VARCHAR(32) NOT NULL PRIMARY KEY," // name of the app (for example "Slack")
71 | // identifier of the application (used to identify the location the request came from)
72 | // essentially the user name (for example, a Slack connection uses the slash command token to identify )
73 | +"data VARCHAR(256) NULL,"
74 | +"description TEXT NULL" // Full-text description
75 | +")");
76 |
77 | /* create the integrations table if it doesnt exist */
78 | connection.query("CREATE TABLE IF NOT EXISTS apps ("
79 | +"id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,"
80 | +"scopes VARCHAR(64) NULL," // optimal scopes this app needs
81 | +"name VARCHAR(32) NOT NULL PRIMARY KEY," // name of the app (for example "Slack")
82 | +"redirect_url VARCHAR(256) NULL," // url to redirect to after authenticating
83 | +"description TEXT NULL" // Full-text description
84 | +")");
85 |
86 | /* create the admin log table if it doesnt exist */
87 | connection.query("CREATE TABLE IF NOT EXISTS adminlog ("
88 | +"id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,"
89 | +"channel VARCHAR(32) NULL,"
90 | +"time BIGINT UNSIGNED NOT NULL,"
91 | +"user VARCHAR(32) NULL,"
92 | +"userid INT UNSIGNED NULL,"
93 | +"action VARCHAR(32) NULL,"
94 | +"name VARCHAR(256) NULL,"
95 | +"data TEXT NULL,"
96 | +"INDEX adminlog_channel (channel ASC)"
97 | +")");
98 |
99 | /*CREATE TABLE `logviewer`.`modlogs` (
100 | `id` BIGINT NOT NULL AUTO_INCREMENT,
101 | `channelid` VARCHAR(32) NOT NULL,
102 | `time` BIGINT NULL,
103 | `user` VARCHAR(32) NULL,
104 | `command` VARCHAR(32) NULL,
105 | `args` VARCHAR(256) NULL,
106 | PRIMARY KEY (`id`),
107 | INDEX `user` (`channelid` ASC, `user` ASC, `time` ASC),
108 | INDEX `channel` (`channelid` ASC, `time` ASC));
109 | */
110 | /* create the modlog table if it doesnt exist */
111 | connection.query("CREATE TABLE IF NOT EXISTS modlog ("
112 | +"id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,"
113 | +"channelid INT(10) UNSIGNED NULL,"
114 | +"time BIGINT UNSIGNED NOT NULL,"
115 | +"user VARCHAR(32) NULL,"
116 | +"userid INT UNSIGNED NULL,"
117 | +"command VARCHAR(32) NULL,"
118 | +"args TEXT NULL,"
119 | +"INDEX modlog_channel (channel ASC, id ASC),"
120 | +"INDEX modlog_user (channel ASC, id ASC, userid ASC),"
121 | +"INDEX modlog_command (channel ASC, id ASC, command ASC),"
122 | +")");
123 |
124 |
125 | });
126 |
127 | self.ensureTablesExist = function(channelObj) {
128 | self.pool.query("CREATE TABLE IF NOT EXISTS chat_"+channelObj.id+" ("
129 | +"id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,"
130 | +"time BIGINT UNSIGNED NOT NULL,"
131 | +"nick VARCHAR(32) NOT NULL,"
132 | +"userid INT UNSIGNED NULL,"
133 | +"text VARCHAR(2047) COLLATE utf8mb4_unicode_ci NOT NULL,"
134 | +"modlog VARCHAR(1024) DEFAULT NULL,"
135 | +"INDEX (userid, time),"
136 | +"INDEX (time),"
137 | +"INDEX (modlog(1), id DESC)"
138 | +")");
139 | self.pool.query("CREATE TABLE IF NOT EXISTS users_"+channelObj.id+" ("
140 | +"id INT UNSIGNED NULL PRIMARY KEY,"
141 | +"nick VARCHAR(32) NOT NULL,"
142 | +"messages INT UNSIGNED DEFAULT '0',"
143 | +"timeouts INT DEFAULT '0',"
144 | +"bans INT UNSIGNED DEFAULT '0',"
145 | +"level INT DEFAULT '0',"
146 | +"INDEX (id ASC),"
147 | +"INDEX (messages DESC),"
148 | +"INDEX (level DESC)"
149 | +")");
150 | }
151 |
152 | /**
153 | * Gets the full list of complete channel objects
154 | */
155 | self.getChannels = function(callback) {
156 | self.pool.query("SELECT * FROM channels WHERE active=1",function(error, results, fields){
157 | callback(results);
158 | });
159 | }
160 |
161 | /**
162 | * Gets a display-ready list of channel objects from the database
163 | */
164 | self.getChannelList = function(callback) {
165 | self.pool.query("SELECT name, id, color, premium > ? AS ispremium FROM channels WHERE active=1",[Math.floor(Date.now()/1000)],function(error, results, fields){
166 | callback(results);
167 | });
168 | }
169 |
170 | /**
171 | * Gets a list of aliases. Probably unused.
172 | */
173 | self.getAliases = function(callback) {
174 | self.pool.query("SELECT name, id, alias FROM aliases",function(error, results, fields){
175 | callback(results);
176 | });
177 | }
178 |
179 | /**
180 | * Gets a complete channel object from a partial channel object (name OR id). It follows aliases if only a channelname is specified.
181 | * @param active If true, only active channels are returned.
182 | */
183 | self.getChannel = function(channelObj, active, callback) {
184 | if(channelObj.id) {
185 | self.pool.query("SELECT * FROM channels WHERE id=? AND active=1",[channelObj.id],function(error, results, fields){
186 | if(results.length == 0 || (active && results[0].active != 1)) callback(null);
187 | else {
188 | callback(results[0])
189 | }
190 | });
191 | } else if(channelObj.name) {
192 | self.pool.query("SELECT * FROM channels WHERE name=?",[channelObj.name],function(error, results, fields){
193 | if(results.length == 0) {
194 | self.pool.query("SELECT id FROM aliases WHERE alias=?",[channelObj.name],function(error, results, fields){
195 | if(results.length == 0) {
196 | callback(null);
197 | } else {
198 | self.getChannel(results[0], active, callback);
199 | }
200 | });
201 | }
202 | else {
203 | if(active && results[0].active != 1) callback(null);
204 | else callback(results[0]);
205 | }
206 | });
207 | } else {
208 | callback(null);
209 | }
210 | }
211 |
212 | /**
213 | * Add a partial channel object (name+id) to the database
214 | */
215 | self.addChannel = function(channelObj, callback) {
216 | self.ensureTablesExist(channelObj);
217 | self.pool.query("INSERT INTO channels (name, id) VALUES (?,?)",[channelObj.name, channelObj.id],function(error, result){
218 | if(error) {
219 | winston.error("Couldnt add channel! "+error);
220 | } else {
221 | self.pool.query("SELECT * FROM channels WHERE name=?",[channelObj.name], function(error, results, fields){
222 | if(error || results.length == 0) {
223 | winston.error("Channel wasnt added properly! "+(error || "No results returned..."));
224 | } else {
225 | callback(results[0]);
226 | }
227 | });
228 | }
229 | });
230 | }
231 |
232 | /**
233 | * Insert a line of chat into the database
234 | */
235 | self.addLine = function(channelObj, nick, userid, time, message, modlog, callback) {
236 | self.pool.query("INSERT INTO ?? (time,nick,userid,text,modlog) VALUES (?,?,?,?)",["chat_"+channelObj.id, Math.floor(time/1000), nick, userid, message, modlog?JSON.stringify(modlog):null], function(error, result) {
237 | if(error) {
238 | winston.error("addModLog: Could not insert! "+error);
239 | return;
240 | }
241 | if(callback) callback(result.insertId);
242 | });
243 | }
244 |
245 | /**
246 | * Get filtered modlogs
247 | */
248 | self.getModLogs = function(channelObj, filters, id, limit, callback) {
249 | if(filters.include.length > 0) {
250 | self.pool.query("SELECT * FROM modlog WHERE channelid=? AND command IN (?) AND user IN (?) AND id < ? LIMIT ? ORDER BY id DESC", [channelObj.id, filters.commands, filters.include, id, limit], function(error, results) {
251 | if(error) {
252 | winston.error("getModLogs: Error retrieving mod logs! "+error);
253 | return;
254 | }
255 | callback(results);
256 | });
257 | } else {
258 | self.pool.query("SELECT * FROM modlog WHERE channelid=? AND command IN (?) AND user NOT IN (?) LIMIT ? OFFSET ?", [channelObj.id, allowed_commands, limit, filters.exclude, offset], function(error, results) {
259 | if(error) {
260 | winston.error("getModLogs: Error retrieving mod logs! "+error);
261 | return;
262 | }
263 | callback(results);
264 | });
265 | }
266 | }
267 |
268 | self.updateStats = function(channelObj, nick, userid, values) {
269 | var changes = "";
270 | var params = {nick: nick, id: userid};
271 | if(values.timeouts) {
272 | changes += ", timeouts = timeouts + "+parseInt(values.timeouts);
273 | params.timeouts = values.timeouts;
274 | }
275 | if(values.bans) {
276 | changes += ", bans = bans + "+parseInt(values.bans);
277 | params.bans = values.bans;
278 | }
279 | if(values.messages) {
280 | changes += ", messages = messages + "+parseInt(values.messages);
281 | params.messages = values.messages;
282 | }
283 | self.pool.query("INSERT INTO ?? SET ? ON DUPLICATE KEY UPDATE nick=?"+changes,["users_"+channelObj.id, params, nick], function(error, result) {
284 | if(error) winston.error(error);
285 | });
286 | };
287 |
288 | self.updateTimeout = function(channelObj, userid, id, time, message, modlog) {
289 | // we use the pool for this instead of the pool
290 | self.pool.query("UPDATE ?? SET time=?, text=?, modlog=? WHERE userid=? AND id=?",["chat_"+channelObj.id, Math.floor(time/1000), message, JSON.stringify(modlog), userid, id]);
291 | }
292 |
293 | function parseModLogs(list){
294 | for(var i=0;i 0) {
320 | if(userid) {
321 | self.pool.query("SELECT id,time,nick,userid,text"+(modlogs?",modlog":"")+" FROM ?? WHERE userid=? AND id < ? ORDER BY id DESC LIMIT ?", ["chat_"+channelObj.id, userid, id, before], function(error, results, fields) {
322 | if(results) beforeRes = results.reverse();
323 | else beforeRes = [];
324 | parseModLogs(beforeRes);
325 | if(afterRes !== null) callback(beforeRes, afterRes);
326 | });
327 | } else {
328 | // we exclude twitchnotify when not checking a specific user
329 | self.pool.query("SELECT id,time,nick,userid,text"+(modlogs?",modlog":"")+" FROM ?? WHERE id < ? ORDER BY id DESC LIMIT ?", ["chat_"+channelObj.id, id, before], function(error, results, fields) {
330 | if(results) beforeRes = results.reverse();
331 | else beforeRes = [];
332 | parseModLogs(beforeRes);
333 | if(afterRes !== null) callback(beforeRes, afterRes);
334 | });
335 | }
336 | } else { beforeRes = []; }
337 | // after
338 | if(after > 0) {
339 | if(userid) {
340 | self.pool.query("SELECT id,time,nick,userid,text"+(modlogs?",modlog":"")+" FROM ?? WHERE userid=? AND id > ? ORDER BY id ASC LIMIT ?", ["chat_"+channelObj.id, userid, id, after], function(error, results, fields) {
341 | if(results) afterRes = results;
342 | else afterRes = [];
343 | parseModLogs(afterRes);
344 | if(beforeRes !== null) callback(beforeRes, afterRes);
345 | });
346 | } else {
347 | // we exclude twitchnotify when not checking a specific user
348 | self.pool.query("SELECT id,time,nick,userid,text"+(modlogs?",modlog":"")+" FROM ?? WHERE id > ? ORDER BY id ASC LIMIT ?", ["chat_"+channelObj.id, id, after], function(error, results, fields) {
349 | if(results) afterRes = results;
350 | else afterRes = [];
351 | parseModLogs(afterRes);
352 | if(beforeRes !== null) callback(beforeRes, afterRes);
353 | });
354 | }
355 | } else {
356 | afterRes = [];
357 | if(beforeRes !== null) callback(beforeRes, afterRes);
358 | }
359 | }
360 |
361 | self.getLogsByTime = function(channelObj, time, before, after, modlogs, callback) {
362 | var beforeRes = null;
363 | var afterRes = null;
364 | // before
365 | if(before > 0) {
366 | self.pool.query("SELECT id,time,nick,userid,text"+(modlogs?",modlog":"")+" FROM ?? WHERE time < ? ORDER BY time DESC LIMIT ?", ["chat_"+channelObj.id, time, before], function(error, results, fields) {
367 | if(results) beforeRes = results.reverse();
368 | else beforeRes = [];
369 | parseModLogs(beforeRes);
370 | if(afterRes !== null) callback(beforeRes, afterRes);
371 | });
372 | } else { beforeRes = []; }
373 | // after
374 | if(after > 0) {
375 | // we exclude twitchnotify when not checking a specific user
376 | self.pool.query("SELECT id,time,nick,userid,text"+(modlogs?",modlog":"")+" FROM ?? WHERE time >= ? ORDER BY time ASC LIMIT ?", ["chat_"+channelObj.id, time, after], function(error, results, fields) {
377 | if(results) afterRes = results;
378 | else afterRes = [];
379 | parseModLogs(afterRes);
380 | if(beforeRes !== null) callback(beforeRes, afterRes);
381 | });
382 | } else {
383 | afterRes = [];
384 | if(beforeRes !== null) callback(beforeRes, afterRes);
385 | }
386 | }
387 |
388 | self.getUserStats = function(channelObj, userid, ranking, callback) {
389 | self.pool.query("SELECT nick, id, messages, timeouts, bans FROM ?? WHERE id = ?", ["users_"+channelObj.id, userid], function(error, results, fields) {
390 | var stats = results[0] || {nick: null, id: userid, timeouts:0, messages: 0, bans: 0};
391 | if(ranking) {
392 | console.log(stats);
393 | self.pool.query("SELECT COUNT(*)+1 as rank FROM ?? WHERE messages > ?", ["users_"+channelObj.id, stats.messages], function(error, results, fields) {
394 | stats.rank = results[0].rank;
395 | callback(stats);
396 | });
397 | }
398 | else callback(stats);
399 | });
400 | }
401 |
402 | self.getAuthUser = function(token, callback) {
403 | self.pool.query("SELECT userid, name FROM auth WHERE token=? AND expires > ?",[token,Math.floor(Date.now()/1000)], function(error, results, fields) {
404 | if(results && results.length>0) callback(results[0]);
405 | else callback(null);
406 | });
407 | }
408 |
409 | self.getUserLevel = function(channelObj, userid, callback) {
410 | self.pool.query("SELECT level FROM ?? WHERE id = ?", ["users_"+channelObj.id, userid], function(error, results, fields) {
411 | if(results && results.length>0) callback(results[0].level || 0);
412 | else callback(0);
413 | });
414 | }
415 |
416 | self.setLevel = function(channelObj, userid, nick, level) {
417 | self.pool.query("INSERT INTO ?? (id,nick,level) VALUES (?,?) ON DUPLICATE KEY UPDATE nick = ?, level = ?",["users_"+channelObj.id, userid, nick, level, nick, level]);
418 | }
419 |
420 | self.getLevels = function(channelObj, callback) {
421 | self.pool.query("SELECT id,nick,level FROM ?? WHERE level != 0", ["users_"+channelObj.id], function(error, results, fields) {
422 | callback(results);
423 | });
424 | }
425 |
426 | self.storeToken = function(userid, name, token, expires) {
427 | self.pool.query("INSERT INTO auth (userid, name, token, expires) VALUES (?,?,?,?)",[userid,name,token,expires]);
428 | }
429 |
430 | self.deleteToken = function(token) {
431 | self.pool.query("DELETE FROM auth WHERE token=?",[token]);
432 | }
433 |
434 | self.checkAndRefreshToken = function(userid, token, expires, callback) {
435 | self.pool.query("UPDATE auth SET expires=? WHERE userid=? AND token=? AND expires > ?",[expires,userid,token,Math.floor(Date.now()/1000)], function(error, result) {
436 | if(callback) callback(result.affectedRows > 0);
437 | });
438 | }
439 |
440 | self.setSetting = function(channelObj, key, val) {
441 | self.pool.query("UPDATE channels SET ??=? WHERE id=?",[key,val,channelObj.id]);
442 | }
443 |
444 | self.getComments = function(channelObj,topic,callback) {
445 | self.pool.query("SELECT * FROM comments WHERE channelid=? AND topic=?",[channelObj.id,topic],function(error,results,fields) {
446 | callback(results);
447 | });
448 | }
449 |
450 | self.getComment = function(channelObj,id,callback) {
451 | self.pool.query("SELECT * FROM comments WHERE id=? AND channelid=?",[id,channelObj.id],function(error,results,fields) {
452 | callback(results[0]);
453 | });
454 | }
455 |
456 | self.addComment = function(channelObj, author, topic, text, callback) {
457 | var d = Math.floor(Date.now()/1000);
458 | self.pool.query("INSERT INTO comments(added,edited,channelid,author,topic,text) VALUES (?,?,?,?,?,?)", [d,d,channelObj.id,author,topic,text], function(error, result) {
459 | if(callback) callback(result.insertId);
460 | });
461 | }
462 |
463 | self.updateComment = function(channelObj,id,newtext) {
464 | self.pool.query("UPDATE comments SET text=?, edited=? WHERE id=? AND channelid=?",[newtext,Math.floor(Date.now()/1000),id,channelObj.id]);
465 | }
466 |
467 | self.deleteComment = function(channelObj,id) {
468 | self.pool.query("DELETE FROM comments WHERE id=? AND channelid=?",[id,channelObj.id]);
469 | }
470 |
471 | self.findUser = function(channelObj, query, callback) {
472 | var searchString = query.replace("_","\\_").replace("*","%")+"%";
473 | searchString = searchString.replace(/%{2,}/g,"%");
474 | self.pool.query("SELECT nick, id FROM ?? WHERE nick LIKE ? LIMIT 11",["users_"+channelObj.id, searchString], function(error,results,fields) {
475 | callback(results);
476 | });
477 | }
478 |
479 | /* "CREATE TABLE IF NOT EXISTS adminlog ("
480 | +"time BIGINT UNSIGNED NOT NULL,"
481 | +"channel VARCHAR(32) NULL,"
482 | +"user VARCHAR(32) NULL,"
483 | +"action VARCHAR(32) NULL," -> setting/level/(dis)connect/(add/edit/remove) comment
484 | +"key VARCHAR(32) NULL," -> setting/user/connection name/comment id
485 | +"data VARCHAR(256) NULL" -> new value/level/key/comment text
486 | +")" */
487 | self.adminLog = function(channelObj, user, action, key, data) {
488 | var d = Math.floor(Date.now()/1000);
489 | self.pool.query("INSERT INTO adminlog(time,channelid,user,action,name,data) VALUES (?,?,?,?,?,?)", [d,channelObj.id,user,action,key,data]);
490 | }
491 |
492 | self.getEvents = function(channelObj, limit, callback) {
493 | self.pool.query("SELECT * FROM (SELECT * FROM adminlog WHERE channelid=? ORDER BY id DESC LIMIT ?) sub ORDER BY id ASC",[channelObj.id,limit], function(error,results,fields) {
494 | if(error) {
495 | winston.error("getEvents: Select failed! "+error);
496 | callback([]);
497 | }
498 | else callback(results);
499 | });
500 | }
501 |
502 | self.getLeaderboard = function(channelObj, offset, limit, callback) {
503 | self.pool.query("SELECT nick, id, messages, timeouts, bans FROM ?? ORDER BY messages DESC LIMIT ? OFFSET ?",["users_"+channelObj.id,limit,offset], function(error,results,fields) {
504 | if(error) {
505 | winston.error("getLeaderboard: Select failed! "+error);
506 | callback([]);
507 | }
508 | else callback(results);
509 | });
510 | }
511 |
512 | // connections
513 | /*
514 | id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
515 | channel VARCHAR(32) NULL,
516 | active tinyint(4) unsigned NOT NULL DEFAULT '1',
517 | type VARCHAR(32) NOT NULL PRIMARY KEY, // name of the app (for example "Slack")
518 | data VARCHAR(256) NULL, // identifier of the application (used to identify the location the request came from)
519 | description TEXT NULL // Full-text description
520 |
521 | self.getIntegrations = function(channel, callback) {
522 | self.pool.query("SELECT * FROM connections WHERE channel=?",[channel], function(error,results,fields) {
523 | callback(results);
524 | });
525 | }
526 |
527 | self.getIntegration = function(channel, id, callback) {
528 | self.pool.query("SELECT * FROM connections WHERE channel=? AND id=?",["users_"+channelObj.id, searchString], function(error,results,fields) {
529 | callback(results[0]);
530 | });
531 | }
532 |
533 | self.addConnection = function(channel, active, type, data, description, callback) {
534 | self.pool.query("INSERT INTO connections(channel, active, type, data, description) VALUES (?,?,?,?,?)",[channel, active, type, data, description], function(error, result) {
535 | if(error) {
536 | winston.error("addLine: Could not insert! "+error);
537 | return;
538 | }
539 | if(callback) callback(result.insertId);
540 | });
541 | }
542 |
543 | self.updateConnection = function(channel, id, active, type, data, description, callback) {
544 | self.pool.query("UPDATE connections SET active=?, type=?, data=?, description=? WHERE id=? AND channel=?",[active, type, data, description, id, channel], function(error,results,fields) {
545 | if(callback) callback(results);
546 | });
547 | }
548 |
549 | self.removeConnection = function(channel, id, callback, callback) {
550 | self.pool.query("DELETE FROM connections WHERE channel=? AND id=?",[channel, id], function(error,results,fields) {
551 | if(callback) callback(results);
552 | });
553 | }
554 | */
555 | // error handling
556 | self.pool.on('error', function(err) {
557 | winston.error(err);
558 | });
559 | }
560 |
561 |
--------------------------------------------------------------------------------
/bot.js:
--------------------------------------------------------------------------------
1 | var fs = require("fs");
2 | var events = require('events');
3 | var winston = require('winston');
4 |
5 | var ircbot = require('./ircbot');
6 | var pubsub = require('./pubsub');
7 | var messagecompressor = require('./messagecompressor');
8 | var TAGS = 1
9 | var PREFIX = 2
10 | var COMMAND = 3
11 | var PARAM = 4
12 | var TRAILING = 5
13 | var TWITCHNOTIFYID = 46325627;
14 | var JTVID = 14027;
15 | function logviewerBot(settings, db, io) {
16 | var self = this;
17 | self.settings = settings;
18 | self.nick = settings.bot.nick;
19 | self.API = null;
20 | self.db = db;
21 | self.io = io;
22 |
23 | self.pubsub = new pubsub(settings, db, io);
24 |
25 | var messagecompressor = require('./messagecompressor');
26 |
27 | var host = "irc.chat.twitch.tv";
28 | var port = 6667;
29 | var hostandport = /([^:^\/]+)(?:[:/](\d+))?/.exec(settings.bot.server);
30 | if(hostandport) {
31 | if(hostandport[1]) {
32 | host = hostandport[1];
33 | }
34 | if(hostandport[2]) {
35 | port = parseInt(hostandport[2]);
36 | }
37 | }
38 | self.bot = new ircbot(host, port);
39 | self.userlevels = {}; // temporary user levels (mod etc)
40 | self.channels = [];
41 | self.id2channelObj = {};
42 | self.name2channelObj = {};
43 |
44 |
45 | self.bot.on("connect", function(){
46 | self.bot.send("CAP REQ :twitch.tv/tags twitch.tv/commands");
47 | var oauth = settings.bot.oauth;
48 | if(!oauth.startsWith("oauth:")) oauth = "oauth:"+oauth;
49 | self.bot.send("PASS "+oauth);
50 | self.bot.send("NICK "+settings.bot.nick);
51 | db.getChannels(function(channels){
52 | for(var i=0;i " + text);
76 |
77 | // if the user is a mod, set his level to 5
78 | if(data[TAGS] && data[TAGS]["mod"] === "1" && self.userlevels[channel]) {
79 | self.userlevels[channel][user] = 5;
80 | }
81 |
82 | // remove the user from the recent timeouts (unless they were VERY recent (<2s ago)
83 | var oldtimeout = (self.timeouts[channel] && self.timeouts[channel][user]) || (self.oldtimeouts[channel] && self.oldtimeouts[channel][user]);
84 | if(oldtimeout) {
85 | var age = Date.now()/1000 - oldtimeout.time;
86 | if(age >= 2) {
87 | if(self.timeouts[channel] && self.timeouts[channel][user]) self.timeouts[channel][user] = undefined;
88 | if(self.oldtimeouts[channel] && self.oldtimeouts[channel][user]) self.oldtimeouts[channel][user] = undefined;
89 | }
90 | }
91 |
92 | if(self.timeouts[channel] && self.timeouts[channel][user]) {
93 | var now =
94 | self.timeouts[channel][user] = undefined;
95 | }
96 | if(self.oldtimeouts[channel] && self.oldtimeouts[channel][user]) self.oldtimeouts[channel][user] = undefined;
97 |
98 |
99 | var time = Math.floor(Date.now()/1000);
100 |
101 | setTimeout(()=>{
102 | var modlog;
103 | if(data[TAGS] && data[TAGS]["id"]) {
104 | var allowedMessage = allowedMessages[data[TAGS]["id"]] || oldAllowedMessages[data[TAGS]["id"]];
105 | if(allowedMessage) {
106 | modlog = allowedMessage.modlog;
107 | }
108 | }
109 | db.addLine(channel, user, data[TAGS]["user-id"], Date.now(), messagecompressor.compressMessage(user, data), modlog, function(id) {
110 | var emittedMsg = {id: id, time: time, nick: user, text: data[0]};
111 | io.to("logs-"+channel+"-"+user).emit("log-add", emittedMsg);
112 | emittedMsg.modlog = modlog;
113 | io.to("logs-"+channel+"-"+user+"-modlogs").emit("log-add", emittedMsg);
114 | });
115 | db.updateStats(channel, user, {messages: 1});
116 | }, 10);
117 | });
118 |
119 | self.bot.on("USERNOTICE", function(data){
120 | if(data[TAGS] && data[TAGS]["msg-id"]=="resub" || data[TAGS]["msg-id"]=="sub") {
121 | var time = Math.floor(Date.now()/1000);
122 | var channel = data[PARAM].slice(1);
123 | var text = data[TAGS]["system-msg"].replace(/\\s/g," ");
124 | if(data[TRAILING]) text += " Message: "+data[TRAILING];
125 | var sub = data[TAGS]["login"];
126 | db.addLine(channel, "twitchnotify", TWITCHNOTIFYID, Date.now(), "dtwitchnotify "+text, null, function(id) {
127 | var irccmd = `@display-name=twitchnotify;color=;subscriber=0;turbo=0;user-type=;emotes=;mod=0 :${sub}!${sub}@${sub}.tmi.twitch.tv PRIVMSG #${channel} :${text}`;
128 | io.to("logs-"+channel+"-twitchnotify").emit("log-add", {id: id, time: time, nick: "twitchnotify", text: irccmd});
129 | io.to("logs-"+channel+"-twitchnotify-modlogs").emit("log-add", {id: id, time: time, nick: "twitchnotify", text: irccmd});
130 | });
131 | db.updateStats(channel, "twitchnotify", {messages: 1});
132 | db.addLine(channel, sub, data[TAGS]["user-id"], Date.now(), "dtwitchnotify "+text, null, function(id) {
133 | var irccmd = `@display-name=twitchnotify;color=;subscriber=0;turbo=0;user-type=;emotes=;mod=0 :${sub}!${sub}@${sub}.tmi.twitch.tv PRIVMSG #${channel} :${text}`;
134 | io.to("logs-"+channel+"-"+sub).emit("log-add", {id: id, time: time, nick: sub, text: irccmd});
135 | io.to("logs-"+channel+"-"+sub+"-modlogs").emit("log-add", {id: id, time: time, nick: sub, text: irccmd});
136 | });
137 | }
138 | });
139 |
140 | // Everything having to do with timeouts/bans
141 | var ROTATECYCLE = 30000;
142 | var MAXDIFF = 5000;
143 |
144 | self.timeouts = {};
145 | self.oldtimeouts = {};
146 |
147 | function rotateTimeouts(){
148 | self.oldtimeouts = self.timeouts;
149 | self.timeouts = {};
150 | }
151 | setInterval(rotateTimeouts, ROTATECYCLE);
152 |
153 | var formatTimespan = function(timespan) {
154 | var age = Math.round(timespan);
155 | var periods = [
156 | {abbr:"y", len: 3600*24*365},
157 | {abbr:"m", len: 3600*24*30},
158 | {abbr:"d", len: 3600*24},
159 | {abbr:" hrs", len: 3600},
160 | {abbr:" min", len: 60},
161 | {abbr:" sec", len: 1},
162 | ];
163 | var res = "";
164 | var count = 0;
165 | for(var i=0;i= periods[i].len) {
167 | var pval = Math.floor(age / periods[i].len);
168 | age = age % periods[i].len;
169 | res += (res?" ":"")+pval+periods[i].abbr;
170 | count ++;
171 | if(count >= 2) break;
172 | }
173 | }
174 | return res;
175 | }
176 |
177 | function formatCount(i) {
178 | return i<=1?"":" ("+i+" times)";
179 | }
180 |
181 | function formatTimeout(user, timeout) {
182 | if(isFinite(timeout.duration)){
183 | // timeout
184 | if(timeout.reasons.length==0)
185 | return "<"+user+" has been timed out for "+formatTimespan(timeout.duration)+formatCount(timeout.count)+">"
186 | else if(timeout.reasons.length==1)
187 | return "<"+user+" has been timed out for "+formatTimespan(timeout.duration)+". Reason: "+timeout.reasons.join(", ")+formatCount(timeout.count)+">"
188 | else
189 | return "<"+user+" has been timed out for "+formatTimespan(timeout.duration)+". Reasons: "+timeout.reasons.join(", ")+formatCount(timeout.count)+">"
190 | } else {
191 | // banned
192 | if(timeout.reasons.length==0)
193 | return "<"+user+" has been banned>"
194 | else if(timeout.reasons.length==1)
195 | return "<"+user+" has been banned. Reason: "+timeout.reasons.join(", ")+">"
196 | else
197 | return "<"+user+" has been banned. Reasons: "+timeout.reasons.join(", ")+">"
198 | }
199 | }
200 |
201 | function emitTimeout(type, channelObj, user, timeout) {
202 | var irccmd = `@display-name=jtv;color=;subscriber=0;turbo=0;user-type=;emotes=;badges= :${user}!${user}@${user}.tmi.twitch.tv PRIVMSG #${channelObj.name} :${timeout.text}`
203 | var time = Math.floor(timeout.time.getTime()/1000);
204 | io.to("logs-"+channelObj.id+"-"+user).emit(type, {id: timeout.id, time: time, nick: user, text: irccmd});
205 | io.to("logs-"+channelObj.id+"-"+user+"-modlogs").emit(type, {id: timeout.id, time: time, nick: user, modlog: timeout.modlog, text: irccmd});
206 | }
207 |
208 | function doTimeout(channelObj, mod, modid, user, userid, duration, reason, inc) {
209 | // search for the user in the recent timeouts
210 | var oldtimeout = (self.timeouts[channelObj.id] && self.timeouts[channelObj.id][userid]) || (self.oldtimeouts[channelObj.id] && self.oldtimeouts[channelObj.id][user]);
211 | var now = new Date();
212 | if(self.timeouts[channelObj.id] === undefined) self.timeouts[channelObj.id] = {};
213 | duration = parseInt(duration) || Infinity;
214 |
215 | if(oldtimeout) {
216 | // if a reason is specified and its new, we add it
217 | if(reason && oldtimeout.reasons.indexOf(reason)<0) {
218 | oldtimeout.reasons.push(reason);
219 | }
220 |
221 | if(mod) oldtimeout.modlog[mod] = duration;
222 | if(isFinite(oldtimeout.duration) && !isFinite(duration)) {
223 | // a user that was timed out got banned now.
224 | db.updateStats(channelObj, user, {timeouts: -1, bans: 1});
225 | }
226 |
227 |
228 | var oldends = oldtimeout.time.getTime()+oldtimeout.duration*1000;
229 | var newends = now.getTime()+duration*1000;
230 | // only completely update significant changes in the end of the timeout
231 | if(Math.abs(oldends-newends) > MAXDIFF) {
232 | oldtimeout.time = now;
233 | oldtimeout.duration = duration;
234 | }
235 |
236 | oldtimeout.count += inc;
237 | oldtimeout.text = formatTimeout(user, oldtimeout);
238 | // put it into the primary rotation again
239 | self.timeouts[channelObj.id][user] = oldtimeout;
240 |
241 | // update the database
242 | if(oldtimeout.id) {
243 | db.updateTimeout(channelObj, user, oldtimeout.id, now.getTime(), "djtv "+oldtimeout.text, oldtimeout.modlog);
244 | // emit timeout via websockets
245 | emitTimeout("log-update", channelObj, user, oldtimeout);
246 | }
247 | else oldtimeout.dirty = true;
248 |
249 | } else {
250 | var modlog = {};
251 | if(mod) modlog[mod] = duration;
252 | var timeout = {time: now, duration: duration, reasons: reason?[reason]:[], count: inc, modlog: modlog};
253 |
254 | timeout.text = formatTimeout(user, timeout);
255 | // add the timeout to the cache with an empty id
256 | self.timeouts[channelObj.id][user] = timeout;
257 | db.addTimeout(channelObj, user, now.getTime(), "djtv "+timeout.text, modlog, function(id){
258 | timeout.id = id;
259 | // if the timeout was dirty, update it again...
260 | if(timeout.dirty) {
261 | db.updateTimeout(channelObj, user, id, timeout.time.getTime(), "djtv "+timeout.text, timeout.modlog);
262 | }
263 | // emit timeout via websockets
264 | emitTimeout("log-add", channelObj, user, timeout);
265 | });
266 | if(isFinite(duration)) db.updateStats(channelObj, user, {timeouts:1});
267 | else db.updateStats(channelObj, user, {bans:1});
268 | }
269 | }
270 |
271 | function doUnban(channelObj, mod, modid, type, user, userid) {
272 | var modlog = {};
273 | modlog[mod] = -1;
274 | var text = `<${user} has been ${type}>`;
275 | db.addLine(channelObj, user, userid, Date.now(), "djtv "+text, modlog, null, function(id){
276 | var irccmd = `@display-name=jtv;color=;subscriber=0;turbo=0;user-type=;emotes=;badges= :${user}!${user}@${user}.tmi.twitch.tv PRIVMSG #${channelObj.name} :${text}`;
277 | io.to("logs-"+channelObj.id+"-"+userid).emit("log-add", {id: id, time: Math.floor(Date.now()/1000), nick: user, text: irccmd});
278 | io.to("logs-"+channelObj.id+"-"+userid+"-modlogs").emit("log-add", {id: id, time: Math.floor(Date.now()/1000), nick: user, modlog: modlog, text: irccmd});
279 | });
280 | }
281 |
282 | function emitRejectedMessage(type, channelObj, command) {
283 | var user = command.args[0];
284 | var message = command.args[1];
285 | var irccmd = `@display-name=twitchbot;color=;subscriber=0;turbo=0;user-type=;emotes=;badges= :${user}!${user}@${user}.tmi.twitch.tv PRIVMSG #${channelObj.name} :${message}`
286 |
287 | var emittedMsg = {id: command.id, time: Math.floor(command.time/1000), nick: user, text: irccmd};
288 | io.to("logs-"+channelObj.id+"-"+user).emit(type, emittedMsg);
289 | emittedMsg.modlog = command.modlog;
290 | console.log("Emitting "+JSON.stringify(emittedMsg));
291 | console.log("to logs-"+channelObj.id+"-"+user+"-modlogs")
292 | io.to("logs-"+channelObj.id+"-"+user+"-modlogs").emit(type, emittedMsg);
293 | }
294 |
295 | var rejectedMessages = {};
296 | var oldRejectedMessages = {};
297 | var allowedMessages = {};
298 | var oldAllowedMessages = {};
299 | function doReject(channelObj, user, command) {
300 | command.time = Date.now();
301 | command.modlog = {};
302 | command.modlog[user] = "reject";
303 | db.addTimeout(channelObj, command.args[0], command.time, "dtwitchbot " + command.args[1], command.modlog, function(id){
304 | command.id = id;
305 | emitRejectedMessage("log-add", channelObj, command);
306 | if(command.dirty) {
307 | db.updateTimeout(channelObj, command.args[0], command.id, command.time, "dtwitchbot " + command.args[1] , command.modlog);
308 | }
309 | });
310 | rejectedMessages[command.msg_id] = command;
311 | }
312 |
313 | function doModerate(channelObj, user, command) {
314 | var action = command.moderation_action.split("_")[0];
315 | var oldModlog = rejectedMessages[command.msg_id] || oldRejectedMessages[command.msg_id];
316 | if(oldModlog) {
317 | oldModlog.modlog[user] = action;
318 | if(oldModlog.id) {
319 | db.updateTimeout(channelObj, oldModlog.args[0], oldModlog.id, oldModlog.time, "dtwitchbot " + oldModlog.args[1], oldModlog.modlog);
320 | emitRejectedMessage("log-update", channelObj, oldModlog);
321 | } else {
322 | oldModlog.dirty = true;
323 | }
324 | } else {
325 | command.time = Date.now();
326 | command.modlog = {};
327 | command.modlog[user] = action;
328 | allowedMessages[command.msg_id] = command;
329 | }
330 | }
331 |
332 | setInterval(function() {
333 | oldRejectedMessages = rejectedMessages;
334 | oldAllowedMessages = allowedMessages;
335 | rejectedMessages = {};
336 | allowedMessages = {};
337 | }, 300000);
338 |
339 |
340 | self.bot.on("CLEARCHAT", function(data){
341 | let user = data[TRAILING];
342 | let userid = data[TAGS]["target-user-id"];
343 | let channelObj = {name: data[PARAM].slice(1), id: data[TAGS]["room-id"];
344 | if(user && user.length > 0) {
345 | let duration,reason;
346 | if(data[TAGS]) {
347 | if(data[TAGS]["ban-duration"]) duration = data[TAGS]["ban-duration"];
348 | if(data[TAGS]["ban-reason"]) reason = data[TAGS]["ban-reason"].replace(/\\s/g," ");
349 | }
350 | doTimeout(channelObj, undefined, user, duration, reason, 1);
351 | } else {
352 | winston.debug("#" + channelObj.name + " ");
353 | db.addTimeout(channelObj, "jtv", Date.now(), "djtv ");
354 | }
355 | });
356 |
357 | var lastSave = Date.now();
358 | self.bot.on("NOTICE", function(data){
359 | //:tmi.twitch.tv NOTICE #ox33 :The moderators of this room are: 0x33, andyroid, anoetictv
360 | //@msg-id=msg_banned :tmi.twitch.tv NOTICE #frankerzbenni :You are permanently banned from talking in frankerzbenni.
361 | let channel = data[PARAM].slice(1);
362 | if(data[TAGS] && (data[TAGS]["msg-id"] === "room_mods" || data[TAGS]["msg-id"] === "no_mods")) {
363 | let users = [];
364 | if(data[TAGS]["msg-id"] === "room_mods") {
365 | let m = /The moderators of this \w+ are: (.*)/.exec(data[TRAILING]);
366 | users = m[1].match(/\w+/g);
367 | }
368 |
369 |
370 | // check if the moderation status of the bot has changed
371 | var ismodded = users.indexOf(self.nick) >= 0;
372 | if(self.userlevels[channel] && ismodded * 5 != (self.userlevels[channel][self.nick] || 0)) {
373 | // emit the moderation status changed event via ws
374 | self.io.to("events-"+channel).emit("ismodded", ismodded);
375 |
376 | if(!ismodded) {
377 | let channelObj = self.findChannelObj({name: channel});
378 | // disable mod log setting
379 | winston.info("Got unmodded in "+channel+" - "+JSON.stringify(channelObj)+" unlistening from mod logs");
380 | self.disableModLogs(channelObj);
381 | self.db.setSetting(channel, "modlogs", "0");
382 | self.API.adminLog(channel, "", "system", "modlogs-disabled", "Detected that the bot is no longer modded in your channel. Disabled mod logs.");
383 | }
384 | }
385 |
386 | let userlist = {};
387 | for(let i=0;i 60*1000) {
397 | // write to file every minute
398 | lastSave = Date.now();
399 | fs.writeFile("mods.json", JSON.stringify(self.userlevels), "utf-8");
400 | }
401 | } else if(data[TAGS] && data[TAGS]["msg-id"] === "msg_banned"){
402 | // we were banned from the channel, leave it.
403 | let channelObj = self.findChannelObj({name: channel});
404 | self.emit("moderator-list-"+channel, []); // emit an empty mod list in case we were waiting for those);
405 | db.setSetting(channel, "active", "0");
406 | self.partChannel(channelObj);
407 | if(self.API) {
408 | self.API.adminLog(channel, "", "system", "banned", "Detected that the bot is banned from the channel. Disabled the logviewer.");
409 | }
410 | } else {
411 | db.addLine(channel, "jtv", 0, Date.now(), "djtv "+data[TRAILING], null);
412 | }
413 | });
414 | var regexes_channel_user =
415 | [
416 | /^#(\w+)\s+(\w+)$/,
417 | /^(\w+)\s+(\w+)$/,
418 | /^logof (\w+)\s+(\w+)$/,
419 | /^!logs? (\w+)\s+(\w+)$/,
420 | ];
421 | var regexes_user_channel =
422 | [
423 | /^(\w+)\s+#(\w+)$/,
424 | /^(\w+)\s+(\w+)/,
425 | /^(\w+) in (\w+)$/,
426 | /^logof (\w+)\s+(\w+)$/,
427 | /^!logs? (\w+)\s+(\w+)$/,
428 | ];
429 |
430 | var getLogs = function(channel, nick, requestedby, callback) {
431 | db.getActiveChannel(channel, function(channelObj) {
432 | if(!channelObj)
433 | {
434 | callback(undefined, nick);
435 | }
436 | if(channelObj.viewlogs > 0) {
437 | db.getUserLevel(channelObj.name, requestedby, function(level){
438 | if(level >= channelObj.viewlogs) {
439 | db.getLogsByNick(channelObj.name, nick, 2, false, function(messages){
440 | for(var i=0;i 0) {
471 | self.checkMods(self.channels[currentchannel%(self.channels.length)]);
472 | currentchannel++;
473 | }
474 | }
475 | setInterval(checkNextMods,(settings.bot.modcheckinterval || 2) * 1000);
476 |
477 | self.bot.connect();
478 |
479 | // react to mod logs, if present
480 | self.pubsub.on("MESSAGE", function(message, flags) {
481 | winston.debug("Handling pubsub message "+JSON.stringify(message));
482 | let topic = message.data.topic.split(".");
483 | if(topic[0] == "chat_moderator_actions") {
484 | var channelid = topic[2];
485 | var channelObj = self.findChannelObj({id: channelid});
486 | var channel = channelObj.name;
487 | var command = JSON.parse(message.data.message).data;
488 | winston.debug(command);
489 | var user = command.created_by;
490 | if(command.moderation_action == "timeout") {
491 | doTimeout(channel, user, command.args[0].toLowerCase(), command.args[1] || 600, command.args[2] || "", 0);
492 | } else if(command.moderation_action == "ban") {
493 | doTimeout(channel, user, command.args[0].toLowerCase(), Infinity, command.args[1] || "", 0);
494 | } else if(command.moderation_action == "unban") {
495 | doUnban(channel, user, "unbanned", command.args[0].toLowerCase());
496 | } else if(command.moderation_action == "untimeout") {
497 | doUnban(channel, user, "untimed out", command.args[0].toLowerCase());
498 | } else if(command.moderation_action == "automod_rejected") {
499 | doReject(channel, user, command);
500 | } else if(command.moderation_action == "denied_automod_message" || command.moderation_action == "allowed_automod_message") {
501 | doModerate(channel, user, command);
502 | } else {
503 | var text = "/"+command.moderation_action;
504 | if(command.args) text += " "+command.args.join(" ");
505 | var modlog = {};
506 | modlog[user] = "";
507 | db.addLine(channel, "jtv", 0, Date.now(), "djtv "+text, modlog, null, function(id) {
508 | var time = Math.floor(Date.now()/1000);
509 | io.to("logs-"+channel+"-"+user).emit("log-add", {id: id, time: time, nick: "jtv", text: `@display-name=jtv;color=;subscriber=0;turbo=0;user-type=;emotes=;badges= :jtv!jtv@jtv.tmi.twitch.tv PRIVMSG #${channel} :${text}`});
510 | io.to("logs-"+channel+"-"+user+"-modlogs").emit("log-add", {id: id, time: time, nick: user, modlog: modlog, text: `@display-name=jtv;color=;subscriber=0;turbo=0;user-type=;emotes=;badges= :${user}!${user}@${user}.tmi.twitch.tv PRIVMSG #${channel} :${text}`});
511 | });
512 | self.API.adminLog(channel, user, "command", command.moderation_action, text);
513 | }
514 | }
515 | });
516 | }
517 |
518 | logviewerBot.prototype = new events.EventEmitter;
519 |
520 | logviewerBot.prototype.findChannelObj = function(channel) {
521 | var self = this;
522 | var channelObj = self.id2channelObj[channel.id];
523 | if(channelObj) return channelObj;
524 | channelObj = self.name2channelObj[channel.name];
525 | if(channelObj) return channelObj;
526 | if(self.channels.indexOf(channel) >= 0) return channel;
527 | else return null;
528 | }
529 |
530 | logviewerBot.prototype.joinChannel = function(channelObj) {
531 | winston.info("Joining channel "+JSON.stringify(channelObj));
532 | var self = this;
533 | if(self.findChannelObj(channelObj)) return;
534 | self.channels.push(channelObj);
535 | self.bot.send("JOIN #"+channelObj.name);
536 | self.bot.send("PRIVMSG #"+channelObj.name+" :.mods");
537 | self.db.ensureTablesExist(channelObj.name);
538 | self.id2channelObj[channelObj.id] = channelObj;
539 | self.name2channelObj[channelObj.name] = channelObj;
540 | }
541 |
542 | logviewerBot.prototype.partChannel = function(channelObj) {
543 | var self = this;
544 | channelObj = self.findChannelObj(channelObj);
545 | let index = self.channels.indexOf(channelObj);
546 | winston.info("Leaving channel "+JSON.stringify(channelObj));
547 | if(index >= 0) {
548 | self.channels.splice(index,1)[0];
549 | self.bot.send("PART #"+channelObj.name);
550 | delete self.id2channelObj[channelObj.id];
551 | delete self.name2channelObj[channelObj.name];
552 | } else {
553 | self.bot.send("PART #"+channelObj.name);
554 | winston.error("Tried to leave channel "+channelObj.name+" that wasnt joined");
555 | }
556 | }
557 |
558 | logviewerBot.prototype.checkMods = function(channelObj) {
559 | var self = this;
560 | self.bot.send("PRIVMSG #"+channelObj.name+" :/mods");
561 | }
562 |
563 | // checks if the logviewer bot is modded in a channel
564 | logviewerBot.prototype.isModded = function(channelObj, callback, force, cacheonly) {
565 | if(!channelObj) {
566 | callback(false);
567 | return;
568 | }
569 | var self = this;
570 | var channel = channelObj.name;
571 | if(self.userlevels[channel] && !force) {
572 | winston.debug("Used cached mod list for channel "+channel+": "+JSON.stringify(self.userlevels[channel]));
573 | callback(self.userlevels[channel][self.nick] == 5);
574 | } else if(!cacheonly) {
575 | winston.debug("Waiting for mod list for channel "+channel);
576 | if(force) self.checkMods(channelObj);
577 | self.once("moderator-list-"+channel, function(list){
578 | if(list.indexOf(self.nick) >= 0) {
579 | callback(true);
580 | } else {
581 | callback(false);
582 | }
583 | });
584 | self.checkMods(channelObj);
585 | } else {
586 | callback(false);
587 | }
588 |
589 | }
590 |
591 | logviewerBot.prototype.enableModLogs = function(channelObj, callback) {
592 | var self = this;
593 |
594 | self.isModded(channelObj, function(isModded) {
595 | if(isModded) {
596 | // we gucci, subscribe to pubsub
597 | winston.debug("Enabling mod logs for "+JSON.stringify(channelObj));
598 | self.pubsub.listenModLogs(channelObj);
599 | channelObj.modlogs = "1";
600 | if(callback) callback(true);
601 | } else {
602 | winston.debug("Bot is not modded in "+channelObj.name);
603 | if(callback) callback(false);
604 | }
605 | });
606 | }
607 |
608 | logviewerBot.prototype.disableModLogs = function(channelObj) {
609 | var self = this;
610 | //channelObj = self.findChannelObj(channelObj);
611 | self.pubsub.unlistenModLogs(channelObj);
612 | channelObj.modlogs = "0";
613 | }
614 |
615 | module.exports = logviewerBot;
616 |
--------------------------------------------------------------------------------